codebind 0.1.0__tar.gz → 0.1.1__tar.gz

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
  Metadata-Version: 2.4
2
2
  Name: codebind
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A minimal model loop over a persistent IPython session.
5
5
  Keywords: ai,ipython,llm,repl,agents
6
6
  Author: Giovanni Gravili
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codebind"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "A minimal model loop over a persistent IPython session."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codebind"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "A minimal model loop over a persistent IPython session."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,6 +8,8 @@ from typing import Any
8
8
  from IPython.core.interactiveshell import InteractiveShell
9
9
  from IPython.utils.capture import capture_output
10
10
 
11
+ from .prompts import CellPrompts
12
+
11
13
 
12
14
  @dataclass(frozen=True, slots=True)
13
15
  class ExecutionReport:
@@ -31,19 +33,20 @@ class IPythonExecutor:
31
33
  def __init__(self, shell: InteractiveShell) -> None:
32
34
  self.shell = shell
33
35
 
34
- def execute(self, cell: str) -> ExecutionReport:
36
+ def execute(self, cell: str, *, prompts: CellPrompts | None = None) -> ExecutionReport:
35
37
  """Execute a cell, replay its visible output, and capture a structured result."""
36
38
  if not isinstance(cell, str) or not cell.strip():
37
39
  raise ValueError("cell must be a non-empty string")
38
40
 
39
- displayhook = self.shell.displayhook
40
- original_prompt = displayhook.write_output_prompt
41
- displayhook.write_output_prompt = lambda: None
41
+ original_prompts = getattr(self.shell, "prompts", None)
42
+ if prompts is not None and original_prompts is not None:
43
+ self.shell.prompts = prompts
42
44
  try:
43
45
  with capture_output() as captured:
44
46
  result = self.shell.run_cell(cell, store_history=False)
45
47
  finally:
46
- displayhook.write_output_prompt = original_prompt
48
+ if prompts is not None and original_prompts is not None:
49
+ self.shell.prompts = original_prompts
47
50
 
48
51
  displays: list[str] = []
49
52
  for output in captured.outputs:
@@ -0,0 +1,55 @@
1
+ """Native IPython prompt rendering for model-authored cells."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from IPython.terminal.prompts import Prompts
6
+ from IPython.terminal.ptutils import IPythonPTLexer
7
+ from prompt_toolkit.document import Document
8
+ from prompt_toolkit.formatted_text import FormattedText, PygmentsTokens
9
+ from prompt_toolkit.shortcuts import print_formatted_text
10
+ from pygments.token import Token
11
+
12
+
13
+ class CellPrompts(Prompts):
14
+ """IPython prompts for one numbered model-authored cell."""
15
+
16
+ def __init__(self, shell: object, number: int) -> None:
17
+ super().__init__(shell)
18
+ self.number = number
19
+
20
+ def in_prompt_tokens(self):
21
+ return [
22
+ (Token.Prompt, "Python ["),
23
+ (Token.PromptNum, str(self.number)),
24
+ (Token.Prompt, "]: "),
25
+ ]
26
+
27
+ def continuation_prompt_tokens(self, width: int | None = None, **_: object):
28
+ width = self._width() if width is None else width
29
+ return [
30
+ (Token.Prompt.Continuation, (" " * (width - 5)) + "...:"),
31
+ (Token.Prompt.Padding, " "),
32
+ ]
33
+
34
+ def out_prompt_tokens(self):
35
+ return [
36
+ (Token.OutPrompt, "Shell ["),
37
+ (Token.OutPromptNum, str(self.number)),
38
+ (Token.OutPrompt, "]: "),
39
+ ]
40
+
41
+
42
+ def render_cell(shell: object, cell: str, prompts: CellPrompts) -> None:
43
+ """Render a cell with IPython's lexer, prompt tokens, and terminal style."""
44
+ lexer = IPythonPTLexer().lex_document(Document(cell))
45
+ pt_app = getattr(shell, "pt_app", None)
46
+ style = pt_app.app.style if pt_app is not None else None
47
+
48
+ for index, _line in enumerate(cell.split("\n")):
49
+ prompt_tokens = (
50
+ prompts.in_prompt_tokens()
51
+ if index == 0
52
+ else prompts.continuation_prompt_tokens(lineno=index - 1)
53
+ )
54
+ print_formatted_text(PygmentsTokens(prompt_tokens), style=style, end="")
55
+ print_formatted_text(FormattedText(lexer(index)), style=style)
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import sys
7
6
  from collections.abc import Mapping, Sequence
8
7
  from typing import Any
9
8
 
@@ -14,6 +13,7 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, System
14
13
  from langchain_core.runnables import Runnable
15
14
 
16
15
  from .execution import ExecutionReport, IPythonExecutor
16
+ from .prompts import CellPrompts, render_cell
17
17
  from .rendering import TerminalRenderer
18
18
 
19
19
 
@@ -77,6 +77,7 @@ class Session:
77
77
  self.instructions = instructions.strip() if instructions else None
78
78
  self.messages: list[BaseMessage] = []
79
79
  self.last_response: AIMessage | None = None
80
+ self.cell_number = 0
80
81
  if self.instructions:
81
82
  self.messages.append(SystemMessage(self.instructions))
82
83
 
@@ -84,6 +85,7 @@ class Session:
84
85
  """Clear conversation history without clearing the shared Python namespace."""
85
86
  self.messages.clear()
86
87
  self.last_response = None
88
+ self.cell_number = 0
87
89
  if self.instructions:
88
90
  self.messages.append(SystemMessage(self.instructions))
89
91
 
@@ -134,18 +136,12 @@ class Session:
134
136
  return _tool_error("InvalidArguments", "ipython requires a string cell argument")
135
137
 
136
138
  cell = arguments["cell"]
137
- self._show_call(cell)
139
+ self.cell_number += 1
140
+ prompts = CellPrompts(self.shell, self.cell_number)
141
+ render_cell(self.shell, cell, prompts)
138
142
  try:
139
- report = self.executor.execute(cell)
143
+ report = self.executor.execute(cell, prompts=prompts)
140
144
  except Exception as error: # The failure must be returned to the model, not end the session.
141
145
  report = _tool_error(type(error).__name__, str(error))
142
146
  self.renderer.tool_output(report)
143
147
  return report
144
-
145
- def _show_call(self, cell: str) -> None:
146
- highlighted = self.shell.pycolorize(cell.rstrip())
147
- sys.stdout.write("\n")
148
- sys.stdout.write(highlighted)
149
- if not highlighted.endswith("\n"):
150
- sys.stdout.write("\n")
151
- sys.stdout.flush()
File without changes
File without changes
File without changes
File without changes