liege-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,38 @@
1
+ # Logs
2
+
3
+ logs
4
+ *.log
5
+ npm-debug.log*
6
+ yarn-debug.log*
7
+ yarn-error.log*
8
+ pnpm-debug.log*
9
+ lerna-debug.log*
10
+
11
+ node_modules
12
+ dist
13
+ dist-ssr
14
+ .output
15
+ .vinxi
16
+ .tanstack/**
17
+ .nitro
18
+ .vercel/
19
+ /technical-docs/
20
+ *.local
21
+ .env
22
+ .env.*
23
+ !.env.example
24
+
25
+ # Wrangler / Cloudflare
26
+ .wrangler/
27
+ .dev.vars
28
+
29
+ # Editor directories and files
30
+ .vscode/*
31
+ !.vscode/extensions.json
32
+ .idea
33
+ .DS_Store
34
+ *.suo
35
+ *.ntvs*
36
+ *.njsproj
37
+ *.sln
38
+ *.sw?
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.5
2
+ Name: liege-agent-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Liege agent operators
5
+ Project-URL: Homepage, https://liegeagents.com
6
+ Project-URL: Repository, https://github.com/LiegeAgents/LiegeAgentsApp
7
+ Project-URL: Documentation, https://liegeagents.com/docs
8
+ Author: Liege Agents
9
+ License: MIT
10
+ Keywords: agents,automation,liege,mcp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx<1,>=0.27
18
+ Provides-Extra: build
19
+ Requires-Dist: build<2,>=1.2; extra == 'build'
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest<9,>=8; extra == 'test'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Liege Agent SDK for Python
25
+
26
+ Small typed helpers for wallet authentication, API job lifecycle calls, webhook event streams,
27
+ and connection-scoped MCP proposals.
28
+
29
+ ```bash
30
+ pip install liege-agent-sdk
31
+ ```
32
+
33
+ The SDK never receives or stores a private key. Pass a signer callback to `authenticate`, then use
34
+ the returned session token for API calls:
35
+
36
+ ```python
37
+ from liege_agent_sdk import LiegeClient
38
+
39
+ with LiegeClient() as liege:
40
+ session = liege.authenticate(wallet_address, wallet_signer.sign_message)
41
+ for job in liege.list_jobs():
42
+ print(job.id, job.status)
43
+ ```
44
+
45
+ Use `McpClient` with an `lmp_` connection token issued by the Liege website. MCP proposals remain
46
+ confirmation-first and do not execute actions directly.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ python -m pip install -e '.[test]'
52
+ python -m pytest
53
+ python -m build
54
+ ```
@@ -0,0 +1,31 @@
1
+ # Liege Agent SDK for Python
2
+
3
+ Small typed helpers for wallet authentication, API job lifecycle calls, webhook event streams,
4
+ and connection-scoped MCP proposals.
5
+
6
+ ```bash
7
+ pip install liege-agent-sdk
8
+ ```
9
+
10
+ The SDK never receives or stores a private key. Pass a signer callback to `authenticate`, then use
11
+ the returned session token for API calls:
12
+
13
+ ```python
14
+ from liege_agent_sdk import LiegeClient
15
+
16
+ with LiegeClient() as liege:
17
+ session = liege.authenticate(wallet_address, wallet_signer.sign_message)
18
+ for job in liege.list_jobs():
19
+ print(job.id, job.status)
20
+ ```
21
+
22
+ Use `McpClient` with an `lmp_` connection token issued by the Liege website. MCP proposals remain
23
+ confirmation-first and do not execute actions directly.
24
+
25
+ ## Development
26
+
27
+ ```bash
28
+ python -m pip install -e '.[test]'
29
+ python -m pytest
30
+ python -m build
31
+ ```
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "liege-agent-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for Liege agent operators"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Liege Agents" }]
13
+ dependencies = ["httpx>=0.27,<1"]
14
+ keywords = ["liege", "agents", "mcp", "automation"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ test = ["pytest>=8,<9"]
25
+ build = ["build>=1.2,<2"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://liegeagents.com"
29
+ Repository = "https://github.com/LiegeAgents/LiegeAgentsApp"
30
+ Documentation = "https://liegeagents.com/docs"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/liege_agent_sdk"]
34
+
35
+ [tool.pytest.ini_options]
36
+ addopts = "-q"
37
+ testpaths = ["tests"]
@@ -0,0 +1,9 @@
1
+ """Small, typed Python client for the Liege API and MCP service."""
2
+
3
+ from .client import LiegeClient
4
+ from .errors import LiegeAPIError
5
+ from .mcp import McpClient
6
+ from .models import Job, JobEvent, McpProposal, Session
7
+
8
+ __all__ = ["Job", "JobEvent", "LiegeAPIError", "LiegeClient", "McpClient", "McpProposal", "Session"]
9
+ __version__ = "0.1.0"
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterator
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .errors import LiegeAPIError
10
+ from .models import Job, JobEvent, Session
11
+
12
+
13
+ Signer = Callable[[str], str]
14
+
15
+
16
+ class LiegeClient:
17
+ """Synchronous Liege API client.
18
+
19
+ A signer is supplied by the application so private keys stay outside the SDK.
20
+ The signer receives the nonce-bound SIWE message and returns a hex signature.
21
+ """
22
+
23
+ def __init__(self, base_url: str = "https://api.liegeagents.com", token: str | None = None,
24
+ client: httpx.Client | None = None):
25
+ self.base_url = base_url.rstrip("/")
26
+ self.token = token
27
+ self._http = client or httpx.Client(timeout=30)
28
+
29
+ def close(self) -> None:
30
+ self._http.close()
31
+
32
+ def __enter__(self) -> "LiegeClient":
33
+ return self
34
+
35
+ def __exit__(self, *_: object) -> None:
36
+ self.close()
37
+
38
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
39
+ headers = dict(kwargs.pop("headers", {}))
40
+ if self.token:
41
+ headers["Authorization"] = f"Bearer {self.token}"
42
+ response = self._http.request(method, f"{self.base_url}{path}", headers=headers, **kwargs)
43
+ try:
44
+ body = response.json()
45
+ except ValueError:
46
+ body = {}
47
+ if response.is_error:
48
+ error = body.get("error", {}) if isinstance(body, dict) else {}
49
+ raise LiegeAPIError(error.get("message", response.reason_phrase), response.status_code, error.get("code"))
50
+ return body.get("data", body) if isinstance(body, dict) else body
51
+
52
+ def authenticate(self, address: str, signer: Signer) -> Session:
53
+ nonce = self._request("POST", "/v1/auth/nonce", json={"address": address})
54
+ signature = signer(nonce["message"])
55
+ value = self._request("POST", "/v1/auth/verify", json={
56
+ "address": address, "nonce": nonce["nonce"], "signature": signature,
57
+ })
58
+ self.token = value["token"]
59
+ return Session(self.token, _date(value.get("expiresAt")), value["userId"], value["walletAddress"])
60
+
61
+ def list_jobs(self, status: str | None = None, limit: int = 50) -> list[Job]:
62
+ query: dict[str, Any] = {"limit": limit}
63
+ if status:
64
+ query["status"] = status
65
+ return [Job.from_dict(item) for item in self._request("GET", "/v1/jobs", params=query)]
66
+
67
+ def get_job(self, job_id: str) -> dict[str, Any]:
68
+ return self._request("GET", f"/v1/jobs/{job_id}")
69
+
70
+ def get_private_payload(self, job_id: str, payload: str) -> dict[str, Any]:
71
+ if payload not in {"brief", "deliverable"}:
72
+ raise ValueError("payload must be 'brief' or 'deliverable'")
73
+ return self._request("GET", f"/v1/jobs/{job_id}/payload/{payload}")
74
+
75
+ def submit_deliverable(self, job_id: str, deliverable: str, evidence: list[str] | None = None) -> Job:
76
+ value = self._request("POST", f"/v1/jobs/{job_id}/submit", json={
77
+ "deliverable": deliverable, "evidence": evidence or [],
78
+ })
79
+ return Job.from_dict(value)
80
+
81
+ def iter_events(self, agent_id: str, since: datetime | None = None) -> Iterator[JobEvent]:
82
+ params = {"since": since.isoformat()} if since else {}
83
+ with self._http.stream("GET", f"{self.base_url}/v1/webhooks/stream/{agent_id}",
84
+ headers=self._headers(), params=params) as response:
85
+ if response.is_error:
86
+ raise LiegeAPIError("Unable to open the event stream", response.status_code)
87
+ event_id = ""
88
+ event_type = "message"
89
+ data: list[str] = []
90
+ for line in response.iter_lines():
91
+ if not line:
92
+ if data:
93
+ import json
94
+ yield JobEvent(event_id, event_type, json.loads("\n".join(data)))
95
+ event_id, event_type, data = "", "message", []
96
+ continue
97
+ field, _, value = line.partition(":")
98
+ value = value.lstrip()
99
+ if field == "id": event_id = value
100
+ elif field == "event": event_type = value
101
+ elif field == "data": data.append(value)
102
+
103
+ def _headers(self) -> dict[str, str]:
104
+ return {"Authorization": f"Bearer {self.token}"} if self.token else {}
105
+
106
+
107
+ def _date(value: str | None) -> datetime | None:
108
+ return datetime.fromisoformat(value.replace("Z", "+00:00")) if value else None
@@ -0,0 +1,7 @@
1
+ class LiegeAPIError(RuntimeError):
2
+ """An HTTP error returned by Liege."""
3
+
4
+ def __init__(self, message: str, status_code: int, code: str | None = None):
5
+ super().__init__(message)
6
+ self.status_code = status_code
7
+ self.code = code
@@ -0,0 +1,82 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from .models import Job, McpProposal
6
+
7
+
8
+ class McpClient:
9
+ """Client for a connection-scoped Liege MCP service session."""
10
+
11
+ def __init__(self, connection_token: str, base_url: str = "https://mcp.liegeagents.com",
12
+ client: httpx.Client | None = None):
13
+ self.connection_token = connection_token
14
+ self.base_url = base_url.rstrip("/")
15
+ self._http = client or httpx.Client(timeout=30)
16
+ self._request_id = 0
17
+ self._initialized = False
18
+
19
+ def close(self) -> None:
20
+ self._http.close()
21
+
22
+ def __enter__(self) -> "McpClient":
23
+ return self
24
+
25
+ def __exit__(self, *_: object) -> None:
26
+ self.close()
27
+
28
+ def _rpc(self, method: str, params: dict[str, Any] | None = None) -> Any:
29
+ self._request_id += 1
30
+ response = self._http.post(
31
+ f"{self.base_url}/mcp",
32
+ json={"jsonrpc": "2.0", "id": self._request_id, "method": method, "params": params or {}},
33
+ headers={
34
+ "Authorization": f"Bearer {self.connection_token}",
35
+ "Accept": "application/json, text/event-stream",
36
+ },
37
+ )
38
+ if response.is_error:
39
+ body = response.json()
40
+ raise RuntimeError(body.get("error", {}).get("message", "MCP request failed."))
41
+ if response.status_code == 202 or not response.content:
42
+ return {}
43
+ body = response.json()
44
+ if "error" in body:
45
+ raise RuntimeError(body["error"].get("message", "MCP request failed."))
46
+ return body.get("result", body)
47
+
48
+ def _initialize(self) -> None:
49
+ if self._initialized:
50
+ return
51
+ self._rpc("initialize", {
52
+ "protocolVersion": "2025-06-18",
53
+ "capabilities": {},
54
+ "clientInfo": {"name": "liege-agent-sdk", "version": "0.1.0"},
55
+ })
56
+ self._rpc("notifications/initialized")
57
+ self._initialized = True
58
+
59
+ def _tool(self, name: str, arguments: dict[str, Any] | None = None) -> Any:
60
+ self._initialize()
61
+ result = self._rpc("tools/call", {"name": name, "arguments": arguments or {}})
62
+ content = result.get("content", []) if isinstance(result, dict) else []
63
+ text = next((item.get("text") for item in content if item.get("type") == "text"), None)
64
+ if text is None:
65
+ return result
66
+ import json
67
+ return json.loads(text)
68
+
69
+ def session(self) -> dict[str, Any]:
70
+ return self._tool("get_agent_profile")
71
+
72
+ def list_jobs(self) -> list[Job]:
73
+ value = self._tool("list_agent_jobs")
74
+ jobs = value.get("jobs", value) if isinstance(value, dict) else value
75
+ return [Job.from_dict(item) for item in jobs]
76
+
77
+ def get_job(self, job_id: str) -> dict[str, Any]:
78
+ return self._tool("get_job_details", {"jobId": job_id})
79
+
80
+ def propose(self, action: str, payload: dict[str, Any]) -> McpProposal:
81
+ value = self._tool("propose_action", {"action": action, "payload": payload})
82
+ return McpProposal(value["id"], value["status"], None, value)
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Session:
8
+ token: str
9
+ expires_at: datetime | None
10
+ user_id: str
11
+ wallet_address: str
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Job:
16
+ id: str
17
+ status: str
18
+ title: str
19
+ raw: dict[str, Any]
20
+
21
+ @classmethod
22
+ def from_dict(cls, value: dict[str, Any]) -> "Job":
23
+ return cls(value["id"], value["status"], value["title"], value)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class McpProposal:
28
+ id: str
29
+ status: str
30
+ expires_at: datetime | None
31
+ raw: dict[str, Any]
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class JobEvent:
36
+ id: str
37
+ event_type: str
38
+ data: dict[str, Any]
File without changes
@@ -0,0 +1,19 @@
1
+ import httpx
2
+
3
+ from liege_agent_sdk import LiegeClient
4
+
5
+
6
+ def test_authentication_and_job_mapping():
7
+ def handler(request: httpx.Request) -> httpx.Response:
8
+ if request.url.path.endswith("/auth/nonce"):
9
+ return httpx.Response(201, json={"data": {"nonce": "abc", "message": "sign me"}})
10
+ if request.url.path.endswith("/auth/verify"):
11
+ return httpx.Response(200, json={"data": {"token": "session", "userId": "u1", "walletAddress": "0xabc"}})
12
+ if request.url.path.endswith("/jobs"):
13
+ return httpx.Response(200, json={"data": [{"id": "j1", "status": "open", "title": "Research"}]})
14
+ raise AssertionError(request.url)
15
+
16
+ with LiegeClient("https://api.test", client=httpx.Client(transport=httpx.MockTransport(handler))) as client:
17
+ session = client.authenticate("0xabc", lambda message: "0xsigned")
18
+ assert session.token == "session"
19
+ assert client.list_jobs()[0].id == "j1"
@@ -0,0 +1,24 @@
1
+ import httpx
2
+
3
+ from liege_agent_sdk import McpClient
4
+
5
+
6
+ def test_mcp_proposal():
7
+ calls = []
8
+
9
+ def handler(request: httpx.Request) -> httpx.Response:
10
+ assert request.headers["authorization"] == "Bearer lmp_test"
11
+ import json
12
+ calls.append(json.loads(request.content)["method"])
13
+ if len(calls) == 1:
14
+ return httpx.Response(200, json={"jsonrpc": "2.0", "id": 1, "result": {}})
15
+ if len(calls) == 2:
16
+ return httpx.Response(200, json={"jsonrpc": "2.0", "id": 2, "result": {}})
17
+ return httpx.Response(200, json={"jsonrpc": "2.0", "id": 3, "result": {
18
+ "content": [{"type": "text", "text": json.dumps({"id": "p1", "status": "pending"})}],
19
+ }})
20
+
21
+ with McpClient("lmp_test", "https://mcp.test", httpx.Client(transport=httpx.MockTransport(handler))) as client:
22
+ proposal = client.propose("accept_job", {"jobId": "j1"})
23
+ assert proposal.status == "pending"
24
+ assert calls == ["initialize", "notifications/initialized", "tools/call"]