refactor(schemas): rename Agent* schemas and WS frame types to Scout*

Rename all Pydantic models referring to the scout subsystem:
AgentConfig → ScoutConfig, ContentTypeConfig → ScoutContentTypeConfig,
AgentCatalogItem → ScoutCatalogItem, AgentCreationCheckRequest/Response →
ScoutCreationCheckRequest/Response, AgentTriggerRequest → ScoutTriggerRequest,
AgentRunLogResponse → ScoutRunLogResponse.

LLM-helper agent schemas in app/agents/* are untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto
2026-05-16 00:58:14 +02:00
parent c2b27d4fb7
commit 105cf52083
4 changed files with 49 additions and 49 deletions

View File

@@ -1,4 +1,4 @@
"""Chatbot Journey — WS-based guided conversation to build an AgentConfig. """Chatbot Journey — WS-based guided conversation to build an ScoutConfig.
The journey is driven entirely through WebSocket frames (no REST endpoints). The journey is driven entirely through WebSocket frames (no REST endpoints).
The device WS handler dispatches ``journey_start`` and ``journey_message`` The device WS handler dispatches ``journey_start`` and ``journey_message``
@@ -13,7 +13,7 @@ Journey flow:
3. FE sends ``journey_message`` frames for each user reply. 3. FE sends ``journey_message`` frames for each user reply.
4. Server appends the user message, calls the LLM (which may read files 4. Server appends the user message, calls the LLM (which may read files
via tools), and sends back a ``journey_reply``. via tools), and sends back a ``journey_reply``.
5. After 3-5 turns the LLM wraps up by emitting an ``AgentConfig`` JSON 5. After 3-5 turns the LLM wraps up by emitting an ``ScoutConfig`` JSON
block delimited by ``AGENT_CONFIG_START`` / ``AGENT_CONFIG_END``. block delimited by ``AGENT_CONFIG_START`` / ``AGENT_CONFIG_END``.
6. Server parses and validates the JSON with Pydantic, sends 6. Server parses and validates the JSON with Pydantic, sends
``journey_reply`` with ``done=True`` and the serialised config. ``journey_reply`` with ``done=True`` and the serialised config.
@@ -34,7 +34,7 @@ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, Tool
from app.agents.filesystem_agent import make_directory_tools from app.agents.filesystem_agent import make_directory_tools
from app.core.langfuse_client import compile_prompt, extract_usage, get_langfuse, get_prompt_or_fallback, langfuse_context from app.core.langfuse_client import compile_prompt, extract_usage, get_langfuse, get_prompt_or_fallback, langfuse_context
from app.core.llm import get_agent_llm, model_for_agent from app.core.llm import get_agent_llm, model_for_agent
from app.schemas import AgentConfig from app.schemas import ScoutConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
_SESSION_TTL_SECONDS: int = 1800 # 30 minutes _SESSION_TTL_SECONDS: int = 1800 # 30 minutes
# Sentinel strings used to delimit the LLM-produced AgentConfig JSON. # Sentinel strings used to delimit the LLM-produced ScoutConfig JSON.
_CONFIG_START = "AGENT_CONFIG_START" _CONFIG_START = "AGENT_CONFIG_START"
_CONFIG_END = "AGENT_CONFIG_END" _CONFIG_END = "AGENT_CONFIG_END"
@@ -92,7 +92,7 @@ def get_journey_session(session_id: str, user_id: str) -> JourneySession | None:
_JOURNEY_SYSTEM_PROMPT = """\ _JOURNEY_SYSTEM_PROMPT = """\
You are a friendly assistant helping a freelancer configure a data-extraction agent. You are a friendly assistant helping a freelancer configure a data-extraction agent.
Your job is to understand what files the user has in their directory and produce a Your job is to understand what files the user has in their directory and produce a
structured AgentConfig JSON that the extraction agent will use as its instruction set. structured ScoutConfig JSON that the extraction agent will use as its instruction set.
You have access to file-system tools to explore the user's directory: You have access to file-system tools to explore the user's directory:
- list_directory: see folder structure and file names - list_directory: see folder structure and file names
@@ -122,7 +122,7 @@ Cover these topics based on what you discovered:
4. Date extraction (e.g. "by Friday" → dueDate) 4. Date extraction (e.g. "by Friday" → dueDate)
5. Exclusion rules (e.g. skip newsletters, skip files with no project match) 5. Exclusion rules (e.g. skip newsletters, skip files with no project match)
### Step 4 — Produce the AgentConfig JSON ### Step 4 — Produce the ScoutConfig JSON
Once you are ≥ 90% confident, output the final config between these exact markers Once you are ≥ 90% confident, output the final config between these exact markers
(each on its own line): (each on its own line):
@@ -168,7 +168,7 @@ def _build_system_prompt(
) -> tuple[str, Any]: ) -> tuple[str, Any]:
"""Return ``(compiled_system_prompt, langfuse_prompt_obj_or_None)``.""" """Return ``(compiled_system_prompt, langfuse_prompt_obj_or_None)``."""
existing_section = ( existing_section = (
"\nThe user already has the following AgentConfig — refine it based on their answers:\n" "\nThe user already has the following ScoutConfig — refine it based on their answers:\n"
f"```json\n{existing_config}\n```\n" f"```json\n{existing_config}\n```\n"
if existing_config if existing_config
else "" else ""
@@ -189,11 +189,11 @@ def _build_system_prompt(
return compiled, prompt_obj return compiled, prompt_obj
# ── AgentConfig extraction ──────────────────────────────────────────────── # ── ScoutConfig extraction ────────────────────────────────────────────────
def _extract_agent_config(text: str) -> str | None: def _extract_agent_config(text: str) -> str | None:
"""Return validated AgentConfig JSON string from between markers, or None. """Return validated ScoutConfig JSON string from between markers, or None.
Parses the JSON with Pydantic to ensure it conforms to the schema before Parses the JSON with Pydantic to ensure it conforms to the schema before
returning. Returns None if markers are absent or JSON is invalid. returning. Returns None if markers are absent or JSON is invalid.
@@ -206,10 +206,10 @@ def _extract_agent_config(text: str) -> str | None:
if not raw: if not raw:
return None return None
try: try:
parsed = AgentConfig.model_validate_json(raw) parsed = ScoutConfig.model_validate_json(raw)
return parsed.model_dump_json() return parsed.model_dump_json()
except Exception as exc: except Exception as exc:
logger.warning("agent_setup: failed to parse AgentConfig JSON: %s", exc) logger.warning("agent_setup: failed to parse ScoutConfig JSON: %s", exc)
return None return None
@@ -475,7 +475,7 @@ async def handle_journey_message(
if turns >= _MAX_TURNS: if turns >= _MAX_TURNS:
nudge_content = ( nudge_content = (
"[System: You have enough information. Please generate the final " "[System: You have enough information. Please generate the final "
f"AgentConfig JSON now, wrapped in {_CONFIG_START} / {_CONFIG_END} markers.]" f"ScoutConfig JSON now, wrapped in {_CONFIG_START} / {_CONFIG_END} markers.]"
) )
session.history.append({"role": "user", "content": nudge_content}) session.history.append({"role": "user", "content": nudge_content})

View File

@@ -30,11 +30,11 @@ from app.core.note_summarizer import generate_note_summary
from app.db import get_session from app.db import get_session
from app.models import ScoutRunLog, LocalScoutConfig from app.models import ScoutRunLog, LocalScoutConfig
from app.schemas import ( from app.schemas import (
AgentCatalogItem, ScoutCatalogItem,
AgentCreationCheckRequest, ScoutCreationCheckRequest,
AgentCreationCheckResponse, ScoutCreationCheckResponse,
AgentRunLogResponse, ScoutRunLogResponse,
AgentTriggerRequest, ScoutTriggerRequest,
UserProfile, UserProfile,
) )
@@ -70,8 +70,8 @@ def _to_data_types(values: list[str]) -> list[str]:
return result return result
def _to_run_log_response(log: ScoutRunLog) -> AgentRunLogResponse: def _to_run_log_response(log: ScoutRunLog) -> ScoutRunLogResponse:
return AgentRunLogResponse( return ScoutRunLogResponse(
id=log.id, id=log.id,
agent_id=log.scout_id, agent_id=log.scout_id,
agent_type=log.scout_type, # type: ignore[arg-type] agent_type=log.scout_type, # type: ignore[arg-type]
@@ -124,28 +124,28 @@ async def _enforce_run_frequency(
# ── Catalog ─────────────────────────────────────────────────────────── # ── Catalog ───────────────────────────────────────────────────────────
@router.get("/catalog", response_model=list[AgentCatalogItem]) @router.get("/catalog", response_model=list[ScoutCatalogItem])
async def get_agent_catalog( async def get_agent_catalog(
current_user: UserProfile = Depends(get_current_user), current_user: UserProfile = Depends(get_current_user),
) -> list[AgentCatalogItem]: ) -> list[ScoutCatalogItem]:
"""Return the static list of available agent types and their descriptions.""" """Return the static list of available agent types and their descriptions."""
return [ return [
AgentCatalogItem( ScoutCatalogItem(
type="local_directory", type="local_directory",
name="Local Directory Monitor", name="Local Directory Monitor",
description="Watches local directories, extracts data from files using AI", description="Watches local directories, extracts data from files using AI",
), ),
AgentCatalogItem( ScoutCatalogItem(
type="gmail", type="gmail",
name="Gmail Connector", name="Gmail Connector",
description="Scans Gmail inbox, extracts tasks/notes from emails", description="Scans Gmail inbox, extracts tasks/notes from emails",
), ),
AgentCatalogItem( ScoutCatalogItem(
type="teams", type="teams",
name="Microsoft Teams Connector", name="Microsoft Teams Connector",
description="Monitors Teams messages, extracts action items", description="Monitors Teams messages, extracts action items",
), ),
AgentCatalogItem( ScoutCatalogItem(
type="outlook", type="outlook",
name="Outlook Connector", name="Outlook Connector",
description="Scans Outlook inbox, extracts tasks/notes", description="Scans Outlook inbox, extracts tasks/notes",
@@ -153,11 +153,11 @@ async def get_agent_catalog(
] ]
@router.post("/can-create", response_model=AgentCreationCheckResponse) @router.post("/can-create", response_model=ScoutCreationCheckResponse)
async def can_create_agent( async def can_create_agent(
body: AgentCreationCheckRequest, body: ScoutCreationCheckRequest,
current_user: UserProfile = Depends(get_current_user), current_user: UserProfile = Depends(get_current_user),
) -> AgentCreationCheckResponse: ) -> ScoutCreationCheckResponse:
"""Check if the user can create one more agent based on billing tier. """Check if the user can create one more agent based on billing tier.
Since configuration is client-owned, the Electron app sends its current Since configuration is client-owned, the Electron app sends its current
@@ -165,7 +165,7 @@ async def can_create_agent(
""" """
limit: int = FEATURES.get(current_user.tier, FEATURES["free"])["batch_active"] limit: int = FEATURES.get(current_user.tier, FEATURES["free"])["batch_active"]
allowed = limit == -1 or body.active_agents < limit allowed = limit == -1 or body.active_agents < limit
return AgentCreationCheckResponse( return ScoutCreationCheckResponse(
allowed=allowed, allowed=allowed,
tier=current_user.tier, tier=current_user.tier,
active_agents=body.active_agents, active_agents=body.active_agents,
@@ -173,12 +173,12 @@ async def can_create_agent(
) )
@router.post("/trigger", response_model=AgentRunLogResponse, status_code=status.HTTP_202_ACCEPTED) @router.post("/trigger", response_model=ScoutRunLogResponse, status_code=status.HTTP_202_ACCEPTED)
async def trigger_agent_run( async def trigger_agent_run(
body: AgentTriggerRequest, body: ScoutTriggerRequest,
current_user: UserProfile = Depends(get_current_user), current_user: UserProfile = Depends(get_current_user),
db: AsyncSession = Depends(get_session), db: AsyncSession = Depends(get_session),
) -> AgentRunLogResponse: ) -> ScoutRunLogResponse:
"""Trigger a local agent run using client-provided configuration.""" """Trigger a local agent run using client-provided configuration."""
_enforce_agent_limit(current_user.tier, body.active_agents) _enforce_agent_limit(current_user.tier, body.active_agents)
await _enforce_run_frequency(current_user.tier, current_user.id, db) await _enforce_run_frequency(current_user.tier, current_user.id, db)

View File

@@ -207,10 +207,10 @@ class WsStreamEnd(BaseModel):
mutations: list[dict[str, Any]] | None = None mutations: list[dict[str, Any]] | None = None
# ── Agent Config V2 ─────────────────────────────────────────────────── # ── Scout Config V2 ───────────────────────────────────────────────────
class ContentTypeConfig(BaseModel): class ScoutContentTypeConfig(BaseModel):
"""Per-type extraction config produced by the journey chatbot.""" """Per-type extraction config produced by the journey chatbot."""
id: str id: str
@@ -220,34 +220,34 @@ class ContentTypeConfig(BaseModel):
extraction_prompt: str extraction_prompt: str
class AgentConfig(BaseModel): class ScoutConfig(BaseModel):
"""Structured agent configuration (replaces freeform prompt_template).""" """Structured scout configuration (replaces freeform prompt_template)."""
content_types: list[ContentTypeConfig] = [] content_types: list[ScoutContentTypeConfig] = []
global_rules: list[str] = [] global_rules: list[str] = []
data_types: list[str] = [] data_types: list[str] = []
# ── Agent Catalog ───────────────────────────────────────────────────── # ── Scout Catalog ─────────────────────────────────────────────────────
class AgentCatalogItem(BaseModel): class ScoutCatalogItem(BaseModel):
type: str type: str
name: str name: str
description: str description: str
class AgentCreationCheckRequest(BaseModel): class ScoutCreationCheckRequest(BaseModel):
active_agents: int = Field(ge=0, default=0) active_agents: int = Field(ge=0, default=0)
class AgentCreationCheckResponse(BaseModel): class ScoutCreationCheckResponse(BaseModel):
allowed: bool allowed: bool
tier: BillingTier tier: BillingTier
active_agents: int active_agents: int
limit: int limit: int
class AgentTriggerRequest(BaseModel): class ScoutTriggerRequest(BaseModel):
directory: str = Field(min_length=1) directory: str = Field(min_length=1)
device_id: str = Field(default="") device_id: str = Field(default="")
agent_id: str | None = None # FE stable agent ID (electron-store UUID) agent_id: str | None = None # FE stable agent ID (electron-store UUID)
@@ -259,9 +259,9 @@ class AgentTriggerRequest(BaseModel):
last_run_at: int | None = None # epoch ms from FE — enables incremental scanning last_run_at: int | None = None # epoch ms from FE — enables incremental scanning
# ── Agent Run Log ───────────────────────────────────────────────────── # ── Scout Run Log ─────────────────────────────────────────────────────
class AgentRunLogResponse(BaseModel): class ScoutRunLogResponse(BaseModel):
id: str id: str
agent_id: str agent_id: str
agent_type: Literal["local", "cloud"] agent_type: Literal["local", "cloud"]

View File

@@ -1,6 +1,6 @@
"""Tests for Local Agent V2 journey setup (Step 4). """Tests for Local Agent V2 journey setup (Step 4).
Covers the chatbot journey that produces a structured AgentConfig JSON Covers the chatbot journey that produces a structured ScoutConfig JSON
instead of a freeform prompt_template string. instead of a freeform prompt_template string.
Unit tests (no LLM) Unit tests (no LLM)
@@ -16,7 +16,7 @@ Eval test (real LLM + Langfuse scoring)
---------------------------------------- ----------------------------------------
4.1 Journey start explores directory → first reply contains a question 4.1 Journey start explores directory → first reply contains a question
Cases 4.24.5 (multi-turn conversations producing a full AgentConfig) are Cases 4.24.5 (multi-turn conversations producing a full ScoutConfig) are
non-deterministic and tested manually — results tracked in Langfuse. non-deterministic and tested manually — results tracked in Langfuse.
Run: Run:
@@ -48,7 +48,7 @@ from app.api.routes.scout_setup import (
) )
from app.core.langfuse_client import get_langfuse from app.core.langfuse_client import get_langfuse
from app.core.ws_context import clear_client_executor, set_client_executor from app.core.ws_context import clear_client_executor, set_client_executor
from app.schemas import AgentConfig from app.schemas import ScoutConfig
from tests.conftest import TEST_USER_IDS from tests.conftest import TEST_USER_IDS
# ── Constants ───────────────────────────────────────────────────────────── # ── Constants ─────────────────────────────────────────────────────────────
@@ -179,7 +179,7 @@ def _evaluate_case(case: dict, reply: dict) -> tuple[float, str]:
def test_4_6a_extract_valid_json(): def test_4_6a_extract_valid_json():
"""_extract_agent_config: valid JSON between markers → returns serialised config.""" """_extract_agent_config: valid JSON between markers → returns serialised config."""
config = AgentConfig( config = ScoutConfig(
content_types=[], content_types=[],
global_rules=["No project = no entity"], global_rules=["No project = no entity"],
data_types=["tasks"], data_types=["tasks"],
@@ -187,7 +187,7 @@ def test_4_6a_extract_valid_json():
text = f"Some preamble\n{_CONFIG_START}\n{config.model_dump_json()}\n{_CONFIG_END}\nTrailing" text = f"Some preamble\n{_CONFIG_START}\n{config.model_dump_json()}\n{_CONFIG_END}\nTrailing"
result = _extract_agent_config(text) result = _extract_agent_config(text)
assert result is not None assert result is not None
parsed = AgentConfig.model_validate_json(result) parsed = ScoutConfig.model_validate_json(result)
assert parsed.global_rules == ["No project = no entity"] assert parsed.global_rules == ["No project = no entity"]