aboutsummaryrefslogtreecommitdiff
path: root/agent/orchestrator
diff options
context:
space:
mode:
Diffstat (limited to 'agent/orchestrator')
-rw-r--r--agent/orchestrator/api.py14
-rw-r--r--agent/orchestrator/matrix_bot.py76
-rw-r--r--agent/orchestrator/models/base.py1
-rw-r--r--agent/orchestrator/models/claude_code.py10
4 files changed, 92 insertions, 9 deletions
diff --git a/agent/orchestrator/api.py b/agent/orchestrator/api.py
index e91ea36..39d1409 100644
--- a/agent/orchestrator/api.py
+++ b/agent/orchestrator/api.py
@@ -4,15 +4,27 @@ Open WebUI (and other OpenAI-API clients) talks to this endpoint set. We accept
the standard `/v1/chat/completions` shape, translate to internal types, hand
off to the orchestrator, and translate the response back to OpenAI shape.
"""
+import asyncio
+from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ConfigDict
from .loop import handle_turn
+from .matrix_bot import run as matrix_run
from .models import Message
-app = FastAPI(title="Sénia")
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ task = asyncio.create_task(matrix_run())
+ try:
+ yield
+ finally:
+ task.cancel()
+
+
+app = FastAPI(title="Sénia", lifespan=lifespan)
class ChatMessage(BaseModel):
diff --git a/agent/orchestrator/matrix_bot.py b/agent/orchestrator/matrix_bot.py
new file mode 100644
index 0000000..30c23f1
--- /dev/null
+++ b/agent/orchestrator/matrix_bot.py
@@ -0,0 +1,76 @@
+"""Matrix bot adapter for Sénia.
+
+Runs as a long-lived asyncio task alongside the API server. Watches one room,
+forwards messages to `handle_turn`, posts the reply back.
+"""
+
+import asyncio
+import logging
+import os
+from collections import deque
+
+from nio import AsyncClient, MatrixRoom, RoomMessageText
+
+from .loop import handle_turn
+from .models import Message
+
+
+log = logging.getLogger(__name__)
+
+HOMESERVER = os.environ.get("MATRIX_HOMESERVER", "https://beep.volfaria.xyz")
+USER_ID = os.environ.get("MATRIX_USER_ID", "@senia:beep.volfaria.xyz")
+TOKEN = os.environ["MATRIX_TOKEN"]
+ROOM = os.environ.get("MATRIX_ROOM", "#senia:beep.volfaria.xyz")
+SENIA_NAME = "Sénia"
+HISTORY_LEN = 30
+
+
+class MatrixBot:
+ def __init__(self):
+ self.client = AsyncClient(HOMESERVER, USER_ID)
+ self.history: deque[Message] = deque(maxlen=HISTORY_LEN)
+ self._room_id: str | None = None
+
+ async def start(self):
+ whoami = await self.client.whoami()
+ self.client.restore_login(
+ user_id=USER_ID, device_id=whoami.device_id, access_token=TOKEN
+ )
+
+ self._room_id = await self._resolve_room(ROOM)
+ await self.client.join(self._room_id)
+
+ self.client.add_event_callback(self._on_message, RoomMessageText)
+ log.info("Sénia matrix bot connected, watching %s", self._room_id)
+ await self.client.sync_forever(timeout=30000, full_state=True)
+
+ async def _resolve_room(self, room: str) -> str:
+ if room.startswith("!"):
+ return room
+ resp = await self.client.room_resolve_alias(room)
+ return resp.room_id
+
+ async def _on_message(self, room: MatrixRoom, event: RoomMessageText):
+ if event.sender == self.client.user_id or room.room_id != self._room_id:
+ return
+
+ sender_name = room.user_name(event.sender) or event.sender
+ self.history.append(Message(role="user", content=event.body, name=sender_name))
+
+ try:
+ reply = await asyncio.to_thread(handle_turn, list(self.history), sender_name)
+ except Exception:
+ log.exception("handle_turn failed")
+ return
+
+ self.history.append(Message(role="assistant", content=reply, name=SENIA_NAME))
+ await self.client.room_send(
+ room_id=self._room_id,
+ message_type="m.room.message",
+ content={"msgtype": "m.text", "body": reply},
+ )
+
+
+async def run():
+ bot = MatrixBot()
+ await bot.start()
diff --git a/agent/orchestrator/models/base.py b/agent/orchestrator/models/base.py
index 4ca1988..cdcbb5c 100644
--- a/agent/orchestrator/models/base.py
+++ b/agent/orchestrator/models/base.py
@@ -21,6 +21,7 @@ Role = Literal["system", "user", "assistant", "tool"]
class Message:
role: Role
content: str
+ name: str | None = None
@dataclass
diff --git a/agent/orchestrator/models/claude_code.py b/agent/orchestrator/models/claude_code.py
index c73df98..293de3e 100644
--- a/agent/orchestrator/models/claude_code.py
+++ b/agent/orchestrator/models/claude_code.py
@@ -52,17 +52,11 @@ class ClaudeCodeBackend(ModelBackend):
return ModelResponse(content=result.stdout.strip())
def _format_prompt(self, request: ModelRequest) -> str:
- """Flatten messages + dynamic context into a single prompt string.
-
- Claude Code's -p mode takes one big prompt, not a structured
- conversation. We render the conversation as a readable transcript with
- the per-turn context prepended. Persona is NOT here; Claude Code
- finds it in the workspace itself.
- """
parts = []
if request.turn_context:
parts.append(f"## Context for this turn\n\n{request.turn_context}\n")
for msg in request.messages:
- parts.append(f"## {_ROLE_LABELS[msg.role]}\n\n{msg.content}\n")
+ label = msg.name or _ROLE_LABELS[msg.role]
+ parts.append(f"## {label}\n\n{msg.content}\n")
parts.append("## Assistant\n\n")
return "\n".join(parts)