connectonion 0.4.11__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- connectonion/__init__.py +11 -5
- connectonion/agent.py +44 -42
- connectonion/cli/commands/init.py +1 -1
- connectonion/cli/commands/project_cmd_lib.py +4 -4
- connectonion/cli/commands/reset_commands.py +1 -1
- connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +15 -11
- connectonion/cli/templates/minimal/agent.py +2 -2
- connectonion/console.py +55 -3
- connectonion/events.py +96 -17
- connectonion/llm.py +21 -3
- connectonion/logger.py +289 -0
- connectonion/prompt_files/eval_expected.md +12 -0
- connectonion/tool_executor.py +43 -32
- connectonion/usage.py +4 -0
- connectonion/useful_events_handlers/reflect.py +14 -10
- connectonion/useful_plugins/__init__.py +2 -1
- connectonion/useful_plugins/calendar_plugin.py +2 -2
- connectonion/useful_plugins/eval.py +130 -0
- connectonion/useful_plugins/gmail_plugin.py +4 -4
- connectonion/useful_plugins/image_result_formatter.py +4 -3
- connectonion/useful_plugins/re_act.py +15 -57
- connectonion/useful_plugins/shell_approval.py +2 -2
- connectonion/useful_tools/gmail.py +2 -2
- connectonion/useful_tools/memory.py +4 -0
- {connectonion-0.4.11.dist-info → connectonion-0.5.0.dist-info}/METADATA +48 -48
- {connectonion-0.4.11.dist-info → connectonion-0.5.0.dist-info}/RECORD +33 -77
- {connectonion-0.4.11.dist-info → connectonion-0.5.0.dist-info}/WHEEL +1 -2
- connectonion/cli/templates/email-agent/.env.example +0 -23
- connectonion/cli/templates/email-agent/README.md +0 -240
- connectonion/cli/templates/email-agent/agent.py +0 -374
- connectonion/cli/templates/email-agent/demo.py +0 -71
- connectonion/cli/templates/meta-agent/.env.example +0 -11
- connectonion/cli/templates/minimal/.env.example +0 -5
- connectonion/cli/templates/playwright/.env.example +0 -5
- connectonion-0.4.11.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/cli/__init__.py +0 -1
- tests/cli/argparse_runner.py +0 -85
- tests/cli/conftest.py +0 -5
- tests/cli/test_browser_cli.py +0 -61
- tests/cli/test_cli.py +0 -143
- tests/cli/test_cli_auth_google.py +0 -344
- tests/cli/test_cli_auth_microsoft.py +0 -256
- tests/cli/test_cli_create.py +0 -283
- tests/cli/test_cli_help.py +0 -200
- tests/cli/test_cli_init.py +0 -318
- tests/conftest.py +0 -283
- tests/debug_gemini_models.py +0 -23
- tests/fixtures/__init__.py +0 -1
- tests/fixtures/test_tools.py +0 -112
- tests/fixtures/trust_fixtures.py +0 -257
- tests/real_api/__init__.py +0 -0
- tests/real_api/conftest.py +0 -9
- tests/real_api/test_llm_do.py +0 -174
- tests/real_api/test_llm_do_comprehensive.py +0 -527
- tests/real_api/test_production_client.py +0 -94
- tests/real_api/test_real_anthropic.py +0 -100
- tests/real_api/test_real_api.py +0 -113
- tests/real_api/test_real_auth.py +0 -130
- tests/real_api/test_real_email.py +0 -95
- tests/real_api/test_real_gemini.py +0 -96
- tests/real_api/test_real_llm_do.py +0 -81
- tests/real_api/test_real_managed.py +0 -208
- tests/real_api/test_real_multi_llm.py +0 -454
- tests/real_api/test_real_openai.py +0 -100
- tests/real_api/test_responses_parse.py +0 -88
- tests/test_diff_writer.py +0 -126
- tests/test_events.py +0 -677
- tests/test_gemini_co.py +0 -70
- tests/test_image_result_formatter.py +0 -88
- tests/test_plugin_system.py +0 -110
- tests/utils/__init__.py +0 -1
- tests/utils/config_helpers.py +0 -188
- tests/utils/mock_helpers.py +0 -237
- /connectonion/{prompts → prompt_files}/__init__.py +0 -0
- /connectonion/{prompts → prompt_files}/analyze_contact.md +0 -0
- /connectonion/{prompts → prompt_files}/react_evaluate.md +0 -0
- /connectonion/{prompts → prompt_files}/react_plan.md +0 -0
- /connectonion/{prompts → prompt_files}/reflect.md +0 -0
- {connectonion-0.4.11.dist-info → connectonion-0.5.0.dist-info}/entry_points.txt +0 -0
tests/test_events.py
DELETED
|
@@ -1,677 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for the event system (on_events parameter)
|
|
3
|
-
"""
|
|
4
|
-
import pytest
|
|
5
|
-
from unittest.mock import Mock
|
|
6
|
-
from connectonion import Agent, after_user_input, before_llm, after_llm, before_tool, after_tool, on_error, on_complete
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def search(query: str) -> str:
|
|
10
|
-
"""Mock search tool"""
|
|
11
|
-
return f"Results for {query}"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def failing_tool(query: str) -> str:
|
|
15
|
-
"""Tool that always fails"""
|
|
16
|
-
raise ValueError("Intentional failure")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TestEventSystem:
|
|
20
|
-
"""Test event system functionality"""
|
|
21
|
-
|
|
22
|
-
def test_after_user_input_fires_once(self):
|
|
23
|
-
"""Test after_user_input fires once per turn"""
|
|
24
|
-
calls = []
|
|
25
|
-
|
|
26
|
-
def track_user_input(agent):
|
|
27
|
-
calls.append('after_user_input')
|
|
28
|
-
# Verify we can access user prompt
|
|
29
|
-
assert agent.current_session['user_prompt'] == "test prompt"
|
|
30
|
-
|
|
31
|
-
agent = Agent(
|
|
32
|
-
"test",
|
|
33
|
-
model="gpt-4o-mini",
|
|
34
|
-
on_events=[after_user_input(track_user_input)]
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
agent.input("test prompt")
|
|
38
|
-
|
|
39
|
-
# Should fire exactly once per turn
|
|
40
|
-
assert calls == ['after_user_input']
|
|
41
|
-
|
|
42
|
-
def test_before_llm_fires_multiple_times(self):
|
|
43
|
-
"""Test before_llm fires before each LLM call"""
|
|
44
|
-
calls = []
|
|
45
|
-
|
|
46
|
-
def track_before_llm(agent):
|
|
47
|
-
calls.append('before_llm')
|
|
48
|
-
|
|
49
|
-
agent = Agent(
|
|
50
|
-
"test",
|
|
51
|
-
tools=[search],
|
|
52
|
-
model="gpt-4o-mini",
|
|
53
|
-
on_events=[before_llm(track_before_llm)]
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
agent.input("Search for Python")
|
|
57
|
-
|
|
58
|
-
# Should fire multiple times (once per LLM call in iteration loop)
|
|
59
|
-
assert len(calls) >= 1
|
|
60
|
-
assert all(c == 'before_llm' for c in calls)
|
|
61
|
-
|
|
62
|
-
def test_after_llm_fires_multiple_times(self):
|
|
63
|
-
"""Test after_llm fires after each LLM response"""
|
|
64
|
-
calls = []
|
|
65
|
-
|
|
66
|
-
def track_after_llm(agent):
|
|
67
|
-
calls.append('after_llm')
|
|
68
|
-
# Verify we can access trace
|
|
69
|
-
trace = agent.current_session['trace']
|
|
70
|
-
llm_calls = [t for t in trace if t['type'] == 'llm_call']
|
|
71
|
-
assert len(llm_calls) > 0
|
|
72
|
-
|
|
73
|
-
agent = Agent(
|
|
74
|
-
"test",
|
|
75
|
-
tools=[search],
|
|
76
|
-
model="gpt-4o-mini",
|
|
77
|
-
on_events=[after_llm(track_after_llm)]
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
agent.input("Search for Python")
|
|
81
|
-
|
|
82
|
-
# Should fire multiple times
|
|
83
|
-
assert len(calls) >= 1
|
|
84
|
-
assert all(c == 'after_llm' for c in calls)
|
|
85
|
-
|
|
86
|
-
def test_before_tool_fires_before_execution(self):
|
|
87
|
-
"""Test before_tool fires before each tool execution"""
|
|
88
|
-
calls = []
|
|
89
|
-
|
|
90
|
-
def track_before_tool(agent):
|
|
91
|
-
calls.append('before_tool')
|
|
92
|
-
# Verify trace doesn't have result yet (tool hasn't run)
|
|
93
|
-
trace = agent.current_session['trace']
|
|
94
|
-
tool_executions = [t for t in trace if t['type'] == 'tool_execution']
|
|
95
|
-
# Before tool runs, latest tool execution should be pending or not exist yet
|
|
96
|
-
if tool_executions:
|
|
97
|
-
# The trace entry exists but hasn't been updated with result yet
|
|
98
|
-
pass
|
|
99
|
-
|
|
100
|
-
agent = Agent(
|
|
101
|
-
"test",
|
|
102
|
-
tools=[search],
|
|
103
|
-
model="gpt-4o-mini",
|
|
104
|
-
on_events=[before_tool(track_before_tool)]
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
agent.input("Search for Python")
|
|
108
|
-
|
|
109
|
-
# Should fire at least once (when search tool is called)
|
|
110
|
-
assert len(calls) >= 1
|
|
111
|
-
|
|
112
|
-
def test_after_tool_fires_after_success(self):
|
|
113
|
-
"""Test after_tool fires after successful tool execution"""
|
|
114
|
-
calls = []
|
|
115
|
-
|
|
116
|
-
def track_after_tool(agent):
|
|
117
|
-
calls.append('after_tool')
|
|
118
|
-
# Verify we can access tool result
|
|
119
|
-
trace = agent.current_session['trace']
|
|
120
|
-
tool_executions = [t for t in trace if t['type'] == 'tool_execution']
|
|
121
|
-
assert len(tool_executions) > 0
|
|
122
|
-
latest_tool = tool_executions[-1]
|
|
123
|
-
assert latest_tool['status'] == 'success'
|
|
124
|
-
assert 'Results for' in latest_tool['result']
|
|
125
|
-
|
|
126
|
-
agent = Agent(
|
|
127
|
-
"test",
|
|
128
|
-
tools=[search],
|
|
129
|
-
model="gpt-4o-mini",
|
|
130
|
-
on_events=[after_tool(track_after_tool)]
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
agent.input("Search for Python")
|
|
134
|
-
|
|
135
|
-
# Should fire at least once
|
|
136
|
-
assert len(calls) >= 1
|
|
137
|
-
|
|
138
|
-
def test_on_error_fires_on_tool_failure(self):
|
|
139
|
-
"""Test on_error fires when tool execution fails"""
|
|
140
|
-
calls = []
|
|
141
|
-
|
|
142
|
-
def track_error(agent):
|
|
143
|
-
calls.append('on_error')
|
|
144
|
-
# Verify we can access error information
|
|
145
|
-
trace = agent.current_session['trace']
|
|
146
|
-
tool_executions = [t for t in trace if t['type'] == 'tool_execution']
|
|
147
|
-
assert len(tool_executions) > 0
|
|
148
|
-
latest_tool = tool_executions[-1]
|
|
149
|
-
assert latest_tool['status'] == 'error'
|
|
150
|
-
assert 'error' in latest_tool
|
|
151
|
-
assert latest_tool['error'] == 'Intentional failure'
|
|
152
|
-
|
|
153
|
-
agent = Agent(
|
|
154
|
-
"test",
|
|
155
|
-
tools=[failing_tool],
|
|
156
|
-
model="gpt-4o-mini",
|
|
157
|
-
on_events=[on_error(track_error)]
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
# This should trigger the failing tool
|
|
161
|
-
result = agent.input("Use failing_tool with query 'test'")
|
|
162
|
-
|
|
163
|
-
# Should fire at least once
|
|
164
|
-
assert len(calls) >= 1
|
|
165
|
-
|
|
166
|
-
def test_multiple_events_same_type_fire_in_order(self):
|
|
167
|
-
"""Test multiple events of same type fire in order"""
|
|
168
|
-
calls = []
|
|
169
|
-
|
|
170
|
-
def handler1(agent):
|
|
171
|
-
calls.append('handler1')
|
|
172
|
-
|
|
173
|
-
def handler2(agent):
|
|
174
|
-
calls.append('handler2')
|
|
175
|
-
|
|
176
|
-
def handler3(agent):
|
|
177
|
-
calls.append('handler3')
|
|
178
|
-
|
|
179
|
-
agent = Agent(
|
|
180
|
-
"test",
|
|
181
|
-
model="gpt-4o-mini",
|
|
182
|
-
on_events=[
|
|
183
|
-
after_user_input(handler1),
|
|
184
|
-
after_user_input(handler2),
|
|
185
|
-
after_user_input(handler3)
|
|
186
|
-
]
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
agent.input("test")
|
|
190
|
-
|
|
191
|
-
# Should fire in order
|
|
192
|
-
assert calls == ['handler1', 'handler2', 'handler3']
|
|
193
|
-
|
|
194
|
-
def test_events_can_modify_messages(self):
|
|
195
|
-
"""Test events can modify agent messages"""
|
|
196
|
-
|
|
197
|
-
def add_system_message(agent):
|
|
198
|
-
agent.current_session['messages'].append({
|
|
199
|
-
'role': 'system',
|
|
200
|
-
'content': 'Added by event'
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
agent = Agent(
|
|
204
|
-
"test",
|
|
205
|
-
model="gpt-4o-mini",
|
|
206
|
-
on_events=[after_user_input(add_system_message)]
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
agent.input("test")
|
|
210
|
-
|
|
211
|
-
# Verify message was added
|
|
212
|
-
messages = agent.current_session['messages']
|
|
213
|
-
system_messages = [m for m in messages if m['role'] == 'system' and m['content'] == 'Added by event']
|
|
214
|
-
assert len(system_messages) == 1
|
|
215
|
-
|
|
216
|
-
def test_event_exception_propagates(self):
|
|
217
|
-
"""Test that event exceptions propagate (fail fast)"""
|
|
218
|
-
|
|
219
|
-
def failing_event(agent):
|
|
220
|
-
raise RuntimeError("Event failed")
|
|
221
|
-
|
|
222
|
-
agent = Agent(
|
|
223
|
-
"test",
|
|
224
|
-
model="gpt-4o-mini",
|
|
225
|
-
on_events=[after_user_input(failing_event)]
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
# Event exception should propagate
|
|
229
|
-
with pytest.raises(RuntimeError, match="Event failed"):
|
|
230
|
-
agent.input("test")
|
|
231
|
-
|
|
232
|
-
def test_mixed_event_types(self):
|
|
233
|
-
"""Test using multiple different event types together"""
|
|
234
|
-
calls = []
|
|
235
|
-
|
|
236
|
-
def track_user(agent):
|
|
237
|
-
calls.append('after_user_input')
|
|
238
|
-
|
|
239
|
-
def track_llm(agent):
|
|
240
|
-
calls.append('after_llm')
|
|
241
|
-
|
|
242
|
-
def track_tool(agent):
|
|
243
|
-
calls.append('after_tool')
|
|
244
|
-
|
|
245
|
-
agent = Agent(
|
|
246
|
-
"test",
|
|
247
|
-
tools=[search],
|
|
248
|
-
model="gpt-4o-mini",
|
|
249
|
-
on_events=[
|
|
250
|
-
after_user_input(track_user),
|
|
251
|
-
after_llm(track_llm),
|
|
252
|
-
after_tool(track_tool)
|
|
253
|
-
]
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
agent.input("Search for Python")
|
|
257
|
-
|
|
258
|
-
# All event types should fire
|
|
259
|
-
assert 'after_user_input' in calls
|
|
260
|
-
assert 'after_llm' in calls
|
|
261
|
-
assert 'after_tool' in calls
|
|
262
|
-
|
|
263
|
-
# after_user_input should fire exactly once
|
|
264
|
-
assert calls.count('after_user_input') == 1
|
|
265
|
-
|
|
266
|
-
def test_no_events_works_normally(self):
|
|
267
|
-
"""Test agent works normally without any events"""
|
|
268
|
-
agent = Agent(
|
|
269
|
-
"test",
|
|
270
|
-
tools=[search],
|
|
271
|
-
model="gpt-4o-mini"
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
result = agent.input("Search for Python")
|
|
275
|
-
|
|
276
|
-
# Should complete normally
|
|
277
|
-
assert result is not None
|
|
278
|
-
|
|
279
|
-
def test_event_receives_agent_instance(self):
|
|
280
|
-
"""Test that events receive the agent instance with all attributes"""
|
|
281
|
-
|
|
282
|
-
def verify_agent_access(agent):
|
|
283
|
-
# Should have access to all agent attributes
|
|
284
|
-
assert hasattr(agent, 'name')
|
|
285
|
-
assert hasattr(agent, 'current_session')
|
|
286
|
-
assert hasattr(agent, 'tools')
|
|
287
|
-
assert hasattr(agent, 'llm')
|
|
288
|
-
assert hasattr(agent, 'console')
|
|
289
|
-
assert agent.name == "test"
|
|
290
|
-
|
|
291
|
-
agent = Agent(
|
|
292
|
-
"test",
|
|
293
|
-
model="gpt-4o-mini",
|
|
294
|
-
on_events=[after_user_input(verify_agent_access)]
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
agent.input("test")
|
|
298
|
-
|
|
299
|
-
def test_event_wrapper_adds_attribute(self):
|
|
300
|
-
"""Test that event wrappers add _event_type attribute"""
|
|
301
|
-
|
|
302
|
-
def my_handler(agent):
|
|
303
|
-
pass
|
|
304
|
-
|
|
305
|
-
wrapped = after_llm(my_handler)
|
|
306
|
-
|
|
307
|
-
assert hasattr(wrapped, '_event_type')
|
|
308
|
-
assert wrapped._event_type == 'after_llm'
|
|
309
|
-
|
|
310
|
-
def test_all_event_types_have_correct_attributes(self):
|
|
311
|
-
"""Test all event wrapper types set correct _event_type"""
|
|
312
|
-
|
|
313
|
-
def handler(agent):
|
|
314
|
-
pass
|
|
315
|
-
|
|
316
|
-
assert after_user_input(handler)._event_type == 'after_user_input'
|
|
317
|
-
assert before_llm(handler)._event_type == 'before_llm'
|
|
318
|
-
assert after_llm(handler)._event_type == 'after_llm'
|
|
319
|
-
assert before_tool(handler)._event_type == 'before_tool'
|
|
320
|
-
assert after_tool(handler)._event_type == 'after_tool'
|
|
321
|
-
assert on_error(handler)._event_type == 'on_error'
|
|
322
|
-
assert on_complete(handler)._event_type == 'on_complete'
|
|
323
|
-
|
|
324
|
-
def test_event_validation_rejects_non_callable(self):
|
|
325
|
-
"""Test that non-callable events are rejected with clear error"""
|
|
326
|
-
|
|
327
|
-
with pytest.raises(TypeError, match="Event must be callable"):
|
|
328
|
-
Agent(
|
|
329
|
-
"test",
|
|
330
|
-
model="gpt-4o-mini",
|
|
331
|
-
on_events=["not a function"] # String instead of callable
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
def test_event_validation_rejects_missing_event_type(self):
|
|
335
|
-
"""Test that events without _event_type are rejected with helpful error"""
|
|
336
|
-
|
|
337
|
-
def my_handler(agent):
|
|
338
|
-
pass
|
|
339
|
-
|
|
340
|
-
with pytest.raises(ValueError, match="missing _event_type.*Did you forget to wrap it"):
|
|
341
|
-
Agent(
|
|
342
|
-
"test",
|
|
343
|
-
model="gpt-4o-mini",
|
|
344
|
-
on_events=[my_handler] # Not wrapped with after_llm(), etc.
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
def test_event_validation_rejects_invalid_event_type(self):
|
|
348
|
-
"""Test that events with invalid _event_type are rejected"""
|
|
349
|
-
|
|
350
|
-
def my_handler(agent):
|
|
351
|
-
pass
|
|
352
|
-
|
|
353
|
-
my_handler._event_type = 'invalid_event_type'
|
|
354
|
-
|
|
355
|
-
with pytest.raises(ValueError, match="Invalid event type"):
|
|
356
|
-
Agent(
|
|
357
|
-
"test",
|
|
358
|
-
model="gpt-4o-mini",
|
|
359
|
-
on_events=[my_handler]
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
def test_on_error_fires_on_tool_not_found(self):
|
|
363
|
-
"""Test on_error fires when tool is not found (consistency with tool execution errors)"""
|
|
364
|
-
calls = []
|
|
365
|
-
|
|
366
|
-
def track_error(agent):
|
|
367
|
-
calls.append('on_error')
|
|
368
|
-
# Verify we can access error information
|
|
369
|
-
trace = agent.current_session['trace']
|
|
370
|
-
tool_executions = [t for t in trace if t['type'] == 'tool_execution']
|
|
371
|
-
assert len(tool_executions) > 0
|
|
372
|
-
latest_tool = tool_executions[-1]
|
|
373
|
-
assert latest_tool['status'] == 'not_found'
|
|
374
|
-
assert 'error' in latest_tool
|
|
375
|
-
assert 'not found' in latest_tool['error']
|
|
376
|
-
|
|
377
|
-
agent = Agent(
|
|
378
|
-
"test",
|
|
379
|
-
tools=[search], # Only has 'search' tool
|
|
380
|
-
model="gpt-4o-mini",
|
|
381
|
-
on_events=[on_error(track_error)]
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
# Directly execute a nonexistent tool to trigger tool not found
|
|
385
|
-
result = agent.execute_tool("nonexistent_tool", {"query": "test"})
|
|
386
|
-
|
|
387
|
-
# Should fire on_error exactly once
|
|
388
|
-
assert len(calls) == 1
|
|
389
|
-
|
|
390
|
-
def test_on_complete_fires_once_per_input(self):
|
|
391
|
-
"""Test on_complete fires exactly once per input() call, after final response"""
|
|
392
|
-
calls = []
|
|
393
|
-
|
|
394
|
-
def track_complete(agent):
|
|
395
|
-
calls.append('on_complete')
|
|
396
|
-
# Verify session exists and has response
|
|
397
|
-
assert agent.current_session is not None
|
|
398
|
-
assert 'trace' in agent.current_session
|
|
399
|
-
|
|
400
|
-
agent = Agent(
|
|
401
|
-
"test",
|
|
402
|
-
tools=[search],
|
|
403
|
-
model="gpt-4o-mini",
|
|
404
|
-
on_events=[on_complete(track_complete)]
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
# First input
|
|
408
|
-
agent.input("Search for Python")
|
|
409
|
-
assert len(calls) == 1
|
|
410
|
-
|
|
411
|
-
# Second input
|
|
412
|
-
agent.input("Search for JavaScript")
|
|
413
|
-
assert len(calls) == 2
|
|
414
|
-
|
|
415
|
-
def test_on_complete_fires_after_tool_execution(self):
|
|
416
|
-
"""Test on_complete fires after all tools have completed"""
|
|
417
|
-
event_order = []
|
|
418
|
-
|
|
419
|
-
def track_after_tool(agent):
|
|
420
|
-
event_order.append('after_tool')
|
|
421
|
-
|
|
422
|
-
def track_complete(agent):
|
|
423
|
-
event_order.append('on_complete')
|
|
424
|
-
|
|
425
|
-
agent = Agent(
|
|
426
|
-
"test",
|
|
427
|
-
tools=[search],
|
|
428
|
-
model="gpt-4o-mini",
|
|
429
|
-
on_events=[
|
|
430
|
-
after_tool(track_after_tool),
|
|
431
|
-
on_complete(track_complete)
|
|
432
|
-
]
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
agent.input("Search for Python")
|
|
436
|
-
|
|
437
|
-
# on_complete should be the last event
|
|
438
|
-
assert event_order[-1] == 'on_complete'
|
|
439
|
-
# after_tool should fire before on_complete
|
|
440
|
-
assert 'after_tool' in event_order
|
|
441
|
-
assert event_order.index('after_tool') < event_order.index('on_complete')
|
|
442
|
-
|
|
443
|
-
def test_after_tool_fires_for_all_executions(self):
|
|
444
|
-
"""Test after_tool fires for ALL tool executions (success, error, not_found)"""
|
|
445
|
-
after_tool_calls = []
|
|
446
|
-
on_error_calls = []
|
|
447
|
-
|
|
448
|
-
def track_after_tool(agent):
|
|
449
|
-
after_tool_calls.append(agent.current_session['trace'][-1]['status'])
|
|
450
|
-
|
|
451
|
-
def track_error(agent):
|
|
452
|
-
on_error_calls.append(agent.current_session['trace'][-1]['status'])
|
|
453
|
-
|
|
454
|
-
agent = Agent(
|
|
455
|
-
"test",
|
|
456
|
-
tools=[search, failing_tool],
|
|
457
|
-
model="gpt-4o-mini",
|
|
458
|
-
on_events=[after_tool(track_after_tool), on_error(track_error)]
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
# Success case
|
|
462
|
-
agent.execute_tool("search", {"query": "test"})
|
|
463
|
-
|
|
464
|
-
# Error case
|
|
465
|
-
agent.execute_tool("failing_tool", {"query": "test"})
|
|
466
|
-
|
|
467
|
-
# Not found case
|
|
468
|
-
agent.execute_tool("nonexistent", {})
|
|
469
|
-
|
|
470
|
-
# after_tool should fire for all 3 executions
|
|
471
|
-
assert len(after_tool_calls) == 3
|
|
472
|
-
assert after_tool_calls == ['success', 'error', 'not_found']
|
|
473
|
-
|
|
474
|
-
# on_error should fire only for error and not_found (2 times)
|
|
475
|
-
assert len(on_error_calls) == 2
|
|
476
|
-
assert on_error_calls == ['error', 'not_found']
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
class TestNewEventSyntax:
|
|
480
|
-
"""Test new decorator and multiple-args wrapper syntax"""
|
|
481
|
-
|
|
482
|
-
def test_decorator_syntax_single_function(self):
|
|
483
|
-
"""Test @before_tool decorator returns single callable function"""
|
|
484
|
-
|
|
485
|
-
@before_tool
|
|
486
|
-
def check_something(agent):
|
|
487
|
-
pass
|
|
488
|
-
|
|
489
|
-
# Should be callable (not a list)
|
|
490
|
-
assert callable(check_something)
|
|
491
|
-
assert hasattr(check_something, '_event_type')
|
|
492
|
-
assert check_something._event_type == 'before_tool'
|
|
493
|
-
|
|
494
|
-
def test_decorator_syntax_all_event_types(self):
|
|
495
|
-
"""Test decorator syntax works for all event types"""
|
|
496
|
-
|
|
497
|
-
@after_user_input
|
|
498
|
-
def handler1(agent):
|
|
499
|
-
pass
|
|
500
|
-
|
|
501
|
-
@before_llm
|
|
502
|
-
def handler2(agent):
|
|
503
|
-
pass
|
|
504
|
-
|
|
505
|
-
@after_llm
|
|
506
|
-
def handler3(agent):
|
|
507
|
-
pass
|
|
508
|
-
|
|
509
|
-
@before_tool
|
|
510
|
-
def handler4(agent):
|
|
511
|
-
pass
|
|
512
|
-
|
|
513
|
-
@after_tool
|
|
514
|
-
def handler5(agent):
|
|
515
|
-
pass
|
|
516
|
-
|
|
517
|
-
@on_error
|
|
518
|
-
def handler6(agent):
|
|
519
|
-
pass
|
|
520
|
-
|
|
521
|
-
@on_complete
|
|
522
|
-
def handler7(agent):
|
|
523
|
-
pass
|
|
524
|
-
|
|
525
|
-
# All should be callable
|
|
526
|
-
assert callable(handler1)
|
|
527
|
-
assert callable(handler2)
|
|
528
|
-
assert callable(handler3)
|
|
529
|
-
assert callable(handler4)
|
|
530
|
-
assert callable(handler5)
|
|
531
|
-
assert callable(handler6)
|
|
532
|
-
assert callable(handler7)
|
|
533
|
-
|
|
534
|
-
# All should have correct _event_type
|
|
535
|
-
assert handler1._event_type == 'after_user_input'
|
|
536
|
-
assert handler2._event_type == 'before_llm'
|
|
537
|
-
assert handler3._event_type == 'after_llm'
|
|
538
|
-
assert handler4._event_type == 'before_tool'
|
|
539
|
-
assert handler5._event_type == 'after_tool'
|
|
540
|
-
assert handler6._event_type == 'on_error'
|
|
541
|
-
assert handler7._event_type == 'on_complete'
|
|
542
|
-
|
|
543
|
-
def test_wrapper_multiple_args_returns_list(self):
|
|
544
|
-
"""Test before_tool(fn1, fn2) returns list"""
|
|
545
|
-
|
|
546
|
-
def handler1(agent):
|
|
547
|
-
pass
|
|
548
|
-
|
|
549
|
-
def handler2(agent):
|
|
550
|
-
pass
|
|
551
|
-
|
|
552
|
-
result = before_tool(handler1, handler2)
|
|
553
|
-
|
|
554
|
-
# Should be a list
|
|
555
|
-
assert isinstance(result, list)
|
|
556
|
-
assert len(result) == 2
|
|
557
|
-
|
|
558
|
-
# Both should have _event_type
|
|
559
|
-
assert result[0]._event_type == 'before_tool'
|
|
560
|
-
assert result[1]._event_type == 'before_tool'
|
|
561
|
-
|
|
562
|
-
def test_wrapper_single_arg_returns_function(self):
|
|
563
|
-
"""Test before_tool(fn) returns single function (backward compatible)"""
|
|
564
|
-
|
|
565
|
-
def handler(agent):
|
|
566
|
-
pass
|
|
567
|
-
|
|
568
|
-
result = before_tool(handler)
|
|
569
|
-
|
|
570
|
-
# Should be callable, not list
|
|
571
|
-
assert callable(result)
|
|
572
|
-
assert not isinstance(result, list)
|
|
573
|
-
assert result._event_type == 'before_tool'
|
|
574
|
-
|
|
575
|
-
def test_agent_accepts_decorated_handlers(self):
|
|
576
|
-
"""Test Agent accepts @decorated handlers in on_events"""
|
|
577
|
-
|
|
578
|
-
@before_tool
|
|
579
|
-
def check_tool(agent):
|
|
580
|
-
pass
|
|
581
|
-
|
|
582
|
-
@after_tool
|
|
583
|
-
def log_tool(agent):
|
|
584
|
-
pass
|
|
585
|
-
|
|
586
|
-
agent = Agent(
|
|
587
|
-
"test",
|
|
588
|
-
model="gpt-4o-mini",
|
|
589
|
-
on_events=[check_tool, log_tool],
|
|
590
|
-
log=False
|
|
591
|
-
)
|
|
592
|
-
|
|
593
|
-
assert len(agent.events['before_tool']) == 1
|
|
594
|
-
assert len(agent.events['after_tool']) == 1
|
|
595
|
-
|
|
596
|
-
def test_agent_accepts_multiple_args_wrapper(self):
|
|
597
|
-
"""Test Agent accepts before_tool(fn1, fn2) in on_events"""
|
|
598
|
-
|
|
599
|
-
def handler1(agent):
|
|
600
|
-
pass
|
|
601
|
-
|
|
602
|
-
def handler2(agent):
|
|
603
|
-
pass
|
|
604
|
-
|
|
605
|
-
def handler3(agent):
|
|
606
|
-
pass
|
|
607
|
-
|
|
608
|
-
agent = Agent(
|
|
609
|
-
"test",
|
|
610
|
-
model="gpt-4o-mini",
|
|
611
|
-
on_events=[
|
|
612
|
-
before_tool(handler1, handler2), # returns [fn, fn]
|
|
613
|
-
after_tool(handler3), # returns fn
|
|
614
|
-
],
|
|
615
|
-
log=False
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
# Should have 2 before_tool handlers and 1 after_tool handler
|
|
619
|
-
assert len(agent.events['before_tool']) == 2
|
|
620
|
-
assert len(agent.events['after_tool']) == 1
|
|
621
|
-
|
|
622
|
-
def test_mixed_decorator_and_wrapper_syntax(self):
|
|
623
|
-
"""Test mixing @decorator and wrapper(fn1, fn2) syntax"""
|
|
624
|
-
|
|
625
|
-
@before_tool
|
|
626
|
-
def decorated_handler(agent):
|
|
627
|
-
pass
|
|
628
|
-
|
|
629
|
-
def wrapper_handler1(agent):
|
|
630
|
-
pass
|
|
631
|
-
|
|
632
|
-
def wrapper_handler2(agent):
|
|
633
|
-
pass
|
|
634
|
-
|
|
635
|
-
agent = Agent(
|
|
636
|
-
"test",
|
|
637
|
-
model="gpt-4o-mini",
|
|
638
|
-
on_events=[
|
|
639
|
-
decorated_handler, # @decorator
|
|
640
|
-
before_tool(wrapper_handler1, wrapper_handler2), # wrapper(fn1, fn2)
|
|
641
|
-
],
|
|
642
|
-
log=False
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
# Should have 3 before_tool handlers total
|
|
646
|
-
assert len(agent.events['before_tool']) == 3
|
|
647
|
-
|
|
648
|
-
def test_handlers_fire_in_order(self):
|
|
649
|
-
"""Test handlers fire in correct order regardless of syntax"""
|
|
650
|
-
calls = []
|
|
651
|
-
|
|
652
|
-
@after_user_input
|
|
653
|
-
def handler1(agent):
|
|
654
|
-
calls.append('handler1')
|
|
655
|
-
|
|
656
|
-
def handler2(agent):
|
|
657
|
-
calls.append('handler2')
|
|
658
|
-
|
|
659
|
-
def handler3(agent):
|
|
660
|
-
calls.append('handler3')
|
|
661
|
-
|
|
662
|
-
agent = Agent(
|
|
663
|
-
"test",
|
|
664
|
-
model="gpt-4o-mini",
|
|
665
|
-
on_events=[
|
|
666
|
-
handler1, # decorated
|
|
667
|
-
after_user_input(handler2, handler3), # wrapper with multiple
|
|
668
|
-
],
|
|
669
|
-
log=False
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
# Manually invoke to test order
|
|
673
|
-
agent.current_session = {'user_prompt': 'test'}
|
|
674
|
-
agent._invoke_events('after_user_input')
|
|
675
|
-
|
|
676
|
-
# Should fire in registration order
|
|
677
|
-
assert calls == ['handler1', 'handler2', 'handler3']
|