openTARS / app /nucleo /anexos.py
ProjectOpenTARS4's picture
Mirror of openTARS 5.2 Ranger (revision 2026110101)
2695555 verified
Raw History Blame Contribute Delete
3.93 kB
# openTARS — nucleo/anexos.py
# 4.5 Endurance: arquivos anexados ao pedido (botão 📎 da janela, /anexar e
# @arquivo no terminal). A leitura de cada arquivo está em tars_anexos.py; aqui
# fica o encaixe no pedido: o conteúdo entra na mensagem, a imagem vai pra um
# modelo que enxerga (ou é descrita por um modelo de visão se o escolhido não
# enxerga) e o contexto da IA sobe se os anexos forem grandes.
#
# Parte do núcleo: o tars.py executa este arquivo DENTRO do próprio
# namespace (ver _carregar_nucleo). Vem depois de "chat".
# flake8: noqa
# ============================================================
# ANEXOS
# ============================================================
TEXTO_PEDIDO_SO_ANEXOS = "Look at the attached file(s) and tell me what they contain."
TOKENS_PROMPT_BASE = 5500 # prompt do openTARS + ferramentas + folga pra resposta e histórico
TOKENS_POR_IMAGEM = 1500
CHARS_POR_TOKEN = 3
CONTEXTO_MAXIMO_ANEXOS = int(os.environ.get("TARS_CONTEXTO_MAXIMO", "0") or 0)
_DEGRAUS_CONTEXTO = (8192, 12288, 16384, 24576, 32768, 49152, 65536)
def contexto_maximo_para_anexos():
"""Até onde o contexto pode subir por causa dos anexos: 16 mil tokens
(32 mil com GPU de 16 GB ou mais), ou TARS_CONTEXTO_MAXIMO."""
if CONTEXTO_MAXIMO_ANEXOS:
return CONTEXTO_MAXIMO_ANEXOS
try:
gpu = detectar_gpu()
except Exception:
gpu = None
return 32768 if gpu and gpu["vram_total_gb"] >= 16 else 16384
def orcamento_de_texto_dos_anexos():
"""Quantos caracteres de anexos cabem num pedido (o que o contexto máximo deixa livre)."""
sobra = contexto_maximo_para_anexos() - TOKENS_PROMPT_BASE
return max(6000, min(80000, sobra * CHARS_POR_TOKEN))
def _garantir_contexto(tokens):
"""Sobe o num_ctx (até o máximo) pra caber 'tokens' de anexos; não desce."""
precisa = TOKENS_PROMPT_BASE + int(tokens)
teto = contexto_maximo_para_anexos()
alvo = next((d for d in _DEGRAUS_CONTEXTO if d >= precisa), _DEGRAUS_CONTEXTO[-1])
alvo = min(alvo, teto)
if alvo > _contexto_minimo["n"] and alvo > 8192:
_contexto_minimo["n"] = alvo
def _descrever_imagem_anexada(anexo, pedido, modelo_atual):
"""Um modelo que não enxerga: o modelo de visão (se instalado) descreve a imagem em texto. Sem ele, só o aviso."""
try:
descricao, _tag = descrever_com_visao(anexo.imagem_b64, pedido)
except Exception:
descricao = None
if descricao:
print(c(f"[openTARS] {t('msg.descrevendo_tela', modelo=modelo_atual, visao=_tag)}", Cor.AMARELO))
return descricao
def montar_mensagem_com_anexos(texto, anexos, modelo):
"""(conteúdo, imagens) da mensagem do usuário com os anexos. Imagens vão
como imagem se o modelo enxerga; senão viram texto (descritas por um
modelo de visão) ou, sem nenhum, só o nome."""
texto = (texto or "").strip() or TEXTO_PEDIDO_SO_ANEXOS
imagens, descricoes = [], {}
enxerga = modelo_enxerga(modelo)
for a in anexos:
if a.tipo != "imagem":
continue
if enxerga:
imagens.append(a.imagem_b64)
continue
descricao = _descrever_imagem_anexada(a, texto, modelo)
if descricao:
descricoes[a.caminho] = descricao
else:
a.avisos.append("you cannot see images and no vision model is installed (ollama pull gemma3:4b "
"or qwen3-vl:2b), so tell the user you can't see this image")
limite = orcamento_de_texto_dos_anexos()
bloco = anexos_lib.montar_bloco(anexos, descricoes, limite_total=limite)
_garantir_contexto(len(bloco) / CHARS_POR_TOKEN + TOKENS_POR_IMAGEM * len(imagens))
return f"{texto}\n\n{bloco}", imagens
def anexar_arquivos(entradas, ja_anexados=()):
"""([Anexo], [erros]) — pro terminal e pra janela."""
return anexos_lib.carregar_varios(entradas, ja_anexados=ja_anexados)