dashboard-nanobot/backend/services/chat_command_service.py

203 lines
8.3 KiB
Python
Raw Permalink Normal View History

2026-03-31 04:31:47 +00:00
import logging
import os
from typing import Any, Dict, List
from fastapi import HTTPException
from sqlmodel import Session
from core.docker_instance import docker_manager
2026-04-04 16:29:37 +00:00
from core.utils import _is_video_attachment_path, _is_visual_attachment_path
2026-03-31 04:31:47 +00:00
from models.bot import BotInstance
2026-04-04 16:29:37 +00:00
from services.bot_service import read_bot_runtime_snapshot
2026-04-14 02:04:12 +00:00
from services.platform_activity_service import record_activity_event
from services.platform_usage_service import create_usage_request, fail_latest_usage
2026-04-04 16:29:37 +00:00
from services.runtime_service import broadcast_runtime_packet, persist_runtime_packet
from services.workspace_service import resolve_workspace_path
2026-03-31 04:31:47 +00:00
logger = logging.getLogger("dashboard.backend")
def _normalize_message_media_item(value: Any) -> str:
return str(value or "").strip().replace("\\", "/").lstrip("/")
def _normalize_message_media_list(raw: Any) -> List[str]:
if not isinstance(raw, list):
return []
rows: List[str] = []
for value in raw:
normalized = _normalize_message_media_item(value)
if normalized:
rows.append(normalized)
return rows
def _build_delivery_command(command: str, checked_attachments: List[str]) -> str:
if not checked_attachments:
return command
attachment_block = "\n".join(f"- {path}" for path in checked_attachments)
if all(_is_visual_attachment_path(path) for path in checked_attachments):
has_video = any(_is_video_attachment_path(path) for path in checked_attachments)
media_label = "图片/视频" if has_video else "图片"
capability_hint = (
"1) 附件已随请求附带;图片在可用时可直接作为多模态输入理解,视频请按附件路径处理。\n"
if has_video
else "1) 附件中的图片已作为多模态输入提供,优先直接理解并回答。\n"
)
if command:
return (
f"{command}\n\n"
"[Attached files]\n"
f"{attachment_block}\n\n"
"【附件处理要求】\n"
f"{capability_hint}"
"2) 若当前模型或接口不支持直接理解该附件,请明确说明后再调用工具解析。\n"
"3) 除非用户明确要求,不要先调用工具读取附件文件。\n"
"4) 回复语言必须遵循 USER.md若未指定则与用户当前输入语言保持一致。\n"
"5) 仅基于可见内容回答;看不清或无法确认的部分请明确说明,不要猜测。"
)
return (
"请先处理已附带的附件列表:\n"
f"{attachment_block}\n\n"
f"请直接分析已附带的{media_label}并总结关键信息。\n"
f"{'图片在可用时可直接作为多模态输入理解,视频请按附件路径处理。' if has_video else ''}\n"
"若当前模型或接口不支持直接理解该附件,请明确说明后再调用工具解析。\n"
"回复语言必须遵循 USER.md若未指定则与用户当前输入语言保持一致。\n"
"仅基于可见内容回答;看不清或无法确认的部分请明确说明,不要猜测。"
)
command_has_paths = all(path in command for path in checked_attachments) if command else False
if command and not command_has_paths:
return (
f"{command}\n\n"
"[Attached files]\n"
f"{attachment_block}\n\n"
"Please process the attached file(s) listed above when answering this request.\n"
"Reply language must follow USER.md. If not specified, use the same language as the user input."
)
if not command:
return (
"Please process the uploaded file(s) listed below:\n"
f"{attachment_block}\n\n"
"Reply language must follow USER.md. If not specified, use the same language as the user input."
)
return command
def send_bot_command(session: Session, bot_id: str, command: str, attachments: Any) -> Dict[str, Any]:
request_id = ""
try:
bot = session.get(BotInstance, bot_id)
if not bot:
raise HTTPException(status_code=404, detail="Bot not found")
2026-04-04 16:29:37 +00:00
runtime_snapshot = read_bot_runtime_snapshot(bot)
2026-03-31 04:31:47 +00:00
normalized_attachments = _normalize_message_media_list(attachments)
text_command = str(command or "").strip()
if not text_command and not normalized_attachments:
raise HTTPException(status_code=400, detail="Command or attachments is required")
checked_attachments: List[str] = []
for rel_path in normalized_attachments:
2026-04-04 16:29:37 +00:00
_, target = resolve_workspace_path(bot_id, rel_path)
2026-03-31 04:31:47 +00:00
if not os.path.isfile(target):
raise HTTPException(status_code=400, detail=f"attachment not found: {rel_path}")
checked_attachments.append(rel_path)
delivery_media = [f"/root/.nanobot/workspace/{path.lstrip('/')}" for path in checked_attachments]
display_command = text_command if text_command else "[attachment message]"
delivery_command = _build_delivery_command(text_command, checked_attachments)
request_id = create_usage_request(
session,
bot_id,
display_command,
attachments=checked_attachments,
channel="dashboard",
metadata={"attachment_count": len(checked_attachments)},
provider=str(runtime_snapshot.get("llm_provider") or "").strip() or None,
model=str(runtime_snapshot.get("llm_model") or "").strip() or None,
)
record_activity_event(
session,
bot_id,
"command_submitted",
request_id=request_id,
channel="dashboard",
detail="command submitted",
metadata={"attachment_count": len(checked_attachments), "has_text": bool(text_command)},
)
session.commit()
outbound_user_packet: Dict[str, Any] | None = None
if display_command or checked_attachments:
outbound_user_packet = {
"type": "USER_COMMAND",
"channel": "dashboard",
"text": display_command,
"media": checked_attachments,
"request_id": request_id,
}
2026-04-04 16:29:37 +00:00
persist_runtime_packet(bot_id, outbound_user_packet)
2026-03-31 04:31:47 +00:00
if outbound_user_packet:
2026-04-04 16:29:37 +00:00
broadcast_runtime_packet(bot_id, outbound_user_packet)
2026-03-31 04:31:47 +00:00
success = docker_manager.send_command(bot_id, delivery_command, media=delivery_media)
if success:
return {"success": True}
detail = docker_manager.get_last_delivery_error(bot_id)
fail_latest_usage(session, bot_id, detail or "command delivery failed")
record_activity_event(
session,
bot_id,
"command_failed",
request_id=request_id,
channel="dashboard",
detail=(detail or "command delivery failed")[:400],
)
session.commit()
2026-04-04 16:29:37 +00:00
broadcast_runtime_packet(
2026-03-31 04:31:47 +00:00
bot_id,
{
"type": "AGENT_STATE",
"channel": "dashboard",
"payload": {
"state": "ERROR",
"action_msg": detail or "command delivery failed",
},
},
)
raise HTTPException(
status_code=502,
detail=f"Failed to deliver command to bot dashboard channel{': ' + detail if detail else ''}",
)
except HTTPException:
raise
except Exception as exc:
logger.exception("send_bot_command failed for bot_id=%s", bot_id)
try:
session.rollback()
except Exception:
pass
if request_id:
try:
fail_latest_usage(session, bot_id, str(exc))
record_activity_event(
session,
bot_id,
"command_failed",
request_id=request_id,
channel="dashboard",
detail=str(exc)[:400],
)
session.commit()
except Exception:
try:
session.rollback()
except Exception:
pass
raise HTTPException(status_code=500, detail=f"Failed to process bot command: {exc}") from exc