camel-ai 0.2.77__py3-none-any.whl → 0.2.79a0__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 +321 -325
- camel/datasets/base_generator.py +39 -10
- camel/environments/single_step.py +28 -3
- camel/memories/__init__.py +1 -2
- camel/memories/blocks/chat_history_block.py +2 -17
- camel/models/aws_bedrock_model.py +1 -17
- camel/models/moonshot_model.py +102 -5
- camel/societies/workforce/events.py +122 -0
- camel/societies/workforce/single_agent_worker.py +164 -34
- camel/societies/workforce/workforce.py +417 -156
- camel/societies/workforce/workforce_callback.py +74 -0
- camel/societies/workforce/workforce_logger.py +144 -140
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/toolkits/excel_toolkit.py +1 -1
- camel/toolkits/file_toolkit.py +3 -2
- camel/utils/context_utils.py +53 -0
- {camel_ai-0.2.77.dist-info → camel_ai-0.2.79a0.dist-info}/METADATA +23 -13
- {camel_ai-0.2.77.dist-info → camel_ai-0.2.79a0.dist-info}/RECORD +21 -18
- {camel_ai-0.2.77.dist-info → camel_ai-0.2.79a0.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.77.dist-info → camel_ai-0.2.79a0.dist-info}/licenses/LICENSE +0 -0
camel/agents/chat_agent.py
CHANGED
|
@@ -30,7 +30,6 @@ import threading
|
|
|
30
30
|
import time
|
|
31
31
|
import uuid
|
|
32
32
|
import warnings
|
|
33
|
-
from dataclasses import dataclass
|
|
34
33
|
from datetime import datetime
|
|
35
34
|
from pathlib import Path
|
|
36
35
|
from typing import (
|
|
@@ -72,7 +71,6 @@ from camel.memories import (
|
|
|
72
71
|
MemoryRecord,
|
|
73
72
|
ScoreBasedContextCreator,
|
|
74
73
|
)
|
|
75
|
-
from camel.memories.blocks.chat_history_block import EmptyMemoryWarning
|
|
76
74
|
from camel.messages import (
|
|
77
75
|
BaseMessage,
|
|
78
76
|
FunctionCallingMessage,
|
|
@@ -160,53 +158,6 @@ SIMPLE_FORMAT_PROMPT = TextPrompt(
|
|
|
160
158
|
)
|
|
161
159
|
|
|
162
160
|
|
|
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
|
-
|
|
210
161
|
class StreamContentAccumulator:
|
|
211
162
|
r"""Manages content accumulation across streaming responses to ensure
|
|
212
163
|
all responses contain complete cumulative content."""
|
|
@@ -452,19 +403,6 @@ class ChatAgent(BaseAgent):
|
|
|
452
403
|
usage. When enabled, removes FUNCTION/TOOL role messages and
|
|
453
404
|
ASSISTANT messages with tool_calls after each step.
|
|
454
405
|
(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:`False`)
|
|
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`)
|
|
468
406
|
retry_attempts (int, optional): Maximum number of retry attempts for
|
|
469
407
|
rate limit errors. (default: :obj:`3`)
|
|
470
408
|
retry_delay (float, optional): Initial delay in seconds between
|
|
@@ -516,9 +454,6 @@ class ChatAgent(BaseAgent):
|
|
|
516
454
|
mask_tool_output: bool = False,
|
|
517
455
|
pause_event: Optional[Union[threading.Event, asyncio.Event]] = None,
|
|
518
456
|
prune_tool_calls_from_memory: bool = False,
|
|
519
|
-
enable_tool_output_cache: bool = False,
|
|
520
|
-
tool_output_cache_threshold: int = 2000,
|
|
521
|
-
tool_output_cache_dir: Optional[Union[str, Path]] = None,
|
|
522
457
|
retry_attempts: int = 3,
|
|
523
458
|
retry_delay: float = 1.0,
|
|
524
459
|
step_timeout: Optional[float] = None,
|
|
@@ -538,28 +473,6 @@ class ChatAgent(BaseAgent):
|
|
|
538
473
|
# Assign unique ID
|
|
539
474
|
self.agent_id = agent_id if agent_id else str(uuid.uuid4())
|
|
540
475
|
|
|
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
|
-
|
|
563
476
|
# Set up memory
|
|
564
477
|
context_creator = ScoreBasedContextCreator(
|
|
565
478
|
self.model_backend.token_counter,
|
|
@@ -606,8 +519,6 @@ class ChatAgent(BaseAgent):
|
|
|
606
519
|
convert_to_function_tool(tool) for tool in (tools or [])
|
|
607
520
|
]
|
|
608
521
|
}
|
|
609
|
-
if self._tool_output_cache_enabled:
|
|
610
|
-
self._ensure_tool_cache_lookup_tool()
|
|
611
522
|
|
|
612
523
|
# Register agent with toolkits that have RegisteredAgentToolkit mixin
|
|
613
524
|
if toolkits_to_register_agent:
|
|
@@ -644,8 +555,6 @@ class ChatAgent(BaseAgent):
|
|
|
644
555
|
r"""Resets the :obj:`ChatAgent` to its initial state."""
|
|
645
556
|
self.terminated = False
|
|
646
557
|
self.init_messages()
|
|
647
|
-
if self._tool_output_cache_enabled:
|
|
648
|
-
self._tool_output_history.clear()
|
|
649
558
|
for terminator in self.response_terminators:
|
|
650
559
|
terminator.reset()
|
|
651
560
|
|
|
@@ -866,178 +775,6 @@ class ChatAgent(BaseAgent):
|
|
|
866
775
|
for tool in tools:
|
|
867
776
|
self.add_tool(tool)
|
|
868
777
|
|
|
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
|
-
|
|
1041
778
|
def add_external_tool(
|
|
1042
779
|
self, tool: Union[FunctionTool, Callable, Dict[str, Any]]
|
|
1043
780
|
) -> None:
|
|
@@ -1082,8 +819,7 @@ class ChatAgent(BaseAgent):
|
|
|
1082
819
|
message: BaseMessage,
|
|
1083
820
|
role: OpenAIBackendRole,
|
|
1084
821
|
timestamp: Optional[float] = None,
|
|
1085
|
-
|
|
1086
|
-
) -> Optional[List[MemoryRecord]]:
|
|
822
|
+
) -> None:
|
|
1087
823
|
r"""Updates the agent memory with a new message.
|
|
1088
824
|
|
|
1089
825
|
If the single *message* exceeds the model's context window, it will
|
|
@@ -1103,29 +839,21 @@ class ChatAgent(BaseAgent):
|
|
|
1103
839
|
timestamp (Optional[float], optional): Custom timestamp for the
|
|
1104
840
|
memory record. If `None`, the current time will be used.
|
|
1105
841
|
(default: :obj:`None`)
|
|
1106
|
-
|
|
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``.
|
|
842
|
+
(default: obj:`None`)
|
|
1113
843
|
"""
|
|
1114
844
|
|
|
1115
|
-
written_records: List[MemoryRecord] = []
|
|
1116
|
-
|
|
1117
845
|
# 1. Helper to write a record to memory
|
|
1118
846
|
def _write_single_record(
|
|
1119
847
|
message: BaseMessage, role: OpenAIBackendRole, timestamp: float
|
|
1120
848
|
):
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
849
|
+
self.memory.write_record(
|
|
850
|
+
MemoryRecord(
|
|
851
|
+
message=message,
|
|
852
|
+
role_at_backend=role,
|
|
853
|
+
timestamp=timestamp,
|
|
854
|
+
agent_id=self.agent_id,
|
|
855
|
+
)
|
|
1126
856
|
)
|
|
1127
|
-
written_records.append(record)
|
|
1128
|
-
self.memory.write_record(record)
|
|
1129
857
|
|
|
1130
858
|
base_ts = (
|
|
1131
859
|
timestamp
|
|
@@ -1140,7 +868,7 @@ class ChatAgent(BaseAgent):
|
|
|
1140
868
|
token_limit = context_creator.token_limit
|
|
1141
869
|
except AttributeError:
|
|
1142
870
|
_write_single_record(message, role, base_ts)
|
|
1143
|
-
return
|
|
871
|
+
return
|
|
1144
872
|
|
|
1145
873
|
# 3. Check if slicing is necessary
|
|
1146
874
|
try:
|
|
@@ -1148,22 +876,20 @@ class ChatAgent(BaseAgent):
|
|
|
1148
876
|
[message.to_openai_message(role)]
|
|
1149
877
|
)
|
|
1150
878
|
|
|
1151
|
-
|
|
1152
|
-
warnings.filterwarnings("ignore", category=EmptyMemoryWarning)
|
|
1153
|
-
_, ctx_tokens = self.memory.get_context()
|
|
879
|
+
_, ctx_tokens = self.memory.get_context()
|
|
1154
880
|
|
|
1155
881
|
remaining_budget = max(0, token_limit - ctx_tokens)
|
|
1156
882
|
|
|
1157
883
|
if current_tokens <= remaining_budget:
|
|
1158
884
|
_write_single_record(message, role, base_ts)
|
|
1159
|
-
return
|
|
885
|
+
return
|
|
1160
886
|
except Exception as e:
|
|
1161
887
|
logger.warning(
|
|
1162
888
|
f"Token calculation failed before chunking, "
|
|
1163
889
|
f"writing message as-is. Error: {e}"
|
|
1164
890
|
)
|
|
1165
891
|
_write_single_record(message, role, base_ts)
|
|
1166
|
-
return
|
|
892
|
+
return
|
|
1167
893
|
|
|
1168
894
|
# 4. Perform slicing
|
|
1169
895
|
logger.warning(
|
|
@@ -1184,18 +910,18 @@ class ChatAgent(BaseAgent):
|
|
|
1184
910
|
|
|
1185
911
|
if not text_to_chunk or not text_to_chunk.strip():
|
|
1186
912
|
_write_single_record(message, role, base_ts)
|
|
1187
|
-
return
|
|
913
|
+
return
|
|
1188
914
|
# Encode the entire text to get a list of all token IDs
|
|
1189
915
|
try:
|
|
1190
916
|
all_token_ids = token_counter.encode(text_to_chunk)
|
|
1191
917
|
except Exception as e:
|
|
1192
918
|
logger.error(f"Failed to encode text for chunking: {e}")
|
|
1193
919
|
_write_single_record(message, role, base_ts) # Fallback
|
|
1194
|
-
return
|
|
920
|
+
return
|
|
1195
921
|
|
|
1196
922
|
if not all_token_ids:
|
|
1197
923
|
_write_single_record(message, role, base_ts) # Nothing to chunk
|
|
1198
|
-
return
|
|
924
|
+
return
|
|
1199
925
|
|
|
1200
926
|
# 1. Base chunk size: one-tenth of the smaller of (a) total token
|
|
1201
927
|
# limit and (b) current remaining budget. This prevents us from
|
|
@@ -1261,8 +987,6 @@ class ChatAgent(BaseAgent):
|
|
|
1261
987
|
# Increment timestamp slightly to maintain order
|
|
1262
988
|
_write_single_record(new_msg, role, base_ts + i * 1e-6)
|
|
1263
989
|
|
|
1264
|
-
return written_records if return_records else None
|
|
1265
|
-
|
|
1266
990
|
def load_memory(self, memory: AgentMemory) -> None:
|
|
1267
991
|
r"""Load the provided memory into the agent.
|
|
1268
992
|
|
|
@@ -1350,6 +1074,10 @@ class ChatAgent(BaseAgent):
|
|
|
1350
1074
|
r"""Summarize the agent's current conversation context and persist it
|
|
1351
1075
|
to a markdown file.
|
|
1352
1076
|
|
|
1077
|
+
.. deprecated:: 0.2.80
|
|
1078
|
+
Use :meth:`asummarize` for async/await support and better
|
|
1079
|
+
performance in parallel summarization workflows.
|
|
1080
|
+
|
|
1353
1081
|
Args:
|
|
1354
1082
|
filename (Optional[str]): The base filename (without extension) to
|
|
1355
1083
|
use for the markdown file. Defaults to a timestamped name when
|
|
@@ -1369,8 +1097,18 @@ class ChatAgent(BaseAgent):
|
|
|
1369
1097
|
Dict[str, Any]: A dictionary containing the summary text, file
|
|
1370
1098
|
path, status message, and optionally structured_summary if
|
|
1371
1099
|
response_format was provided.
|
|
1100
|
+
|
|
1101
|
+
See Also:
|
|
1102
|
+
:meth:`asummarize`: Async version for non-blocking LLM calls.
|
|
1372
1103
|
"""
|
|
1373
1104
|
|
|
1105
|
+
warnings.warn(
|
|
1106
|
+
"summarize() is synchronous. Consider using asummarize() "
|
|
1107
|
+
"for async/await support and better performance.",
|
|
1108
|
+
DeprecationWarning,
|
|
1109
|
+
stacklevel=2,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1374
1112
|
result: Dict[str, Any] = {
|
|
1375
1113
|
"summary": "",
|
|
1376
1114
|
"file_path": None,
|
|
@@ -1520,11 +1258,29 @@ class ChatAgent(BaseAgent):
|
|
|
1520
1258
|
result["status"] = status_message
|
|
1521
1259
|
return result
|
|
1522
1260
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1261
|
+
# handle structured output if response_format was provided
|
|
1262
|
+
structured_output = None
|
|
1263
|
+
if response_format and response.msgs[-1].parsed:
|
|
1264
|
+
structured_output = response.msgs[-1].parsed
|
|
1265
|
+
|
|
1266
|
+
# determine filename: use provided filename, or extract from
|
|
1267
|
+
# structured output, or generate timestamp
|
|
1268
|
+
if filename:
|
|
1269
|
+
base_filename = filename
|
|
1270
|
+
elif structured_output and hasattr(
|
|
1271
|
+
structured_output, 'task_title'
|
|
1272
|
+
):
|
|
1273
|
+
# use task_title from structured output for filename
|
|
1274
|
+
task_title = structured_output.task_title
|
|
1275
|
+
clean_title = ContextUtility.sanitize_workflow_filename(
|
|
1276
|
+
task_title
|
|
1277
|
+
)
|
|
1278
|
+
base_filename = (
|
|
1279
|
+
f"{clean_title}_workflow" if clean_title else "workflow"
|
|
1280
|
+
)
|
|
1281
|
+
else:
|
|
1282
|
+
base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
|
|
1283
|
+
|
|
1528
1284
|
base_filename = Path(base_filename).with_suffix("").name
|
|
1529
1285
|
|
|
1530
1286
|
metadata = context_util.get_session_metadata()
|
|
@@ -1535,11 +1291,274 @@ class ChatAgent(BaseAgent):
|
|
|
1535
1291
|
}
|
|
1536
1292
|
)
|
|
1537
1293
|
|
|
1538
|
-
#
|
|
1294
|
+
# convert structured output to custom markdown if present
|
|
1295
|
+
if structured_output:
|
|
1296
|
+
# convert structured output to custom markdown
|
|
1297
|
+
summary_content = context_util.structured_output_to_markdown(
|
|
1298
|
+
structured_data=structured_output, metadata=metadata
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
# Save the markdown (either custom structured or default)
|
|
1302
|
+
save_status = context_util.save_markdown_file(
|
|
1303
|
+
base_filename,
|
|
1304
|
+
summary_content,
|
|
1305
|
+
title="Conversation Summary"
|
|
1306
|
+
if not structured_output
|
|
1307
|
+
else None,
|
|
1308
|
+
metadata=metadata if not structured_output else None,
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
file_path = (
|
|
1312
|
+
context_util.get_working_directory() / f"{base_filename}.md"
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
# Prepare result dictionary
|
|
1316
|
+
result_dict = {
|
|
1317
|
+
"summary": summary_content,
|
|
1318
|
+
"file_path": str(file_path),
|
|
1319
|
+
"status": save_status,
|
|
1320
|
+
"structured_summary": structured_output,
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
result.update(result_dict)
|
|
1324
|
+
logger.info("Conversation summary saved to %s", file_path)
|
|
1325
|
+
return result
|
|
1326
|
+
|
|
1327
|
+
except Exception as exc:
|
|
1328
|
+
error_message = f"Failed to summarize conversation context: {exc}"
|
|
1329
|
+
logger.error(error_message)
|
|
1330
|
+
result["status"] = error_message
|
|
1331
|
+
return result
|
|
1332
|
+
|
|
1333
|
+
async def asummarize(
|
|
1334
|
+
self,
|
|
1335
|
+
filename: Optional[str] = None,
|
|
1336
|
+
summary_prompt: Optional[str] = None,
|
|
1337
|
+
response_format: Optional[Type[BaseModel]] = None,
|
|
1338
|
+
working_directory: Optional[Union[str, Path]] = None,
|
|
1339
|
+
) -> Dict[str, Any]:
|
|
1340
|
+
r"""Asynchronously summarize the agent's current conversation context
|
|
1341
|
+
and persist it to a markdown file.
|
|
1342
|
+
|
|
1343
|
+
This is the async version of summarize() that uses astep() for
|
|
1344
|
+
non-blocking LLM calls, enabling parallel summarization of multiple
|
|
1345
|
+
agents.
|
|
1346
|
+
|
|
1347
|
+
Args:
|
|
1348
|
+
filename (Optional[str]): The base filename (without extension) to
|
|
1349
|
+
use for the markdown file. Defaults to a timestamped name when
|
|
1350
|
+
not provided.
|
|
1351
|
+
summary_prompt (Optional[str]): Custom prompt for the summarizer.
|
|
1352
|
+
When omitted, a default prompt highlighting key decisions,
|
|
1353
|
+
action items, and open questions is used.
|
|
1354
|
+
response_format (Optional[Type[BaseModel]]): A Pydantic model
|
|
1355
|
+
defining the expected structure of the response. If provided,
|
|
1356
|
+
the summary will be generated as structured output and included
|
|
1357
|
+
in the result.
|
|
1358
|
+
working_directory (Optional[str|Path]): Optional directory to save
|
|
1359
|
+
the markdown summary file. If provided, overrides the default
|
|
1360
|
+
directory used by ContextUtility.
|
|
1361
|
+
|
|
1362
|
+
Returns:
|
|
1363
|
+
Dict[str, Any]: A dictionary containing the summary text, file
|
|
1364
|
+
path, status message, and optionally structured_summary if
|
|
1365
|
+
response_format was provided.
|
|
1366
|
+
"""
|
|
1367
|
+
|
|
1368
|
+
result: Dict[str, Any] = {
|
|
1369
|
+
"summary": "",
|
|
1370
|
+
"file_path": None,
|
|
1371
|
+
"status": "",
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
try:
|
|
1375
|
+
# Use external context if set, otherwise create local one
|
|
1376
|
+
if self._context_utility is None:
|
|
1377
|
+
if working_directory is not None:
|
|
1378
|
+
self._context_utility = ContextUtility(
|
|
1379
|
+
working_directory=str(working_directory)
|
|
1380
|
+
)
|
|
1381
|
+
else:
|
|
1382
|
+
self._context_utility = ContextUtility()
|
|
1383
|
+
context_util = self._context_utility
|
|
1384
|
+
|
|
1385
|
+
# Get conversation directly from agent's memory
|
|
1386
|
+
messages, _ = self.memory.get_context()
|
|
1387
|
+
|
|
1388
|
+
if not messages:
|
|
1389
|
+
status_message = (
|
|
1390
|
+
"No conversation context available to summarize."
|
|
1391
|
+
)
|
|
1392
|
+
result["status"] = status_message
|
|
1393
|
+
return result
|
|
1394
|
+
|
|
1395
|
+
# Convert messages to conversation text
|
|
1396
|
+
conversation_lines = []
|
|
1397
|
+
for message in messages:
|
|
1398
|
+
role = message.get('role', 'unknown')
|
|
1399
|
+
content = message.get('content', '')
|
|
1400
|
+
|
|
1401
|
+
# Handle tool call messages (assistant calling tools)
|
|
1402
|
+
tool_calls = message.get('tool_calls')
|
|
1403
|
+
if tool_calls and isinstance(tool_calls, (list, tuple)):
|
|
1404
|
+
for tool_call in tool_calls:
|
|
1405
|
+
# Handle both dict and object formats
|
|
1406
|
+
if isinstance(tool_call, dict):
|
|
1407
|
+
func_name = tool_call.get('function', {}).get(
|
|
1408
|
+
'name', 'unknown_tool'
|
|
1409
|
+
)
|
|
1410
|
+
func_args_str = tool_call.get('function', {}).get(
|
|
1411
|
+
'arguments', '{}'
|
|
1412
|
+
)
|
|
1413
|
+
else:
|
|
1414
|
+
# Handle object format (Pydantic or similar)
|
|
1415
|
+
func_name = getattr(
|
|
1416
|
+
getattr(tool_call, 'function', None),
|
|
1417
|
+
'name',
|
|
1418
|
+
'unknown_tool',
|
|
1419
|
+
)
|
|
1420
|
+
func_args_str = getattr(
|
|
1421
|
+
getattr(tool_call, 'function', None),
|
|
1422
|
+
'arguments',
|
|
1423
|
+
'{}',
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
# Parse and format arguments for readability
|
|
1427
|
+
try:
|
|
1428
|
+
import json
|
|
1429
|
+
|
|
1430
|
+
args_dict = json.loads(func_args_str)
|
|
1431
|
+
args_formatted = ', '.join(
|
|
1432
|
+
f"{k}={v}" for k, v in args_dict.items()
|
|
1433
|
+
)
|
|
1434
|
+
except (json.JSONDecodeError, ValueError, TypeError):
|
|
1435
|
+
args_formatted = func_args_str
|
|
1436
|
+
|
|
1437
|
+
conversation_lines.append(
|
|
1438
|
+
f"[TOOL CALL] {func_name}({args_formatted})"
|
|
1439
|
+
)
|
|
1440
|
+
|
|
1441
|
+
# Handle tool response messages
|
|
1442
|
+
elif role == 'tool':
|
|
1443
|
+
tool_name = message.get('name', 'unknown_tool')
|
|
1444
|
+
if not content:
|
|
1445
|
+
content = str(message.get('content', ''))
|
|
1446
|
+
conversation_lines.append(
|
|
1447
|
+
f"[TOOL RESULT] {tool_name} → {content}"
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
# Handle regular content messages (user/assistant/system)
|
|
1451
|
+
elif content:
|
|
1452
|
+
conversation_lines.append(f"{role}: {content}")
|
|
1453
|
+
|
|
1454
|
+
conversation_text = "\n".join(conversation_lines).strip()
|
|
1455
|
+
|
|
1456
|
+
if not conversation_text:
|
|
1457
|
+
status_message = (
|
|
1458
|
+
"Conversation context is empty; skipping summary."
|
|
1459
|
+
)
|
|
1460
|
+
result["status"] = status_message
|
|
1461
|
+
return result
|
|
1462
|
+
|
|
1463
|
+
if self._context_summary_agent is None:
|
|
1464
|
+
self._context_summary_agent = ChatAgent(
|
|
1465
|
+
system_message=(
|
|
1466
|
+
"You are a helpful assistant that summarizes "
|
|
1467
|
+
"conversations"
|
|
1468
|
+
),
|
|
1469
|
+
model=self.model_backend,
|
|
1470
|
+
agent_id=f"{self.agent_id}_context_summarizer",
|
|
1471
|
+
)
|
|
1472
|
+
else:
|
|
1473
|
+
self._context_summary_agent.reset()
|
|
1474
|
+
|
|
1475
|
+
if summary_prompt:
|
|
1476
|
+
prompt_text = (
|
|
1477
|
+
f"{summary_prompt.rstrip()}\n\n"
|
|
1478
|
+
f"AGENT CONVERSATION TO BE SUMMARIZED:\n"
|
|
1479
|
+
f"{conversation_text}"
|
|
1480
|
+
)
|
|
1481
|
+
else:
|
|
1482
|
+
prompt_text = (
|
|
1483
|
+
"Summarize the context information in concise markdown "
|
|
1484
|
+
"bullet points highlighting key decisions, action items.\n"
|
|
1485
|
+
f"Context information:\n{conversation_text}"
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
try:
|
|
1489
|
+
# Use structured output if response_format is provided
|
|
1490
|
+
if response_format:
|
|
1491
|
+
response = await self._context_summary_agent.astep(
|
|
1492
|
+
prompt_text, response_format=response_format
|
|
1493
|
+
)
|
|
1494
|
+
else:
|
|
1495
|
+
response = await self._context_summary_agent.astep(
|
|
1496
|
+
prompt_text
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
# Handle streaming response
|
|
1500
|
+
if isinstance(response, AsyncStreamingChatAgentResponse):
|
|
1501
|
+
# Collect final response
|
|
1502
|
+
final_response = await response
|
|
1503
|
+
response = final_response
|
|
1504
|
+
|
|
1505
|
+
except Exception as step_exc:
|
|
1506
|
+
error_message = (
|
|
1507
|
+
f"Failed to generate summary using model: {step_exc}"
|
|
1508
|
+
)
|
|
1509
|
+
logger.error(error_message)
|
|
1510
|
+
result["status"] = error_message
|
|
1511
|
+
return result
|
|
1512
|
+
|
|
1513
|
+
if not response.msgs:
|
|
1514
|
+
status_message = (
|
|
1515
|
+
"Failed to generate summary from model response."
|
|
1516
|
+
)
|
|
1517
|
+
result["status"] = status_message
|
|
1518
|
+
return result
|
|
1519
|
+
|
|
1520
|
+
summary_content = response.msgs[-1].content.strip()
|
|
1521
|
+
if not summary_content:
|
|
1522
|
+
status_message = "Generated summary is empty."
|
|
1523
|
+
result["status"] = status_message
|
|
1524
|
+
return result
|
|
1525
|
+
|
|
1526
|
+
# handle structured output if response_format was provided
|
|
1539
1527
|
structured_output = None
|
|
1540
1528
|
if response_format and response.msgs[-1].parsed:
|
|
1541
1529
|
structured_output = response.msgs[-1].parsed
|
|
1542
|
-
|
|
1530
|
+
|
|
1531
|
+
# determine filename: use provided filename, or extract from
|
|
1532
|
+
# structured output, or generate timestamp
|
|
1533
|
+
if filename:
|
|
1534
|
+
base_filename = filename
|
|
1535
|
+
elif structured_output and hasattr(
|
|
1536
|
+
structured_output, 'task_title'
|
|
1537
|
+
):
|
|
1538
|
+
# use task_title from structured output for filename
|
|
1539
|
+
task_title = structured_output.task_title
|
|
1540
|
+
clean_title = ContextUtility.sanitize_workflow_filename(
|
|
1541
|
+
task_title
|
|
1542
|
+
)
|
|
1543
|
+
base_filename = (
|
|
1544
|
+
f"{clean_title}_workflow" if clean_title else "workflow"
|
|
1545
|
+
)
|
|
1546
|
+
else:
|
|
1547
|
+
base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
|
|
1548
|
+
|
|
1549
|
+
base_filename = Path(base_filename).with_suffix("").name
|
|
1550
|
+
|
|
1551
|
+
metadata = context_util.get_session_metadata()
|
|
1552
|
+
metadata.update(
|
|
1553
|
+
{
|
|
1554
|
+
"agent_id": self.agent_id,
|
|
1555
|
+
"message_count": len(messages),
|
|
1556
|
+
}
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
# convert structured output to custom markdown if present
|
|
1560
|
+
if structured_output:
|
|
1561
|
+
# convert structured output to custom markdown
|
|
1543
1562
|
summary_content = context_util.structured_output_to_markdown(
|
|
1544
1563
|
structured_data=structured_output, metadata=metadata
|
|
1545
1564
|
)
|
|
@@ -1583,8 +1602,6 @@ class ChatAgent(BaseAgent):
|
|
|
1583
1602
|
None
|
|
1584
1603
|
"""
|
|
1585
1604
|
self.memory.clear()
|
|
1586
|
-
if self._tool_output_cache_enabled:
|
|
1587
|
-
self._tool_output_history.clear()
|
|
1588
1605
|
|
|
1589
1606
|
if self.system_message is not None:
|
|
1590
1607
|
self.update_memory(self.system_message, OpenAIBackendRole.SYSTEM)
|
|
@@ -1622,7 +1639,7 @@ class ChatAgent(BaseAgent):
|
|
|
1622
1639
|
message.
|
|
1623
1640
|
"""
|
|
1624
1641
|
self.memory.clear()
|
|
1625
|
-
#
|
|
1642
|
+
# Write system message to memory if provided
|
|
1626
1643
|
if self.system_message is not None:
|
|
1627
1644
|
self.memory.write_record(
|
|
1628
1645
|
MemoryRecord(
|
|
@@ -3015,18 +3032,14 @@ class ChatAgent(BaseAgent):
|
|
|
3015
3032
|
base_timestamp = current_time_ns / 1_000_000_000 # Convert to seconds
|
|
3016
3033
|
|
|
3017
3034
|
self.update_memory(
|
|
3018
|
-
assist_msg,
|
|
3019
|
-
OpenAIBackendRole.ASSISTANT,
|
|
3020
|
-
timestamp=base_timestamp,
|
|
3021
|
-
return_records=self._tool_output_cache_enabled,
|
|
3035
|
+
assist_msg, OpenAIBackendRole.ASSISTANT, timestamp=base_timestamp
|
|
3022
3036
|
)
|
|
3023
3037
|
|
|
3024
3038
|
# Add minimal increment to ensure function message comes after
|
|
3025
|
-
|
|
3039
|
+
self.update_memory(
|
|
3026
3040
|
func_msg,
|
|
3027
3041
|
OpenAIBackendRole.FUNCTION,
|
|
3028
3042
|
timestamp=base_timestamp + 1e-6,
|
|
3029
|
-
return_records=self._tool_output_cache_enabled,
|
|
3030
3043
|
)
|
|
3031
3044
|
|
|
3032
3045
|
# Record information about this tool call
|
|
@@ -3037,20 +3050,6 @@ class ChatAgent(BaseAgent):
|
|
|
3037
3050
|
tool_call_id=tool_call_id,
|
|
3038
3051
|
)
|
|
3039
3052
|
|
|
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
|
-
|
|
3054
3053
|
return tool_record
|
|
3055
3054
|
|
|
3056
3055
|
def _stream(
|
|
@@ -4402,9 +4401,6 @@ class ChatAgent(BaseAgent):
|
|
|
4402
4401
|
tool_execution_timeout=self.tool_execution_timeout,
|
|
4403
4402
|
pause_event=self.pause_event,
|
|
4404
4403
|
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
4404
|
stream_accumulate=self.stream_accumulate,
|
|
4409
4405
|
)
|
|
4410
4406
|
|