#!/usr/bin/env python """RAG Q&A with Citations — ask a question, get an answer grounded ONLY in a curated construction & remodeling knowledge base, with inline [n] citations to the exact sources — and an honest "I couldn't find that in the knowledge base" when the answer isn't there. Real RAG: chunk -> embed (fastembed, ONNX/CPU, no torch) -> retrieve (cosine) -> Claude answers from the retrieved sources only, citing each. No hallucinating past the sources. pip install flask requests anthropic fastembed numpy python app.py # http://127.0.0.1:7860 Project #10 of the "30 Projects in 15 Days" challenge — GritAI. """ import os, re, json import numpy as np from flask import Flask, request, jsonify, Response PORT = int(os.environ.get("PORT", "7860")) ANTHROPIC_KEY = (os.environ.get("ANTHROPIC_API_KEY") or "").strip() or None ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5") TOP_K = 4 # ============================ Construction & Remodeling KB ============================ # General, public industry knowledge (no proprietary data). Each entry is a citable source. KB = [ {"t": "Do I need a building permit?", "x": "Most structural, electrical, plumbing, and mechanical work requires a permit from your local " "building department — including additions, removing or moving walls, new circuits, water-heater " "replacement, decks over a certain height, and re-roofing in many areas. Cosmetic work (paint, " "flooring, cabinets, fixtures that reuse existing connections) usually does not. Permits exist to " "ensure work meets code and passes inspection; unpermitted work can cause problems when you sell. " "When unsure, call your local building department and ask — it's free."}, {"t": "How permits and inspections work", "x": "You (or your licensed contractor) submit plans and pay a fee; the department issues the permit; " "work is done in phases with inspections at key points — typically a rough-in inspection (framing, " "electrical, plumbing before walls are closed) and a final inspection when complete. Don't cover " "walls until the rough-in passes. Keep the permit posted on site. Final sign-off closes the permit."}, {"t": "Hiring a contractor: licenses and insurance", "x": "Verify the contractor holds a current state license for the trade and carries liability insurance " "and workers' compensation. Ask for the license number and confirm it with your state licensing " "board. Uninsured workers injured on your property can become your liability. For larger jobs, a " "general contractor coordinates subcontractors and pulls permits."}, {"t": "Getting and comparing bids", "x": "Get at least three written bids for the same defined scope so they're comparable. A good bid itemizes " "labor, materials, permits, timeline, and payment schedule. Be wary of a bid far below the others — it " "often means missing scope or corners cut. Don't automatically pick the lowest; weigh reputation, " "references, and clarity."}, {"t": "Contracts, deposits, and payment schedules", "x": "Always use a written contract listing scope, materials, price, start/finish dates, and a payment " "schedule tied to milestones. A common structure is a modest deposit, progress payments as phases " "complete, and a final payment after the work passes inspection and you're satisfied. Avoid paying " "large sums up front; never pay in full before work is done."}, {"t": "Mechanic's liens and lien waivers", "x": "If a contractor or supplier isn't paid, they can file a mechanic's lien against your property even if " "you paid your general contractor. Protect yourself by requesting lien waivers from subcontractors and " "suppliers as they're paid, confirming the money reached them."}, {"t": "Budgeting and the contingency fund", "x": "Set aside a contingency of 10–20% of the project budget for surprises — older homes especially hide " "problems (outdated wiring, water damage, non-code framing) behind the walls. Renovations frequently " "run over when hidden conditions surface; the contingency keeps a surprise from stalling the job."}, {"t": "Kitchen remodel: scope, cost drivers, and timeline", "x": "Kitchens are among the most expensive rooms to remodel because they combine cabinetry, countertops, " "appliances, plumbing, electrical, and often flooring. Cabinetry and countertops are usually the " "biggest line items. Moving plumbing or gas lines and relocating walls adds significant cost. A typical " "mid-range kitchen remodel runs several weeks to a couple of months depending on scope and lead times " "on custom cabinets."}, {"t": "Bathroom remodel basics", "x": "Bathrooms pack plumbing, waterproofing, tile, ventilation, and electrical into a small space, so labor " "per square foot is high. Proper waterproofing behind tile in showers is critical to prevent hidden " "rot and mold. A quality exhaust fan vented outside (not into the attic) prevents moisture damage. " "Moving the toilet, tub, or shower drain locations raises cost because it means opening the floor."}, {"t": "Flooring options compared", "x": "Common choices: hardwood (durable, refinishable, higher cost, sensitive to moisture); engineered wood " "(more moisture-stable, thinner wear layer); luxury vinyl plank/LVP (waterproof, budget-friendly, great " "for kitchens/baths/basements); tile (very durable and waterproof, harder and colder underfoot); and " "carpet (soft and cheap, wears faster). Match the material to the room's moisture and traffic."}, {"t": "Interior painting: prep and product", "x": "A lasting paint job is 80% prep: clean surfaces, fill and sand holes, caulk gaps, and prime bare or " "patched areas and stains. Use the right sheen — flat/matte hides wall flaws, eggshell/satin is a good " "durable middle for most walls, and semi-gloss resists moisture on trim, kitchens, and baths. One " "gallon covers roughly 350–400 square feet per coat; most jobs need two coats."}, {"t": "Drywall: hanging, taping, and patching", "x": "Standard wall drywall is ½ inch; ceilings often use 5/8 inch to resist sagging. Screw to the studs, " "then tape and apply three coats of joint compound, sanding between coats for a smooth, feathered seam. " "Small holes are patched with mesh or a patch kit and compound; large holes need a backed drywall " "patch. Use moisture-resistant board in wet areas."}, {"t": "Framing and load-bearing walls", "x": "Walls are framed with studs spaced 16 inches on-center (sometimes 24). A load-bearing wall carries " "weight from above (roof, upper floors) down to the foundation; removing or altering one requires a " "properly sized beam and usually an engineer and a permit. Never remove a wall without confirming " "whether it's load-bearing — getting this wrong is dangerous."}, {"t": "Roofing: materials and when to replace", "x": "Asphalt shingles are the most common and last roughly 15–30 years depending on quality; metal roofs " "last longer at a higher cost. Signs you need a new roof include curling or missing shingles, granules " "collecting in gutters, daylight or leaks in the attic, and sagging. Re-roofing usually needs a permit " "and an inspection."}, {"t": "Insulation and energy efficiency (R-value)", "x": "Insulation is rated by R-value — higher means more resistance to heat flow. Attics benefit most from " "added insulation and are usually the cheapest place to improve efficiency. Air-sealing gaps and leaks " "before insulating matters as much as the insulation itself. Common types include fiberglass batts, " "blown-in cellulose, and spray foam (higher cost, also air-seals)."}, {"t": "Electrical basics and when to call a pro", "x": "Simple like-for-like swaps (a switch or outlet) are often DIY, but new circuits, panel work, and " "anything you're unsure about should go to a licensed electrician and be permitted. Kitchens, baths, " "garages, and outdoors require GFCI protection; many circuits now require AFCI breakers. Never work on " "a live circuit — shut off the breaker and verify it's dead."}, {"t": "Plumbing basics and rough-in", "x": "Plumbing is installed in two stages: the rough-in (supply and drain lines inside the walls and floor, " "inspected before closing up) and the finish (fixtures and trim). Proper drain slope (about ¼ inch per " "foot) and venting are required for drains to flow and to keep sewer gas out. Relocating fixtures means " "opening walls or floors, which drives cost."}, {"t": "Decks: footings, materials, and permits", "x": "Decks generally need a permit and rest on footings dug below the local frost line so they don't heave. " "Materials range from pressure-treated lumber (cheapest) to cedar to composite (low-maintenance, higher " "cost). Proper flashing where the deck ledger meets the house prevents rot and is a common failure " "point. Railings and stair dimensions are code-regulated for safety."}, {"t": "Concrete and foundation cracks", "x": "Thin, hairline cracks in concrete are common from normal curing and shrinkage and are usually cosmetic. " "Warning signs that need a professional include cracks wider than about ¼ inch, horizontal cracks in " "foundation walls, cracks that are growing, or doors and windows that suddenly stick — these can signal " "settlement or structural movement."}, {"t": "HVAC sizing and maintenance", "x": "Heating and cooling equipment must be correctly sized for the home — an oversized system short-cycles " "and controls humidity poorly, while undersized struggles to keep up. Sizing is done with a load " "calculation (Manual J), not a rule of thumb. Basic maintenance — changing filters regularly and an " "annual service — extends equipment life and efficiency."}, {"t": "Older homes: lead paint and asbestos", "x": "Homes built before 1978 may contain lead-based paint, and homes from roughly the 1980s and earlier may " "contain asbestos in flooring, popcorn ceilings, insulation, and duct wrap. Disturbing these during a " "remodel releases hazardous dust. Testing before demolition and using certified abatement pros for " "removal is the safe, and often legally required, path."}, {"t": "Project phases and realistic timelines", "x": "A typical renovation flows through phases: design and permitting, demolition, rough-in (framing, " "electrical, plumbing, HVAC) with inspections, insulation and drywall, then finishes (paint, flooring, " "cabinets, trim, fixtures) and a final inspection. Material lead times — especially custom cabinets, " "windows, and specialty tile — often drive the schedule more than the labor does."}, {"t": "Return on investment of common projects", "x": "Not all projects return equal value at resale. Curb-appeal and maintenance items (a new front door, " "garage door, fresh paint, and minor kitchen and bath updates) tend to recoup a larger share of their " "cost than high-end additions. Over-improving beyond the neighborhood's price range rarely pays back " "the full investment."}, ] # ============================ embeddings + retrieval ============================ _EMB = None _MAT = None # normalized KB embedding matrix def emb_model(): global _EMB if _EMB is None: from fastembed import TextEmbedding _EMB = TextEmbedding("BAAI/bge-small-en-v1.5") return _EMB def _norm(m): n = np.linalg.norm(m, axis=-1, keepdims=True) return m / np.clip(n, 1e-8, None) def build_index(): global _MAT texts = [f"{d['t']}. {d['x']}" for d in KB] vecs = np.array(list(emb_model().passage_embed(texts)), dtype=np.float32) _MAT = _norm(vecs) if os.environ.get("SPACE_ID"): # warm the index at startup on the Space try: build_index() print("[startup] KB index built", flush=True) except Exception as _e: print(f"[startup] index build failed (will retry on first ask): {_e}", flush=True) def retrieve(question, k=TOP_K): if _MAT is None: build_index() q = _norm(np.array(list(emb_model().query_embed([question]))[0], dtype=np.float32).reshape(1, -1))[0] sims = _MAT @ q idx = np.argsort(-sims)[:k] return [(int(i), float(sims[i])) for i in idx] TOOL = { "name": "answer", "description": "Answer the question using ONLY the provided sources.", "input_schema": {"type": "object", "properties": { "found": {"type": "boolean", "description": "true only if the sources actually answer the question."}, "answer": {"type": "string", "description": "The answer, grounded in the sources, with inline citations " "like [1] or [2] pointing to the source numbers used. If found is false, briefly say the " "knowledge base doesn't cover it."}, "used": {"type": "array", "items": {"type": "integer"}, "description": "Source numbers actually cited."}}, "required": ["found", "answer", "used"]}, } def answer_question(question): hits = retrieve(question) sources = [{"n": i + 1, "title": KB[idx]["t"], "text": KB[idx]["x"], "score": round(sc, 3)} for i, (idx, sc) in enumerate(hits)] if not ANTHROPIC_KEY: return {"found": False, "answer": "Set ANTHROPIC_API_KEY on the server to generate answers.", "used": [], "sources": sources} block = "\n\n".join(f"[{s['n']}] {s['title']}\n{s['text']}" for s in sources) sysp = ("You answer questions about construction and home remodeling using ONLY the numbered SOURCES " "provided. Ground every claim in the sources and cite them inline as [n]. If the sources do not " "actually contain the answer, set found=false and say the knowledge base doesn't cover it — never " "guess or use outside knowledge. Be concise and practical.") try: import requests resp = requests.post("https://api.anthropic.com/v1/messages", headers={"x-api-key": ANTHROPIC_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json"}, json={"model": ANTHROPIC_MODEL, "max_tokens": 900, "system": sysp, "tools": [TOOL], "tool_choice": {"type": "tool", "name": "answer"}, "messages": [{"role": "user", "content": f"SOURCES:\n{block}\n\nQUESTION: {question}"}]}, timeout=60) resp.raise_for_status() content = resp.json().get("content", []) data = next((b.get("input") for b in content if b.get("type") == "tool_use"), {}) or {} except Exception as e: import sys; print("answer error:", type(e).__name__, file=sys.stderr, flush=True) # no repr — never log secrets return {"found": False, "answer": "Something went wrong — please try again.", "used": [], "sources": sources} used = set(data.get("used", []) or []) shown = [s for s in sources if s["n"] in used] or (sources if data.get("found") else []) return {"found": bool(data.get("found")), "answer": data.get("answer", ""), "used": sorted(used), "sources": shown} app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 256 * 1024 @app.route("/") def home(): return Response(PAGE, mimetype="text/html") @app.route("/api/ask", methods=["POST"]) def api_ask(): q = (request.get_json(force=True).get("question") or "").strip()[:600] if not q: return jsonify(error="Ask a question."), 400 return jsonify(answer_question(q)) PAGE = """
Ask about construction and home remodeling. The answer is grounded only in a curated knowledge base and cites the exact sources — and it says "I don't know" when the answer isn't there. No made-up facts.