interrupts.py 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. """中断展示与恢复共用一个载荷和标识,批准范围等于卡片展示范围。"""
  2. import hashlib
  3. import json
  4. from fastapi import HTTPException
  5. def pack(item):
  6. value = getattr(item, "value", item)
  7. payload = dict(value) if isinstance(value, dict) else {"message": str(value)}
  8. if getattr(item, "id", None):
  9. payload["_interrupt_id"] = item.id
  10. return payload
  11. def token_for(payload):
  12. return hashlib.sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str).encode()).hexdigest()
  13. def interrupt_frame(writer, payload):
  14. payload = payload if isinstance(payload, dict) else {"message": str(payload)}
  15. token = token_for(payload)
  16. if payload.get("kind") == "ask_user":
  17. return writer.frame("interrupt", interrupt_token=token,
  18. ask_user={"questions": payload["questions"]})
  19. actions = payload.get("action_requests") or []
  20. if actions:
  21. first = actions[0]
  22. return writer.interrupt_tool_approval(
  23. tool=first.get("name", ""), args=first.get("args"),
  24. reason=first.get("description", ""), side_effect="批准后执行卡片中的全部操作",
  25. actions=actions, pending_count=len(actions), interrupt_token=token,
  26. )
  27. return writer.frame("interrupt", message=payload.get("message", "智能体暂停,等待输入"),
  28. interrupt_token=token)
  29. def resume_value(request, state):
  30. from api.chat_routes import _to_official_resume
  31. items = [item for task in getattr(state, "tasks", ()) for item in getattr(task, "interrupts", ())]
  32. if not items:
  33. raise HTTPException(status_code=409, detail={"code": "no_pending_interrupt", "message": "没有待确认请求"})
  34. selected = next((item for item in items if token_for(pack(item)) == request.interrupt_token), None)
  35. if selected is None:
  36. raise HTTPException(status_code=409, detail={"code": "stale_interrupt", "message": "确认请求已变化,请刷新后重试"})
  37. payload = pack(selected)
  38. if payload.get("kind") == "ask_user":
  39. if request.status == "cancelled":
  40. value = {"cancelled": True}
  41. else:
  42. if not request.answers or len(request.answers) != len(payload["questions"]) or any(not a.strip() for a in request.answers):
  43. raise HTTPException(status_code=400, detail={"code": "invalid_answers", "message": "请回答全部问题"})
  44. value = {"answers": request.answers}
  45. else:
  46. if request.approved is None and request.status not in {"cancelled", "error"}:
  47. raise HTTPException(status_code=400, detail={"code": "invalid_decision", "message": "请选择批准或拒绝"})
  48. value = _to_official_resume(request, len(payload.get("action_requests") or []))
  49. interrupt_id = getattr(selected, "id", None)
  50. return {interrupt_id: value} if interrupt_id else value