<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Agents · Avinash Gosavi</title>
  <subtitle>Posts tagged “Agents” by Avinash Gosavi.</subtitle>
  <id>https://www.avinashgosavi.com/feed/topic/agents.xml</id>
  <link href="https://www.avinashgosavi.com/feed/topic/agents.xml" rel="self" type="application/atom+xml"/>
  <link href="https://www.avinashgosavi.com/blog/#agents" rel="alternate" type="text/html"/>
  <updated>2026-10-27T09:36:14+00:00</updated>
  <author><name>Avinash Gosavi</name><uri>https://www.avinashgosavi.com/</uri></author>
  <entry>
    <title type="html">Building Rena — An AI Health Companion with Gemini Live, Veo 2, and Google ADK</title>
    <link href="https://www.avinashgosavi.com/post/building-rena-ai-health-companion-gemini-live/" rel="alternate" type="text/html" title="Building Rena — An AI Health Companion with Gemini Live, Veo 2, and Google ADK"/>
    <id>https://www.avinashgosavi.com/post/building-rena-ai-health-companion-gemini-live/</id>
    <published>2026-03-16T00:00:00+00:00</published>
    <updated>2026-10-27T09:36:14+00:00</updated>
    <category term="Agents"/>
    <category term="Voice AI"/>
    <summary type="html">How I built Rena for the Gemini Live Agent Challenge 2026 — a voice-first health companion using Gemini Live, Veo 2 and Google ADK.</summary>
    <content type="html" xml:base="https://www.avinashgosavi.com/post/building-rena-ai-health-companion-gemini-live/">&lt;p&gt;I built Rena for the &lt;strong&gt;Gemini Live Agent Challenge 2026&lt;/strong&gt; — a personal AI health companion where you never have to open a logging screen. You just talk.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href=&quot;https://rena-490107-f0f28.web.app&quot;&gt;rena-490107-f0f28.web.app&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href=&quot;https://github.com/aag1091-alt/Rena&quot;&gt;github.com/aag1091-alt/Rena&lt;/a&gt; · &lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=f-ZZw0CTivU&quot;&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div style=&quot;position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:12px;margin:1.5rem 0;&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/f-ZZw0CTivU&quot; title=&quot;Rena demo&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; style=&quot;position:absolute;top:0;left:0;width:100%;height:100%;border:0;border-radius:12px;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-idea&quot;&gt;The idea&lt;/h2&gt;

&lt;p&gt;Most health apps fail because tracking is a chore. You have to open the app, find the right screen, tap in numbers — dozens of times a day. Rena flips this with voice-first interaction powered by Gemini Live:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;“I had salmon, rice, and sparkling water for lunch”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Add bodyweight squats to my workout plan”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Plan my meals for tomorrow — I have a long run in the morning”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rena listens, understands, and acts. The app also starts with one question: &lt;em&gt;“What are you working toward?”&lt;/em&gt; You tell her in your own words — &lt;em&gt;“I want to feel confident at my friend’s wedding in July”&lt;/em&gt;, &lt;em&gt;“Beach trip in 10 weeks”&lt;/em&gt; — and every recommendation she makes is filtered through that specific goal, not a generic calorie number.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-app&quot;&gt;The app&lt;/h2&gt;

&lt;p&gt;Four tabs, each with a dedicated voice context:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Home&lt;/strong&gt; — goal countdown, daily calorie breakdown, protein/water progress, AI morning insight&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;History&lt;/strong&gt; — scrollable day log with macros, workouts, and a Gemini-generated daily insight&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Plan&lt;/strong&gt; — workout plan + meal plan, generated through voice conversation with Rena&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scan&lt;/strong&gt; — camera or gallery scan; Rena identifies food and estimates calories per item&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;technologies&quot;&gt;Technologies&lt;/h2&gt;

&lt;h3 id=&quot;ai--ml&quot;&gt;AI &amp;amp; ML&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Technology&lt;/th&gt;
      &lt;th&gt;How it’s used&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Gemini Live API&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Real-time bidirectional voice — the core of all voice interaction&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Gemini 2.5 Flash&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gemini-2.5-flash-native-audio-latest&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;Agent reasoning, plan generation, day insights, coaching scripts&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Gemini Flash Vision&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Food photo recognition — identifies items, returns nutrition estimates&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Veo 2&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;AI-generated exercise demonstration videos&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Imagen&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Vision board image generation on goal setup&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Google ADK&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Agent framework — tools, routing, session management&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Google Cloud TTS&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;en-US-Neural2-F&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;Rena’s coaching voiceover for exercise videos&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;infrastructure&quot;&gt;Infrastructure&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Service&lt;/th&gt;
      &lt;th&gt;Role&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Cloud Run&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Hosts the Rena agent (min 1 instance — no cold-start voice drops)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Firestore&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;All user data — logs, plans, goals, prompts, insights, video jobs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Cloud Storage&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Exercise videos + vision board images&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Firebase Hosting&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;PWA web companion&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Cloud Build&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;CI/CD — GitHub push triggers build + deploy&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;application-stack&quot;&gt;Application stack&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;iOS:&lt;/strong&gt; SwiftUI, AVAudioEngine, URLSessionWebSocketTask&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Web PWA:&lt;/strong&gt; Vanilla JS, Web Audio API + AudioWorklet, WebSocket, Service Worker&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Python, FastAPI, Google ADK&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;

&lt;blockquote class=&quot;diagram-text&quot;&gt;&lt;p&gt;&lt;strong&gt;Diagram: Rena&apos;s architecture: app, agent on Cloud Run, and the services behind it&lt;/strong&gt; (&lt;a href=&quot;https://www.avinashgosavi.com/post/building-rena-ai-health-companion-gemini-live/&quot;&gt;see it in the post&lt;/a&gt;)&lt;/p&gt;&lt;p&gt;The iOS app or web PWA, with Home, History, Plan and Scan tabs, talks to the Rena agent on Cloud Run over a WebSocket for audio and REST for data. Inside the agent, the Google ADK agent core exposes tools: log_meal, log_water, log_workout, log_weight, scan_image, generate_workout_plan, generate_meal_plan, save_tomorrow_plan_note and get_recent_workouts. A context prompt system keeps prompts in Firestore so they can be edited without a redeploy, injects the RENA MEMORY block at every session start, has per-tab contexts for home, history, scan, plan and goal, and sends tool_status WebSocket messages for live save indicators. The agent uses three services: Firestore for all data; Gemini APIs (Live, Flash, Vision, Veo 2, Imagen and TTS); and Cloud Storage with exercise_videos/ and vision_journey/ folders.&lt;/p&gt;&lt;p&gt;&lt;em&gt;One agent on Cloud Run holds the tools and the prompts; everything it stores or generates lives in three services behind it.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Voice session data flow:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote class=&quot;diagram-text&quot;&gt;&lt;p&gt;&lt;strong&gt;Diagram: One Rena voice session, from tap to reply&lt;/strong&gt; (&lt;a href=&quot;https://www.avinashgosavi.com/post/building-rena-ai-health-companion-gemini-live/&quot;&gt;see it in the post&lt;/a&gt;)&lt;/p&gt;&lt;p&gt;The user taps Rena, which opens a WebSocket to /ws/{user_id} with the current tab as context and the user&apos;s timezone. The backend fetches the RENA MEMORY block and the prompt for that tab. Microphone audio is streamed as 16 kHz PCM over the WebSocket into Google ADK and Gemini Live, which works out the intent and calls a tool such as log_meal, log_workout or scan_image. Before the tool runs, a tool_status message such as &quot;Logging your meal…&quot; shows a live save indicator, and the tool writes to Firestore. Rena&apos;s reply is streamed back as 24 kHz audio chunks and played via AVAudioEngine.&lt;/p&gt;&lt;p&gt;&lt;em&gt;One tap, one round trip: the context is loaded before you speak, and the save indicator shows up before the tool runs.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-exercise-video-pipeline&quot;&gt;The exercise video pipeline&lt;/h2&gt;

&lt;p&gt;One of the more ambitious features: AI-generated coaching videos for workout exercises, generated on demand and cached forever.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. Gemini 2.5 Flash writes a coaching script
   (setup cues, movement feel, breath, safety — BLOCK_NONE filters
    so anatomical terms pass through)

2. Random trainer gender picked for visual variety

3. Veo 2 job submitted with exercise + muscles + script as direction
   &quot;No text, subtitles, captions or overlays on screen&quot;
   → returns job_id immediately; stored in Firestore

4. iOS/web polls every 5s for status

5. Google Cloud TTS (en-US-Neural2-F) generates coaching voiceover
   We tried matching a separate trainer voice to the video — the timing
   never lined up. The fix: use Rena&apos;s own voice throughout.
   Same voice the user has been talking to all along.

6. ffmpeg muxes Veo video + TTS audio into a single .mp4

7. Uploaded to GCS — same exercise never regenerates

8. AVQueuePlayer + AVPlayerLooper plays seamlessly on iOS
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Exercises with AI-generated videos available today: Bodyweight Squats, Plank, Walking Lunges, Elliptical Trainer.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;key-challenges&quot;&gt;Key challenges&lt;/h2&gt;

&lt;h3 id=&quot;1-getting-gemini-live-to-behave-inside-adk&quot;&gt;1. Getting Gemini Live to behave inside ADK&lt;/h3&gt;

&lt;p&gt;The hardest part was making the voice agent &lt;em&gt;reliable&lt;/em&gt; rather than just impressive in a demo. Gemini Live tends to interrupt itself, end turns prematurely, or stop responding after tool calls.&lt;/p&gt;

&lt;p&gt;The solution was treating prompts like software — versioned, tested, iterated. Every context (home logging, plan generation, history editing, scan) has its own system prompt stored in Firestore. Live-editable without a redeploy, meaning prompt changes could be tested in the running app within seconds.&lt;/p&gt;

&lt;p&gt;Key prompt techniques:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[RENA MEMORY]&lt;/code&gt; block&lt;/strong&gt; injected at session start — goal, today’s calories, recent meals, workouts, weight trend — so Rena never has to ask what you’ve already told her&lt;/li&gt;
  &lt;li&gt;Explicit post-tool-call instructions: &lt;em&gt;“after saving, tell the user what you logged in one sentence and wait”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;thinking_budget=0&lt;/code&gt; via a monkey-patch on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemini.connect()&lt;/code&gt; — ADK’s standard config path silently dropped &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;thinking_config&lt;/code&gt; before it reached the Live API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;2-real-time-save-indicators&quot;&gt;2. Real-time save indicators&lt;/h3&gt;

&lt;p&gt;When Rena calls a tool there’s a gap — the user doesn’t know if something is happening or if the session stalled. Before each tool runs, the backend sends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;type&quot;: &quot;tool_status&quot;, &quot;message&quot;: &quot;Logging your meal…&quot;}&lt;/code&gt; over the WebSocket. The voice overlay immediately updates in real time — users see &lt;em&gt;“Building your workout plan…”&lt;/em&gt; instead of silence.&lt;/p&gt;

&lt;h3 id=&quot;3-ios-audio-engine-continuity&quot;&gt;3. iOS audio engine continuity&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AVAudioEngine&lt;/code&gt; is notoriously fragile — audio interruptions can destroy it, and restarting introduces latency. We keep the engine running continuously between sessions, routing PCM frames through an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AVAudioMixerNode&lt;/code&gt; tap rather than restarting on every session open.&lt;/p&gt;

&lt;h3 id=&quot;4-veo-2-audio-matching&quot;&gt;4. Veo 2 audio matching&lt;/h3&gt;

&lt;p&gt;First approach: record a trainer’s voice reading the coaching script and mux with the Veo video. The timing never matched — the trainer audio was recorded independently of how the generated video moved.&lt;/p&gt;

&lt;p&gt;Fix: use Rena’s own TTS voice (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;en-US-Neural2-F&lt;/code&gt;) for the coaching audio. Unexpected upside — users are already familiar with Rena’s voice, so hearing her coach them through an exercise feels natural and consistent.&lt;/p&gt;

&lt;h3 id=&quot;5-timezone-aware-context&quot;&gt;5. Timezone-aware context&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;“What did I eat today?”&lt;/em&gt; is hard when users are in different timezones and the server runs in UTC. We added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tz&lt;/code&gt; to the WebSocket URL, cached it per session, and inject &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[CURRENT_LOCAL_TIME]&lt;/code&gt; into every prompt so Rena always knows the user’s local date.&lt;/p&gt;

&lt;h3 id=&quot;6-prompt-iteration-speed&quot;&gt;6. Prompt iteration speed&lt;/h3&gt;

&lt;p&gt;8+ contexts required dozens of prompt iterations each. The Firestore-backed system meant zero redeploys. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seed_prompts.py&lt;/code&gt; script keeps prompts in version control and syncs to Firestore on demand.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;try-it-yourself&quot;&gt;Try it yourself&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Web app (instant, no install):&lt;/strong&gt; &lt;a href=&quot;https://rena-490107-f0f28.web.app&quot;&gt;rena-490107-f0f28.web.app&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Open in Chrome or Safari on your phone&lt;/li&gt;
  &lt;li&gt;Sign in with Google and complete the quick onboarding&lt;/li&gt;
  &lt;li&gt;Tap &lt;strong&gt;⚙ Settings → Seed 7 Days Data&lt;/strong&gt; to pre-populate with realistic history&lt;/li&gt;
  &lt;li&gt;Tap the &lt;strong&gt;Rena orb&lt;/strong&gt; (center tab), allow microphone, and start talking&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
  &lt;p&gt;The web app is optimised for mobile screen sizes. If you’re on desktop, open DevTools and switch to iPhone view.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;iOS app:&lt;/strong&gt; Clone &lt;a href=&quot;https://github.com/aag1091-alt/Rena&quot;&gt;github.com/aag1091-alt/Rena&lt;/a&gt;, open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ios/Rena/Rena.xcodeproj&lt;/code&gt; in Xcode, run on a physical iPhone. The app connects to the live Cloud Run backend — no local server needed.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Push notifications for morning nudges&lt;/li&gt;
  &lt;li&gt;Gallery scan — Rena proactively finds food photos you already took&lt;/li&gt;
  &lt;li&gt;Pattern filling — if nothing is logged by noon, Rena asks about your usual routine&lt;/li&gt;
  &lt;li&gt;Streak milestones with Imagen-evolved vision boards&lt;/li&gt;
  &lt;li&gt;watchOS for passive tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;app-screenshots&quot;&gt;App screenshots&lt;/h2&gt;

&lt;div style=&quot;display:flex; gap:12px; justify-content:center; align-items:flex-start; flex-wrap:nowrap; overflow-x:auto;&quot;&gt;
  &lt;div style=&quot;text-align:center; min-width:0; flex:1;&quot;&gt;
    &lt;picture&gt;
      &lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/rena-home.webp&quot; /&gt;
      &lt;img width=&quot;360&quot; height=&quot;778&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/assets/images/rena-home.jpeg&quot; alt=&quot;Home&quot; style=&quot;width:100%; border-radius:12px; box-shadow:0 2px 10px rgba(0,0,0,0.12);&quot; /&gt;
    &lt;/picture&gt;
    &lt;p style=&quot;margin-top:6px; font-size:0.8rem; color:var(--text-muted);&quot;&gt;Home&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;text-align:center; min-width:0; flex:1;&quot;&gt;
    &lt;picture&gt;
      &lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/rena-history.webp&quot; /&gt;
      &lt;img width=&quot;360&quot; height=&quot;778&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/assets/images/rena-history.jpeg&quot; alt=&quot;History&quot; style=&quot;width:100%; border-radius:12px; box-shadow:0 2px 10px rgba(0,0,0,0.12);&quot; /&gt;
    &lt;/picture&gt;
    &lt;p style=&quot;margin-top:6px; font-size:0.8rem; color:var(--text-muted);&quot;&gt;History&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;text-align:center; min-width:0; flex:1;&quot;&gt;
    &lt;picture&gt;
      &lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/rena-plan.webp&quot; /&gt;
      &lt;img width=&quot;360&quot; height=&quot;778&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/assets/images/rena-plan.jpeg&quot; alt=&quot;Plan&quot; style=&quot;width:100%; border-radius:12px; box-shadow:0 2px 10px rgba(0,0,0,0.12);&quot; /&gt;
    &lt;/picture&gt;
    &lt;p style=&quot;margin-top:6px; font-size:0.8rem; color:var(--text-muted);&quot;&gt;Plan&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;text-align:center; min-width:0; flex:1;&quot;&gt;
    &lt;picture&gt;
      &lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/rena-scan.webp&quot; /&gt;
      &lt;img width=&quot;360&quot; height=&quot;778&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/assets/images/rena-scan.jpeg&quot; alt=&quot;Scan&quot; style=&quot;width:100%; border-radius:12px; box-shadow:0 2px 10px rgba(0,0,0,0.12);&quot; /&gt;
    &lt;/picture&gt;
    &lt;p style=&quot;margin-top:6px; font-size:0.8rem; color:var(--text-muted);&quot;&gt;Scan&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;Built with Google Cloud, Gemini Live API, Veo 2, Imagen, and Google ADK for the Gemini Live Agent Challenge 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Another take on streaming live audio from a phone: &lt;a href=&quot;/post/live-audio-transcription-whisper/&quot;&gt;near real-time transcription with faster-whisper on my own server&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rena’s need to remember users across sessions is where &lt;a href=&quot;/post/vaultmem-encrypted-memory-for-ai-agents/&quot;&gt;VaultMem&lt;/a&gt; started — an encrypted memory library for AI agents.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">AI History Blog Generator</title>
    <link href="https://www.avinashgosavi.com/post/ai-history-blog-generator/" rel="alternate" type="text/html" title="AI History Blog Generator"/>
    <id>https://www.avinashgosavi.com/post/ai-history-blog-generator/</id>
    <published>2026-03-14T00:00:00+00:00</published>
    <updated>2026-10-24T10:36:55+00:00</updated>
    <category term="Agents"/>
    <summary type="html">Building an agent that turns any historical topic into a Jekyll blog, milestone by milestone, using local models through Ollama.</summary>
    <content type="html" xml:base="https://www.avinashgosavi.com/post/ai-history-blog-generator/">&lt;p&gt;I was curious how AI had evolved over the years — not just the big headline moments but the smaller steps in between that actually moved things forward. I wanted something I could browse casually, not a research paper or a Wikipedia rabbit hole. Just a clean walkthrough: milestone by milestone, a bit of context, who was involved, when it happened.&lt;/p&gt;

&lt;p&gt;I couldn’t find exactly that, so I started building it. Around the same time I was also exploring what local models could actually do — running Ollama, trying different models, seeing how far you could get without sending everything to an API. That combination clicked: use a local model to do the writing, keep costs low, and let it run as long as needed. And somewhere while putting that together I thought — wait, this doesn’t have to be about AI. It could be about anything with a history. That’s how &lt;a href=&quot;https://github.com/aag1091-alt/AI-History-Blog-Generator&quot;&gt;AI History Blog Generator&lt;/a&gt; came about — an autonomous agent that turns any historical topic into a fully-functioning Jekyll blog, one milestone at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live examples:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Default (AI history): &lt;a href=&quot;https://aag1091-alt.github.io/AI-History-Blog/&quot;&gt;aag1091-alt.github.io/AI-History-Blog&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Custom topic (Golden Gate Bridge history): &lt;a href=&quot;https://aag1091-alt.github.io/Golden-Gate-History/&quot;&gt;aag1091-alt.github.io/Golden-Gate-History&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-it-does&quot;&gt;What It Does&lt;/h2&gt;

&lt;p&gt;Give it a topic — “the history of jazz music”, “the history of the Golden Gate Bridge”, anything — and it will:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Ask Claude to define &lt;strong&gt;eras&lt;/strong&gt; and &lt;strong&gt;categories&lt;/strong&gt; for the topic (only once, on first run)&lt;/li&gt;
  &lt;li&gt;Pick the next interesting milestone not yet covered&lt;/li&gt;
  &lt;li&gt;Research it via Wikipedia and arXiv&lt;/li&gt;
  &lt;li&gt;Write a full blog post and self-critique it&lt;/li&gt;
  &lt;li&gt;Fetch a relevant Wikimedia image (with NSFW filtering)&lt;/li&gt;
  &lt;li&gt;Generate a &lt;strong&gt;person profile page&lt;/strong&gt; for everyone mentioned in the post&lt;/li&gt;
  &lt;li&gt;Commit and push everything to GitHub Pages&lt;/li&gt;
  &lt;li&gt;Repeat — indefinitely if you want&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It tracks everything in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state.json&lt;/code&gt; file so you can stop it mid-run, restart later, and it picks up exactly where it left off.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;how-the-pipeline-works&quot;&gt;How the Pipeline Works&lt;/h2&gt;

&lt;p&gt;Each post goes through a four-stage LangGraph pipeline:&lt;/p&gt;

&lt;blockquote class=&quot;diagram-text&quot;&gt;&lt;p&gt;&lt;strong&gt;Diagram: The four-stage LangGraph pipeline behind each generated post&lt;/strong&gt; (&lt;a href=&quot;https://www.avinashgosavi.com/post/ai-history-blog-generator/&quot;&gt;see it in the post&lt;/a&gt;)&lt;/p&gt;&lt;p&gt;Claude or OpenAI picks the next milestone to cover. The research node looks it up on Wikipedia, on arXiv for science and tech topics, and fetches a Wikimedia image filtered for relevance and NSFW content. The write node runs a local LLM through Ollama: a WriterAgent with tool calling, using search_wikipedia, search_arxiv and write_post, runs multi-turn until write_post is called. The critique node, also on Ollama with structured output via Pydantic, checks that a year is mentioned, people are named, the section format and word count are right and there are no hallucinated URLs, and returns approved plus a list of issues. If approved, the post is published and pushed. If issues are found, a rewrite node fixes them and loops back, at most twice.&lt;/p&gt;&lt;p&gt;&lt;em&gt;A cloud model only picks the milestone; local models write and critique each post, and a draft with issues gets up to two rewrites.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Milestone selection&lt;/strong&gt; uses Claude (or OpenAI as fallback) via API — this is the “thinking” layer that picks what to write about. &lt;strong&gt;Post writing&lt;/strong&gt; is entirely local via Ollama, so you’re not paying per token for every paragraph.&lt;/p&gt;

&lt;p&gt;Before any of this starts, the agent checks two things to avoid covering the same ground twice: an exact slug match against &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state.json&lt;/code&gt;, and a semantic check — if the proposed milestone shares &amp;gt;40% keyword overlap with something already written in the same year, it’s rejected and a different one is picked.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;models-used&quot;&gt;Models Used&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Task&lt;/th&gt;
      &lt;th&gt;Model&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Milestone selection&lt;/td&gt;
      &lt;td&gt;Claude (Anthropic API) → OpenAI → Claude CLI&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Post writing&lt;/td&gt;
      &lt;td&gt;Ollama — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen2.5:14b&lt;/code&gt; (configurable)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Critique &amp;amp; structured validation&lt;/td&gt;
      &lt;td&gt;Ollama with Pydantic output&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The writing model is fully swappable via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env&lt;/code&gt;. The critique node uses Pydantic models to enforce structured JSON output so there’s no fragile text parsing — if the model doesn’t return valid JSON, it retries.&lt;/p&gt;

&lt;p&gt;Here’s how the models I tested compare on RAM footprint and post quality:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Model&lt;/th&gt;
      &lt;th&gt;RAM&lt;/th&gt;
      &lt;th&gt;RAM usage&lt;/th&gt;
      &lt;th&gt;Post quality&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;llama3.2:3b&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;~2 GB&lt;/td&gt;
      &lt;td&gt;▓▓░░░░░░░░&lt;/td&gt;
      &lt;td&gt;▓▓▓░░░░░░░&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mistral:7b&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;~4 GB&lt;/td&gt;
      &lt;td&gt;▓▓▓░░░░░░░&lt;/td&gt;
      &lt;td&gt;▓▓▓▓▓░░░░░&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen2.5:7b&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;~5 GB&lt;/td&gt;
      &lt;td&gt;▓▓▓▓░░░░░░&lt;/td&gt;
      &lt;td&gt;▓▓▓▓▓▓░░░░&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen2.5:14b&lt;/code&gt; ✦&lt;/td&gt;
      &lt;td&gt;~9 GB&lt;/td&gt;
      &lt;td&gt;▓▓▓▓▓░░░░░&lt;/td&gt;
      &lt;td&gt;▓▓▓▓▓▓▓▓░░&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;llama3.3:70b&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;~43 GB&lt;/td&gt;
      &lt;td&gt;▓▓▓▓▓▓▓▓▓░&lt;/td&gt;
      &lt;td&gt;▓▓▓▓▓▓▓▓▓░&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;✦ default&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen2.5:14b&lt;/code&gt; is the sweet spot for most machines — the quality jump from 7b is noticeable in post structure and factual coherence, and 9 GB fits comfortably on a laptop with 16 GB RAM. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;llama3.2:3b&lt;/code&gt; works if you’re just testing the pipeline; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;llama3.3:70b&lt;/code&gt; produces the best output but needs a well-specced machine or a dedicated GPU box.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h2&gt;

&lt;p&gt;You’ll need &lt;a href=&quot;https://ollama.com&quot;&gt;Ollama&lt;/a&gt; running locally and an Anthropic API key. The model is local so the only API cost is milestone selection (one call per post).&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1. Clone and install&lt;/span&gt;
git clone https://github.com/aag1091-alt/AI-History-Blog-Generator.git
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;AI-History-Blog-Generator
python3 &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; venv .venv &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; requirements.txt

&lt;span class=&quot;c&quot;&gt;# 2. Configure&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; .env.example .env
&lt;span class=&quot;c&quot;&gt;# Set ANTHROPIC_API_KEY and OLLAMA_MODEL in .env&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 3. Create a blog for any topic&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; generate &lt;span class=&quot;nt&quot;&gt;--new&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--topic&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;the history of jazz music&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--dir&lt;/span&gt; ~/projects/jazz-blog &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--github-url&lt;/span&gt; https://github.com/you/jazz-blog

&lt;span class=&quot;c&quot;&gt;# 4. Push to GitHub and enable GitHub Pages in repo settings&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/projects/jazz-blog &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; git push &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; origin main

&lt;span class=&quot;c&quot;&gt;# 5. Generate posts&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; generate          &lt;span class=&quot;c&quot;&gt;# one post&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; generate &lt;span class=&quot;nt&quot;&gt;--daemon&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# run forever (Ctrl+C to stop)&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; generate &lt;span class=&quot;nt&quot;&gt;--status&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# check progress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--new&lt;/code&gt; command clones a Jekyll template (layouts, CSS, dark/light theme, era pages, person profiles, live search) from the &lt;a href=&quot;https://github.com/aag1091-alt/AI-History-Blog/tree/template&quot;&gt;template branch&lt;/a&gt; and wires it up for your topic. From there, every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python -m generate&lt;/code&gt; run adds a new post and pushes it live.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-surprised-me&quot;&gt;What Surprised Me&lt;/h2&gt;

&lt;p&gt;The self-critique loop was the most interesting part to build. Early versions would occasionally write posts with no dates, or refer to people without naming them, or include made-up Wikipedia URLs. The critique node catches all of that with Pydantic-enforced checks and sends the post back for a rewrite with a specific list of issues. Most posts pass on the first try; the rewrite loop rarely triggers more than once.&lt;/p&gt;

&lt;p&gt;The other thing that exceeded my expectations was the image pipeline. Wikimedia Commons has an enormous archive, and the relevance filtering (matching image title keywords against the milestone title) produces surprisingly good results. The NSFW filter — checking filenames, metadata restrictions, and category labels — has never let anything through in my testing.&lt;/p&gt;

&lt;p&gt;What started as wanting a casual way to browse AI history turned into something I keep finding new uses for. If you have a topic you’ve always wanted a proper reference for, this is a pretty fast way to build one.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;Related: &lt;a href=&quot;/post/building-your-website-with-jekyll-and-github-pages/&quot;&gt;Building Your Website with Jekyll and GitHub Pages&lt;/a&gt; walks through the Jekyll + GitHub Pages setup the generated blogs are published with, and &lt;a href=&quot;/uses/&quot;&gt;/uses&lt;/a&gt; lists the home server I run local models on with Ollama.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
</feed>
