agentic-python-coder 3.4.4__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 +204 -44
- agentic_python_coder/mcp_server.py +145 -94
- agentic_python_coder/runner.py +52 -151
- {agentic_python_coder-3.4.4.dist-info → agentic_python_coder-3.5.0.dist-info}/METADATA +4 -2
- {agentic_python_coder-3.4.4.dist-info → agentic_python_coder-3.5.0.dist-info}/RECORD +8 -8
- {agentic_python_coder-3.4.4.dist-info → agentic_python_coder-3.5.0.dist-info}/WHEEL +0 -0
- {agentic_python_coder-3.4.4.dist-info → agentic_python_coder-3.5.0.dist-info}/entry_points.txt +0 -0
- {agentic_python_coder-3.4.4.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
|
|
|
@@ -170,11 +207,17 @@ class PythonKernel:
|
|
|
170
207
|
if with_packages is not None:
|
|
171
208
|
logger.info(f"Starting UV kernel with packages: {with_packages}")
|
|
172
209
|
self.km = UVKernelManager(
|
|
173
|
-
with_packages=with_packages,
|
|
210
|
+
with_packages=with_packages,
|
|
211
|
+
cwd=cwd,
|
|
212
|
+
kernel_name="python3",
|
|
213
|
+
kernel_spec_manager=_native_kernel_spec_manager(),
|
|
174
214
|
)
|
|
175
215
|
else:
|
|
176
216
|
logger.info("Starting standard kernel (no dynamic packages)")
|
|
177
|
-
self.km = KernelManager(
|
|
217
|
+
self.km = KernelManager(
|
|
218
|
+
kernel_name="python3",
|
|
219
|
+
kernel_spec_manager=_native_kernel_spec_manager(),
|
|
220
|
+
)
|
|
178
221
|
|
|
179
222
|
# Start the kernel
|
|
180
223
|
logger.debug(f"Starting kernel in directory: {cwd}")
|
|
@@ -200,30 +243,35 @@ class PythonKernel:
|
|
|
200
243
|
|
|
201
244
|
def _inject_startup_code(self):
|
|
202
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)
|
|
203
248
|
startup_code = """
|
|
204
|
-
|
|
205
|
-
import warnings
|
|
206
|
-
import builtins
|
|
207
|
-
from IPython.core.interactiveshell import InteractiveShell
|
|
208
|
-
|
|
209
|
-
# Configure IPython to display last expression value
|
|
210
|
-
InteractiveShell.ast_node_interactivity = 'last_expr'
|
|
211
|
-
|
|
212
|
-
# Suppress pkg_resources deprecation warning from CPMpy
|
|
213
|
-
warnings.filterwarnings('ignore', message='pkg_resources is deprecated', category=UserWarning)
|
|
214
|
-
warnings.filterwarnings('ignore', message='.*pkg_resources.*', category=DeprecationWarning)
|
|
215
|
-
|
|
216
|
-
# Block interactive input() - not supported in MCP/automated sessions
|
|
217
|
-
def _blocked_input(*args, **kwargs):
|
|
218
|
-
|
|
219
|
-
builtins.input = _blocked_input
|
|
220
|
-
|
|
221
|
-
# Set matplotlib to non-interactive backend if available
|
|
222
|
-
try:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
except ImportError:
|
|
226
|
-
|
|
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
|
|
227
275
|
"""
|
|
228
276
|
self._execute_raw(startup_code)
|
|
229
277
|
|
|
@@ -238,20 +286,53 @@ except ImportError:
|
|
|
238
286
|
except Exception:
|
|
239
287
|
pass # Best effort cleanup
|
|
240
288
|
|
|
241
|
-
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]:
|
|
242
301
|
"""Execute code without locking (internal use only).
|
|
243
302
|
|
|
244
303
|
Args:
|
|
245
304
|
code: Python code to execute
|
|
246
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
|
|
247
308
|
|
|
248
309
|
Returns:
|
|
249
310
|
Dict with stdout, stderr, result, and error fields
|
|
250
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
|
+
|
|
251
332
|
# Send the execution request (silent=False to get execute_result)
|
|
252
333
|
# allow_stdin=False prevents kernel from requesting input
|
|
253
334
|
msg_id = self.kc.execute(
|
|
254
|
-
code, silent=False, store_history=
|
|
335
|
+
code, silent=False, store_history=store_history, allow_stdin=False
|
|
255
336
|
)
|
|
256
337
|
|
|
257
338
|
# Collect output
|
|
@@ -279,17 +360,23 @@ except ImportError:
|
|
|
279
360
|
elif msg_type == "execute_result":
|
|
280
361
|
output["result"] = content["data"].get("text/plain", "")
|
|
281
362
|
elif msg_type == "error":
|
|
282
|
-
|
|
363
|
+
error = f"{content['ename']}: {content['evalue']}"
|
|
283
364
|
# Include traceback for debugging
|
|
284
365
|
if content.get("traceback"):
|
|
285
|
-
|
|
366
|
+
error += "\n" + "\n".join(content["traceback"])
|
|
367
|
+
output["error"] = _ANSI_RE.sub("", error)
|
|
286
368
|
elif msg_type == "status" and content["execution_state"] == "idle":
|
|
287
369
|
completed = True
|
|
288
370
|
break # Only exit on idle status
|
|
289
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
|
|
290
377
|
continue # Keep waiting, don't break
|
|
291
378
|
|
|
292
|
-
if not completed:
|
|
379
|
+
if not completed and interrupt_on_timeout:
|
|
293
380
|
# Deadline hit while the kernel was still running: report a
|
|
294
381
|
# timeout instead of silently returning partial output as success,
|
|
295
382
|
# and interrupt so the kernel doesn't block the next execution
|
|
@@ -304,7 +391,7 @@ except ImportError:
|
|
|
304
391
|
except Exception:
|
|
305
392
|
pass # Best effort
|
|
306
393
|
|
|
307
|
-
return output
|
|
394
|
+
return output, completed
|
|
308
395
|
|
|
309
396
|
def execute(self, code: str, deadline_timeout: int = 60) -> Dict[str, str]:
|
|
310
397
|
"""Execute code with thread-safe locking.
|
|
@@ -346,6 +433,10 @@ except ImportError:
|
|
|
346
433
|
try:
|
|
347
434
|
if self.km.is_alive():
|
|
348
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()
|
|
349
440
|
except Exception:
|
|
350
441
|
pass
|
|
351
442
|
self.km = None # Clear reference
|
|
@@ -368,6 +459,25 @@ except ImportError:
|
|
|
368
459
|
# =============================================================================
|
|
369
460
|
|
|
370
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
|
+
|
|
371
481
|
def create_kernel(
|
|
372
482
|
kernel_id: Optional[str] = None,
|
|
373
483
|
with_packages: Optional[List[str]] = None,
|
|
@@ -418,6 +528,7 @@ def create_kernel(
|
|
|
418
528
|
)
|
|
419
529
|
with _kernels_lock:
|
|
420
530
|
_kernels[kernel_id] = state
|
|
531
|
+
_crashed.pop(kernel_id, None)
|
|
421
532
|
logger.info(f"Created kernel {kernel_id}")
|
|
422
533
|
return kernel_id
|
|
423
534
|
except Exception:
|
|
@@ -453,9 +564,8 @@ def execute_in_kernel(
|
|
|
453
564
|
raise KeyError(f"Kernel {kernel_id} is being created")
|
|
454
565
|
|
|
455
566
|
# Check if alive before execution
|
|
456
|
-
if not state.kernel.
|
|
457
|
-
|
|
458
|
-
_kernels.pop(kernel_id, None)
|
|
567
|
+
if not state.kernel._alive():
|
|
568
|
+
_retire_dead_kernel(kernel_id, state)
|
|
459
569
|
raise RuntimeError(f"Kernel {kernel_id} is dead")
|
|
460
570
|
|
|
461
571
|
# Execute (uses per-kernel lock internally)
|
|
@@ -464,17 +574,54 @@ def execute_in_kernel(
|
|
|
464
574
|
# Update last_used
|
|
465
575
|
state.last_used = time.time()
|
|
466
576
|
|
|
467
|
-
# Check if crashed during execution
|
|
468
|
-
if not state.kernel.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
)
|
|
472
584
|
|
|
473
585
|
# Always include kernel_id in result
|
|
474
586
|
result["kernel_id"] = kernel_id
|
|
475
587
|
return result
|
|
476
588
|
|
|
477
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
|
+
|
|
478
625
|
def shutdown_kernel_by_id(kernel_id: str) -> bool:
|
|
479
626
|
"""Shutdown specific kernel.
|
|
480
627
|
|
|
@@ -483,6 +630,7 @@ def shutdown_kernel_by_id(kernel_id: str) -> bool:
|
|
|
483
630
|
"""
|
|
484
631
|
with _kernels_lock:
|
|
485
632
|
state = _kernels.pop(kernel_id, None)
|
|
633
|
+
_crashed.pop(kernel_id, None) # an explicit shutdown forgets a crash
|
|
486
634
|
|
|
487
635
|
if state is None:
|
|
488
636
|
return False
|
|
@@ -508,15 +656,15 @@ def interrupt_kernel_by_id(kernel_id: str) -> bool:
|
|
|
508
656
|
return False
|
|
509
657
|
state = _kernels[kernel_id]
|
|
510
658
|
|
|
511
|
-
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():
|
|
512
661
|
return False
|
|
513
662
|
|
|
514
|
-
|
|
663
|
+
km.interrupt_kernel()
|
|
515
664
|
time.sleep(0.5) # Grace period
|
|
516
665
|
|
|
517
|
-
if not state.kernel.
|
|
518
|
-
|
|
519
|
-
_kernels.pop(kernel_id, None)
|
|
666
|
+
if not state.kernel._alive():
|
|
667
|
+
_retire_dead_kernel(kernel_id, state)
|
|
520
668
|
raise RuntimeError(f"Kernel {kernel_id} died from interrupt")
|
|
521
669
|
|
|
522
670
|
return True
|
|
@@ -532,10 +680,21 @@ def restart_kernel(
|
|
|
532
680
|
kernel_id: Kernel to restart.
|
|
533
681
|
with_packages: New packages (None = keep existing).
|
|
534
682
|
|
|
683
|
+
A kernel that died (and was dropped from the registry) is restarted
|
|
684
|
+
with the packages and cwd it had.
|
|
685
|
+
|
|
535
686
|
Raises:
|
|
536
687
|
KeyError: If kernel not found.
|
|
537
688
|
RuntimeError: If new kernel fails to start.
|
|
538
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
|
+
|
|
539
698
|
with _kernels_lock:
|
|
540
699
|
if kernel_id not in _kernels:
|
|
541
700
|
raise KeyError(f"Unknown kernel: {kernel_id}")
|
|
@@ -627,6 +786,7 @@ def shutdown_all_kernels():
|
|
|
627
786
|
"""Shutdown all kernels. Called by atexit."""
|
|
628
787
|
with _kernels_lock:
|
|
629
788
|
kernel_ids = list(_kernels.keys())
|
|
789
|
+
_crashed.clear()
|
|
630
790
|
|
|
631
791
|
for kid in kernel_ids:
|
|
632
792
|
try:
|
|
@@ -5,31 +5,33 @@ Supports multiple concurrent kernels via kernel_id parameter.
|
|
|
5
5
|
|
|
6
6
|
import ast
|
|
7
7
|
import asyncio
|
|
8
|
-
import importlib.metadata
|
|
9
8
|
import json
|
|
10
9
|
import logging
|
|
11
10
|
import os
|
|
12
11
|
import signal
|
|
13
12
|
import threading
|
|
13
|
+
from typing import List, Optional
|
|
14
14
|
|
|
15
|
+
import jsonschema
|
|
15
16
|
from mcp.server import Server
|
|
16
17
|
from mcp.server.stdio import stdio_server
|
|
17
18
|
from mcp.types import CallToolResult, ListToolsResult, TextContent, Tool
|
|
19
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
18
20
|
|
|
19
21
|
from agentic_python_coder.kernel import (
|
|
20
22
|
DEFAULT_KERNEL_ID,
|
|
21
23
|
create_kernel,
|
|
22
24
|
execute_in_kernel,
|
|
25
|
+
get_kernel_info,
|
|
23
26
|
interrupt_kernel_by_id,
|
|
24
27
|
kernel_exists,
|
|
25
28
|
kill_all_kernel_process_groups,
|
|
26
29
|
list_kernels,
|
|
30
|
+
probe_kernel,
|
|
27
31
|
restart_kernel,
|
|
28
32
|
shutdown_all_kernels,
|
|
29
33
|
)
|
|
30
34
|
|
|
31
|
-
_MCP_V2 = int(importlib.metadata.version("mcp").split(".")[0]) >= 2
|
|
32
|
-
|
|
33
35
|
# Configure logging
|
|
34
36
|
logger = logging.getLogger(__name__)
|
|
35
37
|
|
|
@@ -59,6 +61,39 @@ def truncate_output(result: dict) -> dict:
|
|
|
59
61
|
return result
|
|
60
62
|
|
|
61
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
|
+
|
|
62
97
|
def get_kernel_id(arguments: dict) -> str:
|
|
63
98
|
"""Get kernel_id from arguments, defaulting to DEFAULT_KERNEL_ID."""
|
|
64
99
|
return arguments.get("kernel_id") or DEFAULT_KERNEL_ID
|
|
@@ -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
|
|
|
@@ -570,20 +603,43 @@ import importlib.metadata
|
|
|
570
603
|
|
|
571
604
|
|
|
572
605
|
async def _on_list_tools(ctx, params) -> ListToolsResult:
|
|
573
|
-
"""
|
|
606
|
+
"""Lowlevel handler: wrap the tool list in its result type."""
|
|
574
607
|
return ListToolsResult(tools=await list_tools())
|
|
575
608
|
|
|
576
609
|
|
|
577
610
|
async def _on_call_tool(ctx, params) -> CallToolResult:
|
|
578
|
-
"""
|
|
611
|
+
"""Lowlevel handler.
|
|
579
612
|
|
|
580
|
-
|
|
581
|
-
a JSON-RPC error instead of an LLM-visible isError result
|
|
582
|
-
|
|
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.
|
|
583
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
|
+
)
|
|
584
640
|
try:
|
|
585
|
-
content = await call_tool(params.name,
|
|
586
|
-
except Exception as e: #
|
|
641
|
+
content = await call_tool(params.name, arguments)
|
|
642
|
+
except Exception as e: # error text reaches the model
|
|
587
643
|
return CallToolResult(
|
|
588
644
|
content=[TextContent(type="text", text=str(e))], is_error=True
|
|
589
645
|
)
|
|
@@ -591,14 +647,9 @@ async def _on_call_tool(ctx, params) -> CallToolResult:
|
|
|
591
647
|
|
|
592
648
|
|
|
593
649
|
def _make_server() -> Server:
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
)
|
|
598
|
-
srv = Server("ipython_mcp")
|
|
599
|
-
srv.list_tools()(list_tools)
|
|
600
|
-
srv.call_tool()(call_tool)
|
|
601
|
-
return srv
|
|
650
|
+
return Server(
|
|
651
|
+
"ipython_mcp", on_list_tools=_on_list_tools, on_call_tool=_on_call_tool
|
|
652
|
+
)
|
|
602
653
|
|
|
603
654
|
|
|
604
655
|
server = _make_server()
|
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<3,>=
|
|
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,,
|
|
File without changes
|
{agentic_python_coder-3.4.4.dist-info → agentic_python_coder-3.5.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agentic_python_coder-3.4.4.dist-info → agentic_python_coder-3.5.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|