agentverse-sdk 0.1.0__py3-none-any.whl

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,3 @@
1
+ """Agentverse SDK — connect any AI agent framework to Agentverse."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,160 @@
1
+ import json
2
+ import logging
3
+ from datetime import datetime, timezone
4
+ from secrets import token_bytes
5
+ from uuid import UUID
6
+
7
+ import httpx
8
+ import requests
9
+ from pydantic import BaseModel
10
+ from uagents_core.config import AgentverseConfig
11
+ from uagents_core.contrib.protocols.chat import chat_protocol_spec
12
+ from uagents_core.envelope import Envelope
13
+ from uagents_core.identity import Identity, is_user_address
14
+ from uagents_core.models import Model
15
+ from uagents_core.protocol import ProtocolSpecification
16
+ from uagents_core.registration import AgentStatusUpdate, RegistrationRequest
17
+ from uagents_core.storage import compute_attestation
18
+ from uagents_core.types import JsonStr
19
+ from uagents_core.utils.messages import generate_message_envelope, parse_envelope_raw
20
+ from uagents_core.utils.registration import (
21
+ _send_post_request_agentverse,
22
+ )
23
+ from uagents_core.utils.resolver import AlmanacResolver
24
+
25
+ from agentverse_sdk._common.config import (
26
+ AGENT_AUTH_TOKEN_VALIDITY,
27
+ DEFAULT_HTTP_REQUESTS_TIMEOUT,
28
+ )
29
+
30
+ CHAT_PROTOCOL = ProtocolSpecification.compute_digest(chat_protocol_spec.manifest())
31
+
32
+
33
+ for ch in ["uagents_core.utils.resolver", "uagents_core.utils.messages", "httpx"]:
34
+ logging.getLogger(ch).setLevel(logging.CRITICAL)
35
+
36
+
37
+ def generate_agent_auth_token(id: Identity) -> str:
38
+ return compute_attestation(
39
+ id, datetime.now(timezone.utc), AGENT_AUTH_TOKEN_VALIDITY, token_bytes(32)
40
+ )
41
+
42
+
43
+ def register_to_agentverse_sync(
44
+ request: RegistrationRequest,
45
+ headers: dict[str, str],
46
+ agentverse: AgentverseConfig,
47
+ timeout: int = DEFAULT_HTTP_REQUESTS_TIMEOUT,
48
+ ):
49
+ _send_post_request_agentverse(
50
+ url=agentverse.agents_api,
51
+ data=request,
52
+ headers=headers,
53
+ timeout=timeout,
54
+ )
55
+
56
+
57
+ def verify_envelope(envelope: Envelope) -> bool:
58
+ try:
59
+ if is_user_address(envelope.sender):
60
+ return True
61
+ return envelope.verify()
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ async def set_agent_status(
67
+ agent: Identity,
68
+ active: bool,
69
+ agentverse: AgentverseConfig,
70
+ timeout: int = DEFAULT_HTTP_REQUESTS_TIMEOUT,
71
+ ):
72
+ update = AgentStatusUpdate(agent_identifier=agent.address, is_active=active)
73
+ update.sign(agent)
74
+
75
+ await _post_data(
76
+ url=f"{agentverse.almanac_api}/agents/{agent.address}/status",
77
+ data=update,
78
+ timeout=timeout,
79
+ )
80
+
81
+
82
+ async def _post_data(
83
+ url: str,
84
+ data: BaseModel,
85
+ headers: dict[str, str] | None = None,
86
+ timeout: int = DEFAULT_HTTP_REQUESTS_TIMEOUT,
87
+ ) -> httpx.Response:
88
+ headers = headers or dict()
89
+ headers["Content-Type"] = "application/json"
90
+
91
+ async with httpx.AsyncClient(timeout=timeout) as client:
92
+ response = await client.post(
93
+ url=url,
94
+ data=data.model_dump_json(),
95
+ headers=headers,
96
+ )
97
+ response.raise_for_status()
98
+
99
+ return response
100
+
101
+
102
+ async def send_message_to_agent(
103
+ destination: str,
104
+ msg: Model,
105
+ sender: Identity,
106
+ *,
107
+ session_id: UUID | None = None,
108
+ agentverse_config: AgentverseConfig,
109
+ sync: bool = False,
110
+ timeout: int = DEFAULT_HTTP_REQUESTS_TIMEOUT,
111
+ response_type: type[Model] | set[type[Model]] | None = None,
112
+ ) -> Model | JsonStr | None:
113
+ resolver = AlmanacResolver(agentverse_config=agentverse_config)
114
+ _, endpoints = await resolver.resolve(destination)
115
+ if len(endpoints) == 0:
116
+ raise RuntimeError(f"Couldn't resolve endpoint for agent {destination}")
117
+
118
+ env = generate_message_envelope(
119
+ destination=destination,
120
+ message_schema_digest=Model.build_schema_digest(msg),
121
+ message_body=json.loads(msg.model_dump_json()),
122
+ sender=sender,
123
+ session_id=session_id,
124
+ )
125
+
126
+ headers = None
127
+ if sync:
128
+ headers = {"x-uagents-connection": "sync"}
129
+
130
+ response = await _post_data(
131
+ url=endpoints[0],
132
+ data=env,
133
+ headers=headers,
134
+ timeout=timeout,
135
+ )
136
+
137
+ if response.is_success and sync:
138
+ return parse_envelope_raw(response.text, response_type)
139
+
140
+ return None
141
+
142
+
143
+ def _post_data_sync(
144
+ url: str,
145
+ data: BaseModel,
146
+ headers: dict[str, str] | None = None,
147
+ timeout: int = DEFAULT_HTTP_REQUESTS_TIMEOUT,
148
+ ) -> requests.Response:
149
+ headers = headers or dict()
150
+ headers["Content-Type"] = "application/json"
151
+
152
+ response = requests.post(
153
+ url=url,
154
+ data=json.dumps(data.model_dump(mode="json")),
155
+ headers=headers,
156
+ timeout=timeout,
157
+ )
158
+ response.raise_for_status()
159
+
160
+ return response
@@ -0,0 +1,4 @@
1
+ DEFAULT_AGENTVERSE_CHAT_ENDPOINT = "/av/chat"
2
+ DEFAULT_HTTP_REQUESTS_TIMEOUT = 10
3
+ AGENT_AUTH_TOKEN_VALIDITY = 60 * 2
4
+ DEFAULT_EMPTY_RESPONSE_TEXT = "Agent returned no response."
@@ -0,0 +1,210 @@
1
+ import contextlib
2
+ import functools
3
+ import traceback
4
+ from contextlib import asynccontextmanager
5
+ from typing import Callable
6
+ from uuid import uuid4
7
+
8
+ from requests import HTTPError
9
+ from uagents_core.contrib.protocols.chat import ChatMessage, TextContent
10
+ from uagents_core.envelope import Envelope
11
+
12
+ from agentverse_sdk._common.av import (
13
+ _post_data,
14
+ _post_data_sync,
15
+ generate_agent_auth_token,
16
+ send_message_to_agent,
17
+ )
18
+ from agentverse_sdk._common.helpers import utc_now
19
+ from agentverse_sdk._common.logger import log_sdk, logger
20
+ from agentverse_sdk._common.types import (
21
+ AgentBatchEvents,
22
+ AgentContext,
23
+ AgentStarletteState,
24
+ AgentUri,
25
+ EventCategory,
26
+ )
27
+
28
+ FAILED_INIT_ERROR_FORMAT = "Failed to initialize agentverse sdk ({})"
29
+
30
+
31
+ def get_agent_address(agent: str | AgentUri) -> str | None:
32
+ if isinstance(agent, str):
33
+ try:
34
+ agent = AgentUri.from_str(agent)
35
+ except Exception:
36
+ return None
37
+
38
+ return agent.identity.address
39
+
40
+
41
+ def _dispatch_event_sync(
42
+ agent: AgentUri | AgentStarletteState, events: AgentBatchEvents
43
+ ):
44
+ _post_data_sync(
45
+ url=f"{agent.agentverse.url}/v1/events",
46
+ data=events,
47
+ headers={"Authorization": f"Agent {generate_agent_auth_token(agent.identity)}"},
48
+ )
49
+
50
+
51
+ async def dispatch_event(
52
+ agent: AgentUri | AgentStarletteState, events: AgentBatchEvents
53
+ ):
54
+ await _post_data(
55
+ url=f"{agent.agentverse.url}/v1/events",
56
+ data=events,
57
+ headers={"Authorization": f"Agent {generate_agent_auth_token(agent.identity)}"},
58
+ )
59
+
60
+
61
+ @contextlib.contextmanager
62
+ def handle_init_errors(uri: AgentUri):
63
+ try:
64
+ yield
65
+ except Exception as e:
66
+ logger.error("Init error suppressed: %s", e)
67
+ event = AgentBatchEvents.from_exception(e, traceback.format_exc())
68
+ try:
69
+ _dispatch_event_sync(uri, event)
70
+ except HTTPError as dispatch_exception:
71
+ log_sdk(
72
+ FAILED_INIT_ERROR_FORMAT.format(
73
+ f"{dispatch_exception.response.status_code} at {utc_now().timestamp()}"
74
+ )
75
+ )
76
+ except Exception as generic_exception:
77
+ log_sdk(FAILED_INIT_ERROR_FORMAT.format(str(generic_exception)))
78
+
79
+
80
+ def report_error(
81
+ ctx: AgentContext, category: EventCategory = "user", reraise: bool = False
82
+ ):
83
+ def decorator(func: Callable):
84
+ @functools.wraps(func)
85
+ async def wrapper(*args, **kwargs):
86
+ try:
87
+ return await func(*args, **kwargs)
88
+ except Exception as e:
89
+ logger.error("Error reported: %s", e)
90
+ event = AgentBatchEvents.from_exception(
91
+ e, traceback.format_exc(), category
92
+ )
93
+ if ctx.agent is not None:
94
+ await dispatch_event_safe(ctx.agent.uri, event)
95
+ if reraise:
96
+ raise e
97
+
98
+ return wrapper
99
+
100
+ return decorator
101
+
102
+
103
+ class ChatProcessingError(Exception):
104
+ """Raised inside @report_error_reply functions to control error handling behavior."""
105
+
106
+ def __init__(
107
+ self,
108
+ message: str,
109
+ category: EventCategory = "system",
110
+ exc: Exception | None = None,
111
+ ):
112
+ self.reply_text = message
113
+ self.category = category
114
+ self.exc = exc
115
+ super().__init__(message)
116
+
117
+
118
+ DEFAULT_ERROR_REPLY = "Agent failed to process request, please retry later."
119
+
120
+
121
+ async def dispatch_event_safe(
122
+ agent: AgentUri | AgentStarletteState, event: AgentBatchEvents
123
+ ):
124
+ """Dispatch event to Agentverse. Swallows failures."""
125
+ try:
126
+ await dispatch_event(agent, event)
127
+ except Exception as e:
128
+ logger.error("Failed to dispatch event: %s", e)
129
+
130
+
131
+ @asynccontextmanager
132
+ async def chat_error_on_fail(
133
+ message: str,
134
+ category: EventCategory = "system",
135
+ include_exc: bool = True,
136
+ ):
137
+ """Wraps an operation. On failure, raises ChatProcessingError."""
138
+ try:
139
+ yield
140
+ except Exception as e:
141
+ raise ChatProcessingError(
142
+ message,
143
+ category=category,
144
+ exc=e if include_exc or category == "system" else None,
145
+ ) from e
146
+
147
+
148
+ def report_error_reply(ctx: AgentContext, category: EventCategory = "system"):
149
+ """Decorator: on exception, dispatch event + send error reply."""
150
+
151
+ def decorator(func: Callable):
152
+ @functools.wraps(func)
153
+ async def wrapper(self, env: Envelope, *args, **kwargs):
154
+ try:
155
+ return await func(self, env, *args, **kwargs)
156
+ except ChatProcessingError as e:
157
+ if e.exc:
158
+ tb = "".join(traceback.format_exception(e.exc))
159
+ await dispatch_event_safe(
160
+ ctx.agent.uri,
161
+ AgentBatchEvents.from_exception(e.exc, tb, e.category),
162
+ )
163
+ else:
164
+ await dispatch_event_safe(
165
+ ctx.agent.uri,
166
+ AgentBatchEvents.from_message(
167
+ e.reply_text, e.category, "error"
168
+ ),
169
+ )
170
+ await _send_error_reply(ctx, env, e.reply_text)
171
+ except Exception as e:
172
+ await dispatch_event_safe(
173
+ ctx.agent.uri,
174
+ AgentBatchEvents.from_exception(
175
+ e, traceback.format_exc(), category
176
+ ),
177
+ )
178
+ await _send_error_reply(ctx, env, DEFAULT_ERROR_REPLY)
179
+
180
+ return wrapper
181
+
182
+ return decorator
183
+
184
+
185
+ async def _send_error_reply(ctx: AgentContext, env: Envelope, text: str):
186
+ """Last-resort error reply. Self-guarded — never raises."""
187
+ if ctx.agent is None:
188
+ return
189
+ try:
190
+ await send_message_to_agent(
191
+ destination=env.sender,
192
+ msg=ChatMessage(
193
+ timestamp=utc_now(),
194
+ msg_id=uuid4(),
195
+ content=[TextContent(text=text)],
196
+ ),
197
+ sender=ctx.agent.uri.identity,
198
+ agentverse_config=ctx.agent.uri.agentverse,
199
+ session_id=env.session,
200
+ )
201
+ except Exception as e:
202
+ logger.error("Failed to send error reply to %s: %s", env.sender, e)
203
+ await dispatch_event_safe(
204
+ ctx.agent.uri,
205
+ AgentBatchEvents.from_message(
206
+ f"Failed to deliver error reply to {env.sender}: {e}",
207
+ "user",
208
+ "info",
209
+ ),
210
+ )
@@ -0,0 +1,5 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def utc_now() -> datetime:
5
+ return datetime.now(timezone.utc)
@@ -0,0 +1,76 @@
1
+ import logging
2
+
3
+ SDK_WARN = 99
4
+
5
+ logger = logging.getLogger("agentverse-sdk")
6
+ logger.setLevel(logging.CRITICAL)
7
+
8
+
9
+ class _AlignedFormatter(logging.Formatter):
10
+ def format(self, record):
11
+ record.levelprefix = f"{record.levelname}:".ljust(10)
12
+ return super().format(record)
13
+
14
+
15
+ class _SDKWarnFilter(logging.Filter):
16
+ def filter(self, record):
17
+ if record.levelno == SDK_WARN:
18
+ record.levelname = "WARNING"
19
+ return True
20
+
21
+
22
+ logger.addFilter(_SDKWarnFilter())
23
+
24
+
25
+ _FMT = "%(levelprefix)s [sdk] %(message)s"
26
+
27
+
28
+ def _make_formatter():
29
+ try:
30
+ from uvicorn.logging import DefaultFormatter
31
+
32
+ fmt = DefaultFormatter(_FMT)
33
+ fmt.level_name_colors[SDK_WARN] = fmt.level_name_colors[logging.WARNING]
34
+ return fmt
35
+ except (ImportError, AttributeError):
36
+ return _AlignedFormatter(_FMT)
37
+
38
+
39
+ def _has_structlog():
40
+ try:
41
+ import structlog.stdlib
42
+
43
+ return any(
44
+ isinstance(h.formatter, structlog.stdlib.ProcessorFormatter)
45
+ for h in logging.getLogger().handlers
46
+ )
47
+ except ImportError:
48
+ return False
49
+
50
+
51
+ class _SmartHandler(logging.StreamHandler):
52
+ """Emits with our formatter until structlog appears, then steps aside."""
53
+
54
+ _structlog_active = False
55
+
56
+ def emit(self, record):
57
+ if not self._structlog_active and _has_structlog():
58
+ self._structlog_active = True
59
+ logger.propagate = True
60
+ if self._structlog_active:
61
+ return
62
+ super().emit(record)
63
+
64
+
65
+ def configure(level: int):
66
+ logger.setLevel(level)
67
+ if not logger.handlers:
68
+ handler = _SmartHandler()
69
+ handler.setLevel(0)
70
+ handler.setFormatter(_make_formatter())
71
+ logger.addHandler(handler)
72
+ logger.propagate = False
73
+
74
+
75
+ def log_sdk(msg: str, *args):
76
+ logger.log(SDK_WARN, msg, *args)
@@ -0,0 +1,120 @@
1
+ import functools
2
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
3
+ from typing import Callable, cast
4
+
5
+ from starlette import status
6
+ from starlette.applications import Starlette
7
+ from starlette.exceptions import HTTPException
8
+ from starlette.requests import Request
9
+ from uagents_core.contrib.protocols.chat import (
10
+ ChatAcknowledgement,
11
+ ChatMessage,
12
+ )
13
+ from uagents_core.envelope import Envelope
14
+ from uagents_core.utils.messages import parse_envelope
15
+
16
+ from agentverse_sdk._common.av import set_agent_status, verify_envelope
17
+ from agentverse_sdk._common.events import dispatch_event, report_error
18
+ from agentverse_sdk._common.logger import logger
19
+ from agentverse_sdk._common.types import (
20
+ AgentBatchEvents,
21
+ AgentContext,
22
+ AgentStarletteState,
23
+ EventCategory,
24
+ )
25
+
26
+
27
+ async def parse_chat_message_from_request(
28
+ request: Request, verify: bool, expected_address: str | None = None
29
+ ) -> tuple[Envelope, ChatMessage | ChatAcknowledgement]:
30
+ malformed_exc = HTTPException(
31
+ status_code=status.HTTP_400_BAD_REQUEST,
32
+ detail="Malformed envelope or chat message",
33
+ )
34
+
35
+ try:
36
+ env = Envelope.model_validate(await request.json())
37
+ if verify and not verify_envelope(env):
38
+ raise HTTPException(
39
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid envelope"
40
+ )
41
+ if expected_address is not None and env.target != expected_address:
42
+ raise HTTPException(
43
+ status_code=status.HTTP_400_BAD_REQUEST,
44
+ detail="Wrong destination address",
45
+ )
46
+ msg = cast(
47
+ ChatMessage | ChatAcknowledgement | str,
48
+ parse_envelope(env, {ChatMessage, ChatAcknowledgement}),
49
+ )
50
+ if isinstance(msg, str):
51
+ raise malformed_exc
52
+
53
+ return env, msg
54
+ except Exception as e:
55
+ raise malformed_exc from e
56
+
57
+
58
+ @asynccontextmanager
59
+ async def agent_status_lifespan(app: Starlette):
60
+ if hasattr(app.state, "agent"):
61
+ agent = app.state.agent
62
+ try:
63
+ await set_agent_status(agent.identity, True, agent.agentverse)
64
+ await dispatch_event(
65
+ agent, AgentBatchEvents.from_message("Agent Started")
66
+ )
67
+ except Exception as e:
68
+ logger.error("Failed to report agent start: %s", e)
69
+ yield
70
+ if hasattr(app.state, "agent"):
71
+ agent = app.state.agent
72
+ try:
73
+ await set_agent_status(agent.identity, False, agent.agentverse)
74
+ await dispatch_event(
75
+ agent, AgentBatchEvents.from_message("Agent Stopped")
76
+ )
77
+ except Exception as e:
78
+ logger.error("Failed to report agent stop: %s", e)
79
+
80
+
81
+ Lifespan = Callable[[Starlette], AbstractAsyncContextManager[None]]
82
+
83
+
84
+ def setup_agent_status_lifespan(
85
+ existing_lifespan: Lifespan | None = None,
86
+ ) -> Lifespan:
87
+ if existing_lifespan is None:
88
+ return agent_status_lifespan
89
+
90
+ @asynccontextmanager
91
+ async def combined_lifespan(app: Starlette):
92
+ async with agent_status_lifespan(app), existing_lifespan(app):
93
+ yield
94
+
95
+ return combined_lifespan
96
+
97
+
98
+ def set_app_state(app: Starlette, agent: AgentStarletteState):
99
+ app.state.agent = agent
100
+
101
+
102
+ def report_error_starlette(ctx: AgentContext, category: EventCategory = "user"):
103
+ def decorator(func: Callable):
104
+ reported = report_error(ctx, category, reraise=True)(func)
105
+
106
+ @functools.wraps(func)
107
+ async def wrapper(*args, **kwargs):
108
+ try:
109
+ return await reported(*args, **kwargs)
110
+ except HTTPException:
111
+ raise
112
+ except Exception as e:
113
+ raise HTTPException(
114
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
115
+ detail="Failed to process request",
116
+ ) from e
117
+
118
+ return wrapper
119
+
120
+ return decorator
@@ -0,0 +1,70 @@
1
+ """
2
+ Async helper for uploading content to Agentverse External Storage.
3
+
4
+ API reference: agentverse-core services/storage/src/api/routes/public/assets.py
5
+ - POST /v1/storage/assets/ accepts Agent attestation auth (get_client dep)
6
+ - Request body: NewAsset {contents (base64), mime_type, lifetime_hours (1-24)}
7
+ - Response: 201 with Asset {asset_id (UUID)}; 403 if agent not delegated
8
+ """
9
+
10
+ import base64
11
+ import traceback
12
+
13
+ import httpx
14
+
15
+ from agentverse_sdk._common.av import generate_agent_auth_token
16
+ from agentverse_sdk._common.events import dispatch_event_safe
17
+ from agentverse_sdk._common.logger import logger
18
+ from agentverse_sdk._common.types import AgentBatchEvents, AgentUri
19
+
20
+
21
+ async def upload_to_storage(
22
+ content: bytes,
23
+ mime_type: str,
24
+ agent_uri: AgentUri,
25
+ ) -> str:
26
+ """Upload content to Agentverse storage, return an agent-storage:// URI."""
27
+ url = f"{agent_uri.agentverse.storage_api}/assets/"
28
+ headers = {
29
+ "Authorization": f"Agent {generate_agent_auth_token(agent_uri.identity)}",
30
+ "Content-Type": "application/json",
31
+ }
32
+ payload = {
33
+ "contents": base64.b64encode(content).decode(),
34
+ "mime_type": mime_type,
35
+ "lifetime_hours": 24,
36
+ }
37
+
38
+ async with httpx.AsyncClient(timeout=10) as client:
39
+ response = await client.post(url, json=payload, headers=headers)
40
+
41
+ if response.status_code != 201:
42
+ raise RuntimeError(f"Storage upload failed: {response.status_code}")
43
+
44
+ asset_id = response.json()["asset_id"]
45
+
46
+ uri = f"agent-storage://{agent_uri.agentverse.base_url}/{asset_id}"
47
+ logger.debug(f"Uploaded to {uri}")
48
+ return uri
49
+
50
+
51
+
52
+ async def upload_to_storage_safe(
53
+ content: bytes,
54
+ mime_type: str,
55
+ agent_uri: AgentUri,
56
+ ) -> str | None:
57
+ """Upload to storage, returning None on failure.
58
+
59
+ Logs the error and dispatches a system event so the Agentverse team
60
+ can detect storage service issues without user intervention.
61
+ """
62
+ try:
63
+ return await upload_to_storage(content, mime_type, agent_uri)
64
+ except Exception as e:
65
+ logger.error("Storage upload failed: %s", e)
66
+ await dispatch_event_safe(
67
+ agent_uri,
68
+ AgentBatchEvents.from_exception(e, traceback.format_exc(), "system"),
69
+ )
70
+ return None