camel-ai 0.2.75a3__py3-none-any.whl → 0.2.75a6__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.

@@ -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,55 @@ 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 proper prefix"""
511
+ if not ref:
512
+ return ref
513
+
514
+ # If ref is purely numeric, add 'e' prefix for main frame
515
+ if ref.isdigit():
516
+ return f'e{ref}'
517
+
518
+ return ref
519
+
520
+ def _process_refs_in_params(
521
+ self, params: Dict[str, Any]
522
+ ) -> Dict[str, Any]:
523
+ """Process parameters to ensure all refs have 'e' prefix."""
524
+ if not params:
525
+ return params
526
+
527
+ # Create a copy to avoid modifying the original
528
+ processed = params.copy()
529
+
530
+ # Handle direct ref parameters
531
+ if 'ref' in processed:
532
+ processed['ref'] = self._ensure_ref_prefix(processed['ref'])
533
+
534
+ # Handle from_ref and to_ref for drag operations
535
+ if 'from_ref' in processed:
536
+ processed['from_ref'] = self._ensure_ref_prefix(
537
+ processed['from_ref']
538
+ )
539
+ if 'to_ref' in processed:
540
+ processed['to_ref'] = self._ensure_ref_prefix(processed['to_ref'])
541
+
542
+ # Handle inputs array for type_multiple
543
+ if 'inputs' in processed and isinstance(processed['inputs'], list):
544
+ processed_inputs = []
545
+ for input_item in processed['inputs']:
546
+ if isinstance(input_item, dict) and 'ref' in input_item:
547
+ processed_input = input_item.copy()
548
+ processed_input['ref'] = self._ensure_ref_prefix(
549
+ input_item['ref']
550
+ )
551
+ processed_inputs.append(processed_input)
552
+ else:
553
+ processed_inputs.append(input_item)
554
+ processed['inputs'] = processed_inputs
555
+
556
+ return processed
557
+
506
558
  @action_logger
507
559
  async def click(self, ref: str) -> Dict[str, Any]:
508
560
  """Click an element."""
@@ -515,6 +567,14 @@ class WebSocketBrowserWrapper:
515
567
  response = await self._send_command('type', {'ref': ref, 'text': text})
516
568
  return response
517
569
 
570
+ @action_logger
571
+ async def type_multiple(
572
+ self, inputs: List[Dict[str, str]]
573
+ ) -> Dict[str, Any]:
574
+ """Type text into multiple elements."""
575
+ response = await self._send_command('type', {'inputs': inputs})
576
+ return response
577
+
518
578
  @action_logger
519
579
  async def select(self, ref: str, value: str) -> Dict[str, Any]:
520
580
  """Select an option."""
@@ -12,6 +12,7 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
+ import warnings
15
16
  from typing import List
16
17
 
17
18
  from camel.toolkits.base import BaseToolkit
@@ -27,7 +28,7 @@ class MathToolkit(BaseToolkit):
27
28
  addition, subtraction, multiplication, division, and rounding.
28
29
  """
29
30
 
30
- def add(self, a: float, b: float) -> float:
31
+ def math_add(self, a: float, b: float) -> float:
31
32
  r"""Adds two numbers.
32
33
 
33
34
  Args:
@@ -39,7 +40,7 @@ class MathToolkit(BaseToolkit):
39
40
  """
40
41
  return a + b
41
42
 
42
- def sub(self, a: float, b: float) -> float:
43
+ def math_subtract(self, a: float, b: float) -> float:
43
44
  r"""Do subtraction between two numbers.
44
45
 
45
46
  Args:
@@ -51,7 +52,9 @@ class MathToolkit(BaseToolkit):
51
52
  """
52
53
  return a - b
53
54
 
54
- def multiply(self, a: float, b: float, decimal_places: int = 2) -> float:
55
+ def math_multiply(
56
+ self, a: float, b: float, decimal_places: int = 2
57
+ ) -> float:
55
58
  r"""Multiplies two numbers.
56
59
 
57
60
  Args:
@@ -65,7 +68,9 @@ class MathToolkit(BaseToolkit):
65
68
  """
66
69
  return round(a * b, decimal_places)
67
70
 
68
- def divide(self, a: float, b: float, decimal_places: int = 2) -> float:
71
+ def math_divide(
72
+ self, a: float, b: float, decimal_places: int = 2
73
+ ) -> float:
69
74
  r"""Divides two numbers.
70
75
 
71
76
  Args:
@@ -79,7 +84,7 @@ class MathToolkit(BaseToolkit):
79
84
  """
80
85
  return round(a / b, decimal_places)
81
86
 
82
- def round(self, a: float, decimal_places: int = 0) -> float:
87
+ def math_round(self, a: float, decimal_places: int = 0) -> float:
83
88
  r"""Rounds a number to a specified number of decimal places.
84
89
 
85
90
  Args:
@@ -101,9 +106,58 @@ class MathToolkit(BaseToolkit):
101
106
  representing the functions in the toolkit.
102
107
  """
103
108
  return [
104
- FunctionTool(self.add),
105
- FunctionTool(self.sub),
106
- FunctionTool(self.multiply),
107
- FunctionTool(self.divide),
108
- FunctionTool(self.round),
109
+ FunctionTool(self.math_add),
110
+ FunctionTool(self.math_subtract),
111
+ FunctionTool(self.math_multiply),
112
+ FunctionTool(self.math_divide),
113
+ FunctionTool(self.math_round),
109
114
  ]
115
+
116
+ # Deprecated method aliases for backward compatibility
117
+ def add(self, *args, **kwargs):
118
+ r"""Deprecated: Use math_add instead."""
119
+ warnings.warn(
120
+ "add is deprecated. Use math_add instead.",
121
+ DeprecationWarning,
122
+ stacklevel=2,
123
+ )
124
+ return self.math_add(*args, **kwargs)
125
+
126
+ def sub(self, *args, **kwargs):
127
+ r"""Deprecated: Use math_subtract instead."""
128
+ warnings.warn(
129
+ "sub is deprecated. Use math_subtract instead.",
130
+ DeprecationWarning,
131
+ stacklevel=2,
132
+ )
133
+ return self.math_subtract(*args, **kwargs)
134
+
135
+ def multiply(self, *args, **kwargs):
136
+ r"""Deprecated: Use math_multiply instead."""
137
+ warnings.warn(
138
+ "multiply is deprecated. Use math_multiply instead.",
139
+ DeprecationWarning,
140
+ stacklevel=2,
141
+ )
142
+ return self.math_multiply(*args, **kwargs)
143
+
144
+ def divide(self, *args, **kwargs):
145
+ r"""Deprecated: Use math_divide instead."""
146
+ warnings.warn(
147
+ "divide is deprecated. Use math_divide instead.",
148
+ DeprecationWarning,
149
+ stacklevel=2,
150
+ )
151
+ return self.math_divide(*args, **kwargs)
152
+
153
+ def round(self, *args, **kwargs):
154
+ r"""Deprecated: Use math_round instead. Note: This was shadowing
155
+ Python's built-in round().
156
+ """
157
+ warnings.warn(
158
+ "round is deprecated. Use math_round instead. This was "
159
+ "shadowing Python's built-in round().",
160
+ DeprecationWarning,
161
+ stacklevel=2,
162
+ )
163
+ return self.math_round(*args, **kwargs)