Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException, Security, Depends | |
| from fastapi.security import APIKeyHeader | |
| from pydantic import BaseModel, HttpUrl | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| import torch | |
| import json | |
| import re | |
| import random | |
| import os | |
| MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct" | |
| app = FastAPI() | |
| # ========== KONFIGURACJA API KEY ========== | |
| # Pobierz klucz z zmiennej środowiskowej (ustaw w Hugging Face Space) | |
| API_KEY = os.getenv("API_KEY", "twoj-domyslny-klucz-testowy") | |
| # Nagłówek do sprawdzania | |
| api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) | |
| async def validate_api_key(api_key: str = Security(api_key_header)): | |
| """Walidacja klucza API""" | |
| if api_key is None: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="Brak klucza API. Dodaj nagłówek 'X-API-Key'" | |
| ) | |
| if api_key != API_KEY: | |
| raise HTTPException( | |
| status_code=403, | |
| detail="Nieprawidłowy klucz API" | |
| ) | |
| return api_key | |
| # ========== LOADING MODEL ========== | |
| print("Loading model...") | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_NAME, | |
| torch_dtype=torch.float32, | |
| device_map="auto" | |
| ) | |
| print("Model loaded") | |
| # ========== ENDPOINTS Z OCHRONĄ ========== | |
| class GenerateRequest(BaseModel): | |
| prompt: str | |
| max_new_tokens: int = 120 | |
| def generate(req: GenerateRequest, api_key: str = Depends(validate_api_key)): | |
| inputs = tokenizer(req.prompt, return_tensors="pt") | |
| with torch.no_grad(): | |
| output = model.generate( | |
| **inputs, | |
| max_new_tokens=req.max_new_tokens, | |
| do_sample=True, | |
| temperature=0.7, | |
| top_p=0.9 | |
| ) | |
| text = tokenizer.decode(output[0], skip_special_tokens=True) | |
| return {"response": text} | |
| # ---------- NEW ENDPOINT ---------- | |
| ALLOWED_DOMAINS = [ | |
| "allegro.pl", | |
| "olx.pl", | |
| "ebay.com", | |
| "www.ebay.com" | |
| ] | |
| class AnalyzeAuctionRequest(BaseModel): | |
| url: HttpUrl | |
| def analyze_auction( | |
| req: AnalyzeAuctionRequest, | |
| api_key: str = Depends(validate_api_key) | |
| ): | |
| url = str(req.url) | |
| if not any(domain in url for domain in ALLOWED_DOMAINS): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Obsługiwane są tylko linki z Allegro, OLX i eBay" | |
| ) | |
| prompt = f""" | |
| Jesteś ekspertem od antyków (monety, broń biała, ceramika). | |
| Twoim zadaniem jest OCENIĆ aukcję i PODJĄĆ JEDNĄ DECYZJĘ. | |
| Musisz wybrać DOKŁADNIE JEDEN status: | |
| - autentyk | |
| - replika | |
| - scam | |
| - nieznane | |
| NIE WOLNO zwracać list, opcji ani schematów. | |
| NIE WOLNO używać znaków | w wartościach. | |
| NIE WOLNO powtarzać przykładu. | |
| Zwróć WYŁĄCZNIE wypełniony JSON pomiędzy znacznikami <json></json>. | |
| <json> | |
| {{ | |
| "status": "autentyk", | |
| "description": "Konkretny opis decyzji, odnoszący się do realiów rynku antyków." | |
| }} | |
| </json> | |
| Link aukcji: | |
| {url} | |
| """ | |
| inputs = tokenizer(prompt, return_tensors="pt") | |
| with torch.no_grad(): | |
| output = model.generate( | |
| **inputs, | |
| max_new_tokens=250, | |
| temperature=0.2, | |
| do_sample=False | |
| ) | |
| text = tokenizer.decode(output[0], skip_special_tokens=True) | |
| # -------- JSON EXTRACTION -------- | |
| json_text = None | |
| # 1 Preferowane: <json>...</json> | |
| tag_match = re.search(r"<json>([\s\S]*?)</json>", text) | |
| if tag_match: | |
| json_text = tag_match.group(1) | |
| # 2 Fallback: pierwsze { ... } | |
| if not json_text: | |
| brace_match = re.search(r"\{[\s\S]*?\}", text) | |
| if brace_match: | |
| json_text = brace_match.group() | |
| # 3 Totalny fallback | |
| if not json_text: | |
| return { | |
| "status": "nieznane", | |
| "description": "Model nie zwrócił możliwej do odczytania struktury danych" | |
| } | |
| try: | |
| return json.loads(json_text) | |
| except Exception: | |
| return { | |
| "status": "nieznane", | |
| "description": "Nie udało się poprawnie sparsować odpowiedzi modelu" | |
| } | |
| #----------- DUMMY RESPONSE----------- | |
| def analyze_auction_dummy( | |
| req: AnalyzeAuctionRequest, | |
| api_key: str = Depends(validate_api_key) | |
| ): | |
| url = str(req.url) | |
| if not any(domain in url for domain in ALLOWED_DOMAINS): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Obsługiwane są tylko linki z Allegro, OLX i eBay" | |
| ) | |
| statuses = ["autentyk", "replika", "scam", "nieznane"] | |
| descriptions = { | |
| "autentyk": "Aukcja wygląda na zgodną z typowymi ofertami autentycznych antyków.", | |
| "replika": "Oferta może dotyczyć współczesnej repliki stylizowanej na antyk.", | |
| "scam": "Występują elementy, które mogą sugerować próbę oszustwa.", | |
| "nieznane": "Na podstawie dostępnych informacji nie można jednoznacznie ocenić aukcji." | |
| } | |
| status = random.choice(statuses) | |
| return { | |
| "status": status, | |
| "description": descriptions[status] | |
| } | |
| # ---------- HEALTH (BEZ OCHRONY) ---------- | |
| def health(): | |
| return {"status": "ok"} |