Skip to content

Commit 7abbd39

Browse files
feat(core): add storage api upload/download to agent (#685)
1 parent cac23ae commit 7abbd39

File tree

3 files changed

+81
-0
lines changed

3 files changed

+81
-0
lines changed

python/uagents-core/uagents_core/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
DEFAULT_CHALLENGE_PATH = "/v1/auth/challenge"
77
DEFAULT_MAILBOX_PATH = "/v1/submit"
88
DEFAULT_PROXY_PATH = "/v1/proxy/submit"
9+
DEFAULT_STORAGE_PATH = "/v1/storage"
910

1011
DEFAULT_MAX_ENDPOINTS = 10
1112

@@ -30,3 +31,7 @@ def mailbox_endpoint(self) -> str:
3031
@property
3132
def proxy_endpoint(self) -> str:
3233
return f"{self.url}{DEFAULT_PROXY_PATH}"
34+
35+
@property
36+
def storage_endpoint(self) -> str:
37+
return f"{self.url}{DEFAULT_STORAGE_PATH}"

python/uagents-core/uagents_core/contrib/protocols/chat/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ class EndStreamContent(Model):
8080
stream_id: UUID4
8181

8282

83+
class CreateResourceContent(Model):
84+
type: Literal["create-resource"]
85+
86+
8387
# The combined agent content types
8488
AgentContent = (
8589
TextContent
@@ -89,6 +93,7 @@ class EndStreamContent(Model):
8993
| EndSessionContent
9094
| StartStreamContent
9195
| EndStreamContent
96+
| CreateResourceContent
9297
)
9398

9499

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import base64
2+
import struct
3+
from typing import Optional
4+
from datetime import datetime
5+
from secrets import token_bytes
6+
7+
import requests
8+
from uagents_core.config import AgentverseConfig
9+
from uagents_core.identity import Identity
10+
11+
12+
def compute_attestation(
13+
identity: Identity, validity_start: datetime, validity_secs: int, nonce: bytes
14+
) -> str:
15+
"""
16+
Compute a valid agent attestation token for authentication.
17+
"""
18+
assert len(nonce) == 32, "Nonce is of invalid length"
19+
20+
valid_from = int(validity_start.timestamp())
21+
valid_to = valid_from + validity_secs
22+
23+
public_key = bytes.fromhex(identity.pub_key)
24+
25+
payload = public_key + struct.pack(">QQ", valid_from, valid_to) + nonce
26+
assert len(payload) == 81, "attestation payload is incorrect"
27+
28+
signature = identity.sign(payload)
29+
attestation = f"attr:{base64.b64encode(payload).decode()}:{signature}"
30+
return attestation
31+
32+
33+
class ExternalStorage:
34+
def __init__(self, identity: Identity, storage_url: Optional[str] = None):
35+
self.identity = identity
36+
self.storage_url = storage_url or AgentverseConfig().storage_endpoint
37+
38+
def _make_attestation(self) -> str:
39+
nonce = token_bytes(32)
40+
now = datetime.utcnow()
41+
return compute_attestation(self.identity, now, 3600, nonce)
42+
43+
def upload(self, asset_id: str, asset_content: str):
44+
url = f"{self.storage_url}/assets/{asset_id}/contents/"
45+
headers = {"Authorization": f"Agent {self._make_attestation()}"}
46+
payload = {
47+
"contents": base64.b64encode(asset_content.encode()).decode(),
48+
"mime_type": "text/plain",
49+
}
50+
51+
response = requests.put(url, json=payload, headers=headers)
52+
if response.status_code != 200:
53+
raise RuntimeError(
54+
f"Upload failed: {response.status_code}, {response.text}"
55+
)
56+
return response
57+
58+
def download(self, asset_id: str) -> str:
59+
url = f"{self.storage_url}/assets/{asset_id}/contents/"
60+
headers = {
61+
"Authorization": f"Agent {self._make_attestation()}",
62+
"accept": "text/plain",
63+
}
64+
65+
response = requests.get(url, headers=headers)
66+
if response.status_code != 200:
67+
raise RuntimeError(
68+
f"Download failed: {response.status_code}, {response.text}"
69+
)
70+
71+
return response

0 commit comments

Comments
 (0)