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,210 @@
|
|
|
1
|
+
"""The visual design system.
|
|
2
|
+
|
|
3
|
+
The model never chooses styling. Every card gets the same hierarchy from these
|
|
4
|
+
helpers. Meaning is carried by typography, text colour and separators, which
|
|
5
|
+
render on every Adaptive Cards host. Container backgrounds (tiles) are an
|
|
6
|
+
enhancement: hosts whose config gives "emphasis" a background show tiles;
|
|
7
|
+
others show the same content without a background, and nothing is lost.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
BAR_CELLS = 20
|
|
12
|
+
|
|
13
|
+
TONES = {
|
|
14
|
+
# tone: (TextBlock color, native Badge style, symbol)
|
|
15
|
+
"success": ("Good", "good", "●"),
|
|
16
|
+
"warning": ("Warning", "warning", "●"),
|
|
17
|
+
"danger": ("Attention", "attention", "●"),
|
|
18
|
+
"info": ("Accent", "informative", "●"),
|
|
19
|
+
"neutral": ("Default", "default", "●"),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def text(content: str, **props) -> dict:
|
|
24
|
+
return {"type": "TextBlock", "text": content, "wrap": True, **props}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def card_title(content: str) -> dict:
|
|
28
|
+
return text(content, size="Large", weight="Bolder", style="heading")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def subtitle(content: str) -> dict:
|
|
32
|
+
return text(content, isSubtle=True, spacing="None")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def heading(content: str) -> dict:
|
|
36
|
+
return text(content, size="Medium", weight="Bolder", style="heading")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def group_title(content: str) -> dict:
|
|
40
|
+
"""Title of a section, column tile or data element."""
|
|
41
|
+
return text(content, size="Default", weight="Bolder", style="heading")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def body(content: str) -> dict:
|
|
45
|
+
return text(content)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def note(content: str) -> dict:
|
|
49
|
+
return text(content, size="Small", isSubtle=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def spaced(element: dict, spacing: str, separator: bool = False) -> dict:
|
|
53
|
+
out = {**element, "spacing": spacing}
|
|
54
|
+
if separator:
|
|
55
|
+
out["separator"] = True
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def row(*columns: dict, spacing: str | None = None) -> dict:
|
|
60
|
+
result = {"type": "ColumnSet", "columns": list(columns)}
|
|
61
|
+
if spacing:
|
|
62
|
+
result["spacing"] = spacing
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def column(items: list[dict], width="stretch", **props) -> dict:
|
|
67
|
+
return {"type": "Column", "width": width, "items": items, **props}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def badge(content: str, tone: str) -> dict:
|
|
71
|
+
color, _, symbol = TONES[tone]
|
|
72
|
+
return text(f"{symbol} {content}", size="Small", weight="Bolder", color=color)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def badge_row(badges: list[dict]) -> dict:
|
|
76
|
+
"""Several badges side by side, left-aligned."""
|
|
77
|
+
return row(*[column([b], "auto", spacing="Medium" if i else "Default") for i, b in enumerate(badges)])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def tile(items: list[dict]) -> dict:
|
|
81
|
+
return column(items, "stretch", style="emphasis")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def metric_tile(label: str, value: str, change: str | None, change_color: str, footnote: str | None) -> dict:
|
|
85
|
+
items = [text(label, size="Small", isSubtle=True),
|
|
86
|
+
text(value, size="ExtraLarge", weight="Bolder", spacing="None")]
|
|
87
|
+
if change:
|
|
88
|
+
items.append(text(change, size="Small", color=change_color, spacing="None"))
|
|
89
|
+
if footnote:
|
|
90
|
+
items.append(text(footnote, size="Small", isSubtle=True, spacing="None"))
|
|
91
|
+
return tile(items)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def bar(fraction: float, *, track: bool, color: str = "Accent") -> dict:
|
|
95
|
+
"""A horizontal bar drawn with block glyphs; renders on every host."""
|
|
96
|
+
filled = max(0, min(BAR_CELLS, round(fraction * BAR_CELLS)))
|
|
97
|
+
if fraction > 0 and filled == 0:
|
|
98
|
+
filled = 1
|
|
99
|
+
runs = []
|
|
100
|
+
if filled:
|
|
101
|
+
runs.append({"type": "TextRun", "text": "█" * filled, "color": color, "fontType": "Monospace"})
|
|
102
|
+
if track and BAR_CELLS - filled:
|
|
103
|
+
runs.append({"type": "TextRun", "text": "█" * (BAR_CELLS - filled), "color": color,
|
|
104
|
+
"isSubtle": True, "fontType": "Monospace"})
|
|
105
|
+
if not runs:
|
|
106
|
+
runs.append({"type": "TextRun", "text": "▏", "color": color, "isSubtle": True, "fontType": "Monospace"})
|
|
107
|
+
return {"type": "RichTextBlock", "inlines": runs}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def bar_row(label: str, fraction: float, value: str, label_width_px: int) -> dict:
|
|
111
|
+
return row(column([text(label, size="Small")], f"{label_width_px}px"),
|
|
112
|
+
column([bar(fraction, track=False)], "auto", verticalContentAlignment="Center"),
|
|
113
|
+
column([text(value, size="Small", weight="Bolder")], "auto"),
|
|
114
|
+
spacing="Small")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def label_value_row(label: str, value: str) -> dict:
|
|
118
|
+
return row(column([text(label, size="Small")]), column([text(value, size="Small", weight="Bolder")], "auto"),
|
|
119
|
+
spacing="Small")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def progress(label: str, value_text: str, fraction: float | None, color: str = "Accent") -> list[dict]:
|
|
123
|
+
items = [label_value_row(label, value_text)]
|
|
124
|
+
if fraction is not None:
|
|
125
|
+
items.append({**bar(fraction, track=True, color=color), "spacing": "None"})
|
|
126
|
+
return items
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def stars(value: float, maximum: int, caption: str) -> dict:
|
|
130
|
+
"""Rounded stars for scanning; the caption always states the exact value."""
|
|
131
|
+
full = min(maximum, int(value + 0.5))
|
|
132
|
+
runs = []
|
|
133
|
+
if full:
|
|
134
|
+
runs.append({"type": "TextRun", "text": "★" * full, "color": "Warning"})
|
|
135
|
+
if maximum - full:
|
|
136
|
+
runs.append({"type": "TextRun", "text": "☆" * (maximum - full), "color": "Warning", "isSubtle": True})
|
|
137
|
+
runs.append({"type": "TextRun", "text": " " + caption, "isSubtle": True})
|
|
138
|
+
return {"type": "RichTextBlock", "inlines": runs}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def bullet_row(marker: str, content: str, marker_width_px: int) -> dict:
|
|
142
|
+
return row(column([text(marker, isSubtle=True)], f"{marker_width_px}px"),
|
|
143
|
+
column([text(content)], "stretch", spacing="None"), spacing="Small")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def table(headers: list[str], rows: list[list[str]], numeric: list[bool], weights: list[int]) -> dict:
|
|
147
|
+
def cell(value: str, header=False) -> dict:
|
|
148
|
+
return {"type": "TableCell", "items": [text(value, weight="Bolder") if header else text(value)]}
|
|
149
|
+
|
|
150
|
+
columns = []
|
|
151
|
+
for is_number, weight in zip(numeric, weights):
|
|
152
|
+
definition = {"width": weight}
|
|
153
|
+
if is_number:
|
|
154
|
+
definition["horizontalCellContentAlignment"] = "Right"
|
|
155
|
+
columns.append(definition)
|
|
156
|
+
return {"type": "Table", "columns": columns, "showGridLines": True, "gridStyle": "default",
|
|
157
|
+
"rows": [{"type": "TableRow", "style": "emphasis", "cells": [cell(h, True) for h in headers]},
|
|
158
|
+
*[{"type": "TableRow", "cells": [cell(v) for v in r]} for r in rows]]}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def button_style(label: str, explicit: str | None, primary_available: bool) -> str | None:
|
|
162
|
+
"""positive for the primary action, destructive for rejecting/deleting."""
|
|
163
|
+
if explicit:
|
|
164
|
+
return None if explicit == "default" else explicit
|
|
165
|
+
words = label.casefold()
|
|
166
|
+
if any(w in words for w in ("reject", "decline", "delete", "remove", "deny", "revoke", "terminate", "discard")):
|
|
167
|
+
return "destructive"
|
|
168
|
+
if primary_available and any(w in words for w in ("approve", "accept", "confirm", "submit", "save", "send",
|
|
169
|
+
"book", "register", "sign up", "pay", "complete", "apply")):
|
|
170
|
+
return "positive"
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def host_config() -> dict:
|
|
175
|
+
"""A recommended modern host config for the Adaptive Cards JS SDK.
|
|
176
|
+
|
|
177
|
+
Cards render correctly without it; with it, tiles, tables and badges get
|
|
178
|
+
soft backgrounds and a cleaner type scale. Pass it to
|
|
179
|
+
new AdaptiveCards.HostConfig(...) in the frontend.
|
|
180
|
+
"""
|
|
181
|
+
foreground = {
|
|
182
|
+
"default": {"default": "#1F2937", "subtle": "#6B7280"},
|
|
183
|
+
"dark": {"default": "#111827", "subtle": "#4B5563"},
|
|
184
|
+
"light": {"default": "#FFFFFF", "subtle": "#F3F4F6"},
|
|
185
|
+
"accent": {"default": "#2563EB", "subtle": "#BFDBFE"},
|
|
186
|
+
"good": {"default": "#15803D", "subtle": "#BBF7D0"},
|
|
187
|
+
"warning": {"default": "#B45309", "subtle": "#FDE68A"},
|
|
188
|
+
"attention": {"default": "#B91C1C", "subtle": "#FECACA"},
|
|
189
|
+
}
|
|
190
|
+
styles = {name: {"backgroundColor": color, "foregroundColors": foreground}
|
|
191
|
+
for name, color in (("default", "#FFFFFF"), ("emphasis", "#F3F4F6"), ("accent", "#EFF6FF"),
|
|
192
|
+
("good", "#F0FDF4"), ("warning", "#FFFBEB"), ("attention", "#FEF2F2"))}
|
|
193
|
+
return {
|
|
194
|
+
"fontFamily": "'Segoe UI', system-ui, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
195
|
+
"fontSizes": {"small": 12, "default": 14, "medium": 16, "large": 20, "extraLarge": 28},
|
|
196
|
+
"fontWeights": {"lighter": 300, "default": 400, "bolder": 600},
|
|
197
|
+
"spacing": {"small": 4, "default": 8, "medium": 16, "large": 24, "extraLarge": 32, "padding": 16},
|
|
198
|
+
"separator": {"lineThickness": 1, "lineColor": "#E5E7EB"},
|
|
199
|
+
"containerStyles": styles,
|
|
200
|
+
"imageSizes": {"small": 32, "medium": 64, "large": 160},
|
|
201
|
+
"actions": {"maxActions": 6, "spacing": "default", "buttonSpacing": 8, "actionsOrientation": "horizontal",
|
|
202
|
+
"actionAlignment": "left", "showCard": {"actionMode": "inline", "inlineTopMargin": 16}},
|
|
203
|
+
"factSet": {"title": {"color": "default", "isSubtle": True, "weight": "default", "wrap": True, "maxWidth": 200},
|
|
204
|
+
"value": {"color": "default", "weight": "bolder", "wrap": True}, "spacing": 12},
|
|
205
|
+
"textStyles": {"heading": {"size": "large", "weight": "bolder", "color": "default"},
|
|
206
|
+
"columnHeader": {"size": "default", "weight": "bolder", "color": "default"}},
|
|
207
|
+
"inputs": {"label": {"inputSpacing": "small", "requiredInputs": {"weight": "bolder", "suffix": " *", "suffixColor": "attention"},
|
|
208
|
+
"optionalInputs": {"weight": "bolder"}},
|
|
209
|
+
"errorMessage": {"size": "small", "color": "attention"}},
|
|
210
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Plain Python functions for agent tool registration.
|
|
2
|
+
|
|
3
|
+
The docstring of build_adaptive_card is the prompt a model sees. It is kept
|
|
4
|
+
short and example-driven for small models (GPT-4.1 mini, Gemini 2.5 Flash):
|
|
5
|
+
the builder is forgiving, so the prompt teaches the happy path only.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from .builder import render_card
|
|
10
|
+
from .catalog import DOCUMENTED, describe, json_schema, resolve_type
|
|
11
|
+
from .normalize import BuilderConfig, Context
|
|
12
|
+
|
|
13
|
+
TOOL_DOC = """Show the user a rich card: a form, summary, comparison, dashboard, approval, chart or table.
|
|
14
|
+
|
|
15
|
+
Describe WHAT to show. The tool does all layout, styling and Adaptive Card JSON;
|
|
16
|
+
never write Adaptive Card JSON yourself.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
request: {"title": "...", "subtitle": "...", "status": "...", "elements": [...], "submit_label": "..."}
|
|
20
|
+
Only "elements" is needed. "status" shows a coloured badge beside the title.
|
|
21
|
+
|
|
22
|
+
Element types (each element is an object with a "type"):
|
|
23
|
+
heading {"type": "heading", "text": "Summary"}
|
|
24
|
+
text {"type": "text", "text": "Paragraph. **bold** and [links](https://example.com) work."}
|
|
25
|
+
note {"type": "note", "text": "Small grey print."}
|
|
26
|
+
list {"type": "list", "items": ["First", "Second"], "numbered": false}
|
|
27
|
+
facts {"type": "facts", "facts": {"Owner": "Dana", "Due": "2026-10-01"}}
|
|
28
|
+
table {"type": "table", "rows": [{"Item": "Laptop", "Qty": 2}, {"Item": "Mouse", "Qty": 5}]}
|
|
29
|
+
metrics {"type": "metrics", "items": [{"label": "Revenue", "value": "$1.2M", "change": "+8%"}]}
|
|
30
|
+
badge {"type": "badge", "text": "Approved", "tone": "success"} tone: success|warning|danger|info|neutral
|
|
31
|
+
image {"type": "image", "url": "https://example.com/photo.png", "alt": "Product photo"}
|
|
32
|
+
chart {"type": "chart", "chart_type": "bar", "title": "Orders", "data": {"Jan": 120, "Feb": 150}}
|
|
33
|
+
chart_type: bar|horizontal_bar|line|pie|donut|stacked_bar|grouped_bar|gauge
|
|
34
|
+
several series: "series": [{"name": "2025", "data": {"Q1": 5, "Q2": 7}}, {"name": "2026", "data": {"Q1": 6, "Q2": 9}}]
|
|
35
|
+
gauge: {"type": "chart", "chart_type": "gauge", "value": 72, "max": 100}
|
|
36
|
+
progress {"type": "progress", "label": "Migration", "value": 70} (a percent, or add "max")
|
|
37
|
+
rating {"type": "rating", "value": 4.5, "count": 120}
|
|
38
|
+
divider {"type": "divider"}
|
|
39
|
+
section {"type": "section", "title": "Details", "elements": [...]}
|
|
40
|
+
columns {"type": "columns", "columns": [{"title": "Basic", "elements": [...]}, {"title": "Pro", "elements": [...]}]}
|
|
41
|
+
buttons {"type": "buttons", "buttons": [{"label": "Approve"}, {"label": "Open ticket", "url": "https://example.com/t/1"}]}
|
|
42
|
+
Inputs (a Submit button is added automatically; "submit_label" renames it):
|
|
43
|
+
text_input {"type": "text_input", "id": "email", "label": "Email", "required": true} also: placeholder, multiline, format (email|tel|url|password)
|
|
44
|
+
number_input {"type": "number_input", "id": "qty", "label": "Quantity", "min": 1, "max": 10}
|
|
45
|
+
date_input {"type": "date_input", "id": "start", "label": "Start date"} (time_input for a time)
|
|
46
|
+
choice {"type": "choice", "id": "plan", "label": "Plan", "options": ["Basic", "Pro"]} add "multiple": true for several answers
|
|
47
|
+
checkbox {"type": "checkbox", "id": "terms", "label": "I agree to the terms", "required": true}
|
|
48
|
+
rating_input {"type": "rating_input", "id": "score", "label": "How was it?"}
|
|
49
|
+
|
|
50
|
+
Rules:
|
|
51
|
+
1. Copy the user's text, numbers, names and options exactly. Never invent data, prices, links or images; leave out anything you don't have.
|
|
52
|
+
2. Only use https links and image URLs that appear in the conversation.
|
|
53
|
+
3. A button with "url" opens that link; a button without "url" submits the card.
|
|
54
|
+
4. Values are flexible: "1,200", "$4.5M", "75%", "March 5, 2026" and "2pm" are all understood.
|
|
55
|
+
5. One call builds the whole card. If status is "successful" the card is shown to the user: do not call again and do not repeat the card as text.
|
|
56
|
+
6. If status is "error", read "message", fix the request and call once more.
|
|
57
|
+
|
|
58
|
+
Example (form):
|
|
59
|
+
{"title": "Book a demo", "elements": [
|
|
60
|
+
{"type": "text", "text": "Pick a time that suits you."},
|
|
61
|
+
{"type": "text_input", "id": "name", "label": "Full name", "required": true},
|
|
62
|
+
{"type": "date_input", "id": "day", "label": "Preferred day"},
|
|
63
|
+
{"type": "choice", "id": "size", "label": "Team size", "options": ["1-10", "11-50", "51+"]}],
|
|
64
|
+
"submit_label": "Request demo"}
|
|
65
|
+
|
|
66
|
+
Example (dashboard):
|
|
67
|
+
{"title": "Weekly sales", "subtitle": "Week ending 11 Sep 2026", "status": "On track", "elements": [
|
|
68
|
+
{"type": "metrics", "items": [{"label": "Revenue", "value": "$48,200", "change": "+12%"}, {"label": "Orders", "value": "312", "change": "-3%"}]},
|
|
69
|
+
{"type": "chart", "chart_type": "bar", "title": "Revenue by region", "data": {"North": 18200, "South": 12100, "West": 17900}},
|
|
70
|
+
{"type": "table", "title": "Top products", "rows": [{"Product": "Desk", "Units": 84}, {"Product": "Chair", "Units": 61}]}]}
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
{"status": "successful" or "error", "message": what to do next, "response": the card for the app, ...}
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
JSON_DOC = TOOL_DOC.replace(
|
|
77
|
+
'Args:\n request: {', 'Args:\n request_json: The request object below, encoded as a JSON string:\n {', 1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_adaptive_card(request: dict) -> dict:
|
|
81
|
+
return render_card(request)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_adaptive_card_json(request_json: str) -> dict:
|
|
85
|
+
return render_card(request_json)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
build_adaptive_card.__doc__ = TOOL_DOC
|
|
89
|
+
build_adaptive_card_json.__doc__ = JSON_DOC
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def describe_adaptive_card(element_type: str = "") -> dict:
|
|
93
|
+
"""Look up the fields and an example for one card element type (e.g. "chart", "choice").
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
element_type: An element type name. Empty returns a one-line summary of every type.
|
|
97
|
+
"""
|
|
98
|
+
if not element_type:
|
|
99
|
+
return {"status": "successful", "response": {name: describe(name)["summary"] for name in DOCUMENTED}}
|
|
100
|
+
resolved = resolve_type(element_type, Context(BuilderConfig()), "$.element_type")
|
|
101
|
+
if not resolved or resolved[0] not in DOCUMENTED:
|
|
102
|
+
return {"status": "error", "response": f"Unknown element type '{element_type}'. Known types: {', '.join(DOCUMENTED)}."}
|
|
103
|
+
return {"status": "successful", "response": describe(resolved[0])}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def make_adaptive_card_tool(config: BuilderConfig | None = None, *, json_input: bool = False) -> Callable:
|
|
107
|
+
"""Create a tool function bound to developer settings (profile, width, limits).
|
|
108
|
+
|
|
109
|
+
Example: make_adaptive_card_tool(BuilderConfig(profile="microsoft")).
|
|
110
|
+
Set json_input=True for providers that need a single string parameter.
|
|
111
|
+
"""
|
|
112
|
+
chosen = config or BuilderConfig()
|
|
113
|
+
if json_input:
|
|
114
|
+
def build_adaptive_card_json(request_json: str) -> dict:
|
|
115
|
+
return render_card(request_json, config=chosen)
|
|
116
|
+
function = build_adaptive_card_json
|
|
117
|
+
function.__doc__ = JSON_DOC
|
|
118
|
+
else:
|
|
119
|
+
def build_adaptive_card(request: dict) -> dict:
|
|
120
|
+
return render_card(request, config=chosen)
|
|
121
|
+
function = build_adaptive_card
|
|
122
|
+
function.__doc__ = TOOL_DOC
|
|
123
|
+
return function
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def tool_schema(*, json_input: bool = False) -> dict:
|
|
127
|
+
"""Provider-neutral name, description and JSON Schema parameters.
|
|
128
|
+
|
|
129
|
+
The object form lists element types as an enum, which helps small models
|
|
130
|
+
choose valid types. It uses $defs/$ref; for providers without $ref support
|
|
131
|
+
use json_input=True (a single string parameter).
|
|
132
|
+
"""
|
|
133
|
+
if json_input:
|
|
134
|
+
return {"name": "build_adaptive_card_json", "description": JSON_DOC,
|
|
135
|
+
"parameters": {"type": "object", "properties": {"request_json": {
|
|
136
|
+
"type": "string", "description": "JSON string of the card request object."}},
|
|
137
|
+
"required": ["request_json"], "additionalProperties": False}}
|
|
138
|
+
return {"name": "build_adaptive_card", "description": TOOL_DOC, "parameters": json_schema()}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Backward-compatible explicit name.
|
|
142
|
+
build_adaptive_card_object = build_adaptive_card
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Agentic-GenUI-Tools
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Modern, deterministic Adaptive Cards from small, forgiving agent tool calls
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: adaptive-cards,agents,tools,google-adk,langchain
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
16
|
+
Requires-Dist: jsonschema>=4; extra == "test"
|
|
17
|
+
Provides-Extra: integration
|
|
18
|
+
Requires-Dist: google-adk>=1; extra == "integration"
|
|
19
|
+
Requires-Dist: langchain-core>=0.3; extra == "integration"
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
22
|
+
Requires-Dist: jsonschema>=4; extra == "dev"
|
|
23
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
24
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# adaptive-card-tools
|
|
28
|
+
|
|
29
|
+
Build modern, deterministic Adaptive Cards from small, forgiving agent tool
|
|
30
|
+
calls. The model describes *what* to show; Python repairs the request,
|
|
31
|
+
applies a consistent visual design, and returns a complete card.
|
|
32
|
+
|
|
33
|
+
Designed for small models such as GPT-4.1 mini and Gemini 2.5 Flash:
|
|
34
|
+
|
|
35
|
+
- **Short prompt.** The tool description is ~1,200 tokens of examples, not a
|
|
36
|
+
schema manual (it was ~8,500 in 0.1.x).
|
|
37
|
+
- **Repair, don't reject.** Wrong type names, Adaptive Card JSON, style keys,
|
|
38
|
+
aliases, typos, strings for numbers and dates, odd option shapes, wrapped
|
|
39
|
+
or broken JSON: all are interpreted. Errors are reserved for requests that
|
|
40
|
+
are unreadable, empty, or over a safety limit.
|
|
41
|
+
- **Accurate.** Repairs change the *form* of a value, never its *facts*. Text
|
|
42
|
+
is never rewritten or truncated, numbers are parsed but never estimated,
|
|
43
|
+
missing chart points are not filled with zeros, and anything that can't be
|
|
44
|
+
read exactly is left out with a warning. Every repair is listed in metadata.
|
|
45
|
+
- **Modern and stable.** The model never chooses styling. Every card gets the
|
|
46
|
+
same hierarchy (title, status badge, KPI tiles, data bars, shaded table
|
|
47
|
+
headers, primary/destructive buttons) built from primitives that render on
|
|
48
|
+
every Adaptive Cards host.
|
|
49
|
+
|
|
50
|
+
Python 3.10+, no runtime dependencies. Import name: `adaptive_card_tools`.
|
|
51
|
+
|
|
52
|
+
Rendered with Microsoft's `adaptivecards` JS SDK. 0.2 with `host_config()` and
|
|
53
|
+
`examples/card-theme.css` (the middle card is the deliberately messy call from
|
|
54
|
+
`examples.py`):
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
<details><summary>0.1 output for comparison</summary>
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
## Install and use
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m pip install .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from adaptive_card_tools import build_adaptive_card
|
|
72
|
+
|
|
73
|
+
result = build_adaptive_card({
|
|
74
|
+
"title": "Weekly sales",
|
|
75
|
+
"subtitle": "Week ending 11 Sep 2026",
|
|
76
|
+
"status": "On track",
|
|
77
|
+
"elements": [
|
|
78
|
+
{"type": "metrics", "items": [
|
|
79
|
+
{"label": "Revenue", "value": "$48,200", "change": "+12%"},
|
|
80
|
+
{"label": "Orders", "value": "312", "change": "-3%"}]},
|
|
81
|
+
{"type": "chart", "chart_type": "bar", "title": "Revenue by region",
|
|
82
|
+
"data": {"North": 18200, "South": 12100, "West": 17900}},
|
|
83
|
+
],
|
|
84
|
+
})
|
|
85
|
+
result["status"] # "successful"
|
|
86
|
+
result["response"] # the Adaptive Card JSON for your frontend
|
|
87
|
+
result["message"] # a short instruction for the model
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Add to an agent
|
|
91
|
+
|
|
92
|
+
The function is a plain Python tool; its docstring is the model's prompt
|
|
93
|
+
(see [docs/TOOL_DOCSTRING.md](docs/TOOL_DOCSTRING.md)).
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Google ADK
|
|
97
|
+
from google.adk.agents import Agent
|
|
98
|
+
from adaptive_card_tools import build_adaptive_card
|
|
99
|
+
|
|
100
|
+
agent = Agent(name="card_assistant", model="gemini-2.5-flash", tools=[build_adaptive_card],
|
|
101
|
+
instruction="When the answer is a form, summary, comparison, dashboard, approval, "
|
|
102
|
+
"table or chart, call build_adaptive_card once with the complete content.")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# LangChain, with the rich schema (element types as an enum, which small models follow well)
|
|
107
|
+
from langchain.agents import create_agent
|
|
108
|
+
from langchain_core.tools import StructuredTool
|
|
109
|
+
from adaptive_card_tools import build_adaptive_card, tool_schema
|
|
110
|
+
|
|
111
|
+
schema = tool_schema()
|
|
112
|
+
card_tool = StructuredTool.from_function(build_adaptive_card, name=schema["name"],
|
|
113
|
+
description=schema["description"], args_schema=schema["parameters"])
|
|
114
|
+
agent = create_agent(model="openai:gpt-4.1-mini", tools=[card_tool])
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If a provider rejects a free-form object parameter (some Gemini setups and
|
|
118
|
+
strict tool modes do), use `build_adaptive_card_json(request_json: str)`. Its
|
|
119
|
+
parser also repairs single quotes, trailing commas, comments, Python
|
|
120
|
+
`True/None`, code fences, prose around the JSON, and unclosed brackets.
|
|
121
|
+
|
|
122
|
+
`make_adaptive_card_tool(BuilderConfig(...), json_input=False)` returns a tool
|
|
123
|
+
bound to your settings. `describe_adaptive_card(type)` is an optional helper
|
|
124
|
+
tool; small models rarely need it.
|
|
125
|
+
|
|
126
|
+
## What the model writes
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{"title": "Book a demo", "elements": [
|
|
130
|
+
{"type": "text", "text": "Pick a time that suits you."},
|
|
131
|
+
{"type": "text_input", "id": "name", "label": "Full name", "required": true},
|
|
132
|
+
{"type": "date_input", "id": "day", "label": "Preferred day"},
|
|
133
|
+
{"type": "choice", "id": "size", "label": "Team size", "options": ["1-10", "11-50", "51+"]}],
|
|
134
|
+
"submit_label": "Request demo"}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Group | Types |
|
|
138
|
+
|---|---|
|
|
139
|
+
| Content | `heading`, `text` (markdown), `note`, `list`, `facts`, `table`, `badge`, `image`, `images`, `media` |
|
|
140
|
+
| Data | `metrics` (KPI tiles), `chart` (`bar`, `horizontal_bar`, `line`, `pie`, `donut`, `stacked_bar`, `grouped_bar`, `gauge`), `progress`, `rating` |
|
|
141
|
+
| Layout | `section`, `columns`, `divider`, `buttons` |
|
|
142
|
+
| Inputs | `text_input`, `number_input`, `date_input`, `time_input`, `datetime_input`, `choice`, `checkbox`, `rating_input` |
|
|
143
|
+
|
|
144
|
+
Card-level fields: `title`, `subtitle`, `status` (a coloured badge next to the
|
|
145
|
+
title), `elements`, `submit_label`, `buttons`. Forms get a Submit button
|
|
146
|
+
automatically; `"buttons": []` suppresses it. Every field name and alias is
|
|
147
|
+
listed in [docs/ELEMENT_REFERENCE.md](docs/ELEMENT_REFERENCE.md).
|
|
148
|
+
|
|
149
|
+
## Normalization
|
|
150
|
+
|
|
151
|
+
What small models actually send, and what happens:
|
|
152
|
+
|
|
153
|
+
| The model sends | The builder does |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `{"request": {...}}`, `{"card": {...}}`, a JSON string, a fenced block, prose around JSON, `{"name":..., "arguments": "..."}` | Unwraps and parses it |
|
|
156
|
+
| Adaptive Card JSON: `AdaptiveCard`, `TextBlock` with `size`/`weight`, `Container`, `ColumnSet`, `FactSet`, `Input.ChoiceSet` + `style: expanded`, `Action.Submit`, `Table` rows/cells, `RichTextBlock` | Translates it; large/bold text becomes a heading, subtle/small text a note |
|
|
157
|
+
| Type names like `Dropdown`, `RadioGroup`, `TextArea`, `EmailField`, `DatePicker`, `Switch`, `kpi`, `hbar`, `RevenueBarGraph`, `chekbox` | Resolves aliases, typos and keywords (and implied settings: radio, multiline, email format…) |
|
|
158
|
+
| No `type`, or an unknown one | Infers it from the fields (`options` → choice, `rows` → table, image URL → image…) |
|
|
159
|
+
| `size`, `color`, `weight`, `spacing`, `width`, `style`… | Ignores styling (reported), but uses it as a hint where it carries meaning |
|
|
160
|
+
| `question`, `prompt`, `isRequired`, `mandatory`, `choices`, `default`, `maxLength`… | Maps field aliases; `name` becomes the id if it looks like one, else the label |
|
|
161
|
+
| `description`, `subtitle`, `caption`, `note` on any element | Shows the text instead of dropping it |
|
|
162
|
+
| `"1,200"`, `"$1.2M"`, `"75%"`, `"(40)"`, `"4.5/5"`, `"12 GB"`; `"yes"`, `"Required"`, `1` | Parses numbers and booleans |
|
|
163
|
+
| `"March 5, 2026"`, `"03/15/2026"`, `"2026/03/05"`; `"9am"`, `"2:30 PM"`, `"21h30"` | Converts to `YYYY-MM-DD` / `HH:MM`; ambiguous dates follow `BuilderConfig(day_first=...)` and warn |
|
|
164
|
+
| Options as strings, `"A, B, C"`, `{title: value}`, `{"s": "Small"}`, `[{"id", "label"}]`, `[[title, value]]` | Normalizes options; defaults match case-insensitively (`"small"` → `Small`) |
|
|
165
|
+
| A dropdown with no options | Asks yes/no for a yes/no question, otherwise shows a text box |
|
|
166
|
+
| Facts as `"Key: Value"` strings, pairs, `{label, value}` objects, or inline keys | Builds a fact set |
|
|
167
|
+
| Tables as records, lists with a header row, ragged rows, `{column: [values]}`, markdown tables | Builds a table; nothing is dropped, blanks show as `—` |
|
|
168
|
+
| Charts as `{label: n}`, `[{label, value}]`, `[[label, n]]`, `labels` + `values`, Chart.js `datasets`, records with numeric fields | Builds series; picks grouped bars for several series, sorts line x values |
|
|
169
|
+
| Line chart over categories, pie with negatives or several series | Switches to a bar chart (reported) |
|
|
170
|
+
| Series with different categories, non-numeric values | Shows a table with blanks rather than inventing zeros |
|
|
171
|
+
| Adjacent single `metric`, `button`, `badge` or `column` elements | Merges them into one row |
|
|
172
|
+
| `www.example.com`, `//cdn…`, spaces in URLs | Normalizes to https; `javascript:`, `data:`, `file:` and relative URLs are dropped with a warning |
|
|
173
|
+
| Buttons without data | Adds `{"action": "<label slug>"}` so the app can tell them apart; data keys that would overwrite answers are removed |
|
|
174
|
+
| Duplicate or invalid ids | Sanitizes and de-duplicates (reported in `metadata.inputIds`) |
|
|
175
|
+
|
|
176
|
+
Nothing is guessed that would change meaning: an unmatched default, an
|
|
177
|
+
unreadable date ("next Tuesday", "March 5" with no year), an out-of-range
|
|
178
|
+
value, or a bad URL is left out and reported in `metadata.warnings`, which
|
|
179
|
+
is also summarized in `message` so the model can decide whether to retry.
|
|
180
|
+
|
|
181
|
+
## Visual design
|
|
182
|
+
|
|
183
|
+
The design lives in [src/adaptive_card_tools/theme.py](src/adaptive_card_tools/theme.py). Meaning is carried by
|
|
184
|
+
typography, text colour and separators, which every host renders:
|
|
185
|
+
|
|
186
|
+
- Header: large title, subtle subtitle, status badge aligned right.
|
|
187
|
+
- `metrics`: big-number tiles with ▲/▼ changes in green/red (`lower_is_better` flips colours).
|
|
188
|
+
- Charts in the portable profile: label | bar | value rows drawn with block
|
|
189
|
+
glyphs, with exact values and pie percentages as text. Progress bars have a
|
|
190
|
+
track. Ratings show stars plus the exact value.
|
|
191
|
+
- Tables: shaded bold header row, numeric columns right-aligned, content-based column widths.
|
|
192
|
+
- Sections: bold title, a separator at the top level. Columns: tiles.
|
|
193
|
+
- Buttons: the primary action is `positive`; reject/delete/decline are `destructive`.
|
|
194
|
+
|
|
195
|
+
Container backgrounds (tiles, table header shading) depend on the host config.
|
|
196
|
+
They show in Teams and with the recommended config, and fall back to plain
|
|
197
|
+
columns elsewhere, so nothing is lost. For the JS SDK, pass
|
|
198
|
+
`adaptive_card_tools.host_config()` to `new AdaptiveCards.HostConfig(...)` and
|
|
199
|
+
include [examples/card-theme.css](examples/card-theme.css) for buttons and inputs; see
|
|
200
|
+
[examples/frontend.js](examples/frontend.js).
|
|
201
|
+
|
|
202
|
+
## Renderer profiles
|
|
203
|
+
|
|
204
|
+
`portable` (default) emits core Adaptive Cards 1.5 only. `microsoft` emits
|
|
205
|
+
native extensions (charts, `Badge`, `Rating`, `ProgressBar`, `ProgressRing`,
|
|
206
|
+
`Input.Rating`) with a core fallback on each.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from adaptive_card_tools import BuilderConfig, make_adaptive_card_tool
|
|
210
|
+
|
|
211
|
+
card_tool = make_adaptive_card_tool(BuilderConfig(
|
|
212
|
+
profile="microsoft",
|
|
213
|
+
native_types=("Badge", "Rating", "Chart.Line"), # optional allow-list
|
|
214
|
+
available_width_px=1200,
|
|
215
|
+
day_first=False,
|
|
216
|
+
))
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Native charts are only used when the data can be drawn exactly; series with
|
|
220
|
+
gaps or non-numeric values use the table fallback. The model cannot select
|
|
221
|
+
the profile, styling or schema version.
|
|
222
|
+
|
|
223
|
+
## Result envelope
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
{
|
|
227
|
+
"status": "successful", # or "error"
|
|
228
|
+
"suggestedWidth": "60%", # 50%, 60%, 80%, 100% or "100%+" (a signal, not CSS)
|
|
229
|
+
"response": {...}, # the card; forward unchanged to the renderer
|
|
230
|
+
"message": "Card built and ready to display. ...", # for the model
|
|
231
|
+
"metadata": {
|
|
232
|
+
"profile": "portable", "schemaVersion": "1.5",
|
|
233
|
+
"inputIds": ["name", "day", "size"],
|
|
234
|
+
"choiceValues": {"size": [{"title": "1-10", "value": "1-10"}, ...]},
|
|
235
|
+
"layout": {"widthPercent": 60, "estimatedMinWidthPx": 480, "overflow": "none", ...},
|
|
236
|
+
"warnings": [], # content left out or reinterpreted
|
|
237
|
+
"normalizations": [...] # every harmless repair
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
On error, `response` and `message` explain the problem and `errors` holds
|
|
243
|
+
`{path, code, message}`. Errors happen only for unreadable JSON, a request
|
|
244
|
+
with nothing to display, or configured limits (bytes, depth, nodes,
|
|
245
|
+
elements, output size).
|
|
246
|
+
|
|
247
|
+
Width is an estimate from structural minima (columns add, stacked content
|
|
248
|
+
takes the widest); the frontend adapter in `examples/frontend.js` applies it
|
|
249
|
+
and scrolls when needed.
|
|
250
|
+
|
|
251
|
+
## Application responsibilities
|
|
252
|
+
|
|
253
|
+
- Render `response` unchanged; handle `Action.Submit` and `Action.OpenUrl` in the frontend.
|
|
254
|
+
- Validate and authorize every submission on the server. Generated ids are
|
|
255
|
+
deterministic for the same request, not stable across edits; give explicit
|
|
256
|
+
ids when your backend expects particular keys.
|
|
257
|
+
- URLs are validated (http/https, no credentials) but never fetched or allow-listed.
|
|
258
|
+
- No network access, randomness, clock or LLM call happens in the build path.
|
|
259
|
+
|
|
260
|
+
## Examples
|
|
261
|
+
|
|
262
|
+
[src/adaptive_card_tools/examples.py](src/adaptive_card_tools/examples.py) has a product comparison, expense form,
|
|
263
|
+
weekly review, approval, questionnaire, KPI dashboard, and a deliberately
|
|
264
|
+
messy small-model call. `python examples/export_examples.py` regenerates
|
|
265
|
+
[examples/generated](examples/generated) (spec, tool call, and both profile outputs) and the docs.
|
|
266
|
+
|
|
267
|
+
## Test and build
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
PYTHONPATH=src python -m unittest discover -s tests -v # core suite, stdlib only
|
|
271
|
+
python -m pip install -e '.[test,integration]' # + official schema, LangChain, ADK
|
|
272
|
+
python -m unittest discover -s tests -v
|
|
273
|
+
python -m pip install -e '.[dev]' && python -m build && python -m twine check dist/*
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
See [docs/VALIDATION.md](docs/VALIDATION.md) for what has been verified.
|
|
277
|
+
|
|
278
|
+
## Sources
|
|
279
|
+
|
|
280
|
+
- [Adaptive Cards documentation and schema explorer](https://adaptivecards.microsoft.com/)
|
|
281
|
+
- [Official core schema](https://adaptivecards.microsoft.com/schemas/adaptive-card.json)
|
|
282
|
+
- [Microsoft host features, ratings and responsive layouts](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format)
|
|
283
|
+
- [Google ADK function tools](https://adk.dev/tools-custom/function-tools/)
|
|
284
|
+
- [LangChain tools](https://docs.langchain.com/oss/python/langchain/tools)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
adaptive_card_tools/__init__.py,sha256=w1w72R4p-fMPurIQB2-LMSRcV2KseSzj7xA6uAveRr0,602
|
|
2
|
+
adaptive_card_tools/builder.py,sha256=ewtHvt4x5T_yrKbK9lYP8zuChKRo2Uu2-tUqR9bl5-8,106753
|
|
3
|
+
adaptive_card_tools/catalog.py,sha256=4F3aTWSm8Fx8ISqFT_6k1c65Ak2fSeM1Ikekq7-dQzY,33678
|
|
4
|
+
adaptive_card_tools/examples.py,sha256=VgA6jtU49eky6-SXvZbImb_pcUXMEjZPimYNFW2KIGM,13660
|
|
5
|
+
adaptive_card_tools/normalize.py,sha256=RK7kKiEdd72n65nuZUMFE2KaiqU8ZQRT7IymhniEx2g,23839
|
|
6
|
+
adaptive_card_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
adaptive_card_tools/theme.py,sha256=bij9s2K84eeVdXzWgQj3d3bY1run0Tbx5pOsLfBf-FA,9427
|
|
8
|
+
adaptive_card_tools/tool.py,sha256=_n7FffRzYuS-KKS5uNTFWRNEJU0UFNMrxuIgQi-BUlQ,8036
|
|
9
|
+
agentic_genui_tools-0.2.0.dist-info/licenses/LICENSE,sha256=yA-1fsPiIyj-pW1akix_jSZLE6UBiO1sN6iDGeV_6oM,1089
|
|
10
|
+
agentic_genui_tools-0.2.0.dist-info/METADATA,sha256=sTUgCrJ5VrSRIu_T_s4bUHiF-MHezr6w_fUmuOdQ73E,14771
|
|
11
|
+
agentic_genui_tools-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
12
|
+
agentic_genui_tools-0.2.0.dist-info/top_level.txt,sha256=t7F0jO9ANAnHXhXw7KIjVdIle3pl5k9_B3-iJ5peLgw,20
|
|
13
|
+
agentic_genui_tools-0.2.0.dist-info/RECORD,,
|