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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +147 -93
- camel/configs/__init__.py +3 -0
- camel/configs/nebius_config.py +103 -0
- camel/models/__init__.py +2 -0
- camel/models/model_factory.py +2 -0
- camel/models/nebius_model.py +83 -0
- camel/models/ollama_model.py +3 -3
- camel/societies/workforce/task_channel.py +120 -27
- camel/societies/workforce/workforce.py +35 -3
- camel/toolkits/github_toolkit.py +104 -17
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +35 -5
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +124 -29
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +1 -1
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +103 -40
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +3 -2
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +8 -1
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +60 -0
- camel/toolkits/math_toolkit.py +64 -10
- camel/toolkits/openai_image_toolkit.py +55 -24
- camel/toolkits/search_toolkit.py +13 -2
- camel/types/enums.py +34 -9
- camel/types/unified_model_type.py +5 -0
- {camel_ai-0.2.75a3.dist-info → camel_ai-0.2.75a6.dist-info}/METADATA +4 -11
- {camel_ai-0.2.75a3.dist-info → camel_ai-0.2.75a6.dist-info}/RECORD +27 -25
- {camel_ai-0.2.75a3.dist-info → camel_ai-0.2.75a6.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.75a3.dist-info → camel_ai-0.2.75a6.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,
|
|
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
|
-
|
|
83
|
-
if (pageUrl === 'about:blank') {
|
|
81
|
+
if (this.isBlankPageUrl(pageUrl)) {
|
|
84
82
|
const tabId = this.generateTabId();
|
|
85
83
|
this.registerNewPage(tabId, page);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
//
|
|
435
|
-
|
|
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:
|
|
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
|
-
|
|
744
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
|
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: [
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
result
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
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,
|
|
212
|
-
const textY = Math.max(14, Math.min(y + 14,
|
|
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="${
|
|
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="${
|
|
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
|
|
367
|
-
|
|
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
|
|
85
|
-
text
|
|
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
|
-
|
|
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."""
|
camel/toolkits/math_toolkit.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
105
|
-
FunctionTool(self.
|
|
106
|
-
FunctionTool(self.
|
|
107
|
-
FunctionTool(self.
|
|
108
|
-
FunctionTool(self.
|
|
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)
|