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,47 @@
|
|
|
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 pydantic
|
|
7
|
+
|
|
8
|
+
from agentstack_sdk.platform.client import PlatformClient, get_platform_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SystemConfiguration(pydantic.BaseModel):
|
|
12
|
+
id: str
|
|
13
|
+
default_llm_model: str | None = None
|
|
14
|
+
default_embedding_model: str | None = None
|
|
15
|
+
updated_at: pydantic.AwareDatetime
|
|
16
|
+
created_by: str
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
async def get(*, client: PlatformClient | None = None) -> SystemConfiguration:
|
|
20
|
+
"""Get the current system configuration."""
|
|
21
|
+
async with client or get_platform_client() as client:
|
|
22
|
+
return pydantic.TypeAdapter(SystemConfiguration).validate_python(
|
|
23
|
+
(await client.get(url="/api/v1/configurations/system")).raise_for_status().json()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
async def update(
|
|
28
|
+
*,
|
|
29
|
+
default_llm_model: str | None = None,
|
|
30
|
+
default_embedding_model: str | None = None,
|
|
31
|
+
client: PlatformClient | None = None,
|
|
32
|
+
) -> SystemConfiguration:
|
|
33
|
+
"""Update the system configuration."""
|
|
34
|
+
async with client or get_platform_client() as client:
|
|
35
|
+
return pydantic.TypeAdapter(SystemConfiguration).validate_python(
|
|
36
|
+
(
|
|
37
|
+
await client.put(
|
|
38
|
+
url="/api/v1/configurations/system",
|
|
39
|
+
json={
|
|
40
|
+
"default_llm_model": default_llm_model,
|
|
41
|
+
"default_embedding_model": default_embedding_model,
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
.raise_for_status()
|
|
46
|
+
.json()
|
|
47
|
+
)
|
|
@@ -0,0 +1,291 @@
|
|
|
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 collections.abc import AsyncIterator
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from uuid import UUID, uuid4
|
|
9
|
+
|
|
10
|
+
import pydantic
|
|
11
|
+
from a2a.types import Artifact, Message
|
|
12
|
+
from pydantic import AwareDatetime, BaseModel, Field, SerializeAsAny, computed_field
|
|
13
|
+
|
|
14
|
+
from agentstack_sdk.platform.client import PlatformClient, get_platform_client
|
|
15
|
+
from agentstack_sdk.platform.common import PaginatedResult
|
|
16
|
+
from agentstack_sdk.platform.provider import Provider
|
|
17
|
+
from agentstack_sdk.platform.types import Metadata, MetadataPatch
|
|
18
|
+
from agentstack_sdk.util.utils import filter_dict, utc_now
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContextHistoryItem(BaseModel):
|
|
22
|
+
id: UUID = Field(default_factory=uuid4)
|
|
23
|
+
data: Artifact | Message
|
|
24
|
+
created_at: AwareDatetime = Field(default_factory=utc_now)
|
|
25
|
+
context_id: str
|
|
26
|
+
|
|
27
|
+
@computed_field
|
|
28
|
+
@property
|
|
29
|
+
def kind(self) -> Literal["message", "artifact"]:
|
|
30
|
+
return getattr(self.data, "kind", "artifact")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ContextToken(pydantic.BaseModel):
|
|
34
|
+
context_id: str
|
|
35
|
+
token: pydantic.Secret[str]
|
|
36
|
+
expires_at: pydantic.AwareDatetime | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ContextPermissions(pydantic.BaseModel):
|
|
40
|
+
files: set[Literal["read", "write", "extract", "*"]] = set()
|
|
41
|
+
vector_stores: set[Literal["read", "write", "*"]] = set()
|
|
42
|
+
context_data: set[Literal["read", "write", "*"]] = set()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Permissions(ContextPermissions):
|
|
46
|
+
llm: set[Literal["*"] | str] = set()
|
|
47
|
+
embeddings: set[Literal["*"] | str] = set()
|
|
48
|
+
a2a_proxy: set[Literal["*"] | str] = set()
|
|
49
|
+
model_providers: set[Literal["read", "write", "*"]] = set()
|
|
50
|
+
variables: SerializeAsAny[set[Literal["read", "write", "*"]]] = set()
|
|
51
|
+
|
|
52
|
+
providers: set[Literal["read", "write", "*"]] = set() # write includes "show logs" permission
|
|
53
|
+
provider_variables: set[Literal["read", "write", "*"]] = set()
|
|
54
|
+
|
|
55
|
+
contexts: set[Literal["read", "write", "*"]] = set()
|
|
56
|
+
|
|
57
|
+
connectors: set[Literal["read", "write", "proxy", "*"]] = set()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Context(pydantic.BaseModel):
|
|
61
|
+
id: str
|
|
62
|
+
created_at: pydantic.AwareDatetime
|
|
63
|
+
updated_at: pydantic.AwareDatetime
|
|
64
|
+
last_active_at: pydantic.AwareDatetime
|
|
65
|
+
created_by: str
|
|
66
|
+
provider_id: str | None = None
|
|
67
|
+
metadata: Metadata | None = None
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
async def create(
|
|
71
|
+
*,
|
|
72
|
+
metadata: Metadata | None = None,
|
|
73
|
+
provider_id: str | None = None,
|
|
74
|
+
client: PlatformClient | None = None,
|
|
75
|
+
) -> Context:
|
|
76
|
+
async with client or get_platform_client() as client:
|
|
77
|
+
return pydantic.TypeAdapter(Context).validate_python(
|
|
78
|
+
(
|
|
79
|
+
await client.post(
|
|
80
|
+
url="/api/v1/contexts",
|
|
81
|
+
json=filter_dict({"metadata": metadata, "provider_id": provider_id}),
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
.raise_for_status()
|
|
85
|
+
.json()
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
async def list(
|
|
90
|
+
*,
|
|
91
|
+
client: PlatformClient | None = None,
|
|
92
|
+
page_token: str | None = None,
|
|
93
|
+
limit: int | None = None,
|
|
94
|
+
order: Literal["asc"] | Literal["desc"] | None = None,
|
|
95
|
+
order_by: Literal["created_at"] | Literal["updated_at"] | None = None,
|
|
96
|
+
include_empty: bool = True,
|
|
97
|
+
provider_id: str | None = None,
|
|
98
|
+
) -> PaginatedResult[Context]:
|
|
99
|
+
# `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
|
|
100
|
+
async with client or get_platform_client() as client:
|
|
101
|
+
return pydantic.TypeAdapter(PaginatedResult[Context]).validate_python(
|
|
102
|
+
(
|
|
103
|
+
await client.get(
|
|
104
|
+
url="/api/v1/contexts",
|
|
105
|
+
params=filter_dict(
|
|
106
|
+
{
|
|
107
|
+
"page_token": page_token,
|
|
108
|
+
"limit": limit,
|
|
109
|
+
"order": order,
|
|
110
|
+
"order_by": order_by,
|
|
111
|
+
"include_empty": include_empty,
|
|
112
|
+
"provider_id": provider_id,
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
.raise_for_status()
|
|
118
|
+
.json()
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def get(
|
|
122
|
+
self: Context | str,
|
|
123
|
+
*,
|
|
124
|
+
client: PlatformClient | None = None,
|
|
125
|
+
) -> Context:
|
|
126
|
+
# `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
|
|
127
|
+
context_id = self if isinstance(self, str) else self.id
|
|
128
|
+
async with client or get_platform_client() as client:
|
|
129
|
+
return pydantic.TypeAdapter(Context).validate_python(
|
|
130
|
+
(await client.get(url=f"/api/v1/contexts/{context_id}")).raise_for_status().json()
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def update(
|
|
134
|
+
self: Context | str,
|
|
135
|
+
*,
|
|
136
|
+
metadata: Metadata | None,
|
|
137
|
+
client: PlatformClient | None = None,
|
|
138
|
+
) -> Context:
|
|
139
|
+
# `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
|
|
140
|
+
context_id = self if isinstance(self, str) else self.id
|
|
141
|
+
async with client or get_platform_client() as client:
|
|
142
|
+
result = pydantic.TypeAdapter(Context).validate_python(
|
|
143
|
+
(await client.put(url=f"/api/v1/contexts/{context_id}", json={"metadata": metadata}))
|
|
144
|
+
.raise_for_status()
|
|
145
|
+
.json()
|
|
146
|
+
)
|
|
147
|
+
if isinstance(self, Context):
|
|
148
|
+
self.__dict__.update(result.__dict__)
|
|
149
|
+
return self
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
async def patch_metadata(
|
|
153
|
+
self: Context | str,
|
|
154
|
+
*,
|
|
155
|
+
metadata: MetadataPatch | None,
|
|
156
|
+
client: PlatformClient | None = None,
|
|
157
|
+
) -> Context:
|
|
158
|
+
# `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
|
|
159
|
+
context_id = self if isinstance(self, str) else self.id
|
|
160
|
+
async with client or get_platform_client() as client:
|
|
161
|
+
result = pydantic.TypeAdapter(Context).validate_python(
|
|
162
|
+
(await client.patch(url=f"/api/v1/contexts/{context_id}/metadata", json={"metadata": metadata}))
|
|
163
|
+
.raise_for_status()
|
|
164
|
+
.json()
|
|
165
|
+
)
|
|
166
|
+
if isinstance(self, Context):
|
|
167
|
+
self.__dict__.update(result.__dict__)
|
|
168
|
+
return self
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
async def delete(
|
|
172
|
+
self: Context | str,
|
|
173
|
+
*,
|
|
174
|
+
client: PlatformClient | None = None,
|
|
175
|
+
) -> None:
|
|
176
|
+
# `self` has a weird type so that you can call both `instance.delete()` or `File.delete("123")`
|
|
177
|
+
context_id = self if isinstance(self, str) else self.id
|
|
178
|
+
async with client or get_platform_client() as client:
|
|
179
|
+
_ = (await client.delete(url=f"/api/v1/contexts/{context_id}")).raise_for_status()
|
|
180
|
+
|
|
181
|
+
async def generate_token(
|
|
182
|
+
self: Context | str,
|
|
183
|
+
*,
|
|
184
|
+
providers: list[str] | list[Provider] | None = None,
|
|
185
|
+
client: PlatformClient | None = None,
|
|
186
|
+
grant_global_permissions: Permissions | None = None,
|
|
187
|
+
grant_context_permissions: ContextPermissions | None = None,
|
|
188
|
+
) -> ContextToken:
|
|
189
|
+
"""
|
|
190
|
+
Generate token for agent authentication
|
|
191
|
+
|
|
192
|
+
@param grant_global_permissions: Global permissions granted by the token. Must be subset of the users permissions
|
|
193
|
+
@param grant_context_permissions: Context permissions granted by the token. Must be subset of the users permissions
|
|
194
|
+
"""
|
|
195
|
+
# `self` has a weird type so that you can call both `instance.content()` to get content of an instance, or `File.content("123")`
|
|
196
|
+
context_id = self if isinstance(self, str) else self.id
|
|
197
|
+
grant_global_permissions = grant_global_permissions or Permissions()
|
|
198
|
+
grant_context_permissions = grant_context_permissions or Permissions()
|
|
199
|
+
|
|
200
|
+
if isinstance(self, Context) and self.metadata and (provider_id := self.metadata.get("provider_id", None)):
|
|
201
|
+
providers = providers or [provider_id]
|
|
202
|
+
|
|
203
|
+
if "*" not in grant_global_permissions.a2a_proxy and not grant_global_permissions.a2a_proxy:
|
|
204
|
+
if not providers:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
"Invalid audience: You must specify providers or use '*' in grant_global_permissions.a2a_proxy."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
grant_global_permissions.a2a_proxy |= {p.id if isinstance(p, Provider) else p for p in providers}
|
|
210
|
+
|
|
211
|
+
async with client or get_platform_client() as client:
|
|
212
|
+
token_response = (
|
|
213
|
+
(
|
|
214
|
+
await client.post(
|
|
215
|
+
url=f"/api/v1/contexts/{context_id}/token",
|
|
216
|
+
json={
|
|
217
|
+
"grant_global_permissions": grant_global_permissions.model_dump(mode="json"),
|
|
218
|
+
"grant_context_permissions": grant_context_permissions.model_dump(mode="json"),
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
.raise_for_status()
|
|
223
|
+
.json()
|
|
224
|
+
)
|
|
225
|
+
return pydantic.TypeAdapter(ContextToken).validate_python({**token_response, "context_id": context_id})
|
|
226
|
+
|
|
227
|
+
async def add_history_item(
|
|
228
|
+
self: Context | str,
|
|
229
|
+
*,
|
|
230
|
+
data: Message | Artifact,
|
|
231
|
+
client: PlatformClient | None = None,
|
|
232
|
+
) -> None:
|
|
233
|
+
"""Add a Message or Artifact to the context history (append-only)"""
|
|
234
|
+
target_context_id = self if isinstance(self, str) else self.id
|
|
235
|
+
async with client or get_platform_client() as platform_client:
|
|
236
|
+
_ = (
|
|
237
|
+
await platform_client.post(
|
|
238
|
+
url=f"/api/v1/contexts/{target_context_id}/history", json=data.model_dump(mode="json")
|
|
239
|
+
)
|
|
240
|
+
).raise_for_status()
|
|
241
|
+
|
|
242
|
+
async def delete_history_from_id(
|
|
243
|
+
self: Context | str,
|
|
244
|
+
*,
|
|
245
|
+
from_id: UUID | str,
|
|
246
|
+
client: PlatformClient | None = None,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Delete all history items from a specific item onwards (inclusive)"""
|
|
249
|
+
target_context_id = self if isinstance(self, str) else self.id
|
|
250
|
+
async with client or get_platform_client() as platform_client:
|
|
251
|
+
_ = (
|
|
252
|
+
await platform_client.delete(
|
|
253
|
+
url=f"/api/v1/contexts/{target_context_id}/history", params={"from_id": str(from_id)}
|
|
254
|
+
)
|
|
255
|
+
).raise_for_status()
|
|
256
|
+
|
|
257
|
+
async def list_history(
|
|
258
|
+
self: Context | str,
|
|
259
|
+
*,
|
|
260
|
+
page_token: str | None = None,
|
|
261
|
+
limit: int | None = None,
|
|
262
|
+
order: Literal["asc"] | Literal["desc"] | None = "asc",
|
|
263
|
+
order_by: Literal["created_at"] | Literal["updated_at"] | None = None,
|
|
264
|
+
client: PlatformClient | None = None,
|
|
265
|
+
) -> PaginatedResult[ContextHistoryItem]:
|
|
266
|
+
"""List all history items for this context in chronological order"""
|
|
267
|
+
target_context_id = self if isinstance(self, str) else self.id
|
|
268
|
+
async with client or get_platform_client() as platform_client:
|
|
269
|
+
return pydantic.TypeAdapter(PaginatedResult[ContextHistoryItem]).validate_python(
|
|
270
|
+
(
|
|
271
|
+
await platform_client.get(
|
|
272
|
+
url=f"/api/v1/contexts/{target_context_id}/history",
|
|
273
|
+
params=filter_dict(
|
|
274
|
+
{"page_token": page_token, "limit": limit, "order": order, "order_by": order_by}
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
.raise_for_status()
|
|
279
|
+
.json()
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
async def list_all_history(
|
|
283
|
+
self: Context | str, client: PlatformClient | None = None
|
|
284
|
+
) -> AsyncIterator[ContextHistoryItem]:
|
|
285
|
+
result = await Context.list_history(self, client=client)
|
|
286
|
+
for item in result.items:
|
|
287
|
+
yield item
|
|
288
|
+
while result.has_more:
|
|
289
|
+
result = await Context.list_history(self, page_token=result.next_page_token, client=client)
|
|
290
|
+
for item in result.items:
|
|
291
|
+
yield item
|
|
@@ -0,0 +1,295 @@
|
|
|
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 collections.abc import AsyncIterator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
import pydantic
|
|
12
|
+
from a2a.types import FilePart, FileWithUri
|
|
13
|
+
|
|
14
|
+
from agentstack_sdk.platform.client import PlatformClient, get_platform_client
|
|
15
|
+
from agentstack_sdk.platform.common import PaginatedResult
|
|
16
|
+
from agentstack_sdk.util.file import LoadedFile, LoadedFileWithUri, PlatformFileUrl
|
|
17
|
+
from agentstack_sdk.util.utils import filter_dict
|
|
18
|
+
|
|
19
|
+
ExtractionFormatLiteral = typing.Literal["markdown", "vendor_specific_json"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExtractedFileInfo(pydantic.BaseModel):
|
|
23
|
+
"""Information about an extracted file."""
|
|
24
|
+
|
|
25
|
+
file_id: str
|
|
26
|
+
format: ExtractionFormatLiteral | None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Extraction(pydantic.BaseModel):
|
|
30
|
+
id: str
|
|
31
|
+
file_id: str
|
|
32
|
+
extracted_files: list[ExtractedFileInfo] = pydantic.Field(default_factory=list)
|
|
33
|
+
status: typing.Literal["pending", "in_progress", "completed", "failed", "cancelled"] = "pending"
|
|
34
|
+
job_id: str | None = None
|
|
35
|
+
error_message: str | None = None
|
|
36
|
+
extraction_metadata: dict[str, typing.Any] | None = None
|
|
37
|
+
started_at: pydantic.AwareDatetime | None = None
|
|
38
|
+
finished_at: pydantic.AwareDatetime | None = None
|
|
39
|
+
created_at: pydantic.AwareDatetime
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class File(pydantic.BaseModel):
|
|
43
|
+
id: str
|
|
44
|
+
filename: str
|
|
45
|
+
content_type: str
|
|
46
|
+
file_size_bytes: int
|
|
47
|
+
created_at: pydantic.AwareDatetime
|
|
48
|
+
created_by: str
|
|
49
|
+
file_type: typing.Literal["user_upload", "extracted_text"]
|
|
50
|
+
parent_file_id: str | None = None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def url(self) -> PlatformFileUrl:
|
|
54
|
+
return PlatformFileUrl(f"agentstack://{self.id}")
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
async def create(
|
|
58
|
+
*,
|
|
59
|
+
filename: str,
|
|
60
|
+
content: typing.BinaryIO | bytes,
|
|
61
|
+
content_type: str = "application/octet-stream",
|
|
62
|
+
client: PlatformClient | None = None,
|
|
63
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
64
|
+
) -> File:
|
|
65
|
+
async with client or get_platform_client() as platform_client:
|
|
66
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
67
|
+
return pydantic.TypeAdapter(File).validate_python(
|
|
68
|
+
(
|
|
69
|
+
await platform_client.post(
|
|
70
|
+
url="/api/v1/files",
|
|
71
|
+
files={"file": (filename, content, content_type)},
|
|
72
|
+
params=context_id and {"context_id": context_id},
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
.raise_for_status()
|
|
76
|
+
.json()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def get(
|
|
80
|
+
self: File | str,
|
|
81
|
+
*,
|
|
82
|
+
client: PlatformClient | None = None,
|
|
83
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
84
|
+
) -> File:
|
|
85
|
+
# `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
|
|
86
|
+
file_id = self if isinstance(self, str) else self.id
|
|
87
|
+
async with client or get_platform_client() as platform_client:
|
|
88
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
89
|
+
return pydantic.TypeAdapter(File).validate_python(
|
|
90
|
+
(
|
|
91
|
+
await platform_client.get(
|
|
92
|
+
url=f"/api/v1/files/{file_id}",
|
|
93
|
+
params=context_id and {"context_id": context_id},
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
.raise_for_status()
|
|
97
|
+
.json()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def delete(
|
|
101
|
+
self: File | str,
|
|
102
|
+
*,
|
|
103
|
+
client: PlatformClient | None = None,
|
|
104
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
105
|
+
) -> None:
|
|
106
|
+
# `self` has a weird type so that you can call both `instance.delete()` or `File.delete("123")`
|
|
107
|
+
file_id = self if isinstance(self, str) else self.id
|
|
108
|
+
async with client or get_platform_client() as platform_client:
|
|
109
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
110
|
+
_ = (
|
|
111
|
+
await platform_client.delete(
|
|
112
|
+
url=f"/api/v1/files/{file_id}", params=context_id and {"context_id": context_id}
|
|
113
|
+
)
|
|
114
|
+
).raise_for_status()
|
|
115
|
+
|
|
116
|
+
@asynccontextmanager
|
|
117
|
+
async def load_content(
|
|
118
|
+
self: File | str,
|
|
119
|
+
*,
|
|
120
|
+
stream: bool = False,
|
|
121
|
+
client: PlatformClient | None = None,
|
|
122
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
123
|
+
) -> AsyncIterator[LoadedFile]:
|
|
124
|
+
# `self` has a weird type so that you can call both `instance.load_content()` to create an extraction for an instance, or `File.load_content("123")`
|
|
125
|
+
file_id = self if isinstance(self, str) else self.id
|
|
126
|
+
async with client or get_platform_client() as platform_client:
|
|
127
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
128
|
+
|
|
129
|
+
file = await File.get(file_id, client=client, context_id=context_id) if isinstance(self, str) else self
|
|
130
|
+
|
|
131
|
+
async with platform_client.stream(
|
|
132
|
+
"GET", url=f"/api/v1/files/{file_id}/content", params=context_id and {"context_id": context_id}
|
|
133
|
+
) as response:
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
if not stream:
|
|
136
|
+
await response.aread()
|
|
137
|
+
yield LoadedFileWithUri(response=response, content_type=file.content_type, filename=file.filename)
|
|
138
|
+
|
|
139
|
+
@asynccontextmanager
|
|
140
|
+
async def load_text_content(
|
|
141
|
+
self: File | str,
|
|
142
|
+
*,
|
|
143
|
+
stream: bool = False,
|
|
144
|
+
client: PlatformClient | None = None,
|
|
145
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
146
|
+
) -> AsyncIterator[LoadedFile]:
|
|
147
|
+
# `self` has a weird type so that you can call both `instance.load_text_content()` to create an extraction for an instance, or `File.load_text_content("123")`
|
|
148
|
+
file_id = self if isinstance(self, str) else self.id
|
|
149
|
+
async with client or get_platform_client() as platform_client:
|
|
150
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
151
|
+
|
|
152
|
+
file = await File.get(file_id, client=client, context_id=context_id) if isinstance(self, str) else self
|
|
153
|
+
|
|
154
|
+
async with platform_client.stream(
|
|
155
|
+
"GET",
|
|
156
|
+
url=f"/api/v1/files/{file_id}/text_content",
|
|
157
|
+
params=context_id and {"context_id": context_id},
|
|
158
|
+
) as response:
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
if not stream:
|
|
161
|
+
await response.aread()
|
|
162
|
+
yield LoadedFileWithUri(response=response, content_type=file.content_type, filename=file.filename)
|
|
163
|
+
|
|
164
|
+
@asynccontextmanager
|
|
165
|
+
async def load_json_content(
|
|
166
|
+
self: File | str,
|
|
167
|
+
*,
|
|
168
|
+
stream: bool = False,
|
|
169
|
+
client: PlatformClient | None = None,
|
|
170
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
171
|
+
) -> AsyncIterator[LoadedFile]:
|
|
172
|
+
# `self` has a weird type so that you can call both `instance.load_json_content()` to create an extraction for an instance, or `File.load_json_content("123")`
|
|
173
|
+
file_id = self if isinstance(self, str) else self.id
|
|
174
|
+
async with client or get_platform_client() as platform_client:
|
|
175
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
176
|
+
|
|
177
|
+
file = await File.get(file_id, client=client, context_id=context_id) if isinstance(self, str) else self
|
|
178
|
+
extraction = await file.get_extraction(client=client, context_id=context_id)
|
|
179
|
+
|
|
180
|
+
for extracted_file_info in extraction.extracted_files:
|
|
181
|
+
if extracted_file_info.format != "vendor_specific_json":
|
|
182
|
+
continue
|
|
183
|
+
extracted_json_file_id = extracted_file_info.file_id
|
|
184
|
+
async with platform_client.stream(
|
|
185
|
+
"GET",
|
|
186
|
+
url=f"/api/v1/files/{extracted_json_file_id}/content",
|
|
187
|
+
params=context_id and {"context_id": context_id},
|
|
188
|
+
) as response:
|
|
189
|
+
response.raise_for_status()
|
|
190
|
+
if not stream:
|
|
191
|
+
await response.aread()
|
|
192
|
+
yield LoadedFileWithUri(response=response, content_type=file.content_type, filename=file.filename)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
raise ValueError("No extracted JSON content available for this file.")
|
|
196
|
+
|
|
197
|
+
async def create_extraction(
|
|
198
|
+
self: File | str,
|
|
199
|
+
*,
|
|
200
|
+
formats: list[ExtractionFormatLiteral] | None = None,
|
|
201
|
+
client: PlatformClient | None = None,
|
|
202
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
203
|
+
) -> Extraction:
|
|
204
|
+
# `self` has a weird type so that you can call both `instance.create_extraction()` to create an extraction for an instance, or `File.create_extraction("123")`
|
|
205
|
+
file_id = self if isinstance(self, str) else self.id
|
|
206
|
+
async with client or get_platform_client() as platform_client:
|
|
207
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
208
|
+
return pydantic.TypeAdapter(Extraction).validate_python(
|
|
209
|
+
(
|
|
210
|
+
await platform_client.post(
|
|
211
|
+
url=f"/api/v1/files/{file_id}/extraction",
|
|
212
|
+
params=context_id and {"context_id": context_id},
|
|
213
|
+
json={"settings": {"formats": formats}} if formats else None,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
.raise_for_status()
|
|
217
|
+
.json()
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def get_extraction(
|
|
221
|
+
self: File | str,
|
|
222
|
+
*,
|
|
223
|
+
client: PlatformClient | None = None,
|
|
224
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
225
|
+
) -> Extraction:
|
|
226
|
+
# `self` has a weird type so that you can call both `instance.get_extraction()` to get an extraction of an instance, or `File.get_extraction("123", "456")`
|
|
227
|
+
file_id = self if isinstance(self, str) else self.id
|
|
228
|
+
async with client or get_platform_client() as platform_client:
|
|
229
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
230
|
+
return pydantic.TypeAdapter(Extraction).validate_python(
|
|
231
|
+
(
|
|
232
|
+
await platform_client.get(
|
|
233
|
+
url=f"/api/v1/files/{file_id}/extraction",
|
|
234
|
+
params=context_id and {"context_id": context_id},
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
.raise_for_status()
|
|
238
|
+
.json()
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def delete_extraction(
|
|
242
|
+
self: File | str,
|
|
243
|
+
*,
|
|
244
|
+
client: PlatformClient | None = None,
|
|
245
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
246
|
+
) -> None:
|
|
247
|
+
# `self` has a weird type so that you can call both `instance.delete_extraction()` or `File.delete_extraction("123", "456")`
|
|
248
|
+
file_id = self if isinstance(self, str) else self.id
|
|
249
|
+
async with client or get_platform_client() as platform_client:
|
|
250
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
251
|
+
_ = (
|
|
252
|
+
await platform_client.delete(
|
|
253
|
+
url=f"/api/v1/files/{file_id}/extraction",
|
|
254
|
+
params=context_id and {"context_id": context_id},
|
|
255
|
+
)
|
|
256
|
+
).raise_for_status()
|
|
257
|
+
|
|
258
|
+
def to_file_part(self: File) -> FilePart:
|
|
259
|
+
return FilePart(file=FileWithUri(name=self.filename, uri=f"agentstack://{self.id}"))
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
async def list(
|
|
263
|
+
*,
|
|
264
|
+
content_type: str | None = None,
|
|
265
|
+
filename_search: str | None = None,
|
|
266
|
+
page_token: str | None = None,
|
|
267
|
+
limit: int | None = None,
|
|
268
|
+
order: Literal["asc"] | Literal["desc"] | None = "asc",
|
|
269
|
+
order_by: Literal["created_at"] | Literal["filename"] | Literal["file_size_bytes"] | None = None,
|
|
270
|
+
client: PlatformClient | None = None,
|
|
271
|
+
context_id: str | None | Literal["auto"] = "auto",
|
|
272
|
+
) -> PaginatedResult[File]:
|
|
273
|
+
# `self` has a weird type so that you can call both `instance.list_history()` or `ProviderBuild.list_history("123")`
|
|
274
|
+
async with client or get_platform_client() as platform_client:
|
|
275
|
+
context_id = platform_client.context_id if context_id == "auto" else context_id
|
|
276
|
+
return pydantic.TypeAdapter(PaginatedResult[File]).validate_python(
|
|
277
|
+
(
|
|
278
|
+
await platform_client.get(
|
|
279
|
+
url="/api/v1/files",
|
|
280
|
+
params=filter_dict(
|
|
281
|
+
{
|
|
282
|
+
"context_id": context_id,
|
|
283
|
+
"content_type": content_type,
|
|
284
|
+
"filename_search": filename_search,
|
|
285
|
+
"page_token": page_token,
|
|
286
|
+
"limit": limit,
|
|
287
|
+
"order": order,
|
|
288
|
+
"order_by": order_by,
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
.raise_for_status()
|
|
294
|
+
.json()
|
|
295
|
+
)
|