![[GCP Practical] LINE Business Card Bot](https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F51ixx8tpbh4lc7167hqw.png)
Upgrade Preamble After refactoring the agent based on Vertex AI ADK, our LINE Name Card...

After refactoring the agent based on Vertex AI ADK, our LINE Name Card Assistant Bot (linebot-namecard-python) entered the production environment for testing. However, in real-world usage scenarios, we quickly identified three core pain points affecting user experience and security:
This article will focus on sharing how we conducted a second wave of upgrades to address the above pain points, implementing Structured Outputs, Disambiguation Lists, Two-Stage Confirmation Mechanism, and the major pitfall we encountered during operations and deployment regarding environment variable recovery!
Previously, when calling gemini-3-flash-preview for name card image parsing, we commanded it via Prompt and manually parsed JSON. To ensure 100% format guarantee, we introduced the native Structured Outputs feature of the Vertex AI API.
In app/gemini_utils.py, we defined the constraint Schema for the name card object, forcing Gemini to strictly adhere to this format for output:
NAMECARD_SCHEMA = {
"type": "OBJECT",
"properties": {
"name": {
"type": "STRING",
"description": "聯絡人姓名,如果看不出來,請填寫 N/A"
},
"title": {
"type": "STRING",
"description": "職稱或頭銜,如果看不出來,請填寫 N/A"
},
"company": {
"type": "STRING",
"description": "公司名稱,如果看不出來,請填寫 N/A"
},
"address": {
"type": "STRING",
"description": "公司或聯絡地址,如果看不出來,請填寫 N/A"
},
"phone": {
"type": "STRING",
"description": (
"電話號碼,格式為 #886-0123-456-789,1234。"
"沒有分機就忽略 ,1234。如果看不出來,請填寫 N/A"
)
},
"email": {
"type": "STRING",
"description": "電子郵件信箱,如果看不出來,請填寫 N/A"
}
},
"required": ["name", "title", "company", "address", "phone", "email"]
}
We only need to specify response_schema in generation_config when instantiating GenerativeModel:
def generate_json_from_image(img: PIL.Image.Image, prompt: str) -> object:
model = GenerativeModel(
"gemini-3-flash-preview",
generation_config={
"response_mime_type": "application/json",
"response_schema": NAMECARD_SCHEMA
},
)
img_part = Part.from_data(data=pil_to_bytes(img), mime_type="image/jpeg")
response = model.generate_content([prompt, img_part], stream=False)
return response
After application, the JSON error rate of the returned response dropped directly to 0%, eliminating complex string cleaning and parser error-prevention logic.
LINE Webhook has an iron rule: the number of message bubbles sent in a single reply_message must be between 1 and 5. If the search results happen to be 5 or more, and a text reply is added, the total will exceed 5, triggering a LINE API 400 error.
We modified the search reply judgment in app/line_handlers.py:
This design not only maintains a clean layout but also completely avoids the pitfall of exceeding the message limit!
elif found_card_ids:
if len(found_card_ids) <= 4:
# If the quantity is less than or equal to 4, directly display Carousel detailed name cards
for card_id in found_card_ids:
card_data = firebase_utils.get_card_by_id(user_id, card_id)
if card_data:
reply_msgs.append(
flex_messages.get_namecard_flex_msg(card_data, card_id)
)
else:
# If the quantity is greater than 4, display as a list Flex Message for disambiguation
cards_list = []
for card_id in found_card_ids:
card_data = firebase_utils.get_card_by_id(user_id, card_id)
if card_data:
cards_list.append({
"card_id": card_id,
"name": card_data.get("name", "N/A"),
"company": card_data.get("company", "N/A"),
"title": card_data.get("title", "N/A")
})
if cards_list:
list_msg = flex_messages.get_namecard_list_flex_msg(
cards=cards_list,
title_text="🔍 Found multiple matching name cards"
)
reply_msgs.append(list_msg)

Under the ADK agent architecture, users can update data through natural conversation (e.g., "Add 'Meeting next Monday' to Evan's memo"). However, if the LLM misinterprets the instruction, Firebase data can be directly overwritten.
To address this, we implemented a Two-Stage Confirmation mechanism:
update_namecard_field and update_namecard_memo) is invoked by the model, the system does not directly rewrite Firebase. Instead, it temporarily stores the content to be modified in user_states in memory and returns True to allow the Agent to continue generating dialogue.action=confirm_update) does the system truly write the data to Firebase.This not only perfectly prevents AI from accidentally triggering tools but also gives users absolute control when modifying data!
# Handle confirmation of modification in handle_postback_event
elif action == 'confirm_update':
state = user_states.get(user_id, {})
if state.get('action') == 'pending_update':
update_type = state.get('update_type')
card_id = state.get('card_id')
# Read data from temporary storage based on update_type, and truly write to Firebase...
if success:
# Reply with successful modification, and automatically display the updated Flex Card for user verification
In addition to code refactoring, we also encountered a significant operational pitfall during deployment.
When we attempted to upload a local folder to Cloud Run using the MCP deployment tool locally, because the command did not include environment variable declaration parameters, the previously working LINE Token and Firebase URL on Cloud Run were all cleared and overwritten. Upon restart, the Container crashed directly with an error:
Specify ChannelSecret as environment variable.
The online service instantly became paralyzed.
Fortunately, Cloud Run fully retains the configuration settings of older versions. We can use the gcloud command to view previous Revisions and restore the lost variables:
gcloud run revisions describe linebot-namecard-python-00096-d89 --project=line-vertex --region=asia-east1
This will output the environment variable values bound to that version.
gcloud run services update linebot-namecard-python --project=line-vertex --region=asia-east1 --set-env-vars="ChannelAccessToken=...,ChannelSecret=..."
By restoring the variables, we seamlessly recovered the service within minutes. This also reminds us: when manually deploying to Cloud Run, always pay extra attention to the inheritance or declaration of environment variables to avoid accidentally clearing the official cloud configuration.
This optimization brought excellent production-level transformations to our LINE Name Card Bot:
The complete and linter-optimized code has been pushed to GitHub. We hope this practical experience helps everyone avoid detours when building production-grade AI Agents! See you next time!
aiMost of us have seen a coding agent fail to complete a task we know it can do. We just don't...
googlecloudWhen building Generative AI applications, developers often encounter a massive bottleneck: sequential...
discussI’ve been thinking about sharing some electronic circuit posts on Dev.to — small circuits, DIY...
agentsWhat nobody tells you about exporting your multi-agent prototype to a local workspace. Every...
agenticarchitectAutonomous agents are genuinely good at answering messy business questions. Give one an LLM and a set...
aiPR volume went up, ticket quality didn't, and the gap got filled with LLMs on both sides of the review: bots reviewing, bots replying, bots occasionally arguing with bots about priorities that only existed in a teammate's head. Our CEO named the actual problem, and it's bigger than code review.
Workflows from the Neura Market marketplace related to this Stable Diffusion resource