Skip to content

Commit 9d6d8ed

Browse files
[ai-form-recognizer] Refactor DocumentModel (Azure#22488)
* [ai-form-recognizer] Diff-squash of documentmodel refactor onto main. * Duplicate prebuilt schemas for tests (don't require dev-tool/register for tests) * Fix lint error * Update some docs
1 parent abfb0cf commit 9d6d8ed

File tree

126 files changed

+9850
-17794
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+9850
-17794
lines changed

common/config/rush/pnpm-lock.yaml

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/formrecognizer/ai-form-recognizer/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44

55
### Features Added
66

7+
- Refactored generic `DocumentModel` support to be more robust to different kinds of models. It now supports strongly-typed results for `prebuilt-read`, `prebuilt-layout`, and `prebuilt-document`. See the "Breaking Changes" section for more information about how to replace existing usage of `PrebuiltModels` with new model code.
8+
79
### Breaking Changes
810

11+
- Removed `PrebuiltModels` and all of its fields as well as related helper types. The strongly-typed functionality previously provided by `PrebuiltModels` is now provided by sample code that you can copy into your own project. See the [`prebuilt` samples directory](https://github.com/azure/azure-sdk-for-js/tree/main/sdk/formrecognizer/ai-form-recognizer/samples-dev/prebuilt/) for models compatible with the current Form Recognizer API.
12+
- Separated URL-based and file-based analysis inputs into two separate methods: `beginAnalyzeDocument` (for file stream inputs) and `beginAnalyzeDocumentFromUrl` (for URL-based inputs). Previously, both were accepted as inputs to a single `beginAnalyzeDocument` method, and a string value would be interpreted as if it were a URL, but this was confusing. The two inputs now have distinct signatures and documentation.
13+
- Removed the `beginExtractLayout`, `beginExtractGeneralDocument`, and `beginReadDocument` methods. Strongly-typed `DocumentModel` instances for the corresponding `prebuilt-layout`, `prebuilt-document`, and `prebuilt-read` models are located in the [`prebuilt` samples directory](https://github.com/azure/azure-sdk-for-js/tree/main/sdk/formrecognizer/ai-form-recognizer/samples-dev/prebuilt/).
14+
915
### Bugs Fixed
1016

1117
### Other Changes
1218

19+
- Strongly-typed analysis functionality now checks that the `DocumentAnalysisClient`'s API version now matches the assumed API version of the `DocumentModel` exactly. This ensures that a strongly-typed model can only be used with the API version it was created for, so that future changes cannot violate the model's schema.
20+
1321
## 4.0.0-beta.5 (2022-06-22)
1422

1523
### Bugs Fixed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { DocumentModelInfo } from "../src";
5+
import { Field } from "./utils";
6+
7+
/**
8+
* @internal
9+
* Field information for the top level fields of `AnalyzeResult`.
10+
*/
11+
export const defaultResultFields: Record<string, Field> = {
12+
pages: {
13+
name: "pages",
14+
docContents: "Extracted pages.",
15+
type: "fr.DocumentPage[]",
16+
optional: true,
17+
},
18+
tables: {
19+
name: "tables",
20+
docContents: "Extracted tables.",
21+
type: "fr.DocumentTable[]",
22+
optional: true,
23+
},
24+
keyValuePairs: {
25+
name: "keyValuePairs",
26+
docContents: "Extracted key-value pairs.",
27+
type: "fr.DocumentKeyValuePair[]",
28+
optional: true,
29+
},
30+
languages: {
31+
name: "languages",
32+
docContents: "Extracted text languages.",
33+
type: "fr.DocumentLanguage[]",
34+
optional: true,
35+
},
36+
styles: {
37+
name: "styles",
38+
docContents: "Extracted font styles.",
39+
type: "fr.DocumentStyle[]",
40+
optional: true,
41+
},
42+
documents: {
43+
name: "documents",
44+
docContents:
45+
"Extracted documents (instances of any of the model's document types and corresponding field schemas).",
46+
type: "fr.Document[]",
47+
optional: true,
48+
},
49+
paragraphs: {
50+
name: "paragraphs",
51+
docContents: "Extracted document paragraphs.",
52+
type: "fr.DocumentParagraph[]",
53+
optional: true,
54+
},
55+
} as const;
56+
57+
const allFeatures = [
58+
"pages",
59+
"tables",
60+
"keyValuePairs",
61+
"languages",
62+
"styles",
63+
"paragraphs",
64+
"_documents",
65+
];
66+
67+
const textFeatures = ["pages", "paragraphs", "styles"];
68+
const layoutFeatures = [...textFeatures, "tables"];
69+
const documentFeatures = [...layoutFeatures, "keyValuePairs"];
70+
71+
/**
72+
* @internal
73+
* @param model - the model to get the features of
74+
* @returns the list of features supported by the model
75+
*/
76+
export function getFeatures(model: DocumentModelInfo): string[] {
77+
return (
78+
(model as any).features ??
79+
{
80+
"prebuilt-read": [...textFeatures, "languages"],
81+
"prebuilt-layout": layoutFeatures,
82+
"prebuilt-document": documentFeatures,
83+
"prebuilt-receipt": [...textFeatures, "keyValuePairs", "_documents"],
84+
"prebuilt-invoice": [...layoutFeatures, "keyValuePairs", "_documents"],
85+
"prebuilt-idDocument": [...textFeatures, "keyValuePairs", "_documents"],
86+
"prebuilt-businessCard": [...textFeatures, "keyValuePairs", "_documents"],
87+
"prebuilt-tax.us.w2": [...textFeatures, "keyValuePairs", "_documents"],
88+
"prebuilt-vaccinationCard": [...textFeatures, "keyValuePairs", "_documents"],
89+
"prebuilt-healthInsuranceCard.us": [...textFeatures, "keyValuePairs", "_documents"],
90+
}[model.modelId] ??
91+
(model.modelId.startsWith("prebuilt-") ? allFeatures : [...documentFeatures, "_documents"])
92+
);
93+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/bin/env node
2+
3+
// Copyright (c) Microsoft Corporation.
4+
// Licensed under the MIT license.
5+
6+
// This file is ignored by the linter because it is impossible to move the copyright header above the shebang line.
7+
8+
import { AzureKeyCredential, KeyCredential, TokenCredential } from "@azure/core-auth";
9+
import type { DefaultAzureCredential } from "@azure/identity";
10+
11+
import { writeFile } from "fs";
12+
13+
import { DocumentModelAdministrationClient } from "../src/documentModelAdministrationClient";
14+
import { writeModelCode } from "./writeModelCode";
15+
16+
import { format } from "prettier";
17+
18+
/**
19+
* @internal
20+
* Prints a help message for the gen-model command.
21+
*/
22+
function printHelp() {
23+
console.error(`
24+
Usage:
25+
gen-model [options] <model-id>
26+
27+
Create a strongly-typed DocumentModel for the Azure Form Recognizer SDK for JavaScript.
28+
29+
Options:
30+
-h, --help \tshow this help message
31+
-o, --output\tdetermines where to output the model
32+
33+
--endpoint <endpoint>\tthe Form Recognizer resource's endpoint
34+
--api-key <api-key> \tthe Form Recognizer resource's API key
35+
36+
Default values:
37+
38+
If the \`--endpoint\` or \`--api-key\` options are not provided, then the values
39+
of the \`FORM_RECOGNIZER_ENDPOINT\` and \`FORM_RECOGNIZER_API_KEY\` environment
40+
variables will be used as defaults, respectively.
41+
42+
Authentication:
43+
44+
If an API key is available (via. the \`--api-key\` option or \`FORM_RECOGNIZER_API_KEY\`
45+
environment variable), then it will be used to authenticate requests to the Form
46+
Recognizer service.
47+
48+
Otherwise, if the \`@azure/identity\` package is installed, the \`DefaultAzureCredential\`
49+
from that package will be used.
50+
51+
One of these methods must be available to authenticate with the service.`);
52+
}
53+
54+
async function tryAad(): Promise<DefaultAzureCredential> {
55+
try {
56+
return new (await import("@azure/identity")).DefaultAzureCredential();
57+
} catch {
58+
throw new Error();
59+
}
60+
}
61+
62+
/**
63+
* @internal
64+
* The main function of the gen-model command.
65+
*/
66+
async function main(): Promise<void> {
67+
const args = process.argv.slice(2);
68+
69+
let modelId: string | undefined = undefined;
70+
let endpoint = process.env.FORM_RECOGNIZER_ENDPOINT;
71+
let apiKey = process.env.FORM_RECOGNIZER_API_KEY;
72+
let output: string | undefined = undefined;
73+
74+
console.error("gen-model - create strong TypeScript types for models");
75+
76+
if (args.some((arg) => arg === "--help" || arg === "-h")) {
77+
printHelp();
78+
return;
79+
}
80+
81+
let idx = 0;
82+
while (idx < args.length) {
83+
switch (args[idx]) {
84+
case "--endpoint":
85+
endpoint = args[(idx += 1)];
86+
break;
87+
case "--api-key":
88+
apiKey = args[(idx += 1)];
89+
break;
90+
case "-o":
91+
case "--output":
92+
output = args[(idx += 1)];
93+
break;
94+
default:
95+
modelId = args[idx];
96+
break;
97+
}
98+
idx += 1;
99+
}
100+
101+
if (idx + 1 <= args.length) {
102+
console.error("warning: unused arguments:", args.slice(idx + 1).join(" "));
103+
}
104+
105+
if (!modelId) {
106+
throw new Error("no model ID provided");
107+
}
108+
109+
if (!endpoint) {
110+
throw new Error("no endpoint provided");
111+
}
112+
113+
// We try API key-based authentication first, then AAD. If neither works, then we throw an error.
114+
let credential: KeyCredential | TokenCredential;
115+
116+
if (apiKey) {
117+
console.error("Using API key authentication.");
118+
credential = new AzureKeyCredential(apiKey);
119+
} else {
120+
try {
121+
credential = await tryAad();
122+
console.error("Using Azure Active Directory authentication (DefaultAzureCredential).");
123+
} catch {
124+
throw new Error(
125+
[
126+
"no authentication method is available;",
127+
"provide an API key or install and configure the `@azure/identity` package",
128+
"(`npm i --save-dev @azure/identity`)",
129+
].join(" ")
130+
);
131+
}
132+
}
133+
134+
const client = new DocumentModelAdministrationClient(endpoint, credential);
135+
136+
const modelInfo = await client.getModel(modelId);
137+
138+
console.error("Generating model code for:", modelInfo.modelId);
139+
140+
const file = await writeModelCode(modelInfo);
141+
142+
const data = Buffer.from(format(file, { parser: "typescript" }), "utf-8");
143+
144+
if (output !== undefined) {
145+
// output is only refined in this context, so assigning it to "path" preserves that
146+
const path = output;
147+
148+
await new Promise<void>((resolve, reject) => {
149+
writeFile(path, data, null, (error) => {
150+
if (error) reject(error);
151+
else resolve();
152+
});
153+
});
154+
} else {
155+
process.stdout.write(data);
156+
}
157+
}
158+
159+
main().catch((e) => {
160+
console.error(e);
161+
console.error("see `gen-model --help` for more information");
162+
process.exit(1);
163+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
/**
5+
* Convert a list of strings into a camelCase joined representation.
6+
*/
7+
export function camelCase(slug: string[]): string {
8+
return slug.map(capitalize).join("");
9+
}
10+
11+
/**
12+
* Capitalize a string.
13+
*/
14+
export function capitalize(s: string): string {
15+
return s.slice(0, 1).toUpperCase() + s.slice(1);
16+
}
17+
18+
/**
19+
* Uncapitalize a string.
20+
*/
21+
export function uncapitalize(s: string): string {
22+
return s.slice(0, 1).toLowerCase() + s.slice(1);
23+
}
24+
25+
/**
26+
* A simple representation of a field in an interface.
27+
*/
28+
export interface Field {
29+
name: string;
30+
docContents: string;
31+
type: string;
32+
optional?: true;
33+
}

0 commit comments

Comments
 (0)