@wordbricks/playwright-mcp 0.1.3
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.
- package/LICENSE +202 -0
- package/README.md +624 -0
- package/cli-wrapper.js +47 -0
- package/cli.js +18 -0
- package/config.d.ts +119 -0
- package/index.d.ts +23 -0
- package/index.js +19 -0
- package/lib/browserContextFactory.js +289 -0
- package/lib/browserServerBackend.js +82 -0
- package/lib/config.js +246 -0
- package/lib/context.js +236 -0
- package/lib/extension/cdpRelay.js +346 -0
- package/lib/extension/extensionContextFactory.js +56 -0
- package/lib/frameworkPatterns.js +35 -0
- package/lib/hooks/core.js +144 -0
- package/lib/hooks/eventConsumer.js +39 -0
- package/lib/hooks/events.js +42 -0
- package/lib/hooks/formatToolCallEvent.js +16 -0
- package/lib/hooks/frameworkStateHook.js +182 -0
- package/lib/hooks/grouping.js +72 -0
- package/lib/hooks/jsonLdDetectionHook.js +175 -0
- package/lib/hooks/networkFilters.js +74 -0
- package/lib/hooks/networkSetup.js +56 -0
- package/lib/hooks/networkTrackingHook.js +55 -0
- package/lib/hooks/pageHeightHook.js +75 -0
- package/lib/hooks/registry.js +39 -0
- package/lib/hooks/requireTabHook.js +26 -0
- package/lib/hooks/schema.js +75 -0
- package/lib/hooks/waitHook.js +33 -0
- package/lib/index.js +39 -0
- package/lib/loop/loop.js +69 -0
- package/lib/loop/loopClaude.js +152 -0
- package/lib/loop/loopOpenAI.js +141 -0
- package/lib/loop/main.js +60 -0
- package/lib/loopTools/context.js +66 -0
- package/lib/loopTools/main.js +51 -0
- package/lib/loopTools/perform.js +32 -0
- package/lib/loopTools/snapshot.js +29 -0
- package/lib/loopTools/tool.js +18 -0
- package/lib/mcp/inProcessTransport.js +72 -0
- package/lib/mcp/proxyBackend.js +115 -0
- package/lib/mcp/server.js +86 -0
- package/lib/mcp/tool.js +29 -0
- package/lib/mcp/transport.js +181 -0
- package/lib/playwrightTransformer.js +497 -0
- package/lib/program.js +111 -0
- package/lib/response.js +186 -0
- package/lib/sessionLog.js +121 -0
- package/lib/tab.js +249 -0
- package/lib/tools/common.js +55 -0
- package/lib/tools/console.js +33 -0
- package/lib/tools/dialogs.js +47 -0
- package/lib/tools/evaluate.js +53 -0
- package/lib/tools/extractFrameworkState.js +214 -0
- package/lib/tools/files.js +44 -0
- package/lib/tools/getSnapshot.js +37 -0
- package/lib/tools/getVisibleHtml.js +52 -0
- package/lib/tools/install.js +53 -0
- package/lib/tools/keyboard.js +78 -0
- package/lib/tools/mouse.js +99 -0
- package/lib/tools/navigate.js +70 -0
- package/lib/tools/network.js +123 -0
- package/lib/tools/networkDetail.js +231 -0
- package/lib/tools/networkSearch/bodySearch.js +141 -0
- package/lib/tools/networkSearch/grouping.js +28 -0
- package/lib/tools/networkSearch/helpers.js +32 -0
- package/lib/tools/networkSearch/searchHtml.js +65 -0
- package/lib/tools/networkSearch/types.js +1 -0
- package/lib/tools/networkSearch/urlSearch.js +82 -0
- package/lib/tools/networkSearch.js +168 -0
- package/lib/tools/pdf.js +40 -0
- package/lib/tools/repl.js +402 -0
- package/lib/tools/screenshot.js +79 -0
- package/lib/tools/scroll.js +126 -0
- package/lib/tools/snapshot.js +139 -0
- package/lib/tools/tabs.js +87 -0
- package/lib/tools/tool.js +33 -0
- package/lib/tools/utils.js +74 -0
- package/lib/tools/wait.js +55 -0
- package/lib/tools.js +67 -0
- package/lib/utils/codegen.js +49 -0
- package/lib/utils/extensionPath.js +6 -0
- package/lib/utils/fileUtils.js +36 -0
- package/lib/utils/graphql.js +258 -0
- package/lib/utils/guid.js +22 -0
- package/lib/utils/httpServer.js +39 -0
- package/lib/utils/log.js +21 -0
- package/lib/utils/manualPromise.js +111 -0
- package/lib/utils/networkFormat.js +12 -0
- package/lib/utils/package.js +20 -0
- package/lib/utils/result.js +2 -0
- package/lib/utils/sanitizeHtml.js +98 -0
- package/lib/utils/truncate.js +103 -0
- package/lib/utils/withTimeout.js +7 -0
- package/package.json +100 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { pipe, reduce, values } from '@fxts/core';
|
|
2
|
+
import { createHookRegistry, setToolHooks } from './core.js';
|
|
3
|
+
import { networkTrackingHooks } from './networkTrackingHook.js';
|
|
4
|
+
import { pageHeightHooks } from './pageHeightHook.js';
|
|
5
|
+
import { waitHooks } from './waitHook.js';
|
|
6
|
+
import { frameworkStateHooks } from './frameworkStateHook.js';
|
|
7
|
+
import { jsonLdDetectionHooks } from './jsonLdDetectionHook.js';
|
|
8
|
+
import { toolNameSchema } from './schema.js';
|
|
9
|
+
import { requireTabHooks } from './requireTabHook.js';
|
|
10
|
+
import { registerGroupingRule } from './grouping.js';
|
|
11
|
+
import { networkGroupingRule } from './networkTrackingHook.js';
|
|
12
|
+
const COMMON_HOOKS = {
|
|
13
|
+
preHooks: [
|
|
14
|
+
requireTabHooks.pre,
|
|
15
|
+
networkTrackingHooks.pre,
|
|
16
|
+
pageHeightHooks.pre,
|
|
17
|
+
frameworkStateHooks.pre,
|
|
18
|
+
jsonLdDetectionHooks.pre
|
|
19
|
+
],
|
|
20
|
+
postHooks: [
|
|
21
|
+
frameworkStateHooks.post,
|
|
22
|
+
jsonLdDetectionHooks.post,
|
|
23
|
+
networkTrackingHooks.post,
|
|
24
|
+
pageHeightHooks.post,
|
|
25
|
+
waitHooks.post
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
const toolHooksConfig = [
|
|
29
|
+
// Example hook registration
|
|
30
|
+
// {
|
|
31
|
+
// toolName: toolNameSchema.enum.browser_click,
|
|
32
|
+
// preHooks: [],
|
|
33
|
+
// postHooks: [],
|
|
34
|
+
// },
|
|
35
|
+
];
|
|
36
|
+
export const buildHookRegistry = () => {
|
|
37
|
+
registerGroupingRule('network-request', networkGroupingRule);
|
|
38
|
+
return pipe(createHookRegistry(), registry => reduce((acc, toolName) => setToolHooks(acc, { toolName, ...COMMON_HOOKS }), registry, values(toolNameSchema.enum)), registry => reduce(setToolHooks, registry, toolHooksConfig));
|
|
39
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Ok, Err } from '../utils/result.js';
|
|
2
|
+
import { hookNameSchema, toolNameSchema } from './schema.js';
|
|
3
|
+
const MESSAGE = 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.';
|
|
4
|
+
const requireTabPreHook = {
|
|
5
|
+
name: hookNameSchema.enum['require-tab-pre'],
|
|
6
|
+
handler: async (ctx) => {
|
|
7
|
+
try {
|
|
8
|
+
// Allow navigate tool to create/open a tab
|
|
9
|
+
if (ctx.toolName === toolNameSchema.enum.browser_navigate)
|
|
10
|
+
return Ok(undefined);
|
|
11
|
+
// If no current tab, emit a standardized error and stop execution
|
|
12
|
+
if (!ctx.context.currentTab()) {
|
|
13
|
+
// Include tabs section in response so the message is rendered consistently
|
|
14
|
+
ctx.response.setIncludeTabs();
|
|
15
|
+
return Err(new Error(MESSAGE));
|
|
16
|
+
}
|
|
17
|
+
return Ok(undefined);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return Err(error);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export const requireTabHooks = {
|
|
25
|
+
pre: requireTabPreHook,
|
|
26
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const hookNameSchema = z.enum([
|
|
3
|
+
'network-tracking-pre',
|
|
4
|
+
'network-tracking-post',
|
|
5
|
+
'page-height-pre',
|
|
6
|
+
'page-height-post',
|
|
7
|
+
'wait-post',
|
|
8
|
+
'framework-state-pre',
|
|
9
|
+
'framework-state-post',
|
|
10
|
+
'json-ld-detection-pre',
|
|
11
|
+
'json-ld-detection-post',
|
|
12
|
+
'require-tab-pre',
|
|
13
|
+
]);
|
|
14
|
+
// Tool names enum - should match actual tool names in
|
|
15
|
+
export const toolNameSchema = z.enum([
|
|
16
|
+
'browser_click',
|
|
17
|
+
'browser_extract_framework_state',
|
|
18
|
+
'browser_get_snapshot',
|
|
19
|
+
// 'browser_get_visible_html',
|
|
20
|
+
'browser_navigate',
|
|
21
|
+
// 'browser_navigate_back',
|
|
22
|
+
// 'browser_navigate_forward',
|
|
23
|
+
'browser_network_detail',
|
|
24
|
+
'browser_network_search',
|
|
25
|
+
'browser_press_key',
|
|
26
|
+
'browser_reload',
|
|
27
|
+
'browser_repl',
|
|
28
|
+
'browser_scroll',
|
|
29
|
+
'browser_wait',
|
|
30
|
+
]);
|
|
31
|
+
export const EventTypeSchema = z.enum([
|
|
32
|
+
'network-request',
|
|
33
|
+
'page-height-change',
|
|
34
|
+
'wait',
|
|
35
|
+
'tool-call',
|
|
36
|
+
'framework-state',
|
|
37
|
+
'json-ld',
|
|
38
|
+
]);
|
|
39
|
+
export const NetworkRequestEventDataSchema = z.object({
|
|
40
|
+
method: z.string(),
|
|
41
|
+
url: z.string(),
|
|
42
|
+
status: z.number(),
|
|
43
|
+
resourceType: z.string(),
|
|
44
|
+
postData: z.string().optional(),
|
|
45
|
+
responseSize: z.number().optional(),
|
|
46
|
+
});
|
|
47
|
+
export const PageHeightChangeEventDataSchema = z.object({
|
|
48
|
+
previousHeight: z.number(),
|
|
49
|
+
currentHeight: z.number(),
|
|
50
|
+
heightChange: z.number(),
|
|
51
|
+
scrollY: z.number(),
|
|
52
|
+
url: z.string(),
|
|
53
|
+
});
|
|
54
|
+
export const WaitEventDataSchema = z.object({
|
|
55
|
+
duration: z.number(),
|
|
56
|
+
});
|
|
57
|
+
export const ToolCallEventDataSchema = z.object({
|
|
58
|
+
toolName: z.string(),
|
|
59
|
+
params: z.record(z.unknown()).optional(),
|
|
60
|
+
executionTime: z.number().optional(),
|
|
61
|
+
success: z.boolean().optional(),
|
|
62
|
+
});
|
|
63
|
+
export const FrameworkStateEventDataSchema = z.object({
|
|
64
|
+
state: z.record(z.any()),
|
|
65
|
+
changes: z.array(z.string()).optional(),
|
|
66
|
+
action: z.enum(['detected', 'changed']),
|
|
67
|
+
});
|
|
68
|
+
export const JsonLdEventDataSchema = z.object({
|
|
69
|
+
state: z.record(z.object({
|
|
70
|
+
count: z.number(),
|
|
71
|
+
indices: z.array(z.number()),
|
|
72
|
+
})),
|
|
73
|
+
changes: z.array(z.string()).optional(),
|
|
74
|
+
action: z.enum(['detected', 'changed']),
|
|
75
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import ms from 'ms';
|
|
2
|
+
import { Ok, Err } from '../utils/result.js';
|
|
3
|
+
import { trackEvent } from './events.js';
|
|
4
|
+
import { hookNameSchema } from './schema.js';
|
|
5
|
+
export const WAIT_TIME_STR = '0.5s';
|
|
6
|
+
export const WAIT_TIME_MS = ms(WAIT_TIME_STR);
|
|
7
|
+
const waitPostHook = {
|
|
8
|
+
name: hookNameSchema.enum['wait-post'],
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
try {
|
|
11
|
+
const tab = ctx.tab || ctx.context.currentTab();
|
|
12
|
+
if (!tab)
|
|
13
|
+
return Ok(undefined);
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
await tab.page.waitForTimeout(WAIT_TIME_MS);
|
|
16
|
+
trackEvent(ctx.context, {
|
|
17
|
+
type: 'wait',
|
|
18
|
+
data: {
|
|
19
|
+
duration: WAIT_TIME_MS,
|
|
20
|
+
},
|
|
21
|
+
timestamp: startTime,
|
|
22
|
+
});
|
|
23
|
+
return Ok(undefined);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return Err(error);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export const waitHooks = {
|
|
31
|
+
post: waitPostHook,
|
|
32
|
+
};
|
|
33
|
+
export const formatWaitEvent = (event) => `Waited ${event.data.duration}ms`;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { BrowserServerBackend } from './browserServerBackend.js';
|
|
17
|
+
import { resolveConfig } from './config.js';
|
|
18
|
+
import { contextFactory } from './browserContextFactory.js';
|
|
19
|
+
import * as mcpServer from './mcp/server.js';
|
|
20
|
+
export async function createConnection(userConfig = {}, contextGetter) {
|
|
21
|
+
const config = await resolveConfig(userConfig);
|
|
22
|
+
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
|
|
23
|
+
return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
|
|
24
|
+
}
|
|
25
|
+
class SimpleBrowserContextFactory {
|
|
26
|
+
name = 'custom';
|
|
27
|
+
description = 'Connect to a browser using a custom context getter';
|
|
28
|
+
_contextGetter;
|
|
29
|
+
constructor(contextGetter) {
|
|
30
|
+
this._contextGetter = contextGetter;
|
|
31
|
+
}
|
|
32
|
+
async createContext() {
|
|
33
|
+
const browserContext = await this._contextGetter();
|
|
34
|
+
return {
|
|
35
|
+
browserContext,
|
|
36
|
+
close: () => browserContext.close()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
package/lib/loop/loop.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import debug from 'debug';
|
|
17
|
+
export async function runTask(delegate, client, task, oneShot = false) {
|
|
18
|
+
const { tools } = await client.listTools();
|
|
19
|
+
const taskContent = oneShot ? `Perform following task: ${task}.` : `Perform following task: ${task}. Once the task is complete, call the "done" tool.`;
|
|
20
|
+
const conversation = delegate.createConversation(taskContent, tools, oneShot);
|
|
21
|
+
for (let iteration = 0; iteration < 5; ++iteration) {
|
|
22
|
+
debug('history')('Making API call for iteration', iteration);
|
|
23
|
+
const toolCalls = await delegate.makeApiCall(conversation);
|
|
24
|
+
if (toolCalls.length === 0)
|
|
25
|
+
throw new Error('Call the "done" tool when the task is complete.');
|
|
26
|
+
const toolResults = [];
|
|
27
|
+
for (const toolCall of toolCalls) {
|
|
28
|
+
const doneResult = delegate.checkDoneToolCall(toolCall);
|
|
29
|
+
if (doneResult !== null)
|
|
30
|
+
return conversation.messages;
|
|
31
|
+
const { name, arguments: args, id } = toolCall;
|
|
32
|
+
try {
|
|
33
|
+
debug('tool')(name, args);
|
|
34
|
+
const response = await client.callTool({
|
|
35
|
+
name,
|
|
36
|
+
arguments: args,
|
|
37
|
+
});
|
|
38
|
+
const responseContent = (response.content || []);
|
|
39
|
+
debug('tool')(responseContent);
|
|
40
|
+
const text = responseContent.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
41
|
+
toolResults.push({
|
|
42
|
+
toolCallId: id,
|
|
43
|
+
content: text,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
debug('tool')(error);
|
|
48
|
+
toolResults.push({
|
|
49
|
+
toolCallId: id,
|
|
50
|
+
content: `Error while executing tool "${name}": ${error instanceof Error ? error.message : String(error)}\n\nPlease try to recover and complete the task.`,
|
|
51
|
+
isError: true,
|
|
52
|
+
});
|
|
53
|
+
// Skip remaining tool calls for this iteration
|
|
54
|
+
for (const remainingToolCall of toolCalls.slice(toolCalls.indexOf(toolCall) + 1)) {
|
|
55
|
+
toolResults.push({
|
|
56
|
+
toolCallId: remainingToolCall.id,
|
|
57
|
+
content: `This tool call is skipped due to previous error.`,
|
|
58
|
+
isError: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
delegate.addToolResults(conversation, toolResults);
|
|
65
|
+
if (oneShot)
|
|
66
|
+
return conversation.messages;
|
|
67
|
+
}
|
|
68
|
+
throw new Error('Failed to perform step, max attempts reached');
|
|
69
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
const model = 'claude-sonnet-4-20250514';
|
|
17
|
+
export class ClaudeDelegate {
|
|
18
|
+
_anthropic;
|
|
19
|
+
async anthropic() {
|
|
20
|
+
if (!this._anthropic) {
|
|
21
|
+
const anthropic = await import('@anthropic-ai/sdk');
|
|
22
|
+
this._anthropic = new anthropic.Anthropic();
|
|
23
|
+
}
|
|
24
|
+
return this._anthropic;
|
|
25
|
+
}
|
|
26
|
+
createConversation(task, tools, oneShot) {
|
|
27
|
+
const llmTools = tools.map(tool => ({
|
|
28
|
+
name: tool.name,
|
|
29
|
+
description: tool.description || '',
|
|
30
|
+
inputSchema: tool.inputSchema,
|
|
31
|
+
}));
|
|
32
|
+
if (!oneShot) {
|
|
33
|
+
llmTools.push({
|
|
34
|
+
name: 'done',
|
|
35
|
+
description: 'Call this tool when the task is complete.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
messages: [{
|
|
44
|
+
role: 'user',
|
|
45
|
+
content: task
|
|
46
|
+
}],
|
|
47
|
+
tools: llmTools,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async makeApiCall(conversation) {
|
|
51
|
+
// Convert generic messages to Claude format
|
|
52
|
+
const claudeMessages = [];
|
|
53
|
+
for (const message of conversation.messages) {
|
|
54
|
+
if (message.role === 'user') {
|
|
55
|
+
claudeMessages.push({
|
|
56
|
+
role: 'user',
|
|
57
|
+
content: message.content
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (message.role === 'assistant') {
|
|
61
|
+
const content = [];
|
|
62
|
+
// Add text content
|
|
63
|
+
if (message.content) {
|
|
64
|
+
content.push({
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: message.content,
|
|
67
|
+
citations: []
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Add tool calls
|
|
71
|
+
if (message.toolCalls) {
|
|
72
|
+
for (const toolCall of message.toolCalls) {
|
|
73
|
+
content.push({
|
|
74
|
+
type: 'tool_use',
|
|
75
|
+
id: toolCall.id,
|
|
76
|
+
name: toolCall.name,
|
|
77
|
+
input: toolCall.arguments
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
claudeMessages.push({
|
|
82
|
+
role: 'assistant',
|
|
83
|
+
content
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (message.role === 'tool') {
|
|
87
|
+
// Tool results are added differently - we need to find if there's already a user message with tool results
|
|
88
|
+
const lastMessage = claudeMessages[claudeMessages.length - 1];
|
|
89
|
+
const toolResult = {
|
|
90
|
+
type: 'tool_result',
|
|
91
|
+
tool_use_id: message.toolCallId,
|
|
92
|
+
content: message.content,
|
|
93
|
+
is_error: message.isError,
|
|
94
|
+
};
|
|
95
|
+
if (lastMessage && lastMessage.role === 'user' && Array.isArray(lastMessage.content)) {
|
|
96
|
+
// Add to existing tool results message
|
|
97
|
+
lastMessage.content.push(toolResult);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Create new tool results message
|
|
101
|
+
claudeMessages.push({
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: [toolResult]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Convert generic tools to Claude format
|
|
109
|
+
const claudeTools = conversation.tools.map(tool => ({
|
|
110
|
+
name: tool.name,
|
|
111
|
+
description: tool.description,
|
|
112
|
+
input_schema: tool.inputSchema,
|
|
113
|
+
}));
|
|
114
|
+
const anthropic = await this.anthropic();
|
|
115
|
+
const response = await anthropic.messages.create({
|
|
116
|
+
model,
|
|
117
|
+
max_tokens: 10000,
|
|
118
|
+
messages: claudeMessages,
|
|
119
|
+
tools: claudeTools,
|
|
120
|
+
});
|
|
121
|
+
// Extract tool calls and add assistant message to generic conversation
|
|
122
|
+
const toolCalls = response.content.filter(block => block.type === 'tool_use');
|
|
123
|
+
const textContent = response.content.filter(block => block.type === 'text').map(block => block.text).join('');
|
|
124
|
+
const llmToolCalls = toolCalls.map(toolCall => ({
|
|
125
|
+
name: toolCall.name,
|
|
126
|
+
arguments: toolCall.input,
|
|
127
|
+
id: toolCall.id,
|
|
128
|
+
}));
|
|
129
|
+
// Add assistant message to generic conversation
|
|
130
|
+
conversation.messages.push({
|
|
131
|
+
role: 'assistant',
|
|
132
|
+
content: textContent,
|
|
133
|
+
toolCalls: llmToolCalls.length > 0 ? llmToolCalls : undefined
|
|
134
|
+
});
|
|
135
|
+
return llmToolCalls;
|
|
136
|
+
}
|
|
137
|
+
addToolResults(conversation, results) {
|
|
138
|
+
for (const result of results) {
|
|
139
|
+
conversation.messages.push({
|
|
140
|
+
role: 'tool',
|
|
141
|
+
toolCallId: result.toolCallId,
|
|
142
|
+
content: result.content,
|
|
143
|
+
isError: result.isError,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
checkDoneToolCall(toolCall) {
|
|
148
|
+
if (toolCall.name === 'done')
|
|
149
|
+
return toolCall.arguments.result;
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
const model = 'gpt-4.1';
|
|
17
|
+
export class OpenAIDelegate {
|
|
18
|
+
_openai;
|
|
19
|
+
async openai() {
|
|
20
|
+
if (!this._openai) {
|
|
21
|
+
const oai = await import('openai');
|
|
22
|
+
this._openai = new oai.OpenAI();
|
|
23
|
+
}
|
|
24
|
+
return this._openai;
|
|
25
|
+
}
|
|
26
|
+
createConversation(task, tools, oneShot) {
|
|
27
|
+
const genericTools = tools.map(tool => ({
|
|
28
|
+
name: tool.name,
|
|
29
|
+
description: tool.description || '',
|
|
30
|
+
inputSchema: tool.inputSchema,
|
|
31
|
+
}));
|
|
32
|
+
if (!oneShot) {
|
|
33
|
+
genericTools.push({
|
|
34
|
+
name: 'done',
|
|
35
|
+
description: 'Call this tool when the task is complete.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
messages: [{
|
|
44
|
+
role: 'user',
|
|
45
|
+
content: task
|
|
46
|
+
}],
|
|
47
|
+
tools: genericTools,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async makeApiCall(conversation) {
|
|
51
|
+
// Convert generic messages to OpenAI format
|
|
52
|
+
const openaiMessages = [];
|
|
53
|
+
for (const message of conversation.messages) {
|
|
54
|
+
if (message.role === 'user') {
|
|
55
|
+
openaiMessages.push({
|
|
56
|
+
role: 'user',
|
|
57
|
+
content: message.content
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (message.role === 'assistant') {
|
|
61
|
+
const toolCalls = [];
|
|
62
|
+
if (message.toolCalls) {
|
|
63
|
+
for (const toolCall of message.toolCalls) {
|
|
64
|
+
toolCalls.push({
|
|
65
|
+
id: toolCall.id,
|
|
66
|
+
type: 'function',
|
|
67
|
+
function: {
|
|
68
|
+
name: toolCall.name,
|
|
69
|
+
arguments: JSON.stringify(toolCall.arguments)
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const assistantMessage = {
|
|
75
|
+
role: 'assistant'
|
|
76
|
+
};
|
|
77
|
+
if (message.content)
|
|
78
|
+
assistantMessage.content = message.content;
|
|
79
|
+
if (toolCalls.length > 0)
|
|
80
|
+
assistantMessage.tool_calls = toolCalls;
|
|
81
|
+
openaiMessages.push(assistantMessage);
|
|
82
|
+
}
|
|
83
|
+
else if (message.role === 'tool') {
|
|
84
|
+
openaiMessages.push({
|
|
85
|
+
role: 'tool',
|
|
86
|
+
tool_call_id: message.toolCallId,
|
|
87
|
+
content: message.content,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Convert generic tools to OpenAI format
|
|
92
|
+
const openaiTools = conversation.tools.map(tool => ({
|
|
93
|
+
type: 'function',
|
|
94
|
+
function: {
|
|
95
|
+
name: tool.name,
|
|
96
|
+
description: tool.description,
|
|
97
|
+
parameters: tool.inputSchema,
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
const openai = await this.openai();
|
|
101
|
+
const response = await openai.chat.completions.create({
|
|
102
|
+
model,
|
|
103
|
+
messages: openaiMessages,
|
|
104
|
+
tools: openaiTools,
|
|
105
|
+
tool_choice: 'auto'
|
|
106
|
+
});
|
|
107
|
+
const message = response.choices[0].message;
|
|
108
|
+
// Extract tool calls and add assistant message to generic conversation
|
|
109
|
+
const toolCalls = message.tool_calls || [];
|
|
110
|
+
const genericToolCalls = toolCalls.map(toolCall => {
|
|
111
|
+
const functionCall = toolCall.function;
|
|
112
|
+
return {
|
|
113
|
+
name: functionCall.name,
|
|
114
|
+
arguments: JSON.parse(functionCall.arguments),
|
|
115
|
+
id: toolCall.id,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
// Add assistant message to generic conversation
|
|
119
|
+
conversation.messages.push({
|
|
120
|
+
role: 'assistant',
|
|
121
|
+
content: message.content || '',
|
|
122
|
+
toolCalls: genericToolCalls.length > 0 ? genericToolCalls : undefined
|
|
123
|
+
});
|
|
124
|
+
return genericToolCalls;
|
|
125
|
+
}
|
|
126
|
+
addToolResults(conversation, results) {
|
|
127
|
+
for (const result of results) {
|
|
128
|
+
conversation.messages.push({
|
|
129
|
+
role: 'tool',
|
|
130
|
+
toolCallId: result.toolCallId,
|
|
131
|
+
content: result.content,
|
|
132
|
+
isError: result.isError,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
checkDoneToolCall(toolCall) {
|
|
137
|
+
if (toolCall.name === 'done')
|
|
138
|
+
return toolCall.arguments.result;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/lib/loop/main.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/* eslint-disable no-console */
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import url from 'url';
|
|
19
|
+
import dotenv from 'dotenv';
|
|
20
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
21
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
22
|
+
import { program } from 'commander';
|
|
23
|
+
import { OpenAIDelegate } from './loopOpenAI.js';
|
|
24
|
+
import { ClaudeDelegate } from './loopClaude.js';
|
|
25
|
+
import { runTask } from './loop.js';
|
|
26
|
+
dotenv.config();
|
|
27
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
28
|
+
async function run(delegate) {
|
|
29
|
+
const transport = new StdioClientTransport({
|
|
30
|
+
command: 'node',
|
|
31
|
+
args: [
|
|
32
|
+
path.resolve(__filename, '../../../cli.js'),
|
|
33
|
+
'--save-session',
|
|
34
|
+
'--output-dir', path.resolve(__filename, '../../../sessions')
|
|
35
|
+
],
|
|
36
|
+
stderr: 'inherit',
|
|
37
|
+
env: process.env,
|
|
38
|
+
});
|
|
39
|
+
const client = new Client({ name: 'test', version: '1.0.0' });
|
|
40
|
+
await client.connect(transport);
|
|
41
|
+
await client.ping();
|
|
42
|
+
for (const task of tasks) {
|
|
43
|
+
const messages = await runTask(delegate, client, task);
|
|
44
|
+
for (const message of messages)
|
|
45
|
+
console.log(`${message.role}: ${message.content}`);
|
|
46
|
+
}
|
|
47
|
+
await client.close();
|
|
48
|
+
}
|
|
49
|
+
const tasks = [
|
|
50
|
+
'Open https://playwright.dev/',
|
|
51
|
+
];
|
|
52
|
+
program
|
|
53
|
+
.option('--model <model>', 'model to use')
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
if (options.model === 'claude')
|
|
56
|
+
await run(new ClaudeDelegate());
|
|
57
|
+
else
|
|
58
|
+
await run(new OpenAIDelegate());
|
|
59
|
+
});
|
|
60
|
+
void program.parseAsync(process.argv);
|