cu-cli 0.1.0b1__py3-none-any.whl
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.
- cu_cli/__init__.py +17 -0
- cu_cli/__main__.py +11 -0
- cu_cli/apiversion.py +124 -0
- cu_cli/cli.py +138 -0
- cu_cli/client.py +138 -0
- cu_cli/commands/__init__.py +4 -0
- cu_cli/commands/_command_spec.py +94 -0
- cu_cli/commands/_help.py +33 -0
- cu_cli/commands/_infra_models.py +184 -0
- cu_cli/commands/_infra_wizard.py +630 -0
- cu_cli/commands/_model_setup.py +46 -0
- cu_cli/commands/_options.py +112 -0
- cu_cli/commands/analyze.py +631 -0
- cu_cli/commands/analyzer.py +1462 -0
- cu_cli/commands/defaults.py +172 -0
- cu_cli/commands/doctor.py +166 -0
- cu_cli/commands/env_var.py +67 -0
- cu_cli/commands/infra.py +302 -0
- cu_cli/commands/profile_cmd.py +525 -0
- cu_cli/commands/upgrade.py +120 -0
- cu_cli/core/__init__.py +17 -0
- cu_cli/core/analyze.py +44 -0
- cu_cli/core/analyzers.py +30 -0
- cu_cli/core/azure_resources.py +486 -0
- cu_cli/core/defaults.py +18 -0
- cu_cli/core/doctor.py +42 -0
- cu_cli/core/foundry.py +68 -0
- cu_cli/core/infra_models.py +367 -0
- cu_cli/core/inputs.py +209 -0
- cu_cli/core/schema.py +24 -0
- cu_cli/errors.py +174 -0
- cu_cli/exit_codes.py +20 -0
- cu_cli/modality.py +24 -0
- cu_cli/output.py +179 -0
- cu_cli/profile.py +30 -0
- cu_cli/py.typed +0 -0
- cu_cli/resources/__init__.py +4 -0
- cu_cli/resources/azd_template/README.md +187 -0
- cu_cli/resources/azd_template/azure.yaml +27 -0
- cu_cli/resources/azd_template/hooks/postprovision.ps1 +320 -0
- cu_cli/resources/azd_template/hooks/postprovision.sh +299 -0
- cu_cli/resources/azd_template/infra/main.bicep +115 -0
- cu_cli/resources/azd_template/infra/main.parameters.json +30 -0
- cu_cli/resources/azd_template/infra/models.json +1 -0
- cu_cli/resources/azd_template/infra/modules/foundry.bicep +122 -0
- cu_cli/schema_validate.py +28 -0
- cu_cli/spec_validate.py +18 -0
- cu_cli/telemetry.py +42 -0
- cu_cli/update_check.py +154 -0
- cu_cli/update_provider.py +92 -0
- cu_cli/windows_self_upgrade.py +245 -0
- cu_cli-0.1.0b1.dist-info/METADATA +345 -0
- cu_cli-0.1.0b1.dist-info/RECORD +56 -0
- cu_cli-0.1.0b1.dist-info/WHEEL +5 -0
- cu_cli-0.1.0b1.dist-info/entry_points.txt +3 -0
- cu_cli-0.1.0b1.dist-info/top_level.txt +1 -0
cu_cli/errors.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Friendly error handling for CLI commands.
|
|
5
|
+
|
|
6
|
+
``CuCliError`` is a ``ClickException`` that prints a single-line ``error:`` with
|
|
7
|
+
an optional ``hint:`` and honors the exit-code convention in ``exit_codes.py``.
|
|
8
|
+
The ``friendly_errors`` decorator turns raw SDK / network / filesystem
|
|
9
|
+
exceptions into clean, actionable CLI errors.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import functools
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
from cu_cli_core.errors import CuCoreError, UsageError, ValidationError
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
from .exit_codes import GENERIC_ERROR, VALIDATION_FAILURE
|
|
23
|
+
|
|
24
|
+
err_console = Console(stderr=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CuCliError(click.ClickException):
|
|
28
|
+
"""A clean, single-line CLI error with an optional hint."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, hint: Optional[str] = None, exit_code: int = GENERIC_ERROR):
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.hint = hint
|
|
33
|
+
self.exit_code = exit_code
|
|
34
|
+
|
|
35
|
+
def show(self, file=None) -> None: # type: ignore[override]
|
|
36
|
+
err_console.print(f"[bold red]error:[/bold red] {self.message}")
|
|
37
|
+
if self.hint:
|
|
38
|
+
err_console.print(f"[dim]hint:[/dim] {self.hint}")
|
|
39
|
+
|
|
40
|
+
def format_message(self) -> str: # type: ignore[override]
|
|
41
|
+
"""Render the message together with the hint.
|
|
42
|
+
|
|
43
|
+
rich-click renders a ``ClickException`` through ``format_message`` (see
|
|
44
|
+
``write_error``), **not** through our ``show`` override — so a hint
|
|
45
|
+
placed only in ``show`` is silently dropped in the CLI's error panel.
|
|
46
|
+
Embedding it here guarantees the resolution guidance always reaches the
|
|
47
|
+
user (reported previously).
|
|
48
|
+
"""
|
|
49
|
+
if self.hint:
|
|
50
|
+
return f"{self.message}\nhint: {self.hint}"
|
|
51
|
+
return self.message
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _attr(obj: object, key: str) -> object:
|
|
55
|
+
"""Read *key* from a mapping or an object (SDK errors mix both shapes)."""
|
|
56
|
+
if obj is None:
|
|
57
|
+
return None
|
|
58
|
+
if isinstance(obj, dict):
|
|
59
|
+
return obj.get(key)
|
|
60
|
+
return getattr(obj, key, None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _walk_service_error(node: object, lines: list, indent: int) -> None:
|
|
64
|
+
"""Append ``code: message`` lines from an (inner)error node and its details.
|
|
65
|
+
|
|
66
|
+
The CU service nests the actionable failure under ``error.innererror`` (a
|
|
67
|
+
mapping) with an optional ``details`` list; each detail carries its own
|
|
68
|
+
``code``/``message``/``target``. The top-level ``error.message`` is only a
|
|
69
|
+
generic string (e.g. ``"Invalid Request."``), so we surface the nested
|
|
70
|
+
detail instead of dropping it.
|
|
71
|
+
"""
|
|
72
|
+
if node is None:
|
|
73
|
+
return
|
|
74
|
+
code = _attr(node, "code")
|
|
75
|
+
message = _attr(node, "message")
|
|
76
|
+
if code or message:
|
|
77
|
+
pad = " " * indent
|
|
78
|
+
label = f"{code}: " if code else ""
|
|
79
|
+
lines.append(f"{pad}{label}{message or ''}".rstrip())
|
|
80
|
+
details = _attr(node, "details") or []
|
|
81
|
+
if isinstance(details, (list, tuple)):
|
|
82
|
+
for d in details:
|
|
83
|
+
dcode = _attr(d, "code")
|
|
84
|
+
dtarget = _attr(d, "target")
|
|
85
|
+
dmsg = _attr(d, "message")
|
|
86
|
+
pad = " " * (indent + 1)
|
|
87
|
+
loc = f" ({dtarget})" if dtarget else ""
|
|
88
|
+
label = f"{dcode}{loc}: " if dcode else (f"({dtarget}) " if dtarget else "")
|
|
89
|
+
lines.append(f"{pad}{label}{dmsg or ''}".rstrip())
|
|
90
|
+
_walk_service_error(_attr(node, "innererror"), lines, indent + 1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _format_service_error(exc: object) -> str:
|
|
94
|
+
"""Build a multi-line message that surfaces the SDK's inner error details."""
|
|
95
|
+
status = _attr(exc, "status_code") or "?"
|
|
96
|
+
err = _attr(exc, "error")
|
|
97
|
+
top_code = _attr(err, "code")
|
|
98
|
+
top_msg = _attr(err, "message") or _attr(exc, "message") or str(exc)
|
|
99
|
+
head = f"service responded {status}"
|
|
100
|
+
if top_code:
|
|
101
|
+
head += f" ({top_code})"
|
|
102
|
+
lines = [f"{head}: {top_msg}"]
|
|
103
|
+
_walk_service_error(_attr(err, "innererror"), lines, 1)
|
|
104
|
+
top_details = _attr(err, "details") or []
|
|
105
|
+
if isinstance(top_details, (list, tuple)):
|
|
106
|
+
for d in top_details:
|
|
107
|
+
_walk_service_error(d, lines, 1)
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def friendly_errors(fn: Callable) -> Callable:
|
|
112
|
+
"""Decorator: turn SDK / network exceptions into clean CLI errors."""
|
|
113
|
+
|
|
114
|
+
@functools.wraps(fn)
|
|
115
|
+
def wrapper(*args, **kwargs):
|
|
116
|
+
try:
|
|
117
|
+
return fn(*args, **kwargs)
|
|
118
|
+
except CuCoreError as exc:
|
|
119
|
+
exit_code = (
|
|
120
|
+
VALIDATION_FAILURE
|
|
121
|
+
if isinstance(exc, (UsageError, ValidationError))
|
|
122
|
+
else GENERIC_ERROR
|
|
123
|
+
)
|
|
124
|
+
raise CuCliError(exc.message, hint=exc.hint, exit_code=exit_code) from exc
|
|
125
|
+
except CuCliError:
|
|
126
|
+
raise
|
|
127
|
+
except click.ClickException:
|
|
128
|
+
raise
|
|
129
|
+
except click.exceptions.Abort:
|
|
130
|
+
raise
|
|
131
|
+
except KeyboardInterrupt:
|
|
132
|
+
err_console.print("[yellow]aborted[/yellow]")
|
|
133
|
+
sys.exit(130)
|
|
134
|
+
except FileNotFoundError as exc:
|
|
135
|
+
raise CuCliError(
|
|
136
|
+
f"file not found: {exc.filename or exc}",
|
|
137
|
+
hint="check the path; globs that match no files are reported.",
|
|
138
|
+
) from exc
|
|
139
|
+
except PermissionError as exc:
|
|
140
|
+
raise CuCliError(f"permission denied: {exc}") from exc
|
|
141
|
+
except Exception as exc: # noqa: BLE001 — translate to a friendly error
|
|
142
|
+
try:
|
|
143
|
+
from azure.core.exceptions import (
|
|
144
|
+
ClientAuthenticationError,
|
|
145
|
+
HttpResponseError,
|
|
146
|
+
ServiceRequestError,
|
|
147
|
+
)
|
|
148
|
+
except Exception: # pragma: no cover - azure always present at runtime
|
|
149
|
+
ClientAuthenticationError = HttpResponseError = ServiceRequestError = () # type: ignore
|
|
150
|
+
|
|
151
|
+
if isinstance(exc, ClientAuthenticationError):
|
|
152
|
+
raise CuCliError(
|
|
153
|
+
"Authentication failed. Run 'cu doctor' to diagnose, or "
|
|
154
|
+
"'az login' to re-authenticate.",
|
|
155
|
+
) from exc
|
|
156
|
+
if isinstance(exc, HttpResponseError):
|
|
157
|
+
status = getattr(exc, "status_code", None)
|
|
158
|
+
# For client (4xx) errors the surfaced inner detail *is* the
|
|
159
|
+
# actionable guidance; a 'cu doctor' nudge only makes sense for
|
|
160
|
+
# auth/connectivity/service (non-4xx) failures.
|
|
161
|
+
client_error = isinstance(status, int) and 400 <= status < 500
|
|
162
|
+
raise CuCliError(
|
|
163
|
+
_format_service_error(exc),
|
|
164
|
+
hint=None if client_error else
|
|
165
|
+
"run 'cu doctor' to verify endpoint, auth, and model deployments.",
|
|
166
|
+
) from exc
|
|
167
|
+
if isinstance(exc, ServiceRequestError):
|
|
168
|
+
raise CuCliError(
|
|
169
|
+
f"Could not reach the endpoint: {exc}. Check the URL and your "
|
|
170
|
+
"network. 'cu doctor' can verify connectivity.",
|
|
171
|
+
) from exc
|
|
172
|
+
raise CuCliError(f"unexpected error: {exc}") from exc
|
|
173
|
+
|
|
174
|
+
return wrapper
|
cu_cli/exit_codes.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Exit-code convention.
|
|
5
|
+
|
|
6
|
+
0 success
|
|
7
|
+
2 validation failure (so agents can branch deterministically)
|
|
8
|
+
non-zero (1, 3, ...) service / auth / config errors
|
|
9
|
+
|
|
10
|
+
Keeping these as named constants makes the contract explicit at every
|
|
11
|
+
``sys.exit`` / ``ClickException`` site.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
SUCCESS = 0
|
|
17
|
+
GENERIC_ERROR = 1
|
|
18
|
+
VALIDATION_FAILURE = 2
|
|
19
|
+
|
|
20
|
+
__all__ = ["SUCCESS", "GENERIC_ERROR", "VALIDATION_FAILURE"]
|
cu_cli/modality.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Service-aligned file extensions used for safe discovery."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
# Keep this list aligned with:
|
|
9
|
+
# https://learn.microsoft.com/azure/ai-services/content-understanding/service-limits
|
|
10
|
+
DOCUMENT_SAMPLE_EXTS = frozenset({
|
|
11
|
+
".pdf", ".tiff",
|
|
12
|
+
".docx", ".xlsx", ".pptx", ".docm", ".xlsm", ".pptm", ".doc", ".xls", ".ppt",
|
|
13
|
+
".odt", ".ods", ".odp", ".epub",
|
|
14
|
+
".txt", ".html", ".md", ".rtf", ".xml", ".json", ".csv", ".tsv", ".kml", ".eml", ".msg",
|
|
15
|
+
})
|
|
16
|
+
IMAGE_EXTS = frozenset({".jpg", ".jpeg", ".jpe", ".png", ".bmp", ".heif", ".heic"})
|
|
17
|
+
AUDIO_EXTS = frozenset({
|
|
18
|
+
".wav", ".mp3", ".mp4", ".opus", ".ogg", ".flac", ".wma", ".aac", ".webm", ".m4a",
|
|
19
|
+
})
|
|
20
|
+
VIDEO_EXTS = frozenset({".mp4", ".m4v", ".flv", ".wmv", ".asf", ".avi", ".mkv", ".mov"})
|
|
21
|
+
|
|
22
|
+
KNOWN_SERVICE_INPUT_EXTS = (
|
|
23
|
+
DOCUMENT_SAMPLE_EXTS | IMAGE_EXTS | AUDIO_EXTS | VIDEO_EXTS
|
|
24
|
+
)
|
cu_cli/output.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Output helpers: JSON / markdown / pretty tables.
|
|
5
|
+
|
|
6
|
+
``console`` is routed to stderr so the stdout pipe contract for
|
|
7
|
+
``cu analyze ... --json | jq`` is never broken by status output. Data
|
|
8
|
+
payloads use ``sys.stdout.write`` / explicit file writes, or ``result_console``
|
|
9
|
+
for Rich-rendered result content (e.g. ``cu profile list`` / ``cu profile show``)
|
|
10
|
+
that still needs to be redirectable/pipeable via stdout.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Iterable
|
|
20
|
+
|
|
21
|
+
from cu_cli_core.serialization import to_plain_value
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
console = Console(stderr=True)
|
|
26
|
+
result_console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EmptyMarkdownOutputError(RuntimeError):
|
|
30
|
+
"""The service succeeded, but its result has no Markdown projection."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_jsonable(obj: Any) -> Any:
|
|
34
|
+
"""Convert Azure SDK model objects to frontend-neutral plain values."""
|
|
35
|
+
return to_plain_value(obj)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dumps_json(obj: Any) -> str:
|
|
39
|
+
return json.dumps(to_jsonable(obj), indent=2, default=str)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _enum_value(obj: Any) -> Any:
|
|
43
|
+
"""Return an enum's ``.value`` so tables show ``ready`` not ``Status.READY``."""
|
|
44
|
+
return getattr(obj, "value", obj)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _md_cell(value: Any) -> str:
|
|
48
|
+
"""Escape a value for a GitHub-flavored-markdown table cell."""
|
|
49
|
+
text = "" if value is None else str(value)
|
|
50
|
+
return text.replace("|", "\\|").replace("\n", " ").strip()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def markdown_table(rows: "Iterable[Iterable[Any]]", headers: "list[str]") -> str:
|
|
54
|
+
"""Render *rows* as a valid GitHub-flavored-markdown table string."""
|
|
55
|
+
lines = [
|
|
56
|
+
"| " + " | ".join(_md_cell(h) for h in headers) + " |",
|
|
57
|
+
"| " + " | ".join("---" for _ in headers) + " |",
|
|
58
|
+
]
|
|
59
|
+
for row in rows:
|
|
60
|
+
lines.append("| " + " | ".join(_md_cell(c) for c in row) + " |")
|
|
61
|
+
return "\n".join(lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def dump_markdown_kv(pairs: dict, headers: "tuple[str, str]" = ("Key", "Value"),
|
|
65
|
+
out: "Path | None" = None) -> None:
|
|
66
|
+
"""Write a two-column mapping as a real markdown table (stdout by default).
|
|
67
|
+
|
|
68
|
+
Unlike the Rich tables (which go to stderr and aren't valid markdown), this
|
|
69
|
+
goes to stdout so ``... --output markdown >> report.md`` produces valid GFM.
|
|
70
|
+
"""
|
|
71
|
+
body = markdown_table([[k, v] for k, v in pairs.items()], list(headers)) + "\n"
|
|
72
|
+
if out is None:
|
|
73
|
+
sys.stdout.write(body)
|
|
74
|
+
sys.stdout.flush()
|
|
75
|
+
else:
|
|
76
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
out.write_text(body, encoding="utf-8")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def dump_json(
|
|
81
|
+
obj: Any,
|
|
82
|
+
out: "Path | None" = None,
|
|
83
|
+
*,
|
|
84
|
+
overwrite: bool = True,
|
|
85
|
+
) -> None:
|
|
86
|
+
payload = dumps_json(obj)
|
|
87
|
+
if out is None:
|
|
88
|
+
sys.stdout.write(payload)
|
|
89
|
+
if not payload.endswith("\n"):
|
|
90
|
+
sys.stdout.write("\n")
|
|
91
|
+
sys.stdout.flush()
|
|
92
|
+
elif not overwrite:
|
|
93
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
created = False
|
|
95
|
+
try:
|
|
96
|
+
with out.open("x", encoding="utf-8") as stream:
|
|
97
|
+
created = True
|
|
98
|
+
stream.write(payload)
|
|
99
|
+
except OSError:
|
|
100
|
+
if created:
|
|
101
|
+
out.unlink(missing_ok=True)
|
|
102
|
+
raise
|
|
103
|
+
else:
|
|
104
|
+
import tempfile
|
|
105
|
+
|
|
106
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
temporary: Path | None = None
|
|
108
|
+
try:
|
|
109
|
+
with tempfile.NamedTemporaryFile(
|
|
110
|
+
mode="w",
|
|
111
|
+
encoding="utf-8",
|
|
112
|
+
dir=out.parent,
|
|
113
|
+
prefix=f".{out.name}.",
|
|
114
|
+
suffix=".tmp",
|
|
115
|
+
delete=False,
|
|
116
|
+
) as stream:
|
|
117
|
+
stream.write(payload)
|
|
118
|
+
temporary = Path(stream.name)
|
|
119
|
+
os.replace(temporary, out)
|
|
120
|
+
except OSError:
|
|
121
|
+
if temporary is not None:
|
|
122
|
+
temporary.unlink(missing_ok=True)
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def render_markdown(result: Any) -> str:
|
|
127
|
+
"""Render an analysis result as LLM-friendly markdown via SDK helper only."""
|
|
128
|
+
try:
|
|
129
|
+
from azure.ai.contentunderstanding import to_llm_input
|
|
130
|
+
except Exception as exc: # noqa: BLE001
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
"Markdown output requires SDK support for to_llm_input(). "
|
|
133
|
+
"Install azure-ai-contentunderstanding>=1.2.0b3."
|
|
134
|
+
) from exc
|
|
135
|
+
|
|
136
|
+
rendered = to_llm_input(result)
|
|
137
|
+
if not isinstance(rendered, str) or not rendered.strip():
|
|
138
|
+
raise EmptyMarkdownOutputError("to_llm_input() returned empty markdown output.")
|
|
139
|
+
return rendered
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def dump_markdown(result: Any, out: "Path | None" = None) -> None:
|
|
143
|
+
body = render_markdown(result)
|
|
144
|
+
if out is None:
|
|
145
|
+
sys.stdout.write(body)
|
|
146
|
+
if not body.endswith("\n"):
|
|
147
|
+
sys.stdout.write("\n")
|
|
148
|
+
sys.stdout.flush()
|
|
149
|
+
else:
|
|
150
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
out.write_text(body, encoding="utf-8")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def analyzer_table(rows: Iterable[Any]) -> Table:
|
|
155
|
+
t = Table(title="Analyzers", show_lines=False)
|
|
156
|
+
t.add_column("Analyzer ID", style="bold", overflow="fold", max_width=36)
|
|
157
|
+
t.add_column("Base")
|
|
158
|
+
t.add_column("Status")
|
|
159
|
+
t.add_column("Modified")
|
|
160
|
+
t.add_column("Description")
|
|
161
|
+
for a in rows:
|
|
162
|
+
t.add_row(
|
|
163
|
+
str(getattr(a, "analyzer_id", "") or ""),
|
|
164
|
+
str(getattr(a, "base_analyzer_id", "") or "—"),
|
|
165
|
+
str(_enum_value(getattr(a, "status", "")) or "—"),
|
|
166
|
+
str(getattr(a, "last_modified_at", "") or "—"),
|
|
167
|
+
(str(getattr(a, "description", "") or "")[:60]),
|
|
168
|
+
)
|
|
169
|
+
return t
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def kv_table(pairs: dict, title: str = "") -> Table:
|
|
173
|
+
t = Table(title=title or None, title_justify="left", show_header=False, box=None)
|
|
174
|
+
t.add_column("key", style="dim")
|
|
175
|
+
t.add_column("value")
|
|
176
|
+
for k, v in pairs.items():
|
|
177
|
+
t.add_row(str(k), str(v) if not isinstance(v, dict)
|
|
178
|
+
else json.dumps(v, indent=2, default=str))
|
|
179
|
+
return t
|
cu_cli/profile.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Standalone access to the shared CU profile implementation."""
|
|
5
|
+
|
|
6
|
+
from cu_cli_core.profiles import (
|
|
7
|
+
DEFAULT_PROFILE_NAME,
|
|
8
|
+
KNOWN_PROFILE_KEYS,
|
|
9
|
+
Profile,
|
|
10
|
+
ProfileStore,
|
|
11
|
+
azure_config_path,
|
|
12
|
+
is_valid_profile_name,
|
|
13
|
+
normalize_profile_name,
|
|
14
|
+
validate_profile_key,
|
|
15
|
+
validate_profile_name,
|
|
16
|
+
validate_profile_value,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DEFAULT_PROFILE_NAME",
|
|
21
|
+
"KNOWN_PROFILE_KEYS",
|
|
22
|
+
"Profile",
|
|
23
|
+
"ProfileStore",
|
|
24
|
+
"azure_config_path",
|
|
25
|
+
"is_valid_profile_name",
|
|
26
|
+
"normalize_profile_name",
|
|
27
|
+
"validate_profile_key",
|
|
28
|
+
"validate_profile_name",
|
|
29
|
+
"validate_profile_value",
|
|
30
|
+
]
|
cu_cli/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Azure Content Understanding starter (azd template)
|
|
2
|
+
|
|
3
|
+
Provision a Microsoft Foundry resource and project, configure a local CU CLI
|
|
4
|
+
profile, and optionally deploy selected supported large language models (LLMs)
|
|
5
|
+
and embeddings models
|
|
6
|
+
supported by the selected Content Understanding API version. The model
|
|
7
|
+
deployments enable prebuilt analyzers such as `prebuilt-invoice` and custom
|
|
8
|
+
analyzers. The template also configures Content Understanding defaults that map
|
|
9
|
+
model names to deployment names. It is purpose-built as a launchpad for
|
|
10
|
+
**Azure Content Understanding in Foundry Tools** workflows and the
|
|
11
|
+
[`cu` CLI](https://github.com/Azure/content-understanding-toolkit/tree/main/cu-cli).
|
|
12
|
+
```sh
|
|
13
|
+
azd init --template kmuthukrishn/content-understanding-starter
|
|
14
|
+
azd env new dev
|
|
15
|
+
azd env set AZURE_LOCATION eastus2
|
|
16
|
+
azd up
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That's it. No app code, no Container Apps, no AI Search — just the bits you
|
|
20
|
+
need to start authoring analyzers.
|
|
21
|
+
|
|
22
|
+
## Permissions required for `azd up`
|
|
23
|
+
|
|
24
|
+
This template deploys at subscription scope. For the complete new-resource
|
|
25
|
+
path, the identity running `azd up` needs **Contributor** or **Owner** on the
|
|
26
|
+
selected subscription, or a custom role with equivalent permissions. This
|
|
27
|
+
allows the deployment to create the resource group, Foundry resource and
|
|
28
|
+
project, and selected model deployments.
|
|
29
|
+
|
|
30
|
+
Contributor cannot create Azure role assignments. If the deployment should
|
|
31
|
+
also assign the generated data-plane role, the identity additionally needs
|
|
32
|
+
**Role Based Access Control Administrator**, **User Access Administrator**, or
|
|
33
|
+
**Owner** on the subscription. If you have Contributor only, set
|
|
34
|
+
`AZURE_ASSIGN_ROLES` to `false`; the post-provision hook uses resource-key
|
|
35
|
+
authentication instead.
|
|
36
|
+
|
|
37
|
+
With Entra authentication, CU CLI also requires **Cognitive Services User** on
|
|
38
|
+
the Microsoft Foundry resource to configure defaults and create, manage, and run
|
|
39
|
+
analyzers. This is data-plane access and is not included in Owner or
|
|
40
|
+
Contributor.
|
|
41
|
+
|
|
42
|
+
## What gets provisioned
|
|
43
|
+
|
|
44
|
+
| Resource | Purpose |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| Resource group `rg-<env>` | Container for everything |
|
|
47
|
+
| `Microsoft.CognitiveServices/accounts` (kind `AIServices`) | Microsoft Foundry resource that exposes Content Understanding and other Foundry Tools through one endpoint |
|
|
48
|
+
| `Microsoft.CognitiveServices/accounts/projects` | Foundry project (defaults to `proj-<env>`) |
|
|
49
|
+
| `Microsoft.CognitiveServices/accounts/deployments` | Models selected from the live Content Understanding and Microsoft Foundry resource catalogs, or none |
|
|
50
|
+
| Role assignment on the calling user | `Cognitive Services User` — permits CU CLI to configure defaults and create, manage, and run analyzers with Entra authentication |
|
|
51
|
+
|
|
52
|
+
The resource endpoint is `https://<resource-name>.services.ai.azure.com/` — the same
|
|
53
|
+
host that serves `/contentunderstanding/...`, `/openai/...`, and the Foundry
|
|
54
|
+
project API.
|
|
55
|
+
|
|
56
|
+
If `FOUNDRY_EXISTING_ENDPOINT` is set, this template skips resource and project
|
|
57
|
+
creation and performs the same live discovery against that existing resource.
|
|
58
|
+
|
|
59
|
+
## Microsoft Foundry resource naming
|
|
60
|
+
|
|
61
|
+
The Microsoft Foundry resource name must be globally unique because it becomes part of a
|
|
62
|
+
public DNS hostname:
|
|
63
|
+
|
|
64
|
+
`https://<resource-name>.services.ai.azure.com/`
|
|
65
|
+
|
|
66
|
+
By default, this template uses `aif-<unique-suffix>`. To make the name more
|
|
67
|
+
meaningful, set an optional prefix and azd will construct:
|
|
68
|
+
|
|
69
|
+
`<prefix>-<unique-suffix>`
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
azd env set FOUNDRY_RESOURCE_PREFIX yslincu
|
|
75
|
+
azd up
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This produces resource names like `yslincu-xbrzt4yrmiexg`.
|
|
79
|
+
|
|
80
|
+
## Live model setup
|
|
81
|
+
|
|
82
|
+
The first infrastructure deployment intentionally creates the Foundry resource
|
|
83
|
+
with an empty `infra/models.json`. The post-provision hook then:
|
|
84
|
+
|
|
85
|
+
1. Calls `GET prebuilt-document` with `CU_API_VERSION`.
|
|
86
|
+
2. Reads `supportedModels.completion` and `supportedModels.embedding`.
|
|
87
|
+
3. Calls `az cognitiveservices account list-models` for the provisioned resource.
|
|
88
|
+
4. Shows only model versions present in both live catalogs.
|
|
89
|
+
5. Deploys the selection and writes it to `infra/models.json`, making subsequent
|
|
90
|
+
Bicep runs repeatable.
|
|
91
|
+
|
|
92
|
+
Choose `0` to deploy no models. In that mode, `prebuilt-digitalParse`,
|
|
93
|
+
`prebuilt-read`, and `prebuilt-layout` are available without language or
|
|
94
|
+
embeddings model deployments.
|
|
95
|
+
Set `CU_MODEL_SELECTION=recommended` for noninteractive selection or
|
|
96
|
+
`CU_MODEL_SELECTION=none` for deterministic setup without model deployments.
|
|
97
|
+
|
|
98
|
+
The generated hook runs `cu _infra-models`, so keep the `cu` CLI installed and
|
|
99
|
+
on `PATH` when running `azd up` (`cu-cli` on macOS). A saved `prompt` selection
|
|
100
|
+
requires an interactive terminal; set `CU_MODEL_SELECTION=recommended`, `none`,
|
|
101
|
+
or explicit `model@version` selectors for unattended runs. Model setup failures
|
|
102
|
+
are reported without blocking CU CLI profile configuration. Resolve the model
|
|
103
|
+
deployment issue and rerun `azd up`; `prebuilt-digitalParse`, `prebuilt-read`,
|
|
104
|
+
and `prebuilt-layout` remain available without language or embeddings models.
|
|
105
|
+
|
|
106
|
+
## Auto-configuring the CU CLI profile
|
|
107
|
+
|
|
108
|
+
After live model setup, the post-provision hook automatically runs:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
cu profile set endpoint <FOUNDRY_ENDPOINT>
|
|
112
|
+
cu profile set default_analyzer prebuilt-layout
|
|
113
|
+
cu doctor
|
|
114
|
+
|
|
115
|
+
# safe first sanity check (no language or embeddings model required):
|
|
116
|
+
cu analyze <file> --analyzer prebuilt-layout
|
|
117
|
+
|
|
118
|
+
# When model deployments and Content Understanding defaults are ready:
|
|
119
|
+
cu analyzer schema create --from-sample <file> --output-file schema.json
|
|
120
|
+
cu analyzer create --name my-analyzer --schema schema.json
|
|
121
|
+
cu analyze <file> --analyzer my-analyzer
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
so the freshly provisioned resource is saved in the default CU CLI profile
|
|
125
|
+
immediately after `azd up`, with `prebuilt-layout` configured as the default
|
|
126
|
+
analyzer. The hook prints the redacted `cu profile show --name default` result.
|
|
127
|
+
If the default profile already has saved values, the hook preserves it and
|
|
128
|
+
explains how to rerun `cu infra generate --force` before `azd up` to replace values.
|
|
129
|
+
|
|
130
|
+
The post-provision hook only prints the custom-analyzer workflow after it
|
|
131
|
+
verifies succeeded chat completion and embeddings model deployments and
|
|
132
|
+
configures Content Understanding defaults. Otherwise it prints the specific
|
|
133
|
+
repair action and retains the `prebuilt-layout` sanity check.
|
|
134
|
+
|
|
135
|
+
On macOS, use `cu-cli` in place of `cu` (macOS ships a built-in `cu` command).
|
|
136
|
+
|
|
137
|
+
Profile setup is automatic and requires no environment variable. To opt out,
|
|
138
|
+
set `CU_DISABLE_AUTO_PROFILE_SETUP=true` before `azd up`. Generated templates
|
|
139
|
+
also honor the legacy `CU_AUTOCONFIG=false` setting. This does not disable live
|
|
140
|
+
model setup; use `azd env set CU_MODEL_SELECTION none` to skip model deployments.
|
|
141
|
+
|
|
142
|
+
## Cleanup
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
azd down --purge
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The `--purge` flag is important — Microsoft Foundry resource names are
|
|
149
|
+
soft-deleted for 48h after `azd down`, and a follow-up `azd up` in the same
|
|
150
|
+
environment would otherwise fail.
|
|
151
|
+
|
|
152
|
+
## Outputs
|
|
153
|
+
|
|
154
|
+
After `azd up`, `azd env get-values` exposes:
|
|
155
|
+
|
|
156
|
+
| Variable | Notes |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `FOUNDRY_ENDPOINT` | `https://<resource-name>.services.ai.azure.com/` |
|
|
159
|
+
| `FOUNDRY_EXISTING_ENDPOINT` | Optional existing endpoint to reuse instead of provisioning a new resource |
|
|
160
|
+
| `FOUNDRY_EXISTING_RESOURCE_GROUP` | Resource group for `FOUNDRY_EXISTING_ENDPOINT` |
|
|
161
|
+
| `FOUNDRY_PROJECT_ENDPOINT` | Project-scoped URL (`/api/projects/<project>`) |
|
|
162
|
+
| `FOUNDRY_RESOURCE_NAME` | Account name (also the custom subdomain) |
|
|
163
|
+
| `FOUNDRY_PROJECT_NAME` | Project name |
|
|
164
|
+
| `CU_ENDPOINT` | Alias of `FOUNDRY_ENDPOINT` for clarity |
|
|
165
|
+
| `MODEL_DEPLOYMENTS` | JSON array describing each deployment |
|
|
166
|
+
| `CU_API_VERSION` | API version used for live `prebuilt-document` model discovery |
|
|
167
|
+
| `CU_MODEL_SELECTION` | `prompt`, `recommended`, `none`, or explicit model selectors |
|
|
168
|
+
| `CU_MODEL_SETUP_COMPLETE` | Prevents repeated prompting after successful setup |
|
|
169
|
+
|
|
170
|
+
## Layout
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
.
|
|
174
|
+
├── azure.yaml # azd project manifest + postprovision hook wiring
|
|
175
|
+
├── infra/
|
|
176
|
+
│ ├── main.bicep # subscription-scope entrypoint
|
|
177
|
+
│ ├── main.parameters.json # azd → Bicep parameter glue
|
|
178
|
+
│ └── modules/
|
|
179
|
+
│ └── foundry.bicep # account + project + persisted model deployments + RBAC
|
|
180
|
+
└── hooks/
|
|
181
|
+
├── postprovision.ps1 # Windows live model setup + cu autoconfig
|
|
182
|
+
└── postprovision.sh # POSIX live model setup + cu autoconfig
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Status
|
|
186
|
+
|
|
187
|
+
Experimental / personal starter. Not yet on awesome-azd.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
|
|
2
|
+
|
|
3
|
+
name: content-understanding-starter
|
|
4
|
+
metadata:
|
|
5
|
+
template: content-understanding-starter@0.1.0
|
|
6
|
+
|
|
7
|
+
infra:
|
|
8
|
+
provider: bicep
|
|
9
|
+
path: infra
|
|
10
|
+
module: main
|
|
11
|
+
|
|
12
|
+
# No `services:` block on purpose — this template is infra-only.
|
|
13
|
+
# The companion CLI (`cu`) is the developer surface; provisioning the Foundry
|
|
14
|
+
# account + project + a model deployment is all this template does.
|
|
15
|
+
|
|
16
|
+
hooks:
|
|
17
|
+
postprovision:
|
|
18
|
+
posix:
|
|
19
|
+
shell: sh
|
|
20
|
+
run: ./hooks/postprovision.sh
|
|
21
|
+
continueOnError: false
|
|
22
|
+
interactive: true
|
|
23
|
+
windows:
|
|
24
|
+
shell: pwsh
|
|
25
|
+
run: ./hooks/postprovision.ps1
|
|
26
|
+
continueOnError: false
|
|
27
|
+
interactive: true
|