agentic-python-coder 3.4.3__py3-none-any.whl → 3.5.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.
- agentic_python_coder/kernel.py +209 -45
- agentic_python_coder/mcp_server.py +167 -81
- agentic_python_coder/runner.py +52 -151
- {agentic_python_coder-3.4.3.dist-info → agentic_python_coder-3.5.0.dist-info}/METADATA +4 -2
- {agentic_python_coder-3.4.3.dist-info → agentic_python_coder-3.5.0.dist-info}/RECORD +8 -8
- {agentic_python_coder-3.4.3.dist-info → agentic_python_coder-3.5.0.dist-info}/WHEEL +1 -1
- {agentic_python_coder-3.4.3.dist-info → agentic_python_coder-3.5.0.dist-info}/entry_points.txt +0 -0
- {agentic_python_coder-3.4.3.dist-info → agentic_python_coder-3.5.0.dist-info}/licenses/LICENSE +0 -0
agentic_python_coder/kernel.py
CHANGED
|
@@ -21,6 +21,7 @@ from queue import Empty
|
|
|
21
21
|
from typing import Any, Dict, List, Optional
|
|
22
22
|
|
|
23
23
|
from jupyter_client import KernelManager
|
|
24
|
+
from jupyter_client.kernelspec import KernelSpecManager
|
|
24
25
|
|
|
25
26
|
log_level = os.environ.get("CODER_LOG_LEVEL", "WARNING").upper()
|
|
26
27
|
logging.basicConfig(
|
|
@@ -45,14 +46,50 @@ class KernelState:
|
|
|
45
46
|
last_used: float = field(default_factory=time.time)
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
def _dead_output() -> Dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"stdout": "",
|
|
52
|
+
"stderr": "",
|
|
53
|
+
"result": None,
|
|
54
|
+
"error": "Kernel died during execution",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
48
58
|
# Kernel registry and global lock
|
|
49
59
|
_kernels: Dict[str, Optional[KernelState]] = {}
|
|
50
60
|
_kernels_lock = threading.Lock()
|
|
51
61
|
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class _CrashRecord:
|
|
65
|
+
"""Restart configuration of a kernel whose process died."""
|
|
66
|
+
|
|
67
|
+
packages: List[str]
|
|
68
|
+
cwd: Optional[str]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Kernels whose process died, by ID, so that restart_kernel() can bring them
|
|
72
|
+
# back with the same packages and cwd. Guarded by _kernels_lock.
|
|
73
|
+
_crashed: Dict[str, _CrashRecord] = {}
|
|
74
|
+
|
|
75
|
+
# ANSI escape sequences (IPython colours its tracebacks)
|
|
76
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
77
|
+
|
|
52
78
|
# Suppress Jupyter warnings
|
|
53
79
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
54
80
|
|
|
55
81
|
|
|
82
|
+
def _native_kernel_spec_manager() -> KernelSpecManager:
|
|
83
|
+
"""Kernelspec manager that only knows ipykernel's built-in "python3" spec.
|
|
84
|
+
|
|
85
|
+
With the default search path, a user or system kernelspec named
|
|
86
|
+
"python3" (Anaconda, Jupyter installs) would replace the interpreter,
|
|
87
|
+
environment and launch arguments. The built-in spec launches
|
|
88
|
+
``sys.executable -m ipykernel_launcher -f {connection_file}``.
|
|
89
|
+
"""
|
|
90
|
+
return KernelSpecManager(kernel_dirs=[])
|
|
91
|
+
|
|
92
|
+
|
|
56
93
|
class UVKernelManager(KernelManager):
|
|
57
94
|
"""KernelManager that can wrap kernel launch with UV for dynamic packages."""
|
|
58
95
|
|
|
@@ -157,7 +194,11 @@ class PythonKernel:
|
|
|
157
194
|
self.with_packages = with_packages or []
|
|
158
195
|
|
|
159
196
|
# Set kernel's working directory
|
|
160
|
-
|
|
197
|
+
# Send the kernel's fd 1 to our stderr: inherited stdout is the
|
|
198
|
+
# JSON-RPC pipe under ipython_mcp, and native code (e.g. solver
|
|
199
|
+
# progress output) writing to fd 1 would corrupt the protocol.
|
|
200
|
+
# print() is unaffected (ipykernel routes it over iopub).
|
|
201
|
+
kernel_kwargs = {"stdout": 2}
|
|
161
202
|
if cwd:
|
|
162
203
|
kernel_kwargs["cwd"] = cwd
|
|
163
204
|
|
|
@@ -166,11 +207,17 @@ class PythonKernel:
|
|
|
166
207
|
if with_packages is not None:
|
|
167
208
|
logger.info(f"Starting UV kernel with packages: {with_packages}")
|
|
168
209
|
self.km = UVKernelManager(
|
|
169
|
-
with_packages=with_packages,
|
|
210
|
+
with_packages=with_packages,
|
|
211
|
+
cwd=cwd,
|
|
212
|
+
kernel_name="python3",
|
|
213
|
+
kernel_spec_manager=_native_kernel_spec_manager(),
|
|
170
214
|
)
|
|
171
215
|
else:
|
|
172
216
|
logger.info("Starting standard kernel (no dynamic packages)")
|
|
173
|
-
self.km = KernelManager(
|
|
217
|
+
self.km = KernelManager(
|
|
218
|
+
kernel_name="python3",
|
|
219
|
+
kernel_spec_manager=_native_kernel_spec_manager(),
|
|
220
|
+
)
|
|
174
221
|
|
|
175
222
|
# Start the kernel
|
|
176
223
|
logger.debug(f"Starting kernel in directory: {cwd}")
|
|
@@ -196,30 +243,35 @@ class PythonKernel:
|
|
|
196
243
|
|
|
197
244
|
def _inject_startup_code(self):
|
|
198
245
|
"""Inject safety and configuration code: block input(), set matplotlib, etc."""
|
|
246
|
+
# Runs inside a function so its imports do not leak into the user
|
|
247
|
+
# namespace (code relying on them would fail when run standalone)
|
|
199
248
|
startup_code = """
|
|
200
|
-
|
|
201
|
-
import warnings
|
|
202
|
-
import builtins
|
|
203
|
-
from IPython.core.interactiveshell import InteractiveShell
|
|
204
|
-
|
|
205
|
-
# Configure IPython to display last expression value
|
|
206
|
-
InteractiveShell.ast_node_interactivity = 'last_expr'
|
|
207
|
-
|
|
208
|
-
# Suppress pkg_resources deprecation warning from CPMpy
|
|
209
|
-
warnings.filterwarnings('ignore', message='pkg_resources is deprecated', category=UserWarning)
|
|
210
|
-
warnings.filterwarnings('ignore', message='.*pkg_resources.*', category=DeprecationWarning)
|
|
211
|
-
|
|
212
|
-
# Block interactive input() - not supported in MCP/automated sessions
|
|
213
|
-
def _blocked_input(*args, **kwargs):
|
|
214
|
-
|
|
215
|
-
builtins.input = _blocked_input
|
|
216
|
-
|
|
217
|
-
# Set matplotlib to non-interactive backend if available
|
|
218
|
-
try:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
except ImportError:
|
|
222
|
-
|
|
249
|
+
def __coder_startup():
|
|
250
|
+
import warnings
|
|
251
|
+
import builtins
|
|
252
|
+
from IPython.core.interactiveshell import InteractiveShell
|
|
253
|
+
|
|
254
|
+
# Configure IPython to display last expression value
|
|
255
|
+
InteractiveShell.ast_node_interactivity = 'last_expr'
|
|
256
|
+
|
|
257
|
+
# Suppress pkg_resources deprecation warning from CPMpy
|
|
258
|
+
warnings.filterwarnings('ignore', message='pkg_resources is deprecated', category=UserWarning)
|
|
259
|
+
warnings.filterwarnings('ignore', message='.*pkg_resources.*', category=DeprecationWarning)
|
|
260
|
+
|
|
261
|
+
# Block interactive input() - not supported in MCP/automated sessions
|
|
262
|
+
def _blocked_input(*args, **kwargs):
|
|
263
|
+
raise RuntimeError("Interactive input() not supported")
|
|
264
|
+
builtins.input = _blocked_input
|
|
265
|
+
|
|
266
|
+
# Set matplotlib to non-interactive backend if available
|
|
267
|
+
try:
|
|
268
|
+
import matplotlib
|
|
269
|
+
matplotlib.use('Agg')
|
|
270
|
+
except ImportError:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
__coder_startup()
|
|
274
|
+
del __coder_startup
|
|
223
275
|
"""
|
|
224
276
|
self._execute_raw(startup_code)
|
|
225
277
|
|
|
@@ -234,20 +286,53 @@ except ImportError:
|
|
|
234
286
|
except Exception:
|
|
235
287
|
pass # Best effort cleanup
|
|
236
288
|
|
|
237
|
-
def
|
|
289
|
+
def _alive(self) -> bool:
|
|
290
|
+
"""True if the kernel process runs (False once shut down/retired)."""
|
|
291
|
+
km = self.km if hasattr(self, "km") else None
|
|
292
|
+
return km is not None and km.is_alive()
|
|
293
|
+
|
|
294
|
+
def _execute_raw(
|
|
295
|
+
self,
|
|
296
|
+
code: str,
|
|
297
|
+
deadline_timeout: int = 60,
|
|
298
|
+
store_history: bool = True,
|
|
299
|
+
interrupt_on_timeout: bool = True,
|
|
300
|
+
) -> Dict[str, str]:
|
|
238
301
|
"""Execute code without locking (internal use only).
|
|
239
302
|
|
|
240
303
|
Args:
|
|
241
304
|
code: Python code to execute
|
|
242
305
|
deadline_timeout: Hard deadline in seconds for entire execution
|
|
306
|
+
store_history: Record the cell in IPython history
|
|
307
|
+
interrupt_on_timeout: Interrupt the kernel when the deadline hits
|
|
243
308
|
|
|
244
309
|
Returns:
|
|
245
310
|
Dict with stdout, stderr, result, and error fields
|
|
246
311
|
"""
|
|
312
|
+
return self._execute_collect(
|
|
313
|
+
code, deadline_timeout, store_history, interrupt_on_timeout
|
|
314
|
+
)[0]
|
|
315
|
+
|
|
316
|
+
def _execute_collect(
|
|
317
|
+
self,
|
|
318
|
+
code: str,
|
|
319
|
+
deadline_timeout: int = 60,
|
|
320
|
+
store_history: bool = True,
|
|
321
|
+
interrupt_on_timeout: bool = True,
|
|
322
|
+
) -> tuple:
|
|
323
|
+
"""Like _execute_raw, but returns (output, completed).
|
|
324
|
+
|
|
325
|
+
completed is False when the kernel died or the deadline hit before
|
|
326
|
+
the kernel reported idle.
|
|
327
|
+
"""
|
|
328
|
+
if self.kc is None or not self._alive():
|
|
329
|
+
# Shut down or retired by another caller while we waited
|
|
330
|
+
return _dead_output(), False
|
|
331
|
+
|
|
247
332
|
# Send the execution request (silent=False to get execute_result)
|
|
248
333
|
# allow_stdin=False prevents kernel from requesting input
|
|
249
334
|
msg_id = self.kc.execute(
|
|
250
|
-
code, silent=False, store_history=
|
|
335
|
+
code, silent=False, store_history=store_history, allow_stdin=False
|
|
251
336
|
)
|
|
252
337
|
|
|
253
338
|
# Collect output
|
|
@@ -275,17 +360,23 @@ except ImportError:
|
|
|
275
360
|
elif msg_type == "execute_result":
|
|
276
361
|
output["result"] = content["data"].get("text/plain", "")
|
|
277
362
|
elif msg_type == "error":
|
|
278
|
-
|
|
363
|
+
error = f"{content['ename']}: {content['evalue']}"
|
|
279
364
|
# Include traceback for debugging
|
|
280
365
|
if content.get("traceback"):
|
|
281
|
-
|
|
366
|
+
error += "\n" + "\n".join(content["traceback"])
|
|
367
|
+
output["error"] = _ANSI_RE.sub("", error)
|
|
282
368
|
elif msg_type == "status" and content["execution_state"] == "idle":
|
|
283
369
|
completed = True
|
|
284
370
|
break # Only exit on idle status
|
|
285
371
|
except Empty:
|
|
372
|
+
# No idle status will ever come from a dead kernel: report
|
|
373
|
+
# the crash now instead of waiting for the deadline
|
|
374
|
+
if not self._alive():
|
|
375
|
+
output["error"] = "Kernel died during execution"
|
|
376
|
+
return output, False
|
|
286
377
|
continue # Keep waiting, don't break
|
|
287
378
|
|
|
288
|
-
if not completed:
|
|
379
|
+
if not completed and interrupt_on_timeout:
|
|
289
380
|
# Deadline hit while the kernel was still running: report a
|
|
290
381
|
# timeout instead of silently returning partial output as success,
|
|
291
382
|
# and interrupt so the kernel doesn't block the next execution
|
|
@@ -300,7 +391,7 @@ except ImportError:
|
|
|
300
391
|
except Exception:
|
|
301
392
|
pass # Best effort
|
|
302
393
|
|
|
303
|
-
return output
|
|
394
|
+
return output, completed
|
|
304
395
|
|
|
305
396
|
def execute(self, code: str, deadline_timeout: int = 60) -> Dict[str, str]:
|
|
306
397
|
"""Execute code with thread-safe locking.
|
|
@@ -342,6 +433,10 @@ except ImportError:
|
|
|
342
433
|
try:
|
|
343
434
|
if self.km.is_alive():
|
|
344
435
|
self.km.shutdown_kernel(now=True)
|
|
436
|
+
else:
|
|
437
|
+
# Dead kernel: still release the control socket,
|
|
438
|
+
# zmq context and connection file
|
|
439
|
+
self.km.cleanup_resources()
|
|
345
440
|
except Exception:
|
|
346
441
|
pass
|
|
347
442
|
self.km = None # Clear reference
|
|
@@ -364,6 +459,25 @@ except ImportError:
|
|
|
364
459
|
# =============================================================================
|
|
365
460
|
|
|
366
461
|
|
|
462
|
+
def _retire_dead_kernel(kernel_id: str, state: KernelState) -> None:
|
|
463
|
+
"""Drop a dead kernel from the registry, keeping its config for restart.
|
|
464
|
+
|
|
465
|
+
Releases the dead kernel's client channels and manager resources.
|
|
466
|
+
Callers must not hold the kernel's exec lock.
|
|
467
|
+
"""
|
|
468
|
+
with _kernels_lock:
|
|
469
|
+
# Record only if this call removed this exact state: a delayed
|
|
470
|
+
# caller must not resurrect an ID that was reset or shut down since
|
|
471
|
+
if _kernels.get(kernel_id) is state:
|
|
472
|
+
_kernels.pop(kernel_id)
|
|
473
|
+
_crashed[kernel_id] = _CrashRecord(list(state.packages), state.cwd)
|
|
474
|
+
try:
|
|
475
|
+
with state.kernel._exec_lock:
|
|
476
|
+
state.kernel.shutdown()
|
|
477
|
+
except Exception:
|
|
478
|
+
pass # Best effort
|
|
479
|
+
|
|
480
|
+
|
|
367
481
|
def create_kernel(
|
|
368
482
|
kernel_id: Optional[str] = None,
|
|
369
483
|
with_packages: Optional[List[str]] = None,
|
|
@@ -414,6 +528,7 @@ def create_kernel(
|
|
|
414
528
|
)
|
|
415
529
|
with _kernels_lock:
|
|
416
530
|
_kernels[kernel_id] = state
|
|
531
|
+
_crashed.pop(kernel_id, None)
|
|
417
532
|
logger.info(f"Created kernel {kernel_id}")
|
|
418
533
|
return kernel_id
|
|
419
534
|
except Exception:
|
|
@@ -449,9 +564,8 @@ def execute_in_kernel(
|
|
|
449
564
|
raise KeyError(f"Kernel {kernel_id} is being created")
|
|
450
565
|
|
|
451
566
|
# Check if alive before execution
|
|
452
|
-
if not state.kernel.
|
|
453
|
-
|
|
454
|
-
_kernels.pop(kernel_id, None)
|
|
567
|
+
if not state.kernel._alive():
|
|
568
|
+
_retire_dead_kernel(kernel_id, state)
|
|
455
569
|
raise RuntimeError(f"Kernel {kernel_id} is dead")
|
|
456
570
|
|
|
457
571
|
# Execute (uses per-kernel lock internally)
|
|
@@ -460,17 +574,54 @@ def execute_in_kernel(
|
|
|
460
574
|
# Update last_used
|
|
461
575
|
state.last_used = time.time()
|
|
462
576
|
|
|
463
|
-
# Check if crashed during execution
|
|
464
|
-
if not state.kernel.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
577
|
+
# Check if crashed during execution (or retired by another caller)
|
|
578
|
+
if not state.kernel._alive():
|
|
579
|
+
_retire_dead_kernel(kernel_id, state)
|
|
580
|
+
result["error"] = (
|
|
581
|
+
f"Kernel {kernel_id} crashed during execution."
|
|
582
|
+
" Call python_reset to restart it."
|
|
583
|
+
)
|
|
468
584
|
|
|
469
585
|
# Always include kernel_id in result
|
|
470
586
|
result["kernel_id"] = kernel_id
|
|
471
587
|
return result
|
|
472
588
|
|
|
473
589
|
|
|
590
|
+
def probe_kernel(
|
|
591
|
+
kernel_id: str, codes: List[str], timeout: int
|
|
592
|
+
) -> Optional[List[Dict[str, Any]]]:
|
|
593
|
+
"""Run introspection snippets in an idle kernel (for python_status).
|
|
594
|
+
|
|
595
|
+
Never waits behind or interrupts running user code: returns None when
|
|
596
|
+
the kernel is unknown, dead or busy; a dead kernel is retired. Snippets
|
|
597
|
+
are not stored in the IPython history. If a snippet does not complete,
|
|
598
|
+
the remaining ones are not sent and the results so far are returned.
|
|
599
|
+
"""
|
|
600
|
+
with _kernels_lock:
|
|
601
|
+
state = _kernels.get(kernel_id)
|
|
602
|
+
if state is None:
|
|
603
|
+
return None
|
|
604
|
+
kernel = state.kernel
|
|
605
|
+
if not kernel._exec_lock.acquire(blocking=False):
|
|
606
|
+
return None # busy
|
|
607
|
+
results: List[Dict[str, Any]] = []
|
|
608
|
+
try:
|
|
609
|
+
if kernel._alive():
|
|
610
|
+
for code in codes:
|
|
611
|
+
output, completed = kernel._execute_collect(
|
|
612
|
+
code, timeout, store_history=False, interrupt_on_timeout=False
|
|
613
|
+
)
|
|
614
|
+
if not completed:
|
|
615
|
+
break
|
|
616
|
+
results.append(output)
|
|
617
|
+
finally:
|
|
618
|
+
kernel._exec_lock.release()
|
|
619
|
+
if not kernel._alive():
|
|
620
|
+
_retire_dead_kernel(kernel_id, state)
|
|
621
|
+
return None
|
|
622
|
+
return results
|
|
623
|
+
|
|
624
|
+
|
|
474
625
|
def shutdown_kernel_by_id(kernel_id: str) -> bool:
|
|
475
626
|
"""Shutdown specific kernel.
|
|
476
627
|
|
|
@@ -479,6 +630,7 @@ def shutdown_kernel_by_id(kernel_id: str) -> bool:
|
|
|
479
630
|
"""
|
|
480
631
|
with _kernels_lock:
|
|
481
632
|
state = _kernels.pop(kernel_id, None)
|
|
633
|
+
_crashed.pop(kernel_id, None) # an explicit shutdown forgets a crash
|
|
482
634
|
|
|
483
635
|
if state is None:
|
|
484
636
|
return False
|
|
@@ -504,15 +656,15 @@ def interrupt_kernel_by_id(kernel_id: str) -> bool:
|
|
|
504
656
|
return False
|
|
505
657
|
state = _kernels[kernel_id]
|
|
506
658
|
|
|
507
|
-
if state is None
|
|
659
|
+
km = state.kernel.km if state is not None else None
|
|
660
|
+
if km is None or not km.is_alive():
|
|
508
661
|
return False
|
|
509
662
|
|
|
510
|
-
|
|
663
|
+
km.interrupt_kernel()
|
|
511
664
|
time.sleep(0.5) # Grace period
|
|
512
665
|
|
|
513
|
-
if not state.kernel.
|
|
514
|
-
|
|
515
|
-
_kernels.pop(kernel_id, None)
|
|
666
|
+
if not state.kernel._alive():
|
|
667
|
+
_retire_dead_kernel(kernel_id, state)
|
|
516
668
|
raise RuntimeError(f"Kernel {kernel_id} died from interrupt")
|
|
517
669
|
|
|
518
670
|
return True
|
|
@@ -528,10 +680,21 @@ def restart_kernel(
|
|
|
528
680
|
kernel_id: Kernel to restart.
|
|
529
681
|
with_packages: New packages (None = keep existing).
|
|
530
682
|
|
|
683
|
+
A kernel that died (and was dropped from the registry) is restarted
|
|
684
|
+
with the packages and cwd it had.
|
|
685
|
+
|
|
531
686
|
Raises:
|
|
532
687
|
KeyError: If kernel not found.
|
|
533
688
|
RuntimeError: If new kernel fails to start.
|
|
534
689
|
"""
|
|
690
|
+
with _kernels_lock:
|
|
691
|
+
crashed = None if kernel_id in _kernels else _crashed.get(kernel_id)
|
|
692
|
+
if crashed is not None:
|
|
693
|
+
packages = with_packages if with_packages is not None else crashed.packages
|
|
694
|
+
create_kernel(kernel_id, packages, crashed.cwd)
|
|
695
|
+
logger.info(f"Restarted crashed kernel {kernel_id}")
|
|
696
|
+
return
|
|
697
|
+
|
|
535
698
|
with _kernels_lock:
|
|
536
699
|
if kernel_id not in _kernels:
|
|
537
700
|
raise KeyError(f"Unknown kernel: {kernel_id}")
|
|
@@ -623,6 +786,7 @@ def shutdown_all_kernels():
|
|
|
623
786
|
"""Shutdown all kernels. Called by atexit."""
|
|
624
787
|
with _kernels_lock:
|
|
625
788
|
kernel_ids = list(_kernels.keys())
|
|
789
|
+
_crashed.clear()
|
|
626
790
|
|
|
627
791
|
for kid in kernel_ids:
|
|
628
792
|
try:
|
|
@@ -10,19 +10,24 @@ import logging
|
|
|
10
10
|
import os
|
|
11
11
|
import signal
|
|
12
12
|
import threading
|
|
13
|
+
from typing import List, Optional
|
|
13
14
|
|
|
15
|
+
import jsonschema
|
|
14
16
|
from mcp.server import Server
|
|
15
17
|
from mcp.server.stdio import stdio_server
|
|
16
|
-
from mcp.types import TextContent, Tool
|
|
18
|
+
from mcp.types import CallToolResult, ListToolsResult, TextContent, Tool
|
|
19
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
17
20
|
|
|
18
21
|
from agentic_python_coder.kernel import (
|
|
19
22
|
DEFAULT_KERNEL_ID,
|
|
20
23
|
create_kernel,
|
|
21
24
|
execute_in_kernel,
|
|
25
|
+
get_kernel_info,
|
|
22
26
|
interrupt_kernel_by_id,
|
|
23
27
|
kernel_exists,
|
|
24
28
|
kill_all_kernel_process_groups,
|
|
25
29
|
list_kernels,
|
|
30
|
+
probe_kernel,
|
|
26
31
|
restart_kernel,
|
|
27
32
|
shutdown_all_kernels,
|
|
28
33
|
)
|
|
@@ -30,8 +35,7 @@ from agentic_python_coder.kernel import (
|
|
|
30
35
|
# Configure logging
|
|
31
36
|
logger = logging.getLogger(__name__)
|
|
32
37
|
|
|
33
|
-
#
|
|
34
|
-
server = Server("ipython_mcp")
|
|
38
|
+
# The MCP server object is created at the end of the module (see _make_server).
|
|
35
39
|
|
|
36
40
|
# Constants
|
|
37
41
|
MAX_OUTPUT = 100 * 1024 # 100KB truncation limit
|
|
@@ -57,6 +61,39 @@ def truncate_output(result: dict) -> dict:
|
|
|
57
61
|
return result
|
|
58
62
|
|
|
59
63
|
|
|
64
|
+
def _status_probe(expr: str) -> str:
|
|
65
|
+
"""Code that writes repr(expr) to stdout.
|
|
66
|
+
|
|
67
|
+
Writing (instead of a last-expression value) keeps the probe out of the
|
|
68
|
+
user's ``_``/``Out``, ``__import__`` keeps it from binding names, and
|
|
69
|
+
sys.stdout.write survives a user-shadowed print.
|
|
70
|
+
"""
|
|
71
|
+
return f"if True:\n __import__('sys').stdout.write(repr({expr}) + '\\n')\n"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _distribution_name(requirement: str) -> Optional[str]:
|
|
75
|
+
"""Distribution name of a requirement string ("pkg>=1", "pkg[extra]").
|
|
76
|
+
|
|
77
|
+
None when the string is not a PEP 508 requirement, e.g. a local path or
|
|
78
|
+
a VCS URL, whose distribution name is unknown before installation.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
return Requirement(requirement).name
|
|
82
|
+
except InvalidRequirement:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _package_versions_probe(packages: List[str]) -> str:
|
|
87
|
+
"""Probe code writing {requirement: version or "?"} to stdout."""
|
|
88
|
+
pairs = [(p, _distribution_name(p)) for p in packages]
|
|
89
|
+
return _status_probe(
|
|
90
|
+
"{req: '?' if name is None else getattr(next(iter("
|
|
91
|
+
"__import__('importlib.metadata').metadata.distributions(name=name)),"
|
|
92
|
+
" None), 'version', '?')"
|
|
93
|
+
f" for req, name in {pairs!r}}}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
60
97
|
def get_kernel_id(arguments: dict) -> str:
|
|
61
98
|
"""Get kernel_id from arguments, defaulting to DEFAULT_KERNEL_ID."""
|
|
62
99
|
return arguments.get("kernel_id") or DEFAULT_KERNEL_ID
|
|
@@ -147,7 +184,6 @@ async def execute_with_timeout(
|
|
|
147
184
|
return {"success": False, "kernel_id": kernel_id, "error": error_msg}
|
|
148
185
|
|
|
149
186
|
|
|
150
|
-
@server.list_tools()
|
|
151
187
|
async def list_tools():
|
|
152
188
|
"""List available MCP tools."""
|
|
153
189
|
return [
|
|
@@ -296,7 +332,6 @@ Note: Call this, then call python_exec to continue working.""",
|
|
|
296
332
|
]
|
|
297
333
|
|
|
298
334
|
|
|
299
|
-
@server.call_tool()
|
|
300
335
|
async def call_tool(name: str, arguments: dict):
|
|
301
336
|
"""Handle tool calls."""
|
|
302
337
|
loop = asyncio.get_running_loop()
|
|
@@ -312,60 +347,57 @@ async def call_tool(name: str, arguments: dict):
|
|
|
312
347
|
None,
|
|
313
348
|
lambda: create_kernel(None, packages if packages else None),
|
|
314
349
|
)
|
|
315
|
-
elif kernel_exists(kernel_id):
|
|
316
|
-
# kernel_id provided and exists: RESET it
|
|
317
|
-
await loop.run_in_executor(
|
|
318
|
-
None,
|
|
319
|
-
lambda: restart_kernel(kernel_id, packages if packages else None),
|
|
320
|
-
)
|
|
321
350
|
else:
|
|
322
|
-
# kernel_id provided
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"error": f"Kernel {kernel_id} not found. Omit kernel_id to create a new kernel.",
|
|
330
|
-
}
|
|
351
|
+
# kernel_id provided: RESET it (also brings back a kernel
|
|
352
|
+
# that crashed, with its packages and cwd)
|
|
353
|
+
try:
|
|
354
|
+
await loop.run_in_executor(
|
|
355
|
+
None,
|
|
356
|
+
lambda: restart_kernel(
|
|
357
|
+
kernel_id, packages if packages else None
|
|
331
358
|
),
|
|
332
359
|
)
|
|
333
|
-
|
|
360
|
+
except KeyError:
|
|
361
|
+
# kernel_id provided but doesn't exist: error
|
|
362
|
+
return [
|
|
363
|
+
TextContent(
|
|
364
|
+
type="text",
|
|
365
|
+
text=json.dumps(
|
|
366
|
+
{
|
|
367
|
+
"success": False,
|
|
368
|
+
"error": f"Kernel {kernel_id} not found. Omit kernel_id to create a new kernel.",
|
|
369
|
+
}
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
]
|
|
373
|
+
# Without packages the kernel keeps the ones it had: report
|
|
374
|
+
# what it actually runs with
|
|
375
|
+
if not packages:
|
|
376
|
+
try:
|
|
377
|
+
packages = list(get_kernel_info(kernel_id)["packages"] or [])
|
|
378
|
+
except KeyError:
|
|
379
|
+
pass
|
|
334
380
|
|
|
335
381
|
if packages:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
import importlib.metadata
|
|
340
|
-
versions = {}
|
|
341
|
-
for pkg in %r:
|
|
342
|
-
try:
|
|
343
|
-
versions[pkg] = importlib.metadata.version(pkg)
|
|
344
|
-
except:
|
|
345
|
-
versions[pkg] = "?"
|
|
346
|
-
versions
|
|
347
|
-
"""
|
|
348
|
-
% packages
|
|
349
|
-
)
|
|
382
|
+
msg = f"Session started with packages: {', '.join(packages)}"
|
|
383
|
+
# Same probe discipline as python_status: no names bound, no
|
|
384
|
+
# history entry, _ untouched
|
|
350
385
|
try:
|
|
351
|
-
|
|
352
|
-
|
|
386
|
+
probe = _package_versions_probe(packages)
|
|
387
|
+
results = await loop.run_in_executor(
|
|
388
|
+
None,
|
|
389
|
+
lambda: probe_kernel(kernel_id, [probe], DEFAULT_TIMEOUT),
|
|
353
390
|
)
|
|
354
|
-
if
|
|
355
|
-
|
|
391
|
+
result = results[0] if results else None
|
|
392
|
+
if result and result.get("error") is None and result.get("stdout"):
|
|
393
|
+
versions = ast.literal_eval(result["stdout"].strip())
|
|
356
394
|
if isinstance(versions, dict):
|
|
357
395
|
pkg_list = ", ".join(
|
|
358
396
|
f"{p} {v}" for p, v in versions.items()
|
|
359
397
|
)
|
|
360
398
|
msg = f"Session started. Packages: {pkg_list}"
|
|
361
|
-
else:
|
|
362
|
-
msg = (
|
|
363
|
-
f"Session started with packages: {', '.join(packages)}"
|
|
364
|
-
)
|
|
365
|
-
else:
|
|
366
|
-
msg = f"Session started with packages: {', '.join(packages)}"
|
|
367
399
|
except Exception:
|
|
368
|
-
|
|
400
|
+
pass
|
|
369
401
|
else:
|
|
370
402
|
msg = "Session started (no packages)"
|
|
371
403
|
|
|
@@ -403,6 +435,8 @@ versions
|
|
|
403
435
|
)
|
|
404
436
|
except (TypeError, ValueError):
|
|
405
437
|
timeout = DEFAULT_TIMEOUT
|
|
438
|
+
if not timeout > 0: # also catches NaN
|
|
439
|
+
timeout = DEFAULT_TIMEOUT
|
|
406
440
|
kernel_id = get_kernel_id(arguments)
|
|
407
441
|
|
|
408
442
|
if not code.strip():
|
|
@@ -459,44 +493,43 @@ versions
|
|
|
459
493
|
}
|
|
460
494
|
|
|
461
495
|
if is_active:
|
|
462
|
-
#
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
result = await execute_with_timeout(
|
|
480
|
-
pkg_code, STATUS_PROBE_TIMEOUT, kernel_id
|
|
481
|
-
)
|
|
482
|
-
if result.get("result"):
|
|
483
|
-
status["packages"] = ast.literal_eval(result["result"])
|
|
484
|
-
except Exception:
|
|
485
|
-
pass
|
|
486
|
-
|
|
487
|
-
# Get user-defined variables
|
|
496
|
+
# Probes run only when the kernel is idle (a busy kernel keeps
|
|
497
|
+
# the defaults above); they never interrupt user code
|
|
498
|
+
probes = {
|
|
499
|
+
"python_version": _status_probe("__import__('sys').version"),
|
|
500
|
+
"packages": _status_probe(
|
|
501
|
+
"[f\"{d.metadata['Name']} {d.version}\" for d in"
|
|
502
|
+
" __import__('importlib.metadata').metadata.distributions()"
|
|
503
|
+
" if d.metadata['Name'] not in"
|
|
504
|
+
" ('pip', 'setuptools', 'wheel', 'ipykernel', 'jupyter-client')"
|
|
505
|
+
"][:20]"
|
|
506
|
+
),
|
|
507
|
+
"variables": _status_probe(
|
|
508
|
+
"[name for name in dir() if not name.startswith('_')"
|
|
509
|
+
" and name not in"
|
|
510
|
+
" ('In', 'Out', 'get_ipython', 'exit', 'quit', 'open')]"
|
|
511
|
+
),
|
|
512
|
+
}
|
|
488
513
|
try:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
var_code, STATUS_PROBE_TIMEOUT, kernel_id
|
|
514
|
+
results = await loop.run_in_executor(
|
|
515
|
+
None,
|
|
516
|
+
lambda: probe_kernel(
|
|
517
|
+
kernel_id, list(probes.values()), STATUS_PROBE_TIMEOUT
|
|
518
|
+
),
|
|
495
519
|
)
|
|
496
|
-
if result.get("result"):
|
|
497
|
-
status["variables"] = ast.literal_eval(result["result"])
|
|
498
520
|
except Exception:
|
|
499
|
-
|
|
521
|
+
results = None
|
|
522
|
+
for key, result in zip(probes, results or []):
|
|
523
|
+
if result.get("error") is None and result.get("stdout"):
|
|
524
|
+
try:
|
|
525
|
+
status[key] = ast.literal_eval(result["stdout"].strip())
|
|
526
|
+
except (ValueError, SyntaxError):
|
|
527
|
+
pass
|
|
528
|
+
# The probe retires a kernel found dead: report it like any
|
|
529
|
+
# kernel that is not in the registry
|
|
530
|
+
if not kernel_exists(kernel_id):
|
|
531
|
+
status["active"] = False
|
|
532
|
+
status["all_kernels"] = [k["id"] for k in list_kernels()]
|
|
500
533
|
|
|
501
534
|
return [TextContent(type="text", text=json.dumps(status))]
|
|
502
535
|
|
|
@@ -569,6 +602,59 @@ import importlib.metadata
|
|
|
569
602
|
]
|
|
570
603
|
|
|
571
604
|
|
|
605
|
+
async def _on_list_tools(ctx, params) -> ListToolsResult:
|
|
606
|
+
"""Lowlevel handler: wrap the tool list in its result type."""
|
|
607
|
+
return ListToolsResult(tools=await list_tools())
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
async def _on_call_tool(ctx, params) -> CallToolResult:
|
|
611
|
+
"""Lowlevel handler.
|
|
612
|
+
|
|
613
|
+
The SDK does not wrap a returned content list, and an exception would
|
|
614
|
+
become a JSON-RPC error instead of an LLM-visible isError result, so both
|
|
615
|
+
are done here.
|
|
616
|
+
"""
|
|
617
|
+
arguments = params.arguments or {}
|
|
618
|
+
# The SDK does not validate arguments against inputSchema
|
|
619
|
+
tool = next((t for t in await list_tools() if t.name == params.name), None)
|
|
620
|
+
if tool is not None:
|
|
621
|
+
schema = tool.model_dump(by_alias=True)["inputSchema"]
|
|
622
|
+
# Strict function calling sends null for omitted optional arguments:
|
|
623
|
+
# treat those as absent so the defaults apply (null for a required
|
|
624
|
+
# argument still fails validation)
|
|
625
|
+
required = set(schema.get("required", []))
|
|
626
|
+
arguments = {
|
|
627
|
+
k: v for k, v in arguments.items() if v is not None or k in required
|
|
628
|
+
}
|
|
629
|
+
try:
|
|
630
|
+
jsonschema.validate(instance=arguments, schema=schema)
|
|
631
|
+
except jsonschema.ValidationError as e:
|
|
632
|
+
return CallToolResult(
|
|
633
|
+
content=[
|
|
634
|
+
TextContent(
|
|
635
|
+
type="text", text=f"Input validation error: {e.message}"
|
|
636
|
+
)
|
|
637
|
+
],
|
|
638
|
+
is_error=True,
|
|
639
|
+
)
|
|
640
|
+
try:
|
|
641
|
+
content = await call_tool(params.name, arguments)
|
|
642
|
+
except Exception as e: # error text reaches the model
|
|
643
|
+
return CallToolResult(
|
|
644
|
+
content=[TextContent(type="text", text=str(e))], is_error=True
|
|
645
|
+
)
|
|
646
|
+
return CallToolResult(content=content)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _make_server() -> Server:
|
|
650
|
+
return Server(
|
|
651
|
+
"ipython_mcp", on_list_tools=_on_list_tools, on_call_tool=_on_call_tool
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
server = _make_server()
|
|
656
|
+
|
|
657
|
+
|
|
572
658
|
async def run_server():
|
|
573
659
|
"""Run the MCP server."""
|
|
574
660
|
async with stdio_server() as (read, write):
|
agentic_python_coder/runner.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
6
6
|
|
|
7
7
|
from agentic_python_coder.agent import create_coding_agent, run_agent
|
|
8
8
|
from agentic_python_coder.llm import LLMConfig
|
|
@@ -35,6 +35,55 @@ def get_system_prompt_path(todo: bool = False) -> Path:
|
|
|
35
35
|
return system_prompt_path
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _tool_call_event(tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
|
39
|
+
"""Log event for one tool call of an assistant message."""
|
|
40
|
+
func = tool_call.get("function", {})
|
|
41
|
+
tool_name = func.get("name", "unknown")
|
|
42
|
+
try:
|
|
43
|
+
tool_args = json.loads(func.get("arguments", "{}"))
|
|
44
|
+
except (json.JSONDecodeError, TypeError):
|
|
45
|
+
tool_args = {}
|
|
46
|
+
|
|
47
|
+
if tool_name == "todo_write":
|
|
48
|
+
todos = tool_args.get("todos", [])
|
|
49
|
+
return {
|
|
50
|
+
"event": "todo",
|
|
51
|
+
"action": "update",
|
|
52
|
+
"count": len(todos),
|
|
53
|
+
"tasks": [
|
|
54
|
+
{"content": t.get("content", ""), "status": t.get("status", "")}
|
|
55
|
+
for t in todos
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
if tool_name in ("python_exec", "save_code"):
|
|
59
|
+
return {"event": tool_name, "code_length": len(tool_args.get("code", ""))}
|
|
60
|
+
return {"event": "tool_call", "tool": tool_name}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _message_events(msg: Any) -> Iterator[Dict[str, Any]]:
|
|
64
|
+
"""Log events for one run_agent message; run_agent only produces dicts."""
|
|
65
|
+
if not isinstance(msg, dict):
|
|
66
|
+
return
|
|
67
|
+
tool_calls = msg.get("tool_calls", [])
|
|
68
|
+
if tool_calls:
|
|
69
|
+
for tool_call in tool_calls:
|
|
70
|
+
yield _tool_call_event(tool_call)
|
|
71
|
+
# Tool responses (role=tool messages with JSON content carrying "success")
|
|
72
|
+
elif msg.get("role") == "tool" and isinstance(msg.get("content"), str):
|
|
73
|
+
try:
|
|
74
|
+
content_data = json.loads(msg["content"])
|
|
75
|
+
if "success" in content_data:
|
|
76
|
+
yield {
|
|
77
|
+
"event": "tool_response",
|
|
78
|
+
"success": content_data.get("success", False),
|
|
79
|
+
"error": content_data.get("error")
|
|
80
|
+
if not content_data.get("success")
|
|
81
|
+
else None,
|
|
82
|
+
}
|
|
83
|
+
except (json.JSONDecodeError, TypeError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
38
87
|
def save_conversation_log(
|
|
39
88
|
working_dir: Path,
|
|
40
89
|
messages: List[Any],
|
|
@@ -63,157 +112,9 @@ def save_conversation_log(
|
|
|
63
112
|
json.dumps({"event": "start", "task": task_basename or "inline"}) + "\n"
|
|
64
113
|
)
|
|
65
114
|
|
|
66
|
-
# Process messages
|
|
67
115
|
for msg in messages:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
tool_calls = msg.get("tool_calls", [])
|
|
71
|
-
if tool_calls:
|
|
72
|
-
for tool_call in tool_calls:
|
|
73
|
-
func = tool_call.get("function", {})
|
|
74
|
-
tool_name = func.get("name", "unknown")
|
|
75
|
-
try:
|
|
76
|
-
tool_args = json.loads(func.get("arguments", "{}"))
|
|
77
|
-
except (json.JSONDecodeError, TypeError):
|
|
78
|
-
tool_args = {}
|
|
79
|
-
|
|
80
|
-
if tool_name == "todo_write":
|
|
81
|
-
todos = tool_args.get("todos", [])
|
|
82
|
-
f.write(
|
|
83
|
-
json.dumps(
|
|
84
|
-
{
|
|
85
|
-
"event": "todo",
|
|
86
|
-
"action": "update",
|
|
87
|
-
"count": len(todos),
|
|
88
|
-
"tasks": [
|
|
89
|
-
{
|
|
90
|
-
"content": t.get("content", ""),
|
|
91
|
-
"status": t.get("status", ""),
|
|
92
|
-
}
|
|
93
|
-
for t in todos
|
|
94
|
-
],
|
|
95
|
-
}
|
|
96
|
-
)
|
|
97
|
-
+ "\n"
|
|
98
|
-
)
|
|
99
|
-
elif tool_name == "python_exec":
|
|
100
|
-
f.write(
|
|
101
|
-
json.dumps(
|
|
102
|
-
{
|
|
103
|
-
"event": "python_exec",
|
|
104
|
-
"code_length": len(tool_args.get("code", "")),
|
|
105
|
-
}
|
|
106
|
-
)
|
|
107
|
-
+ "\n"
|
|
108
|
-
)
|
|
109
|
-
elif tool_name == "save_code":
|
|
110
|
-
f.write(
|
|
111
|
-
json.dumps(
|
|
112
|
-
{
|
|
113
|
-
"event": "save_code",
|
|
114
|
-
"code_length": len(tool_args.get("code", "")),
|
|
115
|
-
}
|
|
116
|
-
)
|
|
117
|
-
+ "\n"
|
|
118
|
-
)
|
|
119
|
-
else:
|
|
120
|
-
f.write(
|
|
121
|
-
json.dumps({"event": "tool_call", "tool": tool_name})
|
|
122
|
-
+ "\n"
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
# Handle tool responses (role=tool messages with content)
|
|
126
|
-
elif msg.get("role") == "tool" and isinstance(msg.get("content"), str):
|
|
127
|
-
try:
|
|
128
|
-
content_data = json.loads(msg["content"])
|
|
129
|
-
if "success" in content_data:
|
|
130
|
-
f.write(
|
|
131
|
-
json.dumps(
|
|
132
|
-
{
|
|
133
|
-
"event": "tool_response",
|
|
134
|
-
"success": content_data.get("success", False),
|
|
135
|
-
"error": content_data.get("error")
|
|
136
|
-
if not content_data.get("success")
|
|
137
|
-
else None,
|
|
138
|
-
}
|
|
139
|
-
)
|
|
140
|
-
+ "\n"
|
|
141
|
-
)
|
|
142
|
-
except (json.JSONDecodeError, TypeError):
|
|
143
|
-
pass
|
|
144
|
-
|
|
145
|
-
# Handle object messages (backward compat)
|
|
146
|
-
elif hasattr(msg, "tool_calls") and msg.tool_calls:
|
|
147
|
-
for tool_call in msg.tool_calls:
|
|
148
|
-
if isinstance(tool_call, dict):
|
|
149
|
-
tool_name = tool_call.get("name", "unknown")
|
|
150
|
-
tool_args = tool_call.get("args", {})
|
|
151
|
-
else:
|
|
152
|
-
tool_name = getattr(tool_call, "name", "unknown")
|
|
153
|
-
tool_args = getattr(tool_call, "args", {})
|
|
154
|
-
|
|
155
|
-
if tool_name == "todo_write":
|
|
156
|
-
todos = tool_args.get("todos", [])
|
|
157
|
-
f.write(
|
|
158
|
-
json.dumps(
|
|
159
|
-
{
|
|
160
|
-
"event": "todo",
|
|
161
|
-
"action": "update",
|
|
162
|
-
"count": len(todos),
|
|
163
|
-
"tasks": [
|
|
164
|
-
{
|
|
165
|
-
"content": t.get("content", ""),
|
|
166
|
-
"status": t.get("status", ""),
|
|
167
|
-
}
|
|
168
|
-
for t in todos
|
|
169
|
-
],
|
|
170
|
-
}
|
|
171
|
-
)
|
|
172
|
-
+ "\n"
|
|
173
|
-
)
|
|
174
|
-
elif tool_name == "python_exec":
|
|
175
|
-
f.write(
|
|
176
|
-
json.dumps(
|
|
177
|
-
{
|
|
178
|
-
"event": "python_exec",
|
|
179
|
-
"code_length": len(tool_args.get("code", "")),
|
|
180
|
-
}
|
|
181
|
-
)
|
|
182
|
-
+ "\n"
|
|
183
|
-
)
|
|
184
|
-
elif tool_name == "save_code":
|
|
185
|
-
f.write(
|
|
186
|
-
json.dumps(
|
|
187
|
-
{
|
|
188
|
-
"event": "save_code",
|
|
189
|
-
"code_length": len(tool_args.get("code", "")),
|
|
190
|
-
}
|
|
191
|
-
)
|
|
192
|
-
+ "\n"
|
|
193
|
-
)
|
|
194
|
-
else:
|
|
195
|
-
f.write(
|
|
196
|
-
json.dumps({"event": "tool_call", "tool": tool_name}) + "\n"
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
elif hasattr(msg, "content") and isinstance(msg.content, str):
|
|
200
|
-
try:
|
|
201
|
-
content_data = json.loads(msg.content)
|
|
202
|
-
if "success" in content_data:
|
|
203
|
-
f.write(
|
|
204
|
-
json.dumps(
|
|
205
|
-
{
|
|
206
|
-
"event": "tool_response",
|
|
207
|
-
"success": content_data.get("success", False),
|
|
208
|
-
"error": content_data.get("error")
|
|
209
|
-
if not content_data.get("success")
|
|
210
|
-
else None,
|
|
211
|
-
}
|
|
212
|
-
)
|
|
213
|
-
+ "\n"
|
|
214
|
-
)
|
|
215
|
-
except (json.JSONDecodeError, TypeError):
|
|
216
|
-
pass
|
|
116
|
+
for event in _message_events(msg):
|
|
117
|
+
f.write(json.dumps(event) + "\n")
|
|
217
118
|
|
|
218
119
|
# Statistics
|
|
219
120
|
if stats:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentic-python-coder
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: A lightweight Python coding agent that writes, executes, and iterates on code through natural language instructions
|
|
5
5
|
Author: Stefan Szeider
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,9 +17,11 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
17
17
|
Classifier: Topic :: Software Development :: Code Generators
|
|
18
18
|
Requires-Python: >=3.13
|
|
19
19
|
Requires-Dist: ipykernel>=7.3.0
|
|
20
|
+
Requires-Dist: jsonschema>=4.20.0
|
|
20
21
|
Requires-Dist: jupyter-client>=8.9.1
|
|
21
|
-
Requires-Dist: mcp<2
|
|
22
|
+
Requires-Dist: mcp<3,>=2
|
|
22
23
|
Requires-Dist: openai>=2.45.0
|
|
24
|
+
Requires-Dist: packaging>=22
|
|
23
25
|
Requires-Dist: python-dotenv>=1.2.2
|
|
24
26
|
Requires-Dist: pyyaml>=6.0.3
|
|
25
27
|
Requires-Dist: rich>=15.0.0
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
agentic_python_coder/__init__.py,sha256=e6o-k__u5xNIiQAkZbHtbZC2-NwpeOdi4MdQtAqJBjE,2112
|
|
2
2
|
agentic_python_coder/agent.py,sha256=0JfVNO-NEss9ZNEYkYvVlJSRzIxNa6sgRqLTdsD4zbs,16484
|
|
3
3
|
agentic_python_coder/cli.py,sha256=NWc1LCEL-L2TVCT6Su9a0AQ3ub3zg5GrY6XG3j_qPgI,14877
|
|
4
|
-
agentic_python_coder/kernel.py,sha256=
|
|
4
|
+
agentic_python_coder/kernel.py,sha256=z-SgBqErdqAJdsG0SRobrFHKoN_jR3lTHufHPL6_CP8,29756
|
|
5
5
|
agentic_python_coder/llm.py,sha256=ZK3b9wLzjDr6BIxUkfKhuiSDbXNR2XaQsE1J-960IzE,6998
|
|
6
|
-
agentic_python_coder/mcp_server.py,sha256=
|
|
6
|
+
agentic_python_coder/mcp_server.py,sha256=Xb0MTdfe32dfQRYN26pdwgWiQ8BqD_VvLX6fU0UJGvk,26037
|
|
7
7
|
agentic_python_coder/project_md.py,sha256=4xkQHlWb4cDPEkvegw1YwcfaTJ3aFbrl0CYcC_xxABY,3249
|
|
8
|
-
agentic_python_coder/runner.py,sha256=
|
|
8
|
+
agentic_python_coder/runner.py,sha256=NoYRUyRusIcK-T7bEka_UoZjvcDxUzkONqe4AIXYPtU,7999
|
|
9
9
|
agentic_python_coder/tools.py,sha256=tjhQbsF3n3bFIegnEOtE8czjzbtMSXy6B9gYeb-YAJ4,12088
|
|
10
10
|
agentic_python_coder/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
agentic_python_coder/examples/clingo/README.md,sha256=SgWBqdJ2cHIfYWRV1YYqzfY0rth7mjCR5Act_hchcx0,2345
|
|
@@ -45,8 +45,8 @@ agentic_python_coder/models/sonnet5.json,sha256=kf6DIkQlcGVZ40IzRv3GXFtNncZF_gKq
|
|
|
45
45
|
agentic_python_coder/models/sonnet55.json,sha256=ibqrXCS21NfgE95XBeyeNrbs6XmV00eKA586uf7o0ic,97
|
|
46
46
|
agentic_python_coder/prompts/system.md,sha256=hREDVAhJXwCBAgJ3AE9JD6j-JRHREUPlkRSvThOvM3A,3807
|
|
47
47
|
agentic_python_coder/prompts/system_todo.md,sha256=TuNeeUNS9KqyF62Da6t2mlUCYhKsgzwGe5ic_vFPDqw,5054
|
|
48
|
-
agentic_python_coder-3.
|
|
49
|
-
agentic_python_coder-3.
|
|
50
|
-
agentic_python_coder-3.
|
|
51
|
-
agentic_python_coder-3.
|
|
52
|
-
agentic_python_coder-3.
|
|
48
|
+
agentic_python_coder-3.5.0.dist-info/METADATA,sha256=07LE-Gb747kq413uiQlC9K71lQIkAAsNsaoEcoESWpM,13313
|
|
49
|
+
agentic_python_coder-3.5.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
50
|
+
agentic_python_coder-3.5.0.dist-info/entry_points.txt,sha256=XIW3nZzL4kJ-Igohilj_mTvBcCyHBouDq2jmGzDILrk,107
|
|
51
|
+
agentic_python_coder-3.5.0.dist-info/licenses/LICENSE,sha256=R9-M_V-SPdN71IphOVzM7kCNpUYs_hwj7rtroTwDhs0,11362
|
|
52
|
+
agentic_python_coder-3.5.0.dist-info/RECORD,,
|
{agentic_python_coder-3.4.3.dist-info → agentic_python_coder-3.5.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agentic_python_coder-3.4.3.dist-info → agentic_python_coder-3.5.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|