aeval-framework 0.1.0__tar.gz
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.
- aeval_framework-0.1.0/.coverage +0 -0
- aeval_framework-0.1.0/.gitignore +23 -0
- aeval_framework-0.1.0/PKG-INFO +42 -0
- aeval_framework-0.1.0/README.md +5 -0
- aeval_framework-0.1.0/pyproject.toml +80 -0
- aeval_framework-0.1.0/src/agent_eval/__init__.py +14 -0
- aeval_framework-0.1.0/src/agent_eval/api/__init__.py +14 -0
- aeval_framework-0.1.0/src/agent_eval/api/app.py +82 -0
- aeval_framework-0.1.0/src/agent_eval/api/events.py +96 -0
- aeval_framework-0.1.0/src/agent_eval/api/routes/__init__.py +0 -0
- aeval_framework-0.1.0/src/agent_eval/api/routes/datasets.py +441 -0
- aeval_framework-0.1.0/src/agent_eval/api/routes/graders.py +19 -0
- aeval_framework-0.1.0/src/agent_eval/api/routes/metrics.py +49 -0
- aeval_framework-0.1.0/src/agent_eval/api/routes/runs.py +573 -0
- aeval_framework-0.1.0/src/agent_eval/api/routes/suites.py +84 -0
- aeval_framework-0.1.0/src/agent_eval/api/routes/tasks.py +114 -0
- aeval_framework-0.1.0/src/agent_eval/api/standalone.py +105 -0
- aeval_framework-0.1.0/src/agent_eval/cli.py +455 -0
- aeval_framework-0.1.0/src/agent_eval/core/__init__.py +48 -0
- aeval_framework-0.1.0/src/agent_eval/core/contract.py +296 -0
- aeval_framework-0.1.0/src/agent_eval/core/metrics.py +184 -0
- aeval_framework-0.1.0/src/agent_eval/core/runner.py +868 -0
- aeval_framework-0.1.0/src/agent_eval/core/suite.py +60 -0
- aeval_framework-0.1.0/src/agent_eval/core/types.py +227 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/__init__.py +31 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/models.py +199 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/quality.py +194 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/sources/__init__.py +45 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/sources/llm_generator.py +219 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/sources/manual.py +172 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/sources/regression.py +201 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/sources/trace_mining.py +277 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/storage.py +342 -0
- aeval_framework-0.1.0/src/agent_eval/dataset/version.py +72 -0
- aeval_framework-0.1.0/src/agent_eval/examples/__init__.py +0 -0
- aeval_framework-0.1.0/src/agent_eval/examples/basic_usage.py +175 -0
- aeval_framework-0.1.0/src/agent_eval/examples/mock_runner.py +195 -0
- aeval_framework-0.1.0/src/agent_eval/graders/__init__.py +91 -0
- aeval_framework-0.1.0/src/agent_eval/graders/artifact_check.py +114 -0
- aeval_framework-0.1.0/src/agent_eval/graders/code_based.py +101 -0
- aeval_framework-0.1.0/src/agent_eval/graders/human.py +77 -0
- aeval_framework-0.1.0/src/agent_eval/graders/metric.py +142 -0
- aeval_framework-0.1.0/src/agent_eval/graders/model_based.py +179 -0
- aeval_framework-0.1.0/src/agent_eval/graders/state_check.py +106 -0
- aeval_framework-0.1.0/src/agent_eval/graders/step_level.py +116 -0
- aeval_framework-0.1.0/src/agent_eval/graders/tool_calls.py +102 -0
- aeval_framework-0.1.0/src/agent_eval/graders/transcript.py +86 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/__init__.py +110 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/answer_relevancy.py +57 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/base.py +155 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/batch_evaluation.py +267 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/context_precision.py +62 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/context_recall.py +71 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/faithfulness.py +72 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/llm_judge.py +100 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/prompt_metric.py +150 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/pytest_plugin.py +308 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/report.py +149 -0
- aeval_framework-0.1.0/src/agent_eval/metrics/synthetic_data.py +203 -0
- aeval_framework-0.1.0/src/agent_eval/storage/__init__.py +17 -0
- aeval_framework-0.1.0/src/agent_eval/storage/memory.py +95 -0
- aeval_framework-0.1.0/src/agent_eval/storage/sqlite.py +240 -0
- aeval_framework-0.1.0/src/agent_eval/trace/__init__.py +16 -0
- aeval_framework-0.1.0/src/agent_eval/trace/phoenix.py +144 -0
- aeval_framework-0.1.0/tests/conftest.py +9 -0
- aeval_framework-0.1.0/tests/test_api.py +325 -0
- aeval_framework-0.1.0/tests/test_builtin_graders.py +298 -0
- aeval_framework-0.1.0/tests/test_cli.py +280 -0
- aeval_framework-0.1.0/tests/test_e2e.py +243 -0
- aeval_framework-0.1.0/tests/test_eval_batch_evaluation.py +298 -0
- aeval_framework-0.1.0/tests/test_eval_dataset_api.py +409 -0
- aeval_framework-0.1.0/tests/test_eval_dataset_sources.py +596 -0
- aeval_framework-0.1.0/tests/test_eval_dataset_storage.py +185 -0
- aeval_framework-0.1.0/tests/test_eval_metric_pipeline.py +319 -0
- aeval_framework-0.1.0/tests/test_eval_metrics_api.py +157 -0
- aeval_framework-0.1.0/tests/test_eval_metrics_module.py +348 -0
- aeval_framework-0.1.0/tests/test_eval_prompt_metric.py +159 -0
- aeval_framework-0.1.0/tests/test_eval_pytest_plugin.py +218 -0
- aeval_framework-0.1.0/tests/test_eval_report.py +206 -0
- aeval_framework-0.1.0/tests/test_eval_task_history.py +167 -0
- aeval_framework-0.1.0/tests/test_graders.py +204 -0
- aeval_framework-0.1.0/tests/test_import_isolation.py +83 -0
- aeval_framework-0.1.0/tests/test_metrics.py +130 -0
- aeval_framework-0.1.0/tests/test_runner.py +555 -0
- aeval_framework-0.1.0/tests/test_sse.py +173 -0
- aeval_framework-0.1.0/tests/test_standalone_api.py +127 -0
- aeval_framework-0.1.0/tests/test_suite.py +209 -0
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
*.db
|
|
11
|
+
*.sqlite3
|
|
12
|
+
|
|
13
|
+
# Node / Next.js (dashboard)
|
|
14
|
+
node_modules/
|
|
15
|
+
.next/
|
|
16
|
+
out/
|
|
17
|
+
*.tsbuildinfo
|
|
18
|
+
|
|
19
|
+
# Env & OS
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
.DS_Store
|
|
23
|
+
Thumbs.db
|
|
@@ -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,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
# Distribution name: aeval-framework (PyPI). Python module: agent_eval, CLI: eval-suite.
|
|
3
|
+
# (`agent-eval` on PyPI belongs to an unrelated project — UK AISI agenteval.)
|
|
4
|
+
name = "aeval-framework"
|
|
5
|
+
version = "0.1.0"
|
|
6
|
+
description = "Aeval — open-source agent evaluation framework driven by OTel traces (suites, graders, metrics, storage, API, CLI)"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
authors = [{ name = "Aeval contributors" }]
|
|
11
|
+
keywords = ["agents", "evaluation", "llm", "otel", "tracing", "testing"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pydantic>=2.10.0",
|
|
24
|
+
"pyyaml>=6.0.0",
|
|
25
|
+
"aiosqlite>=0.20.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
# REST API / SSE service (create_app / create_standalone_app)
|
|
30
|
+
api = [
|
|
31
|
+
"fastapi>=0.115.0",
|
|
32
|
+
"sse-starlette>=2.2.0",
|
|
33
|
+
"uvicorn[standard]>=0.32.0",
|
|
34
|
+
]
|
|
35
|
+
# eval-suite command line
|
|
36
|
+
cli = [
|
|
37
|
+
"typer>=0.12.0",
|
|
38
|
+
"rich>=13.0.0",
|
|
39
|
+
]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.3.0",
|
|
42
|
+
"pytest-asyncio>=0.24.0",
|
|
43
|
+
"pytest-cov>=6.0.0",
|
|
44
|
+
"httpx>=0.28.0",
|
|
45
|
+
"ruff>=0.8.0",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
eval-suite = "agent_eval.cli:main"
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/QiYuyyds/Aeval"
|
|
53
|
+
Repository = "https://github.com/QiYuyyds/Aeval"
|
|
54
|
+
Documentation = "https://github.com/QiYuyyds/Aeval/tree/main/docs"
|
|
55
|
+
Changelog = "https://github.com/QiYuyyds/Aeval/releases"
|
|
56
|
+
|
|
57
|
+
[build-system]
|
|
58
|
+
requires = ["hatchling"]
|
|
59
|
+
build-backend = "hatchling.build"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/agent_eval"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
testpaths = ["tests"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
target-version = "py311"
|
|
70
|
+
line-length = 100
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
74
|
+
ignore = [
|
|
75
|
+
"E501",
|
|
76
|
+
"B008",
|
|
77
|
+
# (str, Enum) mixins are intentional: StrEnum would change str(member) /
|
|
78
|
+
# f-string serialization to the bare value and break API/storage payloads.
|
|
79
|
+
"UP042",
|
|
80
|
+
]
|
|
@@ -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"]
|
|
@@ -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
|