camel-ai 0.2.76a0__py3-none-any.whl → 0.2.76a2__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 (34) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +8 -1
  3. camel/environments/tic_tac_toe.py +1 -1
  4. camel/memories/__init__.py +2 -1
  5. camel/memories/agent_memories.py +3 -1
  6. camel/memories/blocks/chat_history_block.py +17 -2
  7. camel/models/base_model.py +30 -0
  8. camel/societies/workforce/single_agent_worker.py +44 -38
  9. camel/societies/workforce/workforce.py +10 -1
  10. camel/storages/object_storages/google_cloud.py +1 -1
  11. camel/toolkits/__init__.py +9 -2
  12. camel/toolkits/aci_toolkit.py +45 -0
  13. camel/toolkits/context_summarizer_toolkit.py +683 -0
  14. camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +194 -34
  15. camel/toolkits/hybrid_browser_toolkit/config_loader.py +4 -0
  16. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +67 -2
  17. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +62 -45
  18. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +489 -60
  19. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +5 -2
  20. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +72 -12
  21. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +2 -14
  22. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +1 -0
  23. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +228 -62
  24. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +4 -4
  25. camel/toolkits/markitdown_toolkit.py +27 -1
  26. camel/toolkits/note_taking_toolkit.py +18 -8
  27. camel/toolkits/slack_toolkit.py +50 -1
  28. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  29. camel/toolkits/wechat_official_toolkit.py +483 -0
  30. camel/utils/context_utils.py +395 -0
  31. {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/METADATA +84 -6
  32. {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/RECORD +34 -30
  33. {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/WHEEL +0 -0
  34. {camel_ai-0.2.76a0.dist-info → camel_ai-0.2.76a2.dist-info}/licenses/LICENSE +0 -0
camel/__init__.py CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  from camel.logger import disable_logging, enable_logging, set_log_level
16
16
 
17
- __version__ = '0.2.76a0'
17
+ __version__ = '0.2.76a2'
18
18
 
19
19
  __all__ = [
20
20
  '__version__',
@@ -63,6 +63,7 @@ from camel.memories import (
63
63
  MemoryRecord,
64
64
  ScoreBasedContextCreator,
65
65
  )
66
+ from camel.memories.blocks.chat_history_block import EmptyMemoryWarning
66
67
  from camel.messages import (
67
68
  BaseMessage,
68
69
  FunctionCallingMessage,
@@ -841,7 +842,12 @@ class ChatAgent(BaseAgent):
841
842
  current_tokens = token_counter.count_tokens_from_messages(
842
843
  [message.to_openai_message(role)]
843
844
  )
844
- _, ctx_tokens = self.memory.get_context()
845
+ import warnings
846
+
847
+ with warnings.catch_warnings():
848
+ warnings.filterwarnings("ignore", category=EmptyMemoryWarning)
849
+ _, ctx_tokens = self.memory.get_context()
850
+
845
851
  remaining_budget = max(0, token_limit - ctx_tokens)
846
852
 
847
853
  if current_tokens <= remaining_budget:
@@ -1035,6 +1041,7 @@ class ChatAgent(BaseAgent):
1035
1041
  None
1036
1042
  """
1037
1043
  self.memory.clear()
1044
+
1038
1045
  if self.system_message is not None:
1039
1046
  self.update_memory(self.system_message, OpenAIBackendRole.SYSTEM)
1040
1047
 
@@ -483,7 +483,7 @@ class TicTacToeEnv(MultiStepEnv):
483
483
  # Check all win combinations.
484
484
  for a, b, c in TicTacToeEnv.WIN_COMBINATIONS:
485
485
  if board[a] != " " and board[a] == board[b] == board[c]:
486
- return board[a]
486
+ return board[a] # type: ignore[return-value]
487
487
  # Check for draw.
488
488
  if all(cell != " " for cell in board):
489
489
  return "draw"
@@ -18,7 +18,7 @@ from .agent_memories import (
18
18
  VectorDBMemory,
19
19
  )
20
20
  from .base import AgentMemory, BaseContextCreator, MemoryBlock
21
- from .blocks.chat_history_block import ChatHistoryBlock
21
+ from .blocks.chat_history_block import ChatHistoryBlock, EmptyMemoryWarning
22
22
  from .blocks.vectordb_block import VectorDBBlock
23
23
  from .context_creators.score_based import ScoreBasedContextCreator
24
24
  from .records import ContextRecord, MemoryRecord
@@ -35,4 +35,5 @@ __all__ = [
35
35
  'ChatHistoryBlock',
36
36
  'VectorDBBlock',
37
37
  'LongtermAgentMemory',
38
+ 'EmptyMemoryWarning',
38
39
  ]
@@ -51,7 +51,9 @@ class ChatHistoryMemory(AgentMemory):
51
51
  raise ValueError("`window_size` must be non-negative.")
52
52
  self._context_creator = context_creator
53
53
  self._window_size = window_size
54
- self._chat_history_block = ChatHistoryBlock(storage=storage)
54
+ self._chat_history_block = ChatHistoryBlock(
55
+ storage=storage,
56
+ )
55
57
  self._agent_id = agent_id
56
58
 
57
59
  @property
@@ -21,6 +21,17 @@ from camel.storages.key_value_storages.in_memory import InMemoryKeyValueStorage
21
21
  from camel.types import OpenAIBackendRole
22
22
 
23
23
 
24
+ class EmptyMemoryWarning(UserWarning):
25
+ """Warning raised when attempting to access an empty memory.
26
+
27
+ This warning is raised when operations are performed on memory
28
+ that contains no records. It can be safely caught and suppressed
29
+ in contexts where empty memory is expected.
30
+ """
31
+
32
+ pass
33
+
34
+
24
35
  class ChatHistoryBlock(MemoryBlock):
25
36
  r"""An implementation of the :obj:`MemoryBlock` abstract base class for
26
37
  maintaining a record of chat histories.
@@ -39,7 +50,7 @@ class ChatHistoryBlock(MemoryBlock):
39
50
  last message is 1.0, and with each step taken backward, the score
40
51
  of the message is multiplied by the `keep_rate`. Higher `keep_rate`
41
52
  leads to high possibility to keep history messages during context
42
- creation.
53
+ creation. (default: :obj:`0.9`)
43
54
  """
44
55
 
45
56
  def __init__(
@@ -70,7 +81,11 @@ class ChatHistoryBlock(MemoryBlock):
70
81
  """
71
82
  record_dicts = self.storage.load()
72
83
  if len(record_dicts) == 0:
73
- warnings.warn("The `ChatHistoryMemory` is empty.")
84
+ warnings.warn(
85
+ "The `ChatHistoryMemory` is empty.",
86
+ EmptyMemoryWarning,
87
+ stacklevel=1,
88
+ )
74
89
  return list()
75
90
 
76
91
  if window_size is not None and window_size >= 0:
@@ -24,6 +24,7 @@ from openai.lib.streaming.chat import (
24
24
  )
25
25
  from pydantic import BaseModel
26
26
 
27
+ from camel.logger import get_logger as camel_get_logger
27
28
  from camel.messages import OpenAIMessage
28
29
  from camel.types import (
29
30
  ChatCompletion,
@@ -34,6 +35,21 @@ from camel.types import (
34
35
  )
35
36
  from camel.utils import BaseTokenCounter
36
37
 
38
+ if os.environ.get("TRACEROOT_ENABLED", "False").lower() == "true":
39
+ try:
40
+ from traceroot import get_logger # type: ignore[import]
41
+ from traceroot import trace as observe # type: ignore[import]
42
+
43
+ logger = get_logger('base_model')
44
+ except ImportError:
45
+ from camel.utils import observe
46
+
47
+ logger = camel_get_logger('base_model')
48
+ else:
49
+ from camel.utils import observe
50
+
51
+ logger = camel_get_logger('base_model')
52
+
37
53
 
38
54
  class ModelBackendMeta(abc.ABCMeta):
39
55
  r"""Metaclass that automatically preprocesses messages in run method.
@@ -364,6 +380,7 @@ class BaseModelBackend(ABC, metaclass=ModelBackendMeta):
364
380
  """
365
381
  pass
366
382
 
383
+ @observe()
367
384
  def run(
368
385
  self,
369
386
  messages: List[OpenAIMessage],
@@ -403,7 +420,13 @@ class BaseModelBackend(ABC, metaclass=ModelBackendMeta):
403
420
  elif not tools:
404
421
  tools = None
405
422
 
423
+ logger.info("Running model: %s", self.model_type)
424
+ logger.info("Messages: %s", messages)
425
+ logger.info("Response format: %s", response_format)
426
+ logger.info("Tools: %s", tools)
427
+
406
428
  result = self._run(messages, response_format, tools)
429
+ logger.info("Result: %s", result)
407
430
 
408
431
  # Log the response if logging is enabled
409
432
  if log_path:
@@ -411,6 +434,7 @@ class BaseModelBackend(ABC, metaclass=ModelBackendMeta):
411
434
 
412
435
  return result
413
436
 
437
+ @observe()
414
438
  async def arun(
415
439
  self,
416
440
  messages: List[OpenAIMessage],
@@ -448,7 +472,13 @@ class BaseModelBackend(ABC, metaclass=ModelBackendMeta):
448
472
  elif not tools:
449
473
  tools = None
450
474
 
475
+ logger.info("Running model: %s", self.model_type)
476
+ logger.info("Messages: %s", messages)
477
+ logger.info("Response format: %s", response_format)
478
+ logger.info("Tools: %s", tools)
479
+
451
480
  result = await self._arun(messages, response_format, tools)
481
+ logger.info("Result: %s", result)
452
482
 
453
483
  # Log the response if logging is enabled
454
484
  if log_path:
@@ -35,8 +35,8 @@ from camel.tasks.task import Task, TaskState, is_task_result_insufficient
35
35
  class AgentPool:
36
36
  r"""A pool of agent instances for efficient reuse.
37
37
 
38
- This pool manages a collection of pre-cloned agents. It supports
39
- auto-scaling based ondemand and intelligent reuse of existing agents.
38
+ This pool manages a collection of pre-cloned agents with automatic
39
+ scaling and idle timeout cleanup.
40
40
 
41
41
  Args:
42
42
  base_agent (ChatAgent): The base agent to clone from.
@@ -48,6 +48,8 @@ class AgentPool:
48
48
  (default: :obj:`True`)
49
49
  idle_timeout (float): Time in seconds after which idle agents are
50
50
  removed. (default: :obj:`180.0`)
51
+ cleanup_interval (float): Fixed interval in seconds between cleanup
52
+ checks. (default: :obj:`60.0`)
51
53
  """
52
54
 
53
55
  def __init__(
@@ -56,12 +58,14 @@ class AgentPool:
56
58
  initial_size: int = 1,
57
59
  max_size: int = 10,
58
60
  auto_scale: bool = True,
59
- idle_timeout: float = 180.0, # 3 minutes
61
+ idle_timeout: float = 180.0,
62
+ cleanup_interval: float = 60.0,
60
63
  ):
61
64
  self.base_agent = base_agent
62
65
  self.max_size = max_size
63
66
  self.auto_scale = auto_scale
64
67
  self.idle_timeout = idle_timeout
68
+ self.cleanup_interval = cleanup_interval
65
69
 
66
70
  # Pool management
67
71
  self._available_agents: deque = deque()
@@ -73,6 +77,7 @@ class AgentPool:
73
77
  self._total_borrows = 0
74
78
  self._total_clones_created = 0
75
79
  self._pool_hits = 0
80
+ self._agents_cleaned = 0
76
81
 
77
82
  # Initialize pool
78
83
  self._initialize_pool(initial_size)
@@ -82,6 +87,7 @@ class AgentPool:
82
87
  for _ in range(min(size, self.max_size)):
83
88
  agent = self._create_fresh_agent()
84
89
  self._available_agents.append(agent)
90
+ self._agent_last_used[id(agent)] = time.time()
85
91
 
86
92
  def _create_fresh_agent(self) -> ChatAgent:
87
93
  r"""Create a fresh agent instance."""
@@ -94,53 +100,46 @@ class AgentPool:
94
100
  async with self._lock:
95
101
  self._total_borrows += 1
96
102
 
97
- # Try to get from available agents first
98
103
  if self._available_agents:
99
104
  agent = self._available_agents.popleft()
100
105
  self._in_use_agents.add(id(agent))
101
106
  self._pool_hits += 1
102
-
103
- # Reset the agent state
104
- agent.reset()
105
107
  return agent
106
108
 
107
- # Check if we can create new agents
108
- total_agents = len(self._available_agents) + len(
109
- self._in_use_agents
110
- )
111
- if total_agents < self.max_size:
109
+ # Check if we can create a new agent
110
+ if len(self._in_use_agents) < self.max_size or self.auto_scale:
112
111
  agent = self._create_fresh_agent()
113
112
  self._in_use_agents.add(id(agent))
114
113
  return agent
115
114
 
116
- # Pool exhausted, wait and retry or create temporary agent
117
- if self.auto_scale:
118
- # Create a temporary agent that won't be returned to pool
119
- return self._create_fresh_agent()
120
- else:
121
- # Wait for an agent to become available
122
- while not self._available_agents:
123
- await asyncio.sleep(0.1)
124
-
125
- agent = self._available_agents.popleft()
126
- self._in_use_agents.add(id(agent))
127
- agent.reset()
128
- return agent
115
+ # Wait for available agent
116
+ while True:
117
+ async with self._lock:
118
+ if self._available_agents:
119
+ agent = self._available_agents.popleft()
120
+ self._in_use_agents.add(id(agent))
121
+ self._pool_hits += 1
122
+ return agent
123
+ await asyncio.sleep(0.05)
129
124
 
130
125
  async def return_agent(self, agent: ChatAgent) -> None:
131
126
  r"""Return an agent to the pool."""
127
+ agent_id = id(agent)
128
+
132
129
  async with self._lock:
133
- agent_id = id(agent)
130
+ if agent_id not in self._in_use_agents:
131
+ return
134
132
 
135
- if agent_id in self._in_use_agents:
136
- self._in_use_agents.remove(agent_id)
133
+ self._in_use_agents.discard(agent_id)
137
134
 
138
- # Only return to pool if we're under max size
139
- if len(self._available_agents) < self.max_size:
140
- # Reset agent state before returning to pool
141
- agent.reset()
142
- self._available_agents.append(agent)
143
- self._agent_last_used[agent_id] = time.time()
135
+ # Only add back to pool if under max size
136
+ if len(self._available_agents) < self.max_size:
137
+ agent.reset()
138
+ self._agent_last_used[agent_id] = time.time()
139
+ self._available_agents.append(agent)
140
+ else:
141
+ # Remove tracking for agents not returned to pool
142
+ self._agent_last_used.pop(agent_id, None)
144
143
 
145
144
  async def cleanup_idle_agents(self) -> None:
146
145
  r"""Remove idle agents from the pool to free memory."""
@@ -148,30 +147,35 @@ class AgentPool:
148
147
  return
149
148
 
150
149
  async with self._lock:
150
+ if not self._available_agents:
151
+ return
152
+
151
153
  current_time = time.time()
152
154
  agents_to_remove = []
153
155
 
154
156
  for agent in list(self._available_agents):
155
157
  agent_id = id(agent)
156
158
  last_used = self._agent_last_used.get(agent_id, current_time)
157
-
158
159
  if current_time - last_used > self.idle_timeout:
159
160
  agents_to_remove.append(agent)
160
161
 
161
162
  for agent in agents_to_remove:
162
163
  self._available_agents.remove(agent)
163
- agent_id = id(agent)
164
- self._agent_last_used.pop(agent_id, None)
164
+ self._agent_last_used.pop(id(agent), None)
165
+ self._agents_cleaned += 1
165
166
 
166
167
  def get_stats(self) -> dict:
167
168
  r"""Get pool statistics."""
168
169
  return {
169
170
  "available_agents": len(self._available_agents),
170
171
  "in_use_agents": len(self._in_use_agents),
172
+ "pool_size": len(self._available_agents)
173
+ + len(self._in_use_agents),
171
174
  "total_borrows": self._total_borrows,
172
175
  "total_clones_created": self._total_clones_created,
173
176
  "pool_hits": self._pool_hits,
174
177
  "hit_rate": self._pool_hits / max(self._total_borrows, 1),
178
+ "agents_cleaned_up": self._agents_cleaned,
175
179
  }
176
180
 
177
181
 
@@ -477,7 +481,9 @@ class SingleAgentWorker(Worker):
477
481
  r"""Periodically clean up idle agents from the pool."""
478
482
  while True:
479
483
  try:
480
- await asyncio.sleep(60) # Cleanup every minute
484
+ # Fixed interval cleanup
485
+ await asyncio.sleep(self.agent_pool.cleanup_interval)
486
+
481
487
  if self.agent_pool:
482
488
  await self.agent_pool.cleanup_idle_agents()
483
489
  except asyncio.CancelledError:
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
  import asyncio
17
17
  import concurrent.futures
18
18
  import json
19
+ import os
19
20
  import time
20
21
  import uuid
21
22
  from collections import deque
@@ -79,7 +80,15 @@ from camel.utils import dependencies_required
79
80
 
80
81
  from .workforce_logger import WorkforceLogger
81
82
 
82
- logger = get_logger(__name__)
83
+ if os.environ.get("TRACEROOT_ENABLED", "False").lower() == "true":
84
+ try:
85
+ import traceroot # type: ignore[import]
86
+
87
+ logger = traceroot.get_logger('camel')
88
+ except ImportError:
89
+ logger = get_logger(__name__)
90
+ else:
91
+ logger = get_logger(__name__)
83
92
 
84
93
  # Constants for configuration values
85
94
  MAX_TASK_RETRIES = 3
@@ -46,7 +46,7 @@ class GoogleCloudStorage(BaseObjectStorage):
46
46
  create_if_not_exists: bool = True,
47
47
  anonymous: bool = False,
48
48
  ) -> None:
49
- from google.cloud import storage
49
+ from google.cloud import storage # type: ignore[attr-defined]
50
50
 
51
51
  self.create_if_not_exists = create_if_not_exists
52
52
 
@@ -40,6 +40,7 @@ from .google_calendar_toolkit import GoogleCalendarToolkit
40
40
  from .arxiv_toolkit import ArxivToolkit
41
41
  from .slack_toolkit import SlackToolkit
42
42
  from .whatsapp_toolkit import WhatsAppToolkit
43
+ from .wechat_official_toolkit import WeChatOfficialToolkit
43
44
  from .twitter_toolkit import TwitterToolkit
44
45
  from .open_api_toolkit import OpenAPIToolkit
45
46
  from .retrieval_toolkit import RetrievalToolkit
@@ -61,7 +62,7 @@ from .image_analysis_toolkit import ImageAnalysisToolkit
61
62
  from .mcp_toolkit import MCPToolkit
62
63
  from .browser_toolkit import BrowserToolkit
63
64
  from .async_browser_toolkit import AsyncBrowserToolkit
64
- from .file_write_toolkit import FileWriteToolkit
65
+ from .file_toolkit import FileToolkit, FileWriteToolkit
65
66
  from .pptx_toolkit import PPTXToolkit
66
67
  from .terminal_toolkit import TerminalToolkit
67
68
  from .pubmed_toolkit import PubMedToolkit
@@ -87,7 +88,9 @@ from .message_agent_toolkit import AgentCommunicationToolkit
87
88
  from .web_deploy_toolkit import WebDeployToolkit
88
89
  from .screenshot_toolkit import ScreenshotToolkit
89
90
  from .message_integration import ToolkitMessageIntegration
91
+ from .context_summarizer_toolkit import ContextSummarizerToolkit
90
92
  from .notion_mcp_toolkit import NotionMCPToolkit
93
+ from .vertex_ai_veo_toolkit import VertexAIVeoToolkit
91
94
  from .minimax_mcp_toolkit import MinimaxMCPToolkit
92
95
 
93
96
  __all__ = [
@@ -103,6 +106,7 @@ __all__ = [
103
106
  'SearchToolkit',
104
107
  'SlackToolkit',
105
108
  'WhatsAppToolkit',
109
+ 'WeChatOfficialToolkit',
106
110
  'ImageGenToolkit',
107
111
  'TwitterToolkit',
108
112
  'WeatherToolkit',
@@ -136,7 +140,8 @@ __all__ = [
136
140
  'ImageAnalysisToolkit',
137
141
  'BrowserToolkit',
138
142
  'AsyncBrowserToolkit',
139
- 'FileWriteToolkit',
143
+ 'FileToolkit',
144
+ 'FileWriteToolkit', # Deprecated, use FileToolkit instead
140
145
  'PPTXToolkit',
141
146
  'TerminalToolkit',
142
147
  'PubMedToolkit',
@@ -165,6 +170,8 @@ __all__ = [
165
170
  'ScreenshotToolkit',
166
171
  'RegisteredAgentToolkit',
167
172
  'ToolkitMessageIntegration',
173
+ 'ContextSummarizerToolkit',
168
174
  'NotionMCPToolkit',
175
+ 'VertexAIVeoToolkit',
169
176
  'MinimaxMCPToolkit',
170
177
  ]
@@ -12,6 +12,7 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
+ import asyncio
15
16
  import os
16
17
  from typing import TYPE_CHECKING, Dict, List, Optional, Union
17
18
 
@@ -408,6 +409,38 @@ class ACIToolkit(BaseToolkit):
408
409
  )
409
410
  return result
410
411
 
412
+ async def aexecute_function(
413
+ self,
414
+ function_name: str,
415
+ function_arguments: Dict,
416
+ linked_account_owner_id: str,
417
+ allowed_apps_only: bool = False,
418
+ ) -> Dict:
419
+ r"""Execute a function call asynchronously.
420
+
421
+ Args:
422
+ function_name (str): Name of the function to execute.
423
+ function_arguments (Dict): Arguments to pass to the function.
424
+ linked_account_owner_id (str): To specify the end-user (account
425
+ owner) on behalf of whom you want to execute functions
426
+ You need to first link corresponding account with the same
427
+ owner id in the ACI dashboard (https://platform.aci.dev).
428
+ allowed_apps_only (bool): If true, only returns functions/apps
429
+ that are allowed to be used by the agent/accessor, identified
430
+ by the api key. (default: :obj:`False`)
431
+
432
+ Returns:
433
+ Dict: Result of the function execution
434
+ """
435
+ result = await asyncio.to_thread(
436
+ self.client.handle_function_call,
437
+ function_name,
438
+ function_arguments,
439
+ linked_account_owner_id,
440
+ allowed_apps_only,
441
+ )
442
+ return result
443
+
411
444
  def get_tools(self) -> List[FunctionTool]:
412
445
  r"""Get a list of tools (functions) available in the configured apps.
413
446
 
@@ -434,6 +467,8 @@ class ACIToolkit(BaseToolkit):
434
467
  FunctionTool(self.delete_linked_account),
435
468
  FunctionTool(self.function_definition),
436
469
  FunctionTool(self.search_function),
470
+ FunctionTool(self.execute_function),
471
+ FunctionTool(self.aexecute_function),
437
472
  ]
438
473
 
439
474
  for function in _all_function:
@@ -448,6 +483,16 @@ class ACIToolkit(BaseToolkit):
448
483
  linked_account_owner_id=self.linked_account_owner_id,
449
484
  )
450
485
 
486
+ async def async_dummy_func(*, schema=schema, **kwargs):
487
+ return await self.aexecute_function(
488
+ function_name=schema['function']['name'],
489
+ function_arguments=kwargs,
490
+ linked_account_owner_id=self.linked_account_owner_id,
491
+ )
492
+
493
+ # Add async_call method to the sync function for compatibility
494
+ dummy_func.async_call = async_dummy_func # type: ignore[attr-defined]
495
+
451
496
  tool = FunctionTool(
452
497
  func=dummy_func,
453
498
  openai_tool_schema=schema,