first commit
This commit is contained in:
@@ -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",
|
||||
]
|
||||
Reference in New Issue
Block a user