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.
- agentverse_sdk/__init__.py +3 -0
- agentverse_sdk/_common/__init__.py +0 -0
- agentverse_sdk/_common/av.py +160 -0
- agentverse_sdk/_common/config.py +4 -0
- agentverse_sdk/_common/events.py +210 -0
- agentverse_sdk/_common/helpers.py +5 -0
- agentverse_sdk/_common/logger.py +76 -0
- agentverse_sdk/_common/starlette.py +120 -0
- agentverse_sdk/_common/storage.py +70 -0
- agentverse_sdk/_common/types.py +234 -0
- agentverse_sdk/a2a/__init__.py +3 -0
- agentverse_sdk/a2a/_app.py +266 -0
- agentverse_sdk/a2a/content.py +212 -0
- agentverse_sdk/a2a/profile.py +90 -0
- agentverse_sdk/langgraph/__init__.py +3 -0
- agentverse_sdk/langgraph/__main__.py +142 -0
- agentverse_sdk/langgraph/_app.py +415 -0
- agentverse_sdk/langgraph/config.py +21 -0
- agentverse_sdk/langgraph/content.py +244 -0
- agentverse_sdk/langgraph/profile.py +70 -0
- agentverse_sdk-0.1.0.dist-info/METADATA +50 -0
- agentverse_sdk-0.1.0.dist-info/RECORD +24 -0
- agentverse_sdk-0.1.0.dist-info/WHEEL +4 -0
- agentverse_sdk-0.1.0.dist-info/entry_points.txt +2 -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,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,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
|