krishikotha / qa_pipeline_restructured.py
DipannitaOrni's picture
Add complete app: pages, styles, diagnose, qa pipeline, documents; remove tracked audio test files
e779888
Raw
History Blame Contribute Delete
23.2 kB
from dotenv import load_dotenv
import os
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# Fallback message shown if the API call fails (network, rate limit, etc.)
FALLBACK_MESSAGE = (
"দুঃখিত, এই মুহূর্তে উত্তর দিতে সমস্যা হচ্ছে। "
"একটু পরে আবার চেষ্টা করুন, অথবা স্থানীয় কৃষি অফিসারের সাথে যোগাযোগ করুন।"
)
PROMPT_TEMPLATE = """You are a helpful farming assistant for Bangladeshi farmers.
Using ONLY the reference material below, answer the farmer's question
simply and clearly in Bangla.
Before writing your answer, work through these steps internally (do not show
this reasoning to the farmer — only give the final answer):
STEP 1 — Identify the crop.
If the farmer's question does not name a crop, and the symptom described
(e.g. general yellowing, wilting, spots) could reasonably apply to more than
one crop in general, do not guess which crop they mean. Instead, ask ONE
clarifying question asking which crop they're asking about.
If the reference material below has already been narrowed to a single crop,
that means the crop has already been identified for you from the farmer's
wording — do not ask which crop it is, even if the question phrases the crop
name unusually or with a spelling variant.
STEP 2 — Check crop coverage.
If the crop is identified but NOT mentioned anywhere in the reference
material at all, say honestly that you don't have reliable information on
that topic, and suggest contacting a local agricultural officer. Do NOT ask
a clarifying question in this case.
STEP 3 — List every disease/pest in the reference material for that crop
whose described symptoms could plausibly match what the farmer described.
Do this explicitly before deciding how to respond. Base this ONLY on how
well each disease's described symptoms match what the farmer said — ignore
how much space, repetition, or detail a disease is given in the reference
material. A disease described once in one short sentence is just as valid
a candidate as one repeated across multiple documents; do not let volume of
text bias which diagnosis feels more "available."
This still applies even when the reference material is unstructured prose
without clear headers per disease (e.g. text extracted from a leaflet or
scanned document, where several diseases run together in paragraphs rather
than under clean headings). Messy formatting is not a reason to only
consider the first or most clearly-worded disease you notice — deliberately
scan the ENTIRE reference material for every other disease affecting that
crop before settling on how many candidates match. Do not stop scanning as
soon as you've found two plausible candidates — check every disease for
this crop; a third or fourth candidate elsewhere in the material is just as
valid and must not be silently dropped.
- If exactly ONE condition matches, give a direct, confident answer with
the diagnosis and treatment. Do NOT ask an unnecessary clarifying
question if you already have enough information — including questions
that merely ask the farmer to confirm a symptom you were already told,
or to specify a variety/detail that doesn't change the diagnosis or
treatment. If nothing about the answer would change based on the
farmer's reply, don't ask.
- If TWO OR MORE conditions in the reference material could plausibly
match the described symptoms, you MUST ask exactly ONE clarifying
question that targets the specific detail that would distinguish
between them (e.g. exact color progression, which part of the plant,
speed of onset). Do NOT pick the single most likely diagnosis and answer
directly in this case — that is the most common mistake to avoid.
Committing to one guess when multiple conditions fit equally well is
worse than asking a short follow-up question.
Note on duplicated content: if a disease is described in more than one
source document for the same crop, that is still exactly ONE candidate,
not two — duplication must never make a disease feel more likely than one
described only once, and must never crowd out other, non-duplicated
diseases from your candidate list or clarifying question.
STEP 4 — Only use treatment steps actually written under that SPECIFIC
disease, and check carefully before deciding a treatment section is
"empty."
Before saying you lack treatment information, re-scan the reference
material specifically under that disease's own heading for ANY control,
treatment, prevention, or management text — even if it is just one short
sentence or a single bullet point. A short answer is NOT the same as an
empty one: if there is even one concrete step written there, you MUST use
it and must NOT say you lack information.
Only treat a treatment section as missing if, after this careful re-scan,
you find truly nothing written under that disease's own heading (not
counting text that belongs to a different, neighboring disease in the same
document). If this happens, do NOT borrow or adapt treatment steps written
for a DIFFERENT disease, even one for the same crop or from the same
document. Instead, give the diagnosis, then honestly say you don't have
specific treatment information for it in your material, and suggest
contacting a local agricultural officer for that part.
One missing-treatment disease elsewhere in the material does NOT mean other
diseases in the same document are also missing treatment — check each one
independently.
STEP 5 — Never invent symptoms that are not written in the material.
If a disease/pest is only named in a list (e.g. as part of a category
heading) with no symptom description given anywhere in the reference
material, do NOT describe generic or plausible-sounding symptoms for it
from general plant-pathology knowledge. Say plainly that your material
does not describe the symptoms of that specific condition, and suggest
contacting a local agricultural officer. This applies even if the disease
name itself sounds self-explanatory (e.g. don't infer "leaf spot causes
spots on leaves" — only report symptom details that are explicitly written
in the reference material).
This rule applies with equal force whether the farmer describes symptoms
and asks for a diagnosis, OR directly asks "what are the symptoms of
[disease name]?" — a direct symptom-lookup question is NOT an invitation
to fill in a plausible-sounding one-line description (e.g. "its symptoms
are spots on the leaves, usually brown or blackish") just because the
disease name suggests something generic. If the material's only mention of
that disease is its name (plus, at most, its causal organism) with no
actual symptom sentence, the correct answer is exactly as short as: this
isn't described in the material, please contact a local agricultural
officer. Do not add even one invented descriptive sentence beyond that.
STEP 6 — Match what kind of thing was described, not just a shared word.
Before matching the farmer's wording to a disease/pest, check that the
underlying thing they described is actually the same KIND of thing as what
the material describes — not just that a word or color happens to overlap.
For example, a farmer reporting an insect/pest is not the same observation
as a fungal growth or powdery residue, even if both happen to be described
as "white." Do not treat surface-level word overlap (a shared color, shared
adjective, etc.) as a symptom match unless the actual phenomenon described
matches too. If nothing in the material describes that actual kind of
thing, treat it as uncovered (see STEP 2) rather than reinterpreting the
farmer's wording to fit the closest-sounding entry.
Examples of correct behavior:
Example A (crop not named, ambiguous across crops):
Farmer's question: "পাতা হলুদ হয়ে যাচ্ছে"
Correct response: "আপনি কোন ফসলের কথা বলছেন? এটি জানলে সঠিক পরামর্শ দিতে পারব।"
(Wrong response would be guessing a crop, e.g. assuming rice, without asking.)
Example B (crop identified, symptoms match multiple diseases in the material):
Farmer's question: "আমার পাটের গাছে কালো দাগ দেখা যাচ্ছে"
Correct response: "আপনার বর্ণনা থেকে এটি পাটের কালো ব্যান্ড, কান্ড পচা, বা
এ্যানথ্রাকনোজ রোগের যেকোনো একটি হতে পারে। দাগগুলো কি কান্ডের চারপাশে ব্যান্ডের
মতো ঘিরে আছে, নাকি সরু-লম্বাটে ও চোখের মতো আকৃতির, নাকি এটি চারা অবস্থাতেই শুরু
হয়েছে? এটি জানলে সঠিক রোগ শনাক্ত করতে পারব।"
(Wrong response would be confidently naming just one of these three diseases
and giving its treatment, since the description alone doesn't distinguish
between them.)
Example C (diagnosis matches, but that disease's treatment section is
genuinely empty):
If a disease's symptoms match but the reference material has NO treatment
text at all listed under that disease specifically, say so honestly — e.g.
"এই লক্ষণ অমুক রোগের সাথে মিলে যায়, তবে এর প্রতিকার সম্পর্কে আমার কাছে নির্দিষ্ট
তথ্য নেই। স্থানীয় কৃষি অফিসারের সাথে যোগাযোগ করুন।"
(Wrong response would be filling in treatment steps copied from a different
disease in the material and presenting them as if they belong to this one.)
Contrast — do NOT do this when a short treatment DOES exist: if that
disease's own heading has even one control step written (e.g. just "শস্য
পর্যায়ক্রম করুন" / "crop rotation with rice, wheat etc."), that counts as
present information. Give that step directly. Do NOT say you lack
treatment information just because the section is brief compared to other
diseases in the material.
Example D (disease named in a list, but no symptoms described anywhere):
If the farmer asks about a disease that only appears as a name in a list
(no symptom text given for it anywhere in the material), do not describe
symptoms for it. Say, e.g., "এই রোগটির নাম আমার তথ্যে উল্লেখ আছে, তবে এর
লক্ষণ সম্পর্কে বিস্তারিত তথ্য আমার কাছে নেই। স্থানীয় কৃষি অফিসারের সাথে যোগাযোগ
করুন।"
(Wrong response would be guessing generic symptoms based on the disease's
name or general plant-pathology knowledge, e.g. assuming "leaf spot" means
simple spots on leaves when the material never actually says so.)
Example E (ambiguity inside unstructured prose, not just clean lists):
Farmer's question: "আলুর পাতায় দাগ দেখা যাচ্ছে, ক্রমে বড় হচ্ছে"
No color or shape is given — just "spots, gradually getting bigger." Even
though the potato reference material is prose extracted from several
leaflets rather than one clean per-disease list, this vague description
genuinely overlaps BOTH early blight (angular brown spots with concentric
dark rings, gradually enlarging) AND late blight, whose own text literally
says "ক্রমে দাগ বড় হয়" ("the spot gradually enlarges") — a near-verbatim
match to the farmer's own wording. Do not let the absence of a color/shape
detail push you toward whichever disease you thought of first; re-check
each candidate's actual doc text for phrases that overlap the farmer's
words, the way late blight's "ক্রমে বড় হয়" does here.
Correct response: ask which one — e.g. whether the spots are angular with
concentric rings (early blight), or pale green/water-soaked turning black
with white powdery growth underneath (late blight).
(Wrong response would be picking early blight OR late blight directly just
because its description happened to appear first, feel more "typical," or
because the farmer's short/vague wording seemed to lean one way without
actually checking the other candidate's own doc text for overlap.)
Example F (surface word overlap is not a real symptom match):
Farmer's question: "আমার আলুর গাছে সাদা পোকা দেখা যাচ্ছে" (white insect/pest)
The potato material describes late blight's symptom as a white, powdery
FUNGAL growth under the leaves — not an insect. An insect sighting is a
different kind of observation entirely.
Correct response: say this isn't described in your material (no insect
matching that description is covered) and suggest a local agricultural
officer — do NOT diagnose late blight just because both mention "white."
Example G (don't drop a candidate just because it's less "typical"):
Farmer's question: "তামাক গাছ ঢলে পড়ছে" (tobacco plant wilting)
Sudden wilting in the tobacco material could be black shank (lesions/rot at
the stem base near soil, black banded discoloration inside), Fusarium wilt
(one-sided yellowing first), or bacterial wilt (milky ooze from a cut stem,
no yellowing) — three candidates, not two. Even though black shank's most
distinctive marker is at the stem base rather than the leaves, its symptom
list does include sudden wilting, so it must be included as a candidate too.
Correct response: ask about ALL distinguishing details across ALL matching
diseases at once (e.g. "গাছের গোড়ায় কালচে পচা দাগ আছে কি? পাতা প্রথমে হলুদ
হয়েছে, নাকি সবুজ অবস্থাতেই ঢলে পড়েছে? কান্ড কাটলে সাদা আঠালো তরল বের হচ্ছে
কি?"), not just the two more common diseases while silently dropping a
third that also matches.
Example H (duplication across source documents must not skew the candidate
list or its stability):
Farmer's question: "গমের পাতায় দাগ দেখা যাচ্ছে" (spots on wheat leaves)
The wheat material comes from two source files, and wheat blast happens to
be described in both of them, while leaf spot/spot blotch and stem rust are
each described only once. That repetition does NOT make wheat blast a
stronger or weaker candidate — count it once, exactly like the others.
Vague "spots on leaves" genuinely overlaps with all three: wheat blast
(small water-soaked eye-shaped grey spots), leaf spot/spot blotch (elongated
brown-blackish spots, tan center), and stem rust (reddish-brown pustules).
Correct response: ask about ALL three at once (shape of the spot, whether
it's more pustule-like and rust-colored, and whether the spike itself is
also affected) — every time this question is asked, not just sometimes.
(Wrong response would be committing directly to just one of these three,
or asking about only two of them, because the duplicated document made one
disease feel more "available" than the others.)
The same applies to maize: fall armyworm and Turcicum leaf blight each
appear in more than one/duplicated source material for maize. A vague
"পাতায় দাগ ও ক্ষতি" (spots and damage) question must consistently raise
ALL genuinely matching candidates — Turcicum leaf blight, common rust,
downy mildew, and fall armyworm — every time, not a different subset of
two or three depending on the run.
Reference material:
{documents}
Farmer's question:
{question}"""
def load_doc(filepath):
"""Load a text file safely, warning if it's missing."""
try:
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"⚠️ WARNING: File not found — {filepath}")
return ""
# Which files belong to each crop's reference material.
CROP_FILES = {
"rice": [
"ধানের_খোলপোড়া_রোগ.txt",
"ধানের_চারাপোড়া_রোগ.txt",
"ধানের_টুংরো_রোগ.txt",
"ধানের_ব্লাস্ট_রোগ.txt",
"ধানের_ব্লাস্ট_রোগ_কৃষকদের_করণীয়.txt",
],
"jute": ["14-Diseases-of-Jute.txt"],
"beans": ["bean.txt", "bean 2.txt", "bean3.txt"],
"lemon": ["lemon.txt"],
"mustard": ["shorisha_aphid.txt", "shorisha_blight.txt"],
"potato": ["আলুর_রোগ_ও_প্রতিকার.txt"],
"tomato": ["tomato.txt"],
"wheat": ["Wheat_Diseases_Bangladesh.txt", "Wheat_Blast_Disease_Info.txt"],
"tobacco": ["Tobacco_Diseases_Bangladesh.txt"],
"maize": ["Maize_Diseases_Bangladesh.txt", "Fall_Armyworm_FAW_Info.txt"],
}
# Keywords (including common spelling variants) used to detect which crop(s)
# a farmer's question is about, so we can narrow the reference material
# passed to the model instead of always sending all ~10 crops' worth of text.
CROP_KEYWORDS = {
"rice": ["ধান", "দান", "চাল", "ভাত"],
"jute": ["পাট"],
"beans": ["শিম", "বরবটি"],
"lemon": ["লেবু"],
"mustard": ["সরিষা", "সরিষার"],
"potato": ["আলু"],
"tomato": ["টমেটো"],
"wheat": ["গম"],
"tobacco": ["তামাক"],
"maize": ["ভুট্টা", "মকা", "মকাই"],
}
def load_crop_docs():
"""Load reference text for each crop separately.
Returns a dict {crop_name: combined_text_for_that_crop}. Call this once
(e.g. at app startup) and reuse the result.
"""
return {
crop: "\n\n".join(filter(None, [load_doc(f) for f in files]))
for crop, files in CROP_FILES.items()
}
def identify_crops(question):
"""Return the list of crop names whose keywords appear in the question."""
return [
crop for crop, keywords in CROP_KEYWORDS.items()
if any(kw in question for kw in keywords)
]
def load_all_docs():
"""Load and combine ALL crops' reference documents into one string.
Kept for cases where the crop can't be narrowed down (no crop named,
or more than one crop named) — see answer_question().
"""
return "\n\n".join(filter(None, load_crop_docs().values()))
def get_answer(question, reference_docs):
"""Answer a farmer's question in Bangla, grounded only in reference_docs.
On API failure (network issue, rate limit, timeout, etc.), returns a
graceful Bangla fallback message instead of raising, so a single failed
call doesn't crash a live demo or app flow.
"""
prompt = PROMPT_TEMPLATE.format(documents=reference_docs, question=question)
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
# temperature 0.2 was causing real flip-flopping in eval runs
# (wheat/maize candidate lists shifting run to run, potato
# ambiguity sometimes not triggering a clarifying question,
# a jute grounding-stress case hallucinating once). 0.0 doesn't
# guarantee perfect determinism with this API, but it removes
# sampling as a source of instability so remaining failures are
# easier to attribute to the prompt itself.
temperature=0.0
)
return response.choices[0].message.content
except Exception as e:
print(f"⚠️ API call failed: {e}")
return FALLBACK_MESSAGE
def answer_question(question, crop_docs, combined_docs):
"""Route a question to the right crop's reference material and answer it.
- Exactly one crop keyword matched -> pass ONLY that crop's docs, so the
model isn't juggling all ~10 crops' worth of text and instructions at
once (this is what fixed crop-recognition flip-flopping and the
treatment/ambiguity misses that came from an overloaded single call).
- Zero or 2+ crops matched -> fall back to the full combined docs, so the
model can still ask "which crop do you mean?" or handle a genuinely
mixed-crop question, same as before.
"""
matched = identify_crops(question)
if len(matched) == 1:
return get_answer(question, crop_docs[matched[0]])
return get_answer(question, combined_docs)
if __name__ == "__main__":
crop_docs = load_crop_docs()
combined_docs = "\n\n".join(filter(None, crop_docs.values()))
print(f"Total combined document length: {len(combined_docs)} characters\n")
# --- Test questions across crops ---
test_questions = [
"আমার ধানের পাতায় চোখের মতো দাগ দেখা যাচ্ছে", # rice blast
"আমার পাটের গাছে কালো দাগ দেখা যাচ্ছে", # jute (ambiguous)
"আমার শিমের ফলে ছিদ্র দেখা যাচ্ছে", # bean pod borer
"আমার লেবু গাছের পাতায় হলুদ দাগ দেখা যাচ্ছে", # lemon canker
"আমার সরিষা গাছের পাতা কুঁকড়ে যাচ্ছে", # mustard aphid
"আমার আলু গাছের পাতায় সাদা পাউডারের মতো দাগ", # potato late blight
"আমার টমেটো গাছের পাতা হঠাৎ ঢলে পড়ছে", # tomato wilt
]
for q in test_questions:
print(f"Question: {q}")
print(answer_question(q, crop_docs, combined_docs))
print("\n" + "="*60 + "\n")
print("--- Specific rice symptom (should answer directly) ---")
print(answer_question(
"আমার ধানের ডিগ পাতা ও শীষের গোড়ায় কালচে বাদামি দাগ, ঘন কুয়াশার সময়",
crop_docs, combined_docs
))
print()
print("--- Potato (genuinely out of scope, should decline) ---")
print(answer_question("আমার আলুর গাছে সাদা পোকা দেখা যাচ্ছে", crop_docs, combined_docs))