aeval-framework 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. aeval_framework-0.1.0.dist-info/METADATA +42 -0
  2. aeval_framework-0.1.0.dist-info/RECORD +63 -0
  3. aeval_framework-0.1.0.dist-info/WHEEL +4 -0
  4. aeval_framework-0.1.0.dist-info/entry_points.txt +2 -0
  5. agent_eval/__init__.py +14 -0
  6. agent_eval/api/__init__.py +14 -0
  7. agent_eval/api/app.py +82 -0
  8. agent_eval/api/events.py +96 -0
  9. agent_eval/api/routes/__init__.py +0 -0
  10. agent_eval/api/routes/datasets.py +441 -0
  11. agent_eval/api/routes/graders.py +19 -0
  12. agent_eval/api/routes/metrics.py +49 -0
  13. agent_eval/api/routes/runs.py +573 -0
  14. agent_eval/api/routes/suites.py +84 -0
  15. agent_eval/api/routes/tasks.py +114 -0
  16. agent_eval/api/standalone.py +105 -0
  17. agent_eval/cli.py +455 -0
  18. agent_eval/core/__init__.py +48 -0
  19. agent_eval/core/contract.py +296 -0
  20. agent_eval/core/metrics.py +184 -0
  21. agent_eval/core/runner.py +868 -0
  22. agent_eval/core/suite.py +60 -0
  23. agent_eval/core/types.py +227 -0
  24. agent_eval/dataset/__init__.py +31 -0
  25. agent_eval/dataset/models.py +199 -0
  26. agent_eval/dataset/quality.py +194 -0
  27. agent_eval/dataset/sources/__init__.py +45 -0
  28. agent_eval/dataset/sources/llm_generator.py +219 -0
  29. agent_eval/dataset/sources/manual.py +172 -0
  30. agent_eval/dataset/sources/regression.py +201 -0
  31. agent_eval/dataset/sources/trace_mining.py +277 -0
  32. agent_eval/dataset/storage.py +342 -0
  33. agent_eval/dataset/version.py +72 -0
  34. agent_eval/examples/__init__.py +0 -0
  35. agent_eval/examples/basic_usage.py +175 -0
  36. agent_eval/examples/mock_runner.py +195 -0
  37. agent_eval/graders/__init__.py +91 -0
  38. agent_eval/graders/artifact_check.py +114 -0
  39. agent_eval/graders/code_based.py +101 -0
  40. agent_eval/graders/human.py +77 -0
  41. agent_eval/graders/metric.py +142 -0
  42. agent_eval/graders/model_based.py +179 -0
  43. agent_eval/graders/state_check.py +106 -0
  44. agent_eval/graders/step_level.py +116 -0
  45. agent_eval/graders/tool_calls.py +102 -0
  46. agent_eval/graders/transcript.py +86 -0
  47. agent_eval/metrics/__init__.py +110 -0
  48. agent_eval/metrics/answer_relevancy.py +57 -0
  49. agent_eval/metrics/base.py +155 -0
  50. agent_eval/metrics/batch_evaluation.py +267 -0
  51. agent_eval/metrics/context_precision.py +62 -0
  52. agent_eval/metrics/context_recall.py +71 -0
  53. agent_eval/metrics/faithfulness.py +72 -0
  54. agent_eval/metrics/llm_judge.py +100 -0
  55. agent_eval/metrics/prompt_metric.py +150 -0
  56. agent_eval/metrics/pytest_plugin.py +308 -0
  57. agent_eval/metrics/report.py +149 -0
  58. agent_eval/metrics/synthetic_data.py +203 -0
  59. agent_eval/storage/__init__.py +17 -0
  60. agent_eval/storage/memory.py +95 -0
  61. agent_eval/storage/sqlite.py +240 -0
  62. agent_eval/trace/__init__.py +16 -0
  63. agent_eval/trace/phoenix.py +144 -0
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.5
2
+ Name: aeval-framework
3
+ Version: 0.1.0
4
+ Summary: Aeval — open-source agent evaluation framework driven by OTel traces (suites, graders, metrics, storage, API, CLI)
5
+ Project-URL: Homepage, https://github.com/QiYuyyds/Aeval
6
+ Project-URL: Repository, https://github.com/QiYuyyds/Aeval
7
+ Project-URL: Documentation, https://github.com/QiYuyyds/Aeval/tree/main/docs
8
+ Project-URL: Changelog, https://github.com/QiYuyyds/Aeval/releases
9
+ Author: Aeval contributors
10
+ License: MIT
11
+ Keywords: agents,evaluation,llm,otel,testing,tracing
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: aiosqlite>=0.20.0
21
+ Requires-Dist: pydantic>=2.10.0
22
+ Requires-Dist: pyyaml>=6.0.0
23
+ Provides-Extra: api
24
+ Requires-Dist: fastapi>=0.115.0; extra == 'api'
25
+ Requires-Dist: sse-starlette>=2.2.0; extra == 'api'
26
+ Requires-Dist: uvicorn[standard]>=0.32.0; extra == 'api'
27
+ Provides-Extra: cli
28
+ Requires-Dist: rich>=13.0.0; extra == 'cli'
29
+ Requires-Dist: typer>=0.12.0; extra == 'cli'
30
+ Provides-Extra: dev
31
+ Requires-Dist: httpx>=0.28.0; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
34
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.8.0; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # agent-eval
39
+
40
+ Aeval — open-source agent evaluation framework driven by OTel traces.
41
+
42
+ See the repository root [README](../../README.md) (English) / [中文说明](../../README.zh-CN.md) and the [docs](../../docs/) for full documentation.
@@ -0,0 +1,63 @@
1
+ agent_eval/__init__.py,sha256=Bn20xqehr50-Xbz_W-2b_tLA81G2Q6q4xyg7L8iZYMk,347
2
+ agent_eval/cli.py,sha256=fSGZuDwcEjS3rjXjRAmGzDm0Sq90XKD537QQYsjN65I,16526
3
+ agent_eval/api/__init__.py,sha256=HCcNc8esBbtlnZETe2sas6cWe_BEC76WDuMtxxKITNM,290
4
+ agent_eval/api/app.py,sha256=bizUSDaus6AdAmfaMD03WiNsMF6i-2lk0QPTyhoByYg,2591
5
+ agent_eval/api/events.py,sha256=3vfWUTUByGzxjP2jNG_k0pfqb9ZyFGoQfscb35zZey8,4042
6
+ agent_eval/api/standalone.py,sha256=sr9yNNcgeKamP3BNrXwReVnhfCnFB8KylsxPIltx5vM,3112
7
+ agent_eval/api/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ agent_eval/api/routes/datasets.py,sha256=vEWoG-x22XbUBcNUOEiccg2-fJk09xujqQAywBYAXOE,17418
9
+ agent_eval/api/routes/graders.py,sha256=IlpOkPK1W7dz1va4UDhn8Ge92AveeA0afC9yQ9XNvXk,402
10
+ agent_eval/api/routes/metrics.py,sha256=oPdMgS-SuKtpvZWpFZ9xP6OJZo6FxwYeX4h2lfr3o-I,1682
11
+ agent_eval/api/routes/runs.py,sha256=HRB0XvCABYY0KtxKaolACXXE9xkBxU-plHDjYnCM3BQ,18904
12
+ agent_eval/api/routes/suites.py,sha256=wnjx4EgHjQ6IOY8_LE6Vf0jF3DfaCCR5JrtEUMvD1E4,2229
13
+ agent_eval/api/routes/tasks.py,sha256=iPAZJUlmbvrxoPyl2MD6f_Ij-qmXGYsUijGlkAtuaKc,3581
14
+ agent_eval/core/__init__.py,sha256=7wLbb03B1WsvVKJS_JcFLxjEFzra8lUZGhxjKfc6JhY,877
15
+ agent_eval/core/contract.py,sha256=8HtBEz8XDLG3U9dt3858ADwDdAjlEoquZeukoob00ro,8629
16
+ agent_eval/core/metrics.py,sha256=D0Fe3sHhNdNkGW9ROl9TGlDb4qpsSEiUbVt-nRlZftE,5094
17
+ agent_eval/core/runner.py,sha256=n4_pE2csGHAoek1Lng2ZTRC31JmJUpVg-huZSNM0RqI,32991
18
+ agent_eval/core/suite.py,sha256=wVDMo5RaeTwESADgKY0HEh8qDSEfoY0tgNqlwav6mIk,1776
19
+ agent_eval/core/types.py,sha256=4EO6UmeTvpfCcAoGTZPpjdfa0qvpneX5RqATMjtId44,9284
20
+ agent_eval/dataset/__init__.py,sha256=YFWjfanVH7DvCLr2UiPdN905eVq2anupKF8YATClLxE,846
21
+ agent_eval/dataset/models.py,sha256=H_y3w0dpa44SuiOplQwe3wUc0jHi_XTwYnip2G3d0d4,7387
22
+ agent_eval/dataset/quality.py,sha256=gWkju6kOHzJYT384O_Uia1RXzrC4DW13c_xg0J3wZDQ,6739
23
+ agent_eval/dataset/storage.py,sha256=wL3NLL0diL7d-1kHqGuFouemK8dQc4sjpRrxst2PgqU,12769
24
+ agent_eval/dataset/version.py,sha256=jovPkA2IlGFNJZVoQg88CpxhT_IiI-eHBs23el4mlsk,2382
25
+ agent_eval/dataset/sources/__init__.py,sha256=V5qd0_MZv3J4psOFy1ld2LiEgDh9k6RUl-Ih5_zYP9c,1052
26
+ agent_eval/dataset/sources/llm_generator.py,sha256=qH-WIG2ParYmNH-GwJXtbx-13W8ADux4s-Rv1X92JxE,7752
27
+ agent_eval/dataset/sources/manual.py,sha256=D74h2NqAZ7m3ec5W84hKPWQYIA9hveHlb3HqD5y157M,6087
28
+ agent_eval/dataset/sources/regression.py,sha256=uvaYeGqpzv9PRniLZMd4NOA7oa364BqjaZlp4mz5j-M,7387
29
+ agent_eval/dataset/sources/trace_mining.py,sha256=p_ATYS8SsHrfLlZkLpVRwAi63XDKwUaARxd9q6Unuxw,10056
30
+ agent_eval/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ agent_eval/examples/basic_usage.py,sha256=rY4o8eYhTA_zy0WPReQ7BGL1_-dL_DP4X8aI7YSOblw,5410
32
+ agent_eval/examples/mock_runner.py,sha256=XwcNJyfhhgN4WLygNKNLK3WIHvejmnCcvPI8J8_r2GM,6429
33
+ agent_eval/graders/__init__.py,sha256=E8EecyidxG8JwHRUnL594sUEot82T-dgqZH3pWj5noY,3403
34
+ agent_eval/graders/artifact_check.py,sha256=vKVD0AZZTkny9JXDPB9XXMCkqS7WNMJ-AycOoH3lCAk,3607
35
+ agent_eval/graders/code_based.py,sha256=MPIrsdEpyJOusCeM4gwwHiHwN0B3JrLFuEPSqLZHmEg,2984
36
+ agent_eval/graders/human.py,sha256=HEbjmuHWLkScOIbAHZwH3bbck_esCLdyTyrGW_b1NS8,2518
37
+ agent_eval/graders/metric.py,sha256=JE3Oll8bVS371BpPE3BCKBogOzlCm-iJd-RZW_xrrl8,5348
38
+ agent_eval/graders/model_based.py,sha256=PGNNQdanFgGjSO5wYjMvmwAK7L9BropK1DYIaeuAyZA,5477
39
+ agent_eval/graders/state_check.py,sha256=Qb1t-qxMkB9xCQVqsHQqHzGlDRRFZJHoRTDXoI5Oh58,3336
40
+ agent_eval/graders/step_level.py,sha256=Q6p5nurKgyZ_oralaFnNFp9DPJeAshXBC4XIAM3Cwb8,3963
41
+ agent_eval/graders/tool_calls.py,sha256=4XprzqLbB5C3Uqdr2Wgb8wezyNzLH5EWSAPexwrsir0,3053
42
+ agent_eval/graders/transcript.py,sha256=gxY5gMX-IN7OiAtNaCoCcfNIvqAyky0KhgX5DAcaomI,2545
43
+ agent_eval/metrics/__init__.py,sha256=smLtMTeP19q15bJMwIuJbLi2AWcVRbaW9SX3es74GT8,3508
44
+ agent_eval/metrics/answer_relevancy.py,sha256=NMr5T7fw-3xWPAT8WyCZuA7TIhRZWvTq8HsIYw1zrhA,1764
45
+ agent_eval/metrics/base.py,sha256=frv2UQrU6P4vtAKzAU8C5rZKwqIf5Q-ClPD2F9J3tjQ,5080
46
+ agent_eval/metrics/batch_evaluation.py,sha256=T5flsYzP_e7kL9U_IJbv2dib7IUFHI5bYAe06HGrIYc,9551
47
+ agent_eval/metrics/context_precision.py,sha256=GrWKdliY7JmHLN1f3WuTUVzYKtL2tM1PmvOpp-w8an4,2053
48
+ agent_eval/metrics/context_recall.py,sha256=aj9jxubBoJOO_yEeZj4dU1SqSz77e1KHdy-Z8hmHZds,2462
49
+ agent_eval/metrics/faithfulness.py,sha256=0k71cbItS3pzLwh4aRPu3wDfOUybHZJ7PlfIVi_dvns,2594
50
+ agent_eval/metrics/llm_judge.py,sha256=PIDHiFo8ldhW-a0GmhpvII4NqGkPDFkTBrLwlCg5pRI,3202
51
+ agent_eval/metrics/prompt_metric.py,sha256=RY8ISi7dKgbPtl0ovnJIYuUYxUj4fP0msEK5jT_SHuU,5345
52
+ agent_eval/metrics/pytest_plugin.py,sha256=IlT9fVYpvQ0JlhysNucbg0gIbKXCH9fON2swteKUJu4,10901
53
+ agent_eval/metrics/report.py,sha256=7ZIvUGjb82l8DcJ6AC7cPytZX7aZm65aLJWZKe-eheQ,5696
54
+ agent_eval/metrics/synthetic_data.py,sha256=rNQgNF7HpKDrqv9XWFa0nAsDImIOQfsAjYwgV9p--X8,7293
55
+ agent_eval/storage/__init__.py,sha256=1D5QpjD7BI1j5HBfv51m67XwsEr8UFOwm1Hops5D5zg,475
56
+ agent_eval/storage/memory.py,sha256=0bcKS3RoT4Qn9HFpV46RGfKMnzqiMf7Cgybnvo8P_cc,2999
57
+ agent_eval/storage/sqlite.py,sha256=iiVP7vdsq5kI550Hs185TO2ja4g7q0QOtBhu6UBOdXU,8594
58
+ agent_eval/trace/__init__.py,sha256=zaGBaoW2UJ8zbOWJwjf5GsNLEBe1t3DjbrD4WihuaUk,459
59
+ agent_eval/trace/phoenix.py,sha256=XC5VH4bzmXDvlwPbWxelUeBsJxQ5zLfs1yU2mrtQfCg,4208
60
+ aeval_framework-0.1.0.dist-info/METADATA,sha256=gTEq9P4dhAu00Lps2dGoMlPeQQ9lWEcwFwFps2p3rbE,1819
61
+ aeval_framework-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
62
+ aeval_framework-0.1.0.dist-info/entry_points.txt,sha256=oKoXCZu-JXxiaSMdl2WaMqkb5VAcvBT3OFXzwwx4TWM,51
63
+ aeval_framework-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ eval-suite = agent_eval.cli:main
agent_eval/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ Aeval — Agent Evaluation Framework
3
+
4
+ A reusable, open-source evaluation framework for AI agents, driven by OTel traces.
5
+
6
+ Usage:
7
+ from agent_eval import EvalRunner, EvalSuite
8
+
9
+ runner = EvalRunner(agent_runner=my_runner)
10
+ suite = EvalSuite.from_yaml("suite.yaml")
11
+ result = await runner.run_suite(suite)
12
+ """
13
+
14
+ __version__ = "0.1.0"
@@ -0,0 +1,14 @@
1
+ """
2
+ REST API for the Aeval evaluation framework.
3
+
4
+ Provides FastAPI routes for managing suites, runs, and viewing results.
5
+
6
+ Usage:
7
+ from agent_eval.api import create_app
8
+
9
+ app = create_app(runner=my_eval_runner)
10
+ """
11
+
12
+ from agent_eval.api.app import create_app
13
+
14
+ __all__ = ["create_app"]
agent_eval/api/app.py ADDED
@@ -0,0 +1,82 @@
1
+ """
2
+ FastAPI application factory for Aeval.
3
+
4
+ Creates a FastAPI app with all eval routes mounted.
5
+ Can be used standalone or mounted in an existing app.
6
+
7
+ Usage:
8
+ # Standalone
9
+ app = create_app(runner=my_runner)
10
+
11
+ # Mounted in existing FastAPI app
12
+ from fastapi import FastAPI
13
+ app = FastAPI()
14
+ eval_app = create_app(runner=my_runner)
15
+ app.mount("/api/eval", eval_app)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from fastapi import FastAPI
21
+
22
+ from agent_eval.api.routes import datasets, graders, metrics, runs, suites, tasks
23
+ from agent_eval.core.runner import EvalRunner
24
+
25
+ # Global runner reference (set by create_app)
26
+ _runner: EvalRunner | None = None
27
+
28
+
29
+ def _get_runner() -> EvalRunner | None:
30
+ """Get the global EvalRunner instance"""
31
+ return _runner
32
+
33
+
34
+ def set_runner(runner: EvalRunner | None) -> None:
35
+ """Set the global EvalRunner instance.
36
+
37
+ Used by the host app to inject the real runner during async startup
38
+ (e.g. main.py lifespan → eval_integration.config.create_aeval_runner).
39
+ """
40
+ global _runner
41
+ _runner = runner
42
+
43
+
44
+ def create_app(runner: EvalRunner | None = None) -> FastAPI:
45
+ """
46
+ Create a FastAPI app with Aeval routes.
47
+
48
+ Args:
49
+ runner: EvalRunner instance. If None, routes will return 503.
50
+
51
+ Returns:
52
+ FastAPI application
53
+ """
54
+ app = FastAPI(
55
+ title="Aeval API",
56
+ version="0.1.0",
57
+ description="Agent Evaluation Framework API",
58
+ )
59
+
60
+ # Store runner in app state and global reference
61
+ global _runner
62
+ _runner = runner
63
+ app.state.runner = runner
64
+
65
+ # Include routers
66
+ app.include_router(suites.router, prefix="/suites", tags=["suites"])
67
+ app.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
68
+ app.include_router(runs.router, prefix="/runs", tags=["runs"])
69
+ # compare 挂在 /compare (spec §REST API), 不带 /runs 前缀
70
+ app.include_router(runs.compare_router, tags=["compare"])
71
+ app.include_router(graders.router, prefix="/graders", tags=["graders"])
72
+ # 数据集管理 (change ③: 数据集构建闭环 — CRUD/导入/挖掘/生成/质量/to-suite)
73
+ app.include_router(datasets.router, prefix="/datasets", tags=["datasets"])
74
+ # 批量评测 (change ④: 对已有输出直接批量打分 — POST /metrics/batch)
75
+ app.include_router(metrics.router, prefix="/metrics", tags=["metrics"])
76
+
77
+ @app.get("/health")
78
+ async def health():
79
+ # 动态读取全局 runner — host app 会在 startup 阶段注入真实 runner
80
+ return {"status": "ok", "runner_configured": _get_runner() is not None}
81
+
82
+ return app
@@ -0,0 +1,96 @@
1
+ """Run 事件总线 — SSE 实时进度的进程内 fan-out (任务 3.1)。
2
+
3
+ 协议 (设计文档 D3 / §17.3):
4
+ - EvalRunner 进度回调 → per-run 订阅者队列扇出; 事件不落库, 断线恢复
5
+ 完全依赖快照重拉 (GET /runs/{run_id}) + 重订阅
6
+ - 事件类型: task_start / trial_start / trial_complete / task_complete /
7
+ run_complete / error; 载荷含 task_id 与 trial_index (适用时), 按
8
+ (task_id, trial_index) 幂等 — 重复事件由客户端快照自愈兜底
9
+ - run 生命周期由服务端后台任务持有, 与观察连接解耦
10
+
11
+ 终态保留: run_complete 事件缓存于进程内 (有界, LIFO 淘汰), 供 run 结束后
12
+ 订阅的客户端立即收终态并关流; 进程重启后缓存消失, 客户端以快照为准。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import contextlib
19
+ import time
20
+ from collections import OrderedDict
21
+ from typing import Any
22
+
23
+ # 有界队列: 慢订阅者丢最旧事件 (快照可自愈), 不阻塞 run 主流程
24
+ _QUEUE_MAXSIZE = 256
25
+ # 终态事件缓存上限 (防止长进程内存无界增长)
26
+ _TERMINAL_CACHE_MAX = 500
27
+
28
+ TERMINAL_EVENT_TYPES = {"run_complete"}
29
+
30
+
31
+ class RunEventBus:
32
+ """run_id → 订阅者队列集合的事件扇出枢纽 (单事件循环, 无锁)。"""
33
+
34
+ def __init__(self) -> None:
35
+ self._subscribers: dict[str, set[asyncio.Queue]] = {}
36
+ self._terminal: OrderedDict[str, dict[str, Any]] = OrderedDict()
37
+
38
+ # ── 发布 ─────────────────────────────────────────────────────────────
39
+
40
+ def publish(self, run_id: str, event_type: str, data: dict[str, Any] | None = None) -> None:
41
+ """发布一条 run 事件 (非阻塞)。"""
42
+ event: dict[str, Any] = {
43
+ "type": event_type,
44
+ "run_id": run_id,
45
+ "timestamp": time.time() * 1000,
46
+ **(data or {}),
47
+ }
48
+ if event_type in TERMINAL_EVENT_TYPES:
49
+ self._terminal[run_id] = event
50
+ self._terminal.move_to_end(run_id)
51
+ while len(self._terminal) > _TERMINAL_CACHE_MAX:
52
+ self._terminal.popitem(last=False)
53
+
54
+ for queue in self._subscribers.get(run_id, set()):
55
+ _offer(queue, event)
56
+
57
+ # ── 订阅 ─────────────────────────────────────────────────────────────
58
+
59
+ def subscribe(self, run_id: str) -> asyncio.Queue:
60
+ """注册订阅者队列; run 已有缓存终态时立即注入终态事件。"""
61
+ queue: asyncio.Queue = asyncio.Queue(maxsize=_QUEUE_MAXSIZE)
62
+ self._subscribers.setdefault(run_id, set()).add(queue)
63
+ terminal = self._terminal.get(run_id)
64
+ if terminal is not None:
65
+ _offer(queue, terminal)
66
+ return queue
67
+
68
+ def unsubscribe(self, run_id: str, queue: asyncio.Queue) -> None:
69
+ """注销订阅者 (SSE 断开时调用, 防泄漏)。"""
70
+ subscribers = self._subscribers.get(run_id)
71
+ if subscribers is None:
72
+ return
73
+ subscribers.discard(queue)
74
+ if not subscribers:
75
+ self._subscribers.pop(run_id, None)
76
+
77
+ def subscriber_count(self, run_id: str) -> int:
78
+ return len(self._subscribers.get(run_id, set()))
79
+
80
+ def terminal_event(self, run_id: str) -> dict[str, Any] | None:
81
+ return self._terminal.get(run_id)
82
+
83
+
84
+ def _offer(queue: asyncio.Queue, event: dict[str, Any]) -> None:
85
+ """put_nowait + 最旧丢弃溢出策略 (与 AChat event_bus 行为一致)。"""
86
+ try:
87
+ queue.put_nowait(event)
88
+ except asyncio.QueueFull:
89
+ with contextlib.suppress(asyncio.QueueEmpty):
90
+ queue.get_nowait()
91
+ with contextlib.suppress(asyncio.QueueFull): # pragma: no cover - 理论不可达
92
+ queue.put_nowait(event)
93
+
94
+
95
+ # 全局单例 (与 eval API 子应用同生命周期)
96
+ run_event_bus = RunEventBus()
File without changes