first commit
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
"""CAD code generation tool implemented as a specialist subagent."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
from typing import Any, Optional
|
||||
|
||||
from SimpleLLMFunc import llm_chat, tool
|
||||
from SimpleLLMFunc.type import HistoryList
|
||||
|
||||
from .builtin_file_toolkit import create_builtin_file_tools
|
||||
from .command_tools import execute_command
|
||||
from .common import (
|
||||
SUBAGENT_MAX_TOOL_CALLS,
|
||||
build_simplecad_workspace_fact_block,
|
||||
get_config,
|
||||
print_tool_output,
|
||||
)
|
||||
from .sketch_tools import sketch_pad_operations
|
||||
from .subagent_utils import run_subagent_with_events
|
||||
|
||||
|
||||
def create_codegen_subagent_tools(
|
||||
workspace: Optional[str | Path] = None,
|
||||
) -> list[Any]:
|
||||
"""Create the low-level tools owned by the CAD codegen specialist."""
|
||||
|
||||
return [
|
||||
execute_command,
|
||||
sketch_pad_operations,
|
||||
*create_builtin_file_tools(workspace),
|
||||
]
|
||||
|
||||
|
||||
def _build_codegen_request(
|
||||
*,
|
||||
task: str,
|
||||
target_file_path: str,
|
||||
validation_command: Optional[str],
|
||||
requirement_sketch_key: Optional[str] = None,
|
||||
) -> str:
|
||||
parts = [f"target_file: {target_file_path}"]
|
||||
if validation_command and validation_command.strip():
|
||||
parts.append(f"validation_command: {validation_command.strip()}")
|
||||
if requirement_sketch_key and requirement_sketch_key.strip():
|
||||
parts.append(f"requirement_key: {requirement_sketch_key.strip()}")
|
||||
parts.append("")
|
||||
parts.append(build_simplecad_workspace_fact_block())
|
||||
parts.append("")
|
||||
parts.append(task.strip())
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _extract_python_code_block(text: str) -> Optional[str]:
|
||||
match = re.search(r"```python\s*(.*?)```", text, flags=re.DOTALL)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
return None
|
||||
|
||||
|
||||
def _build_missing_code_retry_request(
|
||||
*,
|
||||
target_file_path: str,
|
||||
) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"<RETRY_AFTER_NO_CODE>",
|
||||
f"You ended without writing any script to {target_file_path}.",
|
||||
f"You must write the required Python script directly to {target_file_path} before you finish.",
|
||||
"Do not stop after planning, describing the approach, or pasting a code block in chat.",
|
||||
"Use the file-writing tool now, save the script to disk, then validate/debug until export succeeds.",
|
||||
"</RETRY_AFTER_NO_CODE>",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _build_missing_code_retry_history(
|
||||
*,
|
||||
original_request: str,
|
||||
prior_report: str,
|
||||
) -> HistoryList:
|
||||
history: HistoryList = [{"role": "user", "content": original_request}]
|
||||
if prior_report.strip():
|
||||
history.append({"role": "assistant", "content": prior_report.strip()})
|
||||
return history
|
||||
|
||||
|
||||
def _default_validation_command(
|
||||
target_file_path: str, validation_command: Optional[str]
|
||||
) -> Optional[str]:
|
||||
if validation_command and validation_command.strip():
|
||||
return validation_command.strip()
|
||||
|
||||
if target_file_path.endswith(".py"):
|
||||
script_path = shlex.quote(target_file_path)
|
||||
output_dir = shlex.quote(str(Path(target_file_path).parent or Path(".")))
|
||||
stl_glob = f"{output_dir}/*.stl"
|
||||
step_glob = f"{output_dir}/*.step"
|
||||
stp_glob = f"{output_dir}/*.stp"
|
||||
return (
|
||||
f"uv run python {script_path} && "
|
||||
f"ls {stl_glob} && "
|
||||
f"(ls {step_glob} || ls {stp_glob})"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _read_latest_code(target_file_path: str) -> Optional[str]:
|
||||
candidate = Path(target_file_path)
|
||||
if not candidate.is_absolute():
|
||||
candidate = Path.cwd() / candidate
|
||||
|
||||
if not candidate.exists() or not candidate.is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
return candidate.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@tool(
|
||||
name="cad_code_generator",
|
||||
description=(
|
||||
"Delegate CAD script creation or repair to a specialist subagent that owns "
|
||||
"builtin file tools and can iteratively debug the target file."
|
||||
),
|
||||
best_practices=[
|
||||
"MUST pass requirement_sketch_key (the req_xxxx from make_user_query_more_detailed) so the specialist retrieves the detailed spec.",
|
||||
"Use one complete natural-language task block instead of splitting context across many parameters.",
|
||||
"In the task, always say whether this is create-new-file or modify-existing-code.",
|
||||
"In the task, include the full user intent, failure context, relevant SketchPad ids, and success criteria.",
|
||||
"In the task, explicitly tell the specialist to validate and keep debugging until STL and STEP/STP export succeeds.",
|
||||
"Always provide the exact target_file_path for model.py or the file to repair.",
|
||||
],
|
||||
)
|
||||
async def cad_code_generator(
|
||||
task: str,
|
||||
target_file_path: str,
|
||||
requirement_sketch_key: Optional[str] = None,
|
||||
event_emitter: Any = None,
|
||||
) -> str:
|
||||
"""Run the CAD coding specialist as a single-call subagent.
|
||||
|
||||
Args:
|
||||
task: A complete natural-language mission for the coding specialist.
|
||||
This should explicitly include:
|
||||
- the full user intent,
|
||||
- whether the job is create-new-file or modify-existing-code,
|
||||
- the concrete modification target or creation goal,
|
||||
- any traceback / visual feedback / failure context,
|
||||
- any relevant SketchPad ids that the specialist should inspect,
|
||||
- any reference-code SketchPad ids if they matter,
|
||||
- the expected success criteria,
|
||||
- and an explicit instruction to validate and keep debugging until model export succeeds.
|
||||
Prefer one complete instruction block instead of splitting context across
|
||||
many parameters.
|
||||
target_file_path: The exact path of the script file that the specialist owns.
|
||||
In the normal workflow this should be the final `model.py` path.
|
||||
requirement_sketch_key: REQUIRED. The SketchPad key (e.g. req_xxxx) from make_user_query_more_detailed.
|
||||
The specialist will retrieve and follow this detailed requirement.
|
||||
event_emitter: Optional tool event emitter used to forward nested specialist
|
||||
progress events back to the outer agent event stream.
|
||||
|
||||
Returns:
|
||||
A concise report from the specialist plus the latest code snapshot.
|
||||
"""
|
||||
|
||||
actual_validation_command = _default_validation_command(
|
||||
target_file_path,
|
||||
None,
|
||||
)
|
||||
|
||||
request_payload = _build_codegen_request(
|
||||
task=task.strip(),
|
||||
target_file_path=target_file_path,
|
||||
validation_command=actual_validation_command,
|
||||
requirement_sketch_key=requirement_sketch_key,
|
||||
)
|
||||
|
||||
print_tool_output(
|
||||
"🧠 CAD Code Specialist",
|
||||
"\n".join(
|
||||
[
|
||||
f"Target file: {target_file_path}",
|
||||
f"Validation command: {actual_validation_command or '(not provided)'}",
|
||||
f"Task summary: {task.strip()[:160]}",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
report = await run_subagent_with_events(
|
||||
specialist_callable=cad_code_generator_specialist,
|
||||
specialist_kwargs={
|
||||
"message": request_payload,
|
||||
"history": [],
|
||||
},
|
||||
subagent_label="CAD Code Specialist",
|
||||
event_emitter=event_emitter,
|
||||
status_payload={
|
||||
"target_file_path": target_file_path,
|
||||
"validation_command": actual_validation_command,
|
||||
},
|
||||
)
|
||||
|
||||
written_code = _read_latest_code(target_file_path)
|
||||
latest_code = written_code or _extract_python_code_block(report)
|
||||
if written_code is None:
|
||||
print_tool_output(
|
||||
"⚠️ CAD Code Specialist",
|
||||
"First attempt did not write the target file. Appending a stricter follow-up instruction.",
|
||||
)
|
||||
retry_report = await run_subagent_with_events(
|
||||
specialist_callable=cad_code_generator_specialist,
|
||||
specialist_kwargs={
|
||||
"message": _build_missing_code_retry_request(
|
||||
target_file_path=target_file_path,
|
||||
),
|
||||
"history": _build_missing_code_retry_history(
|
||||
original_request=request_payload,
|
||||
prior_report=report,
|
||||
),
|
||||
},
|
||||
subagent_label="CAD Code Specialist",
|
||||
event_emitter=event_emitter,
|
||||
status_payload={
|
||||
"target_file_path": target_file_path,
|
||||
"validation_command": actual_validation_command,
|
||||
"retry_reason": "no_code_written",
|
||||
},
|
||||
)
|
||||
report = retry_report.strip() or report
|
||||
written_code = _read_latest_code(target_file_path)
|
||||
latest_code = written_code or _extract_python_code_block(report)
|
||||
|
||||
if latest_code is None:
|
||||
return report.strip()
|
||||
|
||||
return (
|
||||
f"{report.strip()}\n\n"
|
||||
f"📁 Target file: {target_file_path}\n"
|
||||
f"📄 Latest code:\n```python\n{latest_code.strip()}\n```"
|
||||
)
|
||||
|
||||
|
||||
@llm_chat(
|
||||
llm_interface=get_config().REASONING_INTERFACE,
|
||||
toolkit=create_codegen_subagent_tools(),
|
||||
max_tool_calls=SUBAGENT_MAX_TOOL_CALLS,
|
||||
stream=True,
|
||||
enable_event=True,
|
||||
timeout=900,
|
||||
temperature=0.8,
|
||||
)
|
||||
async def cad_code_generator_specialist(
|
||||
message: str,
|
||||
history: HistoryList | None = None,
|
||||
) -> None: # type: ignore[misc]
|
||||
"""You are a CAD coding agent. Write Python code directly to the target file with echo_into.
|
||||
|
||||
Always create/write the target file first. Use the workspace facts included in the user message. Read the chosen skill root's `SKILL.md`, then `references/docs/api/README.md`, then the exact API Markdown pages you use. Use the provided `validation_command` exactly; when you run Python in this repo/workspace, prefer `uv run python ...`. Run validation directly with `execute_command`; that tool already allows up to 600 seconds for a command, so use it as the standard execution path. After a successful script run, verify exported files with `ls` instead of rerunning the same script just to check whether STL/STEP outputs exist. Do not print whole solids, assemblies, or full model objects for inspection; use QL queries and print only the small queried facts you need for grounding/debugging. Keep debugging until the script is executed successfully and exports both STL and STEP/STP (for example, ./model.stl and ./model.step).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
__all__ = [
|
||||
"cad_code_generator",
|
||||
"cad_code_generator_specialist",
|
||||
"create_codegen_subagent_tools",
|
||||
]
|
||||
Reference in New Issue
Block a user