millas 0.2.12-beta-2 → 0.2.13
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/package.json +3 -2
- package/src/admin/Admin.js +122 -38
- package/src/admin/ViewContext.js +12 -3
- package/src/admin/resources/AdminResource.js +10 -0
- package/src/admin/static/admin.css +95 -14
- package/src/admin/views/layouts/base.njk +23 -34
- package/src/admin/views/pages/detail.njk +16 -5
- package/src/admin/views/pages/error.njk +65 -0
- package/src/admin/views/pages/list.njk +127 -2
- package/src/admin/views/partials/form-scripts.njk +7 -3
- package/src/admin/views/partials/form-widget.njk +2 -1
- package/src/admin/views/partials/icons.njk +64 -0
- package/src/ai/AIManager.js +954 -0
- package/src/ai/AITokenBudget.js +250 -0
- package/src/ai/PromptGuard.js +216 -0
- package/src/ai/agents.js +218 -0
- package/src/ai/conversation.js +213 -0
- package/src/ai/drivers.js +734 -0
- package/src/ai/files.js +249 -0
- package/src/ai/media.js +303 -0
- package/src/ai/pricing.js +152 -0
- package/src/ai/provider_tools.js +114 -0
- package/src/ai/types.js +356 -0
- package/src/commands/createsuperuser.js +17 -4
- package/src/commands/serve.js +2 -4
- package/src/container/AppInitializer.js +39 -15
- package/src/container/Application.js +31 -1
- package/src/core/foundation.js +1 -1
- package/src/errors/HttpError.js +32 -16
- package/src/facades/AI.js +411 -0
- package/src/facades/Hash.js +67 -0
- package/src/facades/Process.js +144 -0
- package/src/hashing/Hash.js +262 -0
- package/src/http/HtmlEscape.js +162 -0
- package/src/http/MillasRequest.js +63 -7
- package/src/http/MillasResponse.js +70 -4
- package/src/http/ResponseDispatcher.js +21 -27
- package/src/http/SafeFilePath.js +195 -0
- package/src/http/SafeRedirect.js +62 -0
- package/src/http/SecurityBootstrap.js +70 -0
- package/src/http/helpers.js +40 -125
- package/src/http/index.js +10 -1
- package/src/http/middleware/CsrfMiddleware.js +258 -0
- package/src/http/middleware/RateLimiter.js +314 -0
- package/src/http/middleware/SecurityHeaders.js +281 -0
- package/src/i18n/Translator.js +10 -2
- package/src/logger/LogRedactor.js +247 -0
- package/src/logger/Logger.js +1 -1
- package/src/logger/formatters/JsonFormatter.js +11 -4
- package/src/logger/formatters/PrettyFormatter.js +3 -1
- package/src/logger/formatters/SimpleFormatter.js +14 -3
- package/src/middleware/ThrottleMiddleware.js +27 -4
- package/src/process/Process.js +333 -0
- package/src/router/MiddlewareRegistry.js +27 -2
- package/src/scaffold/templates.js +3 -0
- package/src/validation/Validator.js +348 -607
- package/src/admin.zip +0 -0
|
@@ -49,57 +49,82 @@ class AppInitializer {
|
|
|
49
49
|
* Boot the full application and start the HTTP server.
|
|
50
50
|
* Returns a Promise that resolves once the server is listening.
|
|
51
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Boot the full application and start the HTTP server.
|
|
54
|
+
* Called by millas serve — no changes to the developer API.
|
|
55
|
+
*/
|
|
52
56
|
async boot() {
|
|
57
|
+
await this.bootKernel();
|
|
58
|
+
await this._serve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Boot the application kernel only — DI container, providers, DB, auth,
|
|
63
|
+
* cache, mail, queue. No HTTP server, no routes, no listen().
|
|
64
|
+
*
|
|
65
|
+
* Used internally by CLI commands (millas migrate, millas createsuperuser, etc.)
|
|
66
|
+
* via MILLAS_CLI_BOOT=1 environment variable. Developers never call this directly.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Application} the booted kernel
|
|
69
|
+
*/
|
|
70
|
+
async bootKernel() {
|
|
53
71
|
const cfg = this._config;
|
|
54
72
|
|
|
55
|
-
// ── Build the HTTP adapter ───────────────────────────────────────────────
|
|
56
73
|
const ExpressAdapter = require('../http/adapters/ExpressAdapter');
|
|
57
74
|
const expressApp = express();
|
|
58
75
|
this._adapter = new ExpressAdapter(expressApp);
|
|
59
76
|
this._adapter.applyBodyParsers();
|
|
60
77
|
|
|
61
|
-
//
|
|
78
|
+
// ── Security — applied before any routes or developer middleware ──────
|
|
79
|
+
// Reads config/app.js for overrides. All protections are on by default:
|
|
80
|
+
// security headers, CSRF, rate limiting, cookie defaults, allowed hosts.
|
|
81
|
+
const SecurityBootstrap = require('../http/SecurityBootstrap');
|
|
82
|
+
const basePath = cfg.basePath || process.cwd();
|
|
83
|
+
const appConfig = SecurityBootstrap.loadConfig(basePath + '/config/app');
|
|
84
|
+
SecurityBootstrap.apply(this._adapter.nativeApp || expressApp, appConfig);
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
62
87
|
for (const mw of (cfg.adapterMiddleware || [])) {
|
|
63
88
|
this._adapter.applyMiddleware(mw);
|
|
64
89
|
}
|
|
65
90
|
|
|
66
|
-
// ── Build the Application kernel ─────────────────────────────────────────
|
|
67
91
|
this._kernel = new Application(this._adapter);
|
|
68
92
|
|
|
69
|
-
// Bind basePath so all providers can resolve config/model paths
|
|
70
|
-
// without calling process.cwd() at runtime.
|
|
71
|
-
const basePath = cfg.basePath || process.cwd();
|
|
72
93
|
this._kernel._container.instance('basePath', basePath);
|
|
73
94
|
|
|
74
|
-
// Core providers (auto-enabled unless disabled in config)
|
|
75
95
|
const coreProviders = this._buildCoreProviders(cfg);
|
|
76
96
|
this._kernel.providers([...coreProviders, ...cfg.providers]);
|
|
77
97
|
|
|
78
|
-
// Named middleware aliases
|
|
79
98
|
for (const {alias, handler} of (cfg.middleware || [])) {
|
|
80
99
|
this._kernel.middleware(alias, handler);
|
|
81
100
|
}
|
|
82
101
|
|
|
83
|
-
// Route definitions
|
|
84
102
|
if (cfg.routes) {
|
|
85
103
|
this._kernel.routes(cfg.routes);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
|
-
// ── Boot providers ───────────────────────────────────────────────────────
|
|
89
106
|
await this._kernel.boot();
|
|
90
107
|
|
|
91
|
-
|
|
108
|
+
return this._kernel;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Mount routes and start the HTTP server.
|
|
113
|
+
* Internal — called only by boot(). CLI commands stop after bootKernel().
|
|
114
|
+
*/
|
|
115
|
+
async _serve() {
|
|
116
|
+
const cfg = this._config;
|
|
117
|
+
|
|
92
118
|
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
93
119
|
this._kernel.mountRoutes();
|
|
94
120
|
|
|
95
|
-
// Admin panel — mounted between routes and fallbacks
|
|
96
121
|
if (cfg.admin !== null) {
|
|
97
122
|
try {
|
|
98
123
|
const Admin = require('../admin/Admin');
|
|
99
124
|
if (cfg.admin && Object.keys(cfg.admin).length) {
|
|
100
125
|
Admin.configure(cfg.admin);
|
|
101
126
|
}
|
|
102
|
-
Admin.mount(
|
|
127
|
+
Admin.mount(this._adapter.nativeApp);
|
|
103
128
|
} catch (err) {
|
|
104
129
|
process.stderr.write(`[millas] Admin mount failed: ${err.message}\n`);
|
|
105
130
|
}
|
|
@@ -107,9 +132,8 @@ class AppInitializer {
|
|
|
107
132
|
|
|
108
133
|
this._kernel.mountFallbacks();
|
|
109
134
|
|
|
110
|
-
// ── Start the HTTP server ──────────────────────────────────────────────
|
|
111
135
|
const server = new HttpServer(this._kernel, {
|
|
112
|
-
onStart:
|
|
136
|
+
onStart: cfg.onStart || undefined,
|
|
113
137
|
onShutdown: cfg.onShutdown || undefined,
|
|
114
138
|
});
|
|
115
139
|
|
|
@@ -98,6 +98,22 @@ class Application {
|
|
|
98
98
|
await this._providers.boot();
|
|
99
99
|
this._booted = true;
|
|
100
100
|
|
|
101
|
+
// Wire cache, db, and storage into AI manager now that providers are booted
|
|
102
|
+
try {
|
|
103
|
+
const ai = this._container.make('ai');
|
|
104
|
+
if (ai) {
|
|
105
|
+
try { const cache = this._container.make('cache'); if (cache) ai.setCache(cache); } catch {}
|
|
106
|
+
try { const db = this._container.make('db'); if (db) ai.setDb(db); } catch {}
|
|
107
|
+
try { const store = this._container.make('storage'); if (store) ai.setStorage(store); } catch {}
|
|
108
|
+
// Attach files and stores API as properties
|
|
109
|
+
try {
|
|
110
|
+
const { AIFilesAPI, AIStoresAPI } = require('../ai/files');
|
|
111
|
+
if (!ai.files) Object.defineProperty(ai, 'files', { get: () => new AIFilesAPI(ai), configurable: true });
|
|
112
|
+
if (!ai.stores) Object.defineProperty(ai, 'stores', { get: () => new AIStoresAPI(ai), configurable: true });
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* ai not registered — skip */ }
|
|
116
|
+
|
|
101
117
|
this._emitSync('platform.booted', {providers: this._providers.list()});
|
|
102
118
|
|
|
103
119
|
return this;
|
|
@@ -287,9 +303,23 @@ class Application {
|
|
|
287
303
|
});
|
|
288
304
|
this._container.instance('url', urlGenerator);
|
|
289
305
|
|
|
306
|
+
const { HashManager } = require('../hashing/Hash');
|
|
307
|
+
const hashManager = new HashManager({ default: 'bcrypt', bcrypt: { rounds: 12 } });
|
|
308
|
+
this._container.instance('hash', hashManager);
|
|
309
|
+
|
|
310
|
+
const ProcessManager = require('../process/Process').ProcessManager;
|
|
311
|
+
this._container.instance('process', new ProcessManager());
|
|
312
|
+
|
|
313
|
+
const { AIManager } = require('../ai/AIManager');
|
|
314
|
+
const basePath = (() => { try { return this._container.make('basePath'); } catch { return process.cwd(); } })();
|
|
315
|
+
let aiConfig = { default: process.env.AI_PROVIDER || 'anthropic', providers: {} };
|
|
316
|
+
try { aiConfig = require(basePath + '/config/ai'); } catch { /* no config — use env vars */ }
|
|
317
|
+
const aiManager = new AIManager(aiConfig);
|
|
318
|
+
this._container.instance('ai', aiManager);
|
|
319
|
+
|
|
290
320
|
|
|
291
321
|
this._mwRegistry.register('cors', new CorsMiddleware());
|
|
292
|
-
this._mwRegistry.register('throttle',
|
|
322
|
+
this._mwRegistry.register('throttle', ThrottleMiddleware);
|
|
293
323
|
this._mwRegistry.register('log', new LogMiddleware());
|
|
294
324
|
this._mwRegistry.register('auth', AuthMiddleware);
|
|
295
325
|
}
|
package/src/core/foundation.js
CHANGED
|
@@ -41,7 +41,6 @@ const { CacheServiceProvider, StorageServiceProvider } = require('../providers/C
|
|
|
41
41
|
// ── Storage ───────────────────────────────────────────────────────
|
|
42
42
|
const Storage = require('../storage/Storage');
|
|
43
43
|
const LocalDriver = require('../storage/drivers/LocalDriver');
|
|
44
|
-
|
|
45
44
|
module.exports = {
|
|
46
45
|
// ── Millas HTTP layer ──────────────────────────────────────────
|
|
47
46
|
MillasRequest, MillasResponse, ResponseDispatcher, RequestContext,
|
|
@@ -56,4 +55,5 @@ module.exports = {
|
|
|
56
55
|
Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
|
|
57
56
|
// Storage
|
|
58
57
|
Storage, LocalDriver, StorageServiceProvider,
|
|
58
|
+
Log: require('../logger').Log,
|
|
59
59
|
};
|
package/src/errors/HttpError.js
CHANGED
|
@@ -3,27 +3,43 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* HttpError
|
|
5
5
|
*
|
|
6
|
-
* A structured error
|
|
7
|
-
* Thrown by
|
|
8
|
-
*
|
|
6
|
+
* A structured HTTP error with a status code, message, and optional
|
|
7
|
+
* field-level error map. Thrown by abort(), notFound(), forbidden() etc.
|
|
8
|
+
* and caught by the framework's error handler for consistent responses.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* throw new HttpError(
|
|
12
|
-
* throw new HttpError(422, 'Validation failed', { email: ['email is required'] })
|
|
10
|
+
* throw new HttpError(404, 'User not found');
|
|
11
|
+
* throw new HttpError(422, 'Validation failed', { email: ['Required'] });
|
|
13
12
|
*/
|
|
14
13
|
class HttpError extends Error {
|
|
15
14
|
/**
|
|
16
|
-
* @param {number} status HTTP status code
|
|
17
|
-
* @param {string} message
|
|
18
|
-
* @param {object} errors
|
|
15
|
+
* @param {number} status — HTTP status code
|
|
16
|
+
* @param {string} [message]
|
|
17
|
+
* @param {object|null} [errors] — field-level errors { field: [msg, ...] }
|
|
19
18
|
*/
|
|
20
|
-
constructor(status
|
|
21
|
-
super(message);
|
|
22
|
-
this.name
|
|
23
|
-
this.status
|
|
24
|
-
this.
|
|
25
|
-
|
|
19
|
+
constructor(status, message, errors = null) {
|
|
20
|
+
super(message || HttpError.defaultMessage(status));
|
|
21
|
+
this.name = 'HttpError';
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.errors = errors || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static defaultMessage(status) {
|
|
27
|
+
const messages = {
|
|
28
|
+
400: 'Bad Request',
|
|
29
|
+
401: 'Unauthorized',
|
|
30
|
+
403: 'Forbidden',
|
|
31
|
+
404: 'Not Found',
|
|
32
|
+
405: 'Method Not Allowed',
|
|
33
|
+
409: 'Conflict',
|
|
34
|
+
410: 'Gone',
|
|
35
|
+
422: 'Unprocessable Entity',
|
|
36
|
+
429: 'Too Many Requests',
|
|
37
|
+
500: 'Internal Server Error',
|
|
38
|
+
502: 'Bad Gateway',
|
|
39
|
+
503: 'Service Unavailable',
|
|
40
|
+
};
|
|
41
|
+
return messages[status] || 'Error';
|
|
26
42
|
}
|
|
27
43
|
}
|
|
28
44
|
|
|
29
|
-
module.exports = HttpError;
|
|
45
|
+
module.exports = HttpError;
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createFacade } = require('./Facade');
|
|
4
|
+
|
|
5
|
+
// ── Service & types re-exported for application use ───────────────────────────
|
|
6
|
+
const AIService = require('../ai/AIManager');
|
|
7
|
+
const { AIManager, PendingRequest } = require('../ai/AIManager');
|
|
8
|
+
const {
|
|
9
|
+
AIMessage, AIResponse, AIStreamEvent,
|
|
10
|
+
Tool, ToolBuilder, Thread, Prompt, Schema,
|
|
11
|
+
AIError, AIRateLimitError, AIStructuredOutputError, AIProviderError,
|
|
12
|
+
} = require('../ai/types');
|
|
13
|
+
const {
|
|
14
|
+
PendingImage, AIImageResponse,
|
|
15
|
+
PendingAudio, AIAudioResponse,
|
|
16
|
+
PendingTranscription, AITranscriptionResponse,
|
|
17
|
+
PendingReranking, AIRerankResponse,
|
|
18
|
+
} = require('../ai/media');
|
|
19
|
+
const { ConversationThread, AI_MIGRATIONS } = require('../ai/conversation');
|
|
20
|
+
const { AIFile, PendingFile, AIFilesAPI, AIVectorStore, AIStoresAPI } = require('../ai/files');
|
|
21
|
+
const { WebSearch, WebFetch, FileSearch } = require('../ai/provider_tools');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* AI facade — multi-provider LLM client for Millas.
|
|
25
|
+
*
|
|
26
|
+
* Resolved from the DI container as 'ai'.
|
|
27
|
+
* Auto-configured from config/ai.js or environment variables.
|
|
28
|
+
*
|
|
29
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
* SUPPORTED PROVIDERS
|
|
31
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* Provider | Text | Image | TTS | STT | Embed | Rerank
|
|
34
|
+
* ────────────┼──────┼───────┼─────┼─────┼───────┼───────
|
|
35
|
+
* anthropic | ✓ | | | | |
|
|
36
|
+
* openai | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
37
|
+
* gemini | ✓ | ✓ | | | ✓ |
|
|
38
|
+
* groq | ✓ | | | ✓ | |
|
|
39
|
+
* mistral | ✓ | | | ✓ | ✓ |
|
|
40
|
+
* xai | ✓ | ✓ | | | |
|
|
41
|
+
* deepseek | ✓ | | | | |
|
|
42
|
+
* azure | ✓ | | ✓ | ✓ | ✓ |
|
|
43
|
+
* cohere | ✓ | | | | ✓ | ✓
|
|
44
|
+
* ollama | ✓ | | | | ✓ |
|
|
45
|
+
* elevenlabs | | | ✓ | ✓ | |
|
|
46
|
+
*
|
|
47
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
* CONFIGURATION (config/ai.js)
|
|
49
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
*
|
|
51
|
+
* module.exports = {
|
|
52
|
+
* default: 'anthropic',
|
|
53
|
+
* audioProvider: 'elevenlabs', // default provider for TTS/STT
|
|
54
|
+
* rerankProvider: 'cohere', // default provider for reranking
|
|
55
|
+
* providers: {
|
|
56
|
+
* anthropic: { apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-sonnet-4-20250514' },
|
|
57
|
+
* openai: { apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', embeddingModel: 'text-embedding-3-small' },
|
|
58
|
+
* gemini: { apiKey: process.env.GEMINI_API_KEY, model: 'gemini-2.0-flash' },
|
|
59
|
+
* groq: { apiKey: process.env.GROQ_API_KEY, model: 'llama-3.1-70b-versatile' },
|
|
60
|
+
* mistral: { apiKey: process.env.MISTRAL_API_KEY, model: 'mistral-large-latest' },
|
|
61
|
+
* xai: { apiKey: process.env.XAI_API_KEY, model: 'grok-2' },
|
|
62
|
+
* deepseek: { apiKey: process.env.DEEPSEEK_API_KEY, model: 'deepseek-chat' },
|
|
63
|
+
* cohere: { apiKey: process.env.COHERE_API_KEY },
|
|
64
|
+
* elevenlabs: { apiKey: process.env.ELEVENLABS_API_KEY },
|
|
65
|
+
* ollama: { baseUrl: 'http://localhost:11434', model: 'llama3.2' },
|
|
66
|
+
* azure: { apiKey: process.env.AZURE_OPENAI_KEY, endpoint: process.env.AZURE_OPENAI_ENDPOINT, deployment: 'gpt-4o', apiVersion: '2024-02-01' },
|
|
67
|
+
* },
|
|
68
|
+
* };
|
|
69
|
+
*
|
|
70
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
* TEXT GENERATION
|
|
72
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
*
|
|
74
|
+
* // Simplest call
|
|
75
|
+
* const res = await AI.text('What is the capital of Kenya?');
|
|
76
|
+
* console.log(res.text); // 'Nairobi'
|
|
77
|
+
* console.log(res.totalTokens); // 42
|
|
78
|
+
*
|
|
79
|
+
* // Full builder
|
|
80
|
+
* const res = await AI
|
|
81
|
+
* .using('openai')
|
|
82
|
+
* .model('gpt-4o')
|
|
83
|
+
* .system('You are concise.')
|
|
84
|
+
* .temperature(0.7)
|
|
85
|
+
* .maxTokens(500)
|
|
86
|
+
* .retry(3, 1000)
|
|
87
|
+
* .fallback(['anthropic', { provider: 'gemini', model: 'gemini-2.0-flash' }])
|
|
88
|
+
* .cache(600)
|
|
89
|
+
* .tokenBudget(10000)
|
|
90
|
+
* .generate('Explain recursion in one paragraph.');
|
|
91
|
+
*
|
|
92
|
+
* // Extended thinking (Anthropic claude-3-7+)
|
|
93
|
+
* const res = await AI
|
|
94
|
+
* .using('anthropic')
|
|
95
|
+
* .think(16000)
|
|
96
|
+
* .generate('Prove there are infinitely many primes.');
|
|
97
|
+
*
|
|
98
|
+
* console.log(res.thinking); // internal reasoning chain
|
|
99
|
+
* console.log(res.text); // final answer
|
|
100
|
+
*
|
|
101
|
+
* // Per-provider options
|
|
102
|
+
* await AI.providerOptions({
|
|
103
|
+
* openai: { reasoning: { effort: 'low' }, frequency_penalty: 0.5 },
|
|
104
|
+
* anthropic: { thinking: { budget_tokens: 1024 } },
|
|
105
|
+
* }).generate('...');
|
|
106
|
+
*
|
|
107
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
* STREAMING
|
|
109
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
*
|
|
111
|
+
* // Event-based
|
|
112
|
+
* for await (const event of AI.stream('Tell me a story')) {
|
|
113
|
+
* if (event.type === 'delta') process.stdout.write(event.data.text);
|
|
114
|
+
* if (event.type === 'thinking') console.log('[thinking]', event.data.text);
|
|
115
|
+
* if (event.type === 'tool_call') console.log('Tool called:', event.data.name);
|
|
116
|
+
* if (event.type === 'complete') console.log('Tokens:', event.data.totalTokens);
|
|
117
|
+
* }
|
|
118
|
+
*
|
|
119
|
+
* // Token callback shorthand
|
|
120
|
+
* const res = await AI
|
|
121
|
+
* .onToken(chunk => expressRes.write(chunk))
|
|
122
|
+
* .generate('Write a poem about Kenya');
|
|
123
|
+
*
|
|
124
|
+
* // SSE endpoint
|
|
125
|
+
* app.get('/chat', async (req, res) => {
|
|
126
|
+
* res.setHeader('Content-Type', 'text/event-stream');
|
|
127
|
+
* for await (const event of AI.stream(req.query.q)) {
|
|
128
|
+
* if (event.type === 'delta')
|
|
129
|
+
* res.write('data: ' + JSON.stringify({ text: event.data.text }) + '\n\n');
|
|
130
|
+
* if (event.type === 'complete')
|
|
131
|
+
* res.write('data: [DONE]\n\n');
|
|
132
|
+
* }
|
|
133
|
+
* res.end();
|
|
134
|
+
* });
|
|
135
|
+
*
|
|
136
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
* STRUCTURED OUTPUT
|
|
138
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
*
|
|
140
|
+
* const res = await AI
|
|
141
|
+
* .structured(Schema.define({
|
|
142
|
+
* sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
|
|
143
|
+
* confidence: { type: 'number', min: 0, max: 1 },
|
|
144
|
+
* summary: { type: 'string' },
|
|
145
|
+
* keywords: { type: 'array' },
|
|
146
|
+
* }))
|
|
147
|
+
* .generate('Analyse: "Great product, fast shipping!"');
|
|
148
|
+
*
|
|
149
|
+
* res.parsed.sentiment; // 'positive'
|
|
150
|
+
* res.parsed.confidence; // 0.97
|
|
151
|
+
*
|
|
152
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
* TOOL CALLING & AGENTS
|
|
154
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
*
|
|
156
|
+
* const weatherTool = Tool.define('get_weather')
|
|
157
|
+
* .description('Get real-time weather for a city')
|
|
158
|
+
* .parameters({
|
|
159
|
+
* type: 'object',
|
|
160
|
+
* properties: {
|
|
161
|
+
* city: { type: 'string' },
|
|
162
|
+
* units: { type: 'string', enum: ['celsius', 'fahrenheit'] },
|
|
163
|
+
* },
|
|
164
|
+
* required: ['city'],
|
|
165
|
+
* })
|
|
166
|
+
* .handle(async ({ city, units = 'celsius' }) => WeatherAPI.get(city, units))
|
|
167
|
+
* .build();
|
|
168
|
+
*
|
|
169
|
+
* // Agentic loop — tool calls handled automatically
|
|
170
|
+
* const res = await AI
|
|
171
|
+
* .tools([weatherTool, calendarTool])
|
|
172
|
+
* .agent('Book a meeting for a sunny day in Nairobi next week');
|
|
173
|
+
*
|
|
174
|
+
* // Provider-native tools (executed by the AI provider, not your app)
|
|
175
|
+
* await AI.tools([new WebSearch().max(5)]).generate('What happened in AI today?');
|
|
176
|
+
* await AI.tools([new WebFetch().allow(['docs.example.com'])]).generate('Summarise https://...');
|
|
177
|
+
* await AI.tools([new FileSearch({ stores: ['vs_abc'] })]).generate('Find docs about...');
|
|
178
|
+
*
|
|
179
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
* CONVERSATIONS (DB-persisted)
|
|
181
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
182
|
+
*
|
|
183
|
+
* // Start a new conversation
|
|
184
|
+
* const thread = await AI.conversation.forUser(user.id, 'SalesCoach').create('Q3 Review');
|
|
185
|
+
* await thread.addUser('Analyse this transcript...');
|
|
186
|
+
* const res = await AI.withThread(thread).generate();
|
|
187
|
+
* await thread.addAssistant(res.text);
|
|
188
|
+
*
|
|
189
|
+
* const conversationId = thread.id; // store for later
|
|
190
|
+
*
|
|
191
|
+
* // Continue an existing conversation
|
|
192
|
+
* const thread = await AI.conversation.continue(conversationId);
|
|
193
|
+
* await thread.addUser('Tell me more about that.');
|
|
194
|
+
* const res = await AI.withThread(thread).generate();
|
|
195
|
+
*
|
|
196
|
+
* // List a user's conversations
|
|
197
|
+
* const history = await ConversationThread.list(user.id, 'SalesCoach', 20);
|
|
198
|
+
*
|
|
199
|
+
* // In-memory thread (no DB)
|
|
200
|
+
* const thread = AI.thread('You are helpful.').limit(20).summariseWith(async (old) => {
|
|
201
|
+
* return (await AI.text('Summarise: ' + old.map(m=>m.content).join('\n'))).text;
|
|
202
|
+
* });
|
|
203
|
+
* thread.addUser('Hello!');
|
|
204
|
+
*
|
|
205
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
* IMAGE GENERATION
|
|
207
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
*
|
|
209
|
+
* const img = await AI.image('A donut on a counter')
|
|
210
|
+
* .landscape() // or .portrait() or .square()
|
|
211
|
+
* .quality('high') // 'high' | 'standard' | 'low'
|
|
212
|
+
* .using('openai')
|
|
213
|
+
* .generate();
|
|
214
|
+
*
|
|
215
|
+
* await img.store('images/donut.png');
|
|
216
|
+
* const buffer = img.buffer; // raw Buffer
|
|
217
|
+
*
|
|
218
|
+
* // With reference images
|
|
219
|
+
* const img = await AI.image('Update this to an impressionist style')
|
|
220
|
+
* .attachments([AI.files.fromStorage('photo.jpg')])
|
|
221
|
+
* .generate();
|
|
222
|
+
*
|
|
223
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
* AUDIO — TEXT TO SPEECH
|
|
225
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
*
|
|
227
|
+
* const audio = await AI.speak('Welcome to Millas!')
|
|
228
|
+
* .female() // or .male() or .voice('voice-id')
|
|
229
|
+
* .instructions('Speak like a news anchor')
|
|
230
|
+
* .using('elevenlabs')
|
|
231
|
+
* .generate();
|
|
232
|
+
*
|
|
233
|
+
* await audio.store('audio/welcome.mp3');
|
|
234
|
+
* const buffer = audio.buffer;
|
|
235
|
+
*
|
|
236
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
* AUDIO — SPEECH TO TEXT (TRANSCRIPTION)
|
|
238
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
*
|
|
240
|
+
* // From file path
|
|
241
|
+
* const t = await AI.transcribe.fromPath('/recordings/call.mp3')
|
|
242
|
+
* .diarize() // include who said what
|
|
243
|
+
* .language('en')
|
|
244
|
+
* .generate();
|
|
245
|
+
*
|
|
246
|
+
* console.log(t.text); // full transcript
|
|
247
|
+
* console.log(t.speakers); // per-speaker segments
|
|
248
|
+
*
|
|
249
|
+
* // From storage
|
|
250
|
+
* const t = await AI.transcribe.fromStorage('uploads/meeting.mp3').generate();
|
|
251
|
+
*
|
|
252
|
+
* // From buffer
|
|
253
|
+
* const t = await AI.transcribe.fromBuffer(audioBuffer, 'audio.mp3').generate();
|
|
254
|
+
*
|
|
255
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
256
|
+
* EMBEDDINGS
|
|
257
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
258
|
+
*
|
|
259
|
+
* // Single text
|
|
260
|
+
* const [vector] = await AI.using('openai').embed('Napa Valley has great wine.');
|
|
261
|
+
*
|
|
262
|
+
* // Batch
|
|
263
|
+
* const vectors = await AI.embed(['text one', 'text two'], null, 'openai');
|
|
264
|
+
*
|
|
265
|
+
* // Local (Ollama)
|
|
266
|
+
* const [vector] = await AI.using('ollama').embed('Hello world');
|
|
267
|
+
*
|
|
268
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
269
|
+
* RERANKING
|
|
270
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
*
|
|
272
|
+
* const result = await AI
|
|
273
|
+
* .rerank(['Django is a Python framework.', 'Laravel is PHP.', 'React is JS.'])
|
|
274
|
+
* .limit(2)
|
|
275
|
+
* .using('cohere')
|
|
276
|
+
* .rerank('PHP frameworks');
|
|
277
|
+
*
|
|
278
|
+
* result.first.document; // 'Laravel is PHP.'
|
|
279
|
+
* result.first.score; // 0.95
|
|
280
|
+
* for (const r of result) console.log(r.score, r.document);
|
|
281
|
+
*
|
|
282
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
283
|
+
* FILE STORAGE
|
|
284
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
*
|
|
286
|
+
* // Upload a file to the provider
|
|
287
|
+
* const file = await AI.files.fromPath('/reports/q3.pdf').put();
|
|
288
|
+
* const file = await AI.files.fromStorage('uploads/report.pdf').put();
|
|
289
|
+
* const file = await AI.files.fromUrl('https://example.com/doc.pdf').put();
|
|
290
|
+
*
|
|
291
|
+
* console.log(file.id); // 'file-abc123'
|
|
292
|
+
*
|
|
293
|
+
* // Use a stored file in a prompt
|
|
294
|
+
* const res = await AI
|
|
295
|
+
* .withMessage([
|
|
296
|
+
* { type: 'text', text: 'Summarise this document.' },
|
|
297
|
+
* { type: 'file_ref', fileId: file.id },
|
|
298
|
+
* ])
|
|
299
|
+
* .generate();
|
|
300
|
+
*
|
|
301
|
+
* // Retrieve or delete
|
|
302
|
+
* const meta = await AI.files.fromId('file-abc').get();
|
|
303
|
+
* await AI.files.fromId('file-abc').delete();
|
|
304
|
+
*
|
|
305
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
306
|
+
* VECTOR STORES (RAG)
|
|
307
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
308
|
+
*
|
|
309
|
+
* // Create a store
|
|
310
|
+
* const store = await AI.stores.create('Knowledge Base');
|
|
311
|
+
*
|
|
312
|
+
* // Add files
|
|
313
|
+
* await store.add(AI.files.fromPath('/docs/manual.pdf'));
|
|
314
|
+
* await store.add('file-abc123', { author: 'Alice', year: 2025 });
|
|
315
|
+
*
|
|
316
|
+
* // Use it in a prompt with FileSearch tool
|
|
317
|
+
* await AI
|
|
318
|
+
* .tools([new FileSearch({ stores: [store.id] })])
|
|
319
|
+
* .generate('Find docs about authentication');
|
|
320
|
+
*
|
|
321
|
+
* // Remove a file
|
|
322
|
+
* await store.remove('file-abc123', { deleteFile: true });
|
|
323
|
+
*
|
|
324
|
+
* // Retrieve or delete the store
|
|
325
|
+
* const store = await AI.stores.get('vs_abc123');
|
|
326
|
+
* await AI.stores.delete('vs_abc123');
|
|
327
|
+
*
|
|
328
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
329
|
+
* MIDDLEWARE
|
|
330
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
331
|
+
*
|
|
332
|
+
* const logUsage = async (req, next) => {
|
|
333
|
+
* const start = Date.now();
|
|
334
|
+
* const res = await next(req);
|
|
335
|
+
* Log.info(`AI [${req.provider}] ${Date.now()-start}ms — ${res.totalTokens} tokens`);
|
|
336
|
+
* return res;
|
|
337
|
+
* };
|
|
338
|
+
*
|
|
339
|
+
* await AI.use(logUsage).generate('...');
|
|
340
|
+
*
|
|
341
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
342
|
+
* PROMPT TEMPLATES
|
|
343
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
344
|
+
*
|
|
345
|
+
* const prompt = AI.prompt('Translate "{{text}}" to {{language}}.')
|
|
346
|
+
* .with({ text: 'Hello', language: 'Swahili' });
|
|
347
|
+
*
|
|
348
|
+
* await AI.text(prompt.toString());
|
|
349
|
+
*
|
|
350
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
351
|
+
* TESTING
|
|
352
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
*
|
|
354
|
+
* AI.swap({
|
|
355
|
+
* text: async () => new AIResponse({ text: 'Mocked', provider: 'mock', model: 'mock' }),
|
|
356
|
+
* stream: async function*() { yield AIStreamEvent.delta('Mocked'); },
|
|
357
|
+
* });
|
|
358
|
+
* AI.restore();
|
|
359
|
+
*
|
|
360
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
361
|
+
*
|
|
362
|
+
* @see src/ai/AIManager.js
|
|
363
|
+
* @see src/ai/drivers.js
|
|
364
|
+
* @see src/ai/types.js
|
|
365
|
+
* @see src/ai/media.js
|
|
366
|
+
* @see src/ai/conversation.js
|
|
367
|
+
* @see src/ai/files.js
|
|
368
|
+
* @see src/ai/provider_tools.js
|
|
369
|
+
*/
|
|
370
|
+
class AI extends createFacade('ai') {}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
AI,
|
|
374
|
+
// Manager
|
|
375
|
+
AIManager,
|
|
376
|
+
PendingRequest,
|
|
377
|
+
// Core types
|
|
378
|
+
AIMessage,
|
|
379
|
+
AIResponse,
|
|
380
|
+
AIStreamEvent,
|
|
381
|
+
// Tool system
|
|
382
|
+
Tool,
|
|
383
|
+
ToolBuilder,
|
|
384
|
+
// Provider-native tools
|
|
385
|
+
WebSearch,
|
|
386
|
+
WebFetch,
|
|
387
|
+
FileSearch,
|
|
388
|
+
// Memory
|
|
389
|
+
Thread,
|
|
390
|
+
ConversationThread,
|
|
391
|
+
AI_MIGRATIONS,
|
|
392
|
+
// Templating & schema
|
|
393
|
+
Prompt,
|
|
394
|
+
Schema,
|
|
395
|
+
// Media
|
|
396
|
+
PendingImage, AIImageResponse,
|
|
397
|
+
PendingAudio, AIAudioResponse,
|
|
398
|
+
PendingTranscription, AITranscriptionResponse,
|
|
399
|
+
PendingReranking, AIRerankResponse,
|
|
400
|
+
// Files & vector stores
|
|
401
|
+
AIFile,
|
|
402
|
+
PendingFile,
|
|
403
|
+
AIFilesAPI,
|
|
404
|
+
AIVectorStore,
|
|
405
|
+
AIStoresAPI,
|
|
406
|
+
// Errors
|
|
407
|
+
AIError,
|
|
408
|
+
AIRateLimitError,
|
|
409
|
+
AIStructuredOutputError,
|
|
410
|
+
AIProviderError,
|
|
411
|
+
};
|