Spaces:
Runtime error
Runtime error
Upload app.py
#2
by vmcii - opened
app.py
CHANGED
|
@@ -7,6 +7,8 @@ from __future__ import annotations
|
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
import re
|
|
|
|
|
|
|
| 10 |
from pathlib import Path
|
| 11 |
|
| 12 |
import gradio as gr
|
|
@@ -17,6 +19,8 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
| 17 |
from sentence_transformers import SentenceTransformer
|
| 18 |
from retrieval_zerogpu import OriginRetrieval, MODEL_ID
|
| 19 |
|
|
|
|
|
|
|
| 20 |
ROOT = Path(__file__).resolve().parent
|
| 21 |
DATA = ROOT / 'assets/projects_clean.csv'
|
| 22 |
VECTORS = ROOT / 'assets/project_vectors.npz'
|
|
@@ -129,22 +133,86 @@ def enhance(title: str, description: str, language: str, search_payload: dict):
|
|
| 129 |
request = {'proposed_idea':{'title':idea.title,'description':idea.description,'language':lang},
|
| 130 |
'retrieved_projects':context}
|
| 131 |
model_id = os.getenv('ORIGIN_GEMINI_MODEL', 'gemini-2.5-flash')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
try:
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
except Exception as exc:
|
| 146 |
-
#
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
# Recreate response from vetted local CSV and validated generated fields.
|
| 150 |
ranks = [{'rank':i+1, 'project_id':pid} for i,pid in enumerate(ids)]
|
|
|
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
import re
|
| 10 |
+
import logging
|
| 11 |
+
import time
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
import gradio as gr
|
|
|
|
| 19 |
from sentence_transformers import SentenceTransformer
|
| 20 |
from retrieval_zerogpu import OriginRetrieval, MODEL_ID
|
| 21 |
|
| 22 |
+
logger = logging.getLogger('origin.gemini')
|
| 23 |
+
|
| 24 |
ROOT = Path(__file__).resolve().parent
|
| 25 |
DATA = ROOT / 'assets/projects_clean.csv'
|
| 26 |
VECTORS = ROOT / 'assets/project_vectors.npz'
|
|
|
|
| 133 |
request = {'proposed_idea':{'title':idea.title,'description':idea.description,'language':lang},
|
| 134 |
'retrieved_projects':context}
|
| 135 |
model_id = os.getenv('ORIGIN_GEMINI_MODEL', 'gemini-2.5-flash')
|
| 136 |
+
# Use Gemini's supported Pydantic structured-output mode. For Gemini 2.5 Flash,
|
| 137 |
+
# disabling thinking prevents a large hidden token budget consuming the output
|
| 138 |
+
# budget, which can otherwise produce empty/truncated JSON responses.
|
| 139 |
+
config_kwargs = dict(
|
| 140 |
+
system_instruction=SYSTEM_INSTRUCTIONS,
|
| 141 |
+
response_mime_type='application/json',
|
| 142 |
+
response_schema=GeneratedAnalysis,
|
| 143 |
+
temperature=0.2,
|
| 144 |
+
max_output_tokens=4096,
|
| 145 |
+
)
|
| 146 |
+
if model_id.startswith('gemini-2.5-flash'):
|
| 147 |
+
config_kwargs['thinking_config'] = types.ThinkingConfig(thinking_budget=0)
|
| 148 |
+
|
| 149 |
try:
|
| 150 |
+
# 60 seconds per request; avoid SDK retries that can consume extra quota.
|
| 151 |
+
client = genai.Client(
|
| 152 |
+
api_key=key,
|
| 153 |
+
http_options=types.HttpOptions(
|
| 154 |
+
timeout=60000,
|
| 155 |
+
retry_options=types.HttpRetryOptions(attempts=1),
|
| 156 |
+
),
|
| 157 |
+
)
|
| 158 |
+
for attempt in range(2):
|
| 159 |
+
try:
|
| 160 |
+
response = client.models.generate_content(
|
| 161 |
+
model=model_id,
|
| 162 |
+
contents=json.dumps(request, ensure_ascii=False),
|
| 163 |
+
config=types.GenerateContentConfig(**config_kwargs),
|
| 164 |
+
)
|
| 165 |
+
break
|
| 166 |
+
except Exception as exc:
|
| 167 |
+
code = getattr(exc, 'code', None)
|
| 168 |
+
# Retry only a temporarily unavailable server; NEVER retry
|
| 169 |
+
# invalid API keys, bad requests, or exhausted Gemini quota.
|
| 170 |
+
if attempt == 0 and code in (502, 503, 504):
|
| 171 |
+
logger.warning('origin.gemini_retry code=%s model=%s', code, model_id)
|
| 172 |
+
time.sleep(2)
|
| 173 |
+
continue
|
| 174 |
+
raise
|
| 175 |
+
|
| 176 |
+
# The SDK can return a validated Pydantic instance directly.
|
| 177 |
+
if isinstance(getattr(response, 'parsed', None), GeneratedAnalysis):
|
| 178 |
+
generated = response.parsed
|
| 179 |
+
elif getattr(response, 'text', None):
|
| 180 |
+
generated = GeneratedAnalysis.model_validate_json(response.text)
|
| 181 |
+
else:
|
| 182 |
+
# Log only finish-reason name; never log candidate content.
|
| 183 |
+
candidates = getattr(response, 'candidates', None) or []
|
| 184 |
+
reason = getattr(candidates[0], 'finish_reason', None) if candidates else None
|
| 185 |
+
logger.warning('origin.gemini_empty_response finish_reason=%s model=%s',
|
| 186 |
+
getattr(reason, 'name', str(reason)) if reason else 'unknown', model_id)
|
| 187 |
+
raise ValueError('empty_gemini_response')
|
| 188 |
except Exception as exc:
|
| 189 |
+
# Gemini SDK exceptions often contain private request details. Log only
|
| 190 |
+
# safe status and exception TYPE, never exception message or prompt.
|
| 191 |
+
code = getattr(exc, 'code', None)
|
| 192 |
+
if code == 429:
|
| 193 |
+
category = 'quota_or_rate_limit'
|
| 194 |
+
elif code in (401, 403):
|
| 195 |
+
category = 'credentials_or_permissions'
|
| 196 |
+
elif code == 404:
|
| 197 |
+
category = 'model_not_found'
|
| 198 |
+
elif code == 400:
|
| 199 |
+
category = 'invalid_request_or_schema'
|
| 200 |
+
elif code in (500, 502, 503, 504):
|
| 201 |
+
category = 'temporary_api_failure'
|
| 202 |
+
elif isinstance(exc, ValidationError):
|
| 203 |
+
category = 'generated_json_validation'
|
| 204 |
+
elif isinstance(exc, ValueError):
|
| 205 |
+
category = 'empty_or_invalid_json'
|
| 206 |
+
else:
|
| 207 |
+
category = 'sdk_or_network_error'
|
| 208 |
+
logger.error(
|
| 209 |
+
'origin.gemini_failed category=%s status=%s type=%s model=%s',
|
| 210 |
+
category, code if isinstance(code, int) else 'n/a',
|
| 211 |
+
type(exc).__name__, model_id,
|
| 212 |
+
)
|
| 213 |
+
# /search remains independent, and the real React frontend already
|
| 214 |
+
# preserves search results when /enhance fails.
|
| 215 |
+
raise gr.Error('Gemini analysis unavailable. Search results are still available.') from None
|
| 216 |
|
| 217 |
# Recreate response from vetted local CSV and validated generated fields.
|
| 218 |
ranks = [{'rank':i+1, 'project_id':pid} for i,pid in enumerate(ids)]
|