Download app/nucleo/nuvem.py from OpenTARS/openTARS: direct link, hf CLI and curl.
- Browser
- Download file 6.66 kB
-
https://huggingface.co/OpenTARS/openTARS/resolve/main/app/nucleo/nuvem.py
- Command line
-
hf download hf://OpenTARS/openTARS/app/nucleo/nuvem.py
-
curl -L -o nuvem.py https://huggingface.co/OpenTARS/openTARS/resolve/main/app/nucleo/nuvem.py
6.66 kB
| # openTARS — nucleo/nuvem.py | |
| # 5.0 Tesseract: nuvem opcional (qualquer servidor compatível com a API da OpenAI: | |
| # Ollama Cloud, Groq, Cerebras, outro). A parte da API (formato da conversa, streaming) | |
| # fica em tars_openai.py; o endereço e a chave, em tars_primeiro_uso.py (nuvem.json, 0600). | |
| # | |
| # Regras de privacidade: | |
| # - desligada por padrão; só vale se a pessoa ligou e pôs a própria chave; | |
| # - modelo da nuvem NUNCA entra na escolha automática (nem no catálogo): só quando a | |
| # pessoa o escolhe na mão ("api:<nome>"). O que ela escreve só sai do computador | |
| # se ela escolheu isso. | |
| # Parte do núcleo: executada dentro do namespace do tars.py (ver _carregar_nucleo). | |
| # flake8: noqa | |
| _cache_modelos_nuvem = {"dados": None, "ts": 0.0, "url": None} | |
| def nuvem_config(): | |
| """{'ativa', 'provedor', 'endereco', 'chave'}; a variável TARS_CHAVE_API vale mais que o arquivo.""" | |
| dados = primeiro_uso.carregar_nuvem() | |
| chave_ambiente = os.environ.get("TARS_CHAVE_API") | |
| if chave_ambiente: | |
| dados = dict(dados, chave=chave_ambiente) | |
| return dados | |
| def servidor_api(): | |
| """Endereço da nuvem em uso, ou '' (desligada ou sem endereço/chave).""" | |
| dados = nuvem_config() | |
| if not (dados["ativa"] and dados["endereco"] and dados["chave"]): | |
| return "" | |
| return dados["endereco"].rstrip("/") | |
| def _cabecalhos_api(): | |
| chave = nuvem_config()["chave"] | |
| return {"Authorization": f"Bearer {chave}"} if chave else {} | |
| def listar_modelos_api(forcar=False): | |
| """{'api:<nome>', ...} da nuvem (vazio se desligada ou fora do ar).""" | |
| url = servidor_api() | |
| if not url: | |
| return set() | |
| agora = time.time() | |
| cache = _cache_modelos_nuvem | |
| if not forcar and cache["url"] == url and cache["dados"] is not None and agora - cache["ts"] < CACHE_MODELOS_TTL: | |
| return cache["dados"] | |
| try: | |
| r = _SESSION.get(f"{url}/models", headers=_cabecalhos_api(), timeout=TIMEOUT_HTTP_CURTO) | |
| dados = {api.PREFIXO + i for i in api.ler_modelos(r.json())} if r.status_code == 200 else set() | |
| except Exception: | |
| dados = set() | |
| cache.update(dados=dados, ts=agora, url=url) | |
| return dados | |
| def _post_api(corpo, stream=False, timeout=TIMEOUT_HTTP_LONGO): | |
| return _SESSION.post(f"{servidor_api()}/chat/completions", json=corpo, headers=_cabecalhos_api(), | |
| timeout=timeout, stream=stream) | |
| def _chat_unico_api(modelo, prompt, temperatura, timeout, formato, imagens, antes): | |
| mensagens = list(antes or []) + [{"role": "user", "content": prompt, **({"images": imagens} if imagens else {})}] | |
| corpo = api.corpo_chat(modelo, mensagens, stream=False, temperatura=temperatura, formato=formato) | |
| try: | |
| r = _post_api(corpo, timeout=timeout) | |
| if r.status_code != 200 and "response_format" in corpo: # servidor sem JSON schema | |
| corpo.pop("response_format") | |
| r = _post_api(corpo, timeout=timeout) | |
| if r.status_code != 200: | |
| return None | |
| texto = api.resposta_unica(r.json()) | |
| except Exception: | |
| return None | |
| return re.sub(r"<think>.*?</think>", "", texto or "", flags=re.DOTALL).strip() if texto is not None else None | |
| def _turno_api(modelo, mensagens, tools, pensar=True): | |
| """Igual a _executar_turno_streaming, na nuvem: mesmos eventos na tela, mesma resposta | |
| (conteúdo, pensamento, chamadas no formato do Ollama).""" | |
| if not servidor_api(): | |
| return None, t("msg.nuvem_desligada") | |
| corpo = api.corpo_chat(modelo, mensagens, tools=tools, stream=True, temperatura=temperatura_conversa()) | |
| try: | |
| resposta = _post_api(corpo, stream=True) | |
| if resposta.status_code != 200 and tools and resposta.status_code in (400, 422): | |
| detalhe = resposta.text[:300] | |
| if "tool" in detalhe.lower(): | |
| resposta.close() | |
| _MODELOS_SEM_TOOLS.add(modelo) | |
| corpo.pop("tools", None) | |
| resposta = _post_api(corpo, stream=True) | |
| if resposta.status_code != 200: | |
| detalhe = resposta.text[:300].strip() | |
| codigo = resposta.status_code | |
| resposta.close() | |
| if codigo in (401, 403): | |
| return None, t("msg.nuvem_chave") | |
| if codigo == 429: | |
| return None, t("msg.nuvem_limite") | |
| return None, t("msg.erro_servidor_api", codigo=codigo, detalhe=detalhe) | |
| except requests.exceptions.ConnectionError: | |
| return None, t("msg.servidor_api_offline", url=servidor_api()) | |
| except Exception as e: | |
| return None, t("msg.erro_streaming", erro=e) | |
| leitor = api.LeitorStream() | |
| pensando_ativo = escrevendo_ativo = False | |
| _resposta_ativa["r"] = resposta | |
| try: | |
| for linha in resposta.iter_lines(): | |
| if _cancelar.is_set(): | |
| resposta.close() | |
| if pensando_ativo or escrevendo_ativo: | |
| emitir("fim_stream") | |
| raise RespostaCancelada() | |
| for tipo, valor in leitor.alimentar(linha): | |
| if tipo == "pensamento" and pensar: | |
| emitir("pensamento", texto=valor, inicio=not pensando_ativo) | |
| pensando_ativo = True | |
| elif tipo == "resposta": | |
| emitir("resposta", texto=valor, inicio=not escrevendo_ativo, apos_pensamento=pensando_ativo) | |
| escrevendo_ativo = True | |
| elif tipo == "erro": | |
| return None, t("msg.erro_servidor_api", codigo="stream", detalhe=valor) | |
| if leitor.terminou: | |
| break | |
| except RespostaCancelada: | |
| raise | |
| except Exception as e: | |
| if _cancelar.is_set(): | |
| if pensando_ativo or escrevendo_ativo: | |
| emitir("fim_stream") | |
| raise RespostaCancelada() | |
| return None, t("msg.erro_streaming", erro=e) | |
| finally: | |
| _resposta_ativa["r"] = None | |
| resposta.close() | |
| if pensando_ativo or escrevendo_ativo: | |
| emitir("fim_stream") | |
| if not (leitor.terminou or leitor.motivo): | |
| # a conexão acabou no meio: uma chamada de ferramenta cortada ({"app":"fir) viraria execução com argumentos vazios | |
| return None, t("msg.erro_streaming", erro="connection closed before the answer was complete") | |
| if leitor.motivo == "length": | |
| print(c(f"\n[openTARS] {t('msg.limite_contexto', tokens=contexto_ia())}", Cor.AMARELO)) | |
| conteudo = re.sub(r"<think>.*?</think>", "", "".join(leitor.conteudo), flags=re.DOTALL).strip() | |
| return {"content": conteudo, "thinking": "".join(leitor.pensamento).strip(), | |
| "tool_calls": leitor.chamadas}, None | |