arkaos 3.15.0 → 3.17.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/__pycache__/favorites.cpython-313.pyc +0 -0
- package/core/agents/__pycache__/obsidian_export.cpython-313.pyc +0 -0
- package/core/agents/obsidian_export.py +171 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/llm_provider.py +2 -1
- package/dashboard/app/pages/agents/[id].vue +41 -3
- 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 +65 -13
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.17.0
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Export agent profiles to the Obsidian vault (PR86c v3.17.0).
|
|
2
|
+
|
|
3
|
+
Writes a Markdown file with YAML frontmatter + readable sections to
|
|
4
|
+
``<vault>/Agents/<id>.md``. Sibling to ``core/personas/obsidian_store``
|
|
5
|
+
but write-only — agents are still source-of-truth in their YAML files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from core.profile import ProfileManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_AGENTS_SUBDIR = "Agents"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentExportError(RuntimeError):
|
|
20
|
+
"""Raised when the vault is missing or the write fails."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AgentExportResult:
|
|
25
|
+
path: Path
|
|
26
|
+
vault_path: Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def export_agent_to_vault(agent: dict) -> AgentExportResult:
|
|
30
|
+
"""Render `agent` as Markdown and write it to the configured vault.
|
|
31
|
+
|
|
32
|
+
`agent` is the YAML-loaded dict for the agent (the same shape
|
|
33
|
+
returned by ``/api/agents/{id}``).
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(agent, dict) or not agent.get("id"):
|
|
36
|
+
raise AgentExportError("agent payload must include 'id'")
|
|
37
|
+
vault = _resolve_vault_path()
|
|
38
|
+
if vault is None:
|
|
39
|
+
raise AgentExportError(
|
|
40
|
+
"Obsidian vault is not configured (set vaultPath in profile)"
|
|
41
|
+
)
|
|
42
|
+
target_dir = vault / _AGENTS_SUBDIR
|
|
43
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
target = target_dir / f"{agent['id']}.md"
|
|
45
|
+
md = _render(agent)
|
|
46
|
+
tmp = target.with_suffix(target.suffix + ".tmp")
|
|
47
|
+
try:
|
|
48
|
+
tmp.write_text(md, encoding="utf-8")
|
|
49
|
+
tmp.replace(target)
|
|
50
|
+
except OSError as exc:
|
|
51
|
+
raise AgentExportError(f"write failed: {exc}") from exc
|
|
52
|
+
return AgentExportResult(path=target, vault_path=vault)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_vault_path() -> Path | None:
|
|
56
|
+
try:
|
|
57
|
+
profile = ProfileManager().read()
|
|
58
|
+
except Exception: # noqa: BLE001
|
|
59
|
+
return None
|
|
60
|
+
if not profile.vaultPath:
|
|
61
|
+
return None
|
|
62
|
+
path = Path(profile.vaultPath).expanduser()
|
|
63
|
+
return path if path.exists() else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _render(agent: dict) -> str:
|
|
67
|
+
"""Compose the Markdown body — YAML frontmatter + readable sections."""
|
|
68
|
+
fm_lines = [
|
|
69
|
+
"---",
|
|
70
|
+
"type: agent",
|
|
71
|
+
f"id: {agent.get('id', '')}",
|
|
72
|
+
f"name: {_yaml_str(agent.get('name', ''))}",
|
|
73
|
+
f"role: {_yaml_str(agent.get('role', ''))}",
|
|
74
|
+
f"department: {agent.get('department', '')}",
|
|
75
|
+
f"tier: {agent.get('tier', '')}",
|
|
76
|
+
f"model: {agent.get('model', '')}",
|
|
77
|
+
"---",
|
|
78
|
+
"",
|
|
79
|
+
]
|
|
80
|
+
sections: list[str] = []
|
|
81
|
+
sections.append(f"# {agent.get('name') or agent.get('id', '(unnamed)')}")
|
|
82
|
+
if agent.get("role"):
|
|
83
|
+
sections.append(f"*{agent['role']}* · `{agent.get('department', '')}`")
|
|
84
|
+
sections.append("")
|
|
85
|
+
|
|
86
|
+
dna = agent.get("behavioral_dna") or {}
|
|
87
|
+
if dna:
|
|
88
|
+
sections.append("## Behavioural DNA")
|
|
89
|
+
disc = dna.get("disc") or {}
|
|
90
|
+
enn = dna.get("enneagram") or {}
|
|
91
|
+
bf = dna.get("big_five") or {}
|
|
92
|
+
mbti = dna.get("mbti")
|
|
93
|
+
mbti_type = mbti.get("type") if isinstance(mbti, dict) else mbti
|
|
94
|
+
sections.append(f"- **DISC:** {disc.get('primary', '?')}/{disc.get('secondary', '?')}")
|
|
95
|
+
sections.append(f"- **Enneagram:** {enn.get('type', '?')}w{enn.get('wing', '?')}")
|
|
96
|
+
sections.append(f"- **MBTI:** {mbti_type or '—'}")
|
|
97
|
+
if bf:
|
|
98
|
+
ocean = " · ".join(
|
|
99
|
+
f"{k[:1].upper()}{int(v)}"
|
|
100
|
+
for k, v in bf.items()
|
|
101
|
+
if isinstance(v, (int, float))
|
|
102
|
+
)
|
|
103
|
+
if ocean:
|
|
104
|
+
sections.append(f"- **OCEAN:** {ocean}")
|
|
105
|
+
sections.append("")
|
|
106
|
+
|
|
107
|
+
exp = agent.get("expertise") or {}
|
|
108
|
+
if exp:
|
|
109
|
+
sections.append("## Expertise")
|
|
110
|
+
domains = exp.get("domains") or []
|
|
111
|
+
frameworks = exp.get("frameworks") or []
|
|
112
|
+
if domains:
|
|
113
|
+
sections.append("**Domains**")
|
|
114
|
+
sections.extend(f"- {d}" for d in domains)
|
|
115
|
+
sections.append("")
|
|
116
|
+
if frameworks:
|
|
117
|
+
sections.append("**Frameworks**")
|
|
118
|
+
sections.extend(f"- {f}" for f in frameworks)
|
|
119
|
+
sections.append("")
|
|
120
|
+
sections.append(f"*Depth: {exp.get('depth', '—')} · {exp.get('years_equivalent', '—')}y equivalent*")
|
|
121
|
+
sections.append("")
|
|
122
|
+
|
|
123
|
+
mm = agent.get("mental_models") or {}
|
|
124
|
+
if mm:
|
|
125
|
+
primary = mm.get("primary") or []
|
|
126
|
+
secondary = mm.get("secondary") or []
|
|
127
|
+
if primary or secondary:
|
|
128
|
+
sections.append("## Mental Models")
|
|
129
|
+
if primary:
|
|
130
|
+
sections.append("**Primary**")
|
|
131
|
+
sections.extend(f"- {m}" for m in primary)
|
|
132
|
+
sections.append("")
|
|
133
|
+
if secondary:
|
|
134
|
+
sections.append("**Secondary**")
|
|
135
|
+
sections.extend(f"- {m}" for m in secondary)
|
|
136
|
+
sections.append("")
|
|
137
|
+
|
|
138
|
+
comm = agent.get("communication") or {}
|
|
139
|
+
if comm and any(comm.values()):
|
|
140
|
+
sections.append("## Communication")
|
|
141
|
+
if comm.get("tone"):
|
|
142
|
+
sections.append(f"- **Tone:** {comm['tone']}")
|
|
143
|
+
if comm.get("vocabulary_level"):
|
|
144
|
+
sections.append(f"- **Vocabulary:** {comm['vocabulary_level']}")
|
|
145
|
+
if comm.get("preferred_format"):
|
|
146
|
+
sections.append(f"- **Preferred format:** {comm['preferred_format']}")
|
|
147
|
+
if comm.get("language"):
|
|
148
|
+
sections.append(f"- **Language:** {comm['language']}")
|
|
149
|
+
avoid = comm.get("avoid") or []
|
|
150
|
+
if avoid:
|
|
151
|
+
sections.append(f"- **Avoid:** {', '.join(avoid)}")
|
|
152
|
+
sections.append("")
|
|
153
|
+
|
|
154
|
+
linked = agent.get("linked_personas") or []
|
|
155
|
+
if linked:
|
|
156
|
+
sections.append("## Linked Personas")
|
|
157
|
+
sections.extend(f"- [[{p}]]" for p in linked)
|
|
158
|
+
sections.append("")
|
|
159
|
+
|
|
160
|
+
sections.append("")
|
|
161
|
+
sections.append("*Generated by ArkaOS — source of truth lives in the YAML file.*")
|
|
162
|
+
return "\n".join(fm_lines + sections)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _yaml_str(value: object) -> str:
|
|
166
|
+
"""Quote a YAML scalar if it contains special characters."""
|
|
167
|
+
s = "" if value is None else str(value)
|
|
168
|
+
if any(c in s for c in ":#'\"\n"):
|
|
169
|
+
escaped = s.replace('"', '\\"')
|
|
170
|
+
return f'"{escaped}"'
|
|
171
|
+
return s
|
|
Binary file
|
|
@@ -367,7 +367,8 @@ def _current_category() -> str:
|
|
|
367
367
|
|
|
368
368
|
PR60 v2.77.0 — orchestration layers can set
|
|
369
369
|
``ARKA_CALL_CATEGORY=skill:<slug>`` /
|
|
370
|
-
``subagent:<dept>``
|
|
370
|
+
``subagent:<dept>`` or ``subagent:<dept>:<agent_id>`` /
|
|
371
|
+
``plugin:<id>`` / ``mcp:<server>`` before
|
|
371
372
|
invoking the provider so `/arka costs --by-category` (PR47) can
|
|
372
373
|
attribute spend. Returns "" when unset, which lands in the base
|
|
373
374
|
bucket (backward-compatible).
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
const route = useRoute()
|
|
10
10
|
const agentId = route.params.id as string
|
|
11
11
|
|
|
12
|
-
const { fetchApi } = useApi()
|
|
12
|
+
const { fetchApi, apiBase } = useApi()
|
|
13
|
+
const toast = useToast()
|
|
13
14
|
const { data: agent, status, error, refresh } = fetchApi<any>(`/api/agents/${agentId}`)
|
|
14
15
|
|
|
15
16
|
// Per-department activity (PR69 endpoint) for the stats row.
|
|
@@ -27,9 +28,10 @@ const deptActivity = computed<ActivityRow | null>(() =>
|
|
|
27
28
|
(activityData.value?.by_department?.[agent.value?.department ?? ''] ?? null),
|
|
28
29
|
)
|
|
29
30
|
|
|
30
|
-
// PR83d v3.6.0 — activity strip (30d,
|
|
31
|
+
// PR83d v3.6.0 + PR86b v3.16.0 — activity strip (30d, agent or dept scope)
|
|
31
32
|
interface ActivityStrip {
|
|
32
33
|
period: string
|
|
34
|
+
scope: 'agent' | 'department'
|
|
33
35
|
department: string
|
|
34
36
|
calls: number
|
|
35
37
|
cost_usd: number | null
|
|
@@ -63,6 +65,34 @@ function formatRelative(iso: string | null): string {
|
|
|
63
65
|
const favs = useFavorites()
|
|
64
66
|
await favs.load()
|
|
65
67
|
|
|
68
|
+
// PR86c v3.17.0 — export to Obsidian.
|
|
69
|
+
const exporting = ref(false)
|
|
70
|
+
async function exportToVault() {
|
|
71
|
+
if (!agent.value) return
|
|
72
|
+
exporting.value = true
|
|
73
|
+
try {
|
|
74
|
+
const res = await $fetch<{ exported?: boolean, path?: string, error?: string }>(
|
|
75
|
+
`${apiBase}/api/agents/${agentId}/export`,
|
|
76
|
+
{ method: 'POST' },
|
|
77
|
+
)
|
|
78
|
+
if (res.error) throw new Error(res.error)
|
|
79
|
+
toast.add({
|
|
80
|
+
title: 'Exported to Obsidian',
|
|
81
|
+
description: res.path ? res.path.split('/').slice(-3).join('/') : undefined,
|
|
82
|
+
color: 'success',
|
|
83
|
+
icon: 'i-lucide-file-text',
|
|
84
|
+
})
|
|
85
|
+
} catch (err) {
|
|
86
|
+
toast.add({
|
|
87
|
+
title: 'Export failed',
|
|
88
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
89
|
+
color: 'error',
|
|
90
|
+
})
|
|
91
|
+
} finally {
|
|
92
|
+
exporting.value = false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
// PR76 — edit drawer state
|
|
67
97
|
const editOpen = ref(false)
|
|
68
98
|
|
|
@@ -281,6 +311,14 @@ function formatTokens(n: number): string {
|
|
|
281
311
|
:aria-label="favs.isAgentFavorite(agent.id) ? 'Unfavorite' : 'Favorite'"
|
|
282
312
|
@click="favs.toggle('agents', agent.id)"
|
|
283
313
|
/>
|
|
314
|
+
<UButton
|
|
315
|
+
label="Export"
|
|
316
|
+
icon="i-lucide-file-text"
|
|
317
|
+
variant="soft"
|
|
318
|
+
size="sm"
|
|
319
|
+
:loading="exporting"
|
|
320
|
+
@click="exportToVault"
|
|
321
|
+
/>
|
|
284
322
|
<UButton
|
|
285
323
|
label="Edit"
|
|
286
324
|
icon="i-lucide-pencil"
|
|
@@ -346,7 +384,7 @@ function formatTokens(n: number): string {
|
|
|
346
384
|
<div class="flex items-center gap-2">
|
|
347
385
|
<UIcon name="i-lucide-activity" class="size-4 text-primary" />
|
|
348
386
|
<span class="font-semibold uppercase tracking-wide text-muted text-xs">
|
|
349
|
-
30d activity (dept)
|
|
387
|
+
30d activity ({{ activityStrip.scope === 'agent' ? 'agent' : 'dept' }})
|
|
350
388
|
</span>
|
|
351
389
|
</div>
|
|
352
390
|
<div class="flex items-center gap-2">
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -220,11 +220,17 @@ def agents_activity(period: str = "week"):
|
|
|
220
220
|
|
|
221
221
|
@app.get("/api/agents/{agent_id}/activity-strip")
|
|
222
222
|
def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
223
|
-
"""PR83d v3.6.0 — compact activity payload
|
|
223
|
+
"""PR83d v3.6.0 + PR86b v3.16.0 — compact activity payload.
|
|
224
|
+
|
|
225
|
+
When telemetry is tagged ``subagent:<dept>:<agent_id>``, the response
|
|
226
|
+
prefers per-agent stats and includes a ``scope: "agent"`` flag.
|
|
227
|
+
Otherwise it falls back to department-level stats with
|
|
228
|
+
``scope: "department"`` (the original PR83d behaviour).
|
|
224
229
|
|
|
225
230
|
Returns:
|
|
226
231
|
{
|
|
227
232
|
"period": "month",
|
|
233
|
+
"scope": "agent" | "department",
|
|
228
234
|
"department": "<dept>",
|
|
229
235
|
"calls": <int>,
|
|
230
236
|
"cost_usd": <float|null>,
|
|
@@ -233,9 +239,6 @@ def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
|
233
239
|
"dept_rank": <1-based int>|null,
|
|
234
240
|
"dept_count": <int>
|
|
235
241
|
}
|
|
236
|
-
|
|
237
|
-
All values reflect the agent's DEPARTMENT (per-agent attribution
|
|
238
|
-
isn't tracked yet — see PR47 telemetry).
|
|
239
242
|
"""
|
|
240
243
|
agents = _load_agents()
|
|
241
244
|
base = None
|
|
@@ -259,34 +262,51 @@ def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
|
259
262
|
period = "month"
|
|
260
263
|
|
|
261
264
|
summary = summarise(period=period)
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
dept_costs_map: dict[str, float] = {}
|
|
266
|
+
dept_row: dict | None = None
|
|
267
|
+
agent_row: dict | None = None
|
|
264
268
|
for category, row in (summary.by_category or {}).items():
|
|
265
269
|
if not isinstance(category, str) or not category.startswith("subagent:"):
|
|
266
270
|
continue
|
|
267
|
-
|
|
271
|
+
# subagent:<dept> OR subagent:<dept>:<agent_id>
|
|
272
|
+
parts = category.split(":", 2)
|
|
273
|
+
cat_dept = parts[1] if len(parts) >= 2 and parts[1] else "unknown"
|
|
274
|
+
cat_agent = parts[2] if len(parts) == 3 else None
|
|
268
275
|
cost = row.get("total_cost_usd")
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
cost_f = float(cost) if isinstance(cost, (int, float)) else 0.0
|
|
277
|
+
dept_costs_map[cat_dept] = dept_costs_map.get(cat_dept, 0.0) + cost_f
|
|
278
|
+
if cat_dept == dept and cat_agent is None:
|
|
279
|
+
dept_row = row
|
|
280
|
+
if cat_dept == dept and cat_agent == agent_id:
|
|
281
|
+
agent_row = row
|
|
282
|
+
|
|
283
|
+
dept_costs = sorted(dept_costs_map.items(), key=lambda t: t[1], reverse=True)
|
|
274
284
|
dept_rank: Optional[int] = None
|
|
275
285
|
for idx, (d, _) in enumerate(dept_costs, start=1):
|
|
276
286
|
if d == dept:
|
|
277
287
|
dept_rank = idx
|
|
278
288
|
break
|
|
279
289
|
|
|
290
|
+
use_agent_scope = agent_row is not None
|
|
291
|
+
scope = "agent" if use_agent_scope else "department"
|
|
292
|
+
target_row = agent_row if use_agent_scope else dept_row
|
|
293
|
+
|
|
280
294
|
entries, _ = _load_slice(None, _period_cutoff(period, now=None))
|
|
281
295
|
last_used: Optional[str] = None
|
|
296
|
+
expect_cats = (
|
|
297
|
+
{f"subagent:{dept}:{agent_id}"}
|
|
298
|
+
if use_agent_scope
|
|
299
|
+
else {f"subagent:{dept}"}
|
|
300
|
+
)
|
|
282
301
|
for entry in reversed(entries):
|
|
283
302
|
cat = entry.get("category") or ""
|
|
284
|
-
if isinstance(cat, str) and cat
|
|
303
|
+
if isinstance(cat, str) and cat in expect_cats:
|
|
285
304
|
last_used = entry.get("ts")
|
|
286
305
|
break
|
|
287
306
|
|
|
288
307
|
return {
|
|
289
308
|
"period": period,
|
|
309
|
+
"scope": scope,
|
|
290
310
|
"department": dept,
|
|
291
311
|
"calls": int(target_row.get("call_count", 0)) if target_row else 0,
|
|
292
312
|
"cost_usd": (
|
|
@@ -302,6 +322,16 @@ def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
|
|
325
|
+
@app.get("/api/agents/{agent_id}/activity")
|
|
326
|
+
def agent_activity_detail(agent_id: str, period: str = "month"):
|
|
327
|
+
"""PR86b v3.16.0 — alias for /activity-strip. Same payload shape.
|
|
328
|
+
|
|
329
|
+
Exposed so callers can use a more natural name when they want the
|
|
330
|
+
full activity row rather than the compact hero strip.
|
|
331
|
+
"""
|
|
332
|
+
return agent_activity_strip(agent_id, period)
|
|
333
|
+
|
|
334
|
+
|
|
305
335
|
@app.get("/api/agents/{agent_id}")
|
|
306
336
|
def agent_detail(agent_id: str):
|
|
307
337
|
"""Get full agent detail including YAML data."""
|
|
@@ -1331,6 +1361,28 @@ def persona_delete(persona_id: str):
|
|
|
1331
1361
|
return {"error": "Persona not found"}
|
|
1332
1362
|
|
|
1333
1363
|
|
|
1364
|
+
# --- Agent → Obsidian export (PR86c v3.17.0) ---
|
|
1365
|
+
|
|
1366
|
+
@app.post("/api/agents/{agent_id}/export")
|
|
1367
|
+
def agent_export_to_vault(agent_id: str):
|
|
1368
|
+
"""Write the agent profile as Markdown under <vault>/Agents/<id>.md."""
|
|
1369
|
+
detail = agent_detail(agent_id)
|
|
1370
|
+
if "error" in detail:
|
|
1371
|
+
return detail
|
|
1372
|
+
try:
|
|
1373
|
+
from core.agents.obsidian_export import (
|
|
1374
|
+
AgentExportError,
|
|
1375
|
+
export_agent_to_vault,
|
|
1376
|
+
)
|
|
1377
|
+
except Exception as exc: # noqa: BLE001
|
|
1378
|
+
return {"error": f"export module unavailable: {exc}"}
|
|
1379
|
+
try:
|
|
1380
|
+
res = export_agent_to_vault(detail)
|
|
1381
|
+
except AgentExportError as exc:
|
|
1382
|
+
return {"error": str(exc)}
|
|
1383
|
+
return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
|
|
1384
|
+
|
|
1385
|
+
|
|
1334
1386
|
# --- Favorites (PR86a v3.15.0) ---
|
|
1335
1387
|
|
|
1336
1388
|
@app.get("/api/favorites")
|