@realtimex/folio 0.1.2
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/.env.example +20 -0
- package/README.md +63 -0
- package/api/server.ts +130 -0
- package/api/src/config/index.ts +96 -0
- package/api/src/middleware/auth.ts +128 -0
- package/api/src/middleware/errorHandler.ts +88 -0
- package/api/src/middleware/index.ts +4 -0
- package/api/src/middleware/rateLimit.ts +71 -0
- package/api/src/middleware/validation.ts +58 -0
- package/api/src/routes/accounts.ts +142 -0
- package/api/src/routes/baseline-config.ts +124 -0
- package/api/src/routes/chat.ts +154 -0
- package/api/src/routes/health.ts +61 -0
- package/api/src/routes/index.ts +35 -0
- package/api/src/routes/ingestions.ts +275 -0
- package/api/src/routes/migrate.ts +112 -0
- package/api/src/routes/policies.ts +121 -0
- package/api/src/routes/processing.ts +90 -0
- package/api/src/routes/rules.ts +11 -0
- package/api/src/routes/sdk.ts +100 -0
- package/api/src/routes/settings.ts +80 -0
- package/api/src/routes/setup.ts +389 -0
- package/api/src/routes/stats.ts +81 -0
- package/api/src/routes/tts.ts +190 -0
- package/api/src/services/BaselineConfigService.ts +208 -0
- package/api/src/services/ChatService.ts +204 -0
- package/api/src/services/GoogleDriveService.ts +331 -0
- package/api/src/services/GoogleSheetsService.ts +1107 -0
- package/api/src/services/IngestionService.ts +1187 -0
- package/api/src/services/ModelCapabilityService.ts +248 -0
- package/api/src/services/PolicyEngine.ts +1625 -0
- package/api/src/services/PolicyLearningService.ts +527 -0
- package/api/src/services/PolicyLoader.ts +249 -0
- package/api/src/services/RAGService.ts +391 -0
- package/api/src/services/SDKService.ts +249 -0
- package/api/src/services/supabase.ts +113 -0
- package/api/src/utils/Actuator.ts +284 -0
- package/api/src/utils/actions/ActionHandler.ts +34 -0
- package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
- package/api/src/utils/actions/AutoRenameAction.ts +58 -0
- package/api/src/utils/actions/CopyAction.ts +120 -0
- package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
- package/api/src/utils/actions/LogCsvAction.ts +48 -0
- package/api/src/utils/actions/NotifyAction.ts +39 -0
- package/api/src/utils/actions/RenameAction.ts +57 -0
- package/api/src/utils/actions/WebhookAction.ts +58 -0
- package/api/src/utils/actions/utils.ts +293 -0
- package/api/src/utils/llmResponse.ts +61 -0
- package/api/src/utils/logger.ts +67 -0
- package/bin/folio-deploy.js +12 -0
- package/bin/folio-setup.js +45 -0
- package/bin/folio.js +65 -0
- package/dist/api/server.js +106 -0
- package/dist/api/src/config/index.js +81 -0
- package/dist/api/src/middleware/auth.js +93 -0
- package/dist/api/src/middleware/errorHandler.js +73 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +43 -0
- package/dist/api/src/middleware/validation.js +54 -0
- package/dist/api/src/routes/accounts.js +110 -0
- package/dist/api/src/routes/baseline-config.js +91 -0
- package/dist/api/src/routes/chat.js +114 -0
- package/dist/api/src/routes/health.js +52 -0
- package/dist/api/src/routes/index.js +31 -0
- package/dist/api/src/routes/ingestions.js +207 -0
- package/dist/api/src/routes/migrate.js +91 -0
- package/dist/api/src/routes/policies.js +86 -0
- package/dist/api/src/routes/processing.js +75 -0
- package/dist/api/src/routes/rules.js +8 -0
- package/dist/api/src/routes/sdk.js +80 -0
- package/dist/api/src/routes/settings.js +68 -0
- package/dist/api/src/routes/setup.js +315 -0
- package/dist/api/src/routes/stats.js +62 -0
- package/dist/api/src/routes/tts.js +178 -0
- package/dist/api/src/services/BaselineConfigService.js +168 -0
- package/dist/api/src/services/ChatService.js +166 -0
- package/dist/api/src/services/GoogleDriveService.js +280 -0
- package/dist/api/src/services/GoogleSheetsService.js +795 -0
- package/dist/api/src/services/IngestionService.js +990 -0
- package/dist/api/src/services/ModelCapabilityService.js +179 -0
- package/dist/api/src/services/PolicyEngine.js +1353 -0
- package/dist/api/src/services/PolicyLearningService.js +397 -0
- package/dist/api/src/services/PolicyLoader.js +159 -0
- package/dist/api/src/services/RAGService.js +295 -0
- package/dist/api/src/services/SDKService.js +212 -0
- package/dist/api/src/services/supabase.js +72 -0
- package/dist/api/src/utils/Actuator.js +225 -0
- package/dist/api/src/utils/actions/ActionHandler.js +1 -0
- package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
- package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
- package/dist/api/src/utils/actions/CopyAction.js +112 -0
- package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
- package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
- package/dist/api/src/utils/actions/NotifyAction.js +32 -0
- package/dist/api/src/utils/actions/RenameAction.js +51 -0
- package/dist/api/src/utils/actions/WebhookAction.js +51 -0
- package/dist/api/src/utils/actions/utils.js +237 -0
- package/dist/api/src/utils/llmResponse.js +63 -0
- package/dist/api/src/utils/logger.js +51 -0
- package/dist/assets/index-DzN8-j-e.css +1 -0
- package/dist/assets/index-Uy-ai3Dh.js +113 -0
- package/dist/favicon.svg +31 -0
- package/dist/folio-logo.svg +46 -0
- package/dist/index.html +14 -0
- package/docs-dev/FPE-spec.md +196 -0
- package/docs-dev/folio-prd.md +47 -0
- package/docs-dev/foundation-checklist.md +30 -0
- package/docs-dev/hybrid-routing-architecture.md +205 -0
- package/docs-dev/ingestion-engine.md +69 -0
- package/docs-dev/port-from-email-automator.md +32 -0
- package/docs-dev/tech-spec.md +98 -0
- package/index.html +13 -0
- package/package.json +101 -0
- package/public/favicon.svg +31 -0
- package/public/folio-logo.svg +46 -0
- package/scripts/dev-task.mjs +51 -0
- package/scripts/get-latest-migration-timestamp.mjs +34 -0
- package/scripts/migrate.sh +91 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/config.toml +64 -0
- package/supabase/functions/_shared/auth.ts +35 -0
- package/supabase/functions/_shared/cors.ts +12 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
- package/supabase/functions/api-v1-settings/index.ts +66 -0
- package/supabase/functions/setup/index.ts +91 -0
- package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
- package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
- package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
- package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
- package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
- package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
- package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
- package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
- package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
- package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
- package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
- package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
- package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
- package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
- package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
- package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
- package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
- package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
- package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
- package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
- package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
- package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
- package/supabase/migrations/29991231235959_test_migration.sql +0 -0
- package/supabase/templates/confirmation.html +76 -0
- package/supabase/templates/email-change.html +76 -0
- package/supabase/templates/invite.html +72 -0
- package/supabase/templates/magic-link.html +68 -0
- package/supabase/templates/recovery.html +82 -0
- package/tsconfig.api.json +16 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +146 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { SDKService } from "./SDKService.js";
|
|
3
|
+
import { createLogger } from "../utils/logger.js";
|
|
4
|
+
const logger = createLogger("RAGService");
|
|
5
|
+
export class RAGService {
|
|
6
|
+
static MAX_CONCURRENT_EMBED_JOBS = (() => {
|
|
7
|
+
const parsed = Number.parseInt(process.env.RAG_MAX_CONCURRENT_EMBED_JOBS ?? "2", 10);
|
|
8
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;
|
|
9
|
+
})();
|
|
10
|
+
static activeEmbedJobs = 0;
|
|
11
|
+
static embedJobWaiters = [];
|
|
12
|
+
static async acquireEmbedJobSlot() {
|
|
13
|
+
while (this.activeEmbedJobs >= this.MAX_CONCURRENT_EMBED_JOBS) {
|
|
14
|
+
await new Promise((resolve) => this.embedJobWaiters.push(resolve));
|
|
15
|
+
}
|
|
16
|
+
this.activeEmbedJobs += 1;
|
|
17
|
+
}
|
|
18
|
+
static releaseEmbedJobSlot() {
|
|
19
|
+
this.activeEmbedJobs = Math.max(0, this.activeEmbedJobs - 1);
|
|
20
|
+
const waiter = this.embedJobWaiters.shift();
|
|
21
|
+
if (waiter)
|
|
22
|
+
waiter();
|
|
23
|
+
}
|
|
24
|
+
static async resolveEmbeddingModel(settings = {}) {
|
|
25
|
+
const { provider, model } = await SDKService.resolveEmbedProvider(settings);
|
|
26
|
+
return { provider, model };
|
|
27
|
+
}
|
|
28
|
+
static async embedTextWithResolvedModel(text, resolvedModel) {
|
|
29
|
+
const sdk = SDKService.getSDK();
|
|
30
|
+
if (!sdk) {
|
|
31
|
+
throw new Error("RealTimeX SDK not available for embedding");
|
|
32
|
+
}
|
|
33
|
+
const response = await sdk.llm.embed(text, {
|
|
34
|
+
provider: resolvedModel.provider,
|
|
35
|
+
model: resolvedModel.model
|
|
36
|
+
});
|
|
37
|
+
const embedding = response.embeddings?.[0];
|
|
38
|
+
if (!embedding) {
|
|
39
|
+
throw new Error("No embedding returned from SDK");
|
|
40
|
+
}
|
|
41
|
+
return embedding;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Splits a large text into smaller semantic chunks (roughly by paragraphs).
|
|
45
|
+
*/
|
|
46
|
+
static chunkText(text, maxChunkLength = 1000) {
|
|
47
|
+
if (!text || text.trim().length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
const paragraphs = text.split(/\n\s*\n/);
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let currentChunk = "";
|
|
52
|
+
for (const paragraph of paragraphs) {
|
|
53
|
+
const p = paragraph.trim();
|
|
54
|
+
if (!p)
|
|
55
|
+
continue;
|
|
56
|
+
if (p.length > maxChunkLength) {
|
|
57
|
+
// Flush pending chunk before splitting an oversized paragraph
|
|
58
|
+
if (currentChunk.length > 0) {
|
|
59
|
+
chunks.push(currentChunk.trim());
|
|
60
|
+
currentChunk = "";
|
|
61
|
+
}
|
|
62
|
+
// Hard split oversized paragraphs to prevent SDK payload rejection
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < p.length) {
|
|
65
|
+
chunks.push(p.slice(i, i + maxChunkLength));
|
|
66
|
+
i += maxChunkLength;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (currentChunk.length + p.length > maxChunkLength && currentChunk.length > 0) {
|
|
71
|
+
chunks.push(currentChunk.trim());
|
|
72
|
+
currentChunk = "";
|
|
73
|
+
}
|
|
74
|
+
currentChunk += (currentChunk.length > 0 ? "\n\n" : "") + p;
|
|
75
|
+
}
|
|
76
|
+
if (currentChunk.trim().length > 0) {
|
|
77
|
+
chunks.push(currentChunk.trim());
|
|
78
|
+
}
|
|
79
|
+
return chunks;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Generate an embedding for a text string using RealTimeX SDK
|
|
83
|
+
*/
|
|
84
|
+
static async embedText(text, settings) {
|
|
85
|
+
const resolvedModel = await this.resolveEmbeddingModel(settings || {});
|
|
86
|
+
logger.debug(`Generating embedding using ${resolvedModel.provider}/${resolvedModel.model}`);
|
|
87
|
+
return this.embedTextWithResolvedModel(text, resolvedModel);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Process an ingested document's raw text: chunk it, embed it, and store in DB.
|
|
91
|
+
*/
|
|
92
|
+
static async chunkAndEmbed(ingestionId, userId, rawText, supabase, settings) {
|
|
93
|
+
if (rawText.startsWith("[VLM_IMAGE_DATA:")) {
|
|
94
|
+
logger.info(`Skipping chunking and embedding for VLM base64 image data (Ingestion: ${ingestionId})`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const chunks = this.chunkText(rawText);
|
|
98
|
+
if (chunks.length === 0) {
|
|
99
|
+
logger.info(`No text to chunk for ingestion ${ingestionId}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const resolvedModel = await this.resolveEmbeddingModel(settings || {});
|
|
103
|
+
logger.info(`Extracted ${chunks.length} chunks for ingestion ${ingestionId}. Embedding with ${resolvedModel.provider}/${resolvedModel.model}...`);
|
|
104
|
+
// Global gate: background fire-and-forget jobs are bounded process-wide.
|
|
105
|
+
await this.acquireEmbedJobSlot();
|
|
106
|
+
try {
|
|
107
|
+
// To avoid provider throttling, process chunks sequentially inside each job.
|
|
108
|
+
for (const [index, content] of chunks.entries()) {
|
|
109
|
+
try {
|
|
110
|
+
// Content hash is model-agnostic. Model identity is tracked in dedicated columns.
|
|
111
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
112
|
+
// Check if this chunk already exists for this ingestion and this model.
|
|
113
|
+
const { data: existing } = await supabase
|
|
114
|
+
.from("document_chunks")
|
|
115
|
+
.select("id")
|
|
116
|
+
.eq("ingestion_id", ingestionId)
|
|
117
|
+
.eq("content_hash", hash)
|
|
118
|
+
.eq("embedding_provider", resolvedModel.provider)
|
|
119
|
+
.eq("embedding_model", resolvedModel.model)
|
|
120
|
+
.maybeSingle();
|
|
121
|
+
if (existing) {
|
|
122
|
+
continue; // Skip duplicate chunk
|
|
123
|
+
}
|
|
124
|
+
// Spread requests slightly to reduce burstiness against embedding APIs.
|
|
125
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
126
|
+
const embedding = await this.embedTextWithResolvedModel(content, resolvedModel);
|
|
127
|
+
const vector_dim = embedding.length;
|
|
128
|
+
const { error } = await supabase.from("document_chunks").insert({
|
|
129
|
+
user_id: userId,
|
|
130
|
+
ingestion_id: ingestionId,
|
|
131
|
+
content,
|
|
132
|
+
content_hash: hash,
|
|
133
|
+
embedding_provider: resolvedModel.provider,
|
|
134
|
+
embedding_model: resolvedModel.model,
|
|
135
|
+
embedding,
|
|
136
|
+
vector_dim
|
|
137
|
+
});
|
|
138
|
+
if (error) {
|
|
139
|
+
logger.error(`Failed to insert chunk ${index + 1}/${chunks.length} for ${ingestionId}`, { error });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
logger.error(`Failed to process chunk ${index + 1}/${chunks.length} for ${ingestionId}`, {
|
|
144
|
+
error: err instanceof Error ? err.message : String(err)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
this.releaseEmbedJobSlot();
|
|
151
|
+
}
|
|
152
|
+
logger.info(`Successfully stored semantic chunks for ingestion ${ingestionId}`);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Semantically search the document chunks using dynamic pgvector partial indexing.
|
|
156
|
+
*/
|
|
157
|
+
static async runSearchForModel(args) {
|
|
158
|
+
const { userId, supabase, modelScope, queryEmbedding, queryDim, similarityThreshold, topK } = args;
|
|
159
|
+
const { data, error } = await supabase.rpc("search_documents", {
|
|
160
|
+
p_user_id: userId,
|
|
161
|
+
p_embedding_provider: modelScope.provider,
|
|
162
|
+
p_embedding_model: modelScope.model,
|
|
163
|
+
query_embedding: queryEmbedding,
|
|
164
|
+
match_threshold: similarityThreshold,
|
|
165
|
+
match_count: topK,
|
|
166
|
+
query_dim: queryDim
|
|
167
|
+
});
|
|
168
|
+
if (error) {
|
|
169
|
+
throw new Error(`Knowledge base search failed for ${modelScope.provider}/${modelScope.model}: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
return (data || []);
|
|
172
|
+
}
|
|
173
|
+
static async listUserModelScopes(userId, supabase) {
|
|
174
|
+
const { data, error } = await supabase
|
|
175
|
+
.from("document_chunks")
|
|
176
|
+
.select("embedding_provider, embedding_model, vector_dim, created_at")
|
|
177
|
+
.eq("user_id", userId)
|
|
178
|
+
.order("created_at", { ascending: false })
|
|
179
|
+
.limit(2000);
|
|
180
|
+
if (error) {
|
|
181
|
+
logger.warn("Failed to list user embedding scopes for RAG fallback", { error });
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const scopes = new Map();
|
|
185
|
+
for (const row of data || []) {
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
+
const provider = String(row.embedding_provider || "").trim();
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
+
const model = String(row.embedding_model || "").trim();
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
+
const vector_dim = Number(row.vector_dim);
|
|
192
|
+
if (!provider || !model)
|
|
193
|
+
continue;
|
|
194
|
+
const key = `${provider}::${model}`;
|
|
195
|
+
if (!scopes.has(key)) {
|
|
196
|
+
scopes.set(key, {
|
|
197
|
+
provider,
|
|
198
|
+
model,
|
|
199
|
+
vector_dim: Number.isFinite(vector_dim) && vector_dim > 0 ? vector_dim : undefined
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return Array.from(scopes.values());
|
|
204
|
+
}
|
|
205
|
+
static async searchDocuments(query, userId, supabase, options = {}) {
|
|
206
|
+
const { topK = 5, similarityThreshold = 0.7, settings } = options;
|
|
207
|
+
const minThreshold = Math.max(0.1, Math.min(similarityThreshold, 0.4));
|
|
208
|
+
const thresholdLevels = Array.from(new Set([similarityThreshold, minThreshold]));
|
|
209
|
+
const preferred = await this.resolveEmbeddingModel(settings || {});
|
|
210
|
+
const preferredScope = { provider: preferred.provider, model: preferred.model };
|
|
211
|
+
const embeddingCache = new Map();
|
|
212
|
+
const collected = new Map();
|
|
213
|
+
const trySearch = async (scope, threshold) => {
|
|
214
|
+
const cacheKey = `${scope.provider}::${scope.model}`;
|
|
215
|
+
let cached = embeddingCache.get(cacheKey);
|
|
216
|
+
if (!cached) {
|
|
217
|
+
const queryEmbedding = await this.embedTextWithResolvedModel(query, {
|
|
218
|
+
provider: scope.provider,
|
|
219
|
+
model: scope.model,
|
|
220
|
+
});
|
|
221
|
+
cached = { queryEmbedding, queryDim: queryEmbedding.length };
|
|
222
|
+
embeddingCache.set(cacheKey, cached);
|
|
223
|
+
}
|
|
224
|
+
const { queryEmbedding, queryDim } = cached;
|
|
225
|
+
if (scope.vector_dim && scope.vector_dim !== queryDim) {
|
|
226
|
+
logger.warn("Skipping model scope due to vector dimension mismatch", {
|
|
227
|
+
scope,
|
|
228
|
+
queryDim
|
|
229
|
+
});
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
logger.info(`Searching knowledge base (${scope.provider}/${scope.model}, dim=${queryDim}, topK=${topK}, threshold=${threshold})`);
|
|
233
|
+
const hits = await this.runSearchForModel({
|
|
234
|
+
userId,
|
|
235
|
+
supabase,
|
|
236
|
+
modelScope: scope,
|
|
237
|
+
queryEmbedding,
|
|
238
|
+
queryDim,
|
|
239
|
+
similarityThreshold: threshold,
|
|
240
|
+
topK
|
|
241
|
+
});
|
|
242
|
+
for (const hit of hits) {
|
|
243
|
+
if (!collected.has(hit.id)) {
|
|
244
|
+
collected.set(hit.id, hit);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const existing = collected.get(hit.id);
|
|
248
|
+
if (hit.similarity > existing.similarity) {
|
|
249
|
+
collected.set(hit.id, hit);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return hits.length;
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
for (const threshold of thresholdLevels) {
|
|
257
|
+
const hits = await trySearch(preferredScope, threshold);
|
|
258
|
+
if (hits > 0) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
logger.error("Semantic search failed for preferred embedding scope", {
|
|
265
|
+
provider: preferredScope.provider,
|
|
266
|
+
model: preferredScope.model,
|
|
267
|
+
error
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (collected.size === 0) {
|
|
271
|
+
const scopes = await this.listUserModelScopes(userId, supabase);
|
|
272
|
+
const fallbackScopes = scopes.filter((scope) => !(scope.provider === preferredScope.provider && scope.model === preferredScope.model));
|
|
273
|
+
for (const scope of fallbackScopes) {
|
|
274
|
+
try {
|
|
275
|
+
for (const threshold of thresholdLevels) {
|
|
276
|
+
await trySearch(scope, threshold);
|
|
277
|
+
if (collected.size >= topK)
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
logger.warn("Semantic search failed for fallback embedding scope", {
|
|
283
|
+
scope,
|
|
284
|
+
error
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (collected.size >= topK)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return Array.from(collected.values())
|
|
292
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
293
|
+
.slice(0, topK);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { RealtimeXSDK } from "@realtimex/sdk";
|
|
2
|
+
import { createLogger } from "../utils/logger.js";
|
|
3
|
+
const logger = createLogger("SDKService");
|
|
4
|
+
export class SDKService {
|
|
5
|
+
static instance = null;
|
|
6
|
+
static initAttempted = false;
|
|
7
|
+
// Default provider/model configuration
|
|
8
|
+
// realtimexai routes through RealTimeX Desktop to user's configured providers
|
|
9
|
+
static DEFAULT_LLM_PROVIDER = "realtimexai";
|
|
10
|
+
static DEFAULT_LLM_MODEL = "gpt-4o-mini";
|
|
11
|
+
static DEFAULT_EMBED_PROVIDER = "realtimexai";
|
|
12
|
+
static DEFAULT_EMBED_MODEL = "text-embedding-3-small";
|
|
13
|
+
static initialize() {
|
|
14
|
+
if (!this.instance && !this.initAttempted) {
|
|
15
|
+
this.initAttempted = true;
|
|
16
|
+
try {
|
|
17
|
+
this.instance = new RealtimeXSDK({
|
|
18
|
+
realtimex: {
|
|
19
|
+
// @ts-ignore Desktop dev bridge key
|
|
20
|
+
apiKey: "SXKX93J-QSWMB04-K9E0GRE-J5DA8J0"
|
|
21
|
+
},
|
|
22
|
+
permissions: [
|
|
23
|
+
"api.agents", // List agents
|
|
24
|
+
"api.workspaces", // List workspaces
|
|
25
|
+
"api.threads", // List threads
|
|
26
|
+
"webhook.trigger", // Trigger agents
|
|
27
|
+
"activities.read", // Read activities
|
|
28
|
+
"activities.write", // Write activities
|
|
29
|
+
"llm.chat", // Chat completion
|
|
30
|
+
"llm.embed", // Generate embeddings
|
|
31
|
+
"llm.providers", // List LLM providers (chat, embed)
|
|
32
|
+
"vectors.read", // Query vectors
|
|
33
|
+
"vectors.write", // Store vectors
|
|
34
|
+
]
|
|
35
|
+
});
|
|
36
|
+
logger.info("RealTimeX SDK initialized successfully");
|
|
37
|
+
// @ts-ignore ping available in desktop bridge
|
|
38
|
+
this.instance.ping?.().catch(() => {
|
|
39
|
+
logger.warn("Desktop ping failed during startup");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger.error("Failed to initialize SDK", {
|
|
44
|
+
error: error instanceof Error ? error.message : String(error)
|
|
45
|
+
});
|
|
46
|
+
this.instance = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return this.instance;
|
|
50
|
+
}
|
|
51
|
+
static getSDK() {
|
|
52
|
+
if (!this.instance && !this.initAttempted) {
|
|
53
|
+
this.initialize();
|
|
54
|
+
}
|
|
55
|
+
return this.instance;
|
|
56
|
+
}
|
|
57
|
+
static async isAvailable() {
|
|
58
|
+
try {
|
|
59
|
+
const sdk = this.getSDK();
|
|
60
|
+
if (!sdk)
|
|
61
|
+
return false;
|
|
62
|
+
// Try to ping first (faster)
|
|
63
|
+
try {
|
|
64
|
+
// @ts-ignore ping available in desktop bridge
|
|
65
|
+
await sdk.ping();
|
|
66
|
+
return true;
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
// Fallback to providers check if ping not available/fails
|
|
71
|
+
await sdk.llm.chatProviders();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logger.warn("SDK not available", { error: error.message });
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Helper to wrap a promise with a timeout
|
|
83
|
+
*/
|
|
84
|
+
static async withTimeout(promise, timeoutMs, errorMessage) {
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
let timeoutHandle;
|
|
87
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
88
|
+
timeoutHandle = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
const result = await Promise.race([promise, timeoutPromise]);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timeoutHandle);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Cache for default providers (avoid repeated SDK calls)
|
|
99
|
+
static defaultChatProvider = null;
|
|
100
|
+
static defaultEmbedProvider = null;
|
|
101
|
+
/**
|
|
102
|
+
* Get default chat provider/model from SDK dynamically
|
|
103
|
+
*/
|
|
104
|
+
static async getDefaultChatProvider() {
|
|
105
|
+
// Return cached if available
|
|
106
|
+
if (this.defaultChatProvider) {
|
|
107
|
+
return this.defaultChatProvider;
|
|
108
|
+
}
|
|
109
|
+
const sdk = this.getSDK();
|
|
110
|
+
if (!sdk) {
|
|
111
|
+
return {
|
|
112
|
+
provider: this.DEFAULT_LLM_PROVIDER,
|
|
113
|
+
model: this.DEFAULT_LLM_MODEL,
|
|
114
|
+
isDefaultFallback: true
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const { providers } = await this.withTimeout(sdk.llm.chatProviders(), 30000, "Chat providers fetch timed out");
|
|
119
|
+
if (!providers || providers.length === 0) {
|
|
120
|
+
throw new Error("No LLM providers available");
|
|
121
|
+
}
|
|
122
|
+
const preferred = providers.find((item) => item.provider === this.DEFAULT_LLM_PROVIDER);
|
|
123
|
+
const chosen = preferred || providers[0];
|
|
124
|
+
const model = chosen.models?.[0]?.id || this.DEFAULT_LLM_MODEL;
|
|
125
|
+
this.defaultChatProvider = {
|
|
126
|
+
provider: chosen.provider,
|
|
127
|
+
model,
|
|
128
|
+
isDefaultFallback: !preferred
|
|
129
|
+
};
|
|
130
|
+
return this.defaultChatProvider;
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
logger.warn("Failed to get default chat provider from SDK", error);
|
|
135
|
+
return {
|
|
136
|
+
provider: this.DEFAULT_LLM_PROVIDER,
|
|
137
|
+
model: this.DEFAULT_LLM_MODEL,
|
|
138
|
+
isDefaultFallback: true
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get default embedding provider/model from SDK dynamically
|
|
144
|
+
*/
|
|
145
|
+
static async getDefaultEmbedProvider() {
|
|
146
|
+
if (this.defaultEmbedProvider) {
|
|
147
|
+
return this.defaultEmbedProvider;
|
|
148
|
+
}
|
|
149
|
+
const sdk = this.getSDK();
|
|
150
|
+
if (!sdk) {
|
|
151
|
+
return {
|
|
152
|
+
provider: this.DEFAULT_EMBED_PROVIDER,
|
|
153
|
+
model: this.DEFAULT_EMBED_MODEL,
|
|
154
|
+
isDefaultFallback: true
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const { providers } = await this.withTimeout(sdk.llm.embedProviders(), 30000, "Embed providers fetch timed out");
|
|
159
|
+
if (!providers || providers.length === 0) {
|
|
160
|
+
throw new Error("No embedding providers available");
|
|
161
|
+
}
|
|
162
|
+
const preferred = providers.find((p) => p.provider === this.DEFAULT_EMBED_PROVIDER);
|
|
163
|
+
const chosen = preferred || providers[0];
|
|
164
|
+
const model = chosen.models?.[0]?.id || this.DEFAULT_EMBED_MODEL;
|
|
165
|
+
this.defaultEmbedProvider = {
|
|
166
|
+
provider: chosen.provider,
|
|
167
|
+
model,
|
|
168
|
+
isDefaultFallback: !preferred
|
|
169
|
+
};
|
|
170
|
+
return this.defaultEmbedProvider;
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
logger.warn("Failed to get default embed provider from SDK", error);
|
|
175
|
+
return {
|
|
176
|
+
provider: this.DEFAULT_EMBED_PROVIDER,
|
|
177
|
+
model: this.DEFAULT_EMBED_MODEL,
|
|
178
|
+
isDefaultFallback: true
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Resolve LLM provider/model - use settings if available, otherwise use defaults
|
|
184
|
+
*/
|
|
185
|
+
static async resolveChatProvider(settings) {
|
|
186
|
+
if (settings.llm_provider && settings.llm_model) {
|
|
187
|
+
return {
|
|
188
|
+
provider: settings.llm_provider,
|
|
189
|
+
model: settings.llm_model,
|
|
190
|
+
isDefaultFallback: false
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return await this.getDefaultChatProvider();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Resolve embedding provider/model - use settings if available, otherwise use defaults
|
|
197
|
+
*/
|
|
198
|
+
static async resolveEmbedProvider(settings) {
|
|
199
|
+
if (settings.embedding_provider && settings.embedding_model) {
|
|
200
|
+
return {
|
|
201
|
+
provider: settings.embedding_provider,
|
|
202
|
+
model: settings.embedding_model,
|
|
203
|
+
isDefaultFallback: false
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return await this.getDefaultEmbedProvider();
|
|
207
|
+
}
|
|
208
|
+
static clearProviderCache() {
|
|
209
|
+
this.defaultChatProvider = null;
|
|
210
|
+
this.defaultEmbedProvider = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import { config } from "../config/index.js";
|
|
3
|
+
import { createLogger } from "../utils/logger.js";
|
|
4
|
+
const logger = createLogger("SupabaseService");
|
|
5
|
+
let serverClient = null;
|
|
6
|
+
let lastConfigHash = "";
|
|
7
|
+
export function isValidUrl(url) {
|
|
8
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
9
|
+
}
|
|
10
|
+
function isValidAnonKey(key) {
|
|
11
|
+
return key.startsWith("eyJ") || key.startsWith("sb_publishable_");
|
|
12
|
+
}
|
|
13
|
+
function getConfigHash() {
|
|
14
|
+
return `${config.supabase.url}_${config.supabase.anonKey}`;
|
|
15
|
+
}
|
|
16
|
+
export function getServerSupabase(forceRefresh = false) {
|
|
17
|
+
const currentHash = getConfigHash();
|
|
18
|
+
if (serverClient && !forceRefresh && currentHash === lastConfigHash) {
|
|
19
|
+
return serverClient;
|
|
20
|
+
}
|
|
21
|
+
const url = config.supabase.url;
|
|
22
|
+
const key = config.supabase.anonKey;
|
|
23
|
+
if (!url || !key || !isValidUrl(url)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
serverClient = createClient(url, key, {
|
|
28
|
+
auth: {
|
|
29
|
+
autoRefreshToken: false,
|
|
30
|
+
persistSession: false
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
lastConfigHash = currentHash;
|
|
34
|
+
logger.info("Server Supabase client initialized");
|
|
35
|
+
return serverClient;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error("Failed to initialize server Supabase client", {
|
|
39
|
+
error: error instanceof Error ? error.message : String(error)
|
|
40
|
+
});
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function getServiceRoleSupabase() {
|
|
45
|
+
const url = config.supabase.url;
|
|
46
|
+
const key = config.supabase.serviceRoleKey;
|
|
47
|
+
if (!url || !key || !isValidUrl(url)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return createClient(url, key, {
|
|
52
|
+
auth: {
|
|
53
|
+
autoRefreshToken: false,
|
|
54
|
+
persistSession: false
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error("Failed to initialize service-role Supabase client", {
|
|
60
|
+
error: error instanceof Error ? error.message : String(error)
|
|
61
|
+
});
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function getSupabaseConfigFromHeaders(headers) {
|
|
66
|
+
const url = String(headers["x-supabase-url"] || "");
|
|
67
|
+
const anonKey = String(headers["x-supabase-anon-key"] || "");
|
|
68
|
+
if (!url || !anonKey || !isValidUrl(url) || !isValidAnonKey(anonKey)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return { url, anonKey };
|
|
72
|
+
}
|