Agentic-GenUI-Tools 0.2.0__tar.gz

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.
Files changed (67) hide show
  1. agentic_genui_tools-0.2.0/.github/workflows/tests.yml +24 -0
  2. agentic_genui_tools-0.2.0/LICENSE +21 -0
  3. agentic_genui_tools-0.2.0/MANIFEST.in +5 -0
  4. agentic_genui_tools-0.2.0/PKG-INFO +284 -0
  5. agentic_genui_tools-0.2.0/README.md +258 -0
  6. agentic_genui_tools-0.2.0/docs/BUSINESS_EXAMPLES.md +526 -0
  7. agentic_genui_tools-0.2.0/docs/ELEMENT_REFERENCE.md +190 -0
  8. agentic_genui_tools-0.2.0/docs/REQUIREMENTS.md +14 -0
  9. agentic_genui_tools-0.2.0/docs/TOOL_DOCSTRING.md +67 -0
  10. agentic_genui_tools-0.2.0/docs/VALIDATION.md +39 -0
  11. agentic_genui_tools-0.2.0/examples/export_examples.py +42 -0
  12. agentic_genui_tools-0.2.0/examples/frontend.js +69 -0
  13. agentic_genui_tools-0.2.0/examples/generated/approval_request.microsoft.json +137 -0
  14. agentic_genui_tools-0.2.0/examples/generated/approval_request.portable.json +131 -0
  15. agentic_genui_tools-0.2.0/examples/generated/approval_request.spec.json +51 -0
  16. agentic_genui_tools-0.2.0/examples/generated/approval_request.tool-call.json +53 -0
  17. agentic_genui_tools-0.2.0/examples/generated/business_objectives_form.microsoft.json +113 -0
  18. agentic_genui_tools-0.2.0/examples/generated/business_objectives_form.portable.json +113 -0
  19. agentic_genui_tools-0.2.0/examples/generated/business_objectives_form.spec.json +17 -0
  20. agentic_genui_tools-0.2.0/examples/generated/business_objectives_form.tool-call.json +19 -0
  21. agentic_genui_tools-0.2.0/examples/generated/component_showcase.microsoft.json +1269 -0
  22. agentic_genui_tools-0.2.0/examples/generated/component_showcase.portable.json +1211 -0
  23. agentic_genui_tools-0.2.0/examples/generated/component_showcase.spec.json +346 -0
  24. agentic_genui_tools-0.2.0/examples/generated/component_showcase.tool-call.json +348 -0
  25. agentic_genui_tools-0.2.0/examples/generated/expense_form.microsoft.json +230 -0
  26. agentic_genui_tools-0.2.0/examples/generated/expense_form.portable.json +230 -0
  27. agentic_genui_tools-0.2.0/examples/generated/expense_form.spec.json +80 -0
  28. agentic_genui_tools-0.2.0/examples/generated/expense_form.tool-call.json +82 -0
  29. agentic_genui_tools-0.2.0/examples/generated/kpi_dashboard.microsoft.json +575 -0
  30. agentic_genui_tools-0.2.0/examples/generated/kpi_dashboard.portable.json +540 -0
  31. agentic_genui_tools-0.2.0/examples/generated/kpi_dashboard.spec.json +70 -0
  32. agentic_genui_tools-0.2.0/examples/generated/kpi_dashboard.tool-call.json +72 -0
  33. agentic_genui_tools-0.2.0/examples/generated/messy_small_model_call.microsoft.json +275 -0
  34. agentic_genui_tools-0.2.0/examples/generated/messy_small_model_call.portable.json +269 -0
  35. agentic_genui_tools-0.2.0/examples/generated/messy_small_model_call.spec.json +56 -0
  36. agentic_genui_tools-0.2.0/examples/generated/messy_small_model_call.tool-call.json +58 -0
  37. agentic_genui_tools-0.2.0/examples/generated/product_comparison.microsoft.json +308 -0
  38. agentic_genui_tools-0.2.0/examples/generated/product_comparison.portable.json +284 -0
  39. agentic_genui_tools-0.2.0/examples/generated/product_comparison.spec.json +120 -0
  40. agentic_genui_tools-0.2.0/examples/generated/product_comparison.tool-call.json +122 -0
  41. agentic_genui_tools-0.2.0/examples/generated/weekly_business_review.microsoft.json +573 -0
  42. agentic_genui_tools-0.2.0/examples/generated/weekly_business_review.portable.json +531 -0
  43. agentic_genui_tools-0.2.0/examples/generated/weekly_business_review.spec.json +112 -0
  44. agentic_genui_tools-0.2.0/examples/generated/weekly_business_review.tool-call.json +114 -0
  45. agentic_genui_tools-0.2.0/examples/google_adk.py +24 -0
  46. agentic_genui_tools-0.2.0/examples/langchain_agent.py +24 -0
  47. agentic_genui_tools-0.2.0/examples/quickstart.py +23 -0
  48. agentic_genui_tools-0.2.0/pyproject.toml +35 -0
  49. agentic_genui_tools-0.2.0/setup.cfg +4 -0
  50. agentic_genui_tools-0.2.0/src/Agentic_GenUI_Tools.egg-info/PKG-INFO +284 -0
  51. agentic_genui_tools-0.2.0/src/Agentic_GenUI_Tools.egg-info/SOURCES.txt +65 -0
  52. agentic_genui_tools-0.2.0/src/Agentic_GenUI_Tools.egg-info/dependency_links.txt +1 -0
  53. agentic_genui_tools-0.2.0/src/Agentic_GenUI_Tools.egg-info/requires.txt +14 -0
  54. agentic_genui_tools-0.2.0/src/Agentic_GenUI_Tools.egg-info/top_level.txt +1 -0
  55. agentic_genui_tools-0.2.0/src/adaptive_card_tools/__init__.py +10 -0
  56. agentic_genui_tools-0.2.0/src/adaptive_card_tools/builder.py +1891 -0
  57. agentic_genui_tools-0.2.0/src/adaptive_card_tools/catalog.py +488 -0
  58. agentic_genui_tools-0.2.0/src/adaptive_card_tools/examples.py +169 -0
  59. agentic_genui_tools-0.2.0/src/adaptive_card_tools/normalize.py +581 -0
  60. agentic_genui_tools-0.2.0/src/adaptive_card_tools/py.typed +0 -0
  61. agentic_genui_tools-0.2.0/src/adaptive_card_tools/theme.py +210 -0
  62. agentic_genui_tools-0.2.0/src/adaptive_card_tools/tool.py +142 -0
  63. agentic_genui_tools-0.2.0/tests/test_cards.py +572 -0
  64. agentic_genui_tools-0.2.0/tests/test_frameworks.py +51 -0
  65. agentic_genui_tools-0.2.0/tests/vendor/LICENSE-MICROSOFT +21 -0
  66. agentic_genui_tools-0.2.0/tests/vendor/NOTICE.md +13 -0
  67. agentic_genui_tools-0.2.0/tests/vendor/adaptive-card.json +2922 -0
@@ -0,0 +1,24 @@
1
+ name: tests
2
+ on: [push, pull_request]
3
+ jobs:
4
+ core:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ python-version: ['3.10', '3.11', '3.12', '3.13']
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: ${{ matrix.python-version }}
14
+ - run: python -m pip install '.[test]'
15
+ - run: python -m unittest discover -s tests -v
16
+ integration:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: '3.12'
23
+ - run: python -m pip install '.[test,integration]'
24
+ - run: python -m unittest discover -s tests -v
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agentic-GenUI-Tools contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include README.md LICENSE
2
+ recursive-include docs *.md
3
+ recursive-include .github *.yml
4
+ recursive-include examples *.py *.js *.json
5
+ recursive-include tests *.py *.json *.md LICENSE-MICROSOFT
@@ -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
+ ![0.2 output](docs/images/after-0.2.png)
57
+
58
+ <details><summary>0.1 output for comparison</summary>
59
+
60
+ ![0.1 output](docs/images/before-0.1.png)
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,258 @@
1
+ # adaptive-card-tools
2
+
3
+ Build modern, deterministic Adaptive Cards from small, forgiving agent tool
4
+ calls. The model describes *what* to show; Python repairs the request,
5
+ applies a consistent visual design, and returns a complete card.
6
+
7
+ Designed for small models such as GPT-4.1 mini and Gemini 2.5 Flash:
8
+
9
+ - **Short prompt.** The tool description is ~1,200 tokens of examples, not a
10
+ schema manual (it was ~8,500 in 0.1.x).
11
+ - **Repair, don't reject.** Wrong type names, Adaptive Card JSON, style keys,
12
+ aliases, typos, strings for numbers and dates, odd option shapes, wrapped
13
+ or broken JSON: all are interpreted. Errors are reserved for requests that
14
+ are unreadable, empty, or over a safety limit.
15
+ - **Accurate.** Repairs change the *form* of a value, never its *facts*. Text
16
+ is never rewritten or truncated, numbers are parsed but never estimated,
17
+ missing chart points are not filled with zeros, and anything that can't be
18
+ read exactly is left out with a warning. Every repair is listed in metadata.
19
+ - **Modern and stable.** The model never chooses styling. Every card gets the
20
+ same hierarchy (title, status badge, KPI tiles, data bars, shaded table
21
+ headers, primary/destructive buttons) built from primitives that render on
22
+ every Adaptive Cards host.
23
+
24
+ Python 3.10+, no runtime dependencies. Import name: `adaptive_card_tools`.
25
+
26
+ Rendered with Microsoft's `adaptivecards` JS SDK. 0.2 with `host_config()` and
27
+ `examples/card-theme.css` (the middle card is the deliberately messy call from
28
+ `examples.py`):
29
+
30
+ ![0.2 output](docs/images/after-0.2.png)
31
+
32
+ <details><summary>0.1 output for comparison</summary>
33
+
34
+ ![0.1 output](docs/images/before-0.1.png)
35
+
36
+ </details>
37
+
38
+ ## Install and use
39
+
40
+ ```bash
41
+ python -m pip install .
42
+ ```
43
+
44
+ ```python
45
+ from adaptive_card_tools import build_adaptive_card
46
+
47
+ result = build_adaptive_card({
48
+ "title": "Weekly sales",
49
+ "subtitle": "Week ending 11 Sep 2026",
50
+ "status": "On track",
51
+ "elements": [
52
+ {"type": "metrics", "items": [
53
+ {"label": "Revenue", "value": "$48,200", "change": "+12%"},
54
+ {"label": "Orders", "value": "312", "change": "-3%"}]},
55
+ {"type": "chart", "chart_type": "bar", "title": "Revenue by region",
56
+ "data": {"North": 18200, "South": 12100, "West": 17900}},
57
+ ],
58
+ })
59
+ result["status"] # "successful"
60
+ result["response"] # the Adaptive Card JSON for your frontend
61
+ result["message"] # a short instruction for the model
62
+ ```
63
+
64
+ ## Add to an agent
65
+
66
+ The function is a plain Python tool; its docstring is the model's prompt
67
+ (see [docs/TOOL_DOCSTRING.md](docs/TOOL_DOCSTRING.md)).
68
+
69
+ ```python
70
+ # Google ADK
71
+ from google.adk.agents import Agent
72
+ from adaptive_card_tools import build_adaptive_card
73
+
74
+ agent = Agent(name="card_assistant", model="gemini-2.5-flash", tools=[build_adaptive_card],
75
+ instruction="When the answer is a form, summary, comparison, dashboard, approval, "
76
+ "table or chart, call build_adaptive_card once with the complete content.")
77
+ ```
78
+
79
+ ```python
80
+ # LangChain, with the rich schema (element types as an enum, which small models follow well)
81
+ from langchain.agents import create_agent
82
+ from langchain_core.tools import StructuredTool
83
+ from adaptive_card_tools import build_adaptive_card, tool_schema
84
+
85
+ schema = tool_schema()
86
+ card_tool = StructuredTool.from_function(build_adaptive_card, name=schema["name"],
87
+ description=schema["description"], args_schema=schema["parameters"])
88
+ agent = create_agent(model="openai:gpt-4.1-mini", tools=[card_tool])
89
+ ```
90
+
91
+ If a provider rejects a free-form object parameter (some Gemini setups and
92
+ strict tool modes do), use `build_adaptive_card_json(request_json: str)`. Its
93
+ parser also repairs single quotes, trailing commas, comments, Python
94
+ `True/None`, code fences, prose around the JSON, and unclosed brackets.
95
+
96
+ `make_adaptive_card_tool(BuilderConfig(...), json_input=False)` returns a tool
97
+ bound to your settings. `describe_adaptive_card(type)` is an optional helper
98
+ tool; small models rarely need it.
99
+
100
+ ## What the model writes
101
+
102
+ ```json
103
+ {"title": "Book a demo", "elements": [
104
+ {"type": "text", "text": "Pick a time that suits you."},
105
+ {"type": "text_input", "id": "name", "label": "Full name", "required": true},
106
+ {"type": "date_input", "id": "day", "label": "Preferred day"},
107
+ {"type": "choice", "id": "size", "label": "Team size", "options": ["1-10", "11-50", "51+"]}],
108
+ "submit_label": "Request demo"}
109
+ ```
110
+
111
+ | Group | Types |
112
+ |---|---|
113
+ | Content | `heading`, `text` (markdown), `note`, `list`, `facts`, `table`, `badge`, `image`, `images`, `media` |
114
+ | Data | `metrics` (KPI tiles), `chart` (`bar`, `horizontal_bar`, `line`, `pie`, `donut`, `stacked_bar`, `grouped_bar`, `gauge`), `progress`, `rating` |
115
+ | Layout | `section`, `columns`, `divider`, `buttons` |
116
+ | Inputs | `text_input`, `number_input`, `date_input`, `time_input`, `datetime_input`, `choice`, `checkbox`, `rating_input` |
117
+
118
+ Card-level fields: `title`, `subtitle`, `status` (a coloured badge next to the
119
+ title), `elements`, `submit_label`, `buttons`. Forms get a Submit button
120
+ automatically; `"buttons": []` suppresses it. Every field name and alias is
121
+ listed in [docs/ELEMENT_REFERENCE.md](docs/ELEMENT_REFERENCE.md).
122
+
123
+ ## Normalization
124
+
125
+ What small models actually send, and what happens:
126
+
127
+ | The model sends | The builder does |
128
+ |---|---|
129
+ | `{"request": {...}}`, `{"card": {...}}`, a JSON string, a fenced block, prose around JSON, `{"name":..., "arguments": "..."}` | Unwraps and parses it |
130
+ | 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 |
131
+ | 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…) |
132
+ | No `type`, or an unknown one | Infers it from the fields (`options` → choice, `rows` → table, image URL → image…) |
133
+ | `size`, `color`, `weight`, `spacing`, `width`, `style`… | Ignores styling (reported), but uses it as a hint where it carries meaning |
134
+ | `question`, `prompt`, `isRequired`, `mandatory`, `choices`, `default`, `maxLength`… | Maps field aliases; `name` becomes the id if it looks like one, else the label |
135
+ | `description`, `subtitle`, `caption`, `note` on any element | Shows the text instead of dropping it |
136
+ | `"1,200"`, `"$1.2M"`, `"75%"`, `"(40)"`, `"4.5/5"`, `"12 GB"`; `"yes"`, `"Required"`, `1` | Parses numbers and booleans |
137
+ | `"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 |
138
+ | Options as strings, `"A, B, C"`, `{title: value}`, `{"s": "Small"}`, `[{"id", "label"}]`, `[[title, value]]` | Normalizes options; defaults match case-insensitively (`"small"` → `Small`) |
139
+ | A dropdown with no options | Asks yes/no for a yes/no question, otherwise shows a text box |
140
+ | Facts as `"Key: Value"` strings, pairs, `{label, value}` objects, or inline keys | Builds a fact set |
141
+ | Tables as records, lists with a header row, ragged rows, `{column: [values]}`, markdown tables | Builds a table; nothing is dropped, blanks show as `—` |
142
+ | 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 |
143
+ | Line chart over categories, pie with negatives or several series | Switches to a bar chart (reported) |
144
+ | Series with different categories, non-numeric values | Shows a table with blanks rather than inventing zeros |
145
+ | Adjacent single `metric`, `button`, `badge` or `column` elements | Merges them into one row |
146
+ | `www.example.com`, `//cdn…`, spaces in URLs | Normalizes to https; `javascript:`, `data:`, `file:` and relative URLs are dropped with a warning |
147
+ | Buttons without data | Adds `{"action": "<label slug>"}` so the app can tell them apart; data keys that would overwrite answers are removed |
148
+ | Duplicate or invalid ids | Sanitizes and de-duplicates (reported in `metadata.inputIds`) |
149
+
150
+ Nothing is guessed that would change meaning: an unmatched default, an
151
+ unreadable date ("next Tuesday", "March 5" with no year), an out-of-range
152
+ value, or a bad URL is left out and reported in `metadata.warnings`, which
153
+ is also summarized in `message` so the model can decide whether to retry.
154
+
155
+ ## Visual design
156
+
157
+ The design lives in [src/adaptive_card_tools/theme.py](src/adaptive_card_tools/theme.py). Meaning is carried by
158
+ typography, text colour and separators, which every host renders:
159
+
160
+ - Header: large title, subtle subtitle, status badge aligned right.
161
+ - `metrics`: big-number tiles with ▲/▼ changes in green/red (`lower_is_better` flips colours).
162
+ - Charts in the portable profile: label | bar | value rows drawn with block
163
+ glyphs, with exact values and pie percentages as text. Progress bars have a
164
+ track. Ratings show stars plus the exact value.
165
+ - Tables: shaded bold header row, numeric columns right-aligned, content-based column widths.
166
+ - Sections: bold title, a separator at the top level. Columns: tiles.
167
+ - Buttons: the primary action is `positive`; reject/delete/decline are `destructive`.
168
+
169
+ Container backgrounds (tiles, table header shading) depend on the host config.
170
+ They show in Teams and with the recommended config, and fall back to plain
171
+ columns elsewhere, so nothing is lost. For the JS SDK, pass
172
+ `adaptive_card_tools.host_config()` to `new AdaptiveCards.HostConfig(...)` and
173
+ include [examples/card-theme.css](examples/card-theme.css) for buttons and inputs; see
174
+ [examples/frontend.js](examples/frontend.js).
175
+
176
+ ## Renderer profiles
177
+
178
+ `portable` (default) emits core Adaptive Cards 1.5 only. `microsoft` emits
179
+ native extensions (charts, `Badge`, `Rating`, `ProgressBar`, `ProgressRing`,
180
+ `Input.Rating`) with a core fallback on each.
181
+
182
+ ```python
183
+ from adaptive_card_tools import BuilderConfig, make_adaptive_card_tool
184
+
185
+ card_tool = make_adaptive_card_tool(BuilderConfig(
186
+ profile="microsoft",
187
+ native_types=("Badge", "Rating", "Chart.Line"), # optional allow-list
188
+ available_width_px=1200,
189
+ day_first=False,
190
+ ))
191
+ ```
192
+
193
+ Native charts are only used when the data can be drawn exactly; series with
194
+ gaps or non-numeric values use the table fallback. The model cannot select
195
+ the profile, styling or schema version.
196
+
197
+ ## Result envelope
198
+
199
+ ```python
200
+ {
201
+ "status": "successful", # or "error"
202
+ "suggestedWidth": "60%", # 50%, 60%, 80%, 100% or "100%+" (a signal, not CSS)
203
+ "response": {...}, # the card; forward unchanged to the renderer
204
+ "message": "Card built and ready to display. ...", # for the model
205
+ "metadata": {
206
+ "profile": "portable", "schemaVersion": "1.5",
207
+ "inputIds": ["name", "day", "size"],
208
+ "choiceValues": {"size": [{"title": "1-10", "value": "1-10"}, ...]},
209
+ "layout": {"widthPercent": 60, "estimatedMinWidthPx": 480, "overflow": "none", ...},
210
+ "warnings": [], # content left out or reinterpreted
211
+ "normalizations": [...] # every harmless repair
212
+ }
213
+ }
214
+ ```
215
+
216
+ On error, `response` and `message` explain the problem and `errors` holds
217
+ `{path, code, message}`. Errors happen only for unreadable JSON, a request
218
+ with nothing to display, or configured limits (bytes, depth, nodes,
219
+ elements, output size).
220
+
221
+ Width is an estimate from structural minima (columns add, stacked content
222
+ takes the widest); the frontend adapter in `examples/frontend.js` applies it
223
+ and scrolls when needed.
224
+
225
+ ## Application responsibilities
226
+
227
+ - Render `response` unchanged; handle `Action.Submit` and `Action.OpenUrl` in the frontend.
228
+ - Validate and authorize every submission on the server. Generated ids are
229
+ deterministic for the same request, not stable across edits; give explicit
230
+ ids when your backend expects particular keys.
231
+ - URLs are validated (http/https, no credentials) but never fetched or allow-listed.
232
+ - No network access, randomness, clock or LLM call happens in the build path.
233
+
234
+ ## Examples
235
+
236
+ [src/adaptive_card_tools/examples.py](src/adaptive_card_tools/examples.py) has a product comparison, expense form,
237
+ weekly review, approval, questionnaire, KPI dashboard, and a deliberately
238
+ messy small-model call. `python examples/export_examples.py` regenerates
239
+ [examples/generated](examples/generated) (spec, tool call, and both profile outputs) and the docs.
240
+
241
+ ## Test and build
242
+
243
+ ```bash
244
+ PYTHONPATH=src python -m unittest discover -s tests -v # core suite, stdlib only
245
+ python -m pip install -e '.[test,integration]' # + official schema, LangChain, ADK
246
+ python -m unittest discover -s tests -v
247
+ python -m pip install -e '.[dev]' && python -m build && python -m twine check dist/*
248
+ ```
249
+
250
+ See [docs/VALIDATION.md](docs/VALIDATION.md) for what has been verified.
251
+
252
+ ## Sources
253
+
254
+ - [Adaptive Cards documentation and schema explorer](https://adaptivecards.microsoft.com/)
255
+ - [Official core schema](https://adaptivecards.microsoft.com/schemas/adaptive-card.json)
256
+ - [Microsoft host features, ratings and responsive layouts](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format)
257
+ - [Google ADK function tools](https://adk.dev/tools-custom/function-tools/)
258
+ - [LangChain tools](https://docs.langchain.com/oss/python/langchain/tools)