- CustomToolRouter como callback (roda em todas as chamadas) - ToolRouterGuardrail para ativação por request - Modos: jaccard (offline) e embedding (API) - Dockerfile + docker-compose prontos - README com docs completas - Teste offline validando ~74% redução de tokens
251 lines
8.3 KiB
Python
251 lines
8.3 KiB
Python
"""
|
|
Teste rápido do ToolRouter - simula chamadas pra provar a redução de tokens.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
import os
|
|
|
|
# Adiciona o diretório atual ao path
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from tool_router_logger import CustomToolRouter
|
|
|
|
|
|
# Simula tools como se fossem do hermes-agent
|
|
TOOLS_MOCK = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_time",
|
|
"description": "Obtém a hora atual em um fuso horário específico",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"timezone": {
|
|
"type": "string",
|
|
"description": "Fuso horário (ex: America/Sao_Paulo)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "search_web",
|
|
"description": "Pesquisa na web usando um mecanismo de busca. Retorna resultados de páginas web relevantes para a consulta.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string", "description": "Consulta de busca"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "read_file",
|
|
"description": "Lê o conteúdo de um arquivo no sistema de arquivos. Pode ler arquivos de texto e retornar seu conteúdo.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {"type": "string", "description": "Caminho do arquivo"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "write_file",
|
|
"description": "Escreve conteúdo em um arquivo no sistema de arquivos.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {"type": "string", "description": "Caminho do arquivo"},
|
|
"content": {"type": "string", "description": "Conteúdo"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "list_directory",
|
|
"description": "Lista arquivos e diretórios em um determinado caminho.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {"type": "string", "description": "Caminho"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "execute_bash",
|
|
"description": "Executa um comando bash no terminal.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {"type": "string", "description": "Comando bash"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "get_weather",
|
|
"description": "Obtém a previsão do tempo para uma cidade.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"city": {"type": "string", "description": "Nome da cidade"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "send_email",
|
|
"description": "Envia um email para um destinatário.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"to": {"type": "string", "description": "Destinatário"},
|
|
"subject": {"type": "string", "description": "Assunto"},
|
|
"body": {"type": "string", "description": "Corpo do email"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "calculate",
|
|
"description": "Realiza cálculos matemáticos complexos.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"expression": {"type": "string", "description": "Expressão"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "list_tools",
|
|
"description": "Lista todas as ferramentas disponíveis com suas descrições.",
|
|
"parameters": {"type": "object", "properties": {}}
|
|
}
|
|
},
|
|
]
|
|
|
|
|
|
async def test_tool_router():
|
|
"""Testa o router com diferentes mensagens."""
|
|
|
|
router = CustomToolRouter(
|
|
embedding_model="text-embedding-3-small", # modelo real via API
|
|
top_k=3,
|
|
min_similarity=0.15,
|
|
always_include_tools={"get_time", "help", "list_tools"},
|
|
similarity_mode="jaccard", # fallback local (sem precisar de API key)
|
|
)
|
|
|
|
test_cases = [
|
|
("👋 Oi, tudo bem?", "saudação simples - deve selecionar ~0 tools"),
|
|
("Que horas são em São Paulo?", "pergunta sobre hora"),
|
|
("Pesquise sobre IA generativa na web", "pesquisa web"),
|
|
("Leia o arquivo /tmp/test.txt pra mim", "leitura de arquivo"),
|
|
("Qual a previsão do tempo em SP?", "clima/weather"),
|
|
("Execute ls -la no terminal", "comando bash"),
|
|
("Me mande um email para teste@test.com", "envio de email"),
|
|
("Quanto é 2 + 2?", "cálculo"),
|
|
]
|
|
|
|
print("=" * 80)
|
|
print("🧪 TESTE DO TOOL ROUTER PARA REDUÇÃO DE TOKENS")
|
|
print("=" * 80)
|
|
print(f"\nTools disponíveis: {len(TOOLS_MOCK)}")
|
|
print(f"Always include: {router.always_include_tools}")
|
|
print(f"Modo: {router.similarity_mode}")
|
|
print(f"Top-K: {router.top_k}, Min similarity: {router.min_similarity}")
|
|
print()
|
|
|
|
total_original = 0
|
|
total_filtrado = 0
|
|
|
|
for msg, desc in test_cases:
|
|
print(f"\n{'─' * 70}")
|
|
print(f"📝 Teste: '{msg}'")
|
|
print(f"📌 {desc}")
|
|
|
|
# Simula o data dict que o proxy passa
|
|
data = {
|
|
"messages": [
|
|
{"role": "system", "content": "Você é um assistente útil."},
|
|
{"role": "user", "content": msg},
|
|
],
|
|
"tools": TOOLS_MOCK,
|
|
"model": "gpt-4o-mini",
|
|
}
|
|
|
|
tokens_original = len(json.dumps(data["tools"]))
|
|
|
|
# Executa o hook
|
|
result = await router.async_pre_call_hook(
|
|
user_api_key_dict=None, # mock
|
|
cache=None, # mock
|
|
data=data,
|
|
call_type="completion",
|
|
)
|
|
|
|
if isinstance(result, dict):
|
|
new_tools = result.get("tools", [])
|
|
else:
|
|
new_tools = TOOLS_MOCK
|
|
|
|
tokens_filtrado = len(json.dumps(new_tools))
|
|
reducao = (1 - tokens_filtrado / tokens_original) * 100 if tokens_original > 0 else 0
|
|
|
|
selected = [t.get("function", t).get("name", "") for t in new_tools]
|
|
|
|
total_original += tokens_original
|
|
total_filtrado += tokens_filtrado
|
|
|
|
print(f" Tools: {len(TOOLS_MOCK)} → {len(new_tools)}")
|
|
print(f" Selecionadas: {selected}")
|
|
print(f" Tokens (chars): {tokens_original} → {tokens_filtrado} (🔥 -{reducao:.1f}%)")
|
|
|
|
print(f"\n{'=' * 70}")
|
|
reducao_total = (1 - total_filtrado / total_original) * 100
|
|
print(f"📊 TOTAL GERAL: {total_original} → {total_filtrado} chars (🔥 -{reducao_total:.1f}%)")
|
|
print(f" Economia média por chamada: ~{total_original - total_filtrado} chars")
|
|
print(f"{'=' * 70}")
|
|
print()
|
|
|
|
# --- Cenário real: estimativa de tokens LLM ---
|
|
print("\n📈 ESTIMATIVA DE TOKENS REAIS (considerando modelo)")
|
|
print("~4 chars = 1 token em média para tools JSON")
|
|
|
|
tok_original = total_original // 4
|
|
tok_filtrado = total_filtrado // 4
|
|
economia_tokens = tok_original - tok_filtrado
|
|
|
|
print(f" Tokens originais (tools): ~{tok_original}")
|
|
print(f" Tokens filtrados (tools): ~{tok_filtrado}")
|
|
print(f" Economia por chamada: ~{economia_tokens} tokens")
|
|
print(f" Preço @ GPT-4o-mini: ${economia_tokens * 0.000000150:.6f}/chamada (só tools)")
|
|
print(f" Preço @ GPT-4o: ${economia_tokens * 0.0000025:.6f}/chamada (só tools)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(test_tool_router()) |