feat: Add Alateya, Clan, Eonarch agents + fix gateway-router connection

## Agents Added
- Alateya: R&D, biotech, innovations
- Clan (Spirit): Community spirit agent
- Eonarch: Consciousness evolution agent

## Changes
- docker-compose.node1.yml: Added tokens for all 3 new agents
- gateway-bot/http_api.py: Added configs and webhook endpoints
- gateway-bot/clan_prompt.txt: New prompt file
- gateway-bot/eonarch_prompt.txt: New prompt file

## Fixes
- Fixed ROUTER_URL from :9102 to :8000 (internal container port)
- All 9 Telegram agents now working

## Documentation
- Created PROJECT-MASTER-INDEX.md - single entry point
- Added various status documents and scripts

Tokens configured:
- Helion, NUTRA, Agromatrix (existing)
- Alateya, Clan, Eonarch (new)
- Druid, GreenFood, DAARWIZZ (configured)
This commit is contained in:
Apple
2026-01-28 06:40:34 -08:00
parent 4aeb69e7ae
commit 0c8bef82f4
120 changed files with 21905 additions and 425 deletions

View File

@@ -8,7 +8,7 @@ DAARION Memory Service - FastAPI Application
"""
from contextlib import asynccontextmanager
from typing import List, Optional
from fastapi import Depends
from fastapi import Depends, BackgroundTasks
from uuid import UUID
import structlog
from fastapi import FastAPI, HTTPException, Query
@@ -573,6 +573,323 @@ async def delete_fact(
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# AGENT MEMORY (Gateway compatibility endpoint)
# ============================================================================
class AgentMemoryRequest(BaseModel):
"""Request format from Gateway for saving chat history"""
agent_id: str
team_id: Optional[str] = None
channel_id: Optional[str] = None
user_id: str
# Support both formats: new (content) and gateway (body_text)
content: Optional[str] = None
body_text: Optional[str] = None
role: str = "user" # user, assistant, system
# Support both formats: metadata and body_json
metadata: Optional[dict] = None
body_json: Optional[dict] = None
context: Optional[str] = None
scope: Optional[str] = None
kind: Optional[str] = None # "message", "event", etc.
def get_content(self) -> str:
"""Get content from either field"""
return self.content or self.body_text or ""
def get_metadata(self) -> dict:
"""Get metadata from either field"""
return self.metadata or self.body_json or {}
@app.post("/agents/{agent_id}/memory")
async def save_agent_memory(agent_id: str, request: AgentMemoryRequest, background_tasks: BackgroundTasks):
"""
Save chat turn to memory with full ingestion pipeline:
1. Save to PostgreSQL (facts table)
2. Create embedding via Cohere and save to Qdrant
3. Update Knowledge Graph in Neo4j
"""
try:
from datetime import datetime
from uuid import uuid4
# Create a unique key for this conversation event
timestamp = datetime.utcnow().isoformat()
message_id = str(uuid4())
fact_key = f"chat_event:{request.channel_id}:{timestamp}"
# Store as a fact with JSON payload
content = request.get_content()
metadata = request.get_metadata()
# Skip empty messages
if not content or content.startswith("[Photo:"):
logger.debug("skipping_empty_or_photo_message", content=content[:50] if content else "")
return {"status": "ok", "event_id": None, "indexed": False}
# Determine role from kind/body_json if not explicitly set
role = request.role
if request.body_json and request.body_json.get("type") == "agent_response":
role = "assistant"
event_data = {
"message_id": message_id,
"agent_id": agent_id,
"team_id": request.team_id,
"channel_id": request.channel_id,
"user_id": request.user_id,
"role": role,
"content": content,
"metadata": metadata,
"scope": request.scope,
"kind": request.kind,
"timestamp": timestamp
}
# 1. Save to PostgreSQL (isolated by agent_id)
await db.ensure_facts_table()
result = await db.upsert_fact(
user_id=request.user_id,
fact_key=fact_key,
fact_value_json=event_data,
team_id=request.team_id,
agent_id=agent_id # Agent isolation
)
logger.info("agent_memory_saved",
agent_id=agent_id,
user_id=request.user_id,
role=role,
channel_id=request.channel_id,
content_len=len(content))
# 2. Index in Qdrant (async background task)
background_tasks.add_task(
index_message_in_qdrant,
message_id=message_id,
content=content,
agent_id=agent_id,
user_id=request.user_id,
channel_id=request.channel_id,
role=role,
timestamp=timestamp
)
# 3. Update Neo4j graph (async background task)
background_tasks.add_task(
update_neo4j_graph,
message_id=message_id,
content=content,
agent_id=agent_id,
user_id=request.user_id,
channel_id=request.channel_id,
role=role
)
return {
"status": "ok",
"event_id": result.get("fact_id") if result else None,
"message_id": message_id,
"indexed": True
}
except Exception as e:
logger.error("agent_memory_save_failed", error=str(e), agent_id=agent_id)
raise HTTPException(status_code=500, detail=str(e))
async def index_message_in_qdrant(
message_id: str,
content: str,
agent_id: str,
user_id: str,
channel_id: str,
role: str,
timestamp: str
):
"""Index message in Qdrant for semantic search (isolated by agent_id)"""
try:
from .embedding import get_document_embeddings
from qdrant_client.http import models as qmodels
# Skip very short messages
if len(content) < 10:
return
# Generate embedding
embeddings = await get_document_embeddings([content])
if not embeddings or not embeddings[0]:
logger.warning("embedding_failed", message_id=message_id)
return
vector = embeddings[0]
# Use agent-specific collection (isolation!)
collection_name = f"{agent_id}_messages"
# Ensure collection exists
try:
vector_store.client.get_collection(collection_name)
except Exception:
# Create collection if not exists
vector_store.client.create_collection(
collection_name=collection_name,
vectors_config=qmodels.VectorParams(
size=len(vector),
distance=qmodels.Distance.COSINE
)
)
logger.info("created_collection", collection=collection_name)
# Save to agent-specific Qdrant collection
vector_store.client.upsert(
collection_name=collection_name,
points=[
qmodels.PointStruct(
id=message_id,
vector=vector,
payload={
"message_id": message_id,
"agent_id": agent_id,
"user_id": user_id,
"channel_id": channel_id,
"role": role,
"content": content,
"timestamp": timestamp,
"type": "chat_message"
}
)
]
)
logger.info("message_indexed_qdrant",
message_id=message_id,
collection=collection_name,
content_len=len(content),
vector_dim=len(vector))
except Exception as e:
logger.error("qdrant_indexing_failed", error=str(e), message_id=message_id)
async def update_neo4j_graph(
message_id: str,
content: str,
agent_id: str,
user_id: str,
channel_id: str,
role: str
):
"""Update Knowledge Graph in Neo4j (with agent isolation)"""
try:
import httpx
import os
neo4j_url = os.getenv("NEO4J_HTTP_URL", "http://neo4j:7474")
neo4j_user = os.getenv("NEO4J_USER", "neo4j")
neo4j_password = os.getenv("NEO4J_PASSWORD", "DaarionNeo4j2026!")
# Create/update User node and Message relationship
# IMPORTANT: agent_id is added to relationships for filtering
cypher = """
MERGE (u:User {user_id: $user_id})
ON CREATE SET u.created_at = datetime()
ON MATCH SET u.last_seen = datetime()
MERGE (ch:Channel {channel_id: $channel_id})
ON CREATE SET ch.created_at = datetime()
MERGE (a:Agent {agent_id: $agent_id})
ON CREATE SET a.created_at = datetime()
MERGE (u)-[p:PARTICIPATES_IN {agent_id: $agent_id}]->(ch)
ON CREATE SET p.first_seen = datetime()
ON MATCH SET p.last_seen = datetime()
CREATE (m:Message {
message_id: $message_id,
role: $role,
content_preview: $content_preview,
agent_id: $agent_id,
created_at: datetime()
})
CREATE (u)-[:SENT {agent_id: $agent_id}]->(m)
CREATE (m)-[:IN_CHANNEL {agent_id: $agent_id}]->(ch)
CREATE (m)-[:HANDLED_BY]->(a)
RETURN m.message_id as id
"""
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{neo4j_url}/db/neo4j/tx/commit",
auth=(neo4j_user, neo4j_password),
json={
"statements": [{
"statement": cypher,
"parameters": {
"user_id": user_id,
"channel_id": channel_id,
"message_id": message_id,
"role": role,
"content_preview": content[:200] if content else "",
"agent_id": agent_id
}
}]
}
)
if response.status_code == 200:
logger.info("neo4j_graph_updated", message_id=message_id, user_id=user_id, agent_id=agent_id)
else:
logger.warning("neo4j_update_failed",
status=response.status_code,
response=response.text[:200])
except Exception as e:
logger.error("neo4j_update_error", error=str(e), message_id=message_id)
@app.get("/agents/{agent_id}/memory")
async def get_agent_memory(
agent_id: str,
user_id: str = Query(...),
channel_id: Optional[str] = None,
limit: int = Query(default=20, le=100)
):
"""
Get recent chat events for an agent/user (isolated by agent_id).
"""
import json as json_lib
try:
# Query facts filtered by agent_id (database-level filtering)
facts = await db.list_facts(user_id=user_id, agent_id=agent_id, limit=limit)
# Filter for chat events from this channel
events = []
for fact in facts:
if fact.get("fact_key", "").startswith("chat_event:"):
# Handle fact_value_json being string or dict
event_data = fact.get("fact_value_json", {})
if isinstance(event_data, str):
try:
event_data = json_lib.loads(event_data)
except:
event_data = {}
if not isinstance(event_data, dict):
event_data = {}
if channel_id is None or event_data.get("channel_id") == channel_id:
events.append(event_data)
return {"events": events[:limit]}
except Exception as e:
logger.error("agent_memory_get_failed", error=str(e), agent_id=agent_id)
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# ADMIN
# ============================================================================