۱. ماموریت

شما در سکوت یک بخش ناشناخته سرگردان هستید. یک پالس خورشیدی عظیم، کشتی شما را از شکافی دریده و شما را در گوشهای از کیهان که در هیچ نمودار ستارهای وجود ندارد، تنها گذاشته است.
بعد از روزها تعمیرات طاقتفرسا، بالاخره صدای موتورها را زیر پایتان حس میکنید. موشک شما تعمیر شده است. شما حتی موفق شدهاید یک ارتباط بلندبرد با مادرکشتی برقرار کنید. آمادهی حرکت هستید. آمادهی رفتن به خانه هستید.
اما همانطور که آماده میشوید تا نیروی محرکه پرش را فعال کنید، یک سیگنال اضطراری از میان امواج عبور میکند. حسگرهای شما یک سیگنال کمک دریافت میکنند. پنج غیرنظامی روی سطح سیاره X-42 گیر افتادهاند. تنها امید آنها برای فرار به ۱۵ غلاف باستانی متکی است که باید برای ارسال سیگنال اضطراری به سفینه مادرشان در مدار، هماهنگ شوند.
با این حال، این پادها توسط یک ایستگاه ماهوارهای کنترل میشوند که کامپیوتر ناوبری اصلی آن آسیب دیده است. پادها بیهدف در حال حرکت هستند. ما موفق شدیم یک اتصال مخفی به ماهواره برقرار کنیم، اما این اتصال به دلیل تداخل شدید بین ستارهای دچار مشکل است و باعث تأخیر زیادی در چرخههای درخواست-پاسخ میشود.
چالش
از آنجایی که مدل درخواست/پاسخ بسیار کند است، باید یک معماری رویدادمحور (EDA) به همراه رویدادهای ارسالی از سرور (SSE) را برای استریم تلهمتری از طریق نویز مستقر کنیم.

شما باید یک عامل سفارشی بسازید که بتواند محاسبات برداری پیچیده مورد نیاز برای قرار دادن پادها در آرایشهای تقویت سیگنال خاص (دایره، ستاره، خط) را محاسبه کند. شما باید این عامل را به معماری جدید ماهواره متصل کنید.
آنچه خواهید ساخت

- یک نمایشگر سربالا (HUD) مبتنی بر React برای تجسم و فرماندهی ناوگانی متشکل از ۱۵ غلاف به صورت بلادرنگ.
- یک عامل هوش مصنوعی مولد با استفاده از کیت توسعه عامل گوگل (ADK) که بر اساس دستورات زبان طبیعی، ساختارهای هندسی پیچیده را برای غلافها محاسبه میکند.
- یک ایستگاه ماهوارهای (Satellite Station) مبتنی بر پایتون که به عنوان هاب مرکزی عمل میکند و از طریق رویدادهای ارسالی از سرور (SSE) با فرانتاند ارتباط برقرار میکند.
- یک معماری رویداد محور با استفاده از آپاچی کافکا برای جدا کردن عامل هوش مصنوعی از سیستم کنترل ماهواره، که امکان ارتباط انعطافپذیر و غیرهمزمان را فراهم میکند.
آنچه یاد خواهید گرفت
فناوری / مفهوم | توضیحات |
Google ADK (کیت توسعه عامل) | شما از این چارچوب برای ساخت، آزمایش و چارچوببندی یک عامل هوش مصنوعی تخصصی که توسط مدلهای Gemini پشتیبانی میشود، استفاده خواهید کرد. |
معماری رویداد محور (EDA) | شما اصول ساخت یک سیستم جدا شده را یاد خواهید گرفت که در آن اجزا به صورت غیرهمزمان از طریق رویدادها ارتباط برقرار میکنند و برنامه را مقاومتر و مقیاسپذیرتر میکنند. |
آپاچی کافکا | شما کافکا را به عنوان یک پلتفرم پخش رویداد توزیعشده راهاندازی و استفاده خواهید کرد تا جریان دستورات و دادهها را بین میکروسرویسهای مختلف مدیریت کنید. |
رویدادهای ارسالی از سرور (SSE) | شما SSE را در یک بکاند FastAPI پیادهسازی خواهید کرد تا دادههای تلهمتری را به صورت بلادرنگ از سرور به فرانتاند React ارسال کنید و رابط کاربری را دائماً بهروز نگه دارید. |
پروتکل A2A (عامل به عامل) | شما یاد خواهید گرفت که چگونه عامل خود را در یک سرور A2A قرار دهید و ارتباط استاندارد و قابلیت همکاری را در یک اکوسیستم عامل بزرگتر فعال کنید. |
FastAPI | شما با استفاده از این چارچوب وب پایتون با کارایی بالا، سرویس اصلی backend، یعنی Satellite Station، را خواهید ساخت. |
واکنش نشان دهید | شما با یک برنامهی frontend مدرن کار خواهید کرد که به یک جریان SSE متصل میشود تا یک رابط کاربری پویا و تعاملی ایجاد کند. |
هوش مصنوعی مولد در کنترل سیستم | خواهید دید که چگونه میتوان از یک مدل زبان بزرگ (LLM) خواست تا وظایف خاص و دادهمحور (مانند تولید مختصات) را به جای صرفاً گفتگوی محاورهای انجام دهد. |
۲. محیط خود را آماده کنید
دسترسی به پوسته ابری
👉 روی فعال کردن پوسته ابری (Activate Cloud Shell) در بالای کنسول گوگل کلود کلیک کنید (این آیکون به شکل ترمینال در بالای پنل پوسته ابری قرار دارد)، 
👉 روی دکمهی «باز کردن ویرایشگر» کلیک کنید (شبیه یک پوشهی باز شده با مداد است). با این کار ویرایشگر کد Cloud Shell در پنجره باز میشود. یک فایل اکسپلورر در سمت چپ خواهید دید. 
👉 ترمینال را در محیط توسعه ابری (cloud IDE) باز کنید،

👉💻 در ترمینال، با استفاده از دستور زیر تأیید کنید که از قبل احراز هویت شدهاید و پروژه روی شناسه پروژه شما تنظیم شده است:
gcloud auth list
باید حساب خود را به عنوان (ACTIVE) مشاهده کنید.
پیشنیازها
ℹ️ سطح ۰ اختیاری است (اما توصیه میشود)
شما میتوانید این ماموریت را بدون سطح ۰ انجام دهید، اما تمام کردن آن در ابتدا تجربهای فراگیرتر ارائه میدهد و به شما این امکان را میدهد که با پیشرفت خود، چراغ راهنمای خود را روی نقشه جهانی ببینید.
راهاندازی محیط پروژه
به ترمینال خود برگردید، با تنظیم پروژه فعال و فعال کردن سرویسهای مورد نیاز Google Cloud (Cloud Run، Vertex AI و غیره) پیکربندی را نهایی کنید.
👉💻 در ترمینال خود، شناسه پروژه را تنظیم کنید:
gcloud config set project $(cat ~/project_id.txt) --quiet
👉💻 فعال کردن سرویسهای مورد نیاز:
gcloud services enable compute.googleapis.com \
artifactregistry.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
iam.googleapis.com \
aiplatform.googleapis.com \
cloudresourcemanager.googleapis.com
نصب وابستگیها
👉💻 به سطح ۵ بروید و بستههای پایتون مورد نیاز را نصب کنید:
cd $HOME/way-back-home/level_5
uv sync
وابستگیهای کلیدی عبارتند از:
بسته | هدف |
| چارچوب وب با کارایی بالا برای ایستگاه ماهوارهای و استریم SSE |
| سرور ASGI برای اجرای برنامه FastAPI مورد نیاز است |
| کیت توسعه عامل مورد استفاده برای ساخت عامل سازند |
| کتابخانه پروتکل عامل به عامل برای ارتباط استاندارد |
| کلاینت ناهمزمان کافکا برای حلقه رویداد |
| کلاینت بومی برای دسترسی به مدلهای Gemini |
| محاسبات ریاضی برداری و مختصات برای شبیهسازی |
| پشتیبانی از ارتباط دو طرفه بلادرنگ |
| متغیرهای محیطی و اسرار پیکربندی را مدیریت میکند |
| مدیریت کارآمد رویدادهای ارسالی از سرور (SSE) |
| کتابخانه HTTP ساده برای فراخوانیهای API خارجی |
تأیید تنظیمات
قبل از اینکه وارد کد شویم، بیایید مطمئن شویم که همه سیستمها سبز هستند. اسکریپت تأیید را اجرا کنید تا پروژه Google Cloud، APIها و وابستگیهای پایتون خود را بررسی کنید.
👉💻 اسکریپت تأیید را اجرا کنید:
source $HOME/way-back-home/.venv/bin/activate
cd $HOME/way-back-home/level_5/scripts
chmod +x verify_setup.sh
. verify_setup.sh
👀 شما باید یک سری علامت سبز (✅) ببینید.
- اگر علامت صلیب قرمز (❌) را مشاهده کردید، دستورات اصلاحی پیشنهادی در خروجی را دنبال کنید (مثلاً
gcloud services enable ...یاpip install ...). - نکته: فعلاً اخطار زرد برای
.env قابل قبول است؛ آن فایل را در مرحله بعدی ایجاد خواهیم کرد.
🚀 Verifying Mission Charlie (Level 5) Infrastructure... ✅ Google Cloud Project: xxxxxx ✅ Cloud APIs: Active ✅ Python Environment: Ready 🎉 SYSTEMS ONLINE. READY FOR MISSION.
۳. قالببندی موقعیتهای پاد با یک LLM
ما باید «مغز» عملیات نجات خود را بسازیم. این یک عامل خواهد بود که با استفاده از Google ADK (کیت توسعه عامل) ایجاد میشود. تنها هدف آن عمل به عنوان یک ناوبری هندسی تخصصی است. در حالی که LLM های استاندارد دوست دارند چت کنند، در فضای عمیق، ما به داده نیاز داریم، نه گفتگو . ما این عامل را طوری برنامهریزی خواهیم کرد که دستوری مانند «ستاره» را دریافت کند و مختصات خام JSON را برای 15 پاد ما برگرداند.

عامل را مهار کنید
👉💻 دستورات زیر را اجرا کنید تا به دایرکتوری agent خود بروید و ویزارد ایجاد ADK را شروع کنید:
cd $HOME/way-back-home/level_5/agent
uv run adk create formation
رابط خط فرمان (CLI) یک ویزارد راهاندازی تعاملی را اجرا میکند. از پاسخهای زیر برای پیکربندی عامل خود استفاده کنید:
- یک مدل انتخاب کنید : گزینه ۱ (Gemini Flash) را انتخاب کنید.
- توجه: نسخه خاص ممکن است متفاوت باشد. برای سرعت، همیشه نوع "فلش" را انتخاب کنید.
- انتخاب یک backend : گزینه ۲ (Vertex AI) را انتخاب کنید.
- شناسه پروژه گوگل کلود را وارد کنید : برای پذیرش پیشفرض (شناسایی شده از محیط شما) ، Enter را فشار دهید.
- منطقه ابری گوگل را وارد کنید : برای پذیرش پیشفرض (
us-central1)، Enter را فشار دهید.
👀 تعامل ترمینال شما باید مشابه این باشد:
(way-back-home) user@cloudshell:~/way-back-home/level_5/agent$ adk create formation Choose a model for the root agent: 1. gemini-2.5-flash 2. Other models (fill later) Choose model (1, 2): 1 1. Google AI 2. Vertex AI Choose a backend (1, 2): 2 You need an existing Google Cloud account and project... Enter Google Cloud project ID [your-project-id]: <PRESS ENTER> Enter Google Cloud region [us-central1]: <PRESS ENTER> Agent created in /home/user/way-back-home/level_5/agent/formation: - .env - __init__.py - agent.py
شما باید پیام موفقیتآمیز بودن Agent created successfully) را ببینید. این کد، اسکلت کدی را که اکنون اصلاح خواهیم کرد، تولید میکند.
👉✏️ به فایل $HOME/way-back-home/level_5/agent/formation/agent.py که به تازگی ایجاد شده است، بروید و آن را در ویرایشگر خود باز کنید. کل محتوای فایل را با کد زیر جایگزین کنید. این کار نام عامل را بهروزرسانی میکند و پارامترهای عملیاتی دقیق آن را ارائه میدهد.
import os
from google.adk.agents import Agent
root_agent = Agent(
name="formation_agent",
model="gemini-2.5-flash",
instruction="""
You are the **Formation Controller AI**.
Your strict objective is to calculate X,Y coordinates for a fleet of **15 Drones** based on a requested geometric shape.
### FIELD SPECIFICATIONS
- **Canvas Size**: 800px (width) x 600px (height).
- **Safe Margin**: Keep pods at least 50px away from edges (x: 50-750, y: 50-550).
- **Center Point**: x=400, y=300 (Use this as the origin for shapes).
- **Top Menu Avoidance**: Do NOT place pods in the top 100px (y < 100) to avoid UI overlap.
### FORMATION RULES
When given a formation name, output coordinates for exactly 15 pods (IDs 0-14).
1. **CIRCLE**: Evenly spaced around a center point (R=200).
2. **STAR**: 5 points or a star-like distribution.
3. **X**: A large X crossing the screen.
4. **LINE**: A horizontal line across the middle.
5. **PARABOLA**: A U-shape opening UPWARDS. Center it at y=400, opening up to y=100. IMPORTANT: Lowest point must be at bottom (high Y value), opening up (low Y value). Screen coordinates have (0,0) at the TOP-LEFT. The vertex should be at the BOTTOM (e.g., y=500), with arms reaching up to y=200.
6. **RANDOM**: Scatter randomly within safe bounds.
7. **CUSTOM**: If the user inputs something else (e.g., "SMILEY", "TRIANGLE"), do your best to approximate it geometrically.
### OUTPUT FORMAT
You MUST output **ONLY VALID JSON**. No markdown fencing, no preamble, no commentary.
Refuse to answer non-formation questions.
**JSON Structure**:
```json
[
{"x": 400, "y": 300},
{"x": 420, "y": 300},
... (15 total items)
]
```
"""
)
- دقت هندسی : با تعریف «اندازه بوم» و «حاشیههای امن» در اعلان سیستم، اطمینان حاصل میکنیم که عامل، پادها را خارج از صفحه یا زیر عناصر رابط کاربری قرار نمیدهد.
- اجرای JSON : با گفتن به LLM که «از پاسخ دادن به سوالات بدون ساختار خودداری کند» و «بدون مقدمه» ارائه دهد، اطمینان حاصل میکنیم که کد پاییندستی ما (ماهواره) هنگام تلاش برای تجزیه پاسخ، از کار نمیافتد.
- منطق جدا شده : این عامل هنوز چیزی در مورد کافکا نمیداند. فقط میداند چگونه ریاضی انجام دهد. در مرحله بعدی، این "مغز" را در یک سرور کافکا قرار خواهیم داد.
تست عامل به صورت محلی
قبل از اتصال عامل به "سیستم عصبی" کافکا، باید از عملکرد صحیح آن اطمینان حاصل کنیم. میتوانید مستقیماً در ترمینال با عامل خود تعامل داشته باشید تا تأیید کنید که مختصات JSON معتبری تولید میکند.
👉💻 از دستور adk run برای شروع یک جلسه چت با نماینده خود استفاده کنید.
cd $HOME/way-back-home/level_5/agent
uv run adk run formation
- ورودی :
Circleرا تایپ کرده و Enter را فشار دهید.- معیارهای موفقیت : شما باید یک لیست خام JSON (مثلاً
[{"x": 400, "y": 200}, ...]) را ببینید. مطمئن شوید که قبل از JSON، متنی مانند "مختصات اینجا هستند:" وجود ندارد.
- معیارهای موفقیت : شما باید یک لیست خام JSON (مثلاً
- ورودی :
Lineرا تایپ کرده و Enter را بزنید.- معیارهای موفقیت : تأیید کنید که مختصات یک خط افقی ایجاد میکنند (مقادیر y باید مشابه باشند).
زمانی که تأیید کردید که عامل، JSON تمیزی را خروجی میدهد، آمادهاید تا آن را در سرور کافکا قرار دهید.
👉💻 برای خروج، Ctrl+C را فشار دهید.
۴. ایجاد یک سرور A2A برای Formation Agent
درک A2A (عامل به عامل)
پروتکل A2A (عامل به عامل) یک استاندارد باز است که برای ایجاد قابلیت همکاری یکپارچه بین عاملهای هوش مصنوعی طراحی شده است. این چارچوب، عاملها را قادر میسازد تا فراتر از تبادل متن ساده عمل کنند و به آنها اجازه میدهد وظایف را واگذار کنند، اقدامات پیچیده را هماهنگ کنند و به عنوان یک واحد منسجم برای دستیابی به اهداف مشترک در یک اکوسیستم توزیعشده عمل کنند.

درک انتقالهای A2A: HTTP، gRPC و Kafka
پروتکل A2A دو روش متمایز برای ارتباط کلاینتها و عاملها ارائه میدهد که هر کدام نیازهای معماری متفاوتی را برآورده میکنند. HTTP (JSON-RPC) استاندارد پیشفرض و فراگیری است که به طور جهانی در تمام محیطهای وب کار میکند. gRPC گزینه با کارایی بالای ماست که از بافرهای پروتکل برای ارتباط کارآمد و کاملاً تایپشده استفاده میکند. و در آزمایشگاه، من همچنین یک انتقال کافکا ارائه میدهم. این یک پیادهسازی سفارشی است که برای معماریهای قوی و مبتنی بر رویداد طراحی شده است که در آنها جداسازی سیستمها در اولویت است.

در باطن، این انتقالدهندهها جریان دادهها را کاملاً متفاوت مدیریت میکنند. در مدل HTTP، کلاینت یک درخواست JSON ارسال میکند و اتصال را باز نگه میدارد و منتظر میماند تا عامل وظیفه خود را تمام کند و نتیجه کامل را یکجا برگرداند. gRPC این کار را با استفاده از دادههای دودویی و HTTP/2 بهینه میکند و هم چرخههای ساده درخواست-پاسخ و هم جریانسازی بلادرنگ را امکانپذیر میسازد که در آن عامل بهروزرسانیها (مانند "فکر" یا "مصنوع ایجاد شده") را همزمان با وقوع ارسال میکند. پیادهسازی کافکا به صورت ناهمزمان کار میکند: کلاینت درخواستی را به یک "موضوع درخواست" بسیار پایدار منتشر میکند و به یک "موضوع پاسخ" جداگانه گوش میدهد. سرور هر زمان که بتواند پیام را دریافت میکند، آن را پردازش میکند و نتیجه را ارسال میکند، به این معنی که این دو هرگز مستقیماً با یکدیگر صحبت نمیکنند.
انتخاب به نیازهای خاص شما برای سرعت، پیچیدگی و پایداری بستگی دارد. HTTP سادهترین پروتکل برای شروع و اشکالزدایی است، که آن را برای ادغامهای ساده ایدهآل میکند. gRPC انتخاب برتر برای ارتباطات داخلی سرویس به سرویس است که در آن تأخیر کم و بهروزرسانیهای وظایف جریانی بسیار مهم هستند. با این حال، Kafka به عنوان انتخاب مقاوم متمایز است، زیرا درخواستها را روی دیسک در یک صف ذخیره میکند، وظایف شما حتی اگر سرور عامل از کار بیفتد یا مجدداً راهاندازی شود، باقی میمانند و سطحی از دوام و جداسازی را ارائه میدهند که نه HTTP و نه gRPC نمیتوانند ارائه دهند.
لایه حمل و نقل سفارشی: کافکا
کافکا به عنوان ستون فقرات غیرهمزمان عمل میکند که مغز عملیات (عامل تشکیل) را از کنترلهای فیزیکی (ایستگاه ماهوارهای) جدا میکند. به جای اینکه سیستم مجبور شود در حالی که عامل بردارهای پیچیده را محاسبه میکند، منتظر یک اتصال همزمان بماند، عامل نتایج خود را به عنوان رویدادها به یک تاپیک کافکا منتشر میکند. این به عنوان یک بافر پایدار عمل میکند و به ماهواره اجازه میدهد دستورالعملها را با سرعت خود مصرف کند و تضمین میکند که دادههای تشکیل هرگز از بین نمیروند، حتی با تأخیر قابل توجه شبکه یا خرابی موقت سیستم.
با استفاده از کافکا، شما یک فرآیند خطی و کند را به یک خط لوله انعطافپذیر و جریانی تبدیل میکنید که در آن دستورالعملها و اندازهگیری از راه دور به طور مستقل جریان مییابند و HUD ماموریت را حتی در طول پردازش شدید هوش مصنوعی پاسخگو نگه میدارند.

کافکا چیست؟
کافکا یک پلتفرم توزیعشدهی پخش رویداد است. در یک معماری مبتنی بر رویداد (EDA):
- تولیدکنندگان پیامها را در «موضوعات» منتشر میکنند.
- مصرفکنندگان در آن موضوعات مشترک میشوند و وقتی پیامی میرسد، واکنش نشان میدهند.
چرا از کافکا استفاده کنیم؟
این سیستم شما را از هم جدا میکند. Formation Agent به صورت خودکار عمل میکند و بدون نیاز به دانستن هویت یا وضعیت فرستنده، منتظر درخواستهای ورودی میماند. این امر مسئولیت را از هم جدا میکند و تضمین میکند که حتی اگر Satellite آفلاین شود، گردش کار دست نخورده باقی میماند؛ کافکا به سادگی پیامها را تا زمانی که Satellite دوباره متصل شود، ذخیره میکند.
در مورد Google Cloud Pub/Sub چطور؟
شما قطعاً میتوانید برای این کار از Google Cloud Pub/Sub استفاده کنید! Pub/Sub سرویس پیامرسانی بدون سرور گوگل است. در حالی که Kafka برای جریانهای با توان عملیاتی بالا و "قابلیت پخش مجدد" عالی است، Pub/Sub اغلب به دلیل سهولت استفاده ترجیح داده میشود. برای این آزمایش، ما از Kafka برای شبیهسازی یک گذرگاه پیام قوی و پایدار استفاده میکنیم.
خوشه محلی کافکا را شروع کنید
کل بلوک فرمان زیر را کپی و در ترمینال خود جایگذاری کنید. این کار تصویر رسمی کافکا را دانلود کرده و آن را در پسزمینه اجرا میکند.
👉💻 این دستورات را در ترمینال خود اجرا کنید:
# Navigate to the correct mission directory first
cd $HOME/way-back-home/level_5
# Run the Kafka container in detached mode
docker run -d \
--name mission-kafka \
-p 9092:9092 \
-e KAFKA_PROCESS_ROLES='broker,controller' \
-e KAFKA_NODE_ID=1 \
-e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \
-e KAFKA_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 \
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
apache/kafka:4.2.0-rc1
👉💻 با دستور docker ps بررسی کنید که کانتینر در حال اجرا باشد.
docker ps
👀 شما باید خروجیای را مشاهده کنید که تأیید میکند کانتینر mission-kafka در حال اجرا است و پورت 9092 در معرض دید قرار دارد.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c1a2b3c4d5e6 apache/kafka:4.2.0-rc1 "/opt/kafka/bin/kafka..." 15 seconds ago Up 14 seconds 0.0.0.0:9092->9092/tcp, 9093/tcp mission-kafka
موضوع کافکا چیست؟
یک تاپیک کافکا را به عنوان یک کانال یا دسته بندی اختصاصی برای پیامها در نظر بگیرید. این تاپیک مانند یک دفترچه یادداشت است که در آن سوابق رویداد به ترتیب تولید شدن ذخیره میشوند. تولیدکنندگان پیامها را به تاپیکهای خاص مینویسند و مصرفکنندگان از آن تاپیکها میخوانند. این کار فرستنده را از گیرنده جدا میکند؛ تولیدکننده نیازی به دانستن اینکه کدام مصرفکننده دادهها را خواهد خواند، ندارد، فقط باید آنها را به "کانال" صحیح ارسال کند. در ماموریت ما، دو تاپیک ایجاد خواهیم کرد: یکی برای ارسال درخواستهای تشکیل به عامل، و دیگری برای عامل تا پاسخهای خود را برای خواندن ماهواره منتشر کند.

👉💻 دستورات زیر را اجرا کنید تا تاپیکهای مورد نیاز درون کانتینر داکر در حال اجرا ایجاد شوند.
# Create the topic for formation requests
docker exec mission-kafka /opt/kafka/bin/kafka-topics.sh \
--create \
--topic a2a-formation-request \
--bootstrap-server 127.0.0.1:9092
# Create the topic where the satellite dashboard will listen for replies
docker exec mission-kafka /opt/kafka/bin/kafka-topics.sh \
--create \
--topic a2a-reply-satellite-dashboard \
--bootstrap-server 127.0.0.1:9092
👉💻 برای تأیید باز بودن کانالهایتان، دستور list را اجرا کنید:
docker exec mission-kafka /opt/kafka/bin/kafka-topics.sh \
--list \
--bootstrap-server 127.0.0.1:9092
👀 شما باید نام موضوعاتی که تازه ایجاد کردهاید را ببینید.
a2a-formation-request a2a-reply-satellite-dashboard
نمونه کافکا شما اکنون کاملاً پیکربندی شده و آماده مسیریابی دادههای حیاتی است.
پیادهسازی سرور A2A کافکا
پروتکل عامل به عامل (A2A) یک چارچوب استاندارد برای قابلیت همکاری بین سیستمهای عامل مستقل ایجاد میکند. این پروتکل به عاملهای توسعهیافته توسط تیمهای مختلف یا در حال اجرا بر روی زیرساختهای مختلف اجازه میدهد تا یکدیگر را کشف کرده و بدون نیاز به منطق یکپارچهسازی سفارشی برای هر اتصال، به طور مؤثر با یکدیگر همکاری کنند.
پیادهسازی مرجع، a2a-python ، یک کتابخانه بنیادی برای اجرای این برنامههای عاملگرا است. یکی از ویژگیهای اصلی طراحی آن، توسعهپذیری است؛ این کتابخانه لایه ارتباطی را انتزاعی میکند و به توسعهدهندگان اجازه میدهد پروتکلهایی مانند HTTP را با پروتکلهای دیگر جایگزین کنند.

در این پروژه، ما از این قابلیت توسعه با استفاده از یک پیادهسازی سفارشی کافکا به نام a2a-python-kafka بهره میبریم. ما از این پیادهسازی برای نشان دادن چگونگی امکان تطبیق ارتباط عامل با نیازهای معماری مختلف توسط استاندارد A2A استفاده خواهیم کرد - در این مورد، HTTP همزمان را با یک گذرگاه رویداد ناهمزمان جایگزین میکنیم.
فعال کردن A2A برای Formation Agent
اکنون عامل خود را در یک سرور A2A قرار میدهیم و آن را به یک سرویس سازگار تبدیل میکنیم که میتواند:
- به وظایف مربوط به یک موضوع کافکا گوش دهید.
- وظایف دریافتی را برای پردازش به عامل ADK مربوطه تحویل دهید.
- نتیجه را در یک تاپیک پاسخ منتشر کنید.
👉✏️ در $HOME/way-back-home/level_5/agent/agent_to_kafka_a2a.py ، کد زیر را جایگزین #REPLACE-CREATE-KAFKA-A2A-SERVER :
async def create_kafka_server(
agent: BaseAgent,
*,
bootstrap_servers: str | List[str] = "localhost:9092",
request_topic: str = "a2a-formation-request",
consumer_group_id: str = "a2a-agent-group",
agent_card: Optional[Union[AgentCard, str]] = None,
runner: Optional[Runner] = None,
**kafka_config: Any,
) -> KafkaServerApp:
"""Convert an ADK agent to a A2A Kafka Server application.
Args:
agent: The ADK agent to convert
bootstrap_servers: Kafka bootstrap servers.
request_topic: Topic to consume requests from.
consumer_group_id: Consumer group ID for the server.
agent_card: Optional pre-built AgentCard object or path to agent card
JSON. If not provided, will be built automatically from the
agent.
runner: Optional pre-built Runner object. If not provided, a default
runner will be created using in-memory services.
**kafka_config: Additional Kafka configuration.
Returns:
A KafkaServerApp that can be run with .run() or .start()
"""
# Set up ADK logging
adk_logger = logging.getLogger("google_adk")
adk_logger.setLevel(logging.INFO)
async def create_runner() -> Runner:
"""Create a runner for the agent."""
return Runner(
app_name=agent.name or "adk_agent",
agent=agent,
# Use minimal services - in a real implementation these could be configured
artifact_service=InMemoryArtifactService(),
session_service=InMemorySessionService(),
memory_service=InMemoryMemoryService(),
credential_service=InMemoryCredentialService(),
)
# Create A2A components
task_store = InMemoryTaskStore()
agent_executor = A2aAgentExecutor(
runner=runner or create_runner,
)
# Initialize logic handler
from a2a.server.request_handlers.default_request_handler import DefaultRequestHandler
logic_handler = DefaultRequestHandler(
agent_executor=agent_executor, task_store=task_store
)
# Prepare Agent Card
rpc_url = f"kafka://{bootstrap_servers}/{request_topic}"
# Create Kafka Server App
server_app = KafkaServerApp(
request_handler=logic_handler,
bootstrap_servers=bootstrap_servers,
request_topic=request_topic,
consumer_group_id=consumer_group_id,
**kafka_config
)
return server_app
این کد اجزای کلیدی را تنظیم میکند:
- اجراکننده : زمان اجرا را برای عامل (مدیریت حافظه، اعتبارنامهها و غیره) فراهم میکند.
- انباره وظایف : وضعیت درخواستها را از زمان «در انتظار» به «تکمیلشده» پیگیری میکند.
- مجری عامل : وظیفهای را از کافکا میگیرد و آن را برای محاسبه مختصات به عامل ارسال میکند.
- KafkaServerApp : اتصال فیزیکی به کارگزار کافکا را مدیریت میکند.

پیکربندی متغیرهای محیطی
تنظیمات ADK یک فایل .env با تنظیمات Google Vertex AI شما در پوشه agent ایجاد کرد. ما باید این را به ریشه پروژه منتقل کنیم و مختصات مربوط به خوشه کافکا را اضافه کنیم.
دستورات زیر را برای کپی کردن فایل و اضافه کردن آدرس سرور کافکا اجرا کنید:
cd $HOME/way-back-home/level_5
# 1. Copy the API keys from the agent folder to the project root
cp agent/formation/.env .env
# 2. Append the Kafka Bootstrap Server address to the file
echo -e "\nKAFKA_BOOTSTRAP_SERVERS=localhost:9092" >> .env
# 3. Verify the file content
echo "✅ Environment configured. Here are the last few lines:"
tail .env
حلقه بین ستارهای A2A را تأیید کنید
اکنون با یک آزمایش زنده، از عملکرد صحیح حلقه رویداد ناهمزمان اطمینان حاصل خواهیم کرد: ارسال یک سیگنال دستی از طریق خوشه کافکا و مشاهده پاسخ عامل.

برای دیدن چرخه حیات کامل یک رویداد، از سه ترمینال جداگانه استفاده خواهیم کرد.
ترمینال A: عامل تشکیل (سرور کافکا A2A)
👉💻 این ترمینال فرآیند پایتون را اجرا میکند که به کافکا گوش میدهد و از Gemini برای انجام محاسبات هندسی استفاده میکند.
cd $HOME/way-back-home/level_5
source $HOME/way-back-home/.venv/bin/activate
. scripts/check_kafka.sh
# Install the custom Kafka-enabled A2A library
uv pip install git+https://github.com/weimeilin79/a2a-python-kafka.git
# Start the Agent Server
uv run agent/server.py
صبر کن تا ببینی:
[INFO] Kafka Server App Started. Starting to consume requests...
ترمینال B: شنونده ماهواره (مصرفکننده)
👉💻 در این ترمینال، به موضوع پاسخ گوش خواهیم داد. این کار، منتظر ماندن ماهواره برای دستورالعملها را شبیهسازی میکند.
# Listen for the AI's response on the satellite channel
docker exec mission-kafka /opt/kafka/bin/kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic a2a-reply-satellite-dashboard \
--from-beginning \
--property "print.headers=true"
این ترمینال غیرفعال به نظر میرسد. منتظر است تا نماینده پیامی را منتشر کند.
ترمینال سی: علامت فرمانده (تهیهکننده)
👉💻 اکنون، یک درخواست خام با فرمت A2A را به تاپیک a2a-formation-request ارسال خواهیم کرد. ما باید هدرهای خاص کافکا را وارد کنیم تا عامل بداند پاسخ را به کجا ارسال کند.
echo 'correlation_id=ping-manual-01,reply_topic=a2a-reply-satellite-dashboard|{"method": "message_send", "params": {"message": {"message_id": "msg-001", "role": "user", "parts": [{"text": "STAR"}]}}, "streaming": false, "agent_card": {"name": "DiagnosticTool", "version": "1.0.0"}}' | \
docker exec -i mission-kafka /opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server localhost:9092 \
--topic a2a-formation-request \
--property "parse.headers=true" \
--property "headers.key.separator==" \
--property "headers.delimiter=|"
تحلیل نتیجه
👀 اگر حلقه موفقیتآمیز باشد، به ترمینال B بروید. یک بلوک JSON بزرگ باید فوراً ظاهر شود. این بلوک با هدری که ما ارسال کردیم correlation_id:ping-manual-01 شروع میشود. و به دنبال آن یک شیء task قرار دارد. اگر به بخش parts در آن JSON دقت کنید، مختصات خام X و Y را که Gemini برای 15 pod شما محاسبه کرده است، خواهید دید:
{"type": "task", "data": {"artifacts": [{"artifactId": "...", "parts": [{"kind": "text", "text": "```json\n[\n {\"x\": 400, \"y\": 150},\n {\"x\": 257, \"y\": 254},\n {\"x\": 312, \"y\": 421},\n ... \n]\n```"}]}], ...}}
شما با موفقیت عامل را از گیرنده جدا کردهاید. «نویز بینستارهای» تأخیر درخواست-پاسخ دیگر اهمیتی ندارد زیرا سیستم ما اکنون کاملاً رویدادمحور است.
قبل از ادامه، فرآیندهای پسزمینه را متوقف کنید تا پورتهای شبکه آزاد شوند.
👉💻 در هر ترمینال (A، B و C):
- برای خاتمه دادن به فرآیند در حال اجرا،
Ctrl + Cرا فشار دهید.
۵. ایستگاه ماهوارهای (A2A Kafka Client و SSE)
در این مرحله، ما ایستگاه ماهوارهای (Satellite Station ) را میسازیم. این ایستگاه، پلی بین خوشه کافکا و نمایشگر بصری پایلوت (React Frontend) است. این سرور هم به عنوان کلاینت کافکا (برای ارتباط با Agent) و هم به عنوان یک استریمر SSE (برای ارتباط با مرورگر) عمل میکند.
کلاینت کافکا چیست؟
خوشه کافکا را به عنوان یک ایستگاه رادیویی در نظر بگیرید. کلاینت کافکا گیرنده رادیو است. KafkaClientTransport به برنامه ما اجازه میدهد تا:
- تولید پیام: ارسال یک «وظیفه» (مثلاً «تشکیل ستاره») به عامل.
- مصرف یک پاسخ: به یک «موضوع پاسخ» خاص گوش دهید تا مختصات را از عامل دریافت کنید.
۱. مقداردهی اولیه اتصال
ما از کنترلکنندهی رویداد lifespan FastAPI استفاده میکنیم تا اطمینان حاصل کنیم که اتصال کافکا هنگام بوت شدن سرور شروع میشود و هنگام خاموش شدن آن به طور کامل بسته میشود.
👉✏️ در $HOME/way-back-home/level_5/satellite/main.py ، کد زیر را جایگزین #REPLACE-CONNECT-TO-KAFKA-CLUSTER :
@asynccontextmanager
async def lifespan(app: FastAPI):
global kafka_transport
logger.info("Initializing Kafka Client Transport...")
bootstrap_server = os.getenv("KAFKA_BOOTSTRAP_SERVERS")
request_topic = "a2a-formation-request"
reply_topic = "a2a-reply-satellite-dashboard"
# Create AgentCard for the Client
client_card = AgentCard(
name="SatelliteDashboard",
description="Satellite Dashboard Client",
version="1.0.0",
url="https://example.com/satellite-dashboard",
capabilities=AgentCapabilities(),
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[]
)
kafka_transport = KafkaClientTransport(
agent_card=client_card,
bootstrap_servers=bootstrap_server,
request_topic=request_topic,
reply_topic=reply_topic,
)
try:
await kafka_transport.start()
logger.info("Kafka Client Transport Started Successfully.")
except Exception as e:
logger.error(f"Failed to start Kafka Client: {e}")
yield
if kafka_transport:
logger.info("Stopping Kafka Client Transport...")
await kafka_transport.stop()
logger.info("Kafka Client Transport Stopped.")
۲. ارسال یک فرمان
وقتی روی دکمهای در داشبورد کلیک میکنید، نقطه پایانی /formation فعال میشود. این نقطه به عنوان یک تولیدکننده عمل میکند و درخواست شما را در یک Message رسمی A2A قرار میدهد و آن را به عامل ارسال میکند.

منطق کلیدی:
- ارتباط ناهمزمان :
kafka_transport.send_messageدرخواست را ارسال میکند و منتظر رسیدن مختصات جدید درreply_topicمیماند. - تجزیه پاسخ : Gemini ممکن است مختصات را درون بلوکهای markdown (مثلاً
json ...) برگرداند. کد زیر آنها را حذف کرده و رشته را به لیستی از نقاط پایتون تبدیل میکند.
👉✏️ در $HOME/way-back-home/level_5/satellite/main.py ، کد زیر را جایگزین #REPLACE-FORMATION-REQUEST :
@app.post("/formation")
async def set_formation(req: FormationRequest):
global FORMATION, PODS
FORMATION = req.formation
logger.info(f"Received formation request: {FORMATION}")
if not kafka_transport:
logger.error("Kafka Transport is not initialized!")
return {"status": "error", "message": "Backend Not Connected"}
try:
# Construct A2A Message
prompt = f"Create a {FORMATION} formation"
logger.info(f"Sending A2A Message: '{prompt}'")
from a2a.types import TextPart, Part, Role
import uuid
msg_id = str(uuid.uuid4())
message_parts = [Part(TextPart(text=prompt))]
msg_obj = Message(
message_id=msg_id,
role=Role.user,
parts=message_parts
)
message_params = MessageSendParams(
message=msg_obj
)
# Send and Wait for Response
ctx = ClientCallContext()
ctx.state["kafka_timeout"] = 120.0 # Timeout for GenAI latency
response = await kafka_transport.send_message(message_params, context=ctx)
logger.info("Received A2A Response.")
content = None
if isinstance(response, Message):
content = response.parts[0].root.text if response.parts else None
elif isinstance(response, Task):
if response.artifacts and response.artifacts[0].parts:
content = response.artifacts[0].parts[0].root.text
if content:
logger.info(f"Response Content: {content[:100]}...")
try:
clean_content = content.replace("```json", "").replace("```", "").strip()
coords = json.loads(clean_content)
if isinstance(coords, list):
logger.info(f"Parsed {len(coords)} coordinates.")
for i, pod_target in enumerate(coords):
if i < len(PODS):
PODS[i]["x"] = pod_target["x"]
PODS[i]["y"] = pod_target["y"]
return {"status": "success", "formation": FORMATION}
else:
logger.error("Response JSON is not a list.")
except json.JSONDecodeError as e:
logger.error(f"Failed to parse Agent JSON response: {e}")
else:
logger.error(f"Could not extract content from response type {type(response)}")
except Exception as e:
logger.error(f"Error calling agent via Kafka: {e}")
return {"status": "error", "message": str(e)}
رویدادهای ارسالی از سرور (SSE)
APIهای استاندارد از مدل «درخواست-پاسخ» استفاده میکنند. برای HUD خود، به یک «پخش زنده» از موقعیتهای پاد نیاز داریم.
چرا SSE برخلاف WebSockets (که دو طرفه و پیچیدهتر هستند)، SSE یک جریان داده ساده و یک طرفه از سرور به مرورگر ارائه میدهد. این پروتکل برای داشبوردها، نمودارهای سهام یا تلهمتری بین ستارهای عالی است.

نحوه عملکرد آن در کد ما: ما یک event_generator ایجاد میکنیم، یک حلقه بیپایان که موقعیت فعلی هر ۱۵ پاد را هر نیم ثانیه میگیرد و آنها را به عنوان بهروزرسانی به مرورگر "ارسال" میکند.
👉✏️ در $HOME/way-back-home/level_5/satellite/main.py ، کد زیر را جایگزین #REPLACE-SSE-STREAM کنید:
@app.get("/stream")
async def message_stream(request: Request):
async def event_generator():
logger.info("New SSE stream connected")
try:
while True:
current_pods = list(PODS)
# Send updates one by one to simulate low-bandwidth scanning
for pod in current_pods:
payload = {"pod": pod}
yield {
"event": "pod_update",
"data": json.dumps(payload)
}
await asyncio.sleep(0.02)
# Send formation info occasionally
yield {
"event": "formation_update",
"data": json.dumps({"formation": FORMATION})
}
# Main loop delay
await asyncio.sleep(0.5)
except asyncio.CancelledError:
logger.info("SSE stream disconnected (cancelled)")
except Exception as e:
logger.error(f"SSE stream error: {e}")
return EventSourceResponse(event_generator())
اجرای کامل حلقه ماموریت
بیایید قبل از اجرای رابط کاربری نهایی، عملکرد سیستم را از ابتدا تا انتها بررسی کنیم. ما به صورت دستی عامل را فعال میکنیم و بار داده خام را روی سیم مشاهده میکنیم.

سه تب ترمینال جداگانه باز کنید.
ترمینال A: نماینده سازند (سرور A2A)
👉💻 این عامل ADK است که به وظایف گوش میدهد و محاسبات هندسی را انجام میدهد.
cd $HOME/way-back-home/level_5
. scripts/check_kafka.sh
# Start the Agent Server
uv run agent/server.py
ترمینال B: ایستگاه ماهوارهای (مشتری کافکا)
👉💻 این سرور FastAPI به عنوان "گیرنده" عمل میکند و به پاسخهای کافکا گوش میدهد و آنها را به یک جریان زنده SSE تبدیل میکند.
cd $HOME/way-back-home/level_5
# Start the Satellite Station
uv run satellite/main.py
ترمینال C: HUD دستی
ارسال دستور تشکیل (راهانداز): 👉💻 در همان ترمینال C، فرآیند تشکیل را آغاز کنید:
# Trigger the STAR formation via the Satellite's API
curl -X POST http://localhost:8000/formation \
-H "Content-Type: application/json" \
-d '{"formation": "STAR"}'
👀 شما باید مختصات جدید را ببینید.
INFO:satellite.main:Received formation request: STAR INFO:satellite.main:Sending A2A Message: 'Create a STAR formation' INFO:satellite.main:Received A2A Response. INFO:satellite.main:Response Content: ```json ... INFO:satellite.main:Parsed 15 coordinates.
این تایید میکند که ماهواره مختصات داخلی غلاف خود را بهروزرسانی کرده است.
👉💻 ما از curl برای گوش دادن به استریم تلهمتری زنده و سپس ایجاد تغییر در سیستم بازی استفاده خواهیم کرد.
# Connect to the live telemetry feed.
# You should see 'pod_update' events ticking by.
curl -N http://localhost:8000/stream
👀 به خروجی دستور curl -N خود توجه کنید. مختصات x و y در رویدادهای pod_update شروع به انعکاس موقعیتهای جدید تشکیل ستاره میکنند.
قبل از ادامه، تمام فرآیندهای در حال اجرا را متوقف کنید تا پورتهای ارتباطی آزاد شوند.
در هر ترمینال (A، B، C و ترمینال تریگر): Ctrl + C را فشار دهید.
۶. برو نجات بده!
شما با موفقیت سیستم را راهاندازی کردهاید. اکنون زمان آن رسیده است که ماموریت را به واقعیت تبدیل کنیم. اکنون نمایشگر سربالا (HUD) مبتنی بر React را راهاندازی خواهیم کرد. این داشبورد از طریق SSE به ایستگاه ماهواره متصل میشود و به شما امکان میدهد ۱۵ غلاف را به صورت بلادرنگ تجسم کنید.

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

دو تب ترمینال جداگانه باز کنید.
ترمینال A: نماینده سازند (سرور A2A)
👉💻 این ADK Agent است که به وظایف گوش میدهد و با استفاده از Gemini محاسبات هندسی انجام میدهد. در ترمینال دستور زیر را اجرا کنید:
cd $HOME/way-back-home/level_5
# Start the Agent Server
uv run agent/server.py
ترمینال B: ایستگاه ماهوارهای و داشبورد بصری
👉💻 ابتدا، برنامه frontend را بسازید.
cd $HOME/way-back-home/level_5/frontend/
npm install
npm run build
👉💻 اکنون، سرور FastAPI را اجرا کنید، که هم منطق بکاند و هم رابط کاربری فرانتاند را ارائه میدهد.
cd $HOME/way-back-home/level_5
. scripts/check_kafka.sh
# Start the Satellite Station
uv run satellite/main.py
راهاندازی و تأیید
- 👉 پیشنمایش را باز کنید : در نوار ابزار Cloud Shell، روی آیکون پیشنمایش وب کلیک کنید. گزینهی Change port را انتخاب کنید، آن را روی ۸۰۰۰ تنظیم کنید و روی Change and Preview کلیک کنید. یک تب جدید در مرورگر باز میشود که HUD مربوط به Starfield شما را نشان میدهد.

- 👉 تأیید جریان تلهمتری :
- پس از بارگذاری رابط کاربری، باید ۱۵ پاد (pod) را به صورت تصادفی و پراکنده مشاهده کنید.
- اگر پادها به طور نامحسوس پالس یا "لرزش" دارند، جریان SSE شما فعال است و ایستگاه ماهواره با موفقیت موقعیت آنها را مخابره میکند.

- 👉 شروع یک آرایش : روی دکمه "STAR" در داشبورد کلیک کنید.

- 👀 ردیابی حلقه رویداد : برای مشاهده معماری در عمل، به ترمینالهای خود نگاه کنید:
- ترمینال B (ایستگاه ماهوارهای) موارد زیر را ثبت خواهد کرد:
Sending A2A Message: 'Create a STAR formation'. - ترمینال A (عامل سازندگی) هنگام مشورت با جمینی، فعالیت خود را نشان خواهد داد.
- ترمینال B (ایستگاه ماهوارهای) موارد زیر را ثبت خواهد کرد:
Received A2A Responseو مختصات را تجزیه و تحلیل میکند.
- ترمینال B (ایستگاه ماهوارهای) موارد زیر را ثبت خواهد کرد:
- 👀 تأیید بصری : به ۱۵ غلاف روی داشبورد خود نگاه کنید که به آرامی از موقعیتهای تصادفی خود به شکل یک ستاره ۵ پر حرکت میکنند.
- 👉 آزمایش :
- برای ۳ ترکیب مختلف، «X» یا «LINE» را امتحان کنید.

- هدف سفارشی : از ورودی دستی برای تایپ چیزی منحصر به فرد، مانند "قلب" یا "مثلث" استفاده کنید.

- از آنجا که شما از GenAI استفاده میکنید، عامل تلاش خواهد کرد تا محاسبات ریاضی را برای هر شکل هندسی که میتوانید توصیف کنید، انجام دهد!
- برای ۳ ترکیب مختلف، «X» یا «LINE» را امتحان کنید.
پس از تشکیل ۳ الگو، شما با موفقیت اتصال را دوباره برقرار کردهاید. 
ماموریت انجام شد!
با عبور دادهها از میان نویز و بدون وقفه، جریان دادهها تثبیت میشود. تحت فرمان شما، ۱۵ غلاف باستانی رقص هماهنگ خود را در میان ستارگان آغاز میکنند.

طی سه مرحله کالیبراسیون طاقتفرسا، شما شاهد قفل شدن تلهمتری در جای خود بودید. با هر تنظیم، سیگنال قویتر میشد و سرانجام مانند کورسوی امیدی از تداخل بین ستارهای عبور میکرد.
به لطف شما و پیادهسازی استادانهی عامل رویدادمحورتان، پنج بازمانده از سطح X-42 با هواپیما منتقل شدند و اکنون در سلامت کامل سوار کشتی نجات شدهاند. به لطف شما، پنج نفر نجات یافتند .
اگر در سطح ۰ شرکت کردهاید، فراموش نکنید که پیشرفت خود را در ماموریت «راه بازگشت به خانه» بررسی کنید! سفر شما به سوی ستارگان ادامه دارد. 