arkaos 2.3.5 → 2.3.6
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/dashboard-api.py +80 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.3.
|
|
1
|
+
2.3.6
|
|
@@ -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/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
|
|