feat: refactor agent initialization and enhance API proxy configuration in nginx

This commit is contained in:
2026-06-16 20:58:23 +08:00
parent 8a0b8be677
commit 21cea2cf97
3 changed files with 42 additions and 45 deletions
+13 -5
View File
@@ -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
+24 -38
View File
@@ -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
+5 -2
View File
@@ -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;