Support multimodal planner scenes without OCR
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user