YSMlearnsCode commited on
Commit
3c38be2
·
1 Parent(s): 6e7d971

added chat feature

Browse files
Files changed (6) hide show
  1. app.py +32 -10
  2. app/process.py +3 -3
  3. generated/result_script.py +82 -17
  4. main.py +3 -3
  5. prompts/example_code.txt +0 -565
  6. src/llm_client.py +50 -33
app.py CHANGED
@@ -10,13 +10,27 @@ GENERATED_SCRIPT_PATH = PROJECT_ROOT / "generated" / "result_script.py"
10
  sys.path.insert(0, str(PROJECT_ROOT / "app"))
11
 
12
  from app.process import main as generate_from_llm # This runs generation
 
13
 
14
- def generate_script_and_preview(description):
15
  """
16
- Generates the FreeCAD script using process.py logic and returns:
17
  - The script text for preview
18
  - The file path for download
19
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  import builtins
21
  original_input = builtins.input
22
  builtins.input = lambda _: description
@@ -79,7 +93,6 @@ css = """
79
  """
80
 
81
  # Description
82
- # ToDo write as features
83
  cadomatic_description_md = """
84
  <div style="text-align: center;">
85
 
@@ -97,12 +110,14 @@ with gr.Blocks(css=css) as demo:
97
  gr.Markdown(cadomatic_description_md)
98
 
99
  description_input = gr.Textbox(
100
- label="Describe your desired CAD model below-",
101
  lines=2,
102
  placeholder="e.g., Create a flange with OD 100mm, bore size 50mm and 6 m8 holes at PCD 75mm..."
103
  )
104
 
105
- generate_btn = gr.Button("Generate Script", variant="primary")
 
 
106
 
107
  with gr.Row():
108
  with gr.Column(scale=1):
@@ -116,12 +131,13 @@ with gr.Blocks(css=css) as demo:
116
  elem_classes="download-button"
117
  )
118
  with gr.Column(scale=1):
119
- gr.Markdown( #ToDo
120
  """
121
  <div class='instructions'>
122
  <b>Instructions:</b><br>
123
- - Enter the description for your desired CAD part.<br>
124
- - Click on "Generate Script".<br>
 
125
  - Preview the generated Python code.<br>
126
  - Paste the generated code into the python console of your FreeCAD app.<br>
127
  - (or)<br>
@@ -142,8 +158,14 @@ with gr.Blocks(css=css) as demo:
142
  """
143
  )
144
 
145
- generate_btn.click(
146
- fn=generate_script_and_preview,
 
 
 
 
 
 
147
  inputs=description_input,
148
  outputs=[preview_output, download_btn]
149
  )
 
10
  sys.path.insert(0, str(PROJECT_ROOT / "app"))
11
 
12
  from app.process import main as generate_from_llm # This runs generation
13
+ from src.llm_client import reset_memory # ✅ Import reset function for new part
14
 
15
+ def generate_new_part(description):
16
  """
17
+ Generates a completely new FreeCAD script (resets memory) and returns:
18
  - The script text for preview
19
  - The file path for download
20
  """
21
+ reset_memory() # ✅ Start fresh for a new part
22
+ return _generate_script(description)
23
+
24
+ def edit_existing_part(description):
25
+ """
26
+ Edits the existing FreeCAD script using conversation context and returns:
27
+ - The updated script text for preview
28
+ - The file path for download
29
+ """
30
+ return _generate_script(description)
31
+
32
+ def _generate_script(description):
33
+ """Helper to call process.py with given description."""
34
  import builtins
35
  original_input = builtins.input
36
  builtins.input = lambda _: description
 
93
  """
94
 
95
  # Description
 
96
  cadomatic_description_md = """
97
  <div style="text-align: center;">
98
 
 
110
  gr.Markdown(cadomatic_description_md)
111
 
112
  description_input = gr.Textbox(
113
+ label="Describe your desired CAD model or modification below:",
114
  lines=2,
115
  placeholder="e.g., Create a flange with OD 100mm, bore size 50mm and 6 m8 holes at PCD 75mm..."
116
  )
117
 
118
+ with gr.Row():
119
+ generate_new_btn = gr.Button("Generate New Part", variant="primary")
120
+ edit_existing_btn = gr.Button("Edit Existing Part", variant="secondary")
121
 
122
  with gr.Row():
123
  with gr.Column(scale=1):
 
131
  elem_classes="download-button"
132
  )
133
  with gr.Column(scale=1):
134
+ gr.Markdown(
135
  """
136
  <div class='instructions'>
137
  <b>Instructions:</b><br>
138
+ - Enter the description for your desired CAD part or an edit instruction.<br>
139
+ - Click on "Generate New Part" to start fresh.<br>
140
+ - Click on "Edit Existing Part" to modify the last script.<br>
141
  - Preview the generated Python code.<br>
142
  - Paste the generated code into the python console of your FreeCAD app.<br>
143
  - (or)<br>
 
158
  """
159
  )
160
 
161
+ generate_new_btn.click(
162
+ fn=generate_new_part,
163
+ inputs=description_input,
164
+ outputs=[preview_output, download_btn]
165
+ )
166
+
167
+ edit_existing_btn.click(
168
+ fn=edit_existing_part,
169
  inputs=description_input,
170
  outputs=[preview_output, download_btn]
171
  )
app/process.py CHANGED
@@ -10,7 +10,7 @@ from src.llm_client import prompt_llm
10
 
11
  # File paths (relative to project root)
12
  prompt_base = ROOT_DIR / "prompts" / "base_instruction.txt"
13
- prompt_examples = ROOT_DIR / "prompts" / "example_code.txt"
14
  GEN_SCRIPT = ROOT_DIR / "generated" / "result_script.py"
15
  RUN_SCRIPT = ROOT_DIR / "src" / "run_freecad.py"
16
 
@@ -27,8 +27,8 @@ def main():
27
 
28
  # Step 2: Build prompt
29
  base_prompt = prompt_base.read_text(encoding="utf-8").strip()
30
- example_prompt = prompt_examples.read_text(encoding="utf-8").strip()
31
- full_prompt = f"{base_prompt}\n\nExamples:\n{example_prompt}\n\nUser instruction: {user_input.strip()}"
32
 
33
  # Step 3: Get response from LLM
34
  generated_code = prompt_llm(full_prompt)
 
10
 
11
  # File paths (relative to project root)
12
  prompt_base = ROOT_DIR / "prompts" / "base_instruction.txt"
13
+ # prompt_examples = ROOT_DIR / "prompts" / "example_code.txt"
14
  GEN_SCRIPT = ROOT_DIR / "generated" / "result_script.py"
15
  RUN_SCRIPT = ROOT_DIR / "src" / "run_freecad.py"
16
 
 
27
 
28
  # Step 2: Build prompt
29
  base_prompt = prompt_base.read_text(encoding="utf-8").strip()
30
+ # example_prompt = prompt_examples.read_text(encoding="utf-8").strip()
31
+ full_prompt = f"{base_prompt}\n\nExamples:\n\nUser instruction: {user_input.strip()}"
32
 
33
  # Step 3: Get response from LLM
34
  generated_code = prompt_llm(full_prompt)
generated/result_script.py CHANGED
@@ -1,23 +1,88 @@
1
  import FreeCAD as App
2
- import FreeCADGui as Gui
3
- from FreeCAD import Vector, Placement, Rotation
4
  import Part
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- def createBox(length=10.0, width=20.0, height=30.0):
7
- doc = App.newDocument("Box")
8
- box = doc.addObject("Part::Box", "Box")
9
- box.Length = length
10
- box.Width = width
11
- box.Height = height
12
- box.Placement = Placement(Vector(0, 0, 0), Rotation())
13
- box.ViewObject.ShapeColor = (0.8, 0.8, 0.8)
14
- doc.recompute()
15
- Gui.ActiveDocument.ActiveView.viewAxometric()
16
- Gui.SendMsgToActiveView("ViewFit")
17
- return doc
18
-
19
- if __name__ == "__main__":
20
- createBox()
21
 
22
 
23
  import FreeCADGui
 
1
  import FreeCAD as App
 
 
2
  import Part
3
+ import math
4
+ from FreeCAD import Vector, Placement, Rotation
5
+
6
+ doc = App.newDocument()
7
+
8
+ outer_diameter = 100.0
9
+ inner_diameter = 50.0
10
+ thickness = 10.0
11
+ bolt_circle_diameter = 75.0
12
+ num_bolt_holes = 6
13
+ bolt_hole_diameter = 8.0
14
+
15
+ overall_placement_vector = Vector(0, 0, 0)
16
+ overall_rotation_axis = Vector(0, 0, 1)
17
+ overall_rotation_angle = 0.0
18
+
19
+ outer_radius = outer_diameter / 2
20
+ inner_radius = inner_diameter / 2
21
+ bolt_hole_radius = bolt_hole_diameter / 2
22
+
23
+ obj_base = doc.addObject("Part::Cylinder", "FlangeBase")
24
+ obj_base.Radius = outer_radius
25
+ obj_base.Height = thickness
26
+ obj_base.Placement = Placement(Vector(0, 0, 0))
27
+
28
+ obj_center_hole = doc.addObject("Part::Cylinder", "CenterHole")
29
+ obj_center_hole.Radius = inner_radius
30
+ obj_center_hole.Height = thickness
31
+ obj_center_hole.Placement = Placement(Vector(0, 0, 0))
32
+
33
+ bolt_hole_objects = []
34
+ for i in range(num_bolt_holes):
35
+ angle = 2 * math.pi * i / num_bolt_holes
36
+ x = bolt_circle_diameter / 2 * math.cos(angle)
37
+ y = bolt_circle_diameter / 2 * math.sin(angle)
38
+
39
+ obj_bolt_hole = doc.addObject("Part::Cylinder", f"BoltHole_{i+1}")
40
+ obj_bolt_hole.Radius = bolt_hole_radius
41
+ obj_bolt_hole.Height = thickness
42
+ obj_bolt_hole.Placement = Placement(Vector(x, y, 0))
43
+ bolt_hole_objects.append(obj_bolt_hole)
44
+
45
+ doc.recompute()
46
+
47
+ obj_cut_center = doc.addObject("Part::Cut", "FlangeWithCenterHole")
48
+ obj_cut_center.Base = obj_base
49
+ obj_cut_center.Tool = obj_center_hole
50
+ doc.recompute()
51
+
52
+ obj_base.Visibility = False
53
+ obj_center_hole.Visibility = False
54
+
55
+ obj_fused_holes = doc.addObject("Part::MultiFuse", "FusedBoltHoles")
56
+ obj_fused_holes.Objects = bolt_hole_objects
57
+ doc.recompute()
58
+
59
+ for hole_obj in bolt_hole_objects:
60
+ hole_obj.Visibility = False
61
+
62
+ obj_final_flange = doc.addObject("Part::Cut", "FinalFlange")
63
+ obj_final_flange.Base = obj_cut_center
64
+ obj_final_flange.Tool = obj_fused_holes
65
+ doc.recompute()
66
+
67
+ obj_cut_center.Visibility = False
68
+ obj_fused_holes.Visibility = False
69
+
70
+ final_placement = Placement(overall_placement_vector, Rotation(overall_rotation_axis, overall_rotation_angle))
71
+ obj_final_flange.Placement = final_placement
72
+
73
+ rod_length = 200.0
74
+ rod_diameter = inner_diameter
75
+ rod_radius = rod_diameter / 2
76
+
77
+ rod_placement_z = thickness / 2 - rod_length / 2
78
+
79
+ obj_rod = doc.addObject("Part::Cylinder", "Rod")
80
+ obj_rod.Radius = rod_radius
81
+ obj_rod.Height = rod_length
82
+ obj_rod.Placement = Placement(Vector(0, 0, rod_placement_z))
83
 
84
+ doc.recompute()
85
+ App.ActiveDocument.ActiveView.fitAll()
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
 
88
  import FreeCADGui
main.py CHANGED
@@ -4,7 +4,7 @@ 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
 
@@ -21,8 +21,8 @@ def main():
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
 
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
 
 
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\n\nUser instruction: {user_input.strip()}"
26
 
27
 
28
  # Step 3: Get response from LLM
prompts/example_code.txt DELETED
@@ -1,565 +0,0 @@
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"
229
-
230
- - This is a good example for a teapot. Whenever asked to generate a teapot, make something similar:
231
- import FreeCAD as App
232
- import FreeCADGui as Gui
233
- from FreeCAD import Vector, Placement, Rotation
234
- import Part
235
-
236
- # Teapot dimensions
237
- BODY_BOTTOM_RADIUS = 50.0
238
- BODY_MAX_RADIUS = 80.0
239
- BODY_HEIGHT = 100.0
240
- LID_OPENING_RADIUS = 35.0
241
-
242
- # Spout parameters
243
- SPOUT_ATTACH_HEIGHT = BODY_HEIGHT * 0.5 # 50.0
244
- SPOUT_OFFSET_Y = BODY_MAX_RADIUS * 0.7 # 56.0
245
- SPOUT_LENGTH_HORIZONTAL = 60.0
246
- SPOUT_LENGTH_VERTICAL = 30.0
247
- SPOUT_RADIUS = 7.0
248
-
249
- # Handle parameters
250
- HANDLE_ATTACH_TOP_HEIGHT = BODY_HEIGHT * 0.7 # 70.0
251
- HANDLE_ATTACH_BOTTOM_HEIGHT = BODY_HEIGHT * 0.3 # 30.0
252
- HANDLE_OFFSET_Y = -BODY_MAX_RADIUS * 0.7 # -56.0
253
- HANDLE_RADIUS = 6.0
254
-
255
- def createTeapot():
256
- doc = App.newDocument("Teapot")
257
-
258
- # --- 1. Body ---
259
- body_profile_pts = [
260
- Vector(BODY_BOTTOM_RADIUS, 0, 0),
261
- Vector(BODY_MAX_RADIUS, 0, BODY_HEIGHT * 0.4),
262
- Vector(BODY_MAX_RADIUS * 0.8, 0, BODY_HEIGHT * 0.7),
263
- Vector(LID_OPENING_RADIUS, 0, BODY_HEIGHT)
264
- ]
265
- body_spline = Part.BSplineCurve(body_profile_pts)
266
- body_edge = body_spline.toShape()
267
- line1 = Part.LineSegment(Vector(LID_OPENING_RADIUS, 0, BODY_HEIGHT), Vector(0, 0, BODY_HEIGHT)).toShape()
268
- line2 = Part.LineSegment(Vector(0, 0, BODY_HEIGHT), Vector(0, 0, 0)).toShape()
269
- line3 = Part.LineSegment(Vector(0, 0, 0), Vector(BODY_BOTTOM_RADIUS, 0, 0)).toShape()
270
- wire = Part.Wire([body_edge, line1, line2, line3])
271
- face = Part.Face(wire)
272
- body_solid = face.revolve(Vector(0, 0, 0), Vector(0, 0, 1), 360)
273
-
274
- obj_body = doc.addObject("Part::Feature", "Body")
275
- obj_body.Shape = body_solid
276
- obj_body.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
277
-
278
- # --- 2. Lid ---
279
- lid_profile_pts = [
280
- Vector(36.0, 0, 0),
281
- Vector(36.0, 0, 3.0),
282
- Vector(35.0, 0, 3.0 + 20.0 * 0.2),
283
- Vector(17.5, 0, 3.0 + 20.0 * 0.7),
284
- Vector(10.0, 0, 3.0 + 20.0),
285
- Vector(5.0, 0, 3.0 + 20.0 + 15.0 * 0.8),
286
- Vector(0, 0, 3.0 + 20.0 + 15.0)
287
- ]
288
- lid_spline = Part.BSplineCurve(lid_profile_pts)
289
- lid_edge = lid_spline.toShape()
290
- line1 = Part.LineSegment(Vector(0, 0, 3.0 + 20.0 + 15.0), Vector(0, 0, 0)).toShape()
291
- line2 = Part.LineSegment(Vector(0, 0, 0), Vector(36.0, 0, 0)).toShape()
292
- wire_lid = Part.Wire([lid_edge, line1, line2])
293
- face_lid = Part.Face(wire_lid)
294
- lid_solid = face_lid.revolve(Vector(0, 0, 0), Vector(0, 0, 1), 360)
295
-
296
- obj_lid = doc.addObject("Part::Feature", "Lid")
297
- obj_lid.Shape = lid_solid
298
- obj_lid.Placement = Placement(Vector(0, 0, BODY_HEIGHT), Rotation())
299
- obj_lid.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
300
-
301
- # --- 3. Spout (Precomputed final positions) ---
302
- spout_path_pts = [
303
- Vector(0, -121, 66), # Original: (0, -56, 50) -> transformed
304
- Vector(0, -91, 51), # Original: (0, -26, 65) -> transformed
305
- Vector(0, -61, 36) # Original: (0, 4, 80) -> transformed
306
- ]
307
-
308
- spout_curve = Part.BSplineCurve(spout_path_pts)
309
- spout_wire = Part.Wire(spout_curve.toShape())
310
-
311
- tangent_spout = spout_curve.tangent(spout_curve.FirstParameter)[0]
312
- tangent_spout.normalize()
313
-
314
- spout_circle = Part.Circle()
315
- spout_circle.Center = spout_path_pts[0]
316
- spout_circle.Axis = tangent_spout
317
- spout_circle.Radius = SPOUT_RADIUS
318
- spout_profile = Part.Wire(spout_circle.toShape())
319
-
320
- spout_solid = spout_wire.makePipe(spout_profile)
321
- obj_spout = doc.addObject("Part::Feature", "Spout")
322
- obj_spout.Shape = spout_solid
323
- obj_spout.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
324
-
325
- # --- 4. Handle (Precomputed final positions) ---
326
- handle_path_pts = [
327
- Vector(0, 56, 31), # Original: (0, 56, 70) -> transformed
328
- Vector(0, 78, 43), # Original: (0, 78, 58) -> transformed
329
- Vector(0, 78, 79), # Original: (0, 78, 22) -> transformed
330
- Vector(0, 56, 71) # Original: (0, 56, 30) -> transformed
331
- ]
332
-
333
- handle_curve = Part.BSplineCurve(handle_path_pts)
334
- handle_wire = Part.Wire(handle_curve.toShape())
335
-
336
- tangent_handle = handle_curve.tangent(handle_curve.FirstParameter)[0]
337
- tangent_handle.normalize()
338
-
339
- handle_circle = Part.Circle()
340
- handle_circle.Center = handle_path_pts[0]
341
- handle_circle.Axis = tangent_handle
342
- handle_circle.Radius = HANDLE_RADIUS
343
- handle_profile = Part.Wire(handle_circle.toShape())
344
-
345
- handle_solid = handle_wire.makePipe(handle_profile)
346
- obj_handle = doc.addObject("Part::Feature", "Handle")
347
- obj_handle.Shape = handle_solid
348
- obj_handle.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
349
-
350
- # --- 5. Fuse all parts ---
351
- fused = obj_body.Shape.fuse(obj_lid.Shape)
352
- fused = fused.fuse(obj_spout.Shape)
353
- fused = fused.fuse(obj_handle.Shape)
354
-
355
- obj_final = doc.addObject("Part::Feature", "Teapot_Complete")
356
- obj_final.Shape = fused
357
- obj_final.ViewObject.ShapeColor = (0.9, 0.6, 0.6)
358
-
359
- # Hide individual parts for clarity
360
- obj_body.ViewObject.Visibility = False
361
- obj_lid.ViewObject.Visibility = False
362
- obj_spout.ViewObject.Visibility = False
363
- obj_handle.ViewObject.Visibility = False
364
-
365
- doc.recompute()
366
-
367
- Gui.activeDocument().activeView().viewAxometric()
368
- Gui.SendMsgToActiveView("ViewFit")
369
-
370
- return doc
371
-
372
- if __name__ == "__main__":
373
- createTeapot()
374
-
375
- - This is a good example for a herringbone gear. If asked to make a herringbone gear, generate similar:
376
- #Herringbone gear
377
-
378
- import FreeCAD as App
379
- import FreeCADGui as Gui
380
- import Part
381
- import math
382
- from FreeCAD import Vector, Placement, Rotation
383
-
384
- def createHerringboneGear(
385
- num_teeth=20,
386
- module=5.0,
387
- pressure_angle_deg=20.0,
388
- helix_angle_deg=25.0,
389
- face_width=50.0,
390
- central_bore_diameter=20.0,
391
- num_loft_sections=50,
392
- addendum_factor=1.0,
393
- dedendum_factor=1.25,
394
- tooth_radial_offset=1.5 # Teeth pushed radially outward
395
- ):
396
- doc = App.newDocument("HerringboneGear")
397
-
398
- pressure_angle_rad = math.radians(pressure_angle_deg)
399
- helix_angle_rad = math.radians(helix_angle_deg)
400
-
401
- pitch_diameter = module * num_teeth
402
- pitch_radius = pitch_diameter / 2
403
- addendum = addendum_factor * module
404
- dedendum = dedendum_factor * module
405
- root_radius = pitch_radius - dedendum + tooth_radial_offset
406
- outer_radius = pitch_radius + addendum + tooth_radial_offset
407
-
408
- gear_total_height = face_width
409
- half_gear_height = gear_total_height / 2
410
-
411
- total_angular_twist_rad = (face_width * math.tan(helix_angle_rad)) / pitch_radius
412
-
413
- gear_hub = doc.addObject("Part::Cylinder", "GearHub")
414
- gear_hub.Radius = outer_radius - tooth_radial_offset
415
- gear_hub.Height = gear_total_height
416
- gear_hub.Placement.Base = Vector(0, 0, 0)
417
-
418
- if central_bore_diameter > 0:
419
- bore_radius = central_bore_diameter / 2
420
- central_bore = doc.addObject("Part::Cylinder", "CentralBore")
421
- central_bore.Radius = bore_radius
422
- central_bore.Height = gear_total_height
423
- central_bore.Placement.Base = Vector(0, 0, 0)
424
-
425
- hub_base = doc.addObject("Part::Cut", "Hub_With_Bore")
426
- hub_base.Base = gear_hub
427
- hub_base.Tool = central_bore
428
- else:
429
- hub_base = gear_hub
430
-
431
- angle_per_tooth = 360.0 / num_teeth
432
-
433
- effective_half_angle_for_flank_base = (math.pi / num_teeth) / 2
434
- effective_half_angle_for_flank_tip = effective_half_angle_for_flank_base * 0.7
435
-
436
- P_A = Vector(root_radius * math.sin(effective_half_angle_for_flank_base), root_radius * math.cos(effective_half_angle_for_flank_base), 0)
437
- P_B = Vector(root_radius * math.sin(-effective_half_angle_for_flank_base), root_radius * math.cos(-effective_half_angle_for_flank_base), 0)
438
-
439
- P_C = Vector(outer_radius * math.sin(effective_half_angle_for_flank_tip), outer_radius * math.cos(effective_half_angle_for_flank_tip), 0)
440
- P_D = Vector(outer_radius * math.sin(-effective_half_angle_for_flank_tip), outer_radius * math.cos(-effective_half_angle_for_flank_tip), 0)
441
-
442
- e_flank1 = Part.LineSegment(P_B, P_D).toShape()
443
- e_flank2 = Part.LineSegment(P_A, P_C).toShape()
444
-
445
- def offset_midpoint(p1, p2, offset=0.1):
446
- mid = (p1 + p2).multiply(0.5)
447
- vec = p2.sub(p1)
448
- perp = Vector(-vec.y, vec.x, 0)
449
- perp.normalize()
450
- return mid.add(perp.multiply(offset))
451
-
452
- tip_midpoint = offset_midpoint(P_D, P_C, 0.1)
453
- e_tip_arc = Part.ArcOfCircle(P_D, tip_midpoint, P_C).toShape()
454
-
455
- root_midpoint = offset_midpoint(P_A, P_B, 0.1)
456
- e_root_arc = Part.ArcOfCircle(P_A, root_midpoint, P_B).toShape()
457
-
458
- try:
459
- tooth_profile_wire = Part.Wire([e_root_arc, e_flank1, e_tip_arc, e_flank2])
460
- except Exception as e:
461
- App.Console.PrintError(f"Error creating tooth profile wire: {e}. Using fallback wire.\n")
462
- fallback_edges = [
463
- Part.LineSegment(P_A, P_B).toShape(),
464
- Part.LineSegment(P_B, P_D).toShape(),
465
- Part.LineSegment(P_D, P_C).toShape(),
466
- Part.LineSegment(P_C, P_A).toShape()
467
- ]
468
- tooth_profile_wire = Part.Wire(fallback_edges)
469
-
470
- tooth_profile_face = Part.Face(tooth_profile_wire)
471
-
472
- helical_teeth_LH_fused = None
473
- lh_z_start = 0
474
- lh_z_end = half_gear_height
475
- lh_twist_start = 0
476
- lh_twist_end = total_angular_twist_rad / 2
477
-
478
- for tooth_idx in range(num_teeth):
479
- current_tooth_LH_profiles = []
480
- initial_tooth_rotation_deg = tooth_idx * angle_per_tooth
481
-
482
- for i in range(num_loft_sections + 1):
483
- z_pos_current = lh_z_start + (lh_z_end - lh_z_start) * (i / num_loft_sections)
484
- current_slice_twist_angle_rad = lh_twist_start + (lh_twist_end - lh_twist_start) * (i / num_loft_sections)
485
-
486
- combined_rotation_deg = initial_tooth_rotation_deg + math.degrees(current_slice_twist_angle_rad)
487
-
488
- profile_copy = tooth_profile_face.copy()
489
- profile_copy.Placement = Placement(
490
- Vector(0, 0, z_pos_current),
491
- Rotation(Vector(0, 0, 1), combined_rotation_deg)
492
- )
493
- current_tooth_LH_profiles.append(profile_copy)
494
-
495
- helical_tooth_LH_solid = Part.makeLoft(current_tooth_LH_profiles, True)
496
-
497
- if helical_teeth_LH_fused is None:
498
- helical_teeth_LH_fused = helical_tooth_LH_solid
499
- else:
500
- helical_teeth_LH_fused = helical_teeth_LH_fused.fuse(helical_tooth_LH_solid)
501
-
502
- obj_helical_LH = doc.addObject("Part::Feature", "HelicalTeeth_Left_Section")
503
- obj_helical_LH.Shape = helical_teeth_LH_fused
504
- obj_helical_LH.ViewObject.ShapeColor = (0.7, 0.7, 0.9)
505
-
506
- helical_teeth_RH_fused = None
507
- rh_z_start = half_gear_height
508
- rh_z_end = gear_total_height
509
- rh_twist_start = total_angular_twist_rad / 2
510
- rh_twist_end = 0
511
-
512
- for tooth_idx in range(num_teeth):
513
- current_tooth_RH_profiles = []
514
- initial_tooth_rotation_deg = tooth_idx * angle_per_tooth
515
-
516
- for i in range(num_loft_sections + 1):
517
- z_pos_current = rh_z_start + (rh_z_end - rh_z_start) * (i / num_loft_sections)
518
- current_slice_twist_angle_rad = rh_twist_start + (rh_twist_end - rh_twist_start) * (i / num_loft_sections)
519
-
520
- combined_rotation_deg = initial_tooth_rotation_deg + math.degrees(current_slice_twist_angle_rad)
521
-
522
- profile_copy = tooth_profile_face.copy()
523
- profile_copy.Placement = Placement(
524
- Vector(0, 0, z_pos_current),
525
- Rotation(Vector(0, 0, 1), combined_rotation_deg)
526
- )
527
- current_tooth_RH_profiles.append(profile_copy)
528
-
529
- helical_tooth_RH_solid = Part.makeLoft(current_tooth_RH_profiles, True)
530
-
531
- if helical_teeth_RH_fused is None:
532
- helical_teeth_RH_fused = helical_tooth_RH_solid
533
- else:
534
- helical_teeth_RH_fused = helical_teeth_RH_fused.fuse(helical_tooth_RH_solid)
535
-
536
- obj_helical_RH = doc.addObject("Part::Feature", "HelicalTeeth_Right_Section")
537
- obj_helical_RH.Shape = helical_teeth_RH_fused
538
- obj_helical_RH.ViewObject.ShapeColor = (0.7, 0.9, 0.7)
539
-
540
- combined_teeth_sections = doc.addObject("Part::Fuse", "Combined_Teeth_Sections")
541
- combined_teeth_sections.Base = obj_helical_LH
542
- combined_teeth_sections.Tool = obj_helical_RH
543
-
544
- final_gear = doc.addObject("Part::Fuse", "HerringboneGear_Complete")
545
- final_gear.Base = hub_base
546
- final_gear.Tool = combined_teeth_sections
547
-
548
- final_gear.ViewObject.ShapeColor = (0.8, 0.6, 0.9)
549
-
550
- if central_bore_diameter > 0:
551
- central_bore.ViewObject.Visibility = False
552
- gear_hub.ViewObject.Visibility = False
553
- hub_base.ViewObject.Visibility = False
554
- obj_helical_LH.ViewObject.Visibility = False
555
- obj_helical_RH.ViewObject.Visibility = False
556
- combined_teeth_sections.ViewObject.Visibility = False
557
-
558
- doc.recompute()
559
- Gui.activeDocument().activeView().viewAxometric()
560
- Gui.SendMsgToActiveView("ViewFit")
561
-
562
- return doc
563
-
564
- if __name__ == "__main__":
565
- createHerringboneGear()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/llm_client.py CHANGED
@@ -1,15 +1,19 @@
1
  import os
2
  from pathlib import Path
3
- from huggingface_hub import hf_hub_download, InferenceClient
4
  from langchain_community.vectorstores import FAISS
5
- from langchain_huggingface import HuggingFaceEmbeddings
 
 
6
  from dotenv import load_dotenv
 
7
 
8
- # Load HF token from .env
 
9
  load_dotenv()
10
- HF_TOKEN = os.getenv("HF_TOKEN")
11
- if not HF_TOKEN:
12
- raise ValueError("HF_TOKEN missing in .env")
13
 
14
  # FAISS vectorstore setup
15
  REPO_ID = "Yas1n/CADomatic_vectorstore"
@@ -27,49 +31,62 @@ vectorstore = FAISS.load_local(
27
  allow_dangerous_deserialization=True,
28
  index_name="index_oss120b"
29
  )
30
- retriever = vectorstore.as_retriever(search_kwargs={"k": 40})
31
 
32
- # Initialize HF InferenceClient
33
- client = InferenceClient(provider="fireworks-ai", api_key=HF_TOKEN)
34
- MODEL_NAME = "openai/gpt-oss-120b"
35
 
36
- # Prompt function
37
- def prompt_llm(user_prompt: str) -> str:
38
- # Retrieve context from FAISS
 
 
 
 
 
 
39
  docs = retriever.invoke(user_prompt)
40
  context = "\n\n".join(doc.page_content for doc in docs)
41
 
42
- final_prompt = f"""
 
 
 
 
 
 
 
43
  You are a helpful assistant that writes FreeCAD Python scripts from CAD instructions.
44
  Use the following FreeCAD wiki documentation as context:
45
 
46
  {context}
47
 
48
- Instruction:
 
 
 
49
  {user_prompt}
50
 
51
  Respond with valid FreeCAD 1.0.1 Python code only, no extra comments.
52
  """
53
 
 
 
 
54
  try:
55
- completion = client.chat.completions.create(
56
- model=MODEL_NAME,
57
- messages=[{"role": "user", "content": final_prompt}],
58
- )
59
- return completion.choices[0].message.content
60
- except Exception as e:
61
- print("❌ Error in HF response:", e)
62
- return ""
63
 
64
- # Main CLI
65
- if __name__ == "__main__":
66
- prompt = input("Describe your FreeCAD part: ")
67
- code = prompt_llm(prompt)
68
 
69
- # Save generated code
70
- output_path = Path("generated/result_script.py")
71
- output_path.parent.mkdir(parents=True, exist_ok=True)
72
- with open(output_path, "w", encoding="utf-8") as f:
73
- f.write(code)
74
 
75
- print(f"✅ Code generated and written to {output_path}")
 
 
 
 
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 langchain.memory import ConversationBufferMemory
7
+ from langchain.schema import HumanMessage, AIMessage
8
  from dotenv import load_dotenv
9
+ from langchain_huggingface import HuggingFaceEmbeddings
10
 
11
+
12
+ # Load GEMINI API key
13
  load_dotenv()
14
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
15
+ if not GEMINI_API_KEY:
16
+ raise ValueError("GEMINI_API_KEY missing in .env")
17
 
18
  # FAISS vectorstore setup
19
  REPO_ID = "Yas1n/CADomatic_vectorstore"
 
31
  allow_dangerous_deserialization=True,
32
  index_name="index_oss120b"
33
  )
34
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 8})
35
 
36
+ # Conversation memory
37
+ memory = ConversationBufferMemory(return_messages=True)
 
38
 
39
+ # Gemini 2.5 Flash (LangChain wrapper, no genai import needed)
40
+ llm = ChatGoogleGenerativeAI(
41
+ model="gemini-2.5-flash",
42
+ temperature=1.2,
43
+ api_key=GEMINI_API_KEY # Use the key directly
44
+ )
45
+
46
+ def build_prompt(user_prompt: str) -> str:
47
+ """Builds full prompt with RAG context and conversation history."""
48
  docs = retriever.invoke(user_prompt)
49
  context = "\n\n".join(doc.page_content for doc in docs)
50
 
51
+ history_text = ""
52
+ for msg in memory.chat_memory.messages:
53
+ if isinstance(msg, HumanMessage):
54
+ history_text += f"User: {msg.content}\n"
55
+ elif isinstance(msg, AIMessage):
56
+ history_text += f"Assistant: {msg.content}\n"
57
+
58
+ return f"""
59
  You are a helpful assistant that writes FreeCAD Python scripts from CAD instructions.
60
  Use the following FreeCAD wiki documentation as context:
61
 
62
  {context}
63
 
64
+ Here is the conversation so far:
65
+ {history_text}
66
+
67
+ Current instruction:
68
  {user_prompt}
69
 
70
  Respond with valid FreeCAD 1.0.1 Python code only, no extra comments.
71
  """
72
 
73
+ def prompt_llm(user_prompt: str) -> str:
74
+ """Generate FreeCAD code using Gemini 2.5 Flash."""
75
+ final_prompt = build_prompt(user_prompt)
76
  try:
77
+ response = llm.invoke(final_prompt)
78
+ result = response.content.strip()
 
 
 
 
 
 
79
 
80
+ # Save user and AI messages in memory
81
+ memory.chat_memory.add_user_message(user_prompt)
82
+ memory.chat_memory.add_ai_message(result)
 
83
 
84
+ return result
85
+ except Exception as e:
86
+ print("❌ Error generating FreeCAD code:", e)
87
+ return ""
 
88
 
89
+ def reset_memory():
90
+ """Clear conversation history."""
91
+ global memory
92
+ memory = ConversationBufferMemory(return_messages=True)