@postalsys/email-content 0.1.8 → 0.1.9
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.
- package/README.md +17 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/analyze.d.ts +3 -0
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +10 -4
- package/dist/analyze.js.map +1 -1
- package/dist/annotate/agreement.d.ts +99 -0
- package/dist/annotate/agreement.d.ts.map +1 -0
- package/dist/annotate/agreement.js +126 -0
- package/dist/annotate/agreement.js.map +1 -0
- package/dist/annotate/client.d.ts +7 -19
- package/dist/annotate/client.d.ts.map +1 -1
- package/dist/annotate/client.js +28 -82
- package/dist/annotate/client.js.map +1 -1
- package/dist/annotate/http.d.ts +57 -0
- package/dist/annotate/http.d.ts.map +1 -0
- package/dist/annotate/http.js +99 -0
- package/dist/annotate/http.js.map +1 -0
- package/dist/annotate/llm.d.ts +17 -0
- package/dist/annotate/llm.d.ts.map +1 -1
- package/dist/annotate/llm.js +68 -36
- package/dist/annotate/llm.js.map +1 -1
- package/dist/annotate/probabilities.d.ts +48 -0
- package/dist/annotate/probabilities.d.ts.map +1 -0
- package/dist/annotate/probabilities.js +119 -0
- package/dist/annotate/probabilities.js.map +1 -0
- package/dist/annotate/providers.d.ts +26 -0
- package/dist/annotate/providers.d.ts.map +1 -0
- package/dist/annotate/providers.js +43 -0
- package/dist/annotate/providers.js.map +1 -0
- package/dist/annotate/store.d.ts +3 -0
- package/dist/annotate/store.d.ts.map +1 -1
- package/dist/annotate/store.js +3 -2
- package/dist/annotate/store.js.map +1 -1
- package/dist/annotate/typesafe-client.d.ts +64 -0
- package/dist/annotate/typesafe-client.d.ts.map +1 -0
- package/dist/annotate/typesafe-client.js +76 -0
- package/dist/annotate/typesafe-client.js.map +1 -0
- package/dist/annotate/typesafe-prompt.d.ts +64 -0
- package/dist/annotate/typesafe-prompt.d.ts.map +1 -0
- package/dist/annotate/typesafe-prompt.js +170 -0
- package/dist/annotate/typesafe-prompt.js.map +1 -0
- package/dist/classify/rules.d.ts +7 -0
- package/dist/classify/rules.d.ts.map +1 -1
- package/dist/classify/rules.js +67 -14
- package/dist/classify/rules.js.map +1 -1
- package/dist/cli/agreement.d.ts +27 -0
- package/dist/cli/agreement.d.ts.map +1 -0
- package/dist/cli/agreement.js +153 -0
- package/dist/cli/agreement.js.map +1 -0
- package/dist/cli/annotate.d.ts.map +1 -1
- package/dist/cli/annotate.js +28 -16
- package/dist/cli/annotate.js.map +1 -1
- package/dist/cli/audit.d.ts +12 -0
- package/dist/cli/audit.d.ts.map +1 -0
- package/dist/cli/audit.js +134 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/disagreements.d.ts.map +1 -1
- package/dist/cli/disagreements.js +20 -3
- package/dist/cli/disagreements.js.map +1 -1
- package/dist/cli/evaluate.d.ts.map +1 -1
- package/dist/cli/evaluate.js +5 -70
- package/dist/cli/evaluate.js.map +1 -1
- package/dist/cli/main.js +28 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/corpus/analyze.d.ts.map +1 -1
- package/dist/corpus/analyze.js +4 -5
- package/dist/corpus/analyze.js.map +1 -1
- package/dist/corpus/migrations/index.d.ts.map +1 -1
- package/dist/corpus/migrations/index.js +14 -0
- package/dist/corpus/migrations/index.js.map +1 -1
- package/dist/corpus/stats.d.ts +1 -0
- package/dist/corpus/stats.d.ts.map +1 -1
- package/dist/corpus/stats.js +1 -0
- package/dist/corpus/stats.js.map +1 -1
- package/dist/detect/patterns/forward-headers.json +24 -0
- package/dist/detect/patterns/html-clients.json +27 -0
- package/dist/detect/patterns/reply-headers.json +368 -0
- package/dist/detect/reply-header.d.ts.map +1 -1
- package/dist/detect/reply-header.js +12 -1
- package/dist/detect/reply-header.js.map +1 -1
- package/dist/detect/subject.d.ts +16 -0
- package/dist/detect/subject.d.ts.map +1 -0
- package/dist/detect/subject.js +22 -0
- package/dist/detect/subject.js.map +1 -0
- package/dist/eval/pattern-audit.d.ts +56 -0
- package/dist/eval/pattern-audit.d.ts.map +1 -0
- package/dist/eval/pattern-audit.js +111 -0
- package/dist/eval/pattern-audit.js.map +1 -0
- package/dist/eval/reference.d.ts +44 -0
- package/dist/eval/reference.d.ts.map +1 -0
- package/dist/eval/reference.js +87 -0
- package/dist/eval/reference.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mime/flowed.d.ts +14 -0
- package/dist/mime/flowed.d.ts.map +1 -0
- package/dist/mime/flowed.js +71 -0
- package/dist/mime/flowed.js.map +1 -0
- package/dist/mime/parse.js +2 -5
- package/dist/mime/parse.js.map +1 -1
- package/dist/segment/html-blocks.d.ts.map +1 -1
- package/dist/segment/html-blocks.js +11 -7
- package/dist/segment/html-blocks.js.map +1 -1
- package/dist/segment/text-blocks.d.ts.map +1 -1
- package/dist/segment/text-blocks.js +14 -3
- package/dist/segment/text-blocks.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agreement between annotators.
|
|
3
|
+
*
|
|
4
|
+
* `evaluate` scores the extractor against a reference. This scores an annotator against other
|
|
5
|
+
* annotators, which is the question a new annotator has to answer before its labels are worth
|
|
6
|
+
* anything: two strong models agree on 97.6 percent of blocks and a weak one on 80.5
|
|
7
|
+
* (docs/annotation.md), so a candidate's agreement with the strong models says which of those it
|
|
8
|
+
* resembles.
|
|
9
|
+
*
|
|
10
|
+
* Pure over rows, so it is testable without a database.
|
|
11
|
+
*/
|
|
12
|
+
import { type Label } from '../types';
|
|
13
|
+
import { type LabelProbabilities } from './probabilities';
|
|
14
|
+
export interface AnnotatedBlock {
|
|
15
|
+
path: string;
|
|
16
|
+
representation: string;
|
|
17
|
+
blockIndex: number;
|
|
18
|
+
mimePart: string | null;
|
|
19
|
+
blockStart: number | null;
|
|
20
|
+
blockEnd: number | null;
|
|
21
|
+
charCount: number;
|
|
22
|
+
label: Label;
|
|
23
|
+
probabilities: LabelProbabilities | null;
|
|
24
|
+
lang: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface PairCount {
|
|
27
|
+
reference: Label;
|
|
28
|
+
candidate: Label;
|
|
29
|
+
blocks: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ReferenceAgreement {
|
|
32
|
+
name: string;
|
|
33
|
+
/** Blocks labelled by both the candidate and this reference. */
|
|
34
|
+
shared: number;
|
|
35
|
+
agreed: number;
|
|
36
|
+
sharedChars: number;
|
|
37
|
+
agreedChars: number;
|
|
38
|
+
/** The candidate would hide what this reference calls authored: the expensive direction. */
|
|
39
|
+
expensive: {
|
|
40
|
+
blocks: number;
|
|
41
|
+
chars: number;
|
|
42
|
+
};
|
|
43
|
+
/** The candidate leaves visible what this reference calls history: the cheap direction. */
|
|
44
|
+
cheap: {
|
|
45
|
+
blocks: number;
|
|
46
|
+
chars: number;
|
|
47
|
+
};
|
|
48
|
+
/** Most frequent disagreements, largest first. */
|
|
49
|
+
pairs: PairCount[];
|
|
50
|
+
perLabel: {
|
|
51
|
+
label: Label;
|
|
52
|
+
support: number;
|
|
53
|
+
agreed: number;
|
|
54
|
+
}[];
|
|
55
|
+
perLang: {
|
|
56
|
+
lang: string;
|
|
57
|
+
shared: number;
|
|
58
|
+
agreed: number;
|
|
59
|
+
}[];
|
|
60
|
+
}
|
|
61
|
+
export interface AgreementReport {
|
|
62
|
+
candidateBlocks: number;
|
|
63
|
+
/** Candidate blocks no reference has a label for, usually because segmentation changed. */
|
|
64
|
+
unmatched: number;
|
|
65
|
+
/** Cost ratio re-applied to the candidate's stored distributions, or null for stored labels. */
|
|
66
|
+
costRatio: number | null;
|
|
67
|
+
/** Candidate blocks whose label was recomputed from a stored distribution. */
|
|
68
|
+
rescored: number;
|
|
69
|
+
references: ReferenceAgreement[];
|
|
70
|
+
/**
|
|
71
|
+
* Blocks every reference calls authored and the candidate would hide. The gate number: an
|
|
72
|
+
* annotator that hides what all strong models agree was written by the sender cannot be used
|
|
73
|
+
* to size content loss, whatever its overall agreement.
|
|
74
|
+
*/
|
|
75
|
+
consensusExpensive: {
|
|
76
|
+
blocks: number;
|
|
77
|
+
chars: number;
|
|
78
|
+
shared: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Identity of a block. Offsets as well as the index, because annotation runs made under different
|
|
83
|
+
* extractor versions can segment the same message differently, and pairing block 7 with a
|
|
84
|
+
* different block 7 would count a segmentation change as a disagreement.
|
|
85
|
+
*/
|
|
86
|
+
export declare function blockKey(block: Pick<AnnotatedBlock, 'path' | 'representation' | 'blockIndex' | 'mimePart' | 'blockStart' | 'blockEnd'>): string;
|
|
87
|
+
/**
|
|
88
|
+
* Compare a candidate annotator against one or more references.
|
|
89
|
+
*
|
|
90
|
+
* @param candidate the candidate's labels, in run order
|
|
91
|
+
* @param references each reference's labels, in run order
|
|
92
|
+
* @param costRatio when set, re-derive the candidate's label from its stored distribution with
|
|
93
|
+
* this ratio instead of using the stored label; blocks without a distribution keep their label
|
|
94
|
+
*/
|
|
95
|
+
export declare function compareAnnotators(candidate: readonly AnnotatedBlock[], references: readonly {
|
|
96
|
+
name: string;
|
|
97
|
+
blocks: readonly AnnotatedBlock[];
|
|
98
|
+
}[], costRatio?: number | null): AgreementReport;
|
|
99
|
+
//# sourceMappingURL=agreement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement.d.ts","sourceRoot":"","sources":["../../src/annotate/agreement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAkC,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1F,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACzC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,2FAA2F;IAC3F,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,kDAAkD;IAClD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,QAAQ,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/D;AAED,MAAM,WAAW,eAAe;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,2FAA2F;IAC3F,SAAS,EAAE,MAAM,CAAC;IAClB,gGAAgG;IAChG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC;;;;OAIG;IACH,kBAAkB,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC,GAAG,MAAM,CAE/I;AA4ED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,UAAU,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAA;CAAE,EAAE,EAC1E,SAAS,GAAE,MAAM,GAAG,IAAW,GAChC,eAAe,CAoCjB"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agreement between annotators.
|
|
4
|
+
*
|
|
5
|
+
* `evaluate` scores the extractor against a reference. This scores an annotator against other
|
|
6
|
+
* annotators, which is the question a new annotator has to answer before its labels are worth
|
|
7
|
+
* anything: two strong models agree on 97.6 percent of blocks and a weak one on 80.5
|
|
8
|
+
* (docs/annotation.md), so a candidate's agreement with the strong models says which of those it
|
|
9
|
+
* resembles.
|
|
10
|
+
*
|
|
11
|
+
* Pure over rows, so it is testable without a database.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.blockKey = blockKey;
|
|
15
|
+
exports.compareAnnotators = compareAnnotators;
|
|
16
|
+
const probabilities_1 = require("./probabilities");
|
|
17
|
+
/**
|
|
18
|
+
* Identity of a block. Offsets as well as the index, because annotation runs made under different
|
|
19
|
+
* extractor versions can segment the same message differently, and pairing block 7 with a
|
|
20
|
+
* different block 7 would count a segmentation change as a disagreement.
|
|
21
|
+
*/
|
|
22
|
+
function blockKey(block) {
|
|
23
|
+
return [block.path, block.representation, block.mimePart ?? '', block.blockIndex, block.blockStart ?? '', block.blockEnd ?? ''].join('\u0000');
|
|
24
|
+
}
|
|
25
|
+
function byKey(blocks) {
|
|
26
|
+
// Later entries win, so callers pass rows in run order and get the latest label per block.
|
|
27
|
+
return new Map(blocks.map(block => [blockKey(block), block]));
|
|
28
|
+
}
|
|
29
|
+
function tally(map, key, agreed) {
|
|
30
|
+
const entry = map.get(key) ?? { total: 0, agreed: 0 };
|
|
31
|
+
entry.total++;
|
|
32
|
+
entry.agreed += agreed ? 1 : 0;
|
|
33
|
+
map.set(key, entry);
|
|
34
|
+
}
|
|
35
|
+
function compareOne(name, candidate, reference) {
|
|
36
|
+
const result = {
|
|
37
|
+
name,
|
|
38
|
+
shared: 0,
|
|
39
|
+
agreed: 0,
|
|
40
|
+
sharedChars: 0,
|
|
41
|
+
agreedChars: 0,
|
|
42
|
+
expensive: { blocks: 0, chars: 0 },
|
|
43
|
+
cheap: { blocks: 0, chars: 0 },
|
|
44
|
+
pairs: [],
|
|
45
|
+
perLabel: [],
|
|
46
|
+
perLang: []
|
|
47
|
+
};
|
|
48
|
+
const pairs = new Map();
|
|
49
|
+
const byLabel = new Map();
|
|
50
|
+
const byLang = new Map();
|
|
51
|
+
for (const [key, { label, charCount }] of candidate) {
|
|
52
|
+
const ref = reference.get(key);
|
|
53
|
+
if (!ref) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const agreed = label === ref.label;
|
|
57
|
+
result.shared++;
|
|
58
|
+
result.sharedChars += charCount;
|
|
59
|
+
tally(byLabel, ref.label, agreed);
|
|
60
|
+
tally(byLang, ref.lang ?? 'unknown', agreed);
|
|
61
|
+
if (agreed) {
|
|
62
|
+
result.agreed++;
|
|
63
|
+
result.agreedChars += charCount;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const pairKey = `${ref.label}|${label}`;
|
|
67
|
+
const pair = pairs.get(pairKey) ?? { reference: ref.label, candidate: label, blocks: 0 };
|
|
68
|
+
pair.blocks++;
|
|
69
|
+
pairs.set(pairKey, pair);
|
|
70
|
+
if (label === 'REPLY_HISTORY' && probabilities_1.AUTHORED_LABELS.has(ref.label)) {
|
|
71
|
+
result.expensive.blocks++;
|
|
72
|
+
result.expensive.chars += charCount;
|
|
73
|
+
}
|
|
74
|
+
else if (ref.label === 'REPLY_HISTORY' && probabilities_1.AUTHORED_LABELS.has(label)) {
|
|
75
|
+
result.cheap.blocks++;
|
|
76
|
+
result.cheap.chars += charCount;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
result.pairs = [...pairs.values()].sort((a, b) => b.blocks - a.blocks || `${a.reference}${a.candidate}`.localeCompare(`${b.reference}${b.candidate}`));
|
|
80
|
+
result.perLabel = [...byLabel].map(([label, { total, agreed }]) => ({ label, support: total, agreed })).sort((a, b) => b.support - a.support);
|
|
81
|
+
result.perLang = [...byLang].map(([lang, { total, agreed }]) => ({ lang, shared: total, agreed })).sort((a, b) => b.shared - a.shared);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compare a candidate annotator against one or more references.
|
|
86
|
+
*
|
|
87
|
+
* @param candidate the candidate's labels, in run order
|
|
88
|
+
* @param references each reference's labels, in run order
|
|
89
|
+
* @param costRatio when set, re-derive the candidate's label from its stored distribution with
|
|
90
|
+
* this ratio instead of using the stored label; blocks without a distribution keep their label
|
|
91
|
+
*/
|
|
92
|
+
function compareAnnotators(candidate, references, costRatio = null) {
|
|
93
|
+
const judged = new Map();
|
|
94
|
+
let rescored = 0;
|
|
95
|
+
for (const [key, block] of byKey(candidate)) {
|
|
96
|
+
const rescore = costRatio !== null && block.probabilities !== null;
|
|
97
|
+
rescored += rescore ? 1 : 0;
|
|
98
|
+
judged.set(key, { label: rescore ? (0, probabilities_1.applyCostRule)(block.probabilities, costRatio) : block.label, charCount: block.charCount });
|
|
99
|
+
}
|
|
100
|
+
const referenceMaps = references.map(reference => ({ name: reference.name, blocks: byKey(reference.blocks) }));
|
|
101
|
+
let unmatched = 0;
|
|
102
|
+
const consensusExpensive = { blocks: 0, chars: 0, shared: 0 };
|
|
103
|
+
for (const [key, { label, charCount }] of judged) {
|
|
104
|
+
const refs = referenceMaps.map(reference => reference.blocks.get(key));
|
|
105
|
+
if (!refs.some(Boolean)) {
|
|
106
|
+
unmatched++;
|
|
107
|
+
}
|
|
108
|
+
if (!referenceMaps.length || !refs.every(Boolean)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
consensusExpensive.shared++;
|
|
112
|
+
if (label === 'REPLY_HISTORY' && refs.every(ref => probabilities_1.AUTHORED_LABELS.has(ref.label))) {
|
|
113
|
+
consensusExpensive.blocks++;
|
|
114
|
+
consensusExpensive.chars += charCount;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
candidateBlocks: judged.size,
|
|
119
|
+
unmatched,
|
|
120
|
+
costRatio,
|
|
121
|
+
rescored,
|
|
122
|
+
references: referenceMaps.map(reference => compareOne(reference.name, judged, reference.blocks)),
|
|
123
|
+
consensusExpensive
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=agreement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement.js","sourceRoot":"","sources":["../../src/annotate/agreement.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,24 +5,14 @@
|
|
|
5
5
|
* Adding a vendor SDK for one optional command would change that materially, and the surface used
|
|
6
6
|
* here is a single POST with a JSON schema response format.
|
|
7
7
|
*
|
|
8
|
-
* This is the
|
|
9
|
-
* reachable from `email-content annotate`, never
|
|
8
|
+
* This is one of the two places in the codebase that send message content off the machine, the
|
|
9
|
+
* other being `typesafe-client.ts`. Both are only reachable from `email-content annotate`, never
|
|
10
|
+
* from `extract()` or the corpus scanner. See
|
|
10
11
|
* docs/threat-model.md, "Configured exception: LLM annotation".
|
|
11
12
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
model: string;
|
|
16
|
-
/** Per request timeout in milliseconds. */
|
|
17
|
-
timeoutMs: number;
|
|
18
|
-
maxRetries: number;
|
|
19
|
-
}
|
|
20
|
-
export interface Usage {
|
|
21
|
-
promptTokens: number;
|
|
22
|
-
completionTokens: number;
|
|
23
|
-
totalTokens: number;
|
|
24
|
-
requests: number;
|
|
25
|
-
}
|
|
13
|
+
import { ApiError, type ApiConfig, type Usage } from './http';
|
|
14
|
+
export type { Usage } from './http';
|
|
15
|
+
export type OpenAIConfig = ApiConfig;
|
|
26
16
|
export interface CompletionRequest {
|
|
27
17
|
system: string;
|
|
28
18
|
user: string;
|
|
@@ -40,9 +30,7 @@ export interface CompletionClient {
|
|
|
40
30
|
readonly model: string;
|
|
41
31
|
complete<T>(request: CompletionRequest): Promise<T>;
|
|
42
32
|
}
|
|
43
|
-
export declare class OpenAIError extends
|
|
44
|
-
readonly status: number;
|
|
45
|
-
readonly retryable: boolean;
|
|
33
|
+
export declare class OpenAIError extends ApiError {
|
|
46
34
|
constructor(message: string, status: number, retryable: boolean);
|
|
47
35
|
}
|
|
48
36
|
/** Load configuration from the environment, after process.loadEnvFile(). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/annotate/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/annotate/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,QAAQ,EAA8B,KAAK,SAAS,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE1F,YAAY,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEpC,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC;AAErC,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACvD;AAED,qBAAa,WAAY,SAAQ,QAAQ;gBACzB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO;CAIlE;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,YAAY,CAEhG;AAED,qBAAa,YAAa,YAAW,gBAAgB;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAyE;gBAElF,MAAM,EAAE,YAAY;IAIhC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED;;;;;;OAMG;IACG,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,CAAC;CA2C5D"}
|
package/dist/annotate/client.js
CHANGED
|
@@ -6,41 +6,25 @@
|
|
|
6
6
|
* Adding a vendor SDK for one optional command would change that materially, and the surface used
|
|
7
7
|
* here is a single POST with a JSON schema response format.
|
|
8
8
|
*
|
|
9
|
-
* This is the
|
|
10
|
-
* reachable from `email-content annotate`, never
|
|
9
|
+
* This is one of the two places in the codebase that send message content off the machine, the
|
|
10
|
+
* other being `typesafe-client.ts`. Both are only reachable from `email-content annotate`, never
|
|
11
|
+
* from `extract()` or the corpus scanner. See
|
|
11
12
|
* docs/threat-model.md, "Configured exception: LLM annotation".
|
|
12
13
|
*/
|
|
13
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
15
|
exports.OpenAIClient = exports.OpenAIError = void 0;
|
|
15
16
|
exports.configFromEnv = configFromEnv;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
retryable;
|
|
17
|
+
const http_1 = require("./http");
|
|
18
|
+
class OpenAIError extends http_1.ApiError {
|
|
19
19
|
constructor(message, status, retryable) {
|
|
20
|
-
super(message);
|
|
20
|
+
super(message, status, retryable);
|
|
21
21
|
this.name = 'OpenAIError';
|
|
22
|
-
this.status = status;
|
|
23
|
-
this.retryable = retryable;
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
exports.OpenAIError = OpenAIError;
|
|
27
25
|
/** Load configuration from the environment, after process.loadEnvFile(). */
|
|
28
26
|
function configFromEnv(model, overrides = {}) {
|
|
29
|
-
|
|
30
|
-
if (!apiKey) {
|
|
31
|
-
throw new Error('OPENAI_API_KEY is not set. Copy .env.example to .env and fill it in.');
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
apiKey,
|
|
35
|
-
baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
|
36
|
-
model,
|
|
37
|
-
timeoutMs: 120000,
|
|
38
|
-
maxRetries: 5,
|
|
39
|
-
...overrides
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function sleep(ms) {
|
|
43
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
27
|
+
return (0, http_1.apiConfigFromEnv)({ keyVar: 'OPENAI_API_KEY', urlVar: 'OPENAI_BASE_URL', defaultUrl: 'https://api.openai.com/v1' }, model, overrides);
|
|
44
28
|
}
|
|
45
29
|
class OpenAIClient {
|
|
46
30
|
config;
|
|
@@ -59,55 +43,27 @@ class OpenAIClient {
|
|
|
59
43
|
* contract, and a retry would usually fail the same way.
|
|
60
44
|
*/
|
|
61
45
|
async complete(request) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
headers: {
|
|
76
|
-
Authorization: `Bearer ${this.config.apiKey}`,
|
|
77
|
-
'Content-Type': 'application/json'
|
|
78
|
-
},
|
|
79
|
-
body: JSON.stringify({
|
|
80
|
-
model: this.config.model,
|
|
81
|
-
messages: [
|
|
82
|
-
{ role: 'system', content: request.system },
|
|
83
|
-
{ role: 'user', content: request.user }
|
|
84
|
-
],
|
|
85
|
-
response_format: {
|
|
86
|
-
type: 'json_schema',
|
|
87
|
-
json_schema: { name: request.schemaName, strict: true, schema: request.schema }
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
});
|
|
91
|
-
if (!response.ok) {
|
|
92
|
-
const retryable = response.status === 429 || response.status >= 500;
|
|
93
|
-
let detail = `HTTP ${response.status}`;
|
|
94
|
-
try {
|
|
95
|
-
const body = (await response.json());
|
|
96
|
-
if (body.error?.message) {
|
|
97
|
-
detail = body.error.message.slice(0, 300);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Body was not JSON. The status alone is enough.
|
|
102
|
-
}
|
|
103
|
-
const error = new OpenAIError(detail, response.status, retryable);
|
|
104
|
-
if (!retryable) {
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
lastError = error;
|
|
108
|
-
continue;
|
|
46
|
+
return (0, http_1.postJson)({
|
|
47
|
+
url: `${this.config.baseUrl}/chat/completions`,
|
|
48
|
+
apiKey: this.config.apiKey,
|
|
49
|
+
policy: this.config,
|
|
50
|
+
payload: {
|
|
51
|
+
model: this.config.model,
|
|
52
|
+
messages: [
|
|
53
|
+
{ role: 'system', content: request.system },
|
|
54
|
+
{ role: 'user', content: request.user }
|
|
55
|
+
],
|
|
56
|
+
response_format: {
|
|
57
|
+
type: 'json_schema',
|
|
58
|
+
json_schema: { name: request.schemaName, strict: true, schema: request.schema }
|
|
109
59
|
}
|
|
110
|
-
|
|
60
|
+
},
|
|
61
|
+
httpError: (status, body) => {
|
|
62
|
+
const message = body?.error?.message;
|
|
63
|
+
return new OpenAIError(message ? message.slice(0, 300) : `HTTP ${status}`, status, status === 429 || status >= 500);
|
|
64
|
+
},
|
|
65
|
+
parse: raw => {
|
|
66
|
+
const body = raw;
|
|
111
67
|
this.usage.requests++;
|
|
112
68
|
this.usage.promptTokens += body.usage?.prompt_tokens ?? 0;
|
|
113
69
|
this.usage.completionTokens += body.usage?.completion_tokens ?? 0;
|
|
@@ -122,17 +78,7 @@ class OpenAIClient {
|
|
|
122
78
|
}
|
|
123
79
|
return JSON.parse(content);
|
|
124
80
|
}
|
|
125
|
-
|
|
126
|
-
if (err instanceof OpenAIError && !err.retryable) {
|
|
127
|
-
throw err;
|
|
128
|
-
}
|
|
129
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
clearTimeout(timer);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
throw lastError ?? new Error('request failed');
|
|
81
|
+
});
|
|
136
82
|
}
|
|
137
83
|
}
|
|
138
84
|
exports.OpenAIClient = OpenAIClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/annotate/client.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/annotate/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrying JSON POST shared by the annotator clients.
|
|
3
|
+
*
|
|
4
|
+
* Both providers need the same thing: one authenticated POST, exponential backoff on rate limits
|
|
5
|
+
* and overload, and no retry once the provider has said the request itself is wrong. Keeping that
|
|
6
|
+
* loop in one place means a fix to it cannot land in one client and not the other.
|
|
7
|
+
*/
|
|
8
|
+
export interface RetryPolicy {
|
|
9
|
+
/** Per request timeout in milliseconds. */
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
maxRetries: number;
|
|
12
|
+
}
|
|
13
|
+
/** What every provider client needs: where, who, which model, and how hard to retry. */
|
|
14
|
+
export interface ApiConfig extends RetryPolicy {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
model: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load a provider's configuration from the environment, after process.loadEnvFile().
|
|
21
|
+
*
|
|
22
|
+
* Throws when the key is missing. That is the fail closed guarantee in docs/threat-model.md: a
|
|
23
|
+
* provider without a configured key never gets as far as reading a message.
|
|
24
|
+
*/
|
|
25
|
+
export declare function apiConfigFromEnv(env: {
|
|
26
|
+
keyVar: string;
|
|
27
|
+
urlVar: string;
|
|
28
|
+
defaultUrl: string;
|
|
29
|
+
}, model: string, overrides?: Partial<ApiConfig>): ApiConfig;
|
|
30
|
+
/** Token accounting, in one shape for every provider. */
|
|
31
|
+
export interface Usage {
|
|
32
|
+
promptTokens: number;
|
|
33
|
+
completionTokens: number;
|
|
34
|
+
totalTokens: number;
|
|
35
|
+
requests: number;
|
|
36
|
+
}
|
|
37
|
+
/** An HTTP or protocol failure from an annotation provider. */
|
|
38
|
+
export declare class ApiError extends Error {
|
|
39
|
+
readonly status: number;
|
|
40
|
+
readonly retryable: boolean;
|
|
41
|
+
constructor(message: string, status: number, retryable: boolean);
|
|
42
|
+
}
|
|
43
|
+
export interface PostJsonOptions<T> {
|
|
44
|
+
url: string;
|
|
45
|
+
apiKey: string;
|
|
46
|
+
payload: unknown;
|
|
47
|
+
policy: RetryPolicy;
|
|
48
|
+
/** Build the error for a non 2xx response. Decides what, if anything, of the body is surfaced. */
|
|
49
|
+
httpError(status: number, body: unknown): ApiError;
|
|
50
|
+
/**
|
|
51
|
+
* Turn a 2xx body into the result. Runs inside the retry loop: throwing an ApiError with
|
|
52
|
+
* `retryable: false` stops immediately, any other error is retried like a network failure.
|
|
53
|
+
*/
|
|
54
|
+
parse(body: unknown): T;
|
|
55
|
+
}
|
|
56
|
+
export declare function postJson<T>(options: PostJsonOptions<T>): Promise<T>;
|
|
57
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/annotate/http.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW;IACxB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,wFAAwF;AACxF,MAAM,WAAW,SAAU,SAAQ,WAAW;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,CAAC,SAAS,CAAM,GAAG,SAAS,CAa1J;AAED,yDAAyD;AACzD,MAAM,WAAW,KAAK;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,+DAA+D;AAC/D,qBAAa,QAAS,SAAQ,KAAK;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO;CAMlE;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,kGAAkG;IAClG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IACnD;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,CAAC,CAAC;CAC3B;AAMD,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAqDzE"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Retrying JSON POST shared by the annotator clients.
|
|
4
|
+
*
|
|
5
|
+
* Both providers need the same thing: one authenticated POST, exponential backoff on rate limits
|
|
6
|
+
* and overload, and no retry once the provider has said the request itself is wrong. Keeping that
|
|
7
|
+
* loop in one place means a fix to it cannot land in one client and not the other.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ApiError = void 0;
|
|
11
|
+
exports.apiConfigFromEnv = apiConfigFromEnv;
|
|
12
|
+
exports.postJson = postJson;
|
|
13
|
+
/**
|
|
14
|
+
* Load a provider's configuration from the environment, after process.loadEnvFile().
|
|
15
|
+
*
|
|
16
|
+
* Throws when the key is missing. That is the fail closed guarantee in docs/threat-model.md: a
|
|
17
|
+
* provider without a configured key never gets as far as reading a message.
|
|
18
|
+
*/
|
|
19
|
+
function apiConfigFromEnv(env, model, overrides = {}) {
|
|
20
|
+
const apiKey = process.env[env.keyVar];
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
throw new Error(`${env.keyVar} is not set. Copy .env.example to .env and fill it in.`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
apiKey,
|
|
26
|
+
baseUrl: process.env[env.urlVar] || env.defaultUrl,
|
|
27
|
+
model,
|
|
28
|
+
timeoutMs: 120000,
|
|
29
|
+
maxRetries: 5,
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** An HTTP or protocol failure from an annotation provider. */
|
|
34
|
+
class ApiError extends Error {
|
|
35
|
+
status;
|
|
36
|
+
retryable;
|
|
37
|
+
constructor(message, status, retryable) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'ApiError';
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.retryable = retryable;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.ApiError = ApiError;
|
|
45
|
+
function sleep(ms) {
|
|
46
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
47
|
+
}
|
|
48
|
+
async function postJson(options) {
|
|
49
|
+
let lastError = null;
|
|
50
|
+
// Payloads run to hundreds of kilobytes, and nothing about them changes between attempts.
|
|
51
|
+
const body = JSON.stringify(options.payload);
|
|
52
|
+
for (let attempt = 0; attempt <= options.policy.maxRetries; attempt++) {
|
|
53
|
+
if (attempt > 0) {
|
|
54
|
+
// Full jitter, capped, so a burst of workers does not retry in lockstep.
|
|
55
|
+
const backoff = Math.min(30000, 500 * 2 ** (attempt - 1));
|
|
56
|
+
await sleep(Math.random() * backoff);
|
|
57
|
+
}
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timer = setTimeout(() => controller.abort(), options.policy.timeoutMs);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(options.url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
66
|
+
'Content-Type': 'application/json'
|
|
67
|
+
},
|
|
68
|
+
body
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
let detail = null;
|
|
72
|
+
try {
|
|
73
|
+
detail = await response.json();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Body was not JSON. The status alone is enough.
|
|
77
|
+
}
|
|
78
|
+
const error = options.httpError(response.status, detail);
|
|
79
|
+
if (!error.retryable) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
lastError = error;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return options.parse(await response.json());
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (err instanceof ApiError && !err.retryable) {
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw lastError ?? new Error('request failed');
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/annotate/http.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAqBH,4CAaC;AAyCD,4BAqDC;AAjHD;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,GAA2D,EAAE,KAAa,EAAE,YAAgC,EAAE;IAC3I,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,wDAAwD,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO;QACH,MAAM;QACN,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,UAAU;QAClD,KAAK;QACL,SAAS,EAAE,MAAM;QACjB,UAAU,EAAE,CAAC;QACb,GAAG,SAAS;KACf,CAAC;AACN,CAAC;AAUD,+DAA+D;AAC/D,MAAa,QAAS,SAAQ,KAAK;IACtB,MAAM,CAAS;IACf,SAAS,CAAU;IAE5B,YAAY,OAAe,EAAE,MAAc,EAAE,SAAkB;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAVD,4BAUC;AAgBD,SAAS,KAAK,CAAC,EAAU;IACrB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAEM,KAAK,UAAU,QAAQ,CAAI,OAA2B;IACzD,IAAI,SAAS,GAAiB,IAAI,CAAC;IACnC,0FAA0F;IAC1F,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACpE,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YACd,yEAAyE;YACzE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1D,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAE7E,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;gBACtC,MAAM,EAAE,MAAM;gBACd,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,OAAO,EAAE;oBACL,aAAa,EAAE,UAAU,OAAO,CAAC,MAAM,EAAE;oBACzC,cAAc,EAAE,kBAAkB;iBACrC;gBACD,IAAI;aACP,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACf,IAAI,MAAM,GAAY,IAAI,CAAC;gBAC3B,IAAI,CAAC;oBACD,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,CAAC;gBAAC,MAAM,CAAC;oBACL,iDAAiD;gBACrD,CAAC;gBACD,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBACzD,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;oBACnB,MAAM,KAAK,CAAC;gBAChB,CAAC;gBACD,SAAS,GAAG,KAAK,CAAC;gBAClB,SAAS;YACb,CAAC;YAED,OAAO,OAAO,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,IAAI,GAAG,YAAY,QAAQ,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;gBAC5C,MAAM,GAAG,CAAC;YACd,CAAC;YACD,SAAS,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACpE,CAAC;gBAAS,CAAC;YACP,YAAY,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;IACL,CAAC;IAED,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;AACnD,CAAC"}
|