lampson 0.2.10__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,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: lampson
3
+ Version: 0.2.10
4
+ Summary: Lampson SDK: talk to Lampson agents from Python — server mode (HTTP API) or local mode (Lampson as a child process).
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/kitecosmic/lampson
7
+ Keywords: agent,llm,lampson,sdk,mcp
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # lampson (Python SDK)
12
+
13
+ Talk to [Lampson](https://github.com/kitecosmic/lampson) agents from Python. Standard library only.
14
+
15
+ ```python
16
+ from lampson import Lampson
17
+
18
+ # server mode: a running Lampson, an API key from `lampson key add`
19
+ lampson = Lampson(url="https://my-lampson.com/w/<workspace>", key="lk_...")
20
+
21
+ # or local mode: Lampson as a child process on this machine (needs the `lampson` command)
22
+ lampson = Lampson(local={"workspace": "./my-project"})
23
+
24
+ r = lampson.ask(agent="shop", thread="customer-42", text="do you have whole-wheat bread?")
25
+ print(r["text"])
26
+
27
+ for ev in lampson.stream(agent="shop", thread="customer-42", text="and the price?"):
28
+ if ev["kind"] == "assistant":
29
+ print(ev["data"])
30
+
31
+ lampson.close()
32
+ ```
33
+
34
+ The same methods in both modes: `ask`, `stream`, `agents`, `job` (server), `close`, and
35
+ `Lampson.verify_signature(raw_body, signature, secret)` for callbacks.
@@ -0,0 +1,25 @@
1
+ # lampson (Python SDK)
2
+
3
+ Talk to [Lampson](https://github.com/kitecosmic/lampson) agents from Python. Standard library only.
4
+
5
+ ```python
6
+ from lampson import Lampson
7
+
8
+ # server mode: a running Lampson, an API key from `lampson key add`
9
+ lampson = Lampson(url="https://my-lampson.com/w/<workspace>", key="lk_...")
10
+
11
+ # or local mode: Lampson as a child process on this machine (needs the `lampson` command)
12
+ lampson = Lampson(local={"workspace": "./my-project"})
13
+
14
+ r = lampson.ask(agent="shop", thread="customer-42", text="do you have whole-wheat bread?")
15
+ print(r["text"])
16
+
17
+ for ev in lampson.stream(agent="shop", thread="customer-42", text="and the price?"):
18
+ if ev["kind"] == "assistant":
19
+ print(ev["data"])
20
+
21
+ lampson.close()
22
+ ```
23
+
24
+ The same methods in both modes: `ask`, `stream`, `agents`, `job` (server), `close`, and
25
+ `Lampson.verify_signature(raw_body, signature, secret)` for callbacks.
@@ -0,0 +1,292 @@
1
+ """lampson — the Lampson SDK for Python (3.9+, standard library only).
2
+
3
+ from lampson import Lampson
4
+
5
+ Two ways to reach Lampson, the same methods for both:
6
+
7
+ SERVER Lampson(url="https://my-lampson.com/w/<workspace>", key="lk_...")
8
+ talks to a running Lampson (the hub, a VPS, Docker) over its HTTP API /v1. Messages go through each
9
+ conversation's mailbox (queue, quiet window, mid-turn messages), exactly like a channel.
10
+ LOCAL Lampson(local={"workspace": "./my-project"})
11
+ starts Lampson ITSELF as a child process on this machine (`lampson run --stdio`): no hub, no port, no key.
12
+ Needs the `lampson` command (npm i -g lampson, or the installer). Requests are answered one at a time.
13
+
14
+ lampson.ask(agent="shop", thread="customer-42", text="do you have whole-wheat bread?")
15
+ -> {"agent", "thread", "session", "text", "steps", "usage", "stopped"} (raises LampsonError if the turn failed)
16
+ for ev in lampson.stream(agent="shop", thread="customer-42", text="and the price?"): ... # last one: kind "done"
17
+ lampson.agents() -> [{"id", "description"}]
18
+ lampson.close() # local mode: stops the child process
19
+ Lampson.verify_signature(raw_body, headers["X-Lampson-Signature"], secret) # for callbacks / webhooks
20
+
21
+ It is also a context manager: with Lampson(local={...}) as lampson: ...
22
+ Contracts behind it: spec/engine.md (envelope, events), lib/api.syn (HTTP), run.syn (--stdio).
23
+ """
24
+ import hashlib
25
+ import hmac
26
+ import json
27
+ import os
28
+ import shutil
29
+ import subprocess
30
+ import threading
31
+ import urllib.error
32
+ import urllib.request
33
+
34
+ __all__ = ["Lampson", "LampsonError"]
35
+ __version__ = "0.2.10"
36
+
37
+
38
+ class LampsonError(Exception):
39
+ """A failed turn, an HTTP error (status, body) or a local process that died (stderr)."""
40
+
41
+ def __init__(self, message, status=None, body=None, result=None, stderr=None):
42
+ super().__init__(message)
43
+ self.status = status
44
+ self.body = body
45
+ self.result = result
46
+ self.stderr = stderr
47
+
48
+
49
+ def _envelope(msg):
50
+ text = msg.get("text")
51
+ if not isinstance(text, str) or not text.strip():
52
+ raise TypeError("text is required")
53
+ env = {"text": text}
54
+ to = msg.get("agent") or msg.get("to")
55
+ if to:
56
+ env["to"] = to
57
+ for k in ("thread", "session", "images", "meta", "from", "callback", "mode"):
58
+ if msg.get(k) is not None:
59
+ env[k] = msg[k]
60
+ if msg.get("sent_at") is not None:
61
+ env["sent_at"] = msg["sent_at"]
62
+ return env
63
+
64
+
65
+ def _failed(result):
66
+ return isinstance(result.get("stopped"), str) and result["stopped"].startswith("error")
67
+
68
+
69
+ # ---------- server mode: HTTP API /v1 ----------
70
+
71
+ class _Http:
72
+ def __init__(self, url, key, timeout):
73
+ if not url:
74
+ raise TypeError('url is required (e.g. "http://127.0.0.1:8080/w/<workspace>")')
75
+ if not key:
76
+ raise TypeError("key is required (create one with `lampson key add`)")
77
+ base = url.rstrip("/")
78
+ if base.endswith("/v1"):
79
+ base = base[:-3]
80
+ self.base = base
81
+ self.key = key
82
+ self.timeout = timeout or 15 * 60
83
+
84
+ def _open(self, method, path, body=None, accept="application/json"):
85
+ data = None if body is None else json.dumps(body).encode("utf-8")
86
+ req = urllib.request.Request(self.base + "/v1" + path, data=data, method=method, headers={
87
+ "Authorization": "Bearer " + self.key, "Content-Type": "application/json", "Accept": accept})
88
+ try:
89
+ return urllib.request.urlopen(req, timeout=self.timeout)
90
+ except urllib.error.HTTPError as e:
91
+ raw = e.read().decode("utf-8", "replace")
92
+ try:
93
+ parsed = json.loads(raw)
94
+ except ValueError:
95
+ parsed = {"error": raw}
96
+ raise LampsonError(parsed.get("error") or "HTTP %d" % e.code, status=e.code, body=parsed) from None
97
+
98
+ def _json(self, method, path, body=None):
99
+ with self._open(method, path, body) as r:
100
+ raw = r.read().decode("utf-8")
101
+ return json.loads(raw) if raw else {}
102
+
103
+ def agents(self):
104
+ return self._json("GET", "/agents")["agents"]
105
+
106
+ def ask(self, env):
107
+ return self._json("POST", "/messages", env)
108
+
109
+ def job(self, job_id):
110
+ return self._json("GET", "/jobs/" + urllib.request.quote(job_id, safe=""))
111
+
112
+ def stream(self, env):
113
+ with self._open("POST", "/messages/stream", env, accept="text/event-stream") as r:
114
+ data_lines = []
115
+ for raw in r:
116
+ line = raw.decode("utf-8").rstrip("\r\n")
117
+ if line.startswith("data:"):
118
+ data_lines.append(line[5:].lstrip())
119
+ elif line == "" and data_lines:
120
+ try:
121
+ ev = json.loads("\n".join(data_lines))
122
+ except ValueError:
123
+ ev = None
124
+ data_lines = []
125
+ if ev is not None:
126
+ yield ev
127
+ if ev.get("kind") == "done":
128
+ return
129
+
130
+ def close(self):
131
+ pass
132
+
133
+
134
+ # ---------- local mode: `lampson run --stdio` as a child process ----------
135
+
136
+ def _lampson_command():
137
+ for name in ("lampson", "lampson.cmd"):
138
+ found = shutil.which(name)
139
+ if found:
140
+ return [found]
141
+ raise LampsonError("local mode needs the `lampson` command: npm i -g lampson (or pass local={'command': [...]})")
142
+
143
+
144
+ class _Local:
145
+ def __init__(self, workspace=None, home=None, env=None, command=None, spawn=None):
146
+ child_env = dict(os.environ)
147
+ child_env.update(env or {})
148
+ if home:
149
+ child_env["LAMPSON_HOME"] = home
150
+ if spawn:
151
+ # advanced / tests: an exact command that speaks the --stdio protocol
152
+ argv = [spawn["command"]] + list(spawn.get("args", []))
153
+ cwd = spawn.get("cwd")
154
+ else:
155
+ if not workspace:
156
+ raise TypeError("local['workspace'] is required (the project folder the agents work in)")
157
+ ws = os.path.abspath(workspace)
158
+ argv = (command or _lampson_command()) + ["run", "--stdio", "--workspace", ws]
159
+ cwd = ws
160
+ self.proc = subprocess.Popen(argv, cwd=cwd, env=child_env, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
161
+ stderr=subprocess.PIPE, text=True, encoding="utf-8", bufsize=1)
162
+ self.stderr = []
163
+ threading.Thread(target=self._drain_stderr, daemon=True).start()
164
+ self.lock = threading.Lock() # one request at a time: the protocol answers in order
165
+ self.seq = 0
166
+ self.info = None
167
+ while True:
168
+ m = self._read()
169
+ if m.get("kind") == "ready":
170
+ self.info = m.get("data")
171
+ break
172
+
173
+ def _drain_stderr(self):
174
+ for line in self.proc.stderr:
175
+ self.stderr.append(line.rstrip("\n"))
176
+ del self.stderr[:-40]
177
+
178
+ def _read(self):
179
+ while True:
180
+ line = self.proc.stdout.readline()
181
+ if line == "":
182
+ code = self.proc.wait()
183
+ raise LampsonError("lampson exited with code %s\n%s" % (code, "\n".join(self.stderr[-8:])), stderr=list(self.stderr))
184
+ t = line.strip()
185
+ if not t.startswith("{"):
186
+ continue # launcher notices ("lampson updated…") are not protocol
187
+ try:
188
+ return json.loads(t)
189
+ except ValueError:
190
+ continue
191
+
192
+ def _requests(self, req):
193
+ """Sends one request and yields its events until the closing one."""
194
+ if self.proc.poll() is not None:
195
+ raise LampsonError("lampson is not running")
196
+ with self.lock:
197
+ self.seq += 1
198
+ rid = "r%d" % self.seq
199
+ self.proc.stdin.write(json.dumps(dict(req, id=rid)) + "\n")
200
+ self.proc.stdin.flush()
201
+ while True:
202
+ m = self._read()
203
+ if m.get("id") != rid:
204
+ continue
205
+ yield m
206
+ if m.get("kind") in ("done", "agents", "pong", "error"):
207
+ return
208
+
209
+ def _final(self, req):
210
+ last = None
211
+ for m in self._requests(req):
212
+ last = m
213
+ return last
214
+
215
+ def agents(self):
216
+ return self._final({"op": "agents"})["data"]
217
+
218
+ def ask(self, env):
219
+ return self._final(dict(env, op="ask"))["data"]
220
+
221
+ def job(self, job_id):
222
+ raise LampsonError("jobs exist only in server mode (callbacks)")
223
+
224
+ def stream(self, env):
225
+ for m in self._requests(dict(env, op="ask")):
226
+ m = dict(m)
227
+ m.pop("id", None)
228
+ yield m
229
+
230
+ def close(self):
231
+ if self.proc.poll() is None:
232
+ try:
233
+ self.proc.stdin.close()
234
+ self.proc.wait(timeout=5)
235
+ except Exception:
236
+ self.proc.kill()
237
+
238
+
239
+ class Lampson:
240
+ """Server mode: Lampson(url=..., key=...). Local mode: Lampson(local={"workspace": ...})."""
241
+
242
+ def __init__(self, url=None, key=None, local=None, timeout=None):
243
+ if local is not None:
244
+ self._t = _Local(**local)
245
+ self.mode = "local"
246
+ else:
247
+ self._t = _Http(url, key, timeout)
248
+ self.mode = "server"
249
+
250
+ def ask(self, agent=None, text=None, **msg):
251
+ """Send a message to an agent and wait for its answer. Raises LampsonError if the turn ended in an error."""
252
+ result = self._t.ask(_envelope(dict(msg, agent=agent, text=text)))
253
+ if result.get("status") == "queued":
254
+ return result # server mode with callback: {"job", "status"}
255
+ if _failed(result):
256
+ raise LampsonError(result["stopped"], result=result)
257
+ return result
258
+
259
+ def stream(self, agent=None, text=None, **msg):
260
+ """The agent's steps as they happen: session, assistant, tool_call, tool_result…, and "done" last."""
261
+ env = _envelope(dict(msg, agent=agent, text=text))
262
+ if "callback" in env:
263
+ raise TypeError("stream does not take a callback (use ask)")
264
+ return self._t.stream(env)
265
+
266
+ def agents(self):
267
+ """The agents this key (server) or this workspace (local) can use."""
268
+ return self._t.agents()
269
+
270
+ def job(self, job_id):
271
+ """Server mode: status and result of a message sent with a callback."""
272
+ return self._t.job(job_id)
273
+
274
+ def close(self):
275
+ """Local mode: stops the Lampson child process. Harmless in server mode."""
276
+ self._t.close()
277
+
278
+ def __enter__(self):
279
+ return self
280
+
281
+ def __exit__(self, *exc):
282
+ self.close()
283
+ return False
284
+
285
+ @staticmethod
286
+ def verify_signature(raw_body, signature, secret):
287
+ """Checks X-Lampson-Signature: "sha256=" + HMAC-SHA256(raw_body, secret). Pass the raw body (str or bytes)."""
288
+ if not signature or not secret:
289
+ return False
290
+ body = raw_body.encode("utf-8") if isinstance(raw_body, str) else raw_body
291
+ expected = "sha256=" + hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
292
+ return hmac.compare_digest(expected, str(signature))
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: lampson
3
+ Version: 0.2.10
4
+ Summary: Lampson SDK: talk to Lampson agents from Python — server mode (HTTP API) or local mode (Lampson as a child process).
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/kitecosmic/lampson
7
+ Keywords: agent,llm,lampson,sdk,mcp
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # lampson (Python SDK)
12
+
13
+ Talk to [Lampson](https://github.com/kitecosmic/lampson) agents from Python. Standard library only.
14
+
15
+ ```python
16
+ from lampson import Lampson
17
+
18
+ # server mode: a running Lampson, an API key from `lampson key add`
19
+ lampson = Lampson(url="https://my-lampson.com/w/<workspace>", key="lk_...")
20
+
21
+ # or local mode: Lampson as a child process on this machine (needs the `lampson` command)
22
+ lampson = Lampson(local={"workspace": "./my-project"})
23
+
24
+ r = lampson.ask(agent="shop", thread="customer-42", text="do you have whole-wheat bread?")
25
+ print(r["text"])
26
+
27
+ for ev in lampson.stream(agent="shop", thread="customer-42", text="and the price?"):
28
+ if ev["kind"] == "assistant":
29
+ print(ev["data"])
30
+
31
+ lampson.close()
32
+ ```
33
+
34
+ The same methods in both modes: `ask`, `stream`, `agents`, `job` (server), `close`, and
35
+ `Lampson.verify_signature(raw_body, signature, secret)` for callbacks.
@@ -0,0 +1,7 @@
1
+ README.md
2
+ pyproject.toml
3
+ lampson/__init__.py
4
+ lampson.egg-info/PKG-INFO
5
+ lampson.egg-info/SOURCES.txt
6
+ lampson.egg-info/dependency_links.txt
7
+ lampson.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ lampson
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lampson"
7
+ version = "0.2.10"
8
+ description = "Lampson SDK: talk to Lampson agents from Python — server mode (HTTP API) or local mode (Lampson as a child process)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ dependencies = []
13
+ keywords = ["agent", "llm", "lampson", "sdk", "mcp"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/kitecosmic/lampson"
17
+
18
+ [tool.setuptools]
19
+ packages = ["lampson"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+