@realtimex/realtimex-alchemy 1.0.50 → 1.0.52
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/dist/CHANGELOG.md +17 -0
- package/dist/api/index.js +5 -7
- package/dist/api/services/AlchemistService.js +21 -14
- package/dist/api/services/ChatService.js +9 -5
- package/dist/api/services/DeduplicationService.js +5 -3
- package/dist/api/services/EmbeddingService.js +69 -59
- package/dist/api/services/SDKService.js +124 -1
- package/dist/api/services/TransmuteService.js +10 -8
- package/dist/assets/{index-DH6m-zOy.js → index-Be9CS22o.js} +38 -38
- package/dist/index.html +1 -1
- package/package.json +3 -3
- package/supabase/functions/api-v1-settings/index.ts +4 -2
- package/supabase/functions/setup/index.ts +5 -3
- package/supabase/migrations/20260127000002_add_vector_storage.sql +67 -0
- package/supabase/migrations/20260127000003_drop_legacy_llm_fields.sql +19 -0
package/dist/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.52] - 2026-01-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Configuration**: Refactored LLM provider configuration to drop legacy fields and utilize new SDK defaults, simplifying setup.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Embeddings**: Updated `AlchemistService` to allow embedding generation without an explicit `embedding_model` setting, enabling dynamic model resolution via the SDK.
|
|
15
|
+
|
|
16
|
+
## [1.0.51] - 2026-01-27
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Vector Storage**: Migrated embedding storage from local SDK to Supabase `pgvector`. This enables cloud-native similarity search and persistent memory across devices.
|
|
20
|
+
- **Database**: Added `alchemy_vectors` table and HNSW indexes for high-performance semantic retrieval.
|
|
21
|
+
|
|
22
|
+
### Improved
|
|
23
|
+
- **Performance**: Implemented server-side similarity search via `match_vectors` RPC function, reducing latency for RAG and deduplication.
|
|
24
|
+
|
|
8
25
|
## [1.0.50] - 2026-01-26
|
|
9
26
|
|
|
10
27
|
### Improved
|
package/dist/api/index.js
CHANGED
|
@@ -276,11 +276,8 @@ app.post('/api/browser-paths/validate', async (req, res) => {
|
|
|
276
276
|
app.post('/api/test/analyze', async (req, res) => {
|
|
277
277
|
const { text } = req.body;
|
|
278
278
|
try {
|
|
279
|
-
// Fetch settings from database
|
|
280
|
-
let settings = {
|
|
281
|
-
llm_provider: 'realtimexai',
|
|
282
|
-
llm_model: 'gpt-4o'
|
|
283
|
-
};
|
|
279
|
+
// Fetch settings from database (empty object = use dynamic defaults from SDK)
|
|
280
|
+
let settings = {};
|
|
284
281
|
if (SupabaseService.isConfigured()) {
|
|
285
282
|
const supabase = SupabaseService.getServiceRoleClient();
|
|
286
283
|
const { data: userData } = await supabase.rpc('get_any_user_id');
|
|
@@ -306,9 +303,10 @@ app.post('/api/test/analyze', async (req, res) => {
|
|
|
306
303
|
app.post('/api/llm/test', async (req, res) => {
|
|
307
304
|
const { llmProvider, llmModel } = req.body;
|
|
308
305
|
try {
|
|
306
|
+
// Pass provided values (if any) - if not provided, resolveChatProvider will get defaults from SDK
|
|
309
307
|
const settings = {
|
|
310
|
-
llm_provider: llmProvider
|
|
311
|
-
llm_model: llmModel
|
|
308
|
+
llm_provider: llmProvider,
|
|
309
|
+
llm_model: llmModel
|
|
312
310
|
};
|
|
313
311
|
const result = await alchemist.testConnection(settings);
|
|
314
312
|
res.json(result);
|
|
@@ -89,10 +89,9 @@ export class AlchemistService {
|
|
|
89
89
|
skipped: 0,
|
|
90
90
|
errors: 0
|
|
91
91
|
};
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
92
|
+
// Resolve LLM provider dynamically
|
|
93
|
+
const llmConfig = await SDKService.resolveChatProvider(settings);
|
|
94
|
+
console.log('[AlchemistService] LLM Config:', llmConfig);
|
|
96
95
|
for (const entry of allowedEntries) {
|
|
97
96
|
// Emit: Reading
|
|
98
97
|
await this.processingEvents.log({
|
|
@@ -189,7 +188,8 @@ export class AlchemistService {
|
|
|
189
188
|
}, supabase);
|
|
190
189
|
stats.signals++;
|
|
191
190
|
// 5. Generate Embedding & Check for Duplicates (non-blocking)
|
|
192
|
-
|
|
191
|
+
// Note: embedding_model is resolved dynamically via SDK if not in settings
|
|
192
|
+
if (await embeddingService.isAvailable()) {
|
|
193
193
|
await this.processEmbedding(insertedSignal, settings, userId, supabase).catch((err) => {
|
|
194
194
|
console.error('[AlchemistService] Embedding processing failed:', err);
|
|
195
195
|
});
|
|
@@ -261,6 +261,8 @@ export class AlchemistService {
|
|
|
261
261
|
if (!sdk) {
|
|
262
262
|
throw new Error('RealTimeX SDK not available');
|
|
263
263
|
}
|
|
264
|
+
// Resolve LLM provider dynamically from SDK
|
|
265
|
+
const { provider, model } = await SDKService.resolveChatProvider(settings);
|
|
264
266
|
const prompt = `
|
|
265
267
|
Act as "The Alchemist", a high-level intelligence analyst.
|
|
266
268
|
Analyze the following article value based on the content and the User's Interests.
|
|
@@ -309,8 +311,8 @@ export class AlchemistService {
|
|
|
309
311
|
{ role: 'system', content: 'You are a precise analyzer. Return ONLY valid JSON, no other text.' },
|
|
310
312
|
{ role: 'user', content: prompt }
|
|
311
313
|
], {
|
|
312
|
-
provider
|
|
313
|
-
model
|
|
314
|
+
provider,
|
|
315
|
+
model
|
|
314
316
|
});
|
|
315
317
|
// SDK returns response.response?.content based on documentation
|
|
316
318
|
const raw = response.response?.content || '{}';
|
|
@@ -329,16 +331,18 @@ export class AlchemistService {
|
|
|
329
331
|
message: 'RealTimeX SDK not available. Please run via RealTimeX Desktop.'
|
|
330
332
|
};
|
|
331
333
|
}
|
|
334
|
+
// Resolve LLM provider dynamically from SDK
|
|
335
|
+
const { provider, model } = await SDKService.resolveChatProvider(settings);
|
|
332
336
|
const response = await sdk.llm.chat([
|
|
333
337
|
{ role: 'user', content: 'Say "OK"' }
|
|
334
338
|
], {
|
|
335
|
-
provider
|
|
336
|
-
model
|
|
339
|
+
provider,
|
|
340
|
+
model
|
|
337
341
|
});
|
|
338
342
|
return {
|
|
339
343
|
success: true,
|
|
340
344
|
message: `Connection successful!`,
|
|
341
|
-
model
|
|
345
|
+
model
|
|
342
346
|
};
|
|
343
347
|
}
|
|
344
348
|
catch (error) {
|
|
@@ -392,6 +396,8 @@ export class AlchemistService {
|
|
|
392
396
|
async processEmbedding(signal, settings, userId, supabase) {
|
|
393
397
|
try {
|
|
394
398
|
console.log('[AlchemistService] Generating embedding for signal:', signal.id);
|
|
399
|
+
// Resolve embedding provider dynamically
|
|
400
|
+
const { model: embeddingModel } = await SDKService.resolveEmbedProvider(settings);
|
|
395
401
|
// Generate embedding
|
|
396
402
|
const text = `${signal.title} ${signal.summary}`;
|
|
397
403
|
const embedding = await embeddingService.generateEmbedding(text, settings);
|
|
@@ -410,20 +416,21 @@ export class AlchemistService {
|
|
|
410
416
|
.eq('id', signal.id);
|
|
411
417
|
return;
|
|
412
418
|
}
|
|
413
|
-
// Store embedding in
|
|
419
|
+
// Store embedding in Supabase pgvector storage
|
|
414
420
|
await embeddingService.storeSignalEmbedding(signal.id, embedding, {
|
|
415
421
|
title: signal.title,
|
|
416
422
|
summary: signal.summary,
|
|
417
423
|
url: signal.url,
|
|
418
424
|
category: signal.category,
|
|
419
|
-
userId
|
|
420
|
-
|
|
425
|
+
userId,
|
|
426
|
+
model: embeddingModel
|
|
427
|
+
}, supabase);
|
|
421
428
|
// Update signal metadata
|
|
422
429
|
await supabase
|
|
423
430
|
.from('signals')
|
|
424
431
|
.update({
|
|
425
432
|
has_embedding: true,
|
|
426
|
-
embedding_model:
|
|
433
|
+
embedding_model: embeddingModel
|
|
427
434
|
})
|
|
428
435
|
.eq('id', signal.id);
|
|
429
436
|
console.log('[AlchemistService] Embedding processed successfully for signal:', signal.id);
|
|
@@ -64,7 +64,7 @@ export class ChatService {
|
|
|
64
64
|
let sources = [];
|
|
65
65
|
// 3. Retrieve Context (if embedding checks out)
|
|
66
66
|
if (queryEmbedding) {
|
|
67
|
-
const similar = await embeddingService.findSimilarSignals(queryEmbedding, userId, 0.55, // Lowered threshold for better recall
|
|
67
|
+
const similar = await embeddingService.findSimilarSignals(queryEmbedding, userId, supabase, 0.55, // Lowered threshold for better recall
|
|
68
68
|
10 // Increased Top K
|
|
69
69
|
);
|
|
70
70
|
console.log(`[ChatService] RAG Retrieval: Found ${similar.length} signals for query: "${content}"`);
|
|
@@ -106,9 +106,11 @@ Be concise, helpful, and professional.
|
|
|
106
106
|
{ role: 'user', content: finalPrompt } // Current turn with RAG context
|
|
107
107
|
];
|
|
108
108
|
console.log('[ChatService] Final Prompt being sent to LLM:', JSON.stringify(messages, null, 2));
|
|
109
|
+
// Resolve LLM provider dynamically from SDK
|
|
110
|
+
const { provider, model } = await SDKService.resolveChatProvider(settings);
|
|
109
111
|
const response = await sdk.llm.chat(messages, {
|
|
110
|
-
provider
|
|
111
|
-
model
|
|
112
|
+
provider,
|
|
113
|
+
model
|
|
112
114
|
});
|
|
113
115
|
console.log('[ChatService] LLM Response:', JSON.stringify(response, null, 2));
|
|
114
116
|
const aiContent = response.response?.content || "I'm sorry, I couldn't generate a response. The LLM returned empty content.";
|
|
@@ -161,12 +163,14 @@ Be concise, helpful, and professional.
|
|
|
161
163
|
const sdk = SDKService.getSDK();
|
|
162
164
|
if (!sdk)
|
|
163
165
|
return;
|
|
166
|
+
// Resolve LLM provider dynamically from SDK
|
|
167
|
+
const { provider, model } = await SDKService.resolveChatProvider(settings);
|
|
164
168
|
const response = await sdk.llm.chat([
|
|
165
169
|
{ role: 'system', content: 'Generate a very short title (3-5 words) for this chat conversation. Return ONLY the title.' },
|
|
166
170
|
{ role: 'user', content: `User: ${userMsg}\nAI: ${aiMsg}` }
|
|
167
171
|
], {
|
|
168
|
-
provider
|
|
169
|
-
model
|
|
172
|
+
provider,
|
|
173
|
+
model
|
|
170
174
|
});
|
|
171
175
|
const newTitle = response.response?.content?.replace(/['"]/g, '').trim();
|
|
172
176
|
if (newTitle) {
|
|
@@ -18,7 +18,7 @@ export class DeduplicationService {
|
|
|
18
18
|
try {
|
|
19
19
|
// 1. Semantic Check (if embedding exists)
|
|
20
20
|
if (embedding && embedding.length > 0) {
|
|
21
|
-
const similar = await embeddingService.findSimilarSignals(embedding, userId, this.SIMILARITY_THRESHOLD, 5 // Check top 5 matches
|
|
21
|
+
const similar = await embeddingService.findSimilarSignals(embedding, userId, supabase, this.SIMILARITY_THRESHOLD, 5 // Check top 5 matches
|
|
22
22
|
);
|
|
23
23
|
if (similar.length > 0) {
|
|
24
24
|
const bestMatch = similar[0];
|
|
@@ -154,6 +154,8 @@ export class DeduplicationService {
|
|
|
154
154
|
// Fallback: use longer summary
|
|
155
155
|
return existing.length >= newSummary.length ? existing : newSummary;
|
|
156
156
|
}
|
|
157
|
+
// Resolve LLM provider dynamically from SDK
|
|
158
|
+
const { provider, model } = await SDKService.resolveChatProvider(settings);
|
|
157
159
|
// Use LLM to intelligently merge summaries
|
|
158
160
|
const response = await sdk.llm.chat([
|
|
159
161
|
{
|
|
@@ -165,8 +167,8 @@ export class DeduplicationService {
|
|
|
165
167
|
content: `Summary 1: ${existing}\nSummary 2: ${newSummary}\n\nMerged summary:`
|
|
166
168
|
}
|
|
167
169
|
], {
|
|
168
|
-
provider
|
|
169
|
-
model
|
|
170
|
+
provider,
|
|
171
|
+
model
|
|
170
172
|
});
|
|
171
173
|
const merged = response.response?.content?.trim();
|
|
172
174
|
return merged || existing;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { SDKService } from './SDKService.js';
|
|
2
2
|
/**
|
|
3
|
-
* Embedding Service
|
|
4
|
-
*
|
|
3
|
+
* Embedding Service
|
|
4
|
+
* Uses RealTimeX SDK for embedding generation
|
|
5
|
+
* Uses Supabase pgvector for vector storage and similarity search
|
|
5
6
|
* Gracefully degrades if SDK is not available
|
|
6
7
|
*/
|
|
7
8
|
export class EmbeddingService {
|
|
8
|
-
WORKSPACE_ID = 'alchemy-signals';
|
|
9
9
|
SIMILARITY_THRESHOLD = 0.85;
|
|
10
10
|
/**
|
|
11
11
|
* Generate embedding for a single text
|
|
@@ -20,8 +20,8 @@ export class EmbeddingService {
|
|
|
20
20
|
console.warn('[EmbeddingService] RealTimeX SDK not available');
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
const model = settings
|
|
23
|
+
// Resolve embedding provider dynamically from SDK
|
|
24
|
+
const { provider, model } = await SDKService.resolveEmbedProvider(settings);
|
|
25
25
|
const response = await sdk.llm.embed(text, {
|
|
26
26
|
provider,
|
|
27
27
|
model
|
|
@@ -46,8 +46,8 @@ export class EmbeddingService {
|
|
|
46
46
|
console.warn('[EmbeddingService] RealTimeX SDK not available');
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
const model = settings
|
|
49
|
+
// Resolve embedding provider dynamically from SDK
|
|
50
|
+
const { provider, model } = await SDKService.resolveEmbedProvider(settings);
|
|
51
51
|
const response = await sdk.llm.embed(texts, {
|
|
52
52
|
provider,
|
|
53
53
|
model
|
|
@@ -60,22 +60,40 @@ export class EmbeddingService {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Store signal embedding in
|
|
63
|
+
* Store signal embedding in Supabase pgvector storage
|
|
64
64
|
* @param signalId - Unique signal ID
|
|
65
65
|
* @param embedding - Embedding vector
|
|
66
66
|
* @param metadata - Signal metadata
|
|
67
|
+
* @param supabase - Supabase client
|
|
67
68
|
*/
|
|
68
|
-
async storeSignalEmbedding(signalId, embedding, metadata) {
|
|
69
|
+
async storeSignalEmbedding(signalId, embedding, metadata, supabase) {
|
|
69
70
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// Format embedding as pgvector string
|
|
72
|
+
const embeddingStr = `[${embedding.join(',')}]`;
|
|
73
|
+
// Use model from metadata if provided, otherwise try to get default
|
|
74
|
+
let modelName = metadata.model || 'unknown';
|
|
75
|
+
if (!metadata.model) {
|
|
76
|
+
try {
|
|
77
|
+
const { model } = await SDKService.resolveEmbedProvider({});
|
|
78
|
+
modelName = model;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
// Keep 'unknown' if we can't resolve
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const { error } = await supabase
|
|
85
|
+
.from('alchemy_vectors')
|
|
86
|
+
.upsert({
|
|
87
|
+
signal_id: signalId,
|
|
88
|
+
user_id: metadata.userId,
|
|
89
|
+
embedding: embeddingStr,
|
|
90
|
+
model: modelName
|
|
91
|
+
}, {
|
|
92
|
+
onConflict: 'signal_id,model'
|
|
93
|
+
});
|
|
94
|
+
if (error) {
|
|
95
|
+
throw error;
|
|
73
96
|
}
|
|
74
|
-
await sdk.llm.vectors.upsert([{
|
|
75
|
-
id: signalId,
|
|
76
|
-
vector: embedding,
|
|
77
|
-
metadata
|
|
78
|
-
}], { workspaceId: this.WORKSPACE_ID });
|
|
79
97
|
console.log('[EmbeddingService] Stored embedding for signal:', signalId);
|
|
80
98
|
}
|
|
81
99
|
catch (error) {
|
|
@@ -84,32 +102,39 @@ export class EmbeddingService {
|
|
|
84
102
|
}
|
|
85
103
|
}
|
|
86
104
|
/**
|
|
87
|
-
* Find similar signals using semantic search
|
|
105
|
+
* Find similar signals using semantic search via Supabase pgvector
|
|
88
106
|
* @param queryEmbedding - Query embedding vector
|
|
89
107
|
* @param userId - User ID for filtering
|
|
108
|
+
* @param supabase - Supabase client
|
|
90
109
|
* @param threshold - Similarity threshold (0-1)
|
|
91
110
|
* @param limit - Max results
|
|
92
111
|
* @returns Array of similar signals
|
|
93
112
|
*/
|
|
94
|
-
async findSimilarSignals(queryEmbedding, userId, threshold = this.SIMILARITY_THRESHOLD, limit = 10) {
|
|
113
|
+
async findSimilarSignals(queryEmbedding, userId, supabase, threshold = this.SIMILARITY_THRESHOLD, limit = 10) {
|
|
95
114
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
// Format embedding as pgvector string
|
|
116
|
+
const embeddingStr = `[${queryEmbedding.join(',')}]`;
|
|
117
|
+
const { data, error } = await supabase.rpc('match_vectors', {
|
|
118
|
+
query_embedding: embeddingStr,
|
|
119
|
+
match_threshold: threshold,
|
|
120
|
+
match_count: limit,
|
|
121
|
+
target_user_id: userId
|
|
122
|
+
});
|
|
123
|
+
if (error) {
|
|
124
|
+
console.error('[EmbeddingService] Similarity search RPC error:', error.message);
|
|
98
125
|
return [];
|
|
99
126
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
score: r.score,
|
|
112
|
-
metadata: r.metadata || {}
|
|
127
|
+
// Map results to expected format
|
|
128
|
+
return (data || []).map((r) => ({
|
|
129
|
+
id: r.signal_id,
|
|
130
|
+
score: r.similarity,
|
|
131
|
+
metadata: {
|
|
132
|
+
title: r.title,
|
|
133
|
+
summary: r.summary,
|
|
134
|
+
url: r.url,
|
|
135
|
+
category: r.category,
|
|
136
|
+
userId
|
|
137
|
+
}
|
|
113
138
|
}));
|
|
114
139
|
}
|
|
115
140
|
catch (error) {
|
|
@@ -120,39 +145,24 @@ export class EmbeddingService {
|
|
|
120
145
|
/**
|
|
121
146
|
* Delete all embeddings for a user
|
|
122
147
|
* @param userId - User ID
|
|
148
|
+
* @param supabase - Supabase client
|
|
123
149
|
*/
|
|
124
|
-
async deleteUserEmbeddings(userId) {
|
|
150
|
+
async deleteUserEmbeddings(userId, supabase) {
|
|
125
151
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
const { error } = await supabase
|
|
153
|
+
.from('alchemy_vectors')
|
|
154
|
+
.delete()
|
|
155
|
+
.eq('user_id', userId);
|
|
156
|
+
if (error) {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
console.log('[EmbeddingService] Deleted all embeddings for user:', userId);
|
|
129
160
|
}
|
|
130
161
|
catch (error) {
|
|
131
162
|
console.error('[EmbeddingService] Deletion failed:', error.message);
|
|
132
163
|
throw error;
|
|
133
164
|
}
|
|
134
165
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Determine provider from settings
|
|
137
|
-
* @param settings - Alchemy settings
|
|
138
|
-
* @returns Provider name
|
|
139
|
-
*/
|
|
140
|
-
getProvider(settings) {
|
|
141
|
-
// Use embedding_provider if set
|
|
142
|
-
if (settings.embedding_provider) {
|
|
143
|
-
return settings.embedding_provider;
|
|
144
|
-
}
|
|
145
|
-
// Fallback: detect from base URL (legacy)
|
|
146
|
-
if (settings.embedding_base_url) {
|
|
147
|
-
const url = settings.embedding_base_url.toLowerCase();
|
|
148
|
-
if (url.includes('openai'))
|
|
149
|
-
return 'openai';
|
|
150
|
-
if (url.includes('google') || url.includes('gemini'))
|
|
151
|
-
return 'gemini';
|
|
152
|
-
}
|
|
153
|
-
// Default to realtimexai
|
|
154
|
-
return 'realtimexai';
|
|
155
|
-
}
|
|
156
166
|
/**
|
|
157
167
|
* Check if embedding service is available
|
|
158
168
|
* @returns True if SDK is configured and available
|
|
@@ -107,6 +107,130 @@ export class SDKService {
|
|
|
107
107
|
this.instance = null;
|
|
108
108
|
this.initAttempted = false;
|
|
109
109
|
}
|
|
110
|
+
// Cache for default providers (avoid repeated SDK calls)
|
|
111
|
+
static defaultChatProvider = null;
|
|
112
|
+
static defaultEmbedProvider = null;
|
|
113
|
+
/**
|
|
114
|
+
* Get default chat provider/model from SDK dynamically
|
|
115
|
+
* Caches result to avoid repeated SDK calls
|
|
116
|
+
*/
|
|
117
|
+
static async getDefaultChatProvider() {
|
|
118
|
+
// Return cached if available
|
|
119
|
+
if (this.defaultChatProvider) {
|
|
120
|
+
return this.defaultChatProvider;
|
|
121
|
+
}
|
|
122
|
+
const sdk = this.getSDK();
|
|
123
|
+
if (!sdk) {
|
|
124
|
+
throw new Error('RealTimeX SDK not available. Cannot determine default LLM provider.');
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const { providers } = await this.withTimeout(sdk.llm.chatProviders(), 30000, 'Chat providers fetch timed out');
|
|
128
|
+
if (!providers || providers.length === 0) {
|
|
129
|
+
throw new Error('No LLM providers available. Please configure a provider in RealTimeX Desktop.');
|
|
130
|
+
}
|
|
131
|
+
// Find first provider with available models
|
|
132
|
+
for (const p of providers) {
|
|
133
|
+
if (p.models && p.models.length > 0) {
|
|
134
|
+
this.defaultChatProvider = {
|
|
135
|
+
provider: p.provider,
|
|
136
|
+
model: p.models[0].id
|
|
137
|
+
};
|
|
138
|
+
console.log(`[SDKService] Default chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
|
|
139
|
+
return this.defaultChatProvider;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw new Error('No LLM models available. Please configure a model in RealTimeX Desktop.');
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error('[SDKService] Failed to get default chat provider:', error.message);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get default embedding provider/model from SDK dynamically
|
|
151
|
+
* Caches result to avoid repeated SDK calls
|
|
152
|
+
*/
|
|
153
|
+
static async getDefaultEmbedProvider() {
|
|
154
|
+
// Return cached if available
|
|
155
|
+
if (this.defaultEmbedProvider) {
|
|
156
|
+
return this.defaultEmbedProvider;
|
|
157
|
+
}
|
|
158
|
+
const sdk = this.getSDK();
|
|
159
|
+
if (!sdk) {
|
|
160
|
+
throw new Error('RealTimeX SDK not available. Cannot determine default embedding provider.');
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const { providers } = await this.withTimeout(sdk.llm.embedProviders(), 30000, 'Embed providers fetch timed out');
|
|
164
|
+
if (!providers || providers.length === 0) {
|
|
165
|
+
throw new Error('No embedding providers available. Please configure a provider in RealTimeX Desktop.');
|
|
166
|
+
}
|
|
167
|
+
// Find first provider with available models
|
|
168
|
+
for (const p of providers) {
|
|
169
|
+
if (p.models && p.models.length > 0) {
|
|
170
|
+
this.defaultEmbedProvider = {
|
|
171
|
+
provider: p.provider,
|
|
172
|
+
model: p.models[0].id
|
|
173
|
+
};
|
|
174
|
+
console.log(`[SDKService] Default embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
|
|
175
|
+
return this.defaultEmbedProvider;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new Error('No embedding models available. Please configure a model in RealTimeX Desktop.');
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error('[SDKService] Failed to get default embed provider:', error.message);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Default provider/model configuration
|
|
186
|
+
// realtimexai routes through RealTimeX Desktop to user's configured providers
|
|
187
|
+
static DEFAULT_LLM_PROVIDER = 'realtimexai';
|
|
188
|
+
static DEFAULT_LLM_MODEL = 'gpt-4.1-mini';
|
|
189
|
+
static DEFAULT_EMBED_PROVIDER = 'realtimexai';
|
|
190
|
+
static DEFAULT_EMBED_MODEL = 'text-embedding-3-small';
|
|
191
|
+
/**
|
|
192
|
+
* Resolve LLM provider/model - use settings if available, otherwise use defaults
|
|
193
|
+
*/
|
|
194
|
+
static async resolveChatProvider(settings) {
|
|
195
|
+
// If both provider and model are set in settings, use them
|
|
196
|
+
if (settings.llm_provider && settings.llm_model) {
|
|
197
|
+
return { provider: settings.llm_provider, model: settings.llm_model };
|
|
198
|
+
}
|
|
199
|
+
// Try to get from SDK discovery first
|
|
200
|
+
try {
|
|
201
|
+
return await this.getDefaultChatProvider();
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
// Fallback to hardcoded defaults if SDK discovery fails
|
|
205
|
+
console.log(`[SDKService] Using default LLM: ${this.DEFAULT_LLM_PROVIDER}/${this.DEFAULT_LLM_MODEL}`);
|
|
206
|
+
return { provider: this.DEFAULT_LLM_PROVIDER, model: this.DEFAULT_LLM_MODEL };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve embedding provider/model - use settings if available, otherwise use defaults
|
|
211
|
+
*/
|
|
212
|
+
static async resolveEmbedProvider(settings) {
|
|
213
|
+
// If both provider and model are set in settings, use them
|
|
214
|
+
if (settings.embedding_provider && settings.embedding_model) {
|
|
215
|
+
return { provider: settings.embedding_provider, model: settings.embedding_model };
|
|
216
|
+
}
|
|
217
|
+
// Try to get from SDK discovery first
|
|
218
|
+
try {
|
|
219
|
+
return await this.getDefaultEmbedProvider();
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
// Fallback to hardcoded defaults if SDK discovery fails
|
|
223
|
+
console.log(`[SDKService] Using default embedding: ${this.DEFAULT_EMBED_PROVIDER}/${this.DEFAULT_EMBED_MODEL}`);
|
|
224
|
+
return { provider: this.DEFAULT_EMBED_PROVIDER, model: this.DEFAULT_EMBED_MODEL };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Clear provider cache (useful when providers change)
|
|
229
|
+
*/
|
|
230
|
+
static clearProviderCache() {
|
|
231
|
+
this.defaultChatProvider = null;
|
|
232
|
+
this.defaultEmbedProvider = null;
|
|
233
|
+
}
|
|
110
234
|
/**
|
|
111
235
|
* Helper to wrap a promise with a timeout
|
|
112
236
|
*/
|
|
@@ -147,7 +271,6 @@ export class SDKService {
|
|
|
147
271
|
throw new Error('RealTimeX SDK not linked. Cannot get app data dir.');
|
|
148
272
|
}
|
|
149
273
|
try {
|
|
150
|
-
// @ts-ignore - SDK method for getting app data directory
|
|
151
274
|
return await sdk.getAppDataDir();
|
|
152
275
|
}
|
|
153
276
|
catch (error) {
|
|
@@ -225,9 +225,11 @@ export class TransmuteService {
|
|
|
225
225
|
if (!sdk) {
|
|
226
226
|
throw new Error('RealTimeX SDK not available. Please ensure the desktop app is running.');
|
|
227
227
|
}
|
|
228
|
-
//
|
|
229
|
-
const provider =
|
|
230
|
-
|
|
228
|
+
// Resolve LLM provider dynamically from SDK using engine config
|
|
229
|
+
const { provider, model } = await SDKService.resolveChatProvider({
|
|
230
|
+
llm_provider: engine.config.llm_provider,
|
|
231
|
+
llm_model: engine.config.llm_model
|
|
232
|
+
});
|
|
231
233
|
const response = await sdk.llm.chat([
|
|
232
234
|
{ role: 'system', content: systemPrompt },
|
|
233
235
|
{ role: 'user', content: userPrompt }
|
|
@@ -273,12 +275,12 @@ export class TransmuteService {
|
|
|
273
275
|
continue;
|
|
274
276
|
}
|
|
275
277
|
console.log(`[Transmute] Bootstrapping missing category engine: ${title}`);
|
|
278
|
+
// Note: llm_provider and llm_model are intentionally omitted
|
|
279
|
+
// They will be resolved dynamically at runtime via SDKService.resolveChatProvider()
|
|
276
280
|
const config = {
|
|
277
281
|
category,
|
|
278
282
|
execution_mode: 'desktop',
|
|
279
283
|
schedule: 'Daily',
|
|
280
|
-
llm_provider: 'realtimexai',
|
|
281
|
-
llm_model: 'gpt-4o',
|
|
282
284
|
max_signals: 30,
|
|
283
285
|
custom_prompt: `Create a comprehensive daily newsletter focused on ${category}. Highlight the most important developments, key insights, and actionable takeaways. Use a professional, insight-driven tone with clear structure: start with 'The Big Story' followed by 'Quick Hits' for other notable items.`
|
|
284
286
|
};
|
|
@@ -354,12 +356,12 @@ export class TransmuteService {
|
|
|
354
356
|
continue;
|
|
355
357
|
}
|
|
356
358
|
console.log(`[Transmute] Creating dynamic tag engine for "${tag}" (${count} signals)`);
|
|
359
|
+
// Note: llm_provider and llm_model are intentionally omitted
|
|
360
|
+
// They will be resolved dynamically at runtime via SDKService.resolveChatProvider()
|
|
357
361
|
const config = {
|
|
358
362
|
tag,
|
|
359
363
|
execution_mode: 'desktop',
|
|
360
|
-
schedule: 'Daily'
|
|
361
|
-
llm_provider: 'realtimexai',
|
|
362
|
-
llm_model: 'gpt-4o'
|
|
364
|
+
schedule: 'Daily'
|
|
363
365
|
};
|
|
364
366
|
const { error } = await supabase
|
|
365
367
|
.from('engines')
|