ForcePilot/backend/package/yuxi/channels/adapters/urbit/vision.py
Kris 49ecd949e8 feat(urbit): 实现完整的Urbit聊天适配器模块
新增了从错误定义、客户端实现到功能完整的Urbit适配器全套代码,包括认证、消息处理、目录查询、邀请管理、媒体处理、速率限制等核心功能模块
2026-05-12 00:50:27 +08:00

97 lines
2.7 KiB
Python

from __future__ import annotations
import base64
from typing import Any, TYPE_CHECKING
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from .client import UrbitClient
async def analyze_image(
client: UrbitClient,
image_url: str,
prompt: str = "Describe this image in detail.",
*,
vision_api_url: str | None = None,
vision_api_key: str | None = None,
) -> str | None:
try:
from .media import download_media
image_data, content_type = await download_media(client, image_url)
if not image_data:
logger.warning(f"[Urbit] Vision: failed to download image from {image_url}")
return None
logger.info(f"[Urbit] Vision: downloaded {len(image_data)} bytes ({content_type})")
return await _invoke_vision_pipeline(
image_data,
content_type,
prompt,
vision_api_url=vision_api_url,
vision_api_key=vision_api_key,
)
except Exception as e:
logger.warning(f"[Urbit] Vision analysis failed: {e}")
return None
async def _invoke_vision_pipeline(
image_data: bytes,
content_type: str,
prompt: str,
*,
vision_api_url: str | None = None,
vision_api_key: str | None = None,
) -> str | None:
if not vision_api_url:
vision_api_url = "https://api.openai.com/v1/chat/completions"
base64_image = base64.b64encode(image_data).decode("utf-8")
data_url = f"data:{content_type};base64,{base64_image}"
payload = {
"model": "gpt-4o",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": data_url}},
],
}
],
"max_tokens": 300,
}
headers: dict[str, str] = {"Content-Type": "application/json"}
if vision_api_key:
headers["Authorization"] = f"Bearer {vision_api_key}"
import httpx
async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as http:
r = await http.post(vision_api_url, json=payload, headers=headers)
r.raise_for_status()
data = r.json()
choice = data.get("choices", [{}])[0]
message = choice.get("message", {})
result = message.get("content", "")
if result:
logger.info(f"[Urbit] Vision: analysis complete ({len(result)} chars)")
return result or None
async def build_vision_context(
image_urls: list[str],
prompt: str = "What do you see in these images?",
) -> dict[str, Any]:
return {
"images": [{"url": url} for url in image_urls],
"prompt": prompt,
"source": "urbit_vision",
}