admin / Bid-Sentinel
publicBid Scrape and Tracking Application with AI Capability
Bid-Sentinel / bid-sentinel-v2 / make_ai_key_guide.py
7416 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | """Generates docs/AI_API_KEY_SETUP.pdf — how to set the optional AI API key.""" import os from reportlab.lib.colors import HexColor from reportlab.lib.enums import TA_CENTER, TA_LEFT from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import cm from reportlab.lib.utils import ImageReader from reportlab.platypus import ( HRFlowable, Image, ListFlowable, ListItem, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle, ) HERE = os.path.dirname(__file__) LOGO_PATH = os.path.join(HERE, "assets", "bid-sentinel-logo.png") DOCS_DIR = os.path.join(HERE, "docs") os.makedirs(DOCS_DIR, exist_ok=True) BRAND = HexColor("#0b3d91") BRAND_LIGHT = HexColor("#1d5fd6") GREY = HexColor("#475569") CODE_BG = HexColor("#f1f5f9") styles = getSampleStyleSheet() h_title = ParagraphStyle("h_title", parent=styles["Title"], textColor=BRAND, fontSize=20, spaceAfter=2) h_sub = ParagraphStyle("h_sub", parent=styles["Normal"], textColor=GREY, fontSize=11, alignment=TA_CENTER, spaceAfter=14) h1 = ParagraphStyle("h1", parent=styles["Heading1"], textColor=BRAND, fontSize=14, spaceBefore=12, spaceAfter=5) body = ParagraphStyle("body", parent=styles["Normal"], fontSize=10, leading=15, alignment=TA_LEFT, spaceAfter=6) small = ParagraphStyle("small", parent=styles["Normal"], fontSize=8.5, textColor=GREY) code = ParagraphStyle( "code", parent=styles["Code"], fontSize=9, leading=13, backColor=CODE_BG, borderPadding=6, leftIndent=2, spaceBefore=2, spaceAfter=6, ) def steps(items): return ListFlowable( [ListItem(Paragraph(t, body), leftIndent=12) for t in items], bulletType="1", leftIndent=6, ) def info_table(rows, widths): t = Table(rows, colWidths=widths, hAlign="LEFT") t.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), BRAND), ("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#ffffff")), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 9), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [HexColor("#ffffff"), CODE_BG]), ("GRID", (0, 0), (-1, -1), 0.5, HexColor("#cbd5e1")), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5), ("LEFTPADDING", (0, 0), (-1, -1), 7), ])) return t story = [] story.append(Spacer(1, 0.3 * cm)) if os.path.exists(LOGO_PATH): iw, ih = ImageReader(LOGO_PATH).getSize() w = 6 * cm img = Image(LOGO_PATH, width=w, height=w * ih / iw) img.hAlign = "CENTER" story.append(img) story.append(Spacer(1, 0.1 * cm)) story.append(Paragraph("Setting the AI API key", h_title)) story.append(Paragraph("Bid Sentinel — quick setup guide", h_sub)) story.append(HRFlowable(width="100%", thickness=1.5, color=BRAND, spaceAfter=10)) story.append(Paragraph( "The optional <b>AI enhancement</b> uses a low-cost Claude model (Haiku 4.5) " "for richer summaries, a reasoned Fit %, and more accurate certifications and " "technical requirements. The key is <b>not</b> entered in the web app — it is a " "secret, so it lives in an environment variable the backend reads at startup.", body)) story.append(Paragraph("1. Get an API key", h1)) story.append(steps([ "Sign in to the Anthropic Console at https://console.anthropic.com", "Go to <b>API keys</b> → <b>Create key</b>.", "Copy the key — it starts with <font face='Courier'>sk-ant-</font> " "(you won't be able to see it again).", ])) story.append(Paragraph("2. Add the key to the .env file", h1)) story.append(Paragraph( "The <font face='Courier'>.env</font> file sits in the project root, next to " "<font face='Courier'>docker-compose.yml</font>. If you don't have one yet, " "create it from the template:", body)) story.append(Paragraph("cp .env.example .env", code)) story.append(Paragraph( "Open <font face='Courier'>.env</font> and set the key (no quotes, no spaces " "around the = sign):", body)) story.append(Paragraph("ANTHROPIC_API_KEY=sk-ant-your-key-here<br/>AI_MODEL=claude-haiku-4-5", code)) story.append(Paragraph("AI_MODEL is optional — it already defaults to Haiku 4.5.", small)) story.append(Paragraph("3. Restart the containers", h1)) story.append(Paragraph( "Environment variables are only read when a container starts, so recreate them " "(no database wipe is needed — your data is preserved):", body)) story.append(Paragraph("docker compose up -d --build", code)) story.append(Paragraph("4. Confirm the backend can see the key", h1)) story.append(Paragraph("curl http://localhost:8080/api/config/ai", code)) story.append(info_table( [["Response contains", "Meaning"], ['"available": true', "Key detected — you can switch AI on."], ['"available": false', "Key not picked up — see Troubleshooting."]], [6 * cm, 9 * cm])) story.append(Spacer(1, 0.15 * cm)) story.append(Paragraph("You can also check inside the container:", body)) story.append(Paragraph("docker compose exec backend printenv ANTHROPIC_API_KEY", code)) story.append(Paragraph("5. Turn AI on in the app", h1)) story.append(steps([ "Open the dashboard and go to <b>Settings → AI enhancement</b> (admins only).", "The toggle is now enabled — switch it <b>On</b>.", "Click <b>Recompute fit & certs</b> to apply AI to existing opportunities. " "New opportunities are analysed automatically from then on.", ])) story.append(Paragraph("Troubleshooting", h1)) story.append(info_table( [["Symptom", "Fix"], ["Toggle greyed out / available:false", "Ensure .env is in the same folder as docker-compose.yml; the line reads " "exactly ANTHROPIC_API_KEY=sk-ant-... (no quotes/spaces); and you ran " "'docker compose up -d --build' (a plain restart won't reload env)."], ["AI on but a row has no summary", "That row fell back to the deterministic engine (e.g. transient API error). " "Click Recompute to retry."], ["Want to turn AI off", "Toggle it Off in Settings, or remove ANTHROPIC_API_KEY from .env and restart. " "The app then runs fully offline."]], [5 * cm, 10 * cm])) story.append(Paragraph("Notes", h1)) story.append(steps([ "<b>Cost:</b> roughly half a cent per opportunity with Haiku 4.5 (one model " "call each). A full recompute over a few hundred rows is a few dollars at most.", "<b>Security:</b> keep .env out of version control (already in .gitignore). " "Never paste the key into the web UI or commit it.", "<b>Privacy:</b> with AI off, nothing leaves your server. With it on, each " "opportunity's text is sent to the Anthropic API for analysis.", ])) def _footer(canvas, doc): canvas.saveState() canvas.setFont("Helvetica", 8) canvas.setFillColor(GREY) canvas.drawString(2 * cm, 1.1 * cm, "Bid Sentinel — AI API key setup") canvas.drawRightString(A4[0] - 2 * cm, 1.1 * cm, f"Page {doc.page}") canvas.restoreState() doc = SimpleDocTemplate( os.path.join(DOCS_DIR, "AI_API_KEY_SETUP.pdf"), pagesize=A4, leftMargin=2 * cm, rightMargin=2 * cm, topMargin=1.6 * cm, bottomMargin=1.8 * cm, title="Bid Sentinel - AI API key setup", author="Bid Sentinel", ) doc.build(story, onFirstPage=_footer, onLaterPages=_footer) print("docs/AI_API_KEY_SETUP.pdf generated.") |