gx402 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/index.ts +1 -0
- package/package.json +18 -0
- package/patch +290 -0
- package/test/basic.test.ts +73 -0
- package/test/edge.test.ts +237 -0
- package/test/streaming.test.ts +78 -0
- package/tsconfig.json +28 -0
- package/uai.ts +835 -0
package/README.md
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("Hello via Bun!");
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gx402",
|
|
3
|
+
"module": "uai.ts",
|
|
4
|
+
"main": "./uai.ts",
|
|
5
|
+
"version": "1.3.5",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": false,
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@types/bun": "latest"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"typescript": "^5"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@ments/utils": "1.0.2",
|
|
16
|
+
"zod": "^3.25.67"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/patch
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
diff --git a/uai.ts b/uai.ts
|
|
2
|
+
index 1234567..abcdefg 100644
|
|
3
|
+
--- a/uai.ts
|
|
4
|
+
+++ b/uai.ts
|
|
5
|
+
@@ -33,10 +33,17 @@ export interface AgentConfig<I extends z.ZodObject<any>, O extends z.ZodObject<
|
|
6
|
+
|
|
7
|
+
// ### Progress Callback Types
|
|
8
|
+
export interface ProgressUpdate {
|
|
9
|
+
- stage: "server_selection" | "tool_discovery" | "tool_invocation" | "response_generation";
|
|
10
|
+
+ stage: "server_selection" | "tool_discovery" | "tool_invocation" | "response_generation" | "streaming";
|
|
11
|
+
message: string;
|
|
12
|
+
data?: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
+export interface StreamingUpdate {
|
|
16
|
+
+ stage: "streaming";
|
|
17
|
+
+ field: string;
|
|
18
|
+
+ value: string;
|
|
19
|
+
+}
|
|
20
|
+
+
|
|
21
|
+
export type ProgressCallback = (update: ProgressUpdate) => void;
|
|
22
|
+
+export type StreamingCallback = (update: StreamingUpdate) => void;
|
|
23
|
+
|
|
24
|
+
// ### XML Utilities
|
|
25
|
+
@@ -102,10 +109,11 @@ function xmlToObj(xmlContent: string): any {
|
|
26
|
+
/** Calls an LLM API with measurement for logging. */
|
|
27
|
+
async function callLLM(
|
|
28
|
+
llm: LLMType,
|
|
29
|
+
messages: Array<{ role: string; content: string }>,
|
|
30
|
+
options: { temperature?: number; maxTokens?: number } = {},
|
|
31
|
+
- measureFn?: typeof measure
|
|
32
|
+
+ measureFn?: typeof measure,
|
|
33
|
+
+ streamingCallback?: StreamingCallback
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const executeCall = async (measure: typeof measure) => {
|
|
36
|
+
const { temperature = 0.7, maxTokens = 4000 } = options;
|
|
37
|
+
@@ -119,17 +127,20 @@ async function callLLM(
|
|
38
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY!;
|
|
39
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
40
|
+
url = "https://api.anthropic.com/v1/messages";
|
|
41
|
+
- body = { model: llm, max_tokens: maxTokens, messages };
|
|
42
|
+
+ body = { model: llm, max_tokens: maxTokens, messages, stream: !!streamingCallback };
|
|
43
|
+
} else if (llm.includes("deepseek")) {
|
|
44
|
+
headers["Authorization"] = `Bearer ${process.env.DEEPSEEK_API_KEY}`;
|
|
45
|
+
url = "https://api.deepseek.com/v1/chat/completions";
|
|
46
|
+
- body = { model: llm, temperature, messages, max_tokens: maxTokens };
|
|
47
|
+
+ body = { model: llm, temperature, messages, max_tokens: maxTokens, stream: !!streamingCallback };
|
|
48
|
+
} else {
|
|
49
|
+
headers["Authorization"] = `Bearer ${process.env.OPENAI_API_KEY}`;
|
|
50
|
+
url = "https://api.openai.com/v1/chat/completions";
|
|
51
|
+
- body = { model: llm, temperature, messages, max_tokens: maxTokens };
|
|
52
|
+
+ body = { model: llm, temperature, messages, max_tokens: maxTokens, stream: !!streamingCallback };
|
|
53
|
+
}
|
|
54
|
+
+
|
|
55
|
+
const requestBodyStr = JSON.stringify(body);
|
|
56
|
+
+
|
|
57
|
+
+ if (!streamingCallback) {
|
|
58
|
+
const response = await measure(
|
|
59
|
+
async () => {
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
@@ -144,7 +155,98 @@ async function callLLM(
|
|
62
|
+
`HTTP ${llm} API call - Body: ${requestBodyStr.substring(0, 200)}...`
|
|
63
|
+
);
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
return llm.includes("claude") ? data.content[0].text : data.choices[0].message.content;
|
|
66
|
+
+ } else {
|
|
67
|
+
+ // Streaming response handling
|
|
68
|
+
+ const response = await measure(
|
|
69
|
+
+ async () => {
|
|
70
|
+
+ const res = await fetch(url, {
|
|
71
|
+
+ method: "POST",
|
|
72
|
+
+ headers,
|
|
73
|
+
+ body: requestBodyStr,
|
|
74
|
+
+ });
|
|
75
|
+
+ if (!res.ok) {
|
|
76
|
+
+ const errorText = await res.text();
|
|
77
|
+
+ throw new Error(`LLM API error: ${errorText}`);
|
|
78
|
+
+ }
|
|
79
|
+
+ return res;
|
|
80
|
+
+ },
|
|
81
|
+
+ `HTTP ${llm} streaming API call - Body: ${requestBodyStr.substring(0, 200)}...`
|
|
82
|
+
+ );
|
|
83
|
+
+
|
|
84
|
+
+ const reader = response.body?.getReader();
|
|
85
|
+
+ if (!reader) {
|
|
86
|
+
+ throw new Error("No readable stream available");
|
|
87
|
+
+ }
|
|
88
|
+
+
|
|
89
|
+
+ const decoder = new TextDecoder();
|
|
90
|
+
+ let fullResponse = "";
|
|
91
|
+
+ let currentField = "";
|
|
92
|
+
+ let currentValue = "";
|
|
93
|
+
+ let insideTag = false;
|
|
94
|
+
+ let buffer = "";
|
|
95
|
+
+
|
|
96
|
+
+ try {
|
|
97
|
+
+ while (true) {
|
|
98
|
+
+ const { done, value } = await reader.read();
|
|
99
|
+
+ if (done) break;
|
|
100
|
+
+
|
|
101
|
+
+ const chunk = decoder.decode(value, { stream: true });
|
|
102
|
+
+ buffer += chunk;
|
|
103
|
+
+
|
|
104
|
+
+ // Process complete lines for different providers
|
|
105
|
+
+ const lines = buffer.split('\n');
|
|
106
|
+
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
107
|
+
+
|
|
108
|
+
+ for (const line of lines) {
|
|
109
|
+
+ let content = '';
|
|
110
|
+
+
|
|
111
|
+
+ if (llm.includes("claude")) {
|
|
112
|
+
+ // Anthropic streaming format
|
|
113
|
+
+ if (line.startsWith('data: ') && !line.includes('[DONE]')) {
|
|
114
|
+
+ try {
|
|
115
|
+
+ const data = JSON.parse(line.slice(6));
|
|
116
|
+
+ if (data.type === 'content_block_delta' && data.delta?.text) {
|
|
117
|
+
+ content = data.delta.text;
|
|
118
|
+
+ }
|
|
119
|
+
+ } catch (e) {
|
|
120
|
+
+ continue;
|
|
121
|
+
+ }
|
|
122
|
+
+ }
|
|
123
|
+
+ } else {
|
|
124
|
+
+ // OpenAI/DeepSeek streaming format
|
|
125
|
+
+ if (line.startsWith('data: ') && !line.includes('[DONE]')) {
|
|
126
|
+
+ try {
|
|
127
|
+
+ const data = JSON.parse(line.slice(6));
|
|
128
|
+
+ if (data.choices?.[0]?.delta?.content) {
|
|
129
|
+
+ content = data.choices[0].delta.content;
|
|
130
|
+
+ }
|
|
131
|
+
+ } catch (e) {
|
|
132
|
+
+ continue;
|
|
133
|
+
+ }
|
|
134
|
+
+ }
|
|
135
|
+
+ }
|
|
136
|
+
+
|
|
137
|
+
+ if (content) {
|
|
138
|
+
+ fullResponse += content;
|
|
139
|
+
+
|
|
140
|
+
+ // Parse XML tags to detect field changes
|
|
141
|
+
+ for (const char of content) {
|
|
142
|
+
+ if (char === '<') {
|
|
143
|
+
+ insideTag = true;
|
|
144
|
+
+ currentValue = "";
|
|
145
|
+
+ } else if (char === '>' && insideTag) {
|
|
146
|
+
+ insideTag = false;
|
|
147
|
+
+ if (!currentValue.startsWith('/')) {
|
|
148
|
+
+ currentField = currentValue;
|
|
149
|
+
+ currentValue = "";
|
|
150
|
+
+ }
|
|
151
|
+
+ } else if (insideTag) {
|
|
152
|
+
+ currentValue += char;
|
|
153
|
+
+ } else if (currentField && char !== '\n') {
|
|
154
|
+
+ currentValue += char;
|
|
155
|
+
+ streamingCallback({ stage: "streaming", field: currentField, value: currentValue });
|
|
156
|
+
+ }
|
|
157
|
+
+ }
|
|
158
|
+
+ }
|
|
159
|
+
+ }
|
|
160
|
+
+ }
|
|
161
|
+
+ } finally {
|
|
162
|
+
+ reader.releaseLock();
|
|
163
|
+
+ }
|
|
164
|
+
+
|
|
165
|
+
+ return fullResponse;
|
|
166
|
+
+ }
|
|
167
|
+
};
|
|
168
|
+
return measureFn
|
|
169
|
+
? await measureFn(executeCall, `LLM call to ${llm}`)
|
|
170
|
+
@@ -254,7 +356,8 @@ export class Agent<I extends z.ZodObject<any>, O extends z.ZodObject<any>> {
|
|
171
|
+
stage: "response_generation",
|
|
172
|
+
message: "Generating final response...",
|
|
173
|
+
});
|
|
174
|
+
+
|
|
175
|
+
try {
|
|
176
|
+
const response = await measure(
|
|
177
|
+
- async (measure) => await this.generateResponse(validatedInput, toolResults, measure),
|
|
178
|
+
+ async (measure) => await this.generateResponse(validatedInput, toolResults, measure, progressCallback),
|
|
179
|
+
"Generate AI response with toolResults: " + JSON.stringify(toolResults),
|
|
180
|
+
);
|
|
181
|
+
return await measure(
|
|
182
|
+
@@ -409,7 +512,16 @@ export class Agent<I extends z.ZodObject<any>, O extends z.ZodObject<any>> {
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Generates the final response based on input and tool results. */
|
|
186
|
+
- private async generateResponse(input: any, toolResults: Record<string, any>, measureFn: typeof measure): Promise<any> {
|
|
187
|
+
+ private async generateResponse(
|
|
188
|
+
+ input: any,
|
|
189
|
+
+ toolResults: Record<string, any>,
|
|
190
|
+
+ measureFn: typeof measure,
|
|
191
|
+
+ progressCallback?: ProgressCallback
|
|
192
|
+
+ ): Promise<any> {
|
|
193
|
+
+ const streamingCallback: StreamingCallback | undefined = progressCallback ?
|
|
194
|
+
+ (update) => progressCallback(update as ProgressUpdate) :
|
|
195
|
+
+ undefined;
|
|
196
|
+
+
|
|
197
|
+
return await measureFn(
|
|
198
|
+
async (measure) => {
|
|
199
|
+
const systemPrompt = this.config.systemPrompt || null;
|
|
200
|
+
@@ -434,7 +546,8 @@ export class Agent<I extends z.ZodObject<any>, O extends z.ZodObject<any>> {
|
|
201
|
+
const response = await measure(() => callLLM(
|
|
202
|
+
this.config.llm,
|
|
203
|
+
messages,
|
|
204
|
+
- { temperature: this.config.temperature || 0.7, maxTokens: this.config.maxTokens || 4000 },
|
|
205
|
+
+ { temperature: this.config.temperature || 0.7, maxTokens: this.config.maxTokens || 4000 },
|
|
206
|
+
- measure
|
|
207
|
+
+ measure,
|
|
208
|
+
+ streamingCallback
|
|
209
|
+
), userPrompt);
|
|
210
|
+
const parsed = await measure(
|
|
211
|
+
async () => xmlToObj(response),
|
|
212
|
+
diff --git a/test/streaming.test.ts b/test/streaming.test.ts
|
|
213
|
+
new file mode 100644
|
|
214
|
+
index 0000000..1234567
|
|
215
|
+
--- /dev/null
|
|
216
|
+
+++ b/test/streaming.test.ts
|
|
217
|
+
@@ -0,0 +1,72 @@
|
|
218
|
+
+import { test, expect, it, describe } from 'bun:test';
|
|
219
|
+
+import { z } from 'zod';
|
|
220
|
+
+import { Agent, LLM, ProgressUpdate, StreamingUpdate } from '../uai';
|
|
221
|
+
+
|
|
222
|
+
+describe('UAI Streaming Tests', () => {
|
|
223
|
+
+ it('should emit streaming progress updates token-by-token', async () => {
|
|
224
|
+
+ const streamingAgent = new Agent({
|
|
225
|
+
+ llm: LLM['gpt-4o-mini'],
|
|
226
|
+
+ inputFormat: z.object({
|
|
227
|
+
+ question: z.string(),
|
|
228
|
+
+ }),
|
|
229
|
+
+ outputFormat: z.object({
|
|
230
|
+
+ analysis: z.string().describe("Step-by-step analysis of the question"),
|
|
231
|
+
+ answer: z.string().describe("The final answer to the question"),
|
|
232
|
+
+ }),
|
|
233
|
+
+ temperature: 0.7,
|
|
234
|
+
+ });
|
|
235
|
+
+
|
|
236
|
+
+ const input = {
|
|
237
|
+
+ question: 'What are the benefits of renewable energy?',
|
|
238
|
+
+ };
|
|
239
|
+
+
|
|
240
|
+
+ const streamingUpdates: StreamingUpdate[] = [];
|
|
241
|
+
+ const progressUpdates: ProgressUpdate[] = [];
|
|
242
|
+
+
|
|
243
|
+
+ try {
|
|
244
|
+
+ const result = await streamingAgent.run(input, (update) => {
|
|
245
|
+
+ if (update.stage === 'streaming') {
|
|
246
|
+
+ streamingUpdates.push(update as StreamingUpdate);
|
|
247
|
+
+ console.log(`Field: ${update.field}, Value: ${update.value.slice(-20)}...`);
|
|
248
|
+
+ } else {
|
|
249
|
+
+ progressUpdates.push(update);
|
|
250
|
+
+ }
|
|
251
|
+
+ });
|
|
252
|
+
+
|
|
253
|
+
+ console.log('\n--- Final Result ---');
|
|
254
|
+
+ console.log('Analysis:', result.analysis);
|
|
255
|
+
+ console.log('Answer:', result.answer);
|
|
256
|
+
+
|
|
257
|
+
+ // Verify we received streaming updates
|
|
258
|
+
+ expect(streamingUpdates.length).toBeGreaterThan(0);
|
|
259
|
+
+
|
|
260
|
+
+ // Verify we have updates for both fields
|
|
261
|
+
+ const fieldsUpdated = new Set(streamingUpdates.map(u => u.field));
|
|
262
|
+
+ expect(fieldsUpdated.has('analysis')).toBe(true);
|
|
263
|
+
+ expect(fieldsUpdated.has('answer')).toBe(true);
|
|
264
|
+
+
|
|
265
|
+
+ // Verify the streaming updates show progressive content building
|
|
266
|
+
+ const analysisUpdates = streamingUpdates.filter(u => u.field === 'analysis');
|
|
267
|
+
+ const answerUpdates = streamingUpdates.filter(u => u.field === 'answer');
|
|
268
|
+
+
|
|
269
|
+
+ expect(analysisUpdates.length).toBeGreaterThan(1);
|
|
270
|
+
+ expect(answerUpdates.length).toBeGreaterThan(1);
|
|
271
|
+
+
|
|
272
|
+
+ // Verify content is progressively building (each update should contain more text)
|
|
273
|
+
+ for (let i = 1; i < analysisUpdates.length; i++) {
|
|
274
|
+
+ expect(analysisUpdates[i].value.length).toBeGreaterThanOrEqual(analysisUpdates[i-1].value.length);
|
|
275
|
+
+ }
|
|
276
|
+
+
|
|
277
|
+
+ // Verify final result matches last streaming update values
|
|
278
|
+
+ const lastAnalysisUpdate = analysisUpdates[analysisUpdates.length - 1];
|
|
279
|
+
+ const lastAnswerUpdate = answerUpdates[answerUpdates.length - 1];
|
|
280
|
+
+
|
|
281
|
+
+ expect(result.analysis.trim()).toBe(lastAnalysisUpdate.value.trim());
|
|
282
|
+
+ expect(result.answer.trim()).toBe(lastAnswerUpdate.value.trim());
|
|
283
|
+
+
|
|
284
|
+
+ console.log(`\n✅ Received ${streamingUpdates.length} streaming updates across ${fieldsUpdated.size} fields`);
|
|
285
|
+
+
|
|
286
|
+
+ } catch (error) {
|
|
287
|
+
+ console.warn('\n⚠️ Streaming test skipped (this is expected if API keys are not configured):', error.message);
|
|
288
|
+
+ }
|
|
289
|
+
+ }, { timeout: 60000 });
|
|
290
|
+
+});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { test, expect, it, describe } from 'bun:test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Agent, LLM, ProgressUpdate } from '../uai';
|
|
4
|
+
|
|
5
|
+
describe('UAI Library Advanced Tests', () => {
|
|
6
|
+
it('should generate a structured response with a thinking process based on persona', async () => {
|
|
7
|
+
// 1. Define an Agent with more meaningful input and output schemas
|
|
8
|
+
const personaAgent = new Agent({
|
|
9
|
+
llm: LLM['gpt-4o-mini'],
|
|
10
|
+
// Input requires context for how the AI should behave
|
|
11
|
+
inputFormat: z.object({
|
|
12
|
+
user_query: z.string(),
|
|
13
|
+
personality: z.string(),
|
|
14
|
+
tone: z.enum(['humorous', 'formal', 'serious']),
|
|
15
|
+
}),
|
|
16
|
+
// Output is structured to separate reasoning from the final answer
|
|
17
|
+
outputFormat: z.object({
|
|
18
|
+
thinkingProcess: z.string().describe("My step-by-step reasoning for crafting the response, from the persona's point of view."),
|
|
19
|
+
finalResponse: z.string().describe("The final, crafted response for the user, delivered in character."),
|
|
20
|
+
suggestedMarkup: z.string().describe("A sample HTML block based on the response, which could be e.g. a div or a blockquote."),
|
|
21
|
+
status: z.boolean().describe("is it done or not"),
|
|
22
|
+
}),
|
|
23
|
+
temperature: 0.7,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const input = {
|
|
27
|
+
user_query: 'What is the best way to invest $100?',
|
|
28
|
+
personality: 'A cautious, old sea captain who has seen many treasures lost to recklessness.',
|
|
29
|
+
tone: 'serious' as const, // 'as const' helps TypeScript infer the most specific type
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const streamingResponse = {};
|
|
34
|
+
// 2. Run the agent and await the structured result
|
|
35
|
+
const { finalResponse, thinkingProcess, suggestedMarkup, status } = await personaAgent.run(input, (update: any) => {
|
|
36
|
+
if (update.stage === 'streaming') {
|
|
37
|
+
streamingResponse[update.field] = (streamingResponse[update.field] || '') + update.value;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 3. Log the output for easy debugging during test runs
|
|
42
|
+
console.log('\n--- Thinking Process ---');
|
|
43
|
+
console.log(thinkingProcess);
|
|
44
|
+
console.log('\n--- Final Response ---');
|
|
45
|
+
console.log(finalResponse);
|
|
46
|
+
console.log('\n--- Suggested Markup ---');
|
|
47
|
+
console.log(suggestedMarkup);
|
|
48
|
+
console.log('\n--- Status ---');
|
|
49
|
+
console.log(status);
|
|
50
|
+
|
|
51
|
+
// check boolean type prompts
|
|
52
|
+
expect(status).toBe(true);
|
|
53
|
+
|
|
54
|
+
// 4. Verify the structure and content of the response
|
|
55
|
+
expect(thinkingProcess).toBeTypeOf('string');
|
|
56
|
+
expect(thinkingProcess.length).toBeGreaterThan(1); // Assert it's not empty
|
|
57
|
+
|
|
58
|
+
expect(finalResponse).toBeTypeOf('string');
|
|
59
|
+
expect(finalResponse.toLowerCase()).toInclude('sea');
|
|
60
|
+
expect(suggestedMarkup).toBeTypeOf('string');
|
|
61
|
+
// Assert that we received a string that contains markup, which would have failed before the fix.
|
|
62
|
+
expect(suggestedMarkup.trim()).toStartWith('<');
|
|
63
|
+
expect(suggestedMarkup.trim()).toEndWith('>');
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
console.log('\n✅ Agent generated a valid, structured response that fits the persona.');
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// This block allows the test to pass gracefully if API keys aren't configured.
|
|
70
|
+
console.warn('\n⚠️ API call test skipped (this is expected if API keys are not configured):', error.message);
|
|
71
|
+
}
|
|
72
|
+
}, { timeout: 30000 });
|
|
73
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { test, expect, it, describe } from 'bun:test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Agent, LLM, ProgressUpdate } from '../uai';
|
|
4
|
+
|
|
5
|
+
describe('UAI Library Tests', () => {
|
|
6
|
+
it('should process complex input validation successfully', async () => {
|
|
7
|
+
const complexAgent = new Agent({
|
|
8
|
+
llm: LLM.gpt4o,
|
|
9
|
+
inputFormat: z.object({
|
|
10
|
+
userQuery: z.string().describe('The main question or request from the user'),
|
|
11
|
+
context: z.object({
|
|
12
|
+
sessionId: z.string().describe('Unique identifier for this conversation session'),
|
|
13
|
+
previousInteraction: z.string().optional().describe('Summary of previous interaction if any'),
|
|
14
|
+
userPreferences: z.object({
|
|
15
|
+
responseStyle: z.enum(['formal', 'casual', 'technical']).describe('Preferred communication style'),
|
|
16
|
+
detailLevel: z.enum(['brief', 'moderate', 'comprehensive']).describe('Desired level of detail in responses'),
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
metadata: z.object({
|
|
20
|
+
timestamp: z.string().describe('ISO timestamp of the request'),
|
|
21
|
+
platform: z.string().describe('Platform or device used to make the request'),
|
|
22
|
+
urgency: z.enum(['low', 'medium', 'high']).describe('Priority level of the request'),
|
|
23
|
+
}),
|
|
24
|
+
}),
|
|
25
|
+
outputFormat: z.object({
|
|
26
|
+
primaryResponse: z.string().describe('The main answer or response to the user query'),
|
|
27
|
+
analysis: z.object({
|
|
28
|
+
confidence: z.number().describe('Confidence level from 0 to 1 in the response accuracy'),
|
|
29
|
+
reasoning: z.string().describe('Brief explanation of the reasoning process used'),
|
|
30
|
+
complexity: z.enum(['simple', 'moderate', 'complex']).describe('Assessed complexity of the query'),
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await complexAgent.run({
|
|
37
|
+
userQuery: 'What are the best practices for sustainable software development?',
|
|
38
|
+
context: {
|
|
39
|
+
sessionId: 'session_12345',
|
|
40
|
+
previousInteraction: 'User previously asked about green computing',
|
|
41
|
+
userPreferences: {
|
|
42
|
+
responseStyle: 'technical',
|
|
43
|
+
detailLevel: 'comprehensive',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
metadata: {
|
|
47
|
+
timestamp: '2024-12-25T10:30:00Z',
|
|
48
|
+
platform: 'web_browser',
|
|
49
|
+
urgency: 'medium',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
console.log('✅ Complex input validation and processing successful');
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('⚠️ API call test skipped (possibly due to API keys):', error.message);
|
|
55
|
+
if (!error.message.includes('API') && !error.message.includes('401') && !error.message.includes('403')) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should throw error for arrays in output schema', () => {
|
|
62
|
+
expect(() => {
|
|
63
|
+
new Agent({
|
|
64
|
+
llm: LLM.gpt4o,
|
|
65
|
+
inputFormat: z.object({
|
|
66
|
+
message: z.string(),
|
|
67
|
+
}),
|
|
68
|
+
outputFormat: z.object({
|
|
69
|
+
responses: z.array(z.string()), // This should throw an error
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}).toThrow('Arrays are not supported in output schema. Found array at path: responses. Use individual fields like responses_1, responses_2 instead.');
|
|
73
|
+
|
|
74
|
+
console.log('✅ Array validation working correctly');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw error for nested arrays in output schema', () => {
|
|
78
|
+
expect(() => {
|
|
79
|
+
new Agent({
|
|
80
|
+
llm: LLM.gpt4o,
|
|
81
|
+
inputFormat: z.object({
|
|
82
|
+
message: z.string(),
|
|
83
|
+
}),
|
|
84
|
+
outputFormat: z.object({
|
|
85
|
+
data: z.object({
|
|
86
|
+
items: z.array(z.string()), // Nested array should also throw
|
|
87
|
+
}),
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}).toThrow('Arrays are not supported in output schema. Found array at path: data.items. Use individual fields like items_1, items_2 instead.');
|
|
91
|
+
|
|
92
|
+
console.log('✅ Nested array validation working correctly');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should validate input types correctly', async () => {
|
|
96
|
+
const agent = new Agent({
|
|
97
|
+
llm: LLM.gpt4o,
|
|
98
|
+
inputFormat: z.object({
|
|
99
|
+
message: z.string(),
|
|
100
|
+
count: z.number(),
|
|
101
|
+
}),
|
|
102
|
+
outputFormat: z.object({
|
|
103
|
+
response: z.string(),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await agent.run({
|
|
109
|
+
message: 123 as any, // Wrong type
|
|
110
|
+
count: 'not a number' as any, // Wrong type
|
|
111
|
+
});
|
|
112
|
+
expect(false).toBe(true); // Should not reach here
|
|
113
|
+
} catch (error) {
|
|
114
|
+
expect(error.name).toBe('ZodError');
|
|
115
|
+
console.log('✅ Input type validation working correctly');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should validate enum values correctly', async () => {
|
|
120
|
+
const agent = new Agent({
|
|
121
|
+
llm: LLM.gpt4o,
|
|
122
|
+
inputFormat: z.object({
|
|
123
|
+
message: z.string(),
|
|
124
|
+
priority: z.enum(['low', 'medium', 'high']),
|
|
125
|
+
}),
|
|
126
|
+
outputFormat: z.object({
|
|
127
|
+
response: z.string(),
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await agent.run({
|
|
133
|
+
message: 'test',
|
|
134
|
+
priority: 'invalid_priority' as any, // Invalid enum value
|
|
135
|
+
});
|
|
136
|
+
expect(false).toBe(true); // Should not reach here
|
|
137
|
+
} catch (error) {
|
|
138
|
+
expect(error.name).toBe('ZodError');
|
|
139
|
+
console.log('✅ Enum validation working correctly');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should support all LLM model configurations', () => {
|
|
144
|
+
const models = [LLM.gpt4o, LLM.gpt4, LLM.claude, LLM.deepseek];
|
|
145
|
+
|
|
146
|
+
for (const model of models) {
|
|
147
|
+
const agent = new Agent({
|
|
148
|
+
llm: model,
|
|
149
|
+
inputFormat: z.object({
|
|
150
|
+
query: z.string(),
|
|
151
|
+
}),
|
|
152
|
+
outputFormat: z.object({
|
|
153
|
+
result: z.string(),
|
|
154
|
+
metadata: z.object({
|
|
155
|
+
model: z.string(),
|
|
156
|
+
confidence: z.number(),
|
|
157
|
+
}),
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(agent).toBeDefined();
|
|
162
|
+
console.log(`✅ ${model} configuration valid`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle optional fields correctly', async () => {
|
|
167
|
+
const agent = new Agent({
|
|
168
|
+
llm: LLM.gpt4o,
|
|
169
|
+
inputFormat: z.object({
|
|
170
|
+
required: z.string(),
|
|
171
|
+
optional: z.string().optional(),
|
|
172
|
+
}),
|
|
173
|
+
outputFormat: z.object({
|
|
174
|
+
answer: z.string(),
|
|
175
|
+
extra: z.string().optional(),
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await agent.run({
|
|
181
|
+
required: 'test value',
|
|
182
|
+
// optional field omitted
|
|
183
|
+
});
|
|
184
|
+
console.log('✅ Optional fields handled correctly');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.warn('⚠️ Optional fields test skipped due to API limitations');
|
|
187
|
+
if (!error.message.includes('API') && !error.message.includes('401') && !error.message.includes('403')) {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should handle nullable fields correctly', async () => {
|
|
194
|
+
const agent = new Agent({
|
|
195
|
+
llm: LLM.gpt4o,
|
|
196
|
+
inputFormat: z.object({
|
|
197
|
+
required: z.string(),
|
|
198
|
+
nullable: z.string().nullable(),
|
|
199
|
+
}),
|
|
200
|
+
outputFormat: z.object({
|
|
201
|
+
answer: z.string(),
|
|
202
|
+
extra: z.string().nullable(),
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await agent.run({
|
|
208
|
+
required: 'test value',
|
|
209
|
+
nullable: null,
|
|
210
|
+
});
|
|
211
|
+
console.log('✅ Nullable fields handled correctly');
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.warn('⚠️ Nullable fields test skipped due to API limitations');
|
|
214
|
+
if (!error.message.includes('API') && !error.message.includes('401') && !error.message.includes('403')) {
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should configure agent with custom temperature and maxTokens', () => {
|
|
221
|
+
const agent = new Agent({
|
|
222
|
+
llm: LLM.gpt4o,
|
|
223
|
+
inputFormat: z.object({
|
|
224
|
+
query: z.string(),
|
|
225
|
+
}),
|
|
226
|
+
outputFormat: z.object({
|
|
227
|
+
response: z.string(),
|
|
228
|
+
}),
|
|
229
|
+
temperature: 0.1,
|
|
230
|
+
maxTokens: 500,
|
|
231
|
+
systemPrompt: 'You are a precise assistant that gives brief responses.',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(agent).toBeDefined();
|
|
235
|
+
console.log('✅ Custom configuration parameters working correctly');
|
|
236
|
+
});
|
|
237
|
+
});
|