Skip to main content

adaptive-card-tools

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:

  • Short prompt. The tool description is ~1,200 tokens of examples, not a schema manual (it was ~8,500 in 0.1.x).
  • Repair, don't reject. Wrong type names, Adaptive Card JSON, style keys, aliases, typos, strings for numbers and dates, odd option shapes, wrapped or broken JSON: all are interpreted. Errors are reserved for requests that are unreadable, empty, or over a safety limit.
  • Accurate. Repairs change the form of a value, never its facts. Text is never rewritten or truncated, numbers are parsed but never estimated, missing chart points are not filled with zeros, and anything that can't be read exactly is left out with a warning. Every repair is listed in metadata.
  • Modern and stable. The model never chooses styling. Every card gets the same hierarchy (title, status badge, KPI tiles, data bars, shaded table headers, primary/destructive buttons) built from primitives that render on every Adaptive Cards host.

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):

0.2 output

0.1 output for comparison

0.1 output

Install and use

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

Add to an agent

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.

What the model writes

{"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.

Normalization

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.

Visual design

The design lives in src/adaptive_card_tools/theme.py. Meaning is carried by typography, text colour and separators, which every host renders:

  • Header: large title, subtle subtitle, status badge aligned right.
  • metrics: big-number tiles with ▲/▼ changes in green/red (lower_is_better flips colours).
  • Charts in the portable profile: label | bar | value rows drawn with block glyphs, with exact values and pie percentages as text. Progress bars have a track. Ratings show stars plus the exact value.
  • Tables: shaded bold header row, numeric columns right-aligned, content-based column widths.
  • Sections: bold title, a separator at the top level. Columns: tiles.
  • Buttons: the primary action is 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.

Renderer profiles

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.

Result envelope

{
  "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.

Application responsibilities

  • Render response unchanged; handle Action.Submit and Action.OpenUrl in the frontend.
  • Validate and authorize every submission on the server. Generated ids are deterministic for the same request, not stable across edits; give explicit ids when your backend expects particular keys.
  • URLs are validated (http/https, no credentials) but never fetched or allow-listed.
  • No network access, randomness, clock or LLM call happens in the build path.

Examples

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.

Test and build

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.

Sources

Metadata

Release files for Agentic-GenUI-Tools 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for Agentic-GenUI-Tools 0.2.0
File Size Uploaded
agentic_genui_tools-0.2.0.tar.gz 124.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for Agentic-GenUI-Tools 0.2.0
File Interpreter ABI Platform
agentic_genui_tools-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 190.5 kB

Release files / agentic_genui_tools-0.2.0.tar.gz

Download URL agentic_genui_tools-0.2.0.tar.gz
Size 124.3 kB
Tags Source
SHA-256 checksum
How to use checksums
31b989908982c4ab32b39a46b3a88787cebb5813a92e289af4184864da0f928d
BLAKE2b-256 checksum
How to use checksums
5388440fe4189bbe376ce905ee34662e0aa3eec2c95085bdd6873f2c4fdd5ee3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.1

Release files / agentic_genui_tools-0.2.0-py3-none-any.whl

Download URL agentic_genui_tools-0.2.0-py3-none-any.whl
Size 66.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a5e3afada7412f83c4add8dc927fe3ae2e87a9a1ebd71aebcf0035f7e77831f
BLAKE2b-256 checksum
How to use checksums
848861262f6a0b9cd29aa4b2312327ae130354da04ec6a85d57e1883d10c6e13
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.1

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page