@realtimex/email-automator 2.21.2 → 2.21.5
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 +4 -4
- package/README.md +5 -4
- package/api/src/config/index.ts +10 -8
- package/api/src/lib/agent-knowledge-base.ts +83 -0
- package/api/src/middleware/validation.ts +1 -0
- package/api/src/routes/agent.ts +179 -0
- package/api/src/routes/health.ts +1 -1
- package/api/src/routes/index.ts +7 -0
- package/api/src/routes/migrate.ts +8 -1
- package/api/src/routes/tts.ts +187 -0
- package/api/src/services/AgentService.ts +206 -0
- package/api/src/services/RAGService.ts +245 -0
- package/api/src/services/SDKService.ts +23 -2
- package/api/src/services/supabase.ts +2 -2
- package/bin/email-automator-setup.js +6 -6
- package/dist/api/src/config/index.js +10 -8
- package/dist/api/src/lib/agent-knowledge-base.js +72 -0
- package/dist/api/src/middleware/validation.js +1 -0
- package/dist/api/src/routes/agent.js +152 -0
- package/dist/api/src/routes/health.js +1 -1
- package/dist/api/src/routes/index.js +4 -0
- package/dist/api/src/routes/migrate.js +7 -1
- package/dist/api/src/routes/tts.js +174 -0
- package/dist/api/src/services/AgentService.js +167 -0
- package/dist/api/src/services/RAGService.js +170 -0
- package/dist/api/src/services/SDKService.js +20 -2
- package/dist/api/src/services/supabase.js +2 -2
- package/dist/assets/es-BQiNEjuo.js +4 -0
- package/dist/assets/fr-Di3HVss0.js +4 -0
- package/dist/assets/index-CotbFXGe.js +193 -0
- package/dist/assets/index-DTtR5hN2.css +1 -0
- package/dist/assets/ja-OkCqAyrq.js +4 -0
- package/dist/assets/ko-ZvnlIxWp.js +4 -0
- package/dist/assets/vi-BATQPoNy.js +4 -0
- package/dist/index.html +2 -2
- package/docs/README.md +33 -0
- package/docs/user-guide/ACCOUNT.md +39 -0
- package/docs/user-guide/AUTOMATION.md +47 -0
- package/docs/user-guide/CONFIGURATION.md +75 -0
- package/docs/user-guide/DASHBOARD.md +77 -0
- package/docs/user-guide/GETTING-STARTED.md +79 -0
- package/docs/user-guide/TROUBLESHOOTING.md +64 -0
- package/package.json +5 -2
- package/scripts/ingest-knowledge-rag.ts +253 -0
- package/scripts/migrate.sh +88 -2
- package/supabase/migrations/20260203000001_add_auto_speak_setting.sql +10 -0
- package/supabase/migrations/20260203000002_add_tts_settings.sql +29 -0
- package/supabase/migrations/20260203000003_add_knowledge_base_rag.sql +92 -0
- package/supabase/migrations/20260203000004_add_embedding_settings.sql +11 -0
- package/dist/assets/es-CYWTwB_c.js +0 -1
- package/dist/assets/fr-C7ON1HVi.js +0 -1
- package/dist/assets/index-BF23Ab2e.js +0 -130
- package/dist/assets/index-DwCPoGUC.css +0 -1
- package/dist/assets/ja-DGyVGEJe.js +0 -1
- package/dist/assets/ko-DMcYqJvz.js +0 -1
- package/dist/assets/vi-a_omNqsV.js +0 -1
package/.env.example
CHANGED
|
@@ -8,10 +8,10 @@ VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
|
8
8
|
VITE_API_URL=http://localhost:3004
|
|
9
9
|
PORT=3004
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
# LLM Configuration
|
|
12
|
+
# Managed by RealTimeX SDK - No environment variables needed!
|
|
13
|
+
# Configure providers via RealTimeX Desktop (port 3001) or Configuration UI
|
|
14
|
+
# The SDK automatically connects to RealTimeX Desktop for AI operations
|
|
15
15
|
|
|
16
16
|
# Security (required in production)
|
|
17
17
|
JWT_SECRET="your-secure-jwt-secret-min-32-chars"
|
package/README.md
CHANGED
|
@@ -178,10 +178,11 @@ VITE_SUPABASE_ANON_KEY=your-anon-key
|
|
|
178
178
|
VITE_API_URL=http://localhost:3004
|
|
179
179
|
PORT=3004
|
|
180
180
|
|
|
181
|
-
# LLM
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
# LLM Configuration
|
|
182
|
+
# Managed by RealTimeX SDK - No API keys needed!
|
|
183
|
+
# 1. Start RealTimeX Desktop (port 3001)
|
|
184
|
+
# 2. Configure providers via Configuration UI or RealTimeX Desktop
|
|
185
|
+
# The SDK automatically handles provider discovery and routing
|
|
185
186
|
|
|
186
187
|
# Development
|
|
187
188
|
DISABLE_AUTH=true
|
package/api/src/config/index.ts
CHANGED
|
@@ -72,27 +72,29 @@ export const config = {
|
|
|
72
72
|
scriptsDir: join(packageRoot, 'scripts'),
|
|
73
73
|
|
|
74
74
|
// Supabase
|
|
75
|
+
// Supabase Configuration
|
|
76
|
+
// Primary: BYOK via Setup Wizard (credentials passed via HTTP headers)
|
|
77
|
+
// Fallback: Environment variables (for backend development/testing only)
|
|
78
|
+
// WARNING: Env var fallback will be removed in future versions - use Setup Wizard!
|
|
75
79
|
supabase: {
|
|
76
80
|
url: process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || '',
|
|
77
81
|
anonKey: process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || '',
|
|
78
82
|
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY || '',
|
|
79
83
|
},
|
|
80
84
|
|
|
81
|
-
// LLM
|
|
82
|
-
|
|
83
|
-
apiKey: process.env.LLM_API_KEY || '',
|
|
84
|
-
baseUrl: process.env.LLM_BASE_URL,
|
|
85
|
-
model: process.env.LLM_MODEL || 'gpt-4o-mini',
|
|
86
|
-
},
|
|
85
|
+
// LLM Configuration: Managed by RealTimeX SDK (no env vars needed)
|
|
86
|
+
// Users configure providers via RealTimeX Desktop or Configuration UI
|
|
87
87
|
|
|
88
|
-
// OAuth
|
|
88
|
+
// OAuth Configuration
|
|
89
|
+
// Primary: BYOK via Setup Wizard (stored in user_settings)
|
|
90
|
+
// Fallback: Environment variables (for backend development/testing only)
|
|
91
|
+
// WARNING: Env var fallback will be removed in future versions - use Setup Wizard!
|
|
89
92
|
gmail: {
|
|
90
93
|
clientId: process.env.GMAIL_CLIENT_ID || '',
|
|
91
94
|
clientSecret: process.env.GMAIL_CLIENT_SECRET || '',
|
|
92
95
|
redirectUri: process.env.GMAIL_REDIRECT_URI || 'urn:ietf:wg:oauth:2.0:oob',
|
|
93
96
|
},
|
|
94
97
|
|
|
95
|
-
// OAuth - Microsoft
|
|
96
98
|
microsoft: {
|
|
97
99
|
clientId: process.env.MS_GRAPH_CLIENT_ID || '',
|
|
98
100
|
tenantId: process.env.MS_GRAPH_TENANT_ID || 'common',
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Knowledge Base (Backend)
|
|
3
|
+
* Provides comprehensive context about Email-Automator to the LLM
|
|
4
|
+
*
|
|
5
|
+
* This file reads from docs/AGENT-KNOWLEDGE.md which is auto-generated
|
|
6
|
+
* at build time by scripts/generate-agent-docs.ts from user documentation
|
|
7
|
+
*
|
|
8
|
+
* Sources:
|
|
9
|
+
* - docs/user-guide/*.md (primary source)
|
|
10
|
+
* - docs/README.md
|
|
11
|
+
* - CLAUDE.md (technical architecture)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
|
|
21
|
+
// Read generated documentation at runtime
|
|
22
|
+
let CORE_APP_KNOWLEDGE = '';
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Try to read from generated knowledge base
|
|
26
|
+
const docsPath = path.join(__dirname, '../../../docs/AGENT-KNOWLEDGE.md');
|
|
27
|
+
CORE_APP_KNOWLEDGE = fs.readFileSync(docsPath, 'utf-8');
|
|
28
|
+
console.log('[AgentKnowledge] ✓ Loaded knowledge base from docs/AGENT-KNOWLEDGE.md');
|
|
29
|
+
console.log(`[AgentKnowledge] Size: ${(CORE_APP_KNOWLEDGE.length / 1024).toFixed(2)} KB`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn('[AgentKnowledge] ⚠️ Could not load docs/AGENT-KNOWLEDGE.md, using fallback');
|
|
32
|
+
console.warn('[AgentKnowledge] Run: npm run build:docs');
|
|
33
|
+
|
|
34
|
+
// Fallback minimal knowledge
|
|
35
|
+
CORE_APP_KNOWLEDGE = `# Email-Automator Assistant
|
|
36
|
+
|
|
37
|
+
I'm your Email-Automator assistant. I can help you with:
|
|
38
|
+
- Connecting email accounts (Gmail, Outlook)
|
|
39
|
+
- Creating automation rules
|
|
40
|
+
- Managing drafts
|
|
41
|
+
- Configuring AI and voice settings
|
|
42
|
+
|
|
43
|
+
**⚠️ Note:** Full knowledge base not available. Please regenerate by running:
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
npm run build:docs
|
|
46
|
+
\`\`\`
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { CORE_APP_KNOWLEDGE };
|
|
51
|
+
|
|
52
|
+
export function getEnhancedSystemInstruction(
|
|
53
|
+
pageId: string,
|
|
54
|
+
userInstruction: string,
|
|
55
|
+
data?: any
|
|
56
|
+
): string {
|
|
57
|
+
// Base instruction with knowledge
|
|
58
|
+
let instruction = `${userInstruction}
|
|
59
|
+
|
|
60
|
+
${CORE_APP_KNOWLEDGE}
|
|
61
|
+
|
|
62
|
+
# Current Context
|
|
63
|
+
- **Current Page**: ${pageId}
|
|
64
|
+
- **Page Data**: ${JSON.stringify(data || {}, null, 2)}
|
|
65
|
+
|
|
66
|
+
# Response Instructions
|
|
67
|
+
|
|
68
|
+
**CRITICAL - Anti-Hallucination Rules:**
|
|
69
|
+
1. ONLY answer using information explicitly documented in the knowledge base above
|
|
70
|
+
2. If you don't find information about something, say: "I don't have documentation about that"
|
|
71
|
+
3. NEVER invent features, buttons, settings, or workflows not documented above
|
|
72
|
+
4. When uncertain, acknowledge it clearly rather than guessing
|
|
73
|
+
|
|
74
|
+
**When Answering:**
|
|
75
|
+
- Search the knowledge base for relevant information first
|
|
76
|
+
- Reference exact page names and UI elements as documented
|
|
77
|
+
- Provide step-by-step instructions when they exist in the guides
|
|
78
|
+
- If user asks about a different page, guide them: "Go to [Page Name] → [Section]"
|
|
79
|
+
- Use tools when available on current page
|
|
80
|
+
- Be concise and accurate - better to say "I'm not sure" than provide wrong information`;
|
|
81
|
+
|
|
82
|
+
return instruction;
|
|
83
|
+
}
|
|
@@ -83,6 +83,7 @@ export const schemas = {
|
|
|
83
83
|
migrate: z.object({
|
|
84
84
|
projectRef: z.string().min(1, 'Project reference is required'),
|
|
85
85
|
accessToken: z.string().min(1, 'Access token is required for automatic migration'),
|
|
86
|
+
anonKey: z.string().optional(), // For knowledge base ingestion during migration
|
|
86
87
|
}),
|
|
87
88
|
|
|
88
89
|
// Rule schemas - supports both single action (legacy) and actions array
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Request, Response, Router } from 'express';
|
|
2
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import { AgentService } from '../services/AgentService.js';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get Supabase client from request headers (BYOK mode) or environment (server mode)
|
|
9
|
+
*/
|
|
10
|
+
function getSupabaseFromRequest(req: Request): SupabaseClient | null {
|
|
11
|
+
// Try request headers first (BYOK mode - credentials from Setup Wizard)
|
|
12
|
+
const headerUrl = req.headers['x-supabase-url'] as string;
|
|
13
|
+
const headerKey = req.headers['x-supabase-anon-key'] as string;
|
|
14
|
+
|
|
15
|
+
if (headerUrl && headerKey) {
|
|
16
|
+
try {
|
|
17
|
+
return createClient(headerUrl, headerKey);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('[Agent API] Failed to create Supabase client from headers:', error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fallback to environment variables (server mode)
|
|
24
|
+
const envUrl = process.env.SUPABASE_URL;
|
|
25
|
+
const envKey = process.env.SUPABASE_ANON_KEY;
|
|
26
|
+
|
|
27
|
+
if (envUrl && envKey) {
|
|
28
|
+
try {
|
|
29
|
+
return createClient(envUrl, envKey);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('[Agent API] Failed to create Supabase client from env:', error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* POST /api/agent/chat
|
|
40
|
+
* Context-aware chat endpoint with optional RAG
|
|
41
|
+
*/
|
|
42
|
+
router.post('/chat', async (req: Request, res: Response) => {
|
|
43
|
+
try {
|
|
44
|
+
const userId = req.headers['x-user-id'] as string;
|
|
45
|
+
const { message, context, history } = req.body;
|
|
46
|
+
|
|
47
|
+
if (!message) {
|
|
48
|
+
return res.status(400).json({ success: false, error: 'Message is required' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get Supabase client from request (BYOK) or environment
|
|
52
|
+
const supabase = getSupabaseFromRequest(req);
|
|
53
|
+
|
|
54
|
+
// If Supabase is available, use RAG-enhanced agent
|
|
55
|
+
if (supabase) {
|
|
56
|
+
console.log('[Agent API] Using RAG-enhanced agent');
|
|
57
|
+
const agentService = new AgentService(supabase);
|
|
58
|
+
|
|
59
|
+
const response = await agentService.chat(
|
|
60
|
+
userId || 'anonymous',
|
|
61
|
+
message,
|
|
62
|
+
context || { page_id: 'global' },
|
|
63
|
+
history || []
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
res.json({
|
|
67
|
+
success: true,
|
|
68
|
+
response
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
// Fallback: Basic agent without RAG
|
|
72
|
+
console.warn('[Agent API] Running without RAG - Supabase not configured');
|
|
73
|
+
|
|
74
|
+
const { SDKService } = await import('../services/SDKService.js');
|
|
75
|
+
const sdk = SDKService.getSDK();
|
|
76
|
+
|
|
77
|
+
if (!sdk) {
|
|
78
|
+
return res.status(503).json({
|
|
79
|
+
success: false,
|
|
80
|
+
error: 'AI service unavailable. Please ensure RealTimeX Desktop is running.'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build system prompt without RAG
|
|
85
|
+
const baseInstruction = context?.system_instruction ||
|
|
86
|
+
"You are a helpful Email Automator Assistant.";
|
|
87
|
+
|
|
88
|
+
let systemPrompt = `${baseInstruction}
|
|
89
|
+
|
|
90
|
+
# Current Context
|
|
91
|
+
- **Current Page**: ${context?.page_id || 'global'}
|
|
92
|
+
- **Page Data**: ${JSON.stringify(context?.data || {}, null, 2)}
|
|
93
|
+
|
|
94
|
+
# Response Instructions
|
|
95
|
+
**When Answering:**
|
|
96
|
+
- Provide helpful guidance based on the context
|
|
97
|
+
- Use tools when available on current page
|
|
98
|
+
- Be concise and actionable`;
|
|
99
|
+
|
|
100
|
+
// Add Tool Instructions if tools are available
|
|
101
|
+
if (context?.tools && context.tools.length > 0) {
|
|
102
|
+
const toolDescs = context.tools.map((t: any) => {
|
|
103
|
+
const params = t.parameters?.properties
|
|
104
|
+
? Object.entries(t.parameters.properties).map(([key, val]: [string, any]) =>
|
|
105
|
+
`${key}: ${val.type}${val.description ? ` (${val.description})` : ''}`
|
|
106
|
+
).join(', ')
|
|
107
|
+
: 'none';
|
|
108
|
+
const required = t.parameters?.required ? ` [Required: ${t.parameters.required.join(', ')}]` : '';
|
|
109
|
+
return `- ${t.name}: ${t.description}\n Parameters: {${params}}${required}`;
|
|
110
|
+
}).join('\n');
|
|
111
|
+
|
|
112
|
+
systemPrompt += `\n\n# AVAILABLE TOOLS\n${toolDescs}\n\n## Tool Usage Rules:
|
|
113
|
+
1. To execute a tool, output the marker <<<ACTION>>> followed by a JSON object on the same line
|
|
114
|
+
2. The JSON MUST have "name" (tool name) and "args" (object with parameters)
|
|
115
|
+
3. Example: <<<ACTION>>>{"name": "send_draft", "args": {"draft_id": "123"}}
|
|
116
|
+
4. Do NOT wrap the JSON in markdown code blocks
|
|
117
|
+
5. Always include all required parameters as specified above
|
|
118
|
+
6. Provide a friendly explanation BEFORE the <<<ACTION>>> marker`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Prepare messages
|
|
122
|
+
const historyMessages = (history || []).slice(-5).map((m: any) => ({
|
|
123
|
+
role: m.role as 'system' | 'user' | 'assistant',
|
|
124
|
+
content: m.content
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const messages = [
|
|
128
|
+
{ role: 'system' as const, content: systemPrompt },
|
|
129
|
+
...historyMessages,
|
|
130
|
+
{ role: 'user' as const, content: message }
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Call SDK
|
|
134
|
+
const { provider, model } = await SDKService.resolveChatProvider({});
|
|
135
|
+
const llmResponse = await sdk.llm.chat(messages, { provider, model });
|
|
136
|
+
|
|
137
|
+
let content = llmResponse.response?.content || "I couldn't generate a response.";
|
|
138
|
+
let action: { name: string; args: any } | undefined = undefined;
|
|
139
|
+
|
|
140
|
+
// Parse Action
|
|
141
|
+
if (content.includes('<<<ACTION>>>')) {
|
|
142
|
+
const parts = content.split('<<<ACTION>>>');
|
|
143
|
+
content = parts[0].trim();
|
|
144
|
+
try {
|
|
145
|
+
let actionJson = parts[1].trim().replace(/```json\s*/g, '').replace(/```\s*/g, '');
|
|
146
|
+
const parsed = JSON.parse(actionJson);
|
|
147
|
+
|
|
148
|
+
if (parsed?.name && typeof parsed.name === 'string') {
|
|
149
|
+
const toolExists = context?.tools?.some((t: any) => t.name === parsed.name);
|
|
150
|
+
if (toolExists) {
|
|
151
|
+
action = { name: parsed.name, args: parsed.args || {} };
|
|
152
|
+
} else {
|
|
153
|
+
content += `\n\n⚠️ Note: Tool "${parsed.name}" not available.`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
content += `\n\n⚠️ Note: I tried to take an action but the format was incorrect.`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
res.json({
|
|
162
|
+
success: true,
|
|
163
|
+
response: {
|
|
164
|
+
content,
|
|
165
|
+
action,
|
|
166
|
+
usage: (llmResponse.response as any)?.metrics || undefined
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
console.error('[Agent API] Chat failed:', error);
|
|
172
|
+
res.status(500).json({
|
|
173
|
+
success: false,
|
|
174
|
+
error: error.message
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export default router;
|
package/api/src/routes/health.ts
CHANGED
|
@@ -38,7 +38,7 @@ router.get('/', async (_req, res) => {
|
|
|
38
38
|
environment: config.nodeEnv,
|
|
39
39
|
services: {
|
|
40
40
|
database: dbStatus,
|
|
41
|
-
llm:
|
|
41
|
+
llm: 'realtimex_sdk', // Managed by RealTimeX SDK
|
|
42
42
|
gmail: config.gmail.clientId ? 'configured' : 'not_configured',
|
|
43
43
|
microsoft: config.microsoft.clientId ? 'configured' : 'not_configured',
|
|
44
44
|
},
|
package/api/src/routes/index.ts
CHANGED
|
@@ -8,11 +8,15 @@ import settingsRoutes from './settings.js';
|
|
|
8
8
|
import emailsRoutes from './emails.js';
|
|
9
9
|
import migrateRoutes from './migrate.js';
|
|
10
10
|
import sdkRoutes from './sdk.js';
|
|
11
|
+
import ttsRoutes from './tts.js';
|
|
12
|
+
import agentRoutes from './agent.js';
|
|
11
13
|
import rulePacksRoutes from './rulePacks.js';
|
|
12
14
|
import draftsRoutes from './drafts.js';
|
|
13
15
|
|
|
16
|
+
|
|
14
17
|
const router = Router();
|
|
15
18
|
|
|
19
|
+
|
|
16
20
|
router.use('/health', healthRoutes);
|
|
17
21
|
router.use('/auth', authRoutes);
|
|
18
22
|
router.use('/sync', syncRoutes);
|
|
@@ -22,7 +26,10 @@ router.use('/settings', settingsRoutes);
|
|
|
22
26
|
router.use('/emails', emailsRoutes);
|
|
23
27
|
router.use('/migrate', migrateRoutes);
|
|
24
28
|
router.use('/sdk', sdkRoutes);
|
|
29
|
+
router.use('/tts', ttsRoutes);
|
|
30
|
+
router.use('/agent', agentRoutes);
|
|
25
31
|
router.use('/rule-packs', rulePacksRoutes);
|
|
26
32
|
router.use('/drafts', draftsRoutes);
|
|
27
33
|
|
|
34
|
+
|
|
28
35
|
export default router;
|
|
@@ -13,7 +13,7 @@ const logger = createLogger('MigrateRoutes');
|
|
|
13
13
|
router.post('/',
|
|
14
14
|
validateBody(schemas.migrate),
|
|
15
15
|
asyncHandler(async (req, res) => {
|
|
16
|
-
const { projectRef, accessToken } = req.body;
|
|
16
|
+
const { projectRef, accessToken, anonKey } = req.body;
|
|
17
17
|
|
|
18
18
|
logger.info('Starting migration', { projectRef });
|
|
19
19
|
|
|
@@ -30,10 +30,17 @@ router.post('/',
|
|
|
30
30
|
|
|
31
31
|
const scriptPath = join(config.scriptsDir, 'migrate.sh');
|
|
32
32
|
|
|
33
|
+
// Construct Supabase URL from project ref
|
|
34
|
+
const supabaseUrl = `https://${projectRef}.supabase.co`;
|
|
35
|
+
|
|
33
36
|
const env = {
|
|
34
37
|
...process.env,
|
|
35
38
|
SUPABASE_PROJECT_ID: projectRef,
|
|
36
39
|
SUPABASE_ACCESS_TOKEN: accessToken || '',
|
|
40
|
+
SUPABASE_URL: supabaseUrl,
|
|
41
|
+
// Note: migrate.sh will fetch SERVICE_ROLE_KEY using access token
|
|
42
|
+
// anonKey provided as fallback if API fetch fails
|
|
43
|
+
SUPABASE_ANON_KEY: anonKey || '',
|
|
37
44
|
SKIP_FUNCTIONS: '0',
|
|
38
45
|
};
|
|
39
46
|
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
} catch (error: any) {
|
|
35
|
+
console.error('[TTS API] Failed to list providers:', error);
|
|
36
|
+
res.status(503).json({
|
|
37
|
+
success: false,
|
|
38
|
+
error: error.message || 'Failed to list TTS providers. Ensure RealTimeX Desktop is running.'
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* POST /api/tts/speak
|
|
45
|
+
* Generate full audio buffer for text
|
|
46
|
+
* Body: { text: string, provider?: string, voice?: string, speed?: number }
|
|
47
|
+
*/
|
|
48
|
+
router.post('/speak', async (req: Request, res: Response) => {
|
|
49
|
+
try {
|
|
50
|
+
const { text, provider, voice, speed, quality } = req.body;
|
|
51
|
+
|
|
52
|
+
console.log('[TTS API] Received TTS request:', { text: text?.substring(0, 50) + '...', provider, voice, speed, quality });
|
|
53
|
+
|
|
54
|
+
if (!text || typeof text !== 'string') {
|
|
55
|
+
return res.status(400).json({
|
|
56
|
+
success: false,
|
|
57
|
+
error: 'Text is required'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sdk = SDKService.getSDK();
|
|
62
|
+
if (!sdk) {
|
|
63
|
+
return res.status(503).json({
|
|
64
|
+
success: false,
|
|
65
|
+
error: 'RealTimeX SDK not available'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check if TTS module is available
|
|
70
|
+
if (!sdk.tts) {
|
|
71
|
+
return res.status(503).json({
|
|
72
|
+
success: false,
|
|
73
|
+
error: 'TTS module not available. Please ensure RealTimeX Desktop is running and updated to v1.2.3+.'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
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
|
+
console.log('[TTS API] Calling SDK with options:', options);
|
|
84
|
+
const audioBuffer = await sdk.tts.speak(text, options);
|
|
85
|
+
|
|
86
|
+
// Return audio as binary
|
|
87
|
+
res.setHeader('Content-Type', 'audio/mpeg');
|
|
88
|
+
res.send(Buffer.from(audioBuffer));
|
|
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
|
+
const options: any = {};
|
|
136
|
+
if (provider) options.provider = provider;
|
|
137
|
+
if (voice) options.voice = voice;
|
|
138
|
+
if (speed) options.speed = parseFloat(speed);
|
|
139
|
+
if (quality) options.num_inference_steps = parseInt(quality);
|
|
140
|
+
|
|
141
|
+
// Send info event
|
|
142
|
+
res.write(`event: info\n`);
|
|
143
|
+
res.write(`data: ${JSON.stringify({ message: 'Starting TTS generation...' })}\n\n`);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Stream chunks
|
|
147
|
+
for await (const chunk of sdk.tts.speakStream(text, options)) {
|
|
148
|
+
// Encode ArrayBuffer to base64
|
|
149
|
+
const base64Audio = Buffer.from(chunk.audio).toString('base64');
|
|
150
|
+
|
|
151
|
+
res.write(`event: chunk\n`);
|
|
152
|
+
res.write(`data: ${JSON.stringify({
|
|
153
|
+
index: chunk.index,
|
|
154
|
+
total: chunk.total,
|
|
155
|
+
audio: base64Audio,
|
|
156
|
+
mimeType: chunk.mimeType
|
|
157
|
+
})}\n\n`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Send done event
|
|
161
|
+
res.write(`event: done\n`);
|
|
162
|
+
res.write(`data: ${JSON.stringify({ message: 'TTS generation complete' })}\n\n`);
|
|
163
|
+
} catch (streamError: any) {
|
|
164
|
+
res.write(`event: error\n`);
|
|
165
|
+
res.write(`data: ${JSON.stringify({ error: streamError.message })}\n\n`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
res.end();
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
console.error('[TTS API] Failed to stream speech:', error);
|
|
171
|
+
|
|
172
|
+
// If headers not sent yet, send JSON error
|
|
173
|
+
if (!res.headersSent) {
|
|
174
|
+
res.status(500).json({
|
|
175
|
+
success: false,
|
|
176
|
+
error: error.message || 'Failed to stream speech'
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
// Send SSE error event
|
|
180
|
+
res.write(`event: error\n`);
|
|
181
|
+
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
|
182
|
+
res.end();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export default router;
|