camel-ai 0.2.72a10__py3-none-any.whl → 0.2.73a1__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.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +113 -338
- camel/memories/agent_memories.py +18 -17
- camel/societies/workforce/prompts.py +10 -4
- camel/societies/workforce/single_agent_worker.py +7 -5
- camel/toolkits/__init__.py +6 -1
- camel/toolkits/base.py +57 -1
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +796 -1631
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +945 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +210 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +533 -0
- camel/toolkits/message_integration.py +592 -0
- camel/toolkits/notion_mcp_toolkit.py +234 -0
- camel/toolkits/screenshot_toolkit.py +116 -31
- camel/toolkits/search_toolkit.py +20 -2
- camel/toolkits/terminal_toolkit.py +16 -2
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/METADATA +12 -6
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/RECORD +31 -24
- camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
- camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -740
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
- camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
import inspect
|
|
15
|
+
from functools import wraps
|
|
16
|
+
from typing import Callable, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
from camel.logger import get_logger
|
|
19
|
+
from camel.toolkits import BaseToolkit, FunctionTool
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolkitMessageIntegration:
|
|
25
|
+
r"""Integrates user messaging capabilities into CAMEL toolkits and
|
|
26
|
+
functions.
|
|
27
|
+
|
|
28
|
+
This class allows agents to send status updates to users while executing
|
|
29
|
+
toolkit functions in a single step, improving communication and reducing
|
|
30
|
+
the number of tool calls needed.
|
|
31
|
+
|
|
32
|
+
Supports both built-in and custom message handlers with flexible parameter
|
|
33
|
+
names. Can update both toolkit methods and standalone functions.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> # Using default message handler with toolkit
|
|
37
|
+
>>> message_integration = ToolkitMessageIntegration()
|
|
38
|
+
>>> search_with_messaging = message_integration.
|
|
39
|
+
add_messaging_to_toolkit(
|
|
40
|
+
... SearchToolkit()
|
|
41
|
+
... )
|
|
42
|
+
|
|
43
|
+
>>> # Using with standalone functions
|
|
44
|
+
>>> def search_web(query: str) -> list:
|
|
45
|
+
... return ["result1", "result2"]
|
|
46
|
+
...
|
|
47
|
+
>>> enhanced_tools = message_integration.add_messaging_to_functions
|
|
48
|
+
([search_web])
|
|
49
|
+
|
|
50
|
+
>>> # Using custom message handler with different parameters
|
|
51
|
+
>>> def notify_user(severity: str, action: str, details: str = "") ->
|
|
52
|
+
str:
|
|
53
|
+
... '''Send notification to user.
|
|
54
|
+
...
|
|
55
|
+
... Args:
|
|
56
|
+
... severity: Notification level (info/warning/error)
|
|
57
|
+
... action: What action is being performed
|
|
58
|
+
... details: Additional details
|
|
59
|
+
... '''
|
|
60
|
+
... print(f"[{severity}] {action}: {details}")
|
|
61
|
+
... return "Notified"
|
|
62
|
+
...
|
|
63
|
+
>>> message_integration = ToolkitMessageIntegration(
|
|
64
|
+
... message_handler=notify_user,
|
|
65
|
+
... extract_params_callback=lambda kwargs: (
|
|
66
|
+
... kwargs.pop('severity', 'info'),
|
|
67
|
+
... kwargs.pop('action', 'executing'),
|
|
68
|
+
... kwargs.pop('details', '')
|
|
69
|
+
... )
|
|
70
|
+
... )
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
message_handler: Optional[Callable] = None,
|
|
76
|
+
extract_params_callback: Optional[Callable[[dict], tuple]] = None,
|
|
77
|
+
):
|
|
78
|
+
r"""Initialize the toolkit message integration.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
message_handler (Optional[Callable]): Custom message handler
|
|
82
|
+
function. If not provided, uses the built-in
|
|
83
|
+
send_message_to_user. (default: :obj:`None`)
|
|
84
|
+
extract_params_callback (Optional[Callable]): Function to extract
|
|
85
|
+
parameters from kwargs for the custom message handler. Should
|
|
86
|
+
return a tuple of arguments to pass to the message handler. If
|
|
87
|
+
not provided, uses default extraction for built-in handler.
|
|
88
|
+
(default: :obj:`None`)
|
|
89
|
+
"""
|
|
90
|
+
self.message_handler = message_handler or self.send_message_to_user
|
|
91
|
+
self.extract_params_callback = (
|
|
92
|
+
extract_params_callback or self._default_extract_params
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# If custom handler is provided, we'll use its signature and docstring
|
|
96
|
+
self.use_custom_handler = message_handler is not None
|
|
97
|
+
|
|
98
|
+
def _default_extract_params(self, kwargs: dict) -> tuple:
|
|
99
|
+
r"""Default parameter extraction for built-in message handler."""
|
|
100
|
+
return (
|
|
101
|
+
kwargs.pop('message_title', ''),
|
|
102
|
+
kwargs.pop('message_description', ''),
|
|
103
|
+
kwargs.pop('message_attachment', ''),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def send_message_to_user(
|
|
107
|
+
self,
|
|
108
|
+
message_title: str,
|
|
109
|
+
message_description: str,
|
|
110
|
+
message_attachment: str = "",
|
|
111
|
+
) -> str:
|
|
112
|
+
r"""Built-in message handler that sends tidy messages to the user.
|
|
113
|
+
|
|
114
|
+
This one-way tool keeps the user informed about agent progress,
|
|
115
|
+
decisions, or actions. It does not require a response.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
message_title (str): The title of the message.
|
|
119
|
+
message_description (str): The short description message.
|
|
120
|
+
message_attachment (str): The additional attachment of the message,
|
|
121
|
+
which can be a file path or a URL.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
str: Confirmation that the message was successfully sent.
|
|
125
|
+
"""
|
|
126
|
+
print(f"\nAgent Message:\n{message_title}\n{message_description}\n")
|
|
127
|
+
if message_attachment:
|
|
128
|
+
print(message_attachment)
|
|
129
|
+
|
|
130
|
+
logger.info(
|
|
131
|
+
f"\nAgent Message:\n{message_title} "
|
|
132
|
+
f"{message_description} {message_attachment}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
f"Message successfully sent to user: '{message_title} "
|
|
137
|
+
f"{message_description} {message_attachment}'"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def get_message_tool(self) -> FunctionTool:
|
|
141
|
+
r"""Get the send_message_to_user as a standalone FunctionTool.
|
|
142
|
+
|
|
143
|
+
This can be used when you want to provide the messaging capability
|
|
144
|
+
as a separate tool rather than integrating it into other tools.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
FunctionTool: The message sending tool.
|
|
148
|
+
"""
|
|
149
|
+
return FunctionTool(self.send_message_to_user)
|
|
150
|
+
|
|
151
|
+
def add_messaging_to_toolkit(
|
|
152
|
+
self, toolkit: BaseToolkit, tool_names: Optional[List[str]] = None
|
|
153
|
+
) -> BaseToolkit:
|
|
154
|
+
r"""Add messaging capabilities to toolkit methods.
|
|
155
|
+
|
|
156
|
+
This method modifies a toolkit so that specified tools can send
|
|
157
|
+
status messages to users while executing their primary function.
|
|
158
|
+
The tools will accept optional messaging parameters:
|
|
159
|
+
- message_title: Title of the status message
|
|
160
|
+
- message_description: Description of what the tool is doing
|
|
161
|
+
- message_attachment: Optional file path or URL
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
toolkit: The toolkit to add messaging capabilities to
|
|
165
|
+
tool_names: List of specific tool names to modify.
|
|
166
|
+
If None, messaging is added to all tools.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The toolkit with messaging capabilities added
|
|
170
|
+
"""
|
|
171
|
+
original_get_tools = toolkit.get_tools
|
|
172
|
+
|
|
173
|
+
def enhanced_get_tools() -> List[FunctionTool]:
|
|
174
|
+
tools = original_get_tools()
|
|
175
|
+
enhanced_tools = []
|
|
176
|
+
|
|
177
|
+
for tool in tools:
|
|
178
|
+
if tool_names is None or tool.func.__name__ in tool_names:
|
|
179
|
+
enhanced_func = self._add_messaging_to_tool(tool.func)
|
|
180
|
+
enhanced_tools.append(FunctionTool(enhanced_func))
|
|
181
|
+
else:
|
|
182
|
+
enhanced_tools.append(tool)
|
|
183
|
+
|
|
184
|
+
return enhanced_tools
|
|
185
|
+
|
|
186
|
+
# Replace the get_tools method
|
|
187
|
+
toolkit.get_tools = enhanced_get_tools # type: ignore[method-assign]
|
|
188
|
+
return toolkit
|
|
189
|
+
|
|
190
|
+
def add_messaging_to_functions(
|
|
191
|
+
self,
|
|
192
|
+
functions: Union[List[FunctionTool], List[Callable]],
|
|
193
|
+
function_names: Optional[List[str]] = None,
|
|
194
|
+
) -> List[FunctionTool]:
|
|
195
|
+
r"""Add messaging capabilities to a list of functions or FunctionTools.
|
|
196
|
+
|
|
197
|
+
This method enhances functions so they can send status messages to
|
|
198
|
+
users while executing. The enhanced functions will accept optional
|
|
199
|
+
messaging parameters that trigger status updates.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
functions (Union[List[FunctionTool], List[Callable]]): List of
|
|
203
|
+
FunctionTool objects or callable functions to enhance.
|
|
204
|
+
function_names (Optional[List[str]]): List of specific function
|
|
205
|
+
names to modify. If None, messaging is added to all functions.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
List[FunctionTool]: List of enhanced FunctionTool objects
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> # With FunctionTools
|
|
212
|
+
>>> tools = [FunctionTool(search_func), FunctionTool(analyze_func)]
|
|
213
|
+
>>> enhanced_tools = message_integration.add_messaging_to_functions
|
|
214
|
+
(tools)
|
|
215
|
+
|
|
216
|
+
>>> # With callable functions
|
|
217
|
+
>>> funcs = [search_web, analyze_data, generate_report]
|
|
218
|
+
>>> enhanced_tools = message_integration.add_messaging_to_functions
|
|
219
|
+
(
|
|
220
|
+
... funcs,
|
|
221
|
+
... function_names=['search_web', 'analyze_data']
|
|
222
|
+
... )
|
|
223
|
+
"""
|
|
224
|
+
enhanced_tools = []
|
|
225
|
+
|
|
226
|
+
for item in functions:
|
|
227
|
+
# Extract the function based on input type
|
|
228
|
+
if isinstance(item, FunctionTool):
|
|
229
|
+
func = item.func
|
|
230
|
+
elif callable(item):
|
|
231
|
+
func = item
|
|
232
|
+
else:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Invalid item type: {type(item)}. Expected "
|
|
235
|
+
f"FunctionTool or callable."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Check if this function should be enhanced
|
|
239
|
+
if function_names is None or func.__name__ in function_names:
|
|
240
|
+
enhanced_func = self._add_messaging_to_tool(func)
|
|
241
|
+
enhanced_tools.append(FunctionTool(enhanced_func))
|
|
242
|
+
else:
|
|
243
|
+
# Return as FunctionTool regardless of input type
|
|
244
|
+
if isinstance(item, FunctionTool):
|
|
245
|
+
enhanced_tools.append(item)
|
|
246
|
+
else:
|
|
247
|
+
enhanced_tools.append(FunctionTool(func))
|
|
248
|
+
|
|
249
|
+
return enhanced_tools
|
|
250
|
+
|
|
251
|
+
def _add_messaging_to_tool(self, func: Callable) -> Callable:
|
|
252
|
+
r"""Add messaging parameters to a tool function.
|
|
253
|
+
|
|
254
|
+
This internal method modifies the function signature and docstring
|
|
255
|
+
to include optional messaging parameters that trigger status updates.
|
|
256
|
+
"""
|
|
257
|
+
# Get the original signature
|
|
258
|
+
original_sig = inspect.signature(func)
|
|
259
|
+
|
|
260
|
+
# Create new parameters for the enhanced function
|
|
261
|
+
new_params = list(original_sig.parameters.values())
|
|
262
|
+
|
|
263
|
+
# Determine which parameters to add based on handler type
|
|
264
|
+
if self.use_custom_handler:
|
|
265
|
+
# Use the custom handler's signature
|
|
266
|
+
handler_sig = inspect.signature(self.message_handler)
|
|
267
|
+
message_params = []
|
|
268
|
+
|
|
269
|
+
# Add parameters from the custom handler (excluding self if it's a
|
|
270
|
+
# method)
|
|
271
|
+
for param_name, param in handler_sig.parameters.items():
|
|
272
|
+
if param_name != 'self':
|
|
273
|
+
# Create a keyword-only parameter with the same annotation
|
|
274
|
+
# and default
|
|
275
|
+
new_param = inspect.Parameter(
|
|
276
|
+
param_name,
|
|
277
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
278
|
+
default=param.default
|
|
279
|
+
if param.default != inspect.Parameter.empty
|
|
280
|
+
else None,
|
|
281
|
+
annotation=param.annotation
|
|
282
|
+
if param.annotation != inspect.Parameter.empty
|
|
283
|
+
else inspect.Parameter.empty,
|
|
284
|
+
)
|
|
285
|
+
message_params.append(new_param)
|
|
286
|
+
else:
|
|
287
|
+
# Use default parameters for built-in handler
|
|
288
|
+
message_params = [
|
|
289
|
+
inspect.Parameter(
|
|
290
|
+
'message_title',
|
|
291
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
292
|
+
default="",
|
|
293
|
+
annotation=str,
|
|
294
|
+
),
|
|
295
|
+
inspect.Parameter(
|
|
296
|
+
'message_description',
|
|
297
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
298
|
+
default="",
|
|
299
|
+
annotation=str,
|
|
300
|
+
),
|
|
301
|
+
inspect.Parameter(
|
|
302
|
+
'message_attachment',
|
|
303
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
304
|
+
default="",
|
|
305
|
+
annotation=str,
|
|
306
|
+
),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
# Find where to insert the new parameters (before **kwargs if it
|
|
310
|
+
# exists)
|
|
311
|
+
insert_index = len(new_params)
|
|
312
|
+
for i, param in enumerate(new_params):
|
|
313
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
314
|
+
insert_index = i
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
# Insert the message parameters
|
|
318
|
+
for param in reversed(message_params):
|
|
319
|
+
new_params.insert(insert_index, param)
|
|
320
|
+
|
|
321
|
+
# Create the new signature
|
|
322
|
+
new_sig = original_sig.replace(parameters=new_params)
|
|
323
|
+
|
|
324
|
+
@wraps(func)
|
|
325
|
+
def wrapper(*args, **kwargs):
|
|
326
|
+
# Extract parameters using the callback
|
|
327
|
+
try:
|
|
328
|
+
params = self.extract_params_callback(kwargs)
|
|
329
|
+
except KeyError:
|
|
330
|
+
# If parameters are missing, just execute the original function
|
|
331
|
+
return func(*args, **kwargs)
|
|
332
|
+
|
|
333
|
+
# Check if we should send a message
|
|
334
|
+
should_send = False
|
|
335
|
+
if self.use_custom_handler:
|
|
336
|
+
should_send = any(p is not None and p != '' for p in params)
|
|
337
|
+
else:
|
|
338
|
+
# For default handler, params = (title, description,
|
|
339
|
+
# attachment)
|
|
340
|
+
should_send = bool(params[0]) or bool(params[1])
|
|
341
|
+
|
|
342
|
+
# Send message if needed
|
|
343
|
+
if should_send:
|
|
344
|
+
if self.use_custom_handler:
|
|
345
|
+
self.message_handler(*params)
|
|
346
|
+
else:
|
|
347
|
+
# For built-in handler, provide defaults
|
|
348
|
+
title, desc, attach = params
|
|
349
|
+
self.message_handler(
|
|
350
|
+
title or "Executing Tool",
|
|
351
|
+
desc or f"Running {func.__name__}",
|
|
352
|
+
attach or '',
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Execute the original function
|
|
356
|
+
result = func(*args, **kwargs)
|
|
357
|
+
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
# Apply the new signature to the wrapper
|
|
361
|
+
wrapper.__signature__ = new_sig # type: ignore[attr-defined]
|
|
362
|
+
|
|
363
|
+
# Enhance the docstring
|
|
364
|
+
if func.__doc__:
|
|
365
|
+
enhanced_doc = func.__doc__.rstrip()
|
|
366
|
+
lines = enhanced_doc.split('\n')
|
|
367
|
+
|
|
368
|
+
# Find where to insert parameters
|
|
369
|
+
insert_idx = self._find_docstring_insert_point(lines)
|
|
370
|
+
|
|
371
|
+
# Check if we need to create an Args section
|
|
372
|
+
has_args_section = any(
|
|
373
|
+
'Args:' in line
|
|
374
|
+
or 'Arguments:' in line
|
|
375
|
+
or 'Parameters:' in line
|
|
376
|
+
for line in lines
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if not has_args_section and insert_idx is not None:
|
|
380
|
+
# Need to create Args section
|
|
381
|
+
base_indent = self._get_base_indent(lines)
|
|
382
|
+
|
|
383
|
+
# Add blank line before Args section if needed
|
|
384
|
+
if insert_idx > 0 and lines[insert_idx - 1].strip():
|
|
385
|
+
lines.insert(insert_idx, "")
|
|
386
|
+
insert_idx += 1
|
|
387
|
+
|
|
388
|
+
# Add Args section header
|
|
389
|
+
lines.insert(insert_idx, f"{base_indent}Args:")
|
|
390
|
+
insert_idx += 1
|
|
391
|
+
|
|
392
|
+
# Get parameter documentation
|
|
393
|
+
if self.use_custom_handler and self.message_handler.__doc__:
|
|
394
|
+
# Extract parameter docs from custom handler's docstring
|
|
395
|
+
param_docs = self._extract_param_docs_from_handler()
|
|
396
|
+
else:
|
|
397
|
+
# Use default parameter docs
|
|
398
|
+
param_docs = [
|
|
399
|
+
"message_title (str, optional): Title for status "
|
|
400
|
+
"message to user.",
|
|
401
|
+
"message_description (str, optional): Description for "
|
|
402
|
+
"status message.",
|
|
403
|
+
"message_attachment (str, optional): File path or URL to "
|
|
404
|
+
"attach to message.",
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
# Insert the parameter documentation
|
|
408
|
+
if insert_idx is not None and param_docs:
|
|
409
|
+
# Get proper indentation for parameters
|
|
410
|
+
indent = self._get_docstring_indent(lines, insert_idx)
|
|
411
|
+
|
|
412
|
+
# Insert each parameter doc with proper indentation
|
|
413
|
+
for doc in param_docs:
|
|
414
|
+
lines.insert(insert_idx, f"{indent}{doc}")
|
|
415
|
+
insert_idx += 1
|
|
416
|
+
|
|
417
|
+
wrapper.__doc__ = '\n'.join(lines)
|
|
418
|
+
|
|
419
|
+
return wrapper
|
|
420
|
+
|
|
421
|
+
def _find_docstring_insert_point(self, lines: List[str]) -> Optional[int]:
|
|
422
|
+
r"""Find where to insert parameters in a docstring."""
|
|
423
|
+
current_indent = ""
|
|
424
|
+
|
|
425
|
+
# First, look for existing Args section
|
|
426
|
+
for i, line in enumerate(lines):
|
|
427
|
+
if (
|
|
428
|
+
'Args:' in line
|
|
429
|
+
or 'Arguments:' in line
|
|
430
|
+
or 'Parameters:' in line
|
|
431
|
+
):
|
|
432
|
+
current_indent = line[: len(line) - len(line.lstrip())]
|
|
433
|
+
# Find where Args section ends by looking for the last
|
|
434
|
+
# parameter
|
|
435
|
+
last_param_idx = None
|
|
436
|
+
for j in range(i + 1, len(lines)):
|
|
437
|
+
stripped = lines[j].strip()
|
|
438
|
+
# If it's an empty line, skip it
|
|
439
|
+
if not stripped:
|
|
440
|
+
continue
|
|
441
|
+
# If it's a parameter line (has proper indentation and
|
|
442
|
+
# content)
|
|
443
|
+
if lines[j].startswith(current_indent + ' '):
|
|
444
|
+
last_param_idx = j
|
|
445
|
+
else:
|
|
446
|
+
# Hit a line with different indentation or a new
|
|
447
|
+
# section
|
|
448
|
+
if last_param_idx is not None:
|
|
449
|
+
return last_param_idx + 1
|
|
450
|
+
else:
|
|
451
|
+
# No parameters found, insert right after Args:
|
|
452
|
+
return i + 1
|
|
453
|
+
# Args is the last section, return after last parameter
|
|
454
|
+
if last_param_idx is not None:
|
|
455
|
+
return last_param_idx + 1
|
|
456
|
+
else:
|
|
457
|
+
# No parameters found, insert right after Args:
|
|
458
|
+
return i + 1
|
|
459
|
+
|
|
460
|
+
# No Args section, need to create one
|
|
461
|
+
# Try to insert before Returns/Yields/Raises/Examples sections
|
|
462
|
+
for i, line in enumerate(lines):
|
|
463
|
+
stripped = line.strip()
|
|
464
|
+
if any(
|
|
465
|
+
section in line
|
|
466
|
+
for section in [
|
|
467
|
+
'Returns:',
|
|
468
|
+
'Return:',
|
|
469
|
+
'Yields:',
|
|
470
|
+
'Raises:',
|
|
471
|
+
'Examples:',
|
|
472
|
+
'Example:',
|
|
473
|
+
'Note:',
|
|
474
|
+
'Notes:',
|
|
475
|
+
]
|
|
476
|
+
):
|
|
477
|
+
return i
|
|
478
|
+
|
|
479
|
+
# No special sections, add at the end
|
|
480
|
+
return len(lines)
|
|
481
|
+
|
|
482
|
+
def _get_docstring_indent(self, lines: List[str], insert_idx: int) -> str:
|
|
483
|
+
r"""Get the proper indentation for docstring parameters."""
|
|
484
|
+
# Look for Args: or similar section to match indentation
|
|
485
|
+
for i, line in enumerate(lines):
|
|
486
|
+
if (
|
|
487
|
+
'Args:' in line
|
|
488
|
+
or 'Arguments:' in line
|
|
489
|
+
or 'Parameters:' in line
|
|
490
|
+
):
|
|
491
|
+
base_indent = line[: len(line) - len(line.lstrip())]
|
|
492
|
+
# Look at the next line to see parameter indentation
|
|
493
|
+
if i + 1 < len(lines):
|
|
494
|
+
if lines[i + 1].strip():
|
|
495
|
+
next_indent = lines[i + 1][
|
|
496
|
+
: len(lines[i + 1]) - len(lines[i + 1].lstrip())
|
|
497
|
+
]
|
|
498
|
+
if len(next_indent) > len(base_indent):
|
|
499
|
+
return next_indent
|
|
500
|
+
return base_indent + ' '
|
|
501
|
+
|
|
502
|
+
# No Args section, use base indent + 4 spaces
|
|
503
|
+
base_indent = self._get_base_indent(lines)
|
|
504
|
+
return base_indent + ' '
|
|
505
|
+
|
|
506
|
+
def _get_base_indent(self, lines: List[str]) -> str:
|
|
507
|
+
r"""Get the base indentation level of the docstring."""
|
|
508
|
+
# Find first non-empty line to determine base indentation
|
|
509
|
+
for line in lines:
|
|
510
|
+
if line.strip() and not line.strip().startswith('"""'):
|
|
511
|
+
return line[: len(line) - len(line.lstrip())]
|
|
512
|
+
return ' ' # Default indentation
|
|
513
|
+
|
|
514
|
+
def _extract_param_docs_from_handler(self) -> List[str]:
|
|
515
|
+
r"""Extract parameter documentation from the custom handler's
|
|
516
|
+
docstring.
|
|
517
|
+
"""
|
|
518
|
+
if not self.message_handler.__doc__:
|
|
519
|
+
return []
|
|
520
|
+
|
|
521
|
+
docs = []
|
|
522
|
+
handler_sig = inspect.signature(self.message_handler)
|
|
523
|
+
|
|
524
|
+
# Parse the handler's docstring to find parameter descriptions
|
|
525
|
+
lines = self.message_handler.__doc__.split('\n')
|
|
526
|
+
in_args = False
|
|
527
|
+
param_docs = {}
|
|
528
|
+
|
|
529
|
+
for line in lines:
|
|
530
|
+
if (
|
|
531
|
+
'Args:' in line
|
|
532
|
+
or 'Arguments:' in line
|
|
533
|
+
or 'Parameters:' in line
|
|
534
|
+
):
|
|
535
|
+
in_args = True
|
|
536
|
+
continue
|
|
537
|
+
elif in_args and line.strip():
|
|
538
|
+
# Check if we've reached a new section
|
|
539
|
+
stripped_line = line.lstrip()
|
|
540
|
+
if any(
|
|
541
|
+
section in stripped_line
|
|
542
|
+
for section in [
|
|
543
|
+
'Returns:',
|
|
544
|
+
'Return:',
|
|
545
|
+
'Yields:',
|
|
546
|
+
'Raises:',
|
|
547
|
+
'Examples:',
|
|
548
|
+
'Example:',
|
|
549
|
+
'Note:',
|
|
550
|
+
'Notes:',
|
|
551
|
+
]
|
|
552
|
+
):
|
|
553
|
+
# End of Args section
|
|
554
|
+
break
|
|
555
|
+
elif in_args and ':' in line:
|
|
556
|
+
# Parse parameter documentation
|
|
557
|
+
parts = line.strip().split(':', 1)
|
|
558
|
+
if len(parts) == 2:
|
|
559
|
+
param_name = parts[0].strip()
|
|
560
|
+
param_desc = parts[1].strip()
|
|
561
|
+
# Extract just the parameter name (before any type
|
|
562
|
+
# annotation)
|
|
563
|
+
param_name = param_name.split()[0].strip()
|
|
564
|
+
param_docs[param_name] = param_desc
|
|
565
|
+
|
|
566
|
+
# Build documentation for each parameter
|
|
567
|
+
for param_name, param in handler_sig.parameters.items():
|
|
568
|
+
if param_name != 'self':
|
|
569
|
+
# Get type annotation
|
|
570
|
+
annotation = ''
|
|
571
|
+
if param.annotation != inspect.Parameter.empty:
|
|
572
|
+
if hasattr(param.annotation, '__name__'):
|
|
573
|
+
annotation = f" ({param.annotation.__name__})"
|
|
574
|
+
else:
|
|
575
|
+
annotation = f" ({param.annotation!s})"
|
|
576
|
+
|
|
577
|
+
# Check if optional
|
|
578
|
+
optional = (
|
|
579
|
+
', optional'
|
|
580
|
+
if param.default != inspect.Parameter.empty
|
|
581
|
+
else ''
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Get description from parsed docs
|
|
585
|
+
desc = param_docs.get(
|
|
586
|
+
param_name,
|
|
587
|
+
f"Parameter for {self.message_handler.__name__}",
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
docs.append(f"{param_name}{annotation}{optional}: {desc}")
|
|
591
|
+
|
|
592
|
+
return docs
|