arkaos 3.3.0 → 3.5.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__/string_suggester.cpython-313.pyc +0 -0
- package/core/agents/string_suggester.py +111 -0
- package/core/personas/__pycache__/description_drafter.cpython-313.pyc +0 -0
- package/dashboard/app/components/AgentEditDrawer.vue +70 -0
- package/dashboard/app/pages/agents/index.vue +121 -0
- package/dashboard/app/pages/agents/new.vue +74 -0
- package/dashboard/app/pages/personas/[id].vue +54 -0
- package/dashboard/app/pages/personas/index.vue +122 -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 +76 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.5.0
|
|
Binary file
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""AI-assisted single-string suggester (PR83c v3.5.0).
|
|
2
|
+
|
|
3
|
+
Sibling to `core/agents/field_suggester` but for fields that hold ONE
|
|
4
|
+
string value (not a list). Used by the ✨ Generate button next to
|
|
5
|
+
fields like `communication.tone`, `communication.preferred_format`,
|
|
6
|
+
and `communication.language`.
|
|
7
|
+
|
|
8
|
+
Where the list suggester APPENDS, this one REPLACES. The caller is
|
|
9
|
+
expected to swap the field's existing value with the result.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from core.runtime.llm_provider import LLMProvider, LLMUnavailable, get_llm_provider
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_VALID_FIELDS: tuple[str, ...] = ("tone", "preferred_format", "language")
|
|
21
|
+
|
|
22
|
+
_SYSTEM = (
|
|
23
|
+
"You suggest concise single-string values for behavioural agent and "
|
|
24
|
+
"persona profile fields. Return ONLY the value as plain text — no "
|
|
25
|
+
"JSON, no fences, no quotes, no explanations."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_FIELD_HINTS: dict[str, str] = {
|
|
29
|
+
"tone": (
|
|
30
|
+
"Return 2-4 adjectives separated by commas describing the "
|
|
31
|
+
"profile's voice (e.g. 'Direct, analytical, crisp'). "
|
|
32
|
+
"Max 60 characters."
|
|
33
|
+
),
|
|
34
|
+
"preferred_format": (
|
|
35
|
+
"Return a short noun phrase listing the formats this profile "
|
|
36
|
+
"prefers (e.g. 'Briefs, tables, ASCII diagrams'). Max 80 characters."
|
|
37
|
+
),
|
|
38
|
+
"language": (
|
|
39
|
+
"Return a comma-separated list of IETF language tags this profile "
|
|
40
|
+
"writes in (e.g. 'en' or 'en, pt-PT'). Max 20 characters."
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_MAX_LEN: dict[str, int] = {"tone": 60, "preferred_format": 80, "language": 20}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StringSuggestionError(RuntimeError):
|
|
48
|
+
"""LLM produced unusable output or could not be reached."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class StringSuggestionResult:
|
|
53
|
+
value: str
|
|
54
|
+
provider_name: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def suggest_string_field(
|
|
58
|
+
field: str,
|
|
59
|
+
context: dict,
|
|
60
|
+
*,
|
|
61
|
+
provider: LLMProvider | None = None,
|
|
62
|
+
) -> StringSuggestionResult:
|
|
63
|
+
"""Return an AI-suggested single-string value for `field`."""
|
|
64
|
+
if field not in _VALID_FIELDS:
|
|
65
|
+
raise StringSuggestionError(f"unknown field: {field!r}")
|
|
66
|
+
llm = provider or get_llm_provider()
|
|
67
|
+
prompt = _build_prompt(field, context)
|
|
68
|
+
try:
|
|
69
|
+
resp = llm.complete(prompt, max_tokens=200, system=_SYSTEM)
|
|
70
|
+
except LLMUnavailable as exc:
|
|
71
|
+
raise StringSuggestionError(str(exc)) from exc
|
|
72
|
+
value = _clean(resp.text, field)
|
|
73
|
+
if not value:
|
|
74
|
+
raise StringSuggestionError("LLM returned an empty value")
|
|
75
|
+
return StringSuggestionResult(value=value, provider_name=llm.name())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_prompt(field: str, context: dict) -> str:
|
|
79
|
+
name = (context.get("name") or "").strip() or "the entity"
|
|
80
|
+
role = (context.get("role") or context.get("title") or "").strip()
|
|
81
|
+
department = (context.get("department") or "").strip()
|
|
82
|
+
current = (context.get("current") or "").strip()
|
|
83
|
+
lines = [f"Suggest a {field.replace('_', ' ')} for {name}."]
|
|
84
|
+
if role:
|
|
85
|
+
lines.append(f"Role: {role}.")
|
|
86
|
+
if department:
|
|
87
|
+
lines.append(f"Department: {department}.")
|
|
88
|
+
if current:
|
|
89
|
+
lines.append(f"Current value (to be replaced, do not repeat verbatim): {current}.")
|
|
90
|
+
lines.append(_FIELD_HINTS[field])
|
|
91
|
+
return "\n".join(lines)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _clean(text: str, field: str) -> str:
|
|
95
|
+
"""Strip fences, quotes, leading bullets and trim to the field's max."""
|
|
96
|
+
cleaned = text.strip()
|
|
97
|
+
cleaned = re.sub(r"^```(?:\w+)?\s*|\s*```$", "", cleaned, flags=re.MULTILINE)
|
|
98
|
+
cleaned = cleaned.strip()
|
|
99
|
+
# Remove surrounding quotes the LLM may add.
|
|
100
|
+
if len(cleaned) >= 2 and cleaned[0] in '"\'`' and cleaned[-1] == cleaned[0]:
|
|
101
|
+
cleaned = cleaned[1:-1].strip()
|
|
102
|
+
# Drop a leading bullet/numbering.
|
|
103
|
+
cleaned = re.sub(r"^[-*•·]\s+", "", cleaned)
|
|
104
|
+
cleaned = re.sub(r"^\d+[.)]\s+", "", cleaned)
|
|
105
|
+
# Single-line: collapse internal whitespace, drop trailing punctuation.
|
|
106
|
+
cleaned = " ".join(cleaned.split())
|
|
107
|
+
cleaned = cleaned.rstrip(".")
|
|
108
|
+
max_len = _MAX_LEN[field]
|
|
109
|
+
if len(cleaned) > max_len:
|
|
110
|
+
cleaned = cleaned[:max_len].rstrip(", ").rstrip()
|
|
111
|
+
return cleaned
|
|
Binary file
|
|
@@ -115,6 +115,52 @@ function csvToList(value: string): string[] {
|
|
|
115
115
|
type SuggestField = 'mental_models_primary' | 'frameworks' | 'expertise_domains' | 'communication_avoid'
|
|
116
116
|
const suggestingField = ref<SuggestField | null>(null)
|
|
117
117
|
|
|
118
|
+
// PR83c v3.5.0 — single-string suggester.
|
|
119
|
+
type StringField = 'tone' | 'preferred_format'
|
|
120
|
+
const suggestingString = ref<StringField | null>(null)
|
|
121
|
+
|
|
122
|
+
async function suggestString(field: StringField) {
|
|
123
|
+
if (!draft.value || !props.agent) return
|
|
124
|
+
const current
|
|
125
|
+
= field === 'tone' ? draft.value.communication.tone : draft.value.communication.preferred_format
|
|
126
|
+
suggestingString.value = field
|
|
127
|
+
try {
|
|
128
|
+
const res = await $fetch<{ value: string, provider_name: string, error?: string }>(
|
|
129
|
+
`${apiBase}/api/agents/suggest-string`,
|
|
130
|
+
{
|
|
131
|
+
method: 'POST',
|
|
132
|
+
body: {
|
|
133
|
+
field,
|
|
134
|
+
context: {
|
|
135
|
+
name: props.agent.name,
|
|
136
|
+
role: props.agent.role,
|
|
137
|
+
department: props.agent.department,
|
|
138
|
+
current,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
if (res.error) throw new Error(res.error)
|
|
144
|
+
if (field === 'tone') draft.value.communication.tone = res.value
|
|
145
|
+
else draft.value.communication.preferred_format = res.value
|
|
146
|
+
markDirty()
|
|
147
|
+
toast.add({
|
|
148
|
+
title: 'Generated',
|
|
149
|
+
description: `via ${res.provider_name}`,
|
|
150
|
+
color: 'success',
|
|
151
|
+
icon: 'i-lucide-sparkles',
|
|
152
|
+
})
|
|
153
|
+
} catch (err) {
|
|
154
|
+
toast.add({
|
|
155
|
+
title: 'Generate failed',
|
|
156
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
157
|
+
color: 'error',
|
|
158
|
+
})
|
|
159
|
+
} finally {
|
|
160
|
+
suggestingString.value = null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
118
164
|
async function suggest(field: SuggestField) {
|
|
119
165
|
if (!draft.value || !props.agent) return
|
|
120
166
|
const backendField = field === 'mental_models_primary' ? 'mental_models' : field
|
|
@@ -424,6 +470,18 @@ const vocabOptions = [
|
|
|
424
470
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Communication</h3>
|
|
425
471
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
426
472
|
<UFormField label="Tone">
|
|
473
|
+
<template #hint>
|
|
474
|
+
<UButton
|
|
475
|
+
label="Generate"
|
|
476
|
+
icon="i-lucide-sparkles"
|
|
477
|
+
size="xs"
|
|
478
|
+
color="primary"
|
|
479
|
+
variant="soft"
|
|
480
|
+
:loading="suggestingString === 'tone'"
|
|
481
|
+
:disabled="suggestingString !== null"
|
|
482
|
+
@click="suggestString('tone')"
|
|
483
|
+
/>
|
|
484
|
+
</template>
|
|
427
485
|
<UInput v-model="draft.communication.tone" class="w-full" @update:model-value="markDirty" />
|
|
428
486
|
</UFormField>
|
|
429
487
|
<UFormField label="Vocabulary level">
|
|
@@ -435,6 +493,18 @@ const vocabOptions = [
|
|
|
435
493
|
/>
|
|
436
494
|
</UFormField>
|
|
437
495
|
<UFormField label="Preferred format">
|
|
496
|
+
<template #hint>
|
|
497
|
+
<UButton
|
|
498
|
+
label="Generate"
|
|
499
|
+
icon="i-lucide-sparkles"
|
|
500
|
+
size="xs"
|
|
501
|
+
color="primary"
|
|
502
|
+
variant="soft"
|
|
503
|
+
:loading="suggestingString === 'preferred_format'"
|
|
504
|
+
:disabled="suggestingString !== null"
|
|
505
|
+
@click="suggestString('preferred_format')"
|
|
506
|
+
/>
|
|
507
|
+
</template>
|
|
438
508
|
<UInput v-model="draft.communication.preferred_format" class="w-full" @update:model-value="markDirty" />
|
|
439
509
|
</UFormField>
|
|
440
510
|
<UFormField label="Language">
|
|
@@ -138,6 +138,7 @@ const tierColor = (tier: number) => {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
const columns: TableColumn<Agent>[] = [
|
|
141
|
+
{ id: 'select', header: '' },
|
|
141
142
|
{ accessorKey: 'name', header: 'Name' },
|
|
142
143
|
{ accessorKey: 'role', header: 'Role' },
|
|
143
144
|
{ accessorKey: 'department', header: 'Department' },
|
|
@@ -155,6 +156,78 @@ const columns: TableColumn<Agent>[] = [
|
|
|
155
156
|
function goToAgent(id: string) {
|
|
156
157
|
navigateTo(`/agents/${id}`)
|
|
157
158
|
}
|
|
159
|
+
|
|
160
|
+
// PR83b v3.4.0 — bulk selection + delete.
|
|
161
|
+
const confirmDialog = useConfirmDialog()
|
|
162
|
+
const selected = ref<Set<string>>(new Set())
|
|
163
|
+
const bulkDeleting = ref(false)
|
|
164
|
+
|
|
165
|
+
function toggleSelected(id: string) {
|
|
166
|
+
if (selected.value.has(id)) selected.value.delete(id)
|
|
167
|
+
else selected.value.add(id)
|
|
168
|
+
selected.value = new Set(selected.value)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toggleAllVisible() {
|
|
172
|
+
const visibleIds = paginatedAgents.value.map((a) => a.id)
|
|
173
|
+
const allSelected = visibleIds.every((id) => selected.value.has(id))
|
|
174
|
+
const next = new Set(selected.value)
|
|
175
|
+
for (const id of visibleIds) {
|
|
176
|
+
if (allSelected) next.delete(id)
|
|
177
|
+
else next.add(id)
|
|
178
|
+
}
|
|
179
|
+
selected.value = next
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const allVisibleSelected = computed(() => {
|
|
183
|
+
const visibleIds = paginatedAgents.value.map((a) => a.id)
|
|
184
|
+
return visibleIds.length > 0 && visibleIds.every((id) => selected.value.has(id))
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
function clearSelection() {
|
|
188
|
+
selected.value = new Set()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function bulkDelete() {
|
|
192
|
+
if (selected.value.size === 0) return
|
|
193
|
+
const ids = Array.from(selected.value)
|
|
194
|
+
const ok = await confirmDialog({
|
|
195
|
+
title: `Delete ${ids.length} agent${ids.length === 1 ? '' : 's'}?`,
|
|
196
|
+
description: 'YAML files will be removed from disk. This cannot be undone. Tier 0 agents are protected and will be skipped.',
|
|
197
|
+
confirmLabel: `Delete ${ids.length}`,
|
|
198
|
+
cancelLabel: 'Cancel',
|
|
199
|
+
variant: 'danger',
|
|
200
|
+
})
|
|
201
|
+
if (!ok) return
|
|
202
|
+
bulkDeleting.value = true
|
|
203
|
+
const results = await Promise.allSettled(
|
|
204
|
+
ids.map((id) =>
|
|
205
|
+
$fetch<{ deleted?: boolean, error?: string }>(`${apiBase}/api/agents/${id}`, {
|
|
206
|
+
method: 'DELETE',
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
const successes = results.filter(
|
|
211
|
+
(r) => r.status === 'fulfilled' && r.value.deleted,
|
|
212
|
+
).length
|
|
213
|
+
const failures = ids.length - successes
|
|
214
|
+
if (successes > 0) {
|
|
215
|
+
toast.add({
|
|
216
|
+
title: `Deleted ${successes} agent${successes === 1 ? '' : 's'}`,
|
|
217
|
+
description: failures > 0 ? `${failures} skipped (Tier 0 or missing)` : undefined,
|
|
218
|
+
color: failures > 0 ? 'warning' : 'success',
|
|
219
|
+
})
|
|
220
|
+
} else {
|
|
221
|
+
toast.add({
|
|
222
|
+
title: 'Nothing deleted',
|
|
223
|
+
description: 'All targets were protected or missing.',
|
|
224
|
+
color: 'error',
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
clearSelection()
|
|
228
|
+
bulkDeleting.value = false
|
|
229
|
+
await refreshAll()
|
|
230
|
+
}
|
|
158
231
|
</script>
|
|
159
232
|
|
|
160
233
|
<template>
|
|
@@ -233,6 +306,21 @@ function goToAgent(id: string) {
|
|
|
233
306
|
td: 'border-b border-default'
|
|
234
307
|
}"
|
|
235
308
|
>
|
|
309
|
+
<template #select-header>
|
|
310
|
+
<UCheckbox
|
|
311
|
+
:model-value="allVisibleSelected"
|
|
312
|
+
aria-label="Select all visible"
|
|
313
|
+
@update:model-value="toggleAllVisible"
|
|
314
|
+
/>
|
|
315
|
+
</template>
|
|
316
|
+
<template #select-cell="{ row }">
|
|
317
|
+
<UCheckbox
|
|
318
|
+
:model-value="selected.has(row.original.id)"
|
|
319
|
+
:aria-label="`Select ${row.original.name}`"
|
|
320
|
+
@update:model-value="() => toggleSelected(row.original.id)"
|
|
321
|
+
@click.stop
|
|
322
|
+
/>
|
|
323
|
+
</template>
|
|
236
324
|
<template #name-cell="{ row }">
|
|
237
325
|
<button class="text-left font-medium text-primary hover:underline" @click="goToAgent(row.original.id)">
|
|
238
326
|
{{ row.original.name }}
|
|
@@ -277,6 +365,39 @@ function goToAgent(id: string) {
|
|
|
277
365
|
</template>
|
|
278
366
|
</UTable>
|
|
279
367
|
|
|
368
|
+
<Transition
|
|
369
|
+
enter-active-class="transition ease-out duration-150"
|
|
370
|
+
enter-from-class="translate-y-4 opacity-0"
|
|
371
|
+
enter-to-class="translate-y-0 opacity-100"
|
|
372
|
+
leave-active-class="transition ease-in duration-100"
|
|
373
|
+
leave-from-class="translate-y-0 opacity-100"
|
|
374
|
+
leave-to-class="translate-y-4 opacity-0"
|
|
375
|
+
>
|
|
376
|
+
<div
|
|
377
|
+
v-if="selected.size > 0"
|
|
378
|
+
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 rounded-xl border border-default bg-elevated shadow-lg px-4 py-2"
|
|
379
|
+
>
|
|
380
|
+
<span class="text-sm font-semibold">
|
|
381
|
+
{{ selected.size }} selected
|
|
382
|
+
</span>
|
|
383
|
+
<UButton
|
|
384
|
+
label="Clear"
|
|
385
|
+
variant="ghost"
|
|
386
|
+
size="xs"
|
|
387
|
+
@click="clearSelection"
|
|
388
|
+
/>
|
|
389
|
+
<div class="h-5 w-px bg-default" />
|
|
390
|
+
<UButton
|
|
391
|
+
label="Delete"
|
|
392
|
+
icon="i-lucide-trash-2"
|
|
393
|
+
color="error"
|
|
394
|
+
size="sm"
|
|
395
|
+
:loading="bulkDeleting"
|
|
396
|
+
@click="bulkDelete"
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
</Transition>
|
|
400
|
+
|
|
280
401
|
<div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
|
|
281
402
|
<UPagination
|
|
282
403
|
:page="page"
|
|
@@ -88,6 +88,56 @@ const draft = ref<AgentDraft>({
|
|
|
88
88
|
|
|
89
89
|
const saving = ref(false)
|
|
90
90
|
|
|
91
|
+
// PR83c v3.5.0 — single-string suggester.
|
|
92
|
+
type StringField = 'tone' | 'preferred_format'
|
|
93
|
+
const suggestingString = ref<StringField | null>(null)
|
|
94
|
+
|
|
95
|
+
async function suggestString(field: StringField) {
|
|
96
|
+
if (!draft.value.name.trim() || !draft.value.role.trim()) {
|
|
97
|
+
toast.add({
|
|
98
|
+
title: 'Add a name and role first',
|
|
99
|
+
color: 'warning',
|
|
100
|
+
})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const current = field === 'tone' ? draft.value.comm_tone : draft.value.comm_format
|
|
104
|
+
suggestingString.value = field
|
|
105
|
+
try {
|
|
106
|
+
const res = await $fetch<{ value: string, provider_name: string, error?: string }>(
|
|
107
|
+
`${apiBase}/api/agents/suggest-string`,
|
|
108
|
+
{
|
|
109
|
+
method: 'POST',
|
|
110
|
+
body: {
|
|
111
|
+
field,
|
|
112
|
+
context: {
|
|
113
|
+
name: draft.value.name,
|
|
114
|
+
role: draft.value.role,
|
|
115
|
+
department: draft.value.department,
|
|
116
|
+
current,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
if (res.error) throw new Error(res.error)
|
|
122
|
+
if (field === 'tone') draft.value.comm_tone = res.value
|
|
123
|
+
else draft.value.comm_format = res.value
|
|
124
|
+
toast.add({
|
|
125
|
+
title: 'Generated',
|
|
126
|
+
description: `via ${res.provider_name}`,
|
|
127
|
+
color: 'success',
|
|
128
|
+
icon: 'i-lucide-sparkles',
|
|
129
|
+
})
|
|
130
|
+
} catch (err) {
|
|
131
|
+
toast.add({
|
|
132
|
+
title: 'Generate failed',
|
|
133
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
134
|
+
color: 'error',
|
|
135
|
+
})
|
|
136
|
+
} finally {
|
|
137
|
+
suggestingString.value = null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
91
141
|
// PR82b v3.1.0 — AI draft from description.
|
|
92
142
|
const description = ref('')
|
|
93
143
|
const drafting = ref(false)
|
|
@@ -544,12 +594,36 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
|
|
|
544
594
|
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Communication</h3>
|
|
545
595
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
546
596
|
<UFormField label="Tone">
|
|
597
|
+
<template #hint>
|
|
598
|
+
<UButton
|
|
599
|
+
label="Generate"
|
|
600
|
+
icon="i-lucide-sparkles"
|
|
601
|
+
size="xs"
|
|
602
|
+
color="primary"
|
|
603
|
+
variant="soft"
|
|
604
|
+
:loading="suggestingString === 'tone'"
|
|
605
|
+
:disabled="suggestingString !== null"
|
|
606
|
+
@click="suggestString('tone')"
|
|
607
|
+
/>
|
|
608
|
+
</template>
|
|
547
609
|
<UInput v-model="draft.comm_tone" class="w-full" placeholder="Analytical, calm" />
|
|
548
610
|
</UFormField>
|
|
549
611
|
<UFormField label="Vocabulary level">
|
|
550
612
|
<USelect v-model="draft.comm_vocab" :items="vocabOptions" class="w-full" />
|
|
551
613
|
</UFormField>
|
|
552
614
|
<UFormField label="Preferred format">
|
|
615
|
+
<template #hint>
|
|
616
|
+
<UButton
|
|
617
|
+
label="Generate"
|
|
618
|
+
icon="i-lucide-sparkles"
|
|
619
|
+
size="xs"
|
|
620
|
+
color="primary"
|
|
621
|
+
variant="soft"
|
|
622
|
+
:loading="suggestingString === 'preferred_format'"
|
|
623
|
+
:disabled="suggestingString !== null"
|
|
624
|
+
@click="suggestString('preferred_format')"
|
|
625
|
+
/>
|
|
626
|
+
</template>
|
|
553
627
|
<UInput v-model="draft.comm_format" class="w-full" placeholder="Briefs, tables, charts" />
|
|
554
628
|
</UFormField>
|
|
555
629
|
<UFormField label="Language">
|
|
@@ -226,6 +226,48 @@ function csvToList(value: string): string[] {
|
|
|
226
226
|
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid' | 'key_quotes'
|
|
227
227
|
const suggestingField = ref<SuggestField | null>(null)
|
|
228
228
|
|
|
229
|
+
// PR83c v3.5.0 — single-string suggester (tone for personas).
|
|
230
|
+
const suggestingString = ref<'tone' | null>(null)
|
|
231
|
+
|
|
232
|
+
async function suggestString(field: 'tone') {
|
|
233
|
+
if (!draft.value || !detail.value) return
|
|
234
|
+
const current = draft.value.communication.tone
|
|
235
|
+
suggestingString.value = field
|
|
236
|
+
try {
|
|
237
|
+
const res = await $fetch<{ value: string, provider_name: string, error?: string }>(
|
|
238
|
+
`${apiBase}/api/personas/suggest-string`,
|
|
239
|
+
{
|
|
240
|
+
method: 'POST',
|
|
241
|
+
body: {
|
|
242
|
+
field,
|
|
243
|
+
context: {
|
|
244
|
+
name: detail.value.name,
|
|
245
|
+
title: detail.value.title,
|
|
246
|
+
current,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
if (res.error) throw new Error(res.error)
|
|
252
|
+
draft.value.communication.tone = res.value
|
|
253
|
+
markDirty()
|
|
254
|
+
toast.add({
|
|
255
|
+
title: 'Generated',
|
|
256
|
+
description: `via ${res.provider_name}`,
|
|
257
|
+
color: 'success',
|
|
258
|
+
icon: 'i-lucide-sparkles',
|
|
259
|
+
})
|
|
260
|
+
} catch (err) {
|
|
261
|
+
toast.add({
|
|
262
|
+
title: 'Generate failed',
|
|
263
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
264
|
+
color: 'error',
|
|
265
|
+
})
|
|
266
|
+
} finally {
|
|
267
|
+
suggestingString.value = null
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
229
271
|
async function suggest(field: SuggestField) {
|
|
230
272
|
if (!draft.value || !detail.value) return
|
|
231
273
|
const current
|
|
@@ -754,6 +796,18 @@ const vocabOptions = [
|
|
|
754
796
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Communication</h3>
|
|
755
797
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
756
798
|
<UFormField label="Tone">
|
|
799
|
+
<template #hint>
|
|
800
|
+
<UButton
|
|
801
|
+
label="Generate"
|
|
802
|
+
icon="i-lucide-sparkles"
|
|
803
|
+
size="xs"
|
|
804
|
+
color="primary"
|
|
805
|
+
variant="soft"
|
|
806
|
+
:loading="suggestingString === 'tone'"
|
|
807
|
+
:disabled="suggestingString !== null"
|
|
808
|
+
@click="suggestString('tone')"
|
|
809
|
+
/>
|
|
810
|
+
</template>
|
|
757
811
|
<UInput v-model="draft.communication.tone" class="w-full" @update:model-value="markDirty" />
|
|
758
812
|
</UFormField>
|
|
759
813
|
<UFormField label="Vocabulary level">
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { TableColumn } from '@nuxt/ui'
|
|
9
9
|
import type { Persona } from '~/types'
|
|
10
10
|
|
|
11
|
-
const { fetchApi } = useApi()
|
|
11
|
+
const { fetchApi, apiBase } = useApi()
|
|
12
12
|
|
|
13
13
|
const { data, status, error, refresh } = await fetchApi<{
|
|
14
14
|
personas: Persona[]
|
|
@@ -118,6 +118,7 @@ function agentCount(personaId: string): number {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const columns: TableColumn<Persona>[] = [
|
|
121
|
+
{ id: 'select', header: '' },
|
|
121
122
|
{ accessorKey: 'name', header: 'Name' },
|
|
122
123
|
{ accessorKey: 'title', header: 'Title' },
|
|
123
124
|
{ accessorKey: 'source', header: 'Source' },
|
|
@@ -131,6 +132,78 @@ const columns: TableColumn<Persona>[] = [
|
|
|
131
132
|
function goToPersona(id: string) {
|
|
132
133
|
navigateTo(`/personas/${id}`)
|
|
133
134
|
}
|
|
135
|
+
|
|
136
|
+
// PR83b v3.4.0 — bulk selection + delete.
|
|
137
|
+
const toast = useToast()
|
|
138
|
+
const confirmDialog = useConfirmDialog()
|
|
139
|
+
const selected = ref<Set<string>>(new Set())
|
|
140
|
+
const bulkDeleting = ref(false)
|
|
141
|
+
|
|
142
|
+
function toggleSelected(id: string) {
|
|
143
|
+
if (selected.value.has(id)) selected.value.delete(id)
|
|
144
|
+
else selected.value.add(id)
|
|
145
|
+
selected.value = new Set(selected.value)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toggleAllVisible() {
|
|
149
|
+
const visibleIds = paginatedPersonas.value.map((p) => p.id)
|
|
150
|
+
const allSelected = visibleIds.every((id) => selected.value.has(id))
|
|
151
|
+
const next = new Set(selected.value)
|
|
152
|
+
for (const id of visibleIds) {
|
|
153
|
+
if (allSelected) next.delete(id)
|
|
154
|
+
else next.add(id)
|
|
155
|
+
}
|
|
156
|
+
selected.value = next
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const allVisibleSelected = computed(() => {
|
|
160
|
+
const visibleIds = paginatedPersonas.value.map((p) => p.id)
|
|
161
|
+
return visibleIds.length > 0 && visibleIds.every((id) => selected.value.has(id))
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
function clearSelection() {
|
|
165
|
+
selected.value = new Set()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function bulkDelete() {
|
|
169
|
+
if (selected.value.size === 0) return
|
|
170
|
+
const ids = Array.from(selected.value)
|
|
171
|
+
const ok = await confirmDialog({
|
|
172
|
+
title: `Delete ${ids.length} persona${ids.length === 1 ? '' : 's'}?`,
|
|
173
|
+
description: 'Personas will be removed from the JSON store and the Obsidian vault. This cannot be undone.',
|
|
174
|
+
confirmLabel: `Delete ${ids.length}`,
|
|
175
|
+
cancelLabel: 'Cancel',
|
|
176
|
+
variant: 'danger',
|
|
177
|
+
})
|
|
178
|
+
if (!ok) return
|
|
179
|
+
bulkDeleting.value = true
|
|
180
|
+
const results = await Promise.allSettled(
|
|
181
|
+
ids.map((id) =>
|
|
182
|
+
$fetch<{ deleted?: boolean, error?: string }>(`${apiBase}/api/personas/${id}`, {
|
|
183
|
+
method: 'DELETE',
|
|
184
|
+
}),
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
const successes = results.filter(
|
|
188
|
+
(r) => r.status === 'fulfilled' && r.value.deleted,
|
|
189
|
+
).length
|
|
190
|
+
const failures = ids.length - successes
|
|
191
|
+
if (successes > 0) {
|
|
192
|
+
toast.add({
|
|
193
|
+
title: `Deleted ${successes} persona${successes === 1 ? '' : 's'}`,
|
|
194
|
+
description: failures > 0 ? `${failures} failed` : undefined,
|
|
195
|
+
color: failures > 0 ? 'warning' : 'success',
|
|
196
|
+
})
|
|
197
|
+
} else {
|
|
198
|
+
toast.add({
|
|
199
|
+
title: 'Nothing deleted',
|
|
200
|
+
color: 'error',
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
clearSelection()
|
|
204
|
+
bulkDeleting.value = false
|
|
205
|
+
await refreshAll()
|
|
206
|
+
}
|
|
134
207
|
</script>
|
|
135
208
|
|
|
136
209
|
<template>
|
|
@@ -218,6 +291,21 @@ function goToPersona(id: string) {
|
|
|
218
291
|
}"
|
|
219
292
|
@select="(row: Persona) => goToPersona(row.id)"
|
|
220
293
|
>
|
|
294
|
+
<template #select-header>
|
|
295
|
+
<UCheckbox
|
|
296
|
+
:model-value="allVisibleSelected"
|
|
297
|
+
aria-label="Select all visible"
|
|
298
|
+
@update:model-value="toggleAllVisible"
|
|
299
|
+
/>
|
|
300
|
+
</template>
|
|
301
|
+
<template #select-cell="{ row }">
|
|
302
|
+
<UCheckbox
|
|
303
|
+
:model-value="selected.has(row.original.id)"
|
|
304
|
+
:aria-label="`Select ${row.original.name}`"
|
|
305
|
+
@update:model-value="() => toggleSelected(row.original.id)"
|
|
306
|
+
@click.stop
|
|
307
|
+
/>
|
|
308
|
+
</template>
|
|
221
309
|
<template #name-cell="{ row }">
|
|
222
310
|
<span class="font-medium">{{ row.original.name }}</span>
|
|
223
311
|
</template>
|
|
@@ -282,6 +370,39 @@ function goToPersona(id: string) {
|
|
|
282
370
|
</template>
|
|
283
371
|
</UTable>
|
|
284
372
|
|
|
373
|
+
<Transition
|
|
374
|
+
enter-active-class="transition ease-out duration-150"
|
|
375
|
+
enter-from-class="translate-y-4 opacity-0"
|
|
376
|
+
enter-to-class="translate-y-0 opacity-100"
|
|
377
|
+
leave-active-class="transition ease-in duration-100"
|
|
378
|
+
leave-from-class="translate-y-0 opacity-100"
|
|
379
|
+
leave-to-class="translate-y-4 opacity-0"
|
|
380
|
+
>
|
|
381
|
+
<div
|
|
382
|
+
v-if="selected.size > 0"
|
|
383
|
+
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 rounded-xl border border-default bg-elevated shadow-lg px-4 py-2"
|
|
384
|
+
>
|
|
385
|
+
<span class="text-sm font-semibold">
|
|
386
|
+
{{ selected.size }} selected
|
|
387
|
+
</span>
|
|
388
|
+
<UButton
|
|
389
|
+
label="Clear"
|
|
390
|
+
variant="ghost"
|
|
391
|
+
size="xs"
|
|
392
|
+
@click="clearSelection"
|
|
393
|
+
/>
|
|
394
|
+
<div class="h-5 w-px bg-default" />
|
|
395
|
+
<UButton
|
|
396
|
+
label="Delete"
|
|
397
|
+
icon="i-lucide-trash-2"
|
|
398
|
+
color="error"
|
|
399
|
+
size="sm"
|
|
400
|
+
:loading="bulkDeleting"
|
|
401
|
+
@click="bulkDelete"
|
|
402
|
+
/>
|
|
403
|
+
</div>
|
|
404
|
+
</Transition>
|
|
405
|
+
|
|
285
406
|
<div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
|
|
286
407
|
<UPagination
|
|
287
408
|
:page="page"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1094,6 +1094,55 @@ def persona_clone(persona_id: str, body: dict = {}):
|
|
|
1094
1094
|
return {"agent_id": agent_id, "department": department, "file": f"departments/{department}/agents/{agent_id}.yaml"}
|
|
1095
1095
|
|
|
1096
1096
|
|
|
1097
|
+
@app.delete("/api/agents/{agent_id}")
|
|
1098
|
+
def agent_delete(agent_id: str):
|
|
1099
|
+
"""PR83b v3.4.0 — delete an agent's YAML file.
|
|
1100
|
+
|
|
1101
|
+
Refuses to delete Tier 0 (C-Suite) agents — those are governance
|
|
1102
|
+
fixtures and need direct YAML removal to make the intent explicit.
|
|
1103
|
+
|
|
1104
|
+
Resolves the YAML location two ways:
|
|
1105
|
+
1. From the cached registry (covers seeded agents)
|
|
1106
|
+
2. By scanning departments/*/agents/<id>.yaml (covers
|
|
1107
|
+
freshly-created agents that aren't in the registry yet)
|
|
1108
|
+
"""
|
|
1109
|
+
yaml_file = _resolve_agent_yaml(agent_id)
|
|
1110
|
+
if yaml_file is None:
|
|
1111
|
+
return {"error": "Agent not found"}
|
|
1112
|
+
tier = _agent_tier_from_yaml(yaml_file)
|
|
1113
|
+
if tier == 0:
|
|
1114
|
+
return {"error": "Cannot delete Tier 0 (C-Suite) agents from the dashboard"}
|
|
1115
|
+
try:
|
|
1116
|
+
yaml_file.unlink()
|
|
1117
|
+
except OSError as exc:
|
|
1118
|
+
return {"error": f"delete failed: {exc}"}
|
|
1119
|
+
return {"deleted": True, "id": agent_id, "yaml_path": str(yaml_file)}
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _resolve_agent_yaml(agent_id: str) -> Optional[Path]:
|
|
1123
|
+
# 1. Check the cached registry first.
|
|
1124
|
+
for a in _load_agents():
|
|
1125
|
+
if a.get("id") == agent_id:
|
|
1126
|
+
candidate = ARKAOS_ROOT / a.get("file", "")
|
|
1127
|
+
if candidate.exists():
|
|
1128
|
+
return candidate
|
|
1129
|
+
# 2. Filesystem scan — covers freshly-created files.
|
|
1130
|
+
dept_root = ARKAOS_ROOT / "departments"
|
|
1131
|
+
if dept_root.exists():
|
|
1132
|
+
for path in dept_root.glob(f"*/agents/{agent_id}.yaml"):
|
|
1133
|
+
return path
|
|
1134
|
+
return None
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def _agent_tier_from_yaml(yaml_file: Path) -> int:
|
|
1138
|
+
try:
|
|
1139
|
+
import yaml as _yaml
|
|
1140
|
+
raw = _yaml.safe_load(yaml_file.read_text(encoding="utf-8")) or {}
|
|
1141
|
+
except Exception:
|
|
1142
|
+
return 99
|
|
1143
|
+
return int(raw.get("tier") or 99)
|
|
1144
|
+
|
|
1145
|
+
|
|
1097
1146
|
@app.delete("/api/personas/{persona_id}")
|
|
1098
1147
|
def persona_delete(persona_id: str):
|
|
1099
1148
|
mgr = _get_persona_manager()
|
|
@@ -1900,6 +1949,33 @@ def agents_draft(body: dict):
|
|
|
1900
1949
|
return {"draft": res.draft, "provider_name": res.provider_name}
|
|
1901
1950
|
|
|
1902
1951
|
|
|
1952
|
+
# --- AI single-string suggester (PR83c v3.5.0) ---
|
|
1953
|
+
|
|
1954
|
+
@app.post("/api/agents/suggest-string")
|
|
1955
|
+
def agents_suggest_string(body: dict):
|
|
1956
|
+
"""Suggest a single-string value (tone, preferred_format, language)."""
|
|
1957
|
+
return _do_string_suggest(body, source="agent")
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
@app.post("/api/personas/suggest-string")
|
|
1961
|
+
def personas_suggest_string(body: dict):
|
|
1962
|
+
return _do_string_suggest(body, source="persona")
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def _do_string_suggest(body: dict, *, source: str) -> dict:
|
|
1966
|
+
from core.agents.string_suggester import (
|
|
1967
|
+
StringSuggestionError,
|
|
1968
|
+
suggest_string_field,
|
|
1969
|
+
)
|
|
1970
|
+
field = (body.get("field") or "").strip()
|
|
1971
|
+
context = body.get("context") or {}
|
|
1972
|
+
try:
|
|
1973
|
+
res = suggest_string_field(field, context)
|
|
1974
|
+
except StringSuggestionError as exc:
|
|
1975
|
+
return {"error": str(exc)}
|
|
1976
|
+
return {"value": res.value, "provider_name": res.provider_name, "source": source}
|
|
1977
|
+
|
|
1978
|
+
|
|
1903
1979
|
# --- AI list-field suggester (PR81 v2.99.0) ---
|
|
1904
1980
|
|
|
1905
1981
|
@app.post("/api/agents/suggest")
|