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.
- lampson-0.2.10/PKG-INFO +35 -0
- lampson-0.2.10/README.md +25 -0
- lampson-0.2.10/lampson/__init__.py +292 -0
- lampson-0.2.10/lampson.egg-info/PKG-INFO +35 -0
- lampson-0.2.10/lampson.egg-info/SOURCES.txt +7 -0
- lampson-0.2.10/lampson.egg-info/dependency_links.txt +1 -0
- lampson-0.2.10/lampson.egg-info/top_level.txt +1 -0
- lampson-0.2.10/pyproject.toml +19 -0
- lampson-0.2.10/setup.cfg +4 -0
lampson-0.2.10/PKG-INFO
ADDED
|
@@ -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.
|
lampson-0.2.10/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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"]
|
lampson-0.2.10/setup.cfg
ADDED