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:
@@ -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 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.
|
||||
4. Server appends the user message, calls the LLM (which may read files
|
||||
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``.
|
||||
6. Server parses and validates the JSON with Pydantic, sends
|
||||
``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.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.schemas import AgentConfig
|
||||
from app.schemas import ScoutConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_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_END = "AGENT_CONFIG_END"
|
||||
|
||||
@@ -92,7 +92,7 @@ def get_journey_session(session_id: str, user_id: str) -> JourneySession | None:
|
||||
_JOURNEY_SYSTEM_PROMPT = """\
|
||||
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
|
||||
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:
|
||||
- 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)
|
||||
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
|
||||
(each on its own line):
|
||||
|
||||
@@ -168,7 +168,7 @@ def _build_system_prompt(
|
||||
) -> tuple[str, Any]:
|
||||
"""Return ``(compiled_system_prompt, langfuse_prompt_obj_or_None)``."""
|
||||
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"
|
||||
if existing_config
|
||||
else ""
|
||||
@@ -189,11 +189,11 @@ def _build_system_prompt(
|
||||
return compiled, prompt_obj
|
||||
|
||||
|
||||
# ── AgentConfig extraction ────────────────────────────────────────────────
|
||||
# ── ScoutConfig extraction ────────────────────────────────────────────────
|
||||
|
||||
|
||||
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
|
||||
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:
|
||||
return None
|
||||
try:
|
||||
parsed = AgentConfig.model_validate_json(raw)
|
||||
parsed = ScoutConfig.model_validate_json(raw)
|
||||
return parsed.model_dump_json()
|
||||
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
|
||||
|
||||
|
||||
@@ -475,7 +475,7 @@ async def handle_journey_message(
|
||||
if turns >= _MAX_TURNS:
|
||||
nudge_content = (
|
||||
"[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})
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ from app.core.note_summarizer import generate_note_summary
|
||||
from app.db import get_session
|
||||
from app.models import ScoutRunLog, LocalScoutConfig
|
||||
from app.schemas import (
|
||||
AgentCatalogItem,
|
||||
AgentCreationCheckRequest,
|
||||
AgentCreationCheckResponse,
|
||||
AgentRunLogResponse,
|
||||
AgentTriggerRequest,
|
||||
ScoutCatalogItem,
|
||||
ScoutCreationCheckRequest,
|
||||
ScoutCreationCheckResponse,
|
||||
ScoutRunLogResponse,
|
||||
ScoutTriggerRequest,
|
||||
UserProfile,
|
||||
)
|
||||
|
||||
@@ -70,8 +70,8 @@ def _to_data_types(values: list[str]) -> list[str]:
|
||||
return result
|
||||
|
||||
|
||||
def _to_run_log_response(log: ScoutRunLog) -> AgentRunLogResponse:
|
||||
return AgentRunLogResponse(
|
||||
def _to_run_log_response(log: ScoutRunLog) -> ScoutRunLogResponse:
|
||||
return ScoutRunLogResponse(
|
||||
id=log.id,
|
||||
agent_id=log.scout_id,
|
||||
agent_type=log.scout_type, # type: ignore[arg-type]
|
||||
@@ -124,28 +124,28 @@ async def _enforce_run_frequency(
|
||||
|
||||
# ── Catalog ───────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/catalog", response_model=list[AgentCatalogItem])
|
||||
@router.get("/catalog", response_model=list[ScoutCatalogItem])
|
||||
async def get_agent_catalog(
|
||||
current_user: UserProfile = Depends(get_current_user),
|
||||
) -> list[AgentCatalogItem]:
|
||||
) -> list[ScoutCatalogItem]:
|
||||
"""Return the static list of available agent types and their descriptions."""
|
||||
return [
|
||||
AgentCatalogItem(
|
||||
ScoutCatalogItem(
|
||||
type="local_directory",
|
||||
name="Local Directory Monitor",
|
||||
description="Watches local directories, extracts data from files using AI",
|
||||
),
|
||||
AgentCatalogItem(
|
||||
ScoutCatalogItem(
|
||||
type="gmail",
|
||||
name="Gmail Connector",
|
||||
description="Scans Gmail inbox, extracts tasks/notes from emails",
|
||||
),
|
||||
AgentCatalogItem(
|
||||
ScoutCatalogItem(
|
||||
type="teams",
|
||||
name="Microsoft Teams Connector",
|
||||
description="Monitors Teams messages, extracts action items",
|
||||
),
|
||||
AgentCatalogItem(
|
||||
ScoutCatalogItem(
|
||||
type="outlook",
|
||||
name="Outlook Connector",
|
||||
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(
|
||||
body: AgentCreationCheckRequest,
|
||||
body: ScoutCreationCheckRequest,
|
||||
current_user: UserProfile = Depends(get_current_user),
|
||||
) -> AgentCreationCheckResponse:
|
||||
) -> ScoutCreationCheckResponse:
|
||||
"""Check if the user can create one more agent based on billing tier.
|
||||
|
||||
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"]
|
||||
allowed = limit == -1 or body.active_agents < limit
|
||||
return AgentCreationCheckResponse(
|
||||
return ScoutCreationCheckResponse(
|
||||
allowed=allowed,
|
||||
tier=current_user.tier,
|
||||
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(
|
||||
body: AgentTriggerRequest,
|
||||
body: ScoutTriggerRequest,
|
||||
current_user: UserProfile = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_session),
|
||||
) -> AgentRunLogResponse:
|
||||
) -> ScoutRunLogResponse:
|
||||
"""Trigger a local agent run using client-provided configuration."""
|
||||
_enforce_agent_limit(current_user.tier, body.active_agents)
|
||||
await _enforce_run_frequency(current_user.tier, current_user.id, db)
|
||||
|
||||
@@ -207,10 +207,10 @@ class WsStreamEnd(BaseModel):
|
||||
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."""
|
||||
|
||||
id: str
|
||||
@@ -220,34 +220,34 @@ class ContentTypeConfig(BaseModel):
|
||||
extraction_prompt: str
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
"""Structured agent configuration (replaces freeform prompt_template)."""
|
||||
class ScoutConfig(BaseModel):
|
||||
"""Structured scout configuration (replaces freeform prompt_template)."""
|
||||
|
||||
content_types: list[ContentTypeConfig] = []
|
||||
content_types: list[ScoutContentTypeConfig] = []
|
||||
global_rules: list[str] = []
|
||||
data_types: list[str] = []
|
||||
|
||||
|
||||
# ── Agent Catalog ─────────────────────────────────────────────────────
|
||||
# ── Scout Catalog ─────────────────────────────────────────────────────
|
||||
|
||||
class AgentCatalogItem(BaseModel):
|
||||
class ScoutCatalogItem(BaseModel):
|
||||
type: str
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class AgentCreationCheckRequest(BaseModel):
|
||||
class ScoutCreationCheckRequest(BaseModel):
|
||||
active_agents: int = Field(ge=0, default=0)
|
||||
|
||||
|
||||
class AgentCreationCheckResponse(BaseModel):
|
||||
class ScoutCreationCheckResponse(BaseModel):
|
||||
allowed: bool
|
||||
tier: BillingTier
|
||||
active_agents: int
|
||||
limit: int
|
||||
|
||||
|
||||
class AgentTriggerRequest(BaseModel):
|
||||
class ScoutTriggerRequest(BaseModel):
|
||||
directory: str = Field(min_length=1)
|
||||
device_id: str = Field(default="")
|
||||
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
|
||||
|
||||
|
||||
# ── Agent Run Log ─────────────────────────────────────────────────────
|
||||
# ── Scout Run Log ─────────────────────────────────────────────────────
|
||||
|
||||
class AgentRunLogResponse(BaseModel):
|
||||
class ScoutRunLogResponse(BaseModel):
|
||||
id: str
|
||||
agent_id: str
|
||||
agent_type: Literal["local", "cloud"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""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.
|
||||
|
||||
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
|
||||
|
||||
Cases 4.2–4.5 (multi-turn conversations producing a full AgentConfig) are
|
||||
Cases 4.2–4.5 (multi-turn conversations producing a full ScoutConfig) are
|
||||
non-deterministic and tested manually — results tracked in Langfuse.
|
||||
|
||||
Run:
|
||||
@@ -48,7 +48,7 @@ from app.api.routes.scout_setup import (
|
||||
)
|
||||
from app.core.langfuse_client import get_langfuse
|
||||
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
|
||||
|
||||
# ── Constants ─────────────────────────────────────────────────────────────
|
||||
@@ -179,7 +179,7 @@ def _evaluate_case(case: dict, reply: dict) -> tuple[float, str]:
|
||||
|
||||
def test_4_6a_extract_valid_json():
|
||||
"""_extract_agent_config: valid JSON between markers → returns serialised config."""
|
||||
config = AgentConfig(
|
||||
config = ScoutConfig(
|
||||
content_types=[],
|
||||
global_rules=["No project = no entity"],
|
||||
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"
|
||||
result = _extract_agent_config(text)
|
||||
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"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user