agent_runtime.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """Agent 运行时管理 —— 超时控制、重试策略与线程池调度."""
  2. from __future__ import annotations
  3. import concurrent.futures
  4. import logging
  5. from typing import Any, TypeVar
  6. from collections.abc import Callable
  7. from hello_agents import SimpleAgent
  8. from ...settings import get_settings
  9. from .agent_config import papergraph_agent_config
  10. logger = logging.getLogger(__name__)
  11. _T = TypeVar("_T")
  12. def _exception_chain_predicate(exc: BaseException | None, pred) -> bool:
  13. seen: set[int] = set()
  14. depth = 0
  15. cur: BaseException | None = exc
  16. while cur is not None and depth < 12:
  17. if id(cur) in seen:
  18. break
  19. seen.add(id(cur))
  20. try:
  21. if pred(cur):
  22. return True
  23. except Exception:
  24. pass
  25. nxt = cur.__cause__
  26. if nxt is None:
  27. nxt = getattr(cur, "__context__", None)
  28. cur = nxt
  29. depth += 1
  30. return False
  31. def _task_failed_due_to_timeout(exc: BaseException) -> bool:
  32. return _exception_chain_predicate(exc, lambda e: (
  33. isinstance(e, TimeoutError)
  34. or "timeout" in str(e).lower()
  35. or "timed out" in str(e).lower()
  36. ))
  37. def _run_with_optional_timeout(fn: Callable[[], _T], timeout_sec: float | None) -> _T:
  38. if timeout_sec is None or float(timeout_sec) <= 0:
  39. return fn()
  40. with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
  41. fut = ex.submit(fn)
  42. try:
  43. return fut.result(timeout=float(timeout_sec))
  44. except concurrent.futures.TimeoutError as exc:
  45. fut.cancel()
  46. raise TimeoutError(f"agent task timeout after {timeout_sec}s") from exc
  47. def run_agent_task(
  48. *,
  49. task_name: str,
  50. agent_name: str,
  51. llm: Any,
  52. system_prompt: str,
  53. user_prompt: str,
  54. timeout_sec: float | None = None,
  55. retries: int | None = None,
  56. task_logger: logging.Logger | None = None,
  57. ) -> str:
  58. log = task_logger or logger
  59. s = get_settings()
  60. resolved_timeout = float(timeout_sec) if timeout_sec is not None else float(
  61. getattr(s, "agent_runtime_default_timeout_sec", 20.0)
  62. )
  63. resolved_retries = int(retries) if retries is not None else int(
  64. getattr(s, "agent_runtime_default_retries", 1)
  65. )
  66. attempts = max(1, resolved_retries + 1)
  67. last_error: Exception | None = None
  68. for i in range(attempts):
  69. try:
  70. agent = SimpleAgent(
  71. name=agent_name,
  72. llm=llm,
  73. system_prompt=system_prompt,
  74. config=papergraph_agent_config(),
  75. )
  76. raw = _run_with_optional_timeout(lambda: agent.run(user_prompt), resolved_timeout)
  77. return (raw or "").strip()
  78. except Exception as exc:
  79. last_error = exc
  80. if i + 1 < attempts:
  81. log.warning("[%s] attempt %d/%d failed: %s", task_name, i + 1, attempts, exc)
  82. else:
  83. if _task_failed_due_to_timeout(last_error):
  84. log.warning("[%s] failed after %d attempt(s): %s", task_name, attempts, last_error)
  85. else:
  86. log.exception("[%s] failed after %d attempt(s)", task_name, attempts)
  87. raise RuntimeError(f"{task_name}_failed") from last_error
  88. def run_json_task(
  89. *,
  90. task_name: str,
  91. agent_name: str,
  92. llm: Any,
  93. system_prompt: str,
  94. user_prompt: str,
  95. timeout_sec: float | None = None,
  96. retries: int | None = None,
  97. default: dict[str, Any | None] = None,
  98. parse_fn: Callable[[str | None, dict[str, Any | None]]] = None,
  99. task_logger: logging.Logger | None = None,
  100. ) -> dict[str, Any]:
  101. log = task_logger or logger
  102. if parse_fn is None:
  103. from ..search_intent import extract_json_object
  104. parser = extract_json_object
  105. else:
  106. parser = parse_fn
  107. raw = run_agent_task(
  108. task_name=task_name,
  109. agent_name=agent_name,
  110. llm=llm,
  111. system_prompt=system_prompt,
  112. user_prompt=user_prompt,
  113. timeout_sec=timeout_sec,
  114. retries=retries,
  115. task_logger=log,
  116. )
  117. data = parser(raw)
  118. if isinstance(data, dict):
  119. return data
  120. log.warning("[%s] JSON parse failed, fallback to default", task_name)
  121. return dict(default or {})