Agentic-GenUI-Tools 0.2.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.
- adaptive_card_tools/__init__.py +10 -0
- adaptive_card_tools/builder.py +1891 -0
- adaptive_card_tools/catalog.py +488 -0
- adaptive_card_tools/examples.py +169 -0
- adaptive_card_tools/normalize.py +581 -0
- adaptive_card_tools/py.typed +0 -0
- adaptive_card_tools/theme.py +210 -0
- adaptive_card_tools/tool.py +142 -0
- agentic_genui_tools-0.2.0.dist-info/METADATA +284 -0
- agentic_genui_tools-0.2.0.dist-info/RECORD +13 -0
- agentic_genui_tools-0.2.0.dist-info/WHEEL +5 -0
- agentic_genui_tools-0.2.0.dist-info/licenses/LICENSE +21 -0
- agentic_genui_tools-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""The element vocabulary: canonical types, the many names models use for them,
|
|
2
|
+
field aliases, and inference of a missing or unknown type from an element's fields.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from .normalize import Context, as_number, as_url, closest, is_slug_like, token
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Spec:
|
|
14
|
+
kind: str # display | input | layout
|
|
15
|
+
main: str # field that receives a bare value, e.g. {"facts": {...}}
|
|
16
|
+
fields: dict[str, tuple[str, ...]]
|
|
17
|
+
doc: str
|
|
18
|
+
example: dict = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _f(**fields: str) -> dict[str, tuple[str, ...]]:
|
|
22
|
+
return {name: tuple(aliases.split()) for name, aliases in fields.items()}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
INPUT = dict(
|
|
26
|
+
label="question prompt title text caption display_name displayname header heading field_label",
|
|
27
|
+
id="input_id field_id field key identifier ref variable var field_name input_name",
|
|
28
|
+
required="is_required isrequired mandatory must",
|
|
29
|
+
placeholder="hint placeholder_text watermark example sample",
|
|
30
|
+
value="default default_value initial initial_value prefill preset selected selected_value answer current",
|
|
31
|
+
error_message="validation_message error_text error invalid_message errormessage",
|
|
32
|
+
)
|
|
33
|
+
BOUNDS = dict(min="minimum min_value minvalue lower lower_bound floor from start earliest min_date min_time after",
|
|
34
|
+
max="maximum max_value maxvalue upper upper_bound ceiling to end latest max_date max_time before")
|
|
35
|
+
TITLE = "heading header caption name"
|
|
36
|
+
|
|
37
|
+
SPECS: dict[str, Spec] = {
|
|
38
|
+
"heading": Spec("display", "text", _f(text="title heading label content value name header string"),
|
|
39
|
+
"Section heading.", {"type": "heading", "text": "Summary"}),
|
|
40
|
+
"text": Spec("display", "text", _f(text="content body value message paragraph markdown md string copy inlines", title=TITLE),
|
|
41
|
+
"Paragraph. **bold**, _italic_ and [links](https://…) work.", {"type": "text", "text": "Your order has shipped."}),
|
|
42
|
+
"note": Spec("display", "text", _f(text="content body value message string copy"),
|
|
43
|
+
"Small grey print (disclaimer, footnote).", {"type": "note", "text": "Prices exclude tax."}),
|
|
44
|
+
"list": Spec("display", "items", _f(items="list bullets points entries values lines steps elements rows options texts",
|
|
45
|
+
numbered="ordered is_ordered numbering", title=TITLE),
|
|
46
|
+
"Bulleted list; numbered=true for steps.", {"type": "list", "items": ["Fast setup", "No card required"]}),
|
|
47
|
+
"facts": Spec("display", "facts", _f(facts="items data details properties attributes specs pairs fields rows values entries info kv key_values", title=TITLE),
|
|
48
|
+
"Label/value pairs.", {"type": "facts", "facts": {"Owner": "Dana", "Due": "2026-10-01"}}),
|
|
49
|
+
"table": Spec("display", "rows", _f(rows="data records items entries body values products lines list",
|
|
50
|
+
columns="headers header fields keys cols head column_names labels", title=TITLE),
|
|
51
|
+
"Rows of records; columns are inferred.", {"type": "table", "rows": [{"Item": "Laptop", "Qty": 2}, {"Item": "Monitor", "Qty": 3}]}),
|
|
52
|
+
"metrics": Spec("display", "items", _f(items="metrics kpis stats values data figures numbers entries cards tiles", title=TITLE,
|
|
53
|
+
label="metric kpi stat key", value="amount number figure count total score current_value val result",
|
|
54
|
+
change="delta diff difference trend growth variance pct_change percent_change movement change_percent",
|
|
55
|
+
note="period context subtext secondary comparison target description subtitle",
|
|
56
|
+
lower_is_better="good_direction inverse invert better"),
|
|
57
|
+
"Big-number KPI tiles; change like \"+8%\" is coloured.", {"type": "metrics", "items": [{"label": "Revenue", "value": "$1.2M", "change": "+8%"}]}),
|
|
58
|
+
"badge": Spec("display", "text", _f(text="label title value name content", tone="status state color colour style variant severity level intent appearance"),
|
|
59
|
+
"Status pill. tone: success|warning|danger|info|neutral (inferred if omitted).", {"type": "badge", "text": "Approved", "tone": "success"}),
|
|
60
|
+
"image": Spec("display", "url", _f(url="src source href link uri image image_url imageurl img path picture photo",
|
|
61
|
+
alt="alt_text alttext alternate", caption="caption_text description text label subtitle legend",
|
|
62
|
+
title="heading header name", size="scale dimension", shape="style rounded crop"),
|
|
63
|
+
"Image from an https URL you were given.", {"type": "image", "url": "https://example.com/photo.png", "alt": "Product photo"}),
|
|
64
|
+
"images": Spec("display", "images", _f(images="urls items photos pictures gallery sources src thumbnails", title=TITLE),
|
|
65
|
+
"Image gallery.", {"type": "images", "images": ["https://example.com/a.png", "https://example.com/b.png"]}),
|
|
66
|
+
"media": Spec("display", "sources", _f(sources="source url src video audio urls file link href", poster="thumbnail cover preview image poster_url",
|
|
67
|
+
alt="alt_text description", title=TITLE),
|
|
68
|
+
"Video/audio player.", {"type": "media", "sources": "https://example.com/video.mp4"}),
|
|
69
|
+
"chart": Spec("display", "data", _f(chart_type="chart charttype graph graph_type subtype variant style chart_style kind_of_chart",
|
|
70
|
+
data="values points dataset data_points datapoints items records",
|
|
71
|
+
series="datasets lines groups bars", labels="categories x x_values xvalues keys names",
|
|
72
|
+
title=TITLE, x_label="x_axis x_axis_title x_title x_axis_label xname x_name xaxis",
|
|
73
|
+
y_label="y_axis y_axis_title y_title y_axis_label yname y_name yaxis unit units",
|
|
74
|
+
value="current reading", min="minimum", max="maximum target total",
|
|
75
|
+
segments="ranges bands zones thresholds"),
|
|
76
|
+
"chart_type: bar|horizontal_bar|line|pie|donut|stacked_bar|grouped_bar|gauge.",
|
|
77
|
+
{"type": "chart", "chart_type": "bar", "title": "Orders", "data": {"Jan": 120, "Feb": 150, "Mar": 170}}),
|
|
78
|
+
"progress": Spec("display", "value", _f(label="title name text caption", value="progress percent percentage current completed done complete pct amount",
|
|
79
|
+
max="total target goal of out_of maximum"),
|
|
80
|
+
"Progress bar; value is a percent unless max is given.", {"type": "progress", "label": "Migration", "value": 70}),
|
|
81
|
+
"spinner": Spec("display", "label", _f(label="title text message"), "Loading indicator.", {"type": "spinner", "label": "Loading"}),
|
|
82
|
+
"rating": Spec("display", "value", _f(value="rating score stars average avg rate", max="out_of scale max_stars of best maximum",
|
|
83
|
+
count="votes reviews review_count num_ratings ratings number_of_reviews n", title="label heading name"),
|
|
84
|
+
"Star rating (read-only).", {"type": "rating", "value": 4.5, "count": 120}),
|
|
85
|
+
"divider": Spec("display", "", {}, "Thin line between parts of the card.", {"type": "divider"}),
|
|
86
|
+
"section": Spec("layout", "elements", _f(title="heading header label name", elements="items body children content components fields widgets blocks contents questions"),
|
|
87
|
+
"Titled group of elements.", {"type": "section", "title": "Details", "elements": [{"type": "text", "text": "…"}]}),
|
|
88
|
+
"columns": Spec("layout", "columns", _f(columns="cols items children panels cards cells sections groups products plans tiers"),
|
|
89
|
+
"Side-by-side tiles, each {title, elements}.",
|
|
90
|
+
{"type": "columns", "columns": [{"title": "Basic", "elements": [{"type": "facts", "facts": {"Price": "$10"}}]},
|
|
91
|
+
{"title": "Pro", "elements": [{"type": "facts", "facts": {"Price": "$20"}}]}]}),
|
|
92
|
+
"buttons": Spec("layout", "buttons", _f(buttons="actions items links ctas", label="title text name caption",
|
|
93
|
+
url="href link uri target open_url openurl website destination",
|
|
94
|
+
data="payload submit_data params parameters body value",
|
|
95
|
+
action="on_click onclick click behavior action_type kind", style="tone variant color appearance intent importance priority"),
|
|
96
|
+
"Buttons: with url opens a link, without url submits.",
|
|
97
|
+
{"type": "buttons", "buttons": [{"label": "Approve"}, {"label": "View details", "url": "https://example.com/req/42"}]}),
|
|
98
|
+
"text_input": Spec("input", "label", _f(**INPUT, multiline="is_multiline ismultiline multi_line textarea long long_text",
|
|
99
|
+
max_length="maxlength max_chars character_limit max_characters char_limit limit length max_len",
|
|
100
|
+
format="input_format text_format input_type inputtype style subtype variant mode",
|
|
101
|
+
regex="pattern validation validation_regex"),
|
|
102
|
+
"Text box. format: email|tel|url|password; multiline for long answers.",
|
|
103
|
+
{"type": "text_input", "label": "Email", "id": "email", "format": "email", "required": True}),
|
|
104
|
+
"number_input": Spec("input", "label", _f(**INPUT, **BOUNDS, unit="units currency suffix prefix measure"),
|
|
105
|
+
"Number box.", {"type": "number_input", "label": "Quantity", "min": 1}),
|
|
106
|
+
"date_input": Spec("input", "label", _f(**INPUT, **BOUNDS), "Date picker (any common date format).", {"type": "date_input", "label": "Start date"}),
|
|
107
|
+
"time_input": Spec("input", "label", _f(**INPUT, **BOUNDS), "Time picker (9:30, 2pm…).", {"type": "time_input", "label": "Meeting time"}),
|
|
108
|
+
"datetime_input": Spec("input", "label", _f(**INPUT, **BOUNDS), "Date and time pickers side by side.", {"type": "datetime_input", "label": "Pickup"}),
|
|
109
|
+
"choice": Spec("input", "options", _f(**INPUT, options="choices items values list selections answers enum option_list entries",
|
|
110
|
+
multiple="multiselect is_multi_select ismultiselect allow_multiple multi select_multiple many multi_select allowmultiple",
|
|
111
|
+
display="style presentation appearance variant widget ui layout control render_as display_as"),
|
|
112
|
+
"Pick from options. multiple=true for several; display: radio|dropdown.",
|
|
113
|
+
{"type": "choice", "label": "Plan", "options": ["Basic", "Pro", "Enterprise"]}),
|
|
114
|
+
"checkbox": Spec("input", "label", _f(**{**INPUT, "value": "checked is_checked default selected state default_value initial"},
|
|
115
|
+
on_value="value_on valueon checked_value true_value", off_value="value_off valueoff unchecked_value false_value"),
|
|
116
|
+
"Single yes/no checkbox.", {"type": "checkbox", "label": "I agree to the terms", "required": True}),
|
|
117
|
+
"rating_input": Spec("input", "label", _f(**INPUT, max="max_stars stars out_of scale maximum of",
|
|
118
|
+
half_steps="allow_half_steps allowhalfsteps half half_stars allow_half"),
|
|
119
|
+
"Star rating the user can pick.", {"type": "rating_input", "label": "How was your visit?"}),
|
|
120
|
+
# Internal pseudo-types reached through aliases; never documented to the model.
|
|
121
|
+
"column": Spec("layout", "elements", _f(title="heading header label name", elements="items body children content components blocks widgets"), ""),
|
|
122
|
+
"document": Spec("layout", "elements", {}, ""),
|
|
123
|
+
}
|
|
124
|
+
DOCUMENTED = [k for k, s in SPECS.items() if s.doc]
|
|
125
|
+
|
|
126
|
+
TYPE_NAMES = {
|
|
127
|
+
"heading": "heading header headline title h1 h2 h3 h4 h5 h6 subheading subheader sectiontitle sectionheader",
|
|
128
|
+
"text": "text textblock paragraph p para markdown md string content message description richtext richtextblock span label copy prose body summary intro introduction codeblock tagline",
|
|
129
|
+
"note": "note caption footnote footer disclaimer fineprint smallprint subtle helptext remark aside small hint",
|
|
130
|
+
"list": "list bullets bulletlist bulletedlist bulletpoints ul unorderedlist itemlist",
|
|
131
|
+
"facts": "facts factset fact keyvalue keyvalues keyvaluelist properties details specs specifications attributes definitionlist dl kv summarylist",
|
|
132
|
+
"table": "table datatable grid datagrid spreadsheet comparison compare comparisontable matrix",
|
|
133
|
+
"metrics": "metrics metric kpi kpis stat stats statistic statistics scorecard figure figures counter kpicard statcard kpitile",
|
|
134
|
+
"badge": "badge tag pill chip status statusbadge lozenge statuspill",
|
|
135
|
+
"image": "image img photo picture pic logo avatar thumbnail hero banner icon",
|
|
136
|
+
"images": "images imageset gallery carousel photos pictures thumbnails imagegallery",
|
|
137
|
+
"media": "media video audio player videoplayer",
|
|
138
|
+
"chart": "chart graph plot visualization visualisation viz datachart",
|
|
139
|
+
"progress": "progress progressbar completion loadingbar",
|
|
140
|
+
"spinner": "spinner progressring loading loader busy",
|
|
141
|
+
"rating": "rating stars starrating reviews",
|
|
142
|
+
"divider": "divider separator hr rule spacer horizontalrule",
|
|
143
|
+
"section": "section group container panel box block fieldset stack stackpanel vstack div area region well wrapper subform",
|
|
144
|
+
"columns": "columns columnset row hstack sidebyside split layout grouprow",
|
|
145
|
+
"column": "column col",
|
|
146
|
+
"buttons": "buttons button actions action actionset cta ctas buttongroup buttonrow toolbar",
|
|
147
|
+
"text_input": "textinput inputtext input textfield textbox field entry freetext shorttext shortanswer answer",
|
|
148
|
+
"number_input": "numberinput inputnumber number numeric integer int float decimal quantity slider stepper spinbutton range amountinput",
|
|
149
|
+
"date_input": "dateinput inputdate date datepicker calendar dateselector dob birthday",
|
|
150
|
+
"time_input": "timeinput inputtime time timepicker",
|
|
151
|
+
"datetime_input": "datetime datetimeinput datetimepicker timestamp",
|
|
152
|
+
"choice": "choice choices choiceset inputchoiceset options option question multiplechoice selection choiceinput",
|
|
153
|
+
"checkbox": "checkbox toggle switch boolean bool consent agree agreement acceptance tickbox inputtoggle confirm confirmation",
|
|
154
|
+
"rating_input": "ratinginput inputrating starinput starpicker starrating_input feedbackrating ratingpicker",
|
|
155
|
+
"document": "adaptivecard card document form page survey questionnaire dashboard report",
|
|
156
|
+
}
|
|
157
|
+
# Names that also imply a field value, e.g. "radio" is a choice displayed as radio buttons.
|
|
158
|
+
IMPLIED = [
|
|
159
|
+
("choice", {"display": "radio"}, "radio radiogroup radiobuttons radiobutton radioinput singleselect singlechoice radiobuttongroup"),
|
|
160
|
+
("choice", {"display": "dropdown"}, "dropdown select selectbox selectinput combobox combo picker dropdowninput inputdropdown dropdownlist selectlist menu"),
|
|
161
|
+
("choice", {"multiple": True}, "multiselect multichoice multipleselect checkboxes checkboxgroup checkboxlist checklist multiselectinput"),
|
|
162
|
+
("choice", {"options": ["Yes", "No"], "display": "radio"}, "yesno yesnoquestion boolchoice"),
|
|
163
|
+
("text_input", {"multiline": True}, "textarea multilinetext longtext multiline paragraphinput comment comments longanswer"),
|
|
164
|
+
("text_input", {"format": "email"}, "email emailinput emailfield emailaddress"),
|
|
165
|
+
("text_input", {"format": "tel"}, "phone tel telephone phoneinput phonenumber mobile"),
|
|
166
|
+
("text_input", {"format": "url"}, "urlinput urlfield website"),
|
|
167
|
+
("text_input", {"format": "password"}, "password passwordinput secret"),
|
|
168
|
+
("text_input", {"format": "url", "_unsupported": "file upload"}, "file fileupload upload attachment fileinput"),
|
|
169
|
+
("list", {"numbered": True}, "numberedlist orderedlist ol steps stepslist"),
|
|
170
|
+
("chart", {"chart_type": "bar"}, "bar barchart verticalbar verticalbarchart columnchart chartverticalbar histogram"),
|
|
171
|
+
("chart", {"chart_type": "horizontal_bar"}, "horizontalbar hbar horizontalbarchart barh charthorizontalbar"),
|
|
172
|
+
("chart", {"chart_type": "line"}, "line linechart linegraph trend trendline timeseries area areachart sparkline chartline"),
|
|
173
|
+
("chart", {"chart_type": "pie"}, "pie piechart chartpie"),
|
|
174
|
+
("chart", {"chart_type": "donut"}, "donut doughnut donutchart doughnutchart ring chartdonut"),
|
|
175
|
+
("chart", {"chart_type": "stacked_bar"}, "stackedbar stackedbarchart stackedcolumn stackedcolumnchart chartverticalbarstacked"),
|
|
176
|
+
("chart", {"chart_type": "stacked_horizontal_bar"}, "stackedhorizontalbar horizontalstackedbar charthorizontalbarstacked"),
|
|
177
|
+
("chart", {"chart_type": "grouped_bar"}, "groupedbar groupedbarchart clusteredbar clusteredcolumn multibar chartverticalbargrouped"),
|
|
178
|
+
("chart", {"chart_type": "gauge"}, "gauge gaugechart meter dial speedometer chartgauge"),
|
|
179
|
+
("buttons", {"action": "submit"}, "submit submitbutton actionsubmit actionexecute execute"),
|
|
180
|
+
("buttons", {"action": "open_url"}, "link hyperlink openurl actionopenurl linkbutton"),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
TYPE_ALIASES: dict[str, tuple[str, dict]] = {}
|
|
184
|
+
for _canonical, _names in TYPE_NAMES.items():
|
|
185
|
+
for _name in [_canonical, *_names.split()]:
|
|
186
|
+
TYPE_ALIASES.setdefault(token(_name), (_canonical, {}))
|
|
187
|
+
for _canonical, _implied, _names in IMPLIED:
|
|
188
|
+
for _name in _names.split():
|
|
189
|
+
TYPE_ALIASES[token(_name)] = (_canonical, _implied)
|
|
190
|
+
|
|
191
|
+
# Words searched inside unknown type names such as "EmailField" or "RevenueBarGraph".
|
|
192
|
+
_INPUT_WORDS = ("input", "field", "picker", "entry", "textbox", "selector")
|
|
193
|
+
KEYWORDS = [
|
|
194
|
+
(("chart", "graph", "plot", "viz"), "chart"),
|
|
195
|
+
(("button", "action", "cta"), "buttons"),
|
|
196
|
+
(("kpi", "metric", "stat", "scorecard"), "metrics"),
|
|
197
|
+
(("table", "grid"), "table"),
|
|
198
|
+
(("fact", "keyvalue", "propert", "spec"), "facts"),
|
|
199
|
+
(("gallery", "images", "photos"), "images"),
|
|
200
|
+
(("image", "img", "photo", "picture", "logo", "avatar", "thumbnail", "banner"), "image"),
|
|
201
|
+
(("video", "audio", "media"), "media"),
|
|
202
|
+
(("bullet", "list"), "list"),
|
|
203
|
+
(("badge", "tag", "pill", "chip", "status"), "badge"),
|
|
204
|
+
(("progress",), "progress"),
|
|
205
|
+
(("rating", "star"), "rating"),
|
|
206
|
+
(("divider", "separator", "spacer"), "divider"),
|
|
207
|
+
(("heading", "header", "title"), "heading"),
|
|
208
|
+
(("columns", "row"), "columns"),
|
|
209
|
+
(("section", "group", "container", "panel", "card", "box", "block", "stack", "wrapper"), "section"),
|
|
210
|
+
(("text", "paragraph", "label", "description", "markdown", "message", "content"), "text"),
|
|
211
|
+
]
|
|
212
|
+
CHART_WORDS = [("donut", "donut"), ("doughnut", "donut"), ("pie", "pie"), ("gauge", "gauge"), ("line", "line"),
|
|
213
|
+
("area", "line"), ("trend", "line"), ("stackedhorizontal", "stacked_horizontal_bar"),
|
|
214
|
+
("stack", "stacked_bar"), ("group", "grouped_bar"), ("cluster", "grouped_bar"),
|
|
215
|
+
("horizontal", "horizontal_bar"), ("bar", "bar"), ("column", "bar")]
|
|
216
|
+
INPUT_WORDS = [("datetime", ("datetime_input", {})), ("date", ("date_input", {})), ("time", ("time_input", {})),
|
|
217
|
+
("email", ("text_input", {"format": "email"})), ("phone", ("text_input", {"format": "tel"})),
|
|
218
|
+
("password", ("text_input", {"format": "password"})), ("url", ("text_input", {"format": "url"})),
|
|
219
|
+
("number", ("number_input", {})), ("numeric", ("number_input", {})), ("amount", ("number_input", {})),
|
|
220
|
+
("rating", ("rating_input", {})), ("star", ("rating_input", {})),
|
|
221
|
+
("check", ("checkbox", {})), ("toggle", ("checkbox", {})), ("switch", ("checkbox", {})),
|
|
222
|
+
("radio", ("choice", {"display": "radio"})), ("dropdown", ("choice", {"display": "dropdown"})),
|
|
223
|
+
("select", ("choice", {})), ("choice", ("choice", {})), ("option", ("choice", {})),
|
|
224
|
+
("textarea", ("text_input", {"multiline": True})), ("multiline", ("text_input", {"multiline": True}))]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def resolve_type(name, ctx: Context, path: str) -> tuple[str, dict] | None:
|
|
228
|
+
"""Canonical type for a type name: exact alias, typo, then keywords."""
|
|
229
|
+
if not isinstance(name, str) or not name.strip():
|
|
230
|
+
return None
|
|
231
|
+
t = token(name)
|
|
232
|
+
if t in TYPE_ALIASES:
|
|
233
|
+
result = TYPE_ALIASES[t]
|
|
234
|
+
if name != result[0]:
|
|
235
|
+
ctx.fixed(path, "type_alias", source=name, target=result[0])
|
|
236
|
+
return result
|
|
237
|
+
near = closest(t, TYPE_ALIASES, ctx)
|
|
238
|
+
if near:
|
|
239
|
+
ctx.fixed(path, "type_typo", source=name, target=TYPE_ALIASES[near][0])
|
|
240
|
+
return TYPE_ALIASES[near]
|
|
241
|
+
result = None
|
|
242
|
+
if any(w in t for w in _INPUT_WORDS):
|
|
243
|
+
result = next((r for w, r in INPUT_WORDS if w in t), ("text_input", {}))
|
|
244
|
+
else:
|
|
245
|
+
for words, canonical in KEYWORDS:
|
|
246
|
+
if any(w in t for w in words):
|
|
247
|
+
implied = {}
|
|
248
|
+
if canonical == "chart":
|
|
249
|
+
implied = next(({"chart_type": v} for w, v in CHART_WORDS if w in t), {})
|
|
250
|
+
result = (canonical, implied)
|
|
251
|
+
break
|
|
252
|
+
if result:
|
|
253
|
+
ctx.fixed(path, "type_guessed_from_name", source=name, target=result[0])
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------- fields
|
|
258
|
+
|
|
259
|
+
TYPE_KEYS = {"type", "kind", "component", "componenttype", "elementtype", "element", "widget", "control",
|
|
260
|
+
"atype", "typename", "blocktype", "fieldtype", "inputtype", "as"}
|
|
261
|
+
STYLE_KEYS = {token(k) for k in """
|
|
262
|
+
size weight color colour isSubtle subtle spacing separator horizontalAlignment alignment align textAlign
|
|
263
|
+
verticalAlignment verticalContentAlignment width height minHeight maxHeight style bleed fontType font fontSize
|
|
264
|
+
fontWeight fontFamily wrap maxLines backgroundImage background backgroundColor bgColor selectAction fallback
|
|
265
|
+
fallbackText requires isVisible visible hidden $schema version $when msteams targetWidth lang speak rtl padding
|
|
266
|
+
margin border borderRadius rounded roundedCorners shadow elevation class className css theme emphasis iconUrl
|
|
267
|
+
tooltip isEnabled enabled disabled mode associatedInputs gridStyle showGridLines firstRowAsHeader firstRowAsHeaders
|
|
268
|
+
horizontalCellContentAlignment verticalCellContentAlignment imageSize pixelWidth pixelHeight order index uuid
|
|
269
|
+
testId aria ariaLabel role visibility opacity position animation italic bold underline strikethrough highlight
|
|
270
|
+
monospace fontStyle textStyle textColor textSize valueFormat showTitle colorSet showLegend legendPosition level
|
|
271
|
+
stacked horizontal orientation step increment isInline inline columnsCount span colspan rowspan layoutHint
|
|
272
|
+
ordering variantStyle labelPosition labelWidth inputStyle choicesData isHidden debug comment_for_developer
|
|
273
|
+
""".split()}
|
|
274
|
+
CONTENT_KEYS = {token(k) for k in """
|
|
275
|
+
subtitle subheading description desc summary details detail note notes body content message caption footer
|
|
276
|
+
footnote info remark remarks explanation context help helpText tip tips disclaimer text subtext tagline byline
|
|
277
|
+
instructions hint
|
|
278
|
+
""".split()}
|
|
279
|
+
ICON_KEYS = {"icon", "emoji"}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class Fields:
|
|
284
|
+
values: dict
|
|
285
|
+
hints: dict # style properties that carry meaning (size, weight, stacked...)
|
|
286
|
+
extras: list # (key, value) content found under unrecognized keys; rendered as notes
|
|
287
|
+
unknown: list # (key, value) that could not be placed
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def alias_map(canonical: str) -> dict[str, str]:
|
|
291
|
+
spec = SPECS[canonical]
|
|
292
|
+
mapping = {}
|
|
293
|
+
for name, aliases in spec.fields.items():
|
|
294
|
+
mapping.setdefault(token(name), name)
|
|
295
|
+
for name, aliases in spec.fields.items():
|
|
296
|
+
for alias in aliases:
|
|
297
|
+
mapping.setdefault(token(alias), name)
|
|
298
|
+
return mapping
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
FIELD_ALIASES = {name: alias_map(name) for name in SPECS}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def map_fields(canonical: str, raw: dict, path: str, ctx: Context, skip=()) -> Fields:
|
|
305
|
+
spec = SPECS[canonical]
|
|
306
|
+
mapping = FIELD_ALIASES[canonical]
|
|
307
|
+
out = Fields({}, {}, [], [])
|
|
308
|
+
exact = {}
|
|
309
|
+
for key, value in raw.items():
|
|
310
|
+
if key in skip:
|
|
311
|
+
continue
|
|
312
|
+
t = token(key)
|
|
313
|
+
target = mapping.get(t)
|
|
314
|
+
if target is None and t == "name" and spec.kind == "input":
|
|
315
|
+
target = "id" if is_slug_like(value) and " " not in str(value) else "label"
|
|
316
|
+
if target in out.values:
|
|
317
|
+
target = "label" if target == "id" else None
|
|
318
|
+
if target is None and t in {"optional", "isoptional"} and spec.kind == "input":
|
|
319
|
+
out.hints["optional"] = value
|
|
320
|
+
continue
|
|
321
|
+
if target is None and t in ICON_KEYS:
|
|
322
|
+
out.hints["icon"] = value
|
|
323
|
+
continue
|
|
324
|
+
if target is None and t in STYLE_KEYS:
|
|
325
|
+
out.hints[t] = value
|
|
326
|
+
ctx.fixed(f"{path}.{key}", "ignored_style_key")
|
|
327
|
+
continue
|
|
328
|
+
if target is None and t in CONTENT_KEYS:
|
|
329
|
+
if value not in (None, "", [], {}):
|
|
330
|
+
out.extras.append((key, value))
|
|
331
|
+
continue
|
|
332
|
+
if target is None:
|
|
333
|
+
near = closest(t, mapping, ctx)
|
|
334
|
+
if near:
|
|
335
|
+
target = mapping[near]
|
|
336
|
+
ctx.fixed(f"{path}.{key}", "field_typo", target=target)
|
|
337
|
+
if target is None:
|
|
338
|
+
if key != "id" and value not in (None, "", [], {}):
|
|
339
|
+
out.unknown.append((key, value))
|
|
340
|
+
continue
|
|
341
|
+
if value is None:
|
|
342
|
+
continue
|
|
343
|
+
is_exact = t == token(target)
|
|
344
|
+
if target in out.values:
|
|
345
|
+
# Two keys for one field. The exact name wins; distinct text is kept as a note.
|
|
346
|
+
loser = value
|
|
347
|
+
if is_exact and not exact[target]:
|
|
348
|
+
loser, out.values[target], exact[target] = out.values[target], value, True
|
|
349
|
+
if isinstance(loser, str) and loser.strip() and loser != out.values[target] and target in {"label", "text", "title", "caption"}:
|
|
350
|
+
out.extras.append((key, loser))
|
|
351
|
+
else:
|
|
352
|
+
ctx.fixed(f"{path}.{key}", "duplicate_field_ignored", target=target)
|
|
353
|
+
continue
|
|
354
|
+
out.values[target], exact[target] = value, is_exact
|
|
355
|
+
if not is_exact:
|
|
356
|
+
ctx.fixed(f"{path}.{key}", "field_alias", target=target)
|
|
357
|
+
return out
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------- inference
|
|
361
|
+
|
|
362
|
+
_IMAGE_EXT = re.compile(r"\.(png|jpe?g|gif|webp|svg|bmp|avif)(\?|#|$)", re.I)
|
|
363
|
+
_MEDIA_EXT = re.compile(r"\.(mp4|webm|mov|m4v|mp3|wav|ogg|m4a|aac)(\?|#|$)", re.I)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _is_numeric_mapping(value) -> bool:
|
|
367
|
+
return isinstance(value, dict) and bool(value) and all(as_number(v) is not None for v in value.values())
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _looks_like_elements(value) -> bool:
|
|
371
|
+
return isinstance(value, list) and bool(value) and all(
|
|
372
|
+
isinstance(v, (dict, list)) and (not isinstance(v, dict) or any(token(k) in TYPE_KEYS for k in v)) for v in value)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def infer_type(obj: dict) -> tuple[str, dict] | None:
|
|
376
|
+
"""Guess a type from the fields present when no usable type was given."""
|
|
377
|
+
keys = {token(k): v for k, v in obj.items()}
|
|
378
|
+
has = keys.__contains__
|
|
379
|
+
if has("options") or has("choices"):
|
|
380
|
+
return "choice", {}
|
|
381
|
+
if has("rows") or has("records"):
|
|
382
|
+
return "table", {}
|
|
383
|
+
if has("columns"):
|
|
384
|
+
cols = keys["columns"]
|
|
385
|
+
if isinstance(cols, list) and cols and all(isinstance(c, str) for c in cols) and not has("elements"):
|
|
386
|
+
return "table", {}
|
|
387
|
+
return "columns", {}
|
|
388
|
+
if has("facts"):
|
|
389
|
+
return "facts", {}
|
|
390
|
+
if any(has(k) for k in ("metrics", "kpis", "stats")):
|
|
391
|
+
return "metrics", {}
|
|
392
|
+
if has("change") or has("delta") or has("trend") and has("value"):
|
|
393
|
+
return "metrics", {}
|
|
394
|
+
if any(has(k) for k in ("series", "datasets", "charttype")) or (has("data") and _is_numeric_mapping(keys["data"])):
|
|
395
|
+
return "chart", {}
|
|
396
|
+
if has("images") or has("gallery"):
|
|
397
|
+
return "images", {}
|
|
398
|
+
if has("sources") or has("video") or has("audio"):
|
|
399
|
+
return "media", {}
|
|
400
|
+
if has("buttons") or has("actions"):
|
|
401
|
+
return "buttons", {}
|
|
402
|
+
for k in ("elements", "children", "components", "body", "questions", "fields", "items"):
|
|
403
|
+
if has(k) and _looks_like_elements(keys[k]):
|
|
404
|
+
return "section", {}
|
|
405
|
+
if has("items") and isinstance(keys["items"], list):
|
|
406
|
+
return "list", {}
|
|
407
|
+
link = next((keys[k] for k in ("url", "src", "href", "link", "image", "imageurl") if has(k)), None)
|
|
408
|
+
if isinstance(link, str) and as_url(link):
|
|
409
|
+
if _MEDIA_EXT.search(link):
|
|
410
|
+
return "media", {}
|
|
411
|
+
if _IMAGE_EXT.search(link) or has("alt") or has("alttext") or has("src") or has("image") or has("imageurl"):
|
|
412
|
+
return "image", {}
|
|
413
|
+
if any(has(k) for k in ("label", "title", "text")):
|
|
414
|
+
return "buttons", {"action": "open_url"}
|
|
415
|
+
return "image", {}
|
|
416
|
+
if has("rating") or has("stars"):
|
|
417
|
+
return "rating", {}
|
|
418
|
+
if has("progress") or has("percent") or has("percentage"):
|
|
419
|
+
return "progress", {}
|
|
420
|
+
if has("tone") and has("text"):
|
|
421
|
+
return "badge", {}
|
|
422
|
+
if has("text") or has("content") or has("markdown") or has("paragraph") or has("message") or has("body"):
|
|
423
|
+
return "text", {}
|
|
424
|
+
if any(has(k) for k in ("label", "question", "prompt", "placeholder", "required", "isrequired")):
|
|
425
|
+
return "text_input", {}
|
|
426
|
+
if has("title") or has("heading") or has("header"):
|
|
427
|
+
return "heading", {}
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ---------------------------------------------------------------- documentation
|
|
432
|
+
|
|
433
|
+
def describe(canonical: str) -> dict:
|
|
434
|
+
spec = SPECS[canonical]
|
|
435
|
+
return {"type": canonical, "summary": spec.doc,
|
|
436
|
+
"fields": {name: list(aliases) for name, aliases in spec.fields.items()},
|
|
437
|
+
"also_called": sorted({n for n, (c, _) in TYPE_ALIASES.items() if c == canonical})[:25],
|
|
438
|
+
"example": spec.example}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def reference_doc() -> str:
|
|
442
|
+
lines = ["Element reference (every field is optional unless the example shows it is needed; aliases in brackets):"]
|
|
443
|
+
for name in DOCUMENTED:
|
|
444
|
+
spec = SPECS[name]
|
|
445
|
+
lines.append(f"\n{name}: {spec.doc}")
|
|
446
|
+
for fname, aliases in spec.fields.items():
|
|
447
|
+
lines.append(f" {fname}" + (f" [{', '.join(aliases)}]" if aliases else ""))
|
|
448
|
+
return "\n".join(lines)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def json_schema() -> dict:
|
|
452
|
+
"""Rich parameter schema with an enum of element types (small models follow enums well)."""
|
|
453
|
+
scalar = {"description": "Number (or date/time for date and time inputs)."}
|
|
454
|
+
element_props = {
|
|
455
|
+
"type": {"type": "string", "enum": DOCUMENTED},
|
|
456
|
+
"text": {"type": "string"}, "title": {"type": "string"}, "label": {"type": "string"},
|
|
457
|
+
"id": {"type": "string", "description": "Input submission key."},
|
|
458
|
+
"required": {"type": "boolean"}, "placeholder": {"type": "string"},
|
|
459
|
+
"value": {"description": "Initial value, or the number for progress/rating/metric/gauge."},
|
|
460
|
+
"options": {"type": "array", "items": {"type": "string"}, "description": "Choice options."},
|
|
461
|
+
"multiple": {"type": "boolean"}, "multiline": {"type": "boolean"},
|
|
462
|
+
"format": {"type": "string", "enum": ["text", "email", "tel", "url", "password"]},
|
|
463
|
+
"min": scalar, "max": scalar,
|
|
464
|
+
"items": {"type": "array", "items": {}, "description": "list: strings. metrics: [{label, value, change?, note?}]."},
|
|
465
|
+
"facts": {"type": "object", "description": "{label: value}", "additionalProperties": True},
|
|
466
|
+
"rows": {"type": "array", "items": {"type": "object", "additionalProperties": True}},
|
|
467
|
+
"chart_type": {"type": "string", "enum": ["bar", "horizontal_bar", "line", "pie", "donut", "stacked_bar", "grouped_bar", "gauge"]},
|
|
468
|
+
"data": {"type": "object", "description": "Chart data {label: number}.", "additionalProperties": True},
|
|
469
|
+
"series": {"type": "array", "description": "[{name, data: {label: number}}] for several series.",
|
|
470
|
+
"items": {"type": "object", "additionalProperties": True}},
|
|
471
|
+
"url": {"type": "string"}, "alt": {"type": "string"},
|
|
472
|
+
"tone": {"type": "string", "enum": ["success", "warning", "danger", "info", "neutral"]},
|
|
473
|
+
"elements": {"type": "array", "description": "Child elements (section).", "items": {"$ref": "#/$defs/element"}},
|
|
474
|
+
"columns": {"type": "array", "description": "columns: [{title, elements}].",
|
|
475
|
+
"items": {"type": "object", "properties": {"title": {"type": "string"},
|
|
476
|
+
"elements": {"type": "array", "items": {"$ref": "#/$defs/element"}}}}},
|
|
477
|
+
"buttons": {"type": "array", "items": {"$ref": "#/$defs/button"}},
|
|
478
|
+
}
|
|
479
|
+
button = {"type": "object", "properties": {"label": {"type": "string"}, "url": {"type": "string", "description": "Opens this link. Omit to submit."},
|
|
480
|
+
"data": {"type": "object", "additionalProperties": True}}, "required": ["label"]}
|
|
481
|
+
request = {"type": "object", "properties": {
|
|
482
|
+
"title": {"type": "string"}, "subtitle": {"type": "string"}, "status": {"type": "string", "description": "Optional status badge next to the title."},
|
|
483
|
+
"elements": {"type": "array", "items": {"$ref": "#/$defs/element"}},
|
|
484
|
+
"submit_label": {"type": "string"}, "buttons": {"type": "array", "items": {"$ref": "#/$defs/button"}}},
|
|
485
|
+
"required": ["elements"]}
|
|
486
|
+
return {"type": "object", "properties": {"request": request}, "required": ["request"],
|
|
487
|
+
"$defs": {"element": {"type": "object", "properties": element_props, "required": ["type"], "additionalProperties": True},
|
|
488
|
+
"button": button}}
|