millas 0.2.12-beta-1 → 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/ActivityLog.js +153 -52
- package/src/admin/Admin.js +516 -199
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +318 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +393 -97
- package/src/admin/static/admin.css +1422 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +87 -1046
- package/src/admin/views/pages/detail.njk +56 -21
- package/src/admin/views/pages/error.njk +65 -0
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +270 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +480 -0
- package/src/admin/views/partials/form-widget.njk +297 -0
- package/src/admin/views/partials/icons.njk +64 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -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/auth/Auth.js +18 -2
- package/src/auth/AuthUser.js +65 -44
- package/src/cli.js +3 -1
- package/src/commands/createsuperuser.js +267 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +3 -4
- package/src/container/AppInitializer.js +101 -20
- package/src/container/Application.js +31 -1
- package/src/container/MillasApp.js +10 -3
- package/src/container/MillasConfig.js +35 -6
- package/src/core/admin.js +5 -0
- package/src/core/db.js +2 -1
- package/src/core/foundation.js +2 -10
- package/src/core/lang.js +1 -0
- 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/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +643 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- 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 +103 -65
- package/src/logger/formatters/SimpleFormatter.js +14 -3
- package/src/middleware/ThrottleMiddleware.js +27 -4
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +143 -74
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/process/Process.js +333 -0
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +40 -5
- package/src/providers/CacheStorageServiceProvider.js +2 -2
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/LogServiceProvider.js +4 -1
- package/src/providers/MailServiceProvider.js +1 -1
- package/src/providers/QueueServiceProvider.js +1 -1
- package/src/router/MiddlewareRegistry.js +27 -2
- package/src/scaffold/templates.js +80 -21
- package/src/validation/Validator.js +348 -607
|
@@ -25,6 +25,11 @@ const AppInitializer = require("./AppInitializer");
|
|
|
25
25
|
class MillasConfig {
|
|
26
26
|
constructor() {
|
|
27
27
|
this._config = {
|
|
28
|
+
// Absolute path to the project root — passed to all providers
|
|
29
|
+
// so they never need to call process.cwd() at runtime.
|
|
30
|
+
// Set via .configure(__dirname) in bootstrap/app.js.
|
|
31
|
+
basePath: null,
|
|
32
|
+
|
|
28
33
|
// Service providers
|
|
29
34
|
providers: [],
|
|
30
35
|
|
|
@@ -35,13 +40,14 @@ class MillasConfig {
|
|
|
35
40
|
middleware: [],
|
|
36
41
|
|
|
37
42
|
// Core service toggles
|
|
43
|
+
logging: true,
|
|
38
44
|
database: true,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
auth: true, // AuthServiceProvider — always on by default
|
|
46
|
+
cache: true,
|
|
47
|
+
storage: true,
|
|
48
|
+
mail: true,
|
|
49
|
+
queue: true,
|
|
50
|
+
events: true,
|
|
45
51
|
|
|
46
52
|
// Admin panel — null means disabled, {} or options object means enabled
|
|
47
53
|
admin: null,
|
|
@@ -57,6 +63,22 @@ class MillasConfig {
|
|
|
57
63
|
|
|
58
64
|
// ── Chainable config methods ───────────────────────────────────────────────
|
|
59
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Set the application base path. Must be the first call.
|
|
68
|
+
* Pass __dirname from bootstrap/app.js — Laravel style.
|
|
69
|
+
*
|
|
70
|
+
* Millas.configure(__dirname)
|
|
71
|
+
*
|
|
72
|
+
* All providers use this to locate config files, models, and routes
|
|
73
|
+
* without relying on process.cwd() at runtime.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} basePath — absolute path to the project root
|
|
76
|
+
*/
|
|
77
|
+
configure(basePath) {
|
|
78
|
+
this._config.basePath = basePath;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
60
82
|
/**
|
|
61
83
|
* Register application service providers.
|
|
62
84
|
*
|
|
@@ -93,8 +115,15 @@ class MillasConfig {
|
|
|
93
115
|
/**
|
|
94
116
|
* Enable the admin panel.
|
|
95
117
|
*
|
|
118
|
+
* AuthServiceProvider and AdminServiceProvider are registered
|
|
119
|
+
* automatically — no need to add them to .providers([]).
|
|
120
|
+
*
|
|
96
121
|
* .withAdmin()
|
|
97
122
|
* .withAdmin({ prefix: '/cms', title: 'My CMS' })
|
|
123
|
+
*
|
|
124
|
+
* First-time setup:
|
|
125
|
+
* millas migrate # creates users + admin_log + sessions tables
|
|
126
|
+
* millas createsuperuser # creates your first admin user
|
|
98
127
|
*/
|
|
99
128
|
withAdmin(options = {}) {
|
|
100
129
|
this._config.admin = options;
|
package/src/core/db.js
CHANGED
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,12 +55,5 @@ module.exports = {
|
|
|
56
55
|
Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
|
|
57
56
|
// Storage
|
|
58
57
|
Storage, LocalDriver, StorageServiceProvider,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// ── Admin ─────────────────────────────────────────────────────────
|
|
62
|
-
const { Admin, AdminResource, AdminField, AdminFilter } = require('../admin');
|
|
63
|
-
const AdminServiceProvider = require('../providers/AdminServiceProvider');
|
|
64
|
-
|
|
65
|
-
Object.assign(module.exports, {
|
|
66
|
-
Admin, AdminResource, AdminField, AdminFilter, AdminServiceProvider,
|
|
67
|
-
});
|
|
58
|
+
Log: require('../logger').Log,
|
|
59
|
+
};
|
package/src/core/lang.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../i18n')
|
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
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createFacade } = require('./Facade');
|
|
4
|
+
const { HashManager, BcryptDriver } = require('../hashing/Hash');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hash facade — Laravel-style hashing.
|
|
8
|
+
*
|
|
9
|
+
* Resolved from the DI container as 'hash'.
|
|
10
|
+
* Falls back to the exported singleton if used before the container boots
|
|
11
|
+
* (e.g. in migrations or standalone scripts).
|
|
12
|
+
*
|
|
13
|
+
* @class
|
|
14
|
+
*
|
|
15
|
+
* ── Core ─────────────────────────────────────────────────────────────────────
|
|
16
|
+
* @property {function(string, object=): Promise<string>} make
|
|
17
|
+
* Hash a plain-text value.
|
|
18
|
+
* Options: { rounds: 14 } — overrides the configured rounds for this call.
|
|
19
|
+
*
|
|
20
|
+
* @property {function(string, string): Promise<boolean>} check
|
|
21
|
+
* Verify a plain-text value against a stored hash.
|
|
22
|
+
* Returns false (never throws) if either argument is falsy.
|
|
23
|
+
*
|
|
24
|
+
* @property {function(string): boolean} needsRehash
|
|
25
|
+
* Returns true if the hash was made with different rounds than currently
|
|
26
|
+
* configured. Use after login to silently upgrade stored hashes:
|
|
27
|
+
* if (Hash.needsRehash(user.password)) {
|
|
28
|
+
* user.password = await Hash.make(plaintext);
|
|
29
|
+
* await user.save();
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* ── Introspection ─────────────────────────────────────────────────────────────
|
|
33
|
+
* @property {function(string): { alg: string, rounds: number }} info
|
|
34
|
+
* Return metadata about a stored hash.
|
|
35
|
+
* Example: Hash.info(hash) → { alg: 'bcrypt', rounds: 12 }
|
|
36
|
+
*
|
|
37
|
+
* @property {function(string): boolean} isHashed
|
|
38
|
+
* Return true if the value looks like a bcrypt hash.
|
|
39
|
+
* Guards against accidentally double-hashing:
|
|
40
|
+
* if (!Hash.isHashed(value)) value = await Hash.make(value);
|
|
41
|
+
*
|
|
42
|
+
* ── Driver access ─────────────────────────────────────────────────────────────
|
|
43
|
+
* @property {function(string=): BcryptDriver} driver
|
|
44
|
+
* Get a specific driver instance:
|
|
45
|
+
* Hash.driver('bcrypt').make('secret', { rounds: 14 });
|
|
46
|
+
*
|
|
47
|
+
* ── Configuration ─────────────────────────────────────────────────────────────
|
|
48
|
+
* @property {function(number): HashManager} setRounds
|
|
49
|
+
* Change the bcrypt rounds at runtime. Busts the driver cache.
|
|
50
|
+
* Useful in tests: Hash.setRounds(4);
|
|
51
|
+
*
|
|
52
|
+
* @property {function(): number} getRounds
|
|
53
|
+
* Return the currently configured bcrypt rounds.
|
|
54
|
+
*
|
|
55
|
+
* ── Testing ───────────────────────────────────────────────────────────────────
|
|
56
|
+
* @property {function(object): void} swap
|
|
57
|
+
* Swap the underlying instance for a fake:
|
|
58
|
+
* Hash.swap({ make: async () => 'hashed', check: async () => true });
|
|
59
|
+
*
|
|
60
|
+
* @property {function(): void} restore
|
|
61
|
+
* Restore the real implementation after a swap.
|
|
62
|
+
*
|
|
63
|
+
* @see src/hashing/Hash.js
|
|
64
|
+
*/
|
|
65
|
+
class Hash extends createFacade('hash') {}
|
|
66
|
+
|
|
67
|
+
module.exports = Hash
|