@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.
Files changed (163) hide show
  1. package/.env.example +20 -0
  2. package/README.md +63 -0
  3. package/api/server.ts +130 -0
  4. package/api/src/config/index.ts +96 -0
  5. package/api/src/middleware/auth.ts +128 -0
  6. package/api/src/middleware/errorHandler.ts +88 -0
  7. package/api/src/middleware/index.ts +4 -0
  8. package/api/src/middleware/rateLimit.ts +71 -0
  9. package/api/src/middleware/validation.ts +58 -0
  10. package/api/src/routes/accounts.ts +142 -0
  11. package/api/src/routes/baseline-config.ts +124 -0
  12. package/api/src/routes/chat.ts +154 -0
  13. package/api/src/routes/health.ts +61 -0
  14. package/api/src/routes/index.ts +35 -0
  15. package/api/src/routes/ingestions.ts +275 -0
  16. package/api/src/routes/migrate.ts +112 -0
  17. package/api/src/routes/policies.ts +121 -0
  18. package/api/src/routes/processing.ts +90 -0
  19. package/api/src/routes/rules.ts +11 -0
  20. package/api/src/routes/sdk.ts +100 -0
  21. package/api/src/routes/settings.ts +80 -0
  22. package/api/src/routes/setup.ts +389 -0
  23. package/api/src/routes/stats.ts +81 -0
  24. package/api/src/routes/tts.ts +190 -0
  25. package/api/src/services/BaselineConfigService.ts +208 -0
  26. package/api/src/services/ChatService.ts +204 -0
  27. package/api/src/services/GoogleDriveService.ts +331 -0
  28. package/api/src/services/GoogleSheetsService.ts +1107 -0
  29. package/api/src/services/IngestionService.ts +1187 -0
  30. package/api/src/services/ModelCapabilityService.ts +248 -0
  31. package/api/src/services/PolicyEngine.ts +1625 -0
  32. package/api/src/services/PolicyLearningService.ts +527 -0
  33. package/api/src/services/PolicyLoader.ts +249 -0
  34. package/api/src/services/RAGService.ts +391 -0
  35. package/api/src/services/SDKService.ts +249 -0
  36. package/api/src/services/supabase.ts +113 -0
  37. package/api/src/utils/Actuator.ts +284 -0
  38. package/api/src/utils/actions/ActionHandler.ts +34 -0
  39. package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
  40. package/api/src/utils/actions/AutoRenameAction.ts +58 -0
  41. package/api/src/utils/actions/CopyAction.ts +120 -0
  42. package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
  43. package/api/src/utils/actions/LogCsvAction.ts +48 -0
  44. package/api/src/utils/actions/NotifyAction.ts +39 -0
  45. package/api/src/utils/actions/RenameAction.ts +57 -0
  46. package/api/src/utils/actions/WebhookAction.ts +58 -0
  47. package/api/src/utils/actions/utils.ts +293 -0
  48. package/api/src/utils/llmResponse.ts +61 -0
  49. package/api/src/utils/logger.ts +67 -0
  50. package/bin/folio-deploy.js +12 -0
  51. package/bin/folio-setup.js +45 -0
  52. package/bin/folio.js +65 -0
  53. package/dist/api/server.js +106 -0
  54. package/dist/api/src/config/index.js +81 -0
  55. package/dist/api/src/middleware/auth.js +93 -0
  56. package/dist/api/src/middleware/errorHandler.js +73 -0
  57. package/dist/api/src/middleware/index.js +4 -0
  58. package/dist/api/src/middleware/rateLimit.js +43 -0
  59. package/dist/api/src/middleware/validation.js +54 -0
  60. package/dist/api/src/routes/accounts.js +110 -0
  61. package/dist/api/src/routes/baseline-config.js +91 -0
  62. package/dist/api/src/routes/chat.js +114 -0
  63. package/dist/api/src/routes/health.js +52 -0
  64. package/dist/api/src/routes/index.js +31 -0
  65. package/dist/api/src/routes/ingestions.js +207 -0
  66. package/dist/api/src/routes/migrate.js +91 -0
  67. package/dist/api/src/routes/policies.js +86 -0
  68. package/dist/api/src/routes/processing.js +75 -0
  69. package/dist/api/src/routes/rules.js +8 -0
  70. package/dist/api/src/routes/sdk.js +80 -0
  71. package/dist/api/src/routes/settings.js +68 -0
  72. package/dist/api/src/routes/setup.js +315 -0
  73. package/dist/api/src/routes/stats.js +62 -0
  74. package/dist/api/src/routes/tts.js +178 -0
  75. package/dist/api/src/services/BaselineConfigService.js +168 -0
  76. package/dist/api/src/services/ChatService.js +166 -0
  77. package/dist/api/src/services/GoogleDriveService.js +280 -0
  78. package/dist/api/src/services/GoogleSheetsService.js +795 -0
  79. package/dist/api/src/services/IngestionService.js +990 -0
  80. package/dist/api/src/services/ModelCapabilityService.js +179 -0
  81. package/dist/api/src/services/PolicyEngine.js +1353 -0
  82. package/dist/api/src/services/PolicyLearningService.js +397 -0
  83. package/dist/api/src/services/PolicyLoader.js +159 -0
  84. package/dist/api/src/services/RAGService.js +295 -0
  85. package/dist/api/src/services/SDKService.js +212 -0
  86. package/dist/api/src/services/supabase.js +72 -0
  87. package/dist/api/src/utils/Actuator.js +225 -0
  88. package/dist/api/src/utils/actions/ActionHandler.js +1 -0
  89. package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
  90. package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
  91. package/dist/api/src/utils/actions/CopyAction.js +112 -0
  92. package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
  93. package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
  94. package/dist/api/src/utils/actions/NotifyAction.js +32 -0
  95. package/dist/api/src/utils/actions/RenameAction.js +51 -0
  96. package/dist/api/src/utils/actions/WebhookAction.js +51 -0
  97. package/dist/api/src/utils/actions/utils.js +237 -0
  98. package/dist/api/src/utils/llmResponse.js +63 -0
  99. package/dist/api/src/utils/logger.js +51 -0
  100. package/dist/assets/index-DzN8-j-e.css +1 -0
  101. package/dist/assets/index-Uy-ai3Dh.js +113 -0
  102. package/dist/favicon.svg +31 -0
  103. package/dist/folio-logo.svg +46 -0
  104. package/dist/index.html +14 -0
  105. package/docs-dev/FPE-spec.md +196 -0
  106. package/docs-dev/folio-prd.md +47 -0
  107. package/docs-dev/foundation-checklist.md +30 -0
  108. package/docs-dev/hybrid-routing-architecture.md +205 -0
  109. package/docs-dev/ingestion-engine.md +69 -0
  110. package/docs-dev/port-from-email-automator.md +32 -0
  111. package/docs-dev/tech-spec.md +98 -0
  112. package/index.html +13 -0
  113. package/package.json +101 -0
  114. package/public/favicon.svg +31 -0
  115. package/public/folio-logo.svg +46 -0
  116. package/scripts/dev-task.mjs +51 -0
  117. package/scripts/get-latest-migration-timestamp.mjs +34 -0
  118. package/scripts/migrate.sh +91 -0
  119. package/supabase/.temp/cli-latest +1 -0
  120. package/supabase/.temp/gotrue-version +1 -0
  121. package/supabase/.temp/pooler-url +1 -0
  122. package/supabase/.temp/postgres-version +1 -0
  123. package/supabase/.temp/project-ref +1 -0
  124. package/supabase/.temp/rest-version +1 -0
  125. package/supabase/.temp/storage-migration +1 -0
  126. package/supabase/.temp/storage-version +1 -0
  127. package/supabase/config.toml +64 -0
  128. package/supabase/functions/_shared/auth.ts +35 -0
  129. package/supabase/functions/_shared/cors.ts +12 -0
  130. package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
  131. package/supabase/functions/api-v1-settings/index.ts +66 -0
  132. package/supabase/functions/setup/index.ts +91 -0
  133. package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
  134. package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
  135. package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
  136. package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
  137. package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
  138. package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
  139. package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
  140. package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
  141. package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
  142. package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
  143. package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
  144. package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
  145. package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
  146. package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
  147. package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
  148. package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
  149. package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
  150. package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
  151. package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
  152. package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
  153. package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
  154. package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
  155. package/supabase/migrations/29991231235959_test_migration.sql +0 -0
  156. package/supabase/templates/confirmation.html +76 -0
  157. package/supabase/templates/email-change.html +76 -0
  158. package/supabase/templates/invite.html +72 -0
  159. package/supabase/templates/magic-link.html +68 -0
  160. package/supabase/templates/recovery.html +82 -0
  161. package/tsconfig.api.json +16 -0
  162. package/tsconfig.json +25 -0
  163. 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
+ }