KaiWu commited on
Commit
b5baed0
·
1 Parent(s): 8295ef1

规范outpath的路径

Browse files
Files changed (5) hide show
  1. .env +0 -61
  2. .gitignore +27 -0
  3. agent_loop.py +120 -18
  4. outputs/juanyangji.step +0 -0
  5. outputs/model.step +0 -0
.env DELETED
@@ -1,61 +0,0 @@
1
- # API Key (required)
2
- # Get yours at: https://console.anthropic.com/
3
- ANTHROPIC_API_KEY=sk-9wTK68ZO58_s4w5_SUsR7w
4
-
5
- # Model ID (required)
6
- MODEL_ID=gemini-3-flash-thinking
7
-
8
- # Base URL (optional, for Anthropic-compatible providers)
9
- ANTHROPIC_BASE_URL=https://coding.qunhequnhe.com
10
-
11
- # =============================================================================
12
- # Anthropic-compatible providers
13
- #
14
- # Provider MODEL_ID SWE-bench TB2 Base URL
15
- # --------------- -------------------- --------- ------ -------------------
16
- # Anthropic claude-sonnet-4-6 79.6% 59.1% (default)
17
- # MiniMax MiniMax-M2.5 80.2% - see below
18
- # GLM (Zhipu) glm-5 77.8% - see below
19
- # Kimi (Moonshot) kimi-k2.5 76.8% - see below
20
- # DeepSeek deepseek-chat 73.0% - see below
21
- # (V3.2)
22
- #
23
- # SWE-bench = SWE-bench Verified (Feb 2026)
24
- # TB2 = Terminal-Bench 2.0 (Feb 2026)
25
- # =============================================================================
26
-
27
- # ---- International ----
28
-
29
- # MiniMax https://www.minimax.io
30
- # ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
31
- # MODEL_ID=MiniMax-M2.5
32
-
33
- # GLM (Zhipu) https://z.ai
34
- # ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
35
- # MODEL_ID=glm-5
36
-
37
- # Kimi (Moonshot) https://platform.moonshot.ai
38
- # ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic
39
- # MODEL_ID=kimi-k2.5
40
-
41
- # DeepSeek https://platform.deepseek.com
42
- # ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
43
- # MODEL_ID=deepseek-chat
44
-
45
- # ---- China mainland ----
46
-
47
- # MiniMax https://platform.minimax.io
48
- # ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
49
- # MODEL_ID=MiniMax-M2.5
50
-
51
- # GLM (Zhipu) https://open.bigmodel.cn
52
- # ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic
53
- # MODEL_ID=glm-5
54
-
55
- # Kimi (Moonshot) https://platform.moonshot.cn
56
- # ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic
57
- # MODEL_ID=kimi-k2.5
58
-
59
- # DeepSeek (no regional split, same endpoint globally)
60
- # ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
61
- # MODEL_ID=deepseek-chat
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Local secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Python caches and test output
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ # Local virtual environments
17
+ .venv/
18
+ venv/
19
+ env/
20
+
21
+ # Generated CAD artifacts
22
+ outputs/
23
+
24
+ # OS and editor files
25
+ .DS_Store
26
+ .idea/
27
+ .vscode/
agent_loop.py CHANGED
@@ -3,7 +3,9 @@ import os
3
  import subprocess
4
  import sys
5
  import textwrap
 
6
  from pathlib import Path
 
7
 
8
  try:
9
  from dotenv import load_dotenv
@@ -17,6 +19,8 @@ if os.getenv("ANTHROPIC_BASE_URL"):
17
  os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
18
 
19
  WORKDIR = Path.cwd()
 
 
20
 
21
  SYSTEM = f"""
22
  You are a CAD generation agent at {WORKDIR}.
@@ -26,6 +30,7 @@ Your only tool is execute. For CAD tasks:
26
  - The code must assign the final model to a variable named result.
27
  - You may use cadquery as cq; it is pre-imported by the tool.
28
  - Call execute with the code and optional output_path.
 
29
  - If execute returns ok=false, inspect the structured error, fix the code, and call execute again.
30
  - When execute returns ok=true, report the output_path to the user.
31
 
@@ -46,6 +51,77 @@ def safe_path(p: str) -> Path:
46
  return path
47
 
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  CADQUERY_RUNNER = r"""
50
  import contextlib
51
  import io
@@ -159,7 +235,7 @@ def _json_response(payload: dict) -> str:
159
  return json.dumps(payload, ensure_ascii=False, indent=2)
160
 
161
 
162
- def run_execute(code: str, output_path: str = "outputs/model.step") -> str:
163
  try:
164
  if not code or not code.strip():
165
  return _json_response({
@@ -172,17 +248,8 @@ def run_execute(code: str, output_path: str = "outputs/model.step") -> str:
172
  "stderr": "",
173
  })
174
 
175
- output = safe_path(output_path)
176
- if output.suffix.lower() not in (".step", ".stp"):
177
- return _json_response({
178
- "ok": False,
179
- "stage": "input",
180
- "error_type": "UnsupportedOutputFormat",
181
- "error": "Only STEP output is supported in this version. Use a .step or .stp path.",
182
- "traceback_tail": "",
183
- "stdout": "",
184
- "stderr": "",
185
- })
186
  except Exception as exc:
187
  return _json_response({
188
  "ok": False,
@@ -266,14 +333,32 @@ def run_execute(code: str, output_path: str = "outputs/model.step") -> str:
266
  }
267
 
268
  if payload.get("ok") and payload.get("output_path"):
269
- payload["output_path"] = str(Path(payload["output_path"]).relative_to(WORKDIR))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
  return _json_response(payload)
272
 
273
 
274
  # -- The dispatch map: {tool_name: handler} --
275
  TOOL_HANDLERS = {
276
- "execute": lambda **kw: run_execute(kw["code"], kw.get("output_path", "outputs/model.step")),
277
  }
278
 
279
  TOOLS = [
@@ -282,7 +367,9 @@ TOOLS = [
282
  "description": textwrap.dedent("""
283
  Execute CadQuery Python code and export the result as a STEP file.
284
  The code must assign the final CadQuery model to a variable named result.
285
- cadquery is pre-imported as cq. output_path is optional and defaults to outputs/model.step.
 
 
286
  """).strip(),
287
  "input_schema": {
288
  "type": "object",
@@ -293,7 +380,7 @@ TOOLS = [
293
  },
294
  "output_path": {
295
  "type": "string",
296
- "description": "Optional workspace-relative STEP path. Defaults to outputs/model.step.",
297
  },
298
  },
299
  "required": ["code"],
@@ -302,6 +389,13 @@ TOOLS = [
302
  ]
303
 
304
 
 
 
 
 
 
 
 
305
  def agent_loop(messages: list):
306
  client = get_client()
307
  model = os.environ["MODEL_ID"]
@@ -315,10 +409,18 @@ def agent_loop(messages: list):
315
  if response.stop_reason != "tool_use":
316
  return
317
  results = []
 
318
  for block in response.content:
319
  if block.type == "tool_use":
320
- handler = TOOL_HANDLERS.get(block.name)
321
- output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
 
 
 
 
 
 
 
322
  print(f"> {block.name}: {output[:200]}")
323
  results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
324
  messages.append({"role": "user", "content": results})
 
3
  import subprocess
4
  import sys
5
  import textwrap
6
+ from datetime import datetime
7
  from pathlib import Path
8
+ from uuid import uuid4
9
 
10
  try:
11
  from dotenv import load_dotenv
 
19
  os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
20
 
21
  WORKDIR = Path.cwd()
22
+ DEFAULT_OUTPUT_PATH = "outputs/model.step"
23
+ STEP_SUFFIXES = (".step", ".stp")
24
 
25
  SYSTEM = f"""
26
  You are a CAD generation agent at {WORKDIR}.
 
30
  - The code must assign the final model to a variable named result.
31
  - You may use cadquery as cq; it is pre-imported by the tool.
32
  - Call execute with the code and optional output_path.
33
+ - Each successful execute writes into a unique run directory to avoid overwriting previous models.
34
  - If execute returns ok=false, inspect the structured error, fix the code, and call execute again.
35
  - When execute returns ok=true, report the output_path to the user.
36
 
 
51
  return path
52
 
53
 
54
+ def workspace_relative(path: Path) -> str:
55
+ return str(path.relative_to(WORKDIR))
56
+
57
+
58
+ def new_run_id() -> str:
59
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
60
+ return f"{timestamp}_{uuid4().hex[:6]}"
61
+
62
+
63
+ def resolve_run_output(requested_output_path: str) -> tuple[Path, Path, str]:
64
+ requested_path = safe_path(requested_output_path)
65
+ suffix = requested_path.suffix.lower()
66
+
67
+ if suffix in STEP_SUFFIXES:
68
+ output_root = requested_path.parent
69
+ output_name = requested_path.name
70
+ elif suffix:
71
+ raise ValueError("Only STEP output is supported in this version. Use a .step/.stp file path or a directory path.")
72
+ else:
73
+ output_root = requested_path
74
+ output_name = "model.step"
75
+
76
+ for _ in range(20):
77
+ run_id = new_run_id()
78
+ run_dir = output_root / "runs" / run_id
79
+ if not run_dir.exists():
80
+ return run_dir, run_dir / output_name, run_id
81
+
82
+ raise RuntimeError("Failed to allocate a unique output run directory.")
83
+
84
+
85
+ def update_latest_link(output_root: Path, run_dir: Path) -> str | None:
86
+ latest = output_root / "latest"
87
+ try:
88
+ if latest.is_symlink() or latest.is_file():
89
+ latest.unlink()
90
+ elif latest.exists():
91
+ return f"Skipped latest link because {workspace_relative(latest)} already exists and is not a symlink."
92
+
93
+ latest.symlink_to(Path("runs") / run_dir.name, target_is_directory=True)
94
+ except OSError as exc:
95
+ return f"Failed to update latest link: {exc}"
96
+
97
+ return None
98
+
99
+
100
+ def write_manifest(
101
+ run_dir: Path,
102
+ run_id: str,
103
+ requested_output_path: str,
104
+ output_path: Path,
105
+ code: str,
106
+ prompt: str | None,
107
+ payload: dict,
108
+ ) -> Path:
109
+ manifest_path = run_dir / "manifest.json"
110
+ manifest = {
111
+ "run_id": run_id,
112
+ "created_at": datetime.now().astimezone().isoformat(timespec="seconds"),
113
+ "prompt": prompt,
114
+ "requested_output_path": requested_output_path,
115
+ "output_path": workspace_relative(output_path),
116
+ "format": output_path.suffix.lstrip(".").lower(),
117
+ "code": code,
118
+ "stdout": payload.get("stdout", ""),
119
+ "stderr": payload.get("stderr", ""),
120
+ }
121
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
122
+ return manifest_path
123
+
124
+
125
  CADQUERY_RUNNER = r"""
126
  import contextlib
127
  import io
 
235
  return json.dumps(payload, ensure_ascii=False, indent=2)
236
 
237
 
238
+ def run_execute(code: str, output_path: str = DEFAULT_OUTPUT_PATH, prompt: str | None = None) -> str:
239
  try:
240
  if not code or not code.strip():
241
  return _json_response({
 
248
  "stderr": "",
249
  })
250
 
251
+ run_dir, output, run_id = resolve_run_output(output_path)
252
+ output_root = run_dir.parent.parent
 
 
 
 
 
 
 
 
 
253
  except Exception as exc:
254
  return _json_response({
255
  "ok": False,
 
333
  }
334
 
335
  if payload.get("ok") and payload.get("output_path"):
336
+ exported_path = Path(payload["output_path"])
337
+ manifest_path = write_manifest(
338
+ run_dir=run_dir,
339
+ run_id=run_id,
340
+ requested_output_path=output_path,
341
+ output_path=exported_path,
342
+ code=code,
343
+ prompt=prompt,
344
+ payload=payload,
345
+ )
346
+ latest_warning = update_latest_link(output_root, run_dir)
347
+
348
+ payload["run_id"] = run_id
349
+ payload["run_dir"] = workspace_relative(run_dir)
350
+ payload["output_path"] = workspace_relative(exported_path)
351
+ payload["manifest_path"] = workspace_relative(manifest_path)
352
+ payload["latest_path"] = workspace_relative(output_root / "latest")
353
+ if latest_warning:
354
+ payload["warning"] = latest_warning
355
 
356
  return _json_response(payload)
357
 
358
 
359
  # -- The dispatch map: {tool_name: handler} --
360
  TOOL_HANDLERS = {
361
+ "execute": lambda **kw: run_execute(kw["code"], kw.get("output_path", DEFAULT_OUTPUT_PATH)),
362
  }
363
 
364
  TOOLS = [
 
367
  "description": textwrap.dedent("""
368
  Execute CadQuery Python code and export the result as a STEP file.
369
  The code must assign the final CadQuery model to a variable named result.
370
+ cadquery is pre-imported as cq.
371
+ output_path is optional. If it is a .step/.stp file path, the file name is used inside a unique run directory.
372
+ If it is a directory path, model.step is written inside a unique run directory under that root.
373
  """).strip(),
374
  "input_schema": {
375
  "type": "object",
 
380
  },
381
  "output_path": {
382
  "type": "string",
383
+ "description": "Optional workspace-relative STEP file path or output directory. Defaults to outputs/model.step.",
384
  },
385
  },
386
  "required": ["code"],
 
389
  ]
390
 
391
 
392
+ def latest_user_prompt(messages: list) -> str | None:
393
+ for message in reversed(messages):
394
+ if message.get("role") == "user" and isinstance(message.get("content"), str):
395
+ return message["content"]
396
+ return None
397
+
398
+
399
  def agent_loop(messages: list):
400
  client = get_client()
401
  model = os.environ["MODEL_ID"]
 
409
  if response.stop_reason != "tool_use":
410
  return
411
  results = []
412
+ prompt = latest_user_prompt(messages)
413
  for block in response.content:
414
  if block.type == "tool_use":
415
+ if block.name == "execute":
416
+ output = run_execute(
417
+ code=block.input["code"],
418
+ output_path=block.input.get("output_path", DEFAULT_OUTPUT_PATH),
419
+ prompt=prompt,
420
+ )
421
+ else:
422
+ handler = TOOL_HANDLERS.get(block.name)
423
+ output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
424
  print(f"> {block.name}: {output[:200]}")
425
  results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
426
  messages.append({"role": "user", "content": results})
outputs/juanyangji.step DELETED
The diff for this file is too large to render. See raw diff
 
outputs/model.step DELETED
The diff for this file is too large to render. See raw diff