arkaos 2.98.0 → 3.0.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__/field_suggester.cpython-313.pyc +0 -0
- package/core/agents/field_suggester.py +133 -0
- package/dashboard/app/components/AgentEditDrawer.vue +107 -1
- package/dashboard/app/pages/agents/index.vue +8 -0
- package/dashboard/app/pages/agents/new.vue +478 -0
- package/dashboard/app/pages/personas/[id].vue +92 -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 +187 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.0.0
|
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""AI-assisted list-field suggester for agents and personas (PR81).
|
|
2
|
+
|
|
3
|
+
Generates short lists of `mental_models`, `frameworks`, or
|
|
4
|
+
`expertise_domains` for an entity given its existing context.
|
|
5
|
+
|
|
6
|
+
The LLM is told NOT to duplicate items already present in
|
|
7
|
+
`context["current"]`, and to return a strict JSON array of strings.
|
|
8
|
+
|
|
9
|
+
Used by:
|
|
10
|
+
- POST /api/agents/suggest — ✨ Suggest button in AgentEditDrawer
|
|
11
|
+
- POST /api/personas/suggest — ✨ Suggest button in persona edit slideover
|
|
12
|
+
|
|
13
|
+
The module is provider-agnostic: callers can inject a fake `LLMProvider`
|
|
14
|
+
in tests, and production callers fall back to `get_llm_provider()`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
from core.runtime.llm_provider import LLMProvider, LLMUnavailable, get_llm_provider
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_VALID_FIELDS: tuple[str, ...] = (
|
|
27
|
+
"mental_models",
|
|
28
|
+
"frameworks",
|
|
29
|
+
"expertise_domains",
|
|
30
|
+
)
|
|
31
|
+
_MAX_COUNT = 12
|
|
32
|
+
_DEFAULT_COUNT = 5
|
|
33
|
+
|
|
34
|
+
_SYSTEM = (
|
|
35
|
+
"You suggest short, concrete items for behavioural agent and persona "
|
|
36
|
+
"profiles. Return ONLY a JSON array of strings. No prose, no fences, no keys."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_FIELD_LABELS: dict[str, str] = {
|
|
40
|
+
"mental_models": "mental models",
|
|
41
|
+
"frameworks": "frameworks",
|
|
42
|
+
"expertise_domains": "expertise domains",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SuggestionError(RuntimeError):
|
|
47
|
+
"""LLM produced unusable output or could not be reached."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class SuggestionResult:
|
|
52
|
+
suggestions: list[str]
|
|
53
|
+
provider_name: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def suggest_field(
|
|
57
|
+
field: str,
|
|
58
|
+
context: dict,
|
|
59
|
+
*,
|
|
60
|
+
count: int = _DEFAULT_COUNT,
|
|
61
|
+
provider: LLMProvider | None = None,
|
|
62
|
+
) -> SuggestionResult:
|
|
63
|
+
"""Return up to `count` AI-suggested items for the named field."""
|
|
64
|
+
if field not in _VALID_FIELDS:
|
|
65
|
+
raise SuggestionError(f"unknown field: {field!r}")
|
|
66
|
+
count = max(1, min(_MAX_COUNT, int(count)))
|
|
67
|
+
llm = provider or get_llm_provider()
|
|
68
|
+
prompt = _build_prompt(field, context, count)
|
|
69
|
+
try:
|
|
70
|
+
resp = llm.complete(prompt, max_tokens=600, system=_SYSTEM)
|
|
71
|
+
except LLMUnavailable as exc:
|
|
72
|
+
raise SuggestionError(str(exc)) from exc
|
|
73
|
+
items = _parse(resp.text)
|
|
74
|
+
deduped = _dedupe_against_current(items, context.get("current") or [])
|
|
75
|
+
return SuggestionResult(
|
|
76
|
+
suggestions=deduped[:count],
|
|
77
|
+
provider_name=llm.name(),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _build_prompt(field: str, context: dict, count: int) -> str:
|
|
82
|
+
name = (context.get("name") or "").strip() or "the entity"
|
|
83
|
+
role = (context.get("role") or context.get("title") or "").strip()
|
|
84
|
+
department = (context.get("department") or "").strip()
|
|
85
|
+
current = list(context.get("current") or [])[:20]
|
|
86
|
+
lines = [f"Suggest {count} new {_FIELD_LABELS[field]} for {name}."]
|
|
87
|
+
if role:
|
|
88
|
+
lines.append(f"Role: {role}.")
|
|
89
|
+
if department:
|
|
90
|
+
lines.append(f"Department: {department}.")
|
|
91
|
+
if current:
|
|
92
|
+
lines.append(
|
|
93
|
+
"Do NOT repeat any of these items already in the profile: "
|
|
94
|
+
+ ", ".join(current)
|
|
95
|
+
+ "."
|
|
96
|
+
)
|
|
97
|
+
lines.append(
|
|
98
|
+
"Return a JSON array of short strings (2-5 words each). "
|
|
99
|
+
"No explanations, no numbering, no surrounding object."
|
|
100
|
+
)
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse(text: str) -> list[str]:
|
|
105
|
+
cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip(), flags=re.MULTILINE)
|
|
106
|
+
cleaned = cleaned.strip()
|
|
107
|
+
try:
|
|
108
|
+
data = json.loads(cleaned)
|
|
109
|
+
except (json.JSONDecodeError, ValueError):
|
|
110
|
+
return _fallback_lines(cleaned)
|
|
111
|
+
if not isinstance(data, list):
|
|
112
|
+
return _fallback_lines(cleaned)
|
|
113
|
+
return [str(x).strip() for x in data if str(x).strip()]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _fallback_lines(text: str) -> list[str]:
|
|
117
|
+
items: list[str] = []
|
|
118
|
+
for raw in re.split(r"[\n,]", text):
|
|
119
|
+
line = raw.strip(" \t-*•·0123456789.).\"'`")
|
|
120
|
+
if line and 2 <= len(line) <= 80:
|
|
121
|
+
items.append(line)
|
|
122
|
+
return items
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _dedupe_against_current(items: list[str], current: list) -> list[str]:
|
|
126
|
+
seen = {str(c).strip().lower() for c in current}
|
|
127
|
+
out: list[str] = []
|
|
128
|
+
for item in items:
|
|
129
|
+
key = item.strip().lower()
|
|
130
|
+
if key and key not in seen:
|
|
131
|
+
out.append(item)
|
|
132
|
+
seen.add(key)
|
|
133
|
+
return out
|
|
@@ -110,6 +110,76 @@ function csvToList(value: string): string[] {
|
|
|
110
110
|
return value.split(',').map((s) => s.trim()).filter(Boolean)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// PR81 v2.99.0 — AI list-field suggester.
|
|
114
|
+
type SuggestField = 'mental_models_primary' | 'frameworks' | 'expertise_domains'
|
|
115
|
+
const suggestingField = ref<SuggestField | null>(null)
|
|
116
|
+
|
|
117
|
+
async function suggest(field: SuggestField) {
|
|
118
|
+
if (!draft.value || !props.agent) return
|
|
119
|
+
const backendField = field === 'mental_models_primary' ? 'mental_models' : field
|
|
120
|
+
const current
|
|
121
|
+
= field === 'mental_models_primary'
|
|
122
|
+
? draft.value.mental_models.primary
|
|
123
|
+
: field === 'frameworks'
|
|
124
|
+
? draft.value.frameworks
|
|
125
|
+
: draft.value.expertise_domains
|
|
126
|
+
suggestingField.value = field
|
|
127
|
+
try {
|
|
128
|
+
const res = await $fetch<{
|
|
129
|
+
suggestions: string[]
|
|
130
|
+
provider_name: string
|
|
131
|
+
error?: string
|
|
132
|
+
}>(`${apiBase}/api/agents/suggest`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
body: {
|
|
135
|
+
field: backendField,
|
|
136
|
+
count: 5,
|
|
137
|
+
context: {
|
|
138
|
+
name: props.agent.name,
|
|
139
|
+
role: props.agent.role,
|
|
140
|
+
department: props.agent.department,
|
|
141
|
+
current,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
if (res.error) throw new Error(res.error)
|
|
146
|
+
const additions = (res.suggestions ?? []).filter(
|
|
147
|
+
(s) => !current.some((c) => c.toLowerCase() === s.toLowerCase()),
|
|
148
|
+
)
|
|
149
|
+
if (additions.length === 0) {
|
|
150
|
+
toast.add({
|
|
151
|
+
title: 'No new suggestions',
|
|
152
|
+
description: 'The model returned only items you already have.',
|
|
153
|
+
color: 'info',
|
|
154
|
+
})
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const merged = [...current, ...additions]
|
|
158
|
+
if (field === 'mental_models_primary') {
|
|
159
|
+
draft.value.mental_models.primary = merged
|
|
160
|
+
} else if (field === 'frameworks') {
|
|
161
|
+
draft.value.frameworks = merged
|
|
162
|
+
} else {
|
|
163
|
+
draft.value.expertise_domains = merged
|
|
164
|
+
}
|
|
165
|
+
markDirty()
|
|
166
|
+
toast.add({
|
|
167
|
+
title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
|
|
168
|
+
description: `via ${res.provider_name}`,
|
|
169
|
+
color: 'success',
|
|
170
|
+
icon: 'i-lucide-sparkles',
|
|
171
|
+
})
|
|
172
|
+
} catch (err) {
|
|
173
|
+
toast.add({
|
|
174
|
+
title: 'Suggestion failed',
|
|
175
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
176
|
+
color: 'error',
|
|
177
|
+
})
|
|
178
|
+
} finally {
|
|
179
|
+
suggestingField.value = null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
113
183
|
async function save() {
|
|
114
184
|
if (!draft.value || !props.agent?.id) return
|
|
115
185
|
saving.value = true
|
|
@@ -254,7 +324,19 @@ const vocabOptions = [
|
|
|
254
324
|
</section>
|
|
255
325
|
|
|
256
326
|
<section class="space-y-3">
|
|
257
|
-
<
|
|
327
|
+
<div class="flex items-center justify-between">
|
|
328
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Mental models</h3>
|
|
329
|
+
<UButton
|
|
330
|
+
label="Suggest with AI"
|
|
331
|
+
icon="i-lucide-sparkles"
|
|
332
|
+
size="xs"
|
|
333
|
+
color="primary"
|
|
334
|
+
variant="soft"
|
|
335
|
+
:loading="suggestingField === 'mental_models_primary'"
|
|
336
|
+
:disabled="suggestingField !== null"
|
|
337
|
+
@click="suggest('mental_models_primary')"
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
258
340
|
<UFormField label="Primary" help="comma-separated">
|
|
259
341
|
<UInput
|
|
260
342
|
:model-value="listToCsv(draft.mental_models.primary)"
|
|
@@ -274,6 +356,18 @@ const vocabOptions = [
|
|
|
274
356
|
<section class="space-y-3">
|
|
275
357
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Expertise</h3>
|
|
276
358
|
<UFormField label="Domains" help="comma-separated">
|
|
359
|
+
<template #hint>
|
|
360
|
+
<UButton
|
|
361
|
+
label="Suggest with AI"
|
|
362
|
+
icon="i-lucide-sparkles"
|
|
363
|
+
size="xs"
|
|
364
|
+
color="primary"
|
|
365
|
+
variant="soft"
|
|
366
|
+
:loading="suggestingField === 'expertise_domains'"
|
|
367
|
+
:disabled="suggestingField !== null"
|
|
368
|
+
@click="suggest('expertise_domains')"
|
|
369
|
+
/>
|
|
370
|
+
</template>
|
|
277
371
|
<UInput
|
|
278
372
|
:model-value="listToCsv(draft.expertise_domains)"
|
|
279
373
|
@update:model-value="(v: string) => { if (draft) { draft.expertise_domains = csvToList(v); markDirty() } }"
|
|
@@ -281,6 +375,18 @@ const vocabOptions = [
|
|
|
281
375
|
/>
|
|
282
376
|
</UFormField>
|
|
283
377
|
<UFormField label="Frameworks" help="comma-separated">
|
|
378
|
+
<template #hint>
|
|
379
|
+
<UButton
|
|
380
|
+
label="Suggest with AI"
|
|
381
|
+
icon="i-lucide-sparkles"
|
|
382
|
+
size="xs"
|
|
383
|
+
color="primary"
|
|
384
|
+
variant="soft"
|
|
385
|
+
:loading="suggestingField === 'frameworks'"
|
|
386
|
+
:disabled="suggestingField !== null"
|
|
387
|
+
@click="suggest('frameworks')"
|
|
388
|
+
/>
|
|
389
|
+
</template>
|
|
284
390
|
<UInput
|
|
285
391
|
:model-value="listToCsv(draft.frameworks)"
|
|
286
392
|
@update:model-value="(v: string) => { if (draft) { draft.frameworks = csvToList(v); markDirty() } }"
|
|
@@ -167,6 +167,14 @@ function goToAgent(id: string) {
|
|
|
167
167
|
<template #trailing>
|
|
168
168
|
<UBadge v-if="data?.total" :label="data.total" variant="subtle" />
|
|
169
169
|
</template>
|
|
170
|
+
<template #right>
|
|
171
|
+
<UButton
|
|
172
|
+
label="New Agent"
|
|
173
|
+
icon="i-lucide-plus"
|
|
174
|
+
size="sm"
|
|
175
|
+
to="/agents/new"
|
|
176
|
+
/>
|
|
177
|
+
</template>
|
|
170
178
|
</UDashboardNavbar>
|
|
171
179
|
</template>
|
|
172
180
|
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR82a v3.0.0 — /agents/new manual create page.
|
|
3
|
+
//
|
|
4
|
+
// Single-page form (sections, no multi-step) that mirrors the safe-to-edit
|
|
5
|
+
// fields from AgentEditDrawer but in "create" mode:
|
|
6
|
+
// - Identity (name, role, department, tier)
|
|
7
|
+
// - Behavioural DNA (DISC + Enneagram + MBTI + Big Five, with sensible
|
|
8
|
+
// defaults — operator can edit)
|
|
9
|
+
// - Knowledge (mental models, expertise domains, frameworks)
|
|
10
|
+
// - Communication (tone, vocab, format, language, avoid)
|
|
11
|
+
//
|
|
12
|
+
// AI-assist (PR81) is wired on the three list fields so a draft agent
|
|
13
|
+
// can be filled with one click. On Save → POST /api/agents → navigate
|
|
14
|
+
// to /agents/{slug}.
|
|
15
|
+
|
|
16
|
+
import type { Persona } from '~/types'
|
|
17
|
+
|
|
18
|
+
const { fetchApi, apiBase } = useApi()
|
|
19
|
+
const toast = useToast()
|
|
20
|
+
|
|
21
|
+
const { data: personasData } = fetchApi<{ personas: Persona[] }>('/api/personas')
|
|
22
|
+
const personaOptions = computed(() =>
|
|
23
|
+
(personasData.value?.personas ?? []).map((p) => ({
|
|
24
|
+
label: p.name + (p.title ? ` — ${p.title}` : ''),
|
|
25
|
+
value: p.id,
|
|
26
|
+
})),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
interface AgentDraft {
|
|
30
|
+
name: string
|
|
31
|
+
role: string
|
|
32
|
+
department: string
|
|
33
|
+
tier: number
|
|
34
|
+
disc_primary: string
|
|
35
|
+
disc_secondary: string
|
|
36
|
+
enneagram_type: number
|
|
37
|
+
enneagram_wing: number
|
|
38
|
+
mbti: string
|
|
39
|
+
big_five: {
|
|
40
|
+
openness: number
|
|
41
|
+
conscientiousness: number
|
|
42
|
+
extraversion: number
|
|
43
|
+
agreeableness: number
|
|
44
|
+
neuroticism: number
|
|
45
|
+
}
|
|
46
|
+
mental_models_primary: string[]
|
|
47
|
+
expertise_domains: string[]
|
|
48
|
+
expertise_depth: string
|
|
49
|
+
expertise_years: number
|
|
50
|
+
frameworks: string[]
|
|
51
|
+
comm_tone: string
|
|
52
|
+
comm_vocab: string
|
|
53
|
+
comm_format: string
|
|
54
|
+
comm_language: string
|
|
55
|
+
comm_avoid: string[]
|
|
56
|
+
linked_personas: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const draft = ref<AgentDraft>({
|
|
60
|
+
name: '',
|
|
61
|
+
role: '',
|
|
62
|
+
department: 'dev',
|
|
63
|
+
tier: 2,
|
|
64
|
+
disc_primary: 'I',
|
|
65
|
+
disc_secondary: 'S',
|
|
66
|
+
enneagram_type: 5,
|
|
67
|
+
enneagram_wing: 4,
|
|
68
|
+
mbti: 'INTJ',
|
|
69
|
+
big_five: {
|
|
70
|
+
openness: 70,
|
|
71
|
+
conscientiousness: 70,
|
|
72
|
+
extraversion: 50,
|
|
73
|
+
agreeableness: 60,
|
|
74
|
+
neuroticism: 30,
|
|
75
|
+
},
|
|
76
|
+
mental_models_primary: [],
|
|
77
|
+
expertise_domains: [],
|
|
78
|
+
expertise_depth: 'advanced',
|
|
79
|
+
expertise_years: 5,
|
|
80
|
+
frameworks: [],
|
|
81
|
+
comm_tone: '',
|
|
82
|
+
comm_vocab: 'specialist',
|
|
83
|
+
comm_format: '',
|
|
84
|
+
comm_language: 'en',
|
|
85
|
+
comm_avoid: [],
|
|
86
|
+
linked_personas: [],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const saving = ref(false)
|
|
90
|
+
|
|
91
|
+
const departmentOptions = [
|
|
92
|
+
'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
|
|
93
|
+
'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
|
|
94
|
+
].map((d) => ({ label: d, value: d }))
|
|
95
|
+
|
|
96
|
+
const tierOptions = [
|
|
97
|
+
{ label: 'Tier 1 — Squad Lead', value: 1 },
|
|
98
|
+
{ label: 'Tier 2 — Specialist', value: 2 },
|
|
99
|
+
{ label: 'Tier 3 — Support', value: 3 },
|
|
100
|
+
]
|
|
101
|
+
const discOptions = ['D', 'I', 'S', 'C'].map((v) => ({ label: v, value: v }))
|
|
102
|
+
const depthOptions = [
|
|
103
|
+
{ label: 'Intermediate', value: 'intermediate' },
|
|
104
|
+
{ label: 'Advanced', value: 'advanced' },
|
|
105
|
+
{ label: 'Expert', value: 'expert' },
|
|
106
|
+
{ label: 'Master', value: 'master' },
|
|
107
|
+
]
|
|
108
|
+
const vocabOptions = [
|
|
109
|
+
{ label: 'Lay (no jargon)', value: 'lay' },
|
|
110
|
+
{ label: 'Specialist (industry terms)', value: 'specialist' },
|
|
111
|
+
{ label: 'Expert (research-level)', value: 'expert' },
|
|
112
|
+
]
|
|
113
|
+
const mbtiOptions = [
|
|
114
|
+
'INTJ', 'INTP', 'ENTJ', 'ENTP',
|
|
115
|
+
'INFJ', 'INFP', 'ENFJ', 'ENFP',
|
|
116
|
+
'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ',
|
|
117
|
+
'ISTP', 'ISFP', 'ESTP', 'ESFP',
|
|
118
|
+
].map((t) => ({ label: t, value: t }))
|
|
119
|
+
|
|
120
|
+
function listToCsv(list: string[]): string {
|
|
121
|
+
return list.join(', ')
|
|
122
|
+
}
|
|
123
|
+
function csvToList(value: string): string[] {
|
|
124
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// PR81 suggest wiring — three list fields.
|
|
128
|
+
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains'
|
|
129
|
+
const suggestingField = ref<SuggestField | null>(null)
|
|
130
|
+
|
|
131
|
+
async function suggest(field: SuggestField) {
|
|
132
|
+
const current
|
|
133
|
+
= field === 'mental_models'
|
|
134
|
+
? draft.value.mental_models_primary
|
|
135
|
+
: field === 'frameworks'
|
|
136
|
+
? draft.value.frameworks
|
|
137
|
+
: draft.value.expertise_domains
|
|
138
|
+
if (!draft.value.name.trim() || !draft.value.role.trim()) {
|
|
139
|
+
toast.add({
|
|
140
|
+
title: 'Add a name and role first',
|
|
141
|
+
description: 'AI needs the basics to make useful suggestions.',
|
|
142
|
+
color: 'warning',
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
suggestingField.value = field
|
|
147
|
+
try {
|
|
148
|
+
const res = await $fetch<{
|
|
149
|
+
suggestions: string[]
|
|
150
|
+
provider_name: string
|
|
151
|
+
error?: string
|
|
152
|
+
}>(`${apiBase}/api/agents/suggest`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: {
|
|
155
|
+
field,
|
|
156
|
+
count: 5,
|
|
157
|
+
context: {
|
|
158
|
+
name: draft.value.name,
|
|
159
|
+
role: draft.value.role,
|
|
160
|
+
department: draft.value.department,
|
|
161
|
+
current,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
if (res.error) throw new Error(res.error)
|
|
166
|
+
const additions = (res.suggestions ?? []).filter(
|
|
167
|
+
(s) => !current.some((c) => c.toLowerCase() === s.toLowerCase()),
|
|
168
|
+
)
|
|
169
|
+
if (additions.length === 0) {
|
|
170
|
+
toast.add({ title: 'No new suggestions', color: 'info' })
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
const merged = [...current, ...additions]
|
|
174
|
+
if (field === 'mental_models') draft.value.mental_models_primary = merged
|
|
175
|
+
else if (field === 'frameworks') draft.value.frameworks = merged
|
|
176
|
+
else draft.value.expertise_domains = merged
|
|
177
|
+
toast.add({
|
|
178
|
+
title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
|
|
179
|
+
description: `via ${res.provider_name}`,
|
|
180
|
+
color: 'success',
|
|
181
|
+
icon: 'i-lucide-sparkles',
|
|
182
|
+
})
|
|
183
|
+
} catch (err) {
|
|
184
|
+
toast.add({
|
|
185
|
+
title: 'Suggestion failed',
|
|
186
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
187
|
+
color: 'error',
|
|
188
|
+
})
|
|
189
|
+
} finally {
|
|
190
|
+
suggestingField.value = null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const canSave = computed(() => {
|
|
195
|
+
return (
|
|
196
|
+
draft.value.name.trim().length > 0
|
|
197
|
+
&& draft.value.role.trim().length > 0
|
|
198
|
+
&& draft.value.department.trim().length > 0
|
|
199
|
+
&& draft.value.disc_primary !== draft.value.disc_secondary
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
async function save() {
|
|
204
|
+
if (!canSave.value) return
|
|
205
|
+
saving.value = true
|
|
206
|
+
try {
|
|
207
|
+
const body = {
|
|
208
|
+
name: draft.value.name.trim(),
|
|
209
|
+
role: draft.value.role.trim(),
|
|
210
|
+
department: draft.value.department,
|
|
211
|
+
tier: draft.value.tier,
|
|
212
|
+
behavioral_dna: {
|
|
213
|
+
disc: {
|
|
214
|
+
primary: draft.value.disc_primary,
|
|
215
|
+
secondary: draft.value.disc_secondary,
|
|
216
|
+
},
|
|
217
|
+
enneagram: {
|
|
218
|
+
type: draft.value.enneagram_type,
|
|
219
|
+
wing: draft.value.enneagram_wing,
|
|
220
|
+
},
|
|
221
|
+
mbti: draft.value.mbti,
|
|
222
|
+
big_five: draft.value.big_five,
|
|
223
|
+
},
|
|
224
|
+
mental_models: { primary: draft.value.mental_models_primary, secondary: [] },
|
|
225
|
+
expertise: {
|
|
226
|
+
domains: draft.value.expertise_domains,
|
|
227
|
+
frameworks: draft.value.frameworks,
|
|
228
|
+
depth: draft.value.expertise_depth,
|
|
229
|
+
years_equivalent: draft.value.expertise_years,
|
|
230
|
+
},
|
|
231
|
+
communication: {
|
|
232
|
+
tone: draft.value.comm_tone,
|
|
233
|
+
vocabulary_level: draft.value.comm_vocab,
|
|
234
|
+
preferred_format: draft.value.comm_format,
|
|
235
|
+
language: draft.value.comm_language,
|
|
236
|
+
avoid: draft.value.comm_avoid,
|
|
237
|
+
},
|
|
238
|
+
linked_personas: draft.value.linked_personas,
|
|
239
|
+
}
|
|
240
|
+
const res = await $fetch<{
|
|
241
|
+
id: string
|
|
242
|
+
created: boolean
|
|
243
|
+
yaml_path?: string
|
|
244
|
+
error?: string
|
|
245
|
+
}>(`${apiBase}/api/agents`, { method: 'POST', body })
|
|
246
|
+
if (res.error) throw new Error(res.error)
|
|
247
|
+
toast.add({
|
|
248
|
+
title: 'Agent created',
|
|
249
|
+
description: res.yaml_path?.split('/').slice(-3).join('/') ?? res.id,
|
|
250
|
+
color: 'success',
|
|
251
|
+
})
|
|
252
|
+
navigateTo(`/agents/${res.id}`)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
toast.add({
|
|
255
|
+
title: 'Create failed',
|
|
256
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
257
|
+
color: 'error',
|
|
258
|
+
})
|
|
259
|
+
} finally {
|
|
260
|
+
saving.value = false
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const bigFiveLabels: Record<string, string> = {
|
|
265
|
+
openness: 'Openness',
|
|
266
|
+
conscientiousness: 'Conscientiousness',
|
|
267
|
+
extraversion: 'Extraversion',
|
|
268
|
+
agreeableness: 'Agreeableness',
|
|
269
|
+
neuroticism: 'Neuroticism',
|
|
270
|
+
}
|
|
271
|
+
const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
|
|
272
|
+
</script>
|
|
273
|
+
|
|
274
|
+
<template>
|
|
275
|
+
<UDashboardPanel id="agents-new">
|
|
276
|
+
<template #header>
|
|
277
|
+
<UDashboardNavbar title="New Agent">
|
|
278
|
+
<template #leading>
|
|
279
|
+
<UButton
|
|
280
|
+
icon="i-lucide-arrow-left"
|
|
281
|
+
variant="ghost"
|
|
282
|
+
size="sm"
|
|
283
|
+
aria-label="Back to agents"
|
|
284
|
+
to="/agents"
|
|
285
|
+
/>
|
|
286
|
+
</template>
|
|
287
|
+
<template #trailing>
|
|
288
|
+
<UBadge
|
|
289
|
+
label="AI-assisted"
|
|
290
|
+
icon="i-lucide-sparkles"
|
|
291
|
+
color="primary"
|
|
292
|
+
variant="soft"
|
|
293
|
+
size="sm"
|
|
294
|
+
/>
|
|
295
|
+
</template>
|
|
296
|
+
</UDashboardNavbar>
|
|
297
|
+
</template>
|
|
298
|
+
|
|
299
|
+
<template #body>
|
|
300
|
+
<div class="max-w-4xl mx-auto py-2 space-y-6">
|
|
301
|
+
<section class="space-y-3">
|
|
302
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Identity</h3>
|
|
303
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
304
|
+
<UFormField label="Name" required>
|
|
305
|
+
<UInput v-model="draft.name" class="w-full" placeholder="Lucas" />
|
|
306
|
+
</UFormField>
|
|
307
|
+
<UFormField label="Role" required>
|
|
308
|
+
<UInput v-model="draft.role" class="w-full" placeholder="Market & Competitive Intelligence Analyst" />
|
|
309
|
+
</UFormField>
|
|
310
|
+
<UFormField label="Department" required>
|
|
311
|
+
<USelect v-model="draft.department" :items="departmentOptions" class="w-full" />
|
|
312
|
+
</UFormField>
|
|
313
|
+
<UFormField label="Tier">
|
|
314
|
+
<USelect v-model="draft.tier" :items="tierOptions" class="w-full" />
|
|
315
|
+
</UFormField>
|
|
316
|
+
</div>
|
|
317
|
+
</section>
|
|
318
|
+
|
|
319
|
+
<section class="space-y-3">
|
|
320
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Behavioural DNA</h3>
|
|
321
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
322
|
+
<UFormField label="DISC primary">
|
|
323
|
+
<USelect v-model="draft.disc_primary" :items="discOptions" class="w-full" />
|
|
324
|
+
</UFormField>
|
|
325
|
+
<UFormField label="DISC secondary">
|
|
326
|
+
<USelect v-model="draft.disc_secondary" :items="discOptions" class="w-full" />
|
|
327
|
+
</UFormField>
|
|
328
|
+
<UFormField label="Enneagram type">
|
|
329
|
+
<UInput v-model.number="draft.enneagram_type" type="number" :min="1" :max="9" class="w-full" />
|
|
330
|
+
</UFormField>
|
|
331
|
+
<UFormField label="Enneagram wing">
|
|
332
|
+
<UInput v-model.number="draft.enneagram_wing" type="number" :min="1" :max="9" class="w-full" />
|
|
333
|
+
</UFormField>
|
|
334
|
+
<UFormField label="MBTI">
|
|
335
|
+
<USelect v-model="draft.mbti" :items="mbtiOptions" class="w-full" />
|
|
336
|
+
</UFormField>
|
|
337
|
+
</div>
|
|
338
|
+
<p v-if="draft.disc_primary === draft.disc_secondary" class="text-xs text-error">
|
|
339
|
+
DISC primary and secondary must differ.
|
|
340
|
+
</p>
|
|
341
|
+
<div class="space-y-2">
|
|
342
|
+
<p class="text-sm font-semibold text-muted">Big Five (OCEAN)</p>
|
|
343
|
+
<div v-for="key in bigFiveKeys" :key="key" class="flex items-center gap-3">
|
|
344
|
+
<span class="w-40 text-sm text-muted">{{ bigFiveLabels[key] }}</span>
|
|
345
|
+
<UInput
|
|
346
|
+
v-model.number="draft.big_five[key]"
|
|
347
|
+
type="number"
|
|
348
|
+
:min="0"
|
|
349
|
+
:max="100"
|
|
350
|
+
class="w-20"
|
|
351
|
+
/>
|
|
352
|
+
<div class="flex-1 h-2 rounded-full bg-muted/20">
|
|
353
|
+
<div class="h-2 rounded-full bg-primary" :style="{ width: `${draft.big_five[key]}%` }" />
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</section>
|
|
358
|
+
|
|
359
|
+
<section class="space-y-3">
|
|
360
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Knowledge</h3>
|
|
361
|
+
<UFormField label="Mental models (primary)" help="comma-separated">
|
|
362
|
+
<template #hint>
|
|
363
|
+
<UButton
|
|
364
|
+
label="Suggest with AI"
|
|
365
|
+
icon="i-lucide-sparkles"
|
|
366
|
+
size="xs"
|
|
367
|
+
color="primary"
|
|
368
|
+
variant="soft"
|
|
369
|
+
:loading="suggestingField === 'mental_models'"
|
|
370
|
+
:disabled="suggestingField !== null"
|
|
371
|
+
@click="suggest('mental_models')"
|
|
372
|
+
/>
|
|
373
|
+
</template>
|
|
374
|
+
<UInput
|
|
375
|
+
:model-value="listToCsv(draft.mental_models_primary)"
|
|
376
|
+
@update:model-value="(v: string) => { draft.mental_models_primary = csvToList(v) }"
|
|
377
|
+
class="w-full"
|
|
378
|
+
/>
|
|
379
|
+
</UFormField>
|
|
380
|
+
<UFormField label="Expertise domains" help="comma-separated">
|
|
381
|
+
<template #hint>
|
|
382
|
+
<UButton
|
|
383
|
+
label="Suggest with AI"
|
|
384
|
+
icon="i-lucide-sparkles"
|
|
385
|
+
size="xs"
|
|
386
|
+
color="primary"
|
|
387
|
+
variant="soft"
|
|
388
|
+
:loading="suggestingField === 'expertise_domains'"
|
|
389
|
+
:disabled="suggestingField !== null"
|
|
390
|
+
@click="suggest('expertise_domains')"
|
|
391
|
+
/>
|
|
392
|
+
</template>
|
|
393
|
+
<UInput
|
|
394
|
+
:model-value="listToCsv(draft.expertise_domains)"
|
|
395
|
+
@update:model-value="(v: string) => { draft.expertise_domains = csvToList(v) }"
|
|
396
|
+
class="w-full"
|
|
397
|
+
/>
|
|
398
|
+
</UFormField>
|
|
399
|
+
<div class="grid grid-cols-2 gap-3">
|
|
400
|
+
<UFormField label="Depth">
|
|
401
|
+
<USelect v-model="draft.expertise_depth" :items="depthOptions" class="w-full" />
|
|
402
|
+
</UFormField>
|
|
403
|
+
<UFormField label="Years (equivalent)">
|
|
404
|
+
<UInput v-model.number="draft.expertise_years" type="number" :min="0" :max="60" class="w-full" />
|
|
405
|
+
</UFormField>
|
|
406
|
+
</div>
|
|
407
|
+
<UFormField label="Frameworks" help="comma-separated">
|
|
408
|
+
<template #hint>
|
|
409
|
+
<UButton
|
|
410
|
+
label="Suggest with AI"
|
|
411
|
+
icon="i-lucide-sparkles"
|
|
412
|
+
size="xs"
|
|
413
|
+
color="primary"
|
|
414
|
+
variant="soft"
|
|
415
|
+
:loading="suggestingField === 'frameworks'"
|
|
416
|
+
:disabled="suggestingField !== null"
|
|
417
|
+
@click="suggest('frameworks')"
|
|
418
|
+
/>
|
|
419
|
+
</template>
|
|
420
|
+
<UInput
|
|
421
|
+
:model-value="listToCsv(draft.frameworks)"
|
|
422
|
+
@update:model-value="(v: string) => { draft.frameworks = csvToList(v) }"
|
|
423
|
+
class="w-full"
|
|
424
|
+
/>
|
|
425
|
+
</UFormField>
|
|
426
|
+
</section>
|
|
427
|
+
|
|
428
|
+
<section class="space-y-3">
|
|
429
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Communication</h3>
|
|
430
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
431
|
+
<UFormField label="Tone">
|
|
432
|
+
<UInput v-model="draft.comm_tone" class="w-full" placeholder="Analytical, calm" />
|
|
433
|
+
</UFormField>
|
|
434
|
+
<UFormField label="Vocabulary level">
|
|
435
|
+
<USelect v-model="draft.comm_vocab" :items="vocabOptions" class="w-full" />
|
|
436
|
+
</UFormField>
|
|
437
|
+
<UFormField label="Preferred format">
|
|
438
|
+
<UInput v-model="draft.comm_format" class="w-full" placeholder="Briefs, tables, charts" />
|
|
439
|
+
</UFormField>
|
|
440
|
+
<UFormField label="Language">
|
|
441
|
+
<UInput v-model="draft.comm_language" class="w-full" placeholder="en" />
|
|
442
|
+
</UFormField>
|
|
443
|
+
</div>
|
|
444
|
+
<UFormField label="Avoid (phrases)" help="comma-separated">
|
|
445
|
+
<UInput
|
|
446
|
+
:model-value="listToCsv(draft.comm_avoid)"
|
|
447
|
+
@update:model-value="(v: string) => { draft.comm_avoid = csvToList(v) }"
|
|
448
|
+
class="w-full"
|
|
449
|
+
/>
|
|
450
|
+
</UFormField>
|
|
451
|
+
</section>
|
|
452
|
+
|
|
453
|
+
<section class="space-y-3">
|
|
454
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Linked personas</h3>
|
|
455
|
+
<USelectMenu
|
|
456
|
+
v-model="draft.linked_personas"
|
|
457
|
+
:items="personaOptions"
|
|
458
|
+
value-key="value"
|
|
459
|
+
multiple
|
|
460
|
+
placeholder="Select personas to link"
|
|
461
|
+
class="w-full"
|
|
462
|
+
/>
|
|
463
|
+
</section>
|
|
464
|
+
|
|
465
|
+
<div class="flex items-center justify-end gap-2 pt-4 border-t border-default">
|
|
466
|
+
<UButton label="Cancel" variant="ghost" :disabled="saving" to="/agents" />
|
|
467
|
+
<UButton
|
|
468
|
+
label="Create agent"
|
|
469
|
+
icon="i-lucide-check"
|
|
470
|
+
:loading="saving"
|
|
471
|
+
:disabled="!canSave"
|
|
472
|
+
@click="save"
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
</template>
|
|
477
|
+
</UDashboardPanel>
|
|
478
|
+
</template>
|
|
@@ -221,6 +221,62 @@ function csvToList(value: string): string[] {
|
|
|
221
221
|
return value.split(',').map((s) => s.trim()).filter(Boolean)
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// PR81 v2.99.0 — AI list-field suggester for personas.
|
|
225
|
+
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains'
|
|
226
|
+
const suggestingField = ref<SuggestField | null>(null)
|
|
227
|
+
|
|
228
|
+
async function suggest(field: SuggestField) {
|
|
229
|
+
if (!draft.value || !detail.value) return
|
|
230
|
+
const current = (draft.value as any)[field] as string[]
|
|
231
|
+
suggestingField.value = field
|
|
232
|
+
try {
|
|
233
|
+
const res = await $fetch<{
|
|
234
|
+
suggestions: string[]
|
|
235
|
+
provider_name: string
|
|
236
|
+
error?: string
|
|
237
|
+
}>(`${apiBase}/api/personas/suggest`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
body: {
|
|
240
|
+
field,
|
|
241
|
+
count: 5,
|
|
242
|
+
context: {
|
|
243
|
+
name: detail.value.name,
|
|
244
|
+
title: detail.value.title,
|
|
245
|
+
current,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
if (res.error) throw new Error(res.error)
|
|
250
|
+
const additions = (res.suggestions ?? []).filter(
|
|
251
|
+
(s) => !current.some((c) => c.toLowerCase() === s.toLowerCase()),
|
|
252
|
+
)
|
|
253
|
+
if (additions.length === 0) {
|
|
254
|
+
toast.add({
|
|
255
|
+
title: 'No new suggestions',
|
|
256
|
+
description: 'The model returned only items you already have.',
|
|
257
|
+
color: 'info',
|
|
258
|
+
})
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
;(draft.value as any)[field] = [...current, ...additions]
|
|
262
|
+
markDirty()
|
|
263
|
+
toast.add({
|
|
264
|
+
title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
|
|
265
|
+
description: `via ${res.provider_name}`,
|
|
266
|
+
color: 'success',
|
|
267
|
+
icon: 'i-lucide-sparkles',
|
|
268
|
+
})
|
|
269
|
+
} catch (err) {
|
|
270
|
+
toast.add({
|
|
271
|
+
title: 'Suggestion failed',
|
|
272
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
273
|
+
color: 'error',
|
|
274
|
+
})
|
|
275
|
+
} finally {
|
|
276
|
+
suggestingField.value = null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
224
280
|
const mbtiOptions = [
|
|
225
281
|
'INTJ', 'INTP', 'ENTJ', 'ENTP',
|
|
226
282
|
'INFJ', 'INFP', 'ENFJ', 'ENFP',
|
|
@@ -627,6 +683,18 @@ const vocabOptions = [
|
|
|
627
683
|
<section class="space-y-3">
|
|
628
684
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Knowledge</h3>
|
|
629
685
|
<UFormField label="Mental models" help="comma-separated">
|
|
686
|
+
<template #hint>
|
|
687
|
+
<UButton
|
|
688
|
+
label="Suggest with AI"
|
|
689
|
+
icon="i-lucide-sparkles"
|
|
690
|
+
size="xs"
|
|
691
|
+
color="primary"
|
|
692
|
+
variant="soft"
|
|
693
|
+
:loading="suggestingField === 'mental_models'"
|
|
694
|
+
:disabled="suggestingField !== null"
|
|
695
|
+
@click="suggest('mental_models')"
|
|
696
|
+
/>
|
|
697
|
+
</template>
|
|
630
698
|
<UInput
|
|
631
699
|
:model-value="listToCsv(draft.mental_models)"
|
|
632
700
|
@update:model-value="(v: string) => { if (draft) { draft.mental_models = csvToList(v); markDirty() } }"
|
|
@@ -634,6 +702,18 @@ const vocabOptions = [
|
|
|
634
702
|
/>
|
|
635
703
|
</UFormField>
|
|
636
704
|
<UFormField label="Expertise domains" help="comma-separated">
|
|
705
|
+
<template #hint>
|
|
706
|
+
<UButton
|
|
707
|
+
label="Suggest with AI"
|
|
708
|
+
icon="i-lucide-sparkles"
|
|
709
|
+
size="xs"
|
|
710
|
+
color="primary"
|
|
711
|
+
variant="soft"
|
|
712
|
+
:loading="suggestingField === 'expertise_domains'"
|
|
713
|
+
:disabled="suggestingField !== null"
|
|
714
|
+
@click="suggest('expertise_domains')"
|
|
715
|
+
/>
|
|
716
|
+
</template>
|
|
637
717
|
<UInput
|
|
638
718
|
:model-value="listToCsv(draft.expertise_domains)"
|
|
639
719
|
@update:model-value="(v: string) => { if (draft) { draft.expertise_domains = csvToList(v); markDirty() } }"
|
|
@@ -641,6 +721,18 @@ const vocabOptions = [
|
|
|
641
721
|
/>
|
|
642
722
|
</UFormField>
|
|
643
723
|
<UFormField label="Frameworks" help="comma-separated">
|
|
724
|
+
<template #hint>
|
|
725
|
+
<UButton
|
|
726
|
+
label="Suggest with AI"
|
|
727
|
+
icon="i-lucide-sparkles"
|
|
728
|
+
size="xs"
|
|
729
|
+
color="primary"
|
|
730
|
+
variant="soft"
|
|
731
|
+
:loading="suggestingField === 'frameworks'"
|
|
732
|
+
:disabled="suggestingField !== null"
|
|
733
|
+
@click="suggest('frameworks')"
|
|
734
|
+
/>
|
|
735
|
+
</template>
|
|
644
736
|
<UInput
|
|
645
737
|
:model-value="listToCsv(draft.frameworks)"
|
|
646
738
|
@update:model-value="(v: string) => { if (draft) { draft.frameworks = csvToList(v); markDirty() } }"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1683,6 +1683,193 @@ def metrics():
|
|
|
1683
1683
|
return {"entries": entries[-50:], "avg_ms": round(avg_ms, 1), "total_calls": len(entries)}
|
|
1684
1684
|
|
|
1685
1685
|
|
|
1686
|
+
# --- Agent create (PR82 v3.0.0) ---
|
|
1687
|
+
|
|
1688
|
+
@app.post("/api/agents")
|
|
1689
|
+
def agent_create(body: dict):
|
|
1690
|
+
"""Create a new agent YAML file from a manual draft.
|
|
1691
|
+
|
|
1692
|
+
Required body keys: name, role, department, tier.
|
|
1693
|
+
Optional: behavioral_dna, expertise, mental_models, communication,
|
|
1694
|
+
linked_personas, authority.
|
|
1695
|
+
|
|
1696
|
+
Slug rule: <name-kebab>-<random-suffix> when no explicit `id` is
|
|
1697
|
+
given. The endpoint refuses to overwrite an existing file.
|
|
1698
|
+
"""
|
|
1699
|
+
if not isinstance(body, dict):
|
|
1700
|
+
return {"error": "body must be an object"}
|
|
1701
|
+
return _do_agent_create(body)
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def _do_agent_create(body: dict) -> dict:
|
|
1705
|
+
import re
|
|
1706
|
+
import uuid
|
|
1707
|
+
|
|
1708
|
+
name = (body.get("name") or "").strip()
|
|
1709
|
+
role = (body.get("role") or "").strip()
|
|
1710
|
+
department = (body.get("department") or "").strip().lower()
|
|
1711
|
+
tier_raw = body.get("tier")
|
|
1712
|
+
if not name or not role or not department:
|
|
1713
|
+
return {"error": "name, role, and department are required"}
|
|
1714
|
+
try:
|
|
1715
|
+
tier = int(tier_raw) if tier_raw is not None else 2
|
|
1716
|
+
except (TypeError, ValueError):
|
|
1717
|
+
return {"error": "tier must be an integer"}
|
|
1718
|
+
|
|
1719
|
+
dept_dir = ARKAOS_ROOT / "departments" / department / "agents"
|
|
1720
|
+
if not dept_dir.exists():
|
|
1721
|
+
return {"error": f"department '{department}' not found"}
|
|
1722
|
+
|
|
1723
|
+
explicit_id = (body.get("id") or "").strip()
|
|
1724
|
+
if explicit_id:
|
|
1725
|
+
slug = _agent_slugify(explicit_id)
|
|
1726
|
+
else:
|
|
1727
|
+
slug = f"{_agent_slugify(name)}-{uuid.uuid4().hex[:6]}"
|
|
1728
|
+
yaml_file = dept_dir / f"{slug}.yaml"
|
|
1729
|
+
if yaml_file.exists():
|
|
1730
|
+
return {"error": f"agent with id '{slug}' already exists"}
|
|
1731
|
+
|
|
1732
|
+
try:
|
|
1733
|
+
import yaml as _yaml
|
|
1734
|
+
except ImportError:
|
|
1735
|
+
return {"error": "PyYAML unavailable"}
|
|
1736
|
+
|
|
1737
|
+
payload = _build_agent_yaml(slug, name, role, department, tier, body)
|
|
1738
|
+
try:
|
|
1739
|
+
tmp = yaml_file.with_suffix(yaml_file.suffix + ".tmp")
|
|
1740
|
+
tmp.write_text(
|
|
1741
|
+
_yaml.safe_dump(payload, sort_keys=False, allow_unicode=True, default_flow_style=False),
|
|
1742
|
+
encoding="utf-8",
|
|
1743
|
+
)
|
|
1744
|
+
tmp.replace(yaml_file)
|
|
1745
|
+
except OSError as exc:
|
|
1746
|
+
return {"error": f"write failed: {exc}"}
|
|
1747
|
+
return {"id": slug, "created": True, "yaml_path": str(yaml_file)}
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _agent_slugify(text: str) -> str:
|
|
1751
|
+
import re
|
|
1752
|
+
cleaned = re.sub(r"[^a-z0-9-]+", "-", text.lower())
|
|
1753
|
+
cleaned = re.sub(r"-+", "-", cleaned).strip("-")
|
|
1754
|
+
return cleaned or "agent"
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def _build_agent_yaml(
|
|
1758
|
+
slug: str, name: str, role: str, department: str, tier: int, body: dict,
|
|
1759
|
+
) -> dict:
|
|
1760
|
+
"""Compose the YAML payload, applying sensible defaults."""
|
|
1761
|
+
dna = body.get("behavioral_dna") or {}
|
|
1762
|
+
disc = dna.get("disc") or {}
|
|
1763
|
+
enneagram = dna.get("enneagram") or {}
|
|
1764
|
+
big_five = dna.get("big_five") or {}
|
|
1765
|
+
mbti_raw = dna.get("mbti")
|
|
1766
|
+
mbti = mbti_raw.get("type") if isinstance(mbti_raw, dict) else mbti_raw
|
|
1767
|
+
|
|
1768
|
+
expertise = body.get("expertise") or {}
|
|
1769
|
+
mental_models = body.get("mental_models") or {}
|
|
1770
|
+
communication = body.get("communication") or {}
|
|
1771
|
+
authority = body.get("authority") or {}
|
|
1772
|
+
|
|
1773
|
+
payload: dict = {
|
|
1774
|
+
"id": slug,
|
|
1775
|
+
"name": name,
|
|
1776
|
+
"role": role,
|
|
1777
|
+
"department": department,
|
|
1778
|
+
"tier": tier,
|
|
1779
|
+
"model": "opus" if tier == 0 else "sonnet",
|
|
1780
|
+
"behavioral_dna": {
|
|
1781
|
+
"disc": {
|
|
1782
|
+
"primary": (disc.get("primary") or "I").upper(),
|
|
1783
|
+
"secondary": (disc.get("secondary") or "S").upper(),
|
|
1784
|
+
"communication_style": disc.get("communication_style") or "",
|
|
1785
|
+
"under_pressure": disc.get("under_pressure") or "",
|
|
1786
|
+
"motivator": disc.get("motivator") or "",
|
|
1787
|
+
},
|
|
1788
|
+
"enneagram": {
|
|
1789
|
+
"type": int(enneagram.get("type") or 5),
|
|
1790
|
+
"wing": int(enneagram.get("wing") or 4),
|
|
1791
|
+
"core_motivation": enneagram.get("core_motivation") or "",
|
|
1792
|
+
"core_fear": enneagram.get("core_fear") or "",
|
|
1793
|
+
"subtype": enneagram.get("subtype") or "self-preservation",
|
|
1794
|
+
},
|
|
1795
|
+
"big_five": {
|
|
1796
|
+
"openness": int(big_five.get("openness") or 70),
|
|
1797
|
+
"conscientiousness": int(big_five.get("conscientiousness") or 70),
|
|
1798
|
+
"extraversion": int(big_five.get("extraversion") or 50),
|
|
1799
|
+
"agreeableness": int(big_five.get("agreeableness") or 60),
|
|
1800
|
+
"neuroticism": int(big_five.get("neuroticism") or 30),
|
|
1801
|
+
},
|
|
1802
|
+
"mbti": {"type": (mbti or "INTJ").upper()},
|
|
1803
|
+
},
|
|
1804
|
+
"authority": {
|
|
1805
|
+
"delegates_to": _agent_str_list(authority.get("delegates_to") or []),
|
|
1806
|
+
"escalates_to": authority.get("escalates_to") or "",
|
|
1807
|
+
},
|
|
1808
|
+
"expertise": {
|
|
1809
|
+
"domains": _agent_str_list(expertise.get("domains") or []),
|
|
1810
|
+
"frameworks": _agent_str_list(expertise.get("frameworks") or []),
|
|
1811
|
+
"depth": expertise.get("depth") or "advanced",
|
|
1812
|
+
"years_equivalent": int(expertise.get("years_equivalent") or 5),
|
|
1813
|
+
},
|
|
1814
|
+
"mental_models": {
|
|
1815
|
+
"primary": _agent_str_list(mental_models.get("primary") or []),
|
|
1816
|
+
"secondary": _agent_str_list(mental_models.get("secondary") or []),
|
|
1817
|
+
},
|
|
1818
|
+
"communication": {
|
|
1819
|
+
"tone": communication.get("tone") or "",
|
|
1820
|
+
"vocabulary_level": communication.get("vocabulary_level") or "specialist",
|
|
1821
|
+
"preferred_format": communication.get("preferred_format") or "",
|
|
1822
|
+
"language": communication.get("language") or "en",
|
|
1823
|
+
"avoid": _agent_str_list(communication.get("avoid") or []),
|
|
1824
|
+
},
|
|
1825
|
+
"linked_personas": _agent_str_list(body.get("linked_personas") or []),
|
|
1826
|
+
}
|
|
1827
|
+
return payload
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
# --- AI list-field suggester (PR81 v2.99.0) ---
|
|
1831
|
+
|
|
1832
|
+
@app.post("/api/agents/suggest")
|
|
1833
|
+
def agents_suggest(body: dict):
|
|
1834
|
+
"""Suggest list items for the agent edit drawer via LLM.
|
|
1835
|
+
|
|
1836
|
+
Body: {
|
|
1837
|
+
"field": "mental_models" | "frameworks" | "expertise_domains",
|
|
1838
|
+
"context": {"name", "role", "department", "current": [...]},
|
|
1839
|
+
"count": <optional, default 5, max 12>
|
|
1840
|
+
}
|
|
1841
|
+
Returns: {"suggestions": [...], "provider_name": "...", "source": "agent"}
|
|
1842
|
+
"""
|
|
1843
|
+
return _do_field_suggest(body, source="agent")
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
@app.post("/api/personas/suggest")
|
|
1847
|
+
def personas_suggest(body: dict):
|
|
1848
|
+
"""Suggest list items for the persona edit slideover via LLM.
|
|
1849
|
+
|
|
1850
|
+
Same shape as /api/agents/suggest. `context.title` may be passed
|
|
1851
|
+
instead of `context.role` for personas.
|
|
1852
|
+
"""
|
|
1853
|
+
return _do_field_suggest(body, source="persona")
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
def _do_field_suggest(body: dict, *, source: str) -> dict:
|
|
1857
|
+
from core.agents.field_suggester import SuggestionError, suggest_field
|
|
1858
|
+
|
|
1859
|
+
field = (body.get("field") or "").strip()
|
|
1860
|
+
context = body.get("context") or {}
|
|
1861
|
+
count = body.get("count") or 5
|
|
1862
|
+
try:
|
|
1863
|
+
res = suggest_field(field, context, count=int(count))
|
|
1864
|
+
except SuggestionError as exc:
|
|
1865
|
+
return {"error": str(exc)}
|
|
1866
|
+
return {
|
|
1867
|
+
"suggestions": res.suggestions,
|
|
1868
|
+
"provider_name": res.provider_name,
|
|
1869
|
+
"source": source,
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
|
|
1686
1873
|
if __name__ == "__main__":
|
|
1687
1874
|
import argparse
|
|
1688
1875
|
parser = argparse.ArgumentParser()
|