YSMlearnsCode
commited on
Commit
·
41d6e2b
0
Parent(s):
clean hf integration
Browse files- .github/workflows/ci.yml +22 -0
- .gitignore +218 -0
- .python-version +1 -0
- LICENSE +201 -0
- README.md +78 -0
- app/app.py +83 -0
- app/app_prompt.txt +10 -0
- app/process.py +157 -0
- generated/result_script.py +88 -0
- main.py +54 -0
- prompts/base_instruction.txt +18 -0
- prompts/example_code.txt +228 -0
- pyproject.toml +16 -0
- requirements.txt +313 -0
- src/__init__.py +0 -0
- src/llm_client.py +54 -0
- src/rag_builder.py +110 -0
- src/run_freecad.py +11 -0
- uv.lock +0 -0
.github/workflows/ci.yml
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Sync to Hugging Face hub
|
2 |
+
|
3 |
+
on:
|
4 |
+
push:
|
5 |
+
branches: [main]
|
6 |
+
workflow_dispatch:
|
7 |
+
|
8 |
+
jobs:
|
9 |
+
sync-to-hub:
|
10 |
+
runs-on: ubuntu-latest
|
11 |
+
|
12 |
+
steps:
|
13 |
+
- uses: actions/checkout@v3
|
14 |
+
with:
|
15 |
+
fetch-depth: 0
|
16 |
+
lfs: true
|
17 |
+
|
18 |
+
- name: Push to Hugging Face Space
|
19 |
+
env:
|
20 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
21 |
+
run: |
|
22 |
+
git push https://Yas1n:[email protected]/spaces/Yas1n/CADomatic main
|
.gitignore
ADDED
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[codz]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py.cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# UV
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
#uv.lock
|
102 |
+
|
103 |
+
# poetry
|
104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
106 |
+
# commonly ignored for libraries.
|
107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
108 |
+
#poetry.lock
|
109 |
+
#poetry.toml
|
110 |
+
|
111 |
+
# pdm
|
112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
115 |
+
#pdm.lock
|
116 |
+
#pdm.toml
|
117 |
+
.pdm-python
|
118 |
+
.pdm-build/
|
119 |
+
|
120 |
+
# pixi
|
121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
122 |
+
#pixi.lock
|
123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
125 |
+
.pixi
|
126 |
+
|
127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
128 |
+
__pypackages__/
|
129 |
+
|
130 |
+
# Celery stuff
|
131 |
+
celerybeat-schedule
|
132 |
+
celerybeat.pid
|
133 |
+
|
134 |
+
# SageMath parsed files
|
135 |
+
*.sage.py
|
136 |
+
|
137 |
+
# Environments
|
138 |
+
.env
|
139 |
+
.envrc
|
140 |
+
.venv
|
141 |
+
env/
|
142 |
+
venv/
|
143 |
+
ENV/
|
144 |
+
env.bak/
|
145 |
+
venv.bak/
|
146 |
+
|
147 |
+
# Spyder project settings
|
148 |
+
.spyderproject
|
149 |
+
.spyproject
|
150 |
+
|
151 |
+
# Rope project settings
|
152 |
+
.ropeproject
|
153 |
+
|
154 |
+
# mkdocs documentation
|
155 |
+
/site
|
156 |
+
|
157 |
+
# mypy
|
158 |
+
.mypy_cache/
|
159 |
+
.dmypy.json
|
160 |
+
dmypy.json
|
161 |
+
|
162 |
+
# Pyre type checker
|
163 |
+
.pyre/
|
164 |
+
|
165 |
+
# pytype static type analyzer
|
166 |
+
.pytype/
|
167 |
+
|
168 |
+
# Cython debug symbols
|
169 |
+
cython_debug/
|
170 |
+
|
171 |
+
# PyCharm
|
172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
176 |
+
#.idea/
|
177 |
+
|
178 |
+
# Abstra
|
179 |
+
# Abstra is an AI-powered process automation framework.
|
180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
181 |
+
# Learn more at https://abstra.io/docs
|
182 |
+
.abstra/
|
183 |
+
|
184 |
+
# Visual Studio Code
|
185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
189 |
+
# .vscode/
|
190 |
+
|
191 |
+
# Ruff stuff:
|
192 |
+
.ruff_cache/
|
193 |
+
|
194 |
+
# PyPI configuration file
|
195 |
+
.pypirc
|
196 |
+
|
197 |
+
# Cursor
|
198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
201 |
+
.cursorignore
|
202 |
+
.cursorindexingignore
|
203 |
+
|
204 |
+
# Marimo
|
205 |
+
marimo/_static/
|
206 |
+
marimo/_lsp/
|
207 |
+
__marimo__/
|
208 |
+
|
209 |
+
/vectorstore/
|
210 |
+
|
211 |
+
# Ignore FAISS and pickle index files
|
212 |
+
/vectorstore/index.faiss
|
213 |
+
/vectorstore/index.pkl
|
214 |
+
|
215 |
+
# Ignore the generated model
|
216 |
+
app/generated/
|
217 |
+
app/generated/model.FCStd
|
218 |
+
app/generated/model.obj
|
.python-version
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
3.11
|
LICENSE
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Apache License
|
2 |
+
Version 2.0, January 2004
|
3 |
+
http://www.apache.org/licenses/
|
4 |
+
|
5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6 |
+
|
7 |
+
1. Definitions.
|
8 |
+
|
9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
11 |
+
|
12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13 |
+
the copyright owner that is granting the License.
|
14 |
+
|
15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
16 |
+
other entities that control, are controlled by, or are under common
|
17 |
+
control with that entity. For the purposes of this definition,
|
18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
19 |
+
direction or management of such entity, whether by contract or
|
20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22 |
+
|
23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24 |
+
exercising permissions granted by this License.
|
25 |
+
|
26 |
+
"Source" form shall mean the preferred form for making modifications,
|
27 |
+
including but not limited to software source code, documentation
|
28 |
+
source, and configuration files.
|
29 |
+
|
30 |
+
"Object" form shall mean any form resulting from mechanical
|
31 |
+
transformation or translation of a Source form, including but
|
32 |
+
not limited to compiled object code, generated documentation,
|
33 |
+
and conversions to other media types.
|
34 |
+
|
35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
36 |
+
Object form, made available under the License, as indicated by a
|
37 |
+
copyright notice that is included in or attached to the work
|
38 |
+
(an example is provided in the Appendix below).
|
39 |
+
|
40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41 |
+
form, that is based on (or derived from) the Work and for which the
|
42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
44 |
+
of this License, Derivative Works shall not include works that remain
|
45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46 |
+
the Work and Derivative Works thereof.
|
47 |
+
|
48 |
+
"Contribution" shall mean any work of authorship, including
|
49 |
+
the original version of the Work and any modifications or additions
|
50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
54 |
+
means any form of electronic, verbal, or written communication sent
|
55 |
+
to the Licensor or its representatives, including but not limited to
|
56 |
+
communication on electronic mailing lists, source code control systems,
|
57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
59 |
+
excluding communication that is conspicuously marked or otherwise
|
60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
61 |
+
|
62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
64 |
+
subsequently incorporated within the Work.
|
65 |
+
|
66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
71 |
+
Work and such Derivative Works in Source or Object form.
|
72 |
+
|
73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76 |
+
(except as stated in this section) patent license to make, have made,
|
77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78 |
+
where such license applies only to those patent claims licensable
|
79 |
+
by such Contributor that are necessarily infringed by their
|
80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
82 |
+
institute patent litigation against any entity (including a
|
83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84 |
+
or a Contribution incorporated within the Work constitutes direct
|
85 |
+
or contributory patent infringement, then any patent licenses
|
86 |
+
granted to You under this License for that Work shall terminate
|
87 |
+
as of the date such litigation is filed.
|
88 |
+
|
89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
90 |
+
Work or Derivative Works thereof in any medium, with or without
|
91 |
+
modifications, and in Source or Object form, provided that You
|
92 |
+
meet the following conditions:
|
93 |
+
|
94 |
+
(a) You must give any other recipients of the Work or
|
95 |
+
Derivative Works a copy of this License; and
|
96 |
+
|
97 |
+
(b) You must cause any modified files to carry prominent notices
|
98 |
+
stating that You changed the files; and
|
99 |
+
|
100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
101 |
+
that You distribute, all copyright, patent, trademark, and
|
102 |
+
attribution notices from the Source form of the Work,
|
103 |
+
excluding those notices that do not pertain to any part of
|
104 |
+
the Derivative Works; and
|
105 |
+
|
106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107 |
+
distribution, then any Derivative Works that You distribute must
|
108 |
+
include a readable copy of the attribution notices contained
|
109 |
+
within such NOTICE file, excluding those notices that do not
|
110 |
+
pertain to any part of the Derivative Works, in at least one
|
111 |
+
of the following places: within a NOTICE text file distributed
|
112 |
+
as part of the Derivative Works; within the Source form or
|
113 |
+
documentation, if provided along with the Derivative Works; or,
|
114 |
+
within a display generated by the Derivative Works, if and
|
115 |
+
wherever such third-party notices normally appear. The contents
|
116 |
+
of the NOTICE file are for informational purposes only and
|
117 |
+
do not modify the License. You may add Your own attribution
|
118 |
+
notices within Derivative Works that You distribute, alongside
|
119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
120 |
+
that such additional attribution notices cannot be construed
|
121 |
+
as modifying the License.
|
122 |
+
|
123 |
+
You may add Your own copyright statement to Your modifications and
|
124 |
+
may provide additional or different license terms and conditions
|
125 |
+
for use, reproduction, or distribution of Your modifications, or
|
126 |
+
for any such Derivative Works as a whole, provided Your use,
|
127 |
+
reproduction, and distribution of the Work otherwise complies with
|
128 |
+
the conditions stated in this License.
|
129 |
+
|
130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
132 |
+
by You to the Licensor shall be under the terms and conditions of
|
133 |
+
this License, without any additional terms or conditions.
|
134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135 |
+
the terms of any separate license agreement you may have executed
|
136 |
+
with Licensor regarding such Contributions.
|
137 |
+
|
138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
140 |
+
except as required for reasonable and customary use in describing the
|
141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
142 |
+
|
143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144 |
+
agreed to in writing, Licensor provides the Work (and each
|
145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147 |
+
implied, including, without limitation, any warranties or conditions
|
148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150 |
+
appropriateness of using or redistributing the Work and assume any
|
151 |
+
risks associated with Your exercise of permissions under this License.
|
152 |
+
|
153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
154 |
+
whether in tort (including negligence), contract, or otherwise,
|
155 |
+
unless required by applicable law (such as deliberate and grossly
|
156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157 |
+
liable to You for damages, including any direct, indirect, special,
|
158 |
+
incidental, or consequential damages of any character arising as a
|
159 |
+
result of this License or out of the use or inability to use the
|
160 |
+
Work (including but not limited to damages for loss of goodwill,
|
161 |
+
work stoppage, computer failure or malfunction, or any and all
|
162 |
+
other commercial damages or losses), even if such Contributor
|
163 |
+
has been advised of the possibility of such damages.
|
164 |
+
|
165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168 |
+
or other liability obligations and/or rights consistent with this
|
169 |
+
License. However, in accepting such obligations, You may act only
|
170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171 |
+
of any other Contributor, and only if You agree to indemnify,
|
172 |
+
defend, and hold each Contributor harmless for any liability
|
173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
174 |
+
of your accepting any such warranty or additional liability.
|
175 |
+
|
176 |
+
END OF TERMS AND CONDITIONS
|
177 |
+
|
178 |
+
APPENDIX: How to apply the Apache License to your work.
|
179 |
+
|
180 |
+
To apply the Apache License to your work, attach the following
|
181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
182 |
+
replaced with your own identifying information. (Don't include
|
183 |
+
the brackets!) The text should be enclosed in the appropriate
|
184 |
+
comment syntax for the file format. We also recommend that a
|
185 |
+
file or class name and description of purpose be included on the
|
186 |
+
same "printed page" as the copyright notice for easier
|
187 |
+
identification within third-party archives.
|
188 |
+
|
189 |
+
Copyright [yyyy] [name of copyright owner]
|
190 |
+
|
191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192 |
+
you may not use this file except in compliance with the License.
|
193 |
+
You may obtain a copy of the License at
|
194 |
+
|
195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
196 |
+
|
197 |
+
Unless required by applicable law or agreed to in writing, software
|
198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200 |
+
See the License for the specific language governing permissions and
|
201 |
+
limitations under the License.
|
README.md
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# CADomatic 🛠️
|
2 |
+
**From prompt to CAD**
|
3 |
+
|
4 |
+
CADomatic is a Python-based tool that generates editable parametric CAD scripts for FreeCAD. Instead of creating static 3D models, CADomatic produces **fully customizable Python scripts** that build CAD geometry — allowing engineers to programmatically define parts, reuse templates, and iterate fast.
|
5 |
+
|
6 |
+
---
|
7 |
+
|
8 |
+
## 🔍 What It Does
|
9 |
+
|
10 |
+

|
11 |
+
|
12 |
+
- ✅ **Generates editable FreeCAD Python scripts** for parts like screws, nuts, fasteners, and more
|
13 |
+
- ✅ Each script can be modified for custom parameters (length, diameter, features, etc.)
|
14 |
+
- ✅ Outputs native `.py` scripts which use FreeCAD’s API to build geometry
|
15 |
+
- ✅ Enables **version-controlled**, reusable, parametric CAD pipelines
|
16 |
+
- ✅ Eliminates the need for manual modeling in the FreeCAD GUI
|
17 |
+
|
18 |
+
---
|
19 |
+
## 💡 Why Use CADomatic?
|
20 |
+
|
21 |
+
- 🔁 Automate repetitive CAD tasks
|
22 |
+
- 🧱 Build part libraries as **code**
|
23 |
+
- 🧪 Integrate CAD into testing or CI workflows
|
24 |
+
- 🔧 Customize geometry by changing script parameters
|
25 |
+
- 📐 Keep models lightweight and editable at the code level
|
26 |
+
|
27 |
+
---
|
28 |
+
## 💬 Example Prompts
|
29 |
+
|
30 |
+
Here are some example natural language prompts you can use to generate CAD scripts with CADomatic:
|
31 |
+
|
32 |
+
- "Build a flange with a 100mm outer diameter, 10mm thickness, and 6 bolt holes evenly spaced."
|
33 |
+
- "Make a cylindrical spacer, 20mm diameter and 30mm height, with a 5mm through hole."
|
34 |
+
- "Produce a washer with an outer diameter of 25mm and an inner diameter of 10mm."
|
35 |
+
- "Design a toy car with a rectangular box as the body and 4 circular wheels attached to the sides of the box."
|
36 |
+
|
37 |
+
These prompts will be converted into editable Python scripts that you can modify and reuse.
|
38 |
+
|
39 |
+
|
40 |
+
---
|
41 |
+
⚠️ **This is the first version** of CADomatic — a flash of what's possible.
|
42 |
+
Future versions under development will include:
|
43 |
+
- Improved **LLM-driven script generation**
|
44 |
+
- A **dedicated user interface** for part selection and parameter tuning
|
45 |
+
- More robust **template and geometry libraries**
|
46 |
+
---
|
47 |
+
## 🚀 How to Use CADomatic
|
48 |
+
|
49 |
+
### ✅ Prerequisites
|
50 |
+
- Python 3.11+
|
51 |
+
- [FreeCAD](https://www.freecad.org/downloads.php) (must be installed and added to PATH)
|
52 |
+
- [uv](https://github.com/astral-sh/uv) (install via `pip install uv`)
|
53 |
+
|
54 |
+
### ⚙️ Setup
|
55 |
+
```bash
|
56 |
+
git clone https://github.com/yas1nsyed/CADomatic.git
|
57 |
+
cd CADomatic
|
58 |
+
|
59 |
+
# Create and activate virtual environment
|
60 |
+
python -m venv .venv
|
61 |
+
.venv\Scripts\activate
|
62 |
+
|
63 |
+
# Install dependencies
|
64 |
+
uv pip install -r requirements.txt
|
65 |
+
```
|
66 |
+
|
67 |
+
- 🔐 [Set Up Gemini API Key](https://aistudio.google.com/app/apikey)
|
68 |
+
- Create a .env file in the project root:
|
69 |
+
|
70 |
+
- ▶️ Run CADomatic
|
71 |
+
```bash
|
72 |
+
# Run the program
|
73 |
+
uv run main.py
|
74 |
+
```
|
75 |
+
- Enter your prompt (e.g., "Create a 10mm cube with 2mm hole").
|
76 |
+
- FreeCAD will auto-launch with your generated model.
|
77 |
+
|
78 |
+
---
|
app/app.py
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from pathlib import Path
|
3 |
+
from process import generate_script_and_run
|
4 |
+
|
5 |
+
# === File Paths ===
|
6 |
+
generated_dir = Path(__file__).parent / "generated"
|
7 |
+
fcstd_file = generated_dir / "model.FCStd"
|
8 |
+
obj_file = generated_dir / "model.obj"
|
9 |
+
|
10 |
+
# Remove stale files
|
11 |
+
for file in ["generated_model.FCStd", "generated_model.obj"]:
|
12 |
+
fpath = generated_dir / file
|
13 |
+
if fpath.exists():
|
14 |
+
fpath.unlink()
|
15 |
+
|
16 |
+
# === CAD Generation Callback ===
|
17 |
+
def prepare_outputs(description):
|
18 |
+
generate_script_and_run(description)
|
19 |
+
return str(fcstd_file), str(obj_file), str(obj_file)
|
20 |
+
|
21 |
+
# === UI with Custom CSS ===
|
22 |
+
with gr.Blocks(css="""
|
23 |
+
#generate-btn .gr-button {
|
24 |
+
background-color: #28a745 !important;
|
25 |
+
color: white !important;
|
26 |
+
}
|
27 |
+
|
28 |
+
#fcstd-download .gr-button,
|
29 |
+
#obj-download .gr-button {
|
30 |
+
background-color: #fd7e14 !important;
|
31 |
+
color: white !important;
|
32 |
+
}
|
33 |
+
|
34 |
+
.footer-text {
|
35 |
+
text-align: center;
|
36 |
+
font-size: 0.85rem;
|
37 |
+
margin-top: 2em;
|
38 |
+
color: #888;
|
39 |
+
}
|
40 |
+
|
41 |
+
.footer-text a {
|
42 |
+
color: #fd7e14;
|
43 |
+
text-decoration: none;
|
44 |
+
}
|
45 |
+
|
46 |
+
.footer-text a:hover {
|
47 |
+
text-decoration: underline;
|
48 |
+
}
|
49 |
+
""") as demo:
|
50 |
+
|
51 |
+
gr.Markdown("<h1 style='text-align: center;'> CADomatic - FreeCAD Script Generator</h1>")
|
52 |
+
gr.Markdown("Generate 3D models by describing them in plain English. Powered by FreeCAD and LLMs.")
|
53 |
+
|
54 |
+
input_text = gr.Textbox(
|
55 |
+
label="📝 Describe your FreeCAD part",
|
56 |
+
lines=3,
|
57 |
+
placeholder="e.g., Create a 10mm thick cylinder with radius 5mm..."
|
58 |
+
)
|
59 |
+
|
60 |
+
generate_btn = gr.Button("Generate", elem_id="generate-btn")
|
61 |
+
|
62 |
+
model_preview = gr.Model3D(label="🔍 3D Preview", height=400)
|
63 |
+
|
64 |
+
with gr.Row():
|
65 |
+
fcstd_download = gr.DownloadButton("Download .FCStd file", elem_id="fcstd-download")
|
66 |
+
obj_download = gr.DownloadButton("Download .obj file", elem_id="obj-download")
|
67 |
+
|
68 |
+
generate_btn.click(
|
69 |
+
fn=prepare_outputs,
|
70 |
+
inputs=input_text,
|
71 |
+
outputs=[fcstd_download, obj_download, model_preview]
|
72 |
+
)
|
73 |
+
|
74 |
+
gr.HTML("""
|
75 |
+
<div class='footer-text'>
|
76 |
+
<strong>Note:</strong> CADomatic is still under development and still needs to be refined. For best results, run it locally. Toggle to view all in downloaded .FCStd file to see the generated part<br>
|
77 |
+
Please refresh and run if there is no preview<br>
|
78 |
+
View the source on <a href="https://github.com/yas1nsyed/CADomatic" target="_blank">GitHub</a>.
|
79 |
+
</div>
|
80 |
+
""")
|
81 |
+
|
82 |
+
if __name__ == "__main__":
|
83 |
+
demo.launch()
|
app/app_prompt.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
- Generate FreeCAD 1.0 Python code to create:
|
2 |
+
- Requirements:
|
3 |
+
- Must work in FreeCAD command-line mode
|
4 |
+
- No GUI functions (FreeCADGui)
|
5 |
+
- Ensure valid geometry creation
|
6 |
+
- DO NOT wrap the logic inside any `def` functions or `if __name__ == "__main__":` blocks.
|
7 |
+
- Always include `App.newDocument(...)` to create a document.
|
8 |
+
- Always use top-level, immediately executable statements.
|
9 |
+
- Use the `Part` module to construct valid geometry.
|
10 |
+
- Recompute the document after adding objects using `doc.recompute()`.
|
app/process.py
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import os
|
3 |
+
import subprocess
|
4 |
+
import platform
|
5 |
+
from pathlib import Path
|
6 |
+
|
7 |
+
# Define paths
|
8 |
+
APP_DIR = Path(__file__).parent.resolve()
|
9 |
+
PROJECT_ROOT = APP_DIR.parent
|
10 |
+
GEN_DIR = APP_DIR / "generated"
|
11 |
+
GEN_SCRIPT = GEN_DIR / "result_script.py"
|
12 |
+
OBJ_PATH = GEN_DIR / "model.obj"
|
13 |
+
FCSTD_PATH = GEN_DIR / "model.FCStd"
|
14 |
+
PREVIEW_PATH = GEN_DIR / "preview.txt" # Dummy preview
|
15 |
+
|
16 |
+
sys.path.append(str(PROJECT_ROOT))
|
17 |
+
|
18 |
+
from src.llm_client import prompt_llm
|
19 |
+
|
20 |
+
def get_freecad_cmd():
|
21 |
+
"""Get FreeCAD command path for Windows/Linux"""
|
22 |
+
if platform.system() == "Windows":
|
23 |
+
for path in [
|
24 |
+
r"C:\Program Files\FreeCAD 1.0\bin\freecadcmd.exe",
|
25 |
+
r"C:\Program Files\FreeCAD\bin\freecadcmd.exe"
|
26 |
+
]:
|
27 |
+
if os.path.exists(path):
|
28 |
+
return path
|
29 |
+
return "freecadcmd" # Assume it's in PATH
|
30 |
+
|
31 |
+
# Escape Windows paths manually outside the f-string
|
32 |
+
fcstd_path_str = str(FCSTD_PATH).replace("\\", "\\\\")
|
33 |
+
obj_path_str = str(OBJ_PATH).replace("\\", "\\\\")
|
34 |
+
preview_path_str = str(PREVIEW_PATH).replace("\\", "\\\\")
|
35 |
+
|
36 |
+
# Updated export logic with debug logging
|
37 |
+
EXPORT_SNIPPET = f"""
|
38 |
+
import Mesh
|
39 |
+
import os
|
40 |
+
|
41 |
+
print(">>> Running export snippet...")
|
42 |
+
|
43 |
+
try:
|
44 |
+
if App.ActiveDocument:
|
45 |
+
print(">>> Active document found")
|
46 |
+
doc = App.ActiveDocument
|
47 |
+
doc.recompute()
|
48 |
+
doc.saveAs(r"{fcstd_path_str}")
|
49 |
+
print(">>> Document saved")
|
50 |
+
|
51 |
+
objs = []
|
52 |
+
for obj in doc.Objects:
|
53 |
+
if hasattr(obj, "Shape"):
|
54 |
+
objs.append(obj)
|
55 |
+
|
56 |
+
print(f">>> Found {{len(objs)}} shape object(s)")
|
57 |
+
|
58 |
+
if objs:
|
59 |
+
Mesh.export(objs, r"{obj_path_str}")
|
60 |
+
print(">>> Exported OBJ file")
|
61 |
+
|
62 |
+
with open(r"{preview_path_str}", "w") as f:
|
63 |
+
f.write("Preview placeholder")
|
64 |
+
|
65 |
+
else:
|
66 |
+
print(">>> No active document!")
|
67 |
+
|
68 |
+
except Exception as e:
|
69 |
+
App.Console.PrintError("Export failed: " + str(e) + "\\n")
|
70 |
+
"""
|
71 |
+
|
72 |
+
def generate_script_and_run(user_input: str):
|
73 |
+
# Load modular prompt parts
|
74 |
+
base_prompt_path = PROJECT_ROOT / "prompts/base_instruction.txt"
|
75 |
+
example_prompt_path = PROJECT_ROOT / "prompts/example_code.txt"
|
76 |
+
app_prompt_path = APP_DIR / "app_prompt.txt"
|
77 |
+
|
78 |
+
base_instruction = base_prompt_path.read_text(encoding="utf-8").strip()
|
79 |
+
example_code = example_prompt_path.read_text(encoding="utf-8").strip()
|
80 |
+
app_prompt = app_prompt_path.read_text(encoding="utf-8").strip()
|
81 |
+
|
82 |
+
# Build prompt
|
83 |
+
prompt = (
|
84 |
+
f"{base_instruction}\n\n"
|
85 |
+
f"{example_code}\n\n"
|
86 |
+
f"{app_prompt}\n\n"
|
87 |
+
f"User request: {user_input.strip()}"
|
88 |
+
)
|
89 |
+
|
90 |
+
# Generate LLM code
|
91 |
+
generated_code = prompt_llm(prompt)
|
92 |
+
|
93 |
+
# Auto-inject App.newDocument if missing
|
94 |
+
if "App.newDocument" not in generated_code:
|
95 |
+
generated_code = "App.newDocument('Unnamed')\n" + generated_code
|
96 |
+
|
97 |
+
# Clean up markdown formatting
|
98 |
+
# Clean up markdown formatting
|
99 |
+
if generated_code.startswith("```"):
|
100 |
+
generated_code = generated_code[generated_code.find("\n") + 1:].rsplit("```", 1)[0]
|
101 |
+
|
102 |
+
# Unwrap if __name__ == "__main__" blocks (FreeCAD won't execute them)
|
103 |
+
if "__name__" in generated_code and "def " in generated_code:
|
104 |
+
lines = generated_code.splitlines()
|
105 |
+
in_main = False
|
106 |
+
unwrapped = []
|
107 |
+
for line in lines:
|
108 |
+
if line.strip().startswith("if __name__"):
|
109 |
+
in_main = True
|
110 |
+
continue
|
111 |
+
if in_main:
|
112 |
+
# Remove leading indentation (usually 4 spaces)
|
113 |
+
unwrapped.append(line[4:] if line.startswith(" ") else line)
|
114 |
+
else:
|
115 |
+
unwrapped.append(line)
|
116 |
+
generated_code = "\n".join(unwrapped)
|
117 |
+
|
118 |
+
# Create output folder if needed
|
119 |
+
GEN_DIR.mkdir(exist_ok=True)
|
120 |
+
|
121 |
+
# Build final script with export logic
|
122 |
+
full_script = f"{generated_code.strip()}\n\n{EXPORT_SNIPPET}"
|
123 |
+
|
124 |
+
# Write script
|
125 |
+
GEN_SCRIPT.write_text(full_script, encoding="utf-8")
|
126 |
+
|
127 |
+
# Delete old outputs
|
128 |
+
for path in [FCSTD_PATH, OBJ_PATH, PREVIEW_PATH]:
|
129 |
+
if path.exists():
|
130 |
+
path.unlink()
|
131 |
+
|
132 |
+
# Run FreeCAD
|
133 |
+
freecad_cmd = get_freecad_cmd()
|
134 |
+
try:
|
135 |
+
result = subprocess.run(
|
136 |
+
[freecad_cmd, str(GEN_SCRIPT)],
|
137 |
+
cwd=APP_DIR,
|
138 |
+
capture_output=True,
|
139 |
+
text=True,
|
140 |
+
timeout=60
|
141 |
+
)
|
142 |
+
|
143 |
+
# Log output
|
144 |
+
(GEN_DIR / "run_stdout.txt").write_text(result.stdout or "", encoding="utf-8")
|
145 |
+
(GEN_DIR / "run_stderr.txt").write_text(result.stderr or "", encoding="utf-8")
|
146 |
+
|
147 |
+
if result.returncode != 0:
|
148 |
+
raise RuntimeError(result.stderr or result.stdout)
|
149 |
+
|
150 |
+
if not FCSTD_PATH.exists() or not OBJ_PATH.exists():
|
151 |
+
raise FileNotFoundError("One or more output files not created.")
|
152 |
+
|
153 |
+
except Exception as e:
|
154 |
+
FCSTD_PATH.write_text(f"Error: {e}")
|
155 |
+
PREVIEW_PATH.write_text(f"Error: {e}")
|
156 |
+
|
157 |
+
return str(FCSTD_PATH), str(PREVIEW_PATH)
|
generated/result_script.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import FreeCAD as App
|
2 |
+
import FreeCADGui as Gui
|
3 |
+
from FreeCAD import Vector
|
4 |
+
import math
|
5 |
+
|
6 |
+
|
7 |
+
def createFlangeAssembly():
|
8 |
+
doc = App.newDocument("Flange")
|
9 |
+
|
10 |
+
# === Parameters ===
|
11 |
+
FLANGE_OUTER_DIAMETER = 100.0
|
12 |
+
FLANGE_THICKNESS = 7.5
|
13 |
+
BORE_INNER_DIAMETER = 50.0
|
14 |
+
NECK_HEIGHT = 15.0
|
15 |
+
NECK_OUTER_DIAMETER = 60.0 # Keeping this from the template as not specified in prompt
|
16 |
+
NUM_BOLT_HOLES = 6
|
17 |
+
BOLT_HOLE_DIAMETER = 12.0
|
18 |
+
PCD = 75.0
|
19 |
+
|
20 |
+
total_height = FLANGE_THICKNESS + NECK_HEIGHT
|
21 |
+
|
22 |
+
# === 1. Create flange base ===
|
23 |
+
flange = doc.addObject("Part::Cylinder", "Flange_Base")
|
24 |
+
flange.Radius = FLANGE_OUTER_DIAMETER / 2
|
25 |
+
flange.Height = FLANGE_THICKNESS
|
26 |
+
|
27 |
+
# === 2. Cut central bore from flange ===
|
28 |
+
bore = doc.addObject("Part::Cylinder", "Central_Bore_Cutter")
|
29 |
+
bore.Radius = BORE_INNER_DIAMETER / 2
|
30 |
+
bore.Height = FLANGE_THICKNESS
|
31 |
+
bore_cut = doc.addObject("Part::Cut", "Flange_with_Bore")
|
32 |
+
bore_cut.Base = flange
|
33 |
+
bore_cut.Tool = bore
|
34 |
+
|
35 |
+
# === 3. Create neck ===
|
36 |
+
neck_outer = doc.addObject("Part::Cylinder", "Neck_Outer")
|
37 |
+
neck_outer.Radius = NECK_OUTER_DIAMETER / 2
|
38 |
+
neck_outer.Height = NECK_HEIGHT
|
39 |
+
neck_outer.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
|
40 |
+
|
41 |
+
neck_inner = doc.addObject("Part::Cylinder", "Neck_Inner_Cutter")
|
42 |
+
neck_inner.Radius = BORE_INNER_DIAMETER / 2
|
43 |
+
neck_inner.Height = NECK_HEIGHT
|
44 |
+
neck_inner.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
|
45 |
+
|
46 |
+
neck_hollow = doc.addObject("Part::Cut", "Hollow_Neck_Part")
|
47 |
+
neck_hollow.Base = neck_outer
|
48 |
+
neck_hollow.Tool = neck_inner
|
49 |
+
|
50 |
+
# === 4. Fuse flange (with central hole) and neck ===
|
51 |
+
fused = doc.addObject("Part::Fuse", "Flange_and_Neck_Fused")
|
52 |
+
fused.Base = bore_cut
|
53 |
+
fused.Tool = neck_hollow
|
54 |
+
|
55 |
+
# === 5. Cut bolt holes sequentially ===
|
56 |
+
current_shape_obj = fused # Reference to the last cut object in the tree
|
57 |
+
bolt_radius = BOLT_HOLE_DIAMETER / 2
|
58 |
+
bolt_circle_radius = PCD / 2
|
59 |
+
|
60 |
+
for i in range(NUM_BOLT_HOLES):
|
61 |
+
angle_deg = 360 * i / NUM_BOLT_HOLES
|
62 |
+
angle_rad = math.radians(angle_deg)
|
63 |
+
x = bolt_circle_radius * math.cos(angle_rad)
|
64 |
+
y = bolt_circle_radius * math.sin(angle_rad)
|
65 |
+
|
66 |
+
hole_cutter = doc.addObject("Part::Cylinder", f"Bolt_Hole_Cutter_{i+1:02d}")
|
67 |
+
hole_cutter.Radius = bolt_radius
|
68 |
+
hole_cutter.Height = total_height
|
69 |
+
hole_cutter.Placement.Base = Vector(x, y, 0)
|
70 |
+
|
71 |
+
cut_obj = doc.addObject("Part::Cut", f"Flange_with_Hole_{i+1:02d}")
|
72 |
+
cut_obj.Base = current_shape_obj
|
73 |
+
cut_obj.Tool = hole_cutter
|
74 |
+
current_shape_obj = cut_obj # Update for the next iteration
|
75 |
+
|
76 |
+
# === 6. Final result ===
|
77 |
+
# The final object is current_shape_obj after all cuts
|
78 |
+
|
79 |
+
# Recompute and fit view
|
80 |
+
doc.recompute()
|
81 |
+
Gui.activeDocument().activeView().viewAxometric()
|
82 |
+
Gui.SendMsgToActiveView("ViewFit")
|
83 |
+
|
84 |
+
return doc
|
85 |
+
|
86 |
+
if __name__ == "__main__":
|
87 |
+
createFlangeAssembly()
|
88 |
+
|
main.py
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from src.llm_client import prompt_llm
|
2 |
+
from pathlib import Path
|
3 |
+
import subprocess
|
4 |
+
|
5 |
+
# File paths
|
6 |
+
prompt_base = Path("prompts/base_instruction.txt")
|
7 |
+
prompt_examples = Path("prompts/example_code.txt")
|
8 |
+
GEN_SCRIPT = Path("generated/result_script.py")
|
9 |
+
RUN_SCRIPT = Path("src/run_freecad.py")
|
10 |
+
|
11 |
+
# Snippet to adjust FreeCAD GUI view
|
12 |
+
GUI_SNIPPET = """
|
13 |
+
import FreeCADGui
|
14 |
+
FreeCADGui.activeDocument().activeView().viewAxometric()
|
15 |
+
FreeCADGui.SendMsgToActiveView("ViewFit")
|
16 |
+
"""
|
17 |
+
|
18 |
+
def main():
|
19 |
+
# Step 1: Get user input
|
20 |
+
user_input = input("Describe your FreeCAD part: ")
|
21 |
+
|
22 |
+
# Step 2: Build prompt
|
23 |
+
base_prompt = prompt_base.read_text().strip()
|
24 |
+
example_prompt = prompt_examples.read_text().strip()
|
25 |
+
full_prompt = f"{base_prompt}\n\nExamples:\n{example_prompt}\n\nUser instruction: {user_input.strip()}"
|
26 |
+
|
27 |
+
|
28 |
+
# Step 3: Get response from LLM
|
29 |
+
generated_code = prompt_llm(full_prompt)
|
30 |
+
|
31 |
+
# Step 4: Clean up ```python code blocks if any
|
32 |
+
if generated_code.startswith("```"):
|
33 |
+
generated_code = generated_code.strip("`\n ")
|
34 |
+
if generated_code.lower().startswith("python"):
|
35 |
+
generated_code = generated_code[len("python"):].lstrip()
|
36 |
+
|
37 |
+
# Step 5: Append GUI snippet for viewing
|
38 |
+
generated_code += "\n\n" + GUI_SNIPPET
|
39 |
+
|
40 |
+
# Step 6: Save to script file
|
41 |
+
GEN_SCRIPT.write_text(generated_code)
|
42 |
+
print(f"\n Code generated and written to {GEN_SCRIPT}")
|
43 |
+
|
44 |
+
# Step 7: Execute the script via FreeCAD
|
45 |
+
print("Running FreeCAD with the generated script...")
|
46 |
+
try:
|
47 |
+
subprocess.run(["python", str(RUN_SCRIPT)], check=True)
|
48 |
+
except subprocess.CalledProcessError as e:
|
49 |
+
print(f"❌ FreeCAD script execution failed with error code: {e.returncode}")
|
50 |
+
except Exception as e:
|
51 |
+
print(f"❌ Error running run_freecad.py: {e}")
|
52 |
+
|
53 |
+
if __name__ == "__main__":
|
54 |
+
main()
|
prompts/base_instruction.txt
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
You are an assistant that generates valid Python code for FreeCAD.
|
2 |
+
|
3 |
+
- Use Part and Sketcher modules.
|
4 |
+
- Do not use GUI commands unless required.
|
5 |
+
- Make sure the code can be executed using FreeCAD.
|
6 |
+
- make code according to FreeCAD 1.0
|
7 |
+
- make the parts using the freecad part workbench, not partdesign workbench
|
8 |
+
- ensure all dimensions are correct and parts dont intersect each other
|
9 |
+
- Always use from FreeCAD import Vector instead of import FreeCAD.Vector when importing the Vector class in FreeCAD. This is the correct and preferred method.
|
10 |
+
- Always use from FreeCAD import Placement instead of import FreeCAD.Placement when importing the Placement class in FreeCAD. This is the correct and preferred method.
|
11 |
+
- Always use from FreeCAD import Rotation instead of import FreeCAD.Rotation when importing the Rotation class in FreeCAD. This is the correct and preferred method.
|
12 |
+
- use the fuse() function instead of Part.Union(). Use it only where necessary. Only when it is necessary to combine parts
|
13 |
+
- make the design tree as well for all parts. In the design tree, I must be able to change the dimension and placement of parts.
|
14 |
+
- dont make several features and then copy and make one feature at the end.
|
15 |
+
- Make the generated object visible
|
16 |
+
- try to make the design as modular as possible. for example, if i need to generate a cup, make the cup body as one feature in design tree and handle as other feature. in this way, i can edit the dimensions of both separately.
|
17 |
+
- There is no built-in Part.makeTube function in FreeCAD. You're trying to create a hollow cylinder (tube) directly with parameters like outer_radius, inner_radius, height, but FreeCAD expects shapes, not numbers.
|
18 |
+
- dont write comments
|
prompts/example_code.txt
ADDED
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
- Correct Usage of fuse() in FreeCAD-
|
2 |
+
When performing a union (boolean fuse) of multiple shapes in FreeCAD, always use the iterative .fuse() method on Part objects instead of Part.Union().
|
3 |
+
|
4 |
+
Correct Approach:
|
5 |
+
|
6 |
+
fan_final_shape = all_parts_to_fuse[0] # Start with the first shape
|
7 |
+
for shape in all_parts_to_fuse[1:]: # Iterate over remaining shapes
|
8 |
+
fan_final_shape = fan_final_shape.fuse(shape) # Fuse one by one
|
9 |
+
Avoid:
|
10 |
+
|
11 |
+
fan_final_shape = Part.Union(all_parts_to_fuse) # Incorrect method
|
12 |
+
|
13 |
+
|
14 |
+
- When applying a Placement to a FreeCAD shape (like a Part.Solid or Part.Shape), do not use .Placed(placement) — this method does not exist.
|
15 |
+
Instead, use .copy() and assign the Placement directly, like this:
|
16 |
+
|
17 |
+
shape = Part.makeBox(10, 10, 10)
|
18 |
+
placed_shape = shape.copy()
|
19 |
+
placed_shape.Placement = Placement(Vector(x, y, z), Rotation(Vector(0,0,1), angle))
|
20 |
+
Always use .copy() to avoid modifying the original shape directly, and set Placement as an attribute on the copied shape.
|
21 |
+
|
22 |
+
|
23 |
+
- Whenever you are asked to make a fastner including nut bolt and screw, you need to make a similar code as the one given below. you have the rag in your context window from where you must write the necessary function of calculating dimensions from screw_maker.py. You need to then make a dummy function for the variables of the screw as asked-
|
24 |
+
|
25 |
+
from screw_maker import *
|
26 |
+
|
27 |
+
try:
|
28 |
+
import FreeCADGui
|
29 |
+
GUI_AVAILABLE = True
|
30 |
+
except ImportError:
|
31 |
+
GUI_AVAILABLE = False
|
32 |
+
|
33 |
+
|
34 |
+
|
35 |
+
def makeAllMetalFlangedLockNut(self, fa):
|
36 |
+
"""Creates a distorted thread lock nut with a flange
|
37 |
+
Supported types:
|
38 |
+
- ISO 7044 all metal lock nuts with flange
|
39 |
+
- ISO 12126 all metal flanged lock nuts with fine pitch thread
|
40 |
+
"""
|
41 |
+
dia = self.getDia(fa.calc_diam, True)
|
42 |
+
if fa.baseType in ["ISO7044", "ISO12126"]:
|
43 |
+
P, c, _, _, dc, _, _, h, _, m_min, _, s, _, _ = fa.dimTable
|
44 |
+
m_w = m_min
|
45 |
+
else:
|
46 |
+
raise NotImplementedError(f"Unknown fastener type: {fa.Type}")
|
47 |
+
# main hexagonal body of the nut
|
48 |
+
shape = self.makeHexPrism(s, h)
|
49 |
+
# flange of the hex
|
50 |
+
fm = FSFaceMaker()
|
51 |
+
fm.AddPoint((1.05 * dia + s) / 4, 0.0)
|
52 |
+
fm.AddPoint((dc + sqrt3 * c) / 2, 0.0)
|
53 |
+
fm.AddPoint((dc - c) / 2, 0.0)
|
54 |
+
fm.AddArc2(0, c / 2, 150)
|
55 |
+
fm.AddPoint(
|
56 |
+
(1.05 * dia + s) / 4,
|
57 |
+
sqrt3
|
58 |
+
/ 3
|
59 |
+
* ((dc - c) / 2 + c / (4 - 2 * sqrt3) - (1.05 * dia + s) / 4),
|
60 |
+
)
|
61 |
+
flange = self.RevolveZ(fm.GetFace())
|
62 |
+
shape = shape.fuse(flange).removeSplitter()
|
63 |
+
# internal bore
|
64 |
+
fm.Reset()
|
65 |
+
id = self.GetInnerThreadMinDiameter(dia, P, 0.0)
|
66 |
+
bore_cham_ht = (dia * 1.05 - id) / 2 * tan15
|
67 |
+
fm.AddPoint(0.0, 0.0)
|
68 |
+
fm.AddPoint(dia * 1.05 / 2, 0.0)
|
69 |
+
fm.AddPoint(id / 2, bore_cham_ht)
|
70 |
+
fm.AddPoint(id / 2, h - bore_cham_ht)
|
71 |
+
fm.AddPoint(dia * 1.05 / 2, h)
|
72 |
+
fm.AddPoint(0.0, h)
|
73 |
+
bore_cutter = self.RevolveZ(fm.GetFace())
|
74 |
+
shape = shape.cut(bore_cutter)
|
75 |
+
# outer chamfer on the hex
|
76 |
+
fm.Reset()
|
77 |
+
fm.AddPoint((s / sqrt3 + 1.05 * dia / 2) / 2, h)
|
78 |
+
fm.AddPoint(s / sqrt3, h)
|
79 |
+
fm.AddPoint(s / sqrt3, m_w)
|
80 |
+
top_cham_cutter = self.RevolveZ(fm.GetFace())
|
81 |
+
shape = shape.cut(top_cham_cutter)
|
82 |
+
# add modelled threads if needed
|
83 |
+
if fa.Thread:
|
84 |
+
thread_cutter = self.CreateInnerThreadCutter(dia, P, h + P)
|
85 |
+
shape = shape.cut(thread_cutter)
|
86 |
+
return shape
|
87 |
+
|
88 |
+
|
89 |
+
|
90 |
+
if __name__ == "__main__":
|
91 |
+
import sys
|
92 |
+
sys.path.append(r"C:\Users\yasin\Desktop\Code\text2cad_trial")
|
93 |
+
import screw_maker as sm
|
94 |
+
import FreeCAD as App, Part
|
95 |
+
|
96 |
+
s = sm.Screw()
|
97 |
+
|
98 |
+
class DummyFA:
|
99 |
+
baseType = "ISO7044"
|
100 |
+
Type = "ISO7044"
|
101 |
+
calc_diam = "M10"
|
102 |
+
Diameter = "M10"
|
103 |
+
Thread = True
|
104 |
+
LeftHanded = False
|
105 |
+
dimTable = [1.5, 0.5, 0, 0, 17.5, 0, 0, 8.0, 0, 7.5, 0, 15, 0, 0]
|
106 |
+
|
107 |
+
fa = DummyFA()
|
108 |
+
shape = makeAllMetalFlangedLockNut(s, fa)
|
109 |
+
|
110 |
+
doc = App.newDocument("AutoNut")
|
111 |
+
obj = doc.addObject("Part::Feature", "AutoFlangedNut")
|
112 |
+
obj.Shape = shape
|
113 |
+
doc.recompute()
|
114 |
+
|
115 |
+
print("Flanged lock nut generated successfully.")
|
116 |
+
|
117 |
+
if GUI_AVAILABLE:
|
118 |
+
# FreeCADGui.showMainWindow()
|
119 |
+
FreeCADGui.activeDocument().activeView().viewAxometric()
|
120 |
+
FreeCADGui.SendMsgToActiveView("ViewFit")
|
121 |
+
|
122 |
+
|
123 |
+
- when asked to "make a flange of OD 100mm, bore size as 50mm, thickness 7.5mm. the height of the middle hollow neck must be 15mm. make 6 m12 holes at PCD 75mm", make the following code:
|
124 |
+
import FreeCAD as App
|
125 |
+
import FreeCADGui as Gui
|
126 |
+
from FreeCAD import Vector
|
127 |
+
import math
|
128 |
+
|
129 |
+
|
130 |
+
def createFlangeAssembly():
|
131 |
+
doc = App.newDocument("Flange")
|
132 |
+
|
133 |
+
# === Parameters ===
|
134 |
+
FLANGE_OUTER_DIAMETER = 100.0
|
135 |
+
FLANGE_THICKNESS = 7.5
|
136 |
+
BORE_INNER_DIAMETER = 50.0
|
137 |
+
NECK_HEIGHT = 15.0
|
138 |
+
NECK_OUTER_DIAMETER = 60.0
|
139 |
+
NUM_BOLT_HOLES = 6
|
140 |
+
BOLT_HOLE_DIAMETER = 12.0
|
141 |
+
PCD = 75.0
|
142 |
+
|
143 |
+
total_height = FLANGE_THICKNESS + NECK_HEIGHT
|
144 |
+
|
145 |
+
# === 1. Create flange base ===
|
146 |
+
flange = doc.addObject("Part::Cylinder", "Flange")
|
147 |
+
flange.Radius = FLANGE_OUTER_DIAMETER / 2
|
148 |
+
flange.Height = FLANGE_THICKNESS
|
149 |
+
|
150 |
+
# === 2. Cut central bore from flange ===
|
151 |
+
bore = doc.addObject("Part::Cylinder", "CentralBore")
|
152 |
+
bore.Radius = BORE_INNER_DIAMETER / 2
|
153 |
+
bore.Height = FLANGE_THICKNESS
|
154 |
+
bore_cut = doc.addObject("Part::Cut", "FlangeWithBore")
|
155 |
+
bore_cut.Base = flange
|
156 |
+
bore_cut.Tool = bore
|
157 |
+
|
158 |
+
# === 3. Create neck ===
|
159 |
+
neck_outer = doc.addObject("Part::Cylinder", "NeckOuter")
|
160 |
+
neck_outer.Radius = NECK_OUTER_DIAMETER / 2
|
161 |
+
neck_outer.Height = NECK_HEIGHT
|
162 |
+
neck_outer.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
|
163 |
+
|
164 |
+
neck_inner = doc.addObject("Part::Cylinder", "NeckInner")
|
165 |
+
neck_inner.Radius = BORE_INNER_DIAMETER / 2
|
166 |
+
neck_inner.Height = NECK_HEIGHT
|
167 |
+
neck_inner.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
|
168 |
+
|
169 |
+
neck_hollow = doc.addObject("Part::Cut", "HollowNeck")
|
170 |
+
neck_hollow.Base = neck_outer
|
171 |
+
neck_hollow.Tool = neck_inner
|
172 |
+
|
173 |
+
# === 4. Fuse flange (with central hole) and neck ===
|
174 |
+
fused = doc.addObject("Part::Fuse", "FlangeAndNeck")
|
175 |
+
fused.Base = bore_cut
|
176 |
+
fused.Tool = neck_hollow
|
177 |
+
|
178 |
+
# === 5. Cut bolt holes sequentially ===
|
179 |
+
current_shape = fused
|
180 |
+
bolt_radius = BOLT_HOLE_DIAMETER / 2
|
181 |
+
bolt_circle_radius = PCD / 2
|
182 |
+
|
183 |
+
for i in range(NUM_BOLT_HOLES):
|
184 |
+
angle_deg = 360 * i / NUM_BOLT_HOLES
|
185 |
+
angle_rad = math.radians(angle_deg)
|
186 |
+
x = bolt_circle_radius * math.cos(angle_rad)
|
187 |
+
y = bolt_circle_radius * math.sin(angle_rad)
|
188 |
+
|
189 |
+
hole = doc.addObject("Part::Cylinder", f"BoltHole_{i+1:02d}")
|
190 |
+
hole.Radius = bolt_radius
|
191 |
+
hole.Height = total_height
|
192 |
+
hole.Placement.Base = Vector(x, y, 0)
|
193 |
+
|
194 |
+
cut = doc.addObject("Part::Cut", f"Cut_Bolt_{i+1:02d}")
|
195 |
+
cut.Base = current_shape
|
196 |
+
cut.Tool = hole
|
197 |
+
current_shape = cut # update for next iteration
|
198 |
+
|
199 |
+
# === 6. Final result ===
|
200 |
+
|
201 |
+
|
202 |
+
# Recompute and fit view
|
203 |
+
doc.recompute()
|
204 |
+
Gui.activeDocument().activeView().viewAxometric()
|
205 |
+
Gui.SendMsgToActiveView("ViewFit")
|
206 |
+
|
207 |
+
return doc
|
208 |
+
|
209 |
+
if __name__ == "__main__":
|
210 |
+
createFlangeAssembly()
|
211 |
+
|
212 |
+
use this template whenever asked to make a flange
|
213 |
+
|
214 |
+
- Use material only when specified by user. An example of using material is-
|
215 |
+
|
216 |
+
view_obj = final_obj.ViewObject
|
217 |
+
view_obj.ShapeColor = (0.8, 0.8, 0.85) # Light grey-blue tone
|
218 |
+
view_obj.DiffuseColor = [(0.8, 0.8, 0.85)] # Consistent color across faces
|
219 |
+
view_obj.Transparency = 0
|
220 |
+
|
221 |
+
material_obj = doc.addObject("App::MaterialObject", "Material")
|
222 |
+
material_obj.Material = {
|
223 |
+
'Name': 'Stainless steel',
|
224 |
+
'Density': '8000 kg/m^3',
|
225 |
+
'YoungsModulus': '200000 MPa',
|
226 |
+
'PoissonRatio': '0.3'
|
227 |
+
}
|
228 |
+
material_obj.Label = "StainlessSteelMaterial"
|
pyproject.toml
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "cadomatic"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "This is a code to create editable CAD files from natural language prompts"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.11"
|
7 |
+
dependencies = [
|
8 |
+
"python-dotenv>=1.1.1",
|
9 |
+
"langchain>=0.3.27",
|
10 |
+
"beautifulsoup4>=4.13.4",
|
11 |
+
"langchain-google-genai>=2.1.8",
|
12 |
+
"langchain-community>=0.3.27",
|
13 |
+
"faiss-cpu>=1.11.0",
|
14 |
+
"huggingface-hub>=0.34.3",
|
15 |
+
"gradio>=5.39.0",
|
16 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file was autogenerated by uv via the following command:
|
2 |
+
# uv pip compile pyproject.toml --output-file requirements.txt
|
3 |
+
aiofiles==24.1.0
|
4 |
+
# via gradio
|
5 |
+
aiohappyeyeballs==2.6.1
|
6 |
+
# via aiohttp
|
7 |
+
aiohttp==3.12.15
|
8 |
+
# via langchain-community
|
9 |
+
aiosignal==1.4.0
|
10 |
+
# via aiohttp
|
11 |
+
annotated-types==0.7.0
|
12 |
+
# via pydantic
|
13 |
+
anyio==4.10.0
|
14 |
+
# via
|
15 |
+
# gradio
|
16 |
+
# httpx
|
17 |
+
# starlette
|
18 |
+
attrs==25.3.0
|
19 |
+
# via aiohttp
|
20 |
+
beautifulsoup4==4.13.4
|
21 |
+
# via cadomatic (pyproject.toml)
|
22 |
+
brotli==1.1.0
|
23 |
+
# via gradio
|
24 |
+
cachetools==5.5.2
|
25 |
+
# via google-auth
|
26 |
+
certifi==2025.8.3
|
27 |
+
# via
|
28 |
+
# httpcore
|
29 |
+
# httpx
|
30 |
+
# requests
|
31 |
+
charset-normalizer==3.4.2
|
32 |
+
# via requests
|
33 |
+
click==8.2.1
|
34 |
+
# via
|
35 |
+
# typer
|
36 |
+
# uvicorn
|
37 |
+
colorama==0.4.6
|
38 |
+
# via
|
39 |
+
# click
|
40 |
+
# tqdm
|
41 |
+
dataclasses-json==0.6.7
|
42 |
+
# via langchain-community
|
43 |
+
faiss-cpu==1.11.0.post1
|
44 |
+
# via cadomatic (pyproject.toml)
|
45 |
+
fastapi==0.116.1
|
46 |
+
# via gradio
|
47 |
+
ffmpy==0.6.1
|
48 |
+
# via gradio
|
49 |
+
filelock==3.18.0
|
50 |
+
# via huggingface-hub
|
51 |
+
filetype==1.2.0
|
52 |
+
# via langchain-google-genai
|
53 |
+
frozenlist==1.7.0
|
54 |
+
# via
|
55 |
+
# aiohttp
|
56 |
+
# aiosignal
|
57 |
+
fsspec==2025.7.0
|
58 |
+
# via
|
59 |
+
# gradio-client
|
60 |
+
# huggingface-hub
|
61 |
+
google-ai-generativelanguage==0.6.18
|
62 |
+
# via langchain-google-genai
|
63 |
+
google-api-core==2.25.1
|
64 |
+
# via google-ai-generativelanguage
|
65 |
+
google-auth==2.40.3
|
66 |
+
# via
|
67 |
+
# google-ai-generativelanguage
|
68 |
+
# google-api-core
|
69 |
+
googleapis-common-protos==1.70.0
|
70 |
+
# via
|
71 |
+
# google-api-core
|
72 |
+
# grpcio-status
|
73 |
+
gradio==5.41.1
|
74 |
+
# via cadomatic (pyproject.toml)
|
75 |
+
gradio-client==1.11.0
|
76 |
+
# via gradio
|
77 |
+
greenlet==3.2.4
|
78 |
+
# via sqlalchemy
|
79 |
+
groovy==0.1.2
|
80 |
+
# via gradio
|
81 |
+
grpcio==1.74.0
|
82 |
+
# via
|
83 |
+
# google-api-core
|
84 |
+
# grpcio-status
|
85 |
+
grpcio-status==1.74.0
|
86 |
+
# via google-api-core
|
87 |
+
h11==0.16.0
|
88 |
+
# via
|
89 |
+
# httpcore
|
90 |
+
# uvicorn
|
91 |
+
httpcore==1.0.9
|
92 |
+
# via httpx
|
93 |
+
httpx==0.28.1
|
94 |
+
# via
|
95 |
+
# gradio
|
96 |
+
# gradio-client
|
97 |
+
# langsmith
|
98 |
+
# safehttpx
|
99 |
+
httpx-sse==0.4.1
|
100 |
+
# via langchain-community
|
101 |
+
huggingface-hub==0.34.3
|
102 |
+
# via
|
103 |
+
# cadomatic (pyproject.toml)
|
104 |
+
# gradio
|
105 |
+
# gradio-client
|
106 |
+
idna==3.10
|
107 |
+
# via
|
108 |
+
# anyio
|
109 |
+
# httpx
|
110 |
+
# requests
|
111 |
+
# yarl
|
112 |
+
jinja2==3.1.6
|
113 |
+
# via gradio
|
114 |
+
jsonpatch==1.33
|
115 |
+
# via langchain-core
|
116 |
+
jsonpointer==3.0.0
|
117 |
+
# via jsonpatch
|
118 |
+
langchain==0.3.27
|
119 |
+
# via
|
120 |
+
# cadomatic (pyproject.toml)
|
121 |
+
# langchain-community
|
122 |
+
langchain-community==0.3.27
|
123 |
+
# via cadomatic (pyproject.toml)
|
124 |
+
langchain-core==0.3.73
|
125 |
+
# via
|
126 |
+
# langchain
|
127 |
+
# langchain-community
|
128 |
+
# langchain-google-genai
|
129 |
+
# langchain-text-splitters
|
130 |
+
langchain-google-genai==2.1.9
|
131 |
+
# via cadomatic (pyproject.toml)
|
132 |
+
langchain-text-splitters==0.3.9
|
133 |
+
# via langchain
|
134 |
+
langsmith==0.4.13
|
135 |
+
# via
|
136 |
+
# langchain
|
137 |
+
# langchain-community
|
138 |
+
# langchain-core
|
139 |
+
markdown-it-py==3.0.0
|
140 |
+
# via rich
|
141 |
+
markupsafe==3.0.2
|
142 |
+
# via
|
143 |
+
# gradio
|
144 |
+
# jinja2
|
145 |
+
marshmallow==3.26.1
|
146 |
+
# via dataclasses-json
|
147 |
+
mdurl==0.1.2
|
148 |
+
# via markdown-it-py
|
149 |
+
multidict==6.6.3
|
150 |
+
# via
|
151 |
+
# aiohttp
|
152 |
+
# yarl
|
153 |
+
mypy-extensions==1.1.0
|
154 |
+
# via typing-inspect
|
155 |
+
numpy==2.3.2
|
156 |
+
# via
|
157 |
+
# faiss-cpu
|
158 |
+
# gradio
|
159 |
+
# langchain-community
|
160 |
+
# pandas
|
161 |
+
orjson==3.11.1
|
162 |
+
# via
|
163 |
+
# gradio
|
164 |
+
# langsmith
|
165 |
+
packaging==25.0
|
166 |
+
# via
|
167 |
+
# faiss-cpu
|
168 |
+
# gradio
|
169 |
+
# gradio-client
|
170 |
+
# huggingface-hub
|
171 |
+
# langchain-core
|
172 |
+
# langsmith
|
173 |
+
# marshmallow
|
174 |
+
pandas==2.3.1
|
175 |
+
# via gradio
|
176 |
+
pillow==11.3.0
|
177 |
+
# via gradio
|
178 |
+
propcache==0.3.2
|
179 |
+
# via
|
180 |
+
# aiohttp
|
181 |
+
# yarl
|
182 |
+
proto-plus==1.26.1
|
183 |
+
# via
|
184 |
+
# google-ai-generativelanguage
|
185 |
+
# google-api-core
|
186 |
+
protobuf==6.31.1
|
187 |
+
# via
|
188 |
+
# google-ai-generativelanguage
|
189 |
+
# google-api-core
|
190 |
+
# googleapis-common-protos
|
191 |
+
# grpcio-status
|
192 |
+
# proto-plus
|
193 |
+
pyasn1==0.6.1
|
194 |
+
# via
|
195 |
+
# pyasn1-modules
|
196 |
+
# rsa
|
197 |
+
pyasn1-modules==0.4.2
|
198 |
+
# via google-auth
|
199 |
+
pydantic==2.11.7
|
200 |
+
# via
|
201 |
+
# fastapi
|
202 |
+
# gradio
|
203 |
+
# langchain
|
204 |
+
# langchain-core
|
205 |
+
# langchain-google-genai
|
206 |
+
# langsmith
|
207 |
+
# pydantic-settings
|
208 |
+
pydantic-core==2.33.2
|
209 |
+
# via pydantic
|
210 |
+
pydantic-settings==2.10.1
|
211 |
+
# via langchain-community
|
212 |
+
pydub==0.25.1
|
213 |
+
# via gradio
|
214 |
+
pygments==2.19.2
|
215 |
+
# via rich
|
216 |
+
python-dateutil==2.9.0.post0
|
217 |
+
# via pandas
|
218 |
+
python-dotenv==1.1.1
|
219 |
+
# via
|
220 |
+
# cadomatic (pyproject.toml)
|
221 |
+
# pydantic-settings
|
222 |
+
python-multipart==0.0.20
|
223 |
+
# via gradio
|
224 |
+
pytz==2025.2
|
225 |
+
# via pandas
|
226 |
+
pyyaml==6.0.2
|
227 |
+
# via
|
228 |
+
# gradio
|
229 |
+
# huggingface-hub
|
230 |
+
# langchain
|
231 |
+
# langchain-community
|
232 |
+
# langchain-core
|
233 |
+
requests==2.32.4
|
234 |
+
# via
|
235 |
+
# google-api-core
|
236 |
+
# huggingface-hub
|
237 |
+
# langchain
|
238 |
+
# langchain-community
|
239 |
+
# langsmith
|
240 |
+
# requests-toolbelt
|
241 |
+
requests-toolbelt==1.0.0
|
242 |
+
# via langsmith
|
243 |
+
rich==14.1.0
|
244 |
+
# via typer
|
245 |
+
rsa==4.9.1
|
246 |
+
# via google-auth
|
247 |
+
ruff==0.12.7
|
248 |
+
# via gradio
|
249 |
+
safehttpx==0.1.6
|
250 |
+
# via gradio
|
251 |
+
semantic-version==2.10.0
|
252 |
+
# via gradio
|
253 |
+
shellingham==1.5.4
|
254 |
+
# via typer
|
255 |
+
six==1.17.0
|
256 |
+
# via python-dateutil
|
257 |
+
sniffio==1.3.1
|
258 |
+
# via anyio
|
259 |
+
soupsieve==2.7
|
260 |
+
# via beautifulsoup4
|
261 |
+
sqlalchemy==2.0.42
|
262 |
+
# via
|
263 |
+
# langchain
|
264 |
+
# langchain-community
|
265 |
+
starlette==0.47.2
|
266 |
+
# via
|
267 |
+
# fastapi
|
268 |
+
# gradio
|
269 |
+
tenacity==9.1.2
|
270 |
+
# via
|
271 |
+
# langchain-community
|
272 |
+
# langchain-core
|
273 |
+
tomlkit==0.13.3
|
274 |
+
# via gradio
|
275 |
+
tqdm==4.67.1
|
276 |
+
# via huggingface-hub
|
277 |
+
typer==0.16.0
|
278 |
+
# via gradio
|
279 |
+
typing-extensions==4.14.1
|
280 |
+
# via
|
281 |
+
# aiosignal
|
282 |
+
# anyio
|
283 |
+
# beautifulsoup4
|
284 |
+
# fastapi
|
285 |
+
# gradio
|
286 |
+
# gradio-client
|
287 |
+
# huggingface-hub
|
288 |
+
# langchain-core
|
289 |
+
# pydantic
|
290 |
+
# pydantic-core
|
291 |
+
# sqlalchemy
|
292 |
+
# starlette
|
293 |
+
# typer
|
294 |
+
# typing-inspect
|
295 |
+
# typing-inspection
|
296 |
+
typing-inspect==0.9.0
|
297 |
+
# via dataclasses-json
|
298 |
+
typing-inspection==0.4.1
|
299 |
+
# via
|
300 |
+
# pydantic
|
301 |
+
# pydantic-settings
|
302 |
+
tzdata==2025.2
|
303 |
+
# via pandas
|
304 |
+
urllib3==2.5.0
|
305 |
+
# via requests
|
306 |
+
uvicorn==0.35.0
|
307 |
+
# via gradio
|
308 |
+
websockets==15.0.1
|
309 |
+
# via gradio-client
|
310 |
+
yarl==1.20.1
|
311 |
+
# via aiohttp
|
312 |
+
zstandard==0.23.0
|
313 |
+
# via langsmith
|
src/__init__.py
ADDED
File without changes
|
src/llm_client.py
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from pathlib import Path
|
3 |
+
from huggingface_hub import hf_hub_download
|
4 |
+
from langchain_community.vectorstores import FAISS
|
5 |
+
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
import google.generativeai as genai
|
8 |
+
|
9 |
+
# Load API key
|
10 |
+
load_dotenv()
|
11 |
+
genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
|
12 |
+
|
13 |
+
# Set up Hugging Face repo and files
|
14 |
+
REPO_ID = "Yas1n/CADomatic_vectorstore"
|
15 |
+
FILENAME_FAISS = "index.faiss"
|
16 |
+
FILENAME_PKL = "index.pkl"
|
17 |
+
|
18 |
+
# Download once (uses HF cache internally)
|
19 |
+
faiss_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME_FAISS)
|
20 |
+
pkl_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME_PKL)
|
21 |
+
|
22 |
+
# Use same folder for both files
|
23 |
+
download_dir = Path(faiss_path).parent
|
24 |
+
|
25 |
+
# Load vectorstore
|
26 |
+
embedding = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
|
27 |
+
vectorstore = FAISS.load_local(str(download_dir), embeddings=embedding, allow_dangerous_deserialization=True)
|
28 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 40})
|
29 |
+
|
30 |
+
# Gemini 2.5 Flash
|
31 |
+
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=1.2)
|
32 |
+
|
33 |
+
def prompt_llm(user_prompt: str) -> str:
|
34 |
+
docs = retriever.invoke(user_prompt)
|
35 |
+
context = "\n\n".join(doc.page_content for doc in docs)
|
36 |
+
|
37 |
+
final_prompt = f"""
|
38 |
+
You are a helpful assistant that writes FreeCAD Python scripts from CAD instructions.
|
39 |
+
Use the following FreeCAD wiki documentation as context:
|
40 |
+
|
41 |
+
{context}
|
42 |
+
|
43 |
+
Instruction:
|
44 |
+
{user_prompt}
|
45 |
+
|
46 |
+
Respond with valid FreeCAD Python code only, no extra commentary.
|
47 |
+
"""
|
48 |
+
|
49 |
+
try:
|
50 |
+
response = llm.invoke(final_prompt)
|
51 |
+
return response.content
|
52 |
+
except Exception as e:
|
53 |
+
print("❌ Error generating FreeCAD code:", e)
|
54 |
+
return ""
|
src/rag_builder.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import requests
|
3 |
+
from bs4 import BeautifulSoup
|
4 |
+
from urllib.parse import urljoin
|
5 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
6 |
+
from langchain_community.vectorstores import FAISS
|
7 |
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
8 |
+
|
9 |
+
# --- Step 1: ENV setup ---
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
import google.generativeai as genai
|
12 |
+
|
13 |
+
load_dotenv()
|
14 |
+
genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
|
15 |
+
|
16 |
+
# --- Step 2: Crawler ---
|
17 |
+
BASE_URL_WIKI = "https://wiki.freecad.org/Power_users_hub"
|
18 |
+
BASE_URL_GITHUB = "https://github.com/shaise/FreeCAD_FastenersWB"
|
19 |
+
|
20 |
+
DOMAIN_WHITELIST = [
|
21 |
+
"https://wiki.freecad.org",
|
22 |
+
"https://github.com/shaise"
|
23 |
+
]
|
24 |
+
|
25 |
+
# List of language identifiers to exclude (only for wiki)
|
26 |
+
LANG_IDENTIFIERS = [
|
27 |
+
"/id", "/de", "/tr", "/es", "/fr", "/hr", "/it", "/pl",
|
28 |
+
"/pt", "/pt-br", "/ro", "/fi", "/sv", "/cs", "/ru", "/zh-cn",
|
29 |
+
"/zh-tw", "/ja", "/ko"
|
30 |
+
]
|
31 |
+
|
32 |
+
def is_excluded_url(url):
|
33 |
+
url_lower = url.lower()
|
34 |
+
|
35 |
+
# Apply language filters only to FreeCAD wiki URLs
|
36 |
+
if "wiki.freecad.org" in url_lower:
|
37 |
+
if any(lang in url_lower for lang in LANG_IDENTIFIERS):
|
38 |
+
return True
|
39 |
+
|
40 |
+
return (
|
41 |
+
".jpg" in url_lower or
|
42 |
+
".png" in url_lower or
|
43 |
+
"edit§ion" in url_lower
|
44 |
+
)
|
45 |
+
|
46 |
+
def crawl_wiki(start_url, max_pages):
|
47 |
+
visited = set()
|
48 |
+
to_visit = [start_url]
|
49 |
+
pages = []
|
50 |
+
|
51 |
+
while to_visit and len(visited) < max_pages:
|
52 |
+
url = to_visit.pop(0)
|
53 |
+
if url in visited or is_excluded_url(url):
|
54 |
+
continue
|
55 |
+
try:
|
56 |
+
print(f"Fetching: {url}")
|
57 |
+
res = requests.get(url)
|
58 |
+
res.raise_for_status()
|
59 |
+
soup = BeautifulSoup(res.text, "html.parser")
|
60 |
+
visited.add(url)
|
61 |
+
|
62 |
+
for tag in soup(["script", "style", "header", "footer", "nav", "aside"]):
|
63 |
+
tag.extract()
|
64 |
+
text = soup.get_text(separator="\n")
|
65 |
+
clean = "\n".join([line.strip() for line in text.splitlines() if line.strip()])
|
66 |
+
pages.append({"url": url, "text": clean})
|
67 |
+
|
68 |
+
# Queue internal links
|
69 |
+
for a in soup.find_all("a", href=True):
|
70 |
+
full = urljoin(url, a["href"])
|
71 |
+
if any(full.startswith(domain) for domain in DOMAIN_WHITELIST):
|
72 |
+
if full not in visited and not is_excluded_url(full):
|
73 |
+
to_visit.append(full)
|
74 |
+
except Exception as e:
|
75 |
+
print(f"Error fetching {url}: {e}")
|
76 |
+
|
77 |
+
print(f"Crawled {len(pages)} pages from {start_url}")
|
78 |
+
return pages
|
79 |
+
|
80 |
+
# --- Step 3: RAG Build ---
|
81 |
+
def build_vectorstore():
|
82 |
+
wiki_pages = crawl_wiki(BASE_URL_WIKI, max_pages=2000) # Uncomment if you want both
|
83 |
+
github_pages = crawl_wiki(BASE_URL_GITHUB, max_pages=450)
|
84 |
+
pages = wiki_pages + github_pages
|
85 |
+
|
86 |
+
if not pages:
|
87 |
+
print("No pages crawled. Exiting.")
|
88 |
+
return
|
89 |
+
|
90 |
+
texts = [p["text"] for p in pages]
|
91 |
+
metadatas = [{"source": p["url"]} for p in pages]
|
92 |
+
|
93 |
+
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
|
94 |
+
docs = splitter.create_documents(texts, metadatas=metadatas)
|
95 |
+
|
96 |
+
print(f"Split into {len(docs)} chunks")
|
97 |
+
|
98 |
+
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
|
99 |
+
vectorstore = FAISS.from_documents(docs, embeddings)
|
100 |
+
|
101 |
+
src_path = os.path.dirname(os.path.abspath(__file__))
|
102 |
+
root_dir_path = os.path.dirname(src_path)
|
103 |
+
vectorstore_path = os.path.join(root_dir_path, "vectorstore")
|
104 |
+
|
105 |
+
os.makedirs(vectorstore_path, exist_ok=True)
|
106 |
+
vectorstore.save_local(vectorstore_path)
|
107 |
+
print("Vectorstore saved to ./vectorstore")
|
108 |
+
|
109 |
+
if __name__ == "__main__":
|
110 |
+
build_vectorstore()
|
src/run_freecad.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# run_freecad.py
|
2 |
+
import subprocess
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
freecad_exe = r"C:\Program Files\FreeCAD 1.0\bin\freecad.exe"
|
6 |
+
script_path = Path("generated/result_script.py")
|
7 |
+
|
8 |
+
if not script_path.exists():
|
9 |
+
raise FileNotFoundError("Generated script not found. Run main.py first.")
|
10 |
+
|
11 |
+
subprocess.run([freecad_exe, str(script_path)], check=True)
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|