Files
microdao-daarion/services/agents-service/repository_agents.py
Apple 3de3c8cb36 feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
2025-11-27 00:19:40 -08:00

298 lines
10 KiB
Python

"""
Agent Repository — Database operations for agents
Phase 6: CRUD + DB Persistence
"""
import uuid
from typing import List, Optional
from datetime import datetime
import asyncpg
from models import AgentCreate, AgentUpdate, AgentRead, AgentBlueprint
class AgentRepository:
def __init__(self, db_pool: asyncpg.Pool):
self.db = db_pool
# ========================================================================
# Blueprints
# ========================================================================
async def get_blueprint_by_code(self, code: str) -> Optional[AgentBlueprint]:
"""Get blueprint by code"""
row = await self.db.fetchrow(
"""
SELECT id, code, name, description, default_model,
default_tools, default_system_prompt, created_at
FROM agent_blueprints
WHERE code = $1
""",
code
)
if not row:
return None
return AgentBlueprint(
id=str(row['id']),
code=row['code'],
name=row['name'],
description=row['description'],
default_model=row['default_model'],
default_tools=row['default_tools'] or [],
default_system_prompt=row['default_system_prompt'],
created_at=row['created_at']
)
async def list_blueprints(self) -> List[AgentBlueprint]:
"""List all blueprints"""
rows = await self.db.fetch(
"""
SELECT id, code, name, description, default_model,
default_tools, default_system_prompt, created_at
FROM agent_blueprints
ORDER BY name
"""
)
return [
AgentBlueprint(
id=str(row['id']),
code=row['code'],
name=row['name'],
description=row['description'],
default_model=row['default_model'],
default_tools=row['default_tools'] or [],
default_system_prompt=row['default_system_prompt'],
created_at=row['created_at']
)
for row in rows
]
# ========================================================================
# Agents — CRUD
# ========================================================================
async def create_agent(
self,
data: AgentCreate,
actor_id: Optional[str] = None
) -> AgentRead:
"""Create new agent"""
# Get blueprint
blueprint = await self.get_blueprint_by_code(data.blueprint_code)
if not blueprint:
raise ValueError(f"Blueprint '{data.blueprint_code}' not found")
# Generate IDs
agent_id = uuid.uuid4()
external_id = f"agent:{agent_id.hex[:12]}"
# Determine model (use provided or blueprint default)
model = data.model or blueprint.default_model
# Determine system prompt
system_prompt = data.system_prompt or blueprint.default_system_prompt
# Insert
row = await self.db.fetchrow(
"""
INSERT INTO agents (
id, external_id, name, kind, microdao_id, owner_user_id,
blueprint_id, model, tools_enabled, system_prompt,
avatar_url, description, is_active
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING id, external_id, name, kind, microdao_id, owner_user_id,
blueprint_id, model, tools_enabled, system_prompt,
avatar_url, description, is_active, created_at, updated_at
""",
agent_id,
external_id,
data.name,
data.kind.value,
uuid.UUID(data.microdao_id) if data.microdao_id else None,
uuid.UUID(data.owner_user_id or actor_id) if (data.owner_user_id or actor_id) else None,
uuid.UUID(blueprint.id),
model,
data.tools_enabled or [],
system_prompt,
data.avatar_url,
data.description,
True
)
return self._row_to_agent_read(row)
async def update_agent(
self,
agent_id: str,
data: AgentUpdate
) -> AgentRead:
"""Update agent"""
# Build dynamic update query
updates = []
values = []
param_idx = 1
if data.name is not None:
updates.append(f"name = ${param_idx}")
values.append(data.name)
param_idx += 1
if data.description is not None:
updates.append(f"description = ${param_idx}")
values.append(data.description)
param_idx += 1
if data.model is not None:
updates.append(f"model = ${param_idx}")
values.append(data.model)
param_idx += 1
if data.tools_enabled is not None:
updates.append(f"tools_enabled = ${param_idx}")
values.append(data.tools_enabled)
param_idx += 1
if data.system_prompt is not None:
updates.append(f"system_prompt = ${param_idx}")
values.append(data.system_prompt)
param_idx += 1
if data.avatar_url is not None:
updates.append(f"avatar_url = ${param_idx}")
values.append(data.avatar_url)
param_idx += 1
if data.is_active is not None:
updates.append(f"is_active = ${param_idx}")
values.append(data.is_active)
param_idx += 1
if not updates:
# No changes
return await self.get_agent_by_external_id(agent_id)
# Always update updated_at
updates.append(f"updated_at = NOW()")
# Add agent_id to values
values.append(agent_id)
query = f"""
UPDATE agents
SET {', '.join(updates)}
WHERE external_id = ${param_idx}
RETURNING id, external_id, name, kind, microdao_id, owner_user_id,
blueprint_id, model, tools_enabled, system_prompt,
avatar_url, description, is_active, created_at, updated_at
"""
row = await self.db.fetchrow(query, *values)
if not row:
raise ValueError(f"Agent '{agent_id}' not found")
return self._row_to_agent_read(row)
async def delete_agent(self, agent_id: str) -> None:
"""Soft delete agent (set is_active = false)"""
result = await self.db.execute(
"""
UPDATE agents
SET is_active = false, updated_at = NOW()
WHERE external_id = $1
""",
agent_id
)
if result == "UPDATE 0":
raise ValueError(f"Agent '{agent_id}' not found")
async def get_agent_by_external_id(self, external_id: str) -> Optional[AgentRead]:
"""Get agent by external_id"""
row = await self.db.fetchrow(
"""
SELECT id, external_id, name, kind, microdao_id, owner_user_id,
blueprint_id, model, tools_enabled, system_prompt,
avatar_url, description, is_active, created_at, updated_at
FROM agents
WHERE external_id = $1
""",
external_id
)
if not row:
return None
return self._row_to_agent_read(row)
async def list_agents(
self,
microdao_id: Optional[str] = None,
owner_user_id: Optional[str] = None,
kind: Optional[str] = None,
is_active: bool = True
) -> List[AgentRead]:
"""List agents with filters"""
query = """
SELECT id, external_id, name, kind, microdao_id, owner_user_id,
blueprint_id, model, tools_enabled, system_prompt,
avatar_url, description, is_active, created_at, updated_at
FROM agents
WHERE 1=1
"""
values = []
param_idx = 1
if microdao_id:
query += f" AND microdao_id = ${param_idx}"
values.append(uuid.UUID(microdao_id))
param_idx += 1
if owner_user_id:
query += f" AND owner_user_id = ${param_idx}"
values.append(uuid.UUID(owner_user_id))
param_idx += 1
if kind:
query += f" AND kind = ${param_idx}"
values.append(kind)
param_idx += 1
query += f" AND is_active = ${param_idx}"
values.append(is_active)
query += " ORDER BY created_at DESC"
rows = await self.db.fetch(query, *values)
return [self._row_to_agent_read(row) for row in rows]
# ========================================================================
# Helpers
# ========================================================================
def _row_to_agent_read(self, row) -> AgentRead:
"""Convert DB row to AgentRead"""
from models import AgentKind
return AgentRead(
id=str(row['id']),
external_id=row['external_id'],
name=row['name'],
kind=AgentKind(row['kind']),
description=row['description'],
microdao_id=str(row['microdao_id']) if row['microdao_id'] else None,
owner_user_id=str(row['owner_user_id']) if row['owner_user_id'] else None,
blueprint_id=str(row['blueprint_id']) if row['blueprint_id'] else None,
model=row['model'],
tools_enabled=row['tools_enabled'] or [],
system_prompt=row['system_prompt'],
avatar_url=row['avatar_url'],
is_active=row['is_active'],
created_at=row['created_at'],
updated_at=row['updated_at']
)