arkaos 3.0.0 → 3.2.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/core/agents/field_suggester.py +21 -4
- package/dashboard/app/components/AgentEditDrawer.vue +20 -3
- package/dashboard/app/pages/agents/new.vue +130 -3
- package/dashboard/app/pages/personas/[id].vue +58 -3
- package/dashboard/app/types/index.d.ts +2 -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 +36 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.2.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}")
|
|
@@ -27,6 +27,8 @@ _VALID_FIELDS: tuple[str, ...] = (
|
|
|
27
27
|
"mental_models",
|
|
28
28
|
"frameworks",
|
|
29
29
|
"expertise_domains",
|
|
30
|
+
"communication_avoid",
|
|
31
|
+
"key_quotes",
|
|
30
32
|
)
|
|
31
33
|
_MAX_COUNT = 12
|
|
32
34
|
_DEFAULT_COUNT = 5
|
|
@@ -40,6 +42,23 @@ _FIELD_LABELS: dict[str, str] = {
|
|
|
40
42
|
"mental_models": "mental models",
|
|
41
43
|
"frameworks": "frameworks",
|
|
42
44
|
"expertise_domains": "expertise domains",
|
|
45
|
+
"communication_avoid": "phrases this profile should AVOID using",
|
|
46
|
+
"key_quotes": "verbatim or paraphrased key quotes",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Field-specific length hints — different fields want different item shapes.
|
|
50
|
+
_FIELD_LENGTH_HINT: dict[str, str] = {
|
|
51
|
+
"mental_models": "Return a JSON array of short strings (2-5 words each).",
|
|
52
|
+
"frameworks": "Return a JSON array of short strings (2-5 words each).",
|
|
53
|
+
"expertise_domains": "Return a JSON array of short strings (2-5 words each).",
|
|
54
|
+
"communication_avoid": (
|
|
55
|
+
"Return a JSON array of short phrases (2-6 words each) that the "
|
|
56
|
+
"profile would never say or write."
|
|
57
|
+
),
|
|
58
|
+
"key_quotes": (
|
|
59
|
+
"Return a JSON array of full sentences (8-25 words each), each "
|
|
60
|
+
"phrased as if the person said it. No attribution prefixes."
|
|
61
|
+
),
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
|
|
@@ -94,10 +113,8 @@ def _build_prompt(field: str, context: dict, count: int) -> str:
|
|
|
94
113
|
+ ", ".join(current)
|
|
95
114
|
+ "."
|
|
96
115
|
)
|
|
97
|
-
lines.append(
|
|
98
|
-
|
|
99
|
-
"No explanations, no numbering, no surrounding object."
|
|
100
|
-
)
|
|
116
|
+
lines.append(_FIELD_LENGTH_HINT[field])
|
|
117
|
+
lines.append("No explanations, no numbering, no surrounding object.")
|
|
101
118
|
return "\n".join(lines)
|
|
102
119
|
|
|
103
120
|
|
|
@@ -111,7 +111,8 @@ function csvToList(value: string): string[] {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// PR81 v2.99.0 — AI list-field suggester.
|
|
114
|
-
|
|
114
|
+
// PR82c v3.2.0 — extended with 'communication_avoid'.
|
|
115
|
+
type SuggestField = 'mental_models_primary' | 'frameworks' | 'expertise_domains' | 'communication_avoid'
|
|
115
116
|
const suggestingField = ref<SuggestField | null>(null)
|
|
116
117
|
|
|
117
118
|
async function suggest(field: SuggestField) {
|
|
@@ -122,7 +123,9 @@ async function suggest(field: SuggestField) {
|
|
|
122
123
|
? draft.value.mental_models.primary
|
|
123
124
|
: field === 'frameworks'
|
|
124
125
|
? draft.value.frameworks
|
|
125
|
-
:
|
|
126
|
+
: field === 'expertise_domains'
|
|
127
|
+
? draft.value.expertise_domains
|
|
128
|
+
: draft.value.communication.avoid
|
|
126
129
|
suggestingField.value = field
|
|
127
130
|
try {
|
|
128
131
|
const res = await $fetch<{
|
|
@@ -159,8 +162,10 @@ async function suggest(field: SuggestField) {
|
|
|
159
162
|
draft.value.mental_models.primary = merged
|
|
160
163
|
} else if (field === 'frameworks') {
|
|
161
164
|
draft.value.frameworks = merged
|
|
162
|
-
} else {
|
|
165
|
+
} else if (field === 'expertise_domains') {
|
|
163
166
|
draft.value.expertise_domains = merged
|
|
167
|
+
} else {
|
|
168
|
+
draft.value.communication.avoid = merged
|
|
164
169
|
}
|
|
165
170
|
markDirty()
|
|
166
171
|
toast.add({
|
|
@@ -437,6 +442,18 @@ const vocabOptions = [
|
|
|
437
442
|
</UFormField>
|
|
438
443
|
</div>
|
|
439
444
|
<UFormField label="Avoid (phrases)" help="comma-separated">
|
|
445
|
+
<template #hint>
|
|
446
|
+
<UButton
|
|
447
|
+
label="Suggest with AI"
|
|
448
|
+
icon="i-lucide-sparkles"
|
|
449
|
+
size="xs"
|
|
450
|
+
color="primary"
|
|
451
|
+
variant="soft"
|
|
452
|
+
:loading="suggestingField === 'communication_avoid'"
|
|
453
|
+
:disabled="suggestingField !== null"
|
|
454
|
+
@click="suggest('communication_avoid')"
|
|
455
|
+
/>
|
|
456
|
+
</template>
|
|
440
457
|
<UInput
|
|
441
458
|
:model-value="listToCsv(draft.communication.avoid)"
|
|
442
459
|
@update:model-value="(v: string) => { if (draft) { draft.communication.avoid = csvToList(v); markDirty() } }"
|
|
@@ -88,6 +88,83 @@ const draft = ref<AgentDraft>({
|
|
|
88
88
|
|
|
89
89
|
const saving = ref(false)
|
|
90
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
|
+
|
|
91
168
|
const departmentOptions = [
|
|
92
169
|
'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
|
|
93
170
|
'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
|
|
@@ -125,7 +202,8 @@ function csvToList(value: string): string[] {
|
|
|
125
202
|
}
|
|
126
203
|
|
|
127
204
|
// PR81 suggest wiring — three list fields.
|
|
128
|
-
|
|
205
|
+
// PR82c v3.2.0 — extended with 'communication_avoid'.
|
|
206
|
+
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid'
|
|
129
207
|
const suggestingField = ref<SuggestField | null>(null)
|
|
130
208
|
|
|
131
209
|
async function suggest(field: SuggestField) {
|
|
@@ -134,7 +212,9 @@ async function suggest(field: SuggestField) {
|
|
|
134
212
|
? draft.value.mental_models_primary
|
|
135
213
|
: field === 'frameworks'
|
|
136
214
|
? draft.value.frameworks
|
|
137
|
-
:
|
|
215
|
+
: field === 'expertise_domains'
|
|
216
|
+
? draft.value.expertise_domains
|
|
217
|
+
: draft.value.comm_avoid
|
|
138
218
|
if (!draft.value.name.trim() || !draft.value.role.trim()) {
|
|
139
219
|
toast.add({
|
|
140
220
|
title: 'Add a name and role first',
|
|
@@ -173,7 +253,8 @@ async function suggest(field: SuggestField) {
|
|
|
173
253
|
const merged = [...current, ...additions]
|
|
174
254
|
if (field === 'mental_models') draft.value.mental_models_primary = merged
|
|
175
255
|
else if (field === 'frameworks') draft.value.frameworks = merged
|
|
176
|
-
else draft.value.expertise_domains = merged
|
|
256
|
+
else if (field === 'expertise_domains') draft.value.expertise_domains = merged
|
|
257
|
+
else draft.value.comm_avoid = merged
|
|
177
258
|
toast.add({
|
|
178
259
|
title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
|
|
179
260
|
description: `via ${res.provider_name}`,
|
|
@@ -298,6 +379,40 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
|
|
|
298
379
|
|
|
299
380
|
<template #body>
|
|
300
381
|
<div class="max-w-4xl mx-auto py-2 space-y-6">
|
|
382
|
+
<section class="rounded-xl border border-primary/30 bg-primary/5 p-4 space-y-3">
|
|
383
|
+
<div class="flex items-center justify-between gap-3">
|
|
384
|
+
<div>
|
|
385
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-primary flex items-center gap-2">
|
|
386
|
+
<UIcon name="i-lucide-sparkles" class="size-4" />
|
|
387
|
+
Draft with AI
|
|
388
|
+
</h3>
|
|
389
|
+
<p class="text-xs text-muted mt-0.5">
|
|
390
|
+
Describe the agent in plain text — the LLM fills the whole form below.
|
|
391
|
+
You can still edit everything before saving.
|
|
392
|
+
</p>
|
|
393
|
+
</div>
|
|
394
|
+
<UButton
|
|
395
|
+
label="Generate draft"
|
|
396
|
+
icon="i-lucide-wand"
|
|
397
|
+
color="primary"
|
|
398
|
+
:loading="drafting"
|
|
399
|
+
:disabled="description.trim().length < 20"
|
|
400
|
+
@click="draftFromDescription"
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
<UTextarea
|
|
404
|
+
v-model="description"
|
|
405
|
+
placeholder="A senior strategist who decides fast, demands evidence, and is allergic to fluff. Spent 10 years at McKinsey covering CPG..."
|
|
406
|
+
:rows="3"
|
|
407
|
+
class="w-full"
|
|
408
|
+
/>
|
|
409
|
+
<p class="text-xs text-muted">
|
|
410
|
+
Tip: fill in <span class="font-mono">Name</span> /
|
|
411
|
+
<span class="font-mono">Role</span> /
|
|
412
|
+
<span class="font-mono">Department</span> below first for more precise output.
|
|
413
|
+
</p>
|
|
414
|
+
</section>
|
|
415
|
+
|
|
301
416
|
<section class="space-y-3">
|
|
302
417
|
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Identity</h3>
|
|
303
418
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
@@ -442,6 +557,18 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
|
|
|
442
557
|
</UFormField>
|
|
443
558
|
</div>
|
|
444
559
|
<UFormField label="Avoid (phrases)" help="comma-separated">
|
|
560
|
+
<template #hint>
|
|
561
|
+
<UButton
|
|
562
|
+
label="Suggest with AI"
|
|
563
|
+
icon="i-lucide-sparkles"
|
|
564
|
+
size="xs"
|
|
565
|
+
color="primary"
|
|
566
|
+
variant="soft"
|
|
567
|
+
:loading="suggestingField === 'communication_avoid'"
|
|
568
|
+
:disabled="suggestingField !== null"
|
|
569
|
+
@click="suggest('communication_avoid')"
|
|
570
|
+
/>
|
|
571
|
+
</template>
|
|
445
572
|
<UInput
|
|
446
573
|
:model-value="listToCsv(draft.comm_avoid)"
|
|
447
574
|
@update:model-value="(v: string) => { draft.comm_avoid = csvToList(v) }"
|
|
@@ -222,12 +222,16 @@ function csvToList(value: string): string[] {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// PR81 v2.99.0 — AI list-field suggester for personas.
|
|
225
|
-
|
|
225
|
+
// PR82c v3.2.0 — extended with 'communication_avoid' and 'key_quotes'.
|
|
226
|
+
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid' | 'key_quotes'
|
|
226
227
|
const suggestingField = ref<SuggestField | null>(null)
|
|
227
228
|
|
|
228
229
|
async function suggest(field: SuggestField) {
|
|
229
230
|
if (!draft.value || !detail.value) return
|
|
230
|
-
const current
|
|
231
|
+
const current
|
|
232
|
+
= field === 'communication_avoid'
|
|
233
|
+
? (draft.value.communication.avoid ?? [])
|
|
234
|
+
: ((draft.value as any)[field] as string[] ?? [])
|
|
231
235
|
suggestingField.value = field
|
|
232
236
|
try {
|
|
233
237
|
const res = await $fetch<{
|
|
@@ -258,7 +262,12 @@ async function suggest(field: SuggestField) {
|
|
|
258
262
|
})
|
|
259
263
|
return
|
|
260
264
|
}
|
|
261
|
-
|
|
265
|
+
const merged = [...current, ...additions]
|
|
266
|
+
if (field === 'communication_avoid') {
|
|
267
|
+
draft.value.communication.avoid = merged
|
|
268
|
+
} else {
|
|
269
|
+
;(draft.value as any)[field] = merged
|
|
270
|
+
}
|
|
262
271
|
markDirty()
|
|
263
272
|
toast.add({
|
|
264
273
|
title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
|
|
@@ -751,6 +760,52 @@ const vocabOptions = [
|
|
|
751
760
|
<USelect v-model="draft.communication.vocabulary_level" :items="vocabOptions" class="w-full" @update:model-value="markDirty" />
|
|
752
761
|
</UFormField>
|
|
753
762
|
</div>
|
|
763
|
+
<UFormField label="Avoid (phrases)" help="comma-separated">
|
|
764
|
+
<template #hint>
|
|
765
|
+
<UButton
|
|
766
|
+
label="Suggest with AI"
|
|
767
|
+
icon="i-lucide-sparkles"
|
|
768
|
+
size="xs"
|
|
769
|
+
color="primary"
|
|
770
|
+
variant="soft"
|
|
771
|
+
:loading="suggestingField === 'communication_avoid'"
|
|
772
|
+
:disabled="suggestingField !== null"
|
|
773
|
+
@click="suggest('communication_avoid')"
|
|
774
|
+
/>
|
|
775
|
+
</template>
|
|
776
|
+
<UInput
|
|
777
|
+
:model-value="listToCsv(draft.communication.avoid)"
|
|
778
|
+
@update:model-value="(v: string) => { if (draft) { draft.communication.avoid = csvToList(v); markDirty() } }"
|
|
779
|
+
class="w-full"
|
|
780
|
+
/>
|
|
781
|
+
</UFormField>
|
|
782
|
+
</section>
|
|
783
|
+
|
|
784
|
+
<section class="space-y-3">
|
|
785
|
+
<div class="flex items-center justify-between">
|
|
786
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Key quotes</h3>
|
|
787
|
+
<UButton
|
|
788
|
+
label="Suggest with AI"
|
|
789
|
+
icon="i-lucide-sparkles"
|
|
790
|
+
size="xs"
|
|
791
|
+
color="primary"
|
|
792
|
+
variant="soft"
|
|
793
|
+
:loading="suggestingField === 'key_quotes'"
|
|
794
|
+
:disabled="suggestingField !== null"
|
|
795
|
+
@click="suggest('key_quotes')"
|
|
796
|
+
/>
|
|
797
|
+
</div>
|
|
798
|
+
<UTextarea
|
|
799
|
+
:model-value="(draft.key_quotes ?? []).join('\n')"
|
|
800
|
+
:rows="4"
|
|
801
|
+
placeholder="One quote per line. Verbatim or paraphrased."
|
|
802
|
+
@update:model-value="(v: string) => { if (draft) { draft.key_quotes = v.split('\n').map((q) => q.trim()).filter(Boolean); markDirty() } }"
|
|
803
|
+
class="w-full"
|
|
804
|
+
/>
|
|
805
|
+
<p class="text-xs text-muted">
|
|
806
|
+
{{ (draft.key_quotes ?? []).length }} quote{{ (draft.key_quotes ?? []).length === 1 ? '' : 's' }}.
|
|
807
|
+
One per line.
|
|
808
|
+
</p>
|
|
754
809
|
</section>
|
|
755
810
|
</div>
|
|
756
811
|
|
|
@@ -209,9 +209,11 @@ export interface Persona {
|
|
|
209
209
|
mental_models: string[]
|
|
210
210
|
expertise_domains: string[]
|
|
211
211
|
frameworks: string[]
|
|
212
|
+
key_quotes?: string[]
|
|
212
213
|
communication: {
|
|
213
214
|
tone: string
|
|
214
215
|
vocabulary_level: string
|
|
216
|
+
avoid?: string[]
|
|
215
217
|
}
|
|
216
218
|
cloned_to_agents: string[]
|
|
217
219
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1827,6 +1827,42 @@ def _build_agent_yaml(
|
|
|
1827
1827
|
return payload
|
|
1828
1828
|
|
|
1829
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
|
+
|
|
1830
1866
|
# --- AI list-field suggester (PR81 v2.99.0) ---
|
|
1831
1867
|
|
|
1832
1868
|
@app.post("/api/agents/suggest")
|