Skip to content

feat(core): add storage api upload/download to agent #685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions python/uagents-core/uagents_core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
DEFAULT_CHALLENGE_PATH = "/v1/auth/challenge"
DEFAULT_MAILBOX_PATH = "/v1/submit"
DEFAULT_PROXY_PATH = "/v1/proxy/submit"
DEFAULT_STORAGE_PATH = "/v1/storage"

DEFAULT_MAX_ENDPOINTS = 10

Expand All @@ -30,3 +31,7 @@ def mailbox_endpoint(self) -> str:
@property
def proxy_endpoint(self) -> str:
return f"{self.url}{DEFAULT_PROXY_PATH}"

@property
def storage_endpoint(self) -> str:
return f"{self.url}{DEFAULT_STORAGE_PATH}"
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class EndStreamContent(Model):
stream_id: UUID4


class CreateResourceContent(Model):
type: Literal["create-resource"]


# The combined agent content types
AgentContent = (
TextContent
Expand All @@ -89,6 +93,7 @@ class EndStreamContent(Model):
| EndSessionContent
| StartStreamContent
| EndStreamContent
| CreateResourceContent
)


Expand Down
71 changes: 71 additions & 0 deletions python/uagents-core/uagents_core/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import base64
import struct
from typing import Optional
from datetime import datetime
from secrets import token_bytes

import requests
from uagents_core.config import AgentverseConfig
from uagents_core.identity import Identity


def compute_attestation(
identity: Identity, validity_start: datetime, validity_secs: int, nonce: bytes
) -> str:
"""
Compute a valid agent attestation token for authentication.
"""
assert len(nonce) == 32, "Nonce is of invalid length"

valid_from = int(validity_start.timestamp())
valid_to = valid_from + validity_secs

public_key = bytes.fromhex(identity.pub_key)

payload = public_key + struct.pack(">QQ", valid_from, valid_to) + nonce
assert len(payload) == 81, "attestation payload is incorrect"

signature = identity.sign(payload)
attestation = f"attr:{base64.b64encode(payload).decode()}:{signature}"
return attestation


class ExternalStorage:
def __init__(self, identity: Identity, storage_url: Optional[str] = None):
self.identity = identity
self.storage_url = storage_url or AgentverseConfig().storage_endpoint

def _make_attestation(self) -> str:
nonce = token_bytes(32)
now = datetime.utcnow()
return compute_attestation(self.identity, now, 3600, nonce)

def upload(self, asset_id: str, asset_content: str):
url = f"{self.storage_url}/assets/{asset_id}/contents/"
headers = {"Authorization": f"Agent {self._make_attestation()}"}
payload = {
"contents": base64.b64encode(asset_content.encode()).decode(),
"mime_type": "text/plain",
}

response = requests.put(url, json=payload, headers=headers)
if response.status_code != 200:
raise RuntimeError(
f"Upload failed: {response.status_code}, {response.text}"
)
return response

def download(self, asset_id: str) -> str:
url = f"{self.storage_url}/assets/{asset_id}/contents/"
headers = {
"Authorization": f"Agent {self._make_attestation()}",
"accept": "text/plain",
}

response = requests.get(url, headers=headers)
if response.status_code != 200:
raise RuntimeError(
f"Download failed: {response.status_code}, {response.text}"
)

return response
Loading