dashboard-nanobot/backend/services/chat_command_service.py

203 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
from core.utils import _is_video_attachment_path, _is_visual_attachment_path
from models.bot import BotInstance
from services.bot_service import read_bot_runtime_snapshot
from services.platform_activity_service import record_activity_event
from services.platform_usage_service import create_usage_request, fail_latest_usage
from services.runtime_service import broadcast_runtime_packet, persist_runtime_packet
from services.workspace_service import resolve_workspace_path
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")
runtime_snapshot = read_bot_runtime_snapshot(bot)
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:
_, target = resolve_workspace_path(bot_id, rel_path)
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,
}
persist_runtime_packet(bot_id, outbound_user_packet)
if outbound_user_packet:
broadcast_runtime_packet(bot_id, outbound_user_packet)
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()
broadcast_runtime_packet(
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