spyder-code-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,534 @@
1
+ import json
2
+ import re
3
+ import os
4
+ from spyder.api.widgets.main_widget import PluginMainWidget
5
+ from qtpy.QtWidgets import (QVBoxLayout, QTextBrowser, QTextEdit, QPushButton,
6
+ QWidget, QHBoxLayout, QDialog, QLabel, QLineEdit,
7
+ QFormLayout, QMenu, QAction, QFileDialog)
8
+ from openai import OpenAI
9
+ from qtpy.QtCore import QThread, Signal, QTimer
10
+
11
+ SETTINGS_FILE = os.path.join(os.path.expanduser("~"), ".agent_config")
12
+
13
+ # API setting form
14
+ class APIDialog(QDialog):
15
+ def __init__(self, parent=None):
16
+ super().__init__(parent)
17
+ self.setWindowTitle("API Settings")
18
+ self.setFixedWidth(400)
19
+
20
+ # create field
21
+ self.model_name_input = QLineEdit()
22
+ self.base_url_input = QLineEdit()
23
+ self.api_key_input = QLineEdit()
24
+ self.api_key_input.setEchoMode(QLineEdit.Password) # hide pass
25
+
26
+ # form design
27
+ form_layout = QFormLayout()
28
+ form_layout.addRow(QLabel("Model Name"), self.model_name_input)
29
+ form_layout.addRow(QLabel("Base URL"), self.base_url_input)
30
+ form_layout.addRow(QLabel("API Key"), self.api_key_input)
31
+
32
+ # submit
33
+ self.save_btn = QPushButton("Save")
34
+ self.cancel_btn = QPushButton("Cancel")
35
+ self.save_btn.clicked.connect(self.accept)
36
+ self.cancel_btn.clicked.connect(self.reject)
37
+
38
+ btn_layout = QHBoxLayout()
39
+ btn_layout.addStretch()
40
+ btn_layout.addWidget(self.cancel_btn)
41
+ btn_layout.addWidget(self.save_btn)
42
+
43
+ main_layout = QVBoxLayout()
44
+ main_layout.addLayout(form_layout)
45
+ main_layout.addLayout(btn_layout)
46
+ self.setLayout(main_layout)
47
+
48
+ def get_values(self):
49
+ return self.base_url_input.text().strip(), self.api_key_input.text().strip(), self.model_name_input.text().strip()
50
+
51
+ # Define Agent
52
+ class LLMWorker(QThread):
53
+ text_received = Signal(str)
54
+ error_occurred = Signal(str)
55
+ finished_response = Signal()
56
+
57
+ def __init__(self,
58
+ prompt,
59
+ URL,
60
+ API,
61
+ model_name,
62
+ context_code=""):
63
+ super().__init__()
64
+ self.prompt = prompt
65
+ self.context_code = context_code
66
+ self.URL= URL
67
+ self.API= API
68
+ self.model_name= model_name
69
+
70
+ def run(self):
71
+ full_prompt = (
72
+ "You are an expert AI Python Programming Assistant inside Spyder IDE.\n"
73
+ "Always respond ONLY with a JSON object with these exact keys:\n"
74
+ "- error_type: short error class name\n"
75
+ "- description: one sentence explanation\n"
76
+ "- solution: plain text fix\n"
77
+ "- example: only the fixed code snippet\n"
78
+ "- fixed_file: the filename that needs to be changed (e.g. 'model.py')\n"
79
+ "- fixed_code: the complete corrected content of that file\n\n"
80
+ "If no file context exists, only fix the problematic line.\n\n"
81
+ f"--- PROJECT FILES ---\n{self.context_code}\n--------------------\n\n"
82
+ f"User: {self.prompt}"
83
+ )
84
+
85
+ try:
86
+ client = OpenAI(
87
+ base_url= self.URL,
88
+ api_key= self.API
89
+ )
90
+
91
+ response= client.chat.completions.create(
92
+ model= self.model_name,
93
+ messages=[
94
+ {"role" : "system", "content" : full_prompt}
95
+ ],
96
+ temperature=0.8,
97
+ response_format={"type" : "json_object"}
98
+ )
99
+ answer= response.choices[0].message.content
100
+ self.text_received.emit(answer)
101
+
102
+ except Exception as e:
103
+ self.error_occurred.emit(str(e))
104
+
105
+ finally:
106
+ self.finished_response.emit()
107
+
108
+ # Agent-Container
109
+ class AgentContainer(PluginMainWidget):
110
+ def __init__(self,
111
+ *args,
112
+ **kwargs):
113
+ super().__init__(*args, **kwargs)
114
+
115
+ self.current_base_url = ""
116
+ self.current_api_key = ""
117
+ self.current_model_name= ""
118
+
119
+ self.load_setting_from_file()
120
+ def get_title(self):
121
+ return "Code Agent"
122
+
123
+ def set_ipython(self, ipython):
124
+ self.ipython = ipython
125
+ self.shell = None
126
+
127
+ ipython.sig_shellwidget_created.connect(self.inject_error_handler)
128
+
129
+ shell = ipython.get_current_shellwidget()
130
+ if shell:
131
+ self.inject_error_handler(shell)
132
+
133
+ def set_editor(self, editor):
134
+ self.editor = editor
135
+
136
+ def set_projects(self, projects):
137
+ self.projects= projects
138
+
139
+ def get_project_files(self):
140
+ result = {}
141
+ for path in self.selected_files:
142
+ try:
143
+ with open(path, 'r', encoding='utf-8') as f:
144
+ result[os.path.basename(path)] = f.read()
145
+ except Exception:
146
+ pass
147
+ return result
148
+
149
+ def on_project_closed(self, project_path):
150
+ self.active_project = None
151
+ self.add_file.setEnabled(False)
152
+ self.add_file.setMenu(None)
153
+ if hasattr(self, 'selected_project_files'):
154
+ self.selected_project_files.clear()
155
+
156
+ def update_project_files_menu(self, root_path=None):
157
+ if not root_path:
158
+ if not self.active_project:
159
+ return
160
+ if isinstance(self.active_project, str):
161
+ root_path = self.active_project
162
+ else:
163
+ root_path = getattr(self.active_project, 'root_path', None)
164
+
165
+ if not root_path or not os.path.exists(root_path):
166
+ return
167
+
168
+ menu = QMenu(self)
169
+ found_files = False
170
+
171
+ for root, dirs, files in os.walk(root_path):
172
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ('venv', '__pycache__', 'env', '.venv')]
173
+ for file in files:
174
+ if file.endswith('.py'):
175
+ found_files = True
176
+ full_path = os.path.join(root, file)
177
+ rel_path = os.path.relpath(full_path, root_path)
178
+
179
+ action = QAction(rel_path, self)
180
+ action.triggered.connect(lambda checked, path=full_path: self.toggle_project_file(path))
181
+ menu.addAction(action)
182
+
183
+ if found_files:
184
+ self.add_file.setMenu(menu)
185
+ else:
186
+ self.add_file.setMenu(None)
187
+
188
+ def toggle_project_file(self, file_path):
189
+ filename = os.path.basename(file_path)
190
+
191
+ if file_path in self.selected_project_files:
192
+ del self.selected_project_files[file_path]
193
+ self.chat_display.append(f"<b style='color:orange;'>System:</b> Removed <code>{filename}</code> from context.")
194
+ else:
195
+
196
+ try:
197
+ with open(file_path, 'r', encoding='utf-8') as f:
198
+ content = f.read()
199
+ self.selected_project_files[file_path] = content
200
+ self.chat_display.append(f"<b style='color:green;'>System:</b> Added <code>{filename}</code> to context.")
201
+ except Exception as e:
202
+ self.chat_display.append(f"<b style='color:red;'>Error reading file: {e}</b>")
203
+
204
+ def inject_error_handler(self, shell):
205
+ self.shell = shell
206
+ print(f"✅ New shell: {shell}")
207
+ self._connect_signals(shell)
208
+
209
+ def _connect_signals(self, shell):
210
+ print("✅ Connecting signals...")
211
+ self.error_file = os.path.join(os.path.expanduser("~"), ".agent_last_error")
212
+
213
+ code = f"""
214
+ import traceback as _tb, json as _json
215
+
216
+ _original_showtraceback = get_ipython().showtraceback
217
+
218
+ def _agent_showtraceback(*args, **kwargs):
219
+ try:
220
+ with open('{self.error_file}', 'w') as f:
221
+ _json.dump({{'error': ''.join(_tb.format_exc())}}, f)
222
+ except:
223
+ pass
224
+ _original_showtraceback(*args, **kwargs)
225
+
226
+ get_ipython().showtraceback = _agent_showtraceback
227
+ print("__AGENT_READY__")
228
+ """
229
+ shell.execute(code, hidden=False)
230
+
231
+ # check file every 500ms
232
+ self.error_timer = QTimer()
233
+ self.error_timer.setInterval(500)
234
+ self.error_timer.timeout.connect(self.check_error_file)
235
+ self.error_timer.start()
236
+ print("✅ Done!")
237
+
238
+ def check_error_file(self):
239
+ if not hasattr(self, 'error_file'):
240
+ return
241
+ try:
242
+ if os.path.exists(self.error_file):
243
+ with open(self.error_file, 'r') as f:
244
+ data = json.load(f)
245
+ os.remove(self.error_file)
246
+ error_text = data.get('error', '')
247
+ if error_text and 'NoneType: None' not in error_text:
248
+ print("🔴 Error caught:", error_text[:100])
249
+ self.on_auto_error(error_text)
250
+ except Exception:
251
+ pass
252
+
253
+ def on_auto_error(self, error_text):
254
+ self.chat_display.append(
255
+ "<b style='color:orange;'>🔴 Error detected — analyzing...</b>"
256
+ )
257
+ self.user_input.setPlainText(f"Fix this error:\n{error_text}")
258
+ self.send_message()
259
+
260
+ def load_setting_from_file(self):
261
+ if os.path.exists(SETTINGS_FILE):
262
+ try:
263
+ with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
264
+ data = json.load(f)
265
+ self.current_base_url = data.get("base_url", "")
266
+ self.current_api_key = data.get("api_key", "")
267
+ self.current_model_name = data.get("model_name", "")
268
+ except Exception:
269
+ pass
270
+
271
+ def save_settings_to_file(self):
272
+ try:
273
+ data = {
274
+ "base_url": self.current_base_url,
275
+ "api_key": self.current_api_key,
276
+ "model_name": self.current_model_name
277
+ }
278
+ with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
279
+ json.dump(data, f, ensure_ascii=False, indent=4)
280
+ return True
281
+ except Exception:
282
+ return False
283
+
284
+ def summarize_history(self):
285
+ if len(self.conversation_history) < 10:
286
+ return
287
+
288
+ summary_prompt = "Summarize this conversation briefly, keeping key errors and solutions:"
289
+ messages = [{"role": "system", "content": summary_prompt}]
290
+ messages += self.conversation_history
291
+
292
+ client = OpenAI(base_url=self.current_base_url, api_key=self.current_api_key)
293
+ response = client.chat.completions.create(
294
+ model=self.current_model_name,
295
+ messages=messages,
296
+ max_tokens=100
297
+ )
298
+ summary = response.choices[0].message.content
299
+
300
+ self.conversation_history = [
301
+ {"role": "assistant", "content": f"Previous conversation summary: {summary}"}
302
+ ]
303
+
304
+ def setup(self):
305
+ self.conversation_history= []
306
+ self.worker = None
307
+ self.editor = None
308
+ self.active_project = None
309
+ self.selected_project_files = {}
310
+
311
+ # create widget
312
+ self.chat_display = QTextBrowser()
313
+ self.chat_display.setOpenExternalLinks(False)
314
+
315
+ self.selected_files = [] # لیست فایل‌های انتخاب شده
316
+ self.add_file_btn = QPushButton("+ Add file")
317
+ #self.add_file_btn.setFixedWidth(100)
318
+ self.add_file_btn.clicked.connect(self.add_file)
319
+
320
+ self.user_input = QTextEdit()
321
+ self.user_input.setMaximumHeight(80)
322
+
323
+ self.send_btn = QPushButton("Send")
324
+ self.send_btn.setFixedWidth(100)
325
+ self.send_btn.clicked.connect(self.send_message)
326
+
327
+ self.apply_btn = QPushButton("✅ Apply Fix")
328
+ self.apply_btn.setFixedWidth(120)
329
+ self.apply_btn.clicked.connect(self.apply_fix)
330
+
331
+ buttons_layout = QHBoxLayout()
332
+ buttons_layout.addWidget(self.send_btn)
333
+ buttons_layout.addStretch()
334
+ buttons_layout.addWidget(self.apply_btn)
335
+
336
+ layout = QVBoxLayout()
337
+ layout.addWidget(self.add_file_btn, stretch=3)
338
+ layout.addWidget(self.chat_display, stretch=4)
339
+ layout.addWidget(self.user_input, stretch=2)
340
+ layout.addLayout(buttons_layout, stretch=1)
341
+
342
+ # set center
343
+ central = QWidget()
344
+ central.setLayout(layout)
345
+ self.setLayout(QVBoxLayout())
346
+ self.layout().addWidget(central)
347
+
348
+ def add_file(self):
349
+ path, _ = QFileDialog.getOpenFileName(
350
+ self, "Select file", "", "Python files (*.py);;All files (*)"
351
+ )
352
+ if path and path not in self.selected_files:
353
+ self.selected_files.append(path)
354
+ self.chat_display.append(
355
+ f"<b style='color:green;'>📄 Added:</b> {os.path.basename(path)}"
356
+ )
357
+
358
+ def set_API(self):
359
+ dialog = APIDialog(self)
360
+
361
+ dialog.base_url_input.setText(self.current_base_url)
362
+ dialog.api_key_input.setText(self.current_api_key)
363
+ dialog.model_name_input.setText(self.current_model_name)
364
+
365
+ if dialog.exec_() == QDialog.Accepted:
366
+ url, key, name = dialog.get_values()
367
+ if url and key:
368
+ self.current_base_url = url
369
+ self.current_api_key = key
370
+ self.current_model_name= name
371
+
372
+ if self.save_settings_to_file():
373
+ self.chat_display.append("<b style='color:green;'>System:</b> API settings saved to config file.")
374
+ else:
375
+ self.chat_display.append("<b style='color:red;'>System:</b> API updated but failed to save to file.")
376
+ else:
377
+ self.chat_display.append("<b style='color:red;'>System:</b> URL or Key cannot be empty.")
378
+
379
+ def get_current_file_content(self):
380
+ try:
381
+ return self.editor.get_current_editor().toPlainText()
382
+ except:
383
+ return ""
384
+
385
+ def send_message(self):
386
+ if not self.current_base_url or not self.current_api_key or not self.current_model_name:
387
+ self.chat_display.append(
388
+ "<b style='color:orange;'>System:</b> Please set your API settings first."
389
+ )
390
+ self.set_API()
391
+ if not self.current_base_url or not self.current_api_key or not self.current_model_name:
392
+ return
393
+
394
+ user_text = self.user_input.toPlainText().strip()
395
+ if not user_text:
396
+ return
397
+
398
+ self.chat_display.append(f"<b>You:</b> {user_text}")
399
+ self.user_input.clear()
400
+ self.send_btn.setEnabled(False)
401
+ self.chat_display.append("<b>Agent:</b> ...")
402
+
403
+ context = self.get_project_files() or {"current": self.get_current_file_content()}
404
+ context_str = "\n\n".join([
405
+ f"# FILE: {name}\n{code}"
406
+ for name, code in context.items()
407
+ ])
408
+
409
+ self.worker = LLMWorker(
410
+ prompt=user_text,
411
+ context_code=context_str,
412
+ URL= self.current_base_url,
413
+ API= self.current_api_key,
414
+ model_name= self.current_model_name
415
+ )
416
+ self.worker.text_received.connect(self.on_response)
417
+ self.worker.error_occurred.connect(self.on_error)
418
+ self.worker.finished_response.connect(self.on_finished)
419
+ self.worker.start()
420
+
421
+ def format_code_blocks(self, text):
422
+ text = re.sub(
423
+ r'```(?:python)?\n?(.*?)```',
424
+ lambda m: (
425
+ f'<div style="background:#1e1e2e; color:#cdd6f4; '
426
+ f'padding:8px 12px; margin:6px 0; border-radius:4px; '
427
+ f'font-family:monospace; white-space:pre;">'
428
+ f'{m.group(1).strip()}</div>'
429
+ ),
430
+ text, flags=re.DOTALL
431
+ )
432
+ text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
433
+ return text
434
+
435
+ def apply_fix(self):
436
+ if not hasattr(self, 'pending_fix') or not self.pending_fix:
437
+ return
438
+
439
+ target_file = getattr(self, 'pending_fix_file', None)
440
+
441
+ try:
442
+ if target_file:
443
+ for path in self.selected_files:
444
+ if os.path.basename(path) == target_file:
445
+ with open(path, 'w', encoding='utf-8') as f:
446
+ f.write(self.pending_fix)
447
+
448
+ self._reload_file_in_editor(path)
449
+ break
450
+ else:
451
+ editor = self.editor.get_current_editor()
452
+ editor.set_text(self.pending_fix)
453
+
454
+ self.pending_fix = None
455
+ self.pending_fix_file = None
456
+ self.apply_btn.setEnabled(False)
457
+ self.chat_display.append("<b style='color:green;'>✅ Fix applied!</b>")
458
+ except Exception as e:
459
+ self.chat_display.append(f"<b style='color:red;'>Error: {e}</b>")
460
+
461
+ def _reload_file_in_editor(self, path):
462
+ try:
463
+ self.editor.load(path)
464
+ except Exception:
465
+ pass
466
+
467
+ def on_response(self, text):
468
+ try:
469
+ data = json.loads(text)
470
+ except json.JSONDecodeError:
471
+ data = {"solution": text}
472
+
473
+ html_parts = []
474
+ if "error_type" in data:
475
+ html_parts.append(
476
+ f"<div style='border-left:3px solid #c00; "
477
+ f"padding:6px 10px; margin:4px 0; border-radius:4px;'>"
478
+ f"<b style='color:#c00;'>❌ {data['error_type']}</b><br>"
479
+ f"{data.get('description', '')}</div>"
480
+ )
481
+ if "solution" in data:
482
+ solution = self.format_code_blocks(data['solution'])
483
+ html_parts.append(
484
+ f"<div style='border-left:3px solid #0a0; "
485
+ f"padding:6px 10px; margin:4px 0; border-radius:4px;'>"
486
+ f"<b style='color:#070;'>✅ Solution</b><br>{solution}</div>"
487
+ )
488
+ if "example" in data:
489
+ code = data['example'].replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br>')
490
+ html_parts.append(
491
+ f"<div style='color:#cdd6f4; padding:8px 12px; "
492
+ f"margin:4px 0; border-radius:4px; font-family:monospace;'>"
493
+ f"{code}</div>"
494
+ )
495
+ if "fixed_code" in data and data['fixed_code']:
496
+ self.pending_fix= data['fixed_code']
497
+ html_parts.append(
498
+ "<div style='margin:4px 0;'>"
499
+ "<button onclick='void(0)' style='background:#0a0; color:white; "
500
+ "border:none; padding:6px 14px; border-radius:4px; cursor:pointer;'>"
501
+ "✅ Apply Fix</button></div>"
502
+ )
503
+ self.pending_fix = data["fixed_code"]
504
+ self.apply_btn.setEnabled(True)
505
+
506
+ if "fixed_file" in data:
507
+ self.pending_fix_file = data["fixed_file"]
508
+ html_parts.append(
509
+ "<div style='margin:4px 0;'>"
510
+ "<button onclick='void(0)' style='background:#0a0; color:white; "
511
+ "border:none; padding:6px 14px; border-radius:4px; cursor:pointer;'>"
512
+ "✅ File Fixed</button></div>"
513
+ )
514
+
515
+ cursor = self.chat_display.textCursor()
516
+ cursor.movePosition(cursor.End)
517
+ cursor.select(cursor.LineUnderCursor)
518
+ cursor.removeSelectedText()
519
+ cursor.deletePreviousChar()
520
+ self.chat_display.setTextCursor(cursor)
521
+ self.chat_display.append("<b>Agent:</b>")
522
+ self.chat_display.insertHtml("".join(html_parts))
523
+
524
+ def on_error(self, error_msg):
525
+ self.chat_display.undo()
526
+ self.chat_display.append(f"<b style='color:red;'>Error:</b> {error_msg}")
527
+
528
+ def on_finished(self):
529
+ self.send_btn.setEnabled(True)
530
+ if len(self.conversation_history) >= 10:
531
+ self.summarize_history()
532
+
533
+ def update_actions(self):
534
+ pass
@@ -0,0 +1,50 @@
1
+ from spyder.api.plugins import SpyderDockablePlugin, Plugins
2
+ from spyder.api.plugin_registration.decorators import on_plugin_available
3
+ from spyder.api.translations import get_translation
4
+ from .container import AgentContainer
5
+
6
+ _ = get_translation("spyder_code_agent")
7
+
8
+ class CodeAgent(SpyderDockablePlugin):
9
+ NAME = "code_agent"
10
+ REQUIRES = []
11
+ OPTIONAL = [Plugins.Editor, Plugins.IPythonConsole, Plugins.Projects]
12
+ TABIFY = [Plugins.VariableExplorer]
13
+ WIDGET_CLASS = AgentContainer
14
+ CONF_SECTION = NAME
15
+
16
+ @staticmethod
17
+ def get_name():
18
+ return "Code Agent"
19
+
20
+ @staticmethod
21
+ def get_description():
22
+ return "AI coding assistant"
23
+
24
+ @classmethod
25
+ def get_icon(cls):
26
+ return cls.create_icon("python")
27
+
28
+ def on_initialize(self):
29
+ pass
30
+
31
+ @on_plugin_available(plugin=Plugins.Editor)
32
+ def on_editor_available(self):
33
+ editor = self.get_plugin(Plugins.Editor)
34
+ print("✅ Editor found:", editor)
35
+ self.get_widget().set_editor(editor)
36
+
37
+ @on_plugin_available(plugin=Plugins.IPythonConsole)
38
+ def on_ipython_available(self):
39
+ ipython = self.get_plugin(Plugins.IPythonConsole)
40
+ print("✅ IPython found:", ipython)
41
+ self.get_widget().set_ipython(ipython)
42
+
43
+ @on_plugin_available(plugin=Plugins.Projects)
44
+ def on_project_available(self):
45
+ projects = self.get_plugin(Plugins.Projects)
46
+ print("✅ Projects found:", projects)
47
+ self.get_widget().set_projects(projects)
48
+
49
+ def update_font(self):
50
+ pass
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: spyder-code-agent
3
+ Version: 0.1.0
4
+ Summary: AI-powered coding assistant plugin for Spyder IDE
5
+ Author-email: Amir <mohammadamirdehghanian@gmail.com>
6
+ License: MIT
7
+ Keywords: spyder,plugin,ai,assistant,llm
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: spyder>=6.0
14
+ Requires-Dist: openai
15
+ Dynamic: license-file
16
+
17
+ # Spyder Code Agent
18
+
19
+ ![Python](https://img.shields.io/badge/Python-3.10%2B-blue)
20
+ ![Spyder](https://img.shields.io/badge/Spyder-6.0%2B-red)
21
+ ![License](https://img.shields.io/badge/License-MIT-green)
22
+
23
+ **AI‑powered coding assistant for Spyder IDE** – automatically detects runtime errors in your Python/ML scripts and fixes them with one click.
24
+
25
+ ![Spyder Code Agent in action](img/demo.gif)
26
+
27
+ ## Overview
28
+
29
+ Spyder Code Agent is a plugin that turns Spyder into a **self‑debugging IDE**. It listens to errors thrown in the IPython console, sends the relevant code (your selected files + current editor) to any OpenAI‑compatible LLM, and presents a ready‑to‑apply fix. No manual copy‑paste of tracebacks.
30
+
31
+ Perfect for **machine learning and data science** workflows where you frequently tweak code and run into `KeyError`, `ValueError`, shape mismatches, or missing imports.
32
+
33
+ ## ✨ Key Features
34
+
35
+ - **Automatic error capture** – Hooks into Spyder's IPython console; no need to type anything.
36
+ - **Selective file context** – Choose exactly which `.py` files the agent can see (not your whole project).
37
+ - **One‑click code replacement** – After the agent suggests a fix, click **Apply Fix** and the file is updated instantly.
38
+ - **Works with any OpenAI‑compatible API** – Use OpenAI, local LLMs (via vLLM, Ollama, LM Studio), or any custom endpoint.
39
+ - **Persistent API settings** – Base URL, API key, and model name are saved in `~/.agent_config`.
40
+ - **Built for Spyder 6+** – Seamlessly docks into the IDE.
41
+
42
+ ## 📋 Prerequisites
43
+
44
+ - **Spyder IDE** 6.0 or higher
45
+ - **Python** 3.8+
46
+ - An OpenAI‑compatible API endpoint and key (e.g., [OpenAI](https://platform.openai.com/), [Groq](https://groq.com/), [LocalAI](https://localai.io/), etc.)
47
+
48
+ ## ⚙️ Installation
49
+
50
+ ### From PyPI (recommended)
51
+
52
+ ```bash
53
+ pip install spyder-code-agent
54
+ ```
55
+
56
+ ### From source (for development)
57
+
58
+ ```bash
59
+ git clone https://github.com/kasra7900/Spyder-Code-Agent.git
60
+ cd Spyder-Code-Agent
61
+ pip install -e .
62
+ ```
63
+
64
+ ## 🚀 Usage
65
+
66
+ ### 1. First launch – API settings
67
+
68
+ When you first open the Code Agent pane, it will automatically show the **API Settings** dialog:
69
+
70
+ - **Base URL** – e.g., `https://api.openai.com/v1` or `http://localhost:1234/v1`
71
+ - **API Key** – your secret key
72
+ - **Model Name** – e.g., `Deepseek v3`, `gpt-4`, `llama3`, `codellama`
73
+
74
+ Save the settings – they are stored in `~/.agent_config` and reused next time.
75
+
76
+ > **Tip**: You can change these later by deleting the config file (`~/.agent_config`) and restarting Spyder – the dialog will appear again.
77
+
78
+ ### 2. Add files to the context
79
+
80
+ Use the **+ Add file** button to select Python files (`.py`) that the agent should be allowed to read. These files will be included in every analysis.
81
+
82
+ - You can add multiple files.
83
+ - The plugin does **not** scan your whole project – only the files you explicitly add.
84
+
85
+ ### 3. Run your code as usual
86
+
87
+ Write your ML script (e.g., data loading, model training) in the Spyder editor. Execute it in the IPython console.
88
+
89
+ ### 4. When an error occurs…
90
+
91
+ The plugin captures the traceback automatically. It then:
92
+
93
+ - Combines the error message + stack trace
94
+ - Adds the content of all added files + the current editor content
95
+ - Sends everything to the LLM
96
+
97
+ Within seconds, the agent replies with:
98
+
99
+ - **Error type** (e.g., `KeyError`, `IndexError`)
100
+ - **Description** (plain English)
101
+ - **Solution** (explanation + fixed code snippet)
102
+ - **Fixed code** (the complete corrected file)
103
+
104
+ ### 5. Apply the fix
105
+
106
+ If you see a fix you trust, click the **✅ Apply Fix** button. The plugin will:
107
+
108
+ - Replace the target file (if the agent specified a filename) **or** the current editor content
109
+ - Reload the file in Spyder automatically
110
+
111
+ The error is resolved – no manual editing required.
112
+
113
+ ## 🔧 Example workflow
114
+
115
+ Let’s say you have a script `train.py`:
116
+
117
+ ```python
118
+ import pandas as pd
119
+ df = pd.read_csv('data.csv')
120
+ X = df.drop('target', axis=1) # but the column is actually 'label'
121
+ ```
122
+
123
+ Running this raises `KeyError: 'target'`. The agent sees the error, reads `train.py` (added to context), and suggests:
124
+
125
+ ```
126
+ ❌ KeyError
127
+ Description: Column 'target' does not exist in the DataFrame.
128
+ Solution: Change 'target' to the correct column name 'label'.
129
+ Fixed code:
130
+ X = df.drop('label', axis=1)
131
+ ```
132
+
133
+ You click **Apply Fix** – the file is updated instantly.
134
+
135
+ ## ⚙️ Under the hood
136
+
137
+ - **Error hook**: The plugin injects a custom `showtraceback` function into the IPython shell. Every error is written to a temporary file (`~/.agent_last_error`) and picked up by a `QTimer`.
138
+ - **LLM prompt**: The agent constructs a strict JSON prompt (error type, description, solution, fixed code, fixed file). The LLM must respond in that exact format.
139
+ - **File modification**: When you click **Apply Fix**, the plugin writes the new content to the corresponding file path (or to the current editor if no file path is given). It creates no backup by default – be sure to use version control.
140
+
141
+ ## 📄 Configuration
142
+
143
+ The plugin stores settings in `~/.agent_config` (JSON). Example:
144
+
145
+ ```json
146
+ {
147
+ "base_url": "https://api.openai.com/v1",
148
+ "api_key": "sk-...",
149
+ "model_name": "gpt-4"
150
+ }
151
+ ```
152
+
153
+ You can edit this file manually, but it’s easier to use the dialog that appears on first launch.
154
+
155
+ ## 🤝 Contributing
156
+
157
+ Issues and pull requests are welcome! To contribute:
158
+
159
+ 1. Fork the repo.
160
+ 2. Create a feature branch.
161
+ 3. Install development dependencies: `pip install -e .`
162
+ 4. Test your changes in Spyder.
163
+ 5. Submit a PR.
164
+
165
+ ## 📜 License
166
+
167
+ MIT License. See `LICENSE` for details.
168
+
169
+ ---
170
+
171
+ **Made with ❤️ for the data science and Machine Learning community**
172
+
173
+
174
+
@@ -0,0 +1,9 @@
1
+ spyder_code_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ spyder_code_agent/container.py,sha256=3hd2i-NDLKFgW2E-qJrwtyBBZHTUB9sLMDzaN-lltEw,20179
3
+ spyder_code_agent/plugin.py,sha256=WKZT_kOXQUQLQ8HhJvColGVti5efB7U0eXr90vAy9PU,1571
4
+ spyder_code_agent-0.1.0.dist-info/licenses/LICENSE,sha256=ckVzaDtN4nB27FCxSN3sPJegXdA93rJtzGfrofWPEGo,1080
5
+ spyder_code_agent-0.1.0.dist-info/METADATA,sha256=eMN8I2P6Qjn2S9vmkfzKoDP8mIe-2kSuk28mfaUu-5M,6112
6
+ spyder_code_agent-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ spyder_code_agent-0.1.0.dist-info/entry_points.txt,sha256=YD26wFFaoRb8E1MNi8Hg_-CyOtnHbY6hrnLnKf_PzpQ,65
8
+ spyder_code_agent-0.1.0.dist-info/top_level.txt,sha256=37A08pqb-urTYI81Bu6e9uebTdEdbwbHv96cCQhxWRw,18
9
+ spyder_code_agent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [spyder.plugins]
2
+ code_agent = spyder_code_agent.plugin:CodeAgent
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MohammadAmir Dehghanian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ spyder_code_agent