Compare commits

..

6 Commits

@ -7,9 +7,9 @@ GOOGLE_LOCATION=loc_do_seu_projeto
VERTEX_MODEL_NAME=gemini-2.5-flash VERTEX_MODEL_NAME=gemini-2.5-flash
# ============================================ # ============================================
# CONFIGURACOES DO BANCO DE DADOS (LOCAL) # CONFIGURACOES DO BANCO DE DADOS (POSTGRESQL - TOOLS)
# ============================================ # ============================================
# Para desenvolvimento local: PostgreSQL direto # Banco principal (tools)
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
@ -23,21 +23,25 @@ DB_NAME=orquestrador_db
# Comentado ate fazer deploy. Descomente em producao. # 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
# ============================================
# CONFIGURACOES DO BANCO DE DADOS MOCK (MYSQL - DADOS FICTICIOS)
# ============================================
MOCK_DB_HOST=127.0.0.1
MOCK_DB_PORT=3306
MOCK_DB_USER=root
MOCK_DB_PASSWORD=SUA_SENHA
MOCK_DB_NAME=orquestrador_mock
# MOCK_DB_CLOUD_SQL_CONNECTION_NAME=projeto:regiao:instancia-mysql
MOCK_SEED_ENABLED=true
AUTO_SEED_TOOLS=true
AUTO_SEED_MOCK=true
# ============================================ # ============================================
# CONFIGURACOES 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
# ============================================
# CONFIGURACOES DE API - FAKERAPI (Dados ficticios)
# ============================================
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 # AMBIENTE E DEBUG
# ============================================ # ============================================
@ -45,3 +49,10 @@ FAKERAPI_PERSONS_QUANTITY=120
ENVIRONMENT=development ENVIRONMENT=development
# DEBUG deve ser false em producao # DEBUG deve ser false em producao
DEBUG=true DEBUG=true
# ============================================
# CLOUD RUN - REDE PARA MYSQL CORPORATIVO (PRODUCAO)
# ============================================
# Ex.: projects/<project>/locations/<region>/connectors/<connector-name>
# RUN_VPC_CONNECTOR=
# RUN_VPC_EGRESS=private-ranges-only

@ -36,8 +36,8 @@ 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 (Tools)** | PostgreSQL | Banco relacional para metadados de ferramentas |
| **Dados de Teste** | FakerAPI | Geração de dados fictícios para simulação | | **Banco Fictício (Mock)** | MySQL | Dados de negócio simulados usados pelos handlers |
| **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
│ │ └── fakerapi_client.py # Cliente para gerar dados fictícios │ │ └── handlers.py # Handlers consultando MySQL fictício
│ │ │ │
│ ├── repositories/ │ ├── repositories/
│ │ └── tool_repository.py # Acesso a dados de ferramentas │ │ └── tool_repository.py # Acesso a dados de ferramentas
@ -157,7 +157,7 @@ Pipeline CI/CD totalmente gerenciado que faz build da imagem, realiza testes e f
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ Tool Handlers │ │ Tool Handlers │
│ - Executa: consultar_estoque │ │ - Executa: consultar_estoque │
│ - Busca no PostgreSQL │ - Busca no MySQL fictício
└──────┬───────────────────────────┘ └──────┬───────────────────────────┘

@ -51,6 +51,36 @@ curl -X POST http://localhost:8000/chat \
--- ---
### 2.1. Consultar Carro Mais Barato
```bash
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Qual e o carro mais barato que voces tem?",
"user_id": "user-123"
}'
```
**Esperado**: Modelo chama `consultar_estoque` com `ordenar_preco=asc` e `limite=1`
---
### 2.2. Consultar Carro Mais Caro
```bash
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Qual e o carro mais caro que voces tem?",
"user_id": "user-123"
}'
```
**Esperado**: Modelo chama `consultar_estoque` com `ordenar_preco=desc` e `limite=1`
---
### 3. Validar Cliente Venda ### 3. Validar Cliente Venda
```bash ```bash
@ -147,6 +177,18 @@ curl -X POST http://localhost:8000/mock/consultar-estoque \
-d '{"preco_max": 50000}' -d '{"preco_max": 50000}'
``` ```
```bash
curl -X POST http://localhost:8000/mock/consultar-estoque \
-H "Content-Type: application/json" \
-d '{"ordenar_preco": "asc", "limite": 1}'
```
```bash
curl -X POST http://localhost:8000/mock/consultar-estoque \
-H "Content-Type: application/json" \
-d '{"ordenar_preco": "desc", "limite": 1}'
```
### Ver resposta do Vertex AI ### Ver resposta do Vertex AI
Adicione console.log nos handlers para ver o que o Vertex retorna. Adicione console.log nos handlers para ver o que o Vertex retorna.

@ -1,6 +1,7 @@
from typing import List, Dict, Any from typing import List, Dict, Any
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.schemas import ( from app.api.schemas import (
@ -32,6 +33,22 @@ def get_db():
finally: finally:
db.close() db.close()
def _db_error_detail(exc: SQLAlchemyError) -> str:
text = str(exc).lower()
# Heuristica para identificar falhas no MySQL (base ficticia).
mysql_markers = ("mysql", "pymysql", "(2003", "mock", "3306")
if any(marker in text for marker in mysql_markers):
return "Servico temporariamente indisponivel: banco MySQL (dados ficticios) inacessivel."
# Heuristica para identificar falhas no PostgreSQL (tools).
pg_markers = ("postgres", "psycopg", "postgresql", "5432", "tools")
if any(marker in text for marker in pg_markers):
return "Servico temporariamente indisponivel: banco PostgreSQL (tools) inacessivel."
return "Servico temporariamente indisponivel: erro de acesso ao banco de dados."
''' '''
# Removido momentaniamente para teste do Vertex IA # Removido momentaniamente para teste do Vertex IA
@router.post("/chat", response_model=ChatResponse) @router.post("/chat", response_model=ChatResponse)
@ -45,33 +62,48 @@ async def chat(request: ChatRequest, db: Session = Depends(get_db)):
''' '''
@router.post("/chat", response_model=ChatResponse) @router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest, db: Session = Depends(get_db)): async def chat(request: ChatRequest, db: Session = Depends(get_db)):
service = OrquestradorService(db) try:
service = OrquestradorService(db)
result = await service.handle_message( result = await service.handle_message(message=request.message)
message=request.message return ChatResponse(response=result)
) except SQLAlchemyError as exc:
raise HTTPException(
return ChatResponse(response=result) status_code=503,
detail=_db_error_detail(exc),
)
except ValueError as exc:
# Erros de configuracao de Vertex (regiao/projeto/modelo)
raise HTTPException(status_code=500, detail=f"Configuracao invalida do Vertex AI: {exc}")
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=f"Falha temporaria no LLM/Vertex AI: {exc}")
@router.post("/mock/consultar-estoque") @router.post("/mock/consultar-estoque")
async def consultar_estoque_endpoint( async def consultar_estoque_endpoint(
body: ConsultarEstoqueRequest, body: ConsultarEstoqueRequest,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
return await consultar_estoque( try:
preco_max=body.preco_max, return await consultar_estoque(
categoria=body.categoria, preco_max=body.preco_max,
) categoria=body.categoria,
ordenar_preco=body.ordenar_preco,
limite=body.limite,
)
except SQLAlchemyError as exc:
raise HTTPException(status_code=503, detail=_db_error_detail(exc))
@router.post("/mock/validar-cliente-venda") @router.post("/mock/validar-cliente-venda")
async def validar_cliente_venda_endpoint( async def validar_cliente_venda_endpoint(
body: ValidarClienteVendaRequest, body: ValidarClienteVendaRequest,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
return await validar_cliente_venda( try:
cpf=body.cpf, return await validar_cliente_venda(
valor_veiculo=body.valor_veiculo, cpf=body.cpf,
) valor_veiculo=body.valor_veiculo,
)
except SQLAlchemyError as exc:
raise HTTPException(status_code=503, detail=_db_error_detail(exc))
@router.post("/mock/avaliar-veiculo-troca") @router.post("/mock/avaliar-veiculo-troca")
@ -89,17 +121,23 @@ async def avaliar_veiculo_troca_endpoint(
async def agendar_revisao_endpoint( async def agendar_revisao_endpoint(
body: AgendarRevisaoRequest, body: AgendarRevisaoRequest,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
return await agendar_revisao( try:
placa=body.placa, return await agendar_revisao(
data_hora=body.data_hora, placa=body.placa,
) data_hora=body.data_hora,
)
except SQLAlchemyError as exc:
raise HTTPException(status_code=503, detail=_db_error_detail(exc))
@router.post("/mock/cancelar-pedido") @router.post("/mock/cancelar-pedido")
async def cancelar_pedido_endpoint( async def cancelar_pedido_endpoint(
body: CancelarPedidoRequest, body: CancelarPedidoRequest,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
return await cancelar_pedido( try:
numero_pedido=body.numero_pedido, return await cancelar_pedido(
motivo=body.motivo, numero_pedido=body.numero_pedido,
) motivo=body.motivo,
)
except SQLAlchemyError as exc:
raise HTTPException(status_code=503, detail=_db_error_detail(exc))

@ -1,5 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, Literal
class ChatRequest(BaseModel): class ChatRequest(BaseModel):
message: str message: str
@ -26,8 +26,10 @@ class ToolResponse(BaseModel):
class ConsultarEstoqueRequest(BaseModel): class ConsultarEstoqueRequest(BaseModel):
preco_max: float preco_max: Optional[float] = None
categoria: Optional[str] = None categoria: Optional[str] = None
ordenar_preco: Optional[Literal["asc", "desc"]] = None
limite: Optional[int] = None
class ValidarClienteVendaRequest(BaseModel): class ValidarClienteVendaRequest(BaseModel):

@ -12,11 +12,16 @@ class Settings(BaseSettings):
db_password: str db_password: str
db_name: str db_name: str
fakerapi_base_url: str = "https://fakerapi.it/api/v2" # Mock database (MySQL) for fictitious business data
fakerapi_locale: str = "pt_BR" mock_db_host: str = "127.0.0.1"
fakerapi_seed: int = 42 mock_db_port: int = 3306
fakerapi_products_quantity: int = 50 mock_db_user: str = "root"
fakerapi_persons_quantity: int = 120 mock_db_password: str = ""
mock_db_name: str = "orquestrador_mock"
mock_db_cloud_sql_connection_name: str | None = None
mock_seed_enabled: bool = True
auto_seed_tools: bool = True
auto_seed_mock: bool = True
environment: str = "production" environment: str = "production"
debug: bool = False debug: bool = False
@ -24,6 +29,10 @@ class Settings(BaseSettings):
# Cloud SQL # Cloud SQL
cloud_sql_connection_name: str | None = None cloud_sql_connection_name: str | None = None
# Cloud Run networking (for deploy script / documentation)
run_vpc_connector: str | None = None
run_vpc_egress: str = "private-ranges-only"
class Config: class Config:
env_file = ".env" env_file = ".env"
extra = "ignore" extra = "ignore"

@ -3,17 +3,16 @@ from sqlalchemy.orm import sessionmaker, declarative_base
from app.core.settings import settings from app.core.settings import settings
if settings.cloud_sql_connection_name: if settings.cloud_sql_connection_name:
# Cloud Run - Formato para PostgreSQL # Cloud Run - PostgreSQL via Unix socket
# Note que usamos 'host' dentro da query string para apontar o socket
DATABASE_URL = ( DATABASE_URL = (
f"postgresql+psycopg2://{settings.db_user}:{settings.db_password}@/{settings.db_name}" f"postgresql+psycopg://{settings.db_user}:{settings.db_password}@/{settings.db_name}"
f"?host=/cloudsql/{settings.cloud_sql_connection_name}" f"?host=/cloudsql/{settings.cloud_sql_connection_name}"
) )
else: else:
# Ambiente local (via Cloud SQL Proxy) # Ambiente local/VPN - PostgreSQL em host/porta configurados
DATABASE_URL = ( DATABASE_URL = (
f"postgresql+psycopg2://{settings.db_user}:{settings.db_password}@" f"postgresql+psycopg://{settings.db_user}:{settings.db_password}@"
f"127.0.0.1:5432/{settings.db_name}" f"{settings.db_host}:{settings.db_port}/{settings.db_name}"
) )
engine = create_engine( engine = create_engine(

@ -1,28 +1,40 @@
""" """
Inicialização de banco de dados Inicializacao de banco de dados.
Cria tabelas e faz seed dos dados iniciais Cria tabelas e executa seed inicial em ambos os bancos.
""" """
from app.db.database import Base, engine from app.db.database import Base, engine
from app.db.mock_database import MockBase, mock_engine
from app.db.models import Tool from app.db.models import Tool
from app.db.tool_seed import get_tools_definitions, seed_tools from app.db.mock_models import Customer, Order, ReviewSchedule, Vehicle
from app.db.mock_seed import seed_mock_data
from app.db.tool_seed import seed_tools
def init_db(): def init_db():
"""Cria todas as tabelas e faz o seed dos dados iniciais""" """Cria tabelas e executa seed inicial em ambos os bancos."""
print("📊 Inicializando banco de dados...") print("Inicializando bancos...")
# Cria todas as tabelas try:
print("🔨 Criando tabelas...") print("Criando tabelas PostgreSQL (tools)...")
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
print("Populando tools iniciais...")
# Seed das tools seed_tools()
print("📥 Populando tools iniciais...") print("PostgreSQL OK.")
seed_tools() except Exception as exc:
print(f"Aviso: falha no PostgreSQL (tools): {exc}")
print("✅ Banco de dados inicializado com sucesso!")
try:
print("Criando tabelas MySQL (dados ficticios)...")
MockBase.metadata.create_all(bind=mock_engine)
print("Populando dados ficticios iniciais...")
seed_mock_data()
print("MySQL mock OK.")
except Exception as exc:
print(f"Aviso: falha no MySQL mock: {exc}")
print("Bancos inicializados com sucesso!")
if __name__ == "__main__": if __name__ == "__main__":
init_db() init_db()

@ -0,0 +1,31 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from app.core.settings import settings
if settings.mock_db_cloud_sql_connection_name:
# Cloud SQL MySQL via Unix socket
MOCK_DATABASE_URL = (
f"mysql+pymysql://{settings.mock_db_user}:{settings.mock_db_password}@/{settings.mock_db_name}"
f"?unix_socket=/cloudsql/{settings.mock_db_cloud_sql_connection_name}"
)
else:
MOCK_DATABASE_URL = (
f"mysql+pymysql://{settings.mock_db_user}:{settings.mock_db_password}@"
f"{settings.mock_db_host}:{settings.mock_db_port}/{settings.mock_db_name}"
)
mock_engine = create_engine(
MOCK_DATABASE_URL,
pool_pre_ping=True,
connect_args={"connect_timeout": 5},
)
SessionMockLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=mock_engine,
)
MockBase = declarative_base()

@ -0,0 +1,54 @@
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text
from sqlalchemy.sql import func
from app.db.mock_database import MockBase
class Vehicle(MockBase):
__tablename__ = "vehicles"
id = Column(Integer, primary_key=True, index=True)
modelo = Column(String(120), nullable=False)
categoria = Column(String(50), nullable=False, index=True)
preco = Column(Float, nullable=False, index=True)
created_at = Column(DateTime, server_default=func.current_timestamp())
class Customer(MockBase):
__tablename__ = "customers"
id = Column(Integer, primary_key=True, index=True)
cpf = Column(String(11), unique=True, nullable=False, index=True)
nome = Column(String(120), nullable=False)
score = Column(Integer, nullable=False)
limite_credito = Column(Float, nullable=False)
possui_restricao = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, server_default=func.current_timestamp())
class Order(MockBase):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
numero_pedido = Column(String(40), unique=True, nullable=False, index=True)
cpf = Column(String(11), ForeignKey("customers.cpf"), nullable=False, index=True)
status = Column(String(20), nullable=False, default="Ativo")
motivo_cancelamento = Column(Text, nullable=True)
data_cancelamento = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.current_timestamp())
updated_at = Column(
DateTime,
server_default=func.current_timestamp(),
onupdate=func.current_timestamp(),
)
class ReviewSchedule(MockBase):
__tablename__ = "review_schedules"
id = Column(Integer, primary_key=True, index=True)
protocolo = Column(String(50), unique=True, nullable=False, index=True)
placa = Column(String(10), nullable=False, index=True)
data_hora = Column(DateTime, nullable=False)
status = Column(String(20), nullable=False, default="agendado")
created_at = Column(DateTime, server_default=func.current_timestamp())

@ -0,0 +1,96 @@
import random
from datetime import datetime
from app.core.settings import settings
from app.db.mock_database import SessionMockLocal
from app.db.mock_models import Customer, Order, Vehicle
VEHICLE_MODELS = [
"Toyota Corolla",
"Honda Civic",
"Chevrolet Onix",
"Hyundai HB20",
"Volkswagen T-Cross",
"Jeep Compass",
"Fiat Argo",
"Nissan Kicks",
"Renault Duster",
"Ford Ranger",
]
CATEGORIES = ["hatch", "sedan", "suv", "pickup"]
NAMES = [
"Ana Souza",
"Bruno Lima",
"Carla Mendes",
"Diego Santos",
"Eduarda Alves",
"Felipe Rocha",
"Gabriela Costa",
"Henrique Martins",
"Isabela Ferreira",
"Joao Ribeiro",
]
def _cpf_from_index(index: int) -> str:
return str(10_000_000_000 + index).zfill(11)
def seed_mock_data() -> None:
if not settings.mock_seed_enabled:
return
rng = random.Random(42)
db = SessionMockLocal()
try:
if db.query(Vehicle).count() == 0:
vehicles = []
for idx in range(60):
model = VEHICLE_MODELS[idx % len(VEHICLE_MODELS)]
category = CATEGORIES[idx % len(CATEGORIES)]
base_price = 55_000 + (idx * 1_700)
noise = rng.randint(-7_000, 9_000)
vehicles.append(
Vehicle(
modelo=f"{model} {2020 + (idx % 6)}",
categoria=category,
preco=float(max(35_000, base_price + noise)),
)
)
db.add_all(vehicles)
db.commit()
if db.query(Customer).count() == 0:
customers = []
for idx in range(120):
entropy = (idx * 9973) % 10_000
customers.append(
Customer(
cpf=_cpf_from_index(idx),
nome=f"{NAMES[idx % len(NAMES)]} {idx + 1}",
score=300 + (entropy % 550),
limite_credito=float(30_000 + (entropy * 12)),
possui_restricao=(idx % 11 == 0),
)
)
db.add_all(customers)
db.commit()
if db.query(Order).count() == 0:
orders = []
for idx in range(40):
created = datetime(2026, 1, 1, 8, 0, 0)
orders.append(
Order(
numero_pedido=f"PED-{2026}{idx + 1:05d}",
cpf=_cpf_from_index(idx),
status="Ativo",
created_at=created,
)
)
db.add_all(orders)
db.commit()
finally:
db.close()

@ -6,35 +6,53 @@ def get_tools_definitions():
return [ return [
{ {
"name": "consultar_estoque", "name": "consultar_estoque",
"description": "Use esta ferramenta para consultar veículos disponíveis no estoque até um preço máximo informado pelo cliente, opcionalmente filtrando por categoria (Hatch, Sedan, SUV, etc.). Ideal quando o cliente quer saber quais carros cabem no orçamento ou comparar opções dentro de uma faixa de preço.", "description": (
"Use esta ferramenta para consultar veiculos disponiveis no estoque. "
"Voce pode filtrar por preco maximo e categoria, e tambem ordenar por "
"preco para descobrir o carro mais barato (asc + limite=1) ou mais "
"caro (desc + limite=1)."
),
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"preco_max": { "preco_max": {
"type": "number", "type": "number",
"description": "Preço máximo do veículo em reais (BRL)." "description": "Preco maximo do veiculo em reais (BRL). Opcional.",
}, },
"categoria": { "categoria": {
"type": "string", "type": "string",
"description": "Categoria do veículo, por exemplo: Hatch, Sedan, SUV. Opcional." "description": "Categoria do veiculo, por exemplo: Hatch, Sedan, SUV. Opcional.",
},
"ordenar_preco": {
"type": "string",
"description": "Ordenacao do preco. Use 'asc' para mais barato e 'desc' para mais caro.",
},
"limite": {
"type": "integer",
"description": "Quantidade maxima de veiculos retornados.",
}, },
}, },
"required": ["preco_max"], "required": [],
}, },
}, },
{ {
"name": "validar_cliente_venda", "name": "validar_cliente_venda",
"description": "Use esta ferramenta quando precisar avaliar se o cliente pode financiar um veículo específico. Ela recebe o CPF e o valor do veículo, consulta um score simulado e retorna se o cliente está aprovado ou reprovado para a compra, juntamente com o score e um limite de crédito estimado.", "description": (
"Use esta ferramenta quando precisar avaliar se o cliente pode financiar "
"um veiculo especifico. Ela recebe o CPF e o valor do veiculo, consulta "
"um score simulado e retorna se o cliente esta aprovado ou reprovado "
"para a compra, juntamente com o score e um limite de credito estimado."
),
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"cpf": { "cpf": {
"type": "string", "type": "string",
"description": "CPF do cliente, com ou sem formatação (apenas dígitos também é aceito)." "description": "CPF do cliente, com ou sem formatacao (apenas digitos tambem e aceito).",
}, },
"valor_veiculo": { "valor_veiculo": {
"type": "number", "type": "number",
"description": "Valor do veículo em reais (BRL) que o cliente deseja comprar." "description": "Valor do veiculo em reais (BRL) que o cliente deseja comprar.",
}, },
}, },
"required": ["cpf", "valor_veiculo"], "required": ["cpf", "valor_veiculo"],
@ -42,21 +60,26 @@ def get_tools_definitions():
}, },
{ {
"name": "avaliar_veiculo_troca", "name": "avaliar_veiculo_troca",
"description": "Use esta ferramenta quando o cliente quiser saber quanto o carro dele vale como entrada em uma negociação. Ela recebe modelo, ano e quilometragem do veículo atual e devolve um valor estimado de avaliação para troca, já considerando depreciação por ano e quilometragem.", "description": (
"Use esta ferramenta quando o cliente quiser saber quanto o carro dele "
"vale como entrada em uma negociacao. Ela recebe modelo, ano e "
"quilometragem do veiculo atual e devolve um valor estimado de avaliacao "
"para troca, ja considerando depreciacao por ano e quilometragem."
),
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"modelo": { "modelo": {
"type": "string", "type": "string",
"description": "Modelo do veículo que o cliente deseja oferecer na troca (por exemplo, 'Toyota Corolla')." "description": "Modelo do veiculo que o cliente deseja oferecer na troca (por exemplo, 'Toyota Corolla').",
}, },
"ano": { "ano": {
"type": "integer", "type": "integer",
"description": "Ano de fabricação do veículo do cliente." "description": "Ano de fabricacao do veiculo do cliente.",
}, },
"km": { "km": {
"type": "integer", "type": "integer",
"description": "Quilometragem atual do veículo do cliente." "description": "Quilometragem atual do veiculo do cliente.",
}, },
}, },
"required": ["modelo", "ano", "km"], "required": ["modelo", "ano", "km"],
@ -64,17 +87,22 @@ def get_tools_definitions():
}, },
{ {
"name": "agendar_revisao", "name": "agendar_revisao",
"description": "Use esta ferramenta quando o cliente quiser marcar uma revisão ou manutenção para o veículo. Ela recebe a placa e a data/hora desejada, cria um agendamento simulado e retorna um identificador, além do status do agendamento.", "description": (
"Use esta ferramenta quando o cliente quiser marcar uma revisao ou "
"manutencao para o veiculo. Ela recebe a placa e a data/hora desejada, "
"cria um agendamento simulado e retorna um identificador, alem do "
"status do agendamento."
),
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"placa": { "placa": {
"type": "string", "type": "string",
"description": "Placa do veículo que será levado para revisão." "description": "Placa do veiculo que sera levado para revisao.",
}, },
"data_hora": { "data_hora": {
"type": "string", "type": "string",
"description": "Data e hora desejada para a revisão, em formato ISO 8601 (por exemplo, '2026-03-10T09:00:00-03:00')." "description": "Data e hora desejada para a revisao, em formato ISO 8601 (por exemplo, '2026-03-10T09:00:00-03:00').",
}, },
}, },
"required": ["placa", "data_hora"], "required": ["placa", "data_hora"],
@ -82,17 +110,22 @@ def get_tools_definitions():
}, },
{ {
"name": "cancelar_pedido", "name": "cancelar_pedido",
"description": "Use esta ferramenta quando o cliente solicitar o cancelamento de um pedido já registrado. Ela recebe o número do pedido e o motivo do cancelamento, atualiza o status para 'Cancelado' e retorna os detalhes do cancelamento para que você explique o resultado ao cliente.", "description": (
"Use esta ferramenta quando o cliente solicitar o cancelamento de um "
"pedido ja registrado. Ela recebe o numero do pedido e o motivo do "
"cancelamento, atualiza o status para 'Cancelado' e retorna os detalhes "
"do cancelamento para que voce explique o resultado ao cliente."
),
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"numero_pedido": { "numero_pedido": {
"type": "string", "type": "string",
"description": "Número do pedido que o cliente deseja cancelar." "description": "Numero do pedido que o cliente deseja cancelar.",
}, },
"motivo": { "motivo": {
"type": "string", "type": "string",
"description": "Motivo do cancelamento informado pelo cliente (por exemplo, atraso, mudança de planos, condição de pagamento, etc.)." "description": "Motivo do cancelamento informado pelo cliente (por exemplo, atraso, mudanca de planos, condicao de pagamento, etc.).",
}, },
}, },
"required": ["numero_pedido", "motivo"], "required": ["numero_pedido", "motivo"],
@ -109,6 +142,11 @@ def seed_tools():
existing_names = {t.name for t in existing} existing_names = {t.name for t in existing}
for tool_def in get_tools_definitions(): for tool_def in get_tools_definitions():
if tool_def["name"] in existing_names: if tool_def["name"] in existing_names:
repo.update_by_name(
name=tool_def["name"],
description=tool_def["description"],
parameters=tool_def["parameters"],
)
continue continue
repo.create( repo.create(
name=tool_def["name"], name=tool_def["name"],

@ -1,9 +1,12 @@
from fastapi import FastAPI from fastapi import FastAPI
from app.api.routes import router from app.api.routes import router
from app.api.tool_routes import router as tool_router from app.api.tool_routes import router as tool_router
from app.core.settings import settings
from app.db.database import Base, engine from app.db.database import Base, engine
# 👇 IMPORTANTE: registrar models no metadata from app.db.mock_database import MockBase, mock_engine
from app.db.models import Tool from app.db.models import Tool
from app.db.mock_models import Customer, Order, ReviewSchedule, Vehicle
app = FastAPI(title="AI Orquestrador") app = FastAPI(title="AI Orquestrador")
@ -16,21 +19,25 @@ async def startup_event():
""" """
Inicializa o banco de dados e executa seeds automaticamente. Inicializa o banco de dados e executa seeds automaticamente.
""" """
print("[Auto-Seed] Iniciando configuracao do banco...")
# PostgreSQL (tools) e MySQL (mock) sobem de forma independente.
try: try:
print("🚀 [Auto-Seed] Iniciando configuração do banco...")
# 1. Cria as tabelas se não existirem
# O engine deve estar configurado para usar o Unix Socket no Cloud Run
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
if settings.auto_seed_tools:
from app.db.tool_seed import seed_tools
seed_tools()
print("[Auto-Seed] PostgreSQL de tools inicializado.")
except Exception as e:
print(f"[Auto-Seed] Aviso: falha ao inicializar PostgreSQL (tools): {e}")
# 2. Executa a seed das ferramentas try:
from app.db.tool_seed import seed_tools MockBase.metadata.create_all(bind=mock_engine)
seed_tools() if settings.auto_seed_mock and settings.mock_seed_enabled:
from app.db.mock_seed import seed_mock_data
print("✅ [Auto-Seed] Tabelas e ferramentas configuradas com sucesso.") seed_mock_data()
print("[Auto-Seed] MySQL de mock inicializado.")
except Exception as e: except Exception as e:
# IMPORTANTE: Logamos o erro mas NÃO damos 'raise e' print(f"[Auto-Seed] Aviso: falha ao inicializar MySQL (mock): {e}")
# Isso permite que o Uvicorn abra a porta 8080 e o deploy complete
print(f"⚠️ [Auto-Seed] Aviso: Falha na inicialização automática: {e}") print("[Auto-Seed] Startup finalizado.")
print("A aplicação tentará operar, verifique a conexão com o Cloud SQL.")

@ -30,3 +30,14 @@ class ToolRepository:
self.db.delete(tool) self.db.delete(tool)
self.db.commit() self.db.commit()
return tool return tool
def update_by_name(self, name: str, description: str, parameters: dict):
tool = self.db.query(Tool).filter(Tool.name == name).first()
if not tool:
return None
tool.description = description
tool.parameters = parameters
self.db.commit()
self.db.refresh(tool)
return tool

@ -1,57 +0,0 @@
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,13 +1,12 @@
from typing import Optional, List, Dict, Any
from datetime import datetime from datetime import datetime
import hashlib import hashlib
import re import re
from typing import Any, Dict, List, Optional
import httpx
from fastapi import HTTPException from fastapi import HTTPException
from app.core.settings import settings from app.db.mock_database import SessionMockLocal
from app.services.fakerapi_client import FakerApiClient from app.db.mock_models import Customer, Order, ReviewSchedule, Vehicle
def normalize_cpf(value: str) -> str: def normalize_cpf(value: str) -> str:
@ -32,139 +31,75 @@ def _stable_int(seed_text: str) -> int:
return int(digest[:16], 16) return int(digest[:16], 16)
def _cpf_from_any(value: Any) -> str: async def consultar_estoque(
as_int = _stable_int(str(value)) % (10**11) preco_max: Optional[float] = None,
return str(as_int).zfill(11) categoria: Optional[str] = None,
ordenar_preco: Optional[str] = None,
limite: Optional[int] = None,
async def _fetch_faker_products(count: int) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
client = FakerApiClient() db = SessionMockLocal()
try: try:
return await client.fetch_resource("products", quantity=count) query = db.query(Vehicle)
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.",
)
if preco_max is not None:
query = query.filter(Vehicle.preco <= preco_max)
if categoria:
query = query.filter(Vehicle.categoria == categoria.lower())
async def _fetch_faker_persons(count: int) -> List[Dict[str, Any]]: if ordenar_preco in ("asc", "desc"):
client = FakerApiClient() query = query.order_by(Vehicle.preco.asc() if ordenar_preco == "asc" else Vehicle.preco.desc())
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]]: if limite is not None:
raw = await _fetch_faker_products(settings.fakerapi_products_quantity) try:
registros: List[Dict[str, Any]] = [] limite = max(1, int(limite))
query = query.limit(limite)
except (TypeError, ValueError):
pass
for item in raw: rows = query.all()
categories = item.get("categories") return [
if isinstance(categories, list) and categories: {
category_value = str(categories[0]) "id": row.id,
else: "modelo": row.modelo,
category_value = str(item.get("category") or "geral") "categoria": row.categoria,
"preco": _parse_float(row.preco),
registro = { }
"id": item.get("id"), for row in rows
"modelo": item.get("name") or item.get("title") or "Veiculo", ]
"categoria": category_value.lower(), finally:
"preco": _parse_float(item.get("price"), 0.0), db.close()
}
registros.append(registro)
categoria_norm = categoria.lower() if categoria else None
return [
r for r in registros
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]:
cpf_norm = normalize_cpf(cpf) cpf_norm = normalize_cpf(cpf)
raw = await _fetch_faker_persons(settings.fakerapi_persons_quantity) db = SessionMockLocal()
try:
registros: List[Dict[str, Any]] = [] cliente = db.query(Customer).filter(Customer.cpf == cpf_norm).first()
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 cliente:
if not cliente: score = int(cliente.score)
entropy = _stable_int(f"{cpf_norm}:{settings.fakerapi_seed}") limite = _parse_float(cliente.limite_credito, 0.0)
cliente = { restricao = bool(cliente.possui_restricao)
nome = cliente.nome
else:
entropy = _stable_int(cpf_norm)
score = int(300 + (entropy % 550))
limite = float(30000 + (entropy % 150000))
restricao = entropy % 7 == 0
nome = "Cliente Simulado"
aprovado = (not restricao) and (valor_veiculo <= limite)
return {
"aprovado": aprovado,
"cpf": cpf_norm, "cpf": cpf_norm,
"nome": "Cliente Faker", "nome": nome,
"score": int(300 + (entropy % 550)), "score": score,
"limite_credito": float(30000 + (entropy % 150000)), "limite_credito": limite,
"possui_restricao": (entropy % 7 == 0), "possui_restricao": restricao,
"valor_veiculo": valor_veiculo,
} }
finally:
limite = _parse_float(cliente.get("limite_credito", 0), 0.0) db.close()
restricao = bool(cliente.get("possui_restricao", False))
aprovado = (not restricao) and (valor_veiculo <= limite)
return {
"aprovado": aprovado,
"cpf": cpf_norm,
"nome": cliente.get("nome"),
"score": cliente.get("score"),
"limite_credito": limite,
"possui_restricao": restricao,
"valor_veiculo": valor_veiculo,
}
async def avaliar_veiculo_troca(modelo: str, ano: int, km: int) -> Dict[str, Any]: async def avaliar_veiculo_troca(modelo: str, ano: int, km: int) -> Dict[str, Any]:
@ -182,14 +117,74 @@ 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]:
raise HTTPException( try:
status_code=503, dt = datetime.fromisoformat(data_hora.replace("Z", "+00:00"))
detail="FakerAPI nao suporta escrita/persistencia. Endpoint disponivel apenas para leitura de dados ficticios.", except ValueError:
) raise HTTPException(
status_code=400,
detail="data_hora invalida. Use formato ISO 8601, por exemplo: 2026-03-10T09:00:00-03:00",
)
entropy = hashlib.md5(f"{placa}:{data_hora}".encode("utf-8")).hexdigest()[:8].upper()
protocolo = f"REV-{dt.strftime('%Y%m%d')}-{entropy}"
db = SessionMockLocal()
try:
existente = db.query(ReviewSchedule).filter(ReviewSchedule.protocolo == protocolo).first()
if existente:
return {
"protocolo": existente.protocolo,
"placa": existente.placa,
"data_hora": existente.data_hora.isoformat(),
"status": existente.status,
}
agendamento = ReviewSchedule(
protocolo=protocolo,
placa=placa.upper(),
data_hora=dt,
status="agendado",
)
db.add(agendamento)
db.commit()
db.refresh(agendamento)
return {
"protocolo": agendamento.protocolo,
"placa": agendamento.placa,
"data_hora": agendamento.data_hora.isoformat(),
"status": agendamento.status,
}
finally:
db.close()
async def cancelar_pedido(numero_pedido: str, motivo: str) -> Dict[str, Any]: async def cancelar_pedido(numero_pedido: str, motivo: str) -> Dict[str, Any]:
raise HTTPException( db = SessionMockLocal()
status_code=503, try:
detail="FakerAPI nao suporta cancelamento persistente de pedidos. Endpoint indisponivel neste modo.", pedido = db.query(Order).filter(Order.numero_pedido == numero_pedido).first()
) if not pedido:
raise HTTPException(status_code=404, detail="Pedido nao encontrado na base ficticia.")
if pedido.status.lower() == "cancelado":
return {
"numero_pedido": pedido.numero_pedido,
"status": pedido.status,
"motivo": pedido.motivo_cancelamento,
"data_cancelamento": pedido.data_cancelamento.isoformat() if pedido.data_cancelamento else None,
}
pedido.status = "Cancelado"
pedido.motivo_cancelamento = motivo
pedido.data_cancelamento = datetime.utcnow()
db.commit()
db.refresh(pedido)
return {
"numero_pedido": pedido.numero_pedido,
"status": pedido.status,
"motivo": pedido.motivo_cancelamento,
"data_cancelamento": pedido.data_cancelamento.isoformat() if pedido.data_cancelamento else None,
}
finally:
db.close()

@ -1,44 +1,50 @@
#!/bin/bash #!/bin/bash
# Script de deploy para Google Cloud Run com Artifact Registry # Deploy script for Google Cloud Run + Artifact Registry
# Uso: ./deploy.sh # Usage: ./deploy.sh
# Pré-requisitos:
# - gcloud CLI autenticado e com projeto principal setado
# - Artifact Registry Repository já criado (orquestrador)
# - Service Account com roles: Cloud SQL Client, Cloud Run Developer, Artifact Registry Writer
set -e set -euo pipefail
ENV_FILE="${ENV_FILE:-.env.prod}"
if [ ! -f "${ENV_FILE}" ] && [ -f ".env" ]; then
ENV_FILE=".env"
fi
if [ ! -f "${ENV_FILE}" ]; then
echo "ERROR: env file not found. Expected ${ENV_FILE} (or .env)."
exit 1
fi
get_env_value() {
local key="$1"
grep -E "^${key}=" "${ENV_FILE}" | tail -n 1 | cut -d'=' -f2- | tr -d '\r'
}
# Detectar configuração do gcloud
PROJECT_ID=$(gcloud config get-value project) PROJECT_ID=$(gcloud config get-value project)
REGION="us-central1" REGION="us-central1"
SERVICE_NAME="orquestrador" SERVICE_NAME="orquestrador"
REPO_NAME="orquestrador" REPO_NAME="orquestrador"
IMAGE_NAME="$REGION-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$SERVICE_NAME" IMAGE_NAME="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${SERVICE_NAME}"
if [ -z "$PROJECT_ID" ]; then if [ -z "${PROJECT_ID}" ]; then
echo "❌ Projeto não configurado. Execute: gcloud config set project <seu-projeto>" echo "ERROR: gcloud project is not configured."
echo "Run: gcloud config set project <your-project-id>"
exit 1 exit 1
fi fi
echo "🚀 Iniciando deploy para Google Cloud Run" echo "Starting deploy to Cloud Run"
echo "📦 Projeto: $PROJECT_ID" echo "Project: ${PROJECT_ID}"
echo "🌍 Região: $REGION" echo "Region: ${REGION}"
echo "🏗️ Repositório: $REPO_NAME" echo "Env file: ${ENV_FILE}"
# Passo 1: Validar autenticação echo "1) Checking auth..."
echo ""
echo "1⃣ Verificando autenticação GCP..."
ACTIVE_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)") ACTIVE_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)")
if [ -z "$ACTIVE_ACCOUNT" ]; then if [ -z "${ACTIVE_ACCOUNT}" ]; then
echo "❌ Nenhuma conta autenticada. Execute: gcloud auth login" echo "ERROR: no active gcloud account. Run: gcloud auth login"
exit 1 exit 1
fi fi
echo "✅ Autenticado como: $ACTIVE_ACCOUNT" echo "Authenticated as: ${ACTIVE_ACCOUNT}"
# Passo 2: Habilitar APIs echo "2) Enabling required APIs..."
echo ""
echo "2⃣ Habilitando APIs necessárias..."
gcloud services enable \ gcloud services enable \
run.googleapis.com \ run.googleapis.com \
cloudbuild.googleapis.com \ cloudbuild.googleapis.com \
@ -46,78 +52,60 @@ gcloud services enable \
aiplatform.googleapis.com \ aiplatform.googleapis.com \
sqladmin.googleapis.com \ sqladmin.googleapis.com \
--quiet --quiet
echo "✅ APIs habilitadas"
# Passo 3: Build com Cloud Build echo "3) Building image with Cloud Build..."
echo ""
echo "3⃣ Fazendo build da imagem Docker com Cloud Build..."
gcloud builds submit \ gcloud builds submit \
--config=cloudbuild.yaml \ --config=cloudbuild.yaml \
--substitutions=_REGION="$REGION",_REPO_NAME="$REPO_NAME",_IMAGE_NAME="$SERVICE_NAME" --substitutions=_REGION="${REGION}",_REPO_NAME="${REPO_NAME}",_IMAGE_NAME="${SERVICE_NAME}"
echo "✅ Build concluído"
# Passo 4: Deploy para Cloud Run com variáveis de ambiente echo "4) Deploying to Cloud Run..."
echo ""
echo "4⃣ Realizando deploy para Cloud Run com variáveis de ambiente..."
# Ler variáveis do .env (excluindo comentários e linhas vazias, e tratando espaços/caracteres especiais)
ENV_VARS="" ENV_VARS=""
while IFS= read -r line || [[ -n "$line" ]]; do while IFS= read -r line || [[ -n "$line" ]]; do
# Ignorar linhas vazias e comentários
[[ "$line" =~ ^[[:space:]]*# ]] && continue [[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "${line// }" ]] && continue [[ -z "${line// }" ]] && continue
# Adicionar à string de variáveis (key=value) e remover \r (quebras de linha Windows)
if [[ "$line" == *"="* ]]; then if [[ "$line" == *"="* ]]; then
# Limpar caracteres de retorno de carro (\r) para evitar erros no Linux
clean_line=$(echo "$line" | tr -d '\r') clean_line=$(echo "$line" | tr -d '\r')
ENV_VARS="$ENV_VARS$clean_line," ENV_VARS="${ENV_VARS}${clean_line},"
fi fi
done < .env done < "${ENV_FILE}"
# Remover última vírgula
ENV_VARS="${ENV_VARS%,}" ENV_VARS="${ENV_VARS%,}"
if [ -z "$ENV_VARS" ]; then CLOUD_SQL_CONN=$(get_env_value "CLOUD_SQL_CONNECTION_NAME")
echo "⚠️ Nenhuma variável de ambiente encontrada em .env" RUN_VPC_CONN=$(get_env_value "RUN_VPC_CONNECTOR")
# Deploy sem variáveis (pode falhar se a app exigir) RUN_VPC_EGRESS=$(get_env_value "RUN_VPC_EGRESS")
gcloud run deploy "$SERVICE_NAME" \
--image="$IMAGE_NAME:latest" \ DEPLOY_ARGS=(
--region="$REGION" \ --image="${IMAGE_NAME}:latest"
--platform=managed \ --region="${REGION}"
--allow-unauthenticated --platform=managed
--memory=512Mi
--cpu=1
--timeout=3600
--max-instances=10
--allow-unauthenticated
)
if [ -n "${ENV_VARS}" ]; then
DEPLOY_ARGS+=(--set-env-vars="${ENV_VARS}")
else else
# Deploy com as variáveis coletadas echo "WARN: no env vars found in ${ENV_FILE}"
gcloud run deploy "$SERVICE_NAME" \ fi
--image="$IMAGE_NAME:latest" \
--region="$REGION" \ if [ -n "${CLOUD_SQL_CONN}" ]; then
--platform=managed \ DEPLOY_ARGS+=(--add-cloudsql-instances="${CLOUD_SQL_CONN}")
--set-env-vars="$ENV_VARS" \ echo "Cloud SQL socket enabled: ${CLOUD_SQL_CONN}"
--memory=512Mi \
--cpu=1 \
--timeout=3600 \
--max-instances=10 \
--allow-unauthenticated
fi fi
echo "✅ Deploy concluído com variáveis de ambiente"
if [ -n "${RUN_VPC_CONN}" ]; then
# Passo 5: Sucesso DEPLOY_ARGS+=(--vpc-connector="${RUN_VPC_CONN}")
echo "" DEPLOY_ARGS+=(--vpc-egress="${RUN_VPC_EGRESS:-private-ranges-only}")
echo "================================" echo "VPC connector enabled: ${RUN_VPC_CONN}"
echo "✅ PROCESSO FINALIZADO!" fi
echo "================================"
echo "" gcloud run deploy "${SERVICE_NAME}" "${DEPLOY_ARGS[@]}"
echo "📋 Próximos passos:"
echo "" echo "Deploy finished."
echo "1. Verifique o status:" echo "Service URL:"
echo " gcloud run services describe $SERVICE_NAME --region=$REGION" gcloud run services describe "${SERVICE_NAME}" --region="${REGION}" --format='value(status.url)'
echo ""
echo "2. Obtenha a URL da aplicação:"
echo " gcloud run services describe $SERVICE_NAME --region=$REGION --format='value(status.url)'"
echo ""
echo "3. Teste a API:"
echo " curl https://<url-do-seu-servico>/docs"
echo ""
echo "4. Visualize logs:"
echo " gcloud run services logs read $SERVICE_NAME --region=$REGION --limit=50"

Binary file not shown.
Loading…
Cancel
Save