Parse an LLM-generated plan string into a Plan object.
(raw: str)
| 57 | |
| 58 | |
| 59 | def parse_plan(raw: str) -> Plan: |
| 60 | """Parse an LLM-generated plan string into a Plan object.""" |
| 61 | tasks = {int(m.group(1)): m.group(2).strip() for m in _TASK_RE.finditer(raw)} |
| 62 | servers = {int(m.group(1)): m.group(2).strip() for m in _SERVER_RE.finditer(raw)} |
| 63 | # Strip any trailing signature the LLM may copy from the server description |
| 64 | # format "tool_name(param: type)" — only the bare name is needed. |
| 65 | tools = { |
| 66 | int(m.group(1)): m.group(2).strip().split("(")[0].strip() |
| 67 | for m in _TOOL_RE.finditer(raw) |
| 68 | } |
| 69 | deps_raw = {int(m.group(1)): m.group(2).strip() for m in _DEP_RE.finditer(raw)} |
| 70 | outputs = {int(m.group(1)): m.group(2).strip() for m in _OUTPUT_RE.finditer(raw)} |
| 71 | |
| 72 | steps = [] |
| 73 | for n in sorted(tasks): |
| 74 | raw_dep = deps_raw.get(n, "None").strip() |
| 75 | |
| 76 | if raw_dep.lower() == "none": |
| 77 | dependencies = [] |
| 78 | else: |
| 79 | dependencies = [int(x) for x in _DEP_NUM_RE.findall(raw_dep)] |
| 80 | |
| 81 | # Make sure dependency references only point to earlier valid steps. |
| 82 | if not dependencies: |
| 83 | raise ValueError(f"Invalid dependency format for step {n}: {raw_dep}") |
| 84 | |
| 85 | for dep in dependencies: |
| 86 | if dep < 1 or dep >= n: |
| 87 | raise ValueError( |
| 88 | f"Invalid dependency reference for step {n}: #S{dep}" |
| 89 | ) |
| 90 | |
| 91 | steps.append( |
| 92 | PlanStep( |
| 93 | step_number=n, |
| 94 | task=tasks[n], |
| 95 | server=servers.get(n, ""), |
| 96 | tool=tools.get(n, ""), |
| 97 | tool_args={}, |
| 98 | dependencies=dependencies, |
| 99 | expected_output=outputs.get(n, ""), |
| 100 | ) |
| 101 | ) |
| 102 | |
| 103 | return Plan(steps=steps, raw=raw) |
| 104 | |
| 105 | |
| 106 | class Planner: |