import time try: from huggingface_hub import InferenceClient except ImportError: # pragma: no cover InferenceClient = None try: from openai import OpenAI except ImportError: # pragma: no cover OpenAI = None from config import ( HF_MODEL, HF_TOKEN, GROQ_API_KEY, GROQ_MODEL, OPENAI_API_KEY, OPENAI_MODEL, LLM_PROVIDER, MAX_NEW_TOKENS ) # Canlı demoda tek bir asılı isteğin arayüzü kilitlememesi için açık bir # istek aşımı. Önceden hiç tanımlı değildi (client kütüphanelerinin # varsayılanına bırakılmıştı) — bu, ağ sorunlarında belirsiz bekleme # sürelerine yol açabiliyordu. REQUEST_TIMEOUT_SECONDS = 25.0 HF_CLIENT = ( InferenceClient(model=HF_MODEL, token=HF_TOKEN, timeout=REQUEST_TIMEOUT_SECONDS) if HF_TOKEN and InferenceClient else None ) GROQ_CLIENT = ( OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1", timeout=REQUEST_TIMEOUT_SECONDS) if GROQ_API_KEY and OpenAI else None ) OPENAI_CLIENT = ( OpenAI(api_key=OPENAI_API_KEY, timeout=REQUEST_TIMEOUT_SECONDS) if OPENAI_API_KEY and OpenAI else None ) # Geçici ağ/oran-sınırı hatalarında tek seferlik yeniden deneme. Bu sayı # bilinçli olarak düşük tutuldu (1 deneme, kısa bekleme) — amaç kalıcı bir # sağlamlık katmanı değil, "bir kerelik kesinti yüzünden zengin LLM # cevabını kaybetme" riskini azaltmak. Kalıcı hatalarda (ör. geçersiz API # anahtarı) yine de yukarı fırlatılır, sessizce yutulmaz. RETRY_ATTEMPTS = 2 RETRY_BACKOFF_SECONDS = 1.5 def llm_available() -> bool: if LLM_PROVIDER == "auto": return any((HF_CLIENT, GROQ_CLIENT, OPENAI_CLIENT)) if LLM_PROVIDER == "groq": return GROQ_CLIENT is not None if LLM_PROVIDER == "openai": return OPENAI_CLIENT is not None return HF_CLIENT is not None def call_chat_completion(messages: list[dict], response_format: str = "text") -> str: """Call the configured chat completion provider. response_format: "text" (default) — no format constraint, unchanged behavior. "json" — for providers that support it (OpenAI, Groq's OpenAI- compatible API), enforce {"type": "json_object"} at the API level rather than relying solely on prompt wording. Callers that expect strict JSON (e.g. proof_bundle.llm_claim_prompt) should pass response_format="json". """ provider = LLM_PROVIDER if provider == "auto": provider = "huggingface" if HF_CLIENT is not None else "groq" if GROQ_CLIENT is not None else "openai" last_error: Exception | None = None for attempt in range(RETRY_ATTEMPTS): try: return _dispatch_chat_completion(provider, messages, response_format) except Exception as error: # noqa: BLE001 - deliberately broad, see docstring last_error = error if attempt < RETRY_ATTEMPTS - 1: time.sleep(RETRY_BACKOFF_SECONDS) continue assert last_error is not None raise last_error def _dispatch_chat_completion(provider: str, messages: list[dict], response_format: str) -> str: extra_kwargs = {} if response_format == "json" and provider in {"groq", "openai"}: extra_kwargs["response_format"] = {"type": "json_object"} if provider == "groq": if GROQ_CLIENT is None: raise RuntimeError("GROQ_API_KEY tanımlı değil.") response = GROQ_CLIENT.chat.completions.create( model=GROQ_MODEL, messages=messages, max_tokens=MAX_NEW_TOKENS, temperature=0.12, top_p=0.9, **extra_kwargs, ) return response.choices[0].message.content.strip() if provider == "openai": if OPENAI_CLIENT is None: raise RuntimeError("OPENAI_API_KEY tanımlı değil.") response = OPENAI_CLIENT.chat.completions.create( model=OPENAI_MODEL, messages=messages, max_tokens=MAX_NEW_TOKENS, temperature=0.0, top_p=0.9, **extra_kwargs, ) return response.choices[0].message.content.strip() if HF_CLIENT is None: raise RuntimeError("HF_TOKEN veya HUGGINGFACEHUB_API_TOKEN tanımlı değil.") response = HF_CLIENT.chat.completions.create( model=HF_MODEL, messages=messages, max_tokens=MAX_NEW_TOKENS, temperature=0.12, top_p=0.9, ) return response.choices[0].message.content.strip()