Files
microdao-daarion/services/memory-service/org_chat_endpoints.py
Apple 0c8bef82f4 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)
2026-01-28 06:40:34 -08:00

286 lines
9.6 KiB
Python

# Org Chat Logging & Decision Extraction Endpoints
# To be appended to memory-service/app/main.py
import re
from typing import Optional, List
from pydantic import BaseModel
from datetime import datetime, date
# ============================================================================
# ORG CHAT LOGGING
# ============================================================================
class OrgChatMessageCreate(BaseModel):
chat_id: int
chat_type: str # official_ops, mentor_room, public_community
chat_title: Optional[str] = None
message_id: int
sender_telegram_id: Optional[int] = None
sender_account_id: Optional[str] = None # UUID if linked
sender_username: Optional[str] = None
sender_display_name: Optional[str] = None
text: Optional[str] = None
has_media: bool = False
media_type: Optional[str] = None
reply_to_message_id: Optional[int] = None
message_at: datetime
class DecisionRecord(BaseModel):
decision: str
action: Optional[str] = None
owner: Optional[str] = None
due_date: Optional[date] = None
canon_change: bool = False
# Decision extraction patterns
DECISION_PATTERNS = {
'decision': re.compile(r'DECISION:\s*(.+?)(?=\n[A-Z]+:|$)', re.IGNORECASE | re.DOTALL),
'action': re.compile(r'ACTION:\s*(.+?)(?=\n[A-Z]+:|$)', re.IGNORECASE | re.DOTALL),
'owner': re.compile(r'OWNER:\s*(@?\w+)', re.IGNORECASE),
'due': re.compile(r'DUE:\s*(\d{4}-\d{2}-\d{2}|\d{2}\.\d{2}\.\d{4})', re.IGNORECASE),
'canon_change': re.compile(r'CANON_CHANGE:\s*(yes|true|так|1)', re.IGNORECASE),
}
def extract_decision_from_text(text: str) -> Optional[DecisionRecord]:
"""Extract structured decision from message text."""
if not text:
return None
# Check if this looks like a decision
if 'DECISION:' not in text.upper():
return None
decision_match = DECISION_PATTERNS['decision'].search(text)
if not decision_match:
return None
decision_text = decision_match.group(1).strip()
action_match = DECISION_PATTERNS['action'].search(text)
owner_match = DECISION_PATTERNS['owner'].search(text)
due_match = DECISION_PATTERNS['due'].search(text)
canon_match = DECISION_PATTERNS['canon_change'].search(text)
due_date = None
if due_match:
date_str = due_match.group(1)
try:
if '-' in date_str:
due_date = datetime.strptime(date_str, '%Y-%m-%d').date()
else:
due_date = datetime.strptime(date_str, '%d.%m.%Y').date()
except ValueError:
pass
return DecisionRecord(
decision=decision_text,
action=action_match.group(1).strip() if action_match else None,
owner=owner_match.group(1) if owner_match else None,
due_date=due_date,
canon_change=bool(canon_match)
)
@app.post("/org-chat/message")
async def log_org_chat_message(msg: OrgChatMessageCreate):
"""
Log a message from an organizational chat.
Automatically extracts decisions if present.
"""
try:
# Insert message
await db.pool.execute(
"""
INSERT INTO org_chat_messages (
chat_id, chat_type, chat_title, message_id,
sender_telegram_id, sender_account_id, sender_username, sender_display_name,
text, has_media, media_type, reply_to_message_id, message_at
) VALUES ($1, $2, $3, $4, $5, $6::uuid, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (chat_id, message_id) DO UPDATE SET
text = EXCLUDED.text,
has_media = EXCLUDED.has_media
""",
msg.chat_id, msg.chat_type, msg.chat_title, msg.message_id,
msg.sender_telegram_id, msg.sender_account_id, msg.sender_username, msg.sender_display_name,
msg.text, msg.has_media, msg.media_type, msg.reply_to_message_id, msg.message_at
)
# Try to extract decision
decision = None
if msg.text:
decision = extract_decision_from_text(msg.text)
if decision:
await db.pool.execute(
"""
INSERT INTO decision_records (
chat_id, source_message_id, decision, action, owner, due_date, canon_change
) VALUES ($1, $2, $3, $4, $5, $6, $7)
""",
msg.chat_id, msg.message_id, decision.decision, decision.action,
decision.owner, decision.due_date, decision.canon_change
)
logger.info(f"Decision extracted from message {msg.message_id} in chat {msg.chat_id}")
return {
"success": True,
"message_id": msg.message_id,
"decision_extracted": decision is not None
}
except Exception as e:
logger.error(f"Failed to log org chat message: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/org-chat/{chat_id}/messages")
async def get_org_chat_messages(
chat_id: int,
limit: int = Query(default=50, le=200),
since: Optional[datetime] = None
):
"""Get messages from an organizational chat."""
try:
if since:
rows = await db.pool.fetch(
"""
SELECT * FROM org_chat_messages
WHERE chat_id = $1 AND message_at > $2
ORDER BY message_at DESC LIMIT $3
""",
chat_id, since, limit
)
else:
rows = await db.pool.fetch(
"""
SELECT * FROM org_chat_messages
WHERE chat_id = $1
ORDER BY message_at DESC LIMIT $2
""",
chat_id, limit
)
return {"messages": [dict(r) for r in rows], "count": len(rows)}
except Exception as e:
logger.error(f"Failed to get org chat messages: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/decisions")
async def get_decisions(
status: Optional[str] = None,
chat_id: Optional[int] = None,
canon_only: bool = False,
overdue_only: bool = False,
limit: int = Query(default=20, le=100)
):
"""Get decision records with filters."""
try:
conditions = []
params = []
param_idx = 1
if status:
conditions.append(f"status = ${param_idx}")
params.append(status)
param_idx += 1
if chat_id:
conditions.append(f"chat_id = ${param_idx}")
params.append(chat_id)
param_idx += 1
if canon_only:
conditions.append("canon_change = true")
if overdue_only:
conditions.append(f"due_date < ${param_idx} AND status NOT IN ('completed', 'cancelled')")
params.append(date.today())
param_idx += 1
where_clause = " AND ".join(conditions) if conditions else "1=1"
params.append(limit)
rows = await db.pool.fetch(
f"""
SELECT dr.*, ocm.text as source_text, ocm.sender_username
FROM decision_records dr
LEFT JOIN org_chat_messages ocm ON dr.chat_id = ocm.chat_id AND dr.source_message_id = ocm.message_id
WHERE {where_clause}
ORDER BY dr.created_at DESC
LIMIT ${param_idx}
""",
*params
)
return {"decisions": [dict(r) for r in rows], "count": len(rows)}
except Exception as e:
logger.error(f"Failed to get decisions: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.patch("/decisions/{decision_id}/status")
async def update_decision_status(
decision_id: str,
status: str,
updated_by: Optional[str] = None
):
"""Update decision status (pending, in_progress, completed, cancelled)."""
try:
valid_statuses = ['pending', 'in_progress', 'completed', 'cancelled']
if status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"Invalid status. Must be one of: {valid_statuses}")
await db.pool.execute(
"""
UPDATE decision_records
SET status = $1, status_updated_at = NOW(), status_updated_by = $2
WHERE id = $3::uuid
""",
status, updated_by, decision_id
)
return {"success": True, "decision_id": decision_id, "new_status": status}
except Exception as e:
logger.error(f"Failed to update decision status: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/decisions/summary")
async def get_decisions_summary():
"""Get summary of decisions by status."""
try:
rows = await db.pool.fetch(
"""
SELECT
status,
COUNT(*) as count,
COUNT(*) FILTER (WHERE due_date < CURRENT_DATE AND status NOT IN ('completed', 'cancelled')) as overdue
FROM decision_records
GROUP BY status
"""
)
summary = {r['status']: {'count': r['count'], 'overdue': r['overdue']} for r in rows}
# Count canon changes
canon_count = await db.pool.fetchval(
"SELECT COUNT(*) FROM decision_records WHERE canon_change = true"
)
return {
"by_status": summary,
"canon_changes": canon_count,
"total": sum(s['count'] for s in summary.values())
}
except Exception as e:
logger.error(f"Failed to get decisions summary: {e}")
raise HTTPException(status_code=500, detail=str(e))