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
@@ -88,6 +88,45 @@ class ChatHistoryMemory(AgentMemory):
88
88
  def clear(self) -> None:
89
89
  self._chat_history_block.clear()
90
90
 
91
+ def clean_tool_calls(self) -> None:
92
+ r"""Removes tool call messages from memory.
93
+ This method removes all FUNCTION/TOOL role messages and any ASSISTANT
94
+ messages that contain tool_calls in their meta_dict to save token
95
+ usage.
96
+ """
97
+ from camel.types import OpenAIBackendRole
98
+
99
+ # Get all messages from storage
100
+ record_dicts = self._chat_history_block.storage.load()
101
+ if not record_dicts:
102
+ return
103
+
104
+ # Track indices to remove (reverse order for efficient deletion)
105
+ indices_to_remove = []
106
+
107
+ # Identify indices of tool-related messages
108
+ for i, record in enumerate(record_dicts):
109
+ role = record.get('role_at_backend')
110
+
111
+ # Mark FUNCTION messages for removal
112
+ if role == OpenAIBackendRole.FUNCTION.value:
113
+ indices_to_remove.append(i)
114
+ # Mark TOOL messages for removal
115
+ elif role == OpenAIBackendRole.TOOL.value:
116
+ indices_to_remove.append(i)
117
+ # Mark ASSISTANT messages with tool_calls for removal
118
+ elif role == OpenAIBackendRole.ASSISTANT.value:
119
+ meta_dict = record.get('meta_dict', {})
120
+ if meta_dict and 'tool_calls' in meta_dict:
121
+ indices_to_remove.append(i)
122
+
123
+ # Remove records in-place
124
+ for i in reversed(indices_to_remove):
125
+ del record_dicts[i]
126
+
127
+ # Save the modified records back to storage
128
+ self._chat_history_block.storage.save(record_dicts)
129
+
91
130
 
92
131
  class VectorDBMemory(AgentMemory):
93
132
  r"""An agent memory wrapper of :obj:`VectorDBBlock`. This memory queries
camel/memories/base.py CHANGED
@@ -149,6 +149,14 @@ class AgentMemory(MemoryBlock, ABC):
149
149
  """
150
150
  return self.get_context_creator().create_context(self.retrieve())
151
151
 
152
+ def clean_tool_calls(self) -> None:
153
+ r"""Removes tool call messages from memory.
154
+ This is an optional method that can be overridden by subclasses
155
+ to implement cleaning of tool-related messages. By default, it
156
+ does nothing, maintaining backward compatibility.
157
+ """
158
+ pass
159
+
152
160
  def __repr__(self) -> str:
153
161
  r"""Returns a string representation of the AgentMemory.
154
162
 
@@ -243,7 +243,7 @@ class GeminiModel(OpenAICompatibleModel):
243
243
  function_dict = tool.get('function', {})
244
244
  function_dict.pop("strict", None)
245
245
 
246
- # Process parameters to remove anyOf
246
+ # Process parameters to remove anyOf and handle enum/format
247
247
  if 'parameters' in function_dict:
248
248
  params = function_dict['parameters']
249
249
  if 'properties' in params:
@@ -260,6 +260,20 @@ class GeminiModel(OpenAICompatibleModel):
260
260
  'description'
261
261
  ] = prop_value['description']
262
262
 
263
+ # Handle enum and format restrictions for Gemini
264
+ # API enum: only allowed for string type
265
+ if prop_value.get('type') != 'string':
266
+ prop_value.pop('enum', None)
267
+
268
+ # format: only allowed for string, integer, and
269
+ # number types
270
+ if prop_value.get('type') not in [
271
+ 'string',
272
+ 'integer',
273
+ 'number',
274
+ ]:
275
+ prop_value.pop('format', None)
276
+
263
277
  request_config["tools"] = tools
264
278
 
265
279
  return self._client.chat.completions.create(
@@ -283,7 +297,7 @@ class GeminiModel(OpenAICompatibleModel):
283
297
  function_dict = tool.get('function', {})
284
298
  function_dict.pop("strict", None)
285
299
 
286
- # Process parameters to remove anyOf
300
+ # Process parameters to remove anyOf and handle enum/format
287
301
  if 'parameters' in function_dict:
288
302
  params = function_dict['parameters']
289
303
  if 'properties' in params:
@@ -300,6 +314,20 @@ class GeminiModel(OpenAICompatibleModel):
300
314
  'description'
301
315
  ] = prop_value['description']
302
316
 
317
+ # Handle enum and format restrictions for Gemini
318
+ # API enum: only allowed for string type
319
+ if prop_value.get('type') != 'string':
320
+ prop_value.pop('enum', None)
321
+
322
+ # format: only allowed for string, integer, and
323
+ # number types
324
+ if prop_value.get('type') not in [
325
+ 'string',
326
+ 'integer',
327
+ 'number',
328
+ ]:
329
+ prop_value.pop('format', None)
330
+
303
331
  request_config["tools"] = tools
304
332
 
305
333
  return await self._async_client.chat.completions.create(
@@ -20,6 +20,7 @@ from pydantic import BaseModel
20
20
 
21
21
  from camel.configs import MOONSHOT_API_PARAMS, MoonshotConfig
22
22
  from camel.messages import OpenAIMessage
23
+ from camel.models._utils import try_modify_message_with_format
23
24
  from camel.models.openai_compatible_model import OpenAICompatibleModel
24
25
  from camel.types import (
25
26
  ChatCompletion,
@@ -106,6 +107,38 @@ class MoonshotModel(OpenAICompatibleModel):
106
107
  **kwargs,
107
108
  )
108
109
 
110
+ def _prepare_request(
111
+ self,
112
+ messages: List[OpenAIMessage],
113
+ response_format: Optional[Type[BaseModel]] = None,
114
+ tools: Optional[List[Dict[str, Any]]] = None,
115
+ ) -> Dict[str, Any]:
116
+ r"""Prepare the request configuration for Moonshot API.
117
+
118
+ Args:
119
+ messages (List[OpenAIMessage]): Message list with the chat history
120
+ in OpenAI API format.
121
+ response_format (Optional[Type[BaseModel]]): The format of the
122
+ response.
123
+ tools (Optional[List[Dict[str, Any]]]): The schema of the tools to
124
+ use for the request.
125
+
126
+ Returns:
127
+ Dict[str, Any]: The prepared request configuration.
128
+ """
129
+ import copy
130
+
131
+ request_config = copy.deepcopy(self.model_config_dict)
132
+
133
+ if tools:
134
+ request_config["tools"] = tools
135
+ elif response_format:
136
+ # Use the same approach as DeepSeek for structured output
137
+ try_modify_message_with_format(messages[-1], response_format)
138
+ request_config["response_format"] = {"type": "json_object"}
139
+
140
+ return request_config
141
+
109
142
  @observe()
110
143
  async def _arun(
111
144
  self,
@@ -141,10 +174,9 @@ class MoonshotModel(OpenAICompatibleModel):
141
174
  tags=["CAMEL-AI", str(self.model_type)],
142
175
  )
143
176
 
144
- request_config = self.model_config_dict.copy()
145
-
146
- if tools:
147
- request_config["tools"] = tools
177
+ request_config = self._prepare_request(
178
+ messages, response_format, tools
179
+ )
148
180
 
149
181
  return await self._async_client.chat.completions.create(
150
182
  messages=messages,
@@ -23,6 +23,7 @@ from openai.lib.streaming.chat import (
23
23
  from pydantic import BaseModel
24
24
 
25
25
  from camel.configs import OPENAI_API_PARAMS, ChatGPTConfig
26
+ from camel.logger import get_logger
26
27
  from camel.messages import OpenAIMessage
27
28
  from camel.models import BaseModelBackend
28
29
  from camel.types import (
@@ -39,6 +40,8 @@ from camel.utils import (
39
40
  update_langfuse_trace,
40
41
  )
41
42
 
43
+ logger = get_logger(__name__)
44
+
42
45
  if os.environ.get("LANGFUSE_ENABLED", "False").lower() == "true":
43
46
  try:
44
47
  from langfuse.decorators import observe
@@ -273,17 +276,23 @@ class OpenAIModel(BaseModelBackend):
273
276
 
274
277
  # Update Langfuse trace with current agent session and metadata
275
278
  agent_session_id = get_current_agent_session_id()
279
+ model_type_str = str(self.model_type)
280
+ if not agent_session_id:
281
+ agent_session_id = "no-session-id"
282
+ metadata = {
283
+ "source": "camel",
284
+ "agent_id": agent_session_id,
285
+ "agent_type": "camel_chat_agent",
286
+ "model_type": model_type_str,
287
+ }
288
+ metadata = {k: str(v) for k, v in metadata.items()}
276
289
  if agent_session_id:
277
290
  update_langfuse_trace(
278
291
  session_id=agent_session_id,
279
- metadata={
280
- "source": "camel",
281
- "agent_id": agent_session_id,
282
- "agent_type": "camel_chat_agent",
283
- "model_type": str(self.model_type),
284
- },
285
- tags=["CAMEL-AI", str(self.model_type)],
292
+ metadata=metadata,
293
+ tags=["CAMEL-AI", model_type_str],
286
294
  )
295
+ logger.info(f"metadata: {metadata}")
287
296
 
288
297
  messages = self._adapt_messages_for_o1_models(messages)
289
298
  response_format = response_format or self.model_config_dict.get(
@@ -342,17 +351,22 @@ class OpenAIModel(BaseModelBackend):
342
351
 
343
352
  # Update Langfuse trace with current agent session and metadata
344
353
  agent_session_id = get_current_agent_session_id()
345
- if agent_session_id:
354
+ model_type_str = str(self.model_type)
355
+ if not agent_session_id:
356
+ agent_session_id = "no-session-id"
357
+ metadata = {
358
+ "source": "camel",
359
+ "agent_id": agent_session_id,
360
+ "agent_type": "camel_chat_agent",
361
+ "model_type": model_type_str,
362
+ }
363
+ metadata = {k: str(v) for k, v in metadata.items()}
346
364
  update_langfuse_trace(
347
365
  session_id=agent_session_id,
348
- metadata={
349
- "source": "camel",
350
- "agent_id": agent_session_id,
351
- "agent_type": "camel_chat_agent",
352
- "model_type": str(self.model_type),
353
- },
354
- tags=["CAMEL-AI", str(self.model_type)],
366
+ metadata=metadata,
367
+ tags=["CAMEL-AI", model_type_str],
355
368
  )
369
+ logger.info(f"metadata: {metadata}")
356
370
 
357
371
  messages = self._adapt_messages_for_o1_models(messages)
358
372
  response_format = response_format or self.model_config_dict.get(
@@ -202,16 +202,21 @@ TASK_DECOMPOSE_PROMPT = r"""You need to decompose the given task into subtasks a
202
202
  * **DO NOT** use relative references like "the first task," "the paper mentioned above," or "the result from the previous step."
203
203
  * **DO** write explicit instructions. For example, instead of "Analyze the document," write "Analyze the document titled 'The Future of AI'." The system will automatically provide the necessary inputs (like the document itself) from previous steps.
204
204
 
205
- 1. **Strategic Grouping for Sequential Work**:
206
- * If a series of steps must be done in order *and* can be handled by the same worker type, group them into a single subtask to maintain flow and minimize handoffs.
205
+ 2. **Define Clear Deliverables**: Each subtask must specify a clear, concrete deliverable. This tells the agent exactly what to produce and provides a clear "definition of done."
206
+ * **DO NOT** use vague verbs like "analyze," "look into," or "research" without defining the output.
207
+ * **DO** specify the format and content of the output. For example, instead of "Analyze the attached report," write "Summarize the key findings of the attached report in a 3-bullet-point list." Instead of "Find contacts," write "Extract all names and email addresses from the document and return them as a JSON list of objects, where each object has a 'name' and 'email' key."
207
208
 
208
- 2. **Aggressive Parallelization**:
209
+ 3. **Full Workflow Completion & Strategic Grouping**:
210
+ * **Preserve the Entire Goal**: Ensure the decomposed subtasks collectively achieve the *entire* original task. Do not drop or ignore final steps like sending a message, submitting a form, or creating a file.
211
+ * **Group Sequential Actions**: If a series of steps must be done in order *and* can be handled by the same worker type (e.g., read, think, reply), group them into a single, comprehensive subtask. This maintains workflow and ensures the final goal is met.
212
+
213
+ 4. **Aggressive Parallelization**:
209
214
  * **Across Different Worker Specializations**: If distinct phases of the overall task require different types of workers (e.g., research by a 'SearchAgent', then content creation by a 'DocumentAgent'), define these as separate subtasks.
210
215
  * **Within a Single Phase (Data/Task Parallelism)**: If a phase involves repetitive operations on multiple items (e.g., processing 10 documents, fetching 5 web pages, analyzing 3 datasets):
211
216
  * Decompose this into parallel subtasks, one for each item or a small batch of items.
212
217
  * This applies even if the same type of worker handles these parallel subtasks. The goal is to leverage multiple available workers or allow concurrent processing.
213
218
 
214
- 3. **Subtask Design for Efficiency**:
219
+ 5. **Subtask Design for Efficiency**:
215
220
  * **Actionable and Well-Defined**: Each subtask should have a clear, achievable goal.
216
221
  * **Balanced Granularity**: Make subtasks large enough to be meaningful but small enough to enable parallelism and quick feedback. Avoid overly large subtasks that hide parallel opportunities.
217
222
  * **Consider Dependencies**: While you list tasks sequentially, think about the true dependencies. The workforce manager will handle execution based on these implied dependencies and worker availability.
@@ -229,10 +234,10 @@ These principles aim to reduce overall completion time by maximizing concurrent
229
234
  * **Correct Decomposition**:
230
235
  ```xml
231
236
  <tasks>
232
- <task>Create a short blog post about the benefits of Python by researching key benefits, writing a 300-word article, and finding a suitable image.</task>
237
+ <task>Create a short blog post about the benefits of Python by researching key benefits, writing a 300-word article, and finding a suitable image. The final output should be a single string containing the 300-word article followed by the image URL.</task>
233
238
  </tasks>
234
239
  ```
235
- * **Reasoning**: All steps are sequential and can be handled by the same worker type (`Document Agent`). Grouping them into one subtask is efficient and maintains the workflow, following the "Strategic Grouping" principle.
240
+ * **Reasoning**: All steps are sequential and can be handled by the same worker type (`Document Agent`). Grouping them into one subtask is efficient and maintains the workflow, following the "Strategic Grouping" principle. **The deliverable is clearly defined as a single string.**
236
241
 
237
242
  ***
238
243
  **Example 2: Parallel Task Across Different Workers**
@@ -245,14 +250,14 @@ These principles aim to reduce overall completion time by maximizing concurrent
245
250
  * **Correct Decomposition**:
246
251
  ```xml
247
252
  <tasks>
248
- <task>Create a financial summary for Apple (AAPL) for Q2.</task>
249
- <task>Create a financial summary for Google (GOOGL) for Q2.</task>
250
- <task>Perform market sentiment analysis for Apple (AAPL) for Q2.</task>
251
- <task>Perform market sentiment analysis for Google (GOOGL) for Q2.</task>
252
- <task>Compile the provided financial summaries and market sentiment analyses for Apple (AAPL) and Google (GOOGL) into a single Q2 performance report.</task>
253
+ <task>Create a 1-paragraph financial summary for Apple (AAPL) for Q2, covering revenue, net income, and EPS. The output must be a plain text paragraph.</task>
254
+ <task>Create a 1-paragraph financial summary for Google (GOOGL) for Q2, covering revenue, net income, and EPS. The output must be a plain text paragraph.</task>
255
+ <task>Perform a market sentiment analysis for Apple (AAPL) for Q2, returning a single sentiment score from -1 (very negative) to 1 (very positive). The output must be a single floating-point number.</task>
256
+ <task>Perform a market sentiment analysis for Google (GOOGL) for Q2, returning a single sentiment score from -1 (very negative) to 1 (very positive). The output must be a single floating-point number.</task>
257
+ <task>Compile the provided financial summaries and market sentiment scores for Apple (AAPL) and Google (GOOGL) into a single Q2 performance report. The report should be a markdown-formatted document.</task>
253
258
  </tasks>
254
259
  ```
255
- * **Reasoning**: The financial analysis and market research can be done in parallel for both companies. The final report depends on all previous steps. This decomposition leverages worker specialization and parallelism, following the "Aggressive Parallelization" principle.
260
+ * **Reasoning**: The financial analysis and market research can be done in parallel for both companies. The final report depends on all previous steps. This decomposition leverages worker specialization and parallelism, following the "Aggressive Parallelization" principle. **Each subtask has a clearly defined deliverable.**
256
261
  ***
257
262
 
258
263
  **END OF EXAMPLES** - Now, apply these principles and examples to decompose the following task.
@@ -312,6 +317,8 @@ Additional Info: {additional_info}
312
317
  2. **REPLAN**: Modify the task content to address the underlying issue
313
318
  - Use for: Unclear requirements, insufficient context, correctable errors
314
319
  - Provide: Modified task content that addresses the failure cause
320
+ - **CRITICAL**: The replanned task MUST be a clear, actionable
321
+ instruction for an AI agent, not a question or request for a human.
315
322
 
316
323
  3. **DECOMPOSE**: Break the task into smaller, more manageable subtasks
317
324
  - Use for: Complex tasks, capability mismatches, persistent failures
@@ -324,10 +331,13 @@ Additional Info: {additional_info}
324
331
 
325
332
  - **Connection/Network Errors**: Almost always choose RETRY
326
333
  - **Model Processing Errors**: Consider REPLAN if the task can be clarified, otherwise DECOMPOSE
327
- - **Capability Gaps**: Choose DECOMPOSE to break into simpler parts
334
+ - **Capability Gaps**: Choose DECOMPOSE to break into simpler parts. If a
335
+ replan can work, ensure the new task is a command for an agent, not a
336
+ request to a user.
328
337
  - **Ambiguous Requirements**: Choose REPLAN with clearer instructions
329
338
  - **High Failure Count**: Lean towards DECOMPOSE rather than repeated retries
330
- - **Deep Tasks (depth > 2)**: Prefer RETRY or REPLAN over further decomposition
339
+ - **Deep Tasks (depth > 2)**: Prefer RETRY or REPLAN over further
340
+ decomposition
331
341
 
332
342
  **RESPONSE FORMAT:**
333
343
  You must return a valid JSON object with these fields:
@@ -410,11 +410,13 @@ class SingleAgentWorker(Worker):
410
410
  f"{getattr(worker_agent, 'agent_id', worker_agent.role_name)} "
411
411
  f"(from pool/clone of "
412
412
  f"{getattr(self.worker, 'agent_id', self.worker.role_name)}) "
413
- f"to process task {task.content}",
414
- "response_content": response_content,
415
- "tool_calls": final_response.info.get("tool_calls")
416
- if isinstance(response, AsyncStreamingChatAgentResponse)
417
- else response.info.get("tool_calls"),
413
+ f"to process task: {task.content}",
414
+ "response_content": response_content[:50],
415
+ "tool_calls": str(
416
+ final_response.info.get("tool_calls")
417
+ if isinstance(response, AsyncStreamingChatAgentResponse)
418
+ else response.info.get("tool_calls")
419
+ )[:50],
418
420
  "total_tokens": total_tokens,
419
421
  }
420
422
 
@@ -445,11 +447,11 @@ class SingleAgentWorker(Worker):
445
447
  f"\n{color}{task_result.content}{Fore.RESET}\n======", # type: ignore[union-attr]
446
448
  )
447
449
 
450
+ task.result = task_result.content # type: ignore[union-attr]
451
+
448
452
  if task_result.failed: # type: ignore[union-attr]
449
453
  return TaskState.FAILED
450
454
 
451
- task.result = task_result.content # type: ignore[union-attr]
452
-
453
455
  if is_task_result_insufficient(task):
454
456
  print(
455
457
  f"{Fore.RED}Task {task.id}: Content validation failed - "
@@ -1721,23 +1721,51 @@ class Workforce(BaseNode):
1721
1721
 
1722
1722
  def _get_child_nodes_info(self) -> str:
1723
1723
  r"""Get the information of all the child nodes under this node."""
1724
- info = ""
1725
- for child in self._children:
1726
- if isinstance(child, Workforce):
1727
- additional_info = "A Workforce node"
1728
- elif isinstance(child, SingleAgentWorker):
1729
- additional_info = "tools: " + (
1730
- ", ".join(child.worker.tool_dict.keys())
1731
- )
1732
- elif isinstance(child, RolePlayingWorker):
1733
- additional_info = "A Role playing node"
1724
+ return "".join(
1725
+ f"<{child.node_id}>:<{child.description}>:<{self._get_node_info(child)}>\n"
1726
+ for child in self._children
1727
+ )
1728
+
1729
+ def _get_node_info(self, node) -> str:
1730
+ r"""Get descriptive information for a specific node type."""
1731
+ if isinstance(node, Workforce):
1732
+ return "A Workforce node"
1733
+ elif isinstance(node, SingleAgentWorker):
1734
+ return self._get_single_agent_info(node)
1735
+ elif isinstance(node, RolePlayingWorker):
1736
+ return "A Role playing node"
1737
+ else:
1738
+ return "Unknown node"
1739
+
1740
+ def _get_single_agent_info(self, worker: 'SingleAgentWorker') -> str:
1741
+ r"""Get formatted information for a SingleAgentWorker node."""
1742
+ toolkit_tools = self._group_tools_by_toolkit(worker.worker.tool_dict)
1743
+
1744
+ if not toolkit_tools:
1745
+ return "no tools available"
1746
+
1747
+ toolkit_info = []
1748
+ for toolkit_name, tools in sorted(toolkit_tools.items()):
1749
+ tools_str = ', '.join(sorted(tools))
1750
+ toolkit_info.append(f"{toolkit_name}({tools_str})")
1751
+
1752
+ return " | ".join(toolkit_info)
1753
+
1754
+ def _group_tools_by_toolkit(self, tool_dict: dict) -> dict[str, list[str]]:
1755
+ r"""Group tools by their parent toolkit class names."""
1756
+ toolkit_tools: dict[str, list[str]] = {}
1757
+
1758
+ for tool_name, tool in tool_dict.items():
1759
+ if hasattr(tool.func, '__self__'):
1760
+ toolkit_name = tool.func.__self__.__class__.__name__
1734
1761
  else:
1735
- additional_info = "Unknown node"
1736
- info += (
1737
- f"<{child.node_id}>:<{child.description}>:<"
1738
- f"{additional_info}>\n"
1739
- )
1740
- return info
1762
+ toolkit_name = "Standalone"
1763
+
1764
+ if toolkit_name not in toolkit_tools:
1765
+ toolkit_tools[toolkit_name] = []
1766
+ toolkit_tools[toolkit_name].append(tool_name)
1767
+
1768
+ return toolkit_tools
1741
1769
 
1742
1770
  def _get_valid_worker_ids(self) -> set:
1743
1771
  r"""Get all valid worker IDs from child nodes.
@@ -42,4 +42,5 @@ __all__ = [
42
42
  'VectorRecord',
43
43
  'VectorDBStatus',
44
44
  'PgVectorStorage',
45
+ 'SurrealStorage',
45
46
  ]