flowspec2 1.0.0__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.
- flowspec2/__init__.py +105 -0
- flowspec2/authoring/__init__.py +275 -0
- flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
- flowspec2/authoring/authoring-evidence.schema.json +455 -0
- flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
- flowspec2/authoring/benchmark.py +1409 -0
- flowspec2/authoring/corpus/await_correction.case.json +220 -0
- flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
- flowspec2/authoring/corpus/gated_derive.case.json +100 -0
- flowspec2/authoring/corpus/linear.case.json +55 -0
- flowspec2/authoring/corpus/manifest.json +31 -0
- flowspec2/authoring/corpus/subflow.case.json +66 -0
- flowspec2/authoring/corpus/terminal.case.json +94 -0
- flowspec2/authoring/corpus.py +307 -0
- flowspec2/authoring/ctk.py +836 -0
- flowspec2/authoring/detached_signature.py +120 -0
- flowspec2/authoring/evidence.py +311 -0
- flowspec2/authoring/evidence_signature.py +187 -0
- flowspec2/authoring/evidence_verification.py +229 -0
- flowspec2/authoring/gemini.py +215 -0
- flowspec2/authoring/operational-corpus.json +61 -0
- flowspec2/authoring/operational.py +956 -0
- flowspec2/authoring/operational_providers.py +167 -0
- flowspec2/authoring/presentation_review.py +1013 -0
- flowspec2/authoring/presentation_review_signature.py +305 -0
- flowspec2/authoring/projection.py +299 -0
- flowspec2/authoring/provider_prompt.py +83 -0
- flowspec2/backends/__init__.py +82 -0
- flowspec2/backends/http.py +189 -0
- flowspec2/checker.py +238 -0
- flowspec2/cli.py +789 -0
- flowspec2/cli_parser.py +345 -0
- flowspec2/clock.py +23 -0
- flowspec2/compat/__init__.py +47 -0
- flowspec2/compat/models.py +82 -0
- flowspec2/compat/open_workflow.py +616 -0
- flowspec2/compat/rasa.py +19 -0
- flowspec2/compat/rasa_export.py +876 -0
- flowspec2/compat/rasa_import.py +992 -0
- flowspec2/compat/rasa_shared.py +270 -0
- flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
- flowspec2/compat/tool_profiles.py +149 -0
- flowspec2/compat/yaml.py +147 -0
- flowspec2/compiler.py +957 -0
- flowspec2/compiler_contracts.py +17 -0
- flowspec2/compiler_resume_contracts.py +594 -0
- flowspec2/compiler_schema_relations.py +1830 -0
- flowspec2/compiler_tool_contracts.py +245 -0
- flowspec2/compiler_value_contracts.py +447 -0
- flowspec2/derive.py +32 -0
- flowspec2/diagnostics.py +94 -0
- flowspec2/domains.py +489 -0
- flowspec2/experimental/__init__.py +57 -0
- flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
- flowspec2/experimental/v3-preview-loss-policy.json +161 -0
- flowspec2/experimental/v3_lowering.py +928 -0
- flowspec2/experimental/v3_preview.py +2460 -0
- flowspec2/flowspec-2.schema.json +635 -0
- flowspec2/interactive.py +300 -0
- flowspec2/ir.py +1459 -0
- flowspec2/json_codec.py +120 -0
- flowspec2/llm.py +366 -0
- flowspec2/models.py +121 -0
- flowspec2/nodes.py +1537 -0
- flowspec2/observability.py +151 -0
- flowspec2/predicates.py +89 -0
- flowspec2/profiles.py +118 -0
- flowspec2/py.typed +0 -0
- flowspec2/runtime.py +1059 -0
- flowspec2/schema.py +54 -0
- flowspec2/schema_contracts.py +203 -0
- flowspec2/semantic_derive_contracts.py +530 -0
- flowspec2/semantic_path_contracts.py +528 -0
- flowspec2/semantic_predicate_contracts.py +355 -0
- flowspec2/semantic_schema_contracts.py +137 -0
- flowspec2/semantic_source_contracts.py +21 -0
- flowspec2/semantic_state_contracts.py +450 -0
- flowspec2/semantic_support.py +40 -0
- flowspec2/semantics.py +410 -0
- flowspec2/state_migration.py +1034 -0
- flowspec2/subflows/__init__.py +1117 -0
- flowspec2/subflows/address.py +328 -0
- flowspec2/subflows/identification.py +1031 -0
- flowspec2/tools.py +1154 -0
- flowspec2-1.0.0.dist-info/METADATA +482 -0
- flowspec2-1.0.0.dist-info/RECORD +92 -0
- flowspec2-1.0.0.dist-info/WHEEL +4 -0
- flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
- flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "flowspec2/authoring-case@3",
|
|
3
|
+
"identifier": "gated_derivation",
|
|
4
|
+
"prompt": "Author the ordered steps collect_issue_category, collect_issue_detail, and derive_backend_classification. Collect required issue_category from the categorical IssueCategory values Pothole and Obstruction. Collect required issue_detail from categorical IssueDetail values Small and Large, require issue_category first, and ask only when slots.issue_category equals Pothole. Derive backend_classification from issue_category followed by issue_detail with Pothole|Small to POTHOLE_SMALL, Pothole|Large to POTHOLE_LARGE, Obstruction|null to STREET_OBSTRUCTION, and default $from[0]. Add no other semantic behavior. Flow identity, version, route prose, and non-verbatim prompt wording may vary.",
|
|
5
|
+
"feature_tags": ["gate", "derive", "closed_domain"],
|
|
6
|
+
"required_constructs": [
|
|
7
|
+
{
|
|
8
|
+
"identifier": "conditional-gate",
|
|
9
|
+
"pointer_pattern": "/path/*/ask_when",
|
|
10
|
+
"expected": {
|
|
11
|
+
"eq": [
|
|
12
|
+
"slots.issue_category",
|
|
13
|
+
"Pothole"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"identifier": "derive-step",
|
|
19
|
+
"pointer_pattern": "/path/*/derive",
|
|
20
|
+
"expected": "backend_classification"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"identifier": "derive-definition",
|
|
24
|
+
"pointer_pattern": "/derive/*/writes",
|
|
25
|
+
"expected": "backend_classification"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"identifier": "derive-sources",
|
|
29
|
+
"pointer_pattern": "/derive/*/from",
|
|
30
|
+
"expected": [
|
|
31
|
+
"issue_category",
|
|
32
|
+
"issue_detail"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"source": {
|
|
37
|
+
"schema": "flowspec/2",
|
|
38
|
+
"flow": "gated_issue_classification",
|
|
39
|
+
"version": "1.0.0",
|
|
40
|
+
"route": {
|
|
41
|
+
"description": "Classify a street issue with a conditional detail."
|
|
42
|
+
},
|
|
43
|
+
"domains": {
|
|
44
|
+
"IssueCategory": {
|
|
45
|
+
"type": "categorical",
|
|
46
|
+
"values": ["Pothole", "Obstruction"]
|
|
47
|
+
},
|
|
48
|
+
"IssueDetail": {
|
|
49
|
+
"type": "categorical",
|
|
50
|
+
"values": ["Small", "Large"]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"slots": {
|
|
54
|
+
"issue_category": {
|
|
55
|
+
"domain": "IssueCategory",
|
|
56
|
+
"required": true
|
|
57
|
+
},
|
|
58
|
+
"issue_detail": {
|
|
59
|
+
"domain": "IssueDetail",
|
|
60
|
+
"required": true,
|
|
61
|
+
"requires": ["issue_category"]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"path": [
|
|
65
|
+
{
|
|
66
|
+
"step": "collect_issue_category",
|
|
67
|
+
"slot": "issue_category",
|
|
68
|
+
"prompt": {
|
|
69
|
+
"text": "Which type of issue are you reporting?"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"step": "collect_issue_detail",
|
|
74
|
+
"slot": "issue_detail",
|
|
75
|
+
"ask_when": {
|
|
76
|
+
"eq": ["slots.issue_category", "Pothole"]
|
|
77
|
+
},
|
|
78
|
+
"prompt": {
|
|
79
|
+
"text": "How large is the pothole?"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"step": "derive_backend_classification",
|
|
84
|
+
"derive": "backend_classification"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"derive": [
|
|
88
|
+
{
|
|
89
|
+
"writes": "backend_classification",
|
|
90
|
+
"from": ["issue_category", "issue_detail"],
|
|
91
|
+
"lookup": {
|
|
92
|
+
"Pothole|Small": "POTHOLE_SMALL",
|
|
93
|
+
"Pothole|Large": "POTHOLE_LARGE",
|
|
94
|
+
"Obstruction|null": "STREET_OBSTRUCTION"
|
|
95
|
+
},
|
|
96
|
+
"default": "$from[0]"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "flowspec2/authoring-case@3",
|
|
3
|
+
"identifier": "linear_collection",
|
|
4
|
+
"prompt": "Author a flow with the single ordered step collect_request_description. It collects the required request_description slot through the free-text RequestDescription domain and asks the citizen for a short maintenance-request description. Include route trigger examples and an extraction hint. Add no other domain, slot, path step, derive, subflow, confirmation, capability, or terminal behavior. Flow identity, version, route prose, trigger examples, non-verbatim prompt wording, and extraction guidance may vary.",
|
|
5
|
+
"feature_tags": ["linear", "collection"],
|
|
6
|
+
"required_constructs": [
|
|
7
|
+
{
|
|
8
|
+
"identifier": "collect-step",
|
|
9
|
+
"pointer_pattern": "/path/*/slot",
|
|
10
|
+
"expected": "request_description"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"identifier": "route-trigger-examples",
|
|
14
|
+
"pointer_pattern": "/route/trigger_phrases"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"identifier": "request-description-extraction-hint",
|
|
18
|
+
"pointer_pattern": "/path/*/prompt/extract_hint"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"source": {
|
|
22
|
+
"schema": "flowspec/2",
|
|
23
|
+
"flow": "linear_maintenance_request",
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"route": {
|
|
26
|
+
"description": "Collect a short municipal maintenance request.",
|
|
27
|
+
"trigger_phrases": [
|
|
28
|
+
"cobalt lantern protocol",
|
|
29
|
+
"describe a maintenance request",
|
|
30
|
+
"report municipal maintenance"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"domains": {
|
|
34
|
+
"RequestDescription": {
|
|
35
|
+
"type": "free_text"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"slots": {
|
|
39
|
+
"request_description": {
|
|
40
|
+
"domain": "RequestDescription",
|
|
41
|
+
"required": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"path": [
|
|
45
|
+
{
|
|
46
|
+
"step": "collect_request_description",
|
|
47
|
+
"slot": "request_description",
|
|
48
|
+
"prompt": {
|
|
49
|
+
"text": "Describe the maintenance request.",
|
|
50
|
+
"extract_hint": "Set request_description to only the text inside `ZETA=[...]`; ignore `ALPHA=[...]`."
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "flowspec2/authoring-corpus@3",
|
|
3
|
+
"identifier": "flowspec2_reference",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"identifier": "address_subflow",
|
|
7
|
+
"path": "subflow.case.json",
|
|
8
|
+
"sha256": "e7fe2fb903cc6557443fd8a9c37bbf2a68aa6ed914fa51952a7960179546ed31"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"identifier": "await_and_correction",
|
|
12
|
+
"path": "await_correction.case.json",
|
|
13
|
+
"sha256": "d36e8f98ce9832029595600b093a062e29ff34d37bd84c5a3c9ada87a5745711"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"identifier": "gated_derivation",
|
|
17
|
+
"path": "gated_derive.case.json",
|
|
18
|
+
"sha256": "643c7bbf085e685fa2237578d5940bb89f1343f44d92bdc9d4033c53bc0259cd"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"identifier": "linear_collection",
|
|
22
|
+
"path": "linear.case.json",
|
|
23
|
+
"sha256": "dbdb6535bfba1400513748fbbdd7527dcd3db6025492c166e0674cbce1e11048"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"identifier": "terminal_fulfillment",
|
|
27
|
+
"path": "terminal.case.json",
|
|
28
|
+
"sha256": "988b703c5d595346b9a3c0c0fdded54aea74b02d967a6c3330553c183d18da91"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "flowspec2/authoring-case@3",
|
|
3
|
+
"identifier": "address_subflow",
|
|
4
|
+
"prompt": "Author an ordered flow that first runs collect_reference_note for optional reference_note in the optional free-text ReferenceNote domain, then delegates address collection to address@1. Declare address@1 exactly once with required and needs_confirmation both true. Add no other semantic behavior. Flow identity, version, route prose, and non-verbatim prompt wording may vary.",
|
|
5
|
+
"feature_tags": ["subflow", "address"],
|
|
6
|
+
"required_constructs": [
|
|
7
|
+
{
|
|
8
|
+
"identifier": "subflow-anchor",
|
|
9
|
+
"pointer_pattern": "/path/*/use",
|
|
10
|
+
"expected": "address@1"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"identifier": "subflow-declaration",
|
|
14
|
+
"pointer_pattern": "/uses/*/ref",
|
|
15
|
+
"expected": "address@1"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"identifier": "subflow-configuration",
|
|
19
|
+
"pointer_pattern": "/uses/*/with",
|
|
20
|
+
"expected": {
|
|
21
|
+
"required": true,
|
|
22
|
+
"needs_confirmation": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"source": {
|
|
27
|
+
"schema": "flowspec/2",
|
|
28
|
+
"flow": "address_collection",
|
|
29
|
+
"version": "1.0.0",
|
|
30
|
+
"route": {
|
|
31
|
+
"description": "Collect and confirm the address for a municipal request."
|
|
32
|
+
},
|
|
33
|
+
"domains": {
|
|
34
|
+
"ReferenceNote": {
|
|
35
|
+
"type": "free_text",
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"slots": {
|
|
40
|
+
"reference_note": {
|
|
41
|
+
"domain": "ReferenceNote"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"path": [
|
|
45
|
+
{
|
|
46
|
+
"step": "collect_reference_note",
|
|
47
|
+
"slot": "reference_note",
|
|
48
|
+
"prompt": {
|
|
49
|
+
"text": "Add an optional nearby reference point."
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"use": "address@1"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"uses": [
|
|
57
|
+
{
|
|
58
|
+
"ref": "address@1",
|
|
59
|
+
"with": {
|
|
60
|
+
"required": true,
|
|
61
|
+
"needs_confirmation": true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "flowspec2/authoring-case@3",
|
|
3
|
+
"identifier": "terminal_fulfillment",
|
|
4
|
+
"prompt": "Author an ordered flow that collects required problem_description through the free-text ProblemDescription domain in collect_problem_description, then executes the open_ticket terminal. Call open_service_request idempotently with problem_description as problem and store result.protocol_id as protocol_id. On success and fatal reset next; on retryable preserve state. Add no other semantic behavior. Flow identity, version, route prose, and non-verbatim prompt wording may vary.",
|
|
5
|
+
"feature_tags": ["terminal", "tool_contract", "idempotency"],
|
|
6
|
+
"required_constructs": [
|
|
7
|
+
{
|
|
8
|
+
"identifier": "terminal-marker",
|
|
9
|
+
"pointer_pattern": "/path/*/terminal",
|
|
10
|
+
"expected": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"identifier": "terminal-tool",
|
|
14
|
+
"pointer_pattern": "/terminal/tool",
|
|
15
|
+
"expected": "open_service_request"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"identifier": "terminal-idempotency",
|
|
19
|
+
"pointer_pattern": "/terminal/idempotent",
|
|
20
|
+
"expected": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"identifier": "terminal-input",
|
|
24
|
+
"pointer_pattern": "/terminal/input",
|
|
25
|
+
"expected": [
|
|
26
|
+
{
|
|
27
|
+
"param": "problem",
|
|
28
|
+
"slot": "problem_description"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"identifier": "terminal-output",
|
|
34
|
+
"pointer_pattern": "/terminal/outputs/protocol_id",
|
|
35
|
+
"expected": "result.protocol_id"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"source": {
|
|
39
|
+
"schema": "flowspec/2",
|
|
40
|
+
"flow": "terminal_ticket_fulfillment",
|
|
41
|
+
"version": "1.0.0",
|
|
42
|
+
"route": {
|
|
43
|
+
"description": "Collect a problem and create a municipal service ticket."
|
|
44
|
+
},
|
|
45
|
+
"domains": {
|
|
46
|
+
"ProblemDescription": {
|
|
47
|
+
"type": "free_text"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"slots": {
|
|
51
|
+
"problem_description": {
|
|
52
|
+
"domain": "ProblemDescription",
|
|
53
|
+
"required": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"path": [
|
|
57
|
+
{
|
|
58
|
+
"step": "collect_problem_description",
|
|
59
|
+
"slot": "problem_description",
|
|
60
|
+
"prompt": {
|
|
61
|
+
"text": "Describe the service problem."
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"terminal": true
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"terminal": {
|
|
69
|
+
"step": "open_ticket",
|
|
70
|
+
"tool": "open_service_request",
|
|
71
|
+
"idempotent": true,
|
|
72
|
+
"input": [
|
|
73
|
+
{
|
|
74
|
+
"param": "problem",
|
|
75
|
+
"slot": "problem_description"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"outputs": {
|
|
79
|
+
"protocol_id": "result.protocol_id"
|
|
80
|
+
},
|
|
81
|
+
"outcomes": {
|
|
82
|
+
"success": {
|
|
83
|
+
"reset_next": true
|
|
84
|
+
},
|
|
85
|
+
"retryable": {
|
|
86
|
+
"preserve_state": true
|
|
87
|
+
},
|
|
88
|
+
"fatal": {
|
|
89
|
+
"reset_next": true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Versioned, packaged benchmark cases for AI flow authoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from importlib.resources import files
|
|
10
|
+
from importlib.resources.abc import Traversable
|
|
11
|
+
from typing import Any, Final, cast
|
|
12
|
+
|
|
13
|
+
from jsonschema import Draft202012Validator
|
|
14
|
+
from jsonschema.exceptions import ValidationError
|
|
15
|
+
|
|
16
|
+
from flowspec2.json_codec import strict_json_loads
|
|
17
|
+
|
|
18
|
+
from .benchmark import AuthoringBenchmarkCase, RequiredFlowConstruct
|
|
19
|
+
|
|
20
|
+
AUTHORING_CASE_FORMAT: Final[str] = "flowspec2/authoring-case@3"
|
|
21
|
+
AUTHORING_CORPUS_FORMAT: Final[str] = "flowspec2/authoring-corpus@3"
|
|
22
|
+
REFERENCE_AUTHORING_CORPUS_ID: Final[str] = "flowspec2_reference"
|
|
23
|
+
|
|
24
|
+
_IDENTIFIER_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
|
25
|
+
_CASE_PATH_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z][a-z0-9_]*\.case\.json$")
|
|
26
|
+
_DIGEST_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[0-9a-f]{64}$")
|
|
27
|
+
_MANIFEST_SCHEMA: Final[dict[str, Any]] = {
|
|
28
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": False,
|
|
31
|
+
"required": ["format", "identifier", "cases"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"format": {"const": AUTHORING_CORPUS_FORMAT},
|
|
34
|
+
"identifier": {"type": "string", "pattern": _IDENTIFIER_PATTERN.pattern},
|
|
35
|
+
"cases": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"minItems": 1,
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": False,
|
|
41
|
+
"required": ["identifier", "path", "sha256"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"identifier": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"pattern": _IDENTIFIER_PATTERN.pattern,
|
|
46
|
+
},
|
|
47
|
+
"path": {"type": "string", "pattern": _CASE_PATH_PATTERN.pattern},
|
|
48
|
+
"sha256": {"type": "string", "pattern": _DIGEST_PATTERN.pattern},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
_CASE_SCHEMA: Final[dict[str, Any]] = {
|
|
55
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": False,
|
|
58
|
+
"required": [
|
|
59
|
+
"format",
|
|
60
|
+
"identifier",
|
|
61
|
+
"prompt",
|
|
62
|
+
"feature_tags",
|
|
63
|
+
"required_constructs",
|
|
64
|
+
"source",
|
|
65
|
+
],
|
|
66
|
+
"properties": {
|
|
67
|
+
"format": {"const": AUTHORING_CASE_FORMAT},
|
|
68
|
+
"identifier": {"type": "string", "pattern": _IDENTIFIER_PATTERN.pattern},
|
|
69
|
+
"prompt": {"type": "string", "minLength": 1},
|
|
70
|
+
"feature_tags": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"uniqueItems": True,
|
|
73
|
+
"items": {"type": "string", "pattern": _IDENTIFIER_PATTERN.pattern},
|
|
74
|
+
},
|
|
75
|
+
"required_constructs": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"additionalProperties": False,
|
|
80
|
+
"required": ["identifier", "pointer_pattern"],
|
|
81
|
+
"properties": {
|
|
82
|
+
"identifier": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"pattern": _IDENTIFIER_PATTERN.pattern,
|
|
85
|
+
},
|
|
86
|
+
"pointer_pattern": {"type": "string", "pattern": r"^/.+"},
|
|
87
|
+
"expected": {},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
"source": {"type": "object"},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
_manifest_validator: Final[Draft202012Validator] = Draft202012Validator(_MANIFEST_SCHEMA)
|
|
95
|
+
_case_validator: Final[Draft202012Validator] = Draft202012Validator(_CASE_SCHEMA)
|
|
96
|
+
_reference_corpus_cache: AuthoringCorpus | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _canonical_json(json_document: object) -> str:
|
|
100
|
+
return json.dumps(
|
|
101
|
+
json_document,
|
|
102
|
+
ensure_ascii=False,
|
|
103
|
+
allow_nan=False,
|
|
104
|
+
separators=(",", ":"),
|
|
105
|
+
sort_keys=True,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _validation_errors(
|
|
110
|
+
json_document: object,
|
|
111
|
+
validator: Draft202012Validator,
|
|
112
|
+
) -> tuple[str, ...]:
|
|
113
|
+
validation_errors = sorted(
|
|
114
|
+
validator.iter_errors(cast(Any, json_document)),
|
|
115
|
+
key=lambda validation_error: (
|
|
116
|
+
tuple(str(path_segment) for path_segment in validation_error.absolute_path),
|
|
117
|
+
validation_error.message,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
return tuple(
|
|
121
|
+
f"{'.'.join(str(path_segment) for path_segment in validation_error.absolute_path) or '<root>'}: "
|
|
122
|
+
f"{validation_error.message}"
|
|
123
|
+
for validation_error in validation_errors
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load_case_document(case_resource: Traversable) -> dict[str, Any]:
|
|
128
|
+
decoded_case = strict_json_loads(case_resource.read_text(encoding="utf-8"))
|
|
129
|
+
if validation_errors := _validation_errors(decoded_case, _case_validator):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"invalid packaged authoring case {case_resource.name}: " + "; ".join(validation_errors)
|
|
132
|
+
)
|
|
133
|
+
return cast(dict[str, Any], decoded_case)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_manifest(corpus_directory: Traversable) -> dict[str, Any]:
|
|
137
|
+
manifest_resource = corpus_directory.joinpath("manifest.json")
|
|
138
|
+
decoded_manifest = strict_json_loads(manifest_resource.read_text(encoding="utf-8"))
|
|
139
|
+
if validation_errors := _validation_errors(decoded_manifest, _manifest_validator):
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"invalid packaged authoring corpus manifest: " + "; ".join(validation_errors)
|
|
142
|
+
)
|
|
143
|
+
return cast(dict[str, Any], decoded_manifest)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _benchmark_case(case_document: dict[str, Any]) -> AuthoringBenchmarkCase:
|
|
147
|
+
required_constructs = tuple(
|
|
148
|
+
(
|
|
149
|
+
RequiredFlowConstruct.expecting(
|
|
150
|
+
cast(str, required_construct["identifier"]),
|
|
151
|
+
cast(str, required_construct["pointer_pattern"]),
|
|
152
|
+
required_construct["expected"],
|
|
153
|
+
)
|
|
154
|
+
if "expected" in required_construct
|
|
155
|
+
else RequiredFlowConstruct(
|
|
156
|
+
identifier=cast(str, required_construct["identifier"]),
|
|
157
|
+
pointer_pattern=cast(str, required_construct["pointer_pattern"]),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
for required_construct in cast(
|
|
161
|
+
list[dict[str, Any]],
|
|
162
|
+
case_document["required_constructs"],
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
return AuthoringBenchmarkCase.expecting_flow(
|
|
166
|
+
identifier=cast(str, case_document["identifier"]),
|
|
167
|
+
prompt=cast(str, case_document["prompt"]),
|
|
168
|
+
expected_flow=case_document["source"],
|
|
169
|
+
feature_tags=tuple(cast(list[str], case_document["feature_tags"])),
|
|
170
|
+
required_constructs=required_constructs,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True)
|
|
175
|
+
class AuthoringCorpus:
|
|
176
|
+
"""Immutable cases and content identity for one benchmark corpus."""
|
|
177
|
+
|
|
178
|
+
format_identifier: str
|
|
179
|
+
case_format_identifier: str
|
|
180
|
+
identifier: str
|
|
181
|
+
digest: str
|
|
182
|
+
cases: tuple[AuthoringBenchmarkCase, ...]
|
|
183
|
+
|
|
184
|
+
def __post_init__(self) -> None:
|
|
185
|
+
if self.format_identifier != AUTHORING_CORPUS_FORMAT:
|
|
186
|
+
raise ValueError("unknown authoring corpus format")
|
|
187
|
+
if self.case_format_identifier != AUTHORING_CASE_FORMAT:
|
|
188
|
+
raise ValueError("unknown authoring case format")
|
|
189
|
+
if not _IDENTIFIER_PATTERN.fullmatch(self.identifier):
|
|
190
|
+
raise ValueError("authoring corpus identifier is invalid")
|
|
191
|
+
if not _DIGEST_PATTERN.fullmatch(self.digest):
|
|
192
|
+
raise ValueError("authoring corpus digest must be lowercase SHA-256")
|
|
193
|
+
case_identifiers = tuple(benchmark_case.identifier for benchmark_case in self.cases)
|
|
194
|
+
if not case_identifiers:
|
|
195
|
+
raise ValueError("authoring corpus requires at least one case")
|
|
196
|
+
if case_identifiers != tuple(sorted(case_identifiers)):
|
|
197
|
+
raise ValueError("authoring corpus cases must be sorted by identifier")
|
|
198
|
+
if len(set(case_identifiers)) != len(case_identifiers):
|
|
199
|
+
raise ValueError("authoring corpus case identifiers must be unique")
|
|
200
|
+
|
|
201
|
+
def metadata(self) -> dict[str, object]:
|
|
202
|
+
"""Return deterministic corpus provenance without reference answers."""
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"format": self.format_identifier,
|
|
206
|
+
"case_format": self.case_format_identifier,
|
|
207
|
+
"identifier": self.identifier,
|
|
208
|
+
"digest": self.digest,
|
|
209
|
+
"case_identifiers": [benchmark_case.identifier for benchmark_case in self.cases],
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _load_authoring_corpus(
|
|
214
|
+
corpus_directory: Traversable,
|
|
215
|
+
*,
|
|
216
|
+
expected_identifier: str,
|
|
217
|
+
) -> AuthoringCorpus:
|
|
218
|
+
manifest_document = _load_manifest(corpus_directory)
|
|
219
|
+
if manifest_document["identifier"] != expected_identifier:
|
|
220
|
+
raise ValueError("authoring corpus manifest identifier is not the expected contract")
|
|
221
|
+
manifest_cases = cast(list[dict[str, Any]], manifest_document["cases"])
|
|
222
|
+
manifest_identifiers = tuple(
|
|
223
|
+
cast(str, case_contract["identifier"]) for case_contract in manifest_cases
|
|
224
|
+
)
|
|
225
|
+
manifest_paths = tuple(cast(str, case_contract["path"]) for case_contract in manifest_cases)
|
|
226
|
+
if manifest_identifiers != tuple(sorted(manifest_identifiers)):
|
|
227
|
+
raise ValueError("authoring corpus manifest cases must be sorted by identifier")
|
|
228
|
+
if len(set(manifest_identifiers)) != len(manifest_identifiers):
|
|
229
|
+
raise ValueError("authoring corpus manifest case identifiers must be unique")
|
|
230
|
+
if len(set(manifest_paths)) != len(manifest_paths):
|
|
231
|
+
raise ValueError("authoring corpus manifest case paths must be unique")
|
|
232
|
+
packaged_case_paths = {
|
|
233
|
+
child_resource.name
|
|
234
|
+
for child_resource in corpus_directory.iterdir()
|
|
235
|
+
if child_resource.is_file() and child_resource.name.endswith(".case.json")
|
|
236
|
+
}
|
|
237
|
+
if packaged_case_paths != set(manifest_paths):
|
|
238
|
+
missing_paths = sorted(set(manifest_paths) - packaged_case_paths)
|
|
239
|
+
unlisted_paths = sorted(packaged_case_paths - set(manifest_paths))
|
|
240
|
+
raise ValueError(
|
|
241
|
+
"authoring corpus manifest membership mismatch: "
|
|
242
|
+
f"missing={missing_paths}, unlisted={unlisted_paths}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
case_documents: list[dict[str, Any]] = []
|
|
246
|
+
for case_contract in manifest_cases:
|
|
247
|
+
case_path = cast(str, case_contract["path"])
|
|
248
|
+
case_resource = corpus_directory.joinpath(case_path)
|
|
249
|
+
serialized_case = case_resource.read_bytes()
|
|
250
|
+
observed_digest = hashlib.sha256(serialized_case).hexdigest()
|
|
251
|
+
expected_digest = cast(str, case_contract["sha256"])
|
|
252
|
+
if observed_digest != expected_digest:
|
|
253
|
+
raise ValueError(f"packaged authoring case failed integrity verification: {case_path}")
|
|
254
|
+
case_document = _load_case_document(case_resource)
|
|
255
|
+
if case_document["identifier"] != case_contract["identifier"]:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f"packaged authoring case identifier disagrees with manifest: {case_path}"
|
|
258
|
+
)
|
|
259
|
+
case_documents.append(case_document)
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
benchmark_cases = tuple(
|
|
263
|
+
sorted(
|
|
264
|
+
(_benchmark_case(case_document) for case_document in case_documents),
|
|
265
|
+
key=lambda benchmark_case: benchmark_case.identifier,
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
except (ValidationError, ValueError) as case_contract_error:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
"authoring corpus contains an invalid reference flow"
|
|
271
|
+
) from case_contract_error
|
|
272
|
+
contract_document = {
|
|
273
|
+
"format": AUTHORING_CORPUS_FORMAT,
|
|
274
|
+
"case_format": AUTHORING_CASE_FORMAT,
|
|
275
|
+
"identifier": manifest_document["identifier"],
|
|
276
|
+
"cases": case_documents,
|
|
277
|
+
"resolved_acceptance": [
|
|
278
|
+
{
|
|
279
|
+
"identifier": benchmark_case.identifier,
|
|
280
|
+
"contract": benchmark_case.authoring_task().acceptance.to_dict(),
|
|
281
|
+
}
|
|
282
|
+
for benchmark_case in benchmark_cases
|
|
283
|
+
],
|
|
284
|
+
}
|
|
285
|
+
corpus_digest = hashlib.sha256(_canonical_json(contract_document).encode("utf-8")).hexdigest()
|
|
286
|
+
return AuthoringCorpus(
|
|
287
|
+
format_identifier=AUTHORING_CORPUS_FORMAT,
|
|
288
|
+
case_format_identifier=AUTHORING_CASE_FORMAT,
|
|
289
|
+
identifier=cast(str, manifest_document["identifier"]),
|
|
290
|
+
digest=corpus_digest,
|
|
291
|
+
cases=benchmark_cases,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def load_reference_authoring_corpus() -> AuthoringCorpus:
|
|
296
|
+
"""Load and validate the immutable corpus distributed with flowspec2."""
|
|
297
|
+
|
|
298
|
+
global _reference_corpus_cache
|
|
299
|
+
if _reference_corpus_cache is not None:
|
|
300
|
+
return _reference_corpus_cache
|
|
301
|
+
|
|
302
|
+
corpus_directory = files("flowspec2.authoring").joinpath("corpus")
|
|
303
|
+
_reference_corpus_cache = _load_authoring_corpus(
|
|
304
|
+
corpus_directory,
|
|
305
|
+
expected_identifier=REFERENCE_AUTHORING_CORPUS_ID,
|
|
306
|
+
)
|
|
307
|
+
return _reference_corpus_cache
|