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
174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from agent import conversation_turn, direct_plan, react_plan
|
|
|
|
|
|
class FakeUsage:
|
|
def model_dump(self, **_kwargs):
|
|
return {"prompt_tokens": 40, "completion_tokens": 20, "total_tokens": 60}
|
|
|
|
|
|
class FakeResponse:
|
|
def __init__(self, content):
|
|
self.id = "provider-response-123"
|
|
self.model = "planner-test"
|
|
self.created = 123456
|
|
self.usage = FakeUsage()
|
|
self.choices = [
|
|
SimpleNamespace(
|
|
message=SimpleNamespace(content=content),
|
|
finish_reason="stop",
|
|
)
|
|
]
|
|
|
|
def model_dump(self, **_kwargs):
|
|
return {
|
|
"id": self.id,
|
|
"model": self.model,
|
|
"created": self.created,
|
|
"choices": [
|
|
{
|
|
"finish_reason": "stop",
|
|
"message": {"role": "assistant", "content": self.choices[0].message.content},
|
|
}
|
|
],
|
|
"usage": self.usage.model_dump(),
|
|
}
|
|
|
|
|
|
class FakeCompletions:
|
|
def __init__(self, values):
|
|
self.values = iter(values)
|
|
|
|
def create(self, **kwargs):
|
|
assert kwargs["response_format"] == {"type": "json_object"}
|
|
assert kwargs["temperature"] == 0
|
|
return FakeResponse(json.dumps(next(self.values)))
|
|
|
|
|
|
class FakeClient:
|
|
def __init__(self, values):
|
|
self.chat = SimpleNamespace(completions=FakeCompletions(values))
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def provider_environment(monkeypatch):
|
|
monkeypatch.setenv("PHONE_MODEL_PROVIDER", "ark")
|
|
monkeypatch.setenv("ARK_API_KEY", "test-key-not-retained")
|
|
|
|
|
|
def test_direct_plan_requires_fixed_parameters_and_has_no_planner_receipt():
|
|
with pytest.raises(ValueError, match="context"):
|
|
direct_plan(callee_name="Jane", goal="Confirm", context="", instructions="Ask")
|
|
plan = direct_plan(callee_name="Jane", goal="Confirm", context="Tuesday", instructions="Ask")
|
|
assert plan.planner_receipt is None
|
|
assert "confirmation code" in plan.opening_line
|
|
|
|
|
|
def test_react_plan_retains_real_raw_receipt_and_trace():
|
|
client = FakeClient(
|
|
[
|
|
{
|
|
"callee_name": "Jane",
|
|
"goal": "Confirm a dental checkup time",
|
|
"context": "The time and code are absent.",
|
|
"instructions": "Ask for time and code, repeat both, then complete_task.",
|
|
"opening_line": "What exact time and confirmation code do you confirm?",
|
|
"missing_information": ["appointment time", "confirmation code"],
|
|
"decision_summary": "Collect both omitted fields by voice.",
|
|
}
|
|
]
|
|
)
|
|
plan = react_plan(
|
|
"Call Jane, but I forgot the time and code",
|
|
client=client,
|
|
model="planner-test",
|
|
provider_name="injected-test",
|
|
)
|
|
assert plan.missing_information == ["appointment time", "confirmation code"]
|
|
assert [item["stage"] for item in plan.trace] == ["observation", "reason", "action"]
|
|
receipt = plan.planner_receipt
|
|
assert receipt["provider_response_id"] == "provider-response-123"
|
|
assert receipt["usage"]["total_tokens"] == 60
|
|
assert receipt["raw_response"]["choices"][0]["message"]["content"]
|
|
assert receipt["fallback_used"] is False
|
|
assert "test-key-not-retained" not in json.dumps(receipt)
|
|
|
|
|
|
def test_conversation_requires_explicit_confirmation_for_completion():
|
|
plan = direct_plan(
|
|
callee_name="Jane",
|
|
goal="Confirm a time",
|
|
context="Tuesday afternoon",
|
|
instructions="Ask and confirm",
|
|
)
|
|
client = FakeClient(
|
|
[
|
|
{
|
|
"assistant_message": "Thanks. I recorded Tuesday at 3 PM and Maple 7.",
|
|
"explicit_confirmation_observed": True,
|
|
"should_complete": True,
|
|
"completion": {
|
|
"result": "Local confirmation recorded.",
|
|
"appointment_time": "Tuesday at 3 PM",
|
|
"confirmation_number": "MAPLE-7",
|
|
"notes": "No external organization was contacted or booking made.",
|
|
},
|
|
}
|
|
]
|
|
)
|
|
result = conversation_turn(
|
|
plan,
|
|
[],
|
|
"I explicitly confirm Tuesday at 3 PM and Maple seven.",
|
|
client=client,
|
|
model="planner-test",
|
|
provider_name="injected-test",
|
|
)
|
|
assert result["should_complete"] is True
|
|
assert result["completion"]["confirmation_number"] == "MAPLE-7"
|
|
assert result["llm_receipt"]["purpose"] == "post_asr_dialogue"
|
|
|
|
|
|
def test_model_errors_propagate_without_fallback():
|
|
class BrokenCompletions:
|
|
def create(self, **_kwargs):
|
|
raise RuntimeError("provider unavailable")
|
|
|
|
client = SimpleNamespace(chat=SimpleNamespace(completions=BrokenCompletions()))
|
|
with pytest.raises(RuntimeError, match="provider unavailable"):
|
|
react_plan("Call Jane and ask for the missing time", client=client, model="planner-test")
|
|
def test_conversation_turn_rejects_none_critical_completion_fields():
|
|
plan = direct_plan(
|
|
callee_name="Jane",
|
|
goal="Confirm a time",
|
|
context="Tuesday afternoon",
|
|
instructions="Ask and confirm",
|
|
)
|
|
client = FakeClient(
|
|
[
|
|
{
|
|
"assistant_message": "Thanks.",
|
|
"explicit_confirmation_observed": True,
|
|
"should_complete": True,
|
|
"completion": {
|
|
"result": "Local confirmation recorded.",
|
|
"appointment_time": None,
|
|
"confirmation_number": "MAPLE-7",
|
|
"notes": "No external organization was contacted or booking made.",
|
|
},
|
|
}
|
|
]
|
|
)
|
|
with pytest.raises(ValueError, match="without both critical fields"):
|
|
conversation_turn(
|
|
plan,
|
|
[],
|
|
"I explicitly confirm Maple seven.",
|
|
client=client,
|
|
model="planner-test",
|
|
provider_name="injected-test",
|
|
)
|