@@ -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+
109180def 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