♻️ 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_PROJECT_ID=id_do_seu_projeto
GOOGLE_LOCATION=loc_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 # Para desenvolvimento local: PostgreSQL direto
@ -17,34 +18,30 @@ DB_PASSWORD=SUA_SENHA
DB_NAME=orquestrador_db 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 # 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 # Descomente e informe a chave apenas se usar Gemini
# GOOGLE_API_KEY=sua-chave-api-aqui # 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 FAKERAPI_BASE_URL=https://fakerapi.it/api/v2
MOCKAROO_API_KEY=sua-chave-mockaroo-aqui FAKERAPI_LOCALE=pt_BR
MOCKAROO_BASE_URL=https://api.mockaroo.com/api FAKERAPI_SEED=42
FAKERAPI_PRODUCTS_QUANTITY=50
# ============================================ FAKERAPI_PERSONS_QUANTITY=120
# CONFIGURAÇÕES DE COMPORTAMENTO
# ============================================
# Usar Mockaroo para escrita de dados (apenas testes)
USE_MOCKAROO_WRITES=false
# ============================================ # ============================================
# AMBIENTE E DEBUG # AMBIENTE E DEBUG
# ============================================ # ============================================
# Valores: development, staging, production # Valores: development, staging, production
ENVIRONMENT=development ENVIRONMENT=development
# DEBUG deve ser false em produção # DEBUG deve ser false em producao
DEBUG=true 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 | | **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 | | **IA/LLM** | Google Vertex AI | Plataforma de IA empresarial com Gemini 1.5 Pro |
| **Banco de Dados** | PostgreSQL | Banco relacional robusto para dados estruturados | | **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 | | **Containerização** | Docker | Isolamento e deploy consistente |
| **Orquestração** | Google Cloud Build | Pipeline automatizado de build e deploy | | **Orquestração** | Google Cloud Build | Pipeline automatizado de build e deploy |
| **Computação** | Google Cloud Run | Plataforma serverless escalável | | **Computação** | Google Cloud Run | Plataforma serverless escalável |
@ -71,7 +71,7 @@ Orquestrador/
│ │ ├── llm_service.py # Integração com Vertex AI / Gemini │ │ ├── llm_service.py # Integração com Vertex AI / Gemini
│ │ ├── tool_registry.py # Registro e descoberta de ferramentas │ │ ├── tool_registry.py # Registro e descoberta de ferramentas
│ │ ├── handlers.py # Handlers de execução de tools │ │ ├── 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/ │ ├── repositories/
│ │ └── tool_repository.py # Acesso a dados de ferramentas │ │ └── tool_repository.py # Acesso a dados de ferramentas

@ -4,6 +4,7 @@ from pydantic_settings import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):
google_project_id: str google_project_id: str
google_location: str = "us-central1" google_location: str = "us-central1"
vertex_model_name: str = "gemini-2.5-flash"
db_host: str db_host: str
db_port: int = 5432 db_port: int = 5432
@ -11,9 +12,11 @@ class Settings(BaseSettings):
db_password: str db_password: str
db_name: str db_name: str
mockaroo_api_key: str fakerapi_base_url: str = "https://fakerapi.it/api/v2"
mockaroo_base_url: str = "https://api.mockaroo.com/api" fakerapi_locale: str = "pt_BR"
use_mockaroo_writes: bool = False fakerapi_seed: int = 42
fakerapi_products_quantity: int = 50
fakerapi_persons_quantity: int = 120
environment: str = "production" environment: str = "production"
debug: bool = False 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 from typing import Optional, List, Dict, Any
import re
import uuid
from datetime import datetime 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.core.settings import settings
from app.services.fakerapi_client import FakerApiClient
def normalize_cpf(value: str) -> str: def normalize_cpf(value: str) -> str:
return re.sub(r"\D", "", value or "") 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]]: async def consultar_estoque(preco_max: float, categoria: Optional[str] = None) -> List[Dict[str, Any]]:
client = MockarooClient() raw = await _fetch_faker_products(settings.fakerapi_products_quantity)
registros = await client.fetch_schema_data("consultar_estoque", count=200) 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 [ return [
r for r in registros 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]: 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) 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) cliente = next((r for r in registros if normalize_cpf(r.get("cpf", "")) == cpf_norm), None)
if not cliente: if not cliente:
return { entropy = _stable_int(f"{cpf_norm}:{settings.fakerapi_seed}")
"aprovado": False, cliente = {
"motivo": "Cliente não encontrado",
"cpf": cpf_norm, "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)) restricao = bool(cliente.get("possui_restricao", False))
aprovado = (not restricao) and (valor_veiculo <= limite) aprovado = (not restricao) and (valor_veiculo <= limite)
return { 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]: async def agendar_revisao(placa: str, data_hora: str) -> Dict[str, Any]:
if settings.use_mockaroo_writes: raise HTTPException(
client = MockarooClient() status_code=503,
try: detail="FakerAPI nao suporta escrita/persistencia. Endpoint disponivel apenas para leitura de dados ficticios.",
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"}
async def cancelar_pedido(numero_pedido: str, motivo: str) -> Dict[str, Any]: async def cancelar_pedido(numero_pedido: str, motivo: str) -> Dict[str, Any]:
client = MockarooClient() raise HTTPException(
registros = await client.fetch_schema_data("pedidos", count=500) status_code=503,
pedido = next((r for r in registros if str(r.get("numero_pedido")) == str(numero_pedido)), None) detail="FakerAPI nao suporta cancelamento persistente de pedidos. Endpoint indisponivel neste modo.",
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}

@ -1,5 +1,6 @@
from typing import Dict, Any, List from typing import Dict, Any, List, Optional
import vertexai import vertexai
from google.api_core.exceptions import NotFound
from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration
from app.core.settings import settings from app.core.settings import settings
from app.models.tool_model import ToolDefinition from app.models.tool_model import ToolDefinition
@ -13,27 +14,26 @@ class LLMService:
location=settings.google_location location=settings.google_location
) )
self.model = GenerativeModel("gemini-1.5-flash") configured = settings.vertex_model_name.strip()
fallback_models = ["gemini-2.5-flash", "gemini-2.0-flash-001", "gemini-1.5-pro"]
def build_vertex_tools(self, tools: List[ToolDefinition]): # Converte as Tools internas (ToolDefinition) para o formato que o Vertex AI entende. self.model_names = [configured] + [m for m in fallback_models if m != configured]
vertex_tools = [] 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 cada Tool registrada no sistema (depende da proposta do cliente) criamos uma Tool do Vertex AI # para uso de múltiplas funções no mesmo request.
for tool in tools: if not tools:
vertex_tools.append( return None
Tool(
function_declarations=[ function_declarations = [
FunctionDeclaration( FunctionDeclaration(
name=tool.name, name=tool.name,
description=tool.description, description=tool.description,
parameters=tool.parameters parameters=tool.parameters,
)
]
)
) )
for tool in tools
]
return vertex_tools return [Tool(function_declarations=function_declarations)]
""" """
Fluxo principal de geração de resposta. Fluxo principal de geração de resposta.
@ -55,14 +55,26 @@ class LLMService:
# Inicia uma sessão de chat com: # Inicia uma sessão de chat com:
# - histórico (se existir) # - histórico (se existir)
# - ferramentas disponíveis # - ferramentas disponíveis
chat = self.model.start_chat( response = None
history=history or [] last_error = None
)
for model_name in self.model_names:
response = chat.send_message( try:
message, model = GenerativeModel(model_name)
tools=vertex_tools 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) # Pegamos a primeira resposta candidata do modelo (a com maior coerência com o assunto)
# Estrutura interna: # 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.llm = LLMService()
self.registry = ToolRegistry(db) 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: Parametros:
- message: texto enviado pelo usuário - message: texto enviado pelo usuario
- user_id: identificador do usuário (ainda não está sendo usado aqui, - user_id: identificador do usuario (ainda nao esta sendo usado aqui,
mas futuramente servirá para histórico) mas futuramente servira para historico)
""" """
async def handle_message(self, message: str) -> str: async def handle_message(self, message: str) -> str:
@ -25,24 +24,23 @@ class OrquestradorService:
llm_result = await self.llm.generate_response( llm_result = await self.llm.generate_response(
message=message, message=message,
tools=tools tools=tools,
) )
if llm_result["tool_call"]: if llm_result["tool_call"]:
tool_name = llm_result["tool_call"]["name"]
tool_name = llm_result["tool_call"]["name"] # Nome da função que o Gemini quer executar arguments = llm_result["tool_call"]["arguments"]
arguments = llm_result["tool_call"]["arguments"] # Argumentos extraídos da mensagem do usuário
tool_result = await self.registry.execute(tool_name, arguments) tool_result = await self.registry.execute(tool_name, arguments)
# Segunda rodada para formatar resposta # Segunda rodada para formatar resposta
final_response = await self.llm.generate_response( final_response = await self.llm.generate_response(
message=f"Resultado da função {tool_name}: {tool_result}", message=f"Resultado da funcao {tool_name}: {tool_result}",
tools=tools tools=tools,
) )
return final_response["response"] 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. # significa que ele respondeu diretamente em texto.
return llm_result["response"] return llm_result["response"]

Loading…
Cancel
Save