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 CHANGED
@@ -15,8 +15,13 @@ if os.getenv("ANTHROPIC_BASE_URL"):
15
 
16
 
17
  WORKDIR = Path.cwd()
18
- DEFAULT_CADQUERY_OUTPUT_PATH = "outputs/model.step"
19
- DEFAULT_LUX3D_OUTPUT_PATH = "outputs"
 
 
 
 
 
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
- path = (WORKDIR / p).resolve()
11
- if not path.is_relative_to(WORKDIR):
 
12
  raise ValueError(f"Path escapes workspace: {p}")
13
  return path
14
 
15
 
16
  def workspace_relative(path: Path) -> str:
17
- return str(path.relative_to(WORKDIR))
 
 
 
 
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 a workspace-relative image path.
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-relative 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,7 +293,7 @@ TOOL_SCHEMA = {
293
  "properties": {
294
  "image_path": {
295
  "type": "string",
296
- "description": "Workspace-relative path to a source image file. Supported formats: .png, .jpg, .jpeg, .webp.",
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
  }