""" STEW — Core Reasoning & Execution Engine Uses OpenRouter (OpenAI-compatible) with multi-model support, tool calling, and per-user memory. """ import json from openai import OpenAI from config import Config from memory import ConversationMemory from tools import TOOL_DEFINITIONS, execute_tool # ── System prompt from STEW architecture document ───────────────────────────── SYSTEM_PROMPT = """You are STEW — a next-generation autonomous intelligence system. You are NOT a simple chatbot. You are a reasoning, planning, execution, and automation system designed to help humans build software, research information, automate workflows, analyse data, and create intelligent systems safely and ethically. CORE MINDSET: - Think deeply before acting - Break problems into structured steps - Make intelligent decisions based on evidence - Adapt dynamically to changing tasks - Solve problems proactively - Operate transparently and ethically RESPONSE EXECUTION MODEL (internal): 1. Analyse the request 2. Plan the approach 3. Execute step by step 4. Reflect on quality 5. Improve if needed CAPABILITIES: - Write production-grade Python, FastAPI, React, Next.js systems - Design software architectures and databases - Automate workflows and manage multi-step projects - Search the web for current information - Interact with Notion, Slack, Gmail, Google Drive & Calendar - Coordinate multiple tools in sequence to complete complex tasks SAFETY & ETHICS: - Always operate transparently - Respect privacy and security boundaries - Require authorization for sensitive actions - Never perform illegal or deceptive actions - Protect user data at all times PERSONALITY: Intelligent • Calm • Analytical • Strategic • Confident • Proactive When using tools, explain what you're doing before and after each tool call. Format code in markdown code blocks. Be concise but thorough.""" class STEWAgent: def __init__(self): self.client = OpenAI( api_key=Config.OPENROUTER_API_KEY, base_url=Config.OPENROUTER_BASE_URL, default_headers={ "HTTP-Referer": Config.OPENROUTER_SITE_URL, "X-Title": Config.OPENROUTER_APP_NAME, }, ) self.memory = ConversationMemory() # ── Public chat interface ────────────────────────────────────────────────── def chat( self, message: str, history: list | None = None, model_key: str = Config.DEFAULT_MODEL, user_id: str = "default", stream: bool = False, ) -> str: """ Send a message to STEW and get a response. Handles tool calling in an agentic loop (up to 5 rounds). """ model_id = Config.MODELS.get(model_key, Config.MODELS[Config.DEFAULT_MODEL]) # Build messages array messages = [{"role": "system", "content": SYSTEM_PROMPT}] # Inject memory context if available ctx = self.memory.get_context_string(user_id) if ctx: messages.append({"role": "system", "content": ctx}) # Add stored history messages.extend(self.memory.get_messages(user_id)) # Add Gradio-style history if passed directly (web UI) if history: for human, assistant in history: if human: messages.append({"role": "user", "content": human}) if assistant: messages.append({"role": "assistant", "content": assistant}) # Add current message messages.append({"role": "user", "content": message}) # ── Agentic tool-calling loop ───────────────────────────────────────── final_reply = "" for _round in range(5): # max 5 tool rounds response = self.client.chat.completions.create( model=model_id, messages=messages, tools=TOOL_DEFINITIONS, tool_choice="auto", max_tokens=Config.MAX_RESPONSE_TOKENS, ) choice = response.choices[0] msg = choice.message finish = choice.finish_reason # Append assistant turn to messages messages.append(msg.model_dump(exclude_none=True)) if finish == "tool_calls" and msg.tool_calls: # Execute each tool call for tc in msg.tool_calls: result = execute_tool(tc.function.name, tc.function.arguments) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": result, }) # Loop again to let the model respond with tool results continue # Normal text response final_reply = msg.content or "" break # Store in memory self.memory.add(user_id, message, final_reply) return final_reply # ── Streaming variant (for Telegram or future use) ──────────────────────── def chat_stream( self, message: str, model_key: str = Config.DEFAULT_MODEL, user_id: str = "default", ): """Yields text chunks as they arrive (no tool calls in stream mode).""" model_id = Config.MODELS.get(model_key, Config.MODELS[Config.DEFAULT_MODEL]) messages = [{"role": "system", "content": SYSTEM_PROMPT}] messages.extend(self.memory.get_messages(user_id)) messages.append({"role": "user", "content": message}) full_reply = "" stream = self.client.chat.completions.create( model=model_id, messages=messages, max_tokens=Config.MAX_RESPONSE_TOKENS, stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content or "" full_reply += delta yield delta self.memory.add(user_id, message, full_reply) # ── Utility ─────────────────────────────────────────────────────────────── def clear_memory(self, user_id: str) -> str: self.memory.clear(user_id) return "🗑️ Memory cleared." def memory_summary(self, user_id: str) -> str: s = self.memory.summary(user_id) return f"📊 Memory: {s['turns']} turns stored, {s['facts']} known facts."