File size: 9,102 Bytes
7d580fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5220dc
 
7d580fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5220dc
7d580fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5220dc
7d580fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
gemini_vision.py β€” P48: Gemini 1.5 Flash Vision (Manus Killer).

Modulo dedicato per analisi immagini con Gemini 1.5 Flash:
  - Free tier: 15 RPM, 1M token/giorno (AI Studio key, zero costi)
  - POST /api/vision/gemini            β€” analisi diretta Gemini (bypass fallback chain)
  - POST /api/vision/screenshot_analyze β€” Playwright screenshot + Gemini in un'unica call

Gap chiuso (P48):
  - Prima: analyze_image richiedeva GROQ_API_KEY, Gemini era solo fallback #2
  - Dopo: Gemini disponibile come provider primario zero-cost
  - Caso d'uso critico: agente genera app β†’ scatta screenshot β†’ Gemini trova glitch
    visivi β†’ agente si auto-corregge senza supervisione umana

@module gemini_vision
"""
import os, base64, httpx, logging
from fastapi import APIRouter, Depends
from .auth_guard import require_role, AuthRole
from pydantic import BaseModel

router  = APIRouter(prefix="/api/vision", tags=["gemini_vision"])
_logger = logging.getLogger("gemini_vision")

_GEMINI_KEY   = os.getenv("GEMINI_API_KEY", "")
_GEMINI_BASE  = "https://generativelanguage.googleapis.com/v1beta/models"
_GEMINI_MODEL = "gemini-1.5-flash"
_USER_AGENT   = "Mozilla/5.0 (compatible; AgenteAI/3.0)"


# ─── Modelli ──────────────────────────────────────────────────────────────────

class GeminiAnalyzeRequest(BaseModel):
    url:          str = ""
    base64_image: str = ""
    question:     str = "Analizza questa immagine in dettaglio. Descrivi cosa vedi, identifica problemi visivi o errori UI."
    model:        str = "gemini-1.5-flash"
    max_tokens:   int = 800


class ScreenshotAnalyzeRequest(BaseModel):
    url:        str
    question:   str = "Analizza questo screenshot come UI tester esperto. Identifica: 1) Errori visivi (layout rotto, testo troncato, overlap). 2) Errori JS o console visibili. 3) Elementi mancanti o mal posizionati. 4) Problemi di responsive. Rispondi in italiano con bullet points."
    mobile:     bool = False
    width:      int  = 1280


# ─── Core helper ──────────────────────────────────────────────────────────────

async def gemini_analyze(
    image_b64:  str,
    image_mime: str,
    question:   str,
    model:      str = "gemini-1.5-flash",
    max_tokens: int = 800,
    api_key:    str = "",
) -> dict:
    """
    Chiama Gemini Vision API con immagine in base64.
    Riutilizzata da vision.py (fallback chain) e dagli endpoint di questo modulo.
    Ritorna {"ok": True, "description": str, "provider": str}
    oppure  {"ok": False, "error": str}.
    """
    key = api_key or _GEMINI_KEY
    if not key:
        return {"ok": False, "error": "GEMINI_API_KEY non configurato."}
    if not image_b64:
        return {"ok": False, "error": "Nessuna immagine fornita."}

    payload = {
        "contents": [{
            "parts": [
                {"inline_data": {"mime_type": image_mime, "data": image_b64}},
                {"text": question},
            ]
        }],
        "generationConfig": {"maxOutputTokens": max_tokens, "temperature": 0.4},
    }
    try:
        async with httpx.AsyncClient(timeout=40) as c:
            r = await c.post(
                f"{_GEMINI_BASE}/{model}:generateContent?key={key}",
                headers={"Content-Type": "application/json"},
                json=payload,
            )
            if r.status_code == 429:
                return {"ok": False, "error": "Gemini rate limit (15 RPM). Riprova tra qualche secondo."}
            if not r.is_success:
                return {"ok": False, "error": f"Gemini API {r.status_code}: {r.text[:300]}"}
            _cands = r.json().get("candidates") or []
            _parts = (_cands[0].get("content", {}).get("parts") or []) if _cands else []
            _desc  = next((p.get("text", "") for p in _parts if "text" in p), "")
            if not _desc:
                return {"ok": False, "error": "Gemini: risposta vuota (candidates vuoti)."}
            return {"ok": True, "description": _desc, "provider": f"gemini-{model.split('-')[-1]}"}
    except Exception as e:
        _logger.debug("gemini_analyze: %s", type(e).__name__)
        return {"ok": False, "error": f"Errore Gemini: {str(e)[:200]}"}


# ─── Endpoints ────────────────────────────────────────────────────────────────

@router.post("/gemini")
async def gemini_vision_direct(req: GeminiAnalyzeRequest, role: AuthRole = Depends(require_role(AuthRole.MACHINE))):  # GAP-1-fix
    """
    POST /api/vision/gemini β€” Analisi diretta Gemini Vision (bypass fallback chain).
    Accetta url o base64_image. Provider primario zero-cost per vision tasks.
    """
    image_b64  = req.base64_image
    image_mime = "image/jpeg"

    if not image_b64 and req.url:
        try:
            async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c:
                r = await c.get(req.url, headers={"User-Agent": _USER_AGENT})
                if r.status_code != 200:
                    return {"ok": False, "error": f"Download fallito: HTTP {r.status_code}"}
                ct = r.headers.get("content-type", "")
                image_mime = "image/png" if "png" in ct else "image/webp" if "webp" in ct else "image/jpeg"
                image_b64  = base64.b64encode(r.content).decode()
        except Exception as e:
            return {"ok": False, "error": f"Errore download: {str(e)[:200]}"}

    if not image_b64:
        return {"ok": False, "error": "Fornisci url o base64_image."}

    return await gemini_analyze(image_b64, image_mime, req.question, req.model, req.max_tokens)


@router.post("/screenshot_analyze")
async def screenshot_analyze(req: ScreenshotAnalyzeRequest, role: AuthRole = Depends(require_role(AuthRole.MACHINE))):  # GAP-1-fix
    """
    POST /api/vision/screenshot_analyze β€” Playwright screenshot + Gemini analyze.

    Pipeline (P48 Manus Killer):
      1. Playwright screenshot della pagina (via _take_screenshot da api.browser)
      2. Gemini 1.5 Flash analizza UI: glitch, errori, layout, responsive
      3. Struttura il feedback per self-correction loop dell'agente

    Fallback: se Playwright non disponibile β†’ scarica URL come immagine (se immagine diretta).
    """
    if not _GEMINI_KEY:
        return {
            "ok": False,
            "error": "GEMINI_API_KEY non configurato.",
            "hint": "Aggiungi GEMINI_API_KEY in Railway Variables. Free su aistudio.google.com",
        }

    image_b64  = ""
    image_mime = "image/png"
    page_title = req.url

    # Step 1: Playwright screenshot (import interno β€” fail-safe se non disponibile)
    try:
        from api.browser import _take_screenshot as _shot  # type: ignore[import]
        _result = await _shot(req.url, mobile=req.mobile, width=req.width, wait_ms=1500)
        if _result.get("ok") and _result.get("screenshot_b64"):
            image_b64  = _result["screenshot_b64"]
            page_title = _result.get("title", req.url)
    except Exception as _e:
        _logger.debug("screenshot_analyze: browser import failed: %s", _e)

    # Fallback: URL Γ¨ un'immagine diretta
    if not image_b64:
        try:
            async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c:
                r = await c.get(req.url, headers={"User-Agent": _USER_AGENT})
                if r.is_success:
                    ct = r.headers.get("content-type", "")
                    if "image" in ct:
                        image_mime = "image/png" if "png" in ct else "image/jpeg"
                        image_b64  = base64.b64encode(r.content).decode()
        except Exception:
            pass

    if not image_b64:
        return {
            "ok": False,
            "error": f"Screenshot non disponibile per {req.url}.",
            "hint": "Verifica che il backend Railway abbia Playwright installato.",
        }

    # Step 2: Gemini analyze
    _q = req.question or (
        "Sei un UI tester esperto. Analizza questo screenshot e identifica:\n"
        "1. Errori visivi (layout rotto, overlap, testo troncato, colori sbagliati)\n"
        "2. Errori o messaggi di errore visibili nella pagina\n"
        "3. Elementi mancanti o mal posizionati\n"
        "4. Problemi di accessibilita evidenti\n"
        "Rispondi con bullet points. Sii specifico sugli elementi problematici."
    )
    _analysis = await gemini_analyze(image_b64, image_mime, _q, _GEMINI_MODEL, 1000)
    if not _analysis.get("ok"):
        return _analysis

    return {
        "ok":                  True,
        "description":         _analysis["description"],
        "provider":            _analysis.get("provider", "gemini-1.5-flash"),
        "page_title":          page_title,
        "url":                 req.url,
        "screenshot_available": True,
    }