camel-ai 0.2.73a4__py3-none-any.whl → 0.2.80a2__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.
- camel/__init__.py +1 -1
- camel/agents/_utils.py +38 -0
- camel/agents/chat_agent.py +2217 -519
- camel/agents/mcp_agent.py +30 -27
- camel/configs/__init__.py +15 -0
- camel/configs/aihubmix_config.py +88 -0
- camel/configs/amd_config.py +70 -0
- camel/configs/cometapi_config.py +104 -0
- camel/configs/minimax_config.py +93 -0
- camel/configs/nebius_config.py +103 -0
- camel/data_collectors/alpaca_collector.py +15 -6
- camel/datasets/base_generator.py +39 -10
- camel/environments/single_step.py +28 -3
- camel/environments/tic_tac_toe.py +1 -1
- camel/interpreters/__init__.py +2 -0
- camel/interpreters/docker/Dockerfile +3 -12
- camel/interpreters/e2b_interpreter.py +34 -1
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/loaders/__init__.py +11 -2
- camel/loaders/chunkr_reader.py +9 -0
- camel/memories/agent_memories.py +48 -4
- camel/memories/base.py +26 -0
- camel/memories/blocks/chat_history_block.py +122 -4
- camel/memories/context_creators/score_based.py +25 -384
- camel/memories/records.py +88 -8
- camel/messages/base.py +153 -34
- camel/models/__init__.py +10 -0
- camel/models/aihubmix_model.py +83 -0
- camel/models/aiml_model.py +1 -16
- camel/models/amd_model.py +101 -0
- camel/models/anthropic_model.py +6 -19
- camel/models/aws_bedrock_model.py +2 -33
- camel/models/azure_openai_model.py +114 -89
- camel/models/base_audio_model.py +3 -1
- camel/models/base_model.py +32 -14
- camel/models/cohere_model.py +1 -16
- camel/models/cometapi_model.py +83 -0
- camel/models/crynux_model.py +1 -16
- camel/models/deepseek_model.py +1 -16
- camel/models/fish_audio_model.py +6 -0
- camel/models/gemini_model.py +36 -18
- camel/models/groq_model.py +1 -17
- camel/models/internlm_model.py +1 -16
- camel/models/litellm_model.py +1 -16
- camel/models/lmstudio_model.py +1 -17
- camel/models/minimax_model.py +83 -0
- camel/models/mistral_model.py +1 -16
- camel/models/model_factory.py +27 -1
- camel/models/modelscope_model.py +1 -16
- camel/models/moonshot_model.py +105 -24
- camel/models/nebius_model.py +83 -0
- camel/models/nemotron_model.py +0 -5
- camel/models/netmind_model.py +1 -16
- camel/models/novita_model.py +1 -16
- camel/models/nvidia_model.py +1 -16
- camel/models/ollama_model.py +4 -19
- camel/models/openai_compatible_model.py +62 -41
- camel/models/openai_model.py +62 -57
- camel/models/openrouter_model.py +1 -17
- camel/models/ppio_model.py +1 -16
- camel/models/qianfan_model.py +1 -16
- camel/models/qwen_model.py +1 -16
- camel/models/reka_model.py +1 -16
- camel/models/samba_model.py +34 -47
- camel/models/sglang_model.py +64 -31
- camel/models/siliconflow_model.py +1 -16
- camel/models/stub_model.py +0 -4
- camel/models/togetherai_model.py +1 -16
- camel/models/vllm_model.py +1 -16
- camel/models/volcano_model.py +0 -17
- camel/models/watsonx_model.py +1 -16
- camel/models/yi_model.py +1 -16
- camel/models/zhipuai_model.py +60 -16
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/retrievers/auto_retriever.py +1 -0
- camel/runtimes/daytona_runtime.py +11 -12
- camel/societies/__init__.py +2 -0
- camel/societies/workforce/__init__.py +2 -0
- camel/societies/workforce/events.py +122 -0
- camel/societies/workforce/prompts.py +146 -66
- camel/societies/workforce/role_playing_worker.py +15 -11
- camel/societies/workforce/single_agent_worker.py +302 -65
- camel/societies/workforce/structured_output_handler.py +30 -18
- camel/societies/workforce/task_channel.py +163 -27
- camel/societies/workforce/utils.py +107 -13
- camel/societies/workforce/workflow_memory_manager.py +772 -0
- camel/societies/workforce/workforce.py +1949 -579
- camel/societies/workforce/workforce_callback.py +74 -0
- camel/societies/workforce/workforce_logger.py +168 -145
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/storages/key_value_storages/json.py +15 -2
- camel/storages/key_value_storages/mem0_cloud.py +48 -47
- camel/storages/object_storages/google_cloud.py +1 -1
- camel/storages/vectordb_storages/oceanbase.py +13 -13
- camel/storages/vectordb_storages/qdrant.py +3 -3
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/tasks/task.py +4 -3
- camel/toolkits/__init__.py +20 -7
- camel/toolkits/aci_toolkit.py +45 -0
- camel/toolkits/base.py +6 -4
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/dappier_toolkit.py +5 -1
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
- camel/toolkits/excel_toolkit.py +1 -1
- camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +430 -36
- camel/toolkits/function_tool.py +13 -3
- camel/toolkits/github_toolkit.py +104 -17
- camel/toolkits/gmail_toolkit.py +1839 -0
- camel/toolkits/google_calendar_toolkit.py +38 -4
- camel/toolkits/google_drive_mcp_toolkit.py +12 -31
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +15 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +77 -8
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +884 -88
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +959 -89
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +9 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +281 -213
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +23 -3
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +72 -7
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +582 -132
- camel/toolkits/hybrid_browser_toolkit_py/actions.py +158 -0
- camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +55 -8
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +43 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +321 -8
- camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +10 -4
- camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +45 -4
- camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +151 -53
- camel/toolkits/klavis_toolkit.py +5 -1
- camel/toolkits/markitdown_toolkit.py +27 -1
- camel/toolkits/math_toolkit.py +64 -10
- camel/toolkits/mcp_toolkit.py +366 -71
- camel/toolkits/memory_toolkit.py +5 -1
- camel/toolkits/message_integration.py +18 -13
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/note_taking_toolkit.py +19 -10
- camel/toolkits/notion_mcp_toolkit.py +16 -26
- camel/toolkits/openbb_toolkit.py +5 -1
- camel/toolkits/origene_mcp_toolkit.py +8 -49
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/search_toolkit.py +264 -91
- camel/toolkits/slack_toolkit.py +64 -10
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +957 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +17 -11
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/toolkits/zapier_toolkit.py +5 -1
- camel/types/__init__.py +2 -2
- camel/types/enums.py +274 -7
- camel/types/openai_types.py +2 -2
- camel/types/unified_model_type.py +15 -0
- camel/utils/commons.py +36 -5
- camel/utils/constants.py +3 -0
- camel/utils/context_utils.py +1003 -0
- camel/utils/mcp.py +138 -4
- camel/utils/token_counting.py +43 -20
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/METADATA +223 -83
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/RECORD +170 -141
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/openai_agent_toolkit.py +0 -135
- camel/toolkits/terminal_toolkit.py +0 -1550
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.73a4.dist-info → camel_ai-0.2.80a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
from camel.logger import get_logger
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from camel.agents import ChatAgent
|
|
27
|
+
from camel.memories.records import MemoryRecord
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WorkflowSummary(BaseModel):
|
|
33
|
+
r"""Pydantic model for structured workflow summaries.
|
|
34
|
+
|
|
35
|
+
This model defines the schema for workflow memories that can be reused
|
|
36
|
+
by future agents for similar tasks.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
task_title: str = Field(
|
|
40
|
+
description="A short, generic title of the main task (≤ 10 words). "
|
|
41
|
+
"Avoid product- or case-specific names. "
|
|
42
|
+
"Example: 'List GitHub stargazers', "
|
|
43
|
+
"'Remind weekly meetings on Slack', "
|
|
44
|
+
"'Find best leads and turn them into a table on Notion'."
|
|
45
|
+
)
|
|
46
|
+
task_description: str = Field(
|
|
47
|
+
description="One-paragraph summary of what the user asked for "
|
|
48
|
+
"(≤ 80 words). "
|
|
49
|
+
"No implementation details; just the outcome the user wants. "
|
|
50
|
+
"Example: Find academic professors who might be interested in the "
|
|
51
|
+
"upcoming research paper on Graph-based Agentic Memory, extract "
|
|
52
|
+
"their email addresses, affiliations, and research interests, "
|
|
53
|
+
"and create a table on Notion with this information."
|
|
54
|
+
)
|
|
55
|
+
tools: List[str] = Field(
|
|
56
|
+
description="Bullet list of tool calls or functions calls used. "
|
|
57
|
+
"For each: name → what it did → why it was useful (one line each). "
|
|
58
|
+
"This field is explicitly for tool call messages or the MCP "
|
|
59
|
+
"servers used."
|
|
60
|
+
"Example: - ArxivToolkit: get authors from a paper title, "
|
|
61
|
+
"it helped find academic professors who authored a particular "
|
|
62
|
+
"paper, and then get their email addresses, affiliations, and "
|
|
63
|
+
"research interests.",
|
|
64
|
+
default_factory=list,
|
|
65
|
+
)
|
|
66
|
+
steps: List[str] = Field(
|
|
67
|
+
description="Numbered, ordered actions the agent took to complete "
|
|
68
|
+
"the task. Each step starts with a verb and is generic "
|
|
69
|
+
"enough to be repeatable. "
|
|
70
|
+
"Example: 1. Find the upcoming meetings on Google Calendar "
|
|
71
|
+
" today. 2. Send participants a reminder on Slack...",
|
|
72
|
+
default_factory=list,
|
|
73
|
+
)
|
|
74
|
+
failure_and_recovery_strategies: List[str] = Field(
|
|
75
|
+
description="[Optional] Bullet each incident with symptom, "
|
|
76
|
+
" cause (if known), fix/workaround, verification of "
|
|
77
|
+
"recovery. Leave empty if no failures. "
|
|
78
|
+
"failures. Example: Running the script for consumer data "
|
|
79
|
+
"analysis failed since Pandas package was not installed. "
|
|
80
|
+
"Fixed by running 'pip install pandas'.",
|
|
81
|
+
default_factory=list,
|
|
82
|
+
)
|
|
83
|
+
notes_and_observations: str = Field(
|
|
84
|
+
description="[Optional] Anything not covered in previous fields "
|
|
85
|
+
"that is critical to know for future executions of the task. "
|
|
86
|
+
"Leave empty if no notes. Do not repeat any information, or "
|
|
87
|
+
"mention trivial details. Only what is essential. "
|
|
88
|
+
"Example: The user likes to be in the "
|
|
89
|
+
"loop of the task execution, make sure to check with them the "
|
|
90
|
+
"plan before starting to work, and ask them for approval "
|
|
91
|
+
"mid-task by using the HumanToolkit.",
|
|
92
|
+
default="",
|
|
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
|
+
)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def get_instruction_prompt(cls) -> str:
|
|
108
|
+
r"""Get the instruction prompt for this model.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
str: The instruction prompt that guides agents to produce
|
|
112
|
+
structured output matching this schema.
|
|
113
|
+
"""
|
|
114
|
+
return (
|
|
115
|
+
'You are writing a compact "workflow memory" so future agents '
|
|
116
|
+
'can reuse what you just did for future tasks. '
|
|
117
|
+
'Be concise, precise, and action-oriented. Analyze the '
|
|
118
|
+
'conversation and extract the key workflow information '
|
|
119
|
+
'following the provided schema structure. If a field has no '
|
|
120
|
+
'content, still include it per the schema, but keep it empty. '
|
|
121
|
+
'The length of your workflow must be proportional to the '
|
|
122
|
+
'complexity of the task. Example: If the task is simply '
|
|
123
|
+
'about a simple math problem, the workflow must be short, '
|
|
124
|
+
'e.g. <60 words. By contrast, if the task is complex and '
|
|
125
|
+
'multi-step, such as finding particular job applications based '
|
|
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.'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ContextUtility:
|
|
136
|
+
r"""Utility class for context management and file operations.
|
|
137
|
+
|
|
138
|
+
This utility provides generic functionality for managing context files,
|
|
139
|
+
markdown generation, and session management that can be used by
|
|
140
|
+
context-related features.
|
|
141
|
+
|
|
142
|
+
Key features:
|
|
143
|
+
- Session-based directory management
|
|
144
|
+
- Generic markdown file operations
|
|
145
|
+
- Text-based search through files
|
|
146
|
+
- File metadata handling
|
|
147
|
+
- Agent memory record retrieval
|
|
148
|
+
- Shared session management for workforce workflows
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# maximum filename length for workflow files (chosen for filesystem
|
|
152
|
+
# compatibility and readability)
|
|
153
|
+
MAX_WORKFLOW_FILENAME_LENGTH: ClassVar[int] = 50
|
|
154
|
+
|
|
155
|
+
# Class variables for shared session management
|
|
156
|
+
_shared_sessions: ClassVar[Dict[str, 'ContextUtility']] = {}
|
|
157
|
+
_default_workforce_session: ClassVar[Optional['ContextUtility']] = None
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
working_directory: Optional[str] = None,
|
|
162
|
+
session_id: Optional[str] = None,
|
|
163
|
+
create_folder: bool = True,
|
|
164
|
+
):
|
|
165
|
+
r"""Initialize the ContextUtility.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
working_directory (str, optional): The directory path where files
|
|
169
|
+
will be stored. If not provided, a default directory will be
|
|
170
|
+
used.
|
|
171
|
+
session_id (str, optional): The session ID to use. If provided,
|
|
172
|
+
this instance will use the same session folder as other
|
|
173
|
+
instances with the same session_id. If not provided, a new
|
|
174
|
+
session ID will be generated.
|
|
175
|
+
create_folder (bool): Whether to create the session folder
|
|
176
|
+
immediately. If False, the folder will be created only when
|
|
177
|
+
needed (e.g., when saving files). Default is True for
|
|
178
|
+
backward compatibility.
|
|
179
|
+
"""
|
|
180
|
+
self.working_directory_param = working_directory
|
|
181
|
+
self._setup_storage(working_directory, session_id, create_folder)
|
|
182
|
+
|
|
183
|
+
def _setup_storage(
|
|
184
|
+
self,
|
|
185
|
+
working_directory: Optional[str],
|
|
186
|
+
session_id: Optional[str] = None,
|
|
187
|
+
create_folder: bool = True,
|
|
188
|
+
) -> None:
|
|
189
|
+
r"""Initialize session-specific storage paths and optionally create
|
|
190
|
+
directory structure for context file management."""
|
|
191
|
+
self.session_id = session_id or self._generate_session_id()
|
|
192
|
+
|
|
193
|
+
if working_directory:
|
|
194
|
+
self.working_directory = Path(working_directory).resolve()
|
|
195
|
+
else:
|
|
196
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
197
|
+
if camel_workdir:
|
|
198
|
+
self.working_directory = Path(camel_workdir) / "context_files"
|
|
199
|
+
else:
|
|
200
|
+
self.working_directory = Path("context_files")
|
|
201
|
+
|
|
202
|
+
# Create session-specific directory
|
|
203
|
+
self.working_directory = self.working_directory / self.session_id
|
|
204
|
+
|
|
205
|
+
# Only create directory if requested
|
|
206
|
+
if create_folder:
|
|
207
|
+
self.working_directory.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
|
|
209
|
+
def _generate_session_id(self) -> str:
|
|
210
|
+
r"""Create timestamp-based unique identifier for isolating
|
|
211
|
+
current session files from other sessions."""
|
|
212
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
|
|
213
|
+
return f"session_{timestamp}"
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def sanitize_workflow_filename(
|
|
217
|
+
name: str,
|
|
218
|
+
max_length: Optional[int] = None,
|
|
219
|
+
) -> str:
|
|
220
|
+
r"""Sanitize a name string for use as a workflow filename.
|
|
221
|
+
|
|
222
|
+
Converts the input string to a safe filename by:
|
|
223
|
+
- converting to lowercase
|
|
224
|
+
- replacing spaces with underscores
|
|
225
|
+
- removing special characters (keeping only alphanumeric and
|
|
226
|
+
underscores)
|
|
227
|
+
- truncating to maximum length if specified
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
name (str): The name string to sanitize (e.g., role_name or
|
|
231
|
+
task_title).
|
|
232
|
+
max_length (Optional[int]): Maximum length for the sanitized
|
|
233
|
+
filename. If None, uses MAX_WORKFLOW_FILENAME_LENGTH.
|
|
234
|
+
(default: :obj:`None`)
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
str: Sanitized filename string suitable for filesystem use.
|
|
238
|
+
Returns "agent" if sanitization results in empty string.
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> ContextUtility.sanitize_workflow_filename("Data Analyst!")
|
|
242
|
+
'data_analyst'
|
|
243
|
+
>>> ContextUtility.sanitize_workflow_filename("Test@123", 5)
|
|
244
|
+
'test1'
|
|
245
|
+
"""
|
|
246
|
+
if max_length is None:
|
|
247
|
+
max_length = ContextUtility.MAX_WORKFLOW_FILENAME_LENGTH
|
|
248
|
+
|
|
249
|
+
# sanitize: lowercase, spaces to underscores, remove special chars
|
|
250
|
+
clean_name = name.lower().replace(" ", "_")
|
|
251
|
+
clean_name = re.sub(r'[^a-z0-9_]', '', clean_name)
|
|
252
|
+
|
|
253
|
+
# truncate if too long
|
|
254
|
+
if len(clean_name) > max_length:
|
|
255
|
+
clean_name = clean_name[:max_length]
|
|
256
|
+
|
|
257
|
+
# ensure it's not empty after sanitization
|
|
258
|
+
if not clean_name:
|
|
259
|
+
clean_name = "agent"
|
|
260
|
+
|
|
261
|
+
return clean_name
|
|
262
|
+
|
|
263
|
+
# ========= GENERIC FILE MANAGEMENT METHODS =========
|
|
264
|
+
|
|
265
|
+
def _ensure_directory_exists(self) -> None:
|
|
266
|
+
r"""Ensure the working directory exists, creating it if necessary."""
|
|
267
|
+
self.working_directory.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
|
|
269
|
+
def _create_or_update_note(self, note_name: str, content: str) -> str:
|
|
270
|
+
r"""Write content to markdown file, creating new file or
|
|
271
|
+
overwriting existing one with UTF-8 encoding.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
note_name (str): Name of the note (without .md extension).
|
|
275
|
+
content (str): Content to write to the note.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
str: Success message.
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
# Ensure directory exists before writing
|
|
282
|
+
self._ensure_directory_exists()
|
|
283
|
+
file_path = self.working_directory / f"{note_name}.md"
|
|
284
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
285
|
+
f.write(content)
|
|
286
|
+
return f"Note '{note_name}.md' created successfully"
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.error(f"Error creating note {note_name}: {e}")
|
|
289
|
+
return f"Error creating note: {e}"
|
|
290
|
+
|
|
291
|
+
def save_markdown_file(
|
|
292
|
+
self,
|
|
293
|
+
filename: str,
|
|
294
|
+
content: str,
|
|
295
|
+
title: Optional[str] = None,
|
|
296
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
297
|
+
) -> str:
|
|
298
|
+
r"""Generic method to save any markdown content to a file.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
filename (str): Name without .md extension.
|
|
302
|
+
content (str): Main content to save.
|
|
303
|
+
title (str, optional): Title for the markdown file.
|
|
304
|
+
metadata (Dict, optional): Additional metadata to include.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
str: "success" on success, error message starting with "Error:"
|
|
308
|
+
on failure.
|
|
309
|
+
"""
|
|
310
|
+
try:
|
|
311
|
+
markdown_content = ""
|
|
312
|
+
|
|
313
|
+
# Add title if provided
|
|
314
|
+
if title:
|
|
315
|
+
markdown_content += f"# {title}\n\n"
|
|
316
|
+
|
|
317
|
+
# Add metadata section if provided
|
|
318
|
+
if metadata:
|
|
319
|
+
markdown_content += "## Metadata\n\n"
|
|
320
|
+
for key, value in metadata.items():
|
|
321
|
+
markdown_content += f"- {key}: {value}\n"
|
|
322
|
+
markdown_content += "\n"
|
|
323
|
+
|
|
324
|
+
# Add main content
|
|
325
|
+
markdown_content += content
|
|
326
|
+
|
|
327
|
+
self._create_or_update_note(filename, markdown_content)
|
|
328
|
+
logger.info(
|
|
329
|
+
f"Markdown file '{filename}.md' saved successfully to "
|
|
330
|
+
f"{self.working_directory / f'{filename}.md'}"
|
|
331
|
+
)
|
|
332
|
+
return "success"
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Error saving markdown file {filename}: {e}")
|
|
336
|
+
return f"Error: {e}"
|
|
337
|
+
|
|
338
|
+
def structured_output_to_markdown(
|
|
339
|
+
self,
|
|
340
|
+
structured_data: BaseModel,
|
|
341
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
342
|
+
title: Optional[str] = None,
|
|
343
|
+
field_mappings: Optional[Dict[str, str]] = None,
|
|
344
|
+
) -> str:
|
|
345
|
+
r"""Convert any Pydantic BaseModel instance to markdown format.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
structured_data: Any Pydantic BaseModel instance
|
|
349
|
+
metadata: Optional metadata to include in the markdown
|
|
350
|
+
title: Optional custom title, defaults to model class name
|
|
351
|
+
field_mappings: Optional mapping of field names to custom
|
|
352
|
+
section titles
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
str: Markdown formatted content
|
|
356
|
+
"""
|
|
357
|
+
markdown_content = []
|
|
358
|
+
|
|
359
|
+
# Add metadata if provided
|
|
360
|
+
if metadata:
|
|
361
|
+
markdown_content.append("## Metadata\n")
|
|
362
|
+
for key, value in metadata.items():
|
|
363
|
+
markdown_content.append(f"- {key}: {value}")
|
|
364
|
+
markdown_content.append("")
|
|
365
|
+
|
|
366
|
+
# Add title
|
|
367
|
+
if title:
|
|
368
|
+
markdown_content.extend([f"## {title}", ""])
|
|
369
|
+
else:
|
|
370
|
+
model_name = structured_data.__class__.__name__
|
|
371
|
+
markdown_content.extend([f"## {model_name}", ""])
|
|
372
|
+
|
|
373
|
+
# Get model fields and values
|
|
374
|
+
model_dict = structured_data.model_dump()
|
|
375
|
+
|
|
376
|
+
for field_name, field_value in model_dict.items():
|
|
377
|
+
# Use custom mapping or convert field name to title case
|
|
378
|
+
if field_mappings and field_name in field_mappings:
|
|
379
|
+
section_title = field_mappings[field_name]
|
|
380
|
+
else:
|
|
381
|
+
# Convert snake_case to Title Case
|
|
382
|
+
section_title = field_name.replace('_', ' ').title()
|
|
383
|
+
|
|
384
|
+
markdown_content.append(f"### {section_title}")
|
|
385
|
+
|
|
386
|
+
# Handle different data types
|
|
387
|
+
if isinstance(field_value, list):
|
|
388
|
+
if field_value:
|
|
389
|
+
for i, item in enumerate(field_value):
|
|
390
|
+
if isinstance(item, str):
|
|
391
|
+
# Check if it looks like a numbered item already
|
|
392
|
+
if item.strip() and not item.strip()[0].isdigit():
|
|
393
|
+
# For steps or numbered lists, add numbers
|
|
394
|
+
if 'step' in field_name.lower():
|
|
395
|
+
markdown_content.append(f"{i + 1}. {item}")
|
|
396
|
+
else:
|
|
397
|
+
markdown_content.append(f"- {item}")
|
|
398
|
+
else:
|
|
399
|
+
markdown_content.append(f"- {item}")
|
|
400
|
+
else:
|
|
401
|
+
markdown_content.append(f"- {item!s}")
|
|
402
|
+
else:
|
|
403
|
+
markdown_content.append(
|
|
404
|
+
f"(No {section_title.lower()} recorded)"
|
|
405
|
+
)
|
|
406
|
+
elif isinstance(field_value, str):
|
|
407
|
+
if field_value.strip():
|
|
408
|
+
markdown_content.append(field_value)
|
|
409
|
+
else:
|
|
410
|
+
markdown_content.append(
|
|
411
|
+
f"(No {section_title.lower()} provided)"
|
|
412
|
+
)
|
|
413
|
+
elif isinstance(field_value, dict):
|
|
414
|
+
for k, v in field_value.items():
|
|
415
|
+
markdown_content.append(f"- **{k}**: {v}")
|
|
416
|
+
else:
|
|
417
|
+
markdown_content.append(str(field_value))
|
|
418
|
+
|
|
419
|
+
markdown_content.append("")
|
|
420
|
+
|
|
421
|
+
return "\n".join(markdown_content)
|
|
422
|
+
|
|
423
|
+
def load_markdown_file(self, filename: str) -> str:
|
|
424
|
+
r"""Generic method to load any markdown file.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
filename (str): Name without .md extension.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
str: File content or empty string if not found.
|
|
431
|
+
"""
|
|
432
|
+
try:
|
|
433
|
+
file_path = self.working_directory / f"{filename}.md"
|
|
434
|
+
if file_path.exists():
|
|
435
|
+
return file_path.read_text(encoding="utf-8")
|
|
436
|
+
return ""
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logger.error(f"Error loading markdown file {filename}: {e}")
|
|
439
|
+
return ""
|
|
440
|
+
|
|
441
|
+
def file_exists(self, filename: str) -> bool:
|
|
442
|
+
r"""Verify presence of markdown file in current session directory.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
filename (str): Name without .md extension.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
bool: True if file exists, False otherwise.
|
|
449
|
+
"""
|
|
450
|
+
file_path = self.working_directory / f"{filename}.md"
|
|
451
|
+
return file_path.exists()
|
|
452
|
+
|
|
453
|
+
def list_markdown_files(self) -> List[str]:
|
|
454
|
+
r"""Discover all markdown files in current session directory
|
|
455
|
+
and return their base names for reference.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
List[str]: List of filenames without .md extension.
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
md_files = list(self.working_directory.glob("*.md"))
|
|
462
|
+
return [f.stem for f in md_files]
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.error(f"Error listing markdown files: {e}")
|
|
465
|
+
return []
|
|
466
|
+
|
|
467
|
+
# ========= GENERIC AGENT MEMORY METHODS =========
|
|
468
|
+
|
|
469
|
+
def get_agent_memory_records(
|
|
470
|
+
self, agent: "ChatAgent"
|
|
471
|
+
) -> List["MemoryRecord"]:
|
|
472
|
+
r"""Retrieve conversation history from agent's memory system.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
agent (ChatAgent): The agent to extract memory records from.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
List[MemoryRecord]: List of memory records from the agent.
|
|
479
|
+
"""
|
|
480
|
+
try:
|
|
481
|
+
context_records = agent.memory.retrieve()
|
|
482
|
+
return [cr.memory_record for cr in context_records]
|
|
483
|
+
except Exception as e:
|
|
484
|
+
logger.error(f"Error extracting memory records: {e}")
|
|
485
|
+
return []
|
|
486
|
+
|
|
487
|
+
def format_memory_as_conversation(
|
|
488
|
+
self, memory_records: List["MemoryRecord"]
|
|
489
|
+
) -> str:
|
|
490
|
+
r"""Transform structured memory records into human-readable
|
|
491
|
+
conversation format with role labels and message content.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
memory_records (List[MemoryRecord]): Memory records to format.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
str: Formatted conversation text.
|
|
498
|
+
"""
|
|
499
|
+
conversation_lines = []
|
|
500
|
+
|
|
501
|
+
for record in memory_records:
|
|
502
|
+
role = (
|
|
503
|
+
record.role_at_backend.value
|
|
504
|
+
if hasattr(record.role_at_backend, 'value')
|
|
505
|
+
else str(record.role_at_backend)
|
|
506
|
+
)
|
|
507
|
+
content = record.message.content
|
|
508
|
+
conversation_lines.append(f"{role}: {content}")
|
|
509
|
+
|
|
510
|
+
return "\n".join(conversation_lines)
|
|
511
|
+
|
|
512
|
+
# ========= SESSION MANAGEMENT METHODS =========
|
|
513
|
+
|
|
514
|
+
def create_session_directory(
|
|
515
|
+
self, base_dir: Optional[str] = None, session_id: Optional[str] = None
|
|
516
|
+
) -> Path:
|
|
517
|
+
r"""Create a session-specific directory.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
base_dir (str, optional): Base directory. If None, uses current
|
|
521
|
+
working directory.
|
|
522
|
+
session_id (str, optional): Custom session ID. If None, generates
|
|
523
|
+
new one.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Path: The created session directory path.
|
|
527
|
+
"""
|
|
528
|
+
if session_id is None:
|
|
529
|
+
session_id = self._generate_session_id()
|
|
530
|
+
|
|
531
|
+
if base_dir:
|
|
532
|
+
base_path = Path(base_dir).resolve()
|
|
533
|
+
else:
|
|
534
|
+
base_path = self.working_directory.parent
|
|
535
|
+
|
|
536
|
+
session_dir = base_path / session_id
|
|
537
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
538
|
+
return session_dir
|
|
539
|
+
|
|
540
|
+
def get_session_metadata(self) -> Dict[str, Any]:
|
|
541
|
+
r"""Collect comprehensive session information including identifiers,
|
|
542
|
+
timestamps, and directory paths for tracking and reference.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Dict[str, Any]: Session metadata including ID, timestamp,
|
|
546
|
+
directory.
|
|
547
|
+
"""
|
|
548
|
+
return {
|
|
549
|
+
'session_id': self.session_id,
|
|
550
|
+
'working_directory': str(self.working_directory),
|
|
551
|
+
'created_at': datetime.now().isoformat(),
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
def list_sessions(self, base_dir: Optional[str] = None) -> List[str]:
|
|
555
|
+
r"""Discover all available session directories for browsing
|
|
556
|
+
historical conversations and context files.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
base_dir (str, optional): Base directory to search. If None, uses
|
|
560
|
+
parent of working directory.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
List[str]: List of session directory names.
|
|
564
|
+
"""
|
|
565
|
+
try:
|
|
566
|
+
if base_dir:
|
|
567
|
+
search_dir = Path(base_dir)
|
|
568
|
+
else:
|
|
569
|
+
search_dir = self.working_directory.parent
|
|
570
|
+
|
|
571
|
+
session_dirs = [
|
|
572
|
+
d.name
|
|
573
|
+
for d in search_dir.iterdir()
|
|
574
|
+
if d.is_dir() and d.name.startswith('session_')
|
|
575
|
+
]
|
|
576
|
+
return sorted(session_dirs)
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(f"Error listing sessions: {e}")
|
|
579
|
+
return []
|
|
580
|
+
|
|
581
|
+
# ========= GENERIC SEARCH METHODS =========
|
|
582
|
+
|
|
583
|
+
def search_in_file(
|
|
584
|
+
self, file_path: Path, keywords: List[str], top_k: int = 4
|
|
585
|
+
) -> str:
|
|
586
|
+
r"""Perform keyword-based search through file sections,
|
|
587
|
+
ranking results by keyword frequency and returning top matches.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
file_path (Path): Path to the file to search.
|
|
591
|
+
keywords (List[str]): Keywords to search for.
|
|
592
|
+
top_k (int): Maximum number of results to return.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
str: Formatted search results.
|
|
596
|
+
"""
|
|
597
|
+
results: List[Dict[str, Any]] = []
|
|
598
|
+
keyword_terms = [keyword.lower() for keyword in keywords]
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
if not file_path.exists():
|
|
602
|
+
return ""
|
|
603
|
+
|
|
604
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
605
|
+
content = f.read()
|
|
606
|
+
|
|
607
|
+
# Split content into sections (assuming ### headers)
|
|
608
|
+
sections = content.split('### ')[1:] # Skip the header part
|
|
609
|
+
|
|
610
|
+
for i, section in enumerate(sections):
|
|
611
|
+
if not section.strip():
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
section_lower = section.lower()
|
|
615
|
+
|
|
616
|
+
# count how many keywords appear in this section
|
|
617
|
+
keyword_matches = sum(
|
|
618
|
+
1 for keyword in keyword_terms if keyword in section_lower
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
if keyword_matches > 0:
|
|
622
|
+
results.append(
|
|
623
|
+
{
|
|
624
|
+
'content': f"### {section.strip()}",
|
|
625
|
+
'keyword_count': keyword_matches,
|
|
626
|
+
'section_num': i + 1,
|
|
627
|
+
}
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.warning(f"Error reading file {file_path}: {e}")
|
|
632
|
+
return ""
|
|
633
|
+
|
|
634
|
+
# sort by keyword count and limit results
|
|
635
|
+
results.sort(key=lambda x: x['keyword_count'], reverse=True)
|
|
636
|
+
results = results[:top_k]
|
|
637
|
+
|
|
638
|
+
if not results:
|
|
639
|
+
return ""
|
|
640
|
+
|
|
641
|
+
# format results
|
|
642
|
+
formatted_sections = []
|
|
643
|
+
for result in results:
|
|
644
|
+
formatted_sections.append(
|
|
645
|
+
f"Section {result['section_num']} "
|
|
646
|
+
f"(keyword matches: {result['keyword_count']}):\n"
|
|
647
|
+
f"{result['content']}\n"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return "\n---\n".join(formatted_sections)
|
|
651
|
+
|
|
652
|
+
# ========= UTILITY METHODS =========
|
|
653
|
+
|
|
654
|
+
def get_working_directory(self) -> Path:
|
|
655
|
+
r"""Retrieve the session-specific directory path where
|
|
656
|
+
all context files are stored.
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
Path: The working directory path.
|
|
660
|
+
"""
|
|
661
|
+
return self.working_directory
|
|
662
|
+
|
|
663
|
+
def get_session_id(self) -> str:
|
|
664
|
+
r"""Retrieve the unique identifier for the current session
|
|
665
|
+
used for file organization and tracking.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
str: The session ID.
|
|
669
|
+
"""
|
|
670
|
+
return self.session_id
|
|
671
|
+
|
|
672
|
+
def set_session_id(self, session_id: str) -> None:
|
|
673
|
+
r"""Set a new session ID and update the working directory accordingly.
|
|
674
|
+
|
|
675
|
+
This allows sharing session directories between multiple ContextUtility
|
|
676
|
+
instances by using the same session_id.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
session_id (str): The session ID to use.
|
|
680
|
+
"""
|
|
681
|
+
self.session_id = session_id
|
|
682
|
+
|
|
683
|
+
# Update working directory with new session_id
|
|
684
|
+
if self.working_directory_param:
|
|
685
|
+
base_dir = Path(self.working_directory_param).resolve()
|
|
686
|
+
else:
|
|
687
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
688
|
+
if camel_workdir:
|
|
689
|
+
base_dir = Path(camel_workdir) / "context_files"
|
|
690
|
+
else:
|
|
691
|
+
base_dir = Path("context_files")
|
|
692
|
+
|
|
693
|
+
self.working_directory = base_dir / self.session_id
|
|
694
|
+
self.working_directory.mkdir(parents=True, exist_ok=True)
|
|
695
|
+
|
|
696
|
+
def load_markdown_context_to_memory(
|
|
697
|
+
self, agent: "ChatAgent", filename: str, include_metadata: bool = False
|
|
698
|
+
) -> str:
|
|
699
|
+
r"""Load context from a markdown file and append it to agent memory.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
agent (ChatAgent): The agent to append context to.
|
|
703
|
+
filename (str): Name of the markdown file (without .md extension).
|
|
704
|
+
include_metadata (bool): Whether to include metadata section in the
|
|
705
|
+
loaded content. Defaults to False.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
str: Status message indicating success or failure with details.
|
|
709
|
+
"""
|
|
710
|
+
try:
|
|
711
|
+
content = self.load_markdown_file(filename)
|
|
712
|
+
|
|
713
|
+
if not content.strip():
|
|
714
|
+
return f"Context file not found or empty: {filename}"
|
|
715
|
+
|
|
716
|
+
# Filter out metadata section if not requested
|
|
717
|
+
if not include_metadata:
|
|
718
|
+
content = self._filter_metadata_from_content(content)
|
|
719
|
+
|
|
720
|
+
from camel.types import OpenAIBackendRole
|
|
721
|
+
|
|
722
|
+
prefix_prompt = (
|
|
723
|
+
"The following is the context from a previous "
|
|
724
|
+
"session or workflow which might be useful for "
|
|
725
|
+
"to the current task. This information might help you "
|
|
726
|
+
"understand the background, choose which tools to use, "
|
|
727
|
+
"and plan your next steps."
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Append workflow content to the agent's system message
|
|
731
|
+
# This ensures the context persists when agents are cloned
|
|
732
|
+
workflow_content = (
|
|
733
|
+
f"\n\n--- Workflow Memory ---\n{prefix_prompt}\n\n{content}"
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
# Update the original system message to include workflow
|
|
737
|
+
if agent._original_system_message is None:
|
|
738
|
+
logger.error(
|
|
739
|
+
f"Agent {agent.agent_id} has no system message. "
|
|
740
|
+
"Cannot append workflow memory to system message."
|
|
741
|
+
)
|
|
742
|
+
return (
|
|
743
|
+
"Error: Agent has no system message to append workflow to"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Update the current system message
|
|
747
|
+
current_system_message = agent._system_message
|
|
748
|
+
if current_system_message is not None:
|
|
749
|
+
new_sys_content = (
|
|
750
|
+
current_system_message.content + workflow_content
|
|
751
|
+
)
|
|
752
|
+
agent._system_message = (
|
|
753
|
+
current_system_message.create_new_instance(new_sys_content)
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# Replace the system message in memory
|
|
757
|
+
# Clear and re-initialize with updated system message
|
|
758
|
+
agent.memory.clear()
|
|
759
|
+
agent.update_memory(
|
|
760
|
+
agent._system_message, OpenAIBackendRole.SYSTEM
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
char_count = len(content)
|
|
764
|
+
log_msg = (
|
|
765
|
+
f"Context appended to agent {agent.agent_id} "
|
|
766
|
+
f"({char_count} characters)"
|
|
767
|
+
)
|
|
768
|
+
logger.info(log_msg)
|
|
769
|
+
|
|
770
|
+
return log_msg
|
|
771
|
+
|
|
772
|
+
except Exception as e:
|
|
773
|
+
error_msg = f"Failed to load markdown context to memory: {e}"
|
|
774
|
+
logger.error(error_msg)
|
|
775
|
+
return error_msg
|
|
776
|
+
|
|
777
|
+
def _filter_metadata_from_content(self, content: str) -> str:
|
|
778
|
+
r"""Filter out metadata section from markdown content.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
content (str): The full markdown content including metadata.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
str: Content with metadata section removed.
|
|
785
|
+
"""
|
|
786
|
+
lines = content.split('\n')
|
|
787
|
+
filtered_lines = []
|
|
788
|
+
skip_metadata = False
|
|
789
|
+
|
|
790
|
+
for line in lines:
|
|
791
|
+
# Check if we're starting a metadata section
|
|
792
|
+
if line.strip() == "## Metadata":
|
|
793
|
+
skip_metadata = True
|
|
794
|
+
continue
|
|
795
|
+
|
|
796
|
+
# Check if we're starting a new section after metadata
|
|
797
|
+
if (
|
|
798
|
+
skip_metadata
|
|
799
|
+
and line.startswith("## ")
|
|
800
|
+
and "Metadata" not in line
|
|
801
|
+
):
|
|
802
|
+
skip_metadata = False
|
|
803
|
+
|
|
804
|
+
# Add line if we're not in metadata section
|
|
805
|
+
if not skip_metadata:
|
|
806
|
+
filtered_lines.append(line)
|
|
807
|
+
|
|
808
|
+
# Clean up any extra whitespace at the beginning
|
|
809
|
+
result = '\n'.join(filtered_lines).strip()
|
|
810
|
+
return result
|
|
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
|
+
|
|
943
|
+
# ========= SHARED SESSION MANAGEMENT METHODS =========
|
|
944
|
+
|
|
945
|
+
@classmethod
|
|
946
|
+
def get_workforce_shared(
|
|
947
|
+
cls, session_id: Optional[str] = None
|
|
948
|
+
) -> 'ContextUtility':
|
|
949
|
+
r"""Get or create shared workforce context utility with lazy init.
|
|
950
|
+
|
|
951
|
+
This method provides a centralized way to access shared context
|
|
952
|
+
utilities for workforce workflows, ensuring all workforce components
|
|
953
|
+
use the same session directory.
|
|
954
|
+
|
|
955
|
+
Args:
|
|
956
|
+
session_id (str, optional): Custom session ID. If None, uses the
|
|
957
|
+
default workforce session.
|
|
958
|
+
|
|
959
|
+
Returns:
|
|
960
|
+
ContextUtility: Shared context utility instance for workforce.
|
|
961
|
+
"""
|
|
962
|
+
if session_id is None:
|
|
963
|
+
# Use default workforce session
|
|
964
|
+
if cls._default_workforce_session is None:
|
|
965
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
966
|
+
if camel_workdir:
|
|
967
|
+
base_path = os.path.join(
|
|
968
|
+
camel_workdir, "workforce_workflows"
|
|
969
|
+
)
|
|
970
|
+
else:
|
|
971
|
+
base_path = "workforce_workflows"
|
|
972
|
+
|
|
973
|
+
cls._default_workforce_session = cls(
|
|
974
|
+
working_directory=base_path,
|
|
975
|
+
create_folder=False, # Don't create folder until needed
|
|
976
|
+
)
|
|
977
|
+
return cls._default_workforce_session
|
|
978
|
+
|
|
979
|
+
# Use specific session
|
|
980
|
+
if session_id not in cls._shared_sessions:
|
|
981
|
+
camel_workdir = os.environ.get("CAMEL_WORKDIR")
|
|
982
|
+
if camel_workdir:
|
|
983
|
+
base_path = os.path.join(camel_workdir, "workforce_workflows")
|
|
984
|
+
else:
|
|
985
|
+
base_path = "workforce_workflows"
|
|
986
|
+
|
|
987
|
+
cls._shared_sessions[session_id] = cls(
|
|
988
|
+
working_directory=base_path,
|
|
989
|
+
session_id=session_id,
|
|
990
|
+
create_folder=False, # Don't create folder until needed
|
|
991
|
+
)
|
|
992
|
+
return cls._shared_sessions[session_id]
|
|
993
|
+
|
|
994
|
+
@classmethod
|
|
995
|
+
def reset_shared_sessions(cls) -> None:
|
|
996
|
+
r"""Reset shared sessions (useful for testing).
|
|
997
|
+
|
|
998
|
+
This method clears all shared session instances, forcing new ones
|
|
999
|
+
to be created on next access. Primarily used for testing to ensure
|
|
1000
|
+
clean state between tests.
|
|
1001
|
+
"""
|
|
1002
|
+
cls._shared_sessions.clear()
|
|
1003
|
+
cls._default_workforce_session = None
|