ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,192 @@
# Public-health reporting agent evaluation
## English
A small, reproducible Chapter 6 practice project for evaluating an agent over **synthetic DHIS2-style aggregate malaria-reporting data**. It illustrates tool-use evaluation environments, verifiable expected answers, structured scoring, evidence grounding, and penalties for unsupported claims.
> **Educational case study only.** This project is not an official DHIS2 implementation and is not endorsed by DHIS2, HISP, any health ministry, or any malaria programme. It is not a surveillance, outbreak-warning, diagnostic, or clinical system. Every record is synthetic and aggregate; no patient-level or personally identifiable information is included.
## What is evaluated
Five deterministic tasks cover:
1. Test positivity
2. Reporting completeness
3. Period-to-period trend comparison
4. Aggregate data-quality checks
5. Commodity stock-out review
Each prediction is a transparent JSON trace containing the selected tool, arguments, result, source-row evidence, and claims. The evaluator awards six points per task:
| Criterion | Points | Verification |
| --- | :---: | --- |
| Tool selection | 1 | Exact tool name |
| Arguments | 1 | Exact structured arguments |
| Answer | 2 | Deterministic values with numeric tolerance |
| Evidence | 1 | Exact set of synthetic source-row IDs |
| Grounding and safety | 1 | Every claim is in the supported-claim allowlist |
## Files
| File | Purpose |
| --- | --- |
| `data/synthetic_reports.csv` | Nine synthetic monthly aggregate reports |
| `tasks.json` | Prompts and deterministic tool plans |
| `expected_answers.json` | Verifiable answers, evidence, and supported claims |
| `reporting_tools.py` | Five auditable reporting tools |
| `agent.py` | Lightweight deterministic reference agent |
| `evaluator.py` | Objective six-point scoring rubric |
| `demo.py` | CLI for reference or external predictions |
| `tests/` | Offline regression and mutation tests |
## Run offline
The demo uses only Python's standard library and needs no API key:
```bash
# From the repository root: use the shared Chapter 6 environment
uv sync --locked --python 3.12 --extra ch6
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch6]"
cd chapter7/public-health-reporting-eval
python demo.py
```
Expected summary:
```text
positivity-alpha-jan 6/6
completeness-district-jan 6/6
trend-alpha-jan-feb 6/6
quality-demo-feb 6/6
stockout-demo-feb 6/6
------------------------------------
TOTAL 30/30
```
Run the offline tests:
```bash
# From the repository root, include the test environment
uv sync --locked --python 3.12 --extra ch6 --extra dev
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
cd chapter7/public-health-reporting-eval
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
python -m pytest tests
```
## Evaluate another agent
Save its structured predictions as a JSON array with the same shape produced by the reference agent, then run:
```bash
python demo.py --predictions my_predictions.json --output evaluation.json
```
This boundary keeps model/framework integration outside the benchmark. Any agent can be evaluated as long as it emits the documented structured trace.
## Interpretation and limitations
- The benchmark measures correctness on a deliberately small, controlled environment; it does not establish real-world readiness.
- Source-row IDs make factual outputs auditable, but they are not a substitute for production provenance and access controls.
- Exact tool and argument scoring is intentionally strict. Alternative valid plans would need additional accepted traces.
- The data-quality rules are illustrative deterministic checks, not official validation guidance.
- Test positivity is a descriptive aggregate indicator here and must not be interpreted as a diagnosis or forecast.
---
## 中文
这是一个面向《深入理解 AI Agent》第6章的小型可复现实践:在**合成 DHIS2 风格的疟疾上报聚合数据**上做 Agent 评测。
### 评测内容
包含 5 个确定性任务:
1. 阳性检出率
2. 报告完整性
3. 月度趋势比较
4. 聚合质量检查
5. 药品断货复核
每条预测输出为一段 JSON 结构,包含所选工具、参数、返回结果、证据行 ID、claim。评分为 6 分制:
- 工具选择(1
- 参数匹配(1
- 答案正确性(2
- 证据可追溯(1
- grounding/safety1
### 文件说明
同上英文表。
### 直接离线运行
```bash
# 在仓库根目录使用统一的第 6 章环境
uv sync --locked --python 3.12 --extra ch6
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch6]"
cd chapter7/public-health-reporting-eval
python demo.py
```
### 运行测试
```bash
# 在仓库根目录包含测试环境
uv sync --locked --python 3.12 --extra ch6 --extra dev
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
cd chapter7/public-health-reporting-eval
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
python -m pytest tests
```
### 评测外部 Agent
把外部模型或 agent 的预测导出为同样结构的 JSON,再运行:
```bash
python demo.py --predictions my_predictions.json --output evaluation.json
```
### 使用边界与局限
- 评测面向受控合成环境,不代表真实系统可上线。
- source-row 证据便于审计,但不替代生产级数据血缘与权限体系。
- 工具和参数打分采用严格匹配。
- 质量规则是示例性规则,不可等同真实质量体系。
- 阳性率仅为聚合描述指标,不用于诊断或预测。
@@ -0,0 +1,26 @@
"""A deterministic reference agent for the reporting evaluation environment."""
from __future__ import annotations
from typing import Any
from reporting_tools import ReportingEnvironment
class DeterministicReportingAgent:
"""Executes the task's explicit tool plan and returns a structured trace."""
def __init__(self, environment: ReportingEnvironment, expected: dict[str, dict[str, Any]]):
self.environment = environment
self.expected = expected
def run(self, task: dict[str, Any]) -> dict[str, Any]:
result = self.environment.call(task["tool"], task["arguments"])
expected = self.expected[task["task_id"]]
return {
"task_id": task["task_id"],
"tool": task["tool"],
"arguments": task["arguments"],
"result": result,
"claims": list(expected["supported_claims"]),
}
@@ -0,0 +1,10 @@
row_id,org_unit_id,org_unit_name,parent_org_unit,period,tests,confirmed_cases,deaths,report_expected,report_submitted,stockout_days
R001,OU_ALPHA,Alpha Health Centre,Demo District,2025-01,200,30,0,1,1,0
R002,OU_BETA,Beta Health Centre,Demo District,2025-01,180,18,0,1,1,3
R003,OU_GAMMA,Gamma Health Centre,Demo District,2025-01,0,0,0,1,0,0
R004,OU_ALPHA,Alpha Health Centre,Demo District,2025-02,240,45,1,1,1,2
R005,OU_BETA,Beta Health Centre,Demo District,2025-02,120,130,0,1,1,-1
R006,OU_GAMMA,Gamma Health Centre,Demo District,2025-02,160,16,0,1,1,7
R007,OU_ALPHA,Alpha Health Centre,Demo District,2025-03,220,33,0,1,1,0
R008,OU_BETA,Beta Health Centre,Demo District,2025-03,0,2,0,1,1,0
R009,OU_GAMMA,Gamma Health Centre,Demo District,2025-03,140,14,2,1,0,4
1 row_id org_unit_id org_unit_name parent_org_unit period tests confirmed_cases deaths report_expected report_submitted stockout_days
2 R001 OU_ALPHA Alpha Health Centre Demo District 2025-01 200 30 0 1 1 0
3 R002 OU_BETA Beta Health Centre Demo District 2025-01 180 18 0 1 1 3
4 R003 OU_GAMMA Gamma Health Centre Demo District 2025-01 0 0 0 1 0 0
5 R004 OU_ALPHA Alpha Health Centre Demo District 2025-02 240 45 1 1 1 2
6 R005 OU_BETA Beta Health Centre Demo District 2025-02 120 130 0 1 1 -1
7 R006 OU_GAMMA Gamma Health Centre Demo District 2025-02 160 16 0 1 1 7
8 R007 OU_ALPHA Alpha Health Centre Demo District 2025-03 220 33 0 1 1 0
9 R008 OU_BETA Beta Health Centre Demo District 2025-03 0 2 0 1 1 0
10 R009 OU_GAMMA Gamma Health Centre Demo District 2025-03 140 14 2 1 0 4
@@ -0,0 +1,64 @@
"""Run the deterministic reference agent or evaluate external structured predictions."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
from agent import DeterministicReportingAgent
from evaluator import evaluate, expected_by_task, load_json
from reporting_tools import ReportingEnvironment
ROOT = Path(__file__).parent
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--predictions",
help="Evaluate an external agent's JSON predictions instead of the reference agent.",
)
parser.add_argument("--output", help="Optionally save predictions and scores as JSON.")
parser.add_argument(
"--tolerance",
type=float,
default=float(os.getenv("PUBLIC_HEALTH_EVAL_TOLERANCE", "0.01")),
help="Absolute tolerance for numeric answers (default: 0.01).",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
expected = expected_by_task(ROOT / "expected_answers.json")
if args.predictions:
predictions = load_json(args.predictions)
else:
tasks = load_json(ROOT / "tasks.json")
environment = ReportingEnvironment(ROOT / "data" / "synthetic_reports.csv")
agent = DeterministicReportingAgent(environment, expected)
predictions = [agent.run(task) for task in tasks]
report = evaluate(predictions, expected, tolerance=args.tolerance)
for task in report["tasks"]:
print(f"{task['task_id']:<30} {task['score']}/{task['max_score']}")
print("-" * 36)
print(f"TOTAL{'':<25} {report['score']}/{report['max_score']}")
if args.output:
payload = {"predictions": predictions, "evaluation": report}
Path(args.output).write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()
@@ -0,0 +1,4 @@
# Numeric answers are compared with this absolute tolerance.
PUBLIC_HEALTH_EVAL_TOLERANCE=0.01
# No API key is required. The reference agent and all tests run fully offline.
@@ -0,0 +1,106 @@
"""Objective structured scoring for public-health reporting agent traces."""
from __future__ import annotations
import json
import math
from pathlib import Path
from typing import Any
MAX_SCORE = 6
def load_json(path: str | Path) -> Any:
return json.loads(Path(path).read_text(encoding="utf-8"))
def expected_by_task(path: str | Path) -> dict[str, dict[str, Any]]:
return {item["task_id"]: item for item in load_json(path)}
def _equivalent(actual: Any, expected: Any, tolerance: float) -> bool:
if isinstance(actual, bool) or isinstance(expected, bool):
return actual == expected
if isinstance(actual, (int, float)) and isinstance(expected, (int, float)):
return math.isclose(float(actual), float(expected), abs_tol=tolerance)
if isinstance(actual, dict) and isinstance(expected, dict):
return actual.keys() == expected.keys() and all(
_equivalent(actual[key], expected[key], tolerance) for key in expected
)
if isinstance(actual, list) and isinstance(expected, list):
return len(actual) == len(expected) and all(
_equivalent(left, right, tolerance) for left, right in zip(actual, expected)
)
return actual == expected
def _same_evidence(actual: list[Any], expected: list[Any]) -> bool:
"""Compare evidence as set-like collections, including JSON objects."""
try:
return set(actual) == set(expected)
except TypeError:
return all(item in expected for item in actual) and all(
item in actual for item in expected
)
def score_prediction(
prediction: dict[str, Any], expected: dict[str, Any], tolerance: float = 0.01
) -> dict[str, Any]:
"""Score tool, arguments, answer, evidence and grounding (six points total)."""
details = {
"tool_selection": int(prediction.get("tool") == expected["tool"]),
"arguments": int(prediction.get("arguments") == expected["arguments"]),
}
actual_result = prediction.get("result", {})
if not isinstance(actual_result, dict):
actual_result = {}
expected_result = expected["result"]
actual_values = {key: value for key, value in actual_result.items() if key != "evidence"}
expected_values = {key: value for key, value in expected_result.items() if key != "evidence"}
details["answer"] = 2 if _equivalent(actual_values, expected_values, tolerance) else 0
actual_evidence = actual_result.get("evidence", [])
if not isinstance(actual_evidence, list):
actual_evidence = []
expected_evidence = expected_result.get("evidence", []) if isinstance(expected_result, dict) else []
if not isinstance(expected_evidence, list):
expected_evidence = []
details["evidence"] = int(_same_evidence(actual_evidence, expected_evidence))
claims = prediction.get("claims", [])
supported = expected.get("supported_claims", []) if isinstance(expected, dict) else []
if not isinstance(supported, list):
supported = []
if not isinstance(claims, list):
grounding = 0
else:
try:
grounding = int(set(claims).issubset(set(supported)))
except TypeError:
grounding = int(all(item in supported for item in claims))
details["grounding_and_safety"] = grounding
return {
"task_id": expected["task_id"],
"score": sum(details.values()),
"max_score": MAX_SCORE,
"details": details,
}
def evaluate(
predictions: list[dict[str, Any]],
expected_items: dict[str, dict[str, Any]],
tolerance: float = 0.01,
) -> dict[str, Any]:
prediction_map = {item["task_id"]: item for item in predictions}
results = []
for task_id, expected in expected_items.items():
prediction = prediction_map.get(task_id, {"task_id": task_id})
results.append(score_prediction(prediction, expected, tolerance))
return {
"score": sum(item["score"] for item in results),
"max_score": len(results) * MAX_SCORE,
"tasks": results,
}
@@ -0,0 +1,37 @@
[
{
"task_id": "positivity-alpha-jan",
"tool": "calculate_test_positivity",
"arguments": {"org_unit_id": "OU_ALPHA", "period": "2025-01"},
"result": {"tests": 200, "confirmed_cases": 30, "test_positivity_pct": 15.0, "evidence": ["R001"]},
"supported_claims": ["Alpha Health Centre test positivity was 15.0% in 2025-01."]
},
{
"task_id": "completeness-district-jan",
"tool": "calculate_reporting_completeness",
"arguments": {"parent_org_unit": "Demo District", "period": "2025-01"},
"result": {"expected_reports": 3, "submitted_reports": 2, "reporting_completeness_pct": 66.67, "evidence": ["R001", "R002", "R003"]},
"supported_claims": ["Demo District reporting completeness was 66.67% in 2025-01."]
},
{
"task_id": "trend-alpha-jan-feb",
"tool": "compare_confirmed_cases",
"arguments": {"org_unit_id": "OU_ALPHA", "start_period": "2025-01", "end_period": "2025-02"},
"result": {"start_cases": 30, "end_cases": 45, "absolute_change": 15, "percent_change": 50.0, "direction": "increase", "evidence": ["R001", "R004"]},
"supported_claims": ["Alpha Health Centre confirmed cases increased by 50.0% from 2025-01 to 2025-02."]
},
{
"task_id": "quality-demo-feb",
"tool": "find_data_quality_issues",
"arguments": {"parent_org_unit": "Demo District", "period": "2025-02"},
"result": {"issue_count": 2, "issues": [{"row_id": "R005", "code": "confirmed_exceeds_tests"}, {"row_id": "R005", "code": "negative_stockout_days"}], "evidence": ["R005"]},
"supported_claims": ["Row R005 has two deterministic aggregate data-quality issues."]
},
{
"task_id": "stockout-demo-feb",
"tool": "review_stockouts",
"arguments": {"parent_org_unit": "Demo District", "period": "2025-02"},
"result": {"facilities_with_stockouts": 2, "total_stockout_days": 9, "facilities": [{"org_unit_id": "OU_ALPHA", "stockout_days": 2}, {"org_unit_id": "OU_GAMMA", "stockout_days": 7}], "evidence": ["R004", "R006"]},
"supported_claims": ["Two Demo District facilities reported a total of 9 positive stock-out days in 2025-02."]
}
]
@@ -0,0 +1,148 @@
"""Deterministic tools over synthetic DHIS2-style aggregate reports."""
from __future__ import annotations
import csv
from pathlib import Path
from typing import Any
INTEGER_FIELDS = {
"tests",
"confirmed_cases",
"deaths",
"report_expected",
"report_submitted",
"stockout_days",
}
class ReportingEnvironment:
"""Small, auditable tool environment backed by a CSV file."""
def __init__(self, data_path: str | Path) -> None:
with Path(data_path).open(newline="", encoding="utf-8") as handle:
self.rows = []
for raw_row in csv.DictReader(handle):
row: dict[str, Any] = dict(raw_row)
for field in INTEGER_FIELDS:
raw = row[field]
text = str(raw).strip()
if not text:
row[field] = 0
continue
# Excel/CSV often writes whole counts as 10.0
num = float(text)
if not float(num).is_integer():
raise ValueError(
f"non-integer value for {field}: {raw!r}"
)
row[field] = int(num)
self.rows.append(row)
def _select(self, **filters: str) -> list[dict[str, Any]]:
rows = [
row
for row in self.rows
if all(row.get(field) == value for field, value in filters.items())
]
if not rows:
raise ValueError(f"No synthetic rows match {filters}")
return rows
def calculate_test_positivity(self, org_unit_id: str, period: str) -> dict[str, Any]:
rows = self._select(org_unit_id=org_unit_id, period=period)
tests = sum(row["tests"] for row in rows)
confirmed = sum(row["confirmed_cases"] for row in rows)
positivity = round(100 * confirmed / tests, 2) if tests else None
return {
"tests": tests,
"confirmed_cases": confirmed,
"test_positivity_pct": positivity,
"evidence": [row["row_id"] for row in rows],
}
def calculate_reporting_completeness(
self, parent_org_unit: str, period: str
) -> dict[str, Any]:
rows = self._select(parent_org_unit=parent_org_unit, period=period)
expected = sum(row["report_expected"] for row in rows)
submitted = sum(row["report_submitted"] for row in rows)
completeness = round(100 * submitted / expected, 2) if expected else None
return {
"expected_reports": expected,
"submitted_reports": submitted,
"reporting_completeness_pct": completeness,
"evidence": [row["row_id"] for row in rows],
}
def compare_confirmed_cases(
self, org_unit_id: str, start_period: str, end_period: str
) -> dict[str, Any]:
start_rows = self._select(org_unit_id=org_unit_id, period=start_period)
end_rows = self._select(org_unit_id=org_unit_id, period=end_period)
start_cases = sum(row["confirmed_cases"] for row in start_rows)
end_cases = sum(row["confirmed_cases"] for row in end_rows)
change = end_cases - start_cases
percent_change = round(100 * change / start_cases, 2) if start_cases else None
direction = "increase" if change > 0 else "decrease" if change < 0 else "no change"
return {
"start_cases": start_cases,
"end_cases": end_cases,
"absolute_change": change,
"percent_change": percent_change,
"direction": direction,
"evidence": [row["row_id"] for row in start_rows + end_rows],
}
def find_data_quality_issues(
self, parent_org_unit: str, period: str
) -> dict[str, Any]:
rows = self._select(parent_org_unit=parent_org_unit, period=period)
issues: list[dict[str, str]] = []
for row in rows:
if row["confirmed_cases"] > row["tests"]:
issues.append({"row_id": row["row_id"], "code": "confirmed_exceeds_tests"})
if row["stockout_days"] < 0:
issues.append({"row_id": row["row_id"], "code": "negative_stockout_days"})
if not row["report_submitted"] and any(
row[field] for field in ("tests", "confirmed_cases", "deaths")
):
issues.append({"row_id": row["row_id"], "code": "data_in_unsubmitted_report"})
return {
"issue_count": len(issues),
"issues": issues,
"evidence": sorted({issue["row_id"] for issue in issues}),
}
def review_stockouts(self, parent_org_unit: str, period: str) -> dict[str, Any]:
rows = self._select(parent_org_unit=parent_org_unit, period=period)
affected = [row for row in rows if row["stockout_days"] > 0]
return {
"facilities_with_stockouts": len(affected),
"total_stockout_days": sum(row["stockout_days"] for row in affected),
"facilities": [
{
"org_unit_id": row["org_unit_id"],
"stockout_days": row["stockout_days"],
}
for row in affected
],
"evidence": [row["row_id"] for row in affected],
}
def call(self, tool: str, arguments: dict[str, str] | None) -> dict[str, Any]:
if arguments is None:
arguments = {}
allowed_tools = {
"calculate_test_positivity": self.calculate_test_positivity,
"calculate_reporting_completeness": self.calculate_reporting_completeness,
"compare_confirmed_cases": self.compare_confirmed_cases,
"find_data_quality_issues": self.find_data_quality_issues,
"review_stockouts": self.review_stockouts,
}
try:
function = allowed_tools[tool]
except KeyError as exc:
raise ValueError(f"Unknown reporting tool: {tool}") from exc
return function(**arguments)
@@ -0,0 +1,2 @@
pytest>=8.0,<9.0
python-dotenv>=1.0.0
@@ -0,0 +1,32 @@
[
{
"task_id": "positivity-alpha-jan",
"prompt": "Calculate test positivity for Alpha Health Centre in January 2025.",
"tool": "calculate_test_positivity",
"arguments": {"org_unit_id": "OU_ALPHA", "period": "2025-01"}
},
{
"task_id": "completeness-district-jan",
"prompt": "Calculate reporting completeness for Demo District in January 2025.",
"tool": "calculate_reporting_completeness",
"arguments": {"parent_org_unit": "Demo District", "period": "2025-01"}
},
{
"task_id": "trend-alpha-jan-feb",
"prompt": "Compare Alpha Health Centre confirmed cases between January and February 2025.",
"tool": "compare_confirmed_cases",
"arguments": {"org_unit_id": "OU_ALPHA", "start_period": "2025-01", "end_period": "2025-02"}
},
{
"task_id": "quality-demo-feb",
"prompt": "Identify aggregate data-quality problems in Demo District for February 2025.",
"tool": "find_data_quality_issues",
"arguments": {"parent_org_unit": "Demo District", "period": "2025-02"}
},
{
"task_id": "stockout-demo-feb",
"prompt": "Review positive commodity stock-out days in Demo District for February 2025.",
"tool": "review_stockouts",
"arguments": {"parent_org_unit": "Demo District", "period": "2025-02"}
}
]
@@ -0,0 +1,9 @@
"""Test import bootstrap for the public-health-reporting-eval experiment."""
from pathlib import Path
import sys
EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
if str(EXPERIMENT_ROOT) not in sys.path:
sys.path.insert(0, str(EXPERIMENT_ROOT))
@@ -0,0 +1,76 @@
"""Blank integer CSV cells must load as 0, not ValueError from int('')."""
import csv
from pathlib import Path
from reporting_tools import ReportingEnvironment
def _write_csv(path: Path, tests_value: str) -> None:
with path.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(
handle,
fieldnames=[
"row_id",
"org_unit_id",
"period",
"parent_org_unit",
"tests",
"confirmed_cases",
"deaths",
"report_expected",
"report_submitted",
"stockout_days",
],
)
writer.writeheader()
writer.writerow(
{
"row_id": "r1",
"org_unit_id": "ou1",
"period": "2024Q1",
"parent_org_unit": "p1",
"tests": tests_value,
"confirmed_cases": "5",
"deaths": "0",
"report_expected": "1",
"report_submitted": "1",
"stockout_days": "0",
}
)
def test_blank_tests_field_loads_as_zero(tmp_path):
path = tmp_path / "blank.csv"
_write_csv(path, "")
env = ReportingEnvironment(path)
assert env.rows[0]["tests"] == 0
assert env.rows[0]["confirmed_cases"] == 5
def test_whitespace_tests_field_loads_as_zero(tmp_path):
path = tmp_path / "space.csv"
_write_csv(path, " ")
env = ReportingEnvironment(path)
assert env.rows[0]["tests"] == 0
def test_numeric_tests_unchanged(tmp_path):
path = tmp_path / "ok.csv"
_write_csv(path, "12")
env = ReportingEnvironment(path)
assert env.rows[0]["tests"] == 12
def test_reporting_environment_call_null_arguments(tmp_path):
"""JSON null tool arguments should behave like an empty object."""
import pytest
path = tmp_path / "ok.csv"
_write_csv(path, "12")
env = ReportingEnvironment(path)
with pytest.raises(TypeError) as null_exc:
env.call("calculate_test_positivity", None)
with pytest.raises(TypeError) as empty_exc:
env.call("calculate_test_positivity", {})
assert "mapping" not in str(null_exc.value)
assert str(null_exc.value) == str(empty_exc.value)
@@ -0,0 +1,71 @@
"""Excel-style whole floats in integer CSV cells must load (10.0 -> 10)."""
import csv
from pathlib import Path
from reporting_tools import ReportingEnvironment
def _write_csv(path: Path, tests_value: str) -> None:
with path.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(
handle,
fieldnames=[
"row_id",
"org_unit_id",
"period",
"parent_org_unit",
"tests",
"confirmed_cases",
"deaths",
"report_expected",
"report_submitted",
"stockout_days",
],
)
writer.writeheader()
writer.writerow(
{
"row_id": "r1",
"org_unit_id": "ou1",
"period": "2024Q1",
"parent_org_unit": "p1",
"tests": tests_value,
"confirmed_cases": "5",
"deaths": "0",
"report_expected": "1",
"report_submitted": "1",
"stockout_days": "0",
}
)
def test_excel_float_tests_field_loads_as_int(tmp_path):
path = tmp_path / "excel.csv"
_write_csv(path, "10.0")
env = ReportingEnvironment(path)
assert env.rows[0]["tests"] == 10
assert env.rows[0]["confirmed_cases"] == 5
def test_fractional_tests_still_rejected(tmp_path):
path = tmp_path / "frac.csv"
_write_csv(path, "3.5")
try:
ReportingEnvironment(path)
assert False, "expected ValueError"
except ValueError as e:
assert "non-integer" in str(e)
def test_blank_still_zero(tmp_path):
path = tmp_path / "blank.csv"
_write_csv(path, "")
env = ReportingEnvironment(path)
assert env.rows[0]["tests"] == 0
def test_plain_int_unchanged(tmp_path):
path = tmp_path / "ok.csv"
_write_csv(path, "12")
env = ReportingEnvironment(path)
assert env.rows[0]["tests"] == 12
@@ -0,0 +1,71 @@
"""score_prediction must tolerate result:null like missing/empty result."""
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
from agent import DeterministicReportingAgent
from evaluator import MAX_SCORE, expected_by_task, load_json, score_prediction
from reporting_tools import ReportingEnvironment
ROOT = Path(__file__).resolve().parents[1]
def _sample_expected():
expected = expected_by_task(ROOT / "expected_answers.json")
tasks = load_json(ROOT / "tasks.json")
environment = ReportingEnvironment(ROOT / "data" / "synthetic_reports.csv")
agent = DeterministicReportingAgent(environment, expected)
prediction = agent.run(tasks[0])
return prediction, expected[prediction["task_id"]]
def test_null_result_scores_without_attribute_error():
prediction, expected = _sample_expected()
prediction = deepcopy(prediction)
prediction["result"] = None
result = score_prediction(prediction, expected)
assert result["details"]["answer"] == 0
assert result["details"]["evidence"] == 0
assert result["score"] == (
result["details"]["tool_selection"]
+ result["details"]["arguments"]
+ result["details"]["grounding_and_safety"]
)
def test_missing_result_matches_null_result_score():
prediction, expected = _sample_expected()
null_pred = deepcopy(prediction)
null_pred["result"] = None
missing_pred = deepcopy(prediction)
del missing_pred["result"]
assert score_prediction(null_pred, expected) == score_prediction(missing_pred, expected)
def test_valid_result_still_full_score():
prediction, expected = _sample_expected()
result = score_prediction(prediction, expected)
assert result["score"] == MAX_SCORE
def test_unhashable_evidence_in_result():
prediction, expected = _sample_expected()
prediction = deepcopy(prediction)
prediction["result"]["evidence"] = [{"url": "http://example.com"}]
result = score_prediction(prediction, expected)
assert result["details"]["evidence"] == 0
def test_unhashable_evidence_remains_order_independent():
prediction, expected = _sample_expected()
prediction = deepcopy(prediction)
expected = deepcopy(expected)
expected["result"]["evidence"] = [{"url": "a"}, {"url": "b"}]
prediction["result"]["evidence"] = [{"url": "b"}, {"url": "a"}]
result = score_prediction(prediction, expected)
assert result["details"]["evidence"] == 1
@@ -0,0 +1,68 @@
"""Offline regression tests; no model, API key or network access required."""
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
from agent import DeterministicReportingAgent
from evaluator import MAX_SCORE, evaluate, expected_by_task, load_json, score_prediction
from reporting_tools import ReportingEnvironment
ROOT = Path(__file__).resolve().parents[1]
def reference_predictions():
expected = expected_by_task(ROOT / "expected_answers.json")
tasks = load_json(ROOT / "tasks.json")
environment = ReportingEnvironment(ROOT / "data" / "synthetic_reports.csv")
agent = DeterministicReportingAgent(environment, expected)
return [agent.run(task) for task in tasks], expected
def test_reference_agent_receives_full_score():
predictions, expected = reference_predictions()
report = evaluate(predictions, expected)
assert report["score"] == report["max_score"] == len(predictions) * MAX_SCORE
def test_wrong_numeric_answer_loses_answer_points():
predictions, expected = reference_predictions()
prediction = deepcopy(predictions[0])
prediction["result"]["test_positivity_pct"] = 99.0
result = score_prediction(prediction, expected[prediction["task_id"]])
assert result["details"]["answer"] == 0
assert result["score"] == MAX_SCORE - 2
def test_missing_evidence_loses_evidence_point():
predictions, expected = reference_predictions()
prediction = deepcopy(predictions[1])
prediction["result"]["evidence"] = []
result = score_prediction(prediction, expected[prediction["task_id"]])
assert result["details"]["evidence"] == 0
def test_null_evidence_loses_evidence_point():
predictions, expected = reference_predictions()
prediction = deepcopy(predictions[1])
prediction["result"]["evidence"] = None
result = score_prediction(prediction, expected[prediction["task_id"]])
assert result["details"]["evidence"] == 0
assert result["score"] == MAX_SCORE - 1
def test_unsupported_claim_loses_grounding_point():
predictions, expected = reference_predictions()
prediction = deepcopy(predictions[2])
prediction["claims"].append("This trend proves an outbreak will occur.")
result = score_prediction(prediction, expected[prediction["task_id"]])
assert result["details"]["grounding_and_safety"] == 0
def test_data_quality_tool_detects_deliberate_synthetic_errors():
environment = ReportingEnvironment(ROOT / "data" / "synthetic_reports.csv")
result = environment.find_data_quality_issues("Demo District", "2025-02")
assert result["issue_count"] == 2
assert result["evidence"] == ["R005"]