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,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
|
+
)
|