arkaos 3.21.0 → 3.23.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/components/SidebarStatsWidget.vue +61 -0
- package/dashboard/app/layouts/default.vue +4 -1
- package/dashboard/app/pages/personas/[id].vue +23 -0
- package/dashboard/app/pages/personas/compare-with-agent.vue +218 -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 +37 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.23.0
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR87d v3.22.0 — compact stats card mounted at the bottom of the
|
|
3
|
+
// sidebar. Polls /api/sidebar-stats every 60s.
|
|
4
|
+
|
|
5
|
+
interface SidebarStats {
|
|
6
|
+
agents: number
|
|
7
|
+
personas: number
|
|
8
|
+
departments: number
|
|
9
|
+
today_cost_usd: number | null
|
|
10
|
+
today_calls: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { fetchApi } = useApi()
|
|
14
|
+
const { data, refresh } = fetchApi<SidebarStats>('/api/sidebar-stats')
|
|
15
|
+
|
|
16
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
timer = setInterval(() => { refresh() }, 60_000)
|
|
19
|
+
})
|
|
20
|
+
onBeforeUnmount(() => {
|
|
21
|
+
if (timer) clearInterval(timer)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function formatCost(cost: number | null): string {
|
|
25
|
+
if (cost === null || cost === undefined) return '—'
|
|
26
|
+
if (cost < 0.01) return '<$0.01'
|
|
27
|
+
if (cost < 1) return `$${cost.toFixed(3)}`
|
|
28
|
+
return `$${cost.toFixed(2)}`
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div
|
|
34
|
+
v-if="data"
|
|
35
|
+
class="rounded-lg border border-default bg-elevated/20 p-3 mx-2 mb-2 text-xs space-y-1.5"
|
|
36
|
+
aria-label="Workspace quick stats"
|
|
37
|
+
>
|
|
38
|
+
<div class="flex items-center justify-between">
|
|
39
|
+
<span class="text-muted">Agents</span>
|
|
40
|
+
<span class="font-mono font-semibold">{{ data.agents }}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="flex items-center justify-between">
|
|
43
|
+
<span class="text-muted">Personas</span>
|
|
44
|
+
<span class="font-mono font-semibold">{{ data.personas }}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex items-center justify-between">
|
|
47
|
+
<span class="text-muted">Departments</span>
|
|
48
|
+
<span class="font-mono font-semibold">{{ data.departments }}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="border-t border-default/60 mt-2 pt-1.5 flex items-center justify-between">
|
|
51
|
+
<span class="text-muted">Today</span>
|
|
52
|
+
<span class="font-mono font-semibold text-primary">
|
|
53
|
+
{{ formatCost(data.today_cost_usd) }}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex items-center justify-between text-[10px] text-muted/70">
|
|
57
|
+
<span>{{ data.today_calls }} call{{ data.today_calls === 1 ? '' : 's' }}</span>
|
|
58
|
+
<span>auto · 60s</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
@@ -121,12 +121,15 @@ const links = [[{
|
|
|
121
121
|
popover
|
|
122
122
|
/>
|
|
123
123
|
|
|
124
|
+
<!-- PR87d v3.22.0 — quick stats widget above the bottom nav. -->
|
|
125
|
+
<SidebarStatsWidget v-if="!collapsed" class="mt-auto" />
|
|
126
|
+
|
|
124
127
|
<UNavigationMenu
|
|
125
128
|
:collapsed="collapsed"
|
|
126
129
|
:items="links[1]"
|
|
127
130
|
orientation="vertical"
|
|
128
131
|
tooltip
|
|
129
|
-
class="mt-auto"
|
|
132
|
+
:class="collapsed ? 'mt-auto' : ''"
|
|
130
133
|
/>
|
|
131
134
|
</template>
|
|
132
135
|
</UDashboardSidebar>
|
|
@@ -247,6 +247,17 @@ function onCloned(agentId: string) {
|
|
|
247
247
|
navigateTo(`/agents/${agentId}`)
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// PR88a v3.23.0 — Compare with linked agent.
|
|
251
|
+
const compareWithOptions = computed(() =>
|
|
252
|
+
linkedAgentIds.value.map((aid) => ({
|
|
253
|
+
label: `Compare with ${aid}`,
|
|
254
|
+
icon: 'i-lucide-columns-2',
|
|
255
|
+
onSelect: () => navigateTo(
|
|
256
|
+
`/personas/compare-with-agent?persona=${personaId}&agent=${aid}`,
|
|
257
|
+
),
|
|
258
|
+
})),
|
|
259
|
+
)
|
|
260
|
+
|
|
250
261
|
// PR84c v3.9.0 — Auto-fill empty lists in one go.
|
|
251
262
|
const autofilling = ref(false)
|
|
252
263
|
|
|
@@ -496,6 +507,18 @@ const vocabOptions = [
|
|
|
496
507
|
:aria-label="favs.isPersonaFavorite(detail.id) ? 'Unfavorite' : 'Favorite'"
|
|
497
508
|
@click="favs.toggle('personas', detail.id)"
|
|
498
509
|
/>
|
|
510
|
+
<UDropdownMenu
|
|
511
|
+
v-if="compareWithOptions.length > 0"
|
|
512
|
+
:items="compareWithOptions"
|
|
513
|
+
>
|
|
514
|
+
<UButton
|
|
515
|
+
label="Compare"
|
|
516
|
+
icon="i-lucide-columns-2"
|
|
517
|
+
variant="soft"
|
|
518
|
+
size="sm"
|
|
519
|
+
trailing-icon="i-lucide-chevron-down"
|
|
520
|
+
/>
|
|
521
|
+
</UDropdownMenu>
|
|
499
522
|
<UButton
|
|
500
523
|
label="Clone to Agent"
|
|
501
524
|
icon="i-lucide-copy-plus"
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR88a v3.23.0 — Compare a persona against one of its linked agents.
|
|
3
|
+
//
|
|
4
|
+
// Driven by `?persona=p&agent=a`. Useful to see how an agent diverged
|
|
5
|
+
// from the persona it was cloned from (or whose DNA inspired it).
|
|
6
|
+
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const { fetchApi } = useApi()
|
|
9
|
+
|
|
10
|
+
const personaId = computed(() => String(route.query.persona ?? ''))
|
|
11
|
+
const agentId = computed(() => String(route.query.agent ?? ''))
|
|
12
|
+
|
|
13
|
+
interface PersonaDetail {
|
|
14
|
+
id: string
|
|
15
|
+
name?: string
|
|
16
|
+
title?: string
|
|
17
|
+
source?: string
|
|
18
|
+
mbti?: string
|
|
19
|
+
disc?: { primary?: string, secondary?: string }
|
|
20
|
+
enneagram?: { type?: number, wing?: number }
|
|
21
|
+
big_five?: {
|
|
22
|
+
openness?: number
|
|
23
|
+
conscientiousness?: number
|
|
24
|
+
extraversion?: number
|
|
25
|
+
agreeableness?: number
|
|
26
|
+
neuroticism?: number
|
|
27
|
+
}
|
|
28
|
+
mental_models?: string[]
|
|
29
|
+
expertise_domains?: string[]
|
|
30
|
+
frameworks?: string[]
|
|
31
|
+
communication?: { tone?: string, vocabulary_level?: string, avoid?: string[] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AgentDetail {
|
|
35
|
+
id: string
|
|
36
|
+
name?: string
|
|
37
|
+
role?: string
|
|
38
|
+
department?: string
|
|
39
|
+
mbti?: string
|
|
40
|
+
disc?: { primary?: string, secondary?: string }
|
|
41
|
+
enneagram?: { type?: number, wing?: number }
|
|
42
|
+
big_five?: PersonaDetail['big_five']
|
|
43
|
+
mental_models?: { primary?: string[], secondary?: string[] }
|
|
44
|
+
expertise?: { domains?: string[], frameworks?: string[] }
|
|
45
|
+
communication?: { tone?: string, vocabulary_level?: string, avoid?: string[] }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { data: persona, status: pStatus } = fetchApi<PersonaDetail>(
|
|
49
|
+
() => personaId.value ? `/api/personas/${personaId.value}` : '',
|
|
50
|
+
)
|
|
51
|
+
const { data: agent, status: aStatus } = fetchApi<AgentDetail>(
|
|
52
|
+
() => agentId.value ? `/api/agents/${agentId.value}` : '',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const loading = computed(() => pStatus.value === 'pending' || aStatus.value === 'pending')
|
|
56
|
+
const errorMsg = computed(() => {
|
|
57
|
+
if (!personaId.value || !agentId.value) {
|
|
58
|
+
return 'Pass ?persona=p&agent=a'
|
|
59
|
+
}
|
|
60
|
+
if (persona.value && (persona.value as any).error) return `Persona: ${(persona.value as any).error}`
|
|
61
|
+
if (agent.value && (agent.value as any).error) return `Agent: ${(agent.value as any).error}`
|
|
62
|
+
return null
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
function diffClass(left: unknown, right: unknown): string {
|
|
66
|
+
return left !== right
|
|
67
|
+
? 'bg-yellow-500/10 border-yellow-500/30'
|
|
68
|
+
: ''
|
|
69
|
+
}
|
|
70
|
+
function listDiffClass(left: unknown[] | undefined, right: unknown[] | undefined): string {
|
|
71
|
+
const a = JSON.stringify([...(left ?? [])].sort())
|
|
72
|
+
const b = JSON.stringify([...(right ?? [])].sort())
|
|
73
|
+
return a !== b ? 'bg-yellow-500/10 border-yellow-500/30' : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<UDashboardPanel id="persona-vs-agent">
|
|
81
|
+
<template #header>
|
|
82
|
+
<UDashboardNavbar title="Persona vs Agent">
|
|
83
|
+
<template #leading>
|
|
84
|
+
<UButton
|
|
85
|
+
v-if="personaId"
|
|
86
|
+
icon="i-lucide-arrow-left"
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="sm"
|
|
89
|
+
:to="`/personas/${personaId}`"
|
|
90
|
+
aria-label="Back to persona"
|
|
91
|
+
/>
|
|
92
|
+
</template>
|
|
93
|
+
<template #trailing>
|
|
94
|
+
<UBadge label="diverge view" variant="subtle" size="sm" />
|
|
95
|
+
</template>
|
|
96
|
+
</UDashboardNavbar>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<template #body>
|
|
100
|
+
<div v-if="errorMsg" class="p-6 text-center text-sm text-error">
|
|
101
|
+
{{ errorMsg }}
|
|
102
|
+
</div>
|
|
103
|
+
<div v-else-if="loading" class="p-6 text-center text-sm text-muted">
|
|
104
|
+
<UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline" /> Loading…
|
|
105
|
+
</div>
|
|
106
|
+
<div v-else-if="persona && agent" class="space-y-4 max-w-6xl">
|
|
107
|
+
<section class="grid grid-cols-2 gap-3">
|
|
108
|
+
<NuxtLink :to="`/personas/${persona.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
|
|
109
|
+
<p class="text-xs text-muted uppercase tracking-wide">Persona</p>
|
|
110
|
+
<h2 class="text-xl font-bold">{{ persona.name }}</h2>
|
|
111
|
+
<p class="text-sm text-muted">{{ persona.title || '—' }}</p>
|
|
112
|
+
</NuxtLink>
|
|
113
|
+
<NuxtLink :to="`/agents/${agent.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
|
|
114
|
+
<p class="text-xs text-muted uppercase tracking-wide">Agent</p>
|
|
115
|
+
<h2 class="text-xl font-bold">{{ agent.name }}</h2>
|
|
116
|
+
<p class="text-sm text-muted">{{ agent.role }} · {{ agent.department }}</p>
|
|
117
|
+
</NuxtLink>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Behavioural DNA</h3>
|
|
121
|
+
<div class="grid grid-cols-2 gap-3">
|
|
122
|
+
<div :class="['rounded-lg border p-3', diffClass(persona.mbti, agent.mbti)]">
|
|
123
|
+
<p class="text-xs text-muted">MBTI</p>
|
|
124
|
+
<p class="text-lg font-mono font-bold">{{ persona.mbti ?? '—' }}</p>
|
|
125
|
+
</div>
|
|
126
|
+
<div :class="['rounded-lg border p-3', diffClass(persona.mbti, agent.mbti)]">
|
|
127
|
+
<p class="text-xs text-muted">MBTI</p>
|
|
128
|
+
<p class="text-lg font-mono font-bold">{{ agent.mbti ?? '—' }}</p>
|
|
129
|
+
</div>
|
|
130
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.disc?.primary}/${persona.disc?.secondary}`, `${agent.disc?.primary}/${agent.disc?.secondary}`)]">
|
|
131
|
+
<p class="text-xs text-muted">DISC</p>
|
|
132
|
+
<p class="text-lg font-mono font-bold">{{ persona.disc?.primary ?? '?' }}/{{ persona.disc?.secondary ?? '?' }}</p>
|
|
133
|
+
</div>
|
|
134
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.disc?.primary}/${persona.disc?.secondary}`, `${agent.disc?.primary}/${agent.disc?.secondary}`)]">
|
|
135
|
+
<p class="text-xs text-muted">DISC</p>
|
|
136
|
+
<p class="text-lg font-mono font-bold">{{ agent.disc?.primary ?? '?' }}/{{ agent.disc?.secondary ?? '?' }}</p>
|
|
137
|
+
</div>
|
|
138
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.enneagram?.type}w${persona.enneagram?.wing}`, `${agent.enneagram?.type}w${agent.enneagram?.wing}`)]">
|
|
139
|
+
<p class="text-xs text-muted">Enneagram</p>
|
|
140
|
+
<p class="text-lg font-mono font-bold">{{ persona.enneagram?.type ?? '?' }}w{{ persona.enneagram?.wing ?? '?' }}</p>
|
|
141
|
+
</div>
|
|
142
|
+
<div :class="['rounded-lg border p-3', diffClass(`${persona.enneagram?.type}w${persona.enneagram?.wing}`, `${agent.enneagram?.type}w${agent.enneagram?.wing}`)]">
|
|
143
|
+
<p class="text-xs text-muted">Enneagram</p>
|
|
144
|
+
<p class="text-lg font-mono font-bold">{{ agent.enneagram?.type ?? '?' }}w{{ agent.enneagram?.wing ?? '?' }}</p>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Big Five (OCEAN)</h3>
|
|
149
|
+
<div class="space-y-1">
|
|
150
|
+
<div v-for="k in bigFiveKeys" :key="k" class="grid grid-cols-2 gap-3">
|
|
151
|
+
<div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(persona.big_five?.[k], agent.big_five?.[k])]">
|
|
152
|
+
<span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
|
|
153
|
+
<span class="font-mono text-sm">{{ persona.big_five?.[k] ?? '—' }}</span>
|
|
154
|
+
</div>
|
|
155
|
+
<div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(persona.big_five?.[k], agent.big_five?.[k])]">
|
|
156
|
+
<span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
|
|
157
|
+
<span class="font-mono text-sm">{{ agent.big_five?.[k] ?? '—' }}</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Expertise domains</h3>
|
|
163
|
+
<div class="grid grid-cols-2 gap-3">
|
|
164
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.expertise_domains, agent.expertise?.domains)]">
|
|
165
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
166
|
+
<li v-for="d in persona.expertise_domains" :key="d">{{ d }}</li>
|
|
167
|
+
<li v-if="!persona.expertise_domains?.length" class="list-none text-muted italic">none</li>
|
|
168
|
+
</ul>
|
|
169
|
+
</div>
|
|
170
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.expertise_domains, agent.expertise?.domains)]">
|
|
171
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
172
|
+
<li v-for="d in agent.expertise?.domains" :key="d">{{ d }}</li>
|
|
173
|
+
<li v-if="!agent.expertise?.domains?.length" class="list-none text-muted italic">none</li>
|
|
174
|
+
</ul>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Frameworks</h3>
|
|
179
|
+
<div class="grid grid-cols-2 gap-3">
|
|
180
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.frameworks, agent.expertise?.frameworks)]">
|
|
181
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
182
|
+
<li v-for="f in persona.frameworks" :key="f">{{ f }}</li>
|
|
183
|
+
<li v-if="!persona.frameworks?.length" class="list-none text-muted italic">none</li>
|
|
184
|
+
</ul>
|
|
185
|
+
</div>
|
|
186
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.frameworks, agent.expertise?.frameworks)]">
|
|
187
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
188
|
+
<li v-for="f in agent.expertise?.frameworks" :key="f">{{ f }}</li>
|
|
189
|
+
<li v-if="!agent.expertise?.frameworks?.length" class="list-none text-muted italic">none</li>
|
|
190
|
+
</ul>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Mental models</h3>
|
|
195
|
+
<div class="grid grid-cols-2 gap-3">
|
|
196
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.mental_models, agent.mental_models?.primary)]">
|
|
197
|
+
<p class="text-xs text-muted mb-2">Persona — flat list</p>
|
|
198
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
199
|
+
<li v-for="m in persona.mental_models" :key="m">{{ m }}</li>
|
|
200
|
+
<li v-if="!persona.mental_models?.length" class="list-none text-muted italic">none</li>
|
|
201
|
+
</ul>
|
|
202
|
+
</div>
|
|
203
|
+
<div :class="['rounded-lg border p-3', listDiffClass(persona.mental_models, agent.mental_models?.primary)]">
|
|
204
|
+
<p class="text-xs text-muted mb-2">Agent — primary</p>
|
|
205
|
+
<ul class="list-disc list-inside text-sm space-y-1">
|
|
206
|
+
<li v-for="m in agent.mental_models?.primary" :key="m">{{ m }}</li>
|
|
207
|
+
<li v-if="!agent.mental_models?.primary?.length" class="list-none text-muted italic">none</li>
|
|
208
|
+
</ul>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<p class="text-xs text-muted pt-4 italic">
|
|
213
|
+
Cells with a yellow tint differ between persona and agent.
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
</template>
|
|
217
|
+
</UDashboardPanel>
|
|
218
|
+
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1387,6 +1387,43 @@ def agent_export_to_vault(agent_id: str):
|
|
|
1387
1387
|
return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
|
|
1388
1388
|
|
|
1389
1389
|
|
|
1390
|
+
# --- Sidebar stats widget (PR87d v3.22.0) ---
|
|
1391
|
+
|
|
1392
|
+
@app.get("/api/sidebar-stats")
|
|
1393
|
+
def sidebar_stats():
|
|
1394
|
+
"""Compact payload for the sidebar status widget.
|
|
1395
|
+
|
|
1396
|
+
Returns counts the widget actually displays — agents, personas,
|
|
1397
|
+
departments, today's spend. Cheaper than /api/overview/command-center
|
|
1398
|
+
because it skips project scanning, incidents, and quick actions.
|
|
1399
|
+
"""
|
|
1400
|
+
agents = _load_agents()
|
|
1401
|
+
departments = {a.get("department") for a in agents if a.get("department")}
|
|
1402
|
+
persona_count = 0
|
|
1403
|
+
mgr = _get_persona_manager()
|
|
1404
|
+
if mgr:
|
|
1405
|
+
try:
|
|
1406
|
+
persona_count = len(mgr.list() or [])
|
|
1407
|
+
except Exception:
|
|
1408
|
+
persona_count = 0
|
|
1409
|
+
today_cost_usd: float | None = None
|
|
1410
|
+
call_count = 0
|
|
1411
|
+
try:
|
|
1412
|
+
from core.runtime.llm_cost_telemetry import summarise
|
|
1413
|
+
s = summarise(period="today")
|
|
1414
|
+
today_cost_usd = s.total_cost_usd
|
|
1415
|
+
call_count = s.call_count
|
|
1416
|
+
except Exception:
|
|
1417
|
+
pass
|
|
1418
|
+
return {
|
|
1419
|
+
"agents": len(agents),
|
|
1420
|
+
"personas": persona_count,
|
|
1421
|
+
"departments": len(departments),
|
|
1422
|
+
"today_cost_usd": today_cost_usd,
|
|
1423
|
+
"today_calls": call_count,
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
|
|
1390
1427
|
# --- Favorites (PR86a v3.15.0) ---
|
|
1391
1428
|
|
|
1392
1429
|
@app.get("/api/favorites")
|