@ai11y/core 0.0.1
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/.turbo/turbo-build.log +87 -0
- package/CHANGELOG.md +29 -0
- package/README.md +37 -0
- package/dist/agent/agent-adapter.d.mts +17 -0
- package/dist/agent/agent-adapter.d.mts.map +1 -0
- package/dist/agent/agent-adapter.mjs +45 -0
- package/dist/agent/agent-adapter.mjs.map +1 -0
- package/dist/agent/llm-agent.d.mts +13 -0
- package/dist/agent/llm-agent.d.mts.map +1 -0
- package/dist/agent/llm-agent.mjs +41 -0
- package/dist/agent/llm-agent.mjs.map +1 -0
- package/dist/agent/plan.d.mts +27 -0
- package/dist/agent/plan.d.mts.map +1 -0
- package/dist/agent/plan.mjs +28 -0
- package/dist/agent/plan.mjs.map +1 -0
- package/dist/agent/rule-based-agent.d.mts +13 -0
- package/dist/agent/rule-based-agent.d.mts.map +1 -0
- package/dist/agent/rule-based-agent.mjs +152 -0
- package/dist/agent/rule-based-agent.mjs.map +1 -0
- package/dist/agent/tool-contract.d.mts +19 -0
- package/dist/agent/tool-contract.d.mts.map +1 -0
- package/dist/agent/types.d.mts +77 -0
- package/dist/agent/types.d.mts.map +1 -0
- package/dist/client-api.d.mts +49 -0
- package/dist/client-api.d.mts.map +1 -0
- package/dist/client-api.mjs +68 -0
- package/dist/client-api.mjs.map +1 -0
- package/dist/context.d.mts +29 -0
- package/dist/context.d.mts.map +1 -0
- package/dist/dom-actions/click.d.mts +15 -0
- package/dist/dom-actions/click.d.mts.map +1 -0
- package/dist/dom-actions/click.mjs +36 -0
- package/dist/dom-actions/click.mjs.map +1 -0
- package/dist/dom-actions/fill-input.d.mts +17 -0
- package/dist/dom-actions/fill-input.d.mts.map +1 -0
- package/dist/dom-actions/fill-input.mjs +69 -0
- package/dist/dom-actions/fill-input.mjs.map +1 -0
- package/dist/dom-actions/find-element.mjs +17 -0
- package/dist/dom-actions/find-element.mjs.map +1 -0
- package/dist/dom-actions/highlight.d.mts +34 -0
- package/dist/dom-actions/highlight.d.mts.map +1 -0
- package/dist/dom-actions/highlight.mjs +60 -0
- package/dist/dom-actions/highlight.mjs.map +1 -0
- package/dist/dom-actions/navigate.d.mts +16 -0
- package/dist/dom-actions/navigate.d.mts.map +1 -0
- package/dist/dom-actions/navigate.mjs +22 -0
- package/dist/dom-actions/navigate.mjs.map +1 -0
- package/dist/dom-actions/scroll.d.mts +16 -0
- package/dist/dom-actions/scroll.d.mts.map +1 -0
- package/dist/dom-actions/scroll.mjs +32 -0
- package/dist/dom-actions/scroll.mjs.map +1 -0
- package/dist/dom.d.mts +23 -0
- package/dist/dom.d.mts.map +1 -0
- package/dist/dom.mjs +60 -0
- package/dist/dom.mjs.map +1 -0
- package/dist/events.d.mts +35 -0
- package/dist/events.d.mts.map +1 -0
- package/dist/events.mjs +49 -0
- package/dist/events.mjs.map +1 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +18 -0
- package/dist/instruction.d.mts +21 -0
- package/dist/instruction.d.mts.map +1 -0
- package/dist/marker.d.mts +35 -0
- package/dist/marker.d.mts.map +1 -0
- package/dist/marker.mjs +137 -0
- package/dist/marker.mjs.map +1 -0
- package/dist/store.d.mts +56 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +114 -0
- package/dist/store.mjs.map +1 -0
- package/dist/util/attributes.d.mts +57 -0
- package/dist/util/attributes.d.mts.map +1 -0
- package/dist/util/attributes.mjs +68 -0
- package/dist/util/attributes.mjs.map +1 -0
- package/dist/util/format.d.mts +18 -0
- package/dist/util/format.d.mts.map +1 -0
- package/dist/util/format.mjs +21 -0
- package/dist/util/format.mjs.map +1 -0
- package/package.json +26 -0
- package/src/agent/agent-adapter.ts +75 -0
- package/src/agent/index.ts +21 -0
- package/src/agent/llm-agent.ts +64 -0
- package/src/agent/plan.ts +41 -0
- package/src/agent/rule-based-agent.ts +269 -0
- package/src/agent/tool-contract.ts +22 -0
- package/src/agent/types.ts +83 -0
- package/src/client-api.ts +107 -0
- package/src/context.ts +28 -0
- package/src/dom-actions/click.ts +39 -0
- package/src/dom-actions/fill-input.ts +113 -0
- package/src/dom-actions/find-element.ts +14 -0
- package/src/dom-actions/highlight.ts +93 -0
- package/src/dom-actions/index.ts +5 -0
- package/src/dom-actions/navigate.ts +17 -0
- package/src/dom-actions/scroll.ts +29 -0
- package/src/dom.ts +89 -0
- package/src/events.ts +55 -0
- package/src/index.ts +55 -0
- package/src/instruction.ts +6 -0
- package/src/marker.ts +237 -0
- package/src/store.ts +138 -0
- package/src/util/attributes.ts +68 -0
- package/src/util/format.ts +16 -0
- package/src/util/index.ts +2 -0
- package/tsconfig.json +18 -0
- package/tsdown.config.ts +10 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/util/attributes.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Attribute name constants for data-ai-* attributes
|
|
4
|
+
*/
|
|
5
|
+
declare const ATTRIBUTE_ID = "data-ai-id";
|
|
6
|
+
declare const ATTRIBUTE_LABEL = "data-ai-label";
|
|
7
|
+
declare const ATTRIBUTE_INTENT = "data-ai-intent";
|
|
8
|
+
declare const ATTRIBUTE_SENSITIVE = "data-ai-sensitive";
|
|
9
|
+
/**
|
|
10
|
+
* Gets the value of the data-ai-id attribute from an element
|
|
11
|
+
*
|
|
12
|
+
* @param element - The element to get the attribute from
|
|
13
|
+
* @returns The marker ID or null if not found
|
|
14
|
+
*/
|
|
15
|
+
declare function getMarkerId(element: Element): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Gets the value of the data-ai-label attribute from an element
|
|
18
|
+
*
|
|
19
|
+
* @param element - The element to get the attribute from
|
|
20
|
+
* @returns The marker label or null if not found
|
|
21
|
+
*/
|
|
22
|
+
declare function getMarkerLabel(element: Element): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Gets the value of the data-ai-intent attribute from an element
|
|
25
|
+
*
|
|
26
|
+
* @param element - The element to get the attribute from
|
|
27
|
+
* @returns The marker intent or null if not found
|
|
28
|
+
*/
|
|
29
|
+
declare function getMarkerIntent(element: Element): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a query selector for finding an element by marker ID
|
|
32
|
+
*
|
|
33
|
+
* @param markerId - The marker ID to search for
|
|
34
|
+
* @returns A query selector string
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const selector = getMarkerSelector('connect_stripe');
|
|
39
|
+
* // Returns: '[data-ai-id="connect_stripe"]'
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function getMarkerSelector(markerId: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Creates a query selector for finding all elements with marker IDs
|
|
45
|
+
*
|
|
46
|
+
* @returns A query selector string
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const selector = getAllMarkersSelector();
|
|
51
|
+
* // Returns: '[data-ai-id]'
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function getAllMarkersSelector(): string;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ATTRIBUTE_ID, ATTRIBUTE_INTENT, ATTRIBUTE_LABEL, ATTRIBUTE_SENSITIVE, getAllMarkersSelector, getMarkerId, getMarkerIntent, getMarkerLabel, getMarkerSelector };
|
|
57
|
+
//# sourceMappingURL=attributes.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attributes.d.mts","names":[],"sources":["../../src/util/attributes.ts"],"sourcesContent":[],"mappings":";;AAGA;AACA;AACa,cAFA,YAAA,GAEgB,YAAA;AAChB,cAFA,eAAA,GAEmB,eAAA;AAQhB,cATH,gBAAA,GAS+B,gBAAA;AAU5B,cAlBH,mBAAA,GAkBkC,mBAAA;AAU/C;AAgBA;AAeA;;;;iBAnDgB,WAAA,UAAqB;;;;;;;iBAUrB,cAAA,UAAwB;;;;;;;iBAUxB,eAAA,UAAyB;;;;;;;;;;;;;iBAgBzB,iBAAA;;;;;;;;;;;;iBAeA,qBAAA,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region src/util/attributes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Attribute name constants for data-ai-* attributes
|
|
4
|
+
*/
|
|
5
|
+
const ATTRIBUTE_ID = "data-ai-id";
|
|
6
|
+
const ATTRIBUTE_LABEL = "data-ai-label";
|
|
7
|
+
const ATTRIBUTE_INTENT = "data-ai-intent";
|
|
8
|
+
const ATTRIBUTE_SENSITIVE = "data-ai-sensitive";
|
|
9
|
+
/**
|
|
10
|
+
* Gets the value of the data-ai-id attribute from an element
|
|
11
|
+
*
|
|
12
|
+
* @param element - The element to get the attribute from
|
|
13
|
+
* @returns The marker ID or null if not found
|
|
14
|
+
*/
|
|
15
|
+
function getMarkerId(element) {
|
|
16
|
+
return element.getAttribute(ATTRIBUTE_ID);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Gets the value of the data-ai-label attribute from an element
|
|
20
|
+
*
|
|
21
|
+
* @param element - The element to get the attribute from
|
|
22
|
+
* @returns The marker label or null if not found
|
|
23
|
+
*/
|
|
24
|
+
function getMarkerLabel(element) {
|
|
25
|
+
return element.getAttribute(ATTRIBUTE_LABEL);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Gets the value of the data-ai-intent attribute from an element
|
|
29
|
+
*
|
|
30
|
+
* @param element - The element to get the attribute from
|
|
31
|
+
* @returns The marker intent or null if not found
|
|
32
|
+
*/
|
|
33
|
+
function getMarkerIntent(element) {
|
|
34
|
+
return element.getAttribute(ATTRIBUTE_INTENT);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a query selector for finding an element by marker ID
|
|
38
|
+
*
|
|
39
|
+
* @param markerId - The marker ID to search for
|
|
40
|
+
* @returns A query selector string
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const selector = getMarkerSelector('connect_stripe');
|
|
45
|
+
* // Returns: '[data-ai-id="connect_stripe"]'
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
function getMarkerSelector(markerId) {
|
|
49
|
+
return `[${ATTRIBUTE_ID}="${markerId}"]`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates a query selector for finding all elements with marker IDs
|
|
53
|
+
*
|
|
54
|
+
* @returns A query selector string
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const selector = getAllMarkersSelector();
|
|
59
|
+
* // Returns: '[data-ai-id]'
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function getAllMarkersSelector() {
|
|
63
|
+
return `[${ATTRIBUTE_ID}]`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { ATTRIBUTE_ID, ATTRIBUTE_INTENT, ATTRIBUTE_LABEL, ATTRIBUTE_SENSITIVE, getAllMarkersSelector, getMarkerId, getMarkerIntent, getMarkerLabel, getMarkerSelector };
|
|
68
|
+
//# sourceMappingURL=attributes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attributes.mjs","names":[],"sources":["../../src/util/attributes.ts"],"sourcesContent":["/**\n * Attribute name constants for data-ai-* attributes\n */\nexport const ATTRIBUTE_ID = \"data-ai-id\";\nexport const ATTRIBUTE_LABEL = \"data-ai-label\";\nexport const ATTRIBUTE_INTENT = \"data-ai-intent\";\nexport const ATTRIBUTE_SENSITIVE = \"data-ai-sensitive\";\n\n/**\n * Gets the value of the data-ai-id attribute from an element\n *\n * @param element - The element to get the attribute from\n * @returns The marker ID or null if not found\n */\nexport function getMarkerId(element: Element): string | null {\n\treturn element.getAttribute(ATTRIBUTE_ID);\n}\n\n/**\n * Gets the value of the data-ai-label attribute from an element\n *\n * @param element - The element to get the attribute from\n * @returns The marker label or null if not found\n */\nexport function getMarkerLabel(element: Element): string | null {\n\treturn element.getAttribute(ATTRIBUTE_LABEL);\n}\n\n/**\n * Gets the value of the data-ai-intent attribute from an element\n *\n * @param element - The element to get the attribute from\n * @returns The marker intent or null if not found\n */\nexport function getMarkerIntent(element: Element): string | null {\n\treturn element.getAttribute(ATTRIBUTE_INTENT);\n}\n\n/**\n * Creates a query selector for finding an element by marker ID\n *\n * @param markerId - The marker ID to search for\n * @returns A query selector string\n *\n * @example\n * ```ts\n * const selector = getMarkerSelector('connect_stripe');\n * // Returns: '[data-ai-id=\"connect_stripe\"]'\n * ```\n */\nexport function getMarkerSelector(markerId: string): string {\n\treturn `[${ATTRIBUTE_ID}=\"${markerId}\"]`;\n}\n\n/**\n * Creates a query selector for finding all elements with marker IDs\n *\n * @returns A query selector string\n *\n * @example\n * ```ts\n * const selector = getAllMarkersSelector();\n * // Returns: '[data-ai-id]'\n * ```\n */\nexport function getAllMarkersSelector(): string {\n\treturn `[${ATTRIBUTE_ID}]`;\n}\n"],"mappings":";;;;AAGA,MAAa,eAAe;AAC5B,MAAa,kBAAkB;AAC/B,MAAa,mBAAmB;AAChC,MAAa,sBAAsB;;;;;;;AAQnC,SAAgB,YAAY,SAAiC;AAC5D,QAAO,QAAQ,aAAa,aAAa;;;;;;;;AAS1C,SAAgB,eAAe,SAAiC;AAC/D,QAAO,QAAQ,aAAa,gBAAgB;;;;;;;;AAS7C,SAAgB,gBAAgB,SAAiC;AAChE,QAAO,QAAQ,aAAa,iBAAiB;;;;;;;;;;;;;;AAe9C,SAAgB,kBAAkB,UAA0B;AAC3D,QAAO,IAAI,aAAa,IAAI,SAAS;;;;;;;;;;;;;AActC,SAAgB,wBAAgC;AAC/C,QAAO,IAAI,aAAa"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/util/format.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Formats a marker ID into a readable label
|
|
4
|
+
* Converts snake_case to Title Case
|
|
5
|
+
*
|
|
6
|
+
* @param id - The marker ID to format
|
|
7
|
+
* @returns A formatted label
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* formatMarkerId('connect_stripe');
|
|
12
|
+
* // Returns: 'Connect Stripe'
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
declare function formatMarkerId(id: string): string;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { formatMarkerId };
|
|
18
|
+
//# sourceMappingURL=format.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.mts","names":[],"sources":["../../src/util/format.ts"],"sourcesContent":[],"mappings":";;AAaA;;;;;;;;;;;;iBAAgB,cAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/util/format.ts
|
|
2
|
+
/**
|
|
3
|
+
* Formats a marker ID into a readable label
|
|
4
|
+
* Converts snake_case to Title Case
|
|
5
|
+
*
|
|
6
|
+
* @param id - The marker ID to format
|
|
7
|
+
* @returns A formatted label
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* formatMarkerId('connect_stripe');
|
|
12
|
+
* // Returns: 'Connect Stripe'
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
function formatMarkerId(id) {
|
|
16
|
+
return id.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { formatMarkerId };
|
|
21
|
+
//# sourceMappingURL=format.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.mjs","names":[],"sources":["../../src/util/format.ts"],"sourcesContent":["/**\n * Formats a marker ID into a readable label\n * Converts snake_case to Title Case\n *\n * @param id - The marker ID to format\n * @returns A formatted label\n *\n * @example\n * ```ts\n * formatMarkerId('connect_stripe');\n * // Returns: 'Connect Stripe'\n * ```\n */\nexport function formatMarkerId(id: string): string {\n\treturn id.replace(/_/g, \" \").replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,SAAgB,eAAe,IAAoB;AAClD,QAAO,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,UAAU,SAAS,KAAK,aAAa,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai11y/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.mjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"tsdown": "0.18.1",
|
|
17
|
+
"typescript": "5.9.3"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsdown",
|
|
24
|
+
"watch": "tsdown --watch"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Ai11yContext } from "../context.js";
|
|
2
|
+
import { runLLMAgent } from "./llm-agent.js";
|
|
3
|
+
import { runRuleBasedAgent } from "./rule-based-agent.js";
|
|
4
|
+
import type {
|
|
5
|
+
AgentAdapterConfig,
|
|
6
|
+
AgentResponse,
|
|
7
|
+
ConversationMessage,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect if we're likely offline (browser only)
|
|
12
|
+
*/
|
|
13
|
+
function isLikelyOffline(): boolean {
|
|
14
|
+
if (typeof navigator !== "undefined" && "onLine" in navigator) {
|
|
15
|
+
return !navigator.onLine;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Select and run the appropriate agent based on configuration.
|
|
22
|
+
*
|
|
23
|
+
* This adapter handles agent selection logic:
|
|
24
|
+
* - "llm": Always use LLM agent (requires llmConfig)
|
|
25
|
+
* - "rule-based": Always use rule-based agent
|
|
26
|
+
* - "auto": Use LLM if configured, fallback to rule-based on error or when offline
|
|
27
|
+
*/
|
|
28
|
+
export async function runAgentAdapter(
|
|
29
|
+
input: string,
|
|
30
|
+
context: Ai11yContext,
|
|
31
|
+
config: AgentAdapterConfig = {},
|
|
32
|
+
messages?: ConversationMessage[],
|
|
33
|
+
): Promise<AgentResponse> {
|
|
34
|
+
const { mode = "auto", llmConfig = null, forceRuleBased = false } = config;
|
|
35
|
+
|
|
36
|
+
// Force rule-based agent if explicitly requested
|
|
37
|
+
if (forceRuleBased || mode === "rule-based") {
|
|
38
|
+
return runRuleBasedAgent(input, context);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (mode === "llm") {
|
|
42
|
+
if (!llmConfig) {
|
|
43
|
+
console.warn(
|
|
44
|
+
"LLM mode requested but no llmConfig provided. Falling back to rule-based agent.",
|
|
45
|
+
);
|
|
46
|
+
return runRuleBasedAgent(input, context);
|
|
47
|
+
}
|
|
48
|
+
return runLLMAgent(input, context, llmConfig, messages);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Auto mode: try LLM if available, fallback to rule-based
|
|
52
|
+
if (mode === "auto" || !mode) {
|
|
53
|
+
// Check if we're offline - use rule-based
|
|
54
|
+
if (isLikelyOffline()) {
|
|
55
|
+
return runRuleBasedAgent(input, context);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try LLM if configured
|
|
59
|
+
if (llmConfig) {
|
|
60
|
+
try {
|
|
61
|
+
return await runLLMAgent(input, context, llmConfig, messages);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// runLLMAgent already falls back to rule-based, but we catch here for safety
|
|
64
|
+
console.warn("LLM agent failed, using rule-based fallback:", error);
|
|
65
|
+
return runRuleBasedAgent(input, context);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Default to rule-based
|
|
70
|
+
return runRuleBasedAgent(input, context);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fallback (shouldn't reach here)
|
|
74
|
+
return runRuleBasedAgent(input, context);
|
|
75
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent module - provides different agent implementations
|
|
3
|
+
*
|
|
4
|
+
* Agents:
|
|
5
|
+
* - runRuleBasedAgent: Pattern-matching agent for common commands (works offline)
|
|
6
|
+
* - runLLMAgent: LLM-based agent using server-side API
|
|
7
|
+
* - runAgentAdapter: Unified adapter that selects the appropriate agent
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { runAgentAdapter } from "./agent-adapter.js";
|
|
11
|
+
export { runLLMAgent } from "./llm-agent.js";
|
|
12
|
+
export { runRuleBasedAgent } from "./rule-based-agent.js";
|
|
13
|
+
export type {
|
|
14
|
+
AgentAdapterConfig,
|
|
15
|
+
AgentConfig,
|
|
16
|
+
AgentMode,
|
|
17
|
+
AgentRequest,
|
|
18
|
+
AgentResponse,
|
|
19
|
+
ConversationMessage,
|
|
20
|
+
LLMAgentConfig,
|
|
21
|
+
} from "./types.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Ai11yContext } from "../context.js";
|
|
2
|
+
import type {
|
|
3
|
+
AgentResponse,
|
|
4
|
+
ConversationMessage,
|
|
5
|
+
LLMAgentConfig,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* LLM-based agent using server-side API endpoint.
|
|
10
|
+
* Falls back to rule-based agent if the API is not available.
|
|
11
|
+
*/
|
|
12
|
+
export async function runLLMAgent(
|
|
13
|
+
input: string,
|
|
14
|
+
context: Ai11yContext,
|
|
15
|
+
config: LLMAgentConfig,
|
|
16
|
+
messages?: ConversationMessage[],
|
|
17
|
+
): Promise<AgentResponse> {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(config.apiEndpoint, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
input,
|
|
26
|
+
context: {
|
|
27
|
+
markers: context.markers,
|
|
28
|
+
...(context.inViewMarkerIds !== undefined && {
|
|
29
|
+
inViewMarkerIds: context.inViewMarkerIds,
|
|
30
|
+
}),
|
|
31
|
+
...(context.route !== undefined && { route: context.route }),
|
|
32
|
+
...(context.state !== undefined && { state: context.state }),
|
|
33
|
+
...(context.error !== undefined && {
|
|
34
|
+
error: context.error
|
|
35
|
+
? {
|
|
36
|
+
error: {
|
|
37
|
+
message: context.error.error.message,
|
|
38
|
+
},
|
|
39
|
+
meta: context.error.meta,
|
|
40
|
+
timestamp: context.error.timestamp,
|
|
41
|
+
}
|
|
42
|
+
: null,
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
messages: messages?.slice(-10),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const errorText = await response.text();
|
|
51
|
+
throw new Error(
|
|
52
|
+
`API request failed: ${response.status} ${response.statusText}. ${errorText}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data: AgentResponse = await response.json();
|
|
57
|
+
return data;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// If API is not available or there's an error, fall back to rule-based
|
|
60
|
+
console.warn("LLM agent error, falling back to rule-based agent:", error);
|
|
61
|
+
const { runRuleBasedAgent } = await import("./rule-based-agent.js");
|
|
62
|
+
return runRuleBasedAgent(input, context);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Ai11yContext } from "../context.js";
|
|
2
|
+
import { runAgentAdapter } from "./agent-adapter.js";
|
|
3
|
+
import type {
|
|
4
|
+
AgentAdapterConfig,
|
|
5
|
+
AgentResponse,
|
|
6
|
+
ConversationMessage,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Plans instructions based on UI context and user input
|
|
11
|
+
*
|
|
12
|
+
* @param ui - The current UI context
|
|
13
|
+
* @param input - User input/command
|
|
14
|
+
* @param config - Optional agent configuration
|
|
15
|
+
* @param messages - Optional conversation history
|
|
16
|
+
* @returns The full agent response containing both reply and instructions
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const ui = ctx.describe()
|
|
21
|
+
* const response = await plan(ui, "click save button", config)
|
|
22
|
+
* for (const instruction of response.instructions || []) {
|
|
23
|
+
* ctx.act(instruction)
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export async function plan(
|
|
28
|
+
ui: Ai11yContext,
|
|
29
|
+
input: string,
|
|
30
|
+
config?: AgentAdapterConfig,
|
|
31
|
+
messages?: ConversationMessage[],
|
|
32
|
+
): Promise<AgentResponse> {
|
|
33
|
+
const response: AgentResponse = await runAgentAdapter(
|
|
34
|
+
input,
|
|
35
|
+
ui,
|
|
36
|
+
config,
|
|
37
|
+
messages,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { Ai11yContext } from "../context.js";
|
|
2
|
+
import type { Instruction } from "../instruction.js";
|
|
3
|
+
import type { AgentResponse } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rule-based agent that uses pattern matching for common commands.
|
|
7
|
+
* Provides immediate responses without requiring an LLM.
|
|
8
|
+
*/
|
|
9
|
+
export function runRuleBasedAgent(
|
|
10
|
+
input: string,
|
|
11
|
+
context: Ai11yContext,
|
|
12
|
+
): AgentResponse {
|
|
13
|
+
const lowerInput = input.toLowerCase().trim();
|
|
14
|
+
|
|
15
|
+
// Handle navigation commands
|
|
16
|
+
if (
|
|
17
|
+
lowerInput.includes("go to") ||
|
|
18
|
+
lowerInput.includes("navigate to") ||
|
|
19
|
+
lowerInput.includes("open") ||
|
|
20
|
+
lowerInput.includes("take me to")
|
|
21
|
+
) {
|
|
22
|
+
// First, check if input matches any marker
|
|
23
|
+
const searchText = lowerInput
|
|
24
|
+
.replace(/go to|navigate to|open|take me to/g, "")
|
|
25
|
+
.trim();
|
|
26
|
+
const matchingMarker = context.markers.find((m) => {
|
|
27
|
+
const markerText = `${m.label} ${m.intent}`.toLowerCase();
|
|
28
|
+
return (
|
|
29
|
+
markerText.includes(searchText) ||
|
|
30
|
+
lowerInput.includes(m.label.toLowerCase()) ||
|
|
31
|
+
lowerInput.includes(m.intent.toLowerCase())
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// If a marker matches, decide based on element type and visibility
|
|
36
|
+
if (matchingMarker) {
|
|
37
|
+
const isInView =
|
|
38
|
+
context.inViewMarkerIds?.includes(matchingMarker.id) ?? false;
|
|
39
|
+
const isLink = matchingMarker.elementType === "a";
|
|
40
|
+
|
|
41
|
+
// If it's a link marker and in view, click it (navigate)
|
|
42
|
+
if (isLink && isInView) {
|
|
43
|
+
return {
|
|
44
|
+
reply: `Navigating to ${matchingMarker.label}...`,
|
|
45
|
+
instructions: [{ action: "click", id: matchingMarker.id }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If it's a link marker but not in view, scroll to it first
|
|
50
|
+
if (isLink && !isInView) {
|
|
51
|
+
return {
|
|
52
|
+
reply: `Scrolling to ${matchingMarker.label}...`,
|
|
53
|
+
instructions: [{ action: "scroll", id: matchingMarker.id }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// For non-link markers (sections, divs), scroll to them
|
|
58
|
+
return {
|
|
59
|
+
reply: `Scrolling to ${matchingMarker.label}...`,
|
|
60
|
+
instructions: [{ action: "scroll", id: matchingMarker.id }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// No marker found, proceed with route navigation
|
|
65
|
+
const instructions: Instruction[] = [];
|
|
66
|
+
|
|
67
|
+
// Extract route from input
|
|
68
|
+
let route = "/";
|
|
69
|
+
if (lowerInput.includes("billing")) {
|
|
70
|
+
route = "/billing";
|
|
71
|
+
} else if (lowerInput.includes("integrations")) {
|
|
72
|
+
route = "/integrations";
|
|
73
|
+
} else if (lowerInput.includes("home")) {
|
|
74
|
+
route = "/";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (route !== context.route) {
|
|
78
|
+
instructions.push({ action: "navigate", route });
|
|
79
|
+
return {
|
|
80
|
+
reply: `Navigating to ${route === "/" ? "home" : route}...`,
|
|
81
|
+
instructions,
|
|
82
|
+
};
|
|
83
|
+
} else {
|
|
84
|
+
return {
|
|
85
|
+
reply: `You're already on ${route === "/" ? "home" : route}.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle click commands
|
|
91
|
+
if (lowerInput.includes("click") || lowerInput.includes("press")) {
|
|
92
|
+
// Find matching marker
|
|
93
|
+
const matchingMarker = context.markers.find((m) => {
|
|
94
|
+
const markerText = `${m.label} ${m.intent}`.toLowerCase();
|
|
95
|
+
return (
|
|
96
|
+
markerText.includes(lowerInput.replace(/click|press/g, "").trim()) ||
|
|
97
|
+
lowerInput.includes(m.label.toLowerCase()) ||
|
|
98
|
+
lowerInput.includes(m.intent.toLowerCase())
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (matchingMarker) {
|
|
103
|
+
return {
|
|
104
|
+
reply: `Clicking ${matchingMarker.label}...`,
|
|
105
|
+
instructions: [{ action: "click", id: matchingMarker.id }],
|
|
106
|
+
};
|
|
107
|
+
} else {
|
|
108
|
+
return {
|
|
109
|
+
reply: `I couldn't find a button or element matching "${input}". Available options: ${context.markers.map((m) => m.label).join(", ") || "none"}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle scroll commands (scroll to element)
|
|
115
|
+
if (
|
|
116
|
+
lowerInput.includes("scroll to") ||
|
|
117
|
+
lowerInput.includes("scroll") ||
|
|
118
|
+
lowerInput.includes("show me")
|
|
119
|
+
) {
|
|
120
|
+
const matchingMarker = context.markers.find((m) => {
|
|
121
|
+
const markerText = `${m.label} ${m.intent}`.toLowerCase();
|
|
122
|
+
const searchText = lowerInput
|
|
123
|
+
.replace(/scroll to|scroll|show me/g, "")
|
|
124
|
+
.trim();
|
|
125
|
+
return (
|
|
126
|
+
markerText.includes(searchText) ||
|
|
127
|
+
lowerInput.includes(m.label.toLowerCase())
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (matchingMarker) {
|
|
132
|
+
// If user explicitly wants to scroll, use scroll tool
|
|
133
|
+
// Otherwise, if they say "show me" or "highlight", use highlight (which now scrolls)
|
|
134
|
+
if (lowerInput.includes("scroll")) {
|
|
135
|
+
return {
|
|
136
|
+
reply: `Scrolling to ${matchingMarker.label}...`,
|
|
137
|
+
instructions: [{ action: "scroll", id: matchingMarker.id }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle highlight commands
|
|
144
|
+
if (lowerInput.includes("highlight") || lowerInput.includes("show")) {
|
|
145
|
+
const matchingMarker = context.markers.find((m) => {
|
|
146
|
+
const markerText = `${m.label} ${m.intent}`.toLowerCase();
|
|
147
|
+
return (
|
|
148
|
+
markerText.includes(lowerInput.replace(/highlight|show/g, "").trim()) ||
|
|
149
|
+
lowerInput.includes(m.label.toLowerCase())
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (matchingMarker) {
|
|
154
|
+
return {
|
|
155
|
+
reply: `Highlighting ${matchingMarker.label}...`,
|
|
156
|
+
instructions: [{ action: "highlight", id: matchingMarker.id }],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle fill input commands
|
|
162
|
+
if (
|
|
163
|
+
lowerInput.includes("fill") ||
|
|
164
|
+
lowerInput.includes("enter") ||
|
|
165
|
+
lowerInput.includes("type") ||
|
|
166
|
+
lowerInput.includes("put")
|
|
167
|
+
) {
|
|
168
|
+
// Try to extract value from commands like:
|
|
169
|
+
// "fill email with test@example.com"
|
|
170
|
+
// "enter test@example.com in email"
|
|
171
|
+
// "type test@example.com in email input"
|
|
172
|
+
// "put test@example.com in email"
|
|
173
|
+
let value: string | undefined;
|
|
174
|
+
let markerSearchText = lowerInput;
|
|
175
|
+
|
|
176
|
+
// Pattern: "fill [marker] with [value]" or "fill [marker] [value]"
|
|
177
|
+
const fillWithMatch = lowerInput.match(
|
|
178
|
+
/(?:fill|enter|type|put)\s+(.+?)\s+(?:with|in|into)\s+(.+)/i,
|
|
179
|
+
);
|
|
180
|
+
if (fillWithMatch) {
|
|
181
|
+
markerSearchText = fillWithMatch[1].trim();
|
|
182
|
+
value = fillWithMatch[2].trim();
|
|
183
|
+
} else {
|
|
184
|
+
// Pattern: "fill [value] in [marker]" or "enter [value] in [marker]"
|
|
185
|
+
const fillInMatch = lowerInput.match(
|
|
186
|
+
/(?:fill|enter|type|put)\s+(.+?)\s+in\s+(.+)/i,
|
|
187
|
+
);
|
|
188
|
+
if (fillInMatch) {
|
|
189
|
+
value = fillInMatch[1].trim();
|
|
190
|
+
markerSearchText = fillInMatch[2].trim();
|
|
191
|
+
} else {
|
|
192
|
+
// Pattern: "[value] in [marker]"
|
|
193
|
+
const inMatch = lowerInput.match(/(.+?)\s+in\s+(.+)/i);
|
|
194
|
+
if (inMatch) {
|
|
195
|
+
value = inMatch[1].trim();
|
|
196
|
+
markerSearchText = inMatch[2].trim();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find matching marker (look for input-like elements)
|
|
202
|
+
const matchingMarker = context.markers.find((m) => {
|
|
203
|
+
const markerText = `${m.label} ${m.intent}`.toLowerCase();
|
|
204
|
+
const cleanedMarkerSearch = markerSearchText
|
|
205
|
+
.replace(/fill|enter|type|put|input|field/g, "")
|
|
206
|
+
.trim();
|
|
207
|
+
return (
|
|
208
|
+
markerText.includes(cleanedMarkerSearch) ||
|
|
209
|
+
cleanedMarkerSearch.includes(m.label.toLowerCase()) ||
|
|
210
|
+
cleanedMarkerSearch.includes(m.id.toLowerCase())
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (matchingMarker && value) {
|
|
215
|
+
return {
|
|
216
|
+
reply: `Filling ${matchingMarker.label} with "${value}"...`,
|
|
217
|
+
instructions: [
|
|
218
|
+
{
|
|
219
|
+
action: "fillInput",
|
|
220
|
+
id: matchingMarker.id,
|
|
221
|
+
value: value,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
} else if (matchingMarker && !value) {
|
|
226
|
+
return {
|
|
227
|
+
reply: `I found ${matchingMarker.label}, but I need a value to fill. Please specify what to enter, for example: "fill ${matchingMarker.label.toLowerCase()} with [value]"`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Handle error context
|
|
233
|
+
if (context.error) {
|
|
234
|
+
const error = context.error.error;
|
|
235
|
+
const errorMessage = error.message || "An error occurred";
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
lowerInput.includes("retry") ||
|
|
239
|
+
lowerInput.includes("try again") ||
|
|
240
|
+
lowerInput.includes("fix")
|
|
241
|
+
) {
|
|
242
|
+
// Try to find a retry action
|
|
243
|
+
if (context.error.meta?.markerId) {
|
|
244
|
+
const marker = context.markers.find(
|
|
245
|
+
(m) => m.id === context.error?.meta?.markerId,
|
|
246
|
+
);
|
|
247
|
+
if (marker) {
|
|
248
|
+
return {
|
|
249
|
+
reply: `Retrying ${marker.label}...`,
|
|
250
|
+
instructions: [{ action: "click", id: marker.id }],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
reply: `I can help you retry. The error was: ${errorMessage}. Would you like me to try clicking the button again?`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
reply: `I noticed an error: ${errorMessage}. You can ask me to "retry" or "try again" to attempt the action once more.`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Default response
|
|
266
|
+
return {
|
|
267
|
+
reply: `I can help you navigate, click buttons, highlight elements, or fill inputs. Try saying "go to billing", "click connect stripe", "show me the enable billing button", or "fill email with test@example.com".`,
|
|
268
|
+
};
|
|
269
|
+
}
|