♻️ refactor: Migrando a integração de dados fictícios para FakerAPI e ajustando a chamada das tools no Vertex AI.

main
parent 37fa127a80
commit e68b32a177

@ -1,12 +1,13 @@
# ============================================
# CONFIGURAÇÕES DO GOOGLE CLOUD
# CONFIGURACOES DO GOOGLE CLOUD
# ============================================
GOOGLE_PROJECT_ID=id_do_seu_projeto
GOOGLE_LOCATION=loc_do_seu_projeto
VERTEX_MODEL_NAME=gemini-2.5-flash
# ============================================
# CONFIGURAÇÕES DO BANCO DE DADOS (LOCAL)
# CONFIGURACOES DO BANCO DE DADOS (LOCAL)
# ============================================
# Para desenvolvimento local: PostgreSQL direto
@ -17,34 +18,30 @@ DB_PASSWORD=SUA_SENHA
DB_NAME=orquestrador_db
# ============================================
# CONFIGURAÇÕES DO BANCO DE DADOS (CLOUD SQL - PRODUÇÃO)
# CONFIGURACOES DO BANCO DE DADOS (CLOUD SQL - PRODUCAO)
# ============================================
# Comentado até fazer deploy. Descomente em produção.
# Comentado ate fazer deploy. Descomente em producao.
# CLOUD_SQL_CONNECTION_NAME=optimum-tensor-343619:us-central1:orquestrador-db
# ============================================
# CONFIGURAÇÕES DE API - GOOGLE GENERATIVE AI (Gemini)
# CONFIGURACOES DE API - GOOGLE GENERATIVE AI (Gemini)
# ============================================
# Descomente e informe a chave apenas se usar Gemini
# GOOGLE_API_KEY=sua-chave-api-aqui
# ============================================
# CONFIGURAÇÕES DE API - MOCKAROO (Dados fictícios)
# CONFIGURACOES DE API - FAKERAPI (Dados ficticios)
# ============================================
# Obrigatório: Forneça sua chave se usar Mockaroo para dados de teste
MOCKAROO_API_KEY=sua-chave-mockaroo-aqui
MOCKAROO_BASE_URL=https://api.mockaroo.com/api
# ============================================
# CONFIGURAÇÕES DE COMPORTAMENTO
# ============================================
# Usar Mockaroo para escrita de dados (apenas testes)
USE_MOCKAROO_WRITES=false
FAKERAPI_BASE_URL=https://fakerapi.it/api/v2
FAKERAPI_LOCALE=pt_BR
FAKERAPI_SEED=42
FAKERAPI_PRODUCTS_QUANTITY=50
FAKERAPI_PERSONS_QUANTITY=120
# ============================================
# AMBIENTE E DEBUG
# ============================================
# Valores: development, staging, production
ENVIRONMENT=development
# DEBUG deve ser false em produção
# DEBUG deve ser false em producao
DEBUG=true

@ -37,7 +37,7 @@ Retorna: "Encontrei 5 veículos sedans disponíveis até R$ 50.000..."
| **Backend** | FastAPI | Framework web moderno e rápido para APIs Python |
| **IA/LLM** | Google Vertex AI | Plataforma de IA empresarial com Gemini 1.5 Pro |
| **Banco de Dados** | PostgreSQL | Banco relacional robusto para dados estruturados |
| **Dados de Teste** | Mockaroo | Geração de dados fictícios para simulação |
| **Dados de Teste** | FakerAPI | Geração de dados fictícios para simulação |
| **Containerização** | Docker | Isolamento e deploy consistente |
| **Orquestração** | Google Cloud Build | Pipeline automatizado de build e deploy |
| **Computação** | Google Cloud Run | Plataforma serverless escalável |
@ -71,7 +71,7 @@ Orquestrador/
│ │ ├── llm_service.py # Integração com Vertex AI / Gemini
│ │ ├── tool_registry.py # Registro e descoberta de ferramentas
│ │ ├── handlers.py # Handlers de execução de tools
│ │ └── mockaroo_client.py # Cliente para gerar dados fictícios
│ │ └── fakerapi_client.py # Cliente para gerar dados fictícios
│ │
│ ├── repositories/
│ │ └── tool_repository.py # Acesso a dados de ferramentas

@ -4,6 +4,7 @@ from pydantic_settings import BaseSettings
class Settings(BaseSettings):
google_project_id: str
google_location: str = "us-central1"
vertex_model_name: str = "gemini-2.5-flash"
db_host: str
db_port: int = 5432
@ -11,9 +12,11 @@ class Settings(BaseSettings):
db_password: str
db_name: str
mockaroo_api_key: str
mockaroo_base_url: str = "https://api.mockaroo.com/api"
use_mockaroo_writes: bool = False
fakerapi_base_url: str = "https://fakerapi.it/api/v2"
fakerapi_locale: str = "pt_BR"
fakerapi_seed: int = 42
fakerapi_products_quantity: int = 50
fakerapi_persons_quantity: int = 120
environment: str = "production"
debug: bool = False

@ -0,0 +1,57 @@
from typing import Any, Dict, List, Optional
import httpx
from app.core.settings import settings
class FakerApiClient:
def __init__(
self,
base_url: Optional[str] = None,
locale: Optional[str] = None,
seed: Optional[int] = None,
):
self.base_url = (base_url or settings.fakerapi_base_url).rstrip("/")
self.locale = locale or settings.fakerapi_locale
self.seed = settings.fakerapi_seed if seed is None else seed
async def fetch_resource(
self,
resource: str,
quantity: int,
extra_params: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, Any]]:
url = f"{self.base_url}/{resource.lstrip('/')}"
params: Dict[str, Any] = {
"_quantity": quantity,
"_locale": self.locale,
"_seed": self.seed,
}
if extra_params:
params.update(extra_params)
timeout = httpx.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0)
headers = {
"Accept": "application/json",
"User-Agent": "orquestrador-fakerapi-client/1.0",
}
async with httpx.AsyncClient(timeout=timeout, headers=headers) as client:
try:
response = await client.get(url, params=params)
response.raise_for_status()
payload = response.json()
except httpx.ReadTimeout:
# Retry once with smaller payload to reduce timeout risk in free/public APIs.
reduced_quantity = min(quantity, 20)
retry_params = dict(params)
retry_params["_quantity"] = reduced_quantity
response = await client.get(url, params=retry_params)
response.raise_for_status()
payload = response.json()
if isinstance(payload, dict) and isinstance(payload.get("data"), list):
return payload["data"]
if isinstance(payload, list):
return payload
return []

@ -1,38 +1,159 @@
from typing import Optional, List, Dict, Any
import re
import uuid
from datetime import datetime
import hashlib
import re
import httpx
from fastapi import HTTPException
from app.services.mockaroo_client import MockarooClient
from app.core.settings import settings
from app.services.fakerapi_client import FakerApiClient
def normalize_cpf(value: str) -> str:
return re.sub(r"\D", "", value or "")
def _parse_float(value: Any, default: float = 0.0) -> float:
if value is None:
return default
if isinstance(value, (int, float)):
return float(value)
text = str(value).replace("R$", "").replace(" ", "")
text = text.replace(".", "").replace(",", ".") if "," in text else text
try:
return float(text)
except Exception:
return default
def _stable_int(seed_text: str) -> int:
digest = hashlib.sha256(seed_text.encode("utf-8")).hexdigest()
return int(digest[:16], 16)
def _cpf_from_any(value: Any) -> str:
as_int = _stable_int(str(value)) % (10**11)
return str(as_int).zfill(11)
async def _fetch_faker_products(count: int) -> List[Dict[str, Any]]:
client = FakerApiClient()
try:
return await client.fetch_resource("products", quantity=count)
except httpx.HTTPStatusError as exc:
status_code = exc.response.status_code if exc.response is not None else 502
request_url = str(exc.request.url) if exc.request is not None else "desconhecida"
raise HTTPException(
status_code=502,
detail=f"FakerAPI retornou HTTP {status_code} em '{request_url}'.",
)
except httpx.RequestError as exc:
raise HTTPException(
status_code=502,
detail=(
"Falha de rede ao acessar FakerAPI (products). "
f"{exc.__class__.__name__}: {exc}. "
"Verifique egress/NAT do Cloud Run e resolucao DNS."
),
)
except Exception:
raise HTTPException(
status_code=502,
detail="Falha de integracao com FakerAPI ao consultar products.",
)
async def _fetch_faker_persons(count: int) -> List[Dict[str, Any]]:
client = FakerApiClient()
try:
return await client.fetch_resource("persons", quantity=count)
except httpx.HTTPStatusError as exc:
status_code = exc.response.status_code if exc.response is not None else 502
request_url = str(exc.request.url) if exc.request is not None else "desconhecida"
raise HTTPException(
status_code=502,
detail=f"FakerAPI retornou HTTP {status_code} em '{request_url}'.",
)
except httpx.RequestError as exc:
raise HTTPException(
status_code=502,
detail=(
"Falha de rede ao acessar FakerAPI (persons). "
f"{exc.__class__.__name__}: {exc}. "
"Verifique egress/NAT do Cloud Run e resolucao DNS."
),
)
except Exception:
raise HTTPException(
status_code=502,
detail="Falha de integracao com FakerAPI ao consultar persons.",
)
async def consultar_estoque(preco_max: float, categoria: Optional[str] = None) -> List[Dict[str, Any]]:
client = MockarooClient()
registros = await client.fetch_schema_data("consultar_estoque", count=200)
raw = await _fetch_faker_products(settings.fakerapi_products_quantity)
registros: List[Dict[str, Any]] = []
for item in raw:
categories = item.get("categories")
if isinstance(categories, list) and categories:
category_value = str(categories[0])
else:
category_value = str(item.get("category") or "geral")
registro = {
"id": item.get("id"),
"modelo": item.get("name") or item.get("title") or "Veiculo",
"categoria": category_value.lower(),
"preco": _parse_float(item.get("price"), 0.0),
}
registros.append(registro)
categoria_norm = categoria.lower() if categoria else None
return [
r for r in registros
if float(r.get("preco", 0)) <= preco_max and (categoria is None or r.get("categoria") == categoria)
if _parse_float(r.get("preco"), 0.0) <= preco_max
and (categoria_norm is None or str(r.get("categoria", "")).lower() == categoria_norm)
]
async def validar_cliente_venda(cpf: str, valor_veiculo: float) -> Dict[str, Any]:
client = MockarooClient()
registros = await client.fetch_schema_data("clientes_credito", count=500)
cpf_norm = normalize_cpf(cpf)
raw = await _fetch_faker_persons(settings.fakerapi_persons_quantity)
registros: List[Dict[str, Any]] = []
for item in raw:
person_id = item.get("id") or item.get("email") or item.get("firstname")
generated_cpf = _cpf_from_any(person_id)
entropy = _stable_int(f"{generated_cpf}:{settings.fakerapi_seed}")
limite = float(30000 + (entropy % 150000))
score = int(300 + (entropy % 550))
possui_restricao = (entropy % 7 == 0)
nome = f"{item.get('firstname', '')} {item.get('lastname', '')}".strip() or "Cliente"
registros.append(
{
"cpf": generated_cpf,
"nome": nome,
"score": score,
"limite_credito": limite,
"possui_restricao": possui_restricao,
}
)
cliente = next((r for r in registros if normalize_cpf(r.get("cpf", "")) == cpf_norm), None)
if not cliente:
return {
"aprovado": False,
"motivo": "Cliente não encontrado",
entropy = _stable_int(f"{cpf_norm}:{settings.fakerapi_seed}")
cliente = {
"cpf": cpf_norm,
"valor_veiculo": valor_veiculo,
"nome": "Cliente Faker",
"score": int(300 + (entropy % 550)),
"limite_credito": float(30000 + (entropy % 150000)),
"possui_restricao": (entropy % 7 == 0),
}
limite = float(cliente.get("limite_credito", 0))
limite = _parse_float(cliente.get("limite_credito", 0), 0.0)
restricao = bool(cliente.get("possui_restricao", False))
aprovado = (not restricao) and (valor_veiculo <= limite)
return {
@ -61,32 +182,14 @@ async def avaliar_veiculo_troca(modelo: str, ano: int, km: int) -> Dict[str, Any
async def agendar_revisao(placa: str, data_hora: str) -> Dict[str, Any]:
if settings.use_mockaroo_writes:
client = MockarooClient()
try:
return await client.post_json(
"agendamentos_revisao",
{"placa": placa, "data_hora": data_hora, "status": "Agendado"},
)
except Exception:
pass
agendamento_id = str(uuid.uuid4())
return {"id": agendamento_id, "placa": placa, "data_hora": data_hora, "status": "Agendado"}
raise HTTPException(
status_code=503,
detail="FakerAPI nao suporta escrita/persistencia. Endpoint disponivel apenas para leitura de dados ficticios.",
)
async def cancelar_pedido(numero_pedido: str, motivo: str) -> Dict[str, Any]:
client = MockarooClient()
registros = await client.fetch_schema_data("pedidos", count=500)
pedido = next((r for r in registros if str(r.get("numero_pedido")) == str(numero_pedido)), None)
if not pedido:
return {
"status": "NaoEncontrado",
"numero_pedido": numero_pedido,
"motivo": motivo,
}
if settings.use_mockaroo_writes:
try:
await client.delete_json("pedidos", {"numero_pedido": numero_pedido, "motivo": motivo})
except Exception:
pass
return {"status": "Cancelado", "numero_pedido": numero_pedido, "motivo": motivo, "pedido": pedido}
raise HTTPException(
status_code=503,
detail="FakerAPI nao suporta cancelamento persistente de pedidos. Endpoint indisponivel neste modo.",
)

@ -1,5 +1,6 @@
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
import vertexai
from google.api_core.exceptions import NotFound
from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration
from app.core.settings import settings
from app.models.tool_model import ToolDefinition
@ -13,27 +14,26 @@ class LLMService:
location=settings.google_location
)
self.model = GenerativeModel("gemini-1.5-flash")
def build_vertex_tools(self, tools: List[ToolDefinition]): # Converte as Tools internas (ToolDefinition) para o formato que o Vertex AI entende.
vertex_tools = []
# Para cada Tool registrada no sistema (depende da proposta do cliente) criamos uma Tool do Vertex AI
for tool in tools:
vertex_tools.append(
Tool(
function_declarations=[
FunctionDeclaration(
name=tool.name,
description=tool.description,
parameters=tool.parameters
)
]
)
configured = settings.vertex_model_name.strip()
fallback_models = ["gemini-2.5-flash", "gemini-2.0-flash-001", "gemini-1.5-pro"]
self.model_names = [configured] + [m for m in fallback_models if m != configured]
def build_vertex_tools(self, tools: List[ToolDefinition]) -> Optional[List[Tool]]:
# Vertex espera uma lista de Tool, mas com function_declarations agrupadas em um único Tool
# para uso de múltiplas funções no mesmo request.
if not tools:
return None
function_declarations = [
FunctionDeclaration(
name=tool.name,
description=tool.description,
parameters=tool.parameters,
)
for tool in tools
]
return vertex_tools
return [Tool(function_declarations=function_declarations)]
"""
Fluxo principal de geração de resposta.
@ -55,14 +55,26 @@ class LLMService:
# Inicia uma sessão de chat com:
# - histórico (se existir)
# - ferramentas disponíveis
chat = self.model.start_chat(
history=history or []
)
response = chat.send_message(
message,
tools=vertex_tools
)
response = None
last_error = None
for model_name in self.model_names:
try:
model = GenerativeModel(model_name)
chat = model.start_chat(history=history or [])
send_kwargs = {"tools": vertex_tools} if vertex_tools else {}
response = chat.send_message(message, **send_kwargs)
break
except NotFound as err:
last_error = err
continue
if response is None:
if last_error:
raise RuntimeError(
f"Nenhum modelo Vertex disponível. Verifique VERTEX_MODEL_NAME e acesso no projeto. Erro: {last_error}"
) from last_error
raise RuntimeError("Falha ao gerar resposta no Vertex AI.")
# Pegamos a primeira resposta candidata do modelo (a com maior coerência com o assunto)
# Estrutura interna:

@ -1,60 +0,0 @@
from typing import Any, Dict, List, Optional
import httpx
from app.core.settings import settings
class MockarooClient:
def __init__(
self,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
):
self.api_key = api_key or settings.mockaroo_api_key
self.base_url = (base_url or settings.mockaroo_base_url).rstrip("/")
async def fetch_schema_data(
self,
schema_name: str,
count: int = 100,
extra_params: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, Any]]:
url = f"{self.base_url}/{schema_name}"
params: Dict[str, Any] = {
"key": self.api_key,
"count": count,
}
if extra_params:
params.update(extra_params)
async with httpx.AsyncClient() as client:
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
if isinstance(data, list):
return data
return [data]
async def post_json(self, route: str, payload: Dict[str, Any]) -> Dict[str, Any]:
url = f"{self.base_url}/{route}"
params = {"key": self.api_key}
async with httpx.AsyncClient() as client:
response = await client.post(url, params=params, json=payload)
response.raise_for_status()
return response.json()
async def put_json(self, route: str, payload: Dict[str, Any]) -> Dict[str, Any]:
url = f"{self.base_url}/{route}"
params = {"key": self.api_key}
async with httpx.AsyncClient() as client:
response = await client.put(url, params=params, json=payload)
response.raise_for_status()
return response.json()
async def delete_json(self, route: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = f"{self.base_url}/{route}"
params = {"key": self.api_key}
async with httpx.AsyncClient() as client:
response = await client.delete(url, params=params, json=payload or {})
response.raise_for_status()
return response.json()

@ -10,14 +10,13 @@ class OrquestradorService:
self.llm = LLMService()
self.registry = ToolRegistry(db)
"""
Método principal chamado quando o usuário envia uma mensagem.
Metodo principal chamado quando o usuario envia uma mensagem.
Parâmetros:
- message: texto enviado pelo usuário
- user_id: identificador do usuário (ainda não está sendo usado aqui,
mas futuramente servirá para histórico)
Parametros:
- message: texto enviado pelo usuario
- user_id: identificador do usuario (ainda nao esta sendo usado aqui,
mas futuramente servira para historico)
"""
async def handle_message(self, message: str) -> str:
@ -25,24 +24,23 @@ class OrquestradorService:
llm_result = await self.llm.generate_response(
message=message,
tools=tools
tools=tools,
)
if llm_result["tool_call"]:
tool_name = llm_result["tool_call"]["name"] # Nome da função que o Gemini quer executar
arguments = llm_result["tool_call"]["arguments"] # Argumentos extraídos da mensagem do usuário
tool_name = llm_result["tool_call"]["name"]
arguments = llm_result["tool_call"]["arguments"]
tool_result = await self.registry.execute(tool_name, arguments)
# Segunda rodada para formatar resposta
final_response = await self.llm.generate_response(
message=f"Resultado da função {tool_name}: {tool_result}",
tools=tools
message=f"Resultado da funcao {tool_name}: {tool_result}",
tools=tools,
)
return final_response["response"]
# Se o modelo não chamou nenhuma tool,
# Se o modelo nao chamou nenhuma tool,
# significa que ele respondeu diretamente em texto.
return llm_result["response"]

Loading…
Cancel
Save