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.
- spyder_code_agent/__init__.py +0 -0
- spyder_code_agent/container.py +534 -0
- spyder_code_agent/plugin.py +50 -0
- spyder_code_agent-0.1.0.dist-info/METADATA +174 -0
- spyder_code_agent-0.1.0.dist-info/RECORD +9 -0
- spyder_code_agent-0.1.0.dist-info/WHEEL +5 -0
- spyder_code_agent-0.1.0.dist-info/entry_points.txt +2 -0
- spyder_code_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- spyder_code_agent-0.1.0.dist-info/top_level.txt +1 -0
|
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('&', '&').replace('<', '<').replace('>', '>').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
|
+

|
|
20
|
+

|
|
21
|
+

|
|
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
|
+

|
|
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,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
|