@loadmill/droid-cua 2.8.0 → 2.9.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/README.md +7 -4
- package/build/index.js +86 -63
- package/build/src/browser/playwright/connection.js +13 -2
- package/build/src/cli/headless-execution-config.js +4 -36
- package/build/src/cli/ink-shell.js +5 -2
- package/build/src/commands/help.js +2 -3
- package/build/src/commands/loadmill.js +10 -1
- package/build/src/commands/run.js +17 -28
- package/build/src/core/app-context.js +57 -33
- package/build/src/core/context-optimization.js +1 -1
- package/build/src/core/cua-models.js +9 -0
- package/build/src/core/execution-engine.js +2 -8
- package/build/src/core/runtime-config.js +96 -0
- package/build/src/device/loadmill.js +15 -9
- package/build/src/device/openai.js +67 -139
- package/build/src/integrations/loadmill/client.js +149 -131
- package/build/src/integrations/loadmill/executor.js +17 -11
- package/build/src/integrations/loadmill/index.js +1 -1
- package/build/src/integrations/loadmill/interpreter.js +4 -14
- package/build/src/modes/design-mode-ink.js +4 -3
- package/build/src/modes/design-mode.js +4 -3
- package/build/src/modes/execution-mode.js +26 -0
- package/build/src/prompts/execution.js +8 -106
- package/build/src/utils/console-output.js +8 -0
- package/build/src/utils/cua-debug-tracer.js +0 -1
- package/build/src/utils/structured-debug-log-manager.js +4 -1
- package/package.json +1 -1
|
@@ -20,153 +20,171 @@ function normalizeApiBaseUrl(rawBaseUrl) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* @param {{
|
|
24
|
+
* token?: string | null,
|
|
25
|
+
* baseUrl?: string | null,
|
|
26
|
+
* onUnauthorized?: (() => Promise<unknown> | unknown) | null,
|
|
27
|
+
* }} [options]
|
|
25
28
|
*/
|
|
26
|
-
export function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
export function getBaseUrl() {
|
|
34
|
-
return normalizeApiBaseUrl(process.env.LOADMILL_BASE_URL);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Make an authenticated request to Loadmill API
|
|
38
|
-
* @param {string} endpoint - API endpoint (without base URL)
|
|
39
|
-
* @param {Object} options - Fetch options
|
|
40
|
-
* @returns {Promise<Object>}
|
|
41
|
-
*/
|
|
42
|
-
async function apiRequest(endpoint, options = {}) {
|
|
43
|
-
const token = getApiToken();
|
|
44
|
-
if (!token) {
|
|
45
|
-
throw new Error("LOADMILL_API_TOKEN environment variable is not set");
|
|
29
|
+
export function createLoadmillApiClient({ token = null, baseUrl = null, onUnauthorized = null, } = {}) {
|
|
30
|
+
function getApiToken() {
|
|
31
|
+
return typeof token === "string" && token.trim() ? token.trim() : null;
|
|
32
|
+
}
|
|
33
|
+
function getBaseUrl() {
|
|
34
|
+
return normalizeApiBaseUrl(baseUrl);
|
|
46
35
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
36
|
+
async function apiRequest(endpoint, options = {}) {
|
|
37
|
+
const resolvedToken = getApiToken();
|
|
38
|
+
if (!resolvedToken) {
|
|
39
|
+
throw new Error("LOADMILL_API_TOKEN environment variable is not set");
|
|
40
|
+
}
|
|
41
|
+
const resolvedBaseUrl = getBaseUrl();
|
|
42
|
+
const url = `${resolvedBaseUrl}${endpoint}`;
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
...options,
|
|
45
|
+
headers: {
|
|
46
|
+
"Authorization": `Bearer ${resolvedToken}`,
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
...options.headers,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if ((response.status === 401 || response.status === 403) && typeof onUnauthorized === "function") {
|
|
52
|
+
await onUnauthorized();
|
|
53
|
+
}
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const errorText = await response.text();
|
|
56
|
+
throw new Error(`Loadmill API error (${response.status}): ${errorText}`);
|
|
61
57
|
}
|
|
58
|
+
return response.json();
|
|
62
59
|
}
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
60
|
+
async function searchFlows(searchQuery, options = {}) {
|
|
61
|
+
const { limit = 10 } = options;
|
|
62
|
+
const encodedQuery = encodeURIComponent(searchQuery);
|
|
63
|
+
const result = await apiRequest(`/test-flows?search=${encodedQuery}&limit=${limit}`);
|
|
64
|
+
if (Array.isArray(result)) {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
if (result && Array.isArray(result.flows)) {
|
|
68
|
+
return result.flows;
|
|
69
|
+
}
|
|
70
|
+
if (result && Array.isArray(result.data)) {
|
|
71
|
+
return result.data;
|
|
72
|
+
}
|
|
73
|
+
if (result && Array.isArray(result.items)) {
|
|
74
|
+
return result.items;
|
|
75
|
+
}
|
|
76
|
+
if (result && Array.isArray(result.testFlows)) {
|
|
77
|
+
return result.testFlows;
|
|
78
|
+
}
|
|
79
|
+
console.error("[Loadmill] Unexpected API response format:", JSON.stringify(result, null, 2));
|
|
80
|
+
return [];
|
|
66
81
|
}
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const result = await apiRequest(`/test-flows?search=${encodedQuery}&limit=${limit}`);
|
|
80
|
-
// Handle different response formats
|
|
81
|
-
if (Array.isArray(result)) {
|
|
82
|
-
return result;
|
|
82
|
+
async function runTestSuite(suiteId, { flowIds = [], parameters = {} } = {}) {
|
|
83
|
+
const body = {};
|
|
84
|
+
if (flowIds.length > 0) {
|
|
85
|
+
body.flowIds = flowIds;
|
|
86
|
+
}
|
|
87
|
+
if (Object.keys(parameters).length > 0) {
|
|
88
|
+
body.parameters = parameters;
|
|
89
|
+
}
|
|
90
|
+
return await apiRequest(`/test-suites/${suiteId}/run`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: JSON.stringify(body),
|
|
93
|
+
});
|
|
83
94
|
}
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
async function runTestFlow(flowId, suiteId, { parameters = {} } = {}) {
|
|
96
|
+
const body = {
|
|
97
|
+
flows: [flowId],
|
|
98
|
+
inlineParameterOverride: true,
|
|
99
|
+
sharedFlowVersionOverrides: [],
|
|
100
|
+
};
|
|
101
|
+
if (Object.keys(parameters).length > 0) {
|
|
102
|
+
body.overrideParameters = parameters;
|
|
103
|
+
}
|
|
104
|
+
return await apiRequest(`/test-suites/${suiteId}/run?ui=true`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: JSON.stringify(body),
|
|
107
|
+
});
|
|
86
108
|
}
|
|
87
|
-
|
|
88
|
-
return
|
|
109
|
+
async function getTestRunStatus(testRunId) {
|
|
110
|
+
return await apiRequest(`/test-suites-runs/${testRunId}`);
|
|
89
111
|
}
|
|
90
|
-
|
|
91
|
-
return
|
|
112
|
+
async function getFlowRunStatus(flowRunId) {
|
|
113
|
+
return await apiRequest(`/test-flows-runs/${flowRunId}`);
|
|
92
114
|
}
|
|
93
|
-
|
|
94
|
-
return
|
|
115
|
+
async function explainFailure(testRunId) {
|
|
116
|
+
return await apiRequest(`/explain-failures`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
testRunId,
|
|
120
|
+
testRunType: "flowRun"
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
95
123
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
124
|
+
return {
|
|
125
|
+
getApiToken,
|
|
126
|
+
getBaseUrl,
|
|
127
|
+
searchFlows,
|
|
128
|
+
runTestSuite,
|
|
129
|
+
runTestFlow,
|
|
130
|
+
getTestRunStatus,
|
|
131
|
+
getFlowRunStatus,
|
|
132
|
+
explainFailure,
|
|
133
|
+
};
|
|
99
134
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* @param {Object} options.parameters - Parameters to pass to the flows
|
|
106
|
-
* @returns {Promise<Object>} - Run result with runId
|
|
107
|
-
*/
|
|
108
|
-
export async function runTestSuite(suiteId, { flowIds = [], parameters = {} } = {}) {
|
|
109
|
-
const body = {};
|
|
110
|
-
if (flowIds.length > 0) {
|
|
111
|
-
body.flowIds = flowIds;
|
|
112
|
-
}
|
|
113
|
-
if (Object.keys(parameters).length > 0) {
|
|
114
|
-
body.parameters = parameters;
|
|
115
|
-
}
|
|
116
|
-
return apiRequest(`/test-suites/${suiteId}/run`, {
|
|
117
|
-
method: "POST",
|
|
118
|
-
body: JSON.stringify(body),
|
|
119
|
-
});
|
|
135
|
+
export function getApiToken() {
|
|
136
|
+
return createLoadmillApiClient({
|
|
137
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
138
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
139
|
+
}).getApiToken();
|
|
120
140
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
141
|
+
export function getBaseUrl() {
|
|
142
|
+
return createLoadmillApiClient({
|
|
143
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
144
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
145
|
+
}).getBaseUrl();
|
|
146
|
+
}
|
|
147
|
+
export async function searchFlows(searchQuery, options = {}) {
|
|
148
|
+
// Legacy env-based wrapper kept for older call sites. New execution flows
|
|
149
|
+
// should pass an explicit client created from resolved run credentials.
|
|
150
|
+
return await createLoadmillApiClient({
|
|
151
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
152
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
153
|
+
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
154
|
+
}).searchFlows(searchQuery, options);
|
|
155
|
+
}
|
|
156
|
+
export async function runTestSuite(suiteId, options = {}) {
|
|
157
|
+
return await createLoadmillApiClient({
|
|
158
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
159
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
160
|
+
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
161
|
+
}).runTestSuite(suiteId, options);
|
|
162
|
+
}
|
|
163
|
+
export async function runTestFlow(flowId, suiteId, options = {}) {
|
|
164
|
+
return await createLoadmillApiClient({
|
|
165
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
166
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
167
|
+
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
168
|
+
}).runTestFlow(flowId, suiteId, options);
|
|
142
169
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Get the status of a test run
|
|
145
|
-
* @param {string} testRunId - Test run ID
|
|
146
|
-
* @returns {Promise<Object>} - Test run status
|
|
147
|
-
*/
|
|
148
170
|
export async function getTestRunStatus(testRunId) {
|
|
149
|
-
return
|
|
171
|
+
return await createLoadmillApiClient({
|
|
172
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
173
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
174
|
+
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
175
|
+
}).getTestRunStatus(testRunId);
|
|
150
176
|
}
|
|
151
|
-
/**
|
|
152
|
-
* Get the status of a flow run
|
|
153
|
-
* @param {string} flowRunId - Flow run ID
|
|
154
|
-
* @returns {Promise<Object>} - Flow run status
|
|
155
|
-
*/
|
|
156
177
|
export async function getFlowRunStatus(flowRunId) {
|
|
157
|
-
return
|
|
178
|
+
return await createLoadmillApiClient({
|
|
179
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
180
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
181
|
+
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
182
|
+
}).getFlowRunStatus(flowRunId);
|
|
158
183
|
}
|
|
159
|
-
/**
|
|
160
|
-
* Get AI-generated explanation for a failed test run
|
|
161
|
-
* @param {string} testRunId - The flow run ID (from testSuiteFlowRuns[].id)
|
|
162
|
-
* @returns {Promise<Object>} - Explanation with reason, suggestion, etc.
|
|
163
|
-
*/
|
|
164
184
|
export async function explainFailure(testRunId) {
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}),
|
|
171
|
-
});
|
|
185
|
+
return await createLoadmillApiClient({
|
|
186
|
+
token: process.env.LOADMILL_API_TOKEN,
|
|
187
|
+
baseUrl: process.env.LOADMILL_BASE_URL,
|
|
188
|
+
onUnauthorized: globalThis.__DROID_CUA_HANDLE_LOADMILL_UNAUTHORIZED__,
|
|
189
|
+
}).explainFailure(testRunId);
|
|
172
190
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Orchestrates Loadmill flow execution
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { createLoadmillApiClient } from "./client.js";
|
|
5
5
|
import { interpretLoadmillCommand, selectBestFlow } from "./interpreter.js";
|
|
6
6
|
import { logger } from "../../utils/logger.js";
|
|
7
7
|
const POLL_INTERVAL_MS = 5000; // 5 seconds
|
|
@@ -20,11 +20,11 @@ function sleep(ms) {
|
|
|
20
20
|
* @param {Function} onStatusUpdate - Callback for status updates
|
|
21
21
|
* @returns {Promise<Object>} - Final run status
|
|
22
22
|
*/
|
|
23
|
-
async function pollForCompletion(runId, onStatusUpdate = () => { }) {
|
|
23
|
+
async function pollForCompletion(runId, loadmillClient, onStatusUpdate = () => { }) {
|
|
24
24
|
const startTime = Date.now();
|
|
25
25
|
while (Date.now() - startTime < TIMEOUT_MS) {
|
|
26
26
|
try {
|
|
27
|
-
const status = await getTestRunStatus(runId);
|
|
27
|
+
const status = await loadmillClient.getTestRunStatus(runId);
|
|
28
28
|
onStatusUpdate(status);
|
|
29
29
|
// Check if run is complete
|
|
30
30
|
if (status.status === "PASSED" || status.status === "passed") {
|
|
@@ -36,7 +36,7 @@ async function pollForCompletion(runId, onStatusUpdate = () => { }) {
|
|
|
36
36
|
try {
|
|
37
37
|
const failedFlowRun = status.testSuiteFlowRuns?.find(f => f.status === "FAILED");
|
|
38
38
|
if (failedFlowRun) {
|
|
39
|
-
const explanation = await explainFailure(failedFlowRun.id);
|
|
39
|
+
const explanation = await loadmillClient.explainFailure(failedFlowRun.id);
|
|
40
40
|
if (explanation?.reason) {
|
|
41
41
|
failureReason = explanation.reason;
|
|
42
42
|
}
|
|
@@ -69,22 +69,28 @@ async function pollForCompletion(runId, onStatusUpdate = () => { }) {
|
|
|
69
69
|
* @returns {Promise<{success: boolean, error?: string, result?: Object}>}
|
|
70
70
|
*/
|
|
71
71
|
export async function executeLoadmillCommand(userInput, options = {}) {
|
|
72
|
-
const { onProgress = () => { } } = options;
|
|
72
|
+
const { onProgress = () => { }, loadmillClient = createLoadmillApiClient(), openAiClient = null, } = options;
|
|
73
73
|
// Check for API token
|
|
74
|
-
if (!getApiToken()) {
|
|
74
|
+
if (!loadmillClient.getApiToken()) {
|
|
75
75
|
return {
|
|
76
76
|
success: false,
|
|
77
77
|
error: "LOADMILL_API_TOKEN environment variable is not set. Please set it in your .env file."
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
if (!openAiClient) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: "OpenAI API key is not configured."
|
|
84
|
+
};
|
|
85
|
+
}
|
|
80
86
|
try {
|
|
81
87
|
// Step 1: Interpret the command
|
|
82
88
|
onProgress({ step: "interpreting", message: "Interpreting command..." });
|
|
83
|
-
const interpreted = await interpretLoadmillCommand(userInput);
|
|
89
|
+
const interpreted = await interpretLoadmillCommand(userInput, { client: openAiClient });
|
|
84
90
|
logger.debug("Loadmill command interpreted", interpreted);
|
|
85
91
|
// Step 2: Search for flows
|
|
86
92
|
onProgress({ step: "searching", message: `Searching for flows matching "${interpreted.searchQuery}"...` });
|
|
87
|
-
const flows = await searchFlows(interpreted.searchQuery);
|
|
93
|
+
const flows = await loadmillClient.searchFlows(interpreted.searchQuery);
|
|
88
94
|
// Ensure flows is an array
|
|
89
95
|
if (!Array.isArray(flows) || flows.length === 0) {
|
|
90
96
|
return {
|
|
@@ -94,7 +100,7 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
94
100
|
}
|
|
95
101
|
// Step 3: Select best match
|
|
96
102
|
onProgress({ step: "selecting", message: `Found ${flows.length} flow(s). Selecting best match...` });
|
|
97
|
-
const { selectedFlow, confidence } = await selectBestFlow(flows, interpreted.searchQuery);
|
|
103
|
+
const { selectedFlow, confidence } = await selectBestFlow(flows, interpreted.searchQuery, { client: openAiClient });
|
|
98
104
|
if (!selectedFlow) {
|
|
99
105
|
return {
|
|
100
106
|
success: false,
|
|
@@ -119,7 +125,7 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
119
125
|
step: "running",
|
|
120
126
|
message: `Running flow "${flowName}" (confidence: ${(confidence * 100).toFixed(0)}%)...`
|
|
121
127
|
});
|
|
122
|
-
const runResult = await runTestFlow(selectedFlow.id, selectedFlow.testSuiteId, {
|
|
128
|
+
const runResult = await loadmillClient.runTestFlow(selectedFlow.id, selectedFlow.testSuiteId, {
|
|
123
129
|
parameters: interpreted.parameters
|
|
124
130
|
});
|
|
125
131
|
const runId = runResult.testSuiteRunId || runResult.id || runResult.runId;
|
|
@@ -131,7 +137,7 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
131
137
|
}
|
|
132
138
|
// Step 5: Poll for completion
|
|
133
139
|
onProgress({ step: "polling", message: `Test started (ID: ${runId}). Waiting for completion...`, runId });
|
|
134
|
-
const finalResult = await pollForCompletion(runId, (status) => {
|
|
140
|
+
const finalResult = await pollForCompletion(runId, loadmillClient, (status) => {
|
|
135
141
|
onProgress({ step: "polling", message: `Status: ${status.status}...`, runId });
|
|
136
142
|
});
|
|
137
143
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Loadmill integration public exports
|
|
3
3
|
*/
|
|
4
|
-
export { getApiToken, searchFlows, runTestFlow, getFlowRunStatus } from "./client.js";
|
|
4
|
+
export { createLoadmillApiClient, getApiToken, searchFlows, runTestFlow, getFlowRunStatus } from "./client.js";
|
|
5
5
|
export { interpretLoadmillCommand, selectBestFlow } from "./interpreter.js";
|
|
6
6
|
export { executeLoadmillCommand } from "./executor.js";
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI-powered text interpretation for Loadmill commands
|
|
3
3
|
*/
|
|
4
|
-
import OpenAI from "openai";
|
|
5
4
|
import { buildLoadmillCommandInterpretationMessages, buildLoadmillFlowSelectionMessages, } from "../../prompts/loadmill.js";
|
|
6
|
-
let openai = null;
|
|
7
|
-
function getOpenAI() {
|
|
8
|
-
if (!openai) {
|
|
9
|
-
openai = new OpenAI({
|
|
10
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
return openai;
|
|
14
|
-
}
|
|
15
5
|
/**
|
|
16
6
|
* Interpret a natural language Loadmill command into structured data
|
|
17
7
|
* @param {string} userInput - Natural language command
|
|
18
8
|
* @returns {Promise<{searchQuery: string, parameters: Object, action: 'run'|'search'}>}
|
|
19
9
|
*/
|
|
20
|
-
export async function interpretLoadmillCommand(userInput) {
|
|
21
|
-
const response = await
|
|
10
|
+
export async function interpretLoadmillCommand(userInput, { client }) {
|
|
11
|
+
const response = await client.chat.completions.create({
|
|
22
12
|
model: "gpt-4o-mini",
|
|
23
13
|
messages: buildLoadmillCommandInterpretationMessages(userInput),
|
|
24
14
|
response_format: { type: "json_object" }
|
|
@@ -37,7 +27,7 @@ export async function interpretLoadmillCommand(userInput) {
|
|
|
37
27
|
* @param {string} originalQuery - Original user query
|
|
38
28
|
* @returns {Promise<{selectedFlow: Object|null, confidence: number}>}
|
|
39
29
|
*/
|
|
40
|
-
export async function selectBestFlow(flows, originalQuery) {
|
|
30
|
+
export async function selectBestFlow(flows, originalQuery, { client }) {
|
|
41
31
|
// Ensure flows is an array
|
|
42
32
|
if (!flows || !Array.isArray(flows) || flows.length === 0) {
|
|
43
33
|
return { selectedFlow: null, confidence: 0 };
|
|
@@ -51,7 +41,7 @@ export async function selectBestFlow(flows, originalQuery) {
|
|
|
51
41
|
const suite = f.testSuiteDescription || "";
|
|
52
42
|
return `${i + 1}. ID: ${f.id}, Name: "${name}"${suite ? `, Suite: "${suite}"` : ""}`;
|
|
53
43
|
}).join("\n");
|
|
54
|
-
const response = await
|
|
44
|
+
const response = await client.chat.completions.create({
|
|
55
45
|
model: "gpt-4o-mini",
|
|
56
46
|
messages: buildLoadmillFlowSelectionMessages(originalQuery, flowList),
|
|
57
47
|
response_format: { type: "json_object" }
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getScreenshotAsBase64 } from "../device/connection.js";
|
|
2
|
-
import { sendCUARequest, reviseTestScript } from "../device/openai.js";
|
|
3
2
|
import { buildDesignModePrompt, buildDesignRecoveryPrompt } from "../core/prompts.js";
|
|
4
3
|
import { saveTest } from "../test-store/test-manager.js";
|
|
5
4
|
import { logger } from "../utils/logger.js";
|
|
@@ -23,6 +22,8 @@ export class DesignModeInk {
|
|
|
23
22
|
this.baseDesignPrompt = null;
|
|
24
23
|
this.consecutiveErrorCount = 0;
|
|
25
24
|
this.maxConsecutiveErrors = 3;
|
|
25
|
+
this.sendCUARequest = executionEngine.sendCUARequest;
|
|
26
|
+
this.reviseTestScript = executionEngine.reviseTestScript;
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Start design mode conversation
|
|
@@ -183,7 +184,7 @@ export class DesignModeInk {
|
|
|
183
184
|
}
|
|
184
185
|
// Get screenshot and send to model
|
|
185
186
|
const screenshotBase64 = await getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
186
|
-
const response = await sendCUARequest({
|
|
187
|
+
const response = await this.sendCUARequest({
|
|
187
188
|
messages: this.session.messages,
|
|
188
189
|
screenshotBase64,
|
|
189
190
|
previousResponseId: this.session.previousResponseId,
|
|
@@ -271,7 +272,7 @@ export class DesignModeInk {
|
|
|
271
272
|
const revision = confirm;
|
|
272
273
|
// Use simple chat completion for revision instead of CUA
|
|
273
274
|
addOutput({ type: 'info', text: 'Revising test script...' });
|
|
274
|
-
currentScript = await reviseTestScript(currentScript, revision);
|
|
275
|
+
currentScript = await this.reviseTestScript(currentScript, revision);
|
|
275
276
|
// Display the revised script
|
|
276
277
|
addOutput({ type: 'info', text: '' });
|
|
277
278
|
addOutput({ type: 'system', text: '=== Generated Test Script ===' });
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import readline from "readline";
|
|
2
2
|
import { getScreenshotAsBase64 } from "../device/connection.js";
|
|
3
|
-
import { sendCUARequest, reviseTestScript } from "../device/openai.js";
|
|
4
3
|
import { buildDesignModePrompt, buildDesignRecoveryPrompt } from "../core/prompts.js";
|
|
5
4
|
import { saveTest } from "../test-store/test-manager.js";
|
|
6
5
|
import { logger } from "../utils/logger.js";
|
|
@@ -20,6 +19,8 @@ export class DesignMode {
|
|
|
20
19
|
this.baseDesignPrompt = null;
|
|
21
20
|
this.consecutiveErrorCount = 0;
|
|
22
21
|
this.maxConsecutiveErrors = 3;
|
|
22
|
+
this.sendCUARequest = executionEngine.sendCUARequest;
|
|
23
|
+
this.reviseTestScript = executionEngine.reviseTestScript;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Start design mode conversation
|
|
@@ -147,7 +148,7 @@ export class DesignMode {
|
|
|
147
148
|
const keypressHandler = this.setupEscDetection();
|
|
148
149
|
// Get screenshot and send to model
|
|
149
150
|
const screenshotBase64 = await getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
150
|
-
const response = await sendCUARequest({
|
|
151
|
+
const response = await this.sendCUARequest({
|
|
151
152
|
messages: this.session.messages,
|
|
152
153
|
screenshotBase64,
|
|
153
154
|
previousResponseId: this.session.previousResponseId,
|
|
@@ -262,7 +263,7 @@ export class DesignMode {
|
|
|
262
263
|
const revision = confirm;
|
|
263
264
|
// Use simple chat completion for revision instead of CUA
|
|
264
265
|
console.log("Revising test script...");
|
|
265
|
-
currentScript = await reviseTestScript(currentScript, revision);
|
|
266
|
+
currentScript = await this.reviseTestScript(currentScript, revision);
|
|
266
267
|
// Display the revised script
|
|
267
268
|
console.log("\n=== Generated Test Script ===");
|
|
268
269
|
console.log(currentScript);
|
|
@@ -228,6 +228,14 @@ export class ExecutionMode {
|
|
|
228
228
|
}
|
|
229
229
|
const navigationTarget = extractNavigateTarget(instruction);
|
|
230
230
|
this.session.addToTranscript(`[Navigate] ${navigationTarget}`);
|
|
231
|
+
emitDesktopDebug("runner.navigate.started", "execution", {
|
|
232
|
+
runId: context?.runId,
|
|
233
|
+
stepId: stepContext?.stepId,
|
|
234
|
+
instructionIndex: stepContext?.instructionIndex
|
|
235
|
+
}, {
|
|
236
|
+
instruction,
|
|
237
|
+
target: navigationTarget
|
|
238
|
+
});
|
|
231
239
|
this.emit(addOutput, "system", `Navigating to ${navigationTarget}...`, context, stepContext, {
|
|
232
240
|
eventType: "system_message",
|
|
233
241
|
payload: {
|
|
@@ -250,6 +258,15 @@ export class ExecutionMode {
|
|
|
250
258
|
navigatedUrl: this.session.currentUrl,
|
|
251
259
|
});
|
|
252
260
|
this.session.installRecoverySystemPrompt(navigationRecoveryPrompt);
|
|
261
|
+
emitDesktopDebug("runner.navigate.completed", "execution", {
|
|
262
|
+
runId: context?.runId,
|
|
263
|
+
stepId: stepContext?.stepId,
|
|
264
|
+
instructionIndex: stepContext?.instructionIndex
|
|
265
|
+
}, {
|
|
266
|
+
instruction,
|
|
267
|
+
target: navigationTarget,
|
|
268
|
+
url: normalizedUrl
|
|
269
|
+
});
|
|
253
270
|
this.emit(addOutput, "success", `Navigation complete: ${normalizedUrl}`, context, stepContext, {
|
|
254
271
|
eventType: "system_message",
|
|
255
272
|
payload: {
|
|
@@ -266,6 +283,15 @@ export class ExecutionMode {
|
|
|
266
283
|
retryCount,
|
|
267
284
|
message,
|
|
268
285
|
});
|
|
286
|
+
emitDesktopDebug("runner.navigate.failed", "execution", {
|
|
287
|
+
runId: context?.runId,
|
|
288
|
+
stepId: stepContext?.stepId,
|
|
289
|
+
instructionIndex: stepContext?.instructionIndex
|
|
290
|
+
}, {
|
|
291
|
+
instruction,
|
|
292
|
+
target: navigationTarget,
|
|
293
|
+
message
|
|
294
|
+
});
|
|
269
295
|
this.emit(addOutput, "error", `Navigation failed: ${message}`, context, stepContext, {
|
|
270
296
|
eventType: "error",
|
|
271
297
|
payload: {
|