@ai11y/agent 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 +31 -0
- package/CHANGELOG.md +34 -0
- package/README.md +99 -0
- package/dist/agent.d.mts +13 -0
- package/dist/agent.d.mts.map +1 -0
- package/dist/agent.mjs +221 -0
- package/dist/agent.mjs.map +1 -0
- package/dist/fastify.d.mts +58 -0
- package/dist/fastify.d.mts.map +1 -0
- package/dist/fastify.mjs +82 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +5 -0
- package/dist/llm-provider.d.mts +12 -0
- package/dist/llm-provider.d.mts.map +1 -0
- package/dist/llm-provider.mjs +17 -0
- package/dist/llm-provider.mjs.map +1 -0
- package/dist/tool-registry.d.mts +52 -0
- package/dist/tool-registry.d.mts.map +1 -0
- package/dist/tool-registry.mjs +181 -0
- package/dist/tool-registry.mjs.map +1 -0
- package/dist/types.d.mts +14 -0
- package/dist/types.d.mts.map +1 -0
- package/package.json +40 -0
- package/src/agent.ts +416 -0
- package/src/fastify.ts +111 -0
- package/src/index.ts +11 -0
- package/src/llm-provider.ts +24 -0
- package/src/tool-registry.ts +235 -0
- package/src/types.ts +10 -0
- package/tsconfig.json +20 -0
- package/tsdown.config.ts +11 -0
package/src/agent.ts
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import type { AgentRequest, AgentResponse, Instruction } from "@ai11y/core";
|
|
2
|
+
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createLLM } from "./llm-provider.js";
|
|
5
|
+
import {
|
|
6
|
+
createDefaultToolRegistry,
|
|
7
|
+
type ToolRegistry,
|
|
8
|
+
} from "./tool-registry.js";
|
|
9
|
+
import type { ServerConfig } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format context for the LLM prompt
|
|
13
|
+
*/
|
|
14
|
+
function formatContextForPrompt(context: AgentRequest["context"]): string {
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
|
|
17
|
+
if (context.route) {
|
|
18
|
+
parts.push(`Current route: ${context.route}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (context.error) {
|
|
22
|
+
const error = context.error.error;
|
|
23
|
+
parts.push(
|
|
24
|
+
`\n! Last error: ${error.message}${context.error.meta?.markerId ? ` (related to marker: ${context.error.meta.markerId})` : ""}`,
|
|
25
|
+
);
|
|
26
|
+
parts.push(
|
|
27
|
+
"The user may want to retry the failed action. Look for markers related to the error.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (context.state && Object.keys(context.state).length > 0) {
|
|
32
|
+
parts.push(`\nApplication state: ${JSON.stringify(context.state)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (context.markers.length > 0) {
|
|
36
|
+
parts.push("\nAvailable UI elements (markers):");
|
|
37
|
+
for (const marker of context.markers) {
|
|
38
|
+
const isInView = context.inViewMarkerIds?.includes(marker.id) ?? false;
|
|
39
|
+
const inViewStatus = isInView ? " [IN VIEW]" : "";
|
|
40
|
+
let markerLine = ` - ${marker.label} (ID: ${marker.id}, Type: ${marker.elementType})${inViewStatus}: ${marker.intent}`;
|
|
41
|
+
|
|
42
|
+
if (marker.value !== undefined) {
|
|
43
|
+
markerLine += `\n Current value: "${marker.value}"`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (marker.options !== undefined) {
|
|
47
|
+
const optionsList = marker.options
|
|
48
|
+
.map((opt) => `${opt.label} (${opt.value})`)
|
|
49
|
+
.join(", ");
|
|
50
|
+
markerLine += `\n Available options: ${optionsList}`;
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
marker.selectedOptions !== undefined &&
|
|
54
|
+
marker.selectedOptions.length > 0
|
|
55
|
+
) {
|
|
56
|
+
markerLine += `\n Selected: ${marker.selectedOptions.join(", ")}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
parts.push(markerLine);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
parts.push("\nNo UI elements are currently available.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (context.inViewMarkerIds && context.inViewMarkerIds.length > 0) {
|
|
66
|
+
parts.push(`\nVisible markers: ${context.inViewMarkerIds.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return parts.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert tool registry to LangChain tools
|
|
74
|
+
*/
|
|
75
|
+
function createLangChainTools(
|
|
76
|
+
toolRegistry: ToolRegistry,
|
|
77
|
+
context: AgentRequest["context"],
|
|
78
|
+
): DynamicStructuredTool[] {
|
|
79
|
+
const tools: DynamicStructuredTool[] = [];
|
|
80
|
+
const toolDefinitions = toolRegistry.getToolDefinitions();
|
|
81
|
+
|
|
82
|
+
for (const toolDef of toolDefinitions) {
|
|
83
|
+
const def = toolDef.function;
|
|
84
|
+
|
|
85
|
+
const zodSchema: Record<string, z.ZodTypeAny> = {};
|
|
86
|
+
for (const [key, param] of Object.entries(
|
|
87
|
+
def.parameters.properties,
|
|
88
|
+
) as Array<[string, { type: string; description: string }]>) {
|
|
89
|
+
const zodType =
|
|
90
|
+
param.type === "string"
|
|
91
|
+
? z.string()
|
|
92
|
+
: param.type === "number"
|
|
93
|
+
? z.number()
|
|
94
|
+
: param.type === "boolean"
|
|
95
|
+
? z.boolean()
|
|
96
|
+
: z.unknown();
|
|
97
|
+
zodSchema[key] = zodType.describe(param.description);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const schema = z.object(zodSchema);
|
|
101
|
+
|
|
102
|
+
const tool = new DynamicStructuredTool({
|
|
103
|
+
name: def.name,
|
|
104
|
+
description: def.description,
|
|
105
|
+
schema,
|
|
106
|
+
func: async (args: Record<string, unknown>) => {
|
|
107
|
+
const result = await toolRegistry.executeToolCall(
|
|
108
|
+
def.name,
|
|
109
|
+
args,
|
|
110
|
+
context,
|
|
111
|
+
);
|
|
112
|
+
return JSON.stringify(result);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
tools.push(tool);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return tools;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if user input matches any marker and return matching marker info
|
|
124
|
+
* Also checks if user is referring to a UI element (button, link, etc.) vs just a route
|
|
125
|
+
*/
|
|
126
|
+
function findMatchingMarker(
|
|
127
|
+
input: string,
|
|
128
|
+
markers: AgentRequest["context"]["markers"],
|
|
129
|
+
): {
|
|
130
|
+
marker: AgentRequest["context"]["markers"][0];
|
|
131
|
+
searchText: string;
|
|
132
|
+
isElementReference: boolean;
|
|
133
|
+
} | null {
|
|
134
|
+
const lowerInput = input.toLowerCase().trim();
|
|
135
|
+
|
|
136
|
+
const hasNavigationLanguage =
|
|
137
|
+
lowerInput.includes("go to") ||
|
|
138
|
+
lowerInput.includes("navigate to") ||
|
|
139
|
+
lowerInput.includes("open") ||
|
|
140
|
+
lowerInput.includes("take me to");
|
|
141
|
+
|
|
142
|
+
if (!hasNavigationLanguage) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const elementKeywords = [
|
|
147
|
+
"button",
|
|
148
|
+
"link",
|
|
149
|
+
"nav button",
|
|
150
|
+
"navigation button",
|
|
151
|
+
"nav link",
|
|
152
|
+
"navigation link",
|
|
153
|
+
"element",
|
|
154
|
+
"item",
|
|
155
|
+
"tab",
|
|
156
|
+
];
|
|
157
|
+
const isElementReference = elementKeywords.some((keyword) =>
|
|
158
|
+
lowerInput.includes(keyword),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const searchText = lowerInput
|
|
162
|
+
.replace(/go to|navigate to|open|take me to/g, "")
|
|
163
|
+
.trim();
|
|
164
|
+
|
|
165
|
+
const matchingMarker = markers.find((m) => {
|
|
166
|
+
const markerText = `${m.label} ${m.intent}`.toLowerCase();
|
|
167
|
+
return (
|
|
168
|
+
markerText.includes(searchText) ||
|
|
169
|
+
lowerInput.includes(m.label.toLowerCase()) ||
|
|
170
|
+
lowerInput.includes(m.intent.toLowerCase())
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return matchingMarker
|
|
175
|
+
? { marker: matchingMarker, searchText, isElementReference }
|
|
176
|
+
: null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* LangChain response with tool calls
|
|
181
|
+
*/
|
|
182
|
+
interface LangChainResponseWithTools {
|
|
183
|
+
tool_calls?: Array<{
|
|
184
|
+
name?: string;
|
|
185
|
+
args?: Record<string, unknown>;
|
|
186
|
+
function?: {
|
|
187
|
+
name?: string;
|
|
188
|
+
arguments?: string | Record<string, unknown>;
|
|
189
|
+
};
|
|
190
|
+
}>;
|
|
191
|
+
additional_kwargs?: {
|
|
192
|
+
tool_calls?: Array<{
|
|
193
|
+
name?: string;
|
|
194
|
+
args?: Record<string, unknown>;
|
|
195
|
+
function?: {
|
|
196
|
+
name?: string;
|
|
197
|
+
arguments?: string | Record<string, unknown>;
|
|
198
|
+
};
|
|
199
|
+
}>;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Type guard to check if response has tool calls
|
|
205
|
+
*/
|
|
206
|
+
function hasToolCalls(
|
|
207
|
+
response: unknown,
|
|
208
|
+
): response is LangChainResponseWithTools {
|
|
209
|
+
const kwargs = (response as LangChainResponseWithTools).additional_kwargs;
|
|
210
|
+
return (
|
|
211
|
+
typeof response === "object" &&
|
|
212
|
+
response !== null &&
|
|
213
|
+
("tool_calls" in response ||
|
|
214
|
+
("additional_kwargs" in response &&
|
|
215
|
+
typeof kwargs === "object" &&
|
|
216
|
+
kwargs !== null &&
|
|
217
|
+
"tool_calls" in kwargs))
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Run the LLM agent on the server
|
|
223
|
+
*/
|
|
224
|
+
export async function runAgent(
|
|
225
|
+
request: AgentRequest,
|
|
226
|
+
config: ServerConfig,
|
|
227
|
+
toolRegistry: ToolRegistry = createDefaultToolRegistry(),
|
|
228
|
+
): Promise<AgentResponse> {
|
|
229
|
+
const llm = await createLLM(config);
|
|
230
|
+
|
|
231
|
+
const markerMatch = findMatchingMarker(
|
|
232
|
+
request.input,
|
|
233
|
+
request.context.markers,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const contextPrompt = formatContextForPrompt(request.context);
|
|
237
|
+
const langchainTools = createLangChainTools(toolRegistry, request.context);
|
|
238
|
+
|
|
239
|
+
const llmWithTools =
|
|
240
|
+
langchainTools.length > 0 && typeof llm.bindTools === "function"
|
|
241
|
+
? llm.bindTools(langchainTools)
|
|
242
|
+
: llm;
|
|
243
|
+
|
|
244
|
+
let recentMarkerContext = "";
|
|
245
|
+
if (request.messages && request.messages.length > 0) {
|
|
246
|
+
const lastFewMessages = request.messages.slice(-4);
|
|
247
|
+
for (const msg of lastFewMessages) {
|
|
248
|
+
for (const marker of request.context.markers) {
|
|
249
|
+
if (
|
|
250
|
+
msg.content.toLowerCase().includes(marker.label.toLowerCase()) ||
|
|
251
|
+
msg.content.toLowerCase().includes(marker.id.toLowerCase())
|
|
252
|
+
) {
|
|
253
|
+
recentMarkerContext += `\nRecently discussed: ${marker.label} (${marker.id}) - ${marker.intent}`;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let markerMatchGuidance = "";
|
|
261
|
+
if (markerMatch) {
|
|
262
|
+
const marker = markerMatch.marker;
|
|
263
|
+
const isInView =
|
|
264
|
+
request.context.inViewMarkerIds?.includes(marker.id) ?? false;
|
|
265
|
+
const isLink = marker.elementType === "a";
|
|
266
|
+
|
|
267
|
+
if (isLink && isInView) {
|
|
268
|
+
markerMatchGuidance = `\n\n⚠️ Match found: "${marker.label}" (${marker.id}) is a visible link. Use 'click' tool.`;
|
|
269
|
+
} else if (isLink && !isInView) {
|
|
270
|
+
markerMatchGuidance = `\n\n🚨 Match found: "${marker.label}" (${marker.id}) is a link NOT in view. You MUST call BOTH 'scroll' and 'click' (in that order)—two tool calls. Do not call only scroll.`;
|
|
271
|
+
} else if (!isInView) {
|
|
272
|
+
markerMatchGuidance = `\n\n🚨 Match found: "${marker.label}" (${marker.id}) is NOT in view. You MUST call BOTH 'scroll' and 'click' (or the appropriate action) in that order—two tool calls. Do not call only scroll.`;
|
|
273
|
+
} else {
|
|
274
|
+
markerMatchGuidance = `\n\nMatch found: "${marker.label}" (${marker.id}) is an element. Use 'scroll' tool.`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const systemPrompt = `You are a helpful AI agent embedded in a web application. Help users navigate, interact with UI elements, and resolve errors.
|
|
279
|
+
|
|
280
|
+
${contextPrompt}${recentMarkerContext}${markerMatchGuidance}
|
|
281
|
+
|
|
282
|
+
Reading marker values:
|
|
283
|
+
- You can read current values from markers in the context above
|
|
284
|
+
- For input/textarea elements: The "Current value" field shows what's currently entered (password fields show "[REDACTED]" for privacy)
|
|
285
|
+
- For select elements: The "Available options" shows all choices, and "Selected" shows what's currently selected
|
|
286
|
+
- When users ask "what's in [field]" or "what's the value of [marker]", read the value from the context and respond with it
|
|
287
|
+
- You don't need a tool to read values - they're already in the context provided above
|
|
288
|
+
|
|
289
|
+
Security rules:
|
|
290
|
+
- NEVER fill password fields - if a user asks to fill a password, politely explain that sending passwords is a security risk and they should enter it manually
|
|
291
|
+
- Password field values are redacted as "[REDACTED]" in the context for privacy protection
|
|
292
|
+
|
|
293
|
+
Pronoun resolution:
|
|
294
|
+
- When the user says "it", "that", "this", they're referring to the most recently discussed marker
|
|
295
|
+
- Look at the recent conversation history (shown above as "Recently discussed") to identify which specific marker was discussed
|
|
296
|
+
- Prefer specific interactive elements (buttons, inputs, links) over parent sections (those with "section" or "slide" in the label/id) when resolving pronouns
|
|
297
|
+
- Example: If user asked about "password input" and then says "highlight it", use the password input marker (e.g. fill_demo_password), NOT the parent section marker (e.g. slide_fill_input)
|
|
298
|
+
|
|
299
|
+
Navigation rules:
|
|
300
|
+
- "navigate to [element]" = scroll to that element (use 'scroll' tool)
|
|
301
|
+
- "navigate to [route]" = route navigation (use 'navigate' tool with route path)
|
|
302
|
+
- If marker matches and is in inViewMarkerIds + elementType='a' → use 'click'
|
|
303
|
+
- If no marker matches → use 'navigate' with route path
|
|
304
|
+
- For affirmative responses after discussing a marker, interact with that marker using the appropriate tool.
|
|
305
|
+
|
|
306
|
+
Scroll-then-act rule (CRITICAL):
|
|
307
|
+
- When the user wants to INTERACT with an element (click, press, increment, submit, fill, etc.) and the target marker is NOT in inViewMarkerIds: you MUST emit TWO tool calls in order—(1) 'scroll' to that marker, (2) the action ('click', 'fillInput', etc.). Emitting only 'scroll' is WRONG; the user asked for an action, so you must call both scroll and the action.
|
|
308
|
+
- Prefer the specific interactive element for the action (e.g. the button click_demo_increment for "increment counter"), not a parent section (e.g. slide_click). Emit one scroll to the exact target element, then the action.
|
|
309
|
+
- If the user only wants to see or navigate to an element (no click/fill), use a single 'scroll' to that element.
|
|
310
|
+
|
|
311
|
+
Relative scrolling rules (for "scroll to next" or "scroll to previous"):
|
|
312
|
+
- Markers are listed in document order (top to bottom) in the markers array
|
|
313
|
+
- CRITICAL: Always skip markers that are in inViewMarkerIds - only scroll to markers NOT currently in view
|
|
314
|
+
- For "scroll to next": Find the first marker in the markers array that comes after any currently visible markers and is NOT in inViewMarkerIds
|
|
315
|
+
- For "scroll to previous": Find the first marker in the markers array that comes before any currently visible markers and is NOT in inViewMarkerIds
|
|
316
|
+
- This prevents getting stuck on sections already in view
|
|
317
|
+
|
|
318
|
+
Multiple actions rules:
|
|
319
|
+
- When user says "all" (e.g., "highlight all badges", "click all buttons"), generate MULTIPLE tool calls - one for each matching marker
|
|
320
|
+
- When user specifies a count (e.g., "click 10 times", "increment counter 5 times"), generate MULTIPLE tool calls - repeat the action the specified number of times
|
|
321
|
+
- CRITICAL: You can and should call the same tool multiple times in a single response when the user requests multiple actions
|
|
322
|
+
- For "highlight all [type]" or "click all [type]": Find all markers matching the type and generate one tool call per marker
|
|
323
|
+
- For "[action] [count] times": Generate [count] identical tool calls`;
|
|
324
|
+
|
|
325
|
+
const conversationMessages: Array<{ role: string; content: string }> = [
|
|
326
|
+
{ role: "system", content: systemPrompt },
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
if (request.messages && request.messages.length > 0) {
|
|
330
|
+
const recentMessages = request.messages.slice(-10);
|
|
331
|
+
for (const msg of recentMessages) {
|
|
332
|
+
conversationMessages.push({
|
|
333
|
+
role: msg.role,
|
|
334
|
+
content: msg.content,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
conversationMessages.push({ role: "user", content: request.input });
|
|
340
|
+
|
|
341
|
+
const response = await llmWithTools.invoke(conversationMessages);
|
|
342
|
+
|
|
343
|
+
const instructions: Instruction[] = [];
|
|
344
|
+
let reply = "";
|
|
345
|
+
|
|
346
|
+
if (response.content) {
|
|
347
|
+
if (typeof response.content === "string") {
|
|
348
|
+
reply = response.content;
|
|
349
|
+
} else if (Array.isArray(response.content)) {
|
|
350
|
+
reply = response.content
|
|
351
|
+
.map((c) => {
|
|
352
|
+
if (typeof c === "string") return c;
|
|
353
|
+
if (c && typeof c === "object" && "text" in c) return c.text;
|
|
354
|
+
return "";
|
|
355
|
+
})
|
|
356
|
+
.join("");
|
|
357
|
+
} else {
|
|
358
|
+
reply = String(response.content);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// LangChain stores tool calls in response.tool_calls or response.additional_kwargs.tool_calls
|
|
363
|
+
const toolCallObjects: Array<{
|
|
364
|
+
name?: string;
|
|
365
|
+
args?: Record<string, unknown>;
|
|
366
|
+
function?: {
|
|
367
|
+
name?: string;
|
|
368
|
+
arguments?: string | Record<string, unknown>;
|
|
369
|
+
};
|
|
370
|
+
}> = hasToolCalls(response)
|
|
371
|
+
? response.tool_calls || response.additional_kwargs?.tool_calls || []
|
|
372
|
+
: [];
|
|
373
|
+
|
|
374
|
+
for (const toolCall of toolCallObjects) {
|
|
375
|
+
const toolName = toolCall.name || toolCall.function?.name;
|
|
376
|
+
const toolArgs =
|
|
377
|
+
toolCall.args ||
|
|
378
|
+
(toolCall.function?.arguments
|
|
379
|
+
? typeof toolCall.function.arguments === "string"
|
|
380
|
+
? JSON.parse(toolCall.function.arguments)
|
|
381
|
+
: toolCall.function.arguments
|
|
382
|
+
: {});
|
|
383
|
+
|
|
384
|
+
if (toolName) {
|
|
385
|
+
const converted = toolRegistry.convertToolCall({
|
|
386
|
+
type: "function",
|
|
387
|
+
function: {
|
|
388
|
+
name: toolName,
|
|
389
|
+
arguments: JSON.stringify(toolArgs),
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (converted) {
|
|
394
|
+
instructions.push(converted);
|
|
395
|
+
} else {
|
|
396
|
+
try {
|
|
397
|
+
const result = await toolRegistry.executeToolCall(
|
|
398
|
+
toolName,
|
|
399
|
+
toolArgs,
|
|
400
|
+
request.context,
|
|
401
|
+
);
|
|
402
|
+
if (result && typeof result === "object" && "action" in result) {
|
|
403
|
+
instructions.push(result as Instruction);
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(`Error executing tool ${toolName}:`, error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
reply: reply || "I'm here to help!",
|
|
414
|
+
instructions: instructions.length > 0 ? instructions : undefined,
|
|
415
|
+
};
|
|
416
|
+
}
|
package/src/fastify.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { AgentRequest } from "@ai11y/core";
|
|
2
|
+
import type {
|
|
3
|
+
FastifyInstance,
|
|
4
|
+
FastifyPluginOptions,
|
|
5
|
+
FastifyReply,
|
|
6
|
+
FastifyRequest,
|
|
7
|
+
} from "fastify";
|
|
8
|
+
import { runAgent } from "./agent.js";
|
|
9
|
+
import {
|
|
10
|
+
createDefaultToolRegistry,
|
|
11
|
+
type ToolRegistry,
|
|
12
|
+
} from "./tool-registry.js";
|
|
13
|
+
import type { ServerConfig } from "./types.js";
|
|
14
|
+
|
|
15
|
+
interface FastifyAi11yOptions extends FastifyPluginOptions {
|
|
16
|
+
config: ServerConfig;
|
|
17
|
+
toolRegistry?: ToolRegistry;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fastify plugin for ai11y server
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import Fastify from 'fastify';
|
|
26
|
+
* import { ai11yPlugin } from '@ai11y/agent/fastify';
|
|
27
|
+
*
|
|
28
|
+
* const fastify = Fastify();
|
|
29
|
+
*
|
|
30
|
+
* await fastify.register(ai11yPlugin, {
|
|
31
|
+
* config: {
|
|
32
|
+
* provider: 'openai',
|
|
33
|
+
* apiKey: process.env.OPENAI_API_KEY!,
|
|
34
|
+
* model: 'gpt-4o-mini'
|
|
35
|
+
* }
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* await fastify.listen({ port: 3000 });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export async function ai11yPlugin(
|
|
42
|
+
fastify: FastifyInstance,
|
|
43
|
+
options: FastifyAi11yOptions,
|
|
44
|
+
) {
|
|
45
|
+
const { config, toolRegistry = createDefaultToolRegistry() } = options;
|
|
46
|
+
|
|
47
|
+
// Validate config
|
|
48
|
+
if (!config.apiKey) {
|
|
49
|
+
throw new Error("API key is required");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* POST /ai11y/agent
|
|
54
|
+
* Main endpoint for agent requests
|
|
55
|
+
*/
|
|
56
|
+
fastify.post<{ Body: AgentRequest }>(
|
|
57
|
+
"/ai11y/agent",
|
|
58
|
+
async (
|
|
59
|
+
request: FastifyRequest<{ Body: AgentRequest }>,
|
|
60
|
+
reply: FastifyReply,
|
|
61
|
+
) => {
|
|
62
|
+
try {
|
|
63
|
+
const response = await runAgent(request.body, config, toolRegistry);
|
|
64
|
+
return reply.send(response);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
fastify.log.error(error, "Error processing agent request");
|
|
67
|
+
return reply.status(500).send({
|
|
68
|
+
error: "Failed to process agent request",
|
|
69
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* GET /ai11y/health
|
|
77
|
+
* Health check endpoint
|
|
78
|
+
*/
|
|
79
|
+
fastify.get("/ai11y/health", async (_request, reply) => {
|
|
80
|
+
return reply.send({ status: "ok" });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Helper function to create a tool registry with custom tools.
|
|
86
|
+
* Returns a ToolRegistry instance that supports method chaining.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const registry = createToolRegistry()
|
|
91
|
+
* .register({
|
|
92
|
+
* name: "custom_action",
|
|
93
|
+
* description: "Perform a custom action",
|
|
94
|
+
* parameters: {
|
|
95
|
+
* type: "object",
|
|
96
|
+
* properties: {
|
|
97
|
+
* param: { type: "string", description: "A parameter" }
|
|
98
|
+
* },
|
|
99
|
+
* required: ["param"]
|
|
100
|
+
* }
|
|
101
|
+
* }, async (args) => {
|
|
102
|
+
* // Execute custom logic
|
|
103
|
+
* return { success: true };
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function createToolRegistry(): ToolRegistry {
|
|
108
|
+
return createDefaultToolRegistry();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type { ServerConfig } from "./types.js";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai11y Server
|
|
3
|
+
*
|
|
4
|
+
* Server-side agent implementation for ai11y.
|
|
5
|
+
* Handles LLM API calls securely on the server using LangChain.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { runAgent } from "./agent.js";
|
|
9
|
+
export { createLLM } from "./llm-provider.js";
|
|
10
|
+
export { createDefaultToolRegistry, ToolRegistry } from "./tool-registry.js";
|
|
11
|
+
export type { ServerConfig } from "./types.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Provider Factory
|
|
3
|
+
* Creates LangChain-compatible LLM instances for OpenAI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
7
|
+
import type { ServerConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a LangChain LLM instance for OpenAI
|
|
11
|
+
*/
|
|
12
|
+
export async function createLLM(config: ServerConfig): Promise<BaseChatModel> {
|
|
13
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
14
|
+
return new ChatOpenAI({
|
|
15
|
+
modelName: config.model || "gpt-5-nano",
|
|
16
|
+
temperature: config.temperature ?? 0,
|
|
17
|
+
openAIApiKey: config.apiKey,
|
|
18
|
+
configuration: config.baseURL
|
|
19
|
+
? {
|
|
20
|
+
baseURL: config.baseURL,
|
|
21
|
+
}
|
|
22
|
+
: undefined,
|
|
23
|
+
});
|
|
24
|
+
}
|