socaity-cli 0.1.7__tar.gz → 0.1.8__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.8}/PKG-INFO +5 -5
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/README.md +3 -3
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/pyproject.toml +2 -2
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/backend_client.py +7 -1
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/cli.py +203 -4
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/clients/catalog.py +121 -4
- socaity_cli-0.1.8/socaity_cli/clients/conversations.py +81 -0
- socaity_cli-0.1.8/socaity_cli/clients/files.py +156 -0
- socaity_cli-0.1.8/socaity_cli/clients/interrupts.py +61 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/clients/jobs.py +16 -1
- socaity_cli-0.1.8/socaity_cli/clients/workflows.py +249 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/credentials.py +14 -8
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/deploy_messages.py +2 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/http.py +18 -3
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/sdk_commands.py +27 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli.egg-info/PKG-INFO +5 -5
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli.egg-info/SOURCES.txt +3 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli.egg-info/requires.txt +1 -1
- socaity_cli-0.1.7/socaity_cli/clients/files.py +0 -82
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/setup.cfg +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/__init__.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/__main__.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/apipod_commands.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/auth.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/clients/__init__.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/clients/analytics.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/clients/deployment.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/clients/projects.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/clients/sdk_install.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/deployment.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/errors.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/prompts.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/registry_push.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli/requires.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli.egg-info/dependency_links.txt +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli.egg-info/entry_points.txt +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/socaity_cli.egg-info/top_level.txt +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/test/test_cli.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/test/test_credentials.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/test/test_private_slot_error.py +0 -0
- {socaity_cli-0.1.7 → socaity_cli-0.1.8}/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.8
|
|
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.8"
|
|
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,14 @@
|
|
|
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
|
|
7
9
|
from socaity_cli.clients.projects import ProjectsClient
|
|
8
10
|
from socaity_cli.clients.sdk_install import SdkInstallClient
|
|
11
|
+
from socaity_cli.clients.workflows import WorkflowsClient
|
|
9
12
|
from socaity_cli.http import BackendHttp
|
|
10
13
|
|
|
11
14
|
|
|
@@ -18,6 +21,9 @@ class SocaityBackendClient(
|
|
|
18
21
|
FilesClient,
|
|
19
22
|
SdkInstallClient,
|
|
20
23
|
DeploymentClient,
|
|
24
|
+
ConversationsClient,
|
|
25
|
+
InterruptsClient,
|
|
26
|
+
WorkflowsClient,
|
|
21
27
|
):
|
|
22
28
|
"""Facade over domain mixins. Prefer importing a domain client when you only need one surface."""
|
|
23
29
|
pass
|
|
@@ -3,12 +3,14 @@
|
|
|
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:
|
|
@@ -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.search_workflows(query=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)
|
|
@@ -137,7 +260,7 @@ Examples:
|
|
|
137
260
|
socaity build main.py
|
|
138
261
|
socaity start --port 8123
|
|
139
262
|
socaity deploy serverless-runpod
|
|
140
|
-
socaity -
|
|
263
|
+
socaity --project-dir simple_test_service deploy serverless-runpod --yes
|
|
141
264
|
""",
|
|
142
265
|
)
|
|
143
266
|
parser.add_argument(
|
|
@@ -146,12 +269,19 @@ Examples:
|
|
|
146
269
|
help="Install an AI service (requires login)",
|
|
147
270
|
)
|
|
148
271
|
parser.add_argument(
|
|
149
|
-
"-C",
|
|
150
272
|
"--project-dir",
|
|
273
|
+
dest="project_dir",
|
|
151
274
|
default=None,
|
|
152
275
|
metavar="DIR",
|
|
153
276
|
help="Service project directory for apipod commands (scan/build/start/deploy).",
|
|
154
277
|
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"-C",
|
|
280
|
+
"--cwd",
|
|
281
|
+
dest="project_dir",
|
|
282
|
+
metavar="DIR",
|
|
283
|
+
help=argparse.SUPPRESS,
|
|
284
|
+
)
|
|
155
285
|
|
|
156
286
|
subparsers = parser.add_subparsers(dest="command")
|
|
157
287
|
|
|
@@ -201,6 +331,59 @@ Examples:
|
|
|
201
331
|
)
|
|
202
332
|
jobs_parser.add_argument("--limit", type=int, default=25)
|
|
203
333
|
|
|
334
|
+
interrupts_parser = subparsers.add_parser(
|
|
335
|
+
"interrupts", help="Your HIT approval inbox: list, inspect, or answer agent action requests (requires login)"
|
|
336
|
+
)
|
|
337
|
+
interrupts_parser.add_argument("interrupt_id", nargs="?", default=None, help="Interrupt id for detail or resolve")
|
|
338
|
+
interrupts_parser.add_argument(
|
|
339
|
+
"--resolve", default=None, choices=("approve", "edit", "reject", "respond"),
|
|
340
|
+
help="Answer the given interrupt with this decision",
|
|
341
|
+
)
|
|
342
|
+
interrupts_parser.add_argument("--message", default=None, help="Reject feedback or respond answer text")
|
|
343
|
+
interrupts_parser.add_argument("--expand", default=None, help="Comma-separated: chat,chat_item,job")
|
|
344
|
+
interrupts_parser.add_argument("--status", default=None, help="Filter by status (default: pending)")
|
|
345
|
+
interrupts_parser.add_argument(
|
|
346
|
+
"--no-continue", action="store_true",
|
|
347
|
+
help="Record the decision without enqueuing the agent continue job",
|
|
348
|
+
)
|
|
349
|
+
interrupts_parser.add_argument("--limit", type=int, default=25)
|
|
350
|
+
|
|
351
|
+
workflows_parser = subparsers.add_parser(
|
|
352
|
+
"workflows", help="Manage and run your workflows (requires login)"
|
|
353
|
+
)
|
|
354
|
+
workflows_sub = workflows_parser.add_subparsers(dest="workflows_command")
|
|
355
|
+
|
|
356
|
+
wf_list = workflows_sub.add_parser("list", help="List your workflows plus public ones")
|
|
357
|
+
wf_list.add_argument("--q", default=None, help="Fuzzy search over title, goal, slug, and node text")
|
|
358
|
+
wf_list.add_argument("--limit", type=int, default=25)
|
|
359
|
+
|
|
360
|
+
wf_get = workflows_sub.add_parser("get", help="Show one workflow including its document")
|
|
361
|
+
wf_get.add_argument("workflow_id", help="Workflow id (wf_...) or slug")
|
|
362
|
+
|
|
363
|
+
wf_push = workflows_sub.add_parser("push", help="Upsert a workflow from a JSON document file")
|
|
364
|
+
wf_push.add_argument("file", help="Path to the workflow document JSON")
|
|
365
|
+
wf_push.add_argument("--slug", default=None, help="Stable per-owner handle")
|
|
366
|
+
wf_push.add_argument("--message", default=None, help="Revision message (changelog)")
|
|
367
|
+
|
|
368
|
+
wf_publish = workflows_sub.add_parser("publish", help="Publish a workflow (pins a revision)")
|
|
369
|
+
wf_publish.add_argument("workflow_id", help="Workflow id (wf_...) or slug")
|
|
370
|
+
wf_publish.add_argument("--revision-id", default=None, help="rv_ id to pin; default latest valid")
|
|
371
|
+
|
|
372
|
+
wf_runs = workflows_sub.add_parser("runs", help="List workflow runs, or inspect one (wr_ id)")
|
|
373
|
+
wf_runs.add_argument(
|
|
374
|
+
"workflow_id", nargs="?", default=None,
|
|
375
|
+
help="Workflow id to filter by, or a run id (wr_...) for a detailed view",
|
|
376
|
+
)
|
|
377
|
+
wf_runs.add_argument("--limit", type=int, default=25)
|
|
378
|
+
|
|
379
|
+
wf_run = workflows_sub.add_parser("run", help="Run a workflow via the inference gateway (requires socaity)")
|
|
380
|
+
wf_run.add_argument("workflow_id", help="Workflow id (wf_...) or slug")
|
|
381
|
+
wf_run.add_argument(
|
|
382
|
+
"--input", action="append", default=None, metavar="KEY=VALUE",
|
|
383
|
+
help="Workflow input (repeatable). Values parse as JSON when possible.",
|
|
384
|
+
)
|
|
385
|
+
wf_run.add_argument("--timeout", type=float, default=None, help="Give up waiting after this many seconds")
|
|
386
|
+
|
|
204
387
|
projects_parser = subparsers.add_parser("projects", help="List your projects or inspect one (requires login)")
|
|
205
388
|
projects_parser.add_argument("project_id", nargs="?", default=None, help="Project id for a detailed view")
|
|
206
389
|
projects_parser.add_argument("--expand", default=None, help="Comma-separated: jobs (nested: jobs.data, ...)")
|
|
@@ -224,7 +407,7 @@ Examples:
|
|
|
224
407
|
"Examples:\n"
|
|
225
408
|
" socaity deploy\n"
|
|
226
409
|
" socaity deploy serverless-runpod\n"
|
|
227
|
-
" socaity -
|
|
410
|
+
" socaity --project-dir simple_test_service deploy serverless-runpod --yes\n"
|
|
228
411
|
" socaity deploy --skip-build\n"
|
|
229
412
|
" socaity deploy --resume DEPLOYMENT_ID"
|
|
230
413
|
),
|
|
@@ -318,6 +501,22 @@ Examples:
|
|
|
318
501
|
_run_jobs(args.job_id, args.expand, args.status, args.visibility, args.limit, q=args.q)
|
|
319
502
|
return
|
|
320
503
|
|
|
504
|
+
if args.command == "interrupts":
|
|
505
|
+
require_credentials()
|
|
506
|
+
_run_interrupts(
|
|
507
|
+
args.interrupt_id, args.resolve, args.message,
|
|
508
|
+
args.expand, args.status, args.limit, no_continue=args.no_continue,
|
|
509
|
+
)
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
if args.command == "workflows":
|
|
513
|
+
if not args.workflows_command:
|
|
514
|
+
workflows_parser.print_help()
|
|
515
|
+
return
|
|
516
|
+
require_credentials()
|
|
517
|
+
_run_workflows(args)
|
|
518
|
+
return
|
|
519
|
+
|
|
321
520
|
if args.command == "projects":
|
|
322
521
|
require_credentials()
|
|
323
522
|
_run_projects(args.project_id, args.expand, args.limit)
|
|
@@ -1,15 +1,84 @@
|
|
|
1
1
|
"""AI catalog reads (`/v1/catalog`)."""
|
|
2
|
+
import re
|
|
2
3
|
from typing import Dict, List, Optional, Union
|
|
3
4
|
|
|
4
5
|
from socaity_schemas.platform import AIModel, AIService, Job, ServiceCategory
|
|
5
6
|
|
|
6
7
|
SERVICE_LIST_FIELDS = ["id", "name", "display_name", "short_desc", "categories", "is_official", "n_usages"]
|
|
8
|
+
SERVICE_DETAIL_EXPAND = ["deployments.contract", "endpoints", "models"]
|
|
7
9
|
MODEL_LIST_FIELDS = ["id", "name", "display_name", "family", "release_date", "hugging_face_url"]
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class CatalogClient:
|
|
11
13
|
"""Mixin: catalog list/get/search. Requires ``BackendHttp`` methods on ``self``."""
|
|
12
14
|
|
|
15
|
+
SERVICE_DETAIL_EXPAND = SERVICE_DETAIL_EXPAND
|
|
16
|
+
|
|
17
|
+
def search_services(
|
|
18
|
+
self,
|
|
19
|
+
query: 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 = 20,
|
|
26
|
+
offset: int = 0,
|
|
27
|
+
mine: bool = False,
|
|
28
|
+
) -> List[AIService]:
|
|
29
|
+
"""Find AI services in the socaity catalog.
|
|
30
|
+
|
|
31
|
+
Start here for any generation task. Search by capability ("text to image",
|
|
32
|
+
"voice cloning"). Results are slim catalog rows; call ``get_service`` with
|
|
33
|
+
a hit ``id`` for endpoints and the call signature under
|
|
34
|
+
``deployments.contract``.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
query: Free-text search. Omit to browse.
|
|
38
|
+
category: Category id (not display name).
|
|
39
|
+
filters: Repeatable ``field:operator:value`` strings.
|
|
40
|
+
expand: Relations to embed (``deployments``, ``models``, ``pricing``).
|
|
41
|
+
fields: Sparse fieldset, e.g. ``["id", "name"]``.
|
|
42
|
+
sort: ``field:asc`` or ``field:desc``.
|
|
43
|
+
limit: Page size, 1 to 100.
|
|
44
|
+
offset: Skip this many rows; increase by ``limit`` for the next page.
|
|
45
|
+
mine: Only services owned by the caller.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Service rows, or an empty list when nothing matches.
|
|
49
|
+
"""
|
|
50
|
+
service_filters = list(filters or [])
|
|
51
|
+
if category:
|
|
52
|
+
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
|
+
|
|
13
82
|
def query_services(
|
|
14
83
|
self,
|
|
15
84
|
q: Optional[str] = None,
|
|
@@ -21,6 +90,7 @@ class CatalogClient:
|
|
|
21
90
|
offset: int = 0,
|
|
22
91
|
known_ids: Optional[List[str]] = None,
|
|
23
92
|
known_fieldset: Optional[str] = None,
|
|
93
|
+
mine: bool = False,
|
|
24
94
|
) -> List[AIService]:
|
|
25
95
|
rows = self._unwrap_list(self._get("v1/catalog/services", params={
|
|
26
96
|
"q": q,
|
|
@@ -32,6 +102,7 @@ class CatalogClient:
|
|
|
32
102
|
"offset": offset,
|
|
33
103
|
"known_ids": known_ids or [],
|
|
34
104
|
"known_fieldset": known_fieldset,
|
|
105
|
+
"mine": mine,
|
|
35
106
|
}))
|
|
36
107
|
return [AIService(**row) for row in rows]
|
|
37
108
|
|
|
@@ -54,14 +125,29 @@ class CatalogClient:
|
|
|
54
125
|
|
|
55
126
|
def get_service(
|
|
56
127
|
self,
|
|
57
|
-
|
|
128
|
+
identifier: str,
|
|
58
129
|
expand: Optional[List[str]] = None,
|
|
59
130
|
fields: Optional[List[str]] = None,
|
|
60
131
|
filters: Optional[List[str]] = None,
|
|
61
132
|
) -> Optional[AIService]:
|
|
62
|
-
|
|
133
|
+
"""Fetch one catalog service by id, name, or ``owner/service``.
|
|
134
|
+
|
|
135
|
+
Default expand embeds ``deployments.contract``, platform ``endpoints`` and
|
|
136
|
+
``models``. The call signature for ``run_service`` lives under
|
|
137
|
+
``deployments[].contract.endpoints[].parameters``.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
identifier: Service id or slug from ``search_services``.
|
|
141
|
+
expand: Override the default expand list.
|
|
142
|
+
fields: Sparse fieldset.
|
|
143
|
+
filters: Post-fetch filters.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The service record, or ``None`` when not found or not visible.
|
|
147
|
+
"""
|
|
148
|
+
row = self._get(f"v1/catalog/services/{identifier}", params={
|
|
63
149
|
"filter": filters or [],
|
|
64
|
-
"expand": expand if expand is not None else
|
|
150
|
+
"expand": expand if expand is not None else SERVICE_DETAIL_EXPAND,
|
|
65
151
|
"fields": fields or [],
|
|
66
152
|
})
|
|
67
153
|
return AIService(**row) if row else None
|
|
@@ -144,4 +230,35 @@ class CatalogClient:
|
|
|
144
230
|
return self.query_models(q=query, filters=filters, limit=limit)
|
|
145
231
|
if collection == "jobs":
|
|
146
232
|
return self.query_jobs(q=query, filters=filters, visibility=visibility, limit=limit)
|
|
147
|
-
return self.
|
|
233
|
+
return self.search_services(query=query, filters=filters, limit=limit)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _service_matches_query(service: AIService, text: str) -> bool:
|
|
237
|
+
tokens = _query_tokens(text)
|
|
238
|
+
if not tokens:
|
|
239
|
+
return True
|
|
240
|
+
hay = _fold(" ".join(filter(None, (
|
|
241
|
+
getattr(service, "name", None),
|
|
242
|
+
getattr(service, "display_name", None),
|
|
243
|
+
getattr(service, "short_desc", None),
|
|
244
|
+
getattr(service, "description", None),
|
|
245
|
+
))))
|
|
246
|
+
return all(token in hay for token in tokens)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
_QUERY_STOP = frozenset({
|
|
250
|
+
"a", "an", "the", "by", "for", "of", "and", "or", "to", "in", "on",
|
|
251
|
+
"image", "images", "video", "audio", "text", "generation", "generate",
|
|
252
|
+
"model", "models", "service", "services", "ai", "good", "best",
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _query_tokens(text: str) -> list[str]:
|
|
257
|
+
"""Distinctive tokens. Drop filler so 'Labs image generation' still hits flux."""
|
|
258
|
+
raw = _fold(text).split()
|
|
259
|
+
kept = [token for token in raw if token not in _QUERY_STOP and len(token) > 2]
|
|
260
|
+
return kept or raw
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _fold(value: str) -> str:
|
|
264
|
+
return re.sub(r"[^a-z0-9]+", " ", (value or "").lower()).strip()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Conversations facade (`/v1/conversations`)."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from socaity_schemas.platform import Chat, ChatItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConversationsClient:
|
|
9
|
+
"""Mixin: conversation metadata and items. Requires ``BackendHttp`` methods on ``self``."""
|
|
10
|
+
|
|
11
|
+
def list_conversations(
|
|
12
|
+
self,
|
|
13
|
+
q: Optional[str] = None,
|
|
14
|
+
filters: Optional[List[str]] = None,
|
|
15
|
+
expand: Optional[List[str]] = None,
|
|
16
|
+
fields: Optional[List[str]] = None,
|
|
17
|
+
sort: Optional[str] = None,
|
|
18
|
+
limit: int = 100,
|
|
19
|
+
offset: int = 0,
|
|
20
|
+
) -> List[Chat]:
|
|
21
|
+
"""Conversations owned by the caller, newest activity first (fuzzy ``q`` via Typesense)."""
|
|
22
|
+
rows = self._unwrap_list(self._get("v1/conversations", params={
|
|
23
|
+
"q": q,
|
|
24
|
+
"filter": filters or [],
|
|
25
|
+
"expand": expand or [],
|
|
26
|
+
"fields": fields or [],
|
|
27
|
+
"sort": sort,
|
|
28
|
+
"limit": limit,
|
|
29
|
+
"offset": offset,
|
|
30
|
+
}))
|
|
31
|
+
return [Chat(**row) for row in rows]
|
|
32
|
+
|
|
33
|
+
def get_conversation(
|
|
34
|
+
self,
|
|
35
|
+
conversation_id: str,
|
|
36
|
+
expand: Optional[List[str]] = None,
|
|
37
|
+
fields: Optional[List[str]] = None,
|
|
38
|
+
) -> Optional[Chat]:
|
|
39
|
+
"""Fetch one conversation by id; relations omitted unless expanded."""
|
|
40
|
+
row = self._get(f"v1/conversations/{conversation_id}", params={
|
|
41
|
+
"expand": expand or [],
|
|
42
|
+
"fields": fields or [],
|
|
43
|
+
})
|
|
44
|
+
return Chat(**row) if row else None
|
|
45
|
+
|
|
46
|
+
def list_conversation_items(
|
|
47
|
+
self,
|
|
48
|
+
conversation_id: str,
|
|
49
|
+
branch: str = "active",
|
|
50
|
+
after: Optional[str] = None,
|
|
51
|
+
order: str = "asc",
|
|
52
|
+
filters: Optional[List[str]] = None,
|
|
53
|
+
limit: int = 100,
|
|
54
|
+
) -> List[ChatItem]:
|
|
55
|
+
"""Typed items of one conversation. ``branch=active`` walks the active chain; ``all`` is the full tree."""
|
|
56
|
+
rows = self._get(f"v1/conversations/{conversation_id}/items", params={
|
|
57
|
+
"branch": branch,
|
|
58
|
+
"after": after,
|
|
59
|
+
"order": order,
|
|
60
|
+
"filter": filters or [],
|
|
61
|
+
"limit": limit,
|
|
62
|
+
}) or []
|
|
63
|
+
return [ChatItem(**row) for row in rows]
|
|
64
|
+
|
|
65
|
+
def update_conversation(
|
|
66
|
+
self,
|
|
67
|
+
conversation_id: str,
|
|
68
|
+
title: Optional[str] = None,
|
|
69
|
+
status: Optional[str] = None,
|
|
70
|
+
) -> Optional[Chat]:
|
|
71
|
+
"""Patch title and/or status on a conversation the caller owns."""
|
|
72
|
+
row = self._request(
|
|
73
|
+
"PATCH",
|
|
74
|
+
f"v1/conversations/{conversation_id}",
|
|
75
|
+
params={"title": title, "status": status},
|
|
76
|
+
)
|
|
77
|
+
return Chat(**row) if row else None
|
|
78
|
+
|
|
79
|
+
def delete_conversation(self, conversation_id: str) -> bool:
|
|
80
|
+
"""Soft-delete a conversation and clear its linked job data."""
|
|
81
|
+
return bool(self._request("DELETE", f"v1/conversations/{conversation_id}"))
|