Skip to content

Commit 328bec8

Browse files
enhance: Workflow better filenames and custom session id (#3285)
Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
1 parent a9a407f commit 328bec8

File tree

5 files changed

+332
-53
lines changed

5 files changed

+332
-53
lines changed

camel/agents/chat_agent.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,11 +1247,29 @@ def summarize(
12471247
result["status"] = status_message
12481248
return result
12491249

1250-
base_filename = (
1251-
filename
1252-
if filename
1253-
else f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1254-
)
1250+
# handle structured output if response_format was provided
1251+
structured_output = None
1252+
if response_format and response.msgs[-1].parsed:
1253+
structured_output = response.msgs[-1].parsed
1254+
1255+
# determine filename: use provided filename, or extract from
1256+
# structured output, or generate timestamp
1257+
if filename:
1258+
base_filename = filename
1259+
elif structured_output and hasattr(
1260+
structured_output, 'task_title'
1261+
):
1262+
# use task_title from structured output for filename
1263+
task_title = structured_output.task_title
1264+
clean_title = ContextUtility.sanitize_workflow_filename(
1265+
task_title
1266+
)
1267+
base_filename = (
1268+
f"{clean_title}_workflow" if clean_title else "workflow"
1269+
)
1270+
else:
1271+
base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1272+
12551273
base_filename = Path(base_filename).with_suffix("").name
12561274

12571275
metadata = context_util.get_session_metadata()
@@ -1262,11 +1280,9 @@ def summarize(
12621280
}
12631281
)
12641282

1265-
# Handle structured output if response_format was provided
1266-
structured_output = None
1267-
if response_format and response.msgs[-1].parsed:
1268-
structured_output = response.msgs[-1].parsed
1269-
# Convert structured output to custom markdown
1283+
# convert structured output to custom markdown if present
1284+
if structured_output:
1285+
# convert structured output to custom markdown
12701286
summary_content = context_util.structured_output_to_markdown(
12711287
structured_data=structured_output, metadata=metadata
12721288
)

camel/societies/workforce/single_agent_worker.py

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def __init__(
8080
self._in_use_agents: set = set()
8181
self._agent_last_used: dict = {}
8282
self._lock = asyncio.Lock()
83+
self._condition = asyncio.Condition(self._lock)
8384

8485
# Statistics
8586
self._total_borrows = 0
@@ -105,36 +106,31 @@ def _create_fresh_agent(self) -> ChatAgent:
105106

106107
async def get_agent(self) -> ChatAgent:
107108
r"""Get an agent from the pool, creating one if necessary."""
108-
async with self._lock:
109+
async with self._condition:
109110
self._total_borrows += 1
110111

111-
if self._available_agents:
112-
agent = self._available_agents.popleft()
113-
self._in_use_agents.add(id(agent))
114-
self._pool_hits += 1
115-
return agent
116-
117-
# Check if we can create a new agent
118-
if len(self._in_use_agents) < self.max_size or self.auto_scale:
119-
agent = self._create_fresh_agent()
120-
self._in_use_agents.add(id(agent))
121-
return agent
122-
123-
# Wait for available agent
124-
while True:
125-
async with self._lock:
112+
# Try to get available agent or create new one
113+
while True:
126114
if self._available_agents:
127115
agent = self._available_agents.popleft()
128116
self._in_use_agents.add(id(agent))
129117
self._pool_hits += 1
130118
return agent
131-
await asyncio.sleep(0.05)
119+
120+
# Check if we can create a new agent
121+
if len(self._in_use_agents) < self.max_size or self.auto_scale:
122+
agent = self._create_fresh_agent()
123+
self._in_use_agents.add(id(agent))
124+
return agent
125+
126+
# Wait for an agent to be returned
127+
await self._condition.wait()
132128

133129
async def return_agent(self, agent: ChatAgent) -> None:
134130
r"""Return an agent to the pool."""
135131
agent_id = id(agent)
136132

137-
async with self._lock:
133+
async with self._condition:
138134
if agent_id not in self._in_use_agents:
139135
return
140136

@@ -145,6 +141,8 @@ async def return_agent(self, agent: ChatAgent) -> None:
145141
agent.reset()
146142
self._agent_last_used[agent_id] = time.time()
147143
self._available_agents.append(agent)
144+
# Notify one waiting coroutine that an agent is available
145+
self._condition.notify()
148146
else:
149147
# Remove tracking for agents not returned to pool
150148
self._agent_last_used.pop(agent_id, None)
@@ -154,7 +152,7 @@ async def cleanup_idle_agents(self) -> None:
154152
if not self.auto_scale:
155153
return
156154

157-
async with self._lock:
155+
async with self._condition:
158156
if not self._available_agents:
159157
return
160158

@@ -428,6 +426,7 @@ async def _process_task(
428426
"usage"
429427
) or final_response.info.get("token_usage")
430428
else:
429+
final_response = response
431430
usage_info = response.info.get("usage") or response.info.get(
432431
"token_usage"
433432
)
@@ -562,10 +561,11 @@ async def _periodic_cleanup(self):
562561
while True:
563562
try:
564563
# Fixed interval cleanup
565-
await asyncio.sleep(self.agent_pool.cleanup_interval)
566-
567564
if self.agent_pool:
565+
await asyncio.sleep(self.agent_pool.cleanup_interval)
568566
await self.agent_pool.cleanup_idle_agents()
567+
else:
568+
break
569569
except asyncio.CancelledError:
570570
break
571571
except Exception as e:
@@ -583,7 +583,8 @@ def save_workflow_memories(self) -> Dict[str, Any]:
583583
584584
This method generates a workflow summary from the worker agent's
585585
conversation history and saves it to a markdown file. The filename
586-
is based on the worker's description for easy loading later.
586+
is based on either the worker's explicit role_name or the generated
587+
task_title from the summary.
587588
588589
Returns:
589590
Dict[str, Any]: Result dictionary with keys:
@@ -603,13 +604,31 @@ def save_workflow_memories(self) -> Dict[str, Any]:
603604
self.worker.set_context_utility(context_util)
604605

605606
# prepare workflow summarization components
606-
filename = self._generate_workflow_filename()
607607
structured_prompt = self._prepare_workflow_prompt()
608608
agent_to_summarize = self._select_agent_for_summarization(
609609
context_util
610610
)
611611

612+
# check if we should use role_name or let summarize extract
613+
# task_title
614+
role_name = getattr(self.worker, 'role_name', 'assistant')
615+
use_role_name_for_filename = role_name.lower() not in {
616+
'assistant',
617+
'agent',
618+
'user',
619+
'system',
620+
}
621+
612622
# generate and save workflow summary
623+
# if role_name is explicit, use it for filename
624+
# if role_name is generic, pass none to let summarize use
625+
# task_title
626+
filename = (
627+
self._generate_workflow_filename()
628+
if use_role_name_for_filename
629+
else None
630+
)
631+
613632
result = agent_to_summarize.summarize(
614633
filename=filename,
615634
summary_prompt=structured_prompt,
@@ -716,12 +735,23 @@ def _find_workflow_files(
716735
)
717736
return []
718737

719-
# generate filename-safe search pattern from worker description
738+
# generate filename-safe search pattern from worker role name
720739
if pattern is None:
721-
# sanitize description: spaces to underscores, remove special chars
722-
clean_desc = self.description.lower().replace(" ", "_")
723-
clean_desc = re.sub(r'[^a-z0-9_]', '', clean_desc)
724-
pattern = f"{clean_desc}_workflow*.md"
740+
from camel.utils.context_utils import ContextUtility
741+
742+
# get role_name (always available, defaults to "assistant")
743+
role_name = getattr(self.worker, 'role_name', 'assistant')
744+
clean_name = ContextUtility.sanitize_workflow_filename(role_name)
745+
746+
# check if role_name is generic
747+
generic_names = {'assistant', 'agent', 'user', 'system'}
748+
if clean_name in generic_names:
749+
# for generic role names, search for all workflow files
750+
# since filename is based on task_title
751+
pattern = "*_workflow*.md"
752+
else:
753+
# for explicit role names, search for role-specific files
754+
pattern = f"{clean_name}_workflow*.md"
725755

726756
# Get the base workforce_workflows directory
727757
camel_workdir = os.environ.get("CAMEL_WORKDIR")
@@ -816,15 +846,21 @@ def _validate_workflow_save_requirements(self) -> Optional[Dict[str, Any]]:
816846
return None
817847

818848
def _generate_workflow_filename(self) -> str:
819-
r"""Generate a filename for the workflow based on worker description.
849+
r"""Generate a filename for the workflow based on worker role name.
850+
851+
Uses the worker's explicit role_name when available.
820852
821853
Returns:
822-
str: Sanitized filename without timestamp (session already has
823-
timestamp).
854+
str: Sanitized filename without timestamp and without .md
855+
extension. Format: {role_name}_workflow
824856
"""
825-
clean_desc = self.description.lower().replace(" ", "_")
826-
clean_desc = re.sub(r'[^a-z0-9_]', '', clean_desc)
827-
return f"{clean_desc}_workflow"
857+
from camel.utils.context_utils import ContextUtility
858+
859+
# get role_name (always available, defaults to "assistant"/"Assistant")
860+
role_name = getattr(self.worker, 'role_name', 'assistant')
861+
clean_name = ContextUtility.sanitize_workflow_filename(role_name)
862+
863+
return f"{clean_name}_workflow"
828864

829865
def _prepare_workflow_prompt(self) -> str:
830866
r"""Prepare the structured prompt for workflow summarization.

camel/societies/workforce/workforce.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -463,20 +463,27 @@ def __init__(
463463
# Helper for propagating pause control to externally supplied agents
464464
# ------------------------------------------------------------------
465465

466-
def _get_or_create_shared_context_utility(self) -> "ContextUtility":
466+
def _get_or_create_shared_context_utility(
467+
self,
468+
session_id: Optional[str] = None,
469+
) -> "ContextUtility":
467470
r"""Get or create the shared context utility for workflow management.
468471
469472
This method creates the context utility only when needed, avoiding
470473
unnecessary session folder creation during initialization.
471474
475+
Args:
476+
session_id (Optional[str]): Custom session ID to use. If None,
477+
auto-generates a timestamped session ID. (default: :obj:`None`)
478+
472479
Returns:
473480
ContextUtility: The shared context utility instance.
474481
"""
475482
if self._shared_context_utility is None:
476483
from camel.utils.context_utils import ContextUtility
477484

478-
self._shared_context_utility = (
479-
ContextUtility.get_workforce_shared()
485+
self._shared_context_utility = ContextUtility.get_workforce_shared(
486+
session_id=session_id
480487
)
481488
return self._shared_context_utility
482489

@@ -2132,7 +2139,10 @@ def reset(self) -> None:
21322139
else:
21332140
self.metrics_logger = WorkforceLogger(workforce_id=self.node_id)
21342141

2135-
def save_workflow_memories(self) -> Dict[str, str]:
2142+
def save_workflow_memories(
2143+
self,
2144+
session_id: Optional[str] = None,
2145+
) -> Dict[str, str]:
21362146
r"""Save workflow memories for all SingleAgentWorker instances in the
21372147
workforce.
21382148
@@ -2142,6 +2152,12 @@ def save_workflow_memories(self) -> Dict[str, str]:
21422152
method.
21432153
Other worker types are skipped.
21442154
2155+
Args:
2156+
session_id (Optional[str]): Custom session ID to use for saving
2157+
workflows. If None, auto-generates a timestamped session ID.
2158+
Useful for organizing workflows by project or context.
2159+
(default: :obj:`None`)
2160+
21452161
Returns:
21462162
Dict[str, str]: Dictionary mapping worker node IDs to save results.
21472163
Values are either file paths (success) or error messages
@@ -2150,15 +2166,22 @@ def save_workflow_memories(self) -> Dict[str, str]:
21502166
Example:
21512167
>>> workforce = Workforce("My Team")
21522168
>>> # ... add workers and process tasks ...
2153-
>>> results = workforce.save_workflows()
2169+
>>> # save with auto-generated session id
2170+
>>> results = workforce.save_workflow_memories()
21542171
>>> print(results)
2155-
{'worker_123': '/path/to/data_analyst_workflow_20250122.md',
2172+
{'worker_123': '/path/to/developer_agent_workflow.md',
21562173
'worker_456': 'error: No conversation context available'}
2174+
>>> # save with custom project id
2175+
>>> results = workforce.save_workflow_memories(
2176+
... session_id="project_123"
2177+
... )
21572178
"""
21582179
results = {}
21592180

21602181
# Get or create shared context utility for this save operation
2161-
shared_context_utility = self._get_or_create_shared_context_utility()
2182+
shared_context_utility = self._get_or_create_shared_context_utility(
2183+
session_id=session_id
2184+
)
21622185

21632186
for child in self._children:
21642187
if isinstance(child, SingleAgentWorker):

0 commit comments

Comments
 (0)