@openenthrium/oe-runtime-sdk 1.7.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/README.md +139 -0
- package/index.js +58 -0
- package/package.json +64 -0
- package/src/data/connectionTypes.json +18587 -0
- package/src/engine/index.js +183 -0
- package/src/engine/llm.js +71 -0
- package/src/engine/promptBuilder.js +38 -0
- package/src/index.js +156 -0
- package/src/middleware/auth.js +63 -0
- package/src/providers/embedding/index.js +69 -0
- package/src/providers/llm/index.js +136 -0
- package/src/routes/admin.js +686 -0
- package/src/routes/agents.js +193 -0
- package/src/routes/apiKeys.js +55 -0
- package/src/routes/audio.js +111 -0
- package/src/routes/auth.js +83 -0
- package/src/routes/chat.js +381 -0
- package/src/routes/connectors.js +435 -0
- package/src/routes/dashboard.js +244 -0
- package/src/routes/documents.js +443 -0
- package/src/routes/embed.js +103 -0
- package/src/routes/marketplace.js +49 -0
- package/src/routes/models.js +81 -0
- package/src/routes/oauth.js +423 -0
- package/src/routes/projects.js +238 -0
- package/src/routes/settings.js +45 -0
- package/src/routes/setup.js +42 -0
- package/src/routes/sso.js +218 -0
- package/src/routes/superadmin.js +51 -0
- package/src/routes/templates.js +75 -0
- package/src/routes/threads.js +53 -0
- package/src/routes/workspaces.js +464 -0
- package/src/telemetry/bootstrap.js +39 -0
- package/src/telemetry/registration.js +16 -0
- package/src/utils/activityLog.js +16 -0
- package/src/utils/agentChain.js +141 -0
- package/src/utils/buildVisualization.js +102 -0
- package/src/utils/dlpScanner.js +57 -0
- package/src/utils/ingestionQueue.js +240 -0
- package/src/utils/prepareConnectors.js +66 -0
- package/src/utils/rag/_settings.js +6 -0
- package/src/utils/rag/chroma.js +82 -0
- package/src/utils/rag/lancedb.js +103 -0
- package/src/utils/rag/milvus.js +116 -0
- package/src/utils/rag/pgvector.js +104 -0
- package/src/utils/rag/pinecone.js +71 -0
- package/src/utils/rag/qdrant.js +106 -0
- package/src/utils/rag/weaviate.js +137 -0
- package/src/utils/rag/zilliz.js +115 -0
- package/src/utils/scheduler.js +168 -0
- package/src/utils/tier.js +106 -0
- package/src/utils/tools/adapters/_template.js +74 -0
- package/src/utils/tools/adapters/box.js +71 -0
- package/src/utils/tools/adapters/confluence.js +80 -0
- package/src/utils/tools/adapters/database.js +215 -0
- package/src/utils/tools/adapters/dropbox.js +74 -0
- package/src/utils/tools/adapters/elasticsearch.js +90 -0
- package/src/utils/tools/adapters/filesystem.js +197 -0
- package/src/utils/tools/adapters/freshdesk.js +87 -0
- package/src/utils/tools/adapters/gdrive.js +326 -0
- package/src/utils/tools/adapters/github.js +169 -0
- package/src/utils/tools/adapters/gmail.js +157 -0
- package/src/utils/tools/adapters/graphql.js +73 -0
- package/src/utils/tools/adapters/hubspot.js +101 -0
- package/src/utils/tools/adapters/image-gen.js +120 -0
- package/src/utils/tools/adapters/jira.js +86 -0
- package/src/utils/tools/adapters/kafka.js +126 -0
- package/src/utils/tools/adapters/ldap.js +118 -0
- package/src/utils/tools/adapters/mcp-client.js +138 -0
- package/src/utils/tools/adapters/mongodb.js +119 -0
- package/src/utils/tools/adapters/mqtt.js +106 -0
- package/src/utils/tools/adapters/music-gen.js +118 -0
- package/src/utils/tools/adapters/notion.js +93 -0
- package/src/utils/tools/adapters/ocr.js +107 -0
- package/src/utils/tools/adapters/onedrive.js +72 -0
- package/src/utils/tools/adapters/redis.js +104 -0
- package/src/utils/tools/adapters/rest-api.js +119 -0
- package/src/utils/tools/adapters/s3.js +142 -0
- package/src/utils/tools/adapters/search.js +80 -0
- package/src/utils/tools/adapters/sftp.js +121 -0
- package/src/utils/tools/adapters/shell.js +97 -0
- package/src/utils/tools/adapters/slack.js +83 -0
- package/src/utils/tools/adapters/speech.js +160 -0
- package/src/utils/tools/adapters/ssh.js +113 -0
- package/src/utils/tools/adapters/video-gen.js +152 -0
- package/src/utils/tools/adapters/web3.js +111 -0
- package/src/utils/tools/adapters/zendesk.js +82 -0
- package/src/utils/tools/adapters/zoho-mail.js +246 -0
- package/src/utils/tools/registry.js +229 -0
- package/src/utils/vectorStore.js +68 -0
- package/src/utils/workflowEngine.js +4 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate } = require("../middleware/auth");
|
|
3
|
+
const { similaritySearch } = require("../utils/vectorStore");
|
|
4
|
+
const { getLLMClient, getSetting } = require("../providers/llm");
|
|
5
|
+
|
|
6
|
+
function normalizeRefusal(response, refusalMsg) {
|
|
7
|
+
const trimmed = (response || "").trim();
|
|
8
|
+
if (trimmed.length > 300) return response;
|
|
9
|
+
const lower = trimmed.toLowerCase();
|
|
10
|
+
const indicators = [
|
|
11
|
+
"there is no relevant information in this workspace",
|
|
12
|
+
"there's no relevant information in this workspace",
|
|
13
|
+
"no relevant information in this workspace",
|
|
14
|
+
"i'm sorry, i can't assist",
|
|
15
|
+
"i'm sorry, i cannot assist",
|
|
16
|
+
"i cannot assist with that",
|
|
17
|
+
"i can't assist with that",
|
|
18
|
+
"i'm unable to assist with that",
|
|
19
|
+
"i don't have relevant information",
|
|
20
|
+
"i'm sorry, i don't have",
|
|
21
|
+
];
|
|
22
|
+
return indicators.some(ind => lower.includes(ind)) ? refusalMsg : response;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function resolveThreadId(db, threadUid) {
|
|
26
|
+
if (!threadUid) return null;
|
|
27
|
+
const thread = await db.thread.findUnique({ where: { uid: threadUid } });
|
|
28
|
+
return thread?.id ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── GET history ───────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
router.get("/:slug/history", authenticate, async (req, res) => {
|
|
34
|
+
const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
|
|
35
|
+
if (!workspace) return res.status(404).json({ error: "Workspace not found" });
|
|
36
|
+
|
|
37
|
+
const threadId = await resolveThreadId(req.db, req.query.threadId || null);
|
|
38
|
+
const chats = await req.db.chat.findMany({
|
|
39
|
+
where: { workspaceId: workspace.id, threadId },
|
|
40
|
+
orderBy: { createdAt: "desc" },
|
|
41
|
+
take: 100
|
|
42
|
+
});
|
|
43
|
+
res.json({
|
|
44
|
+
messages: chats.reverse().map(c => ({
|
|
45
|
+
...c,
|
|
46
|
+
sources: c.sources ? JSON.parse(c.sources) : [],
|
|
47
|
+
}))
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── POST chat ─────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
router.post("/:slug", authenticate, async (req, res) => {
|
|
54
|
+
const { message: _message, threadId: threadUid, bypassDlp } = req.body;
|
|
55
|
+
let message = _message;
|
|
56
|
+
if (!message) return res.status(400).json({ error: "Message required" });
|
|
57
|
+
|
|
58
|
+
const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
|
|
59
|
+
if (!workspace) return res.status(404).json({ error: "Workspace not found" });
|
|
60
|
+
|
|
61
|
+
const threadId = await resolveThreadId(req.db, threadUid || null);
|
|
62
|
+
const temperature = workspace.temperature ?? 0.7;
|
|
63
|
+
const historyLimit = (workspace.chatHistory ?? 20) * 2;
|
|
64
|
+
|
|
65
|
+
const chatUserId = req.user.id === 0 ? null : req.user.id;
|
|
66
|
+
if (!bypassDlp) {
|
|
67
|
+
await req.db.chat.create({
|
|
68
|
+
data: { workspaceId: workspace.id, threadId, userId: chatUserId, role: "user", content: message }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── RAG retrieval ─────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const topK = parseInt((await getSetting("rag_top_k")) || "15");
|
|
75
|
+
const kbShares = await req.db.workspaceKBShare.findMany({
|
|
76
|
+
where: { targetWorkspaceId: workspace.id },
|
|
77
|
+
include: { sourceWorkspace: { select: { slug: true, name: true } } },
|
|
78
|
+
});
|
|
79
|
+
const [ownSources, ...sharedResultArrays] = await Promise.all([
|
|
80
|
+
similaritySearch(workspace.slug, message, topK),
|
|
81
|
+
...kbShares.map(s =>
|
|
82
|
+
similaritySearch(s.sourceWorkspace.slug, message, topK).catch(err => {
|
|
83
|
+
console.error(`[KB Share] search failed for "${s.sourceWorkspace.slug}":`, err.message);
|
|
84
|
+
return [];
|
|
85
|
+
})
|
|
86
|
+
),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const taggedOwn = ownSources.map(s => ({ ...s, _wsName: null }));
|
|
90
|
+
const taggedSharedGroups = sharedResultArrays.map((arr, i) =>
|
|
91
|
+
arr.map(s => ({ ...s, _wsName: kbShares[i].sourceWorkspace.name }))
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
let sources;
|
|
95
|
+
if (taggedSharedGroups.length === 0) {
|
|
96
|
+
sources = taggedOwn.sort((a, b) => (b.score || 0) - (a.score || 0)).slice(0, topK);
|
|
97
|
+
} else {
|
|
98
|
+
const numSources = 1 + taggedSharedGroups.length;
|
|
99
|
+
const baseSlots = Math.floor(topK / numSources);
|
|
100
|
+
const extraSlots = topK - baseSlots * numSources;
|
|
101
|
+
const byScore = arr => [...arr].sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
102
|
+
const ownSorted = byScore(taggedOwn);
|
|
103
|
+
const sharedSorted = taggedSharedGroups.map(byScore);
|
|
104
|
+
const guaranteed = [
|
|
105
|
+
...ownSorted.slice(0, baseSlots),
|
|
106
|
+
...sharedSorted.flatMap(arr => arr.slice(0, baseSlots)),
|
|
107
|
+
];
|
|
108
|
+
const remaining = [
|
|
109
|
+
...ownSorted.slice(baseSlots),
|
|
110
|
+
...sharedSorted.flatMap(arr => arr.slice(baseSlots)),
|
|
111
|
+
].sort((a, b) => (b.score || 0) - (a.score || 0)).slice(0, extraSlots);
|
|
112
|
+
sources = [...guaranteed, ...remaining].sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const context = sources.map((s, i) => {
|
|
116
|
+
const label = s._wsName ? `[Source ${i + 1} from "${s._wsName}"]` : `[Source ${i + 1}]`;
|
|
117
|
+
return `${label}: ${s.text}`;
|
|
118
|
+
}).join("\n\n");
|
|
119
|
+
|
|
120
|
+
// ── History ───────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const history = await req.db.chat.findMany({
|
|
123
|
+
where: { workspaceId: workspace.id, threadId },
|
|
124
|
+
orderBy: { createdAt: "desc" },
|
|
125
|
+
take: historyLimit
|
|
126
|
+
});
|
|
127
|
+
const skipCount = bypassDlp === "warn" ? 2 : 1;
|
|
128
|
+
const historyMessages = history.reverse().slice(0, -skipCount).reduce((acc, c) => {
|
|
129
|
+
let content = c.content || "";
|
|
130
|
+
if (c.role === "assistant") {
|
|
131
|
+
if (content.startsWith("🚫 **Your message was blocked**") ||
|
|
132
|
+
content.startsWith("⚠️ **Security Warning:**")) return acc;
|
|
133
|
+
for (const prefix of ["> 🔒 **Redaction Notice:**", "> 📋 **Audit Notice:**"]) {
|
|
134
|
+
if (content.startsWith(prefix)) {
|
|
135
|
+
const split = content.indexOf("\n\n");
|
|
136
|
+
if (split !== -1) content = content.slice(split + 2).trim();
|
|
137
|
+
if (!content) return acc;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
acc.push({ role: c.role, content });
|
|
143
|
+
return acc;
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
// ── DLP scan ──────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
let dlpNoticePrefix = "";
|
|
149
|
+
if (!bypassDlp) {
|
|
150
|
+
const { scanMessage } = require("../utils/dlpScanner");
|
|
151
|
+
const dlpPolicies = workspace.dlpEnabled
|
|
152
|
+
? await req.db.dlpPolicy.findMany({ where: { enabled: true } })
|
|
153
|
+
: [];
|
|
154
|
+
if (dlpPolicies.length > 0) {
|
|
155
|
+
const { blocked, violations, redactedText } = scanMessage(message, dlpPolicies);
|
|
156
|
+
|
|
157
|
+
for (const v of violations) {
|
|
158
|
+
await req.db.dlpViolation.create({
|
|
159
|
+
data: {
|
|
160
|
+
policyId: v.policyId,
|
|
161
|
+
policyName: v.policyName,
|
|
162
|
+
action: v.action,
|
|
163
|
+
userId: req.user?.id || null,
|
|
164
|
+
userEmail: req.user?.email || null,
|
|
165
|
+
workspaceId: workspace.id || null,
|
|
166
|
+
workspaceName: workspace.name || null,
|
|
167
|
+
snippet: v.snippet,
|
|
168
|
+
}
|
|
169
|
+
}).catch(e => console.error("[DLP] violation insert failed:", e.message));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const sseHeaders = () => {
|
|
173
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
174
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
175
|
+
res.setHeader("Connection", "keep-alive");
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (blocked) {
|
|
179
|
+
const names = [...new Set(violations.filter(v => v.action === "block").map(v => v.policyName))].join(", ");
|
|
180
|
+
const blockMsg = `🚫 **Your message was blocked** by your organization's security policy (${names}). Please remove sensitive content and try again.`;
|
|
181
|
+
try {
|
|
182
|
+
await req.db.chat.create({ data: { workspaceId: workspace.id, threadId, userId: chatUserId, role: "assistant", content: blockMsg } });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.error("[DLP] blocked message save failed:", e.message);
|
|
185
|
+
}
|
|
186
|
+
sseHeaders();
|
|
187
|
+
res.write(`data: ${JSON.stringify({ chunk: blockMsg })}\n\n`);
|
|
188
|
+
res.write(`data: ${JSON.stringify({ done: true, sources: [] })}\n\n`);
|
|
189
|
+
return res.end();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const redactHits = violations.filter(v => v.action === "redact");
|
|
193
|
+
if (redactHits.length > 0) {
|
|
194
|
+
message = redactedText;
|
|
195
|
+
const names = [...new Set(redactHits.map(v => v.policyName))].join(", ");
|
|
196
|
+
dlpNoticePrefix = `> 🔒 **Redaction Notice:** Sensitive content was detected by the "${names}" policy. Your message has been redacted and logged before sending to the AI.\n\n`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const warnHits = violations.filter(v => v.action === "warn");
|
|
200
|
+
if (warnHits.length > 0) {
|
|
201
|
+
const names = [...new Set(warnHits.map(v => v.policyName))];
|
|
202
|
+
const warnMsg = `⚠️ **Security Warning:** Your message was flagged by the "${names.join('", "')}" policy and has been logged. You can choose to send it anyway or cancel.`;
|
|
203
|
+
await req.db.chat.create({ data: { workspaceId: workspace.id, threadId, userId: chatUserId, role: "assistant", content: warnMsg } }).catch(() => {});
|
|
204
|
+
sseHeaders();
|
|
205
|
+
res.write(`data: ${JSON.stringify({ dlp: { action: "warn", policyNames: names, message: warnMsg } })}\n\n`);
|
|
206
|
+
res.write(`data: ${JSON.stringify({ done: true, sources: [] })}\n\n`);
|
|
207
|
+
return res.end();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const auditHits = violations.filter(v => v.action === "audit");
|
|
211
|
+
if (auditHits.length > 0) {
|
|
212
|
+
const names = [...new Set(auditHits.map(v => v.policyName))].join(", ");
|
|
213
|
+
dlpNoticePrefix = `> 📋 **Audit Notice:** Your message was logged for compliance (${names}).\n\n`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── System prompt ─────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
const refusalMsg = workspace.queryRefusalResponse ||
|
|
221
|
+
"There is no relevant information in this workspace to answer your query.";
|
|
222
|
+
|
|
223
|
+
const basePrompt = workspace.systemPrompt ||
|
|
224
|
+
`You are a knowledgeable AI assistant for this workspace.
|
|
225
|
+
|
|
226
|
+
CAPABILITIES:
|
|
227
|
+
1. INGESTED DOCUMENT CONTEXT (highest priority)
|
|
228
|
+
- The Document Context section below contains text extracted from files uploaded or ingested into this workspace, including files shared from other workspaces.
|
|
229
|
+
- Sources labelled [Source N] are from this workspace. Sources labelled [Source N from "Workspace Name"] are from a shared workspace called "Workspace Name".
|
|
230
|
+
- Always answer from this context first. Present all relevant data, tables, and details exactly as found. Do not summarise or truncate.
|
|
231
|
+
- If the context contains a partial dataset (e.g. sample rows from a CSV), present what is available and note it may be a sample.
|
|
232
|
+
|
|
233
|
+
STRICT RULES:
|
|
234
|
+
- Answer ONLY from the provided context. Do NOT use general knowledge or outside information.
|
|
235
|
+
- Provide thorough, complete, and well-structured answers.
|
|
236
|
+
- Include all relevant details, steps, lists, or tables present in the source documents.
|
|
237
|
+
- If a process has multiple steps, list every step in full.
|
|
238
|
+
- Use bullet points, numbered lists, or markdown tables when the answer contains multiple items.
|
|
239
|
+
- Only respond with the refusal message if there is genuinely no relevant information in the context.`;
|
|
240
|
+
|
|
241
|
+
const docSection = context.length > 0
|
|
242
|
+
? `\n\n--- Document Context ---\n${context}\n--- End of Context ---\n\nThe context above contains the relevant data from ingested files. Present all data, tables, lists, or information from it that helps answer the question. Only use the response "${refusalMsg}" if the context contains absolutely no information related to the question.`
|
|
243
|
+
: `\n\nNo documents found in this workspace. Respond with: "${refusalMsg}"`;
|
|
244
|
+
|
|
245
|
+
const guardrail = `STRICT OUTPUT CONSTRAINT: You are a workspace-scoped assistant. You MUST follow these rules above everything else:
|
|
246
|
+
1. Answer ONLY from the Document Context provided below.
|
|
247
|
+
2. If the answer is not in the context, respond with ONLY this exact text: "${refusalMsg}" — nothing else.
|
|
248
|
+
3. Do NOT generate safety warnings, disclaimers, privacy advice, or any content not found in the context.
|
|
249
|
+
4. Do NOT use phrases like "I'm sorry", "I can't assist", "I should warn you", or any variation. Your only allowed non-answer is: "${refusalMsg}"
|
|
250
|
+
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
const systemPrompt = guardrail + basePrompt + docSection;
|
|
254
|
+
|
|
255
|
+
// ── Stream response ───────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
let fullResponse = "";
|
|
258
|
+
let inputTokens = 0;
|
|
259
|
+
let outputTokens = 0;
|
|
260
|
+
let clientGone = false;
|
|
261
|
+
req.on("close", () => { clientGone = true; });
|
|
262
|
+
|
|
263
|
+
function safeWrite(data) {
|
|
264
|
+
if (clientGone) return;
|
|
265
|
+
try { res.write(data); } catch { clientGone = true; }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
270
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
271
|
+
res.setHeader("Connection", "keep-alive");
|
|
272
|
+
|
|
273
|
+
if (dlpNoticePrefix) {
|
|
274
|
+
safeWrite(`data: ${JSON.stringify({ chunk: dlpNoticePrefix })}\n\n`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { provider, client } = await getLLMClient();
|
|
278
|
+
const model = (await getSetting("llm_model")) || process.env.OPENAI_MODEL || process.env.OLLAMA_MODEL || "gpt-4o";
|
|
279
|
+
|
|
280
|
+
if (provider === "anthropic") {
|
|
281
|
+
const stream = await client.messages.create({
|
|
282
|
+
model: model || "claude-sonnet-4-6",
|
|
283
|
+
max_tokens: 4096,
|
|
284
|
+
system: systemPrompt,
|
|
285
|
+
messages: [...historyMessages, { role: "user", content: message }],
|
|
286
|
+
temperature,
|
|
287
|
+
stream: true
|
|
288
|
+
});
|
|
289
|
+
for await (const event of stream) {
|
|
290
|
+
if (event.type === "message_start") inputTokens = event.message?.usage?.input_tokens || 0;
|
|
291
|
+
if (event.type === "message_delta") outputTokens = event.usage?.output_tokens || 0;
|
|
292
|
+
if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
|
293
|
+
const chunk = event.delta.text;
|
|
294
|
+
fullResponse += chunk;
|
|
295
|
+
safeWrite(`data: ${JSON.stringify({ chunk })}\n\n`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
const stream = await client.chat.completions.create({
|
|
300
|
+
model,
|
|
301
|
+
messages: [{ role: "system", content: systemPrompt }, ...historyMessages, { role: "user", content: message }],
|
|
302
|
+
temperature,
|
|
303
|
+
max_tokens: 4096,
|
|
304
|
+
stream: true,
|
|
305
|
+
stream_options: { include_usage: true }
|
|
306
|
+
});
|
|
307
|
+
for await (const part of stream) {
|
|
308
|
+
if (part.usage) {
|
|
309
|
+
inputTokens = part.usage.prompt_tokens || 0;
|
|
310
|
+
outputTokens = part.usage.completion_tokens || 0;
|
|
311
|
+
}
|
|
312
|
+
const chunk = part.choices?.[0]?.delta?.content || "";
|
|
313
|
+
if (chunk) {
|
|
314
|
+
fullResponse += chunk;
|
|
315
|
+
safeWrite(`data: ${JSON.stringify({ chunk })}\n\n`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!inputTokens && !outputTokens && fullResponse) {
|
|
319
|
+
inputTokens = Math.ceil((systemPrompt.length + message.length) / 4);
|
|
320
|
+
outputTokens = Math.ceil(fullResponse.length / 4);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Save & done ───────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
const sourcesData = sources.length > 0
|
|
327
|
+
? JSON.stringify(sources.map(s => ({ text: s.text.slice(0, 200), metadata: s.metadata })))
|
|
328
|
+
: null;
|
|
329
|
+
|
|
330
|
+
const normalizedFull = normalizeRefusal(fullResponse, refusalMsg);
|
|
331
|
+
const wasNormalized = normalizedFull !== fullResponse;
|
|
332
|
+
if (wasNormalized) fullResponse = normalizedFull;
|
|
333
|
+
|
|
334
|
+
await req.db.chat.create({
|
|
335
|
+
data: {
|
|
336
|
+
workspaceId: workspace.id,
|
|
337
|
+
threadId,
|
|
338
|
+
userId: chatUserId,
|
|
339
|
+
role: "assistant",
|
|
340
|
+
content: dlpNoticePrefix + fullResponse,
|
|
341
|
+
sources: sourcesData,
|
|
342
|
+
inputTokens,
|
|
343
|
+
outputTokens,
|
|
344
|
+
model
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (!clientGone) {
|
|
349
|
+
const doneEvt = { done: true, sources };
|
|
350
|
+
if (wasNormalized) doneEvt.content = dlpNoticePrefix + fullResponse;
|
|
351
|
+
res.write(`data: ${JSON.stringify(doneEvt)}\n\n`);
|
|
352
|
+
res.end();
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
console.error("Chat error:", err.message);
|
|
356
|
+
if (!clientGone) {
|
|
357
|
+
try {
|
|
358
|
+
res.write(`data: ${JSON.stringify({ error: "Failed to get response. Check your LLM configuration." })}\n\n`);
|
|
359
|
+
res.end();
|
|
360
|
+
} catch { /* client already gone */ }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ── DELETE history ────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
router.delete("/:slug/history", authenticate, async (req, res) => {
|
|
368
|
+
const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
|
|
369
|
+
if (!workspace) return res.status(404).json({ error: "Workspace not found" });
|
|
370
|
+
|
|
371
|
+
const where = { workspaceId: workspace.id };
|
|
372
|
+
if (req.query.threadId === "none") {
|
|
373
|
+
where.threadId = null;
|
|
374
|
+
} else if (req.query.threadId) {
|
|
375
|
+
where.threadId = parseInt(req.query.threadId);
|
|
376
|
+
}
|
|
377
|
+
await req.db.chat.deleteMany({ where });
|
|
378
|
+
res.json({ success: true });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
module.exports = router;
|