camel-ai 0.2.71a2__py3-none-any.whl → 0.2.71a4__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 +297 -16
- camel/interpreters/docker_interpreter.py +3 -2
- camel/loaders/base_loader.py +85 -0
- camel/messages/base.py +2 -6
- camel/services/agent_openapi_server.py +380 -0
- camel/societies/workforce/workforce.py +144 -33
- camel/toolkits/__init__.py +7 -4
- camel/toolkits/craw4ai_toolkit.py +2 -2
- camel/toolkits/file_write_toolkit.py +6 -6
- 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 +1008 -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} +202 -23
- camel/toolkits/note_taking_toolkit.py +90 -0
- camel/toolkits/openai_image_toolkit.py +292 -0
- camel/toolkits/slack_toolkit.py +4 -4
- camel/toolkits/terminal_toolkit.py +223 -73
- camel/types/agents/tool_calling_record.py +4 -1
- camel/types/enums.py +24 -24
- camel/utils/mcp_client.py +37 -1
- camel/utils/tool_result.py +44 -0
- {camel_ai-0.2.71a2.dist-info → camel_ai-0.2.71a4.dist-info}/METADATA +58 -5
- {camel_ai-0.2.71a2.dist-info → camel_ai-0.2.71a4.dist-info}/RECORD +30 -26
- camel/toolkits/dalle_toolkit.py +0 -175
- camel/toolkits/non_visual_browser_toolkit/browser_non_visual_toolkit.py +0 -446
- {camel_ai-0.2.71a2.dist-info → camel_ai-0.2.71a4.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a2.dist-info → camel_ai-0.2.71a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,24 +12,24 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
import json
|
|
15
|
-
import logging
|
|
16
15
|
import re
|
|
17
16
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
18
17
|
|
|
18
|
+
from camel.logger import get_logger
|
|
19
19
|
from camel.models import BaseModelBackend, ModelFactory
|
|
20
20
|
from camel.types import ModelPlatformType, ModelType
|
|
21
21
|
|
|
22
22
|
from .actions import ActionExecutor
|
|
23
|
-
from .
|
|
23
|
+
from .browser_session import NVBrowserSession
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from camel.agents import ChatAgent
|
|
27
27
|
|
|
28
|
-
logger =
|
|
28
|
+
logger = get_logger(__name__)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class PlaywrightLLMAgent:
|
|
32
|
-
"""High-level orchestration: snapshot ↔ LLM ↔ action executor."""
|
|
32
|
+
r"""High-level orchestration: snapshot ↔ LLM ↔ action executor."""
|
|
33
33
|
|
|
34
34
|
# System prompt as class constant to avoid recreation
|
|
35
35
|
SYSTEM_PROMPT = """
|
|
@@ -90,8 +90,8 @@ what was accomplished
|
|
|
90
90
|
self.action_history: List[Dict[str, Any]] = []
|
|
91
91
|
if model_backend is None:
|
|
92
92
|
model_backend = ModelFactory.create(
|
|
93
|
-
model_platform=ModelPlatformType.
|
|
94
|
-
model_type=ModelType.
|
|
93
|
+
model_platform=ModelPlatformType.DEFAULT,
|
|
94
|
+
model_type=ModelType.DEFAULT,
|
|
95
95
|
model_config_dict={"temperature": 0, "top_p": 1},
|
|
96
96
|
)
|
|
97
97
|
self.model_backend = model_backend
|
|
@@ -99,16 +99,19 @@ what was accomplished
|
|
|
99
99
|
self._chat_agent: Optional[ChatAgent] = None
|
|
100
100
|
|
|
101
101
|
async def navigate(self, url: str) -> str:
|
|
102
|
+
r"""Navigate to a URL and return the snapshot."""
|
|
102
103
|
try:
|
|
103
104
|
# NVBrowserSession handles waits internally
|
|
104
105
|
logger.debug("Navigated to URL: %s", url)
|
|
105
106
|
await self._session.visit(url)
|
|
106
107
|
return await self._session.get_snapshot(force_refresh=True)
|
|
107
108
|
except Exception as exc:
|
|
108
|
-
|
|
109
|
+
error_msg = f"Error: could not navigate to {url} - {exc}"
|
|
110
|
+
logger.error(error_msg)
|
|
111
|
+
return error_msg
|
|
109
112
|
|
|
110
113
|
def _get_chat_agent(self) -> "ChatAgent":
|
|
111
|
-
"""Get or create the ChatAgent instance."""
|
|
114
|
+
r"""Get or create the ChatAgent instance."""
|
|
112
115
|
from camel.agents import ChatAgent
|
|
113
116
|
|
|
114
117
|
if self._chat_agent is None:
|
|
@@ -165,12 +168,16 @@ what was accomplished
|
|
|
165
168
|
logger.warning(
|
|
166
169
|
"Could not parse JSON from LLM response: %s", content[:200]
|
|
167
170
|
)
|
|
171
|
+
return self._get_fallback_response("Parsing error")
|
|
172
|
+
|
|
173
|
+
def _get_fallback_response(self, error_msg: str) -> Dict[str, Any]:
|
|
174
|
+
r"""Generate a fallback response structure."""
|
|
168
175
|
return {
|
|
169
|
-
"plan": ["Could not parse response"],
|
|
176
|
+
"plan": [f"Could not parse response: {error_msg}"],
|
|
170
177
|
"action": {
|
|
171
178
|
"type": "finish",
|
|
172
179
|
"ref": None,
|
|
173
|
-
"summary": "Parsing error",
|
|
180
|
+
"summary": f"Parsing error: {error_msg}",
|
|
174
181
|
},
|
|
175
182
|
}
|
|
176
183
|
|
|
@@ -181,7 +188,7 @@ what was accomplished
|
|
|
181
188
|
is_initial: bool,
|
|
182
189
|
history: Optional[List[Dict[str, Any]]] = None,
|
|
183
190
|
) -> Dict[str, Any]:
|
|
184
|
-
"""Call the LLM (via CAMEL ChatAgent) to get plan & next action."""
|
|
191
|
+
r"""Call the LLM (via CAMEL ChatAgent) to get plan & next action."""
|
|
185
192
|
# Build user message
|
|
186
193
|
if is_initial:
|
|
187
194
|
user_content = f"Snapshot:\n{snapshot}\n\nTask: {prompt}"
|
|
@@ -208,6 +215,7 @@ what was accomplished
|
|
|
208
215
|
return self._safe_parse_json(content)
|
|
209
216
|
|
|
210
217
|
async def process_command(self, prompt: str, max_steps: int = 15):
|
|
218
|
+
r"""Process a command using LLM-guided browser automation."""
|
|
211
219
|
# initial full snapshot
|
|
212
220
|
full_snapshot = await self._session.get_snapshot()
|
|
213
221
|
assert self._session.snapshot is not None
|
|
@@ -270,9 +278,11 @@ what was accomplished
|
|
|
270
278
|
logger.info("Process completed with %d steps", steps)
|
|
271
279
|
|
|
272
280
|
async def _run_action(self, action: Dict[str, Any]) -> str:
|
|
281
|
+
r"""Execute a single action and return the result."""
|
|
273
282
|
if action.get("type") == "navigate":
|
|
274
283
|
return await self.navigate(action.get("url", ""))
|
|
275
284
|
return await self._session.exec_action(action)
|
|
276
285
|
|
|
277
286
|
async def close(self):
|
|
287
|
+
r"""Clean up browser session and resources."""
|
|
278
288
|
await self._session.close()
|
|
@@ -57,13 +57,12 @@ class NVBrowserSession:
|
|
|
57
57
|
|
|
58
58
|
def __new__(
|
|
59
59
|
cls, *, headless: bool = True, user_data_dir: Optional[str] = None
|
|
60
|
-
):
|
|
61
|
-
loop
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return cls._sessions[loop]
|
|
60
|
+
) -> "NVBrowserSession":
|
|
61
|
+
# Defer event loop lookup until we actually need it
|
|
62
|
+
# This allows creation outside of async context
|
|
63
|
+
instance = super().__new__(cls)
|
|
64
|
+
instance._initialized = False
|
|
65
|
+
return instance
|
|
67
66
|
|
|
68
67
|
def __init__(
|
|
69
68
|
self, *, headless: bool = True, user_data_dir: Optional[str] = None
|
|
@@ -90,6 +89,47 @@ class NVBrowserSession:
|
|
|
90
89
|
# Browser lifecycle helpers
|
|
91
90
|
# ------------------------------------------------------------------
|
|
92
91
|
async def ensure_browser(self) -> None:
|
|
92
|
+
r"""Ensure browser is ready, implementing singleton pattern per event
|
|
93
|
+
loop.
|
|
94
|
+
"""
|
|
95
|
+
# Check if we need to reuse or create a session for this event loop
|
|
96
|
+
try:
|
|
97
|
+
loop = asyncio.get_running_loop()
|
|
98
|
+
except RuntimeError as e:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
"ensure_browser() must be called from within an async context"
|
|
101
|
+
) from e
|
|
102
|
+
|
|
103
|
+
# Check if there's already a session for this loop
|
|
104
|
+
if loop in self._sessions and self._sessions[loop] is not self:
|
|
105
|
+
# Copy the existing session's browser resources
|
|
106
|
+
existing = self._sessions[loop]
|
|
107
|
+
# Wait for existing session to be fully initialized
|
|
108
|
+
async with existing._ensure_lock:
|
|
109
|
+
if (
|
|
110
|
+
existing._initialized
|
|
111
|
+
and existing._page is not None
|
|
112
|
+
and existing._playwright is not None
|
|
113
|
+
):
|
|
114
|
+
try:
|
|
115
|
+
# Verify the page is still responsive
|
|
116
|
+
await existing._page.title()
|
|
117
|
+
self._playwright = existing._playwright
|
|
118
|
+
self._browser = existing._browser
|
|
119
|
+
self._context = existing._context
|
|
120
|
+
self._page = existing._page
|
|
121
|
+
self.snapshot = existing.snapshot
|
|
122
|
+
self.executor = existing.executor
|
|
123
|
+
self._initialized = True
|
|
124
|
+
return
|
|
125
|
+
except Exception:
|
|
126
|
+
# Existing session is broken, continue with new
|
|
127
|
+
# initialization
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
# Register this instance for the current loop
|
|
131
|
+
self._sessions[loop] = self
|
|
132
|
+
|
|
93
133
|
# Serialise initialisation to avoid race conditions where multiple
|
|
94
134
|
# concurrent coroutine calls create multiple browser instances for
|
|
95
135
|
# the same NVBrowserSession.
|
|
@@ -98,6 +138,7 @@ class NVBrowserSession:
|
|
|
98
138
|
|
|
99
139
|
# Moved original logic to helper
|
|
100
140
|
async def _ensure_browser_inner(self) -> None:
|
|
141
|
+
r"""Internal browser initialization logic."""
|
|
101
142
|
from playwright.async_api import async_playwright
|
|
102
143
|
|
|
103
144
|
if self._page is not None:
|
|
@@ -144,11 +185,23 @@ class NVBrowserSession:
|
|
|
144
185
|
r"""Close all browser resources, ensuring cleanup even if some
|
|
145
186
|
operations fail.
|
|
146
187
|
"""
|
|
147
|
-
#
|
|
148
|
-
|
|
188
|
+
# Remove this session from the sessions dict and close resources
|
|
189
|
+
try:
|
|
190
|
+
loop = asyncio.get_running_loop()
|
|
191
|
+
if loop in self._sessions and self._sessions[loop] is self:
|
|
192
|
+
del self._sessions[loop]
|
|
193
|
+
except RuntimeError:
|
|
194
|
+
pass # No running loop, that's okay
|
|
195
|
+
|
|
196
|
+
# Clean up any stale loop references
|
|
197
|
+
stale_loops = [loop for loop in self._sessions if loop.is_closed()]
|
|
198
|
+
for loop in stale_loops:
|
|
199
|
+
del self._sessions[loop]
|
|
200
|
+
|
|
149
201
|
await self._close_session()
|
|
150
202
|
|
|
151
203
|
async def _close_session(self) -> None:
|
|
204
|
+
r"""Internal session cleanup with comprehensive error handling."""
|
|
152
205
|
errors: list[str] = []
|
|
153
206
|
|
|
154
207
|
# Close context first (which closes pages)
|
|
@@ -204,6 +257,7 @@ class NVBrowserSession:
|
|
|
204
257
|
# Convenience wrappers around common actions
|
|
205
258
|
# ------------------------------------------------------------------
|
|
206
259
|
async def visit(self, url: str) -> str:
|
|
260
|
+
r"""Navigate to a URL with proper error handling."""
|
|
207
261
|
await self.ensure_browser()
|
|
208
262
|
assert self._page is not None
|
|
209
263
|
|
|
@@ -233,7 +287,7 @@ class NVBrowserSession:
|
|
|
233
287
|
force_refresh=force_refresh, diff_only=diff_only
|
|
234
288
|
)
|
|
235
289
|
|
|
236
|
-
async def exec_action(self, action:
|
|
290
|
+
async def exec_action(self, action: Dict[str, Any]) -> str:
|
|
237
291
|
await self.ensure_browser()
|
|
238
292
|
assert self.executor is not None
|
|
239
293
|
return await self.executor.execute(action)
|