camel-ai 0.2.71a7__py3-none-any.whl → 0.2.71a9__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/societies/workforce/single_agent_worker.py +53 -9
- camel/societies/workforce/task_channel.py +4 -1
- camel/societies/workforce/workforce.py +146 -14
- camel/tasks/task.py +104 -4
- camel/toolkits/file_write_toolkit.py +19 -8
- camel/toolkits/hybrid_browser_toolkit/actions.py +28 -18
- camel/toolkits/hybrid_browser_toolkit/agent.py +7 -1
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +48 -18
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +447 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +272 -85
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +5 -4
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +572 -17
- camel/toolkits/note_taking_toolkit.py +24 -9
- camel/toolkits/pptx_toolkit.py +21 -8
- camel/toolkits/search_toolkit.py +15 -5
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/METADATA +1 -1
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/RECORD +20 -20
- camel/toolkits/hybrid_browser_toolkit/stealth_config.py +0 -116
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/licenses/LICENSE +0 -0
camel/__init__.py
CHANGED
|
@@ -22,6 +22,7 @@ from typing import Any, List, Optional
|
|
|
22
22
|
from colorama import Fore
|
|
23
23
|
|
|
24
24
|
from camel.agents import ChatAgent
|
|
25
|
+
from camel.agents.chat_agent import AsyncStreamingChatAgentResponse
|
|
25
26
|
from camel.societies.workforce.prompts import PROCESS_TASK_PROMPT
|
|
26
27
|
from camel.societies.workforce.structured_output_handler import (
|
|
27
28
|
StructuredOutputHandler,
|
|
@@ -285,6 +286,7 @@ class SingleAgentWorker(Worker):
|
|
|
285
286
|
"""
|
|
286
287
|
# Get agent efficiently (from pool or by cloning)
|
|
287
288
|
worker_agent = await self._get_worker_agent()
|
|
289
|
+
response_content = ""
|
|
288
290
|
|
|
289
291
|
try:
|
|
290
292
|
dependency_tasks_info = self._get_dep_tasks_info(dependencies)
|
|
@@ -314,11 +316,23 @@ class SingleAgentWorker(Worker):
|
|
|
314
316
|
)
|
|
315
317
|
)
|
|
316
318
|
response = await worker_agent.astep(enhanced_prompt)
|
|
319
|
+
|
|
320
|
+
# Handle streaming response
|
|
321
|
+
if isinstance(response, AsyncStreamingChatAgentResponse):
|
|
322
|
+
content = ""
|
|
323
|
+
async for chunk in response:
|
|
324
|
+
if chunk.msg:
|
|
325
|
+
content = chunk.msg.content
|
|
326
|
+
response_content = content
|
|
327
|
+
else:
|
|
328
|
+
# Regular ChatAgentResponse
|
|
329
|
+
response_content = (
|
|
330
|
+
response.msg.content if response.msg else ""
|
|
331
|
+
)
|
|
332
|
+
|
|
317
333
|
task_result = (
|
|
318
334
|
self.structured_handler.parse_structured_response(
|
|
319
|
-
response_text=
|
|
320
|
-
if response.msg
|
|
321
|
-
else "",
|
|
335
|
+
response_text=response_content,
|
|
322
336
|
schema=TaskResult,
|
|
323
337
|
fallback_values={
|
|
324
338
|
"content": "Task processing failed",
|
|
@@ -331,13 +345,41 @@ class SingleAgentWorker(Worker):
|
|
|
331
345
|
response = await worker_agent.astep(
|
|
332
346
|
prompt, response_format=TaskResult
|
|
333
347
|
)
|
|
334
|
-
|
|
348
|
+
|
|
349
|
+
# Handle streaming response for native output
|
|
350
|
+
if isinstance(response, AsyncStreamingChatAgentResponse):
|
|
351
|
+
task_result = None
|
|
352
|
+
async for chunk in response:
|
|
353
|
+
if chunk.msg and chunk.msg.parsed:
|
|
354
|
+
task_result = chunk.msg.parsed
|
|
355
|
+
response_content = chunk.msg.content
|
|
356
|
+
# If no parsed result found in streaming, create fallback
|
|
357
|
+
if task_result is None:
|
|
358
|
+
task_result = TaskResult(
|
|
359
|
+
content="Failed to parse streaming response",
|
|
360
|
+
failed=True,
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
# Regular ChatAgentResponse
|
|
364
|
+
task_result = response.msg.parsed
|
|
365
|
+
response_content = (
|
|
366
|
+
response.msg.content if response.msg else ""
|
|
367
|
+
)
|
|
335
368
|
|
|
336
369
|
# Get token usage from the response
|
|
337
|
-
|
|
338
|
-
|
|
370
|
+
if isinstance(response, AsyncStreamingChatAgentResponse):
|
|
371
|
+
# For streaming responses, get the final response info
|
|
372
|
+
final_response = await response
|
|
373
|
+
usage_info = final_response.info.get(
|
|
374
|
+
"usage"
|
|
375
|
+
) or final_response.info.get("token_usage")
|
|
376
|
+
else:
|
|
377
|
+
usage_info = response.info.get("usage") or response.info.get(
|
|
378
|
+
"token_usage"
|
|
379
|
+
)
|
|
380
|
+
total_tokens = (
|
|
381
|
+
usage_info.get("total_tokens", 0) if usage_info else 0
|
|
339
382
|
)
|
|
340
|
-
total_tokens = usage_info.get("total_tokens", 0)
|
|
341
383
|
|
|
342
384
|
except Exception as e:
|
|
343
385
|
print(
|
|
@@ -369,8 +411,10 @@ class SingleAgentWorker(Worker):
|
|
|
369
411
|
f"(from pool/clone of "
|
|
370
412
|
f"{getattr(self.worker, 'agent_id', self.worker.role_name)}) "
|
|
371
413
|
f"to process task {task.content}",
|
|
372
|
-
"response_content":
|
|
373
|
-
"tool_calls":
|
|
414
|
+
"response_content": response_content,
|
|
415
|
+
"tool_calls": final_response.info.get("tool_calls")
|
|
416
|
+
if isinstance(response, AsyncStreamingChatAgentResponse)
|
|
417
|
+
else response.info.get("tool_calls"),
|
|
374
418
|
"total_tokens": total_tokens,
|
|
375
419
|
}
|
|
376
420
|
|
|
@@ -91,11 +91,14 @@ class TaskChannel:
|
|
|
91
91
|
"""
|
|
92
92
|
async with self._condition:
|
|
93
93
|
while True:
|
|
94
|
-
for packet in self._task_dict.
|
|
94
|
+
for task_id, packet in list(self._task_dict.items()):
|
|
95
95
|
if packet.publisher_id != publisher_id:
|
|
96
96
|
continue
|
|
97
97
|
if packet.status != PacketStatus.RETURNED:
|
|
98
98
|
continue
|
|
99
|
+
# Remove the task to prevent returning it again
|
|
100
|
+
del self._task_dict[task_id]
|
|
101
|
+
self._condition.notify_all()
|
|
99
102
|
return packet.task
|
|
100
103
|
await self._condition.wait()
|
|
101
104
|
|
|
@@ -25,6 +25,7 @@ from typing import (
|
|
|
25
25
|
Coroutine,
|
|
26
26
|
Deque,
|
|
27
27
|
Dict,
|
|
28
|
+
Generator,
|
|
28
29
|
List,
|
|
29
30
|
Optional,
|
|
30
31
|
Set,
|
|
@@ -420,6 +421,11 @@ class Workforce(BaseNode):
|
|
|
420
421
|
"CodeExecutionToolkit, and ThinkingToolkit. To customize "
|
|
421
422
|
"runtime worker creation, pass a ChatAgent instance."
|
|
422
423
|
)
|
|
424
|
+
else:
|
|
425
|
+
# Validate new_worker_agent if provided
|
|
426
|
+
self._validate_agent_compatibility(
|
|
427
|
+
new_worker_agent, "new_worker_agent"
|
|
428
|
+
)
|
|
423
429
|
|
|
424
430
|
if self.share_memory:
|
|
425
431
|
logger.info(
|
|
@@ -432,6 +438,42 @@ class Workforce(BaseNode):
|
|
|
432
438
|
# Helper for propagating pause control to externally supplied agents
|
|
433
439
|
# ------------------------------------------------------------------
|
|
434
440
|
|
|
441
|
+
def _validate_agent_compatibility(
|
|
442
|
+
self, agent: ChatAgent, agent_context: str = "agent"
|
|
443
|
+
) -> None:
|
|
444
|
+
r"""Validate that agent configuration is compatible with workforce
|
|
445
|
+
settings.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
agent (ChatAgent): The agent to validate.
|
|
449
|
+
agent_context (str): Context description for error messages.
|
|
450
|
+
|
|
451
|
+
Raises:
|
|
452
|
+
ValueError: If agent has tools and stream mode enabled but
|
|
453
|
+
use_structured_output_handler is False.
|
|
454
|
+
"""
|
|
455
|
+
agent_has_tools = (
|
|
456
|
+
bool(agent.tool_dict) if hasattr(agent, 'tool_dict') else False
|
|
457
|
+
)
|
|
458
|
+
agent_stream_mode = (
|
|
459
|
+
getattr(agent.model_backend, 'stream', False)
|
|
460
|
+
if hasattr(agent, 'model_backend')
|
|
461
|
+
else False
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
agent_has_tools
|
|
466
|
+
and agent_stream_mode
|
|
467
|
+
and not self.use_structured_output_handler
|
|
468
|
+
):
|
|
469
|
+
raise ValueError(
|
|
470
|
+
f"{agent_context} has tools and stream mode enabled, but "
|
|
471
|
+
"use_structured_output_handler is False. Native structured "
|
|
472
|
+
"output doesn't work with tool calls in stream mode. "
|
|
473
|
+
"Please set use_structured_output_handler=True when creating "
|
|
474
|
+
"the Workforce."
|
|
475
|
+
)
|
|
476
|
+
|
|
435
477
|
def _attach_pause_event_to_agent(self, agent: ChatAgent) -> None:
|
|
436
478
|
r"""Ensure the given ChatAgent shares this workforce's pause_event.
|
|
437
479
|
|
|
@@ -551,6 +593,10 @@ class Workforce(BaseNode):
|
|
|
551
593
|
continue
|
|
552
594
|
|
|
553
595
|
if not memory_records:
|
|
596
|
+
logger.warning(
|
|
597
|
+
"No valid memory records could be reconstructed "
|
|
598
|
+
"for sharing"
|
|
599
|
+
)
|
|
554
600
|
return
|
|
555
601
|
|
|
556
602
|
# Share with coordinator agent
|
|
@@ -668,12 +714,21 @@ class Workforce(BaseNode):
|
|
|
668
714
|
if task_id in self._task_start_times:
|
|
669
715
|
del self._task_start_times[task_id]
|
|
670
716
|
|
|
671
|
-
|
|
717
|
+
if task_id in self._task_dependencies:
|
|
718
|
+
del self._task_dependencies[task_id]
|
|
719
|
+
|
|
720
|
+
if task_id in self._assignees:
|
|
721
|
+
del self._assignees[task_id]
|
|
722
|
+
|
|
723
|
+
def _decompose_task(
|
|
724
|
+
self, task: Task
|
|
725
|
+
) -> Union[List[Task], Generator[List[Task], None, None]]:
|
|
672
726
|
r"""Decompose the task into subtasks. This method will also set the
|
|
673
727
|
relationship between the task and its subtasks.
|
|
674
728
|
|
|
675
729
|
Returns:
|
|
676
|
-
List[Task]
|
|
730
|
+
Union[List[Task], Generator[List[Task], None, None]]:
|
|
731
|
+
The subtasks or generator of subtasks.
|
|
677
732
|
"""
|
|
678
733
|
decompose_prompt = WF_TASK_DECOMPOSE_PROMPT.format(
|
|
679
734
|
content=task.content,
|
|
@@ -681,13 +736,30 @@ class Workforce(BaseNode):
|
|
|
681
736
|
additional_info=task.additional_info,
|
|
682
737
|
)
|
|
683
738
|
self.task_agent.reset()
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
#
|
|
687
|
-
if
|
|
688
|
-
|
|
739
|
+
result = task.decompose(self.task_agent, decompose_prompt)
|
|
740
|
+
|
|
741
|
+
# Handle both streaming and non-streaming results
|
|
742
|
+
if isinstance(result, Generator):
|
|
743
|
+
# This is a generator (streaming mode)
|
|
744
|
+
def streaming_with_dependencies():
|
|
745
|
+
all_subtasks = []
|
|
746
|
+
for new_tasks in result:
|
|
747
|
+
all_subtasks.extend(new_tasks)
|
|
748
|
+
# Update dependency tracking for each batch of new tasks
|
|
749
|
+
if new_tasks:
|
|
750
|
+
self._update_dependencies_for_decomposition(
|
|
751
|
+
task, all_subtasks
|
|
752
|
+
)
|
|
753
|
+
yield new_tasks
|
|
689
754
|
|
|
690
|
-
|
|
755
|
+
return streaming_with_dependencies()
|
|
756
|
+
else:
|
|
757
|
+
# This is a regular list (non-streaming mode)
|
|
758
|
+
subtasks = result
|
|
759
|
+
# Update dependency tracking for decomposed task
|
|
760
|
+
if subtasks:
|
|
761
|
+
self._update_dependencies_for_decomposition(task, subtasks)
|
|
762
|
+
return subtasks
|
|
691
763
|
|
|
692
764
|
def _analyze_failure(
|
|
693
765
|
self, task: Task, error_message: str
|
|
@@ -991,8 +1063,13 @@ class Workforce(BaseNode):
|
|
|
991
1063
|
tasks_dict = {task.id: task for task in self._pending_tasks}
|
|
992
1064
|
|
|
993
1065
|
# Check if all provided IDs exist
|
|
994
|
-
|
|
995
|
-
|
|
1066
|
+
invalid_ids = [
|
|
1067
|
+
task_id for task_id in task_ids if task_id not in tasks_dict
|
|
1068
|
+
]
|
|
1069
|
+
if invalid_ids:
|
|
1070
|
+
logger.warning(
|
|
1071
|
+
f"Task IDs not found in pending tasks: {invalid_ids}"
|
|
1072
|
+
)
|
|
996
1073
|
return False
|
|
997
1074
|
|
|
998
1075
|
# Check if we have the same number of tasks
|
|
@@ -1133,7 +1210,17 @@ class Workforce(BaseNode):
|
|
|
1133
1210
|
task.state = TaskState.FAILED
|
|
1134
1211
|
# The agent tend to be overconfident on the whole task, so we
|
|
1135
1212
|
# decompose the task into subtasks first
|
|
1136
|
-
|
|
1213
|
+
subtasks_result = self._decompose_task(task)
|
|
1214
|
+
|
|
1215
|
+
# Handle both streaming and non-streaming results
|
|
1216
|
+
if isinstance(subtasks_result, Generator):
|
|
1217
|
+
# This is a generator (streaming mode)
|
|
1218
|
+
subtasks = []
|
|
1219
|
+
for new_tasks in subtasks_result:
|
|
1220
|
+
subtasks.extend(new_tasks)
|
|
1221
|
+
else:
|
|
1222
|
+
# This is a regular list (non-streaming mode)
|
|
1223
|
+
subtasks = subtasks_result
|
|
1137
1224
|
if self.metrics_logger and subtasks:
|
|
1138
1225
|
self.metrics_logger.log_task_decomposed(
|
|
1139
1226
|
parent_task_id=task.id, subtask_ids=[st.id for st in subtasks]
|
|
@@ -1250,7 +1337,17 @@ class Workforce(BaseNode):
|
|
|
1250
1337
|
task.state = TaskState.FAILED # TODO: Add logic for OPEN
|
|
1251
1338
|
|
|
1252
1339
|
# Decompose the task into subtasks first
|
|
1253
|
-
|
|
1340
|
+
subtasks_result = self._decompose_task(task)
|
|
1341
|
+
|
|
1342
|
+
# Handle both streaming and non-streaming results
|
|
1343
|
+
if isinstance(subtasks_result, Generator):
|
|
1344
|
+
# This is a generator (streaming mode)
|
|
1345
|
+
subtasks = []
|
|
1346
|
+
for new_tasks in subtasks_result:
|
|
1347
|
+
subtasks.extend(new_tasks)
|
|
1348
|
+
else:
|
|
1349
|
+
# This is a regular list (non-streaming mode)
|
|
1350
|
+
subtasks = subtasks_result
|
|
1254
1351
|
if subtasks:
|
|
1255
1352
|
# If decomposition happened, the original task becomes a container.
|
|
1256
1353
|
# We only execute its subtasks.
|
|
@@ -1420,12 +1517,18 @@ class Workforce(BaseNode):
|
|
|
1420
1517
|
|
|
1421
1518
|
Raises:
|
|
1422
1519
|
RuntimeError: If called while workforce is running (not paused).
|
|
1520
|
+
ValueError: If worker has tools and stream mode enabled but
|
|
1521
|
+
use_structured_output_handler is False.
|
|
1423
1522
|
"""
|
|
1424
1523
|
if self._state == WorkforceState.RUNNING:
|
|
1425
1524
|
raise RuntimeError(
|
|
1426
1525
|
"Cannot add workers while workforce is running. "
|
|
1427
1526
|
"Pause the workforce first."
|
|
1428
1527
|
)
|
|
1528
|
+
|
|
1529
|
+
# Validate worker agent compatibility
|
|
1530
|
+
self._validate_agent_compatibility(worker, "Worker agent")
|
|
1531
|
+
|
|
1429
1532
|
# Ensure the worker agent shares this workforce's pause control
|
|
1430
1533
|
self._attach_pause_event_to_agent(worker)
|
|
1431
1534
|
|
|
@@ -2057,7 +2160,6 @@ class Workforce(BaseNode):
|
|
|
2057
2160
|
logger.error(
|
|
2058
2161
|
f"JSON parsing error in worker creation: Invalid "
|
|
2059
2162
|
f"response format - {e}. Response content: "
|
|
2060
|
-
f"format - {e}. Response content: "
|
|
2061
2163
|
f"{response.msg.content[:100]}..."
|
|
2062
2164
|
)
|
|
2063
2165
|
raise RuntimeError(
|
|
@@ -2070,6 +2172,14 @@ class Workforce(BaseNode):
|
|
|
2070
2172
|
new_node_conf.sys_msg,
|
|
2071
2173
|
)
|
|
2072
2174
|
|
|
2175
|
+
# Validate the new agent compatibility before creating worker
|
|
2176
|
+
try:
|
|
2177
|
+
self._validate_agent_compatibility(
|
|
2178
|
+
new_agent, f"Agent for task {task.id}"
|
|
2179
|
+
)
|
|
2180
|
+
except ValueError as e:
|
|
2181
|
+
raise ValueError(f"Cannot create worker for task {task.id}: {e!s}")
|
|
2182
|
+
|
|
2073
2183
|
new_node = SingleAgentWorker(
|
|
2074
2184
|
description=new_node_conf.description,
|
|
2075
2185
|
worker=new_agent,
|
|
@@ -2335,7 +2445,17 @@ class Workforce(BaseNode):
|
|
|
2335
2445
|
|
|
2336
2446
|
elif recovery_decision.strategy == RecoveryStrategy.DECOMPOSE:
|
|
2337
2447
|
# Decompose the task into subtasks
|
|
2338
|
-
|
|
2448
|
+
subtasks_result = self._decompose_task(task)
|
|
2449
|
+
|
|
2450
|
+
# Handle both streaming and non-streaming results
|
|
2451
|
+
if isinstance(subtasks_result, Generator):
|
|
2452
|
+
# This is a generator (streaming mode)
|
|
2453
|
+
subtasks = []
|
|
2454
|
+
for new_tasks in subtasks_result:
|
|
2455
|
+
subtasks.extend(new_tasks)
|
|
2456
|
+
else:
|
|
2457
|
+
# This is a regular list (non-streaming mode)
|
|
2458
|
+
subtasks = subtasks_result
|
|
2339
2459
|
if self.metrics_logger and subtasks:
|
|
2340
2460
|
self.metrics_logger.log_task_decomposed(
|
|
2341
2461
|
parent_task_id=task.id,
|
|
@@ -3189,6 +3309,18 @@ class Workforce(BaseNode):
|
|
|
3189
3309
|
)
|
|
3190
3310
|
|
|
3191
3311
|
agent = ChatAgent(sys_msg, **(agent_kwargs or {}))
|
|
3312
|
+
|
|
3313
|
+
# Validate agent compatibility
|
|
3314
|
+
try:
|
|
3315
|
+
workforce_instance._validate_agent_compatibility(
|
|
3316
|
+
agent, "Worker agent"
|
|
3317
|
+
)
|
|
3318
|
+
except ValueError as e:
|
|
3319
|
+
return {
|
|
3320
|
+
"status": "error",
|
|
3321
|
+
"message": str(e),
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3192
3324
|
workforce_instance.add_single_agent_worker(description, agent)
|
|
3193
3325
|
|
|
3194
3326
|
return {
|
camel/tasks/task.py
CHANGED
|
@@ -19,6 +19,7 @@ from typing import (
|
|
|
19
19
|
Any,
|
|
20
20
|
Callable,
|
|
21
21
|
Dict,
|
|
22
|
+
Generator,
|
|
22
23
|
List,
|
|
23
24
|
Literal,
|
|
24
25
|
Optional,
|
|
@@ -30,6 +31,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
32
33
|
from camel.agents import ChatAgent
|
|
34
|
+
from camel.agents.chat_agent import StreamingChatAgentResponse
|
|
33
35
|
import uuid
|
|
34
36
|
|
|
35
37
|
from camel.logger import get_logger
|
|
@@ -402,9 +404,9 @@ class Task(BaseModel):
|
|
|
402
404
|
agent: "ChatAgent",
|
|
403
405
|
prompt: Optional[str] = None,
|
|
404
406
|
task_parser: Callable[[str, str], List["Task"]] = parse_response,
|
|
405
|
-
) -> List["Task"]:
|
|
406
|
-
r"""Decompose a task to a list of sub-tasks.
|
|
407
|
-
|
|
407
|
+
) -> Union[List["Task"], Generator[List["Task"], None, None]]:
|
|
408
|
+
r"""Decompose a task to a list of sub-tasks. Automatically detects
|
|
409
|
+
streaming or non-streaming based on agent configuration.
|
|
408
410
|
|
|
409
411
|
Args:
|
|
410
412
|
agent (ChatAgent): An agent that used to decompose the task.
|
|
@@ -415,7 +417,10 @@ class Task(BaseModel):
|
|
|
415
417
|
the default parse_response will be used.
|
|
416
418
|
|
|
417
419
|
Returns:
|
|
418
|
-
List[Task]
|
|
420
|
+
Union[List[Task], Generator[List[Task], None, None]]: If agent is
|
|
421
|
+
configured for streaming, returns a generator that yields lists
|
|
422
|
+
of new tasks as they are parsed. Otherwise returns a list of
|
|
423
|
+
all tasks.
|
|
419
424
|
"""
|
|
420
425
|
|
|
421
426
|
role_name = agent.role_name
|
|
@@ -427,6 +432,72 @@ class Task(BaseModel):
|
|
|
427
432
|
role_name=role_name, content=content
|
|
428
433
|
)
|
|
429
434
|
response = agent.step(msg)
|
|
435
|
+
|
|
436
|
+
# Auto-detect streaming based on response type
|
|
437
|
+
from camel.agents.chat_agent import StreamingChatAgentResponse
|
|
438
|
+
|
|
439
|
+
if isinstance(response, StreamingChatAgentResponse):
|
|
440
|
+
return self._decompose_streaming(response, task_parser)
|
|
441
|
+
else:
|
|
442
|
+
return self._decompose_non_streaming(response, task_parser)
|
|
443
|
+
|
|
444
|
+
def _decompose_streaming(
|
|
445
|
+
self,
|
|
446
|
+
response: "StreamingChatAgentResponse",
|
|
447
|
+
task_parser: Callable[[str, str], List["Task"]],
|
|
448
|
+
) -> Generator[List["Task"], None, None]:
|
|
449
|
+
r"""Handle streaming response for task decomposition.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
response: Streaming response from agent
|
|
453
|
+
task_parser: Function to parse tasks from response
|
|
454
|
+
|
|
455
|
+
Yields:
|
|
456
|
+
List[Task]: New tasks as they are parsed from streaming response
|
|
457
|
+
"""
|
|
458
|
+
accumulated_content = ""
|
|
459
|
+
yielded_count = 0
|
|
460
|
+
|
|
461
|
+
# Process streaming response
|
|
462
|
+
for chunk in response:
|
|
463
|
+
accumulated_content = chunk.msg.content
|
|
464
|
+
|
|
465
|
+
# Try to parse partial tasks from accumulated content
|
|
466
|
+
try:
|
|
467
|
+
current_tasks = self._parse_partial_tasks(accumulated_content)
|
|
468
|
+
|
|
469
|
+
# Yield new tasks if we have more than previously yielded
|
|
470
|
+
if len(current_tasks) > yielded_count:
|
|
471
|
+
new_tasks = current_tasks[yielded_count:]
|
|
472
|
+
for task in new_tasks:
|
|
473
|
+
task.additional_info = self.additional_info
|
|
474
|
+
task.parent = self
|
|
475
|
+
yield new_tasks
|
|
476
|
+
yielded_count = len(current_tasks)
|
|
477
|
+
|
|
478
|
+
except Exception:
|
|
479
|
+
# If parsing fails, continue accumulating
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
# Final complete parsing
|
|
483
|
+
final_tasks = task_parser(accumulated_content, self.id)
|
|
484
|
+
for task in final_tasks:
|
|
485
|
+
task.additional_info = self.additional_info
|
|
486
|
+
task.parent = self
|
|
487
|
+
self.subtasks = final_tasks
|
|
488
|
+
|
|
489
|
+
def _decompose_non_streaming(
|
|
490
|
+
self, response, task_parser: Callable[[str, str], List["Task"]]
|
|
491
|
+
) -> List["Task"]:
|
|
492
|
+
r"""Handle non-streaming response for task decomposition.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
response: Regular response from agent
|
|
496
|
+
task_parser: Function to parse tasks from response
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
List[Task]: All parsed tasks
|
|
500
|
+
"""
|
|
430
501
|
tasks = task_parser(response.msg.content, self.id)
|
|
431
502
|
for task in tasks:
|
|
432
503
|
task.additional_info = self.additional_info
|
|
@@ -434,6 +505,35 @@ class Task(BaseModel):
|
|
|
434
505
|
self.subtasks = tasks
|
|
435
506
|
return tasks
|
|
436
507
|
|
|
508
|
+
def _parse_partial_tasks(self, response: str) -> List["Task"]:
|
|
509
|
+
r"""Parse tasks from potentially incomplete response.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
response: Partial response content
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
List[Task]: Tasks parsed from complete <task></task> blocks
|
|
516
|
+
"""
|
|
517
|
+
pattern = r"<task>(.*?)</task>"
|
|
518
|
+
tasks_content = re.findall(pattern, response, re.DOTALL)
|
|
519
|
+
|
|
520
|
+
tasks = []
|
|
521
|
+
task_id = self.id or "0"
|
|
522
|
+
|
|
523
|
+
for i, content in enumerate(tasks_content, 1):
|
|
524
|
+
stripped_content = content.strip()
|
|
525
|
+
if validate_task_content(stripped_content, f"{task_id}.{i}"):
|
|
526
|
+
tasks.append(
|
|
527
|
+
Task(content=stripped_content, id=f"{task_id}.{i}")
|
|
528
|
+
)
|
|
529
|
+
else:
|
|
530
|
+
logger.warning(
|
|
531
|
+
f"Skipping invalid subtask {task_id}.{i} "
|
|
532
|
+
f"during streaming decomposition: "
|
|
533
|
+
f"Content '{stripped_content[:50]}...' failed validation"
|
|
534
|
+
)
|
|
535
|
+
return tasks
|
|
536
|
+
|
|
437
537
|
def compose(
|
|
438
538
|
self,
|
|
439
539
|
agent: "ChatAgent",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
import os
|
|
14
15
|
import re
|
|
15
16
|
from datetime import datetime
|
|
16
17
|
from pathlib import Path
|
|
@@ -37,7 +38,7 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
37
38
|
|
|
38
39
|
def __init__(
|
|
39
40
|
self,
|
|
40
|
-
|
|
41
|
+
working_directory: Optional[str] = None,
|
|
41
42
|
timeout: Optional[float] = None,
|
|
42
43
|
default_encoding: str = "utf-8",
|
|
43
44
|
backup_enabled: bool = True,
|
|
@@ -45,8 +46,11 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
45
46
|
r"""Initialize the FileWriteToolkit.
|
|
46
47
|
|
|
47
48
|
Args:
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
working_directory (str, optional): The default directory for
|
|
50
|
+
output files. If not provided, it will be determined by the
|
|
51
|
+
`CAMEL_WORKDIR` environment variable (if set). If the
|
|
52
|
+
environment variable is not set, it defaults to
|
|
53
|
+
`camel_working_dir`.
|
|
50
54
|
timeout (Optional[float]): The timeout for the toolkit.
|
|
51
55
|
(default: :obj:`None`)
|
|
52
56
|
default_encoding (str): Default character encoding for text
|
|
@@ -55,13 +59,20 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
55
59
|
before overwriting. (default: :obj:`True`)
|
|
56
60
|
"""
|
|
57
61
|
super().__init__(timeout=timeout)
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
if working_directory:
|
|
63
|
+
self.working_directory = Path(working_directory).resolve()
|
|
64
|
+
else:
|
|
65
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
66
|
+
if camel_workdir:
|
|
67
|
+
self.working_directory = Path(camel_workdir).resolve()
|
|
68
|
+
else:
|
|
69
|
+
self.working_directory = Path("./camel_working_dir").resolve()
|
|
70
|
+
self.working_directory.mkdir(parents=True, exist_ok=True)
|
|
60
71
|
self.default_encoding = default_encoding
|
|
61
72
|
self.backup_enabled = backup_enabled
|
|
62
73
|
logger.info(
|
|
63
74
|
f"FileWriteToolkit initialized with output directory"
|
|
64
|
-
f": {self.
|
|
75
|
+
f": {self.working_directory}, encoding: {default_encoding}"
|
|
65
76
|
)
|
|
66
77
|
|
|
67
78
|
def _resolve_filepath(self, file_path: str) -> Path:
|
|
@@ -80,7 +91,7 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
80
91
|
"""
|
|
81
92
|
path_obj = Path(file_path)
|
|
82
93
|
if not path_obj.is_absolute():
|
|
83
|
-
path_obj = self.
|
|
94
|
+
path_obj = self.working_directory / path_obj
|
|
84
95
|
|
|
85
96
|
sanitized_filename = self._sanitize_filename(path_obj.name)
|
|
86
97
|
path_obj = path_obj.parent / sanitized_filename
|
|
@@ -916,7 +927,7 @@ class FileWriteToolkit(BaseToolkit):
|
|
|
916
927
|
- CSV: string or list of lists
|
|
917
928
|
- JSON: string or serializable object
|
|
918
929
|
filename (str): The name or path of the file. If a relative path is
|
|
919
|
-
supplied, it is resolved to self.
|
|
930
|
+
supplied, it is resolved to self.working_directory.
|
|
920
931
|
encoding (Optional[str]): The character encoding to use. (default:
|
|
921
932
|
:obj: `None`)
|
|
922
933
|
use_latex (bool): Whether to use LaTeX for math rendering.
|