persona-sdk-python 0.2.24__tar.gz → 0.2.26__tar.gz
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.
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/.github/workflows/ci.yml +4 -3
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/.github/workflows/deploy.yml +5 -4
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/PKG-INFO +83 -2
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/README.md +81 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/__init__.py +27 -0
- persona_sdk_python-0.2.26/persona_sdk/api/channels.py +64 -0
- persona_sdk_python-0.2.26/persona_sdk/api/model_aliases.py +131 -0
- persona_sdk_python-0.2.26/persona_sdk/api/notifications.py +50 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/client.py +10 -5
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/pyproject.toml +13 -2
- persona_sdk_python-0.2.26/tests/__init__.py +0 -0
- persona_sdk_python-0.2.26/tests/conftest.py +53 -0
- persona_sdk_python-0.2.26/tests/test_model_aliases.py +236 -0
- persona_sdk_python-0.2.26/tests/test_notifications.py +68 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/uv.lock +176 -6
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/.gitignore +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/DEPLOY.md +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/MIGRATION_GITHUB.md +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/_serde.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/__init__.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/agents.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/credentials.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/features.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/human_tasks.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/knowledge_bases.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/markup_rules.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/missions.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/projects.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/providers.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/service_prices.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/sessions.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/triggers.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/api/workflows.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/auth/__init__.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/auth/api_key_auth.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/auth/base.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/auth/bearer_token_auth.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/runners/__init__.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/runners/agent_runner.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/runners/resource_retriever.py +0 -0
- {persona_sdk_python-0.2.24 → persona_sdk_python-0.2.26}/persona_sdk/runners/tool_invoker.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: CI
|
|
2
2
|
|
|
3
|
-
# Smoke su PR:
|
|
4
|
-
#
|
|
3
|
+
# Smoke + test su PR: verifica che uv.lock risolva (--frozen), che il package importi
|
|
4
|
+
# e che i test passino (httpx MockTransport: niente rete, niente server).
|
|
5
5
|
# Il publish avviene solo al push su main (deploy.yml).
|
|
6
6
|
|
|
7
7
|
on:
|
|
@@ -23,7 +23,8 @@ jobs:
|
|
|
23
23
|
python-version: '3.12'
|
|
24
24
|
- name: Installa uv
|
|
25
25
|
run: pip install uv
|
|
26
|
-
- name: Smoke import
|
|
26
|
+
- name: Smoke import + test
|
|
27
27
|
run: |
|
|
28
28
|
uv sync --frozen
|
|
29
29
|
uv run python -c "import persona_sdk; print('smoke ok:', persona_sdk.__name__)"
|
|
30
|
+
uv run pytest -q
|
|
@@ -64,7 +64,7 @@ jobs:
|
|
|
64
64
|
echo "Versione finale: ${VERSION}"
|
|
65
65
|
|
|
66
66
|
test:
|
|
67
|
-
name: Test (smoke import)
|
|
67
|
+
name: Test (smoke import + pytest)
|
|
68
68
|
runs-on: ubuntu-latest
|
|
69
69
|
if: github.event_name == 'push' || inputs.mode == 'test-and-publish'
|
|
70
70
|
steps:
|
|
@@ -74,12 +74,13 @@ jobs:
|
|
|
74
74
|
python-version: '3.12'
|
|
75
75
|
- name: Installa uv
|
|
76
76
|
run: pip install uv
|
|
77
|
-
- name: Smoke import
|
|
78
|
-
#
|
|
79
|
-
#
|
|
77
|
+
- name: Smoke import + test
|
|
78
|
+
# lo smoke verifica che uv.lock risolva e che il package importi;
|
|
79
|
+
# pytest esegue i test (httpx MockTransport: niente rete, niente server)
|
|
80
80
|
run: |
|
|
81
81
|
uv sync --frozen
|
|
82
82
|
uv run python -c "import persona_sdk; print('smoke ok:', persona_sdk.__name__)"
|
|
83
|
+
uv run pytest -q
|
|
83
84
|
|
|
84
85
|
build-package:
|
|
85
86
|
name: Build & check sdist/wheel
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: persona-sdk-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.26
|
|
4
4
|
Summary: Async Python SDK for the Persona AI platform
|
|
5
5
|
Project-URL: Homepage, https://persona.applica.guru
|
|
6
6
|
Project-URL: Repository, https://bitbucket.org/applicaguru/persona-sdk-python
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Requires-Dist: httpx>=0.27.0
|
|
22
|
-
Requires-Dist: persona-models>=0.2.
|
|
22
|
+
Requires-Dist: persona-models>=0.2.20
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
<div align="center">
|
|
@@ -140,6 +140,8 @@ The `PersonaSdk` instance exposes one client per resource:
|
|
|
140
140
|
| `sdk.features` | Feature templates and MCP tools |
|
|
141
141
|
| `sdk.credentials` | OAuth credentials and authorization |
|
|
142
142
|
| `sdk.service_prices` | Service pricing |
|
|
143
|
+
| `sdk.model_aliases` | Model aliases (`alias:<name>` -> concrete model), impact, health check, suggestions |
|
|
144
|
+
| `sdk.notifications` | In-platform notifications (badge, read state) |
|
|
143
145
|
|
|
144
146
|
Each API method is fully async. Always close the SDK when done to release the
|
|
145
147
|
underlying HTTP connection pool:
|
|
@@ -223,6 +225,85 @@ page = await sdk.agents.list(keyword="bot", page=1, size=20)
|
|
|
223
225
|
# page.size -> 20
|
|
224
226
|
```
|
|
225
227
|
|
|
228
|
+
## Model aliases
|
|
229
|
+
|
|
230
|
+
A model alias is a named indirection: agents set their model to `alias:<name>` and
|
|
231
|
+
the alias holds the concrete `provider/model`, ordered fallbacks and optional
|
|
232
|
+
defaults, so changing one alias moves every agent that uses it. Writing needs a
|
|
233
|
+
super admin, a project admin or the project API key.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from persona_sdk import ModelAlias, ModelAliasDefaults, alias_ref
|
|
237
|
+
|
|
238
|
+
# Create: only name, target, projectId, description, fallbacks and defaults are
|
|
239
|
+
# sent (health, history and timestamps are managed by the server). A plain dict
|
|
240
|
+
# with the same keys works too.
|
|
241
|
+
alias = await sdk.model_aliases.create(
|
|
242
|
+
ModelAlias(
|
|
243
|
+
name="smart-default",
|
|
244
|
+
target="openai/gpt-5",
|
|
245
|
+
fallbacks=["anthropic/claude-sonnet-4"],
|
|
246
|
+
defaults=ModelAliasDefaults(max_output_tokens=4000),
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Point an agent at it
|
|
251
|
+
agent.model.model_name = alias_ref("smart-default") # "alias:smart-default"
|
|
252
|
+
|
|
253
|
+
page = await sdk.model_aliases.list(keyword="smart", page=1, size=20)
|
|
254
|
+
alias = await sdk.model_aliases.get(alias.id)
|
|
255
|
+
|
|
256
|
+
# Move every agent to another model (name and scope are immutable)
|
|
257
|
+
alias.target = "grok/grok-4"
|
|
258
|
+
alias = await sdk.model_aliases.update(alias.id, alias)
|
|
259
|
+
|
|
260
|
+
impact = await sdk.model_aliases.impact(alias.id) # AliasImpact: total, usages
|
|
261
|
+
alias = await sdk.model_aliases.check(alias.id) # runs the health check now
|
|
262
|
+
print(alias.health.status, alias.health.issues)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Suggestions
|
|
266
|
+
|
|
267
|
+
Replacement and fallback candidates for the alias' target (or for another model,
|
|
268
|
+
e.g. a fallback that went missing). A proposal only: nothing is applied.
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
sugg = await sdk.model_aliases.suggestions(alias.id, model="grok/grok-4", limit=5)
|
|
272
|
+
for s in sugg.replacements:
|
|
273
|
+
print(s.model, s.context_window, s.warnings)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Deleting an alias that is in use
|
|
277
|
+
|
|
278
|
+
Deleting an alias still referenced by agents is refused with a `409`. The SDK
|
|
279
|
+
raises the usual `httpx.HTTPStatusError`; `ModelAliasesApi.in_use` reads the
|
|
280
|
+
blocking agents from it, or returns `None` for any other error:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
import httpx
|
|
284
|
+
from persona_sdk import ModelAliasesApi
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
await sdk.model_aliases.remove(alias.id)
|
|
288
|
+
except httpx.HTTPStatusError as e:
|
|
289
|
+
impact = ModelAliasesApi.in_use(e)
|
|
290
|
+
if impact is None:
|
|
291
|
+
raise
|
|
292
|
+
print(f"used by {impact.total} agent(s):", [u.agent_id for u in impact.usages])
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Notifications
|
|
296
|
+
|
|
297
|
+
In-platform notifications (e.g. an alias whose model is being retired). A project
|
|
298
|
+
sees its own; platform-wide ones are for super admins.
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
page = await sdk.notifications.list(unread_only=True, page=1, size=20)
|
|
302
|
+
badge = await sdk.notifications.unread_count() # int
|
|
303
|
+
n = await sdk.notifications.mark_read(page.items[0].id) # -> Notification
|
|
304
|
+
await sdk.notifications.mark_all_read(project_id="p1") # returns None
|
|
305
|
+
```
|
|
306
|
+
|
|
226
307
|
## Workflows
|
|
227
308
|
|
|
228
309
|
If your workflows service runs on a different host, pass `workflows_url`:
|
|
@@ -116,6 +116,8 @@ The `PersonaSdk` instance exposes one client per resource:
|
|
|
116
116
|
| `sdk.features` | Feature templates and MCP tools |
|
|
117
117
|
| `sdk.credentials` | OAuth credentials and authorization |
|
|
118
118
|
| `sdk.service_prices` | Service pricing |
|
|
119
|
+
| `sdk.model_aliases` | Model aliases (`alias:<name>` -> concrete model), impact, health check, suggestions |
|
|
120
|
+
| `sdk.notifications` | In-platform notifications (badge, read state) |
|
|
119
121
|
|
|
120
122
|
Each API method is fully async. Always close the SDK when done to release the
|
|
121
123
|
underlying HTTP connection pool:
|
|
@@ -199,6 +201,85 @@ page = await sdk.agents.list(keyword="bot", page=1, size=20)
|
|
|
199
201
|
# page.size -> 20
|
|
200
202
|
```
|
|
201
203
|
|
|
204
|
+
## Model aliases
|
|
205
|
+
|
|
206
|
+
A model alias is a named indirection: agents set their model to `alias:<name>` and
|
|
207
|
+
the alias holds the concrete `provider/model`, ordered fallbacks and optional
|
|
208
|
+
defaults, so changing one alias moves every agent that uses it. Writing needs a
|
|
209
|
+
super admin, a project admin or the project API key.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from persona_sdk import ModelAlias, ModelAliasDefaults, alias_ref
|
|
213
|
+
|
|
214
|
+
# Create: only name, target, projectId, description, fallbacks and defaults are
|
|
215
|
+
# sent (health, history and timestamps are managed by the server). A plain dict
|
|
216
|
+
# with the same keys works too.
|
|
217
|
+
alias = await sdk.model_aliases.create(
|
|
218
|
+
ModelAlias(
|
|
219
|
+
name="smart-default",
|
|
220
|
+
target="openai/gpt-5",
|
|
221
|
+
fallbacks=["anthropic/claude-sonnet-4"],
|
|
222
|
+
defaults=ModelAliasDefaults(max_output_tokens=4000),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Point an agent at it
|
|
227
|
+
agent.model.model_name = alias_ref("smart-default") # "alias:smart-default"
|
|
228
|
+
|
|
229
|
+
page = await sdk.model_aliases.list(keyword="smart", page=1, size=20)
|
|
230
|
+
alias = await sdk.model_aliases.get(alias.id)
|
|
231
|
+
|
|
232
|
+
# Move every agent to another model (name and scope are immutable)
|
|
233
|
+
alias.target = "grok/grok-4"
|
|
234
|
+
alias = await sdk.model_aliases.update(alias.id, alias)
|
|
235
|
+
|
|
236
|
+
impact = await sdk.model_aliases.impact(alias.id) # AliasImpact: total, usages
|
|
237
|
+
alias = await sdk.model_aliases.check(alias.id) # runs the health check now
|
|
238
|
+
print(alias.health.status, alias.health.issues)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Suggestions
|
|
242
|
+
|
|
243
|
+
Replacement and fallback candidates for the alias' target (or for another model,
|
|
244
|
+
e.g. a fallback that went missing). A proposal only: nothing is applied.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
sugg = await sdk.model_aliases.suggestions(alias.id, model="grok/grok-4", limit=5)
|
|
248
|
+
for s in sugg.replacements:
|
|
249
|
+
print(s.model, s.context_window, s.warnings)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Deleting an alias that is in use
|
|
253
|
+
|
|
254
|
+
Deleting an alias still referenced by agents is refused with a `409`. The SDK
|
|
255
|
+
raises the usual `httpx.HTTPStatusError`; `ModelAliasesApi.in_use` reads the
|
|
256
|
+
blocking agents from it, or returns `None` for any other error:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import httpx
|
|
260
|
+
from persona_sdk import ModelAliasesApi
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
await sdk.model_aliases.remove(alias.id)
|
|
264
|
+
except httpx.HTTPStatusError as e:
|
|
265
|
+
impact = ModelAliasesApi.in_use(e)
|
|
266
|
+
if impact is None:
|
|
267
|
+
raise
|
|
268
|
+
print(f"used by {impact.total} agent(s):", [u.agent_id for u in impact.usages])
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Notifications
|
|
272
|
+
|
|
273
|
+
In-platform notifications (e.g. an alias whose model is being retired). A project
|
|
274
|
+
sees its own; platform-wide ones are for super admins.
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
page = await sdk.notifications.list(unread_only=True, page=1, size=20)
|
|
278
|
+
badge = await sdk.notifications.unread_count() # int
|
|
279
|
+
n = await sdk.notifications.mark_read(page.items[0].id) # -> Notification
|
|
280
|
+
await sdk.notifications.mark_all_read(project_id="p1") # returns None
|
|
281
|
+
```
|
|
282
|
+
|
|
202
283
|
## Workflows
|
|
203
284
|
|
|
204
285
|
If your workflows service runs on a different host, pass `workflows_url`:
|
|
@@ -6,6 +6,8 @@ from persona_sdk.api.features import FeaturesApi
|
|
|
6
6
|
from persona_sdk.api.knowledge_bases import KnowledgeBasesApi
|
|
7
7
|
from persona_sdk.api.markup_rules import MarkupRulesApi
|
|
8
8
|
from persona_sdk.api.missions import MissionsApi
|
|
9
|
+
from persona_sdk.api.model_aliases import ModelAliasesApi
|
|
10
|
+
from persona_sdk.api.notifications import NotificationsApi
|
|
9
11
|
from persona_sdk.api.projects import ProjectsApi
|
|
10
12
|
from persona_sdk.api.providers import ProvidersApi
|
|
11
13
|
from persona_sdk.api.service_prices import ServicePricesApi
|
|
@@ -13,6 +15,7 @@ from persona_sdk.api.sessions import SessionsApi
|
|
|
13
15
|
from persona_sdk.api.triggers import TriggersApi
|
|
14
16
|
from persona_sdk.api.workflows import WorkflowsApi
|
|
15
17
|
from persona_sdk.api.human_tasks import HumanTasksApi
|
|
18
|
+
from persona_sdk.api.channels import ChannelsApi
|
|
16
19
|
from persona_sdk.auth import (
|
|
17
20
|
ApiKeyAuthenticationProvider,
|
|
18
21
|
AuthenticationProvider,
|
|
@@ -35,6 +38,16 @@ from persona_models.knowledge_bases import (
|
|
|
35
38
|
from persona_models.markup import MarkupRule
|
|
36
39
|
from persona_models.mcp import GenerationRequest
|
|
37
40
|
from persona_models.missions import CreateMissionRequest, Mission
|
|
41
|
+
from persona_models.model_aliases import (
|
|
42
|
+
AliasImpact,
|
|
43
|
+
AliasSuggestions,
|
|
44
|
+
ModelAlias,
|
|
45
|
+
ModelAliasDefaults,
|
|
46
|
+
alias_name_from_ref,
|
|
47
|
+
alias_ref,
|
|
48
|
+
is_alias_ref,
|
|
49
|
+
)
|
|
50
|
+
from persona_models.notifications import Notification
|
|
38
51
|
from persona_models.projects import Project
|
|
39
52
|
from persona_models.providers import Provider, ProviderModel
|
|
40
53
|
from persona_models.service_prices import ServicePrice, UsageCost
|
|
@@ -54,6 +67,8 @@ __all__ = [
|
|
|
54
67
|
"KnowledgeBasesApi",
|
|
55
68
|
"MarkupRulesApi",
|
|
56
69
|
"MissionsApi",
|
|
70
|
+
"ModelAliasesApi",
|
|
71
|
+
"NotificationsApi",
|
|
57
72
|
"ProjectsApi",
|
|
58
73
|
"ProvidersApi",
|
|
59
74
|
"ServicePricesApi",
|
|
@@ -61,6 +76,7 @@ __all__ = [
|
|
|
61
76
|
"TriggersApi",
|
|
62
77
|
"WorkflowsApi",
|
|
63
78
|
"HumanTasksApi",
|
|
79
|
+
"ChannelsApi",
|
|
64
80
|
# Domain models (re-exported from persona-models)
|
|
65
81
|
"Paginated",
|
|
66
82
|
"AgentConfiguration",
|
|
@@ -75,6 +91,14 @@ __all__ = [
|
|
|
75
91
|
"GenerationResponse",
|
|
76
92
|
"CreateMissionRequest",
|
|
77
93
|
"Mission",
|
|
94
|
+
"ModelAlias",
|
|
95
|
+
"ModelAliasDefaults",
|
|
96
|
+
"AliasImpact",
|
|
97
|
+
"AliasSuggestions",
|
|
98
|
+
"Notification",
|
|
99
|
+
"alias_ref",
|
|
100
|
+
"is_alias_ref",
|
|
101
|
+
"alias_name_from_ref",
|
|
78
102
|
"Project",
|
|
79
103
|
"Provider",
|
|
80
104
|
"ProviderModel",
|
|
@@ -139,6 +163,7 @@ class PersonaSdk:
|
|
|
139
163
|
self.agents = AgentsApi(self.client)
|
|
140
164
|
self.workflows = WorkflowsApi(self.workflows_client)
|
|
141
165
|
self.human_tasks = HumanTasksApi(self.workflows_client)
|
|
166
|
+
self.channels = ChannelsApi(self.workflows_client)
|
|
142
167
|
self.triggers = TriggersApi(self.client)
|
|
143
168
|
self.sessions = SessionsApi(self.client)
|
|
144
169
|
self.credentials = CredentialsApi(self.client)
|
|
@@ -149,6 +174,8 @@ class PersonaSdk:
|
|
|
149
174
|
self.service_prices = ServicePricesApi(self.client)
|
|
150
175
|
self.providers = ProvidersApi(self.client)
|
|
151
176
|
self.markup_rules = MarkupRulesApi(self.client)
|
|
177
|
+
self.model_aliases = ModelAliasesApi(self.client)
|
|
178
|
+
self.notifications = NotificationsApi(self.client)
|
|
152
179
|
|
|
153
180
|
async def close(self):
|
|
154
181
|
await self.client.close()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from persona_sdk.client import PersonaClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ChannelsApi:
|
|
9
|
+
"""
|
|
10
|
+
Notification channels: where the project's human tasks are announced
|
|
11
|
+
and can be answered outside the inbox (a Telegram group, one message
|
|
12
|
+
per task). Linking happens through deep links: the service
|
|
13
|
+
returns a one-shot URL (``t.me/<bot>?startgroup=...`` for a group,
|
|
14
|
+
``t.me/<bot>?start=...`` for the user's own account).
|
|
15
|
+
|
|
16
|
+
Lives on the workflows service (same client as ``HumanTasksApi``).
|
|
17
|
+
Identity endpoints require a user (IAM bearer) session.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client: PersonaClient):
|
|
21
|
+
self._client = client
|
|
22
|
+
|
|
23
|
+
async def providers(self) -> list[dict[str, Any]]:
|
|
24
|
+
"""The providers enabled on the server (e.g. the Telegram bot)."""
|
|
25
|
+
return await self._client.get("/channels/providers")
|
|
26
|
+
|
|
27
|
+
async def list(self) -> list[dict[str, Any]]:
|
|
28
|
+
"""The project's channels, with their setup status."""
|
|
29
|
+
return await self._client.get("/channels")
|
|
30
|
+
|
|
31
|
+
async def create_link(self, provider: str = "telegram") -> dict[str, Any]:
|
|
32
|
+
"""One-shot deep link that links a group to the project."""
|
|
33
|
+
return await self._client.post(f"/channels/{provider}/link-token")
|
|
34
|
+
|
|
35
|
+
async def update(
|
|
36
|
+
self,
|
|
37
|
+
channel_id: str,
|
|
38
|
+
include_payload: bool | None = None,
|
|
39
|
+
locale: str | None = None,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
body: dict[str, Any] = {}
|
|
42
|
+
if include_payload is not None:
|
|
43
|
+
body["includePayload"] = include_payload
|
|
44
|
+
if locale:
|
|
45
|
+
body["locale"] = locale
|
|
46
|
+
return await self._client.patch(f"/channels/{channel_id}", json=body)
|
|
47
|
+
|
|
48
|
+
async def verify(self, channel_id: str) -> dict[str, Any]:
|
|
49
|
+
"""Re-checks the channel setup (the bot can still post in the group)."""
|
|
50
|
+
return await self._client.post(f"/channels/{channel_id}/verify")
|
|
51
|
+
|
|
52
|
+
async def unlink(self, channel_id: str) -> None:
|
|
53
|
+
await self._client.delete(f"/channels/{channel_id}")
|
|
54
|
+
|
|
55
|
+
async def my_identities(self) -> list[dict[str, Any]]:
|
|
56
|
+
"""The caller's linked accounts."""
|
|
57
|
+
return await self._client.get("/channels/identities/me")
|
|
58
|
+
|
|
59
|
+
async def create_identity_link(self, provider: str = "telegram") -> dict[str, Any]:
|
|
60
|
+
"""One-shot deep link that links the caller's account."""
|
|
61
|
+
return await self._client.post(f"/channels/identities/{provider}/link-token")
|
|
62
|
+
|
|
63
|
+
async def unlink_identity(self, provider: str = "telegram") -> None:
|
|
64
|
+
await self._client.delete(f"/channels/identities/me/{provider}")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from persona_models.common import Paginated
|
|
7
|
+
from persona_models.model_aliases import AliasImpact, AliasSuggestions, ModelAlias
|
|
8
|
+
|
|
9
|
+
from persona_sdk.client import PersonaClient
|
|
10
|
+
|
|
11
|
+
# The only fields a client controls. Health, history and timestamps are managed by
|
|
12
|
+
# the server and persona-core rejects ``createdAt: null`` / ``updatedAt: null``.
|
|
13
|
+
_INPUT_FIELDS = {
|
|
14
|
+
"name": "name",
|
|
15
|
+
"target": "target",
|
|
16
|
+
"project_id": "projectId",
|
|
17
|
+
"projectId": "projectId",
|
|
18
|
+
"description": "description",
|
|
19
|
+
"fallbacks": "fallbacks",
|
|
20
|
+
"defaults": "defaults",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _alias_payload(alias: ModelAlias | dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
"""Build the write body: user-controlled fields only, camelCase, no ``None``."""
|
|
26
|
+
if isinstance(alias, ModelAlias):
|
|
27
|
+
data = alias.model_dump(by_alias=True, exclude_none=True)
|
|
28
|
+
return {k: data[k] for k in set(_INPUT_FIELDS.values()) if k in data}
|
|
29
|
+
payload: dict[str, Any] = {}
|
|
30
|
+
for key, value in alias.items():
|
|
31
|
+
field = _INPUT_FIELDS.get(key)
|
|
32
|
+
if field is None or value is None:
|
|
33
|
+
continue
|
|
34
|
+
if field == "defaults" and hasattr(value, "model_dump"):
|
|
35
|
+
value = value.model_dump(by_alias=True, exclude_none=True)
|
|
36
|
+
elif field == "defaults":
|
|
37
|
+
value = {k: v for k, v in value.items() if v is not None}
|
|
38
|
+
payload[field] = value
|
|
39
|
+
return payload
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ModelAliasesApi:
|
|
43
|
+
"""Model aliases: a named indirection ``alias:<name>`` to a concrete
|
|
44
|
+
``provider/model``, with ordered fallbacks and optional defaults. Writing needs
|
|
45
|
+
a super admin, a project admin or the project API key."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, client: PersonaClient):
|
|
48
|
+
self._client = client
|
|
49
|
+
|
|
50
|
+
async def list(
|
|
51
|
+
self,
|
|
52
|
+
project_id: str | None = None,
|
|
53
|
+
keyword: str | None = None,
|
|
54
|
+
page: int = 1,
|
|
55
|
+
size: int = 100,
|
|
56
|
+
) -> Paginated[ModelAlias]:
|
|
57
|
+
"""Aliases visible to the caller (their project's and the global ones)."""
|
|
58
|
+
params: dict[str, Any] = {"page": page, "size": size}
|
|
59
|
+
if project_id:
|
|
60
|
+
params["projectId"] = project_id
|
|
61
|
+
if keyword:
|
|
62
|
+
params["keyword"] = keyword
|
|
63
|
+
data = await self._client.get("/model-aliases", params=params)
|
|
64
|
+
return Paginated[ModelAlias].model_validate(data)
|
|
65
|
+
|
|
66
|
+
async def get(self, alias_id: str) -> ModelAlias:
|
|
67
|
+
data = await self._client.get(f"/model-aliases/{alias_id}")
|
|
68
|
+
return ModelAlias.model_validate(data)
|
|
69
|
+
|
|
70
|
+
async def create(self, alias: ModelAlias | dict[str, Any]) -> ModelAlias:
|
|
71
|
+
"""Create an alias. Only name, target, projectId, description, fallbacks and
|
|
72
|
+
defaults are sent; server-managed fields are never sent."""
|
|
73
|
+
data = await self._client.post("/model-aliases", json=_alias_payload(alias))
|
|
74
|
+
return ModelAlias.model_validate(data)
|
|
75
|
+
|
|
76
|
+
async def update(
|
|
77
|
+
self, alias_id: str, alias: ModelAlias | dict[str, Any]
|
|
78
|
+
) -> ModelAlias:
|
|
79
|
+
"""Change target, fallbacks, defaults and description (``name`` and scope
|
|
80
|
+
are immutable)."""
|
|
81
|
+
data = await self._client.put(
|
|
82
|
+
f"/model-aliases/{alias_id}", json=_alias_payload(alias)
|
|
83
|
+
)
|
|
84
|
+
return ModelAlias.model_validate(data)
|
|
85
|
+
|
|
86
|
+
async def remove(self, alias_id: str) -> None:
|
|
87
|
+
"""Delete an alias. Refused with a 409 while agents still reference it: use
|
|
88
|
+
``ModelAliasesApi.in_use`` on the raised error to read which ones."""
|
|
89
|
+
await self._client.delete(f"/model-aliases/{alias_id}")
|
|
90
|
+
|
|
91
|
+
async def impact(self, alias_id: str) -> AliasImpact:
|
|
92
|
+
"""Agents that reference the alias, and in which model slot."""
|
|
93
|
+
data = await self._client.get(f"/model-aliases/{alias_id}/impact")
|
|
94
|
+
return AliasImpact.model_validate(data)
|
|
95
|
+
|
|
96
|
+
async def check(self, alias_id: str) -> ModelAlias:
|
|
97
|
+
"""Run the health check now; stores the result in ``health``."""
|
|
98
|
+
data = await self._client.post(f"/model-aliases/{alias_id}/check")
|
|
99
|
+
return ModelAlias.model_validate(data)
|
|
100
|
+
|
|
101
|
+
async def suggestions(
|
|
102
|
+
self, alias_id: str, model: str | None = None, limit: int | None = None
|
|
103
|
+
) -> AliasSuggestions:
|
|
104
|
+
"""Replacement and fallback candidates for the alias' target, or for
|
|
105
|
+
``model``. A proposal only: nothing is applied."""
|
|
106
|
+
params: dict[str, Any] = {}
|
|
107
|
+
if model:
|
|
108
|
+
params["model"] = model
|
|
109
|
+
if limit is not None:
|
|
110
|
+
params["limit"] = limit
|
|
111
|
+
data = await self._client.get(
|
|
112
|
+
f"/model-aliases/{alias_id}/suggestions", params=params or None
|
|
113
|
+
)
|
|
114
|
+
return AliasSuggestions.model_validate(data)
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def in_use(error: BaseException) -> AliasImpact | None:
|
|
118
|
+
"""The agents blocking a delete, when ``error`` is the 409 raised by
|
|
119
|
+
:meth:`remove`; ``None`` for any other error."""
|
|
120
|
+
if (
|
|
121
|
+
not isinstance(error, httpx.HTTPStatusError)
|
|
122
|
+
or error.response.status_code != 409
|
|
123
|
+
):
|
|
124
|
+
return None
|
|
125
|
+
try:
|
|
126
|
+
impact = error.response.json()["detail"]["impact"]
|
|
127
|
+
if not isinstance(impact.get("total"), int):
|
|
128
|
+
return None
|
|
129
|
+
return AliasImpact.model_validate(impact)
|
|
130
|
+
except Exception:
|
|
131
|
+
return None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from persona_models.common import Paginated
|
|
6
|
+
from persona_models.notifications import Notification
|
|
7
|
+
|
|
8
|
+
from persona_sdk.client import PersonaClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NotificationsApi:
|
|
12
|
+
"""In-platform notifications. A project sees its own; platform-wide ones (no
|
|
13
|
+
project) are for super admins. Read state is shared by everyone who can see a
|
|
14
|
+
notification."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: PersonaClient):
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
async def list(
|
|
20
|
+
self,
|
|
21
|
+
unread_only: bool = False,
|
|
22
|
+
include_resolved: bool = False,
|
|
23
|
+
project_id: str | None = None,
|
|
24
|
+
page: int = 1,
|
|
25
|
+
size: int = 20,
|
|
26
|
+
) -> Paginated[Notification]:
|
|
27
|
+
params: dict[str, Any] = {"page": page, "size": size}
|
|
28
|
+
if unread_only:
|
|
29
|
+
params["unreadOnly"] = True
|
|
30
|
+
if include_resolved:
|
|
31
|
+
params["includeResolved"] = True
|
|
32
|
+
if project_id:
|
|
33
|
+
params["projectId"] = project_id
|
|
34
|
+
data = await self._client.get("/notifications", params=params)
|
|
35
|
+
return Paginated[Notification].model_validate(data)
|
|
36
|
+
|
|
37
|
+
async def unread_count(self, project_id: str | None = None) -> int:
|
|
38
|
+
"""Open, unread notifications: the value of the badge."""
|
|
39
|
+
params = {"projectId": project_id} if project_id else None
|
|
40
|
+
data = await self._client.get("/notifications/unread-count", params=params)
|
|
41
|
+
return data["count"]
|
|
42
|
+
|
|
43
|
+
async def mark_read(self, notification_id: str) -> Notification:
|
|
44
|
+
data = await self._client.post(f"/notifications/{notification_id}/read")
|
|
45
|
+
return Notification.model_validate(data)
|
|
46
|
+
|
|
47
|
+
async def mark_all_read(self, project_id: str | None = None) -> None:
|
|
48
|
+
"""Mark as read everything the caller can see."""
|
|
49
|
+
params = {"projectId": project_id} if project_id else None
|
|
50
|
+
await self._client.post("/notifications/read-all", params=params)
|
|
@@ -8,6 +8,11 @@ from persona_sdk.auth import (
|
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def _body(response: httpx.Response) -> Any:
|
|
12
|
+
"""Decoded JSON body, or ``None`` for an empty one (e.g. ``204 No Content``)."""
|
|
13
|
+
return response.json() if response.content else None
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
class PersonaClient:
|
|
12
17
|
"""
|
|
13
18
|
HTTP client for the Persona API.
|
|
@@ -56,7 +61,7 @@ class PersonaClient:
|
|
|
56
61
|
async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
57
62
|
response = await self._client.get(path, params=params)
|
|
58
63
|
response.raise_for_status()
|
|
59
|
-
return response
|
|
64
|
+
return _body(response)
|
|
60
65
|
|
|
61
66
|
async def post(
|
|
62
67
|
self,
|
|
@@ -66,22 +71,22 @@ class PersonaClient:
|
|
|
66
71
|
) -> Any:
|
|
67
72
|
response = await self._client.post(path, json=json, params=params)
|
|
68
73
|
response.raise_for_status()
|
|
69
|
-
return response
|
|
74
|
+
return _body(response)
|
|
70
75
|
|
|
71
76
|
async def put(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
72
77
|
response = await self._client.put(path, json=json)
|
|
73
78
|
response.raise_for_status()
|
|
74
|
-
return response
|
|
79
|
+
return _body(response)
|
|
75
80
|
|
|
76
81
|
async def patch(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
77
82
|
response = await self._client.patch(path, json=json)
|
|
78
83
|
response.raise_for_status()
|
|
79
|
-
return response
|
|
84
|
+
return _body(response)
|
|
80
85
|
|
|
81
86
|
async def delete(self, path: str) -> Any:
|
|
82
87
|
response = await self._client.delete(path)
|
|
83
88
|
response.raise_for_status()
|
|
84
|
-
return response
|
|
89
|
+
return _body(response)
|
|
85
90
|
|
|
86
91
|
async def close(self):
|
|
87
92
|
await self._client.aclose()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "persona-sdk-python"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.26"
|
|
4
4
|
description = "Async Python SDK for the Persona AI platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Framework :: AsyncIO",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"persona-models>=0.2.
|
|
25
|
+
"persona-models>=0.2.20",
|
|
26
26
|
"httpx>=0.27.0",
|
|
27
27
|
]
|
|
28
28
|
|
|
@@ -36,3 +36,14 @@ build-backend = "hatchling.build"
|
|
|
36
36
|
|
|
37
37
|
[tool.hatch.build.targets.wheel]
|
|
38
38
|
packages = ["persona_sdk"]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=8",
|
|
43
|
+
"pytest-asyncio>=0.23",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
49
|
+
testpaths = ["tests"]
|
|
File without changes
|