codegraff 0.1.1__cp313-cp313-win_amd64.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.
codegraff/__init__.py ADDED
@@ -0,0 +1,270 @@
1
+ """Python SDK for the codegraff agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Iterator, Optional
9
+
10
+ from codegraff._native import GraffApi, ChatStreamHandle, version as _native_version
11
+ from codegraff._validation import validate
12
+
13
+ __all__ = ["Graff", "GraffSession", "Sandbox", "version"]
14
+
15
+
16
+ def version() -> str:
17
+ return _native_version()
18
+
19
+
20
+ @dataclass
21
+ class AgentEvent:
22
+ type: str
23
+ data: dict[str, Any] = field(default_factory=dict)
24
+
25
+ def __repr__(self) -> str:
26
+ return f"AgentEvent({self.type!r}, {self.data})"
27
+
28
+
29
+ def _parse_event(raw: str) -> AgentEvent:
30
+ d = json.loads(raw)
31
+ t = d.pop("type", "unknown")
32
+ return AgentEvent(type=t, data=d)
33
+
34
+
35
+ class Graff:
36
+ """Long-lived codegraff instance. Mirrors the TS SDK's Graff class."""
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ cwd: Optional[str] = None,
42
+ provider: Optional[str] = None,
43
+ api_key: Optional[str] = None,
44
+ model: Optional[str] = None,
45
+ max_tokens: Optional[int] = None,
46
+ ):
47
+ validate("init", {"cwd": cwd, "provider": provider, "api_key": api_key, "model": model, "max_tokens": max_tokens})
48
+ if api_key and not provider:
49
+ raise ValueError("provider is required when api_key is supplied")
50
+
51
+ if provider:
52
+ os.environ["FORGE_SESSION__PROVIDER_ID"] = provider
53
+ if model:
54
+ os.environ["FORGE_SESSION__MODEL_ID"] = model
55
+ if max_tokens is not None:
56
+ os.environ["FORGE_MAX_TOKENS"] = str(max_tokens)
57
+
58
+ self._api = GraffApi.init(cwd or os.getcwd())
59
+
60
+ if api_key and provider:
61
+ # extra_params is a required positional in the native binding
62
+ # (Option without a #[pyo3(signature)] default), so pass None.
63
+ self._api.upsert_credential(provider, api_key, None)
64
+
65
+ def chat(
66
+ self,
67
+ prompt: str,
68
+ *,
69
+ conversation_id: Optional[str] = None,
70
+ model: Optional[str] = None,
71
+ ) -> Iterator[AgentEvent]:
72
+ """Run a chat turn. Yields AgentEvent objects."""
73
+ validate("chat", {"prompt": prompt, "conversation_id": conversation_id, "model": model})
74
+ handle = self._api.chat(prompt, conversation_id, model)
75
+ yield AgentEvent(type="ConversationStarted", data={"conversationId": handle.conversation_id})
76
+ while True:
77
+ raw = handle.next_event()
78
+ if raw is None:
79
+ break
80
+ yield _parse_event(raw)
81
+
82
+ def session(
83
+ self,
84
+ *,
85
+ conversation_id: Optional[str] = None,
86
+ model: Optional[str] = None,
87
+ ) -> "GraffSession":
88
+ return GraffSession(self, conversation_id=conversation_id, model=model)
89
+
90
+ # -- Conversation management --
91
+
92
+ def list_conversations(self, limit: Optional[int] = None) -> list[dict]:
93
+ return json.loads(self._api.list_conversations(limit))
94
+
95
+ def get_conversation(self, id: str) -> Optional[dict]:
96
+ raw = self._api.get_conversation(id)
97
+ return json.loads(raw) if raw else None
98
+
99
+ def last_conversation(self) -> Optional[dict]:
100
+ raw = self._api.last_conversation()
101
+ return json.loads(raw) if raw else None
102
+
103
+ def delete_conversation(self, id: str) -> None:
104
+ self._api.delete_conversation(id)
105
+
106
+ def compact_conversation(self, id: str) -> dict:
107
+ return json.loads(self._api.compact_conversation(id))
108
+
109
+ # -- Agents --
110
+
111
+ def get_agent_infos(self) -> list[dict]:
112
+ return json.loads(self._api.get_agent_infos())
113
+
114
+ # -- Auth --
115
+
116
+ def upsert_credential(
117
+ self,
118
+ provider_id: str,
119
+ api_key: str,
120
+ extra_params: Optional[list[list[str]]] = None,
121
+ ) -> None:
122
+ self._api.upsert_credential(provider_id, api_key, extra_params)
123
+
124
+ def remove_credential(self, provider_id: str) -> None:
125
+ self._api.remove_credential(provider_id)
126
+
127
+ # -- Sandboxes (gateway HTTP) --
128
+
129
+ def create_sandbox(self, **kwargs) -> "Sandbox":
130
+ return Sandbox.create(self, **kwargs)
131
+
132
+ def get_sandbox(self, id: str) -> "Sandbox":
133
+ return Sandbox.get(self, id)
134
+
135
+ def list_sandboxes(self) -> list[dict]:
136
+ return Sandbox.list(self)
137
+
138
+ def version(self) -> str:
139
+ return self._api.version()
140
+
141
+
142
+ class GraffSession:
143
+ """Multi-turn session that preserves conversation_id across sends."""
144
+
145
+ def __init__(
146
+ self,
147
+ graff: Graff,
148
+ *,
149
+ conversation_id: Optional[str] = None,
150
+ model: Optional[str] = None,
151
+ ):
152
+ self._graff = graff
153
+ self._conversation_id = conversation_id
154
+ self._model = model
155
+
156
+ @property
157
+ def conversation_id(self) -> Optional[str]:
158
+ return self._conversation_id
159
+
160
+ def send(self, prompt: str) -> Iterator[AgentEvent]:
161
+ for event in self._graff.chat(prompt, conversation_id=self._conversation_id, model=self._model):
162
+ if event.type == "ConversationStarted" and not self._conversation_id:
163
+ self._conversation_id = event.data.get("conversationId")
164
+ yield event
165
+
166
+
167
+ # -- Sandbox (thin HTTP client hitting the gateway) --
168
+
169
+ import urllib.request
170
+ import urllib.error
171
+
172
+ _GATEWAY_URL = os.environ.get("CODEGRAFF_GATEWAY_URL", "https://gateway.codegraff.com")
173
+
174
+
175
+ def _gateway_fetch(api_key: str, method: str, path: str, body: Optional[dict] = None) -> Any:
176
+ url = f"{_GATEWAY_URL}{path}"
177
+ data = json.dumps(body).encode() if body is not None else None
178
+ req = urllib.request.Request(
179
+ url,
180
+ data=data,
181
+ method=method,
182
+ headers={
183
+ "Authorization": f"Bearer {api_key}",
184
+ "Content-Type": "application/json",
185
+ "User-Agent": "codegraff-python-sdk/0.1.0",
186
+ },
187
+ )
188
+ try:
189
+ with urllib.request.urlopen(req) as resp:
190
+ ct = resp.headers.get("Content-Type", "")
191
+ raw = resp.read()
192
+ if "application/json" in ct:
193
+ return json.loads(raw)
194
+ return raw.decode()
195
+ except urllib.error.HTTPError as e:
196
+ body_text = e.read().decode()[:300] if e.fp else ""
197
+ raise RuntimeError(f"Gateway {method} {path} failed ({e.code}): {body_text}") from e
198
+
199
+
200
+ def _resolve_api_key() -> str:
201
+ for var in ("CODEGRAFF_API_KEY", "CG_API_KEY"):
202
+ val = os.environ.get(var, "")
203
+ if val:
204
+ return val
205
+ return ""
206
+
207
+
208
+ class Sandbox:
209
+ """Cloud sandbox managed through the codegraff gateway."""
210
+
211
+ def __init__(self, graff: Graff, id: str):
212
+ self._graff = graff
213
+ self._api_key = _resolve_api_key()
214
+ self.id = id
215
+
216
+ @staticmethod
217
+ def create(
218
+ graff: Graff,
219
+ *,
220
+ language: str = "javascript",
221
+ auto_stop_minutes: int = 30,
222
+ labels: Optional[dict[str, str]] = None,
223
+ ) -> "Sandbox":
224
+ api_key = _resolve_api_key()
225
+ data = _gateway_fetch(api_key, "POST", "/v1/sandboxes", {
226
+ "language": language,
227
+ "autoStopMinutes": auto_stop_minutes,
228
+ "labels": labels or {},
229
+ })
230
+ return Sandbox(graff, data["id"])
231
+
232
+ @staticmethod
233
+ def get(graff: Graff, id: str) -> "Sandbox":
234
+ api_key = _resolve_api_key()
235
+ _gateway_fetch(api_key, "GET", f"/v1/sandboxes/{id}")
236
+ return Sandbox(graff, id)
237
+
238
+ @staticmethod
239
+ def list(graff: Graff) -> list[dict]:
240
+ api_key = _resolve_api_key()
241
+ return _gateway_fetch(api_key, "GET", "/v1/sandboxes")
242
+
243
+ def info(self) -> dict:
244
+ return _gateway_fetch(self._api_key, "GET", f"/v1/sandboxes/{self.id}")
245
+
246
+ def exec(self, command: str, *, cwd: Optional[str] = None, env: Optional[dict] = None, timeout_seconds: int = 300) -> dict:
247
+ return _gateway_fetch(self._api_key, "POST", f"/v1/sandboxes/{self.id}/exec", {
248
+ "command": command, "cwd": cwd, "env": env, "timeoutSeconds": timeout_seconds,
249
+ })
250
+
251
+ def upload(self, content: bytes | str, dest_path: str) -> None:
252
+ import base64
253
+ b = content if isinstance(content, bytes) else content.encode()
254
+ _gateway_fetch(self._api_key, "POST", f"/v1/sandboxes/{self.id}/upload", {
255
+ "path": dest_path, "contentBase64": base64.b64encode(b).decode(),
256
+ })
257
+
258
+ def download(self, path: str) -> bytes:
259
+ import base64
260
+ data = _gateway_fetch(self._api_key, "POST", f"/v1/sandboxes/{self.id}/download", {"path": path})
261
+ return base64.b64decode(data["contentBase64"])
262
+
263
+ def stop(self) -> dict:
264
+ return _gateway_fetch(self._api_key, "POST", f"/v1/sandboxes/{self.id}/stop")
265
+
266
+ def start(self) -> dict:
267
+ return _gateway_fetch(self._api_key, "POST", f"/v1/sandboxes/{self.id}/start")
268
+
269
+ def destroy(self) -> None:
270
+ _gateway_fetch(self._api_key, "DELETE", f"/v1/sandboxes/{self.id}")
Binary file
@@ -0,0 +1,81 @@
1
+ """dhi-backed input validation for the codegraff Python SDK.
2
+
3
+ Mirrors the TS SDK's ``validation.js``. dhi (https://github.com/justrach/dhi) is
4
+ a Pydantic-compatible validator. Validation is **advisory**: if dhi can't be
5
+ imported, we fall back to a minimal check so validation never becomes a *new*
6
+ hard failure. We only read the pass/fail result — the original option values
7
+ reach the native layer unchanged (no coercion, no mutation).
8
+
9
+ dhi ships native wheels for cp39-cp314 (incl. free-threaded cp313t/cp314t). The
10
+ native build (>=1.3.3) matches Pydantic on the type checks we use and, on a
11
+ free-threaded interpreter, no longer re-enables the GIL. A pure-Python fallback
12
+ is only used where no native wheel exists (e.g. Windows) and is looser on
13
+ ``Optional[...]`` fields.
14
+ """
15
+
16
+ # Keep these annotations as real objects (no `from __future__ import annotations`).
17
+ # dhi's old pure-Python fallback silently skipped validation when annotations were
18
+ # stringized; native dhi (>=1.3.2) resolves them, but there's no reason to add the
19
+ # future import here.
20
+ import warnings
21
+ from typing import Optional
22
+
23
+ _models = None
24
+ _warned = False
25
+
26
+
27
+ def _build():
28
+ from dhi import BaseModel # lazy: SDK still imports if dhi is absent
29
+
30
+ class InitOptions(BaseModel):
31
+ cwd: Optional[str] = None
32
+ provider: Optional[str] = None
33
+ api_key: Optional[str] = None
34
+ model: Optional[str] = None
35
+ max_tokens: Optional[int] = None
36
+
37
+ class ChatOptions(BaseModel):
38
+ prompt: str
39
+ conversation_id: Optional[str] = None
40
+ model: Optional[str] = None
41
+
42
+ return {"init": InitOptions, "chat": ChatOptions}
43
+
44
+
45
+ def _get():
46
+ global _models
47
+ if _models is None:
48
+ _models = _build()
49
+ return _models
50
+
51
+
52
+ def _fallback(kind: str, opts: dict) -> None:
53
+ if kind == "chat" and not isinstance(opts.get("prompt"), str):
54
+ raise TypeError("codegraff chat: `prompt` must be a string")
55
+ if kind == "init":
56
+ ak = opts.get("api_key")
57
+ if ak is not None and not isinstance(ak, str):
58
+ raise TypeError("codegraff init: `api_key` must be a string")
59
+
60
+
61
+ def validate(kind: str, opts: dict) -> None:
62
+ """Validate public options for `kind` in ('init', 'chat').
63
+
64
+ Raises ValueError on invalid input; returns None on success. Never mutates
65
+ `opts`.
66
+ """
67
+ global _warned
68
+ try:
69
+ models = _get()
70
+ except Exception as e: # dhi missing / failed to import
71
+ if not _warned:
72
+ _warned = True
73
+ warnings.warn(f"codegraff: dhi validation unavailable ({e}); using basic checks")
74
+ return _fallback(kind, opts)
75
+ model = models.get(kind)
76
+ if model is None:
77
+ return
78
+ try:
79
+ model(**opts)
80
+ except Exception as e:
81
+ raise ValueError(f"codegraff {kind}: invalid options — {e}") from None
codegraff/py.typed ADDED
File without changes
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: codegraff
3
+ Version: 0.1.1
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Topic :: Software Development :: Libraries
8
+ Requires-Dist: dhi>=1.3.3
9
+ Summary: Python SDK for the codegraff agent (PyO3 bindings).
10
+ Keywords: ai,agent,sandbox,codegraff
11
+ License: MIT
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
14
+ Project-URL: Homepage, https://codegraff.com
15
+ Project-URL: Repository, https://github.com/justrach/codegraff
16
+
17
+ # codegraff
18
+
19
+ The Python SDK for the [CodeGraff](https://github.com/justrach/codegraff) coding
20
+ agent. Run the agent **in-process** (PyO3 native bindings) — streaming events,
21
+ multi-turn sessions, BYOK auth, and cloud sandboxes — with synchronous
22
+ generators that fit straight into any Python app or web framework.
23
+
24
+ ```bash
25
+ pip install codegraff
26
+ ```
27
+
28
+ ```python
29
+ from codegraff import Graff
30
+
31
+ graff = Graff() # reads ~/.forge/forge.toml, like the graff CLI
32
+
33
+ for ev in graff.chat("explain monads in 3 sentences"):
34
+ if ev.type == "TaskMessage" and ev.data.get("content", {}).get("kind") == "Markdown":
35
+ print(ev.data["content"]["text"], end="", flush=True)
36
+ ```
37
+
38
+ ## BYOK
39
+
40
+ ```python
41
+ import os
42
+ from codegraff import Graff
43
+
44
+ graff = Graff(
45
+ provider="codegraff", # or "openai" / "anthropic" / "open_router" / "xai" / ...
46
+ api_key=os.environ["CODEGRAFF_API_KEY"],
47
+ model="deepseek-v4-pro",
48
+ )
49
+ ```
50
+
51
+ `Graff(...)` is a synchronous constructor; `chat()` is a blocking generator that
52
+ releases the GIL while it waits on the agent, so it drives async servers without
53
+ starving the loop. Inputs are validated at the boundary with
54
+ [dhi](https://github.com/justrach/dhi).
55
+
56
+ ## What you get
57
+
58
+ - **Streaming chat** — `for ev in graff.chat(prompt)` yields `AgentEvent`s
59
+ (`TaskMessage`, `ToolCallStart`/`End`, `TaskReasoning`, `TaskComplete`, …).
60
+ - **Multi-turn** — pass `conversation_id`, or use `graff.session()`.
61
+ - **Conversation management** — `list_conversations`, `get_conversation`,
62
+ `last_conversation`, `compact_conversation`, `delete_conversation`.
63
+ - **Agents & auth** — `get_agent_infos`, `upsert_credential`, `remove_credential`.
64
+ - **Cloud sandboxes** — `graff.create_sandbox()` → `exec` / `upload` / `download` /
65
+ `stop` / `start` / `destroy` (via the CodeGraff gateway).
66
+
67
+ ## Install notes
68
+
69
+ **Platform support (0.1.0):** prebuilt wheels are published **only for macOS
70
+ arm64** on Python **3.12 / 3.13 / 3.14t**, and there is **no sdist** — so
71
+ `pip install codegraff` fails on Linux, Windows, Intel macOS, or Python
72
+ 3.9–3.11 until the CI wheel matrix lands. To run elsewhere today, build from the
73
+ full repo (not `pip install` — the crate has workspace path deps):
74
+
75
+ ```bash
76
+ git clone https://github.com/justrach/codegraff
77
+ cd codegraff/sdk/python
78
+ pip install maturin && maturin develop --release
79
+ ```
80
+
81
+ ## Docs & examples
82
+
83
+ - **In-depth guide:** [docs/sdk/python.md](../../docs/sdk/python.md)
84
+ - **turboAPI HTTP example:** [`example/`](./example)
85
+ - **TypeScript SDK:** [`@codegraff/sdk`](../typescript) · [guide](../../docs/sdk/typescript.md)
86
+
87
+ Requires Python >= 3.9. MIT licensed.
88
+
@@ -0,0 +1,8 @@
1
+ codegraff/__init__.py,sha256=Nm0dtzrb5Qt-T91dA0Nbx4hNGuQpExotRvKBk2EUAYQ,9055
2
+ codegraff/_native.cp313-win_amd64.pyd,sha256=WymNF3K2YNDBeNUqQIZ2TKV6loux4o-SUxIJeJaESTc,40040960
3
+ codegraff/_validation.py,sha256=uWyK5LBeiNJ4eyOWZr8y2iNRJzWWp2IgsA7Twfb-wTM,2896
4
+ codegraff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ codegraff-0.1.1.dist-info/METADATA,sha256=3AYzPFm_cvgfo74s7EiNZecgXdItGaQdwU1QJ3301fk,3298
6
+ codegraff-0.1.1.dist-info/WHEEL,sha256=SOvIh6ndLyztvkrMKsamHag5pxo6YUrIipqSIMTC7-I,97
7
+ codegraff-0.1.1.dist-info/sboms/forge_sdk_python.cyclonedx.json,sha256=7_7xuVtJ9lEEO5yP8ckec-Sp7bOCT0V-OUi_OoOPVVs,758081
8
+ codegraff-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.13.3)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64