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,223 @@
|
|
|
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 json
|
|
7
|
+
import logging
|
|
8
|
+
import traceback
|
|
9
|
+
from collections.abc import AsyncIterator
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from types import NoneType
|
|
12
|
+
from typing import Any, Final
|
|
13
|
+
|
|
14
|
+
import pydantic
|
|
15
|
+
|
|
16
|
+
from agentstack_sdk.a2a.extensions.base import (
|
|
17
|
+
BaseExtensionClient,
|
|
18
|
+
BaseExtensionServer,
|
|
19
|
+
BaseExtensionSpec,
|
|
20
|
+
)
|
|
21
|
+
from agentstack_sdk.a2a.types import AgentMessage, Metadata
|
|
22
|
+
from agentstack_sdk.types import JsonValue
|
|
23
|
+
from agentstack_sdk.util import resource_context
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Error(pydantic.BaseModel):
|
|
29
|
+
"""
|
|
30
|
+
Represents error information for displaying exceptions in the UI.
|
|
31
|
+
|
|
32
|
+
This extension helps display errors in a user-friendly way with:
|
|
33
|
+
- A clear error title (exception type)
|
|
34
|
+
- A descriptive error message
|
|
35
|
+
|
|
36
|
+
Visually, this may appear as an error card in the UI.
|
|
37
|
+
|
|
38
|
+
Properties:
|
|
39
|
+
- title: Title of the error (typically the exception class name).
|
|
40
|
+
- message: The error message describing what went wrong.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
title: str
|
|
44
|
+
message: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ErrorGroup(pydantic.BaseModel):
|
|
48
|
+
"""
|
|
49
|
+
Represents a group of errors.
|
|
50
|
+
|
|
51
|
+
Properties:
|
|
52
|
+
- message: A message describing the group of errors.
|
|
53
|
+
- errors: A list of error objects.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
message: str
|
|
57
|
+
errors: list[Error]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ErrorMetadata(pydantic.BaseModel):
|
|
61
|
+
"""
|
|
62
|
+
Metadata containing an error (or group of errors) and an optional stack trace.
|
|
63
|
+
|
|
64
|
+
Properties:
|
|
65
|
+
- error: The error object or group of errors.
|
|
66
|
+
- stack_trace: Optional formatted stack trace for debugging.
|
|
67
|
+
- context: Optional context dictionary.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
error: Error | ErrorGroup
|
|
71
|
+
stack_trace: str | None = None
|
|
72
|
+
context: JsonValue | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ErrorExtensionParams(pydantic.BaseModel):
|
|
76
|
+
"""
|
|
77
|
+
Configuration parameters for the error extension.
|
|
78
|
+
|
|
79
|
+
Properties:
|
|
80
|
+
- include_stacktrace: Whether to include stack traces in error messages (default: False).
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
include_stacktrace: bool = False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ErrorExtensionSpec(BaseExtensionSpec[ErrorExtensionParams]):
|
|
87
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/error/v1"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_stacktrace(exc: BaseException, include_cause: bool = True) -> str:
|
|
91
|
+
"""Format exception with full traceback including nested causes."""
|
|
92
|
+
return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__, chain=include_cause))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_error(exc: BaseException) -> Error | ErrorGroup:
|
|
96
|
+
"""
|
|
97
|
+
Extract error information from an exception, handling:
|
|
98
|
+
- BaseExceptionGroup (returns ErrorGroup)
|
|
99
|
+
- FrameworkError from beeai_framework (uses .explain() method)
|
|
100
|
+
"""
|
|
101
|
+
# Handle BaseExceptionGroup by recursively extracting errors from each exception
|
|
102
|
+
if isinstance(exc, BaseExceptionGroup):
|
|
103
|
+
errors: list[Error] = []
|
|
104
|
+
for sub_exc in exc.exceptions:
|
|
105
|
+
extracted = _extract_error(sub_exc)
|
|
106
|
+
if isinstance(extracted, ErrorGroup):
|
|
107
|
+
errors.extend(extracted.errors)
|
|
108
|
+
else:
|
|
109
|
+
errors.append(extracted)
|
|
110
|
+
return ErrorGroup(message=str(exc), errors=errors)
|
|
111
|
+
|
|
112
|
+
# Try to handle FrameworkError if beeai_framework is available
|
|
113
|
+
try:
|
|
114
|
+
from beeai_framework.errors import FrameworkError
|
|
115
|
+
|
|
116
|
+
if isinstance(exc, FrameworkError):
|
|
117
|
+
# FrameworkError has special .explain() method
|
|
118
|
+
return Error(title=exc.name(), message=exc.explain())
|
|
119
|
+
except ImportError:
|
|
120
|
+
# beeai_framework not installed, continue with standard handling
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
return Error(title=type(exc).__name__, message=str(exc))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ErrorExtensionServer(BaseExtensionServer[ErrorExtensionSpec, NoneType]):
|
|
127
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
128
|
+
super().__init__(*args, **kwargs)
|
|
129
|
+
|
|
130
|
+
@asynccontextmanager
|
|
131
|
+
async def lifespan(self) -> AsyncIterator[None]:
|
|
132
|
+
"""Set up request-scoped error context using ContextVar."""
|
|
133
|
+
with use_error_extension_context(server=self, context={}):
|
|
134
|
+
yield
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def context(self) -> JsonValue:
|
|
138
|
+
"""Get the current request's error context."""
|
|
139
|
+
try:
|
|
140
|
+
return get_error_extension_context().context
|
|
141
|
+
except LookupError:
|
|
142
|
+
# Fallback for when lifespan hasn't been entered yet
|
|
143
|
+
logger.warning(
|
|
144
|
+
"Attempted to use error context when the error extension is not initialized. Make sure to add the ErrorExtensionServer to the agent dependencies."
|
|
145
|
+
)
|
|
146
|
+
return {}
|
|
147
|
+
|
|
148
|
+
def error_metadata(self, error: BaseException) -> Metadata:
|
|
149
|
+
"""
|
|
150
|
+
Create metadata for an error.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
error: The exception to convert to metadata
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Metadata dictionary with error information
|
|
157
|
+
"""
|
|
158
|
+
error_data = _extract_error(error)
|
|
159
|
+
stack_trace = _format_stacktrace(error) if self.spec.params.include_stacktrace else None
|
|
160
|
+
return Metadata(
|
|
161
|
+
{
|
|
162
|
+
self.spec.URI: ErrorMetadata(
|
|
163
|
+
error=error_data,
|
|
164
|
+
stack_trace=stack_trace,
|
|
165
|
+
context=self.context or None,
|
|
166
|
+
).model_dump(mode="json")
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def message(
|
|
171
|
+
self,
|
|
172
|
+
error: BaseException,
|
|
173
|
+
) -> AgentMessage:
|
|
174
|
+
"""
|
|
175
|
+
Create an AgentMessage with error metadata and serialized text representation.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
error: The exception to include in the message
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
AgentMessage with error metadata and markdown-formatted text
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
metadata = self.error_metadata(error)
|
|
185
|
+
error_metadata = ErrorMetadata.model_validate(metadata[self.spec.URI])
|
|
186
|
+
|
|
187
|
+
# Serialize to markdown for display
|
|
188
|
+
text_lines: list[str] = []
|
|
189
|
+
if isinstance(error_metadata.error, ErrorGroup):
|
|
190
|
+
text_lines.append(f"## {error_metadata.error.message}\n")
|
|
191
|
+
for err in error_metadata.error.errors:
|
|
192
|
+
text_lines.append(f"### {err.title}\n{err.message}")
|
|
193
|
+
else:
|
|
194
|
+
text_lines.append(f"## {error_metadata.error.title}\n{error_metadata.error.message}")
|
|
195
|
+
|
|
196
|
+
# Add context if present
|
|
197
|
+
if error_metadata.context:
|
|
198
|
+
text_lines.append(f"## Context\n```json\n{json.dumps(error_metadata.context, indent=2)}\n```")
|
|
199
|
+
|
|
200
|
+
if error_metadata.stack_trace:
|
|
201
|
+
text_lines.append(f"## Stack Trace\n```\n{error_metadata.stack_trace}\n```")
|
|
202
|
+
|
|
203
|
+
text = "\n\n".join(text_lines)
|
|
204
|
+
|
|
205
|
+
return AgentMessage(text=text, metadata=metadata)
|
|
206
|
+
except Exception as error_exc:
|
|
207
|
+
return AgentMessage(text=f"Failed to create error message: {error_exc!s}\noriginal exc: {error!s}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ErrorExtensionClient(BaseExtensionClient[ErrorExtensionSpec, ErrorMetadata]): ...
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
DEFAULT_ERROR_EXTENSION: Final = ErrorExtensionServer(ErrorExtensionSpec(ErrorExtensionParams()))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ErrorContext(pydantic.BaseModel, arbitrary_types_allowed=True):
|
|
217
|
+
server: ErrorExtensionServer = pydantic.Field(default=DEFAULT_ERROR_EXTENSION)
|
|
218
|
+
context: JsonValue = pydantic.Field(default_factory=dict)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
get_error_extension_context, use_error_extension_context = resource_context(
|
|
222
|
+
factory=ErrorContext, default_factory=ErrorContext
|
|
223
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
from typing import TYPE_CHECKING, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from a2a.server.agent_execution.context import RequestContext
|
|
9
|
+
from a2a.types import Message as A2AMessage
|
|
10
|
+
from pydantic import TypeAdapter
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from agentstack_sdk.a2a.extensions.base import (
|
|
14
|
+
BaseExtensionClient,
|
|
15
|
+
BaseExtensionServer,
|
|
16
|
+
NoParamsBaseExtensionSpec,
|
|
17
|
+
)
|
|
18
|
+
from agentstack_sdk.a2a.extensions.common.form import FormRender, FormResponse
|
|
19
|
+
from agentstack_sdk.a2a.types import AgentMessage, InputRequired
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from agentstack_sdk.server.context import RunContext
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FormRequestExtensionSpec(NoParamsBaseExtensionSpec):
|
|
28
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/form_request/v1"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FormRequestExtensionServer(BaseExtensionServer[FormRequestExtensionSpec, FormResponse]):
|
|
32
|
+
@override
|
|
33
|
+
def handle_incoming_message(self, message: A2AMessage, run_context: RunContext, request_context: RequestContext):
|
|
34
|
+
super().handle_incoming_message(message, run_context, request_context)
|
|
35
|
+
self.context = run_context
|
|
36
|
+
|
|
37
|
+
async def request_form(self, *, form: FormRender, model: type[T] = FormResponse) -> T | None:
|
|
38
|
+
message = await self.context.yield_async(
|
|
39
|
+
InputRequired(message=AgentMessage(text=form.title, metadata={self.spec.URI: form}))
|
|
40
|
+
)
|
|
41
|
+
return self.parse_form_response(message=message, model=model) if message else None
|
|
42
|
+
|
|
43
|
+
def parse_form_response(self, *, message: A2AMessage, model: type[T] = FormResponse) -> T | None:
|
|
44
|
+
form_response = self.parse_client_metadata(message)
|
|
45
|
+
if form_response is None:
|
|
46
|
+
return None
|
|
47
|
+
if model is FormResponse:
|
|
48
|
+
return cast(T, form_response)
|
|
49
|
+
return TypeAdapter(model).validate_python(dict(form_response))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FormRequestExtensionClient(BaseExtensionClient[FormRequestExtensionSpec, FormRender]): ...
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from agentstack_sdk.a2a.extensions.base import BaseExtensionClient, BaseExtensionServer, BaseExtensionSpec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CheckboxField(BaseModel):
|
|
15
|
+
id: str
|
|
16
|
+
label: str
|
|
17
|
+
default_value: bool = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CheckboxGroupField(BaseModel):
|
|
21
|
+
id: str
|
|
22
|
+
type: Literal["checkbox_group"] = "checkbox_group"
|
|
23
|
+
fields: list[CheckboxField]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OptionItem(BaseModel):
|
|
27
|
+
label: str
|
|
28
|
+
value: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SingleSelectField(BaseModel):
|
|
32
|
+
type: Literal["single_select"] = "single_select"
|
|
33
|
+
id: str
|
|
34
|
+
label: str
|
|
35
|
+
options: list[OptionItem]
|
|
36
|
+
default_value: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SettingsRender(BaseModel):
|
|
40
|
+
fields: list[CheckboxGroupField | SingleSelectField]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CheckboxFieldValue(BaseModel):
|
|
44
|
+
value: bool | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CheckboxGroupFieldValue(BaseModel):
|
|
48
|
+
type: Literal["checkbox_group"] = "checkbox_group"
|
|
49
|
+
values: dict[str, CheckboxFieldValue]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SingleSelectFieldValue(BaseModel):
|
|
53
|
+
type: Literal["single_select"] = "single_select"
|
|
54
|
+
value: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
SettingsFieldValue = CheckboxGroupFieldValue | SingleSelectFieldValue
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AgentRunSettings(BaseModel):
|
|
61
|
+
values: dict[str, SettingsFieldValue]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SettingsExtensionSpec(BaseExtensionSpec[SettingsRender | None]):
|
|
65
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/settings/v1"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SettingsExtensionServer(BaseExtensionServer[SettingsExtensionSpec, AgentRunSettings]):
|
|
69
|
+
def parse_settings_response(self) -> AgentRunSettings:
|
|
70
|
+
return AgentRunSettings.model_validate(self._metadata_from_client)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SettingsExtensionClient(BaseExtensionClient[SettingsExtensionSpec, SettingsRender]): ...
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
from types import NoneType
|
|
7
|
+
|
|
8
|
+
import pydantic
|
|
9
|
+
from a2a.types import DataPart, FilePart, Part, TextPart
|
|
10
|
+
|
|
11
|
+
from agentstack_sdk.a2a.extensions.base import (
|
|
12
|
+
BaseExtensionClient,
|
|
13
|
+
BaseExtensionServer,
|
|
14
|
+
NoParamsBaseExtensionSpec,
|
|
15
|
+
)
|
|
16
|
+
from agentstack_sdk.a2a.types import AgentMessage, Metadata
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Trajectory(pydantic.BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Represents trajectory information for an agent's reasoning or tool execution
|
|
22
|
+
steps. Helps track the agent's decision-making process and provides
|
|
23
|
+
transparency into how the agent arrived at its response.
|
|
24
|
+
|
|
25
|
+
Trajectory can capture intermediate steps like:
|
|
26
|
+
- A reasoning step with a message
|
|
27
|
+
- A tool execution with tool name, input, and output
|
|
28
|
+
|
|
29
|
+
This information can be used for debugging, audit trails, and providing
|
|
30
|
+
users with insight into the agent's thought process.
|
|
31
|
+
|
|
32
|
+
Visually, this may appear as an accordion component in the UI.
|
|
33
|
+
|
|
34
|
+
Properties:
|
|
35
|
+
- title: Title of the trajectory update.
|
|
36
|
+
- content: Markdown-formatted content of the trajectory update.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
title: str | None = None
|
|
40
|
+
content: str | None = None
|
|
41
|
+
group_id: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TrajectoryExtensionSpec(NoParamsBaseExtensionSpec):
|
|
45
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/trajectory/v1"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TrajectoryExtensionServer(BaseExtensionServer[TrajectoryExtensionSpec, NoneType]):
|
|
49
|
+
def trajectory_metadata(
|
|
50
|
+
self, *, title: str | None = None, content: str | None = None, group_id: str | None = None
|
|
51
|
+
) -> Metadata:
|
|
52
|
+
return Metadata(
|
|
53
|
+
{self.spec.URI: Trajectory(title=title, content=content, group_id=group_id).model_dump(mode="json")}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def message(
|
|
57
|
+
self,
|
|
58
|
+
text: str | None = None,
|
|
59
|
+
parts: list[Part | TextPart | FilePart | DataPart] | None = None,
|
|
60
|
+
trajectory_title: str | None = None,
|
|
61
|
+
trajectory_content: str | None = None,
|
|
62
|
+
) -> AgentMessage:
|
|
63
|
+
return AgentMessage(
|
|
64
|
+
text=text,
|
|
65
|
+
parts=parts or [],
|
|
66
|
+
metadata=self.trajectory_metadata(title=trajectory_title, content=trajectory_content),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TrajectoryExtensionClient(BaseExtensionClient[TrajectoryExtensionSpec, Trajectory]): ...
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Literal, TypeAlias
|
|
5
|
+
|
|
6
|
+
from a2a.types import (
|
|
7
|
+
Artifact,
|
|
8
|
+
DataPart,
|
|
9
|
+
FilePart,
|
|
10
|
+
FileWithBytes,
|
|
11
|
+
FileWithUri,
|
|
12
|
+
Message,
|
|
13
|
+
Part,
|
|
14
|
+
Role,
|
|
15
|
+
TaskArtifactUpdateEvent,
|
|
16
|
+
TaskState,
|
|
17
|
+
TaskStatus,
|
|
18
|
+
TaskStatusUpdateEvent,
|
|
19
|
+
TextPart,
|
|
20
|
+
)
|
|
21
|
+
from pydantic import Field, model_validator
|
|
22
|
+
|
|
23
|
+
from agentstack_sdk.types import JsonDict, JsonValue
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Metadata(dict[str, JsonValue]): ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
RunYield: TypeAlias = (
|
|
30
|
+
Message # includes AgentMessage (subclass)
|
|
31
|
+
| Part
|
|
32
|
+
| TaskStatus # includes InputRequired and AuthRequired (subclasses)
|
|
33
|
+
| Artifact
|
|
34
|
+
| TextPart
|
|
35
|
+
| FilePart
|
|
36
|
+
| FileWithBytes
|
|
37
|
+
| FileWithUri
|
|
38
|
+
| Metadata
|
|
39
|
+
| DataPart
|
|
40
|
+
| TaskStatusUpdateEvent
|
|
41
|
+
| TaskArtifactUpdateEvent
|
|
42
|
+
| str
|
|
43
|
+
| JsonDict
|
|
44
|
+
| Exception
|
|
45
|
+
)
|
|
46
|
+
RunYieldResume: TypeAlias = Message | None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgentArtifact(Artifact):
|
|
50
|
+
artifact_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
51
|
+
parts: list[Part | TextPart | FilePart | DataPart]
|
|
52
|
+
|
|
53
|
+
@model_validator(mode="after")
|
|
54
|
+
def text_message_validate(self):
|
|
55
|
+
self.parts = [part if isinstance(part, Part) else Part(root=part) for part in self.parts] # pyright: ignore [reportIncompatibleVariableOverride]
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ArtifactChunk(Artifact):
|
|
60
|
+
last_chunk: bool = False
|
|
61
|
+
parts: list[Part | TextPart | FilePart | DataPart]
|
|
62
|
+
|
|
63
|
+
@model_validator(mode="after")
|
|
64
|
+
def text_message_validate(self):
|
|
65
|
+
self.parts = [part if isinstance(part, Part) else Part(root=part) for part in self.parts] # pyright: ignore [reportIncompatibleVariableOverride]
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AgentMessage(Message):
|
|
70
|
+
message_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
71
|
+
role: Literal[Role.agent] = Role.agent # pyright: ignore [reportIncompatibleVariableOverride]
|
|
72
|
+
text: str | None = Field(default=None, exclude=True)
|
|
73
|
+
parts: list[Part | TextPart | FilePart | DataPart] = Field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
@model_validator(mode="after")
|
|
76
|
+
def text_message_validate(self):
|
|
77
|
+
self.parts = [part if isinstance(part, Part) else Part(root=part) for part in self.parts] # pyright: ignore [reportIncompatibleVariableOverride]
|
|
78
|
+
if self.parts and self.text is not None:
|
|
79
|
+
raise ValueError("Message cannot have both parts and text")
|
|
80
|
+
if self.text is not None:
|
|
81
|
+
self.parts.append(Part(root=TextPart(text=self.text)))
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class InputRequired(TaskStatus):
|
|
86
|
+
message: Message | None = None
|
|
87
|
+
state: Literal[TaskState.input_required] = TaskState.input_required # pyright: ignore [reportIncompatibleVariableOverride]
|
|
88
|
+
text: str | None = Field(default=None, exclude=True)
|
|
89
|
+
parts: list[Part | TextPart | DataPart | FilePart] = Field(exclude=True, default_factory=list)
|
|
90
|
+
|
|
91
|
+
@model_validator(mode="after")
|
|
92
|
+
def text_message_validate(self):
|
|
93
|
+
self.parts = [part if isinstance(part, Part) else Part(root=part) for part in self.parts]
|
|
94
|
+
if sum((self.message is not None, self.text is not None, bool(self.parts))) != 1:
|
|
95
|
+
raise ValueError("At most one of message, text, or parts must be provided.")
|
|
96
|
+
if self.text is not None:
|
|
97
|
+
self.message = AgentMessage(text=self.text)
|
|
98
|
+
elif self.parts:
|
|
99
|
+
self.message = AgentMessage(parts=self.parts)
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AuthRequired(InputRequired):
|
|
104
|
+
state: Literal[TaskState.auth_required] = TaskState.auth_required # pyright: ignore [reportIncompatibleVariableOverride]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from .client import *
|
|
5
|
+
from .configuration import *
|
|
6
|
+
from .file import *
|
|
7
|
+
from .model_provider import *
|
|
8
|
+
from .provider import *
|
|
9
|
+
from .provider_build import *
|
|
10
|
+
from .user import *
|
|
11
|
+
from .user_feedback import *
|
|
12
|
+
from .vector_store import *
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import os
|
|
6
|
+
import ssl
|
|
7
|
+
import typing
|
|
8
|
+
from collections.abc import AsyncIterator, Mapping
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from httpx import URL, AsyncBaseTransport
|
|
13
|
+
from httpx._client import EventHook
|
|
14
|
+
from httpx._config import DEFAULT_LIMITS, DEFAULT_MAX_REDIRECTS, Limits
|
|
15
|
+
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes, ProxyTypes, QueryParamTypes, TimeoutTypes
|
|
16
|
+
from pydantic import Secret
|
|
17
|
+
from typing_extensions import override
|
|
18
|
+
|
|
19
|
+
from agentstack_sdk.util import resource_context
|
|
20
|
+
|
|
21
|
+
DEFAULT_SDK_TIMEOUT: typing.Final = httpx.Timeout(timeout=30, read=None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlatformClient(httpx.AsyncClient):
|
|
25
|
+
context_id: str | None = None
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
context_id: str | None = None, # Enter context scope
|
|
30
|
+
auth_token: str | Secret[str] | None = None,
|
|
31
|
+
*,
|
|
32
|
+
auth: AuthTypes | None = None,
|
|
33
|
+
params: QueryParamTypes | None = None,
|
|
34
|
+
headers: HeaderTypes | None = None,
|
|
35
|
+
cookies: CookieTypes | None = None,
|
|
36
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
37
|
+
cert: CertTypes | None = None,
|
|
38
|
+
http1: bool = True,
|
|
39
|
+
http2: bool = False,
|
|
40
|
+
proxy: ProxyTypes | None = None,
|
|
41
|
+
mounts: None | (Mapping[str, AsyncBaseTransport | None]) = None,
|
|
42
|
+
timeout: TimeoutTypes = DEFAULT_SDK_TIMEOUT,
|
|
43
|
+
follow_redirects: bool = False,
|
|
44
|
+
limits: Limits = DEFAULT_LIMITS,
|
|
45
|
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
|
46
|
+
event_hooks: None | (Mapping[str, list[EventHook]]) = None,
|
|
47
|
+
base_url: URL | str = "",
|
|
48
|
+
transport: AsyncBaseTransport | None = None,
|
|
49
|
+
trust_env: bool = True,
|
|
50
|
+
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
|
51
|
+
) -> None:
|
|
52
|
+
if not base_url:
|
|
53
|
+
base_url = os.environ.get("PLATFORM_URL", "http://127.0.0.1:8333")
|
|
54
|
+
super().__init__(
|
|
55
|
+
auth=auth,
|
|
56
|
+
params=params,
|
|
57
|
+
headers=headers,
|
|
58
|
+
cookies=cookies,
|
|
59
|
+
verify=verify,
|
|
60
|
+
cert=cert,
|
|
61
|
+
http1=http1,
|
|
62
|
+
http2=http2,
|
|
63
|
+
proxy=proxy,
|
|
64
|
+
mounts=mounts,
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
follow_redirects=follow_redirects,
|
|
67
|
+
limits=limits,
|
|
68
|
+
max_redirects=max_redirects,
|
|
69
|
+
event_hooks=event_hooks,
|
|
70
|
+
base_url=base_url,
|
|
71
|
+
transport=transport,
|
|
72
|
+
trust_env=trust_env,
|
|
73
|
+
default_encoding=default_encoding,
|
|
74
|
+
)
|
|
75
|
+
self.context_id = context_id
|
|
76
|
+
if auth_token:
|
|
77
|
+
self.headers["Authorization"] = f"Bearer {auth_token}"
|
|
78
|
+
self._ref_count: int = 0
|
|
79
|
+
self._context_manager_lock: asyncio.Lock = asyncio.Lock()
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
async def __aenter__(self) -> typing.Self:
|
|
83
|
+
async with self._context_manager_lock:
|
|
84
|
+
self._ref_count += 1
|
|
85
|
+
if self._ref_count == 1:
|
|
86
|
+
_ = await super().__aenter__()
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
async def __aexit__(
|
|
91
|
+
self,
|
|
92
|
+
exc_type: type[BaseException] | None = None,
|
|
93
|
+
exc_value: BaseException | None = None,
|
|
94
|
+
traceback: TracebackType | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
async with self._context_manager_lock:
|
|
97
|
+
self._ref_count -= 1
|
|
98
|
+
if self._ref_count == 0:
|
|
99
|
+
await super().__aexit__(exc_type, exc_value, traceback)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
get_platform_client, set_platform_client = resource_context(factory=PlatformClient, default_factory=PlatformClient)
|
|
103
|
+
|
|
104
|
+
P = typing.ParamSpec("P")
|
|
105
|
+
T = typing.TypeVar("T", bound=PlatformClient)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def wrap_context(
|
|
109
|
+
context: typing.Callable[P, contextlib.AbstractContextManager[T]],
|
|
110
|
+
) -> typing.Callable[P, contextlib.AbstractAsyncContextManager[T]]:
|
|
111
|
+
@contextlib.asynccontextmanager
|
|
112
|
+
async def use_async_resource(*args: P.args, **kwargs: P.kwargs) -> AsyncIterator[T]:
|
|
113
|
+
with context(*args, **kwargs) as resource:
|
|
114
|
+
async with resource:
|
|
115
|
+
yield resource
|
|
116
|
+
|
|
117
|
+
return use_async_resource
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
use_platform_client = wrap_context(set_platform_client)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__ = ["PlatformClient", "get_platform_client", "set_platform_client", "use_platform_client"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T", bound=BaseModel)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PaginatedResult(BaseModel, Generic[T]):
|
|
12
|
+
items: list[T]
|
|
13
|
+
total_count: int
|
|
14
|
+
has_more: bool = False
|
|
15
|
+
next_page_token: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GithubVersionType(StrEnum):
|
|
19
|
+
HEAD = "head"
|
|
20
|
+
TAG = "tag"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ResolvedGithubUrl(BaseModel):
|
|
24
|
+
host: str = "github.com"
|
|
25
|
+
org: str
|
|
26
|
+
repo: str
|
|
27
|
+
version: str
|
|
28
|
+
version_type: GithubVersionType
|
|
29
|
+
commit_hash: str
|
|
30
|
+
path: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ResolvedDockerImageID(BaseModel):
|
|
34
|
+
registry: str
|
|
35
|
+
repository: str
|
|
36
|
+
tag: str
|
|
37
|
+
digest: str
|