arkaos 2.99.0 → 3.1.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__/draft_builder.cpython-313.pyc +0 -0
- package/core/agents/__pycache__/field_suggester.cpython-313.pyc +0 -0
- package/core/agents/draft_builder.py +156 -0
- package/dashboard/app/pages/agents/index.vue +8 -0
- package/dashboard/app/pages/agents/new.vue +589 -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 +180 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.1.0
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""AI-powered agent draft builder (PR82b v3.1.0).
|
|
2
|
+
|
|
3
|
+
Given a free-text description (and optionally a name/role/department),
|
|
4
|
+
produce a full agent draft: behavioural DNA, expertise, mental models,
|
|
5
|
+
and communication block — all in one LLM call.
|
|
6
|
+
|
|
7
|
+
Used by `POST /api/agents/draft` to power the "AI draft from
|
|
8
|
+
description" textarea on `/agents/new`. The operator then reviews and
|
|
9
|
+
edits the generated draft before clicking Create.
|
|
10
|
+
|
|
11
|
+
Provider-agnostic: callers can inject a fake `LLMProvider` in tests.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from core.runtime.llm_provider import LLMProvider, LLMUnavailable, get_llm_provider
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_SYSTEM = """You design behavioural-DNA profiles for AI agents. Read the
|
|
24
|
+
operator's description carefully, then emit a single JSON object that
|
|
25
|
+
follows this exact schema. Use ONLY the JSON keys listed — no prose,
|
|
26
|
+
no markdown fences, no extra fields.
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
"behavioral_dna": {
|
|
30
|
+
"disc": {
|
|
31
|
+
"primary": "D|I|S|C",
|
|
32
|
+
"secondary": "D|I|S|C",
|
|
33
|
+
"communication_style": "<one sentence>",
|
|
34
|
+
"under_pressure": "<one sentence>",
|
|
35
|
+
"motivator": "<one sentence>"
|
|
36
|
+
},
|
|
37
|
+
"enneagram": {
|
|
38
|
+
"type": 1-9,
|
|
39
|
+
"wing": 1-9,
|
|
40
|
+
"core_motivation": "<one sentence>",
|
|
41
|
+
"core_fear": "<one sentence>",
|
|
42
|
+
"subtype": "self-preservation|social|sexual"
|
|
43
|
+
},
|
|
44
|
+
"big_five": {
|
|
45
|
+
"openness": 0-100,
|
|
46
|
+
"conscientiousness": 0-100,
|
|
47
|
+
"extraversion": 0-100,
|
|
48
|
+
"agreeableness": 0-100,
|
|
49
|
+
"neuroticism": 0-100
|
|
50
|
+
},
|
|
51
|
+
"mbti": "<4-letter type>"
|
|
52
|
+
},
|
|
53
|
+
"expertise": {
|
|
54
|
+
"domains": ["<domain>", ...],
|
|
55
|
+
"frameworks": ["<framework>", ...],
|
|
56
|
+
"depth": "intermediate|advanced|expert|master",
|
|
57
|
+
"years_equivalent": <int>
|
|
58
|
+
},
|
|
59
|
+
"mental_models": {
|
|
60
|
+
"primary": ["<model>", ...],
|
|
61
|
+
"secondary": ["<model>", ...]
|
|
62
|
+
},
|
|
63
|
+
"communication": {
|
|
64
|
+
"tone": "<adjective list>",
|
|
65
|
+
"vocabulary_level": "lay|specialist|expert",
|
|
66
|
+
"preferred_format": "<format hint>",
|
|
67
|
+
"language": "<two-letter code, e.g. en>",
|
|
68
|
+
"avoid": ["<phrase>", ...]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Rules:
|
|
73
|
+
- DISC primary MUST differ from secondary.
|
|
74
|
+
- Pick concrete frameworks (e.g. "Porter's Five Forces") not abstract verbs.
|
|
75
|
+
- Provide 4-8 expertise.domains, 4-8 expertise.frameworks, 3-6 mental_models.primary.
|
|
76
|
+
- Keep all string values terse and concrete."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DraftError(RuntimeError):
|
|
80
|
+
"""LLM produced unusable output or could not be reached."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class DraftResult:
|
|
85
|
+
draft: dict
|
|
86
|
+
provider_name: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def draft_agent(
|
|
90
|
+
description: str,
|
|
91
|
+
*,
|
|
92
|
+
name: str = "",
|
|
93
|
+
role: str = "",
|
|
94
|
+
department: str = "",
|
|
95
|
+
tier: int = 2,
|
|
96
|
+
provider: LLMProvider | None = None,
|
|
97
|
+
) -> DraftResult:
|
|
98
|
+
"""Return a full agent draft inferred from a free-text description."""
|
|
99
|
+
description = (description or "").strip()
|
|
100
|
+
if len(description) < 20:
|
|
101
|
+
raise DraftError("description must be at least 20 characters")
|
|
102
|
+
llm = provider or get_llm_provider()
|
|
103
|
+
prompt = _build_prompt(description, name, role, department, tier)
|
|
104
|
+
try:
|
|
105
|
+
resp = llm.complete(prompt, max_tokens=2000, system=_SYSTEM)
|
|
106
|
+
except LLMUnavailable as exc:
|
|
107
|
+
raise DraftError(str(exc)) from exc
|
|
108
|
+
draft = _parse(resp.text)
|
|
109
|
+
_validate(draft)
|
|
110
|
+
return DraftResult(draft=draft, provider_name=llm.name())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _build_prompt(description: str, name: str, role: str, department: str, tier: int) -> str:
|
|
114
|
+
lines = ["Design an agent profile from this description:", "", description.strip(), ""]
|
|
115
|
+
if name:
|
|
116
|
+
lines.append(f"Name: {name}")
|
|
117
|
+
if role:
|
|
118
|
+
lines.append(f"Role: {role}")
|
|
119
|
+
if department:
|
|
120
|
+
lines.append(f"Department: {department}")
|
|
121
|
+
lines.append(f"Tier: {tier}")
|
|
122
|
+
lines.append("")
|
|
123
|
+
lines.append("Return ONLY the JSON object — no prose, no markdown fences.")
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse(text: str) -> dict:
|
|
128
|
+
cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip(), flags=re.MULTILINE)
|
|
129
|
+
cleaned = cleaned.strip()
|
|
130
|
+
try:
|
|
131
|
+
data = json.loads(cleaned)
|
|
132
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
133
|
+
raise DraftError(f"LLM returned non-JSON: {exc}") from exc
|
|
134
|
+
if not isinstance(data, dict):
|
|
135
|
+
raise DraftError("LLM returned a non-object JSON value")
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _validate(draft: dict) -> None:
|
|
140
|
+
"""Sanity-check the LLM payload — catches the common failure modes
|
|
141
|
+
before the operator sees a half-broken form."""
|
|
142
|
+
dna = draft.get("behavioral_dna")
|
|
143
|
+
if not isinstance(dna, dict):
|
|
144
|
+
raise DraftError("missing behavioral_dna block")
|
|
145
|
+
disc = dna.get("disc") or {}
|
|
146
|
+
if disc.get("primary") and disc.get("primary") == disc.get("secondary"):
|
|
147
|
+
raise DraftError("DISC primary and secondary must differ")
|
|
148
|
+
valid_disc = {"D", "I", "S", "C"}
|
|
149
|
+
if disc.get("primary") and str(disc["primary"]).upper() not in valid_disc:
|
|
150
|
+
raise DraftError(f"invalid DISC primary: {disc.get('primary')!r}")
|
|
151
|
+
if disc.get("secondary") and str(disc["secondary"]).upper() not in valid_disc:
|
|
152
|
+
raise DraftError(f"invalid DISC secondary: {disc.get('secondary')!r}")
|
|
153
|
+
big_five = dna.get("big_five") or {}
|
|
154
|
+
for axis, value in big_five.items():
|
|
155
|
+
if not isinstance(value, (int, float)) or not 0 <= value <= 100:
|
|
156
|
+
raise DraftError(f"big_five.{axis} must be 0..100, got {value!r}")
|
|
@@ -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,589 @@
|
|
|
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
|
+
// PR82b v3.1.0 — AI draft from description.
|
|
92
|
+
const description = ref('')
|
|
93
|
+
const drafting = ref(false)
|
|
94
|
+
|
|
95
|
+
async function draftFromDescription() {
|
|
96
|
+
const desc = description.value.trim()
|
|
97
|
+
if (desc.length < 20) {
|
|
98
|
+
toast.add({
|
|
99
|
+
title: 'Add more detail',
|
|
100
|
+
description: 'Describe the agent in at least a sentence or two.',
|
|
101
|
+
color: 'warning',
|
|
102
|
+
})
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
drafting.value = true
|
|
106
|
+
try {
|
|
107
|
+
const res = await $fetch<{
|
|
108
|
+
draft: any
|
|
109
|
+
provider_name: string
|
|
110
|
+
error?: string
|
|
111
|
+
}>(`${apiBase}/api/agents/draft`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: {
|
|
114
|
+
description: desc,
|
|
115
|
+
name: draft.value.name,
|
|
116
|
+
role: draft.value.role,
|
|
117
|
+
department: draft.value.department,
|
|
118
|
+
tier: draft.value.tier,
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
if (res.error) throw new Error(res.error)
|
|
122
|
+
applyDraft(res.draft)
|
|
123
|
+
toast.add({
|
|
124
|
+
title: 'Draft generated',
|
|
125
|
+
description: `via ${res.provider_name} — review and edit before creating.`,
|
|
126
|
+
color: 'success',
|
|
127
|
+
icon: 'i-lucide-sparkles',
|
|
128
|
+
})
|
|
129
|
+
} catch (err) {
|
|
130
|
+
toast.add({
|
|
131
|
+
title: 'Draft failed',
|
|
132
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
133
|
+
color: 'error',
|
|
134
|
+
})
|
|
135
|
+
} finally {
|
|
136
|
+
drafting.value = false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyDraft(d: any) {
|
|
141
|
+
const dna = d?.behavioral_dna ?? {}
|
|
142
|
+
const disc = dna.disc ?? {}
|
|
143
|
+
const enn = dna.enneagram ?? {}
|
|
144
|
+
const bf = dna.big_five ?? {}
|
|
145
|
+
if (disc.primary) draft.value.disc_primary = String(disc.primary).toUpperCase()
|
|
146
|
+
if (disc.secondary) draft.value.disc_secondary = String(disc.secondary).toUpperCase()
|
|
147
|
+
if (enn.type) draft.value.enneagram_type = Number(enn.type)
|
|
148
|
+
if (enn.wing) draft.value.enneagram_wing = Number(enn.wing)
|
|
149
|
+
if (dna.mbti) draft.value.mbti = String(dna.mbti).toUpperCase()
|
|
150
|
+
for (const key of ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const) {
|
|
151
|
+
if (typeof bf[key] === 'number') draft.value.big_five[key] = bf[key]
|
|
152
|
+
}
|
|
153
|
+
const exp = d?.expertise ?? {}
|
|
154
|
+
if (Array.isArray(exp.domains)) draft.value.expertise_domains = exp.domains.map(String)
|
|
155
|
+
if (Array.isArray(exp.frameworks)) draft.value.frameworks = exp.frameworks.map(String)
|
|
156
|
+
if (exp.depth) draft.value.expertise_depth = String(exp.depth)
|
|
157
|
+
if (typeof exp.years_equivalent === 'number') draft.value.expertise_years = exp.years_equivalent
|
|
158
|
+
const mm = d?.mental_models ?? {}
|
|
159
|
+
if (Array.isArray(mm.primary)) draft.value.mental_models_primary = mm.primary.map(String)
|
|
160
|
+
const comm = d?.communication ?? {}
|
|
161
|
+
if (comm.tone) draft.value.comm_tone = String(comm.tone)
|
|
162
|
+
if (comm.vocabulary_level) draft.value.comm_vocab = String(comm.vocabulary_level)
|
|
163
|
+
if (comm.preferred_format) draft.value.comm_format = String(comm.preferred_format)
|
|
164
|
+
if (comm.language) draft.value.comm_language = String(comm.language)
|
|
165
|
+
if (Array.isArray(comm.avoid)) draft.value.comm_avoid = comm.avoid.map(String)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const departmentOptions = [
|
|
169
|
+
'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
|
|
170
|
+
'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
|
|
171
|
+
].map((d) => ({ label: d, value: d }))
|
|
172
|
+
|
|
173
|
+
const tierOptions = [
|
|
174
|
+
{ label: 'Tier 1 — Squad Lead', value: 1 },
|
|
175
|
+
{ label: 'Tier 2 — Specialist', value: 2 },
|
|
176
|
+
{ label: 'Tier 3 — Support', value: 3 },
|
|
177
|
+
]
|
|
178
|
+
const discOptions = ['D', 'I', 'S', 'C'].map((v) => ({ label: v, value: v }))
|
|
179
|
+
const depthOptions = [
|
|
180
|
+
{ label: 'Intermediate', value: 'intermediate' },
|
|
181
|
+
{ label: 'Advanced', value: 'advanced' },
|
|
182
|
+
{ label: 'Expert', value: 'expert' },
|
|
183
|
+
{ label: 'Master', value: 'master' },
|
|
184
|
+
]
|
|
185
|
+
const vocabOptions = [
|
|
186
|
+
{ label: 'Lay (no jargon)', value: 'lay' },
|
|
187
|
+
{ label: 'Specialist (industry terms)', value: 'specialist' },
|
|
188
|
+
{ label: 'Expert (research-level)', value: 'expert' },
|
|
189
|
+
]
|
|
190
|
+
const mbtiOptions = [
|
|
191
|
+
'INTJ', 'INTP', 'ENTJ', 'ENTP',
|
|
192
|
+
'INFJ', 'INFP', 'ENFJ', 'ENFP',
|
|
193
|
+
'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ',
|
|
194
|
+
'ISTP', 'ISFP', 'ESTP', 'ESFP',
|
|
195
|
+
].map((t) => ({ label: t, value: t }))
|
|
196
|
+
|
|
197
|
+
function listToCsv(list: string[]): string {
|
|
198
|
+
return list.join(', ')
|
|
199
|
+
}
|
|
200
|
+
function csvToList(value: string): string[] {
|
|
201
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// PR81 suggest wiring — three list fields.
|
|
205
|
+
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains'
|
|
206
|
+
const suggestingField = ref<SuggestField | null>(null)
|
|
207
|
+
|
|
208
|
+
async function suggest(field: SuggestField) {
|
|
209
|
+
const current
|
|
210
|
+
= field === 'mental_models'
|
|
211
|
+
? draft.value.mental_models_primary
|
|
212
|
+
: field === 'frameworks'
|
|
213
|
+
? draft.value.frameworks
|
|
214
|
+
: draft.value.expertise_domains
|
|
215
|
+
if (!draft.value.name.trim() || !draft.value.role.trim()) {
|
|
216
|
+
toast.add({
|
|
217
|
+
title: 'Add a name and role first',
|
|
218
|
+
description: 'AI needs the basics to make useful suggestions.',
|
|
219
|
+
color: 'warning',
|
|
220
|
+
})
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
suggestingField.value = field
|
|
224
|
+
try {
|
|
225
|
+
const res = await $fetch<{
|
|
226
|
+
suggestions: string[]
|
|
227
|
+
provider_name: string
|
|
228
|
+
error?: string
|
|
229
|
+
}>(`${apiBase}/api/agents/suggest`, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
body: {
|
|
232
|
+
field,
|
|
233
|
+
count: 5,
|
|
234
|
+
context: {
|
|
235
|
+
name: draft.value.name,
|
|
236
|
+
role: draft.value.role,
|
|
237
|
+
department: draft.value.department,
|
|
238
|
+
current,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
if (res.error) throw new Error(res.error)
|
|
243
|
+
const additions = (res.suggestions ?? []).filter(
|
|
244
|
+
(s) => !current.some((c) => c.toLowerCase() === s.toLowerCase()),
|
|
245
|
+
)
|
|
246
|
+
if (additions.length === 0) {
|
|
247
|
+
toast.add({ title: 'No new suggestions', color: 'info' })
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
const merged = [...current, ...additions]
|
|
251
|
+
if (field === 'mental_models') draft.value.mental_models_primary = merged
|
|
252
|
+
else if (field === 'frameworks') draft.value.frameworks = merged
|
|
253
|
+
else draft.value.expertise_domains = merged
|
|
254
|
+
toast.add({
|
|
255
|
+
title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
|
|
256
|
+
description: `via ${res.provider_name}`,
|
|
257
|
+
color: 'success',
|
|
258
|
+
icon: 'i-lucide-sparkles',
|
|
259
|
+
})
|
|
260
|
+
} catch (err) {
|
|
261
|
+
toast.add({
|
|
262
|
+
title: 'Suggestion failed',
|
|
263
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
264
|
+
color: 'error',
|
|
265
|
+
})
|
|
266
|
+
} finally {
|
|
267
|
+
suggestingField.value = null
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const canSave = computed(() => {
|
|
272
|
+
return (
|
|
273
|
+
draft.value.name.trim().length > 0
|
|
274
|
+
&& draft.value.role.trim().length > 0
|
|
275
|
+
&& draft.value.department.trim().length > 0
|
|
276
|
+
&& draft.value.disc_primary !== draft.value.disc_secondary
|
|
277
|
+
)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
async function save() {
|
|
281
|
+
if (!canSave.value) return
|
|
282
|
+
saving.value = true
|
|
283
|
+
try {
|
|
284
|
+
const body = {
|
|
285
|
+
name: draft.value.name.trim(),
|
|
286
|
+
role: draft.value.role.trim(),
|
|
287
|
+
department: draft.value.department,
|
|
288
|
+
tier: draft.value.tier,
|
|
289
|
+
behavioral_dna: {
|
|
290
|
+
disc: {
|
|
291
|
+
primary: draft.value.disc_primary,
|
|
292
|
+
secondary: draft.value.disc_secondary,
|
|
293
|
+
},
|
|
294
|
+
enneagram: {
|
|
295
|
+
type: draft.value.enneagram_type,
|
|
296
|
+
wing: draft.value.enneagram_wing,
|
|
297
|
+
},
|
|
298
|
+
mbti: draft.value.mbti,
|
|
299
|
+
big_five: draft.value.big_five,
|
|
300
|
+
},
|
|
301
|
+
mental_models: { primary: draft.value.mental_models_primary, secondary: [] },
|
|
302
|
+
expertise: {
|
|
303
|
+
domains: draft.value.expertise_domains,
|
|
304
|
+
frameworks: draft.value.frameworks,
|
|
305
|
+
depth: draft.value.expertise_depth,
|
|
306
|
+
years_equivalent: draft.value.expertise_years,
|
|
307
|
+
},
|
|
308
|
+
communication: {
|
|
309
|
+
tone: draft.value.comm_tone,
|
|
310
|
+
vocabulary_level: draft.value.comm_vocab,
|
|
311
|
+
preferred_format: draft.value.comm_format,
|
|
312
|
+
language: draft.value.comm_language,
|
|
313
|
+
avoid: draft.value.comm_avoid,
|
|
314
|
+
},
|
|
315
|
+
linked_personas: draft.value.linked_personas,
|
|
316
|
+
}
|
|
317
|
+
const res = await $fetch<{
|
|
318
|
+
id: string
|
|
319
|
+
created: boolean
|
|
320
|
+
yaml_path?: string
|
|
321
|
+
error?: string
|
|
322
|
+
}>(`${apiBase}/api/agents`, { method: 'POST', body })
|
|
323
|
+
if (res.error) throw new Error(res.error)
|
|
324
|
+
toast.add({
|
|
325
|
+
title: 'Agent created',
|
|
326
|
+
description: res.yaml_path?.split('/').slice(-3).join('/') ?? res.id,
|
|
327
|
+
color: 'success',
|
|
328
|
+
})
|
|
329
|
+
navigateTo(`/agents/${res.id}`)
|
|
330
|
+
} catch (err) {
|
|
331
|
+
toast.add({
|
|
332
|
+
title: 'Create failed',
|
|
333
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
334
|
+
color: 'error',
|
|
335
|
+
})
|
|
336
|
+
} finally {
|
|
337
|
+
saving.value = false
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const bigFiveLabels: Record<string, string> = {
|
|
342
|
+
openness: 'Openness',
|
|
343
|
+
conscientiousness: 'Conscientiousness',
|
|
344
|
+
extraversion: 'Extraversion',
|
|
345
|
+
agreeableness: 'Agreeableness',
|
|
346
|
+
neuroticism: 'Neuroticism',
|
|
347
|
+
}
|
|
348
|
+
const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
|
|
349
|
+
</script>
|
|
350
|
+
|
|
351
|
+
<template>
|
|
352
|
+
<UDashboardPanel id="agents-new">
|
|
353
|
+
<template #header>
|
|
354
|
+
<UDashboardNavbar title="New Agent">
|
|
355
|
+
<template #leading>
|
|
356
|
+
<UButton
|
|
357
|
+
icon="i-lucide-arrow-left"
|
|
358
|
+
variant="ghost"
|
|
359
|
+
size="sm"
|
|
360
|
+
aria-label="Back to agents"
|
|
361
|
+
to="/agents"
|
|
362
|
+
/>
|
|
363
|
+
</template>
|
|
364
|
+
<template #trailing>
|
|
365
|
+
<UBadge
|
|
366
|
+
label="AI-assisted"
|
|
367
|
+
icon="i-lucide-sparkles"
|
|
368
|
+
color="primary"
|
|
369
|
+
variant="soft"
|
|
370
|
+
size="sm"
|
|
371
|
+
/>
|
|
372
|
+
</template>
|
|
373
|
+
</UDashboardNavbar>
|
|
374
|
+
</template>
|
|
375
|
+
|
|
376
|
+
<template #body>
|
|
377
|
+
<div class="max-w-4xl mx-auto py-2 space-y-6">
|
|
378
|
+
<section class="rounded-xl border border-primary/30 bg-primary/5 p-4 space-y-3">
|
|
379
|
+
<div class="flex items-center justify-between gap-3">
|
|
380
|
+
<div>
|
|
381
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-primary flex items-center gap-2">
|
|
382
|
+
<UIcon name="i-lucide-sparkles" class="size-4" />
|
|
383
|
+
Draft with AI
|
|
384
|
+
</h3>
|
|
385
|
+
<p class="text-xs text-muted mt-0.5">
|
|
386
|
+
Describe the agent in plain text — the LLM fills the whole form below.
|
|
387
|
+
You can still edit everything before saving.
|
|
388
|
+
</p>
|
|
389
|
+
</div>
|
|
390
|
+
<UButton
|
|
391
|
+
label="Generate draft"
|
|
392
|
+
icon="i-lucide-wand"
|
|
393
|
+
color="primary"
|
|
394
|
+
:loading="drafting"
|
|
395
|
+
:disabled="description.trim().length < 20"
|
|
396
|
+
@click="draftFromDescription"
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
<UTextarea
|
|
400
|
+
v-model="description"
|
|
401
|
+
placeholder="A senior strategist who decides fast, demands evidence, and is allergic to fluff. Spent 10 years at McKinsey covering CPG..."
|
|
402
|
+
:rows="3"
|
|
403
|
+
class="w-full"
|
|
404
|
+
/>
|
|
405
|
+
<p class="text-xs text-muted">
|
|
406
|
+
Tip: fill in <span class="font-mono">Name</span> /
|
|
407
|
+
<span class="font-mono">Role</span> /
|
|
408
|
+
<span class="font-mono">Department</span> below first for more precise output.
|
|
409
|
+
</p>
|
|
410
|
+
</section>
|
|
411
|
+
|
|
412
|
+
<section class="space-y-3">
|
|
413
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Identity</h3>
|
|
414
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
415
|
+
<UFormField label="Name" required>
|
|
416
|
+
<UInput v-model="draft.name" class="w-full" placeholder="Lucas" />
|
|
417
|
+
</UFormField>
|
|
418
|
+
<UFormField label="Role" required>
|
|
419
|
+
<UInput v-model="draft.role" class="w-full" placeholder="Market & Competitive Intelligence Analyst" />
|
|
420
|
+
</UFormField>
|
|
421
|
+
<UFormField label="Department" required>
|
|
422
|
+
<USelect v-model="draft.department" :items="departmentOptions" class="w-full" />
|
|
423
|
+
</UFormField>
|
|
424
|
+
<UFormField label="Tier">
|
|
425
|
+
<USelect v-model="draft.tier" :items="tierOptions" class="w-full" />
|
|
426
|
+
</UFormField>
|
|
427
|
+
</div>
|
|
428
|
+
</section>
|
|
429
|
+
|
|
430
|
+
<section class="space-y-3">
|
|
431
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Behavioural DNA</h3>
|
|
432
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
433
|
+
<UFormField label="DISC primary">
|
|
434
|
+
<USelect v-model="draft.disc_primary" :items="discOptions" class="w-full" />
|
|
435
|
+
</UFormField>
|
|
436
|
+
<UFormField label="DISC secondary">
|
|
437
|
+
<USelect v-model="draft.disc_secondary" :items="discOptions" class="w-full" />
|
|
438
|
+
</UFormField>
|
|
439
|
+
<UFormField label="Enneagram type">
|
|
440
|
+
<UInput v-model.number="draft.enneagram_type" type="number" :min="1" :max="9" class="w-full" />
|
|
441
|
+
</UFormField>
|
|
442
|
+
<UFormField label="Enneagram wing">
|
|
443
|
+
<UInput v-model.number="draft.enneagram_wing" type="number" :min="1" :max="9" class="w-full" />
|
|
444
|
+
</UFormField>
|
|
445
|
+
<UFormField label="MBTI">
|
|
446
|
+
<USelect v-model="draft.mbti" :items="mbtiOptions" class="w-full" />
|
|
447
|
+
</UFormField>
|
|
448
|
+
</div>
|
|
449
|
+
<p v-if="draft.disc_primary === draft.disc_secondary" class="text-xs text-error">
|
|
450
|
+
DISC primary and secondary must differ.
|
|
451
|
+
</p>
|
|
452
|
+
<div class="space-y-2">
|
|
453
|
+
<p class="text-sm font-semibold text-muted">Big Five (OCEAN)</p>
|
|
454
|
+
<div v-for="key in bigFiveKeys" :key="key" class="flex items-center gap-3">
|
|
455
|
+
<span class="w-40 text-sm text-muted">{{ bigFiveLabels[key] }}</span>
|
|
456
|
+
<UInput
|
|
457
|
+
v-model.number="draft.big_five[key]"
|
|
458
|
+
type="number"
|
|
459
|
+
:min="0"
|
|
460
|
+
:max="100"
|
|
461
|
+
class="w-20"
|
|
462
|
+
/>
|
|
463
|
+
<div class="flex-1 h-2 rounded-full bg-muted/20">
|
|
464
|
+
<div class="h-2 rounded-full bg-primary" :style="{ width: `${draft.big_five[key]}%` }" />
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</section>
|
|
469
|
+
|
|
470
|
+
<section class="space-y-3">
|
|
471
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Knowledge</h3>
|
|
472
|
+
<UFormField label="Mental models (primary)" help="comma-separated">
|
|
473
|
+
<template #hint>
|
|
474
|
+
<UButton
|
|
475
|
+
label="Suggest with AI"
|
|
476
|
+
icon="i-lucide-sparkles"
|
|
477
|
+
size="xs"
|
|
478
|
+
color="primary"
|
|
479
|
+
variant="soft"
|
|
480
|
+
:loading="suggestingField === 'mental_models'"
|
|
481
|
+
:disabled="suggestingField !== null"
|
|
482
|
+
@click="suggest('mental_models')"
|
|
483
|
+
/>
|
|
484
|
+
</template>
|
|
485
|
+
<UInput
|
|
486
|
+
:model-value="listToCsv(draft.mental_models_primary)"
|
|
487
|
+
@update:model-value="(v: string) => { draft.mental_models_primary = csvToList(v) }"
|
|
488
|
+
class="w-full"
|
|
489
|
+
/>
|
|
490
|
+
</UFormField>
|
|
491
|
+
<UFormField label="Expertise domains" help="comma-separated">
|
|
492
|
+
<template #hint>
|
|
493
|
+
<UButton
|
|
494
|
+
label="Suggest with AI"
|
|
495
|
+
icon="i-lucide-sparkles"
|
|
496
|
+
size="xs"
|
|
497
|
+
color="primary"
|
|
498
|
+
variant="soft"
|
|
499
|
+
:loading="suggestingField === 'expertise_domains'"
|
|
500
|
+
:disabled="suggestingField !== null"
|
|
501
|
+
@click="suggest('expertise_domains')"
|
|
502
|
+
/>
|
|
503
|
+
</template>
|
|
504
|
+
<UInput
|
|
505
|
+
:model-value="listToCsv(draft.expertise_domains)"
|
|
506
|
+
@update:model-value="(v: string) => { draft.expertise_domains = csvToList(v) }"
|
|
507
|
+
class="w-full"
|
|
508
|
+
/>
|
|
509
|
+
</UFormField>
|
|
510
|
+
<div class="grid grid-cols-2 gap-3">
|
|
511
|
+
<UFormField label="Depth">
|
|
512
|
+
<USelect v-model="draft.expertise_depth" :items="depthOptions" class="w-full" />
|
|
513
|
+
</UFormField>
|
|
514
|
+
<UFormField label="Years (equivalent)">
|
|
515
|
+
<UInput v-model.number="draft.expertise_years" type="number" :min="0" :max="60" class="w-full" />
|
|
516
|
+
</UFormField>
|
|
517
|
+
</div>
|
|
518
|
+
<UFormField label="Frameworks" help="comma-separated">
|
|
519
|
+
<template #hint>
|
|
520
|
+
<UButton
|
|
521
|
+
label="Suggest with AI"
|
|
522
|
+
icon="i-lucide-sparkles"
|
|
523
|
+
size="xs"
|
|
524
|
+
color="primary"
|
|
525
|
+
variant="soft"
|
|
526
|
+
:loading="suggestingField === 'frameworks'"
|
|
527
|
+
:disabled="suggestingField !== null"
|
|
528
|
+
@click="suggest('frameworks')"
|
|
529
|
+
/>
|
|
530
|
+
</template>
|
|
531
|
+
<UInput
|
|
532
|
+
:model-value="listToCsv(draft.frameworks)"
|
|
533
|
+
@update:model-value="(v: string) => { draft.frameworks = csvToList(v) }"
|
|
534
|
+
class="w-full"
|
|
535
|
+
/>
|
|
536
|
+
</UFormField>
|
|
537
|
+
</section>
|
|
538
|
+
|
|
539
|
+
<section class="space-y-3">
|
|
540
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Communication</h3>
|
|
541
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
542
|
+
<UFormField label="Tone">
|
|
543
|
+
<UInput v-model="draft.comm_tone" class="w-full" placeholder="Analytical, calm" />
|
|
544
|
+
</UFormField>
|
|
545
|
+
<UFormField label="Vocabulary level">
|
|
546
|
+
<USelect v-model="draft.comm_vocab" :items="vocabOptions" class="w-full" />
|
|
547
|
+
</UFormField>
|
|
548
|
+
<UFormField label="Preferred format">
|
|
549
|
+
<UInput v-model="draft.comm_format" class="w-full" placeholder="Briefs, tables, charts" />
|
|
550
|
+
</UFormField>
|
|
551
|
+
<UFormField label="Language">
|
|
552
|
+
<UInput v-model="draft.comm_language" class="w-full" placeholder="en" />
|
|
553
|
+
</UFormField>
|
|
554
|
+
</div>
|
|
555
|
+
<UFormField label="Avoid (phrases)" help="comma-separated">
|
|
556
|
+
<UInput
|
|
557
|
+
:model-value="listToCsv(draft.comm_avoid)"
|
|
558
|
+
@update:model-value="(v: string) => { draft.comm_avoid = csvToList(v) }"
|
|
559
|
+
class="w-full"
|
|
560
|
+
/>
|
|
561
|
+
</UFormField>
|
|
562
|
+
</section>
|
|
563
|
+
|
|
564
|
+
<section class="space-y-3">
|
|
565
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Linked personas</h3>
|
|
566
|
+
<USelectMenu
|
|
567
|
+
v-model="draft.linked_personas"
|
|
568
|
+
:items="personaOptions"
|
|
569
|
+
value-key="value"
|
|
570
|
+
multiple
|
|
571
|
+
placeholder="Select personas to link"
|
|
572
|
+
class="w-full"
|
|
573
|
+
/>
|
|
574
|
+
</section>
|
|
575
|
+
|
|
576
|
+
<div class="flex items-center justify-end gap-2 pt-4 border-t border-default">
|
|
577
|
+
<UButton label="Cancel" variant="ghost" :disabled="saving" to="/agents" />
|
|
578
|
+
<UButton
|
|
579
|
+
label="Create agent"
|
|
580
|
+
icon="i-lucide-check"
|
|
581
|
+
:loading="saving"
|
|
582
|
+
:disabled="!canSave"
|
|
583
|
+
@click="save"
|
|
584
|
+
/>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
</template>
|
|
588
|
+
</UDashboardPanel>
|
|
589
|
+
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1683,6 +1683,186 @@ 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 agent draft from description (PR82b v3.1.0) ---
|
|
1831
|
+
|
|
1832
|
+
@app.post("/api/agents/draft")
|
|
1833
|
+
def agents_draft(body: dict):
|
|
1834
|
+
"""Generate a full agent draft from a free-text description.
|
|
1835
|
+
|
|
1836
|
+
Body: {
|
|
1837
|
+
"description": "...", # min 20 chars
|
|
1838
|
+
"name": "Lucas", # optional
|
|
1839
|
+
"role": "Market Analyst",# optional
|
|
1840
|
+
"department": "strategy",# optional
|
|
1841
|
+
"tier": 2 # optional, default 2
|
|
1842
|
+
}
|
|
1843
|
+
Returns: {"draft": {...behavioral_dna, expertise, mental_models,
|
|
1844
|
+
communication...}, "provider_name": "..."}
|
|
1845
|
+
"""
|
|
1846
|
+
from core.agents.draft_builder import DraftError, draft_agent
|
|
1847
|
+
|
|
1848
|
+
description = (body.get("description") or "").strip()
|
|
1849
|
+
try:
|
|
1850
|
+
tier = int(body.get("tier") or 2)
|
|
1851
|
+
except (TypeError, ValueError):
|
|
1852
|
+
tier = 2
|
|
1853
|
+
try:
|
|
1854
|
+
res = draft_agent(
|
|
1855
|
+
description,
|
|
1856
|
+
name=(body.get("name") or "").strip(),
|
|
1857
|
+
role=(body.get("role") or "").strip(),
|
|
1858
|
+
department=(body.get("department") or "").strip(),
|
|
1859
|
+
tier=tier,
|
|
1860
|
+
)
|
|
1861
|
+
except DraftError as exc:
|
|
1862
|
+
return {"error": str(exc)}
|
|
1863
|
+
return {"draft": res.draft, "provider_name": res.provider_name}
|
|
1864
|
+
|
|
1865
|
+
|
|
1686
1866
|
# --- AI list-field suggester (PR81 v2.99.0) ---
|
|
1687
1867
|
|
|
1688
1868
|
@app.post("/api/agents/suggest")
|