Support selectable demo exchanges and configurable analysis limits
Persist manual requests until exchange confirmation, preserve stop protection, and reconcile fills before settling plans. Tighten model stream, crawler, skill, research accounting, and frontend race handling, with regression coverage. Keep queued analyses pending until a fresh account snapshot is available.
This commit is contained in:
1 parent
fcaedfb358
commit
285b3952d7
33 files changed
+1890
-325
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from datetime import UTC, datetime
|
||||
from typing import Annotated, Literal
|
||||
@@ -26,10 +27,11 @@ router = APIRouter(prefix="/api/chat", tags=["chat"], dependencies=[Depends(requ
|
||||
MAX_TOOL_ROUNDS = 8
|
||||
# 发给前端展示的工具结果长度
|
||||
TOOL_PREVIEW_CHARS = 2000
|
||||
STOPPED_RESULT = json.dumps({"error": "用户已停止生成,工具未执行"}, ensure_ascii=False)
|
||||
STOPPED_RESULT = json.dumps({"error": "用户已停止生成,未完成的工具结果尚未确认,请核对后再重试"}, ensure_ascii=False)
|
||||
|
||||
# api 只有一个进程:同一时间只允许一轮生成,Reset 也要等生成结束
|
||||
generating = asyncio.Lock()
|
||||
logger = logging.getLogger("alphapulse.api.chat")
|
||||
|
||||
|
||||
class MessageOut(BaseModel):
|
||||
@@ -180,35 +182,50 @@ async def send_message(body: SendIn) -> AsyncIterator[ServerSentEvent]:
|
||||
truncated_message = f"模型输出达到最大输出上限({provider.max_tokens} token)被截断,可在「环境变量」页调大「最大输出」"
|
||||
if completion.truncated and not completion.tool_calls:
|
||||
assistant["error"] = truncated_message
|
||||
await save_messages(ChatMessage(session_id=current.id, role="assistant", content=assistant, provider_id=provider.id, usage=completion.usage))
|
||||
messages.append({"role": "assistant", **assistant})
|
||||
yield event("usage", completion.usage)
|
||||
if not completion.tool_calls:
|
||||
if completion.truncated:
|
||||
yield event("error", {"message": truncated_message})
|
||||
break
|
||||
|
||||
pending = list(completion.tool_calls)
|
||||
assistant_saved = False
|
||||
try:
|
||||
# 消息提交和本地进度必须一起完成,避免取消恰好发生在提交之后而留下孤立调用。
|
||||
with anyio.CancelScope(shield=True):
|
||||
await save_messages(ChatMessage(session_id=current.id, role="assistant", content=assistant, provider_id=provider.id, usage=completion.usage))
|
||||
assistant_saved = True
|
||||
messages.append({"role": "assistant", **assistant})
|
||||
yield event("usage", completion.usage)
|
||||
if not completion.tool_calls:
|
||||
if completion.truncated:
|
||||
yield event("error", {"message": truncated_message})
|
||||
break
|
||||
while pending:
|
||||
call = pending[0]
|
||||
yield event("tool_call", {"id": call["id"], "name": call["name"], "arguments": call["arguments"]})
|
||||
# 截断时不执行,把原因告诉模型,让它精简后重试
|
||||
outcome = ToolOutcome(llm.TRUNCATED_RESULT) if completion.truncated else await run_tool(call["name"], call["arguments"])
|
||||
result = {"tool_call_id": call["id"], "name": call["name"], "result": outcome.result}
|
||||
await save_messages(ChatMessage(session_id=current.id, role="tool", content=result))
|
||||
pending.pop(0)
|
||||
with anyio.CancelScope(shield=True):
|
||||
await save_messages(ChatMessage(session_id=current.id, role="tool", content=result))
|
||||
pending.pop(0)
|
||||
messages.append({"role": "tool", **result})
|
||||
yield event("tool_result", {"id": call["id"], "name": call["name"], "preview": outcome.result[:TOOL_PREVIEW_CHARS]})
|
||||
if outcome.event:
|
||||
yield event(*outcome.event)
|
||||
except (asyncio.CancelledError, GeneratorExit):
|
||||
# 每个工具调用都要有结果,否则下一轮发给模型的历史不合法
|
||||
await save_shielded(*(
|
||||
ChatMessage(session_id=current.id, role="tool", content={"tool_call_id": c["id"], "name": c["name"], "result": STOPPED_RESULT})
|
||||
for c in pending
|
||||
))
|
||||
if assistant_saved and pending:
|
||||
await save_shielded(*(
|
||||
ChatMessage(session_id=current.id, role="tool", content={"tool_call_id": c["id"], "name": c["name"], "result": STOPPED_RESULT})
|
||||
for c in pending
|
||||
))
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("Chat 工具轮次失败")
|
||||
if assistant_saved and pending:
|
||||
failed = json.dumps({"error": "工具轮次异常中止,执行结果未确认;后续工具未执行"}, ensure_ascii=False)
|
||||
await save_shielded(*(
|
||||
ChatMessage(session_id=current.id, role="tool", content={"tool_call_id": c["id"], "name": c["name"], "result": failed})
|
||||
for c in pending
|
||||
))
|
||||
yield event("error", {"message": "工具轮次执行失败,请查看服务日志"})
|
||||
return
|
||||
yield event("done", {})
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user