camel-ai 0.2.71a1__py3-none-any.whl → 0.2.71a3__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/_types.py +6 -2
- camel/agents/chat_agent.py +357 -18
- camel/messages/base.py +2 -6
- camel/messages/func_message.py +32 -5
- camel/services/agent_openapi_server.py +380 -0
- camel/societies/workforce/single_agent_worker.py +1 -5
- camel/societies/workforce/workforce.py +68 -8
- camel/tasks/task.py +2 -2
- camel/toolkits/__init__.py +2 -2
- camel/toolkits/craw4ai_toolkit.py +27 -7
- camel/toolkits/file_write_toolkit.py +110 -31
- camel/toolkits/human_toolkit.py +19 -14
- camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/__init__.py +2 -2
- camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/actions.py +47 -11
- camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/agent.py +21 -11
- camel/toolkits/{non_visual_browser_toolkit/nv_browser_session.py → hybrid_browser_toolkit/browser_session.py} +64 -10
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +1002 -0
- camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/snapshot.py +16 -4
- camel/toolkits/{non_visual_browser_toolkit/snapshot.js → hybrid_browser_toolkit/unified_analyzer.js} +171 -15
- camel/toolkits/jina_reranker_toolkit.py +3 -4
- camel/toolkits/terminal_toolkit.py +189 -48
- camel/toolkits/video_download_toolkit.py +1 -2
- camel/types/agents/tool_calling_record.py +4 -1
- camel/types/enums.py +24 -24
- camel/utils/message_summarizer.py +148 -0
- camel/utils/tool_result.py +44 -0
- {camel_ai-0.2.71a1.dist-info → camel_ai-0.2.71a3.dist-info}/METADATA +19 -5
- {camel_ai-0.2.71a1.dist-info → camel_ai-0.2.71a3.dist-info}/RECORD +31 -28
- camel/toolkits/non_visual_browser_toolkit/browser_non_visual_toolkit.py +0 -446
- {camel_ai-0.2.71a1.dist-info → camel_ai-0.2.71a3.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a1.dist-info → camel_ai-0.2.71a3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
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
|
+
|
|
16
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
17
|
+
|
|
18
|
+
from fastapi import APIRouter, FastAPI, HTTPException
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from camel.agents.chat_agent import ChatAgent
|
|
22
|
+
from camel.messages import BaseMessage
|
|
23
|
+
from camel.models import ModelFactory
|
|
24
|
+
from camel.toolkits import FunctionTool
|
|
25
|
+
from camel.types import RoleType
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InitRequest(BaseModel):
|
|
29
|
+
r"""Request schema for initializing a ChatAgent via the OpenAPI server.
|
|
30
|
+
|
|
31
|
+
Defines the configuration used to create a new agent, including the model,
|
|
32
|
+
system message, tool names, and generation parameters.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
model_type (Optional[str]): The model type to use. Should match a key
|
|
36
|
+
supported by the model manager, e.g., "gpt-4o-mini".
|
|
37
|
+
(default: :obj:`"gpt-4o-mini"`)
|
|
38
|
+
model_platform (Optional[str]): The model platform to use.
|
|
39
|
+
(default: :obj:`"openai"`)
|
|
40
|
+
tools_names (Optional[List[str]]): A list of tool names to load from
|
|
41
|
+
the tool registry. These tools will be available to the agent.
|
|
42
|
+
(default: :obj:`None`)
|
|
43
|
+
external_tools (Optional[List[Dict[str, Any]]]): Tool definitions
|
|
44
|
+
provided directly as dictionaries, bypassing the registry.
|
|
45
|
+
Currently not supported. (default: :obj:`None`)
|
|
46
|
+
agent_id (str): The unique identifier for the agent. Must be provided
|
|
47
|
+
explicitly to support multi-agent routing and control.
|
|
48
|
+
system_message (Optional[str]): The system prompt for the agent,
|
|
49
|
+
describing its behavior or role. (default: :obj:`None`)
|
|
50
|
+
message_window_size (Optional[int]): The number of recent messages to
|
|
51
|
+
retain in memory for context. (default: :obj:`None`)
|
|
52
|
+
token_limit (Optional[int]): The token budget for contextual memory.
|
|
53
|
+
(default: :obj:`None`)
|
|
54
|
+
output_language (Optional[str]): Preferred output language for the
|
|
55
|
+
agent's replies. (default: :obj:`None`)
|
|
56
|
+
max_iteration (Optional[int]): Maximum number of model
|
|
57
|
+
calling iterations allowed per step. If `None` (default), there's
|
|
58
|
+
no explicit limit. If `1`, it performs a single model call. If `N
|
|
59
|
+
> 1`, it allows up to N model calls. (default: :obj:`None`)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
model_type: Optional[str] = "gpt-4o-mini"
|
|
63
|
+
model_platform: Optional[str] = "openai"
|
|
64
|
+
|
|
65
|
+
tools_names: Optional[List[str]] = None
|
|
66
|
+
external_tools: Optional[List[Dict[str, Any]]] = None
|
|
67
|
+
|
|
68
|
+
agent_id: str # Required: explicitly set agent_id to
|
|
69
|
+
# support future multi-agent and permission control
|
|
70
|
+
|
|
71
|
+
system_message: Optional[str] = None
|
|
72
|
+
message_window_size: Optional[int] = None
|
|
73
|
+
token_limit: Optional[int] = None
|
|
74
|
+
output_language: Optional[str] = None
|
|
75
|
+
max_iteration: Optional[int] = None # Changed from Optional[bool] = False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class StepRequest(BaseModel):
|
|
79
|
+
r"""Request schema for sending a user message to a ChatAgent.
|
|
80
|
+
|
|
81
|
+
Supports plain text input or structured message dictionaries, with an
|
|
82
|
+
optional response format for controlling output structure.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
input_message (Union[str, Dict[str, Any]]): The user message to send.
|
|
86
|
+
Can be a plain string or a message dict with role, content, etc.
|
|
87
|
+
response_format (Optional[str]): Optional format name that maps to a
|
|
88
|
+
registered response schema. Not currently in use.
|
|
89
|
+
(default: :obj:`None`)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
input_message: Union[str, Dict[str, Any]]
|
|
93
|
+
response_format: Optional[str] = None # reserved, not used yet
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ChatAgentOpenAPIServer:
|
|
97
|
+
r"""A FastAPI server wrapper for managing ChatAgents via OpenAPI routes.
|
|
98
|
+
|
|
99
|
+
This server exposes a versioned REST API for interacting with CAMEL
|
|
100
|
+
agents, supporting initialization, message passing, memory inspection,
|
|
101
|
+
and optional tool usage. It supports multi-agent use cases by mapping
|
|
102
|
+
unique agent IDs to active ChatAgent instances.
|
|
103
|
+
|
|
104
|
+
Typical usage includes initializing agents with system prompts and tools,
|
|
105
|
+
exchanging messages using /step or /astep endpoints, and inspecting agent
|
|
106
|
+
memory with /history.
|
|
107
|
+
|
|
108
|
+
Supports pluggable tool and response format registries for customizing
|
|
109
|
+
agent behavior or output schemas.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
tool_registry: Optional[Dict[str, List[FunctionTool]]] = None,
|
|
115
|
+
response_format_registry: Optional[Dict[str, Type[BaseModel]]] = None,
|
|
116
|
+
):
|
|
117
|
+
r"""Initializes the OpenAPI server for managing ChatAgents.
|
|
118
|
+
|
|
119
|
+
Sets up internal agent storage, tool and response format registries,
|
|
120
|
+
and prepares versioned API routes.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
tool_registry (Optional[Dict[str, List[FunctionTool]]]): A mapping
|
|
124
|
+
from tool names to lists of FunctionTool instances available
|
|
125
|
+
to agents via the "tools_names" field. If not provided, an
|
|
126
|
+
empty registry is used. (default: :obj:`None`)
|
|
127
|
+
response_format_registry (Optional[Dict[str, Type[BaseModel]]]):
|
|
128
|
+
A mapping from format names to Pydantic output schemas for
|
|
129
|
+
structured response parsing. Used for controlling the format
|
|
130
|
+
of step results. (default: :obj:`None`)
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
# Initialize FastAPI app and agent
|
|
134
|
+
self.app = FastAPI(title="CAMEL OpenAPI-compatible Server")
|
|
135
|
+
self.agents: Dict[str, ChatAgent] = {}
|
|
136
|
+
self.tool_registry = tool_registry or {}
|
|
137
|
+
self.response_format_registry = response_format_registry or {}
|
|
138
|
+
self._setup_routes()
|
|
139
|
+
|
|
140
|
+
def _parse_input_message_for_step(
|
|
141
|
+
self, raw: Union[str, dict]
|
|
142
|
+
) -> BaseMessage:
|
|
143
|
+
r"""Parses raw input into a BaseMessage object.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
raw (str or dict): User input as plain text or dict.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
BaseMessage: Parsed input message.
|
|
150
|
+
"""
|
|
151
|
+
if isinstance(raw, str):
|
|
152
|
+
return BaseMessage.make_user_message(role_name="User", content=raw)
|
|
153
|
+
elif isinstance(raw, dict):
|
|
154
|
+
if isinstance(raw.get("role_type"), str):
|
|
155
|
+
raw["role_type"] = RoleType(raw["role_type"].lower())
|
|
156
|
+
return BaseMessage(**raw)
|
|
157
|
+
raise HTTPException(
|
|
158
|
+
status_code=400, detail="Unsupported input format."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _resolve_response_format_for_step(
|
|
162
|
+
self, name: Optional[str]
|
|
163
|
+
) -> Optional[Type[BaseModel]]:
|
|
164
|
+
r"""Resolves the response format by name.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name (str or None): Optional format name.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Optional[Type[BaseModel]]: Response schema class.
|
|
171
|
+
"""
|
|
172
|
+
if name is None:
|
|
173
|
+
return None
|
|
174
|
+
if name not in self.response_format_registry:
|
|
175
|
+
raise HTTPException(
|
|
176
|
+
status_code=400, detail=f"Unknown response_format: {name}"
|
|
177
|
+
)
|
|
178
|
+
return self.response_format_registry[name]
|
|
179
|
+
|
|
180
|
+
def _setup_routes(self):
|
|
181
|
+
r"""Registers OpenAPI endpoints for agent creation and interaction.
|
|
182
|
+
|
|
183
|
+
This includes routes for initializing agents (/init), sending
|
|
184
|
+
messages (/step and /astep), resetting agent memory (/reset), and
|
|
185
|
+
retrieving conversation history (/history). All routes are added
|
|
186
|
+
under the /v1/agents namespace.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
router = APIRouter(prefix="/v1/agents")
|
|
190
|
+
|
|
191
|
+
@router.post("/init")
|
|
192
|
+
def init_agent(request: InitRequest):
|
|
193
|
+
r"""Initializes a ChatAgent instance with a model,
|
|
194
|
+
system message, and optional tools.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
request (InitRequest): The agent config including
|
|
198
|
+
model, tools, system message, and agent ID.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
dict: A message with the agent ID and status.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
agent_id = request.agent_id
|
|
205
|
+
if agent_id in self.agents:
|
|
206
|
+
return {
|
|
207
|
+
"agent_id": agent_id,
|
|
208
|
+
"message": "Agent already exists.",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
model_type = request.model_type
|
|
212
|
+
model_platform = request.model_platform
|
|
213
|
+
|
|
214
|
+
model = ModelFactory.create(
|
|
215
|
+
model_platform=model_platform, # type: ignore[arg-type]
|
|
216
|
+
model_type=model_type, # type: ignore[arg-type]
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# tools lookup
|
|
220
|
+
tools = []
|
|
221
|
+
if request.tools_names:
|
|
222
|
+
for name in request.tools_names:
|
|
223
|
+
if name in self.tool_registry:
|
|
224
|
+
tools.extend(self.tool_registry[name])
|
|
225
|
+
else:
|
|
226
|
+
raise HTTPException(
|
|
227
|
+
status_code=400,
|
|
228
|
+
detail=f"Tool '{name}' " f"not found in registry",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# system message
|
|
232
|
+
system_message = request.system_message
|
|
233
|
+
|
|
234
|
+
agent = ChatAgent(
|
|
235
|
+
model=model,
|
|
236
|
+
tools=tools, # type: ignore[arg-type]
|
|
237
|
+
external_tools=request.external_tools, # type: ignore[arg-type]
|
|
238
|
+
system_message=system_message,
|
|
239
|
+
message_window_size=request.message_window_size,
|
|
240
|
+
token_limit=request.token_limit,
|
|
241
|
+
output_language=request.output_language,
|
|
242
|
+
max_iteration=request.max_iteration,
|
|
243
|
+
agent_id=agent_id,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self.agents[agent_id] = agent
|
|
247
|
+
return {"agent_id": agent_id, "message": "Agent initialized."}
|
|
248
|
+
|
|
249
|
+
@router.post("/astep/{agent_id}")
|
|
250
|
+
async def astep_agent(agent_id: str, request: StepRequest):
|
|
251
|
+
r"""Runs one async step of agent response.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
agent_id (str): The ID of the target agent.
|
|
255
|
+
request (StepRequest): The input message.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
dict: The model response in serialized form.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
if agent_id not in self.agents:
|
|
262
|
+
raise HTTPException(status_code=404, detail="Agent not found.")
|
|
263
|
+
|
|
264
|
+
agent = self.agents[agent_id]
|
|
265
|
+
input_message = self._parse_input_message_for_step(
|
|
266
|
+
request.input_message
|
|
267
|
+
)
|
|
268
|
+
format_cls = self._resolve_response_format_for_step(
|
|
269
|
+
request.response_format
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
response = await agent.astep(
|
|
274
|
+
input_message=input_message, response_format=format_cls
|
|
275
|
+
)
|
|
276
|
+
return response.model_dump()
|
|
277
|
+
except Exception as e:
|
|
278
|
+
raise HTTPException(
|
|
279
|
+
status_code=500,
|
|
280
|
+
detail=f"Unexpected error during async step: {e!s}",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
@router.get("/list_agent_ids")
|
|
284
|
+
def list_agent_ids():
|
|
285
|
+
r"""Returns a list of all active agent IDs.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
dict: A dictionary containing all registered agent IDs.
|
|
289
|
+
"""
|
|
290
|
+
return {"agent_ids": list(self.agents.keys())}
|
|
291
|
+
|
|
292
|
+
@router.post("/delete/{agent_id}")
|
|
293
|
+
def delete_agent(agent_id: str):
|
|
294
|
+
r"""Deletes an agent from the server.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
agent_id (str): The ID of the agent to delete.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
dict: A confirmation message upon successful deletion.
|
|
301
|
+
"""
|
|
302
|
+
if agent_id not in self.agents:
|
|
303
|
+
raise HTTPException(status_code=404, detail="Agent not found.")
|
|
304
|
+
|
|
305
|
+
del self.agents[agent_id]
|
|
306
|
+
return {"message": f"Agent {agent_id} deleted."}
|
|
307
|
+
|
|
308
|
+
@router.post("/step/{agent_id}")
|
|
309
|
+
def step_agent(agent_id: str, request: StepRequest):
|
|
310
|
+
r"""Runs one step of synchronous agent response.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
agent_id (str): The ID of the target agent.
|
|
314
|
+
request (StepRequest): The input message.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
dict: The model response in serialized form.
|
|
318
|
+
"""
|
|
319
|
+
if agent_id not in self.agents:
|
|
320
|
+
raise HTTPException(status_code=404, detail="Agent not found.")
|
|
321
|
+
|
|
322
|
+
agent = self.agents[agent_id]
|
|
323
|
+
input_message = self._parse_input_message_for_step(
|
|
324
|
+
request.input_message
|
|
325
|
+
)
|
|
326
|
+
format_cls = self._resolve_response_format_for_step(
|
|
327
|
+
request.response_format
|
|
328
|
+
)
|
|
329
|
+
try:
|
|
330
|
+
response = agent.step(
|
|
331
|
+
input_message=input_message, response_format=format_cls
|
|
332
|
+
)
|
|
333
|
+
return response.model_dump()
|
|
334
|
+
except Exception as e:
|
|
335
|
+
raise HTTPException(
|
|
336
|
+
status_code=500,
|
|
337
|
+
detail=f"Unexpected error during step: {e!s}",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
@router.post("/reset/{agent_id}")
|
|
341
|
+
def reset_agent(agent_id: str):
|
|
342
|
+
r"""Clears memory for a specific agent.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
agent_id (str): The ID of the agent to reset.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
dict: A message confirming reset success.
|
|
349
|
+
"""
|
|
350
|
+
if agent_id not in self.agents:
|
|
351
|
+
raise HTTPException(status_code=404, detail="Agent not found.")
|
|
352
|
+
self.agents[agent_id].reset()
|
|
353
|
+
return {"message": f"Agent {agent_id} reset."}
|
|
354
|
+
|
|
355
|
+
@router.get("/history/{agent_id}")
|
|
356
|
+
def get_agent_chat_history(agent_id: str):
|
|
357
|
+
r"""Returns the chat history of an agent.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
agent_id (str): The ID of the agent to query.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
list: The list of conversation messages.
|
|
364
|
+
"""
|
|
365
|
+
if agent_id not in self.agents:
|
|
366
|
+
raise HTTPException(
|
|
367
|
+
status_code=404, detail=f"Agent {agent_id} not found."
|
|
368
|
+
)
|
|
369
|
+
return self.agents[agent_id].chat_history
|
|
370
|
+
|
|
371
|
+
# Register all routes to the main FastAPI app
|
|
372
|
+
self.app.include_router(router)
|
|
373
|
+
|
|
374
|
+
def get_app(self) -> FastAPI:
|
|
375
|
+
r"""Returns the FastAPI app instance.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
FastAPI: The wrapped application object.
|
|
379
|
+
"""
|
|
380
|
+
return self.app
|
|
@@ -43,8 +43,6 @@ class AgentPool:
|
|
|
43
43
|
(default: :obj:`10`)
|
|
44
44
|
auto_scale (bool): Whether to automatically scale the pool size.
|
|
45
45
|
(default: :obj:`True`)
|
|
46
|
-
scale_factor (float): Factor by which to scale the pool when needed.
|
|
47
|
-
(default: :obj:`1.5`)
|
|
48
46
|
idle_timeout (float): Time in seconds after which idle agents are
|
|
49
47
|
removed. (default: :obj:`180.0`)
|
|
50
48
|
"""
|
|
@@ -55,13 +53,11 @@ class AgentPool:
|
|
|
55
53
|
initial_size: int = 1,
|
|
56
54
|
max_size: int = 10,
|
|
57
55
|
auto_scale: bool = True,
|
|
58
|
-
scale_factor: float = 1.5,
|
|
59
56
|
idle_timeout: float = 180.0, # 3 minutes
|
|
60
57
|
):
|
|
61
58
|
self.base_agent = base_agent
|
|
62
59
|
self.max_size = max_size
|
|
63
60
|
self.auto_scale = auto_scale
|
|
64
|
-
self.scale_factor = scale_factor
|
|
65
61
|
self.idle_timeout = idle_timeout
|
|
66
62
|
|
|
67
63
|
# Pool management
|
|
@@ -332,7 +328,7 @@ class SingleAgentWorker(Worker):
|
|
|
332
328
|
# Store the actual token usage for this specific task
|
|
333
329
|
task.additional_info["token_usage"] = {"total_tokens": total_tokens}
|
|
334
330
|
|
|
335
|
-
print(f"======\n{Fore.GREEN}
|
|
331
|
+
print(f"======\n{Fore.GREEN}Response from {self}:{Fore.RESET}")
|
|
336
332
|
|
|
337
333
|
try:
|
|
338
334
|
result_dict = json.loads(response.msg.content)
|
|
@@ -395,6 +395,40 @@ class Workforce(BaseNode):
|
|
|
395
395
|
"better context continuity during task handoffs."
|
|
396
396
|
)
|
|
397
397
|
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
# Helper for propagating pause control to externally supplied agents
|
|
400
|
+
# ------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
def _attach_pause_event_to_agent(self, agent: ChatAgent) -> None:
|
|
403
|
+
r"""Ensure the given ChatAgent shares this workforce's pause_event.
|
|
404
|
+
|
|
405
|
+
If the agent already has a different pause_event we overwrite it and
|
|
406
|
+
emit a debug log (it is unlikely an agent needs multiple independent
|
|
407
|
+
pause controls once managed by this workforce)."""
|
|
408
|
+
try:
|
|
409
|
+
existing_pause_event = getattr(agent, "pause_event", None)
|
|
410
|
+
if existing_pause_event is not self._pause_event:
|
|
411
|
+
if existing_pause_event is not None:
|
|
412
|
+
logger.debug(
|
|
413
|
+
f"Overriding pause_event for agent {agent.agent_id} "
|
|
414
|
+
f"(had different pause_event: "
|
|
415
|
+
f"{id(existing_pause_event)} "
|
|
416
|
+
f"-> {id(self._pause_event)})"
|
|
417
|
+
)
|
|
418
|
+
agent.pause_event = self._pause_event
|
|
419
|
+
except AttributeError:
|
|
420
|
+
# Should not happen, but guard against unexpected objects
|
|
421
|
+
logger.warning(
|
|
422
|
+
f"Cannot attach pause_event to object {type(agent)} - "
|
|
423
|
+
f"missing pause_event attribute"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def _ensure_pause_event_in_kwargs(self, kwargs: Optional[Dict]) -> Dict:
|
|
427
|
+
r"""Insert pause_event into kwargs dict for ChatAgent construction."""
|
|
428
|
+
new_kwargs = dict(kwargs) if kwargs else {}
|
|
429
|
+
new_kwargs.setdefault("pause_event", self._pause_event)
|
|
430
|
+
return new_kwargs
|
|
431
|
+
|
|
398
432
|
def __repr__(self):
|
|
399
433
|
return (
|
|
400
434
|
f"Workforce {self.node_id} ({self.description}) - "
|
|
@@ -1138,6 +1172,9 @@ class Workforce(BaseNode):
|
|
|
1138
1172
|
Returns:
|
|
1139
1173
|
Workforce: The workforce node itself.
|
|
1140
1174
|
"""
|
|
1175
|
+
# Ensure the worker agent shares this workforce's pause control
|
|
1176
|
+
self._attach_pause_event_to_agent(worker)
|
|
1177
|
+
|
|
1141
1178
|
worker_node = SingleAgentWorker(
|
|
1142
1179
|
description=description,
|
|
1143
1180
|
worker=worker,
|
|
@@ -1184,6 +1221,18 @@ class Workforce(BaseNode):
|
|
|
1184
1221
|
Returns:
|
|
1185
1222
|
Workforce: The workforce node itself.
|
|
1186
1223
|
"""
|
|
1224
|
+
# Ensure provided kwargs carry pause_event so that internally created
|
|
1225
|
+
# ChatAgents (assistant/user/summarizer) inherit it.
|
|
1226
|
+
assistant_agent_kwargs = self._ensure_pause_event_in_kwargs(
|
|
1227
|
+
assistant_agent_kwargs
|
|
1228
|
+
)
|
|
1229
|
+
user_agent_kwargs = self._ensure_pause_event_in_kwargs(
|
|
1230
|
+
user_agent_kwargs
|
|
1231
|
+
)
|
|
1232
|
+
summarize_agent_kwargs = self._ensure_pause_event_in_kwargs(
|
|
1233
|
+
summarize_agent_kwargs
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1187
1236
|
worker_node = RolePlayingWorker(
|
|
1188
1237
|
description=description,
|
|
1189
1238
|
assistant_role_name=assistant_role_name,
|
|
@@ -1212,6 +1261,9 @@ class Workforce(BaseNode):
|
|
|
1212
1261
|
Returns:
|
|
1213
1262
|
Workforce: The workforce node itself.
|
|
1214
1263
|
"""
|
|
1264
|
+
# Align child workforce's pause_event with this one for unified
|
|
1265
|
+
# control of worker agents only.
|
|
1266
|
+
workforce._pause_event = self._pause_event
|
|
1215
1267
|
self._children.append(workforce)
|
|
1216
1268
|
return self
|
|
1217
1269
|
|
|
@@ -1245,14 +1297,17 @@ class Workforce(BaseNode):
|
|
|
1245
1297
|
# Handle asyncio.Event in a thread-safe way
|
|
1246
1298
|
if self._loop and not self._loop.is_closed():
|
|
1247
1299
|
# If we have a loop, use it to set the event safely
|
|
1248
|
-
asyncio.run_coroutine_threadsafe(
|
|
1249
|
-
self._async_reset(), self._loop
|
|
1250
|
-
).result()
|
|
1251
|
-
else:
|
|
1252
1300
|
try:
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1301
|
+
asyncio.run_coroutine_threadsafe(
|
|
1302
|
+
self._async_reset(), self._loop
|
|
1303
|
+
).result()
|
|
1304
|
+
except RuntimeError as e:
|
|
1305
|
+
logger.warning(f"Failed to reset via existing loop: {e}")
|
|
1306
|
+
# Fallback to direct event manipulation
|
|
1307
|
+
self._pause_event.set()
|
|
1308
|
+
else:
|
|
1309
|
+
# No active loop, directly set the event
|
|
1310
|
+
self._pause_event.set()
|
|
1256
1311
|
|
|
1257
1312
|
if hasattr(self, 'metrics_logger') and self.metrics_logger is not None:
|
|
1258
1313
|
self.metrics_logger.reset_task_data()
|
|
@@ -1656,7 +1711,12 @@ class Workforce(BaseNode):
|
|
|
1656
1711
|
model_config_dict={"temperature": 0},
|
|
1657
1712
|
)
|
|
1658
1713
|
|
|
1659
|
-
return ChatAgent(
|
|
1714
|
+
return ChatAgent(
|
|
1715
|
+
worker_sys_msg,
|
|
1716
|
+
model=model,
|
|
1717
|
+
tools=function_list, # type: ignore[arg-type]
|
|
1718
|
+
pause_event=self._pause_event,
|
|
1719
|
+
)
|
|
1660
1720
|
|
|
1661
1721
|
async def _get_returned_task(self) -> Optional[Task]:
|
|
1662
1722
|
r"""Get the task that's published by this node and just get returned
|
camel/tasks/task.py
CHANGED
|
@@ -56,7 +56,7 @@ class TaskValidationMode(Enum):
|
|
|
56
56
|
def validate_task_content(
|
|
57
57
|
content: str,
|
|
58
58
|
task_id: str = "unknown",
|
|
59
|
-
min_length: int =
|
|
59
|
+
min_length: int = 1,
|
|
60
60
|
mode: TaskValidationMode = TaskValidationMode.INPUT,
|
|
61
61
|
check_failure_patterns: bool = True,
|
|
62
62
|
) -> bool:
|
|
@@ -69,7 +69,7 @@ def validate_task_content(
|
|
|
69
69
|
task_id (str): Task ID for logging purposes.
|
|
70
70
|
(default: :obj:`"unknown"`)
|
|
71
71
|
min_length (int): Minimum content length after stripping whitespace.
|
|
72
|
-
(default: :obj:`
|
|
72
|
+
(default: :obj:`1`)
|
|
73
73
|
mode (TaskValidationMode): Validation mode - INPUT for task content,
|
|
74
74
|
OUTPUT for task results. (default: :obj:`TaskValidationMode.INPUT`)
|
|
75
75
|
check_failure_patterns (bool): Whether to check for failure indicators
|
camel/toolkits/__init__.py
CHANGED
|
@@ -77,7 +77,7 @@ from .aci_toolkit import ACIToolkit
|
|
|
77
77
|
from .playwright_mcp_toolkit import PlaywrightMCPToolkit
|
|
78
78
|
from .wolfram_alpha_toolkit import WolframAlphaToolkit
|
|
79
79
|
from .task_planning_toolkit import TaskPlanningToolkit
|
|
80
|
-
from .
|
|
80
|
+
from .hybrid_browser_toolkit import HybridBrowserToolkit
|
|
81
81
|
from .edgeone_pages_mcp_toolkit import EdgeOnePagesMCPToolkit
|
|
82
82
|
from .google_drive_mcp_toolkit import GoogleDriveMCPToolkit
|
|
83
83
|
from .craw4ai_toolkit import Crawl4AIToolkit
|
|
@@ -146,7 +146,7 @@ __all__ = [
|
|
|
146
146
|
'WolframAlphaToolkit',
|
|
147
147
|
'BohriumToolkit',
|
|
148
148
|
'TaskPlanningToolkit',
|
|
149
|
-
'
|
|
149
|
+
'HybridBrowserToolkit',
|
|
150
150
|
'EdgeOnePagesMCPToolkit',
|
|
151
151
|
'GoogleDriveMCPToolkit',
|
|
152
152
|
'Crawl4AIToolkit',
|
|
@@ -31,6 +31,16 @@ class Crawl4AIToolkit(BaseToolkit):
|
|
|
31
31
|
timeout: Optional[float] = None,
|
|
32
32
|
):
|
|
33
33
|
super().__init__(timeout=timeout)
|
|
34
|
+
self._client = None
|
|
35
|
+
|
|
36
|
+
async def _get_client(self):
|
|
37
|
+
r"""Get or create the AsyncWebCrawler client."""
|
|
38
|
+
if self._client is None:
|
|
39
|
+
from crawl4ai import AsyncWebCrawler
|
|
40
|
+
|
|
41
|
+
self._client = AsyncWebCrawler()
|
|
42
|
+
await self._client.__aenter__()
|
|
43
|
+
return self._client
|
|
34
44
|
|
|
35
45
|
async def scrape(self, url: str) -> str:
|
|
36
46
|
r"""Scrapes a webpage and returns its content.
|
|
@@ -47,19 +57,29 @@ class Crawl4AIToolkit(BaseToolkit):
|
|
|
47
57
|
str: The scraped content of the webpage as a string. If the
|
|
48
58
|
scraping fails, it will return an error message.
|
|
49
59
|
"""
|
|
50
|
-
from crawl4ai import
|
|
60
|
+
from crawl4ai import CrawlerRunConfig
|
|
51
61
|
|
|
52
62
|
try:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
client = await self._get_client()
|
|
64
|
+
config = CrawlerRunConfig(
|
|
65
|
+
only_text=True,
|
|
66
|
+
)
|
|
67
|
+
content = await client.arun(url, crawler_config=config)
|
|
68
|
+
return str(content.markdown) if content.markdown else ""
|
|
59
69
|
except Exception as e:
|
|
60
70
|
logger.error(f"Error scraping {url}: {e}")
|
|
61
71
|
return f"Error scraping {url}: {e}"
|
|
62
72
|
|
|
73
|
+
async def __aenter__(self):
|
|
74
|
+
"""Async context manager entry."""
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
78
|
+
"""Async context manager exit - cleanup the client."""
|
|
79
|
+
if self._client is not None:
|
|
80
|
+
await self._client.__aexit__(exc_type, exc_val, exc_tb)
|
|
81
|
+
self._client = None
|
|
82
|
+
|
|
63
83
|
def get_tools(self) -> List[FunctionTool]:
|
|
64
84
|
r"""Returns a list of FunctionTool objects representing the
|
|
65
85
|
functions in the toolkit.
|