This commit is contained in:
Nicolò Boschi 2025-11-04 15:46:23 +01:00
parent 1b7e0bc380
commit 45b3a68332
6 changed files with 350 additions and 325 deletions

View file

@ -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)

View file

@ -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,28 +281,35 @@ 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():
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(
@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(
)
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)
@ -305,14 +321,14 @@ async def api_graph(
raise HTTPException(status_code=500, detail=str(e))
@app.post(
@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):
)
async def api_search(request: SearchRequest):
"""Run a search and return results with trace."""
try:
# Run search with tracing
@ -339,14 +355,14 @@ async def api_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post(
@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):
)
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'
@ -374,14 +390,14 @@ async def api_world_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post(
@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):
)
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'
@ -409,14 +425,14 @@ async def api_agent_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post(
@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):
)
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'
@ -444,24 +460,24 @@ async def api_opinion_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post(
@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.
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
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):
)
async def api_think(request: ThinkRequest):
try:
# Use the memory system's think_async method
result = await app.state.memory.think_async(
@ -484,14 +500,14 @@ async def api_think(request: ThinkRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.get(
@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():
)
async def api_agents():
"""Get list of available agents from database."""
try:
agent_list = await app.state.memory.list_agents()
@ -503,30 +519,30 @@ async def api_agents():
raise HTTPException(status_code=500, detail=str(e))
@app.post(
@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.
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
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
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):
)
async def api_batch_put(request: BatchPutRequest):
try:
# Validate agent_id - prevent writing to reserved agents
RESERVED_AGENT_IDS = {"locomo"}
@ -569,8 +585,8 @@ async def api_batch_put(request: BatchPutRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/locomo")
async def api_locomo():
@app.get("/api/locomo")
async def api_locomo():
"""Get Locomo benchmark results."""
import json
try:
@ -587,6 +603,10 @@ async def api_locomo():
raise HTTPException(status_code=500, detail=str(e))
# Create default app instance
app = create_app()
if __name__ == "__main__":
import uvicorn
print("\n" + "=" * 80)

View file

@ -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 = '<span style="color: #ff9800;">🔄 Searching...</span>';
@ -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) => `
<div style="margin-bottom: 15px; padding: 15px; background: white; border-radius: 6px; border-left: 4px solid #4caf50; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<div style="display: flex; align-items: center;">
<span style="background: #4caf50; color: white; padding: 4px 8px; border-radius: 12px; font-size: 11px; font-weight: bold; margin-right: 10px;">NEW</span>
<span style="color: #666; font-size: 12px;">#${idx + 1}</span>
</div>
<div style="font-size: 14px; color: #333; line-height: 1.5;">${opinion}</div>
<span style="background: #e3f2fd; color: #1976d2; padding: 3px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;">
${(opinion.confidence * 100).toFixed(0)}% confidence
</span>
</div>
<div style="font-size: 14px; color: #333; line-height: 1.5;">${opinion.text}</div>
</div>
`).join('');
newOpinionsDiv.style.display = 'block';

View file

@ -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();

View file

@ -4,7 +4,7 @@
<title>Memory Graph - Live Visualization</title>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="./static/css/styles.css">
</head>
<body>
<div class="breadcrumb-container">
@ -300,7 +300,7 @@
</div>
</div>
<script src="/static/js/app.js"></script>
<script src="/static/js/locomo.js"></script>
<script src="./static/js/app.js"></script>
<script src="./static/js/locomo.js"></script>
</body>
</html>

View file

@ -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