arkaos 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/core/agents/__pycache__/string_suggester.cpython-313.pyc +0 -0
- package/core/agents/string_suggester.py +111 -0
- package/dashboard/app/components/AgentEditDrawer.vue +70 -0
- package/dashboard/app/pages/agents/[id].vue +75 -0
- package/dashboard/app/pages/agents/new.vue +74 -0
- package/dashboard/app/pages/personas/[id].vue +54 -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 +111 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.6.0
|
|
Binary file
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""AI-assisted single-string suggester (PR83c v3.5.0).
|
|
2
|
+
|
|
3
|
+
Sibling to `core/agents/field_suggester` but for fields that hold ONE
|
|
4
|
+
string value (not a list). Used by the ✨ Generate button next to
|
|
5
|
+
fields like `communication.tone`, `communication.preferred_format`,
|
|
6
|
+
and `communication.language`.
|
|
7
|
+
|
|
8
|
+
Where the list suggester APPENDS, this one REPLACES. The caller is
|
|
9
|
+
expected to swap the field's existing value with the result.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from core.runtime.llm_provider import LLMProvider, LLMUnavailable, get_llm_provider
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_VALID_FIELDS: tuple[str, ...] = ("tone", "preferred_format", "language")
|
|
21
|
+
|
|
22
|
+
_SYSTEM = (
|
|
23
|
+
"You suggest concise single-string values for behavioural agent and "
|
|
24
|
+
"persona profile fields. Return ONLY the value as plain text — no "
|
|
25
|
+
"JSON, no fences, no quotes, no explanations."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_FIELD_HINTS: dict[str, str] = {
|
|
29
|
+
"tone": (
|
|
30
|
+
"Return 2-4 adjectives separated by commas describing the "
|
|
31
|
+
"profile's voice (e.g. 'Direct, analytical, crisp'). "
|
|
32
|
+
"Max 60 characters."
|
|
33
|
+
),
|
|
34
|
+
"preferred_format": (
|
|
35
|
+
"Return a short noun phrase listing the formats this profile "
|
|
36
|
+
"prefers (e.g. 'Briefs, tables, ASCII diagrams'). Max 80 characters."
|
|
37
|
+
),
|
|
38
|
+
"language": (
|
|
39
|
+
"Return a comma-separated list of IETF language tags this profile "
|
|
40
|
+
"writes in (e.g. 'en' or 'en, pt-PT'). Max 20 characters."
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_MAX_LEN: dict[str, int] = {"tone": 60, "preferred_format": 80, "language": 20}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StringSuggestionError(RuntimeError):
|
|
48
|
+
"""LLM produced unusable output or could not be reached."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class StringSuggestionResult:
|
|
53
|
+
value: str
|
|
54
|
+
provider_name: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def suggest_string_field(
|
|
58
|
+
field: str,
|
|
59
|
+
context: dict,
|
|
60
|
+
*,
|
|
61
|
+
provider: LLMProvider | None = None,
|
|
62
|
+
) -> StringSuggestionResult:
|
|
63
|
+
"""Return an AI-suggested single-string value for `field`."""
|
|
64
|
+
if field not in _VALID_FIELDS:
|
|
65
|
+
raise StringSuggestionError(f"unknown field: {field!r}")
|
|
66
|
+
llm = provider or get_llm_provider()
|
|
67
|
+
prompt = _build_prompt(field, context)
|
|
68
|
+
try:
|
|
69
|
+
resp = llm.complete(prompt, max_tokens=200, system=_SYSTEM)
|
|
70
|
+
except LLMUnavailable as exc:
|
|
71
|
+
raise StringSuggestionError(str(exc)) from exc
|
|
72
|
+
value = _clean(resp.text, field)
|
|
73
|
+
if not value:
|
|
74
|
+
raise StringSuggestionError("LLM returned an empty value")
|
|
75
|
+
return StringSuggestionResult(value=value, provider_name=llm.name())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_prompt(field: str, context: dict) -> str:
|
|
79
|
+
name = (context.get("name") or "").strip() or "the entity"
|
|
80
|
+
role = (context.get("role") or context.get("title") or "").strip()
|
|
81
|
+
department = (context.get("department") or "").strip()
|
|
82
|
+
current = (context.get("current") or "").strip()
|
|
83
|
+
lines = [f"Suggest a {field.replace('_', ' ')} for {name}."]
|
|
84
|
+
if role:
|
|
85
|
+
lines.append(f"Role: {role}.")
|
|
86
|
+
if department:
|
|
87
|
+
lines.append(f"Department: {department}.")
|
|
88
|
+
if current:
|
|
89
|
+
lines.append(f"Current value (to be replaced, do not repeat verbatim): {current}.")
|
|
90
|
+
lines.append(_FIELD_HINTS[field])
|
|
91
|
+
return "\n".join(lines)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _clean(text: str, field: str) -> str:
|
|
95
|
+
"""Strip fences, quotes, leading bullets and trim to the field's max."""
|
|
96
|
+
cleaned = text.strip()
|
|
97
|
+
cleaned = re.sub(r"^```(?:\w+)?\s*|\s*```$", "", cleaned, flags=re.MULTILINE)
|
|
98
|
+
cleaned = cleaned.strip()
|
|
99
|
+
# Remove surrounding quotes the LLM may add.
|
|
100
|
+
if len(cleaned) >= 2 and cleaned[0] in '"\'`' and cleaned[-1] == cleaned[0]:
|
|
101
|
+
cleaned = cleaned[1:-1].strip()
|
|
102
|
+
# Drop a leading bullet/numbering.
|
|
103
|
+
cleaned = re.sub(r"^[-*•·]\s+", "", cleaned)
|
|
104
|
+
cleaned = re.sub(r"^\d+[.)]\s+", "", cleaned)
|
|
105
|
+
# Single-line: collapse internal whitespace, drop trailing punctuation.
|
|
106
|
+
cleaned = " ".join(cleaned.split())
|
|
107
|
+
cleaned = cleaned.rstrip(".")
|
|
108
|
+
max_len = _MAX_LEN[field]
|
|
109
|
+
if len(cleaned) > max_len:
|
|
110
|
+
cleaned = cleaned[:max_len].rstrip(", ").rstrip()
|
|
111
|
+
return cleaned
|
|
@@ -115,6 +115,52 @@ function csvToList(value: string): string[] {
|
|
|
115
115
|
type SuggestField = 'mental_models_primary' | 'frameworks' | 'expertise_domains' | 'communication_avoid'
|
|
116
116
|
const suggestingField = ref<SuggestField | null>(null)
|
|
117
117
|
|
|
118
|
+
// PR83c v3.5.0 — single-string suggester.
|
|
119
|
+
type StringField = 'tone' | 'preferred_format'
|
|
120
|
+
const suggestingString = ref<StringField | null>(null)
|
|
121
|
+
|
|
122
|
+
async function suggestString(field: StringField) {
|
|
123
|
+
if (!draft.value || !props.agent) return
|
|
124
|
+
const current
|
|
125
|
+
= field === 'tone' ? draft.value.communication.tone : draft.value.communication.preferred_format
|
|
126
|
+
suggestingString.value = field
|
|
127
|
+
try {
|
|
128
|
+
const res = await $fetch<{ value: string, provider_name: string, error?: string }>(
|
|
129
|
+
`${apiBase}/api/agents/suggest-string`,
|
|
130
|
+
{
|
|
131
|
+
method: 'POST',
|
|
132
|
+
body: {
|
|
133
|
+
field,
|
|
134
|
+
context: {
|
|
135
|
+
name: props.agent.name,
|
|
136
|
+
role: props.agent.role,
|
|
137
|
+
department: props.agent.department,
|
|
138
|
+
current,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
if (res.error) throw new Error(res.error)
|
|
144
|
+
if (field === 'tone') draft.value.communication.tone = res.value
|
|
145
|
+
else draft.value.communication.preferred_format = res.value
|
|
146
|
+
markDirty()
|
|
147
|
+
toast.add({
|
|
148
|
+
title: 'Generated',
|
|
149
|
+
description: `via ${res.provider_name}`,
|
|
150
|
+
color: 'success',
|
|
151
|
+
icon: 'i-lucide-sparkles',
|
|
152
|
+
})
|
|
153
|
+
} catch (err) {
|
|
154
|
+
toast.add({
|
|
155
|
+
title: 'Generate failed',
|
|
156
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
157
|
+
color: 'error',
|
|
158
|
+
})
|
|
159
|
+
} finally {
|
|
160
|
+
suggestingString.value = null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
118
164
|
async function suggest(field: SuggestField) {
|
|
119
165
|
if (!draft.value || !props.agent) return
|
|
120
166
|
const backendField = field === 'mental_models_primary' ? 'mental_models' : field
|
|
@@ -424,6 +470,18 @@ const vocabOptions = [
|
|
|
424
470
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Communication</h3>
|
|
425
471
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
426
472
|
<UFormField label="Tone">
|
|
473
|
+
<template #hint>
|
|
474
|
+
<UButton
|
|
475
|
+
label="Generate"
|
|
476
|
+
icon="i-lucide-sparkles"
|
|
477
|
+
size="xs"
|
|
478
|
+
color="primary"
|
|
479
|
+
variant="soft"
|
|
480
|
+
:loading="suggestingString === 'tone'"
|
|
481
|
+
:disabled="suggestingString !== null"
|
|
482
|
+
@click="suggestString('tone')"
|
|
483
|
+
/>
|
|
484
|
+
</template>
|
|
427
485
|
<UInput v-model="draft.communication.tone" class="w-full" @update:model-value="markDirty" />
|
|
428
486
|
</UFormField>
|
|
429
487
|
<UFormField label="Vocabulary level">
|
|
@@ -435,6 +493,18 @@ const vocabOptions = [
|
|
|
435
493
|
/>
|
|
436
494
|
</UFormField>
|
|
437
495
|
<UFormField label="Preferred format">
|
|
496
|
+
<template #hint>
|
|
497
|
+
<UButton
|
|
498
|
+
label="Generate"
|
|
499
|
+
icon="i-lucide-sparkles"
|
|
500
|
+
size="xs"
|
|
501
|
+
color="primary"
|
|
502
|
+
variant="soft"
|
|
503
|
+
:loading="suggestingString === 'preferred_format'"
|
|
504
|
+
:disabled="suggestingString !== null"
|
|
505
|
+
@click="suggestString('preferred_format')"
|
|
506
|
+
/>
|
|
507
|
+
</template>
|
|
438
508
|
<UInput v-model="draft.communication.preferred_format" class="w-full" @update:model-value="markDirty" />
|
|
439
509
|
</UFormField>
|
|
440
510
|
<UFormField label="Language">
|
|
@@ -27,6 +27,38 @@ const deptActivity = computed<ActivityRow | null>(() =>
|
|
|
27
27
|
(activityData.value?.by_department?.[agent.value?.department ?? ''] ?? null),
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
// PR83d v3.6.0 — activity strip (30d, dept-level + last_used + rank)
|
|
31
|
+
interface ActivityStrip {
|
|
32
|
+
period: string
|
|
33
|
+
department: string
|
|
34
|
+
calls: number
|
|
35
|
+
cost_usd: number | null
|
|
36
|
+
tokens_in: number
|
|
37
|
+
tokens_out: number
|
|
38
|
+
last_used: string | null
|
|
39
|
+
dept_rank: number | null
|
|
40
|
+
dept_count: number
|
|
41
|
+
}
|
|
42
|
+
const { data: activityStrip } = fetchApi<ActivityStrip>(
|
|
43
|
+
`/api/agents/${agentId}/activity-strip?period=month`,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
function formatRelative(iso: string | null): string {
|
|
47
|
+
if (!iso) return 'never'
|
|
48
|
+
const ts = Date.parse(iso)
|
|
49
|
+
if (Number.isNaN(ts)) return 'never'
|
|
50
|
+
const diff = Date.now() - ts
|
|
51
|
+
const minutes = Math.floor(diff / 60_000)
|
|
52
|
+
if (minutes < 1) return 'just now'
|
|
53
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
54
|
+
const hours = Math.floor(minutes / 60)
|
|
55
|
+
if (hours < 24) return `${hours}h ago`
|
|
56
|
+
const days = Math.floor(hours / 24)
|
|
57
|
+
if (days < 30) return `${days}d ago`
|
|
58
|
+
const months = Math.floor(days / 30)
|
|
59
|
+
return `${months}mo ago`
|
|
60
|
+
}
|
|
61
|
+
|
|
30
62
|
// PR76 — edit drawer state
|
|
31
63
|
const editOpen = ref(false)
|
|
32
64
|
|
|
@@ -291,6 +323,49 @@ function formatTokens(n: number): string {
|
|
|
291
323
|
</div>
|
|
292
324
|
</section>
|
|
293
325
|
|
|
326
|
+
<!-- ===== ACTIVITY STRIP (PR83d) ===== -->
|
|
327
|
+
<section
|
|
328
|
+
v-if="activityStrip"
|
|
329
|
+
class="rounded-xl border border-default bg-elevated/10 p-4"
|
|
330
|
+
>
|
|
331
|
+
<div class="flex flex-wrap items-center gap-x-6 gap-y-3 text-sm">
|
|
332
|
+
<div class="flex items-center gap-2">
|
|
333
|
+
<UIcon name="i-lucide-activity" class="size-4 text-primary" />
|
|
334
|
+
<span class="font-semibold uppercase tracking-wide text-muted text-xs">
|
|
335
|
+
30d activity (dept)
|
|
336
|
+
</span>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="flex items-center gap-2">
|
|
339
|
+
<span class="text-muted">Calls</span>
|
|
340
|
+
<span class="font-mono font-semibold">{{ activityStrip.calls }}</span>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="flex items-center gap-2">
|
|
343
|
+
<span class="text-muted">Cost</span>
|
|
344
|
+
<span class="font-mono font-semibold">{{ formatCost(activityStrip.cost_usd) }}</span>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="flex items-center gap-2">
|
|
347
|
+
<span class="text-muted">Tokens</span>
|
|
348
|
+
<span class="font-mono">
|
|
349
|
+
{{ formatTokens(activityStrip.tokens_in) }} /
|
|
350
|
+
{{ formatTokens(activityStrip.tokens_out) }}
|
|
351
|
+
</span>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="flex items-center gap-2">
|
|
354
|
+
<span class="text-muted">Last used</span>
|
|
355
|
+
<span class="font-mono">{{ formatRelative(activityStrip.last_used) }}</span>
|
|
356
|
+
</div>
|
|
357
|
+
<div v-if="activityStrip.dept_rank" class="flex items-center gap-2">
|
|
358
|
+
<span class="text-muted">Dept rank</span>
|
|
359
|
+
<UBadge
|
|
360
|
+
:label="`#${activityStrip.dept_rank} of ${activityStrip.dept_count}`"
|
|
361
|
+
:color="activityStrip.dept_rank <= 3 ? 'primary' : 'neutral'"
|
|
362
|
+
variant="subtle"
|
|
363
|
+
size="sm"
|
|
364
|
+
/>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</section>
|
|
368
|
+
|
|
294
369
|
<AgentEditDrawer
|
|
295
370
|
v-model="editOpen"
|
|
296
371
|
:agent="agent"
|
|
@@ -88,6 +88,56 @@ const draft = ref<AgentDraft>({
|
|
|
88
88
|
|
|
89
89
|
const saving = ref(false)
|
|
90
90
|
|
|
91
|
+
// PR83c v3.5.0 — single-string suggester.
|
|
92
|
+
type StringField = 'tone' | 'preferred_format'
|
|
93
|
+
const suggestingString = ref<StringField | null>(null)
|
|
94
|
+
|
|
95
|
+
async function suggestString(field: StringField) {
|
|
96
|
+
if (!draft.value.name.trim() || !draft.value.role.trim()) {
|
|
97
|
+
toast.add({
|
|
98
|
+
title: 'Add a name and role first',
|
|
99
|
+
color: 'warning',
|
|
100
|
+
})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const current = field === 'tone' ? draft.value.comm_tone : draft.value.comm_format
|
|
104
|
+
suggestingString.value = field
|
|
105
|
+
try {
|
|
106
|
+
const res = await $fetch<{ value: string, provider_name: string, error?: string }>(
|
|
107
|
+
`${apiBase}/api/agents/suggest-string`,
|
|
108
|
+
{
|
|
109
|
+
method: 'POST',
|
|
110
|
+
body: {
|
|
111
|
+
field,
|
|
112
|
+
context: {
|
|
113
|
+
name: draft.value.name,
|
|
114
|
+
role: draft.value.role,
|
|
115
|
+
department: draft.value.department,
|
|
116
|
+
current,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
if (res.error) throw new Error(res.error)
|
|
122
|
+
if (field === 'tone') draft.value.comm_tone = res.value
|
|
123
|
+
else draft.value.comm_format = res.value
|
|
124
|
+
toast.add({
|
|
125
|
+
title: 'Generated',
|
|
126
|
+
description: `via ${res.provider_name}`,
|
|
127
|
+
color: 'success',
|
|
128
|
+
icon: 'i-lucide-sparkles',
|
|
129
|
+
})
|
|
130
|
+
} catch (err) {
|
|
131
|
+
toast.add({
|
|
132
|
+
title: 'Generate failed',
|
|
133
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
134
|
+
color: 'error',
|
|
135
|
+
})
|
|
136
|
+
} finally {
|
|
137
|
+
suggestingString.value = null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
91
141
|
// PR82b v3.1.0 — AI draft from description.
|
|
92
142
|
const description = ref('')
|
|
93
143
|
const drafting = ref(false)
|
|
@@ -544,12 +594,36 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
|
|
|
544
594
|
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Communication</h3>
|
|
545
595
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
546
596
|
<UFormField label="Tone">
|
|
597
|
+
<template #hint>
|
|
598
|
+
<UButton
|
|
599
|
+
label="Generate"
|
|
600
|
+
icon="i-lucide-sparkles"
|
|
601
|
+
size="xs"
|
|
602
|
+
color="primary"
|
|
603
|
+
variant="soft"
|
|
604
|
+
:loading="suggestingString === 'tone'"
|
|
605
|
+
:disabled="suggestingString !== null"
|
|
606
|
+
@click="suggestString('tone')"
|
|
607
|
+
/>
|
|
608
|
+
</template>
|
|
547
609
|
<UInput v-model="draft.comm_tone" class="w-full" placeholder="Analytical, calm" />
|
|
548
610
|
</UFormField>
|
|
549
611
|
<UFormField label="Vocabulary level">
|
|
550
612
|
<USelect v-model="draft.comm_vocab" :items="vocabOptions" class="w-full" />
|
|
551
613
|
</UFormField>
|
|
552
614
|
<UFormField label="Preferred format">
|
|
615
|
+
<template #hint>
|
|
616
|
+
<UButton
|
|
617
|
+
label="Generate"
|
|
618
|
+
icon="i-lucide-sparkles"
|
|
619
|
+
size="xs"
|
|
620
|
+
color="primary"
|
|
621
|
+
variant="soft"
|
|
622
|
+
:loading="suggestingString === 'preferred_format'"
|
|
623
|
+
:disabled="suggestingString !== null"
|
|
624
|
+
@click="suggestString('preferred_format')"
|
|
625
|
+
/>
|
|
626
|
+
</template>
|
|
553
627
|
<UInput v-model="draft.comm_format" class="w-full" placeholder="Briefs, tables, charts" />
|
|
554
628
|
</UFormField>
|
|
555
629
|
<UFormField label="Language">
|
|
@@ -226,6 +226,48 @@ function csvToList(value: string): string[] {
|
|
|
226
226
|
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid' | 'key_quotes'
|
|
227
227
|
const suggestingField = ref<SuggestField | null>(null)
|
|
228
228
|
|
|
229
|
+
// PR83c v3.5.0 — single-string suggester (tone for personas).
|
|
230
|
+
const suggestingString = ref<'tone' | null>(null)
|
|
231
|
+
|
|
232
|
+
async function suggestString(field: 'tone') {
|
|
233
|
+
if (!draft.value || !detail.value) return
|
|
234
|
+
const current = draft.value.communication.tone
|
|
235
|
+
suggestingString.value = field
|
|
236
|
+
try {
|
|
237
|
+
const res = await $fetch<{ value: string, provider_name: string, error?: string }>(
|
|
238
|
+
`${apiBase}/api/personas/suggest-string`,
|
|
239
|
+
{
|
|
240
|
+
method: 'POST',
|
|
241
|
+
body: {
|
|
242
|
+
field,
|
|
243
|
+
context: {
|
|
244
|
+
name: detail.value.name,
|
|
245
|
+
title: detail.value.title,
|
|
246
|
+
current,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
if (res.error) throw new Error(res.error)
|
|
252
|
+
draft.value.communication.tone = res.value
|
|
253
|
+
markDirty()
|
|
254
|
+
toast.add({
|
|
255
|
+
title: 'Generated',
|
|
256
|
+
description: `via ${res.provider_name}`,
|
|
257
|
+
color: 'success',
|
|
258
|
+
icon: 'i-lucide-sparkles',
|
|
259
|
+
})
|
|
260
|
+
} catch (err) {
|
|
261
|
+
toast.add({
|
|
262
|
+
title: 'Generate failed',
|
|
263
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
264
|
+
color: 'error',
|
|
265
|
+
})
|
|
266
|
+
} finally {
|
|
267
|
+
suggestingString.value = null
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
229
271
|
async function suggest(field: SuggestField) {
|
|
230
272
|
if (!draft.value || !detail.value) return
|
|
231
273
|
const current
|
|
@@ -754,6 +796,18 @@ const vocabOptions = [
|
|
|
754
796
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Communication</h3>
|
|
755
797
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
756
798
|
<UFormField label="Tone">
|
|
799
|
+
<template #hint>
|
|
800
|
+
<UButton
|
|
801
|
+
label="Generate"
|
|
802
|
+
icon="i-lucide-sparkles"
|
|
803
|
+
size="xs"
|
|
804
|
+
color="primary"
|
|
805
|
+
variant="soft"
|
|
806
|
+
:loading="suggestingString === 'tone'"
|
|
807
|
+
:disabled="suggestingString !== null"
|
|
808
|
+
@click="suggestString('tone')"
|
|
809
|
+
/>
|
|
810
|
+
</template>
|
|
757
811
|
<UInput v-model="draft.communication.tone" class="w-full" @update:model-value="markDirty" />
|
|
758
812
|
</UFormField>
|
|
759
813
|
<UFormField label="Vocabulary level">
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -218,6 +218,90 @@ def agents_activity(period: str = "week"):
|
|
|
218
218
|
return {"by_department": out, "period": period}
|
|
219
219
|
|
|
220
220
|
|
|
221
|
+
@app.get("/api/agents/{agent_id}/activity-strip")
|
|
222
|
+
def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
223
|
+
"""PR83d v3.6.0 — compact activity payload for the agent hero strip.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
{
|
|
227
|
+
"period": "month",
|
|
228
|
+
"department": "<dept>",
|
|
229
|
+
"calls": <int>,
|
|
230
|
+
"cost_usd": <float|null>,
|
|
231
|
+
"tokens_in": <int>, "tokens_out": <int>,
|
|
232
|
+
"last_used": "<ISO ts>"|null,
|
|
233
|
+
"dept_rank": <1-based int>|null,
|
|
234
|
+
"dept_count": <int>
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
All values reflect the agent's DEPARTMENT (per-agent attribution
|
|
238
|
+
isn't tracked yet — see PR47 telemetry).
|
|
239
|
+
"""
|
|
240
|
+
agents = _load_agents()
|
|
241
|
+
base = None
|
|
242
|
+
for a in agents:
|
|
243
|
+
if a.get("id") == agent_id:
|
|
244
|
+
base = dict(a)
|
|
245
|
+
break
|
|
246
|
+
if not base:
|
|
247
|
+
return {"error": "Agent not found"}
|
|
248
|
+
dept = base.get("department") or ""
|
|
249
|
+
try:
|
|
250
|
+
from core.runtime.llm_cost_telemetry import (
|
|
251
|
+
VALID_PERIODS,
|
|
252
|
+
_load_slice,
|
|
253
|
+
_period_cutoff,
|
|
254
|
+
summarise,
|
|
255
|
+
)
|
|
256
|
+
except Exception:
|
|
257
|
+
return {"error": "telemetry unavailable"}
|
|
258
|
+
if period not in VALID_PERIODS:
|
|
259
|
+
period = "month"
|
|
260
|
+
|
|
261
|
+
summary = summarise(period=period)
|
|
262
|
+
dept_costs: list[tuple[str, float]] = []
|
|
263
|
+
target_row: dict | None = None
|
|
264
|
+
for category, row in (summary.by_category or {}).items():
|
|
265
|
+
if not isinstance(category, str) or not category.startswith("subagent:"):
|
|
266
|
+
continue
|
|
267
|
+
cat_dept = category.split(":", 1)[1] or "unknown"
|
|
268
|
+
cost = row.get("total_cost_usd")
|
|
269
|
+
dept_costs.append((cat_dept, float(cost) if isinstance(cost, (int, float)) else 0.0))
|
|
270
|
+
if cat_dept == dept:
|
|
271
|
+
target_row = row
|
|
272
|
+
|
|
273
|
+
dept_costs.sort(key=lambda t: t[1], reverse=True)
|
|
274
|
+
dept_rank: Optional[int] = None
|
|
275
|
+
for idx, (d, _) in enumerate(dept_costs, start=1):
|
|
276
|
+
if d == dept:
|
|
277
|
+
dept_rank = idx
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
entries, _ = _load_slice(None, _period_cutoff(period, now=None))
|
|
281
|
+
last_used: Optional[str] = None
|
|
282
|
+
for entry in reversed(entries):
|
|
283
|
+
cat = entry.get("category") or ""
|
|
284
|
+
if isinstance(cat, str) and cat == f"subagent:{dept}":
|
|
285
|
+
last_used = entry.get("ts")
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
"period": period,
|
|
290
|
+
"department": dept,
|
|
291
|
+
"calls": int(target_row.get("call_count", 0)) if target_row else 0,
|
|
292
|
+
"cost_usd": (
|
|
293
|
+
float(target_row.get("total_cost_usd"))
|
|
294
|
+
if target_row and isinstance(target_row.get("total_cost_usd"), (int, float))
|
|
295
|
+
else None
|
|
296
|
+
),
|
|
297
|
+
"tokens_in": int(target_row.get("total_tokens_in", 0)) if target_row else 0,
|
|
298
|
+
"tokens_out": int(target_row.get("total_tokens_out", 0)) if target_row else 0,
|
|
299
|
+
"last_used": last_used,
|
|
300
|
+
"dept_rank": dept_rank,
|
|
301
|
+
"dept_count": len(dept_costs),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
221
305
|
@app.get("/api/agents/{agent_id}")
|
|
222
306
|
def agent_detail(agent_id: str):
|
|
223
307
|
"""Get full agent detail including YAML data."""
|
|
@@ -1949,6 +2033,33 @@ def agents_draft(body: dict):
|
|
|
1949
2033
|
return {"draft": res.draft, "provider_name": res.provider_name}
|
|
1950
2034
|
|
|
1951
2035
|
|
|
2036
|
+
# --- AI single-string suggester (PR83c v3.5.0) ---
|
|
2037
|
+
|
|
2038
|
+
@app.post("/api/agents/suggest-string")
|
|
2039
|
+
def agents_suggest_string(body: dict):
|
|
2040
|
+
"""Suggest a single-string value (tone, preferred_format, language)."""
|
|
2041
|
+
return _do_string_suggest(body, source="agent")
|
|
2042
|
+
|
|
2043
|
+
|
|
2044
|
+
@app.post("/api/personas/suggest-string")
|
|
2045
|
+
def personas_suggest_string(body: dict):
|
|
2046
|
+
return _do_string_suggest(body, source="persona")
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def _do_string_suggest(body: dict, *, source: str) -> dict:
|
|
2050
|
+
from core.agents.string_suggester import (
|
|
2051
|
+
StringSuggestionError,
|
|
2052
|
+
suggest_string_field,
|
|
2053
|
+
)
|
|
2054
|
+
field = (body.get("field") or "").strip()
|
|
2055
|
+
context = body.get("context") or {}
|
|
2056
|
+
try:
|
|
2057
|
+
res = suggest_string_field(field, context)
|
|
2058
|
+
except StringSuggestionError as exc:
|
|
2059
|
+
return {"error": str(exc)}
|
|
2060
|
+
return {"value": res.value, "provider_name": res.provider_name, "source": source}
|
|
2061
|
+
|
|
2062
|
+
|
|
1952
2063
|
# --- AI list-field suggester (PR81 v2.99.0) ---
|
|
1953
2064
|
|
|
1954
2065
|
@app.post("/api/agents/suggest")
|