Skip to content

Commit 0d0307d

Browse files
tbitcsoz-agent
andcommitted
feat(skills): wire LLM agent runner for richer skill generation
When ANTHROPIC_API_KEY, OPENAI_API_KEY, or Ollama is configured, build_skill() now calls AgentRunner.run_task() with a structured JSON prompt to produce richer activation rules, input/output schemas, epistemic contract, tools, tests, and stop conditions. Falls back silently to the deterministic stub when: - No provider env var is set - AgentRunner import fails (SDK not installed) - LLM returns non-JSON or any exception is raised No new test-time dependencies; the stub path is unchanged. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent e8255f5 commit 0d0307d

1 file changed

Lines changed: 97 additions & 21 deletions

File tree

src/specsmith/skills_builder.py

Lines changed: 97 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,37 +106,113 @@ def _generate_skill_id(name: str) -> str:
106106
return f"skill-{slug}-{uuid.uuid4().hex[:6]}"
107107

108108

109+
_SKILL_PROMPT = """
110+
You are an expert agent-skill designer for the specsmith AEE framework.
111+
Given a description, produce a JSON object (no markdown fences) with these exact keys:
112+
name - short title (≤ 60 chars)
113+
purpose - one-sentence purpose
114+
activation_rules - list of 2-4 strings describing when to activate
115+
input_schema - dict of {field: type description}
116+
output_schema - dict of {field: type description}
117+
epistemic_contract - one sentence about verifiability guarantees
118+
tools_used - list of tool names (e.g. read_file, run_shell, run_tests)
119+
tests_required - list of 1-3 test descriptions
120+
stop_conditions - list of 2-3 stop conditions
121+
tags - list of keyword tags
122+
123+
Description: {description}
124+
125+
Respond with ONLY valid JSON. No explanation, no markdown.
126+
"""
127+
128+
129+
def _build_skill_with_llm(description: str, tags: list[str]) -> SkillSpec | None:
130+
"""Attempt to build a richer skill spec via the LLM provider.
131+
132+
Returns None if no provider is available or the call fails.
133+
"""
134+
import os
135+
136+
# Detect whether any provider is configured.
137+
has_anthropic = bool(os.environ.get("ANTHROPIC_API_KEY"))
138+
has_openai = bool(os.environ.get("OPENAI_API_KEY"))
139+
has_ollama = bool(os.environ.get("OLLAMA_HOST") or os.environ.get("SPECSMITH_PROVIDER") == "ollama")
140+
141+
if not (has_anthropic or has_openai or has_ollama):
142+
return None # No provider — skip LLM, fall through to stub
143+
144+
try:
145+
from specsmith.agent.runner import AgentRunner # type: ignore[import]
146+
147+
runner = AgentRunner(project_dir=".")
148+
prompt = _SKILL_PROMPT.format(description=description)
149+
raw = runner.run_task(prompt)
150+
if not raw:
151+
return None
152+
153+
# Strip accidental markdown fences
154+
raw = raw.strip()
155+
if raw.startswith("```"):
156+
raw = "\n".join(raw.splitlines()[1:])
157+
if raw.endswith("```"):
158+
raw = raw[: raw.rfind("```")]
159+
raw = raw.strip()
160+
161+
data = json.loads(raw)
162+
skill_id = _generate_skill_id(data.get("name", description))
163+
return SkillSpec(
164+
id=skill_id,
165+
name=data.get("name", description[:60]),
166+
purpose=data.get("purpose", description),
167+
activation_rules=data.get("activation_rules", []),
168+
input_schema=data.get("input_schema", {}),
169+
output_schema=data.get("output_schema", {}),
170+
epistemic_contract=data.get("epistemic_contract", ""),
171+
tools_used=data.get("tools_used", []),
172+
tests_required=data.get("tests_required", []),
173+
stop_conditions=data.get("stop_conditions", []),
174+
tags=tags + data.get("tags", []),
175+
)
176+
except Exception: # noqa: BLE001 — always fall back to stub
177+
return None
178+
179+
109180
def build_skill(
110181
description: str,
111182
project_dir: str = ".",
112183
tags: list[str] | None = None,
113184
) -> SkillSpec:
114185
"""Build a skill from a natural-language description.
115186
116-
Currently generates a deterministic skill spec from the description.
117-
When an AI provider is configured, this will use the LLM to generate
118-
richer skill content.
187+
When an AI provider is configured (ANTHROPIC_API_KEY, OPENAI_API_KEY,
188+
or Ollama), uses the LLM to generate a richer skill spec.
189+
Falls back to a deterministic stub when no provider is available or
190+
the LLM call fails.
119191
"""
120-
# Deterministic generation (no LLM required)
121-
name = description.strip()[:60]
122-
skill_id = _generate_skill_id(name)
123-
124-
spec = SkillSpec(
125-
id=skill_id,
126-
name=name,
127-
purpose=description.strip(),
128-
activation_rules=[f"User requests: {description[:80]}"],
129-
input_schema={"task": "string", "context": "string (optional)"},
130-
output_schema={"result": "string", "confidence": "number"},
131-
epistemic_contract="Output must be verifiable against the input task.",
132-
tools_used=["read_file", "run_shell"],
133-
tests_required=[f"Verify {name} produces correct output"],
134-
stop_conditions=["Confidence below 0.3", "Timeout exceeded"],
135-
tags=tags or [],
136-
)
192+
tags = tags or []
193+
194+
# Try LLM-enriched generation first.
195+
spec = _build_skill_with_llm(description, tags)
196+
197+
if spec is None:
198+
# Deterministic fallback — no LLM required.
199+
name = description.strip()[:60]
200+
spec = SkillSpec(
201+
id=_generate_skill_id(name),
202+
name=name,
203+
purpose=description.strip(),
204+
activation_rules=[f"User requests: {description[:80]}"],
205+
input_schema={"task": "string", "context": "string (optional)"},
206+
output_schema={"result": "string", "confidence": "number"},
207+
epistemic_contract="Output must be verifiable against the input task.",
208+
tools_used=["read_file", "run_shell"],
209+
tests_required=[f"Verify {name} produces correct output"],
210+
stop_conditions=["Confidence below 0.3", "Timeout exceeded"],
211+
tags=tags,
212+
)
137213

138214
# Save to disk
139-
skill_dir = Path(project_dir).resolve() / ".specsmith" / "skills" / skill_id
215+
skill_dir = Path(project_dir).resolve() / ".specsmith" / "skills" / spec.id
140216
skill_dir.mkdir(parents=True, exist_ok=True)
141217
(skill_dir / "SKILL.md").write_text(spec.to_markdown(), encoding="utf-8")
142218

0 commit comments

Comments
 (0)