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,141 @@
|
|
|
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 os
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from types import NoneType
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import pydantic
|
|
13
|
+
from a2a.server.agent_execution.context import RequestContext
|
|
14
|
+
from a2a.types import Message as A2AMessage
|
|
15
|
+
from fastapi.security.utils import get_authorization_scheme_param
|
|
16
|
+
from pydantic.networks import HttpUrl
|
|
17
|
+
from typing_extensions import override
|
|
18
|
+
|
|
19
|
+
from agentstack_sdk.a2a.extensions.base import (
|
|
20
|
+
BaseExtensionClient,
|
|
21
|
+
BaseExtensionServer,
|
|
22
|
+
BaseExtensionSpec,
|
|
23
|
+
)
|
|
24
|
+
from agentstack_sdk.a2a.extensions.exceptions import ExtensionError
|
|
25
|
+
from agentstack_sdk.platform import use_platform_client
|
|
26
|
+
from agentstack_sdk.platform.client import PlatformClient
|
|
27
|
+
from agentstack_sdk.server.middleware.platform_auth_backend import PlatformAuthenticatedUser
|
|
28
|
+
from agentstack_sdk.util.httpx import BearerAuth
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from agentstack_sdk.server.context import RunContext
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PlatformApiExtensionMetadata(pydantic.BaseModel):
|
|
35
|
+
base_url: HttpUrl | None = None
|
|
36
|
+
auth_token: pydantic.Secret[str] | None = None
|
|
37
|
+
expires_at: pydantic.AwareDatetime | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PlatformApiExtension(pydantic.BaseModel):
|
|
41
|
+
"""
|
|
42
|
+
Request authentication token and url to be able to access the agentstack API
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PlatformApiExtensionParams(pydantic.BaseModel):
|
|
47
|
+
auto_use: bool = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PlatformApiExtensionSpec(BaseExtensionSpec[PlatformApiExtensionParams]):
|
|
51
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/services/platform_api/v1"
|
|
52
|
+
|
|
53
|
+
def __init__(self, params: PlatformApiExtensionParams | None = None) -> None:
|
|
54
|
+
super().__init__(params or PlatformApiExtensionParams())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PlatformApiExtensionServer(BaseExtensionServer[PlatformApiExtensionSpec, PlatformApiExtensionMetadata]):
|
|
58
|
+
context_id: str | None = None
|
|
59
|
+
|
|
60
|
+
@asynccontextmanager
|
|
61
|
+
@override
|
|
62
|
+
async def lifespan(self) -> AsyncIterator[None]:
|
|
63
|
+
"""Called when entering the agent context after the first message was parsed (__call__ was already called)"""
|
|
64
|
+
if self.data and self.spec.params.auto_use:
|
|
65
|
+
async with self.use_client():
|
|
66
|
+
yield
|
|
67
|
+
else:
|
|
68
|
+
yield
|
|
69
|
+
|
|
70
|
+
def _get_header_token(self, request_context: RequestContext) -> pydantic.Secret[str] | None:
|
|
71
|
+
header_token = None
|
|
72
|
+
call_context = request_context.call_context
|
|
73
|
+
assert call_context
|
|
74
|
+
if isinstance(call_context.user, PlatformAuthenticatedUser):
|
|
75
|
+
header_token = call_context.user.auth_token.get_secret_value()
|
|
76
|
+
elif auth_header := call_context.state.get("headers", {}).get("authorization", None):
|
|
77
|
+
_scheme, header_token = get_authorization_scheme_param(auth_header)
|
|
78
|
+
return pydantic.Secret(header_token) if header_token else None
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def handle_incoming_message(self, message: A2AMessage, run_context: RunContext, request_context: RequestContext):
|
|
82
|
+
super().handle_incoming_message(message, run_context, request_context)
|
|
83
|
+
# we assume that request context id is the same ID as the platform context id
|
|
84
|
+
# if different IDs are passed, api requests to platform using this token will fail
|
|
85
|
+
self.context_id = request_context.context_id
|
|
86
|
+
|
|
87
|
+
self._metadata_from_client = self._metadata_from_client or PlatformApiExtensionMetadata()
|
|
88
|
+
data = self._metadata_from_client
|
|
89
|
+
data.base_url = data.base_url or HttpUrl(os.getenv("PLATFORM_URL", "http://127.0.0.1:8333"))
|
|
90
|
+
data.auth_token = data.auth_token or self._get_header_token(request_context)
|
|
91
|
+
|
|
92
|
+
if not data.auth_token:
|
|
93
|
+
raise ExtensionError(self.spec, "Platform extension metadata was not provided")
|
|
94
|
+
|
|
95
|
+
@asynccontextmanager
|
|
96
|
+
async def use_client(self) -> AsyncIterator[PlatformClient]:
|
|
97
|
+
if not self.data or not self.data.auth_token:
|
|
98
|
+
raise ExtensionError(self.spec, "Platform extension metadata was not provided")
|
|
99
|
+
async with use_platform_client(
|
|
100
|
+
context_id=self.context_id,
|
|
101
|
+
base_url=str(self.data.base_url),
|
|
102
|
+
auth_token=self.data.auth_token.get_secret_value(),
|
|
103
|
+
) as client:
|
|
104
|
+
yield client
|
|
105
|
+
|
|
106
|
+
async def create_httpx_auth(self) -> BearerAuth:
|
|
107
|
+
if not self.data or not self.data.auth_token:
|
|
108
|
+
raise ExtensionError(self.spec, "Platform extension metadata was not provided")
|
|
109
|
+
return BearerAuth(token=self.data.auth_token.get_secret_value())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PlatformApiExtensionClient(BaseExtensionClient[PlatformApiExtensionSpec, NoneType]):
|
|
113
|
+
def api_auth_metadata(
|
|
114
|
+
self,
|
|
115
|
+
*,
|
|
116
|
+
auth_token: pydantic.Secret[str] | str,
|
|
117
|
+
expires_at: pydantic.AwareDatetime | None = None,
|
|
118
|
+
base_url: HttpUrl | None = None,
|
|
119
|
+
) -> dict[str, dict[str, str]]:
|
|
120
|
+
return {
|
|
121
|
+
self.spec.URI: {
|
|
122
|
+
**PlatformApiExtensionMetadata(
|
|
123
|
+
base_url=base_url,
|
|
124
|
+
auth_token=pydantic.Secret("replaced below"),
|
|
125
|
+
expires_at=expires_at,
|
|
126
|
+
).model_dump(mode="json"),
|
|
127
|
+
"auth_token": auth_token if isinstance(auth_token, str) else auth_token.get_secret_value(),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class _PlatformSelfRegistrationExtension(pydantic.BaseModel):
|
|
133
|
+
"""Internal extension"""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class _PlatformSelfRegistrationExtensionParams(pydantic.BaseModel):
|
|
137
|
+
self_registration_id: str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class _PlatformSelfRegistrationExtensionSpec(BaseExtensionSpec[_PlatformSelfRegistrationExtensionParams]):
|
|
141
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/services/platform-self-registration/v1"
|
|
@@ -0,0 +1,114 @@
|
|
|
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 uuid
|
|
7
|
+
from types import NoneType
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
9
|
+
|
|
10
|
+
import a2a.types
|
|
11
|
+
from mcp import Tool
|
|
12
|
+
from mcp.types import Implementation
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from agentstack_sdk.a2a.extensions.base import BaseExtensionClient, BaseExtensionServer, BaseExtensionSpec
|
|
16
|
+
from agentstack_sdk.a2a.extensions.tools.exceptions import ToolCallRejectionError
|
|
17
|
+
from agentstack_sdk.a2a.types import AgentMessage, InputRequired
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from agentstack_sdk.server.context import RunContext
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolCallServer(BaseModel):
|
|
24
|
+
name: str = Field(description="The programmatic name of the server.")
|
|
25
|
+
title: str | None = Field(description="A human-readable title for the server.")
|
|
26
|
+
version: str = Field(description="The version of the server.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ToolCallRequest(BaseModel):
|
|
30
|
+
name: str = Field(description="The programmatic name of the tool.")
|
|
31
|
+
title: str | None = Field(None, description="A human-readable title for the tool.")
|
|
32
|
+
description: str | None = Field(None, description="A human-readable description of the tool.")
|
|
33
|
+
|
|
34
|
+
input: dict[str, Any] | None = Field(description="The input for the tool.")
|
|
35
|
+
|
|
36
|
+
server: ToolCallServer | None = Field(None, description="The server executing the tool.")
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def from_mcp_tool(
|
|
40
|
+
tool: Tool, input: dict[str, Any] | None, server: Implementation | None = None
|
|
41
|
+
) -> ToolCallRequest:
|
|
42
|
+
return ToolCallRequest(
|
|
43
|
+
name=tool.name,
|
|
44
|
+
title=tool.annotations.title if tool.annotations else None,
|
|
45
|
+
description=tool.description,
|
|
46
|
+
input=input,
|
|
47
|
+
server=ToolCallServer(name=server.name, title=server.title, version=server.version) if server else None,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ToolCallResponse(BaseModel):
|
|
52
|
+
action: Literal["accept", "reject"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ToolCallExtensionParams(BaseModel):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ToolCallExtensionSpec(BaseExtensionSpec[ToolCallExtensionParams]):
|
|
60
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/tools/call/v1"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ToolCallExtensionMetadata(BaseModel):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ToolCallExtensionServer(BaseExtensionServer[ToolCallExtensionSpec, ToolCallExtensionMetadata]):
|
|
68
|
+
def create_request_message(self, *, request: ToolCallRequest):
|
|
69
|
+
return AgentMessage(
|
|
70
|
+
text="Tool call approval requested", metadata={self.spec.URI: request.model_dump(mode="json")}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def parse_response(self, *, message: a2a.types.Message):
|
|
74
|
+
if not message or not message.metadata or not (data := message.metadata.get(self.spec.URI)):
|
|
75
|
+
raise RuntimeError("Invalid mcp response")
|
|
76
|
+
return ToolCallResponse.model_validate(data)
|
|
77
|
+
|
|
78
|
+
async def request_tool_call_approval(
|
|
79
|
+
self,
|
|
80
|
+
request: ToolCallRequest,
|
|
81
|
+
*,
|
|
82
|
+
context: RunContext,
|
|
83
|
+
) -> ToolCallResponse:
|
|
84
|
+
message = self.create_request_message(request=request)
|
|
85
|
+
message = await context.yield_async(InputRequired(message=message))
|
|
86
|
+
if message:
|
|
87
|
+
result = self.parse_response(message=message)
|
|
88
|
+
match result.action:
|
|
89
|
+
case "accept":
|
|
90
|
+
return result
|
|
91
|
+
case "reject":
|
|
92
|
+
raise ToolCallRejectionError("User has rejected the tool call")
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
raise RuntimeError("Yield did not return a message")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ToolCallExtensionClient(BaseExtensionClient[ToolCallExtensionSpec, NoneType]):
|
|
99
|
+
def create_response_message(self, *, response: ToolCallResponse, task_id: str | None):
|
|
100
|
+
return a2a.types.Message(
|
|
101
|
+
message_id=str(uuid.uuid4()),
|
|
102
|
+
role=a2a.types.Role.user,
|
|
103
|
+
parts=[],
|
|
104
|
+
task_id=task_id,
|
|
105
|
+
metadata={self.spec.URI: response.model_dump(mode="json")},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def parse_request(self, *, message: a2a.types.Message):
|
|
109
|
+
if not message or not message.metadata or not (data := message.metadata.get(self.spec.URI)):
|
|
110
|
+
raise ValueError("Invalid tool call request")
|
|
111
|
+
return ToolCallRequest.model_validate(data)
|
|
112
|
+
|
|
113
|
+
def metadata(self) -> dict[str, Any]:
|
|
114
|
+
return {self.spec.URI: ToolCallExtensionMetadata().model_dump(mode="json")}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from .agent_detail import *
|
|
5
|
+
from .canvas import *
|
|
6
|
+
from .citation import *
|
|
7
|
+
from .error import *
|
|
8
|
+
from .form_request import *
|
|
9
|
+
from .settings import *
|
|
10
|
+
from .trajectory import *
|
|
@@ -0,0 +1,54 @@
|
|
|
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 types import NoneType
|
|
8
|
+
|
|
9
|
+
import pydantic
|
|
10
|
+
|
|
11
|
+
from agentstack_sdk.a2a.extensions.base import BaseExtensionClient, BaseExtensionServer, BaseExtensionSpec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentDetailTool(pydantic.BaseModel):
|
|
15
|
+
name: str
|
|
16
|
+
description: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentDetailContributor(pydantic.BaseModel):
|
|
20
|
+
name: str
|
|
21
|
+
email: str | None = None
|
|
22
|
+
url: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EnvVar(pydantic.BaseModel):
|
|
26
|
+
name: str
|
|
27
|
+
description: str | None = None
|
|
28
|
+
required: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentDetail(pydantic.BaseModel, extra="allow"):
|
|
32
|
+
interaction_mode: str | None = pydantic.Field("multi-turn", examples=["multi-turn", "single-turn"])
|
|
33
|
+
user_greeting: str | None = None
|
|
34
|
+
input_placeholder: str | None = None
|
|
35
|
+
tools: list[AgentDetailTool] | None = None
|
|
36
|
+
framework: str | None = None
|
|
37
|
+
license: str | None = None
|
|
38
|
+
programming_language: str | None = "Python"
|
|
39
|
+
homepage_url: str | None = None
|
|
40
|
+
source_code_url: str | None = None
|
|
41
|
+
container_image_url: str | None = None
|
|
42
|
+
author: AgentDetailContributor | None = None
|
|
43
|
+
contributors: list[AgentDetailContributor] | None = None
|
|
44
|
+
variables: list[EnvVar] | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentDetailExtensionSpec(BaseExtensionSpec[AgentDetail]):
|
|
48
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/agent-detail/v1"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AgentDetailExtensionServer(BaseExtensionServer[AgentDetailExtensionSpec, NoneType]): ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AgentDetailExtensionClient(BaseExtensionClient[AgentDetailExtensionSpec, AgentDetail]): ...
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
|
7
|
+
|
|
8
|
+
import pydantic
|
|
9
|
+
from a2a.server.agent_execution.context import RequestContext
|
|
10
|
+
from a2a.types import Artifact, TextPart
|
|
11
|
+
from a2a.types import Message as A2AMessage
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from agentstack_sdk.server.context import RunContext
|
|
16
|
+
|
|
17
|
+
from agentstack_sdk.a2a.extensions.base import (
|
|
18
|
+
BaseExtensionServer,
|
|
19
|
+
NoParamsBaseExtensionSpec,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CanvasEditRequestMetadata(pydantic.BaseModel):
|
|
24
|
+
start_index: int
|
|
25
|
+
end_index: int
|
|
26
|
+
description: str
|
|
27
|
+
artifact_id: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CanvasEditRequest(pydantic.BaseModel):
|
|
31
|
+
start_index: int
|
|
32
|
+
end_index: int
|
|
33
|
+
description: str
|
|
34
|
+
artifact: Artifact
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CanvasExtensionSpec(NoParamsBaseExtensionSpec):
|
|
38
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/canvas/v1"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CanvasExtensionServer(BaseExtensionServer[CanvasExtensionSpec, CanvasEditRequestMetadata]):
|
|
42
|
+
@override
|
|
43
|
+
def handle_incoming_message(self, message: A2AMessage, run_context: RunContext, request_context: RequestContext):
|
|
44
|
+
if message.metadata and self.spec.URI in message.metadata and message.parts:
|
|
45
|
+
message.parts = [part for part in message.parts if not isinstance(part.root, TextPart)]
|
|
46
|
+
|
|
47
|
+
super().handle_incoming_message(message, run_context, request_context)
|
|
48
|
+
self.context = run_context
|
|
49
|
+
|
|
50
|
+
async def parse_canvas_edit_request(self, *, message: A2AMessage) -> CanvasEditRequest | None:
|
|
51
|
+
if not message or not message.metadata or not (data := message.metadata.get(self.spec.URI)):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
metadata = CanvasEditRequestMetadata.model_validate(data)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
artifact = await anext(
|
|
58
|
+
artifact
|
|
59
|
+
async for artifact in self.context.load_history()
|
|
60
|
+
if isinstance(artifact, Artifact) and artifact.parts
|
|
61
|
+
if artifact.artifact_id == metadata.artifact_id
|
|
62
|
+
)
|
|
63
|
+
except StopAsyncIteration as e:
|
|
64
|
+
raise ValueError(f"Artifact {metadata.artifact_id} not found in history") from e
|
|
65
|
+
|
|
66
|
+
return CanvasEditRequest(
|
|
67
|
+
start_index=metadata.start_index,
|
|
68
|
+
end_index=metadata.end_index,
|
|
69
|
+
description=metadata.description,
|
|
70
|
+
artifact=artifact,
|
|
71
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
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 Citation(pydantic.BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Represents an inline citation, providing info about information source. This
|
|
22
|
+
is supposed to be rendered as an inline icon, optionally marking a text
|
|
23
|
+
range it belongs to.
|
|
24
|
+
|
|
25
|
+
If Citation is included together with content in the message part,
|
|
26
|
+
the citation belongs to that content and renders at the Part position.
|
|
27
|
+
This way may be used for non-text content, like images and files.
|
|
28
|
+
|
|
29
|
+
Alternatively, `start_index` and `end_index` may define a text range,
|
|
30
|
+
counting characters in the current Message across all Parts containing plain
|
|
31
|
+
text, where the citation will be rendered. If one of `start_index` and
|
|
32
|
+
`end_index` is missing or their values are equal, the citation renders only
|
|
33
|
+
as an inline icon at that position.
|
|
34
|
+
|
|
35
|
+
If both `start_index` and `end_index` are not present and Part has empty
|
|
36
|
+
content, the citation renders as inline icon only at the Part position.
|
|
37
|
+
|
|
38
|
+
Properties:
|
|
39
|
+
- url: URL of the source document.
|
|
40
|
+
- title: Title of the source document.
|
|
41
|
+
- description: Accompanying text, which may be a general description of the
|
|
42
|
+
source document, or a specific snippet.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
start_index: int | None = None
|
|
46
|
+
end_index: int | None = None
|
|
47
|
+
url: str | None = None
|
|
48
|
+
title: str | None = None
|
|
49
|
+
description: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CitationMetadata(pydantic.BaseModel):
|
|
53
|
+
citations: list[Citation] = pydantic.Field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CitationExtensionSpec(NoParamsBaseExtensionSpec):
|
|
57
|
+
URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/citation/v1"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CitationExtensionServer(BaseExtensionServer[CitationExtensionSpec, NoneType]):
|
|
61
|
+
def citation_metadata(self, *, citations: list[Citation]) -> Metadata:
|
|
62
|
+
return Metadata({self.spec.URI: CitationMetadata(citations=citations).model_dump(mode="json")})
|
|
63
|
+
|
|
64
|
+
def message(
|
|
65
|
+
self,
|
|
66
|
+
text: str | None = None,
|
|
67
|
+
parts: list[Part | TextPart | FilePart | DataPart] | None = None,
|
|
68
|
+
*,
|
|
69
|
+
citations: list[Citation],
|
|
70
|
+
) -> AgentMessage:
|
|
71
|
+
return AgentMessage(
|
|
72
|
+
text=text,
|
|
73
|
+
parts=parts or [],
|
|
74
|
+
metadata=self.citation_metadata(citations=citations),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CitationExtensionClient(BaseExtensionClient[CitationExtensionSpec, CitationMetadata]): ...
|