"""Chat engine — manages conversations with streaming support.""" import json import logging from typing import Optional, AsyncIterator from config import config from database.repositories import ConversationRepo, MessageRepo from agent.synapse_agent import SynapseAgent from memory.manager import MemoryManager logger = logging.getLogger("synapse.chat") class ChatEngine: """Manages chat sessions with the Synapse agent.""" def __init__(self): self._active_sessions: dict[str, SynapseAgent] = {} def get_agent(self, user_id: str = "guest", conversation_id: str = None, mode: str = "agent", personality_prompt: str = None) -> SynapseAgent: key = f"{user_id}:{conversation_id or 'new'}" if key not in self._active_sessions: self._active_sessions[key] = SynapseAgent( user_id=user_id, conversation_id=conversation_id, mode=mode, personality_prompt=personality_prompt, ) return self._active_sessions[key] async def send_message(self, content: str, user_id: str = "guest", conversation_id: str = None, mode: str = "agent", personality_prompt: str = None, files: list[dict] = None) -> dict: if not conversation_id: conv = await ConversationRepo.create(user_id=user_id) conversation_id = conv["id"] await MessageRepo.create( conversation_id=conversation_id, role="user", content=content, metadata={"files": files} if files else None, ) history = await MessageRepo.get_last_n(conversation_id, n=20) conversation_messages = [ {"role": m["role"], "content": m["content"]} for m in history ] agent = self.get_agent(user_id, conversation_id, mode, personality_prompt) result = await agent.process(content, conversation_messages, mode) await MessageRepo.create( conversation_id=conversation_id, role="assistant", content=result.get("content", ""), reasoning=json.dumps(result.get("reasoning")) if result.get("reasoning") else None, tool_calls=json.dumps(result.get("tools_used")) if result.get("tools_used") else None, model=result.get("model"), ) msg_count = len(await MessageRepo.get_conversation_messages(conversation_id)) if msg_count == 2: auto_title = content[:60] + ("..." if len(content) > 60 else "") await ConversationRepo.update_title(conversation_id, auto_title) result["conversation_id"] = conversation_id return result async def stream_message(self, content: str, user_id: str = "guest", conversation_id: str = None, mode: str = "agent", personality_prompt: str = None) -> AsyncIterator[str]: if not conversation_id: conv = await ConversationRepo.create(user_id=user_id) conversation_id = conv["id"] yield json.dumps({"event": "conversation_created", "id": conversation_id}) await MessageRepo.create( conversation_id=conversation_id, role="user", content=content, ) history = await MessageRepo.get_last_n(conversation_id, n=20) conversation_messages = [ {"role": m["role"], "content": m["content"]} for m in history ] agent = self.get_agent(user_id, conversation_id, mode, personality_prompt) from models.router import router tool_defs = agent.tool_executor.get_tool_definitions() system_msg = agent._build_system_message(tool_defs) all_messages = [system_msg] + conversation_messages try: stream = await router.chat( messages=all_messages, temperature=config.groq.temperature, max_tokens=config.groq.max_tokens, stream=True, ) full_content = "" async for chunk in self._process_stream(stream): full_content += chunk yield json.dumps({"event": "token", "content": chunk}) await MessageRepo.create( conversation_id=conversation_id, role="assistant", content=full_content, ) yield json.dumps({"event": "done", "conversation_id": conversation_id}) except Exception as e: logger.error(f"Stream error: {e}") yield json.dumps({"event": "error", "message": str(e)}) async def _process_stream(self, stream) -> AsyncIterator[str]: try: async for chunk in stream: if hasattr(chunk, "choices") and chunk.choices: delta = chunk.choices[0].delta if hasattr(delta, "content") and delta.content: yield delta.content elif isinstance(chunk, dict): choices = chunk.get("choices", []) if choices: delta = choices[0].get("delta", {}) content = delta.get("content", "") if content: yield content except Exception as e: logger.error(f"Stream processing error: {e}") yield f"\n[Stream error: {e}]" async def get_conversation_history(self, conversation_id: str) -> list[dict]: return await MessageRepo.get_conversation_messages(conversation_id) async def create_conversation(self, user_id: str, title: str = "New Chat", mode: str = "agent", personality_id: str = "synapse", folder_id: str = None) -> dict: return await ConversationRepo.create( user_id=user_id, title=title, personality_id=personality_id, mode=mode, folder_id=folder_id, ) async def get_user_conversations(self, user_id: str, folder_id: str = None) -> list[dict]: return await ConversationRepo.get_user_conversations(user_id, folder_id) async def delete_conversation(self, conv_id: str) -> bool: return await ConversationRepo.delete(conv_id) async def update_title(self, conv_id: str, title: str) -> None: await ConversationRepo.update_title(conv_id, title) async def toggle_pin(self, conv_id: str) -> None: await ConversationRepo.toggle_pin(conv_id) async def toggle_bookmark(self, conv_id: str) -> None: await ConversationRepo.toggle_bookmark(conv_id) async def search_conversations(self, user_id: str, query: str) -> list[dict]: return await ConversationRepo.search(user_id, query) async def search_messages(self, user_id: str, query: str) -> list[dict]: return await MessageRepo.search(user_id, query) chat_engine = ChatEngine()