Skip to content

Commit 6126e80

Browse files
authored
enhance: Support async summarize in ChatAgent for faster Workforce sa… (#3299)
1 parent 1244ef1 commit 6126e80

File tree

4 files changed

+523
-11
lines changed

4 files changed

+523
-11
lines changed

camel/agents/chat_agent.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,10 @@ def summarize(
10771077
r"""Summarize the agent's current conversation context and persist it
10781078
to a markdown file.
10791079
1080+
.. deprecated:: 0.2.80
1081+
Use :meth:`asummarize` for async/await support and better
1082+
performance in parallel summarization workflows.
1083+
10801084
Args:
10811085
filename (Optional[str]): The base filename (without extension) to
10821086
use for the markdown file. Defaults to a timestamped name when
@@ -1096,7 +1100,18 @@ def summarize(
10961100
Dict[str, Any]: A dictionary containing the summary text, file
10971101
path, status message, and optionally structured_summary if
10981102
response_format was provided.
1103+
1104+
See Also:
1105+
:meth:`asummarize`: Async version for non-blocking LLM calls.
10991106
"""
1107+
import warnings
1108+
1109+
warnings.warn(
1110+
"summarize() is synchronous. Consider using asummarize() "
1111+
"for async/await support and better performance.",
1112+
DeprecationWarning,
1113+
stacklevel=2,
1114+
)
11001115

11011116
result: Dict[str, Any] = {
11021117
"summary": "",
@@ -1319,6 +1334,271 @@ def summarize(
13191334
result["status"] = error_message
13201335
return result
13211336

1337+
async def asummarize(
1338+
self,
1339+
filename: Optional[str] = None,
1340+
summary_prompt: Optional[str] = None,
1341+
response_format: Optional[Type[BaseModel]] = None,
1342+
working_directory: Optional[Union[str, Path]] = None,
1343+
) -> Dict[str, Any]:
1344+
r"""Asynchronously summarize the agent's current conversation context
1345+
and persist it to a markdown file.
1346+
1347+
This is the async version of summarize() that uses astep() for
1348+
non-blocking LLM calls, enabling parallel summarization of multiple
1349+
agents.
1350+
1351+
Args:
1352+
filename (Optional[str]): The base filename (without extension) to
1353+
use for the markdown file. Defaults to a timestamped name when
1354+
not provided.
1355+
summary_prompt (Optional[str]): Custom prompt for the summarizer.
1356+
When omitted, a default prompt highlighting key decisions,
1357+
action items, and open questions is used.
1358+
response_format (Optional[Type[BaseModel]]): A Pydantic model
1359+
defining the expected structure of the response. If provided,
1360+
the summary will be generated as structured output and included
1361+
in the result.
1362+
working_directory (Optional[str|Path]): Optional directory to save
1363+
the markdown summary file. If provided, overrides the default
1364+
directory used by ContextUtility.
1365+
1366+
Returns:
1367+
Dict[str, Any]: A dictionary containing the summary text, file
1368+
path, status message, and optionally structured_summary if
1369+
response_format was provided.
1370+
"""
1371+
1372+
result: Dict[str, Any] = {
1373+
"summary": "",
1374+
"file_path": None,
1375+
"status": "",
1376+
}
1377+
1378+
try:
1379+
# Use external context if set, otherwise create local one
1380+
if self._context_utility is None:
1381+
if working_directory is not None:
1382+
self._context_utility = ContextUtility(
1383+
working_directory=str(working_directory)
1384+
)
1385+
else:
1386+
self._context_utility = ContextUtility()
1387+
context_util = self._context_utility
1388+
1389+
# Get conversation directly from agent's memory
1390+
messages, _ = self.memory.get_context()
1391+
1392+
if not messages:
1393+
status_message = (
1394+
"No conversation context available to summarize."
1395+
)
1396+
result["status"] = status_message
1397+
return result
1398+
1399+
# Convert messages to conversation text
1400+
conversation_lines = []
1401+
for message in messages:
1402+
role = message.get('role', 'unknown')
1403+
content = message.get('content', '')
1404+
1405+
# Handle tool call messages (assistant calling tools)
1406+
tool_calls = message.get('tool_calls')
1407+
if tool_calls and isinstance(tool_calls, (list, tuple)):
1408+
for tool_call in tool_calls:
1409+
# Handle both dict and object formats
1410+
if isinstance(tool_call, dict):
1411+
func_name = tool_call.get('function', {}).get(
1412+
'name', 'unknown_tool'
1413+
)
1414+
func_args_str = tool_call.get('function', {}).get(
1415+
'arguments', '{}'
1416+
)
1417+
else:
1418+
# Handle object format (Pydantic or similar)
1419+
func_name = getattr(
1420+
getattr(tool_call, 'function', None),
1421+
'name',
1422+
'unknown_tool',
1423+
)
1424+
func_args_str = getattr(
1425+
getattr(tool_call, 'function', None),
1426+
'arguments',
1427+
'{}',
1428+
)
1429+
1430+
# Parse and format arguments for readability
1431+
try:
1432+
import json
1433+
1434+
args_dict = json.loads(func_args_str)
1435+
args_formatted = ', '.join(
1436+
f"{k}={v}" for k, v in args_dict.items()
1437+
)
1438+
except (json.JSONDecodeError, ValueError, TypeError):
1439+
args_formatted = func_args_str
1440+
1441+
conversation_lines.append(
1442+
f"[TOOL CALL] {func_name}({args_formatted})"
1443+
)
1444+
1445+
# Handle tool response messages
1446+
elif role == 'tool':
1447+
tool_name = message.get('name', 'unknown_tool')
1448+
if not content:
1449+
content = str(message.get('content', ''))
1450+
conversation_lines.append(
1451+
f"[TOOL RESULT] {tool_name}{content}"
1452+
)
1453+
1454+
# Handle regular content messages (user/assistant/system)
1455+
elif content:
1456+
conversation_lines.append(f"{role}: {content}")
1457+
1458+
conversation_text = "\n".join(conversation_lines).strip()
1459+
1460+
if not conversation_text:
1461+
status_message = (
1462+
"Conversation context is empty; skipping summary."
1463+
)
1464+
result["status"] = status_message
1465+
return result
1466+
1467+
if self._context_summary_agent is None:
1468+
self._context_summary_agent = ChatAgent(
1469+
system_message=(
1470+
"You are a helpful assistant that summarizes "
1471+
"conversations"
1472+
),
1473+
model=self.model_backend,
1474+
agent_id=f"{self.agent_id}_context_summarizer",
1475+
)
1476+
else:
1477+
self._context_summary_agent.reset()
1478+
1479+
if summary_prompt:
1480+
prompt_text = (
1481+
f"{summary_prompt.rstrip()}\n\n"
1482+
f"AGENT CONVERSATION TO BE SUMMARIZED:\n"
1483+
f"{conversation_text}"
1484+
)
1485+
else:
1486+
prompt_text = (
1487+
"Summarize the context information in concise markdown "
1488+
"bullet points highlighting key decisions, action items.\n"
1489+
f"Context information:\n{conversation_text}"
1490+
)
1491+
1492+
try:
1493+
# Use structured output if response_format is provided
1494+
if response_format:
1495+
response = await self._context_summary_agent.astep(
1496+
prompt_text, response_format=response_format
1497+
)
1498+
else:
1499+
response = await self._context_summary_agent.astep(
1500+
prompt_text
1501+
)
1502+
1503+
# Handle streaming response
1504+
if isinstance(response, AsyncStreamingChatAgentResponse):
1505+
# Collect final response
1506+
final_response = await response
1507+
response = final_response
1508+
1509+
except Exception as step_exc:
1510+
error_message = (
1511+
f"Failed to generate summary using model: {step_exc}"
1512+
)
1513+
logger.error(error_message)
1514+
result["status"] = error_message
1515+
return result
1516+
1517+
if not response.msgs:
1518+
status_message = (
1519+
"Failed to generate summary from model response."
1520+
)
1521+
result["status"] = status_message
1522+
return result
1523+
1524+
summary_content = response.msgs[-1].content.strip()
1525+
if not summary_content:
1526+
status_message = "Generated summary is empty."
1527+
result["status"] = status_message
1528+
return result
1529+
1530+
# handle structured output if response_format was provided
1531+
structured_output = None
1532+
if response_format and response.msgs[-1].parsed:
1533+
structured_output = response.msgs[-1].parsed
1534+
1535+
# determine filename: use provided filename, or extract from
1536+
# structured output, or generate timestamp
1537+
if filename:
1538+
base_filename = filename
1539+
elif structured_output and hasattr(
1540+
structured_output, 'task_title'
1541+
):
1542+
# use task_title from structured output for filename
1543+
task_title = structured_output.task_title
1544+
clean_title = ContextUtility.sanitize_workflow_filename(
1545+
task_title
1546+
)
1547+
base_filename = (
1548+
f"{clean_title}_workflow" if clean_title else "workflow"
1549+
)
1550+
else:
1551+
base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1552+
1553+
base_filename = Path(base_filename).with_suffix("").name
1554+
1555+
metadata = context_util.get_session_metadata()
1556+
metadata.update(
1557+
{
1558+
"agent_id": self.agent_id,
1559+
"message_count": len(messages),
1560+
}
1561+
)
1562+
1563+
# convert structured output to custom markdown if present
1564+
if structured_output:
1565+
# convert structured output to custom markdown
1566+
summary_content = context_util.structured_output_to_markdown(
1567+
structured_data=structured_output, metadata=metadata
1568+
)
1569+
1570+
# Save the markdown (either custom structured or default)
1571+
save_status = context_util.save_markdown_file(
1572+
base_filename,
1573+
summary_content,
1574+
title="Conversation Summary"
1575+
if not structured_output
1576+
else None,
1577+
metadata=metadata if not structured_output else None,
1578+
)
1579+
1580+
file_path = (
1581+
context_util.get_working_directory() / f"{base_filename}.md"
1582+
)
1583+
1584+
# Prepare result dictionary
1585+
result_dict = {
1586+
"summary": summary_content,
1587+
"file_path": str(file_path),
1588+
"status": save_status,
1589+
"structured_summary": structured_output,
1590+
}
1591+
1592+
result.update(result_dict)
1593+
logger.info("Conversation summary saved to %s", file_path)
1594+
return result
1595+
1596+
except Exception as exc:
1597+
error_message = f"Failed to summarize conversation context: {exc}"
1598+
logger.error(error_message)
1599+
result["status"] = error_message
1600+
return result
1601+
13221602
def clear_memory(self) -> None:
13231603
r"""Clear the agent's memory and reset to initial state.
13241604

0 commit comments

Comments
 (0)