Author runnable skill bodies via OpenAI-compatible codegen model
create_skill now writes a real predict/act body: the small decision model still authors title + description (engine.generate), then a larger OpenAI-compatible model (default qwen38-iq3s) writes the runnable code against the SKILL.md contract. Bodies persist to data/skills/<cat>/<name>.py, are hot-loaded via importlib, merged into the running tree, and the request re-dispatches to the new leaf. The dashboard decision-flow view shows the title/description with a writing badge while the body is being written. Codegen failure degrades to a navigable stub.
This commit is contained in:
@@ -12,10 +12,20 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from semif_agent.cli import build_scheduler, load_config
|
||||
from semif_agent.codegen import CodegenClient, generate_skill_body
|
||||
from semif_agent.decisions import Request
|
||||
from semif_agent.dream import dream
|
||||
from semif_agent.engine import EngineUnavailable
|
||||
from semif_agent.skills import CategoryDraft, SkillDraft, generate_category, generate_skill
|
||||
from semif_agent.skills import (
|
||||
CategoryDraft,
|
||||
SkillBodyStore,
|
||||
SkillDraft,
|
||||
build_skills,
|
||||
build_tree,
|
||||
generate_category,
|
||||
generate_skill,
|
||||
materialize_skill,
|
||||
)
|
||||
|
||||
|
||||
def require_real(config: dict):
|
||||
@@ -140,6 +150,38 @@ def test_generate_skill(tmp_path):
|
||||
assert draft.name and draft.description
|
||||
|
||||
|
||||
def test_generate_skill_body_codegen(tmp_path):
|
||||
"""A real OpenAI-compatible model writes a runnable skill body.
|
||||
|
||||
Slow: uses the big codegen model (qwen38-iq3s by default). Run this one in
|
||||
the background and poll — long-lived ssh sessions get SIGHUP'd.
|
||||
"""
|
||||
config = load_config()
|
||||
require_real(config)
|
||||
codegen_cfg = config.get("codegen", {})
|
||||
client = CodegenClient(
|
||||
base_url=codegen_cfg.get("base_url", "http://localhost:11434/v1"),
|
||||
model=codegen_cfg.get("model", "qwen38-iq3s"),
|
||||
timeout=float(codegen_cfg.get("timeout", 600.0)),
|
||||
)
|
||||
tree = build_tree(build_skills({"skills": {}}))
|
||||
draft = SkillDraft(
|
||||
name="check_service",
|
||||
description="Check whether a service is reachable.",
|
||||
)
|
||||
code = generate_skill_body(
|
||||
client,
|
||||
Request("is my home server reachable right now?"),
|
||||
"tracking",
|
||||
draft,
|
||||
tree,
|
||||
)
|
||||
print(f"generated {len(code)} bytes of skill body")
|
||||
store = SkillBodyStore(str(tmp_path / "skills"))
|
||||
skill = materialize_skill(draft, "tracking", store)
|
||||
assert callable(skill.predict) and callable(skill.act)
|
||||
|
||||
|
||||
def test_create_skill_empty_category_does_not_wedge(tmp_path):
|
||||
"""A dispatch that lands on an empty category must not leave the scheduler wedged.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user