connectonion 0.5.8__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 +78 -0
- connectonion/address.py +320 -0
- connectonion/agent.py +450 -0
- connectonion/announce.py +84 -0
- connectonion/asgi.py +287 -0
- connectonion/auto_debug_exception.py +181 -0
- connectonion/cli/__init__.py +3 -0
- connectonion/cli/browser_agent/__init__.py +5 -0
- connectonion/cli/browser_agent/browser.py +243 -0
- connectonion/cli/browser_agent/prompt.md +107 -0
- connectonion/cli/commands/__init__.py +1 -0
- connectonion/cli/commands/auth_commands.py +527 -0
- connectonion/cli/commands/browser_commands.py +27 -0
- connectonion/cli/commands/create.py +511 -0
- connectonion/cli/commands/deploy_commands.py +220 -0
- connectonion/cli/commands/doctor_commands.py +173 -0
- connectonion/cli/commands/init.py +469 -0
- connectonion/cli/commands/project_cmd_lib.py +828 -0
- connectonion/cli/commands/reset_commands.py +149 -0
- connectonion/cli/commands/status_commands.py +168 -0
- connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
- connectonion/cli/docs/connectonion.md +1256 -0
- connectonion/cli/docs.md +123 -0
- connectonion/cli/main.py +148 -0
- connectonion/cli/templates/meta-agent/README.md +287 -0
- connectonion/cli/templates/meta-agent/agent.py +196 -0
- connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
- connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
- connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
- connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
- connectonion/cli/templates/minimal/README.md +56 -0
- connectonion/cli/templates/minimal/agent.py +40 -0
- connectonion/cli/templates/playwright/README.md +118 -0
- connectonion/cli/templates/playwright/agent.py +336 -0
- connectonion/cli/templates/playwright/prompt.md +102 -0
- connectonion/cli/templates/playwright/requirements.txt +3 -0
- connectonion/cli/templates/web-research/agent.py +122 -0
- connectonion/connect.py +128 -0
- connectonion/console.py +539 -0
- connectonion/debug_agent/__init__.py +13 -0
- connectonion/debug_agent/agent.py +45 -0
- connectonion/debug_agent/prompts/debug_assistant.md +72 -0
- connectonion/debug_agent/runtime_inspector.py +406 -0
- connectonion/debug_explainer/__init__.py +10 -0
- connectonion/debug_explainer/explain_agent.py +114 -0
- connectonion/debug_explainer/explain_context.py +263 -0
- connectonion/debug_explainer/explainer_prompt.md +29 -0
- connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
- connectonion/debugger_ui.py +1039 -0
- connectonion/decorators.py +208 -0
- connectonion/events.py +248 -0
- connectonion/execution_analyzer/__init__.py +9 -0
- connectonion/execution_analyzer/execution_analysis.py +93 -0
- connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
- connectonion/host.py +579 -0
- connectonion/interactive_debugger.py +342 -0
- connectonion/llm.py +801 -0
- connectonion/llm_do.py +307 -0
- connectonion/logger.py +300 -0
- connectonion/prompt_files/__init__.py +1 -0
- connectonion/prompt_files/analyze_contact.md +62 -0
- connectonion/prompt_files/eval_expected.md +12 -0
- connectonion/prompt_files/react_evaluate.md +11 -0
- connectonion/prompt_files/react_plan.md +16 -0
- connectonion/prompt_files/reflect.md +22 -0
- connectonion/prompts.py +144 -0
- connectonion/relay.py +200 -0
- connectonion/static/docs.html +688 -0
- connectonion/tool_executor.py +279 -0
- connectonion/tool_factory.py +186 -0
- connectonion/tool_registry.py +105 -0
- connectonion/trust.py +166 -0
- connectonion/trust_agents.py +71 -0
- connectonion/trust_functions.py +88 -0
- connectonion/tui/__init__.py +57 -0
- connectonion/tui/divider.py +39 -0
- connectonion/tui/dropdown.py +251 -0
- connectonion/tui/footer.py +31 -0
- connectonion/tui/fuzzy.py +56 -0
- connectonion/tui/input.py +278 -0
- connectonion/tui/keys.py +35 -0
- connectonion/tui/pick.py +130 -0
- connectonion/tui/providers.py +155 -0
- connectonion/tui/status_bar.py +163 -0
- connectonion/usage.py +161 -0
- connectonion/useful_events_handlers/__init__.py +16 -0
- connectonion/useful_events_handlers/reflect.py +116 -0
- connectonion/useful_plugins/__init__.py +20 -0
- connectonion/useful_plugins/calendar_plugin.py +163 -0
- connectonion/useful_plugins/eval.py +139 -0
- connectonion/useful_plugins/gmail_plugin.py +162 -0
- connectonion/useful_plugins/image_result_formatter.py +127 -0
- connectonion/useful_plugins/re_act.py +78 -0
- connectonion/useful_plugins/shell_approval.py +159 -0
- connectonion/useful_tools/__init__.py +44 -0
- connectonion/useful_tools/diff_writer.py +192 -0
- connectonion/useful_tools/get_emails.py +183 -0
- connectonion/useful_tools/gmail.py +1596 -0
- connectonion/useful_tools/google_calendar.py +613 -0
- connectonion/useful_tools/memory.py +380 -0
- connectonion/useful_tools/microsoft_calendar.py +604 -0
- connectonion/useful_tools/outlook.py +488 -0
- connectonion/useful_tools/send_email.py +205 -0
- connectonion/useful_tools/shell.py +97 -0
- connectonion/useful_tools/slash_command.py +201 -0
- connectonion/useful_tools/terminal.py +285 -0
- connectonion/useful_tools/todo_list.py +241 -0
- connectonion/useful_tools/web_fetch.py +216 -0
- connectonion/xray.py +467 -0
- connectonion-0.5.8.dist-info/METADATA +741 -0
- connectonion-0.5.8.dist-info/RECORD +113 -0
- connectonion-0.5.8.dist-info/WHEEL +4 -0
- connectonion-0.5.8.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Execute agent tools with xray context injection, timing, error handling, and trace recording
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [time, json, typing, xray.py] | imported by [agent.py] | tested by [tests/test_tool_executor.py]
|
|
5
|
+
Data flow: receives from Agent → tool_calls: List[ToolCall], tools: ToolRegistry, agent: Agent, logger: Logger → for each tool: injects xray context via inject_xray_context() → executes tool_func(**tool_args) → records timing and result → appends to agent.current_session['trace'] → clears xray context → adds tool result to messages
|
|
6
|
+
State/Effects: mutates agent.current_session['messages'] by appending assistant message with tool_calls and tool result messages | mutates agent.current_session['trace'] by appending tool_execution entries | calls logger.log_tool_call() and logger.log_tool_result() for user feedback | injects/clears xray context via thread-local storage
|
|
7
|
+
Integration: exposes execute_and_record_tools(tool_calls, tools, agent, logger), execute_single_tool(...) | uses logger.log_tool_call(name, args) for natural function-call style output: greet(name='Alice') | creates trace entries with type, tool_name, arguments, call_id, result, status, timing, iteration, timestamp
|
|
8
|
+
Performance: times each tool execution in milliseconds | executes tools sequentially (not parallel) | trace entry added BEFORE auto-trace so xray.trace() sees it
|
|
9
|
+
Errors: catches all tool execution exceptions | wraps errors in trace_entry with error, error_type fields | returns error message to LLM for retry | prints error to logger with red ✗
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
import json
|
|
14
|
+
from typing import List, Dict, Any, Optional, Callable
|
|
15
|
+
|
|
16
|
+
from .xray import (
|
|
17
|
+
inject_xray_context,
|
|
18
|
+
clear_xray_context,
|
|
19
|
+
is_xray_enabled
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def execute_and_record_tools(
|
|
24
|
+
tool_calls: List,
|
|
25
|
+
tools: Any, # ToolRegistry
|
|
26
|
+
agent: Any,
|
|
27
|
+
logger: Any # Logger instance
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Execute requested tools and update conversation messages.
|
|
30
|
+
|
|
31
|
+
Uses agent.current_session as single source of truth for messages and trace.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
tool_calls: List of tool calls from LLM response
|
|
35
|
+
tools: ToolRegistry containing tools
|
|
36
|
+
agent: Agent instance with current_session containing messages and trace
|
|
37
|
+
logger: Logger for output (always provided by Agent)
|
|
38
|
+
"""
|
|
39
|
+
# Format and add assistant message with tool calls
|
|
40
|
+
_add_assistant_message(agent.current_session['messages'], tool_calls)
|
|
41
|
+
|
|
42
|
+
# before_tools fires ONCE before ALL tools in the batch execute
|
|
43
|
+
agent._invoke_events('before_tools')
|
|
44
|
+
|
|
45
|
+
# Execute each tool
|
|
46
|
+
for tool_call in tool_calls:
|
|
47
|
+
# Execute the tool and get trace entry
|
|
48
|
+
trace_entry = execute_single_tool(
|
|
49
|
+
tool_name=tool_call.name,
|
|
50
|
+
tool_args=tool_call.arguments,
|
|
51
|
+
tool_id=tool_call.id,
|
|
52
|
+
tools=tools,
|
|
53
|
+
agent=agent,
|
|
54
|
+
logger=logger
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Add result to conversation messages
|
|
58
|
+
_add_tool_result_message(
|
|
59
|
+
agent.current_session['messages'],
|
|
60
|
+
tool_call.id,
|
|
61
|
+
trace_entry["result"]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Note: trace_entry already added to session in execute_single_tool
|
|
65
|
+
# (before auto-trace, so it shows up in xray.trace() output)
|
|
66
|
+
|
|
67
|
+
# Fire events AFTER tool result message is added (proper message ordering)
|
|
68
|
+
# on_error fires first for errors/not_found
|
|
69
|
+
if trace_entry["status"] in ("error", "not_found"):
|
|
70
|
+
agent._invoke_events('on_error')
|
|
71
|
+
|
|
72
|
+
# after_each_tool fires for EACH tool execution (success, error, not_found)
|
|
73
|
+
# WARNING: Do NOT add messages here - it breaks Anthropic's message ordering
|
|
74
|
+
agent._invoke_events('after_each_tool')
|
|
75
|
+
|
|
76
|
+
# after_tools fires ONCE after ALL tools in the batch complete
|
|
77
|
+
# This is the safe place to add messages (e.g., reflection) because all
|
|
78
|
+
# tool_results have been added and message ordering is correct for all LLMs
|
|
79
|
+
agent._invoke_events('after_tools')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def execute_single_tool(
|
|
83
|
+
tool_name: str,
|
|
84
|
+
tool_args: Dict,
|
|
85
|
+
tool_id: str,
|
|
86
|
+
tools: Any, # ToolRegistry
|
|
87
|
+
agent: Any,
|
|
88
|
+
logger: Any # Logger instance
|
|
89
|
+
) -> Dict[str, Any]:
|
|
90
|
+
"""Execute a single tool and return trace entry.
|
|
91
|
+
|
|
92
|
+
Uses agent.current_session as single source of truth.
|
|
93
|
+
Checks for __xray_enabled__ attribute to auto-print Rich tables.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
tool_name: Name of the tool to execute
|
|
97
|
+
tool_args: Arguments to pass to the tool
|
|
98
|
+
tool_id: ID of the tool call
|
|
99
|
+
tools: ToolRegistry containing tools
|
|
100
|
+
agent: Agent instance with current_session
|
|
101
|
+
logger: Logger for output (always provided by Agent)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dict trace entry with: type, tool_name, arguments, call_id, result, status, timing, iteration, timestamp
|
|
105
|
+
"""
|
|
106
|
+
# Log tool call before execution
|
|
107
|
+
logger.log_tool_call(tool_name, tool_args)
|
|
108
|
+
|
|
109
|
+
# Create single trace entry
|
|
110
|
+
trace_entry = {
|
|
111
|
+
"type": "tool_execution",
|
|
112
|
+
"tool_name": tool_name,
|
|
113
|
+
"arguments": tool_args,
|
|
114
|
+
"call_id": tool_id,
|
|
115
|
+
"timing": 0,
|
|
116
|
+
"status": "pending",
|
|
117
|
+
"result": None,
|
|
118
|
+
"iteration": agent.current_session['iteration'],
|
|
119
|
+
"timestamp": time.time()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Check if tool exists
|
|
123
|
+
tool_func = tools.get(tool_name)
|
|
124
|
+
if tool_func is None:
|
|
125
|
+
error_msg = f"Tool '{tool_name}' not found"
|
|
126
|
+
|
|
127
|
+
# Update trace entry
|
|
128
|
+
trace_entry["result"] = error_msg
|
|
129
|
+
trace_entry["status"] = "not_found"
|
|
130
|
+
trace_entry["error"] = error_msg
|
|
131
|
+
|
|
132
|
+
# Add trace entry to session (so on_error handlers can see it)
|
|
133
|
+
agent.current_session['trace'].append(trace_entry)
|
|
134
|
+
|
|
135
|
+
# Logger output
|
|
136
|
+
logger.print(f"[red]✗[/red] {error_msg}")
|
|
137
|
+
|
|
138
|
+
# Note: on_error event will fire in execute_and_record_tools after result message added
|
|
139
|
+
|
|
140
|
+
return trace_entry
|
|
141
|
+
|
|
142
|
+
# Check if tool has @xray decorator
|
|
143
|
+
xray_enabled = is_xray_enabled(tool_func)
|
|
144
|
+
|
|
145
|
+
# Prepare context data for xray
|
|
146
|
+
previous_tools = [
|
|
147
|
+
entry.get("tool_name") for entry in agent.current_session['trace']
|
|
148
|
+
if entry.get("type") == "tool_execution"
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# Inject xray context before tool execution
|
|
152
|
+
inject_xray_context(
|
|
153
|
+
agent=agent,
|
|
154
|
+
user_prompt=agent.current_session.get('user_prompt', ''),
|
|
155
|
+
messages=agent.current_session['messages'].copy(),
|
|
156
|
+
iteration=agent.current_session['iteration'],
|
|
157
|
+
previous_tools=previous_tools
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Initialize timing (for error case if before_tool fails)
|
|
161
|
+
tool_start = time.time()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Set pending_tool for before_tool handlers to access
|
|
165
|
+
agent.current_session['pending_tool'] = {
|
|
166
|
+
'name': tool_name,
|
|
167
|
+
'arguments': tool_args,
|
|
168
|
+
'id': tool_id
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Invoke before_each_tool events
|
|
172
|
+
agent._invoke_events('before_each_tool')
|
|
173
|
+
|
|
174
|
+
# Clear pending_tool after event (it's only valid during before_tool)
|
|
175
|
+
agent.current_session.pop('pending_tool', None)
|
|
176
|
+
|
|
177
|
+
# Execute the tool with timing (restart timer AFTER events for accurate tool timing)
|
|
178
|
+
tool_start = time.time()
|
|
179
|
+
result = tool_func(**tool_args)
|
|
180
|
+
tool_duration = (time.time() - tool_start) * 1000 # milliseconds
|
|
181
|
+
|
|
182
|
+
# Update trace entry
|
|
183
|
+
trace_entry["timing"] = tool_duration
|
|
184
|
+
trace_entry["result"] = str(result)
|
|
185
|
+
trace_entry["status"] = "success"
|
|
186
|
+
|
|
187
|
+
# Add trace entry to session BEFORE auto-trace
|
|
188
|
+
# (so it shows up in xray.trace() output)
|
|
189
|
+
agent.current_session['trace'].append(trace_entry)
|
|
190
|
+
|
|
191
|
+
# Logger output - result on separate line
|
|
192
|
+
logger.log_tool_result(str(result), tool_duration)
|
|
193
|
+
|
|
194
|
+
# Auto-print Rich table if @xray enabled
|
|
195
|
+
if xray_enabled:
|
|
196
|
+
logger.print_xray_table(
|
|
197
|
+
tool_name=tool_name,
|
|
198
|
+
tool_args=tool_args,
|
|
199
|
+
result=result,
|
|
200
|
+
timing=tool_duration,
|
|
201
|
+
agent=agent
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Note: after_tool event will fire in execute_and_record_tools after result message added
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
# Calculate timing from initial start (includes before_tool if it succeeded)
|
|
208
|
+
tool_duration = (time.time() - tool_start) * 1000
|
|
209
|
+
|
|
210
|
+
# Update trace entry
|
|
211
|
+
trace_entry["timing"] = tool_duration
|
|
212
|
+
trace_entry["status"] = "error"
|
|
213
|
+
trace_entry["error"] = str(e)
|
|
214
|
+
trace_entry["error_type"] = type(e).__name__
|
|
215
|
+
|
|
216
|
+
error_msg = f"Error executing tool: {str(e)}"
|
|
217
|
+
trace_entry["result"] = error_msg
|
|
218
|
+
|
|
219
|
+
# Add error trace entry to session (so on_error handlers can see it)
|
|
220
|
+
agent.current_session['trace'].append(trace_entry)
|
|
221
|
+
|
|
222
|
+
# Logger output
|
|
223
|
+
time_str = f"{tool_duration/1000:.4f}s" if tool_duration < 100 else f"{tool_duration/1000:.1f}s"
|
|
224
|
+
logger.print(f"[red]✗[/red] Error ({time_str}): {str(e)}")
|
|
225
|
+
|
|
226
|
+
# Note: on_error event will fire in execute_and_record_tools after result message added
|
|
227
|
+
|
|
228
|
+
finally:
|
|
229
|
+
# Clear xray context after tool execution
|
|
230
|
+
clear_xray_context()
|
|
231
|
+
|
|
232
|
+
return trace_entry
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _add_assistant_message(messages: List[Dict], tool_calls: List) -> None:
|
|
236
|
+
"""Format and add assistant message with tool calls.
|
|
237
|
+
|
|
238
|
+
Preserves extra_content (e.g., Gemini 3 thought_signature) which must be
|
|
239
|
+
echoed back to the LLM for certain providers to work correctly.
|
|
240
|
+
See: https://ai.google.dev/gemini-api/docs/thinking#openai-sdk
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
messages: Conversation messages list (will be mutated)
|
|
244
|
+
tool_calls: Tool calls from LLM response
|
|
245
|
+
"""
|
|
246
|
+
assistant_tool_calls = []
|
|
247
|
+
for tool_call in tool_calls:
|
|
248
|
+
tc_dict = {
|
|
249
|
+
"id": tool_call.id,
|
|
250
|
+
"type": "function",
|
|
251
|
+
"function": {
|
|
252
|
+
"name": tool_call.name,
|
|
253
|
+
"arguments": json.dumps(tool_call.arguments)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
# Only include extra_content if present (Gemini rejects null values)
|
|
257
|
+
if tool_call.extra_content:
|
|
258
|
+
tc_dict["extra_content"] = tool_call.extra_content
|
|
259
|
+
assistant_tool_calls.append(tc_dict)
|
|
260
|
+
|
|
261
|
+
messages.append({
|
|
262
|
+
"role": "assistant",
|
|
263
|
+
"tool_calls": assistant_tool_calls
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _add_tool_result_message(messages: List[Dict], tool_id: str, result: Any) -> None:
|
|
268
|
+
"""Add tool result message to conversation.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
messages: Conversation messages list (will be mutated)
|
|
272
|
+
tool_id: ID of the tool call
|
|
273
|
+
result: Result from tool execution
|
|
274
|
+
"""
|
|
275
|
+
messages.append({
|
|
276
|
+
"role": "tool",
|
|
277
|
+
"content": str(result),
|
|
278
|
+
"tool_call_id": tool_id
|
|
279
|
+
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Convert Python functions and class methods into agent-compatible tool schemas
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [inspect, functools, typing] | imported by [agent.py, __init__.py] | tested by [tests/test_tool_factory.py]
|
|
5
|
+
Data flow: receives func: Callable → inspects signature with inspect.signature() → extracts type hints with get_type_hints() → maps Python types to JSON Schema via TYPE_MAP → creates tool with .name, .description, .to_function_schema(), .run() attributes → returns wrapped Callable
|
|
6
|
+
State/Effects: no side effects | pure function transformations | preserves @xray and @replay decorator flags via hasattr checks | creates wrapper functions for bound methods to maintain self reference
|
|
7
|
+
Integration: exposes create_tool_from_function(func), extract_methods_from_instance(obj), is_class_instance(obj) | used by Agent.__init__ to auto-convert tools | supports both standalone functions and bound methods | skips private methods (starting with _)
|
|
8
|
+
Performance: uses inspect module (relatively fast) | TYPE_MAP provides O(1) type lookups | caches nothing (recreates on each call)
|
|
9
|
+
Errors: skips methods without type annotations | skips methods without return type hint | handles inspection failures gracefully | wraps functions with functools.wraps to preserve metadata
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import functools
|
|
14
|
+
from typing import Callable, Dict, Any, get_type_hints, List
|
|
15
|
+
|
|
16
|
+
# Map Python types to JSON Schema types
|
|
17
|
+
TYPE_MAP = {
|
|
18
|
+
str: "string",
|
|
19
|
+
int: "integer",
|
|
20
|
+
float: "number",
|
|
21
|
+
bool: "boolean",
|
|
22
|
+
list: "array",
|
|
23
|
+
dict: "object",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def create_tool_from_function(func: Callable) -> Callable:
|
|
27
|
+
"""
|
|
28
|
+
Converts a Python function into a tool that is compatible with the Agent,
|
|
29
|
+
by inspecting its signature and docstring.
|
|
30
|
+
"""
|
|
31
|
+
name = func.__name__
|
|
32
|
+
description = inspect.getdoc(func) or f"Execute the {name} tool."
|
|
33
|
+
|
|
34
|
+
# Build the parameters schema from the function signature
|
|
35
|
+
sig = inspect.signature(func)
|
|
36
|
+
type_hints = get_type_hints(func)
|
|
37
|
+
|
|
38
|
+
properties = {}
|
|
39
|
+
required = []
|
|
40
|
+
|
|
41
|
+
for param in sig.parameters.values():
|
|
42
|
+
param_name = param.name
|
|
43
|
+
|
|
44
|
+
# Skip 'self' parameter for bound methods
|
|
45
|
+
if param_name == 'self':
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Use 'str' as a fallback if no type hint is available
|
|
49
|
+
param_type = type_hints.get(param_name, str)
|
|
50
|
+
schema_type = TYPE_MAP.get(param_type, "string")
|
|
51
|
+
|
|
52
|
+
properties[param_name] = {"type": schema_type}
|
|
53
|
+
|
|
54
|
+
if param.default is inspect.Parameter.empty:
|
|
55
|
+
required.append(param_name)
|
|
56
|
+
|
|
57
|
+
parameters_schema = {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": properties,
|
|
60
|
+
}
|
|
61
|
+
if required:
|
|
62
|
+
parameters_schema["required"] = required
|
|
63
|
+
|
|
64
|
+
# For bound methods, create a wrapper function that preserves the method
|
|
65
|
+
if inspect.ismethod(func):
|
|
66
|
+
base_func = getattr(func, "__func__", func)
|
|
67
|
+
|
|
68
|
+
@functools.wraps(base_func)
|
|
69
|
+
def wrapper(*args, **kwargs):
|
|
70
|
+
return func(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
# Preserve decorator flags from the underlying function (for backward compatibility)
|
|
73
|
+
# Note: xray context is now injected for ALL tools automatically,
|
|
74
|
+
# so @xray decorator is optional
|
|
75
|
+
for attr in ("__xray_enabled__", "__replay_enabled__"):
|
|
76
|
+
if hasattr(base_func, attr):
|
|
77
|
+
try:
|
|
78
|
+
setattr(wrapper, attr, getattr(base_func, attr))
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Ensure tool naming/docs are consistent for the Agent
|
|
83
|
+
wrapper.__name__ = name
|
|
84
|
+
wrapper.__doc__ = description
|
|
85
|
+
tool_func = wrapper
|
|
86
|
+
else:
|
|
87
|
+
tool_func = func
|
|
88
|
+
|
|
89
|
+
# Attach the necessary attributes for Agent compatibility
|
|
90
|
+
tool_func.name = name
|
|
91
|
+
tool_func.description = description
|
|
92
|
+
tool_func.get_parameters_schema = lambda: parameters_schema
|
|
93
|
+
tool_func.to_function_schema = lambda: {
|
|
94
|
+
"name": name,
|
|
95
|
+
"description": description,
|
|
96
|
+
"parameters": parameters_schema,
|
|
97
|
+
}
|
|
98
|
+
tool_func.run = tool_func # The agent calls .run() - this should be the decorated function
|
|
99
|
+
|
|
100
|
+
return tool_func
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def extract_methods_from_instance(instance) -> List[Callable]:
|
|
104
|
+
"""
|
|
105
|
+
Extract public methods from a class instance that can be used as tools.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
instance: A class instance to extract methods from
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of method functions that have proper type annotations
|
|
112
|
+
"""
|
|
113
|
+
methods = []
|
|
114
|
+
|
|
115
|
+
for name in dir(instance):
|
|
116
|
+
# Skip private methods (starting with _)
|
|
117
|
+
if name.startswith('_'):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
attr = getattr(instance, name)
|
|
121
|
+
|
|
122
|
+
# Check if it's a callable method (not a property or static value)
|
|
123
|
+
if not callable(attr):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Skip built-in methods like __class__, etc.
|
|
127
|
+
if isinstance(attr, type):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Check if it's actually a bound method (has __self__)
|
|
131
|
+
if not hasattr(attr, '__self__'):
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Check if method has proper type annotations
|
|
135
|
+
try:
|
|
136
|
+
sig = inspect.signature(attr)
|
|
137
|
+
type_hints = get_type_hints(attr)
|
|
138
|
+
|
|
139
|
+
# Must have return type annotation to be a valid tool
|
|
140
|
+
if 'return' not in type_hints:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Process method as tool, preserving self reference
|
|
144
|
+
tool = create_tool_from_function(attr)
|
|
145
|
+
methods.append(tool)
|
|
146
|
+
|
|
147
|
+
except (ValueError, TypeError):
|
|
148
|
+
# Skip methods that can't be inspected
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
return methods
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def is_class_instance(obj) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Check if an object is a class instance (not a function, class, or module).
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
obj: Object to check
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if obj is a class instance with callable methods
|
|
163
|
+
"""
|
|
164
|
+
# Must be an object with a class
|
|
165
|
+
if not hasattr(obj, '__class__'):
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
# Should not be a function, method, or class itself
|
|
169
|
+
if inspect.isfunction(obj) or inspect.ismethod(obj) or inspect.isclass(obj):
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# Should not be a module
|
|
173
|
+
if inspect.ismodule(obj):
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
# Should not be built-in types
|
|
177
|
+
if isinstance(obj, (list, dict, tuple, set, str, int, float, bool, type(None))):
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
# Should have some callable attributes (methods)
|
|
181
|
+
has_methods = any(
|
|
182
|
+
callable(getattr(obj, name, None)) and not name.startswith('_')
|
|
183
|
+
for name in dir(obj)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return has_methods
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Store and manage agent tools and class instances with O(1) lookup and conflict detection
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: None (standalone module) | imported by [agent.py] | tested by [tests/unit/test_tool_registry.py]
|
|
5
|
+
Data flow: Agent.__init__() creates ToolRegistry → .add(tool) stores tool with tool.name key → .add_instance(name, instance) stores class instances → .get(name) returns tool or None → __getattr__ enables agent.tools.send() attribute access → __iter__ yields tools for LLM schema generation
|
|
6
|
+
State/Effects: stores tools in _tools dict and instances in _instances dict | no file I/O or external effects | raises ValueError on duplicate names or conflicts between tool/instance names
|
|
7
|
+
Integration: exposes ToolRegistry class with add(), add_instance(), get(), get_instance(), remove(), names() | supports iteration (for tool in registry) | supports len() and bool | supports 'in' operator | attribute access checks instances first, then tools
|
|
8
|
+
Performance: O(1) dict-based lookup for all operations | iteration yields tools only (not instances) | memory proportional to number of tools/instances
|
|
9
|
+
Errors: raises ValueError for duplicate tool names | raises ValueError if tool name conflicts with instance name | raises AttributeError for unknown tool/instance names via __getattr__
|
|
10
|
+
|
|
11
|
+
Agent tools and instances with attribute access and conflict detection.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
# Call tools
|
|
15
|
+
agent.tools.send(to, subject, body)
|
|
16
|
+
agent.tools.search(query)
|
|
17
|
+
|
|
18
|
+
# Access class instances (for properties)
|
|
19
|
+
agent.tools.gmail.my_id
|
|
20
|
+
agent.tools.calendar.timezone
|
|
21
|
+
|
|
22
|
+
# API
|
|
23
|
+
agent.tools.add(tool)
|
|
24
|
+
agent.tools.add_instance('gmail', gmail_obj)
|
|
25
|
+
agent.tools.get('send')
|
|
26
|
+
agent.tools.get_instance('gmail')
|
|
27
|
+
|
|
28
|
+
# Iteration (tools only)
|
|
29
|
+
for tool in agent.tools:
|
|
30
|
+
print(tool.name)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ToolRegistry:
|
|
35
|
+
"""Agent tools and class instances with attribute access and conflict detection."""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self._tools = {}
|
|
39
|
+
self._instances = {}
|
|
40
|
+
|
|
41
|
+
def add(self, tool):
|
|
42
|
+
"""Add a tool. Raises ValueError if name conflicts with existing tool or instance."""
|
|
43
|
+
name = tool.name
|
|
44
|
+
if name in self._tools:
|
|
45
|
+
raise ValueError(f"Duplicate tool: '{name}'")
|
|
46
|
+
if name in self._instances:
|
|
47
|
+
raise ValueError(f"Tool name '{name}' conflicts with instance name")
|
|
48
|
+
self._tools[name] = tool
|
|
49
|
+
|
|
50
|
+
def add_instance(self, name: str, instance):
|
|
51
|
+
"""Add a class instance. Raises ValueError if name conflicts."""
|
|
52
|
+
if name in self._instances:
|
|
53
|
+
raise ValueError(f"Duplicate instance: '{name}'")
|
|
54
|
+
if name in self._tools:
|
|
55
|
+
raise ValueError(f"Instance name '{name}' conflicts with tool name")
|
|
56
|
+
self._instances[name] = instance
|
|
57
|
+
|
|
58
|
+
def get(self, name, default=None):
|
|
59
|
+
"""Get tool by name."""
|
|
60
|
+
return self._tools.get(name, default)
|
|
61
|
+
|
|
62
|
+
def get_instance(self, name, default=None):
|
|
63
|
+
"""
|
|
64
|
+
this will be called like agent.tools.get_instance('deep_research_agent').tools.get_instance('web')
|
|
65
|
+
Get instance by name."""
|
|
66
|
+
return self._instances.get(name, default)
|
|
67
|
+
|
|
68
|
+
def remove(self, name) -> bool:
|
|
69
|
+
"""Remove tool by name."""
|
|
70
|
+
if name in self._tools:
|
|
71
|
+
del self._tools[name]
|
|
72
|
+
return True
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def names(self):
|
|
76
|
+
"""List all tool names."""
|
|
77
|
+
return list(self._tools.keys())
|
|
78
|
+
|
|
79
|
+
def __getattr__(self, name):
|
|
80
|
+
"""Attribute access: agent.tools.send() or agent.tools.gmail.my_id"""
|
|
81
|
+
if name.startswith('_'):
|
|
82
|
+
raise AttributeError(name)
|
|
83
|
+
# Check instances first (gmail, calendar)
|
|
84
|
+
if name in self._instances:
|
|
85
|
+
return self._instances[name]
|
|
86
|
+
# Then check tools (send, reply)
|
|
87
|
+
tool = self._tools.get(name)
|
|
88
|
+
if tool is None:
|
|
89
|
+
raise AttributeError(f"No tool or instance: '{name}'")
|
|
90
|
+
return tool
|
|
91
|
+
|
|
92
|
+
def __iter__(self):
|
|
93
|
+
"""Iterate over tools only (for LLM schemas)."""
|
|
94
|
+
return iter(self._tools.values())
|
|
95
|
+
|
|
96
|
+
def __len__(self):
|
|
97
|
+
"""Count of tools (not instances)."""
|
|
98
|
+
return len(self._tools)
|
|
99
|
+
|
|
100
|
+
def __bool__(self):
|
|
101
|
+
return len(self._tools) > 0
|
|
102
|
+
|
|
103
|
+
def __contains__(self, name):
|
|
104
|
+
"""Check if tool or instance exists."""
|
|
105
|
+
return name in self._tools or name in self._instances
|