Repository navigation
Expand file tree
/
Copy pathtest_model_errors.py
More file actions
143 lines (103 loc) · 5.01 KB
/
Copy pathtest_model_errors.py
File metadata and controls
143 lines (103 loc) · 5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""One refused call, one clean sentence.
Acceptance 3's last sentences. A 401 or 403 must not be retried by waku's
own non-streaming fallback: each call site reaches the provider once per
turn. A 429 is left to the SDK's own retry policy.
And the sentence reaches a reader by BOTH routes it can take: the
gateway's "done" event, and a graph node's entry in a run's `errors` map.
The second one was specified and not built — a node error was `repr(exc)`,
so a tenant who hit the free-tier cap inside a workflow read
`Refused('Free tier used up. Add your own key in Models.')` in the Graph
card instead of the sentence written for them.
"""
from __future__ import annotations
import pytest
from evals.helpers import response, text_block
class Refused(Exception):
"""Shaped like the SDK's APIStatusError: a status and a message."""
def __init__(self, status_code: int, message: str):
super().__init__(message)
self.status_code = status_code
self.message = message
def test_a_401_is_not_retried_without_streaming():
calls = []
class Client:
class messages:
@staticmethod
def stream(**kwargs):
calls.append("stream")
raise Refused(401, "Free tier used up. Add your own key in Models.")
@staticmethod
def create(**kwargs):
calls.append("create")
raise AssertionError("the fallback must not run on a 4xx")
from prometheus.loop import agent
from prometheus.tools.registry import ToolRegistry
with pytest.raises(Refused):
agent.run_loop(Client(), model="m", system="s", messages=[],
tools=ToolRegistry(), stream=True)
assert calls == ["stream"], f"expected one call, got {calls}"
def test_a_500_still_falls_back():
"""The fallback exists for transport faults. Keep it for those."""
calls = []
class Client:
class messages:
@staticmethod
def stream(**kwargs):
calls.append("stream")
raise Refused(500, "internal")
@staticmethod
def create(**kwargs):
calls.append("create")
return response([text_block("ok")])
from prometheus.loop import agent
from prometheus.tools.registry import ToolRegistry
result = agent.run_loop(Client(), model="m", system="s", messages=[],
tools=ToolRegistry(), stream=True)
assert calls == ["stream", "create"], f"expected fallback, got {calls}"
assert result.reply == "ok"
def test_the_done_event_carries_only_the_message():
from prometheus.loop.agent import error_text
assert error_text(Refused(403, "Free tier used up. Add your own key in Models.")) \
== "Free tier used up. Add your own key in Models."
def test_a_non_provider_error_keeps_todays_text():
from prometheus.loop.agent import error_text
assert error_text(ValueError("bad input")) == "ValueError: bad input"
def test_a_status_code_without_a_message_keeps_todays_text():
"""The guard needs BOTH a message and a status_code. The test above
exercises the neither-half; this is the status_code-alone half, which an
implementation that checked only `status_code` would pass while turning
every bare HTTP error into an empty string on the screen."""
from prometheus.loop.agent import error_text
class NoMessage(Exception):
def __init__(self):
super().__init__("upstream said no")
self.status_code = 503
self.message = ""
assert error_text(NoMessage()) == "NoMessage: upstream said no"
def test_a_refused_call_inside_a_graph_node_reads_as_the_message():
"""Acceptance 3's other half. A node error is recorded, never raised, so
this string IS what the Graph card shows whoever ran the workflow."""
from prometheus.graph import END, START, Graph, Node, run_graph
def refuse(state: dict) -> dict:
raise Refused(403, "Free tier used up. Add your own key in Models.")
graph = Graph("gated")
graph.add_node(Node(name="gate", fn=refuse, kind="tool"))
graph.add_edge(START, "gate")
graph.add_edge("gate", END)
state = run_graph(graph, {})
assert state["errors"]["gate"] == "Free tier used up. Add your own key in Models.", (
f"the graph node's error is {state['errors']['gate']!r} — a tenant reads "
f"this in the Graph card, so it has to be the provider's sentence, not "
f"a Python repr"
)
def test_an_ordinary_graph_node_bug_still_reads_as_a_bug():
"""The other direction: a plain exception inside a node is a bug, not a
decision, and keeps the type name that says so."""
from prometheus.graph import END, START, Graph, Node, run_graph
def explode(state: dict) -> dict:
raise ValueError("bad input")
graph = Graph("buggy")
graph.add_node(Node(name="step", fn=explode, kind="tool"))
graph.add_edge(START, "step")
graph.add_edge("step", END)
assert run_graph(graph, {})["errors"]["step"] == "ValueError: bad input"