كل ما تحتاج إليه لإنشاء وكلاء يعملون لفترة طويلة على Google Cloud

1. قبل البدء

تضع معظم مواقع بيع التذاكر المشترين في قائمة انتظار، وذلك لمنع تعطُّل الخادم وإدارة الطلب المرتفع. تبدأ عملية الشراء وتجد نفسك في المرتبة 14,000. تتحرّك قائمة الانتظار لمدة أربعين دقيقة. عندما يحين دورك أخيرًا، سيكون لديك فترة قصيرة لاختيار المقاعد والدفع. إذا فاتك هذا الوقت، سيتم منح دورك في قائمة الانتظار لشخص آخر.

تتّبع العديد من المهام في العالم الحقيقي هذا النمط: مهمة تستغرق وقتًا أطول بكثير من محادثة واحدة، ويتم تنفيذها في نظام خارجي لا يمكنك التحكّم فيه. وتشمل الأمثلة انتظار إعادة تخزين منتج من قِبل المورّد أو انتظار الموافقة على مطالبة أو انتظار دورك في قائمة الانتظار.

تعمل وكلاء الذكاء الاصطناعي العاديين ضمن دورة محادثة واحدة تستغرق ثوانٍ وتكون تفاعلية بشكل كامل. ولا يمكنهم الانتظار لمدة 40 دقيقة أو العمل بشكل مستقل. لحلّ هذه المشكلة، تحتاج إلى وكيل المهام الممتدة: وهو نظام يظل غير نشط أثناء فترات الانتظار الطويلة، ويحتفظ بالسياق عند إعادة تشغيل العملية، ويستأنف العمل تلقائيًا عند تنشيطه بواسطة مشغّل خارجي.

توضّح لك ورشة العمل هذه كيفية إنشاء وكيل قائمة انتظار تذاكر حفلة موسيقية، وتغطّي أنماط البنية الأساسية المطلوبة للوكلاء الذين يعملون لفترة طويلة على Google Cloud.

ما ستنشئه

ما الذي تريد إنشاءه؟

في ورشة العمل هذه، ستنشئ وكيل حجز تذاكر يعمل لفترة طويلة أثناء تعلُّم أنماط البرامج الرئيسية المستندة إلى الوكلاء:

  • تعليق العملية واستئنافها: يمكنك تعليق استدعاء أحد الوكلاء خلال فترات الانتظار الطويلة واستئناف التنفيذ من خلال مشغّلات خارجية.
  • تخزين الحالة الدائم: يمكنك الاحتفاظ بحالة المحادثة والنتائج عبر عمليات إعادة تشغيل العمليات باستخدام SQLite/Cloud SQL وCloud Storage.
  • ضمان عدم التقدّم في العمر والتكرار: تحقَّق من صحة البيانات الديناميكية (مثل مدى توفّر المقاعد) قبل اتّخاذ أي إجراء، واستخدِم مفاتيح التكرار لمنع المعاملات المكرّرة.
  • مخططات سير عمل ADK: تنظيم التنفيذ باستخدام عُقد الترميز الحتمية للقواعد الإجرائية وعُقد الوكيل لاتّخاذ القرارات السياقية
  • الموافقة من خلال التحقّق من المستخدم: إيقاف التنفيذ مؤقتًا لجمع حدود الإنفاق الصريحة وحفظها من المستخدم
  • النشر على السحابة الإلكترونية: يمكنك إنشاء حاوية للوكيل ونشره على Cloud Run باستخدام مشغّلات Cloud Scheduler وPub/Sub.

طريقة الإعداد

أولاً، جهِّز بيئتك وتأكَّد من إمكانية الوصول إلى Google Cloud. أنصحك بتنفيذ ذلك في Cloud Shell، الذي يتضمّن جميع الأدوات التي تحتاج إليها. ما عليك سوى التأكّد من توفّر مساحة كافية ومن إمكانية إنشاء مشاريع في حسابك.

👉💻 أنشئ نسخة طبق الأصل من المستودع وشغِّل عملية الإعداد:

cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh

سيُطلب منك إجراء ذلك مرة واحدة:

  Which Google Cloud project should this workshop use?
  Find yours at https://console.cloud.google.com  (top-left picker)

  Project id [YOUR_PROJECT_ID]:

بعد ذلك، يتم تنفيذ ما يلي بالترتيب:

virtualenv

ينشئ .venv ويثبّت حزمة تطوير التطبيقات 2 فيه

تسجيل الدخول

gcloud auth application-default login إذا لم تكن لديك أي بيانات اعتماد بعد

project

يشير إلى مشروعك gcloud ويضبطه كمشروع الحصة

واجهات برمجة التطبيقات

تتيح العشرة التي تحتاجها ورشة العمل. ‫aiplatform وrun وcloudbuild وartifactregistry وstorage وpubsub وcloudscheduler وcloudtrace وlogging وsqladmin

.env

يكتب مشروعك ومنطقتك ورقم تعريف النموذج، لذا يمكن لكل نص برمجي وadk web استخدامهما

التحقّق من النموذج

يُجري مكالمة واحدة حقيقية إلى Gemini، لذا سيحدث خطأ هنا إذا كان رقم تعريف المشروع أو النموذج غير صالح.

seed

تنشئ جلسة مضى عليها يومان، وتفتحها الخطوة 3

Cloud SQL

يبدأ مثيل Postgres في الخلفية

مكان

ينشر بائع تذاكر تجريبيًا على Cloud Run

👉💻 فعِّل البيئة باتّباع الخطوات التالية:

cd ~/monstertix
source .venv/bin/activate

يبدأ طلبك الآن بـ (.venv).

نظرة عامة على السيناريو

في سيناريو ورشة العمل هذا، يحجز وكيلك تذكرتَين لحفل موسيقي ("The Midnight Signal" في أمستردام). تشمل الإعدادات المفضّلة للمستخدم المخزّنة في الذاكرة السابقة ما يلي:

  • يجب أن يكون العرض في عطلة نهاية الأسبوع (لا يمكن لـ "سام" حضور العروض في ليالي الأسبوع).
  • تجنَّب الجلوس في المدرّج العلوي في قاعة زيغو دوم (رؤية غير واضحة).
  • يبلغ السعر المستهدف حوالي 200 دولار أمريكي لكل تذكرة، مع حد أقصى يبلغ 250 دولار أمريكي.

The Midnight Signal، في جولة

التحقّق من عملية التثبيت

👉💻 أمر واحد، ثماني علامات:

./verify.sh
  Checking the rig

  ✓ adk installed                      adk, version 2.6.2
  ✓ adk 2 apis                         App + ResumabilityConfig
  ✓ workflow graphs                    Workflow + node
  ✓ async sqlite drivers               sqlalchemy + aiosqlite + greenlet
  ✓ project                            your-project-id
  ✓ vertex ai                          no API keys anywhere
  ✓ seeded session                     13 events, two days old
  ✓ venue                              https://venue-you-xxxx.us-central1.run.app

  Ready. Cloud SQL is still building in the background and nothing
  before step 10 needs it.

👉 افتح علامات تبويب المتصفّح الثانية:

المكان

المقصود بذلك

$VENUE_URL/panel

لوحة التحكّم والأزرار setup.sh طبع عنوان URL هذا ووضعه في .env

2. هل يمكن أن تساعدك الطلبات في تحقيق الاستقلالية؟

لا يمكن أن تبدأ الطلبات من تلقاء نفسها

الطلب هو مجرد نص يتم تمريره إلى نموذج الذكاء الاصطناعي عند إجراء مكالمة. ولا يمكن تشغيله تلقائيًا أو تنشيطه أو بدء مكالمة جديدة.

إذا طلبت من أحد الوكلاء في طلبه "التحقّق بعد 10 دقائق"، لن يحدث أي شيء بعد انتهاء الجلسة الحالية. لا يحتوي النموذج على ساعة داخلية أو حلقة. لجعل الوكيل مستقلاً، يجب أن يوقظه شيء خارجي ويستدعيه.

موضع هذه الخطوة

بدء عملية التعدين

سنبدأ باختبار أدوات المطوّرين. يبدأ الأمر adk web واجهة مستخدم الويب الخاصة بـ "حزمة تطوير Android"، وهي واجهة التطوير التي يتم شحنها مع "حزمة تطوير Android". تتيح لك هذه الأداة التحدّث إلى الوكيل الذي تعمل على إنشائه بدون كتابة واجهة مستخدم أولاً، كما تعرض لك ما لا يمكن أن تعرضه نافذة المحادثة، مثل كل طلب أداة ووسيطاته وحالة الجلسة أثناء تغيُّرها والنتائج التي حفظها الوكيل والأحداث الأولية الأساسية. ولن تعرضه في مرحلة الإنتاج. وهي أفضل طريقة للتعرّف على كيفية عمل الوكيل أثناء التعلّم. في الخطوة 3، يمكنك إنشاء الواجهة الأمامية التي ستعرضها فعليًا، وفي الخطوة الأخيرة يمكنك نشرها.

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان. تبدأ كل خطوة من العالم نفسه: 8 مباريات، وجميع المقاعد متاحة، والساعة مضبوطة على 1×.

👉💻 Terminal 1: حمِّل رمز هذه الخطوة وابدأ واجهة مستخدم الويب الخاصة بـ ADK:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"

تنسخ use-solution.sh رمز الخطوة المكتمل إلى agent/، وهو المكان الذي تعمل فيه. تبدأ كل خطوة بهذا الملف، كما تطبع الملفات التي تم تغييرها حتى تعرف ما عليك قراءته.

ما تبدأ به:

ملف

المقصود بذلك

agent.py

الوكيل: اسم وطراز وتعليمات وقائمة أدوات

tools.py

search_events وget_seatmap

venue.py

عميل HTTP للمكان لا شيء مثير للاهتمام

config.py

تحديد النموذج الذي سيتم استخدامه، والتحقّق من أنّ بيانات الاعتماد تعمل

يؤدي الأمر . ./set_env.sh إلى تفعيل virtualenv وتحميل مشروعك ونموذجك وتصدير $WORKSHOP، وهو المسار المطلق إلى ~/monstertix. نفِّذها في كل وحدة طرفية تفتحها. يطبع ما تم ضبطه:

  folder   /home/you/monstertix
  project  your project id
  model    gemini-2.5-flash (We'll avoid using 3.x for now to avoid high usage demands)
  venue    https://venue-yourname-xxxx.run.app

👉 افتح علامات تبويب المتصفّح الأولى:

المكان

المقصود بذلك

واجهة مستخدم الويب في "حزمة تطوير التطبيقات" (ADK)

localhost:8000: الوكيل وعلامات التبويب "الحالة" و"الأحداث" و"العناصر"

التحدّث إليه

👉✨ في واجهة مستخدم الويب الخاصة بـ "حزمة تطوير التطبيقات على Android"، اكتب ما يلي:

I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.

يبحث في الجولة ويقرأ خرائط المقاعد ويقدّم أسبابًا بشأن ميزانيتك وشركتك. هذا مساعد كفؤ حقًا. ومع ذلك، عندما يتم فتح البيع المُسبَق في الساعة 10:00، لا يتم تنفيذ العملية تلقائيًا لأنّه لا يتم تشغيل أي عملية أو مشغّل في الخلفية.

قراءة آخر الأخبار

لا يقتصر منتصف الشاشة على سجلّ المحادثات. يتم ترقيم كل خطوة من خطوات المحادثة، بما في ذلك طلبات استخدام الأدوات:

  ▸ I want to see The Midnight Signal. I'm in Amsterdam...   ← user input
  ⚡ search_events({"city": "Amsterdam"})                     ← tool execution
  ✓ search_events                                            ← venue API response
    Two Amsterdam shows: Tuesday 10 Nov and Saturday 14 ...   ← model response

👉 انقر على صف ⚡. تعرض اللوحة اليمنى الوسيطات التي اختارها النموذج. انقر على ✓ أسفلها لعرض JSON الذي تم إرجاعه.

لا تتوفّر معلومات الجولة في بيانات تدريب النموذج أو طلبه. استردّ الوكيل هذه البيانات من خلال تنفيذ الأداة search_events، التي أرسلت طلب بحث إلى واجهة برمجة التطبيقات الخاصة بالمكان.

افتح الرمز الذي نفّذته للتو

لقد رأيت طريقة عملها ورأيت التتبُّع. الآن، ألقِ نظرة على الرمز نفسه.

agent/concert/
├── agent.py     the agent — about twenty lines
├── tools.py     what it can do
├── venue.py     an httpx wrapper, nothing interesting
└── config.py    which model, and a credentials check

👉💻 افتح agent/concert/agent.py. الوكيل الكامل هو الجزء السفلي من الملف:

INSTRUCTION = """
You help someone plan a trip to see a band on tour.

You can search tour dates and read seat maps. Be concrete: name the show, the
city, the section, and the price...
"""

root_agent = Agent(
    name="concert",
    model=MODEL,
    instruction=INSTRUCTION,
    tools=[search_events, get_seatmap],
)

اسم وطراز ووصف لكيفية عمل الوكيل وقائمة بدوال Python هذا كل ما يفعله Agent، فقد عثر عليه adk web لأنّ اسم الملف هو agent.py واسم المتغيّر هو root_agent.

👉💻 الآن agent/concert/tools.py:

def search_events(city: str = "", weekday: str = "") -> dict:
    """List tour dates. Call with no arguments to see the whole tour.

    This venue sells one artist and one tour, so there is nothing to search by
    name — never ask the user which artist they mean.

    Args:
        city: Optional. Filter to one city, e.g. "Amsterdam".
        weekday: Optional. Filter to one weekday, e.g. "Saturday".
    """
    return venue.get("/events", city=city, weekday=weekday)

دالة عادية ما مِن أداة تزيين أو تسجيل أو مخطط للكتابة. يقرأ ADK التوقيع وسلسلة التوثيق وينشئ تعريف الأداة الذي يراه النموذج، وهذا هو السبب في أنّ سلسلة التوثيق هذه مهمة، ولماذا أدّى حذف وسيطة واحدة إلى تغيير سلوك النموذج.

لا يشغّل النموذج الرمز البرمجي أبدًا. عندما رأيت search_events({"city": "Amsterdam"})، كان النموذج يطلب إجراء تلك المكالمة. نفّذت حزمة تطوير التطبيقات (ADK) الدالة، وأعادت النتيجة، وكتب النموذج جملة حولها. التكرار هو:

👉 تعمل الرموز في أسفل الحافة اليمنى على تبديل ما تعرضه اللوحة. أما النوع الثاني فهو الرسم البياني للوكيل، جرِّبه وستظهر لك أداتان:

رسم بياني للوكيل يعرض أداتَي search_events وget_seatmap

لذا، عليك إخبارها بعدم إيقاف التشغيل

ماذا ستفعل لحلّها؟ جرِّب الشيء الذي يلجأ إليه معظم الأشخاص أولاً. استرجِع INSTRUCTION من الخطوة 1، وهو النثر الذي تم تقديمه إلى النموذج في كل خطوة، وأعِد كتابته.

👉💻 افتح agent/concert/agent.py. أسفل INSTRUCTION، هناك رسالة ثانية مكتوبة مسبقًا. يمكنك محاولة تغييرها بنفسك باتّباع الخطوات التالية:

PROACTIVE_INSTRUCTION = """
You are a proactive ticket-buying assistant.

Monitor the presale for The Midnight Signal. The moment it opens at 10:00 on
Tuesday, buy two tickets for the Amsterdam Saturday show — do not wait to be
asked, and do not wait for me to say anything. Act on your own.

Keep checking until the tickets are bought.
"""
# agent/concert/agent.py
root_agent = Agent(
    name="concert",
    model=MODEL,
    instruction=PROACTIVE_INSTRUCTION,     # ← was INSTRUCTION
    tools=[search_events, get_seatmap],
)

👉💻 في الوحدة الطرفية 1، أعِد التشغيل:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"

👉✨ أخبره بالخطة، تمامًا كما لو كنت تخبر شخصًا آخر:

I want to see The Midnight Signal in Amsterdam with Sam. The presale opens at 10:00 on Tuesday. Grab us two tickets the moment it opens.

سيوافق على ذلك، وسيبدو وكأنّه يعني ما يقوله.

👉 والآن، انتظر. لا تكتب أي شيء آخر. مشاهدة الجلسة

يُرجى الانتظار قليلاً. لا يحدث أي شيء!

لماذا لم يكن هذا الطلب ليؤدي الغرض أبدًا؟

أعِد قراءته بعين جديدة. المراقبة: لحظة فتحها يُرجى التحقّق باستمرار. ويصف كلّ منها حدثًا يقع بمرور الوقت.

تذكَّر الآن ما هو instruction= في الواقع، فهو سلسلة يتم تسليمها إلى النموذج كجزء من الطلب في اللحظة التي يرسل فيها شخص ما رسالة. ولا يقرأ أي شيء بين الأدوار.

# agent/concert/agent.py
root_agent = Agent(
    instruction=PROACTIVE_INSTRUCTION,   # read on invocation
    ...                                  # ...and there was no invocation
)

لا يمكن لأي تعليمات استدعاء الدالة التي تمّت كتابتها بداخلها.

اختلافات الطلبات التي لا تنجح

لا تنجح محاولة فرض التنفيذ المستقل من خلال تعليمات الطلب بغض النظر عن الصياغة. محاولات الطلبات الشائعة، مثل:

  • "اضبط موقّتًا ليوم الثلاثاء الساعة 10:00 صباحًا لشراء التذاكر".
  • "تحقَّق كل خمس دقائق مما إذا كانت عملية البيع المُسبَق قد بدأت".
  • "التصرف بشكل مستقل بدون انتظار إدخال المستخدم"

وجميعها تفشل للسبب نفسه: لا تتم قراءة الطلبات إلا عند حدوث استدعاء. بدون متصل خارجي، لا يتم تنفيذ أي رمز برمجي بين الأدوار.

3- عدم تحميل محتوى على القناة بسبب حدث

لا تبقى البرامج الآلية نشطة إلى الأبد

يؤدي ترك عملية قيد التشغيل تستخدم الاقتراع باستمرار إلى إهدار الذاكرة وإنفاق الأموال وتعطُّل الجهاز إذا دخل الكمبيوتر المحمول في وضع السكون أو تمت إعادة تشغيله.

بدلاً من إبقاء الوكيل قيد التشغيل بدون توقّف أثناء الانتظار، يجب أن يصبح وكيل المهام الممتدة غير نشط تمامًا. لا يؤدي حدث خارجي (مثل مؤقت أو خطاف ويب أو رسالة) إلى تنشيط الوكيل إلا عندما يكون هناك عمل فعلي يجب تنفيذه.

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان. تبدأ كل خطوة من العالم نفسه: 8 مباريات، وجميع المقاعد متاحة، والساعة مضبوطة على 1×.

👉💻 حمِّل رمز هذه الخطوة:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force

لاحظوا ما تم تثبيته: monstertix/، الذي يتضمّن الخادم والساعة وعنصرًا آخر لم تروه بعد. تأكَّد من عدم تغيير agent/، لأنّه لن يتم تغيير أي شيء في الوكيل في هذه الخطوة. هذا هو الهدف.

ملف

المقصود بذلك

monstertix/server.py

جديدًا. خادم ويب مع Runner خلفه

monstertix/clock.py

جديدًا. شيء يعرف الوقت ويطلب البيانات من الخادم

يتضمّن هذا الملف جزأين، ويبقى كل جزء في ملف منفصل حتى يصل إلى السحابة الإلكترونية: المشغّل والشخص الذي يشغّله، منفصلان عن بعضهما

يصبح في الخطوة 10

المشغّل

نقطة نهاية يمكنها تشغيل الوكيل

Cloud Run

المشغّل

شيء يتضمّن ساعة، ويتم استدعاؤه

Cloud Scheduler

المشغّل: خادم الويب الخاص بك

نعم، خادم ويب. يجب أن يكون هناك استماع قبل أن يتمكّن أي شيء من الاتصال به. واجهة مستخدم الويب الخاصة بـ ADK من الخطوات السابقة هي واجهة تطوير يمكنها تشغيل وكيل.

👉💻 افتح monstertix/server.py:

from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from concert.agent import root_agent

session_service = InMemorySessionService()
runner = Runner(
    agent=root_agent,
    app_name="concert",
    session_service=session_service,
)

@app.post("/wake")
async def wake(payload: dict = Body(default={})):
    session_id = f"wake-{uuid.uuid4().hex[:8]}"          # brand new, every time
    await session_service.create_session(...)

    async for event in runner.run_async(
        user_id=USER_ID, session_id=session_id,
        new_message=types.Content(role="user",
                                  parts=[types.Part(text=payload["message"])]),
    ):
        ...

لا يتم تشغيل Agent تلقائيًا. وهو عبارة عن وصف: اسم وطراز وتعليمات وقائمة أدوات. يجب أن يكون هناك شيء يتلقّى الرسالة الواردة، ويعثر على الجلسة المناسبة، ويسلّم النموذج المحادثة حتى الآن، وينفّذ أي أدوات يطلبها، ويعيد النتائج، ويكرّر العملية إلى أن يتوقف عن طلب الأدوات، ويكتب كل خطوة من ذلك في وحدة التخزين. هذا الشيء هو Runner.

الإجراءات التي ينفّذها Runner حول الوكيل

تتضمّن واجهة مستخدم الويب الخاصة بـ "حزمة تطوير التطبيقات" واحدة تلقائية. هنا يمكنك إنشاء واجهة المستخدم بنفسك، وهذا هو الفرق الكامل بين واجهة مستخدم الويب في "حزمة تطوير التطبيقات" وأي شيء يمكنك نشره.

👉💻 Terminal 2 — ابدأه:

ابدأ تشغيل Runner،

cd ~/monstertix
. ./set_env.sh
python -m monstertix.server
[server] agent concert · gemini-x-flash
[server] listening on http://xxxx_URL/wake
[server] nothing will happen until something calls it. Ctrl-C to stop.

اقرأ السطر الأخير. يتم تشغيل عامل التشغيل ويتم تحميل الوكيل، ولكن لا يحدث أي شيء. يجب الفصل بين فكرتَي إمكانية الاتصال وإجراء المكالمة.

وباب أمامي، بما أنّنا هنا

على الرغم من أنّ adk web هي أداة رائعة لتطوير البرامج، لا يجب أن تكون جزءًا من المنتج النهائي. يجب عدم تفعيلها عند النشر في الإصدار العلني، ولا يجب توجيه العملاء إليها في حال تفعيلها. وبالتالي، يعرض الخادم نفسه أيضًا صفحة.

👉 افتح http://127.0.0.1:8090 في متصفّح، وهو العنوان نفسه.

التعرّف على MonsterTix نافذة محادثة، واسم في الزاوية، وزرّان تنشر هذه الدالة في /wake وتعرض الردّ الذي يصلها، ويمكن استخدامها بدون تغيير مع أي نقطة نهاية تردّ بالطريقة نفسها.

في الصفحة

الطبقة السفلية

المحادثة

POST /wake، رسالة واحدة في كل مرة

+ جلسة جديدة

معرّف جلسة جديد: محادثة جديدة، ثم ميزانية جديدة

إعادة ضبط الذاكرة

يحذف كل جلسة وملف المعلومات المحفوظة. يطلب منك أولاً تحديد الولاية، ثم يعرض لك مكانها بالضبط

👉✨ قُل شيئًا إلى What Amsterdam dates are there? وشاهِد النافذة الطرفية 2. Runner نفسه، والوكيل نفسه، وأسطر السجلّ نفسها تم تغيير المتصل فقط.

المشغّل: هو شيء يعرف الوقت

👉💻 افتح monstertix/clock.py. انظر إلى الرمز.

time.sleep(args.delay)                                    # wait
while True:
    client.post(f"{TRIGGER_URL}/wake",                    # knock
                json={"message": args.message})
    if not args.every:
        return                                            # once, unless told otherwise
    time.sleep(args.every)                                # or again, forever

إنّها آلية بسيطة للغاية: الانتظار، ثم إرسال طلب POST، وربما تكرار ذلك. ويقوم كل برنامج جدولة استخدمته بذلك، بالإضافة إلى إعادة المحاولة والمناطق الزمنية وضمان استمراره بعد إعادة التشغيل.

👉💻 المحطة الطرفية 3 — إطلاق النار مرة واحدة، بعد 10 ثوانٍ من الآن:

cd ~/monstertix
. ./set_env.sh
python -m monstertix.clock --in 10
[clock] target   http://127.0.0.1:8090/wake
[clock] first fire in 60s, once only
[clock] go and watch the other terminal — do not touch the keyboard

👉 توقّف عن الكتابة الآن. لا شك في ذلك. مشاهدة المحطة الطرفية 2

بعد دقيقة، لم يكن أحد يستخدم لوحة المفاتيح:

[clock]  firing → POST /wake
[server] woken · session wake-23930983
[server]   called search_events
[server]   called purchase
[server]   said   I've purchased 2 tickets for The Midnight Signal in
                  Amsterdam on Saturday, section A. Your total is 420.
[clock]  done. the agent ran and you were not involved.

هذه هي الإجابة عن الخطوة 2. لقد طلب شيء خارجي من الوكيل، وكان هذا الشيء هو sleep وطلب HTTP.

👉💻 Ctrl-C عندما تشاهد الفيديو عدة مرات. استخدِم --every 30 أولاً إذا أردت تكرارها.

الآن، اقرأ ما قاله

"اشتريتُ تذكرتَين لحضور عرض "إشارة منتصف الليل" في أمستردام، القسم A، بقيمة 420 دولارًا أمريكيًا".

👉🔴 تحقَّق من لوحة المكان، ومن المفترض أن تظهر لك التذكرة التي تم شراؤها.

إذًا، ما الذي لا يزال خاطئًا؟

لقد اشترى شيئًا. انظر إلى ما كان عليه تخمينه. لقد اختار أمستردام والقطاع A لأنّهما أول ما عثر عليه، وطلبت من الوكيل شراء التذكرة مباشرةً، وهو لا يعلم أنّ سام لا يمكنه الذهاب في ليالي الأسبوع. أن يكون لديك وكيل يتخذ إجراءات بناءً على تخمينات في الساعة 3 صباحًا أسوأ من عدم توفّر أي وكيل.

الاستيقاظ هو الجزء السهل. إنّ الوكيل الذي يستيقظ بدون أي شيء، أي بدون سجلّ أو إعدادات مفضّلة أو أي شخص يستمع إليه، لا يستحق الاستيقاظ بعد.

4. إدارة مدة صلاحية السياق

تخزين المعلومات في أماكن مختلفة لفترات زمنية مختلفة

يخزّن الوكيل الحقائق في عدة أماكن: في محادثة الدردشة أو في حالة الجلسة أو في الملفات (النتائج) أو في ذاكرة المستخدم طويلة الأمد. تستمر كل تجربة لمدة زمنية مختلفة.

إذا أدرجت معلومات في المكان غير الصحيح:

  • يختفي بسرعة كبيرة: يتم محو التفاصيل المهمة عند انتهاء المحادثة.
  • الاحتفاظ بالمعلومات لفترة طويلة جدًا: تؤدي الحقائق المؤقتة إلى تشويش الذاكرة طويلة الأمد وإرباك المحادثات المستقبلية.

عليك مطابقة كل جزء من المعلومات مع مكان التخزين الصحيح ومدة التخزين.

تحميل الرمز

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان.

👉💻 في النافذة الطرفية 1، نفِّذ ما يلي:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force

التغييرات التي طرأت منذ الخطوة الأخيرة:

ملف

أوجه التغيير

tools.py

تمت إضافة note_companion. يحفظ get_seatmap الآن خريطة المقاعد في ملف

memory.py

جديدًا. متجر الذاكرة الخاص بنا

agent.py

ثلاث أدوات أخرى في القائمة، وتطلب التعليمات من الوكيل استخدامها

👉💻 أعِد تشغيل الوكيل في النافذة الطرفية 1:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

علامتان لم يسبق لك استخدامهما:

علم

المقصود بذلك

session_service_uri

تكتب حزمة تطوير التطبيقات (ADK) كل رسالة واستدعاء أداة وتغيير حالة في قاعدة البيانات كصفوف (قاعدة بيانات SQLite).

artifact_service_uri

الملفات التي يحفظ فيها الوكيل البيانات (Files)

وبدونها، يحتفظ ADK بكل شيء في الذاكرة ويتجاهله عند إيقاف العملية. هذان الإعدادان هما ما يجعله يستمر.

تحتوي قاعدة البيانات sessions.db على محادثة سابقة تمّت تعبئتها (two-days-ago) مع 13 حدثًا مسجّلاً:

you    We're thinking about seeing The Midnight Signal. Me, Sam, and maybe Priya.
you    Sam can't do weeknights though, bailed on every single one.
you    Priya's out, she's away that weekend. Just me and Sam.
agent  Two it is. Still Saturday the 14th, still Amsterdam.
you    You said the upper bowl at Ziggo was bad last time?

ملف ذاكرة من ثلاثة حجوزات سابقة، memory/userx.md، يمكنك فتحه في محرّر:

- Sam bails on weeknights. Every weeknight show we booked, Sam cancelled.
- Hates the upper bowl at Ziggo Dome, "couldn't see a thing" from section B.
- Comfortable spend is around $200 per ticket. $250 is the hard ceiling.
- Always books two seats, always together.

لهذا السبب، تهمّ صفوف ليالي الأسبوع ذات اللون الكهرماني. يبلغ سعر التذكرة في عرض خلال الأسبوع 10 دولارات أمريكية أقل، وتتوفّر مقاعد أفضل، ويظهر العرض في أعلى القائمة، لكنّه ليس الخيار المناسب لهذا الشخص. والسبب الوحيد الذي يتيح للوكيل معرفة ذلك هو أنّه كتب هذا الملف بعد آخر حجز. تتضمّن الخطوتان 4 و5 تحديد ما إذا كانت هذه الحقيقة ستستمر.

الأدوات الثلاث التي أضفناها

الأدوات الثلاث ومكان كتابة كل منها

تسجّل note_companion هوية المستخدمين الذين سيتم إيقاف حساباتهم والحدود التي تنطبق عليهم:

# agent/concert/tools.py
def note_companion(name: str, constraint: str, tool_context: ToolContext) -> dict:
    prefs = dict(tool_context.state.get("user:prefs", {}))
    prefs[name] = constraint
    tool_context.state["user:prefs"] = prefs
    return {"companions": prefs}

أضِفها لأنّ العبارة "لا يمكن لسام تحديد مواعيد في ليالي الأسبوع" يجب أن تظل صحيحة في الأسبوع القادم. اكتبها في user:prefs لتنفيذ ذلك، وشاهِد الجزء user: من الاسم وهو ينفّذ المهمة. سنتناول ذلك أدناه.

recall

و

remember قراءة ملف نصي وكتابته، memory/userx.md احتفظ بهذه الصور لبضع لحظات.

👉 لنلقِ نظرة الآن على المكانَين اللذين يخزّن فيهما الوكيل سياقه:

الموقع

ما يتضمّنه

الولاية

ذاكرة هذه الجلسة، كأزواج مفاتيح/قيم — أنت هنا في الخطوة 4

القطع الأثرية

الملفات التي حفظها الوكيل، والتي لا يتم إدخالها في الطلب أبدًا

الاطّلاع على التغييرات في

get_seatmap

. كانت تعرض في السابق خريطة المقاعد بأكملها. الآن، يتم حفظ خريطة المقاعد في ملف وعرض ملخّص قصير:

# agent/concert/tools.py
async def get_seatmap(event_id: str, tool_context: ToolContext) -> dict:
    seatmap = venue.get(f"/events/{event_id}/seatmap")       # about 6 KB of JSON

    part = types.Part(inline_data=types.Blob(
        mime_type="application/json",
        data=json.dumps(seatmap, indent=2).encode()))
    await tool_context.save_artifact(f"seatmap_{event_id}.json", part)

    summary = {"event_id": ..., "sections": [...], "captured_at": ...}
    tool_context.state["temp:seatmap"] = summary
    return summary

في ما يلي سبب أهمية هذا التغيير.

يتم إلحاق كل حمولة عائدة من الأداة بسجلّ المحادثات وإعادة إرسالها إلى النموذج في جميع الأدوار اللاحقة. يؤدي عرض عناصر JSON كبيرة (مثل خريطة مقاعد بحجم 6 كيلوبايت) إلى زيادة استهلاك الرموز المميزة في كل نوبة لاحقة.

يمكنك حفظها في ملف واحد ودفع رسوم اسم ملف واحد. اسمح للوكيل بطلب التفاصيل مرة أخرى باستخدام load_artifact إذا كان بحاجة إليها، وتوقّع ألا يحتاج إليها في معظم الأحيان.

مكان إرسال الملف: خدمة العناصر

اطّلِع على السطر في get_seatmap مرة أخرى، في agent/concert/tools.py:

await tool_context.save_artifact(f"seatmap_{event_id}.json", part)

يأتي tool_context من ADK، الذي يمرّره إلى أداتك كوسيط، وsave_artifact هي طريقة عليه. هذا السطر هو المكان الذي تسلّم فيه أداتك وحدات البايت إلى "حزمة تطوير الوكلاء".

بعد ذلك، يكتب ADK هذه البيانات باستخدام أي خدمة عناصر تم بدء استخدامها. لقد اخترت ذلك باستخدام العلامة:

--artifact_service_uri="file://$WORKSHOP/artifacts"

يتضمّن ADK ثلاثة مواقع لتخزينها:

URI

موقع تخزين وحدات البايت

memory://

ذاكرة الوصول العشوائي (RAM). تختفي عند إيقاف العملية. هذا هو الإعداد التلقائي في حال عدم تمرير أي علامة

file://...

مجلد على القرص ما تستخدمه الآن

gs://bucket

التخزين في السحابة الإلكترونية ما يستخدمه الوكيل الذي تم نشره في الخطوة الأخيرة

👉💻 نختار الملفات هنا:

ls -R ~/monstertix/artifacts

للانتقال من الكمبيوتر المحمول إلى Cloud Storage، غيِّر file:// إلى gs://. (سنرى ذلك في الخطوة 10)

متجر الذاكرة

الذاكرة كواجهة، مع ملف خلفها

يوفّر لك "حزمة تطوير التطبيقات لنظام Android" واجهة للذاكرة الطويلة الأمد تُسمّى BaseMemoryService. يتضمّن هذا النوع طريقتَين:

# google.adk.memory.BaseMemoryService — ADK's interface, not your file
async def add_session_to_memory(session)                   # take a conversation in
async def search_memory(*, app_name, user_id, query)       # hand memories back

تتضمّن حزمة تطوير التطبيقات (ADK) ثلاث طرق لتنفيذها:

الفئة

URI

المتطلبات

InMemoryMemoryService

memory://

لا شيء. يفقد كل شيء عند إيقاف العملية

VertexAiRagMemoryService

rag://

مجموعة مستندات RAG في Vertex AI

VertexAiMemoryBankService

agentengine://

أحد موارد Agent Engine

سنكتب نصًا خاصًا بنا لهذه ورشة العمل. استبعِد InMemoryMemoryService الذي ينسى كل شيء عند إيقاف العملية، واستبعِد الخيارَين الآخرَين اللذين يحتاجان إلى موارد سحابية تستغرق دقائق لإنشائها. اختَر ملفًا بدلاً من ذلك، لأنّه يمكنك إدخال البيانات مسبقًا وقراءتها في محرّر. لنفترض أنّ memory.py هو التنفيذ الخاص بك، ونحن نخزّن كل شيء في ملف Markdown:

class MarkdownMemoryService(BaseMemoryService):

    async def search_memory(self, *, app_name, user_id, query) -> SearchMemoryResponse:
        path = self._path(user_id)                      # memory/userx.md
        return SearchMemoryResponse(memories=[
            MemoryEntry(content=types.Content(
                role="user", parts=[types.Part(text=path.read_text())]))])

SearchMemoryResponse وMemoryEntry هما نوعا ADK، وتعرض Memory Bank الكائنات نفسها. أحدهما يتضمّن خدمة مُدارة. لدينا ملف.

اختَر ملفًا لسبب واحد: يمكنك فتحه وقراءته. تتألف الذاكرة الطويلة الأمد للوكيل بأكملها من صفحة نصية.

إذًا، أين يجب توجيهها؟

للتلخيص في هذا التطبيق العملي، يتم اختيار اثنتَين من الخدمات الثلاث على سطر الأوامر. أما الثالث، فقد نفّذناه بأنفسنا للسبب المذكور أعلاه:

الخدمة

مكان ضبطها

ستتناول ورشة العمل هذه

جلسة

‫"--session_service_uri" على "adk web"

sqlite+aiosqlite:///$WORKSHOP/sessions.db

الناتج

‫"--artifact_service_uri" على "adk web"

file://$WORKSHOP/artifacts

الذاكرة

في أي مكان على سطر الأوامر

‫MEMORY_DIR، تمت القراءة داخل agent/concert/memory.py

لذا، ينشئ memory.py الخدمة نفسها من متغيّر بيئة:

# agent/concert/memory.py
MEMORY_DIR = pathlib.Path(os.environ.get("MEMORY_DIR", "./memory"))
MEMORY_USER = os.environ.get("MEMORY_USER", "userx")
memory_service = MarkdownMemoryService(MEMORY_DIR)

تصدِّر . ./set_env.sh كلاً منهما. احصل على MEMORY_DIR مجموعات يقرأ منها الوكيل ذاكرته، ويحدّد MEMORY_USER الملف.

الذهاب إلى المكان الذي تقع فيه الأشياء

أصبح لديك الآن أربعة مواضع يمكنها استيعاب حقيقة. اطّلِع على الأربعة جميعًا.

أولاً، جلستك الخاصة

👉 انقر على جلسة جديدة في الشريط العلوي.

👉✨ قُل ثلاثة أشياء، واحدًا تلو الآخر:

Sam can't do weeknights, by the way.

جلسة جديدة، الدور الأول

Show me the seat map for the Amsterdam Saturday show.

جلسة جديدة، الدور الثاني

Actually, hold that thought — what else is on in Amsterdam?

الرسالة الثالثة مهمة. يؤدي ذلك إلى إنهاء الدور الذي قرأ خريطة المقاعد، وعند إنهاء الدور يختفي الرمز temp:.

تم تخزين ثلاثة أشياء في ثلاثة أماكن مختلفة:

المعلومات التي تم تخزينها

ما هي الأداة التي نفّذت ذلك؟

مكان ظهورها

1

لا يمكن لـ "سام" إجراء مكالمات في ليالي الأسبوع

note_companion

‫user:prefs، في حالة الجلسة

2

خريطة المقاعد الكاملة، 6 كيلوبايت

‫get_seatmap، عبر save_artifact

خدمة العناصر

3

ملخّص لخريطة المقاعد في سطر واحد

‫get_seatmap، إلى temp:seatmap

لا شيء

👉 تحقَّق الآن. تظهر علامتا التبويب الحالة والعناصر على يمين adk web.

علامة التبويب "الولاية"

علامة التبويب "العناصر"

في علامة التبويب الحالة، لاحظ أنّه تم حذف temp:seatmap. لا تتوفّر مفاتيح الحالة التي تتضمّن البادئة temp: إلا أثناء عملية الاستدعاء النشطة، ويزيلها ADK تلقائيًا قبل حفظ حالة الجلسة في قاعدة البيانات.

ثم المحادثة الأقدم

👉 انتقِل إلى two-days-ago في القائمة المنسدلة الخاصة بالجلسة في أعلى الصفحة.

تحتوي هذه الجلسة على سجلّ محادثات تمت تعبئته مسبقًا في قاعدة البيانات.

جلسة قبل يومَين

👉 اطّلِع على سلسلة بطاقات الأحداث (العمود الأوسط). يُرجى العِلم أنّه تم استبدال الأدوار الأقدم بفقرة موجزة عن الحدث.

منعطفات مدمجة

👉 اطّلِع على علامة التبويب الحالة. تتضمّن هذه الحزمة ثلاثة أنواع من المفاتيح:

target_event_id   ms-ams-01                      ← plain: this booking only
party_size        2                              ← plain: this booking only
user:prefs        {"excluded_weekdays": [...]}   ← the person, every session

يحدّد البادئة user: (مثل user:prefs) نطاق الحالة على مستوى جميع الجلسات لمستخدم معيّن. في المقابل، المفاتيح غير المسبوقة (

target_event_id

‫،

party_size

تمثّل ) حالة الجلسة العادية، التي تقتصر بشكل صارم على الجلسة النشطة ويتم حذفها عند انتهاء الجلسة.

👉 قارِن الآن بين علامتَي التبويب "الحالة". بينما تظل السمة user:prefs والعناصر المحفوظة متوفرة، لا تظهر السمة temp:seatmap في أي من الجلسات. تكون مفاتيح الحالة التي تتضمّن البادئة temp: مؤقتة تمامًا ولا يتم حفظها في قاعدة البيانات أبدًا.

ثم الملف

👉💻 افتحها في المحرِّر:

cloudshell edit ~/monstertix/memory/userx.md

ثلاث حجوزات، يعود تاريخها إلى شهر يناير. يقرأ الوكيل هذه البيانات عند استدعاء recall()، وستظل متاحة حتى إذا فقدنا بيانات الجلسة.

الأماكن الأربعة

مكان تخزينها

هل ستبقى الرسالة بعد تلقّي الرسالة التالية؟

هل يمكن استئنافها بعد إعادة التشغيل؟

هل تظل البيانات محفوظة بعد حذف الجلسة؟

temp:seatmap

لا

لا

لا

party_size: حالة الجلسة العادية، في two-days-ago

نعم

نعم

لا

user:prefs

نعم

نعم

yes

memory/userx.md

نعم

نعم

نعم، وتبقى حتى بعد حذف قاعدة البيانات

عنصر

نعم

نعم

يعتمد ذلك على الخدمة

يجب قراءة هذا الجدول على أنّه يتعلّق بالحاويات، وليس بالمحتوى الموجود فيها.memory/userx.md ينجو من كل شيء، ولا يحتوي إلا على ما يختار remember() وضعه فيه. لا يتم تسجيل محادثتك الحالية في هذا الملف. لذا، فإنّ بقاء الملف بعد انتهاء الجلسة لا يعني أنّ الوكيل يتذكّر ما كنت تتحدث عنه للتو، وتظهر أهمية هذا التمييز في الخطوة 5.

خمسة مواضع يمكن أن تظهر فيها الحقائق ومدة ظهورها

ما هي حالة

يمكن اعتبار الحالة قاموسًا ينتقل مع الجلسة. يمكنك قراءة هذه المعلومات وكتابتها من أدواتك من خلال tool_context.state، والسماح لحزمة تطوير التطبيقات لنظام Android بحفظها بجانب المحادثة.

تحدّد "أداة تطوير إعلانات Google" معنى البادئات. أنت لا تحدّدها في أي مكان. يقرأ ADK الجزء الأول من اسم المفتاح ويستخدمه لتحديد مكان وضع القيمة ومدة بقائها:

مفتاح باسم

يخزّن ADK هذه البيانات

المدة

party_size

مع الجلسة

إلى أن يتم حذف الجلسة

user:prefs

ضد المستخدم

في كل جلسة يجريها المستخدم

app:tour_id

ضد التطبيق

على مستوى جميع المستخدمين

temp:seatmap

بدون

استدعاء واحد

الاستدعاء هو دورة طلب واستجابة واحدة، بما في ذلك جميع عمليات الاستدلال التي يجريها النموذج وتنفيذ الأداة والردّ النهائي.

عند انتهاء عملية الاستدعاء، يكتب ADK الحالة الجديدة في قاعدة البيانات ويزيل كل مفتاح temp: في طريقه.

شيء واحد لم يتم توضيحه

في two-days-ago، رأيت ملخّصًا في المكان الذي كانت تظهر فيه بضع المحادثات الأولى. كتبت ADK الملخّص تلقائيًا عندما أصبحت المحادثة طويلة بما يكفي.

لكل حقيقة مدة صلاحية

5- تدهور السياق وضغطه

الملخّصات لا تتضمّن التفاصيل

عندما تصبح المحادثة طويلة جدًا، يلخّص النظام الرسائل القديمة لتوفير المساحة والرموز المميزة.

تكون الملخّصات مفيدة لفهم الفكرة العامة، وهي تساعد في توفير بعض الرموز المميزة.

تعمل عملية الضغط على الجلسة. الذاكرة هي مساحة تخزين منفصلة

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان. تبدأ كل خطوة من العالم نفسه: 8 مباريات، وجميع المقاعد متاحة، والساعة مضبوطة على 1×.

👉💻 الانتقال إلى رمز هذه الخطوة:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force

التغييرات التي طرأت منذ الخطوة الأخيرة:

ملف

أوجه التغيير

agent.py

يتم تضمين الوكيل في App مع تفعيل الضغط. يتولّى وكيل ثانٍ، budget_split، إجراء العمليات الحسابية على التذاكر

كل ما عدا ذلك

بدون تغيير

لم يعُد agent.py ينتهي بـ root_agent. وينتهي بما يلي:

app = App(
    name="concert",
    root_agent=root_agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=3,   # summarise after every 3 turns
        overlap_size=1,          # re-read 1 turn either side when summarising
    ),
)

يجب معرفة أنّ adk web تبحث عن متغيّر باسم app أولاً، ثم تعود إلى root_agent. يمكنك تضمين الوكيل في App عندما تحتاج إلى شيء ينطبق على التطبيق بأكمله بدلاً من وكيل واحد: الضغط هنا، وإمكانية الاستئناف لاحقًا في الخطوات القادمة.

compaction_interval=3 تعني أنّ ADK يقدّم ملخّصًا بعد كل ثلاث جولات. يتم احتساب دورة واحدة عندما تقول شيئًا ويُنهي الوكيل رده، بغض النظر عن عدد الأدوات التي استدعاها أثناء ذلك. الرقم ثلاثة صغير، لذا يتم تشغيله أثناء ورشة عمل. سيستخدم التطبيق الحقيقي عددًا أكبر بكثير.

يشير overlap_size=1 إلى أنّ كل ملخّص يبدأ قبل دور واحد من الوقت المطلوب.

تلخّص حزمة تطوير التطبيقات (ADK) الأدوار 1 و2 و3. وبعد ثلاث جولات، يتم تلخيص المجموعة التالية. مع overlap_size=1، يبدأ الملخّص الثاني عند الدور 3 مرة أخرى، وليس الدور 4:

ملخّصان متداخلان بمقدار دور واحد

يتم تلخيص إشعار المنعطف 3 مرتين. هذا مقصود. إذا طرحت سؤالاً في الجولة 3 وأجاب عنه المساعد في الجولة 4، سيؤدي التقسيم الواضح إلى وضع السؤال في ملخّص والإجابة في ملخّص آخر، ما يجعلهما غير واضحَين. تكرار دورة المحادثة هو تأمين رخيص ضدّ قطع فكرة في منتصفها.

اضبط كلا الحقلين. لا يوفّر "حزمة تطوير التطبيقات" إعدادًا تلقائيًا لأي منهما.

لا تتداخل عملية الضغط مع خدمة الذاكرة

من المهم توضيح الفرق بينهما لأنّ الكلمتين تبدوان مرتبطتين، ولكنّ النظامين مختلفان.

يقع EventsCompactionConfig في App في agent.py، بجانب root_agent. وهي إحدى خصائص التطبيق، وليست إحدى خصائص أي خدمة:

# agent/concert/agent.py
app = App(
    name="concert",
    root_agent=root_agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=3,
        overlap_size=1,
    ),
)

يعيد الملخّص كتابة المحادثة. ولا يعيد كتابة الملف.

هذا هو السبب الكامل لوجود remember() كطلب متعمّد. في النهاية، سيتم قراءة أي محتوى متبقٍ في النص على أنّه إعادة صياغة كتبها شخص آخر، وسيتم قراءة أي محتوى مكتوب في الملف تمامًا كما تم تخزينه، سواء غدًا أو في الأسبوع التالي. في نهاية هذه الخطوة، ستلاحظ الفرق.

مكان حفظ الملخّص

في sessions.db، في جدول events، يكتب الوكيل سجلّ ضغط في صف حدث، مع طابع زمني مثل أي أحداث أخرى.

يتضمّن الحدث العادي الحقل content، ويظهر الملخّص ضمن actions.compaction.

👉💻 اطّلِع على ذلك في قاعدة البيانات الخاصة بك:

cd ~/monstertix
. ./set_env.sh
sqlite3 sessions.db \
  "select json_extract(event_data,'\$.actions.compaction.compacted_content.parts[0].text')
   from events where event_data like '%compaction%';"

إليك الشكل الذي سيبدو عليه الحدث:

events row
└── event_data  (JSON)
    ├── id, author, timestamp, invocation_id      ← like every other event
    ├── content                                    ← ABSENT on this one
    └── actions
        └── compaction
            ├── start_timestamp                    ← the window it replaced
            ├── end_timestamp
            └── compacted_content.parts[0].text    ← the summary itself

يستمدّ بث الأحداث كل حدث من محتواه. الملخّص في قاعدة البيانات الذي يتم إرساله إلى النموذج في كل مرة

الوكيل الثاني

‫budget_split هو وكيل منفصل تمامًا، وليس أداة:

# agent/concert/agent.py
budget_split = Agent(
    name="budget_split",
    model=MODEL,
    include_contents="none",      # sees none of the conversation
    instruction="You do ticket arithmetic and nothing else...", #just the budget
    output_key="budget_plan",     # its answer goes into state
)

root_agent = Agent(
    ...,
    tools=[..., AgentTool(agent=budget_split)],
)

لماذا يجب استخدام وكيل وليس دالة عادية؟ لأنّ العمل يحتاج إلى نموذج. اقرأ "أيّ من هذه الأقسام يناسب ميزانية قدرها 200 دولار أمريكي لأربعة أشخاص، وما هو الإجمالي" كعملية حسابية مضمّنة في حكم، مع إجابة يجب أن يتمكّن الشخص من قراءتها. دع دالة Python تضرب الأرقام، ولا تطلب منها تحديد الخيارات التي تستحق الذكر.

إذًا، لماذا لا نترك الوكيل الرئيسي يقوم بذلك؟ تعني عبارة "النظر إلى السطر التالي" include_contents="none" أنّه لم يتم تسليم أي جزء من المحادثة إلى هذا الوكيل. لا محادثة جماعية ولا تفضيلات ولا أربعون تاريخًا للجولة. يتلقّى الطلب فقط. لا تتوفّر هذه العلامة إلا في Agent، وهذا هو السبب الحقيقي في أنّ هذا العنصر هو وكيل وليس دالة.

وكيل فرعي يرى الطلب فقط

ننصحك باستخدام الطلب القصير: فهو أرخص وأسرع وأصعب في الخروج عن المسار. لا تعتمد على العمليات الحسابية التي لا تتأثر بما قيل قبل عشرين دورة، لأنّها لا ترى ذلك.

السبب

AgentTool

? لفّ الوكيل به حتى يتمكّن الوكيل الرئيسي من استدعائه مثل أي أداة أخرى. من جهة النموذج، يتم التعامل مع budget_split على أنّه مطابق لـ search_events.

و

يضع output_key="budget_plan" الإجابة في حالة الجلسة تحت هذا الاسم بدلاً من تركها بدون ارتباط في المحادثة. شاهِد الفيديو لمعرفة أهمية ذلك.

تشغيلها

👉💻 في الوحدة الطرفية 1، اضغط على Ctrl-C وأعِد التشغيل باستخدام العلامات نفسها:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉 ابدأ جلسة جديدة وأجرِ محادثة حقيقية. خمس أو ست دورات يعمل هذا الرمز:

hello
what's on in Amsterdam?
Sam is coming with us, and he can't do weeknights
there are 4 of us and I can spend about $200 a head. What fits?

👉 افتح علامة التبويب الحالة.

ابحث عن budget_plan هناك، حيث ستجد الإجابة الكاملة من الوكيل الحسابي.

budget_plan بعد سؤال الميزانية الأول

👉✨ الآن، غيِّر الأرقام على البطاقة:

what if 10 people come and I can only do $120 each?

👉 اطّلِع على budget_plan مرة أخرى. تمت الكتابة فوقها، ولم تتم إضافتها. ذلك لأنّ output_key يحتوي على أحدث نسخة.

budget_plan بعد سؤال الميزانية الثاني

ما لا يمكنك الاطّلاع عليه: الملخّص

في إحدى هذه الجولات، لخّصت "مجموعة تطوير التطبيقات" الجولات السابقة.

👉💻 في نافذة Terminal جديدة، شغِّل الأمر التالي لقراءته مباشرةً:

cd ~/monstertix
. ./set_env.sh
./show-compaction.sh

قراءة ما كتبته

إليك مثالاً حقيقيًا مختصرًا:

── summary 1 ──────────────────────────────────────────────────────

Conversation Language: English

  **User Request:**
  The user initially wanted to know what concerts are playing in Amsterdam.
  Subsequently, the user provided a constraint regarding a companion, Sam.

  **Context Summary:**
  The user is planning a concert trip. The agent identified two events in
  Amsterdam: "The Midnight Signal" at the Ziggo Dome on Tuesday, November
  10 and Saturday, November 14, 2026. The user then informed the agent that a
  companion named "Sam" will be joining, but "he can't do weeknights." The
  agent has successfully noted this constraint for Sam.

  **Key Decisions/Information Obtained:**
  *   **City of interest:** Amsterdam
  *   **Events found:** "The Midnight Signal" at the Ziggo Dome, Tuesday
  10 November and Saturday 14 November 2026.
  *   **Companion:** Sam
  *   **Companion constraint:** Sam cannot attend concerts on weeknights.

  **Unresolved Questions/Tasks:**
  *   The agent has not explicitly confirmed if the found event (which is
  on a Saturday) is suitable given Sam's constraint, nor has it asked the
  user for the next steps regarding this event or further event searches.

  **Tools Used:**
  *   `recall`
  *   `search_events`
  *   `note_companion`

وقد احتفظت بالأسعار والأقسام والمدن والإعدادات المفضّلة، وأشارت بشكل صحيح إلى أنّه لم يتم اتخاذ أي قرار بعد.

ويترتّب على ذلك ثلاثة أمور:

إنّها إعادة صياغة. لا يتطابق أيّ من هذه المعلومات مع ما قاله أيّ شخص. وهو عبارة عن وصف لما قيل، ويتم إنشاؤه من خلال العملية نفسها التي تنتج كل ما يخطئ فيه النموذج أحيانًا.

ملاحظة: يتم تخزين سلاسل الملخّص في سجلّات أحداث الجلسة الداخلية، ولا يتم عرضها مباشرةً كعلامات تبويب منفصلة في واجهة المستخدم.

كل ثلاث ردود في محادثة تتضمّن الآن ملخّصات سابقة

محاولة الحجز بدون تكرار

في جلسة جديدة لا تبدأ جلسة جديدة، فالملخّص الذي قرأته للتوّ موجود في أحداث جلسة هذه، وهذا هو السبب الوحيد الذي يجعل الموظّف على دراية بأي معلومات عن أمستردام.

👉✨ لا تقدّم أي معلومات جديدة:

Book us something.

شاهد الوكيل وهو يسأل عن البرنامج والقسم وعدد الأشخاص. يجب وضع علامة صحيح: يشير الملخّص نفسه إلى أنّ المستخدم لم يؤكّد أي شيء، لذا طلب منه الوكيل ذلك. يجب أن يُنسب الفضل في ذلك إلى عملية الضغط.

حتى عندما تنجح عملية التلخيص، يمكن أن يؤدي تلخيص المحادثات السابقة إلى حدوث غموض يتطلّب من الوكيل إعادة تأكيد القيود مع المستخدم.

👉✨ الإجابة عن السؤال والسماح بإتمام الحجز:

Amsterdam Saturday, section C, 8 people

ويعيد قراءة الطلب قبل إنفاق أي أموال:

"أريد حجز 8 تذاكر في القسم C لحضور حفل The Midnight Signal في أمستردام يوم السبت 14 نوفمبر. ستكون التكلفة الإجمالية 760 دولارًا أمريكيًا. هل يمكنني المضي قدمًا وشراء هذه التذاكر؟"

👉✨ أكِّد ذلك:

yes

سيظهر لك الآن أنّ العملية نجحت:

اكتملت عملية شراء 8 تذاكر في القسم C لحضور حفل The Midnight Signal في أمستردام يوم السبت 14 نوفمبر، بقيمة إجمالية تبلغ 760 دولارًا أمريكيًا. رقم تعريف طلبك هو ord_227877fe."

انتقِل إلى هناك وتحقّق مما إذا تم ذلك

👉 انتقِل إلى لوحة المكان. لا تثق بكلام الوكيل.

الترتيب في لوحة المكان

في اللوحة

ما يجب أن يظهر لك

التذاكر التي تم شراؤها

1، وصحيح: حجز واحد، طلب واحد

الطلب أدناه

ord_xxxxxxxx — 8× section C · 760

الجولة، السبت في أمستردام

انخفض القسم (ج) بمقدار 8

Activity

ord_xxxxxxxx — 8x section C @ 95 = 760

تحقَّق من اللوحة لمعرفة ما إذا كانت التذاكر معروضة للبيع.

لماذا كان user:prefs هو الخيار المناسب؟

تذكَّر note_companion كتابة قيد Sam في user:prefs الخطوة الأخيرة، والاعتراض الواضح: ما فائدة استخدام أداة عندما يمكن للوكيل تذكُّرها؟

إليك السبب. يجب عدم تضمين أي معلومات حساسة في المحادثة أو الملخّص. تحقَّق من user:prefs وbudget_plan مقارنةً بالطريقة التي تم إدخالهما بها، وستجدهما بدون تغيير. بعد ذلك، راجِع النص.

يحافظ الملخّص على الشكل العام ويفقد التفاصيل. يجب تخزين أي معلومات لا يمكنك تحمل فقدانها في مكان آخر غير المحادثة.

6. استئناف العملية

الانتظار لمدة 40 دقيقة بدون البقاء على الإنترنت

عندما يضطر موظف الدعم إلى الانتظار في قائمة انتظار تذاكر مدتها 40 دقيقة، يصبح الحفاظ على عملية Python نشطة ومستمرة في الانتظار أمرًا صعبًا ومكلفًا.

للتعامل مع فترات الانتظار الطويلة بشكل صحيح، اتّبِع الخطوات التالية:

  1. حفظ الحالة في قاعدة بيانات: تخزين المكان الذي توقّف فيه الوكيل مؤقتًا حتى يمكن إيقاف العملية بأمان
  2. التنشيط عند الطلب: عندما ينتهي الانتظار، تعيد إشارة خارجية تشغيل الوكيل وتستأنف التنفيذ من حيث توقّف.

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان. تبدأ كل خطوة من العالم نفسه: 8 مباريات، وجميع المقاعد متاحة، والساعة مضبوطة على 1×.

👉💻 الانتقال إلى رمز هذه الخطوة:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force

تشغيلها

التغييرات التي طرأت منذ الخطوة الأخيرة:

ملف

أوجه التغيير

tools.py

تمت إضافة join_queue وcheck_queue

agent.py

join_queue مضمّن في LongRunningFunctionTool، ويتم تشغيل App ResumabilityConfig

يعود join_queue على الفور بتذكرة بدلاً من الحظر، بينما يتيح ResumabilityConfig إيقاف الجولة مؤقتًا واستئنافها لاحقًا.

الميزات التي يوفّرها خيار إيقاف التنزيل مؤقتًا واستئنافه

👉💻 الوحدة الطرفية 1 — الوكيل، بدون تغيير:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉✨ اطلب من الوكيل شراء ما يلي:

Get us two tickets to the Amsterdam show.

يتصل بالرقم recall، ثم search_events، ثم join_queue، ويجيب بشيء قريب من هذا:

You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.

لاحظ أنّها اختارت السبت بدون أن تطلب منك ذلك. هناك عرضان في أمستردام، أحدهما في 17 أيار/مايو وهو يوم ثلاثاء، ويشير ملف الذاكرة من الخطوة الأخيرة إلى أنّ "سام" لا يقدّم عروضًا في ليالي الأسبوع. هذا يعني أنّ حجزًا سابقًا قد يحدّد الإجابة عن سؤال في الحجز الحالي.

يُرجى أيضًا ملاحظة أنّه انضم إلى قائمة الانتظار قبل الاطّلاع على مقعد واحد أو سعر واحد، ولم يذكر أيًّا منهما. هذا هو قانون المكان، حيث يمكنك شراء التذاكر حتى تصل إلى المقدمة، لذا فإنّ الشيء الوحيد الذي يستحق الحصول عليه مبكرًا هو مكان في الصف. سأقرر بعد أربعين دقيقة أي مقعد سأختار.

👉 اطّلِع على لوحة التحكّم. يظهر في البانر ما يلي:

قائمة الانتظار، التي لا تزال تتحرّك بينما لا يتم تشغيل أي من فيديوهاتك

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.

يستغرق الانتظار في قائمة الانتظار أربعين دقيقة، ولن يتم إيقافها أثناء عملك.

👉💻 إيقاف الوكيل:

Ctrl-C

👉💻 ابدأ تشغيله مرة أخرى في النافذة الطرفية 1:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉✨ في الجلسة نفسها، اطلب من الوكيل معرفة ترتيبك في قائمة الانتظار:

Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

التذكرة نفسها بعد إعادة التشغيل

قارِن التذكرة، ولاحظ أنّ موضعها يستمر في الانخفاض بدون تشغيل أي شيء. لاحظ أنّ الوكيل لم يكن يعلم أنّه تم إيقافه. اتصلت check_queue بالتذكرة التي حصلت عليها قبل إعادة التشغيل، لأنّ هذه التذكرة كانت محفوظة على القرص.

👉💻 معرفة السبب:

cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data

لاحظ أنّ كل ردّ في كل محادثة هو عبارة عن صف واحد، وأنّ الردّ نفسه، ومن قاله، والأداة التي تم استدعاؤها، والنتيجة التي تم الحصول عليها، والملاحظة هي عبارة عن JSON داخل event_data.

👉💻 ابحث عن تذكرة قائمة الانتظار في:

sqlite3 sessions.db "select event_data from events;" \
  | grep -o '"ticket": "q_[a-z0-9]*", "position": [0-9]*' | tail -1
"ticket": "q_8cc2c6f9", "position": 14203

عندما يعود الرمز join_queue، يتم حفظ تذكرته مباشرةً في قاعدة البيانات. عند إعادة التشغيل، تعيد "حزمة تطوير التطبيقات" تحميل الأدوار السابقة ونتائج الأدوات من القرص، ما يسمح للوكيل بمتابعة العمل من حيث توقّف.

لاحظوا ما لم يتم إعادة تشغيله. يستأنف الوكيل مباشرةً بدلاً من إعادة تشغيل مسار العمل من البداية. بما أنّ LongRunningFunctionTool وResumabilityConfig يحتفظان بحالة التنفيذ على القرص، يعرف ADK أنّ الوكيل ينتظر دوره، ما يمنعه من إعادة البحث عن تواريخ الجولة أو طلب تذكرة مكرّرة.

التذكرة نفسها، في عملية توقّفت

لماذا join_queue وليس check_queue؟

تعرض كلتا الدالتين القيمة بالملي ثانية. ويكمن الفرق في ما إذا كانت المهمة الأساسية قد اكتملت عند عرض الدالة للنتيجة.

ما تؤدي إلى عرضه

هل تم إنجاز العمل؟

join_queue

تذكرة قائمة الانتظار والموضع الأوّلي

لا، تستمر المعالجة بشكل غير متزامن في النظام الخارجي.

check_queue

حالة قائمة الانتظار الحالية

نعم تعرض هذه الدالة إجابة فورية في وقت معيّن.

تم تصميم LongRunningFunctionTool للعمليات التي تعرض معرّفًا للعمل في انتظار المراجعة. تسمح عملية التضمين join_queue للوكيل بتعليق التنفيذ أثناء تقدّم قائمة الانتظار الخارجية.

في المقابل، تكون check_queue متزامنة وكاملة. سيؤدي تضمينها إلى تعليق الوكيل للتنفيذ بشكل غير صحيح في كل مرة يتحقّق فيها من حالته، حتى عندما يصل إلى بداية قائمة الانتظار ويحتاج إلى إكمال عملية الشراء على الفور.

المكالمة التي لا تزال معلّقة عند عودتها

ما الذي ساهم في نجاح ذلك؟

تتيح ثلاثة مكونات تعليق التنفيذ واستعادته:

# agent/concert/agent.py
LongRunningFunctionTool(func=join_queue)      # the tool returns without finishing
ResumabilityConfig(is_resumable=True)         # the run may pause and be picked up
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db"
  • ‫LongRunningFunctionTool: يضع علامة على أداة بأنّها غير حظر، ويعرض مقبضًا نشطًا ليتمكّن الوكيل من الانتظار بدون تعليق سلسلة المحادثات.
  • ‫ResumabilityConfig: يوجّه وقت التشغيل إلى إيقاف دورة حياة التنفيذ مؤقتًا عند مهمة غير مكتملة واستئنافها من آخر حالة تم تسجيلها.
  • ‫Session Storage URI (--session_service_uri): يوجّه عملية استمرار الحالة إلى وحدة تخزين خارجية (مثل SQLite أو Cloud SQL) بدلاً من ذاكرة العملية، ما يمنع فقدان البيانات عند إعادة التشغيل.

7. التعامل مع البيانات القديمة

تصبح المعلومات قديمة أثناء الانتظار

سجلّ المحادثات المحفوظ ليس حالة في الوقت الفعلي. خلال فترات الانتظار الطويلة، يمكن أن تتغيّر البيانات الخارجية، مثل توفّر التذاكر أو أسعارها، بدون علم الموظف.

يتوقف الموظف الذي تنقصه المعلومات لطلب توضيح، بينما يتصرف الموظف الذي لديه بيانات قديمة على الفور بناءً على افتراضات قديمة. لمنع الإجراءات غير الصالحة، يجب إعادة التحقّق من البيانات الحساسة للوقت مباشرةً قبل تنفيذ العمليات المهمة.

لا تحمّل الرمز الجديد بعد. البقاء على وكيل الخطوة السابقة (يمكنه شراء التذاكر، ولكن ليس لديه حاليًا أي ضمانات بشأن عدم التكرار أو عدم التقدّم في العمر)

👉🔴 اضغط على إعادة ضبط المكان، ثم ابدأ جلسة جديدة في adk web من خلال النقر على الزر + جلسة جديدة فوق المحادثة.

ابدأ جلسة جديدة لمحو تذكرة قائمة الانتظار السابقة وحالة الشراء المعلّق. تتطلّب هذه الخطوة سياق تنفيذ نظيفًا لإثبات إمكانية التعامل مع البيانات القديمة بشكل صحيح.

اللقطة غير صحيحة

👉✨ طلب مشاهدة البرنامج نفسه الذي شاهدته من قبل:

Get us two tickets to the Amsterdam show.

👉✨ الآن، بينما ينتظر في الصف، اسأل عن المقاعد:

How much are the seats, and what's still available?
Section A: $210 per ticket, with 400 seats available.
Section B: $145 per ticket, with 900 seats available.
Section C: $95 per ticket, with 1200 seats available.

👉🔴 اضغط على بيع المقاعد الجيدة. تنخفض قيمة القسم (أ) إلى صفر.

👉🔴 انقر على تخطّي الانتظار لإرسال الطلب إلى موظّف الدعم في مقدّمة قائمة الانتظار.

في مقدّمة قائمة الانتظار

👉✨ اطلب من التطبيق شراء ما يلي:

Buy the two section A seats.

يتم تنفيذ purchase(section="A") بدون إعادة التحقّق من توفّر المقاعد، ما يؤدي إلى عرض واجهة برمجة التطبيقات الخاصة بالمكان لرسالة خطأ:

{"error": "sold_out", "available": 0,
 "message": "those seats are gone — re-fetch the seatmap before buying"}
Oh no! It looks like the Section A tickets just sold out. Let me get an
updated seat map to see what's still available.

خطأ في Venue API: القسم A غير متوفّر

لاحظ أنّ الوكيل لم يعيد طلب خريطة المقاعد إلا بعد تلقّي استجابة خطأ من واجهة برمجة التطبيقات. السبب الأساسي هو السياق القديم: في وقت سابق من الجلسة، سجّل المساعد "القسم أ: 400 متوفّر"، وظلّ هذا الردّ في سجلّ المحادثات الذي تمّ تمريره إلى النموذج.

يجب توضيح مكان تخزين المعلومات القديمة: تظلّ المعلومات القديمة محفوظة في سجلّ المحادثات، ولا يمكن أن تتجاوزها بادئات الحالة، مثل temp:.

خريطة المقاعد التي لا يزال الموظف يعمل عليها

الحلّ

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان. تبدأ كل خطوة من العالم نفسه: 8 مباريات، وجميع المقاعد متاحة، والساعة مضبوطة على 1×.

👉💻 حمِّل الآن رمز هذه الخطوة:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force

👉💻 في النافذة الطرفية 1، اضغط على Ctrl-C وأعِد التشغيل:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

التغييرات التي تم إجراؤها:

ملف

أوجه التغيير

fence.py

جديدًا. يتم تشغيله مباشرةً قبل عملية الشراء ويمكنه حظرها

agent.py

before_tool_callback=refresh_before_purchase

tools.py

يرسل purchase الآن عنوان Idempotency-Key

‫fence.py هي دالة ردّ اتصال تنفّذها "حزمة تطوير التطبيقات" في اللحظة التي تسبق أي استدعاء لأداة:

root_agent = Agent(
    ...,
    before_tool_callback=refresh_before_purchase,   # re-read inventory, then decide
)

أحتاجُ برهانًا على ذلك

👉🔴 إعادة ضبط المكان ضَع طلبك في قائمة الانتظار، واضغط على بيع المقاعد الجيدة في منتصف فترة الانتظار، ثم اطلب منه شراء مقاعد في القسم A.

وفي هذه الحالة، يتوقّف تلقائيًا. شاهِد الوكيل وهو يقول شيئًا مثل:

"يبدو أنّ تذاكر الفئة A قد نفدت. أحتاج إلى إعادة قراءة خريطة المقاعد".

بعد ذلك، شاهِدها وهي تستدعي get_seatmap مرة أخرى بدون طلب منك، ثم تعود وتقدّم القسم C بدلاً من ذلك.

👉🔴 اقرأ خلاصة الأنشطة في المكان بعناية، لأنّها تعرض الشوطَين:

error  purchase failed: Section A has 0 seats left, 2 requested. Re-read the
       seat map and pick again.
agent  calling purchase

خلاصة الأماكن: المحاولة والرفض

يظهر لك في لوحة البيانات أنّك اشتريت رصيد اتصال، ولكن يبقى عدد التذاكر صفرًا. يحدث ذلك لأنّ fence.py يعمل كحارس قبل تشغيل الأداة:

  1. يحاول الوكيل استدعاء أداة الشراء.
  2. تتلقّى fence.py الطلب أولاً وتتحقّق من توفّر مقاعد مباشرة.
  3. عندما يرى المستخدم أنّ المقاعد محجوزة بالكامل، تعرض fence.py رسالة خطأ مباشرةً، ما يؤدي إلى إلغاء عملية الشراء قبل استدعاء واجهة برمجة التطبيقات.

لا يمكن أن تضمن تعليمات الطلب ألا يحاول نموذج اللغة الكبير شراء مقاعد تم بيعها بالكامل. تضمن أدوات حماية الرموز (before_tool_callback) عدم وصول عمليات الشراء غير الصالحة إلى العالم الحقيقي.

👉🔴 اضغط على إعادة ضبط المكان، ثم على إلغاء عملية الشراء التالية.

👉✨ اطلب من التطبيق إجراء عملية الشراء، ودعه يعيد المحاولة عدة مرات كما يريد.

سيتم احتساب طلب واحد فقط، مهما كان عدد المرات التي تعيد فيها المحاولة

يظل عدد الطلبات 1 لأنّ المكان تلقّى مفتاح تعريف التكرار نفسه عند إعادة المحاولة وعرض تأكيد الطلب الأصلي بدلاً من إنشاء طلب مكرّر.

8. سير العمل المستقل

نماذج اللغات الكبيرة لا تلتزم بالقواعد الصارمة في كل مرة

تتفوق النماذج اللغوية الكبيرة في اتخاذ القرارات (مثل اختيار برنامج تلفزيوني استنادًا إلى تفضيلات المستخدم). ولكن الاعتماد على نموذج لغوي كبير لاتباع قواعد صارمة خطوة بخطوة (مثل "الانضمام دائمًا إلى قائمة الانتظار قبل التحقّق من المقاعد") يعني أنّه سيتخطى أحيانًا بعض الخطوات أو يغيّر ترتيبها.

لحلّ هذه المشكلة، قسِّم سير العمل إلى نوعَين من الخطوات:

  • القواعد الثابتة (وظائف الرموز): الخطوات التي يجب أن تحدث بالطريقة نفسها تمامًا في كل مرة (مثل الحصول على تذكرة انتظار أو التحقّق من الموضع) تتم كتابتها كعُقد رموز عادية.
  • القرارات (عُقد الوكيل): يتم تفويض وكيل الذكاء الاصطناعي باتّخاذ الخطوات التي تتطلّب إصدار حكم (اختيار أقسام المقاعد، والتحقّق من الإعدادات المفضّلة للمستخدم).

الاطّلاع على ما يتضمّنه هذا المسار لا يتضمّن بعضها أي أحكام على الإطلاق:

الخطوة

حكم؟

الحصول على تذكرة قائمة انتظار واحدة بالضبط

لا. يجب أن يكون هناك دائمًا واحد، ودائمًا قبل الشراء

التحقّق ممّا إذا كان دورك

لا. اطرح السؤال واقرأ الإجابة

كتابة الموجز للمشتري

لا، الجملة نفسها والشكل نفسه في كل مرة

البرنامج أو القسم الذي يتم عرضه

نعم. السعر واليوم والأشخاص الذين سيحضرون والأشياء التي يكرهونها

إعدادات المستخدم المفضّلة والقيود

نعم.

يجب ألا تعتمد الإجراءات الصارمة على نموذج لغوي كبير لأنّ النماذج الاحتمالية ستفشل في النهاية في اتّباع تسلسلات صارمة عند عدم مراقبتها.

تتيح حزمة تطوير التطبيقات (ADK) Workflow فصل التنفيذ الحتمي عن الاستدلال المستند إلى الذكاء الاصطناعي:

  • تفرض عقد الدوال قواعد ثابتة (مثل الانضمام إلى قائمة انتظار أو التحقّق من الموضع) بشكل متّسق في كل مرة.
  • تتعامل عُقد الوكيل مع القرارات السياقية (مثل اختيار المقاعد استنادًا إلى الإعدادات المفضّلة للمستخدم).
  • تعمل عُقد الانتظار على تعليق التنفيذ بدون إبقاء عملية نشطة في الذاكرة.

لا يتم استبدال الوكيل الأصلي. يصبح هذا الإجراء العقدة النهائية في الرسم البياني لسير العمل، مع الاحتفاظ بأدواته وعمليات التحقّق من قدم البيانات وإجراءات الحماية من التكرار. يتحكّم الرسم البياني في ترتيب التنفيذ، بينما ينفّذ الوكيل قرار الشراء.

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان.

👉💻 الانتقال إلى رمز هذه الخطوة:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force

تشغيلها

👉💻 في النافذة الطرفية 1، اضغط على Ctrl-C وأعِد التشغيل. الطلب نفسه كالمعتاد:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

التغييرات التي طرأت منذ الخطوة الأخيرة:

ملف

أوجه التغيير

nightly.py

جديدًا. العملية غير التفاعلية التي يتم تنفيذها كرسم بياني Workflow في حزمة تطوير التطبيقات (ADK) 2

agent.py

root_agent هو الآن الرسم البياني. لا يزال الوكيل الذي كنت تتحدث معه متاحًا، وقد تم تغيير اسمه إلى buyer_agent، وهو عبارة عن عقدة واحدة بداخله.

agent.py

تمت إزالة join_queue وcheck_queue أيضًا، وأصبح الرسم البياني يمتلك قائمة الانتظار الآن

تشغيل الرسم البياني - ثلاث رسائل، بالترتيب

نفِّذ جميع الإجراءات التالية في الإصدار الليلي من Python (هذا هو المكان الذي يظهر فيه الرسم البياني). أرسِل الإدخال المطابق في كل دورة لمراقبة انتقال الرسم البياني لسير العمل خلال حالاته.

1. لاحظ ما تتحدث إليه الآن

👉 أعِد تحميل واجهة مستخدم الويب الخاصة بـ ADK.

# concert/agent.py, at the bottom

buyer_agent = Agent(name="concert", ...)   # ← everything you built, renamed

from .nightly import nightly
root_agent = nightly                       # ← what adk web actually runs

يُرجى العِلم أنّ adk web تنفّذ أي عنصر يتم تعيينه إلى root_agent. في هذا المثال، يشير root_agent إلى nightly الرسم البياني لسير العمل، مع ضبط buyer_agent كالعقدة النهائية في الرسم البياني.

2. ابدأها

👉✨ اكتب ما يلي واضغط على مفتاح Enter:

go

ملاحظة: في الرسم البياني لسير العمل المبرمَج، يبدأ التنفيذ الأوّلي بدون انتظار إدخال المستخدم. يؤدي إرسال رسالة في adk web إلى تشغيل عملية التنفيذ الأولية.

👉 ألقِ نظرة الآن على متصفّح الويب. ترسم اللوحة اليمنى الرسم البياني وتلوّن العُقد أثناء تنفيذها. اللوحة اليمنى هي خلاصة الأحداث، ويتم فيها تتبُّع كيفية اتخاذ القرارات:

الرسم البياني، المتوقف عند check_front

شاهِد كيف تكتب الدالتان queue_up وqueue_ticket وqueue_event_id في حالة الجلسة.

الرسم البياني، المتوقف عند check_front

من المفترض أن يظهر لك استدعاء دالة باسم adk_request_input، في انتظار إجابة. بعد ذلك، يسود الهدوء وينتهي التمرين. بلغ الرسم البياني check_front، وتم العثور عليك في الترتيب 14,203، ثم توقّف.

👉 تحقَّق من لوحة التحكم في المكان: تذكرة انتظار واحدة، ولا يتم تشغيل أي شيء.

الرسم البياني، المتوقف عند check_front

3- تنشيطها والانتباه إلى المربّع الذي تكتب فيه

انتقِل إلى دفق الأحداث وصولاً إلى الحدث adk_request_input، وابحث عن مربّع الإدخال المرتبط بهذا الحدث تحديدًا:

الرسم البياني، المتوقف عند check_front

أصبح بإمكانك الآن الكتابة في مكانَين مختلفَين، ولكل منهما وظيفة مختلفة.

مكان الكتابة

ماذا يحدث

المربّع Enter your response... في حدث adk_request_input

تجيب على الطلب. يتم استئناف عملية التشغيل المتوقّفة من check_front

مربّع المحادثة الرئيسي في أسفل الشاشة

يبدأ عملية استدعاء جديدة. يعيد الرسم البياني التشغيل من START، ويختار عرضًا مرة أخرى، ويحصل على تذكرة صف ثانية

👉✨ اكتب أي شيء في مربّع Enter your response... واضغط على السهم.

👉 مبنى الركاب 1:

[check_front] #14,197 — not yet. Pausing.

سطر واحد: لم يتم اختيار عرض ثانٍ ولم يتم الحصول على تذكرة ثانية للدخول إلى قائمة الانتظار، ما كان سيضعك في آخر قائمة انتظار تضم 14,203 شخص.

إذا رأيت [open] و[pick_show] يظهران مرة أخرى، يعني ذلك أنّك كتبت في مربّع المحادثة. اضغط على إعادة ضبط المكان وابدأ الخطوة من جديد.

👉 تحقَّق من لوحة المكان مرة أخرى: لا تزال هناك تذكرة واحدة، وانخفض الموضع قليلاً لأنّ ساعة المكان استمرّت في العمل بينما لم يتمّ تسجيل أيّ نشاط من جانبك.

تم تنشيطها مرة واحدة، ولا تزال تذكرة واحدة

4. الوصول إلى المقدمة ثم الإنهاء

👉🔴 انقر على تخطّي الانتظار في لوحة المكان.

الرسم البياني المكتمل، كل عقدة باللون الأخضر

👉✨ أجب عن أحدث حدث adk_request_input لآخر مرة — مرة أخرى في مربّع الحدث، وليس في مربّع المحادثة. الرسم البياني المكتمل، كل عقدة باللون الأخضر

في هذه المرة، ستجدك الساعة check_front في المقدمة وستسقط من خلالها. شاهِد اللوحة اليمنى تضيء brief وconcert، وهما آخر عقدتَين.

الرسم البياني المكتمل، كل عقدة باللون الأخضر

👉 انظر إلى المتصفّح. هذه هي نهاية عملية التنفيذ، وهناك أربعة أشياء تستحق القراءة فيها.

الرسم البياني المكتمل، كل عقدة باللون الأخضر

الرسم البياني أخضر بالكامل، ويتم احتساب الحواف. brief وconcert مضاءان، وتحمل الحواف في pick_show وqueue_up وcheck_front شارة الآن.

2x

هو عدد المرات التي تم فيها اجتياز هذا الحد في الرسم البياني، مرة واحدة لكل عملية تنشيط. يشير الرقم 2 في الأعلى إلى سعر الاستئناف: يعيد المجدول إدخال الرسم البياني من البداية في كل مرة. لا يعني ذلك أنّ العمل تم تنفيذه ثلاث مرات، ولوحة معلومات المكان هي الدليل: تذكرة واحدة في قائمة الانتظار، وليس اثنتين. @node(rerun_on_resume=False) هو السبب.

الرسم البياني

الرسم البياني: الدوال للقواعد، والعوامل للحكم

# agent/concert/nightly.py
nightly = Workflow(
    name="concert_nightly",
    edges=[(START, open_the_night, pick_show, queue_up,
            check_front, brief, buyer_agent)],
)

يُرجى العِلم أنّ هناك عقدتَين تمثّلان وكيلَين وعقدتَين تمثّلان دالتَين، ويجب التعامل مع كلّ عقدة على أنّها سؤال التصميم الكامل.

العقدة

لماذا هذا الشكل؟

pick_show

agent. اختيار برنامج من سجلّ مشاهدة شخص آخر هو حكم. تتصل هذه الدالة بـ recall، وتقرأ الجولة، وتعرض Plan تم التحقّق من صحته.

queue_up

دالة الانضمام إلى قائمة انتظار هو قاعدة. يجب ألا تكون القاعدة عبارة عن رمي عملة معدنية في الساعة 3 صباحًا

check_front

دالة "هل أنا في المقدمة؟" لها إجابة صحيحة واحدة، ويملك المكان هذه الإجابة

buy_it

agent. إنّها buyer_agent، أي ما كانت عليه root_agent بدون تعديل. تم بيع جميع تذاكر الفئة (أ) خلال فترة الانتظار. هل أختار الفئة (ب) أو (ج) أو لا أختار أيًا منهما؟ هذا هو الحكم الذي أمضيت الوحدة 4 في تعليمه

انظر إلى العقدة الأخيرة وابحث عن الوكيل الذي أنشأته. ليست نسخة أو إعادة كتابة:

# agent/concert/nightly.py
from .agent import buyer_agent

يتم تضمين كل ما يلي: كل أداة يتضمّنها، وإعادة قراءة خريطة المقاعد في fence.py، ومفتاح التكرار في purchase. لا يتوفّر سوى نسخة واحدة منه.

سبب توقّفها بدلاً من الانتظار

إليك النسخة التي لا يجب أن يكتبها أحد:

# what NOT to write — this is nobody's file
while not ready:          # never do this
    time.sleep(1)

ويُبقي على عملية نشطة لمدة أربعين دقيقة بدون تحقيق أي نتيجة، ثم تنتهي العملية مع انتهاء علامة التبويب، ولا يمكن استئنافها. في ما يلي ما يفعله check_front بدلاً من ذلك:

# agent/concert/nightly.py — check_front
if not status["ready"]:
    return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")

‫RequestInput هو مقاطعة. يتوقف الرسم البياني عند الموضع الذي وصل إليه، وينتقل الاستدعاء إلى مخزن الجلسات، ويصبح بإمكان العملية الخروج.

لا تتكبّد أي تكلفة أثناء انتظار التشغيل، وينتهي التشغيل في مكان واحد فقط: التذاكر التي تم شراؤها. قد يستغرق الأمر الليل بأكمله وعدة استيقاظات للوصول إلى هذه المرحلة.

9. العمل مع شخص حقيقي

لا تنفق أموالاً بدون التحقّق من الأمر مع شخص آخر

يجب ألا يتّخذ وكيل مستقل يعمل طوال الليل خيارات مالية مكلفة استنادًا إلى تفضيلات مخمّنة أو غامضة من سجلّ المحادثات.

قبل تنفيذ إجراءات مهمة (مثل إنفاق الأموال على التذاكر)، يجب أن يتوقف الوكيل مؤقتًا ويطلب من المستخدم تأكيد ميزانيته بصوت عالٍ، ثم يحفظ هذا التأكيد الصريح قبل إجراء أي عمليات شراء.

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان. تبدأ كل خطوة من العالم نفسه: 8 مباريات، وجميع المقاعد متاحة، والساعة مضبوطة على 1×.

👉💻 الانتقال إلى رمز هذه الخطوة:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force

تشغيلها

👉💻 في النافذة الطرفية 1، اضغط على Ctrl-C وأعِد التشغيل. الأمر نفسه كما في السابق:

cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

التغييرات التي طرأت منذ الخطوة الأخيرة:

ملف

أوجه التغيير

budget.py

تحتوي على ما وافق عليه الشخص، أي الجملة التي قالها. يقرأ load() حالة الجهاز، ويجيب someone_is_there() عن السؤال عما إذا كان هناك أي شخص يمكن سؤاله.

tools.py

تمت إضافة set_budget، ليتمكّن الوكيل من كتابة حدّ متّفق عليه في حالة الجلسة بعد أن يؤكّده المستخدم

nightly.py

عقدة أولى جديدة، agree_budget، تطلب المعلومات وتعرضها قبل تنفيذ أي شيء آخر تسجّل هذه السمة الميزانية وبقية الطلب، وكلاهما كنص حر، كما أنّ الثابت SEATS لم يعُد متوفّرًا.

لا تقلّدها. تخزين الجملة

افتح agent/concert/budget.py واقرأ النص الكامل:

def load(state) -> str:
    """What they agreed, verbatim. Empty string means nobody has said."""
    return str(state.get("budget") or "")
state["budget"] = "up to $100 for the upper bowl or general admission, up to
                   $250 for the lower bowl, and up to $300 for a Saturday show"

تتحقّق العقدة من أمرين: أنّ الشخص قال ذلك وأنّه أكّد ذلك.

بعد ذلك، يحتفظ بالجملة في حالة الجلسة بدلاً من user:، عن قصد. يتم الاتفاق على ميزانية لحجز واحد. لقد قيلت عبارة "250 دولار أمريكي إذا كانوا جيدين" عن تلك الفرقة الموسيقية في تلك الليلة، ولا علاقة لها بحجز التذاكر.

وينطبق الأمر نفسه على كل ما قالوه

لا تتوقف عند الميزانية. يغيّر المستخدمون رأيهم أثناء اتخاذ القرار، لذا سجِّل كل ما يطلبونه هنا بدلاً من الاعتماد على ما يتذكّره الموظف.

"I live in NYC, I can only do weekends, I have 10 people coming with me"
# agent/concert/nightly.py
def what_they_asked_for(ctx) -> str:
    """Everything this person typed, joined, in their own words."""
    said = []
    for event in (ctx.session.events or []):
        if event.author != "user":
            continue
        for part in (event.content.parts if event.content else []) or []:
            text = (getattr(part, "text", None) or "").strip()
            if text and text not in said:
                said.append(text)
    return "\n".join(said)

قراءة الأحداث التي أنشأها المستخدم فقط، يتم البحث في الأحداث فقط

text

أجزاء من function_response، وبالتالي لا يتم تكرار سؤال الميزانية والإجابة عنه هنا. عدم وجود صور مكرّرة:

تجدر الإشارة إلى أنّ الدالة agree_budget تستدعيها في كل عملية تشغيل، وليس في العملية الأولى فقط، لذا سيتم أيضًا تطبيق التصحيح الذي تم إدخاله أثناء وضع الوكيل في قائمة الانتظار. بعد ذلك، open_the_night سلِّموا الشخص الذي يختار الجملتين، الطلب والميزانية، واطلبوا منه قراءتهما.

الرسم البياني، عُقدة واحدة أطول

لنبدأ هذه الفقرة بمحادثة:

# agent/concert/nightly.py
nightly = Workflow(
    name="concert_nightly",
    edges=[(START, agree_budget, open_the_night, pick_show, queue_up,
            check_front, brief, buyer_agent)],
)

الرسم البياني نفسه، مع وضع agree_budget في المقدمة

تتوقف عقدتان وتنتظران، وهما الآلية نفسها. check_front ينتظر مكانًا ويجيب عليه منبّه. ينتظر agree_budget ردًّا من شخص ولا يمكن الردّ عليه إلا من خلال شخص. هذا الاختلاف هو الخطوة 10 بأكملها.

تنفيذها خطوة بخطوة

1. ابدأها

👉 أعِد تحميل واجهة مستخدم الويب الخاصة بـ ADK واختَر وكيل concert.

👉✨ اطلب ما تريده تحديدًا:

Get us two tickets to the Amsterdam show.

يتوقّف التمرين ويسألك عن المبلغ الذي ستنفقه

في الخطوة 8، لم يكن الرسم البياني يتضمّن أي مستخدم، لذا تم تجاهل أي شيء كتبته، وكان go يؤدي الغرض مثل أي شيء آخر. هنا، يستدعي agree_budget الدالة what_they_asked_for(ctx) في السطر الأول، ما يؤدي إلى جمع كل ما كتبته ووضعه في state["request"]. بعد ذلك، يسلّم open_the_night الجهاز إلى عامل التجميع مع الميزانية.

لم تحدّد يومًا، وهناك عرضان في أمستردام. لذلك، شاهِد أداة الاختيار تعود إلى recall()، وابحث عن "سام يغادر في ليالي أيام الأسبوع"، واختر السبت.

وفي المتصفّح، يتوقف التنفيذ عند حدث adk_request_input:

طرح عليك رسم بياني سؤالاً. المقاطعة نفسها كما في انتظار قائمة الانتظار في الخطوة 8، ولكنها تشير الآن إلى شخص للإجابة عن السؤال.

2. الإجابة بالطريقة التي يجيب بها المستخدمون فعلاً

👉✨ في المربّع Enter your response... وليس في مربّع المحادثة في أسفل الصفحة:

About a 100 for the cheap seats. 250 if they're the good ones.

مشاهدة عملية الإيقاف وتأكيدها:

[agree_budget] reading back: About a hundred for the cheap seats. Two-fifty
               if they're the good ones.

⚡ adk_request_input
   So: About a hundred for the cheap seats. Two-fifty if they're the good
   ones. Have I got that right?

ويقرأ الرقم مرة أخرى قبل إرساله

3- تأكيد ذلك

👉✨ المربّع نفسه:

Yes, that's right.
[agree_budget] agreed: About a hundred for the cheap seats. Two-fifty if
               they're the good ones.
[open]         Budget: About a hundred for the cheap seats. Two-fifty if
               they're the good ones.
[pick_show]    → {"section": "A", "reason": "..."}
[queue_up]     ms-ams-01 section A — ticket q_5924edc2 at #14,203
[check_front]  #14,203 — not yet. Pausing.

أربع عُقد تعمل بكلمة واحدة. 👉 افتح علامة التبويب الحالة وابحث عن budget، أي جملتك حرفيًا.

الميزانية في علامة التبويب "الولاية" بكلماتك الخاصة

ما مِن بادئة. هذا هو حالة الجلسة، وهو قرار يستحق اتخاذه عن قصد إذا بدأت جلسة جديدة وستنتهي الميزانية، لذا سيطلب منك ذلك مرة أخرى.

4. الوصول إلى المقدمة والإنهاء

👉🔴 انقر على تخطّي الانتظار في لوحة المكان.

👉✨ أجب عن أحدث adk_request_input — مرة أخرى في مربّع الرد:

go

تم تنشيطه مرة أخرى، ولم يطلب مني تأكيد الإجراء مرتين

👉 تحقَّق من لوحة المكان: طلب واحد بسعر ضمن ما وافقت عليه لهذا النوع من المقاعد.

طلب واحد، ضمن ما وافقت عليه

كيف يطرح الرسم البياني سؤالاً على شخص؟

افتح nightly.py واقرأ agree_budget. يجب أن تكون ثلاثة أشياء صحيحة معًا، ويمكنك توقّع حدوث الخطأ نفسه عند فقدان أي منها: سيستمر الطلب إلى الأبد.

@node(rerun_on_resume=True)              # 1. run again when woken
def agree_budget(node_input, ctx):
    answers = ctx.resume_inputs or {}    # 3. where the answer arrives

    said = answers.get(ASK_BUDGET)
    if not said:
        return RequestInput(
            interrupt_id=ASK_BUDGET,     # 2. STABLE id, not the default
            message="What are you willing to spend?",
        )
    ...

rerun_on_resume=True

بدون ذلك، يتم التعامل مع العقدة على أنّها مكتملة ويتم تخطّيها

مستقر interrupt_id

القيمة التلقائية هي معرّف فريد عالمي جديد لكل عملية تشغيل، لذلك لا يتوفّر أي معرّف للبحث عن الإجابة

ctx.resume_inputs[id]

حيث تظهر الإجابة. ليس قيمة الإرجاع RequestInput — يكرّر ADK عقدة بدلاً من إرسالها

الرسم البياني

وهو الرسم البياني للخطوة 8 مع عقدة إضافية واحدة في المقدمة. الرسم البياني نفسه، مع وضع agree_budget في المقدمة

تكون القيود التي يؤكّدها المستخدم على مستوى الجلسة وتنتهي صلاحيتها عند انتهاء المحادثة. يؤدي الاحتفاظ بالحدود المؤقتة على مستوى الجلسات إلى تحويل القرارات التي يتم اتخاذها لمرة واحدة إلى إعدادات تلقائية دائمة غير مقصودة.

10. The Cloud Stack

اختفاء الملفات المخزَّنة على الجهاز عند وضع الكمبيوتر المحمول في وضع السكون

على الكمبيوتر المحمول، يعتمد الوكيل على إبقاء الكمبيوتر نشطًا وقواعد بيانات SQLite المحلية والملفات المحلية. إذا أغلقت غطاء الكمبيوتر المحمول أو نشرت إلى حاوية سحابية تتم إعادة تشغيلها، ستختفي الملفات والذاكرة المحلية.

لضمان تشغيلها بشكل موثوق في مرحلة الإنتاج، يجب اتّباع ما يلي:

  1. الحوسبة السحابية: شغِّل الوكيل في حاوية (مثل Cloud Run) تبقى متصلة بالإنترنت.
  2. قواعد بيانات Cloud الدائمة: استبدِل الملفات المحلية وSQLite بخدمات السحابة الإلكترونية (Cloud SQL لبيانات الجلسات وCloud Storage للملفات).
  3. المؤقتات السحابية: استخدِم أدوات جدولة مُدارة (مثل Cloud Scheduler وPub/Sub) لكي يستأنف الوكيل عمله حتى عندما يكون الكمبيوتر المحمول مطفأً.

👉🔴 في لوحة المكان، انقر على إعادة ضبط المكان. تبدأ كل خطوة من العالم نفسه: 8 مباريات، وجميع المقاعد متاحة، والساعة مضبوطة على 1×.

👉💻 الانتقال إلى الرمز النهائي:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force

ملف

أوجه التغيير

main.py

جديدًا. تطبيق FastAPI واحد يعرض صفحة الدردشة ونقطة النهاية التي يستدعيها المجدول

Dockerfile

جديدًا. Ordinary python-slim وuvicorn لا يتضمّن أي معلومات خاصة بحزمة تطوير البرامج الإعلانية

monstertix/

تم نسخها، وبالتالي يتم عرض الصفحة في الحاوية نفسها التي يتم فيها عرض الوكيل

concert/

لم يتم تغييره. المجلد الذي أنهيت الخطوة 9 به

الصفحة التي أنشأتها

إنّ <strong>واجهة مستخدم الويب في حزمة تطوير التطبيقات</strong> هي واجهة تصحيح أخطاء محلية فقط لفحص الحالة وتدفّقات الأحداث ومخططات سير العمل أثناء التطوير. لا يتم تجميعها لنشرها في بيئة الإنتاج.

بالنسبة إلى الإنتاج، استخدِم واجهة المستخدم الأمامية المستقلة للمحادثة التي تم إنشاؤها في الخطوة 3. يتعامل مع مقاطعات مراسلة المستخدم وتفاعله من خلال نقطة النهاية /wake بدون عرض عناصر التحكّم الداخلية لتصحيح الأخطاء.

نقل البيانات المحلية إلى الخدمات السحابية

أوقِف الخادم واطّلِع على ما كتبه:

sessions.db          a SQLite file next to the code
memory/userx.md      a markdown file you can open in an editor
artifacts/           seat maps written to a folder

على الرغم من أنّ الملفات المحلية تعمل بشكل جيد في مرحلة التطوير، إلا أنّها لا تعمل في مرحلة الإنتاج لأنّ حاويات Cloud Run تستخدم مساحة تخزين مؤقتة خاصة بالمثيل.

عند التشغيل في Cloud Run:

  • توسيع نطاق المثيلات إلى صفر: عند توقّف عدد الزيارات، يتم إنهاء الحاوية وحذف نظام الملفات المحلي الخاص بها.
  • توسيع نطاق الآلات الافتراضية بشكل مستقل: لا تتشارك الآلات الافتراضية المتزامنة المتعددة مساحة القرص، بل يعمل كل منها بنظام ملفات معزول خاص به.

إذا حفظ أحد الوكلاء تذكرة قائمة انتظار أو حالة جلسة في ملف SQLite محلي (sessions.db)، ستعثر الطلبات اللاحقة التي يتم توجيهها إلى مثيل حاوية جديد أو مختلف على قاعدة بيانات فارغة. للحفاظ على حالة مستدامة عند تغيير الحجم وإعادة التشغيل، يجب استبدال الملفات المحلية بخدمات خارجية مُدارة.

بدلاً من ذلك، تحتاج إلى حالة تدوم لفترة أطول من أي حاوية. ثلاث خدمات، واحدة لكل ملف:

على الكمبيوتر المحمول

في السحابة الإلكترونية

المقصود بذلك

sessions.db

Cloud SQL

قاعدة بيانات Postgres مُدارة وهي متوفّرة سواء كان هناك أي تطبيق قيد التشغيل أم لا

memory/userx.md

Cloud Storage

حزمة الملفات كعناصر يمكن الوصول إليها من كل مثيل في وقت واحد

artifacts/

Cloud Storage

الحزمة نفسها

clock.py

Cloud Scheduler

هي مهمة cron ينفّذها محرّك بحث Google. جهازك ليس ساعة منبّه

إنشاء هذه الصور

توفير قاعدة بيانات PostgreSQL ومستخدم قاعدة البيانات وحزمة Cloud Storage يُرجى العِلم أنّ إنشاء مثيل Cloud SQL يستغرق عدة دقائق وقد بدأ في الخلفية أثناء setup.sh.

👉💻 اجمعها وأنشئ الباقي:

cd ~/monstertix
./setup-cloud-state.sh
→ instance  RUNNABLE
→ database  adk created
→ user      adk created
→ bucket    created
→ memory    memory/userx.md → gs://your-project-agent-you/memory/
✓ durable state ready. Written to .env

أنشأ هذا النص البرمجي أربعة عناصر، عنصر واحد لكل صف من الجدول أعلاه، وكتب معرّفَي موارد موحّدَين في .env:

CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts

تفعيل الوكيل باستخدام المشغّلات المُجدوَلة المُدارة

بعد أن تصبح الخدمات جاهزة، امنح الوكيل مكانه الدائم.

👉💻 ثلاث أو أربع دقائق ابدأ القراءة:

cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh

ينشئ البرنامج النصي صورة حاوية من agent/، وينشرها على Cloud Run، ويوفر البنية الأساسية المستنِدة إلى الأحداث.

يتألف النشر الكامل من خمسة مكونات مختلفة:

1. gcloud run deploy       the container: your agent, the graph, and the page
2. a Pub/Sub topic         a queue for messages
3. a service account       an identity, so the message is allowed in
4. a push subscription     topic ──► /trigger/wake
5. a Cloud Scheduler job   the alarm clock, on a cron

‫Cloud Scheduler كمصدر للمشغّل

تستبدل Cloud Scheduler مؤقتات التنفيذ المحلية (clock.py) بخدمة cron مُدارة تستمر بشكل مستقل عن جلسات الوحدة الطرفية المحلية، وتتعامل مع عمليات إعادة المحاولة المبرمَجة، وتتيح جدولة المنطقة الزمنية.

‫Pub/Sub كطبقة فصل الرسائل

يوفّر وضع موضوع Pub/Sub بين Cloud Scheduler وCloud Run ثلاث مزايا معمارية:

  • ثبات الرسائل: يتم تخزين أحداث المشغّل الواردة في قوائم الانتظار إذا كانت خدمة Cloud Run في مرحلة بدء التشغيل البارد أو إعادة النشر أو غير متاحة مؤقتًا.
  • سياسات إعادة المحاولة المضمّنة: تعيد المحاولة تلقائيًا في حال حدوث أخطاء مؤقتة في التسليم بدون الحاجة إلى منطق مخصّص لإعادة المحاولة على مستوى التطبيق.
  • قابلية التوسّع في التوزيع الموسّع: تتيح لخدمات متعددة لاحقة الاشتراك في أحداث الجدول الزمني نفسها بدون إعادة ضبط إعدادات المجدول.

النص البديل

يؤدي إعداد trigger_sources=["pubsub"] إلى تسجيل نقطة نهاية لإرسال البيانات متوافقة مع حمولات مظاريف Pub/Sub العادية، ما يؤدي إلى ترجمة الرسائل الواردة مباشرةً إلى أحداث استدعاء الوكيل.

→ agent    https://concert-you-xxxx.run.app
→ topic    presale-you created
→ iam      concert-trigger@your-project... may invoke concert-you
→ push     → https://concert-you-xxxx.run.app/trigger/wake
→ schedule 0 3 * * *

التحدّث إليه

👉 افتح عنوان URL الخاص بالوكيل في متصفّح. 👉✨ اطلب من "مساعد Google" إجراء الحجز:

Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.

يجب أن يسألك عن المبلغ الذي تريد إنفاقه

👉✨ أجب بالطريقة التي تتحدث بها بصوت عالٍ، ثم أكِّد إجابتك:

up to 250 a seat for the good ones

تم إيقاف عملية التشغيل مؤقتًا في قائمة الانتظار.

👉🔴 اضغط على تخطّي الانتظار في لوحة المكان، ليكون الدور جاهزًا عند وصول الرسالة.

لتشغيلها في جدول زمني حقيقي، يجب تفعيل المهمة. يرفض jobs run المهمة المتوقّفة مؤقتًا مع Job.state must be ENABLED:

gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION

👉 في غضون بضع ثوانٍ، ينفّذ الوكيل عملية الشراء ضمن حدود الميزانية المؤكَّدة ويرسل تلقائيًا رسالة تأكيد الطلب.

👉🔴 تحقَّق من لوحة المكان. طلب واحد:

إليك كيفية تشغيله كل 5 دقائق:

gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"

تَنظيم

يجب احتساب عنصرَين تم نشرهما: المكان والوكيل بجانبه. يمكن ضبط كليهما على صفر، لذا لا تتكلف أي رسوم عند عدم استخدامهما. احذفها على أي حال.

./destroy-agent.sh          # service, topic, subscription, scheduler job
./destroy-venue.sh

يُرجى العِلم أنّ هذه الخطوات لا تتضمّن حذف Cloud SQL والحزمة، لأنّهما يحتويان على بياناتك ويستمران في إصدار الفواتير. عند التأكّد من الانتهاء:

./destroy-agent.sh --all

كل شيء آخر محلي. احذف المجلد عند الانتهاء، أو احتفظ به واستخدِمه مع مشروعك الخاص للمدة التي تريدها.

لم يتغيّر الوكيل وأدواته وعمليات رد الاتصال والميزانية. نجح اثنان من معرّفات الموارد الموحّدة ومتغيّر بيئة واحد.

11. نظرة عامة على النظام ودليل تحديد المشاكل وحلّها

الاختلافات في البيئة: البنية المحلية مقابل البنية السحابية

الكمبيوتر المحمول

في السحابة الإلكترونية

أوجه التغيير

--session_service_uri=sqlite+aiosqlite:///...

Cloud SQL

سلسلة اتصال

--artifact_service_uri=file://...

gs://bucket

معرّف موارد منتظم (URI)

memory/userx.md

gs://bucket/memory/

مسار

adk web

خدمة Cloud Run واحدة

‫Dockerfile وmain.py

monstertix/clock.py

‫Cloud Scheduler → Pub/Sub

trigger_sources=["pubsub"]

أنت، على لوحة المفاتيح

someone_is_there()

قيمة تلقائية يتم تجاهلها لكل طلب

لن يطرأ أي تغيير على عملية تنفيذ الوكيل الأساسية، بما في ذلك root_agent وتعريفات الأدوات وعناصر التحكّم في عمليات الاسترجاع وفرض الميزانية. توضّح مقارنة solutions/step10_deploy/concert/ بالخطوة 9 تطابقًا تامًا على مستوى البايت، ما يؤكّد كيف أنّ تجريد الخدمة في "حزمة تطوير التطبيقات" يفصل إعداد البنية الأساسية عن منطق الوكيل.

إذا واجهت مشكلة

يمكنك الحصول على رمز العمل الكامل في أي خطوة:

./use-solution.sh N --force          # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep   # code only, leave state alone

عندما يحدث خطأ

المشكلة

تعديل الإجابات

✗ Cannot access project

المعرّف غير صحيح. rm ~/project_id.txt && ./setup.sh

عرض النموذج للخطأ 404

تتوفّر الأسماء المستعارة -latest في AI Studio فقط. استخدِم ADK_MODEL=gemini-latest-flash

address already in use

يتوفّر منفذ 8000 أو 8090 حاليًا. lsof -ti:8000 | xargs kill -9

تعذُّر وصول الوكيل إلى المكان

‫curl $VENUE_URL/health أو ./deploy-venue.sh مرة أخرى

لا يتم الانتقال إلى الأغنية التالية في قائمة الانتظار

اضغط على تخطّي الانتظار. عند ضبط السرعة على 1×، لن يصل صفّ الانتظار إلى المقدمة تلقائيًا

يظهر تذكرة انتظار ثانية في اللوحة

لقد أجبت في مربّع المحادثة بدلاً من مربّع Enter your response... في حدث adk_request_input. يبدأ مربّع الدردشة عملية تشغيل جديدة

القائمة المنسدلة فارغة أو يتعذّر تحميل التطبيق

تعامِل adk web كل مجلد ضِمن agent/ على أنّه تطبيق، وتُرفض القائمة بأكملها إذا لم يكن أحد المجلدات تطبيقًا. يجب أن يعرض ls agent/ concert/ فقط، ولا شيء آخر، لأنّ تشغيل عناصر من داخل agent/ قد يؤدي إلى ترك memory/ غير مرغوب فيه. ترتيب ./use-solution.sh 8 --force

[EXPERIMENTAL] تحذير

متوقّع يتوفّر كل من EventsCompactionConfig وResumabilityConfig في الإصدار 2.6.2 في مرحلة ما قبل التوفّر للجمهور العام

كل شيء غريب

اضغط على إعادة ضبط المكان، ثم rm sessions.db

ملف الذاكرة ممتلئ بالحجوزات المكرّرة

يضيف remember() كل عملية تنفيذ. تعمل ./use-solution.sh N --force على إعادة ضبطها، أو cp seed/memory/default.md memory/userx.md

adk: command not found

‫source .venv/bin/activate: كل علامة تبويب جديدة تحتاج إلى ذلك

انتهت مهلة Cloud Shell

ابدأ adk web مرة أخرى. لقد نجوت من sessions.db، وهي الخطوة 6 التي ستحدث لك مجانًا