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.
Files changed (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. 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