KaiWu commited on
Commit
8295ef1
·
1 Parent(s): ab037c6
Files changed (4) hide show
  1. agent_loop.py +269 -54
  2. environment.yml +10 -0
  3. outputs/juanyangji.step +0 -0
  4. outputs/model.step +0 -0
agent_loop.py CHANGED
@@ -1,9 +1,15 @@
 
1
  import os
2
  import subprocess
 
 
3
  from pathlib import Path
4
 
5
- from anthropic import Anthropic
6
- from dotenv import load_dotenv
 
 
 
7
 
8
  load_dotenv(override=True)
9
 
@@ -11,10 +17,26 @@ if os.getenv("ANTHROPIC_BASE_URL"):
11
  os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
12
 
13
  WORKDIR = Path.cwd()
14
- client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
15
- MODEL = os.environ["MODEL_ID"]
16
 
17
- SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
 
20
  def safe_path(p: str) -> Path:
@@ -24,76 +46,269 @@ def safe_path(p: str) -> Path:
24
  return path
25
 
26
 
27
- def run_bash(command: str) -> str:
28
- dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
29
- if any(d in command for d in dangerous):
30
- return "Error: Dangerous command blocked"
31
- try:
32
- r = subprocess.run(command, shell=True, cwd=WORKDIR,
33
- capture_output=True, text=True, timeout=120)
34
- out = (r.stdout + r.stderr).strip()
35
- return out[:50000] if out else "(no output)"
36
- except subprocess.TimeoutExpired:
37
- return "Error: Timeout (120s)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
 
 
 
 
 
 
39
 
40
- def run_read(path: str, limit: int = None) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  try:
42
- text = safe_path(path).read_text()
43
- lines = text.splitlines()
44
- if limit and limit < len(lines):
45
- lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
46
- return "\n".join(lines)[:50000]
47
- except Exception as e:
48
- return f"Error: {e}"
 
 
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- def run_write(path: str, content: str) -> str:
52
  try:
53
- fp = safe_path(path)
54
- fp.parent.mkdir(parents=True, exist_ok=True)
55
- fp.write_text(content)
56
- return f"Wrote {len(content)} bytes to {path}"
57
- except Exception as e:
58
- return f"Error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- def run_edit(path: str, old_text: str, new_text: str) -> str:
62
  try:
63
- fp = safe_path(path)
64
- content = fp.read_text()
65
- if old_text not in content:
66
- return f"Error: Text not found in {path}"
67
- fp.write_text(content.replace(old_text, new_text, 1))
68
- return f"Edited {path}"
69
- except Exception as e:
70
- return f"Error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  # -- The dispatch map: {tool_name: handler} --
74
  TOOL_HANDLERS = {
75
- "bash": lambda **kw: run_bash(kw["command"]),
76
- "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
77
- "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
78
- "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
79
  }
80
 
81
  TOOLS = [
82
- {"name": "bash", "description": "Run a shell command.",
83
- "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
84
- {"name": "read_file", "description": "Read file contents.",
85
- "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
86
- {"name": "write_file", "description": "Write content to file.",
87
- "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
88
- {"name": "edit_file", "description": "Replace exact text in file.",
89
- "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  ]
91
 
92
 
93
  def agent_loop(messages: list):
 
 
 
94
  while True:
95
  response = client.messages.create(
96
- model=MODEL, system=SYSTEM, messages=messages,
97
  tools=TOOLS, max_tokens=8000,
98
  )
99
  messages.append({"role": "assistant", "content": response.content})
@@ -113,7 +328,7 @@ if __name__ == "__main__":
113
  history = []
114
  while True:
115
  try:
116
- query = input("\033[36ms02 >> \033[0m")
117
  except (EOFError, KeyboardInterrupt):
118
  break
119
  if query.strip().lower() in ("q", "exit", ""):
 
1
+ import json
2
  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
10
+ except ImportError:
11
+ def load_dotenv(*args, **kwargs):
12
+ return False
13
 
14
  load_dotenv(override=True)
15
 
 
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}.
23
+
24
+ Your only tool is execute. For CAD tasks:
25
+ - Write CadQuery Python code.
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
+
32
+ Do not ask the user to run commands or create files manually.
33
+ """.strip()
34
+
35
+
36
+ def get_client():
37
+ from anthropic import Anthropic
38
+
39
+ return Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
40
 
41
 
42
  def safe_path(p: str) -> Path:
 
46
  return path
47
 
48
 
49
+ CADQUERY_RUNNER = r"""
50
+ import contextlib
51
+ import io
52
+ import json
53
+ import sys
54
+ import traceback
55
+ from pathlib import Path
56
+
57
+ stdout_buffer = io.StringIO()
58
+ stderr_buffer = io.StringIO()
59
+
60
+
61
+ def tail(text, limit=4000):
62
+ if not text:
63
+ return ""
64
+ return text[-limit:]
65
+
66
+
67
+ def fail(stage, exc=None, error_type=None, error=None):
68
+ payload = {
69
+ "ok": False,
70
+ "stage": stage,
71
+ "error_type": error_type or (type(exc).__name__ if exc else "Error"),
72
+ "error": error or (str(exc) if exc else ""),
73
+ "traceback_tail": tail(traceback.format_exc() if exc else ""),
74
+ "stdout": tail(stdout_buffer.getvalue()),
75
+ "stderr": tail(stderr_buffer.getvalue()),
76
+ }
77
+ print(json.dumps(payload, ensure_ascii=False))
78
+
79
+
80
+ try:
81
+ request = json.loads(sys.stdin.read())
82
+ code = request["code"]
83
+ output_path = Path(request["output_path"])
84
+ except Exception as exc:
85
+ fail("input", exc)
86
+ sys.exit(0)
87
+
88
+ try:
89
+ import cadquery as cq
90
+ except Exception as exc:
91
+ fail("import", exc)
92
+ sys.exit(0)
93
+
94
+ try:
95
+ compiled = compile(code, "<cadquery_code>", "exec")
96
+ except Exception as exc:
97
+ fail("syntax", exc)
98
+ sys.exit(0)
99
+
100
+ namespace = {
101
+ "__name__": "__cadquery_user_code__",
102
+ "cq": cq,
103
+ }
104
 
105
+ try:
106
+ with contextlib.redirect_stdout(stdout_buffer), contextlib.redirect_stderr(stderr_buffer):
107
+ exec(compiled, namespace)
108
+ except Exception as exc:
109
+ fail("execution", exc)
110
+ sys.exit(0)
111
 
112
+ if "result" not in namespace:
113
+ fail("result", error_type="MissingResult", error="CadQuery code must assign the final model to variable 'result'.")
114
+ sys.exit(0)
115
+
116
+ result = namespace["result"]
117
+ if result is None:
118
+ fail("result", error_type="InvalidResult", error="'result' is None.")
119
+ sys.exit(0)
120
+
121
+ exportable_types = tuple(
122
+ value
123
+ for name in ("Workplane", "Shape", "Assembly", "Sketch")
124
+ for value in [getattr(cq, name, None)]
125
+ if isinstance(value, type)
126
+ )
127
+
128
+ is_exportable = isinstance(result, exportable_types)
129
+ if isinstance(result, (list, tuple)) and result:
130
+ is_exportable = all(isinstance(item, exportable_types) for item in result)
131
+
132
+ if not is_exportable:
133
+ fail(
134
+ "result",
135
+ error_type="InvalidResultType",
136
+ error=f"'result' must be a CadQuery object or a non-empty list/tuple of CadQuery objects, got {type(result).__name__}.",
137
+ )
138
+ sys.exit(0)
139
+
140
+ try:
141
+ output_path.parent.mkdir(parents=True, exist_ok=True)
142
+ cq.exporters.export(result, str(output_path))
143
+ except Exception as exc:
144
+ fail("export", exc)
145
+ sys.exit(0)
146
+
147
+ payload = {
148
+ "ok": True,
149
+ "stage": "done",
150
+ "output_path": str(output_path),
151
+ "stdout": tail(stdout_buffer.getvalue()),
152
+ "stderr": tail(stderr_buffer.getvalue()),
153
+ }
154
+ print(json.dumps(payload, ensure_ascii=False))
155
+ """
156
+
157
+
158
+ 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({
166
+ "ok": False,
167
+ "stage": "input",
168
+ "error_type": "EmptyCode",
169
+ "error": "execute requires non-empty CadQuery code.",
170
+ "traceback_tail": "",
171
+ "stdout": "",
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,
189
+ "stage": "input",
190
+ "error_type": type(exc).__name__,
191
+ "error": str(exc),
192
+ "traceback_tail": "",
193
+ "stdout": "",
194
+ "stderr": "",
195
+ })
196
+
197
+ request = {
198
+ "code": code,
199
+ "output_path": str(output),
200
+ }
201
 
 
202
  try:
203
+ process = subprocess.run(
204
+ [sys.executable, "-c", CADQUERY_RUNNER],
205
+ input=json.dumps(request, ensure_ascii=False),
206
+ cwd=WORKDIR,
207
+ capture_output=True,
208
+ text=True,
209
+ timeout=120,
210
+ )
211
+ except subprocess.TimeoutExpired as exc:
212
+ return _json_response({
213
+ "ok": False,
214
+ "stage": "execution",
215
+ "error_type": "TimeoutExpired",
216
+ "error": "CadQuery execution timed out after 120 seconds.",
217
+ "traceback_tail": "",
218
+ "stdout": exc.stdout[-4000:] if exc.stdout else "",
219
+ "stderr": exc.stderr[-4000:] if exc.stderr else "",
220
+ })
221
+ except Exception as exc:
222
+ return _json_response({
223
+ "ok": False,
224
+ "stage": "subprocess",
225
+ "error_type": type(exc).__name__,
226
+ "error": str(exc),
227
+ "traceback_tail": "",
228
+ "stdout": "",
229
+ "stderr": "",
230
+ })
231
 
232
+ raw_output = process.stdout.strip()
233
+ if not raw_output:
234
+ return _json_response({
235
+ "ok": False,
236
+ "stage": "subprocess",
237
+ "error_type": "NoRunnerOutput",
238
+ "error": "CadQuery runner returned no JSON output.",
239
+ "traceback_tail": "",
240
+ "stdout": "",
241
+ "stderr": process.stderr[-4000:],
242
+ })
243
 
 
244
  try:
245
+ payload = json.loads(raw_output.splitlines()[-1])
246
+ except Exception as exc:
247
+ return _json_response({
248
+ "ok": False,
249
+ "stage": "subprocess",
250
+ "error_type": type(exc).__name__,
251
+ "error": f"Failed to parse CadQuery runner output: {exc}",
252
+ "traceback_tail": "",
253
+ "stdout": raw_output[-4000:],
254
+ "stderr": process.stderr[-4000:],
255
+ })
256
+
257
+ if process.returncode != 0 and payload.get("ok") is not False:
258
+ payload = {
259
+ "ok": False,
260
+ "stage": "subprocess",
261
+ "error_type": "RunnerFailed",
262
+ "error": f"CadQuery runner exited with code {process.returncode}.",
263
+ "traceback_tail": "",
264
+ "stdout": raw_output[-4000:],
265
+ "stderr": process.stderr[-4000:],
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 = [
280
+ {
281
+ "name": "execute",
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",
289
+ "properties": {
290
+ "code": {
291
+ "type": "string",
292
+ "description": "CadQuery Python code. It must assign the final model to variable 'result'.",
293
+ },
294
+ "output_path": {
295
+ "type": "string",
296
+ "description": "Optional workspace-relative STEP path. Defaults to outputs/model.step.",
297
+ },
298
+ },
299
+ "required": ["code"],
300
+ },
301
+ },
302
  ]
303
 
304
 
305
  def agent_loop(messages: list):
306
+ client = get_client()
307
+ model = os.environ["MODEL_ID"]
308
+
309
  while True:
310
  response = client.messages.create(
311
+ model=model, system=SYSTEM, messages=messages,
312
  tools=TOOLS, max_tokens=8000,
313
  )
314
  messages.append({"role": "assistant", "content": response.content})
 
328
  history = []
329
  while True:
330
  try:
331
+ query = input("\033[36mcad agent >> \033[0m")
332
  except (EOFError, KeyboardInterrupt):
333
  break
334
  if query.strip().lower() in ("q", "exit", ""):
environment.yml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ name: aigc
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python
6
+ - cadquery
7
+ - pip
8
+ - pip:
9
+ - anthropic
10
+ - python-dotenv
outputs/juanyangji.step ADDED
The diff for this file is too large to render. See raw diff
 
outputs/model.step ADDED
The diff for this file is too large to render. See raw diff