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
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:
@@ -0,0 +1,9 @@
|
||||
"""Test import bootstrap for the agent-cost-analysis 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,44 @@
|
||||
"""Tracer.chat must tolerate response.usage == None (OpenAI-compatible providers)."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import config
|
||||
from tracer import Tracer
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
def __init__(self, usage):
|
||||
self.chat = SimpleNamespace(
|
||||
completions=SimpleNamespace(create=self._create)
|
||||
)
|
||||
self._usage = usage
|
||||
|
||||
def _create(self, **_kwargs):
|
||||
return SimpleNamespace(usage=self._usage)
|
||||
|
||||
|
||||
def test_chat_tolerates_null_usage():
|
||||
tr = Tracer(_FakeClient(None), pricing=config.default_pricing())
|
||||
resp = tr.chat(step="turn-1", tool="query_order", model="m", messages=[])
|
||||
assert resp.usage is None
|
||||
assert len(tr.spans) == 1
|
||||
s = tr.spans[0]
|
||||
assert s.prompt_tokens == 0
|
||||
assert s.completion_tokens == 0
|
||||
assert s.cost_usd == 0.0
|
||||
assert s.latency_s >= 0.0
|
||||
|
||||
|
||||
def test_chat_keeps_real_usage():
|
||||
usage = SimpleNamespace(
|
||||
prompt_tokens=100,
|
||||
completion_tokens=20,
|
||||
prompt_tokens_details=SimpleNamespace(cached_tokens=10),
|
||||
)
|
||||
tr = Tracer(_FakeClient(usage), pricing=config.default_pricing())
|
||||
tr.chat(step="turn-1", tool="query_order", model="m", messages=[])
|
||||
s = tr.spans[0]
|
||||
assert s.prompt_tokens == 100
|
||||
assert s.completion_tokens == 20
|
||||
assert s.cached_tokens == 10
|
||||
assert s.cost_usd > 0
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Regression tests for offline trace parsing (实验 7-9 成本分析).
|
||||
|
||||
Covers two crash classes found in --offline mode:
|
||||
- Tracer.from_records: trace JSON with explicit null token fields -> int(None) TypeError
|
||||
- demo.collect_offline: scenario dict missing the optional "spans" key -> KeyError
|
||||
"""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
import config
|
||||
import demo
|
||||
from tracer import Tracer
|
||||
|
||||
|
||||
def _span(**overrides):
|
||||
span = {
|
||||
"step": "turn-1",
|
||||
"tool": "query_order",
|
||||
"kind": "llm",
|
||||
"prompt_tokens": 100,
|
||||
"cached_tokens": 10,
|
||||
"completion_tokens": 12,
|
||||
"tool_ctx_tokens": 50,
|
||||
"latency_s": 1.2,
|
||||
}
|
||||
span.update(overrides)
|
||||
return span
|
||||
|
||||
|
||||
def test_from_records_tolerates_null_fields():
|
||||
"""Explicit JSON nulls in numeric fields are coerced, not int(None) TypeError."""
|
||||
records = [_span(prompt_tokens=None, cached_tokens=None,
|
||||
completion_tokens=None, tool_ctx_tokens=None, latency_s=None)]
|
||||
tr = Tracer.from_records(records, pricing=config.default_pricing())
|
||||
s = tr.spans[0]
|
||||
assert s.prompt_tokens == 0
|
||||
assert s.cached_tokens == 0
|
||||
assert s.completion_tokens == 0
|
||||
assert s.tool_ctx_tokens == -1 # null tool_ctx 视为「未知」
|
||||
assert s.latency_s == 0.0
|
||||
|
||||
|
||||
def test_from_records_tolerates_missing_fields():
|
||||
"""Minimal span dicts (only step/tool) still parse."""
|
||||
tr = Tracer.from_records([{"step": "turn-1", "tool": "query_order"}],
|
||||
pricing=config.default_pricing())
|
||||
assert tr.spans[0].prompt_tokens == 0
|
||||
assert tr.spans[0].tool_ctx_tokens == -1
|
||||
|
||||
|
||||
def test_from_records_keeps_real_values():
|
||||
"""Normal values pass through unchanged (no coercion side effects)."""
|
||||
tr = Tracer.from_records([_span()], pricing=config.default_pricing())
|
||||
s = tr.spans[0]
|
||||
assert (s.prompt_tokens, s.cached_tokens, s.completion_tokens) == (100, 10, 12)
|
||||
assert s.tool_ctx_tokens == 50
|
||||
assert s.latency_s == 1.2
|
||||
|
||||
|
||||
def test_from_records_keeps_zero_tool_ctx_tokens():
|
||||
"""Explicit 0 means known-zero tool context, not unknown (-1)."""
|
||||
tr = Tracer.from_records([_span(tool_ctx_tokens=0)],
|
||||
pricing=config.default_pricing())
|
||||
assert tr.spans[0].tool_ctx_tokens == 0
|
||||
assert tr.total_tool_ctx_tokens() == 0
|
||||
|
||||
|
||||
def _write_trace(tmp_path, scenarios):
|
||||
path = tmp_path / "trace.json"
|
||||
path.write_text(json.dumps({"model": "gpt-5.6-luna", "scenarios": scenarios}),
|
||||
encoding="utf-8")
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_collect_offline_skips_scenario_without_spans(tmp_path, capsys):
|
||||
"""A scenario missing 'spans' is skipped with a warning, not a KeyError crash."""
|
||||
trace = _write_trace(tmp_path, [
|
||||
{"key": "naive", "name": "A naive"}, # no spans -> skip
|
||||
{"key": "both", "name": "B both", "spans": [_span()]}, # valid
|
||||
])
|
||||
tracers = demo.collect_offline(["naive", "both"], config.default_pricing(), trace)
|
||||
assert [k for k, _ in tracers] == ["both"]
|
||||
assert "缺少 spans" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_collect_offline_exits_when_no_usable_scenario(tmp_path):
|
||||
"""When every selected scenario lacks spans, exit cleanly like the empty-trace path."""
|
||||
trace = _write_trace(tmp_path, [{"key": "naive", "name": "A naive"}])
|
||||
with pytest.raises(SystemExit):
|
||||
demo.collect_offline(["naive"], config.default_pricing(), trace)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user