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.
Files changed (44) hide show
  1. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/PKG-INFO +5 -5
  2. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/README.md +3 -3
  3. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/pyproject.toml +2 -2
  4. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/backend_client.py +9 -1
  5. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/cli.py +211 -7
  6. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/__init__.py +8 -0
  7. socaity_cli-0.1.9/socaity_cli/clients/catalog.py +224 -0
  8. socaity_cli-0.1.9/socaity_cli/clients/conversations.py +87 -0
  9. socaity_cli-0.1.9/socaity_cli/clients/files.py +156 -0
  10. socaity_cli-0.1.9/socaity_cli/clients/interrupts.py +61 -0
  11. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/jobs.py +36 -1
  12. socaity_cli-0.1.9/socaity_cli/clients/profile.py +29 -0
  13. socaity_cli-0.1.9/socaity_cli/clients/workflows.py +249 -0
  14. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/credentials.py +14 -8
  15. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/deploy_messages.py +2 -0
  16. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/errors.py +4 -0
  17. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/http.py +35 -13
  18. socaity_cli-0.1.9/socaity_cli/sdk_commands.py +58 -0
  19. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/PKG-INFO +5 -5
  20. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/SOURCES.txt +4 -0
  21. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/requires.txt +1 -1
  22. socaity_cli-0.1.7/socaity_cli/clients/catalog.py +0 -147
  23. socaity_cli-0.1.7/socaity_cli/clients/files.py +0 -82
  24. socaity_cli-0.1.7/socaity_cli/sdk_commands.py +0 -28
  25. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/setup.cfg +0 -0
  26. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/__init__.py +0 -0
  27. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/__main__.py +0 -0
  28. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/apipod_commands.py +0 -0
  29. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/auth.py +0 -0
  30. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/analytics.py +0 -0
  31. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/deployment.py +0 -0
  32. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/projects.py +0 -0
  33. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/clients/sdk_install.py +0 -0
  34. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/deployment.py +0 -0
  35. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/prompts.py +0 -0
  36. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/registry_push.py +0 -0
  37. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli/requires.py +0 -0
  38. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/dependency_links.txt +0 -0
  39. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/entry_points.txt +0 -0
  40. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/socaity_cli.egg-info/top_level.txt +0 -0
  41. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/test/test_cli.py +0 -0
  42. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/test/test_credentials.py +0 -0
  43. {socaity_cli-0.1.7 → socaity_cli-0.1.9}/test/test_private_slot_error.py +0 -0
  44. {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.7
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.4
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 `-C DIR` with scan/build/start/deploy to point at a service project directory
53
- (e.g. `socaity -C simple_test_service deploy serverless-runpod --yes`).
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 `-C DIR` with scan/build/start/deploy to point at a service project directory
36
- (e.g. `socaity -C simple_test_service deploy serverless-runpod --yes`).
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"
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.4",
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.list_services(category=category, limit=limit)
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.list_models(family=family, limit=limit)
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
- hits = client.search(query, collection=collection, limit=limit)
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 -C simple_test_service deploy serverless-runpod --yes
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 -C simple_test_service deploy serverless-runpod --yes\n"
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()