arkaos 3.16.0 → 3.18.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/personas/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/personas/schema.py +4 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/dashboard/app/components/AgentEditDrawer.vue +15 -0
- package/dashboard/app/components/MarkdownEditor.vue +74 -0
- package/dashboard/app/pages/agents/[id].vue +63 -1
- package/dashboard/app/pages/personas/[id].vue +37 -0
- package/dashboard/package.json +1 -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 +26 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.18.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
|
package/core/personas/schema.py
CHANGED
|
@@ -61,6 +61,10 @@ class Persona(BaseModel):
|
|
|
61
61
|
# Communication
|
|
62
62
|
communication: PersonaCommunication = Field(default_factory=PersonaCommunication)
|
|
63
63
|
|
|
64
|
+
# PR86d v3.18.0 — long-form Markdown bio. Free-text field, never
|
|
65
|
+
# parsed; rendered in the dashboard with `marked` for preview.
|
|
66
|
+
bio_md: str = ""
|
|
67
|
+
|
|
64
68
|
# Metadata
|
|
65
69
|
created_at: str = ""
|
|
66
70
|
updated_at: str = ""
|
|
Binary file
|
|
@@ -58,6 +58,7 @@ interface AgentDraft {
|
|
|
58
58
|
avoid: string[]
|
|
59
59
|
}
|
|
60
60
|
linked_personas: string[]
|
|
61
|
+
bio_md: string
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const draft = ref<AgentDraft | null>(null)
|
|
@@ -88,6 +89,7 @@ watch(
|
|
|
88
89
|
avoid: agent.communication?.avoid ?? [],
|
|
89
90
|
},
|
|
90
91
|
linked_personas: agent.linked_personas ?? [],
|
|
92
|
+
bio_md: agent.bio_md ?? '',
|
|
91
93
|
}
|
|
92
94
|
dirty.value = false
|
|
93
95
|
} else if (!open) {
|
|
@@ -323,6 +325,7 @@ async function save() {
|
|
|
323
325
|
},
|
|
324
326
|
communication: draft.value.communication,
|
|
325
327
|
linked_personas: draft.value.linked_personas,
|
|
328
|
+
bio_md: draft.value.bio_md,
|
|
326
329
|
}
|
|
327
330
|
const res = await $fetch<{
|
|
328
331
|
id: string
|
|
@@ -653,6 +656,18 @@ const vocabOptions = [
|
|
|
653
656
|
</UFormField>
|
|
654
657
|
</section>
|
|
655
658
|
|
|
659
|
+
<section class="space-y-3">
|
|
660
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
661
|
+
Bio (Markdown)
|
|
662
|
+
</h3>
|
|
663
|
+
<MarkdownEditor
|
|
664
|
+
:model-value="draft.bio_md"
|
|
665
|
+
:rows="10"
|
|
666
|
+
placeholder="A free-text Markdown bio for this agent — context, voice samples, internal notes."
|
|
667
|
+
@update:model-value="(v: string) => { if (draft) { draft.bio_md = v; markDirty() } }"
|
|
668
|
+
/>
|
|
669
|
+
</section>
|
|
670
|
+
|
|
656
671
|
<section class="space-y-3">
|
|
657
672
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
658
673
|
Linked personas
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR86d v3.18.0 — Markdown editor with Edit / Preview tabs.
|
|
3
|
+
//
|
|
4
|
+
// Standalone component used in agent + persona edit forms. The model
|
|
5
|
+
// value is the raw Markdown string. Preview is rendered via marked
|
|
6
|
+
// (already a deps after this PR).
|
|
7
|
+
|
|
8
|
+
import { marked } from 'marked'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
modelValue: string
|
|
12
|
+
placeholder?: string
|
|
13
|
+
rows?: number
|
|
14
|
+
}>()
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
(e: 'update:modelValue', value: string): void
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const tab = ref<'edit' | 'preview'>('edit')
|
|
20
|
+
|
|
21
|
+
const value = computed({
|
|
22
|
+
get: () => props.modelValue ?? '',
|
|
23
|
+
set: (v: string) => emit('update:modelValue', v),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const html = computed(() => {
|
|
27
|
+
if (!value.value.trim()) {
|
|
28
|
+
return '<p class="text-muted italic">Nothing to preview yet.</p>'
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return marked.parse(value.value, { breaks: true, gfm: true })
|
|
32
|
+
} catch {
|
|
33
|
+
return '<p class="text-error">Markdown parse failed.</p>'
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div class="space-y-2">
|
|
40
|
+
<div class="flex items-center gap-1 text-xs">
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
class="px-2 py-1 rounded-md transition-colors"
|
|
44
|
+
:class="tab === 'edit' ? 'bg-elevated/60 text-default font-semibold' : 'text-muted hover:text-default'"
|
|
45
|
+
@click="tab = 'edit'"
|
|
46
|
+
>
|
|
47
|
+
Edit
|
|
48
|
+
</button>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class="px-2 py-1 rounded-md transition-colors"
|
|
52
|
+
:class="tab === 'preview' ? 'bg-elevated/60 text-default font-semibold' : 'text-muted hover:text-default'"
|
|
53
|
+
@click="tab = 'preview'"
|
|
54
|
+
>
|
|
55
|
+
Preview
|
|
56
|
+
</button>
|
|
57
|
+
<span class="ml-auto text-xs text-muted">
|
|
58
|
+
Markdown · GFM · {{ value.length }} char{{ value.length === 1 ? '' : 's' }}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
<UTextarea
|
|
62
|
+
v-if="tab === 'edit'"
|
|
63
|
+
v-model="value"
|
|
64
|
+
:rows="rows ?? 8"
|
|
65
|
+
:placeholder="placeholder ?? 'Write a Markdown bio…\n\n# Heading\n- bullets\n**bold**'"
|
|
66
|
+
class="w-full font-mono text-sm"
|
|
67
|
+
/>
|
|
68
|
+
<div
|
|
69
|
+
v-else
|
|
70
|
+
class="rounded-lg border border-default bg-elevated/10 p-4 min-h-[12rem] prose prose-sm dark:prose-invert max-w-none"
|
|
71
|
+
v-html="html"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
@@ -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,45 @@ function formatRelative(iso: string | null): string {
|
|
|
64
65
|
const favs = useFavorites()
|
|
65
66
|
await favs.load()
|
|
66
67
|
|
|
68
|
+
// PR86d v3.18.0 — render Markdown bio.
|
|
69
|
+
import { marked } from 'marked'
|
|
70
|
+
function markedHtml(src: string): string {
|
|
71
|
+
if (!src?.trim()) return ''
|
|
72
|
+
try {
|
|
73
|
+
return marked.parse(src, { breaks: true, gfm: true }) as string
|
|
74
|
+
} catch {
|
|
75
|
+
return ''
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// PR86c v3.17.0 — export to Obsidian.
|
|
80
|
+
const exporting = ref(false)
|
|
81
|
+
async function exportToVault() {
|
|
82
|
+
if (!agent.value) return
|
|
83
|
+
exporting.value = true
|
|
84
|
+
try {
|
|
85
|
+
const res = await $fetch<{ exported?: boolean, path?: string, error?: string }>(
|
|
86
|
+
`${apiBase}/api/agents/${agentId}/export`,
|
|
87
|
+
{ method: 'POST' },
|
|
88
|
+
)
|
|
89
|
+
if (res.error) throw new Error(res.error)
|
|
90
|
+
toast.add({
|
|
91
|
+
title: 'Exported to Obsidian',
|
|
92
|
+
description: res.path ? res.path.split('/').slice(-3).join('/') : undefined,
|
|
93
|
+
color: 'success',
|
|
94
|
+
icon: 'i-lucide-file-text',
|
|
95
|
+
})
|
|
96
|
+
} catch (err) {
|
|
97
|
+
toast.add({
|
|
98
|
+
title: 'Export failed',
|
|
99
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
100
|
+
color: 'error',
|
|
101
|
+
})
|
|
102
|
+
} finally {
|
|
103
|
+
exporting.value = false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
// PR76 — edit drawer state
|
|
68
108
|
const editOpen = ref(false)
|
|
69
109
|
|
|
@@ -282,6 +322,14 @@ function formatTokens(n: number): string {
|
|
|
282
322
|
:aria-label="favs.isAgentFavorite(agent.id) ? 'Unfavorite' : 'Favorite'"
|
|
283
323
|
@click="favs.toggle('agents', agent.id)"
|
|
284
324
|
/>
|
|
325
|
+
<UButton
|
|
326
|
+
label="Export"
|
|
327
|
+
icon="i-lucide-file-text"
|
|
328
|
+
variant="soft"
|
|
329
|
+
size="sm"
|
|
330
|
+
:loading="exporting"
|
|
331
|
+
@click="exportToVault"
|
|
332
|
+
/>
|
|
285
333
|
<UButton
|
|
286
334
|
label="Edit"
|
|
287
335
|
icon="i-lucide-pencil"
|
|
@@ -381,6 +429,20 @@ function formatTokens(n: number): string {
|
|
|
381
429
|
</div>
|
|
382
430
|
</section>
|
|
383
431
|
|
|
432
|
+
<!-- ===== BIO (PR86d) ===== -->
|
|
433
|
+
<section
|
|
434
|
+
v-if="(agent as any).bio_md"
|
|
435
|
+
class="rounded-xl border border-default bg-elevated/10 p-5"
|
|
436
|
+
>
|
|
437
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted mb-3">
|
|
438
|
+
Bio
|
|
439
|
+
</h3>
|
|
440
|
+
<div
|
|
441
|
+
class="prose prose-sm dark:prose-invert max-w-none"
|
|
442
|
+
v-html="markedHtml((agent as any).bio_md)"
|
|
443
|
+
/>
|
|
444
|
+
</section>
|
|
445
|
+
|
|
384
446
|
<AgentEditDrawer
|
|
385
447
|
v-model="editOpen"
|
|
386
448
|
:agent="agent"
|
|
@@ -230,6 +230,17 @@ const suggestingField = ref<SuggestField | null>(null)
|
|
|
230
230
|
const favs = useFavorites()
|
|
231
231
|
await favs.load()
|
|
232
232
|
|
|
233
|
+
// PR86d v3.18.0 — render Markdown bio.
|
|
234
|
+
import { marked } from 'marked'
|
|
235
|
+
function markedHtml(src: string): string {
|
|
236
|
+
if (!src?.trim()) return ''
|
|
237
|
+
try {
|
|
238
|
+
return marked.parse(src, { breaks: true, gfm: true }) as string
|
|
239
|
+
} catch {
|
|
240
|
+
return ''
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
233
244
|
// PR85a v3.11.0 — Clone to Agent dialog.
|
|
234
245
|
const cloneOpen = ref(false)
|
|
235
246
|
function onCloned(agentId: string) {
|
|
@@ -560,6 +571,20 @@ const vocabOptions = [
|
|
|
560
571
|
</div>
|
|
561
572
|
</section>
|
|
562
573
|
|
|
574
|
+
<!-- BIO (PR86d) -->
|
|
575
|
+
<section
|
|
576
|
+
v-if="(detail as any).bio_md"
|
|
577
|
+
class="rounded-xl border border-default bg-elevated/10 p-5"
|
|
578
|
+
>
|
|
579
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted mb-3">
|
|
580
|
+
Bio
|
|
581
|
+
</h3>
|
|
582
|
+
<div
|
|
583
|
+
class="prose prose-sm dark:prose-invert max-w-none"
|
|
584
|
+
v-html="markedHtml((detail as any).bio_md)"
|
|
585
|
+
/>
|
|
586
|
+
</section>
|
|
587
|
+
|
|
563
588
|
<!-- TABS -->
|
|
564
589
|
<UTabs :items="tabs" default-value="dna" class="w-full">
|
|
565
590
|
<template #content="{ item }">
|
|
@@ -937,6 +962,18 @@ const vocabOptions = [
|
|
|
937
962
|
</UFormField>
|
|
938
963
|
</section>
|
|
939
964
|
|
|
965
|
+
<section class="space-y-3">
|
|
966
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
967
|
+
Bio (Markdown)
|
|
968
|
+
</h3>
|
|
969
|
+
<MarkdownEditor
|
|
970
|
+
:model-value="(draft as any).bio_md ?? ''"
|
|
971
|
+
:rows="10"
|
|
972
|
+
placeholder="A free-text Markdown bio for this persona — voice samples, context, references."
|
|
973
|
+
@update:model-value="(v: string) => { if (draft) { (draft as any).bio_md = v; markDirty() } }"
|
|
974
|
+
/>
|
|
975
|
+
</section>
|
|
976
|
+
|
|
940
977
|
<section class="space-y-3">
|
|
941
978
|
<div class="flex items-center justify-between">
|
|
942
979
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Key quotes</h3>
|
package/dashboard/package.json
CHANGED
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -451,6 +451,9 @@ def agent_update(agent_id: str, body: dict):
|
|
|
451
451
|
raw["communication"] = comm
|
|
452
452
|
if "linked_personas" in body:
|
|
453
453
|
raw["linked_personas"] = _agent_str_list(body["linked_personas"])
|
|
454
|
+
# PR86d v3.18.0 — Markdown bio (long-form free text).
|
|
455
|
+
if "bio_md" in body and isinstance(body["bio_md"], str):
|
|
456
|
+
raw["bio_md"] = body["bio_md"]
|
|
454
457
|
|
|
455
458
|
try:
|
|
456
459
|
tmp = yaml_file.with_suffix(yaml_file.suffix + ".tmp")
|
|
@@ -1101,6 +1104,7 @@ def persona_update(persona_id: str, body: dict):
|
|
|
1101
1104
|
communication=PersonaCommunication(
|
|
1102
1105
|
**(merged.get("communication", {}) or {}),
|
|
1103
1106
|
),
|
|
1107
|
+
bio_md=merged.get("bio_md", "") or "",
|
|
1104
1108
|
created_at=merged.get("created_at", ""),
|
|
1105
1109
|
)
|
|
1106
1110
|
|
|
@@ -1361,6 +1365,28 @@ def persona_delete(persona_id: str):
|
|
|
1361
1365
|
return {"error": "Persona not found"}
|
|
1362
1366
|
|
|
1363
1367
|
|
|
1368
|
+
# --- Agent → Obsidian export (PR86c v3.17.0) ---
|
|
1369
|
+
|
|
1370
|
+
@app.post("/api/agents/{agent_id}/export")
|
|
1371
|
+
def agent_export_to_vault(agent_id: str):
|
|
1372
|
+
"""Write the agent profile as Markdown under <vault>/Agents/<id>.md."""
|
|
1373
|
+
detail = agent_detail(agent_id)
|
|
1374
|
+
if "error" in detail:
|
|
1375
|
+
return detail
|
|
1376
|
+
try:
|
|
1377
|
+
from core.agents.obsidian_export import (
|
|
1378
|
+
AgentExportError,
|
|
1379
|
+
export_agent_to_vault,
|
|
1380
|
+
)
|
|
1381
|
+
except Exception as exc: # noqa: BLE001
|
|
1382
|
+
return {"error": f"export module unavailable: {exc}"}
|
|
1383
|
+
try:
|
|
1384
|
+
res = export_agent_to_vault(detail)
|
|
1385
|
+
except AgentExportError as exc:
|
|
1386
|
+
return {"error": str(exc)}
|
|
1387
|
+
return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
|
|
1388
|
+
|
|
1389
|
+
|
|
1364
1390
|
# --- Favorites (PR86a v3.15.0) ---
|
|
1365
1391
|
|
|
1366
1392
|
@app.get("/api/favorites")
|