codebind 0.1.0__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.
codebind-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Giovanni Gravili
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: codebind
3
+ Version: 0.1.0
4
+ Summary: A minimal model loop over a persistent IPython session.
5
+ Keywords: ai,ipython,llm,repl,agents
6
+ Author: Giovanni Gravili
7
+ Author-email: Giovanni Gravili <ghovax@users.noreply.github.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: ipython>=9.0
18
+ Requires-Dist: langchain-core>=1.0
19
+ Requires-Dist: models-provider>=0.1.0
20
+ Requires-Dist: rich>=14.0
21
+ Requires-Python: >=3.13
22
+ Project-URL: Repository, https://github.com/ghovax/codebind
23
+ Project-URL: Issues, https://github.com/ghovax/codebind/issues
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Codebind
27
+
28
+ Codebind runs a model with one tool: an IPython cell executed in the session shared with the user. Conversation history belongs to a `Session`; the model is selected independently for every call.
29
+
30
+ ## Installation
31
+
32
+ Run Codebind without installing it permanently:
33
+
34
+ ```console
35
+ uvx codebind
36
+ ```
37
+
38
+ Or install it with any Python package installer:
39
+
40
+ ```console
41
+ pip install codebind
42
+ ```
43
+
44
+ After installation, start the preconfigured shell from any directory:
45
+
46
+ ```console
47
+ codebind
48
+ ```
49
+
50
+ It opens IPython with `chat` and `Models` already available and renders a short Markdown usage guide in the terminal. It does not modify the user's global IPython profile.
51
+
52
+ ```python
53
+ models = Models({"openai": "OPENAI_API_KEY"})
54
+
55
+ chat.ask(
56
+ "Inspect this project and tell me what to implement first.",
57
+ models.chat("openai/gpt-5", reasoning_effort="medium"),
58
+ )
59
+ ```
60
+
61
+ Codebind does not load files or construct a project prompt automatically. The user states what should be loaded as context. Pass `instructions=` to `Session` only when an application needs its own system instructions.
62
+
63
+ ## ChatGPT account login
64
+
65
+ Models Provider can start its OpenAI browser sign-in flow directly from IPython:
66
+
67
+ ```python
68
+ import webbrowser
69
+
70
+ models = Models()
71
+ authorization = await models.sign_in("openai")
72
+ webbrowser.open(authorization.url)
73
+ await authorization.complete()
74
+
75
+ chat.ask(
76
+ "Inspect this project.",
77
+ models.chat("openai/gpt-5", authorization=authorization),
78
+ )
79
+ ```
80
+
81
+ The authorization remains in memory for this session. Persistent credential storage belongs to the host application.
82
+
83
+ OpenAI officially supports ChatGPT subscription sign-in for Codex clients. Models Provider reproduces that account-access boundary for this library; it is separate from the public, pay-as-you-go OpenAI API and may require compatibility updates when the Codex account protocol changes.
@@ -0,0 +1,58 @@
1
+ # Codebind
2
+
3
+ Codebind runs a model with one tool: an IPython cell executed in the session shared with the user. Conversation history belongs to a `Session`; the model is selected independently for every call.
4
+
5
+ ## Installation
6
+
7
+ Run Codebind without installing it permanently:
8
+
9
+ ```console
10
+ uvx codebind
11
+ ```
12
+
13
+ Or install it with any Python package installer:
14
+
15
+ ```console
16
+ pip install codebind
17
+ ```
18
+
19
+ After installation, start the preconfigured shell from any directory:
20
+
21
+ ```console
22
+ codebind
23
+ ```
24
+
25
+ It opens IPython with `chat` and `Models` already available and renders a short Markdown usage guide in the terminal. It does not modify the user's global IPython profile.
26
+
27
+ ```python
28
+ models = Models({"openai": "OPENAI_API_KEY"})
29
+
30
+ chat.ask(
31
+ "Inspect this project and tell me what to implement first.",
32
+ models.chat("openai/gpt-5", reasoning_effort="medium"),
33
+ )
34
+ ```
35
+
36
+ Codebind does not load files or construct a project prompt automatically. The user states what should be loaded as context. Pass `instructions=` to `Session` only when an application needs its own system instructions.
37
+
38
+ ## ChatGPT account login
39
+
40
+ Models Provider can start its OpenAI browser sign-in flow directly from IPython:
41
+
42
+ ```python
43
+ import webbrowser
44
+
45
+ models = Models()
46
+ authorization = await models.sign_in("openai")
47
+ webbrowser.open(authorization.url)
48
+ await authorization.complete()
49
+
50
+ chat.ask(
51
+ "Inspect this project.",
52
+ models.chat("openai/gpt-5", authorization=authorization),
53
+ )
54
+ ```
55
+
56
+ The authorization remains in memory for this session. Persistent credential storage belongs to the host application.
57
+
58
+ OpenAI officially supports ChatGPT subscription sign-in for Codex clients. Models Provider reproduces that account-access boundary for this library; it is separate from the public, pay-as-you-go OpenAI API and may require compatibility updates when the Codex account protocol changes.
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "codebind"
3
+ version = "0.1.0"
4
+ description = "A minimal model loop over a persistent IPython session."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "ai",
11
+ "ipython",
12
+ "llm",
13
+ "repl",
14
+ "agents",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "ipython>=9.0",
27
+ "langchain-core>=1.0",
28
+ "models-provider>=0.1.0",
29
+ "rich>=14.0",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "Giovanni Gravili"
34
+ email = "ghovax@users.noreply.github.com"
35
+
36
+ [project.urls]
37
+ Repository = "https://github.com/ghovax/codebind"
38
+ Issues = "https://github.com/ghovax/codebind/issues"
39
+
40
+ [project.scripts]
41
+ codebind = "codebind.cli:main"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "pytest>=8.4",
46
+ "ruff>=0.13",
47
+ ]
48
+
49
+ [tool.ruff]
50
+ line-length = 100
51
+ target-version = "py313"
52
+
53
+ [tool.ruff.lint]
54
+ select = [
55
+ "E4",
56
+ "E7",
57
+ "E9",
58
+ "F",
59
+ "B",
60
+ "RUF",
61
+ ]
62
+
63
+ [build-system]
64
+ requires = ["uv_build>=0.12.3,<0.13.0"]
65
+ build-backend = "uv_build"
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "codebind"
3
+ version = "0.1.0"
4
+ description = "A minimal model loop over a persistent IPython session."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Giovanni Gravili", email = "ghovax@users.noreply.github.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ keywords = ["ai", "ipython", "llm", "repl", "agents"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "ipython>=9.0",
24
+ "langchain-core>=1.0",
25
+ "models-provider>=0.1.0",
26
+ "rich>=14.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/ghovax/codebind"
31
+ Issues = "https://github.com/ghovax/codebind/issues"
32
+
33
+ [project.scripts]
34
+ codebind = "codebind.cli:main"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "pytest>=8.4",
39
+ "ruff>=0.13",
40
+ ]
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py313"
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E4", "E7", "E9", "F", "B", "RUF"]
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.12.3,<0.13.0"]
51
+ build-backend = "uv_build"
@@ -0,0 +1,16 @@
1
+ """A minimal model loop over the current IPython session."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from .execution import ExecutionReport, IPythonExecutor
6
+ from .session import Session
7
+
8
+
9
+ __version__ = version("codebind")
10
+
11
+ __all__ = [
12
+ "ExecutionReport",
13
+ "IPythonExecutor",
14
+ "Session",
15
+ "__version__",
16
+ ]
@@ -0,0 +1,45 @@
1
+ """Project-local IPython launcher with Codebind conveniences preloaded."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from typing import Any
7
+
8
+ from IPython.core.interactiveshell import InteractiveShell
9
+ from IPython.terminal.ipapp import TerminalIPythonApp
10
+ from models_provider import Models
11
+ from rich.console import Console
12
+ from rich.markdown import Markdown
13
+
14
+ from . import __version__
15
+ from .session import Session
16
+
17
+
18
+ BANNER = """\
19
+ - `models = Models({...})`
20
+ - `chat.ask("...", models.chat("provider/model"))`
21
+ """
22
+
23
+
24
+ def namespace(shell: InteractiveShell) -> dict[str, Any]:
25
+ """Build the small namespace exposed by the Codebind shell."""
26
+ return {
27
+ "chat": Session(shell=shell),
28
+ "Models": Models,
29
+ }
30
+
31
+
32
+ def main() -> None:
33
+ """Start IPython in the current directory with Codebind preloaded."""
34
+ parser = argparse.ArgumentParser(description="Start a model-enabled IPython session.")
35
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
36
+ parser.parse_args()
37
+
38
+ Console().print(Markdown(BANNER))
39
+ application = TerminalIPythonApp.instance()
40
+ application.display_banner = False
41
+ application.initialize([])
42
+ shell = application.shell
43
+ shell.enable_tip = False
44
+ shell.push(namespace(shell))
45
+ application.start()
@@ -0,0 +1,67 @@
1
+ """Execution of model-authored cells in a shared IPython namespace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from typing import Any
7
+
8
+ from IPython.core.interactiveshell import InteractiveShell
9
+ from IPython.utils.capture import capture_output
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ExecutionReport:
14
+ """The model-facing result of one IPython cell."""
15
+
16
+ ok: bool
17
+ stdout: str
18
+ stderr: str
19
+ result: str | None
20
+ displays: tuple[str, ...]
21
+ error: dict[str, str] | None
22
+
23
+ def as_dict(self) -> dict[str, Any]:
24
+ """Return a JSON-serializable representation."""
25
+ return asdict(self)
26
+
27
+
28
+ class IPythonExecutor:
29
+ """Run cells through one existing IPython shell."""
30
+
31
+ def __init__(self, shell: InteractiveShell) -> None:
32
+ self.shell = shell
33
+
34
+ def execute(self, cell: str) -> ExecutionReport:
35
+ """Execute a cell, replay its visible output, and capture a structured result."""
36
+ if not isinstance(cell, str) or not cell.strip():
37
+ raise ValueError("cell must be a non-empty string")
38
+
39
+ displayhook = self.shell.displayhook
40
+ original_prompt = displayhook.write_output_prompt
41
+ displayhook.write_output_prompt = lambda: None
42
+ try:
43
+ with capture_output() as captured:
44
+ result = self.shell.run_cell(cell, store_history=False)
45
+ finally:
46
+ displayhook.write_output_prompt = original_prompt
47
+
48
+ displays: list[str] = []
49
+ for output in captured.outputs:
50
+ data = getattr(output, "data", None)
51
+ if isinstance(data, dict) and "text/plain" in data:
52
+ displays.append(str(data["text/plain"]))
53
+
54
+ exception = result.error_before_exec or result.error_in_exec
55
+ error = (
56
+ {"type": type(exception).__name__, "message": str(exception)}
57
+ if exception is not None
58
+ else None
59
+ )
60
+ return ExecutionReport(
61
+ ok=result.success,
62
+ stdout=captured.stdout,
63
+ stderr=captured.stderr,
64
+ result=repr(result.result) if result.result is not None else None,
65
+ displays=tuple(displays),
66
+ error=error,
67
+ )
File without changes
@@ -0,0 +1,54 @@
1
+ """Terminal rendering for model responses and IPython tool output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from itertools import chain
6
+
7
+ from rich.console import Console
8
+ from rich.markdown import Markdown
9
+ from rich.segment import Segments
10
+ from rich.text import Text
11
+
12
+ from .execution import ExecutionReport
13
+
14
+
15
+ _MAX_OUTPUT_LINES = 10
16
+
17
+
18
+ def _visible_output(report: ExecutionReport) -> str:
19
+ parts = [part for part in (report.stdout, report.stderr) if part]
20
+ if report.displays:
21
+ parts.extend(report.displays)
22
+ elif not parts and report.result is not None:
23
+ parts.append(report.result)
24
+ if not parts and report.error is not None:
25
+ parts.append(f"{report.error['type']}: {report.error['message']}")
26
+ return "\n".join(part.rstrip("\n") for part in parts if part).strip("\n")
27
+
28
+
29
+ class TerminalRenderer:
30
+ """Render concise tool previews and Markdown answers to the active terminal."""
31
+
32
+ def __init__(self) -> None:
33
+ self.console = Console()
34
+
35
+ def tool_output(self, report: ExecutionReport) -> None:
36
+ """Show a bounded preview while leaving the report itself intact for the model."""
37
+ visible = _visible_output(report)
38
+ if not visible:
39
+ return
40
+ output = Text.from_ansi(visible)
41
+ options = self.console.options
42
+ complete = self.console.render_lines(output, options, pad=False, new_lines=True)
43
+ preview = complete[:_MAX_OUTPUT_LINES]
44
+ self.console.print(Segments(chain.from_iterable(preview)), end="")
45
+ if len(complete) > len(preview):
46
+ self.console.print(
47
+ f"[dim]{len(preview)} of {len(complete)} lines shown; "
48
+ "the complete result was returned to the model.[/dim]"
49
+ )
50
+
51
+ def assistant(self, text: str) -> None:
52
+ """Render the final model response as terminal Markdown."""
53
+ self.console.print()
54
+ self.console.print(Markdown(text))
@@ -0,0 +1,151 @@
1
+ """Conversation state and the single IPython-tool model loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from collections.abc import Mapping, Sequence
8
+ from typing import Any
9
+
10
+ from IPython import get_ipython
11
+ from IPython.core.interactiveshell import InteractiveShell
12
+ from langchain_core.language_models import BaseChatModel
13
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
14
+ from langchain_core.runnables import Runnable
15
+
16
+ from .execution import ExecutionReport, IPythonExecutor
17
+ from .rendering import TerminalRenderer
18
+
19
+
20
+ IPYTHON_TOOL = {
21
+ "type": "function",
22
+ "function": {
23
+ "name": "ipython",
24
+ "description": (
25
+ "Execute an IPython cell in the state-persistent session shared with the user."
26
+ ),
27
+ "parameters": {
28
+ "type": "object",
29
+ "properties": {
30
+ "cell": {
31
+ "type": "string",
32
+ "description": "A complete IPython cell to execute.",
33
+ }
34
+ },
35
+ "required": ["cell"],
36
+ "additionalProperties": False,
37
+ },
38
+ },
39
+ }
40
+
41
+
42
+ def _message_text(message: AIMessage) -> str:
43
+ content = message.content
44
+ if isinstance(content, str):
45
+ return content
46
+ if not isinstance(content, Sequence) or isinstance(content, (bytes, bytearray, str)):
47
+ return str(content)
48
+ parts: list[str] = []
49
+ for block in content:
50
+ if isinstance(block, str):
51
+ parts.append(block)
52
+ elif isinstance(block, Mapping) and isinstance(block.get("text"), str):
53
+ parts.append(block["text"])
54
+ return "".join(parts)
55
+
56
+
57
+ def _tool_error(error_type: str, message: str) -> ExecutionReport:
58
+ return ExecutionReport(False, "", "", None, (), {"type": error_type, "message": message})
59
+
60
+
61
+ class Session:
62
+ """Keep conversation history while choosing the model independently for every turn."""
63
+
64
+ def __init__(
65
+ self,
66
+ *,
67
+ shell: InteractiveShell | None = None,
68
+ instructions: str | None = None,
69
+ ) -> None:
70
+ resolved_shell = shell or get_ipython()
71
+ if resolved_shell is None:
72
+ raise RuntimeError("Session must be created inside IPython or given an IPython shell.")
73
+
74
+ self.shell = resolved_shell
75
+ self.executor = IPythonExecutor(resolved_shell)
76
+ self.renderer = TerminalRenderer()
77
+ self.instructions = instructions.strip() if instructions else None
78
+ self.messages: list[BaseMessage] = []
79
+ self.last_response: AIMessage | None = None
80
+ if self.instructions:
81
+ self.messages.append(SystemMessage(self.instructions))
82
+
83
+ def clear(self) -> None:
84
+ """Clear conversation history without clearing the shared Python namespace."""
85
+ self.messages.clear()
86
+ self.last_response = None
87
+ if self.instructions:
88
+ self.messages.append(SystemMessage(self.instructions))
89
+
90
+ def ask(self, prompt: str, model: BaseChatModel) -> None:
91
+ """Run one user turn with the explicitly supplied model."""
92
+ text = prompt.strip()
93
+ if not text:
94
+ raise ValueError("prompt cannot be empty")
95
+
96
+ bound_model = self._bind(model)
97
+ self.messages.append(HumanMessage(text))
98
+
99
+ while True:
100
+ response = bound_model.invoke(tuple(self.messages))
101
+ if not isinstance(response, AIMessage):
102
+ raise TypeError("model must return an AIMessage")
103
+ self.messages.append(response)
104
+ self.last_response = response
105
+
106
+ if not response.tool_calls:
107
+ answer = _message_text(response)
108
+ if answer:
109
+ self.renderer.assistant(answer)
110
+ return
111
+
112
+ for call in response.tool_calls:
113
+ report = self._execute_call(call)
114
+ identifier = str(call.get("id") or f"ipython-{len(self.messages)}")
115
+ self.messages.append(
116
+ ToolMessage(
117
+ json.dumps(report.as_dict(), ensure_ascii=False),
118
+ tool_call_id=identifier,
119
+ )
120
+ )
121
+
122
+ @staticmethod
123
+ def _bind(model: BaseChatModel) -> Runnable[Any, BaseMessage]:
124
+ try:
125
+ return model.bind_tools([IPYTHON_TOOL], parallel_tool_calls=False)
126
+ except NotImplementedError:
127
+ return model.bind(tools=[IPYTHON_TOOL], parallel_tool_calls=False)
128
+
129
+ def _execute_call(self, call: Mapping[str, Any]) -> ExecutionReport:
130
+ if call.get("name") != "ipython":
131
+ return _tool_error("UnknownTool", f"unknown tool: {call.get('name')!r}")
132
+ arguments = call.get("args")
133
+ if not isinstance(arguments, Mapping) or not isinstance(arguments.get("cell"), str):
134
+ return _tool_error("InvalidArguments", "ipython requires a string cell argument")
135
+
136
+ cell = arguments["cell"]
137
+ self._show_call(cell)
138
+ try:
139
+ report = self.executor.execute(cell)
140
+ except Exception as error: # The failure must be returned to the model, not end the session.
141
+ report = _tool_error(type(error).__name__, str(error))
142
+ self.renderer.tool_output(report)
143
+ 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()