camel-ai 0.2.75a3__py3-none-any.whl → 0.2.75a5__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 CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  from camel.logger import disable_logging, enable_logging, set_log_level
16
16
 
17
- __version__ = '0.2.75a3'
17
+ __version__ = '0.2.75a5'
18
18
 
19
19
  __all__ = [
20
20
  '__version__',
@@ -674,12 +674,29 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
674
674
  "total_tabs": 0,
675
675
  }
676
676
 
677
- async def browser_type(self, *, ref: str, text: str) -> Dict[str, Any]:
678
- r"""Types text into an input element on the page.
677
+ async def browser_type(
678
+ self,
679
+ *,
680
+ ref: Optional[str] = None,
681
+ text: Optional[str] = None,
682
+ inputs: Optional[List[Dict[str, str]]] = None,
683
+ ) -> Dict[str, Any]:
684
+ r"""Types text into one or more input elements on the page.
685
+
686
+ This method supports two modes:
687
+ 1. Single input mode (backward compatible): Provide 'ref' and 'text'
688
+ 2. Multiple inputs mode: Provide 'inputs' as a list of dictionaries
689
+ with 'ref' and 'text' keys
679
690
 
680
691
  Args:
681
- ref (str): The `ref` ID of the input element, from a snapshot.
682
- text (str): The text to type into the element.
692
+ ref (Optional[str]): The `ref` ID of the input element, from a
693
+ snapshot. Required when using single input mode.
694
+ text (Optional[str]): The text to type into the element. Required
695
+ when using single input mode.
696
+ inputs (Optional[List[Dict[str, str]]]): List of dictionaries,
697
+ each containing 'ref' and 'text' keys for typing into multiple
698
+ elements. Example: [{'ref': '1', 'text': 'username'},
699
+ {'ref': '2', 'text': 'password'}]
683
700
 
684
701
  Returns:
685
702
  Dict[str, Any]: A dictionary with the result of the action:
@@ -689,10 +706,23 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
689
706
  - "tabs" (List[Dict]): Information about all open tabs.
690
707
  - "current_tab" (int): Index of the active tab.
691
708
  - "total_tabs" (int): Total number of open tabs.
709
+ - "details" (Dict[str, Any]): When using multiple inputs,
710
+ contains success/error status for each ref.
692
711
  """
693
712
  try:
694
713
  ws_wrapper = await self._get_ws_wrapper()
695
- result = await ws_wrapper.type(ref, text)
714
+
715
+ # Handle single input mode (backward compatibility)
716
+ if ref is not None and text is not None:
717
+ result = await ws_wrapper.type(ref, text)
718
+ # Handle multiple inputs mode
719
+ elif inputs is not None:
720
+ result = await ws_wrapper.type_multiple(inputs)
721
+ else:
722
+ raise ValueError(
723
+ "Either provide 'ref' and 'text' for single input, "
724
+ "or 'inputs' for multiple inputs"
725
+ )
696
726
 
697
727
  # Add tab information
698
728
  tab_info = await ws_wrapper.get_tab_info()
@@ -72,21 +72,19 @@ export class HybridBrowserSession {
72
72
  this.context = await this.browser.newContext(contextOptions);
73
73
  }
74
74
 
75
- // Handle existing pages
76
75
  const pages = this.context.pages();
77
76
  if (pages.length > 0) {
78
- // Map existing pages - for CDP, only use pages with about:blank URL
77
+ // Map existing pages - for CDP, find ONE available blank page
79
78
  let availablePageFound = false;
80
79
  for (const page of pages) {
81
80
  const pageUrl = page.url();
82
- // In CDP mode, only consider pages with about:blank as available
83
- if (pageUrl === 'about:blank') {
81
+ if (this.isBlankPageUrl(pageUrl)) {
84
82
  const tabId = this.generateTabId();
85
83
  this.registerNewPage(tabId, page);
86
- if (!this.currentTabId) {
87
- this.currentTabId = tabId;
88
- availablePageFound = true;
89
- }
84
+ this.currentTabId = tabId;
85
+ availablePageFound = true;
86
+ console.log(`[CDP] Registered blank page as initial tab: ${tabId}, URL: ${pageUrl}`);
87
+ break; // Only register ONE page initially
90
88
  }
91
89
  }
92
90
 
@@ -157,8 +155,45 @@ export class HybridBrowserSession {
157
155
  return `${browserConfig.tabIdPrefix}${String(++this.tabCounter).padStart(browserConfig.tabCounterPadding, '0')}`;
158
156
  }
159
157
 
158
+ private isBlankPageUrl(url: string): boolean {
159
+ // Unified blank page detection logic used across the codebase
160
+ const browserConfig = this.configLoader.getBrowserConfig();
161
+ return (
162
+ // Standard about:blank variations (prefix match for query params)
163
+ url === 'about:blank' ||
164
+ url.startsWith('about:blank?') ||
165
+ // Configured blank page URLs (exact match for compatibility)
166
+ browserConfig.blankPageUrls.includes(url) ||
167
+ // Empty URL
168
+ url === '' ||
169
+ // Data URLs (often used for blank pages)
170
+ url.startsWith(browserConfig.dataUrlPrefix || 'data:')
171
+ );
172
+ }
173
+
160
174
  async getCurrentPage(): Promise<Page> {
161
175
  if (!this.currentTabId || !this.pages.has(this.currentTabId)) {
176
+ // In CDP mode, try to create a new page if none exists
177
+ const browserConfig = this.configLoader.getBrowserConfig();
178
+ if (browserConfig.connectOverCdp && this.context) {
179
+ console.log('[CDP] No active page found, attempting to create new page...');
180
+ try {
181
+ const newPage = await this.context.newPage();
182
+ const newTabId = this.generateTabId();
183
+ this.registerNewPage(newTabId, newPage);
184
+ this.currentTabId = newTabId;
185
+
186
+ // Set page timeouts
187
+ newPage.setDefaultNavigationTimeout(browserConfig.navigationTimeout);
188
+ newPage.setDefaultTimeout(browserConfig.navigationTimeout);
189
+
190
+ console.log(`[CDP] Created new page with tab ID: ${newTabId}`);
191
+ return newPage;
192
+ } catch (error) {
193
+ console.error('[CDP] Failed to create new page:', error);
194
+ throw new Error('No active page available and failed to create new page in CDP mode');
195
+ }
196
+ }
162
197
  throw new Error('No active page available');
163
198
  }
164
199
  return this.pages.get(this.currentTabId)!;
@@ -416,25 +451,67 @@ export class HybridBrowserSession {
416
451
 
417
452
  /**
418
453
  * Simplified type implementation using Playwright's aria-ref selector
454
+ * Supports both single and multiple input operations
419
455
  */
420
- private async performType(page: Page, ref: string, text: string): Promise<{ success: boolean; error?: string }> {
456
+ private async performType(page: Page, ref: string | undefined, text: string | undefined, inputs?: Array<{ ref: string; text: string }>): Promise<{ success: boolean; error?: string; details?: Record<string, any> }> {
421
457
  try {
422
458
  // Ensure we have the latest snapshot
423
459
  await (page as any)._snapshotForAI();
424
460
 
425
- // Use Playwright's aria-ref selector
426
- const selector = `aria-ref=${ref}`;
427
- const element = await page.locator(selector).first();
428
-
429
- const exists = await element.count() > 0;
430
- if (!exists) {
431
- return { success: false, error: `Element with ref ${ref} not found` };
461
+ // Handle multiple inputs if provided
462
+ if (inputs && inputs.length > 0) {
463
+ const results: Record<string, { success: boolean; error?: string }> = {};
464
+
465
+ for (const input of inputs) {
466
+ const selector = `aria-ref=${input.ref}`;
467
+ const element = await page.locator(selector).first();
468
+
469
+ const exists = await element.count() > 0;
470
+ if (!exists) {
471
+ results[input.ref] = { success: false, error: `Element with ref ${input.ref} not found` };
472
+ continue;
473
+ }
474
+
475
+ try {
476
+ // Type text using Playwright's built-in fill method
477
+ await element.fill(input.text);
478
+ results[input.ref] = { success: true };
479
+ } catch (error) {
480
+ results[input.ref] = { success: false, error: `Type failed: ${error}` };
481
+ }
482
+ }
483
+
484
+ // Check if all inputs were successful
485
+ const allSuccess = Object.values(results).every(r => r.success);
486
+ const errors = Object.entries(results)
487
+ .filter(([_, r]) => !r.success)
488
+ .map(([ref, r]) => `${ref}: ${r.error}`)
489
+ .join('; ');
490
+
491
+ return {
492
+ success: allSuccess,
493
+ error: allSuccess ? undefined : `Some inputs failed: ${errors}`,
494
+ details: results
495
+ };
432
496
  }
433
497
 
434
- // Type text using Playwright's built-in fill method
435
- await element.fill(text);
498
+ // Handle single input (backward compatibility)
499
+ if (ref && text !== undefined) {
500
+ const selector = `aria-ref=${ref}`;
501
+ const element = await page.locator(selector).first();
502
+
503
+ const exists = await element.count() > 0;
504
+ if (!exists) {
505
+ return { success: false, error: `Element with ref ${ref} not found` };
506
+ }
507
+
508
+ // Type text using Playwright's built-in fill method
509
+ await element.fill(text);
510
+
511
+ return { success: true };
512
+ }
436
513
 
437
- return { success: true };
514
+ return { success: false, error: 'No valid input provided' };
438
515
  } catch (error) {
439
516
  return { success: false, error: `Type failed: ${error}` };
440
517
  }
@@ -572,6 +649,8 @@ export class HybridBrowserSession {
572
649
  // No need to pre-fetch snapshot - each action method handles this
573
650
 
574
651
  let newTabId: string | undefined;
652
+ let customMessage: string | undefined;
653
+ let actionDetails: Record<string, any> | undefined;
575
654
 
576
655
  switch (action.type) {
577
656
  case 'click': {
@@ -596,12 +675,20 @@ export class HybridBrowserSession {
596
675
  elementSearchTime = Date.now() - elementSearchStart;
597
676
  const typeStart = Date.now();
598
677
 
599
- const typeResult = await this.performType(page, action.ref, action.text);
678
+ const typeResult = await this.performType(page, action.ref, action.text, action.inputs);
600
679
 
601
680
  if (!typeResult.success) {
602
681
  throw new Error(`Type failed: ${typeResult.error}`);
603
682
  }
604
683
 
684
+ // Set custom message and details if multiple inputs were used
685
+ if (typeResult.details) {
686
+ const successCount = Object.values(typeResult.details).filter((r: any) => r.success).length;
687
+ const totalCount = Object.keys(typeResult.details).length;
688
+ customMessage = `Typed text into ${successCount}/${totalCount} elements`;
689
+ actionDetails = typeResult.details;
690
+ }
691
+
605
692
  actionExecutionTime = Date.now() - typeStart;
606
693
  break;
607
694
  }
@@ -689,7 +776,7 @@ export class HybridBrowserSession {
689
776
 
690
777
  return {
691
778
  success: true,
692
- message: `Action ${action.type} executed successfully`,
779
+ message: customMessage || `Action ${action.type} executed successfully`,
693
780
  timing: {
694
781
  total_time_ms: totalTime,
695
782
  element_search_time_ms: elementSearchTime,
@@ -699,6 +786,7 @@ export class HybridBrowserSession {
699
786
  network_idle_time_ms: stabilityResult.networkIdleTime,
700
787
  },
701
788
  ...(newTabId && { newTabId }), // Include new tab ID if present
789
+ ...(actionDetails && { details: actionDetails }), // Include action details if present
702
790
  };
703
791
  } catch (error) {
704
792
  const totalTime = Date.now() - startTime;
@@ -740,16 +828,23 @@ export class HybridBrowserSession {
740
828
 
741
829
  try {
742
830
  // Get current page to check if it's blank
743
- const currentPage = await this.getCurrentPage();
744
- const currentUrl = currentPage.url();
831
+ let currentPage: Page;
832
+ let currentUrl: string;
833
+
834
+ try {
835
+ currentPage = await this.getCurrentPage();
836
+ currentUrl = currentPage.url();
837
+ } catch (error: any) {
838
+ // If no active page is available, getCurrentPage() will create one in CDP mode
839
+ console.log('[visitPage] Failed to get current page:', error);
840
+ throw new Error(`No active page available: ${error?.message || error}`);
841
+ }
745
842
 
746
843
  // Check if current page is blank or if this is the first navigation
747
844
  const browserConfig = this.configLoader.getBrowserConfig();
748
- const isBlankPage = (
749
- browserConfig.blankPageUrls.includes(currentUrl) ||
750
- currentUrl === browserConfig.defaultStartUrl ||
751
- currentUrl.startsWith(browserConfig.dataUrlPrefix) // data URLs are often used for blank pages
752
- );
845
+
846
+ // Use unified blank page detection
847
+ const isBlankPage = this.isBlankPageUrl(currentUrl) || currentUrl === browserConfig.defaultStartUrl;
753
848
 
754
849
  const shouldUseCurrentTab = isBlankPage || !this.hasNavigatedBefore;
755
850
 
@@ -804,7 +899,7 @@ export class HybridBrowserSession {
804
899
  const pageUrl = page.url();
805
900
  // Check if this page is not already tracked and is blank
806
901
  const isTracked = Array.from(this.pages.values()).includes(page);
807
- if (!isTracked && pageUrl === 'about:blank') {
902
+ if (!isTracked && this.isBlankPageUrl(pageUrl)) {
808
903
  newPage = page;
809
904
  newTabId = this.generateTabId();
810
905
  this.registerNewPage(newTabId, newPage);
@@ -117,7 +117,7 @@ function getDefaultBrowserConfig(): BrowserConfig {
117
117
  consoleLogLimit: 1000,
118
118
  scrollPositionScale: 0.1,
119
119
  navigationDelay: 100,
120
- blankPageUrls: ['about:blank', ''],
120
+ blankPageUrls: [],
121
121
  dataUrlPrefix: 'data:',
122
122
  domContentLoadedState: 'domcontentloaded',
123
123
  networkIdleState: 'networkidle',
@@ -69,35 +69,52 @@ export class HybridBrowserToolkit {
69
69
  }
70
70
 
71
71
  async visitPage(url: string): Promise<any> {
72
- const result = await this.session.visitPage(url);
73
-
74
- // Format response for Python layer compatibility
75
- const response: any = {
76
- result: result.message,
77
- snapshot: '',
78
- };
79
-
80
- if (result.success) {
81
- const snapshotStart = Date.now();
82
- response.snapshot = await this.getPageSnapshot(this.viewportLimit);
83
- const snapshotTime = Date.now() - snapshotStart;
72
+ try {
73
+ // Ensure browser is initialized before visiting page
74
+ await this.session.ensureBrowser();
75
+
76
+ const result = await this.session.visitPage(url);
77
+
78
+ // Format response for Python layer compatibility
79
+ const response: any = {
80
+ result: result.message,
81
+ snapshot: '',
82
+ };
83
+
84
+ if (result.success) {
85
+ const snapshotStart = Date.now();
86
+ response.snapshot = await this.getPageSnapshot(this.viewportLimit);
87
+ const snapshotTime = Date.now() - snapshotStart;
88
+
89
+ if (result.timing) {
90
+ result.timing.snapshot_time_ms = snapshotTime;
91
+ }
92
+ }
84
93
 
94
+ // Include timing if available
85
95
  if (result.timing) {
86
- result.timing.snapshot_time_ms = snapshotTime;
96
+ response.timing = result.timing;
87
97
  }
98
+
99
+ // Include newTabId if present
100
+ if (result.newTabId) {
101
+ response.newTabId = result.newTabId;
102
+ }
103
+
104
+ return response;
105
+ } catch (error) {
106
+ console.error('[visitPage] Error:', error);
107
+ return {
108
+ result: `Navigation to ${url} failed: ${error}`,
109
+ snapshot: '',
110
+ timing: {
111
+ total_time_ms: 0,
112
+ navigation_time_ms: 0,
113
+ dom_content_loaded_time_ms: 0,
114
+ network_idle_time_ms: 0,
115
+ }
116
+ };
88
117
  }
89
-
90
- // Include timing if available
91
- if (result.timing) {
92
- response.timing = result.timing;
93
- }
94
-
95
- // Include newTabId if present
96
- if (result.newTabId) {
97
- response.newTabId = result.newTabId;
98
- }
99
-
100
- return response;
101
118
  }
102
119
 
103
120
  async getPageSnapshot(viewportLimit: boolean = false): Promise<string> {
@@ -179,7 +196,40 @@ export class HybridBrowserToolkit {
179
196
  // Use sharp for image processing
180
197
  const sharp = require('sharp');
181
198
  const page = await this.session.getCurrentPage();
182
- const viewport = page.viewportSize() || { width: 1280, height: 720 };
199
+ let viewport = page.viewportSize();
200
+
201
+ // In CDP mode, viewportSize might be null, get it from window dimensions
202
+ if (!viewport) {
203
+ const windowSize = await page.evaluate(() => ({
204
+ width: window.innerWidth,
205
+ height: window.innerHeight
206
+ }));
207
+ viewport = windowSize;
208
+ }
209
+
210
+ // Get device pixel ratio to handle high DPI screens
211
+ const dpr = await page.evaluate(() => window.devicePixelRatio) || 1;
212
+
213
+ // Get actual screenshot dimensions
214
+ const metadata = await sharp(screenshotBuffer).metadata();
215
+ const screenshotWidth = metadata.width || viewport.width;
216
+ const screenshotHeight = metadata.height || viewport.height;
217
+
218
+ // Calculate scaling factor between CSS pixels and screenshot pixels
219
+ const scaleX = screenshotWidth / viewport.width;
220
+ const scaleY = screenshotHeight / viewport.height;
221
+
222
+ // Debug logging for CDP mode
223
+ if (process.env.HYBRID_BROWSER_DEBUG === '1') {
224
+ console.log('[CDP Debug] Viewport size:', viewport);
225
+ console.log('[CDP Debug] Device pixel ratio:', dpr);
226
+ console.log('[CDP Debug] Screenshot dimensions:', { width: screenshotWidth, height: screenshotHeight });
227
+ console.log('[CDP Debug] Scale factors:', { scaleX, scaleY });
228
+ console.log('[CDP Debug] Elements with coordinates:', elementsWithCoords.length);
229
+ elementsWithCoords.slice(0, 3).forEach(([ref, element]) => {
230
+ console.log(`[CDP Debug] Element ${ref}:`, element.coordinates);
231
+ });
232
+ }
183
233
 
184
234
  // Filter elements visible in viewport
185
235
  const visibleElements = elementsWithCoords.filter(([ref, element]) => {
@@ -198,18 +248,19 @@ export class HybridBrowserToolkit {
198
248
  const coords = element.coordinates!;
199
249
  const isClickable = clickableElements.has(ref);
200
250
 
201
- // Use original coordinates for elements within viewport
202
- // Clamp only to prevent marks from extending beyond screenshot bounds
203
- const x = Math.max(0, coords.x);
204
- const y = Math.max(0, coords.y);
205
- const maxWidth = viewport.width - x;
206
- const maxHeight = viewport.height - y;
207
- const width = Math.min(coords.width, maxWidth);
208
- const height = Math.min(coords.height, maxHeight);
251
+ // Scale coordinates from CSS pixels to screenshot pixels
252
+ const x = Math.max(0, coords.x * scaleX);
253
+ const y = Math.max(0, coords.y * scaleY);
254
+ const width = coords.width * scaleX;
255
+ const height = coords.height * scaleY;
256
+
257
+ // Clamp to screenshot bounds
258
+ const clampedWidth = Math.min(width, screenshotWidth - x);
259
+ const clampedHeight = Math.min(height, screenshotHeight - y);
209
260
 
210
261
  // Position text to be visible even if element is partially cut off
211
- const textX = Math.max(2, Math.min(x + 2, viewport.width - 40));
212
- const textY = Math.max(14, Math.min(y + 14, viewport.height - 4));
262
+ const textX = Math.max(2, Math.min(x + 2, screenshotWidth - 40));
263
+ const textY = Math.max(14, Math.min(y + 14, screenshotHeight - 4));
213
264
 
214
265
  // Different colors for clickable vs non-clickable elements
215
266
  const colors = isClickable ? {
@@ -223,7 +274,7 @@ export class HybridBrowserToolkit {
223
274
  };
224
275
 
225
276
  return `
226
- <rect x="${x}" y="${y}" width="${width}" height="${height}"
277
+ <rect x="${x}" y="${y}" width="${clampedWidth}" height="${clampedHeight}"
227
278
  fill="${colors.fill}" stroke="${colors.stroke}" stroke-width="2" rx="2"/>
228
279
  <text x="${textX}" y="${textY}" font-family="Arial, sans-serif"
229
280
  font-size="12" fill="${colors.textFill}" font-weight="bold">${ref}</text>
@@ -231,7 +282,7 @@ export class HybridBrowserToolkit {
231
282
  }).join('');
232
283
 
233
284
  const svgOverlay = `
234
- <svg width="${viewport.width}" height="${viewport.height}" xmlns="http://www.w3.org/2000/svg">
285
+ <svg width="${screenshotWidth}" height="${screenshotHeight}" xmlns="http://www.w3.org/2000/svg">
235
286
  ${marks}
236
287
  </svg>
237
288
  `;
@@ -363,8 +414,20 @@ export class HybridBrowserToolkit {
363
414
  return this.executeActionWithSnapshot(action);
364
415
  }
365
416
 
366
- async type(ref: string, text: string): Promise<any> {
367
- const action: BrowserAction = { type: 'type', ref, text };
417
+ async type(refOrInputs: string | Array<{ ref: string; text: string }>, text?: string): Promise<any> {
418
+ let action: BrowserAction;
419
+
420
+ if (typeof refOrInputs === 'string') {
421
+ // Single input mode (backward compatibility)
422
+ if (text === undefined) {
423
+ throw new Error('Text parameter is required when ref is a string');
424
+ }
425
+ action = { type: 'type', ref: refOrInputs, text };
426
+ } else {
427
+ // Multiple inputs mode
428
+ action = { type: 'type', inputs: refOrInputs };
429
+ }
430
+
368
431
  return this.executeActionWithSnapshot(action);
369
432
  }
370
433
 
@@ -81,8 +81,9 @@ export interface ClickAction {
81
81
 
82
82
  export interface TypeAction {
83
83
  type: 'type';
84
- ref: string;
85
- text: string;
84
+ ref?: string; // Optional for backward compatibility
85
+ text?: string; // Optional for backward compatibility
86
+ inputs?: Array<{ ref: string; text: string }>; // New field for multiple inputs
86
87
  }
87
88
 
88
89
  export interface SelectAction {
@@ -160,7 +160,14 @@ class WebSocketBrowserServer {
160
160
 
161
161
  case 'type':
162
162
  if (!this.toolkit) throw new Error('Toolkit not initialized');
163
- return await this.toolkit.type(params.ref, params.text);
163
+ // Handle both single input and multiple inputs
164
+ if (params.inputs) {
165
+ // Multiple inputs mode - pass inputs array directly
166
+ return await this.toolkit.type(params.inputs);
167
+ } else {
168
+ // Single input mode - pass ref and text
169
+ return await this.toolkit.type(params.ref, params.text);
170
+ }
164
171
 
165
172
  case 'select':
166
173
  if (!this.toolkit) throw new Error('Toolkit not initialized');
@@ -396,6 +396,9 @@ class WebSocketBrowserWrapper:
396
396
  """Send a command to the WebSocket server and get response."""
397
397
  await self._ensure_connection()
398
398
 
399
+ # Process params to ensure refs have 'e' prefix
400
+ params = self._process_refs_in_params(params)
401
+
399
402
  message_id = str(uuid.uuid4())
400
403
  message = {'id': message_id, 'command': command, 'params': params}
401
404
 
@@ -503,6 +506,50 @@ class WebSocketBrowserWrapper:
503
506
 
504
507
  return ToolResult(text=response['text'], images=response['images'])
505
508
 
509
+ def _ensure_ref_prefix(self, ref: str) -> str:
510
+ """Ensure ref has 'e' prefix."""
511
+ if ref and not ref.startswith('e'):
512
+ return f'e{ref}'
513
+ return ref
514
+
515
+ def _process_refs_in_params(
516
+ self, params: Dict[str, Any]
517
+ ) -> Dict[str, Any]:
518
+ """Process parameters to ensure all refs have 'e' prefix."""
519
+ if not params:
520
+ return params
521
+
522
+ # Create a copy to avoid modifying the original
523
+ processed = params.copy()
524
+
525
+ # Handle direct ref parameters
526
+ if 'ref' in processed:
527
+ processed['ref'] = self._ensure_ref_prefix(processed['ref'])
528
+
529
+ # Handle from_ref and to_ref for drag operations
530
+ if 'from_ref' in processed:
531
+ processed['from_ref'] = self._ensure_ref_prefix(
532
+ processed['from_ref']
533
+ )
534
+ if 'to_ref' in processed:
535
+ processed['to_ref'] = self._ensure_ref_prefix(processed['to_ref'])
536
+
537
+ # Handle inputs array for type_multiple
538
+ if 'inputs' in processed and isinstance(processed['inputs'], list):
539
+ processed_inputs = []
540
+ for input_item in processed['inputs']:
541
+ if isinstance(input_item, dict) and 'ref' in input_item:
542
+ processed_input = input_item.copy()
543
+ processed_input['ref'] = self._ensure_ref_prefix(
544
+ input_item['ref']
545
+ )
546
+ processed_inputs.append(processed_input)
547
+ else:
548
+ processed_inputs.append(input_item)
549
+ processed['inputs'] = processed_inputs
550
+
551
+ return processed
552
+
506
553
  @action_logger
507
554
  async def click(self, ref: str) -> Dict[str, Any]:
508
555
  """Click an element."""
@@ -515,6 +562,14 @@ class WebSocketBrowserWrapper:
515
562
  response = await self._send_command('type', {'ref': ref, 'text': text})
516
563
  return response
517
564
 
565
+ @action_logger
566
+ async def type_multiple(
567
+ self, inputs: List[Dict[str, str]]
568
+ ) -> Dict[str, Any]:
569
+ """Type text into multiple elements."""
570
+ response = await self._send_command('type', {'inputs': inputs})
571
+ return response
572
+
518
573
  @action_logger
519
574
  async def select(self, ref: str, value: str) -> Dict[str, Any]:
520
575
  """Select an option."""
@@ -14,9 +14,8 @@
14
14
 
15
15
  import base64
16
16
  import os
17
- import uuid
18
17
  from io import BytesIO
19
- from typing import List, Literal, Optional
18
+ from typing import List, Literal, Optional, Union
20
19
 
21
20
  from openai import OpenAI
22
21
  from PIL import Image
@@ -64,13 +63,15 @@ class OpenAIImageToolkit(BaseToolkit):
64
63
  Literal["auto", "low", "medium", "high", "standard", "hd"]
65
64
  ] = "standard",
66
65
  response_format: Optional[Literal["url", "b64_json"]] = "b64_json",
67
- n: Optional[int] = 1,
68
66
  background: Optional[
69
67
  Literal["transparent", "opaque", "auto"]
70
68
  ] = "auto",
71
69
  style: Optional[Literal["vivid", "natural"]] = None,
72
70
  working_directory: Optional[str] = "image_save",
73
71
  ):
72
+ # NOTE: Some arguments are set in the constructor to prevent the agent
73
+ # from making invalid API calls with model-specific parameters. For
74
+ # example, the 'style' argument is only supported by 'dall-e-3'.
74
75
  r"""Initializes a new instance of the OpenAIImageToolkit class.
75
76
 
76
77
  Args:
@@ -94,8 +95,6 @@ class OpenAIImageToolkit(BaseToolkit):
94
95
  (default: :obj:`"standard"`)
95
96
  response_format (Optional[Literal["url", "b64_json"]]):
96
97
  The format of the response.(default: :obj:`"b64_json"`)
97
- n (Optional[int]): The number of images to generate.
98
- (default: :obj:`1`)
99
98
  background (Optional[Literal["transparent", "opaque", "auto"]]):
100
99
  The background of the image.(default: :obj:`"auto"`)
101
100
  style (Optional[Literal["vivid", "natural"]]): The style of the
@@ -111,7 +110,6 @@ class OpenAIImageToolkit(BaseToolkit):
111
110
  self.size = size
112
111
  self.quality = quality
113
112
  self.response_format = response_format
114
- self.n = n
115
113
  self.background = background
116
114
  self.style = style
117
115
  self.working_directory: str = working_directory or "image_save"
@@ -140,11 +138,12 @@ class OpenAIImageToolkit(BaseToolkit):
140
138
  )
141
139
  return None
142
140
 
143
- def _build_base_params(self, prompt: str) -> dict:
141
+ def _build_base_params(self, prompt: str, n: Optional[int] = None) -> dict:
144
142
  r"""Build base parameters dict for OpenAI API calls.
145
143
 
146
144
  Args:
147
145
  prompt (str): The text prompt for the image operation.
146
+ n (Optional[int]): The number of images to generate.
148
147
 
149
148
  Returns:
150
149
  dict: Parameters dictionary with non-None values.
@@ -152,8 +151,8 @@ class OpenAIImageToolkit(BaseToolkit):
152
151
  params = {"prompt": prompt, "model": self.model}
153
152
 
154
153
  # basic parameters supported by all models
155
- if self.n is not None:
156
- params["n"] = self.n # type: ignore[assignment]
154
+ if n is not None:
155
+ params["n"] = n # type: ignore[assignment]
157
156
  if self.size is not None:
158
157
  params["size"] = self.size
159
158
 
@@ -184,13 +183,16 @@ class OpenAIImageToolkit(BaseToolkit):
184
183
  return params
185
184
 
186
185
  def _handle_api_response(
187
- self, response, image_name: str, operation: str
186
+ self, response, image_name: Union[str, List[str]], operation: str
188
187
  ) -> str:
189
188
  r"""Handle API response from OpenAI image operations.
190
189
 
191
190
  Args:
192
191
  response: The response object from OpenAI API.
193
- image_name (str): Name for the saved image file.
192
+ image_name (Union[str, List[str]]): Name(s) for the saved image
193
+ file(s). If str, the same name is used for all images (will
194
+ cause error for multiple images). If list, must have exactly
195
+ the same length as the number of images generated.
194
196
  operation (str): Operation type for success message ("generated").
195
197
 
196
198
  Returns:
@@ -201,6 +203,21 @@ class OpenAIImageToolkit(BaseToolkit):
201
203
  logger.error(error_msg)
202
204
  return error_msg
203
205
 
206
+ # Validate image_name parameter
207
+ if isinstance(image_name, list):
208
+ if len(image_name) != len(response.data):
209
+ error_msg = (
210
+ f"Error: Number of image names"
211
+ f" ({len(image_name)}) does not match number of "
212
+ f"images generated({len(response.data)})"
213
+ )
214
+ logger.error(error_msg)
215
+ return error_msg
216
+ image_names = image_name
217
+ else:
218
+ # If string, use same name for all images
219
+ image_names = [image_name] * len(response.data)
220
+
204
221
  results = []
205
222
 
206
223
  for i, image_data in enumerate(response.data):
@@ -215,18 +232,27 @@ class OpenAIImageToolkit(BaseToolkit):
215
232
  image_bytes = base64.b64decode(image_b64)
216
233
  os.makedirs(self.working_directory, exist_ok=True)
217
234
 
218
- # Add index to filename when multiple images
219
- if len(response.data) > 1:
220
- filename = f"{image_name}_{i+1}_{uuid.uuid4().hex}.png"
221
- else:
222
- filename = f"{image_name}_{uuid.uuid4().hex}.png"
235
+ filename = f"{image_names[i]}"
223
236
 
224
237
  image_path = os.path.join(self.working_directory, filename)
225
238
 
226
- with open(image_path, "wb") as f:
227
- f.write(image_bytes)
228
-
229
- results.append(f"Image saved to {image_path}")
239
+ # Check if file already exists
240
+ if os.path.exists(image_path):
241
+ error_msg = (
242
+ f"Error: File '{image_path}' already exists. "
243
+ "Please use a different image_name."
244
+ )
245
+ logger.error(error_msg)
246
+ return error_msg
247
+
248
+ try:
249
+ with open(image_path, "wb") as f:
250
+ f.write(image_bytes)
251
+ results.append(f"Image saved to {image_path}")
252
+ except Exception as e:
253
+ error_msg = f"Error saving image to '{image_path}': {e!s}"
254
+ logger.error(error_msg)
255
+ return error_msg
230
256
  else:
231
257
  error_msg = (
232
258
  f"No valid image data (URL or base64) found in image {i+1}"
@@ -254,7 +280,8 @@ class OpenAIImageToolkit(BaseToolkit):
254
280
  def generate_image(
255
281
  self,
256
282
  prompt: str,
257
- image_name: str = "image",
283
+ image_name: Union[str, List[str]] = "image.png",
284
+ n: int = 1,
258
285
  ) -> str:
259
286
  r"""Generate an image using OpenAI's Image Generation models.
260
287
  The generated image will be saved locally (for ``b64_json`` response
@@ -263,14 +290,18 @@ class OpenAIImageToolkit(BaseToolkit):
263
290
 
264
291
  Args:
265
292
  prompt (str): The text prompt to generate the image.
266
- image_name (str): The name of the image to save.
267
- (default: :obj:`"image"`)
293
+ image_name (Union[str, List[str]]): The name(s) of the image(s) to
294
+ save. The image name must end with `.png`. If str: same name
295
+ used for all images (causes error if n > 1). If list: must
296
+ match the number of images being generated (n parameter).
297
+ (default: :obj:`"image.png"`)
298
+ n (int): The number of images to generate. (default: :obj:`1`)
268
299
 
269
300
  Returns:
270
301
  str: the content of the model response or format of the response.
271
302
  """
272
303
  try:
273
- params = self._build_base_params(prompt)
304
+ params = self._build_base_params(prompt, n)
274
305
  response = self.client.images.generate(**params)
275
306
  return self._handle_api_response(response, image_name, "generated")
276
307
  except Exception as e:
camel/types/enums.py CHANGED
@@ -213,8 +213,7 @@ class ModelType(UnifiedModelType, Enum):
213
213
  MISTRAL_MIXTRAL_8x22B = "open-mixtral-8x22b"
214
214
  MISTRAL_NEMO = "open-mistral-nemo"
215
215
  MISTRAL_PIXTRAL_12B = "pixtral-12b-2409"
216
- MISTRAL_MEDIUM_3 = "mistral-medium-latest"
217
- MAGISTRAL_MEDIUM = "magistral-medium-2506"
216
+ MISTRAL_MEDIUM_3_1 = "mistral-medium-2508"
218
217
  MISTRAL_SMALL_3_2 = "mistral-small-2506"
219
218
 
220
219
  # Reka models
@@ -663,8 +662,7 @@ class ModelType(UnifiedModelType, Enum):
663
662
  ModelType.MISTRAL_PIXTRAL_12B,
664
663
  ModelType.MISTRAL_8B,
665
664
  ModelType.MISTRAL_3B,
666
- ModelType.MISTRAL_MEDIUM_3,
667
- ModelType.MAGISTRAL_MEDIUM,
665
+ ModelType.MISTRAL_MEDIUM_3_1,
668
666
  ModelType.MISTRAL_SMALL_3_2,
669
667
  }
670
668
 
@@ -1225,7 +1223,7 @@ class ModelType(UnifiedModelType, Enum):
1225
1223
  ModelType.NETMIND_DEEPSEEK_R1,
1226
1224
  ModelType.NETMIND_DEEPSEEK_V3,
1227
1225
  ModelType.NOVITA_DEEPSEEK_V3_0324,
1228
- ModelType.MISTRAL_MEDIUM_3,
1226
+ ModelType.MISTRAL_MEDIUM_3_1,
1229
1227
  ModelType.ERNIE_4_5_TURBO_128K,
1230
1228
  ModelType.DEEPSEEK_V3,
1231
1229
  ModelType.MOONSHOT_KIMI_K2,
@@ -1336,10 +1334,6 @@ class ModelType(UnifiedModelType, Enum):
1336
1334
  ModelType.TOGETHER_LLAMA_4_SCOUT,
1337
1335
  }:
1338
1336
  return 10_000_000
1339
- elif self in {
1340
- ModelType.MAGISTRAL_MEDIUM,
1341
- }:
1342
- return 40_000
1343
1337
 
1344
1338
  else:
1345
1339
  logger.warning(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camel-ai
3
- Version: 0.2.75a3
3
+ Version: 0.2.75a5
4
4
  Summary: Communicative Agents for AI Society Study
5
5
  Project-URL: Homepage, https://www.camel-ai.org/
6
6
  Project-URL: Repository, https://github.com/camel-ai/camel
@@ -91,6 +91,7 @@ Requires-Dist: playwright>=1.50.0; extra == 'all'
91
91
  Requires-Dist: prance<24,>=23.6.21.0; extra == 'all'
92
92
  Requires-Dist: praw<8,>=7.7.1; extra == 'all'
93
93
  Requires-Dist: pre-commit<4,>=3; extra == 'all'
94
+ Requires-Dist: protobuf>=6.0.0; extra == 'all'
94
95
  Requires-Dist: psycopg[binary]<4,>=3.1.18; extra == 'all'
95
96
  Requires-Dist: pyautogui<0.10,>=0.9.54; extra == 'all'
96
97
  Requires-Dist: pydub<0.26,>=0.25.1; extra == 'all'
@@ -128,7 +129,6 @@ Requires-Dist: sympy<2,>=1.13.3; extra == 'all'
128
129
  Requires-Dist: tabulate>=0.9.0; extra == 'all'
129
130
  Requires-Dist: tavily-python<0.6,>=0.5.0; extra == 'all'
130
131
  Requires-Dist: textblob<0.18,>=0.17.1; extra == 'all'
131
- Requires-Dist: traceroot==0.0.4a5; extra == 'all'
132
132
  Requires-Dist: transformers<5,>=4; extra == 'all'
133
133
  Requires-Dist: tree-sitter-python<0.24,>=0.23.6; extra == 'all'
134
134
  Requires-Dist: tree-sitter<0.24,>=0.23.2; extra == 'all'
@@ -193,7 +193,6 @@ Requires-Dist: ipykernel<7,>=6.0.0; extra == 'dev-tools'
193
193
  Requires-Dist: jupyter-client<9,>=8.6.2; extra == 'dev-tools'
194
194
  Requires-Dist: langfuse>=2.60.5; extra == 'dev-tools'
195
195
  Requires-Dist: mcp>=1.3.0; extra == 'dev-tools'
196
- Requires-Dist: traceroot==0.0.4a5; extra == 'dev-tools'
197
196
  Requires-Dist: tree-sitter-python<0.24,>=0.23.6; extra == 'dev-tools'
198
197
  Requires-Dist: tree-sitter<0.24,>=0.23.2; extra == 'dev-tools'
199
198
  Requires-Dist: typer>=0.15.2; extra == 'dev-tools'
@@ -345,6 +344,7 @@ Requires-Dist: nebula3-python==3.8.2; extra == 'rag'
345
344
  Requires-Dist: neo4j<6,>=5.18.0; extra == 'rag'
346
345
  Requires-Dist: numpy<=2.2,>=1.2; extra == 'rag'
347
346
  Requires-Dist: pandasai<3,>=2.3.0; extra == 'rag'
347
+ Requires-Dist: protobuf>=6.0.0; extra == 'rag'
348
348
  Requires-Dist: pymilvus<3,>=2.4.0; extra == 'rag'
349
349
  Requires-Dist: pyobvector>=0.1.18; extra == 'rag'
350
350
  Requires-Dist: pytidb-experimental==0.0.1.dev4; extra == 'rag'
@@ -366,6 +366,7 @@ Requires-Dist: mem0ai>=0.1.73; extra == 'storage'
366
366
  Requires-Dist: nebula3-python==3.8.2; extra == 'storage'
367
367
  Requires-Dist: neo4j<6,>=5.18.0; extra == 'storage'
368
368
  Requires-Dist: pgvector<0.3,>=0.2.4; extra == 'storage'
369
+ Requires-Dist: protobuf>=6.0.0; extra == 'storage'
369
370
  Requires-Dist: psycopg[binary]<4,>=3.1.18; extra == 'storage'
370
371
  Requires-Dist: pymilvus<3,>=2.4.0; extra == 'storage'
371
372
  Requires-Dist: pyobvector>=0.1.18; extra == 'storage'
@@ -1,4 +1,4 @@
1
- camel/__init__.py,sha256=700Qx-U389P5R9yaCb-KQVk4xdMQWzsrmTGUuos90kc,901
1
+ camel/__init__.py,sha256=wTQgonEYCTyRP1M-YWHFakaFzw2aGRPRjVe8PVbM284,901
2
2
  camel/generators.py,sha256=JRqj9_m1PF4qT6UtybzTQ-KBT9MJQt18OAAYvQ_fr2o,13844
3
3
  camel/human.py,sha256=Xg8x1cS5KK4bQ1SDByiHZnzsRpvRP-KZViNvmu38xo4,5475
4
4
  camel/logger.py,sha256=WgEwael_eT6D-lVAKHpKIpwXSTjvLbny5jbV1Ab8lnA,5760
@@ -358,7 +358,7 @@ camel/toolkits/note_taking_toolkit.py,sha256=FWnkKdPyTENzHDPSFKo9zOrhhONfJkFpRvn
358
358
  camel/toolkits/notion_mcp_toolkit.py,sha256=ie_6Z-7DqDhgTiwYX8L3X47rfWGwzgwQH_s2DaK1ckc,8362
359
359
  camel/toolkits/notion_toolkit.py,sha256=jmmVWk_WazRNWnx4r9DAvhFTAL-n_ige0tb32UHJ_ik,9752
360
360
  camel/toolkits/open_api_toolkit.py,sha256=Venfq8JwTMQfzRzzB7AYmYUMEX35hW0BjIv_ozFMiNk,23316
361
- camel/toolkits/openai_image_toolkit.py,sha256=RDCPYnEPWibFmAgyQNUck0UWKguD7fni93lpMBMh1UQ,11180
361
+ camel/toolkits/openai_image_toolkit.py,sha256=6Hcadut2cwJrgh5RObtErYNzsl93gC28NoJTS-HvYw8,12930
362
362
  camel/toolkits/openbb_toolkit.py,sha256=8yBZL9E2iSgskosBQhD3pTP56oV6gerWpFjIJc_2UMo,28935
363
363
  camel/toolkits/origene_mcp_toolkit.py,sha256=og3H-F5kWRRIyOyhF7BR-ig_JIpZPKE0DCA1QJZw5BY,3354
364
364
  camel/toolkits/page_script.js,sha256=mXepZPnQNVLp_Wgb64lv7DMQIJYl_XIHJHLVt1OFZO4,13146
@@ -390,18 +390,18 @@ camel/toolkits/zapier_toolkit.py,sha256=A83y1UcfuopH7Fx82pORzypl1StbhBjB2HhyOqYa
390
390
  camel/toolkits/hybrid_browser_toolkit/__init__.py,sha256=vxjWhq7GjUKE5I9RGQU_GoikZJ-AVK4ertdvEqp9pd0,802
391
391
  camel/toolkits/hybrid_browser_toolkit/config_loader.py,sha256=yUmbmt-TWLbCyxGa63lVJ47KRdZsEJI6-2y3x0SfdBM,7040
392
392
  camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py,sha256=gotOOlXJjfjv9Qnn89PLNhJ4_Rw_aMMU6gTJcG-uCf8,7938
393
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py,sha256=OXvXTRJMqS4tQqrOeNbK4EVv3meO8I8bfz_3azSJFZA,53547
394
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py,sha256=UJFxHuGBJR3C9UmtWoC_F2bm_Uwnf9EurhkSseAqrBI,21888
393
+ camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py,sha256=f1m0cBvQCM5XlgPKWLSUGAlTqRqlI1ZKYs3KcrLqJ-w,54919
394
+ camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py,sha256=Cx3DpZctlwNlSoNa4sa3DOky_nPUKId98jCq63uXNgc,23903
395
395
  camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json,sha256=_-YE9S_C1XT59A6upQp9lLuZcC67cV9QlbwAsEKkfyw,156337
396
396
  camel/toolkits/hybrid_browser_toolkit/ts/package.json,sha256=pUQm0xwXR7ZyWNv6O2QtHW00agnfAoX9F_XGXZlAxl4,745
397
397
  camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json,sha256=SwpQnq4Q-rwRobF2iWrP96mgmgwaVPZEv-nii5QIYEU,523
398
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js,sha256=qj8Rr7bl17yciJ0ndB-rVsm25LxVUk3Mk_Kft_Z2p2o,9387
398
+ camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js,sha256=NeBhFUQl6dOyDeeh0c8LZ7f3qZssMHIrqJSP1mZEeiE,9672
399
399
  camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js,sha256=NNwM_H2xaDrlrdac0PJK1iUBwdiuQsg9qKaMhHAvZuI,3160
400
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts,sha256=e-epyloFdIOHWKXL-mTeeHxmRssMHs2E7mP9qRzFAgM,38704
401
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts,sha256=xt5dyj-6LK7T8IRSQ4sb4RBvD-xYPG68vWKqCET1QU4,6235
402
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts,sha256=RFfmwYWmHd9VJkXQPaONiqbqKp652c17T0YKo4RFYBY,19708
400
+ camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts,sha256=fcqZoTJArx9UTBzjelZkX7zse5HwbqqwFjQeMbeRWQI,42893
401
+ camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts,sha256=36o-N3YNTtLxdp5MfGHj4h7LowCx0o3oxKZKFBoFchA,6218
402
+ camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts,sha256=oz5rs3u7vqWxssCYNWkqJu4JOmk7dRbf2SMIEputHEs,22149
403
403
  camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts,sha256=uJGHmGs640iCrjllqXDXwDE4hGW1VJA2YL6BkFkzYNs,353
404
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts,sha256=ZT_5P9tun4ZCasIlDSS2tBTtpiI2DfcD4kOIpmXjrDw,2801
404
+ camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts,sha256=NFOJKROXYDHSXxcr15LHm36rPfRpVS2hsIobXcZgTRU,2964
405
405
  camel/toolkits/hybrid_browser_toolkit_py/__init__.py,sha256=tPQloCw-Ayl1lPfyvzGJkMFa1ze3ilXJq_1Oz5jg5s4,796
406
406
  camel/toolkits/hybrid_browser_toolkit_py/actions.py,sha256=S77VLL9hOq5M2RdPQrIAgjxinwS2ETd6kH6m6vtub5o,20935
407
407
  camel/toolkits/hybrid_browser_toolkit_py/agent.py,sha256=0ifwhYUDJ5GLxfdpC5KquPaW1a0QBAutp2Y9y0YFujw,11685
@@ -437,7 +437,7 @@ camel/toolkits/open_api_specs/web_scraper/openapi.yaml,sha256=u_WalQ01e8W1D27VnZ
437
437
  camel/toolkits/open_api_specs/web_scraper/paths/__init__.py,sha256=OKCZrQCDwaWtXIN_2rA9FSqEvgpQRieRoHh7Ek6N16A,702
438
438
  camel/toolkits/open_api_specs/web_scraper/paths/scraper.py,sha256=aWy1_ppV4NVVEZfnbN3tu9XA9yAPAC9bRStJ5JuXMRU,1117
439
439
  camel/types/__init__.py,sha256=EOmWlqS7aE5cB51_Vv7vHUexKeBbx9FSsfynl5vKjwo,2565
440
- camel/types/enums.py,sha256=5L4KfHVRDdIIlaMaU-Lv-P5de3AOHeh6EfZfQBveJz4,63964
440
+ camel/types/enums.py,sha256=x76UYX53jaUF0-qgUa0KFwRufo8cekb4-QGirlfTKB0,63781
441
441
  camel/types/mcp_registries.py,sha256=dl4LgYtSaUhsqAKpz28k_SA9La11qxqBvDLaEuyzrFE,4971
442
442
  camel/types/openai_types.py,sha256=m7oWb8nWYWOAwBRY1mP9mS9RVufXeDVj-fGvHAhXuMU,2120
443
443
  camel/types/unified_model_type.py,sha256=U3NUZux7QuMIxPW2H0qDp9BOyDJFHAx6jUmDNw5_9KM,5912
@@ -466,7 +466,7 @@ camel/verifiers/math_verifier.py,sha256=tA1D4S0sm8nsWISevxSN0hvSVtIUpqmJhzqfbuMo
466
466
  camel/verifiers/models.py,sha256=GdxYPr7UxNrR1577yW4kyroRcLGfd-H1GXgv8potDWU,2471
467
467
  camel/verifiers/physics_verifier.py,sha256=c1grrRddcrVN7szkxhv2QirwY9viIRSITWeWFF5HmLs,30187
468
468
  camel/verifiers/python_verifier.py,sha256=ogTz77wODfEcDN4tMVtiSkRQyoiZbHPY2fKybn59lHw,20558
469
- camel_ai-0.2.75a3.dist-info/METADATA,sha256=86OHr9M5A_cTjIAvHhESEpWG4t091n5rCO3QS5o7GQM,52289
470
- camel_ai-0.2.75a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
471
- camel_ai-0.2.75a3.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
472
- camel_ai-0.2.75a3.dist-info/RECORD,,
469
+ camel_ai-0.2.75a5.dist-info/METADATA,sha256=R6EVTTS1N9_Tr6y_hwN4hC0ppiWl8z6XrXcmZEGmqcc,52328
470
+ camel_ai-0.2.75a5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
471
+ camel_ai-0.2.75a5.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
472
+ camel_ai-0.2.75a5.dist-info/RECORD,,