| """AI-validáló: a Playwright screenshot-jait és a chat-válaszokat |
| ellenőrzi Claude vision-API-val az `EXPECTED_FINDINGS` paritás-elvárások |
| alapján. |
| |
| A `prototype-agentic` E2E manuális tesztelést a user szemmel ellenőrizte: |
| látta-e MAGAS finding az audit_demo-ban, GDPR-aszimmetria a compliance-ben, |
| top red flags a DD-ben. Ez a modul ezt **automatizálja** Claude-dal: |
| |
| validate_screenshot(image_path, expected_findings: list[str]) → ValidationResult |
| |
| Minden screenshot-ra/válaszra a Claude egy strukturált értékelést ad: |
| * mely várt findingek látszanak (igen/részben/nem) |
| * vannak-e meglepetések (false positive vagy hiányzó) |
| * áttekintés (1-2 mondat) |
| |
| A modul fail-fast: ha az ANTHROPIC_API_KEY nincs beállítva, üzenettel skip. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import base64 |
| import json |
| import os |
| from dataclasses import asdict, dataclass |
| from pathlib import Path |
| from typing import Literal |
|
|
|
|
| @dataclass |
| class ValidationResult: |
| """Egy teszt-eset AI-validáció eredménye.""" |
| test_case: str |
| expected_count: int |
| found_count: int |
| missing: list[str] |
| surprises: list[str] |
| overall: Literal["pass", "partial", "fail"] |
| summary: str |
|
|
| def to_dict(self) -> dict: |
| return asdict(self) |
|
|
|
|
| def _claude_vision_validate( |
| image_b64: str, |
| test_case_label: str, |
| expected_findings: list[str], |
| raw_text_context: str = "", |
| ) -> ValidationResult: |
| """Claude vision-hívás screenshot + szöveges várt-finding listával. |
| |
| Részletes magyar prompt: a Claude visszaad JSON-t ami megmondja, melyik |
| expected_finding látszik a screenshot-on (vagy a raw_text-ben). |
| """ |
| try: |
| from langchain_anthropic import ChatAnthropic |
| except ImportError: |
| return ValidationResult( |
| test_case=test_case_label, |
| expected_count=len(expected_findings), |
| found_count=0, |
| missing=expected_findings, |
| surprises=[], |
| overall="fail", |
| summary="langchain_anthropic nincs telepítve", |
| ) |
|
|
| expected_block = "\n".join(f" - {f}" for f in expected_findings) |
| user_prompt = f"""Egy screenshot-ot és egy szöveges kontextust adok az `Agentic Document Intelligence Platform` UI-jából. |
| |
| Teszt-eset: **{test_case_label}** |
| |
| Várt findingek (paritás a `prototype-agentic` `EXPECTED_FINDINGS.md`-vel): |
| {expected_block} |
| |
| Szöveges kontextus (ha van): |
| {raw_text_context[:3000] if raw_text_context else "(üres)"} |
| |
| Feladatod: állapítsd meg, mely várt findingek látszanak a screenshot-on |
| vagy a szöveges kontextusban. Adj vissza JSON-t a következő mezőkkel: |
| - `found`: list[str] — a megtalált findingek (`expected_findings`-ből) |
| - `missing`: list[str] — a NEM megtalált findingek |
| - `surprises`: list[str] — más, nem várt findingek vagy gyanús minták (max 5) |
| - `overall`: "pass" | "partial" | "fail" |
| ("pass" = minden megvan, "partial" = legalább 50%, "fail" = kevesebb mint 50%) |
| - `summary`: 1-2 mondatos értékelés magyarul |
| |
| Strict JSON, magyarázat nélkül.""" |
|
|
| try: |
| llm = ChatAnthropic( |
| model_name=os.getenv("CLAUDE_MODEL", "claude-haiku-4-5-20251001"), |
| temperature=0, |
| ) |
| from langchain_core.messages import HumanMessage |
| msg = HumanMessage(content=[ |
| {"type": "text", "text": user_prompt}, |
| { |
| "type": "image", |
| "source_type": "base64", |
| "data": image_b64, |
| "mime_type": "image/png", |
| }, |
| ]) |
| response = llm.invoke([msg]) |
| content = response.content |
| text = content if isinstance(content, str) else "\n".join( |
| p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text" |
| ) |
| |
| start = text.find("{") |
| end = text.rfind("}") |
| if start < 0 or end < 0: |
| raise ValueError("Nem talált JSON-t a Claude-válaszban") |
| data = json.loads(text[start:end + 1]) |
| return ValidationResult( |
| test_case=test_case_label, |
| expected_count=len(expected_findings), |
| found_count=len(data.get("found", [])), |
| missing=data.get("missing", []), |
| surprises=data.get("surprises", []), |
| overall=data.get("overall", "partial"), |
| summary=data.get("summary", ""), |
| ) |
| except Exception as exc: |
| return ValidationResult( |
| test_case=test_case_label, |
| expected_count=len(expected_findings), |
| found_count=0, |
| missing=expected_findings, |
| surprises=[], |
| overall="fail", |
| summary=f"AI-validáció hiba: {type(exc).__name__}: {exc}", |
| ) |
|
|
|
|
| def validate_screenshot( |
| image_path: Path, |
| test_case_label: str, |
| expected_findings: list[str], |
| raw_text_context: str = "", |
| ) -> ValidationResult: |
| """A screenshot fájl + várt findings → ValidationResult. |
| |
| Args: |
| image_path: a Playwright `full_page=True` screenshot |
| test_case_label: pl. "audit_demo / Eredmények tab" |
| expected_findings: paritás-listák a `prototype-agentic/test_data/EXPECTED_FINDINGS.md`-ből |
| raw_text_context: opcionális szöveges kontextus (pl. chat-válasz, DOCX-text) |
| """ |
| if not image_path.exists(): |
| return ValidationResult( |
| test_case=test_case_label, |
| expected_count=len(expected_findings), |
| found_count=0, |
| missing=expected_findings, |
| surprises=[], |
| overall="fail", |
| summary=f"Nem létezik a screenshot: {image_path}", |
| ) |
|
|
| image_b64 = base64.standard_b64encode(image_path.read_bytes()).decode("ascii") |
| return _claude_vision_validate(image_b64, test_case_label, expected_findings, raw_text_context) |
|
|
|
|
| def write_validation_report(out_dir: Path, results: list[ValidationResult]) -> None: |
| """Markdown report írás a `snapshots/{testcase}/ai_validation.md`-be.""" |
| out_dir.mkdir(parents=True, exist_ok=True) |
| md = ["# AI-validáció", ""] |
| for r in results: |
| emoji = {"pass": "[OK]", "partial": "[RÉSZBEN]", "fail": "[FAIL]"}[r.overall] |
| md.append(f"## {emoji} {r.test_case}") |
| md.append(f"- Várt: {r.expected_count}, talált: {r.found_count}") |
| if r.missing: |
| md.append(f"- Hiányzó: {', '.join(r.missing)}") |
| if r.surprises: |
| md.append(f"- Meglepetések: {', '.join(r.surprises)}") |
| md.append(f"- {r.summary}") |
| md.append("") |
| (out_dir / "ai_validation.md").write_text("\n".join(md), encoding="utf-8") |
|
|
| |
| (out_dir / "ai_validation.json").write_text( |
| json.dumps([r.to_dict() for r in results], ensure_ascii=False, indent=2), |
| encoding="utf-8", |
| ) |
|
|