camel-ai 0.2.79a0__py3-none-any.whl → 0.2.79a1__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/agents/_utils.py +38 -0
- camel/agents/chat_agent.py +788 -255
- camel/memories/agent_memories.py +34 -0
- camel/memories/base.py +26 -0
- camel/memories/blocks/chat_history_block.py +115 -0
- camel/memories/context_creators/score_based.py +25 -384
- camel/messages/base.py +26 -0
- camel/models/azure_openai_model.py +113 -67
- camel/models/model_factory.py +17 -1
- camel/models/openai_compatible_model.py +62 -32
- camel/models/openai_model.py +61 -35
- camel/models/samba_model.py +34 -15
- camel/models/sglang_model.py +41 -11
- camel/societies/workforce/__init__.py +2 -0
- camel/societies/workforce/role_playing_worker.py +15 -11
- camel/societies/workforce/single_agent_worker.py +86 -364
- camel/societies/workforce/utils.py +2 -1
- camel/societies/workforce/workflow_memory_manager.py +772 -0
- camel/societies/workforce/workforce.py +96 -32
- camel/storages/vectordb_storages/oceanbase.py +5 -4
- camel/toolkits/file_toolkit.py +166 -0
- camel/toolkits/message_integration.py +15 -13
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +112 -79
- camel/types/enums.py +1 -0
- camel/utils/context_utils.py +148 -2
- {camel_ai-0.2.79a0.dist-info → camel_ai-0.2.79a1.dist-info}/METADATA +1 -1
- {camel_ai-0.2.79a0.dist-info → camel_ai-0.2.79a1.dist-info}/RECORD +30 -29
- {camel_ai-0.2.79a0.dist-info → camel_ai-0.2.79a1.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.79a0.dist-info → camel_ai-0.2.79a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,7 +11,6 @@
|
|
|
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 atexit
|
|
15
14
|
import os
|
|
16
15
|
import platform
|
|
17
16
|
import select
|
|
@@ -42,12 +41,10 @@ logger = get_logger(__name__)
|
|
|
42
41
|
try:
|
|
43
42
|
import docker
|
|
44
43
|
from docker.errors import APIError, NotFound
|
|
45
|
-
from docker.models.containers import Container
|
|
46
44
|
except ImportError:
|
|
47
45
|
docker = None
|
|
48
46
|
NotFound = None
|
|
49
47
|
APIError = None
|
|
50
|
-
Container = None
|
|
51
48
|
|
|
52
49
|
|
|
53
50
|
def _to_plain(text: str) -> str:
|
|
@@ -149,8 +146,6 @@ class TerminalToolkit(BaseToolkit):
|
|
|
149
146
|
self.initial_env_path: Optional[str] = None
|
|
150
147
|
self.python_executable = sys.executable
|
|
151
148
|
|
|
152
|
-
atexit.register(self.__del__)
|
|
153
|
-
|
|
154
149
|
self.log_dir = os.path.abspath(
|
|
155
150
|
session_logs_dir or os.path.join(self.working_dir, "terminal_logs")
|
|
156
151
|
)
|
|
@@ -400,9 +395,11 @@ class TerminalToolkit(BaseToolkit):
|
|
|
400
395
|
with self._session_lock:
|
|
401
396
|
if session_id in self.shell_sessions:
|
|
402
397
|
self.shell_sessions[session_id]["error"] = str(e)
|
|
403
|
-
except Exception:
|
|
404
|
-
|
|
405
|
-
|
|
398
|
+
except Exception as cleanup_error:
|
|
399
|
+
logger.warning(
|
|
400
|
+
f"[SESSION {session_id}] Failed to store error state: "
|
|
401
|
+
f"{cleanup_error}"
|
|
402
|
+
)
|
|
406
403
|
finally:
|
|
407
404
|
try:
|
|
408
405
|
with self._session_lock:
|
|
@@ -480,39 +477,40 @@ class TerminalToolkit(BaseToolkit):
|
|
|
480
477
|
|
|
481
478
|
warning_message = (
|
|
482
479
|
"\n--- WARNING: Process is still actively outputting "
|
|
483
|
-
"after max wait time. Consider
|
|
484
|
-
"
|
|
480
|
+
"after max wait time. Consider waiting before "
|
|
481
|
+
"sending the next command. ---"
|
|
485
482
|
)
|
|
486
483
|
return "".join(output_parts) + warning_message
|
|
487
484
|
|
|
488
485
|
def shell_exec(self, id: str, command: str, block: bool = True) -> str:
|
|
489
|
-
r"""
|
|
490
|
-
blocking mode (waits for completion) or non-blocking mode
|
|
491
|
-
(runs in the background). A unique session ID is created for
|
|
492
|
-
each session.
|
|
486
|
+
r"""Executes a shell command in blocking or non-blocking mode.
|
|
493
487
|
|
|
494
488
|
Args:
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
489
|
+
id (str): A unique identifier for the command's session. This ID is
|
|
490
|
+
used to interact with non-blocking processes.
|
|
491
|
+
command (str): The shell command to execute.
|
|
492
|
+
block (bool, optional): Determines the execution mode. Defaults to
|
|
493
|
+
True. If `True` (blocking mode), the function waits for the
|
|
494
|
+
command to complete and returns the full output. Use this for
|
|
495
|
+
most commands . If `False` (non-blocking mode), the function
|
|
496
|
+
starts the command in the background. Use this only for
|
|
497
|
+
interactive sessions or long-running tasks, or servers.
|
|
502
498
|
|
|
503
499
|
Returns:
|
|
504
|
-
str:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
500
|
+
str: The output of the command execution, which varies by mode.
|
|
501
|
+
In blocking mode, returns the complete standard output and
|
|
502
|
+
standard error from the command.
|
|
503
|
+
In non-blocking mode, returns a confirmation message with the
|
|
504
|
+
session `id`. To interact with the background process, use
|
|
505
|
+
other functions: `shell_view(id)` to see output,
|
|
506
|
+
`shell_write_to_process(id, "input")` to send input, and
|
|
507
|
+
`shell_kill_process(id)` to terminate.
|
|
508
508
|
"""
|
|
509
509
|
if self.safe_mode:
|
|
510
510
|
is_safe, message = self._sanitize_command(command)
|
|
511
511
|
if not is_safe:
|
|
512
512
|
return f"Error: {message}"
|
|
513
513
|
command = message
|
|
514
|
-
else:
|
|
515
|
-
command = command
|
|
516
514
|
|
|
517
515
|
if self.use_docker_backend:
|
|
518
516
|
# For Docker, we always run commands in a shell
|
|
@@ -570,7 +568,10 @@ class TerminalToolkit(BaseToolkit):
|
|
|
570
568
|
log_entry += f"--- Error ---\n{error_msg}\n"
|
|
571
569
|
return error_msg
|
|
572
570
|
except Exception as e:
|
|
573
|
-
if
|
|
571
|
+
if (
|
|
572
|
+
isinstance(e, (subprocess.TimeoutExpired, TimeoutError))
|
|
573
|
+
or "timed out" in str(e).lower()
|
|
574
|
+
):
|
|
574
575
|
error_msg = (
|
|
575
576
|
f"Error: Command timed out after "
|
|
576
577
|
f"{self.timeout} seconds."
|
|
@@ -593,6 +594,14 @@ class TerminalToolkit(BaseToolkit):
|
|
|
593
594
|
f"> {command}\n",
|
|
594
595
|
)
|
|
595
596
|
|
|
597
|
+
# PYTHONUNBUFFERED=1 for real-time output
|
|
598
|
+
# Without this, Python subprocesses buffer output (4KB buffer)
|
|
599
|
+
# and shell_view() won't see output until buffer fills or process
|
|
600
|
+
# exits
|
|
601
|
+
env_vars = os.environ.copy()
|
|
602
|
+
env_vars["PYTHONUNBUFFERED"] = "1"
|
|
603
|
+
docker_env = {"PYTHONUNBUFFERED": "1"}
|
|
604
|
+
|
|
596
605
|
with self._session_lock:
|
|
597
606
|
self.shell_sessions[session_id] = {
|
|
598
607
|
"id": session_id,
|
|
@@ -606,6 +615,8 @@ class TerminalToolkit(BaseToolkit):
|
|
|
606
615
|
else "local",
|
|
607
616
|
}
|
|
608
617
|
|
|
618
|
+
process = None
|
|
619
|
+
exec_socket = None
|
|
609
620
|
try:
|
|
610
621
|
if not self.use_docker_backend:
|
|
611
622
|
process = subprocess.Popen(
|
|
@@ -617,6 +628,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
617
628
|
text=True,
|
|
618
629
|
cwd=self.working_dir,
|
|
619
630
|
encoding="utf-8",
|
|
631
|
+
env=env_vars,
|
|
620
632
|
)
|
|
621
633
|
with self._session_lock:
|
|
622
634
|
self.shell_sessions[session_id]["process"] = process
|
|
@@ -630,6 +642,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
630
642
|
stdin=True,
|
|
631
643
|
tty=True,
|
|
632
644
|
workdir=self.docker_workdir,
|
|
645
|
+
environment=docker_env,
|
|
633
646
|
)
|
|
634
647
|
exec_id = exec_instance['Id']
|
|
635
648
|
exec_socket = self.docker_api_client.exec_start(
|
|
@@ -643,15 +656,29 @@ class TerminalToolkit(BaseToolkit):
|
|
|
643
656
|
|
|
644
657
|
self._start_output_reader_thread(session_id)
|
|
645
658
|
|
|
646
|
-
#
|
|
647
|
-
initial_output = self._collect_output_until_idle(session_id)
|
|
648
|
-
|
|
659
|
+
# Return immediately with session ID and instructions
|
|
649
660
|
return (
|
|
650
|
-
f"Session
|
|
651
|
-
f"
|
|
661
|
+
f"Session '{session_id}' started.\n\n"
|
|
662
|
+
f"You could use:\n"
|
|
663
|
+
f" - shell_view('{session_id}') - get output\n"
|
|
664
|
+
f" - shell_write_to_process('{session_id}', '<input>')"
|
|
665
|
+
f" - send input\n"
|
|
666
|
+
f" - shell_kill_process('{session_id}') - terminate"
|
|
652
667
|
)
|
|
653
668
|
|
|
654
669
|
except Exception as e:
|
|
670
|
+
# Clean up resources on failure
|
|
671
|
+
if process is not None:
|
|
672
|
+
try:
|
|
673
|
+
process.terminate()
|
|
674
|
+
except Exception:
|
|
675
|
+
pass
|
|
676
|
+
if exec_socket is not None:
|
|
677
|
+
try:
|
|
678
|
+
exec_socket.close()
|
|
679
|
+
except Exception:
|
|
680
|
+
pass
|
|
681
|
+
|
|
655
682
|
with self._session_lock:
|
|
656
683
|
if session_id in self.shell_sessions:
|
|
657
684
|
self.shell_sessions[session_id]["running"] = False
|
|
@@ -714,18 +741,16 @@ class TerminalToolkit(BaseToolkit):
|
|
|
714
741
|
return f"Error writing to session '{id}': {e}"
|
|
715
742
|
|
|
716
743
|
def shell_view(self, id: str) -> str:
|
|
717
|
-
r"""
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
new output.
|
|
744
|
+
r"""Retrieves new output from a non-blocking session.
|
|
745
|
+
|
|
746
|
+
This function returns only NEW output since the last call. It does NOT
|
|
747
|
+
wait or block - it returns immediately with whatever is available.
|
|
722
748
|
|
|
723
749
|
Args:
|
|
724
750
|
id (str): The unique session ID of the non-blocking process.
|
|
725
751
|
|
|
726
752
|
Returns:
|
|
727
|
-
str:
|
|
728
|
-
an empty string if there is no new output.
|
|
753
|
+
str: New output if available, or a status message.
|
|
729
754
|
"""
|
|
730
755
|
with self._session_lock:
|
|
731
756
|
if id not in self.shell_sessions:
|
|
@@ -734,7 +759,6 @@ class TerminalToolkit(BaseToolkit):
|
|
|
734
759
|
is_running = session["running"]
|
|
735
760
|
|
|
736
761
|
# If session is terminated, drain the queue and return
|
|
737
|
-
# with a status message.
|
|
738
762
|
if not is_running:
|
|
739
763
|
final_output = []
|
|
740
764
|
try:
|
|
@@ -742,9 +766,13 @@ class TerminalToolkit(BaseToolkit):
|
|
|
742
766
|
final_output.append(session["output_stream"].get_nowait())
|
|
743
767
|
except Empty:
|
|
744
768
|
pass
|
|
745
|
-
return "".join(final_output) + "\n--- SESSION TERMINATED ---"
|
|
746
769
|
|
|
747
|
-
|
|
770
|
+
if final_output:
|
|
771
|
+
return "".join(final_output) + "\n\n--- SESSION TERMINATED ---"
|
|
772
|
+
else:
|
|
773
|
+
return "--- SESSION TERMINATED (no new output) ---"
|
|
774
|
+
|
|
775
|
+
# For running session, check for new output
|
|
748
776
|
output = []
|
|
749
777
|
try:
|
|
750
778
|
while True:
|
|
@@ -752,38 +780,18 @@ class TerminalToolkit(BaseToolkit):
|
|
|
752
780
|
except Empty:
|
|
753
781
|
pass
|
|
754
782
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
"""
|
|
768
|
-
with self._session_lock:
|
|
769
|
-
if id not in self.shell_sessions:
|
|
770
|
-
return f"Error: No session found with ID '{id}'."
|
|
771
|
-
session = self.shell_sessions[id]
|
|
772
|
-
if not session["running"]:
|
|
773
|
-
return (
|
|
774
|
-
"Session is no longer running. "
|
|
775
|
-
"Use shell_view to get final output."
|
|
776
|
-
)
|
|
777
|
-
|
|
778
|
-
output_collected = []
|
|
779
|
-
end_time = time.time() + wait_seconds
|
|
780
|
-
while time.time() < end_time and session["running"]:
|
|
781
|
-
new_output = self.shell_view(id)
|
|
782
|
-
if new_output:
|
|
783
|
-
output_collected.append(new_output)
|
|
784
|
-
time.sleep(0.2)
|
|
785
|
-
|
|
786
|
-
return "".join(output_collected)
|
|
783
|
+
if output:
|
|
784
|
+
return "".join(output)
|
|
785
|
+
else:
|
|
786
|
+
# No new output - guide the agent
|
|
787
|
+
return (
|
|
788
|
+
"[No new output]\n"
|
|
789
|
+
"Session is running but idle. Actions could take:\n"
|
|
790
|
+
" - For interactive sessions: Send input "
|
|
791
|
+
"with shell_write_to_process()\n"
|
|
792
|
+
" - For long tasks: Check again later (don't poll "
|
|
793
|
+
"too frequently)"
|
|
794
|
+
)
|
|
787
795
|
|
|
788
796
|
def shell_kill_process(self, id: str) -> str:
|
|
789
797
|
r"""This function forcibly terminates a running non-blocking process.
|
|
@@ -894,8 +902,17 @@ class TerminalToolkit(BaseToolkit):
|
|
|
894
902
|
except EOFError:
|
|
895
903
|
return f"User input interrupted for session '{id}'."
|
|
896
904
|
|
|
897
|
-
def
|
|
898
|
-
|
|
905
|
+
def __enter__(self):
|
|
906
|
+
r"""Context manager entry."""
|
|
907
|
+
return self
|
|
908
|
+
|
|
909
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
910
|
+
r"""Context manager exit - clean up all sessions."""
|
|
911
|
+
self.cleanup()
|
|
912
|
+
return False
|
|
913
|
+
|
|
914
|
+
def cleanup(self):
|
|
915
|
+
r"""Clean up all active sessions."""
|
|
899
916
|
with self._session_lock:
|
|
900
917
|
session_ids = list(self.shell_sessions.keys())
|
|
901
918
|
for session_id in session_ids:
|
|
@@ -904,7 +921,24 @@ class TerminalToolkit(BaseToolkit):
|
|
|
904
921
|
"running", False
|
|
905
922
|
)
|
|
906
923
|
if is_running:
|
|
907
|
-
|
|
924
|
+
try:
|
|
925
|
+
self.shell_kill_process(session_id)
|
|
926
|
+
except Exception as e:
|
|
927
|
+
logger.warning(
|
|
928
|
+
f"Failed to kill session '{session_id}' "
|
|
929
|
+
f"during cleanup: {e}"
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
cleanup._manual_timeout = True # type: ignore[attr-defined]
|
|
933
|
+
|
|
934
|
+
def __del__(self):
|
|
935
|
+
r"""Fallback cleanup in destructor."""
|
|
936
|
+
try:
|
|
937
|
+
self.cleanup()
|
|
938
|
+
except Exception:
|
|
939
|
+
pass
|
|
940
|
+
|
|
941
|
+
__del__._manual_timeout = True # type: ignore[attr-defined]
|
|
908
942
|
|
|
909
943
|
def get_tools(self) -> List[FunctionTool]:
|
|
910
944
|
r"""Returns a list of FunctionTool objects representing the functions
|
|
@@ -917,7 +951,6 @@ class TerminalToolkit(BaseToolkit):
|
|
|
917
951
|
return [
|
|
918
952
|
FunctionTool(self.shell_exec),
|
|
919
953
|
FunctionTool(self.shell_view),
|
|
920
|
-
FunctionTool(self.shell_wait),
|
|
921
954
|
FunctionTool(self.shell_write_to_process),
|
|
922
955
|
FunctionTool(self.shell_kill_process),
|
|
923
956
|
FunctionTool(self.shell_ask_user_for_help),
|
camel/types/enums.py
CHANGED
camel/utils/context_utils.py
CHANGED
|
@@ -91,6 +91,17 @@ class WorkflowSummary(BaseModel):
|
|
|
91
91
|
"mid-task by using the HumanToolkit.",
|
|
92
92
|
default="",
|
|
93
93
|
)
|
|
94
|
+
tags: List[str] = Field(
|
|
95
|
+
description="3-10 categorization tags that describe the workflow "
|
|
96
|
+
"type, domain, and key capabilities. Use lowercase with hyphens. "
|
|
97
|
+
"Tags should be broad, reusable categories to help with semantic "
|
|
98
|
+
"matching to similar tasks. "
|
|
99
|
+
"Examples: 'data-analysis', 'web-scraping', 'api-integration', "
|
|
100
|
+
"'code-generation', 'file-processing', 'database-query', "
|
|
101
|
+
"'text-processing', 'image-manipulation', 'email-automation', "
|
|
102
|
+
"'report-generation'.",
|
|
103
|
+
default_factory=list,
|
|
104
|
+
)
|
|
94
105
|
|
|
95
106
|
@classmethod
|
|
96
107
|
def get_instruction_prompt(cls) -> str:
|
|
@@ -112,7 +123,12 @@ class WorkflowSummary(BaseModel):
|
|
|
112
123
|
'about a simple math problem, the workflow must be short, '
|
|
113
124
|
'e.g. <60 words. By contrast, if the task is complex and '
|
|
114
125
|
'multi-step, such as finding particular job applications based '
|
|
115
|
-
'on user CV, the workflow must be longer, e.g. about 120 words.'
|
|
126
|
+
'on user CV, the workflow must be longer, e.g. about 120 words. '
|
|
127
|
+
'For tags, provide 3-5 broad categorization tags using lowercase '
|
|
128
|
+
'with hyphens (e.g., "data-analysis", "web-scraping") that '
|
|
129
|
+
'describe the workflow domain, type, and key capabilities to '
|
|
130
|
+
'help future agents discover this workflow when working on '
|
|
131
|
+
'similar tasks.'
|
|
116
132
|
)
|
|
117
133
|
|
|
118
134
|
|
|
@@ -533,7 +549,6 @@ class ContextUtility:
|
|
|
533
549
|
'session_id': self.session_id,
|
|
534
550
|
'working_directory': str(self.working_directory),
|
|
535
551
|
'created_at': datetime.now().isoformat(),
|
|
536
|
-
'base_directory': str(self.working_directory.parent),
|
|
537
552
|
}
|
|
538
553
|
|
|
539
554
|
def list_sessions(self, base_dir: Optional[str] = None) -> List[str]:
|
|
@@ -794,6 +809,137 @@ class ContextUtility:
|
|
|
794
809
|
result = '\n'.join(filtered_lines).strip()
|
|
795
810
|
return result
|
|
796
811
|
|
|
812
|
+
# ========= WORKFLOW INFO METHODS =========
|
|
813
|
+
|
|
814
|
+
def extract_workflow_info(self, file_path: str) -> Dict[str, Any]:
|
|
815
|
+
r"""Extract info from a workflow markdown file.
|
|
816
|
+
|
|
817
|
+
This method reads only the essential info from a workflow file
|
|
818
|
+
(title, description, tags) for use in workflow selection without
|
|
819
|
+
loading the entire workflow content.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
file_path (str): Full path to the workflow markdown file.
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
Dict[str, Any]: Workflow info including title, description,
|
|
826
|
+
tags, and file_path. Returns empty dict on error.
|
|
827
|
+
"""
|
|
828
|
+
import re
|
|
829
|
+
|
|
830
|
+
try:
|
|
831
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
832
|
+
content = f.read()
|
|
833
|
+
|
|
834
|
+
metadata: Dict[str, Any] = {'file_path': file_path}
|
|
835
|
+
|
|
836
|
+
# extract task title
|
|
837
|
+
title_match = re.search(
|
|
838
|
+
r'### Task Title\s*\n(.+?)(?:\n###|\n\n|$)', content, re.DOTALL
|
|
839
|
+
)
|
|
840
|
+
if title_match:
|
|
841
|
+
metadata['title'] = title_match.group(1).strip()
|
|
842
|
+
else:
|
|
843
|
+
metadata['title'] = ""
|
|
844
|
+
|
|
845
|
+
# extract task description
|
|
846
|
+
desc_match = re.search(
|
|
847
|
+
r'### Task Description\s*\n(.+?)(?:\n###|\n\n|$)',
|
|
848
|
+
content,
|
|
849
|
+
re.DOTALL,
|
|
850
|
+
)
|
|
851
|
+
if desc_match:
|
|
852
|
+
metadata['description'] = desc_match.group(1).strip()
|
|
853
|
+
else:
|
|
854
|
+
metadata['description'] = ""
|
|
855
|
+
|
|
856
|
+
# extract tags
|
|
857
|
+
tags_match = re.search(
|
|
858
|
+
r'### Tags\s*\n(.+?)(?:\n###|\n\n|$)', content, re.DOTALL
|
|
859
|
+
)
|
|
860
|
+
if tags_match:
|
|
861
|
+
tags_section = tags_match.group(1).strip()
|
|
862
|
+
# Parse bullet list of tags
|
|
863
|
+
tags = [
|
|
864
|
+
line.strip().lstrip('- ')
|
|
865
|
+
for line in tags_section.split('\n')
|
|
866
|
+
if line.strip().startswith('-')
|
|
867
|
+
]
|
|
868
|
+
metadata['tags'] = tags
|
|
869
|
+
else:
|
|
870
|
+
metadata['tags'] = []
|
|
871
|
+
|
|
872
|
+
return metadata
|
|
873
|
+
|
|
874
|
+
except Exception as e:
|
|
875
|
+
logger.warning(
|
|
876
|
+
f"Error extracting workflow info from {file_path}: {e}"
|
|
877
|
+
)
|
|
878
|
+
return {}
|
|
879
|
+
|
|
880
|
+
def get_all_workflows_info(
|
|
881
|
+
self, session_id: Optional[str] = None
|
|
882
|
+
) -> List[Dict[str, Any]]:
|
|
883
|
+
r"""Get info from all workflow files in workforce_workflows.
|
|
884
|
+
|
|
885
|
+
This method scans the workforce_workflows directory for workflow
|
|
886
|
+
markdown files and extracts their info for use in workflow
|
|
887
|
+
selection.
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
session_id (Optional[str]): If provided, only return workflows
|
|
891
|
+
from this specific session. If None, returns workflows from
|
|
892
|
+
all sessions.
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
List[Dict[str, Any]]: List of workflow info dicts, sorted
|
|
896
|
+
by session timestamp (newest first).
|
|
897
|
+
"""
|
|
898
|
+
import glob
|
|
899
|
+
import re
|
|
900
|
+
|
|
901
|
+
workflows_metadata = []
|
|
902
|
+
|
|
903
|
+
# Determine base directory for workforce workflows
|
|
904
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
905
|
+
if camel_workdir:
|
|
906
|
+
base_dir = os.path.join(camel_workdir, "workforce_workflows")
|
|
907
|
+
else:
|
|
908
|
+
base_dir = "workforce_workflows"
|
|
909
|
+
|
|
910
|
+
# Build search pattern
|
|
911
|
+
if session_id:
|
|
912
|
+
search_pattern = os.path.join(
|
|
913
|
+
base_dir, session_id, "*_workflow.md"
|
|
914
|
+
)
|
|
915
|
+
else:
|
|
916
|
+
search_pattern = os.path.join(base_dir, "*", "*_workflow.md")
|
|
917
|
+
|
|
918
|
+
# Find all workflow files
|
|
919
|
+
workflow_files = glob.glob(search_pattern)
|
|
920
|
+
|
|
921
|
+
if not workflow_files:
|
|
922
|
+
logger.info(f"No workflow files found in {base_dir}")
|
|
923
|
+
return []
|
|
924
|
+
|
|
925
|
+
# Sort by session timestamp (newest first)
|
|
926
|
+
def extract_session_timestamp(filepath: str) -> str:
|
|
927
|
+
match = re.search(r'session_(\d{8}_\d{6}_\d{6})', filepath)
|
|
928
|
+
return match.group(1) if match else ""
|
|
929
|
+
|
|
930
|
+
workflow_files.sort(key=extract_session_timestamp, reverse=True)
|
|
931
|
+
|
|
932
|
+
# Extract info from each file
|
|
933
|
+
for file_path in workflow_files:
|
|
934
|
+
metadata = self.extract_workflow_info(file_path)
|
|
935
|
+
if metadata: # Only add if extraction succeeded
|
|
936
|
+
workflows_metadata.append(metadata)
|
|
937
|
+
|
|
938
|
+
logger.info(
|
|
939
|
+
f"Found {len(workflows_metadata)} workflow file(s) with info"
|
|
940
|
+
)
|
|
941
|
+
return workflows_metadata
|
|
942
|
+
|
|
797
943
|
# ========= SHARED SESSION MANAGEMENT METHODS =========
|
|
798
944
|
|
|
799
945
|
@classmethod
|