@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,190 @@
|
|
|
1
|
+
import { Request, Response, Router } from "express";
|
|
2
|
+
import { SDKService } from "../services/SDKService.js";
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/tts/providers
|
|
8
|
+
* List available TTS providers and their configuration options
|
|
9
|
+
*/
|
|
10
|
+
router.get("/providers", async (req: Request, res: Response) => {
|
|
11
|
+
try {
|
|
12
|
+
const sdk = SDKService.getSDK();
|
|
13
|
+
if (!sdk) {
|
|
14
|
+
return res.status(503).json({
|
|
15
|
+
success: false,
|
|
16
|
+
error: "RealTimeX SDK not available. Please ensure RealTimeX Desktop is running."
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if TTS module is available
|
|
21
|
+
if (!sdk.tts) {
|
|
22
|
+
return res.status(503).json({
|
|
23
|
+
success: false,
|
|
24
|
+
error: "TTS module not available. Please ensure RealTimeX Desktop is running and updated to v1.2.3+."
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const providers = await sdk.tts.listProviders();
|
|
29
|
+
|
|
30
|
+
res.json({
|
|
31
|
+
success: true,
|
|
32
|
+
providers
|
|
33
|
+
});
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
console.error("[TTS API] Failed to list providers:", error);
|
|
37
|
+
res.status(503).json({
|
|
38
|
+
success: false,
|
|
39
|
+
error: error.message || "Failed to list TTS providers. Ensure RealTimeX Desktop is running."
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* POST /api/tts/speak
|
|
46
|
+
* Generate full audio buffer for text
|
|
47
|
+
* Body: { text: string, provider?: string, voice?: string, speed?: number }
|
|
48
|
+
*/
|
|
49
|
+
router.post("/speak", async (req: Request, res: Response) => {
|
|
50
|
+
try {
|
|
51
|
+
const { text, provider, voice, speed, quality } = req.body;
|
|
52
|
+
|
|
53
|
+
if (!text || typeof text !== "string") {
|
|
54
|
+
return res.status(400).json({
|
|
55
|
+
success: false,
|
|
56
|
+
error: "Text is required"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sdk = SDKService.getSDK();
|
|
61
|
+
if (!sdk) {
|
|
62
|
+
return res.status(503).json({
|
|
63
|
+
success: false,
|
|
64
|
+
error: "RealTimeX SDK not available"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check if TTS module is available
|
|
69
|
+
if (!sdk.tts) {
|
|
70
|
+
return res.status(503).json({
|
|
71
|
+
success: false,
|
|
72
|
+
error: "TTS module not available. Please ensure RealTimeX Desktop is running and updated to v1.2.3+."
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
const options: any = {};
|
|
78
|
+
if (provider) options.provider = provider;
|
|
79
|
+
if (voice) options.voice = voice;
|
|
80
|
+
if (speed) options.speed = parseFloat(speed);
|
|
81
|
+
if (quality) options.num_inference_steps = parseInt(quality);
|
|
82
|
+
|
|
83
|
+
const audioBuffer = await sdk.tts.speak(text, options);
|
|
84
|
+
|
|
85
|
+
// Return audio as binary
|
|
86
|
+
res.setHeader("Content-Type", "audio/mpeg");
|
|
87
|
+
res.send(Buffer.from(audioBuffer));
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
console.error("[TTS API] Failed to generate speech:", error);
|
|
91
|
+
res.status(500).json({
|
|
92
|
+
success: false,
|
|
93
|
+
error: error.message || "Failed to generate speech"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* POST /api/tts/stream
|
|
100
|
+
* Stream audio chunks via Server-Sent Events
|
|
101
|
+
* Body: { text: string, provider?: string, voice?: string, speed?: number }
|
|
102
|
+
*/
|
|
103
|
+
router.post("/stream", async (req: Request, res: Response) => {
|
|
104
|
+
try {
|
|
105
|
+
const { text, provider, voice, speed, quality } = req.body;
|
|
106
|
+
|
|
107
|
+
if (!text || typeof text !== "string") {
|
|
108
|
+
return res.status(400).json({
|
|
109
|
+
success: false,
|
|
110
|
+
error: "Text is required"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sdk = SDKService.getSDK();
|
|
115
|
+
if (!sdk) {
|
|
116
|
+
return res.status(503).json({
|
|
117
|
+
success: false,
|
|
118
|
+
error: "RealTimeX SDK not available"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if TTS module is available
|
|
123
|
+
if (!sdk.tts) {
|
|
124
|
+
return res.status(503).json({
|
|
125
|
+
success: false,
|
|
126
|
+
error: "TTS module not available. Please ensure RealTimeX Desktop is running and updated to v1.2.3+."
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Set up SSE
|
|
131
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
132
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
133
|
+
res.setHeader("Connection", "keep-alive");
|
|
134
|
+
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
const options: any = {};
|
|
137
|
+
if (provider) options.provider = provider;
|
|
138
|
+
if (voice) options.voice = voice;
|
|
139
|
+
if (speed) options.speed = parseFloat(speed);
|
|
140
|
+
if (quality) options.num_inference_steps = parseInt(quality);
|
|
141
|
+
|
|
142
|
+
// Send info event
|
|
143
|
+
res.write(`event: info\n`);
|
|
144
|
+
res.write(`data: ${JSON.stringify({ message: "Starting TTS generation..." })}\n\n`);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Stream chunks
|
|
148
|
+
for await (const chunk of sdk.tts.speakStream(text, options)) {
|
|
149
|
+
// Encode ArrayBuffer to base64
|
|
150
|
+
const base64Audio = Buffer.from(chunk.audio).toString("base64");
|
|
151
|
+
|
|
152
|
+
res.write(`event: chunk\n`);
|
|
153
|
+
res.write(`data: ${JSON.stringify({
|
|
154
|
+
index: chunk.index,
|
|
155
|
+
total: chunk.total,
|
|
156
|
+
audio: base64Audio,
|
|
157
|
+
mimeType: chunk.mimeType
|
|
158
|
+
})}\n\n`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Send done event
|
|
162
|
+
res.write(`event: done\n`);
|
|
163
|
+
res.write(`data: ${JSON.stringify({ message: "TTS generation complete" })}\n\n`);
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
} catch (streamError: any) {
|
|
166
|
+
res.write(`event: error\n`);
|
|
167
|
+
res.write(`data: ${JSON.stringify({ error: streamError.message })}\n\n`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
res.end();
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
} catch (error: any) {
|
|
173
|
+
console.error("[TTS API] Failed to stream speech:", error);
|
|
174
|
+
|
|
175
|
+
// If headers not sent yet, send JSON error
|
|
176
|
+
if (!res.headersSent) {
|
|
177
|
+
res.status(500).json({
|
|
178
|
+
success: false,
|
|
179
|
+
error: error.message || "Failed to stream speech"
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
// Send SSE error event
|
|
183
|
+
res.write(`event: error\n`);
|
|
184
|
+
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
|
185
|
+
res.end();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export default router;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { createLogger } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
const logger = createLogger("BaselineConfigService");
|
|
5
|
+
|
|
6
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface BaselineField {
|
|
9
|
+
key: string;
|
|
10
|
+
type: "string" | "number" | "date" | "currency" | "string[]";
|
|
11
|
+
description: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
is_default: boolean; // default fields can be disabled but not deleted
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BaselineConfig {
|
|
17
|
+
id: string;
|
|
18
|
+
user_id: string;
|
|
19
|
+
version: number;
|
|
20
|
+
context: string | null;
|
|
21
|
+
fields: BaselineField[];
|
|
22
|
+
is_active: boolean;
|
|
23
|
+
created_at: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Default schema ───────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_BASELINE_FIELDS: BaselineField[] = [
|
|
29
|
+
{
|
|
30
|
+
key: "document_type",
|
|
31
|
+
type: "string",
|
|
32
|
+
description: 'Type of document (e.g. "invoice", "contract", "receipt", "report", "statement")',
|
|
33
|
+
enabled: true,
|
|
34
|
+
is_default: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "issuer",
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Person or organisation that sent or issued the document",
|
|
40
|
+
enabled: true,
|
|
41
|
+
is_default: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "recipient",
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Person or organisation the document is addressed to",
|
|
47
|
+
enabled: true,
|
|
48
|
+
is_default: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "date",
|
|
52
|
+
type: "date",
|
|
53
|
+
description: "Primary date on the document in ISO 8601 format (YYYY-MM-DD)",
|
|
54
|
+
enabled: true,
|
|
55
|
+
is_default: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "amount",
|
|
59
|
+
type: "currency",
|
|
60
|
+
description: "Primary monetary value if present (numeric, no currency symbol)",
|
|
61
|
+
enabled: true,
|
|
62
|
+
is_default: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "currency",
|
|
66
|
+
type: "string",
|
|
67
|
+
description: 'Three-letter currency code if present (e.g. "USD", "EUR", "GBP")',
|
|
68
|
+
enabled: true,
|
|
69
|
+
is_default: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "subject",
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "One-sentence description of what this document is about",
|
|
75
|
+
enabled: true,
|
|
76
|
+
is_default: true,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "tags",
|
|
80
|
+
type: "string[]",
|
|
81
|
+
description: 'Semantic labels that describe this document (e.g. ["subscription", "renewal", "tax", "refund"])',
|
|
82
|
+
enabled: true,
|
|
83
|
+
is_default: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "suggested_filename",
|
|
87
|
+
type: "string",
|
|
88
|
+
description: 'A highly descriptive, concise filename for this document using the format: YYYY-MM-DD_Issuer_DocType. Do not include file extensions. If date is missing, omit it.',
|
|
89
|
+
enabled: true,
|
|
90
|
+
is_default: true,
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// ─── Service ──────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export class BaselineConfigService {
|
|
97
|
+
/**
|
|
98
|
+
* Return the active config for a user.
|
|
99
|
+
* Returns null if none has been saved yet — callers should fall back to DEFAULT_BASELINE_FIELDS.
|
|
100
|
+
*/
|
|
101
|
+
static async getActive(supabase: SupabaseClient, userId: string): Promise<BaselineConfig | null> {
|
|
102
|
+
const { data, error } = await supabase
|
|
103
|
+
.from("baseline_configs")
|
|
104
|
+
.select("*")
|
|
105
|
+
.eq("user_id", userId)
|
|
106
|
+
.eq("is_active", true)
|
|
107
|
+
.maybeSingle();
|
|
108
|
+
|
|
109
|
+
if (error) {
|
|
110
|
+
logger.warn("Failed to fetch active baseline config", { error });
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return data as BaselineConfig | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Return all saved versions for a user, newest first.
|
|
118
|
+
*/
|
|
119
|
+
static async list(supabase: SupabaseClient, userId: string): Promise<BaselineConfig[]> {
|
|
120
|
+
const { data, error } = await supabase
|
|
121
|
+
.from("baseline_configs")
|
|
122
|
+
.select("*")
|
|
123
|
+
.eq("user_id", userId)
|
|
124
|
+
.order("version", { ascending: false });
|
|
125
|
+
|
|
126
|
+
if (error) throw new Error(`Failed to list baseline configs: ${error.message}`);
|
|
127
|
+
return (data ?? []) as BaselineConfig[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Save a new config version.
|
|
132
|
+
* Always creates a new immutable row — never mutates existing versions.
|
|
133
|
+
* If activate=true, the new version is immediately set as active and the
|
|
134
|
+
* previous active version is deactivated.
|
|
135
|
+
*/
|
|
136
|
+
static async save(
|
|
137
|
+
supabase: SupabaseClient,
|
|
138
|
+
userId: string,
|
|
139
|
+
payload: { context?: string | null; fields: BaselineField[] },
|
|
140
|
+
activate: boolean
|
|
141
|
+
): Promise<BaselineConfig> {
|
|
142
|
+
// Determine next version number for this user
|
|
143
|
+
const { data: latest } = await supabase
|
|
144
|
+
.from("baseline_configs")
|
|
145
|
+
.select("version")
|
|
146
|
+
.eq("user_id", userId)
|
|
147
|
+
.order("version", { ascending: false })
|
|
148
|
+
.limit(1)
|
|
149
|
+
.maybeSingle();
|
|
150
|
+
|
|
151
|
+
const nextVersion = (latest?.version ?? 0) + 1;
|
|
152
|
+
|
|
153
|
+
if (activate) {
|
|
154
|
+
// Deactivate all existing versions first
|
|
155
|
+
await supabase
|
|
156
|
+
.from("baseline_configs")
|
|
157
|
+
.update({ is_active: false })
|
|
158
|
+
.eq("user_id", userId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { data, error } = await supabase
|
|
162
|
+
.from("baseline_configs")
|
|
163
|
+
.insert({
|
|
164
|
+
user_id: userId,
|
|
165
|
+
version: nextVersion,
|
|
166
|
+
context: payload.context ?? null,
|
|
167
|
+
fields: payload.fields,
|
|
168
|
+
is_active: activate,
|
|
169
|
+
})
|
|
170
|
+
.select()
|
|
171
|
+
.single();
|
|
172
|
+
|
|
173
|
+
if (error || !data) throw new Error(`Failed to save baseline config: ${error?.message}`);
|
|
174
|
+
logger.info(`Saved baseline config v${nextVersion} for user ${userId} (active: ${activate})`);
|
|
175
|
+
return data as BaselineConfig;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Activate a specific saved version.
|
|
180
|
+
* Deactivates all other versions for this user atomically.
|
|
181
|
+
*/
|
|
182
|
+
static async activate(supabase: SupabaseClient, userId: string, id: string): Promise<boolean> {
|
|
183
|
+
// Verify the config belongs to this user
|
|
184
|
+
const { data: target } = await supabase
|
|
185
|
+
.from("baseline_configs")
|
|
186
|
+
.select("id, version")
|
|
187
|
+
.eq("id", id)
|
|
188
|
+
.eq("user_id", userId)
|
|
189
|
+
.maybeSingle();
|
|
190
|
+
|
|
191
|
+
if (!target) return false;
|
|
192
|
+
|
|
193
|
+
// Deactivate all, then activate the target
|
|
194
|
+
await supabase
|
|
195
|
+
.from("baseline_configs")
|
|
196
|
+
.update({ is_active: false })
|
|
197
|
+
.eq("user_id", userId);
|
|
198
|
+
|
|
199
|
+
const { error } = await supabase
|
|
200
|
+
.from("baseline_configs")
|
|
201
|
+
.update({ is_active: true })
|
|
202
|
+
.eq("id", id);
|
|
203
|
+
|
|
204
|
+
if (error) throw new Error(`Failed to activate baseline config: ${error.message}`);
|
|
205
|
+
logger.info(`Activated baseline config v${target.version} for user ${userId}`);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { SDKService } from "./SDKService.js";
|
|
3
|
+
import { RAGService, RetrievedChunk } from "./RAGService.js";
|
|
4
|
+
import { createLogger } from "../utils/logger.js";
|
|
5
|
+
import { Actuator } from "../utils/Actuator.js";
|
|
6
|
+
import { extractLlmResponse, previewLlmText } from "../utils/llmResponse.js";
|
|
7
|
+
|
|
8
|
+
const logger = createLogger("ChatService");
|
|
9
|
+
|
|
10
|
+
export interface Message {
|
|
11
|
+
id: string;
|
|
12
|
+
role: "user" | "assistant" | "system";
|
|
13
|
+
content: string;
|
|
14
|
+
context_sources?: RetrievedChunk[];
|
|
15
|
+
created_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ChatService {
|
|
19
|
+
/**
|
|
20
|
+
* Send a message to an existing session, augment with RAG context if needed,
|
|
21
|
+
* and stream the AI response back into the database.
|
|
22
|
+
*/
|
|
23
|
+
static async handleMessage(
|
|
24
|
+
sessionId: string,
|
|
25
|
+
userId: string,
|
|
26
|
+
content: string,
|
|
27
|
+
supabase: SupabaseClient
|
|
28
|
+
): Promise<Message> {
|
|
29
|
+
// 1. Get User/Session Settings (Models to use)
|
|
30
|
+
const { data: settings } = await supabase
|
|
31
|
+
.from("user_settings")
|
|
32
|
+
.select("llm_provider, llm_model, embedding_provider, embedding_model")
|
|
33
|
+
.eq("user_id", userId)
|
|
34
|
+
.maybeSingle();
|
|
35
|
+
|
|
36
|
+
const llmSettings = {
|
|
37
|
+
llm_provider: settings?.llm_provider ?? undefined,
|
|
38
|
+
llm_model: settings?.llm_model ?? undefined,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const embedSettings = {
|
|
42
|
+
embedding_provider: settings?.embedding_provider ?? undefined,
|
|
43
|
+
embedding_model: settings?.embedding_model ?? undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 2. Resolve Providers
|
|
47
|
+
const chatProvider = await SDKService.resolveChatProvider(llmSettings);
|
|
48
|
+
|
|
49
|
+
// 3. Save User Message
|
|
50
|
+
const { error: userMsgErr } = await supabase.from("chat_messages").insert({
|
|
51
|
+
session_id: sessionId,
|
|
52
|
+
user_id: userId,
|
|
53
|
+
role: "user",
|
|
54
|
+
content
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (userMsgErr) {
|
|
58
|
+
logger.error(`Failed to save user message for session ${sessionId}`, { error: userMsgErr });
|
|
59
|
+
throw new Error("Failed to save message");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 4. Retrieve semantic context (Dynamic RAG)
|
|
63
|
+
let contextSources: RetrievedChunk[] = [];
|
|
64
|
+
try {
|
|
65
|
+
Actuator.logEvent(null, userId, "analysis", "RAG Retrieval", {
|
|
66
|
+
action: "RAG query request",
|
|
67
|
+
session_id: sessionId,
|
|
68
|
+
top_k: 5,
|
|
69
|
+
threshold: 0.65,
|
|
70
|
+
embedding_provider: embedSettings.embedding_provider ?? "auto",
|
|
71
|
+
embedding_model: embedSettings.embedding_model ?? "auto",
|
|
72
|
+
query_preview: content.slice(0, 180),
|
|
73
|
+
}, supabase);
|
|
74
|
+
contextSources = await RAGService.searchDocuments(
|
|
75
|
+
content,
|
|
76
|
+
userId,
|
|
77
|
+
supabase,
|
|
78
|
+
{ topK: 5, similarityThreshold: 0.65, settings: embedSettings }
|
|
79
|
+
);
|
|
80
|
+
Actuator.logEvent(null, userId, "analysis", "RAG Retrieval", {
|
|
81
|
+
action: "RAG query response",
|
|
82
|
+
session_id: sessionId,
|
|
83
|
+
hits: contextSources.length,
|
|
84
|
+
top_similarity: contextSources[0]?.similarity ?? null,
|
|
85
|
+
ingestion_ids: Array.from(new Set(contextSources.map((c) => c.ingestion_id))).slice(0, 5),
|
|
86
|
+
}, supabase);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.warn(`Semantic search failed during chat. Proceeding without context.`, { error: err });
|
|
89
|
+
Actuator.logEvent(null, userId, "error", "RAG Retrieval", {
|
|
90
|
+
action: "RAG query failed",
|
|
91
|
+
session_id: sessionId,
|
|
92
|
+
error: err instanceof Error ? err.message : String(err),
|
|
93
|
+
}, supabase);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 5. Fetch Chat History
|
|
97
|
+
const { data: history, error: historyError } = await supabase
|
|
98
|
+
.from("chat_messages")
|
|
99
|
+
.select("role, content")
|
|
100
|
+
.eq("session_id", sessionId)
|
|
101
|
+
.order("created_at", { ascending: false })
|
|
102
|
+
.limit(20);
|
|
103
|
+
|
|
104
|
+
if (historyError) {
|
|
105
|
+
logger.error(`Failed to fetch chat history for session ${sessionId}`, { error: historyError });
|
|
106
|
+
throw new Error("Failed to load chat history");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const chatHistory: Array<{ role: "user" | "assistant" | "system"; content: string }> = (history || [])
|
|
110
|
+
.reverse()
|
|
111
|
+
.map((m) => ({
|
|
112
|
+
role: m.role as "user" | "assistant" | "system",
|
|
113
|
+
content: String(m.content ?? "")
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
// 6. Build the Augmented Prompt
|
|
117
|
+
let systemPrompt = `You are the Folio AI Agent, a brilliant and precise autonomous filing assistant.\n`;
|
|
118
|
+
systemPrompt += `You have been asked a question. `;
|
|
119
|
+
|
|
120
|
+
if (contextSources.length > 0) {
|
|
121
|
+
systemPrompt += `Below is exact extracted text from the user's documents retrieved via Semantic Search to answer the question. Cite the context when responding.\n\n`;
|
|
122
|
+
systemPrompt += `--- CONTEXT SOURCES ---\n`;
|
|
123
|
+
contextSources.forEach((c, idx) => {
|
|
124
|
+
systemPrompt += `[Source ${idx + 1}]:\n${c.content}\n\n`;
|
|
125
|
+
});
|
|
126
|
+
systemPrompt += `--- END CONTEXT ---\n`;
|
|
127
|
+
} else {
|
|
128
|
+
systemPrompt += `No specific documents were retrieved for this query. Answer conversationally, but let the user know you couldn't find exact files matching their question.`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const messagesForLLM: Array<{ role: "user" | "assistant" | "system"; content: string }> = [
|
|
132
|
+
{ role: "system", content: systemPrompt },
|
|
133
|
+
...chatHistory
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// 7. Get AI Response via SDK
|
|
137
|
+
const sdk = SDKService.getSDK();
|
|
138
|
+
if (!sdk) {
|
|
139
|
+
throw new Error("RealTimeX SDK not available");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let replyContent = "I am unable to process that request.";
|
|
143
|
+
try {
|
|
144
|
+
Actuator.logEvent(null, userId, "analysis", "Chat", {
|
|
145
|
+
action: "LLM request (chat response)",
|
|
146
|
+
session_id: sessionId,
|
|
147
|
+
provider: chatProvider.provider,
|
|
148
|
+
model: chatProvider.model,
|
|
149
|
+
messages_count: messagesForLLM.length,
|
|
150
|
+
context_sources_count: contextSources.length,
|
|
151
|
+
}, supabase);
|
|
152
|
+
|
|
153
|
+
const completion = await sdk.llm.chat(messagesForLLM, {
|
|
154
|
+
provider: chatProvider.provider,
|
|
155
|
+
model: chatProvider.model,
|
|
156
|
+
temperature: 0.3
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const raw = extractLlmResponse(completion);
|
|
160
|
+
Actuator.logEvent(null, userId, "analysis", "Chat", {
|
|
161
|
+
action: "LLM response (chat response)",
|
|
162
|
+
session_id: sessionId,
|
|
163
|
+
provider: chatProvider.provider,
|
|
164
|
+
model: chatProvider.model,
|
|
165
|
+
raw_length: raw.length,
|
|
166
|
+
raw_preview: previewLlmText(raw),
|
|
167
|
+
}, supabase);
|
|
168
|
+
|
|
169
|
+
if (raw.trim()) {
|
|
170
|
+
replyContent = raw;
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
174
|
+
logger.error(`Chat completion failed for session ${sessionId}`, { error: err });
|
|
175
|
+
Actuator.logEvent(null, userId, "error", "Chat", {
|
|
176
|
+
action: "LLM chat failed",
|
|
177
|
+
session_id: sessionId,
|
|
178
|
+
provider: chatProvider.provider,
|
|
179
|
+
model: chatProvider.model,
|
|
180
|
+
error: msg,
|
|
181
|
+
}, supabase);
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 8. Save Assistant Reply
|
|
186
|
+
const { data: aiMsg, error: aiMsgErr } = await supabase.from("chat_messages")
|
|
187
|
+
.insert({
|
|
188
|
+
session_id: sessionId,
|
|
189
|
+
user_id: userId,
|
|
190
|
+
role: "assistant",
|
|
191
|
+
content: replyContent,
|
|
192
|
+
context_sources: contextSources
|
|
193
|
+
})
|
|
194
|
+
.select("*")
|
|
195
|
+
.single();
|
|
196
|
+
|
|
197
|
+
if (aiMsgErr) {
|
|
198
|
+
logger.error(`Failed to save AI message for session ${sessionId}`, { error: aiMsgErr });
|
|
199
|
+
throw new Error("Failed to save AI response");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return aiMsg as Message;
|
|
203
|
+
}
|
|
204
|
+
}
|