১. ভূমিকা
A2UI এআই এজেন্টদেরকে সমৃদ্ধ ও ইন্টারেক্টিভ ইউজার ইন্টারফেস তৈরি করতে সক্ষম করে, যা যথেচ্ছ কোড এক্সিকিউট না করেই ওয়েব, মোবাইল এবং ডেস্কটপে স্বাভাবিকভাবে রেন্ডার হয়। শুধুমাত্র টেক্সট-ভিত্তিক প্রতিক্রিয়া বা ঝুঁকিপূর্ণ কোড এক্সিকিউশনের পরিবর্তে, A2UI এজেন্টদেরকে ডিক্লারেটিভ কম্পোনেন্ট ডেসক্রিপশন পাঠাতে দেয়, যা ক্লায়েন্টরা তাদের নিজস্ব নেটিভ উইজেট ব্যবহার করে রেন্ডার করে। এটি অনেকটা এজেন্টদের একটি সার্বজনীন UI ভাষায় কথা বলার মতো।
এই হ্যান্ডস-অন ল্যাবে আপনি প্রথমে এজেন্ট ডেভেলপমেন্ট কিট (ADK) এবং জেমিনি ৩.১ ফ্ল্যাশ ইমেজ (যা ন্যানো ব্যানানা ২ নামেও পরিচিত) ব্যবহার করে একটি ইমেজ জেনারেশন এজেন্ট তৈরি করবেন। এরপর আপনি A2UI ব্যবহার করে একটি কাস্টম ইন্টারফেস তৈরি করবেন যা একটি সাধারণ চ্যাটবটের চেয়ে উন্নত, এবং এর মাধ্যমে দেখানো হবে কীভাবে আরও সমৃদ্ধ এজেন্ট-ব্যবহারকারী মিথস্ক্রিয়া সক্ষম করার জন্য ডাইনামিকভাবে ইন্টারফেস তৈরি করা যায়।
আপনি যা শিখবেন
- ADK পাইথন ব্যবহার করে একটি এজেন্ট তৈরি করুন
- ফ্রন্টএন্ডে A2UI কম্পোনেন্টগুলো স্ট্রিম করার জন্য এজেন্টকে কনফিগার করুন।
- A2UI এলিমেন্টগুলো রেন্ডার করার জন্য একটি কাস্টম ফ্রন্টএন্ড তৈরি করুন
পূর্বশর্ত
- এআই এজেন্ট সম্পর্কে প্রাথমিক জ্ঞান
- পাইথন সিনট্যাক্স সম্পর্কে প্রাথমিক ধারণা
- ফ্রন্টএন্ড ধারণা সম্পর্কে প্রাথমিক জ্ঞান
২. সেটআপ
এই কোডল্যাবের জন্য প্রয়োজনীয় গুগল ক্লাউড প্রজেক্টটি চালু করতে নিচের নির্দেশাবলী অনুসরণ করুন। প্রজেক্টটি চালু করার পর, ক্লাউড শেলে এই কোডল্যাবটি চালানোর পরামর্শ দেওয়া হচ্ছে, কারণ এটিতে এটি চালানোর জন্য প্রয়োজনীয় সমস্ত টুলস আগে থেকেই দেওয়া থাকে।
আপনি যদি এই কোডল্যাবটি আপনার স্থানীয় পরিবেশে চালাতে চান, তবে এগিয়ে যাওয়ার আগে আপনাকে পাইথন, ইউভি এবং একটি কোড এডিটর ইনস্টল করতে হবে। অন্যথায় উল্লেখ না করা পর্যন্ত, এই কোডল্যাবের সমস্ত নির্দেশাবলী ধরে নেওয়া হয়েছে যে আপনি এটি ক্লাউড শেলে চালাচ্ছেন।
স্ব-গতিতে পরিবেশ সেটআপ
- Google Cloud Console- এ সাইন-ইন করুন এবং একটি নতুন প্রজেক্ট তৈরি করুন অথবা বিদ্যমান কোনো প্রজেক্ট পুনরায় ব্যবহার করুন। যদি আপনার আগে থেকে Gmail বা Google Workspace অ্যাকাউন্ট না থাকে, তবে আপনাকে অবশ্যই একটি তৈরি করতে হবে।



- প্রজেক্টের নামটি হলো এই প্রজেক্টের অংশগ্রহণকারীদের প্রদর্শিত নাম। এটি একটি ক্যারেক্টার স্ট্রিং যা গুগল এপিআই ব্যবহার করে না। আপনি যেকোনো সময় এটি আপডেট করতে পারেন।
- প্রজেক্ট আইডি সমস্ত গুগল ক্লাউড প্রজেক্ট জুড়ে অনন্য এবং অপরিবর্তনীয় (একবার সেট করার পর এটি পরিবর্তন করা যায় না)। ক্লাউড কনসোল স্বয়ংক্রিয়ভাবে একটি অনন্য স্ট্রিং তৈরি করে; সাধারণত এটি কী তা নিয়ে আপনার মাথা ঘামানোর দরকার নেই। বেশিরভাগ কোডল্যাবে, আপনাকে আপনার প্রজেক্ট আইডি উল্লেখ করতে হবে (যা সাধারণত
PROJECT_IDহিসাবে চিহ্নিত করা হয়)। তৈরি করা আইডিটি আপনার পছন্দ না হলে, আপনি এলোমেলোভাবে আরেকটি তৈরি করতে পারেন। বিকল্পভাবে, আপনি আপনার নিজের আইডি দিয়ে চেষ্টা করে দেখতে পারেন যে সেটি উপলব্ধ আছে কিনা। এই ধাপের পরে এটি পরিবর্তন করা যাবে না এবং প্রজেক্টের পুরো সময়কাল জুড়ে এটি অপরিবর্তিত থাকবে। - আপনার অবগতির জন্য জানাচ্ছি যে, তৃতীয় একটি ভ্যালু রয়েছে, যা হলো প্রজেক্ট নম্বর , এবং কিছু এপিআই এটি ব্যবহার করে থাকে। ডকুমেন্টেশনে এই তিনটি ভ্যালু সম্পর্কে আরও বিস্তারিত জানুন।
- এরপর, ক্লাউড রিসোর্স/এপিআই ব্যবহার করার জন্য আপনাকে ক্লাউড কনসোলে বিলিং চালু করতে হবে। এই কোডল্যাবটি সম্পন্ন করতে খুব বেশি খরচ হবে না, এমনকি আদৌ কোনো খরচ নাও হতে পারে। এই টিউটোরিয়ালের পর বিলিং এড়াতে রিসোর্সগুলো বন্ধ করার জন্য, আপনি আপনার তৈরি করা রিসোর্সগুলো অথবা প্রজেক্টটি ডিলিট করে দিতে পারেন। নতুন গুগল ক্লাউড ব্যবহারকারীরা ৩০০ মার্কিন ডলারের ফ্রি ট্রায়াল প্রোগ্রামের জন্য যোগ্য।
ক্লাউড শেল শুরু করুন
যদিও গুগল ক্লাউড আপনার ল্যাপটপ থেকে দূরবর্তীভাবে পরিচালনা করা যায়, এই কোডল্যাবে আপনি গুগল ক্লাউড শেল ব্যবহার করবেন, যা ক্লাউডে চালিত একটি কমান্ড লাইন পরিবেশ।
গুগল ক্লাউড কনসোল থেকে, উপরের ডানদিকের টুলবারে থাকা ক্লাউড শেল আইকনটিতে ক্লিক করুন:

পরিবেশটি প্রস্তুত করতে এবং এর সাথে সংযোগ স্থাপন করতে মাত্র কয়েক মুহূর্ত সময় লাগবে। এটি শেষ হলে, আপনি এইরকম কিছু দেখতে পাবেন:

এই ভার্চুয়াল মেশিনটিতে আপনার প্রয়োজনীয় সমস্ত ডেভেলপমেন্ট টুলস লোড করা আছে। এটি একটি স্থায়ী ৫ জিবি হোম ডিরেক্টরি প্রদান করে এবং গুগল ক্লাউডে চলে, যা নেটওয়ার্ক পারফরম্যান্স ও অথেনটিকেশনকে ব্যাপকভাবে উন্নত করে। এই কোডল্যাবে আপনার সমস্ত কাজ একটি ব্রাউজারের মধ্যেই করা যাবে। আপনাকে কিছুই ইনস্টল করতে হবে না।
৩. একটি নতুন ADK এজেন্ট তৈরি করুন
- এই ওয়ার্কশপটির জন্য
a2ui_labনামে একটি ফোল্ডার তৈরি করুন:
mkdir -p ~/a2ui_lab && cd ~/a2ui_lab
- এই ফোল্ডারে uv প্যাকেজ ম্যানেজার কনফিগার করুন এবং ডিপেন্ডেন্সিগুলো ইনস্টল করুন:
uv init && uv add google-adk fastapi uvicorn a2ui-agent-sdk
- এআই প্ল্যাটফর্ম এপিআই সক্রিয় করুন (জেমিনি মডেল কল করার জন্য)
gcloud services enable aiplatform.googleapis.com
- এই ফোল্ডারে ADK এজেন্টটি চালু করুন:
export GOOGLE_CLOUD_PROJECT=`gcloud config get project`
uv run adk create --model gemini-3.5-flash --project $GOOGLE_CLOUD_PROJECT --region global art_creator
আপনি এর অনুরূপ একটি আউটপুট দেখতে পাবেন:
$ uv run adk create --model gemini-3.5-flash --project $GOOGLE_CLOUD_PROJECT --region global art_creator Agent created in ~/a2ui_lab/art_creator: - .env - __init__.py - agent.py ⚠️ WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env. Please ensure .env is added to your .gitignore to avoid committing secrets to version control.
অনুগ্রহ করে মনে রাখবেন যে, uv run হলো এমন একটি কমান্ড যা বর্তমান uv রিপোজিটরির প্রেক্ষাপটে কমান্ডগুলো কার্যকর করে, যেটি আমরা uv init চালানোর মাধ্যমে তৈরি করেছি। যখন আমরা google-adk প্যাকেজ ডিপেন্ডেন্সিটি যোগ করেছিলাম, তখন এই রিপোজিটরিতে adk কমান্ডটি ইনস্টল করা হয়েছিল।
ADK ডকুমেন্টেশনে আপনি প্রায়শই uv run প্রিফিক্স ছাড়া adk কমান্ডগুলো দেখতে পাবেন, কিন্তু এই ওয়ার্কশপে কমান্ড চালানোর সময় সর্বদা adk এর আগে uv run প্রিফিক্সটি ব্যবহার করুন, যাতে এটি সঠিক কমান্ড লাইন ইউটিলিটিটি চালায়।
এখন যেহেতু এজেন্টের মূল কাঠামো তৈরি হয়ে গেছে, আমরা agent.py ফাইলে ইমেজ জেনারেশন এজেন্টটি সংজ্ঞায়িত করতে পারি।
- নিম্নলিখিত কমান্ডটি ব্যবহার করে ক্লাউড শেল এডিটরটি খুলুন:
cloudshell workspace ~/a2ui_lab
-
art_creator/agent.pyফাইলের বিষয়বস্তু নিচের কোড দিয়ে প্রতিস্থাপন করুন:
art_creator/ agent.py
import os
import time
from google.adk.agents.llm_agent import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
# Load env variables
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
async def generate_image(prompt: str, tool_context: ToolContext) -> dict:
"""Generates a high-quality image based on the user's detailed description prompt.
Args:
prompt: A descriptive text prompt describing the image to generate.
tool_context: Context for the tool execution.
"""
from google.genai import Client
client = Client(
vertexai=True,
project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
location=os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
)
try:
response = client.models.generate_content(
model="gemini-3.1-flash-image",
contents=prompt,
config=types.GenerateContentConfig(
response_modalities=['TEXT', 'IMAGE'],
)
)
image_bytes = None
for part in response.parts or []:
if part.inline_data is not None:
image_bytes = part.inline_data.data
break
if not image_bytes:
return {"status": "failed", "detail": "No image data returned"}
filename = f"image_{int(time.time())}.png"
await tool_context.save_artifact(
filename,
types.Part.from_bytes(data=image_bytes, mime_type='image/png'),
)
return {
"status": "success",
"filename": filename,
"url": f"/api/artifacts/{tool_context.session.id}/{filename}"
}
except Exception as e:
return {"status": "failed", "detail": str(e)}
root_agent = Agent(
name="art_agent",
model="gemini-3.5-flash",
description="A basic art generation agent.",
instruction=(
"You are an art assistant. When the user describes an image they want to generate, "
"use the generate_image tool to create it, then return a text message containing the image's URL."
),
tools=[generate_image],
)
- এখন আপনি ADK-এর ডেভেলপমেন্ট UI-তে
uv run adk webকমান্ডটি ব্যবহার করে এজেন্টটি পরীক্ষা করতে পারেন।
uv run adk web --port 8080 --allow_origins "*" --reload_agents
এরপর ওয়েব প্রিভিউ বাটনে ক্লিক করে পোর্ট ৮০৮০-তে প্রিভিউ নির্বাচন করুন। এতে আপনার ব্রাউজারে ডেভেলপমেন্ট ইউআই খুলে যাবে।
এজেন্টের সক্ষমতা পরীক্ষা করার জন্য ADK-এর ডেভেলপমেন্ট UI ব্যবহার করে এটিকে কয়েকটি প্রম্পট দিন, যেমন:
- গাছের নিচে ঘুমন্ত অ্যানিমে মেয়ে। প্যাস্টেল রঙ। ১৬:৯
- হ্রদের জলে প্রতিফলিত একটি কেবিনের ছবি। পড়ন্ত বিকেল। স্মৃতিকাতর অনুভূতি।
আপনি দেখবেন এজেন্টটি টেক্সট এবং তৈরি হওয়া ছবিটি দিয়ে সাড়া দিচ্ছে।

৪. একটি সহজ ফ্রন্টএন্ড তৈরি করুন
এখন আমরা আমাদের এজেন্টের জন্য একটি ডেডিকেটেড ওয়েব অ্যাপ তৈরি করব। আমরা আমাদের ADK রানার চালানোর জন্য FastAPI ব্যবহার করব এবং একটি সাধারণ এক-পৃষ্ঠার চ্যাট ইন্টারফেস পরিবেশন করব।
প্রথমে, আপনার টার্মিনালে Ctrl+C চেপে ADK ডেভ সার্ভারটি বন্ধ করুন। এরপর, ওয়ার্কস্পেস রুটে ( ~/a2ui_lab/main.py ) main.py নামে একটি ফাইল তৈরি করুন এবং তাতে নিম্নলিখিত বিষয়বস্তু যোগ করুন:
main.py
import os
import logging
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from google.adk.runners import InMemoryRunner
from google.adk.agents.run_config import RunConfig
from google.genai import types
from art_creator.agent import root_agent
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Art Agent - Simple Chat")
class ChatRequest(BaseModel):
prompt: str
session_id: str = "default_session"
static_dir = os.path.join(os.path.dirname(__file__), "static")
os.makedirs(static_dir, exist_ok=True)
runner = InMemoryRunner(agent=root_agent)
runner.auto_create_session = True
@app.get("/api/artifacts/{session_id}/{filename}")
async def get_artifact(session_id: str, filename: str):
user_id = "default_user"
part = await runner.artifact_service.load_artifact(
app_name=runner.app_name,
user_id=user_id,
filename=filename,
session_id=session_id
)
if not part:
raise HTTPException(status_code=404, detail="Artifact not found")
if part.inline_data:
from fastapi.responses import Response
return Response(content=part.inline_data.data, media_type=part.inline_data.mime_type)
raise HTTPException(status_code=400, detail="Unsupported artifact format")
@app.post("/api/chat")
async def chat_endpoint(request: ChatRequest):
if not request.prompt.strip():
raise HTTPException(status_code=400, detail="Prompt cannot be empty")
user_id = "default_user"
content = types.Content(
role="user",
parts=[types.Part.from_text(text=request.prompt)]
)
full_response_text = ""
try:
async for event in runner.run_async(
user_id=user_id,
session_id=request.session_id,
new_message=content,
run_config=RunConfig(save_input_blobs_as_artifacts=True),
):
if event.content and event.content.parts:
if event.author != "user":
for part in event.content.parts:
if part.text:
full_response_text += part.text
elif part.inline_data:
try:
# Process raw binary/custom text parts (A2UI callback packages)
text_data = part.inline_data.data.decode("utf-8")
full_response_text += text_data
except Exception:
pass
except Exception as e:
logger.exception("Error running ADK agent:")
raise HTTPException(status_code=500, detail=str(e))
image_url = None
try:
artifact_keys = await runner.artifact_service.list_artifact_keys(
app_name=runner.app_name,
user_id=user_id,
session_id=request.session_id
)
image_keys = [k for k in artifact_keys if k.startswith("image_") and k.endswith(".png")]
if image_keys:
sorted_keys = sorted(image_keys, reverse=True)
image_url = f"/api/artifacts/{request.session_id}/{sorted_keys[0]}"
except Exception:
pass
return {
"text": full_response_text.strip(),
"image_url": image_url
}
app.mount("/static", StaticFiles(directory=static_dir), name="static")
@app.get("/")
async def read_index():
from fastapi.responses import FileResponse
return FileResponse(os.path.join(static_dir, "index.html"))
এরপরে, ফ্রন্টএন্ড ফাইলগুলো সংরক্ষণের জন্য static ডিরেক্টরিটি তৈরি করুন:
mkdir -p static
এখন ইনডেক্স HTML ( static/index.html ) যোগ করুন:
স্থির/সূচক.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Art Agent - Simple Chat</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div id="chat-messages" class="messages">
<div class="message system">
<strong>System:</strong> Welcome to the Art Agent! Describe the image you want to generate.
</div>
</div>
<form id="chat-form" class="input-form">
<input type="text" id="user-input" placeholder="Type image description..." autocomplete="off" required>
<button type="submit">Generate</button>
</form>
</div>
<script src="/static/app.js"></script>
</body>
</html>
এবং স্টাইলিং CSS ( static/style.css ):
স্থির/স্টাইল.সিএসএস
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f7f9fa;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
}
.container {
width: 100%;
max-width: 600px;
background: #ffffff;
border: 1px solid #e1e8ed;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
height: 80vh;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
padding: 10px 14px;
border-radius: 6px;
max-width: 85%;
line-height: 1.4;
word-wrap: break-word;
}
.message.user {
background-color: #e8f5fe;
align-self: flex-end;
}
.message.agent {
background-color: #f1f3f4;
align-self: flex-start;
}
.message.system {
background-color: #fff;
border: 1px solid #e1e8ed;
color: #657786;
align-self: center;
font-size: 13px;
text-align: center;
}
.input-form {
display: flex;
border-top: 1px solid #e1e8ed;
padding: 12px;
}
.input-form input {
flex: 1;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
font-size: 14px;
outline: none;
}
.input-form button {
background-color: #1da1f2;
color: white;
border: none;
border-radius: 4px;
padding: 10px 16px;
margin-left: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
}
.input-form button:hover {
background-color: #1a91da;
}
.generated-img {
max-width: 100%;
border-radius: 4px;
margin-top: 8px;
display: block;
}
.image-card {
border: 1px solid #ddd;
border-radius: 6px;
padding: 12px;
background-color: #fff;
margin-top: 8px;
}
.btn-download {
background-color: #1da1f2;
color: white;
border: none;
border-radius: 4px;
padding: 8px 12px;
font-size: 13px;
font-weight: bold;
cursor: pointer;
margin-top: 8px;
width: 100%;
}
অবশেষে, জাভাস্ক্রিপ্ট কন্ট্রোলারটি ( static/app.js ) যোগ করুন:
static/app.js
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const chatMessages = document.getElementById('chat-messages');
const sessionId = "session_" + Math.random().toString(36).substring(2, 9);
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = userInput.value.trim();
if (!prompt) return;
userInput.value = '';
appendMessage('user', prompt);
const tempBubble = appendMessage('agent', '...');
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, session_id: sessionId })
});
const data = await response.json();
tempBubble.remove();
const textValue = (data && typeof data === 'object' && data.text) ? data.text : "";
const imageUrl = (data && typeof data === 'object' && data.image_url) ? data.image_url : null;
appendMessage('agent', textValue, imageUrl);
} catch (error) {
tempBubble.remove();
appendMessage('agent', `Error: ${error.message}`);
}
});
function appendMessage(sender, text, imageUrl = null) {
const bubble = document.createElement('div');
bubble.className = `message ${sender}`;
const content = document.createElement('span');
content.innerHTML = `<strong>${sender === 'user' ? 'You' : 'Agent'}:</strong> `;
const textNode = document.createTextNode(text);
content.appendChild(textNode);
bubble.appendChild(content);
// Always format multi-line JSON blocks nicely if the message is from the agent and looks like JSON
if (sender === 'agent' && text && (text.startsWith('{') || text.startsWith('['))) {
bubble.style.fontFamily = 'monospace';
bubble.style.whiteSpace = 'pre-wrap';
bubble.style.fontSize = '12px';
}
if (imageUrl) {
const card = document.createElement('div');
card.className = 'image-card';
const img = document.createElement('img');
img.src = imageUrl;
img.className = 'generated-img';
card.appendChild(img);
const dlBtn = document.createElement('button');
dlBtn.className = 'btn-download';
dlBtn.textContent = 'Download PNG';
dlBtn.onclick = () => {
const link = document.createElement('a');
link.href = imageUrl;
link.download = `generation-${Date.now()}.png`;
link.click();
};
card.appendChild(dlBtn);
bubble.appendChild(card);
}
chatMessages.appendChild(bubble);
chatMessages.scrollTop = chatMessages.scrollHeight;
return bubble;
}
});
FastAPI সার্ভার চালু করে আপনার ওয়েব অ্যাপটি পরীক্ষা করুন:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
আপনার কাস্টমাইজড চ্যাট অ্যাক্সেস করতে পোর্ট ৮০৮০-তে ওয়েব প্রিভিউ ব্যবহার করুন। এখন আপনি সরাসরি এজেন্টের সাথে কথা বলতে পারবেন।

৫. A2UI বার্তা প্রেরণের জন্য এজেন্টকে কনফিগার করুন।
এখন, শুধু টেক্সটের পরিবর্তে স্ট্রাকচার্ড UI রিটার্ন করার জন্য এজেন্টটিকে আপডেট করা যাক। এজেন্টের জন্য একটি A2UI-সচেতন সিস্টেম প্রম্পট তৈরি করতে আমরা অফিসিয়াল a2ui-agent-sdk ব্যবহার করব।
A2UI SDK ব্যবহার করার সময়, এজেন্টের নির্দেশাবলী সরাসরি সংজ্ঞায়িত করার পরিবর্তে, আমরা A2uiSchemaManager ক্লাসটি ব্যবহার করি। এই ক্লাসটি এজেন্টের সিস্টেম প্রম্পটকে এমনভাবে গঠন করে যাতে এটি A2UI-এর ইন্টারফেস তৈরির ক্ষমতা বুঝতে পারে, যার মধ্যে কম্পোনেন্ট ক্যাটালগ, সম্পূর্ণ কম্পোনেন্ট স্কিমা এবং ব্যবহারের উদাহরণ (যদি উপলব্ধ থাকে) অ্যাক্সেস করার সুবিধাও অন্তর্ভুক্ত।
- প্রথমে, Ctrl+C চেপে FastAPI সার্ভারটি বন্ধ করুন।
-
A2uiSchemaManagerএবং আমাদের নতুনa2ui_callbackহুকটি অন্তর্ভুক্ত করতেart_creator/agent.pyফাইলটি পরিবর্তন করুন:
art_creator/agent.py
import os
import time
from google.adk.agents.llm_agent import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
from a2ui.schema.manager import A2uiSchemaManager
from a2ui.basic_catalog.provider import BasicCatalog
# Load env variables
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
async def generate_image(prompt: str, tool_context: ToolContext) -> dict:
"""Generates a high-quality image based on the user's detailed description prompt."""
from google.genai import Client
client = Client(
vertexai=True,
project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
location=os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
)
try:
response = client.models.generate_content(
model="gemini-3.1-flash-image",
contents=prompt,
config=types.GenerateContentConfig(
response_modalities=['TEXT', 'IMAGE'],
)
)
image_bytes = None
for part in response.parts or []:
if part.inline_data is not None:
image_bytes = part.inline_data.data
break
if not image_bytes:
return {"status": "failed", "detail": "No image data returned"}
filename = f"image_{int(time.time())}.png"
await tool_context.save_artifact(
filename,
types.Part.from_bytes(data=image_bytes, mime_type='image/png'),
)
return {
"status": "success",
"filename": filename,
"url": f"/api/artifacts/{tool_context.session.id}/{filename}"
}
except Exception as e:
return {"status": "failed", "detail": str(e)}
schema_manager = A2uiSchemaManager(
version="0.8",
catalogs=[BasicCatalog.get_config("0.8")],
)
instruction = schema_manager.generate_system_prompt(
role_description=(
"You are a specialized Image Creator agent. "
"When given an image description, analyze the prompt and ask the user for any missing details. "
"The image generation prompt should include: "
"Subject, environment, style, lighting, color and mood. "
),
workflow_description=(
"1. if the user greets you, greet the user back explaining your purpose. "
"2. if the user describes an image, DO NOT GENERATE IT IMMEDIATELY: compare with "
" the ideal generation prompt and ask the user for any missing details using "
" rich A2UI UI elements only. NOTE: only run this step once per image, if the user "
" decides to not detail one or more elements it is ok. "
"3. combine the original prompt with the responses in the UI elements and call `generate_image` "
" with the generated prompt. "
" DO NOT INCLUDE EXAMPLES IN THE GENERATED PROMPT, ONLY THE ELEMENTS THE USER ASKED FOR. "
"4. display the resulting image to the user in a card including the image, the prompt and a "
" download button"
),
ui_description=(
"Use Card, Text, Image, Multichoice and Button components to present the options. "
"Always include a single choice selection box for image resolution (1K, 2K or 4K) and one for "
"aspect ratio (1:1, 16:9 or 9:16). "
"When rendering the final output (generated image) always render the generated image using an "
"Image component with the url bound to the image's URL/path returned by the tool. "
"Add a Text component with the prompt that generated the image. "
"Include a Button component labeled 'Download PNG' to allow downloading the image. "
"Do NOT use markdown formatting in text values. Use the usageHint property for heading levels instead. "
"Respond ONLY with the A2UI JSON array. Do NOT include any text "
"outside the JSON. Put all explanations into Text components."
),
include_schema=True,
)
root_agent = Agent(
model="gemini-3.5-flash",
name="art_agent_a2ui",
instruction=instruction,
tools=[generate_image],
)
লক্ষ্য করুন, এখন এজেন্ট নির্দেশাবলী এজেন্ট সংজ্ঞায় হার্ডকোড করার পরিবর্তে schema_manager.generate_system_prompt কলের মাধ্যমে তৈরি করা হচ্ছে।
FastAPI সার্ভার চালু করে আপনার ওয়েব অ্যাপটি পরীক্ষা করুন:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
আপনার কাস্টমাইজ করা চ্যাট অ্যাক্সেস করতে পোর্ট ৮০৮০-তে ওয়েব প্রিভিউ ব্যবহার করুন। আপনি লক্ষ্য করবেন যে, এখন এজেন্ট সাধারণ টেক্সটের পরিবর্তে JSON মেসেজ পাঠাচ্ছে। এটি হলো A2UI এলিমেন্টগুলোর অভ্যন্তরীণ রূপ, যা আমরা পরবর্তী অংশে রেন্ডার করব।

৬. এজেন্টের জন্য একটি কাস্টম ফ্রন্টএন্ড তৈরি করুন
এই পর্যায়ে, আমাদের ক্লায়েন্ট কিছু পরিষ্কার A2UI মেসেজের ( beginRendering , surfaceUpdate , এবং dataModelUpdate ) একটি তালিকা পায়। এই কম্পোনেন্টগুলোকে বাস্তবে কাজ করতে দেখার জন্য আমরা এখন সাধারণ জাভাস্ক্রিপ্টে একটি কাস্টম ক্লায়েন্ট-সাইড রেন্ডারিং ইঞ্জিন তৈরি করব।
এখানে A2UI পার্সিং এবং রেন্ডারিং লজিক সহ সম্পূর্ণ static/app.js দেওয়া হলো:
static/app.js
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const chatMessages = document.getElementById('chat-messages');
const sessionId = "session_" + Math.random().toString(36).substring(2, 9);
async function sendChat(prompt, showInUi = true) {
if (!prompt) return;
if (showInUi) {
appendMessage('user', prompt);
}
const tempBubble = appendMessage('agent', '...');
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, session_id: sessionId })
});
const data = await response.json();
tempBubble.remove();
let text = data.text || "";
let a2uiMessages = [];
// 1. Extract <a2ui-json> blocks
const a2uiRegex = /<a2ui-json>(.*?)<\/a2ui-json>/gs;
let match;
while ((match = a2uiRegex.exec(text)) !== null) {
try {
const jsonStr = match[1].trim();
const parsed = JSON.parse(jsonStr);
const parsedList = Array.isArray(parsed) ? parsed : [parsed];
for (const msg of parsedList) {
if (msg && typeof msg === 'object') {
a2uiMessages.push(msg);
}
}
} catch (e) {
console.error("Error parsing <a2ui-json> block:", e);
}
}
// 2. Extract <a2a_datapart_json> blocks (for robust history/callback parsing)
const a2aRegex = /<a2a_datapart_json>(.*?)<\/a2a_datapart_json>/gs;
while ((match = a2aRegex.exec(text)) !== null) {
try {
const jsonStr = match[1].trim();
const parsed = JSON.parse(jsonStr);
const dataMsg = (parsed && parsed.kind === 'data') ? parsed.data : parsed;
if (dataMsg && typeof dataMsg === 'object') {
a2uiMessages.push(dataMsg);
}
} catch (e) {
console.error("Error parsing <a2a_datapart_json> block:", e);
}
}
// 3. Clean XML and A2UI JSON tags from displayed conversational text
const cleanText = text.replace(/<(a2ui-json|a2a_datapart_json)>.*?<\/\1>/gs, '').trim();
const imageUrl = (data && typeof data === 'object' && data.image_url) ? data.image_url : null;
appendMessage('agent', cleanText, imageUrl, a2uiMessages);
} catch (error) {
tempBubble.remove();
appendMessage('agent', `Error: ${error.message}`);
}
}
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = userInput.value.trim();
if (!prompt) return;
userInput.value = '';
await sendChat(prompt, true);
});
function renderA2UI(a2uiMessages) {
try {
let rootId = null;
const components = {};
const dataModel = {};
for (const msg of a2uiMessages) {
if (msg.beginRendering) {
rootId = msg.beginRendering.root;
} else if (msg.surfaceUpdate) {
for (const item of msg.surfaceUpdate.components) {
components[item.id] = item.component;
}
} else if (msg.dataModelUpdate) {
for (const item of msg.dataModelUpdate.contents) {
const val = item.valueString !== undefined ? item.valueString :
item.valueBool !== undefined ? item.valueBool :
item.valueInt !== undefined ? item.valueInt :
item.valueDouble !== undefined ? item.valueDouble : item.valueString;
dataModel[item.key] = val;
}
}
}
if (!rootId || Object.keys(components).length === 0) {
return null;
}
function resolveValue(valObj) {
if (!valObj) return '';
let val = '';
if (typeof valObj === 'string') val = valObj;
else if (valObj.literalString !== undefined) val = valObj.literalString;
else if (valObj.path !== undefined) val = dataModel[valObj.path] || '';
else val = JSON.stringify(valObj);
// Dynamically replace any wrong session IDs in artifact URLs with the active sessionId
if (typeof val === 'string' && val.includes('/api/artifacts/')) {
val = val.replace(/\/api\/artifacts\/session_[a-z0-9]+/g, `/api/artifacts/${sessionId}`);
}
return val;
}
function buildElement(id) {
try {
const compDesc = components[id];
if (!compDesc) return null;
const type = Object.keys(compDesc)[0];
const props = compDesc[type];
const el = document.createElement('div');
el.className = `a2ui-component a2ui-${type.toLowerCase()}`;
el.style.margin = '4px 0';
if (type === 'Column') {
el.style.display = 'flex';
el.style.flexDirection = 'column';
el.style.gap = '8px';
const children = props.children?.explicitList || [];
for (const childId of children) {
const childEl = buildElement(childId);
if (childEl) el.appendChild(childEl);
}
} else if (type === 'Row') {
el.style.display = 'flex';
el.style.flexDirection = 'row';
el.style.gap = '8px';
el.style.alignItems = 'center';
const children = props.children?.explicitList || [];
for (const childId of children) {
const childEl = buildElement(childId);
if (childEl) el.appendChild(childEl);
}
} else if (type === 'Card') {
el.style.border = '1px solid #ddd';
el.style.borderRadius = '6px';
el.style.padding = '12px';
el.style.backgroundColor = '#fdfdfd';
el.style.marginTop = '8px';
if (props.child) {
const childEl = buildElement(props.child);
if (childEl) el.appendChild(childEl);
}
} else if (type === 'Text') {
const textVal = resolveValue(props.text);
const tag = props.usageHint === 'h1' ? 'h3' :
props.usageHint === 'h2' ? 'h4' : 'p';
const textEl = document.createElement(tag);
textEl.textContent = textVal;
textEl.style.margin = '0 0 4px 0';
el.appendChild(textEl);
} else if (type === 'Image') {
const srcVal = resolveValue(props.url) || resolveValue(props.src);
const imgEl = document.createElement('img');
imgEl.src = srcVal;
imgEl.style.maxWidth = '100%';
imgEl.style.borderRadius = '4px';
imgEl.style.display = 'block';
imgEl.style.marginTop = '6px';
imgEl.className = 'generated-img';
el.appendChild(imgEl);
} else if (type === 'Divider') {
const hrEl = document.createElement('hr');
hrEl.style.border = '0';
hrEl.style.borderTop = '1px solid #eee';
hrEl.style.margin = '12px 0';
el.appendChild(hrEl);
} else if (type === 'MultipleChoice') {
const labelVal = resolveValue(props.label);
const options = props.options?.explicitList || (Array.isArray(props.options) ? props.options : []);
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '4px';
container.style.margin = '8px 0';
if (labelVal) {
const labelEl = document.createElement('label');
labelEl.textContent = labelVal;
labelEl.style.fontSize = '12px';
labelEl.style.fontWeight = 'bold';
container.appendChild(labelEl);
}
const selectEl = document.createElement('select');
selectEl.className = 'a2ui-select';
selectEl.name = id;
for (const option of options) {
const optEl = document.createElement('option');
optEl.value = option.value !== undefined ? option.value : (option.id !== undefined ? option.id : '');
optEl.textContent = resolveValue(option.label);
selectEl.appendChild(optEl);
}
container.appendChild(selectEl);
el.appendChild(container);
} else if (type === 'Button') {
let labelVal = props.label ? resolveValue(props.label) : '';
if (!labelVal && props.child) {
const childComp = components[props.child];
if (childComp && childComp.Text) {
labelVal = resolveValue(childComp.Text.text);
}
}
const btnEl = document.createElement('button');
btnEl.className = 'btn-download';
btnEl.textContent = labelVal || 'Submit';
btnEl.addEventListener('click', (e) => {
e.preventDefault();
const action = props.action;
let downloadUrl = null;
if (action && (action.name === 'download_file' || action.name === 'download') && action.context) {
const urlContext = action.context.find(ctx => ctx.key === 'url');
if (urlContext) {
downloadUrl = resolveValue(urlContext.value);
}
}
const isDownload = downloadUrl || (labelVal && labelVal.toLowerCase().includes('download'));
if (isDownload) {
const finalUrl = downloadUrl || (el.closest('.message')?.querySelector('img')?.src);
if (finalUrl) {
const link = document.createElement('a');
link.href = finalUrl;
link.download = `generation-${Date.now()}.png`;
link.click();
}
} else {
const bubbleEl = el.closest('.message');
const selects = bubbleEl.querySelectorAll('.a2ui-select');
let answers = [];
selects.forEach(sel => {
let labelText = sel.previousSibling ? sel.previousSibling.textContent : sel.name;
const selectedText = sel.options[sel.selectedIndex]?.textContent || sel.value;
answers.push(`- ${labelText}: ${selectedText}`);
});
if (answers.length > 0) {
const responseText = `Selected options:\n` + answers.join('\n');
sendChat(responseText, false);
} else {
sendChat(labelVal || 'Submit', false);
}
}
});
el.appendChild(btnEl);
}
return el;
} catch (err) {
console.error('Error building component:', id, err);
return null;
}
}
return buildElement(rootId);
} catch (err) {
console.error('Error in renderA2UI:', err);
return null;
}
}
function appendMessage(sender, text, imageUrl = null, a2ui = null) {
const bubble = document.createElement('div');
bubble.className = `message ${sender}`;
const textSpan = document.createElement('span');
textSpan.innerHTML = `<strong>${sender === 'user' ? 'You' : 'Agent'}:</strong> `;
bubble.appendChild(textSpan);
if (text) {
const textContent = document.createTextNode(text);
textSpan.appendChild(textContent);
}
if (sender === 'agent' && a2ui && a2ui.length > 0) {
const a2uiEl = renderA2UI(a2ui);
if (a2uiEl) {
bubble.appendChild(a2uiEl);
}
}
if (imageUrl) {
const imgContainer = document.createElement('div');
imgContainer.style.marginTop = '8px';
const img = document.createElement('img');
img.src = imageUrl;
img.style.maxWidth = '100%';
img.style.borderRadius = '4px';
img.className = 'generated-img';
imgContainer.appendChild(img);
bubble.appendChild(imgContainer);
}
chatMessages.appendChild(bubble);
chatMessages.scrollTop = chatMessages.scrollHeight;
return bubble;
}
});
FastAPI অ্যাপ সার্ভারটি আবার চালু করুন:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
এবং আপনার সম্পূর্ণ ডায়নামিক A2UI আর্ট ক্রিয়েটর এজেন্টের সাথে কথা বলুন!

৭. অভিনন্দন!
আপনি A2UI ব্যবহার করে একটি ADK এজেন্ট তৈরি করেছেন যা গতিশীলভাবে UI উপাদান তৈরি করে। আপনি বিভিন্ন ফ্রেমওয়ার্ক ইন্টিগ্রেশনগুলো অন্বেষণ করে অথবা নীচের রেফারেন্সে থাকা ডকুমেন্টেশন দেখে আপনার শেখার যাত্রা চালিয়ে যেতে পারেন।
একটি প্রোডাকশন ফ্রন্টএন্ড তৈরি করুন
এই কর্মশালায় আমরা শিক্ষামূলক উদ্দেশ্যে বিশেষভাবে তৈরি একটি বিশুদ্ধ JS ফ্রন্টএন্ড ব্যবহার করেছি, কিন্তু প্রোডাকশনের জন্য আপনাকে A2UI-এর অফিসিয়াল রেন্ডারারগুলোর মধ্যে একটি ব্যবহার করে ফ্রন্টএন্ড তৈরি করতে হবে:
প্ল্যাটফর্ম | রেন্ডারার | ইনস্টল করুন |
ওয়েব (রিঅ্যাক্ট) | @a2ui/react | npm install @a2ui/react |
ওয়েব (সাহিত্য) | @a2ui/lit | npm install @a2ui/lit |
ওয়েব (অ্যাঙ্গুলার) | @a2ui/angular | npm install @a2ui/angular |
মোবাইল/ডেস্কটপ | ফ্লাটার জেনইউআই এসডিকে |