File size: 6,850 Bytes
7ff7119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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"
        )
        # JSON-extract
        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")

    # JSON is, gépi feldolgozáshoz
    (out_dir / "ai_validation.json").write_text(
        json.dumps([r.to_dict() for r in results], ensure_ascii=False, indent=2),
        encoding="utf-8",
    )