m3d-core / m3d /modules /intent_planner.py
jkorstad's picture
std/intent-planner agent module
5965257 verified
Raw History Blame Contribute Delete
5.16 kB
"""std/intent-planner — the agent-class module (L5 intent-driven editing).
Takes a natural-language intent + an asset manifest (.asset.json) and emits an
executable edit DAG scoped to the asset's modalities. The planner is pluggable:
rule_based_planner is the deterministic default (and what tests use); a real LLM
(Hermes harness, any open model) plugs in as llm(ctx) -> plan-dict without any
other change. Emitted plans run directly through the standard DAG runner —
including human-gate nodes for the steps the user hand-edits in Blender etc.
"""
from __future__ import annotations
import json
import time
from m3d.module import M3DModule
from m3d.store import Store
MANIFEST = {
"id": "std/intent-planner",
"version": "0.1.0",
"class": "agent",
"modalities": ["intent"],
"runtime": {"type": "local-process", "ref": "python:m3d.modules.intent_planner"},
"interface": {"tools": ["plan"]},
"io": {
"inputs": [{"name": "intent", "kind": "text"}, {"name": "manifest_uri", "kind": "uri:asset-manifest"}],
"outputs": [{"name": "plan", "kind": "workflow"}],
},
"license": {"code": "MIT", "model_licenses": [], "usage_profiles": ["personal", "commercial", "redistribution"]},
"health": {"probe": "in-process", "fallbacks": []},
}
# ---------------------------------------------------------------- default LLM
def rule_based_planner(ctx: dict) -> dict:
"""Deterministic intent->edit-pathway planner. Maps intent keywords against
the manifest's parts; missing-modality intents raise instead of inventing nodes.
Swap for a real open LLM call without changing the contract: llm(ctx) -> plan."""
intent: str = ctx["intent"].lower()
parts = {p["role"]: p["uri"] for p in ctx.get("manifest", {}).get("parts", [])}
nodes, edges = [], []
producers = []
wants_texture = any(w in intent for w in ("texture", "color", "colour", "material", "paint", "albedo", "tatter"))
wants_mesh = any(w in intent for w in ("sculpt", "reshape", "tatter", "mesh", "geometry", "holes"))
wants_audio = any(w in intent for w in ("sound", "audio", "footstep", "sfx", "music", "voice", "heavier"))
if wants_texture:
if "texture/albedo" not in parts:
raise ValueError("intent needs a texture edit but asset manifest has no texture/albedo part")
nodes.append({
"id": "tex",
"module": "std/mock-texture",
"tool": "repaint",
"inputs": {"uri": parts["texture/albedo"], "style": "tattered" if "tatter" in intent else "repaint"},
})
producers.append("tex")
if wants_mesh:
if "mesh" not in parts:
raise ValueError("intent needs a mesh edit but asset manifest has no mesh part")
nodes.append({
"id": "mesh-edit",
"type": "human-gate",
"offer": ["approve", "edit-externally", "regenerate"],
"recipe": {"editor": "blender", "hint": f"edit {parts['mesh']} per intent"},
})
producers.append("mesh-edit")
if wants_audio:
if "audio" not in parts:
raise ValueError("intent needs an audio edit but asset manifest has no audio part")
nodes.append({
"id": "sfx",
"module": "std/mock-sfx",
"tool": "process",
"inputs": {"uri": parts["audio"], "intensity": 1.5},
})
producers.append("sfx")
if not producers:
raise ValueError(f"no modality matched intent: {intent!r}")
nodes.append({"id": "review", "type": "human-gate", "offer": ["approve", "edit-externally", "regenerate"]})
nodes.append({
"id": "post",
"module": "std/gltf-mcp",
"tool": "inspect",
"inputs": {"uri": parts.get("mesh", "asset://missing")},
})
for p in producers:
edges.append(f"{p}->review")
edges.append("review[approve]->post")
return {
"id": f"intent-{int(time.time())}",
"version": "0.1.0",
"license": "CC-BY-4.0",
"nodes": nodes,
"edges": edges,
"report": {
"planner": "rule_based_planner",
"modalities_touched": [p.rsplit("-", 1)[0] for p in producers],
"untouched": [r for r in parts if r not in ("texture/albedo", "mesh", "audio")],
},
}
# ---------------------------------------------------------------- module
def build_module(store: Store, llm=rule_based_planner) -> M3DModule:
m = M3DModule(MANIFEST)
@m.tool()
def plan(intent: str, manifest_uri: str) -> dict:
"""Turn a natural-language intent + asset manifest into an executable edit DAG."""
path = store.resolve(manifest_uri)
manifest = json.loads(path.read_text())
ctx = {"intent": intent, "manifest": manifest}
plan_wf = llm(ctx)
if not isinstance(plan_wf, dict) or "nodes" not in plan_wf or "edges" not in plan_wf:
return {"ok": False, "error": f"planner returned non-workflow: {plan_wf!r}"}
return {
"ok": True,
"plan": plan_wf,
"report": plan_wf.get("report", {}),
}
return m