agentic-python-coder 2.2.1__py3-none-any.whl → 2.3.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.
@@ -1,6 +1,6 @@
1
1
  """Python Coding Agent - A minimal coding assistant using LangGraph and OpenRouter."""
2
2
 
3
- __version__ = "2.2.1"
3
+ __version__ = "2.3.0"
4
4
 
5
5
  # High-level API (recommended for most users)
6
6
  from agentic_python_coder.runner import solve_task
@@ -21,12 +21,33 @@ from agentic_python_coder.llm import (
21
21
  DEFAULT_MODEL,
22
22
  )
23
23
 
24
+ # Kernel management (multi-kernel API)
25
+ from agentic_python_coder.kernel import (
26
+ # Core functions
27
+ create_kernel,
28
+ execute_in_kernel,
29
+ shutdown_kernel_by_id,
30
+ interrupt_kernel_by_id,
31
+ restart_kernel,
32
+ # Query functions
33
+ list_kernels,
34
+ kernel_exists,
35
+ get_kernel_info,
36
+ shutdown_all_kernels,
37
+ # Backward compat
38
+ get_kernel,
39
+ shutdown_kernel,
40
+ # Constants
41
+ DEFAULT_KERNEL_ID,
42
+ MAX_KERNELS,
43
+ )
44
+
24
45
  __all__ = [
25
46
  # Version
26
47
  "__version__",
27
48
  # High-level
28
49
  "solve_task",
29
- # Low-level
50
+ # Low-level agent
30
51
  "create_coding_agent",
31
52
  "run_agent",
32
53
  "get_final_response",
@@ -36,4 +57,18 @@ __all__ = [
36
57
  "load_model_config",
37
58
  "list_available_models",
38
59
  "DEFAULT_MODEL",
60
+ # Kernel management
61
+ "create_kernel",
62
+ "execute_in_kernel",
63
+ "shutdown_kernel_by_id",
64
+ "interrupt_kernel_by_id",
65
+ "restart_kernel",
66
+ "list_kernels",
67
+ "kernel_exists",
68
+ "get_kernel_info",
69
+ "shutdown_all_kernels",
70
+ "get_kernel",
71
+ "shutdown_kernel",
72
+ "DEFAULT_KERNEL_ID",
73
+ "MAX_KERNELS",
39
74
  ]
@@ -1,8 +1,11 @@
1
1
  """ReAct agent for Python coding tasks."""
2
2
 
3
+ import json
4
+ import os
5
+ import time
3
6
  from typing import Dict, Any, List, Optional
4
7
  from pathlib import Path
5
- from langgraph.prebuilt import create_react_agent
8
+ from langchain.agents import create_agent
6
9
  from langgraph.checkpoint.memory import InMemorySaver
7
10
 
8
11
  from agentic_python_coder.llm import get_openrouter_llm
@@ -45,7 +48,7 @@ def create_coding_agent(
45
48
  working_directory: Directory for file operations
46
49
  system_prompt: System prompt as string (takes precedence over path)
47
50
  system_prompt_path: Path to system prompt file (used if system_prompt not provided)
48
- model: Optional model name (defaults to claude-sonnet-4.5)
51
+ model: Optional model name (uses configured default if not specified)
49
52
  project_prompt: Optional project-specific prompt
50
53
  with_packages: Optional list of packages for dynamic mode
51
54
  task_content: Task description/content
@@ -66,8 +69,6 @@ def create_coding_agent(
66
69
 
67
70
  # Store packages for kernel initialization
68
71
  if with_packages is not None:
69
- import os
70
-
71
72
  os.environ["CODER_WITH_PACKAGES"] = ",".join(with_packages)
72
73
 
73
74
  # Get LLM instance
@@ -108,15 +109,13 @@ def create_coding_agent(
108
109
 
109
110
  # Create the agent with memory
110
111
  checkpointer = InMemorySaver()
111
- agent = create_react_agent(
112
- llm, tools, prompt=combined_prompt, checkpointer=checkpointer
112
+ agent = create_agent(
113
+ llm, tools, system_prompt=combined_prompt, checkpointer=checkpointer
113
114
  )
114
115
 
115
116
  # Store metadata for run_agent to use
116
117
  agent._coder_metadata = {
117
118
  "working_directory": working_directory,
118
- "with_packages": with_packages,
119
- "task_basename": task_basename,
120
119
  }
121
120
 
122
121
  return agent
@@ -126,24 +125,21 @@ def _print_tool_progress(tool_name: str, args: dict):
126
125
  """Print progress info for a tool call."""
127
126
  if tool_name == "python_exec" and "code" in args:
128
127
  code = args["code"]
128
+ code_stripped = code.strip()
129
129
  if "def " in code:
130
- func_match = code.split("def ")[1].split("(")[0] if "def " in code else ""
130
+ func_match = code.split("def ")[1].split("(")[0]
131
131
  print(f" {tool_name}: defining function {func_match}()")
132
132
  elif "class " in code:
133
- class_match = (
134
- code.split("class ")[1].split("(")[0].split(":")[0]
135
- if "class " in code
136
- else ""
137
- )
133
+ class_match = code.split("class ")[1].split("(")[0].split(":")[0]
138
134
  print(f" {tool_name}: defining class {class_match}")
139
- elif "import " in code and len(code.strip().split("\n")) == 1:
140
- print(f" {tool_name}: {code.strip()}")
141
- elif "=" in code and len(code.strip().split("\n")) == 1:
135
+ elif "import " in code and len(code_stripped.split("\n")) == 1:
136
+ print(f" {tool_name}: {code_stripped}")
137
+ elif "=" in code and len(code_stripped.split("\n")) == 1:
142
138
  var_name = code.split("=")[0].strip()
143
139
  print(f" {tool_name}: assigning variable {var_name}")
144
- elif code.strip().startswith("print("):
140
+ elif code_stripped.startswith("print("):
145
141
  print(
146
- f" {tool_name}: {code.strip()[:50]}{'...' if len(code.strip()) > 50 else ''}"
142
+ f" {tool_name}: {code_stripped[:50]}{'...' if len(code_stripped) > 50 else ''}"
147
143
  )
148
144
  elif "read_csv" in code or "read_excel" in code or "read_json" in code:
149
145
  print(f" {tool_name}: loading data file")
@@ -170,32 +166,23 @@ def _print_tool_progress(tool_name: str, args: dict):
170
166
  print(f"\n {tool_name}:")
171
167
  todos = args["todos"]
172
168
  for todo in todos:
173
- status_symbol = (
174
- ""
175
- if todo["status"] == "completed"
176
- else "☐"
177
- if todo["status"] == "pending"
178
- else "▶"
179
- )
180
- print(f" {status_symbol} {todo['content']}")
169
+ status = todo.get("status", "")
170
+ content = todo.get("content", "")
171
+ status_symbol = "" if status == "completed" else "☐" if status == "pending" else "▶"
172
+ print(f" {status_symbol} {content}")
181
173
  else:
182
- arg_str = str(args)[:30]
183
- if len(str(args)) > 30:
184
- arg_str += "..."
185
- print(f" {tool_name}: {arg_str}")
174
+ args_str = str(args)
175
+ arg_display = args_str[:30] + "..." if len(args_str) > 30 else args_str
176
+ print(f" {tool_name}: {arg_display}")
186
177
 
187
178
 
188
- def _process_tool_calls(msg, current_tools: dict, stats: dict, quiet: bool):
179
+ def _process_tool_calls(msg, stats: dict, quiet: bool):
189
180
  """Process tool calls from a message, updating stats and optionally printing."""
190
181
  if hasattr(msg, "tool_calls") and msg.tool_calls:
191
182
  for tool_call in msg.tool_calls:
192
183
  tool_name = tool_call.get("name") or tool_call.get("function", {}).get(
193
184
  "name"
194
185
  )
195
- tool_id = tool_call.get("id")
196
- if tool_id:
197
- current_tools[tool_id] = tool_name
198
-
199
186
  if tool_name:
200
187
  stats["tool_usage"][tool_name] = (
201
188
  stats["tool_usage"].get(tool_name, 0) + 1
@@ -213,9 +200,6 @@ def _process_tool_calls(msg, current_tools: dict, stats: dict, quiet: bool):
213
200
  for tool_call in tool_calls:
214
201
  function = tool_call.get("function", {})
215
202
  tool_name = function.get("name")
216
- tool_id = tool_call.get("id")
217
- if tool_id and tool_name:
218
- current_tools[tool_id] = tool_name
219
203
 
220
204
  if tool_name:
221
205
  stats["tool_usage"][tool_name] = (
@@ -225,11 +209,9 @@ def _process_tool_calls(msg, current_tools: dict, stats: dict, quiet: bool):
225
209
  if not quiet and tool_name:
226
210
  args_str = function.get("arguments", "{}")
227
211
  try:
228
- import json
229
-
230
212
  args = json.loads(args_str)
231
213
  _print_tool_progress(tool_name, args)
232
- except Exception:
214
+ except json.JSONDecodeError:
233
215
  print(f" {tool_name}")
234
216
 
235
217
 
@@ -243,6 +225,7 @@ def _update_token_stats(msg, stats: dict):
243
225
  "completion_tokens", 0
244
226
  )
245
227
  stats["token_consumption"]["total_tokens"] += usage.get("total_tokens", 0)
228
+ return # Avoid double-counting if both metadata sources exist
246
229
 
247
230
  if hasattr(msg, "usage_metadata") and msg.usage_metadata:
248
231
  stats["token_consumption"]["input_tokens"] += msg.usage_metadata.get(
@@ -285,7 +268,6 @@ def run_agent(
285
268
  config = {"configurable": {"thread_id": thread_id}, "recursion_limit": limit}
286
269
 
287
270
  messages = []
288
- current_tools = {}
289
271
 
290
272
  # Initialize statistics
291
273
  stats = {
@@ -294,8 +276,6 @@ def run_agent(
294
276
  "execution_time_seconds": 0,
295
277
  }
296
278
 
297
- import time
298
-
299
279
  start_time = time.time()
300
280
 
301
281
  # Stream the agent's work
@@ -313,7 +293,7 @@ def run_agent(
313
293
  messages.append(msg)
314
294
 
315
295
  # Process tool calls (always update stats, optionally print)
316
- _process_tool_calls(msg, current_tools, stats, quiet)
296
+ _process_tool_calls(msg, stats, quiet)
317
297
 
318
298
  # Update token statistics
319
299
  _update_token_stats(msg, stats)
@@ -23,7 +23,11 @@ from agentic_python_coder.project_md import (
23
23
  check_packages_available,
24
24
  create_project_prompt,
25
25
  )
26
- from agentic_python_coder.llm import DEFAULT_MODEL, list_available_models, load_model_config
26
+ from agentic_python_coder.llm import (
27
+ DEFAULT_MODEL,
28
+ list_available_models,
29
+ load_model_config,
30
+ )
27
31
  from agentic_python_coder import __version__
28
32
 
29
33
 
@@ -88,7 +92,8 @@ def parse_args():
88
92
  )
89
93
 
90
94
  parser.add_argument(
91
- "--version", "-V",
95
+ "--version",
96
+ "-V",
92
97
  action="version",
93
98
  version=f"%(prog)s {__version__}",
94
99
  )
@@ -106,7 +111,9 @@ def parse_args():
106
111
  help="Path to task file (creates {basename}_code.py and {basename}.jsonl)",
107
112
  )
108
113
 
109
- parser.add_argument("--model", help=f"Model name or JSON file (default: {DEFAULT_MODEL})")
114
+ parser.add_argument(
115
+ "--model", help=f"Model name or JSON file (default: {DEFAULT_MODEL})"
116
+ )
110
117
 
111
118
  parser.add_argument(
112
119
  "--interactive", "-i", action="store_true", help="Interactive mode"
@@ -145,7 +152,8 @@ def parse_args():
145
152
  )
146
153
 
147
154
  parser.add_argument(
148
- "--quiet", "-q",
155
+ "--quiet",
156
+ "-q",
149
157
  action="store_true",
150
158
  help="Suppress console output during execution",
151
159
  )
@@ -176,13 +184,13 @@ def copy_resource_dir(source_path, dest_path: Path):
176
184
  dest_item = dest_path / item.name
177
185
  if item.is_file():
178
186
  # Skip __pycache__ and .pyc files
179
- if item.name.endswith('.pyc') or item.name == '__pycache__':
187
+ if item.name.endswith(".pyc") or item.name == "__pycache__":
180
188
  continue
181
189
  # Read and write file content
182
190
  content = item.read_bytes()
183
191
  dest_item.write_bytes(content)
184
192
  elif item.is_dir():
185
- if item.name == '__pycache__':
193
+ if item.name == "__pycache__":
186
194
  continue
187
195
  copy_resource_dir(item, dest_item)
188
196
 
@@ -216,7 +224,9 @@ def init_examples(template: str = "all"):
216
224
 
217
225
  print(f"\nExamples initialized in: {output_dir.absolute()}")
218
226
  print("\nUsage:")
219
- print(f" coder --with cpmpy --project {output_dir}/cpmpy/cpmpy.md --task {output_dir}/cpmpy/sample_tasks/n_queens.md")
227
+ print(
228
+ f" coder --with cpmpy --project {output_dir}/cpmpy/cpmpy.md --task {output_dir}/cpmpy/sample_tasks/n_queens.md"
229
+ )
220
230
 
221
231
 
222
232
  def validate_packages(packages):