Files
EPEEAIKit/art-agent/backend/app/agent/loop.py
2026-04-12 01:02:14 +08:00

186 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Agent Loop 主循环LLM 对话 -> 工具调用 -> 结果回传 -> 继续。"""
import json
from typing import AsyncGenerator, Optional
from openai import AsyncOpenAI
from app.agent.tools import TOOL_DEFINITIONS, execute_tool
def _get_client() -> AsyncOpenAI:
return AsyncOpenAI()
SYSTEM_PROMPT = """\
你是一个专业的游戏美术 AI 助手。你的工作是帮助美术人员通过对话生成游戏美术资源。
## 你的能力
- 根据用户的文字描述生成图片UI图标、按钮、插画、立绘、概念图等
- 理解用户的审美意图,将中文描述转化为高质量的英文生成 prompt
- 根据用户反馈迭代修改(调整颜色、风格、构图等)
- 如果用户提供了参考图,将参考图的风格元素融入生成 prompt
## 工作流程
1. 理解用户需求,必要时追问细节(尺寸、风格、用途等)
2. 将需求转化为详细的英文 prompt调用 generate_image 工具生成图片
3. 向用户展示结果并询问反馈
4. 根据反馈调整 prompt 并重新生成
## 生成 prompt 要求
- 必须使用英文
- 尽量详细描述:主体内容、风格、颜色方案、光照、构图、材质等
- 如果用户要求游戏 UI 元素,添加相关关键词如 "game UI", "icon", "button"
- 如果用户提供了参考图,在 prompt 中描述参考图的风格特征
## 注意事项
- 用中文和用户交流
- 生成图片后简要说明你使用的 prompt 思路
- 主动建议迭代方向
"""
async def run_agent_loop(
messages: list[dict],
ref_image_url: Optional[str] = None,
) -> AsyncGenerator[dict, None]:
"""
运行 Agent Loop以 SSE 事件流形式 yield 结果。
事件类型:
- text_delta: LLM 文字增量
- tool_start: 开始执行工具
- image_result: 图片生成结果
- done: 完成
- error: 错误
"""
# 构建消息列表
api_messages = [{"role": "system", "content": SYSTEM_PROMPT}]
for msg in messages:
if msg["role"] == "user" and ref_image_url:
# 最后一条用户消息附加参考图GPT-4o vision
if msg == messages[-1] or (
msg.get("role") == "user"
and messages.index(msg) == len(messages) - 1
):
api_messages.append({
"role": "user",
"content": [
{"type": "text", "text": msg["content"]},
{
"type": "image_url",
"image_url": {"url": ref_image_url},
},
],
})
continue
api_messages.append({"role": msg["role"], "content": msg["content"]})
client = _get_client()
max_iterations = 5
for _ in range(max_iterations):
try:
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=api_messages,
tools=TOOL_DEFINITIONS,
stream=True,
)
except Exception as e:
yield {"type": "error", "data": {"message": str(e)}}
return
collected_text = ""
tool_calls_data: dict[int, dict] = {}
async for chunk in response:
delta = chunk.choices[0].delta if chunk.choices else None
if not delta:
continue
# 文字内容
if delta.content:
collected_text += delta.content
yield {"type": "text_delta", "data": {"text": delta.content}}
# 工具调用(流式累积)
if delta.tool_calls:
for tc in delta.tool_calls:
idx = tc.index
if idx not in tool_calls_data:
tool_calls_data[idx] = {
"id": "",
"name": "",
"arguments": "",
}
if tc.id:
tool_calls_data[idx]["id"] = tc.id
if tc.function and tc.function.name:
tool_calls_data[idx]["name"] = tc.function.name
if tc.function and tc.function.arguments:
tool_calls_data[idx]["arguments"] += tc.function.arguments
finish_reason = chunk.choices[0].finish_reason if chunk.choices else None
# 如果没有工具调用,对话结束
if not tool_calls_data:
yield {"type": "done", "data": {}}
return
# 将 assistant 消息(含 tool_calls加入历史
assistant_msg: dict = {"role": "assistant"}
if collected_text:
assistant_msg["content"] = collected_text
else:
assistant_msg["content"] = None
assistant_msg["tool_calls"] = []
for idx in sorted(tool_calls_data.keys()):
tc_data = tool_calls_data[idx]
assistant_msg["tool_calls"].append({
"id": tc_data["id"],
"type": "function",
"function": {
"name": tc_data["name"],
"arguments": tc_data["arguments"],
},
})
api_messages.append(assistant_msg)
# 依次执行每个工具调用
for idx in sorted(tool_calls_data.keys()):
tc_data = tool_calls_data[idx]
tool_name = tc_data["name"]
yield {
"type": "tool_start",
"data": {"tool": tool_name, "message": "正在生成图片..."},
}
try:
arguments = json.loads(tc_data["arguments"])
except json.JSONDecodeError:
arguments = {}
result = await execute_tool(tool_name, arguments, ref_image_url)
# 如果有图片结果,推送给前端
if result.get("images"):
yield {
"type": "image_result",
"data": {"images": result["images"]},
}
# 工具结果回传给 LLM
api_messages.append({
"role": "tool",
"tool_call_id": tc_data["id"],
"content": json.dumps(result, ensure_ascii=False),
})
# 清除 ref_image_url避免后续轮次重复附加
ref_image_url = None
yield {"type": "done", "data": {}}