kisan-sathi / app.py
sxandie's picture
regex adjustments
0cdf331
Raw
History Blame Contribute Delete
26 kB
import os
import asyncio
import pandas as pd
import json
import datetime
import gradio as gr
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from fastapi import Form, UploadFile, File, FastAPI
from pydantic import BaseModel
# Set backend before imports to trigger appropriate load
try:
import llama_cpp
default_backend = "llama_cpp"
except ImportError:
default_backend = "mock"
os.environ["BACKEND"] = os.environ.get("BACKEND", default_backend)
import src.llm as llm
import src.rag as rag
import src.contacts as contacts
import src.ledger as ledger
import src.db as db
# Initialize SQLite Database on startup
db.init_db()
# Preloaded data files
PROFILE_PATH = os.path.dirname(os.path.abspath(__file__)) + "/src/data/user_profile.json"
# Translatable strings database
STRINGS = {
"en": {
"title": "🚜 Kisan-Sathi (Farmer Friend)",
"subtitle": "Your personal offline agricultural assistant",
"save_success": "Entry saved successfully!",
"save_fail": "Error saving entry."
},
"hi": {
"title": "🚜 किसान-साथी (Kisan-Sathi)",
"subtitle": "आपका अपना ऑफलाइन कृषि मित्र और बहीखाता सहायक",
"save_success": "लेनदेन सफलतापूर्वक सहेजा गया!",
"save_fail": "बचत करने में त्रुटि।"
}
}
# User Profile Management
def load_user_profile():
return db.get_profile()
def save_user_profile(name, state, district):
return db.save_profile(name, state, district)
# Helper function to compute nearest pending task from all DB calendars for a crop
def get_nearest_pending_task(crop_name):
conn = db.get_db()
cursor = conn.cursor()
cursor.execute(
"SELECT id, sow_date, crop, variety FROM calendars WHERE lower(crop) = ? ORDER BY id DESC",
(crop_name.lower(),)
)
cals = cursor.fetchall()
if not cals:
conn.close()
return None
nearest_task = None
for cal in cals:
cursor.execute(
"""
SELECT id, stage, action, intervention, target_date, status
FROM tasks WHERE calendar_id = ? AND status = 'pending' ORDER BY target_date ASC
""",
(cal["id"],)
)
tasks = cursor.fetchall()
for t in tasks:
t_date = datetime.datetime.strptime(t["target_date"], "%Y-%m-%d").date()
today = datetime.date.today()
diff_days = (t_date - today).days
if nearest_task is None or t_date < datetime.datetime.strptime(nearest_task["task"]["target_date"], "%Y-%m-%d").date():
nearest_task = {
"task": dict(t),
"calendar": dict(cal),
"diff_days": diff_days
}
conn.close()
return nearest_task
# Fallback general month-based advisory
def get_fallback_nudge(crop, lang):
today = datetime.date.today()
month = today.month
day = today.day
if lang == "hi":
crop_display = "गेहूं" if crop.lower() == "wheat" else "आलू"
elif lang == "hinglish":
crop_display = "Wheat" if crop.lower() == "wheat" else "Aloo"
else:
crop_display = "Wheat" if crop.lower() == "wheat" else "Potato"
if crop.lower() == "wheat":
if month == 11:
stage_en = "Sowing (बुवाई) - Nov 15 to Dec 15"
stage_hi = "बुवाई (Sowing) - १५ नवंबर से १५ दिसंबर"
advice_en = "Sow at 4-5 cm depth. Seed rate: 40-50 kg/acre. Apply DAP."
advice_hi = "४-५ सेमी गहराई पर बोएं। यूरिया और डीएपी का प्रयोग करें।"
elif month == 12:
stage_en = "CRI Irrigation (सिंचाई - CRI) - Dec 05 to Dec 15"
stage_hi = "ताज जड़ विकास सिंचाई (CRI Stage) - ०५ दिसंबर से १५ दिसंबर"
advice_en = "21-25 days after sowing. Critical root development stage."
advice_hi = "बुवाई के २१-२५ दिन बाद। जड़ विकास के लिए सबसे महत्वपूर्ण सिंचाई।"
elif month == 1:
stage_en = "Tillering (कल्ले फूटना) - Dec 25 to Jan 10"
stage_hi = "कल्ले फूटना (Tillering Stage) - २५ दिसंबर से १० जनवरी"
advice_en = "Apply first top dressing of Urea (40 kg/acre) and perform weeding."
advice_hi = "यूरिया की पहली टॉप ड्रेसिंग (४० किग्रा/एकड़) करें और निराई करें।"
elif month == 2:
stage_en = "Flowering (फूल आना) - Jan 25 to Feb 15"
stage_hi = "फूल आना (Flowering Stage) - २५ जनवरी से १५ फरवरी"
advice_en = "Maintain light moisture. Crucial for grain yield."
advice_hi = "हल्की नमी बनाए रखें। दाना बनने की प्रक्रिया के लिए महत्वपूर्ण।"
elif month in [4, 5]:
stage_en = "Harvesting (कटाई) - Apr 01 to Apr 30"
stage_hi = "कटाई (Harvesting) - ०१ अप्रैल से ३० अप्रैल"
advice_en = "Harvest when grains are dry and golden (moisture < 14%)."
advice_hi = "फसल सुनहरी होने और दाने में १४% से कम नमी होने पर कटाई करें।"
else:
stage_en = "Upcoming: Sowing (बुवाई) starts Nov 15"
stage_hi = "आगामी चरण: बुवाई (Sowing) १५ नवंबर से शुरू"
advice_en = "Prepare field by deep ploughing and testing soil during dry months."
advice_hi = "गर्मियों में गहरी जुताई करें और मिट्टी का परीक्षण करवाएं।"
else: # Potato
if month in [10, 11] and day <= 10:
stage_en = "Sowing (बुवाई) - Oct 15 to Nov 10"
stage_hi = "बुवाई (Sowing) - १५ अक्टूबर से १० नवंबर"
advice_en = "Use certified seed tubers. Spacing 60x20 cm. Apply NPK."
advice_hi = "प्रमाणित बीज कंदों का प्रयोग करें। ६०x२० सेमी दूरी रखें।"
elif month == 11:
stage_en = "Earthing Up (मिट्टी चढ़ाना) - Nov 15 to Nov 30"
stage_hi = "मिट्टी चढ़ाना (Earthing Up) - १५ नवंबर से ३० नवंबर"
advice_en = "Apply Urea and mound soil around stems 25 days post-sowing."
advice_hi = "बुवाई के २५ दिन बाद यूरिया डालें और पौधों के तने के चारों ओर मिट्टी चढ़ाएं।"
elif month in [12, 1] and (month == 12 or day <= 15):
stage_en = "Blight Monitoring (झुलसा निगरानी) - Dec 01 to Jan 15"
stage_hi = "झुलसा रोग निगरानी (Blight Monitoring) - ०१ दिसंबर से १५ जनवरी"
advice_en = "Watch for dark spots. Spray Mancozeb (2g/L) on cloudy/foggy days."
advice_hi = "पत्तियों पर काले धब्बों की निगरानी करें। फफूंदनाशक का छिड़काव करें।"
elif month in [2, 3]:
stage_en = "Harvesting (कटाई) - Feb 15 to Mar 15"
stage_hi = "कटाई (Harvesting) - १५ फरवरी से १५ मार्च"
advice_en = "Cut foliage 10 days before harvest to thicken potato skin."
advice_hi = "खुदाई से १० दिन पहले सिंचाई रोकें और डंठल काट लें ताकि छिलका सख्त हो।"
else:
stage_en = "Upcoming: Sowing (बुवाई) starts Oct 15"
stage_hi = "आगामी चरण: बुवाई (Sowing) १५ अक्टूबर से शुरू"
advice_en = "Procure certified seeds from cold storage and keep them in shade."
advice_hi = "कोल्ड स्टोरेज से प्रमाणित बीज खरीदें और उन्हें छाया में रखें।"
if lang == "hi":
return f"💡 **इस सप्ताह आपके खेत पर ({crop_display}):** {stage_hi}\n📌 **सलाह:** {advice_hi}"
elif lang == "hinglish":
return f"💡 **Is week aapke khet par ({crop_display}):** {stage_hi}\n📌 **Salah:** {advice_hi}"
else:
return f"💡 **This week on your farm ({crop_display}):** {stage_en}\n📌 **Advice:** {advice_en}"
# Proactive Nudge logic based on calendar and season
def get_proactive_nudge(crop, lang):
calendars = db.get_calendars()
if not calendars:
return get_fallback_nudge(crop, lang)
nudges = []
for cal in calendars:
# Find nearest pending task for this calendar
pending_tasks = [t for t in cal["tasks"] if t["status"] == "pending"]
if not pending_tasks:
continue
# Sort pending tasks by target_date
pending_tasks.sort(key=lambda t: t["target_date"])
t = pending_tasks[0]
t_date = datetime.datetime.strptime(t["target_date"], "%Y-%m-%d").date()
today = datetime.date.today()
diff_days = (t_date - today).days
# Decode action
action_dict = t["action"]
if isinstance(action_dict, str):
try:
action_dict = json.loads(action_dict)
except:
action_dict = {"en": t["action"], "hi": t["action"], "hinglish": t["action"]}
action_text = action_dict.get(lang, action_dict.get("en", ""))
sow_dt = datetime.datetime.strptime(cal["sow_date"], "%Y-%m-%d")
sow_display = sow_dt.strftime("%d %b %Y")
# Get localized crop name
crop_display = cal["crop"]
if cal["crop"].lower() in db.CROP_TEMPLATES:
crop_display = db.CROP_TEMPLATES[cal["crop"].lower()]["display"].get(lang, cal["crop"])
variety_suffix = f" ({cal['variety']})" if cal["variety"] else ""
if diff_days == 0:
if lang == "hi":
nudge = f"💡 **आज का काम ({crop_display}{variety_suffix}):** {action_text} (बुवाई: {sow_display})"
elif lang == "hinglish":
nudge = f"💡 **Aaj ka kaam ({crop_display}{variety_suffix}):** {action_text} (Sowing: {sow_display})"
else:
nudge = f"💡 **Today's Action ({crop_display}{variety_suffix}):** {action_text} (Sown: {sow_display})"
elif diff_days > 0:
if lang == "hi":
nudge = f"💡 **अगला काम ({crop_display}{variety_suffix}):** {diff_days} दिन में {action_text} ({sow_display} को बोया)"
elif lang == "hinglish":
nudge = f"💡 **Agla kaam ({crop_display}{variety_suffix}):** {diff_days} din me {action_text} ({sow_display} ko boya)"
else:
nudge = f"💡 **Next Action ({crop_display}{variety_suffix}):** {action_text} in {diff_days} days (Sown on {sow_display})"
else: # overdue
overdue_days = abs(diff_days)
if lang == "hi":
nudge = f"⚠️ **लंबित काम ({crop_display}{variety_suffix}):** {overdue_days} दिन पहले होना था - {action_text} ({sow_display} को बोया)"
elif lang == "hinglish":
nudge = f"⚠️ **Overdue kaam ({crop_display}{variety_suffix}):** {overdue_days} din pehle hona tha - {action_text} ({sow_display} ko boya)"
else:
nudge = f"⚠️ **Overdue Action ({crop_display}{variety_suffix}):** {action_text} was due {overdue_days} days ago (Sown on {sow_display})"
nudges.append(nudge)
if nudges:
return "\n\n".join(nudges)
return get_fallback_nudge(crop, lang)
# Load crop calendar data helper
def load_calendar(crop):
crop_key = crop.lower()
if crop_key in db.CROP_TEMPLATES:
t = db.CROP_TEMPLATES[crop_key]
records = []
for stage in t["stages"]:
records.append({
"Crop": crop,
"Stage (चरण)": stage["action"]["hi"],
"Timing (समय सीमा)": f"Day {stage['day_offset']}",
"Action & Advice (कार्य और सलाह)": stage["intervention"]["hi"]
})
return pd.DataFrame(records)
return pd.DataFrame()
def parse_ledger_text(text, lang):
"""Parses natural language transaction using LLM helper."""
if not text.strip():
return "today", "", "", 0, "sale"
extracted = ledger.parse_transaction(text, lambda p, system, stream: llm.generate(p, system, stream=stream))
return (
extracted.get("date", "today"),
extracted.get("item", ""),
extracted.get("qty", ""),
extracted.get("price", 0),
extracted.get("type", "sale")
)
# Pydantic Schemas for JSON endpoints
class ProfileSaveRequest(BaseModel):
name: str
state: str
district: str
class ParseLedgerRequest(BaseModel):
text: str
lang: str
class SaveLedgerRequest(BaseModel):
date: str
item: str
qty: str
price: float
type: str
lang: str
class CalendarAddRequest(BaseModel):
crop: str
variety: str = ""
sow_date: str
location: str = ""
class CalendarDeleteRequest(BaseModel):
calendar_id: int
class CalendarUpdateRequest(BaseModel):
calendar_id: int
sow_date: str
class TaskToggleRequest(BaseModel):
task_id: int
status: str
class LedgerDeleteRequest(BaseModel):
entry_id: int
# Instantiate FastAPI App
app = FastAPI()
# Mount static files folder
app.mount("/assets", StaticFiles(directory="assets"), name="assets")
# Root path handler to serve the custom frontend Single Page App
@app.get("/", response_class=HTMLResponse)
def get_homepage():
index_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "index.html")
with open(index_path, "r", encoding="utf-8") as f:
return f.read()
# Profile Endpoints
@app.get("/api/profile")
def get_profile():
profile = load_user_profile()
return profile if profile else {}
@app.post("/api/profile")
def post_profile(req: ProfileSaveRequest):
success = save_user_profile(req.name, req.state, req.district)
return {"success": success}
# Proactive Nudge Endpoint
@app.get("/api/nudge/{crop}/{lang}")
def get_nudge(crop: str, lang: str):
nudge = get_proactive_nudge(crop, lang)
return {"nudge": nudge}
# Calendar Endpoint
@app.get("/api/calendar/{crop}")
def get_calendar(crop: str):
calendar_df = load_calendar(crop)
if calendar_df.empty:
return []
return calendar_df.to_dict(orient="records")
# Contacts Endpoint
@app.get("/api/contacts")
def get_contacts():
return contacts.load_contacts()
# Personalized Calendar Endpoints
@app.get("/api/calendars")
def get_calendars():
return db.get_calendars()
@app.post("/api/calendar/add")
def post_calendar_add(req: CalendarAddRequest):
try:
cal_id = db.add_calendar(req.crop, req.variety, req.sow_date, req.location)
return {"success": True, "calendar_id": cal_id}
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/calendar/delete")
def post_calendar_delete(req: CalendarDeleteRequest):
success = db.delete_calendar(req.calendar_id)
return {"success": success}
@app.post("/api/calendar/update")
def post_calendar_update(req: CalendarUpdateRequest):
try:
success = db.update_calendar_sow_date(req.calendar_id, req.sow_date)
return {"success": success}
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/task/toggle")
def post_task_toggle(req: TaskToggleRequest):
success = db.update_task_status(req.task_id, req.status)
return {"success": success}
# Ledger Endpoints
@app.post("/api/ledger/parse")
def post_ledger_parse(req: ParseLedgerRequest):
parsed = parse_ledger_text(req.text, req.lang)
return {
"date": parsed[0],
"item": parsed[1],
"qty": parsed[2],
"price": parsed[3],
"type": parsed[4]
}
@app.post("/api/ledger/save")
def post_ledger_save(req: SaveLedgerRequest):
success = ledger.add_entry(req.date, req.item, req.qty, req.price, req.type)
msg = STRINGS[req.lang]["save_success"] if success else STRINGS[req.lang]["save_fail"]
return {"success": success, "message": msg}
@app.post("/api/ledger/delete")
def post_ledger_delete(req: LedgerDeleteRequest):
success = db.delete_ledger_entry(req.entry_id)
return {"success": success}
@app.post("/api/ledger/clear")
def post_ledger_clear():
success = db.clear_ledger()
return {"success": success}
@app.post("/api/reset")
def post_reset():
success = db.clear_all_data()
return {"success": success}
@app.get("/api/ledger/export")
def get_ledger_export():
import io
import csv
entries, _ = db.get_ledger_entries()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["Date", "Type", "Item", "Quantity", "Price", "Timestamp"])
for r in entries:
writer.writerow([r["date"], r["type"], r["item"], r["qty"], r["price"], r["created_at"]])
output.seek(0)
return StreamingResponse(
io.BytesIO(output.getvalue().encode("utf-8")),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=ledger_export.csv"}
)
@app.get("/api/ledger/stats")
def get_ledger_stats():
df, summary = ledger.get_ledger_data()
transactions = []
if not df.empty:
for r in df.to_dict(orient="records"):
# Map localized or standard keys
date_val = r.get("Date", "")
item_val = r.get("Item", "")
qty_val = r.get("Quantity", "")
price_val = r.get("Price", 0)
raw_type = str(r.get("Type", "")).lower()
tx_id = r.get("Id")
type_val = "Sale" if "sale" in raw_type or "बिक्री" in raw_type else "Purchase"
transactions.append({
"Id": tx_id,
"Date": date_val,
"Item": item_val,
"Quantity": qty_val,
"Price": price_val,
"Type": type_val
})
return {
"summary": summary,
"transactions": transactions
}
# Chatbot Multimodal Streaming API
@app.post("/api/ask")
async def post_ask(
message: str = Form(""),
lang: str = Form("hi"),
crop: str = Form("Wheat"),
history: str = Form("[]"),
image: UploadFile = File(None),
audio: UploadFile = File(None)
):
# Save uploaded media files locally in a temp folder
image_path = None
audio_path = None
# Load profile details dynamically to customize prompt metadata
profile = load_user_profile() or {}
user_name = profile.get("name", "Ramesh Kumar")
user_state = profile.get("state", "Uttar Pradesh")
user_district = profile.get("district", "Kanpur Dehat")
history_list = []
if history:
try:
history_list = json.loads(history)
except Exception as e:
print(f"[app.py] Error parsing history: {e}")
temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp")
os.makedirs(temp_dir, exist_ok=True)
if image and image.filename:
image_path = os.path.join(temp_dir, image.filename)
with open(image_path, "wb") as f:
f.write(await image.read())
if audio and audio.filename:
audio_path = os.path.join(temp_dir, audio.filename)
with open(audio_path, "wb") as f:
f.write(await audio.read())
async def event_generator():
# Resolve user message query
check_text = message
if audio_path:
check_text = llm.transcribe_audio(audio_path, message)
# Emergency safety bypass router
is_emergency, warning_msg = contacts.check_emergency_query(check_text)
if is_emergency:
yield warning_msg
# Cleanup temp files
if image_path and os.path.exists(image_path):
try: os.remove(image_path)
except: pass
if audio_path and os.path.exists(audio_path):
try: os.remove(audio_path)
except: pass
return
# Construct language instruction
if lang == "hi":
lang_instruction = "You MUST write your entire response in Hindi language using Devanagari script (हिन्दी)."
elif lang == "hinglish":
lang_instruction = "You MUST write your entire response in Hinglish (Hindi language written in Roman/Latin script using English alphabetic characters, e.g., 'Aloo ki kheti ke liye dhyan dein...')."
else:
lang_instruction = "You MUST write your entire response in English."
# Local agricultural RAG context retrieval
grounding_text, sources = rag.retrieve_guides(check_text, crop, lang)
if grounding_text:
source_cite = "Sources used: " + ", ".join(sources)
system_prompt = (
f"You are Kisan-Sathi, a friendly agricultural expert helping {user_name} in {user_district}, {user_state}.\n"
f"{lang_instruction}\n"
f"Here is verified agricultural guide context regarding the question:\n{grounding_text}\n"
f"Provide a short, direct answer in a friendly, conversational tone. Ground your answer in this context.\n"
f"CRITICAL: Keep your response extremely short, crisp, and precise (maximum 120-150 words, or 3-4 bullet points max). Do not write a long essay or generic introduction. Focus only on direct, actionable advice.\n"
f"Cite the source at the very end as: '{source_cite}'."
)
else:
system_prompt = (
f"You are Kisan-Sathi, a friendly agricultural expert helping {user_name} in {user_district}, {user_state}.\n"
f"{lang_instruction}\n"
f"Advise them on {crop} crop management using best practices for {user_district}, {user_state}.\n"
f"CRITICAL: Keep your response extremely short, crisp, and precise (maximum 120-150 words, or 3-4 bullet points max). Do not write a long essay or generic introduction. Focus only on direct, actionable advice.\n"
f"Suggest contacting a local agricultural officer for specific localized chemical treatments if unsure."
)
# Run backend LLM generation
response_stream = llm.generate(
check_text,
system=system_prompt,
image_path=image_path,
audio_path=audio_path,
history=history_list,
stream=True
)
import re
for chunk in response_stream:
if chunk.startswith("JSON_OUTPUT:"):
continue
cleaned_chunk = re.sub(r'\|?<\|im_(?:end|start)\|>', '', chunk)
yield cleaned_chunk
await asyncio.sleep(0.01)
# Clean up temp files
if image_path and os.path.exists(image_path):
try: os.remove(image_path)
except: pass
if audio_path and os.path.exists(audio_path):
try: os.remove(audio_path)
except: pass
return StreamingResponse(event_generator(), media_type="text/plain")
# Fallback Gradio interface
def gradio_chat_respond(message, history):
formatted_history = []
if history:
for turn in history:
if isinstance(turn, dict):
formatted_history.append(turn)
elif isinstance(turn, (list, tuple)) and len(turn) == 2:
formatted_history.append({"role": "user", "content": turn[0]})
formatted_history.append({"role": "assistant", "content": turn[1]})
response_stream = llm.generate(
message,
system="You are Kisan-Sathi, a friendly agricultural expert.",
history=formatted_history,
stream=True
)
response_accumulator = ""
for chunk in response_stream:
if chunk.startswith("JSON_OUTPUT:"):
continue
response_accumulator += chunk
yield response_accumulator
demo = gr.ChatInterface(
gradio_chat_respond,
title="किसान-साथी (Kisan-Sathi) Fallback",
description="Ask Sathi anything about farming, or use the custom home page at http://localhost:7860/"
)
app = gr.mount_gradio_app(app, demo, path="/gradio")
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=7860)