AgroChat / app.py
mutoni1923's picture
Deploy AgroChat without large model weights
ee140b6
Raw History Blame Contribute Delete
4.77 kB
"""AgroChat: agriculture QA with a fine-tuned sentence embedder.
Pipeline:
1. Embed the user question with the fine-tuned (or baseline) encoder.
2. Compute cosine similarity against every stored question embedding.
3. Return the best answer if similarity >= threshold; otherwise fallback.
"""
import csv
import os
import time
from pathlib import Path
import numpy as np
from flask import Flask, jsonify, render_template, request
from sentence_transformers import SentenceTransformer
from config import FALLBACK, MODEL_NAME, SIMILARITY_THRESHOLD as _DEFAULT_TH
BASE_DIR = Path(__file__).resolve().parent
KNOWLEDGE_FILE = BASE_DIR / "data" / "qa_train.csv" # retrieved answers come from training split
SIMILARITY_THRESHOLD = float(os.getenv("SIMILARITY_THRESHOLD", str(_DEFAULT_TH)))
app = Flask(__name__)
# ------------------------------------------------------------------ knowledge
def load_knowledge(path: Path) -> list[dict]:
if not path.exists():
raise FileNotFoundError(
f"{path} not found. Run `python scripts/prepare_data.py` first."
)
with open(path, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
for r in rows:
if not r.get("question") or not r.get("answer"):
raise ValueError("Every row must have a question and an answer.")
return rows
knowledge = load_knowledge(KNOWLEDGE_FILE)
QUESTIONS = [r["question"] for r in knowledge]
# ------------------------------------------------------------------ model
def _resolve_embedder() -> tuple[SentenceTransformer, str]:
env_path = os.getenv("EMBEDDER_PATH")
if env_path and Path(env_path).exists():
return SentenceTransformer(env_path), f"finetuned:{env_path}"
local = BASE_DIR / "models" / "agri-minilm"
if local.exists():
return SentenceTransformer(str(local)), f"finetuned:{local}"
return SentenceTransformer(MODEL_NAME), f"baseline:{MODEL_NAME}"
model, EMBEDDER_TAG = _resolve_embedder()
question_embeddings = model.encode(
QUESTIONS, normalize_embeddings=True, convert_to_numpy=True
)
# ------------------------------------------------------------------ inference
def retrieve_answer(user_question: str) -> dict:
start = time.perf_counter()
query_embedding = model.encode(
[user_question], normalize_embeddings=True, convert_to_numpy=True
)[0]
scores = question_embeddings @ query_embedding
best_index = int(np.argmax(scores))
best_score = float(scores[best_index])
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
if best_score < SIMILARITY_THRESHOLD:
return {
"answer": FALLBACK,
"topic": None,
"matched_question": None,
"similarity": round(best_score, 4),
"accepted": False,
"response_time_ms": elapsed_ms,
}
row = knowledge[best_index]
return {
"answer": row["answer"],
"topic": row.get("category", "agriculture"),
"matched_question": row["question"],
"similarity": round(best_score, 4),
"accepted": True,
"response_time_ms": elapsed_ms,
}
# ------------------------------------------------------------------ routes
@app.route("/", methods=["GET", "POST"])
def home():
if request.method == "GET":
return render_template("index.html")
question = request.form.get("question", "").strip()
if not question:
return render_template(
"index.html", initial_error="Please enter a question."
), 400
if len(question) > 500:
return render_template(
"index.html",
initial_error="Please keep questions under 500 characters.",
), 400
return render_template(
"index.html",
initial_question=question,
initial_response=retrieve_answer(question),
)
@app.route("/api/health", methods=["GET"])
def health():
return jsonify({
"status": "ok",
"knowledge_base_size": len(knowledge),
"embedder": EMBEDDER_TAG,
"similarity_threshold": SIMILARITY_THRESHOLD,
})
@app.route("/api/chat", methods=["POST"])
def chat():
data = request.get_json(silent=True) or {}
question = data.get("question", "")
if not isinstance(question, str):
return jsonify({"error": "Question must be text."}), 400
question = question.strip()
if not question:
return jsonify({"error": "Please enter a question."}), 400
if len(question) > 500:
return jsonify({
"error": "Please keep questions under 500 characters."
}), 400
return jsonify(retrieve_answer(question))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.getenv("PORT", "7860")))