Skip to content

cu analyze: default analyzer by file type, markdown anchors, --format both, cu resolve, result deletion - #26

Open
Arlind Nocaj (ArlindNocaj) wants to merge 3 commits into
Azure:mainfrom
ArlindNocaj:main
Open

Arlind Nocaj (ArlindNocaj) wants to merge 3 commits into
Azure:mainfrom
ArlindNocaj:main

Conversation

@ArlindNocaj

@ArlindNocaj Arlind Nocaj (ArlindNocaj) commented Sep 10, 2026

Copy link
Copy Markdown

Makes cu-cli usable by a coding agent with nothing but CU_ENDPOINT (+ az login). Built fresh on the latest main; additive — no existing flag, command or doc text removed. One commit, 16 files (+3 re-recorded cassettes), no new dependencies.

Key improvements

  • Usable without CU expertise. cu analyze doc.pdf just works: the analyzer is chosen by file type (prebuilt-documentSearch for documents), no profile setup, no model choice.
  • Anchors in the markdown. Every section, table, figure (and with --level paragraph every paragraph) carries <!--s3-->-style ids. Without them an agent has no way to point back to where in the document a statement came from.
  • Offline cu resolve. Turns an anchor into page, bounding box, text and surrounding blocks from the saved JSON — no service call. An agent reads the markdown, cites ids, and resolves them on demand only when it needs to look at a figure, a table region or the page itself.
  • Two files from one call. --format both --output-file doc writes doc.md and doc.json and lists the paths on stdout. The agent's context is not polluted with the whole document: it can grep/head the markdown for the parts it needs and keep the JSON on disk for cu resolve.
export CU_ENDPOINT=https://<resource>.services.ai.azure.com/
cu analyze report.pdf                                                  # works: markdown with anchors → stdout
cu analyze report.pdf --level paragraph --format both --output-file report   # report.md + report.json
cu resolve report.json p30 --around 1                                  # page, bbox, text, neighbours — offline
Before After
cu analyze doc.pdf errors: no default_analyzer configured uses CU_DEFAULT_ANALYZER › profile › prebuilt-documentSearch (documents) / *Search by file type
--llm-input xor --json — two billed calls for two files --format md|json|both (--json = shorthand, --llm-input hidden alias); --format both --output-file NAMENAME.md + NAME.json, one call; batch gets both sidecars. Written paths go to stdout, one per line (gog-style: data on stdout, diagnostics on stderr)
Markdown has no way back to the page <!--s3--> / <!--t0--> / <!--f0--> anchors (+ <!--p30--> with --level paragraph) = service-result array indices; stripping them yields the previous markdown; +0.2 % tokens coarse, +6 % paragraph
new cu resolve RESULT.json ID… [--around N] — local, no endpoint
result stays on the service, operation id echoed in JSON deleted after retrieval (--keep-result opts out); id omitted

Docs: Quickstart section in the root README ("which command when"), two short additive paragraphs in cu-cli/README.md, CHANGELOG Unreleased.

Deliberately left out (follow-ups): separate --md-rich/--md/--map output flags, cu resolve --page/--images, grounded field front matter, --schema ephemeral extraction, agent skill.

Verification: cu-cli/scripts/ci.sh green (headers, ruff, mypy, unit, integration playback) on Python 3.12. test_validate_release.py::test_cli_requires_stable_core_dependency fails on main already (not in ci.sh), unchanged here.

… both, cu resolve, result deletion (#2)

Makes cu-cli usable by an agent with nothing but CU_ENDPOINT. Additive on top of
the existing analyze command; no flags or behaviour removed.

- Without --analyzer, cu analyze uses CU_DEFAULT_ANALYZER, the profile
  default_analyzer, or a default by file type (prebuilt-documentSearch for
  documents, prebuilt-*Search otherwise) instead of failing.
- Markdown output carries <!--sN-->/<!--tN-->/<!--fN--> anchors before sections,
  tables and figures; --level paragraph adds <!--pN-->. Ids are the service-result
  array indices; stripping the anchors yields the previous markdown. Page and
  classification-segment spans are shifted so the SDK still slices correctly.
- --format md|json|both selects the view (--json = --format json, --llm-input
  stays as a hidden alias of --format md). `--format both --output-file NAME`
  writes NAME.md and NAME.json from one billed call; with --output-dir every
  input gets .result.md and .result.json. Written paths are printed to stdout,
  one per line (diagnostics stay on stderr).
- New `cu resolve RESULT.json ID... [--around N]`: page, bbox, text and
  neighbouring blocks, offline. Sections report the pages of their blocks.
- Results are deleted on the service after retrieval (--keep-result opts out),
  using the poller's operation id; the id is omitted from JSON output.
- Root README: Quickstart section. cu-cli README/CHANGELOG: short additions.
  Analyze cassettes re-recorded with the DELETE call.

Co-authored-by: Arlind Nocaj <2978097+ArlindNocaj@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 28e0a742-7dd0-43e4-8531-72d7aa37ff9a
…n GitHub (#3)

Co-authored-by: Arlind Nocaj <2978097+ArlindNocaj@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 28e0a742-7dd0-43e4-8531-72d7aa37ff9a
Co-authored-by: Arlind Nocaj <2978097+ArlindNocaj@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 28e0a742-7dd0-43e4-8531-72d7aa37ff9a
@ArlindNocaj

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

Comment thread cu-cli/README.md
Without `--analyzer`, `cu analyze` uses `CU_DEFAULT_ANALYZER`, the profile's
`default_analyzer`, or — by file type — `prebuilt-documentSearch` for documents
and `prebuilt-imageSearch` / `-audioSearch` / `-videoSearch` otherwise. Every
result is deleted from the service right after retrieval (`--keep-result` keeps it).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the result will still deleted automatically by CU service after 24 hours, --keep-result can be misleading. Another possible name is --delay-delete

Comment thread README.md

More tools will be added over time.

## Quickstart

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move the content for quickstart into cu-cli/README.md directly as pypi will get the content from cu-cli/README.md

Comment thread README.md
## Quickstart

```bash
pip install cu-cli

@yungshinlintw Yung-Shin Lin (yungshinlintw) Sep 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we want this to be a quickstart, we can add the following step to allow user to optionally use a multi-page pdf that they can try right away and show the benefit prebuilt-documentSearch

# Uncomment the following to use a sample file
# curl -L https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/financial_table_and_chart.pdf -o doc.pdf

cu analyze doc.pdf

Comment thread README.md
| I want to… | Command | You get |
| --- | --- | --- |
| Read a document as markdown | `cu analyze doc.pdf` | Text, tables, summary, figure descriptions, anchors on sections/tables/figures. Default `-a prebuilt-documentSearch`. |
| …and cite any paragraph later | `cu analyze doc.pdf --level paragraph --format both --output-file doc` | `doc.md` with `<!--p3-->` per paragraph (+6.5 % tokens) and `doc.json`, one call; paths printed to stdout. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not quote the 6.5% token increase as it's not a universal ratio for all files

Comment thread README.md
| I want to… | Command | You get |
| --- | --- | --- |
| Read a document as markdown | `cu analyze doc.pdf` | Text, tables, summary, figure descriptions, anchors on sections/tables/figures. Default `-a prebuilt-documentSearch`. |
| …and cite any paragraph later | `cu analyze doc.pdf --level paragraph --format both --output-file doc` | `doc.md` with `<!--p3-->` per paragraph (+6.5 % tokens) and `doc.json`, one call; paths printed to stdout. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should explain what 'p3' means as it's not clear from the context for readers

Comment thread README.md
| Page + bbox + context of an anchor | `cu resolve doc.json p3 --around 1` | Page, bbox (inches), text, block before/after. Local, no endpoint. |
| Cheapest OCR + layout, no LLM | `cu analyze doc.pdf -a prebuilt-layout` | Markdown + tables, no summary/figure text. `-a prebuilt-read` for text only. |
| Which fields are in here? | `cu analyze doc.pdf -a prebuilt-documentFields` | LLM-proposed key/values. `-a prebuilt-documentFieldSchema` proposes a schema. |
| Extract fields (invoice, receipt, ID…) | `cu analyze invoice.pdf -a prebuilt-invoice --json` | Typed fields with **confidence**, **bounding box** (`source`) and **span**. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the "You get" column, please add the following link for users to check out supported prebuilt analyzers:

https://learn.microsoft.com/azure/ai-services/content-understanding/concepts/prebuilt-analyzers

Comment thread README.md
| Cheapest OCR + layout, no LLM | `cu analyze doc.pdf -a prebuilt-layout` | Markdown + tables, no summary/figure text. `-a prebuilt-read` for text only. |
| Which fields are in here? | `cu analyze doc.pdf -a prebuilt-documentFields` | LLM-proposed key/values. `-a prebuilt-documentFieldSchema` proposes a schema. |
| Extract fields (invoice, receipt, ID…) | `cu analyze invoice.pdf -a prebuilt-invoice --json` | Typed fields with **confidence**, **bounding box** (`source`) and **span**. |
| Extract *my* fields | `cu analyzer schema create --from-sample f.pdf …` → `cu analyzer create` | Custom analyzer — [Create a custom analyzer][cu_custom_analyzer]. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please direct user to this section in README for more details in creating custom analyzer:
https://github.com/Azure/content-understanding-toolkit/blob/main/cu-cli/README.md#create-a-custom-analyzer

Comment thread README.md
| …and cite any paragraph later | `cu analyze doc.pdf --level paragraph --format both --output-file doc` | `doc.md` with `<!--p3-->` per paragraph (+6.5 % tokens) and `doc.json`, one call; paths printed to stdout. |
| Page + bbox + context of an anchor | `cu resolve doc.json p3 --around 1` | Page, bbox (inches), text, block before/after. Local, no endpoint. |
| Cheapest OCR + layout, no LLM | `cu analyze doc.pdf -a prebuilt-layout` | Markdown + tables, no summary/figure text. `-a prebuilt-read` for text only. |
| Which fields are in here? | `cu analyze doc.pdf -a prebuilt-documentFields` | LLM-proposed key/values. `-a prebuilt-documentFieldSchema` proposes a schema. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not mention prebuilt-documentFieldSchema here as it's specifically used as an optional helper when creating custom analyzer, and it's advanced usage, so we do not need to mention it in a quickstart

Comment thread README.md
| Which fields are in here? | `cu analyze doc.pdf -a prebuilt-documentFields` | LLM-proposed key/values. `-a prebuilt-documentFieldSchema` proposes a schema. |
| Extract fields (invoice, receipt, ID…) | `cu analyze invoice.pdf -a prebuilt-invoice --json` | Typed fields with **confidence**, **bounding box** (`source`) and **span**. |
| Extract *my* fields | `cu analyzer schema create --from-sample f.pdf …` → `cu analyzer create` | Custom analyzer — [Create a custom analyzer][cu_custom_analyzer]. |
| Batch a folder | `cu analyze --source docs/ --format both --output-dir out/ --yes` | `NAME.result.md` + `.result.json` per file, paths on stdout. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the description for per file, paths on stdout, it is a little too short to be clear for what they mean.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings affect Markdown anchors, format validation, output collisions, modality selection, and result cleanup.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR makes cu-cli more agent-friendly with automatic analyzer selection, anchored Markdown, offline resolution, dual-format output, and result cleanup.

Changes:

  • Adds modality-based defaults and CU_DEFAULT_ANALYZER.
  • Adds rich Markdown anchors and cu resolve.
  • Adds --format both, documentation, tests, recordings, and cleanup behavior.
File summaries
File Review summary
README.md Adds Quickstart guidance.
cu-cli/README.md Documents the new CLI workflow and options.
cu-cli/packages/standalone/tests/unit/test_cli.py Tests CLI defaults and output behavior.
cu-cli/packages/standalone/tests/unit/commands/test_analyze_contract.py Updates analyze contract tests.
cu-cli/packages/standalone/tests/integration/recordings/analyze_single.yaml Refreshes single-file playback.
cu-cli/packages/standalone/tests/integration/recordings/analyze_batch.yaml Refreshes batch playback.
cu-cli/packages/standalone/src/cu_cli/output.py Critical (3 votes): SDK result models are not converted before Markdown rendering, so normal Markdown output lacks anchors.
cu-cli/packages/standalone/src/cu_cli/modality.py Moderate (2 votes): .webm is classified as audio despite documented video precedence.
cu-cli/packages/standalone/src/cu_cli/commands/resolve.py Adds offline anchor resolution.
cu-cli/packages/standalone/src/cu_cli/commands/analyze.py Moderate findings: conflicting --llm-input and format flags are not rejected (1 vote); dual-output collision checks cover only JSON (3 votes); conflicting --json and --format md silently selects Markdown (2 votes).
cu-cli/packages/standalone/src/cu_cli/cli.py Registers resolve and updates help.
cu-cli/packages/core/tests/test_rich_markdown.py Tests anchor mapping and resolution.
cu-cli/packages/core/tests/test_environment.py Tests environment registration.
cu-cli/packages/core/tests/test_analysis.py Tests result deletion.
cu-cli/packages/core/src/cu_cli_core/rich_markdown.py Moderate findings (1 vote each): paragraph anchors are omitted inside tables or figures; coarse sections may lack page mappings; malformed coordinates can raise ValueError.
cu-cli/packages/core/src/cu_cli_core/environment.py Registers analyzer environment configuration.
cu-cli/packages/core/src/cu_cli_core/command_spec.py Defines the new command options.
cu-cli/packages/core/src/cu_cli_core/analysis.py Moderate (3 votes): cleanup failures are ignored, so retained service results can be reported as successful deletion.
cu-cli/CHANGELOG.md Records the unreleased changes.
Review details

Suppressed comments (4)

cu-cli/packages/core/src/cu_cli_core/rich_markdown.py:167

  • --level paragraph promises an anchor for every paragraph, but this condition drops every paragraph whose span starts inside a table or figure. Those paragraphs remain in the service paragraphs array and are not resolvable by their p<i> ids, so the advertised paragraph-level citation coverage is incomplete. Either emit paragraph anchors for these entries or change the public contract and examples to document the exception.
        for index, paragraph in enumerate(content.get("paragraphs") or []):
            if "span" not in paragraph:
                continue
            start = int(paragraph["span"]["offset"])
            if any(lo <= start < hi for lo, hi in containers):

cu-cli/packages/core/src/cu_cli_core/rich_markdown.py:192

  • In coarse mode, paragraph ids are intentionally absent, so a section containing only paragraphs has no child with a bbox. The page calculation therefore leaves pages unset for that section even though the section is anchored and cu resolve is supposed to map anchors back to document location. Derive section pages from pages[].spans (or retain enough paragraph/page metadata) instead of relying only on ids present in the coarse map.
    for entry in ids.values():
        if entry["kind"] == "section":
            pages = sorted({int(ids[c]["bbox"]["page"]) for c in _child_ids(entry, ids) if ids[c].get("bbox")})
            if pages:
                entry["pages"] = pages

cu-cli/packages/core/src/cu_cli_core/rich_markdown.py:57

  • A source that matches _SOURCE_RE but contains non-numeric coordinates, such as D(1,a,b,c,d), raises ValueError from this conversion instead of returning None. That aborts rich-markdown rendering for a malformed optional source; catch ValueError so malformed sources are handled like the missing/malformed cases this parser is intended to tolerate.
    values = [float(v) for v in match.group(2).split(",") if v.strip()]

cu-cli/packages/standalone/src/cu_cli/commands/analyze.py:337

  • --llm-input is documented as an alias for --format md, but this validation does not reject --llm-input --format both. That combination reaches the both branch, writes JSON and Markdown, and silently ignores the alias instead of enforcing the selected view. Include "both" in the conflict check (and use a format-neutral error message).
    if llm_input and (json_output or output_format == "json"):
        raise CuCliError("--llm-input and --json cannot be combined.", exit_code=VALIDATION_FAILURE)
  • Files reviewed: 19/20 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +136 to +139
if isinstance(result, Mapping): # raw service JSON: inject the <!--id--> anchors
from cu_cli_core.rich_markdown import unwrap_result, with_rich_markdown

result = _as_sdk_result(with_rich_markdown(unwrap_result(result), level))
Comment on lines +91 to +92
except Exception: # noqa: BLE001 - a failed cleanup must not fail the analysis
return False
Comment on lines +338 to +341
if json_output:
output_format = "json" if output_format in (None, "json") else output_format
both = output_format == "both"
json_output = output_format in ("json", "both")
Comment on lines +615 to +617
if both:
sibling = md_path or _md_sibling(job.out_path)
dump_markdown(result, out=sibling, level=level)
def modality_of(path: "str | os.PathLike[str]") -> str:
"""Best-effort modality from the file extension (documents when unknown)."""
ext = os.path.splitext(os.fspath(path))[1].lower()
if ext in VIDEO_EXTS:
Comment thread README.md

`cu analyze` picks the analyzer by file type (`prebuilt-documentSearch` for documents) and
prints LLM-ready markdown: text, tables, a document summary, a description of every figure,
and small anchors — `<!--s1-->` section, `<!--t0-->` table, `<!--f0-->` figure — that

@yungshinlintw Yung-Shin Lin (yungshinlintw) Sep 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot assume that all users want to save the JSON, and we have many customers mainly use markdown. Currently, we don't have direct customers asking anchor from markdown back to JSON for grounding purpose as far as I know (also checked with PMs).

However, the anchors in the md seems to be a useful addition.

While I like that anchor format, I think:

  • It should not be default
  • It should be merged as an option into CU SDK to_llm_input (Chien Yuan Chang (@chienyuanchang) can help with this for all SDK languages, starting with Python first)
  • We should control the generation of anchor with --anchor-level none (default) | coarse | paragraph

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants