Compare commits

...
5 changed files with 217 additions and 3 deletions
+3
View File
@@ -123,6 +123,9 @@ ignore = [
"F821", # undefined name (forward references in type hints)
]
[tool.ruff.lint.isort]
known-third-party = ["alembic"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
@@ -288,8 +288,9 @@ async def test_full_api_workflow(api_client, test_bank_id):
# 10. Clean Up
# ================================================================
# Note: No delete bank endpoint in API, so test data remains in DB
# Using timestamped bank IDs prevents conflicts between test runs
# Clean up the test bank (delete bank endpoint is tested separately)
response = await api_client.delete(f"/v1/default/banks/{test_bank_id}")
assert response.status_code == 200
@pytest.mark.asyncio
@@ -488,6 +489,87 @@ async def test_document_deletion_with_slashes_in_id(api_client):
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
@pytest.mark.asyncio
async def test_delete_bank(api_client):
"""Test delete bank endpoint.
Workflow:
1. Create a bank by storing memories
2. Verify bank exists with data
3. Delete the bank
4. Verify bank and all data is deleted
"""
test_bank_id = f"delete_bank_test_{datetime.now().timestamp()}"
# 1. Create bank by storing memories with a document
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
"content": "Alice is a software engineer at TechCorp.",
"context": "team info",
"document_id": "team-doc-1",
},
{
"content": "Bob is the CTO and leads the engineering team.",
"context": "team info",
"document_id": "team-doc-1",
},
]
},
)
assert response.status_code == 200
assert response.json()["success"] is True
# 2. Verify bank exists with data
# Check profile
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
assert response.status_code == 200
# Check stats show data exists
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/stats")
assert response.status_code == 200
stats = response.json()
assert stats["total_nodes"] > 0
# Check documents exist
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/documents")
assert response.status_code == 200
assert len(response.json()["items"]) > 0
# Check bank is in list
response = await api_client.get("/v1/default/banks")
assert response.status_code == 200
bank_ids = [b["bank_id"] for b in response.json()["banks"]]
assert test_bank_id in bank_ids
# 3. Delete the bank
response = await api_client.delete(f"/v1/default/banks/{test_bank_id}")
assert response.status_code == 200
delete_result = response.json()
assert delete_result["success"] is True
assert delete_result["deleted_count"] > 0
assert "deleted successfully" in delete_result["message"]
# 4. Verify bank and all data is deleted
# Bank should not be in list
response = await api_client.get("/v1/default/banks")
assert response.status_code == 200
bank_ids = [b["bank_id"] for b in response.json()["banks"]]
assert test_bank_id not in bank_ids
# Stats should show zero data (profile auto-creates empty bank)
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/stats")
assert response.status_code == 200
stats = response.json()
assert stats["total_nodes"] == 0
assert stats["total_documents"] == 0
# Clean up the auto-created empty bank
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
@pytest.mark.asyncio
async def test_async_retain(api_client):
"""Test asynchronous retain functionality.
@@ -0,0 +1,30 @@
import { NextResponse } from "next/server";
import { sdk, lowLevelClient } from "@/lib/hindsight-client";
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ bankId: string }> }
) {
try {
const { bankId } = await params;
if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
const response = await sdk.deleteBank({
client: lowLevelClient,
path: { bank_id: bankId },
});
if (response.error) {
console.error("API error deleting bank:", response.error);
return NextResponse.json({ error: "Failed to delete bank" }, { status: 500 });
}
return NextResponse.json(response.data, { status: 200 });
} catch (error) {
console.error("Error deleting bank:", error);
return NextResponse.json({ error: "Failed to delete bank" }, { status: 500 });
}
}
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { client } from "@/lib/api";
import { useBank } from "@/lib/bank-context";
import { Button } from "@/components/ui/button";
@@ -14,6 +15,16 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
RefreshCw,
Save,
@@ -26,6 +37,7 @@ import {
Link2,
FolderOpen,
Activity,
Trash2,
} from "lucide-react";
interface DispositionTraits {
@@ -158,7 +170,8 @@ function DispositionEditor({
}
export function BankProfileView() {
const { currentBank } = useBank();
const router = useRouter();
const { currentBank, setCurrentBank, loadBanks } = useBank();
const [profile, setProfile] = useState<BankProfile | null>(null);
const [stats, setStats] = useState<BankStats | null>(null);
const [operations, setOperations] = useState<Operation[]>([]);
@@ -166,6 +179,10 @@ export function BankProfileView() {
const [saving, setSaving] = useState(false);
const [editMode, setEditMode] = useState(false);
// Delete state
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// Edit state
const [editBackground, setEditBackground] = useState("");
const [editDisposition, setEditDisposition] = useState<DispositionTraits>({
@@ -226,6 +243,24 @@ export function BankProfileView() {
setEditMode(false);
};
const handleDeleteBank = async () => {
if (!currentBank) return;
setIsDeleting(true);
try {
await client.deleteBank(currentBank);
setShowDeleteDialog(false);
setCurrentBank(null);
await loadBanks();
router.push("/");
} catch (error) {
console.error("Error deleting bank:", error);
alert("Error deleting bank: " + (error as Error).message);
} finally {
setIsDeleting(false);
}
};
useEffect(() => {
if (currentBank) {
loadData();
@@ -296,6 +331,10 @@ export function BankProfileView() {
<Button onClick={() => setEditMode(true)} size="sm">
Edit Profile
</Button>
<Button onClick={() => setShowDeleteDialog(true)} variant="destructive" size="sm">
<Trash2 className="w-4 h-4 mr-2" />
Delete Bank
</Button>
</>
)}
</div>
@@ -547,6 +586,53 @@ export function BankProfileView() {
)}
</CardContent>
</Card>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Memory Bank</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2 text-sm text-muted-foreground">
<p>
Are you sure you want to delete the memory bank{" "}
<span className="font-semibold text-foreground">{currentBank}</span>?
</p>
<p className="text-red-600 dark:text-red-400 font-medium">
This action cannot be undone. All memories, entities, documents, and the bank
profile will be permanently deleted.
</p>
{stats && (
<p>
This will delete {stats.total_nodes} memories, {stats.total_documents}{" "}
documents, and {stats.total_links} links.
</p>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteBank}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="w-4 h-4 mr-2" />
Delete Bank
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
+13
View File
@@ -183,6 +183,19 @@ export class ControlPlaneClient {
});
}
/**
* Delete an entire memory bank and all its data
*/
async deleteBank(bankId: string) {
return this.fetchApi<{
success: boolean;
message: string;
deleted_count: number;
}>(`/api/banks/${bankId}`, {
method: "DELETE",
});
}
/**
* Get chunk
*/