{Připravte se} – nebo to nechte na Copilotovi! Od nuly ke stovce s Adaptive Cards
Přestaňte ručně psát JSON pro Adaptive Cards – raději investujte úsilí do toho, abyste se tomu vyhnuli.
V poslední době jsem se trochu víc ponořil do Adaptive Cards. Kromě pár jednoduchých formulářů a drobných úprav existujících karet jsem doteď neměl důvod jít do větší hloubky. U uživatelů ale evidentně bodují – kdo byl na některém z našich bootcamp events, ten viděl registrační formulář, kterým první den zajišťujeme provisioning environments. A zpětná vazba na tenhle způsob práce bývá většinou hodně pozitivní.
Když jsem se do toho ale pustil naplno, velmi rychle jsem se posunul od:
- stavění karet ve WYSIWYG editoru, přes
- ruční psaní JSONu, až po
- situaci, kdy se JSON píše za mě,
- pak se generuje za běhu a automaticky se uživatelům rovnou zobrazuje,
- a nakonec až k tomu, že se za běhu nejen vygeneruje a zobrazí, ale zároveň se i odpovědi vyhodnotí smysluplným způsobem – a to bez toho, abych napsal jedinou složenou závorku
Zní to zajímavě? Pojďme se podívat, jak dělat skvělé věci s (nebo spíš bez psaní) JSONu!
Huh? Aren’t We All About Conversational Experiences Now?
Konverzační rozhraní jsou skvělá, ale někdy se klasický formulář prostě nedá ničím nahradit. Pokud potřebujete najednou posbírat víc údajů a rovnou je i validovat, Adaptive Card před uživatelem často dává perfektní smysl.
Jenže pokud jste na tom podobně jako já, v tu chvíli obvykle přichází část, kterou raději nebudu citovat: ručně skládat JSON pro Adaptive Card je piplačka a často i slušně frustrující. Adaptive Card Designer v Copilot Studio mám rád – low-code WYSIWYG přístup je super – ale u složitějších formulářů mi přišel dost těžkopádný. Neexistuje něco lepšího?
Lepší cesta
Řešení mých trápení s JSONem mi celou dobu leželo přímo před očima – Test Pane. Možná jsem na to přišel trochu pozdě, ale teprve nedávno mi došlo, jak dobře umí Copilot Studio samo nabídnout použitelný „startovní“ návrh.
Pro ukázku se podívejme, co si Test Pane vezme z následujícího zadání:
1
2
3
4
5
6
7
8
9
10
11
12
13
Napište JSON pro Adaptive Card, který uživatele vyzve k zadání stravovacích preferencí.
Každou část umístěte do containerů.
V první části uveďte kartu nadpisem a krátkým úvodem, proč tyto informace sbíráte (aby bylo možné na akci zajistit vhodný výběr jídla a pití).
Ve druhé části se zeptejte na jeden výběr z běžných typů stravování (např. vegan, vegetarian apod. – připravte pro to rozšířený, co nejkompletnější seznam možností) a přidejte podobné otázky na oblíbené jídlo a oblíbený nápoj, u obou s populárními možnostmi (v kompaktních seznamech).
V poslední části přidejte odkaz na dokument se zásadami ochrany osobních údajů s placeholder URL a tlačítko pro odeslání.
Zajistěte, aby byla všechna pole povinná, každé mělo odpovídající popisek a smysluplnou chybovou hlášku. Pro pozadí v každé části použijte bílé opakující se obrázkové pozadí s motivem jídla.
Text v celé kartě výrazně „ozdobte“ emoji s tématikou jídla.
To vypadá nadějně!
Po rychlém copy/paste se karta vykreslí v Adaptive Card Designer:
Copilot Studio mi dokonce rovnou nastavilo i output variables pro použití v topicu – a v konverzaci to vypadá skvěle:
Vy jste fakt řekli Copy/Paste?!
Dost možná to už řada z Vás dělá. V tomhle rychle se měnícím světě je někdy těžké poznat, co je „wow“ a co už je dneska běžná věc. Každopádně jsem si říkal, že to jde udělat líp. JSON, který vygeneruje LLM, je přece jen obyčejný string, ne? A se stringy umíme pracovat a předávat je dál, takže by to mělo jít řešit za běhu. Otázka je, jestli je to jen efektní trik, nebo to má reálný přínos. Pro teď berme, že hlavně ten efektní trik.
Ať to vypadá dobře
Zkuste se zamyslet nad tím, jak prezentovat informace získané z nástrojů. Copilot Studio umí zobrazit výsledek nástroje jako Adaptive Card, ale co když chcete zkombinovat data z více nástrojů? A klidně i zobrazit úplně jiný formát podle toho, co jednotlivé nástroje vrátí?
Například: mám agenta, který mi pomáhá plánovat cesty. Agent má nástroj na vyhledání informací o trase, které se můžou výrazně lišit podle zvoleného způsobu dopravy:
- U dlouhých cest autem chci vědět, kde jsou po cestě odpočívadla a čerpací stanice
- U cest vlakem potřebuji vědět, kde přestupovat, jak dlouho trvá každý úsek a kde vlak staví
- U cest letadlem mě zajímají dostupné časy letů
Agent má zároveň nástroj, který umí zjistit počasí v konkrétní lokalitě, a já bych to chtěl spojit s informacemi výše do jednoho přehledného „snapshotu“:
Všimněte si, že informace o cestě i počasí jsou spojené do jedné přehledné karty, která vypadá dobře a ukazuje všechny podstatné údaje.
Když ale požádám o jinou trasu, dostanu jiné informace. Tentokrát nástroj pro trasu vyhodnotil, že je vhodnější cesta vlakem, a agent proto zobrazil více úseků celé cesty včetně přestupů a zastávek.
Jak to celé funguje? V tomhle příkladu jsem chtěl co nejvíc spoléhat na generative orchestration, aby odvedla hlavní práci – volání nástrojů i generování odpovědi. Nakonec jsem zvolil jedno jediné topic, které zachytává a (když je to potřeba) upravuje odpovědi posílané uživateli. Tohle topic se spouští na OnGeneratedResponse, dá mi přístup k System.Response.FormattedText (tedy k zprávě, která se má uživateli právě odeslat) a umožní mi rozhodnout, jestli zprávu zachytím a změním.
Jádrem celého řešení je Custom Prompt uvnitř tohoto topic, který si bere několik vstupů:
- Zprávu, která se má odeslat uživateli
- Objekt, který reprezentuje kolekci dvojic scénář/popisu formátu (inicializoval jsem ho jako globální proměnnou v Conversation Start). Například:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"scenario":"nejvhodnější pro dlouhé cesty autem",
"formdescription":"Na https://www.pexels.com/search/{insert destination here} vyhledejte obrázek související s cílem (pokud žádný nenajdete, obrázek nevypisujte). Otevřete formulář s tímto obrázkem. Zobrazte výchozí místo a cíl jako textová pole, stručný přehled počasí v cíli, odhadovaný čas jízdy a seznam doporučených zastávek. U každé zastávky zobrazte seznam obchodů spolu s vhodným emoji podle typu podniku. Bohatě doplňte dalšími vhodnými emoji"
},
{
"scenario":"nejvhodnější pro plánování cesty letadlem",
"formdescription":"Otevřete formulář s obrázkem letadla. Zobrazte výchozí místo a cíl, odhadovanou dobu cesty a seznam zajímavých míst"
},
{
"scenario":"nejvhodnější pro cestování vlakem",
"formdescription":"Otevřete formulář s obrázkem rušného nástupiště. Na začátku formuláře zobrazte výchozí místo, každou jednotlivou jízdu vlakem jako samostatnou sekci včetně zastávek a formulář zakončete názvem cíle, obrázkem cíle a informacemi o počasí v cíli"
}
]
Prompt vypadá takto. V podstatě jen říká: „vyhodnoťte, jestli se na zprávu, která se má odeslat, hodí některý ze scénářů v JSONu – a pokud ano, použijte ho pro vygenerování JSONu“:
Když uživatel požádá agenta o nalezení trasy mezi dvěma místy, proběhne následující:
- Orchestrator rozpozná, že je potřeba zavolat nástroje pro vyhledání informací o trase a o počasí, a spustí oba
- Při sestavování původní odpovědi se vyvolá událost OnGeneratedResponse a spustí se moje topic
- Moje topic předá původní odpověď a objekt scenario/formdescription do custom promptu
- Custom prompt vyhodnotí, jestli má scénář, který odpovídá odpovědi, a pokud ano, vygeneruje odpovídající Adaptive Card JSON
- Výstup se vrátí zpět do topic, která zkontroluje, jestli se karta vygenerovala, a pokud ano, pošle JSON do uzlu Adaptive Card:
Níže větvím topic podle toho, jestli prompt vygeneroval formulář, nebo ne…
…a pak posílám výstupní JSON do uzlu Message, který zobrazí Adaptive Card (Message místo „Ask with Adaptive Card“, protože nečekám odpověď a nechci blokovat běh topic).
Teď musím přidat poměrně zásadní upozornění: tenhle přístup vypadá skvěle na screenshotech, ale v praxi – podle toho, jakým způsobem se agent používá – není uživatelský zážitek ideální. V situacích, kde je podporované streamování odpovědi (například v Test Pane), se uživateli nejdřív pošle část původní naformátované odpovědi a teprve potom se spustí topic, který ji přeformátuje. Tomu by se dalo předejít řízenějším flow – tedy kdyby volání nástrojů inicioval topic a nespoléhalo se to tolik na orchestrator. V dalším příkladu ukážu, jak na to.
Co Adaptive Card Submission?
Dostali jsme se do bodu, kdy se Adaptive Card JSON generuje za běhu a dynamicky: podle přirozeného jazykového popisu formulářů, které mám definované v globální proměnné, se zobrazuje zásadně odlišný obsah. To je hodně užitečné a rozhodně to překonává ruční ladění JSONu ve stylu „kde mi zase chybí čárka nebo složená závorka“.
Pořád mi ale přijde, že se dá jít dál. Co kdybychom uměli za běhu pochopit i to, jak uživatel na Adaptive Card odpověděl? A co kdybychom kolem více chytrých kroků postavili hezky vypadající interaktivní proces, který bude mít reálnou hodnotu v praxi?
Rozhodovat o tom, jaká data zobrazit, je jedna věc. Navrhnout formulář tak, aby ho člověk co nejrychleji a bez chyb vyplnil, už vyžaduje další úvahy. Nebylo by bezpečnější nechat vývojáře napsat statický, řízený formulář, u kterého přesně víme, co se zobrazí? Možná…
Nenuťte mě přemýšlet…
Každý, kdo někdy dělal digitální formuláře, ví, že dynamika je téměř vždy nutnost. Spousta projektů digitální transformace začíná tím, že se „jen“ převedou papírové formuláře do elektronické podoby – a teprve pak dojde, že to může být chytřejší:
- „Proč mám vyplňovat fakturační i doručovací adresu, když jsou stejné?“
- „Proč mám vyplňovat část o ženském zdraví, když už jsem uvedl, že jsem muž?“
- „Proč tu vidím tolik možností pro kraj/region, když už jste ode mě dostali informaci, ve které zemi jsem?“
Realita je taková, že uživatelé chytrá řešení ne chtějí očekávají. A pokud před ně stavíte formulář, abyste od nich získali informace, musíte to udělat inteligentně.
Co to má společného s Adaptive Cards? Ve chvíli, kdy od uživatele sbíráte a ověřujete více údajů, musí to být cílené. Musí to dávat smysl, být bez zbytečného tření a přizpůsobené situaci. Card má posbírat vše potřebné, aniž by se z toho stala otravná administrativní překážka. Ano, určitou míru dynamiky zvládnete přes proměnné – schovat/zobrazit pole nebo předvyplnit volby – jenže s rostoucím počtem proměnných roste složitost prakticky exponenciálně.
Pojďme tedy generovat formuláře, které uživatelům dávají smysl podle jejich záměru, a jejich vstupy využít k tomu, abychom vraceli smysluplné odpovědi – i v situaci, kdy je kombinací a proměnných prakticky nekonečno.
Context Specific Adaptive Cards
V další části se podívám na konkrétní příklad: digitálního travel agenta, jehož cílem je pomoct uživateli sestavit itinerář pro cestu do vybrané země.
Co od toho uživatel očekává? Chce doporučení, která odpovídají tomu, co ho jako turistu baví. Potřebuje, aby agent pochopil jeho záměr a připravil itinerář, který bude víc než jen výpis z vyhledávače typu „cool things to do in this country“. Chce nápady, které dávají smysl v rámci plánovaných termínů, sedí na konkrétní zemi, kam míří, a hlavně ho budou bavit podle jeho osobních preferencí.
Jasně, šlo by to vyřešit čistě konverzačně. Jenže to neustálé doptávání, aby agent opravdu pochopil, co je potřeba, se může rychle změnit v únavné kolečko. Formulář s pár cílenými otázkami, navržený tak, aby rychle vytěžil uživatelův záměr, je často přesně to, co potřebujete: pomůže vytvořit „turistický profil“ a z něj pak poskládat konkrétní a relevantní itinerář.
Pojďme se podívat, jak můj Travel Agent odpoví uživateli, který plánuje cestu do Španělska:
Na první pohled to vypadá jako docela generický formulář — dokud ho neporovnáte s podobným požadavkem pro San Marino:
Poznámka: San Marino nemá pláže (na rozdíl od Španělska) a obsah formuláře je tomu odpovídajícím způsobem přizpůsobený.
A nejde jen o to — i jednotlivé možnosti u každé otázky jsou přizpůsobené konkrétní zemi:
Formulář navíc obsahuje i pole pro začátek a konec cesty. S těmito informacemi už máte vše potřebné k tomu, abyste určili, jaký typ cestovatele uživatel je — a na tom pak postavili personalizovaná doporučení.
Tyto informace pak můžete použít k vygenerování itineráře na míru podle uživatelových preferencí:
Hezké, že? Ale to nejdůležitější…
Všechno tady vzniklo za běhu — jak generování formuláře, tak i interpretace uživatelovy odpovědi.
Jak to celé poskládat dohromady
Tentokrát jsem do vlastního promptu dal detailnější instrukce – protože měl na starosti vytvořit Adaptive Card JSON a jako vstup bral jenom zemi:
Pak jsem vytvořil ještě druhý prompt pro sestavení turistického profilu. Ten už bere jako vstupy zemi, původní Adaptive Card a odpověď uživatele po odeslání formuláře. Původní JSON karty jsem přidal záměrně – chtěl jsem, aby prompt dokázal z odpovědí odvodit nejen preference, ale i to, co uživatel naopak nechce. Zemi jsem poslal také, aby měl prompt co nejvíc kontextu pro rozhodování o potřebách uživatele.
Celou cestu jsem zabalil do topicu, který se spustí ve chvíli, kdy uživatel chce naplánovat cestu do nějaké země.
Topic vyžadoval drobné úpravy. V principu byl flow následující:
- Zavolat flow pro generování karty
- Výstup poslat do uzlu Adaptive Card
- Výstup z uzlu Adaptive Card poslat do flow pro generování profilu
- Vypsat profil uživateli
- Přes uzel Generative Answers vygenerovat a zobrazit itinerář
Většina je poměrně přímočará – jediná komplikace je dynamická povaha Adaptive Card. Trochu jsem si pomohl tím, že jsem si vynutil fixní počet otázek (viz prompt pro generování karty), takže jsem se mohl spolehnout na fixní počet výstupů z karty. Po menší úpravě topic YAML…
…jsem pak dokázal nastavit jednu proměnnou, která reprezentuje celou odpověď, a tu poslat do promptu pro vytvoření turistického profilu (výstup z promptu jsem pak jednoduše vrátil uživateli jako zprávu)…
…a nakonec už šlo jen o to poslat profil do uzlu Generative Answers (s oporou ve veřejných webech) a nechat vygenerovat finální itinerář:
Shrnutí
A je to. Dynamické formuláře tak mohou posouvat „conversational intelligence“ bez jediné řádky JSON. Adaptive Cards se v řadě scénářů skvěle hodí a jejich tvorba může být překvapivě jednoduchá – zvlášť když tu největší práci odvede Copilot.
Jak Adaptive Cards používáte Vy? A kde by se u Vás ještě mohly hodit? Napište nám do komentářů níže.
Tento článek vznikl s využitím materiálu z microsoft.github.io. Osobní postřehy a komentáře jsou moje vlastní.


















