<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: İclal Doğan</title>
    <description>The latest articles on DEV Community by İclal Doğan (@iclaldogan).</description>
    <link>https://dev.to/iclaldogan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3954902%2F2bd39086-cd3f-4b07-9bd7-f830c749f7ea.jpg</url>
      <title>DEV Community: İclal Doğan</title>
      <link>https://dev.to/iclaldogan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iclaldogan"/>
    <language>en</language>
    <item>
      <title>I Built a 1920s Butler AI That Runs Entirely on My Linux Machine. Then I Abandoned It. Then Copilot Helped Me Fix It.</title>
      <dc:creator>İclal Doğan</dc:creator>
      <pubDate>Sat, 06 Jun 2026 12:22:25 +0000</pubDate>
      <link>https://dev.to/iclaldogan/i-built-a-1920s-butler-ai-that-runs-entirely-on-my-linux-machine-then-i-abandoned-it-then-copilot-5ela</link>
      <guid>https://dev.to/iclaldogan/i-built-a-1920s-butler-ai-that-runs-entirely-on-my-linux-machine-then-i-abandoned-it-then-copilot-5ela</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bantz&lt;/strong&gt; is a local-first, offline-capable AI assistant that runs entirely on your Linux machine. It presents itself as a 1920s English butler — always polite, subtly sarcastic, and absolutely convinced he is a real person standing in the room with you.&lt;/p&gt;

&lt;p&gt;I'm a Turkish speaker on a Linux desktop. Every cloud assistant I tried spoke to me in a foreign language, phoned home to someone else's server, and forgot everything the moment the session ended. I wanted something different: an assistant that speaks Turkish natively, runs on my own hardware, remembers its context, and actually controls my desktop. So I started building Bantz.&lt;/p&gt;

&lt;p&gt;The concept is ambitious. At its core: a Turkish ↔ English translation layer powered by Helsinki-NLP's MarianMT, a multi-step tool planner that can chain web search, Gmail, Calendar, shell commands, filesystem access, and AT-SPI desktop automation — all coordinated by an LLM running locally via Ollama. On top of that: voice I/O via faster-whisper and Piper TTS, persistent memory backed by ChromaDB + a SQLite knowledge graph (MemPalace), a 6-state butler persona that shifts tone based on CPU load and time of day, and a Textual TUI with a live health-status bar.&lt;/p&gt;

&lt;p&gt;The architecture was genuinely interesting. The execution, as of May 2026, was a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub repo:&lt;/strong&gt; &lt;a href="https://github.com/miclaldogan/bantzv2" rel="noopener noreferrer"&gt;github.com/miclaldogan/bantzv2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broadcast Channel&lt;/strong&gt; — chatting with Bantz, web search + desktop control in action:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2Fseg1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2Fseg1.gif" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full walkthrough&lt;/strong&gt; — all pages of the Operations Center:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2Fseg5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2Fseg5.gif" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2FbantzChat.jpeg" width="800" height="450"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2FbantzVitals.jpeg" width="800" height="450"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2FbantzLogs.jpeg" width="800" height="450"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2FbantzDirectives.jpeg" width="800" height="450"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2FbantzAnomalyWatch.jpeg" width="800" height="450"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmiclaldogan%2Fbantzv2%2Fmain%2Fbantz-demo%2FbantzSettings.jpeg" width="800" height="450"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before — May 2026
&lt;/h3&gt;

&lt;p&gt;I had a 17-issue backlog and a &lt;code&gt;BROKEN_STATE.md&lt;/code&gt; file I'd written to document the damage. Here's what it said:&lt;/p&gt;

&lt;p&gt;The feature audit was brutal:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Voice input (Whisper)&lt;/td&gt;
&lt;td&gt;🔴 Broken — 3 packages missing, Picovoice key unset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TUI status bar&lt;/td&gt;
&lt;td&gt;❌ Didn't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-run onboarding&lt;/td&gt;
&lt;td&gt;❌ Blank cursor, zero guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-provider LLM support&lt;/td&gt;
&lt;td&gt;🔴 Every finalizer hardcoded &lt;code&gt;ollama&lt;/code&gt; directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turkish response latency&lt;/td&gt;
&lt;td&gt;🔴 12–18 seconds end-to-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--doctor&lt;/code&gt; diagnostic&lt;/td&gt;
&lt;td&gt;🔴 Actively lying — reported working memory as broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TUI rendering&lt;/td&gt;
&lt;td&gt;🔴 Entire layout duplicated on screen after every message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop UI logs&lt;/td&gt;
&lt;td&gt;🔴 WebSocket handler silently crashed on every log event&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most embarrassing part: I'd built a sophisticated multi-provider LLM router (&lt;code&gt;router.py&lt;/code&gt;) that could dispatch to Claude, OpenAI, Gemini, or Ollama based on config — and then every single callsite in &lt;code&gt;finalizer.py&lt;/code&gt;, &lt;code&gt;summarizer.py&lt;/code&gt;, and the streaming path had just... hardcoded &lt;code&gt;from bantz.llm.ollama import ollama&lt;/code&gt; directly. The router was completely bypassed. Anyone who configured &lt;code&gt;BANTZ_LLM_PROVIDER=claude&lt;/code&gt; would get Ollama responses with no error, no warning, nothing.&lt;/p&gt;

&lt;p&gt;The first-run experience was particularly painful. &lt;code&gt;bantz --once "merhaba"&lt;/code&gt; would hang in complete silence for up to 30 seconds as MarianMT loaded, Ollama inferred, and Piper synthesized — all sequentially, all silently. New users killed the process and never came back.&lt;/p&gt;

&lt;h3&gt;
  
  
  After — June 2026
&lt;/h3&gt;

&lt;p&gt;Seven issues closed in a single focused session, all squash-merged to &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #467 — 1-line fix, total silence explained.&lt;/strong&gt; The &lt;code&gt;_WSLogHandler&lt;/code&gt; inside &lt;code&gt;WsBroadcastServer&lt;/code&gt; referenced &lt;code&gt;self._log_q&lt;/code&gt; but the actual queue attribute was &lt;code&gt;self._q&lt;/code&gt;. One character typo. Every log record since the WebSocket server was written had thrown an &lt;code&gt;AttributeError&lt;/code&gt; that got silently swallowed, so the Tauri desktop UI had received zero log output. Fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PRs #468 &amp;amp; #469 — The router that wasn't routed to.&lt;/strong&gt; Replaced all three &lt;code&gt;finalizer.py&lt;/code&gt; callsites and the &lt;code&gt;summarizer.py&lt;/code&gt; Gemini/Ollama fallback chain with &lt;code&gt;from bantz.llm.router import get_llm&lt;/code&gt;. Added &lt;code&gt;get_llm = get_provider&lt;/code&gt; as a convenience alias in &lt;code&gt;router.py&lt;/code&gt;. Claude, OpenAI, and Gemini users now actually get their configured provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #470 — Service dots that told the truth.&lt;/strong&gt; The TUI's health-status bar was initialised with the hardcoded key &lt;code&gt;"Ollama"&lt;/code&gt; and &lt;code&gt;_probe_services()&lt;/code&gt; only called &lt;code&gt;check_ollama()&lt;/code&gt;. Added dynamic service key resolution from config, new &lt;code&gt;check_claude()&lt;/code&gt; and &lt;code&gt;check_openai()&lt;/code&gt; coroutines, and a dispatch table to route to the right health check based on the active provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #471 — The TUI duplication bug.&lt;/strong&gt; &lt;code&gt;_erase_prompt_line()&lt;/code&gt; used &lt;code&gt;os.write(1, ...)&lt;/code&gt; to send raw ANSI cursor-movement escapes directly to stdout while Rich Live was simultaneously rendering to the same terminal. This caused a race condition that reproduced the entire TUI block below the real one on every message. The fix: added a &lt;code&gt;Layout(name="prompt", size=1)&lt;/code&gt; panel to the layout tree and replaced the raw write with a state variable (&lt;code&gt;self._prompt_text = ""&lt;/code&gt;). The next Live refresh cycle clears the row cleanly, no escapes needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #472 — Cutting Turkish response latency from 18s to under 10s.&lt;/strong&gt; Two independent problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;bridge.to_turkish()&lt;/code&gt; ran on the &lt;strong&gt;full accumulated response&lt;/strong&gt; after all LLM inference finished — sequential, never overlapping.&lt;/li&gt;
&lt;li&gt;No caching. Identical butler stock phrases like &lt;code&gt;"Done. ✓"&lt;/code&gt; re-ran the full neural translation model every single time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Added a 256-entry FIFO LRU cache to &lt;code&gt;_Translator&lt;/code&gt; — common phrases now translate in ~0ms after the first call. Then rewrote &lt;code&gt;finalize_stream()._stream()&lt;/code&gt; to buffer LLM tokens until sentence boundaries (&lt;code&gt;(?&amp;lt;=[.!?])\s+&lt;/code&gt;) and call &lt;code&gt;bridge.to_turkish()&lt;/code&gt; per sentence immediately, yielding translated output while the LLM continues generating the next sentence. Translation now overlaps inference instead of running after it. Also removed the redundant &lt;code&gt;await _to_tr("".join(parts))&lt;/code&gt; re-translation in &lt;code&gt;ws_server.py&lt;/code&gt;'s streaming path — &lt;code&gt;finalize_stream&lt;/code&gt; already emits pre-translated tokens when the bridge is enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue #463 — Already fixed (no PR needed).&lt;/strong&gt; Copilot confirmed &lt;code&gt;Live(screen=False)&lt;/code&gt; was already set and &lt;code&gt;REFRESH_FPS = 4&lt;/code&gt; was already present. Closed with an explanatory comment. Sometimes the right fix is recognising there's nothing to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;I used Copilot in agent mode (Claude Sonnet 4.6) for the entire session. What struck me most was the discipline it brought to a codebase I'd let get messy.&lt;/p&gt;

&lt;p&gt;For every issue, Copilot followed the same workflow without being told to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the affected file before touching anything.&lt;/strong&gt; Not a summary — the actual file, end-to-end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search for the exact symbol causing the problem.&lt;/strong&gt; For issue #462, it searched &lt;code&gt;_q|_log_q&lt;/code&gt; in &lt;code&gt;ws_server.py&lt;/code&gt; and immediately surfaced the mismatch. For #465, it searched for every &lt;code&gt;"Ollama"&lt;/code&gt; string literal and found three hardcoded sites at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the minimal change.&lt;/strong&gt; No unrelated refactors. The &lt;code&gt;_log_q&lt;/code&gt; → &lt;code&gt;_q&lt;/code&gt; fix is literally one word on one line. The router migration replaced 3 identical patterns with identical 2-line substitutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify syntax before committing.&lt;/strong&gt; &lt;code&gt;python -m py_compile &amp;lt;file&amp;gt;&lt;/code&gt; on every changed file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the commit message and PR body, then create and merge the PR via &lt;code&gt;gh&lt;/code&gt;.&lt;/strong&gt; Including verifying the issue closed with &lt;code&gt;gh issue view NNN --json state&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most valuable moment was on issue #422 (translation latency). I knew the translation was slow but I'd assumed it was just a hardware limitation — MarianMT on CPU takes what it takes. Copilot traced the full data flow from &lt;code&gt;finalize_stream()&lt;/code&gt; through &lt;code&gt;ws_server.py&lt;/code&gt; and identified that the bottleneck wasn't just the model — it was the architecture: sequential execution after completion, plus identical inputs being re-translated on every call. The sentence-boundary streaming approach it introduced had never occurred to me, and it worked on the first try.&lt;/p&gt;

&lt;p&gt;The other moment I appreciated: issue #463. Rather than making a change to justify its existence, Copilot searched for &lt;code&gt;screen=&lt;/code&gt; in &lt;code&gt;live_ui.py&lt;/code&gt;, confirmed the value was already &lt;code&gt;False&lt;/code&gt;, checked &lt;code&gt;REFRESH_FPS&lt;/code&gt;, and told me the issue was already resolved. Closing a bug report with "this is already fixed" is the correct outcome. That kind of restraint is hard to get from a tool optimised to produce output.&lt;/p&gt;

&lt;p&gt;Bantz isn't finished, voice input still needs its three packages, the &lt;code&gt;--doctor&lt;/code&gt; output still needs polish, and I want to add a proper onboarding flow. But the core pipeline now works correctly for all supported LLM providers, the TUI renders cleanly, Turkish responses arrive in under 10 seconds, and the butler's logs finally reach the desktop UI. That's a project that went from "broken in embarrassing ways" to "actually ships" — and Copilot was the pairing partner that made it happen in a single afternoon.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
