first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
"""Main entry point for the tools module."""
# Import all split tool modules.
from .common import print_tool_output
from .command_tools import (
execute_command,
)
# Requirement refinement tools.
from .requirements_tools import (
make_user_query_more_detailed,
)
# Code generation and execution tools.
from .code_tools import (
cad_code_generator,
)
# SketchPad operation tools.
from .sketch_tools import (
sketch_pad_operations,
)
# Model multi-view rendering tools.
from .model_view_tools import get_visual_feedback
from .builtin_file_toolkit import create_builtin_file_tools
# Export all tool functions to maintain backward compatibility.
__all__ = [
"make_user_query_more_detailed",
"cad_code_generator",
"execute_command",
"sketch_pad_operations",
"get_visual_feedback",
"print_tool_output",
"create_builtin_file_tools",
]
@@ -0,0 +1,17 @@
from __future__ import annotations
from pathlib import Path
from typing import List, Optional
from SimpleLLMFunc.builtin import FileToolset
from SimpleLLMFunc.tool import Tool
def create_builtin_file_tools(workspace: Optional[str | Path] = None) -> List[Tool]:
"""Create SimpleLLMFunc builtin file tools scoped to the active workspace."""
root = Path(workspace).expanduser().resolve() if workspace else Path.cwd().resolve()
return FileToolset(root).toolset
__all__ = ["create_builtin_file_tools"]
+274
View File
@@ -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",
]
@@ -0,0 +1,93 @@
import asyncio
from SimpleLLMFunc import tool
from .common import print_tool_output
EXECUTE_COMMAND_TIMEOUT_SECONDS = 600
def _build_command_failure_message(result) -> str:
parts = [f"Command failed with exit code {result.returncode}."]
stdout = result.stdout.strip()
stderr = result.stderr.strip()
if stdout:
parts.append(f"STDOUT:\n{stdout}")
if stderr:
parts.append(f"STDERR:\n{stderr}")
return "\n\n".join(parts)
def _build_command_timeout_message(exc) -> str:
parts = [
f"Command timed out after {EXECUTE_COMMAND_TIMEOUT_SECONDS} seconds.",
"The process may be stuck, waiting for input, or simply taking too long.",
]
stdout = (exc.stdout or "").strip()
stderr = (exc.stderr or "").strip()
if stdout:
parts.append(f"Partial STDOUT:\n{stdout}")
if stderr:
parts.append(f"Partial STDERR:\n{stderr}")
return "\n\n".join(parts)
@tool(
name="execute_command",
description="Execute a system command in shell and return the output.",
)
async def execute_command(command: str) -> str:
"""Execute a system command in shell and return the output.
Args:
command: The system command to execute, recommended commands are uv run python <script path>
Returns:
The command output (stdout on success, stderr on failure)
"""
import subprocess
import time
try:
print_tool_output("⚡ Running Command", f"Executing: {command}")
start_time = time.time()
result = await asyncio.to_thread(
subprocess.run,
command,
shell=True,
capture_output=True,
text=True,
timeout=EXECUTE_COMMAND_TIMEOUT_SECONDS,
)
execution_time = time.time() - start_time
if result.returncode == 0:
print_tool_output(
"✅ Command Completed",
f"Return code: {result.returncode}, Time: {execution_time:.2f}s, Output: {len(result.stdout)} chars",
)
return result.stdout.strip()
else:
print_tool_output(
"❌ Command Failed",
f"Command failed.\nError: {result.stderr.strip()}",
)
return _build_command_failure_message(result)
except subprocess.TimeoutExpired as exc:
print_tool_output(
"⏱️ Command Timed Out", f"Timeout while executing command: {str(exc)}"
)
return _build_command_timeout_message(exc)
except Exception as e:
print_tool_output("💥 Command Error", f"Command execution failed: {str(e)}")
return f"Command execution failed: {str(e)}"
+94
View File
@@ -0,0 +1,94 @@
"""
Common tool functions and configuration.
"""
from pathlib import Path
from config.config import get_config
import asyncio
import concurrent.futures
config = get_config()
SUBAGENT_MAX_TOOL_CALLS = 100
_SKILL_RELATIVE_ROOTS = [
Path("skills/simplecad-self-evolve"),
Path("workspace/skills/simplecad-self-evolve"),
]
def print_tool_output(title: str, content: str, style: str = "cyan"):
"""Simplified tool output function using plain print and separator lines."""
print("\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
print(f"{title}")
print(content)
print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
def safe_asyncio_run(coro_func, *args, **kwargs):
"""Helper function for safely running an async function with passed-in arguments."""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, coro_func(*args, **kwargs))
return future.result(timeout=30)
else:
return loop.run_until_complete(coro_func(*args, **kwargs))
except RuntimeError:
return asyncio.run(coro_func(*args, **kwargs))
def build_simplecad_workspace_fact_block() -> str:
"""Return explicit working-directory facts for SimpleCAD subagents."""
cwd = Path.cwd().resolve()
repo_root = Path(__file__).resolve().parents[1]
discovered_skill_roots: list[Path] = []
candidate_bases = [cwd, repo_root, *cwd.parents]
seen_candidates: set[Path] = set()
for base in candidate_bases:
for relative_root in _SKILL_RELATIVE_ROOTS:
candidate = (base / relative_root).resolve()
if candidate in seen_candidates:
continue
seen_candidates.add(candidate)
if candidate.is_dir():
discovered_skill_roots.append(candidate)
preferred_skill_root = discovered_skill_roots[0] if discovered_skill_roots else None
def _display_path(path: Path) -> str:
try:
relative = path.relative_to(cwd).as_posix()
suffix = "/" if path.is_dir() else ""
return f"./{relative}{suffix}"
except ValueError:
return str(path)
lines = [
"[Workspace Facts]",
f"Current working directory: {cwd}",
f"Repository root: {repo_root}",
"Use relative paths from this directory.",
"Skill root: use the preferred skill root below.",
"Skill layout: <skill_root>/SKILL.md, <skill_root>/references/docs/api/README.md, <skill_root>/references/docs/api/*.md, <skill_root>/references/docs/core/*.md, <skill_root>/scripts/, <skill_root>/cases/",
]
if preferred_skill_root is not None:
lines.extend(
[
f"Preferred skill root: {_display_path(preferred_skill_root)}",
"You MUST read these files before choosing APIs:",
f"- {_display_path(preferred_skill_root / 'SKILL.md')}",
f"- {_display_path(preferred_skill_root / 'references/docs/api/README.md')}",
]
)
if discovered_skill_roots:
lines.append("Detected skill roots:")
for skill_root in discovered_skill_roots:
lines.append(f"- {_display_path(skill_root)}")
return "\n".join(lines)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,127 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Optional
from context.conversation_manager import get_current_context, get_current_sketch_pad
def _content_item_type(item: Any) -> Optional[str]:
if isinstance(item, dict):
value = item.get("type")
return value if isinstance(value, str) else None
value = getattr(item, "type", None)
return value if isinstance(value, str) else None
def _content_item_image_payload(item: Any) -> Any:
if isinstance(item, dict):
return item.get("image_url")
return getattr(item, "image_url", None)
def _image_payload_local_path(image_payload: Any) -> Optional[str]:
if isinstance(image_payload, dict):
value = image_payload.get("local_path")
return value if isinstance(value, str) else None
value = getattr(image_payload, "local_path", None)
return value if isinstance(value, str) else None
def _normalize_existing_file_path(path_value: Any) -> Optional[str]:
if not isinstance(path_value, (str, Path)):
return None
raw_value = str(path_value).strip()
if not raw_value:
return None
candidate = Path(raw_value).expanduser()
if not candidate.is_absolute():
candidate = (Path.cwd() / candidate).resolve()
try:
if candidate.exists() and candidate.is_file():
return str(candidate)
except OSError:
return None
return None
def _resolve_sketch_pad_image_path(reference: str) -> Optional[str]:
if not reference.startswith("key:"):
return None
sketch_pad = get_current_sketch_pad()
if sketch_pad is None:
return None
try:
value = sketch_pad.get_value(reference[4:])
except Exception:
return None
direct_path = _normalize_existing_file_path(value)
if direct_path is not None:
return direct_path
if not isinstance(value, dict):
return None
for key in ("local_path", "path", "image_path", "query_image_path"):
resolved_path = _normalize_existing_file_path(value.get(key))
if resolved_path is not None:
return resolved_path
return None
def get_latest_uploaded_reference_image_path() -> Optional[str]:
context = get_current_context()
if context is None:
return None
try:
messages = context.retrieve_full_messages()
except Exception:
try:
messages = context.retrieve_messages()
except Exception:
return None
for message in reversed(messages):
if getattr(message, "role", None) != "user":
continue
content = getattr(message, "content", None)
if not isinstance(content, list):
continue
for item in reversed(content):
if _content_item_type(item) != "image_url":
continue
image_payload = _content_item_image_payload(item)
resolved_path = _normalize_existing_file_path(
_image_payload_local_path(image_payload)
)
if resolved_path is not None:
return resolved_path
return None
def resolve_reference_image_path(query_image_path: Optional[str]) -> Optional[str]:
if isinstance(query_image_path, str) and query_image_path.strip():
stripped_path = query_image_path.strip()
sketch_pad_path = _resolve_sketch_pad_image_path(stripped_path)
if sketch_pad_path is not None:
return sketch_pad_path
direct_path = _normalize_existing_file_path(stripped_path)
if direct_path is not None:
return direct_path
return get_latest_uploaded_reference_image_path()
@@ -0,0 +1,328 @@
"""Requirement refinement tool implemented as a specialist subagent."""
from __future__ import annotations
import base64
from pathlib import Path
from typing import Any, Optional, Union
from SimpleLLMFunc import llm_chat, tool
from SimpleLLMFunc.type import HistoryList
from context.conversation_manager import get_current_sketch_pad
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 .reference_image import resolve_reference_image_path
from .sketch_tools import sketch_pad_operations
from .subagent_utils import run_subagent_with_events
def create_requirement_refinement_subagent_tools(
workspace: Optional[str | Path] = None,
) -> list[Any]:
"""Create the low-level tools owned by the requirement specialist.
Specialist only produces formatted text; SketchPad storage is done by the caller.
"""
return [
execute_command,
sketch_pad_operations,
*create_builtin_file_tools(workspace),
]
_REQUIRED_SECTIONS = [
"## API Reference",
"## Refined User Requirements",
"## Parameter Table",
"## Modeling Process",
"## Notes",
]
def _normalize_requirement_output(text: str) -> str:
"""Trim preamble and ensure all required sections exist."""
t = text.strip()
for h in _REQUIRED_SECTIONS:
idx = t.find(h)
if idx >= 0:
t = t[idx:]
break
for h in _REQUIRED_SECTIONS:
if h not in t:
t += f"\n\n{h}\n"
return t.strip()
def _build_requirement_request(
*,
query: str,
query_image_path: Optional[str],
) -> str:
parts = [
"Generate a detailed modeling specification. Output must include: ## API Reference, ## Refined User Requirements, ## Parameter Table, ## Modeling Process, ## Notes.",
"Use the workspace facts below.",
"Before you write the final answer, you MUST use file tools to read the preferred skill root's `SKILL.md`, then `references/docs/api/README.md`, then the exact API Markdown pages you cite.",
"Do not answer from memory. If you have not read those files yet, continue using tools.",
"In `## API Reference`, cite the concrete file paths you read and only recommend APIs whose exact Markdown pages you actually opened.",
"If the task mentions SketchPad keys, use `sketch_pad_operations` to retrieve them before refining the requirement.",
"",
build_simplecad_workspace_fact_block(),
"",
"[User Query]",
query.strip(),
]
if query_image_path and query_image_path.strip():
parts.append("\n[Reference image attached below]")
return "\n".join(parts)
def _image_path_to_base64_data_url(image_path: str) -> Optional[str]:
"""Read image file and return data URL for OpenAI API."""
p = Path(image_path)
if not p.exists() or not p.is_file():
return None
ext = p.suffix.lower()
mime_map = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
}
mime = mime_map.get(ext, "image/jpeg")
try:
b64 = base64.b64encode(p.read_bytes()).decode("utf-8")
return f"data:{mime};base64,{b64}"
except Exception:
return None
def _build_message_with_image(
text: str,
query_image_path: Optional[str],
) -> Union[str, list[dict[str, Any]]]:
"""Build message: text only, or text + image as OpenAI content array."""
if not query_image_path or not query_image_path.strip():
return text
data_url = _image_path_to_base64_data_url(query_image_path.strip())
if not data_url:
raise RuntimeError(
f"Failed to load reference image: {query_image_path.strip()}"
)
return [
{"type": "text", "text": text},
{"type": "image_url", "image_url": {"url": data_url}},
]
@tool(
name="make_user_query_more_detailed",
description=(
"Refine and expand the user's modeling requirement through a specialist subagent. "
"The specialist can inspect local skill docs, inspect APIs, read local files, and consult SketchPad "
"before producing a structured modeling specification."
),
best_practices=[
"Pass the complete user request in `query`, not only a short delta fragment.",
"If the user provided a reference image, pass its workspace-local path in `query_image_path`.",
"If `query_image_path` is omitted, the tool will automatically reuse the latest uploaded image from the active conversation when available.",
"Use this tool when the modeling request is vague, underspecified, or needs a step-by-step plan before coding.",
"The specialist will read local skill docs directly with its file tools to ground the refinement.",
"The final result should include a structured modeling process, not only rewritten prose.",
],
)
async def make_user_query_more_detailed(
query: str,
query_image_path: Optional[str] = None,
event_emitter: Any = None,
) -> str:
"""Refine the user's modeling request via a requirement specialist subagent.
The refined requirement is always stored in SketchPad for downstream tools to reference.
Args:
query: The user's original request. This may also mention SketchPad ids that the
specialist should inspect.
query_image_path: Optional workspace-local reference image path, typically something
like `./uploads/<conversation_id>/query_image_001.png`.
event_emitter: Optional tool event emitter used to forward nested specialist activity.
Returns:
str: Refined requirement text with SketchPad key for reference.
"""
print_tool_output(
title="Requirement Refinement Started",
content=f"Request: {query}",
)
requested_query_image_path = (
query_image_path.strip()
if isinstance(query_image_path, str) and query_image_path.strip()
else None
)
resolved_query_image_path = resolve_reference_image_path(query_image_path)
if requested_query_image_path is not None and resolved_query_image_path is None:
raise RuntimeError(f"Reference image not found: {requested_query_image_path}")
if resolved_query_image_path is not None:
print_tool_output(
title="Reference Image Attached",
content=f"Using reference image: {resolved_query_image_path}",
)
text_content = _build_requirement_request(
query=query,
query_image_path=resolved_query_image_path,
)
message = _build_message_with_image(text_content, resolved_query_image_path)
result_text = await run_subagent_with_events(
specialist_callable=requirement_refinement_specialist,
specialist_kwargs={
"message": message,
"history": [],
},
subagent_label="Requirement Refinement Specialist",
event_emitter=event_emitter,
status_payload={
"query": query,
"query_image_path": resolved_query_image_path,
},
response_transform=_normalize_requirement_output,
)
final_text = result_text.strip()
print_tool_output(title="Refined User Requirements", content=final_text)
sketch_pad = get_current_sketch_pad()
if sketch_pad is None:
raise RuntimeError(
"The refined requirement must be written to SketchPad, but there is no active conversation context."
)
import uuid
sketch_key = f"req_{uuid.uuid4().hex[:8]}"
try:
await sketch_pad.set_item(
key=sketch_key,
value=final_text,
ttl=None,
summary=None,
tags={"detailed_query", "requirements", "expanded"},
)
print_tool_output(
title="💾 Stored In SketchPad",
content=f"Key: {sketch_key}\nThe refined requirement has been saved for downstream tools.",
)
return (
"Detailed requirements generated and stored in SketchPad:\n\n"
f"🔑 SketchPad Key: {sketch_key}\n"
"# Tags: detailed_query, requirements, expanded\n"
f'💡 Tip: You can now reference key "{sketch_key}" in later tool calls, for example:\n'
"- include it in the natural-language task for `cad_code_generator`\n"
"- store it alongside other constraints or debugging notes in SketchPad\n"
"- create the target folder first, then use `echo_into` to write a file if needed\n"
)
except Exception as exc:
print_tool_output(
"❌ SketchPad Store Failed", f"Failed to store in SketchPad: {exc}"
)
raise RuntimeError(
f"The refined requirement must be written to SketchPad, but storage failed: {exc}"
) from exc
@llm_chat(
llm_interface=get_config().MULTIMODALITY_INTERFACE,
toolkit=create_requirement_refinement_subagent_tools(),
max_tool_calls=SUBAGENT_MAX_TOOL_CALLS,
stream=True,
enable_event=True,
timeout=600,
temperature=1.0,
)
async def requirement_refinement_specialist(
message: Union[str, list[dict[str, Any]]],
history: HistoryList | None = None,
) -> None: # type: ignore[misc]
"""Generate a detailed modeling specification. Output: ## API Reference, ## Refined User Requirements, ## Parameter Table, ## Modeling Process, ## Notes.
Use the workspace facts included in the user message. Read `SKILL.md`, then the API index, then the exact API Markdown pages you cite.
REQUIRED: The detailed query MUST use exactly correct API names and code snippets.
Tools: execute_command, sketch_pad_operations, read_file, grep, sed, echo_into.
You MUST read SKILL.md and the API index before choosing APIs. Retrieve SketchPad artifacts when task mentions keys.
<EXAMPLE>
User: "Create a 7.62mm rifle cartridge model"
## Refined User Requirements
1. **Target Object**: A standard 7.62mm caliber rifle cartridge (Full Metal Jacket type).
2. **Components**: The model consists of four parts: the bullet tip (projectile), the cartridge case (neck, shoulder, body), the rim/extractor groove, and a primer base.
3. **Dimensions**:
- **Projectile**: Diameter 7.62mm, ogive shape with a rounded tip.
- **Case Body**: Maximum diameter approx 11.3mm, total case length 51mm (based on 7.62x51mm NATO standard).
- **Shoulder/Neck**: Tapered transition from body to 7.62mm neck.
4. **Output**: A single combined solid representing the exterior geometry of the cartridge.
## Parameter Table
| Parameter | Type | Default Value | Calculation Logic |
|---|---|---|---|
| bullet_dia | float | 7.62 | Nominal caliber |
| case_body_dia | float | 11.3 | Max diameter of the case body |
| total_length | float | 71.0 | Full cartridge length including projectile |
| body_length | float | 38.0 | Length from base to shoulder |
| shoulder_length | float | 3.5 | Length of the tapered shoulder |
| neck_length | float | 8.0 | Length of the neck holding the bullet |
| case_length | float | 51.0 | body_length + shoulder_length + neck_length |
| rim_dia | float | 11.5 | Diameter of the base rim |
## Modeling Process
1. **Create Case Main Body**
- **Purpose**: Create the main cylindrical propellant chamber.
- **API**: `make_cylinder_rsolid`
- **Spatial Reasoning**: Cylinder radius `case_body_dia/2`, height `body_length`, base at (0,0,0).
2. **Create Shoulder and Neck**
- **Purpose**: Model the tapered transition and casing neck.
- **API**: `make_cone_rsolid`, `make_cylinder_rsolid`, `translate_shape`
- **Spatial Reasoning**: Shoulder cone bottom radius `case_body_dia/2`, top `bullet_dia/2`, height `shoulder_length`, translate to Z=body_length. Neck cylinder radius `bullet_dia/2`, height `neck_length`, translate to Z=body_length+shoulder_length.
3. **Create Projectile**
- **Purpose**: Form the aerodynamic tip.
- **API**: `make_cone_rsolid`, `union_rsolidlist`
- **Spatial Reasoning**: Cone base radius `bullet_dia/2`, height `total_length-case_length`, translate to Z=case_length.
4. **Add Extractor Groove and Rim**
- **Purpose**: Model the base where extractor grips.
- **API**: `make_cylinder_rsolid`, `cut_rsolidlist`
- **Spatial Reasoning**: Rim cylinder radius `rim_dia/2`, height 1.5. Cut groove with smaller cylinder.
5. **Final Assembly**
- **Purpose**: Combine into single manifold solid.
- **API**: `union_rsolidlist`
- **Spatial Reasoning**: Boolean union on body, shoulder, neck, projectile, rim.
## Notes
Based on 7.62x51mm NATO standard. APIs must be verified against SKILL.md.
</EXAMPLE>
"""
pass
__all__ = [
"make_user_query_more_detailed",
"requirement_refinement_specialist",
"create_requirement_refinement_subagent_tools",
]
+185
View File
@@ -0,0 +1,185 @@
"""SketchPad operation tools."""
from SimpleLLMFunc import tool
from typing import Optional, Any, Tuple, List
import uuid
from .common import print_tool_output
from context.conversation_manager import get_current_sketch_pad
@tool(
name="sketch_pad_operations",
description="Store, retrieve, search and manage data in SketchPad. Supports key-value storage with automatic summarization.",
)
async def sketch_pad_operations(
operation: str,
key: Optional[str] = None,
value: Optional[str] = None,
tags: Optional[str] = None,
search_query: Optional[str] = None,
ttl: Optional[int] = None,
) -> str:
"""
Perform operations on SketchPad storage.
Args:
operation: One of "store", "retrieve", "delete", "list", "search_tags", "search", "clear", "stats"
key: Key for store/retrieve/delete operations
value: Value to store (required for store operation)
tags: Comma-separated tags for store operation, marking the item with specific labels
search_query: Query for search operations
ttl: Time to live in seconds (optional for store)
Returns:
str: Result of the operation
"""
sketch_pad = get_current_sketch_pad()
if sketch_pad is None:
return "Error: No active conversation context. SketchPad operations must be called within a conversation context."
try:
if operation == "store":
if not value:
return "Error: value is required for store operation"
# Parse tags
tag_set = set()
if tags:
tag_set = set(tag.strip() for tag in tags.split(","))
# Generate a stable default key when none is provided.
actual_key_input = key or f"item_{uuid.uuid4().hex[:8]}"
actual_key = await sketch_pad.set_item(
key=actual_key_input,
value=value,
ttl=ttl,
summary=None,
tags=tag_set,
)
print_tool_output(
title="✅ SketchPad Store Succeeded",
content=f"Key: {actual_key}\nValue length: {len(str(value))} chars\nTags: {tags or 'None'}",
)
return f"Stored successfully with key: {actual_key}"
elif operation == "retrieve":
if not key:
return "Error: key is required for retrieve operation"
value = sketch_pad.get_value(key)
if value is None:
print_tool_output(
"❌ SketchPad Retrieve Failed", f"Key '{key}' not found"
)
return f"Key '{key}' not found"
value_str = str(value)
print_tool_output(
title="✅ SketchPad Retrieve Succeeded",
content=(
f"Key: {key}\nValue: {value_str[:200]}..."
if len(value_str) > 200
else f"Key: {key}\nValue: {value_str}"
),
)
return value_str
elif operation == "delete":
if not key:
return "Error: key is required for delete operation"
success = sketch_pad.delete(key)
if success:
print_tool_output(
"✅ SketchPad Delete Succeeded", f"Key '{key}' deleted"
)
return f"Key '{key}' deleted successfully"
else:
print_tool_output(
"❌ SketchPad Delete Failed", f"Key '{key}' not found"
)
return f"Key '{key}' not found"
elif operation == "list":
items = sketch_pad.list_items(include_value=False)
if not items:
return "SketchPad is empty"
result = "SketchPad contents:\n"
for list_item in items:
summary = list_item.summary or "No summary"
result += f"- {list_item.key}: {summary[:50]}...\n"
print_tool_output("📋 SketchPad Contents", result)
return result
elif operation == "search_tags":
if not search_query:
return "Error: search_query is required for search_tags operation"
# Parse tag query
tag_set = set(tag.strip() for tag in search_query.split(","))
results_raw = sketch_pad.search_by_tags(tag_set)
results_tags: List[Tuple[str, Any]] = list(results_raw)
if not results_tags:
return f"No items found with tags: {search_query}"
result = f"Found {len(results_tags)} items with tags '{search_query}':\n"
for key, tag_item in results_tags[:5]:
summary = tag_item.summary or "No summary"
result += f"- {key}: {summary[:50]}...\n"
print_tool_output("🔍 SketchPad Tag Search Results", result)
return result
elif operation == "search":
if not search_query:
return "Error: search_query is required for search operation"
results_raw = sketch_pad.search_by_content(search_query)
results_content: List[Tuple[str, Any]] = list(results_raw)
if not results_content:
return f"No items found for query: {search_query}"
result = f"Found {len(results_content)} items for '{search_query}':\n"
for key, content_item in results_content[:5]:
summary = content_item.summary or "No summary"
result += f"- {key}: {summary[:50]}...\n"
print_tool_output("🔍 SketchPad Content Search Results", result)
return result
elif operation == "clear":
sketch_pad.clear()
print_tool_output("🗑️ SketchPad Cleared", "All items have been removed")
return "SketchPad cleared successfully"
elif operation == "stats":
stats = sketch_pad.get_statistics()
result = "SketchPad Statistics:\n"
result += f"- Total items: {stats.total_items}\n"
result += f"- Max items: {stats.max_items}\n"
result += f"- Items with summary: {stats.items_with_summary}\n"
result += f"- Total accesses: {stats.total_accesses}\n"
result += f"- Memory usage: {stats.memory_usage_percent:.1f}%\n"
if getattr(stats, "popular_tags", None):
result += f"- Popular tags: {', '.join(stats.popular_tags.keys())}\n"
if getattr(stats, "content_types", None):
result += f"- Content types: {', '.join(stats.content_types.keys())}\n"
print_tool_output("📊 SketchPad Statistics", result)
return result
else:
return f"Error: Unknown operation '{operation}'. Supported: store, retrieve, delete, list, search_tags, search, clear, stats"
except Exception as e:
error_msg = f"SketchPad operation failed: {str(e)}"
print_tool_output("❌ SketchPad Operation Failed", error_msg)
return error_msg
@@ -0,0 +1,173 @@
from __future__ import annotations
from typing import Any, Awaitable, Callable, Optional
from SimpleLLMFunc.hooks import (
CustomEvent,
ReactEndEvent,
ToolCallEndEvent,
ToolCallErrorEvent,
ToolCallStartEvent,
is_event_yield,
is_response_yield,
)
from react_stream import extract_output_text, extract_response_reasoning
async def emit_subagent_progress(
event_emitter: Any,
event_name: str,
data: dict[str, Any],
) -> None:
if event_emitter is None:
return
emit = getattr(event_emitter, "emit", None)
if not callable(emit):
return
awaitable_result: Any = emit(event_name, data)
await awaitable_result
async def run_subagent_with_events(
*,
specialist_callable: Callable[..., Any],
specialist_kwargs: dict[str, Any],
subagent_label: str,
event_emitter: Any,
status_payload: Optional[dict[str, Any]] = None,
response_transform: Optional[Callable[[str], str]] = None,
captured_tool_results: Optional[list[tuple[str, str]]] = None,
) -> str:
"""Run a streaming llm_function specialist and bridge its events outward."""
report = ""
start_payload = {"phase": "started", "message": f"{subagent_label} started."}
if status_payload:
start_payload.update(status_payload)
start_payload["subagent_label"] = subagent_label
await emit_subagent_progress(event_emitter, "subagent_status", start_payload)
async for output in specialist_callable(**specialist_kwargs):
if is_response_yield(output):
response_text = extract_output_text(output, f"subagent_{subagent_label}")
if response_text:
report += response_text
await emit_subagent_progress(
event_emitter,
"subagent_response",
{
"subagent_label": subagent_label,
"delta_text": response_text,
},
)
reasoning_text = extract_response_reasoning(output.response)
if reasoning_text:
await emit_subagent_progress(
event_emitter,
"subagent_reasoning",
{
"subagent_label": subagent_label,
"delta_reasoning": reasoning_text,
},
)
continue
if not is_event_yield(output):
continue
event = output.event
if isinstance(event, ToolCallStartEvent):
await emit_subagent_progress(
event_emitter,
"subagent_tool_start",
{
"subagent_label": subagent_label,
"nested_tool_name": event.tool_name,
"nested_tool_call_id": event.tool_call_id,
"arguments": event.arguments,
},
)
continue
if isinstance(event, ToolCallEndEvent):
if captured_tool_results is not None and event.result is not None:
captured_tool_results.append((event.tool_name, str(event.result)))
await emit_subagent_progress(
event_emitter,
"subagent_tool_end",
{
"subagent_label": subagent_label,
"nested_tool_name": event.tool_name,
"nested_tool_call_id": event.tool_call_id,
"arguments": event.arguments,
"result": event.result,
"execution_time": event.execution_time,
"success": event.success,
},
)
continue
if isinstance(event, ToolCallErrorEvent):
await emit_subagent_progress(
event_emitter,
"subagent_tool_error",
{
"subagent_label": subagent_label,
"nested_tool_name": event.tool_name,
"nested_tool_call_id": event.tool_call_id,
"arguments": event.arguments,
"error_message": event.error_message,
"execution_time": event.execution_time,
},
)
continue
if isinstance(event, CustomEvent):
if event.event_name.startswith("subagent_") and isinstance(
event.data, dict
):
forwarded_data = dict(event.data)
forwarded_data.setdefault("subagent_label", subagent_label)
forwarded_data.setdefault("source_tool_name", event.tool_name)
forwarded_data.setdefault("source_tool_call_id", event.tool_call_id)
await emit_subagent_progress(
event_emitter,
event.event_name,
forwarded_data,
)
continue
await emit_subagent_progress(
event_emitter,
"subagent_custom",
{
"subagent_label": subagent_label,
"nested_tool_name": event.tool_name,
"nested_tool_call_id": event.tool_call_id,
"custom_event_name": event.event_name,
"data": event.data,
},
)
continue
if isinstance(event, ReactEndEvent):
if not report and isinstance(event.final_response, str):
report = event.final_response
finish_payload = {"phase": "finished", "message": f"{subagent_label} finished."}
if status_payload:
finish_payload.update(status_payload)
finish_payload["subagent_label"] = subagent_label
await emit_subagent_progress(event_emitter, "subagent_status", finish_payload)
final_report = response_transform(report) if response_transform else report
return final_report
__all__ = ["emit_subagent_progress", "run_subagent_with_events"]