arkaos 3.1.0 → 3.3.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/field_suggester.py +21 -4
- package/core/personas/__pycache__/description_drafter.cpython-313.pyc +0 -0
- package/core/personas/description_drafter.py +111 -0
- package/dashboard/app/components/AgentEditDrawer.vue +20 -3
- package/dashboard/app/components/PersonaWizard.vue +102 -10
- package/dashboard/app/pages/agents/new.vue +19 -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 +37 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.0
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
|
Binary file
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""AI-powered persona draft from a free-text description (PR83a v3.3.0).
|
|
2
|
+
|
|
3
|
+
Sibling to `core/personas/builder.PersonaBuilder` but does NOT require
|
|
4
|
+
indexed content. Useful when:
|
|
5
|
+
|
|
6
|
+
- The operator wants to model a persona quickly without ingesting sources
|
|
7
|
+
- A YouTuber / author isn't yet in the knowledge base
|
|
8
|
+
- The persona is a synthetic archetype rather than a real person
|
|
9
|
+
|
|
10
|
+
Reuses the same JSON schema and parsing as the vector-driven builder so
|
|
11
|
+
the resulting Persona is interchangeable.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import uuid
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
|
|
20
|
+
from core.personas.builder import (
|
|
21
|
+
_PERSONA_SYSTEM_PROMPT,
|
|
22
|
+
_extract_json_object,
|
|
23
|
+
)
|
|
24
|
+
from core.personas.schema import (
|
|
25
|
+
Persona,
|
|
26
|
+
PersonaBigFive,
|
|
27
|
+
PersonaCommunication,
|
|
28
|
+
PersonaDISC,
|
|
29
|
+
PersonaEnneagram,
|
|
30
|
+
)
|
|
31
|
+
from core.runtime.llm_provider import LLMProvider, LLMUnavailable, get_llm_provider
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_DESCRIPTION_MIN_CHARS = 20
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PersonaDraftError(RuntimeError):
|
|
38
|
+
"""LLM produced unusable output or could not be reached."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class PersonaDraftResult:
|
|
43
|
+
persona: Persona
|
|
44
|
+
provider_name: str
|
|
45
|
+
raw_response: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def draft_persona_from_description(
|
|
49
|
+
description: str,
|
|
50
|
+
*,
|
|
51
|
+
name: str,
|
|
52
|
+
source_label: str = "",
|
|
53
|
+
provider: LLMProvider | None = None,
|
|
54
|
+
) -> PersonaDraftResult:
|
|
55
|
+
"""Generate a Persona draft from a free-text description."""
|
|
56
|
+
description = (description or "").strip()
|
|
57
|
+
if not name or not name.strip():
|
|
58
|
+
raise PersonaDraftError("name must not be empty")
|
|
59
|
+
if len(description) < _DESCRIPTION_MIN_CHARS:
|
|
60
|
+
raise PersonaDraftError(
|
|
61
|
+
f"description must be at least {_DESCRIPTION_MIN_CHARS} characters"
|
|
62
|
+
)
|
|
63
|
+
llm = provider or get_llm_provider()
|
|
64
|
+
prompt = _build_prompt(name.strip(), description)
|
|
65
|
+
try:
|
|
66
|
+
resp = llm.complete(prompt, max_tokens=3000, system=_PERSONA_SYSTEM_PROMPT)
|
|
67
|
+
except LLMUnavailable as exc:
|
|
68
|
+
raise PersonaDraftError(str(exc)) from exc
|
|
69
|
+
persona = _parse(name.strip(), source_label.strip() or name.strip(), resp.text)
|
|
70
|
+
return PersonaDraftResult(
|
|
71
|
+
persona=persona, provider_name=llm.name(), raw_response=resp.text,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build_prompt(name: str, description: str) -> str:
|
|
76
|
+
return (
|
|
77
|
+
f"Person: {name}\n\n"
|
|
78
|
+
f"Description provided by the operator:\n{description}\n\n"
|
|
79
|
+
"Build the persona purely from the description above. If a field is "
|
|
80
|
+
"not implied, choose the closest neutral default rather than "
|
|
81
|
+
"fabricating. NEVER invent quotes — leave key_quotes empty if no "
|
|
82
|
+
"quotes are present in the description."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse(name: str, source_label: str, raw: str) -> Persona:
|
|
87
|
+
data = _extract_json_object(raw)
|
|
88
|
+
if data is None:
|
|
89
|
+
raise PersonaDraftError(
|
|
90
|
+
f"LLM did not return a JSON object; raw response: {raw[:200]!r}"
|
|
91
|
+
)
|
|
92
|
+
try:
|
|
93
|
+
return Persona(
|
|
94
|
+
id=str(uuid.uuid4()),
|
|
95
|
+
name=name,
|
|
96
|
+
title=str(data.get("title") or ""),
|
|
97
|
+
tagline=str(data.get("tagline") or ""),
|
|
98
|
+
source=source_label,
|
|
99
|
+
disc=PersonaDISC(**(data.get("disc") or {})),
|
|
100
|
+
enneagram=PersonaEnneagram(**(data.get("enneagram") or {})),
|
|
101
|
+
big_five=PersonaBigFive(**(data.get("big_five") or {})),
|
|
102
|
+
mbti=str(data.get("mbti") or "").upper() or "INTJ",
|
|
103
|
+
mental_models=[str(x) for x in (data.get("mental_models") or [])],
|
|
104
|
+
expertise_domains=[str(x) for x in (data.get("expertise_domains") or [])],
|
|
105
|
+
frameworks=[str(x) for x in (data.get("frameworks") or [])],
|
|
106
|
+
key_quotes=[str(x) for x in (data.get("key_quotes") or [])],
|
|
107
|
+
communication=PersonaCommunication(**(data.get("communication") or {})),
|
|
108
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
109
|
+
)
|
|
110
|
+
except (TypeError, ValueError) as exc:
|
|
111
|
+
raise PersonaDraftError(f"persona schema mismatch: {exc}") from exc
|
|
@@ -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() } }"
|
|
@@ -37,6 +37,12 @@ const sourceLineCount = computed(() =>
|
|
|
37
37
|
.length,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
+
// PR83a v3.3.0 — Mode 3: build from a free-text description (no chunks).
|
|
41
|
+
type Mode = 'sources' | 'existing' | 'description'
|
|
42
|
+
const mode = ref<Mode>('sources')
|
|
43
|
+
const description = ref('')
|
|
44
|
+
const descriptionLength = computed(() => description.value.trim().length)
|
|
45
|
+
|
|
40
46
|
// ─── Step 2 state ────────────────────────────────────────────────────────
|
|
41
47
|
const ingestJobs = ref<Array<{
|
|
42
48
|
source: string
|
|
@@ -81,7 +87,14 @@ const tierOptions = [
|
|
|
81
87
|
|
|
82
88
|
|
|
83
89
|
async function startIngest() {
|
|
84
|
-
if (
|
|
90
|
+
if (mode.value === 'description') {
|
|
91
|
+
// PR83a — no ingest, no chunks. Build directly from description.
|
|
92
|
+
if (descriptionLength.value < 20 || !name.value.trim()) return
|
|
93
|
+
step.value = 3
|
|
94
|
+
await runDescriptionBuild()
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (mode.value === 'existing' || skipIngest.value) {
|
|
85
98
|
// Jump straight to step 3 — operator says content is already indexed.
|
|
86
99
|
step.value = 3
|
|
87
100
|
await runBuild()
|
|
@@ -166,6 +179,36 @@ onBeforeUnmount(() => {
|
|
|
166
179
|
// ─── Step 3: build the persona draft ────────────────────────────────────
|
|
167
180
|
|
|
168
181
|
|
|
182
|
+
async function runDescriptionBuild() {
|
|
183
|
+
building.value = true
|
|
184
|
+
buildError.value = null
|
|
185
|
+
draft.value = null
|
|
186
|
+
try {
|
|
187
|
+
const res = await $fetch<{ persona: Persona, provider_name: string, error?: string }>(
|
|
188
|
+
`${apiBase}/api/personas/draft`,
|
|
189
|
+
{
|
|
190
|
+
method: 'POST',
|
|
191
|
+
body: {
|
|
192
|
+
name: name.value.trim(),
|
|
193
|
+
description: description.value.trim(),
|
|
194
|
+
source_label: sourceLabel.value.trim() || name.value.trim(),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
if ('error' in res && typeof (res as any).error === 'string') {
|
|
199
|
+
throw new Error((res as any).error)
|
|
200
|
+
}
|
|
201
|
+
draft.value = res.persona
|
|
202
|
+
chunksUsed.value = 0
|
|
203
|
+
step.value = 4
|
|
204
|
+
} catch (err) {
|
|
205
|
+
buildError.value = err instanceof Error ? err.message : 'unknown error'
|
|
206
|
+
} finally {
|
|
207
|
+
building.value = false
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
169
212
|
async function runBuild() {
|
|
170
213
|
building.value = true
|
|
171
214
|
buildError.value = null
|
|
@@ -308,7 +351,28 @@ function backToStep1() {
|
|
|
308
351
|
/>
|
|
309
352
|
</UFormField>
|
|
310
353
|
|
|
354
|
+
<UFormField label="How should we generate this persona?">
|
|
355
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
356
|
+
<button
|
|
357
|
+
v-for="m in ([
|
|
358
|
+
{ key: 'sources', title: 'Ingest sources', desc: 'YouTube, articles, PDFs — best fidelity' },
|
|
359
|
+
{ key: 'existing', title: 'Existing chunks', desc: 'Use what is already indexed' },
|
|
360
|
+
{ key: 'description', title: 'From description', desc: 'No sources — pure description' },
|
|
361
|
+
] as const)"
|
|
362
|
+
:key="m.key"
|
|
363
|
+
type="button"
|
|
364
|
+
class="text-left rounded-lg border p-3 transition-colors"
|
|
365
|
+
:class="mode === m.key ? 'border-primary bg-primary/5' : 'border-default hover:border-primary/40'"
|
|
366
|
+
@click="mode = m.key"
|
|
367
|
+
>
|
|
368
|
+
<p class="text-sm font-semibold">{{ m.title }}</p>
|
|
369
|
+
<p class="text-xs text-muted mt-1">{{ m.desc }}</p>
|
|
370
|
+
</button>
|
|
371
|
+
</div>
|
|
372
|
+
</UFormField>
|
|
373
|
+
|
|
311
374
|
<UFormField
|
|
375
|
+
v-if="mode === 'sources'"
|
|
312
376
|
label="Sources (one URL per line)"
|
|
313
377
|
help="YouTube videos, articles, PDFs, blog posts about this person. The builder will search the indexed chunks and synthesise their behavioural DNA. Up to 50 sources per batch."
|
|
314
378
|
>
|
|
@@ -317,28 +381,56 @@ function backToStep1() {
|
|
|
317
381
|
:rows="6"
|
|
318
382
|
placeholder="https://www.youtube.com/watch?v=... https://example.com/article https://example.com/paper.pdf"
|
|
319
383
|
class="w-full font-mono text-sm"
|
|
320
|
-
:disabled="skipIngest"
|
|
321
384
|
/>
|
|
322
385
|
</UFormField>
|
|
323
386
|
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
387
|
+
<UFormField
|
|
388
|
+
v-else-if="mode === 'description'"
|
|
389
|
+
label="Description"
|
|
390
|
+
help="Plain-text description of the person — their style, beliefs, what they do, how they talk. The LLM uses this verbatim. Minimum 20 characters."
|
|
391
|
+
>
|
|
392
|
+
<UTextarea
|
|
393
|
+
v-model="description"
|
|
394
|
+
:rows="6"
|
|
395
|
+
placeholder="A direct-response copywriter who treats offers as the only true growth lever. Punchy, allergic to fluff. Loves Hormozi-style hooks."
|
|
396
|
+
class="w-full"
|
|
329
397
|
/>
|
|
398
|
+
</UFormField>
|
|
399
|
+
|
|
400
|
+
<div
|
|
401
|
+
v-if="mode === 'sources'"
|
|
402
|
+
class="flex items-center justify-between text-xs text-muted"
|
|
403
|
+
>
|
|
404
|
+
<span>{{ sourceLineCount }} source{{ sourceLineCount === 1 ? '' : 's' }} detected</span>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<div v-else-if="mode === 'existing'" class="text-xs text-muted">
|
|
408
|
+
We will search the vector DB for chunks tagged with this name and synthesise from what we find. Make sure you've ingested content for this person first.
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div v-else-if="mode === 'description'" class="text-xs text-muted">
|
|
412
|
+
{{ descriptionLength }} character{{ descriptionLength === 1 ? '' : 's' }} ·
|
|
413
|
+
{{ descriptionLength >= 20 ? 'ready' : `${20 - descriptionLength} more needed` }}
|
|
330
414
|
</div>
|
|
331
415
|
|
|
332
416
|
<div class="flex justify-end gap-2 pt-4">
|
|
333
417
|
<UButton
|
|
334
|
-
:label="
|
|
418
|
+
:label="(
|
|
419
|
+
mode === 'sources' ? `Index ${sourceLineCount} source${sourceLineCount === 1 ? '' : 's'} & build`
|
|
420
|
+
: mode === 'existing' ? 'Generate from existing knowledge'
|
|
421
|
+
: 'Generate from description'
|
|
422
|
+
)"
|
|
335
423
|
icon="i-lucide-arrow-right"
|
|
336
|
-
:disabled="
|
|
424
|
+
:disabled="(
|
|
425
|
+
!name.trim()
|
|
426
|
+
|| (mode === 'sources' && (sourceLineCount === 0 || sourceLineCount > 50))
|
|
427
|
+
|| (mode === 'description' && descriptionLength < 20)
|
|
428
|
+
)"
|
|
337
429
|
size="md"
|
|
338
430
|
@click="startIngest"
|
|
339
431
|
/>
|
|
340
432
|
</div>
|
|
341
|
-
<p v-if="sourceLineCount > 50" class="text-xs text-red-400">
|
|
433
|
+
<p v-if="mode === 'sources' && sourceLineCount > 50" class="text-xs text-red-400">
|
|
342
434
|
Over the 50-source cap. Trim the list before continuing.
|
|
343
435
|
</p>
|
|
344
436
|
</div>
|
|
@@ -202,7 +202,8 @@ function csvToList(value: string): string[] {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
// PR81 suggest wiring — three list fields.
|
|
205
|
-
|
|
205
|
+
// PR82c v3.2.0 — extended with 'communication_avoid'.
|
|
206
|
+
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid'
|
|
206
207
|
const suggestingField = ref<SuggestField | null>(null)
|
|
207
208
|
|
|
208
209
|
async function suggest(field: SuggestField) {
|
|
@@ -211,7 +212,9 @@ async function suggest(field: SuggestField) {
|
|
|
211
212
|
? draft.value.mental_models_primary
|
|
212
213
|
: field === 'frameworks'
|
|
213
214
|
? draft.value.frameworks
|
|
214
|
-
:
|
|
215
|
+
: field === 'expertise_domains'
|
|
216
|
+
? draft.value.expertise_domains
|
|
217
|
+
: draft.value.comm_avoid
|
|
215
218
|
if (!draft.value.name.trim() || !draft.value.role.trim()) {
|
|
216
219
|
toast.add({
|
|
217
220
|
title: 'Add a name and role first',
|
|
@@ -250,7 +253,8 @@ async function suggest(field: SuggestField) {
|
|
|
250
253
|
const merged = [...current, ...additions]
|
|
251
254
|
if (field === 'mental_models') draft.value.mental_models_primary = merged
|
|
252
255
|
else if (field === 'frameworks') draft.value.frameworks = merged
|
|
253
|
-
else draft.value.expertise_domains = merged
|
|
256
|
+
else if (field === 'expertise_domains') draft.value.expertise_domains = merged
|
|
257
|
+
else draft.value.comm_avoid = merged
|
|
254
258
|
toast.add({
|
|
255
259
|
title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
|
|
256
260
|
description: `via ${res.provider_name}`,
|
|
@@ -553,6 +557,18 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
|
|
|
553
557
|
</UFormField>
|
|
554
558
|
</div>
|
|
555
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>
|
|
556
572
|
<UInput
|
|
557
573
|
:model-value="listToCsv(draft.comm_avoid)"
|
|
558
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,43 @@ def _build_agent_yaml(
|
|
|
1827
1827
|
return payload
|
|
1828
1828
|
|
|
1829
1829
|
|
|
1830
|
+
# --- AI persona draft from description (PR83a v3.3.0) ---
|
|
1831
|
+
|
|
1832
|
+
@app.post("/api/personas/draft")
|
|
1833
|
+
def personas_draft(body: dict):
|
|
1834
|
+
"""Generate a Persona draft from a free-text description (no vector DB).
|
|
1835
|
+
|
|
1836
|
+
Body: {
|
|
1837
|
+
"description": "...", # min 20 chars
|
|
1838
|
+
"name": "Alex Carter", # required
|
|
1839
|
+
"source_label": "..." # optional
|
|
1840
|
+
}
|
|
1841
|
+
Returns: {"persona": {...}, "provider_name": "..."}
|
|
1842
|
+
|
|
1843
|
+
Sibling to /api/personas/build (which requires indexed chunks). Useful
|
|
1844
|
+
when the operator wants a quick draft without ingesting sources first.
|
|
1845
|
+
The result is NOT saved — operator reviews + POSTs to /api/personas.
|
|
1846
|
+
"""
|
|
1847
|
+
from core.personas.description_drafter import (
|
|
1848
|
+
PersonaDraftError,
|
|
1849
|
+
draft_persona_from_description,
|
|
1850
|
+
)
|
|
1851
|
+
|
|
1852
|
+
description = (body.get("description") or "").strip()
|
|
1853
|
+
name = (body.get("name") or "").strip()
|
|
1854
|
+
source_label = (body.get("source_label") or "").strip()
|
|
1855
|
+
try:
|
|
1856
|
+
res = draft_persona_from_description(
|
|
1857
|
+
description, name=name, source_label=source_label,
|
|
1858
|
+
)
|
|
1859
|
+
except PersonaDraftError as exc:
|
|
1860
|
+
return {"error": str(exc)}
|
|
1861
|
+
return {
|
|
1862
|
+
"persona": res.persona.model_dump(),
|
|
1863
|
+
"provider_name": res.provider_name,
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
|
|
1830
1867
|
# --- AI agent draft from description (PR82b v3.1.0) ---
|
|
1831
1868
|
|
|
1832
1869
|
@app.post("/api/agents/draft")
|