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