@aeye/ai 0.3.7 → 0.3.9

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 CHANGED
@@ -2,114 +2,48 @@
2
2
 
3
3
  > **Multi-provider AI library with intelligent model selection, type-safe context management, and comprehensive hooks system.**
4
4
 
5
- The `@aeye/ai` package is the main AI library built on `@aeye/core`, providing a unified interface for working with multiple AI providers (OpenAI, Anthropic, Google, etc.) with automatic model selection, cost tracking, and extensible architecture.
5
+ The `@aeye/ai` package is the main AI library built on `@aeye/core`, providing a unified interface for working with multiple AI providers with automatic model selection, cost tracking, and extensible architecture.
6
6
 
7
7
  ```ts
8
+ import { AI } from '@aeye/ai';
9
+ import { OpenAIProvider } from '@aeye/openai';
10
+ import z from 'zod';
11
+
12
+ const openai = new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! });
13
+
8
14
  const ai = AI.with<MyContext>()
9
15
  .providers({ openai })
10
- .create(/* options, hooks, etc */)
11
-
12
- ai.chat.get(request, ctx?)
13
- ai.chat.stream(request, ctx?)
14
- ai.image.generate.get(request, ctx?)
15
- ai.image.generate.stream(request, ctx?)
16
- ai.image.edit.get(request, ctx?)
17
- ai.image.edit.stream(request, ctx?)
18
- ai.image.analyze.get(request, ctx?)
19
- ai.image.analyze.stream(request, ctx?)
20
- ai.transcribe.get(request, ctx?)
21
- ai.transcribe.stream(request, ctx?)
22
- ai.speech.get(request, ctx?)
23
- ai.speech.stream(request, ctx?)
16
+ .create({ /* defaultContext, providedContext, defaultWeights, hooks, ... */ });
17
+
18
+ // Low-level APIs
19
+ ai.chat.get(request, ctx?) // or .stream(request, ctx?)
20
+ ai.image.generate.get(request, ctx?) // or .stream(request, ctx?)
21
+ ai.image.edit.get(request, ctx?) // or .stream(request, ctx?)
22
+ ai.image.analyze.get(request, ctx?) // or .stream(request, ctx?)
23
+ ai.transcribe.get(request, ctx?) // or .stream(request, ctx?)
24
+ ai.speech.get(request, ctx?) // or .stream(request, ctx?)
24
25
  ai.embed.get(request, ctx?)
25
- ai.embed.stream(request, ctx?)
26
- ai.models.list() // get(id), search(criteria), select(criteria), refresh()
27
- ai.providers.openai // Provider
28
- ai.hooks // Hook events to implement BYOK
29
- ai.components // all prompts/tools/agents created on this ai
30
-
31
- // Define prompts, tools, agents
32
- const chatPrompt = ai.prompt({
33
- name: 'chat_prompt',
34
- description: 'A helpful chat assistant that answers user questions',
35
- content: `You are a helpful assistant. The user {{user}} has asked: {{question}}
36
- Answer their question clearly and concisely. If you need more information, use the available tools.`,
37
- input: (input: { user: string; question: string }) => input,
38
- schema: z.object({ answer: z.string() }),
39
- refs: [relevantInfo],
40
- });
26
+ ai.models.list() // .get(id), .search(criteria), .select(criteria), .refresh()
41
27
 
42
- const analyzeConversation = ai.prompt({
43
- name: 'analyze_conversation',
44
- description: 'Analyzes a conversation to judge how accurately the agent performed the requests',
45
- content: `You are a conversational analyst. Analyze the following conversation between user {{user}} and the assistant.
46
- Messages: {{messages}}
47
- Return a summary of how well the agent performed and a rating between 0 and 100.`,
48
- input: (input: { user: string; messages: string }) => input,
49
- schema: z.object({ summary: z.string(), rating: z.number() }),
50
- });
51
-
52
- // Tool
53
- const relevantInfo = ai.tool({
54
- name: 'relevant_info',
55
- description: `Searches for info relevant to a user's question`,
56
- instructions: 'Use the relevant_info tool to look up information to better answer questions',
57
- schema: z.object({
58
- query: z.string(),
59
- }),
60
- call: async ({ query }, refs, ctx) => {
61
- return await DB.getRelevantInfo(query, 10);
62
- }
63
- });
28
+ // Components bound to this AI instance
29
+ const myTool = ai.tool({ name, description, schema, call, ... });
30
+ const myPrompt = ai.prompt({ name, description, content, tools, schema, ... });
31
+ const myAgent = ai.agent({ name, description, refs, call });
64
32
 
65
- // Agent
66
- const chatAgent = ai.agent({
67
- name: 'chat_agent',
68
- description: 'An agent to chat with the user and analyze the conversation',
69
- refs: [relevantInfo, chatPrompt, analyzeConversation],
70
- call: async (input: { user: string; question: string }, refs, ctx) => {
71
- // Get the chat response
72
- const chatResponse = await refs[1].run({
73
- user: input.user,
74
- question: input.question
75
- }, ctx);
76
-
77
- // Analyze the conversation
78
- const messages = JSON.stringify([
79
- { role: 'user', content: input.question },
80
- { role: 'assistant', content: chatResponse.answer }
81
- ]);
82
-
83
- const analysis = await refs[2].run({
84
- user: input.user,
85
- messages
86
- }, ctx);
87
-
88
- return {
89
- answer: chatResponse.answer,
90
- analysis: {
91
- summary: analysis.summary,
92
- rating: analysis.rating
93
- }
94
- };
95
- },
96
- });
97
-
98
- // Use the agent
99
- const result = await chatAgent.run(
100
- { user: 'ClickerMonkey', question: 'What is TypeScript?' },
101
- { messages: [] }
102
- );
103
- console.log(result.answer);
104
- console.log(`Quality rating: ${result.analysis.rating}/100`);
33
+ myTool.run(input, ctx?)
34
+ myPrompt.run(input, ctx?) // streaming generator
35
+ myPrompt.get('result', input, ctx?) // awaitable result
36
+ myPrompt.get('stream', input, ctx?) // stream all events
37
+ myPrompt.get('streamContent', input, ctx?) // stream text only
38
+ myAgent.run(input, ctx?)
105
39
  ```
106
40
 
107
41
  ## Features
108
42
 
109
- - **Multi-Provider Support**: Single interface for OpenAI, Anthropic, Google, Replicate, and custom providers
43
+ - **Multi-Provider Support**: Single interface for OpenAI, OpenRouter, Replicate, AWS Bedrock, and custom providers
110
44
  - **Intelligent Model Selection**: Automatic model selection based on capabilities, cost, speed, and quality
111
45
  - **Type-Safe Context**: Strongly-typed context and metadata with compiler validation
112
- - **Comprehensive APIs**: Chat, Image Generation/Analysis, Speech Synthesis/Transcription, Embeddings
46
+ - **Comprehensive APIs**: Chat, Image Generation/Analysis/Editing, Speech Synthesis, Transcription, Embeddings
113
47
  - **Lifecycle Hooks**: Intercept and modify operations at every stage
114
48
  - **Cost Tracking**: Automatic token usage and cost calculation
115
49
  - **Streaming Support**: Full streaming support across all compatible capabilities
@@ -129,13 +63,14 @@ console.log(`Quality rating: ${result.analysis.rating}/100`);
129
63
  - [Transcribe API](#transcribe-api)
130
64
  - [Embed API](#embed-api)
131
65
  - [Models API](#models-api)
66
+ - [Tools, Prompts & Agents](#tools-prompts--agents)
132
67
  - [Advanced Features](#advanced-features)
133
68
  - [Custom Context & Metadata](#custom-context--metadata)
134
69
  - [Lifecycle Hooks](#lifecycle-hooks)
135
70
  - [Model Selection](#model-selection)
136
71
  - [Model Sources](#model-sources)
137
72
  - [Custom Providers](#custom-providers)
138
- - [TypeScript Guide](#typescript-guide)
73
+ - [Extending AI Instances](#extending-ai-instances)
139
74
  - [Examples](#examples)
140
75
 
141
76
  ## Installation
@@ -147,7 +82,10 @@ npm install @aeye/ai @aeye/core
147
82
  You'll also need provider packages:
148
83
 
149
84
  ```bash
150
- npm install @aeye/openai @aeye/anthropic # etc.
85
+ npm install @aeye/openai openai # OpenAI
86
+ npm install @aeye/openrouter # OpenRouter
87
+ npm install @aeye/replicate replicate # Replicate
88
+ npm install @aeye/aws # AWS Bedrock
151
89
  ```
152
90
 
153
91
  ## Quick Start
@@ -157,29 +95,23 @@ import { AI } from '@aeye/ai';
157
95
  import { OpenAIProvider } from '@aeye/openai';
158
96
  import { OpenRouterProvider } from '@aeye/openrouter';
159
97
 
160
- // Create providers
161
- const openai = new OpenAIProvider({ apiKey: '123' });
162
- const openrouter = new OpenRouterProvider({ apiKey: 'abc' });
98
+ const openai = new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! });
99
+ const openrouter = new OpenRouterProvider({ apiKey: process.env.OPENROUTER_API_KEY! });
163
100
 
164
- // Create an AI instance
165
101
  const ai = AI.with()
166
102
  .providers({ openai, openrouter })
167
- .create({
168
- defaultContext: {
169
- apiVersion: 'v1'
170
- }
171
- });
103
+ .create();
172
104
 
173
105
  // Simple chat completion
174
- const response = await ai.chat.get([
175
- { role: 'user', content: 'What is TypeScript?' }
176
- ]);
106
+ const response = await ai.chat.get({
107
+ messages: [{ role: 'user', content: 'What is TypeScript?' }]
108
+ });
177
109
  console.log(response.content);
178
110
 
179
111
  // Streaming
180
- for await (const chunk of ai.chat.stream([
181
- { role: 'user', content: 'Write a poem' }
182
- ])) {
112
+ for await (const chunk of ai.chat.stream({
113
+ messages: [{ role: 'user', content: 'Write a poem' }]
114
+ })) {
183
115
  if (chunk.content) {
184
116
  process.stdout.write(chunk.content);
185
117
  }
@@ -188,127 +120,76 @@ for await (const chunk of ai.chat.stream([
188
120
 
189
121
  ## Architecture
190
122
 
191
- The `@aeye/ai` library is structured around several key components:
123
+ ```mermaid
124
+ graph TD
125
+ AI["<b>AI Class</b><br/>Context Management<br/>Model Registry<br/>Lifecycle Hooks"]
126
+ APIs["<b>APIs</b><br/>Chat · Image<br/>Speech · Embed"]
127
+ Registry["<b>Registry</b><br/>Models · Search · Select"]
128
+ Providers["<b>Providers</b><br/>OpenAI · OpenRouter<br/>Replicate · AWS · Custom"]
192
129
 
193
- ```
194
- ┌─────────────────────────────────────────────────────────┐
195
- │ AI Class │
196
- │ - Context Management │
197
- │ - Model Registry │
198
- │ - Lifecycle Hooks │
199
- └─────────────────┬───────────────────────────────────────┘
200
- │
201
- ┌────────┴─────────┐
202
- │ │
203
- ┌────▼────┐ ┌─────▼──────┐
204
- │ APIs │ │ Registry │
205
- │ │ │ │
206
- │ • Chat │ │ • Models │
207
- │ • Image │ │ • Search │
208
- │ • Speech│ │ • Select │
209
- │ • Embed │ └────┬───────┘
210
- └────┬────┘ │
211
- │ ┌──────▼──────┐
212
- │ │ Providers │
213
- │ │ │
214
- │ │ • OpenAI │
215
- │ │ • Anthropic │
216
- │ │ • Google │
217
- │ │ • Custom │
218
- └─────────┴─────────────┘
130
+ AI --> APIs
131
+ AI --> Registry
132
+ Registry --> Providers
133
+ APIs --> Providers
219
134
  ```
220
135
 
221
- ### Key Components
222
-
223
- 1. **AI Class**: Central orchestrator managing context, metadata, providers, and APIs
224
- 2. **APIs**: Specialized interfaces for different capabilities (chat, images, speech, etc.)
225
- 3. **Model Registry**: Centralized model management with selection logic
226
- 4. **Providers**: Pluggable implementations for different AI services
227
- 5. **Context System**: Type-safe context and metadata threading
228
-
229
136
  ## Core Concepts
230
137
 
231
138
  ### Context
232
139
 
233
- Context is data passed through your entire AI operation. It's composed of:
140
+ Context is data passed through your entire AI operation:
234
141
 
235
- - **Default Context**: Static values provided at AI creation
236
- - **Provided Context**: Async-loaded values (e.g., from database)
237
- - **Required Context**: Values that must be provided per-request
142
+ - **Default Context**: Static values provided at AI creation via `defaultContext`
143
+ - **Provided Context**: Async-loaded values (e.g., from a database) via `providedContext`
144
+ - **Required Context**: Values that must be supplied per-call
238
145
 
239
146
  ```typescript
240
147
  interface AppContext {
241
- user: User;
242
- db: Database;
243
- apiVersion: string;
148
+ userId: string;
149
+ user?: User; // loaded by providedContext
150
+ db?: Database; // loaded by providedContext
244
151
  }
245
152
 
246
153
  const ai = AI.with<AppContext>()
247
154
  .providers({ openai })
248
155
  .create({
249
- defaultContext: {
250
- apiVersion: 'v1'
251
- },
252
156
  providedContext: async (ctx) => ({
253
- user: await getUser(ctx.userId),
254
- db: database
255
- })
157
+ user: await db.users.findById(ctx.userId),
158
+ db,
159
+ }),
256
160
  });
257
161
 
258
- // Usage: only provide what's not already available
259
- await ai.chat.get(request, { userId: '123' });
162
+ // At call time only userId is required — the rest is loaded automatically
163
+ const response = await ai.chat.get(
164
+ { messages: [{ role: 'user', content: 'Hi!' }] },
165
+ { userId: 'user123' }
166
+ );
260
167
  ```
261
168
 
262
169
  ### Metadata
263
170
 
264
- Metadata controls model selection and operation configuration:
171
+ Metadata controls model selection and per-request configuration:
265
172
 
266
173
  ```typescript
267
174
  interface AppMetadata {
268
175
  priority: 'cost' | 'speed' | 'quality';
269
176
  }
270
177
 
271
- const ai = AI.with<AppContext, AppMetadata>()
178
+ const ai = AI.with<{}, AppMetadata>()
272
179
  .providers({ openai, openrouter })
273
180
  .create({
274
- defaultMetadata: {
275
- priority: 'balanced'
276
- }
181
+ defaultMetadata: { priority: 'cost' },
277
182
  });
278
183
 
279
- // Override per request
280
- await ai.chat.get(request, {
281
- metadata: {
282
- model: 'gpt-4', // Specific model
283
- required: ['chat', 'vision'], // Required capabilities
284
- weights: { cost: 0.7, speed: 0.3 } // Selection weights
285
- }
286
- });
287
- ```
288
-
289
- ### Model Selection
290
-
291
- Models are automatically selected based on:
292
-
293
- 1. **Capabilities**: Required and optional features
294
- 2. **Constraints**: Provider filters, context window, budget
295
- 3. **Scoring**: Weighted evaluation of cost, speed, accuracy, context size
296
-
297
- ```typescript
298
- // Automatic selection for vision task
184
+ // Override per request via context
299
185
  const response = await ai.chat.get(
300
- [{
301
- role: 'user',
302
- content: [
303
- { type: 'text', content: 'What is in this image?' },
304
- { type: 'image', content: imageUrl }
305
- ]
306
- }],
186
+ { messages: [{ role: 'user', content: 'Hello' }] },
307
187
  {
308
188
  metadata: {
309
- required: ['chat', 'vision'],
310
- weights: { cost: 0.5, speed: 0.5 }
311
- }
189
+ model: 'openai/gpt-4o', // pin a specific model
190
+ weights: { cost: 0.2, speed: 0.3, accuracy: 0.5 },
191
+ priority: 'quality',
192
+ },
312
193
  }
313
194
  );
314
195
  ```
@@ -317,33 +198,31 @@ const response = await ai.chat.get(
317
198
 
318
199
  ### Chat API
319
200
 
320
- The Chat API provides conversational AI with automatic model selection.
321
-
322
- #### Methods
323
-
324
- **`chat.get(messages, context?)`**
325
-
326
- Execute a non-streaming chat completion.
201
+ **`chat.get(request, ctx?)`** — Non-streaming chat completion.
327
202
 
328
203
  ```typescript
329
- const response = await ai.chat.get([
330
- { role: 'system', content: 'You are a helpful assistant.' },
331
- { role: 'user', content: 'Hello!' }
332
- ]);
204
+ const response = await ai.chat.get({
205
+ messages: [
206
+ { role: 'system', content: 'You are a helpful assistant.' },
207
+ { role: 'user', content: 'Hello!' },
208
+ ],
209
+ temperature: 0.7,
210
+ maxTokens: 1024,
211
+ });
333
212
 
334
213
  console.log(response.content);
335
- console.log(response.usage); // Token usage
336
- console.log(response.finishReason);
214
+ console.log(response.usage?.text?.input, 'input tokens');
215
+ console.log(response.usage?.text?.output, 'output tokens');
216
+ console.log(response.usage?.cost, 'cost in dollars');
217
+ console.log(response.finishReason); // 'stop' | 'length' | 'tool_calls' | ...
337
218
  ```
338
219
 
339
- **`chat.stream(messages, context?)`**
340
-
341
- Execute a streaming chat completion.
220
+ **`chat.stream(request, ctx?)`** — Streaming chat completion.
342
221
 
343
222
  ```typescript
344
- for await (const chunk of ai.chat.stream([
345
- { role: 'user', content: 'Count to 10' }
346
- ])) {
223
+ for await (const chunk of ai.chat.stream({
224
+ messages: [{ role: 'user', content: 'Count to 10' }]
225
+ })) {
347
226
  if (chunk.content) {
348
227
  process.stdout.write(chunk.content);
349
228
  }
@@ -353,849 +232,567 @@ for await (const chunk of ai.chat.stream([
353
232
  }
354
233
  ```
355
234
 
356
- #### Request Format
235
+ **Vision:**
357
236
 
358
237
  ```typescript
359
- interface Request {
360
- messages: Message[];
361
- maxTokens?: number;
362
- temperature?: number;
363
- topP?: number;
364
- tools?: Tool[];
365
- responseFormat?: { type: 'json_object' | 'text' };
366
- // ... other options
367
- }
368
-
369
- type Message =
370
- | { role: 'system' | 'user' | 'assistant', content: string }
371
- | { role: 'user', content: ContentPart[] }; // Multimodal
372
-
373
- type ContentPart =
374
- | { type: 'text', content: string }
375
- | { type: 'image', content: string }; // URL or base64
376
- ```
377
-
378
- #### Response Format
379
-
380
- TODO: updated
381
-
382
- #### Examples
383
-
384
- **Basic Conversation**
385
-
386
- ```typescript
387
- const messages = [
388
- { role: 'system', content: 'You are a TypeScript expert.' },
389
- { role: 'user', content: 'Explain generics' }
390
- ];
391
-
392
- const response = await ai.chat.get(messages);
393
- console.log(response.content);
394
- ```
395
-
396
- **Vision Analysis**
397
-
398
- ```typescript
399
- const response = await ai.chat.get([
400
- {
238
+ const response = await ai.chat.get({
239
+ messages: [{
401
240
  role: 'user',
402
241
  content: [
403
242
  { type: 'text', content: 'What is in this image?' },
404
- { type: 'image', content: 'https://example.com/image.jpg' }
405
- ]
406
- }
407
- ], {
408
- metadata: {
409
- required: ['chat', 'vision']
410
- }
243
+ { type: 'image', content: 'https://example.com/image.jpg' },
244
+ ],
245
+ }],
411
246
  });
412
- ```
413
-
414
- **Function Calling**
415
-
416
- ```typescript
417
- const tools = [
418
- {
419
- name: 'get_weather',
420
- description: 'Get weather for a location',
421
- parameters: {
422
- type: 'object',
423
- properties: {
424
- location: { type: 'string' }
425
- }
426
- }
427
- }
428
- ];
429
-
430
- const response = await ai.chat.get(
431
- [{ role: 'user', content: 'What is the weather in NYC?' }],
432
- {
433
- metadata: {
434
- required: ['chat', 'tools']
435
- }
436
- }
437
- );
438
-
439
- if (response.toolCalls) {
440
- for (const call of response.toolCalls) {
441
- console.log(`Tool: ${call.function.name}`);
442
- console.log(`Args: ${call.function.arguments}`);
443
- }
444
- }
445
- ```
446
-
447
- **Streaming with Token Counting**
448
-
449
- TODO: update with new types
450
-
451
- ```typescript
452
- let totalTokens = 0;
453
-
454
- for await (const chunk of ai.chat.stream(messages)) {
455
- if (chunk.content) {
456
- process.stdout.write(chunk.content);
457
- }
458
- if (chunk.usage) {
459
- totalTokens = chunk.usage.totalTokens;
460
- }
461
- }
462
-
463
- console.log(`\nTotal tokens: ${totalTokens}`);
247
+ // vision capability is detected automatically from the message content
464
248
  ```
465
249
 
466
250
  ### Image API
467
251
 
468
- The Image API provides image generation, editing, and analysis.
469
-
470
- #### Sub-APIs
471
-
472
- **`image.generate.get(request, context?)`**
473
-
474
- Generate images from text prompts.
252
+ **`image.generate.get(request, ctx?)`** — Generate images from a text prompt.
475
253
 
476
254
  ```typescript
477
255
  const response = await ai.image.generate.get({
478
256
  prompt: 'A futuristic city at sunset',
479
257
  n: 2,
480
258
  size: '1024x1024',
481
- quality: 'hd'
259
+ quality: 'high',
482
260
  });
483
261
 
484
262
  for (const image of response.images) {
485
- console.log(image.url);
263
+ console.log(image.url ?? image.b64_json);
486
264
  }
487
265
  ```
488
266
 
489
- **`image.generate.stream(request, context?)`**
490
-
491
- Stream image generation progress.
267
+ **`image.edit.get(request, ctx?)`** — Edit an image with inpainting.
492
268
 
493
269
  ```typescript
494
- for await (const chunk of ai.image.generate.stream({
495
- prompt: 'A majestic mountain landscape'
496
- })) {
497
- if (chunk.progress) {
498
- console.log(`Progress: ${chunk.progress}%`);
499
- }
500
- if (chunk.done && chunk.image) {
501
- console.log('Image URL:', chunk.image.url);
502
- }
503
- }
504
- ```
505
-
506
- **`image.edit.get(request, context?)`**
270
+ import fs from 'fs';
507
271
 
508
- Edit existing images with prompts.
509
-
510
- ```typescript
511
272
  const response = await ai.image.edit.get({
512
- image: imageBuffer,
513
- mask: maskBuffer,
514
273
  prompt: 'Add a sunset in the background',
515
- size: '1024x1024'
274
+ image: fs.readFileSync('photo.png'),
275
+ mask: fs.readFileSync('mask.png'),
276
+ size: '1024x1024',
516
277
  });
517
278
  ```
518
279
 
519
- **`image.analyze.get(request, context?)`**
520
-
521
- Analyze images with vision models (uses chat models with vision capability).
280
+ **`image.analyze.get(request, ctx?)`** — Analyze image content (uses a vision chat model).
522
281
 
523
282
  ```typescript
524
283
  const response = await ai.image.analyze.get({
525
284
  prompt: 'Describe this image in detail',
526
285
  images: ['https://example.com/photo.jpg'],
527
- maxTokens: 500
528
286
  });
529
-
530
287
  console.log(response.content);
531
288
  ```
532
289
 
533
- #### Examples
534
-
535
- **Batch Generation**
536
-
537
- ```typescript
538
- const response = await ai.image.generate.get({
539
- prompt: 'Logo designs for a tech startup',
540
- n: 4,
541
- size: '512x512'
542
- });
543
-
544
- response.images.forEach((img, i) => {
545
- console.log(`Design ${i + 1}: ${img.url}`);
546
- });
547
- ```
548
-
549
- **Style Transfer**
550
-
551
- ```typescript
552
- const response = await ai.image.generate.get({
553
- prompt: 'Van Gogh style starry night over modern city',
554
- quality: 'hd',
555
- style: 'vivid'
556
- });
557
- ```
558
-
559
290
  ### Speech API
560
291
 
561
- Text-to-speech synthesis.
562
-
563
- **`speech.get(request, context?)`**
292
+ **`speech.get(request, ctx?)`** — Text-to-speech synthesis.
564
293
 
565
294
  ```typescript
295
+ import fs from 'fs';
296
+ import { Readable } from 'stream';
297
+
566
298
  const response = await ai.speech.get({
567
- text: 'Hello, this is a test of text to speech.',
299
+ text: 'Hello, this is a text-to-speech example.',
568
300
  voice: 'alloy',
569
301
  speed: 1.0,
570
- responseFormat: 'mp3'
571
302
  });
572
303
 
573
- // Save audio
574
- fs.writeFileSync('output.mp3', response.audioBuffer);
575
- ```
576
-
577
- **`speech.stream(request, context?)`**
578
-
579
- ```typescript
580
- for await (const chunk of ai.speech.stream({
581
- text: 'Streaming audio generation...',
582
- voice: 'nova'
583
- })) {
584
- if (chunk.audioData) {
585
- // Process audio chunks in real-time
586
- audioStream.write(chunk.audioData);
587
- }
588
- }
304
+ // Pipe the audio stream to a file
305
+ const fileStream = fs.createWriteStream('output.mp3');
306
+ Readable.fromWeb(response.audio).pipe(fileStream);
589
307
  ```
590
308
 
591
309
  ### Transcribe API
592
310
 
593
- Speech-to-text transcription.
594
-
595
- **`transcribe.get(request, context?)`**
311
+ **`transcribe.get(request, ctx?)`** — Speech-to-text transcription.
596
312
 
597
313
  ```typescript
598
- const audioBuffer = fs.readFileSync('recording.mp3');
314
+ import fs from 'fs';
599
315
 
600
316
  const response = await ai.transcribe.get({
601
- audio: audioBuffer,
317
+ audio: fs.readFileSync('recording.mp3'),
602
318
  language: 'en',
603
- responseFormat: 'verbose_json',
604
- timestampGranularities: ['word', 'segment']
605
319
  });
606
320
 
607
321
  console.log(response.text);
608
- console.log('Words:', response.words);
609
- console.log('Segments:', response.segments);
610
322
  ```
611
323
 
612
324
  ### Embed API
613
325
 
614
- Generate text embeddings for semantic search and similarity.
615
-
616
- **`embed.get(request, context?)`**
326
+ **`embed.get(request, ctx?)`** — Generate text embeddings.
617
327
 
618
328
  ```typescript
619
329
  const response = await ai.embed.get({
620
330
  texts: [
621
331
  'TypeScript is a typed superset of JavaScript',
622
332
  'Python is a high-level programming language',
623
- 'Rust is a systems programming language'
624
333
  ],
625
- dimensions: 1536
626
334
  });
627
335
 
628
- // Get embeddings
629
336
  response.embeddings.forEach(({ embedding, index }) => {
630
337
  console.log(`Text ${index}: ${embedding.length} dimensions`);
631
338
  });
632
-
633
- // Calculate similarity
634
- function cosineSimilarity(a: number[], b: number[]) {
635
- const dot = a.reduce((sum, val, i) => sum + val * b[i], 0);
636
- const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
637
- const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
638
- return dot / (magA * magB);
639
- }
640
-
641
- const sim = cosineSimilarity(
642
- response.embeddings[0].embedding,
643
- response.embeddings[1].embedding
644
- );
645
- console.log('Similarity:', sim);
646
339
  ```
647
340
 
648
341
  ### Models API
649
342
 
650
- Explore and manage available models.
651
-
652
- **`models.list()`**
653
-
654
343
  ```typescript
344
+ // List all registered models
655
345
  const models = ai.models.list();
656
- console.log(`Available models: ${models.length}`);
657
346
 
658
- models.forEach(model => {
659
- console.log(`${model.provider}/${model.id}`);
660
- console.log(` Tier: ${model.tier}`);
661
- console.log(` Capabilities: ${Array.from(model.capabilities).join(', ')}`);
662
- console.log(` Cost: $${model.pricing.text.input}/1M input tokens`);
347
+ // Get a specific model
348
+ const model = ai.models.get('gpt-4o');
349
+ if (model) {
350
+ console.log('Context window:', model.contextWindow);
351
+ console.log('Capabilities:', Array.from(model.capabilities).join(', '));
352
+ console.log('Input cost:', model.pricing.text?.input, 'per 1M tokens');
353
+ }
354
+
355
+ // Search with selection criteria
356
+ const results = ai.models.search({
357
+ capabilities: new Set(['chat', 'structured']),
358
+ weights: { cost: 0.6, speed: 0.4 },
359
+ providers: { allow: ['openai', 'openrouter'] },
360
+ contextWindow: { min: 100000 },
663
361
  });
362
+ console.log('Best match:', results[0]?.model.id);
363
+
364
+ // Refresh model list from all sources
365
+ await ai.models.refresh();
664
366
  ```
665
367
 
666
- **`models.get(id)`**
368
+ ## Tools, Prompts & Agents
369
+
370
+ Use `ai.tool()`, `ai.prompt()`, and `ai.agent()` to create components that are automatically wired to the AI instance (executor, streamer, and context are injected automatically).
371
+
372
+ ### Tool
667
373
 
668
374
  ```typescript
669
- const model = ai.models.get('gpt-4');
670
- if (model) {
671
- console.log('Context window:', model.contextWindow);
672
- console.log('Max output:', model.maxOutputTokens);
673
- }
375
+ import z from 'zod';
376
+
377
+ const searchKnowledge = ai.tool({
378
+ name: 'searchKnowledge',
379
+ description: 'Search the knowledge base for relevant information',
380
+ instructions: 'Use this to find information relevant to the query: "{{query}}"',
381
+ schema: z.object({
382
+ query: z.string().describe('Search query'),
383
+ limit: z.number().int().min(1).max(20).optional().describe('Max results (default: 5)'),
384
+ }),
385
+ call: async ({ query, limit = 5 }, _refs, ctx) => {
386
+ // ctx is the full AI context including your custom fields
387
+ return { results: await vectorSearch(query, limit) };
388
+ },
389
+ });
674
390
  ```
675
391
 
676
- **`models.search(criteria)`**
392
+ ### Prompt
677
393
 
678
394
  ```typescript
679
- const results = ai.models.search({
680
- required: ['chat', 'structured'],
681
- optional: ['vision'],
682
- weights: { cost: 0.6, speed: 0.4 },
683
- providers: { allow: ['openai', 'anthropic'] },
684
- minContextWindow: 100000
395
+ const knowledgeAssistant = ai.prompt({
396
+ name: 'knowledgeAssistant',
397
+ description: 'Answers questions using the knowledge base',
398
+ content: `You are a helpful assistant. Use searchKnowledge to find information, then answer the question.
399
+
400
+ Question: {{question}}`,
401
+ input: (input: { question: string }) => ({ question: input.question }),
402
+ tools: [searchKnowledge],
403
+ schema: z.object({
404
+ answer: z.string(),
405
+ sources: z.array(z.string()),
406
+ }),
685
407
  });
686
408
 
687
- console.log('Best match:', results[0].model.id);
688
- console.log('Score:', results[0].score);
409
+ // Get structured result
410
+ const result = await knowledgeAssistant.get('result', { question: 'How does X work?' });
411
+ console.log(result?.answer);
412
+
413
+ // Stream text
414
+ for await (const text of knowledgeAssistant.get('streamContent', { question: 'How does X work?' })) {
415
+ process.stdout.write(text);
416
+ }
689
417
  ```
690
418
 
691
- **`models.refresh()`**
419
+ ### Agent
692
420
 
693
421
  ```typescript
694
- console.log('Refreshing models...');
695
- await ai.models.refresh();
696
- console.log(`Now have ${ai.models.list().length} models`);
422
+ const chatAgent = ai.agent({
423
+ name: 'chatAgent',
424
+ description: 'Chat with the user and analyze the response quality',
425
+ refs: [searchKnowledge, knowledgeAssistant] as const,
426
+ call: async (
427
+ input: { userId: string; question: string },
428
+ [search, assistant],
429
+ ctx
430
+ ) => {
431
+ // Step 1: Search for relevant context
432
+ const { results } = await search.run({ query: input.question }, ctx);
433
+
434
+ // Step 2: Generate answer with knowledge context
435
+ const answer = await assistant.get(
436
+ 'result',
437
+ { question: input.question },
438
+ { ...ctx, messages: [] }
439
+ );
440
+
441
+ return {
442
+ userId: input.userId,
443
+ question: input.question,
444
+ answer: answer?.answer,
445
+ sources: answer?.sources ?? [],
446
+ contextUsed: results.length,
447
+ };
448
+ },
449
+ });
450
+
451
+ const result = await chatAgent.run({ userId: 'user1', question: 'What is aeye?' });
452
+ console.log(result.answer);
697
453
  ```
698
454
 
699
455
  ## Advanced Features
700
456
 
701
457
  ### Custom Context & Metadata
702
458
 
703
- Define your application-specific context and metadata types:
704
-
705
459
  ```typescript
706
460
  interface AppContext {
707
- user: User;
708
- organization: Organization;
709
- db: Database;
710
- requestId: string;
461
+ userId: string;
462
+ user?: User;
463
+ db?: Database;
711
464
  }
712
465
 
713
466
  interface AppMetadata {
467
+ feature: 'chat' | 'analysis';
714
468
  priority: 'low' | 'normal' | 'high';
715
- category: 'support' | 'sales' | 'internal';
716
- maxCost: number;
717
469
  }
718
470
 
719
471
  const ai = AI.with<AppContext, AppMetadata>()
720
- .providers({ openai, anthropic })
472
+ .providers({ openai, openrouter })
721
473
  .create({
722
- defaultContext: {
723
- requestId: uuid()
724
- },
725
474
  providedContext: async (ctx) => {
726
- // Load from database
727
475
  const user = await db.users.findById(ctx.userId);
728
- const org = await db.orgs.findById(user.organizationId);
729
- return { user, organization: org, db };
476
+ return { user, db };
730
477
  },
731
478
  defaultMetadata: {
732
479
  priority: 'normal',
733
- maxCost: 0.10
734
- }
480
+ },
735
481
  });
736
482
 
737
- // Use with minimal input
738
- const response = await ai.chat.get(messages, {
739
- userId: '123', // Rest is loaded automatically
740
- metadata: {
741
- priority: 'high',
742
- category: 'support'
743
- }
744
- });
483
+ // At call time only userId is required
484
+ const response = await ai.chat.get(
485
+ { messages: [{ role: 'user', content: 'Hello' }] },
486
+ { userId: '123', metadata: { feature: 'chat' } }
487
+ );
745
488
  ```
746
489
 
747
490
  ### Lifecycle Hooks
748
491
 
749
- Intercept and modify AI operations at key points:
750
-
751
492
  ```typescript
752
- const ai = AI.with<AppContext, AppMetadata>()
753
- .providers({ openai, anthropic })
754
- .create({
755
- // ... config
756
- })
493
+ const ai = AI.with<AppContext>()
494
+ .providers({ openai })
495
+ .create({ /* ... */ })
757
496
  .withHooks({
758
- beforeModelSelection: async (ctx, metadata) => {
759
- // Adjust selection based on user tier
760
- if (ctx.user.tier === 'free') {
761
- return {
762
- ...metadata,
763
- weights: { cost: 1.0, speed: 0 } // Prioritize cost
764
- };
497
+ beforeModelSelection: async (ctx, request, metadata) => {
498
+ // Adjust weights based on context (return modified metadata)
499
+ if (ctx.user?.tier === 'free') {
500
+ return { ...metadata, weights: { cost: 1.0 } };
765
501
  }
766
502
  return metadata;
767
503
  },
768
504
 
769
- onModelSelected: async (ctx, selected) => {
770
- console.log(`Selected: ${selected.model.id}`);
771
-
772
- // Override model for specific users
773
- if (ctx.user.betaTester && selected.model.id === 'gpt-4') {
774
- return {
775
- ...selected,
776
- model: ai.models.get('gpt-4-turbo')!
777
- };
778
- }
505
+ onModelSelected: async (ctx, request, selected) => {
506
+ console.log(`Selected: ${selected.model.id} (score: ${selected.score.toFixed(3)})`);
507
+ // Return a modified selected model to override, or undefined to accept
779
508
  },
780
509
 
781
- beforeRequest: async (ctx, request, selected, estimatedTokens, estimatedCost) => {
782
- // Check budget
783
- if (estimatedCost > ctx.user.remainingBudget) {
510
+ beforeRequest: async (ctx, request, selected, estimatedUsage, estimatedCost) => {
511
+ // Throw to cancel the request
512
+ if (ctx.user && estimatedCost > ctx.user.budgetRemaining) {
784
513
  throw new Error('Insufficient budget');
785
514
  }
786
-
787
- // Log request
788
- await db.logs.create({
789
- userId: ctx.user.id,
790
- model: selected.model.id,
791
- estimatedTokens,
792
- estimatedCost,
793
- timestamp: new Date()
794
- });
515
+ console.log(
516
+ `[${ctx.userId}] ${selected.model.id} — ` +
517
+ `~${estimatedUsage.text?.input ?? 0} input tokens, ~$${estimatedCost.toFixed(5)}`
518
+ );
795
519
  },
796
520
 
797
521
  afterRequest: async (ctx, request, response, responseComplete, selected, usage, cost) => {
798
- // Track actual usage
799
- await db.users.update(ctx.user.id, {
800
- tokensUsed: usage.totalTokens,
801
- costAccrued: cost,
802
- remainingBudget: ctx.user.remainingBudget - cost
803
- });
804
-
805
- // Update model metrics
806
- await db.modelMetrics.increment(selected.model.id, {
807
- requestCount: 1,
808
- successCount: 1,
809
- totalTokens: usage.totalTokens,
810
- totalCost: cost
811
- });
522
+ // Record actual usage
523
+ if (ctx.user) {
524
+ ctx.user.budgetRemaining -= cost;
525
+ ctx.user.totalSpent += cost;
526
+ await ctx.user.save();
527
+ }
528
+ console.log(
529
+ `[${ctx.userId}] ${usage.text?.input ?? 0} in / ${usage.text?.output ?? 0} out, ` +
530
+ `cost: $${cost.toFixed(5)}`
531
+ );
812
532
  },
813
533
 
814
- onError: (type, message, error, ctx) => {
815
- logger.error('AI Error', {
816
- type,
817
- message,
818
- error,
819
- userId: ctx?.user?.id,
820
- stack: error?.stack
821
- });
822
-
823
- // Send to monitoring
824
- monitoring.captureException(error, {
825
- tags: { type, userId: ctx?.user?.id }
826
- });
827
- }
534
+ onError: (errorType, message, error, ctx) => {
535
+ console.error(`[AI ${errorType}] ${message}`, { error, userId: ctx?.userId });
536
+ },
828
537
  });
829
538
  ```
830
539
 
831
540
  ### Model Selection
832
541
 
833
- Fine-tune model selection with weights and constraints:
834
-
835
542
  ```typescript
836
543
  // Cost-optimized
837
- await ai.chat.get(messages, {
838
- metadata: {
839
- weights: {
840
- cost: 0.9,
841
- speed: 0.05,
842
- accuracy: 0.05
843
- }
844
- }
845
- });
544
+ await ai.chat.get(
545
+ { messages },
546
+ { metadata: { weights: { cost: 0.9, speed: 0.05, accuracy: 0.05 } } }
547
+ );
846
548
 
847
- // Performance-optimized
848
- await ai.chat.get(messages, {
849
- metadata: {
850
- weights: {
851
- cost: 0.1,
852
- speed: 0.5,
853
- accuracy: 0.4
854
- },
855
- minContextWindow: 128000
549
+ // Quality-optimized with large context
550
+ await ai.chat.get(
551
+ { messages },
552
+ {
553
+ metadata: {
554
+ weights: { cost: 0.1, speed: 0.4, accuracy: 0.5 },
555
+ contextWindow: { min: 128000 },
556
+ }
856
557
  }
857
- });
558
+ );
858
559
 
859
560
  // Provider-specific
860
- await ai.chat.get(messages, {
861
- metadata: {
862
- providers: {
863
- allow: ['anthropic'], // Only use Anthropic models
864
- deny: ['replicate'] // Exclude Replicate
865
- }
866
- }
867
- });
561
+ await ai.chat.get(
562
+ { messages },
563
+ { metadata: { providers: { allow: ['openai'], deny: ['replicate'] } } }
564
+ );
868
565
 
869
- // Budget-constrained
870
- await ai.chat.get(messages, {
871
- metadata: {
872
- budget: {
873
- maxCostPerRequest: 0.05,
874
- maxCostPerMillionTokens: 10.0
875
- }
876
- }
877
- });
566
+ // Explicit model override
567
+ await ai.chat.get(
568
+ { messages },
569
+ { metadata: { model: 'openai/gpt-4o' } }
570
+ );
878
571
  ```
879
572
 
880
- #### Selection Profiles
881
-
882
- Use pre-configured profiles:
573
+ ### Named Weight Profiles
883
574
 
884
575
  ```typescript
885
576
  const ai = AI.with()
886
- .providers({ openai, anthropic })
577
+ .providers({ openai, openrouter })
887
578
  .create({
888
- profiles: {
889
- costPriority: { cost: 0.9, speed: 0.1 },
890
- balanced: { cost: 0.5, speed: 0.3, accuracy: 0.2 },
891
- performance: { cost: 0.1, speed: 0.5, accuracy: 0.4 }
892
- }
579
+ weightProfiles: {
580
+ costPriority: { cost: 0.9, speed: 0.1 },
581
+ balanced: { cost: 0.4, speed: 0.3, accuracy: 0.3 },
582
+ performance: { cost: 0.1, speed: 0.4, accuracy: 0.5 },
583
+ },
893
584
  });
894
585
 
895
- // Reference profile in metadata
896
- await ai.chat.get(messages, {
897
- metadata: {
898
- weights: config.profiles.costPriority
899
- }
900
- });
586
+ await ai.chat.get(
587
+ { messages },
588
+ { metadata: { weightProfile: 'balanced' } }
589
+ );
901
590
  ```
902
591
 
903
592
  ### Model Sources
904
593
 
905
- Enrich model information from external registries:
906
-
907
594
  ```typescript
908
- import { openRouterSource } from '@aeye/openrouter';
595
+ import { OpenRouterModelSource } from '@aeye/openrouter';
596
+
597
+ const source = new OpenRouterModelSource({ apiKey: process.env.OPENROUTER_API_KEY });
909
598
 
910
599
  const ai = AI.with()
911
- .providers({ openai, anthropic, openrouter })
600
+ .providers({ openai, openrouter })
912
601
  .create({
913
- // Use OpenRouter as a model source to get pricing/capabilities
914
- modelSources: [openRouterSource],
915
-
916
- // Or configure it explicitly
917
- fetchOpenRouterModels: {
918
- enabled: true,
919
- apiKey: process.env.OPENROUTER_API_KEY
920
- }
602
+ modelSources: [source],
921
603
  });
922
604
 
923
- // Models now have enriched information from OpenRouter
605
+ // Fetch and register all models from sources
924
606
  await ai.models.refresh();
925
- const models = ai.models.list();
926
607
  ```
927
608
 
928
- ### Custom Providers
609
+ ### Model Overrides
929
610
 
930
- You can create custom provider implementations - just implement the Provider interface.
611
+ ```typescript
612
+ const ai = AI.with()
613
+ .providers({ openai })
614
+ .create({
615
+ modelOverrides: [
616
+ {
617
+ modelPattern: /gpt-4/,
618
+ overrides: {
619
+ pricing: { text: { input: 30, output: 60 } },
620
+ },
621
+ },
622
+ ],
623
+ });
624
+ ```
931
625
 
932
- ## TypeScript Guide
626
+ ### Strict Mode
933
627
 
934
- ### Type Safety
628
+ Tool and Prompt schemas can ride the LLM's grammar-constrained strict mode for guaranteed type-safe output. Mechanics — descriptors, dialects, schema rewriting — live in [`@aeye/core`](../core/README.md#strict-mode). What `@aeye/ai` adds: model selection respects the `strict` flag (selection filters to strict-capable models when `strict: true`, scores them higher when `strict: <number>`).
935
629
 
936
- The library provides full TypeScript support with generic type inference:
630
+ Splat the curated `strictSupport` overrides from `@aeye/models` so model selection knows which models actually support strict — without it, every model defaults to lenient even when the underlying API supports strict:
937
631
 
938
632
  ```typescript
939
- // Define your types
940
- interface MyContext {
941
- userId: string;
942
- sessionId: string;
943
- }
633
+ import { models, strictSupport } from '@aeye/models';
944
634
 
945
- interface MyMetadata {
946
- priority: 'low' | 'high';
947
- }
948
-
949
- // Create strongly-typed AI instance
950
- const ai = AI.with<MyContext, MyMetadata>()
635
+ const ai = AI.with()
951
636
  .providers({ openai })
952
637
  .create({
953
- defaultContext: {
954
- sessionId: 'default' // ✓ Valid
955
- // foo: 'bar' // ✗ TypeScript error
956
- }
638
+ models,
639
+ modelOverrides: [...strictSupport],
957
640
  });
641
+ ```
642
+
643
+ `strictSupport` is a `ModelOverride[]` you can combine with your own overrides:
958
644
 
959
- // Type-safe usage
960
- await ai.chat.get(messages, {
961
- userId: '123', // ✓ Required (not in default)
962
- sessionId: 'override', // ✓ Optional (in default)
963
- // invalidField: 'x' // ✗ TypeScript error
964
- metadata: {
965
- priority: 'high' // ✓ Valid enum value
966
- // priority: 'medium' // ✗ TypeScript error
645
+ ```typescript
646
+ modelOverrides: [
647
+ ...strictSupport,
648
+ { modelPattern: /gpt-4/, overrides: { tier: 'flagship' } },
649
+ ],
650
+ ```
651
+
652
+ ### Custom Providers
653
+
654
+ Extend an existing provider or implement the `Provider<TConfig>` interface from `@aeye/core`:
655
+
656
+ ```typescript
657
+ import { OpenAIProvider, OpenAIConfig } from '@aeye/openai';
658
+ import OpenAI from 'openai';
659
+
660
+ class MyProvider extends OpenAIProvider {
661
+ readonly name = 'my-provider';
662
+
663
+ protected createClient(config: OpenAIConfig) {
664
+ return new OpenAI({
665
+ apiKey: config.apiKey,
666
+ baseURL: 'https://my-api.example.com/v1',
667
+ });
967
668
  }
968
- });
669
+ }
969
670
  ```
970
671
 
971
672
  ### Extending AI Instances
972
673
 
973
- Create extended instances for specific use cases:
674
+ Share providers and configuration between multiple AI instances:
974
675
 
975
676
  ```typescript
976
- // Base AI instance
977
677
  const baseAI = AI.with<BaseContext>()
978
- .providers({ openai, anthropic })
979
- .create({ /* ... */ });
678
+ .providers({ openai, openrouter })
679
+ .create({ /* base config */ });
980
680
 
981
- // Extended for chat feature
982
- interface ChatContext extends BaseContext {
983
- chat: Chat;
984
- chatMessage: ChatMessage;
681
+ // Extend with additional context for a specific feature
682
+ interface ChatFeatureContext extends BaseContext {
683
+ chatId: string;
684
+ chatHistory: Message[];
985
685
  }
986
686
 
987
- const chatAI = baseAI.extend<ChatContext>({
988
- defaultContext: {
989
- // chat and chatMessage will be provided per-request
990
- },
991
- modelOverrides: [
992
- {
993
- modelPattern: /gpt/,
994
- overrides: {
995
- pricing: { /* custom pricing */ }
996
- }
997
- }
998
- ]
687
+ const chatAI = baseAI.extend<ChatFeatureContext>({
688
+ defaultContext: { chatHistory: [] },
999
689
  });
1000
690
 
1001
- // Use extended instance
1002
- await chatAI.chat.get(messages, {
1003
- chat,
1004
- chatMessage
1005
- });
691
+ const response = await chatAI.chat.get(
692
+ { messages: [{ role: 'user', content: 'Hello' }] },
693
+ { chatId: 'chat-123', chatHistory: [] }
694
+ );
1006
695
  ```
1007
696
 
1008
697
  ## Examples
1009
698
 
1010
- ### Complete Application Example
699
+ ### Complete Application
1011
700
 
1012
701
  ```typescript
1013
- // types.ts
1014
- export interface AppContext {
1015
- user: User;
1016
- organization: Organization;
1017
- db: Database;
1018
- cache: Redis;
1019
- requestId: string;
702
+ import { AI } from '@aeye/ai';
703
+ import { OpenAIProvider } from '@aeye/openai';
704
+ import { OpenRouterProvider } from '@aeye/openrouter';
705
+ import z from 'zod';
706
+
707
+ interface User {
708
+ id: string;
709
+ budgetRemaining: number;
710
+ totalSpent: number;
711
+ save: () => Promise<void>;
1020
712
  }
1021
713
 
1022
- export interface AppMetadata {
1023
- feature: 'chat' | 'summary' | 'analysis';
1024
- priority: 'low' | 'normal' | 'high';
1025
- maxCost: number;
714
+ interface AppContext {
715
+ userId: string;
716
+ user?: User;
1026
717
  }
1027
718
 
1028
- // ai.ts
1029
- import { AI } from '@aeye/ai';
1030
- import { openai } from '@aeye/openai';
1031
- import { anthropic } from '@aeye/anthropic';
719
+ const openai = new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! });
720
+ const openrouter = new OpenRouterProvider({ apiKey: process.env.OPENROUTER_API_KEY! });
1032
721
 
1033
- export const ai = AI.with<AppContext, AppMetadata>()
1034
- .providers({ openai, anthropic })
722
+ export const ai = AI.with<AppContext>()
723
+ .providers({ openai, openrouter })
1035
724
  .create({
1036
- defaultContext: {
1037
- requestId: () => uuid(),
1038
- cache: redis
1039
- },
1040
- providedContext: async (ctx) => {
1041
- const user = await db.users.findById(ctx.userId);
1042
- const org = await db.organizations.findById(user.organizationId);
1043
- return { user, organization: org, db };
1044
- },
1045
- defaultMetadata: {
1046
- priority: 'normal',
1047
- maxCost: 0.10
1048
- },
1049
- profiles: {
1050
- costPriority: { cost: 0.9, speed: 0.1 },
1051
- balanced: { cost: 0.5, speed: 0.3, accuracy: 0.2 },
1052
- performance: { cost: 0.1, speed: 0.5, accuracy: 0.4 }
1053
- }
1054
- })
1055
- .withHooks({
1056
- beforeRequest: async (ctx, request, selected, estimatedTokens, estimatedCost) => {
1057
- const cost = estimateCost(tokens, selected.model);
1058
- if (cost > ctx.user.remainingBudget) {
1059
- throw new Error('Insufficient budget');
1060
- }
1061
- },
1062
- afterRequest: async (ctx, request, response, responseComplete, selected, usage, cost) => {
1063
- await trackUsage(ctx.user.id, usage, cost);
725
+ providedContext: async (ctx) => ({
726
+ user: await getUser(ctx.userId),
727
+ }),
728
+ defaultWeights: { cost: 0.5, speed: 0.3, accuracy: 0.2 },
729
+ hooks: {
730
+ beforeRequest: async (ctx, _request, selected, estimatedUsage, estimatedCost) => {
731
+ if (ctx.user && estimatedCost > ctx.user.budgetRemaining) {
732
+ throw new Error(
733
+ `Estimated cost $${estimatedCost.toFixed(4)} exceeds budget ` +
734
+ `$${ctx.user.budgetRemaining.toFixed(4)}`
735
+ );
736
+ }
737
+ },
738
+ afterRequest: async (ctx, _request, _response, _complete, _selected, usage, cost) => {
739
+ if (ctx.user) {
740
+ ctx.user.budgetRemaining -= cost;
741
+ ctx.user.totalSpent += cost;
742
+ await ctx.user.save();
743
+ }
744
+ },
745
+ onError: (errorType, message, error, ctx) => {
746
+ console.error(`[AI ${errorType}] ${message}`, { userId: ctx?.userId, error });
747
+ },
1064
748
  },
1065
- onError: (type, message, error, ctx) => {
1066
- logger.error({ type, message, error, userId: ctx?.user?.id });
1067
- }
1068
749
  });
1069
750
 
1070
- // features/chat.ts
1071
- export async function handleChatMessage(
1072
- userId: string,
1073
- message: string
1074
- ) {
1075
- const response = await ai.chat.get(
1076
- [
1077
- { role: 'system', content: 'You are a helpful assistant.' },
1078
- { role: 'user', content: message }
1079
- ],
1080
- {
1081
- userId,
1082
- metadata: {
1083
- feature: 'chat',
1084
- priority: 'normal'
1085
- }
1086
- }
1087
- );
1088
-
1089
- return response.content;
1090
- }
1091
-
1092
- // features/analysis.ts
1093
- export async function analyzeDocument(
1094
- userId: string,
1095
- document: string
1096
- ) {
1097
- const response = await ai.chat.get(
1098
- [
1099
- { role: 'system', content: 'You are a document analysis expert.' },
1100
- { role: 'user', content: `Analyze this document:\n\n${document}` }
1101
- ],
1102
- {
1103
- userId,
1104
- metadata: {
1105
- feature: 'analysis',
1106
- priority: 'high',
1107
- required: ['chat'],
1108
- weights: { accuracy: 0.7, cost: 0.3 }
1109
- }
1110
- }
1111
- );
1112
-
1113
- return response.content;
1114
- }
1115
- ```
1116
-
1117
- ### Prompt Engineering with Context
1118
-
1119
- ```typescript
1120
- import { ai } from './ai';
1121
-
1122
- const summarizer = ai.prompt({
1123
- name: 'summarizer',
1124
- description: 'Summarize documents',
1125
-
1126
- input: async (params: { document: string }, ctx) => {
1127
- // Access context in prompt construction
1128
- const userPrefs = await ctx.db.getUserPreferences(ctx.user.id);
1129
-
1130
- return [{
1131
- role: 'system',
1132
- content: `Summarize documents in ${userPrefs.language}.`
1133
- }, {
1134
- role: 'user',
1135
- content: `Summarize:\n\n${params.document}`
1136
- }];
751
+ // Define a tool
752
+ const webSearch = ai.tool({
753
+ name: 'webSearch',
754
+ description: 'Search the web for recent information',
755
+ schema: z.object({ query: z.string() }),
756
+ call: async ({ query }) => {
757
+ // Integrate with a search API
758
+ return { results: [`Result for: ${query}`] };
1137
759
  },
1138
-
1139
- config: {
1140
- maxTokens: 500,
1141
- temperature: 0.3
1142
- }
1143
760
  });
1144
761
 
1145
- // Use prompt
1146
- const result = await summarizer.execute(
1147
- { document: longText },
1148
- { userId: '123' }
1149
- );
1150
- ```
1151
-
1152
- ### Multi-Step Agent
1153
-
1154
- ```typescript
1155
- const researchAgent = ai.agent({
762
+ // Define a research prompt
763
+ const researcher = ai.prompt({
1156
764
  name: 'researcher',
1157
- description: 'Research topics using multiple AI calls',
1158
-
1159
- call: async (input: { topic: string }, refs, ctx) => {
1160
- // Step 1: Generate research questions
1161
- const questions = await ai.chat.get([{
1162
- role: 'user',
1163
- content: `Generate 3 research questions about: ${input.topic}`
1164
- }], ctx);
1165
-
1166
- // Step 2: Answer each question
1167
- const answers = [];
1168
- for (const question of parseQuestions(questions.content)) {
1169
- const answer = await ai.chat.get([{
1170
- role: 'user',
1171
- content: question
1172
- }], ctx);
1173
- answers.push({ question, answer: answer.content });
1174
- }
765
+ description: 'Researches a topic and provides a structured summary',
766
+ content: `You are a research assistant. Research the following topic and provide a clear summary.
1175
767
 
1176
- // Step 3: Synthesize final report
1177
- const report = await ai.chat.get([{
1178
- role: 'user',
1179
- content: `Create a research report from these Q&As:\n${JSON.stringify(answers, null, 2)}`
1180
- }], ctx);
1181
-
1182
- return report.content;
1183
- }
768
+ Topic: {{topic}}`,
769
+ input: (input: { topic: string }) => ({ topic: input.topic }),
770
+ tools: [webSearch],
771
+ schema: z.object({
772
+ summary: z.string(),
773
+ keyFindings: z.array(z.string()),
774
+ sources: z.array(z.string()),
775
+ }),
1184
776
  });
1185
777
 
1186
- const report = await researchAgent.execute(
1187
- { topic: 'Quantum Computing' },
1188
- { userId: '123' }
1189
- );
778
+ // Use the prompt
779
+ async function researchTopic(userId: string, topic: string) {
780
+ const result = await researcher.get('result', { topic }, { userId });
781
+ return result;
782
+ }
783
+
784
+ const findings = await researchTopic('user123', 'TypeScript 5.0 features');
785
+ console.log(findings?.summary);
786
+ console.log(findings?.keyFindings);
1190
787
  ```
1191
788
 
1192
789
  ## License
1193
790
 
1194
- GPL-3.0
791
+ GPL-3.0 © [ClickerMonkey](https://github.com/ClickerMonkey)
1195
792
 
1196
- ---
793
+ ## Links
1197
794
 
1198
- For more information, visit:
1199
- - GitHub: https://github.com/yourusername/aeye
1200
- - Documentation: https://aeye.dev
1201
- - Discord: https://discord.gg/aeye
795
+ - [Root README](../../README.md)
796
+ - [@aeye/core README](../core/README.md)
797
+ - [GitHub Repository](https://github.com/ClickerMonkey/aeye)
798
+ - [Issue Tracker](https://github.com/ClickerMonkey/aeye/issues)