agentstack-sdk 0.5.2rc2__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.
- agentstack_sdk/__init__.py +6 -0
- agentstack_sdk/a2a/__init__.py +2 -0
- agentstack_sdk/a2a/extensions/__init__.py +8 -0
- agentstack_sdk/a2a/extensions/auth/__init__.py +5 -0
- agentstack_sdk/a2a/extensions/auth/oauth/__init__.py +4 -0
- agentstack_sdk/a2a/extensions/auth/oauth/oauth.py +151 -0
- agentstack_sdk/a2a/extensions/auth/oauth/storage/__init__.py +5 -0
- agentstack_sdk/a2a/extensions/auth/oauth/storage/base.py +11 -0
- agentstack_sdk/a2a/extensions/auth/oauth/storage/memory.py +38 -0
- agentstack_sdk/a2a/extensions/auth/secrets/__init__.py +4 -0
- agentstack_sdk/a2a/extensions/auth/secrets/secrets.py +77 -0
- agentstack_sdk/a2a/extensions/base.py +205 -0
- agentstack_sdk/a2a/extensions/common/__init__.py +4 -0
- agentstack_sdk/a2a/extensions/common/form.py +149 -0
- agentstack_sdk/a2a/extensions/exceptions.py +11 -0
- agentstack_sdk/a2a/extensions/interactions/__init__.py +4 -0
- agentstack_sdk/a2a/extensions/interactions/approval.py +125 -0
- agentstack_sdk/a2a/extensions/services/__init__.py +8 -0
- agentstack_sdk/a2a/extensions/services/embedding.py +106 -0
- agentstack_sdk/a2a/extensions/services/form.py +54 -0
- agentstack_sdk/a2a/extensions/services/llm.py +100 -0
- agentstack_sdk/a2a/extensions/services/mcp.py +193 -0
- agentstack_sdk/a2a/extensions/services/platform.py +141 -0
- agentstack_sdk/a2a/extensions/tools/__init__.py +5 -0
- agentstack_sdk/a2a/extensions/tools/call.py +114 -0
- agentstack_sdk/a2a/extensions/tools/exceptions.py +6 -0
- agentstack_sdk/a2a/extensions/ui/__init__.py +10 -0
- agentstack_sdk/a2a/extensions/ui/agent_detail.py +54 -0
- agentstack_sdk/a2a/extensions/ui/canvas.py +71 -0
- agentstack_sdk/a2a/extensions/ui/citation.py +78 -0
- agentstack_sdk/a2a/extensions/ui/error.py +223 -0
- agentstack_sdk/a2a/extensions/ui/form_request.py +52 -0
- agentstack_sdk/a2a/extensions/ui/settings.py +73 -0
- agentstack_sdk/a2a/extensions/ui/trajectory.py +70 -0
- agentstack_sdk/a2a/types.py +104 -0
- agentstack_sdk/platform/__init__.py +12 -0
- agentstack_sdk/platform/client.py +123 -0
- agentstack_sdk/platform/common.py +37 -0
- agentstack_sdk/platform/configuration.py +47 -0
- agentstack_sdk/platform/context.py +291 -0
- agentstack_sdk/platform/file.py +295 -0
- agentstack_sdk/platform/model_provider.py +131 -0
- agentstack_sdk/platform/provider.py +219 -0
- agentstack_sdk/platform/provider_build.py +190 -0
- agentstack_sdk/platform/types.py +45 -0
- agentstack_sdk/platform/user.py +70 -0
- agentstack_sdk/platform/user_feedback.py +42 -0
- agentstack_sdk/platform/variables.py +44 -0
- agentstack_sdk/platform/vector_store.py +217 -0
- agentstack_sdk/py.typed +0 -0
- agentstack_sdk/server/__init__.py +4 -0
- agentstack_sdk/server/agent.py +594 -0
- agentstack_sdk/server/app.py +87 -0
- agentstack_sdk/server/constants.py +9 -0
- agentstack_sdk/server/context.py +68 -0
- agentstack_sdk/server/dependencies.py +117 -0
- agentstack_sdk/server/exceptions.py +3 -0
- agentstack_sdk/server/middleware/__init__.py +3 -0
- agentstack_sdk/server/middleware/platform_auth_backend.py +131 -0
- agentstack_sdk/server/server.py +376 -0
- agentstack_sdk/server/store/__init__.py +3 -0
- agentstack_sdk/server/store/context_store.py +35 -0
- agentstack_sdk/server/store/memory_context_store.py +59 -0
- agentstack_sdk/server/store/platform_context_store.py +58 -0
- agentstack_sdk/server/telemetry.py +53 -0
- agentstack_sdk/server/utils.py +26 -0
- agentstack_sdk/types.py +15 -0
- agentstack_sdk/util/__init__.py +4 -0
- agentstack_sdk/util/file.py +260 -0
- agentstack_sdk/util/httpx.py +18 -0
- agentstack_sdk/util/logging.py +63 -0
- agentstack_sdk/util/resource_context.py +44 -0
- agentstack_sdk/util/utils.py +47 -0
- agentstack_sdk-0.5.2rc2.dist-info/METADATA +120 -0
- agentstack_sdk-0.5.2rc2.dist-info/RECORD +76 -0
- agentstack_sdk-0.5.2rc2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import functools
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
10
|
+
from configparser import RawConfigParser
|
|
11
|
+
from contextlib import asynccontextmanager, nullcontext, suppress
|
|
12
|
+
from datetime import timedelta
|
|
13
|
+
from ssl import CERT_NONE
|
|
14
|
+
from typing import IO, Any, Literal
|
|
15
|
+
|
|
16
|
+
import uvicorn
|
|
17
|
+
import uvicorn.config as uvicorn_config
|
|
18
|
+
from a2a.server.agent_execution import RequestContextBuilder
|
|
19
|
+
from a2a.server.events import QueueManager
|
|
20
|
+
from a2a.server.tasks import PushNotificationConfigStore, PushNotificationSender, TaskStore
|
|
21
|
+
from a2a.types import AgentExtension
|
|
22
|
+
from fastapi import FastAPI
|
|
23
|
+
from fastapi.applications import AppType
|
|
24
|
+
from fastapi.responses import PlainTextResponse
|
|
25
|
+
from httpx import HTTPError, HTTPStatusError
|
|
26
|
+
from pydantic import AnyUrl
|
|
27
|
+
from starlette.authentication import AuthenticationBackend, AuthenticationError
|
|
28
|
+
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
29
|
+
from starlette.requests import HTTPConnection
|
|
30
|
+
from starlette.types import Lifespan
|
|
31
|
+
from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_attempt, wait_exponential
|
|
32
|
+
|
|
33
|
+
from agentstack_sdk.platform import get_platform_client
|
|
34
|
+
from agentstack_sdk.platform.client import PlatformClient
|
|
35
|
+
from agentstack_sdk.platform.provider import Provider
|
|
36
|
+
from agentstack_sdk.server.agent import Agent, AgentFactory
|
|
37
|
+
from agentstack_sdk.server.agent import agent as agent_decorator
|
|
38
|
+
from agentstack_sdk.server.store.context_store import ContextStore
|
|
39
|
+
from agentstack_sdk.server.store.memory_context_store import InMemoryContextStore
|
|
40
|
+
from agentstack_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
|
|
41
|
+
from agentstack_sdk.server.utils import cancel_task
|
|
42
|
+
from agentstack_sdk.util.logging import configure_logger as configure_logger_func
|
|
43
|
+
from agentstack_sdk.util.logging import logger
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Server:
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._agent_factory: AgentFactory | None = None
|
|
49
|
+
self._agent: Agent | None = None
|
|
50
|
+
self.server: uvicorn.Server | None = None
|
|
51
|
+
self._context_store: ContextStore | None = None
|
|
52
|
+
self._self_registration_client: PlatformClient | None = None
|
|
53
|
+
self._self_registration_id: str | None = None
|
|
54
|
+
self._provider_id: str | None = None
|
|
55
|
+
self._all_configured_variables: set[str] = set()
|
|
56
|
+
|
|
57
|
+
@functools.wraps(agent_decorator)
|
|
58
|
+
def agent(*args, **kwargs) -> Callable:
|
|
59
|
+
self, other_args = args[0], args[1:] # Must hide self due to pyright issues
|
|
60
|
+
if self._agent_factory:
|
|
61
|
+
raise ValueError("Server can have only one agent.")
|
|
62
|
+
|
|
63
|
+
def decorator(fn: Callable) -> Callable:
|
|
64
|
+
self._agent_factory = agent_decorator(*other_args, **kwargs)(fn) # pyright: ignore [reportArgumentType]
|
|
65
|
+
return fn
|
|
66
|
+
|
|
67
|
+
return decorator
|
|
68
|
+
|
|
69
|
+
async def serve(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
configure_logger: bool = True,
|
|
73
|
+
configure_telemetry: bool = False,
|
|
74
|
+
self_registration: bool = True,
|
|
75
|
+
self_registration_id: str | None = None,
|
|
76
|
+
task_store: TaskStore | None = None,
|
|
77
|
+
context_store: ContextStore | None = None,
|
|
78
|
+
queue_manager: QueueManager | None = None,
|
|
79
|
+
task_timeout: timedelta = timedelta(minutes=10),
|
|
80
|
+
push_config_store: PushNotificationConfigStore | None = None,
|
|
81
|
+
push_sender: PushNotificationSender | None = None,
|
|
82
|
+
request_context_builder: RequestContextBuilder | None = None,
|
|
83
|
+
host: str = "127.0.0.1",
|
|
84
|
+
port: int = 10000,
|
|
85
|
+
url: str | None = None,
|
|
86
|
+
uds: str | None = None,
|
|
87
|
+
fd: int | None = None,
|
|
88
|
+
loop: Literal["none", "auto", "asyncio", "uvloop"] = "auto",
|
|
89
|
+
http: type[asyncio.Protocol] | uvicorn_config.HTTPProtocolType = "auto",
|
|
90
|
+
ws: type[asyncio.Protocol] | uvicorn_config.WSProtocolType = "auto",
|
|
91
|
+
ws_max_size: int = 16 * 1024 * 1024,
|
|
92
|
+
ws_max_queue: int = 32,
|
|
93
|
+
ws_ping_interval: float | None = 20.0,
|
|
94
|
+
ws_ping_timeout: float | None = 20.0,
|
|
95
|
+
ws_per_message_deflate: bool = True,
|
|
96
|
+
lifespan: uvicorn_config.LifespanType = "auto",
|
|
97
|
+
lifespan_fn: Lifespan[AppType] | None = None,
|
|
98
|
+
env_file: str | os.PathLike[str] | None = None,
|
|
99
|
+
log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = uvicorn_config.LOGGING_CONFIG,
|
|
100
|
+
log_level: str | int | None = None,
|
|
101
|
+
access_log: bool = True,
|
|
102
|
+
use_colors: bool | None = None,
|
|
103
|
+
interface: uvicorn_config.InterfaceType = "auto",
|
|
104
|
+
reload: bool = False,
|
|
105
|
+
reload_dirs: list[str] | str | None = None,
|
|
106
|
+
reload_delay: float = 0.25,
|
|
107
|
+
reload_includes: list[str] | str | None = None,
|
|
108
|
+
reload_excludes: list[str] | str | None = None,
|
|
109
|
+
workers: int | None = None,
|
|
110
|
+
proxy_headers: bool = True,
|
|
111
|
+
server_header: bool = True,
|
|
112
|
+
date_header: bool = True,
|
|
113
|
+
forwarded_allow_ips: list[str] | str | None = None,
|
|
114
|
+
root_path: str = "",
|
|
115
|
+
limit_concurrency: int | None = None,
|
|
116
|
+
limit_max_requests: int | None = None,
|
|
117
|
+
backlog: int = 2048,
|
|
118
|
+
timeout_keep_alive: int = 5,
|
|
119
|
+
timeout_notify: int = 30,
|
|
120
|
+
timeout_worker_healthcheck: int = 5,
|
|
121
|
+
timeout_graceful_shutdown: int | None = None,
|
|
122
|
+
callback_notify: Callable[..., Awaitable[None]] | None = None,
|
|
123
|
+
ssl_keyfile: str | os.PathLike[str] | None = None,
|
|
124
|
+
ssl_certfile: str | os.PathLike[str] | None = None,
|
|
125
|
+
ssl_keyfile_password: str | None = None,
|
|
126
|
+
ssl_version: int = uvicorn_config.SSL_PROTOCOL_VERSION,
|
|
127
|
+
ssl_cert_reqs: int = CERT_NONE,
|
|
128
|
+
ssl_ca_certs: str | None = None,
|
|
129
|
+
ssl_ciphers: str = "TLSv1",
|
|
130
|
+
headers: list[tuple[str, str]] | None = None,
|
|
131
|
+
factory: bool = False,
|
|
132
|
+
h11_max_incomplete_event_size: int | None = None,
|
|
133
|
+
self_registration_client_factory: Callable[[], PlatformClient] | None = None,
|
|
134
|
+
auth_backend: AuthenticationBackend | None = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
if self.server:
|
|
137
|
+
raise RuntimeError("The server is already running")
|
|
138
|
+
if not self._agent_factory:
|
|
139
|
+
raise ValueError("Agent is not registered")
|
|
140
|
+
|
|
141
|
+
context_store = context_store or InMemoryContextStore()
|
|
142
|
+
self._agent = self._agent_factory(context_store.modify_dependencies)
|
|
143
|
+
card_url = url and url.strip()
|
|
144
|
+
self._agent.card.url = card_url.rstrip("/") if card_url else f"http://{host}:{port}"
|
|
145
|
+
|
|
146
|
+
self._self_registration_client = (
|
|
147
|
+
self_registration_client_factory() if self_registration_client_factory else None
|
|
148
|
+
)
|
|
149
|
+
self._self_registration_id = urllib.parse.quote(self_registration_id or self._agent.card.name)
|
|
150
|
+
|
|
151
|
+
if headers is None:
|
|
152
|
+
headers = [("server", "a2a")]
|
|
153
|
+
elif not any(k.lower() == "server" for k, _ in headers):
|
|
154
|
+
headers.append(("server", "a2a"))
|
|
155
|
+
|
|
156
|
+
import uvicorn
|
|
157
|
+
|
|
158
|
+
from agentstack_sdk.server.app import create_app
|
|
159
|
+
|
|
160
|
+
@asynccontextmanager
|
|
161
|
+
async def _lifespan_fn(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
162
|
+
async with self._self_registration_client or nullcontext():
|
|
163
|
+
register_task = asyncio.create_task(self._register_agent()) if self_registration else None
|
|
164
|
+
reload_task = asyncio.create_task(self._reload_variables_periodically()) if self_registration else None
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
async with lifespan_fn(app) if lifespan_fn else nullcontext(): # pyright: ignore [reportArgumentType]
|
|
168
|
+
yield
|
|
169
|
+
finally:
|
|
170
|
+
if register_task:
|
|
171
|
+
with suppress(Exception):
|
|
172
|
+
await cancel_task(register_task)
|
|
173
|
+
if reload_task:
|
|
174
|
+
with suppress(Exception):
|
|
175
|
+
await cancel_task(reload_task)
|
|
176
|
+
|
|
177
|
+
card_url = AnyUrl(self._agent.card.url)
|
|
178
|
+
if card_url.host == "invalid":
|
|
179
|
+
self._agent.card.url = f"http://{host}:{port}"
|
|
180
|
+
|
|
181
|
+
if self_registration:
|
|
182
|
+
from agentstack_sdk.a2a.extensions.services.platform import (
|
|
183
|
+
_PlatformSelfRegistrationExtensionParams,
|
|
184
|
+
_PlatformSelfRegistrationExtensionSpec,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
self._agent.card.capabilities.extensions = [
|
|
188
|
+
*(self._agent.card.capabilities.extensions or []),
|
|
189
|
+
*_PlatformSelfRegistrationExtensionSpec(
|
|
190
|
+
_PlatformSelfRegistrationExtensionParams(self_registration_id=self._self_registration_id)
|
|
191
|
+
).to_agent_card_extensions(),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
app = create_app(
|
|
195
|
+
self._agent,
|
|
196
|
+
lifespan=_lifespan_fn,
|
|
197
|
+
task_store=task_store,
|
|
198
|
+
context_store=context_store,
|
|
199
|
+
queue_manager=queue_manager,
|
|
200
|
+
push_config_store=push_config_store,
|
|
201
|
+
push_sender=push_sender,
|
|
202
|
+
task_timeout=task_timeout,
|
|
203
|
+
request_context_builder=request_context_builder,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if auth_backend:
|
|
207
|
+
|
|
208
|
+
def on_error(connection: HTTPConnection, error: AuthenticationError) -> PlainTextResponse:
|
|
209
|
+
return PlainTextResponse("Unauthorized", status_code=401)
|
|
210
|
+
|
|
211
|
+
app.add_middleware(AuthenticationMiddleware, backend=auth_backend, on_error=on_error)
|
|
212
|
+
|
|
213
|
+
if configure_logger:
|
|
214
|
+
configure_logger_func(log_level)
|
|
215
|
+
|
|
216
|
+
if configure_telemetry:
|
|
217
|
+
configure_telemetry_func(app)
|
|
218
|
+
|
|
219
|
+
config = uvicorn.Config(
|
|
220
|
+
app,
|
|
221
|
+
host,
|
|
222
|
+
port,
|
|
223
|
+
uds,
|
|
224
|
+
fd,
|
|
225
|
+
loop,
|
|
226
|
+
http,
|
|
227
|
+
ws,
|
|
228
|
+
ws_max_size,
|
|
229
|
+
ws_max_queue,
|
|
230
|
+
ws_ping_interval,
|
|
231
|
+
ws_ping_timeout,
|
|
232
|
+
ws_per_message_deflate,
|
|
233
|
+
lifespan,
|
|
234
|
+
env_file,
|
|
235
|
+
log_config if not configure_logger else None,
|
|
236
|
+
log_level,
|
|
237
|
+
access_log,
|
|
238
|
+
use_colors,
|
|
239
|
+
interface,
|
|
240
|
+
reload,
|
|
241
|
+
reload_dirs,
|
|
242
|
+
reload_delay,
|
|
243
|
+
reload_includes,
|
|
244
|
+
reload_excludes,
|
|
245
|
+
workers,
|
|
246
|
+
proxy_headers,
|
|
247
|
+
server_header,
|
|
248
|
+
date_header,
|
|
249
|
+
forwarded_allow_ips,
|
|
250
|
+
root_path,
|
|
251
|
+
limit_concurrency,
|
|
252
|
+
limit_max_requests,
|
|
253
|
+
backlog,
|
|
254
|
+
timeout_keep_alive,
|
|
255
|
+
timeout_notify,
|
|
256
|
+
timeout_graceful_shutdown,
|
|
257
|
+
timeout_worker_healthcheck,
|
|
258
|
+
callback_notify,
|
|
259
|
+
ssl_keyfile,
|
|
260
|
+
ssl_certfile,
|
|
261
|
+
ssl_keyfile_password,
|
|
262
|
+
ssl_version,
|
|
263
|
+
ssl_cert_reqs,
|
|
264
|
+
ssl_ca_certs,
|
|
265
|
+
ssl_ciphers,
|
|
266
|
+
headers,
|
|
267
|
+
factory,
|
|
268
|
+
h11_max_incomplete_event_size,
|
|
269
|
+
)
|
|
270
|
+
self.server = uvicorn.Server(config)
|
|
271
|
+
await self.server.serve()
|
|
272
|
+
|
|
273
|
+
@functools.wraps(serve)
|
|
274
|
+
def run(*args, **kwargs) -> None:
|
|
275
|
+
self = args[0] # Must hide self due to pyright issues
|
|
276
|
+
asyncio.run(self.serve(**kwargs))
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def should_exit(self) -> bool:
|
|
280
|
+
return self.server.should_exit if self.server else False
|
|
281
|
+
|
|
282
|
+
@should_exit.setter
|
|
283
|
+
def should_exit(self, value: bool) -> None:
|
|
284
|
+
if self.server:
|
|
285
|
+
self.server.should_exit = value
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def _platform_url(self) -> str:
|
|
289
|
+
return os.getenv("PLATFORM_URL", "http://127.0.0.1:8333")
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def _production_mode(self) -> bool:
|
|
293
|
+
return os.getenv("PRODUCTION_MODE", "").lower() in ["true", "1"]
|
|
294
|
+
|
|
295
|
+
async def _reload_variables_periodically(self):
|
|
296
|
+
while True:
|
|
297
|
+
await asyncio.sleep(5)
|
|
298
|
+
await self._load_variables()
|
|
299
|
+
|
|
300
|
+
async def _load_variables(self, first_run: bool = False) -> None:
|
|
301
|
+
from agentstack_sdk.a2a.extensions import AgentDetail, AgentDetailExtensionSpec
|
|
302
|
+
|
|
303
|
+
assert self.server and self._agent
|
|
304
|
+
if not self._provider_id:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
variables = await Provider.list_variables(self._provider_id, client=self._self_registration_client)
|
|
308
|
+
old_variables = self._all_configured_variables.copy()
|
|
309
|
+
|
|
310
|
+
for variable in list(self._all_configured_variables - variables.keys()): # reset removed variables
|
|
311
|
+
os.environ.pop(variable, None)
|
|
312
|
+
self._all_configured_variables.remove(variable)
|
|
313
|
+
|
|
314
|
+
os.environ.update(variables)
|
|
315
|
+
self._all_configured_variables.update(variables.keys())
|
|
316
|
+
|
|
317
|
+
if dirty := old_variables != self._all_configured_variables:
|
|
318
|
+
logger.info(f"Environment variables reloaded dynamically: {self._all_configured_variables}")
|
|
319
|
+
|
|
320
|
+
if first_run or dirty:
|
|
321
|
+
for extension in self._agent.card.capabilities.extensions or []:
|
|
322
|
+
match extension:
|
|
323
|
+
case AgentExtension(uri=AgentDetailExtensionSpec.URI, params=params):
|
|
324
|
+
variables = AgentDetail.model_validate(params).variables or []
|
|
325
|
+
if missing_keys := [env for env in variables if env.required and os.getenv(env.name) is None]:
|
|
326
|
+
logger.warning(
|
|
327
|
+
f"Missing required env variables: {missing_keys}, "
|
|
328
|
+
f"add them using `agentstack env add <agent> key=value`"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
async def _register_agent(self) -> None:
|
|
332
|
+
"""If not in PRODUCTION mode, register agent to the agentstack platform and provide missing env variables"""
|
|
333
|
+
assert self.server and self._agent
|
|
334
|
+
if self._production_mode:
|
|
335
|
+
logger.debug("Agent is not automatically registered in the production mode.")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
host = re.sub(r"localhost|127\.0\.0\.1", "host.docker.internal", self.server.config.host)
|
|
339
|
+
provider_location = f"http://{host}:{self.server.config.port}#{self._self_registration_id}"
|
|
340
|
+
logger.info("Registering agent to the agentstack platform")
|
|
341
|
+
try:
|
|
342
|
+
async for attempt in AsyncRetrying(
|
|
343
|
+
stop=stop_after_attempt(10),
|
|
344
|
+
wait=wait_exponential(max=10),
|
|
345
|
+
retry=retry_if_exception_type(HTTPError),
|
|
346
|
+
reraise=True,
|
|
347
|
+
):
|
|
348
|
+
with attempt:
|
|
349
|
+
try:
|
|
350
|
+
provider = await Provider.get_by_location(
|
|
351
|
+
location=provider_location, client=self._self_registration_client
|
|
352
|
+
)
|
|
353
|
+
await provider.patch(agent_card=self._agent.card, client=self._self_registration_client)
|
|
354
|
+
except HTTPStatusError as error:
|
|
355
|
+
if error.response.status_code != 404:
|
|
356
|
+
raise
|
|
357
|
+
provider = await Provider.create(
|
|
358
|
+
location=provider_location,
|
|
359
|
+
client=self._self_registration_client,
|
|
360
|
+
agent_card=self._agent.card,
|
|
361
|
+
)
|
|
362
|
+
self._provider_id = provider.id
|
|
363
|
+
logger.debug("Agent registered to the agentstack server.")
|
|
364
|
+
await self._load_variables()
|
|
365
|
+
logger.debug("Environment variables loaded dynamically.")
|
|
366
|
+
logger.info("Agent registered successfully")
|
|
367
|
+
except HTTPStatusError as e:
|
|
368
|
+
with suppress(Exception):
|
|
369
|
+
if error_message := e.response.json().get("detail"):
|
|
370
|
+
logger.info(f"Agent can not be registered to agentstack server: {error_message}")
|
|
371
|
+
return
|
|
372
|
+
logger.info(f"Agent can not be registered to agentstack server: {e}")
|
|
373
|
+
except HTTPError as e:
|
|
374
|
+
logger.info(f"Can not reach server, check if running on {get_platform_client().base_url} : {e}")
|
|
375
|
+
except Exception as e:
|
|
376
|
+
logger.info(f"Agent can not be registered to agentstack server: {e}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import abc
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from typing import TYPE_CHECKING, Protocol
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from a2a.types import Artifact, Message
|
|
12
|
+
|
|
13
|
+
from agentstack_sdk.platform.context import ContextHistoryItem
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from agentstack_sdk.server.dependencies import Dependency, Depends
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContextStoreInstance(Protocol):
|
|
20
|
+
async def load_history(
|
|
21
|
+
self, load_history_items: bool = False
|
|
22
|
+
) -> AsyncIterator[ContextHistoryItem | Message | Artifact]:
|
|
23
|
+
yield ... # type: ignore
|
|
24
|
+
|
|
25
|
+
async def store(self, data: Message | Artifact) -> None: ...
|
|
26
|
+
|
|
27
|
+
async def delete_history_from_id(self, from_id: UUID) -> None: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContextStore(abc.ABC):
|
|
31
|
+
def modify_dependencies(self, dependencies: dict[str, Depends]) -> None:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
async def create(self, context_id: str, initialized_dependencies: list[Dependency]) -> ContextStoreInstance: ...
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from a2a.types import Artifact, Message
|
|
9
|
+
from cachetools import TTLCache
|
|
10
|
+
|
|
11
|
+
from agentstack_sdk.platform.context import ContextHistoryItem
|
|
12
|
+
from agentstack_sdk.server.dependencies import Dependency
|
|
13
|
+
from agentstack_sdk.server.store.context_store import ContextStore, ContextStoreInstance
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MemoryContextStoreInstance(ContextStoreInstance):
|
|
17
|
+
def __init__(self, context_id: str):
|
|
18
|
+
self.context_id = context_id
|
|
19
|
+
self._history: list[ContextHistoryItem] = []
|
|
20
|
+
|
|
21
|
+
async def load_history(
|
|
22
|
+
self, load_history_items: bool = False
|
|
23
|
+
) -> AsyncIterator[ContextHistoryItem | Message | Artifact]:
|
|
24
|
+
for item in self._history.copy():
|
|
25
|
+
if load_history_items:
|
|
26
|
+
yield item.model_copy(deep=True)
|
|
27
|
+
else:
|
|
28
|
+
yield item.data.model_copy(deep=True)
|
|
29
|
+
|
|
30
|
+
async def store(self, data: Message | Artifact) -> None:
|
|
31
|
+
self._history.append(ContextHistoryItem(data=data.model_copy(deep=True), context_id=self.context_id))
|
|
32
|
+
|
|
33
|
+
async def delete_history_from_id(self, from_id: UUID) -> None:
|
|
34
|
+
# Does not allow to delete from an artifact onwards
|
|
35
|
+
index = next(
|
|
36
|
+
(i for i, item in enumerate(self._history) if item.id == from_id),
|
|
37
|
+
None,
|
|
38
|
+
)
|
|
39
|
+
if index is not None:
|
|
40
|
+
self._history = self._history[:index]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InMemoryContextStore(ContextStore):
|
|
44
|
+
def __init__(self, max_contexts: int = 1000, context_ttl: timedelta = timedelta(hours=1)):
|
|
45
|
+
"""
|
|
46
|
+
Initialize in-memory context store with TTL cache.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
max_contexts: Maximum number of contexts to keep in memory
|
|
50
|
+
ttl_seconds: Time-to-live for context instances in seconds (default: 1 hour)
|
|
51
|
+
"""
|
|
52
|
+
self._instances: TTLCache[str, MemoryContextStoreInstance] = TTLCache(
|
|
53
|
+
maxsize=max_contexts, ttl=context_ttl.total_seconds()
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def create(self, context_id: str, initialized_dependencies: list[Dependency]) -> ContextStoreInstance:
|
|
57
|
+
if context_id not in self._instances:
|
|
58
|
+
self._instances[context_id] = MemoryContextStoreInstance(context_id)
|
|
59
|
+
return self._instances[context_id]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from a2a.types import Artifact, Message
|
|
8
|
+
|
|
9
|
+
from agentstack_sdk.a2a.extensions.services.platform import (
|
|
10
|
+
PlatformApiExtensionServer,
|
|
11
|
+
PlatformApiExtensionSpec,
|
|
12
|
+
)
|
|
13
|
+
from agentstack_sdk.platform.context import Context, ContextHistoryItem
|
|
14
|
+
from agentstack_sdk.server.constants import _IMPLICIT_DEPENDENCY_PREFIX
|
|
15
|
+
from agentstack_sdk.server.dependencies import Dependency, Depends
|
|
16
|
+
from agentstack_sdk.server.store.context_store import ContextStore, ContextStoreInstance
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlatformContextStore(ContextStore):
|
|
20
|
+
def modify_dependencies(self, dependencies: dict[str, Depends]) -> None:
|
|
21
|
+
for dependency in dependencies.values():
|
|
22
|
+
if dependency.extension is None:
|
|
23
|
+
continue
|
|
24
|
+
if dependency.extension.spec.URI == PlatformApiExtensionSpec.URI:
|
|
25
|
+
dependency.extension.spec.required = True
|
|
26
|
+
break
|
|
27
|
+
else:
|
|
28
|
+
dependencies[f"{_IMPLICIT_DEPENDENCY_PREFIX}_{PlatformApiExtensionSpec.URI}"] = Depends(
|
|
29
|
+
PlatformApiExtensionServer(PlatformApiExtensionSpec())
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
async def create(self, context_id: str, initialized_dependencies: list[Dependency]) -> ContextStoreInstance:
|
|
33
|
+
[platform_ext] = [d for d in initialized_dependencies if isinstance(d, PlatformApiExtensionServer)]
|
|
34
|
+
return PlatformContextStoreInstance(context_id=context_id, platform_extension=platform_ext)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlatformContextStoreInstance(ContextStoreInstance):
|
|
38
|
+
def __init__(self, context_id: str, platform_extension: PlatformApiExtensionServer):
|
|
39
|
+
self._context_id = context_id
|
|
40
|
+
self._platform_extension = platform_extension
|
|
41
|
+
|
|
42
|
+
async def load_history(
|
|
43
|
+
self, load_history_items: bool = False
|
|
44
|
+
) -> AsyncIterator[ContextHistoryItem | Message | Artifact]:
|
|
45
|
+
async with self._platform_extension.use_client():
|
|
46
|
+
async for history_item in Context.list_all_history(self._context_id):
|
|
47
|
+
if load_history_items:
|
|
48
|
+
yield history_item
|
|
49
|
+
else:
|
|
50
|
+
yield history_item.data
|
|
51
|
+
|
|
52
|
+
async def store(self, data: Message | Artifact) -> None:
|
|
53
|
+
async with self._platform_extension.use_client():
|
|
54
|
+
await Context.add_history_item(self._context_id, data=data)
|
|
55
|
+
|
|
56
|
+
async def delete_history_from_id(self, from_id: UUID) -> None:
|
|
57
|
+
async with self._platform_extension.use_client():
|
|
58
|
+
await Context.delete_history_from_id(self._context_id, from_id=from_id)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from opentelemetry import metrics, trace
|
|
8
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
9
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
|
10
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
11
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
12
|
+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
13
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
14
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
15
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
16
|
+
from opentelemetry.sdk.resources import (
|
|
17
|
+
SERVICE_NAME,
|
|
18
|
+
SERVICE_VERSION,
|
|
19
|
+
Resource,
|
|
20
|
+
)
|
|
21
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
22
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
23
|
+
|
|
24
|
+
from agentstack_sdk import __version__
|
|
25
|
+
|
|
26
|
+
root_logger = logging.getLogger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def configure_telemetry(app: FastAPI) -> None:
|
|
30
|
+
"""Utility that configures opentelemetry with OTLP exporter and FastAPI instrumentation"""
|
|
31
|
+
|
|
32
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
33
|
+
|
|
34
|
+
resource = Resource(attributes={SERVICE_NAME: "agentstack-sdk-a2a-server", SERVICE_VERSION: __version__})
|
|
35
|
+
|
|
36
|
+
# Traces
|
|
37
|
+
provider = TracerProvider(resource=resource)
|
|
38
|
+
processor = BatchSpanProcessor(OTLPSpanExporter())
|
|
39
|
+
provider.add_span_processor(processor)
|
|
40
|
+
trace.set_tracer_provider(provider)
|
|
41
|
+
|
|
42
|
+
# Metrics
|
|
43
|
+
meter_provider = MeterProvider(
|
|
44
|
+
resource=resource,
|
|
45
|
+
metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())],
|
|
46
|
+
)
|
|
47
|
+
metrics.set_meter_provider(meter_provider)
|
|
48
|
+
|
|
49
|
+
# Logs
|
|
50
|
+
logger_provider = LoggerProvider(resource=resource)
|
|
51
|
+
processor = BatchLogRecordProcessor(OTLPLogExporter())
|
|
52
|
+
logger_provider.add_log_record_processor(processor)
|
|
53
|
+
root_logger.addHandler(LoggingHandler(logger_provider=logger_provider))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from asyncio import CancelledError
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
|
|
8
|
+
from a2a.server.events import QueueManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def cancel_task(task: asyncio.Task):
|
|
12
|
+
task.cancel()
|
|
13
|
+
with suppress(CancelledError):
|
|
14
|
+
await task
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def close_queue(queue_manager: QueueManager, queue_name: str, immediate: bool = False):
|
|
18
|
+
"""Closes a queue without blocking the QueueManager
|
|
19
|
+
|
|
20
|
+
By default, QueueManager.close() will block all QueueManager operations (creating new queues, etc)
|
|
21
|
+
until all queue events are processed. This can have unexpected side effects, we avoid this by closing queue
|
|
22
|
+
independently and then removing it from queue_manager
|
|
23
|
+
"""
|
|
24
|
+
if queue := await queue_manager.get(queue_name):
|
|
25
|
+
await queue.close(immediate=immediate)
|
|
26
|
+
await queue_manager.close(queue_name)
|
agentstack_sdk/types.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
JsonValue: TypeAlias = list["JsonValue"] | dict[str, "JsonValue"] | str | bool | int | float | None
|
|
8
|
+
JsonDict: TypeAlias = dict[str, JsonValue]
|
|
9
|
+
else:
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from typing_extensions import TypeAliasType
|
|
13
|
+
|
|
14
|
+
JsonValue = TypeAliasType("JsonValue", "Union[dict[str, JsonValue], list[JsonValue], str, int, float, bool, None]") # noqa: UP007
|
|
15
|
+
JsonDict = TypeAliasType("JsonDict", "dict[str, JsonValue]")
|