@lantos1618/better-ui 0.1.0
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/LICENSE +21 -0
- package/README.md +190 -0
- package/lib/aui/README.md +136 -0
- package/lib/aui/__tests__/aui-complete.test.ts +251 -0
- package/lib/aui/__tests__/aui-comprehensive.test.ts +376 -0
- package/lib/aui/__tests__/aui-concise.test.ts +278 -0
- package/lib/aui/__tests__/aui-integration.test.ts +309 -0
- package/lib/aui/__tests__/aui-simple.test.ts +116 -0
- package/lib/aui/__tests__/aui.test.ts +269 -0
- package/lib/aui/__tests__/concise-api.test.ts +165 -0
- package/lib/aui/__tests__/core.test.ts +265 -0
- package/lib/aui/__tests__/simple-api.test.ts +200 -0
- package/lib/aui/ai-assistant.ts +408 -0
- package/lib/aui/ai-control.ts +353 -0
- package/lib/aui/client/use-aui.ts +55 -0
- package/lib/aui/client-control.ts +551 -0
- package/lib/aui/client-executor.ts +417 -0
- package/lib/aui/components/ToolRenderer.tsx +22 -0
- package/lib/aui/core.ts +137 -0
- package/lib/aui/demo.tsx +89 -0
- package/lib/aui/examples/ai-complete-demo.tsx +359 -0
- package/lib/aui/examples/ai-control-demo.tsx +356 -0
- package/lib/aui/examples/ai-control-tools.ts +308 -0
- package/lib/aui/examples/concise-api.tsx +153 -0
- package/lib/aui/examples/index.tsx +163 -0
- package/lib/aui/examples/quick-demo.tsx +91 -0
- package/lib/aui/examples/simple-demo.tsx +71 -0
- package/lib/aui/examples/simple-tools.tsx +160 -0
- package/lib/aui/examples/user-api.tsx +208 -0
- package/lib/aui/examples/user-requested.tsx +174 -0
- package/lib/aui/examples/weather-search-tools.tsx +119 -0
- package/lib/aui/examples.tsx +367 -0
- package/lib/aui/hooks/useAUITool.ts +142 -0
- package/lib/aui/hooks/useAUIToolEnhanced.ts +343 -0
- package/lib/aui/hooks/useAUITools.ts +195 -0
- package/lib/aui/index.ts +156 -0
- package/lib/aui/provider.tsx +45 -0
- package/lib/aui/server-control.ts +386 -0
- package/lib/aui/server-executor.ts +165 -0
- package/lib/aui/server.ts +167 -0
- package/lib/aui/tool-registry.ts +380 -0
- package/lib/aui/tools/advanced-examples.tsx +86 -0
- package/lib/aui/tools/ai-complete.ts +375 -0
- package/lib/aui/tools/api-tools.tsx +230 -0
- package/lib/aui/tools/data-tools.tsx +232 -0
- package/lib/aui/tools/dom-tools.tsx +202 -0
- package/lib/aui/tools/examples.ts +43 -0
- package/lib/aui/tools/file-tools.tsx +202 -0
- package/lib/aui/tools/form-tools.tsx +233 -0
- package/lib/aui/tools/index.ts +8 -0
- package/lib/aui/tools/navigation-tools.tsx +172 -0
- package/lib/aui/tools/notification-tools.ts +213 -0
- package/lib/aui/tools/state-tools.tsx +209 -0
- package/lib/aui/types.ts +47 -0
- package/lib/aui/vercel-ai.ts +100 -0
- package/package.json +51 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { AUIContext } from './core';
|
|
2
|
+
import { AIControlledTool } from './ai-control';
|
|
3
|
+
|
|
4
|
+
export interface CacheStrategy {
|
|
5
|
+
key: (toolName: string, input: any) => string;
|
|
6
|
+
ttl?: number; // Time to live in milliseconds
|
|
7
|
+
maxSize?: number; // Maximum number of cached items
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ClientExecutorOptions {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
cache?: {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
strategy?: CacheStrategy;
|
|
15
|
+
storage?: 'memory' | 'localStorage' | 'sessionStorage';
|
|
16
|
+
};
|
|
17
|
+
retry?: {
|
|
18
|
+
attempts?: number;
|
|
19
|
+
delay?: number;
|
|
20
|
+
backoff?: 'linear' | 'exponential';
|
|
21
|
+
};
|
|
22
|
+
timeout?: number;
|
|
23
|
+
headers?: HeadersInit;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CacheEntry {
|
|
27
|
+
data: any;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
ttl?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ClientExecutor {
|
|
33
|
+
private cache: Map<string, CacheEntry> = new Map();
|
|
34
|
+
private options: Required<ClientExecutorOptions>;
|
|
35
|
+
private localStorage?: Storage;
|
|
36
|
+
private sessionStorage?: Storage;
|
|
37
|
+
|
|
38
|
+
constructor(options: ClientExecutorOptions = {}) {
|
|
39
|
+
this.options = {
|
|
40
|
+
baseUrl: options.baseUrl || '/api/aui/execute',
|
|
41
|
+
cache: {
|
|
42
|
+
enabled: options.cache?.enabled ?? true,
|
|
43
|
+
strategy: options.cache?.strategy || {
|
|
44
|
+
key: (tool, input) => `${tool}:${JSON.stringify(input)}`,
|
|
45
|
+
ttl: 5 * 60 * 1000, // 5 minutes default
|
|
46
|
+
maxSize: 100,
|
|
47
|
+
},
|
|
48
|
+
storage: options.cache?.storage || 'memory',
|
|
49
|
+
},
|
|
50
|
+
retry: {
|
|
51
|
+
attempts: options.retry?.attempts ?? 3,
|
|
52
|
+
delay: options.retry?.delay ?? 1000,
|
|
53
|
+
backoff: options.retry?.backoff || 'exponential',
|
|
54
|
+
},
|
|
55
|
+
timeout: options.timeout ?? 30000, // 30 seconds default
|
|
56
|
+
headers: options.headers || {},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (typeof window !== 'undefined') {
|
|
60
|
+
this.localStorage = window.localStorage;
|
|
61
|
+
this.sessionStorage = window.sessionStorage;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async execute<TInput = any, TOutput = any>(
|
|
66
|
+
toolName: string,
|
|
67
|
+
input: TInput,
|
|
68
|
+
context?: Partial<AUIContext>
|
|
69
|
+
): Promise<TOutput> {
|
|
70
|
+
// Check cache first if enabled
|
|
71
|
+
if (this.options.cache.enabled) {
|
|
72
|
+
const cached = this.getFromCache(toolName, input);
|
|
73
|
+
if (cached !== null) {
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Execute with retry logic
|
|
79
|
+
let lastError: Error | null = null;
|
|
80
|
+
for (let attempt = 0; attempt < this.options.retry.attempts!; attempt++) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await this.executeOnce<TInput, TOutput>(
|
|
83
|
+
toolName,
|
|
84
|
+
input,
|
|
85
|
+
context
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Store in cache if enabled
|
|
89
|
+
if (this.options.cache.enabled) {
|
|
90
|
+
this.storeInCache(toolName, input, result);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
lastError = error as Error;
|
|
96
|
+
|
|
97
|
+
// Don't retry on certain errors
|
|
98
|
+
if (error instanceof Error) {
|
|
99
|
+
if (error.message.includes('Tool') && error.message.includes('not found')) {
|
|
100
|
+
throw error; // Don't retry if tool doesn't exist
|
|
101
|
+
}
|
|
102
|
+
if (error.message.includes('Rate limit')) {
|
|
103
|
+
throw error; // Don't retry on rate limit
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Calculate delay for next attempt
|
|
108
|
+
if (attempt < this.options.retry.attempts! - 1) {
|
|
109
|
+
const delay = this.calculateRetryDelay(attempt);
|
|
110
|
+
await this.sleep(delay);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw lastError || new Error('Execution failed after retries');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async executeOnce<TInput = any, TOutput = any>(
|
|
119
|
+
toolName: string,
|
|
120
|
+
input: TInput,
|
|
121
|
+
context?: Partial<AUIContext>
|
|
122
|
+
): Promise<TOutput> {
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeoutId = setTimeout(
|
|
125
|
+
() => controller.abort(),
|
|
126
|
+
this.options.timeout
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(this.options.baseUrl, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
...this.options.headers,
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
tool: toolName,
|
|
138
|
+
input,
|
|
139
|
+
context,
|
|
140
|
+
}),
|
|
141
|
+
signal: controller.signal,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
clearTimeout(timeoutId);
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const error = await response.json();
|
|
148
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const result = await response.json();
|
|
152
|
+
return result.data || result;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
|
|
156
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
157
|
+
throw new Error(`Execution timeout after ${this.options.timeout}ms`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getFromCache(toolName: string, input: any): any | null {
|
|
165
|
+
const key = this.options.cache.strategy!.key(toolName, input);
|
|
166
|
+
|
|
167
|
+
// Check appropriate storage
|
|
168
|
+
let entry: CacheEntry | null = null;
|
|
169
|
+
|
|
170
|
+
switch (this.options.cache.storage) {
|
|
171
|
+
case 'localStorage':
|
|
172
|
+
if (this.localStorage) {
|
|
173
|
+
const stored = this.localStorage.getItem(key);
|
|
174
|
+
if (stored) {
|
|
175
|
+
try {
|
|
176
|
+
entry = JSON.parse(stored);
|
|
177
|
+
} catch {
|
|
178
|
+
// Invalid cache entry
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'sessionStorage':
|
|
185
|
+
if (this.sessionStorage) {
|
|
186
|
+
const stored = this.sessionStorage.getItem(key);
|
|
187
|
+
if (stored) {
|
|
188
|
+
try {
|
|
189
|
+
entry = JSON.parse(stored);
|
|
190
|
+
} catch {
|
|
191
|
+
// Invalid cache entry
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
default: // memory
|
|
198
|
+
entry = this.cache.get(key) || null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!entry) return null;
|
|
202
|
+
|
|
203
|
+
// Check if entry is still valid
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
const ttl = entry.ttl || this.options.cache.strategy!.ttl;
|
|
206
|
+
|
|
207
|
+
if (ttl && now - entry.timestamp > ttl) {
|
|
208
|
+
// Entry expired
|
|
209
|
+
this.removeFromCache(key);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return entry.data;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private storeInCache(toolName: string, input: any, data: any): void {
|
|
217
|
+
const key = this.options.cache.strategy!.key(toolName, input);
|
|
218
|
+
const entry: CacheEntry = {
|
|
219
|
+
data,
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
ttl: this.options.cache.strategy!.ttl,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
switch (this.options.cache.storage) {
|
|
225
|
+
case 'localStorage':
|
|
226
|
+
if (this.localStorage) {
|
|
227
|
+
try {
|
|
228
|
+
this.localStorage.setItem(key, JSON.stringify(entry));
|
|
229
|
+
this.enforceMaxSize('localStorage');
|
|
230
|
+
} catch {
|
|
231
|
+
// Storage quota exceeded
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'sessionStorage':
|
|
237
|
+
if (this.sessionStorage) {
|
|
238
|
+
try {
|
|
239
|
+
this.sessionStorage.setItem(key, JSON.stringify(entry));
|
|
240
|
+
this.enforceMaxSize('sessionStorage');
|
|
241
|
+
} catch {
|
|
242
|
+
// Storage quota exceeded
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
default: // memory
|
|
248
|
+
this.cache.set(key, entry);
|
|
249
|
+
this.enforceMaxSize('memory');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private removeFromCache(key: string): void {
|
|
254
|
+
switch (this.options.cache.storage) {
|
|
255
|
+
case 'localStorage':
|
|
256
|
+
if (this.localStorage) {
|
|
257
|
+
this.localStorage.removeItem(key);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case 'sessionStorage':
|
|
262
|
+
if (this.sessionStorage) {
|
|
263
|
+
this.sessionStorage.removeItem(key);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
default:
|
|
268
|
+
this.cache.delete(key);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private enforceMaxSize(storage: 'memory' | 'localStorage' | 'sessionStorage'): void {
|
|
273
|
+
const maxSize = this.options.cache.strategy!.maxSize;
|
|
274
|
+
if (!maxSize) return;
|
|
275
|
+
|
|
276
|
+
if (storage === 'memory') {
|
|
277
|
+
if (this.cache.size > maxSize) {
|
|
278
|
+
// Remove oldest entries
|
|
279
|
+
const entries = Array.from(this.cache.entries());
|
|
280
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
281
|
+
|
|
282
|
+
const toRemove = entries.slice(0, entries.length - maxSize);
|
|
283
|
+
toRemove.forEach(([key]) => this.cache.delete(key));
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
const storageObj = storage === 'localStorage' ? this.localStorage : this.sessionStorage;
|
|
287
|
+
if (!storageObj) return;
|
|
288
|
+
|
|
289
|
+
// Get all cache keys
|
|
290
|
+
const cacheKeys: Array<{ key: string; timestamp: number }> = [];
|
|
291
|
+
for (let i = 0; i < storageObj.length; i++) {
|
|
292
|
+
const key = storageObj.key(i);
|
|
293
|
+
if (key) {
|
|
294
|
+
try {
|
|
295
|
+
const entry = JSON.parse(storageObj.getItem(key)!);
|
|
296
|
+
if (entry && entry.timestamp) {
|
|
297
|
+
cacheKeys.push({ key, timestamp: entry.timestamp });
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
// Skip invalid entries
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (cacheKeys.length > maxSize) {
|
|
306
|
+
cacheKeys.sort((a, b) => a.timestamp - b.timestamp);
|
|
307
|
+
const toRemove = cacheKeys.slice(0, cacheKeys.length - maxSize);
|
|
308
|
+
toRemove.forEach(({ key }) => storageObj.removeItem(key));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private calculateRetryDelay(attempt: number): number {
|
|
314
|
+
const baseDelay = this.options.retry.delay!;
|
|
315
|
+
|
|
316
|
+
if (this.options.retry.backoff === 'exponential') {
|
|
317
|
+
return baseDelay * Math.pow(2, attempt);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return baseDelay * (attempt + 1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private sleep(ms: number): Promise<void> {
|
|
324
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
clearCache(): void {
|
|
328
|
+
switch (this.options.cache.storage) {
|
|
329
|
+
case 'localStorage':
|
|
330
|
+
if (this.localStorage) {
|
|
331
|
+
this.localStorage.clear();
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'sessionStorage':
|
|
336
|
+
if (this.sessionStorage) {
|
|
337
|
+
this.sessionStorage.clear();
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
|
|
341
|
+
default:
|
|
342
|
+
this.cache.clear();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
getCacheStats(): {
|
|
347
|
+
size: number;
|
|
348
|
+
storage: string;
|
|
349
|
+
enabled: boolean;
|
|
350
|
+
} {
|
|
351
|
+
let size = 0;
|
|
352
|
+
|
|
353
|
+
switch (this.options.cache.storage) {
|
|
354
|
+
case 'localStorage':
|
|
355
|
+
size = this.localStorage?.length || 0;
|
|
356
|
+
break;
|
|
357
|
+
|
|
358
|
+
case 'sessionStorage':
|
|
359
|
+
size = this.sessionStorage?.length || 0;
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
default:
|
|
363
|
+
size = this.cache.size;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
size,
|
|
368
|
+
storage: this.options.cache.storage!,
|
|
369
|
+
enabled: this.options.cache.enabled,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Default client executor instance
|
|
375
|
+
export const clientExecutor = new ClientExecutor();
|
|
376
|
+
|
|
377
|
+
// Enhanced client execution with optimistic updates
|
|
378
|
+
export async function executeClientTool<TInput = any, TOutput = any>(
|
|
379
|
+
toolName: string,
|
|
380
|
+
input: TInput,
|
|
381
|
+
options?: {
|
|
382
|
+
optimistic?: () => TOutput;
|
|
383
|
+
onSuccess?: (result: TOutput) => void;
|
|
384
|
+
onError?: (error: Error) => void;
|
|
385
|
+
context?: Partial<AUIContext>;
|
|
386
|
+
}
|
|
387
|
+
): Promise<TOutput> {
|
|
388
|
+
// Return optimistic result immediately if provided
|
|
389
|
+
if (options?.optimistic) {
|
|
390
|
+
const optimisticResult = options.optimistic();
|
|
391
|
+
|
|
392
|
+
// Execute in background
|
|
393
|
+
clientExecutor.execute<TInput, TOutput>(toolName, input, options?.context)
|
|
394
|
+
.then(result => {
|
|
395
|
+
options?.onSuccess?.(result);
|
|
396
|
+
})
|
|
397
|
+
.catch(error => {
|
|
398
|
+
options?.onError?.(error);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return optimisticResult;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Normal execution
|
|
405
|
+
try {
|
|
406
|
+
const result = await clientExecutor.execute<TInput, TOutput>(
|
|
407
|
+
toolName,
|
|
408
|
+
input,
|
|
409
|
+
options?.context
|
|
410
|
+
);
|
|
411
|
+
options?.onSuccess?.(result);
|
|
412
|
+
return result;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
options?.onError?.(error as Error);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AUITool } from '../index';
|
|
3
|
+
|
|
4
|
+
interface ToolRendererProps {
|
|
5
|
+
tool: AUITool;
|
|
6
|
+
input?: any;
|
|
7
|
+
result?: any;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
error?: Error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ToolRenderer({ tool, input, result, loading, error }: ToolRendererProps) {
|
|
13
|
+
if (!tool.renderer) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="p-4 bg-gray-100 rounded">
|
|
16
|
+
<pre className="text-sm">{JSON.stringify(result, null, 2)}</pre>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return tool.renderer({ data: result, input, loading, error });
|
|
22
|
+
}
|
package/lib/aui/core.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export interface AUIContext {
|
|
5
|
+
cache: Map<string, any>;
|
|
6
|
+
fetch: typeof fetch;
|
|
7
|
+
user?: any;
|
|
8
|
+
session?: any;
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
headers?: HeadersInit;
|
|
11
|
+
cookies?: Record<string, string>;
|
|
12
|
+
isServer?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ToolConfig<TInput, TOutput> {
|
|
16
|
+
name: string;
|
|
17
|
+
inputSchema?: z.ZodType<TInput>;
|
|
18
|
+
executeHandler?: (params: { input: TInput; ctx?: AUIContext }) => Promise<TOutput> | TOutput;
|
|
19
|
+
clientHandler?: (params: { input: TInput; ctx: AUIContext }) => Promise<TOutput> | TOutput;
|
|
20
|
+
renderHandler?: (props: { data: TOutput; input?: TInput; loading?: boolean; error?: Error }) => ReactElement;
|
|
21
|
+
middleware?: Array<(params: { input: TInput; ctx: AUIContext; next: () => Promise<TOutput> }) => Promise<TOutput>>;
|
|
22
|
+
description?: string;
|
|
23
|
+
tags?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AUITool<TInput = any, TOutput = any> {
|
|
27
|
+
protected config: ToolConfig<TInput, TOutput> = { name: '' };
|
|
28
|
+
|
|
29
|
+
constructor(name: string) {
|
|
30
|
+
this.config.name = name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
input<T>(schema: z.ZodType<T>): AUITool<T, TOutput> {
|
|
34
|
+
this.config.inputSchema = schema as any;
|
|
35
|
+
return this as any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
execute<O>(handler: (params: { input: TInput; ctx?: AUIContext }) => O | Promise<O>): AUITool<TInput, O> {
|
|
39
|
+
this.config.executeHandler = handler as any;
|
|
40
|
+
return this as any;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clientExecute(handler: (params: { input: TInput; ctx: AUIContext }) => TOutput | Promise<TOutput>): this {
|
|
44
|
+
this.config.clientHandler = handler;
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
render(component: (props: { data: TOutput; input?: TInput; loading?: boolean; error?: Error }) => ReactElement): this {
|
|
49
|
+
this.config.renderHandler = component;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async run(input: TInput, ctx?: AUIContext): Promise<TOutput> {
|
|
54
|
+
const validated = this.config.inputSchema ? this.config.inputSchema.parse(input) : input;
|
|
55
|
+
const context = ctx || {
|
|
56
|
+
cache: new Map(),
|
|
57
|
+
fetch: globalThis.fetch?.bind(globalThis) || (() => Promise.reject(new Error('Fetch not available'))),
|
|
58
|
+
isServer: typeof window === 'undefined',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Apply middleware if present
|
|
62
|
+
if (this.config.middleware?.length) {
|
|
63
|
+
let index = 0;
|
|
64
|
+
const next = async (): Promise<TOutput> => {
|
|
65
|
+
if (index >= this.config.middleware!.length) {
|
|
66
|
+
return this.executeCore(validated, context);
|
|
67
|
+
}
|
|
68
|
+
const middleware = this.config.middleware![index++];
|
|
69
|
+
return middleware({ input: validated, ctx: context, next });
|
|
70
|
+
};
|
|
71
|
+
return next();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this.executeCore(validated, context);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async executeCore(input: TInput, ctx: AUIContext): Promise<TOutput> {
|
|
78
|
+
// Use clientHandler if it exists and we're on the client
|
|
79
|
+
if (!ctx.isServer && this.config.clientHandler) {
|
|
80
|
+
return this.config.clientHandler({ input, ctx });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!this.config.executeHandler) {
|
|
84
|
+
throw new Error(`Tool ${this.config.name} has no execute handler`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return this.config.executeHandler({ input, ctx });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
middleware(fn: (params: { input: TInput; ctx: AUIContext; next: () => Promise<TOutput> }) => Promise<TOutput>): this {
|
|
91
|
+
if (!this.config.middleware) {
|
|
92
|
+
this.config.middleware = [];
|
|
93
|
+
}
|
|
94
|
+
this.config.middleware.push(fn);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe(description: string): this {
|
|
99
|
+
this.config.description = description;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
tag(...tags: string[]): this {
|
|
104
|
+
if (!this.config.tags) {
|
|
105
|
+
this.config.tags = [];
|
|
106
|
+
}
|
|
107
|
+
this.config.tags.push(...tags);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get name() { return this.config.name; }
|
|
112
|
+
get schema() { return this.config.inputSchema; }
|
|
113
|
+
get inputSchema() { return this.config.inputSchema; }
|
|
114
|
+
get outputSchema() { return undefined; } // Can be extended if needed
|
|
115
|
+
get isServerOnly() { return !this.config.clientHandler; }
|
|
116
|
+
get metadata() { return { description: this.config.description, tags: this.config.tags }; }
|
|
117
|
+
get renderer() { return this.config.renderHandler; }
|
|
118
|
+
get description() { return this.config.description; }
|
|
119
|
+
get tags() { return this.config.tags || []; }
|
|
120
|
+
|
|
121
|
+
toJSON() {
|
|
122
|
+
return {
|
|
123
|
+
name: this.config.name,
|
|
124
|
+
description: this.config.description,
|
|
125
|
+
tags: this.config.tags || [],
|
|
126
|
+
hasInput: !!this.config.inputSchema,
|
|
127
|
+
hasExecute: !!this.config.executeHandler,
|
|
128
|
+
hasClientExecute: !!this.config.clientHandler,
|
|
129
|
+
hasRender: !!this.config.renderHandler,
|
|
130
|
+
hasMiddleware: !!(this.config.middleware && this.config.middleware.length > 0)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getConfig(): Readonly<ToolConfig<TInput, TOutput>> {
|
|
135
|
+
return { ...this.config };
|
|
136
|
+
}
|
|
137
|
+
}
|
package/lib/aui/demo.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import aui from './index';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// EXACT API AS REQUESTED BY USER
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
// Simple tool - just 2 methods
|
|
9
|
+
const simpleTool = aui
|
|
10
|
+
.tool('weather')
|
|
11
|
+
.input(z.object({ city: z.string() }))
|
|
12
|
+
.execute(async ({ input }) => ({ temp: 72, city: input.city }))
|
|
13
|
+
.render(({ data }) => <div>{data.city}: {data.temp}°</div>);
|
|
14
|
+
|
|
15
|
+
// Complex tool - adds client optimization
|
|
16
|
+
const complexTool = aui
|
|
17
|
+
.tool('search')
|
|
18
|
+
.input(z.object({ query: z.string() }))
|
|
19
|
+
.execute(async ({ input }) => {
|
|
20
|
+
// Mock database search
|
|
21
|
+
return { results: [`Result for ${input.query}`] };
|
|
22
|
+
})
|
|
23
|
+
.clientExecute(async ({ input, ctx }) => {
|
|
24
|
+
// Only when you need caching, offline, etc.
|
|
25
|
+
const cached = ctx.cache.get(input.query);
|
|
26
|
+
return cached || ctx.fetch('/api/tools/search', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify(input)
|
|
30
|
+
}).then(r => r.json());
|
|
31
|
+
})
|
|
32
|
+
.render(({ data }) => <SearchResults results={data} />);
|
|
33
|
+
|
|
34
|
+
// Component for rendering search results
|
|
35
|
+
function SearchResults({ results }: { results: any }) {
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
{results.results?.map((r: string, i: number) => (
|
|
39
|
+
<div key={i}>{r}</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// USAGE EXAMPLES
|
|
47
|
+
// ============================================
|
|
48
|
+
|
|
49
|
+
// Direct execution
|
|
50
|
+
async function exampleUsage() {
|
|
51
|
+
const ctx = aui.createContext();
|
|
52
|
+
|
|
53
|
+
// Execute weather tool
|
|
54
|
+
const weatherResult = await simpleTool.run({ city: 'Tokyo' }, ctx);
|
|
55
|
+
console.log(weatherResult); // { temp: 72, city: 'Tokyo' }
|
|
56
|
+
|
|
57
|
+
// Execute search tool
|
|
58
|
+
const searchResult = await complexTool.run({ query: 'AI tools' }, ctx);
|
|
59
|
+
console.log(searchResult); // { results: ['Result for AI tools'] }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// React component usage
|
|
63
|
+
import { useAUITool } from './hooks/useAUITool';
|
|
64
|
+
|
|
65
|
+
export function WeatherWidget() {
|
|
66
|
+
const { execute, data, loading } = useAUITool(simpleTool);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
<button onClick={() => execute({ city: 'Tokyo' })}>
|
|
71
|
+
Get Weather
|
|
72
|
+
</button>
|
|
73
|
+
{loading && <span>Loading...</span>}
|
|
74
|
+
{data && simpleTool.renderer && simpleTool.renderer({ data })}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// AI Agent can discover and use tools
|
|
80
|
+
export function getToolsForAI() {
|
|
81
|
+
return aui.getTools().map((tool: any) => ({
|
|
82
|
+
name: tool.name,
|
|
83
|
+
description: tool.description,
|
|
84
|
+
schema: tool.schema,
|
|
85
|
+
execute: (input: any) => tool.run(input)
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { simpleTool, complexTool };
|