File size: 17,799 Bytes
5bbfb4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
"""
IPM Limits accuracy test: compare Marco's stated limits against real ChromaDB data.

For each scenario:
  1. Pull the real IPM chunk directly from ChromaDB
  2. Ask Marco the same question
  3. Extract numbers from both
  4. Compare β€” flag mismatches
"""

import httpx
import time
import sys
import io
import re
import os
from pathlib import Path

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")

# Add project root to path so we can import app modules
sys.path.insert(0, str(Path(__file__).parent.parent))
os.environ.setdefault("PYTHONPATH", str(Path(__file__).parent.parent))

BASE_URL = "http://localhost:8000"
TIMEOUT  = 60.0
SEP = "-" * 80

# ── Real data lookup via IPMRetriever ─────────────────────────────────────────
def get_real_ipm_data(crop_code: str, disease_query: str, farming_type: str = "conventional") -> dict:
    """
    Pull the real IPM chunk from ChromaDB for this crop+disease.
    Returns dict with raw chunk text and key extracted fields.
    """
    from app.services.ipm_retriever import ipm_retriever
    results = ipm_retriever.search_excel(
        query=disease_query,
        crop_code=crop_code,
        farming_type=farming_type if farming_type == "organic" else None,
        n_results=3,
    )
    if not results:
        return {"found": False, "chunks": []}

    chunks = []
    for r in results:
        text = r.get("text", "")
        meta = r.get("metadata", {})
        chunks.append({
            "crop": meta.get("crop_full", ""),
            "pest": meta.get("pest_name", ""),
            "text": text,
            "raw_limits": _extract_limits_from_chunk(text, farming_type),
        })
    return {"found": True, "chunks": chunks}


def _extract_limits_from_chunk(text: str, farming_type: str) -> list:
    """
    Extract treatment limits from a chunk text.
    Looks for patterns like: (max N/anno), max N trattamenti, [gruppo max N],
    "Al massimo N interventi", "Massimo N interventi"
    """
    limits = []
    # Pattern: (max N/anno) or (max N/stagione)
    for m in re.finditer(r"max\s+(\d+)\s*/\s*anno", text, re.IGNORECASE):
        limits.append({"type": "per_anno", "value": int(m.group(1)), "context": m.group(0)})
    # Pattern: [gruppo max N]
    for m in re.finditer(r"gruppo\s+max\s+(\d+)", text, re.IGNORECASE):
        limits.append({"type": "gruppo", "value": int(m.group(1)), "context": m.group(0)})
    # Pattern: numero massimo trattamenti / max N trattamenti / Al massimo N interventi
    for m in re.finditer(r"(?:al\s+)?(?:massimo|max)(?:\s+di)?\s+(\d+)\s+(?:trattament|interventi|applicazion)", text, re.IGNORECASE):
        limits.append({"type": "trattamenti", "value": int(m.group(1)), "context": m.group(0)})
    # Pattern: "massimo N" standalone (e.g. "Massimo 10 interventi tra...")
    for m in re.finditer(r"[Mm]assimo\s+(\d+)\s+interventi", text):
        limits.append({"type": "trattamenti", "value": int(m.group(1)), "context": m.group(0)})
    return limits


def _extract_numbers_from_response(response: str) -> list:
    """Extract treatment-related numbers from Marco's response."""
    numbers = []
    patterns = [
        # Italian: "massimo di 2 trattamenti", "al massimo 6", "max 3/anno"
        r"(?:max|massimo|al massimo)(?:\s+di)?\s+(\d+)\s*(?:trattament|applicazion|volte|per anno|/anno|stagione|interventi)",
        # Italian: "limitato a 2 per anno", "limitato a 2 trattamenti"
        r"limitato\s+a\s+(\d+)\s*(?:per\s+(?:anno|stagione)|trattament|applicazion|volte)",
        # Italian: "Fino a X volte per stagione", "fino a X trattamenti per stagione"
        r"fino\s+a\s+(\d+)\s+(?:volte|trattament|applicazion)\s+(?:per|alla?)\s+stagione",
        # Italian: "fino a X volte" (bare)
        r"fino\s+a\s+(\d+)\s+(?:volte|trattament|applicazion)",
        # Italian: "puoi applicarlo/effettuare fino a X"
        r"puoi\s+(?:\w+\s+){0,4}fino\s+a\s+(\d+)",
        # Italian: "X volte per stagione"
        r"(\d+)\s+volte\s+per\s+stagione",
        # Italian: "Applicazioni: Fino a X" / "Applicazioni: X"
        r"[Aa]pplicazioni\s*:\s*(?:[Ff]ino\s+a\s+)?(\d+)",
        # Italian: "2 trattamenti per anno/stagione"
        r"(\d+)\s+(?:trattament|applicazion|interventi)\s+(?:per|all')\s*(?:anno|stagione)",
        r"(\d+)\s+(?:trattament|applicazion|interventi)\s+massim",
        # Italian: "X–Y volte/trattamenti" β€” take first number of range
        r"(\d+)(?:–|-)\d+\s+(?:volte|trattament|applicazion)\s+per\s+stagione",
        r"da\s+(\d+)\s+a\s+\d+\s+(?:trattament|applicazion|volte)",
        # Label app format: "Grapevine: 2–6" or "2-6 per season"
        r"(?:Grapevine|Vite|Fruit trees|Tomato|Pomodoro)\s*:\s*(\d+)",
        r"label apps.*?(\d+)",
        # English
        r"maximum\s+(?:of\s+)?(\d+)\s+(?:application|treatment|time)",
        r"(\d+)\s+(?:application|treatment)s?\s+per\s+(?:season|year)",
        r"up\s+to\s+(\d+)\s+(?:application|treatment)",
        r"(\d+)(?:–|-)\d+\s+(?:application|treatment|time)s?\s+per",
    ]
    for pat in patterns:
        for m in re.finditer(pat, response, re.IGNORECASE):
            try:
                numbers.append(int(m.group(1)))
            except (ValueError, IndexError):
                pass
    return list(set(numbers))


def ask_marco(client: httpx.Client, session_id: str, message: str,
              crop_type: str = None, farming_type: str = "conventional",
              bbch_stage: str = None) -> str:
    payload = {
        "session_id": session_id,
        "message": message,
        "farming_type": farming_type,
        "weather_enabled": False,
    }
    if crop_type:
        payload["crop_type"] = crop_type
    if bbch_stage:
        payload["bbch_stage"] = bbch_stage

    resp = client.post(f"{BASE_URL}/api/chat", json=payload, timeout=TIMEOUT)
    if resp.status_code != 200:
        return f"[ERROR {resp.status_code}]"
    return resp.json().get("response", "")


# ── Test scenarios ─────────────────────────────────────────────────────────────
SCENARIOS = [
    {
        "id": 1,
        "label": "Vite / oidio / conventional",
        "crop_code": "vite", "farming_type": "conventional", "bbch_stage": "57",
        "ipm_query": "vite oidio oidium sostanze trattamento massimo",
        "question": "ho l'oidio sulle mie viti BBCH 57, cosa uso e quanti trattamenti posso fare?",
    },
    {
        "id": 2,
        "label": "Vite / peronospora / conventional",
        "crop_code": "vite", "farming_type": "conventional", "bbch_stage": "71",
        "ipm_query": "vite peronospora Plasmopara viticola sostanze trattamento massimo",
        "question": "peronospora sulle viti BBCH 71, quali prodotti e quanti trattamenti?",
    },
    {
        "id": 3,
        "label": "Vite / botrytis / organic",
        "crop_code": "vite", "farming_type": "organic", "bbch_stage": "79",
        "ipm_query": "vite botrytis muffa grigia biologico sostanze ammesse massimo",
        "question": "ho botrytis sulle viti BBCH 79, sono in biologico, cosa uso e quante volte?",
    },
    {
        "id": 4,
        "label": "Pomodoro / peronospora / conventional",
        "crop_code": "pomodoro", "farming_type": "conventional", "bbch_stage": "71",
        "ipm_query": "pomodoro peronospora sostanze trattamento massimo numero",
        "question": "peronospora sul pomodoro BBCH 71, prodotto AllTech e limite trattamenti IPM?",
    },
    {
        "id": 5,
        "label": "Pomodoro / alternaria / conventional",
        "crop_code": "pomodoro", "farming_type": "conventional", "bbch_stage": "71",
        "ipm_query": "pomodoro alternaria early blight sostanze trattamento massimo",
        "question": "alternaria sul pomodoro BBCH 71, cosa uso e quanti trattamenti max?",
    },
    {
        "id": 6,
        "label": "Pomodoro / botrytis / organic",
        "crop_code": "pomodoro", "farming_type": "organic", "bbch_stage": "79",
        "ipm_query": "pomodoro botrytis biologico sostanze ammesse massimo",
        "question": "botrytis sul pomodoro biologico BBCH 79, prodotto e limite trattamenti?",
    },
    {
        "id": 7,
        "label": "Melo / ticchiolatura / conventional",
        "crop_code": "melo", "farming_type": "conventional", "bbch_stage": "65",
        "ipm_query": "melo ticchiolatura Venturia inaequalis sostanze massimo trattamenti",
        "question": "ticchiolatura sul melo BBCH 65, dimmi prodotto AllTech e max trattamenti IPM",
    },
    {
        "id": 8,
        "label": "Melo / oidio / organic",
        "crop_code": "melo", "farming_type": "organic", "bbch_stage": "65",
        "ipm_query": "melo oidio biologico sostanze ammesse massimo",
        "question": "oidio sul melo biologico BBCH 65, cosa posso usare e quante volte?",
    },
    {
        "id": 9,
        "label": "Pero / ticchiolatura / conventional",
        "crop_code": "pero", "farming_type": "conventional", "bbch_stage": "65",
        "ipm_query": "pero ticchiolatura Venturia pirina sostanze massimo trattamenti",
        "question": "ticchiolatura sul pero BBCH 65, prodotto AllTech e limite trattamenti IPM?",
    },
    {
        "id": 10,
        "label": "Fragola / botrytis / organic",
        "crop_code": "fragola", "farming_type": "organic", "bbch_stage": "71",
        "ipm_query": "fragola botrytis biologico sostanze ammesse massimo trattamenti",
        "question": "botrytis sulle fragole biologico BBCH 71, prodotto e quanti trattamenti?",
    },
    {
        "id": 11,
        "label": "Olivo / occhio di pavone / conventional",
        "crop_code": "olivo", "farming_type": "conventional", "bbch_stage": "60",
        "ipm_query": "olivo occhio di pavone Spilocaea oleagina sostanze massimo",
        "question": "occhio di pavone sull'olivo BBCH 60, cosa uso e quanti trattamenti max?",
    },
    {
        "id": 12,
        "label": "Vite / peronospora / organic",
        "crop_code": "vite", "farming_type": "organic", "bbch_stage": "57",
        "ipm_query": "vite peronospora biologico sostanze ammesse massimo trattamenti",
        "question": "peronospora sulle viti biologico BBCH 57, prodotto e limite trattamenti IPM?",
    },
    {
        "id": 13,
        "label": "Pomodoro / oidio / conventional",
        "crop_code": "pomodoro", "farming_type": "conventional", "bbch_stage": "65",
        "ipm_query": "pomodoro oidio sostanze massimo trattamenti",
        "question": "oidio sul pomodoro BBCH 65, prodotto AllTech e max trattamenti secondo IPM?",
    },
    {
        "id": 14,
        "label": "Melo / peronospora / conventional",
        "crop_code": "melo", "farming_type": "conventional", "bbch_stage": "71",
        "ipm_query": "melo peronospora Phytophthora cactorum sostanze massimo",
        "question": "peronospora sul melo BBCH 71, prodotto e limite trattamenti IPM?",
    },
    {
        "id": 15,
        "label": "Vite / oidio / organic",
        "crop_code": "vite", "farming_type": "organic", "bbch_stage": "71",
        "ipm_query": "vite oidio biologico sostanze ammesse massimo",
        "question": "oidio sulle viti biologico BBCH 71, cosa posso usare e quante volte?",
    },
]


def main():
    print("\n" + "=" * 80)
    print("  IPM LIMITS vs REAL DATA β€” 15 scenarios")
    print("=" * 80 + "\n")

    results = []

    with httpx.Client() as client:
        for sc in SCENARIOS:
            session_id = f"ipm_vs_data_{sc['id']}_{int(time.time())}"
            print(SEP)
            print(f"  [{sc['id']:02d}] {sc['label']}")
            print(SEP)

            # ── Step 1: Get real data from ChromaDB ──────────────────────────
            real = get_real_ipm_data(sc["crop_code"], sc["ipm_query"], sc["farming_type"])

            print(f"  REAL DATA ({sc['crop_code']} / {sc['farming_type']}):")
            if not real["found"]:
                print("    [NOT FOUND in ChromaDB]")
                real_limits = []
                real_substances_text = ""
            else:
                real_limits = []
                real_substances_text = ""
                for i, chunk in enumerate(real["chunks"][:2]):
                    print(f"    Chunk {i+1}: {chunk['pest']} on {chunk['crop']}")
                    # Print relevant lines from chunk
                    for line in chunk["text"].split("\n"):
                        line = line.strip()
                        if any(kw in line.lower() for kw in [
                            "sostanz", "massimo", "max", "limite", "limitaz",
                            "gruppo", "trattament", "biologico", "convenzional"
                        ]):
                            print(f"      > {line[:120]}")
                            real_substances_text += line + " "
                    real_limits.extend(chunk["raw_limits"])

            if real_limits:
                print(f"    EXTRACTED LIMITS: {real_limits}")
            else:
                print(f"    EXTRACTED LIMITS: none found (may be in free text)")

            # ── Step 2: Ask Marco ────────────────────────────────────────────
            print(f"\n  USER: {sc['question']}")
            t0 = time.time()
            response = ask_marco(
                client, session_id, sc["question"],
                crop_type=sc["crop_code"],
                farming_type=sc["farming_type"],
                bbch_stage=sc.get("bbch_stage"),
            )
            elapsed = time.time() - t0

            print(f"  MARCO ({elapsed:.1f}s):")
            print(f"    {response[:600]}{'...' if len(response) > 600 else ''}")

            # ── Step 3: Extract numbers from Marco's response ────────────────
            marco_numbers = _extract_numbers_from_response(response)
            real_numbers  = [l["value"] for l in real_limits]

            print(f"\n  COMPARISON:")
            print(f"    Real limits (from data) : {real_numbers if real_numbers else 'not extracted (check raw text)'}")
            print(f"    Marco stated numbers    : {marco_numbers if marco_numbers else 'none detected'}")

            # ── Step 4: Verdict ──────────────────────────────────────────────
            # If real_numbers found, check if Marco's numbers match or include them
            # If no real_numbers extracted, check at minimum that Marco stated SOME limit
            if real_numbers and marco_numbers:
                # Check if any of Marco's numbers match any real limit
                matches = [n for n in marco_numbers if n in real_numbers]
                correct = len(matches) > 0
                verdict = "MATCH" if correct else "MISMATCH"
                detail = f"real={real_numbers}, marco={marco_numbers}"
            elif real_numbers and not marco_numbers:
                correct = False
                verdict = "MISSING"
                detail = f"real data has limits {real_numbers} but Marco stated no number"
            elif not real_numbers and marco_numbers:
                correct = True   # Can't verify, but at least stated something
                verdict = "UNVERIFIABLE (stated)"
                detail = f"no limit extracted from raw data, Marco stated {marco_numbers}"
            else:
                correct = None
                verdict = "UNVERIFIABLE"
                detail = "neither data nor Marco had extractable numbers"

            icon = "[OK]  " if correct else ("[?]   " if correct is None else "[FAIL]")
            print(f"    {icon} {verdict}: {detail}")

            results.append({
                "id": sc["id"],
                "label": sc["label"],
                "correct": correct,
                "verdict": verdict,
                "real_numbers": real_numbers,
                "marco_numbers": marco_numbers,
                "real_text_snippet": real_substances_text[:200],
            })
            print()

    # ── Summary ────────────────────────────────────────────────────────────────
    print("\n" + "=" * 80)
    print("  SUMMARY β€” Real Data vs Marco")
    print("=" * 80)
    print(f"  {'ID':<4} {'Verdict':<20} {'Real':<15} {'Marco':<15} Label")
    print(f"  {'-'*4} {'-'*20} {'-'*15} {'-'*15} {'-'*30}")
    for r in results:
        icon = "[OK]" if r["correct"] else ("[?] " if r["correct"] is None else "[!]")
        print(f"  {r['id']:<4} {icon} {r['verdict']:<16} {str(r['real_numbers']):<15} {str(r['marco_numbers']):<15} {r['label']}")

    verifiable = [r for r in results if r["correct"] is not None]
    correct_v  = [r for r in verifiable if r["correct"]]
    unverif    = [r for r in results if r["correct"] is None]
    matches    = [r for r in verifiable if r["verdict"] == "MATCH"]
    mismatches = [r for r in verifiable if r["verdict"] == "MISMATCH"]
    missing    = [r for r in verifiable if r["verdict"] == "MISSING"]

    print(f"\n  Verifiable scenarios : {len(verifiable)}/15")
    print(f"  MATCH   (correct)    : {len(matches)}")
    print(f"  MISMATCH (wrong num) : {len(mismatches)}")
    print(f"  MISSING  (no number) : {len(missing)}")
    print(f"  Unverifiable         : {len(unverif)}")
    if verifiable:
        acc = len(correct_v) / len(verifiable) * 100
        print(f"  Accuracy (verifiable): {acc:.1f}%")
    print("=" * 80 + "\n")


if __name__ == "__main__":
    main()