Log task planner conversations locally
Tests / Test apps.device-host-agent.tests.test_mcp_token.test_load_or_create_concurrent_calls_do_not_corrupt failed

This commit is contained in:
showtan001
2026-08-30 22:38:28 +08:00
parent 5458f3b8a4
commit dd8df33910
3 changed files with 40 additions and 10 deletions
+2 -1
View File
@@ -231,12 +231,14 @@ def create_application(
"MCP token generated at %s", mcp_token_path "MCP token generated at %s", mcp_token_path
) )
mcp_busy_tracker = McpBusyTracker(ttl_seconds=20.0) mcp_busy_tracker = McpBusyTracker(ttl_seconds=20.0)
conversation_log = ConversationLogStore(resolved_config.conversation_log_path) if resolved_config.mode == "local" else None
executor = AssignmentExecutor( executor = AssignmentExecutor(
create_execution_factories( create_execution_factories(
resolved_manager, resolved_manager,
metadata_store=metadata_store, metadata_store=metadata_store,
timeline=timeline, timeline=timeline,
host_agent_config=resolved_config, host_agent_config=resolved_config,
conversation_log=conversation_log,
), ),
mcp_busy_tracker=mcp_busy_tracker, mcp_busy_tracker=mcp_busy_tracker,
) )
@@ -246,7 +248,6 @@ def create_application(
status_tracker=status_tracker, status_tracker=status_tracker,
) )
planner_config = load_planner_config() planner_config = load_planner_config()
conversation_log = ConversationLogStore(resolved_config.conversation_log_path) if resolved_config.mode == "local" else None
conversation_agent = ( conversation_agent = (
ConversationAgent( ConversationAgent(
config=planner_config, config=planner_config,
+10 -2
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import os import os
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from typing import Any
from device.manager import DeviceManager from device.manager import DeviceManager
from host_agent.cloud_planner_client import CloudProxyToolCallingClient from host_agent.cloud_planner_client import CloudProxyToolCallingClient
@@ -35,6 +36,7 @@ def create_execution_factories(
metadata_store: TaskMetadataStore | None = None, metadata_store: TaskMetadataStore | None = None,
timeline: Timeline | None = None, timeline: Timeline | None = None,
host_agent_config: HostAgentConfig | None = None, host_agent_config: HostAgentConfig | None = None,
conversation_log: Any | None = None,
) -> ExecutionFactories: ) -> ExecutionFactories:
shared_workflow_store = workflow_store or WorkflowStore() shared_workflow_store = workflow_store or WorkflowStore()
resolved_host_agent_config = host_agent_config resolved_host_agent_config = host_agent_config
@@ -48,7 +50,10 @@ def create_execution_factories(
), ),
metadata_store=metadata_store, metadata_store=metadata_store,
timeline=timeline, timeline=timeline,
planner=_host_agent_planner(resolved_host_agent_config), planner=_host_agent_planner(
resolved_host_agent_config,
event_logger=conversation_log.append if conversation_log is not None else None,
),
planner_config=_host_agent_planner_config(), planner_config=_host_agent_planner_config(),
device_platform_provider=lambda device_id: _device_platform( device_platform_provider=lambda device_id: _device_platform(
manager, device_id manager, device_id
@@ -86,6 +91,8 @@ def _host_agent_planner_config() -> PlannerConfig:
def _host_agent_planner( def _host_agent_planner(
host_agent_config: HostAgentConfig | None, host_agent_config: HostAgentConfig | None,
*,
event_logger: Callable[[dict[str, Any]], None] | None = None,
) -> Planner | None: ) -> Planner | None:
"""Build the `AIPlanner` explicitly when the cloud-proxy transport is """Build the `AIPlanner` explicitly when the cloud-proxy transport is
selected, so its `ToolCallingClient` is a `CloudProxyToolCallingClient` selected, so its `ToolCallingClient` is a `CloudProxyToolCallingClient`
@@ -100,11 +107,12 @@ def _host_agent_planner(
resolved_config = host_agent_config or load_host_agent_config() resolved_config = host_agent_config or load_host_agent_config()
if resolved_config.ai_planner_transport != "cloud": if resolved_config.ai_planner_transport != "cloud":
return None return AIPlanner(config=planner_config, event_logger=event_logger)
return AIPlanner( return AIPlanner(
client=CloudProxyToolCallingClient(resolved_config), client=CloudProxyToolCallingClient(resolved_config),
config=planner_config, config=planner_config,
event_logger=event_logger,
) )
+28 -7
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from core.errors import TaskFailedError from core.errors import TaskFailedError
@@ -23,9 +24,11 @@ class AIPlanner(Planner):
*, *,
client: ToolCallingClient | None = None, client: ToolCallingClient | None = None,
config: PlannerConfig | None = None, config: PlannerConfig | None = None,
event_logger: Callable[[dict[str, Any]], None] | None = None,
) -> None: ) -> None:
self.config = config or load_config() self.config = config or load_config()
self.client = client or build_client(self.config) self.client = client or build_client(self.config)
self.event_logger = event_logger
def plan( def plan(
self, self,
@@ -49,13 +52,24 @@ class AIPlanner(Planner):
"corrected action or finish the task if it cannot proceed.\n" "corrected action or finish the task if it cannot proceed.\n"
f"Previous failure: {context.step_results[-1].error or 'unknown error'}" f"Previous failure: {context.step_results[-1].error or 'unknown error'}"
) )
decision = self.client.decide( self._log({"type": "llm_request", "task_id": context.task_id, "goal": goal,
system_prompt=PLANNER_SYSTEM_PROMPT, "system_prompt": PLANNER_SYSTEM_PROMPT, "user_prompt": user_prompt,
user_prompt=user_prompt, "has_screenshot": screenshot is not None})
screenshot=screenshot, try:
tools=ALL_TOOL_SPECS, decision = self.client.decide(
timeout=self.config.timeout, system_prompt=PLANNER_SYSTEM_PROMPT,
) user_prompt=user_prompt,
screenshot=screenshot,
tools=ALL_TOOL_SPECS,
timeout=self.config.timeout,
)
except Exception as exc:
self._log({"type": "agent_error", "task_id": context.task_id, "error": str(exc)})
raise
self._log({"type": "llm_response", "task_id": context.task_id,
"content": decision.text_output, "thinking": decision.thinking,
"tool_name": decision.tool_name, "arguments": decision.arguments,
"purpose": decision.purpose, "expected_outcome": decision.expected_outcome})
if decision.tool_name == FINISH_TASK_TOOL: if decision.tool_name == FINISH_TASK_TOOL:
if decision.arguments.get("success"): if decision.arguments.get("success"):
@@ -85,6 +99,13 @@ class AIPlanner(Planner):
# (mapped to an empty plan above), never via this hook. # (mapped to an empty plan above), never via this hook.
return False return False
def _log(self, event: dict[str, Any]) -> None:
if self.event_logger is not None:
try:
self.event_logger(event)
except Exception:
pass
def _history_summary(world: "WorldState | None") -> list[dict[str, Any]]: def _history_summary(world: "WorldState | None") -> list[dict[str, Any]]:
if world is None: if world is None: