@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.
Files changed (107) hide show
  1. package/.turbo/turbo-build.log +87 -0
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +37 -0
  4. package/dist/agent/agent-adapter.d.mts +17 -0
  5. package/dist/agent/agent-adapter.d.mts.map +1 -0
  6. package/dist/agent/agent-adapter.mjs +45 -0
  7. package/dist/agent/agent-adapter.mjs.map +1 -0
  8. package/dist/agent/llm-agent.d.mts +13 -0
  9. package/dist/agent/llm-agent.d.mts.map +1 -0
  10. package/dist/agent/llm-agent.mjs +41 -0
  11. package/dist/agent/llm-agent.mjs.map +1 -0
  12. package/dist/agent/plan.d.mts +27 -0
  13. package/dist/agent/plan.d.mts.map +1 -0
  14. package/dist/agent/plan.mjs +28 -0
  15. package/dist/agent/plan.mjs.map +1 -0
  16. package/dist/agent/rule-based-agent.d.mts +13 -0
  17. package/dist/agent/rule-based-agent.d.mts.map +1 -0
  18. package/dist/agent/rule-based-agent.mjs +152 -0
  19. package/dist/agent/rule-based-agent.mjs.map +1 -0
  20. package/dist/agent/tool-contract.d.mts +19 -0
  21. package/dist/agent/tool-contract.d.mts.map +1 -0
  22. package/dist/agent/types.d.mts +77 -0
  23. package/dist/agent/types.d.mts.map +1 -0
  24. package/dist/client-api.d.mts +49 -0
  25. package/dist/client-api.d.mts.map +1 -0
  26. package/dist/client-api.mjs +68 -0
  27. package/dist/client-api.mjs.map +1 -0
  28. package/dist/context.d.mts +29 -0
  29. package/dist/context.d.mts.map +1 -0
  30. package/dist/dom-actions/click.d.mts +15 -0
  31. package/dist/dom-actions/click.d.mts.map +1 -0
  32. package/dist/dom-actions/click.mjs +36 -0
  33. package/dist/dom-actions/click.mjs.map +1 -0
  34. package/dist/dom-actions/fill-input.d.mts +17 -0
  35. package/dist/dom-actions/fill-input.d.mts.map +1 -0
  36. package/dist/dom-actions/fill-input.mjs +69 -0
  37. package/dist/dom-actions/fill-input.mjs.map +1 -0
  38. package/dist/dom-actions/find-element.mjs +17 -0
  39. package/dist/dom-actions/find-element.mjs.map +1 -0
  40. package/dist/dom-actions/highlight.d.mts +34 -0
  41. package/dist/dom-actions/highlight.d.mts.map +1 -0
  42. package/dist/dom-actions/highlight.mjs +60 -0
  43. package/dist/dom-actions/highlight.mjs.map +1 -0
  44. package/dist/dom-actions/navigate.d.mts +16 -0
  45. package/dist/dom-actions/navigate.d.mts.map +1 -0
  46. package/dist/dom-actions/navigate.mjs +22 -0
  47. package/dist/dom-actions/navigate.mjs.map +1 -0
  48. package/dist/dom-actions/scroll.d.mts +16 -0
  49. package/dist/dom-actions/scroll.d.mts.map +1 -0
  50. package/dist/dom-actions/scroll.mjs +32 -0
  51. package/dist/dom-actions/scroll.mjs.map +1 -0
  52. package/dist/dom.d.mts +23 -0
  53. package/dist/dom.d.mts.map +1 -0
  54. package/dist/dom.mjs +60 -0
  55. package/dist/dom.mjs.map +1 -0
  56. package/dist/events.d.mts +35 -0
  57. package/dist/events.d.mts.map +1 -0
  58. package/dist/events.mjs +49 -0
  59. package/dist/events.mjs.map +1 -0
  60. package/dist/index.d.mts +21 -0
  61. package/dist/index.mjs +18 -0
  62. package/dist/instruction.d.mts +21 -0
  63. package/dist/instruction.d.mts.map +1 -0
  64. package/dist/marker.d.mts +35 -0
  65. package/dist/marker.d.mts.map +1 -0
  66. package/dist/marker.mjs +137 -0
  67. package/dist/marker.mjs.map +1 -0
  68. package/dist/store.d.mts +56 -0
  69. package/dist/store.d.mts.map +1 -0
  70. package/dist/store.mjs +114 -0
  71. package/dist/store.mjs.map +1 -0
  72. package/dist/util/attributes.d.mts +57 -0
  73. package/dist/util/attributes.d.mts.map +1 -0
  74. package/dist/util/attributes.mjs +68 -0
  75. package/dist/util/attributes.mjs.map +1 -0
  76. package/dist/util/format.d.mts +18 -0
  77. package/dist/util/format.d.mts.map +1 -0
  78. package/dist/util/format.mjs +21 -0
  79. package/dist/util/format.mjs.map +1 -0
  80. package/package.json +26 -0
  81. package/src/agent/agent-adapter.ts +75 -0
  82. package/src/agent/index.ts +21 -0
  83. package/src/agent/llm-agent.ts +64 -0
  84. package/src/agent/plan.ts +41 -0
  85. package/src/agent/rule-based-agent.ts +269 -0
  86. package/src/agent/tool-contract.ts +22 -0
  87. package/src/agent/types.ts +83 -0
  88. package/src/client-api.ts +107 -0
  89. package/src/context.ts +28 -0
  90. package/src/dom-actions/click.ts +39 -0
  91. package/src/dom-actions/fill-input.ts +113 -0
  92. package/src/dom-actions/find-element.ts +14 -0
  93. package/src/dom-actions/highlight.ts +93 -0
  94. package/src/dom-actions/index.ts +5 -0
  95. package/src/dom-actions/navigate.ts +17 -0
  96. package/src/dom-actions/scroll.ts +29 -0
  97. package/src/dom.ts +89 -0
  98. package/src/events.ts +55 -0
  99. package/src/index.ts +55 -0
  100. package/src/instruction.ts +6 -0
  101. package/src/marker.ts +237 -0
  102. package/src/store.ts +138 -0
  103. package/src/util/attributes.ts +68 -0
  104. package/src/util/format.ts +16 -0
  105. package/src/util/index.ts +2 -0
  106. package/tsconfig.json +18 -0
  107. 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
+ }