refactor(agent): 按需加载 Agent 运行时 (#6336)

This commit is contained in:
InfinityPacer
2026-08-16 19:25:24 +08:00
committed by GitHub
parent e5f0c53069
commit 5b367011c5
56 changed files with 5059 additions and 628 deletions
+93 -16
View File
@@ -20,10 +20,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app import schemas
from app.api.response import ResponseAPIRouter
from app.agent.callback import StreamingHandler
from app.agent.orchestrator import MoviePilotAgent, ReplyMode, agent_manager
from app.agent.contracts import ReplyMode, build_display_message
from app.agent.llm.capability import AgentCapabilityManager
from app.agent.mcp import agent_mcp_manager
from app.agent.runtime_loader import (
get_moviepilot_agent_type,
get_running_agent_manager,
)
from app.chain.message import MessageChain
from app.command import Command
from app.runtime.config import global_vars, settings
@@ -254,7 +257,7 @@ async def test_agent_mcp_server(
)
class _WebAgentStreamingHandler(StreamingHandler):
class _WebAgentStreamingHandlerMixin:
"""
Web 前端专用流式处理器,将工具提示和文本统一回调给 SSE。
"""
@@ -342,7 +345,28 @@ class _WebAgentStreamingHandler(StreamingHandler):
return True
class _WebAgentMoviePilotAgent(MoviePilotAgent):
def _get_web_agent_streaming_handler_type() -> type:
"""首次构造 Web Agent 时才解析完整流式处理器实现。"""
global _WEB_AGENT_STREAMING_HANDLER_TYPE
if _WEB_AGENT_STREAMING_HANDLER_TYPE is not None:
return _WEB_AGENT_STREAMING_HANDLER_TYPE
with _WEB_AGENT_STREAMING_HANDLER_TYPE_LOCK:
if _WEB_AGENT_STREAMING_HANDLER_TYPE is None:
from app.agent.callback import StreamingHandler
_WEB_AGENT_STREAMING_HANDLER_TYPE = type(
"_RuntimeWebAgentStreamingHandler",
(_WebAgentStreamingHandlerMixin, StreamingHandler),
{"__module__": __name__},
)
return _WEB_AGENT_STREAMING_HANDLER_TYPE
_WEB_AGENT_STREAMING_HANDLER_TYPE_LOCK = Lock()
_WEB_AGENT_STREAMING_HANDLER_TYPE: Optional[type] = None
class _WebAgentMoviePilotAgentMixin:
"""
Web 前端专用 Agent,强制使用流式推理。
"""
@@ -355,7 +379,9 @@ class _WebAgentMoviePilotAgent(MoviePilotAgent):
) -> None:
super().__init__(*args, **kwargs)
self._notification_callback = notification_callback
self.stream_handler = _WebAgentStreamingHandler(self._emit_output)
self.stream_handler = _get_web_agent_streaming_handler_type()(
self._emit_output
)
def _should_stream(self) -> bool:
"""Web 对话实时输出,复用会话执行后台任务时改用非流式广播。"""
@@ -381,7 +407,9 @@ class _WebAgentMoviePilotAgent(MoviePilotAgent):
:param output_callback: 当前请求的输出回调
"""
self.output_callback = output_callback
if output_callback and isinstance(self.stream_handler, _WebAgentStreamingHandler):
if output_callback and isinstance(
self.stream_handler, _WebAgentStreamingHandlerMixin
):
self.stream_handler.set_emit_callback(self._emit_output)
async def _is_system_admin_context(self) -> bool:
@@ -420,6 +448,30 @@ class _WebAgentMoviePilotAgent(MoviePilotAgent):
logger.debug(f"Web智能体输出回调失败: {e}")
def _build_web_agent_type(agent_base_type: type) -> type:
"""为 Web 通道组合唯一的运行时 Agent 类型。"""
return type(
"_RuntimeWebAgentMoviePilotAgent",
(_WebAgentMoviePilotAgentMixin, agent_base_type),
{"__module__": __name__},
)
_WEB_AGENT_TYPE_LOCK = Lock()
_WEB_AGENT_TYPE: Optional[type] = None
def _get_web_agent_type() -> type:
"""在真实 Web Agent 调用边界 single-flight 解析运行时类型。"""
global _WEB_AGENT_TYPE
if _WEB_AGENT_TYPE is not None:
return _WEB_AGENT_TYPE
with _WEB_AGENT_TYPE_LOCK:
if _WEB_AGENT_TYPE is None:
_WEB_AGENT_TYPE = _build_web_agent_type(get_moviepilot_agent_type())
return _WEB_AGENT_TYPE
def _build_web_agent_session_id(user: User, session_id: Optional[str]) -> str:
"""
构建前端 Agent 会话 ID。
@@ -1131,7 +1183,7 @@ def _build_web_agent_display_message_from_events(
:param events: 已转换的 WebAgent SSE 事件列表
:return: 可持久化的助手展示消息
"""
message = MoviePilotAgent.build_display_message(
message = build_display_message(
role="assistant",
status="streaming",
)
@@ -1725,7 +1777,8 @@ async def get_agent_chat_session(
if server_session_id != session_id:
chat = await _get_accessible_agent_chat(oper, server_session_id, current_user)
if not chat:
if agent_manager.is_session_busy(server_session_id):
manager = get_running_agent_manager()
if manager and manager.is_session_busy(server_session_id):
return schemas.Response(
success=True,
data={
@@ -1737,7 +1790,10 @@ async def get_agent_chat_session(
)
return schemas.Response(success=False, message="会话不存在或无权访问")
data = AgentChatOper.to_detail(chat)
data["is_processing"] = agent_manager.is_session_busy(chat.session_id)
manager = get_running_agent_manager()
data["is_processing"] = bool(
manager and manager.is_session_busy(chat.session_id)
)
return schemas.Response(success=True, data=data)
@@ -1836,7 +1892,8 @@ async def stop_web_agent_session_task(
if chat and not _can_access_agent_chat(chat, current_user):
return schemas.Response(success=False, message="会话不存在或无权访问")
stopped = await agent_manager.stop_current_task(server_session_id)
manager = get_running_agent_manager()
stopped = await manager.stop_current_task(server_session_id) if manager else False
return schemas.Response(
success=True,
data={"stopped": stopped},
@@ -1881,7 +1938,8 @@ async def web_agent_stream(
)
is_secret_confirmation_control = (
is_secret_confirmation_candidate
and agent_manager.matches_secret_confirmation(
and (manager := get_running_agent_manager()) is not None
and manager.matches_secret_confirmation(
session_id,
str(current_user.id),
channel=MessageChannel.WebAgent.value,
@@ -1943,7 +2001,7 @@ async def web_agent_stream(
display_messages = []
if payload.echo_user:
display_messages.append(
MoviePilotAgent.build_display_message(
build_display_message(
role="user",
content=display_prompt or prompt,
attachments=user_attachments,
@@ -2038,6 +2096,19 @@ async def web_agent_stream(
media_type="text/event-stream",
)
manager = get_running_agent_manager()
if manager is None:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": "智能助手服务尚未就绪,请稍后重试。"},
locale=locale,
)
]),
media_type="text/event-stream",
)
transcript = _transcribe_web_agent_audio_refs(payload.audio_refs or [])
prompt = _merge_web_agent_prompt_with_transcript(prompt, transcript)
display_prompt = _merge_web_agent_prompt_with_transcript(display_prompt, transcript)
@@ -2077,7 +2148,7 @@ async def web_agent_stream(
)
display_messages = []
if payload.echo_user and not is_secret_confirmation_control:
user_display_message = MoviePilotAgent.build_display_message(
user_display_message = build_display_message(
role="user",
content=display_prompt or prompt,
attachments=user_attachments,
@@ -2085,7 +2156,7 @@ async def web_agent_stream(
if payload.choice_selection:
user_display_message["choice_selection"] = payload.choice_selection
display_messages.append(user_display_message)
assistant_display_message = MoviePilotAgent.build_display_message(
assistant_display_message = build_display_message(
role="assistant",
status="streaming",
)
@@ -2132,7 +2203,10 @@ async def web_agent_stream(
async def run_agent() -> None:
"""后台执行 Agent,并将结果写入事件队列。"""
try:
await agent_manager.process_message(
runtime_manager = get_running_agent_manager()
if runtime_manager is None:
raise RuntimeError("智能助手服务尚未就绪,请稍后重试。")
await runtime_manager.process_message(
session_id=session_id,
user_id=str(current_user.id),
message=prompt,
@@ -2151,9 +2225,12 @@ async def web_agent_stream(
else None
),
notification_callback=notification_callback,
agent_factory=_WebAgentMoviePilotAgent,
agent_factory=_get_web_agent_type(),
wait_for_completion=True,
)
except asyncio.CancelledError:
# 显式停止会话沿用正常终止语义;服务关闭会由 manager 的稳定异常分支处理。
pass
except Exception as err:
logger.error(f"Web智能助手执行失败: {str(err)}")
error_event = {
+60 -20
View File
@@ -9,16 +9,17 @@ from fastapi.responses import JSONResponse, StreamingResponse
from app import schemas
from app.api.endpoints.openai import (
MODEL_ID,
_CollectingMoviePilotAgent,
_is_manager_unavailable,
_run_managed_agent,
)
from app.api.openai_utils import (
build_anthropic_messages,
build_prompt,
build_session_id,
)
from app.agent.runtime_loader import get_running_agent_manager
from app.runtime.config import settings
from app.application.security.access import anthropic_api_key_header
from app.schemas.types import MessageChannel
ANTHROPIC_ERROR_RESPONSES = {
400: {"model": schemas.AnthropicErrorResponse, "description": "请求格式错误"},
@@ -60,19 +61,31 @@ def _check_auth(api_key: Optional[str]) -> Optional[JSONResponse]:
async def _stream_anthropic_response(
agent: _CollectingMoviePilotAgent,
manager,
session_id: str,
user_id: str,
prompt: str,
images: List[str],
) -> AsyncIterator[str]:
event_queue: asyncio.Queue = asyncio.Queue()
if hasattr(agent.stream_handler, "bind_queue"):
agent.stream_handler.bind_queue(event_queue)
message_id = f"msg_{uuid.uuid4().hex}"
async def _run_agent():
try:
await agent.process(prompt, images=images, files=None)
await _run_managed_agent(
manager=manager,
session_id=session_id,
user_id=user_id,
username="anthropic-client",
source="anthropic",
prompt=prompt,
images=images,
stream_mode=True,
event_queue=event_queue,
)
except asyncio.CancelledError:
await event_queue.put({"error": "MoviePilot AI agent is unavailable."})
except Exception as exc:
await event_queue.put({"error": str(exc)})
finally:
@@ -87,7 +100,12 @@ async def _stream_anthropic_response(
if item is None:
break
if isinstance(item, dict) and item.get("error"):
raise RuntimeError(str(item["error"]))
yield (
"event: error\n"
f"data: {json.dumps({'type': 'error', 'error': {'type': 'api_error', 'message': str(item['error'])}}, ensure_ascii=False)}\n\n"
)
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'}, ensure_ascii=False)}\n\n"
return
text = str(item or "")
if not text:
continue
@@ -96,6 +114,7 @@ async def _stream_anthropic_response(
yield f"event: message_delta\ndata: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'usage': {'output_tokens': 0}}, ensure_ascii=False)}\n\n"
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'}, ensure_ascii=False)}\n\n"
finally:
await manager.clear_session(session_id=session_id, user_id=user_id)
if not task.done():
task.cancel()
try:
@@ -132,6 +151,13 @@ async def messages(
503,
error_type="api_error",
)
manager = get_running_agent_manager()
if manager is None:
return _anthropic_error_response(
"MoviePilot AI agent is unavailable.",
503,
error_type="api_error",
)
normalized_messages = build_anthropic_messages(payload.system, payload.messages)
try:
@@ -141,19 +167,15 @@ async def messages(
session_seed = anthropic_version or "anthropic"
session_id = build_session_id(f"{session_seed}:{uuid.uuid4().hex}", SESSION_PREFIX)
# 兼容接口的 API_TOKEN 客户端按管理员级 MoviePilot Agent 集成处理。
agent = _CollectingMoviePilotAgent(
session_id=session_id,
user_id=session_id,
channel=MessageChannel.Web.value,
source="anthropic",
username="anthropic-client",
stream_mode=payload.stream,
)
if payload.stream:
return StreamingResponse(
_stream_anthropic_response(agent=agent, prompt=prompt, images=images),
_stream_anthropic_response(
manager=manager,
session_id=session_id,
user_id=session_id,
prompt=prompt,
images=images,
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
@@ -162,14 +184,32 @@ async def messages(
},
)
collected_messages = []
try:
result = await agent.process(prompt, images=images, files=None)
result, collected_messages = await _run_managed_agent(
manager=manager,
session_id=session_id,
user_id=session_id,
username="anthropic-client",
source="anthropic",
prompt=prompt,
images=images,
stream_mode=False,
)
except Exception as exc:
if _is_manager_unavailable(exc):
return _anthropic_error_response(
"MoviePilot AI agent is unavailable.",
503,
error_type="api_error",
)
return _anthropic_error_response(str(exc), 500, error_type="api_error")
finally:
await manager.clear_session(session_id=session_id, user_id=session_id)
content = "\n\n".join(
message.strip()
for message in agent.collected_messages
for message in collected_messages
if message and message.strip()
).strip()
if not content and result:
+13 -3
View File
@@ -9,7 +9,8 @@ from sqlalchemy.orm import Session
from app import schemas
from app.api.response import ResponseAPIRouter
from app.agent.orchestrator import ReplyMode, agent_manager
from app.agent.contracts import ReplyMode
from app.agent.runtime_loader import get_running_agent_manager
from app.agent.prompt.transfer_redo import (
build_batch_manual_redo_prompt,
build_manual_redo_prompt,
@@ -31,6 +32,7 @@ from app.runtime.progress import ProgressHelper
from app.application.history import clear_transfer_failures
from app.schemas.types import EventType
from app.foundation.text import cut as jieba_cut
from app.runtime.log import logger
router = ResponseAPIRouter()
@@ -58,7 +60,11 @@ def _start_ai_redo_task(history_id: int, prompt: str, progress_key: str):
async def runner():
try:
await agent_manager.run_background_prompt(
manager = get_running_agent_manager()
if manager is None:
logger.warning("智能助手服务未运行,跳过单条整理历史 AI 重做")
raise RuntimeError("智能助手服务未运行")
await manager.run_background_prompt(
message=prompt,
session_prefix=f"__agent_manual_redo_{history_id}",
output_callback=update_output,
@@ -103,7 +109,11 @@ def _start_batch_ai_redo_task(
async def runner():
try:
await agent_manager.run_background_prompt(
manager = get_running_agent_manager()
if manager is None:
logger.warning("智能助手服务未运行,跳过批量整理历史 AI 重做")
raise RuntimeError("智能助手服务未运行")
await manager.run_background_prompt(
message=prompt,
session_prefix="__agent_manual_redo_batch",
output_callback=update_output,
+11 -3
View File
@@ -5,13 +5,19 @@ from fastapi.responses import HTMLResponse
from app import schemas
from app.api.response import ResponseAPIRouter
from app.agent.llm import LLMProviderManager, render_auth_result_html
from app.db.models import User
from app.api.deps import get_current_active_superuser_async
router = ResponseAPIRouter()
def _get_llm_provider_manager_type() -> type:
"""在真实管理请求边界解析 provider 运行时。"""
from app.agent.llm.provider import LLMProviderManager
return LLMProviderManager
@router.post(
"/manage",
summary="LLM提供商统一管理",
@@ -37,7 +43,7 @@ async def manage_provider(
"callback_url",
str(request.url_for("llm_provider_auth_callback", provider_id=payload.target)),
)
result = await LLMProviderManager().provider_manage(
result = await _get_llm_provider_manager_type()().provider_manage(
payload.target, payload.action, **params
)
return schemas.Response(
@@ -70,11 +76,13 @@ async def llm_provider_auth_callback(
"""
处理需要浏览器回跳的 OAuth provider。
"""
success, message = await LLMProviderManager().handle_chatgpt_callback(
success, message = await _get_llm_provider_manager_type()().handle_chatgpt_callback(
provider_id,
code,
state,
error,
error_description,
)
from app.agent.llm.provider import render_auth_result_html
return HTMLResponse(content=render_auth_result_html(success, message))
+243 -36
View File
@@ -2,6 +2,7 @@ import asyncio
import json
import time
import uuid
from threading import Lock
from typing import AsyncIterator, List, Optional, Tuple
from fastapi import APIRouter, Request, Security
@@ -15,8 +16,11 @@ from app.api.openai_utils import (
build_responses_input,
build_session_id,
)
from app.agent.callback import StreamingHandler
from app.agent.orchestrator import MoviePilotAgent
from app.agent.runtime_loader import (
get_moviepilot_agent_type,
get_running_agent_manager,
)
from app.agent.contracts import ReplyMode
from app.runtime.config import settings
from app.application.security.access import openai_bearer_scheme
from app.schemas.types import MessageChannel
@@ -35,7 +39,7 @@ MODEL_ID = "moviepilot-agent"
SESSION_PREFIX = "openai:"
class _CollectingMoviePilotAgent(MoviePilotAgent):
class _CollectingMoviePilotAgentMixin:
"""
捕获 Agent 最终输出,避免再通过消息渠道二次发送。
"""
@@ -45,11 +49,38 @@ class _CollectingMoviePilotAgent(MoviePilotAgent):
self.collected_messages: List[str] = []
self.stream_mode = stream_mode
if stream_mode:
self.stream_handler = _OpenAIStreamingHandler()
self.stream_handler = _get_openai_streaming_handler_type()()
def _should_stream(self) -> bool:
return self.stream_mode
def configure_protocol_request(
self,
*,
stream_mode: bool,
event_queue: Optional[asyncio.Queue],
) -> None:
"""切换请求级输出目标,并保持已编译工具引用的 handler identity。"""
self.collected_messages = []
self.stream_mode = stream_mode
if isinstance(self.stream_handler, _OpenAIStreamingHandlerMixin):
self.stream_handler.bind_queue(event_queue if stream_mode else None)
return
if not stream_mode:
return
self.stream_handler = _get_openai_streaming_handler_type()()
self.stream_handler.bind_queue(event_queue)
# 已编译工具持有旧 handler;identity 变化时必须重建图和工具目录。
self._compiled_agent_bundle = None
def release_protocol_request(
self,
event_queue: Optional[asyncio.Queue],
) -> None:
"""释放已结束请求的输出队列,不影响同会话已重绑的新请求。"""
if isinstance(self.stream_handler, _OpenAIStreamingHandlerMixin):
self.stream_handler.unbind_queue(event_queue)
async def send_agent_message(self, message: str, title: str = ""):
text = (message or "").strip()
if title and text:
@@ -62,7 +93,7 @@ class _CollectingMoviePilotAgent(MoviePilotAgent):
self.stream_handler.emit(text)
class _OpenAIStreamingHandler(StreamingHandler):
class _OpenAIStreamingHandlerMixin:
"""
将 Agent 流式输出转发到 OpenAI SSE 队列,不向站内消息系统落消息。
"""
@@ -71,9 +102,15 @@ class _OpenAIStreamingHandler(StreamingHandler):
super().__init__()
self._event_queue: Optional[asyncio.Queue] = None
def bind_queue(self, queue: asyncio.Queue):
def bind_queue(self, queue: Optional[asyncio.Queue]):
"""绑定当前协议请求的输出队列。"""
self._event_queue = queue
def unbind_queue(self, queue: Optional[asyncio.Queue]) -> None:
"""仅当仍指向该请求时解除绑定,避免清掉已排队的新请求。"""
if self._event_queue is queue:
self._event_queue = None
def emit(self, token: str):
emitted = super().emit(token)
if emitted and self._event_queue is not None:
@@ -121,18 +158,67 @@ class _OpenAIStreamingHandler(StreamingHandler):
return True, final_text
def _get_openai_streaming_handler_type() -> type:
"""首次兼容协议调用时才解析完整流式处理器。"""
global _OPENAI_STREAMING_HANDLER_TYPE
if _OPENAI_STREAMING_HANDLER_TYPE is not None:
return _OPENAI_STREAMING_HANDLER_TYPE
with _OPENAI_STREAMING_HANDLER_TYPE_LOCK:
if _OPENAI_STREAMING_HANDLER_TYPE is None:
from app.agent.callback import StreamingHandler
_OPENAI_STREAMING_HANDLER_TYPE = type(
"_RuntimeOpenAIStreamingHandler",
(_OpenAIStreamingHandlerMixin, StreamingHandler),
{"__module__": __name__},
)
return _OPENAI_STREAMING_HANDLER_TYPE
_OPENAI_STREAMING_HANDLER_TYPE_LOCK = Lock()
_OPENAI_STREAMING_HANDLER_TYPE: Optional[type] = None
def _build_collecting_agent_type(agent_base_type: type) -> type:
"""为 OpenAI 与 Anthropic 兼容协议组合唯一的运行时类型。"""
return type(
"_RuntimeCollectingMoviePilotAgent",
(_CollectingMoviePilotAgentMixin, agent_base_type),
{"__module__": __name__},
)
_COLLECTING_AGENT_TYPE_LOCK = Lock()
_COLLECTING_AGENT_TYPE: Optional[type] = None
def _get_collecting_agent_type() -> type:
"""在首个真实兼容协议请求边界 single-flight 解析 Agent 类型。"""
global _COLLECTING_AGENT_TYPE
if _COLLECTING_AGENT_TYPE is not None:
return _COLLECTING_AGENT_TYPE
with _COLLECTING_AGENT_TYPE_LOCK:
if _COLLECTING_AGENT_TYPE is None:
_COLLECTING_AGENT_TYPE = _build_collecting_agent_type(
get_moviepilot_agent_type()
)
return _COLLECTING_AGENT_TYPE
def _sse_payload(data: dict) -> str:
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
async def _stream_response(
agent: _CollectingMoviePilotAgent,
manager,
session_id: str,
user_id: str,
username: str,
prompt: str,
images: List[str],
cleanup_session: bool,
) -> AsyncIterator[str]:
event_queue: asyncio.Queue = asyncio.Queue()
if isinstance(agent.stream_handler, _OpenAIStreamingHandler):
agent.stream_handler.bind_queue(event_queue)
created = int(time.time())
completion_id = f"chatcmpl-{uuid.uuid4().hex}"
@@ -140,7 +226,19 @@ async def _stream_response(
async def _run_agent():
try:
await agent.process(prompt, images=images, files=None)
await _run_managed_agent(
manager=manager,
session_id=session_id,
user_id=user_id,
username=username,
source="openai",
prompt=prompt,
images=images,
stream_mode=True,
event_queue=event_queue,
)
except asyncio.CancelledError:
await event_queue.put({"error": "MoviePilot AI agent is unavailable."})
except Exception as exc:
await event_queue.put({"error": str(exc)})
finally:
@@ -170,7 +268,17 @@ async def _stream_response(
if item is None:
break
if isinstance(item, dict) and item.get("error"):
raise RuntimeError(str(item["error"]))
yield _sse_payload(
{
"error": {
"message": str(item["error"]),
"type": "server_error",
"code": "agent_execution_failed",
}
}
)
yield "data: [DONE]\n\n"
return
text = str(item or "")
if not text:
continue
@@ -208,6 +316,10 @@ async def _stream_response(
)
yield "data: [DONE]\n\n"
finally:
if cleanup_session:
await manager.clear_session(session_id=session_id, user_id=user_id)
elif not task.done():
await manager.stop_current_task(session_id)
if not task.done():
task.cancel()
try:
@@ -218,6 +330,57 @@ async def _stream_response(
await task
def _is_manager_unavailable(error: BaseException) -> bool:
"""识别 manager acceptance gate 的稳定错误,不导入完整编排模块。"""
return getattr(error, "code", None) == "agent_manager_unavailable"
async def _run_managed_agent(
*,
manager,
session_id: str,
user_id: str,
username: str,
source: str,
prompt: str,
images: List[str],
stream_mode: bool,
event_queue: Optional[asyncio.Queue] = None,
) -> tuple[str, List[str]]:
"""通过 AgentManager 执行协议请求,并在 worker 内配置请求级输出。"""
agent_holder = {}
def configure_agent(agent) -> None:
agent.configure_protocol_request(
stream_mode=stream_mode,
event_queue=event_queue,
)
agent_holder["agent"] = agent
try:
result = await manager.process_message(
session_id=session_id,
user_id=user_id,
message=prompt,
images=images,
files=None,
channel=MessageChannel.Web.value,
source=source,
username=username,
reply_mode=ReplyMode.CAPTURE_ONLY,
allow_message_tools=True,
agent_factory=_get_collecting_agent_type(),
agent_setup=configure_agent,
wait_for_completion=True,
)
agent = agent_holder.get("agent")
return result, list(agent.collected_messages if agent else [])
finally:
agent = agent_holder.get("agent")
if agent is not None:
agent.release_protocol_request(event_queue)
def _error_response(
message: str,
status_code: int,
@@ -310,6 +473,14 @@ async def chat_completions(
error_type="server_error",
code="ai_agent_disabled",
)
manager = get_running_agent_manager()
if manager is None:
return _error_response(
"MoviePilot AI agent is unavailable.",
503,
error_type="server_error",
code="ai_agent_unavailable",
)
if not payload.messages:
return _error_response(
@@ -337,19 +508,17 @@ async def chat_completions(
session_id = build_session_id(session_key, SESSION_PREFIX)
username = str(payload.user or "openai-client")
# 兼容接口的 API_TOKEN 客户端按管理员级 MoviePilot Agent 集成处理。
agent = _CollectingMoviePilotAgent(
session_id=session_id,
user_id=session_key,
channel=MessageChannel.Web.value,
source="openai",
username=username,
stream_mode=payload.stream,
)
if payload.stream:
return StreamingResponse(
_stream_response(agent=agent, prompt=prompt, images=images),
_stream_response(
manager=manager,
session_id=session_id,
user_id=session_key,
username=username,
prompt=prompt,
images=images,
cleanup_session=not use_server_session,
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
@@ -358,19 +527,39 @@ async def chat_completions(
},
)
collected_messages = []
try:
result = await agent.process(prompt, images=images, files=None)
result, collected_messages = await _run_managed_agent(
manager=manager,
session_id=session_id,
user_id=session_key,
username=username,
source="openai",
prompt=prompt,
images=images,
stream_mode=False,
)
except Exception as exc:
if _is_manager_unavailable(exc):
return _error_response(
"MoviePilot AI agent is unavailable.",
503,
error_type="server_error",
code="ai_agent_unavailable",
)
return _error_response(
str(exc),
500,
error_type="server_error",
code="agent_execution_failed",
)
finally:
if not use_server_session:
await manager.clear_session(session_id=session_id, user_id=session_key)
content = "\n\n".join(
message.strip()
for message in agent.collected_messages
for message in collected_messages
if message and message.strip()
).strip()
if not content and result:
@@ -403,6 +592,14 @@ async def responses(
error_type="server_error",
code="ai_agent_disabled",
)
manager = get_running_agent_manager()
if manager is None:
return _error_response(
"MoviePilot AI agent is unavailable.",
503,
error_type="server_error",
code="ai_agent_unavailable",
)
if payload.stream:
return _error_response(
@@ -430,29 +627,39 @@ async def responses(
session_key = str(payload.user or uuid.uuid4())
session_id = build_session_id(session_key, SESSION_PREFIX)
# 兼容接口的 API_TOKEN 客户端按管理员级 MoviePilot Agent 集成处理。
agent = _CollectingMoviePilotAgent(
session_id=session_id,
user_id=session_key,
channel=MessageChannel.Web.value,
source="openai.responses",
username=str(payload.user or "openai-client"),
stream_mode=False,
)
collected_messages = []
try:
result = await agent.process(prompt, images=images, files=None)
result, collected_messages = await _run_managed_agent(
manager=manager,
session_id=session_id,
user_id=session_key,
username=str(payload.user or "openai-client"),
source="openai.responses",
prompt=prompt,
images=images,
stream_mode=False,
)
except Exception as exc:
if _is_manager_unavailable(exc):
return _error_response(
"MoviePilot AI agent is unavailable.",
503,
error_type="server_error",
code="ai_agent_unavailable",
)
return _error_response(
str(exc),
500,
error_type="server_error",
code="agent_execution_failed",
)
finally:
if not payload.user:
await manager.clear_session(session_id=session_id, user_id=session_key)
content = "\n\n".join(
message.strip()
for message in agent.collected_messages
for message in collected_messages
if message and message.strip()
).strip()
if not content and result: