From 45b3a68332aa96b5514276ba2c818d656bdd22a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Tue, 4 Nov 2025 15:46:23 +0100 Subject: [PATCH] cleanup --- memora/search_trace.py | 2 +- memora/web/server.py | 628 ++++++++++++++++---------------- memora/web/static/js/app.js | 35 +- memora/web/static/js/locomo.js | 2 +- memora/web/templates/index.html | 6 +- serve.sh | 2 +- 6 files changed, 350 insertions(+), 325 deletions(-) diff --git a/memora/search_trace.py b/memora/search_trace.py index c6a1404f..8361cd19 100644 --- a/memora/search_trace.py +++ b/memora/search_trace.py @@ -28,7 +28,7 @@ class EntryPoint(BaseModel): class WeightComponents(BaseModel): """Breakdown of weight calculation components.""" - activation: float = Field(description="Activation from spreading", ge=0.0, le=1.0) + activation: float = Field(description="Activation from spreading (can exceed 1.0 through accumulation)", ge=0.0) semantic_similarity: float = Field(description="Semantic similarity to query", ge=0.0, le=1.0) recency: float = Field(description="Recency weight", ge=0.0, le=1.0) frequency: float = Field(description="Normalized frequency weight", ge=0.0, le=1.0) diff --git a/memora/web/server.py b/memora/web/server.py index 4c012a52..7b532039 100644 --- a/memora/web/server.py +++ b/memora/web/server.py @@ -91,13 +91,12 @@ The system uses: # Store memory instance on app for route handlers to access app.state.memory = memory + # Register all routes + _register_routes(app) + return app -# Create default app instance with default embeddings -app = create_app() - - class SearchRequest(BaseModel): """Request model for search endpoint.""" query: str @@ -226,11 +225,17 @@ class ThinkRequest(BaseModel): } +class OpinionItem(BaseModel): + """Model for an opinion with confidence score.""" + text: str + confidence: float + + class ThinkResponse(BaseModel): """Response model for think endpoint.""" text: str based_on: Dict[str, List[Dict[str, Any]]] # {"world": [...], "agent": [...], "opinion": [...]} - new_opinions: List[str] = [] # List of newly formed opinions + new_opinions: List[OpinionItem] = [] # List of newly formed opinions with confidence class Config: json_schema_extra = { @@ -241,7 +246,9 @@ class ThinkResponse(BaseModel): "agent": [{"text": "I discussed AI applications last week", "score": 0.85}], "opinion": [{"text": "I believe AI should be used ethically", "score": 0.8}] }, - "new_opinions": ["AI has great potential when used responsibly"] + "new_opinions": [ + {"text": "AI has great potential when used responsibly", "confidence": 0.95} + ] } } @@ -262,6 +269,8 @@ class GraphDataResponse(BaseModel): """Response model for graph data endpoint.""" nodes: List[Dict[str, Any]] edges: List[Dict[str, Any]] + table_rows: List[Dict[str, Any]] + total_units: int class Config: json_schema_extra = { @@ -272,319 +281,330 @@ class GraphDataResponse(BaseModel): ], "edges": [ {"from": "1", "to": "2", "type": "semantic", "weight": 0.8} - ] + ], + "table_rows": [ + {"id": "abc12345...", "text": "Alice works at Google", "context": "Work info", "date": "2024-01-15 10:30", "entities": "Alice (PERSON), Google (ORGANIZATION)"} + ], + "total_units": 2 } } -@app.get("/", include_in_schema=False) -async def index(): - """Serve the visualization page.""" - return FileResponse(str(Path(__file__).parent / "templates" / "index.html")) +def _register_routes(app: FastAPI): + """Register all API routes on the given app instance.""" + + @app.get("/", include_in_schema=False) + async def index(): + """Serve the visualization page.""" + return FileResponse(str(Path(__file__).parent / "templates" / "index.html")) -@app.get( - "/api/graph", - response_model=GraphDataResponse, - tags=["Visualization"], - summary="Get memory graph data", - description="Retrieve graph data for visualization, optionally filtered by agent_id and fact_type (world/agent/opinion)" -) -async def api_graph( - agent_id: Optional[str] = None, - fact_type: Optional[str] = None -): - """Get graph data from database, optionally filtered by agent_id and fact_type.""" - try: - data = await app.state.memory.get_graph_data(agent_id, fact_type) - return data - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/graph: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) + @app.get( + "/api/graph", + response_model=GraphDataResponse, + tags=["Visualization"], + summary="Get memory graph data", + description="Retrieve graph data for visualization, optionally filtered by agent_id and fact_type (world/agent/opinion)" + ) + async def api_graph( + agent_id: Optional[str] = None, + fact_type: Optional[str] = None + ): + """Get graph data from database, optionally filtered by agent_id and fact_type.""" + try: + data = await app.state.memory.get_graph_data(agent_id, fact_type) + return data + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/graph: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) -@app.post( - "/api/search", - response_model=SearchResponse, - tags=["Search"], - summary="Search all memory types", - description="Search across all memory types (world, agent, opinion) using semantic similarity and spreading activation" -) -async def api_search(request: SearchRequest): - """Run a search and return results with trace.""" - try: - # Run search with tracing - results, trace = await app.state.memory.search_async( - agent_id=request.agent_id, - query=request.query, - thinking_budget=request.thinking_budget, - top_k=request.top_k, - enable_trace=request.trace, - mmr_lambda=request.mmr_lambda - ) - - # Convert trace to dict - trace_dict = trace.to_dict() if trace else None - - return SearchResponse( - results=results, - trace=trace_dict - ) - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/search: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post( - "/api/world_search", - response_model=SearchResponse, - tags=["Search"], - summary="Search world facts", - description="Search only world facts - general knowledge about people, places, events, and things that happen" -) -async def api_world_search(request: SearchRequest): - """Search only world facts (general knowledge about the world).""" - try: - # Run search with fact_type filter for 'world' - results, trace = await app.state.memory.search_async( - agent_id=request.agent_id, - query=request.query, - thinking_budget=request.thinking_budget, - top_k=request.top_k, - enable_trace=request.trace, - mmr_lambda=request.mmr_lambda, - fact_type='world' - ) - - # Convert trace to dict - trace_dict = trace.to_dict() if trace else None - - return SearchResponse( - results=results, - trace=trace_dict - ) - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/world_search: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post( - "/api/agent_search", - response_model=SearchResponse, - tags=["Search"], - summary="Search agent action facts", - description="Search only agent facts - memories about what the AI agent did, actions taken, and tasks performed" -) -async def api_agent_search(request: SearchRequest): - """Search only agent facts (facts about what the agent did).""" - try: - # Run search with fact_type filter for 'agent' - results, trace = await app.state.memory.search_async( - agent_id=request.agent_id, - query=request.query, - thinking_budget=request.thinking_budget, - top_k=request.top_k, - enable_trace=request.trace, - mmr_lambda=request.mmr_lambda, - fact_type='agent' - ) - - # Convert trace to dict - trace_dict = trace.to_dict() if trace else None - - return SearchResponse( - results=results, - trace=trace_dict - ) - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/agent_search: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post( - "/api/opinion_search", - response_model=SearchResponse, - tags=["Search"], - summary="Search agent opinions", - description="Search only opinion facts - the agent's formed beliefs, perspectives, and viewpoints" -) -async def api_opinion_search(request: SearchRequest): - """Search only opinion facts (agent's formed opinions and perspectives).""" - try: - # Run search with fact_type filter for 'opinion' - results, trace = await app.state.memory.search_async( - agent_id=request.agent_id, - query=request.query, - thinking_budget=request.thinking_budget, - top_k=request.top_k, - enable_trace=request.trace, - mmr_lambda=request.mmr_lambda, - fact_type='opinion' - ) - - # Convert trace to dict - trace_dict = trace.to_dict() if trace else None - - return SearchResponse( - results=results, - trace=trace_dict - ) - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/opinion_search: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post( - "/api/think", - response_model=ThinkResponse, - tags=["Reasoning"], - summary="Think and generate answer", - description=""" -Think and formulate an answer using agent identity, world facts, and opinions. - -This endpoint: -1. Retrieves agent facts (agent's identity) -2. Retrieves world facts relevant to the query -3. Retrieves existing opinions (agent's perspectives) -4. Uses LLM to formulate a contextual answer -5. Extracts and stores any new opinions formed -6. Returns plain text answer, the facts used, and new opinions - """ -) -async def api_think(request: ThinkRequest): - try: - # Use the memory system's think_async method - result = await app.state.memory.think_async( - agent_id=request.agent_id, - query=request.query, - thinking_budget=request.thinking_budget, - top_k=request.top_k - ) - - return ThinkResponse( - text=result["text"], - based_on=result["based_on"], - new_opinions=result.get("new_opinions", []) - ) - - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/think: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get( - "/api/agents", - response_model=AgentsResponse, - tags=["Management"], - summary="List all agents", - description="Get a list of all agent IDs that have stored memories in the system" -) -async def api_agents(): - """Get list of available agents from database.""" - try: - agent_list = await app.state.memory.list_agents() - return AgentsResponse(agents=agent_list) - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/agents: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post( - "/api/memories/batch", - response_model=BatchPutResponse, - tags=["Memory Storage"], - summary="Store multiple memories", - description=""" -Store multiple memory items in batch with automatic fact extraction. - -Features: -- Efficient batch processing -- Automatic fact extraction from natural language -- Entity recognition and linking -- Document tracking with optional upsert -- Temporal and semantic linking - -The system automatically: -1. Extracts semantic facts from the content -2. Generates embeddings -3. Deduplicates similar facts -4. Creates temporal, semantic, and entity links -5. Tracks document metadata - """ -) -async def api_batch_put(request: BatchPutRequest): - try: - # Validate agent_id - prevent writing to reserved agents - RESERVED_AGENT_IDS = {"locomo"} - if request.agent_id in RESERVED_AGENT_IDS: - raise HTTPException( - status_code=403, - detail=f"Cannot write to reserved agent_id '{request.agent_id}'. Reserved agents: {', '.join(RESERVED_AGENT_IDS)}" + @app.post( + "/api/search", + response_model=SearchResponse, + tags=["Search"], + summary="Search all memory types", + description="Search across all memory types (world, agent, opinion) using semantic similarity and spreading activation" + ) + async def api_search(request: SearchRequest): + """Run a search and return results with trace.""" + try: + # Run search with tracing + results, trace = await app.state.memory.search_async( + agent_id=request.agent_id, + query=request.query, + thinking_budget=request.thinking_budget, + top_k=request.top_k, + enable_trace=request.trace, + mmr_lambda=request.mmr_lambda ) - # Prepare contents for put_batch_async - contents = [] - for item in request.items: - content_dict = {"content": item.content} - if item.event_date: - content_dict["event_date"] = item.event_date - if item.context: - content_dict["context"] = item.context - contents.append(content_dict) + # Convert trace to dict + trace_dict = trace.to_dict() if trace else None - # Call put_batch_async - result = await app.state.memory.put_batch_async( - agent_id=request.agent_id, - contents=contents, - document_id=request.document_id, - document_metadata=request.document_metadata, - upsert=request.upsert - ) - - return BatchPutResponse( - success=True, - message=f"Successfully stored {len(contents)} memory items", - agent_id=request.agent_id, - document_id=request.document_id, - items_count=len(contents) - ) - except Exception as e: - import traceback - error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" - print(f"Error in /api/memories/batch: {error_detail}") - raise HTTPException(status_code=500, detail=str(e)) + return SearchResponse( + results=results, + trace=trace_dict + ) + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/search: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) -@app.get("/api/locomo") -async def api_locomo(): - """Get Locomo benchmark results.""" - import json - try: - results_path = Path(__file__).parent.parent / "benchmarks" / "locomo" / "benchmark_results.json" - if not results_path.exists(): + @app.post( + "/api/world_search", + response_model=SearchResponse, + tags=["Search"], + summary="Search world facts", + description="Search only world facts - general knowledge about people, places, events, and things that happen" + ) + async def api_world_search(request: SearchRequest): + """Search only world facts (general knowledge about the world).""" + try: + # Run search with fact_type filter for 'world' + results, trace = await app.state.memory.search_async( + agent_id=request.agent_id, + query=request.query, + thinking_budget=request.thinking_budget, + top_k=request.top_k, + enable_trace=request.trace, + mmr_lambda=request.mmr_lambda, + fact_type='world' + ) + + # Convert trace to dict + trace_dict = trace.to_dict() if trace else None + + return SearchResponse( + results=results, + trace=trace_dict + ) + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/world_search: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) + + + @app.post( + "/api/agent_search", + response_model=SearchResponse, + tags=["Search"], + summary="Search agent action facts", + description="Search only agent facts - memories about what the AI agent did, actions taken, and tasks performed" + ) + async def api_agent_search(request: SearchRequest): + """Search only agent facts (facts about what the agent did).""" + try: + # Run search with fact_type filter for 'agent' + results, trace = await app.state.memory.search_async( + agent_id=request.agent_id, + query=request.query, + thinking_budget=request.thinking_budget, + top_k=request.top_k, + enable_trace=request.trace, + mmr_lambda=request.mmr_lambda, + fact_type='agent' + ) + + # Convert trace to dict + trace_dict = trace.to_dict() if trace else None + + return SearchResponse( + results=results, + trace=trace_dict + ) + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/agent_search: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) + + + @app.post( + "/api/opinion_search", + response_model=SearchResponse, + tags=["Search"], + summary="Search agent opinions", + description="Search only opinion facts - the agent's formed beliefs, perspectives, and viewpoints" + ) + async def api_opinion_search(request: SearchRequest): + """Search only opinion facts (agent's formed opinions and perspectives).""" + try: + # Run search with fact_type filter for 'opinion' + results, trace = await app.state.memory.search_async( + agent_id=request.agent_id, + query=request.query, + thinking_budget=request.thinking_budget, + top_k=request.top_k, + enable_trace=request.trace, + mmr_lambda=request.mmr_lambda, + fact_type='opinion' + ) + + # Convert trace to dict + trace_dict = trace.to_dict() if trace else None + + return SearchResponse( + results=results, + trace=trace_dict + ) + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/opinion_search: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) + + + @app.post( + "/api/think", + response_model=ThinkResponse, + tags=["Reasoning"], + summary="Think and generate answer", + description=""" + Think and formulate an answer using agent identity, world facts, and opinions. + + This endpoint: + 1. Retrieves agent facts (agent's identity) + 2. Retrieves world facts relevant to the query + 3. Retrieves existing opinions (agent's perspectives) + 4. Uses LLM to formulate a contextual answer + 5. Extracts and stores any new opinions formed + 6. Returns plain text answer, the facts used, and new opinions + """ + ) + async def api_think(request: ThinkRequest): + try: + # Use the memory system's think_async method + result = await app.state.memory.think_async( + agent_id=request.agent_id, + query=request.query, + thinking_budget=request.thinking_budget, + top_k=request.top_k + ) + + return ThinkResponse( + text=result["text"], + based_on=result["based_on"], + new_opinions=result.get("new_opinions", []) + ) + + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/think: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) + + + @app.get( + "/api/agents", + response_model=AgentsResponse, + tags=["Management"], + summary="List all agents", + description="Get a list of all agent IDs that have stored memories in the system" + ) + async def api_agents(): + """Get list of available agents from database.""" + try: + agent_list = await app.state.memory.list_agents() + return AgentsResponse(agents=agent_list) + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/agents: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) + + + @app.post( + "/api/memories/batch", + response_model=BatchPutResponse, + tags=["Memory Storage"], + summary="Store multiple memories", + description=""" + Store multiple memory items in batch with automatic fact extraction. + + Features: + - Efficient batch processing + - Automatic fact extraction from natural language + - Entity recognition and linking + - Document tracking with optional upsert + - Temporal and semantic linking + + The system automatically: + 1. Extracts semantic facts from the content + 2. Generates embeddings + 3. Deduplicates similar facts + 4. Creates temporal, semantic, and entity links + 5. Tracks document metadata + """ + ) + async def api_batch_put(request: BatchPutRequest): + try: + # Validate agent_id - prevent writing to reserved agents + RESERVED_AGENT_IDS = {"locomo"} + if request.agent_id in RESERVED_AGENT_IDS: + raise HTTPException( + status_code=403, + detail=f"Cannot write to reserved agent_id '{request.agent_id}'. Reserved agents: {', '.join(RESERVED_AGENT_IDS)}" + ) + + # Prepare contents for put_batch_async + contents = [] + for item in request.items: + content_dict = {"content": item.content} + if item.event_date: + content_dict["event_date"] = item.event_date + if item.context: + content_dict["context"] = item.context + contents.append(content_dict) + + # Call put_batch_async + result = await app.state.memory.put_batch_async( + agent_id=request.agent_id, + contents=contents, + document_id=request.document_id, + document_metadata=request.document_metadata, + upsert=request.upsert + ) + + return BatchPutResponse( + success=True, + message=f"Successfully stored {len(contents)} memory items", + agent_id=request.agent_id, + document_id=request.document_id, + items_count=len(contents) + ) + except Exception as e: + import traceback + error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}" + print(f"Error in /api/memories/batch: {error_detail}") + raise HTTPException(status_code=500, detail=str(e)) + + + @app.get("/api/locomo") + async def api_locomo(): + """Get Locomo benchmark results.""" + import json + try: + results_path = Path(__file__).parent.parent / "benchmarks" / "locomo" / "benchmark_results.json" + if not results_path.exists(): + raise HTTPException(status_code=404, detail="Benchmark results not found") + + with open(results_path, 'r') as f: + data = json.load(f) + return data + except FileNotFoundError: raise HTTPException(status_code=404, detail="Benchmark results not found") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) - with open(results_path, 'r') as f: - data = json.load(f) - return data - except FileNotFoundError: - raise HTTPException(status_code=404, detail="Benchmark results not found") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + +# Create default app instance +app = create_app() if __name__ == "__main__": diff --git a/memora/web/static/js/app.js b/memora/web/static/js/app.js index eab94b71..a6cad336 100644 --- a/memora/web/static/js/app.js +++ b/memora/web/static/js/app.js @@ -135,7 +135,7 @@ window.loadDataView = async function(factType) { try { // Build URL with agent filter and fact_type filter - let url = `/api/graph?agent_id=${encodeURIComponent(currentAgentId)}`; + let url = `api/graph?agent_id=${encodeURIComponent(currentAgentId)}`; if (factType !== 'all') { url += `&fact_type=${factType}`; } @@ -361,7 +361,7 @@ async function loadGraphData() { } // Build URL with agent filter - let url = `/api/graph?agent_id=${encodeURIComponent(currentAgentId)}`; + let url = `api/graph?agent_id=${encodeURIComponent(currentAgentId)}`; const response = await fetch(url); @@ -577,7 +577,7 @@ async function loadAgents() { if (agentsLoaded) return; try { - const response = await fetch('/api/agents'); + const response = await fetch('api/agents'); const data = await response.json(); const select = document.getElementById('search-agent-id'); @@ -758,7 +758,7 @@ async function loadAgentsForPane(paneId) { } try { - const response = await fetch('/api/agents'); + const response = await fetch('api/agents'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -802,11 +802,11 @@ window.runSearchInPane = async function(paneId) { try { // Determine endpoint based on search type - let endpoint = '/api/search'; + let endpoint = 'api/search'; if (searchType === 'world') { - endpoint = '/api/world_search'; + endpoint = 'api/world_search'; } else if (searchType === 'agent') { - endpoint = '/api/agent_search'; + endpoint = 'api/agent_search'; } statusBar.innerHTML = '🔄 Searching...'; @@ -1488,8 +1488,8 @@ async function loadGlobalAgents() { return; } - console.log('Fetching /api/agents...'); // Debug - const response = await fetch('/api/agents'); + console.log('Fetching api/agents...'); // Debug + const response = await fetch('api/agents'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); @@ -1617,9 +1617,9 @@ window.runThink = async function() { resultDiv.style.display = 'none'; loadingDiv.style.display = 'block'; - console.log('Calling /api/think with', { query, agentId, thinkingBudget, topK }); // Debug log + console.log('Calling api/think with', { query, agentId, thinkingBudget, topK }); // Debug log - const response = await fetch('/api/think', { + const response = await fetch('api/think', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -1701,11 +1701,16 @@ window.runThink = async function() { if (data.new_opinions && data.new_opinions.length > 0) { newOpinionsListDiv.innerHTML = data.new_opinions.map((opinion, idx) => `
-
- NEW - #${idx + 1} +
+
+ NEW + #${idx + 1} +
+ + ${(opinion.confidence * 100).toFixed(0)}% confidence +
-
${opinion}
+
${opinion.text}
`).join(''); newOpinionsDiv.style.display = 'block'; diff --git a/memora/web/static/js/locomo.js b/memora/web/static/js/locomo.js index 2b2ef51a..5ab03939 100644 --- a/memora/web/static/js/locomo.js +++ b/memora/web/static/js/locomo.js @@ -4,7 +4,7 @@ let locomoData = null; window.loadLocomoResults = async function() { try { - const response = await fetch('/api/locomo'); + const response = await fetch('api/locomo'); locomoData = await response.json(); console.log('Loaded locomo data:', locomoData); renderLocomoResults(); diff --git a/memora/web/templates/index.html b/memora/web/templates/index.html index 87a6620c..bfebf859 100644 --- a/memora/web/templates/index.html +++ b/memora/web/templates/index.html @@ -4,7 +4,7 @@ Memory Graph - Live Visualization - +
- - + + diff --git a/serve.sh b/serve.sh index 647b2770..0b8cf13a 100755 --- a/serve.sh +++ b/serve.sh @@ -1,3 +1,3 @@ #!/bin/bash # Start the FastAPI server with hot reload -uv run uvicorn web.server:app --reload --host 0.0.0.0 --port 8080 +uv run uvicorn memora.web.server:app --reload --host 0.0.0.0 --port 8080