ndi-cli 0.21.0__tar.gz → 0.26.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.21.0 → ndi_cli-0.26.0}/PKG-INFO +5 -4
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/README.md +3 -2
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/pyproject.toml +2 -2
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/pyproject.toml.orig +2 -2
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/__init__.py +1 -1
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_cli.py +29 -14
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_local.py +1 -25
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_output.py +4 -1
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_render.py +35 -0
- ndi_cli-0.26.0/src/ndi_cli/_systemone.py +81 -0
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_tools.py +21 -38
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_workspace.py +20 -7
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/commands.py +0 -2
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/LICENSE +0 -0
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_config.py +0 -0
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_docops.py +0 -0
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_entrypoint.py +0 -0
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_jobs.py +0 -0
- {ndi_cli-0.21.0 → ndi_cli-0.26.0}/src/ndi_cli/_login.py +0 -0
- {ndi_cli-0.21.0 → ndi_cli-0.26.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.26.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
|
|
@@ -57,12 +57,13 @@ config location. Workspace-scoped verbs also take `--workspace ID`.
|
|
|
57
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 |
|
|
58
58
|
| `ndi files upload\|list\|get\|delete` | Workspace files (a directory walks nested files; `--ingest` then queues ingestion) |
|
|
59
59
|
| `ndi ingest` | Queue ingestion |
|
|
60
|
-
| `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 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
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 (`--cursor` pages an earlier run) |
|
|
62
|
+
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--path` scopes it to files, `--cursor` pages an earlier run) |
|
|
63
63
|
| `ndi je-testing QUERY` | Journal-entry testing over a ledger package (`--path` to scope it, `--reasoning-effort` for the thinking budget) |
|
|
64
64
|
| `ndi ledger-understanding start\|get\|sources\|select\|selection` | Measure the ledger package, confirm sources, or read the latest report |
|
|
65
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 `@-`) |
|
|
66
67
|
|
|
67
68
|
## Sources
|
|
68
69
|
|
|
@@ -33,12 +33,13 @@ config location. Workspace-scoped verbs also take `--workspace ID`.
|
|
|
33
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 |
|
|
34
34
|
| `ndi files upload\|list\|get\|delete` | Workspace files (a directory walks nested files; `--ingest` then queues ingestion) |
|
|
35
35
|
| `ndi ingest` | Queue ingestion |
|
|
36
|
-
| `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 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
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 (`--cursor` pages an earlier run) |
|
|
38
|
+
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--path` scopes it to files, `--cursor` pages an earlier run) |
|
|
39
39
|
| `ndi je-testing QUERY` | Journal-entry testing over a ledger package (`--path` to scope it, `--reasoning-effort` for the thinking budget) |
|
|
40
40
|
| `ndi ledger-understanding start\|get\|sources\|select\|selection` | Measure the ledger package, confirm sources, or read the latest report |
|
|
41
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 `@-`) |
|
|
42
43
|
|
|
43
44
|
## Sources
|
|
44
45
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ndi-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.26.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.26.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
|
|
|
@@ -8,16 +8,18 @@ when ``$NDI_CLI_SOCKET`` is set, before any client is built.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
|
+
import copy
|
|
11
12
|
import os
|
|
12
13
|
import sys
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
13
15
|
|
|
14
16
|
from ndi_sdk.client import NdiClient
|
|
15
17
|
from ndi_sdk.errors import NdiError, NdiStatusError
|
|
16
18
|
from ndi_sdk.resources.tools import SyncTools
|
|
17
19
|
|
|
18
|
-
from ndi_cli import _docops, _jobs, _login, _tools, _workspace
|
|
20
|
+
from ndi_cli import _docops, _jobs, _login, _systemone, _tools, _workspace
|
|
19
21
|
from ndi_cli._config import ConfigError, resolve_settings
|
|
20
|
-
from ndi_cli._local import
|
|
22
|
+
from ndi_cli._local import SOCKET_ENV, TOKEN_ENV, LocalTransport
|
|
21
23
|
from ndi_cli._output import full_json, output_limit, print_full_or_summary
|
|
22
24
|
from ndi_cli._tools import parse_pages
|
|
23
25
|
|
|
@@ -33,8 +35,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
33
35
|
# Presence, not truthiness: an empty $NDI_CLI_SOCKET still means "inside an
|
|
34
36
|
# agent run", and must not silently unlock the full platform CLI.
|
|
35
37
|
in_agent_run = socket_path is not None
|
|
36
|
-
|
|
37
|
-
parser = _build_parser(in_agent_run=in_agent_run, graph=in_agent_run and bool(os.environ.get(KG_ENV)))
|
|
38
|
+
parser = _build_parser(in_agent_run=in_agent_run)
|
|
38
39
|
args = parser.parse_args(argv)
|
|
39
40
|
if args.command is None:
|
|
40
41
|
parser.print_help()
|
|
@@ -100,22 +101,35 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
100
101
|
def _run_tool(args: argparse.Namespace, tools: SyncTools, workspace_id: str, printer) -> int:
|
|
101
102
|
"""Run a tool once per positional target (``file-metadata a b``), each printed as its own call would print it.
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
105
107
|
"""
|
|
106
108
|
targets = getattr(args, "targets", [None])
|
|
107
109
|
if len(targets) == 1:
|
|
108
110
|
args.target = targets[0]
|
|
109
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))
|
|
110
123
|
status = 0
|
|
111
|
-
for index,
|
|
112
|
-
args.target = target
|
|
124
|
+
for index, (per_target, response) in enumerate(fetched):
|
|
113
125
|
if index:
|
|
114
126
|
print()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
status = max(status, _report_error(
|
|
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))
|
|
119
133
|
return status
|
|
120
134
|
|
|
121
135
|
|
|
@@ -173,7 +187,7 @@ def _validation_error_lines(detail: object) -> list[str]:
|
|
|
173
187
|
return lines
|
|
174
188
|
|
|
175
189
|
|
|
176
|
-
def _build_parser(*, in_agent_run: bool = False
|
|
190
|
+
def _build_parser(*, in_agent_run: bool = False) -> argparse.ArgumentParser:
|
|
177
191
|
parser = argparse.ArgumentParser(
|
|
178
192
|
prog="ndi",
|
|
179
193
|
description=(
|
|
@@ -196,10 +210,11 @@ def _build_parser(*, in_agent_run: bool = False, graph: bool = False) -> argpars
|
|
|
196
210
|
)
|
|
197
211
|
sub = parser.add_subparsers(dest="command")
|
|
198
212
|
_login.add_parsers(sub)
|
|
199
|
-
_tools.add_parsers(sub, common, in_agent_run=in_agent_run
|
|
213
|
+
_tools.add_parsers(sub, common, in_agent_run=in_agent_run)
|
|
200
214
|
_docops.add_parsers(sub, common)
|
|
201
215
|
_jobs.add_parsers(sub, common)
|
|
202
216
|
_workspace.add_parsers(sub, common)
|
|
217
|
+
_systemone.add_parsers(sub, default_timeout=DEFAULT_TIMEOUT_SECONDS)
|
|
203
218
|
return parser
|
|
204
219
|
|
|
205
220
|
|
|
@@ -21,9 +21,6 @@ from ndi_cli.commands import WorkspaceCommand
|
|
|
21
21
|
|
|
22
22
|
SOCKET_ENV = "NDI_CLI_SOCKET"
|
|
23
23
|
TOKEN_ENV = "NDI_CLI_TOKEN"
|
|
24
|
-
#: Set (to any non-empty value) by the worker for an agent run that has the graph
|
|
25
|
-
#: surface; inside an agent run ``kg-answer`` is registered only when it is.
|
|
26
|
-
KG_ENV = "NDI_CLI_KG"
|
|
27
24
|
MAX_MESSAGE_BYTES = 16 * 1024 * 1024
|
|
28
25
|
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
29
26
|
|
|
@@ -101,29 +98,8 @@ class SearchCommand(BaseModel):
|
|
|
101
98
|
body: SearchRequest
|
|
102
99
|
|
|
103
100
|
|
|
104
|
-
class KgAnswerRequest(BaseModel):
|
|
105
|
-
"""``kg-answer``'s question as a socket body.
|
|
106
|
-
|
|
107
|
-
The agent passes its verbatim question; the server runs the graph pass and
|
|
108
|
-
returns the rendered outcome (answered text + citations, or evidence + gap).
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
query: str
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class KgAnswerResponse(BaseModel):
|
|
115
|
-
"""The rendered outcome of one graph pass, ready to print or inject as a tool result."""
|
|
116
|
-
|
|
117
|
-
text: str
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class KgAnswerCommand(BaseModel):
|
|
121
|
-
command: Literal[WorkspaceCommand.KG_ANSWER] = WorkspaceCommand.KG_ANSWER
|
|
122
|
-
body: KgAnswerRequest
|
|
123
|
-
|
|
124
|
-
|
|
125
101
|
Command = Annotated[
|
|
126
|
-
FolderCommand | FileCommand | ReadCommand | AskCommand | SqlCommand | SearchCommand
|
|
102
|
+
FolderCommand | FileCommand | ReadCommand | AskCommand | SqlCommand | SearchCommand,
|
|
127
103
|
Field(discriminator="command"),
|
|
128
104
|
]
|
|
129
105
|
COMMAND = TypeAdapter(Command)
|
|
@@ -49,7 +49,8 @@ def shortened(full: str, args, *, get_full: str = "rerun with --json") -> bool:
|
|
|
49
49
|
"""Whether ``full`` is too long for stdout; if so, tell stderr how to get it.
|
|
50
50
|
|
|
51
51
|
A file target is never shortened — the limit protects a reader's context,
|
|
52
|
-
and ``--save`` / ``--out-dir`` asked for the whole thing. ``
|
|
52
|
+
and ``--save`` / ``--out-dir`` asked for the whole thing. A limit of ``0``
|
|
53
|
+
asked for the text rendering, so nothing goes to stderr. ``get_full`` names
|
|
53
54
|
the command that prints it whole.
|
|
54
55
|
"""
|
|
55
56
|
if getattr(args, "save", None) or getattr(args, "out_dir", None):
|
|
@@ -57,6 +58,8 @@ def shortened(full: str, args, *, get_full: str = "rerun with --json") -> bool:
|
|
|
57
58
|
limit = output_limit()
|
|
58
59
|
if len(full) <= limit:
|
|
59
60
|
return False
|
|
61
|
+
if limit == 0:
|
|
62
|
+
return True
|
|
60
63
|
print(
|
|
61
64
|
f"note: output shortened — the full response is {len(full):,} chars, over the {limit:,} limit "
|
|
62
65
|
f"(${OUTPUT_LIMIT_ENV}); {get_full} for all of it",
|
|
@@ -42,6 +42,7 @@ from ndi_sdk.models.ledger_understanding import (
|
|
|
42
42
|
SelectionSummary,
|
|
43
43
|
SourceCatalog,
|
|
44
44
|
)
|
|
45
|
+
from ndi_sdk.models.systemone import ChoiceAnswer, NoulAnswer, ScoreAnswer, SystemOneResponse
|
|
45
46
|
from ndi_sdk.models.workspaces import Workspace, WorkspaceStats
|
|
46
47
|
from ndi_sdk.models.tools import (
|
|
47
48
|
FileMetadataToolResponse,
|
|
@@ -591,6 +592,40 @@ def schema_validation(result: ExtractSchemaValidationResponse) -> str:
|
|
|
591
592
|
return "\n".join(lines)
|
|
592
593
|
|
|
593
594
|
|
|
595
|
+
def _probability(value: float) -> str:
|
|
596
|
+
return f"{value:.3f}"
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _systemone_answer(answer) -> str:
|
|
600
|
+
if isinstance(answer, NoulAnswer):
|
|
601
|
+
return f"noul p(true)={_probability(answer.noul)}"
|
|
602
|
+
if isinstance(answer, ChoiceAnswer):
|
|
603
|
+
ranked = sorted(answer.probabilities.items(), key=lambda item: -item[1])
|
|
604
|
+
spread = " ".join(f"{option}={_probability(p)}" for option, p in ranked)
|
|
605
|
+
return f"choice {answer.choice} (confidence {_probability(answer.confidence)}) {spread}"
|
|
606
|
+
if isinstance(answer, ScoreAnswer):
|
|
607
|
+
# The score is an expected level index; name the level it rounds to.
|
|
608
|
+
named = ""
|
|
609
|
+
if answer.legend:
|
|
610
|
+
named = f" ≈ {answer.legend[min(max(round(answer.score), 0), len(answer.legend) - 1)]!r}"
|
|
611
|
+
spread = " ".join(
|
|
612
|
+
f"{name}={_probability(answer.probabilities[str(index)])}"
|
|
613
|
+
for index, name in enumerate(answer.legend)
|
|
614
|
+
if str(index) in answer.probabilities
|
|
615
|
+
)
|
|
616
|
+
return f"score {answer.score:.2f}{named} (confidence {_probability(answer.confidence)}) {spread}"
|
|
617
|
+
return f"{answer.type} (not known to this CLI) {json.dumps(answer.data, sort_keys=True)}"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def systemone(response: SystemOneResponse) -> str:
|
|
621
|
+
usage = response.usage
|
|
622
|
+
tokens = f" tokens in={usage.input_tokens} out={usage.output_tokens}" if usage.input_tokens is not None else ""
|
|
623
|
+
lines = [f"model {response.model} {response.evaluation_time_ms:.0f} ms{tokens}"]
|
|
624
|
+
width = max((len(key) for key in response.answers), default=0)
|
|
625
|
+
lines.extend(f"{key:<{width}} {_systemone_answer(answer)}" for key, answer in response.answers.items())
|
|
626
|
+
return "\n".join(lines)
|
|
627
|
+
|
|
628
|
+
|
|
594
629
|
def workspace(item: Workspace) -> str:
|
|
595
630
|
purpose = f" purpose={item.purpose}" if item.purpose else ""
|
|
596
631
|
return f"{item.workspace_id} {item.name} {item.domain_slug}@{item.domain_version} {item.status}{purpose}"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""``ndi systemone``: answer typed questions about a state in one synchronous call. No workspace, no job."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ndi_sdk.client import NdiClient
|
|
11
|
+
from ndi_sdk.models.systemone import SystemOneRequest
|
|
12
|
+
from pydantic import JsonValue, ValidationError
|
|
13
|
+
|
|
14
|
+
from ndi_cli import _render
|
|
15
|
+
from ndi_cli._workspace import _dump_or_text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_parsers(sub: argparse._SubParsersAction, *, default_timeout: float) -> None:
|
|
19
|
+
systemone = sub.add_parser(
|
|
20
|
+
"systemone",
|
|
21
|
+
help="Answer noul, choice, and score questions about a state.",
|
|
22
|
+
description=(
|
|
23
|
+
"Answer typed questions about a state in one call. A VALUE is inline, @FILE reads a file, and @- reads stdin."
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
systemone.add_argument(
|
|
27
|
+
"--json", action="store_true", help="Always print the full response JSON, never the shortened text rendering."
|
|
28
|
+
)
|
|
29
|
+
systemone.add_argument(
|
|
30
|
+
"--timeout",
|
|
31
|
+
type=float,
|
|
32
|
+
default=default_timeout,
|
|
33
|
+
metavar="SECONDS",
|
|
34
|
+
help=f"HTTP timeout for the call (default {default_timeout:.0f}s).",
|
|
35
|
+
)
|
|
36
|
+
state = systemone.add_mutually_exclusive_group(required=True)
|
|
37
|
+
state.add_argument("--state", default=None, metavar="VALUE", help="The state as text.")
|
|
38
|
+
state.add_argument("--state-json", default=None, metavar="VALUE", help="The state as any JSON value.")
|
|
39
|
+
systemone.add_argument(
|
|
40
|
+
"--questions",
|
|
41
|
+
required=True,
|
|
42
|
+
metavar="VALUE",
|
|
43
|
+
help='JSON object of named questions, e.g. {"approve": {"type": "noul", "instructions": "..."}}.',
|
|
44
|
+
)
|
|
45
|
+
systemone.set_defaults(run=_run, family="systemone", needs_workspace=False, needs_client=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_value(value: str, flag: str) -> str:
|
|
49
|
+
if not value.startswith("@"):
|
|
50
|
+
return value
|
|
51
|
+
if value == "@-":
|
|
52
|
+
return sys.stdin.read()
|
|
53
|
+
try:
|
|
54
|
+
return Path(value[1:]).read_text(encoding="utf-8")
|
|
55
|
+
except OSError as exc:
|
|
56
|
+
raise ValueError(f"cannot read {flag} {value[1:]!r}: {exc.strerror or exc}") from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _read_json(value: str, flag: str) -> JsonValue:
|
|
60
|
+
try:
|
|
61
|
+
return json.loads(_read_value(value, flag))
|
|
62
|
+
except json.JSONDecodeError as exc:
|
|
63
|
+
raise ValueError(f"{flag} is not valid JSON: {exc}") from None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _request(args: argparse.Namespace) -> SystemOneRequest:
|
|
67
|
+
state = _read_json(args.state_json, "--state-json") if args.state is None else _read_value(args.state, "--state")
|
|
68
|
+
questions = _read_json(args.questions, "--questions")
|
|
69
|
+
if not isinstance(questions, dict) or not questions:
|
|
70
|
+
raise ValueError("--questions must be a non-empty JSON object of named questions")
|
|
71
|
+
try:
|
|
72
|
+
return SystemOneRequest.model_validate({"state": state, "questions": questions})
|
|
73
|
+
except ValidationError as exc:
|
|
74
|
+
problems = "; ".join(f"{'.'.join(str(part) for part in error['loc'])}: {error['msg']}" for error in exc.errors())
|
|
75
|
+
raise ValueError(f"--questions: {problems}") from None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run(client: NdiClient, workspace_id: str, args: argparse.Namespace) -> int:
|
|
79
|
+
request = _request(args)
|
|
80
|
+
response = client.systemone.evaluate(request.state, request.questions)
|
|
81
|
+
return _dump_or_text(response, args, text=_render.systemone(response), stem="systemone")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The workspace-tool verbs: ``ask-file`` everywhere, and the agent run's file commands
|
|
1
|
+
"""The workspace-tool verbs: ``ask-file`` everywhere, and the agent run's file commands.
|
|
2
2
|
|
|
3
3
|
The agent-only commands have no SDK method: their /v1 routes are unpublished, so
|
|
4
4
|
they reach the run's socket through the tools' transport directly.
|
|
@@ -24,8 +24,6 @@ from ndi_cli import _render
|
|
|
24
24
|
from ndi_cli._local import (
|
|
25
25
|
FileMetadataRequest,
|
|
26
26
|
FolderMetadataRequest,
|
|
27
|
-
KgAnswerRequest,
|
|
28
|
-
KgAnswerResponse,
|
|
29
27
|
ReadFileRequest,
|
|
30
28
|
RunSqlRequest,
|
|
31
29
|
SearchRequest,
|
|
@@ -35,11 +33,11 @@ from ndi_cli.commands import WorkspaceCommand
|
|
|
35
33
|
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
36
34
|
|
|
37
35
|
|
|
38
|
-
def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser, *, in_agent_run: bool
|
|
36
|
+
def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser, *, in_agent_run: bool) -> None:
|
|
39
37
|
"""Register the tool verbs.
|
|
40
38
|
|
|
41
39
|
Outside an agent run only ``ask-file`` is registered. Inside one the file
|
|
42
|
-
commands are too
|
|
40
|
+
commands are too.
|
|
43
41
|
"""
|
|
44
42
|
if in_agent_run:
|
|
45
43
|
folder = sub.add_parser(
|
|
@@ -100,18 +98,29 @@ def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser
|
|
|
100
98
|
read = sub.add_parser(
|
|
101
99
|
WorkspaceCommand.READ_FILE,
|
|
102
100
|
parents=[common],
|
|
103
|
-
help="Print the text of one uploaded
|
|
101
|
+
help="Print the text of one or more uploaded files.",
|
|
104
102
|
description=(
|
|
105
103
|
"Content goes to stdout; a one-line footer (pages, components returned/omitted, truncation) goes to "
|
|
106
104
|
"stderr. Scope with --component (names from file-metadata) or --pages; images are refused — use ask-file."
|
|
107
105
|
),
|
|
108
106
|
)
|
|
109
|
-
read.add_argument(
|
|
107
|
+
read.add_argument(
|
|
108
|
+
"targets",
|
|
109
|
+
nargs="+",
|
|
110
|
+
metavar="path",
|
|
111
|
+
help="Uploaded source paths. Several are fetched together, each under a `==> path <==` header.",
|
|
112
|
+
)
|
|
110
113
|
read.add_argument("--component", default=None, help="One tab or section, named as file-metadata lists it.")
|
|
111
114
|
read.add_argument("--pages", default=None, metavar="SPEC", help="1-based pages, e.g. 3 or 1-5,8. Not for spreadsheets.")
|
|
112
115
|
read.add_argument("--max-chars", type=int, default=None, help="Character budget for the returned text.")
|
|
113
116
|
read.set_defaults(
|
|
114
|
-
run=_run_read_file,
|
|
117
|
+
run=_run_read_file,
|
|
118
|
+
render=_render_read_file,
|
|
119
|
+
verbatim=True,
|
|
120
|
+
label_targets=True,
|
|
121
|
+
family="tool",
|
|
122
|
+
needs_workspace=True,
|
|
123
|
+
needs_client=True,
|
|
115
124
|
)
|
|
116
125
|
|
|
117
126
|
ask_file = sub.add_parser(
|
|
@@ -164,29 +173,11 @@ def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser
|
|
|
164
173
|
)
|
|
165
174
|
search.add_argument("query", help="What to look for.")
|
|
166
175
|
search.add_argument("-k", type=int, default=10, help="Number of hits (default 10).")
|
|
167
|
-
search.add_argument(
|
|
176
|
+
search.add_argument(
|
|
177
|
+
"--path-prefix", default=None, help="Only files under this directory prefix, or only the one file at this full path."
|
|
178
|
+
)
|
|
168
179
|
search.add_argument("--category", action="append", default=None, dest="categories", help="Category filter; repeatable.")
|
|
169
180
|
search.set_defaults(run=_run_search, render=_render.search, family="tool", needs_workspace=True, needs_client=True)
|
|
170
|
-
if not graph:
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
kg_answer = sub.add_parser(
|
|
174
|
-
WorkspaceCommand.KG_ANSWER,
|
|
175
|
-
parents=[common],
|
|
176
|
-
help="Retrieve from the workspace knowledge graph in one call — entities and pages, never an answer.",
|
|
177
|
-
description=(
|
|
178
|
-
"Runs the full graph pass: locates the entities the question names and reads the pages they cite. "
|
|
179
|
-
"Prints the entity cards and the pages read: a page marked (full) is the document's text, quotable as "
|
|
180
|
-
"printed; re-read only a page shown in part, for what lies past the cut. The graph only knows what its own build extracted — a question resting on material "
|
|
181
|
-
"outside its scope (parquet, CSV, anything not yet built into it) gets nothing from this call, and "
|
|
182
|
-
"even a covered entity's pages are a starting sample, not the workspace. "
|
|
183
|
-
"Call this first, once, with the question verbatim."
|
|
184
|
-
),
|
|
185
|
-
)
|
|
186
|
-
kg_answer.add_argument("query", help="The question, verbatim.")
|
|
187
|
-
kg_answer.set_defaults(
|
|
188
|
-
run=_run_kg_answer, render=_render_kg_answer, verbatim=True, family="tool", needs_workspace=True, needs_client=True
|
|
189
|
-
)
|
|
190
181
|
|
|
191
182
|
|
|
192
183
|
def _agent_command(
|
|
@@ -245,7 +236,7 @@ def parse_pages(spec: str | None) -> list[int] | None:
|
|
|
245
236
|
|
|
246
237
|
|
|
247
238
|
def _run_read_file(tools: SyncTools, workspace_id: str, args: argparse.Namespace) -> ReadFileResponse:
|
|
248
|
-
body = ReadFileRequest(path=args.
|
|
239
|
+
body = ReadFileRequest(path=args.target, pages=parse_pages(args.pages), component=args.component, max_chars=args.max_chars)
|
|
249
240
|
return _agent_command(tools, ReadFileResponse, workspace_id, WorkspaceCommand.READ_FILE, body)
|
|
250
241
|
|
|
251
242
|
|
|
@@ -271,11 +262,3 @@ def _run_run_sql(tools: SyncTools, workspace_id: str, args: argparse.Namespace)
|
|
|
271
262
|
def _run_search(tools: SyncTools, workspace_id: str, args: argparse.Namespace) -> SearchResponse:
|
|
272
263
|
body = SearchRequest(query=args.query, k=args.k, path_prefix=args.path_prefix, categories=args.categories)
|
|
273
264
|
return _agent_command(tools, SearchResponse, workspace_id, WorkspaceCommand.HYBRID_SEARCH, body)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def _run_kg_answer(tools: SyncTools, workspace_id: str, args: argparse.Namespace) -> KgAnswerResponse:
|
|
277
|
-
return _agent_command(tools, KgAnswerResponse, workspace_id, WorkspaceCommand.KG_ANSWER, KgAnswerRequest(query=args.query))
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def _render_kg_answer(response: KgAnswerResponse) -> str:
|
|
281
|
-
return response.text
|
|
@@ -8,11 +8,13 @@ import sys
|
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from pathlib import Path
|
|
11
|
+
from typing import get_args
|
|
11
12
|
|
|
12
13
|
from ndi_sdk.client import NdiClient
|
|
13
14
|
from ndi_sdk.credentials import write_config
|
|
14
15
|
from ndi_sdk.errors import ErrorCode, JobFailedError, JobTimeoutError, NdiError, NdiStatusError
|
|
15
16
|
from ndi_sdk.models.jobs import Job, UploadFileResult
|
|
17
|
+
from ndi_sdk.models.tools import AutomaticSearchMode
|
|
16
18
|
from ndi_sdk.resources.jobs import check_terminal
|
|
17
19
|
|
|
18
20
|
from ndi_cli import _render
|
|
@@ -117,7 +119,6 @@ def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser
|
|
|
117
119
|
|
|
118
120
|
deep = sub.add_parser("deep-search", parents=[common], help="Run an agentic workspace search.")
|
|
119
121
|
deep.add_argument("query")
|
|
120
|
-
deep.add_argument("--effort", choices=("low", "medium", "high"), default=None)
|
|
121
122
|
deep.add_argument("--prefix", default=None, dest="path_prefix")
|
|
122
123
|
deep.add_argument(
|
|
123
124
|
"--path",
|
|
@@ -139,8 +140,7 @@ def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser
|
|
|
139
140
|
add_job_output_args(fact)
|
|
140
141
|
fact.set_defaults(run=_run_fact_search, family="workspace", needs_workspace=True, needs_client=True)
|
|
141
142
|
|
|
142
|
-
# No --effort, --session-id, or --allow-clarification: the route is
|
|
143
|
-
# single-shot and the service chooses the pipeline.
|
|
143
|
+
# No --effort, --session-id, or --allow-clarification: the route is single-shot.
|
|
144
144
|
automatic = sub.add_parser(
|
|
145
145
|
"automatic-search",
|
|
146
146
|
parents=[common],
|
|
@@ -148,7 +148,14 @@ def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser
|
|
|
148
148
|
)
|
|
149
149
|
automatic.add_argument("query")
|
|
150
150
|
automatic.add_argument("--prefix", default=None, dest="path_prefix")
|
|
151
|
-
automatic.add_argument("--
|
|
151
|
+
automatic.add_argument("--path", action="append", dest="paths", default=None, help="Scope to this file; repeatable.")
|
|
152
|
+
automatic.add_argument("--top-k", type=int, default=None, help="Cap the citation list when fact search answers.")
|
|
153
|
+
automatic.add_argument(
|
|
154
|
+
"--mode",
|
|
155
|
+
choices=get_args(AutomaticSearchMode),
|
|
156
|
+
default=None,
|
|
157
|
+
help="auto (the default) lets the service choose; fact or agent runs that route directly.",
|
|
158
|
+
)
|
|
152
159
|
_add_context_args(automatic)
|
|
153
160
|
add_job_output_args(automatic)
|
|
154
161
|
automatic.set_defaults(run=_run_automatic_search, family="workspace", needs_workspace=True, needs_client=True)
|
|
@@ -161,6 +168,7 @@ def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser
|
|
|
161
168
|
"--cursor", default=None, help="Page an earlier run: paste its result's next_cursor. A page is a new job."
|
|
162
169
|
)
|
|
163
170
|
filtered.add_argument("--prefix", default=None, dest="path_prefix")
|
|
171
|
+
filtered.add_argument("--path", action="append", dest="paths", default=None, help="Scope to this file; repeatable.")
|
|
164
172
|
_add_context_args(filtered)
|
|
165
173
|
# No --result-unit: the server deprecates the grouping override and returns
|
|
166
174
|
# catalog entries. No --allow-clarification: a clarification is answered by a
|
|
@@ -534,7 +542,6 @@ def _run_deep_search(client: NdiClient, workspace_id: str, args: argparse.Namesp
|
|
|
534
542
|
workspace_id,
|
|
535
543
|
query=args.query,
|
|
536
544
|
context=args.context,
|
|
537
|
-
effort=args.effort,
|
|
538
545
|
path_prefix=args.path_prefix,
|
|
539
546
|
paths=args.paths,
|
|
540
547
|
)
|
|
@@ -559,7 +566,9 @@ def _run_automatic_search(client: NdiClient, workspace_id: str, args: argparse.N
|
|
|
559
566
|
query=args.query,
|
|
560
567
|
context=args.context,
|
|
561
568
|
path_prefix=args.path_prefix,
|
|
569
|
+
paths=args.paths,
|
|
562
570
|
top_k=args.top_k,
|
|
571
|
+
mode=args.mode,
|
|
563
572
|
)
|
|
564
573
|
return _finish_job(client, job, args, _render.automatic_search_job, "automatic-search")
|
|
565
574
|
|
|
@@ -575,8 +584,11 @@ def _run_filtered_search(client: NdiClient, workspace_id: str, args: argparse.Na
|
|
|
575
584
|
# A page re-executes the parent's own stored query, so the scoping flags
|
|
576
585
|
# cannot apply to it. Accepting and dropping them would quietly answer a
|
|
577
586
|
# different question than the one typed.
|
|
578
|
-
if args.path_prefix or args.context is not None:
|
|
579
|
-
print(
|
|
587
|
+
if args.path_prefix or args.paths or args.context is not None:
|
|
588
|
+
print(
|
|
589
|
+
"error: --prefix, --path, --context and --no-context belong to the initial query, not a page",
|
|
590
|
+
file=sys.stderr,
|
|
591
|
+
)
|
|
580
592
|
return 2
|
|
581
593
|
job = client.search.filtered_page(workspace_id, cursor=args.cursor)
|
|
582
594
|
else:
|
|
@@ -585,6 +597,7 @@ def _run_filtered_search(client: NdiClient, workspace_id: str, args: argparse.Na
|
|
|
585
597
|
query=args.query,
|
|
586
598
|
context=args.context,
|
|
587
599
|
path_prefix=args.path_prefix,
|
|
600
|
+
paths=args.paths,
|
|
588
601
|
)
|
|
589
602
|
return _finish_job(client, job, args, _render.filtered_search_job, "filtered-search")
|
|
590
603
|
|
|
@@ -13,7 +13,6 @@ class WorkspaceCommand(StrEnum):
|
|
|
13
13
|
KG_INFO = "kg-info"
|
|
14
14
|
KG_SEARCH = "kg-search"
|
|
15
15
|
KG_WALK = "kg-walk"
|
|
16
|
-
KG_ANSWER = "kg-answer"
|
|
17
16
|
|
|
18
17
|
@property
|
|
19
18
|
def activity_label(self) -> str:
|
|
@@ -27,5 +26,4 @@ class WorkspaceCommand(StrEnum):
|
|
|
27
26
|
self.KG_INFO: "Sizing up the knowledge graph",
|
|
28
27
|
self.KG_SEARCH: "Searching the knowledge graph",
|
|
29
28
|
self.KG_WALK: "Following graph relations",
|
|
30
|
-
self.KG_ANSWER: "Reading the knowledge graph",
|
|
31
29
|
}[self]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|