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.

Files changed (32) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/_types.py +6 -2
  3. camel/agents/chat_agent.py +297 -16
  4. camel/interpreters/docker_interpreter.py +3 -2
  5. camel/loaders/base_loader.py +85 -0
  6. camel/messages/base.py +2 -6
  7. camel/services/agent_openapi_server.py +380 -0
  8. camel/societies/workforce/workforce.py +144 -33
  9. camel/toolkits/__init__.py +7 -4
  10. camel/toolkits/craw4ai_toolkit.py +2 -2
  11. camel/toolkits/file_write_toolkit.py +6 -6
  12. camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/__init__.py +2 -2
  13. camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/actions.py +47 -11
  14. camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/agent.py +21 -11
  15. camel/toolkits/{non_visual_browser_toolkit/nv_browser_session.py → hybrid_browser_toolkit/browser_session.py} +64 -10
  16. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +1008 -0
  17. camel/toolkits/{non_visual_browser_toolkit → hybrid_browser_toolkit}/snapshot.py +16 -4
  18. camel/toolkits/{non_visual_browser_toolkit/snapshot.js → hybrid_browser_toolkit/unified_analyzer.js} +202 -23
  19. camel/toolkits/note_taking_toolkit.py +90 -0
  20. camel/toolkits/openai_image_toolkit.py +292 -0
  21. camel/toolkits/slack_toolkit.py +4 -4
  22. camel/toolkits/terminal_toolkit.py +223 -73
  23. camel/types/agents/tool_calling_record.py +4 -1
  24. camel/types/enums.py +24 -24
  25. camel/utils/mcp_client.py +37 -1
  26. camel/utils/tool_result.py +44 -0
  27. {camel_ai-0.2.71a2.dist-info → camel_ai-0.2.71a4.dist-info}/METADATA +58 -5
  28. {camel_ai-0.2.71a2.dist-info → camel_ai-0.2.71a4.dist-info}/RECORD +30 -26
  29. camel/toolkits/dalle_toolkit.py +0 -175
  30. camel/toolkits/non_visual_browser_toolkit/browser_non_visual_toolkit.py +0 -446
  31. {camel_ai-0.2.71a2.dist-info → camel_ai-0.2.71a4.dist-info}/WHEEL +0 -0
  32. {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 .nv_browser_session import NVBrowserSession
23
+ from .browser_session import NVBrowserSession
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from camel.agents import ChatAgent
27
27
 
28
- logger = logging.getLogger(__name__)
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.OPENAI,
94
- model_type=ModelType.GPT_4O_MINI,
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
- return f"Error: could not navigate - {exc}"
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 = asyncio.get_running_loop()
62
- if loop not in cls._sessions:
63
- instance = super().__new__(cls)
64
- instance._initialized = False
65
- cls._sessions[loop] = instance
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
- # The close method will now only close the *current* event-loop's
148
- # browser instance. Use `close_all_sessions` for a full cleanup.
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: dict[str, Any]) -> str:
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)