feat: support image inputs in assistant tests

This commit is contained in:
Xin Wang
2026-08-23 23:46:18 +08:00
parent 0966f382b6
commit 9e4dfbe596
26 changed files with 1010 additions and 146 deletions
+89
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import base64
from dataclasses import dataclass
from io import BytesIO
from typing import Any
@@ -30,6 +31,14 @@ VISION_ANALYSIS_SYSTEM_PROMPT = (
)
@dataclass(frozen=True)
class VisionInputResolution:
"""The active model path for one explicit user image turn."""
native: bool
config: AssistantConfig
def _require(value: str, label: str) -> str:
if value:
return value
@@ -246,3 +255,83 @@ def config_with_main_llm_as_vision(cfg: AssistantConfig) -> AssistantConfig:
result.vision_llm_api_key = cfg.llm_api_key
result.vision_llm_base_url = cfg.llm_base_url
return result
def resolve_vision_input(
cfg: AssistantConfig,
workflow_scope: dict[str, Any] | None = None,
) -> VisionInputResolution:
"""Resolve native versus auxiliary vision using the active runtime scope."""
if cfg.type != "workflow":
if not cfg.vision_enabled:
raise ValueError("当前助手未开启视觉能力")
if cfg.vision_model_resource_id:
if not cfg.vision_llm_support_image_input:
raise ValueError("当前视觉模型不支持图片输入")
return VisionInputResolution(native=False, config=cfg)
if not cfg.llm_support_image_input:
raise ValueError("当前大语言模型不支持图片输入")
return VisionInputResolution(native=True, config=cfg)
scope = workflow_scope or {}
if not scope.get("enabled"):
raise ValueError("当前 Workflow Agent 节点未启用视觉能力")
vision_resource_id = str(scope.get("vision_model_resource_id") or "")
llm_resource_id = str(scope.get("llm_resource_id") or "")
resource_id = vision_resource_id or llm_resource_id
resource = cfg.workflow_model_resources.get(resource_id)
if resource is not None:
if not resource.support_image_input:
raise ValueError(f"当前视觉模型不支持图片输入: {resource.id}")
result = config_with_vision_resource(cfg, resource)
elif not vision_resource_id:
result = config_with_main_llm_as_vision(cfg)
else:
raise ValueError(f"视觉模型资源未加载: {vision_resource_id}")
result.vision_prompt_mode = normalize_vision_prompt_mode(
scope.get("vision_prompt_mode")
)
result.vision_prompt = str(scope.get("vision_prompt") or "").strip()
return VisionInputResolution(
native=not bool(vision_resource_id),
config=result,
)
async def build_image_user_message(
cfg: AssistantConfig,
image_bytes: bytes,
prompt_text: str,
*,
workflow_scope: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the same model-facing user message used by an explicit photo turn."""
question = prompt_text.strip() or "请根据用户刚提交的图片进行回复。"
frame = await asyncio.to_thread(image_frame_from_jpeg, image_bytes)
resolution = resolve_vision_input(cfg, workflow_scope)
if resolution.native:
data_uri = await asyncio.to_thread(image_data_uri, frame)
return {
"role": "user",
"content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": data_uri}},
],
}
observation = await analyze_image_with_vision_model(
resolution.config,
frame,
question,
)
return {
"role": "user",
"content": (
f"{question}\n\n"
"[视觉模型对用户刚提交图片的观察]\n"
f"{observation or '视觉模型没有返回有效观察结果。'}"
),
}