The Docu-Bot System is a custom automation tool designed to solve a common IT problem: the disconnect between "fixing the issue" (the technical work) and "writing the documentation" (the administrative work).
By integrating hardware macros (Stream Deck) with Cloud AI (Google Gemma 3), this tool allows me to capture real-time "trial and error" logs during deployment and automatically synthesize them into professional, print-ready HTML/PDF guides upon project completion.
The solution utilizes a two-stage architecture: The Logger (Real-time capture) and The Writer (Post-project synthesis).
The system relies on two primary Python executables. The first handles input, the second handles generation.
Runs silently in the background. It utilizes pyautogui for screen capture and
requests to push payloads to Discord.
AI Model Strategy: To avoid the restrictive rate limits of proprietary models, I implemented Google's
Gemma 3 27b (IT). This "Open Weights" multimodal model offers near-GPT-4 level vision capabilities
while allowing for essentially unlimited daily requests (14,000+). This allows every single screenshot
to be analyzed by AI without fear of hitting quotas.
Below is the complete source code for doc_tool.py.
import sys
import requests
import pyperclip
import time
import tkinter as tk
from tkinter import ttk, font
from io import BytesIO
from datetime import datetime
import mss
import mss.tools
from PIL import Image, ImageGrab
import pyautogui
import ctypes
# --- OPTIONAL IMPORTS ---
try:
from google import genai
HAS_AI = True
except ImportError:
HAS_AI = False
# --- CONFIGURATION ---
# 1. PASTE YOUR DISCORD WEBHOOK URL HERE
WEBHOOK_URL = "YOUR_DISCORD_WEBHOOK_URL_HERE"
# 2. PASTE YOUR GOOGLE GEMINI API KEY HERE
GEMINI_API_KEY = "YOUR_GEMINI_API_KEY_HERE"
# Files to store your state
CONFIG_FILE = "current_project.txt"
HISTORY_FILE = "project_history.txt"
# --- AI SETUP ---
client = None
if HAS_AI and GEMINI_API_KEY != "YOUR_GEMINI_API_KEY_HERE":
try:
client = genai.Client(api_key=GEMINI_API_KEY)
except Exception as e:
print(f"AI Init Error: {e}")
# --- MODERN UI HELPERS ---
class DarkDialog:
def __init__(self, title, prompt, initial_value=None, options=None, is_input_box=False):
self.result = None
self.root = tk.Tk()
self.root.title(title)
# --- CUSTOM THEME (Dark & Red) ---
BG_COLOR = "#1E1E1E"
FG_COLOR = "#FFFFFF"
ACCENT_COLOR = "#AB162B"
INPUT_BG = "#2D2D2D"
self.root.configure(bg=BG_COLOR)
try:
self.root.update()
# Windows Dark Title Bar Hack
ctypes.windll.dwmapi.DwmSetWindowAttribute(
ctypes.windll.user32.GetParent(self.root.winfo_id()), 20, ctypes.byref(ctypes.c_int(1)), 4
)
except Exception:
pass
try:
custom_font = font.Font(family="Segoe UI", size=11)
except:
custom_font = font.Font(family="Arial", size=11)
w, h = 400, 200 if is_input_box else 180
ws, hs = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
x_pos = int((ws/2) - (w/2))
y_pos = int((hs/2) - (h/2))
self.root.geometry(f"{w}x{h}+{x_pos}+{y_pos}")
self.root.lift()
self.root.attributes('-topmost', True)
self.root.focus_force()
lbl = tk.Label(self.root, text=prompt, bg=BG_COLOR, fg=FG_COLOR, font=custom_font, wraplength=380)
lbl.pack(pady=(20, 10), padx=20)
self.var = tk.StringVar()
if initial_value:
self.var.set(initial_value)
if options:
self.entry = ttk.Combobox(self.root, textvariable=self.var, values=options, font=custom_font)
self.entry.pack(pady=5, padx=20, fill="x")
else:
self.entry = tk.Entry(self.root, textvariable=self.var, bg=INPUT_BG, fg=FG_COLOR,
insertbackground="white", font=custom_font, relief="flat")
self.entry.pack(pady=5, padx=20, fill="x")
self.entry.focus_force()
if hasattr(self.entry, 'select_range'):
self.entry.select_range(0, 'end')
self.entry.icursor('end')
btn_frame = tk.Frame(self.root, bg=BG_COLOR)
btn_frame.pack(pady=20)
btn_text = "Confirm"
ok_btn = tk.Button(btn_frame, text=btn_text, command=self.on_confirm,
bg=ACCENT_COLOR, fg="white", font=("Segoe UI", 10, "bold"),
activebackground="#8a1222", activeforeground="white",
relief="flat", padx=15, pady=5)
ok_btn.pack(side="left", padx=10)
self.root.bind('', self.on_confirm)
self.root.bind('', lambda e: self.root.destroy())
self.root.mainloop()
def on_confirm(self, event=None):
self.result = self.var.get().strip()
self.root.destroy()
# --- HELPER: GET CONTEXT ---
def get_project_context():
try:
with open(CONFIG_FILE, "r") as f:
data = f.read().strip().split("|")
if len(data) >= 2:
t_id = data[1] if data[1] != "None" else None
return data[0], t_id
return data[0], None
except FileNotFoundError:
return "General", None
# --- HELPER: UPLOAD IMAGE ---
def _upload_image_object(img, source_label="Screenshot", ai_analysis=None):
img_byte_arr = BytesIO()
img.save(img_byte_arr, format='PNG')
img_byte_arr.seek(0)
name, thread_id = get_project_context()
timestamp = datetime.now().strftime("%H-%M-%S")
target_url = f"{WEBHOOK_URL}?thread_id={thread_id}" if thread_id else WEBHOOK_URL
# Construct Message
message_content = f"**[{name}]** {source_label}"
if ai_analysis:
message_content += f"\n> **π€ AI Insight:** {ai_analysis}"
payload = {"content": message_content}
files = {
"file": (f"image_{timestamp}.png", img_byte_arr, "image/png")
}
try:
requests.post(target_url, data=payload, files=files)
print(f"Uploaded to {name}")
except Exception as e:
print(f"Upload failed: {e}")
# --- AI LOGIC ---
def process_image_intelligence(img):
"""Runs Cloud AI analysis on the image using Gemma 3."""
if not client: return None
# EXCLUSIVE: Using Gemma 3 27b-it (14,400 daily limit)
model_name = "gemma-3-27b-it"
prompt = (
"Describe what you see in this image in plain English. "
"If it shows a technical error, code issue, or warning, suggest a potential solution. "
"Otherwise, just describe the content concisely."
)
try:
response = client.models.generate_content(
model=model_name,
contents=[prompt, img]
)
print(f"AI Success using model: {model_name}")
return response.text.strip()
except Exception as e:
print(f"Model '{model_name}' failed. Reason: {e}")
return None
# --- EUREKA MODE ---
def eureka_log():
"""Captures the solution with a special tag for the documentation generator."""
pyperclip.copy("")
pyautogui.hotkey('winleft', 'shift', 's')
print("Waiting for snip...")
start_time = time.time()
img = None
while (time.time() - start_time) < 60:
img = ImageGrab.grabclipboard()
if isinstance(img, Image.Image):
break
time.sleep(0.5)
if not img: return
dlg = DarkDialog("Eureka!", "What was the fix? (Be specific):", is_input_box=True)
fix_description = dlg.result
if not fix_description: return
# Eureka ALWAYS uses AI
ai_text = process_image_intelligence(img)
combined_analysis = f"USER FIX: {fix_description}\nIMAGE CONTEXT: {ai_text}"
_upload_image_object(img, source_label="βββ SOLUTION VERIFIED βββ", ai_analysis=combined_analysis)
# --- STANDARD CAPTURE FUNCTIONS ---
def snip_and_log():
pyperclip.copy("")
pyautogui.hotkey('winleft', 'shift', 's')
start_time = time.time()
while (time.time() - start_time) < 60:
img = ImageGrab.grabclipboard()
if isinstance(img, Image.Image):
# Process AI (Always on now due to Gemma 3 capacity)
ai_text = process_image_intelligence(img)
_upload_image_object(img, source_label="Snipped Region", ai_analysis=ai_text)
return
time.sleep(0.5)
def quick_note():
name, _ = get_project_context()
dlg = DarkDialog("Quick Note", f"Log entry for [{name}]:", is_input_box=True)
if dlg.result:
send_text_to_discord(dlg.result)
def send_text_to_discord(text):
name, thread_id = get_project_context()
target_url = f"{WEBHOOK_URL}?thread_id={thread_id}" if thread_id else WEBHOOK_URL
data = {"username": "Docu-Bot", "content": f"**[{name}]** {text}"}
requests.post(target_url, json=data)
def log_code_block():
content = pyperclip.paste()
if not content: return
name, thread_id = get_project_context()
target_url = f"{WEBHOOK_URL}?thread_id={thread_id}" if thread_id else WEBHOOK_URL
if len(content) > 1900:
from io import BytesIO
file_data = BytesIO(content.encode('utf-8'))
payload = {"content": f"**[{name}]** Code Snippet (Converted to file):"}
files = {"file": ("snippet.txt", file_data, "text/plain")}
requests.post(target_url, data=payload, files=files)
else:
formatted_msg = f"**[{name}] Code Snippet:**\n```\n{content}\n```"
requests.post(target_url, json={"content": formatted_msg})
def capture_screen(monitor_index=1):
with mss.mss() as sct:
if monitor_index >= len(sct.monitors): monitor_index = 1
monitor = sct.monitors[monitor_index]
sct_img = sct.grab(monitor)
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
mon_label = "Primary" if monitor_index == 1 else f"Monitor {monitor_index}"
_upload_image_object(img, source_label=f"Screenshot ({mon_label})")
def log_clipboard():
img = ImageGrab.grabclipboard()
if isinstance(img, Image.Image):
# We process AI for clipboard images too now
ai_text = process_image_intelligence(img)
_upload_image_object(img, source_label="Clipboard Image", ai_analysis=ai_text)
else:
content = pyperclip.paste()
if content:
send_text_to_discord(f"**Reference:**\n{content}")
# --- PROJECT MANAGEMENT ---
def save_project_history(name, thread_id):
entry = f"{name}|{thread_id}"
try:
with open(HISTORY_FILE, "r") as f:
history = [line.strip() for line in f.readlines()]
except FileNotFoundError:
history = []
history = [h for h in history if not h.startswith(f"{name}|")]
history.insert(0, entry)
with open(HISTORY_FILE, "w") as f:
f.write("\n".join(history[:10]))
def set_project():
try:
with open(HISTORY_FILE, "r") as f:
raw_history = [line.strip() for line in f.readlines()]
except FileNotFoundError:
raw_history = []
history_map = {}
display_list = []
for line in raw_history:
parts = line.split("|")
if len(parts) >= 2:
history_map[parts[0]] = parts[1]
display_list.append(parts[0])
dialog = DarkDialog("Select Context", "Select Active Project:",
initial_value=display_list[0] if display_list else "General",
options=display_list + ["NEW PROJECT..."])
name = dialog.result
if not name: return
thread_id = None
is_new_entry = (name == "NEW PROJECT...") or (name not in history_map)
if is_new_entry:
if name == "NEW PROJECT...":
name_dlg = DarkDialog("New Project", "Enter Project Name:", is_input_box=True)
name = name_dlg.result
if name:
id_dlg = DarkDialog("Thread ID", f"Paste Discord Thread ID for '{name}':\n(Leave empty for default)", is_input_box=True)
thread_id = id_dlg.result
elif name in history_map:
thread_id = history_map[name]
if name:
save_id = thread_id if thread_id else "None"
with open(CONFIG_FILE, "w") as f:
f.write(f"{name}|{save_id}")
save_project_history(name, save_id)
# --- MAIN EXECUTION ---
if __name__ == "__main__":
if len(sys.argv) > 1:
command = sys.argv[1]
if command == "set_project": set_project()
elif command == "note": quick_note()
elif command == "clipboard": log_clipboard()
elif command == "code": log_code_block()
elif command == "snip": snip_and_log()
elif command == "eureka": eureka_log()
elif command == "screenshot":
try: mon_idx = int(sys.argv[2])
except IndexError: mon_idx = 1
capture_screen(mon_idx)
This script acts as the "Technical Writer." It is executed when a project is marked complete. It uses a sophisticated Two-Stage AI Pipeline to ensure documentation accuracy and includes a real-time progress dashboard.
Below is the logic for the documentation generator, including the Tkinter Dashboard and the streamlined Gemma 3 logic.
import requests
import os
import re
import datetime
import tkinter as tk
from tkinter import ttk, font, scrolledtext
import ctypes
from google import genai
# --- CONFIGURATION ---
# 1. DISCORD BOT TOKEN (Required to READ history)
DISCORD_BOT_TOKEN = "PASTE_YOUR_BOT_TOKEN_HERE"
# 2. GOOGLE GEMINI API KEY (Required to WRITE the report)
GEMINI_API_KEY = "PASTE_YOUR_GEMINI_API_KEY_HERE"
# 3. OUTPUT FOLDER
OUTPUT_DIR = "Finished_Docs"
HISTORY_FILE = "project_history.txt"
# --- SETUP CLIENTS ---
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
try:
client = genai.Client(api_key=GEMINI_API_KEY)
except Exception as e:
print(f"Error initializing AI: {e}")
exit()
# --- UI CLASS (Dark Mode Selection) ---
class DarkDialog:
def __init__(self, title, prompt, initial_value=None, options=None, is_input_box=False):
self.result = None
self.root = tk.Tk()
self.root.title(title)
self.setup_window(prompt, initial_value, options, is_input_box)
self.root.mainloop()
def setup_window(self, prompt, initial_value, options, is_input_box):
BG_COLOR, FG_COLOR, ACCENT = "#1E1E1E", "#FFFFFF", "#AB162B"
self.root.configure(bg=BG_COLOR)
self.apply_dark_title_bar()
self.center_window(400, 220 if is_input_box else 180)
self.root.attributes('-topmost', True)
lbl = tk.Label(self.root, text=prompt, bg=BG_COLOR, fg=FG_COLOR, font=("Segoe UI", 11), wraplength=380)
lbl.pack(pady=(20, 10), padx=20)
self.var = tk.StringVar()
if initial_value: self.var.set(initial_value)
if options:
self.entry = ttk.Combobox(self.root, textvariable=self.var, values=options, font=("Segoe UI", 11))
self.entry.pack(pady=5, padx=20, fill="x")
else:
self.entry = tk.Entry(self.root, textvariable=self.var, bg="#2D2D2D", fg=FG_COLOR, insertbackground="white", font=("Segoe UI", 11), relief="flat")
self.entry.pack(pady=5, padx=20, fill="x")
self.entry.focus_force()
btn_frame = tk.Frame(self.root, bg=BG_COLOR)
btn_frame.pack(pady=20)
btn_text = "Confirm" if is_input_box else "Generate Reports"
tk.Button(btn_frame, text=btn_text, command=self.on_confirm, bg=ACCENT, fg="white", font=("Segoe UI", 10, "bold"), relief="flat", padx=15, pady=5).pack()
self.root.bind('', self.on_confirm)
self.root.bind('', lambda e: self.root.destroy())
def apply_dark_title_bar(self):
try:
self.root.update()
ctypes.windll.dwmapi.DwmSetWindowAttribute(ctypes.windll.user32.GetParent(self.root.winfo_id()), 20, ctypes.byref(ctypes.c_int(1)), 4)
except: pass
def center_window(self, w, h):
ws, hs = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
self.root.geometry(f"{w}x{h}+{int((ws/2)-(w/2))}+{int((hs/2)-(h/2))}")
def on_confirm(self, event=None):
self.result = self.var.get().strip()
self.root.destroy()
# --- PROGRESS DASHBOARD ---
class ProgressWindow:
def __init__(self, project_name):
self.root = tk.Tk()
self.root.title(f"Generating: {project_name}")
BG, FG, ACCENT = "#1E1E1E", "#E0E0E0", "#007ACC"
self.root.configure(bg=BG)
# Sizing
w, h = 900, 600
ws, hs = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
self.root.geometry(f"{w}x{h}+{int((ws/2)-(w/2))}+{int((hs/2)-(h/2))}")
# 1. Header
header = tk.Label(self.root, text="AUTOMATED DOCUMENTATION ENGINE", bg=BG, fg="#555", font=("Segoe UI", 9, "bold"))
header.pack(pady=(10, 0), anchor="w", padx=10)
title = tk.Label(self.root, text=project_name, bg=BG, fg="white", font=("Segoe UI", 20, "bold"))
title.pack(pady=(0, 10), anchor="w", padx=10)
# 2. Split View
pane = tk.PanedWindow(self.root, orient=tk.HORIZONTAL, bg=BG, sashwidth=4, sashrelief="flat")
pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Left: System Logs
log_frame = tk.Frame(pane, bg=BG)
tk.Label(log_frame, text="SYSTEM LOGS", bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(anchor="w")
self.log_box = scrolledtext.ScrolledText(log_frame, bg="#111", fg="#00FF00", font=("Consolas", 9), height=10)
self.log_box.pack(fill=tk.BOTH, expand=True)
pane.add(log_frame, minsize=300)
# Right: AI Brain
brain_frame = tk.Frame(pane, bg=BG)
tk.Label(brain_frame, text="AI INTERPRETATION (THE GOLDEN PATH)", bg=BG, fg="#007ACC", font=("Segoe UI", 8, "bold")).pack(anchor="w")
self.brain_box = scrolledtext.ScrolledText(brain_frame, bg="#252526", fg="#D4D4D4", font=("Segoe UI", 10), height=10)
self.brain_box.pack(fill=tk.BOTH, expand=True)
pane.add(brain_frame, minsize=400)
# 3. Footer / Status
self.status_lbl = tk.Label(self.root, text="Initializing...", bg="#007ACC", fg="white", font=("Segoe UI", 10), anchor="w", padx=10, pady=5)
self.status_lbl.pack(fill="x", side="bottom")
self.root.update()
def log(self, text):
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
self.log_box.insert(tk.END, f"[{timestamp}] {text}\n")
self.log_box.see(tk.END)
self.root.update()
def set_brain(self, text):
self.brain_box.delete('1.0', tk.END)
self.brain_box.insert(tk.END, text)
self.root.update()
def set_status(self, text, color="#007ACC"):
self.status_lbl.config(text=text, bg=color)
self.root.update()
def close(self):
self.root.destroy()
# --- HELPER: GET PROJECT ---
def select_project_target():
history_map = {}
display_list = []
try:
with open(HISTORY_FILE, "r") as f:
lines = [line.strip() for line in f.readlines()]
for line in lines:
parts = line.split("|")
if len(parts) >= 2:
history_map[parts[0]] = parts[1]
display_list.append(parts[0])
except FileNotFoundError: pass
dialog = DarkDialog("Docu-Maker", "Select a Project to Document:",
initial_value=display_list[0] if display_list else "",
options=display_list + ["Manual Input..."])
selection = dialog.result
if not selection: return None, None
if selection == "Manual Input...":
id_dlg = DarkDialog("Manual Input", "Paste the Discord Thread ID:", is_input_box=True)
return "Manual_Project", id_dlg.result
return selection, history_map.get(selection, selection)
def get_user_hints():
dialog = DarkDialog("AI Guidance", "Any specific 'Golden Path' hints? (e.g. 'Ignore the DNS errors'):", is_input_box=True)
return dialog.result if dialog.result else "None provided."
# --- LOGIC ---
def get_thread_history(thread_id, progress):
progress.log(f"Connecting to Discord (Thread: {thread_id})...")
headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
base_url = f"https://discord.com/api/v10/channels/{thread_id}"
resp = requests.get(base_url, headers=headers)
if resp.status_code != 200:
progress.log(f"β Error fetching thread: {resp.status_code}")
return None, None
project_name = resp.json().get("name", "Project_Report")
progress.log("Downloading message history...")
messages = []
last_id = None
count = 0
while True:
url = f"{base_url}/messages?limit=100"
if last_id: url += f"&before={last_id}"
m_resp = requests.get(url, headers=headers)
if m_resp.status_code != 200: break
batch = m_resp.json()
if not batch: break
messages.extend(batch)
last_id = batch[-1]["id"]
count += len(batch)
progress.log(f" Fetched {count} messages...")
progress.log("Download Complete.")
return project_name, messages
def format_log_for_ai(messages):
transcript = "PROJECT LOG TRANSCRIPT:\n\n"
for msg in reversed(messages):
author = msg["author"]["username"]
content = msg["content"]
timestamp = msg["timestamp"].split("T")[0]
if author == "Docu-Bot": author = "System Log"
transcript += f"[{timestamp}] {author}: {content}\n"
if msg.get("attachments"):
for att in msg["attachments"]:
if att.get("content_type", "").startswith("image/"):
transcript += f" [IMAGE_EMBED_SOURCE: {att['url']}]\n"
transcript += "---\n"
return transcript
def call_ai_generator(prompt, progress, stage_name):
# EXCLUSIVE: Using Gemma 3 27b-it
model_name = "gemma-3-27b-it"
progress.log(f"Starting {stage_name} (Using {model_name})...")
try:
response = client.models.generate_content(model=model_name, contents=prompt)
progress.log(" Success!")
return response.text
except Exception as e:
progress.log(f" β Failed: {e}")
return None
# --- STAGES ---
def extract_golden_logic(project_name, transcript, user_hints, progress):
progress.set_status("Stage 1: Analyzing Logs for Solutions...")
prompt = f"""
You are a Lead Engineer reviewing logs for "{project_name}".
USER HINTS: {user_hints}
CRITICAL: Look for entries labeled "βββ SOLUTION VERIFIED βββ". Treat these as absolute truth.
TASK: Extract ONLY the final, working configuration. Ignore errors.
OUTPUT: A clean, step-by-step summary.
LOGS: {transcript}
"""
return call_ai_generator(prompt, progress, "Stage 1 (Filter)")
def generate_process_log(project_name, transcript, progress):
progress.set_status("Stage 2: Writing Process Log...")
prompt = f"""
Create a "Process & Troubleshooting Log" HTML file for "{project_name}".
STYLE: Dark/Neutral theme. Document the journey, errors, and debugging process.
IMAGES: Embed [IMAGE_EMBED_SOURCE: url] as <img src="url">.
LOGS: {transcript}
"""
return call_ai_generator(prompt, progress, "Stage 2 (Messy Log)")
def generate_golden_guide(project_name, clean_summary, progress):
progress.set_status("Stage 3: Drafting Golden Guide...")
today_date = datetime.datetime.now().strftime("%B %d, %Y")
prompt = f"""
Create a "Technical Configuration Guide" HTML file for "{project_name}".
AUDIENCE: Future staff. CONSTRAINT: No trial and error. Final Steps only.
STYLE (RCBC):
1. BANNER: <img src="images/banner.png" style="width:100%; max-width:800px; display:block; margin-bottom: 20px;">
2. SUB-HEADER: "FACILITY: {project_name.upper()} // UPDATED: {today_date}"
3. H1 HEADERS: Uppercase, #C8102E (RCBC Red).
4. IMAGES: Embed images from summary.
CLEAN SUMMARY: {clean_summary}
"""
return call_ai_generator(prompt, progress, "Stage 3 (Golden Guide)")
def clean_ai_output(text):
if not text: return ""
if text.startswith("```html"): text = text.replace("```html", "", 1)
if text.endswith("```"): text = text[:-3]
return text.strip()
def sanitize_filename(name):
# Fixed regex to properly handle windows invalid chars
return re.sub(r'[<>:"/\\|?*]', '_', name).strip()
# --- MAIN ---
if __name__ == "__main__":
p_name, t_id = select_project_target()
if t_id:
hints = get_user_hints()
# Open Progress Dashboard
progress = ProgressWindow(p_name)
real_name, msgs = get_thread_history(t_id, progress)
if msgs:
log_text = format_log_for_ai(msgs)
final_name = sanitize_filename(real_name if real_name else p_name)
# 1. RUN THE FILTER
golden_summary = extract_golden_logic(final_name, log_text, hints, progress)
# --- DISPLAY AI BRAIN ---
if golden_summary:
progress.set_brain(golden_summary)
# Save the "Brain" to a file for audit
brain_path = os.path.join(OUTPUT_DIR, f"{final_name}_AI_Context.txt")
with open(brain_path, "w", encoding="utf-8") as f: f.write(golden_summary)
# 2. GENERATE MESSY LOG
html_1 = generate_process_log(final_name, log_text, progress)
if html_1:
path_1 = os.path.join(OUTPUT_DIR, f"{final_name}_Process_Log.html")
with open(path_1, "w", encoding="utf-8") as f: f.write(clean_ai_output(html_1))
progress.log(f"Saved: {final_name}_Process_Log.html")
# 3. GENERATE GOLDEN GUIDE
if golden_summary:
html_2 = generate_golden_guide(final_name, golden_summary, progress)
if html_2:
path_2 = os.path.join(OUTPUT_DIR, f"{final_name}_RCBC_Guide.html")
with open(path_2, "w", encoding="utf-8") as f: f.write(clean_ai_output(html_2))
progress.log(f"Saved: {final_name}_RCBC_Guide.html")
progress.set_status("COMPLETE. You may close this window.", color="#28a745")
progress.root.mainloop() # Keep window open
else:
print("No messages found.")
The most complex challenge in automated documentation is separating "signal" from "noise." A raw work log is full of dead ends, syntax errors, and failed attempts. If you feed that directly into a document, you get a confusing guide.
To solve this, Docu-Bot utilizes a Two-Stage AI Pipeline that mimics a human editor's workflow. By switching to the Gemma 3 model, I was able to remove complex failover logic and rely on a single, high-capacity model for the entire pipeline.
The system is designed to capture every type of data an administrator encounters during a deployment.
| Feature | Description & Functionality |
|---|---|
| πΈ Snip & Log |
Function: Captures a region of the screen. AI Action: ALWAYS ON. Every capture is analyzed by Gemma 3 for context, error codes, and UI elements. Use Case: Documenting error messages, weird behavior, or "Before" states. |
| β Eureka Mode |
Function: Captures the "Success" state. AI Action: Tags the entry as SOLUTION VERIFIED. The Writer script prioritizes these tags above all others.Use Case: "I just fixed it. This is the working setting." |
| π Quick Note |
Function: Opens a dark-mode input box to type a manual log entry. Use Case: Adding context that isn't on screen (e.g., "Rebooting server now" or "Switched physical cables"). |
| π Clipboard Capture |
Function: Reads the current system clipboard. Logic: If clipboard contains text, logs it as a reference. Use Case: Quickly logging URLs, IP addresses, or screenshots taken via Windows Native tools. |
Follow these steps to deploy the Docu-Bot environment on a Windows 10/11 workstation.
1. Install the required Python libraries:
pip install requests mss pillow pyautogui google-genai
2. Create a folder named Docu-Bot and place both doc_tool.py and doc_maker.py inside.
To integrate with the Stream Deck, I utilized a "Batch File Wrapper" approach (`launcher.bat`) to execute Python commands cleanly.
| Button Name | Stream Deck "App/File" Path | Function |
|---|---|---|
| πΈ SNIP | .../launcher.bat snip |
Triggers region capture + AI Analysis. |
| β EUREKA | .../launcher.bat eureka |
Triggers solution capture + "Solution Verified" tagging. |
| π NOTE | .../launcher.bat note |
Opens the popup input box for manual text logging. |
| π PROJECT | .../launcher.bat set_project |
Opens the Project Selector to switch logging threads. |
doc_maker.py script is designed to be theme-agnostic. The CSS block inside the script can be easily modified to support any corporate identity, dark mode, or personal branding.
The deployment of the Docu-Bot system has fundamentally shifted the department's workflow. By moving documentation from a "post-project chore" to a "real-time background process," we achieve complete data capture without slowing down technical operations.
The tool adds no delay to the troubleshooting process. A single button press captures the context ("Snip-and-Go"), allowing the engineer to immediately return to solving the problem. The documentation builds itself in the background while the work happens.
Documentation for complex deployments (e.g., Esports Lab) previously took 2-4 hours of post-project writing. With Docu-Bot, the final synthesis takes < 5 minutes, eliminating the administrative backlog entirely.
By capturing "Eureka" moments instantly, the system eliminates "Memory Drift"βthe common error where admins forget specific registry keys or IP addresses by the time they sit down to write the guide.