Spaces:
Running
Running
feat: Implement Neo4j knowledge graph clearing functionality with a new API endpoint and frontend UI.
Browse files
frontend/src/components/SettingsPanel.tsx
CHANGED
|
@@ -68,6 +68,8 @@ export function SettingsPanel({ isOpen: controlledIsOpen, onToggle, activeSectio
|
|
| 68 |
const [showWithdrawConfirm, setShowWithdrawConfirm] = useState(false);
|
| 69 |
const [isClearingHistory, setIsClearingHistory] = useState(false);
|
| 70 |
const [clearHistoryResult, setClearHistoryResult] = useState<{success: boolean; message: string} | null>(null);
|
|
|
|
|
|
|
| 71 |
// Wakeword state
|
| 72 |
const [wakewordEnabled, setWakewordEnabled] = useState(true);
|
| 73 |
const [wakewordThreshold, setWakewordThreshold] = useState(0.5);
|
|
@@ -558,6 +560,49 @@ export function SettingsPanel({ isOpen: controlledIsOpen, onToggle, activeSectio
|
|
| 558 |
</button>
|
| 559 |
</div>
|
| 560 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
{/* Clear Conversation History Section */}
|
| 562 |
<div className="p-6 rounded-2xl bg-surface-subtle/40 border border-surface-overlay">
|
| 563 |
<div className="flex items-start gap-4 mb-6">
|
|
|
|
| 68 |
const [showWithdrawConfirm, setShowWithdrawConfirm] = useState(false);
|
| 69 |
const [isClearingHistory, setIsClearingHistory] = useState(false);
|
| 70 |
const [clearHistoryResult, setClearHistoryResult] = useState<{success: boolean; message: string} | null>(null);
|
| 71 |
+
const [isClearingNeo4j, setIsClearingNeo4j] = useState(false);
|
| 72 |
+
const [neo4jResult, setNeo4jResult] = useState<{status: string; message: string} | null>(null);
|
| 73 |
// Wakeword state
|
| 74 |
const [wakewordEnabled, setWakewordEnabled] = useState(true);
|
| 75 |
const [wakewordThreshold, setWakewordThreshold] = useState(0.5);
|
|
|
|
| 560 |
</button>
|
| 561 |
</div>
|
| 562 |
|
| 563 |
+
{/* Clear Neo4j Knowledge Graph */}
|
| 564 |
+
<div className="p-6 rounded-2xl bg-purple-500/5 border border-purple-500/20">
|
| 565 |
+
<div className="flex items-start gap-4 mb-6">
|
| 566 |
+
<div className="w-10 h-10 rounded-xl bg-purple-500/10 flex items-center justify-center text-purple-400">
|
| 567 |
+
<AlertTriangle className="w-5 h-5" />
|
| 568 |
+
</div>
|
| 569 |
+
<div>
|
| 570 |
+
<p className="text-xs font-bold text-purple-400 uppercase tracking-wide">Clear Knowledge Graph</p>
|
| 571 |
+
<p className="text-xs text-secondary mt-1">Deletes all Neo4j graph data including people, medications, symptoms, events, and their relationships.</p>
|
| 572 |
+
</div>
|
| 573 |
+
</div>
|
| 574 |
+
|
| 575 |
+
{neo4jResult && (
|
| 576 |
+
<div className={`mb-4 p-3 rounded-lg text-xs ${neo4jResult.status === 'cleared' ? 'bg-success/10 text-success' : neo4jResult.status === 'error' ? 'bg-accent-pink/10 text-accent-pink' : 'bg-surface-overlay text-secondary'}`}>
|
| 577 |
+
{neo4jResult.message}
|
| 578 |
+
</div>
|
| 579 |
+
)}
|
| 580 |
+
|
| 581 |
+
<button
|
| 582 |
+
onClick={async () => {
|
| 583 |
+
setIsClearingNeo4j(true);
|
| 584 |
+
setNeo4jResult(null);
|
| 585 |
+
try {
|
| 586 |
+
const res = await fetch(`${API_BASE}/api/stream/clear-neo4j`, { method: 'POST' });
|
| 587 |
+
const data = await res.json();
|
| 588 |
+
setNeo4jResult(data);
|
| 589 |
+
} catch (err) {
|
| 590 |
+
setNeo4jResult({ status: 'error', message: String(err) });
|
| 591 |
+
} finally {
|
| 592 |
+
setIsClearingNeo4j(false);
|
| 593 |
+
}
|
| 594 |
+
}}
|
| 595 |
+
disabled={isClearingNeo4j}
|
| 596 |
+
className="btn bg-purple-500/20 border border-purple-500/30 text-purple-400 hover:bg-purple-500/30 px-6 text-xs font-bold rounded-xl transition-all active:scale-95 disabled:opacity-50"
|
| 597 |
+
>
|
| 598 |
+
{isClearingNeo4j ? (
|
| 599 |
+
<><Loader2 className="w-3 h-3 animate-spin mr-2" /> Clearing...</>
|
| 600 |
+
) : (
|
| 601 |
+
<><RotateCcw className="w-3 h-3 mr-2" /> Clear Graph</>
|
| 602 |
+
)}
|
| 603 |
+
</button>
|
| 604 |
+
</div>
|
| 605 |
+
|
| 606 |
{/* Clear Conversation History Section */}
|
| 607 |
<div className="p-6 rounded-2xl bg-surface-subtle/40 border border-surface-overlay">
|
| 608 |
<div className="flex items-start gap-4 mb-6">
|
src/reachy_mini_conversation_app/memory_graph.py
CHANGED
|
@@ -82,6 +82,32 @@ class GraphMemory:
|
|
| 82 |
self._driver.close()
|
| 83 |
self._driver = None
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
@property
|
| 86 |
def is_connected(self) -> bool:
|
| 87 |
"""Check if the driver is connected."""
|
|
|
|
| 82 |
self._driver.close()
|
| 83 |
self._driver = None
|
| 84 |
|
| 85 |
+
def clear_all(self) -> Dict[str, int]:
|
| 86 |
+
"""Delete all nodes and relationships from the graph.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Dict with counts of deleted nodes and relationships.
|
| 90 |
+
"""
|
| 91 |
+
if not self._driver:
|
| 92 |
+
logger.warning("Neo4j not connected. Clear skipped.")
|
| 93 |
+
return {"nodes_deleted": 0, "relationships_deleted": 0}
|
| 94 |
+
|
| 95 |
+
# Count before deleting
|
| 96 |
+
count_result = self._execute(
|
| 97 |
+
"MATCH (n) OPTIONAL MATCH (n)-[r]-() "
|
| 98 |
+
"RETURN count(DISTINCT n) AS nodes, count(DISTINCT r) AS rels"
|
| 99 |
+
)
|
| 100 |
+
nodes = count_result[0]["nodes"] if count_result else 0
|
| 101 |
+
rels = count_result[0]["rels"] if count_result else 0
|
| 102 |
+
|
| 103 |
+
# Delete everything
|
| 104 |
+
self._execute("MATCH (n) DETACH DELETE n")
|
| 105 |
+
logger.warning(
|
| 106 |
+
"DEV: Cleared Neo4j graph — %d nodes, %d relationships deleted", nodes, rels
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
return {"nodes_deleted": nodes, "relationships_deleted": rels}
|
| 110 |
+
|
| 111 |
@property
|
| 112 |
def is_connected(self) -> bool:
|
| 113 |
"""Check if the driver is connected."""
|
src/reachy_mini_conversation_app/stream_api.py
CHANGED
|
@@ -955,6 +955,53 @@ async def reset_database() -> dict[str, Any]:
|
|
| 955 |
}
|
| 956 |
|
| 957 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 958 |
@router.get("/reports")
|
| 959 |
async def list_reports(limit: int = 20) -> list[dict[str, Any]]:
|
| 960 |
"""Return saved reports, newest first."""
|
|
|
|
| 955 |
}
|
| 956 |
|
| 957 |
|
| 958 |
+
@router.post("/clear-neo4j")
|
| 959 |
+
async def clear_neo4j_graph() -> dict[str, Any]:
|
| 960 |
+
"""Clear all nodes and relationships from the Neo4j knowledge graph.
|
| 961 |
+
|
| 962 |
+
WARNING: This deletes all graph data including people, medications,
|
| 963 |
+
symptoms, events, and their relationships. Only use during development.
|
| 964 |
+
"""
|
| 965 |
+
try:
|
| 966 |
+
from reachy_mini_conversation_app.session_enrichment import (
|
| 967 |
+
get_session_enrichment,
|
| 968 |
+
)
|
| 969 |
+
|
| 970 |
+
enrichment = get_session_enrichment()
|
| 971 |
+
if enrichment and hasattr(enrichment, "_graph") and enrichment._graph:
|
| 972 |
+
graph = enrichment._graph
|
| 973 |
+
if graph.is_connected:
|
| 974 |
+
result = graph.clear_all()
|
| 975 |
+
return {
|
| 976 |
+
"status": "cleared",
|
| 977 |
+
"message": f"Neo4j graph cleared: {result['nodes_deleted']} nodes and {result['relationships_deleted']} relationships deleted.",
|
| 978 |
+
**result,
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
# Try direct connection if enrichment doesn't have a graph
|
| 982 |
+
from reachy_mini_conversation_app.memory_graph import GraphMemory
|
| 983 |
+
|
| 984 |
+
graph = GraphMemory()
|
| 985 |
+
if graph.connect():
|
| 986 |
+
try:
|
| 987 |
+
result = graph.clear_all()
|
| 988 |
+
return {
|
| 989 |
+
"status": "cleared",
|
| 990 |
+
"message": f"Neo4j graph cleared: {result['nodes_deleted']} nodes and {result['relationships_deleted']} relationships deleted.",
|
| 991 |
+
**result,
|
| 992 |
+
}
|
| 993 |
+
finally:
|
| 994 |
+
graph.close()
|
| 995 |
+
|
| 996 |
+
return {
|
| 997 |
+
"status": "unavailable",
|
| 998 |
+
"message": "Neo4j is not connected. Nothing to clear.",
|
| 999 |
+
}
|
| 1000 |
+
except Exception as e:
|
| 1001 |
+
logger.error("Failed to clear Neo4j graph: %s", e)
|
| 1002 |
+
return {"status": "error", "message": str(e)}
|
| 1003 |
+
|
| 1004 |
+
|
| 1005 |
@router.get("/reports")
|
| 1006 |
async def list_reports(limit: int = 20) -> list[dict[str, Any]]:
|
| 1007 |
"""Return saved reports, newest first."""
|