[Gemini API in Action] Adding a "Detailed Research Report" Button to a LINE Bot: Using Google Search Grounding to Turn Summaries A developer has enhanced a LINE bot with a 'Detailed Research Report' button powered by Google Search Grounding in the Gemini API. The feature lets the model autonomously search for external context, counter-arguments, and source verification, returning citations for transparency. The implementation is open-sourced in the linebot-helper-python repository. My LINE Bot has always had a summary feature: you drop a URL in, it crawls the content, generates a summary, and attaches a social media post draft along with a button to save it as a bookmark. This feature has been around since 2024, but it has always only solved the "what is this about" problem. I often find myself wanting to know three other things: What is the background context of the things discussed in this article? Have there been counter-arguments from others? Are the numbers mentioned sourced, or are they just the author's own claims? Summaries can't answer these because the input for a summary is only the article itself. The model has no other materials; if you ask it for a "critical analysis," it can only circle around the original text or start hallucinating. Google Search Grounding fills exactly this gap. I used it as a search assistant in a previous article https://dev.to/evanlin/gemini-30google-search-building-a-news-and-information-assistant-with-google-search-grounding-36hp ; back then, the purpose was to answer questions. This time, I wanted to try another approach: give an existing article to the model, let it search for information outside the article on its own, and then look back to review the article. The result is a new "📄 Detailed Research Report" button on the summary card. About one to two minutes after clicking it, the Bot pushes a web link to you. Main Repo: https://github.com/kkdai/linebot-helper-python https://github.com/kkdai/linebot-helper-python Before Grounding, to let a model read real-time information from the web, you had to build a pipeline yourself: first, ask the model to extract keywords from the article, use those keywords to call a search API, crawl the search result pages one by one, stuff them into the prompt, and then ask the model to summarize. This involves three or more API calls, any of which could fail, and the quality of the extracted keywords directly determines whether the retrieved information is useful. Grounding integrates this entire process into the model. You simply attach a google search tool in the GenerateContentConfig , and the model handles the rest: it decides whether to search, what to search for, how many times to search, and judges which results are worth using. For the "Research Report" topic, the model deciding what to search for is particularly valuable. When writing the prompt, I don't know what article the user will provide, so I naturally can't write the specific keywords to search. But after the model reads the article, it knows; it will look for the context of the topic and check if there are opposing views. Another advantage I care about is that citations are returned . The grounding metadata in the model's response contains the actual web pages it referenced, including titles and URLs. This means phrases like "according to other reports" in the report aren't just the model speaking from memory; there are corresponding web pages you can click to verify. For information-based products, this makes a huge difference. The code to extract sources is in loader/langtools.py , written defensively because these fields don't exist at all if no search was triggered: php def extract grounding sources response - list: """Extract citations from grounding metadata same approach as chat session .""" sources = try: if getattr response, 'candidates', None : candidate = response.candidates 0 metadata = getattr candidate, 'grounding metadata', None chunks = getattr metadata, 'grounding chunks', None if metadata else None for chunk in chunks or : web = getattr chunk, 'web', None if web: sources.append { 'title': getattr web, 'title', '' or '', 'uri': getattr web, 'uri', '' or '', } except Exception as e: logging.warning f"Failed to extract grounding sources: {e}" return sources The entire flow starts from the button on the summary card, goes through a re-crawl and a grounding call, and ends with a temporary webpage. php graph TD A User sends URL -- |Summary Flex Bubble| B 📄 Detailed Research Report Button B -- |Postback with bookmark doc id| C Verify bookmark ownership C -- |Immediate Reply: Researching| D LINE Chatroom C -- |Background Task| E load url: Re-crawl original text E -- F Gemini + Google Search Grounding F -- |Markdown + Citations| G render report page to HTML G -- |Store in memory ReportStore| H Get uuid report id H -- |Push Link| I GET /reports/:id Temporary Webpage The button carries the bookmark's document ID, not the URL itself. This follows the existing "Save Bookmark" mechanism. The benefit is that using the doc ID allows verifying that the bookmark actually belongs to the user before generating the report. Conversely, if Firestore isn't connected or the doc ID can't be retrieved, this button won't appear. The key to generate research report isn't the code, but the prompt. I explicitly ask the model to search proactively and require it to label which information comes from the search and which comes from the original text: prompt = f"""You are a rigorous research analyst. Please write a detailed research report based on the following article content, in Traditional Chinese Taiwan usage , Markdown format starting from level, do not include the main article title . Required Structure: Executive Summary 3-5 sentences explaining what this is about and why it matters Background Context The history and context of this topic, combined with relevant information you searched for Core Arguments & Evidence Organize the article's claims and supporting evidence point by point, labeling the strength of evidence Data & Fact Summary Key numbers, dates, people, and organizations from the text, using tables or lists Counter-perspectives & Critique Search for related reports, compare other viewpoints; point out blind spots, assumptions, or controversies in the article Further Questions 3-5 questions worth investigating further Requirements: - Please proactively search for supplementary background and comparative information outside the article, and label in the text whether the information comes from search or the original text. - Be specific rather than abstract; clearly label unsupported inferences as "speculation". - Use full-width punctuation, avoid AI-sounding clichés. Original URL: {url} Article Content: {text}""" The phrases "label the strength of evidence" and "clearly label unsupported inferences as speculation" are the parts of the prompt I care about most. Without them, every sentence in the report would sound equally confident, and the reader wouldn't be able to distinguish what the article said, what the model added from search results, and what it inferred itself. The part for attaching the tool is very short; whether tools is provided or not is the difference between having grounding or not: python def call with grounding: bool : client = get vertex client tools = types.Tool google search=types.GoogleSearch if with grounding else None return client.models.generate content model="gemini-3.1-flash-lite", contents=prompt, config=types.GenerateContentConfig temperature=0.4, tools=tools, max output tokens=16384, labels={"client id": "info helper"}, try: try: response = call with grounding=True except Exception as e: logging.warning f"Grounded research call failed, retrying without tools: {e}" response = call with grounding=False I used two layers of try because grounding involves external searches, so the failure rate is naturally higher than pure text generation. When the tool call fails, instead of returning "Generation failed," it's better to retry once with the same prompt but without the tool. In this case, the user gets a pure article analysis without comparative views or sources, but at least they have something. This degradation is intentional, not an accident. The report is a full Markdown document, often thousands of words long, which can't fit into a LINE message. Making it a Flex Message isn't suitable either because it contains tables and multi-level headings. So, I turned it into a webpage. But then I had to decide: should these reports be stored in a database? I chose not to. The reports are only stored in memory and disappear as soon as the Cloud Run instance is recycled: class ReportStore: def init self, ttl seconds: float = DEFAULT REPORT TTL SECONDS : self.ttl = ttl seconds self. reports: Dict str, dict = {} self. lock = Lock def put self, html: str - str: report id = uuid.uuid4 .hex with self. lock: self. purge expired self. reports report id = { "html": html, "created at": time.time , } return report id The report id uses uuid.uuid4 .hex because this URL has no login protection; anyone with the link can open it, so the ID must be unguessable. The page itself also includes