Compare commits
6 Commits
70c19d3064
...
fbd308d288
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbd308d288 | ||
|
|
105cf52083 | ||
|
|
c2b27d4fb7 | ||
|
|
b92e72b685 | ||
|
|
1ccb0282fe | ||
|
|
1a20c11e86 |
41
alembic/versions/007_rename_agents_to_scouts.py
Normal file
41
alembic/versions/007_rename_agents_to_scouts.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Rename agents to scouts.
|
||||
|
||||
Revision ID: 007
|
||||
Revises: d6e3f4a5b6c7
|
||||
Create Date: 2026-05-15
|
||||
|
||||
Renames the entire agents subsystem identifiers to scouts.
|
||||
Pre-1.0 — no data preservation concerns beyond ALTER TABLE rename.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "007"
|
||||
down_revision: Union[str, None] = "d6e3f4a5b6c7"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Tables
|
||||
op.rename_table("local_agent_configs", "local_scout_configs")
|
||||
op.rename_table("cloud_agent_configs", "cloud_scout_configs")
|
||||
op.rename_table("agent_run_logs", "scout_run_logs")
|
||||
|
||||
# Columns
|
||||
op.alter_column("local_scout_configs", "agent_config", new_column_name="scout_config")
|
||||
op.alter_column("scout_run_logs", "agent_id", new_column_name="scout_id")
|
||||
op.alter_column("scout_run_logs", "agent_type", new_column_name="scout_type")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.alter_column("scout_run_logs", "scout_type", new_column_name="agent_type")
|
||||
op.alter_column("scout_run_logs", "scout_id", new_column_name="agent_id")
|
||||
op.alter_column("local_scout_configs", "scout_config", new_column_name="agent_config")
|
||||
|
||||
op.rename_table("scout_run_logs", "agent_run_logs")
|
||||
op.rename_table("cloud_scout_configs", "cloud_agent_configs")
|
||||
op.rename_table("local_scout_configs", "local_agent_configs")
|
||||
@@ -9,7 +9,7 @@ available during the WebSocket handshake).
|
||||
|
||||
Protocol:
|
||||
1. Client connects → JWT validated → connection accepted.
|
||||
2. Client sends ``device_hello`` frame: ``{ type, device_id, agent_ids }``.
|
||||
2. Client sends ``device_hello`` frame: ``{ type, device_id, scout_ids }``.
|
||||
3. Backend registers the connection in ``DeviceConnectionManager``.
|
||||
4. Session enters message dispatch loop + heartbeat.
|
||||
|
||||
@@ -39,10 +39,10 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy import update
|
||||
|
||||
from app.api.routes.agent_setup import handle_journey_message, handle_journey_start
|
||||
from app.api.routes.scout_setup import handle_journey_message, handle_journey_start
|
||||
from app.config.settings import settings
|
||||
from app.core.agent_runner import trigger_pending_runs
|
||||
from app.core.agent_session_buffer import session_buffer
|
||||
from app.core.scout_runner import trigger_pending_runs
|
||||
from app.core.scout_session_buffer import session_buffer
|
||||
from app.core.brief_agent import run_home_brief, run_project_brief
|
||||
from app.core.deep_agent import run_contextual_stream, run_home_stream, run_task_brief_research_stream
|
||||
from app.core.output_formatter import extract_canvas_block
|
||||
@@ -51,7 +51,7 @@ from app.core.memory_middleware import MemoryMiddleware
|
||||
from app.core.output_formatter import StreamFormatter
|
||||
from app.core.ws_context import clear_client_executor, set_client_executor
|
||||
from app.db import async_session
|
||||
from app.models import AgentRunLog
|
||||
from app.models import ScoutRunLog
|
||||
from app.schemas import WsFrameType, WsStreamEnd
|
||||
from app.schemas.contextual import ContextualScope, render_scope_block
|
||||
|
||||
@@ -100,7 +100,7 @@ async def device_ws(websocket: WebSocket) -> None:
|
||||
if hello.get("type") != WsFrameType.device_hello:
|
||||
raise ValueError("expected device_hello as first frame")
|
||||
device_id: str = hello["device_id"]
|
||||
agent_ids: list[str] = hello.get("agent_ids", [])
|
||||
scout_ids: list[str] = hello.get("scout_ids", [])
|
||||
except (KeyError, ValueError, json.JSONDecodeError) as exc:
|
||||
logger.warning("device_ws: invalid device_hello from user=%s: %s", user_id, exc)
|
||||
await websocket.close(code=1008)
|
||||
@@ -109,10 +109,10 @@ async def device_ws(websocket: WebSocket) -> None:
|
||||
# ── 3. Register connection ────────────────────────────────────────
|
||||
device_manager.register(user_id, device_id, websocket)
|
||||
logger.info(
|
||||
"device_ws: connected user=%s device=%s agents=%s",
|
||||
"device_ws: connected user=%s device=%s scouts=%s",
|
||||
user_id,
|
||||
device_id,
|
||||
agent_ids,
|
||||
scout_ids,
|
||||
)
|
||||
|
||||
# Trigger any overdue agent runs now that the device is connected.
|
||||
@@ -306,7 +306,7 @@ def get_session_buffer(user_id: str, session_id: str, channel: str = "contextual
|
||||
Defined at module level so tests can monkeypatch it.
|
||||
The channel kwarg is accepted for forward-compatibility.
|
||||
"""
|
||||
from app.core.agent_session_buffer import ContextualBufferProxy # noqa: PLC0415
|
||||
from app.core.scout_session_buffer import ContextualBufferProxy # noqa: PLC0415
|
||||
return ContextualBufferProxy(session_buffer, user_id, session_id)
|
||||
|
||||
|
||||
@@ -822,14 +822,14 @@ async def _heartbeat_loop(websocket: WebSocket) -> None:
|
||||
# ── Disconnect cleanup ────────────────────────────────────────────────
|
||||
|
||||
async def _mark_runs_disconnected(user_id: str) -> None:
|
||||
"""Mark all in-progress AgentRunLog rows as 'error' for this user."""
|
||||
"""Mark all in-progress ScoutRunLog rows as 'error' for this user."""
|
||||
try:
|
||||
async with async_session() as db:
|
||||
await db.execute(
|
||||
update(AgentRunLog)
|
||||
update(ScoutRunLog)
|
||||
.where(
|
||||
AgentRunLog.user_id == user_id,
|
||||
AgentRunLog.status == "running",
|
||||
ScoutRunLog.user_id == user_id,
|
||||
ScoutRunLog.status == "running",
|
||||
)
|
||||
.values(
|
||||
status="error",
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Agent routes.
|
||||
"""Scout routes.
|
||||
|
||||
Backend responsibilities are intentionally minimal:
|
||||
GET /agents/catalog — static catalog for UI display
|
||||
POST /agents/can-create — billing eligibility check
|
||||
POST /agents/trigger — trigger a local agent run
|
||||
GET /scouts/catalog — static catalog for UI display
|
||||
POST /scouts/can-create — billing eligibility check
|
||||
POST /scouts/trigger — trigger a local scout run
|
||||
|
||||
Agent configuration is owned by the Electron app and is not persisted
|
||||
in backend agent-config tables.
|
||||
Scout configuration is owned by the Electron app and is not persisted
|
||||
in backend scout-config tables.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,23 +24,23 @@ from pydantic import BaseModel
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.billing.tier_manager import FEATURES
|
||||
from app.core.agent_runner import is_agent_running, run_local_agent
|
||||
from app.core.scout_runner import is_agent_running, run_local_agent
|
||||
from app.core.device_manager import device_manager
|
||||
from app.core.note_summarizer import generate_note_summary
|
||||
from app.db import get_session
|
||||
from app.models import AgentRunLog, LocalAgentConfig
|
||||
from app.models import ScoutRunLog, LocalScoutConfig
|
||||
from app.schemas import (
|
||||
AgentCatalogItem,
|
||||
AgentCreationCheckRequest,
|
||||
AgentCreationCheckResponse,
|
||||
AgentRunLogResponse,
|
||||
AgentTriggerRequest,
|
||||
ScoutCatalogItem,
|
||||
ScoutCreationCheckRequest,
|
||||
ScoutCreationCheckResponse,
|
||||
ScoutRunLogResponse,
|
||||
ScoutTriggerRequest,
|
||||
UserProfile,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/agents", tags=["agents"])
|
||||
router = APIRouter(prefix="/scouts", tags=["scouts"])
|
||||
|
||||
|
||||
# ── Datetime helpers ──────────────────────────────────────────────────
|
||||
@@ -70,11 +70,11 @@ def _to_data_types(values: list[str]) -> list[str]:
|
||||
return result
|
||||
|
||||
|
||||
def _to_run_log_response(log: AgentRunLog) -> AgentRunLogResponse:
|
||||
return AgentRunLogResponse(
|
||||
def _to_run_log_response(log: ScoutRunLog) -> ScoutRunLogResponse:
|
||||
return ScoutRunLogResponse(
|
||||
id=log.id,
|
||||
agent_id=log.agent_id,
|
||||
agent_type=log.agent_type, # type: ignore[arg-type]
|
||||
agent_id=log.scout_id,
|
||||
agent_type=log.scout_type, # type: ignore[arg-type]
|
||||
status=log.status, # type: ignore[arg-type]
|
||||
items_processed=log.items_processed,
|
||||
items_created=log.items_created,
|
||||
@@ -108,9 +108,9 @@ async def _enforce_run_frequency(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
result = await db.execute(
|
||||
select(func.count(AgentRunLog.id)).where(
|
||||
AgentRunLog.user_id == user_id,
|
||||
AgentRunLog.started_at >= today_start,
|
||||
select(func.count(ScoutRunLog.id)).where(
|
||||
ScoutRunLog.user_id == user_id,
|
||||
ScoutRunLog.started_at >= today_start,
|
||||
)
|
||||
)
|
||||
runs_today: int = result.scalar_one()
|
||||
@@ -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)
|
||||
@@ -188,7 +188,7 @@ async def trigger_agent_run(
|
||||
if body.last_run_at
|
||||
else None
|
||||
)
|
||||
config = LocalAgentConfig(
|
||||
config = LocalScoutConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=current_user.id,
|
||||
device_id=body.device_id,
|
||||
@@ -196,7 +196,7 @@ async def trigger_agent_run(
|
||||
directory_paths=[body.directory],
|
||||
data_types=_to_data_types(body.what_to_extract),
|
||||
prompt_template=body.custom_agent_prompt or "",
|
||||
agent_config=body.agent_config,
|
||||
scout_config=body.agent_config,
|
||||
file_extensions=[],
|
||||
schedule_cron=body.batch_interval,
|
||||
enabled=True,
|
||||
@@ -212,9 +212,9 @@ async def trigger_agent_run(
|
||||
detail="Agent is already running. Only one run per agent is allowed at a time.",
|
||||
)
|
||||
|
||||
run_log = AgentRunLog(
|
||||
agent_id=stable_agent_id,
|
||||
agent_type="local",
|
||||
run_log = ScoutRunLog(
|
||||
scout_id=stable_agent_id,
|
||||
scout_type="local",
|
||||
user_id=current_user.id,
|
||||
status="running",
|
||||
)
|
||||
@@ -18,7 +18,7 @@ from app.agents.project_agent import PROJECT_TOOLS
|
||||
from app.agents.relations_agent import make_query_relations_tool
|
||||
from app.agents.task_agent import TASK_TOOLS
|
||||
from app.agents.timeline_agent import TIMELINE_TOOLS
|
||||
from app.core.agent_session_buffer import session_buffer
|
||||
from app.core.scout_session_buffer import session_buffer
|
||||
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.memory_middleware import MemoryMiddleware
|
||||
|
||||
@@ -48,7 +48,7 @@ from app.core.llm import get_agent_llm, model_for_agent
|
||||
from app.core.preprocessors import detect_content_type, preprocess
|
||||
from app.core.ws_context import clear_client_executor, execute_on_client, set_client_executor
|
||||
from app.db import async_session
|
||||
from app.models import AgentRunLog, CloudAgentConfig, LocalAgentConfig
|
||||
from app.models import ScoutRunLog, CloudScoutConfig, LocalScoutConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -169,7 +169,7 @@ def _is_overdue(schedule_cron: str, last_run_at: datetime | None) -> bool:
|
||||
next_run: datetime = cron.get_next(datetime)
|
||||
return now >= next_run
|
||||
except Exception as exc:
|
||||
logger.warning("agent_runner: cannot parse cron %r: %s", schedule_cron, exc)
|
||||
logger.warning("scout_runner: cannot parse cron %r: %s", schedule_cron, exc)
|
||||
return False
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ async def _run_agent_with_tools(
|
||||
call_name = str(call.get("name", ""))
|
||||
call_args = call.get("args", {})
|
||||
logger.info(
|
||||
"agent_runner: tool_call name=%s args=%s",
|
||||
"scout_runner: tool_call name=%s args=%s",
|
||||
call_name,
|
||||
json.dumps(call_args, ensure_ascii=True)[:800],
|
||||
)
|
||||
@@ -305,7 +305,7 @@ async def _run_agent_with_tools(
|
||||
tool_output = await tool_fn.ainvoke(call_args)
|
||||
|
||||
logger.info(
|
||||
"agent_runner: tool_result name=%s output=%s",
|
||||
"scout_runner: tool_result name=%s output=%s",
|
||||
call_name,
|
||||
str(tool_output)[:200],
|
||||
)
|
||||
@@ -360,7 +360,7 @@ async def _scan_directories(
|
||||
try:
|
||||
result = await execute_on_client(action="list_directory", data={"path": path})
|
||||
except Exception as exc:
|
||||
logger.warning("agent_runner: list_directory failed %r: %s", path, exc)
|
||||
logger.warning("scout_runner: list_directory failed %r: %s", path, exc)
|
||||
return
|
||||
for entry in result.get("entries", []):
|
||||
entry_path = entry.get("path", "")
|
||||
@@ -414,7 +414,7 @@ async def _fetch_projects() -> list[dict]:
|
||||
result = await execute_on_client(action="select", table="projects")
|
||||
return result.get("rows", [])
|
||||
except Exception as exc:
|
||||
logger.warning("agent_runner: failed to fetch projects: %s", exc)
|
||||
logger.warning("scout_runner: failed to fetch projects: %s", exc)
|
||||
return []
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ async def _fetch_domain_entities(domain: str, project_id: str) -> list[dict]:
|
||||
)
|
||||
return result.get("rows", [])
|
||||
except Exception as exc:
|
||||
logger.warning("agent_runner: failed to fetch %s: %s", domain, exc)
|
||||
logger.warning("scout_runner: failed to fetch %s: %s", domain, exc)
|
||||
return []
|
||||
|
||||
|
||||
@@ -555,8 +555,8 @@ def _get_no_match_behavior(agent_config: dict) -> str:
|
||||
|
||||
async def run_local_agent(
|
||||
user_id: str,
|
||||
config: LocalAgentConfig,
|
||||
run_log: AgentRunLog,
|
||||
config: LocalScoutConfig,
|
||||
run_log: ScoutRunLog,
|
||||
device_mgr: DeviceConnectionManager,
|
||||
run_context: dict | None = None,
|
||||
) -> None:
|
||||
@@ -586,7 +586,7 @@ async def run_local_agent(
|
||||
|
||||
if not is_online:
|
||||
logger.info(
|
||||
"agent_runner: skip run=%s — device %r offline for user=%s",
|
||||
"scout_runner: skip run=%s — device %r offline for user=%s",
|
||||
run_id,
|
||||
target_device_id or "<any>",
|
||||
user_id,
|
||||
@@ -605,7 +605,7 @@ async def run_local_agent(
|
||||
errors: list[str] = []
|
||||
items_processed = 0
|
||||
items_created = 0
|
||||
agent_config: dict = config.agent_config or {}
|
||||
agent_config: dict = config.scout_config or {}
|
||||
processing_tools = _build_processing_tools(config.data_types)
|
||||
|
||||
try:
|
||||
@@ -616,7 +616,7 @@ async def run_local_agent(
|
||||
last_run_at=config.last_run_at,
|
||||
)
|
||||
logger.info(
|
||||
"agent_runner: run=%s found %d file(s) after filtering", run_id, len(file_paths)
|
||||
"scout_runner: run=%s found %d file(s) after filtering", run_id, len(file_paths)
|
||||
)
|
||||
|
||||
if not file_paths:
|
||||
@@ -641,7 +641,7 @@ async def run_local_agent(
|
||||
raw_content: str = file_result.get("content", "")
|
||||
if not raw_content.strip():
|
||||
logger.debug(
|
||||
"agent_runner: run=%s skipping empty file %r", run_id, file_path
|
||||
"scout_runner: run=%s skipping empty file %r", run_id, file_path
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -651,7 +651,7 @@ async def run_local_agent(
|
||||
preprocessed = preprocess(content_type, raw_content)
|
||||
|
||||
logger.info(
|
||||
"agent_runner: run=%s file=%r content_type=%s clean_len=%d",
|
||||
"scout_runner: run=%s file=%r content_type=%s clean_len=%d",
|
||||
run_id, file_path, content_type, len(preprocessed.clean_text),
|
||||
)
|
||||
|
||||
@@ -711,19 +711,19 @@ async def run_local_agent(
|
||||
projects_block = _format_projects(projects)
|
||||
|
||||
logger.info(
|
||||
"agent_runner: run=%s file=%r created=%d result=%s",
|
||||
"scout_runner: run=%s file=%r created=%d result=%s",
|
||||
run_id, file_path, file_created, result_text[:200],
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
errors.append(f"Error processing '{file_path}': {exc}")
|
||||
logger.error(
|
||||
"agent_runner: run=%s file=%r failed: %s", run_id, file_path, exc
|
||||
"scout_runner: run=%s file=%r failed: %s", run_id, file_path, exc
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
errors.append(f"Agent run failed: {exc}")
|
||||
logger.error("agent_runner: run=%s failed: %s", run_id, exc)
|
||||
logger.error("scout_runner: run=%s failed: %s", run_id, exc)
|
||||
finally:
|
||||
_running_agents.discard(agent_id)
|
||||
clear_client_executor()
|
||||
@@ -744,7 +744,7 @@ async def run_local_agent(
|
||||
errors=errors,
|
||||
)
|
||||
logger.info(
|
||||
"agent_runner: run=%s done status=%s processed=%d created=%d errors=%d",
|
||||
"scout_runner: run=%s done status=%s processed=%d created=%d errors=%d",
|
||||
run_id,
|
||||
final_status,
|
||||
items_processed,
|
||||
@@ -762,7 +762,7 @@ async def run_local_agent(
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"agent_runner: run=%s failed to send run_complete: %s", run_id, exc
|
||||
"scout_runner: run=%s failed to send run_complete: %s", run_id, exc
|
||||
)
|
||||
|
||||
|
||||
@@ -773,8 +773,8 @@ _CLOUD_DEFAULT_LOOKBACK_DAYS: int = 7
|
||||
|
||||
async def run_cloud_agent(
|
||||
user_id: str,
|
||||
config: CloudAgentConfig,
|
||||
run_log: AgentRunLog,
|
||||
config: CloudScoutConfig,
|
||||
run_log: ScoutRunLog,
|
||||
device_mgr: DeviceConnectionManager,
|
||||
) -> None:
|
||||
"""Execute a cloud connector agent run end-to-end.
|
||||
@@ -797,7 +797,7 @@ async def run_cloud_agent(
|
||||
# ── 1. Device online check ─────────────────────────────────────────
|
||||
if not device_mgr.is_online(user_id):
|
||||
logger.info(
|
||||
"agent_runner: skip cloud run=%s — no device online for user=%s",
|
||||
"scout_runner: skip cloud run=%s — no device online for user=%s",
|
||||
run_id,
|
||||
user_id,
|
||||
)
|
||||
@@ -822,7 +822,7 @@ async def run_cloud_agent(
|
||||
try:
|
||||
credentials_info = decrypt_token(config.oauth_token_encrypted)
|
||||
except ValueError as exc:
|
||||
logger.error("agent_runner: failed to decrypt OAuth token for agent %s: %s", config.id, exc)
|
||||
logger.error("scout_runner: failed to decrypt OAuth token for agent %s: %s", config.id, exc)
|
||||
await _finalize_run(
|
||||
run_log,
|
||||
status="error",
|
||||
@@ -868,7 +868,7 @@ async def run_cloud_agent(
|
||||
raw_messages = []
|
||||
except RuntimeError as exc:
|
||||
logger.error(
|
||||
"agent_runner: provider fetch failed for cloud agent %s: %s", config.id, exc
|
||||
"scout_runner: provider fetch failed for cloud agent %s: %s", config.id, exc
|
||||
)
|
||||
await _finalize_run(
|
||||
run_log,
|
||||
@@ -881,7 +881,7 @@ async def run_cloud_agent(
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"agent_runner: cloud agent %s fetched %d item(s) from %s for user=%s",
|
||||
"scout_runner: cloud agent %s fetched %d item(s) from %s for user=%s",
|
||||
config.id,
|
||||
len(raw_messages),
|
||||
config.provider,
|
||||
@@ -941,16 +941,16 @@ async def run_cloud_agent(
|
||||
new_encrypted = encrypt_token(refreshed)
|
||||
async with async_session() as db:
|
||||
cfg_result = await db.execute(
|
||||
select(CloudAgentConfig).where(CloudAgentConfig.id == config.id)
|
||||
select(CloudScoutConfig).where(CloudScoutConfig.id == config.id)
|
||||
)
|
||||
cfg_row = cfg_result.scalar_one_or_none()
|
||||
if cfg_row:
|
||||
cfg_row.oauth_token_encrypted = new_encrypted
|
||||
await db.commit()
|
||||
logger.debug("agent_runner: refreshed OAuth token persisted for agent %s", config.id)
|
||||
logger.debug("scout_runner: refreshed OAuth token persisted for agent %s", config.id)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"agent_runner: failed to persist refreshed token for agent %s: %s",
|
||||
"scout_runner: failed to persist refreshed token for agent %s: %s",
|
||||
config.id,
|
||||
exc,
|
||||
)
|
||||
@@ -974,7 +974,7 @@ async def run_cloud_agent(
|
||||
config_type="cloud",
|
||||
)
|
||||
logger.info(
|
||||
"agent_runner: cloud run=%s done status=%s processed=%d created=%d errors=%d",
|
||||
"scout_runner: cloud run=%s done status=%s processed=%d created=%d errors=%d",
|
||||
run_id,
|
||||
final_status,
|
||||
items_processed,
|
||||
@@ -996,7 +996,7 @@ async def trigger_pending_runs(
|
||||
Called as a background task from the device WS endpoint on ``device_hello``.
|
||||
"""
|
||||
logger.info(
|
||||
"agent_runner: pending-run scan skipped for user=%s device=%s (client-owned agent config)",
|
||||
"scout_runner: pending-run scan skipped for user=%s device=%s (client-owned agent config)",
|
||||
user_id,
|
||||
device_id,
|
||||
)
|
||||
@@ -1007,7 +1007,7 @@ async def trigger_pending_runs(
|
||||
|
||||
|
||||
async def _finalize_run(
|
||||
run_log: AgentRunLog,
|
||||
run_log: ScoutRunLog,
|
||||
*,
|
||||
status: str,
|
||||
items_processed: int = 0,
|
||||
@@ -1031,14 +1031,14 @@ async def _finalize_run(
|
||||
if update_config_last_run and config_id:
|
||||
if config_type == "local":
|
||||
cfg_result = await db.execute(
|
||||
select(LocalAgentConfig).where(LocalAgentConfig.id == config_id)
|
||||
select(LocalScoutConfig).where(LocalScoutConfig.id == config_id)
|
||||
)
|
||||
cfg = cfg_result.scalar_one_or_none()
|
||||
if cfg:
|
||||
cfg.last_run_at = now
|
||||
elif config_type == "cloud":
|
||||
cfg_result = await db.execute(
|
||||
select(CloudAgentConfig).where(CloudAgentConfig.id == config_id)
|
||||
select(CloudScoutConfig).where(CloudScoutConfig.id == config_id)
|
||||
)
|
||||
cfg = cfg_result.scalar_one_or_none()
|
||||
if cfg:
|
||||
@@ -1047,5 +1047,5 @@ async def _finalize_run(
|
||||
await db.commit()
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"agent_runner: failed to finalize run_log=%s: %s", run_log.id, exc
|
||||
"scout_runner: failed to finalize run_log=%s: %s", run_log.id, exc
|
||||
)
|
||||
@@ -8,7 +8,7 @@ blocking the event loop.
|
||||
Token refresh is handled transparently: when the stored access token has
|
||||
expired, ``google.auth.transport.requests.Request`` will use the refresh
|
||||
token to obtain a fresh one. The caller is responsible for persisting
|
||||
any refreshed credentials back to ``CloudAgentConfig.oauth_token_encrypted``
|
||||
any refreshed credentials back to ``CloudScoutConfig.oauth_token_encrypted``
|
||||
(see ``agent_runner.run_cloud_agent``).
|
||||
|
||||
Credential dict shape (Google OAuth2):
|
||||
|
||||
@@ -124,12 +124,12 @@ def create_app() -> FastAPI:
|
||||
app.add_middleware(SanitizerMiddleware)
|
||||
app.add_middleware(TierRateLimitMiddleware)
|
||||
|
||||
from app.api.routes import agents, auth, billing, chat, device_ws, memory
|
||||
from app.api.routes import scouts, auth, billing, chat, device_ws, memory
|
||||
|
||||
app.include_router(auth.router, prefix="/api/v1")
|
||||
app.include_router(chat.router, prefix="/api/v1")
|
||||
app.include_router(billing.router, prefix="/api/v1")
|
||||
app.include_router(agents.router, prefix="/api/v1")
|
||||
app.include_router(scouts.router, prefix="/api/v1")
|
||||
app.include_router(device_ws.router, prefix="/api/v1")
|
||||
app.include_router(memory.router, prefix="/api/v1")
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"""SQLAlchemy ORM models for all persistent tables.
|
||||
|
||||
Only auth, billing, agent config, and memory data live here.
|
||||
Only auth, billing, scout config, and memory data live here.
|
||||
User content (notes, tasks, etc.) lives exclusively on the client.
|
||||
|
||||
Table inventory:
|
||||
users — account credentials + tier
|
||||
refresh_tokens — hashed refresh token store
|
||||
subscriptions — Stripe subscription records
|
||||
local_agent_configs — per-device batch agent configs
|
||||
cloud_agent_configs — OAuth-backed cloud agent configs
|
||||
agent_run_logs — execution history for all agents
|
||||
local_scout_configs — per-device batch scout configs
|
||||
cloud_scout_configs — OAuth-backed cloud scout configs
|
||||
scout_run_logs — execution history for all scouts
|
||||
memory_core — per-user persistent key/value preferences (encrypted)
|
||||
memory_associative — per-user semantic memory with embeddings (encrypted)
|
||||
memory_episodic — per-user session summaries (encrypted)
|
||||
@@ -158,8 +158,8 @@ class Subscription(Base):
|
||||
user: Mapped[User] = relationship(back_populates="subscription")
|
||||
|
||||
|
||||
class LocalAgentConfig(Base):
|
||||
__tablename__ = "local_agent_configs"
|
||||
class LocalScoutConfig(Base):
|
||||
__tablename__ = "local_scout_configs"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
||||
@@ -172,7 +172,7 @@ class LocalAgentConfig(Base):
|
||||
directory_paths: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
||||
data_types: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
||||
prompt_template: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
agent_config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
scout_config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
file_extensions: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
||||
schedule_cron: Mapped[str] = mapped_column(String(100), nullable=False, default="0 */6 * * *")
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
@@ -184,17 +184,17 @@ class LocalAgentConfig(Base):
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
run_logs: Mapped[list[AgentRunLog]] = relationship(
|
||||
back_populates="local_agent",
|
||||
primaryjoin="and_(AgentRunLog.agent_id == LocalAgentConfig.id, AgentRunLog.agent_type == 'local')",
|
||||
foreign_keys="AgentRunLog.agent_id",
|
||||
run_logs: Mapped[list["ScoutRunLog"]] = relationship(
|
||||
back_populates="local_scout",
|
||||
primaryjoin="and_(ScoutRunLog.scout_id == LocalScoutConfig.id, ScoutRunLog.scout_type == 'local')",
|
||||
foreign_keys="ScoutRunLog.scout_id",
|
||||
cascade="all, delete-orphan",
|
||||
overlaps="run_logs,cloud_agent",
|
||||
overlaps="run_logs,cloud_scout",
|
||||
)
|
||||
|
||||
|
||||
class CloudAgentConfig(Base):
|
||||
__tablename__ = "cloud_agent_configs"
|
||||
class CloudScoutConfig(Base):
|
||||
__tablename__ = "cloud_scout_configs"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
||||
@@ -218,25 +218,25 @@ class CloudAgentConfig(Base):
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
run_logs: Mapped[list[AgentRunLog]] = relationship(
|
||||
back_populates="cloud_agent",
|
||||
primaryjoin="and_(AgentRunLog.agent_id == CloudAgentConfig.id, AgentRunLog.agent_type == 'cloud')",
|
||||
foreign_keys="AgentRunLog.agent_id",
|
||||
run_logs: Mapped[list["ScoutRunLog"]] = relationship(
|
||||
back_populates="cloud_scout",
|
||||
primaryjoin="and_(ScoutRunLog.scout_id == CloudScoutConfig.id, ScoutRunLog.scout_type == 'cloud')",
|
||||
foreign_keys="ScoutRunLog.scout_id",
|
||||
cascade="all, delete-orphan",
|
||||
overlaps="run_logs,local_agent",
|
||||
overlaps="run_logs,local_scout",
|
||||
)
|
||||
|
||||
|
||||
class AgentRunLog(Base):
|
||||
__tablename__ = "agent_run_logs"
|
||||
class ScoutRunLog(Base):
|
||||
__tablename__ = "scout_run_logs"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
||||
)
|
||||
# Plain string — not a FK because it references either local_agent_configs or cloud_agent_configs
|
||||
# depending on agent_type. Query by (agent_id, agent_type) to locate the source config.
|
||||
agent_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
agent_type: Mapped[str] = mapped_column(AgentTypeEnum, nullable=False)
|
||||
# Plain string — not a FK because it references either local_scout_configs or cloud_scout_configs
|
||||
# depending on scout_type. Query by (scout_id, scout_type) to locate the source config.
|
||||
scout_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
scout_type: Mapped[str] = mapped_column(AgentTypeEnum, nullable=False)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
@@ -250,17 +250,17 @@ class AgentRunLog(Base):
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
local_agent: Mapped[LocalAgentConfig | None] = relationship(
|
||||
local_scout: Mapped["LocalScoutConfig | None"] = relationship(
|
||||
back_populates="run_logs",
|
||||
primaryjoin="and_(AgentRunLog.agent_id == LocalAgentConfig.id, AgentRunLog.agent_type == 'local')",
|
||||
foreign_keys="AgentRunLog.agent_id",
|
||||
overlaps="run_logs,cloud_agent",
|
||||
primaryjoin="and_(ScoutRunLog.scout_id == LocalScoutConfig.id, ScoutRunLog.scout_type == 'local')",
|
||||
foreign_keys="ScoutRunLog.scout_id",
|
||||
overlaps="run_logs,cloud_scout",
|
||||
)
|
||||
cloud_agent: Mapped[CloudAgentConfig | None] = relationship(
|
||||
cloud_scout: Mapped["CloudScoutConfig | None"] = relationship(
|
||||
back_populates="run_logs",
|
||||
primaryjoin="and_(AgentRunLog.agent_id == CloudAgentConfig.id, AgentRunLog.agent_type == 'cloud')",
|
||||
foreign_keys="AgentRunLog.agent_id",
|
||||
overlaps="run_logs,local_agent",
|
||||
primaryjoin="and_(ScoutRunLog.scout_id == CloudScoutConfig.id, ScoutRunLog.scout_type == 'cloud')",
|
||||
foreign_keys="ScoutRunLog.scout_id",
|
||||
overlaps="run_logs,local_scout",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ class WsDeviceHello(BaseModel):
|
||||
|
||||
type: Literal[WsFrameType.device_hello] = WsFrameType.device_hello
|
||||
device_id: str
|
||||
agent_ids: list[str] = Field(default_factory=list)
|
||||
scout_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -35,7 +35,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from app.core.agent_runner import (
|
||||
from app.core.scout_runner import (
|
||||
_format_metadata,
|
||||
_format_projects,
|
||||
_get_extraction_rules,
|
||||
@@ -44,7 +44,7 @@ from app.core.agent_runner import (
|
||||
)
|
||||
from app.core.device_manager import DeviceConnectionManager
|
||||
from app.core.langfuse_client import get_langfuse
|
||||
from app.models import AgentRunLog, LocalAgentConfig
|
||||
from app.models import ScoutRunLog, LocalScoutConfig
|
||||
from tests.conftest import TEST_USER_IDS
|
||||
|
||||
# ── Constants ─────────────────────────────────────────────────────────────
|
||||
@@ -127,8 +127,8 @@ def _make_config(
|
||||
agent_config: dict | None = None,
|
||||
directory: str = "/emails",
|
||||
device_id: str = "dev-001",
|
||||
) -> LocalAgentConfig:
|
||||
return LocalAgentConfig(
|
||||
) -> LocalScoutConfig:
|
||||
return LocalScoutConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=_USER_ID,
|
||||
device_id=device_id,
|
||||
@@ -136,7 +136,7 @@ def _make_config(
|
||||
directory_paths=[directory],
|
||||
data_types=["tasks", "notes", "timelines"],
|
||||
prompt_template="",
|
||||
agent_config=agent_config or _AGENT_CONFIG,
|
||||
scout_config=agent_config or _AGENT_CONFIG,
|
||||
file_extensions=[".html", ".eml"],
|
||||
schedule_cron="0 */6 * * *",
|
||||
enabled=True,
|
||||
@@ -144,11 +144,11 @@ def _make_config(
|
||||
)
|
||||
|
||||
|
||||
def _make_run_log(agent_id: str) -> AgentRunLog:
|
||||
return AgentRunLog(
|
||||
def _make_run_log(agent_id: str) -> ScoutRunLog:
|
||||
return ScoutRunLog(
|
||||
id=str(uuid.uuid4()),
|
||||
agent_id=agent_id,
|
||||
agent_type="local",
|
||||
scout_id=agent_id,
|
||||
scout_type="local",
|
||||
user_id=_USER_ID,
|
||||
status="running",
|
||||
started_at=datetime.now(timezone.utc),
|
||||
@@ -271,7 +271,7 @@ async def test_2_9_device_offline():
|
||||
run_log = _make_run_log(config.id)
|
||||
mgr = _make_manager(online=False)
|
||||
|
||||
with patch("app.core.agent_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
with patch("app.core.scout_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
await run_local_agent(_USER_ID, config, run_log, mgr)
|
||||
|
||||
_, kwargs = mock_fin.call_args
|
||||
@@ -295,8 +295,8 @@ async def test_2_10_empty_file():
|
||||
projects=[_PROJECTS["alpha"]],
|
||||
)
|
||||
|
||||
with patch("app.core.agent_runner._make_agent_executor", return_value=executor), \
|
||||
patch("app.core.agent_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
with patch("app.core.scout_runner._make_agent_executor", return_value=executor), \
|
||||
patch("app.core.scout_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
await run_local_agent(_USER_ID, config, run_log, mgr)
|
||||
|
||||
_, kwargs = mock_fin.call_args
|
||||
@@ -326,9 +326,9 @@ async def test_2_8_items_created_count():
|
||||
_tool_calls_out.extend(["create_task", "create_note", "update_task"])
|
||||
return "Done."
|
||||
|
||||
with patch("app.core.agent_runner._make_agent_executor", return_value=executor), \
|
||||
patch("app.core.agent_runner._run_agent_with_tools", side_effect=mock_run_agent), \
|
||||
patch("app.core.agent_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
with patch("app.core.scout_runner._make_agent_executor", return_value=executor), \
|
||||
patch("app.core.scout_runner._run_agent_with_tools", side_effect=mock_run_agent), \
|
||||
patch("app.core.scout_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
await run_local_agent(_USER_ID, config, run_log, mgr)
|
||||
|
||||
_, kwargs = mock_fin.call_args
|
||||
@@ -377,8 +377,8 @@ async def test_eval_runner(runner_case, pytestconfig):
|
||||
) if lf else nullcontext()
|
||||
|
||||
with obs_ctx as obs:
|
||||
with patch("app.core.agent_runner._make_agent_executor", return_value=executor), \
|
||||
patch("app.core.agent_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
with patch("app.core.scout_runner._make_agent_executor", return_value=executor), \
|
||||
patch("app.core.scout_runner._finalize_run", new_callable=AsyncMock) as mock_fin:
|
||||
await run_local_agent(_USER_ID, config, run_log, mgr)
|
||||
|
||||
_, kwargs = mock_fin.call_args
|
||||
|
||||
@@ -22,7 +22,7 @@ import pytest
|
||||
from app.core.device_manager import DeviceConnectionManager
|
||||
from app.db import get_session
|
||||
from app.main import app
|
||||
from app.models import AgentRunLog
|
||||
from app.models import ScoutRunLog
|
||||
from tests.conftest import TEST_USER_IDS, make_jwt
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -33,9 +33,9 @@ _FREE_UID = TEST_USER_IDS["free"]
|
||||
_PRO_UID = TEST_USER_IDS["pro"]
|
||||
|
||||
|
||||
def _device_hello(device_id: str = "dev-001", agent_ids: list[str] | None = None) -> str:
|
||||
def _device_hello(device_id: str = "dev-001", scout_ids: list[str] | None = None) -> str:
|
||||
return json.dumps(
|
||||
{"type": "device_hello", "device_id": device_id, "agent_ids": agent_ids or []}
|
||||
{"type": "device_hello", "device_id": device_id, "scout_ids": scout_ids or []}
|
||||
)
|
||||
|
||||
|
||||
@@ -262,10 +262,10 @@ async def test_mark_runs_disconnected_updates_db(db_session):
|
||||
|
||||
user_id = TEST_USER_IDS["free"]
|
||||
|
||||
run_log = AgentRunLog(
|
||||
run_log = ScoutRunLog(
|
||||
id=str(uuid.uuid4()),
|
||||
agent_id=str(uuid.uuid4()),
|
||||
agent_type="local",
|
||||
scout_id=str(uuid.uuid4()),
|
||||
scout_type="local",
|
||||
user_id=user_id,
|
||||
status="running",
|
||||
started_at=datetime.now(timezone.utc),
|
||||
@@ -280,7 +280,7 @@ async def test_mark_runs_disconnected_updates_db(db_session):
|
||||
# Verify through the same session factory.
|
||||
async with _TestSessionLocal() as s:
|
||||
result = await s.execute(
|
||||
select(AgentRunLog).where(AgentRunLog.id == run_log.id)
|
||||
select(ScoutRunLog).where(ScoutRunLog.id == run_log.id)
|
||||
)
|
||||
updated = result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -37,7 +37,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from app.api.routes.agent_setup import (
|
||||
from app.api.routes.scout_setup import (
|
||||
_CONFIG_END,
|
||||
_CONFIG_START,
|
||||
_MAX_TURNS,
|
||||
@@ -48,7 +48,7 @@ from app.api.routes.agent_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"]
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ async def test_4_6f_nudge_uses_new_markers():
|
||||
# Return plain text — no markers — to trigger the nudge path.
|
||||
return "I still need more information from you."
|
||||
|
||||
from app.api.routes.agent_setup import JourneySession
|
||||
from app.api.routes.scout_setup import JourneySession
|
||||
|
||||
fake_session = JourneySession(
|
||||
session_id=session_id,
|
||||
@@ -248,7 +248,7 @@ async def test_4_6f_nudge_uses_new_markers():
|
||||
_sessions[session_id] = fake_session
|
||||
|
||||
try:
|
||||
with patch("app.api.routes.agent_setup._call_llm_with_tools", side_effect=_mock_llm):
|
||||
with patch("app.api.routes.scout_setup._call_llm_with_tools", side_effect=_mock_llm):
|
||||
await handle_journey_message(_USER_ID, {
|
||||
"session_id": session_id,
|
||||
"message": "one more message to trigger nudge",
|
||||
|
||||
@@ -322,7 +322,7 @@ def test_home_request_calls_memory_middleware(client):
|
||||
):
|
||||
with client.websocket_connect(f"/api/v1/ws/device?token={token}") as ws:
|
||||
ws.send_text(json.dumps({
|
||||
"type": "device_hello", "device_id": "dev-mem", "agent_ids": []
|
||||
"type": "device_hello", "device_id": "dev-mem", "scout_ids": []
|
||||
}))
|
||||
ws.send_text(json.dumps({
|
||||
"type": "home_request",
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_home_request_produces_stream_frames(client):
|
||||
with patch("app.api.routes.device_ws.run_home_stream", side_effect=_mock_home_stream):
|
||||
with client.websocket_connect(f"/api/v1/ws/device?token={token}") as ws:
|
||||
ws.send_text(json.dumps({
|
||||
"type": "device_hello", "device_id": "dev-1", "agent_ids": []
|
||||
"type": "device_hello", "device_id": "dev-1", "scout_ids": []
|
||||
}))
|
||||
ws.send_text(json.dumps({
|
||||
"type": "home_request",
|
||||
@@ -85,7 +85,7 @@ def test_home_request_request_id_propagated(client):
|
||||
with patch("app.api.routes.device_ws.run_home_stream", side_effect=_stream):
|
||||
with client.websocket_connect(f"/api/v1/ws/device?token={token}") as ws:
|
||||
ws.send_text(json.dumps({
|
||||
"type": "device_hello", "device_id": "dev-3", "agent_ids": []
|
||||
"type": "device_hello", "device_id": "dev-3", "scout_ids": []
|
||||
}))
|
||||
ws.send_text(json.dumps({
|
||||
"type": "home_request",
|
||||
@@ -106,7 +106,7 @@ def test_tool_result_dispatch_silent_on_unknown_id(client):
|
||||
with patch("app.api.routes.device_ws._HEARTBEAT_INTERVAL", 0.05):
|
||||
with client.websocket_connect(f"/api/v1/ws/device?token={token}") as ws:
|
||||
ws.send_text(json.dumps({
|
||||
"type": "device_hello", "device_id": "dev-4", "agent_ids": []
|
||||
"type": "device_hello", "device_id": "dev-4", "scout_ids": []
|
||||
}))
|
||||
ws.send_text(json.dumps({
|
||||
"type": "tool_result", "id": "no-such-id", "ok": True
|
||||
|
||||
Reference in New Issue
Block a user