diff --git a/hindsight-api/pyproject.toml b/hindsight-api/pyproject.toml index 03aa6272..5c35d652 100644 --- a/hindsight-api/pyproject.toml +++ b/hindsight-api/pyproject.toml @@ -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" diff --git a/hindsight-api/tests/test_http_api_integration.py b/hindsight-api/tests/test_http_api_integration.py index 5c3b7b15..5781b15d 100644 --- a/hindsight-api/tests/test_http_api_integration.py +++ b/hindsight-api/tests/test_http_api_integration.py @@ -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. diff --git a/hindsight-control-plane/src/app/api/banks/[bankId]/route.ts b/hindsight-control-plane/src/app/api/banks/[bankId]/route.ts new file mode 100644 index 00000000..a540bb8b --- /dev/null +++ b/hindsight-control-plane/src/app/api/banks/[bankId]/route.ts @@ -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 }); + } +} diff --git a/hindsight-control-plane/src/components/bank-profile-view.tsx b/hindsight-control-plane/src/components/bank-profile-view.tsx index 3aec2680..8f3e54e6 100644 --- a/hindsight-control-plane/src/components/bank-profile-view.tsx +++ b/hindsight-control-plane/src/components/bank-profile-view.tsx @@ -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(null); const [stats, setStats] = useState(null); const [operations, setOperations] = useState([]); @@ -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({ @@ -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() { + )} @@ -547,6 +586,53 @@ export function BankProfileView() { )} + + {/* Delete Confirmation Dialog */} + + + + Delete Memory Bank + +
+

+ Are you sure you want to delete the memory bank{" "} + {currentBank}? +

+

+ This action cannot be undone. All memories, entities, documents, and the bank + profile will be permanently deleted. +

+ {stats && ( +

+ This will delete {stats.total_nodes} memories, {stats.total_documents}{" "} + documents, and {stats.total_links} links. +

+ )} +
+
+
+ + Cancel + + {isDeleting ? ( + <> + + Deleting... + + ) : ( + <> + + Delete Bank + + )} + + +
+
); } diff --git a/hindsight-control-plane/src/lib/api.ts b/hindsight-control-plane/src/lib/api.ts index f71f6c44..f26d680f 100644 --- a/hindsight-control-plane/src/lib/api.ts +++ b/hindsight-control-plane/src/lib/api.ts @@ -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 */