ndi-cli 0.16.0__tar.gz → 0.25.0__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.
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/PKG-INFO +13 -11
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/README.md +11 -9
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/pyproject.toml +2 -2
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/pyproject.toml.orig +2 -2
- ndi_cli-0.25.0/src/ndi_cli/__init__.py +9 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_cli.py +63 -23
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_local.py +46 -94
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_render.py +55 -223
- ndi_cli-0.25.0/src/ndi_cli/_systemone.py +72 -0
- ndi_cli-0.25.0/src/ndi_cli/_tools.py +264 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_workspace.py +44 -9
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/commands.py +0 -2
- ndi_cli-0.16.0/src/ndi_cli/__init__.py +0 -9
- ndi_cli-0.16.0/src/ndi_cli/_tools.py +0 -404
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/LICENSE +0 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_config.py +0 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_docops.py +0 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_entrypoint.py +0 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_jobs.py +0 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_login.py +0 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_output.py +0 -0
- {ndi_cli-0.16.0 → ndi_cli-0.25.0}/src/ndi_cli/_sources.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndi-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: NDI platform CLI: document operations, jobs, workspaces, and the agent workspace tools
|
|
5
5
|
Keywords: ndi,cli,document-intelligence,coding-agents
|
|
6
6
|
Author: Nace AI
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Dist: ndi-sdk>=0.
|
|
19
|
+
Requires-Dist: ndi-sdk>=0.45,<1
|
|
20
20
|
Requires-Dist: truststore>=0.10.4,<1
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Project-URL: Homepage, https://ndi-api.nace.ai
|
|
@@ -25,8 +25,9 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
# ndi-cli
|
|
26
26
|
|
|
27
27
|
`ndi` — the NDI platform CLI. Document operations, jobs, and workspace
|
|
28
|
-
lifecycle wrap the same `/v1` methods as `ndi-sdk`.
|
|
29
|
-
|
|
28
|
+
lifecycle wrap the same `/v1` methods as `ndi-sdk`. `ask-file` also runs
|
|
29
|
+
inside an agent sandbox, next to the file commands only an agent run has; every
|
|
30
|
+
other verb is refused there.
|
|
30
31
|
|
|
31
32
|
## Setup
|
|
32
33
|
|
|
@@ -56,12 +57,13 @@ config location. Workspace-scoped verbs also take `--workspace ID`.
|
|
|
56
57
|
| `ndi workspace global-context [--set TEXT\|--file PATH\|--clear]` | Show, replace, or remove (`--clear`) the background every query without `--context` runs with; `--no-context` on a query runs with none |
|
|
57
58
|
| `ndi files upload\|list\|get\|delete` | Workspace files (a directory walks nested files; `--ingest` then queues ingestion) |
|
|
58
59
|
| `ndi ingest` | Queue ingestion |
|
|
59
|
-
| `ndi automatic-search QUERY` | Service answers with fact search or an agent over files, tables and the catalog; prints the route, the output job id, and the answer (plus the catalog table when there is one) |
|
|
60
|
-
| `ndi deep-search QUERY` / `ndi fact-search QUERY` | Agentic / single-shot search; then `ndi job` on each printed `grounding_job_ids` entry |
|
|
61
|
-
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--cursor` pages an earlier run) |
|
|
60
|
+
| `ndi automatic-search QUERY` | Service answers with fact search or an agent over files, tables and the catalog (`--mode fact\|agent` skips the routing step, `--path` scopes it to files); prints the route, the output job id, and the answer (plus the catalog table when there is one) |
|
|
61
|
+
| `ndi deep-search QUERY` / `ndi fact-search QUERY` | Agentic / single-shot search (`--path` on `deep-search` scopes it to files); then `ndi job` on each printed `grounding_job_ids` entry |
|
|
62
|
+
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--path` scopes it to files, `--cursor` pages an earlier run) |
|
|
62
63
|
| `ndi je-testing QUERY` | Journal-entry testing over a ledger package (`--path` to scope it, `--reasoning-effort` for the thinking budget) |
|
|
63
64
|
| `ndi ledger-understanding start\|get\|sources\|select\|selection` | Measure the ledger package, confirm sources, or read the latest report |
|
|
64
|
-
| `ndi
|
|
65
|
+
| `ndi ask-file PATH QUESTION` | Cited answer over one document, image or recording |
|
|
66
|
+
| `ndi systemone --state TEXT --questions JSON` | Answer `noul`, `choice` and `score` questions about a state in one call, no workspace (`--state-json` for a JSON state; any value may be `@FILE` or `@-`) |
|
|
65
67
|
|
|
66
68
|
## Sources
|
|
67
69
|
|
|
@@ -100,8 +102,8 @@ The default (`auto`) prints the full JSON response. When that is over
|
|
|
100
102
|
`$NDI_OUTPUT_LIMIT` characters (default 40,000), the compact text rendering
|
|
101
103
|
prints instead and stderr carries `note: output shortened …` with the full
|
|
102
104
|
size. `--json` always prints it whole, and `--save` / `--out-dir` always write
|
|
103
|
-
it whole. Commands whose output is the content itself (`
|
|
104
|
-
|
|
105
|
+
it whole. Commands whose output is the content itself (`ws context`, parse
|
|
106
|
+
markdown) print it as before. `--save PATH` / `--out-dir DIR` write files. `--async`
|
|
105
107
|
submits and prints the job id.
|
|
106
108
|
|
|
107
109
|
When a Parse result has external complete content, `-o auto` and `-o md` print
|
|
@@ -122,4 +124,4 @@ The server checks a bounded byte prefix, rejects conclusive filename/content
|
|
|
122
124
|
conflicts, and treats the multipart `Content-Type` as advisory.
|
|
123
125
|
|
|
124
126
|
See the [CLI guide](https://docs.ndi.nace.ai/guides/cli) for the full command
|
|
125
|
-
reference. `SKILL.md` is the agent-facing guide to
|
|
127
|
+
reference. `SKILL.md` is the agent-facing guide to reading a workspace with `ndi`.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# ndi-cli
|
|
2
2
|
|
|
3
3
|
`ndi` — the NDI platform CLI. Document operations, jobs, and workspace
|
|
4
|
-
lifecycle wrap the same `/v1` methods as `ndi-sdk`.
|
|
5
|
-
|
|
4
|
+
lifecycle wrap the same `/v1` methods as `ndi-sdk`. `ask-file` also runs
|
|
5
|
+
inside an agent sandbox, next to the file commands only an agent run has; every
|
|
6
|
+
other verb is refused there.
|
|
6
7
|
|
|
7
8
|
## Setup
|
|
8
9
|
|
|
@@ -32,12 +33,13 @@ config location. Workspace-scoped verbs also take `--workspace ID`.
|
|
|
32
33
|
| `ndi workspace global-context [--set TEXT\|--file PATH\|--clear]` | Show, replace, or remove (`--clear`) the background every query without `--context` runs with; `--no-context` on a query runs with none |
|
|
33
34
|
| `ndi files upload\|list\|get\|delete` | Workspace files (a directory walks nested files; `--ingest` then queues ingestion) |
|
|
34
35
|
| `ndi ingest` | Queue ingestion |
|
|
35
|
-
| `ndi automatic-search QUERY` | Service answers with fact search or an agent over files, tables and the catalog; prints the route, the output job id, and the answer (plus the catalog table when there is one) |
|
|
36
|
-
| `ndi deep-search QUERY` / `ndi fact-search QUERY` | Agentic / single-shot search; then `ndi job` on each printed `grounding_job_ids` entry |
|
|
37
|
-
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--cursor` pages an earlier run) |
|
|
36
|
+
| `ndi automatic-search QUERY` | Service answers with fact search or an agent over files, tables and the catalog (`--mode fact\|agent` skips the routing step, `--path` scopes it to files); prints the route, the output job id, and the answer (plus the catalog table when there is one) |
|
|
37
|
+
| `ndi deep-search QUERY` / `ndi fact-search QUERY` | Agentic / single-shot search (`--path` on `deep-search` scopes it to files); then `ndi job` on each printed `grounding_job_ids` entry |
|
|
38
|
+
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--path` scopes it to files, `--cursor` pages an earlier run) |
|
|
38
39
|
| `ndi je-testing QUERY` | Journal-entry testing over a ledger package (`--path` to scope it, `--reasoning-effort` for the thinking budget) |
|
|
39
40
|
| `ndi ledger-understanding start\|get\|sources\|select\|selection` | Measure the ledger package, confirm sources, or read the latest report |
|
|
40
|
-
| `ndi
|
|
41
|
+
| `ndi ask-file PATH QUESTION` | Cited answer over one document, image or recording |
|
|
42
|
+
| `ndi systemone --state TEXT --questions JSON` | Answer `noul`, `choice` and `score` questions about a state in one call, no workspace (`--state-json` for a JSON state; any value may be `@FILE` or `@-`) |
|
|
41
43
|
|
|
42
44
|
## Sources
|
|
43
45
|
|
|
@@ -76,8 +78,8 @@ The default (`auto`) prints the full JSON response. When that is over
|
|
|
76
78
|
`$NDI_OUTPUT_LIMIT` characters (default 40,000), the compact text rendering
|
|
77
79
|
prints instead and stderr carries `note: output shortened …` with the full
|
|
78
80
|
size. `--json` always prints it whole, and `--save` / `--out-dir` always write
|
|
79
|
-
it whole. Commands whose output is the content itself (`
|
|
80
|
-
|
|
81
|
+
it whole. Commands whose output is the content itself (`ws context`, parse
|
|
82
|
+
markdown) print it as before. `--save PATH` / `--out-dir DIR` write files. `--async`
|
|
81
83
|
submits and prints the job id.
|
|
82
84
|
|
|
83
85
|
When a Parse result has external complete content, `-o auto` and `-o md` print
|
|
@@ -98,4 +100,4 @@ The server checks a bounded byte prefix, rejects conclusive filename/content
|
|
|
98
100
|
conflicts, and treats the multipart `Content-Type` as advisory.
|
|
99
101
|
|
|
100
102
|
See the [CLI guide](https://docs.ndi.nace.ai/guides/cli) for the full command
|
|
101
|
-
reference. `SKILL.md` is the agent-facing guide to
|
|
103
|
+
reference. `SKILL.md` is the agent-facing guide to reading a workspace with `ndi`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ndi-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.0"
|
|
4
4
|
description = "NDI platform CLI: document operations, jobs, workspaces, and the agent workspace tools"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Typing :: Typed",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
|
-
"ndi-sdk>=0.
|
|
27
|
+
"ndi-sdk>=0.45,<1",
|
|
28
28
|
"truststore>=0.10.4,<1",
|
|
29
29
|
]
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ndi-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.0"
|
|
4
4
|
description = "NDI platform CLI: document operations, jobs, workspaces, and the agent workspace tools"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Nace AI", email = "engineering@nace.ai" }
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
"Typing :: Typed",
|
|
28
28
|
]
|
|
29
29
|
dependencies = [
|
|
30
|
-
"ndi-sdk>=0.
|
|
30
|
+
"ndi-sdk>=0.45,<1",
|
|
31
31
|
"truststore>=0.10.4,<1",
|
|
32
32
|
]
|
|
33
33
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""``ndi`` — the NDI platform CLI.
|
|
2
|
+
|
|
3
|
+
Document operations, jobs, and workspace lifecycle wrap ``ndi-sdk`` ``/v1``
|
|
4
|
+
methods. Inside an agent sandbox the workspace tools (``folder-metadata``,
|
|
5
|
+
``file-metadata``, ``read-file``, ``ask-file``, ``run-sql``, ``hybrid-search``)
|
|
6
|
+
are the whole surface; outside one only ``ask-file`` is.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.25.0"
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
"""``ndi`` entry point: workspace tools plus the platform verbs.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Inside an agent sandbox the workspace tools are the only verbs, and the file
|
|
4
|
+
commands with no public route exist only there. Every other verb is refused
|
|
5
|
+
when ``$NDI_CLI_SOCKET`` is set, before any client is built.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import argparse
|
|
11
|
+
import copy
|
|
10
12
|
import os
|
|
11
13
|
import sys
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
15
|
|
|
13
16
|
from ndi_sdk.client import NdiClient
|
|
14
17
|
from ndi_sdk.errors import NdiError, NdiStatusError
|
|
15
18
|
from ndi_sdk.resources.tools import SyncTools
|
|
16
19
|
|
|
17
|
-
from ndi_cli import _docops, _jobs, _login, _tools, _workspace
|
|
20
|
+
from ndi_cli import _docops, _jobs, _login, _systemone, _tools, _workspace
|
|
18
21
|
from ndi_cli._config import ConfigError, resolve_settings
|
|
19
|
-
from ndi_cli._local import
|
|
22
|
+
from ndi_cli._local import SOCKET_ENV, TOKEN_ENV, LocalTransport
|
|
20
23
|
from ndi_cli._output import full_json, output_limit, print_full_or_summary
|
|
21
|
-
from ndi_cli._tools import
|
|
24
|
+
from ndi_cli._tools import parse_pages
|
|
22
25
|
|
|
23
26
|
# qa-file runs a durable workflow that legitimately takes minutes; the metadata,
|
|
24
27
|
# read, and run-sql tools answer in seconds but share the generous bound.
|
|
@@ -32,10 +35,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
32
35
|
# Presence, not truthiness: an empty $NDI_CLI_SOCKET still means "inside an
|
|
33
36
|
# agent run", and must not silently unlock the full platform CLI.
|
|
34
37
|
in_agent_run = socket_path is not None
|
|
35
|
-
|
|
36
|
-
# a human CLI user gets all kg-* commands ("all").
|
|
37
|
-
graph: GraphCommands = ("answer" if os.environ.get(KG_ENV) else "none") if in_agent_run else "all"
|
|
38
|
-
parser = _build_parser(graph=graph)
|
|
38
|
+
parser = _build_parser(in_agent_run=in_agent_run)
|
|
39
39
|
args = parser.parse_args(argv)
|
|
40
40
|
if args.command is None:
|
|
41
41
|
parser.print_help()
|
|
@@ -78,8 +78,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
78
78
|
if in_agent_run:
|
|
79
79
|
workspace_id = os.environ.get("NDI_WORKSPACE_ID", "")
|
|
80
80
|
transport = LocalTransport(socket_path, args.timeout, os.environ.get(TOKEN_ENV, ""))
|
|
81
|
-
|
|
82
|
-
return _print_tool(args, response)
|
|
81
|
+
return _run_tool(args, SyncTools(transport), workspace_id, _print_tool)
|
|
83
82
|
settings = resolve_settings(
|
|
84
83
|
workspace_override=workspace_flag,
|
|
85
84
|
require_workspace=getattr(args, "needs_workspace", True),
|
|
@@ -87,25 +86,65 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
87
86
|
timeout = getattr(args, "timeout", DEFAULT_TIMEOUT_SECONDS)
|
|
88
87
|
with NdiClient(api_key=settings.api_key, base_url=settings.base_url, timeout=timeout) as client:
|
|
89
88
|
if family == "tool":
|
|
90
|
-
return
|
|
89
|
+
return _run_tool(args, client.tools, settings.workspace_id, _print_tool_full)
|
|
91
90
|
return args.run(client, settings.workspace_id, args)
|
|
92
91
|
except ConfigError as exc:
|
|
93
92
|
print(f"error: {exc}", file=sys.stderr)
|
|
94
93
|
return 2
|
|
95
|
-
except NdiStatusError as exc:
|
|
96
|
-
code = f" [{exc.code}]" if exc.code else ""
|
|
97
|
-
print(f"error: {exc.status_code}{code} {exc.message}", file=sys.stderr)
|
|
98
|
-
for line in _validation_error_lines(exc.body.detail if exc.body else None):
|
|
99
|
-
print(f" - {line}", file=sys.stderr)
|
|
100
|
-
return 1
|
|
101
94
|
except NdiError as exc:
|
|
102
|
-
|
|
103
|
-
return 1
|
|
95
|
+
return _report_error(exc)
|
|
104
96
|
except ValueError as exc:
|
|
105
97
|
print(f"error: {exc}", file=sys.stderr)
|
|
106
98
|
return 2
|
|
107
99
|
|
|
108
100
|
|
|
101
|
+
def _run_tool(args: argparse.Namespace, tools: SyncTools, workspace_id: str, printer) -> int:
|
|
102
|
+
"""Run a tool once per positional target (``file-metadata a b``), each printed as its own call would print it.
|
|
103
|
+
|
|
104
|
+
The targets are fetched concurrently and printed in the order given. With several
|
|
105
|
+
targets a failing one reports on stderr and the rest still run, so one bad path costs
|
|
106
|
+
the caller that path's error line, not every other path's answer.
|
|
107
|
+
"""
|
|
108
|
+
targets = getattr(args, "targets", [None])
|
|
109
|
+
if len(targets) == 1:
|
|
110
|
+
args.target = targets[0]
|
|
111
|
+
return printer(args, args.run(tools, workspace_id, args))
|
|
112
|
+
|
|
113
|
+
def fetch(target: str) -> tuple[argparse.Namespace, object]:
|
|
114
|
+
per_target = copy.copy(args)
|
|
115
|
+
per_target.target = target
|
|
116
|
+
try:
|
|
117
|
+
return per_target, per_target.run(tools, workspace_id, per_target)
|
|
118
|
+
except NdiError as exc:
|
|
119
|
+
return per_target, exc
|
|
120
|
+
|
|
121
|
+
with ThreadPoolExecutor(max_workers=len(targets)) as pool:
|
|
122
|
+
fetched = list(pool.map(fetch, targets))
|
|
123
|
+
status = 0
|
|
124
|
+
for index, (per_target, response) in enumerate(fetched):
|
|
125
|
+
if index:
|
|
126
|
+
print()
|
|
127
|
+
if getattr(args, "label_targets", False):
|
|
128
|
+
print(f"==> {per_target.target} <==")
|
|
129
|
+
if isinstance(response, NdiError):
|
|
130
|
+
status = max(status, _report_error(response, target=per_target.target))
|
|
131
|
+
else:
|
|
132
|
+
status = max(status, printer(per_target, response))
|
|
133
|
+
return status
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _report_error(exc: NdiError, *, target: str | None = None) -> int:
|
|
137
|
+
prefix = f"{target}: " if target is not None else ""
|
|
138
|
+
if not isinstance(exc, NdiStatusError):
|
|
139
|
+
print(f"error: {prefix}{exc}", file=sys.stderr)
|
|
140
|
+
return 1
|
|
141
|
+
code = f" [{exc.code}]" if exc.code else ""
|
|
142
|
+
print(f"error: {prefix}{exc.status_code}{code} {exc.message}", file=sys.stderr)
|
|
143
|
+
for line in _validation_error_lines(exc.body.detail if exc.body else None):
|
|
144
|
+
print(f" - {line}", file=sys.stderr)
|
|
145
|
+
return 1
|
|
146
|
+
|
|
147
|
+
|
|
109
148
|
def _print_tool(args: argparse.Namespace, response) -> int:
|
|
110
149
|
"""The text rendering. An agent run always gets it: the renderings are what its agents are evaluated against."""
|
|
111
150
|
if args.json:
|
|
@@ -148,12 +187,12 @@ def _validation_error_lines(detail: object) -> list[str]:
|
|
|
148
187
|
return lines
|
|
149
188
|
|
|
150
189
|
|
|
151
|
-
def _build_parser(*,
|
|
190
|
+
def _build_parser(*, in_agent_run: bool = False) -> argparse.ArgumentParser:
|
|
152
191
|
parser = argparse.ArgumentParser(
|
|
153
192
|
prog="ndi",
|
|
154
193
|
description=(
|
|
155
194
|
"NDI platform CLI. Login, document operations, jobs, and workspace lifecycle talk to /v1; "
|
|
156
|
-
"the
|
|
195
|
+
"the workspace tools also run inside an agent sandbox. Credentials come from "
|
|
157
196
|
"$NDI_API_KEY / $NDI_BASE_URL or ~/.ndi/config.toml."
|
|
158
197
|
),
|
|
159
198
|
)
|
|
@@ -171,10 +210,11 @@ def _build_parser(*, graph: GraphCommands = "all") -> argparse.ArgumentParser:
|
|
|
171
210
|
)
|
|
172
211
|
sub = parser.add_subparsers(dest="command")
|
|
173
212
|
_login.add_parsers(sub)
|
|
174
|
-
_tools.add_parsers(sub, common,
|
|
213
|
+
_tools.add_parsers(sub, common, in_agent_run=in_agent_run)
|
|
175
214
|
_docops.add_parsers(sub, common)
|
|
176
215
|
_jobs.add_parsers(sub, common)
|
|
177
216
|
_workspace.add_parsers(sub, common)
|
|
217
|
+
_systemone.add_parsers(sub, common)
|
|
178
218
|
return parser
|
|
179
219
|
|
|
180
220
|
|
|
@@ -14,29 +14,60 @@ import socket
|
|
|
14
14
|
from typing import Annotated, Literal, TypeVar
|
|
15
15
|
|
|
16
16
|
from ndi_sdk.errors import ErrorBody, NdiConnectionError, NdiTimeoutError, error_class_for
|
|
17
|
-
from ndi_sdk.models.tools import
|
|
18
|
-
FileMetadataRequest,
|
|
19
|
-
FolderMetadataRequest,
|
|
20
|
-
KgSearchRequest,
|
|
21
|
-
KgWalkRequest,
|
|
22
|
-
QaFileRequest,
|
|
23
|
-
ReadFileRequest,
|
|
24
|
-
RunSqlRequest,
|
|
25
|
-
SearchRequest,
|
|
26
|
-
)
|
|
17
|
+
from ndi_sdk.models.tools import QaFileRequest
|
|
27
18
|
from pydantic import BaseModel, Field, TypeAdapter
|
|
28
19
|
|
|
29
20
|
from ndi_cli.commands import WorkspaceCommand
|
|
30
21
|
|
|
31
22
|
SOCKET_ENV = "NDI_CLI_SOCKET"
|
|
32
23
|
TOKEN_ENV = "NDI_CLI_TOKEN"
|
|
33
|
-
#: Set (to any non-empty value) by the worker for an agent run that has the graph
|
|
34
|
-
#: surface; inside an agent run the ``kg-*`` commands are registered only when it is.
|
|
35
|
-
KG_ENV = "NDI_CLI_KG"
|
|
36
24
|
MAX_MESSAGE_BYTES = 16 * 1024 * 1024
|
|
37
25
|
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
38
26
|
|
|
39
27
|
|
|
28
|
+
# The socket bodies of the agent-only commands. Their /v1 routes are unpublished,
|
|
29
|
+
# so the SDK has no method or request model for them.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FolderMetadataRequest(BaseModel):
|
|
33
|
+
"""Non-recursive by default; the server treats a narrow as recursive unless ``recursive`` is sent."""
|
|
34
|
+
|
|
35
|
+
directory: str = "/"
|
|
36
|
+
recursive: bool | None = None
|
|
37
|
+
category: str | None = None
|
|
38
|
+
ingestion_status: str | None = None
|
|
39
|
+
kind: str | None = None
|
|
40
|
+
name_contains: str | None = None
|
|
41
|
+
page_size: int = 100
|
|
42
|
+
start_after: str | None = None
|
|
43
|
+
include_tables: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FileMetadataRequest(BaseModel):
|
|
47
|
+
path: str
|
|
48
|
+
component: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ReadFileRequest(BaseModel):
|
|
52
|
+
"""``pages`` are absolute and one-based; spreadsheets have none, so scope those with ``component``."""
|
|
53
|
+
|
|
54
|
+
path: str
|
|
55
|
+
pages: list[int] | None = None
|
|
56
|
+
component: str | None = None
|
|
57
|
+
max_chars: int | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RunSqlRequest(BaseModel):
|
|
61
|
+
sql: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SearchRequest(BaseModel):
|
|
65
|
+
query: str
|
|
66
|
+
k: int = 10
|
|
67
|
+
path_prefix: str | None = None
|
|
68
|
+
categories: list[str] | None = None
|
|
69
|
+
|
|
70
|
+
|
|
40
71
|
class FolderCommand(BaseModel):
|
|
41
72
|
command: Literal[WorkspaceCommand.FOLDER_METADATA] = WorkspaceCommand.FOLDER_METADATA
|
|
42
73
|
body: FolderMetadataRequest
|
|
@@ -67,95 +98,16 @@ class SearchCommand(BaseModel):
|
|
|
67
98
|
body: SearchRequest
|
|
68
99
|
|
|
69
100
|
|
|
70
|
-
class KgInfoRequest(BaseModel):
|
|
71
|
-
"""``kg-info``'s scope as a socket body.
|
|
72
|
-
|
|
73
|
-
Defined here rather than imported from ``ndi_sdk.models.tools`` like its
|
|
74
|
-
siblings: on the wire ``kg-info`` is a GET carrying query parameters, so it has
|
|
75
|
-
no request model to reuse. Only the scope crosses — the drawable sample is for
|
|
76
|
-
a client that renders a graph, and ``_kg_info_command`` refuses it.
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
path_prefix: str | None = None
|
|
80
|
-
hub_node_type: str | None = None
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class KgInfoCommand(BaseModel):
|
|
84
|
-
command: Literal[WorkspaceCommand.KG_INFO] = WorkspaceCommand.KG_INFO
|
|
85
|
-
body: KgInfoRequest
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class KgSearchCommand(BaseModel):
|
|
89
|
-
command: Literal[WorkspaceCommand.KG_SEARCH] = WorkspaceCommand.KG_SEARCH
|
|
90
|
-
body: KgSearchRequest
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class KgWalkCommand(BaseModel):
|
|
94
|
-
command: Literal[WorkspaceCommand.KG_WALK] = WorkspaceCommand.KG_WALK
|
|
95
|
-
body: KgWalkRequest
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class KgAnswerRequest(BaseModel):
|
|
99
|
-
"""``kg-answer``'s question as a socket body.
|
|
100
|
-
|
|
101
|
-
The agent passes its verbatim question; the server runs the graph pass and
|
|
102
|
-
returns the rendered outcome (answered text + citations, or evidence + gap).
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
query: str
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class KgAnswerResponse(BaseModel):
|
|
109
|
-
"""The rendered outcome of one graph pass, ready to print or inject as a tool result."""
|
|
110
|
-
|
|
111
|
-
text: str
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class KgAnswerCommand(BaseModel):
|
|
115
|
-
command: Literal[WorkspaceCommand.KG_ANSWER] = WorkspaceCommand.KG_ANSWER
|
|
116
|
-
body: KgAnswerRequest
|
|
117
|
-
|
|
118
|
-
|
|
119
101
|
Command = Annotated[
|
|
120
|
-
FolderCommand
|
|
121
|
-
| FileCommand
|
|
122
|
-
| ReadCommand
|
|
123
|
-
| AskCommand
|
|
124
|
-
| SqlCommand
|
|
125
|
-
| SearchCommand
|
|
126
|
-
| KgInfoCommand
|
|
127
|
-
| KgSearchCommand
|
|
128
|
-
| KgWalkCommand
|
|
129
|
-
| KgAnswerCommand,
|
|
102
|
+
FolderCommand | FileCommand | ReadCommand | AskCommand | SqlCommand | SearchCommand,
|
|
130
103
|
Field(discriminator="command"),
|
|
131
104
|
]
|
|
132
105
|
COMMAND = TypeAdapter(Command)
|
|
133
106
|
|
|
134
107
|
|
|
135
|
-
def _kg_info_command(params: object) -> Command:
|
|
136
|
-
"""Build the ``kg-info`` command from the SDK method's query parameters.
|
|
137
|
-
|
|
138
|
-
Refuses ``include_graph`` instead of dropping it: the sample is the whole point
|
|
139
|
-
of that flag, and answering without one would read as a graph with no structure.
|
|
140
|
-
"""
|
|
141
|
-
if not isinstance(params, dict):
|
|
142
|
-
raise ValueError("The internal CLI accepts workspace commands only.")
|
|
143
|
-
if params.get("include_graph"):
|
|
144
|
-
raise ValueError("The internal CLI does not serve kg-info's drawable graph sample.")
|
|
145
|
-
prefix = params.get("path_prefix")
|
|
146
|
-
if prefix is not None and not isinstance(prefix, str):
|
|
147
|
-
raise ValueError("kg-info's path_prefix must be a string.")
|
|
148
|
-
hub_node_type = params.get("hub_node_type")
|
|
149
|
-
if hub_node_type is not None and not isinstance(hub_node_type, str):
|
|
150
|
-
raise ValueError("kg-info's hub_node_type must be a string.")
|
|
151
|
-
return KgInfoCommand(body=KgInfoRequest(path_prefix=prefix, hub_node_type=hub_node_type))
|
|
152
|
-
|
|
153
|
-
|
|
154
108
|
def _command(method: str, path: str, kwargs: dict[str, object]) -> Command:
|
|
155
|
-
"""One socket command per
|
|
109
|
+
"""One socket command per tool call, named by the endpoint the call addresses."""
|
|
156
110
|
name = path.rsplit("/", 1)[-1]
|
|
157
|
-
if method == "GET" and name == WorkspaceCommand.KG_INFO:
|
|
158
|
-
return _kg_info_command(kwargs.get("params"))
|
|
159
111
|
body = kwargs.get("body")
|
|
160
112
|
if method != "POST" or not isinstance(body, BaseModel):
|
|
161
113
|
raise ValueError("The internal CLI accepts workspace commands only.")
|