اپلیکیشن هوش مصنوعی Agentic بسازید که مشتری شما بتواند از طریق تلگرام با آن تعامل داشته باشد

۱. مقدمه

تجربه‌های هوش مصنوعی Agentic یکپارچه و تعاملی ایجاد کنید که مشتریان شما بتوانند مستقیماً از طریق برنامه پیام‌رسانی که قبلاً از آن استفاده کرده‌اند، با آن تعامل داشته باشند. نحوه توسعه و استقرار برنامه‌های هوشمندی را که به راحتی در رابط‌های وب و کانال‌های پیام‌رسانی مدرن اجرا می‌شوند، کشف کنید.

آنچه خواهید ساخت

ادغام بین یک برنامه کامل "دربان رستوران" مبتنی بر ADK که توسط Gemini ارائه می‌شود و به مشتریان کمک می‌کند تا منوی رستوران را مرور کرده و رزرو کنند، و برنامه چت تلگرام. می‌توانید با ربات تلگرام تعامل داشته باشید و از آنها توضیحات زبان طبیعی مانند "من چیزی تند و گیاهی می‌خواهم" بخواهید. سپس ربات به عامل ADK متصل می‌شود که از یک پایگاه داده Cloud SQL PostgreSQL به طور کامل از طریق MCP Toolbox for Databases می‌خواند و در آن می‌نویسد، که تمام دسترسی به پایگاه داده - از جمله تولید خودکار جاسازی برای جستجوی برداری - را مدیریت می‌کند، در همین حال کاربر می‌تواند ببیند که ربات پیام را تأیید می‌کند و در حالی که منتظر پاسخ از عامل ADK است، تایپ می‌کند ... typing برای پاسخ.

c1d28343ed68358a.png

آنچه یاد خواهید گرفت

  • یک برنامه کاربردی مبتنی بر ADK با پشتیبانی Gemini به نام "دربان رستوران" راه‌اندازی کنید.
  • راه اندازی ربات چت تلگرام با استفاده از BotFather
  • یک برنامه پایتون بنویسید که به وب هوک ربات گوش دهد
  • ارسال اقدام چت برای ارائه ... typing در تلگرام روی پیام کاربر، و انجام نظرسنجی برای ارسال ... typing دوره‌ای در حالی که منتظر پاسخ واقعی هستید
  • برای پردازش درخواست کاربر، با نقطه پایانی ابریِ دربان Restaurant Concierge تماس بگیرید
  • مدیریت بازگشت از عامل ADK و ارسال پیام به تلگرام و بستن بافر
  • برنامه پایتون را روی فضای ابری اجرا کنید
  • با ربات تلگرام خود تعامل داشته باشید

پیش‌نیازها

۲. تنظیمات محیط - ادامه از آزمایشگاه کد قبلی

روایت‌هایی که در این آزمایشگاه کد ارائه می‌دهیم در واقع ادامه‌ی این آزمایشگاه کد پیش‌نیاز است: Agentic RAG با ADK، MCP Toolbox و Cloud SQL یا Agents at Scale: Multi-Agent Architecture with A2A Protocol on Agent Runtime and ADK Integration . می‌توانید کار خود را از آزمایشگاه کد قبلی ادامه دهید.

می‌توانیم ساخت را در دایرکتوری کاری قبلی codelab شروع کنیم (دایرکتوری کاری باید build-agent-adk-toolbox-cloudsql یا adk-a2a-agent-runtime-starter باشد). برای جلوگیری از سردرگمی، بیایید نام دایرکتوری را به همان نام دایرکتوری که هنگام شروع مجدد استفاده می‌کنیم، تغییر دهیم.

اگر از آزمایشگاه Agentic RAG با ADK، MCP Toolbox و Cloud SQL ادامه می‌دهید:

mv ~/build-agent-adk-toolbox-cloudsql ~/build-agent-adk-telegram

در غیر این صورت، اگر از آزمایشگاه Agents at Scale: معماری چندعاملی با پروتکل A2A در زمان اجرای Agent و ادغام ADK ادامه می‌دهید.

mv ~/adk-a2a-agent-runtime-starter ~/build-agent-adk-telegram

سپس، دایرکتوری کاری خود را به آن تغییر دهید

cloudshell workspace ~/build-agent-adk-telegram && cd ~/build-agent-adk-telegram
source .env

پس از آن، تأیید کنید که restaurant-agent شما از قبل مستقر شده است و URL عمومی برای دسترسی دارید.

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

echo "      ✓ Agent service deployed"
echo "      Agent URL: $AGENT_URL"
echo ""

اگر می‌توانید به URL دسترسی پیدا کنید، می‌توانید به بخش بعدی بروید: Create Telegram Bot

۳. تنظیمات محیط - شروع تازه با مخزن آغازین

این مرحله محیط Cloud Shell شما را آماده می‌کند، پروژه Google Cloud شما را پیکربندی می‌کند و مخزن اولیه را کلون می‌کند.

پوسته ابری را باز کنید

Cloud Shell را در مرورگر خود باز کنید. Cloud Shell یک محیط از پیش پیکربندی شده با تمام ابزارهای مورد نیاز برای این آزمایشگاه کد را فراهم می‌کند. در صورت درخواست، روی تأیید (Authorize) کلیک کنید.

سپس روی « مشاهده » -> « ترمینال » کلیک کنید تا ترمینال باز شود. رابط کاربری شما باید شبیه به این باشد.

۸۶۳۰۷fac5da2f077.png

این رابط اصلی ما خواهد بود، IDE در بالا، ترمینال در پایین

دایرکتوری کاری خود را تنظیم کنید

مخزن اولیه را کلون کنید، تمام کدهایی که در این codelab می‌نویسید اینجا قرار می‌گیرند:

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

فایل .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 تنظیم کنید

با بررسی متن زرد رنگ کنار دایرکتوری کاری خود در اعلان ترمینال Cloud Shell، مطمئن شوید که پروژه به درستی تنظیم شده است. باید شناسه پروژه شما نمایش داده شود.

5c515e235ee1179f.png

راه‌اندازی زیرساخت اولیه

ابتدا، باید وابستگی‌های پایتون را با استفاده از uv نصب کنیم، uv یک بسته سریع پایتون و مدیر پروژه است که با زبان Rust نوشته شده است (مستندات uv). این codelab از آن برای سرعت و سادگی در نگهداری پروژه پایتون استفاده می‌کند.

uv sync

سپس، اسکریپت راه‌اندازی کامل را اجرا کنید، که نمونه Cloud SQL را ایجاد می‌کند، داده‌ها را بارگذاری می‌کند و سرویس Toolbox را مستقر می‌کند که به عنوان وضعیت اولیه عامل رستوران ما عمل خواهد کرد.

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

این باعث می‌شود:

  • ایجاد یک نمونه Cloud SQL و ایجاد پایگاه داده (مرحله 1)
  • ایجاد پیکربندی محیط عامل و شروع سرویس جعبه ابزار محلی (مرحله 2)
  • استقرار سرویس‌های Toolbox و Agent در Cloud Run (فاز 3)

پس از اتمام این استقرار، می‌توانید به رابط کاربری ADK Dev در آدرس اینترنتی Cloud Run دسترسی داشته باشید.

source .env
AGENT_URL=$(gcloud run services describe restaurant-agent \
    --region="$REGION" \
    --format='value(status.url)')

echo "      ✓ Agent service deployed"
echo "      Agent URL: $AGENT_URL"
echo ""

رابط کاربری ADK dev را باز کنید، restaurant_agent را انتخاب کنید و با کوئری‌هایی مانند مثال زیر تست کنید:

What Italian dishes do you have?

یا،

I want something spicy and creamy

حالا، اقدام بعدی این است که چگونه می‌توانیم از رابط توسعه وب صرف به کانال پیام‌رسان تلگرام منتقل شویم؟

۴. ایجاد ربات تلگرام

تلگرام یک پلتفرم پیام‌رسان رایگان شناخته‌شده است که به‌طور گسترده برای تعاملات مبتنی بر جامعه مورد استفاده قرار می‌گیرد، یکی از دلایل آن این است که روش‌های زیادی برای ادغام آسان ارائه می‌دهد، به همین دلیل افراد می‌توانند به راحتی ربات خود را با عملکردهای متنوع و زیادی ایجاد کنند.

در این مورد، ما برای اولین بار از BotFather برای ایجاد ربات خود استفاده خواهیم کرد. به خاطر داشته باشید که اگرچه ما برای این جلسه از تلگرام استفاده می‌کنیم، اما همین روش را می‌توان برای واتس‌اپ یا سایر پلتفرم‌های پیام‌رسان دلخواه نیز استفاده کرد.

با استفاده از BotFather ربات خودتان را بسازید

برای شروع ساخت ربات تلگرام خود، مرورگر وب خود را باز کنید و به آدرس https://telegram.me/BotFather مراجعه کنید.

۱b817e758c699a79.png

شروع تعامل با BotFather

ad3daa08e73502db.png

دستور /start را ارسال کنید

برای شروع کار با BotFather و شروع به ایجاد اولین ربات خود، باید پیام /start را به BotFather فراخوانی کنید، سپس تمام دستوراتی را که برای تعامل بیشتر با شما دارد، به اشتراک می‌گذارد.

/start

ایجاد ربات را با دستور /newbot آغاز کنید

بیایید با ارسال دستور /newbot به BotFather، ربات جدید خود را ایجاد کنیم. از شما نامی برای ربات خود و سپس username ربات را می‌خواهد که همیشه باید به bot ختم شود. برای مثال TetrisBot یا tetris_bot . این نام باید منحصر به فرد باشد.

۱f6a74f494d48986.png

پس از ایجاد موفقیت‌آمیز ربات، پیام زیر را از BotFather دریافت خواهید کرد.

Done! Congratulations on your new bot. You will find it at t.me/AdkTelegramTest_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:
<YOUR_TELEGRAM_API_KEY>
Keep your token secure and store it safely, it can be used by anyone to control your bot.

For a description of the Bot API, see this page: https://core.telegram.org/bots/api

به YOUR_TELEGRAM_API_KEY توجه داشته باشید، ما در بخش بعدی از آن استفاده خواهیم کرد.

۵. اپلیکیشن وب‌هوک تلگرام را توسعه دهید

بیایید دایرکتوری کاری را برای شروع توسعه برنامه وب هوک تلگرام خود آماده کنیم.

mkdir ~/build-agent-adk-telegram/telegram-integration
cd ~/build-agent-adk-telegram

افزودن وابستگی‌های مورد نیاز

اسکریپت requirements.txt را با محتوای زیر ایجاد کنید تا وابستگی‌های کافی برای اسکریپت شنونده وب‌هوک تلگرام فراهم شود.

cloudshell edit ./telegram-integration/requirements.txt

سپس وابستگی‌های زیر را اضافه کنید

python-telegram-bot[webhooks]
httpx

ایجاد اسکریپت برای شنونده وب هوک تلگرام

وقتی وابستگی‌ها نصب شدند، می‌توانیم یک اسکریپت پایتون main.py برای برنامه‌ی یکپارچه‌سازی ایجاد کنیم.

cloudshell edit ~/build-agent-adk-telegram/telegram-integration/main.py

سپس کد زیر را در آن کپی کنید

# ./telegram-integration/main.py

import asyncio
import os
import sys
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, CallbackContext
from telegram.constants import ChatAction
import httpx

# Read token from environment variable
TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
ADK_SERVER_URL = os.environ.get("ADK_SERVER_URL", "http://localhost:8000")
ADK_APP_NAME = os.environ.get("ADK_APP_NAME", "restaurant_agent")

# Parse base URL out of ADK_SERVER_URL
BASE_URL = ADK_SERVER_URL.rstrip('/')
if BASE_URL.endswith('/run'):
    BASE_URL = BASE_URL[:-4]
elif BASE_URL.endswith('/query'):
    BASE_URL = BASE_URL[:-6]

if not TOKEN:
    print("Error: TELEGRAM_BOT_TOKEN environment variable not set.")
    print("Please set it before running the application.")
    sys.exit(1)

async def start(update: Update, context: CallbackContext) -> None:
    """Send a message when the command /start is issued."""
    await update.message.reply_text('Hi! I am your ADK Integration Bot. Send me a message and I will forward it to the ADK server.')

async def send_typing_loop(chat_id: int, bot, stop_event: asyncio.Event):
    """Send typing action periodically until the stop event is set."""
    while not stop_event.is_set():
        try:
            await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
            # The research suggested repeating every 4 seconds
            await asyncio.sleep(4)
        except Exception as e:
            print(f"Error sending chat action: {e}")
            await asyncio.sleep(1) # Wait a bit before retrying if error

async def handle_message(update: Update, context: CallbackContext) -> None:
    """Handle incoming user messages."""
    user_message = update.message.text
    chat_id = update.message.chat_id
    raw_user_id = str(update.message.from_user.id)
    
    # Derive unique user_id and session_id for this user
    user_id = f"tg_{raw_user_id}"
    session_id = f"tg_sess_{raw_user_id}"

    print(f"Received message from {user_id}: {user_message}")

    # Create a stop event for the typing loop
    stop_event = asyncio.Event()
    
    # Start the typing loop as a background task
    typing_task = asyncio.create_task(send_typing_loop(chat_id, context.bot, stop_event))

    try:
        async with httpx.AsyncClient() as client:
            # 1. Check if the session exists
            session_url = f"{BASE_URL}/apps/{ADK_APP_NAME}/users/{user_id}/sessions/{session_id}"
            session_check = await client.get(session_url, timeout=10.0)
            
            if session_check.status_code == 404:
                # 2. If session doesn't exist, create it
                print(f"Session {session_id} not found. Creating session...")
                session_create = await client.post(session_url, json={}, timeout=10.0)
                if session_create.status_code != 200:
                    raise Exception(f"Failed to create session: {session_create.status_code} {session_create.text}")
            elif session_check.status_code != 200:
                raise Exception(f"Error checking session: {session_check.status_code} {session_check.text}")
            
            # 3. Run the ADK agent
            run_url = f"{BASE_URL}/run"
            payload = {
                "appName": ADK_APP_NAME,
                "userId": user_id,
                "sessionId": session_id,
                "newMessage": {
                    "role": "user",
                    "parts": [{"text": user_message}]
                }
            }
            response = await client.post(run_url, json=payload, timeout=60.0)
            
        if response.status_code == 200:
            events = response.json()
            if isinstance(events, list) and len(events) > 0:
                # The last event contains the final text response
                last_event = events[-1]
                content = last_event.get("content", {})
                parts = content.get("parts", [])
                if parts and "text" in parts[0]:
                    reply_text = parts[0]["text"]
                else:
                    reply_text = "ADK agent returned an empty or non-text response."
            else:
                reply_text = "No events returned from ADK agent."
        else:
            reply_text = f"Error communicating with ADK server (Status: {response.status_code})."
            
    except Exception as e:
        reply_text = f"Failed to connect to ADK server: {e}"
    finally:
        # Stop the typing loop
        stop_event.set()
        await typing_task

    # Send the final response back to the user
    await update.message.reply_text(reply_text)

def main() -> None:
    """Start the bot."""
    # Create the Application and pass it your bot's token.
    application = Application.builder().token(TOKEN).build()

    # on different commands - answer in Telegram
    application.add_handler(CommandHandler("start", start))

    # on non command i.e message - echo the message on Telegram
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))

    # Check if running in webhook mode (e.g., on Cloud Run)
    port = os.environ.get("PORT")
    service_url = os.environ.get("SERVICE_URL")

    if port and service_url:
        if not service_url.startswith("http"):
            service_url = f"https://{service_url}"
        
        print(f"Starting bot in WEBHOOK mode on port {port} with url {service_url}")
        
        application.run_webhook(
            listen="0.0.0.0",
            port=int(port),
            url_path=TOKEN,
            webhook_url=f"{service_url}/{TOKEN}",
            allowed_updates=Update.ALL_TYPES
        )
    else:
        print("Starting bot in POLLING mode")
        # Run the bot until the user presses Ctrl-C
        application.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()

درک کد ادغام ربات تلگرام

۲۳b346f5ceb4712a.png

وقتی کاربری پیامی ارسال می‌کند، خط لوله زیر تحت handle_message() اجرا می‌شود.

مرحله ۱: استخراج هویت و نشست

این ربات، شناسه کاربری تلگرام را به شناسه‌های منحصر به فرد ADK نگاشت می‌کند تا جلسات کاربر را متمایز نگه دارد:

user_id = f"tg_{raw_user_id}"
session_id = f"tg_sess_{raw_user_id}"

مرحله ۲: وضعیت «تایپ» ناهمزمان (خطوط ۵۳ تا ۵۸)

برای اطمینان از یک تجربه کاربری بسیار پاسخگو در حالی که عامل ADK درخواست را پردازش می‌کند (که می‌تواند چند ثانیه طول بکشد)، ربات یک حلقه پس‌زمینه ناهمزمان را شروع می‌کند:

  • asyncio.Event به صورت stop_event نمونه‌سازی می‌شود.
  • تابع asyncio.create_task در پس‌زمینه، تابع send_typing_loop(...) را اجرا می‌کند.
  • این حلقه هر ۴ ثانیه یک اکشن ChatAction.TYPING به تلگرام ارسال می‌کند تا زمانی که stop_event تنظیم شود.

مرحله 3: تأیید و ایجاد نشست ADK (خطوط 61 تا 72)

قبل از اجرای عامل، ربات بررسی می‌کند که آیا جلسه‌ای از قبل وجود دارد یا خیر:

  1. یک درخواست GET به /apps/{appName}/users/{userId}/sessions/{sessionId} ارسال می‌کند.
  2. اگر پاسخ 404 Not Found باشد، جلسه را از طریق یک درخواست POST به همان URL با یک بدنه JSON خالی ایجاد می‌کند.
  3. اگر وضعیتی غیر از 200 یا 404 برگردانده شود، یک استثنا ایجاد شده است.

مرحله ۴: ارسال درخواست به عامل (خطوط ۷۴ تا ۸۵)

بار داده پیام به نقطه پایانی ADK /run ارسال می‌شود:

  • نقطه پایانی : POST /run
  • زمان انتظار درخواست روی 60.0 ثانیه تنظیم شده است تا امکان استدلال پیچیده یا تأخیر در بالادست فراهم شود.
  • ساختار بار مفید :
{
  "appName": "restaurant_agent",
  "userId": "tg_<user_id>",
  "sessionId": "tg_sess_<user_id>",
  "newMessage": {
    "role": "user",
    "parts": [{"text": "<user_message>"}]
  }
}

مرحله ۵: تجزیه پاسخ (خطوط ۸۷ تا ۱۰۱)

سرور ADK لیستی از رویدادهای پیام را برمی‌گرداند. ربات آرایه‌ی برگردانده شده را بررسی می‌کند:

  • آخرین رویداد موجود در لیست ( events[-1] ) را بازیابی می‌کند.
  • از طریق event["content"]["parts"][0]["text"] به محتوای متن هدایت می‌شود.
  • اگر هیچ رویدادی برگردانده نشود یا ساختار متنی وجود نداشته باشد، یک متن توصیفی جایگزین تنظیم می‌شود.

مرحله 6: بررسی و ارسال پاسخ (خطوط 103 تا 111)

  • در بلوک finally ، stop_event تنظیم شده و حلقه‌ی عملیات تایپ را متوقف می‌کند.
  • ربات منتظر تکمیل typing_task می‌ماند تا از پاک‌سازی منابع اطمینان حاصل کند.
  • در نهایت، ربات با متن پاسخ تجزیه‌شده به چت تلگرام پاسخ می‌دهد.

۶. اپلیکیشن وب‌هوک تلگرام را روی Cloud Run مستقر کنید

در مرحله بعد، شنونده وب‌هوک تلگرام را روی Cloud Run مستقر خواهیم کرد تا ربات ما بتواند با آن ارتباط برقرار کند.

ایجاد فایل داکر

ابتدا باید Dockerfile را ایجاد کنیم.

cloudshell edit ~/build-agent-adk-telegram/telegram-integration/Dockerfile

سپس کد زیر را در آن کپی کنید

# Use an official Python runtime as a parent image
FROM python:3.11-slim

# Prevent Python from writing pyc files to disc and buffering stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Set the working directory in the container
WORKDIR /app

# Install system dependencies if needed
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Copy the dependencies file to the working directory
COPY requirements.txt .

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY main.py .

# Expose the port that Cloud Run will provide via environment variable
EXPOSE 8080

# Run main.py when the container launches
CMD ["python", "main.py"]

این سرویس با استفاده از python:3.11-slim کانتینریزه شده است تا حجم تصویر را کوچک نگه دارد:

  • وابستگی‌ها را از requirements.txt ( python-telegram-bot[webhooks] و httpx ) نصب می‌کند.
  • پورت استاندارد 8080 را در معرض نمایش قرار می‌دهد.
  • python main.py را اجرا می‌کند.

متغیرهای محیطی را آماده کنید

پس از آن، بیایید دوباره بررسی کنیم که آیا عامل ما با موفقیت مستقر شده است یا خیر

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

echo "      ✓ Agent service deployed"
echo "      Agent URL: $AGENT_URL"
echo ""

در مرحله بعد، TELEGRAM_BOT_TOKEN که قبلاً به دست آوردیم را در فایل .env قرار می‌دهیم.

echo "TELEGRAM_BOT_TOKEN=YOUR_TELEGRAM_API_KEY" >> .env

سپس، داده‌های .env را با سایر مقادیر مورد نیاز خود پر می‌کنیم.

echo "ADK_SERVER_URL=$AGENT_URL" >> .env
echo "ADK_APP_NAME=restaurant_agent" >> .env
echo "SERVICE_NAME=telegram-integration" >> .env
source .env

ایجاد اسکریپت استقرار

بیایید اسکریپت استقراری ایجاد کنیم که بررسی‌های کامل را انجام داده و برنامه را در Cloud Run مستقر کند.

cloudshell edit ~/build-agent-adk-telegram/telegram-integration/deploy.sh

و کد زیر را در فایل کپی کنید

#!/usr/bin/env bash
# ./telegram-integration/deploy.sh

# Exit immediately if a command exits with a non-zero status
set -euo pipefail

# Color codes for neat terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0;37m' # No Color
# Load environment variables from .env if it exists
if [ -f .env ]; then
    echo -e "${GREEN}✔ Loading environment variables from .env...${NC}"
    export $(grep -v '^#' .env | xargs)
fi

echo -e "${BLUE}====================================================${NC}"
echo -e "${BLUE}   Google Cloud Run Deployment: Telegram Bot        ${NC}"
echo -e "${BLUE}====================================================${NC}"

# 1. Check for gcloud CLI
if ! command -v gcloud &> /dev/null; then
    echo -e "${RED}Error: 'gcloud' CLI is not installed.${NC}"
    echo "Please install the Google Cloud SDK and try again."
    echo "See: https://cloud.google.com/sdk/docs/install"
    exit 1
fi

# 2. Check active gcloud account/auth
ACTIVE_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null || true)
if [ -z "$ACTIVE_ACCOUNT" ]; then
    echo -e "${RED}Error: No active Google Cloud account found.${NC}"
    echo "Please run: gcloud auth login"
    exit 1
fi

# 3. Detect / Prompt for GCP Project
DEFAULT_PROJECT=${GCP_PROJECT_ID:-$(gcloud config get-value project 2>/dev/null || true)}
if [ -n "${DEFAULT_PROJECT}" ]; then
    echo -e "${GREEN}✔ Using GCP Project: $DEFAULT_PROJECT${NC}"
    GCP_PROJECT="$DEFAULT_PROJECT"
else
    echo -n "Enter GCP Project ID: "
    read -r GCP_PROJECT
fi

if [ -z "$GCP_PROJECT" ]; then
    echo -e "${RED}Error: GCP Project ID is required.${NC}"
    exit 1
fi

# Set active project
gcloud config set project "$GCP_PROJECT" &> /dev/null

# 4. Configure Service Parameters
DEFAULT_SERVICE=${SERVICE_NAME:-"telegram-integration"}
if [ -n "${SERVICE_NAME:-}" ]; then
    echo -e "${GREEN}✔ Using Cloud Run Service Name: $SERVICE_NAME${NC}"
else
    echo -n "Enter Cloud Run Service Name [Default: $DEFAULT_SERVICE]: "
    read -r SERVICE_NAME
    SERVICE_NAME=${SERVICE_NAME:-$DEFAULT_SERVICE}
fi

DEFAULT_REGION=${REGION:-"us-central1"}
if [ -n "${REGION:-}" ]; then
    echo -e "${GREEN}✔ Using Cloud Run Region: $REGION${NC}"
else
    echo -n "Enter Cloud Run Region [Default: $DEFAULT_REGION]: "
    read -r REGION
    REGION=${REGION:-$DEFAULT_REGION}
fi

DEFAULT_ADK_APP=${ADK_APP_NAME:-"restaurant_agent"}
if [ -n "${ADK_APP_NAME:-}" ]; then
    echo -e "${GREEN}✔ Using ADK App Name: $ADK_APP_NAME${NC}"
    ADK_APP="$ADK_APP_NAME"
else
    echo -n "Enter ADK App Name [Default: $DEFAULT_ADK_APP]: "
    read -r ADK_APP
    ADK_APP=${ADK_APP:-$DEFAULT_ADK_APP}
fi

# 5. Retrieve/Prompt for Telegram Bot Token
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
    echo -e "${GREEN}✔ Found TELEGRAM_BOT_TOKEN in environment.${NC}"
    BOT_TOKEN="$TELEGRAM_BOT_TOKEN"
else
    echo -e "${YELLOW}TELEGRAM_BOT_TOKEN is not set in your environment.${NC}"
    echo -n "Enter your Telegram Bot Token (input will be hidden): "
    read -s -r BOT_TOKEN
    echo ""
fi

if [ -z "$BOT_TOKEN" ]; then
    echo -e "${RED}Error: Telegram Bot Token is required.${NC}"
    exit 1
fi

# 6. Retrieve/Prompt for ADK Server URL
DEFAULT_ADK_URL="http://localhost:8000"
if [ -n "${ADK_SERVER_URL:-}" ]; then
    echo -e "${GREEN}✔ Found ADK_SERVER_URL in environment: $ADK_SERVER_URL${NC}"
    ADK_URL="$ADK_SERVER_URL"
else
    echo -n "Enter your ADK Server URL [Default: $DEFAULT_ADK_URL]: "
    read -r ADK_URL
    ADK_URL=${ADK_URL:-$DEFAULT_ADK_URL}
fi

# Enable required GCP services
echo -e "\n${YELLOW}Checking and enabling required GCP services...${NC}"
gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com --project "$GCP_PROJECT"

# Determine source directory dynamically
SOURCE_DIR="."
if [ -d "telegram-integration" ]; then
    SOURCE_DIR="telegram-integration"
    echo -e "${GREEN}✔ Found source directory: telegram-integration${NC}"
elif [ -f "Dockerfile" ]; then
    SOURCE_DIR="."
    echo -e "${GREEN}✔ Dockerfile found in current directory. Using current directory as source.${NC}"
else
    echo -e "${RED}Error: Could not find source directory 'telegram-integration' or Dockerfile in current directory.${NC}"
    exit 1
fi

# 7. First-pass Deployment with placeholder SERVICE_URL
# This boots the container in Webhook mode (so health check binds to port)
# but uses a high-reliability placeholder URL (google.com) to pass DNS verification checks.
echo -e "\n${YELLOW}Deploying to Cloud Run (Step 1/2: Initial Deploy)...${NC}"
gcloud run deploy "$SERVICE_NAME" \
  --source "$SOURCE_DIR" \
  --region "$REGION" \
  --allow-unauthenticated \
  --set-env-vars "TELEGRAM_BOT_TOKEN=$BOT_TOKEN,ADK_SERVER_URL=$ADK_URL,ADK_APP_NAME=$ADK_APP,SERVICE_URL=https://google.com" \
  --project "$GCP_PROJECT"

# 8. Retrieve the actual service URL
echo -e "\n${YELLOW}Retrieving service URL...${NC}"
SERVICE_URL=$(gcloud run services describe "$SERVICE_NAME" --region "$REGION" --project "$GCP_PROJECT" --format 'value(status.url)')
echo -e "${GREEN}✔ Service URL is: $SERVICE_URL${NC}"

# 9. Update service environment variables with the real SERVICE_URL
# This triggers a rolling update and registers the correct webhook with Telegram automatically!
echo -e "\n${YELLOW}Updating configuration with final Webhook URL (Step 2/2)...${NC}"
gcloud run services update "$SERVICE_NAME" \
  --region "$REGION" \
  --set-env-vars "TELEGRAM_BOT_TOKEN=$BOT_TOKEN,ADK_SERVER_URL=$ADK_URL,ADK_APP_NAME=$ADK_APP,SERVICE_URL=$SERVICE_URL" \
  --project "$GCP_PROJECT"

echo -e "\n${GREEN}====================================================${NC}"
echo -e "${GREEN}   Deployment Completed Successfully! 🎉            ${NC}"
echo -e "${GREEN}====================================================${NC}"
echo -e "Service Name:   ${BLUE}$SERVICE_NAME${NC}"
echo -e "Region:         ${BLUE}$REGION${NC}"
echo -e "Active URL:     ${BLUE}$SERVICE_URL${NC}"
echo -e "Webhook Path:   ${BLUE}$SERVICE_URL/<bot-token>${NC}"
echo -e "ADK Backend:    ${BLUE}$ADK_URL${NC}"
echo -e "ADK App Name:   ${BLUE}$ADK_APP${NC}"
echo -e "${GREEN}====================================================${NC}"
echo "Your Telegram Bot has been configured to use webhooks."
echo "Any message sent to your bot will now trigger this Cloud Run instance."

اسکریپت دابل-دپلوی (deploy.sh)

هنگام استقرار در Google Cloud Run، ربات باید URL خود (SERVICE_URL) را در محیط خود مشخص کند تا بتواند آن را به عنوان هدف وب‌هوک با تلگرام ثبت کند. برای حل این وابستگی دایره‌ای (URL تا زمان استقرار ناشناخته است، اما سرویس برای بوت شدن بدون خرابی در بررسی سلامت به URL نیاز دارد)، deploy.sh یک استقرار دو مرحله‌ای انجام می‌دهد:

  1. مرحله ۱: استقرار اولیه : کانتینر را با یک DNS جایگزین ( https://google.com ) بوت می‌کند تا سرویس با موفقیت راه‌اندازی شود، به پورت محلی متصل شود و بررسی‌های اولیه سلامت Cloud Run را پشت سر بگذارد.
  2. مرحله ۲: دریافت URL : به صورت برنامه‌نویسی‌شده، نقطه پایانی Cloud Run تازه ایجاد شده را با استفاده gcloud run services describe استخراج می‌کند.
  3. مرحله ۳: به‌روزرسانی پیکربندی : متغیرهای محیطی را با URL سرویس زنده واقعی به‌روزرسانی می‌کند. این کار باعث به‌روزرسانی روان و بی‌نقص در Cloud Run می‌شود و با خیال راحت هدف وب‌هوک صحیح را با API تلگرام ثبت می‌کند.

استقرار در Cloud Run

اسکریپت استقرار، آدرس URL مربوط به Agent را چاپ می‌کند. آن را در مرورگر خود باز کنید تا به همان رابط کاربری ADK dev که روی Cloud Run اجرا می‌شود، دسترسی پیدا کنید.

cd ~/build-agent-adk-telegram
bash ./telegram-integration/deploy.sh

اگر همه چیز خوب پیش برود، اکنون زمان آن رسیده است که مستقیماً از طریق برنامه چت تلگرام با ربات خود چت کنید، رباتی را که تازه ایجاد کرده‌اید پیدا کنید و با آن تعامل داشته باشید:

What Italian dishes do you have?

یا،

I want something spicy and creamy

به وضعیت ارسال وضعیت "...در حال تایپ کردن" ربات توجه کنید و به زودی، پیام ADK که قبلاً ایجاد کرده‌اید را برمی‌گرداند!

c62fd4016ddd3c9b.png

۷. تبریک می‌گویم!

شما دستیار هوشمند منوی رستوران ما، ADK، مبتنی بر عامل هوش مصنوعی را با تلگرام، از طریق ارتباط با سرور کلاینت HTTP، ساخته، مستقر و کاملاً یکپارچه کرده‌اید و به افراد اجازه می‌دهید منوی مورد علاقه خود را جستجو کرده و رستوران را رزرو کنند.

آنچه آموخته‌اید

  • استقرار و پیکربندی Restaurant Concierge، agent مبتنی بر ADK و MCP Toolbox در Cloud Run
  • نحوه راه اندازی ربات تلگرام با استفاده از BotFather
  • نحوه نوشتن اسکریپت‌های پایتون برای گوش دادن به وب‌هوک تلگرام و تعامل با عامل ADK برای ارسال پرس‌وجو و پاسخ کاربران بر اساس آن
  • نحوه پیاده‌سازی "... typing" در تلگرام برای ارسال سیگنال به پیام‌ها، در حالی که منتظر پاسخ عامل ADK هستیم، به عنوان بازخورد بلادرنگ به کاربران پردازش می‌شود.
  • نحوه استقرار اسکریپت پایتون در فضای ابری و امکان تعامل با آن

تمیز کردن

برای جلوگیری از تحمیل هزینه به حساب Google Cloud خود، منابع ایجاد شده در این codelab را حذف کنید.

gcloud projects delete $GOOGLE_CLOUD_PROJECT

گزینه ۲: حذف منابع تکی

# If you follow from previous A2A Agent Runtime codelab
# Delete the Agent Runtime deployment (skip if not found)
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'),
)
try:
    agent = client.agent_engines.get(name='$RESERVATION_AGENT_RESOURCE_NAME')
    agent.delete(force=True)
    print('Agent Runtime deployment deleted.')
except Exception as e:
    print(f'No agent deployment found or already deleted, skipping. ({e})')
"

# Delete GCS staging bucket (skip if STAGING_BUCKET is not set)
if [ -n "$STAGING_BUCKET" ]; then
  gsutil rm -r gs://$STAGING_BUCKET
else
  echo "STAGING_BUCKET not set, skipping bucket deletion."
fi

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

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