erabi-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.
- erabi_sdk-0.1.0/PKG-INFO +36 -0
- erabi_sdk-0.1.0/README.md +27 -0
- erabi_sdk-0.1.0/erabi_sdk/__init__.py +35 -0
- erabi_sdk-0.1.0/erabi_sdk/canonical.py +80 -0
- erabi_sdk-0.1.0/erabi_sdk/client.py +187 -0
- erabi_sdk-0.1.0/erabi_sdk/envelope.py +44 -0
- erabi_sdk-0.1.0/erabi_sdk/integrations/__init__.py +10 -0
- erabi_sdk-0.1.0/erabi_sdk/integrations/autogen.py +22 -0
- erabi_sdk-0.1.0/erabi_sdk/integrations/crewai.py +21 -0
- erabi_sdk-0.1.0/erabi_sdk/integrations/langchain.py +22 -0
- erabi_sdk-0.1.0/erabi_sdk/integrations/tools.py +89 -0
- erabi_sdk-0.1.0/erabi_sdk/keys.py +100 -0
- erabi_sdk-0.1.0/erabi_sdk.egg-info/PKG-INFO +36 -0
- erabi_sdk-0.1.0/erabi_sdk.egg-info/SOURCES.txt +17 -0
- erabi_sdk-0.1.0/erabi_sdk.egg-info/dependency_links.txt +1 -0
- erabi_sdk-0.1.0/erabi_sdk.egg-info/requires.txt +1 -0
- erabi_sdk-0.1.0/erabi_sdk.egg-info/top_level.txt +1 -0
- erabi_sdk-0.1.0/pyproject.toml +15 -0
- erabi_sdk-0.1.0/setup.cfg +4 -0
erabi_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: erabi-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Erabi Protocol SDK for Python agents: register, fire intents, report outcomes, get paid.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: cryptography>=42
|
|
9
|
+
|
|
10
|
+
# erabi-sdk
|
|
11
|
+
|
|
12
|
+
The Erabi Protocol SDK for Python agents: register, fire intents, report outcomes,
|
|
13
|
+
get paid — three lines to join the open intent exchange.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from erabi_sdk import Erabi
|
|
17
|
+
|
|
18
|
+
erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
|
|
19
|
+
choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
|
|
20
|
+
# later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every paid influence on the network is signed, labeled, and inspectable: sponsored
|
|
24
|
+
candidates arrive in a separate array, each with a publicly verifiable
|
|
25
|
+
`DisclosureRecord`. Organic results are never reordered by payment. Outcomes only
|
|
26
|
+
count when both parties sign them, and reputation is recomputable by anyone from
|
|
27
|
+
public evidence.
|
|
28
|
+
|
|
29
|
+
Signing is byte-for-byte compatible with the TypeScript SDK (Ed25519 over RFC 8785
|
|
30
|
+
canonical JSON), verified against frozen cross-SDK test vectors.
|
|
31
|
+
|
|
32
|
+
Framework bindings (LangChain, CrewAI, AutoGen) live in `erabi_sdk.integrations` and
|
|
33
|
+
need no framework imports of their own.
|
|
34
|
+
|
|
35
|
+
Spec, schemas, and the reference node: https://github.com/HMAKT99/Erabi
|
|
36
|
+
Apache-2.0.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# erabi-sdk
|
|
2
|
+
|
|
3
|
+
The Erabi Protocol SDK for Python agents: register, fire intents, report outcomes,
|
|
4
|
+
get paid — three lines to join the open intent exchange.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from erabi_sdk import Erabi
|
|
8
|
+
|
|
9
|
+
erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
|
|
10
|
+
choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
|
|
11
|
+
# later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Every paid influence on the network is signed, labeled, and inspectable: sponsored
|
|
15
|
+
candidates arrive in a separate array, each with a publicly verifiable
|
|
16
|
+
`DisclosureRecord`. Organic results are never reordered by payment. Outcomes only
|
|
17
|
+
count when both parties sign them, and reputation is recomputable by anyone from
|
|
18
|
+
public evidence.
|
|
19
|
+
|
|
20
|
+
Signing is byte-for-byte compatible with the TypeScript SDK (Ed25519 over RFC 8785
|
|
21
|
+
canonical JSON), verified against frozen cross-SDK test vectors.
|
|
22
|
+
|
|
23
|
+
Framework bindings (LangChain, CrewAI, AutoGen) live in `erabi_sdk.integrations` and
|
|
24
|
+
need no framework imports of their own.
|
|
25
|
+
|
|
26
|
+
Spec, schemas, and the reference node: https://github.com/HMAKT99/Erabi
|
|
27
|
+
Apache-2.0.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from .canonical import canonicalize
|
|
2
|
+
from .client import DEFAULT_ENDPOINTS, Erabi, ErabiError
|
|
3
|
+
from .envelope import create_envelope, sign_payload, signing_input
|
|
4
|
+
from .keys import (
|
|
5
|
+
AGENT_ID_PREFIX,
|
|
6
|
+
agent_id_from_public_key,
|
|
7
|
+
b58decode,
|
|
8
|
+
b58encode,
|
|
9
|
+
generate_seed,
|
|
10
|
+
public_key_from_seed,
|
|
11
|
+
public_key_to_string,
|
|
12
|
+
sign,
|
|
13
|
+
signature_to_string,
|
|
14
|
+
verify,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"canonicalize",
|
|
19
|
+
"DEFAULT_ENDPOINTS",
|
|
20
|
+
"Erabi",
|
|
21
|
+
"ErabiError",
|
|
22
|
+
"create_envelope",
|
|
23
|
+
"sign_payload",
|
|
24
|
+
"signing_input",
|
|
25
|
+
"AGENT_ID_PREFIX",
|
|
26
|
+
"agent_id_from_public_key",
|
|
27
|
+
"b58decode",
|
|
28
|
+
"b58encode",
|
|
29
|
+
"generate_seed",
|
|
30
|
+
"public_key_from_seed",
|
|
31
|
+
"public_key_to_string",
|
|
32
|
+
"sign",
|
|
33
|
+
"signature_to_string",
|
|
34
|
+
"verify",
|
|
35
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""RFC 8785 (JCS) canonical JSON, matching @erabi/crypto byte-for-byte.
|
|
2
|
+
|
|
3
|
+
Numbers follow ECMAScript Number→string semantics: shortest round-trip
|
|
4
|
+
digits, decimal notation for 1e-6 <= |x| < 1e21, exponential outside, no
|
|
5
|
+
zero-padded exponents. Values JSON cannot represent raise loudly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
__all__ = ["canonicalize", "format_number"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def format_number(value: float) -> str:
|
|
17
|
+
if isinstance(value, bool): # guard: bools are not numbers here
|
|
18
|
+
raise TypeError("bool passed to format_number")
|
|
19
|
+
if math.isnan(value) or math.isinf(value):
|
|
20
|
+
raise TypeError(f"canonicalize: non-finite number {value!r} is not valid JSON")
|
|
21
|
+
if value == 0:
|
|
22
|
+
return "0"
|
|
23
|
+
|
|
24
|
+
negative = value < 0
|
|
25
|
+
magnitude = abs(value)
|
|
26
|
+
|
|
27
|
+
if 1e-6 <= magnitude < 1e21:
|
|
28
|
+
# Decimal (positional) notation from shortest round-trip digits.
|
|
29
|
+
if float(magnitude).is_integer():
|
|
30
|
+
body = str(int(magnitude))
|
|
31
|
+
else:
|
|
32
|
+
body = format(Decimal(repr(magnitude)), "f")
|
|
33
|
+
body = body.rstrip("0").rstrip(".") if "." in body else body
|
|
34
|
+
return ("-" if negative else "") + body
|
|
35
|
+
|
|
36
|
+
# Exponential notation: normalized mantissa, no zero-padded exponent.
|
|
37
|
+
mantissa, _, exponent = repr(magnitude).partition("e")
|
|
38
|
+
if not exponent:
|
|
39
|
+
# repr produced positional form for an exponential-range value.
|
|
40
|
+
dec = Decimal(repr(magnitude)).normalize()
|
|
41
|
+
sign, digits, exp = dec.as_tuple()
|
|
42
|
+
digit_str = "".join(str(d) for d in digits)
|
|
43
|
+
e = exp + len(digit_str) - 1
|
|
44
|
+
mantissa = digit_str[0] + ("." + digit_str[1:] if len(digit_str) > 1 else "")
|
|
45
|
+
exponent = str(e)
|
|
46
|
+
exp_int = int(exponent)
|
|
47
|
+
mantissa = mantissa.rstrip("0").rstrip(".") if "." in mantissa else mantissa
|
|
48
|
+
exp_str = f"e+{exp_int}" if exp_int >= 0 else f"e-{abs(exp_int)}"
|
|
49
|
+
return ("-" if negative else "") + mantissa + exp_str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _serialize(value: Any) -> str:
|
|
53
|
+
if value is None:
|
|
54
|
+
return "null"
|
|
55
|
+
if value is True:
|
|
56
|
+
return "true"
|
|
57
|
+
if value is False:
|
|
58
|
+
return "false"
|
|
59
|
+
if isinstance(value, str):
|
|
60
|
+
return json.dumps(value, ensure_ascii=False)
|
|
61
|
+
if isinstance(value, int):
|
|
62
|
+
return str(value)
|
|
63
|
+
if isinstance(value, float):
|
|
64
|
+
return format_number(value)
|
|
65
|
+
if isinstance(value, (list, tuple)):
|
|
66
|
+
return "[" + ",".join(_serialize(item) for item in value) + "]"
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
members = []
|
|
69
|
+
# Sort by UTF-16 code units (RFC 8785 §3.2.3), not code points.
|
|
70
|
+
for key in sorted(value.keys(), key=lambda k: k.encode("utf-16-be")):
|
|
71
|
+
if not isinstance(key, str):
|
|
72
|
+
raise TypeError("canonicalize: object keys must be strings")
|
|
73
|
+
member = value[key]
|
|
74
|
+
members.append(json.dumps(key, ensure_ascii=False) + ":" + _serialize(member))
|
|
75
|
+
return "{" + ",".join(members) + "}"
|
|
76
|
+
raise TypeError(f"canonicalize: cannot canonicalize value of type {type(value).__name__}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def canonicalize(value: Any) -> str:
|
|
80
|
+
return _serialize(value)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Erabi Python SDK: the 3-line integration, mirroring @erabi/sdk.
|
|
2
|
+
|
|
3
|
+
from erabi_sdk import Erabi
|
|
4
|
+
erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
|
|
5
|
+
choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
|
|
6
|
+
# later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
import uuid
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from .canonical import canonicalize
|
|
18
|
+
from .envelope import create_envelope
|
|
19
|
+
from .keys import agent_id_from_public_key, generate_seed, public_key_from_seed, public_key_to_string
|
|
20
|
+
|
|
21
|
+
__all__ = ["Erabi", "ErabiError", "DEFAULT_ENDPOINTS"]
|
|
22
|
+
|
|
23
|
+
DEFAULT_ENDPOINTS = {
|
|
24
|
+
"registry": "http://localhost:4001",
|
|
25
|
+
"exchange": "http://localhost:4002",
|
|
26
|
+
"attribution": "http://localhost:4003",
|
|
27
|
+
"reputation": "http://localhost:4004",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ErabiError(Exception):
|
|
32
|
+
def __init__(self, status: int, code: str, message: str):
|
|
33
|
+
super().__init__(f"[{status} {code}] {message}")
|
|
34
|
+
self.status = status
|
|
35
|
+
self.code = code
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _request(method: str, url: str, body: Optional[Any] = None) -> Dict[str, Any]:
|
|
39
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
40
|
+
request = urllib.request.Request(
|
|
41
|
+
url, data=data, method=method, headers={"content-type": "application/json"}
|
|
42
|
+
)
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(request) as response:
|
|
45
|
+
text = response.read().decode("utf-8")
|
|
46
|
+
return json.loads(text) if text else {}
|
|
47
|
+
except urllib.error.HTTPError as err:
|
|
48
|
+
try:
|
|
49
|
+
payload = json.loads(err.read().decode("utf-8"))
|
|
50
|
+
error = payload.get("error", {})
|
|
51
|
+
raise ErabiError(err.code, error.get("code", "http_error"), error.get("message", str(err)))
|
|
52
|
+
except (ValueError, KeyError):
|
|
53
|
+
raise ErabiError(err.code, "http_error", str(err))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Erabi:
|
|
57
|
+
def __init__(self, seed: bytes, manifest: Dict[str, Any], endpoints: Dict[str, str], node_id: str):
|
|
58
|
+
self.seed = seed
|
|
59
|
+
self.manifest = manifest
|
|
60
|
+
self.id: str = manifest["id"]
|
|
61
|
+
self.endpoints = endpoints
|
|
62
|
+
self.node_id = node_id
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def register(
|
|
66
|
+
cls,
|
|
67
|
+
name: str,
|
|
68
|
+
capabilities: List[str],
|
|
69
|
+
endpoints: Optional[Dict[str, str]] = None,
|
|
70
|
+
endpoint: str = "https://example.invalid/agent",
|
|
71
|
+
owner_type: str = "individual",
|
|
72
|
+
verification: Optional[List[str]] = None,
|
|
73
|
+
payout_binding: Optional[str] = None,
|
|
74
|
+
accepts_sponsored: bool = False,
|
|
75
|
+
max_sponsored_ratio: float = 0.3,
|
|
76
|
+
human_in_loop: bool = True,
|
|
77
|
+
referrer: Optional[str] = None,
|
|
78
|
+
node_id: str = "erabi-sdk-py",
|
|
79
|
+
seed: Optional[bytes] = None,
|
|
80
|
+
) -> "Erabi":
|
|
81
|
+
merged = {**DEFAULT_ENDPOINTS, **(endpoints or {})}
|
|
82
|
+
seed = seed or generate_seed()
|
|
83
|
+
public_key = public_key_from_seed(seed)
|
|
84
|
+
agent_id = agent_id_from_public_key(public_key)
|
|
85
|
+
created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
86
|
+
manifest = {
|
|
87
|
+
"spec_version": "0.1",
|
|
88
|
+
"id": agent_id,
|
|
89
|
+
"name": name,
|
|
90
|
+
"public_key": public_key_to_string(public_key),
|
|
91
|
+
"owner": {
|
|
92
|
+
"type": owner_type,
|
|
93
|
+
"verification": verification or [],
|
|
94
|
+
"payout_binding": payout_binding,
|
|
95
|
+
},
|
|
96
|
+
"capabilities": capabilities,
|
|
97
|
+
"endpoint": endpoint,
|
|
98
|
+
"roles": ["consumer", "provider"],
|
|
99
|
+
"policy": {
|
|
100
|
+
"accepts_sponsored": accepts_sponsored,
|
|
101
|
+
"max_sponsored_ratio": max_sponsored_ratio,
|
|
102
|
+
"human_in_loop": human_in_loop,
|
|
103
|
+
},
|
|
104
|
+
"referrer": referrer,
|
|
105
|
+
"created_at": created,
|
|
106
|
+
}
|
|
107
|
+
client = cls(seed, manifest, merged, node_id)
|
|
108
|
+
_request("POST", f"{merged['registry']}/v1/agents", client.signed(manifest))
|
|
109
|
+
return client
|
|
110
|
+
|
|
111
|
+
def signed(self, payload: Any) -> Dict[str, Any]:
|
|
112
|
+
return create_envelope(payload, self.seed, self.id, self.node_id)
|
|
113
|
+
|
|
114
|
+
def intent(
|
|
115
|
+
self,
|
|
116
|
+
category: str,
|
|
117
|
+
query: Optional[str] = None,
|
|
118
|
+
constraints: Optional[Dict[str, Any]] = None,
|
|
119
|
+
human_in_loop: Optional[bool] = None,
|
|
120
|
+
context: Any = None,
|
|
121
|
+
ttl_ms: int = 3000,
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
if context is not None:
|
|
124
|
+
context_hash = "sha256:" + hashlib.sha256(canonicalize(context).encode()).hexdigest()
|
|
125
|
+
else:
|
|
126
|
+
context_hash = "sha256:" + hashlib.sha256(b"erabi:no-context").hexdigest()
|
|
127
|
+
intent = {
|
|
128
|
+
"intent_id": str(uuid.uuid4()),
|
|
129
|
+
"agent_id": self.id,
|
|
130
|
+
"category": category,
|
|
131
|
+
"query": query or category,
|
|
132
|
+
"constraints": constraints or {},
|
|
133
|
+
"context_hash": context_hash,
|
|
134
|
+
"human_in_loop": (
|
|
135
|
+
human_in_loop
|
|
136
|
+
if human_in_loop is not None
|
|
137
|
+
else self.manifest["policy"]["human_in_loop"]
|
|
138
|
+
),
|
|
139
|
+
"ttl_ms": ttl_ms,
|
|
140
|
+
}
|
|
141
|
+
return _request("POST", f"{self.endpoints['exchange']}/v1/intents", self.signed(intent))
|
|
142
|
+
|
|
143
|
+
def report_outcome(
|
|
144
|
+
self,
|
|
145
|
+
auction_id: str,
|
|
146
|
+
provider_id: str,
|
|
147
|
+
kind: str,
|
|
148
|
+
value_usd: float = 0.0,
|
|
149
|
+
rail_receipt: Optional[Dict[str, str]] = None,
|
|
150
|
+
) -> Dict[str, Any]:
|
|
151
|
+
return _request(
|
|
152
|
+
"POST",
|
|
153
|
+
f"{self.endpoints['attribution']}/v1/events",
|
|
154
|
+
self.signed(
|
|
155
|
+
{
|
|
156
|
+
"event_id": str(uuid.uuid4()),
|
|
157
|
+
"auction_id": auction_id,
|
|
158
|
+
"kind": kind,
|
|
159
|
+
"provider_id": provider_id,
|
|
160
|
+
"value_usd": value_usd,
|
|
161
|
+
"rail_receipt": rail_receipt,
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def confirm_outcome(self, event_id: str, event_hash: str) -> Dict[str, Any]:
|
|
167
|
+
return _request(
|
|
168
|
+
"POST",
|
|
169
|
+
f"{self.endpoints['attribution']}/v1/events/{event_id}/confirm",
|
|
170
|
+
self.signed({"event_id": event_id, "hash": event_hash}),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def discover(self, capability: str, limit: int = 10) -> Dict[str, Any]:
|
|
174
|
+
return _request(
|
|
175
|
+
"POST",
|
|
176
|
+
f"{self.endpoints['registry']}/v1/discover",
|
|
177
|
+
{"capability": capability, "limit": limit},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def my_reputation(self) -> Dict[str, Any]:
|
|
181
|
+
return _request("GET", f"{self.endpoints['reputation']}/v1/reputation/{self.id}")
|
|
182
|
+
|
|
183
|
+
def my_earnings(self) -> Dict[str, Any]:
|
|
184
|
+
return _request("GET", f"{self.endpoints['attribution']}/v1/earnings/{self.id}")
|
|
185
|
+
|
|
186
|
+
def feedback(self) -> Dict[str, Any]:
|
|
187
|
+
return _request("GET", f"{self.endpoints['attribution']}/v1/feedback/{self.id}")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Spec §6 envelopes: sig = ed25519(canonical_json(payload) || ts || nonce)."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from .canonical import canonicalize
|
|
8
|
+
from .keys import sign, signature_to_string
|
|
9
|
+
|
|
10
|
+
__all__ = ["signing_input", "sign_payload", "create_envelope"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def now_iso() -> str:
|
|
14
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.") + (
|
|
15
|
+
f"{datetime.now(timezone.utc).microsecond // 1000:03d}Z"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def signing_input(payload: Any, ts: str, nonce: str) -> bytes:
|
|
20
|
+
return (canonicalize(payload) + ts + nonce).encode("utf-8")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sign_payload(payload: Any, ts: str, nonce: str, seed: bytes) -> str:
|
|
24
|
+
return signature_to_string(sign(signing_input(payload, ts, nonce), seed))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_envelope(
|
|
28
|
+
payload: Any,
|
|
29
|
+
seed: bytes,
|
|
30
|
+
key_id: str,
|
|
31
|
+
node_id: str = "erabi-sdk-py",
|
|
32
|
+
ts: Optional[str] = None,
|
|
33
|
+
nonce: Optional[str] = None,
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
ts = ts or now_iso()
|
|
36
|
+
nonce = nonce or str(uuid.uuid4())
|
|
37
|
+
return {
|
|
38
|
+
"payload": payload,
|
|
39
|
+
"sig": sign_payload(payload, ts, nonce, seed),
|
|
40
|
+
"key_id": key_id,
|
|
41
|
+
"ts": ts,
|
|
42
|
+
"nonce": nonce,
|
|
43
|
+
"node_id": node_id,
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Framework bindings: thin wrappers over the Erabi SDK.
|
|
2
|
+
|
|
3
|
+
Each module exposes the same four operations (discover, intent,
|
|
4
|
+
report_outcome, my_reputation) in the shape its framework expects, without
|
|
5
|
+
importing the framework itself — the host application supplies it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .tools import TOOL_SPECS, build_callables
|
|
9
|
+
|
|
10
|
+
__all__ = ["TOOL_SPECS", "build_callables"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""AutoGen binding: register the Erabi callables on agents.
|
|
2
|
+
|
|
3
|
+
register_erabi_tools(erabi, caller=assistant, executor=user_proxy)
|
|
4
|
+
|
|
5
|
+
Uses the standard `register_for_llm` / `register_for_execution` pattern; the
|
|
6
|
+
framework objects are supplied by the host application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..client import Erabi
|
|
12
|
+
from .tools import TOOL_SPECS, build_callables
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_erabi_tools(client: Erabi, caller: Any, executor: Any) -> None:
|
|
16
|
+
callables = build_callables(client)
|
|
17
|
+
for spec in TOOL_SPECS:
|
|
18
|
+
func = callables[spec["name"]]
|
|
19
|
+
func.__name__ = spec["name"]
|
|
20
|
+
func.__doc__ = spec["description"]
|
|
21
|
+
caller.register_for_llm(name=spec["name"], description=spec["description"])(func)
|
|
22
|
+
executor.register_for_execution(name=spec["name"])(func)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""CrewAI binding: decorate the Erabi callables with crewai's @tool.
|
|
2
|
+
|
|
3
|
+
from crewai.tools import tool
|
|
4
|
+
tools = get_erabi_tools(erabi, tool)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, List
|
|
8
|
+
|
|
9
|
+
from ..client import Erabi
|
|
10
|
+
from .tools import TOOL_SPECS, build_callables
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_erabi_tools(client: Erabi, tool_decorator: Callable[..., Any]) -> List[Any]:
|
|
14
|
+
callables = build_callables(client)
|
|
15
|
+
tools = []
|
|
16
|
+
for spec in TOOL_SPECS:
|
|
17
|
+
func = callables[spec["name"]]
|
|
18
|
+
func.__name__ = spec["name"]
|
|
19
|
+
func.__doc__ = spec["description"]
|
|
20
|
+
tools.append(tool_decorator(spec["name"])(func))
|
|
21
|
+
return tools
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""LangChain binding: `get_erabi_tools(client, Tool)` → list of Tool.
|
|
2
|
+
|
|
3
|
+
from langchain_core.tools import Tool
|
|
4
|
+
tools = get_erabi_tools(erabi, Tool)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List
|
|
8
|
+
|
|
9
|
+
from ..client import Erabi
|
|
10
|
+
from .tools import TOOL_SPECS, build_callables
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_erabi_tools(client: Erabi, tool_class: Any) -> List[Any]:
|
|
14
|
+
callables = build_callables(client)
|
|
15
|
+
return [
|
|
16
|
+
tool_class(
|
|
17
|
+
name=spec["name"],
|
|
18
|
+
description=spec["description"],
|
|
19
|
+
func=callables[spec["name"]],
|
|
20
|
+
)
|
|
21
|
+
for spec in TOOL_SPECS
|
|
22
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Framework-neutral tool specs shared by all bindings."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Callable, Dict, List
|
|
5
|
+
|
|
6
|
+
from ..client import Erabi
|
|
7
|
+
|
|
8
|
+
TOOL_SPECS: List[Dict[str, Any]] = [
|
|
9
|
+
{
|
|
10
|
+
"name": "erabi_discover",
|
|
11
|
+
"description": (
|
|
12
|
+
"Find providers on the Erabi network for a capability, ranked by "
|
|
13
|
+
"reputation x freshness with public evidence trails."
|
|
14
|
+
),
|
|
15
|
+
"parameters": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"capability": {"type": "string"},
|
|
19
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
|
|
20
|
+
},
|
|
21
|
+
"required": ["capability"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "erabi_intent",
|
|
26
|
+
"description": (
|
|
27
|
+
"Fire a moment of choice: returns organic candidates plus clearly "
|
|
28
|
+
"labeled sponsored candidates with signed disclosures. Query must be PII-free."
|
|
29
|
+
),
|
|
30
|
+
"parameters": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"category": {"type": "string"},
|
|
34
|
+
"query": {"type": "string"},
|
|
35
|
+
"max_price_usd": {"type": "number"},
|
|
36
|
+
},
|
|
37
|
+
"required": ["category"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "erabi_report_outcome",
|
|
42
|
+
"description": (
|
|
43
|
+
"Report a signed outcome event (selection/conversion/task_success) for a "
|
|
44
|
+
"provider chosen from a consideration set."
|
|
45
|
+
),
|
|
46
|
+
"parameters": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"auction_id": {"type": "string"},
|
|
50
|
+
"provider_id": {"type": "string"},
|
|
51
|
+
"kind": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"enum": ["selection", "click", "conversion", "task_success", "assisted"],
|
|
54
|
+
},
|
|
55
|
+
"value_usd": {"type": "number"},
|
|
56
|
+
},
|
|
57
|
+
"required": ["auction_id", "provider_id", "kind"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "erabi_my_reputation",
|
|
62
|
+
"description": "This agent's reputation score and verifiable evidence trail.",
|
|
63
|
+
"parameters": {"type": "object", "properties": {}},
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_callables(client: Erabi) -> Dict[str, Callable[..., str]]:
|
|
69
|
+
"""Name → callable returning a JSON string, the lingua franca of tools."""
|
|
70
|
+
|
|
71
|
+
def discover(capability: str, limit: int = 10) -> str:
|
|
72
|
+
return json.dumps(client.discover(capability, limit))
|
|
73
|
+
|
|
74
|
+
def intent(category: str, query: str = "", max_price_usd: float = 0.0) -> str:
|
|
75
|
+
constraints = {"max_price_usd": max_price_usd} if max_price_usd else {}
|
|
76
|
+
return json.dumps(client.intent(category=category, query=query or None, constraints=constraints))
|
|
77
|
+
|
|
78
|
+
def report_outcome(auction_id: str, provider_id: str, kind: str, value_usd: float = 0.0) -> str:
|
|
79
|
+
return json.dumps(client.report_outcome(auction_id, provider_id, kind, value_usd))
|
|
80
|
+
|
|
81
|
+
def my_reputation() -> str:
|
|
82
|
+
return json.dumps(client.my_reputation())
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"erabi_discover": discover,
|
|
86
|
+
"erabi_intent": intent,
|
|
87
|
+
"erabi_report_outcome": report_outcome,
|
|
88
|
+
"erabi_my_reputation": my_reputation,
|
|
89
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Ed25519 identity: keypairs, base58 wire encodings, agent ids."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
7
|
+
Ed25519PrivateKey,
|
|
8
|
+
Ed25519PublicKey,
|
|
9
|
+
)
|
|
10
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
11
|
+
Encoding,
|
|
12
|
+
NoEncryption,
|
|
13
|
+
PrivateFormat,
|
|
14
|
+
PublicFormat,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AGENT_ID_PREFIX",
|
|
19
|
+
"b58encode",
|
|
20
|
+
"b58decode",
|
|
21
|
+
"generate_seed",
|
|
22
|
+
"public_key_from_seed",
|
|
23
|
+
"public_key_to_string",
|
|
24
|
+
"agent_id_from_public_key",
|
|
25
|
+
"sign",
|
|
26
|
+
"signature_to_string",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
AGENT_ID_PREFIX = "erabi:agent:"
|
|
30
|
+
_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def b58encode(data: bytes) -> str:
|
|
34
|
+
n = int.from_bytes(data, "big")
|
|
35
|
+
out = ""
|
|
36
|
+
while n > 0:
|
|
37
|
+
n, rem = divmod(n, 58)
|
|
38
|
+
out = _ALPHABET[rem] + out
|
|
39
|
+
pad = 0
|
|
40
|
+
for byte in data:
|
|
41
|
+
if byte == 0:
|
|
42
|
+
pad += 1
|
|
43
|
+
else:
|
|
44
|
+
break
|
|
45
|
+
return "1" * pad + out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def b58decode(text: str) -> bytes:
|
|
49
|
+
n = 0
|
|
50
|
+
for char in text:
|
|
51
|
+
n = n * 58 + _ALPHABET.index(char)
|
|
52
|
+
body = n.to_bytes((n.bit_length() + 7) // 8, "big") if n > 0 else b""
|
|
53
|
+
pad = 0
|
|
54
|
+
for char in text:
|
|
55
|
+
if char == "1":
|
|
56
|
+
pad += 1
|
|
57
|
+
else:
|
|
58
|
+
break
|
|
59
|
+
return b"\x00" * pad + body
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def generate_seed() -> bytes:
|
|
63
|
+
return os.urandom(32)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def public_key_from_seed(seed: bytes) -> bytes:
|
|
67
|
+
private = Ed25519PrivateKey.from_private_bytes(seed)
|
|
68
|
+
return private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def public_key_to_string(public_key: bytes) -> str:
|
|
72
|
+
return "ed25519:" + b58encode(public_key)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def agent_id_from_public_key(public_key: bytes) -> str:
|
|
76
|
+
return AGENT_ID_PREFIX + b58encode(public_key)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def sign(message: bytes, seed: bytes) -> bytes:
|
|
80
|
+
return Ed25519PrivateKey.from_private_bytes(seed).sign(message)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def signature_to_string(signature: bytes) -> str:
|
|
84
|
+
return "ed25519:" + b58encode(signature)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def keypair_from_seed(seed: bytes) -> Tuple[bytes, bytes]:
|
|
88
|
+
return seed, public_key_from_seed(seed)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def verify(message: bytes, signature: bytes, public_key: bytes) -> bool:
|
|
92
|
+
try:
|
|
93
|
+
Ed25519PublicKey.from_public_bytes(public_key).verify(signature, message)
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _private_bytes(private: Ed25519PrivateKey) -> bytes:
|
|
100
|
+
return private.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: erabi-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Erabi Protocol SDK for Python agents: register, fire intents, report outcomes, get paid.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: cryptography>=42
|
|
9
|
+
|
|
10
|
+
# erabi-sdk
|
|
11
|
+
|
|
12
|
+
The Erabi Protocol SDK for Python agents: register, fire intents, report outcomes,
|
|
13
|
+
get paid — three lines to join the open intent exchange.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from erabi_sdk import Erabi
|
|
17
|
+
|
|
18
|
+
erabi = Erabi.register(name="MyAgent", capabilities=["agent.research"])
|
|
19
|
+
choices = erabi.intent(category="data.financial", constraints={"max_price_usd": 1})
|
|
20
|
+
# later: erabi.report_outcome(choices["auction_id"], provider_id, "task_success")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every paid influence on the network is signed, labeled, and inspectable: sponsored
|
|
24
|
+
candidates arrive in a separate array, each with a publicly verifiable
|
|
25
|
+
`DisclosureRecord`. Organic results are never reordered by payment. Outcomes only
|
|
26
|
+
count when both parties sign them, and reputation is recomputable by anyone from
|
|
27
|
+
public evidence.
|
|
28
|
+
|
|
29
|
+
Signing is byte-for-byte compatible with the TypeScript SDK (Ed25519 over RFC 8785
|
|
30
|
+
canonical JSON), verified against frozen cross-SDK test vectors.
|
|
31
|
+
|
|
32
|
+
Framework bindings (LangChain, CrewAI, AutoGen) live in `erabi_sdk.integrations` and
|
|
33
|
+
need no framework imports of their own.
|
|
34
|
+
|
|
35
|
+
Spec, schemas, and the reference node: https://github.com/HMAKT99/Erabi
|
|
36
|
+
Apache-2.0.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
erabi_sdk/__init__.py
|
|
4
|
+
erabi_sdk/canonical.py
|
|
5
|
+
erabi_sdk/client.py
|
|
6
|
+
erabi_sdk/envelope.py
|
|
7
|
+
erabi_sdk/keys.py
|
|
8
|
+
erabi_sdk.egg-info/PKG-INFO
|
|
9
|
+
erabi_sdk.egg-info/SOURCES.txt
|
|
10
|
+
erabi_sdk.egg-info/dependency_links.txt
|
|
11
|
+
erabi_sdk.egg-info/requires.txt
|
|
12
|
+
erabi_sdk.egg-info/top_level.txt
|
|
13
|
+
erabi_sdk/integrations/__init__.py
|
|
14
|
+
erabi_sdk/integrations/autogen.py
|
|
15
|
+
erabi_sdk/integrations/crewai.py
|
|
16
|
+
erabi_sdk/integrations/langchain.py
|
|
17
|
+
erabi_sdk/integrations/tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography>=42
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
erabi_sdk
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "erabi-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Erabi Protocol SDK for Python agents: register, fire intents, report outcomes, get paid."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
dependencies = ["cryptography>=42"]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["setuptools>=68"]
|
|
12
|
+
build-backend = "setuptools.build_meta"
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.packages.find]
|
|
15
|
+
include = ["erabi_sdk*"]
|