golem-agent-sdk 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.
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.3
2
+ Name: golem-agent-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Golem extensions
5
+ Author: Nawaz Gafar
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # golem-sdk
10
+
11
+ Python SDK for [Golem](https://github.com/terracotta4u/golem) extensions (channels, providers, and other long-running processes).
12
+
13
+ Install from PyPI:
14
+
15
+ ```sh
16
+ uv add golem-agent-sdk
17
+ ```
18
+
19
+ Or from this directory:
20
+
21
+ ```sh
22
+ uv sync --dev
23
+ ```
24
+
25
+ Import as `golem`. The wire protocol lives in Golem: [docs/extensions.md](https://github.com/terracotta4u/golem/blob/main/docs/extensions.md).
26
+
27
+ ```python
28
+ from golem import Client, Extension, Provider
29
+
30
+ client = Client.from_env()
31
+ client.wait_ready()
32
+ text = client.send(conversation_id, "telegram", "hello")
33
+ ```
34
+
35
+ `post_turn` and `stream_turn` expose the same flow as SSE events (`log`, `done`, `error`).
36
+
37
+ A provider extension binds a loopback callback, registers, and heartbeats:
38
+
39
+ ```python
40
+ from golem import Extension, Message, Provider
41
+
42
+ class Echo(Provider):
43
+ def chat(self, model, messages, tools=None) -> Message:
44
+ return Message(role="assistant", content=messages[-1].content)
45
+
46
+ Extension.from_env("golem-echo").provider("echo", Echo()).run()
47
+ ```
48
+
49
+ `task()` runs your own loop (a channel). `capability()` registers kinds Golem does not call.
50
+
51
+ ## Develop
52
+
53
+ ```sh
54
+ uv sync --dev
55
+ uv run pytest
56
+ ```
@@ -0,0 +1,48 @@
1
+ # golem-sdk
2
+
3
+ Python SDK for [Golem](https://github.com/terracotta4u/golem) extensions (channels, providers, and other long-running processes).
4
+
5
+ Install from PyPI:
6
+
7
+ ```sh
8
+ uv add golem-agent-sdk
9
+ ```
10
+
11
+ Or from this directory:
12
+
13
+ ```sh
14
+ uv sync --dev
15
+ ```
16
+
17
+ Import as `golem`. The wire protocol lives in Golem: [docs/extensions.md](https://github.com/terracotta4u/golem/blob/main/docs/extensions.md).
18
+
19
+ ```python
20
+ from golem import Client, Extension, Provider
21
+
22
+ client = Client.from_env()
23
+ client.wait_ready()
24
+ text = client.send(conversation_id, "telegram", "hello")
25
+ ```
26
+
27
+ `post_turn` and `stream_turn` expose the same flow as SSE events (`log`, `done`, `error`).
28
+
29
+ A provider extension binds a loopback callback, registers, and heartbeats:
30
+
31
+ ```python
32
+ from golem import Extension, Message, Provider
33
+
34
+ class Echo(Provider):
35
+ def chat(self, model, messages, tools=None) -> Message:
36
+ return Message(role="assistant", content=messages[-1].content)
37
+
38
+ Extension.from_env("golem-echo").provider("echo", Echo()).run()
39
+ ```
40
+
41
+ `task()` runs your own loop (a channel). `capability()` registers kinds Golem does not call.
42
+
43
+ ## Develop
44
+
45
+ ```sh
46
+ uv sync --dev
47
+ uv run pytest
48
+ ```
@@ -0,0 +1,28 @@
1
+ from golem.client import Client, GolemError, TurnEvent
2
+ from golem.extension import Extension
3
+ from golem.provider import (
4
+ FunctionCall,
5
+ JSONSchema,
6
+ Message,
7
+ Provider,
8
+ ToolCall,
9
+ ToolDef,
10
+ UnsupportedFormat,
11
+ )
12
+
13
+ __all__ = [
14
+ "Client",
15
+ "Extension",
16
+ "FunctionCall",
17
+ "GolemError",
18
+ "JSONSchema",
19
+ "Message",
20
+ "Provider",
21
+ "ToolCall",
22
+ "ToolDef",
23
+ "TurnEvent",
24
+ "UnsupportedFormat",
25
+ "__version__",
26
+ ]
27
+
28
+ __version__ = "0.1.0"
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ import urllib.error
7
+ import urllib.request
8
+ from dataclasses import dataclass
9
+ from typing import Any, Iterator
10
+
11
+
12
+ class GolemError(Exception):
13
+ def __init__(self, message: str, status: int | None = None) -> None:
14
+ super().__init__(message)
15
+ self.status = status
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class TurnEvent:
20
+ name: str
21
+ data: dict[str, Any]
22
+
23
+
24
+ class Client:
25
+ def __init__(self, url: str, token: str = "") -> None:
26
+ self.url = url.rstrip("/")
27
+ self.token = token
28
+
29
+ @classmethod
30
+ def from_env(cls) -> Client:
31
+ url = os.environ.get("GOLEM_URL", "").strip() or "http://127.0.0.1:8743"
32
+ token = os.environ.get("GOLEM_TOKEN", "").strip()
33
+ return cls(url, token)
34
+
35
+ def wait_ready(self, timeout: float = 30.0) -> None:
36
+ deadline = time.monotonic() + timeout
37
+ last: Exception | None = None
38
+ while time.monotonic() < deadline:
39
+ try:
40
+ body = self._request("GET", "/v1/health")
41
+ if body.get("ok"):
42
+ return
43
+ last = GolemError("golem health not ok")
44
+ except Exception as exc:
45
+ last = exc
46
+ time.sleep(0.4)
47
+ if last is None:
48
+ last = GolemError("golem not ready")
49
+ raise GolemError(f"golem not ready: {last}") from last
50
+
51
+ def post_turn(self, conversation_id: str, channel: str, text: str) -> str:
52
+ accepted = self._request(
53
+ "POST",
54
+ f"/v1/conversations/{conversation_id}/turns",
55
+ {"channel": channel, "text": text},
56
+ )
57
+ if accepted.get("error"):
58
+ raise GolemError(accepted["error"])
59
+ turn_id = accepted.get("id")
60
+ if not turn_id:
61
+ raise GolemError("missing turn id")
62
+ return str(turn_id)
63
+
64
+ def stream_turn(self, turn_id: str) -> Iterator[TurnEvent]:
65
+ req = urllib.request.Request(
66
+ self.url + f"/v1/turns/{turn_id}",
67
+ headers=self._headers(),
68
+ method="GET",
69
+ )
70
+ try:
71
+ with urllib.request.urlopen(req, timeout=None) as resp:
72
+ for name, raw in _read_sse(resp):
73
+ data = json.loads(raw) if raw else {}
74
+ if not isinstance(data, dict):
75
+ raise GolemError(f"invalid event data: {raw}")
76
+ yield TurnEvent(name=name, data=data)
77
+ if name in ("done", "error"):
78
+ return
79
+ except urllib.error.HTTPError as exc:
80
+ raw = exc.read()
81
+ raise GolemError(
82
+ f"unexpected status {exc.code}: {raw.decode(errors='replace')}",
83
+ status=exc.code,
84
+ ) from exc
85
+ except urllib.error.URLError as exc:
86
+ raise GolemError(f"request failed: {exc.reason}") from exc
87
+ except json.JSONDecodeError as exc:
88
+ raise GolemError(f"invalid json: {exc}") from exc
89
+
90
+ def send(self, conversation_id: str, channel: str, text: str) -> str:
91
+ turn_id = self.post_turn(conversation_id, channel, text)
92
+ for ev in self.stream_turn(turn_id):
93
+ if ev.name == "done":
94
+ return str(ev.data.get("text") or "")
95
+ if ev.name == "error":
96
+ raise GolemError(str(ev.data.get("error") or "turn failed"))
97
+ raise GolemError("turn ended without done")
98
+
99
+ def register(self, name: str, callback_url: str, capabilities: list[dict[str, Any]]) -> None:
100
+ body = self._request(
101
+ "POST",
102
+ "/v1/extensions/register",
103
+ {"name": name, "callback_url": callback_url, "capabilities": capabilities},
104
+ )
105
+ if not isinstance(body, dict) or not body.get("ok"):
106
+ raise GolemError("register failed")
107
+
108
+ def heartbeat(self, name: str) -> None:
109
+ body = self._request("POST", "/v1/extensions/heartbeat", {"name": name})
110
+ if not isinstance(body, dict) or not body.get("ok"):
111
+ raise GolemError("heartbeat failed")
112
+
113
+ def _headers(self, body: dict[str, Any] | None = None) -> dict[str, str]:
114
+ headers: dict[str, str] = {}
115
+ if self.token:
116
+ headers["Authorization"] = "Bearer " + self.token
117
+ if body is not None:
118
+ headers["Content-Type"] = "application/json"
119
+ return headers
120
+
121
+ def _request(self, method: str, path: str, body: dict[str, Any] | None = None) -> Any:
122
+ data = None
123
+ headers = self._headers(body)
124
+ if body is not None:
125
+ data = json.dumps(body).encode()
126
+ req = urllib.request.Request(
127
+ self.url + path,
128
+ data=data,
129
+ headers=headers,
130
+ method=method,
131
+ )
132
+ try:
133
+ with urllib.request.urlopen(req, timeout=30) as resp:
134
+ raw = resp.read()
135
+ except urllib.error.HTTPError as exc:
136
+ raw = exc.read()
137
+ raise GolemError(
138
+ f"unexpected status {exc.code}: {raw.decode(errors='replace')}",
139
+ status=exc.code,
140
+ ) from exc
141
+ except urllib.error.URLError as exc:
142
+ raise GolemError(f"request failed: {exc.reason}") from exc
143
+ if not raw:
144
+ return {}
145
+ try:
146
+ return json.loads(raw)
147
+ except json.JSONDecodeError as exc:
148
+ raise GolemError(f"invalid json: {raw.decode(errors='replace')}") from exc
149
+
150
+
151
+ def _read_sse(resp: Any) -> Iterator[tuple[str, str]]:
152
+ event = "message"
153
+ data: list[str] = []
154
+ while True:
155
+ line = resp.readline()
156
+ if not line:
157
+ break
158
+ text = line.decode("utf-8", errors="replace").rstrip("\r\n")
159
+ if text == "":
160
+ if data:
161
+ yield event, "\n".join(data)
162
+ event = "message"
163
+ data = []
164
+ continue
165
+ if text.startswith(":"):
166
+ continue
167
+ if text.startswith("event:"):
168
+ event = text[6:].lstrip()
169
+ elif text.startswith("data:"):
170
+ value = text[5:]
171
+ if value.startswith(" "):
172
+ value = value[1:]
173
+ data.append(value)
174
+ if data:
175
+ yield event, "\n".join(data)
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import signal
5
+ import sys
6
+ import threading
7
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
+ from types import FrameType
9
+ from typing import Any, Callable
10
+
11
+ from golem.client import Client, GolemError
12
+ from golem.provider import JSONSchema, Message, Provider, ToolDef, UnsupportedFormat
13
+
14
+ Task = Callable[[Client, threading.Event], None]
15
+
16
+
17
+ class Extension:
18
+ def __init__(
19
+ self,
20
+ name: str,
21
+ client: Client | str | None = None,
22
+ token: str = "",
23
+ *,
24
+ heartbeat_interval: float = 10.0,
25
+ ) -> None:
26
+ name = name.strip()
27
+ if not name:
28
+ raise ValueError("name is required")
29
+ self.name = name
30
+ if isinstance(client, Client):
31
+ self.client = client
32
+ elif isinstance(client, str):
33
+ self.client = Client(client, token)
34
+ else:
35
+ self.client = Client.from_env()
36
+ self.heartbeat_interval = heartbeat_interval
37
+ self._provider_id = ""
38
+ self._provider: Provider | None = None
39
+ self._tasks: list[Task] = []
40
+ self._extra_caps: list[dict[str, Any]] = []
41
+
42
+ @classmethod
43
+ def from_env(cls, name: str, **kwargs: Any) -> Extension:
44
+ return cls(name, Client.from_env(), **kwargs)
45
+
46
+ def provider(self, provider_id: str, impl: Provider) -> Extension:
47
+ provider_id = provider_id.strip()
48
+ if not provider_id:
49
+ raise ValueError("provider id is required")
50
+ if self._provider is not None:
51
+ raise ValueError("provider already set")
52
+ self._provider_id = provider_id
53
+ self._provider = impl
54
+ return self
55
+
56
+ def capability(self, cap: dict[str, Any]) -> Extension:
57
+ self._extra_caps.append(cap)
58
+ return self
59
+
60
+ def task(self, fn: Task) -> Extension:
61
+ self._tasks.append(fn)
62
+ return self
63
+
64
+ def run(self, stop: threading.Event | None = None) -> None:
65
+ own_stop = stop is None
66
+ if stop is None:
67
+ stop = threading.Event()
68
+ if threading.current_thread() is threading.main_thread():
69
+ def handle(_signum: int, _frame: FrameType | None) -> None:
70
+ stop.set()
71
+
72
+ signal.signal(signal.SIGINT, handle)
73
+ signal.signal(signal.SIGTERM, handle)
74
+
75
+ server = ThreadingHTTPServer(("127.0.0.1", 0), _handler(self))
76
+ callback = f"http://127.0.0.1:{server.server_address[1]}"
77
+ serving = threading.Thread(target=server.serve_forever, daemon=True)
78
+ serving.start()
79
+ try:
80
+ self._loop(callback, stop)
81
+ finally:
82
+ if own_stop:
83
+ stop.set()
84
+ server.shutdown()
85
+ serving.join(timeout=2)
86
+ server.server_close()
87
+
88
+ def _loop(self, callback: str, stop: threading.Event) -> None:
89
+ while not stop.is_set():
90
+ try:
91
+ self.client.wait_ready(timeout=5)
92
+ self._register(callback)
93
+ break
94
+ except GolemError as exc:
95
+ print(f"{self.name}: {exc}", file=sys.stderr, flush=True)
96
+ if stop.wait(2):
97
+ return
98
+ else:
99
+ return
100
+
101
+ print(f"{self.name}: {callback} → {self.client.url}", file=sys.stderr, flush=True)
102
+ threading.Thread(target=self._heartbeat_loop, args=(callback, stop), daemon=True).start()
103
+ for fn in self._tasks:
104
+ threading.Thread(target=self._run_task, args=(fn, stop), daemon=True).start()
105
+ stop.wait()
106
+
107
+ def _register(self, callback: str) -> None:
108
+ self.client.register(self.name, callback, self._capabilities())
109
+
110
+ def _heartbeat_loop(self, callback: str, stop: threading.Event) -> None:
111
+ while not stop.wait(self.heartbeat_interval):
112
+ try:
113
+ self.client.heartbeat(self.name)
114
+ except GolemError as exc:
115
+ if exc.status == 404:
116
+ try:
117
+ self._register(callback)
118
+ except GolemError:
119
+ pass
120
+
121
+ def _run_task(self, fn: Task, stop: threading.Event) -> None:
122
+ try:
123
+ fn(self.client, stop)
124
+ except Exception as exc:
125
+ print(f"{self.name} task: {exc}", file=sys.stderr, flush=True)
126
+
127
+ def _capabilities(self) -> list[dict[str, Any]]:
128
+ caps: list[dict[str, Any]] = []
129
+ if self._provider is not None:
130
+ cap: dict[str, Any] = {"kind": "provider", "id": self._provider_id, "chat": True}
131
+ if _overrides(self._provider, "chat_structured"):
132
+ cap["structured"] = True
133
+ if _overrides(self._provider, "embed"):
134
+ cap["embed"] = True
135
+ caps.append(cap)
136
+ caps.extend(self._extra_caps)
137
+ return caps
138
+
139
+ def _dispatch(self, path: str, body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
140
+ if self._provider is None:
141
+ return 404, _error("not found")
142
+ model = str(body.get("model") or "")
143
+ try:
144
+ if path == "/v1/chat":
145
+ msg = self._provider.chat(model, _messages(body), _tools(body))
146
+ return 200, msg.to_dict()
147
+ if path == "/v1/chat/structured":
148
+ raw = body.get("schema") if isinstance(body.get("schema"), dict) else {}
149
+ data = self._provider.chat_structured(model, _messages(body), JSONSchema.from_dict(raw))
150
+ return 200, {"data": data}
151
+ if path == "/v1/embed":
152
+ texts = body.get("texts") or []
153
+ if not isinstance(texts, list):
154
+ return 400, _error("texts must be a list")
155
+ vectors = self._provider.embed(model, [str(t) for t in texts])
156
+ return 200, {"vectors": vectors}
157
+ except UnsupportedFormat as exc:
158
+ return 400, {"error": {"code": "unsupported_format", "message": str(exc) or "no json schema"}}
159
+ except NotImplementedError:
160
+ return 404, _error("not found")
161
+ except Exception as exc:
162
+ return 500, _error(str(exc))
163
+ return 404, _error("not found")
164
+
165
+
166
+ def _overrides(provider: Provider, method: str) -> bool:
167
+ return getattr(type(provider), method) is not getattr(Provider, method)
168
+
169
+
170
+ def _messages(body: dict[str, Any]) -> list[Message]:
171
+ out: list[Message] = []
172
+ for raw in body.get("messages") or []:
173
+ if isinstance(raw, dict):
174
+ out.append(Message.from_dict(raw))
175
+ return out
176
+
177
+
178
+ def _tools(body: dict[str, Any]) -> list[ToolDef] | None:
179
+ raw = body.get("tools") or []
180
+ if not raw:
181
+ return None
182
+ return [ToolDef.from_dict(item) for item in raw if isinstance(item, dict)]
183
+
184
+
185
+ def _error(message: str) -> dict[str, Any]:
186
+ return {"error": {"message": message}}
187
+
188
+
189
+ def _handler(ext: Extension) -> type[BaseHTTPRequestHandler]:
190
+ class Handler(BaseHTTPRequestHandler):
191
+ protocol_version = "HTTP/1.1"
192
+
193
+ def log_message(self, format: str, *args: object) -> None:
194
+ return
195
+
196
+ def do_POST(self) -> None:
197
+ if not self._authorized():
198
+ self._write(401, _error("unauthorized"))
199
+ return
200
+ length = int(self.headers.get("Content-Length") or 0)
201
+ raw = self.rfile.read(length) if length else b""
202
+ if not raw:
203
+ body: dict[str, Any] = {}
204
+ else:
205
+ try:
206
+ parsed = json.loads(raw)
207
+ except json.JSONDecodeError:
208
+ self._write(400, _error("invalid json"))
209
+ return
210
+ if not isinstance(parsed, dict):
211
+ self._write(400, _error("invalid json"))
212
+ return
213
+ body = parsed
214
+ status, payload = ext._dispatch(self.path, body)
215
+ self._write(status, payload)
216
+
217
+ def do_GET(self) -> None:
218
+ if not self._authorized():
219
+ self._write(401, _error("unauthorized"))
220
+ return
221
+ self._write(404, _error("not found"))
222
+
223
+ def _authorized(self) -> bool:
224
+ token = ext.client.token
225
+ if not token:
226
+ return True
227
+ return self.headers.get("Authorization") == "Bearer " + token
228
+
229
+ def _write(self, status: int, payload: dict[str, Any]) -> None:
230
+ raw = json.dumps(payload).encode()
231
+ self.send_response(status)
232
+ self.send_header("Content-Type", "application/json")
233
+ self.send_header("Content-Length", str(len(raw)))
234
+ self.end_headers()
235
+ self.wfile.write(raw)
236
+
237
+ return Handler
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+
8
+ class UnsupportedFormat(Exception):
9
+ pass
10
+
11
+
12
+ @dataclass
13
+ class FunctionCall:
14
+ name: str
15
+ arguments: str = ""
16
+
17
+
18
+ @dataclass
19
+ class ToolCall:
20
+ id: str
21
+ type: str = "function"
22
+ function: FunctionCall = field(default_factory=lambda: FunctionCall(""))
23
+
24
+
25
+ @dataclass
26
+ class Message:
27
+ role: str
28
+ content: str = ""
29
+ tool_calls: list[ToolCall] = field(default_factory=list)
30
+ tool_call_id: str = ""
31
+
32
+ @classmethod
33
+ def from_dict(cls, data: dict[str, Any]) -> Message:
34
+ calls: list[ToolCall] = []
35
+ for raw in data.get("tool_calls") or []:
36
+ if not isinstance(raw, dict):
37
+ continue
38
+ fn = raw.get("function") if isinstance(raw.get("function"), dict) else {}
39
+ calls.append(
40
+ ToolCall(
41
+ id=str(raw.get("id") or ""),
42
+ type=str(raw.get("type") or "function"),
43
+ function=FunctionCall(
44
+ name=str(fn.get("name") or ""),
45
+ arguments=str(fn.get("arguments") or ""),
46
+ ),
47
+ )
48
+ )
49
+ return cls(
50
+ role=str(data.get("role") or ""),
51
+ content=str(data.get("content") or ""),
52
+ tool_calls=calls,
53
+ tool_call_id=str(data.get("tool_call_id") or ""),
54
+ )
55
+
56
+ def to_dict(self) -> dict[str, Any]:
57
+ out: dict[str, Any] = {"role": self.role}
58
+ if self.content:
59
+ out["content"] = self.content
60
+ if self.tool_calls:
61
+ out["tool_calls"] = [
62
+ {
63
+ "id": call.id,
64
+ "type": call.type,
65
+ "function": {"name": call.function.name, "arguments": call.function.arguments},
66
+ }
67
+ for call in self.tool_calls
68
+ ]
69
+ if self.tool_call_id:
70
+ out["tool_call_id"] = self.tool_call_id
71
+ return out
72
+
73
+
74
+ @dataclass
75
+ class ToolDef:
76
+ name: str
77
+ description: str = ""
78
+ parameters: dict[str, Any] = field(default_factory=dict)
79
+
80
+ @classmethod
81
+ def from_dict(cls, data: dict[str, Any]) -> ToolDef:
82
+ params = data.get("parameters")
83
+ return cls(
84
+ name=str(data.get("name") or ""),
85
+ description=str(data.get("description") or ""),
86
+ parameters=params if isinstance(params, dict) else {},
87
+ )
88
+
89
+
90
+ @dataclass
91
+ class JSONSchema:
92
+ name: str
93
+ strict: bool = False
94
+ schema: dict[str, Any] = field(default_factory=dict)
95
+
96
+ @classmethod
97
+ def from_dict(cls, data: dict[str, Any]) -> JSONSchema:
98
+ inner = data.get("schema")
99
+ return cls(
100
+ name=str(data.get("name") or ""),
101
+ strict=bool(data.get("strict")),
102
+ schema=inner if isinstance(inner, dict) else {},
103
+ )
104
+
105
+
106
+ class Provider(ABC):
107
+ @abstractmethod
108
+ def chat(self, model: str, messages: list[Message], tools: list[ToolDef] | None = None) -> Message:
109
+ raise NotImplementedError
110
+
111
+ def chat_structured(self, model: str, messages: list[Message], schema: JSONSchema) -> Any:
112
+ raise UnsupportedFormat("no json schema")
113
+
114
+ def embed(self, model: str, texts: list[str]) -> list[list[float]]:
115
+ raise NotImplementedError
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "golem-agent-sdk"
3
+ version = "0.1.0"
4
+ description = "Python SDK for Golem extensions"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = []
8
+
9
+ [[project.authors]]
10
+ name = "Nawaz Gafar"
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.12.5,<0.13.0"]
14
+ build-backend = "uv_build"
15
+
16
+ [tool.uv.build-backend]
17
+ module-root = ""
18
+ module-name = "golem"
19
+
20
+ [tool.pytest.ini_options]
21
+ pythonpath = ["."]
22
+ testpaths = ["tests"]
23
+
24
+ [dependency-groups]
25
+ dev = ["pytest>=9.1.1"]
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "golem-agent-sdk"
3
+ version = "0.1.0"
4
+ description = "Python SDK for Golem extensions"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Nawaz Gafar" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = []
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.12.5,<0.13.0"]
14
+ build-backend = "uv_build"
15
+
16
+ [tool.uv.build-backend]
17
+ module-root = ""
18
+ module-name = "golem"
19
+
20
+ [tool.pytest.ini_options]
21
+ pythonpath = ["."]
22
+ testpaths = ["tests"]
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "pytest>=9.1.1",
27
+ ]