testronaut 1.0.0 → 1.0.2
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/package.json +1 -1
- package/agent-framework/bin/agent +0 -16
- package/agent-framework/core/browser.js +0 -43
- package/agent-framework/core/llm.js +0 -0
- package/agent-framework/core/tools.js +0 -9
- package/agent-framework/index.js +0 -6
- package/agent-framework/runner/agentRunner.js +0 -26
- package/agent-framework/runner/testronaut.js +0 -16
- package/agent-framework/tests/testRunner.js +0 -13
- package/agent-framework/workflows/loginWorkflow.js +0 -10
- package/missions/chat.mission.js +0 -26
- package/missions/dashboard.mission.js +0 -19
- package/missions/login.mission.js +0 -19
- package/missions/logout.mission.js +0 -29
- package/missions/startPublicChat.mission.js +0 -20
- package/old/agent.js +0 -230
- package/old/agent2.js +0 -263
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { runWorkflow } from '../runner/agentRunner.js';
|
|
3
|
-
import { loginWorkflow } from '../workflows/loginWorkflow.js';
|
|
4
|
-
|
|
5
|
-
const workflows = {
|
|
6
|
-
login: loginWorkflow,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const name = process.argv[2];
|
|
10
|
-
if (!name || !workflows[name]) {
|
|
11
|
-
console.log('Usage: agent <workflow>');
|
|
12
|
-
console.log('Available workflows:', Object.keys(workflows).join(', '));
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
await runWorkflow(workflows[name]);
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { chromium } from 'playwright';
|
|
2
|
-
import * as cheerio from 'cheerio';
|
|
3
|
-
|
|
4
|
-
export class Browser {
|
|
5
|
-
async start() {
|
|
6
|
-
this.browser = await chromium.launch({ headless: true });
|
|
7
|
-
this.page = await this.browser.newPage();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async navigate({ url }) {
|
|
11
|
-
await this.page.goto(url);
|
|
12
|
-
return `Navigated to ${url}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async fill({ selector, text }) {
|
|
16
|
-
await this.page.waitForSelector(selector);
|
|
17
|
-
await this.page.fill(selector, text);
|
|
18
|
-
return `Filled ${selector}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async click({ selector, delayMs = 2000 }) {
|
|
22
|
-
await this.page.waitForSelector(selector);
|
|
23
|
-
await this.page.click(selector);
|
|
24
|
-
await this.page.waitForTimeout(delayMs);
|
|
25
|
-
return `Clicked ${selector}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async get_dom({ limit = 50000 }) {
|
|
29
|
-
const html = await this.page.content();
|
|
30
|
-
const $ = cheerio.load(html);
|
|
31
|
-
$('script, style, meta, link, noscript, iframe, canvas, svg').remove();
|
|
32
|
-
return $.html().slice(0, limit);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async check_text({ text }) {
|
|
36
|
-
const content = await this.page.content();
|
|
37
|
-
return content.includes(text) ? `FOUND: ${text}` : `NOT FOUND: ${text}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async close() {
|
|
41
|
-
await this.browser.close();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
File without changes
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Browser } from './browser.js';
|
|
2
|
-
|
|
3
|
-
export const TOOL_MAP = {
|
|
4
|
-
navigate: (browser, args) => browser.navigate(args),
|
|
5
|
-
fill: (browser, args) => browser.fill(args),
|
|
6
|
-
click: (browser, args) => browser.click(args),
|
|
7
|
-
get_dom: (browser, args) => browser.get_dom(args),
|
|
8
|
-
check_text: (browser, args) => browser.check_text(args),
|
|
9
|
-
};
|
package/agent-framework/index.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Browser } from '../core/browser.js';
|
|
2
|
-
import { TOOL_MAP } from '../core/tools.js';
|
|
3
|
-
|
|
4
|
-
export async function runWorkflow(workflow) {
|
|
5
|
-
const browser = new Browser();
|
|
6
|
-
await browser.start();
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
for (const step of workflow.steps) {
|
|
10
|
-
const fn = TOOL_MAP[step.type];
|
|
11
|
-
if (!fn) throw new Error(`Unsupported step: ${step.type}`);
|
|
12
|
-
|
|
13
|
-
const result = await fn(browser, step);
|
|
14
|
-
console.log(`[${step.type}] → ${result}`);
|
|
15
|
-
|
|
16
|
-
if (step.expect && !result.includes('FOUND')) {
|
|
17
|
-
throw new Error(`Expectation failed: ${step.text}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
console.log('\n✅ Workflow complete: SUCCESS');
|
|
21
|
-
} catch (err) {
|
|
22
|
-
console.error('\n❌ Workflow failed:', err.message);
|
|
23
|
-
} finally {
|
|
24
|
-
await browser.close();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { runSuite } from './suiteRunner.js';
|
|
2
|
-
|
|
3
|
-
const objectives = [];
|
|
4
|
-
|
|
5
|
-
export function mission(name, fn) {
|
|
6
|
-
console.log(`\n🚀 Mission: ${name}`);
|
|
7
|
-
fn();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function objective(goalDescription, workflow) {
|
|
11
|
-
objectives.push({ name: goalDescription, workflow });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function launch(preMissionBootstrap) {
|
|
15
|
-
await runSuite(objectives, preMissionBootstrap);
|
|
16
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { runWorkflow } from '../runner/agentRunner.js';
|
|
2
|
-
import { loginWorkflow } from '../workflows/loginWorkflow.js';
|
|
3
|
-
|
|
4
|
-
const workflows = [
|
|
5
|
-
{ name: 'Login Workflow', wf: loginWorkflow },
|
|
6
|
-
];
|
|
7
|
-
|
|
8
|
-
for (const { name, wf } of workflows) {
|
|
9
|
-
console.log(`\n=== Running test: ${name} ===`);
|
|
10
|
-
await runWorkflow(wf);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
console.log('\n✅ All tests complete');
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export const loginWorkflow = {
|
|
2
|
-
goal: 'Login to Collegium and verify dashboard access',
|
|
3
|
-
steps: [
|
|
4
|
-
{ type: 'navigate', url: 'https://staging.collegiumbuilt.com/login' },
|
|
5
|
-
{ type: 'fill', selector: '#\:r0\:', text: 'owner@collegiumbuilt.com' },
|
|
6
|
-
{ type: 'fill', selector: '#auth-login-v2-password', text: 'letmein123' },
|
|
7
|
-
{ type: 'click', selector: "button[type='submit']", delayMs: 3000 },
|
|
8
|
-
{ type: 'check_text', text: 'Dashboard', expect: true },
|
|
9
|
-
],
|
|
10
|
-
};
|
package/missions/chat.mission.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { runMissions } from '../runner/testronaut.js';
|
|
2
|
-
import { loginMission } from './login.mission.js';
|
|
3
|
-
import { logoutMission } from './logout.mission.js';
|
|
4
|
-
import { startPublicChatMission } from './startPublicChat.mission.js';
|
|
5
|
-
|
|
6
|
-
export const chatMission =
|
|
7
|
-
`Input the text "give me the main categories for compliance for AER" into the text field with the placeholder text "Chat with an AI".
|
|
8
|
-
Then submit the message.
|
|
9
|
-
You will likely need to wait several seconds for a response (about 30 seconds).
|
|
10
|
-
The response will appear in the text response section of the page.
|
|
11
|
-
the response will be a thoughtfull response from an LLM.
|
|
12
|
-
Check if this response was sent and if it was related to the question asked.
|
|
13
|
-
If found, report SUCCESS. Otherwise, report FAILURE.`
|
|
14
|
-
|
|
15
|
-
export async function executeDashboardMission() {
|
|
16
|
-
await runMissions({
|
|
17
|
-
preMission: [loginMission, startPublicChatMission],
|
|
18
|
-
mission: chatMission,
|
|
19
|
-
postMission: logoutMission,
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
25
|
-
executeDashboardMission();
|
|
26
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { runMissions } from '../runner/testronaut.js';
|
|
2
|
-
import { loginMission } from './login.mission.js';
|
|
3
|
-
import { logoutMission } from './logout.mission.js';
|
|
4
|
-
|
|
5
|
-
export const dashboardMission = `use 'check_text' to verify that "${process.env.AFTER_LOGIN_CHECK}" appears on the page.
|
|
6
|
-
If found, report SUCCESS. Otherwise, report FAILURE.`
|
|
7
|
-
|
|
8
|
-
export async function executeDashboardMission() {
|
|
9
|
-
await runMissions({
|
|
10
|
-
preMission: loginMission,
|
|
11
|
-
mission: dashboardMission,
|
|
12
|
-
postMission: logoutMission,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
18
|
-
executeDashboardMission();
|
|
19
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { runMissions } from '../runner/testronaut.js';
|
|
2
|
-
|
|
3
|
-
export const loginMission = `Visit ${process.env.URL}.
|
|
4
|
-
Fill in the username field with ${process.env.USERNAME} and password field with ${process.env.PASSWORD}.
|
|
5
|
-
Then click the button most likely to login the user.
|
|
6
|
-
If the login is successful, confirm that the next page after login is shown by checking for text "${process.env.AFTER_LOGIN_CHECK}".
|
|
7
|
-
If so, report SUCCESS stating the reason why.
|
|
8
|
-
Otherwise, report FAILURE stating the reason why.
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
export async function executeLoginMission() {
|
|
12
|
-
await runMissions({
|
|
13
|
-
mission: loginMission
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
18
|
-
executeLoginMission();
|
|
19
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { runMissions } from '../runner/testronaut.js';
|
|
2
|
-
import { loginMission } from './login.mission.js';
|
|
3
|
-
|
|
4
|
-
export const logoutMission = `
|
|
5
|
-
Locate and click the Logout button or link.
|
|
6
|
-
The selector may be something like [data-testid="logout"] or a visible text node like "Logout" or "Sign out" or hidden under an avatar or username.
|
|
7
|
-
After clicking, use 'check_text' to confirm you've returned to the login screen.
|
|
8
|
-
You may need to use 'expand_menu' on a selector such as '[data-testid="user-dropdown-trigger"]'.
|
|
9
|
-
After logging out, use 'check_text' to verify that "${process.env.AFTER_LOGOUT_CHECK}" appears on the page.
|
|
10
|
-
If found, report SUCCESS. Otherwise, report FAILURE.
|
|
11
|
-
`;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// export const logoutMission = `
|
|
15
|
-
// If the logout button is not visible, expand the user menu by clicking the hamburger icon or profile avatar.
|
|
16
|
-
// Then click the button most likely to logout the user.
|
|
17
|
-
// After logging out, confirm that the login form or login page is shown or confirm that the words "${process.env.AFTER_LOGOUT_CHECK}" appear.
|
|
18
|
-
// If so, report SUCCESS. Otherwise, report FAILURE.`
|
|
19
|
-
|
|
20
|
-
export async function executeLogoutMission() {
|
|
21
|
-
await runMissions({
|
|
22
|
-
preMission: loginMission,
|
|
23
|
-
mission: logoutMission
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
28
|
-
executeLogoutMission();
|
|
29
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { runMissions } from '../runner/testronaut.js';
|
|
2
|
-
import { loginMission } from './login.mission.js';
|
|
3
|
-
import { logoutMission } from './logout.mission.js';
|
|
4
|
-
|
|
5
|
-
export const startPublicChatMission = `Start a chat by clicking the first chat button under the public libraries.
|
|
6
|
-
To confirm if a chat has started a text field with the place holder text "Chat with an AI" should appear.
|
|
7
|
-
If found, report SUCCESS. Otherwise, report FAILURE.`
|
|
8
|
-
|
|
9
|
-
export async function executeDashboardMission() {
|
|
10
|
-
await runMissions({
|
|
11
|
-
preMission: loginMission,
|
|
12
|
-
mission: startPublicChatMission,
|
|
13
|
-
postMission: logoutMission,
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
19
|
-
executeDashboardMission();
|
|
20
|
-
}
|
package/old/agent.js
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
// agent.js — Autonomous web agent with auto DOM refresh between steps
|
|
2
|
-
|
|
3
|
-
import 'dotenv/config';
|
|
4
|
-
import OpenAI from 'openai';
|
|
5
|
-
import { chromium } from 'playwright';
|
|
6
|
-
|
|
7
|
-
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
8
|
-
|
|
9
|
-
class Browser {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.browser = null;
|
|
12
|
-
this.page = null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async start() {
|
|
16
|
-
this.browser = await chromium.launch({ headless: true });
|
|
17
|
-
const context = await this.browser.newContext();
|
|
18
|
-
this.page = await context.newPage();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async navigate({ url }) {
|
|
22
|
-
await this.page.goto(url, { timeout: 30_000 });
|
|
23
|
-
return `navigated to ${this.page.url()}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async fill({ selector, text }) {
|
|
27
|
-
await this.page.waitForSelector(selector, { timeout: 30_000 });
|
|
28
|
-
await this.page.fill(selector, text);
|
|
29
|
-
return `filled ${selector}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async click({ selector }) {
|
|
33
|
-
await this.page.waitForSelector(selector, { timeout: 30_000 });
|
|
34
|
-
await this.page.click(selector);
|
|
35
|
-
return `clicked ${selector}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async get_dom({ limit = 5000 }) {
|
|
39
|
-
const html = await this.page.content();
|
|
40
|
-
return html.slice(0, limit);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async close() {
|
|
44
|
-
await this.browser?.close();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const toolsSchema = [
|
|
49
|
-
{
|
|
50
|
-
type: 'function',
|
|
51
|
-
function: {
|
|
52
|
-
name: 'navigate',
|
|
53
|
-
description: 'Navigate to a URL',
|
|
54
|
-
parameters: {
|
|
55
|
-
type: 'object',
|
|
56
|
-
properties: {
|
|
57
|
-
url: { type: 'string' },
|
|
58
|
-
},
|
|
59
|
-
required: ['url'],
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
type: 'function',
|
|
65
|
-
function: {
|
|
66
|
-
name: 'fill',
|
|
67
|
-
description: 'Fill in a field by CSS selector',
|
|
68
|
-
parameters: {
|
|
69
|
-
type: 'object',
|
|
70
|
-
properties: {
|
|
71
|
-
selector: { type: 'string' },
|
|
72
|
-
text: { type: 'string' },
|
|
73
|
-
},
|
|
74
|
-
required: ['selector', 'text'],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
type: 'function',
|
|
80
|
-
function: {
|
|
81
|
-
name: 'click',
|
|
82
|
-
description: 'Click a button or link by CSS selector',
|
|
83
|
-
parameters: {
|
|
84
|
-
type: 'object',
|
|
85
|
-
properties: {
|
|
86
|
-
selector: { type: 'string' },
|
|
87
|
-
},
|
|
88
|
-
required: ['selector'],
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
type: 'function',
|
|
94
|
-
function: {
|
|
95
|
-
name: 'get_dom',
|
|
96
|
-
description: 'Return trimmed HTML content for GPT to inspect and find selectors',
|
|
97
|
-
parameters: {
|
|
98
|
-
type: 'object',
|
|
99
|
-
properties: {
|
|
100
|
-
limit: { type: 'number', default: 5000 },
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
const TOOL_MAP = {
|
|
108
|
-
navigate: (b, args) => b.navigate(args),
|
|
109
|
-
fill: (b, args) => b.fill(args),
|
|
110
|
-
click: (b, args) => b.click(args),
|
|
111
|
-
get_dom: (b, args) => b.get_dom(args),
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
async function runAgent(goal, maxTurns = 10) {
|
|
115
|
-
const browser = new Browser();
|
|
116
|
-
await browser.start();
|
|
117
|
-
|
|
118
|
-
const messages = [
|
|
119
|
-
{
|
|
120
|
-
role: 'system',
|
|
121
|
-
content: `
|
|
122
|
-
You are an autonomous web agent. Use function calls to complete the user's goal.
|
|
123
|
-
If you are unsure of the selectors for inputs or buttons, call 'get_dom' to retrieve page HTML,
|
|
124
|
-
analyze it, then make your best guess based on labels, names, types, and placeholder values.
|
|
125
|
-
After completing the goal, respond with a final plain-text message starting with SUCCESS or FAILURE.
|
|
126
|
-
`.trim(),
|
|
127
|
-
},
|
|
128
|
-
{ role: 'user', content: goal },
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
for (let turn = 0; turn < maxTurns; turn++) {
|
|
133
|
-
const response = await openai.chat.completions.create({
|
|
134
|
-
model: 'gpt-4o',
|
|
135
|
-
messages,
|
|
136
|
-
tools: toolsSchema,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const msg = response.choices[0].message;
|
|
140
|
-
|
|
141
|
-
if (msg.tool_calls?.length) {
|
|
142
|
-
messages.push(msg); // assistant's intent
|
|
143
|
-
|
|
144
|
-
for (const call of msg.tool_calls) {
|
|
145
|
-
const fnName = call.function.name;
|
|
146
|
-
const args = JSON.parse(call.function.arguments || '{}');
|
|
147
|
-
console.log(`[model] → ${fnName}`, args);
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
let result;
|
|
151
|
-
try {
|
|
152
|
-
result = await TOOL_MAP[fnName](browser, args);
|
|
153
|
-
if (typeof result !== 'string') {
|
|
154
|
-
result = JSON.stringify(result ?? ''); // make sure it's always a string
|
|
155
|
-
}
|
|
156
|
-
} catch (err) {
|
|
157
|
-
result = `ERROR: ${err.message}`;
|
|
158
|
-
}
|
|
159
|
-
console.log(`[tool ] ← ${result}`);
|
|
160
|
-
|
|
161
|
-
messages.push({
|
|
162
|
-
role: 'tool',
|
|
163
|
-
tool_call_id: call.id,
|
|
164
|
-
name: fnName,
|
|
165
|
-
content: result,
|
|
166
|
-
});
|
|
167
|
-
} catch (err) {
|
|
168
|
-
console.error(`[error] ${err.message}`);
|
|
169
|
-
messages.push({
|
|
170
|
-
role: 'tool',
|
|
171
|
-
tool_call_id: call.id,
|
|
172
|
-
name: fnName,
|
|
173
|
-
content: `ERROR: ${err.message}`,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Check if GPT declared final answer
|
|
182
|
-
const final = msg.content?.trim().toLowerCase();
|
|
183
|
-
if (final?.startsWith('success') || final?.startsWith('failure')) {
|
|
184
|
-
console.log('\n┏━ FINAL AGENT RESPONSE ━━━━━━━━━━━━━━━━━━━');
|
|
185
|
-
console.log(msg.content);
|
|
186
|
-
console.log('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Otherwise, auto-inject get_dom so GPT can see the page
|
|
191
|
-
const domCallId = `get_dom_${Date.now()}`;
|
|
192
|
-
const domAssistantMsg = {
|
|
193
|
-
role: 'assistant',
|
|
194
|
-
tool_calls: [
|
|
195
|
-
{
|
|
196
|
-
id: domCallId,
|
|
197
|
-
function: {
|
|
198
|
-
name: 'get_dom',
|
|
199
|
-
arguments: JSON.stringify({ limit: 5000 }),
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
};
|
|
204
|
-
messages.push(domAssistantMsg);
|
|
205
|
-
|
|
206
|
-
const domHtml = await TOOL_MAP.get_dom(browser, { limit: 5000 });
|
|
207
|
-
messages.push({
|
|
208
|
-
role: 'tool',
|
|
209
|
-
tool_call_id: domCallId,
|
|
210
|
-
name: 'get_dom',
|
|
211
|
-
content: domHtml,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
console.log(`[auto] → Injected DOM for next reasoning step`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
console.log('🛑 Agent ran out of turns.');
|
|
218
|
-
} finally {
|
|
219
|
-
await browser.close();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
(async () => {
|
|
224
|
-
await runAgent(`
|
|
225
|
-
Visit https://staging.collegiumbuilt.com/login.
|
|
226
|
-
Log in using owner@collegiumbuilt.com and password C0ll3g1um_Bu1lt!.
|
|
227
|
-
After clicking the login button, if you see the word "Dashboard" on the page, report SUCCESS.
|
|
228
|
-
Otherwise, report FAILURE.
|
|
229
|
-
`);
|
|
230
|
-
})();
|
package/old/agent2.js
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
// agent.js — Autonomous web agent with exclusion-based DOM trimming
|
|
2
|
-
|
|
3
|
-
import 'dotenv/config';
|
|
4
|
-
import OpenAI from 'openai';
|
|
5
|
-
import { chromium } from 'playwright';
|
|
6
|
-
import * as cheerio from 'cheerio';
|
|
7
|
-
|
|
8
|
-
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
9
|
-
|
|
10
|
-
class Browser {
|
|
11
|
-
constructor() {
|
|
12
|
-
this.browser = null;
|
|
13
|
-
this.page = null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async start() {
|
|
17
|
-
this.browser = await chromium.launch({ headless: true });
|
|
18
|
-
const context = await this.browser.newContext();
|
|
19
|
-
this.page = await context.newPage();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async navigate({ url }) {
|
|
23
|
-
await this.page.goto(url, { timeout: 30_000 });
|
|
24
|
-
return `navigated to ${this.page.url()}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async fill({ selector, text }) {
|
|
28
|
-
await this.page.waitForSelector(selector, { timeout: 30_000 });
|
|
29
|
-
await this.page.fill(selector, text);
|
|
30
|
-
return `filled ${selector}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async click({ selector, waitFor = 'load' }) {
|
|
34
|
-
await this.page.waitForSelector(selector, { timeout: 30_000 });
|
|
35
|
-
|
|
36
|
-
// Wait for possible page navigation or DOM change after click
|
|
37
|
-
const [response] = await Promise.all([
|
|
38
|
-
this.page.waitForLoadState(waitFor, { timeout: 10_000 }).catch(() => null), // wait for network/DOM
|
|
39
|
-
this.page.click(selector),
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
return `clicked ${selector}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async get_dom({ limit = 5000, exclude = true }) {
|
|
46
|
-
const html = await this.page.content();
|
|
47
|
-
|
|
48
|
-
if (exclude) {
|
|
49
|
-
const $ = cheerio.load(html);
|
|
50
|
-
$('script, style, meta, link, noscript, iframe, canvas, svg').remove();
|
|
51
|
-
|
|
52
|
-
$('*').each((_, el) => {
|
|
53
|
-
const $el = $(el);
|
|
54
|
-
if (!$el.text().trim() && $el.children().length === 0) {
|
|
55
|
-
$el.remove();
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
$('*')
|
|
60
|
-
.contents()
|
|
61
|
-
.each(function () {
|
|
62
|
-
if (this.type === 'comment') {
|
|
63
|
-
$(this).remove();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return $.html().slice(0, limit);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return html.slice(0, limit);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async close() {
|
|
74
|
-
await this.browser?.close();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const toolsSchema = [
|
|
79
|
-
{
|
|
80
|
-
type: 'function',
|
|
81
|
-
function: {
|
|
82
|
-
name: 'navigate',
|
|
83
|
-
description: 'Navigate to a URL',
|
|
84
|
-
parameters: {
|
|
85
|
-
type: 'object',
|
|
86
|
-
properties: {
|
|
87
|
-
url: { type: 'string' },
|
|
88
|
-
},
|
|
89
|
-
required: ['url'],
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
type: 'function',
|
|
95
|
-
function: {
|
|
96
|
-
name: 'fill',
|
|
97
|
-
description: 'Fill in a field by CSS selector',
|
|
98
|
-
parameters: {
|
|
99
|
-
type: 'object',
|
|
100
|
-
properties: {
|
|
101
|
-
selector: { type: 'string' },
|
|
102
|
-
text: { type: 'string' },
|
|
103
|
-
},
|
|
104
|
-
required: ['selector', 'text'],
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
type: 'function',
|
|
110
|
-
function: {
|
|
111
|
-
name: 'click',
|
|
112
|
-
description: 'Click a button or link by CSS selector',
|
|
113
|
-
parameters: {
|
|
114
|
-
type: 'object',
|
|
115
|
-
properties: {
|
|
116
|
-
selector: { type: 'string' },
|
|
117
|
-
waitFor: {
|
|
118
|
-
type: 'string',
|
|
119
|
-
enum: ['load', 'domcontentloaded', 'networkidle'],
|
|
120
|
-
default: 'load',
|
|
121
|
-
description: 'Wait condition after click',
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
required: ['selector'],
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
type: 'function',
|
|
130
|
-
function: {
|
|
131
|
-
name: 'get_dom',
|
|
132
|
-
description: 'Return trimmed HTML from the current page',
|
|
133
|
-
parameters: {
|
|
134
|
-
type: 'object',
|
|
135
|
-
properties: {
|
|
136
|
-
limit: {
|
|
137
|
-
type: 'number',
|
|
138
|
-
default: 5000,
|
|
139
|
-
description: 'Max characters to return',
|
|
140
|
-
},
|
|
141
|
-
exclude: {
|
|
142
|
-
type: 'boolean',
|
|
143
|
-
default: true,
|
|
144
|
-
description: 'Whether to exclude noisy tags like script/style/etc.',
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
];
|
|
151
|
-
|
|
152
|
-
const TOOL_MAP = {
|
|
153
|
-
navigate: (b, args) => b.navigate(args),
|
|
154
|
-
fill: (b, args) => b.fill(args),
|
|
155
|
-
click: (b, args) => b.click(args),
|
|
156
|
-
get_dom: (b, args) => b.get_dom(args),
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
async function runAgent(goal, maxTurns = 10) {
|
|
160
|
-
const browser = new Browser();
|
|
161
|
-
await browser.start();
|
|
162
|
-
|
|
163
|
-
const messages = [
|
|
164
|
-
{
|
|
165
|
-
role: 'system',
|
|
166
|
-
content: `
|
|
167
|
-
You are an autonomous web agent. Use function calls to complete the user's goal.
|
|
168
|
-
If you are unsure of the selectors for inputs or buttons, call 'get_dom' to retrieve page HTML,
|
|
169
|
-
analyze it, then make your best guess based on labels, names, types, and placeholder values.
|
|
170
|
-
After completing the goal, respond with a final plain-text message starting with SUCCESS or FAILURE.
|
|
171
|
-
`.trim(),
|
|
172
|
-
},
|
|
173
|
-
{ role: 'user', content: goal },
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
for (let turn = 0; turn < maxTurns; turn++) {
|
|
178
|
-
const response = await openai.chat.completions.create({
|
|
179
|
-
model: 'gpt-4o',
|
|
180
|
-
messages,
|
|
181
|
-
tools: toolsSchema,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const msg = response.choices[0].message;
|
|
185
|
-
|
|
186
|
-
if (msg.tool_calls?.length) {
|
|
187
|
-
messages.push(msg);
|
|
188
|
-
|
|
189
|
-
for (const call of msg.tool_calls) {
|
|
190
|
-
const fnName = call.function.name;
|
|
191
|
-
const args = JSON.parse(call.function.arguments || '{}');
|
|
192
|
-
console.log(`[model] → ${fnName}`, args);
|
|
193
|
-
|
|
194
|
-
let result;
|
|
195
|
-
try {
|
|
196
|
-
result = await TOOL_MAP[fnName](browser, args);
|
|
197
|
-
if (typeof result !== 'string') {
|
|
198
|
-
result = JSON.stringify(result ?? '');
|
|
199
|
-
}
|
|
200
|
-
} catch (err) {
|
|
201
|
-
result = `ERROR: ${err.message}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
console.log(`[tool ] ← ${result}`);
|
|
205
|
-
|
|
206
|
-
messages.push({
|
|
207
|
-
role: 'tool',
|
|
208
|
-
tool_call_id: call.id,
|
|
209
|
-
name: fnName,
|
|
210
|
-
content: result,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const final = msg.content?.trim().toLowerCase();
|
|
218
|
-
if (final?.startsWith('success') || final?.startsWith('failure')) {
|
|
219
|
-
console.log('\n┏━ FINAL AGENT RESPONSE ━━━━━━━━━━━━━━━━━━━');
|
|
220
|
-
console.log(msg.content);
|
|
221
|
-
console.log('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const domCallId = `get_dom_${Date.now()}`;
|
|
226
|
-
messages.push({
|
|
227
|
-
role: 'assistant',
|
|
228
|
-
tool_calls: [
|
|
229
|
-
{
|
|
230
|
-
id: domCallId,
|
|
231
|
-
function: {
|
|
232
|
-
name: 'get_dom',
|
|
233
|
-
arguments: JSON.stringify({ limit: 5000, exclude: true }),
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
],
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const domHtml = await TOOL_MAP.get_dom(browser, { limit: 5000, exclude: true });
|
|
240
|
-
messages.push({
|
|
241
|
-
role: 'tool',
|
|
242
|
-
tool_call_id: domCallId,
|
|
243
|
-
name: 'get_dom',
|
|
244
|
-
content: typeof domHtml === 'string' ? domHtml : JSON.stringify(domHtml),
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
console.log(`[auto] → Injected DOM for next reasoning step`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
console.log('🛑 Agent ran out of turns.');
|
|
251
|
-
} finally {
|
|
252
|
-
await browser.close();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
(async () => {
|
|
257
|
-
await runAgent(`
|
|
258
|
-
Visit ${process.env.URL}.
|
|
259
|
-
Log in using ${process.env.USERNAME} and password C0ll3g1um_Bu1lt!.
|
|
260
|
-
After clicking the login button, if you see the word "${process.env.AFTER_LOGIN_CHECK}" on the page, report SUCCESS.
|
|
261
|
-
Otherwise, report FAILURE.
|
|
262
|
-
`);
|
|
263
|
-
})();
|