socaity-cli 0.1.7__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.7 → socaity_cli-0.1.9}/PKG-INFO +5 -5
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/README.md +3 -3
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/pyproject.toml +2 -2
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/backend_client.py +9 -1
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/cli.py +211 -7
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/__init__.py +8 -0
- socaity_cli-0.1.9/socaity_cli/clients/catalog.py +224 -0
- socaity_cli-0.1.9/socaity_cli/clients/conversations.py +87 -0
- socaity_cli-0.1.9/socaity_cli/clients/files.py +156 -0
- socaity_cli-0.1.9/socaity_cli/clients/interrupts.py +61 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/jobs.py +36 -1
- socaity_cli-0.1.9/socaity_cli/clients/profile.py +29 -0
- socaity_cli-0.1.9/socaity_cli/clients/workflows.py +249 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/credentials.py +14 -8
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/deploy_messages.py +2 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/errors.py +4 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/http.py +35 -13
- socaity_cli-0.1.9/socaity_cli/sdk_commands.py +58 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/PKG-INFO +5 -5
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/SOURCES.txt +4 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/requires.txt +1 -1
- socaity_cli-0.1.7/socaity_cli/clients/catalog.py +0 -147
- socaity_cli-0.1.7/socaity_cli/clients/files.py +0 -82
- socaity_cli-0.1.7/socaity_cli/sdk_commands.py +0 -28
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/setup.cfg +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/__init__.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/__main__.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/apipod_commands.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/auth.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/analytics.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/deployment.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/projects.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/sdk_install.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/deployment.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/prompts.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/registry_push.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/requires.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/dependency_links.txt +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/entry_points.txt +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/top_level.txt +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/test/test_cli.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/test/test_credentials.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/test/test_private_slot_error.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.9}/test/test_registry_push.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socaity-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Socaity platform CLI: login, catalog browsing, SDK installs and APIPod deployment commands.
|
|
5
5
|
Author: SocAIty
|
|
6
6
|
License: MIT
|
|
@@ -9,7 +9,7 @@ Project-URL: Homepage, https://www.socaity.ai
|
|
|
9
9
|
Requires-Python: >=3.8
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: httpx
|
|
12
|
-
Requires-Dist: socaity-schemas>=0.1.
|
|
12
|
+
Requires-Dist: socaity-schemas>=0.1.6
|
|
13
13
|
Provides-Extra: dev
|
|
14
14
|
Requires-Dist: pytest; extra == "dev"
|
|
15
15
|
Requires-Dist: ruff; extra == "dev"
|
|
@@ -49,15 +49,15 @@ You get `socaity-cli` as part of `pip install socaity` or `pip install apipod` a
|
|
|
49
49
|
| `socaity start [entrypoint]` | `apipod` | Run an APIPod service locally |
|
|
50
50
|
| `socaity deploy [target]` | `apipod` | Deploy to Socaity (wraps `apipod deploy`) |
|
|
51
51
|
|
|
52
|
-
Use
|
|
53
|
-
(e.g. `socaity -
|
|
52
|
+
Use `--project-dir DIR` with scan/build/start/deploy to point at a service project directory
|
|
53
|
+
(e.g. `socaity --project-dir simple_test_service deploy serverless-runpod --yes`).
|
|
54
54
|
|
|
55
55
|
## Configuration
|
|
56
56
|
|
|
57
57
|
| Env var | Default | Purpose |
|
|
58
58
|
|---------|---------|---------|
|
|
59
59
|
| `SOCAITY_API_KEY` | from login | API key (wins over stored credentials) |
|
|
60
|
-
| `SOCAITY_BACKEND_URL` | `https://webapi.socaity.ai` | Platform API |
|
|
60
|
+
| `SOCAITY_BACKEND_URL` | `https://webapi.socaity.ai` | Platform API (env wins; login file is fallback only, never written into the process) |
|
|
61
61
|
| `SOCAITY_FRONTEND_URL` | `https://socaity.ai` | Web UI for browser login |
|
|
62
62
|
|
|
63
63
|
## Using it as a library
|
|
@@ -32,15 +32,15 @@ You get `socaity-cli` as part of `pip install socaity` or `pip install apipod` a
|
|
|
32
32
|
| `socaity start [entrypoint]` | `apipod` | Run an APIPod service locally |
|
|
33
33
|
| `socaity deploy [target]` | `apipod` | Deploy to Socaity (wraps `apipod deploy`) |
|
|
34
34
|
|
|
35
|
-
Use
|
|
36
|
-
(e.g. `socaity -
|
|
35
|
+
Use `--project-dir DIR` with scan/build/start/deploy to point at a service project directory
|
|
36
|
+
(e.g. `socaity --project-dir simple_test_service deploy serverless-runpod --yes`).
|
|
37
37
|
|
|
38
38
|
## Configuration
|
|
39
39
|
|
|
40
40
|
| Env var | Default | Purpose |
|
|
41
41
|
|---------|---------|---------|
|
|
42
42
|
| `SOCAITY_API_KEY` | from login | API key (wins over stored credentials) |
|
|
43
|
-
| `SOCAITY_BACKEND_URL` | `https://webapi.socaity.ai` | Platform API |
|
|
43
|
+
| `SOCAITY_BACKEND_URL` | `https://webapi.socaity.ai` | Platform API (env wins; login file is fallback only, never written into the process) |
|
|
44
44
|
| `SOCAITY_FRONTEND_URL` | `https://socaity.ai` | Web UI for browser login |
|
|
45
45
|
|
|
46
46
|
## Using it as a library
|
|
@@ -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 = [
|
|
@@ -14,7 +14,7 @@ license = { text = "MIT" }
|
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
dependencies = [
|
|
16
16
|
"httpx",
|
|
17
|
-
"socaity-schemas>=0.1.
|
|
17
|
+
"socaity-schemas>=0.1.6",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
"""Composed platform backend client (catalog, jobs, projects, analytics, files, sdk, deployment)."""
|
|
1
|
+
"""Composed platform backend client (catalog, jobs, projects, analytics, files, sdk, deployment, workflows)."""
|
|
2
2
|
from socaity_cli.clients.analytics import AnalyticsClient
|
|
3
3
|
from socaity_cli.clients.catalog import CatalogClient
|
|
4
|
+
from socaity_cli.clients.conversations import ConversationsClient
|
|
4
5
|
from socaity_cli.clients.deployment import DeploymentClient
|
|
5
6
|
from socaity_cli.clients.files import FilesClient
|
|
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
|
from socaity_cli.http import BackendHttp
|
|
10
14
|
|
|
11
15
|
|
|
@@ -18,6 +22,10 @@ class SocaityBackendClient(
|
|
|
18
22
|
FilesClient,
|
|
19
23
|
SdkInstallClient,
|
|
20
24
|
DeploymentClient,
|
|
25
|
+
ConversationsClient,
|
|
26
|
+
InterruptsClient,
|
|
27
|
+
WorkflowsClient,
|
|
28
|
+
ProfileClient,
|
|
21
29
|
):
|
|
22
30
|
"""Facade over domain mixins. Prefer importing a domain client when you only need one surface."""
|
|
23
31
|
pass
|
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
6
8
|
|
|
7
9
|
from socaity_cli.apipod_commands import run_build, run_deploy, run_scan, run_start
|
|
8
10
|
from socaity_cli.auth import run_login, run_whoami
|
|
9
11
|
from socaity_cli.backend_client import SocaityBackendClient
|
|
10
12
|
from socaity_cli.credentials import clear_credentials, require_credentials
|
|
11
|
-
from socaity_cli.sdk_commands import run_install, run_update
|
|
13
|
+
from socaity_cli.sdk_commands import run_install, run_update, run_workflow_command
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def _run_list(entity: str, category: str | None, family: str | None, limit: int) -> None:
|
|
15
17
|
client = SocaityBackendClient()
|
|
16
18
|
if entity == "services":
|
|
17
|
-
services = client.
|
|
19
|
+
services = client.query_services(category=category, limit=limit)
|
|
18
20
|
if not services:
|
|
19
21
|
print("No services found.")
|
|
20
22
|
return
|
|
@@ -24,7 +26,7 @@ def _run_list(entity: str, category: str | None, family: str | None, limit: int)
|
|
|
24
26
|
desc = f" - {service.short_desc}" if service.short_desc else ""
|
|
25
27
|
print(f"{name}{official}{desc}")
|
|
26
28
|
else:
|
|
27
|
-
models = client.
|
|
29
|
+
models = client.query_models(family=family, limit=limit)
|
|
28
30
|
if not models:
|
|
29
31
|
print("No models found.")
|
|
30
32
|
return
|
|
@@ -75,6 +77,127 @@ def _run_jobs(
|
|
|
75
77
|
_print_job(job)
|
|
76
78
|
|
|
77
79
|
|
|
80
|
+
def _run_interrupts(
|
|
81
|
+
interrupt_id: str | None,
|
|
82
|
+
resolve: str | None,
|
|
83
|
+
message: str | None,
|
|
84
|
+
expand: str | None,
|
|
85
|
+
status: str | None,
|
|
86
|
+
limit: int,
|
|
87
|
+
no_continue: bool = False,
|
|
88
|
+
) -> None:
|
|
89
|
+
client = SocaityBackendClient()
|
|
90
|
+
expand_list = _split_csv(expand)
|
|
91
|
+
|
|
92
|
+
if interrupt_id and resolve:
|
|
93
|
+
result = client.resolve_interrupt(
|
|
94
|
+
interrupt_id, decision=resolve, message=message, continue_run=not no_continue,
|
|
95
|
+
)
|
|
96
|
+
if not result:
|
|
97
|
+
print(f"Interrupt '{interrupt_id}' not found.")
|
|
98
|
+
return
|
|
99
|
+
print(result.model_dump_json(indent=2, exclude_none=True))
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if interrupt_id:
|
|
103
|
+
interrupt = client.get_interrupt(interrupt_id, expand=expand_list)
|
|
104
|
+
if not interrupt:
|
|
105
|
+
print(f"Interrupt '{interrupt_id}' not found.")
|
|
106
|
+
return
|
|
107
|
+
print(interrupt.model_dump_json(indent=2, exclude_none=True))
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
filters = [f"status:eq:{status}"] if status else None
|
|
111
|
+
interrupts = client.query_interrupts(filters=filters, expand=expand_list, limit=limit)
|
|
112
|
+
if not interrupts:
|
|
113
|
+
print("No pending interrupts. Your agents are not waiting on you.")
|
|
114
|
+
return
|
|
115
|
+
for row in interrupts:
|
|
116
|
+
decisions = "|".join(row.allowed_decisions or [])
|
|
117
|
+
print(f"{row.id} {row.action_name} [{row.status}] ({decisions}) {row.description or ''}".rstrip())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_inputs(pairs: list[str] | None) -> dict:
|
|
121
|
+
"""Parse repeatable ``--input key=value`` flags. Values parse as JSON when possible."""
|
|
122
|
+
inputs: dict = {}
|
|
123
|
+
for pair in pairs or []:
|
|
124
|
+
key, separator, value = pair.partition("=")
|
|
125
|
+
if not separator or not key.strip():
|
|
126
|
+
print(f"Ignoring malformed --input '{pair}' (expected key=value).")
|
|
127
|
+
continue
|
|
128
|
+
try:
|
|
129
|
+
inputs[key.strip()] = json.loads(value)
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
inputs[key.strip()] = value
|
|
132
|
+
return inputs
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _run_workflows(args) -> None:
|
|
136
|
+
client = SocaityBackendClient()
|
|
137
|
+
command = args.workflows_command
|
|
138
|
+
|
|
139
|
+
if command == "list":
|
|
140
|
+
workflows = client.query_workflows(q=args.q, limit=args.limit)
|
|
141
|
+
if not workflows:
|
|
142
|
+
print("No workflows found.")
|
|
143
|
+
return
|
|
144
|
+
for workflow in workflows:
|
|
145
|
+
visibility = "public" if workflow.is_public else "private"
|
|
146
|
+
print(f"{workflow.id} {workflow.slug or '-'} [{visibility}] {workflow.title or ''}".rstrip())
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if command == "get":
|
|
150
|
+
workflow = client.get_workflow(args.workflow_id)
|
|
151
|
+
if not workflow:
|
|
152
|
+
print(f"Workflow '{args.workflow_id}' not found.")
|
|
153
|
+
return
|
|
154
|
+
print(workflow.model_dump_json(indent=2, exclude_none=True))
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
if command == "push":
|
|
158
|
+
path = Path(args.file)
|
|
159
|
+
if not path.is_file():
|
|
160
|
+
print(f"File '{args.file}' not found.")
|
|
161
|
+
return
|
|
162
|
+
document = json.loads(path.read_text(encoding="utf-8"))
|
|
163
|
+
result = client.upsert_workflow(document, slug=args.slug, message=args.message)
|
|
164
|
+
if not result:
|
|
165
|
+
print("Workflow upsert failed.")
|
|
166
|
+
return
|
|
167
|
+
state = "unchanged" if result.unchanged else ("forked" if result.forked else "saved")
|
|
168
|
+
print(f"{result.workflow.id} revision {result.revision.id} [{state}]")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
if command == "publish":
|
|
172
|
+
workflow = client.publish_workflow(args.workflow_id, revision_id=args.revision_id)
|
|
173
|
+
if not workflow:
|
|
174
|
+
print(f"Workflow '{args.workflow_id}' not found or not publishable.")
|
|
175
|
+
return
|
|
176
|
+
print(f"{workflow.id} published revision {workflow.published_revision_id}")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if command == "runs":
|
|
180
|
+
if args.workflow_id and args.workflow_id.startswith("wr_"):
|
|
181
|
+
run = client.get_workflow_run(args.workflow_id, expand=["traces"])
|
|
182
|
+
if not run:
|
|
183
|
+
print(f"Workflow run '{args.workflow_id}' not found.")
|
|
184
|
+
return
|
|
185
|
+
print(run.model_dump_json(indent=2, exclude_none=True))
|
|
186
|
+
return
|
|
187
|
+
runs = client.query_workflow_runs(workflow_id=args.workflow_id, limit=args.limit)
|
|
188
|
+
if not runs:
|
|
189
|
+
print("No workflow runs found.")
|
|
190
|
+
return
|
|
191
|
+
for run in runs:
|
|
192
|
+
cost = f" {run.total_cost_eur:.4f} EUR" if run.total_cost_eur is not None else ""
|
|
193
|
+
print(f"{run.id} [{run.status}] {run.workflow_id or '?'} {run.created_at or ''}{cost}")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if command == "run":
|
|
197
|
+
run_workflow_command(args.workflow_id, _parse_inputs(args.input), timeout_s=args.timeout)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
|
|
78
201
|
def _run_projects(project_id: str | None, expand: str | None, limit: int) -> None:
|
|
79
202
|
client = SocaityBackendClient()
|
|
80
203
|
expand_list = _split_csv(expand)
|
|
@@ -101,7 +224,12 @@ def _run_search(query: str, collections: str | None, limit: int) -> None:
|
|
|
101
224
|
targets = [part.strip() for part in (collections or "services,models").split(",") if part.strip()]
|
|
102
225
|
found = False
|
|
103
226
|
for collection in targets:
|
|
104
|
-
|
|
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)
|
|
105
233
|
if not hits:
|
|
106
234
|
continue
|
|
107
235
|
found = True
|
|
@@ -137,7 +265,7 @@ Examples:
|
|
|
137
265
|
socaity build main.py
|
|
138
266
|
socaity start --port 8123
|
|
139
267
|
socaity deploy serverless-runpod
|
|
140
|
-
socaity -
|
|
268
|
+
socaity --project-dir simple_test_service deploy serverless-runpod --yes
|
|
141
269
|
""",
|
|
142
270
|
)
|
|
143
271
|
parser.add_argument(
|
|
@@ -146,12 +274,19 @@ Examples:
|
|
|
146
274
|
help="Install an AI service (requires login)",
|
|
147
275
|
)
|
|
148
276
|
parser.add_argument(
|
|
149
|
-
"-C",
|
|
150
277
|
"--project-dir",
|
|
278
|
+
dest="project_dir",
|
|
151
279
|
default=None,
|
|
152
280
|
metavar="DIR",
|
|
153
281
|
help="Service project directory for apipod commands (scan/build/start/deploy).",
|
|
154
282
|
)
|
|
283
|
+
parser.add_argument(
|
|
284
|
+
"-C",
|
|
285
|
+
"--cwd",
|
|
286
|
+
dest="project_dir",
|
|
287
|
+
metavar="DIR",
|
|
288
|
+
help=argparse.SUPPRESS,
|
|
289
|
+
)
|
|
155
290
|
|
|
156
291
|
subparsers = parser.add_subparsers(dest="command")
|
|
157
292
|
|
|
@@ -201,6 +336,59 @@ Examples:
|
|
|
201
336
|
)
|
|
202
337
|
jobs_parser.add_argument("--limit", type=int, default=25)
|
|
203
338
|
|
|
339
|
+
interrupts_parser = subparsers.add_parser(
|
|
340
|
+
"interrupts", help="Your HIT approval inbox: list, inspect, or answer agent action requests (requires login)"
|
|
341
|
+
)
|
|
342
|
+
interrupts_parser.add_argument("interrupt_id", nargs="?", default=None, help="Interrupt id for detail or resolve")
|
|
343
|
+
interrupts_parser.add_argument(
|
|
344
|
+
"--resolve", default=None, choices=("approve", "edit", "reject", "respond"),
|
|
345
|
+
help="Answer the given interrupt with this decision",
|
|
346
|
+
)
|
|
347
|
+
interrupts_parser.add_argument("--message", default=None, help="Reject feedback or respond answer text")
|
|
348
|
+
interrupts_parser.add_argument("--expand", default=None, help="Comma-separated: chat,chat_item,job")
|
|
349
|
+
interrupts_parser.add_argument("--status", default=None, help="Filter by status (default: pending)")
|
|
350
|
+
interrupts_parser.add_argument(
|
|
351
|
+
"--no-continue", action="store_true",
|
|
352
|
+
help="Record the decision without enqueuing the agent continue job",
|
|
353
|
+
)
|
|
354
|
+
interrupts_parser.add_argument("--limit", type=int, default=25)
|
|
355
|
+
|
|
356
|
+
workflows_parser = subparsers.add_parser(
|
|
357
|
+
"workflows", help="Manage and run your workflows (requires login)"
|
|
358
|
+
)
|
|
359
|
+
workflows_sub = workflows_parser.add_subparsers(dest="workflows_command")
|
|
360
|
+
|
|
361
|
+
wf_list = workflows_sub.add_parser("list", help="List your workflows plus public ones")
|
|
362
|
+
wf_list.add_argument("--q", default=None, help="Fuzzy search over title, goal, slug, and node text")
|
|
363
|
+
wf_list.add_argument("--limit", type=int, default=25)
|
|
364
|
+
|
|
365
|
+
wf_get = workflows_sub.add_parser("get", help="Show one workflow including its document")
|
|
366
|
+
wf_get.add_argument("workflow_id", help="Workflow id (wf_...) or slug")
|
|
367
|
+
|
|
368
|
+
wf_push = workflows_sub.add_parser("push", help="Upsert a workflow from a JSON document file")
|
|
369
|
+
wf_push.add_argument("file", help="Path to the workflow document JSON")
|
|
370
|
+
wf_push.add_argument("--slug", default=None, help="Stable per-owner handle")
|
|
371
|
+
wf_push.add_argument("--message", default=None, help="Revision message (changelog)")
|
|
372
|
+
|
|
373
|
+
wf_publish = workflows_sub.add_parser("publish", help="Publish a workflow (pins a revision)")
|
|
374
|
+
wf_publish.add_argument("workflow_id", help="Workflow id (wf_...) or slug")
|
|
375
|
+
wf_publish.add_argument("--revision-id", default=None, help="rv_ id to pin; default latest valid")
|
|
376
|
+
|
|
377
|
+
wf_runs = workflows_sub.add_parser("runs", help="List workflow runs, or inspect one (wr_ id)")
|
|
378
|
+
wf_runs.add_argument(
|
|
379
|
+
"workflow_id", nargs="?", default=None,
|
|
380
|
+
help="Workflow id to filter by, or a run id (wr_...) for a detailed view",
|
|
381
|
+
)
|
|
382
|
+
wf_runs.add_argument("--limit", type=int, default=25)
|
|
383
|
+
|
|
384
|
+
wf_run = workflows_sub.add_parser("run", help="Run a workflow via the inference gateway (requires socaity)")
|
|
385
|
+
wf_run.add_argument("workflow_id", help="Workflow id (wf_...) or slug")
|
|
386
|
+
wf_run.add_argument(
|
|
387
|
+
"--input", action="append", default=None, metavar="KEY=VALUE",
|
|
388
|
+
help="Workflow input (repeatable). Values parse as JSON when possible.",
|
|
389
|
+
)
|
|
390
|
+
wf_run.add_argument("--timeout", type=float, default=None, help="Give up waiting after this many seconds")
|
|
391
|
+
|
|
204
392
|
projects_parser = subparsers.add_parser("projects", help="List your projects or inspect one (requires login)")
|
|
205
393
|
projects_parser.add_argument("project_id", nargs="?", default=None, help="Project id for a detailed view")
|
|
206
394
|
projects_parser.add_argument("--expand", default=None, help="Comma-separated: jobs (nested: jobs.data, ...)")
|
|
@@ -224,7 +412,7 @@ Examples:
|
|
|
224
412
|
"Examples:\n"
|
|
225
413
|
" socaity deploy\n"
|
|
226
414
|
" socaity deploy serverless-runpod\n"
|
|
227
|
-
" socaity -
|
|
415
|
+
" socaity --project-dir simple_test_service deploy serverless-runpod --yes\n"
|
|
228
416
|
" socaity deploy --skip-build\n"
|
|
229
417
|
" socaity deploy --resume DEPLOYMENT_ID"
|
|
230
418
|
),
|
|
@@ -318,6 +506,22 @@ Examples:
|
|
|
318
506
|
_run_jobs(args.job_id, args.expand, args.status, args.visibility, args.limit, q=args.q)
|
|
319
507
|
return
|
|
320
508
|
|
|
509
|
+
if args.command == "interrupts":
|
|
510
|
+
require_credentials()
|
|
511
|
+
_run_interrupts(
|
|
512
|
+
args.interrupt_id, args.resolve, args.message,
|
|
513
|
+
args.expand, args.status, args.limit, no_continue=args.no_continue,
|
|
514
|
+
)
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
if args.command == "workflows":
|
|
518
|
+
if not args.workflows_command:
|
|
519
|
+
workflows_parser.print_help()
|
|
520
|
+
return
|
|
521
|
+
require_credentials()
|
|
522
|
+
_run_workflows(args)
|
|
523
|
+
return
|
|
524
|
+
|
|
321
525
|
if args.command == "projects":
|
|
322
526
|
require_credentials()
|
|
323
527
|
_run_projects(args.project_id, args.expand, args.limit)
|
|
@@ -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
|
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""AI catalog reads (`/v1/catalog`)."""
|
|
2
|
+
import re
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from socaity_schemas.platform import AIModel, AIService, ServiceCategory
|
|
6
|
+
|
|
7
|
+
SERVICE_LIST_FIELDS = ["id", "name", "display_name", "short_desc", "categories", "is_official", "n_usages"]
|
|
8
|
+
SERVICE_DETAIL_EXPAND = ["deployments.contract", "endpoints", "models"]
|
|
9
|
+
MODEL_LIST_FIELDS = ["id", "name", "display_name", "family", "release_date", "hugging_face_url"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CatalogClient:
|
|
13
|
+
"""Mixin: catalog query/get. Requires ``BackendHttp`` methods on ``self``."""
|
|
14
|
+
|
|
15
|
+
SERVICE_DETAIL_EXPAND = SERVICE_DETAIL_EXPAND
|
|
16
|
+
|
|
17
|
+
def query_services(
|
|
18
|
+
self,
|
|
19
|
+
q: Optional[str] = None,
|
|
20
|
+
category: Optional[str] = None,
|
|
21
|
+
filters: Optional[List[str]] = None,
|
|
22
|
+
expand: Optional[List[str]] = None,
|
|
23
|
+
fields: Optional[List[str]] = None,
|
|
24
|
+
sort: Optional[str] = None,
|
|
25
|
+
limit: int = 100,
|
|
26
|
+
offset: int = 0,
|
|
27
|
+
known_ids: Optional[List[str]] = None,
|
|
28
|
+
known_fieldset: Optional[str] = None,
|
|
29
|
+
mine: bool = False,
|
|
30
|
+
) -> List[AIService]:
|
|
31
|
+
"""Query AI services in the socaity catalog.
|
|
32
|
+
|
|
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``.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
q: Free-text query. Omit to browse.
|
|
40
|
+
category: Category id (not display name). Appended as a filter.
|
|
41
|
+
filters: Repeatable ``field:operator:value`` strings.
|
|
42
|
+
expand: Relations to embed (``deployments``, ``models``, ``pricing``).
|
|
43
|
+
fields: Sparse fieldset, e.g. ``["id", "name"]``.
|
|
44
|
+
sort: ``field:asc`` or ``field:desc``.
|
|
45
|
+
limit: Page size, 1 to 100.
|
|
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.
|
|
49
|
+
mine: Only services owned by the caller.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Service rows, or an empty list when nothing matches.
|
|
53
|
+
"""
|
|
54
|
+
service_filters = list(filters or [])
|
|
55
|
+
if category:
|
|
56
|
+
service_filters.append(f"categories:contains:{category}")
|
|
57
|
+
rows = self._unwrap_list(self._get("v1/catalog/services", params={
|
|
58
|
+
"q": q,
|
|
59
|
+
"filter": service_filters,
|
|
60
|
+
"expand": expand or [],
|
|
61
|
+
"fields": fields if fields is not None else SERVICE_LIST_FIELDS,
|
|
62
|
+
"sort": sort,
|
|
63
|
+
"limit": limit,
|
|
64
|
+
"offset": offset,
|
|
65
|
+
"known_ids": known_ids or [],
|
|
66
|
+
"known_fieldset": known_fieldset,
|
|
67
|
+
"mine": mine,
|
|
68
|
+
}))
|
|
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
|
|
90
|
+
|
|
91
|
+
def get_service(
|
|
92
|
+
self,
|
|
93
|
+
identifier: str,
|
|
94
|
+
expand: Optional[List[str]] = None,
|
|
95
|
+
fields: Optional[List[str]] = None,
|
|
96
|
+
filters: Optional[List[str]] = None,
|
|
97
|
+
) -> Optional[AIService]:
|
|
98
|
+
"""Fetch one catalog service by id, name, or ``owner/service``.
|
|
99
|
+
|
|
100
|
+
Default expand embeds ``deployments.contract``, platform ``endpoints`` and
|
|
101
|
+
``models``. The call signature for ``run_service`` lives under
|
|
102
|
+
``deployments[].contract.endpoints[].parameters``.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
identifier: Service id or slug from ``query_services``.
|
|
106
|
+
expand: Override the default expand list.
|
|
107
|
+
fields: Sparse fieldset.
|
|
108
|
+
filters: Post-fetch filters.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The service record, or ``None`` when not found or not visible.
|
|
112
|
+
"""
|
|
113
|
+
row = self._get(f"v1/catalog/services/{identifier}", params={
|
|
114
|
+
"filter": filters or [],
|
|
115
|
+
"expand": expand if expand is not None else SERVICE_DETAIL_EXPAND,
|
|
116
|
+
"fields": fields or [],
|
|
117
|
+
})
|
|
118
|
+
return AIService(**row) if row else None
|
|
119
|
+
|
|
120
|
+
def query_models(
|
|
121
|
+
self,
|
|
122
|
+
q: Optional[str] = None,
|
|
123
|
+
family: Optional[str] = None,
|
|
124
|
+
task: Optional[str] = None,
|
|
125
|
+
filters: Optional[List[str]] = None,
|
|
126
|
+
expand: Optional[List[str]] = None,
|
|
127
|
+
fields: Optional[List[str]] = None,
|
|
128
|
+
sort: Optional[str] = None,
|
|
129
|
+
limit: int = 100,
|
|
130
|
+
offset: int = 0,
|
|
131
|
+
known_ids: Optional[List[str]] = None,
|
|
132
|
+
known_fieldset: Optional[str] = None,
|
|
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}")
|
|
157
|
+
rows = self._unwrap_list(self._get("v1/catalog/models", params={
|
|
158
|
+
"q": q,
|
|
159
|
+
"filter": model_filters,
|
|
160
|
+
"expand": expand or [],
|
|
161
|
+
"fields": fields if fields is not None else MODEL_LIST_FIELDS,
|
|
162
|
+
"sort": sort,
|
|
163
|
+
"limit": limit,
|
|
164
|
+
"offset": offset,
|
|
165
|
+
"known_ids": known_ids or [],
|
|
166
|
+
"known_fieldset": known_fieldset,
|
|
167
|
+
}))
|
|
168
|
+
return [AIModel(**row) for row in rows]
|
|
169
|
+
|
|
170
|
+
def get_model(
|
|
171
|
+
self,
|
|
172
|
+
id_or_name: str,
|
|
173
|
+
expand: Optional[List[str]] = None,
|
|
174
|
+
fields: Optional[List[str]] = None,
|
|
175
|
+
) -> Optional[AIModel]:
|
|
176
|
+
row = self._get(f"v1/catalog/models/{id_or_name}", params={
|
|
177
|
+
"expand": expand if expand is not None else ["scores"],
|
|
178
|
+
"fields": fields or [],
|
|
179
|
+
})
|
|
180
|
+
return AIModel(**row) if row else None
|
|
181
|
+
|
|
182
|
+
def query_categories(self) -> List[ServiceCategory]:
|
|
183
|
+
"""Query catalog task categories."""
|
|
184
|
+
rows = self._get("v1/catalog/categories") or []
|
|
185
|
+
if isinstance(rows, dict):
|
|
186
|
+
rows = rows.get("entities") or rows.get("data") or []
|
|
187
|
+
return [ServiceCategory(**row) for row in rows]
|
|
188
|
+
|
|
189
|
+
def list_pricing_rules(self, active_only: bool = True) -> List[dict]:
|
|
190
|
+
rows = self._get("v1/catalog/pricing-rules", params={"active_only": active_only}) or []
|
|
191
|
+
if isinstance(rows, dict):
|
|
192
|
+
rows = rows.get("entities") or rows.get("data") or []
|
|
193
|
+
return rows if isinstance(rows, list) else []
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _service_matches_query(service: AIService, text: str) -> bool:
|
|
197
|
+
tokens = _query_tokens(text)
|
|
198
|
+
if not tokens:
|
|
199
|
+
return True
|
|
200
|
+
hay = _fold(" ".join(filter(None, (
|
|
201
|
+
getattr(service, "name", None),
|
|
202
|
+
getattr(service, "display_name", None),
|
|
203
|
+
getattr(service, "short_desc", None),
|
|
204
|
+
getattr(service, "description", None),
|
|
205
|
+
))))
|
|
206
|
+
return all(token in hay for token in tokens)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
_QUERY_STOP = frozenset({
|
|
210
|
+
"a", "an", "the", "by", "for", "of", "and", "or", "to", "in", "on",
|
|
211
|
+
"image", "images", "video", "audio", "text", "generation", "generate",
|
|
212
|
+
"model", "models", "service", "services", "ai", "good", "best",
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _query_tokens(text: str) -> list[str]:
|
|
217
|
+
"""Distinctive tokens. Drop filler so 'Labs image generation' still hits flux."""
|
|
218
|
+
raw = _fold(text).split()
|
|
219
|
+
kept = [token for token in raw if token not in _QUERY_STOP and len(token) > 2]
|
|
220
|
+
return kept or raw
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _fold(value: str) -> str:
|
|
224
|
+
return re.sub(r"[^a-z0-9]+", " ", (value or "").lower()).strip()
|