|
23 | 23 | import threading |
24 | 24 | import time |
25 | 25 | import uuid |
| 26 | +from datetime import datetime |
26 | 27 | from pathlib import Path |
27 | 28 | from typing import ( |
28 | 29 | TYPE_CHECKING, |
|
93 | 94 | model_from_json_schema, |
94 | 95 | ) |
95 | 96 | from camel.utils.commons import dependencies_required |
| 97 | +from camel.utils.context_utils import ContextUtility |
96 | 98 |
|
97 | 99 | if TYPE_CHECKING: |
98 | 100 | from camel.terminators import ResponseTerminator |
@@ -535,6 +537,8 @@ def __init__( |
535 | 537 | self.retry_attempts = max(1, retry_attempts) |
536 | 538 | self.retry_delay = max(0.0, retry_delay) |
537 | 539 | self.step_timeout = step_timeout |
| 540 | + self._context_utility: Optional[ContextUtility] = None |
| 541 | + self._context_summary_agent: Optional["ChatAgent"] = None |
538 | 542 | self.stream_accumulate = stream_accumulate |
539 | 543 |
|
540 | 544 | def reset(self): |
@@ -1042,6 +1046,149 @@ def save_memory(self, path: str) -> None: |
1042 | 1046 | json_store.save(to_save) |
1043 | 1047 | logger.info(f"Memory saved to {path}") |
1044 | 1048 |
|
| 1049 | + def summarize( |
| 1050 | + self, |
| 1051 | + filename: Optional[str] = None, |
| 1052 | + summary_prompt: Optional[str] = None, |
| 1053 | + ) -> Dict[str, Any]: |
| 1054 | + r"""Summarize the agent's current conversation context and persist it |
| 1055 | + to a markdown file. |
| 1056 | +
|
| 1057 | + Args: |
| 1058 | + filename (Optional[str]): The base filename (without extension) to |
| 1059 | + use for the markdown file. Defaults to a timestamped name when |
| 1060 | + not provided. |
| 1061 | + summary_prompt (Optional[str]): Custom prompt for the summarizer. |
| 1062 | + When omitted, a default prompt highlighting key decisions, |
| 1063 | + action items, and open questions is used. |
| 1064 | +
|
| 1065 | + Returns: |
| 1066 | + Dict[str, Any]: A dictionary containing the summary text, file |
| 1067 | + path, and status message. |
| 1068 | + """ |
| 1069 | + |
| 1070 | + result: Dict[str, Any] = { |
| 1071 | + "summary": "", |
| 1072 | + "file_path": None, |
| 1073 | + "status": "", |
| 1074 | + } |
| 1075 | + |
| 1076 | + try: |
| 1077 | + if self._context_utility is None: |
| 1078 | + self._context_utility = ContextUtility() |
| 1079 | + |
| 1080 | + # Get conversation directly from agent's memory |
| 1081 | + messages, _ = self.memory.get_context() |
| 1082 | + |
| 1083 | + if not messages: |
| 1084 | + status_message = ( |
| 1085 | + "No conversation context available to summarize." |
| 1086 | + ) |
| 1087 | + result["status"] = status_message |
| 1088 | + return result |
| 1089 | + |
| 1090 | + # Convert messages to conversation text |
| 1091 | + conversation_lines = [] |
| 1092 | + for message in messages: |
| 1093 | + role = message.get('role', 'unknown') |
| 1094 | + content = message.get('content', '') |
| 1095 | + if content: |
| 1096 | + conversation_lines.append(f"{role}: {content}") |
| 1097 | + |
| 1098 | + conversation_text = "\n".join(conversation_lines).strip() |
| 1099 | + |
| 1100 | + if not conversation_text: |
| 1101 | + status_message = ( |
| 1102 | + "Conversation context is empty; skipping summary." |
| 1103 | + ) |
| 1104 | + result["status"] = status_message |
| 1105 | + return result |
| 1106 | + |
| 1107 | + if self._context_summary_agent is None: |
| 1108 | + self._context_summary_agent = ChatAgent( |
| 1109 | + system_message=( |
| 1110 | + "You are a helpful assistant that summarizes " |
| 1111 | + "conversations into concise markdown bullet lists." |
| 1112 | + ), |
| 1113 | + model=self.model_backend, |
| 1114 | + agent_id=f"{self.agent_id}_context_summarizer", |
| 1115 | + ) |
| 1116 | + else: |
| 1117 | + self._context_summary_agent.reset() |
| 1118 | + |
| 1119 | + prompt_text = summary_prompt or ( |
| 1120 | + "Summarize the following conversation in concise markdown " |
| 1121 | + "bullet points highlighting key decisions, action items, and " |
| 1122 | + "open questions.\n\n" |
| 1123 | + f"{conversation_text}" |
| 1124 | + ) |
| 1125 | + |
| 1126 | + try: |
| 1127 | + response = self._context_summary_agent.step(prompt_text) |
| 1128 | + except Exception as step_exc: |
| 1129 | + error_message = ( |
| 1130 | + f"Failed to generate summary using model: {step_exc}" |
| 1131 | + ) |
| 1132 | + logger.error(error_message) |
| 1133 | + result["status"] = error_message |
| 1134 | + return result |
| 1135 | + |
| 1136 | + if not response.msgs: |
| 1137 | + status_message = ( |
| 1138 | + "Failed to generate summary from model response." |
| 1139 | + ) |
| 1140 | + result["status"] = status_message |
| 1141 | + return result |
| 1142 | + |
| 1143 | + summary_content = response.msgs[-1].content.strip() |
| 1144 | + if not summary_content: |
| 1145 | + status_message = "Generated summary is empty." |
| 1146 | + result["status"] = status_message |
| 1147 | + return result |
| 1148 | + |
| 1149 | + base_filename = ( |
| 1150 | + filename |
| 1151 | + if filename |
| 1152 | + else f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501 |
| 1153 | + ) |
| 1154 | + base_filename = Path(base_filename).with_suffix("").name |
| 1155 | + |
| 1156 | + metadata = self._context_utility.get_session_metadata() |
| 1157 | + metadata.update( |
| 1158 | + { |
| 1159 | + "agent_id": self.agent_id, |
| 1160 | + "message_count": len(messages), |
| 1161 | + } |
| 1162 | + ) |
| 1163 | + |
| 1164 | + save_status = self._context_utility.save_markdown_file( |
| 1165 | + base_filename, |
| 1166 | + summary_content, |
| 1167 | + title="Conversation Summary", |
| 1168 | + metadata=metadata, |
| 1169 | + ) |
| 1170 | + |
| 1171 | + file_path = ( |
| 1172 | + self._context_utility.get_working_directory() |
| 1173 | + / f"{base_filename}.md" |
| 1174 | + ) |
| 1175 | + |
| 1176 | + result.update( |
| 1177 | + { |
| 1178 | + "summary": summary_content, |
| 1179 | + "file_path": str(file_path), |
| 1180 | + "status": save_status, |
| 1181 | + } |
| 1182 | + ) |
| 1183 | + logger.info("Conversation summary saved to %s", file_path) |
| 1184 | + return result |
| 1185 | + |
| 1186 | + except Exception as exc: |
| 1187 | + error_message = f"Failed to summarize conversation context: {exc}" |
| 1188 | + logger.error(error_message) |
| 1189 | + result["status"] = error_message |
| 1190 | + return result |
| 1191 | + |
1045 | 1192 | def clear_memory(self) -> None: |
1046 | 1193 | r"""Clear the agent's memory and reset to initial state. |
1047 | 1194 |
|
|
0 commit comments