arkaos 2.3.5 → 2.4.0
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/VERSION +1 -1
- package/dashboard/app/pages/knowledge.vue +34 -14
- package/installer/index.js +62 -27
- package/installer/prompts.js +168 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/dashboard-api.py +80 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.4.0
|
|
@@ -178,25 +178,45 @@ onUnmounted(() => {
|
|
|
178
178
|
})
|
|
179
179
|
|
|
180
180
|
async function handleIngest() {
|
|
181
|
-
if (!detectedType.value) return
|
|
181
|
+
if (!detectedType.value && activeInputMode.value !== 'text') return
|
|
182
182
|
|
|
183
183
|
ingestError.value = null
|
|
184
|
-
const source = ingestUrl.value.trim() || ingestFile.value?.name || ''
|
|
185
|
-
const type = detectedType.value
|
|
186
|
-
|
|
187
|
-
// Clear form immediately so user can submit more
|
|
188
|
-
ingestUrl.value = ''
|
|
189
|
-
clearFile()
|
|
190
|
-
pasteText.value = ''
|
|
191
|
-
pasteTitle.value = ''
|
|
192
184
|
|
|
193
185
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
186
|
+
// File upload — use multipart form
|
|
187
|
+
if (activeInputMode.value === 'file' && ingestFile.value) {
|
|
188
|
+
const formData = new FormData()
|
|
189
|
+
formData.append('file', ingestFile.value)
|
|
190
|
+
await $fetch(`${apiBase}/api/knowledge/upload-file`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: formData,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
// Text paste — save to temp file via API
|
|
196
|
+
else if (activeInputMode.value === 'text' && pasteText.value.length > 10) {
|
|
197
|
+
await $fetch(`${apiBase}/api/knowledge/ingest`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
body: { source: pasteText.value.slice(0, 100), type: 'markdown', text: pasteText.value, title: pasteTitle.value },
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
// URL or Research — standard ingest
|
|
203
|
+
else {
|
|
204
|
+
const source = ingestUrl.value.trim()
|
|
205
|
+
const type = detectedType.value
|
|
206
|
+
if (!source || !type) return
|
|
207
|
+
await $fetch(`${apiBase}/api/knowledge/ingest`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
body: { source, type },
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Clear form immediately
|
|
214
|
+
ingestUrl.value = ''
|
|
215
|
+
clearFile()
|
|
216
|
+
pasteText.value = ''
|
|
217
|
+
pasteTitle.value = ''
|
|
198
218
|
|
|
199
|
-
// Refresh jobs table + connect WebSocket
|
|
219
|
+
// Refresh jobs table + connect WebSocket
|
|
200
220
|
fetchJobs()
|
|
201
221
|
connectWebSocket()
|
|
202
222
|
} catch (err) {
|
package/installer/index.js
CHANGED
|
@@ -13,19 +13,22 @@ const VERSION = JSON.parse(readFileSync(join(ARKAOS_ROOT, "package.json"), "utf-
|
|
|
13
13
|
export async function install({ runtime, path, force }) {
|
|
14
14
|
const startTime = Date.now();
|
|
15
15
|
const config = getRuntimeConfig(runtime);
|
|
16
|
-
const
|
|
17
|
-
const isUpgrade = existsSync(join(installDir, "install-manifest.json"));
|
|
16
|
+
const isUpgrade = existsSync(join(path || join(homedir(), ".arkaos"), "install-manifest.json"));
|
|
18
17
|
|
|
19
18
|
console.log(`
|
|
20
19
|
╔══════════════════════════════════════════════════════════╗
|
|
21
20
|
║ ArkaOS v${VERSION} — The Operating System for AI Agent Teams ║
|
|
22
21
|
╚══════════════════════════════════════════════════════════╝
|
|
23
22
|
|
|
24
|
-
Runtime:
|
|
25
|
-
|
|
26
|
-
Mode: ${isUpgrade ? "Upgrade" : "Fresh install"}
|
|
23
|
+
Runtime: ${config.name}
|
|
24
|
+
Mode: ${isUpgrade ? "Upgrade" : "Fresh install"}
|
|
27
25
|
`);
|
|
28
26
|
|
|
27
|
+
// ═══ Interactive Setup ═══
|
|
28
|
+
const { runSetupPrompts } = await import("./prompts.js");
|
|
29
|
+
const userConfig = await runSetupPrompts(isUpgrade);
|
|
30
|
+
const installDir = userConfig.installDir;
|
|
31
|
+
|
|
29
32
|
// ═══ Step 1: Create directories ═══
|
|
30
33
|
step(1, 12, "Creating directories...");
|
|
31
34
|
ensureDir(installDir);
|
|
@@ -52,9 +55,9 @@ export async function install({ runtime, path, force }) {
|
|
|
52
55
|
const pythonCmd = checkPython();
|
|
53
56
|
ok(`Found: ${pythonCmd}`);
|
|
54
57
|
|
|
55
|
-
// ═══ Step 4: Install Python core +
|
|
58
|
+
// ═══ Step 4: Install Python core + dependencies based on user choices ═══
|
|
56
59
|
step(4, 12, "Installing Python dependencies (this may take a minute)...");
|
|
57
|
-
installAllPythonDeps(pythonCmd);
|
|
60
|
+
installAllPythonDeps(pythonCmd, userConfig);
|
|
58
61
|
|
|
59
62
|
// ═══ Step 5: Copy configuration files ═══
|
|
60
63
|
step(5, 12, "Copying configuration files...");
|
|
@@ -83,32 +86,60 @@ export async function install({ runtime, path, force }) {
|
|
|
83
86
|
writeFileSync(join(skillsDir, ".arkaos-root"), ARKAOS_ROOT);
|
|
84
87
|
|
|
85
88
|
const profilePath = join(installDir, "profile.json");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
const profile = {
|
|
90
|
+
version: "2",
|
|
91
|
+
language: userConfig.language,
|
|
92
|
+
market: userConfig.market,
|
|
93
|
+
role: userConfig.role,
|
|
94
|
+
company: userConfig.company,
|
|
95
|
+
projectsDir: userConfig.projectsDir,
|
|
96
|
+
vaultPath: userConfig.vaultPath,
|
|
97
|
+
created: existsSync(profilePath)
|
|
98
|
+
? JSON.parse(readFileSync(profilePath, "utf-8")).created
|
|
99
|
+
: new Date().toISOString(),
|
|
100
|
+
updated: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
|
103
|
+
ok("Profile saved");
|
|
104
|
+
|
|
105
|
+
// Save API keys if provided
|
|
106
|
+
if (userConfig.openaiKey || userConfig.googleKey || userConfig.falKey) {
|
|
107
|
+
const keysPath = join(installDir, "keys.json");
|
|
108
|
+
const keys = existsSync(keysPath) ? JSON.parse(readFileSync(keysPath, "utf-8")) : {};
|
|
109
|
+
if (userConfig.openaiKey) keys.OPENAI_API_KEY = userConfig.openaiKey;
|
|
110
|
+
if (userConfig.googleKey) keys.GOOGLE_API_KEY = userConfig.googleKey;
|
|
111
|
+
if (userConfig.falKey) keys.FAL_API_KEY = userConfig.falKey;
|
|
112
|
+
writeFileSync(keysPath, JSON.stringify(keys, null, 2));
|
|
113
|
+
try { chmodSync(keysPath, 0o600); } catch {}
|
|
114
|
+
ok("API keys saved");
|
|
94
115
|
}
|
|
95
116
|
|
|
96
|
-
// ═══ Step 10: Index knowledge base
|
|
97
|
-
step(10, 12, "
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
// ═══ Step 10: Index knowledge base ═══
|
|
118
|
+
step(10, 12, "Setting up knowledge base...");
|
|
119
|
+
if (userConfig.installKnowledge) {
|
|
120
|
+
const kbDb = join(installDir, "knowledge.db");
|
|
121
|
+
// Index ArkaOS skills first
|
|
100
122
|
try {
|
|
101
123
|
execSync(`${pythonCmd} "${join(ARKAOS_ROOT, "scripts", "knowledge-index.py")}" --dir "${join(ARKAOS_ROOT, "departments")}" --db "${kbDb}"`, {
|
|
102
|
-
stdio: "pipe",
|
|
103
|
-
timeout: 60000,
|
|
104
|
-
env: { ...process.env, ARKAOS_ROOT },
|
|
124
|
+
stdio: "pipe", timeout: 60000, env: { ...process.env, ARKAOS_ROOT },
|
|
105
125
|
});
|
|
106
|
-
ok("ArkaOS skills indexed
|
|
126
|
+
ok("ArkaOS skills indexed");
|
|
107
127
|
} catch {
|
|
108
|
-
warn("
|
|
128
|
+
warn("Skill indexing skipped");
|
|
129
|
+
}
|
|
130
|
+
// Index user's Obsidian vault if provided
|
|
131
|
+
if (userConfig.vaultPath && existsSync(userConfig.vaultPath)) {
|
|
132
|
+
try {
|
|
133
|
+
execSync(`${pythonCmd} "${join(ARKAOS_ROOT, "scripts", "knowledge-index.py")}" --vault "${userConfig.vaultPath}" --db "${kbDb}"`, {
|
|
134
|
+
stdio: "pipe", timeout: 120000, env: { ...process.env, ARKAOS_ROOT },
|
|
135
|
+
});
|
|
136
|
+
ok(`Obsidian vault indexed: ${userConfig.vaultPath}`);
|
|
137
|
+
} catch {
|
|
138
|
+
warn("Vault indexing skipped (run 'npx arkaos index' later)");
|
|
139
|
+
}
|
|
109
140
|
}
|
|
110
141
|
} else {
|
|
111
|
-
ok("Knowledge base
|
|
142
|
+
ok("Knowledge base skipped (install later with 'npx arkaos index')");
|
|
112
143
|
}
|
|
113
144
|
|
|
114
145
|
// ═══ Step 11: Verify installation ═══
|
|
@@ -213,7 +244,7 @@ function checkPython() {
|
|
|
213
244
|
process.exit(1);
|
|
214
245
|
}
|
|
215
246
|
|
|
216
|
-
function installAllPythonDeps(pythonCmd) {
|
|
247
|
+
function installAllPythonDeps(pythonCmd, userConfig = {}) {
|
|
217
248
|
// Core dependencies
|
|
218
249
|
const coreDeps = "pyyaml pydantic rich click jinja2";
|
|
219
250
|
// Knowledge + Vector DB
|
|
@@ -225,7 +256,11 @@ function installAllPythonDeps(pythonCmd) {
|
|
|
225
256
|
// Transcription
|
|
226
257
|
const transcriptionDeps = "faster-whisper";
|
|
227
258
|
|
|
228
|
-
|
|
259
|
+
// Build deps list based on user choices
|
|
260
|
+
let allDeps = coreDeps;
|
|
261
|
+
if (userConfig.installKnowledge !== false) allDeps += ` ${knowledgeDeps}`;
|
|
262
|
+
allDeps += ` ${ingestDeps}`;
|
|
263
|
+
if (userConfig.installDashboard !== false) allDeps += ` ${dashboardDeps}`;
|
|
229
264
|
|
|
230
265
|
try {
|
|
231
266
|
// Try uv first (faster)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts for ArkaOS installer.
|
|
3
|
+
* Asks user for directories, language, market, preferences.
|
|
4
|
+
* Nothing is hardcoded — everything comes from the user.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
|
|
14
|
+
function ask(question, defaultValue = "") {
|
|
15
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
18
|
+
resolve(answer.trim() || defaultValue);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function askYN(question, defaultYes = true) {
|
|
24
|
+
const suffix = defaultYes ? " [Y/n]" : " [y/N]";
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
27
|
+
const a = answer.trim().toLowerCase();
|
|
28
|
+
if (!a) resolve(defaultYes);
|
|
29
|
+
else resolve(a === "y" || a === "yes");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function askChoice(question, options) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
console.log(` ${question}`);
|
|
37
|
+
options.forEach((opt, i) => {
|
|
38
|
+
console.log(` ${i + 1}) ${opt.label}`);
|
|
39
|
+
});
|
|
40
|
+
rl.question(` Choose [1-${options.length}]: `, (answer) => {
|
|
41
|
+
const idx = parseInt(answer.trim()) - 1;
|
|
42
|
+
if (idx >= 0 && idx < options.length) {
|
|
43
|
+
resolve(options[idx].value);
|
|
44
|
+
} else {
|
|
45
|
+
resolve(options[0].value); // Default to first
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runSetupPrompts(isUpgrade = false) {
|
|
52
|
+
console.log(`
|
|
53
|
+
╔══════════════════════════════════════════════════════╗
|
|
54
|
+
║ ArkaOS Setup — Let's configure your environment ║
|
|
55
|
+
╚══════════════════════════════════════════════════════╝
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
const config = {};
|
|
59
|
+
|
|
60
|
+
// ── Language ──
|
|
61
|
+
config.language = await askChoice("What is your primary language?", [
|
|
62
|
+
{ label: "English", value: "en" },
|
|
63
|
+
{ label: "Português", value: "pt" },
|
|
64
|
+
{ label: "Español", value: "es" },
|
|
65
|
+
{ label: "Français", value: "fr" },
|
|
66
|
+
{ label: "Deutsch", value: "de" },
|
|
67
|
+
{ label: "Italiano", value: "it" },
|
|
68
|
+
{ label: "中文 (Chinese)", value: "zh" },
|
|
69
|
+
{ label: "日本語 (Japanese)", value: "ja" },
|
|
70
|
+
{ label: "한국어 (Korean)", value: "ko" },
|
|
71
|
+
{ label: "Other", value: "other" },
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
if (config.language === "other") {
|
|
75
|
+
config.language = await ask("Enter language code (e.g., nl, pl, ru)");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Market/Country ──
|
|
79
|
+
config.market = await ask("What is your primary market/country?", "");
|
|
80
|
+
console.log(" (e.g., United States, Portugal, Brazil, Germany, Global)");
|
|
81
|
+
if (!config.market) {
|
|
82
|
+
config.market = await ask("Market/Country");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Role ──
|
|
86
|
+
config.role = await askChoice("What best describes your role?", [
|
|
87
|
+
{ label: "Developer / Engineer", value: "developer" },
|
|
88
|
+
{ label: "Founder / CEO", value: "founder" },
|
|
89
|
+
{ label: "Marketing / Growth", value: "marketing" },
|
|
90
|
+
{ label: "Product Manager", value: "product" },
|
|
91
|
+
{ label: "Designer", value: "designer" },
|
|
92
|
+
{ label: "Consultant / Agency", value: "consultant" },
|
|
93
|
+
{ label: "Other", value: "other" },
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// ── Company ──
|
|
97
|
+
config.company = await ask("Company or organization name (optional)", "");
|
|
98
|
+
|
|
99
|
+
// ── Directories ──
|
|
100
|
+
console.log("\n ── Directories ──\n");
|
|
101
|
+
|
|
102
|
+
config.projectsDir = await ask(
|
|
103
|
+
"Where are your projects?",
|
|
104
|
+
join(homedir(), "Projects")
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
config.vaultPath = await ask(
|
|
108
|
+
"Where is your Obsidian vault? (leave empty if none)",
|
|
109
|
+
""
|
|
110
|
+
);
|
|
111
|
+
if (config.vaultPath && !existsSync(config.vaultPath)) {
|
|
112
|
+
console.log(` ⚠ Directory not found: ${config.vaultPath}`);
|
|
113
|
+
const create = await askYN("Create it?", false);
|
|
114
|
+
if (!create) config.vaultPath = "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
config.installDir = await ask(
|
|
118
|
+
"ArkaOS data directory",
|
|
119
|
+
join(homedir(), ".arkaos")
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// ── Features ──
|
|
123
|
+
console.log("\n ── Optional Features ──\n");
|
|
124
|
+
|
|
125
|
+
config.installDashboard = await askYN("Install monitoring dashboard?", true);
|
|
126
|
+
config.installKnowledge = await askYN("Install knowledge base (vector DB)?", true);
|
|
127
|
+
config.installTranscription = await askYN("Install audio transcription (Whisper)?", false);
|
|
128
|
+
|
|
129
|
+
// ── API Keys (optional) ──
|
|
130
|
+
console.log("\n ── API Keys (optional, can be configured later) ──\n");
|
|
131
|
+
|
|
132
|
+
config.openaiKey = await ask("OpenAI API key (for Whisper, embeddings — leave empty to skip)", "");
|
|
133
|
+
config.googleKey = await ask("Google API key (Gemini, Nano Banana — leave empty to skip)", "");
|
|
134
|
+
config.falKey = await ask("fal.ai API key (image/video generation — leave empty to skip)", "");
|
|
135
|
+
|
|
136
|
+
// ── Summary ──
|
|
137
|
+
console.log(`
|
|
138
|
+
── Configuration Summary ──
|
|
139
|
+
|
|
140
|
+
Language: ${config.language}
|
|
141
|
+
Market: ${config.market || "(not set)"}
|
|
142
|
+
Role: ${config.role}
|
|
143
|
+
Company: ${config.company || "(not set)"}
|
|
144
|
+
Projects dir: ${config.projectsDir}
|
|
145
|
+
Obsidian vault: ${config.vaultPath || "(none)"}
|
|
146
|
+
Install dir: ${config.installDir}
|
|
147
|
+
Dashboard: ${config.installDashboard ? "Yes" : "No"}
|
|
148
|
+
Knowledge DB: ${config.installKnowledge ? "Yes" : "No"}
|
|
149
|
+
Transcription: ${config.installTranscription ? "Yes" : "No"}
|
|
150
|
+
OpenAI key: ${config.openaiKey ? "configured" : "not set"}
|
|
151
|
+
Google key: ${config.googleKey ? "configured" : "not set"}
|
|
152
|
+
fal.ai key: ${config.falKey ? "configured" : "not set"}
|
|
153
|
+
`);
|
|
154
|
+
|
|
155
|
+
const confirmed = await askYN("Proceed with this configuration?", true);
|
|
156
|
+
if (!confirmed) {
|
|
157
|
+
console.log("\n Installation cancelled.\n");
|
|
158
|
+
rl.close();
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
rl.close();
|
|
163
|
+
return config;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function closePrompts() {
|
|
167
|
+
try { rl.close(); } catch {}
|
|
168
|
+
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/scripts/dashboard-api.py
CHANGED
|
@@ -345,6 +345,71 @@ def job_cancel(job_id: str):
|
|
|
345
345
|
return {"error": "Can only cancel queued jobs"}
|
|
346
346
|
|
|
347
347
|
|
|
348
|
+
@app.post("/api/knowledge/upload")
|
|
349
|
+
async def knowledge_upload(file: Any = None):
|
|
350
|
+
"""Upload a file for ingestion."""
|
|
351
|
+
from fastapi import UploadFile, File as FastAPIFile
|
|
352
|
+
# Re-import with proper type
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Actual upload endpoint with proper signature
|
|
357
|
+
from fastapi import UploadFile, File as FastAPIFile
|
|
358
|
+
|
|
359
|
+
@app.post("/api/knowledge/upload-file")
|
|
360
|
+
async def knowledge_upload_file(file: UploadFile):
|
|
361
|
+
"""Upload and ingest a file (PDF, audio, markdown)."""
|
|
362
|
+
import threading
|
|
363
|
+
|
|
364
|
+
media_dir = Path.home() / ".arkaos" / "media"
|
|
365
|
+
media_dir.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
|
|
367
|
+
# Save uploaded file
|
|
368
|
+
file_path = media_dir / file.filename
|
|
369
|
+
content = await file.read()
|
|
370
|
+
file_path.write_bytes(content)
|
|
371
|
+
|
|
372
|
+
source = str(file_path)
|
|
373
|
+
from core.knowledge.ingest import detect_source_type
|
|
374
|
+
source_type = detect_source_type(source)
|
|
375
|
+
|
|
376
|
+
store = _get_vector_store()
|
|
377
|
+
if not store:
|
|
378
|
+
from core.knowledge.vector_store import VectorStore
|
|
379
|
+
kb_db = Path.home() / ".arkaos" / "knowledge.db"
|
|
380
|
+
kb_db.parent.mkdir(parents=True, exist_ok=True)
|
|
381
|
+
store = VectorStore(kb_db)
|
|
382
|
+
|
|
383
|
+
job_mgr = _get_job_manager()
|
|
384
|
+
job = job_mgr.create(source=source, source_type=source_type, title=file.filename)
|
|
385
|
+
job_id = job.id
|
|
386
|
+
|
|
387
|
+
def run_ingest():
|
|
388
|
+
from core.jobs.manager import JobManager as _JM
|
|
389
|
+
from core.knowledge.ingest import IngestEngine
|
|
390
|
+
local_mgr = _JM()
|
|
391
|
+
engine = IngestEngine(store)
|
|
392
|
+
def on_progress(pct, msg):
|
|
393
|
+
status = "embedding" if "embed" in msg.lower() or "index" in msg.lower() else "processing"
|
|
394
|
+
local_mgr.update_progress(job_id, pct, msg, status)
|
|
395
|
+
broadcast_from_thread({"type": "job_progress", "job_id": job_id, "progress": pct, "message": msg, "status": status})
|
|
396
|
+
try:
|
|
397
|
+
local_mgr.start(job_id)
|
|
398
|
+
result = engine.ingest(source, source_type, on_progress=on_progress)
|
|
399
|
+
if result.success:
|
|
400
|
+
local_mgr.complete(job_id, chunks_created=result.chunks_created)
|
|
401
|
+
broadcast_from_thread({"type": "job_complete", "job_id": job_id, "chunks_created": result.chunks_created})
|
|
402
|
+
else:
|
|
403
|
+
local_mgr.fail(job_id, result.error)
|
|
404
|
+
broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": result.error})
|
|
405
|
+
except Exception as e:
|
|
406
|
+
local_mgr.fail(job_id, str(e))
|
|
407
|
+
broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": str(e)})
|
|
408
|
+
|
|
409
|
+
threading.Thread(target=run_ingest, daemon=True).start()
|
|
410
|
+
return {"job_id": job_id, "source_type": source_type, "filename": file.filename, "status": "queued"}
|
|
411
|
+
|
|
412
|
+
|
|
348
413
|
@app.post("/api/knowledge/ingest")
|
|
349
414
|
def knowledge_ingest(body: dict):
|
|
350
415
|
"""Ingest content into the knowledge base. Runs in background with SQLite job tracking."""
|
|
@@ -352,6 +417,21 @@ def knowledge_ingest(body: dict):
|
|
|
352
417
|
|
|
353
418
|
source = body.get("source", "")
|
|
354
419
|
source_type = body.get("type", "")
|
|
420
|
+
text_content = body.get("text", "")
|
|
421
|
+
text_title = body.get("title", "")
|
|
422
|
+
|
|
423
|
+
# Handle direct text paste — save to temp markdown file
|
|
424
|
+
if text_content and len(text_content) > 10:
|
|
425
|
+
media_dir = Path.home() / ".arkaos" / "media"
|
|
426
|
+
media_dir.mkdir(parents=True, exist_ok=True)
|
|
427
|
+
safe_name = "".join(c if c.isalnum() or c in " -_" else "" for c in (text_title or source)[:40]).strip() or "pasted-text"
|
|
428
|
+
text_path = media_dir / f"{safe_name}.md"
|
|
429
|
+
# Add title as heading
|
|
430
|
+
md_content = f"# {text_title}\n\n{text_content}" if text_title else text_content
|
|
431
|
+
text_path.write_text(md_content, encoding="utf-8")
|
|
432
|
+
source = str(text_path)
|
|
433
|
+
source_type = "markdown"
|
|
434
|
+
|
|
355
435
|
if not source:
|
|
356
436
|
return {"error": "source is required"}
|
|
357
437
|
|