cu-cli-core 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_core/__init__.py +12 -0
- cu_cli_core/analysis.py +374 -0
- cu_cli_core/client.py +39 -0
- cu_cli_core/command_spec.py +1099 -0
- cu_cli_core/contracts.py +404 -0
- cu_cli_core/defaults.py +150 -0
- cu_cli_core/environment.py +102 -0
- cu_cli_core/errors.py +77 -0
- cu_cli_core/input_planning.py +322 -0
- cu_cli_core/operations/__init__.py +4 -0
- cu_cli_core/operations/analysis.py +294 -0
- cu_cli_core/operations/analyzer_copy.py +327 -0
- cu_cli_core/operations/analyzers.py +85 -0
- cu_cli_core/operations/profiles.py +146 -0
- cu_cli_core/operations/schema.py +48 -0
- cu_cli_core/operations/validation.py +41 -0
- cu_cli_core/profiles.py +604 -0
- cu_cli_core/py.typed +0 -0
- cu_cli_core/resources/openapi/2025-11-01/ContentUnderstanding.json +3643 -0
- cu_cli_core/resources/openapi/2026-06-01-preview/ContentUnderstanding.json +4050 -0
- cu_cli_core/schema.py +389 -0
- cu_cli_core/schema_validation.py +312 -0
- cu_cli_core/serialization.py +75 -0
- cu_cli_core/service_options.py +71 -0
- cu_cli_core/spec_validation.py +141 -0
- cu_cli_core-0.1.0b1.dist-info/METADATA +26 -0
- cu_cli_core-0.1.0b1.dist-info/RECORD +29 -0
- cu_cli_core-0.1.0b1.dist-info/WHEEL +5 -0
- cu_cli_core-0.1.0b1.dist-info/top_level.txt +1 -0
cu_cli_core/schema.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Custom-analyzer schema authoring (Click-free).
|
|
5
|
+
|
|
6
|
+
Pure helpers to generate a starter analyzer schema and to pick a template
|
|
7
|
+
completion model, plus a client-injected ``suggest_schema_from_sample`` that
|
|
8
|
+
derives a field schema from one local document via ``prebuilt-documentFieldSchema``.
|
|
9
|
+
|
|
10
|
+
Nothing here prints or resolves auth — callers pass a built client and the
|
|
11
|
+
resolved completion model; the command layer handles config, client
|
|
12
|
+
construction, and any user-facing warnings.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import mimetypes
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Mapping, Protocol
|
|
20
|
+
|
|
21
|
+
from .defaults import COMPLETION_MODEL_PREFERENCE, PREFERRED_EMBEDDING_MODEL
|
|
22
|
+
from .errors import ValidationError
|
|
23
|
+
DOCUMENT_SAMPLE_EXTS = frozenset(
|
|
24
|
+
{
|
|
25
|
+
".pdf",
|
|
26
|
+
".tiff",
|
|
27
|
+
".docx",
|
|
28
|
+
".xlsx",
|
|
29
|
+
".pptx",
|
|
30
|
+
".docm",
|
|
31
|
+
".xlsm",
|
|
32
|
+
".pptm",
|
|
33
|
+
".doc",
|
|
34
|
+
".xls",
|
|
35
|
+
".ppt",
|
|
36
|
+
".odt",
|
|
37
|
+
".ods",
|
|
38
|
+
".odp",
|
|
39
|
+
".epub",
|
|
40
|
+
".txt",
|
|
41
|
+
".html",
|
|
42
|
+
".md",
|
|
43
|
+
".rtf",
|
|
44
|
+
".xml",
|
|
45
|
+
".json",
|
|
46
|
+
".csv",
|
|
47
|
+
".tsv",
|
|
48
|
+
".kml",
|
|
49
|
+
".eml",
|
|
50
|
+
".msg",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
MODALITY_BASE: dict[str, str] = {
|
|
55
|
+
"document": "prebuilt-document",
|
|
56
|
+
"image": "prebuilt-image",
|
|
57
|
+
"audio": "prebuilt-audio",
|
|
58
|
+
"video": "prebuilt-video",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
FIELD_SCHEMA_SUGGEST_ANALYZER_ID = "prebuilt-documentFieldSchema"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _ModelDeploymentConfig(Protocol):
|
|
65
|
+
@property
|
|
66
|
+
def model_deployments(self) -> Mapping[str, str]: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def starter_schema(
|
|
70
|
+
analyzer_id: str,
|
|
71
|
+
base: str,
|
|
72
|
+
modality: str,
|
|
73
|
+
api_version: str,
|
|
74
|
+
*,
|
|
75
|
+
completion_model: str,
|
|
76
|
+
template_type: str,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""A minimal, valid analyzer schema for an agent/human to fill in.
|
|
79
|
+
|
|
80
|
+
The resolved ``apiVersion`` is stamped so authored schemas are
|
|
81
|
+
self-describing.
|
|
82
|
+
"""
|
|
83
|
+
extraction_fields = {
|
|
84
|
+
"example_string_field": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"method": "extract",
|
|
87
|
+
"description": (
|
|
88
|
+
"TODO: replace this field. Describe specifically what to extract, "
|
|
89
|
+
"where it appears in the document, and any formatting expectations. "
|
|
90
|
+
"Example: 'Full legal name of the vendor as printed in the invoice header.'"
|
|
91
|
+
),
|
|
92
|
+
"estimateSourceAndConfidence": True,
|
|
93
|
+
},
|
|
94
|
+
"example_number_field": {
|
|
95
|
+
"type": "number",
|
|
96
|
+
"method": "extract",
|
|
97
|
+
"description": (
|
|
98
|
+
"TODO: replace this field. Use for amounts, totals, rates, or "
|
|
99
|
+
"other numeric values. Example: 'Invoice total amount before tax.'"
|
|
100
|
+
),
|
|
101
|
+
"estimateSourceAndConfidence": True,
|
|
102
|
+
},
|
|
103
|
+
"example_summary": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"method": "generate",
|
|
106
|
+
"description": (
|
|
107
|
+
"Provide a one-line summary of the document's main purpose and "
|
|
108
|
+
"key outcome. Keep it concise and factual."
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
"example_classify_field": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"method": "classify",
|
|
114
|
+
"description": (
|
|
115
|
+
"TODO: replace this field. Use `classify` only when the value comes "
|
|
116
|
+
"from a closed set. Always include `enum`."
|
|
117
|
+
),
|
|
118
|
+
"enum": ["option_a", "option_b", "other"],
|
|
119
|
+
"enumDescriptions": {
|
|
120
|
+
"option_a": "Primary category when the content strongly matches pattern A.",
|
|
121
|
+
"option_b": "Secondary category when the content matches pattern B.",
|
|
122
|
+
"other": "Fallback category when neither option_a nor option_b applies.",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"example_table_field": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"method": "extract",
|
|
128
|
+
"description": (
|
|
129
|
+
"TODO: replace this field. Use for repeating table rows such as "
|
|
130
|
+
"line items, transactions, or schedule entries."
|
|
131
|
+
),
|
|
132
|
+
"items": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"description": "One extracted table row.",
|
|
135
|
+
"properties": {
|
|
136
|
+
"column_description": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
"method": "extract",
|
|
139
|
+
"description": "Text content for the description column.",
|
|
140
|
+
},
|
|
141
|
+
"column_amount": {
|
|
142
|
+
"type": "number",
|
|
143
|
+
"method": "extract",
|
|
144
|
+
"description": "Numeric value in the amount column.",
|
|
145
|
+
},
|
|
146
|
+
"column_category": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"method": "classify",
|
|
149
|
+
"description": (
|
|
150
|
+
"Classify the column value into a stable category used by "
|
|
151
|
+
"downstream business logic."
|
|
152
|
+
),
|
|
153
|
+
"enum": ["product", "service", "fee", "tax", "other"],
|
|
154
|
+
"enumDescriptions": {
|
|
155
|
+
"product": "Physical good or inventory item.",
|
|
156
|
+
"service": "Labor or service charge.",
|
|
157
|
+
"fee": "Non-tax fee such as handling or processing.",
|
|
158
|
+
"tax": "Tax line item.",
|
|
159
|
+
"other": "Row does not fit the predefined categories.",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Categories are description-only by default so the emitted template can be
|
|
168
|
+
# created immediately. Routing a category to another analyzer is optional and
|
|
169
|
+
# requires that analyzer (prebuilt or custom) to already exist — otherwise the
|
|
170
|
+
# service rejects create with InvalidAnalyzerId. See the description below.
|
|
171
|
+
classification_categories = {
|
|
172
|
+
"invoice": {
|
|
173
|
+
"description": (
|
|
174
|
+
"Vendor invoices requesting payment for goods or services, typically "
|
|
175
|
+
"including vendor details, line items, totals, and payment terms."
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
"purchase_order": {
|
|
179
|
+
"description": (
|
|
180
|
+
"Purchase orders that authorize a purchase, typically including a PO "
|
|
181
|
+
"number, buyer/supplier details, ordered items, quantities, and prices."
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
"receipt": {
|
|
185
|
+
"description": (
|
|
186
|
+
"Retail or expense receipts confirming a completed payment, typically "
|
|
187
|
+
"including merchant, date, purchased items, and total paid."
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
"other": {
|
|
191
|
+
"description": (
|
|
192
|
+
"Fallback category for content that does not match any category above."
|
|
193
|
+
),
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if template_type == "classification":
|
|
198
|
+
return {
|
|
199
|
+
"apiVersion": api_version,
|
|
200
|
+
"analyzerId": analyzer_id,
|
|
201
|
+
"description": (
|
|
202
|
+
"TODO: classify inputs into the categories below; give each a clear "
|
|
203
|
+
"description. Optional: add \"analyzerId\": \"<existing-analyzer-id>\" to a "
|
|
204
|
+
"category to route matching content to that analyzer for extraction "
|
|
205
|
+
"(the analyzer — prebuilt like prebuilt-invoice or a custom one — must "
|
|
206
|
+
"already exist, otherwise create fails with InvalidAnalyzerId)."
|
|
207
|
+
),
|
|
208
|
+
"baseAnalyzerId": base,
|
|
209
|
+
"config": {
|
|
210
|
+
"estimateFieldSourceAndConfidence": True,
|
|
211
|
+
"enableSegment": True,
|
|
212
|
+
"contentCategories": classification_categories,
|
|
213
|
+
},
|
|
214
|
+
"models": {"completion": completion_model, "embedding": PREFERRED_EMBEDDING_MODEL},
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"apiVersion": api_version,
|
|
219
|
+
"analyzerId": analyzer_id,
|
|
220
|
+
"description": f"TODO: one-sentence description of what this {modality} analyzer extracts.",
|
|
221
|
+
"baseAnalyzerId": base,
|
|
222
|
+
"fieldSchema": {
|
|
223
|
+
"name": f"{analyzer_id.replace('-', '_')}_schema",
|
|
224
|
+
"description": "TODO: one-sentence summary of the extraction.",
|
|
225
|
+
"fields": extraction_fields,
|
|
226
|
+
},
|
|
227
|
+
"config": {"estimateFieldSourceAndConfidence": True},
|
|
228
|
+
"models": {"completion": completion_model, "embedding": PREFERRED_EMBEDDING_MODEL},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def suggested_fields_from_result(result: Any) -> dict[str, Any]:
|
|
233
|
+
"""Extract the ``schema.valueJson`` field map from a suggestion result."""
|
|
234
|
+
contents = getattr(result, "contents", None) or []
|
|
235
|
+
if not contents:
|
|
236
|
+
return {}
|
|
237
|
+
for content in contents:
|
|
238
|
+
fields = getattr(content, "fields", None)
|
|
239
|
+
if fields is None and isinstance(content, dict):
|
|
240
|
+
fields = content.get("fields")
|
|
241
|
+
if not isinstance(fields, dict):
|
|
242
|
+
if fields is not None and hasattr(fields, "as_dict"):
|
|
243
|
+
fields = fields.as_dict()
|
|
244
|
+
else:
|
|
245
|
+
continue
|
|
246
|
+
schema_field = fields.get("schema")
|
|
247
|
+
if schema_field is None:
|
|
248
|
+
continue
|
|
249
|
+
if hasattr(schema_field, "as_dict"):
|
|
250
|
+
schema_field = schema_field.as_dict()
|
|
251
|
+
if not isinstance(schema_field, dict):
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
value_json = schema_field.get("valueJson")
|
|
255
|
+
if isinstance(value_json, dict) and value_json:
|
|
256
|
+
return value_json
|
|
257
|
+
return {}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _add_placeholder_descriptions(fields: dict[str, Any]) -> None:
|
|
261
|
+
"""Fill description gaps in service-suggested field definitions."""
|
|
262
|
+
|
|
263
|
+
def visit(definition: Any, placeholder: str) -> None:
|
|
264
|
+
if not isinstance(definition, dict):
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
description = definition.get("description")
|
|
268
|
+
if description is None or (isinstance(description, str) and not description.strip()):
|
|
269
|
+
definition["description"] = placeholder
|
|
270
|
+
|
|
271
|
+
properties = definition.get("properties")
|
|
272
|
+
if isinstance(properties, dict):
|
|
273
|
+
for property_name, property_definition in properties.items():
|
|
274
|
+
visit(
|
|
275
|
+
property_definition,
|
|
276
|
+
f"TODO: describe the '{property_name}' field.",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
items = definition.get("items")
|
|
280
|
+
if isinstance(items, dict):
|
|
281
|
+
visit(
|
|
282
|
+
items,
|
|
283
|
+
"TODO: describe one item in this array.",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
for field_name, field_definition in fields.items():
|
|
287
|
+
visit(
|
|
288
|
+
field_definition,
|
|
289
|
+
f"TODO: describe the '{field_name}' field.",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def template_completion_model(cfg: _ModelDeploymentConfig) -> str:
|
|
294
|
+
"""Pick a completion model to stamp into a generated schema template."""
|
|
295
|
+
for model in COMPLETION_MODEL_PREFERENCE:
|
|
296
|
+
if model in cfg.model_deployments:
|
|
297
|
+
return model
|
|
298
|
+
|
|
299
|
+
for model in cfg.model_deployments:
|
|
300
|
+
if model.startswith("prebuilt-analyzer-"):
|
|
301
|
+
continue
|
|
302
|
+
if model == PREFERRED_EMBEDDING_MODEL or model.endswith("-mini"):
|
|
303
|
+
continue
|
|
304
|
+
return model
|
|
305
|
+
|
|
306
|
+
# Default completion model when none is configured (recommended: gpt-5.2).
|
|
307
|
+
return COMPLETION_MODEL_PREFERENCE[0]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def validate_document_sample(sample_path: Path) -> None:
|
|
311
|
+
"""Raise a validation error if *sample_path* is unusable."""
|
|
312
|
+
if not sample_path.exists() or not sample_path.is_file():
|
|
313
|
+
raise ValidationError(f"sample file not found: {sample_path}")
|
|
314
|
+
if sample_path.suffix.lower() not in DOCUMENT_SAMPLE_EXTS:
|
|
315
|
+
raise ValidationError(
|
|
316
|
+
f"--from-sample expects a document file, got '{sample_path.suffix}'.",
|
|
317
|
+
hint="supported document formats include pdf/docx/pptx/xlsx/txt/html.",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def suggest_schema_from_sample(
|
|
322
|
+
client: Any,
|
|
323
|
+
*,
|
|
324
|
+
sample_path: Path,
|
|
325
|
+
analyzer_id: str,
|
|
326
|
+
api_version: str,
|
|
327
|
+
completion_model: str,
|
|
328
|
+
) -> tuple[dict[str, Any], bool]:
|
|
329
|
+
"""Suggest an extraction schema from one document sample via *client*.
|
|
330
|
+
|
|
331
|
+
Returns ``(payload, found_fields)`` where ``found_fields`` is ``True`` when
|
|
332
|
+
the service returned a non-empty field schema (otherwise ``payload`` is the
|
|
333
|
+
default extraction template). MVP behavior intentionally supports exactly
|
|
334
|
+
one local document sample.
|
|
335
|
+
"""
|
|
336
|
+
validate_document_sample(sample_path)
|
|
337
|
+
|
|
338
|
+
payload = starter_schema(
|
|
339
|
+
analyzer_id,
|
|
340
|
+
base=MODALITY_BASE["document"],
|
|
341
|
+
modality="document",
|
|
342
|
+
api_version=api_version,
|
|
343
|
+
completion_model=completion_model,
|
|
344
|
+
template_type="extraction",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
sample_bytes = sample_path.read_bytes()
|
|
348
|
+
sample_mime = mimetypes.guess_type(sample_path.name)[0] or "application/octet-stream"
|
|
349
|
+
|
|
350
|
+
input_payload: Any
|
|
351
|
+
try:
|
|
352
|
+
from azure.ai.contentunderstanding import models as _cu_models
|
|
353
|
+
|
|
354
|
+
input_cls = getattr(_cu_models, "AnalyzeInput", None) or getattr(
|
|
355
|
+
_cu_models, "AnalysisInput", None
|
|
356
|
+
)
|
|
357
|
+
if input_cls is None:
|
|
358
|
+
raise AttributeError("AnalyzeInput/AnalysisInput model is not available")
|
|
359
|
+
|
|
360
|
+
input_payload = input_cls(
|
|
361
|
+
name=sample_path.name,
|
|
362
|
+
mime_type=sample_mime,
|
|
363
|
+
data=sample_bytes,
|
|
364
|
+
)
|
|
365
|
+
except (ImportError, AttributeError, TypeError):
|
|
366
|
+
# SDK compatibility: older wheels may not expose AnalyzeInput.
|
|
367
|
+
input_payload = {
|
|
368
|
+
"name": sample_path.name,
|
|
369
|
+
"mime_type": sample_mime,
|
|
370
|
+
"data": sample_bytes,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
poller = client.begin_analyze(
|
|
374
|
+
FIELD_SCHEMA_SUGGEST_ANALYZER_ID,
|
|
375
|
+
inputs=[input_payload],
|
|
376
|
+
)
|
|
377
|
+
result = poller.result()
|
|
378
|
+
suggested_fields = suggested_fields_from_result(result)
|
|
379
|
+
if suggested_fields:
|
|
380
|
+
_add_placeholder_descriptions(suggested_fields)
|
|
381
|
+
payload["fieldSchema"]["fields"] = suggested_fields
|
|
382
|
+
payload["description"] = (
|
|
383
|
+
f"Suggested from sample '{sample_path.name}' using prebuilt-documentFieldSchema."
|
|
384
|
+
)
|
|
385
|
+
payload["fieldSchema"]["description"] = (
|
|
386
|
+
"Suggested extraction schema derived from one sample document."
|
|
387
|
+
)
|
|
388
|
+
return payload, True
|
|
389
|
+
return payload, False
|