Spaces:
Sleeping
Sleeping
| import os, io, json, time, pickle, hashlib | |
| from typing import List, Dict, Tuple | |
| import gradio as gr | |
| import numpy as np | |
| # ============ Lightweight local model (CPU) ============ | |
| from transformers import AutoModelForSeq2SeqLM, AutoTokenizer | |
| LLM_ID = os.environ.get("LLM_ID", "google/flan-t5-base") | |
| MAX_NEW_TOKENS = int(os.environ.get("MAX_NEW_TOKENS", "256")) | |
| _tokenizer = AutoTokenizer.from_pretrained(LLM_ID) | |
| _model = AutoModelForSeq2SeqLM.from_pretrained(LLM_ID) | |
| def llm(prompt: str, temperature: float = 0.2): | |
| inputs = _tokenizer(prompt, return_tensors="pt") | |
| outputs = _model.generate( | |
| **inputs, | |
| do_sample=temperature > 0.0, | |
| temperature=temperature, | |
| max_new_tokens=MAX_NEW_TOKENS, | |
| num_beams=1 if temperature > 0 else 4, | |
| ) | |
| return _tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| # ============ Tiny RAG store (local, CPU) ============ | |
| from sentence_transformers import SentenceTransformer | |
| EMBED_ID = os.environ.get("EMBED_ID", "sentence-transformers/all-MiniLM-L6-v2") | |
| _embedder = SentenceTransformer(EMBED_ID) | |
| DB_DIR = os.environ.get("DB_DIR", "./data/agri_space_db") | |
| os.makedirs(DB_DIR, exist_ok=True) | |
| def _db_path(ns: str) -> str: | |
| h = hashlib.sha1(ns.encode()).hexdigest()[:10] | |
| return os.path.join(DB_DIR, f"rag_{h}.pkl") | |
| def _load_db(ns: str): | |
| p = _db_path(ns) | |
| if os.path.exists(p): | |
| with open(p, "rb") as f: | |
| return pickle.load(f) | |
| return {"chunks": [], "embeds": None} | |
| def _save_db(ns: str, db: Dict): | |
| with open(_db_path(ns), "wb") as f: | |
| pickle.dump(db, f) | |
| def chunk_text(text: str, max_tokens:int=220, overlap:int=40) -> List[str]: | |
| words = text.split() | |
| chunks = [] | |
| i = 0 | |
| step = max_tokens - overlap | |
| while i < len(words): | |
| chunk = " ".join(words[i:i+max_tokens]) | |
| chunks.append(chunk) | |
| i += step | |
| return chunks | |
| def add_docs(namespace: str, docs: List[Tuple[str, str]]) -> str: | |
| db = _load_db(namespace) | |
| new_chunks = [] | |
| for name, text in docs: | |
| for ch in chunk_text(text): | |
| new_chunks.append({"name": name, "text": ch}) | |
| if not new_chunks: | |
| return "No text detected." | |
| embeds = _embedder.encode([c["text"] for c in new_chunks], normalize_embeddings=True) | |
| if db["chunks"]: | |
| db["chunks"].extend(new_chunks) | |
| if db["embeds"] is not None: | |
| db["embeds"] = np.vstack([db["embeds"], embeds]) | |
| else: | |
| db["embeds"] = embeds | |
| else: | |
| db["chunks"] = new_chunks | |
| db["embeds"] = embeds | |
| _save_db(namespace, db) | |
| return f"Added {len(new_chunks)} chunks to namespace '{namespace}'." | |
| def retrieve(namespace: str, question: str, top_k:int=5) -> List[Dict]: | |
| db = _load_db(namespace) | |
| if not db["chunks"]: | |
| return [] | |
| qv = _embedder.encode([question], normalize_embeddings=True)[0] | |
| sims = db["embeds"] @ qv | |
| idx = np.argsort(-sims)[:top_k] | |
| results = [] | |
| for i in idx: | |
| ch = db["chunks"][int(i)] | |
| results.append({"name": ch["name"], "text": ch["text"], "score": float(sims[int(i)])}) | |
| return results | |
| def rag_answer(namespace: str, question: str, temperature: float=0.2, top_k:int=5) -> Tuple[str, str]: | |
| ctx = retrieve(namespace, question, top_k=top_k) | |
| if not ctx: | |
| return ("No documents indexed yet. Add farm PDFs/text first.", "") | |
| context_text = "\n\n".join([f"[{i+1}] {c['name']}: {c['text'][:600]}" for i,c in enumerate(ctx)]) | |
| prompt = f"""You are AgriTech Copilot for vertical agriculture, homesteading, off-grid power, regenerative farming, and preparedness. | |
| Use ONLY the context when answering. Be practical and safe. Cite the snippets you used by bracket numbers. | |
| Question: {question} | |
| Context: | |
| {context_text} | |
| Answer with steps, materials, and cautions where relevant. Include citations like [1], [2].""" | |
| ans = llm(prompt, temperature=temperature) | |
| sources = "\n".join([f"[{i+1}] {c['name']} (score {c['score']:.3f})" for i,c in enumerate(ctx)]) | |
| return ans, sources | |
| # ============ Agri/Off-grid calculators ============ | |
| def seed_bed_planner(crop:str, bed_length_ft:float, bed_width_ft:float, in_row_spacing_in:float, row_spacing_in:float): | |
| bed_len_in = bed_length_ft*12.0 | |
| bed_w_in = bed_width_ft*12.0 | |
| rows = max(1, int(bed_w_in // row_spacing_in)) | |
| plants_per_row = max(1, int(bed_len_in // in_row_spacing_in)) | |
| total_plants = rows * plants_per_row | |
| return { | |
| "rows": rows, | |
| "plants_per_row": plants_per_row, | |
| "total_plants": total_plants, | |
| "note": f"Estimate for {crop}. Adjust for cultivar and local climate." | |
| } | |
| def solar_offgrid_sizer(daily_wh:float, sun_hours:float=5.0, system_voltage:float=24.0): | |
| panel_watts = daily_wh / sun_hours * 1.25 | |
| battery_wh = daily_wh * 2.0 | |
| battery_ah = battery_wh / system_voltage | |
| inverter_w = daily_wh / 24 * 3 | |
| return { | |
| "suggested_pv_watts": round(panel_watts, 1), | |
| "battery_wh": round(battery_wh, 1), | |
| "battery_ah_at_voltage": round(battery_ah, 1), | |
| "inverter_watts_peak": round(inverter_w, 1), | |
| "note": "Sizing is conservative. Validate with a local installer. Use fuses, disconnects, and proper wire gauge." | |
| } | |
| def survival_checklist(days:int=7, people:int=2): | |
| water_gal = people * days * 1.0 | |
| calories = people * days * 2000 | |
| items = [ | |
| f"Water: ~{water_gal} gallons", | |
| f"Food: ~{calories} kcal", | |
| "Heat/cook: camp stove + fuel", | |
| "Light: headlamps + batteries", | |
| "Sanitation: bags, bleach, soap", | |
| "Med kit: prescriptions, dressings, tape", | |
| "Tools: multitool, cordage, tarp", | |
| "Comms: power bank, radio", | |
| "Docs: IDs, cash, copies" | |
| ] | |
| return "\n".join(items) | |
| # ============ Gradio UI ============ | |
| with gr.Blocks(title="Vertical AI: Agriculture - Homesteading - Off-Grid - Preparedness") as demo: | |
| gr.Markdown("## Vertical AI Copilot — Agriculture · Homesteading · Off-Grid · Preparedness") | |
| # ---------------- Agri Copilot (Chat) ---------------- | |
| with gr.Tab("Agri Copilot (Chat)"): | |
| chat_in = gr.Textbox(label="Question", lines=3) | |
| temp = gr.Slider(0.0, 1.0, value=0.2, step=0.05, label="Creativity") | |
| out = gr.Textbox(label="Response", lines=12) | |
| ask_btn = gr.Button("Answer") | |
| def answer_plain(q, t): | |
| prompt = "You are a practical agriculture/off-grid assistant.\n\nQ: {}\nA:".format(q) | |
| return llm(prompt, temperature=t) | |
| ask_btn.click(answer_plain, [chat_in, temp], [out]) | |
| # ---------------- Knowledge (RAG) ---------------- | |
| with gr.Tab("Knowledge (RAG)"): | |
| ns2 = gr.Textbox(value="farm-commons", label="Namespace") | |
| up = gr.Files(label="Upload .txt/.md", file_types=["text"]) | |
| add_btn = gr.Button("Add to Knowledge") | |
| add_out = gr.Textbox(label="Indexer Output") | |
| def add_action(namespace, files): | |
| docs = [] | |
| if files: | |
| for f in files: | |
| try: | |
| # Name | |
| name = os.path.basename(getattr(f, "name", "uploaded.txt")) | |
| # Read as text safely | |
| if hasattr(f, "read"): | |
| content = f.read() | |
| if isinstance(content, bytes): | |
| content = content.decode("utf-8", errors="ignore") | |
| else: | |
| # f may be a path-like | |
| with open(f, "r", encoding="utf-8", errors="ignore") as fh: | |
| content = fh.read() | |
| docs.append((name, content)) | |
| except Exception as e: | |
| return f"Error reading {name}: {e}" | |
| if not docs: | |
| return "No files processed." | |
| return add_docs(namespace, docs) | |
| add_btn.click(add_action, [ns2, up], [add_out]) | |
| q2 = gr.Textbox(label="Question", lines=3) | |
| k = gr.Slider(1, 10, value=5, step=1, label="Top-K") | |
| t2 = gr.Slider(0.0, 1.0, value=0.2, step=0.05, label="Creativity") | |
| ask2 = gr.Button("Answer with RAG") | |
| ans2 = gr.Textbox(label="Answer", lines=12) | |
| src2 = gr.Textbox(label="Sources", lines=8) | |
| ask2.click( | |
| lambda ns, q, t, topk: rag_answer(ns, q, t, int(topk)), | |
| [ns2, q2, t2, k], | |
| [ans2, src2] | |
| ) | |
| # ---------------- Tools ---------------- | |
| with gr.Tab("Tools"): | |
| # Seed/Bed Planner | |
| crop = gr.Textbox(label="Crop", value="Carrot") | |
| bl = gr.Number(label="Bed length (ft)", value=50) | |
| bw = gr.Number(label="Bed width (ft)", value=2.5) | |
| ir = gr.Number(label="In-row spacing (in)", value=2.0) | |
| rs = gr.Number(label="Row spacing (in)", value=6.0) | |
| calc1 = gr.Button("Calculate") | |
| out1 = gr.JSON(label="Plan") | |
| calc1.click(seed_bed_planner, [crop, bl, bw, ir, rs], [out1]) | |
| # Off-Grid Solar Sizer | |
| dwh = gr.Number(label="Daily energy use (Wh/day)", value=3000) | |
| sun = gr.Number(label="Sun hours/day", value=5.0) | |
| volt = gr.Number(label="System voltage (V)", value=24.0) | |
| calc2 = gr.Button("Size System") | |
| out2 = gr.JSON(label="Sizing") | |
| calc2.click(solar_offgrid_sizer, [dwh, sun, volt], [out2]) | |
| # Survival Checklist | |
| days = gr.Number(label="Days", value=7) | |
| ppl = gr.Number(label="People", value=2) | |
| calc3 = gr.Button("Build Checklist") | |
| out3 = gr.Textbox(label="Checklist", lines=10) | |
| calc3.click(survival_checklist, [days, ppl], [out3]) | |
| # ---------------- About ---------------- | |
| with gr.Tab("About & Safety"): | |
| gr.Markdown( | |
| "**Safety:** General guidance only. Follow local laws, building/electrical codes, and best practices." | |
| ) | |
| demo.launch() | |