arkaos 3.16.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/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/dashboard/app/pages/agents/[id].vue +38 -1
- 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 +22 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.17.0
|
|
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
|
|
@@ -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.
|
|
@@ -64,6 +65,34 @@ function formatRelative(iso: string | null): string {
|
|
|
64
65
|
const favs = useFavorites()
|
|
65
66
|
await favs.load()
|
|
66
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
|
+
|
|
67
96
|
// PR76 — edit drawer state
|
|
68
97
|
const editOpen = ref(false)
|
|
69
98
|
|
|
@@ -282,6 +311,14 @@ function formatTokens(n: number): string {
|
|
|
282
311
|
:aria-label="favs.isAgentFavorite(agent.id) ? 'Unfavorite' : 'Favorite'"
|
|
283
312
|
@click="favs.toggle('agents', agent.id)"
|
|
284
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
|
+
/>
|
|
285
322
|
<UButton
|
|
286
323
|
label="Edit"
|
|
287
324
|
icon="i-lucide-pencil"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1361,6 +1361,28 @@ def persona_delete(persona_id: str):
|
|
|
1361
1361
|
return {"error": "Persona not found"}
|
|
1362
1362
|
|
|
1363
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
|
+
|
|
1364
1386
|
# --- Favorites (PR86a v3.15.0) ---
|
|
1365
1387
|
|
|
1366
1388
|
@app.get("/api/favorites")
|