arkaos 3.24.0 → 3.26.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/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/vector_store.py +13 -0
- package/dashboard/app/components/KnowledgeSourcesList.vue +157 -0
- package/dashboard/app/pages/agents/[id].vue +66 -0
- package/dashboard/app/pages/knowledge.vue +5 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +107 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.26.0
|
|
Binary file
|
|
@@ -283,6 +283,19 @@ class VectorStore:
|
|
|
283
283
|
"db_path": self._db_path,
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
def list_sources(self) -> list[dict]:
|
|
287
|
+
"""PR88c v3.25.0 — distinct sources with chunk counts.
|
|
288
|
+
|
|
289
|
+
Returns rows sorted by chunk count desc so the noisiest
|
|
290
|
+
sources surface first.
|
|
291
|
+
"""
|
|
292
|
+
rows = self._db.execute(
|
|
293
|
+
"SELECT source, COUNT(*) AS chunks FROM chunks "
|
|
294
|
+
"WHERE source IS NOT NULL AND source != '' "
|
|
295
|
+
"GROUP BY source ORDER BY chunks DESC"
|
|
296
|
+
).fetchall()
|
|
297
|
+
return [{"source": r["source"], "chunks": int(r["chunks"])} for r in rows]
|
|
298
|
+
|
|
286
299
|
def clear(self) -> None:
|
|
287
300
|
"""Remove all data."""
|
|
288
301
|
if self._vec_available:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR88c v3.25.0 — Listing + management of indexed knowledge sources.
|
|
3
|
+
//
|
|
4
|
+
// Sits below the ingest UI on /knowledge. Loads GET /api/knowledge/sources
|
|
5
|
+
// (returns `{sources: [{source, chunks}], total}`), supports search,
|
|
6
|
+
// per-row Delete (DELETE /api/knowledge/sources?source=...). Pagination
|
|
7
|
+
// inline.
|
|
8
|
+
|
|
9
|
+
interface SourceRow {
|
|
10
|
+
source: string
|
|
11
|
+
chunks: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { fetchApi, apiBase } = useApi()
|
|
15
|
+
const toast = useToast()
|
|
16
|
+
const confirmDialog = useConfirmDialog()
|
|
17
|
+
|
|
18
|
+
const { data, status, error, refresh } = await fetchApi<{
|
|
19
|
+
sources: SourceRow[]
|
|
20
|
+
total: number
|
|
21
|
+
}>('/api/knowledge/sources')
|
|
22
|
+
|
|
23
|
+
const sources = computed(() => data.value?.sources ?? [])
|
|
24
|
+
const search = ref('')
|
|
25
|
+
const page = ref(1)
|
|
26
|
+
const pageSize = 15
|
|
27
|
+
|
|
28
|
+
const filtered = computed(() => {
|
|
29
|
+
const q = search.value.toLowerCase().trim()
|
|
30
|
+
if (!q) return sources.value
|
|
31
|
+
return sources.value.filter((s) => s.source.toLowerCase().includes(q))
|
|
32
|
+
})
|
|
33
|
+
const totalPages = computed(() => Math.max(1, Math.ceil(filtered.value.length / pageSize)))
|
|
34
|
+
const paged = computed(() =>
|
|
35
|
+
filtered.value.slice((page.value - 1) * pageSize, page.value * pageSize),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
watch(search, () => { page.value = 1 })
|
|
39
|
+
|
|
40
|
+
async function remove(row: SourceRow) {
|
|
41
|
+
const ok = await confirmDialog({
|
|
42
|
+
title: 'Delete source?',
|
|
43
|
+
description: `Removes ${row.chunks} chunk${row.chunks === 1 ? '' : 's'} from the vector store. This cannot be undone.`,
|
|
44
|
+
confirmLabel: 'Delete',
|
|
45
|
+
cancelLabel: 'Cancel',
|
|
46
|
+
variant: 'danger',
|
|
47
|
+
})
|
|
48
|
+
if (!ok) return
|
|
49
|
+
try {
|
|
50
|
+
const res = await $fetch<{ deleted?: number, error?: string }>(
|
|
51
|
+
`${apiBase}/api/knowledge/sources`,
|
|
52
|
+
{ method: 'DELETE', query: { source: row.source } },
|
|
53
|
+
)
|
|
54
|
+
if (res.error) throw new Error(res.error)
|
|
55
|
+
toast.add({
|
|
56
|
+
title: `Removed ${res.deleted ?? 0} chunks`,
|
|
57
|
+
description: row.source,
|
|
58
|
+
color: 'success',
|
|
59
|
+
})
|
|
60
|
+
await refresh()
|
|
61
|
+
} catch (err) {
|
|
62
|
+
toast.add({
|
|
63
|
+
title: 'Delete failed',
|
|
64
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
65
|
+
color: 'error',
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sourceLabel(src: string): string {
|
|
71
|
+
if (src.startsWith('http')) {
|
|
72
|
+
try {
|
|
73
|
+
const u = new URL(src)
|
|
74
|
+
return u.hostname + u.pathname
|
|
75
|
+
} catch {
|
|
76
|
+
return src
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return src
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<UCard>
|
|
85
|
+
<template #header>
|
|
86
|
+
<div class="flex items-center justify-between gap-3">
|
|
87
|
+
<div>
|
|
88
|
+
<h3 class="text-lg font-bold">Indexed sources</h3>
|
|
89
|
+
<p class="text-xs text-muted mt-0.5">
|
|
90
|
+
Every distinct source contributing chunks to the vector store.
|
|
91
|
+
<span v-if="data?.total">{{ data.total }} total.</span>
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
<UButton
|
|
95
|
+
icon="i-lucide-refresh-cw"
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="sm"
|
|
98
|
+
aria-label="Refresh"
|
|
99
|
+
:loading="status === 'pending'"
|
|
100
|
+
@click="refresh"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<div v-if="error" class="py-6 text-center text-sm text-error">
|
|
106
|
+
Failed to load sources.
|
|
107
|
+
</div>
|
|
108
|
+
<div v-else-if="!sources.length" class="py-6 text-center text-sm text-muted">
|
|
109
|
+
<UIcon name="i-lucide-database" class="size-6 mx-auto mb-2" />
|
|
110
|
+
No sources indexed yet. Use the ingest panel above to add content.
|
|
111
|
+
</div>
|
|
112
|
+
<div v-else class="space-y-3">
|
|
113
|
+
<UInput
|
|
114
|
+
v-model="search"
|
|
115
|
+
icon="i-lucide-search"
|
|
116
|
+
placeholder="Filter by source URL or path…"
|
|
117
|
+
class="w-full"
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<ul class="space-y-1.5">
|
|
121
|
+
<li
|
|
122
|
+
v-for="row in paged"
|
|
123
|
+
:key="row.source"
|
|
124
|
+
class="flex items-center gap-3 rounded-lg border border-default p-2.5 hover:border-primary/40 transition-colors"
|
|
125
|
+
>
|
|
126
|
+
<UIcon
|
|
127
|
+
:name="row.source.startsWith('http') ? 'i-lucide-link' : 'i-lucide-file-text'"
|
|
128
|
+
class="size-4 text-muted shrink-0"
|
|
129
|
+
/>
|
|
130
|
+
<div class="flex-1 min-w-0">
|
|
131
|
+
<p class="text-sm font-mono truncate" :title="row.source">
|
|
132
|
+
{{ sourceLabel(row.source) }}
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
<UBadge :label="`${row.chunks} chunk${row.chunks === 1 ? '' : 's'}`" variant="subtle" size="xs" />
|
|
136
|
+
<UButton
|
|
137
|
+
icon="i-lucide-trash-2"
|
|
138
|
+
color="error"
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="xs"
|
|
141
|
+
aria-label="Delete source"
|
|
142
|
+
@click="remove(row)"
|
|
143
|
+
/>
|
|
144
|
+
</li>
|
|
145
|
+
</ul>
|
|
146
|
+
|
|
147
|
+
<div v-if="totalPages > 1" class="flex items-center justify-center pt-2">
|
|
148
|
+
<UPagination
|
|
149
|
+
:page="page"
|
|
150
|
+
:total="filtered.length"
|
|
151
|
+
:items-per-page="pageSize"
|
|
152
|
+
@update:page="(val) => page = val"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</UCard>
|
|
157
|
+
</template>
|
|
@@ -28,6 +28,35 @@ const deptActivity = computed<ActivityRow | null>(() =>
|
|
|
28
28
|
(activityData.value?.by_department?.[agent.value?.department ?? ''] ?? null),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
// PR88d v3.26.0 — agent history (git log + trash entries)
|
|
32
|
+
interface HistoryEvent {
|
|
33
|
+
kind: string
|
|
34
|
+
ts: string | null
|
|
35
|
+
summary: string
|
|
36
|
+
ref?: string
|
|
37
|
+
author?: string
|
|
38
|
+
}
|
|
39
|
+
const { data: historyData } = fetchApi<{ events: HistoryEvent[] }>(
|
|
40
|
+
`/api/agents/${agentId}/history?limit=20`,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const historyEvents = computed<HistoryEvent[]>(() => historyData.value?.events ?? [])
|
|
44
|
+
|
|
45
|
+
function historyKindIcon(kind: string): string {
|
|
46
|
+
return ({
|
|
47
|
+
'git-commit': 'i-lucide-git-commit',
|
|
48
|
+
'agent-delete': 'i-lucide-trash-2',
|
|
49
|
+
'agent-move': 'i-lucide-folder-tree',
|
|
50
|
+
} as Record<string, string>)[kind] ?? 'i-lucide-circle'
|
|
51
|
+
}
|
|
52
|
+
function historyKindColor(kind: string): string {
|
|
53
|
+
return ({
|
|
54
|
+
'git-commit': 'text-blue-500',
|
|
55
|
+
'agent-delete': 'text-red-500',
|
|
56
|
+
'agent-move': 'text-amber-500',
|
|
57
|
+
} as Record<string, string>)[kind] ?? 'text-muted'
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
// PR83d v3.6.0 + PR86b v3.16.0 — activity strip (30d, agent or dept scope)
|
|
32
61
|
interface ActivityStrip {
|
|
33
62
|
period: string
|
|
@@ -443,6 +472,43 @@ function formatTokens(n: number): string {
|
|
|
443
472
|
/>
|
|
444
473
|
</section>
|
|
445
474
|
|
|
475
|
+
<!-- ===== HISTORY TIMELINE (PR88d) ===== -->
|
|
476
|
+
<section
|
|
477
|
+
v-if="historyEvents.length > 0"
|
|
478
|
+
class="rounded-xl border border-default bg-elevated/10 p-5"
|
|
479
|
+
>
|
|
480
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted mb-4">
|
|
481
|
+
History
|
|
482
|
+
</h3>
|
|
483
|
+
<ol class="relative border-l border-default ml-2 space-y-3">
|
|
484
|
+
<li
|
|
485
|
+
v-for="(ev, idx) in historyEvents"
|
|
486
|
+
:key="idx"
|
|
487
|
+
class="ml-4"
|
|
488
|
+
>
|
|
489
|
+
<span
|
|
490
|
+
class="absolute -left-1.5 size-3 rounded-full bg-elevated border border-default flex items-center justify-center"
|
|
491
|
+
>
|
|
492
|
+
<UIcon :name="historyKindIcon(ev.kind)" :class="['size-2', historyKindColor(ev.kind)]" />
|
|
493
|
+
</span>
|
|
494
|
+
<div class="rounded-lg border border-default p-3 bg-elevated/20">
|
|
495
|
+
<div class="flex items-center gap-2 flex-wrap text-xs">
|
|
496
|
+
<span class="font-mono text-muted">{{ ev.ts ? formatRelative(ev.ts) : '—' }}</span>
|
|
497
|
+
<UBadge
|
|
498
|
+
:label="ev.kind"
|
|
499
|
+
:color="ev.kind === 'git-commit' ? 'primary' : ev.kind === 'agent-move' ? 'warning' : 'error'"
|
|
500
|
+
variant="subtle"
|
|
501
|
+
size="xs"
|
|
502
|
+
/>
|
|
503
|
+
<code v-if="ev.ref" class="font-mono text-muted">{{ ev.ref }}</code>
|
|
504
|
+
<span v-if="ev.author" class="text-muted">· {{ ev.author }}</span>
|
|
505
|
+
</div>
|
|
506
|
+
<p class="text-sm mt-1">{{ ev.summary }}</p>
|
|
507
|
+
</div>
|
|
508
|
+
</li>
|
|
509
|
+
</ol>
|
|
510
|
+
</section>
|
|
511
|
+
|
|
446
512
|
<AgentEditDrawer
|
|
447
513
|
v-model="editOpen"
|
|
448
514
|
:agent="agent"
|
|
@@ -906,6 +906,11 @@ function escapeRegex(value: string): string {
|
|
|
906
906
|
</div>
|
|
907
907
|
</template>
|
|
908
908
|
</div>
|
|
909
|
+
|
|
910
|
+
<!-- PR88c v3.25.0 — Indexed sources management -->
|
|
911
|
+
<div class="mt-6">
|
|
912
|
+
<KnowledgeSourcesList />
|
|
913
|
+
</div>
|
|
909
914
|
</template>
|
|
910
915
|
</template>
|
|
911
916
|
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -322,6 +322,96 @@ def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
|
|
325
|
+
@app.get("/api/agents/{agent_id}/history")
|
|
326
|
+
def agent_history(agent_id: str, limit: int = 20):
|
|
327
|
+
"""PR88d v3.26.0 — combined history for an agent.
|
|
328
|
+
|
|
329
|
+
Sources:
|
|
330
|
+
- git log of the YAML file (commit hash, date, subject, author)
|
|
331
|
+
- trash entries (agent-delete + agent-move) where ``item_id``
|
|
332
|
+
matches
|
|
333
|
+
|
|
334
|
+
Returns ``{events: [{kind, ts, summary, ref?, author?}]}`` sorted
|
|
335
|
+
desc by ts.
|
|
336
|
+
"""
|
|
337
|
+
events: list[dict] = []
|
|
338
|
+
yaml_file = _resolve_agent_yaml(agent_id)
|
|
339
|
+
if yaml_file is not None:
|
|
340
|
+
events.extend(_agent_git_log(yaml_file, limit=limit))
|
|
341
|
+
try:
|
|
342
|
+
from core import trash as _trash
|
|
343
|
+
for entry in _trash.list_trash(limit=50):
|
|
344
|
+
if entry.get("item_id") == agent_id and str(entry.get("kind", "")).startswith("agent-"):
|
|
345
|
+
events.append({
|
|
346
|
+
"kind": entry.get("kind"),
|
|
347
|
+
"ts": _trash_ts_to_iso(entry.get("timestamp")),
|
|
348
|
+
"summary": _trash_summary(entry),
|
|
349
|
+
"ref": entry.get("id"),
|
|
350
|
+
})
|
|
351
|
+
except Exception: # noqa: BLE001
|
|
352
|
+
pass
|
|
353
|
+
events.sort(key=lambda e: str(e.get("ts") or ""), reverse=True)
|
|
354
|
+
return {"events": events[: max(0, int(limit))]}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _agent_git_log(yaml_file: Path, limit: int = 20) -> list[dict]:
|
|
358
|
+
"""Run ``git log`` on the YAML file. Best-effort — empty on error."""
|
|
359
|
+
try:
|
|
360
|
+
rel = yaml_file.relative_to(ARKAOS_ROOT).as_posix()
|
|
361
|
+
except ValueError:
|
|
362
|
+
return []
|
|
363
|
+
try:
|
|
364
|
+
result = subprocess.run(
|
|
365
|
+
[
|
|
366
|
+
"git", "log", "--follow", f"-n{int(limit)}",
|
|
367
|
+
"--pretty=format:%H%x09%cI%x09%an%x09%s",
|
|
368
|
+
"--", rel,
|
|
369
|
+
],
|
|
370
|
+
cwd=str(ARKAOS_ROOT),
|
|
371
|
+
capture_output=True, text=True, timeout=5,
|
|
372
|
+
)
|
|
373
|
+
except (subprocess.SubprocessError, OSError):
|
|
374
|
+
return []
|
|
375
|
+
if result.returncode != 0:
|
|
376
|
+
return []
|
|
377
|
+
rows: list[dict] = []
|
|
378
|
+
for line in result.stdout.strip().split("\n"):
|
|
379
|
+
if not line:
|
|
380
|
+
continue
|
|
381
|
+
parts = line.split("\t", 3)
|
|
382
|
+
if len(parts) < 4:
|
|
383
|
+
continue
|
|
384
|
+
sha, iso, author, subject = parts
|
|
385
|
+
rows.append({
|
|
386
|
+
"kind": "git-commit",
|
|
387
|
+
"ts": iso,
|
|
388
|
+
"summary": subject,
|
|
389
|
+
"ref": sha[:8],
|
|
390
|
+
"author": author,
|
|
391
|
+
})
|
|
392
|
+
return rows
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _trash_ts_to_iso(ts: object) -> str | None:
|
|
396
|
+
if ts is None:
|
|
397
|
+
return None
|
|
398
|
+
try:
|
|
399
|
+
from datetime import datetime, timezone
|
|
400
|
+
return datetime.fromtimestamp(float(ts), tz=timezone.utc).isoformat()
|
|
401
|
+
except (TypeError, ValueError, OSError):
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _trash_summary(entry: dict) -> str:
|
|
406
|
+
kind = entry.get("kind") or ""
|
|
407
|
+
if kind == "agent-move":
|
|
408
|
+
new_path = entry.get("new_path") or ""
|
|
409
|
+
return f"Moved to {Path(new_path).parent.parent.name if new_path else '?'}"
|
|
410
|
+
if kind == "agent-delete":
|
|
411
|
+
return "Deleted (restorable from /trash)"
|
|
412
|
+
return kind
|
|
413
|
+
|
|
414
|
+
|
|
325
415
|
@app.get("/api/agents/{agent_id}/activity")
|
|
326
416
|
def agent_activity_detail(agent_id: str, period: str = "month"):
|
|
327
417
|
"""PR86b v3.16.0 — alias for /activity-strip. Same payload shape.
|
|
@@ -839,6 +929,23 @@ def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
|
|
|
839
929
|
return {"results": results, "query": q, "total": len(results)}
|
|
840
930
|
|
|
841
931
|
|
|
932
|
+
@app.get("/api/knowledge/sources")
|
|
933
|
+
def knowledge_list_sources():
|
|
934
|
+
"""PR88c v3.25.0 — list every distinct source + chunk count.
|
|
935
|
+
|
|
936
|
+
Returns ``{sources: [{source, chunks}], total: N}``. Sorted
|
|
937
|
+
descending by chunk count.
|
|
938
|
+
"""
|
|
939
|
+
store = _get_vector_store()
|
|
940
|
+
if not store:
|
|
941
|
+
return {"sources": [], "total": 0, "error": "vector store unavailable"}
|
|
942
|
+
try:
|
|
943
|
+
rows = store.list_sources()
|
|
944
|
+
except Exception as exc: # noqa: BLE001
|
|
945
|
+
return {"sources": [], "total": 0, "error": str(exc)}
|
|
946
|
+
return {"sources": rows, "total": len(rows)}
|
|
947
|
+
|
|
948
|
+
|
|
842
949
|
@app.delete("/api/knowledge/sources")
|
|
843
950
|
def knowledge_delete_source(source: str = Query(...)):
|
|
844
951
|
"""PR71 v2.88.0 — remove all chunks from a given source.
|