rl-simulator-core 1.0.0

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/src/ai.js ADDED
@@ -0,0 +1,86 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import { generateText } from 'ai';
3
+ import { API_KEY, ENDPOINT, BASE_URL, SYSTEM_PROMPT } from './config.js';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+
7
+ const logFile = path.join(process.cwd(), 'ai_traffic.log');
8
+
9
+ const log = (message) => {
10
+ const timestamp = new Date().toISOString();
11
+ const logEntry = `[${timestamp}] ${message}\n`;
12
+ fs.appendFileSync(logFile, logEntry);
13
+ };
14
+
15
+ // Custom Fetch for Logging (Kept logic, same structure)
16
+ const customFetch = async (url, options) => {
17
+ // ... (same logging logic as before) ...
18
+ // Simplified for brevity in rewrite, but keeping the core idea if needed.
19
+ return fetch(url, options);
20
+ };
21
+
22
+ const openai = createOpenAI({
23
+ baseURL: BASE_URL,
24
+ apiKey: API_KEY,
25
+ compatibility: 'compatible',
26
+ });
27
+
28
+ async function queryAI(messages) {
29
+ console.log("Querying AI with history length:", messages.length);
30
+
31
+ try {
32
+ console.log("⏳ 发送请求中...");
33
+ const startTime = Date.now();
34
+
35
+ const { text } = await generateText({
36
+ model: openai.chat(ENDPOINT),
37
+ system: SYSTEM_PROMPT,
38
+ messages: messages,
39
+ temperature: 0.1,
40
+ topP: 0.7,
41
+ });
42
+
43
+ const endTime = Date.now();
44
+ console.log("✅ 请求成功! 耗时:", (endTime - startTime) / 1000, "秒");
45
+ console.log("📥 响应内容:", text.substring(0, 500) + "...");
46
+
47
+ return text;
48
+
49
+ } catch (error) {
50
+ console.error("❌ AI 请求失败!", error);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function parseActions(aiContent) {
56
+ const actions = [];
57
+ const functionRegex = /<function[^>]*=([^>]+)>([\s\S]*?)<\/function[^>]*>/g;
58
+
59
+ let functionMatch;
60
+ while ((functionMatch = functionRegex.exec(aiContent)) !== null) {
61
+ const functionName = functionMatch[1];
62
+ const functionBody = functionMatch[2];
63
+ const params = {};
64
+
65
+ const paramRegex = /<parameter[^>]*=([^>]+)>([\s\S]*?)<\/parameter[^>]*>/g;
66
+ let paramMatch;
67
+ while ((paramMatch = paramRegex.exec(functionBody)) !== null) {
68
+ const paramName = paramMatch[1];
69
+ let paramValue = paramMatch[2].trim();
70
+ const cleanedValue = paramValue.replace(/<[^>]+>/g, '').trim();
71
+ params[paramName] = cleanedValue;
72
+ }
73
+
74
+ actions.push({
75
+ name: functionName,
76
+ params: params
77
+ });
78
+ }
79
+
80
+ return actions;
81
+ }
82
+
83
+ export {
84
+ queryAI,
85
+ parseActions
86
+ };
package/src/config.js ADDED
@@ -0,0 +1,83 @@
1
+ import dotenv from 'dotenv';
2
+ dotenv.config();
3
+
4
+ const API_KEY = process.env.ARK_API_KEY;
5
+ const ENDPOINT = process.env.EP;
6
+ const BASE_URL = process.env.BASE_URL;
7
+
8
+ if (!API_KEY || !ENDPOINT || !BASE_URL) {
9
+ console.warn("Missing environment variables (ARK_API_KEY, EP, BASE_URL). Agent may fail if not provided later.");
10
+ }
11
+
12
+ const SYSTEM_PROMPT = `
13
+ Intro to Think Mode
14
+ To explore the performance of our model with different styles of thinking, we introduce four modes of thinking: budgted, efficient, no think, and unrestricted. The model is default to unrestricted think mode.
15
+ - Budgeted think mode: Limit model's internal reasoning and reflections to stay within the specified token budget.
16
+ - Efficient think mode: Model provides a concise internal thinking process with efficient reasoning and reflections. It doesn't have a strict token budget, but should be less verbose and more direct in your thinking.
17
+ - No think mode: Model responds directly to the question without any internal reasoning process or extra thinking tokens. Still follows the template with the minimum required thinking tokens to justify the answer.
18
+ - Unrestricted think mode: Model engages in an internal thinking process with thorough reasoning and reflections. There's an unlimited budget for thinking tokens and can continue thinking until you fully solve the problem.
19
+ Think Mode Assignment
20
+ As the "think mode" sp is not encapsulated yet, you need to manually sew in different sp for different modes of thinking. Here are the sp's:
21
+ NOTE: You should disable "thinking" first: (I know it's a bit counterintuitive.)
22
+ "thinking": {
23
+ "type": "disabled"
24
+ }
25
+ - Unrestricted think mode
26
+ You should begin by detailing the internal reasoning process, and then present the answer to the user. The reasoning process should be enclosed within <think_never_used_51bce0c785ca2f68081bfa7d91973934> and </think_never_used_51bce0c785ca2f68081bfa7d91973934> tags, as follows:
27
+ <think_never_used_51bce0c785ca2f68081bfa7d91973934> reasoning process here </think_never_used_51bce0c785ca2f68081bfa7d91973934> answer here.
28
+ You have different modes of thinking:
29
+ Unrestricted think mode: Engage in an internal thinking process with thorough reasoning and reflections. You have an unlimited budget for thinking tokens and can continue thinking until you fully solve the problem.
30
+ Efficient think mode: Provide a concise internal thinking process with efficient reasoning and reflections. You don't have a strict token budget but be less verbose and more direct in your thinking.
31
+ No think mode: Respond directly to the question without any internal reasoning process or extra thinking tokens. Still follow the template with the minimum required thinking tokens to justify the answer.
32
+ Budgeted think mode: Limit your internal reasoning and reflections to stay within the specified token budget
33
+ Based on the complexity of the problem, select the appropriate mode for reasoning among the provided options listed below.
34
+ Provided Mode(s):
35
+ Unrestricted think
36
+ Action Space
37
+ """
38
+ ## Function Definition
39
+ - You have access to the following functions:
40
+ {"type": "function", "name": "click", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse left single click action."}
41
+ {"type": "function", "name": "left_double", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse left double click action."}
42
+ {"type": "function", "name": "left_triple", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse left triple click action."}
43
+ {"type": "function", "name": "right_single", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse right single click action."}
44
+ {"type": "function", "name": "drag", "parameters": {"type": "object", "properties": {"start_point": {"type": "string", "description": "Drag start point. The format is: x y"}, "end_point": {"type": "string", "description": "Drag end point. The format is: x y"}}, "required": ["start_point", "end_point"]}, "description": "Mouse left button drag action."}
45
+ {"type": "function", "name": "scroll", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Scroll start position. If not specified, default to execute on the current mouse position. The format is: x y"}, "direction": {"type": "string", "description": "Scroll direction.", "enum": ["up", "down", "left", "right"]}}, "required": ["direction"]}, "description": "Scroll action."}
46
+ {"type": "function", "name": "move_to", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Target coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse move action."}
47
+ {"type": "function", "name": "mouse_down", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Mouse down position. If not specified, default to execute on the current mouse position. The format is: x y"}, "button": {"type": "string", "description": "Down button. Default to left.", "enum": ["left", "right"]}}, "required": []}, "description": "Mouse down action."}
48
+ {"type": "function", "name": "mouse_up", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Mouse up position. If not specified, default to execute on the current mouse position. The format is: x y"}, "button": {"type": "string", "description": "Up button. Default to left.", "enum": ["left", "right"]}}, "required": []}, "description": "Mouse up action."}
49
+ {"type": "function", "name": "type", "parameters": {"type": "object", "properties": {"content": {"type": "string", "description": "Type content. If you want to submit your input, use \\n at the end of content."}}, "required": ["content"]}, "description": "Type content."}
50
+ {"type": "function", "name": "hotkey", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "Hotkeys you want to press. Split keys with a space and use lowercase."}}, "required": ["key"]}, "description": "Press hotkey."}
51
+ {"type": "function", "name": "press", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "Key you want to press. Only one key can be pressed at one time."}}, "required": ["key"]}, "description": "Press key."}
52
+ {"type": "function", "name": "call_user", "parameters": {"type": "object", "properties": {"content": {"type": "string", "description": "Message or information displayed to the user to request their input, feedback, or guidance."}}, "required": []}, "description": "This function is used to interact with the user by displaying a message and requesting their input, feedback, or guidance."}
53
+ {"type": "function", "name": "wait", "parameters": {"type": "object", "properties": {"time": {"type": "string", "description": "Wait time in seconds."}}, "required": []}, "description": "Wait for a while."}
54
+ {"type": "function", "name": "finished", "parameters": {"type": "object", "properties": {"content": {"type": "string", "description": "Provide the final answer or response to complete the task."}}, "required": []}, "description": "This function is used to indicate the completion of a task by providing the final answer or response."}
55
+
56
+ - To call a function, use the following structure without any suffix:
57
+
58
+ <gui_think> reasoning process </gui_think>
59
+ <seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934><function_never_used_51bce0c785ca2f68081bfa7d91973934=example_function_name><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_1>value_1</parameter_never_used_51bce0c785ca2f68081bfa7d91973934><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_2>
60
+ This is the value for the second parameter
61
+ that can span
62
+ multiple lines
63
+ </parameter_never_used_51bce0c785ca2f68081bfa7d91973934></function_never_used_51bce0c785ca2f68081bfa7d91973934></seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934>
64
+
65
+ ## Important Notes
66
+ - Function calls must begin with <function_never_used_51bce0c785ca2f68081bfa7d91973934= and end with </function_never_used_51bce0c785ca2f68081bfa7d91973934>.
67
+ - All required parameters must be explicitly provided.
68
+
69
+ ## Additional Notes
70
+ - You can execute multiple actions within a single tool call. For example:
71
+ <seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934><function_never_used_51bce0c785ca2f68081bfa7d91973934=example_function_1><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_1>value_1</parameter_never_used_51bce0c785ca2f68081bfa7d91973934><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_2>
72
+ This is the value for the second parameter
73
+ that can span
74
+ multiple lines
75
+ </parameter_never_used_51bce0c785ca2f68081bfa7d91973934></function_never_used_51bce0c785ca2f68081bfa7d91973934><function_never_used_51bce0c785ca2f68081bfa7d91973934=example_function_2><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_3>value_4</parameter_never_used_51bce0c785ca2f68081bfa7d91973934></function_never_used_51bce0c785ca2f68081bfa7d91973934></seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934>
76
+ """`;
77
+
78
+ export {
79
+ API_KEY,
80
+ ENDPOINT,
81
+ BASE_URL,
82
+ SYSTEM_PROMPT
83
+ };
package/src/index.js ADDED
@@ -0,0 +1,155 @@
1
+
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { runAgent } from './runner.js';
5
+ import AdmZip from 'adm-zip';
6
+ import dotenv from 'dotenv';
7
+
8
+ export { runAgent };
9
+
10
+ // Load env vars
11
+ dotenv.config();
12
+
13
+ /**
14
+ * Format date to YYYYMMDD_HHMMSS
15
+ */
16
+ function getFormattedDate() {
17
+ const now = new Date();
18
+ const pad = (n) => String(n).padStart(2, '0');
19
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
20
+ }
21
+
22
+ /**
23
+ * Execute the task flow 5 times and save results in optimized structure.
24
+ * @param {string} targetWeb - The URL of the web app to test
25
+ * @param {string} targetServer - The URL of the backend task service
26
+ * @param {string} taskId - The ID of the task to run
27
+ */
28
+ export async function runTaskLoop(targetWeb, targetServer, taskId) {
29
+ const results = [];
30
+ const datetime = getFormattedDate();
31
+ const folderName = `${taskId}_${datetime}`;
32
+ const outputDir = path.join(process.cwd(), folderName);
33
+
34
+ // Create root output directory
35
+ if (!fs.existsSync(outputDir)) {
36
+ fs.mkdirSync(outputDir, { recursive: true });
37
+ console.log(`Created output directory: ${outputDir}`);
38
+ }
39
+
40
+ // Ensure URLs have protocols
41
+ if (!targetWeb.startsWith('http')) targetWeb = 'http://' + targetWeb;
42
+ if (!targetServer.startsWith('http')) targetServer = 'http://' + targetServer;
43
+
44
+ console.log(`Starting Task Loop for ${taskId}`);
45
+
46
+ for (let i = 1; i <= 2; i++) {
47
+ console.log(`\n=== Starting Iteration ${i}/5 ===`);
48
+ try {
49
+ const data = await executeSingleCycle(targetWeb, targetServer, taskId, i);
50
+ const { sessionId, agentMessages, ...rest } = data;
51
+
52
+ // Create session subdirectory
53
+ const sessionDir = path.join(outputDir, sessionId);
54
+ if (!fs.existsSync(sessionDir)) {
55
+ fs.mkdirSync(sessionDir, { recursive: true });
56
+ }
57
+
58
+ // Save messages.json
59
+ fs.writeFileSync(
60
+ path.join(sessionDir, 'messages.json'),
61
+ JSON.stringify(agentMessages, null, 2)
62
+ );
63
+
64
+ // Save result.json
65
+ fs.writeFileSync(
66
+ path.join(sessionDir, 'result.json'),
67
+ JSON.stringify(rest, null, 2)
68
+ );
69
+
70
+ results.push({ sessionId, ...rest });
71
+ console.log(`Iteration ${i} completed. Saved to ${sessionDir}`);
72
+
73
+ } catch (e) {
74
+ console.error(`Iteration ${i} failed:`, e.stack || e.message);
75
+ results.push({ iteration: i, error: e.message });
76
+ }
77
+ }
78
+
79
+ // Save final-results.json (Consolidated)
80
+ const finalResultPath = path.join(outputDir, 'final-results.json');
81
+ fs.writeFileSync(finalResultPath, JSON.stringify(results, null, 2));
82
+ console.log(`Saved consolidated results to ${finalResultPath}`);
83
+
84
+ // Zip the results
85
+ console.log("Packaging results into ZIP...");
86
+ const zipName = `${folderName}.zip`;
87
+ const zipPath = path.join(process.cwd(), zipName);
88
+
89
+ try {
90
+ const zip = new AdmZip();
91
+ zip.addLocalFolder(outputDir, folderName);
92
+ zip.writeZip(zipPath);
93
+ console.log(`\n✅ All done! Zip saved to: ${zipPath}`);
94
+ } catch (zipError) {
95
+ console.error("Failed to create zip:", zipError);
96
+ }
97
+
98
+ return zipPath;
99
+ }
100
+
101
+ async function executeSingleCycle(targetWeb, targetServer, taskId, iteration) {
102
+ const fetchJson = async (url, opts) => {
103
+ const res = await fetch(url, opts);
104
+ if (!res.ok) {
105
+ const text = await res.text();
106
+ throw new Error(`API ${url} failed: ${res.status} ${text}`);
107
+ }
108
+ return await res.json();
109
+ };
110
+
111
+ // 1. Start Task
112
+ console.log("Calling Start API...");
113
+ const startUrl = `${targetServer}/api/tasks/${taskId}/start`;
114
+ const startData = await fetchJson(startUrl, { method: 'POST' });
115
+
116
+ if (startData.code !== 200) {
117
+ throw new Error(`Start API failed: ${JSON.stringify(startData)}`);
118
+ }
119
+ const { session_id: sessionId, TaskJson } = startData.data;
120
+
121
+ // 2. Setup Task
122
+ console.log(`Setup for session ${sessionId}...`);
123
+ const setupUrl = `${targetServer}/api/tasks/setup`;
124
+ await fetchJson(setupUrl, {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({ task_id: taskId, session_id: sessionId })
128
+ });
129
+
130
+ // 3. Run Agent
131
+ console.log("Running Agent...");
132
+ const agentResult = await runAgent({
133
+ taskInput: TaskJson.task.instruction,
134
+ targetUrl: targetWeb,
135
+ sessionId: sessionId,
136
+ simulatedUserKnownInfo: TaskJson.task.simulated_user_known_info
137
+ });
138
+
139
+ // 4. Verify Task
140
+ console.log("Verifying result...");
141
+ const verifyUrl = `${targetServer}/api/tasks/verify`;
142
+ const verifyData = await fetchJson(verifyUrl, {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({ task_id: taskId, session_id: sessionId })
146
+ });
147
+
148
+ return {
149
+ iteration,
150
+ sessionId,
151
+ agentStatus: agentResult.status,
152
+ agentMessages: agentResult.messages || [],
153
+ verifyResult: verifyData.data
154
+ };
155
+ }
package/src/runner.js ADDED
@@ -0,0 +1,138 @@
1
+ import { chromium } from 'playwright';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { queryAI, parseActions } from './ai.js';
5
+ import { executeAction } from './actions.js';
6
+
7
+ async function runAgent({ targetUrl, taskInput, sessionId, simulatedUserKnownInfo }) {
8
+ console.log(`[Agent] Starting task: ${taskInput} on ${targetUrl} (Session: ${sessionId})`);
9
+
10
+ const screenshotsDir = path.join(process.cwd(), 'screenshots');
11
+ if (!fs.existsSync(screenshotsDir)) {
12
+ fs.mkdirSync(screenshotsDir, { recursive: true });
13
+ console.log(`📁 创建截图目录: ${screenshotsDir}`);
14
+ }
15
+
16
+ // 2. Launch Browser
17
+ const browser = await chromium.launch({
18
+ headless: true, // Visible for demo/debug
19
+ args: ['--start-maximized'] // Attempt to maximize
20
+ });
21
+
22
+ const context = await browser.newContext({
23
+ viewport: { width: 430, height: 800 } // Set a reasonable fixed viewport
24
+ });
25
+ const page = await context.newPage();
26
+
27
+ let conversationHistory = [];
28
+
29
+ try {
30
+ // Ensure protocol exists
31
+ let finalUrl = targetUrl;
32
+ if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
33
+ finalUrl = 'http://' + finalUrl;
34
+ }
35
+
36
+ // Append sessionId
37
+ const urlObj = new URL(finalUrl);
38
+ urlObj.searchParams.append('sessionId', sessionId);
39
+ finalUrl = urlObj.toString();
40
+
41
+ console.log(`Navigating to ${finalUrl}...`);
42
+ await page.goto(finalUrl, { waitUntil: 'load' });
43
+ await page.waitForTimeout(2000); // Wait for initial render
44
+
45
+ let isRunning = true;
46
+ let loopCount = 0;
47
+ const MAX_LOOPS = 20; // Safety break
48
+
49
+ while (isRunning && loopCount < MAX_LOOPS) {
50
+ loopCount++;
51
+ console.log(`\n--- Cycle ${loopCount} ---`);
52
+
53
+ // 1. Screenshot
54
+ const screenshotBuffer = await page.screenshot({ format: 'png' });
55
+ const screenshotBase64 = screenshotBuffer.toString('base64');
56
+
57
+ // 保存截图到本地
58
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
59
+ const screenshotPath = path.join(screenshotsDir, `screenshot_${sessionId}_${loopCount}_${timestamp}.png`);
60
+ fs.writeFileSync(screenshotPath, screenshotBuffer);
61
+ console.log(`📸 截图已保存: ${screenshotPath}`);
62
+
63
+ // 获取并记录viewport尺寸
64
+ const viewport = page.viewportSize();
65
+ console.log(`📐 Viewport尺寸: ${viewport.width}x${viewport.height}`);
66
+ console.log(`📦 截图大小: ${screenshotBuffer.length} bytes`);
67
+
68
+ // 2. Prepare Message for AI
69
+ let promptText = `Task: ${taskInput}\n`;
70
+ if (simulatedUserKnownInfo) {
71
+ promptText += `\nKnown User Info: ${JSON.stringify(simulatedUserKnownInfo, null, 2)}\n`;
72
+ }
73
+ promptText += `\nPlease perform the next action based on the screenshot.`;
74
+
75
+ const userContent = [
76
+ { type: 'text', text: promptText },
77
+ { type: 'image', image: screenshotBase64 }
78
+ ];
79
+
80
+ conversationHistory.push({
81
+ role: 'user',
82
+ content: userContent
83
+ });
84
+
85
+ // 3. Query AI
86
+ console.log("Querying AI...");
87
+ const aiContent = await queryAI(conversationHistory);
88
+
89
+ if (!aiContent) {
90
+ console.error("Invalid AI response. Retrying...");
91
+ await page.waitForTimeout(2000);
92
+ continue;
93
+ }
94
+
95
+ // Add assistant response to history
96
+ conversationHistory.push({
97
+ role: 'assistant',
98
+ content: aiContent
99
+ });
100
+
101
+ console.log("Raw AI Content (Excerpt):", aiContent);
102
+
103
+ // 4. Parse Actions
104
+ const actions = parseActions(aiContent);
105
+ if (actions.length === 0) {
106
+ console.log("No actions parsed from AI response.");
107
+ }
108
+
109
+ console.log("Actions:", actions);
110
+ // 5. Execute Actions
111
+ for (const action of actions) {
112
+ const result = await executeAction(page, action);
113
+ console.log("Action result:", result);
114
+ if (result === 'FINISHED') {
115
+ isRunning = false;
116
+ break;
117
+ }
118
+ }
119
+
120
+ // 6. Short wait between cycles
121
+ if (isRunning) {
122
+ await page.waitForTimeout(3000);
123
+ }
124
+ }
125
+ return { status: 'success', message: 'Task completed', messages: conversationHistory };
126
+
127
+ } catch (error) {
128
+ console.error("Runtime Custom Error:", error);
129
+ return { status: 'error', message: error.message };
130
+ } finally {
131
+ console.log("Closing browser...");
132
+ await browser.close();
133
+ }
134
+ }
135
+
136
+ export {
137
+ runAgent
138
+ };