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,415 @@
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 re
15
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
16
+
17
+ from camel.logger import get_logger
18
+ from camel.storages.vectordb_storages import (
19
+ BaseVectorStorage,
20
+ VectorDBQuery,
21
+ VectorDBQueryResult,
22
+ VectorDBStatus,
23
+ VectorRecord,
24
+ )
25
+ from camel.types import VectorDistance
26
+ from camel.utils import dependencies_required
27
+
28
+ if TYPE_CHECKING:
29
+ from surrealdb import Surreal # type: ignore[import-not-found]
30
+
31
+ logger = get_logger(__name__)
32
+
33
+
34
+ class SurrealStorage(BaseVectorStorage):
35
+ r"""An implementation of the `BaseVectorStorage` using SurrealDB,
36
+ a scalable, distributed database with WebSocket support, for
37
+ efficient vector storage and similarity search.
38
+
39
+ SurrealDB official site and documentation can be found at:
40
+ `SurrealDB <https://surrealdb.com>`_
41
+
42
+ Args:
43
+ url (str): WebSocket URL for connecting to SurrealDB
44
+ (default: "ws://localhost:8000/rpc").
45
+ table (str): Name of the table used for storing vectors
46
+ (default: "vector_store").
47
+ vector_dim (int): Dimensionality of the stored vectors.
48
+ distance (VectorDistance): Distance metric used for similarity
49
+ comparisons (default: VectorDistance.COSINE).
50
+ namespace (str): SurrealDB namespace to use (default: "default").
51
+ database (str): SurrealDB database name (default: "demo").
52
+ user (str): Username for authentication (default: "root").
53
+ password (str): Password for authentication (default: "root").
54
+
55
+ Notes:
56
+ - SurrealDB supports flexible schema and powerful querying capabilities
57
+ via SQL-like syntax over WebSocket.
58
+ - This implementation manages connection setup and ensures the target
59
+ table exists.
60
+ - Suitable for applications requiring distributed vector storage and
61
+ search with real-time updates.
62
+ """
63
+
64
+ @dependencies_required('surrealdb')
65
+ def __init__(
66
+ self,
67
+ *,
68
+ url: str = "ws://localhost:8000/rpc",
69
+ table: str = "vector_store",
70
+ vector_dim: int = 786,
71
+ distance: VectorDistance = VectorDistance.COSINE,
72
+ namespace: str = "default",
73
+ database: str = "demo",
74
+ user: str = "root",
75
+ password: str = "root",
76
+ ) -> None:
77
+ r"""Initialize SurrealStorage with connection settings and ensure
78
+ the target table exists.
79
+
80
+ Args:
81
+ url (str): WebSocket URL for connecting to SurrealDB.
82
+ (default: :obj:`"ws://localhost:8000/rpc"`)
83
+ table (str): Name of the table used for vector storage.
84
+ (default: :obj:`"vector_store"`)
85
+ vector_dim (int): Dimensionality of the stored vectors.
86
+ (default: :obj:`786`)
87
+ distance (VectorDistance): Distance metric for similarity
88
+ searches. (default: :obj:`VectorDistance.COSINE`)
89
+ namespace (str): SurrealDB namespace to use.
90
+ (default: :obj:`"default"`)
91
+ database (str): SurrealDB database name.
92
+ (default: :obj:`"demo"`)
93
+ user (str): Username for authentication.
94
+ (default: :obj:`"root"`)
95
+ password (str): Password for authentication.
96
+ (default: :obj:`"root"`)
97
+ """
98
+
99
+ self.url = url
100
+ self.table = table
101
+ self.ns = namespace
102
+ self.db = database
103
+ self.user = user
104
+ self.password = password
105
+ self.vector_dim = vector_dim
106
+ self.distance = distance
107
+ self._check_and_create_table()
108
+ self._surreal_client = None
109
+
110
+ def _table_exists(self) -> bool:
111
+ r"""Check whether the target table exists in the database.
112
+
113
+ Returns:
114
+ bool: True if the table exists, False otherwise.
115
+ """
116
+ from surrealdb import Surreal # type: ignore[import-not-found]
117
+
118
+ with Surreal(self.url) as db:
119
+ db.signin({"username": self.user, "password": self.password})
120
+ db.use(self.ns, self.db)
121
+ res = db.query_raw("INFO FOR DB;")
122
+ tables = res['result'][0]['result'].get('tables', {})
123
+ return self.table in tables
124
+
125
+ def _get_table_info(self) -> Dict[str, int]:
126
+ r"""Retrieve dimension and record count from the table metadata.
127
+
128
+ Returns:
129
+ Dict[str, int]: A dictionary with 'dim' and 'count' keys.
130
+ """
131
+ from surrealdb import Surreal # type: ignore[import-not-found]
132
+
133
+ if not self._table_exists():
134
+ return {"dim": self.vector_dim, "count": 0}
135
+ with Surreal(self.url) as db:
136
+ db.signin({"username": self.user, "password": self.password})
137
+ db.use(self.ns, self.db)
138
+ res = db.query_raw(f"INFO FOR TABLE {self.table};")
139
+
140
+ indexes = res['result'][0]['result'].get("indexes", {})
141
+
142
+ dim = self.vector_dim
143
+ idx_def = indexes.get("hnsw_idx")
144
+ if idx_def and isinstance(idx_def, str):
145
+ m = re.search(r"DIMENSION\s+(\d+)", idx_def)
146
+ if m:
147
+ dim = int(m.group(1))
148
+ cnt = db.query_raw(f"SELECT COUNT() AS count FROM {self.table};")
149
+ try:
150
+ count = cnt['result'][0]['result'][0]['count']
151
+ except (KeyError, IndexError, TypeError):
152
+ logger.warning(
153
+ "Unexpected result format when counting records: %s", cnt
154
+ )
155
+ count = 0
156
+
157
+ return {"dim": dim, "count": count}
158
+
159
+ def _create_table(self):
160
+ r"""Define and create the vector storage table with HNSW index."""
161
+ from surrealdb import Surreal # type: ignore[import-not-found]
162
+
163
+ with Surreal(self.url) as db:
164
+ db.signin({"username": self.user, "password": self.password})
165
+ db.use(self.ns, self.db)
166
+ db.query_raw(
167
+ f"""DEFINE TABLE {self.table} SCHEMALESS;
168
+ DEFINE FIELD payload ON {self.table} TYPE object;
169
+ DEFINE FIELD embedding ON {self.table} TYPE array;
170
+ DEFINE INDEX hnsw_idx ON {self.table}
171
+ FIELDS embedding HNSW DIMENSION {self.vector_dim};
172
+ """
173
+ )
174
+ logger.info(f"Table '{self.table}' created successfully.")
175
+
176
+ def _drop_table(self):
177
+ r"""Drop the vector storage table if it exists."""
178
+ from surrealdb import Surreal # type: ignore[import-not-found]
179
+
180
+ with Surreal(self.url) as db:
181
+ db.signin({"username": self.user, "password": self.password})
182
+ db.use(self.ns, self.db)
183
+ db.query_raw(f"REMOVE TABLE IF EXISTS {self.table};")
184
+ logger.info(f"Table '{self.table}' deleted successfully.")
185
+
186
+ def _check_and_create_table(self):
187
+ r"""Check if the table exists and matches the expected vector
188
+ dimension. If not, create a new table.
189
+ """
190
+ if self._table_exists():
191
+ in_dim = self._get_table_info()["dim"]
192
+ if in_dim != self.vector_dim:
193
+ raise ValueError(
194
+ f"Table {self.table} exists with dimension {in_dim}, "
195
+ f"expected {self.vector_dim}"
196
+ )
197
+ else:
198
+ self._create_table()
199
+
200
+ def _validate_and_convert_records(
201
+ self, records: List[VectorRecord]
202
+ ) -> List[Dict]:
203
+ r"""Validate and convert VectorRecord instances into
204
+ SurrealDB-compatible dictionaries.
205
+
206
+ Args:
207
+ records (List[VectorRecord]): List of vector records to insert.
208
+
209
+ Returns:
210
+ List[Dict]: Transformed list of dicts ready for insertion.
211
+ """
212
+ validate_data = []
213
+ for record in records:
214
+ if len(record.vector) != self.vector_dim:
215
+ raise ValueError(
216
+ f"Vector dimension mismatch: expected {self.vector_dim}, "
217
+ f"got {len(record.vector)}"
218
+ )
219
+ record_dict = {
220
+ "payload": record.payload if record.payload else {},
221
+ "embedding": record.vector,
222
+ }
223
+ validate_data.append(record_dict)
224
+
225
+ return validate_data
226
+
227
+ def query(
228
+ self,
229
+ query: VectorDBQuery,
230
+ **kwargs: Any,
231
+ ) -> List[VectorDBQueryResult]:
232
+ r"""Perform a top-k similarity search using the configured distance
233
+ metric.
234
+
235
+ Args:
236
+ query (VectorDBQuery): Query containing the query vector
237
+ and top_k value.
238
+
239
+ Returns:
240
+ List[VectorDBQueryResult]: Ranked list of matching records
241
+ with similarity scores.
242
+ """
243
+ from surrealdb import Surreal # type: ignore[import-not-found]
244
+
245
+ metric = {
246
+ VectorDistance.COSINE: "cosine",
247
+ VectorDistance.EUCLIDEAN: "euclidean",
248
+ VectorDistance.DOT: "dot",
249
+ }[self.distance]
250
+
251
+ metric_func = {
252
+ VectorDistance.COSINE: "vector::similarity::cosine",
253
+ VectorDistance.EUCLIDEAN: "vector::distance::euclidean",
254
+ VectorDistance.DOT: "vector::dot",
255
+ }.get(self.distance)
256
+
257
+ if not metric_func:
258
+ raise ValueError(f"Unsupported distance metric: {self.distance}")
259
+
260
+ with Surreal(self.url) as db:
261
+ db.signin({"username": self.user, "password": self.password})
262
+ db.use(self.ns, self.db)
263
+
264
+ # Use parameterized query to prevent SQL injection
265
+ sql_query = f"""SELECT payload, embedding,
266
+ {metric_func}(embedding, $query_vec) AS score
267
+ FROM {self.table}
268
+ WHERE embedding <|{query.top_k},{metric}|> $query_vec
269
+ ORDER BY score;
270
+ """
271
+
272
+ response = db.query_raw(
273
+ sql_query, {"query_vec": query.query_vector}
274
+ )
275
+
276
+ if not response.get("result") or not response["result"]:
277
+ return []
278
+
279
+ results = response["result"][0]
280
+
281
+ if "result" not in results:
282
+ return []
283
+
284
+ return [
285
+ VectorDBQueryResult(
286
+ record=VectorRecord(
287
+ vector=row["embedding"], payload=row.get("payload", {})
288
+ ),
289
+ similarity=(
290
+ 1.0 - row["score"]
291
+ if self.distance == VectorDistance.COSINE
292
+ else 1.0 / (1.0 + row["score"])
293
+ if self.distance == VectorDistance.EUCLIDEAN
294
+ else row["score"]
295
+ ),
296
+ )
297
+ for row in results["result"]
298
+ ]
299
+
300
+ def add(self, records: List[VectorRecord], **kwargs) -> None:
301
+ r"""Insert validated vector records into the SurrealDB table.
302
+
303
+ Args:
304
+ records (List[VectorRecord]): List of vector records to add.
305
+ """
306
+ logger.info(
307
+ "Adding %d records to table '%s'.", len(records), self.table
308
+ )
309
+ from surrealdb import Surreal # type: ignore[import-not-found]
310
+
311
+ try:
312
+ with Surreal(self.url) as db:
313
+ db.signin({"username": self.user, "password": self.password})
314
+ db.use(self.ns, self.db)
315
+
316
+ validated_records = self._validate_and_convert_records(records)
317
+ for record in validated_records:
318
+ db.create(self.table, record)
319
+
320
+ logger.info(
321
+ "Successfully added %d records to table '%s'.",
322
+ len(records),
323
+ self.table,
324
+ )
325
+ except Exception as e:
326
+ logger.error(
327
+ "Failed to add records to table '%s': %s",
328
+ self.table,
329
+ str(e),
330
+ exc_info=True,
331
+ )
332
+ raise
333
+
334
+ def delete(
335
+ self, ids: Optional[List[str]] = None, if_all: bool = False, **kwargs
336
+ ) -> None:
337
+ r"""Delete specific records by ID or clear the entire table.
338
+
339
+ Args:
340
+ ids (Optional[List[str]]): List of record IDs to delete.
341
+ if_all (bool): Whether to delete all records in the table.
342
+ """
343
+ from surrealdb import Surreal # type: ignore[import-not-found]
344
+ from surrealdb.data.types.record_id import ( # type: ignore[import-not-found]
345
+ RecordID,
346
+ )
347
+
348
+ try:
349
+ with Surreal(self.url) as db:
350
+ db.signin({"username": self.user, "password": self.password})
351
+ db.use(self.ns, self.db)
352
+
353
+ if if_all:
354
+ db.delete(self.table, **kwargs)
355
+ logger.info(
356
+ f"Deleted all records from table '{self.table}'"
357
+ )
358
+ return
359
+
360
+ if not ids:
361
+ raise ValueError(
362
+ "Either `ids` must be provided or `if_all=True`"
363
+ )
364
+
365
+ for id_str in ids:
366
+ rec = RecordID(self.table, id_str)
367
+ db.delete(rec, **kwargs)
368
+ logger.info(f"Deleted record {rec}")
369
+
370
+ except Exception as e:
371
+ logger.exception("Error deleting records from SurrealDB")
372
+ raise RuntimeError(f"Failed to delete records {ids!r}") from e
373
+
374
+ def status(self) -> VectorDBStatus:
375
+ r"""Retrieve the status of the vector table including dimension and
376
+ count.
377
+
378
+ Returns:
379
+ VectorDBStatus: Object containing vector table metadata.
380
+ """
381
+ status = self._get_table_info()
382
+
383
+ dim = status.get("dim")
384
+ count = status.get("count")
385
+
386
+ if dim is None or count is None:
387
+ raise ValueError("Vector dimension and count cannot be None")
388
+
389
+ return VectorDBStatus(
390
+ vector_dim=dim,
391
+ vector_count=count,
392
+ )
393
+
394
+ def clear(self) -> None:
395
+ r"""Reset the vector table by dropping and recreating it."""
396
+ self._drop_table()
397
+ self._create_table()
398
+
399
+ def load(self) -> None:
400
+ r"""Load the collection hosted on cloud service."""
401
+ # SurrealDB doesn't require explicit loading
402
+ raise NotImplementedError("SurrealDB does not support loading")
403
+
404
+ @property
405
+ def client(self) -> "Surreal":
406
+ r"""Provides access to the underlying SurrealDB client."""
407
+ if self._surreal_client is None:
408
+ from surrealdb import Surreal # type: ignore[import-not-found]
409
+
410
+ self._surreal_client = Surreal(self.url)
411
+ self._surreal_client.signin( # type: ignore[attr-defined]
412
+ {"username": self.user, "password": self.password}
413
+ )
414
+ self._surreal_client.use(self.ns, self.db) # type: ignore[attr-defined]
415
+ return self._surreal_client
@@ -31,7 +31,7 @@ from .meshy_toolkit import MeshyToolkit
31
31
  from .openbb_toolkit import OpenBBToolkit
32
32
  from .bohrium_toolkit import BohriumToolkit
33
33
 
34
- from .base import BaseToolkit
34
+ from .base import BaseToolkit, RegisteredAgentToolkit
35
35
  from .google_maps_toolkit import GoogleMapsToolkit
36
36
  from .code_execution import CodeExecutionToolkit
37
37
  from .github_toolkit import GithubToolkit
@@ -74,6 +74,7 @@ from .jina_reranker_toolkit import JinaRerankerToolkit
74
74
  from .pulse_mcp_search_toolkit import PulseMCPSearchToolkit
75
75
  from .klavis_toolkit import KlavisToolkit
76
76
  from .aci_toolkit import ACIToolkit
77
+ from .origene_mcp_toolkit import OrigeneToolkit
77
78
  from .playwright_mcp_toolkit import PlaywrightMCPToolkit
78
79
  from .wolfram_alpha_toolkit import WolframAlphaToolkit
79
80
  from .task_planning_toolkit import TaskPlanningToolkit
@@ -84,6 +85,9 @@ from .craw4ai_toolkit import Crawl4AIToolkit
84
85
  from .markitdown_toolkit import MarkItDownToolkit
85
86
  from .note_taking_toolkit import NoteTakingToolkit
86
87
  from .message_agent_toolkit import AgentCommunicationToolkit
88
+ from .web_deploy_toolkit import WebDeployToolkit
89
+ from .screenshot_toolkit import ScreenshotToolkit
90
+ from .message_integration import ToolkitMessageIntegration
87
91
 
88
92
  __all__ = [
89
93
  'BaseToolkit',
@@ -141,6 +145,7 @@ __all__ = [
141
145
  'OpenAIAgentToolkit',
142
146
  'SearxNGToolkit',
143
147
  'JinaRerankerToolkit',
148
+ 'OrigeneToolkit',
144
149
  'PulseMCPSearchToolkit',
145
150
  'KlavisToolkit',
146
151
  'ACIToolkit',
@@ -156,4 +161,8 @@ __all__ = [
156
161
  'MarkItDownToolkit',
157
162
  'NoteTakingToolkit',
158
163
  'AgentCommunicationToolkit',
164
+ 'WebDeployToolkit',
165
+ 'ScreenshotToolkit',
166
+ 'RegisteredAgentToolkit',
167
+ 'ToolkitMessageIntegration',
159
168
  ]
camel/toolkits/base.py CHANGED
@@ -12,11 +12,18 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
- from typing import List, Literal, Optional
15
+ from typing import TYPE_CHECKING, List, Literal, Optional
16
16
 
17
+ from camel.logger import get_logger
17
18
  from camel.toolkits import FunctionTool
18
19
  from camel.utils import AgentOpsMeta, with_timeout
19
20
 
21
+ if TYPE_CHECKING:
22
+ from camel.agents import ChatAgent
23
+
24
+
25
+ logger = get_logger(__name__)
26
+
20
27
 
21
28
  class BaseToolkit(metaclass=AgentOpsMeta):
22
29
  r"""Base class for toolkits.
@@ -63,3 +70,52 @@ class BaseToolkit(metaclass=AgentOpsMeta):
63
70
  the MCP server in.
64
71
  """
65
72
  self.mcp.run(mode)
73
+
74
+
75
+ class RegisteredAgentToolkit:
76
+ r"""Mixin class for toolkits that need to register a ChatAgent.
77
+
78
+ This mixin provides a standard interface for toolkits that require
79
+ a reference to a ChatAgent instance. The ChatAgent will check if a
80
+ toolkit has this mixin and automatically register itself.
81
+ """
82
+
83
+ def __init__(self):
84
+ self._agent: Optional["ChatAgent"] = None
85
+
86
+ @property
87
+ def agent(self) -> Optional["ChatAgent"]:
88
+ r"""Get the registered ChatAgent instance.
89
+
90
+ Returns:
91
+ Optional[ChatAgent]: The registered agent, or None if not
92
+ registered.
93
+
94
+ Note:
95
+ If None is returned, it means the toolkit has not been registered
96
+ with a ChatAgent yet. Make sure to pass this toolkit to a ChatAgent
97
+ via the toolkits parameter during initialization.
98
+ """
99
+ if self._agent is None:
100
+ logger.warning(
101
+ f"{self.__class__.__name__} does not have a "
102
+ f"registered ChatAgent. "
103
+ f"Please ensure this toolkit is passed to a ChatAgent via the "
104
+ f"'toolkits_to_register_agent' parameter during ChatAgent "
105
+ f"initialization if you want to use the tools that require a "
106
+ f"registered agent."
107
+ )
108
+ return self._agent
109
+
110
+ def register_agent(self, agent: "ChatAgent") -> None:
111
+ r"""Register a ChatAgent with this toolkit.
112
+
113
+ This method allows registering an agent after initialization. The
114
+ ChatAgent will automatically call this method if the toolkit to
115
+ register inherits from RegisteredAgentToolkit.
116
+
117
+ Args:
118
+ agent (ChatAgent): The ChatAgent instance to register.
119
+ """
120
+ self._agent = agent
121
+ logger.info(f"Agent registered with {self.__class__.__name__}")
@@ -52,7 +52,7 @@ class HumanToolkit(BaseToolkit):
52
52
  logger.info(f"User reply: {reply}")
53
53
  return reply
54
54
 
55
- def send_message_to_user(self, message: str) -> None:
55
+ def send_message_to_user(self, message: str) -> str:
56
56
  r"""Use this tool to send a tidy message to the user in one short
57
57
  sentence.
58
58
 
@@ -68,9 +68,13 @@ class HumanToolkit(BaseToolkit):
68
68
 
69
69
  Args:
70
70
  message (str): The tidy and informative message for the user.
71
+
72
+ Returns:
73
+ str: Confirmation that the message was successfully sent.
71
74
  """
72
75
  print(f"\nAgent Message:\n{message}")
73
76
  logger.info(f"\nAgent Message:\n{message}")
77
+ return f"Message successfully sent to user: '{message}'"
74
78
 
75
79
  def get_tools(self) -> List[FunctionTool]:
76
80
  r"""Returns a list of FunctionTool objects representing the