新增cdsl
This commit is contained in:
@@ -15,6 +15,7 @@ import httpx
|
||||
from app.models.contracts import ChatMessage
|
||||
from app.services.engine_service import build_revision, load_engine, validate_cdsl
|
||||
from app.services.library import CdslLibrary
|
||||
from app.services.part_skills import PartSkillLibrary
|
||||
from app.services.sse import event
|
||||
from app.services.storage import WorkspaceStore, now_iso
|
||||
from app.settings import ProviderConfig, ProviderModel, Settings
|
||||
@@ -141,12 +142,25 @@ def invalid_cdsl_result(error: ValueError) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def invalid_design_intent_result(error: Exception) -> dict[str, Any]:
|
||||
code = str(getattr(error, "code", "INVALID_DESIGN_INTENT"))
|
||||
return {
|
||||
"ok": False,
|
||||
"code": code,
|
||||
"message": f"The DesignIntent plan was rejected: {error}. Correct the complete plan before generating CDSL.",
|
||||
}
|
||||
|
||||
|
||||
def user_visible_tool_message(result: dict[str, Any], user_text: str) -> str:
|
||||
code = str(result.get("code") or "")
|
||||
if code == "INVALID_CDSL":
|
||||
if any("\u4e00" <= char <= "\u9fff" for char in str(user_text or "")):
|
||||
return "CDSL 不符合 engine 的模型契约,正在请求模型按 schema 修正后重新生成。"
|
||||
return "The CDSL model does not match the engine contract. Asking the model to correct it and retry."
|
||||
if code in {"INVALID_DESIGN_INTENT", "INTENT_CDSL_MISMATCH", "DESIGN_INTENT_REQUIRED", "DESIGN_INTENT_BLOCKED"}:
|
||||
if any("\u4e00" <= char <= "\u9fff" for char in str(user_text or "")):
|
||||
return "设计意图尚未通过校验,未进入 CAD 构建。"
|
||||
return "The design intent has not passed validation, so CAD construction has not started."
|
||||
return str(result.get("message") or result.get("summary") or "")
|
||||
|
||||
|
||||
@@ -187,6 +201,22 @@ def _cdsl_tool_schema() -> dict[str, Any]:
|
||||
CDSL_TOOL_SCHEMA = _cdsl_tool_schema()
|
||||
|
||||
|
||||
def _design_intent_tool_schema() -> dict[str, Any]:
|
||||
engine_dir = Path(__file__).resolve().parents[2] / "engine" / "cdsl_engine"
|
||||
try:
|
||||
schema = json.loads((engine_dir / "design_intent_schema.json").read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise RuntimeError("Local DesignIntent JSON Schema is unavailable or invalid") from error
|
||||
# The model cannot forge storage/audit fields. They are added only after
|
||||
# backend validation by WorkspaceStore.create_design_intent().
|
||||
for field in ("intent_id", "created_at", "part_skill_ids", "part_skill_selection"):
|
||||
schema.get("properties", {}).pop(field, None)
|
||||
return schema
|
||||
|
||||
|
||||
DESIGN_INTENT_TOOL_SCHEMA = _design_intent_tool_schema()
|
||||
|
||||
|
||||
TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
@@ -197,6 +227,7 @@ TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}, "limit": {"type": "integer", "minimum": 1, "maximum": 8}},
|
||||
"required": ["query"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -205,7 +236,24 @@ TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
"function": {
|
||||
"name": "read_cdsl_reference",
|
||||
"description": "Read one official CDSL sample by part_id. Use this before creating geometry based on a reference.",
|
||||
"parameters": {"type": "object", "properties": {"part_id": {"type": "string"}}, "required": ["part_id"]},
|
||||
"parameters": {"type": "object", "properties": {"part_id": {"type": "string"}}, "required": ["part_id"], "additionalProperties": False},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "propose_design_intent",
|
||||
"description": "Submit a complete semantic DesignIntent plan before searching CDSL references or generating CDSL.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intent": DESIGN_INTENT_TOOL_SCHEMA,
|
||||
"summary": {"type": "string", "minLength": 1},
|
||||
"assumptions": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["intent", "summary", "assumptions"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -224,11 +272,12 @@ TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"design_intent_id": {"type": "string", "pattern": "^intent_[a-z0-9]{12}$"},
|
||||
"cdsl": CDSL_TOOL_SCHEMA,
|
||||
"summary": {"type": "string", "minLength": 1},
|
||||
"assumptions": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["cdsl", "summary"],
|
||||
"required": ["design_intent_id", "cdsl", "summary", "assumptions"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
@@ -242,13 +291,11 @@ def tools_for_model(model: ProviderModel) -> list[dict[str, Any]]:
|
||||
if not model.strict_tool_schema:
|
||||
return tools
|
||||
|
||||
generate_tool = next(
|
||||
tool for tool in tools
|
||||
if tool.get("function", {}).get("name") == "generate_cdsl_model"
|
||||
)
|
||||
# This flag constrains function arguments only. It has no effect on normal
|
||||
# assistant text, the user's prompt, or the natural-language summary.
|
||||
generate_tool["function"]["strict"] = True
|
||||
for tool in tools:
|
||||
if tool.get("function", {}).get("name") in {"propose_design_intent", "generate_cdsl_model"}:
|
||||
# This flag constrains function arguments only. It has no effect on
|
||||
# normal assistant text, the user's prompt, or the summary.
|
||||
tool["function"]["strict"] = True
|
||||
return tools
|
||||
|
||||
|
||||
@@ -352,9 +399,12 @@ def system_prompt(
|
||||
user_text: str,
|
||||
viewer_context: list[dict[str, Any]] | None = None,
|
||||
task_id: str = "",
|
||||
part_skill_context: str = "",
|
||||
) -> str:
|
||||
skill_path = settings.engine_root.parent.parent / "agent" / "skills" / "cad-engine" / "SKILL.md"
|
||||
skill = skill_path.read_text(encoding="utf-8") if skill_path.is_file() else ""
|
||||
planning_recipe_path = skill_path.with_name("planning-recipe.md")
|
||||
planning_recipe = planning_recipe_path.read_text(encoding="utf-8") if planning_recipe_path.is_file() else ""
|
||||
readme_path = settings.engine_root / "README.md"
|
||||
engine_readme = readme_path.read_text(encoding="utf-8") if readme_path.is_file() else ""
|
||||
profile_schema_path = settings.engine_root / "profile_schema.json"
|
||||
@@ -379,6 +429,12 @@ Tool call contract:
|
||||
- Do not append prose, Markdown code fences, comments, or a second JSON value.
|
||||
- For generate_cdsl_model, pass the complete CDSL as the cdsl object directly,
|
||||
not as Markdown and not as a concatenated JSON string.
|
||||
- Call propose_design_intent first. Its `intent` is the complete semantic
|
||||
planning JSON, without sketch coordinates, raw CAD code, storage IDs, or
|
||||
part-skill IDs. The backend chooses and persists part skills itself.
|
||||
- Do not call search_cdsl_library, read_cdsl_reference, or
|
||||
generate_cdsl_model until propose_design_intent returns an accepted
|
||||
`intent_id`. Pass that exact ID to generate_cdsl_model.
|
||||
- The generate_cdsl_model `cdsl` parameter is the complete machine-enforced
|
||||
schema. Satisfy its nested object and array types exactly; do not substitute
|
||||
a shorthand array for an object. For example, every hole position is
|
||||
@@ -399,12 +455,34 @@ Workflow limits:
|
||||
{response_language_instruction(user_text)}
|
||||
|
||||
You generate parameterized CDSL, never raw CAD source code. For new CAD requests:
|
||||
1. Search the local official CDSL library.
|
||||
2. Read at least one relevant reference when a match exists.
|
||||
3. Call generate_cdsl_model only when the request is sufficiently specified.
|
||||
4. Never claim success unless the tool returns a successful CDSL-only STEP and GLB artifact.
|
||||
1. Use the injected part-skill guidance, when present, only to establish the
|
||||
structural plan, feature dependency order, and parameter roles.
|
||||
2. Call propose_design_intent. A blocking question or capability gap must make
|
||||
the plan `needs_clarification`; then ask one concise user-facing question
|
||||
and do not call CDSL tools.
|
||||
3. Only after an accepted ready intent, search the local official CDSL library.
|
||||
Read at least one relevant reference when a match exists; samples provide
|
||||
schema-valid expressions, not higher-priority part intent.
|
||||
4. Generate a complete CDSL whose feature IDs, atomics, dependencies, profiles,
|
||||
and selector evidence exactly realize the accepted DesignIntent, then call
|
||||
generate_cdsl_model with its intent ID.
|
||||
5. Never claim success unless the tool returns a successful CDSL-only STEP and GLB artifact.
|
||||
|
||||
For a revision, call read_current_cdsl first and preserve unrelated features.
|
||||
Precedence is strict: explicit user request, then CDSL schema/runtime, then
|
||||
part-skill guidance, then CDSL-library examples. Part skills never authorize
|
||||
build123d source, an unknown atomic/profile, an invented selector, or a free-
|
||||
coordinate substitute for a capability the runtime cannot express. For an
|
||||
unsupported requested structure, ask one concise clarification question or
|
||||
state the blocker rather than fabricating geometry. If a part-family conflict
|
||||
is injected, preserve the current part unless the user explicitly requests a
|
||||
whole-part replacement. A primary-family conflict is a hard clarification
|
||||
stop: ask one concise question and do not call generate_cdsl_model until the
|
||||
user resolves it.
|
||||
|
||||
For a revision, call read_current_cdsl first and preserve unrelated features,
|
||||
then propose a revise DesignIntent with base_revision_id set to the current
|
||||
successful revision. Do not search references or generate CDSL before that plan
|
||||
is accepted.
|
||||
Do not output compiler_context, unknown_shape, complex_arc_shape, entities,
|
||||
contour_edges_mm, or contour_regions_mm. Use only self-contained named profiles
|
||||
and feature atomic IDs defined in the engine schema below. Read the engine
|
||||
@@ -415,6 +493,9 @@ dimensions or intent are missing. Ordinary explanations must not create CAD.
|
||||
Local skill:
|
||||
{skill}
|
||||
|
||||
DesignIntent planning recipe:
|
||||
{planning_recipe}
|
||||
|
||||
Local engine guide:
|
||||
{engine_readme}
|
||||
|
||||
@@ -423,15 +504,29 @@ Authoritative engine schema:
|
||||
|
||||
Supported named profile types:
|
||||
{supported_profiles}
|
||||
|
||||
Injected part-skill context:
|
||||
{part_skill_context or "No part-family skill guidance was selected for this request."}
|
||||
{viewer_selection_prompt(viewer_context, task_id)}
|
||||
"""
|
||||
|
||||
|
||||
def part_skill_root(settings: Settings) -> Path:
|
||||
return settings.engine_root.parent.parent / "agent" / "skills" / "cad-engine" / "references" / "part-skills"
|
||||
|
||||
|
||||
class AgentService:
|
||||
def __init__(self, settings: Settings, store: WorkspaceStore, library: CdslLibrary) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
settings: Settings,
|
||||
store: WorkspaceStore,
|
||||
library: CdslLibrary,
|
||||
part_skill_library: PartSkillLibrary | None = None,
|
||||
) -> None:
|
||||
self.settings = settings
|
||||
self.store = store
|
||||
self.library = library
|
||||
self.part_skill_library = part_skill_library or PartSkillLibrary(part_skill_root(settings))
|
||||
|
||||
async def stream(
|
||||
self,
|
||||
@@ -489,7 +584,29 @@ class AgentService:
|
||||
yield event("progress", {"step": "analyze_request", "label": "分析需求", "status": "running", "message": "正在整理当前会话和 CAD 需求。"})
|
||||
references: list[str] = []
|
||||
library_searches = 0
|
||||
model_messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt(self.settings, user_text, viewer_context, task_id)}]
|
||||
current_task = self.store.read_task(task_id) if task_id else None
|
||||
inherited_skill_ids = self.part_skill_library.inherited_from_task(current_task)
|
||||
part_skill_selection = self.part_skill_library.select(user_text, inherited_skill_ids)
|
||||
intent_state: dict[str, Any] = {
|
||||
"phase": "WAITING_FOR_INTENT",
|
||||
"design_intent_id": "",
|
||||
}
|
||||
yield event("progress", {
|
||||
"step": "select_part_skill",
|
||||
"label": "识别零件族",
|
||||
"status": "success",
|
||||
"message": "已完成零件族与辅助建模规则识别。",
|
||||
})
|
||||
model_messages: list[dict[str, Any]] = [{
|
||||
"role": "system",
|
||||
"content": system_prompt(
|
||||
self.settings,
|
||||
user_text,
|
||||
viewer_context,
|
||||
task_id,
|
||||
self.part_skill_library.render_context(part_skill_selection),
|
||||
),
|
||||
}]
|
||||
model_messages.extend(messages_for_model(messages))
|
||||
if attachment_message:
|
||||
model_messages.append({"role": "user", "content": attachment_message})
|
||||
@@ -591,14 +708,30 @@ class AgentService:
|
||||
"message": "Agent 正在调用本地 CAD 工具。",
|
||||
})
|
||||
try:
|
||||
result, generated = await self._run_tool(name, arguments, task_id, user_text, references)
|
||||
result, generated = await self._run_tool(
|
||||
name,
|
||||
arguments,
|
||||
task_id,
|
||||
user_text,
|
||||
references,
|
||||
part_skill_selection=part_skill_selection,
|
||||
intent_state=intent_state,
|
||||
)
|
||||
except (ValueError, RuntimeError) as error:
|
||||
if name == "generate_cdsl_model":
|
||||
code = str(getattr(error, "code", ""))
|
||||
if code in {"INVALID_DESIGN_INTENT", "INTENT_CDSL_MISMATCH", "DESIGN_INTENT_REQUIRED", "DESIGN_INTENT_BLOCKED"}:
|
||||
result = invalid_design_intent_result(error)
|
||||
generated = None
|
||||
if name in {"propose_design_intent", "generate_cdsl_model"} and code != "DESIGN_INTENT_BLOCKED":
|
||||
required_tool_name = name
|
||||
elif name == "generate_cdsl_model":
|
||||
result = invalid_cdsl_result(error)
|
||||
generated = None
|
||||
required_tool_name = name
|
||||
else:
|
||||
raise
|
||||
if result.get("task_id"):
|
||||
task_id = str(result["task_id"])
|
||||
if generated:
|
||||
task_id = generated["task_id"]
|
||||
model_messages.append({
|
||||
@@ -612,7 +745,14 @@ class AgentService:
|
||||
"status": "success" if result.get("ok", True) else "error",
|
||||
"message": user_visible_tool_message(result, user_text),
|
||||
})
|
||||
if name == "generate_cdsl_model" and result.get("ok"):
|
||||
if name == "propose_design_intent" and result.get("ok"):
|
||||
yield event("progress", {
|
||||
"step": "validate_design_intent",
|
||||
"label": "校验设计意图",
|
||||
"status": "success",
|
||||
"message": "设计意图已通过结构、依赖和能力边界校验。",
|
||||
})
|
||||
if name in {"propose_design_intent", "generate_cdsl_model"} and result.get("ok"):
|
||||
required_tool_name = None
|
||||
if generated:
|
||||
result_payload = {
|
||||
@@ -625,6 +765,8 @@ class AgentService:
|
||||
"parametersPath": generated.get("parameters_path"),
|
||||
"selectorPath": generated.get("selector_path"),
|
||||
"edgesPath": generated.get("edges_path"),
|
||||
"designIntentId": generated.get("design_intent_id"),
|
||||
"designIntentPath": generated.get("design_intent_path"),
|
||||
"summary": generated["summary"],
|
||||
"referenceIds": generated["reference_ids"],
|
||||
"engine": generated["engine"],
|
||||
@@ -632,6 +774,12 @@ class AgentService:
|
||||
successful_result = result_payload
|
||||
assistant_parts.append({"type": "data-cad-result", "data": result_payload})
|
||||
yield event("cad_result", result_payload)
|
||||
yield event("progress", {
|
||||
"step": "build_cad",
|
||||
"label": "构建 CAD",
|
||||
"status": "success",
|
||||
"message": "已通过 cdsl_only runtime 构建 STEP 和 GLB。",
|
||||
})
|
||||
if iteration == 7:
|
||||
error_payload = {"stage": "agent", "message": "Agent tool loop reached its safety limit."}
|
||||
assistant_parts.append({"type": "data-cad-error", "data": error_payload})
|
||||
@@ -749,15 +897,87 @@ class AgentService:
|
||||
raise RuntimeError(f"LLM request failed ({response.status_code}): {response.text[:800]}")
|
||||
return response.json()
|
||||
|
||||
async def _run_tool(self, name: str, arguments: dict[str, Any], task_id: str, request: str, references: list[str]) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
async def _run_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any],
|
||||
task_id: str,
|
||||
request: str,
|
||||
references: list[str],
|
||||
*,
|
||||
part_skill_selection: dict[str, Any] | None = None,
|
||||
intent_state: dict[str, Any] | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
state = intent_state if intent_state is not None else {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
||||
phase = str(state.get("phase") or "WAITING_FOR_INTENT")
|
||||
if name == "propose_design_intent":
|
||||
intent = arguments.get("intent")
|
||||
if not isinstance(intent, dict):
|
||||
raise ValueError("propose_design_intent requires an intent JSON object")
|
||||
if any(field in intent for field in ("intent_id", "created_at", "part_skill_ids", "part_skill_selection")):
|
||||
raise ValueError("DesignIntent audit fields are assigned only by the backend")
|
||||
summary = str(arguments.get("summary") or "").strip()
|
||||
assumptions = arguments.get("assumptions")
|
||||
if not summary or not isinstance(assumptions, list) or not all(isinstance(item, str) for item in assumptions):
|
||||
raise ValueError("propose_design_intent requires a summary and an array of string assumptions")
|
||||
selection = part_skill_selection or self.part_skill_library.select(request)
|
||||
if selection.get("conflict"):
|
||||
return {
|
||||
"ok": False,
|
||||
"code": "DESIGN_INTENT_BLOCKED",
|
||||
"message": str(selection["conflict"].get("message") or "The current part family must be clarified before planning."),
|
||||
}, None
|
||||
engine = load_engine(self.settings)
|
||||
current_task = self.store.read_task(task_id) if task_id else None
|
||||
current_revision_id = str((current_task or {}).get("current_revision") or "")
|
||||
if current_revision_id and intent.get("mode") != "revise":
|
||||
raise engine.DesignIntentError("INVALID_DESIGN_INTENT", "A task with a successful revision requires a revise DesignIntent")
|
||||
if intent.get("mode") == "revise" and not current_revision_id:
|
||||
raise engine.DesignIntentError("INVALID_DESIGN_INTENT", "A revise DesignIntent requires a current successful revision")
|
||||
normalized = deepcopy(intent)
|
||||
if assumptions:
|
||||
normalized["assumptions"] = list(dict.fromkeys([
|
||||
*normalized.get("assumptions", []),
|
||||
*(item.strip() for item in assumptions if item.strip()),
|
||||
]))
|
||||
normalized = engine.validate_design_intent(normalized, engine, current_revision_id=current_revision_id)
|
||||
persisted = self.store.create_design_intent(task_id or None, request, normalized, selection)
|
||||
state["design_intent_id"] = persisted["intent_id"]
|
||||
if normalized["status"] != "ready":
|
||||
state["phase"] = "WAITING_FOR_INTENT"
|
||||
return {
|
||||
"ok": False,
|
||||
"code": "DESIGN_INTENT_BLOCKED",
|
||||
"task_id": persisted["task_id"],
|
||||
"design_intent_id": persisted["intent_id"],
|
||||
"status": normalized["status"],
|
||||
"intent": persisted["intent"],
|
||||
"message": "The DesignIntent is saved but blocked. Ask the user only about its blocking question or capability gap.",
|
||||
}, None
|
||||
state["phase"] = "INTENT_ACCEPTED"
|
||||
return {
|
||||
"ok": True,
|
||||
"task_id": persisted["task_id"],
|
||||
"design_intent_id": persisted["intent_id"],
|
||||
"design_intent_path": persisted["path"],
|
||||
"status": "accepted",
|
||||
"intent": persisted["intent"],
|
||||
"summary": summary,
|
||||
}, None
|
||||
if name == "search_cdsl_library":
|
||||
if phase not in {"INTENT_ACCEPTED", "LIBRARY_REFERENCE", "WAITING_FOR_CDSL"}:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "Submit and receive an accepted DesignIntent before searching CDSL references."}, None
|
||||
results = self.library.search(str(arguments.get("query") or request), int(arguments.get("limit") or 5))
|
||||
state["phase"] = "LIBRARY_REFERENCE"
|
||||
return {"ok": True, "results": results}, None
|
||||
if name == "read_cdsl_reference":
|
||||
if phase not in {"INTENT_ACCEPTED", "LIBRARY_REFERENCE", "WAITING_FOR_CDSL"}:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "Submit and receive an accepted DesignIntent before reading CDSL references."}, None
|
||||
part_id = str(arguments.get("part_id") or "")
|
||||
sample = self.library.read_sample(part_id)
|
||||
if part_id not in references:
|
||||
references.append(part_id)
|
||||
state["phase"] = "WAITING_FOR_CDSL"
|
||||
return {"ok": True, "part_id": part_id, "cdsl": sample}, None
|
||||
if name == "read_current_cdsl":
|
||||
if not task_id:
|
||||
@@ -767,6 +987,17 @@ class AgentService:
|
||||
return {"ok": False, "message": "The current task has no successful CDSL revision."}, None
|
||||
return {"ok": True, "task_id": task_id, "cdsl": json.loads(path.read_text(encoding="utf-8"))}, None
|
||||
if name == "generate_cdsl_model":
|
||||
if phase not in {"INTENT_ACCEPTED", "LIBRARY_REFERENCE", "WAITING_FOR_CDSL"}:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "Submit and receive an accepted DesignIntent before generating CDSL."}, None
|
||||
design_intent_id = str(arguments.get("design_intent_id") or "")
|
||||
if not design_intent_id or design_intent_id != str(state.get("design_intent_id") or "") or not task_id:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "generate_cdsl_model must use the accepted DesignIntent ID for this task."}, None
|
||||
intent_record = self.store.read_design_intent(task_id, design_intent_id)
|
||||
if not intent_record or intent_record["record"].get("status") != "accepted":
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "The requested DesignIntent is not accepted for this task."}, None
|
||||
intent = intent_record["intent"]
|
||||
if intent.get("status") != "ready":
|
||||
return {"ok": False, "code": "DESIGN_INTENT_BLOCKED", "message": "The DesignIntent is blocked and cannot be built."}, None
|
||||
cdsl = arguments.get("cdsl")
|
||||
if isinstance(cdsl, str):
|
||||
cdsl = json.loads(cdsl)
|
||||
@@ -775,19 +1006,47 @@ class AgentService:
|
||||
# Reject malformed model output before build_revision allocates a task
|
||||
# directory or revision. build_revision will assign the real task ID.
|
||||
preflight_cdsl = {**cdsl, "part_id": str(cdsl.get("part_id") or "agent_preflight")}
|
||||
validate_cdsl(preflight_cdsl, load_engine(self.settings))
|
||||
engine = load_engine(self.settings)
|
||||
current_path = self.store.current_cdsl_path(task_id)
|
||||
current_cdsl = json.loads(current_path.read_text(encoding="utf-8")) if current_path else None
|
||||
engine.validate_intent_cdsl(intent, preflight_cdsl, engine, current_cdsl=current_cdsl)
|
||||
validate_cdsl(preflight_cdsl, engine)
|
||||
summary = str(arguments.get("summary") or "CDSL CAD model")
|
||||
yieldable = await asyncio.to_thread(
|
||||
build_revision,
|
||||
settings=self.settings,
|
||||
store=self.store,
|
||||
task_id=task_id or None,
|
||||
request=request,
|
||||
cdsl=cdsl,
|
||||
reference_ids=list(references),
|
||||
summary=summary,
|
||||
)
|
||||
return {"ok": True, "summary": summary, "task_id": yieldable["task_id"], "revision_id": yieldable["revision_id"]}, yieldable
|
||||
raw_assumptions = arguments.get("assumptions") or []
|
||||
if not isinstance(raw_assumptions, list) or not all(isinstance(item, str) for item in raw_assumptions):
|
||||
raise ValueError("generate_cdsl_model assumptions must be an array of strings")
|
||||
assumptions = [item.strip() for item in raw_assumptions if item.strip()]
|
||||
selection = part_skill_selection or self.part_skill_library.select(request)
|
||||
part_skill_audit = self.part_skill_library.audit(selection, cdsl, assumptions)
|
||||
state["phase"] = "BUILDING"
|
||||
try:
|
||||
yieldable = await asyncio.to_thread(
|
||||
build_revision,
|
||||
settings=self.settings,
|
||||
store=self.store,
|
||||
task_id=task_id or None,
|
||||
request=request,
|
||||
cdsl=cdsl,
|
||||
reference_ids=list(references),
|
||||
summary=summary,
|
||||
part_skills=part_skill_audit,
|
||||
generation_assumptions=assumptions,
|
||||
design_intent=intent,
|
||||
design_intent_path=str(intent_record["record"].get("path") or ""),
|
||||
)
|
||||
except Exception:
|
||||
# The accepted plan stays current so the model can submit a
|
||||
# corrected implementation without silently replanning.
|
||||
state["phase"] = "INTENT_ACCEPTED"
|
||||
raise
|
||||
state["phase"] = "COMPLETED"
|
||||
return {
|
||||
"ok": True,
|
||||
"summary": summary,
|
||||
"task_id": yieldable["task_id"],
|
||||
"revision_id": yieldable["revision_id"],
|
||||
"design_intent_id": design_intent_id,
|
||||
}, yieldable
|
||||
raise ValueError(f"Unknown agent tool: {name}")
|
||||
|
||||
@staticmethod
|
||||
@@ -800,7 +1059,8 @@ class AgentService:
|
||||
"search_cdsl_library": "检索 CDSL 模型库",
|
||||
"read_cdsl_reference": "读取 CDSL 参考模型",
|
||||
"read_current_cdsl": "读取当前 CDSL",
|
||||
"generate_cdsl_model": "生成 CDSL CAD 模型",
|
||||
"propose_design_intent": "生成设计意图",
|
||||
"generate_cdsl_model": "生成 CDSL",
|
||||
}.get(name, "调用 CAD 工具")
|
||||
|
||||
def _attachment_message(self, conversation: dict[str, Any], model: ProviderModel) -> list[dict[str, Any]] | str:
|
||||
|
||||
Reference in New Issue
Block a user