@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,183 @@
|
|
|
1
|
+
const { createLLMClient } = require("./llm");
|
|
2
|
+
const { buildSystemPrompt, applyParams } = require("./promptBuilder");
|
|
3
|
+
const { getToolDefinitions, getAnthropicToolDefinitions, executeTool } = require("../utils/tools/registry");
|
|
4
|
+
|
|
5
|
+
function conditionMet(condition, output) {
|
|
6
|
+
switch (condition) {
|
|
7
|
+
case "always": return true;
|
|
8
|
+
case "on_success": return true;
|
|
9
|
+
case "on_critical": return /CRITICAL|🚨/i.test(output || "");
|
|
10
|
+
case "on_warning": return /WARNING|CRITICAL|⚠️|🚨/i.test(output || "");
|
|
11
|
+
default: return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function friendlyToolName(rawName, connectors) {
|
|
16
|
+
const connId = rawName.match(/^conn_(\d+)_/)?.[1];
|
|
17
|
+
if (connId) {
|
|
18
|
+
const conn = (connectors || []).find(c => c.id === parseInt(connId));
|
|
19
|
+
return conn ? conn.name : rawName.replace(/^conn_\d+_/, "");
|
|
20
|
+
}
|
|
21
|
+
return rawName.replace(/_/g, " ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run an agent. Pure function — no Prisma, no Express, no SSE.
|
|
26
|
+
* All side-effects go through hooks.
|
|
27
|
+
*
|
|
28
|
+
* agentSpec: {
|
|
29
|
+
* systemPrompt: string,
|
|
30
|
+
* workflow: [{ name, content }], ← parsed array
|
|
31
|
+
* params: [{ name, default }],
|
|
32
|
+
* paramValues: object,
|
|
33
|
+
* maxRounds: number,
|
|
34
|
+
* input: string,
|
|
35
|
+
* appendToPrompt: string,
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* llmConfig: { provider, apiKey, model, baseURL?, azureEndpoint?, azureDeployment? }
|
|
39
|
+
*
|
|
40
|
+
* connectors: [{ id, name, type, config, authConfig, ... }]
|
|
41
|
+
*
|
|
42
|
+
* hooks: {
|
|
43
|
+
* onToolCall(friendlyName): called each time a tool fires
|
|
44
|
+
* onDone(output, toolCallNames): called on clean completion
|
|
45
|
+
* onError(err): called on failure
|
|
46
|
+
* checkCancel(): async fn, return true to abort mid-run
|
|
47
|
+
* toolExecutor(name, args, conns): optional override — use to inject db in web layer
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* Returns: { output, toolCalls }
|
|
51
|
+
*/
|
|
52
|
+
async function run(agentSpec, llmConfig, connectors, hooks = {}) {
|
|
53
|
+
const { onToolCall, onDone, onError, checkCancel } = hooks;
|
|
54
|
+
|
|
55
|
+
// Allow caller to inject db-aware tool executor (web layer);
|
|
56
|
+
// fall back to registry directly with null db (CLI / standalone).
|
|
57
|
+
const execTool = hooks.toolExecutor
|
|
58
|
+
|| ((name, args, conns) => executeTool(name, args, conns, null));
|
|
59
|
+
|
|
60
|
+
// Build system prompt
|
|
61
|
+
const basePrompt = buildSystemPrompt(agentSpec);
|
|
62
|
+
let systemPrompt = applyParams(basePrompt, agentSpec.params || [], agentSpec.paramValues);
|
|
63
|
+
if (agentSpec.appendToPrompt) systemPrompt += "\n\n" + agentSpec.appendToPrompt;
|
|
64
|
+
|
|
65
|
+
const userMessage = agentSpec.input?.trim() || "Execute the agent task now using the available tools.";
|
|
66
|
+
const MAX_ROUNDS = agentSpec.maxRounds || 25;
|
|
67
|
+
const hasWorkflow = (agentSpec.workflow || []).length > 0;
|
|
68
|
+
|
|
69
|
+
const { provider, client, model } = await createLLMClient(llmConfig);
|
|
70
|
+
|
|
71
|
+
let fullOutput = "";
|
|
72
|
+
const allToolCalls = [];
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (provider === "anthropic") {
|
|
76
|
+
const tools = getAnthropicToolDefinitions(connectors);
|
|
77
|
+
const msgs = [{ role: "user", content: userMessage }];
|
|
78
|
+
let madeToolCall = false;
|
|
79
|
+
|
|
80
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
81
|
+
const resp = await client.messages.create({
|
|
82
|
+
model: model || "claude-sonnet-4-6",
|
|
83
|
+
max_tokens: 4096,
|
|
84
|
+
system: systemPrompt,
|
|
85
|
+
messages: msgs,
|
|
86
|
+
tools: tools.length ? tools : undefined,
|
|
87
|
+
temperature: 0.3,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (resp.stop_reason !== "tool_use" || !tools.length) {
|
|
91
|
+
if (hasWorkflow && tools.length && !madeToolCall && round === 0) {
|
|
92
|
+
msgs.push({ role: "assistant", content: resp.content });
|
|
93
|
+
msgs.push({ role: "user", content: "Do not plan or summarise. Call your first tool RIGHT NOW." });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
fullOutput = resp.content?.find(b => b.type === "text")?.text || "";
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
madeToolCall = true;
|
|
101
|
+
const toolUseBlocks = resp.content.filter(b => b.type === "tool_use");
|
|
102
|
+
|
|
103
|
+
for (const tb of toolUseBlocks) {
|
|
104
|
+
const name = friendlyToolName(tb.name, connectors);
|
|
105
|
+
allToolCalls.push(name);
|
|
106
|
+
onToolCall?.(name);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
msgs.push({ role: "assistant", content: resp.content });
|
|
110
|
+
const toolResults = [];
|
|
111
|
+
for (const tb of toolUseBlocks) {
|
|
112
|
+
const result = await execTool(tb.name, tb.input, connectors);
|
|
113
|
+
hooks.onToolResult?.(friendlyToolName(tb.name, connectors), String(result));
|
|
114
|
+
toolResults.push({ type: "tool_result", tool_use_id: tb.id, content: String(result) });
|
|
115
|
+
}
|
|
116
|
+
msgs.push({ role: "user", content: toolResults });
|
|
117
|
+
|
|
118
|
+
if (checkCancel && await checkCancel()) { fullOutput = "[Run cancelled by user]"; break; }
|
|
119
|
+
|
|
120
|
+
if (round === MAX_ROUNDS - 1) {
|
|
121
|
+
const fr = await client.messages.create({
|
|
122
|
+
model: model || "claude-sonnet-4-6", max_tokens: 4096,
|
|
123
|
+
system: systemPrompt, messages: msgs, temperature: 0.3,
|
|
124
|
+
});
|
|
125
|
+
fullOutput = fr.content?.find(b => b.type === "text")?.text || "";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
} else {
|
|
130
|
+
const tools = getToolDefinitions(connectors);
|
|
131
|
+
const messages = [{ role: "system", content: systemPrompt }, { role: "user", content: userMessage }];
|
|
132
|
+
let madeToolCall = false;
|
|
133
|
+
|
|
134
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
135
|
+
const reqBody = { model, messages, temperature: 0.3, max_tokens: 4096 };
|
|
136
|
+
if (tools.length) { reqBody.tools = tools; reqBody.tool_choice = "auto"; }
|
|
137
|
+
|
|
138
|
+
const resp = await client.chat.completions.create(reqBody);
|
|
139
|
+
const choice = resp.choices[0];
|
|
140
|
+
|
|
141
|
+
if (choice.finish_reason !== "tool_calls" || !tools.length) {
|
|
142
|
+
if (hasWorkflow && tools.length && !madeToolCall && round === 0) {
|
|
143
|
+
messages.push(choice.message);
|
|
144
|
+
messages.push({ role: "user", content: "Do not plan or summarise. Call your first tool RIGHT NOW." });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
fullOutput = choice.message.content || "";
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
madeToolCall = true;
|
|
152
|
+
messages.push(choice.message);
|
|
153
|
+
|
|
154
|
+
for (const tc of choice.message.tool_calls || []) {
|
|
155
|
+
let args = {};
|
|
156
|
+
try { args = JSON.parse(tc.function.arguments); } catch { /**/ }
|
|
157
|
+
const name = friendlyToolName(tc.function.name, connectors);
|
|
158
|
+
allToolCalls.push(name);
|
|
159
|
+
onToolCall?.(name);
|
|
160
|
+
const result = await execTool(tc.function.name, args, connectors);
|
|
161
|
+
hooks.onToolResult?.(name, String(result));
|
|
162
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: String(result) });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (checkCancel && await checkCancel()) { fullOutput = "[Run cancelled by user]"; break; }
|
|
166
|
+
|
|
167
|
+
if (round === MAX_ROUNDS - 1) {
|
|
168
|
+
const fr = await client.chat.completions.create({ model, messages, temperature: 0.3, max_tokens: 4096 });
|
|
169
|
+
fullOutput = fr.choices[0].message.content || "";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onDone?.(fullOutput, allToolCalls);
|
|
175
|
+
return { output: fullOutput, toolCalls: allToolCalls };
|
|
176
|
+
|
|
177
|
+
} catch (err) {
|
|
178
|
+
onError?.(err);
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { run, conditionMet };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const OPENAI_COMPATIBLE = {
|
|
2
|
+
groq: { baseURL: "https://api.groq.com/openai/v1" },
|
|
3
|
+
togetherai: { baseURL: "https://api.together.xyz/v1" },
|
|
4
|
+
mistral: { baseURL: "https://api.mistral.ai/v1" },
|
|
5
|
+
openrouter: { baseURL: "https://openrouter.ai/api/v1" },
|
|
6
|
+
perplexity: { baseURL: "https://api.perplexity.ai" },
|
|
7
|
+
deepseek: { baseURL: "https://api.deepseek.com/v1" },
|
|
8
|
+
xai: { baseURL: "https://api.x.ai/v1" },
|
|
9
|
+
fireworks: { baseURL: "https://api.fireworks.ai/inference/v1" },
|
|
10
|
+
gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/" },
|
|
11
|
+
nvidiaNim: { baseURL: "https://integrate.api.nvidia.com/v1" },
|
|
12
|
+
sambanova: { baseURL: "https://api.sambanova.ai/v1" },
|
|
13
|
+
litellm: { baseURL: null, apiKey: "no-key", localDefault: "http://localhost:4000" },
|
|
14
|
+
ollama: { baseURL: null, apiKey: "ollama", localDefault: "http://localhost:11434/v1" },
|
|
15
|
+
lmstudio: { baseURL: null, apiKey: "lmstudio", localDefault: "http://localhost:1234/v1" },
|
|
16
|
+
"generic-openai": { baseURL: null, localDefault: "" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create an LLM client from a plain config object — no Prisma, no DB.
|
|
21
|
+
*
|
|
22
|
+
* config: { provider, apiKey, model, baseURL?, azureEndpoint?, azureDeployment? }
|
|
23
|
+
* Returns: { provider, client, model }
|
|
24
|
+
*/
|
|
25
|
+
async function createLLMClient(config) {
|
|
26
|
+
const openaiPkg = require("openai");
|
|
27
|
+
const OpenAI = openaiPkg.OpenAI || openaiPkg.default || openaiPkg;
|
|
28
|
+
const provider = config.provider || "openai";
|
|
29
|
+
const model = config.model || "gpt-4o";
|
|
30
|
+
|
|
31
|
+
if (provider === "azure") {
|
|
32
|
+
const endpoint = config.azureEndpoint || process.env.AZURE_OPENAI_ENDPOINT;
|
|
33
|
+
const deployment = config.azureDeployment || process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
34
|
+
const key = config.apiKey || process.env.AZURE_OPENAI_API_KEY;
|
|
35
|
+
return {
|
|
36
|
+
provider: "azure", model,
|
|
37
|
+
client: new OpenAI({
|
|
38
|
+
apiKey: key,
|
|
39
|
+
baseURL: `${endpoint}/openai/deployments/${deployment}`,
|
|
40
|
+
defaultQuery: { "api-version": "2024-08-01-preview" },
|
|
41
|
+
defaultHeaders: { "api-key": key },
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (provider === "anthropic") {
|
|
47
|
+
const Anthropic = require("@anthropic-ai/sdk");
|
|
48
|
+
return {
|
|
49
|
+
provider: "anthropic", model,
|
|
50
|
+
client: new Anthropic({ apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (provider === "openai") {
|
|
55
|
+
return {
|
|
56
|
+
provider: "openai", model,
|
|
57
|
+
client: new OpenAI({ apiKey: config.apiKey || process.env.OPENAI_API_KEY }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const compat = OPENAI_COMPATIBLE[provider];
|
|
62
|
+
if (compat) {
|
|
63
|
+
const baseURL = config.baseURL || compat.baseURL || compat.localDefault;
|
|
64
|
+
const key = config.apiKey || compat.apiKey || "no-key";
|
|
65
|
+
return { provider, model, client: new OpenAI({ apiKey: key, baseURL }) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new Error(`Unknown LLM provider: ${provider}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { createLLMClient, OPENAI_COMPATIBLE };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the full system prompt from an agentSpec or a DB agent object.
|
|
3
|
+
*
|
|
4
|
+
* Accepts workflow as either:
|
|
5
|
+
* - a parsed array [{ name, content }] (agentSpec from CLI / engine)
|
|
6
|
+
* - a JSON string "[{...}]" (DB agent from Platform routes)
|
|
7
|
+
*/
|
|
8
|
+
function buildSystemPrompt(agentSpec) {
|
|
9
|
+
const raw = agentSpec.workflow ?? agentSpec.steps ?? [];
|
|
10
|
+
const steps = typeof raw === "string" ? JSON.parse(raw || "[]") : raw;
|
|
11
|
+
|
|
12
|
+
const todayDate = new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
|
13
|
+
const dateHeader = `SYSTEM: Today's date is ${todayDate}. Use this exact date whenever instructions say "today's date" or "TODAY".`;
|
|
14
|
+
|
|
15
|
+
if (!steps.length) return dateHeader + "\n\n" + (agentSpec.systemPrompt || "");
|
|
16
|
+
|
|
17
|
+
const parts = [dateHeader];
|
|
18
|
+
if (agentSpec.systemPrompt?.trim()) parts.push(agentSpec.systemPrompt.trim());
|
|
19
|
+
|
|
20
|
+
steps.forEach((step, i) => {
|
|
21
|
+
const header = `step_${i + 1}: — ${step.name || `Step ${i + 1}`}`;
|
|
22
|
+
const body = step.content?.trim() || "";
|
|
23
|
+
parts.push(body ? `${header}\n\n${body}` : header);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return parts.join("\n\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applyParams(template, paramDefs, paramValues) {
|
|
30
|
+
let result = template || "";
|
|
31
|
+
for (const p of (paramDefs || [])) {
|
|
32
|
+
const val = String(paramValues?.[p.name] ?? p.default ?? "");
|
|
33
|
+
result = result.split(`{{${p.name}}}`).join(val);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { buildSystemPrompt, applyParams };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require("dotenv").config();
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const cors = require("cors");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { PrismaClient } = require("@prisma/client");
|
|
7
|
+
|
|
8
|
+
const { version } = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../package.json"), "utf8"));
|
|
9
|
+
|
|
10
|
+
const authRoutes = require("./routes/auth");
|
|
11
|
+
const workspaceRoutes = require("./routes/workspaces");
|
|
12
|
+
const documentRoutes = require("./routes/documents");
|
|
13
|
+
const chatRoutes = require("./routes/chat");
|
|
14
|
+
const threadRoutes = require("./routes/threads");
|
|
15
|
+
const adminRoutes = require("./routes/admin");
|
|
16
|
+
const settingsRoutes = require("./routes/settings");
|
|
17
|
+
const modelsRoutes = require("./routes/models");
|
|
18
|
+
const audioRoutes = require("./routes/audio");
|
|
19
|
+
const dashboardRoutes = require("./routes/dashboard");
|
|
20
|
+
const embedRoutes = require("./routes/embed");
|
|
21
|
+
const apiKeyRoutes = require("./routes/apiKeys");
|
|
22
|
+
const oauthRoutes = require("./routes/oauth");
|
|
23
|
+
const agentRoutes = require("./routes/agents");
|
|
24
|
+
const projectRoutes = require("./routes/projects");
|
|
25
|
+
const templateRoutes = require("./routes/templates");
|
|
26
|
+
const ingestionQueue = require("./utils/ingestionQueue");
|
|
27
|
+
|
|
28
|
+
const app = express();
|
|
29
|
+
const prisma = new PrismaClient();
|
|
30
|
+
const PORT = process.env.SERVER_PORT || 3001;
|
|
31
|
+
|
|
32
|
+
app.use(cors({ origin: "*" }));
|
|
33
|
+
app.use(express.json({ limit: "50mb" }));
|
|
34
|
+
app.use(express.urlencoded({ extended: true }));
|
|
35
|
+
|
|
36
|
+
app.use((req, _res, next) => { req.db = prisma; next(); });
|
|
37
|
+
|
|
38
|
+
const commercialRoutesPath = path.resolve(__dirname, "../../commercial/routes");
|
|
39
|
+
const isCommercial = fs.existsSync(commercialRoutesPath);
|
|
40
|
+
if (isCommercial) {
|
|
41
|
+
try {
|
|
42
|
+
require(commercialRoutesPath).register(app);
|
|
43
|
+
console.log("[Commercial] Routes loaded");
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log("[Commercial] Failed to load routes:", e.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
app.get("/api/instance", async (req, res) => {
|
|
50
|
+
const isEnterprise = isCommercial;
|
|
51
|
+
|
|
52
|
+
const licenseType = isEnterprise ? "enterprise" : "community";
|
|
53
|
+
const edition = isEnterprise ? "Open Enthrium Commercial" : "Open Enthrium Community";
|
|
54
|
+
const price = isEnterprise ? "custom" : "free";
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const [brandingName, brandingUrl, brandingLogo] = await Promise.all([
|
|
58
|
+
req.db.setting.findUnique({ where: { key: "branding_name" } }),
|
|
59
|
+
req.db.setting.findUnique({ where: { key: "branding_url" } }),
|
|
60
|
+
req.db.setting.findUnique({ where: { key: "branding_logo" } }),
|
|
61
|
+
]);
|
|
62
|
+
res.set("Cache-Control", "no-store");
|
|
63
|
+
res.json({
|
|
64
|
+
licenseType,
|
|
65
|
+
edition,
|
|
66
|
+
price,
|
|
67
|
+
brandingName: brandingName?.value || null,
|
|
68
|
+
brandingUrl: brandingUrl?.value || null,
|
|
69
|
+
brandingLogo: brandingLogo?.value || null,
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
res.set("Cache-Control", "no-store");
|
|
73
|
+
res.json({ licenseType, edition, price, brandingName: null, brandingUrl: null, brandingLogo: null });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
app.use("/api/auth", authRoutes);
|
|
78
|
+
app.use("/api/workspaces", workspaceRoutes);
|
|
79
|
+
app.use("/api/documents", documentRoutes);
|
|
80
|
+
app.use("/api/chat", chatRoutes);
|
|
81
|
+
app.use("/api/threads", threadRoutes);
|
|
82
|
+
app.use("/api/admin", adminRoutes);
|
|
83
|
+
app.use("/api/settings", settingsRoutes);
|
|
84
|
+
app.use("/api/models", modelsRoutes);
|
|
85
|
+
app.use("/api/audio", audioRoutes);
|
|
86
|
+
app.use("/api/dashboard", dashboardRoutes);
|
|
87
|
+
app.use("/api/embed", embedRoutes);
|
|
88
|
+
app.use("/api/admin/api-keys", apiKeyRoutes);
|
|
89
|
+
app.use("/api/oauth", oauthRoutes);
|
|
90
|
+
app.use("/api/admin", agentRoutes);
|
|
91
|
+
app.use("/api/admin/workspaces/:workspaceId/projects", projectRoutes);
|
|
92
|
+
app.use("/api/admin/templates", templateRoutes);
|
|
93
|
+
|
|
94
|
+
app.get("/api/health", (_req, res) => res.json({ status: "ok", version }));
|
|
95
|
+
|
|
96
|
+
// Feature flags — readable by any authenticated user
|
|
97
|
+
app.get("/api/features", async (req, res) => {
|
|
98
|
+
try {
|
|
99
|
+
const rows = await prisma.setting.findMany({
|
|
100
|
+
where: { key: { in: ["feature.kbSharing"] } },
|
|
101
|
+
});
|
|
102
|
+
const s = Object.fromEntries(rows.map(r => [r.key, r.value]));
|
|
103
|
+
res.json({
|
|
104
|
+
kbSharing: s["feature.kbSharing"] === "true",
|
|
105
|
+
});
|
|
106
|
+
} catch { res.json({ kbSharing: false }); }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
async function recoverPendingJobs() {
|
|
110
|
+
try {
|
|
111
|
+
const stuck = await prisma.document.findMany({
|
|
112
|
+
where: { status: { in: ["queued", "ingesting"] } },
|
|
113
|
+
include: { workspace: true }
|
|
114
|
+
});
|
|
115
|
+
if (!stuck.length) return;
|
|
116
|
+
|
|
117
|
+
console.log(`[Queue] Recovering ${stuck.length} interrupted document(s)…`);
|
|
118
|
+
|
|
119
|
+
for (const doc of stuck) {
|
|
120
|
+
if (doc.type === "website-crawl") continue; // crawler tracker docs have no chunks of their own
|
|
121
|
+
|
|
122
|
+
const reset = { status: "queued", chunksProcessed: 0, cancelRequested: false, errorMessage: null };
|
|
123
|
+
|
|
124
|
+
if (doc.type === "url") {
|
|
125
|
+
await prisma.document.update({ where: { uid: doc.uid }, data: reset });
|
|
126
|
+
ingestionQueue.enqueue(prisma, doc.workspace, doc, doc.name, "url");
|
|
127
|
+
} else if (doc.sourcePath && fs.existsSync(doc.sourcePath)) {
|
|
128
|
+
await prisma.document.update({ where: { uid: doc.uid }, data: reset });
|
|
129
|
+
const sourceType = doc.type === "ocr" ? "ocr" : "file";
|
|
130
|
+
const keepFile = !doc.sourcePath.includes("uploads");
|
|
131
|
+
ingestionQueue.enqueue(prisma, doc.workspace, doc, doc.sourcePath, sourceType, keepFile);
|
|
132
|
+
} else {
|
|
133
|
+
await prisma.document.update({
|
|
134
|
+
where: { uid: doc.uid },
|
|
135
|
+
data: { status: "failed", errorMessage: "Source file unavailable after server restart. Please re-upload." }
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error("[Queue] Recovery error:", err.message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Serve built frontend in production
|
|
145
|
+
if (process.env.NODE_ENV === "production") {
|
|
146
|
+
const path = require("path");
|
|
147
|
+
const publicDir = path.join(__dirname, "../../public");
|
|
148
|
+
app.use(express.static(publicDir));
|
|
149
|
+
app.get("*", (_req, res) => res.sendFile(path.join(publicDir, "index.html")));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
app.listen(PORT, async () => {
|
|
153
|
+
console.log(`Open Enthrium server running on port ${PORT}`);
|
|
154
|
+
await recoverPendingJobs();
|
|
155
|
+
try { await require("./telemetry/bootstrap")(prisma); } catch {}
|
|
156
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const jwt = require("jsonwebtoken");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const _commercialExists = fs.existsSync(path.resolve(__dirname, "../../../commercial/routes"));
|
|
7
|
+
|
|
8
|
+
function authenticate(req, res, next) {
|
|
9
|
+
const header = req.headers.authorization;
|
|
10
|
+
if (!header) return res.status(401).json({ error: "No token provided" });
|
|
11
|
+
|
|
12
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : header;
|
|
13
|
+
try {
|
|
14
|
+
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
|
15
|
+
next();
|
|
16
|
+
} catch {
|
|
17
|
+
res.status(401).json({ error: "Invalid or expired token" });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireAdmin(req, res, next) {
|
|
22
|
+
if (req.user?.role !== "admin") return res.status(403).json({ error: "Admin only" });
|
|
23
|
+
next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireManagerOrAdmin(req, res, next) {
|
|
27
|
+
if (req.user?.role !== "admin" && req.user?.role !== "manager") {
|
|
28
|
+
return res.status(403).json({ error: "Manager or admin access required" });
|
|
29
|
+
}
|
|
30
|
+
next();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function authenticateApiKey(req, res, next) {
|
|
34
|
+
const header = req.headers.authorization;
|
|
35
|
+
if (!header?.startsWith("Bearer ")) {
|
|
36
|
+
return res.status(401).json({ error: "API key required. Use: Authorization: Bearer emb_..." });
|
|
37
|
+
}
|
|
38
|
+
const rawKey = header.slice(7);
|
|
39
|
+
if (!rawKey.startsWith("emb_")) {
|
|
40
|
+
return res.status(401).json({ error: "Invalid API key format" });
|
|
41
|
+
}
|
|
42
|
+
const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
43
|
+
const apiKey = await req.db.apiKey.findFirst({ where: { keyHash, revoked: false } });
|
|
44
|
+
if (!apiKey) return res.status(401).json({ error: "Invalid or revoked API key" });
|
|
45
|
+
|
|
46
|
+
await req.db.apiKey.update({ where: { id: apiKey.id }, data: { lastUsedAt: new Date() } });
|
|
47
|
+
req.apiKey = apiKey;
|
|
48
|
+
next();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requireManagerOrAdminOrUser(req, res, next) {
|
|
52
|
+
if (!req.user) return res.status(401).json({ error: "Not authenticated" });
|
|
53
|
+
next();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requireCommercial(req, res, next) {
|
|
57
|
+
if (!_commercialExists) {
|
|
58
|
+
return res.status(403).json({ error: "This feature requires a commercial license." });
|
|
59
|
+
}
|
|
60
|
+
next();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { authenticate, requireAdmin, requireManagerOrAdmin, requireManagerOrAdminOrUser, authenticateApiKey, requireCommercial };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const { PrismaClient } = require("@prisma/client");
|
|
2
|
+
const db = new PrismaClient();
|
|
3
|
+
|
|
4
|
+
async function getSetting(key) {
|
|
5
|
+
const s = await db.setting.findUnique({ where: { key } });
|
|
6
|
+
return s?.value || null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Embed texts and return { embeddings, tokensUsed, model }.
|
|
11
|
+
* tokensUsed is the actual count from the API response (0 for local providers).
|
|
12
|
+
*/
|
|
13
|
+
async function embed(texts) {
|
|
14
|
+
const { default: OpenAI } = await import("openai");
|
|
15
|
+
|
|
16
|
+
const provider = (await getSetting("embedding_provider")) || process.env.EMBEDDING_PROVIDER || "openai";
|
|
17
|
+
const model = (await getSetting("embedding_model")) || process.env.EMBEDDING_MODEL || "text-embedding-3-small";
|
|
18
|
+
|
|
19
|
+
let res;
|
|
20
|
+
|
|
21
|
+
if (provider === "azure") {
|
|
22
|
+
const endpoint = (await getSetting("embedding_azure_endpoint")) || (await getSetting("llm_azure_endpoint")) || process.env.AZURE_OPENAI_ENDPOINT;
|
|
23
|
+
const deployment = (await getSetting("embedding_azure_deployment")) || process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT;
|
|
24
|
+
const apiKey = (await getSetting("embedding_api_key")) || (await getSetting("llm_api_key")) || process.env.AZURE_OPENAI_API_KEY;
|
|
25
|
+
const client = new OpenAI({
|
|
26
|
+
apiKey,
|
|
27
|
+
baseURL: `${endpoint}/openai/deployments/${deployment}`,
|
|
28
|
+
defaultQuery: { "api-version": "2024-08-01-preview" },
|
|
29
|
+
defaultHeaders: { "api-key": apiKey }
|
|
30
|
+
});
|
|
31
|
+
res = await client.embeddings.create({ model, input: texts });
|
|
32
|
+
|
|
33
|
+
} else if (provider === "ollama") {
|
|
34
|
+
const rawUrl = (await getSetting("embedding_base_url")) || (await getSetting("llm_base_url")) || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
|
|
35
|
+
const baseURL = rawUrl.replace(/\/v1\/?$/, "") + "/v1";
|
|
36
|
+
const client = new OpenAI({ baseURL, apiKey: "ollama" });
|
|
37
|
+
res = await client.embeddings.create({ model: model || "nomic-embed-text", input: texts });
|
|
38
|
+
|
|
39
|
+
} else if (provider === "lmstudio" || provider === "generic-openai") {
|
|
40
|
+
const rawUrl = (await getSetting("embedding_base_url")) || process.env.EMBEDDING_BASE_URL || "http://localhost:1234/v1";
|
|
41
|
+
const apiKey = (await getSetting("embedding_api_key")) || "lmstudio";
|
|
42
|
+
const client = new OpenAI({ baseURL: rawUrl, apiKey });
|
|
43
|
+
res = await client.embeddings.create({ model, input: texts });
|
|
44
|
+
|
|
45
|
+
} else if (provider === "gemini") {
|
|
46
|
+
const apiKey = (await getSetting("embedding_api_key")) || process.env.GEMINI_API_KEY;
|
|
47
|
+
const client = new OpenAI({ apiKey, baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/" });
|
|
48
|
+
res = await client.embeddings.create({ model: model || "text-embedding-004", input: texts });
|
|
49
|
+
|
|
50
|
+
} else if (provider === "cohere") {
|
|
51
|
+
const apiKey = (await getSetting("embedding_api_key")) || process.env.COHERE_API_KEY;
|
|
52
|
+
const client = new OpenAI({ apiKey, baseURL: "https://api.cohere.com/compatibility/v1" });
|
|
53
|
+
res = await client.embeddings.create({ model: model || "embed-english-v3.0", input: texts });
|
|
54
|
+
|
|
55
|
+
} else {
|
|
56
|
+
// Default: OpenAI
|
|
57
|
+
const apiKey = (await getSetting("embedding_api_key")) || (await getSetting("llm_api_key")) || process.env.OPENAI_API_KEY;
|
|
58
|
+
const client = new OpenAI({ apiKey });
|
|
59
|
+
res = await client.embeddings.create({ model, input: texts });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
embeddings: res.data.map(d => d.embedding),
|
|
64
|
+
tokensUsed: res.usage?.total_tokens || 0,
|
|
65
|
+
model: model
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { embed };
|