arkaos 3.0.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 CHANGED
@@ -1 +1 @@
1
- 3.0.0
1
+ 3.1.0
@@ -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}")
@@ -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',
@@ -298,6 +375,40 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
298
375
 
299
376
  <template #body>
300
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
+
301
412
  <section class="space-y-3">
302
413
  <h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Identity</h3>
303
414
  <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.0.0"
3
+ version = "3.1.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -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")