@@ -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