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.
Files changed (56) hide show
  1. cu_cli/__init__.py +17 -0
  2. cu_cli/__main__.py +11 -0
  3. cu_cli/apiversion.py +124 -0
  4. cu_cli/cli.py +138 -0
  5. cu_cli/client.py +138 -0
  6. cu_cli/commands/__init__.py +4 -0
  7. cu_cli/commands/_command_spec.py +94 -0
  8. cu_cli/commands/_help.py +33 -0
  9. cu_cli/commands/_infra_models.py +184 -0
  10. cu_cli/commands/_infra_wizard.py +630 -0
  11. cu_cli/commands/_model_setup.py +46 -0
  12. cu_cli/commands/_options.py +112 -0
  13. cu_cli/commands/analyze.py +631 -0
  14. cu_cli/commands/analyzer.py +1462 -0
  15. cu_cli/commands/defaults.py +172 -0
  16. cu_cli/commands/doctor.py +166 -0
  17. cu_cli/commands/env_var.py +67 -0
  18. cu_cli/commands/infra.py +302 -0
  19. cu_cli/commands/profile_cmd.py +525 -0
  20. cu_cli/commands/upgrade.py +120 -0
  21. cu_cli/core/__init__.py +17 -0
  22. cu_cli/core/analyze.py +44 -0
  23. cu_cli/core/analyzers.py +30 -0
  24. cu_cli/core/azure_resources.py +486 -0
  25. cu_cli/core/defaults.py +18 -0
  26. cu_cli/core/doctor.py +42 -0
  27. cu_cli/core/foundry.py +68 -0
  28. cu_cli/core/infra_models.py +367 -0
  29. cu_cli/core/inputs.py +209 -0
  30. cu_cli/core/schema.py +24 -0
  31. cu_cli/errors.py +174 -0
  32. cu_cli/exit_codes.py +20 -0
  33. cu_cli/modality.py +24 -0
  34. cu_cli/output.py +179 -0
  35. cu_cli/profile.py +30 -0
  36. cu_cli/py.typed +0 -0
  37. cu_cli/resources/__init__.py +4 -0
  38. cu_cli/resources/azd_template/README.md +187 -0
  39. cu_cli/resources/azd_template/azure.yaml +27 -0
  40. cu_cli/resources/azd_template/hooks/postprovision.ps1 +320 -0
  41. cu_cli/resources/azd_template/hooks/postprovision.sh +299 -0
  42. cu_cli/resources/azd_template/infra/main.bicep +115 -0
  43. cu_cli/resources/azd_template/infra/main.parameters.json +30 -0
  44. cu_cli/resources/azd_template/infra/models.json +1 -0
  45. cu_cli/resources/azd_template/infra/modules/foundry.bicep +122 -0
  46. cu_cli/schema_validate.py +28 -0
  47. cu_cli/spec_validate.py +18 -0
  48. cu_cli/telemetry.py +42 -0
  49. cu_cli/update_check.py +154 -0
  50. cu_cli/update_provider.py +92 -0
  51. cu_cli/windows_self_upgrade.py +245 -0
  52. cu_cli-0.1.0b1.dist-info/METADATA +345 -0
  53. cu_cli-0.1.0b1.dist-info/RECORD +56 -0
  54. cu_cli-0.1.0b1.dist-info/WHEEL +5 -0
  55. cu_cli-0.1.0b1.dist-info/entry_points.txt +3 -0
  56. 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,4 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Bundled azd/Bicep resources emitted by ``cu infra generate``."""
@@ -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