async_runner.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """find_agent 异步运行与资源清理。"""
  2. from __future__ import annotations
  3. import asyncio
  4. import logging
  5. import threading
  6. from supply_agent.agent.core import Agent
  7. from supply_agent.types import AgentResult
  8. logger = logging.getLogger(__name__)
  9. _thread_loop = threading.local()
  10. _ASYNC_CLEANUP_TIMEOUT_SECONDS = 5.0
  11. async def _drain_event_loop() -> None:
  12. """Bound cleanup of httpx/OpenAI background tasks."""
  13. loop = asyncio.get_running_loop()
  14. pending = {
  15. task
  16. for task in asyncio.all_tasks(loop)
  17. if task is not asyncio.current_task() and not task.done()
  18. }
  19. if not pending:
  20. return
  21. done, still_pending = await asyncio.wait(
  22. pending,
  23. timeout=_ASYNC_CLEANUP_TIMEOUT_SECONDS,
  24. )
  25. for task in done:
  26. if task.cancelled():
  27. continue
  28. error = task.exception()
  29. if error is not None:
  30. logger.debug("pending async task error during drain: %s", error)
  31. if still_pending:
  32. logger.warning(
  33. "Cancelling %d async cleanup task(s) after %.0fs",
  34. len(still_pending),
  35. _ASYNC_CLEANUP_TIMEOUT_SECONDS,
  36. )
  37. for task in still_pending:
  38. task.cancel()
  39. await asyncio.wait(still_pending, timeout=1.0)
  40. async def _close_agent_async_resources(agent: Agent) -> None:
  41. """在事件循环关闭前显式释放异步 HTTP 连接。"""
  42. async_client = getattr(agent.llm, "_async_client", None)
  43. if async_client is not None:
  44. try:
  45. await asyncio.wait_for(
  46. async_client.close(),
  47. timeout=_ASYNC_CLEANUP_TIMEOUT_SECONDS,
  48. )
  49. except TimeoutError:
  50. logger.warning(
  51. "close async llm client timed out after %.0fs",
  52. _ASYNC_CLEANUP_TIMEOUT_SECONDS,
  53. )
  54. except Exception:
  55. logger.debug("close async llm client failed", exc_info=True)
  56. await _drain_event_loop()
  57. async def arun_find_agent(
  58. agent: Agent,
  59. user_input: str,
  60. *,
  61. timeout_seconds: float,
  62. ) -> AgentResult:
  63. """在单个事件循环内运行 find_agent 并确保资源释放。"""
  64. try:
  65. return await asyncio.wait_for(
  66. agent.arun(user_input),
  67. timeout=timeout_seconds,
  68. )
  69. except TimeoutError:
  70. logger.error("find_agent timed out after %.0fs", timeout_seconds)
  71. raise TimeoutError(
  72. f"find_agent timed out after {timeout_seconds:.0f}s"
  73. ) from None
  74. finally:
  75. await _close_agent_async_resources(agent)
  76. def _run_coroutine(coro) -> AgentResult:
  77. """同步入口:主线程用 asyncio.run,worker 线程复用线程级 loop。"""
  78. try:
  79. asyncio.get_running_loop()
  80. except RuntimeError:
  81. pass
  82. else:
  83. raise RuntimeError("run_find_agent 不能在已运行的事件循环内调用")
  84. if threading.current_thread() is threading.main_thread():
  85. return asyncio.run(coro)
  86. loop = getattr(_thread_loop, "loop", None)
  87. if loop is None or loop.is_closed():
  88. loop = asyncio.new_event_loop()
  89. _thread_loop.loop = loop
  90. asyncio.set_event_loop(loop)
  91. return loop.run_until_complete(coro)