diff --git a/backend/app.py b/backend/app.py index 69ddbac..b604f71 100644 --- a/backend/app.py +++ b/backend/app.py @@ -11,7 +11,10 @@ from uvicorn.logging import DefaultFormatter from backend.core.config import Settings from backend.core.database import close_engine, close_valkey, init_db -from backend.core.dependencies import get_agent_service +from backend.core.agent import AgentService, set_service +from backend.repositories.memory_repository import MemoryRepository +from backend.services.agent_factory import build_agent +from backend.services.rag_service import RagService from backend.routes.admin import router as admin_router from backend.routes.auth import router as auth_router from backend.routes.chat import router as chat_router @@ -60,7 +63,9 @@ class AppBuilder: """Builds and configures the FastAPI application with DI wiring. Accepts a ``Settings`` instance so tests can supply custom config. - The ``AgentService`` is lazily initialised inside the lifespan. + The ``AgentService`` is built inside the lifespan hook, which wires + ``MemoryRepository``, ``RagService``, and the agent factory together + and registers the result as a module-level singleton. """ def __init__(self, settings: Settings | None = None) -> None: @@ -110,9 +115,12 @@ class AppBuilder: # Create database tables if they don't exist. await init_db() - # Initialise the agent service on startup. - agent_service = await get_agent_service() - agent_service.initialize() + # Build agent with full dependency injection via the factory. + memory_repo = MemoryRepository() + rag_service = RagService() + agent = build_agent(self._settings, memory_repo, rag_service) + agent_service = AgentService(agent=agent, memory_repo=memory_repo) + set_service(agent_service) yield diff --git a/backend/core/agent.py b/backend/core/agent.py index b14feda..acc9380 100644 --- a/backend/core/agent.py +++ b/backend/core/agent.py @@ -27,41 +27,28 @@ from __future__ import annotations import time from dataclasses import dataclass +from typing import Any from pydantic_ai import Agent -from pydantic_ai.tools import Tool -from pydantic_ai_harness import CodeMode -from backend.core.config import Settings from backend.repositories.memory_repository import MemoryRepository -from backend.services.agent_factory import build_agent -from backend.services.rag_service import RagService from pydantic_deep.deps import DeepAgentDeps -class AgentService: - """Encapsulates agent construction, execution, and teardown. - Uses lazy initialization so the agent is not built at import time. - Accepts a ``Settings`` instance for testability (dependency injection). +class AgentService: + """Thin orchestrator for agent execution. + + Receives a pre-built ``Agent`` and its ``MemoryRepository`` via + constructor injection so the lifespan (or tests) control wiring. + No lazy initialisation — the agent is ready to run immediately. """ - def __init__(self, settings: Settings) -> None: - self._settings = settings - self._agent: Agent[any, str] | None = None + def __init__(self, agent: Agent[Any, str], memory_repo: MemoryRepository) -> None: + self._agent = agent + self._memory_repo = memory_repo # ── Lifecycle ────────────────────────────────────────────────────────── - def initialize(self) -> None: - """Build the agent. Idempotent — safe to call multiple times.""" - if self._agent is not None: - return - - memory_repo = MemoryRepository() - rag_service = RagService() - - # The factory handles all complex initialization logic - self._agent = build_agent(self._settings, memory_repo, rag_service) - async def shutdown(self) -> None: """Release agent resources.""" self._agent = None @@ -70,17 +57,13 @@ class AgentService: async def ask(self, prompt: str, session_id: str | None = None) -> AskResult: """Send a prompt to the agent and return the text output with usage stats.""" - self.initialize() - assert self._agent is not None - - # Dependency injection using the repository's backend - memory_repo = MemoryRepository() - deps = DeepAgentDeps(backend=memory_repo.backend) + di = DeepAgentDeps(backend=self._memory_repo.backend) t0 = time.monotonic() - result = await self._agent.run(prompt, deps=deps) + result = await self._agent.run(prompt, deps=di) elapsed = time.monotonic() - t0 usage = result.usage + return AskResult( output=result.output, input_tokens=usage.input_tokens, @@ -89,6 +72,7 @@ class AgentService: elapsed_seconds=round(elapsed, 2), ) + @dataclass class AskResult: """Result of a single agent ``ask()`` call with token usage information.""" @@ -108,20 +92,22 @@ class AskResult: elapsed_seconds: float = 0.0 """Wall-clock time in seconds the agent took to respond.""" + # --------------------------------------------------------------------------- -# Singleton — lazy-initialised on the first call to ``ask()``. -# Keeps the existing module-level ``ask`` facade so downstream imports in -# ``chat.py`` continue to work unchanged. +# Singleton — wired once at startup by the lifespan hook in ``app.py``. +# Routes access it through ``get_service()``. # --------------------------------------------------------------------------- -from backend.core.config import settings as _settings # noqa: E402 +_service: AgentService | None = None -_service = AgentService(_settings) def get_service() -> AgentService: """Return the singleton AgentService instance.""" + assert _service is not None, "AgentService has not been initialised — did the lifespan run?" return _service -async def ask(prompt: str, session_id: str | None = None) -> AskResult: - """Send a prompt to the agent and return the text output with usage stats.""" - return await _service.ask(prompt, session_id=session_id) + +def set_service(service: AgentService) -> None: + """Set the module-level singleton (called by the lifespan hook).""" + global _service + _service = service diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 3abb0ae..ee98d5a 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -12,8 +12,11 @@ server { # ── API proxy ───────────────────────────────────────────────── # Container runtime (Docker/Podman) resolves backend:8000 via - # its built-in DNS. nginx caches the resolved IP at startup, so - # restarting the backend container needs a frontend restart too. + # its built-in DNS (127.0.0.11). Without a resolver, nginx caches + # the resolved IP at startup and returns 502 when the backend + # container is recreated. The resolver + valid=5s forces periodic + # re-resolution so the frontend survives backend restarts. + resolver 127.0.0.11 valid=5s; location /api/ { proxy_pass http://backend:8000; proxy_set_header Host $host;