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.
Files changed (76) hide show
  1. agentstack_sdk/__init__.py +6 -0
  2. agentstack_sdk/a2a/__init__.py +2 -0
  3. agentstack_sdk/a2a/extensions/__init__.py +8 -0
  4. agentstack_sdk/a2a/extensions/auth/__init__.py +5 -0
  5. agentstack_sdk/a2a/extensions/auth/oauth/__init__.py +4 -0
  6. agentstack_sdk/a2a/extensions/auth/oauth/oauth.py +151 -0
  7. agentstack_sdk/a2a/extensions/auth/oauth/storage/__init__.py +5 -0
  8. agentstack_sdk/a2a/extensions/auth/oauth/storage/base.py +11 -0
  9. agentstack_sdk/a2a/extensions/auth/oauth/storage/memory.py +38 -0
  10. agentstack_sdk/a2a/extensions/auth/secrets/__init__.py +4 -0
  11. agentstack_sdk/a2a/extensions/auth/secrets/secrets.py +77 -0
  12. agentstack_sdk/a2a/extensions/base.py +205 -0
  13. agentstack_sdk/a2a/extensions/common/__init__.py +4 -0
  14. agentstack_sdk/a2a/extensions/common/form.py +149 -0
  15. agentstack_sdk/a2a/extensions/exceptions.py +11 -0
  16. agentstack_sdk/a2a/extensions/interactions/__init__.py +4 -0
  17. agentstack_sdk/a2a/extensions/interactions/approval.py +125 -0
  18. agentstack_sdk/a2a/extensions/services/__init__.py +8 -0
  19. agentstack_sdk/a2a/extensions/services/embedding.py +106 -0
  20. agentstack_sdk/a2a/extensions/services/form.py +54 -0
  21. agentstack_sdk/a2a/extensions/services/llm.py +100 -0
  22. agentstack_sdk/a2a/extensions/services/mcp.py +193 -0
  23. agentstack_sdk/a2a/extensions/services/platform.py +141 -0
  24. agentstack_sdk/a2a/extensions/tools/__init__.py +5 -0
  25. agentstack_sdk/a2a/extensions/tools/call.py +114 -0
  26. agentstack_sdk/a2a/extensions/tools/exceptions.py +6 -0
  27. agentstack_sdk/a2a/extensions/ui/__init__.py +10 -0
  28. agentstack_sdk/a2a/extensions/ui/agent_detail.py +54 -0
  29. agentstack_sdk/a2a/extensions/ui/canvas.py +71 -0
  30. agentstack_sdk/a2a/extensions/ui/citation.py +78 -0
  31. agentstack_sdk/a2a/extensions/ui/error.py +223 -0
  32. agentstack_sdk/a2a/extensions/ui/form_request.py +52 -0
  33. agentstack_sdk/a2a/extensions/ui/settings.py +73 -0
  34. agentstack_sdk/a2a/extensions/ui/trajectory.py +70 -0
  35. agentstack_sdk/a2a/types.py +104 -0
  36. agentstack_sdk/platform/__init__.py +12 -0
  37. agentstack_sdk/platform/client.py +123 -0
  38. agentstack_sdk/platform/common.py +37 -0
  39. agentstack_sdk/platform/configuration.py +47 -0
  40. agentstack_sdk/platform/context.py +291 -0
  41. agentstack_sdk/platform/file.py +295 -0
  42. agentstack_sdk/platform/model_provider.py +131 -0
  43. agentstack_sdk/platform/provider.py +219 -0
  44. agentstack_sdk/platform/provider_build.py +190 -0
  45. agentstack_sdk/platform/types.py +45 -0
  46. agentstack_sdk/platform/user.py +70 -0
  47. agentstack_sdk/platform/user_feedback.py +42 -0
  48. agentstack_sdk/platform/variables.py +44 -0
  49. agentstack_sdk/platform/vector_store.py +217 -0
  50. agentstack_sdk/py.typed +0 -0
  51. agentstack_sdk/server/__init__.py +4 -0
  52. agentstack_sdk/server/agent.py +594 -0
  53. agentstack_sdk/server/app.py +87 -0
  54. agentstack_sdk/server/constants.py +9 -0
  55. agentstack_sdk/server/context.py +68 -0
  56. agentstack_sdk/server/dependencies.py +117 -0
  57. agentstack_sdk/server/exceptions.py +3 -0
  58. agentstack_sdk/server/middleware/__init__.py +3 -0
  59. agentstack_sdk/server/middleware/platform_auth_backend.py +131 -0
  60. agentstack_sdk/server/server.py +376 -0
  61. agentstack_sdk/server/store/__init__.py +3 -0
  62. agentstack_sdk/server/store/context_store.py +35 -0
  63. agentstack_sdk/server/store/memory_context_store.py +59 -0
  64. agentstack_sdk/server/store/platform_context_store.py +58 -0
  65. agentstack_sdk/server/telemetry.py +53 -0
  66. agentstack_sdk/server/utils.py +26 -0
  67. agentstack_sdk/types.py +15 -0
  68. agentstack_sdk/util/__init__.py +4 -0
  69. agentstack_sdk/util/file.py +260 -0
  70. agentstack_sdk/util/httpx.py +18 -0
  71. agentstack_sdk/util/logging.py +63 -0
  72. agentstack_sdk/util/resource_context.py +44 -0
  73. agentstack_sdk/util/utils.py +47 -0
  74. agentstack_sdk-0.5.2rc2.dist-info/METADATA +120 -0
  75. agentstack_sdk-0.5.2rc2.dist-info/RECORD +76 -0
  76. 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,5 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .call import *
5
+ from .exceptions import *
@@ -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,6 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ class ToolCallRejectionError(RuntimeError):
6
+ pass
@@ -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]): ...