Spaces:
Running
Running
KaiWu commited on
Commit ·
557e100
1
Parent(s): 7574957
feat(core): 可配置 artifact root 与 CadQuery STL 预览导出
Browse files- config: 引入 ARTIFACT_ROOT,支持通过 AIGC_ARTIFACT_ROOT 环境变量
指向工作区外的持久化目录(容器/HF Spaces 场景)
- outputs: safe_path 与 workspace_relative 允许 ARTIFACT_ROOT
之下的路径,manifest 增补 generator/preview_path/warning 字段
- cadquery_tool: 每次导出 STEP 的同时生成 preview.stl,供 Web UI
的 3D 预览使用;导出失败以 warning 返回,不影响主流程
- prompts & lux3d_tool: 放宽 image_path 描述,覆盖 workspace 与
artifact 目录两类来源
- agent_core/config.py +7 -3
- agent_core/outputs.py +13 -5
- agent_core/prompts.py +1 -2
- agent_core/tools/cadquery_tool.py +14 -1
- agent_core/tools/lux3d_tool.py +2 -3
agent_core/config.py
CHANGED
|
@@ -15,8 +15,13 @@ if os.getenv("ANTHROPIC_BASE_URL"):
|
|
| 15 |
|
| 16 |
|
| 17 |
WORKDIR = Path.cwd()
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
STEP_SUFFIXES = (".step", ".stp")
|
| 21 |
IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".webp")
|
| 22 |
|
|
@@ -35,4 +40,3 @@ def get_client():
|
|
| 35 |
|
| 36 |
def get_model_id() -> str:
|
| 37 |
return os.environ["MODEL_ID"]
|
| 38 |
-
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
WORKDIR = Path.cwd()
|
| 18 |
+
ARTIFACT_ROOT = Path(os.getenv("AIGC_ARTIFACT_ROOT", "outputs"))
|
| 19 |
+
if not ARTIFACT_ROOT.is_absolute():
|
| 20 |
+
ARTIFACT_ROOT = WORKDIR / ARTIFACT_ROOT
|
| 21 |
+
ARTIFACT_ROOT = ARTIFACT_ROOT.resolve()
|
| 22 |
+
|
| 23 |
+
DEFAULT_CADQUERY_OUTPUT_PATH = str(ARTIFACT_ROOT / "model.step")
|
| 24 |
+
DEFAULT_LUX3D_OUTPUT_PATH = str(ARTIFACT_ROOT)
|
| 25 |
STEP_SUFFIXES = (".step", ".stp")
|
| 26 |
IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".webp")
|
| 27 |
|
|
|
|
| 40 |
|
| 41 |
def get_model_id() -> str:
|
| 42 |
return os.environ["MODEL_ID"]
|
|
|
agent_core/outputs.py
CHANGED
|
@@ -1,20 +1,26 @@
|
|
| 1 |
import json
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
from pathlib import Path
|
| 4 |
from uuid import uuid4
|
| 5 |
|
| 6 |
-
from agent_core.config import DEFAULT_LUX3D_OUTPUT_PATH, STEP_SUFFIXES, WORKDIR
|
| 7 |
|
| 8 |
|
| 9 |
def safe_path(p: str) -> Path:
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
raise ValueError(f"Path escapes workspace: {p}")
|
| 13 |
return path
|
| 14 |
|
| 15 |
|
| 16 |
def workspace_relative(path: Path) -> str:
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
def new_run_id() -> str:
|
|
@@ -92,13 +98,16 @@ def write_manifest(
|
|
| 92 |
manifest = {
|
| 93 |
"run_id": run_id,
|
| 94 |
"created_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
|
|
| 95 |
"prompt": prompt,
|
| 96 |
"requested_output_path": requested_output_path,
|
| 97 |
"output_path": workspace_relative(output_path),
|
|
|
|
| 98 |
"format": output_path.suffix.lstrip(".").lower(),
|
| 99 |
"code": code,
|
| 100 |
"stdout": payload.get("stdout", ""),
|
| 101 |
"stderr": payload.get("stderr", ""),
|
|
|
|
| 102 |
}
|
| 103 |
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 104 |
return manifest_path
|
|
@@ -135,4 +144,3 @@ def write_lux3d_manifest(
|
|
| 135 |
}
|
| 136 |
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 137 |
return manifest_path
|
| 138 |
-
|
|
|
|
| 1 |
import json
|
| 2 |
+
import os
|
| 3 |
from datetime import datetime
|
| 4 |
from pathlib import Path
|
| 5 |
from uuid import uuid4
|
| 6 |
|
| 7 |
+
from agent_core.config import ARTIFACT_ROOT, DEFAULT_LUX3D_OUTPUT_PATH, STEP_SUFFIXES, WORKDIR
|
| 8 |
|
| 9 |
|
| 10 |
def safe_path(p: str) -> Path:
|
| 11 |
+
raw_path = Path(p)
|
| 12 |
+
path = (raw_path if raw_path.is_absolute() else WORKDIR / raw_path).resolve()
|
| 13 |
+
if not path.is_relative_to(WORKDIR) and not path.is_relative_to(ARTIFACT_ROOT):
|
| 14 |
raise ValueError(f"Path escapes workspace: {p}")
|
| 15 |
return path
|
| 16 |
|
| 17 |
|
| 18 |
def workspace_relative(path: Path) -> str:
|
| 19 |
+
absolute = path if path.is_absolute() else WORKDIR / path
|
| 20 |
+
normalized = Path(os.path.normpath(str(absolute)))
|
| 21 |
+
if normalized.is_relative_to(WORKDIR):
|
| 22 |
+
return str(normalized.relative_to(WORKDIR))
|
| 23 |
+
return str(normalized)
|
| 24 |
|
| 25 |
|
| 26 |
def new_run_id() -> str:
|
|
|
|
| 98 |
manifest = {
|
| 99 |
"run_id": run_id,
|
| 100 |
"created_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
| 101 |
+
"generator": "cadquery",
|
| 102 |
"prompt": prompt,
|
| 103 |
"requested_output_path": requested_output_path,
|
| 104 |
"output_path": workspace_relative(output_path),
|
| 105 |
+
"preview_path": payload.get("preview_path"),
|
| 106 |
"format": output_path.suffix.lstrip(".").lower(),
|
| 107 |
"code": code,
|
| 108 |
"stdout": payload.get("stdout", ""),
|
| 109 |
"stderr": payload.get("stderr", ""),
|
| 110 |
+
"warning": payload.get("warning"),
|
| 111 |
}
|
| 112 |
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 113 |
return manifest_path
|
|
|
|
| 144 |
}
|
| 145 |
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 146 |
return manifest_path
|
|
|
agent_core/prompts.py
CHANGED
|
@@ -17,7 +17,7 @@ For CadQuery tasks:
|
|
| 17 |
- If execute_cadquery returns ok=false, inspect the structured error, fix the code, and call execute_cadquery again.
|
| 18 |
|
| 19 |
For image-to-3D tasks:
|
| 20 |
-
- The user must provide
|
| 21 |
- If the user asks for image-to-3D generation but does not provide an image path, ask for the image path. Do not guess.
|
| 22 |
- Call generate_3d_model with the image_path and optional output_path.
|
| 23 |
- Do not ask the user to read files, convert images to base64, call APIs, or download results manually.
|
|
@@ -26,4 +26,3 @@ When a tool returns ok=true, report output_path, run_dir, and manifest_path to t
|
|
| 26 |
|
| 27 |
Do not ask the user to run commands or create files manually.
|
| 28 |
""".strip()
|
| 29 |
-
|
|
|
|
| 17 |
- If execute_cadquery returns ok=false, inspect the structured error, fix the code, and call execute_cadquery again.
|
| 18 |
|
| 19 |
For image-to-3D tasks:
|
| 20 |
+
- The user must provide an image path from the workspace or configured artifact directory.
|
| 21 |
- If the user asks for image-to-3D generation but does not provide an image path, ask for the image path. Do not guess.
|
| 22 |
- Call generate_3d_model with the image_path and optional output_path.
|
| 23 |
- Do not ask the user to read files, convert images to base64, call APIs, or download results manually.
|
|
|
|
| 26 |
|
| 27 |
Do not ask the user to run commands or create files manually.
|
| 28 |
""".strip()
|
|
|
agent_core/tools/cadquery_tool.py
CHANGED
|
@@ -44,6 +44,7 @@ try:
|
|
| 44 |
request = json.loads(sys.stdin.read())
|
| 45 |
code = request["code"]
|
| 46 |
output_path = Path(request["output_path"])
|
|
|
|
| 47 |
except Exception as exc:
|
| 48 |
fail("input", exc)
|
| 49 |
sys.exit(0)
|
|
@@ -107,10 +108,19 @@ except Exception as exc:
|
|
| 107 |
fail("export", exc)
|
| 108 |
sys.exit(0)
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
payload = {
|
| 111 |
"ok": True,
|
| 112 |
"stage": "done",
|
| 113 |
"output_path": str(output_path),
|
|
|
|
|
|
|
| 114 |
"stdout": tail(stdout_buffer.getvalue()),
|
| 115 |
"stderr": tail(stderr_buffer.getvalue()),
|
| 116 |
}
|
|
@@ -132,6 +142,7 @@ def run_execute_cadquery(code: str, output_path: str = DEFAULT_CADQUERY_OUTPUT_P
|
|
| 132 |
})
|
| 133 |
|
| 134 |
run_dir, output, run_id = resolve_run_output(output_path)
|
|
|
|
| 135 |
output_root = run_dir.parent.parent
|
| 136 |
except Exception as exc:
|
| 137 |
return json_response({
|
|
@@ -147,6 +158,7 @@ def run_execute_cadquery(code: str, output_path: str = DEFAULT_CADQUERY_OUTPUT_P
|
|
| 147 |
request = {
|
| 148 |
"code": code,
|
| 149 |
"output_path": str(output),
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
try:
|
|
@@ -231,6 +243,8 @@ def run_execute_cadquery(code: str, output_path: str = DEFAULT_CADQUERY_OUTPUT_P
|
|
| 231 |
payload["run_id"] = run_id
|
| 232 |
payload["run_dir"] = workspace_relative(run_dir)
|
| 233 |
payload["output_path"] = workspace_relative(exported_path)
|
|
|
|
|
|
|
| 234 |
payload["manifest_path"] = workspace_relative(manifest_path)
|
| 235 |
payload["latest_path"] = workspace_relative(output_root / "latest")
|
| 236 |
if latest_warning:
|
|
@@ -263,4 +277,3 @@ TOOL_SCHEMA = {
|
|
| 263 |
"required": ["code"],
|
| 264 |
},
|
| 265 |
}
|
| 266 |
-
|
|
|
|
| 44 |
request = json.loads(sys.stdin.read())
|
| 45 |
code = request["code"]
|
| 46 |
output_path = Path(request["output_path"])
|
| 47 |
+
preview_path = Path(request["preview_path"])
|
| 48 |
except Exception as exc:
|
| 49 |
fail("input", exc)
|
| 50 |
sys.exit(0)
|
|
|
|
| 108 |
fail("export", exc)
|
| 109 |
sys.exit(0)
|
| 110 |
|
| 111 |
+
preview_error = None
|
| 112 |
+
try:
|
| 113 |
+
preview_path.parent.mkdir(parents=True, exist_ok=True)
|
| 114 |
+
cq.exporters.export(result, str(preview_path))
|
| 115 |
+
except Exception as exc:
|
| 116 |
+
preview_error = str(exc)
|
| 117 |
+
|
| 118 |
payload = {
|
| 119 |
"ok": True,
|
| 120 |
"stage": "done",
|
| 121 |
"output_path": str(output_path),
|
| 122 |
+
"preview_path": str(preview_path) if preview_path.exists() else None,
|
| 123 |
+
"warning": f"Failed to export STL preview: {preview_error}" if preview_error else None,
|
| 124 |
"stdout": tail(stdout_buffer.getvalue()),
|
| 125 |
"stderr": tail(stderr_buffer.getvalue()),
|
| 126 |
}
|
|
|
|
| 142 |
})
|
| 143 |
|
| 144 |
run_dir, output, run_id = resolve_run_output(output_path)
|
| 145 |
+
preview = run_dir / "preview.stl"
|
| 146 |
output_root = run_dir.parent.parent
|
| 147 |
except Exception as exc:
|
| 148 |
return json_response({
|
|
|
|
| 158 |
request = {
|
| 159 |
"code": code,
|
| 160 |
"output_path": str(output),
|
| 161 |
+
"preview_path": str(preview),
|
| 162 |
}
|
| 163 |
|
| 164 |
try:
|
|
|
|
| 243 |
payload["run_id"] = run_id
|
| 244 |
payload["run_dir"] = workspace_relative(run_dir)
|
| 245 |
payload["output_path"] = workspace_relative(exported_path)
|
| 246 |
+
if payload.get("preview_path"):
|
| 247 |
+
payload["preview_path"] = workspace_relative(Path(payload["preview_path"]))
|
| 248 |
payload["manifest_path"] = workspace_relative(manifest_path)
|
| 249 |
payload["latest_path"] = workspace_relative(output_root / "latest")
|
| 250 |
if latest_warning:
|
|
|
|
| 277 |
"required": ["code"],
|
| 278 |
},
|
| 279 |
}
|
|
|
agent_core/tools/lux3d_tool.py
CHANGED
|
@@ -285,7 +285,7 @@ TOOL_SCHEMA = {
|
|
| 285 |
"name": "generate_3d_model",
|
| 286 |
"description": textwrap.dedent("""
|
| 287 |
Generate a non-precise 3D model from a local image using the Lux3D image-to-3D API.
|
| 288 |
-
The input is a workspace
|
| 289 |
The tool reads the image, creates a Lux3D task, polls for completion, downloads the result, and writes a manifest.
|
| 290 |
""").strip(),
|
| 291 |
"input_schema": {
|
|
@@ -293,7 +293,7 @@ TOOL_SCHEMA = {
|
|
| 293 |
"properties": {
|
| 294 |
"image_path": {
|
| 295 |
"type": "string",
|
| 296 |
-
"description": "
|
| 297 |
},
|
| 298 |
"output_path": {
|
| 299 |
"type": "string",
|
|
@@ -303,4 +303,3 @@ TOOL_SCHEMA = {
|
|
| 303 |
"required": ["image_path"],
|
| 304 |
},
|
| 305 |
}
|
| 306 |
-
|
|
|
|
| 285 |
"name": "generate_3d_model",
|
| 286 |
"description": textwrap.dedent("""
|
| 287 |
Generate a non-precise 3D model from a local image using the Lux3D image-to-3D API.
|
| 288 |
+
The input is a workspace or artifact image path, not image bytes or base64 text.
|
| 289 |
The tool reads the image, creates a Lux3D task, polls for completion, downloads the result, and writes a manifest.
|
| 290 |
""").strip(),
|
| 291 |
"input_schema": {
|
|
|
|
| 293 |
"properties": {
|
| 294 |
"image_path": {
|
| 295 |
"type": "string",
|
| 296 |
+
"description": "Path to a source image file in the workspace or artifact directory. Supported formats: .png, .jpg, .jpeg, .webp.",
|
| 297 |
},
|
| 298 |
"output_path": {
|
| 299 |
"type": "string",
|
|
|
|
| 303 |
"required": ["image_path"],
|
| 304 |
},
|
| 305 |
}
|
|
|