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_API_KEY=local-key
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
```
@@ -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
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,
and final replies in a local SQLite database. View them at
`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,
screenshot: bytes | None = None,
) -> list[PlannedStep]:
scene_json = _without_ocr(scene.to_dict()) if self.config.multimodal else scene.to_dict()
user_prompt = planner_user_prompt(
goal=goal,
scene_json=scene.to_dict(),
scene_json=scene_json,
history_summary=_history_summary(world),
device_platform=context.device_platform,
)
@@ -94,3 +95,16 @@ def _history_summary(world: "WorldState | None") -> list[dict[str, Any]]:
}
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"
API_KEY_ENV = "AI_PLANNER_API_KEY"
BASE_URL_ENV = "AI_PLANNER_BASE_URL"
MULTIMODAL_ENV = "AI_PLANNER_MULTIMODAL"
SUPPORTED_PROVIDERS = frozenset(DEFAULT_MODEL_BY_PROVIDER)
@@ -32,6 +33,7 @@ class PlannerConfig:
thinking_budget_tokens: int | None = None
api_key: str | None = None
base_url: str | None = None
multimodal: bool = False
def resolved_model(self) -> str:
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)),
api_key=values.get(API_KEY_ENV) or _provider_key(values),
base_url=values.get(BASE_URL_ENV) or None,
multimodal=_parse_bool(values.get(MULTIMODAL_ENV), default=False),
)