socaity-cli 0.1.8__tar.gz → 0.1.9__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.
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/PKG-INFO +1 -1
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/pyproject.toml +1 -1
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/backend_client.py +2 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/cli.py +9 -4
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/__init__.py +8 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/catalog.py +69 -109
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/conversations.py +11 -5
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/files.py +6 -6
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/jobs.py +21 -1
- socaity_cli-0.1.9/socaity_cli/clients/profile.py +29 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/workflows.py +9 -9
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/errors.py +4 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/http.py +17 -10
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/sdk_commands.py +15 -12
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/PKG-INFO +1 -1
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/SOURCES.txt +1 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/README.md +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/setup.cfg +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/__init__.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/__main__.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/apipod_commands.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/auth.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/analytics.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/deployment.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/interrupts.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/projects.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/sdk_install.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/credentials.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/deploy_messages.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/deployment.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/prompts.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/registry_push.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/requires.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/dependency_links.txt +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/entry_points.txt +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/requires.txt +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/top_level.txt +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_cli.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_credentials.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_private_slot_error.py +0 -0
- {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_registry_push.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "socaity-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "Socaity platform CLI: login, catalog browsing, SDK installs and APIPod deployment commands."
|
|
9
9
|
requires-python = ">=3.8"
|
|
10
10
|
authors = [
|
|
@@ -6,6 +6,7 @@ from socaity_cli.clients.deployment import DeploymentClient
|
|
|
6
6
|
from socaity_cli.clients.files import FilesClient
|
|
7
7
|
from socaity_cli.clients.interrupts import InterruptsClient
|
|
8
8
|
from socaity_cli.clients.jobs import JobsClient
|
|
9
|
+
from socaity_cli.clients.profile import ProfileClient
|
|
9
10
|
from socaity_cli.clients.projects import ProjectsClient
|
|
10
11
|
from socaity_cli.clients.sdk_install import SdkInstallClient
|
|
11
12
|
from socaity_cli.clients.workflows import WorkflowsClient
|
|
@@ -24,6 +25,7 @@ class SocaityBackendClient(
|
|
|
24
25
|
ConversationsClient,
|
|
25
26
|
InterruptsClient,
|
|
26
27
|
WorkflowsClient,
|
|
28
|
+
ProfileClient,
|
|
27
29
|
):
|
|
28
30
|
"""Facade over domain mixins. Prefer importing a domain client when you only need one surface."""
|
|
29
31
|
pass
|
|
@@ -16,7 +16,7 @@ from socaity_cli.sdk_commands import run_install, run_update, run_workflow_comma
|
|
|
16
16
|
def _run_list(entity: str, category: str | None, family: str | None, limit: int) -> None:
|
|
17
17
|
client = SocaityBackendClient()
|
|
18
18
|
if entity == "services":
|
|
19
|
-
services = client.
|
|
19
|
+
services = client.query_services(category=category, limit=limit)
|
|
20
20
|
if not services:
|
|
21
21
|
print("No services found.")
|
|
22
22
|
return
|
|
@@ -26,7 +26,7 @@ def _run_list(entity: str, category: str | None, family: str | None, limit: int)
|
|
|
26
26
|
desc = f" - {service.short_desc}" if service.short_desc else ""
|
|
27
27
|
print(f"{name}{official}{desc}")
|
|
28
28
|
else:
|
|
29
|
-
models = client.
|
|
29
|
+
models = client.query_models(family=family, limit=limit)
|
|
30
30
|
if not models:
|
|
31
31
|
print("No models found.")
|
|
32
32
|
return
|
|
@@ -137,7 +137,7 @@ def _run_workflows(args) -> None:
|
|
|
137
137
|
command = args.workflows_command
|
|
138
138
|
|
|
139
139
|
if command == "list":
|
|
140
|
-
workflows = client.
|
|
140
|
+
workflows = client.query_workflows(q=args.q, limit=args.limit)
|
|
141
141
|
if not workflows:
|
|
142
142
|
print("No workflows found.")
|
|
143
143
|
return
|
|
@@ -224,7 +224,12 @@ def _run_search(query: str, collections: str | None, limit: int) -> None:
|
|
|
224
224
|
targets = [part.strip() for part in (collections or "services,models").split(",") if part.strip()]
|
|
225
225
|
found = False
|
|
226
226
|
for collection in targets:
|
|
227
|
-
|
|
227
|
+
if collection == "models":
|
|
228
|
+
hits = client.query_models(q=query, limit=limit)
|
|
229
|
+
elif collection == "jobs":
|
|
230
|
+
hits = client.query_jobs(q=query, limit=limit)
|
|
231
|
+
else:
|
|
232
|
+
hits = client.query_services(q=query, limit=limit)
|
|
228
233
|
if not hits:
|
|
229
234
|
continue
|
|
230
235
|
found = True
|
|
@@ -3,16 +3,24 @@ from socaity_cli.clients.analytics import AnalyticsClient
|
|
|
3
3
|
from socaity_cli.clients.catalog import CatalogClient
|
|
4
4
|
from socaity_cli.clients.deployment import DeploymentClient
|
|
5
5
|
from socaity_cli.clients.files import FilesClient
|
|
6
|
+
from socaity_cli.clients.conversations import ConversationsClient
|
|
7
|
+
from socaity_cli.clients.interrupts import InterruptsClient
|
|
6
8
|
from socaity_cli.clients.jobs import JobsClient
|
|
9
|
+
from socaity_cli.clients.profile import ProfileClient
|
|
7
10
|
from socaity_cli.clients.projects import ProjectsClient
|
|
8
11
|
from socaity_cli.clients.sdk_install import SdkInstallClient
|
|
12
|
+
from socaity_cli.clients.workflows import WorkflowsClient
|
|
9
13
|
|
|
10
14
|
__all__ = [
|
|
11
15
|
"AnalyticsClient",
|
|
12
16
|
"CatalogClient",
|
|
17
|
+
"ConversationsClient",
|
|
13
18
|
"DeploymentClient",
|
|
14
19
|
"FilesClient",
|
|
20
|
+
"InterruptsClient",
|
|
15
21
|
"JobsClient",
|
|
22
|
+
"ProfileClient",
|
|
16
23
|
"ProjectsClient",
|
|
17
24
|
"SdkInstallClient",
|
|
25
|
+
"WorkflowsClient",
|
|
18
26
|
]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""AI catalog reads (`/v1/catalog`)."""
|
|
2
2
|
import re
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import List, Optional
|
|
4
4
|
|
|
5
|
-
from socaity_schemas.platform import AIModel, AIService,
|
|
5
|
+
from socaity_schemas.platform import AIModel, AIService, ServiceCategory
|
|
6
6
|
|
|
7
7
|
SERVICE_LIST_FIELDS = ["id", "name", "display_name", "short_desc", "categories", "is_official", "n_usages"]
|
|
8
8
|
SERVICE_DETAIL_EXPAND = ["deployments.contract", "endpoints", "models"]
|
|
@@ -10,38 +10,42 @@ MODEL_LIST_FIELDS = ["id", "name", "display_name", "family", "release_date", "hu
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class CatalogClient:
|
|
13
|
-
"""Mixin: catalog
|
|
13
|
+
"""Mixin: catalog query/get. Requires ``BackendHttp`` methods on ``self``."""
|
|
14
14
|
|
|
15
15
|
SERVICE_DETAIL_EXPAND = SERVICE_DETAIL_EXPAND
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def query_services(
|
|
18
18
|
self,
|
|
19
|
-
|
|
19
|
+
q: Optional[str] = None,
|
|
20
20
|
category: Optional[str] = None,
|
|
21
21
|
filters: Optional[List[str]] = None,
|
|
22
22
|
expand: Optional[List[str]] = None,
|
|
23
23
|
fields: Optional[List[str]] = None,
|
|
24
24
|
sort: Optional[str] = None,
|
|
25
|
-
limit: int =
|
|
25
|
+
limit: int = 100,
|
|
26
26
|
offset: int = 0,
|
|
27
|
+
known_ids: Optional[List[str]] = None,
|
|
28
|
+
known_fieldset: Optional[str] = None,
|
|
27
29
|
mine: bool = False,
|
|
28
30
|
) -> List[AIService]:
|
|
29
|
-
"""
|
|
31
|
+
"""Query AI services in the socaity catalog.
|
|
30
32
|
|
|
31
|
-
Start here for any generation task.
|
|
32
|
-
"voice cloning"). Results are slim catalog rows;
|
|
33
|
-
a hit ``id`` for endpoints and the call
|
|
34
|
-
``deployments.contract``.
|
|
33
|
+
Start here for any generation task. Query by capability ("text to image",
|
|
34
|
+
"voice cloning") or browse with filters. Results are slim catalog rows;
|
|
35
|
+
call ``get_service`` with a hit ``id`` for endpoints and the call
|
|
36
|
+
signature under ``deployments.contract``.
|
|
35
37
|
|
|
36
38
|
Args:
|
|
37
|
-
|
|
38
|
-
category: Category id (not display name).
|
|
39
|
+
q: Free-text query. Omit to browse.
|
|
40
|
+
category: Category id (not display name). Appended as a filter.
|
|
39
41
|
filters: Repeatable ``field:operator:value`` strings.
|
|
40
42
|
expand: Relations to embed (``deployments``, ``models``, ``pricing``).
|
|
41
43
|
fields: Sparse fieldset, e.g. ``["id", "name"]``.
|
|
42
44
|
sort: ``field:asc`` or ``field:desc``.
|
|
43
45
|
limit: Page size, 1 to 100.
|
|
44
46
|
offset: Skip this many rows; increase by ``limit`` for the next page.
|
|
47
|
+
known_ids: Client cache ids for delta fetch.
|
|
48
|
+
known_fieldset: Client cache fieldset for delta fetch.
|
|
45
49
|
mine: Only services owned by the caller.
|
|
46
50
|
|
|
47
51
|
Returns:
|
|
@@ -50,51 +54,9 @@ class CatalogClient:
|
|
|
50
54
|
service_filters = list(filters or [])
|
|
51
55
|
if category:
|
|
52
56
|
service_filters.append(f"categories:contains:{category}")
|
|
53
|
-
rows = self.query_services(
|
|
54
|
-
q=query,
|
|
55
|
-
filters=service_filters or None,
|
|
56
|
-
expand=expand,
|
|
57
|
-
fields=fields,
|
|
58
|
-
sort=sort,
|
|
59
|
-
limit=limit,
|
|
60
|
-
offset=offset,
|
|
61
|
-
mine=mine,
|
|
62
|
-
)
|
|
63
|
-
# Stale Typesense (watermark after empty first sync, or a later
|
|
64
|
-
# write-through of one service) can return [] or the wrong hit while
|
|
65
|
-
# GET /services still lists the row. Browse + local token match fills
|
|
66
|
-
# the gap without waiting for an index rebuild.
|
|
67
|
-
if query and not mine:
|
|
68
|
-
matched = [row for row in rows if _service_matches_query(row, query)]
|
|
69
|
-
if matched:
|
|
70
|
-
return matched[:limit]
|
|
71
|
-
browsed = self.query_services(
|
|
72
|
-
filters=service_filters or None,
|
|
73
|
-
expand=expand,
|
|
74
|
-
fields=fields,
|
|
75
|
-
sort=sort,
|
|
76
|
-
limit=max(limit, 100),
|
|
77
|
-
offset=0,
|
|
78
|
-
)
|
|
79
|
-
return [row for row in browsed if _service_matches_query(row, query)][:limit]
|
|
80
|
-
return rows
|
|
81
|
-
|
|
82
|
-
def query_services(
|
|
83
|
-
self,
|
|
84
|
-
q: Optional[str] = None,
|
|
85
|
-
filters: Optional[List[str]] = None,
|
|
86
|
-
expand: Optional[List[str]] = None,
|
|
87
|
-
fields: Optional[List[str]] = None,
|
|
88
|
-
sort: Optional[str] = None,
|
|
89
|
-
limit: int = 100,
|
|
90
|
-
offset: int = 0,
|
|
91
|
-
known_ids: Optional[List[str]] = None,
|
|
92
|
-
known_fieldset: Optional[str] = None,
|
|
93
|
-
mine: bool = False,
|
|
94
|
-
) -> List[AIService]:
|
|
95
57
|
rows = self._unwrap_list(self._get("v1/catalog/services", params={
|
|
96
58
|
"q": q,
|
|
97
|
-
"filter":
|
|
59
|
+
"filter": service_filters,
|
|
98
60
|
"expand": expand or [],
|
|
99
61
|
"fields": fields if fields is not None else SERVICE_LIST_FIELDS,
|
|
100
62
|
"sort": sort,
|
|
@@ -104,24 +66,27 @@ class CatalogClient:
|
|
|
104
66
|
"known_fieldset": known_fieldset,
|
|
105
67
|
"mine": mine,
|
|
106
68
|
}))
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
69
|
+
services = [AIService(**row) for row in rows]
|
|
70
|
+
# Stale Typesense (watermark after empty first sync, or a later
|
|
71
|
+
# write-through of one service) can return [] or the wrong hit while
|
|
72
|
+
# GET /services still lists the row. Browse + local token match fills
|
|
73
|
+
# the gap without waiting for an index rebuild.
|
|
74
|
+
if q and not mine:
|
|
75
|
+
matched = [row for row in services if _service_matches_query(row, q)]
|
|
76
|
+
if matched:
|
|
77
|
+
return matched[:limit]
|
|
78
|
+
browsed = self._unwrap_list(self._get("v1/catalog/services", params={
|
|
79
|
+
"filter": service_filters,
|
|
80
|
+
"expand": expand or [],
|
|
81
|
+
"fields": fields if fields is not None else SERVICE_LIST_FIELDS,
|
|
82
|
+
"sort": sort,
|
|
83
|
+
"limit": max(limit, 100),
|
|
84
|
+
"offset": 0,
|
|
85
|
+
"mine": mine,
|
|
86
|
+
}))
|
|
87
|
+
browsed_services = [AIService(**row) for row in browsed]
|
|
88
|
+
return [row for row in browsed_services if _service_matches_query(row, q)][:limit]
|
|
89
|
+
return services
|
|
125
90
|
|
|
126
91
|
def get_service(
|
|
127
92
|
self,
|
|
@@ -137,7 +102,7 @@ class CatalogClient:
|
|
|
137
102
|
``deployments[].contract.endpoints[].parameters``.
|
|
138
103
|
|
|
139
104
|
Args:
|
|
140
|
-
identifier: Service id or slug from ``
|
|
105
|
+
identifier: Service id or slug from ``query_services``.
|
|
141
106
|
expand: Override the default expand list.
|
|
142
107
|
fields: Sparse fieldset.
|
|
143
108
|
filters: Post-fetch filters.
|
|
@@ -155,6 +120,8 @@ class CatalogClient:
|
|
|
155
120
|
def query_models(
|
|
156
121
|
self,
|
|
157
122
|
q: Optional[str] = None,
|
|
123
|
+
family: Optional[str] = None,
|
|
124
|
+
task: Optional[str] = None,
|
|
158
125
|
filters: Optional[List[str]] = None,
|
|
159
126
|
expand: Optional[List[str]] = None,
|
|
160
127
|
fields: Optional[List[str]] = None,
|
|
@@ -164,9 +131,32 @@ class CatalogClient:
|
|
|
164
131
|
known_ids: Optional[List[str]] = None,
|
|
165
132
|
known_fieldset: Optional[str] = None,
|
|
166
133
|
) -> List[AIModel]:
|
|
134
|
+
"""Query AI models in the catalog.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
q: Free-text query. Omit to browse.
|
|
138
|
+
family: Model family, e.g. ``flux``. Appended as a filter.
|
|
139
|
+
task: Task slug, e.g. ``text2image``. Appended as a filter.
|
|
140
|
+
filters: Repeatable ``field:operator:value`` strings.
|
|
141
|
+
expand: Relations to embed (``scores``, ``capabilities``).
|
|
142
|
+
fields: Sparse fieldset.
|
|
143
|
+
sort: ``field:asc`` or ``field:desc``.
|
|
144
|
+
limit: Page size.
|
|
145
|
+
offset: Skip this many rows.
|
|
146
|
+
known_ids: Client cache ids for delta fetch.
|
|
147
|
+
known_fieldset: Client cache fieldset for delta fetch.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Model rows, or an empty list when nothing matches.
|
|
151
|
+
"""
|
|
152
|
+
model_filters = list(filters or [])
|
|
153
|
+
if family:
|
|
154
|
+
model_filters.append(f"family:eq:{family}")
|
|
155
|
+
if task:
|
|
156
|
+
model_filters.append(f"tasks:contains:{task}")
|
|
167
157
|
rows = self._unwrap_list(self._get("v1/catalog/models", params={
|
|
168
158
|
"q": q,
|
|
169
|
-
"filter":
|
|
159
|
+
"filter": model_filters,
|
|
170
160
|
"expand": expand or [],
|
|
171
161
|
"fields": fields if fields is not None else MODEL_LIST_FIELDS,
|
|
172
162
|
"sort": sort,
|
|
@@ -177,22 +167,6 @@ class CatalogClient:
|
|
|
177
167
|
}))
|
|
178
168
|
return [AIModel(**row) for row in rows]
|
|
179
169
|
|
|
180
|
-
def list_models(
|
|
181
|
-
self,
|
|
182
|
-
family: Optional[str] = None,
|
|
183
|
-
task: Optional[str] = None,
|
|
184
|
-
fields: Optional[List[str]] = None,
|
|
185
|
-
expand: Optional[List[str]] = None,
|
|
186
|
-
limit: int = 100,
|
|
187
|
-
offset: int = 0,
|
|
188
|
-
) -> List[AIModel]:
|
|
189
|
-
filters: List[str] = []
|
|
190
|
-
if family:
|
|
191
|
-
filters.append(f"family:eq:{family}")
|
|
192
|
-
if task:
|
|
193
|
-
filters.append(f"tasks:contains:{task}")
|
|
194
|
-
return self.query_models(filters=filters or None, expand=expand, fields=fields, limit=limit, offset=offset)
|
|
195
|
-
|
|
196
170
|
def get_model(
|
|
197
171
|
self,
|
|
198
172
|
id_or_name: str,
|
|
@@ -205,33 +179,19 @@ class CatalogClient:
|
|
|
205
179
|
})
|
|
206
180
|
return AIModel(**row) if row else None
|
|
207
181
|
|
|
208
|
-
def
|
|
182
|
+
def query_categories(self) -> List[ServiceCategory]:
|
|
183
|
+
"""Query catalog task categories."""
|
|
209
184
|
rows = self._get("v1/catalog/categories") or []
|
|
210
185
|
if isinstance(rows, dict):
|
|
211
186
|
rows = rows.get("entities") or rows.get("data") or []
|
|
212
187
|
return [ServiceCategory(**row) for row in rows]
|
|
213
188
|
|
|
214
|
-
def list_pricing_rules(self, active_only: bool = True) -> List[
|
|
189
|
+
def list_pricing_rules(self, active_only: bool = True) -> List[dict]:
|
|
215
190
|
rows = self._get("v1/catalog/pricing-rules", params={"active_only": active_only}) or []
|
|
216
191
|
if isinstance(rows, dict):
|
|
217
192
|
rows = rows.get("entities") or rows.get("data") or []
|
|
218
193
|
return rows if isinstance(rows, list) else []
|
|
219
194
|
|
|
220
|
-
def search(
|
|
221
|
-
self,
|
|
222
|
-
query: str,
|
|
223
|
-
collection: str = "services",
|
|
224
|
-
filters: Optional[List[str]] = None,
|
|
225
|
-
limit: int = 20,
|
|
226
|
-
visibility: str = "user_visible",
|
|
227
|
-
) -> List[Union[AIService, AIModel, Job]]:
|
|
228
|
-
"""Full-text search via catalog or jobs ``q`` param (Typesense)."""
|
|
229
|
-
if collection == "models":
|
|
230
|
-
return self.query_models(q=query, filters=filters, limit=limit)
|
|
231
|
-
if collection == "jobs":
|
|
232
|
-
return self.query_jobs(q=query, filters=filters, visibility=visibility, limit=limit)
|
|
233
|
-
return self.search_services(query=query, filters=filters, limit=limit)
|
|
234
|
-
|
|
235
195
|
|
|
236
196
|
def _service_matches_query(service: AIService, text: str) -> bool:
|
|
237
197
|
tokens = _query_tokens(text)
|
|
@@ -6,9 +6,9 @@ from socaity_schemas.platform import Chat, ChatItem
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ConversationsClient:
|
|
9
|
-
"""Mixin: conversation
|
|
9
|
+
"""Mixin: conversation query/get/items. Requires ``BackendHttp`` methods on ``self``."""
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def query_conversations(
|
|
12
12
|
self,
|
|
13
13
|
q: Optional[str] = None,
|
|
14
14
|
filters: Optional[List[str]] = None,
|
|
@@ -43,7 +43,7 @@ class ConversationsClient:
|
|
|
43
43
|
})
|
|
44
44
|
return Chat(**row) if row else None
|
|
45
45
|
|
|
46
|
-
def
|
|
46
|
+
def query_conversation_items(
|
|
47
47
|
self,
|
|
48
48
|
conversation_id: str,
|
|
49
49
|
branch: str = "active",
|
|
@@ -67,15 +67,21 @@ class ConversationsClient:
|
|
|
67
67
|
conversation_id: str,
|
|
68
68
|
title: Optional[str] = None,
|
|
69
69
|
status: Optional[str] = None,
|
|
70
|
+
active_item_id: Optional[str] = None,
|
|
70
71
|
) -> Optional[Chat]:
|
|
71
|
-
"""Patch title and/or
|
|
72
|
+
"""Patch title, status, and/or active_item_id (1/N switch) on a conversation the caller owns."""
|
|
72
73
|
row = self._request(
|
|
73
74
|
"PATCH",
|
|
74
75
|
f"v1/conversations/{conversation_id}",
|
|
75
|
-
params={"title": title, "status": status},
|
|
76
|
+
params={"title": title, "status": status, "active_item_id": active_item_id},
|
|
76
77
|
)
|
|
77
78
|
return Chat(**row) if row else None
|
|
78
79
|
|
|
80
|
+
def fork_conversation(self, conversation_id: str) -> Optional[Chat]:
|
|
81
|
+
"""Copy the conversation tree into a new chats.id."""
|
|
82
|
+
row = self._request("POST", f"v1/conversations/{conversation_id}/fork")
|
|
83
|
+
return Chat(**row) if row else None
|
|
84
|
+
|
|
79
85
|
def delete_conversation(self, conversation_id: str) -> bool:
|
|
80
86
|
"""Soft-delete a conversation and clear its linked job data."""
|
|
81
87
|
return bool(self._request("DELETE", f"v1/conversations/{conversation_id}"))
|
|
@@ -4,11 +4,11 @@ from typing import Dict, List, Optional, Union
|
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
|
-
FILE_LIST_PURPOSES = ["USER_UPLOAD", "
|
|
7
|
+
FILE_LIST_PURPOSES = ["USER_UPLOAD", "JOB_OUTPUT"]
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class FilesClient:
|
|
11
|
-
"""Mixin: storage usage,
|
|
11
|
+
"""Mixin: storage usage, query, get, upload, update, delete. Requires ``BackendHttp`` on ``self``."""
|
|
12
12
|
|
|
13
13
|
def get_storage_usage(self) -> Optional[Dict]:
|
|
14
14
|
"""Return the caller's storage quota and current usage.
|
|
@@ -18,7 +18,7 @@ class FilesClient:
|
|
|
18
18
|
"""
|
|
19
19
|
return self._get("v1/files/usage")
|
|
20
20
|
|
|
21
|
-
def
|
|
21
|
+
def query_files(
|
|
22
22
|
self,
|
|
23
23
|
purpose: Union[str, List[str]] = FILE_LIST_PURPOSES,
|
|
24
24
|
include_expired: bool = False,
|
|
@@ -28,7 +28,7 @@ class FilesClient:
|
|
|
28
28
|
limit: int = 25,
|
|
29
29
|
offset: int = 0,
|
|
30
30
|
) -> List[Dict]:
|
|
31
|
-
"""
|
|
31
|
+
"""Query stored files visible to the caller.
|
|
32
32
|
|
|
33
33
|
Args:
|
|
34
34
|
purpose: Filter by reference purpose. Default: uploads and job results.
|
|
@@ -83,7 +83,7 @@ class FilesClient:
|
|
|
83
83
|
"""Patch file metadata the caller owns. Requires approval in agent mode.
|
|
84
84
|
|
|
85
85
|
Args:
|
|
86
|
-
file_id: File id from ``
|
|
86
|
+
file_id: File id from ``query_files`` or ``get_file``.
|
|
87
87
|
is_public: Publish or unpublish the file.
|
|
88
88
|
expires_at: ISO timestamp when the file should expire.
|
|
89
89
|
clear_expires_at: Clear expiry and keep the file permanently.
|
|
@@ -101,7 +101,7 @@ class FilesClient:
|
|
|
101
101
|
"""Hard-delete a file the caller owns. Requires approval in agent mode.
|
|
102
102
|
|
|
103
103
|
Args:
|
|
104
|
-
file_id: File id from ``
|
|
104
|
+
file_id: File id from ``query_files`` or ``get_file``.
|
|
105
105
|
|
|
106
106
|
Returns:
|
|
107
107
|
``True`` when the delete succeeded, ``False`` otherwise.
|
|
@@ -24,6 +24,26 @@ class JobsClient:
|
|
|
24
24
|
known_ids: Optional[List[str]] = None,
|
|
25
25
|
known_fieldset: Optional[str] = None,
|
|
26
26
|
) -> List[Job]:
|
|
27
|
+
"""Search and list jobs.
|
|
28
|
+
|
|
29
|
+
Last N jobs: ``limit=N`` (catalog default sort is newest first).
|
|
30
|
+
Pass ``q`` to search display names and prompts.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
q: Optional search text.
|
|
34
|
+
filters: Optional catalog filter strings.
|
|
35
|
+
expand: Extra relations. Empty keeps the list slim.
|
|
36
|
+
fields: Optional field projection.
|
|
37
|
+
visibility: ``user_visible`` (default) or ``public``.
|
|
38
|
+
sort: Sort spec. Empty keeps the catalog default (newest first).
|
|
39
|
+
limit: Maximum rows (default 100).
|
|
40
|
+
offset: Skip this many rows.
|
|
41
|
+
known_ids: Client cache ids for delta fetch.
|
|
42
|
+
known_fieldset: Client cache fieldset for delta fetch.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Job records the caller is allowed to see.
|
|
46
|
+
"""
|
|
27
47
|
rows = self._unwrap_list(self._get("v1/jobs", params={
|
|
28
48
|
"q": q,
|
|
29
49
|
"visibility": visibility,
|
|
@@ -54,7 +74,7 @@ class JobsClient:
|
|
|
54
74
|
earlier result.
|
|
55
75
|
|
|
56
76
|
Args:
|
|
57
|
-
job_id: Platform job id from ``run_service`` or ``
|
|
77
|
+
job_id: Platform job id from ``run_service`` or ``query_jobs``.
|
|
58
78
|
|
|
59
79
|
Returns:
|
|
60
80
|
The job record, or ``None`` when not found or not visible.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Identity and token exchange (`/v1/profile`, `/v1/cli-auth`)."""
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ProfileClient:
|
|
6
|
+
"""Mixin: caller identity. Requires ``BackendHttp`` methods on ``self``."""
|
|
7
|
+
|
|
8
|
+
def whoami(self) -> Optional[Dict[str, Any]]:
|
|
9
|
+
"""Return the authenticated caller's identity.
|
|
10
|
+
|
|
11
|
+
Used by hosts that need to verify an opaque ``sk_``/``tk_`` key.
|
|
12
|
+
The backend owns the answer; there is nothing to check locally.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Identity dict with ``user_id`` when the Bearer key is valid,
|
|
16
|
+
or ``None`` when the key is unknown.
|
|
17
|
+
"""
|
|
18
|
+
return self._get("v1/profile/whoami")
|
|
19
|
+
|
|
20
|
+
def exchange_cli_auth(self) -> Optional[Dict[str, Any]]:
|
|
21
|
+
"""Exchange the current Bearer token (JWT) for a platform ``tk_`` key.
|
|
22
|
+
|
|
23
|
+
The Authorization header is the token to exchange, not a platform key.
|
|
24
|
+
MCP OAuth uses this after verifying a Supabase JWT.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Payload containing ``api_key``, or ``None`` when exchange fails.
|
|
28
|
+
"""
|
|
29
|
+
return self._post("v1/cli-auth/exchange")
|
|
@@ -11,7 +11,7 @@ from socaity_schemas.platform.workflow import (
|
|
|
11
11
|
WorkflowUpsertResult,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
WORKFLOW_DETAIL_EXPAND = ["document"]
|
|
14
|
+
WORKFLOW_DETAIL_EXPAND = ["document", "layout"]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _dump_model(value: Union[Dict[str, Any], Any, None]) -> Optional[Dict[str, Any]]:
|
|
@@ -26,9 +26,9 @@ class WorkflowsClient:
|
|
|
26
26
|
|
|
27
27
|
WORKFLOW_DETAIL_EXPAND = WORKFLOW_DETAIL_EXPAND
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def query_workflows(
|
|
30
30
|
self,
|
|
31
|
-
|
|
31
|
+
q: Optional[str] = None,
|
|
32
32
|
filters: Optional[List[str]] = None,
|
|
33
33
|
expand: Optional[List[str]] = None,
|
|
34
34
|
fields: Optional[List[str]] = None,
|
|
@@ -38,7 +38,7 @@ class WorkflowsClient:
|
|
|
38
38
|
) -> List[Workflow]:
|
|
39
39
|
"""Find workflows: your own plus public ones.
|
|
40
40
|
|
|
41
|
-
``
|
|
41
|
+
``q`` searches title, goal, slug and node text. Results are slim
|
|
42
42
|
catalog rows; call ``get_workflow`` with a hit ``id`` (or slug) for the
|
|
43
43
|
full document.
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ class WorkflowsClient:
|
|
|
46
46
|
``slug:eq:<slug>`` and ``visibility:eq:mine|public|blended``.
|
|
47
47
|
|
|
48
48
|
Args:
|
|
49
|
-
|
|
49
|
+
q: Free-text query. Omit to browse.
|
|
50
50
|
filters: Repeatable ``field:operator:value`` strings.
|
|
51
51
|
expand: Relations to embed (``document``, ``revisions``).
|
|
52
52
|
fields: Sparse fieldset, e.g. ``["id", "slug", "title"]``.
|
|
@@ -58,7 +58,7 @@ class WorkflowsClient:
|
|
|
58
58
|
Workflow rows, or an empty list when nothing matches.
|
|
59
59
|
"""
|
|
60
60
|
rows = self._unwrap_list(self._get("v1/workflows", params={
|
|
61
|
-
"q":
|
|
61
|
+
"q": q,
|
|
62
62
|
"filter": filters or [],
|
|
63
63
|
"expand": expand or [],
|
|
64
64
|
"fields": fields or [],
|
|
@@ -82,8 +82,8 @@ class WorkflowsClient:
|
|
|
82
82
|
expects. Pin an older state via ``revision_id`` or ``version``.
|
|
83
83
|
|
|
84
84
|
Args:
|
|
85
|
-
workflow_id: ``wf_`` id or slug from ``
|
|
86
|
-
expand: Override the default expand (``document``, ``revisions``).
|
|
85
|
+
workflow_id: ``wf_`` id or slug from ``query_workflows``.
|
|
86
|
+
expand: Override the default expand (``document``, ``layout``, ``revisions``).
|
|
87
87
|
revision_id: ``rv_`` id to pin; wins over ``version``.
|
|
88
88
|
version: Valid version int; default is the latest.
|
|
89
89
|
|
|
@@ -157,7 +157,7 @@ class WorkflowsClient:
|
|
|
157
157
|
)
|
|
158
158
|
return WorkflowUpsertResult(**row) if row else None
|
|
159
159
|
|
|
160
|
-
def
|
|
160
|
+
def query_workflow_revisions(
|
|
161
161
|
self,
|
|
162
162
|
workflow_id: str,
|
|
163
163
|
expand: Optional[List[str]] = None,
|
|
@@ -40,3 +40,7 @@ class RegistryQuotaExceededError(BackendApiError):
|
|
|
40
40
|
|
|
41
41
|
def __init__(self, message: str, *, detail: Any = None):
|
|
42
42
|
super().__init__(413, message, code="REGISTRY_QUOTA_EXCEEDED", detail=detail)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BackendTransportError(Exception):
|
|
46
|
+
"""Network failure talking to the Socaity backend."""
|
|
@@ -6,7 +6,12 @@ from typing import Any, Dict, Optional
|
|
|
6
6
|
import httpx
|
|
7
7
|
|
|
8
8
|
from socaity_cli.credentials import DEFAULT_BACKEND_URL, get_api_key, load_credentials
|
|
9
|
-
from socaity_cli.errors import
|
|
9
|
+
from socaity_cli.errors import (
|
|
10
|
+
BackendApiError,
|
|
11
|
+
BackendTransportError,
|
|
12
|
+
PrivateSlotLimitError,
|
|
13
|
+
RegistryQuotaExceededError,
|
|
14
|
+
)
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
def resolve_backend_url(backend_url: Optional[str] = None) -> str:
|
|
@@ -82,16 +87,18 @@ class BackendHttp:
|
|
|
82
87
|
headers=self._auth_headers,
|
|
83
88
|
timeout=400,
|
|
84
89
|
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
except Exception as e:
|
|
93
|
-
print(f"Request to {path} failed: {e}")
|
|
90
|
+
except httpx.HTTPError as exc:
|
|
91
|
+
raise BackendTransportError(f"Request to {path} failed: {exc}") from exc
|
|
92
|
+
|
|
93
|
+
if response.status_code in (200, 201, 202):
|
|
94
|
+
return response.json() if response.content else {}
|
|
95
|
+
self._raise_for_structured_error(response.status_code, response.text)
|
|
96
|
+
if response.status_code == 404:
|
|
94
97
|
return None
|
|
98
|
+
raise BackendApiError(
|
|
99
|
+
response.status_code,
|
|
100
|
+
(response.text or f"HTTP {response.status_code}")[:300],
|
|
101
|
+
)
|
|
95
102
|
|
|
96
103
|
@staticmethod
|
|
97
104
|
def _catalog_params(params: Dict) -> Dict:
|
|
@@ -33,22 +33,25 @@ def run_workflow_command(workflow: str, inputs: dict, timeout_s: float | None =
|
|
|
33
33
|
"""Run a workflow through the inference gateway and print the terminal result."""
|
|
34
34
|
import json
|
|
35
35
|
|
|
36
|
-
from socaity.
|
|
36
|
+
from socaity.client import SocaityClient
|
|
37
|
+
from socaity.core.serialize import serialize_job
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
print(f"[{progress:>4.0%}] {message}")
|
|
39
|
+
client = SocaityClient()
|
|
40
40
|
|
|
41
|
-
def
|
|
42
|
-
|
|
41
|
+
def on_progress(event) -> None:
|
|
42
|
+
if event.kind == "started":
|
|
43
|
+
print(f"Job {event.job_id} submitted. Waiting...")
|
|
44
|
+
elif event.kind == "progress" and event.message:
|
|
45
|
+
print(f"[{(event.progress or 0):>4.0%}] {event.message}")
|
|
43
46
|
|
|
44
47
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
timeout_s=timeout_s
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
48
|
+
job = client.run_workflow(workflow, inputs=inputs)
|
|
49
|
+
unsubscribe = job.subscribe(on_started=on_progress, on_progress=on_progress, replay=True)
|
|
50
|
+
try:
|
|
51
|
+
job.get_result(timeout_s=timeout_s)
|
|
52
|
+
finally:
|
|
53
|
+
unsubscribe()
|
|
54
|
+
result = serialize_job(job)
|
|
52
55
|
except (ValueError, RuntimeError) as error:
|
|
53
56
|
print(str(error))
|
|
54
57
|
raise SystemExit(1)
|
|
@@ -29,6 +29,7 @@ socaity_cli/clients/deployment.py
|
|
|
29
29
|
socaity_cli/clients/files.py
|
|
30
30
|
socaity_cli/clients/interrupts.py
|
|
31
31
|
socaity_cli/clients/jobs.py
|
|
32
|
+
socaity_cli/clients/profile.py
|
|
32
33
|
socaity_cli/clients/projects.py
|
|
33
34
|
socaity_cli/clients/sdk_install.py
|
|
34
35
|
socaity_cli/clients/workflows.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|