camel-ai 0.2.71a12__py3-none-any.whl → 0.2.72__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.

Files changed (42) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +260 -488
  3. camel/memories/agent_memories.py +39 -0
  4. camel/memories/base.py +8 -0
  5. camel/models/gemini_model.py +30 -2
  6. camel/models/moonshot_model.py +36 -4
  7. camel/models/openai_model.py +29 -15
  8. camel/societies/workforce/prompts.py +24 -14
  9. camel/societies/workforce/single_agent_worker.py +9 -7
  10. camel/societies/workforce/workforce.py +44 -16
  11. camel/storages/vectordb_storages/__init__.py +1 -0
  12. camel/storages/vectordb_storages/surreal.py +415 -0
  13. camel/toolkits/__init__.py +10 -1
  14. camel/toolkits/base.py +57 -1
  15. camel/toolkits/human_toolkit.py +5 -1
  16. camel/toolkits/hybrid_browser_toolkit/config_loader.py +127 -414
  17. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +783 -1626
  18. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +489 -0
  19. camel/toolkits/markitdown_toolkit.py +2 -2
  20. camel/toolkits/message_integration.py +592 -0
  21. camel/toolkits/note_taking_toolkit.py +195 -26
  22. camel/toolkits/openai_image_toolkit.py +5 -5
  23. camel/toolkits/origene_mcp_toolkit.py +97 -0
  24. camel/toolkits/screenshot_toolkit.py +213 -0
  25. camel/toolkits/search_toolkit.py +115 -36
  26. camel/toolkits/terminal_toolkit.py +379 -165
  27. camel/toolkits/video_analysis_toolkit.py +13 -13
  28. camel/toolkits/video_download_toolkit.py +11 -11
  29. camel/toolkits/web_deploy_toolkit.py +1024 -0
  30. camel/types/enums.py +6 -3
  31. camel/types/unified_model_type.py +16 -4
  32. camel/utils/mcp_client.py +8 -0
  33. {camel_ai-0.2.71a12.dist-info → camel_ai-0.2.72.dist-info}/METADATA +6 -3
  34. {camel_ai-0.2.71a12.dist-info → camel_ai-0.2.72.dist-info}/RECORD +36 -36
  35. camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
  36. camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
  37. camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -739
  38. camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
  39. camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
  40. camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
  41. {camel_ai-0.2.71a12.dist-info → camel_ai-0.2.72.dist-info}/WHEEL +0 -0
  42. {camel_ai-0.2.71a12.dist-info → camel_ai-0.2.72.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