arkaos 3.30.0 → 3.32.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/departments/[dept].vue +26 -0
- package/dashboard/app/pages/departments/compare.vue +192 -0
- package/dashboard/app/pages/personas/[id].vue +43 -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 +49 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.32.0
|
|
@@ -37,6 +37,21 @@ const { data, status, error, refresh } = await fetchApi<DeptDetail>(
|
|
|
37
37
|
`/api/departments/${deptId.value}`,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
+
// PR90b v3.32.0 — Compare with another department.
|
|
41
|
+
const { data: deptListData } = fetchApi<{ departments: Array<{ department: string }> }>(
|
|
42
|
+
'/api/departments',
|
|
43
|
+
)
|
|
44
|
+
const compareOptions = computed(() =>
|
|
45
|
+
(deptListData.value?.departments ?? [])
|
|
46
|
+
.filter((d) => d.department !== deptId.value)
|
|
47
|
+
.map((d) => ({
|
|
48
|
+
label: `Compare with ${d.department}`,
|
|
49
|
+
icon: 'i-lucide-columns-2',
|
|
50
|
+
onSelect: () =>
|
|
51
|
+
navigateTo(`/departments/compare?a=${deptId.value}&b=${d.department}`),
|
|
52
|
+
})),
|
|
53
|
+
)
|
|
54
|
+
|
|
40
55
|
const errorMsg = computed(() => data.value?.error || error.value?.message || null)
|
|
41
56
|
const detail = computed<DeptDetail | null>(() => {
|
|
42
57
|
if (!data.value || data.value.error) return null
|
|
@@ -65,6 +80,17 @@ const tierColor = (tier: number | undefined) => {
|
|
|
65
80
|
<template #leading>
|
|
66
81
|
<UButton icon="i-lucide-arrow-left" variant="ghost" size="sm" to="/departments" aria-label="Back" />
|
|
67
82
|
</template>
|
|
83
|
+
<template #right>
|
|
84
|
+
<UDropdownMenu v-if="compareOptions.length > 0" :items="compareOptions">
|
|
85
|
+
<UButton
|
|
86
|
+
label="Compare"
|
|
87
|
+
icon="i-lucide-columns-2"
|
|
88
|
+
variant="soft"
|
|
89
|
+
size="sm"
|
|
90
|
+
trailing-icon="i-lucide-chevron-down"
|
|
91
|
+
/>
|
|
92
|
+
</UDropdownMenu>
|
|
93
|
+
</template>
|
|
68
94
|
</UDashboardNavbar>
|
|
69
95
|
</template>
|
|
70
96
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR90b v3.32.0 — Compare two departments side-by-side.
|
|
3
|
+
//
|
|
4
|
+
// Driven by `?a=dept1&b=dept2`. Reuses /api/departments/{id}. Shows
|
|
5
|
+
// agent count + tier distribution + workflows count + 30d cost.
|
|
6
|
+
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const { fetchApi } = useApi()
|
|
9
|
+
|
|
10
|
+
const deptA = computed(() => String(route.query.a ?? ''))
|
|
11
|
+
const deptB = computed(() => String(route.query.b ?? ''))
|
|
12
|
+
|
|
13
|
+
interface AgentLite {
|
|
14
|
+
id: string
|
|
15
|
+
name?: string
|
|
16
|
+
role?: string
|
|
17
|
+
tier?: number
|
|
18
|
+
mbti?: string
|
|
19
|
+
}
|
|
20
|
+
interface WorkflowLite {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
tier: string
|
|
24
|
+
command: string
|
|
25
|
+
phases_count: number
|
|
26
|
+
}
|
|
27
|
+
interface DeptDetail {
|
|
28
|
+
department: string
|
|
29
|
+
agents: AgentLite[]
|
|
30
|
+
workflows: WorkflowLite[]
|
|
31
|
+
calls_30d: number
|
|
32
|
+
cost_usd_30d: number | null
|
|
33
|
+
error?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { data: a } = fetchApi<DeptDetail>(
|
|
37
|
+
() => deptA.value ? `/api/departments/${deptA.value}` : '',
|
|
38
|
+
)
|
|
39
|
+
const { data: b } = fetchApi<DeptDetail>(
|
|
40
|
+
() => deptB.value ? `/api/departments/${deptB.value}` : '',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const errorMsg = computed(() => {
|
|
44
|
+
if (!deptA.value || !deptB.value) return 'Pass ?a=dept1&b=dept2'
|
|
45
|
+
if (a.value?.error) return `Left: ${a.value.error}`
|
|
46
|
+
if (b.value?.error) return `Right: ${b.value.error}`
|
|
47
|
+
return null
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function diffClass(left: unknown, right: unknown): string {
|
|
51
|
+
return left !== right ? 'bg-yellow-500/10 border-yellow-500/30' : ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatCost(cost: number | null | undefined): string {
|
|
55
|
+
if (cost === null || cost === undefined) return '—'
|
|
56
|
+
if (cost < 0.01) return '<$0.01'
|
|
57
|
+
if (cost < 1) return `$${cost.toFixed(3)}`
|
|
58
|
+
return `$${cost.toFixed(2)}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tierColor = (tier: number | undefined) => {
|
|
62
|
+
const m: Record<number, 'error' | 'warning' | 'primary' | 'neutral'> = {
|
|
63
|
+
0: 'error', 1: 'warning', 2: 'primary', 3: 'neutral',
|
|
64
|
+
}
|
|
65
|
+
return m[tier ?? 99] ?? 'neutral'
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<UDashboardPanel id="departments-compare">
|
|
71
|
+
<template #header>
|
|
72
|
+
<UDashboardNavbar title="Compare departments">
|
|
73
|
+
<template #leading>
|
|
74
|
+
<UButton icon="i-lucide-arrow-left" variant="ghost" size="sm" to="/departments" aria-label="Back" />
|
|
75
|
+
</template>
|
|
76
|
+
<template #trailing>
|
|
77
|
+
<UBadge label="2-way" variant="subtle" size="sm" />
|
|
78
|
+
</template>
|
|
79
|
+
</UDashboardNavbar>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<template #body>
|
|
83
|
+
<div v-if="errorMsg" class="p-6 text-center text-sm text-error">{{ errorMsg }}</div>
|
|
84
|
+
<div v-else-if="!a || !b" class="p-6 text-center text-sm text-muted">
|
|
85
|
+
<UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline" /> Loading…
|
|
86
|
+
</div>
|
|
87
|
+
<div v-else class="space-y-4 max-w-6xl">
|
|
88
|
+
<section class="grid grid-cols-2 gap-3">
|
|
89
|
+
<NuxtLink :to="`/departments/${a.department}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
|
|
90
|
+
<p class="text-xs text-muted uppercase tracking-wide">Left</p>
|
|
91
|
+
<h2 class="text-xl font-bold capitalize">{{ a.department }}</h2>
|
|
92
|
+
</NuxtLink>
|
|
93
|
+
<NuxtLink :to="`/departments/${b.department}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
|
|
94
|
+
<p class="text-xs text-muted uppercase tracking-wide">Right</p>
|
|
95
|
+
<h2 class="text-xl font-bold capitalize">{{ b.department }}</h2>
|
|
96
|
+
</NuxtLink>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Stats</h3>
|
|
100
|
+
<div class="grid grid-cols-2 gap-3">
|
|
101
|
+
<div :class="['rounded-lg border p-3', diffClass(a.agents.length, b.agents.length)]">
|
|
102
|
+
<p class="text-xs text-muted">Agents</p>
|
|
103
|
+
<p class="text-2xl font-bold">{{ a.agents.length }}</p>
|
|
104
|
+
</div>
|
|
105
|
+
<div :class="['rounded-lg border p-3', diffClass(a.agents.length, b.agents.length)]">
|
|
106
|
+
<p class="text-xs text-muted">Agents</p>
|
|
107
|
+
<p class="text-2xl font-bold">{{ b.agents.length }}</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div :class="['rounded-lg border p-3', diffClass(a.workflows.length, b.workflows.length)]">
|
|
110
|
+
<p class="text-xs text-muted">Workflows</p>
|
|
111
|
+
<p class="text-2xl font-bold">{{ a.workflows.length }}</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div :class="['rounded-lg border p-3', diffClass(a.workflows.length, b.workflows.length)]">
|
|
114
|
+
<p class="text-xs text-muted">Workflows</p>
|
|
115
|
+
<p class="text-2xl font-bold">{{ b.workflows.length }}</p>
|
|
116
|
+
</div>
|
|
117
|
+
<div :class="['rounded-lg border p-3', diffClass(a.calls_30d, b.calls_30d)]">
|
|
118
|
+
<p class="text-xs text-muted">Calls (30d)</p>
|
|
119
|
+
<p class="text-2xl font-bold">{{ a.calls_30d }}</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div :class="['rounded-lg border p-3', diffClass(a.calls_30d, b.calls_30d)]">
|
|
122
|
+
<p class="text-xs text-muted">Calls (30d)</p>
|
|
123
|
+
<p class="text-2xl font-bold">{{ b.calls_30d }}</p>
|
|
124
|
+
</div>
|
|
125
|
+
<div :class="['rounded-lg border p-3', diffClass(a.cost_usd_30d, b.cost_usd_30d)]">
|
|
126
|
+
<p class="text-xs text-muted">Cost (30d)</p>
|
|
127
|
+
<p class="text-2xl font-bold">{{ formatCost(a.cost_usd_30d) }}</p>
|
|
128
|
+
</div>
|
|
129
|
+
<div :class="['rounded-lg border p-3', diffClass(a.cost_usd_30d, b.cost_usd_30d)]">
|
|
130
|
+
<p class="text-xs text-muted">Cost (30d)</p>
|
|
131
|
+
<p class="text-2xl font-bold">{{ formatCost(b.cost_usd_30d) }}</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Agents</h3>
|
|
136
|
+
<div class="grid grid-cols-2 gap-3">
|
|
137
|
+
<div class="rounded-lg border border-default p-3 space-y-1">
|
|
138
|
+
<NuxtLink
|
|
139
|
+
v-for="ag in a.agents"
|
|
140
|
+
:key="ag.id"
|
|
141
|
+
:to="`/agents/${ag.id}`"
|
|
142
|
+
class="flex items-center gap-2 text-sm hover:text-primary truncate"
|
|
143
|
+
>
|
|
144
|
+
<UBadge :label="`T${ag.tier}`" :color="tierColor(ag.tier)" variant="subtle" size="xs" />
|
|
145
|
+
<span class="font-medium truncate">{{ ag.name }}</span>
|
|
146
|
+
<span class="text-xs text-muted truncate">— {{ ag.role }}</span>
|
|
147
|
+
</NuxtLink>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="rounded-lg border border-default p-3 space-y-1">
|
|
150
|
+
<NuxtLink
|
|
151
|
+
v-for="ag in b.agents"
|
|
152
|
+
:key="ag.id"
|
|
153
|
+
:to="`/agents/${ag.id}`"
|
|
154
|
+
class="flex items-center gap-2 text-sm hover:text-primary truncate"
|
|
155
|
+
>
|
|
156
|
+
<UBadge :label="`T${ag.tier}`" :color="tierColor(ag.tier)" variant="subtle" size="xs" />
|
|
157
|
+
<span class="font-medium truncate">{{ ag.name }}</span>
|
|
158
|
+
<span class="text-xs text-muted truncate">— {{ ag.role }}</span>
|
|
159
|
+
</NuxtLink>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<h3
|
|
164
|
+
v-if="a.workflows.length > 0 || b.workflows.length > 0"
|
|
165
|
+
class="text-sm font-semibold uppercase tracking-wide text-muted pt-2"
|
|
166
|
+
>
|
|
167
|
+
Workflows
|
|
168
|
+
</h3>
|
|
169
|
+
<div v-if="a.workflows.length > 0 || b.workflows.length > 0" class="grid grid-cols-2 gap-3">
|
|
170
|
+
<div class="rounded-lg border border-default p-3 space-y-1">
|
|
171
|
+
<p v-for="w in a.workflows" :key="w.id" class="text-sm truncate">
|
|
172
|
+
<span class="font-mono text-xs text-muted">{{ w.command || w.id }}</span>
|
|
173
|
+
· {{ w.name }}
|
|
174
|
+
</p>
|
|
175
|
+
<p v-if="!a.workflows.length" class="text-sm text-muted italic">No workflows</p>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="rounded-lg border border-default p-3 space-y-1">
|
|
178
|
+
<p v-for="w in b.workflows" :key="w.id" class="text-sm truncate">
|
|
179
|
+
<span class="font-mono text-xs text-muted">{{ w.command || w.id }}</span>
|
|
180
|
+
· {{ w.name }}
|
|
181
|
+
</p>
|
|
182
|
+
<p v-if="!b.workflows.length" class="text-sm text-muted italic">No workflows</p>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<p class="text-xs text-muted pt-4 italic">
|
|
187
|
+
Cells with a yellow tint differ between the two departments.
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
191
|
+
</UDashboardPanel>
|
|
192
|
+
</template>
|
|
@@ -241,6 +241,41 @@ function markedHtml(src: string): string {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
// PR90a v3.31.0 — download persona as Markdown.
|
|
245
|
+
const downloadingMd = ref(false)
|
|
246
|
+
async function downloadMarkdown() {
|
|
247
|
+
if (!detail.value) return
|
|
248
|
+
downloadingMd.value = true
|
|
249
|
+
try {
|
|
250
|
+
const blob = await $fetch<Blob>(
|
|
251
|
+
`${apiBase}/api/personas/${personaId}/markdown`,
|
|
252
|
+
{ responseType: 'blob' },
|
|
253
|
+
)
|
|
254
|
+
const url = URL.createObjectURL(blob)
|
|
255
|
+
const a = document.createElement('a')
|
|
256
|
+
a.href = url
|
|
257
|
+
a.download = `${detail.value.name || personaId}.md`
|
|
258
|
+
document.body.appendChild(a)
|
|
259
|
+
a.click()
|
|
260
|
+
a.remove()
|
|
261
|
+
URL.revokeObjectURL(url)
|
|
262
|
+
toast.add({
|
|
263
|
+
title: 'Markdown downloaded',
|
|
264
|
+
description: `${detail.value.name || personaId}.md`,
|
|
265
|
+
color: 'success',
|
|
266
|
+
icon: 'i-lucide-download',
|
|
267
|
+
})
|
|
268
|
+
} catch (err) {
|
|
269
|
+
toast.add({
|
|
270
|
+
title: 'Download failed',
|
|
271
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
272
|
+
color: 'error',
|
|
273
|
+
})
|
|
274
|
+
} finally {
|
|
275
|
+
downloadingMd.value = false
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
244
279
|
// PR85a v3.11.0 — Clone to Agent dialog.
|
|
245
280
|
const cloneOpen = ref(false)
|
|
246
281
|
function onCloned(agentId: string) {
|
|
@@ -526,6 +561,14 @@ const vocabOptions = [
|
|
|
526
561
|
size="sm"
|
|
527
562
|
@click="cloneOpen = true"
|
|
528
563
|
/>
|
|
564
|
+
<UButton
|
|
565
|
+
label="MD"
|
|
566
|
+
icon="i-lucide-download"
|
|
567
|
+
variant="ghost"
|
|
568
|
+
size="sm"
|
|
569
|
+
:loading="downloadingMd"
|
|
570
|
+
@click="downloadMarkdown"
|
|
571
|
+
/>
|
|
529
572
|
<UButton label="Edit" icon="i-lucide-pencil" size="sm" @click="startEdit" />
|
|
530
573
|
<UButton
|
|
531
574
|
icon="i-lucide-trash-2"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -322,6 +322,55 @@ def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
|
|
325
|
+
@app.get("/api/personas/{persona_id}/markdown")
|
|
326
|
+
def persona_download_markdown(persona_id: str):
|
|
327
|
+
"""PR90a v3.31.0 — return the persona as a Markdown file.
|
|
328
|
+
|
|
329
|
+
Renders via ObsidianPersonaStore._render so the output matches
|
|
330
|
+
exactly what gets written when the operator clicks Save with a
|
|
331
|
+
configured vault. Responds with ``text/markdown`` and an
|
|
332
|
+
attachment Content-Disposition.
|
|
333
|
+
"""
|
|
334
|
+
detail = persona_detail(persona_id)
|
|
335
|
+
if "error" in detail:
|
|
336
|
+
return detail
|
|
337
|
+
from core.personas.obsidian_store import ObsidianPersonaStore
|
|
338
|
+
from core.personas.schema import (
|
|
339
|
+
Persona, PersonaDISC, PersonaEnneagram, PersonaBigFive, PersonaCommunication,
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
persona = Persona(
|
|
343
|
+
id=detail.get("id", ""),
|
|
344
|
+
name=detail.get("name", ""),
|
|
345
|
+
title=detail.get("title", ""),
|
|
346
|
+
tagline=detail.get("tagline", ""),
|
|
347
|
+
source=detail.get("source", ""),
|
|
348
|
+
disc=PersonaDISC(**(detail.get("disc") or {})),
|
|
349
|
+
enneagram=PersonaEnneagram(**(detail.get("enneagram") or {})),
|
|
350
|
+
big_five=PersonaBigFive(**(detail.get("big_five") or {})),
|
|
351
|
+
mbti=detail.get("mbti", "INTJ"),
|
|
352
|
+
mental_models=detail.get("mental_models") or [],
|
|
353
|
+
expertise_domains=detail.get("expertise_domains") or [],
|
|
354
|
+
frameworks=detail.get("frameworks") or [],
|
|
355
|
+
key_quotes=detail.get("key_quotes") or [],
|
|
356
|
+
communication=PersonaCommunication(**(detail.get("communication") or {})),
|
|
357
|
+
bio_md=detail.get("bio_md", "") or "",
|
|
358
|
+
created_at=detail.get("created_at", ""),
|
|
359
|
+
)
|
|
360
|
+
except (TypeError, ValueError) as exc:
|
|
361
|
+
return {"error": f"persona schema mismatch: {exc}"}
|
|
362
|
+
content = ObsidianPersonaStore._render(persona)
|
|
363
|
+
filename = f"{persona.name or persona.id}.md".replace("/", "-")
|
|
364
|
+
from fastapi import Response
|
|
365
|
+
return Response(
|
|
366
|
+
content=content,
|
|
367
|
+
media_type="text/markdown",
|
|
368
|
+
headers={
|
|
369
|
+
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
325
374
|
@app.get("/api/agents/{agent_id}/yaml")
|
|
326
375
|
def agent_download_yaml(agent_id: str):
|
|
327
376
|
"""PR89d v3.30.0 — return the raw YAML for the agent.
|