ndi-cli 0.7.0__tar.gz → 0.8.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.7.0 → ndi_cli-0.8.0}/PKG-INFO +10 -1
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/README.md +9 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/pyproject.toml +1 -1
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/pyproject.toml.orig +1 -1
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/__init__.py +1 -1
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_render.py +126 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_workspace.py +41 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/LICENSE +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_cli.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_config.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_docops.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_entrypoint.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_jobs.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_local.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_login.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_output.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_sources.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/_tools.py +0 -0
- {ndi_cli-0.7.0 → ndi_cli-0.8.0}/src/ndi_cli/commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndi-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -56,6 +56,7 @@ config location. Workspace-scoped verbs also take `--workspace ID`.
|
|
|
56
56
|
| `ndi files upload\|list\|get\|delete` | Workspace files (`--ingest` uploads then queues ingestion) |
|
|
57
57
|
| `ndi ingest` | Queue ingestion |
|
|
58
58
|
| `ndi deep-search QUERY` / `ndi fact-search QUERY` | Agentic / single-shot search |
|
|
59
|
+
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--cursor` pages an earlier run) |
|
|
59
60
|
| `ndi je-testing QUERY` | Journal-entry testing over a ledger package (`--path` to scope it, `--effort` for the level) |
|
|
60
61
|
| `ndi folder-metadata` / `file-metadata` / `read-file` / `ask-file` / `run-sql` / `hybrid-search` | Read-only workspace tools (the sandbox surface) |
|
|
61
62
|
|
|
@@ -82,6 +83,14 @@ to **stderr**. `-o auto|md|json|payload|id` picks the shape. `--json` is an
|
|
|
82
83
|
alias for `-o json`. `--save PATH` / `--out-dir DIR` write files. `--async`
|
|
83
84
|
submits and prints the job id.
|
|
84
85
|
|
|
86
|
+
When a Parse result has external complete content, `-o auto` and `-o md` print
|
|
87
|
+
the inline preview. Use `-o json` or `-o payload` to inspect its disclosure and
|
|
88
|
+
authenticated full-content URL. A final-size preview carries
|
|
89
|
+
`content_truncated`; reduced-layout Office parsing carries `parse_fidelity`.
|
|
90
|
+
Documents use `document.content_url`; workbooks use the affected
|
|
91
|
+
`document.spreadsheet.sheets[].content_url`. The CLI does not download these
|
|
92
|
+
URLs automatically.
|
|
93
|
+
|
|
85
94
|
API failures exit 1. Usage / config errors exit 2. Schema-validation
|
|
86
95
|
failures also show up to five field constraints, each limited to 500
|
|
87
96
|
characters; request input and unrelated error-detail fields are omitted.
|
|
@@ -32,6 +32,7 @@ config location. Workspace-scoped verbs also take `--workspace ID`.
|
|
|
32
32
|
| `ndi files upload\|list\|get\|delete` | Workspace files (`--ingest` uploads then queues ingestion) |
|
|
33
33
|
| `ndi ingest` | Queue ingestion |
|
|
34
34
|
| `ndi deep-search QUERY` / `ndi fact-search QUERY` | Agentic / single-shot search |
|
|
35
|
+
| `ndi filtered-search QUERY` | Corpus filter/rank/count over the metadata catalog (`--cursor` pages an earlier run) |
|
|
35
36
|
| `ndi je-testing QUERY` | Journal-entry testing over a ledger package (`--path` to scope it, `--effort` for the level) |
|
|
36
37
|
| `ndi folder-metadata` / `file-metadata` / `read-file` / `ask-file` / `run-sql` / `hybrid-search` | Read-only workspace tools (the sandbox surface) |
|
|
37
38
|
|
|
@@ -58,6 +59,14 @@ to **stderr**. `-o auto|md|json|payload|id` picks the shape. `--json` is an
|
|
|
58
59
|
alias for `-o json`. `--save PATH` / `--out-dir DIR` write files. `--async`
|
|
59
60
|
submits and prints the job id.
|
|
60
61
|
|
|
62
|
+
When a Parse result has external complete content, `-o auto` and `-o md` print
|
|
63
|
+
the inline preview. Use `-o json` or `-o payload` to inspect its disclosure and
|
|
64
|
+
authenticated full-content URL. A final-size preview carries
|
|
65
|
+
`content_truncated`; reduced-layout Office parsing carries `parse_fidelity`.
|
|
66
|
+
Documents use `document.content_url`; workbooks use the affected
|
|
67
|
+
`document.spreadsheet.sheets[].content_url`. The CLI does not download these
|
|
68
|
+
URLs automatically.
|
|
69
|
+
|
|
61
70
|
API failures exit 1. Usage / config errors exit 2. Schema-validation
|
|
62
71
|
failures also show up to five field constraints, each limited to 500
|
|
63
72
|
characters; request input and unrelated error-detail fields are omitted.
|
|
@@ -21,6 +21,7 @@ from ndi_sdk.models.jobs import (
|
|
|
21
21
|
ClassifyResult,
|
|
22
22
|
DeepSearchV2Result,
|
|
23
23
|
ExtractResult,
|
|
24
|
+
FilteredSearchResult,
|
|
24
25
|
GroundResult,
|
|
25
26
|
IngestionResult,
|
|
26
27
|
IntelligentSearchResult,
|
|
@@ -706,6 +707,131 @@ def je_testing_job(job: Job) -> str:
|
|
|
706
707
|
return "\n".join(lines)
|
|
707
708
|
|
|
708
709
|
|
|
710
|
+
#: What a cell's non-``found`` status means, glossed for a reader who has only
|
|
711
|
+
#: this text. A field nothing could be read from is not an absent one, and
|
|
712
|
+
#: neither is one that resisted normalizing — keeping them distinct is what
|
|
713
|
+
#: makes a thin row interpretable without opening the JSON.
|
|
714
|
+
_CELL_STATUS = {
|
|
715
|
+
"not_found": "examined; field absent",
|
|
716
|
+
"ambiguous": "present, not normalizable",
|
|
717
|
+
"unexamined": "extraction could not say",
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _cell_value(cell) -> str:
|
|
722
|
+
value = cell.value
|
|
723
|
+
if value is None:
|
|
724
|
+
return _CELL_STATUS.get(cell.status, cell.status)
|
|
725
|
+
match value.kind:
|
|
726
|
+
case "money":
|
|
727
|
+
# Amounts keep their own currency here because the server never
|
|
728
|
+
# converts one either; comparing across currencies is unsupported,
|
|
729
|
+
# so dropping the code would invent a comparison.
|
|
730
|
+
assumed = " (currency assumed)" if value.currency_source == "assumed" else ""
|
|
731
|
+
return f"{value.currency} {value.amount}{assumed}"
|
|
732
|
+
case "text":
|
|
733
|
+
return _clip(value.text)
|
|
734
|
+
case "date":
|
|
735
|
+
return value.date
|
|
736
|
+
case "number":
|
|
737
|
+
return value.number
|
|
738
|
+
case "boolean":
|
|
739
|
+
return "true" if value.value else "false"
|
|
740
|
+
return value.kind
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _page_range(row) -> str:
|
|
744
|
+
if row.page_start is None:
|
|
745
|
+
return ""
|
|
746
|
+
if row.page_end is not None and row.page_end != row.page_start:
|
|
747
|
+
return f" (pages {row.page_start}-{row.page_end})"
|
|
748
|
+
return f" (page {row.page_start})"
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _filtered_rows(result: FilteredSearchResult) -> list[str]:
|
|
752
|
+
"""The rows, carrying the columns their membership hinges on.
|
|
753
|
+
|
|
754
|
+
A count-style question confirms ``total_matches`` and returns no rows at
|
|
755
|
+
all, so an empty listing is an answer rather than a gap — it prints nothing
|
|
756
|
+
instead of an empty header.
|
|
757
|
+
"""
|
|
758
|
+
if not result.rows:
|
|
759
|
+
return []
|
|
760
|
+
# The server marks which columns its published query filtered or ranked on,
|
|
761
|
+
# and those are the values every row's membership depends on; `requested` is
|
|
762
|
+
# what the reader asked to see. Everything else is context, left for --json.
|
|
763
|
+
hinges = [column.field for column in result.columns if column.filtered]
|
|
764
|
+
shown = hinges + [column.field for column in result.columns if column.requested and column.field not in hinges]
|
|
765
|
+
lines = [f"rows ({len(result.rows)}):"]
|
|
766
|
+
for row in result.rows:
|
|
767
|
+
where = f" [{row.component}]" if row.component else ""
|
|
768
|
+
lines.append(f" {row.source_path}{where}{_page_range(row)}")
|
|
769
|
+
for field in shown or list(dict.fromkeys(cell.field for cell in row.cells)):
|
|
770
|
+
# A repeated column carries one cell per item, so join them rather
|
|
771
|
+
# than showing the first and implying the field held one value.
|
|
772
|
+
values = [_cell_value(cell) for cell in row.cells if cell.field == field]
|
|
773
|
+
if values:
|
|
774
|
+
lines.append(f" {field}: {'; '.join(values)}")
|
|
775
|
+
return lines
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _filtered_caveats(result: FilteredSearchResult) -> list[str]:
|
|
779
|
+
lines: list[str] = []
|
|
780
|
+
if not result.exhaustive:
|
|
781
|
+
lines.append("not exhaustive: the matches are valid, but total_matches is a floor and not a census")
|
|
782
|
+
if result.truncated:
|
|
783
|
+
lines.append("truncated: the published query matched more than this page carries")
|
|
784
|
+
if result.catalog_drifted:
|
|
785
|
+
lines.append("catalog drifted: the underlying data changed since the query first ran")
|
|
786
|
+
# `exhaustive` is scope-wide and almost always false, so it cannot say *what*
|
|
787
|
+
# was incomplete. Only a resolved value satisfies a predicate, so a field the
|
|
788
|
+
# extraction could not read drops its components silently — a correct query
|
|
789
|
+
# coming back thin for a reason unrelated to the question.
|
|
790
|
+
for entry in result.field_readability:
|
|
791
|
+
read = f"{entry.readable} of {entry.in_scope} values read"
|
|
792
|
+
if entry.readable == 0:
|
|
793
|
+
lines.append(f"unreadable: {entry.field} — {read}; a thin result here reflects that, not an absence")
|
|
794
|
+
else:
|
|
795
|
+
lines.append(f"partly readable: {entry.field} — {read}")
|
|
796
|
+
check = result.content_verification
|
|
797
|
+
if check and check.unverified_candidates:
|
|
798
|
+
lines.append(
|
|
799
|
+
f"content check: {check.unverified_candidates} of {check.candidates} candidates unverified "
|
|
800
|
+
f"for {_clip(check.condition, 120)!r} — unknown is not a non-match"
|
|
801
|
+
)
|
|
802
|
+
lines.extend(_coverage_lines(result.coverage))
|
|
803
|
+
if result.exhausted:
|
|
804
|
+
lines.append("exhausted: step budget ran out before the query finished")
|
|
805
|
+
return lines
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def filtered_search_job(job: Job) -> str:
|
|
809
|
+
"""The totals, then the rows behind them — a catalog answer read at a glance.
|
|
810
|
+
|
|
811
|
+
A filter/rank/count question is answered by a number, so this leads with the
|
|
812
|
+
counts and prints the published query under them: the figure and the
|
|
813
|
+
statement it came out of travel together, the way a JET figure names its
|
|
814
|
+
receipt. Rows carry the columns the matches hinge on; ``-o json`` has every
|
|
815
|
+
cell, citation and column the catalog returned.
|
|
816
|
+
"""
|
|
817
|
+
result = job.result
|
|
818
|
+
if not isinstance(result, FilteredSearchResult):
|
|
819
|
+
return job_line(job)
|
|
820
|
+
lines: list[str] = []
|
|
821
|
+
if result.interpretation:
|
|
822
|
+
lines.append(f"interpretation: {_clip(result.interpretation)}")
|
|
823
|
+
files = f" ({result.total_files} files)" if result.total_files is not None else ""
|
|
824
|
+
confidence = f" | confidence: {result.confidence}" if result.confidence else ""
|
|
825
|
+
lines.append(f"matches: {result.total_matches}{files}{confidence}")
|
|
826
|
+
if result.applied_query:
|
|
827
|
+
lines.append(f"query: {_clip(result.applied_query, 300)}")
|
|
828
|
+
lines.extend(_filtered_rows(result))
|
|
829
|
+
lines.extend(_filtered_caveats(result))
|
|
830
|
+
if result.next_cursor:
|
|
831
|
+
lines.append(f"next: {result.next_cursor}")
|
|
832
|
+
return "\n".join(lines)
|
|
833
|
+
|
|
834
|
+
|
|
709
835
|
def _search_result(answer: str | None, clarification: str | None, evidences: list, *, receipts: int) -> str:
|
|
710
836
|
lines: list[str] = []
|
|
711
837
|
if clarification:
|
|
@@ -95,6 +95,22 @@ def add_parsers(sub: argparse._SubParsersAction, common: argparse.ArgumentParser
|
|
|
95
95
|
add_job_output_args(fact)
|
|
96
96
|
fact.set_defaults(run=_run_fact_search, family="workspace", needs_workspace=True, needs_client=True)
|
|
97
97
|
|
|
98
|
+
filtered = sub.add_parser(
|
|
99
|
+
"filtered-search", parents=[common], help="Run a filter/rank/count query over the metadata catalog."
|
|
100
|
+
)
|
|
101
|
+
filtered.add_argument("query", nargs="?", default=None)
|
|
102
|
+
filtered.add_argument(
|
|
103
|
+
"--cursor", default=None, help="Page an earlier run: paste its result's next_cursor. A page is a new job."
|
|
104
|
+
)
|
|
105
|
+
filtered.add_argument("--prefix", default=None, dest="path_prefix")
|
|
106
|
+
filtered.add_argument("--context", default=None)
|
|
107
|
+
# No --result-unit: the server deprecates the grouping override and returns
|
|
108
|
+
# catalog entries. No --allow-clarification: a clarification is answered by a
|
|
109
|
+
# new initial query, and a shell has nowhere to keep the thread — the reason
|
|
110
|
+
# deep-search omits --session-id.
|
|
111
|
+
add_job_output_args(filtered)
|
|
112
|
+
filtered.set_defaults(run=_run_filtered_search, family="workspace", needs_workspace=True, needs_client=True)
|
|
113
|
+
|
|
98
114
|
jet = sub.add_parser("je-testing", parents=[common], help="Run journal-entry testing over a ledger package.")
|
|
99
115
|
jet.add_argument("query")
|
|
100
116
|
jet.add_argument("--effort", choices=("low", "medium", "high"), default=None)
|
|
@@ -237,6 +253,31 @@ def _run_fact_search(client: NdiClient, workspace_id: str, args: argparse.Namesp
|
|
|
237
253
|
return _finish_job(client, job, args, _render.search_job, "fact-search")
|
|
238
254
|
|
|
239
255
|
|
|
256
|
+
def _run_filtered_search(client: NdiClient, workspace_id: str, args: argparse.Namespace) -> int:
|
|
257
|
+
if args.query and args.cursor:
|
|
258
|
+
print("error: pass a query or --cursor, not both", file=sys.stderr)
|
|
259
|
+
return 2
|
|
260
|
+
if not args.query and not args.cursor:
|
|
261
|
+
print("error: pass a query, or --cursor to page an earlier run", file=sys.stderr)
|
|
262
|
+
return 2
|
|
263
|
+
if args.cursor:
|
|
264
|
+
# A page re-executes the parent's own stored query, so the scoping flags
|
|
265
|
+
# cannot apply to it. Accepting and dropping them would quietly answer a
|
|
266
|
+
# different question than the one typed.
|
|
267
|
+
if args.path_prefix or args.context:
|
|
268
|
+
print("error: --prefix and --context belong to the initial query, not a page", file=sys.stderr)
|
|
269
|
+
return 2
|
|
270
|
+
job = client.search.filtered_page(workspace_id, cursor=args.cursor)
|
|
271
|
+
else:
|
|
272
|
+
job = client.search.filtered(
|
|
273
|
+
workspace_id,
|
|
274
|
+
query=args.query,
|
|
275
|
+
context=args.context,
|
|
276
|
+
path_prefix=args.path_prefix,
|
|
277
|
+
)
|
|
278
|
+
return _finish_job(client, job, args, _render.filtered_search_job, "filtered-search")
|
|
279
|
+
|
|
280
|
+
|
|
240
281
|
def _run_je_testing(client: NdiClient, workspace_id: str, args: argparse.Namespace) -> int:
|
|
241
282
|
job = client.search.je_testing(
|
|
242
283
|
workspace_id,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|