camel-ai 0.2.75a6__py3-none-any.whl → 0.2.76__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 +1001 -205
- camel/agents/mcp_agent.py +30 -27
- camel/configs/__init__.py +6 -0
- camel/configs/amd_config.py +70 -0
- camel/configs/cometapi_config.py +104 -0
- camel/data_collectors/alpaca_collector.py +15 -6
- camel/environments/tic_tac_toe.py +1 -1
- camel/interpreters/__init__.py +2 -0
- camel/interpreters/docker/Dockerfile +3 -12
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/loaders/__init__.py +11 -2
- camel/loaders/chunkr_reader.py +9 -0
- camel/memories/__init__.py +2 -1
- camel/memories/agent_memories.py +3 -1
- camel/memories/blocks/chat_history_block.py +21 -3
- camel/memories/records.py +88 -8
- camel/messages/base.py +127 -34
- camel/models/__init__.py +4 -0
- camel/models/amd_model.py +101 -0
- camel/models/azure_openai_model.py +0 -6
- camel/models/base_model.py +30 -0
- camel/models/cometapi_model.py +83 -0
- camel/models/model_factory.py +4 -0
- camel/models/openai_compatible_model.py +0 -6
- camel/models/openai_model.py +0 -6
- camel/models/zhipuai_model.py +61 -2
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/retrievers/auto_retriever.py +1 -0
- camel/runtimes/daytona_runtime.py +11 -12
- camel/societies/workforce/prompts.py +131 -50
- camel/societies/workforce/single_agent_worker.py +434 -49
- camel/societies/workforce/structured_output_handler.py +30 -18
- camel/societies/workforce/task_channel.py +43 -0
- camel/societies/workforce/utils.py +105 -12
- camel/societies/workforce/workforce.py +1322 -311
- camel/societies/workforce/workforce_logger.py +24 -5
- camel/storages/key_value_storages/json.py +15 -2
- camel/storages/object_storages/google_cloud.py +1 -1
- camel/storages/vectordb_storages/oceanbase.py +10 -11
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/tasks/task.py +4 -3
- camel/toolkits/__init__.py +18 -5
- camel/toolkits/aci_toolkit.py +45 -0
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
- camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +194 -34
- camel/toolkits/function_tool.py +6 -1
- camel/toolkits/google_drive_mcp_toolkit.py +12 -31
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +12 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +79 -2
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +95 -59
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +619 -95
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +7 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +115 -219
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +1 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +39 -6
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +405 -131
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +9 -5
- camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +98 -31
- camel/toolkits/markitdown_toolkit.py +27 -1
- camel/toolkits/mcp_toolkit.py +348 -348
- camel/toolkits/message_integration.py +3 -0
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/note_taking_toolkit.py +18 -8
- camel/toolkits/notion_mcp_toolkit.py +16 -26
- camel/toolkits/origene_mcp_toolkit.py +8 -49
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/slack_toolkit.py +50 -1
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +924 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +17 -11
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/types/enums.py +124 -1
- camel/types/unified_model_type.py +5 -0
- camel/utils/commons.py +17 -0
- camel/utils/context_utils.py +804 -0
- camel/utils/mcp.py +136 -2
- camel/utils/token_counting.py +25 -17
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/METADATA +158 -59
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/RECORD +95 -76
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/terminal_toolkit.py +0 -1788
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/licenses/LICENSE +0 -0
camel/agents/chat_agent.py
CHANGED
|
@@ -14,15 +14,24 @@
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
+
import atexit
|
|
18
|
+
import base64
|
|
17
19
|
import concurrent.futures
|
|
20
|
+
import hashlib
|
|
21
|
+
import inspect
|
|
18
22
|
import json
|
|
19
|
-
import
|
|
20
|
-
import
|
|
23
|
+
import math
|
|
24
|
+
import os
|
|
21
25
|
import random
|
|
26
|
+
import re
|
|
27
|
+
import tempfile
|
|
22
28
|
import textwrap
|
|
23
29
|
import threading
|
|
24
30
|
import time
|
|
25
31
|
import uuid
|
|
32
|
+
import warnings
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from datetime import datetime
|
|
26
35
|
from pathlib import Path
|
|
27
36
|
from typing import (
|
|
28
37
|
TYPE_CHECKING,
|
|
@@ -63,6 +72,7 @@ from camel.memories import (
|
|
|
63
72
|
MemoryRecord,
|
|
64
73
|
ScoreBasedContextCreator,
|
|
65
74
|
)
|
|
75
|
+
from camel.memories.blocks.chat_history_block import EmptyMemoryWarning
|
|
66
76
|
from camel.messages import (
|
|
67
77
|
BaseMessage,
|
|
68
78
|
FunctionCallingMessage,
|
|
@@ -92,16 +102,31 @@ from camel.utils import (
|
|
|
92
102
|
model_from_json_schema,
|
|
93
103
|
)
|
|
94
104
|
from camel.utils.commons import dependencies_required
|
|
105
|
+
from camel.utils.context_utils import ContextUtility
|
|
95
106
|
|
|
96
107
|
if TYPE_CHECKING:
|
|
97
108
|
from camel.terminators import ResponseTerminator
|
|
98
109
|
|
|
99
110
|
logger = get_logger(__name__)
|
|
100
111
|
|
|
112
|
+
# Cleanup temp files on exit
|
|
113
|
+
_temp_files: Set[str] = set()
|
|
114
|
+
_temp_files_lock = threading.Lock()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _cleanup_temp_files():
|
|
118
|
+
with _temp_files_lock:
|
|
119
|
+
for path in _temp_files:
|
|
120
|
+
try:
|
|
121
|
+
os.unlink(path)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
atexit.register(_cleanup_temp_files)
|
|
127
|
+
|
|
101
128
|
# AgentOps decorator setting
|
|
102
129
|
try:
|
|
103
|
-
import os
|
|
104
|
-
|
|
105
130
|
if os.getenv("AGENTOPS_API_KEY") is not None:
|
|
106
131
|
from agentops import track_agent
|
|
107
132
|
else:
|
|
@@ -135,13 +160,60 @@ SIMPLE_FORMAT_PROMPT = TextPrompt(
|
|
|
135
160
|
)
|
|
136
161
|
|
|
137
162
|
|
|
163
|
+
@dataclass
|
|
164
|
+
class _ToolOutputHistoryEntry:
|
|
165
|
+
tool_name: str
|
|
166
|
+
tool_call_id: str
|
|
167
|
+
result_text: str
|
|
168
|
+
record_uuids: List[str]
|
|
169
|
+
record_timestamps: List[float]
|
|
170
|
+
preview_text: str
|
|
171
|
+
cached: bool = False
|
|
172
|
+
cache_id: Optional[str] = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _ToolOutputCacheManager:
|
|
176
|
+
r"""Minimal persistent store for caching verbose tool outputs."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, base_dir: Union[str, Path]) -> None:
|
|
179
|
+
self.base_dir = Path(base_dir).expanduser().resolve()
|
|
180
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
|
|
182
|
+
def save(
|
|
183
|
+
self,
|
|
184
|
+
tool_name: str,
|
|
185
|
+
tool_call_id: str,
|
|
186
|
+
content: str,
|
|
187
|
+
) -> Tuple[str, Path]:
|
|
188
|
+
cache_id = uuid.uuid4().hex
|
|
189
|
+
filename = f"{cache_id}.txt"
|
|
190
|
+
path = self.base_dir / filename
|
|
191
|
+
header = (
|
|
192
|
+
f"# Cached tool output\n"
|
|
193
|
+
f"tool_name: {tool_name}\n"
|
|
194
|
+
f"tool_call_id: {tool_call_id}\n"
|
|
195
|
+
f"cache_id: {cache_id}\n"
|
|
196
|
+
f"---\n"
|
|
197
|
+
)
|
|
198
|
+
path.write_text(f"{header}{content}", encoding="utf-8")
|
|
199
|
+
return cache_id, path
|
|
200
|
+
|
|
201
|
+
def load(self, cache_id: str) -> str:
|
|
202
|
+
path = self.base_dir / f"{cache_id}.txt"
|
|
203
|
+
if not path.exists():
|
|
204
|
+
raise FileNotFoundError(
|
|
205
|
+
f"Cached tool output {cache_id} not found at {path}"
|
|
206
|
+
)
|
|
207
|
+
return path.read_text(encoding="utf-8")
|
|
208
|
+
|
|
209
|
+
|
|
138
210
|
class StreamContentAccumulator:
|
|
139
211
|
r"""Manages content accumulation across streaming responses to ensure
|
|
140
212
|
all responses contain complete cumulative content."""
|
|
141
213
|
|
|
142
214
|
def __init__(self):
|
|
143
215
|
self.base_content = "" # Content before tool calls
|
|
144
|
-
self.current_content =
|
|
216
|
+
self.current_content = [] # Accumulated streaming fragments
|
|
145
217
|
self.tool_status_messages = [] # Accumulated tool status messages
|
|
146
218
|
|
|
147
219
|
def set_base_content(self, content: str):
|
|
@@ -150,7 +222,7 @@ class StreamContentAccumulator:
|
|
|
150
222
|
|
|
151
223
|
def add_streaming_content(self, new_content: str):
|
|
152
224
|
r"""Add new streaming content."""
|
|
153
|
-
self.current_content
|
|
225
|
+
self.current_content.append(new_content)
|
|
154
226
|
|
|
155
227
|
def add_tool_status(self, status_message: str):
|
|
156
228
|
r"""Add a tool status message."""
|
|
@@ -159,16 +231,18 @@ class StreamContentAccumulator:
|
|
|
159
231
|
def get_full_content(self) -> str:
|
|
160
232
|
r"""Get the complete accumulated content."""
|
|
161
233
|
tool_messages = "".join(self.tool_status_messages)
|
|
162
|
-
|
|
234
|
+
current = "".join(self.current_content)
|
|
235
|
+
return self.base_content + tool_messages + current
|
|
163
236
|
|
|
164
237
|
def get_content_with_new_status(self, status_message: str) -> str:
|
|
165
238
|
r"""Get content with a new status message appended."""
|
|
166
239
|
tool_messages = "".join([*self.tool_status_messages, status_message])
|
|
167
|
-
|
|
240
|
+
current = "".join(self.current_content)
|
|
241
|
+
return self.base_content + tool_messages + current
|
|
168
242
|
|
|
169
243
|
def reset_streaming_content(self):
|
|
170
244
|
r"""Reset only the streaming content, keep base and tool status."""
|
|
171
|
-
self.current_content =
|
|
245
|
+
self.current_content = []
|
|
172
246
|
|
|
173
247
|
|
|
174
248
|
class StreamingChatAgentResponse:
|
|
@@ -189,13 +263,10 @@ class StreamingChatAgentResponse:
|
|
|
189
263
|
def _ensure_latest_response(self):
|
|
190
264
|
r"""Ensure we have the latest response by consuming the generator."""
|
|
191
265
|
if not self._consumed:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
self._consumed = True
|
|
197
|
-
except StopIteration:
|
|
198
|
-
self._consumed = True
|
|
266
|
+
for response in self._generator:
|
|
267
|
+
self._responses.append(response)
|
|
268
|
+
self._current_response = response
|
|
269
|
+
self._consumed = True
|
|
199
270
|
|
|
200
271
|
@property
|
|
201
272
|
def msgs(self) -> List[BaseMessage]:
|
|
@@ -233,17 +304,14 @@ class StreamingChatAgentResponse:
|
|
|
233
304
|
r"""Make this object iterable."""
|
|
234
305
|
if self._consumed:
|
|
235
306
|
# If already consumed, iterate over stored responses
|
|
236
|
-
|
|
307
|
+
yield from self._responses
|
|
237
308
|
else:
|
|
238
309
|
# If not consumed, consume and yield
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
self._consumed = True
|
|
245
|
-
except StopIteration:
|
|
246
|
-
self._consumed = True
|
|
310
|
+
for response in self._generator:
|
|
311
|
+
self._responses.append(response)
|
|
312
|
+
self._current_response = response
|
|
313
|
+
yield response
|
|
314
|
+
self._consumed = True
|
|
247
315
|
|
|
248
316
|
def __getattr__(self, name):
|
|
249
317
|
r"""Forward any other attribute access to the latest response."""
|
|
@@ -274,13 +342,10 @@ class AsyncStreamingChatAgentResponse:
|
|
|
274
342
|
async def _ensure_latest_response(self):
|
|
275
343
|
r"""Ensure the latest response by consuming the async generator."""
|
|
276
344
|
if not self._consumed:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
self._consumed = True
|
|
282
|
-
except StopAsyncIteration:
|
|
283
|
-
self._consumed = True
|
|
345
|
+
async for response in self._async_generator:
|
|
346
|
+
self._responses.append(response)
|
|
347
|
+
self._current_response = response
|
|
348
|
+
self._consumed = True
|
|
284
349
|
|
|
285
350
|
async def _get_final_response(self) -> ChatAgentResponse:
|
|
286
351
|
r"""Get the final response after consuming the entire stream."""
|
|
@@ -306,14 +371,11 @@ class AsyncStreamingChatAgentResponse:
|
|
|
306
371
|
else:
|
|
307
372
|
# If not consumed, consume and yield
|
|
308
373
|
async def _consume_and_yield():
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
self._consumed = True
|
|
315
|
-
except StopAsyncIteration:
|
|
316
|
-
self._consumed = True
|
|
374
|
+
async for response in self._async_generator:
|
|
375
|
+
self._responses.append(response)
|
|
376
|
+
self._current_response = response
|
|
377
|
+
yield response
|
|
378
|
+
self._consumed = True
|
|
317
379
|
|
|
318
380
|
return _consume_and_yield()
|
|
319
381
|
|
|
@@ -381,14 +443,28 @@ class ChatAgent(BaseAgent):
|
|
|
381
443
|
for individual tool execution. If None, wait indefinitely.
|
|
382
444
|
mask_tool_output (Optional[bool]): Whether to return a sanitized
|
|
383
445
|
placeholder instead of the raw tool output. (default: :obj:`False`)
|
|
384
|
-
pause_event (Optional[asyncio.Event]): Event to
|
|
385
|
-
agent's operation. When clear, the agent will
|
|
386
|
-
|
|
446
|
+
pause_event (Optional[Union[threading.Event, asyncio.Event]]): Event to
|
|
447
|
+
signal pause of the agent's operation. When clear, the agent will
|
|
448
|
+
pause its execution. Use threading.Event for sync operations or
|
|
449
|
+
asyncio.Event for async operations. (default: :obj:`None`)
|
|
387
450
|
prune_tool_calls_from_memory (bool): Whether to clean tool
|
|
388
451
|
call messages from memory after response generation to save token
|
|
389
452
|
usage. When enabled, removes FUNCTION/TOOL role messages and
|
|
390
453
|
ASSISTANT messages with tool_calls after each step.
|
|
391
454
|
(default: :obj:`False`)
|
|
455
|
+
enable_tool_output_cache (bool, optional): Whether to offload verbose
|
|
456
|
+
historical tool outputs to a local cache and replace them with
|
|
457
|
+
lightweight references in memory. Only older tool results whose
|
|
458
|
+
payload length exceeds ``tool_output_cache_threshold`` are cached.
|
|
459
|
+
(default: :obj:`True`)
|
|
460
|
+
tool_output_cache_threshold (int, optional): Minimum character length
|
|
461
|
+
of a tool result before it becomes eligible for caching. Values
|
|
462
|
+
below or equal to zero disable caching regardless of the toggle.
|
|
463
|
+
(default: :obj:`2000`)
|
|
464
|
+
tool_output_cache_dir (Optional[Union[str, Path]], optional): Target
|
|
465
|
+
directory for cached tool outputs. When omitted, a ``tool_cache``
|
|
466
|
+
directory relative to the current working directory is used.
|
|
467
|
+
(default: :obj:`None`)
|
|
392
468
|
retry_attempts (int, optional): Maximum number of retry attempts for
|
|
393
469
|
rate limit errors. (default: :obj:`3`)
|
|
394
470
|
retry_delay (float, optional): Initial delay in seconds between
|
|
@@ -396,6 +472,10 @@ class ChatAgent(BaseAgent):
|
|
|
396
472
|
step_timeout (Optional[float], optional): Timeout in seconds for the
|
|
397
473
|
entire step operation. If None, no timeout is applied.
|
|
398
474
|
(default: :obj:`None`)
|
|
475
|
+
stream_accumulate (bool, optional): When True, partial streaming
|
|
476
|
+
updates return accumulated content (current behavior). When False,
|
|
477
|
+
partial updates return only the incremental delta. (default:
|
|
478
|
+
:obj:`True`)
|
|
399
479
|
"""
|
|
400
480
|
|
|
401
481
|
def __init__(
|
|
@@ -434,11 +514,15 @@ class ChatAgent(BaseAgent):
|
|
|
434
514
|
stop_event: Optional[threading.Event] = None,
|
|
435
515
|
tool_execution_timeout: Optional[float] = None,
|
|
436
516
|
mask_tool_output: bool = False,
|
|
437
|
-
pause_event: Optional[asyncio.Event] = None,
|
|
517
|
+
pause_event: Optional[Union[threading.Event, asyncio.Event]] = None,
|
|
438
518
|
prune_tool_calls_from_memory: bool = False,
|
|
519
|
+
enable_tool_output_cache: bool = True,
|
|
520
|
+
tool_output_cache_threshold: int = 2000,
|
|
521
|
+
tool_output_cache_dir: Optional[Union[str, Path]] = None,
|
|
439
522
|
retry_attempts: int = 3,
|
|
440
523
|
retry_delay: float = 1.0,
|
|
441
524
|
step_timeout: Optional[float] = None,
|
|
525
|
+
stream_accumulate: bool = True,
|
|
442
526
|
) -> None:
|
|
443
527
|
if isinstance(model, ModelManager):
|
|
444
528
|
self.model_backend = model
|
|
@@ -454,6 +538,28 @@ class ChatAgent(BaseAgent):
|
|
|
454
538
|
# Assign unique ID
|
|
455
539
|
self.agent_id = agent_id if agent_id else str(uuid.uuid4())
|
|
456
540
|
|
|
541
|
+
self._tool_output_cache_enabled = (
|
|
542
|
+
enable_tool_output_cache and tool_output_cache_threshold > 0
|
|
543
|
+
)
|
|
544
|
+
self._tool_output_cache_threshold = max(0, tool_output_cache_threshold)
|
|
545
|
+
self._tool_output_cache_dir: Optional[Path]
|
|
546
|
+
self._tool_output_cache_manager: Optional[_ToolOutputCacheManager]
|
|
547
|
+
if self._tool_output_cache_enabled:
|
|
548
|
+
cache_dir = (
|
|
549
|
+
Path(tool_output_cache_dir).expanduser()
|
|
550
|
+
if tool_output_cache_dir is not None
|
|
551
|
+
else Path("tool_cache")
|
|
552
|
+
)
|
|
553
|
+
self._tool_output_cache_dir = cache_dir
|
|
554
|
+
self._tool_output_cache_manager = _ToolOutputCacheManager(
|
|
555
|
+
cache_dir
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
self._tool_output_cache_dir = None
|
|
559
|
+
self._tool_output_cache_manager = None
|
|
560
|
+
self._tool_output_history: List[_ToolOutputHistoryEntry] = []
|
|
561
|
+
self._cache_lookup_tool_name = "retrieve_cached_tool_output"
|
|
562
|
+
|
|
457
563
|
# Set up memory
|
|
458
564
|
context_creator = ScoreBasedContextCreator(
|
|
459
565
|
self.model_backend.token_counter,
|
|
@@ -500,6 +606,8 @@ class ChatAgent(BaseAgent):
|
|
|
500
606
|
convert_to_function_tool(tool) for tool in (tools or [])
|
|
501
607
|
]
|
|
502
608
|
}
|
|
609
|
+
if self._tool_output_cache_enabled:
|
|
610
|
+
self._ensure_tool_cache_lookup_tool()
|
|
503
611
|
|
|
504
612
|
# Register agent with toolkits that have RegisteredAgentToolkit mixin
|
|
505
613
|
if toolkits_to_register_agent:
|
|
@@ -522,16 +630,22 @@ class ChatAgent(BaseAgent):
|
|
|
522
630
|
self.tool_execution_timeout = tool_execution_timeout
|
|
523
631
|
self.mask_tool_output = mask_tool_output
|
|
524
632
|
self._secure_result_store: Dict[str, Any] = {}
|
|
633
|
+
self._secure_result_store_lock = threading.Lock()
|
|
525
634
|
self.pause_event = pause_event
|
|
526
635
|
self.prune_tool_calls_from_memory = prune_tool_calls_from_memory
|
|
527
636
|
self.retry_attempts = max(1, retry_attempts)
|
|
528
637
|
self.retry_delay = max(0.0, retry_delay)
|
|
529
638
|
self.step_timeout = step_timeout
|
|
639
|
+
self._context_utility: Optional[ContextUtility] = None
|
|
640
|
+
self._context_summary_agent: Optional["ChatAgent"] = None
|
|
641
|
+
self.stream_accumulate = stream_accumulate
|
|
530
642
|
|
|
531
643
|
def reset(self):
|
|
532
644
|
r"""Resets the :obj:`ChatAgent` to its initial state."""
|
|
533
645
|
self.terminated = False
|
|
534
646
|
self.init_messages()
|
|
647
|
+
if self._tool_output_cache_enabled:
|
|
648
|
+
self._tool_output_history.clear()
|
|
535
649
|
for terminator in self.response_terminators:
|
|
536
650
|
terminator.reset()
|
|
537
651
|
|
|
@@ -715,6 +829,20 @@ class ChatAgent(BaseAgent):
|
|
|
715
829
|
# Ensure the new memory has the system message
|
|
716
830
|
self.init_messages()
|
|
717
831
|
|
|
832
|
+
def set_context_utility(
|
|
833
|
+
self, context_utility: Optional[ContextUtility]
|
|
834
|
+
) -> None:
|
|
835
|
+
r"""Set the context utility for the agent.
|
|
836
|
+
|
|
837
|
+
This allows external components (like SingleAgentWorker) to provide
|
|
838
|
+
a shared context utility instance for workflow management.
|
|
839
|
+
|
|
840
|
+
Args:
|
|
841
|
+
context_utility (ContextUtility, optional): The context utility
|
|
842
|
+
to use. If None, the agent will create its own when needed.
|
|
843
|
+
"""
|
|
844
|
+
self._context_utility = context_utility
|
|
845
|
+
|
|
718
846
|
def _get_full_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
719
847
|
r"""Returns a list of tool schemas of all tools, including internal
|
|
720
848
|
and external tools.
|
|
@@ -738,6 +866,178 @@ class ChatAgent(BaseAgent):
|
|
|
738
866
|
for tool in tools:
|
|
739
867
|
self.add_tool(tool)
|
|
740
868
|
|
|
869
|
+
def retrieve_cached_tool_output(self, cache_id: str) -> str:
|
|
870
|
+
r"""Load a cached tool output by its cache identifier.
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
cache_id (str): Identifier provided in cached tool messages.
|
|
874
|
+
|
|
875
|
+
Returns:
|
|
876
|
+
str: The cached content or an explanatory error message.
|
|
877
|
+
"""
|
|
878
|
+
if not self._tool_output_cache_manager:
|
|
879
|
+
return "Tool output caching is disabled for this agent instance."
|
|
880
|
+
|
|
881
|
+
normalized_cache_id = cache_id.strip()
|
|
882
|
+
if not normalized_cache_id:
|
|
883
|
+
return "Please provide a non-empty cache_id."
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
return self._tool_output_cache_manager.load(normalized_cache_id)
|
|
887
|
+
except FileNotFoundError:
|
|
888
|
+
return (
|
|
889
|
+
f"Cache entry '{normalized_cache_id}' was not found. "
|
|
890
|
+
"Verify the identifier and try again."
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
def _ensure_tool_cache_lookup_tool(self) -> None:
|
|
894
|
+
if not self._tool_output_cache_enabled:
|
|
895
|
+
return
|
|
896
|
+
lookup_name = self._cache_lookup_tool_name
|
|
897
|
+
if lookup_name in self._internal_tools:
|
|
898
|
+
return
|
|
899
|
+
lookup_tool = convert_to_function_tool(
|
|
900
|
+
self.retrieve_cached_tool_output
|
|
901
|
+
)
|
|
902
|
+
self._internal_tools[lookup_tool.get_function_name()] = lookup_tool
|
|
903
|
+
|
|
904
|
+
def _serialize_tool_result(self, result: Any) -> str:
|
|
905
|
+
if isinstance(result, str):
|
|
906
|
+
return result
|
|
907
|
+
try:
|
|
908
|
+
return json.dumps(result, ensure_ascii=False)
|
|
909
|
+
except (TypeError, ValueError):
|
|
910
|
+
return str(result)
|
|
911
|
+
|
|
912
|
+
def _summarize_tool_result(self, text: str, limit: int = 160) -> str:
|
|
913
|
+
normalized = re.sub(r"\s+", " ", text).strip()
|
|
914
|
+
if len(normalized) <= limit:
|
|
915
|
+
return normalized
|
|
916
|
+
return normalized[: max(0, limit - 3)].rstrip() + "..."
|
|
917
|
+
|
|
918
|
+
def _register_tool_output_for_cache(
|
|
919
|
+
self,
|
|
920
|
+
func_name: str,
|
|
921
|
+
tool_call_id: str,
|
|
922
|
+
result_text: str,
|
|
923
|
+
records: List[MemoryRecord],
|
|
924
|
+
) -> None:
|
|
925
|
+
if not records:
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
entry = _ToolOutputHistoryEntry(
|
|
929
|
+
tool_name=func_name,
|
|
930
|
+
tool_call_id=tool_call_id,
|
|
931
|
+
result_text=result_text,
|
|
932
|
+
record_uuids=[str(record.uuid) for record in records],
|
|
933
|
+
record_timestamps=[record.timestamp for record in records],
|
|
934
|
+
preview_text=self._summarize_tool_result(result_text),
|
|
935
|
+
)
|
|
936
|
+
self._tool_output_history.append(entry)
|
|
937
|
+
self._process_tool_output_cache()
|
|
938
|
+
|
|
939
|
+
def _process_tool_output_cache(self) -> None:
|
|
940
|
+
if (
|
|
941
|
+
not self._tool_output_cache_enabled
|
|
942
|
+
or not self._tool_output_history
|
|
943
|
+
or self._tool_output_cache_manager is None
|
|
944
|
+
):
|
|
945
|
+
return
|
|
946
|
+
|
|
947
|
+
# Only cache older results; keep the latest expanded for immediate use.
|
|
948
|
+
for entry in self._tool_output_history[:-1]:
|
|
949
|
+
if entry.cached:
|
|
950
|
+
continue
|
|
951
|
+
if len(entry.result_text) < self._tool_output_cache_threshold:
|
|
952
|
+
continue
|
|
953
|
+
self._cache_tool_output_entry(entry)
|
|
954
|
+
|
|
955
|
+
def _cache_tool_output_entry(self, entry: _ToolOutputHistoryEntry) -> None:
|
|
956
|
+
if self._tool_output_cache_manager is None or not entry.record_uuids:
|
|
957
|
+
return
|
|
958
|
+
|
|
959
|
+
try:
|
|
960
|
+
cache_id, cache_path = self._tool_output_cache_manager.save(
|
|
961
|
+
entry.tool_name,
|
|
962
|
+
entry.tool_call_id,
|
|
963
|
+
entry.result_text,
|
|
964
|
+
)
|
|
965
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
966
|
+
logger.warning(
|
|
967
|
+
"Failed to persist cached tool output for %s (%s): %s",
|
|
968
|
+
entry.tool_name,
|
|
969
|
+
entry.tool_call_id,
|
|
970
|
+
exc,
|
|
971
|
+
)
|
|
972
|
+
return
|
|
973
|
+
|
|
974
|
+
timestamp = (
|
|
975
|
+
entry.record_timestamps[0]
|
|
976
|
+
if entry.record_timestamps
|
|
977
|
+
else time.time_ns() / 1_000_000_000
|
|
978
|
+
)
|
|
979
|
+
reference_message = FunctionCallingMessage(
|
|
980
|
+
role_name=self.role_name,
|
|
981
|
+
role_type=self.role_type,
|
|
982
|
+
meta_dict={
|
|
983
|
+
"cache_id": cache_id,
|
|
984
|
+
"cached_preview": entry.preview_text,
|
|
985
|
+
"cached_tool_output_path": str(cache_path),
|
|
986
|
+
},
|
|
987
|
+
content="",
|
|
988
|
+
func_name=entry.tool_name,
|
|
989
|
+
result=self._build_cache_reference_text(entry, cache_id),
|
|
990
|
+
tool_call_id=entry.tool_call_id,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
chat_history_block = getattr(self.memory, "_chat_history_block", None)
|
|
994
|
+
storage = getattr(chat_history_block, "storage", None)
|
|
995
|
+
if storage is None:
|
|
996
|
+
return
|
|
997
|
+
|
|
998
|
+
existing_records = storage.load()
|
|
999
|
+
updated_records = [
|
|
1000
|
+
record
|
|
1001
|
+
for record in existing_records
|
|
1002
|
+
if record["uuid"] not in entry.record_uuids
|
|
1003
|
+
]
|
|
1004
|
+
new_record = MemoryRecord(
|
|
1005
|
+
message=reference_message,
|
|
1006
|
+
role_at_backend=OpenAIBackendRole.FUNCTION,
|
|
1007
|
+
timestamp=timestamp,
|
|
1008
|
+
agent_id=self.agent_id,
|
|
1009
|
+
)
|
|
1010
|
+
updated_records.append(new_record.to_dict())
|
|
1011
|
+
updated_records.sort(key=lambda record: record["timestamp"])
|
|
1012
|
+
storage.clear()
|
|
1013
|
+
storage.save(updated_records)
|
|
1014
|
+
|
|
1015
|
+
logger.info(
|
|
1016
|
+
"Cached tool output '%s' (%s) to %s with cache_id=%s",
|
|
1017
|
+
entry.tool_name,
|
|
1018
|
+
entry.tool_call_id,
|
|
1019
|
+
cache_path,
|
|
1020
|
+
cache_id,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
entry.cached = True
|
|
1024
|
+
entry.cache_id = cache_id
|
|
1025
|
+
entry.record_uuids = [str(new_record.uuid)]
|
|
1026
|
+
entry.record_timestamps = [timestamp]
|
|
1027
|
+
|
|
1028
|
+
def _build_cache_reference_text(
|
|
1029
|
+
self, entry: _ToolOutputHistoryEntry, cache_id: str
|
|
1030
|
+
) -> str:
|
|
1031
|
+
preview = entry.preview_text or "[no preview available]"
|
|
1032
|
+
return (
|
|
1033
|
+
"[cached tool output]\n"
|
|
1034
|
+
f"tool: {entry.tool_name}\n"
|
|
1035
|
+
f"cache_id: {cache_id}\n"
|
|
1036
|
+
f"preview: {preview}\n"
|
|
1037
|
+
f"Use `{self._cache_lookup_tool_name}` with this cache_id to "
|
|
1038
|
+
"retrieve the full content."
|
|
1039
|
+
)
|
|
1040
|
+
|
|
741
1041
|
def add_external_tool(
|
|
742
1042
|
self, tool: Union[FunctionTool, Callable, Dict[str, Any]]
|
|
743
1043
|
) -> None:
|
|
@@ -782,7 +1082,8 @@ class ChatAgent(BaseAgent):
|
|
|
782
1082
|
message: BaseMessage,
|
|
783
1083
|
role: OpenAIBackendRole,
|
|
784
1084
|
timestamp: Optional[float] = None,
|
|
785
|
-
|
|
1085
|
+
return_records: bool = False,
|
|
1086
|
+
) -> Optional[List[MemoryRecord]]:
|
|
786
1087
|
r"""Updates the agent memory with a new message.
|
|
787
1088
|
|
|
788
1089
|
If the single *message* exceeds the model's context window, it will
|
|
@@ -802,24 +1103,29 @@ class ChatAgent(BaseAgent):
|
|
|
802
1103
|
timestamp (Optional[float], optional): Custom timestamp for the
|
|
803
1104
|
memory record. If `None`, the current time will be used.
|
|
804
1105
|
(default: :obj:`None`)
|
|
805
|
-
|
|
1106
|
+
return_records (bool, optional): When ``True`` the method returns
|
|
1107
|
+
the list of :class:`MemoryRecord` objects written to memory.
|
|
1108
|
+
(default: :obj:`False`)
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
Optional[List[MemoryRecord]]: The records that were written when
|
|
1112
|
+
``return_records`` is ``True``; otherwise ``None``.
|
|
806
1113
|
"""
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
import uuid as _uuid
|
|
1114
|
+
|
|
1115
|
+
written_records: List[MemoryRecord] = []
|
|
810
1116
|
|
|
811
1117
|
# 1. Helper to write a record to memory
|
|
812
1118
|
def _write_single_record(
|
|
813
1119
|
message: BaseMessage, role: OpenAIBackendRole, timestamp: float
|
|
814
1120
|
):
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
agent_id=self.agent_id,
|
|
821
|
-
)
|
|
1121
|
+
record = MemoryRecord(
|
|
1122
|
+
message=message,
|
|
1123
|
+
role_at_backend=role,
|
|
1124
|
+
timestamp=timestamp,
|
|
1125
|
+
agent_id=self.agent_id,
|
|
822
1126
|
)
|
|
1127
|
+
written_records.append(record)
|
|
1128
|
+
self.memory.write_record(record)
|
|
823
1129
|
|
|
824
1130
|
base_ts = (
|
|
825
1131
|
timestamp
|
|
@@ -834,26 +1140,30 @@ class ChatAgent(BaseAgent):
|
|
|
834
1140
|
token_limit = context_creator.token_limit
|
|
835
1141
|
except AttributeError:
|
|
836
1142
|
_write_single_record(message, role, base_ts)
|
|
837
|
-
return
|
|
1143
|
+
return written_records if return_records else None
|
|
838
1144
|
|
|
839
1145
|
# 3. Check if slicing is necessary
|
|
840
1146
|
try:
|
|
841
1147
|
current_tokens = token_counter.count_tokens_from_messages(
|
|
842
1148
|
[message.to_openai_message(role)]
|
|
843
1149
|
)
|
|
844
|
-
|
|
1150
|
+
|
|
1151
|
+
with warnings.catch_warnings():
|
|
1152
|
+
warnings.filterwarnings("ignore", category=EmptyMemoryWarning)
|
|
1153
|
+
_, ctx_tokens = self.memory.get_context()
|
|
1154
|
+
|
|
845
1155
|
remaining_budget = max(0, token_limit - ctx_tokens)
|
|
846
1156
|
|
|
847
1157
|
if current_tokens <= remaining_budget:
|
|
848
1158
|
_write_single_record(message, role, base_ts)
|
|
849
|
-
return
|
|
1159
|
+
return written_records if return_records else None
|
|
850
1160
|
except Exception as e:
|
|
851
1161
|
logger.warning(
|
|
852
1162
|
f"Token calculation failed before chunking, "
|
|
853
1163
|
f"writing message as-is. Error: {e}"
|
|
854
1164
|
)
|
|
855
1165
|
_write_single_record(message, role, base_ts)
|
|
856
|
-
return
|
|
1166
|
+
return written_records if return_records else None
|
|
857
1167
|
|
|
858
1168
|
# 4. Perform slicing
|
|
859
1169
|
logger.warning(
|
|
@@ -874,18 +1184,18 @@ class ChatAgent(BaseAgent):
|
|
|
874
1184
|
|
|
875
1185
|
if not text_to_chunk or not text_to_chunk.strip():
|
|
876
1186
|
_write_single_record(message, role, base_ts)
|
|
877
|
-
return
|
|
1187
|
+
return written_records if return_records else None
|
|
878
1188
|
# Encode the entire text to get a list of all token IDs
|
|
879
1189
|
try:
|
|
880
1190
|
all_token_ids = token_counter.encode(text_to_chunk)
|
|
881
1191
|
except Exception as e:
|
|
882
1192
|
logger.error(f"Failed to encode text for chunking: {e}")
|
|
883
1193
|
_write_single_record(message, role, base_ts) # Fallback
|
|
884
|
-
return
|
|
1194
|
+
return written_records if return_records else None
|
|
885
1195
|
|
|
886
1196
|
if not all_token_ids:
|
|
887
1197
|
_write_single_record(message, role, base_ts) # Nothing to chunk
|
|
888
|
-
return
|
|
1198
|
+
return written_records if return_records else None
|
|
889
1199
|
|
|
890
1200
|
# 1. Base chunk size: one-tenth of the smaller of (a) total token
|
|
891
1201
|
# limit and (b) current remaining budget. This prevents us from
|
|
@@ -910,7 +1220,7 @@ class ChatAgent(BaseAgent):
|
|
|
910
1220
|
|
|
911
1221
|
# 4. Calculate how many chunks we will need with this body size.
|
|
912
1222
|
num_chunks = math.ceil(len(all_token_ids) / chunk_body_limit)
|
|
913
|
-
group_id = str(
|
|
1223
|
+
group_id = str(uuid.uuid4())
|
|
914
1224
|
|
|
915
1225
|
for i in range(num_chunks):
|
|
916
1226
|
start_idx = i * chunk_body_limit
|
|
@@ -951,6 +1261,8 @@ class ChatAgent(BaseAgent):
|
|
|
951
1261
|
# Increment timestamp slightly to maintain order
|
|
952
1262
|
_write_single_record(new_msg, role, base_ts + i * 1e-6)
|
|
953
1263
|
|
|
1264
|
+
return written_records if return_records else None
|
|
1265
|
+
|
|
954
1266
|
def load_memory(self, memory: AgentMemory) -> None:
|
|
955
1267
|
r"""Load the provided memory into the agent.
|
|
956
1268
|
|
|
@@ -1028,6 +1340,242 @@ class ChatAgent(BaseAgent):
|
|
|
1028
1340
|
json_store.save(to_save)
|
|
1029
1341
|
logger.info(f"Memory saved to {path}")
|
|
1030
1342
|
|
|
1343
|
+
def summarize(
|
|
1344
|
+
self,
|
|
1345
|
+
filename: Optional[str] = None,
|
|
1346
|
+
summary_prompt: Optional[str] = None,
|
|
1347
|
+
response_format: Optional[Type[BaseModel]] = None,
|
|
1348
|
+
working_directory: Optional[Union[str, Path]] = None,
|
|
1349
|
+
) -> Dict[str, Any]:
|
|
1350
|
+
r"""Summarize the agent's current conversation context and persist it
|
|
1351
|
+
to a markdown file.
|
|
1352
|
+
|
|
1353
|
+
Args:
|
|
1354
|
+
filename (Optional[str]): The base filename (without extension) to
|
|
1355
|
+
use for the markdown file. Defaults to a timestamped name when
|
|
1356
|
+
not provided.
|
|
1357
|
+
summary_prompt (Optional[str]): Custom prompt for the summarizer.
|
|
1358
|
+
When omitted, a default prompt highlighting key decisions,
|
|
1359
|
+
action items, and open questions is used.
|
|
1360
|
+
response_format (Optional[Type[BaseModel]]): A Pydantic model
|
|
1361
|
+
defining the expected structure of the response. If provided,
|
|
1362
|
+
the summary will be generated as structured output and included
|
|
1363
|
+
in the result.
|
|
1364
|
+
working_directory (Optional[str|Path]): Optional directory to save
|
|
1365
|
+
the markdown summary file. If provided, overrides the default
|
|
1366
|
+
directory used by ContextUtility.
|
|
1367
|
+
|
|
1368
|
+
Returns:
|
|
1369
|
+
Dict[str, Any]: A dictionary containing the summary text, file
|
|
1370
|
+
path, status message, and optionally structured_summary if
|
|
1371
|
+
response_format was provided.
|
|
1372
|
+
"""
|
|
1373
|
+
|
|
1374
|
+
result: Dict[str, Any] = {
|
|
1375
|
+
"summary": "",
|
|
1376
|
+
"file_path": None,
|
|
1377
|
+
"status": "",
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
try:
|
|
1381
|
+
# Use external context if set, otherwise create local one
|
|
1382
|
+
if self._context_utility is None:
|
|
1383
|
+
if working_directory is not None:
|
|
1384
|
+
self._context_utility = ContextUtility(
|
|
1385
|
+
working_directory=str(working_directory)
|
|
1386
|
+
)
|
|
1387
|
+
else:
|
|
1388
|
+
self._context_utility = ContextUtility()
|
|
1389
|
+
context_util = self._context_utility
|
|
1390
|
+
|
|
1391
|
+
# Get conversation directly from agent's memory
|
|
1392
|
+
messages, _ = self.memory.get_context()
|
|
1393
|
+
|
|
1394
|
+
if not messages:
|
|
1395
|
+
status_message = (
|
|
1396
|
+
"No conversation context available to summarize."
|
|
1397
|
+
)
|
|
1398
|
+
result["status"] = status_message
|
|
1399
|
+
return result
|
|
1400
|
+
|
|
1401
|
+
# Convert messages to conversation text
|
|
1402
|
+
conversation_lines = []
|
|
1403
|
+
for message in messages:
|
|
1404
|
+
role = message.get('role', 'unknown')
|
|
1405
|
+
content = message.get('content', '')
|
|
1406
|
+
|
|
1407
|
+
# Handle tool call messages (assistant calling tools)
|
|
1408
|
+
tool_calls = message.get('tool_calls')
|
|
1409
|
+
if tool_calls and isinstance(tool_calls, (list, tuple)):
|
|
1410
|
+
for tool_call in tool_calls:
|
|
1411
|
+
# Handle both dict and object formats
|
|
1412
|
+
if isinstance(tool_call, dict):
|
|
1413
|
+
func_name = tool_call.get('function', {}).get(
|
|
1414
|
+
'name', 'unknown_tool'
|
|
1415
|
+
)
|
|
1416
|
+
func_args_str = tool_call.get('function', {}).get(
|
|
1417
|
+
'arguments', '{}'
|
|
1418
|
+
)
|
|
1419
|
+
else:
|
|
1420
|
+
# Handle object format (Pydantic or similar)
|
|
1421
|
+
func_name = getattr(
|
|
1422
|
+
getattr(tool_call, 'function', None),
|
|
1423
|
+
'name',
|
|
1424
|
+
'unknown_tool',
|
|
1425
|
+
)
|
|
1426
|
+
func_args_str = getattr(
|
|
1427
|
+
getattr(tool_call, 'function', None),
|
|
1428
|
+
'arguments',
|
|
1429
|
+
'{}',
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
# Parse and format arguments for readability
|
|
1433
|
+
try:
|
|
1434
|
+
import json
|
|
1435
|
+
|
|
1436
|
+
args_dict = json.loads(func_args_str)
|
|
1437
|
+
args_formatted = ', '.join(
|
|
1438
|
+
f"{k}={v}" for k, v in args_dict.items()
|
|
1439
|
+
)
|
|
1440
|
+
except (json.JSONDecodeError, ValueError, TypeError):
|
|
1441
|
+
args_formatted = func_args_str
|
|
1442
|
+
|
|
1443
|
+
conversation_lines.append(
|
|
1444
|
+
f"[TOOL CALL] {func_name}({args_formatted})"
|
|
1445
|
+
)
|
|
1446
|
+
|
|
1447
|
+
# Handle tool response messages
|
|
1448
|
+
elif role == 'tool':
|
|
1449
|
+
tool_name = message.get('name', 'unknown_tool')
|
|
1450
|
+
if not content:
|
|
1451
|
+
content = str(message.get('content', ''))
|
|
1452
|
+
conversation_lines.append(
|
|
1453
|
+
f"[TOOL RESULT] {tool_name} → {content}"
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
# Handle regular content messages (user/assistant/system)
|
|
1457
|
+
elif content:
|
|
1458
|
+
conversation_lines.append(f"{role}: {content}")
|
|
1459
|
+
|
|
1460
|
+
conversation_text = "\n".join(conversation_lines).strip()
|
|
1461
|
+
|
|
1462
|
+
if not conversation_text:
|
|
1463
|
+
status_message = (
|
|
1464
|
+
"Conversation context is empty; skipping summary."
|
|
1465
|
+
)
|
|
1466
|
+
result["status"] = status_message
|
|
1467
|
+
return result
|
|
1468
|
+
|
|
1469
|
+
if self._context_summary_agent is None:
|
|
1470
|
+
self._context_summary_agent = ChatAgent(
|
|
1471
|
+
system_message=(
|
|
1472
|
+
"You are a helpful assistant that summarizes "
|
|
1473
|
+
"conversations"
|
|
1474
|
+
),
|
|
1475
|
+
model=self.model_backend,
|
|
1476
|
+
agent_id=f"{self.agent_id}_context_summarizer",
|
|
1477
|
+
)
|
|
1478
|
+
else:
|
|
1479
|
+
self._context_summary_agent.reset()
|
|
1480
|
+
|
|
1481
|
+
if summary_prompt:
|
|
1482
|
+
prompt_text = (
|
|
1483
|
+
f"{summary_prompt.rstrip()}\n\n"
|
|
1484
|
+
f"AGENT CONVERSATION TO BE SUMMARIZED:\n"
|
|
1485
|
+
f"{conversation_text}"
|
|
1486
|
+
)
|
|
1487
|
+
else:
|
|
1488
|
+
prompt_text = (
|
|
1489
|
+
"Summarize the context information in concise markdown "
|
|
1490
|
+
"bullet points highlighting key decisions, action items.\n"
|
|
1491
|
+
f"Context information:\n{conversation_text}"
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
try:
|
|
1495
|
+
# Use structured output if response_format is provided
|
|
1496
|
+
if response_format:
|
|
1497
|
+
response = self._context_summary_agent.step(
|
|
1498
|
+
prompt_text, response_format=response_format
|
|
1499
|
+
)
|
|
1500
|
+
else:
|
|
1501
|
+
response = self._context_summary_agent.step(prompt_text)
|
|
1502
|
+
except Exception as step_exc:
|
|
1503
|
+
error_message = (
|
|
1504
|
+
f"Failed to generate summary using model: {step_exc}"
|
|
1505
|
+
)
|
|
1506
|
+
logger.error(error_message)
|
|
1507
|
+
result["status"] = error_message
|
|
1508
|
+
return result
|
|
1509
|
+
|
|
1510
|
+
if not response.msgs:
|
|
1511
|
+
status_message = (
|
|
1512
|
+
"Failed to generate summary from model response."
|
|
1513
|
+
)
|
|
1514
|
+
result["status"] = status_message
|
|
1515
|
+
return result
|
|
1516
|
+
|
|
1517
|
+
summary_content = response.msgs[-1].content.strip()
|
|
1518
|
+
if not summary_content:
|
|
1519
|
+
status_message = "Generated summary is empty."
|
|
1520
|
+
result["status"] = status_message
|
|
1521
|
+
return result
|
|
1522
|
+
|
|
1523
|
+
base_filename = (
|
|
1524
|
+
filename
|
|
1525
|
+
if filename
|
|
1526
|
+
else f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
|
|
1527
|
+
)
|
|
1528
|
+
base_filename = Path(base_filename).with_suffix("").name
|
|
1529
|
+
|
|
1530
|
+
metadata = context_util.get_session_metadata()
|
|
1531
|
+
metadata.update(
|
|
1532
|
+
{
|
|
1533
|
+
"agent_id": self.agent_id,
|
|
1534
|
+
"message_count": len(messages),
|
|
1535
|
+
}
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
# Handle structured output if response_format was provided
|
|
1539
|
+
structured_output = None
|
|
1540
|
+
if response_format and response.msgs[-1].parsed:
|
|
1541
|
+
structured_output = response.msgs[-1].parsed
|
|
1542
|
+
# Convert structured output to custom markdown
|
|
1543
|
+
summary_content = context_util.structured_output_to_markdown(
|
|
1544
|
+
structured_data=structured_output, metadata=metadata
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
# Save the markdown (either custom structured or default)
|
|
1548
|
+
save_status = context_util.save_markdown_file(
|
|
1549
|
+
base_filename,
|
|
1550
|
+
summary_content,
|
|
1551
|
+
title="Conversation Summary"
|
|
1552
|
+
if not structured_output
|
|
1553
|
+
else None,
|
|
1554
|
+
metadata=metadata if not structured_output else None,
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
file_path = (
|
|
1558
|
+
context_util.get_working_directory() / f"{base_filename}.md"
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
# Prepare result dictionary
|
|
1562
|
+
result_dict = {
|
|
1563
|
+
"summary": summary_content,
|
|
1564
|
+
"file_path": str(file_path),
|
|
1565
|
+
"status": save_status,
|
|
1566
|
+
"structured_summary": structured_output,
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
result.update(result_dict)
|
|
1570
|
+
logger.info("Conversation summary saved to %s", file_path)
|
|
1571
|
+
return result
|
|
1572
|
+
|
|
1573
|
+
except Exception as exc:
|
|
1574
|
+
error_message = f"Failed to summarize conversation context: {exc}"
|
|
1575
|
+
logger.error(error_message)
|
|
1576
|
+
result["status"] = error_message
|
|
1577
|
+
return result
|
|
1578
|
+
|
|
1031
1579
|
def clear_memory(self) -> None:
|
|
1032
1580
|
r"""Clear the agent's memory and reset to initial state.
|
|
1033
1581
|
|
|
@@ -1035,6 +1583,9 @@ class ChatAgent(BaseAgent):
|
|
|
1035
1583
|
None
|
|
1036
1584
|
"""
|
|
1037
1585
|
self.memory.clear()
|
|
1586
|
+
if self._tool_output_cache_enabled:
|
|
1587
|
+
self._tool_output_history.clear()
|
|
1588
|
+
|
|
1038
1589
|
if self.system_message is not None:
|
|
1039
1590
|
self.update_memory(self.system_message, OpenAIBackendRole.SYSTEM)
|
|
1040
1591
|
|
|
@@ -1070,8 +1621,6 @@ class ChatAgent(BaseAgent):
|
|
|
1070
1621
|
r"""Initializes the stored messages list with the current system
|
|
1071
1622
|
message.
|
|
1072
1623
|
"""
|
|
1073
|
-
import time
|
|
1074
|
-
|
|
1075
1624
|
self.memory.clear()
|
|
1076
1625
|
# avoid UserWarning: The `ChatHistoryMemory` is empty.
|
|
1077
1626
|
if self.system_message is not None:
|
|
@@ -1084,6 +1633,17 @@ class ChatAgent(BaseAgent):
|
|
|
1084
1633
|
)
|
|
1085
1634
|
)
|
|
1086
1635
|
|
|
1636
|
+
def reset_to_original_system_message(self) -> None:
|
|
1637
|
+
r"""Reset system message to original, removing any appended context.
|
|
1638
|
+
|
|
1639
|
+
This method reverts the agent's system message back to its original
|
|
1640
|
+
state, removing any workflow context or other modifications that may
|
|
1641
|
+
have been appended. Useful for resetting agent state in multi-turn
|
|
1642
|
+
scenarios.
|
|
1643
|
+
"""
|
|
1644
|
+
self._system_message = self._original_system_message
|
|
1645
|
+
self.init_messages()
|
|
1646
|
+
|
|
1087
1647
|
def record_message(self, message: BaseMessage) -> None:
|
|
1088
1648
|
r"""Records the externally provided message into the agent memory as if
|
|
1089
1649
|
it were an answer of the :obj:`ChatAgent` from the backend. Currently,
|
|
@@ -1145,7 +1705,7 @@ class ChatAgent(BaseAgent):
|
|
|
1145
1705
|
|
|
1146
1706
|
# Create a prompt based on the schema
|
|
1147
1707
|
format_instruction = (
|
|
1148
|
-
"\n\nPlease respond in the following JSON format:\n
|
|
1708
|
+
"\n\nPlease respond in the following JSON format:\n{\n"
|
|
1149
1709
|
)
|
|
1150
1710
|
|
|
1151
1711
|
properties = schema.get("properties", {})
|
|
@@ -1232,6 +1792,33 @@ class ChatAgent(BaseAgent):
|
|
|
1232
1792
|
# and True to indicate we used prompt formatting
|
|
1233
1793
|
return modified_message, None, True
|
|
1234
1794
|
|
|
1795
|
+
def _is_called_from_registered_toolkit(self) -> bool:
|
|
1796
|
+
r"""Check if current step/astep call originates from a
|
|
1797
|
+
RegisteredAgentToolkit.
|
|
1798
|
+
|
|
1799
|
+
This method uses stack inspection to detect if the current call
|
|
1800
|
+
is originating from a toolkit that inherits from
|
|
1801
|
+
RegisteredAgentToolkit. When detected, tools should be disabled to
|
|
1802
|
+
prevent recursive calls.
|
|
1803
|
+
|
|
1804
|
+
Returns:
|
|
1805
|
+
bool: True if called from a RegisteredAgentToolkit, False otherwise
|
|
1806
|
+
"""
|
|
1807
|
+
from camel.toolkits.base import RegisteredAgentToolkit
|
|
1808
|
+
|
|
1809
|
+
try:
|
|
1810
|
+
for frame_info in inspect.stack():
|
|
1811
|
+
frame_locals = frame_info.frame.f_locals
|
|
1812
|
+
if 'self' in frame_locals:
|
|
1813
|
+
caller_self = frame_locals['self']
|
|
1814
|
+
if isinstance(caller_self, RegisteredAgentToolkit):
|
|
1815
|
+
return True
|
|
1816
|
+
|
|
1817
|
+
except Exception:
|
|
1818
|
+
return False
|
|
1819
|
+
|
|
1820
|
+
return False
|
|
1821
|
+
|
|
1235
1822
|
def _apply_prompt_based_parsing(
|
|
1236
1823
|
self,
|
|
1237
1824
|
response: ModelResponse,
|
|
@@ -1248,7 +1835,6 @@ class ChatAgent(BaseAgent):
|
|
|
1248
1835
|
try:
|
|
1249
1836
|
# Try to extract JSON from the response content
|
|
1250
1837
|
import json
|
|
1251
|
-
import re
|
|
1252
1838
|
|
|
1253
1839
|
from pydantic import ValidationError
|
|
1254
1840
|
|
|
@@ -1287,8 +1873,7 @@ class ChatAgent(BaseAgent):
|
|
|
1287
1873
|
|
|
1288
1874
|
if not message.parsed:
|
|
1289
1875
|
logger.warning(
|
|
1290
|
-
f"Failed to parse JSON from response: "
|
|
1291
|
-
f"{content}"
|
|
1876
|
+
f"Failed to parse JSON from response: {content}"
|
|
1292
1877
|
)
|
|
1293
1878
|
|
|
1294
1879
|
except Exception as e:
|
|
@@ -1425,6 +2010,10 @@ class ChatAgent(BaseAgent):
|
|
|
1425
2010
|
except ImportError:
|
|
1426
2011
|
pass # Langfuse not available
|
|
1427
2012
|
|
|
2013
|
+
# Check if this call is from a RegisteredAgentToolkit to prevent tool
|
|
2014
|
+
# use
|
|
2015
|
+
disable_tools = self._is_called_from_registered_toolkit()
|
|
2016
|
+
|
|
1428
2017
|
# Handle response format compatibility with non-strict tools
|
|
1429
2018
|
original_response_format = response_format
|
|
1430
2019
|
input_message, response_format, used_prompt_formatting = (
|
|
@@ -1456,8 +2045,13 @@ class ChatAgent(BaseAgent):
|
|
|
1456
2045
|
|
|
1457
2046
|
while True:
|
|
1458
2047
|
if self.pause_event is not None and not self.pause_event.is_set():
|
|
1459
|
-
|
|
1460
|
-
|
|
2048
|
+
# Use efficient blocking wait for threading.Event
|
|
2049
|
+
if isinstance(self.pause_event, threading.Event):
|
|
2050
|
+
self.pause_event.wait()
|
|
2051
|
+
else:
|
|
2052
|
+
# Fallback for asyncio.Event in sync context
|
|
2053
|
+
while not self.pause_event.is_set():
|
|
2054
|
+
time.sleep(0.001)
|
|
1461
2055
|
|
|
1462
2056
|
try:
|
|
1463
2057
|
openai_messages, num_tokens = self.memory.get_context()
|
|
@@ -1472,7 +2066,9 @@ class ChatAgent(BaseAgent):
|
|
|
1472
2066
|
num_tokens=num_tokens,
|
|
1473
2067
|
current_iteration=iteration_count,
|
|
1474
2068
|
response_format=response_format,
|
|
1475
|
-
tool_schemas=
|
|
2069
|
+
tool_schemas=[]
|
|
2070
|
+
if disable_tools
|
|
2071
|
+
else self._get_full_tool_schemas(),
|
|
1476
2072
|
prev_num_openai_messages=prev_num_openai_messages,
|
|
1477
2073
|
)
|
|
1478
2074
|
prev_num_openai_messages = len(openai_messages)
|
|
@@ -1487,7 +2083,7 @@ class ChatAgent(BaseAgent):
|
|
|
1487
2083
|
if self.stop_event and self.stop_event.is_set():
|
|
1488
2084
|
# Use the _step_terminate to terminate the agent with reason
|
|
1489
2085
|
logger.info(
|
|
1490
|
-
f"Termination triggered at iteration
|
|
2086
|
+
f"Termination triggered at iteration {iteration_count}"
|
|
1491
2087
|
)
|
|
1492
2088
|
return self._step_terminate(
|
|
1493
2089
|
accumulated_context_tokens,
|
|
@@ -1510,8 +2106,11 @@ class ChatAgent(BaseAgent):
|
|
|
1510
2106
|
self.pause_event is not None
|
|
1511
2107
|
and not self.pause_event.is_set()
|
|
1512
2108
|
):
|
|
1513
|
-
|
|
1514
|
-
|
|
2109
|
+
if isinstance(self.pause_event, threading.Event):
|
|
2110
|
+
self.pause_event.wait()
|
|
2111
|
+
else:
|
|
2112
|
+
while not self.pause_event.is_set():
|
|
2113
|
+
time.sleep(0.001)
|
|
1515
2114
|
result = self._execute_tool(tool_call_request)
|
|
1516
2115
|
tool_call_records.append(result)
|
|
1517
2116
|
|
|
@@ -1637,6 +2236,10 @@ class ChatAgent(BaseAgent):
|
|
|
1637
2236
|
except ImportError:
|
|
1638
2237
|
pass # Langfuse not available
|
|
1639
2238
|
|
|
2239
|
+
# Check if this call is from a RegisteredAgentToolkit to prevent tool
|
|
2240
|
+
# use
|
|
2241
|
+
disable_tools = self._is_called_from_registered_toolkit()
|
|
2242
|
+
|
|
1640
2243
|
# Handle response format compatibility with non-strict tools
|
|
1641
2244
|
original_response_format = response_format
|
|
1642
2245
|
input_message, response_format, used_prompt_formatting = (
|
|
@@ -1664,7 +2267,12 @@ class ChatAgent(BaseAgent):
|
|
|
1664
2267
|
prev_num_openai_messages: int = 0
|
|
1665
2268
|
while True:
|
|
1666
2269
|
if self.pause_event is not None and not self.pause_event.is_set():
|
|
1667
|
-
|
|
2270
|
+
if isinstance(self.pause_event, asyncio.Event):
|
|
2271
|
+
await self.pause_event.wait()
|
|
2272
|
+
elif isinstance(self.pause_event, threading.Event):
|
|
2273
|
+
# For threading.Event in async context, run in executor
|
|
2274
|
+
loop = asyncio.get_event_loop()
|
|
2275
|
+
await loop.run_in_executor(None, self.pause_event.wait)
|
|
1668
2276
|
try:
|
|
1669
2277
|
openai_messages, num_tokens = self.memory.get_context()
|
|
1670
2278
|
accumulated_context_tokens += num_tokens
|
|
@@ -1672,13 +2280,14 @@ class ChatAgent(BaseAgent):
|
|
|
1672
2280
|
return self._step_terminate(
|
|
1673
2281
|
e.args[1], tool_call_records, "max_tokens_exceeded"
|
|
1674
2282
|
)
|
|
1675
|
-
|
|
1676
2283
|
response = await self._aget_model_response(
|
|
1677
2284
|
openai_messages,
|
|
1678
2285
|
num_tokens=num_tokens,
|
|
1679
2286
|
current_iteration=iteration_count,
|
|
1680
2287
|
response_format=response_format,
|
|
1681
|
-
tool_schemas=
|
|
2288
|
+
tool_schemas=[]
|
|
2289
|
+
if disable_tools
|
|
2290
|
+
else self._get_full_tool_schemas(),
|
|
1682
2291
|
prev_num_openai_messages=prev_num_openai_messages,
|
|
1683
2292
|
)
|
|
1684
2293
|
prev_num_openai_messages = len(openai_messages)
|
|
@@ -1693,7 +2302,7 @@ class ChatAgent(BaseAgent):
|
|
|
1693
2302
|
if self.stop_event and self.stop_event.is_set():
|
|
1694
2303
|
# Use the _step_terminate to terminate the agent with reason
|
|
1695
2304
|
logger.info(
|
|
1696
|
-
f"Termination triggered at iteration
|
|
2305
|
+
f"Termination triggered at iteration {iteration_count}"
|
|
1697
2306
|
)
|
|
1698
2307
|
return self._step_terminate(
|
|
1699
2308
|
accumulated_context_tokens,
|
|
@@ -1716,7 +2325,13 @@ class ChatAgent(BaseAgent):
|
|
|
1716
2325
|
self.pause_event is not None
|
|
1717
2326
|
and not self.pause_event.is_set()
|
|
1718
2327
|
):
|
|
1719
|
-
|
|
2328
|
+
if isinstance(self.pause_event, asyncio.Event):
|
|
2329
|
+
await self.pause_event.wait()
|
|
2330
|
+
elif isinstance(self.pause_event, threading.Event):
|
|
2331
|
+
loop = asyncio.get_event_loop()
|
|
2332
|
+
await loop.run_in_executor(
|
|
2333
|
+
None, self.pause_event.wait
|
|
2334
|
+
)
|
|
1720
2335
|
tool_call_record = await self._aexecute_tool(
|
|
1721
2336
|
tool_call_request
|
|
1722
2337
|
)
|
|
@@ -1969,11 +2584,6 @@ class ChatAgent(BaseAgent):
|
|
|
1969
2584
|
Returns:
|
|
1970
2585
|
List[OpenAIMessage]: The sanitized OpenAI messages.
|
|
1971
2586
|
"""
|
|
1972
|
-
import hashlib
|
|
1973
|
-
import os
|
|
1974
|
-
import re
|
|
1975
|
-
import tempfile
|
|
1976
|
-
|
|
1977
2587
|
# Create a copy of messages for logging to avoid modifying the
|
|
1978
2588
|
# original messages
|
|
1979
2589
|
sanitized_messages = []
|
|
@@ -2014,7 +2624,14 @@ class ChatAgent(BaseAgent):
|
|
|
2014
2624
|
|
|
2015
2625
|
# Save image to temp directory for viewing
|
|
2016
2626
|
try:
|
|
2017
|
-
|
|
2627
|
+
# Sanitize img_format to prevent path
|
|
2628
|
+
# traversal
|
|
2629
|
+
safe_format = re.sub(
|
|
2630
|
+
r'[^a-zA-Z0-9]', '', img_format
|
|
2631
|
+
)[:10]
|
|
2632
|
+
img_filename = (
|
|
2633
|
+
f"image_{img_hash}.{safe_format}"
|
|
2634
|
+
)
|
|
2018
2635
|
|
|
2019
2636
|
temp_dir = tempfile.gettempdir()
|
|
2020
2637
|
img_path = os.path.join(
|
|
@@ -2029,6 +2646,9 @@ class ChatAgent(BaseAgent):
|
|
|
2029
2646
|
base64_data
|
|
2030
2647
|
)
|
|
2031
2648
|
)
|
|
2649
|
+
# Register for cleanup
|
|
2650
|
+
with _temp_files_lock:
|
|
2651
|
+
_temp_files.add(img_path)
|
|
2032
2652
|
|
|
2033
2653
|
# Create a file:// URL that can be
|
|
2034
2654
|
# opened
|
|
@@ -2281,7 +2901,8 @@ class ChatAgent(BaseAgent):
|
|
|
2281
2901
|
try:
|
|
2282
2902
|
raw_result = tool(**args)
|
|
2283
2903
|
if self.mask_tool_output:
|
|
2284
|
-
self.
|
|
2904
|
+
with self._secure_result_store_lock:
|
|
2905
|
+
self._secure_result_store[tool_call_id] = raw_result
|
|
2285
2906
|
result = (
|
|
2286
2907
|
"[The tool has been executed successfully, but the output"
|
|
2287
2908
|
" from the tool is masked. You can move forward]"
|
|
@@ -2339,7 +2960,7 @@ class ChatAgent(BaseAgent):
|
|
|
2339
2960
|
# Capture the error message to prevent framework crash
|
|
2340
2961
|
error_msg = f"Error executing async tool '{func_name}': {e!s}"
|
|
2341
2962
|
result = f"Tool execution failed: {error_msg}"
|
|
2342
|
-
|
|
2963
|
+
logger.warning(error_msg)
|
|
2343
2964
|
return self._record_tool_calling(func_name, args, result, tool_call_id)
|
|
2344
2965
|
|
|
2345
2966
|
def _record_tool_calling(
|
|
@@ -2390,20 +3011,22 @@ class ChatAgent(BaseAgent):
|
|
|
2390
3011
|
# This ensures the assistant message (tool call) always appears before
|
|
2391
3012
|
# the function message (tool result) in the conversation context
|
|
2392
3013
|
# Use time.time_ns() for nanosecond precision to avoid collisions
|
|
2393
|
-
import time
|
|
2394
|
-
|
|
2395
3014
|
current_time_ns = time.time_ns()
|
|
2396
3015
|
base_timestamp = current_time_ns / 1_000_000_000 # Convert to seconds
|
|
2397
3016
|
|
|
2398
3017
|
self.update_memory(
|
|
2399
|
-
assist_msg,
|
|
3018
|
+
assist_msg,
|
|
3019
|
+
OpenAIBackendRole.ASSISTANT,
|
|
3020
|
+
timestamp=base_timestamp,
|
|
3021
|
+
return_records=self._tool_output_cache_enabled,
|
|
2400
3022
|
)
|
|
2401
3023
|
|
|
2402
3024
|
# Add minimal increment to ensure function message comes after
|
|
2403
|
-
self.update_memory(
|
|
3025
|
+
func_records = self.update_memory(
|
|
2404
3026
|
func_msg,
|
|
2405
3027
|
OpenAIBackendRole.FUNCTION,
|
|
2406
3028
|
timestamp=base_timestamp + 1e-6,
|
|
3029
|
+
return_records=self._tool_output_cache_enabled,
|
|
2407
3030
|
)
|
|
2408
3031
|
|
|
2409
3032
|
# Record information about this tool call
|
|
@@ -2414,6 +3037,20 @@ class ChatAgent(BaseAgent):
|
|
|
2414
3037
|
tool_call_id=tool_call_id,
|
|
2415
3038
|
)
|
|
2416
3039
|
|
|
3040
|
+
if (
|
|
3041
|
+
self._tool_output_cache_enabled
|
|
3042
|
+
and not mask_output
|
|
3043
|
+
and func_records
|
|
3044
|
+
and self._tool_output_cache_manager is not None
|
|
3045
|
+
):
|
|
3046
|
+
serialized_result = self._serialize_tool_result(result)
|
|
3047
|
+
self._register_tool_output_for_cache(
|
|
3048
|
+
func_name,
|
|
3049
|
+
tool_call_id,
|
|
3050
|
+
serialized_result,
|
|
3051
|
+
cast(List[MemoryRecord], func_records),
|
|
3052
|
+
)
|
|
3053
|
+
|
|
2417
3054
|
return tool_record
|
|
2418
3055
|
|
|
2419
3056
|
def _stream(
|
|
@@ -2482,7 +3119,7 @@ class ChatAgent(BaseAgent):
|
|
|
2482
3119
|
# Check termination condition
|
|
2483
3120
|
if self.stop_event and self.stop_event.is_set():
|
|
2484
3121
|
logger.info(
|
|
2485
|
-
f"Termination triggered at iteration
|
|
3122
|
+
f"Termination triggered at iteration {iteration_count}"
|
|
2486
3123
|
)
|
|
2487
3124
|
yield self._step_terminate(
|
|
2488
3125
|
num_tokens, tool_call_records, "termination_triggered"
|
|
@@ -2665,12 +3302,6 @@ class ChatAgent(BaseAgent):
|
|
|
2665
3302
|
stream_completed = False
|
|
2666
3303
|
|
|
2667
3304
|
for chunk in stream:
|
|
2668
|
-
# Update token usage if available
|
|
2669
|
-
if chunk.usage:
|
|
2670
|
-
self._update_token_usage_tracker(
|
|
2671
|
-
step_token_usage, safe_model_dump(chunk.usage)
|
|
2672
|
-
)
|
|
2673
|
-
|
|
2674
3305
|
# Process chunk delta
|
|
2675
3306
|
if chunk.choices and len(chunk.choices) > 0:
|
|
2676
3307
|
choice = chunk.choices[0]
|
|
@@ -2703,12 +3334,6 @@ class ChatAgent(BaseAgent):
|
|
|
2703
3334
|
# If we have complete tool calls, execute them with
|
|
2704
3335
|
# sync status updates
|
|
2705
3336
|
if accumulated_tool_calls:
|
|
2706
|
-
# Record assistant message with tool calls first
|
|
2707
|
-
self._record_assistant_tool_calls_message(
|
|
2708
|
-
accumulated_tool_calls,
|
|
2709
|
-
content_accumulator.get_full_content(),
|
|
2710
|
-
)
|
|
2711
|
-
|
|
2712
3337
|
# Execute tools synchronously with
|
|
2713
3338
|
# optimized status updates
|
|
2714
3339
|
for (
|
|
@@ -2741,7 +3366,49 @@ class ChatAgent(BaseAgent):
|
|
|
2741
3366
|
)
|
|
2742
3367
|
|
|
2743
3368
|
self.record_message(final_message)
|
|
2744
|
-
|
|
3369
|
+
elif chunk.usage and not chunk.choices:
|
|
3370
|
+
# Handle final chunk with usage but empty choices
|
|
3371
|
+
# This happens when stream_options={"include_usage": True}
|
|
3372
|
+
# Update the final usage from this chunk
|
|
3373
|
+
self._update_token_usage_tracker(
|
|
3374
|
+
step_token_usage, safe_model_dump(chunk.usage)
|
|
3375
|
+
)
|
|
3376
|
+
|
|
3377
|
+
# Create final response with final usage
|
|
3378
|
+
final_content = content_accumulator.get_full_content()
|
|
3379
|
+
if final_content.strip():
|
|
3380
|
+
final_message = BaseMessage(
|
|
3381
|
+
role_name=self.role_name,
|
|
3382
|
+
role_type=self.role_type,
|
|
3383
|
+
meta_dict={},
|
|
3384
|
+
content=final_content,
|
|
3385
|
+
)
|
|
3386
|
+
|
|
3387
|
+
if response_format:
|
|
3388
|
+
self._try_format_message(
|
|
3389
|
+
final_message, response_format
|
|
3390
|
+
)
|
|
3391
|
+
|
|
3392
|
+
# Create final response with final usage (not partial)
|
|
3393
|
+
final_response = ChatAgentResponse(
|
|
3394
|
+
msgs=[final_message],
|
|
3395
|
+
terminated=False,
|
|
3396
|
+
info={
|
|
3397
|
+
"id": getattr(chunk, 'id', ''),
|
|
3398
|
+
"usage": step_token_usage.copy(),
|
|
3399
|
+
"finish_reasons": ["stop"],
|
|
3400
|
+
"num_tokens": self._get_token_count(final_content),
|
|
3401
|
+
"tool_calls": tool_call_records or [],
|
|
3402
|
+
"external_tool_requests": None,
|
|
3403
|
+
"streaming": False,
|
|
3404
|
+
"partial": False,
|
|
3405
|
+
},
|
|
3406
|
+
)
|
|
3407
|
+
yield final_response
|
|
3408
|
+
break
|
|
3409
|
+
elif stream_completed:
|
|
3410
|
+
# If we've already seen finish_reason but no usage chunk, exit
|
|
3411
|
+
break
|
|
2745
3412
|
|
|
2746
3413
|
return stream_completed, tool_calls_complete
|
|
2747
3414
|
|
|
@@ -2821,72 +3488,70 @@ class ChatAgent(BaseAgent):
|
|
|
2821
3488
|
accumulated_tool_calls: Dict[str, Any],
|
|
2822
3489
|
tool_call_records: List[ToolCallingRecord],
|
|
2823
3490
|
) -> Generator[ChatAgentResponse, None, None]:
|
|
2824
|
-
r"""Execute multiple tools synchronously with
|
|
2825
|
-
|
|
2826
|
-
non-blocking status streaming."""
|
|
2827
|
-
|
|
2828
|
-
def tool_worker(result_queue, tool_call_data):
|
|
2829
|
-
try:
|
|
2830
|
-
tool_call_record = self._execute_tool_from_stream_data(
|
|
2831
|
-
tool_call_data
|
|
2832
|
-
)
|
|
2833
|
-
result_queue.put(tool_call_record)
|
|
2834
|
-
except Exception as e:
|
|
2835
|
-
logger.error(f"Error in threaded tool execution: {e}")
|
|
2836
|
-
result_queue.put(None)
|
|
3491
|
+
r"""Execute multiple tools synchronously with proper content
|
|
3492
|
+
accumulation, using ThreadPoolExecutor for better timeout handling."""
|
|
2837
3493
|
|
|
2838
3494
|
tool_calls_to_execute = []
|
|
2839
3495
|
for _tool_call_index, tool_call_data in accumulated_tool_calls.items():
|
|
2840
3496
|
if tool_call_data.get('complete', False):
|
|
2841
3497
|
tool_calls_to_execute.append(tool_call_data)
|
|
2842
3498
|
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
f"Calling function: {function_name} with arguments: {args}"
|
|
2862
|
-
)
|
|
2863
|
-
|
|
2864
|
-
# wait for tool thread to finish with optional timeout
|
|
2865
|
-
thread.join(self.tool_execution_timeout)
|
|
3499
|
+
if not tool_calls_to_execute:
|
|
3500
|
+
# No tools to execute, return immediately
|
|
3501
|
+
return
|
|
3502
|
+
yield # Make this a generator
|
|
3503
|
+
|
|
3504
|
+
# Execute tools using ThreadPoolExecutor for proper timeout handling
|
|
3505
|
+
# Use max_workers=len() for parallel execution, with min of 1
|
|
3506
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
3507
|
+
max_workers=max(1, len(tool_calls_to_execute))
|
|
3508
|
+
) as executor:
|
|
3509
|
+
# Submit all tools first (parallel execution)
|
|
3510
|
+
futures_map = {}
|
|
3511
|
+
for tool_call_data in tool_calls_to_execute:
|
|
3512
|
+
function_name = tool_call_data['function']['name']
|
|
3513
|
+
try:
|
|
3514
|
+
args = json.loads(tool_call_data['function']['arguments'])
|
|
3515
|
+
except json.JSONDecodeError:
|
|
3516
|
+
args = tool_call_data['function']['arguments']
|
|
2866
3517
|
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
logger.warning(
|
|
2871
|
-
f"Function '{function_name}' timed out after "
|
|
2872
|
-
f"{self.tool_execution_timeout} seconds"
|
|
3518
|
+
# Log debug info
|
|
3519
|
+
logger.info(
|
|
3520
|
+
f"Calling function: {function_name} with arguments: {args}"
|
|
2873
3521
|
)
|
|
2874
3522
|
|
|
2875
|
-
#
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3523
|
+
# Submit tool execution (non-blocking)
|
|
3524
|
+
future = executor.submit(
|
|
3525
|
+
self._execute_tool_from_stream_data, tool_call_data
|
|
3526
|
+
)
|
|
3527
|
+
futures_map[future] = (function_name, tool_call_data)
|
|
3528
|
+
|
|
3529
|
+
# Wait for all futures to complete (or timeout)
|
|
3530
|
+
for future in concurrent.futures.as_completed(
|
|
3531
|
+
futures_map.keys(),
|
|
3532
|
+
timeout=self.tool_execution_timeout
|
|
3533
|
+
if self.tool_execution_timeout
|
|
3534
|
+
else None,
|
|
3535
|
+
):
|
|
3536
|
+
function_name, tool_call_data = futures_map[future]
|
|
2884
3537
|
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3538
|
+
try:
|
|
3539
|
+
tool_call_record = future.result()
|
|
3540
|
+
if tool_call_record:
|
|
3541
|
+
tool_call_records.append(tool_call_record)
|
|
3542
|
+
logger.info(
|
|
3543
|
+
f"Function output: {tool_call_record.result}"
|
|
3544
|
+
)
|
|
3545
|
+
except concurrent.futures.TimeoutError:
|
|
3546
|
+
logger.warning(
|
|
3547
|
+
f"Function '{function_name}' timed out after "
|
|
3548
|
+
f"{self.tool_execution_timeout} seconds"
|
|
3549
|
+
)
|
|
3550
|
+
future.cancel()
|
|
3551
|
+
except Exception as e:
|
|
3552
|
+
logger.error(
|
|
3553
|
+
f"Error executing tool '{function_name}': {e}"
|
|
3554
|
+
)
|
|
2890
3555
|
|
|
2891
3556
|
# Ensure this function remains a generator (required by type signature)
|
|
2892
3557
|
return
|
|
@@ -2906,10 +3571,19 @@ class ChatAgent(BaseAgent):
|
|
|
2906
3571
|
tool = self._internal_tools[function_name]
|
|
2907
3572
|
try:
|
|
2908
3573
|
result = tool(**args)
|
|
3574
|
+
# First, create and record the assistant message with tool
|
|
3575
|
+
# call
|
|
3576
|
+
assist_msg = FunctionCallingMessage(
|
|
3577
|
+
role_name=self.role_name,
|
|
3578
|
+
role_type=self.role_type,
|
|
3579
|
+
meta_dict=None,
|
|
3580
|
+
content="",
|
|
3581
|
+
func_name=function_name,
|
|
3582
|
+
args=args,
|
|
3583
|
+
tool_call_id=tool_call_id,
|
|
3584
|
+
)
|
|
2909
3585
|
|
|
2910
|
-
#
|
|
2911
|
-
# message assistant message with tool_calls was already
|
|
2912
|
-
# recorded in _record_assistant_tool_calls_message
|
|
3586
|
+
# Then create the tool response message
|
|
2913
3587
|
func_msg = FunctionCallingMessage(
|
|
2914
3588
|
role_name=self.role_name,
|
|
2915
3589
|
role_type=self.role_type,
|
|
@@ -2920,7 +3594,23 @@ class ChatAgent(BaseAgent):
|
|
|
2920
3594
|
tool_call_id=tool_call_id,
|
|
2921
3595
|
)
|
|
2922
3596
|
|
|
2923
|
-
|
|
3597
|
+
# Record both messages with precise timestamps to ensure
|
|
3598
|
+
# correct ordering
|
|
3599
|
+
current_time_ns = time.time_ns()
|
|
3600
|
+
base_timestamp = (
|
|
3601
|
+
current_time_ns / 1_000_000_000
|
|
3602
|
+
) # Convert to seconds
|
|
3603
|
+
|
|
3604
|
+
self.update_memory(
|
|
3605
|
+
assist_msg,
|
|
3606
|
+
OpenAIBackendRole.ASSISTANT,
|
|
3607
|
+
timestamp=base_timestamp,
|
|
3608
|
+
)
|
|
3609
|
+
self.update_memory(
|
|
3610
|
+
func_msg,
|
|
3611
|
+
OpenAIBackendRole.FUNCTION,
|
|
3612
|
+
timestamp=base_timestamp + 1e-6,
|
|
3613
|
+
)
|
|
2924
3614
|
|
|
2925
3615
|
return ToolCallingRecord(
|
|
2926
3616
|
tool_name=function_name,
|
|
@@ -2934,7 +3624,7 @@ class ChatAgent(BaseAgent):
|
|
|
2934
3624
|
f"Error executing tool '{function_name}': {e!s}"
|
|
2935
3625
|
)
|
|
2936
3626
|
result = {"error": error_msg}
|
|
2937
|
-
|
|
3627
|
+
logger.warning(error_msg)
|
|
2938
3628
|
|
|
2939
3629
|
# Record error response
|
|
2940
3630
|
func_msg = FunctionCallingMessage(
|
|
@@ -3004,10 +3694,19 @@ class ChatAgent(BaseAgent):
|
|
|
3004
3694
|
else:
|
|
3005
3695
|
# Fallback: synchronous call
|
|
3006
3696
|
result = tool(**args)
|
|
3697
|
+
# First, create and record the assistant message with tool
|
|
3698
|
+
# call
|
|
3699
|
+
assist_msg = FunctionCallingMessage(
|
|
3700
|
+
role_name=self.role_name,
|
|
3701
|
+
role_type=self.role_type,
|
|
3702
|
+
meta_dict=None,
|
|
3703
|
+
content="",
|
|
3704
|
+
func_name=function_name,
|
|
3705
|
+
args=args,
|
|
3706
|
+
tool_call_id=tool_call_id,
|
|
3707
|
+
)
|
|
3007
3708
|
|
|
3008
|
-
#
|
|
3009
|
-
# message assistant message with tool_calls was already
|
|
3010
|
-
# recorded in _record_assistant_tool_calls_message
|
|
3709
|
+
# Then create the tool response message
|
|
3011
3710
|
func_msg = FunctionCallingMessage(
|
|
3012
3711
|
role_name=self.role_name,
|
|
3013
3712
|
role_type=self.role_type,
|
|
@@ -3018,7 +3717,23 @@ class ChatAgent(BaseAgent):
|
|
|
3018
3717
|
tool_call_id=tool_call_id,
|
|
3019
3718
|
)
|
|
3020
3719
|
|
|
3021
|
-
|
|
3720
|
+
# Record both messages with precise timestamps to ensure
|
|
3721
|
+
# correct ordering
|
|
3722
|
+
current_time_ns = time.time_ns()
|
|
3723
|
+
base_timestamp = (
|
|
3724
|
+
current_time_ns / 1_000_000_000
|
|
3725
|
+
) # Convert to seconds
|
|
3726
|
+
|
|
3727
|
+
self.update_memory(
|
|
3728
|
+
assist_msg,
|
|
3729
|
+
OpenAIBackendRole.ASSISTANT,
|
|
3730
|
+
timestamp=base_timestamp,
|
|
3731
|
+
)
|
|
3732
|
+
self.update_memory(
|
|
3733
|
+
func_msg,
|
|
3734
|
+
OpenAIBackendRole.FUNCTION,
|
|
3735
|
+
timestamp=base_timestamp + 1e-6,
|
|
3736
|
+
)
|
|
3022
3737
|
|
|
3023
3738
|
return ToolCallingRecord(
|
|
3024
3739
|
tool_name=function_name,
|
|
@@ -3032,7 +3747,7 @@ class ChatAgent(BaseAgent):
|
|
|
3032
3747
|
f"Error executing async tool '{function_name}': {e!s}"
|
|
3033
3748
|
)
|
|
3034
3749
|
result = {"error": error_msg}
|
|
3035
|
-
|
|
3750
|
+
logger.warning(error_msg)
|
|
3036
3751
|
|
|
3037
3752
|
# Record error response
|
|
3038
3753
|
func_msg = FunctionCallingMessage(
|
|
@@ -3142,7 +3857,7 @@ class ChatAgent(BaseAgent):
|
|
|
3142
3857
|
# Check termination condition
|
|
3143
3858
|
if self.stop_event and self.stop_event.is_set():
|
|
3144
3859
|
logger.info(
|
|
3145
|
-
f"Termination triggered at iteration
|
|
3860
|
+
f"Termination triggered at iteration {iteration_count}"
|
|
3146
3861
|
)
|
|
3147
3862
|
yield self._step_terminate(
|
|
3148
3863
|
num_tokens, tool_call_records, "termination_triggered"
|
|
@@ -3369,18 +4084,13 @@ class ChatAgent(BaseAgent):
|
|
|
3369
4084
|
response_format: Optional[Type[BaseModel]] = None,
|
|
3370
4085
|
) -> AsyncGenerator[Union[ChatAgentResponse, Tuple[bool, bool]], None]:
|
|
3371
4086
|
r"""Async version of process streaming chunks with
|
|
3372
|
-
content accumulator.
|
|
4087
|
+
content accumulator.
|
|
4088
|
+
"""
|
|
3373
4089
|
|
|
3374
4090
|
tool_calls_complete = False
|
|
3375
4091
|
stream_completed = False
|
|
3376
4092
|
|
|
3377
4093
|
async for chunk in stream:
|
|
3378
|
-
# Update token usage if available
|
|
3379
|
-
if chunk.usage:
|
|
3380
|
-
self._update_token_usage_tracker(
|
|
3381
|
-
step_token_usage, safe_model_dump(chunk.usage)
|
|
3382
|
-
)
|
|
3383
|
-
|
|
3384
4094
|
# Process chunk delta
|
|
3385
4095
|
if chunk.choices and len(chunk.choices) > 0:
|
|
3386
4096
|
choice = chunk.choices[0]
|
|
@@ -3413,13 +4123,6 @@ class ChatAgent(BaseAgent):
|
|
|
3413
4123
|
# If we have complete tool calls, execute them with
|
|
3414
4124
|
# async status updates
|
|
3415
4125
|
if accumulated_tool_calls:
|
|
3416
|
-
# Record assistant message with
|
|
3417
|
-
# tool calls first
|
|
3418
|
-
self._record_assistant_tool_calls_message(
|
|
3419
|
-
accumulated_tool_calls,
|
|
3420
|
-
content_accumulator.get_full_content(),
|
|
3421
|
-
)
|
|
3422
|
-
|
|
3423
4126
|
# Execute tools asynchronously with real-time
|
|
3424
4127
|
# status updates
|
|
3425
4128
|
async for (
|
|
@@ -3454,7 +4157,49 @@ class ChatAgent(BaseAgent):
|
|
|
3454
4157
|
)
|
|
3455
4158
|
|
|
3456
4159
|
self.record_message(final_message)
|
|
3457
|
-
|
|
4160
|
+
elif chunk.usage and not chunk.choices:
|
|
4161
|
+
# Handle final chunk with usage but empty choices
|
|
4162
|
+
# This happens when stream_options={"include_usage": True}
|
|
4163
|
+
# Update the final usage from this chunk
|
|
4164
|
+
self._update_token_usage_tracker(
|
|
4165
|
+
step_token_usage, safe_model_dump(chunk.usage)
|
|
4166
|
+
)
|
|
4167
|
+
|
|
4168
|
+
# Create final response with final usage
|
|
4169
|
+
final_content = content_accumulator.get_full_content()
|
|
4170
|
+
if final_content.strip():
|
|
4171
|
+
final_message = BaseMessage(
|
|
4172
|
+
role_name=self.role_name,
|
|
4173
|
+
role_type=self.role_type,
|
|
4174
|
+
meta_dict={},
|
|
4175
|
+
content=final_content,
|
|
4176
|
+
)
|
|
4177
|
+
|
|
4178
|
+
if response_format:
|
|
4179
|
+
self._try_format_message(
|
|
4180
|
+
final_message, response_format
|
|
4181
|
+
)
|
|
4182
|
+
|
|
4183
|
+
# Create final response with final usage (not partial)
|
|
4184
|
+
final_response = ChatAgentResponse(
|
|
4185
|
+
msgs=[final_message],
|
|
4186
|
+
terminated=False,
|
|
4187
|
+
info={
|
|
4188
|
+
"id": getattr(chunk, 'id', ''),
|
|
4189
|
+
"usage": step_token_usage.copy(),
|
|
4190
|
+
"finish_reasons": ["stop"],
|
|
4191
|
+
"num_tokens": self._get_token_count(final_content),
|
|
4192
|
+
"tool_calls": tool_call_records or [],
|
|
4193
|
+
"external_tool_requests": None,
|
|
4194
|
+
"streaming": False,
|
|
4195
|
+
"partial": False,
|
|
4196
|
+
},
|
|
4197
|
+
)
|
|
4198
|
+
yield final_response
|
|
4199
|
+
break
|
|
4200
|
+
elif stream_completed:
|
|
4201
|
+
# If we've already seen finish_reason but no usage chunk, exit
|
|
4202
|
+
break
|
|
3458
4203
|
|
|
3459
4204
|
# Yield the final status as a tuple
|
|
3460
4205
|
yield (stream_completed, tool_calls_complete)
|
|
@@ -3547,15 +4292,18 @@ class ChatAgent(BaseAgent):
|
|
|
3547
4292
|
) -> ChatAgentResponse:
|
|
3548
4293
|
r"""Create a streaming response using content accumulator."""
|
|
3549
4294
|
|
|
3550
|
-
# Add new content
|
|
4295
|
+
# Add new content; only build full content when needed
|
|
3551
4296
|
accumulator.add_streaming_content(new_content)
|
|
3552
|
-
|
|
4297
|
+
if self.stream_accumulate:
|
|
4298
|
+
message_content = accumulator.get_full_content()
|
|
4299
|
+
else:
|
|
4300
|
+
message_content = new_content
|
|
3553
4301
|
|
|
3554
4302
|
message = BaseMessage(
|
|
3555
4303
|
role_name=self.role_name,
|
|
3556
4304
|
role_type=self.role_type,
|
|
3557
4305
|
meta_dict={},
|
|
3558
|
-
content=
|
|
4306
|
+
content=message_content,
|
|
3559
4307
|
)
|
|
3560
4308
|
|
|
3561
4309
|
return ChatAgentResponse(
|
|
@@ -3565,7 +4313,7 @@ class ChatAgent(BaseAgent):
|
|
|
3565
4313
|
"id": response_id,
|
|
3566
4314
|
"usage": step_token_usage.copy(),
|
|
3567
4315
|
"finish_reasons": ["streaming"],
|
|
3568
|
-
"num_tokens": self._get_token_count(
|
|
4316
|
+
"num_tokens": self._get_token_count(message_content),
|
|
3569
4317
|
"tool_calls": tool_call_records or [],
|
|
3570
4318
|
"external_tool_requests": None,
|
|
3571
4319
|
"streaming": True,
|
|
@@ -3621,10 +4369,12 @@ class ChatAgent(BaseAgent):
|
|
|
3621
4369
|
configuration.
|
|
3622
4370
|
"""
|
|
3623
4371
|
# Create a new instance with the same configuration
|
|
3624
|
-
# If with_memory is True, set system_message to None
|
|
3625
|
-
#
|
|
4372
|
+
# If with_memory is True, set system_message to None (it will be
|
|
4373
|
+
# copied from memory below, including any workflow context)
|
|
4374
|
+
# If with_memory is False, use the current system message
|
|
4375
|
+
# (which may include appended workflow context)
|
|
3626
4376
|
# To avoid duplicated system memory.
|
|
3627
|
-
system_message = None if with_memory else self.
|
|
4377
|
+
system_message = None if with_memory else self._system_message
|
|
3628
4378
|
|
|
3629
4379
|
# Clone tools and collect toolkits that need registration
|
|
3630
4380
|
cloned_tools, toolkits_to_register = self._clone_tools()
|
|
@@ -3638,7 +4388,7 @@ class ChatAgent(BaseAgent):
|
|
|
3638
4388
|
self.memory.get_context_creator(), "token_limit", None
|
|
3639
4389
|
),
|
|
3640
4390
|
output_language=self._output_language,
|
|
3641
|
-
tools=cloned_tools,
|
|
4391
|
+
tools=cast(List[Union[FunctionTool, Callable]], cloned_tools),
|
|
3642
4392
|
toolkits_to_register_agent=toolkits_to_register,
|
|
3643
4393
|
external_tools=[
|
|
3644
4394
|
schema for schema in self._external_tool_schemas.values()
|
|
@@ -3652,6 +4402,10 @@ class ChatAgent(BaseAgent):
|
|
|
3652
4402
|
tool_execution_timeout=self.tool_execution_timeout,
|
|
3653
4403
|
pause_event=self.pause_event,
|
|
3654
4404
|
prune_tool_calls_from_memory=self.prune_tool_calls_from_memory,
|
|
4405
|
+
enable_tool_output_cache=self._tool_output_cache_enabled,
|
|
4406
|
+
tool_output_cache_threshold=self._tool_output_cache_threshold,
|
|
4407
|
+
tool_output_cache_dir=self._tool_output_cache_dir,
|
|
4408
|
+
stream_accumulate=self.stream_accumulate,
|
|
3655
4409
|
)
|
|
3656
4410
|
|
|
3657
4411
|
# Copy memory if requested
|
|
@@ -3666,9 +4420,7 @@ class ChatAgent(BaseAgent):
|
|
|
3666
4420
|
|
|
3667
4421
|
def _clone_tools(
|
|
3668
4422
|
self,
|
|
3669
|
-
) -> Tuple[
|
|
3670
|
-
List[Union[FunctionTool, Callable]], List[RegisteredAgentToolkit]
|
|
3671
|
-
]:
|
|
4423
|
+
) -> Tuple[List[FunctionTool], List[RegisteredAgentToolkit]]:
|
|
3672
4424
|
r"""Clone tools and return toolkits that need agent registration.
|
|
3673
4425
|
|
|
3674
4426
|
This method handles stateful toolkits by cloning them if they have
|
|
@@ -3720,18 +4472,62 @@ class ChatAgent(BaseAgent):
|
|
|
3720
4472
|
# Toolkit doesn't support cloning, use original
|
|
3721
4473
|
cloned_toolkits[toolkit_id] = toolkit_instance
|
|
3722
4474
|
|
|
4475
|
+
if getattr(
|
|
4476
|
+
tool.func, "__message_integration_enhanced__", False
|
|
4477
|
+
):
|
|
4478
|
+
cloned_tools.append(
|
|
4479
|
+
FunctionTool(
|
|
4480
|
+
func=tool.func,
|
|
4481
|
+
openai_tool_schema=tool.get_openai_tool_schema(),
|
|
4482
|
+
)
|
|
4483
|
+
)
|
|
4484
|
+
continue
|
|
4485
|
+
|
|
3723
4486
|
# Get the method from the cloned (or original) toolkit
|
|
3724
4487
|
toolkit = cloned_toolkits[toolkit_id]
|
|
3725
4488
|
method_name = tool.func.__name__
|
|
4489
|
+
|
|
3726
4490
|
if hasattr(toolkit, method_name):
|
|
3727
4491
|
new_method = getattr(toolkit, method_name)
|
|
3728
|
-
|
|
4492
|
+
# Wrap cloned method into a new FunctionTool,
|
|
4493
|
+
# preserving schema
|
|
4494
|
+
try:
|
|
4495
|
+
new_tool = FunctionTool(
|
|
4496
|
+
func=new_method,
|
|
4497
|
+
openai_tool_schema=tool.get_openai_tool_schema(),
|
|
4498
|
+
)
|
|
4499
|
+
cloned_tools.append(new_tool)
|
|
4500
|
+
except Exception as e:
|
|
4501
|
+
# If wrapping fails, fallback to wrapping the original
|
|
4502
|
+
# function with its schema to maintain consistency
|
|
4503
|
+
logger.warning(
|
|
4504
|
+
f"Failed to wrap cloned toolkit "
|
|
4505
|
+
f"method '{method_name}' "
|
|
4506
|
+
f"with FunctionTool: {e}. Using original "
|
|
4507
|
+
f"function with preserved schema instead."
|
|
4508
|
+
)
|
|
4509
|
+
cloned_tools.append(
|
|
4510
|
+
FunctionTool(
|
|
4511
|
+
func=tool.func,
|
|
4512
|
+
openai_tool_schema=tool.get_openai_tool_schema(),
|
|
4513
|
+
)
|
|
4514
|
+
)
|
|
3729
4515
|
else:
|
|
3730
|
-
# Fallback to original function
|
|
3731
|
-
cloned_tools.append(
|
|
4516
|
+
# Fallback to original function wrapped in FunctionTool
|
|
4517
|
+
cloned_tools.append(
|
|
4518
|
+
FunctionTool(
|
|
4519
|
+
func=tool.func,
|
|
4520
|
+
openai_tool_schema=tool.get_openai_tool_schema(),
|
|
4521
|
+
)
|
|
4522
|
+
)
|
|
3732
4523
|
else:
|
|
3733
|
-
# Not a toolkit method,
|
|
3734
|
-
cloned_tools.append(
|
|
4524
|
+
# Not a toolkit method, preserve FunctionTool schema directly
|
|
4525
|
+
cloned_tools.append(
|
|
4526
|
+
FunctionTool(
|
|
4527
|
+
func=tool.func,
|
|
4528
|
+
openai_tool_schema=tool.get_openai_tool_schema(),
|
|
4529
|
+
)
|
|
4530
|
+
)
|
|
3735
4531
|
|
|
3736
4532
|
return cloned_tools, toolkits_to_register
|
|
3737
4533
|
|