Boopster commited on
Commit
7767771
·
1 Parent(s): 5eec55e

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."""