diff --git a/backend/app/routers/openclaw.py b/backend/app/routers/openclaw.py index 84ec1a4..645630d 100644 --- a/backend/app/routers/openclaw.py +++ b/backend/app/routers/openclaw.py @@ -12,6 +12,7 @@ import os import re import shutil import socket +import sqlite3 from datetime import datetime, timezone from pathlib import Path from typing import Any, Optional @@ -473,6 +474,77 @@ async def list_openclaw_skills(): return {"skills": skills, "count": len(skills)} +# ═════════════════════════════════════════════════════════════════════════════════ +# 4.a Agent Memory (QMD) +# ═════════════════════════════════════════════════════════════════════════════════ + +@router.get("/agents/{agent_name}/memory") +async def get_agent_memory(agent_name: str, limit: int = 100): + """Attempt to read the agent's memory (QMD) SQLite database.""" + ws_dir = _workspace() / agent_name + if not ws_dir.is_dir(): + return {"has_db": False, "error": "Workspace not found"} + + db_files = list(ws_dir.rglob("*.sqlite")) + list(ws_dir.rglob("*.db")) + list(ws_dir.rglob("*.qmd")) + if not db_files: + return {"has_db": False, "message": "No SQLite memory database found"} + + db_path = db_files[0] + result = { + "has_db": True, + "db_path": str(db_path.relative_to(_home())), + "size": db_path.stat().st_size, + "timeline": [], + "stats": {"total_thoughts": 0} + } + + def _read_db(): + timeline = [] + try: + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # Check if common thought tables exist + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = [row["name"] for row in cursor.fetchall()] + + result["tables"] = tables + + # Very heuristic approach to grab 'thoughts' or 'history' + target_table = None + for t in ["thoughts", "messages", "history", "runs", "memory", "qmd_memory", "events"]: + if t in tables: + target_table = t + break + + if target_table: + # Get column names + cursor.execute(f"PRAGMA table_info('{target_table}')") + columns = [c[1] for c in cursor.fetchall()] + + query = f"SELECT * FROM {target_table} ORDER BY rowid DESC LIMIT ?" + if "timestamp" in columns or "created_at" in columns: + order_col = "timestamp" if "timestamp" in columns else "created_at" + query = f"SELECT * FROM {target_table} ORDER BY {order_col} DESC LIMIT ?" + + cursor.execute(query, (limit,)) + rows = cursor.fetchall() + timeline = [dict(r) for r in rows] + + # Count total + cursor.execute(f"SELECT COUNT(*) FROM {target_table}") + result["stats"]["total_thoughts"] = cursor.fetchone()[0] + + conn.close() + return timeline + except Exception as e: + log.error(f"Error reading QMD db: {e}") + return [] + + result["timeline"] = await asyncio.to_thread(_read_db) + return result + # ═════════════════════════════════════════════════════════════════════════════════ # 5. Logs # ═════════════════════════════════════════════════════════════════════════════════ diff --git a/backend/test_qmd.py b/backend/test_qmd.py new file mode 100644 index 0000000..15f4ac0 --- /dev/null +++ b/backend/test_qmd.py @@ -0,0 +1,47 @@ +import os +from pathlib import Path +import sqlite3 + +def explore_qmd(): + foxy_home_str = os.environ.get("FOXY_HOME", "/home/foxy/.openclaw") + # If running on windows local, this might resolve weirdly. + # We will try the raw string first. + home = Path(foxy_home_str) + + # Alternatively try C:\home\foxy\.openclaw if windows + if not home.exists() and os.name == 'nt': + alt_home = Path("C:") / foxy_home_str + if alt_home.exists(): + home = alt_home + + workspace = home / "workspace" + print("Exploring workspace:", workspace) + print("Exists:", workspace.exists()) + + if not workspace.exists(): + print("Cannot find workspace.") + return + + sqlite_files = list(workspace.rglob("*.sqlite")) + list(workspace.rglob("*.db")) + list(workspace.rglob("*.qmd")) + print("Found sqlite files:", len(sqlite_files)) + + for f in sqlite_files: + print("\n--- SCHEMA FOR", f) + try: + conn = sqlite3.connect(f) + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + for t in tables: + table_name = t[0] + print(f"Table: {table_name}") + cursor.execute(f"PRAGMA table_info('{table_name}')") + columns = cursor.fetchall() + for c in columns: + print(f" - {c[1]} ({c[2]})") + conn.close() + except Exception as e: + print("Error reading db:", e) + +if __name__ == "__main__": + explore_qmd() diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index ece8e4e..4c25e48 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -246,6 +246,17 @@ export interface AgentFileResult { language: string; } +export interface AgentMemoryResult { + has_db: boolean; + db_path?: string; + size?: number; + tables?: string[]; + timeline: Record[]; + stats: { total_thoughts: number }; + message?: string; + error?: string; +} + // ─── HTTP Helpers ─────────────────────────────────────────────────────────── async function request(path: string, options?: RequestInit): Promise { @@ -339,6 +350,8 @@ export const api = { method: 'PUT', body: JSON.stringify({ content }), }), + openclawAgentMemory: (name: string, limit: number = 100) => + request(`/api/openclaw/agents/${encodeURIComponent(name)}/memory?limit=${limit}`), openclawSkills: () => request<{ skills: OpenClawSkill[]; count: number }>('/api/openclaw/skills'), openclawLogs: (params: { lines?: number; level?: string }) => { const qs = new URLSearchParams(); diff --git a/frontend/src/pages/OpenClaw.tsx b/frontend/src/pages/OpenClaw.tsx index 2288147..13303a2 100644 --- a/frontend/src/pages/OpenClaw.tsx +++ b/frontend/src/pages/OpenClaw.tsx @@ -6,7 +6,7 @@ import { yaml } from '@codemirror/lang-yaml'; import { oneDark } from '@codemirror/theme-one-dark'; import type { OpenClawStatus, OpenClawAgent, OpenClawSkill, OpenClawLogs, - OpenClawModels, FileTreeNode, ReadFileResult + OpenClawModels, FileTreeNode, ReadFileResult, AgentMemoryResult } from '../api/client'; import { api } from '../api/client'; @@ -336,13 +336,107 @@ function renderValue(v: unknown): string { // ═════════════════════════════════════════════════════════════════════════════════ -// Tab 3 — Agents +// Tab 3 — Agents (with Memory Viewer) // ═════════════════════════════════════════════════════════════════════════════════ +function AgentMemoryViewer({ agentName }: { agentName: string }) { + const [memory, setMemory] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let active = true; + api.openclawAgentMemory(agentName) + .then(res => { + if (active) { + setMemory(res); + setLoading(false); + } + }) + .catch(err => { + console.error(err); + if (active) setLoading(false); + }); + return () => { active = false; }; + }, [agentName]); + + if (loading) { + return ( +
+ +

Connexion à la mémoire QMD de l'agent...

+
+ ); + } + + if (!memory || !memory.has_db) { + return ( +
+ 🗄️ +

Mémoire Introuvable

+

+ Aucune base de données SQLite QMD n'a été trouvée pour cet agent dans le workspace. + Lumina ne dispose pas (encore) d'une trame chronologique ici. +

+

Astuce : Vérifiez la configuration du dossier workspace sous FOXY_HOME.

+
+ ); + } + + return ( +
+
+
+

+ 🧠 Labyrinth Memoire QMD +

+

{memory.db_path} ({formatSize(memory.size || 0)})

+
+
+
{memory.stats.total_thoughts}
+
Pensées / Actions
+
+
+ +
+ {memory.timeline.length === 0 ? ( +
La mémoire existe mais elle est actuellement vide.
+ ) : ( +
+ {memory.timeline.map((item: Record, idx: number) => ( +
+ {/* Node dot */} +
+ + {/* Content Card */} +
+
+ + {item.timestamp || item.created_at || 'Inconnu'} + + {(item.type || item.event_type || item.role) && ( + + {item.type || item.event_type || item.role} + + )} +
+
+                    {item.content || item.message || JSON.stringify(item, null, 2)}
+                  
+
+
+ ))} +
+ )} +
+
+ ); +} + function AgentsTab() { const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); const [expandedAgent, setExpandedAgent] = useState(null); + const [activeView, setActiveView] = useState<'identity' | 'memory'>('identity'); const [deleting, setDeleting] = useState(null); const [activeFile, setActiveFile] = useState<{agent: string, file: string} | null>(null); @@ -437,7 +531,12 @@ function AgentsTab() { >
{ setExpandedAgent(expandedAgent === agent.name ? null : agent.name); setActiveFile(null); setIsFullscreen(false); }} + onClick={() => { + setExpandedAgent(expandedAgent === agent.name ? null : agent.name); + setActiveFile(null); + setIsFullscreen(false); + setActiveView('identity'); + }} >
@@ -476,13 +575,33 @@ function AgentsTab() { {expandedAgent === agent.name && (
- {/* Agent Identity Files Manager */} - {agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && ( -
-

- 🧠 Fichiers d'identité -

-
+ {/* View Tabs */} +
+ + +
+ + {activeView === 'memory' ? ( + + ) : ( + <> + {/* Agent Identity Files Manager */} + {agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && ( +
+

+ ⚙️ Fichiers d'identité +

+
{agent.identity_files.map(file => { const isActive = activeFile?.agent === agent.name && activeFile?.file === file.name; return ( @@ -506,10 +625,10 @@ function AgentsTab() { {/* Identity File Editor */} {activeFile?.agent === agent.name && ( -
-
+
workspace/{agent.name}/{activeFile.file}