{
  "source_file": "0930",
  "source_platform": "Hashnode",
  "author": "Wesley Kambale",
  "title_listed": "Build Your First AI Agent with Gemini and LlamaIndex",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://kambale.dev/build-your-first-ai-agent-with-gemini-and-llamaindex",
  "publish_date": "2025-09-14",
  "corpus_stage": "",
  "notes": "",
  "id": "bbb88f6adb23",
  "collected_at": "2026-10-02T14:11:11",
  "title": "Build Your First AI Agent with Gemini and LlamaIndex",
  "body_markdown": "Build Your First AI Agent with Gemini and LlamaIndex\nBuilding a Tourism AI Assistant with Agentic Workflows\nPublished\nSeptember 14, 2025\n•\n11\nmin read\n•\nView as Markdown\nW\nWesley Kambale\nI'm a Machine Learning Engineer passionate about building production-ready ML systems for the African market.\nWith experience in TensorFlow, Keras, and Python-based workflows, I help teams bridge the gap between machine learning research and real-world deployment—especially on resource-constrained devices.\nI'm also a Google Developer Expert in AI. I regularly speak at tech conferences including PyCon Africa, DevFest Kampala, DevFest Nairobi and more and also write technical articles on AI/ML here.\nPart of series\nMachine Learning\nThe world of LLMs is moving beyond simple chatbots. The new frontier is\nAI agents\n: systems that can reason, plan, and use external tools to accomplish complex tasks. In tourism, this means our assistant can automatically fetch up-to-date info on destinations, guide users to attractions or restaurants, find relevant images, and even draft promotional posts. Under the hood, we use Google’s advanced Gemini model as the “brain” and LlamaIndex to connect Gemini to custom Python tools. The agent follows the\nReAct (Reason+Act)\nframework: it thinks about the query, chooses tools, acts (calls them), then reasons over the results. The result is a flexible travel assistant that can handle multi-step tasks in a human-like way.\nOur agent will demonstrate how to:\nCombine LLMs and Tools:\nOvercome LLM limitations by letting them call Python functions (tools) for fresh data.\nBuild Custom Tools:\nWrite scrapers and generators (e.g. for attractions and images) that the agent can invoke.\nUse Gemini as the LLM:\nLeverage Google’s multimodal Gemini 2.5 Pro model (our most advanced AI model) for reasoning.\nLeverage LlamaIndex:\nUse LlamaIndex’s\nReActAgent\nand\nFunctionTool\nwrappers to glue tools and the LLM together.\nSee the ReAct Loop in Action:\nPeek at the agent’s chain of thought as it solves a query by sequentially “thinking” and “acting”.\nAdvanced Prompting:\nGo beyond raw data by feeding tool outputs into a carefully crafted prompt for insightful recommendations.\nBy the end, you’ll have a working Tourism AI Assistant that answers travel questions, scrapes live data, finds images, and even tweets about destinations—all powered by Gemini and LlamaIndex.\nOur Technology Stack\nBefore coding, let’s understand the pieces we’ll use:\nGoogle Gemini 2.5 Pro:\nA state-of-the-art multimodal LLM (text+code+images+video) with strong reasoning capabilities. This is the “brain” of our agent. We use the\nmodels/gemini-2.5-pro\nendpoint via Google’s AI Generative SDK.\nLlamaIndex:\nAn open-source framework for building LLM apps. It provides the\nReActAgent\nclass and\nFunctionTool\nwrapper to connect LLMs with Python tools. LlamaIndex lets us expose any function to the agent in a structured way.\nWeb Scraping Libraries:\nWe’ll use\nrequests\nand\nbeautifulsoup4\nto fetch live data from travel sites (for attractions, etc.). (In production, a tourism API is often preferred to avoid brittle scrapers, but for our tutorial we’ll show how to scrape responsibly.)\nImage Search:\nWe’ll implement a simple DuckDuckGo image scraper to let the agent fetch pictures of destinations.\nContent Generator:\nA function to draft a promotional social-media post about a place or event, illustrating how the agent can take action, not just fetch data.\nInstalling Dependencies\nFirst, install the required Python packages. In your notebook or script, run:\n!pip install -q llama-index llama-index-llms-gemini google-generativeai python-dotenv beautifulsoup4 requests\nllama-index-llms-gemini:\nAdds Gemini support to LlamaIndex.\ngoogle-generativeai:\nGoogle’s SDK to call the Gemini API.\npython-dotenv:\nFor loading API keys from a\n.env\nfile (or Colab secrets).\nbeautifulsoup4 & requests:\nFor our web-scraping tools.\nWe’ll also enable detailed logging to trace the agent’s reasoning:\nimport\nos, logging\nlogging.basicConfig(level=logging.INFO, format=\n'%(asctime)s - %(levelname)s - %(message)s'\n)\nlogger = logging.getLogger(__name__)\nAuthentication\nTo use Gemini, you need a Google AI Studio API key:\nCreate an API key in Google AI Studio.\nStore it as\nGOOGLE_API_KEY\nin your environment (or Colab secrets).\nThen configure the Google Generative AI SDK:\nimport\ngoogle.generativeai\nas\ngenai\nfrom\ndotenv\nimport\nload_dotenv\n\nload_dotenv()\n# if using .env file\nGOOGLE_API_KEY = os.getenv(\n\"GOOGLE_API_KEY\"\n)\nif\nnot\nGOOGLE_API_KEY:\nraise\nValueError(\n\"Please set GOOGLE_API_KEY environment variable.\"\n)\ngenai.configure(api_key=GOOGLE_API_KEY)\nlogger.info(\n\"Configured Google API key.\"\n)\nConfiguring the LLM and Embedding Model\nWe tell LlamaIndex which LLM (Gemini) and embedding model to use. The embedding model isn’t crucial here (we won’t do retrieval-heavy tasks), but LlamaIndex requires one. We’ll use a small HuggingFace model:\nfrom\nllama_index.llms.gemini\nimport\nGemini\nfrom\nllama_index.core\nimport\nSettings\nfrom\nllama_index.embeddings.huggingface\nimport\nHuggingFaceEmbedding\n\nMODEL_NAME =\n\"models/gemini-2.5-pro\"\n# Google’s Gemini 2.5 Pro model\nlogger.info(\n\"Configuring LlamaIndex settings...\"\n)\nSettings.embed_model = HuggingFaceEmbedding(model_name=\n\"BAAI/bge-small-en-v1.5\"\n)\nSettings.llm = Gemini(model=MODEL_NAME)\n# uses GOOGLE_API_KEY automatically\nlogger.info(\nf\"Using Gemini LLM:\n{MODEL_NAME}\n\"\n)\nNote: Gemini 2.5 Pro is a powerful model that excels at complex tasks. It supports text, code, and images, and even has built-in “thinking” ability for chain-of-thought reasoning.\nCrafting Our Tools\nAn agent is only as capable as its tools. A\ntool\nhere is a Python function that performs some action—like fetching data or creating content—that the LLM alone cannot do (e.g. real-time web queries). We’ll create a small toolkit for tourism tasks:\nTool 1:\nget_city_attractions(city)\n– Scrape top attractions for a city. (As an example, we’ll scrape a known travel guide or Wikipedia for a few highlights.)\nTool 2:\nget_city_restaurants(city)\n– (Optional) Scrape or list popular restaurants. For brevity, this could return a static list or use a simple scrape.\nTool 3:\nsearch_for_destination_images(place)\n– Search DuckDuckGo for images of a place (like “Eiffel Tower Paris”). Returns a few image URLs.\nTool 4:\ngenerate_tourism_post(place, summary)\n– Generate a friendly promotional social-media post (tweet) about an attraction or city, given its name and a short summary.\nEach tool will have a clear docstring explaining its purpose and inputs. The agent reads these docstrings to know when to use which tool. For example:\nimport\nrequests\nfrom\nbs4\nimport\nBeautifulSoup\ndef\nget_city_attractions\n(\ncity: str\n) -> str:\n\"\"\"\n    Fetches the top tourist attractions for the given city.\n    Returns a bullet list of attractions and brief info.\n    \"\"\"\ntry\n:\n# Example: scrape PlanetWare or Wikipedia page for top attractions\nurl =\nf\"https://www.planetware.com/\n{city.lower()}\n/top-rated-tourist-attractions-in-\n{city.lower()}\n.htm\"\nresp = requests.get(url)\n        soup = BeautifulSoup(resp.text,\n\"html.parser\"\n)\n        attractions = [h2.get_text(strip=\nTrue\n)\nfor\nh2\nin\nsoup.find_all(\n'h2'\n)[:\n5\n]]\nif\nnot\nattractions:\nraise\nException(\n\"No attractions found\"\n)\nreturn\n\"\\n\"\n.join(\nf\"-\n{a}\n\"\nfor\na\nin\nattractions)\nexcept\nException\nas\ne:\nreturn\nf\"Could not retrieve attractions for\n{city}\n(error:\n{e}\n)\"\ndef\nget_city_restaurants\n(\ncity: str\n) -> str:\n\"\"\"\n    Returns a short list of popular restaurants in the city.\n    (For demo purposes, this may be a static list or scraped from a simple source.)\n    \"\"\"\n# Placeholder: in a real app, use an API or reliable source\ndummy_data = {\n\"Paris\"\n: [\n\"Le Jules Verne (Eiffel Tower)\"\n,\n\"L'Ambroisie\"\n,\n\"Septime\"\n],\n\"Rome\"\n: [\n\"Da Enzo al 29\"\n,\n\"Roscioli\"\n,\n\"La Pergola\"\n]\n    }\nreturn\n\"\\n\"\n.join(\nf\"-\n{r}\n\"\nfor\nr\nin\ndummy_data.get(city, [\n\"No data available\"\n]))\ndef\nsearch_for_destination_images\n(\nquery: str\n) -> list:\n\"\"\"\n    Searches DuckDuckGo Images for the query and returns a list of image URLs.\n    \"\"\"\ntry\n:\n        res = requests.get(\nf\"https://duckduckgo.com/?q=\n{query.replace(\n' '\n,\n'+'\n)}\n&iar=images&iax=images\"\n)\n        soup = BeautifulSoup(res.text,\n\"html.parser\"\n)\n        imgs = soup.select(\n\"img.tile--img__img\"\n)[:\n5\n]\nreturn\n[img.get(\n\"src\"\n)\nfor\nimg\nin\nimgs\nif\nimg.get(\n\"src\"\n)]\nexcept\nException\nas\ne:\nreturn\n[\nf\"Image search failed:\n{e}\n\"\n]\ndef\ngenerate_tourism_post\n(\nplace: str, summary: str\n) -> str:\n\"\"\"\n    Generates a friendly social media post about a place.\n    \"\"\"\nprompt =\nf\"Write an enthusiastic tweet about visiting\n{place}\n. Summary:\n{summary}\n\"\nresp = genai.generate_message(model=MODEL_NAME, messages=[{\n\"role\"\n:\n\"user\"\n,\n\"content\"\n: prompt}])\nreturn\nresp.last\nNote:\nIn production, scraping arbitrary sites (like PlanetWare) can be fragile. Sites may block scrapers, change layout, or have legal terms against scraping. For robust applications, a dedicated travel data API is preferable. Here we use simple scrapers for illustration.\nWrapping Functions for LlamaIndex\nLlamaIndex needs tools wrapped in\nFunctionTool\nobjects. This exposes each function’s signature and docstring to the LLM. Docstrings become part of the agent’s understanding of when to use each tool. For example:\nfrom\nllama_index.core.tools\nimport\nFunctionTool\n\nlogger.info(\n\"Wrapping functions into LlamaIndex tools...\"\n)\ntools = [\n    FunctionTool.from_defaults(fn=get_city_attractions,\n                               name=\n\"get_city_attractions\"\n,\n                               description=\n\"Get the top tourist attractions in a city.\"\n),\n    FunctionTool.from_defaults(fn=get_city_restaurants,\n                               name=\n\"get_city_restaurants\"\n,\n                               description=\n\"Get popular restaurants in a city.\"\n),\n    FunctionTool.from_defaults(fn=search_for_destination_images,\n                               name=\n\"search_for_destination_images\"\n,\n                               description=\n\"Search for images of a place.\"\n),\n    FunctionTool.from_defaults(fn=generate_tourism_post,\n                               name=\n\"generate_tourism_post\"\n,\n                               description=\n\"Create a social media post promoting an attraction or city.\"\n)\n]\nlogger.info(\nf\"Created\n{len(tools)}\ntools:\n{[t.name\nfor\nt\nin\ntools]}\n\"\n)\nEach\nFunctionTool\nincludes the function name, parameters, and a human-readable docstring. The agent will\nchoose which tool to call\nby looking at your query and the tool descriptions, then pass the right arguments.\nInitializing the ReAct Agent\nWith tools ready, we create the agent. We use LlamaIndex’s\nReActAgent\n, which implements the Reason-and-Act loop. The agent will\nthink\nabout the user’s question, decide\nwhich tool\nto use and with what inputs,\nexecute\nthe tool, then observe the result and repeat as needed. Setting\nverbose=True\nlets us see the entire chain of thought:\nfrom\nllama_index.core.agent\nimport\nReActAgent\n\nlogger.info(\n\"Initializing the ReAct agent...\"\n)\nagent = ReActAgent.from_tools(tools=tools, llm=Settings.llm, verbose=\nTrue\n)\nlogger.info(\n\"Tourism Agent is ready to go!\"\n)\nUnder the hood,\nReActAgent\nwill prompt Gemini with a system message describing the tools and this reasoning framework. The LLM will follow a structure like:\nThought: I should use [tool] because ...\nAction: [tool] with input {...}\nObservation: (tool output)\nThought: ...\ncontinue\nor final answer.\nThis explicit reasoning flow is characteristic of the ReAct approach.\nTrying Out the Tourism Agent\nOur agent is now online! Let’s test it with a couple of example queries and inspect its reasoning (verbose output):\nScenario 1: Simple Question\nUser:\n“What are the top attractions in Paris?”\nresponse = agent.chat(\"What are the top tourist attractions in Paris?\")\nprint(response)\nAgent’s (verbose) Thought Process:\nThought:\nThe user asks for top attractions in Paris. The\nget_city_attractions\ntool seems appropriate.\nAction:\nget_city_attractions\nAction Input:\n{\"city\": \"Paris\"}\nObservation:\n- Eiffel Tower\n  - Louvre Museum\n  - Notre-Dame Cathedral\n  - Sacré-Cœur Basilica\n  - Musée d\n'Orsay\nThought:\nI have the list of attractions. Now I answer the user using this info.\nAnswer:\n“Top attractions in Paris include the Eiffel Tower, Louvre Museum, Notre-Dame Cathedral, Sacré-Cœur, and Musée d’Orsay.”\nAgent Answer:\nTop attractions in Paris include: Eiffel Tower, Louvre Museum, Notre-Dame Cathedral, Sacré-Cœur Basilica, and Musée d’Orsay.\nThis shows the ReAct loop: the agent identified the need for a tool, executed it, and synthesized the output.\nScenario 2: Multi-Step Task\nUser:\n“Find art-related attractions in Paris and then write a tweet about them.”\nThis requires multiple steps: identify art attractions, then generate a tweet.\nresponse = agent.chat(\n\"Find art-related attractions in Paris and draft a tweet about them.\"\n)\nprint(response)\nAgent’s Thought Process:\nThought:\nThe user needs two things: 1) art-related attractions in Paris, 2) a tweet about them. First, get all attractions.\nAction:\nget_city_attractions\nInput:\n{\"city\": \"Paris\"}\nObservation:\n(same list as before)\nThought:\nThe attractions list is obtained. Now pick those related to art (e.g. museums, galleries). These include Louvre Museum and Musée d'Orsay. I will create a summary and use the\ngenerate_tourism_post\ntool.\nAction:\ngenerate_tourism_post\nInput:\n{\"place\": \"Paris Museums\", \"summary\": \"Highlights: Louvre Museum and Musée d'Orsay\"}\nObservation:\nExplore Paris\n's Artistic Treasures!\n  Don’t miss the Louvre Museum’s timeless classics and the Musée d’Orsay’s Impressionist masterpieces. From ancient wonders to modern art, Paris has it all. Bon voyage and happy exploring!\nThought:\nI have the tweet text. Now I can share the final answer.\nAnswer:\nHere’s a tweet promoting Paris’s art attractions:… (tweet above)…\nAgent Answer:\n“Here’s a tweet about Paris’s art attractions: Explore Paris’s Artistic Treasures! Don’t miss the Louvre Museum’s timeless classics and the Musée d’Orsay’s Impressionist masterpieces. From ancient wonders to modern art, Paris has it all. Bon voyage and happy exploring!”\nThis demonstrates chaining: the agent used\nget_city_attractions\n, filtered the results, then used\ngenerate_tourism_post\nwith the summary. The LLM reasoned at each step, as expected in a ReAct agent.\nAdvanced Prompting: Personalized Recommendations\nSo far we’ve fetched raw data and posted about it. But LLMs can also provide\ninsights\non top of data. For example, suppose a user asks:\n“I’m a history buff visiting Rome. Which attractions should I see?”\nSimply returning a list of\nall\nRome attractions isn’t ideal. Instead, we want\npersonalized recommendations\n. We can achieve this by combining our tools with a clever prompt:\nGet raw data via tool:\nCall\nget_city_attractions(\"Rome\")\n.\nEngineer a focused prompt:\nTell Gemini to read the list and pick the best 3-5 for a history enthusiast, explaining why.\nGenerate the answer with Gemini:\nThe LLM acts as a reasoning layer on the data.\nFor example:\n# Step 1: fetch attractions\nattractions = get_city_attractions(\n\"Rome\"\n)\n# (assume this returns a bullet list of sites like Colosseum, Vatican, Pantheon, etc.)\n# Step 2: craft a targeted prompt\nprompt =\nf\"\"\"\nYou are an expert tour guide for Rome. A user is interested in historical sites. \nFrom the following list of Rome attractions, recommend 3-5 must-see locations for a history buff. Explain why each is a good choice.\nRome Attractions:\n{attractions}\nFocus on historical significance and include brief descriptions.\n\"\"\"\n# Step 3: call Gemini directly with this prompt\nhistorical_resp = genai.generate_message(model=MODEL_NAME, messages=[{\n\"role\"\n:\n\"user\"\n,\n\"content\"\n: prompt}])\nprint(historical_resp.last)\nSample Output:\nExcellent choices for a history enthusiast in Rome include:\nColosseum\n– The iconic ancient amphitheater where gladiators once fought. A symbol of Rome’s imperial past and architecture.\nRoman Forum\n– The heart of ancient Rome’s political and social life. You can walk among ruins of temples and government buildings.\nPantheon\n– A 2,000-year-old temple-turned-church, showcasing Rome’s engineering and dedication to the gods. Its monumental dome is a marvel.\nVatican Museums & St. Peter’s Basilica\n– While a church, the Vatican holds vast historical and artistic treasures spanning millennia, including Raphael’s Rooms and Michelangelo’s Pietà.\nCatacombs of Callixtus\n– Underground burial chambers that reveal early Christian history and traditions.\nEach of these sites offers rich historical insights into Rome’s past civilizations and will captivate any history buff!\nThis “reasoning layer” approach leverages Gemini’s understanding to\nanalyze and filter\nthe tool output. We transformed a raw list into a personalized recommendation list with explanation. One could even wrap this workflow into a new tool (e.g.\nrecommend_attractions_for_interest\n) for the agent to use directly.\nConclusion\nWe’ve built a fully functional\nTourism AI Assistant\nthat can interpret user requests, choose the right tool, fetch live data, and present it in a helpful way. We saw:\nSetting up the agent:\nConfiguring Gemini and LlamaIndex.\nWriting tools:\nPython functions with clear docstrings, wrapped as\nFunctionTool\nfor the agent.\nUsing ReAct:\nThe agent’s chain of thought (“Thought”, “Action”, “Observation”) shows how it plans and executes.\nMultimodal capability:\nAlthough we didn’t show it here, Gemini supports images and code, so you could extend this agent to analyze photos of landmarks or compute routes.\nAdvanced prompting:\nWe enhanced raw data by feeding it into Gemini with a crafted prompt, yielding richer, customized advice.\nThis agentic architecture is highly extensible. Next steps might include integrating real travel APIs (for flights or hotels), adding a\nmemory\nso the assistant recalls user preferences, or connecting to mapping services for directions. The key idea is that the LLM does the reasoning, while we supply specialized tools for any real-world data or action.\nHappy travels and happy building!\n#\ngemini\n#\nai-agents\n#\ntourism\n#\nllamaindex",
  "metadata": {
    "title": "Build Your First AI Agent with Gemini and LlamaIndex",
    "description": "Building a Tourism AI Assistant with Agentic Workflows",
    "keywords": null,
    "author": null,
    "og:title": "Build Your First AI Agent with Gemini and LlamaIndex",
    "og:description": "Building a Tourism AI Assistant with Agentic Workflows",
    "og:url": "https://kambale.dev/build-your-first-ai-agent-with-gemini-and-llamaindex",
    "og:site_name": "Wesley Kambale",
    "og:image": "https://cdn.hashnode.com/res/hashnode/image/upload/v1757888787651/f3e8974d-1a20-44b2-93b1-805982d9a788.png",
    "og:image:width": "1200",
    "og:image:height": "630",
    "og:type": "article",
    "twitter:card": "summary_large_image",
    "twitter:creator": "https://twitter.com/WesKambale",
    "twitter:title": "Build Your First AI Agent with Gemini and LlamaIndex",
    "twitter:description": "Building a Tourism AI Assistant with Agentic Workflows",
    "twitter:image": "https://cdn.hashnode.com/res/hashnode/image/upload/v1757888787651/f3e8974d-1a20-44b2-93b1-805982d9a788.png",
    "article:published_time": "2025-09-14T22:27:20.815Z",
    "article:author": "https://kambale.dev/@kambale"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/bbb88f6adb23/code/body-000.txt",
      "chars": 110,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-001.txt",
      "chars": 148,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-002.txt",
      "chars": 326,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-003.txt",
      "chars": 476,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-004.txt",
      "chars": 825,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-005.txt",
      "chars": 516,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-006.txt",
      "chars": 509,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-007.txt",
      "chars": 342,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-008.txt",
      "chars": 1029,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-009.txt",
      "chars": 214,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-010.txt",
      "chars": 139,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-011.txt",
      "chars": 88,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-012.txt",
      "chars": 102,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-013.txt",
      "chars": 109,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-014.txt",
      "chars": 227,
      "language": "text"
    },
    {
      "file": "attachments/bbb88f6adb23/code/body-015.txt",
      "chars": 707,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1757888693269/ee93cfd2-3554-4dbf-baf2-4d7f2867425f.png",
      "bytes": 56248,
      "reused": true,
      "local_path": "attachments/bbb88f6adb23/images/000.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1757888693269/ee93cfd2-3554-4dbf-baf2-4d7f2867425f.png",
      "alt": "Build Your First AI Agent with Gemini and LlamaIndex",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://kambale.dev/build-your-first-ai-agent-with-gemini-and-llamaindex.md",
      "label": "View as Markdown",
      "type": "a"
    },
    {
      "href": "https://hashnode.com/@kambale",
      "label": "W",
      "type": "a"
    },
    {
      "href": "https://twitter.com/WesKambale",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://linkedin.com/in/WesKambale",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/series/ml-series",
      "label": "Part of series Machine Learning",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/gemini",
      "label": "# gemini",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/ai-agents",
      "label": "# ai-agents",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/tourism",
      "label": "# tourism",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/llamaindex",
      "label": "# llamaindex",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:50:48Z",
  "source_url": "https://kambale.dev/build-your-first-ai-agent-with-gemini-and-llamaindex",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 18041,
  "expected_body_images": 1,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/bbb88f6adb23/pages/source.html",
  "local_html": "attachments/bbb88f6adb23/pages/article.html",
  "body_base_path": "attachments/bbb88f6adb23/pages/",
  "body_markdown_path": "attachments/bbb88f6adb23/pages/article.md"
}