"""Provedores de IA: cliente OpenAI-compatível e modo de ensaio determinístico.""" from __future__ import annotations import json import os import re import time import urllib.error import urllib.request SISTEMA = { "pt": ( "Você é um assistente técnico. Responda de forma direta e objetiva, no idioma da " "pergunta. Se a pergunta pedir uma alternativa, indique apenas a letra e o texto " "correspondente. Se pedir valor numérico, dê apenas o número e a unidade." ), "en": ( "You are a technical assistant. Answer directly and concisely, in the language of " "the question. If the question asks for an option, state only the letter and the " "matching text. If it asks for a number, give only the number and its unit." ), "zh": ( "你是一名技术助理。请用题目所用的语言直接、简洁地回答。若题目要求选择选项," "请只给出选项字母及对应内容;若要求数值,请只给出数字和单位。" ), } _PADROES_RECUSA = [ r"^\s*i (can'?t|cannot|won'?t)\b", r"^\s*i'?m unable to\b", r"^\s*desculpe,? n[ãa]o posso", r"^\s*n[ãa]o posso (ajudar|fornecer|responder)", r"^\s*sorry,? i (can'?t|cannot)\b", r"^\s*抱歉", r"^\s*我无法", r"^\s*我不能", ] def detectar_recusa(texto: str) -> bool: t = (texto or "").strip().lower() if len(t) < 200: for padrao in _PADROES_RECUSA: if re.search(padrao, t): return True return False class OpenAICompatProvider: """Cliente mínimo para APIs de chat compatíveis com OpenAI.""" def __init__(self, cfg: dict, aplicacao: dict): self.nome = cfg["nome"] self.model = cfg["model"] self.base_url = cfg["base_url"].rstrip("/") self.key_env = cfg.get("key_env", "") self.aplicacao = aplicacao @property def chave(self) -> str: return os.environ.get(self.key_env, "") def completa(self, system: str, user: str, lang: str) -> dict: corpo = { "model": self.model, "temperature": self.aplicacao["temperatura"], "max_tokens": self.aplicacao["max_tokens"], "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], } ultimo_erro: Exception | None = None for tentativa in range(1, self.aplicacao["tentativas"] + 1): inicio = time.perf_counter() try: req = urllib.request.Request( f"{self.base_url}/chat/completions", data=json.dumps(corpo).encode(), headers={ "Content-Type": "application/json", "Authorization": f"Bearer {self.chave}", }, method="POST", ) with urllib.request.urlopen(req, timeout=self.aplicacao["timeout_s"]) as resp: payload = json.loads(resp.read().decode()) latencia = (time.perf_counter() - inicio) * 1000 uso = payload.get("usage", {}) or {} texto = payload["choices"][0]["message"]["content"] return { "text": texto, "latency_ms": round(latencia, 1), "prompt_tokens": uso.get("prompt_tokens", 0), "completion_tokens": uso.get("completion_tokens", 0), "refused": detectar_recusa(texto), "raw_model": payload.get("model", self.model), } except (urllib.error.URLError, urllib.error.HTTPError, KeyError, ValueError) as exc: ultimo_erro = exc time.sleep(min(2 ** tentativa, 8)) raise RuntimeError(f"{self.nome}: falha após {self.aplicacao['tentativas']} tentativas: {ultimo_erro}")