एजेंट डेवलपमेंट किट के साथ मल्टीमॉडल इस्तेमाल करना: Gemini 2.5, Firestore, और Cloud Run के साथ Personal खर्च असिस्टेंट का इस्तेमाल करना

1. 📖 परिचय

db9331886978d543.png

क्या आप कभी अपने सभी व्यक्तिगत खर्चों का प्रबंधन करने में निराश और बहुत आलसी हुए हैं? मैं भी! इसलिए इस कोडलैब में, हम एक व्यक्तिगत व्यय प्रबंधक सहायक का निर्माण करेंगे - जो हमारे लिए सभी काम करने के लिए जेमिनी 2.5 द्वारा संचालित होगा! अपलोड की गई रसीदों के प्रबंधन से लेकर यह विश्लेषण करने तक कि क्या आपने कॉफी खरीदने के लिए पहले ही बहुत अधिक खर्च कर दिया है!

यह सहायक वेब ब्राउज़र के माध्यम से चैट वेब इंटरफेस के रूप में उपलब्ध होगा, जिसमें आप इसके साथ संवाद कर सकते हैं, कुछ रसीदों की तस्वीरें अपलोड कर सकते हैं और सहायक से उन्हें संग्रहीत करने के लिए कह सकते हैं, या शायद फ़ाइल प्राप्त करने और कुछ व्यय विश्लेषण करने के लिए कुछ रसीदों को खोजना चाह सकते हैं. और यह सब Google Agent Development Kit फ़्रेमवर्क के आधार पर बनाया गया है

एप्लिकेशन को दो सेवाओं में विभाजित किया गया है: फ्रंटएंड और बैकएंड; जिससे आप त्वरित प्रोटोटाइप बना सकते हैं और यह अनुभव कर सकते हैं कि यह कैसा लगता है, और यह भी समझ सकते हैं कि एपीआई अनुबंध दोनों को एकीकृत करने के लिए कैसा दिखता है.

कोडलैब के ज़रिए, आपको यहां दिया गया तरीका अपनाना होगा:

  1. अपना Google क्लाउड प्रोजेक्ट तैयार करें और उस पर सभी आवश्यक API सक्षम करें
  2. Google क्लाउड स्टोरेज पर बकेट और फ़ायरस्टोर पर डेटाबेस सेटअप करें
  3. फायरस्टोर इंडेक्सिंग बनाएँ
  4. अपने कोडिंग वातावरण के लिए कार्यक्षेत्र सेटअप करें
  5. ADK एजेंट स्रोत कोड, उपकरण, प्रॉम्प्ट आदि की संरचना करना.
  6. ADK के लोकल वेब डेवलपमेंट यूज़र इंटरफ़ेस का इस्तेमाल करके एजेंट की जांच करना
  7. Gradio लाइब्रेरी का इस्तेमाल करके फ़्रंटएंड सेवा - चैट इंटरफ़ेस बनाएँ, ताकि कुछ क्वेरी भेजी जा सकें और रसीद की इमेज अपलोड की जा सकें
  8. बैकएंड सेवा बनाएँ - FastAPI का उपयोग करके HTTP सर्वर, जहाँ हमारा ADK एजेंट कोड, SessionService और Artifact Service स्थित है
  9. Cloud Run पर ऐप्लिकेशन को डिप्लॉय करने के लिए, एनवायरमेंट वैरिएबल और ज़रूरी फ़ाइलों को मैनेज करना
  10. एप्लिकेशन को क्लाउड रन पर तैनात करें

आर्किटेक्चर की खास जानकारी

90805d85052a5e5a.jpeg

ज़रूरी शर्तें

  • पायथन के साथ सहजता से काम करना
  • एचटीटीपी सेवा का इस्तेमाल करके, बुनियादी फ़ुल-स्टैक आर्किटेक्चर के बारे में जानकारी

आप क्या सीखेंगे

  • Gradio की मदद से, वेब के लिए फ़्रंटएंड प्रोटोटाइप बनाना
  • FastAPI और Pydantic के साथ बैकएंड सेवा विकास
  • ADK एजेंट की विभिन्न क्षमताओं का उपयोग करते हुए उसका आर्किटेक्चर तैयार करना
  • उपकरण का उपयोग
  • सत्र और कलाकृति प्रबंधन
  • Gemini को भेजे जाने से पहले, इनपुट में बदलाव करने के लिए कॉल बैक का इस्तेमाल
  • टास्क को पूरा करने की परफ़ॉर्मेंस को बेहतर बनाने के लिए, BuiltInPlanner का इस्तेमाल करना
  • ADK के लोकल वेब इंटरफ़ेस की मदद से, तुरंत डीबग करना
  • ADK कॉलबैक का उपयोग करके शीघ्र इंजीनियरिंग और जेमिनी अनुरोध संशोधन के माध्यम से सूचना पार्सिंग और पुनर्प्राप्ति के माध्यम से मल्टीमॉडल इंटरैक्शन को अनुकूलित करने की रणनीति
  • वेक्टर डेटाबेस के रूप में फ़ायरस्टोर का उपयोग करके एजेंटिक पुनर्प्राप्ति संवर्धित पीढ़ी
  • Pydantic-settings की मदद से YAML फ़ाइल में पर्यावरण वैरिएबल प्रबंधित करें
  • Dockerfile का उपयोग करके क्लाउड रन पर एप्लिकेशन तैनात करें और YAML फ़ाइल के साथ पर्यावरण चर प्रदान करें

आपको क्या चाहिए होगा

  • क्रोम वेब ब्राउज़र
  • एक जीमेल खाता
  • बिलिंग सक्षम क्लाउड प्रोजेक्ट

यह कोडलैब, सभी लेवल के डेवलपर के लिए बनाया गया है. इसमें शुरुआती डेवलपर भी शामिल हैं. इसमें सैंपल ऐप्लिकेशन में Python का इस्तेमाल किया गया है. हालाँकि, प्रस्तुत अवधारणाओं को समझने के लिए पायथन ज्ञान की आवश्यकता नहीं है.

2. 🚀शुरू करने से पहले

क्लाउड कंसोल में सक्रिय प्रोजेक्ट चुनें

यह कोडलैब यह मानता है कि आपके पास पहले से ही बिलिंग सक्षम एक Google क्लाउड प्रोजेक्ट है. यदि आपके पास अभी तक यह सुविधा नहीं है, तो आप आरंभ करने के लिए नीचे दिए गए निर्देशों का पालन कर सकते हैं.

  1. Google Cloud Console में, प्रोजेक्ट चयनकर्ता पृष्ठ पर, Google Cloud प्रोजेक्ट चुनें या बनाएं.
  2. पक्का करें कि आपके Cloud प्रोजेक्ट के लिए बिलिंग चालू हो. किसी प्रोजेक्ट पर बिलिंग सक्षम है या नहीं, इसकी जांच करने का तरीका जानें.

fcdd90149a030bf5.png

फ़ायरस्टोर डेटाबेस तैयार करें

इसके बाद, हमें एक फायरस्टोर डेटाबेस भी बनाना होगा. नेटिव मोड में फायरस्टोर एक NoSQL दस्तावेज़ डेटाबेस है जो स्वचालित स्केलिंग, उच्च प्रदर्शन और अनुप्रयोग विकास में आसानी के लिए बनाया गया है. यह एक वेक्टर डेटाबेस के रूप में भी कार्य कर सकता है जो हमारी प्रयोगशाला के लिए पुनर्प्राप्ति संवर्धित पीढ़ी तकनीक का समर्थन कर सकता है.

  1. खोज बार पर "firestore" खोजें और Firestore उत्पाद पर क्लिक करें

44bbce791824bed6.png

  1. फिर, फ़ायरस्टोर डेटाबेस बनाएँ बटन पर क्लिक करें
  2. डेटाबेस आईडी के नाम के तौर पर (डिफ़ॉल्ट) का इस्तेमाल करें और Standard Edition को चुना हुआ रहने दें. इस लैब डेमो के लिए, ओपन सुरक्षा नियमों के साथ फ़ायरस्टोर नेटिव का उपयोग करें.
  1. आप यह भी देखेंगे कि इस डेटाबेस में वास्तव में फ्री-टियर उपयोग YEAY! है इसके बाद, डेटाबेस बनाएँ बटन पर क्लिक करें

b97d210c465be94c.png

इन चरणों के बाद, आपको उस Firestore डेटाबेस पर रीडायरेक्ट कर दिया जाएगा जिसे आपने अभी-अभी बनाया है

Cloud Shell टर्मिनल में क्लाउड प्रोजेक्ट सेट अप करना

  1. आप क्लाउड शेल का इस्तेमाल करेंगे, जो Google क्लाउड में चलने वाला एक कमांड-लाइन वातावरण है और bq के साथ पहले से लोड होता है. Google क्लाउड कंसोल के शीर्ष पर क्लाउड शेल सक्रिय करें पर क्लिक करें.

26f20e837ff06119.png

  1. क्लाउड शेल से कनेक्ट होने के बाद, आप निम्न कमांड का उपयोग करके जांचते हैं कि आप पहले से ही प्रमाणित हैं और प्रोजेक्ट आपकी प्रोजेक्ट आईडी पर सेट है:
gcloud auth list
  1. यह पुष्टि करने के लिए कि gcloud कमांड आपके प्रोजेक्ट के बारे में जानता है, क्लाउड शेल में निम्नलिखित कमांड चलाएँ.
gcloud config list project
  1. यदि आपका प्रोजेक्ट सेट नहीं है, तो उसे सेट करने के लिए निम्नलिखित कमांड का उपयोग करें:
gcloud config set project <YOUR_PROJECT_ID>

वैकल्पिक रूप से, आप कंसोल में PROJECT_ID आईडी भी देख सकते हैं

bb98435b79995b15.jpeg

इस पर क्लिक करें और आपको अपनी पूरी परियोजना और परियोजना आईडी दाईं ओर दिखाई देगी

ffa73dee57de5307.jpeg

  1. नीचे दिखाए गए आदेश के माध्यम से आवश्यक API सक्षम करें. इसमें कुछ मिनट लग सकते हैं, इसलिए कृपया धैर्य रखें.
gcloud services enable aiplatform.googleapis.com \
                       firestore.googleapis.com \
                       run.googleapis.com \
                       cloudbuild.googleapis.com \
                       cloudresourcemanager.googleapis.com

कमांड के सफल निष्पादन पर, आपको नीचे दिखाए गए संदेश के समान एक संदेश दिखाई देगा:

Operation "operations/..." finished successfully.

gcloud कमांड के बजाय, कंसोल का इस्तेमाल करके भी ऐसा किया जा सकता है. इसके लिए, हर प्रॉडक्ट को खोजें या इस लिंक का इस्तेमाल करें.

अगर कोई एपीआई छूट जाता है, तो उसे लागू करने के दौरान कभी भी चालू किया जा सकता है.

gcloud कमांड और उपयोग के लिए दस्तावेज़ीकरण देखें.

Google Cloud Storage बकेट तैयार करना

इसके बाद, हमें उसी टर्मिनल से GCS बकेट को तैयार करना होगा, ताकि अपलोड की गई फ़ाइल को सेव किया जा सके. बकेट बनाने के लिए निम्नलिखित कमांड चलाएँ, व्यक्तिगत व्यय सहायक रसीदों से संबंधित एक अद्वितीय लेकिन प्रासंगिक बकेट नाम की आवश्यकता होगी, इसलिए हम आपके प्रोजेक्ट आईडी के साथ संयुक्त निम्नलिखित बकेट नाम का उपयोग करेंगे

gsutil mb -l us-central1 gs://personal-expense-{your-project-id}

इससे यह आउटपुट दिखेगा

Creating gs://personal-expense-{your-project-id}

आप ब्राउज़र के ऊपर बाईं ओर नेविगेशन मेनू में जाकर क्लाउड स्टोरेज -> बकेट चुनकर इसकी पुष्टि कर सकते हैं

7b9fd51982d351fa.png

फायरस्टोर मूलतः एक NoSQL डेटाबेस है, जो डेटा मॉडल में बेहतर प्रदर्शन और लचीलापन प्रदान करता है, लेकिन जटिल प्रश्नों के मामले में इसकी सीमाएँ हैं. चूंकि हम कुछ मिश्रित बहु-क्षेत्रीय क्वेरीज़ और वेक्टर खोज का उपयोग करने की योजना बना रहे हैं, इसलिए हमें पहले कुछ इंडेक्स बनाने की आवश्यकता होगी. आप इस दस्तावेज़ में विवरण के बारे में अधिक पढ़ सकते हैं

  1. संयुक्त क्वेरीज़ का समर्थन करने हेतु इंडेक्स बनाने हेतु निम्न कमांड चलाएँ
gcloud firestore indexes composite create \
        --collection-group=personal-expense-assistant-receipts \
        --field-config field-path=total_amount,order=ASCENDING \
        --field-config field-path=transaction_time,order=ASCENDING \
        --field-config field-path=__name__,order=ASCENDING \
        --database="(default)"
  1. और वेक्टर खोज का समर्थन करने के लिए इसे चलाएँ
gcloud firestore indexes composite create \
        --collection-group="personal-expense-assistant-receipts" \
        --query-scope=COLLECTION \
        --field-config field-path="embedding",vector-config='{"dimension":"768", "flat": "{}"}' \
        --database="(default)"

आप क्लाउड कंसोल में फ़ायरस्टोर पर जाकर, (डिफ़ॉल्ट) डेटाबेस इंस्टेंस पर क्लिक करके और नेविगेशन बार पर इंडेक्स चुनकर बनाए गए इंडेक्स की जांच कर सकते हैं

9849724dd55dfab7.png

क्लाउड शेल एडिटर पर जाएं और ऐप्लिकेशन वर्किंग डायरेक्टरी सेटअप करें

अब हम कोडिंग से जुड़े कुछ काम करने के लिए, कोड एडिटर सेट अप कर सकते हैं. इसके लिए, हम Cloud Shell Editor का इस्तेमाल करेंगे

  1. 'एडिटर खोलें' बटन पर क्लिक करें. इससे Cloud Shell Editor खुल जाएगा. यहां हम अपना कोड लिख सकते हैं 168eacea651b086c.png
  2. इसके बाद, हमें यह भी देखना होगा कि शेल को आपके पास मौजूद सही PROJECT ID के लिए पहले से कॉन्फ़िगर किया गया है या नहीं. अगर आपको टर्मिनल में $आइकॉन से पहले ( ) के अंदर वैल्यू दिखती है, तो इसका मतलब है कि शेल को पहले से कॉन्फ़िगर किया गया है. नीचे दिए गए स्क्रीनशॉट में, वैल्यू "adk-multimodal-tool" है. यह वैल्यू, आपके चालू शेल सेशन के लिए कॉन्फ़िगर किए गए प्रोजेक्ट को दिखाती है.

10a99ff80839b635.png

अगर दिखाया गया मान पहले से ही सही है, तो आप अगला आदेश छोड़ सकते हैं. हालांकि, अगर यह सही नहीं है या मौजूद नहीं है, तो यह कमांड चलाएं

gcloud config set project <YOUR_PROJECT_ID>
  1. इसके बाद, आइए इस कोडलैब के लिए गिटहब से टेम्पलेट कार्यशील निर्देशिका को क्लोन करें, निम्नलिखित कमांड चलाएँ. इससे personal-expense-assistant डायरेक्ट्री में वर्किंग डायरेक्ट्री बन जाएगी
git clone https://github.com/alphinside/personal-expense-assistant-adk-codelab-starter.git personal-expense-assistant
  1. इसके बाद, क्लाउड शेल एडिटर के सबसे ऊपरी भाग में जाएं और फ़ाइल->फ़ोल्डर खोलें पर क्लिक करें, अपनी उपयोगकर्ता नाम निर्देशिका ढूंढें और व्यक्तिगत-व्यय-सहायक निर्देशिका ढूंढें, फिर ठीक बटन पर क्लिक करें. इससे चुनी गई डायरेक्ट्री, मुख्य वर्किंग डायरेक्ट्री बन जाएगी. इस उदाहरण में, उपयोगकर्ता नाम alvinprayuda है. इसलिए, डायरेक्ट्री का पाथ यहां दिखाया गया है

c87d2b76896d0c59.png

524b9e6369f68cca.png

अब, आपका क्लाउड शेल संपादक इस तरह दिखना चाहिए

9a58ccc43f48338d.png

पर्यावरण सेटअप

पायथन वर्चुअल वातावरण तैयार करें

अगला कदम विकास वातावरण तैयार करना है. आपका वर्तमान सक्रिय टर्मिनल personal-expense-assistant कार्यशील निर्देशिका के अंदर होना चाहिए. हम इस कोडलैब में पायथन 3.12 का उपयोग करेंगे और पायथन संस्करण और वर्चुअल वातावरण बनाने और प्रबंधित करने की आवश्यकता को सरल बनाने के लिए uv पायथन प्रोजेक्ट मैनेजर का उपयोग करेंगे

  1. अगर आपने अभी तक टर्मिनल नहीं खोला है, तो टर्मिनल -> नया टर्मिनल पर क्लिक करके इसे खोलें , या Ctrl + Shift + C का इस्तेमाल करें , इससे ब्राउज़र के निचले हिस्से में एक टर्मिनल विंडो खुल जाएगी

8635b60ae2f45bbc.jpeg

  1. अब uv का उपयोग करके वर्चुअल वातावरण को आरंभ करें , ये कमांड चलाएँ
cd ~/personal-expense-assistant
uv sync --frozen

इससे .venv निर्देशिका बन जाएगी और निर्भरताएं इंस्टॉल हो जाएंगी. pyproject.toml पर एक नज़र डालने से आपको इस तरह दिखाई गई निर्भरताओं के बारे में जानकारी मिलेगी

dependencies = [
    "datasets>=3.5.0",
    "google-adk==1.18",
    "google-cloud-firestore>=2.20.1",
    "gradio>=5.23.1",
    "pydantic>=2.10.6",
    "pydantic-settings[yaml]>=2.8.1",
]

सेटअप कॉन्फ़िगरेशन फ़ाइलें

अब हमें इस परियोजना के लिए कॉन्फ़िगरेशन फ़ाइलें सेट अप करनी होंगी. हम YAML फ़ाइल से कॉन्फ़िगरेशन पढ़ने के लिए pydantic-settings का उपयोग करते हैं.

हमने पहले ही settings.yaml.example के अंदर फ़ाइल टेम्प्लेट प्रदान कर दिया है , हमें फ़ाइल को कॉपी करके उसका नाम बदलकर settings.yaml करना होगा. फ़ाइल बनाने के लिए यह आदेश चलाएँ

cp settings.yaml.example settings.yaml

फिर, निम्न मान को फ़ाइल में कॉपी करें

GCLOUD_LOCATION: "us-central1"
GCLOUD_PROJECT_ID: "your-project-id"
BACKEND_URL: "http://localhost:8081/chat"
STORAGE_BUCKET_NAME: "personal-expense-{your-project-id}"
DB_COLLECTION_NAME: "personal-expense-assistant-receipts"

इस कोडलैब के लिए, हम GCLOUD_LOCATION, BACKEND_URL, और DB_COLLECTION_NAME के लिए पूर्व-कॉन्फ़िगर किए गए मानों का उपयोग कर रहे हैं .

अब हम अगले चरण पर जा सकते हैं, एजेंट का निर्माण और फिर सेवाएँ

3. 🚀 Google ADK और Gemini 2.5 का उपयोग करके एजेंट बनाएँ

ADK डायरेक्ट्री स्ट्रक्चर के बारे में जानकारी

आइए, सबसे पहले यह जानते हैं कि ADK में क्या-क्या सुविधाएं मिलती हैं और एजेंट कैसे बनाया जाता है. ADK के पूरे दस्तावेज़ को इस यूआरएल पर ऐक्सेस किया जा सकता है . ADK, सीएलआई कमांड को लागू करने के लिए कई सुविधाएं देता है. इनमें से कुछ यहां दिए गए हैं :

  • एजेंट डायरेक्ट्री स्ट्रक्चर सेट अप करना
  • सीएलआई इनपुट आउटपुट के ज़रिए, बातचीत करने की सुविधा को तुरंत आज़माएं
  • लोकल डेवलपमेंट यूज़र इंटरफ़ेस (यूआई) वेब इंटरफ़ेस को तुरंत सेटअप करना

अब, आइए CLI कमांड का उपयोग करके एजेंट निर्देशिका संरचना बनाएं. निम्न आदेश चलाएँ.

uv run adk create expense_manager_agent

पूछे जाने पर, मॉडल gemini-2.5-flash और Vertex AI बैकएंड चुनें. इसके बाद विज़ार्ड प्रोजेक्ट आईडी और स्थान पूछेगा. आप एंटर दबाकर डिफ़ॉल्ट विकल्पों को स्वीकार कर सकते हैं, या आवश्यकतानुसार उन्हें बदल सकते हैं. बस दोबारा जांच लें कि आप इस लैब में पहले बनाई गई सही प्रोजेक्ट आईडी का उपयोग कर रहे हैं. आउटपुट इस तरह दिखेगा:

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, check out this link for details:
https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-cloud-vertex-ai

Enter Google Cloud project ID [going-multimodal-lab]: 
Enter Google Cloud region [us-central1]: 

Agent created in /home/username/personal-expense-assistant/expense_manager_agent:
- .env
- __init__.py
- agent.py

यह निम्नलिखित एजेंट निर्देशिका संरचना बनाएगा

expense_manager_agent/
├── __init__.py
├── .env
├── agent.py

init.py और agent.py की जांच करने पर, आपको यह कोड दिखेगा

# __init__.py

from . import agent
# agent.py

from google.adk.agents import Agent

root_agent = Agent(
    model='gemini-2.5-flash',
    name='root_agent',
    description='A helpful assistant for user questions.',
    instruction='Answer user questions to the best of your knowledge',
)

अब आप इसे चलाकर परीक्षण कर सकते हैं

uv run adk run expense_manager_agent

जब भी आपका परीक्षण पूरा हो जाए, तो आप exit टाइप करके या Ctrl+D दबाकर एजेंट से बाहर निकल सकते हैं.

अपना व्यय प्रबंधक एजेंट बनाना

आइए अपना व्यय प्रबंधक एजेंट बनाएं! expense_manager_agent/agent.py फ़ाइल खोलें और नीचे दिए गए कोड को कॉपी करें जिसमें root_agent होगा.

# expense_manager_agent/agent.py

from google.adk.agents import Agent
from expense_manager_agent.tools import (
    store_receipt_data,
    search_receipts_by_metadata_filter,
    search_relevant_receipts_by_natural_language_query,
    get_receipt_data_by_image_id,
)
from expense_manager_agent.callbacks import modify_image_data_in_history
import os
from settings import get_settings
from google.adk.planners import BuiltInPlanner
from google.genai import types

SETTINGS = get_settings()
os.environ["GOOGLE_CLOUD_PROJECT"] = SETTINGS.GCLOUD_PROJECT_ID
os.environ["GOOGLE_CLOUD_LOCATION"] = SETTINGS.GCLOUD_LOCATION
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "TRUE"

# Get the code file directory path and read the task prompt file
current_dir = os.path.dirname(os.path.abspath(__file__))
prompt_path = os.path.join(current_dir, "task_prompt.md")
with open(prompt_path, "r") as file:
    task_prompt = file.read()

root_agent = Agent(
    name="expense_manager_agent",
    model="gemini-2.5-flash",
    description=(
        "Personal expense agent to help user track expenses, analyze receipts, and manage their financial records"
    ),
    instruction=task_prompt,
    tools=[
        store_receipt_data,
        get_receipt_data_by_image_id,
        search_receipts_by_metadata_filter,
        search_relevant_receipts_by_natural_language_query,
    ],
    planner=BuiltInPlanner(
        thinking_config=types.ThinkingConfig(
            thinking_budget=2048,
        )
    ),
    before_model_callback=modify_image_data_in_history,
)

कोड स्पष्टीकरण

इस स्क्रिप्ट में, एजेंट को शुरू करने की सुविधा शामिल है. इसमें हम इन चीज़ों को शुरू करते हैं:

  • इस्तेमाल किए जाने वाले मॉडल को gemini-2.5-flash पर सेट करें
  • एजेंट विवरण और निर्देश को सिस्टम प्रॉम्प्ट के रूप में सेट करें, जिसे task_prompt.md से पढ़ा जा रहा है
  • एजेंट की कार्यक्षमता का समर्थन करने के लिए आवश्यक उपकरण प्रदान करें
  • Gemini 2.5 Flash की थिंकिंग क्षमताओं का इस्तेमाल करके, फ़ाइनल जवाब जनरेट करने या टास्क पूरा करने से पहले प्लान बनाने की सुविधा चालू करें
  • Gemini को अनुरोध भेजने से पहले, कॉल बैक इंटरसेप्ट सेट अप करें. इससे अनुमान लगाने से पहले भेजे जाने वाले इमेज डेटा की संख्या को सीमित किया जा सकेगा

4. 🚀 एजेंट टूल्स को कॉन्फ़िगर करना

हमारे व्यय प्रबंधक एजेंट के पास निम्नलिखित क्षमताएं होंगी:

  • रसीद छवि से डेटा निकालें और डेटा और फ़ाइल संग्रहीत करें
  • खर्च के डेटा पर सटीक खोज करना
  • व्यय डेटा पर प्रासंगिक खोज

इसलिए हमें इस कार्यक्षमता का समर्थन करने के लिए उपयुक्त उपकरणों की आवश्यकता है. expense_manager_agent निर्देशिका में एक नई फ़ाइल बनाएँ और उसका नाम tools.py रखें

touch expense_manager_agent/tools.py

expense_manage_agent/tools.py खोलें, फिर नीचे दिए गए कोड को कॉपी करें

# expense_manager_agent/tools.py

import datetime
from typing import Dict, List, Any
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector
from google.cloud.firestore_v1 import FieldFilter
from google.cloud.firestore_v1.base_query import And
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from settings import get_settings
from google import genai

SETTINGS = get_settings()
DB_CLIENT = firestore.Client(
    project=SETTINGS.GCLOUD_PROJECT_ID
)  # Will use "(default)" database
COLLECTION = DB_CLIENT.collection(SETTINGS.DB_COLLECTION_NAME)
GENAI_CLIENT = genai.Client(
    vertexai=True, location=SETTINGS.GCLOUD_LOCATION, project=SETTINGS.GCLOUD_PROJECT_ID
)
EMBEDDING_DIMENSION = 768
EMBEDDING_FIELD_NAME = "embedding"
INVALID_ITEMS_FORMAT_ERR = """
Invalid items format. Must be a list of dictionaries with 'name', 'price', and 'quantity' keys."""
RECEIPT_DESC_FORMAT = """
Store Name: {store_name}
Transaction Time: {transaction_time}
Total Amount: {total_amount}
Currency: {currency}
Purchased Items:
{purchased_items}
Receipt Image ID: {receipt_id}
"""


def sanitize_image_id(image_id: str) -> str:
    """Sanitize image ID by removing any leading/trailing whitespace."""
    if image_id.startswith("[IMAGE-"):
        image_id = image_id.split("ID ")[1].split("]")[0]

    return image_id.strip()


def store_receipt_data(
    image_id: str,
    store_name: str,
    transaction_time: str,
    total_amount: float,
    purchased_items: List[Dict[str, Any]],
    currency: str = "IDR",
) -> str:
    """
    Store receipt data in the database.

    Args:
        image_id (str): The unique identifier of the image. For example IMAGE-POSITION 0-ID 12345,
            the ID of the image is 12345.
        store_name (str): The name of the store.
        transaction_time (str): The time of purchase, in ISO format ("YYYY-MM-DDTHH:MM:SS.ssssssZ").
        total_amount (float): The total amount spent.
        purchased_items (List[Dict[str, Any]]): A list of items purchased with their prices. Each item must have:
            - name (str): The name of the item.
            - price (float): The price of the item.
            - quantity (int, optional): The quantity of the item. Defaults to 1 if not provided.
        currency (str, optional): The currency of the transaction, can be derived from the store location.
            If unsure, default is "IDR".

    Returns:
        str: A success message with the receipt ID.

    Raises:
        Exception: If the operation failed or input is invalid.
    """
    try:
        # In case of it provide full image placeholder, extract the id string
        image_id = sanitize_image_id(image_id)

        # Check if the receipt already exists
        doc = get_receipt_data_by_image_id(image_id)

        if doc:
            return f"Receipt with ID {image_id} already exists"

        # Validate transaction time
        if not isinstance(transaction_time, str):
            raise ValueError(
                "Invalid transaction time: must be a string in ISO format 'YYYY-MM-DDTHH:MM:SS.ssssssZ'"
            )
        try:
            datetime.datetime.fromisoformat(transaction_time.replace("Z", "+00:00"))
        except ValueError:
            raise ValueError(
                "Invalid transaction time format. Must be in ISO format 'YYYY-MM-DDTHH:MM:SS.ssssssZ'"
            )

        # Validate items format
        if not isinstance(purchased_items, list):
            raise ValueError(INVALID_ITEMS_FORMAT_ERR)

        for _item in purchased_items:
            if (
                not isinstance(_item, dict)
                or "name" not in _item
                or "price" not in _item
            ):
                raise ValueError(INVALID_ITEMS_FORMAT_ERR)

            if "quantity" not in _item:
                _item["quantity"] = 1

        # Create a combined text from all receipt information for better embedding
        result = GENAI_CLIENT.models.embed_content(
            model="text-embedding-004",
            contents=RECEIPT_DESC_FORMAT.format(
                store_name=store_name,
                transaction_time=transaction_time,
                total_amount=total_amount,
                currency=currency,
                purchased_items=purchased_items,
                receipt_id=image_id,
            ),
        )

        embedding = result.embeddings[0].values

        doc = {
            "receipt_id": image_id,
            "store_name": store_name,
            "transaction_time": transaction_time,
            "total_amount": total_amount,
            "currency": currency,
            "purchased_items": purchased_items,
            EMBEDDING_FIELD_NAME: Vector(embedding),
        }

        COLLECTION.add(doc)

        return f"Receipt stored successfully with ID: {image_id}"
    except Exception as e:
        raise Exception(f"Failed to store receipt: {str(e)}")


def search_receipts_by_metadata_filter(
    start_time: str,
    end_time: str,
    min_total_amount: float = -1.0,
    max_total_amount: float = -1.0,
) -> str:
    """
    Filter receipts by metadata within a specific time range and optionally by amount.

    Args:
        start_time (str): The start datetime for the filter (in ISO format, e.g. 'YYYY-MM-DDTHH:MM:SS.ssssssZ').
        end_time (str): The end datetime for the filter (in ISO format, e.g. 'YYYY-MM-DDTHH:MM:SS.ssssssZ').
        min_total_amount (float): The minimum total amount for the filter (inclusive). Defaults to -1.
        max_total_amount (float): The maximum total amount for the filter (inclusive). Defaults to -1.

    Returns:
        str: A string containing the list of receipt data matching all applied filters.

    Raises:
        Exception: If the search failed or input is invalid.
    """
    try:
        # Validate start and end times
        if not isinstance(start_time, str) or not isinstance(end_time, str):
            raise ValueError("start_time and end_time must be strings in ISO format")
        try:
            datetime.datetime.fromisoformat(start_time.replace("Z", "+00:00"))
            datetime.datetime.fromisoformat(end_time.replace("Z", "+00:00"))
        except ValueError:
            raise ValueError("start_time and end_time must be strings in ISO format")

        # Start with the base collection reference
        query = COLLECTION

        # Build the composite query by properly chaining conditions
        # Notes that this demo assume 1 user only,
        # need to refactor the query for multiple user
        filters = [
            FieldFilter("transaction_time", ">=", start_time),
            FieldFilter("transaction_time", "<=", end_time),
        ]

        # Add optional filters
        if min_total_amount != -1:
            filters.append(FieldFilter("total_amount", ">=", min_total_amount))

        if max_total_amount != -1:
            filters.append(FieldFilter("total_amount", "<=", max_total_amount))

        # Apply the filters
        composite_filter = And(filters=filters)
        query = query.where(filter=composite_filter)

        # Execute the query and collect results
        search_result_description = "Search by Metadata Results:\n"
        for doc in query.stream():
            data = doc.to_dict()
            data.pop(
                EMBEDDING_FIELD_NAME, None
            )  # Remove embedding as it's not needed for display

            search_result_description += f"\n{RECEIPT_DESC_FORMAT.format(**data)}"

        return search_result_description
    except Exception as e:
        raise Exception(f"Error filtering receipts: {str(e)}")


def search_relevant_receipts_by_natural_language_query(
    query_text: str, limit: int = 5
) -> str:
    """
    Search for receipts with content most similar to the query using vector search.
    This tool can be use for user query that is difficult to translate into metadata filters.
    Such as store name or item name which sensitive to string matching.
    Use this tool if you cannot utilize the search by metadata filter tool.

    Args:
        query_text (str): The search text (e.g., "coffee", "dinner", "groceries").
        limit (int, optional): Maximum number of results to return (default: 5).

    Returns:
        str: A string containing the list of contextually relevant receipt data.

    Raises:
        Exception: If the search failed or input is invalid.
    """
    try:
        # Generate embedding for the query text
        result = GENAI_CLIENT.models.embed_content(
            model="text-embedding-004", contents=query_text
        )
        query_embedding = result.embeddings[0].values

        # Notes that this demo assume 1 user only,
        # need to refactor the query for multiple user
        vector_query = COLLECTION.find_nearest(
            vector_field=EMBEDDING_FIELD_NAME,
            query_vector=Vector(query_embedding),
            distance_measure=DistanceMeasure.EUCLIDEAN,
            limit=limit,
        )

        # Execute the query and collect results
        search_result_description = "Search by Contextual Relevance Results:\n"
        for doc in vector_query.stream():
            data = doc.to_dict()
            data.pop(
                EMBEDDING_FIELD_NAME, None
            )  # Remove embedding as it's not needed for display
            search_result_description += f"\n{RECEIPT_DESC_FORMAT.format(**data)}"

        return search_result_description
    except Exception as e:
        raise Exception(f"Error searching receipts: {str(e)}")


def get_receipt_data_by_image_id(image_id: str) -> Dict[str, Any]:
    """
    Retrieve receipt data from the database using the image_id.

    Args:
        image_id (str): The unique identifier of the receipt image. For example, if the placeholder is
            [IMAGE-ID 12345], the ID to use is 12345.

    Returns:
        Dict[str, Any]: A dictionary containing the receipt data with the following keys:
            - receipt_id (str): The unique identifier of the receipt image.
            - store_name (str): The name of the store.
            - transaction_time (str): The time of purchase in UTC.
            - total_amount (float): The total amount spent.
            - currency (str): The currency of the transaction.
            - purchased_items (List[Dict[str, Any]]): List of items purchased with their details.
        Returns an empty dictionary if no receipt is found.
    """
    # In case of it provide full image placeholder, extract the id string
    image_id = sanitize_image_id(image_id)

    # Query the receipts collection for documents with matching receipt_id (image_id)
    # Notes that this demo assume 1 user only,
    # need to refactor the query for multiple user
    query = COLLECTION.where(filter=FieldFilter("receipt_id", "==", image_id)).limit(1)
    docs = list(query.stream())

    if not docs:
        return {}

    # Get the first matching document
    doc_data = docs[0].to_dict()
    doc_data.pop(EMBEDDING_FIELD_NAME, None)

    return doc_data

कोड के बारे में जानकारी

इस उपकरण फ़ंक्शन कार्यान्वयन में हम इन 2 मुख्य विचारों के आसपास उपकरण डिज़ाइन करते हैं:

  • रसीद डेटा को पार्स करें और इमेज आईडी स्ट्रिंग प्लेसहोल्डर [IMAGE-ID <hash-of-image-1>] का इस्तेमाल करके मूल फ़ाइल से मैपिंग करें
  • फायरस्टोर डेटाबेस का उपयोग करके डेटा संग्रहीत करना और पुनर्प्राप्त करना

टूल "store_receipt_data"

747fb55e801455f4.png

यह उपकरण ऑप्टिकल कैरेक्टर रिकॉग्निशन उपकरण है, यह छवि डेटा से आवश्यक जानकारी को पार्स करेगा, साथ ही छवि आईडी स्ट्रिंग को पहचानेगा और उन्हें फायरस्टोर डेटाबेस में संग्रहीत करने के लिए एक साथ मैप करेगा.

इसके अलावा, यह टूल text-embedding-004 का उपयोग करके रसीद की सामग्री को एम्बेडिंग में भी परिवर्तित करता है ताकि सभी मेटाडेटा और एम्बेडिंग एक साथ संग्रहीत और अनुक्रमित हो जाएं. क्वेरी या प्रासंगिक खोज द्वारा पुनः प्राप्त करने के लिए लचीलापन सक्षम करना.

इस टूल को सफलतापूर्वक निष्पादित करने के बाद, आप देख सकते हैं कि रसीद डेटा पहले से ही फायरस्टोर डेटाबेस में अनुक्रमित है जैसा कि नीचे दिखाया गया है

636d56be9880f3c7.png

टूल "search_receipts_by_metadata_filter"

6d8fbd9b43ff7ea7.png

यह उपकरण उपयोगकर्ता क्वेरी को मेटाडेटा क्वेरी फ़िल्टर में परिवर्तित करता है जो दिनांक सीमा और/या कुल लेनदेन के आधार पर खोज का समर्थन करता है. यह सभी मिलान किए गए रसीद डेटा को वापस कर देगा, जहां प्रक्रिया में हम एम्बेडिंग फ़ील्ड को छोड़ देंगे क्योंकि एजेंट को संदर्भगत समझ के लिए इसकी आवश्यकता नहीं है

टूल "search_relevant_receipts_by_natural_language_query"

7262c75114af0060.png

यह हमारा रिट्रीवल ऑग्मेंटेड जेनरेशन (RAG) टूल है. हमारे एजेंट के पास वेक्टर डेटाबेस से प्रासंगिक रसीदें प्राप्त करने के लिए अपनी स्वयं की क्वेरी डिजाइन करने की क्षमता है और यह भी चुन सकता है कि इस उपकरण का उपयोग कब करना है. एजेंट को स्वतंत्र रूप से यह निर्णय लेने की अनुमति देना कि वह इस RAG टूल का उपयोग करेगा या नहीं और अपनी स्वयं की क्वेरी डिज़ाइन करेगा या नहीं, एजेंटिक RAG दृष्टिकोण की परिभाषाओं में से एक है.

हम इसे न सिर्फ़ अपनी क्वेरी बनाने की अनुमति देते हैं, बल्कि यह भी चुनने की अनुमति देते हैं कि इसे कितने काम के दस्तावेज़ों को वापस पाना है. उचित त्वरित इंजीनियरिंग के साथ संयुक्त, उदाहरणार्थ

# Example prompt

Always filter the result from tool
search_relevant_receipts_by_natural_language_query as the returned 
result may contain irrelevant information

इससे यह टूल एक शक्तिशाली टूल बन जाएगा जो लगभग किसी भी चीज़ को खोजने में सक्षम है, हालांकि निकटतम पड़ोसी खोज की गैर-सटीक प्रकृति के कारण यह सभी अपेक्षित परिणाम नहीं लौटा सकता है.

5. 🚀 कॉलबैक के माध्यम से वार्तालाप संदर्भ संशोधन

गूगल ADK हमें विभिन्न स्तरों पर एजेंट रनटाइम को "अवरोधित" करने में सक्षम बनाता है. आप इस विस्तृत क्षमता के बारे में इस दस्तावेज़ में अधिक पढ़ सकते हैं . इस लैब में, हम पुराने वार्तालाप इतिहास संदर्भ में छवि डेटा को हटाने के लिए LLM को भेजे जाने से पहले अनुरोध को संशोधित करने के लिए before_model_callback का उपयोग करते हैं ( केवल अंतिम 3 उपयोगकर्ता इंटरैक्शन में छवि डेटा शामिल करें) दक्षता के लिए

हालाँकि, हम अभी भी चाहते हैं कि आवश्यकता पड़ने पर एजेंट के पास छवि डेटा संदर्भ हो. इसलिए हम वार्तालाप में प्रत्येक छवि बाइट डेटा के बाद एक स्ट्रिंग छवि आईडी प्लेसहोल्डर जोड़ने के लिए एक तंत्र जोड़ते हैं. इससे एजेंट को छवि आईडी को उसके वास्तविक फ़ाइल डेटा से लिंक करने में मदद मिलेगी जिसका उपयोग छवि भंडारण या पुनर्प्राप्ति समय दोनों में किया जा सकता है. संरचना इस तरह दिखेगी

<image-byte-data-1>
[IMAGE-ID <hash-of-image-1>]
<image-byte-data-2>
[IMAGE-ID <hash-of-image-2>]
And so on..

और जब वार्तालाप इतिहास में बाइट डेटा अप्रचलित हो रहा हो, तब भी स्ट्रिंग पहचानकर्ता उपकरण उपयोग की सहायता से डेटा तक पहुंच को सक्षम करने के लिए मौजूद रहता है. छवि डेटा हटाए जाने के बाद इतिहास संरचना का उदाहरण

[IMAGE-ID <hash-of-image-1>]
[IMAGE-ID <hash-of-image-2>]
And so on..

आइए प्रारंभ करें! expense_manager_agent निर्देशिका में एक नई फ़ाइल बनाएँ और उसका नाम callbacks.py रखें

touch expense_manager_agent/callbacks.py

expense_manager_agent/callbacks.py फ़ाइल खोलें, फिर नीचे दिए गए कोड को कॉपी करें

# expense_manager_agent/callbacks.py

import hashlib
from google.genai import types
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest


def modify_image_data_in_history(
    callback_context: CallbackContext, llm_request: LlmRequest
) -> None:
    # The following code will modify the request sent to LLM
    # We will only keep image data in the last 3 user messages using a reverse and counter approach

    # Count how many user messages we've processed
    user_message_count = 0

    # Process the reversed list
    for content in reversed(llm_request.contents):
        # Only count for user manual query, not function call
        if (content.role == "user") and (content.parts[0].function_response is None):
            user_message_count += 1
            modified_content_parts = []

            # Check any missing image ID placeholder for any image data
            # Then remove image data from conversation history if more than 3 user messages
            for idx, part in enumerate(content.parts):
                if part.inline_data is None:
                    modified_content_parts.append(part)
                    continue

                if (
                    (idx + 1 >= len(content.parts))
                    or (content.parts[idx + 1].text is None)
                    or (not content.parts[idx + 1].text.startswith("[IMAGE-ID "))
                ):
                    # Generate hash ID for the image and add a placeholder
                    image_data = part.inline_data.data
                    hasher = hashlib.sha256(image_data)
                    image_hash_id = hasher.hexdigest()[:12]
                    placeholder = f"[IMAGE-ID {image_hash_id}]"

                    # Only keep image data in the last 3 user messages
                    if user_message_count <= 3:
                        modified_content_parts.append(part)

                    modified_content_parts.append(types.Part(text=placeholder))

                else:
                    # Only keep image data in the last 3 user messages
                    if user_message_count <= 3:
                        modified_content_parts.append(part)

            # This will modify the contents inside the llm_request
            content.parts = modified_content_parts

6. 🚀 प्रॉम्प्ट

जटिल अंतःक्रिया और क्षमताओं वाले एजेंट को डिजाइन करने के लिए, हमें एजेंट को मार्गदर्शन देने के लिए एक अच्छा संकेत खोजने की आवश्यकता होती है, ताकि वह उस तरह से व्यवहार कर सके जैसा हम चाहते हैं.

पहले हमारे पास बातचीत के इतिहास में इमेज डेटा को संभालने का एक तरीका था, और हमारे पास ऐसे टूल भी थे जिनका इस्तेमाल करना आसान नहीं था, जैसे search_relevant_receipts_by_natural_language_query. हम यह भी चाहते हैं कि एजेंट सही रसीद इमेज खोजकर हमें दे सके. इसका मतलब यह है कि हमें यह सारी जानकारी उचित प्रॉम्प्ट संरचना में सही ढंग से बतानी होगी.

हम एजेंट से कहेंगे कि वह आउटपुट को इस मार्कडाउन फ़ॉर्मैट में स्ट्रक्चर करे, ताकि सोचने की प्रोसेस, फ़ाइनल जवाब, और अटैचमेंट ( अगर कोई हो) को पार्स किया जा सके

# THINKING PROCESS

Thinking process here

# FINAL RESPONSE

Response to the user here

Attachments put inside json block

{
    "attachments": [
      "[IMAGE-ID <hash-id-1>]",
      "[IMAGE-ID <hash-id-2>]",
      ...
    ]
}

आइए व्यय प्रबंधक एजेंट व्यवहार की हमारी प्रारंभिक अपेक्षा को प्राप्त करने के लिए निम्नलिखित संकेत से शुरुआत करें. task_prompt.md फ़ाइल हमारी मौजूदा कार्यशील निर्देशिका में पहले से मौजूद होनी चाहिए, लेकिन हमें इसे expense_manager_agent निर्देशिका में ले जाना होगा. इसे स्थानांतरित करने के लिए निम्न आदेश चलाएँ

mv task_prompt.md expense_manager_agent/task_prompt.md

7. 🚀 एजेंट का परीक्षण

अब आइए CLI के माध्यम से एजेंट के साथ संवाद करने का प्रयास करें, निम्नलिखित कमांड चलाएँ

uv run adk run expense_manager_agent

यह इस तरह का आउटपुट दिखाएगा, जहां आप एजेंट के साथ बारी-बारी से चैट कर सकते हैं, हालांकि आप इस इंटरफ़ेस के माध्यम से केवल टेक्स्ट भेज सकते हैं

Log setup complete: /tmp/agents_log/agent.xxxx_xxx.log
To access latest log: tail -F /tmp/agents_log/agent.latest.log
Running agent root_agent, type exit to exit.
user: hello
[root_agent]: Hello there! How can I help you today?
user: 

अब सीएलआई इंटरैक्शन के अलावा, ADK हमें डेवलपमेंट यूज़र इंटरफ़ेस (यूआई) की सुविधा भी देता है. इसकी मदद से, हम इंटरैक्शन के दौरान क्या हो रहा है, यह देख सकते हैं और उससे इंटरैक्ट कर सकते हैं. लोकल डेवलपमेंट यूज़र इंटरफ़ेस (यूआई) सर्वर शुरू करने के लिए, यह कमांड चलाएं

uv run adk web --port 8080

इससे आपको इस उदाहरण जैसा आउटपुट मिलेगा. इसका मतलब है कि अब वेब इंटरफ़ेस को ऐक्सेस किया जा सकता है

INFO:     Started server process [xxxx]
INFO:     Waiting for application startup.

+-----------------------------------------------------------------------------+
| ADK Web Server started                                                      |
|                                                                             |
| For local testing, access at http://localhost:8080.                         |
+-----------------------------------------------------------------------------+

INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)

अब इसे देखने के लिए, Cloud Shell Editor के सबसे ऊपर मौजूद वेब प्रीव्यू बटन पर क्लिक करें. इसके बाद, पोर्ट 8080 पर प्रीव्यू करें को चुनें

edc73e971b9fc60c.png

आपको निम्न वेब पेज दिखाई देगा, जहां आप ऊपर बाईं ओर स्थित ड्रॉप डाउन बटन पर उपलब्ध एजेंट चुन सकते हैं ( हमारे मामले में यह expense_manager_agent होना चाहिए) और बॉट के साथ इंटरैक्ट कर सकते हैं. आपको बाईं ओर मौजूद विंडो में, एजेंट के रनटाइम के दौरान लॉग की जानकारी के बारे में कई तरह की जानकारी दिखेगी

16c333a4b782eeba.png

आइए, कुछ कार्रवाइयाँ करके देखते हैं! इन 2 उदाहरण रसीदों को अपलोड करें ( स्रोत : हगिंग फेस डेटासेट mousserlane/id_receipt_dataset) . प्रत्येक चित्र पर राइट क्लिक करें और चित्र को इस रूप में सहेजें.. चुनें. ( इससे रसीद की इमेज डाउनलोड हो जाएगी), फिर "क्लिप" आइकन पर क्लिक करके फ़ाइल को बॉट पर अपलोड करें और कहें कि आप इन रसीदों को स्टोर करना चाहते हैं

2975b3452e0ac0bd.png 143a2e147a18fc38.png

इसके बाद, कुछ खोजने या फ़ाइल वापस पाने के लिए, यहां दी गई क्वेरी आज़माएं

  • "साल 2023 के खर्चों और उनके कुल योग की जानकारी दो"
  • "मुझे इंडोमारेट से रसीद फ़ाइल दो"

जब आप कुछ उपकरणों का उपयोग करते हैं, तो आप यह जांच सकते हैं कि विकास UI में क्या हो रहा है

da461a67b7d81ad5.png

देखें कि एजेंट आपको कैसे जवाब देता है और जांचें कि क्या वह task_prompt.py के अंदर प्रॉम्प्ट में दिए गए सभी नियमों का अनुपालन करता है. बधाई हो! अब आपके पास एक पूर्ण कार्यशील विकास एजेंट है.

अब इसे पूरा करने का समय आ गया है. इसके लिए, आपको एक अच्छा यूज़र इंटरफ़ेस (यूआई) बनाना होगा. साथ ही, इमेज फ़ाइल को अपलोड और डाउनलोड करने की सुविधाएं देनी होंगी.

8. 🚀 ग्रैडियो का उपयोग करके फ्रंटएंड सेवा बनाएँ

हम एक चैट वेब इंटरफ़ेस बनाएंगे जो इस तरह दिखेगा

db9331886978d543.png

इसमें एक चैट इंटरफेस है जिसमें उपयोगकर्ताओं के लिए टेक्स्ट भेजने और रसीद छवि फ़ाइल(फ़ाइलों) को अपलोड करने हेतु एक इनपुट फ़ील्ड है.

हम फ़्रंटएंड सेवा को Gradio का इस्तेमाल करके बनाएंगे.

नई फ़ाइल बनाएं और उसका नाम frontend.py रखें

touch frontend.py

फिर निम्नलिखित कोड को कॉपी करें और इसे सेव करें

import mimetypes
import gradio as gr
import requests
import base64
from typing import List, Dict, Any
from settings import get_settings
from PIL import Image
import io
from schema import ImageData, ChatRequest, ChatResponse


SETTINGS = get_settings()


def encode_image_to_base64_and_get_mime_type(image_path: str) -> ImageData:
    """Encode a file to base64 string and get MIME type.

    Reads an image file and returns the base64-encoded image data and its MIME type.

    Args:
        image_path: Path to the image file to encode.

    Returns:
        ImageData object containing the base64 encoded image data and its MIME type.
    """
    # Read the image file
    with open(image_path, "rb") as file:
        image_content = file.read()

    # Get the mime type
    mime_type = mimetypes.guess_type(image_path)[0]

    # Base64 encode the image
    base64_data = base64.b64encode(image_content).decode("utf-8")

    # Return as ImageData object
    return ImageData(serialized_image=base64_data, mime_type=mime_type)


def decode_base64_to_image(base64_data: str) -> Image.Image:
    """Decode a base64 string to PIL Image.

    Converts a base64-encoded image string back to a PIL Image object
    that can be displayed or processed further.

    Args:
        base64_data: Base64 encoded string of the image.

    Returns:
        PIL Image object of the decoded image.
    """
    # Decode the base64 string and convert to PIL Image
    image_data = base64.b64decode(base64_data)
    image_buffer = io.BytesIO(image_data)
    image = Image.open(image_buffer)

    return image


def get_response_from_llm_backend(
    message: Dict[str, Any],
    history: List[Dict[str, Any]],
) -> List[str | gr.Image]:
    """Send the message and history to the backend and get a response.

    Args:
        message: Dictionary containing the current message with 'text' and optional 'files' keys.
        history: List of previous message dictionaries in the conversation.

    Returns:
        List containing text response and any image attachments from the backend service.
    """
    # Extract files and convert to base64
    image_data = []
    if uploaded_files := message.get("files", []):
        for file_path in uploaded_files:
            image_data.append(encode_image_to_base64_and_get_mime_type(file_path))

    # Prepare the request payload
    payload = ChatRequest(
        text=message["text"],
        files=image_data,
        session_id="default_session",
        user_id="default_user",
    )

    # Send request to backend
    try:
        response = requests.post(SETTINGS.BACKEND_URL, json=payload.model_dump())
        response.raise_for_status()  # Raise exception for HTTP errors

        result = ChatResponse(**response.json())
        if result.error:
            return [f"Error: {result.error}"]

        chat_responses = []

        if result.thinking_process:
            chat_responses.append(
                gr.ChatMessage(
                    role="assistant",
                    content=result.thinking_process,
                    metadata={"title": "🧠 Thinking Process"},
                )
            )

        chat_responses.append(gr.ChatMessage(role="assistant", content=result.response))

        if result.attachments:
            for attachment in result.attachments:
                image_data = attachment.serialized_image
                chat_responses.append(gr.Image(decode_base64_to_image(image_data)))

        return chat_responses
    except requests.exceptions.RequestException as e:
        return [f"Error connecting to backend service: {str(e)}"]


if __name__ == "__main__":
    demo = gr.ChatInterface(
        get_response_from_llm_backend,
        title="Personal Expense Assistant",
        description="This assistant can help you to store receipts data, find receipts, and track your expenses during certain period.",
        type="messages",
        multimodal=True,
        textbox=gr.MultimodalTextbox(file_count="multiple", file_types=["image"]),
    )

    demo.launch(
        server_name="0.0.0.0",
        server_port=8080,
    )

उसके बाद, हम निम्नलिखित कमांड के साथ फ्रंटएंड सेवा चलाने का प्रयास कर सकते हैं. main.py फ़ाइल का नाम बदलकर frontend.py करना न भूलें

uv run frontend.py

आपको अपने Cloud Console में, इससे मिलता-जुलता आउटपुट दिखेगा

* Running on local URL:  http://0.0.0.0:8080

To create a public link, set `share=True` in `launch()`.

इसके बाद आप स्थानीय URL लिंक पर ctrl+क्लिक करके वेब इंटरफ़ेस की जांच कर सकते हैं. इसके अलावा, Cloud Editor में सबसे ऊपर दाईं ओर मौजूद वेब प्रीव्यू बटन पर क्लिक करके भी फ़्रंटएंड ऐप्लिकेशन को ऐक्सेस किया जा सकता है. इसके बाद, पोर्ट 8080 पर प्रीव्यू करें को चुनें

b477bc3c686a5fc3.jpeg

आपको वेब इंटरफ़ेस दिखाई देगा, हालाँकि, बैकएंड सेवा अभी तक सेट अप नहीं होने के कारण चैट सबमिट करते समय आपको अपेक्षित त्रुटि मिलेगी

b5de2f284155dac2.png

अब, सेवा को चलने दें और इसे अभी बंद न करें. हम बैकएंड सेवा को दूसरे टर्मिनल टैब में चलाएंगे

कोड स्पष्टीकरण

इस फ्रंटएंड कोड में, सबसे पहले हम उपयोगकर्ता को टेक्स्ट भेजने और कई फाइलें अपलोड करने में सक्षम बनाते हैं. ग्रैडियो हमें gr.ChatInterface विधि को gr.MultimodalTextbox के साथ मिलाकर इस प्रकार की कार्यक्षमता बनाने की अनुमति देता है

अब फ़ाइल और टेक्स्ट को बैकएंड पर भेजने से पहले, हमें फ़ाइल के MIME प्रकार का पता लगाना होगा क्योंकि बैकएंड को इसकी आवश्यकता है. हमें छवि फ़ाइल बाइट को बेस64 में एनकोड करना होगा और इसे माइमटाइप के साथ भेजना होगा.

class ImageData(BaseModel):
    """Model for image data with hash identifier.

    Attributes:
        serialized_image: Optional Base64 encoded string of the image content.
        mime_type: MIME type of the image.
    """

    serialized_image: str
    mime_type: str

फ़्रंटएंड और बैकएंड के बीच इंटरैक्शन के लिए इस्तेमाल किया गया स्कीमा, schema.py में तय किया गया है. हम स्कीमा में डेटा सत्यापन लागू करने के लिए Pydantic BaseModel का उपयोग करते हैं

प्रतिक्रिया प्राप्त करते समय, हम पहले से ही अलग कर लेते हैं कि कौन सा भाग विचार प्रक्रिया, अंतिम प्रतिक्रिया और लगाव है. इस प्रकार हम प्रत्येक घटक को UI घटक के साथ प्रदर्शित करने के लिए ग्रैडियो घटक का उपयोग कर सकते हैं.

class ChatResponse(BaseModel):
    """Model for a chat response.

    Attributes:
        response: The text response from the model.
        thinking_process: Optional thinking process of the model.
        attachments: List of image data to be displayed to the user.
        error: Optional error message if something went wrong.
    """

    response: str
    thinking_process: str = ""
    attachments: List[ImageData] = []
    error: Optional[str] = None

9. 🚀 FastAPI का उपयोग करके बैकएंड सेवा बनाएँ

इसके बाद, हमें बैकएंड का निर्माण करना होगा जो एजेंट रनटाइम को निष्पादित करने में सक्षम होने के लिए अन्य घटकों के साथ हमारे एजेंट को आरंभ कर सके.

नई फ़ाइल बनाएँ और उसका नाम backend.py रखें

touch backend.py

और निम्नलिखित कोड को कॉपी करें

from expense_manager_agent.agent import root_agent as expense_manager_agent
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.adk.events import Event
from fastapi import FastAPI, Body, Depends
from typing import AsyncIterator
from types import SimpleNamespace
import uvicorn
from contextlib import asynccontextmanager
from utils import (
    extract_attachment_ids_and_sanitize_response,
    download_image_from_gcs,
    extract_thinking_process,
    format_user_request_to_adk_content_and_store_artifacts,
)
from schema import ImageData, ChatRequest, ChatResponse
import logger
from google.adk.artifacts import GcsArtifactService
from settings import get_settings

SETTINGS = get_settings()
APP_NAME = "expense_manager_app"


# Application state to hold service contexts
class AppContexts(SimpleNamespace):
    """A class to hold application contexts with attribute access"""

    session_service: InMemorySessionService = None
    artifact_service: GcsArtifactService = None
    expense_manager_agent_runner: Runner = None


# Initialize application state
app_contexts = AppContexts()


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Initialize service contexts during application startup
    app_contexts.session_service = InMemorySessionService()
    app_contexts.artifact_service = GcsArtifactService(
        bucket_name=SETTINGS.STORAGE_BUCKET_NAME
    )
    app_contexts.expense_manager_agent_runner = Runner(
        agent=expense_manager_agent,  # The agent we want to run
        app_name=APP_NAME,  # Associates runs with our app
        session_service=app_contexts.session_service,  # Uses our session manager
        artifact_service=app_contexts.artifact_service,  # Uses our artifact manager
    )

    logger.info("Application started successfully")
    yield
    logger.info("Application shutting down")
    # Perform cleanup during application shutdown if necessary


# Helper function to get application state as a dependency
async def get_app_contexts() -> AppContexts:
    return app_contexts


# Create FastAPI app
app = FastAPI(title="Personal Expense Assistant API", lifespan=lifespan)


@app.post("/chat", response_model=ChatResponse)
async def chat(
    request: ChatRequest = Body(...),
    app_context: AppContexts = Depends(get_app_contexts),
) -> ChatResponse:
    """Process chat request and get response from the agent"""

    # Prepare the user's message in ADK format and store image artifacts
    content = await format_user_request_to_adk_content_and_store_artifacts(
        request=request,
        app_name=APP_NAME,
        artifact_service=app_context.artifact_service,
    )

    final_response_text = "Agent did not produce a final response."  # Default

    # Use the session ID from the request or default if not provided
    session_id = request.session_id
    user_id = request.user_id

    # Create session if it doesn't exist
    if not await app_context.session_service.get_session(
        app_name=APP_NAME, user_id=user_id, session_id=session_id
    ):
        await app_context.session_service.create_session(
            app_name=APP_NAME, user_id=user_id, session_id=session_id
        )

    try:
        # Process the message with the agent
        # Type annotation: runner.run_async returns an AsyncIterator[Event]
        events_iterator: AsyncIterator[Event] = (
            app_context.expense_manager_agent_runner.run_async(
                user_id=user_id, session_id=session_id, new_message=content
            )
        )
        async for event in events_iterator:  # event has type Event
            # Key Concept: is_final_response() marks the concluding message for the turn
            if event.is_final_response():
                if event.content and event.content.parts:
                    # Extract text from the first part
                    final_response_text = event.content.parts[0].text
                elif event.actions and event.actions.escalate:
                    # Handle potential errors/escalations
                    final_response_text = f"Agent escalated: {event.error_message or 'No specific message.'}"
                break  # Stop processing events once the final response is found

        logger.info(
            "Received final response from agent", raw_final_response=final_response_text
        )

        # Extract and process any attachments and thinking process in the response
        base64_attachments = []
        sanitized_text, attachment_ids = extract_attachment_ids_and_sanitize_response(
            final_response_text
        )
        sanitized_text, thinking_process = extract_thinking_process(sanitized_text)

        # Download images from GCS and replace hash IDs with base64 data
        for image_hash_id in attachment_ids:
            # Download image data and get MIME type
            result = await download_image_from_gcs(
                artifact_service=app_context.artifact_service,
                image_hash=image_hash_id,
                app_name=APP_NAME,
                user_id=user_id,
                session_id=session_id,
            )
            if result:
                base64_data, mime_type = result
                base64_attachments.append(
                    ImageData(serialized_image=base64_data, mime_type=mime_type)
                )

        logger.info(
            "Processed response with attachments",
            sanitized_response=sanitized_text,
            thinking_process=thinking_process,
            attachment_ids=attachment_ids,
        )

        return ChatResponse(
            response=sanitized_text,
            thinking_process=thinking_process,
            attachments=base64_attachments,
        )

    except Exception as e:
        logger.error("Error processing chat request", error_message=str(e))
        return ChatResponse(
            response="", error=f"Error in generating response: {str(e)}"
        )


# Only run the server if this file is executed directly
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8081)

उसके बाद हम बैकएंड सेवा चलाने का प्रयास कर सकते हैं. याद रखें कि पिछले चरण में हमने फ्रंटएंड सेवा को सही तरीके से चलाया था, अब हमें नया टर्मिनल खोलना होगा और इस बैकएंड सेवा को चलाने का प्रयास करना होगा

  1. एक नया टर्मिनल बनाएं. नीचे के क्षेत्र में अपने टर्मिनल पर जाएँ और नया टर्मिनल बनाने के लिए "+" बटन ढूंढें. वैकल्पिक रूप से, आप नया टर्मिनल खोलने के लिए Ctrl + Shift + C दबा सकते हैं

235e2f9144d82803.jpeg

  1. इसके बाद, पक्का करें कि आप वर्किंग डायरेक्ट्री personal-expense-assistant में हों. इसके बाद, यह कमांड चलाएँ
uv run backend.py
  1. यदि सफल रहा, तो यह इस तरह आउटपुट दिखाएगा
INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)

कोड स्पष्टीकरण

ADK एजेंट, सेशन सर्विस और आर्टिफैक्ट सर्विस को इनिशियलाइज़ करना

बैकएंड सेवा में एजेंट को चलाने के लिए हमें एक रनर बनाना होगा जो SessionService और हमारे एजेंट दोनों को ले सके. SessionService बातचीत के इतिहास और स्थिति का प्रबंधन करेगा, इसलिए जब इसे Runner के साथ एकीकृत किया जाएगा, तो यह हमारे एजेंट को चल रही बातचीत का संदर्भ प्राप्त करने की क्षमता प्रदान करेगा.

हम अपलोड की गई फ़ाइल को संभालने के लिए ArtifactService का भी उपयोग करते हैं. आप ADK सत्र और आर्टिफ़ैक्ट्स के बारे में अधिक जानकारी यहाँ पढ़ सकते हैं

...

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Initialize service contexts during application startup
    app_contexts.session_service = InMemorySessionService()
    app_contexts.artifact_service = GcsArtifactService(
        bucket_name=SETTINGS.STORAGE_BUCKET_NAME
    )
    app_contexts.expense_manager_agent_runner = Runner(
        agent=expense_manager_agent,  # The agent we want to run
        app_name=APP_NAME,  # Associates runs with our app
        session_service=app_contexts.session_service,  # Uses our session manager
        artifact_service=app_contexts.artifact_service,  # Uses our artifact manager
    )

    logger.info("Application started successfully")
    yield
    logger.info("Application shutting down")
    # Perform cleanup during application shutdown if necessary

...

इस डेमो में, हमने InMemorySessionService और GcsArtifactService का इस्तेमाल किया है, ताकि इन्हें हमारे एजेंट Runner के साथ इंटिग्रेट किया जा सके. बातचीत का इतिहास मेमोरी में सेव होता है. इसलिए, बैकएंड सेवा बंद होने या फिर से शुरू होने पर, यह मिट जाएगा. हम इन्हें FastAPI ऐप्लिकेशन के लाइफ़साइकल में शुरू करते हैं, ताकि इन्हें /chat रूट में डिपेंडेंसी के तौर पर इंजेक्ट किया जा सके.

GcsArtifactService की मदद से इमेज अपलोड और डाउनलोड करना

अपलोड की गई सभी इमेज को GcsArtifactService, आर्टफ़ैक्ट के तौर पर सेव करेगा. इसे utils.py में मौजूद format_user_request_to_adk_content_and_store_artifacts फ़ंक्शन में देखा जा सकता है

...    

# Prepare the user's message in ADK format and store image artifacts
content = await asyncio.to_thread(
    format_user_request_to_adk_content_and_store_artifacts,
    request=request,
    app_name=APP_NAME,
    artifact_service=app_context.artifact_service,
)

...

एजेंट रनर जिन अनुरोधों को प्रोसेस करेगा उन्हें types.Content टाइप में फ़ॉर्मैट किया जाना चाहिए. फ़ंक्शन के अंदर, हम हर इमेज डेटा को प्रोसेस करते हैं और उसके आईडी को निकालते हैं, ताकि उसे इमेज आईडी प्लेसहोल्डर से बदला जा सके.

रेगेक्स का उपयोग करके छवि आईडी निकालने के बाद अनुलग्नकों को डाउनलोड करने के लिए समान तंत्र का उपयोग किया जाता है:

...
sanitized_text, attachment_ids = extract_attachment_ids_and_sanitize_response(
    final_response_text
)
sanitized_text, thinking_process = extract_thinking_process(sanitized_text)

# Download images from GCS and replace hash IDs with base64 data
for image_hash_id in attachment_ids:
    # Download image data and get MIME type
    result = await asyncio.to_thread(
        download_image_from_gcs,
        artifact_service=app_context.artifact_service,
        image_hash=image_hash_id,
        app_name=APP_NAME,
        user_id=user_id,
        session_id=session_id,
    )
...

10. 🚀 इंटिग्रेशन टेस्ट

अब, आपके पास विभिन्न क्लाउड कंसोल टैब में चलने वाली कई सेवाएँ होनी चाहिए:

  • पोर्ट 8080 पर फ्रंटएंड सेवा चलती है
* Running on local URL:  http://0.0.0.0:8080

To create a public link, set `share=True` in `launch()`.
  • बैकएंड सेवा पोर्ट 8081 पर चलती है
INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)

वर्तमान स्थिति में, आप अपनी रसीद की तस्वीरें अपलोड करने और पोर्ट 8080 पर वेब एप्लिकेशन से सहायक के साथ सहजता से चैट करने में सक्षम होंगे.

अपने क्लाउड शेल एडिटर के ऊपरी हिस्से में वेब पूर्वावलोकन बटन पर क्लिक करें और पोर्ट 8080 पर पूर्वावलोकन करें चुनें

edc73e971b9fc60c.png

अब आइये सहायक के साथ कुछ बातचीत करें!

निम्नलिखित रसीदें डाउनलोड करें. इन रसीद डेटा की तिथि सीमा वर्ष 2023-2024 के बीच है और सहायक से इसे संग्रहीत/अपलोड करने के लिए कहें

  • रसीद ड्राइव ( स्रोत: गले लगाने वाले चेहरे के डेटासेट mousserlane/id_receipt_dataset)

विभिन्न बातें पूछें

  • "मुझे 2023-2024 के दौरान मासिक व्यय का विवरण दीजिए"
  • "मुझे कॉफ़ी लेन-देन की रसीद दिखाओ"
  • "मुझे याकिनिकु लाइक से रसीद फ़ाइल दो"
  • वगैरह

यहां बातचीत के कुछ स्निपेट दिए गए हैं

e01dc7a8ec673aa4.png

9341212f8d54c98a.png

11. 🚀 Cloud Run पर डिप्लॉय किया जा रहा है

अब, निश्चित रूप से हम इस अद्भुत ऐप को कहीं से भी एक्सेस करना चाहते हैं. ऐसा करने के लिए, हम इस एप्लिकेशन को पैकेज कर सकते हैं और इसे क्लाउड रन पर तैनात कर सकते हैं. इस डेमो के लिए, इस सेवा को सार्वजनिक सेवा के तौर पर दिखाया जाएगा. इसे अन्य लोग ऐक्सेस कर सकते हैं. हालांकि, ध्यान रखें कि इस तरह के ऐप्लिकेशन के लिए यह सबसे सही तरीका नहीं है, क्योंकि यह निजी ऐप्लिकेशन के लिए ज़्यादा सही है

90805d85052a5e5a.jpeg

इस कोडलैब में, हम फ्रंटएंड और बैकएंड दोनों सेवाओं को एक कंटेनर में रखेंगे. दोनों सेवाओं को प्रबंधित करने के लिए हमें पर्यवेक्षक की सहायता की आवश्यकता होगी. आप supervisord.conf फ़ाइल का निरीक्षण कर सकते हैं और Dockerfile की जांच कर सकते हैं कि हमने supervisord को एंट्रीपॉइंट के रूप में सेट किया है.

इस समय, हमारे पास Cloud Run पर ऐप्लिकेशन डिप्लॉय करने के लिए ज़रूरी सभी फ़ाइलें मौजूद हैं. इसलिए, अब हम इसे डिप्लॉय करते हैं. Cloud Shell टर्मिनल पर जाएं और पक्का करें कि मौजूदा प्रोजेक्ट, आपके एक्टिव प्रोजेक्ट के लिए कॉन्फ़िगर किया गया हो. अगर ऐसा नहीं है, तो प्रोजेक्ट आईडी सेट करने के लिए, gcloud configure कमांड का इस्तेमाल करें:

gcloud config set project [PROJECT_ID]

फिर, इसे क्लाउड रन पर तैनात करने के लिए निम्नलिखित कमांड चलाएँ.

gcloud run deploy personal-expense-assistant \
                  --source . \
                  --port=8080 \
                  --allow-unauthenticated \
                  --env-vars-file=settings.yaml \
                  --memory 1024Mi \
                  --region us-central1

अगर आपको Docker इमेज के लिए आर्टफ़ैक्ट रजिस्ट्री बनाने की पुष्टि करने के लिए कहा जाता है, तो बस Y जवाब दें. ध्यान दें कि हम यहां बिना पुष्टि किए ऐक्सेस करने की अनुमति दे रहे हैं, क्योंकि यह एक डेमो ऐप्लिकेशन है. हमारा सुझाव है कि आप अपने एंटरप्राइज़ और प्रोडक्शन ऐप्लिकेशन के लिए, पुष्टि करने के सही तरीके का इस्तेमाल करें.

डप्लॉयमेंट पूरा होने के बाद, आपको यहां दिए गए लिंक जैसा लिंक मिलेगा:

https://personal-expense-assistant-*******.us-central1.run.app

अब गुप्त विंडो या अपने मोबाइल डिवाइस से ऐप्लिकेशन का इस्तेमाल करें. यह पहले से लाइव होना चाहिए.

12. 🎯 चैलेंज

अब आपको अपनी खोज करने की क्षमता को बेहतर बनाने का मौका मिला है. क्या आपके पास कोड में बदलाव करने के लिए ज़रूरी जानकारी है, ताकि बैकएंड एक से ज़्यादा उपयोगकर्ताओं को शामिल कर सके? किन कॉम्पोनेंट को अपडेट करना है?

13. 🧹 सफाई

इस कोडलैब में इस्तेमाल किए गए संसाधनों के लिए, अपने Google Cloud खाते से शुल्क न लिए जाने के लिए, यह तरीका अपनाएं:

  1. Google Cloud Console में, संसाधन मैनेज करें पेज पर जाएं.
  2. प्रोजेक्ट की सूची में, वह प्रोजेक्ट चुनें जिसे आपको मिटाना है. इसके बाद, मिटाएं पर क्लिक करें.
  3. डायलॉग बॉक्स में, प्रोजेक्ट आईडी डालें. इसके बाद, प्रोजेक्ट मिटाने के लिए बंद करें पर क्लिक करें.
  4. इसके अलावा, कंसोल पर Cloud Run पर जाकर, अभी-अभी डिप्लॉय की गई सेवा को चुनें और उसे मिटाएं.