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,131 @@
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 enum import StrEnum
7
+
8
+ import pydantic
9
+
10
+ from agentstack_sdk.platform.client import PlatformClient, get_platform_client
11
+
12
+
13
+ class ModelProviderType(StrEnum):
14
+ ANTHROPIC = "anthropic"
15
+ CEREBRAS = "cerebras"
16
+ CHUTES = "chutes"
17
+ COHERE = "cohere"
18
+ DEEPSEEK = "deepseek"
19
+ GEMINI = "gemini"
20
+ GITHUB = "github"
21
+ GROQ = "groq"
22
+ WATSONX = "watsonx"
23
+ JAN = "jan"
24
+ MISTRAL = "mistral"
25
+ MOONSHOT = "moonshot"
26
+ NVIDIA = "nvidia"
27
+ OLLAMA = "ollama"
28
+ OPENAI = "openai"
29
+ OPENROUTER = "openrouter"
30
+ PERPLEXITY = "perplexity"
31
+ TOGETHER = "together"
32
+ VOYAGE = "voyage"
33
+ RITS = "rits"
34
+ OTHER = "other"
35
+
36
+
37
+ class ModelWithScore(pydantic.BaseModel):
38
+ model_id: str
39
+ score: float
40
+
41
+
42
+ class ModelCapability(StrEnum):
43
+ LLM = "llm"
44
+ EMBEDDING = "embedding"
45
+
46
+
47
+ class ModelProvider(pydantic.BaseModel):
48
+ id: str
49
+ name: str | None = None
50
+ description: str | None = None
51
+ type: ModelProviderType
52
+ base_url: pydantic.HttpUrl
53
+ watsonx_project_id: str | None = None
54
+ watsonx_space_id: str | None = None
55
+ created_at: pydantic.AwareDatetime
56
+ capabilities: set[ModelCapability]
57
+
58
+ @staticmethod
59
+ async def create(
60
+ *,
61
+ name: str | None = None,
62
+ description: str | None = None,
63
+ type: ModelProviderType,
64
+ base_url: str | pydantic.HttpUrl,
65
+ watsonx_project_id: str | None = None,
66
+ watsonx_space_id: str | None = None,
67
+ api_key: str,
68
+ client: PlatformClient | None = None,
69
+ ) -> ModelProvider:
70
+ async with client or get_platform_client() as client:
71
+ return pydantic.TypeAdapter(ModelProvider).validate_python(
72
+ (
73
+ await client.post(
74
+ url="/api/v1/model_providers",
75
+ json={
76
+ "name": name,
77
+ "description": description,
78
+ "type": type,
79
+ "base_url": str(base_url),
80
+ "watsonx_project_id": watsonx_project_id,
81
+ "watsonx_space_id": watsonx_space_id,
82
+ "api_key": api_key,
83
+ },
84
+ )
85
+ )
86
+ .raise_for_status()
87
+ .json()
88
+ )
89
+
90
+ async def get(self: ModelProvider | str, *, client: PlatformClient | None = None) -> ModelProvider:
91
+ model_provider_id = self if isinstance(self, str) else self.id
92
+ async with client or get_platform_client() as client:
93
+ result = pydantic.TypeAdapter(ModelProvider).validate_python(
94
+ (await client.get(url=f"/api/v1/model_providers/{model_provider_id}")).raise_for_status().json()
95
+ )
96
+ if isinstance(self, ModelProvider):
97
+ self.__dict__.update(result.__dict__)
98
+ return self
99
+ return result
100
+
101
+ async def delete(self: ModelProvider | str, *, client: PlatformClient | None = None) -> None:
102
+ # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `File.get("123")` to obtain a new instance
103
+ model_provider_id = self if isinstance(self, str) else self.id
104
+ async with client or get_platform_client() as client:
105
+ _ = (await client.delete(f"/api/v1/model_providers/{model_provider_id}")).raise_for_status()
106
+
107
+ @staticmethod
108
+ async def match(
109
+ *,
110
+ capability: ModelCapability = ModelCapability.LLM,
111
+ suggested_models: tuple[str, ...] | None = None,
112
+ client: PlatformClient | None = None,
113
+ ) -> list[ModelWithScore]:
114
+ async with client or get_platform_client() as client:
115
+ return pydantic.TypeAdapter(list[ModelWithScore]).validate_python(
116
+ (
117
+ await client.post(
118
+ "/api/v1/model_providers/match",
119
+ json={"suggested_models": suggested_models, "capability": capability},
120
+ )
121
+ )
122
+ .raise_for_status()
123
+ .json()["items"]
124
+ )
125
+
126
+ @staticmethod
127
+ async def list(*, client: PlatformClient | None = None) -> list[ModelProvider]:
128
+ async with client or get_platform_client() as client:
129
+ return pydantic.TypeAdapter(list[ModelProvider]).validate_python(
130
+ (await client.get(url="/api/v1/model_providers")).raise_for_status().json()["items"]
131
+ )
@@ -0,0 +1,219 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ import typing
6
+ import urllib.parse
7
+ from contextlib import asynccontextmanager
8
+ from datetime import timedelta
9
+ from uuid import UUID
10
+
11
+ import pydantic
12
+ from a2a.client import ClientConfig, ClientFactory
13
+ from a2a.types import AgentCard
14
+
15
+ from agentstack_sdk.platform.client import PlatformClient, get_platform_client
16
+ from agentstack_sdk.platform.common import ResolvedDockerImageID, ResolvedGithubUrl
17
+ from agentstack_sdk.util.utils import filter_dict, parse_stream
18
+
19
+
20
+ class ProviderErrorMessage(pydantic.BaseModel):
21
+ message: str
22
+
23
+
24
+ class EnvVar(pydantic.BaseModel):
25
+ name: str
26
+ description: str | None = None
27
+ required: bool = False
28
+
29
+
30
+ class VersionInfo(pydantic.BaseModel):
31
+ docker: ResolvedDockerImageID | None = None
32
+ github: ResolvedGithubUrl | None = None
33
+
34
+
35
+ class Provider(pydantic.BaseModel):
36
+ id: str
37
+ auto_stop_timeout: timedelta
38
+ source: str
39
+ origin: str
40
+ version_info: VersionInfo = pydantic.Field(default_factory=VersionInfo)
41
+ registry: str | None = None
42
+ created_at: pydantic.AwareDatetime
43
+ updated_at: pydantic.AwareDatetime
44
+ last_active_at: pydantic.AwareDatetime
45
+ agent_card: AgentCard
46
+ state: typing.Literal["missing", "starting", "ready", "running", "error", "online", "offline"] = "missing"
47
+ managed: bool
48
+ last_error: ProviderErrorMessage | None = None
49
+ created_by: UUID
50
+ missing_configuration: list[EnvVar] = pydantic.Field(default_factory=list)
51
+
52
+ @staticmethod
53
+ async def create(
54
+ *,
55
+ location: str,
56
+ agent_card: AgentCard | None = None,
57
+ origin: str | None = None,
58
+ auto_stop_timeout: timedelta | None = None,
59
+ variables: dict[str, str] | None = None,
60
+ client: PlatformClient | None = None,
61
+ ) -> "Provider":
62
+ auto_stop_timeout_sec = auto_stop_timeout.total_seconds() if auto_stop_timeout is not None else None
63
+
64
+ async with client or get_platform_client() as client:
65
+ return pydantic.TypeAdapter(Provider).validate_python(
66
+ (
67
+ await client.post(
68
+ url="/api/v1/providers",
69
+ json=filter_dict(
70
+ {
71
+ "location": location,
72
+ "agent_card": agent_card.model_dump(mode="json") if agent_card else None,
73
+ "origin": origin,
74
+ "variables": variables,
75
+ "auto_stop_timeout_sec": auto_stop_timeout_sec,
76
+ }
77
+ ),
78
+ )
79
+ )
80
+ .raise_for_status()
81
+ .json()
82
+ )
83
+
84
+ async def patch(
85
+ self: "Provider | str",
86
+ *,
87
+ location: str | None = None,
88
+ agent_card: AgentCard | None = None,
89
+ origin: str | None = None,
90
+ auto_stop_timeout: timedelta | None = None,
91
+ variables: dict[str, str] | None = None,
92
+ client: PlatformClient | None = None,
93
+ ) -> "Provider":
94
+ # `self` has a weird type so that you can call both `instance.patch()` to update an instance, or `Provider.patch("123", ...)` to update a provider
95
+
96
+ provider_id = self if isinstance(self, str) else self.id
97
+ payload = filter_dict(
98
+ {
99
+ "location": location,
100
+ "agent_card": agent_card.model_dump(mode="json") if agent_card else None,
101
+ "variables": variables,
102
+ "auto_stop_timeout_sec": None if auto_stop_timeout is None else auto_stop_timeout.total_seconds(),
103
+ "origin": origin,
104
+ }
105
+ )
106
+ if not payload:
107
+ return await Provider.get(self)
108
+
109
+ async with client or get_platform_client() as client:
110
+ return pydantic.TypeAdapter(Provider).validate_python(
111
+ (await client.patch(url=f"/api/v1/providers/{provider_id}", json=payload)).raise_for_status().json()
112
+ )
113
+
114
+ @asynccontextmanager
115
+ async def a2a_client(self, client: PlatformClient | None = None):
116
+ async with client or get_platform_client() as client:
117
+ yield ClientFactory(ClientConfig(httpx_client=client)).create(card=self.agent_card)
118
+
119
+ @staticmethod
120
+ async def preview(
121
+ *,
122
+ location: str,
123
+ agent_card: AgentCard | None = None,
124
+ client: PlatformClient | None = None,
125
+ ) -> "Provider":
126
+ async with client or get_platform_client() as client:
127
+ return pydantic.TypeAdapter(Provider).validate_python(
128
+ (
129
+ await client.post(
130
+ url="/api/v1/providers/preview",
131
+ json={
132
+ "location": location,
133
+ "agent_card": agent_card.model_dump(mode="json") if agent_card else None,
134
+ },
135
+ )
136
+ )
137
+ .raise_for_status()
138
+ .json()
139
+ )
140
+
141
+ async def get(self: "Provider | str", *, client: PlatformClient | None = None) -> "Provider":
142
+ # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `Provider.get("123")` to obtain a new instance
143
+ provider_id = self if isinstance(self, str) else self.id
144
+ async with client or get_platform_client() as client:
145
+ result = pydantic.TypeAdapter(Provider).validate_json(
146
+ (await client.get(url=f"/api/v1/providers/{provider_id}")).raise_for_status().content
147
+ )
148
+ if isinstance(self, Provider):
149
+ self.__dict__.update(result.__dict__)
150
+ return self
151
+ return result
152
+
153
+ @staticmethod
154
+ async def get_by_location(*, location: str, client: PlatformClient | None = None) -> "Provider":
155
+ async with client or get_platform_client() as client:
156
+ return pydantic.TypeAdapter(Provider).validate_json(
157
+ (await client.get(url=f"/api/v1/providers/by-location/{urllib.parse.quote(location, safe='')}"))
158
+ .raise_for_status()
159
+ .content
160
+ )
161
+
162
+ async def delete(self: "Provider | str", *, client: PlatformClient | None = None) -> None:
163
+ # `self` has a weird type so that you can call both `instance.delete()` or `Provider.delete("123")`
164
+ provider_id = self if isinstance(self, str) else self.id
165
+ async with client or get_platform_client() as client:
166
+ _ = (await client.delete(f"/api/v1/providers/{provider_id}")).raise_for_status()
167
+
168
+ async def update_variables(
169
+ self: "Provider | str",
170
+ *,
171
+ variables: dict[str, str | None] | dict[str, str],
172
+ client: PlatformClient | None = None,
173
+ ) -> None:
174
+ # `self` has a weird type so that you can call both `instance.delete()` or `Provider.delete("123")`
175
+ provider_id = self if isinstance(self, str) else self.id
176
+ async with client or get_platform_client() as client:
177
+ _ = (
178
+ await client.put(f"/api/v1/providers/{provider_id}/variables", json={"variables": variables})
179
+ ).raise_for_status()
180
+
181
+ async def stream_logs(
182
+ self: "Provider| str", *, client: PlatformClient | None = None
183
+ ) -> typing.AsyncIterator[dict[str, typing.Any]]:
184
+ # `self` has a weird type so that you can call both `instance.stream_logs()` or `ProviderBuild.stream_logs("123")`
185
+ provider_id = self if isinstance(self, str) else self.id
186
+ async with (
187
+ client or get_platform_client() as client,
188
+ client.stream(
189
+ "GET",
190
+ url=f"/api/v1/providers/{provider_id}/logs",
191
+ timeout=timedelta(hours=1).total_seconds(),
192
+ ) as response,
193
+ ):
194
+ async for line in parse_stream(response):
195
+ yield line
196
+
197
+ async def list_variables(self: "Provider | str", *, client: PlatformClient | None = None) -> dict[str, str]:
198
+ # `self` has a weird type so that you can call both `instance.delete()` or `Provider.delete("123")`
199
+ provider_id = self if isinstance(self, str) else self.id
200
+ async with client or get_platform_client() as client:
201
+ result = await client.get(f"/api/v1/providers/{provider_id}/variables")
202
+ return result.raise_for_status().json()["variables"]
203
+
204
+ @staticmethod
205
+ async def list(
206
+ *, origin: str | None = None, user_owned: bool | None = None, client: PlatformClient | None = None
207
+ ) -> list["Provider"]:
208
+ async with client or get_platform_client() as client:
209
+ params = filter_dict({"origin": origin, "user_owned": user_owned})
210
+ return pydantic.TypeAdapter(list[Provider]).validate_python(
211
+ (
212
+ await client.get(
213
+ url="/api/v1/providers",
214
+ params=params,
215
+ )
216
+ )
217
+ .raise_for_status()
218
+ .json()["items"]
219
+ )
@@ -0,0 +1,190 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from datetime import timedelta
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+ from typing import Any, Literal, TypeAlias
10
+ from uuid import UUID
11
+
12
+ import pydantic
13
+ from pydantic import Field
14
+
15
+ from agentstack_sdk.platform.client import PlatformClient, get_platform_client
16
+ from agentstack_sdk.platform.common import PaginatedResult, ResolvedGithubUrl
17
+ from agentstack_sdk.util.utils import filter_dict, parse_stream
18
+
19
+
20
+ class BuildState(StrEnum):
21
+ MISSING = "missing"
22
+ IN_PROGRESS = "in_progress"
23
+ BUILD_COMPLETED = "build_completed"
24
+ COMPLETED = "completed"
25
+ FAILED = "failed"
26
+
27
+
28
+ class AddProvider(pydantic.BaseModel):
29
+ """
30
+ Will add a new provider or update an existing one with the same base docker image ID
31
+ (docker registry + repository, excluding tag)
32
+ """
33
+
34
+ type: Literal["add_provider"] = "add_provider"
35
+ auto_stop_timeout_sec: int | None = pydantic.Field(
36
+ default=None,
37
+ gt=0,
38
+ le=600,
39
+ description=(
40
+ "Timeout after which the agent provider will be automatically downscaled if unused."
41
+ "Contact administrator if you need to increase this value."
42
+ ),
43
+ )
44
+ variables: dict[str, str] | None = None
45
+
46
+
47
+ class UpdateProvider(pydantic.BaseModel):
48
+ """Will update provider specified by ID"""
49
+
50
+ type: Literal["update_provider"] = "update_provider"
51
+ provider_id: UUID
52
+
53
+
54
+ class NoAction(pydantic.BaseModel):
55
+ type: Literal["no_action"] = "no_action"
56
+
57
+
58
+ class BuildConfiguration(pydantic.BaseModel):
59
+ dockerfile_path: Path | None = Field(
60
+ default=None,
61
+ description=(
62
+ "Path to Dockerfile relative to the repository path "
63
+ "(provider_build.source.path or repository root if not defined)"
64
+ ),
65
+ )
66
+
67
+
68
+ OnCompleteAction: TypeAlias = AddProvider | UpdateProvider | NoAction
69
+
70
+
71
+ class ProviderBuild(pydantic.BaseModel):
72
+ id: str
73
+ created_at: pydantic.AwareDatetime
74
+ status: BuildState
75
+ source: ResolvedGithubUrl
76
+ destination: str
77
+ provider_id: str | None = None
78
+ build_configuration: BuildConfiguration | None = None
79
+ created_by: str
80
+ error_message: str | None = None
81
+ provider_origin: str
82
+
83
+ @staticmethod
84
+ async def create(
85
+ *,
86
+ location: str,
87
+ client: PlatformClient | None = None,
88
+ on_complete: OnCompleteAction | None = None,
89
+ build_configuration: BuildConfiguration | None = None,
90
+ ) -> ProviderBuild:
91
+ on_complete = on_complete or NoAction()
92
+ async with client or get_platform_client() as client:
93
+ return pydantic.TypeAdapter(ProviderBuild).validate_python(
94
+ (
95
+ await client.post(
96
+ url="/api/v1/provider_builds",
97
+ json={
98
+ "location": location,
99
+ "on_complete": on_complete.model_dump(exclude_none=True, mode="json"),
100
+ "build_configuration": build_configuration.model_dump(exclude_none=True, mode="json")
101
+ if build_configuration
102
+ else None,
103
+ },
104
+ )
105
+ )
106
+ .raise_for_status()
107
+ .json()
108
+ )
109
+
110
+ @staticmethod
111
+ async def preview(
112
+ *, location: str, client: PlatformClient | None = None, on_complete: OnCompleteAction | None = None
113
+ ) -> ProviderBuild:
114
+ on_complete = on_complete or NoAction()
115
+ async with client or get_platform_client() as client:
116
+ return pydantic.TypeAdapter(ProviderBuild).validate_python(
117
+ (
118
+ await client.post(
119
+ url="/api/v1/provider_builds/preview",
120
+ json={"location": location, "on_complete": on_complete.model_dump(exclude_none=True)},
121
+ )
122
+ )
123
+ .raise_for_status()
124
+ .json()
125
+ )
126
+
127
+ async def stream_logs(
128
+ self: ProviderBuild | str, *, client: PlatformClient | None = None
129
+ ) -> AsyncIterator[dict[str, Any]]:
130
+ # `self` has a weird type so that you can call both `instance.stream_logs()` or `ProviderBuild.stream_logs("123")`
131
+ provider_build_id = self if isinstance(self, str) else self.id
132
+ async with (
133
+ client or get_platform_client() as client,
134
+ client.stream(
135
+ "GET",
136
+ url=f"/api/v1/provider_builds/{provider_build_id}/logs",
137
+ timeout=timedelta(hours=1).total_seconds(),
138
+ ) as response,
139
+ ):
140
+ async for line in parse_stream(response):
141
+ yield line
142
+
143
+ async def get(self: ProviderBuild | str, *, client: PlatformClient | None = None) -> ProviderBuild:
144
+ # `self` has a weird type so that you can call both `instance.get()` to update an instance, or `ProviderBuild.get("123")` to obtain a new instance
145
+ provider_build_id = self if isinstance(self, str) else self.id
146
+ async with client or get_platform_client() as client:
147
+ result = pydantic.TypeAdapter(ProviderBuild).validate_json(
148
+ (await client.get(url=f"/api/v1/provider_builds/{provider_build_id}")).raise_for_status().content
149
+ )
150
+ if isinstance(self, ProviderBuild):
151
+ self.__dict__.update(result.__dict__)
152
+ return self
153
+ return result
154
+
155
+ async def delete(self: ProviderBuild | str, *, client: PlatformClient | None = None) -> None:
156
+ # `self` has a weird type so that you can call both `instance.delete()` or `ProviderBuild.delete("123")`
157
+ provider_build_id = self if isinstance(self, str) else self.id
158
+ async with client or get_platform_client() as client:
159
+ _ = (await client.delete(f"/api/v1/provider_builds/{provider_build_id}")).raise_for_status()
160
+
161
+ @staticmethod
162
+ async def list(
163
+ *,
164
+ page_token: str | None = None,
165
+ limit: int | None = None,
166
+ order: Literal["asc"] | Literal["desc"] | None = "asc",
167
+ order_by: Literal["created_at"] | Literal["updated_at"] | None = None,
168
+ user_owned: bool | None = None,
169
+ client: PlatformClient | None = None,
170
+ ) -> PaginatedResult[ProviderBuild]:
171
+ # `self` has a weird type so that you can call both `instance.list_history()` or `ProviderBuild.list_history("123")`
172
+ async with client or get_platform_client() as platform_client:
173
+ return pydantic.TypeAdapter(PaginatedResult[ProviderBuild]).validate_python(
174
+ (
175
+ await platform_client.get(
176
+ url="/api/v1/provider_builds",
177
+ params=filter_dict(
178
+ {
179
+ "page_token": page_token,
180
+ "limit": limit,
181
+ "order": order,
182
+ "order_by": order_by,
183
+ "user_owned": user_owned,
184
+ }
185
+ ),
186
+ )
187
+ )
188
+ .raise_for_status()
189
+ .json()
190
+ )
@@ -0,0 +1,45 @@
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 typing
7
+ from textwrap import dedent
8
+
9
+ import pydantic
10
+ from pydantic import AfterValidator, Field
11
+
12
+
13
+ def validate_metadata(metadata: dict[str, str | None]) -> dict[str, str | None]:
14
+ if len(metadata) > 16:
15
+ raise ValueError("Metadata must be less than 16 keys.")
16
+ if any(len(k) > 64 for k in metadata):
17
+ raise ValueError("Metadata keys must be less than 64 characters.")
18
+ if any(len(v) > 512 for v in metadata.values() if v is not None):
19
+ raise ValueError("Metadata values must be less than 512 characters.")
20
+ return metadata
21
+
22
+
23
+ Metadata = typing.Annotated[
24
+ dict[str, str],
25
+ pydantic.Field(
26
+ description=dedent(
27
+ """
28
+ Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional
29
+ information about the object in a structured format, and querying for objects via API or the dashboard.
30
+
31
+ Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of
32
+ 512 characters.
33
+ """,
34
+ )
35
+ ),
36
+ pydantic.AfterValidator(validate_metadata),
37
+ ]
38
+
39
+ MetadataPatch = typing.Annotated[
40
+ dict[str, str | None],
41
+ Field(
42
+ description="Metadata update, will add or overwrite existing keys, None will delete the key from metadata.",
43
+ ),
44
+ AfterValidator(validate_metadata),
45
+ ]
@@ -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 enum import StrEnum
7
+
8
+ import pydantic
9
+
10
+ from agentstack_sdk.platform.client import PlatformClient, get_platform_client
11
+ from agentstack_sdk.platform.common import PaginatedResult
12
+
13
+
14
+ class UserRole(StrEnum):
15
+ ADMIN = "admin"
16
+ DEVELOPER = "developer"
17
+ USER = "user"
18
+
19
+
20
+ class ChangeRoleResponse(pydantic.BaseModel):
21
+ user_id: str
22
+ new_role: UserRole
23
+
24
+
25
+ class User(pydantic.BaseModel):
26
+ id: str
27
+ role: UserRole
28
+ email: str
29
+ created_at: pydantic.AwareDatetime
30
+ role_updated_at: pydantic.AwareDatetime | None = None
31
+
32
+ @staticmethod
33
+ async def get(*, client: PlatformClient | None = None) -> User:
34
+ async with client or get_platform_client() as client:
35
+ return pydantic.TypeAdapter(User).validate_python(
36
+ (await client.get(url="/api/v1/user")).raise_for_status().json()
37
+ )
38
+
39
+ @staticmethod
40
+ async def list(
41
+ *,
42
+ email: str | None = None,
43
+ limit: int = 40,
44
+ page_token: str | None = None,
45
+ client: PlatformClient | None = None,
46
+ ) -> PaginatedResult[User]:
47
+ async with client or get_platform_client() as client:
48
+ params: dict[str, int | str] = {"limit": limit}
49
+ if email:
50
+ params["email"] = email
51
+ if page_token:
52
+ params["page_token"] = page_token
53
+
54
+ return pydantic.TypeAdapter(PaginatedResult[User]).validate_python(
55
+ (await client.get(url="/api/v1/users", params=params)).raise_for_status().json()
56
+ )
57
+
58
+ @staticmethod
59
+ async def set_role(
60
+ user_id: str,
61
+ new_role: UserRole,
62
+ *,
63
+ client: PlatformClient | None = None,
64
+ ) -> ChangeRoleResponse:
65
+ async with client or get_platform_client() as client:
66
+ return pydantic.TypeAdapter(ChangeRoleResponse).validate_python(
67
+ (await client.put(url=f"/api/v1/users/{user_id}/role", json={"new_role": new_role}))
68
+ .raise_for_status()
69
+ .json()
70
+ )