বৃহৎ পরিসরে এজেন্ট: এজেন্ট রানটাইমে A2A প্রোটোকল এবং ADK ইন্টিগ্রেশন সহ মাল্টি-এজেন্ট আর্কিটেকচার

১. ভূমিকা

এআই এজেন্টরা যত বেশি দায়িত্ব গ্রহণ করে, একটিমাত্র এজেন্টের সবকিছু পরিচালনা করা ততই রক্ষণাবেক্ষণ, সম্প্রসারণ এবং উন্নত করা কঠিন হয়ে পড়ে। বিভিন্ন সক্ষমতার জন্য প্রায়শই ভিন্ন ভিন্ন স্থাপনা কৌশল, হালনাগাদ চক্র, বা এমনকি সেগুলোর দায়িত্বে থাকা ভিন্ন ভিন্ন দলের প্রয়োজন হয়।

  • A2A (এজেন্ট২এজেন্ট) প্রোটোকল যোগাযোগের দিকটির সমাধান করে — এটি এজেন্টরা কীভাবে একে অপরের সক্ষমতা আবিষ্কার করে এবং বিভিন্ন ফ্রেমওয়ার্ক ও সংস্থার মধ্যে সহযোগিতা করে, তার একটি মান নির্ধারণ করে।
  • জেমিনি এন্টারপ্রাইজ এজেন্ট প্ল্যাটফর্ম রানটাইম ডেপ্লয়মেন্টের দিকটি সমাধান করে — এটি একটি সম্পূর্ণভাবে পরিচালিত, সার্ভারবিহীন প্ল্যাটফর্ম যা আপনার এজেন্টদের হোস্ট করে এবং এতে রয়েছে বিল্ট-ইন A2A সাপোর্ট, অটো-স্কেলিং, সুরক্ষিত এন্ডপয়েন্ট, পারসিস্টেন্ট সেশন এবং কোনো ইনফ্রাস্ট্রাকচার ম্যানেজমেন্টের প্রয়োজন নেই।

একসাথে, এগুলো আপনাকে বিশেষায়িত এজেন্ট তৈরি করতে, সেগুলোকে আবিষ্কারযোগ্য A2A পরিষেবা হিসেবে স্থাপন করতে এবং একাধিক এজেন্ট সিস্টেমে একত্রিত করতে দেয়।

আপনি যা তৈরি করবেন

একটি রিজার্ভেশন এজেন্ট যা ADK সেশন স্টেট ব্যবহার করে রেস্তোরাঁর টেবিল বুকিং (তৈরি, চেক এবং বাতিল) পরিচালনা করে, এবং এই সেশন স্টেটটি Gemini Enterprise Agent Platform Sessions দ্বারা পরিচালিত হয়। আপনি এই এজেন্টটিকে Gemini Enterprise Agent Platform Runtime-এ ডেপ্লয় করেন, যেখানে এটি A2A প্রোটোকলের এজেন্ট কার্ডের মাধ্যমে ডিসকভারেবল হয়ে ওঠে। এরপর আপনি Foodie Finds রেস্তোরাঁর কনসিয়ার্জ এজেন্টটিকে ( প্রি-রিকুইজিট কোডল্যাব থেকে; আপনি যদি কোডল্যাবটি না দেখে থাকেন তবে চিন্তার কিছু নেই—আমরা আপনার জন্য একটি স্টার্টার রিপোজিটরি প্রস্তুত করেছি) একটি রিমোট A2A সাব-এজেন্ট হিসেবে রিজার্ভেশন এজেন্টটিকে কনজিউম করার জন্য আপগ্রেড করেন। এর ফলস্বরূপ: একটি মাল্টি-এজেন্ট সিস্টেম তৈরি হয়, যেখানে অর্কেস্ট্রেটর মেনু কোয়েরিগুলোকে MCP Toolbox-এ এবং রিজার্ভেশন রিকোয়েস্টগুলোকে রিমোট A2A এজেন্টের কাছে রাউট করে।

143fadef342e67a6.jpeg

আপনি যা শিখবেন

  • একটি ADK এজেন্ট তৈরি করুন যা রিজার্ভেশন ডেটা পরিচালনা করতে ম্যানেজড সেশন সার্ভিস ব্যবহার করে।
  • এজেন্ট কার্ড এবং স্কিল ব্যবহার করে একজন ADK এজেন্টকে একটি A2A সার্ভার হিসেবে উন্মোচন করুন।
  • জেমিনি এন্টারপ্রাইজ এজেন্ট রানটাইমে একটি A2A এজেন্ট স্থাপন করুন
  • RemoteA2aAgent ব্যবহার করে অন্য একটি ADK এজেন্ট থেকে একটি রিমোট A2A এজেন্ট গ্রহণ করা এবং প্রমাণীকৃত অনুরোধ পরিচালনা করা।
  • মাল্টি-এজেন্ট সিস্টেম পর্যায়ক্রমে পরীক্ষা করুন: স্থানীয় A2A, স্থাপনকৃত A2A, আংশিক ইন্টিগ্রেশন, সম্পূর্ণ স্থাপন।

পূর্বশর্ত

২. পরিবেশ সেটআপ - পূর্ববর্তী কোডল্যাবের ধারাবাহিকতায়

এই কোডল্যাবে আমরা যে বিবরণগুলো প্রদান করেছি, তা আসলে আমাদের পূর্বশর্ত কোডল্যাব ‘Agentic RAG with ADK, MCP Toolbox, and Cloud SQL’- এরই ধারাবাহিকতা। আপনি পূর্ববর্তী কোডল্যাব থেকে আপনার কাজ চালিয়ে যেতে পারেন।

আমরা আগের কোডল্যাব ওয়ার্কিং ডিরেক্টরিতে বিল্ড করা শুরু করতে পারি (ওয়ার্কিং ডিরেক্টরিটি হবে build-agent-adk-toolbox-cloudsql )। বিভ্রান্তি এড়াতে, নতুন করে শুরু করার সময় আমরা যে ডিরেক্টরি নামটি ব্যবহার করি, সেই একই নামে ডিরেক্টরিটির নাম পরিবর্তন করে নিই।

mv ~/build-agent-adk-toolbox-cloudsql ~/adk-a2a-agent-runtime-starter
cloudshell workspace ~/adk-a2a-agent-runtime-starter && cd ~/adk-a2a-agent-runtime-starter
source .env

পূর্ববর্তী কোডল্যাবের মূল ফাইলগুলো যথাস্থানে আছে কিনা তা যাচাই করুন:

echo "--- Restaurant Agent ---"
cat restaurant_agent/agent.py | head -5
echo ""
echo "--- Toolbox Config ---"
cat tools.yaml | head -5

আপনি LlmAgent ইম্পোর্ট সহ restaurant_agent/agent.py ফাইলটি এবং আপনার টুলবক্স কনফিগারেশন সহ tools.yaml ফাইলটি দেখতে পাবেন।

এরপরে, চলুন আমাদের পাইথন এনভায়রনমেন্টটি পুনরায় চালু করি।

rm -rf .venv
uv sync

এছাড়াও, ডেটাবেসটি প্রস্তুত আছে কিনা তা যাচাই করুন:

uv run python scripts/verify_seed.py

আপনি যদি আগের কোডল্যাবের প্রতিটি পরীক্ষার বিবরণ অনুসরণ করেন, তাহলে আপনি এই ধরনের আউটপুট দেখতে পারেন।

Menu Items: 16/15
Embeddings: 16/15

✗ Database not ready

কোনো সমস্যা নেই! ডেটা ইনজেশন চেকের সময় আপনি যে অতিরিক্ত ডেটা ইনপুট করেন, তা ডাটাবেস চেক করার সময় বিবেচনা করা হয় না। যতক্ষণ আপনার কাছে ১৫ বা তার বেশি ডেটা থাকবে, ততক্ষণ কোনো সমস্যা নেই!

প্রয়োজনীয় এপিআই সক্রিয় করুন

এরপরে, জেমিনি এন্টারপ্রাইজ এজেন্ট প্ল্যাটফর্মের সাথে যোগাযোগ করার জন্য প্রয়োজনীয় এপিআই (API) সক্রিয় করা নিশ্চিত করতে হবে।

gcloud services enable \
  cloudresourcemanager.googleapis.com

পরবর্তী বিভাগে— A2A Protocol and Gemini Enterprise Agent Runtime এ যাওয়ার জন্য আপনার কাছে প্রয়োজনীয় ফাইল এবং পরিকাঠামো ইতিমধ্যেই থাকা উচিত!

৩. পরিবেশ সেটআপ - স্টার্টার রিপো দিয়ে নতুন করে শুরু করুন

এই ধাপে আপনার ক্লাউড শেল এনভায়রনমেন্ট প্রস্তুত করা হয়, আপনার গুগল ক্লাউড প্রজেক্ট কনফিগার করা হয় এবং স্টার্টার রিপোজিটরি ক্লোন করা হয়।

ওপেন ক্লাউড শেল

আপনার ব্রাউজারে ক্লাউড শেল খুলুন। ক্লাউড শেল এই কোডল্যাবের জন্য আপনার প্রয়োজনীয় সমস্ত সরঞ্জাম সহ একটি পূর্ব-কনফিগার করা পরিবেশ প্রদান করে। অনুরোধ করা হলে অনুমোদন করুন (Authorize) বোতামে ক্লিক করুন।

এরপর টার্মিনাল খোলার জন্য " ভিউ " -> " টার্মিনাল "-এ ক্লিক করুন। আপনার ইন্টারফেসটি দেখতে এইরকম হবে।

86307fac5da2f077.png

এটাই হবে আমাদের মূল ইন্টারফেস, উপরে IDE, নিচে টার্মিনাল।

আপনার ওয়ার্কিং ডিরেক্টরি সেট আপ করুন

স্টার্টার রিপোজিটরিটি ক্লোন করুন, এই কোডল্যাবে আপনার লেখা সমস্ত কোড এখানেই থাকবে:

rm -rf ~/adk-a2a-agent-runtime-starter
git clone https://github.com/alphinside/adk-a2a-agent-runtime-starter.git
cloudshell workspace ~/adk-a2a-agent-runtime-starter && cd ~/adk-a2a-agent-runtime-starter

প্রদত্ত টেমপ্লেট থেকে .env ফাইলটি তৈরি করুন:

cp .env.example .env

আপনার টার্মিনালে প্রজেক্ট সেটআপ সহজ করার জন্য, এই প্রজেক্ট সেটআপ স্ক্রিপ্টটি আপনার ওয়ার্কিং ডিরেক্টরিতে ডাউনলোড করুন:

curl -sL https://raw.githubusercontent.com/alphinside/cloud-trial-project-setup/main/setup_verify_trial_project.sh -o setup_verify_trial_project.sh

স্ক্রিপ্টটি চালান। এটি আপনার ট্রায়াল বিলিং অ্যাকাউন্ট যাচাই করে, একটি নতুন প্রজেক্ট তৈরি করে (অথবা বিদ্যমান কোনো প্রজেক্টকে বৈধতা দেয়), আপনার প্রজেক্ট আইডি বর্তমান ডিরেক্টরির একটি .env ফাইলে সংরক্ষণ করে এবং gcloud এ সক্রিয় প্রজেক্টটি সেট করে।

bash setup_verify_trial_project.sh && source .env

স্ক্রিপ্টটি করবে:

  1. আপনার একটি সক্রিয় ট্রায়াল বিলিং অ্যাকাউন্ট আছে কিনা তা যাচাই করুন।
  2. .env ফাইলে কোনো বিদ্যমান প্রজেক্ট আছে কিনা তা পরীক্ষা করুন (যদি থাকে)।
  3. একটি নতুন প্রকল্প তৈরি করুন অথবা বিদ্যমান প্রকল্পটি পুনরায় ব্যবহার করুন।
  4. ট্রায়াল বিলিং অ্যাকাউন্টটি আপনার প্রোজেক্টের সাথে লিঙ্ক করুন।
  5. প্রজেক্ট আইডিটি .env ফাইলে সংরক্ষণ করুন।
  6. প্রজেক্টটিকে সক্রিয় gcloud প্রজেক্ট হিসেবে সেট করুন।

ক্লাউড শেল টার্মিনাল প্রম্পটে আপনার ওয়ার্কিং ডিরেক্টরির পাশে থাকা হলুদ লেখাটি দেখে প্রজেক্টটি সঠিকভাবে সেট করা হয়েছে কিনা তা যাচাই করুন। সেখানে আপনার প্রজেক্ট আইডি প্রদর্শিত হওয়া উচিত।

5c515e235ee1179f.png

প্রয়োজনীয় এপিআই সক্রিয় করুন

এরপরে, জেমিনি এন্টারপ্রাইজ এজেন্ট প্ল্যাটফর্মের সাথে যোগাযোগ করার জন্য প্রয়োজনীয় এপিআই (API) সক্রিয় করা নিশ্চিত করতে হবে।

gcloud services enable \
  aiplatform.googleapis.com \
  cloudresourcemanager.googleapis.com

স্টার্টার অবকাঠামো সেটআপ

প্রথমে, আমাদের uv ব্যবহার করে পাইথন ডিপেন্ডেন্সিগুলো ইনস্টল করতে হবে। এটি রাস্ট (Rust) ভাষায় লেখা একটি দ্রুতগতির পাইথন প্যাকেজ ও প্রজেক্ট ম্যানেজার (uv ডকুমেন্টেশন দেখুন)। এই কোডল্যাবটি পাইথন প্রজেক্ট রক্ষণাবেক্ষণে গতি ও সরলতার জন্য এটি ব্যবহার করে।

uv sync

এরপর, সম্পূর্ণ সেটআপ স্ক্রিপ্টটি চালান, যা ক্লাউড এসকিউএল ইনস্ট্যান্স তৈরি করে, ডেটা সিড করে এবং টুলবক্স সার্ভিসটি ডেপ্লয় করে, যা আমাদের রেস্তোরাঁ এজেন্টের প্রাথমিক অবস্থা হিসেবে কাজ করবে।

bash scripts/full_setup.sh > logs/full_setup.log 2>&1 &

৪. ধারণা: এজেন্ট২এজেন্ট (A2A) প্রোটোকল এবং জেমিনি এন্টারপ্রাইজ এজেন্ট রানটাইম

বিল্ড করার আগে, আমাদের এজেন্টিক অ্যাপ্লিকেশনটিকে স্কেল করার জন্য এই কোডল্যাবে উপস্থাপিত দুটি মূল প্রযুক্তি সম্পর্কে সংক্ষেপে জেনে নেওয়া যাক।

এজেন্ট২এজেন্ট (A2A) প্রোটোকল

এজেন্টটুএজেন্ট (A2A) প্রোটোকল হলো একটি উন্মুক্ত স্ট্যান্ডার্ড, যা এআই এজেন্টদের মধ্যে নির্বিঘ্ন যোগাযোগ এবং সহযোগিতা সক্ষম করার জন্য ডিজাইন করা হয়েছে। যেখানে এমসিপি (মডেল কনটেক্সট প্রোটোকল) এজেন্টদের টুলস এবং ডেটার সাথে সংযুক্ত করে, সেখানে এ২এ এজেন্টদের অন্যান্য এজেন্টের সাথে সংযুক্ত করে — যা তাদের একে অপরের সক্ষমতা আবিষ্কার করতে, কাজ অর্পণ করতে এবং বিভিন্ন ফ্রেমওয়ার্ক ও সংস্থার মধ্যে সহযোগিতা করতে সক্ষম করে।

5586b67d0437d79f.png

(MCP-এর মাধ্যমে) একটি এজেন্টকে টুল হিসেবে মোড়ানো এবং A2A-এর মাধ্যমে এটিকে উন্মুক্ত করার মধ্যে মূল পার্থক্য হলো: টুলগুলো স্টেটলেস এবং একক কাজ সম্পাদন করে, অন্যদিকে A2A এজেন্টরা যুক্তি দিয়ে ভাবতে পারে, স্টেট বজায় রাখতে পারে এবং আলোচনা বা স্পষ্টীকরণের মতো একাধিক ধাপের মিথস্ক্রিয়া পরিচালনা করতে পারে। A2A-এর মাধ্যমে উন্মুক্ত করা একটি এজেন্ট একটি ফাংশন কলে পর্যবসিত না হয়ে তার সম্পূর্ণ সক্ষমতা ধরে রাখে।

A2A তিনটি মূল ধারণা সংজ্ঞায়িত করে:

  1. এজেন্ট কার্ড — একটি JSON ডকুমেন্ট যা একজন এজেন্টের কাজ, তার দক্ষতা এবং তার এন্ডপয়েন্ট বর্ণনা করে। অন্যান্য এজেন্টরা তাদের সক্ষমতা জানার জন্য এই কার্ডটি ফেচ করে।
  2. বার্তা — একটি A2A এন্ডপয়েন্টে পাঠানো ব্যবহারকারী বা এজেন্টের অনুরোধ, যা একটি টাস্ক চালু করে।
  3. টাস্ক — কাজের একটি একক, যার একটি জীবনচক্র (জমা দেওয়া → চলমান → সম্পন্ন/ব্যর্থ) এবং ফলাফল ধারণকারী আর্টিফ্যাক্ট রয়েছে।

e7e3224d05b725f0.jpeg

আরও বিস্তারিত জানতে, দেখুন A2A কী?

জেমিনি এন্টারপ্রাইজ এজেন্ট প্ল্যাটফর্ম রানটাইম

এজেন্ট রানটাইম হলো গুগল ক্লাউডের একটি সম্পূর্ণ পরিচালিত পরিষেবা, যা এন্টারপ্রাইজ নিরাপত্তা বৈশিষ্ট্যসহ (যেমন VPC সার্ভিস কন্ট্রোল, CMEK) প্রোডাকশনে এআই এজেন্ট স্থাপন, স্কেলিং এবং পরিচালনার জন্য ব্যবহৃত হয়। এটি পরিকাঠামোর দায়িত্ব সামলায়, ফলে আপনি এজেন্ট লজিকের উপর মনোযোগ দিতে পারেন।

8ecbfbce8f0b9557.png

এজেন্ট রানটাইম যা প্রদান করে:

  • পরিচালিত ডেপ্লয়মেন্ট — একটিমাত্র SDK কলের মাধ্যমে ADK, LangGraph বা যেকোনো পাইথন ফ্রেমওয়ার্ক দিয়ে তৈরি এজেন্ট ডেপ্লয় করুন।
  • A2A হোস্টিং — স্বয়ংক্রিয় এজেন্ট কার্ড পরিবেশন এবং প্রমাণীকৃত অ্যাক্সেস সহ এজেন্টদের A2A-সম্মত এন্ডপয়েন্ট হিসাবে স্থাপন করুন।
  • স্থায়ী সেশনVertexAiSessionService বিভিন্ন অনুরোধ জুড়ে কথোপকথনের ইতিহাস এবং অবস্থা সংরক্ষণ করে।
  • স্বয়ংক্রিয় স্কেলিং — কোনো পরিকাঠামো ব্যবস্থাপনা ছাড়াই শূন্য থেকে ট্র্যাফিক সামাল দেওয়ার জন্য স্কেল করে।
  • পর্যবেক্ষণযোগ্যতা — গুগল ক্লাউডের পর্যবেক্ষণ স্ট্যাকের মাধ্যমে অন্তর্নির্মিত ট্রেসিং, লগিং এবং মনিটরিং
  • এবং আরও অনেক বৈশিষ্ট্য রয়েছে, বিস্তারিত জানতে এই ডকুমেন্টেশনটি দেখুন।

এই কোডল্যাবে, আপনি এজেন্ট রানটাইমে রিজার্ভেশন এজেন্টটি ডেপ্লয় করবেন। ডেপ্লয়মেন্ট প্রক্রিয়াটি আপনার এজেন্ট কোডকে সিরিয়ালাইজ (পিকল) করে এবং আপলোড করে। এজেন্ট রানটাইম একটি সার্ভারলেস এন্ডপয়েন্ট প্রোভিশন করে যা A2A প্রোটোকলটি সরবরাহ করে — অন্যান্য এজেন্ট (বা ক্লায়েন্ট) গুগল ক্লাউড ক্রেডেনশিয়াল দ্বারা অথেন্টিকেট হয়ে স্ট্যান্ডার্ড HTTP কলের মাধ্যমে এর সাথে ইন্টারঅ্যাক্ট করে।

৫. রিজার্ভেশন এজেন্ট তৈরি করুন

এই ধাপে একটি নতুন ADK এজেন্ট তৈরি করা হয়, যা সেশন স্টেট ব্যবহার করে রেস্তোরাঁর রিজার্ভেশন পরিচালনা করে। এজেন্টটি ফোন নম্বরকে লুকআপ কী হিসেবে ব্যবহার করে তিনটি অপারেশন—তৈরি করা, যাচাই করা এবং বাতিল করা—সমর্থন করে। রিজার্ভেশনের সমস্ত ডেটা ADK-এর সেশন স্টেটে সংরক্ষিত থাকে।

এজেন্টকে কাঠামোবদ্ধ করুন

সঠিক মডেল এবং প্রজেক্ট কনফিগারেশন সহ এজেন্ট ডিরেক্টরি কাঠামো তৈরি করতে adk create ব্যবহার করুন:

source .env
uv run adk create reservation_agent \
    --model gemini-2.5-flash \
    --project ${GOOGLE_CLOUD_PROJECT} \
    --region ${GOOGLE_CLOUD_LOCATION}

এটি এজেন্ট প্ল্যাটফর্মে জেমিনি মডেলের জন্য পূর্ব-কনফিগার করা __init__.py , agent.py এবং .env ফাইলসহ একটি reservation_agent/ ডিরেক্টরি তৈরি করে।

adk-a2a-agent-runtime-starter/
├── reservation_agent/
│   ├── __init__.py
│   ├── agent.py
│   └── .env
├── logs
├── scripts
└── ...

এরপর, এজেন্ট কোডটি আপডেট করা যাক।

এজেন্ট কোড লিখুন

তৈরি হওয়া এজেন্ট ফাইলটি খুলুন:

cloudshell edit reservation_agent/agent.py

তারপর বিষয়বস্তুগুলো নিম্নলিখিত দিয়ে প্রতিস্থাপন করুন:

# reservation_agent/agent.py
from google.adk.agents import LlmAgent
from google.adk.tools import ToolContext

# App-scoped state prefix ensures reservations persist across all sessions.
# See https://adk.dev/sessions/state/ for state scope details.
STATE_PREFIX = "app:reservation:"


def create_reservation(
    phone_number: str,
    name: str,
    party_size: int,
    date: str,
    time: str,
    tool_context: ToolContext,
) -> dict:
    """Create a new restaurant reservation.

    Args:
        phone_number: Customer's phone number, used as the reservation ID.
        name: Name for the reservation.
        party_size: Number of guests.
        date: Reservation date (e.g., '2025-07-15' or 'this Friday').
        time: Reservation time (e.g., '7:00 PM').

    Returns:
        Confirmation of the reservation.
    """
    reservation = {
        "name": name,
        "party_size": party_size,
        "date": date,
        "time": time,
        "status": "confirmed",
    }
    tool_context.state[f"{STATE_PREFIX}{phone_number}"] = reservation
    return {
        "status": "confirmed",
        "message": f"Reservation created for {name}, party of {party_size} on {date} at {time}. Phone: {phone_number}.",
    }


def check_reservation(phone_number: str, tool_context: ToolContext) -> dict:
    """Look up an existing reservation by phone number.

    Args:
        phone_number: The phone number used when the reservation was created.
        tool_context: ADK tool context for state access.

    Returns:
        The reservation details, or a message if not found.
    """
    reservation = tool_context.state.get(f"{STATE_PREFIX}{phone_number}")
    if reservation:
        return {"found": True, "reservation": reservation}
    return {"found": False, "message": f"No reservation found for {phone_number}."}


def cancel_reservation(phone_number: str, tool_context: ToolContext) -> dict:
    """Cancel an existing reservation by phone number.

    Args:
        phone_number: The phone number used when the reservation was created.
        tool_context: ADK tool context for state access.

    Returns:
        Confirmation of cancellation, or a message if not found.
    """
    key = f"{STATE_PREFIX}{phone_number}"
    reservation = tool_context.state.get(key)
    if not reservation:
        return {"success": False, "message": f"No reservation found for {phone_number}."}
    if reservation.get("status") == "cancelled":
        return {"success": False, "message": f"Reservation for {phone_number} is already cancelled."}
    reservation["status"] = "cancelled"
    tool_context.state[key] = reservation
    return {"success": True, "message": f"Reservation for {reservation['name']} ({phone_number}) has been cancelled."}


root_agent = LlmAgent(
    name="reservation_agent",
    model="gemini-2.5-flash",
    instruction="""You are a friendly reservation assistant for "Foodie Finds" restaurant.
You help diners create, check, and cancel table reservations.

When a diner wants to make a reservation, collect these details:
- Name for the reservation
- Phone number (used as the reservation ID)
- Party size (number of guests)
- Date
- Time

Always confirm the details before creating the reservation.
When checking or cancelling, ask for the phone number if not provided.
Be concise and professional.""",
    tools=[create_reservation, check_reservation, cancel_reservation],
)

৬. A2A সার্ভার কনফিগারেশন প্রস্তুত করুন

A2A এজেন্ট কার্ডের সংজ্ঞা দিন

এজেন্ট কার্ড হলো আপনার এজেন্টের সক্ষমতার একটি কাঠামোগত বিবরণ — অন্যান্য এজেন্ট ও ক্লায়েন্টরা আপনার এজেন্ট কী করেন তা জানতে এটি ব্যবহার করেন। কার্ড কনফিগারেশন তৈরি করুন:

cloudshell edit reservation_agent/a2a_config.py

নিম্নলিখিতটি reservation_agent/a2a_config.py ফাইলে কপি করুন:

# reservation_agent/a2a_config.py
from a2a.types import AgentSkill
from vertexai.preview.reasoning_engines.templates.a2a import create_agent_card

reservation_skill = AgentSkill(
    id="manage_reservations",
    name="Restaurant Reservations",
    description="Create, check, and cancel table reservations at Foodie Finds restaurant",
    tags=["reservations", "restaurant", "booking"],
    examples=[
        "Book a table for 4 on Friday at 7pm",
        "Check reservation for 555-0101",
        "Cancel my reservation, phone number 555-0101",
    ],
    input_modes=["text/plain"],
    output_modes=["text/plain"],
)

agent_card = create_agent_card(
    agent_name="Reservation Agent",
    description="Handles restaurant table reservations — create, check, and cancel bookings for Foodie Finds restaurant.",
    skills=[reservation_skill],
)

A2A এক্সিকিউটর তৈরি করুন

এক্সিকিউটরটি A2A প্রোটোকল এবং ADK এজেন্টের মধ্যে সংযোগ স্থাপন করে। এটি A2A অনুরোধ গ্রহণ করে, সেগুলোকে ADK এজেন্টের মাধ্যমে চালনা করে এবং ফলাফল A2A টাস্ক হিসেবে ফেরত দেয়:

cloudshell edit reservation_agent/executor.py

নিম্নলিখিতটি reservation_agent/executor.py ফাইলে কপি করুন:

# reservation_agent/executor.py
import os
from typing import NoReturn

import vertexai
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import TaskState, TextPart, UnsupportedOperationError
from a2a.utils import new_agent_text_message
from a2a.utils.errors import ServerError
from google.adk.artifacts import InMemoryArtifactService
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService, VertexAiSessionService
from google.genai import types

from reservation_agent.agent import root_agent as reservation_agent


class ReservationAgentExecutor(AgentExecutor):
    """Bridge between the A2A protocol and the ADK reservation agent.

    Uses InMemorySessionService for local testing, VertexAiSessionService
    when deployed to Agent Runtime (detected via GOOGLE_CLOUD_AGENT_ENGINE_ID).
    """

    def __init__(self) -> None:
        self.agent = None
        self.runner = None

    def _init_agent(self) -> None:
        if self.agent is not None:
            return

        self.agent = reservation_agent
        engine_id = os.environ.get("GOOGLE_CLOUD_AGENT_ENGINE_ID")

        if engine_id:
            project = os.environ.get("GOOGLE_CLOUD_PROJECT")
            location = os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1")
            vertexai.init(project=project, location=location)
            session_service = VertexAiSessionService(
                project=project, location=location, agent_engine_id=engine_id,
            )
            app_name = engine_id
        else:
            session_service = InMemorySessionService()
            app_name = self.agent.name

        self.runner = Runner(
            app_name=app_name,
            agent=self.agent,
            artifact_service=InMemoryArtifactService(),
            session_service=session_service,
            memory_service=InMemoryMemoryService(),
        )

    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
        if self.agent is None:
            self._init_agent()

        query = context.get_user_input()
        updater = TaskUpdater(event_queue, context.task_id, context.context_id)
        user_id = context.message.metadata.get("user_id", "a2a-user") if context.message.metadata else "a2a-user"

        if not context.current_task:
            await updater.submit()
        await updater.start_work()

        try:
            session = await self._get_or_create_session(context.context_id, user_id)
            content = types.Content(role="user", parts=[types.Part(text=query)])

            async for event in self.runner.run_async(
                session_id=session.id, user_id=user_id, new_message=content,
            ):
                if event.is_final_response():
                    parts = event.content.parts
                    answer = " ".join(p.text for p in parts if p.text) or "No response."
                    await updater.add_artifact([TextPart(text=answer)], name="answer")
                    await updater.complete()
                    break
        except Exception as e:
            await updater.update_status(
                TaskState.failed, message=new_agent_text_message(f"Error: {e!s}"),
            )
            raise

    async def _get_or_create_session(self, context_id: str, user_id: str):
        app_name = self.runner.app_name
        if context_id:
            session = await self.runner.session_service.get_session(
                app_name=app_name, session_id=context_id, user_id=user_id,
            )
            if session:
                return session
        session = await self.runner.session_service.create_session(
            app_name=app_name, user_id=user_id, session_id=context_id,
        )
        return session

    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> NoReturn:
        raise ServerError(error=UnsupportedOperationError())

এক্সিকিউটর স্বয়ংক্রিয়ভাবে তার পরিবেশ শনাক্ত করে: যখন GOOGLE_CLOUD_AGENT_ENGINE_ID সেট করা থাকে (এজেন্ট রানটাইম এটি ডিপ্লয় করার সময় যুক্ত করে), তখন এটি স্থায়ী সেশনের জন্য VertexAiSessionService ব্যবহার করে। স্থানীয়ভাবে, এটি InMemorySessionService এ ফিরে যায়।

আপনার reservation_agent ডিরেক্টরিতে এখন নিম্নলিখিত বিষয়গুলো থাকা উচিত:

reservation_agent/
├── __init__.py
├── agent.py
├── a2a_config.py
├── executor.py
└── .env

৭. এজেন্ট প্ল্যাটফর্ম এসডিকে ব্যবহার করে এ২এ এজেন্ট প্রস্তুত করা এবং স্থানীয়ভাবে পরীক্ষা করা

এই ধাপে এজেন্ট প্ল্যাটফর্ম SDK-এর (ব্যাকওয়ার্ড কম্প্যাটিবিলিটির জন্য SDK-এর নামে এখনও vertex টার্ম ব্যবহার করা হচ্ছে) A2aAgent ক্লাস ব্যবহার করে রিজার্ভেশন এজেন্টকে একটি A2A-সম্মত এজেন্ট হিসেবে র‍্যাপ করা হয়, এবং তারপর স্থানীয়ভাবে সম্পূর্ণ A2A প্রোটোকল ফ্লো—এজেন্ট কার্ড পুনরুদ্ধার, মেসেজ পাঠানো এবং টাস্ক পুনরুদ্ধার—পরীক্ষা করা হয়। এই একই A2aAgent অবজেক্টটি আপনি পরবর্তী ধাপে এজেন্ট রানটাইমে ডেপ্লয় করবেন।

নির্ভরতা যোগ করুন

এজেন্ট রানটাইম ও ADK সাপোর্ট সহ এজেন্ট প্ল্যাটফর্ম SDK এবং A2A SDK ইনস্টল করুন:

uv add "google-cloud-aiplatform[agent_engines,adk]==1.149.0" "a2a-sdk==0.3.26"

A2A উপাদানগুলো বুঝুন

A2A-এর জন্য একটি ADK এজেন্টকে প্রস্তুত করতে তিনটি উপাদানের প্রয়োজন হয়:

  1. এজেন্ট কার্ড — এটি একটি 'বিজনেস কার্ড' যা এজেন্টের সক্ষমতা, দক্ষতা এবং এন্ডপয়েন্ট ইউআরএল বর্ণনা করে। অন্যান্য এজেন্টরা আপনার এজেন্ট কী কাজ করে তা জানতে এটি ব্যবহার করে।
  2. এজেন্ট এক্সিকিউটর — এটি A2A প্রোটোকল এবং আপনার ADK এজেন্টের লজিকের মধ্যে সংযোগ স্থাপনকারী সেতু। এটি A2A অনুরোধ গ্রহণ করে, সেগুলোকে ADK এজেন্টের মাধ্যমে চালনা করে এবং ফলাফলকে A2A টাস্ক হিসেবে ফেরত দেয়।
  3. A2aAgent — এজেন্ট প্ল্যাটফর্ম SDK ক্লাস যা কার্ড এবং এক্সিকিউটরকে একত্রিত করে একটি স্থাপনযোগ্য ইউনিটে পরিণত করে।

টেস্ট স্ক্রিপ্ট তৈরি করুন

স্থানীয়ভাবে পরীক্ষা করার জন্য নিম্নলিখিত স্ক্রিপ্টটি তৈরি করুন।

cloudshell edit scripts/test_a2a_agent_local.py

নিম্নলিখিতটি scripts/test_a2a_agent_local.py ফাইলে কপি করুন:

# scripts/test_a2a_agent_local.py
import asyncio
import json
import os
from pprint import pprint

from dotenv import load_dotenv
from starlette.requests import Request
from vertexai.preview.reasoning_engines import A2aAgent

from reservation_agent.a2a_config import agent_card
from reservation_agent.executor import ReservationAgentExecutor

load_dotenv()


# --- Helper functions for building mock requests ---

def receive_wrapper(data: dict):
    async def receive():
        byte_data = json.dumps(data).encode("utf-8")
        return {"type": "http.request", "body": byte_data, "more_body": False}
    return receive

def build_post_request(data: dict = None, path_params: dict = None) -> Request:
    scope = {"type": "http", "http_version": "1.1", "headers": [(b"content-type", b"application/json")], "app": None}
    if path_params:
        scope["path_params"] = path_params
    return Request(scope, receive_wrapper(data))

def build_get_request(path_params: dict) -> Request:
    scope = {"type": "http", "http_version": "1.1", "query_string": b"", "app": None}
    if path_params:
        scope["path_params"] = path_params
    async def receive():
        return {"type": "http.disconnect"}
    return Request(scope, receive)


# --- Helper: poll for task completion ---

async def wait_for_task(a2a_agent, task_id, max_retries=30):
    """Poll on_get_task until the task reaches a terminal state."""
    for _ in range(max_retries):
        request = build_get_request({"id": task_id})
        result = await a2a_agent.on_get_task(request=request, context=None)
        state = result.get("status", {}).get("state", "")
        if state in ["completed", "failed"]:
            return result
        await asyncio.sleep(1)
    return result


def print_task_answer(result):
    """Extract and print the answer from task artifacts."""
    print(f"Status: {result.get('status', {}).get('state')}")
    for artifact in result.get("artifacts", []):
        if artifact.get("parts") and "text" in artifact["parts"][0]:
            print(f"Answer: {artifact['parts'][0]['text']}")


# --- Local test ---

async def main():
    # Create and set up the A2A agent locally
    a2a_agent = A2aAgent(agent_card=agent_card, agent_executor_builder=ReservationAgentExecutor)
    a2a_agent.set_up()

    # 1. Get agent card
    print("=" * 50)
    print("1. Retrieving agent card...")
    print("=" * 50)
    request = build_get_request(None)
    card_response = await a2a_agent.handle_authenticated_agent_card(request=request, context=None)
    print(f"Agent: {card_response.get('name')}")
    print(f"Skills: {[s.get('name') for s in card_response.get('skills', [])]}")

    # 2. Create a reservation
    print("\n" + "=" * 50)
    print("2. Creating a reservation...")
    print("=" * 50)
    message_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Book a table for 2 on Saturday at 6pm. Name: Bob, Phone: 555-0202"}],
            "role": "ROLE_USER",
        },
    }
    request = build_post_request(message_data)
    response = await a2a_agent.on_message_send(request=request, context=None)
    task_id = response["task"]["id"]
    context_id = response["task"].get("contextId")
    print(f"Task ID: {task_id}")

    # 3. Wait for result
    print("\n" + "=" * 50)
    print("3. Waiting for task result...")
    print("=" * 50)
    result = await wait_for_task(a2a_agent, task_id)
    print_task_answer(result)

    # 4. Check the reservation (same context for session continuity)
    print("\n" + "=" * 50)
    print("4. Checking the reservation...")
    print("=" * 50)
    check_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Check the reservation for 555-0202"}],
            "role": "ROLE_USER",
            "contextId": context_id,
        },
    }
    request = build_post_request(check_data)
    check_response = await a2a_agent.on_message_send(request=request, context=None)
    check_result = await wait_for_task(a2a_agent, check_response["task"]["id"])
    print_task_answer(check_result)

    # 5. Cancel the reservation
    print("\n" + "=" * 50)
    print("5. Cancelling the reservation...")
    print("=" * 50)
    cancel_data = {
        "message": {
            "messageId": f"msg-{os.urandom(4).hex()}",
            "content": [{"text": "Cancel the reservation for 555-0202"}],
            "role": "ROLE_USER",
            "contextId": context_id,
        },
    }
    request = build_post_request(cancel_data)
    cancel_response = await a2a_agent.on_message_send(request=request, context=None)
    cancel_result = await wait_for_task(a2a_agent, cancel_response["task"]["id"])
    print_task_answer(cancel_result)

    print("\n" + "=" * 50)
    print("All tests passed!")
    print("=" * 50)


if __name__ == "__main__":
    asyncio.run(main())

টেস্ট স্ক্রিপ্টটি পূর্ববর্তী ধাপে আপনার তৈরি করা এজেন্ট কার্ড এবং এক্সিকিউটর ইম্পোর্ট করে — কোনো পুনরাবৃত্তি নেই। এটি একটি লোকাল A2aAgent তৈরি করবে, মক HTTP রিকোয়েস্টের মাধ্যমে A2A প্রোটোকল কল সিমুলেট করবে এবং তিনটি রিজার্ভেশন অপারেশনই যাচাই করবে।

যেহেতু স্থানীয়ভাবে কোনো GOOGLE_CLOUD_AGENT_ENGINE_ID সেট করা নেই, তাই এক্সিকিউটরটি InMemorySessionService ব্যবহার করে। এজেন্ট রানটাইমে ডেপ্লয় করা হলে, সেই একই এক্সিকিউটর স্থায়ী সেশনগুলোর জন্য স্বয়ংক্রিয়ভাবে VertexAiSessionService এ সুইচ করে।

পরীক্ষাটি চালান

PYTHONPATH=. uv run python scripts/test_a2a_agent_local.py

আউটপুটটি পাঁচটি ধাপের মধ্য দিয়ে যায়:

  1. এজেন্ট কার্ড — এজেন্টের সক্ষমতা ও দক্ষতা পুনরুদ্ধার করে।
  2. রিজার্ভেশন তৈরি করুন — একটি টেবিল বুক করে এবং নিশ্চিতকরণ সহ একটি টাস্ক ফেরত দেয়।
  3. কাজের ফলাফল পান — উত্তরসহ সম্পন্ন হওয়া কাজটি পুনরুদ্ধার করে।
  4. রিজার্ভেশন চেক করুন — ফোন নম্বর দিয়ে রিজার্ভেশনটি খুঁজে বের করুন।
  5. রিজার্ভেশন বাতিল করুন — বুকিং বাতিল করে নিশ্চিত করে

আউটপুটের একটি উদাহরণ নিচে দেখানো হলো।

==================================================
1. Retrieving agent card...
==================================================
Agent: Reservation Agent
Skills: ['Restaurant Reservations']

==================================================
2. Creating a reservation...
==================================================
Task ID: f7f7004d-cfea-49c2-b57d-5bca9959e193

==================================================
3. Waiting for task result...
==================================================
Status: TASK_STATE_COMPLETED
Answer: Your reservation for Bob, party of 2, on Saturday at 6:00 PM has been confirmed. The phone number associated is 555-0202.

==================================================
4. Checking the reservation...
==================================================
Status: TASK_STATE_COMPLETED
Answer: I found a reservation for Bob, party of 2, on Saturday at 6:00 PM. The reservation status is confirmed.

==================================================
5. Cancelling the reservation...
==================================================
Status: TASK_STATE_COMPLETED
Answer: Your reservation for Bob (555-0202) has been cancelled.

==================================================
All tests passed!
==================================================

এই পর্যায়ে আপনি যাচাই করেছেন: A2A এজেন্ট কার্ডটিতে সঠিক দক্ষতার বর্ণনা রয়েছে, তিনটি রিজার্ভেশন অপারেশনই A2A প্রোটোকলের মেসেজ/টাস্ক ফ্লো-এর মাধ্যমে কাজ করে, এবং একই কনটেক্সটের মধ্যেকার মেসেজগুলোতে স্টেট বজায় থাকে।

৮. এজেন্ট রানটাইমে রিজার্ভেশন এজেন্ট স্থাপন করুন

এই ধাপে রিজার্ভেশন এজেন্টকে জেমিনি এন্টারপ্রাইজ এজেন্ট প্ল্যাটফর্ম রানটাইম-এ স্থাপন করা হয় — এটি একটি সম্পূর্ণভাবে পরিচালিত, সার্ভারবিহীন প্ল্যাটফর্ম যা আপনার এজেন্টকে হোস্ট করে এবং এটিকে একটি সুরক্ষিত A2A এন্ডপয়েন্ট হিসেবে উন্মুক্ত করে। স্থাপনের পরে, যেকোনো অনুমোদিত ক্লায়েন্ট স্ট্যান্ডার্ড A2A HTTP এন্ডপয়েন্টের মাধ্যমে এজেন্টটিকে খুঁজে পেতে এবং এর সাথে যোগাযোগ করতে পারে।

স্টেজিং বাকেট তৈরি করুন

এজেন্ট রানটাইম স্টেজিং-এর জন্য একটি ক্লাউড স্টোরেজ বাকেট তৈরি করুন। ডেপ্লয়মেন্টের সময় আপনার এজেন্টের কোড এবং ডিপেন্ডেন্সি আপলোড করার জন্য এজেন্ট রানটাইম এই বাকেটটি ব্যবহার করে:

STAGING_BUCKET="${GOOGLE_CLOUD_PROJECT}-adk-a2a-agent-runtime"
gsutil mb -l $REGION -p $GOOGLE_CLOUD_PROJECT gs://$STAGING_BUCKET 2>/dev/null || echo "Bucket already exists"
echo "STAGING_BUCKET=$STAGING_BUCKET" >> .env
source .env

ডিপ্লয়মেন্ট স্ক্রিপ্ট তৈরি করুন

এরপরে, আমাদের ডেপ্লয়মেন্ট স্ক্রিপ্টটি প্রস্তুত করতে হবে।

cloudshell edit scripts/deploy_a2a_agent_runtime.py

নিম্নলিখিতটি scripts/deploy_a2a_agent_runtime.py ফাইলে কপি করুন:

# scripts/deploy_a2a_agent_runtime.py
import os
from pathlib import Path

import vertexai
from dotenv import load_dotenv
from google.genai import types
from vertexai.preview.reasoning_engines import A2aAgent

from reservation_agent.a2a_config import agent_card
from reservation_agent.executor import ReservationAgentExecutor

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
STAGING_BUCKET = os.environ.get("STAGING_BUCKET", f"{PROJECT_ID}-adk-a2a-agent-runtime")
BUCKET_URI = f"gs://{STAGING_BUCKET}"

a2a_agent = A2aAgent(
    agent_card=agent_card,
    agent_executor_builder=ReservationAgentExecutor,
)


def main():
    vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=BUCKET_URI)
    client = vertexai.Client(
        project=PROJECT_ID,
        location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    print("Deploying Reservation Agent to Agent Runtime...")
    print("This may take 3-5 minutes.")

    remote_agent = client.agent_engines.create(
        agent=a2a_agent,
        config={
            "display_name": agent_card.name,
            "description": agent_card.description,
            "requirements": [
                "google-cloud-aiplatform[agent_engines,adk]==1.149.0",
                "a2a-sdk==0.3.26",
                "google-adk==1.29.0",
                "cloudpickle",
                "pydantic"
            ],
            "extra_packages": [
                "./reservation_agent",
            ],
            "http_options": {
                "api_version": "v1beta1",
            },
            "staging_bucket": BUCKET_URI,
        },
    )

    resource_name = remote_agent.api_resource.name
    print(f"\nDeployment complete!")
    print(f"Resource name: {resource_name}")

    env_path = Path(".env")
    lines = env_path.read_text().splitlines() if env_path.exists() else []
    lines = [l for l in lines if not l.startswith("RESERVATION_AGENT_RESOURCE_NAME=")]
    lines.append(f"RESERVATION_AGENT_RESOURCE_NAME={resource_name}")
    env_path.write_text("\n".join(lines) + "\n")
    print("Written RESERVATION_AGENT_RESOURCE_NAME to .env")


if __name__ == "__main__":
    main()

ডিপ্লয় স্ক্রিপ্টটি লোকাল টেস্টিং-এ ব্যবহৃত একই agent_card এবং ReservationAgentExecutor ইম্পোর্ট করে — কোনো কোড ডুপ্লিকেশন নেই। এজেন্ট রানটাইম ডিপ্লয়মেন্টের জন্য A2aAgent অবজেক্টটিকে তার ডিপেন্ডেন্সিগুলোসহ সিরিয়ালাইজ (পিকল) করে। ডিপ্লয়মেন্ট স্ক্রিপ্টের শেষে, এটি .env ফাইলে RESERVATION_AGENT_RESOURCE_NAME ভ্যালুটি লিখে দেবে।

এজেন্ট রানটাইমে স্থাপন করুন

ডিপ্লয়মেন্ট স্ক্রিপ্টটি চালান:

PYTHONPATH=. uv run python scripts/deploy_a2a_agent_runtime.py

ডেপ্লয়মেন্ট হতে ৩-৫ মিনিট সময় লাগে। স্ক্রিপ্টটি এজেন্ট রানটাইমে একটি সার্ভারলেস এন্ডপয়েন্ট প্রোভিশন করে, যা রিজার্ভেশন এজেন্টকে হোস্ট করে। সফল ডেপ্লয়মেন্টের পর আপনি নিচে দেখানো ছবির মতো আউটপুট দেখতে পাবেন।

Deploying Reservation Agent to Agent Runtime...
This may take 3-5 minutes.

Deployment complete!
Resource name: projects/your-project-number/locations/us-central1/reasoningEngines/your-agent-deployment-unique-id
Written RESERVATION_AGENT_RESOURCE_NAME to .env

আপনি ক্লাউড কনসোলে ডেপ্লয় করা এজেন্টটি দেখতে পারেন। কনসোল সার্চ বারে Agent Platform লিখে সার্চ করুন।

af3751f461e4708c.png

তারপর, বাম ট্যাবে, Agents উপর মাউস রাখুন এবং Deployments নির্বাচন করুন।

8a9c7fd127e60aca.png

আপনি নিচে দেখানো ছবির মতো ডেপ্লয়মেন্ট লিস্টে Reservation Agent তালিকাভুক্ত দেখতে পাবেন।

a38b46bcb6c8e4db.png

স্থাপন করা এজেন্ট পরীক্ষা করুন

এখন, আমরা ডেপ্লয় করা এজেন্টটি পরীক্ষা করার জন্য প্রস্তুত, ডেপ্লয় করা এজেন্টটির জন্য একটি টেস্ট স্ক্রিপ্ট তৈরি করুন:

cloudshell edit scripts/test_a2a_agent_runtime.py

নিম্নলিখিতটি scripts/test_a2a_agent_runtime.py ফাইলে কপি করুন:

# scripts/test_a2a_agent_runtime.py
import asyncio
import os
import time

import vertexai
from a2a.types import TaskState
from dotenv import load_dotenv
from google.genai import types

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
RESOURCE_NAME = os.environ["RESERVATION_AGENT_RESOURCE_NAME"]


async def main():
    vertexai.init(project=PROJECT_ID, location=REGION)
    client = vertexai.Client(
        project=PROJECT_ID, location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    agent = client.agent_engines.get(name=RESOURCE_NAME)

    # 1. Get agent card
    print("=" * 50)
    print("1. Retrieving agent card...")
    print("=" * 50)
    card = await agent.handle_authenticated_agent_card()
    print(f"Agent: {card.name}")
    print(f"URL: {card.url}")
    print(f"Skills: {[s.name for s in card.skills]}")

    # 2. Send a reservation request
    print("\n" + "=" * 50)
    print("2. Sending reservation request...")
    print("=" * 50)
    message_data = {
        "messageId": "msg-remote-001",
        "role": "user",
        "parts": [{"kind": "text", "text": "Book a table for 3 on Sunday at noon. Name: Carol, Phone: 555-0303"}],
    }
    response = await agent.on_message_send(**message_data)

    task_object = None
    for chunk in response:
        if isinstance(chunk, tuple) and len(chunk) > 0 and hasattr(chunk[0], "id"):
            task_object = chunk[0]
            break

    task_id = task_object.id
    print(f"Task ID: {task_id}")
    print(f"Status: {task_object.status.state}")

    # 3. Poll for result
    print("\n" + "=" * 50)
    print("3. Waiting for result...")
    print("=" * 50)
    result = None
    for _ in range(30):
        try:
            result = await agent.on_get_task(id=task_id)
            if result.status.state in [TaskState.completed, TaskState.failed]:
                break
        except Exception:
            pass
        time.sleep(1)

    print(f"Final status: {result.status.state}")
    if result.artifacts:
        for artifact in result.artifacts:
            if artifact.parts and hasattr(artifact.parts[0], "root") and hasattr(artifact.parts[0].root, "text"):
                print(f"Answer: {artifact.parts[0].root.text}")

    print("\n" + "=" * 50)
    print("Remote agent test passed!")
    print("=" * 50)


if __name__ == "__main__":
    asyncio.run(main())

তাহলে, পরীক্ষাটি চালানো যাক।

source .env
uv run python scripts/test_a2a_agent_runtime.py

আউটপুটে 'রেস্তোরাঁ রিজার্ভেশন' দক্ষতা সহ এজেন্ট কার্ডটি দেখানো হয়, এবং এরপর রিজার্ভেশন নিশ্চিতকরণের মাধ্যমে কাজটি সম্পন্ন হয়।

==================================================
1. Retrieving agent card...
==================================================
Agent: Reservation Agent
URL: https://us-central1-aiplatform.googleapis.com/v1beta1/projects/your-project-id/locations/us-central1/reasoningEngines/your-agent-unique-id/a2a
Skills: ['Restaurant Reservations']

==================================================
2. Sending reservation request...
==================================================
Task ID: b34585d0-5f03-4cb0-85a3-40710a0d224d
Status: TaskState.completed

==================================================
3. Waiting for result...
==================================================
Final status: TaskState.completed
Answer: Your reservation for Carol, party of 3 on Sunday at noon with phone number 555-0303 is confirmed.

==================================================
Remote agent test passed!
==================================================

রিজার্ভেশন এজেন্টটি এখন এজেন্ট রানটাইমে একটি পরিচালিত A2A এন্ডপয়েন্ট হিসেবে সফলভাবে চলছে।

৯. A2A রিজার্ভেশন এজেন্টকে রুট রেস্টুরেন্ট এজেন্টের সাথে একীভূত করুন

এই ধাপে রেস্তোরাঁ এজেন্টকে আপগ্রেড করা হয়, যাতে এটি ডেপ্লয় করা রিজার্ভেশন এজেন্টকে একটি রিমোট A2A সাব-এজেন্ট হিসেবে ব্যবহার করতে পারে। অর্কেস্ট্রেটরটি স্থানীয়ভাবে চলে, আর রিজার্ভেশন এজেন্টটি চলে এজেন্ট রানটাইমে — এটি একটি আংশিক ইন্টিগ্রেশন যা সম্পূর্ণ ডেপ্লয়মেন্টের আগে A2A সংযোগটি যাচাই করে।

A2A এজেন্ট কার্ডের URL সমাধান করুন

RemoteA2aAgent এর সক্ষমতাগুলো জানার জন্য ডেপ্লয় করা রিজার্ভেশন এজেন্টের কার্ড URL প্রয়োজন। এমন একটি স্ক্রিপ্ট তৈরি করুন যা এজেন্ট রানটাইম থেকে এই URL-টি সংগ্রহ করে রেস্তোরাঁ এজেন্টের .env ফাইলে লিখে রাখবে:

cloudshell edit scripts/resolve_agent_card_url.py

নিম্নলিখিতটি scripts/resolve_agent_card_url.py ফাইলে কপি করুন:

# scripts/resolve_agent_card_url.py
import asyncio
import os
from pathlib import Path

import vertexai
from dotenv import load_dotenv
from google.genai import types

load_dotenv()

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
REGION = os.environ["REGION"]
RESOURCE_NAME = os.environ["RESERVATION_AGENT_RESOURCE_NAME"]


async def main():
    vertexai.init(project=PROJECT_ID, location=REGION)
    client = vertexai.Client(
        project=PROJECT_ID, location=REGION,
        http_options=types.HttpOptions(api_version="v1beta1"),
    )

    agent = client.agent_engines.get(name=RESOURCE_NAME)
    card = await agent.handle_authenticated_agent_card()
    card_url = f"{card.url}/v1/card"

    print(f"Agent: {card.name}")
    print(f"Card URL: {card_url}")

    # Write to restaurant_agent/.env
    # Write to both restaurant_agent/.env (for adk web) and root .env (for Cloud Run deploy)
    for env_path in [Path("restaurant_agent/.env"), Path(".env")]:
        lines = env_path.read_text().splitlines() if env_path.exists() else []
        lines = [l for l in lines if not l.startswith("RESERVATION_AGENT_CARD_URL=")]
        lines.append(f"RESERVATION_AGENT_CARD_URL={card_url}")
        env_path.write_text("\n".join(lines) + "\n")
        print(f"Written RESERVATION_AGENT_CARD_URL to {env_path}")


if __name__ == "__main__":
    asyncio.run(main())

এজেন্ট কার্ডের URL দিয়ে .env ফাইলটি পূরণ করতে স্ক্রিপ্টটি চালান।

uv run python scripts/resolve_agent_card_url.py
source .env

রেস্তোরাঁর এজেন্টকে আপডেট করুন

রেস্তোরাঁ এজেন্ট ফাইলটি খুলুন:

cloudshell edit restaurant_agent/agent.py

তারপর, রিমোট রিজার্ভেশন এজেন্টকে সাব-এজেন্ট হিসেবে অন্তর্ভুক্ত করে এমন আপডেট করা সংস্করণটি দিয়ে বিষয়বস্তু প্রতিস্থাপন করুন:

# restaurant_agent/agent.py
import os

import httpx
from google.adk.agents import LlmAgent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.auth import default
from google.auth.transport.requests import Request as AuthRequest
from toolbox_adk import ToolboxToolset

TOOLBOX_URL = os.environ.get("TOOLBOX_URL", "http://127.0.0.1:5000")
RESERVATION_AGENT_CARD_URL = os.environ.get("RESERVATION_AGENT_CARD_URL", "")

toolbox = ToolboxToolset(TOOLBOX_URL)


class GoogleCloudAuth(httpx.Auth):
    """Auto-refreshing Google Cloud authentication for httpx.

    Refreshes the access token before each request if expired,
    so long-running agents never hit 401 errors.
    """

    def __init__(self):
        self.credentials, _ = default(
            scopes=["https://www.googleapis.com/auth/cloud-platform"]
        )

    def auth_flow(self, request):
        # Refresh the token if it is expired or missing
        if not self.credentials.valid:
            self.credentials.refresh(AuthRequest())
            
        request.headers["Authorization"] = f"Bearer {self.credentials.token}"
        yield request


reservation_remote_agent = RemoteA2aAgent(
    name="reservation_agent",
    description="Handles restaurant table reservations — create, check, and cancel bookings. Delegate to this agent when the user wants to book a table, check a reservation, or cancel a reservation.",
    agent_card=RESERVATION_AGENT_CARD_URL,
    httpx_client=httpx.AsyncClient(auth=GoogleCloudAuth(), timeout=60),
)

root_agent = LlmAgent(
    name="restaurant_agent",
    model="gemini-2.5-flash",
    instruction="""You are a friendly and knowledgeable concierge at "Foodie Finds," a restaurant. Your job:
- Help diners browse the menu by category or cuisine type.
- Provide full details about specific dishes, including ingredients, price, and dietary information.
- Recommend dishes based on natural language descriptions of what the diner is craving.
- Add new menu items when asked.
- For reservation requests (booking, checking, or cancelling tables), delegate to the reservation_agent.

When a diner asks about a specific dish by name or cuisine, use the get-item-details tool.
When a diner asks for a specific category or cuisine type, use the search-menu tool.
When a diner describes what kind of food they want — by flavor, texture, dietary needs, or cravings — use the search-menu-by-description tool for semantic search.

When in doubt between search-menu and search-menu-by-description, prefer search-menu-by-description — it searches dish descriptions and finds more relevant matches.
If a dish is not available (available is false), let the diner know and suggest similar alternatives from the search results.
Be conversational, knowledgeable, and concise.""",
    tools=[toolbox],
    sub_agents=[reservation_remote_agent],
)

পূর্ববর্তী সংস্করণ থেকে প্রধান পরিবর্তনগুলো হলো:

  • GoogleCloudAuth — একটি কাস্টম httpx.Auth হ্যান্ডলার যা প্রতিটি অনুরোধের আগে গুগল ক্লাউড অ্যাক্সেস টোকেন রিফ্রেশ করে। এজেন্ট রানটাইমের জন্য প্রমাণীকৃত A2A কল প্রয়োজন, এবং টোকেনগুলো একটি নির্দিষ্ট সময় পরে মেয়াদোত্তীর্ণ হয়ে যায়।
  • RemoteA2aAgent resolve স্ক্রিপ্ট দ্বারা লিখিত .env ফাইল থেকে RESERVATION_AGENT_CARD_URL পড়ে এবং প্রমাণীকৃত httpx_client ব্যবহার করে।
  • সাব-এজেন্ট হিসেবে নিবন্ধিত — ADK-এর অর্কেস্ট্রেটর স্বয়ংক্রিয়ভাবে রিজার্ভেশন অনুরোধগুলো এর কাছে অর্পণ করে।
  • সংরক্ষণ প্রতিনিধিদলের উল্লেখ করার জন্য হালনাগাদ নির্দেশনা

ইন্টিগ্রেটেড এজেন্টটি স্থানীয়ভাবে পরীক্ষা করুন

স্টার্টার এজেন্টের জন্য এমসিপি টুলবক্সের সাথে ইন্টিগ্রেশন প্রয়োজন, যার জন্য প্রয়োজনীয় ফাইলটি পূর্ববর্তী কোডল্যাব অথবা স্টার্টার রিপো থেকে ইতোমধ্যেই সরবরাহ করা থাকার কথা। আমাদের শুধু এটা নিশ্চিত করতে হবে যে টুলবক্স প্রসেসটি যেন সঠিকভাবে চলে।

যদি আপনার .env ফাইলের TOOLBOX_URL ইতিমধ্যেই কোনো ক্লাউড রান সার্ভিসকে নির্দেশ করে (আগের কোডল্যাব থেকে অথবা স্টার্টার রিপোর full_setup.sh থেকে), তাহলে আপনি এই ধাপটি বাদ দিতে পারেন — এজেন্টটি ডেপ্লয় করা টুলবক্সের সাথে সংযুক্ত হবে।

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

if curl -s http://127.0.0.1:5000/api/toolsets > /dev/null 2>&1; then
  echo "Toolbox already running on port 5000"
else
  set -a; source .env; set +a
  ./toolbox --config=tools.yaml > logs/toolbox.log 2>&1 &
  echo "Toolbox started"
fi

তারপর, আমরা ADK ওয়েব ডেভ UI-এর মাধ্যমে রেস্তোরাঁ এজেন্টের সাথে যোগাযোগ করার চেষ্টা করতে পারি।

uv run adk web --allow_origins "regex:https://.*\.cloudshell\.dev" --port 8080

ক্লাউড শেল ওয়েব প্রিভিউ ব্যবহার করে ADK ওয়েব UI খুলুন (ওয়েব প্রিভিউ বোতামে ক্লিক করুন, পোর্ট পরিবর্তন করে 8080 করুন) এবং তারপরে restaurant_agent নির্বাচন করুন।

65a055b70ab52aa8.png

একটি মিশ্র কথোপকথন পরীক্ষা করুন:

মেনু কোয়েরি

What Italian dishes do you have?

রিজার্ভেশন অনুরোধ

I want to create reservation under name Bob, phone number 123456

রিজার্ভেশন যাচাই করুন

নতুন সেশন তৈরি করুন (নতুন করে কথোপকথন শুরু করুন):

Check the reservation for 123456

92cef3bc7671129a.png

16bfd60f202dcaa7.png

c5326bbf6fa778e2.png

দুইবার Ctrl+C চেপে adk web প্রসেসটি বন্ধ করুন। এরপর এজেন্টটি সম্পূর্ণরূপে ডেপ্লয় করে সিস্টেমটি সম্পূর্ণ করা যাক।

১০. আপডেট করা রেস্টুরেন্ট এজেন্টটি ক্লাউড রানে স্থাপন করুন।

এই ধাপে A2A ইন্টিগ্রেশন সহ রেস্তোরাঁ এজেন্টটিকে ক্লাউড রান-এ পুনরায় স্থাপন করা হয়, যার মাধ্যমে সম্পূর্ণরূপে স্থাপিত মাল্টি-এজেন্ট সিস্টেমটি সম্পন্ন হয়।

এজেন্ট রানটাইম অ্যাক্সেস করার অনুমতি দিন

ক্লাউড রান সার্ভিস অ্যাকাউন্টের এজেন্ট রানটাইম কল করার অনুমতি প্রয়োজন। ডিফল্ট কম্পিউট ইঞ্জিন সার্ভিস অ্যাকাউন্টকে roles/aiplatform.user রোলটি প্রদান করুন:

PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
  --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --role="roles/aiplatform.user"

ক্লাউড রানে স্থাপন করুন

এই সেটআপে, আমরা ধরে নিচ্ছি যে রেস্তোরাঁ এজেন্ট সার্ভিসটি আগের কোডল্যাব থেকে অথবা নতুন করে শুরু করলে scripts/full_setup.sh চালানোর মাধ্যমে আগে থেকেই বিদ্যমান আছে। এটি আপডেট করা কোড (নতুন RemoteA2aAgent ইন্টিগ্রেশন) সহ পুনরায় ডিপ্লয় করে এবং রিজার্ভেশন এজেন্ট কার্ডের URL-টিকে একটি নতুন env var হিসেবে যোগ করে — বিদ্যমান env var-গুলো ( TOOLBOX_URL , GOOGLE_CLOUD_PROJECT , ইত্যাদি) অপরিবর্তিত থাকে।

gcloud run deploy restaurant-agent \
  --source . \
  --region=$REGION \
  --allow-unauthenticated \
  --update-env-vars="RESERVATION_AGENT_CARD_URL=$RESERVATION_AGENT_CARD_URL" \
  --min-instances=0 \
  --max-instances=1 \
  --memory=1Gi \
  --port=8080

সম্পূর্ণরূপে স্থাপন করা সিস্টেমটি পরীক্ষা করুন

ডেপ্লয় করা সার্ভিসের URL-টি নিন:

AGENT_URL=$(gcloud run services describe restaurant-agent --region=$REGION --format='value(status.url)')
echo "Agent URL: $AGENT_URL"

আপনার ব্রাউজারে ইউআরএলটি খুলুন। ADK ওয়েব UI লোড হবে — এটি সেই একই ইন্টারফেস যা আপনি স্থানীয়ভাবে ব্যবহার করতেন, যা এখন ক্লাউড রানে চলছে।

এজেন্টের সাথে নির্দ্বিধায় গল্প করুন।

মেনু কোয়েরি

What spicy dishes do you have?

রিজার্ভেশন অনুরোধ

Book a table for 4 on Friday at 7pm. Name: Eve, Phone: 555-0505

রিজার্ভেশন যাচাই করুন

নতুন সেশন তৈরি করুন (নতুন করে কথোপকথন শুরু করুন):

Check reservation for 555-0505

69ae9a7c35255fc.png

55145841338ec9b3.png

মাল্টি-এজেন্ট সিস্টেমটি সম্পূর্ণরূপে স্থাপন করা হয়েছে। ক্লাউড রান-এ থাকা রেস্তোরাঁ এজেন্টটি দুটি ব্যাকএন্ড সার্ভিসের মধ্যে সমন্বয় সাধন করে: মেনু পরিচালনার জন্য এমসিপি টুলবক্স এবং এজেন্ট রানটাইম-এ থাকা এ২এ রিজার্ভেশন এজেন্ট।

১১. অভিনন্দন!

আপনি গুগল ক্লাউডে A2A প্রোটোকল ব্যবহার করে একটি মাল্টি-এজেন্ট সিস্টেম তৈরি ও স্থাপন করেছেন।

আপনি যা শিখেছেন

  • একটি ADK এজেন্ট তৈরি করেছি যা ডাটাবেস ছাড়াই সেশন স্টেট ( ToolContext ) ব্যবহার করে রিজার্ভেশন ডেটা পরিচালনা করে।
  • এজেন্ট প্ল্যাটফর্ম SDK ব্যবহার করে এজেন্ট রানটাইমে একটি A2A এজেন্ট স্থাপন করা হয়েছে।
  • RemoteA2aAgent সাব-এজেন্ট হিসেবে ব্যবহার করে অন্য একটি ADK এজেন্ট থেকে একটি রিমোট A2A এজেন্ট গ্রহণ করা হয়েছে।
  • সিস্টেমটি পর্যায়ক্রমে পরীক্ষা করা হয়েছে: স্থানীয় A2A → ডেপ্লয়েড A2A → আংশিক ইন্টিগ্রেশন → সম্পূর্ণ ডেপ্লয়মেন্ট

পরিষ্কার করা

আপনার গুগল ক্লাউড অ্যাকাউন্টে চার্জ হওয়া এড়াতে, এই কোডল্যাবে তৈরি করা রিসোর্সগুলো মুছে ফেলুন।

gcloud projects delete $GOOGLE_CLOUD_PROJECT

বিকল্প ২: স্বতন্ত্র রিসোর্সগুলো মুছে ফেলুন

# Delete the Agent Runtime deployment
uv run python -c "
import vertexai
from google.genai import types
vertexai.init(project='$GOOGLE_CLOUD_PROJECT', location='$REGION')
client = vertexai.Client(
    project='$GOOGLE_CLOUD_PROJECT', location='$REGION',
    http_options=types.HttpOptions(api_version='v1beta1'),
)
agent = client.agent_engines.get(name='$RESERVATION_AGENT_RESOURCE_NAME')
agent.delete(force=True)
print('Agent Runtime deployment deleted.')
"

# Delete Cloud Run services
gcloud run services delete restaurant-agent --region=$REGION --quiet
gcloud run services delete toolbox-service --region=$REGION --quiet

# Delete Cloud SQL instance
gcloud sql instances delete $DB_INSTANCE --quiet

# Delete GCS staging bucket
gsutil rm -r gs://$STAGING_BUCKET