backend/src/agents/context_builder.py
"""Context Builder — no-op Phase 1 implementation (`context-builder-design.md`, Revision 3). Exists to establish the interface boundary Phase 2 (memory + RAG retrieval) will fill in, so that growth lands in this one module instead of the Orchestrator. Milestone 7 delivers the class and its unit tests only — the Orchestrator's conditional call site (invoked only on the low-confidence/no-route branch, after the Router, never on the high-confidence path) is Milestone 8 scope. """ from __future__ import annotations import logging import time from abc import ABC, abstractmethod from typing import Any from agents.base.agent_context import AgentContext from models.context import RequestContext from observability import log_event, record_metric class ContextBuilder(ABC): """Produces a `RequestContext` from the raw query + `AgentContext`. Never called by the Router (which stays a pure function of the raw query) and never called by Skills directly — only the Orchestrator calls this, and only on the low-confidence/no-route branch (`context-builder-design.md` §2, §8). """ @abstractmethod async def build(self, query: str, agent_context: AgentContext) -> RequestContext: """Build the `RequestContext` for this query.""" def _emit_start( self, *, trace_id: str | None = None, session_id: str | None = None, ) -> None: log_event( "build_start", component=type(self).__name__, trace_id=trace_id, fields={"session_id": session_id}, ) def _emit_complete( self, *, latency_ms: float, retrieved_context_count: int, trace_id: str | None = None, session_id: str | None = None, ) -> None: fields: dict[str, Any] = { "session_id": session_id, "retrieved_context_count": retrieved_context_count, "latency_ms": round(latency_ms, 2), } log_event( "build_complete", component=type(self).__name__, trace_id=trace_id, fields=fields, level=logging.INFO, ) record_metric( "context_builder.latency_ms", latency_ms, tags={"component": type(self).__name__}, ) class PassthroughContextBuilder(ContextBuilder): """Phase 1 no-op: no memory store, no vector index, no external calls.""" async def build(self, query: str, agent_context: AgentContext) -> RequestContext: trace_id = agent_context.trace_id session_id = agent_context.session_id self._emit_start(trace_id=trace_id, session_id=session_id) started = time.perf_counter() context = RequestContext( user_query=query, conversation_history=[], retrieved_context=[], session_id=agent_context.session_id, trace_id=agent_context.trace_id, system_prompt=agent_context.system_prompt, ) latency_ms = (time.perf_counter() - started) * 1000 self._emit_complete( latency_ms=latency_ms, retrieved_context_count=len(context.retrieved_context), trace_id=trace_id, session_id=session_id, ) return context
Follow the work.
Occasional updates on SignalFoundry, MarketCompass, and what we are building at CompassFoundry Labs.