Observed since 20 Oct, 22:16 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Agentic-GenUI-Tools
Modern, deterministic Adaptive Cards from small, forgiving agent tool calls
Build modern, deterministic Adaptive Cards from small, forgiving agent tool calls. The model describes what to show; Python repairs the request, applies a consistent visual design, and returns a complete card.
Designed for small models such as GPT-4.1 mini and Gemini 2.5 Flash:
Python 3.10+, no runtime dependencies. Import name: adaptive_card_tools.
Rendered with Microsoft's adaptivecards JS SDK. 0.2 with host_config() and
examples/card-theme.css (the middle card is the deliberately messy call from
examples.py):
python -m pip install .
from adaptive_card_tools import build_adaptive_card
result = build_adaptive_card({
"title": "Weekly sales",
"subtitle": "Week ending 11 Sep 2026",
"status": "On track",
"elements": [
{"type": "metrics", "items": [
{"label": "Revenue", "value": "$48,200", "change": "+12%"},
{"label": "Orders", "value": "312", "change": "-3%"}]},
{"type": "chart", "chart_type": "bar", "title": "Revenue by region",
"data": {"North": 18200, "South": 12100, "West": 17900}},
],
})
result["status"] # "successful"
result["response"] # the Adaptive Card JSON for your frontend
result["message"] # a short instruction for the model
The function is a plain Python tool; its docstring is the model's prompt (see docs/TOOL_DOCSTRING.md).
# Google ADK
from google.adk.agents import Agent
from adaptive_card_tools import build_adaptive_card
agent = Agent(name="card_assistant", model="gemini-2.5-flash", tools=[build_adaptive_card],
instruction="When the answer is a form, summary, comparison, dashboard, approval, "
"table or chart, call build_adaptive_card once with the complete content.")
# LangChain, with the rich schema (element types as an enum, which small models follow well)
from langchain.agents import create_agent
from langchain_core.tools import StructuredTool
from adaptive_card_tools import build_adaptive_card, tool_schema
schema = tool_schema()
card_tool = StructuredTool.from_function(build_adaptive_card, name=schema["name"],
description=schema["description"], args_schema=schema["parameters"])
agent = create_agent(model="openai:gpt-4.1-mini", tools=[card_tool])
If a provider rejects a free-form object parameter (some Gemini setups and
strict tool modes do), use build_adaptive_card_json(request_json: str). Its
parser also repairs single quotes, trailing commas, comments, Python
True/None, code fences, prose around the JSON, and unclosed brackets.
make_adaptive_card_tool(BuilderConfig(...), json_input=False) returns a tool
bound to your settings. describe_adaptive_card(type) is an optional helper
tool; small models rarely need it.
{"title": "Book a demo", "elements": [
{"type": "text", "text": "Pick a time that suits you."},
{"type": "text_input", "id": "name", "label": "Full name", "required": true},
{"type": "date_input", "id": "day", "label": "Preferred day"},
{"type": "choice", "id": "size", "label": "Team size", "options": ["1-10", "11-50", "51+"]}],
"submit_label": "Request demo"}
| Group | Types |
|---|---|
| Content | heading, text (markdown), note, list, facts, table, badge, image, images, media |
| Data | metrics (KPI tiles), chart (bar, horizontal_bar, line, pie, donut, stacked_bar, grouped_bar, gauge), progress, rating |
| Layout | section, columns, divider, buttons |
| Inputs | text_input, number_input, date_input, time_input, datetime_input, choice, checkbox, rating_input |
Card-level fields: title, subtitle, status (a coloured badge next to the
title), elements, submit_label, buttons. Forms get a Submit button
automatically; "buttons": [] suppresses it. Every field name and alias is
listed in docs/ELEMENT_REFERENCE.md.
What small models actually send, and what happens:
| The model sends | The builder does |
|---|---|
{"request": {...}}, {"card": {...}}, a JSON string, a fenced block, prose around JSON, {"name":..., "arguments": "..."} | Unwraps and parses it |
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 |
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…) |
No type, or an unknown one | Infers it from the fields (options → choice, rows → table, image URL → image…) |
size, color, weight, spacing, width, style… | Ignores styling (reported), but uses it as a hint where it carries meaning |
question, prompt, isRequired, mandatory, choices, default, maxLength… | Maps field aliases; name becomes the id if it looks like one, else the label |
description, subtitle, caption, note on any element | Shows the text instead of dropping it |
"1,200", "$1.2M", "75%", "(40)", "4.5/5", "12 GB"; "yes", "Required", 1 | Parses numbers and booleans |
"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 |
Options as strings, "A, B, C", {title: value}, {"s": "Small"}, [{"id", "label"}], [[title, value]] | Normalizes options; defaults match case-insensitively ("small" → Small) |
| A dropdown with no options | Asks yes/no for a yes/no question, otherwise shows a text box |
Facts as "Key: Value" strings, pairs, {label, value} objects, or inline keys | Builds a fact set |
Tables as records, lists with a header row, ragged rows, {column: [values]}, markdown tables | Builds a table; nothing is dropped, blanks show as — |
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 |
| Line chart over categories, pie with negatives or several series | Switches to a bar chart (reported) |
| Series with different categories, non-numeric values | Shows a table with blanks rather than inventing zeros |
Adjacent single metric, button, badge or column elements | Merges them into one row |
www.example.com, //cdn…, spaces in URLs | Normalizes to https; javascript:, data:, file: and relative URLs are dropped with a warning |
| Buttons without data | Adds {"action": "<label slug>"} so the app can tell them apart; data keys that would overwrite answers are removed |
| Duplicate or invalid ids | Sanitizes and de-duplicates (reported in metadata.inputIds) |
Nothing is guessed that would change meaning: an unmatched default, an
unreadable date ("next Tuesday", "March 5" with no year), an out-of-range
value, or a bad URL is left out and reported in metadata.warnings, which
is also summarized in message so the model can decide whether to retry.
The design lives in src/adaptive_card_tools/theme.py. Meaning is carried by typography, text colour and separators, which every host renders:
metrics: big-number tiles with ▲/▼ changes in green/red (lower_is_better flips colours).positive; reject/delete/decline are destructive.Container backgrounds (tiles, table header shading) depend on the host config.
They show in Teams and with the recommended config, and fall back to plain
columns elsewhere, so nothing is lost. For the JS SDK, pass
adaptive_card_tools.host_config() to new AdaptiveCards.HostConfig(...) and
include examples/card-theme.css for buttons and inputs; see
examples/frontend.js.
portable (default) emits core Adaptive Cards 1.5 only. microsoft emits
native extensions (charts, Badge, Rating, ProgressBar, ProgressRing,
Input.Rating) with a core fallback on each.
from adaptive_card_tools import BuilderConfig, make_adaptive_card_tool
card_tool = make_adaptive_card_tool(BuilderConfig(
profile="microsoft",
native_types=("Badge", "Rating", "Chart.Line"), # optional allow-list
available_width_px=1200,
day_first=False,
))
Native charts are only used when the data can be drawn exactly; series with gaps or non-numeric values use the table fallback. The model cannot select the profile, styling or schema version.
{
"status": "successful", # or "error"
"suggestedWidth": "60%", # 50%, 60%, 80%, 100% or "100%+" (a signal, not CSS)
"response": {...}, # the card; forward unchanged to the renderer
"message": "Card built and ready to display. ...", # for the model
"metadata": {
"profile": "portable", "schemaVersion": "1.5",
"inputIds": ["name", "day", "size"],
"choiceValues": {"size": [{"title": "1-10", "value": "1-10"}, ...]},
"layout": {"widthPercent": 60, "estimatedMinWidthPx": 480, "overflow": "none", ...},
"warnings": [], # content left out or reinterpreted
"normalizations": [...] # every harmless repair
}
}
On error, response and message explain the problem and errors holds
{path, code, message}. Errors happen only for unreadable JSON, a request
with nothing to display, or configured limits (bytes, depth, nodes,
elements, output size).
Width is an estimate from structural minima (columns add, stacked content
takes the widest); the frontend adapter in examples/frontend.js applies it
and scrolls when needed.
response unchanged; handle Action.Submit and Action.OpenUrl in the frontend.src/adaptive_card_tools/examples.py has a product comparison, expense form,
weekly review, approval, questionnaire, KPI dashboard, and a deliberately
messy small-model call. python examples/export_examples.py regenerates
examples/generated (spec, tool call, and both profile outputs) and the docs.
PYTHONPATH=src python -m unittest discover -s tests -v # core suite, stdlib only
python -m pip install -e '.[test,integration]' # + official schema, LangChain, ADK
python -m unittest discover -s tests -v
python -m pip install -e '.[dev]' && python -m build && python -m twine check dist/*
See docs/VALIDATION.md for what has been verified.
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
pytestEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | pytest>=8; extra == "test" | runtime |
jsonschemaEnvironment{
"extras": "",
"marker": "extra == \"test\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | jsonschema>=4; extra == "test" | runtime |
google-adkEnvironment{
"extras": "",
"marker": "extra == \"integration\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | google-adk>=1; extra == "integration" | runtime |
langchain-coreEnvironment{
"extras": "",
"marker": "extra == \"integration\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | langchain-core>=0.3; extra == "integration" | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | pytest>=8; extra == "dev" | runtime |
jsonschemaEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | jsonschema>=4; extra == "dev" | runtime |
buildEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | build>=1; extra == "dev" | runtime |
twineEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.10",
"evaluation": "NOT_EVALUATED"
} | twine>=5; extra == "dev" | runtime |
bdist_wheel · 64.67 KB · Python py3
sdist · 121.36 KB · Python source
Observed since 20 Oct, 22:16 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:a1ed75fa308ddfcef5a9e7103c16cf4143875ecca108afa2a644c254cec245f5