📝 docs(comments): documentar fluxos, rotas e configuracoes do servico

main
parent 8e35d33fd3
commit e6ce076785

@ -56,3 +56,10 @@ DEBUG=true
# Ex.: projects/<project>/locations/<region>/connectors/<connector-name>
# RUN_VPC_CONNECTOR=
# RUN_VPC_EGRESS=private-ranges-only
# ============================================
# TELEGRAM (SERVICO SATELITE)
# ============================================
TELEGRAM_BOT_TOKEN=
TELEGRAM_POLLING_TIMEOUT=30
TELEGRAM_REQUEST_TIMEOUT=45

@ -254,3 +254,25 @@ O projeto está em evolução contínua com planos para:
## 📧 Contato
Para dúvidas, sugestões ou contribuições, entre em contato com o time de desenvolvimento.
---
## Telegram Satellite Service
Foi adicionado um servico satelite dedicado ao Telegram.
Arquivos principais:
- `app/integrations/telegram_satellite_service.py`: interface Telegram via long polling, chamando o `OrquestradorService`
Como executar:
```bash
python -m app.integrations.telegram_satellite_service
```
Variaveis necessarias:
- `TELEGRAM_BOT_TOKEN`
- `TELEGRAM_POLLING_TIMEOUT` (opcional, padrao `30`)
- `TELEGRAM_REQUEST_TIMEOUT` (opcional, padrao `45`)

@ -27,6 +27,7 @@ router = APIRouter()
def get_db():
"""Fornece uma sessao de banco para a request e garante o fechamento."""
db = SessionLocal()
try:
yield db
@ -35,6 +36,7 @@ def get_db():
def _db_error_detail(exc: SQLAlchemyError) -> str:
"""Converte erros de banco em mensagens amigaveis para a API."""
text = str(exc).lower()
# Heuristica para identificar falhas no MySQL de tools.
@ -66,6 +68,7 @@ async def chat(request: ChatRequest, db: Session = Depends(get_db)):
'''
@router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest, db: Session = Depends(get_db)):
"""Processa mensagem do usuario via orquestrador e retorna resposta do chat."""
try:
service = OrquestradorService(db)
result = await service.handle_message(message=request.message)
@ -86,6 +89,7 @@ async def chat(request: ChatRequest, db: Session = Depends(get_db)):
async def consultar_estoque_endpoint(
body: ConsultarEstoqueRequest,
) -> List[Dict[str, Any]]:
"""Consulta estoque de veiculos mock com filtros opcionais."""
try:
return await consultar_estoque(
preco_max=body.preco_max,
@ -101,6 +105,7 @@ async def consultar_estoque_endpoint(
async def validar_cliente_venda_endpoint(
body: ValidarClienteVendaRequest,
) -> Dict[str, Any]:
"""Valida elegibilidade de cliente para uma venda."""
try:
return await validar_cliente_venda(
cpf=body.cpf,
@ -114,6 +119,7 @@ async def validar_cliente_venda_endpoint(
async def avaliar_veiculo_troca_endpoint(
body: AvaliarVeiculoTrocaRequest,
) -> Dict[str, Any]:
"""Avalia valor de troca de um veiculo com base em modelo, ano e quilometragem."""
return await avaliar_veiculo_troca(
modelo=body.modelo,
ano=body.ano,
@ -125,6 +131,7 @@ async def avaliar_veiculo_troca_endpoint(
async def agendar_revisao_endpoint(
body: AgendarRevisaoRequest,
) -> Dict[str, Any]:
"""Agenda revisao para uma placa em data/hora informada."""
try:
return await agendar_revisao(
placa=body.placa,
@ -137,7 +144,9 @@ async def agendar_revisao_endpoint(
@router.post("/mock/cancelar-pedido")
async def cancelar_pedido_endpoint(
body: CancelarPedidoRequest,
) -> Dict[str, Any]:
"""Cancela pedido de venda existente registrando o motivo."""
try:
return await cancelar_pedido(
numero_pedido=body.numero_pedido,

@ -1,6 +1,8 @@
from pydantic import BaseModel
from typing import Dict, Any, Optional, Literal
# Deifição de como vou pedir os parâmetros de cada item.
class ChatRequest(BaseModel):
message: str
# user_id: str -> Removido momentaniamente para testar o VertexIA

@ -9,6 +9,7 @@ router = APIRouter(prefix="/tools", tags=["Tools"])
# Dependency para abrir e fechar conexão automaticamente
def get_db():
"""Fornece sessao para operacoes de tools e fecha conexao ao final da request."""
db = SessionLocal()
try:
yield db
@ -16,8 +17,9 @@ def get_db():
db.close()
@router.post("/", response_model=ToolResponse)
@router.post("/", response_model=ToolResponse) # Desenvolver uma tela de cadastro e atualização
def create_tool(tool: ToolCreate, db: Session = Depends(get_db)):
"""Cria uma nova tool persistida no banco."""
repo = ToolRepository(db)
return repo.create(
name=tool.name,
@ -28,12 +30,14 @@ def create_tool(tool: ToolCreate, db: Session = Depends(get_db)):
@router.get("/", response_model=list[ToolResponse])
def list_tools(db: Session = Depends(get_db)):
"""Lista todas as tools cadastradas."""
repo = ToolRepository(db)
return repo.get_all()
@router.get("/{tool_id}", response_model=ToolResponse)
def get_tool(tool_id: int, db: Session = Depends(get_db)):
"""Busca uma tool por ID e retorna 404 quando inexistente."""
repo = ToolRepository(db)
tool = repo.get_by_id(tool_id)
@ -45,6 +49,7 @@ def get_tool(tool_id: int, db: Session = Depends(get_db)):
@router.delete("/{tool_id}")
def delete_tool(tool_id: int, db: Session = Depends(get_db)):
"""Remove uma tool por ID e retorna 404 quando nao encontrada."""
repo = ToolRepository(db)
tool = repo.delete(tool_id)

@ -35,6 +35,11 @@ class Settings(BaseSettings):
run_vpc_connector: str | None = None
run_vpc_egress: str = "private-ranges-only"
# Telegram satellite
telegram_bot_token: str | None = None
telegram_polling_timeout: int = 30
telegram_request_timeout: int = 45
class Config:
env_file = ".env"
extra = "ignore"

@ -35,10 +35,12 @@ NAMES = [
def _cpf_from_index(index: int) -> str:
"""Gera um CPF numerico deterministico de 11 digitos a partir do indice."""
return str(10_000_000_000 + index).zfill(11)
def seed_mock_data() -> None:
"""Popula dados mock iniciais de veiculos, clientes e pedidos quando habilitado."""
if not settings.mock_seed_enabled:
return

@ -3,6 +3,7 @@ from app.repositories.tool_repository import ToolRepository
def get_tools_definitions():
"""Retorna as definicoes padrao de tools usadas para seed e sincronizacao."""
return [
{
"name": "consultar_estoque",
@ -135,6 +136,7 @@ def get_tools_definitions():
def seed_tools():
"""Insere ou atualiza as tools padrao no banco de dados."""
db = SessionLocal()
try:
repo = ToolRepository(db)

@ -5,15 +5,19 @@ from app.db.models import Tool
class ToolRepository:
def __init__(self, db: Session):
"""Inicializa o repositorio com a sessao ativa do banco."""
self.db = db
def get_all(self):
"""Retorna todas as tools cadastradas."""
return self.db.query(Tool).all()
def get_by_id(self, tool_id: int):
"""Busca uma tool pelo ID."""
return self.db.query(Tool).filter(Tool.id == tool_id).first()
def create(self, name: str, description: str, parameters: dict):
"""Cria e persiste uma nova tool."""
tool = Tool(
name=name,
description=description,
@ -25,6 +29,7 @@ class ToolRepository:
return tool
def delete(self, tool_id: int):
"""Remove uma tool por ID quando existente."""
tool = self.get_by_id(tool_id)
if tool:
self.db.delete(tool)
@ -32,6 +37,7 @@ class ToolRepository:
return tool
def update_by_name(self, name: str, description: str, parameters: dict):
"""Atualiza descricao e parametros de uma tool encontrada pelo nome."""
tool = self.db.query(Tool).filter(Tool.name == name).first()
if not tool:
return None

@ -10,10 +10,12 @@ from app.db.mock_models import Customer, Order, ReviewSchedule, Vehicle
def normalize_cpf(value: str) -> str:
"""Normaliza CPF removendo qualquer caractere nao numerico."""
return re.sub(r"\D", "", value or "")
def _parse_float(value: Any, default: float = 0.0) -> float:
"""Converte entradas numericas/textuais para float com fallback padrao."""
if value is None:
return default
if isinstance(value, (int, float)):
@ -27,6 +29,7 @@ def _parse_float(value: Any, default: float = 0.0) -> float:
def _stable_int(seed_text: str) -> int:
"""Gera inteiro deterministico a partir de um texto usando hash SHA-256."""
digest = hashlib.sha256(seed_text.encode("utf-8")).hexdigest()
return int(digest[:16], 16)
@ -37,6 +40,7 @@ async def consultar_estoque(
ordenar_preco: Optional[str] = None,
limite: Optional[int] = None,
) -> List[Dict[str, Any]]:
"""Consulta veiculos no estoque com filtros opcionais e ordenacao por preco."""
db = SessionMockLocal()
try:
query = db.query(Vehicle)
@ -71,6 +75,7 @@ async def consultar_estoque(
async def validar_cliente_venda(cpf: str, valor_veiculo: float) -> Dict[str, Any]:
"""Avalia aprovacao de compra com base em score, limite e restricao do cliente."""
cpf_norm = normalize_cpf(cpf)
db = SessionMockLocal()
try:
@ -103,6 +108,7 @@ async def validar_cliente_venda(cpf: str, valor_veiculo: float) -> Dict[str, Any
async def avaliar_veiculo_troca(modelo: str, ano: int, km: int) -> Dict[str, Any]:
"""Calcula valor estimado de troca usando depreciacao por ano e quilometragem."""
ano_atual = datetime.now().year
idade = max(0, ano_atual - ano)
base = 80000.0
@ -117,6 +123,7 @@ 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]:
"""Cria ou reaproveita agendamento de revisao a partir de placa e data/hora."""
try:
dt = datetime.fromisoformat(data_hora.replace("Z", "+00:00"))
except ValueError:
@ -160,6 +167,7 @@ async def agendar_revisao(placa: str, data_hora: str) -> Dict[str, Any]:
async def cancelar_pedido(numero_pedido: str, motivo: str) -> Dict[str, Any]:
"""Cancela pedido existente e registra motivo e data de cancelamento."""
db = SessionMockLocal()
try:
pedido = db.query(Order).filter(Order.numero_pedido == numero_pedido).first()

@ -1,7 +1,9 @@
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 vertexai.generative_models import FunctionDeclaration, GenerativeModel, Tool
from app.core.settings import settings
from app.models.tool_model import ToolDefinition
@ -9,9 +11,10 @@ from app.models.tool_model import ToolDefinition
class LLMService:
def __init__(self):
"""Inicializa o cliente Vertex AI e define modelos de fallback."""
vertexai.init(
project=settings.google_project_id,
location=settings.google_location
location=settings.google_location,
)
configured = settings.vertex_model_name.strip()
@ -19,8 +22,8 @@ class LLMService:
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.
"""Converte tools internas para o formato esperado pelo Vertex AI."""
# Vertex espera uma lista de Tool, com function_declarations agrupadas em um unico Tool.
if not tools:
return None
@ -35,26 +38,15 @@ class LLMService:
return [Tool(function_declarations=function_declarations)]
"""
Fluxo principal de geração de resposta.
Parâmetros:
- message: mensagem do usuário
- tools: lista de ferramentas disponíveis
- history: histórico da conversa (memória)
"""
async def generate_response(
self,
message: str,
tools: List[ToolDefinition],
history: List[Dict[str, Any]] = None
history: List[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Gera resposta textual ou chamada de tool a partir da mensagem do usuario."""
vertex_tools = self.build_vertex_tools(tools)
vertex_tools = self.build_vertex_tools(tools) # Convertendo tools para formato do Vertex
# Inicia uma sessão de chat com:
# - histórico (se existir)
# - ferramentas disponíveis
response = None
last_error = None
@ -72,28 +64,22 @@ class LLMService:
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}"
f"Nenhum modelo Vertex disponivel. 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:
# response.candidates -> lista de possíveis respostas
# content.parts -> partes da resposta
part = response.candidates[0].content.parts[0]
# Verificação se o modelo decidiu chamar alguma função, se decidiu, retornará o nome da função que ele quer executar e o argumento que ele extraiu da mensagem do usuário.
if part.function_call:
return {
"response": None,
"tool_call": {
"name": part.function_call.name,
"arguments": dict(part.function_call.args)
}
"arguments": dict(part.function_call.args),
},
}
# Caso não ocorra a chamada de uma função, significa que o modelo respondeu diretamente em texto
return {
"response": response.text,
"tool_call": None
"tool_call": None,
}

@ -7,18 +7,12 @@ from app.services.tool_registry import ToolRegistry
class OrquestradorService:
def __init__(self, db: Session):
"""Inicializa servicos de LLM e registro de tools para a sessao atual."""
self.llm = LLMService()
self.registry = ToolRegistry(db)
"""
Metodo principal chamado quando o usuario envia uma mensagem.
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:
"""Processa mensagem, executa tool quando necessario e retorna resposta final."""
tools = self.registry.get_tools()

@ -1,15 +1,15 @@
from typing import List, Dict, Callable
from typing import Callable, Dict, List
from sqlalchemy.orm import Session
from app.models.tool_model import ToolDefinition
from app.repositories.tool_repository import ToolRepository
from app.services.handlers import (
consultar_estoque,
validar_cliente_venda,
avaliar_veiculo_troca,
agendar_revisao,
avaliar_veiculo_troca,
cancelar_pedido,
consultar_estoque,
validar_cliente_venda,
)
@ -25,6 +25,7 @@ HANDLERS: Dict[str, Callable] = {
class ToolRegistry:
def __init__(self, db: Session):
"""Carrega tools do banco e registra apenas as que possuem handler conhecido."""
self._tools = []
repo = ToolRepository(db)
db_tools = repo.get_all()
@ -39,33 +40,26 @@ class ToolRegistry:
handler=handler,
)
# O método abaixo serve para adicionar uma nova tool ao sistema (podendo inserir a lógica de adcicionar ao BD futuramente).
def register_tool(self, name, description, parameters, handler):
"""Registra uma tool em memoria para uso pelo orquestrador."""
self._tools.append(
ToolDefinition(
name=name,
description=description,
parameters=parameters,
handler=handler
handler=handler,
)
)
# Retorna todas as tools registradas, isso será usado pelo LLMService para enviar as ferramentas para o modelo
def get_tools(self) -> List[ToolDefinition]:
"""Retorna a lista atual de tools registradas."""
return self._tools
"""
Essa função é responsável por executar a tool solicitada pelo modelo.
Parâmetros:
- name: nome da função que o Gemini decidiu chamar
- arguments: argumentos extraídos da mensagem do usuário
"""
async def execute(self, name: str, arguments: dict):
tool = next((t for t in self._tools if t.name == name), None) # Procura dentro da lista de tools aquela que tem o mesmo nome
"""Executa a tool solicitada pelo modelo com os argumentos extraidos."""
tool = next((t for t in self._tools if t.name == name), None)
if not tool:
raise Exception(f"Tool {name} não encontrada.")
raise Exception(f"Tool {name} nao encontrada.")
return await tool.handler(**arguments)

Loading…
Cancel
Save