Support multimodal planner scenes without OCR

This commit is contained in:
showtan001
2026-08-30 21:56:11 +08:00
parent 5b8daab457
commit 44e1a6651a
3 changed files with 23 additions and 1 deletions
+5
View File
@@ -72,6 +72,7 @@ export AI_PLANNER_PROVIDER=openai-compatible
export AI_PLANNER_MODEL=qwen2.5 export AI_PLANNER_MODEL=qwen2.5
export AI_PLANNER_API_KEY=local-key export AI_PLANNER_API_KEY=local-key
export AI_PLANNER_BASE_URL=http://127.0.0.1:11434/v1 export AI_PLANNER_BASE_URL=http://127.0.0.1:11434/v1
export AI_PLANNER_MULTIMODAL=true
uv run --package device-host-agent device-host-agent setup uv run --package device-host-agent device-host-agent setup
uv run --package device-host-agent device-host-agent uv run --package device-host-agent device-host-agent
``` ```
@@ -115,6 +116,10 @@ The compact form `{ "role": "user", "text": "...", "image_base64": "..." }`
is also accepted. The model can inspect the supplied image and then call a is also accepted. The model can inspect the supplied image and then call a
phone tool such as `tap` in the same conversation. phone tool such as `tap` in the same conversation.
Set `AI_PLANNER_MULTIMODAL=true` for a vision model. The Planner sends the
screenshot and omits OCR-only elements and OCR metadata from the structured
scene payload, avoiding duplicate OCR text.
Local mode records LLM responses, reasoning fields, tool calls, tool results, Local mode records LLM responses, reasoning fields, tool calls, tool results,
and final replies in a local SQLite database. View them at and final replies in a local SQLite database. View them at
`http://127.0.0.1:8765/conversations`; image bytes are excluded. Set `http://127.0.0.1:8765/conversations`; image bytes are excluded. Set
+15 -1
View File
@@ -36,9 +36,10 @@ class AIPlanner(Planner):
world: "WorldState | None" = None, world: "WorldState | None" = None,
screenshot: bytes | None = None, screenshot: bytes | None = None,
) -> list[PlannedStep]: ) -> list[PlannedStep]:
scene_json = _without_ocr(scene.to_dict()) if self.config.multimodal else scene.to_dict()
user_prompt = planner_user_prompt( user_prompt = planner_user_prompt(
goal=goal, goal=goal,
scene_json=scene.to_dict(), scene_json=scene_json,
history_summary=_history_summary(world), history_summary=_history_summary(world),
device_platform=context.device_platform, device_platform=context.device_platform,
) )
@@ -94,3 +95,16 @@ def _history_summary(world: "WorldState | None") -> list[dict[str, Any]]:
} }
for event in world.history for event in world.history
] ]
def _without_ocr(scene_json: dict[str, Any]) -> dict[str, Any]:
cleaned = dict(scene_json)
elements = cleaned.get("elements")
if isinstance(elements, list):
cleaned["elements"] = [
{key: value for key, value in element.items() if key not in {"source", "confidence", "foreground_color", "background_color"}}
for element in elements
if isinstance(element, dict) and element.get("source") != "ocr"
]
cleaned.pop("ocr_elements", None)
return cleaned
+3
View File
@@ -19,6 +19,7 @@ TIMEOUT_ENV = "AI_PLANNER_TIMEOUT_SECONDS"
THINKING_BUDGET_ENV = "AI_PLANNER_THINKING_BUDGET_TOKENS" THINKING_BUDGET_ENV = "AI_PLANNER_THINKING_BUDGET_TOKENS"
API_KEY_ENV = "AI_PLANNER_API_KEY" API_KEY_ENV = "AI_PLANNER_API_KEY"
BASE_URL_ENV = "AI_PLANNER_BASE_URL" BASE_URL_ENV = "AI_PLANNER_BASE_URL"
MULTIMODAL_ENV = "AI_PLANNER_MULTIMODAL"
SUPPORTED_PROVIDERS = frozenset(DEFAULT_MODEL_BY_PROVIDER) SUPPORTED_PROVIDERS = frozenset(DEFAULT_MODEL_BY_PROVIDER)
@@ -32,6 +33,7 @@ class PlannerConfig:
thinking_budget_tokens: int | None = None thinking_budget_tokens: int | None = None
api_key: str | None = None api_key: str | None = None
base_url: str | None = None base_url: str | None = None
multimodal: bool = False
def resolved_model(self) -> str: def resolved_model(self) -> str:
return self.model or DEFAULT_MODEL_BY_PROVIDER[self.provider] return self.model or DEFAULT_MODEL_BY_PROVIDER[self.provider]
@@ -47,6 +49,7 @@ def load_config(env: Mapping[str, str] | None = None) -> PlannerConfig:
thinking_budget_tokens=_parse_thinking_budget(values.get(THINKING_BUDGET_ENV)), thinking_budget_tokens=_parse_thinking_budget(values.get(THINKING_BUDGET_ENV)),
api_key=values.get(API_KEY_ENV) or _provider_key(values), api_key=values.get(API_KEY_ENV) or _provider_key(values),
base_url=values.get(BASE_URL_ENV) or None, base_url=values.get(BASE_URL_ENV) or None,
multimodal=_parse_bool(values.get(MULTIMODAL_ENV), default=False),
) )