fix: PEP8 Style

This commit is contained in:
2026-03-04 16:20:56 -05:00
parent aab046b2ef
commit 6fc6338413
17 changed files with 1086 additions and 217 deletions

833
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,9 @@ dependencies = [
"dotenv (>=0.9.9,<0.10.0)", "dotenv (>=0.9.9,<0.10.0)",
"fastapi[standard] (>=0.128.5,<0.129.0)", "fastapi[standard] (>=0.128.5,<0.129.0)",
"langgraph-checkpoint-postgres (>=3.0.4,<4.0.0)", "langgraph-checkpoint-postgres (>=3.0.4,<4.0.0)",
"psycopg[binary] (>=3.3.3,<4.0.0)" "psycopg[binary] (>=3.3.3,<4.0.0)",
"fastmcp (>=3.1.0,<4.0.0)",
"flake8 (>=7.3.0,<8.0.0)"
] ]
[tool.poetry] [tool.poetry]

View File

@@ -6,24 +6,22 @@ from langchain_core.runnables import RunnableConfig
from typing import Literal, Callable, Any, Union from typing import Literal, Callable, Any, Union
from .schemas import MessagesState from .schemas import MessagesState
from dataclasses import dataclass from dataclasses import dataclass
import logging
@dataclass @dataclass
class AgentConfig: class AgentConfig:
"""Configuración inmutable del agente para fácil testing.""" """Configuración inmutable del agente para fácil testing."""
system_prompt: str = ( system_prompt: str = (
"helpful assistant that can call tools when needed. Always respond with a message. " "helpful assistant that can call tools when needed."
" Always respond with a message."
) )
max_iterations: int = 10 max_iterations: int = 200
timeout_seconds: float = 30.0 timeout_seconds: float = 30.0
class Agent: class Agent:
""" """
Agente conversacional basado en LangGraph. Agente conversacional basado en LangGraph.
Soporta tools pasadas como lista de instancias. Soporta tools pasadas como lista de instancias.
""" """
@@ -73,7 +71,9 @@ class Agent:
"""Expone el grafo para inspección en tests.""" """Expone el grafo para inspección en tests."""
return self._build_agent() return self._build_agent()
async def ainvoke(self, state: dict, config: RunnableConfig | None = None) -> dict: async def ainvoke(
self, state: dict, config: RunnableConfig | None = None
) -> dict:
""" """
Versión asíncrona de invoke, necesaria para checkpointers de DB. Versión asíncrona de invoke, necesaria para checkpointers de DB.
""" """
@@ -86,7 +86,9 @@ class Agent:
# Llamamos al método ainvoke del grafo compilado # Llamamos al método ainvoke del grafo compilado
return await self._compiled_agent.ainvoke(state, config=config) return await self._compiled_agent.ainvoke(state, config=config)
def invoke(self, state: dict, config: RunnableConfig | None = None) -> dict: def invoke(
self, state: dict, config: RunnableConfig | None = None
) -> dict:
""" """
Ejecuta el agente con el estado inicial. Ejecuta el agente con el estado inicial.

View File

@@ -1,7 +1,7 @@
prompts: prompts:
NALIIA_PROMPT: | NALIIA_PROMPT: |
""" """
Eres Naliia, asistente virtual de un centro de belleza. Eres Naliia, asistente virtual.
Responde usando formato Markdown: Responde usando formato Markdown:
- Usa **negrita** para énfasis. - Usa **negrita** para énfasis.
@@ -9,19 +9,11 @@ prompts:
- Usa saltos de línea entre secciones. - Usa saltos de línea entre secciones.
- Destaca opciones importantes con `código inline`. - Destaca opciones importantes con `código inline`.
Ejemplo de formato: *Lo que NUNCA debes hacer:*
¡Hola! Bienvenido/a, soy **Naliia**, tu asistente virtual.
Puedo ayudarte con: ❌ Dar diagnósticos médicos
❌ Recomendar tratamientos clínicos sin evaluación
**Agendar una cita** ❌ Minimizar riesgos ("es super seguro, no pasa nada")
Ver horarios disponibles y reservar tu servicio ❌ Prometer resultados específicos
❌ Agendar procedimientos invasivos sin mencionar evaluación previa
**Información sobre servicios y productos**
Conocer nuestros tratamientos y productos
**Cancelar una cita**
Si necesitas cancelar alguna reserva previa
¿En qué puedo ayudarte hoy?
""" """

View File

@@ -1,67 +1,70 @@
import asyncio
from datetime import datetime
from fastmcp import Client
from langchain.tools import tool from langchain.tools import tool
from ..logger import logger from ..logger import logger
from .schemes import ScheduleSchema
MCP_SERVER_URL = "http://localhost:8000/mcp"
_MCP_CLIENT = Client(MCP_SERVER_URL)
class NaliiaTools: class NaliiaTools:
def __init__(self):
self._session_active = False
def get_tools(self): def get_tools(self):
return [ return [
self.verificar_usuario_registrado, self.get_current_datetime,
self.registrar_usuario_nuevo, self.schedule_appointment
self.consultar_sedes,
self.consultar_agenda_disponible,
self.agendar_cita
] ]
@tool @tool
def verificar_usuario_registrado(phone: str) -> bool: def get_current_datetime() -> datetime:
""" """
Verifica si el usuario esta registrado. Consulta la fecha actual, no se aceptan fechas
en el pasado con respecto a esta fecha.
Args:
phone: Numero de contacto del cliente ejemplo 30123334
""" """
logger.info("Llamando a Verificar usuario.") return datetime.now()
return False @tool(args_schema=ScheduleSchema)
def schedule_appointment(
@tool schedule_date, schedule_time,
def registrar_usuario_nuevo(full_name: str, phone: str) -> bool: service_center, customer,
professional, description
) -> bool:
""" """
En caso de que no se haya podido verificar el usuario, sera necesario Permite al Cliente al Agendar una Cita
registrarlo como un usuario nuevo.
Args:
full_name: Nombre completo del cliente.
phone: Numero de contacto del cliente.
"""
logger.info("Llamando a Registrar Nuevo Usuario.")
return True
@tool
def consultar_sedes() -> list:
"""
Brinda informacion al usuario acerca de las sedes disponibles y sus horarios.
"""
logger.info("Llamando a consultar sedes.")
return []
@tool
def consultar_agenda_disponible() -> list:
"""
Ayuda a consultar al cliente los horarios de atencion disponibles para agendar su cita.
"""
logger.info("Llamando a consultar agenda disponible.")
return []
@tool
def agendar_cita() -> bool:
"""
Ayuda al cliente a confirmar su cita.
""" """
logger.info("Llamando a agendar cita.") logger.info("Llamando a agendar cita.")
logger.info([
schedule_date,
schedule_time,
service_center,
customer,
professional,
description
])
example_schedule = {
'professional': 6,
'description': "Bien Bonito Todo!",
'customer': 4,
'date': '2026-03-25 14:00',
'service_center': 11
}
async def call_tool():
async with _MCP_CLIENT:
result = await _MCP_CLIENT.call_tool(
"create_schedule", example_schedule)
logger.info(result)
asyncio.run(call_tool())
return True return True

View File

@@ -0,0 +1,37 @@
from datetime import date, time
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
class ScheduleSchema(BaseModel):
"""
Scheme for Schedule Appointment
"""
schedule_date: date = Field(
description="Date in format DD-MM-YYYY")
schedule_time: time = Field(
description="Time in format 12h (HH:MM)")
service_center: Optional[PositiveInt] = Field(
None, description="ID Service center Example: 1")
customer: Optional[PositiveInt] = Field(
None, description="ID Customer Example: 1")
professional: Optional[PositiveInt] = Field(
None, description="ID Professional Example: 1")
description: str = Field(
default="General Schedule",
max_length=255,
description="Max 255 characters."
)
class Config:
json_schema_extra = {
"example": {
"schedule_date": "15-03-2026",
"schedule_time": "02:00",
"service_center": 1,
"professional": 12,
"customer": 999,
"description": "Review"
}
}

View File

@@ -1,17 +1,12 @@
from naliiabot.bot.agent.agent import Agent
import os
from naliiabot.bot.agent.agent import Agent, AgentConfig
from naliiabot.bot.tools.naliia_tools import NaliiaTools
from naliiabot.bot.factories.llm_factory import LLMFactory
from naliiabot.bot.prompts.load_prompt import get_prompt_template
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from psycopg_pool import AsyncConnectionPool from psycopg_pool import AsyncConnectionPool
from psycopg.rows import dict_row from psycopg.rows import dict_row
from ..logger import logger from .settings import _settings as st
from .settings import _settings
_connection_string = f"postgresql://{_settings.DB_USER}:{_settings.DB_PASSWORD}@localhost:5432/{_settings.DB_NAME}" _protocol = "postgresql://"
_connection_string =\
f"{_protocol}{st.DB_USER}:{st.DB_PASSWORD}@localhost:5432/{st.DB_NAME}"
agent_instance: Agent | None = None agent_instance: Agent | None = None
db_pool = None db_pool = None

View File

@@ -4,6 +4,7 @@ from langchain_core.messages import HumanMessage
router = APIRouter() router = APIRouter()
@router.post("/chat") @router.post("/chat")
def chat(messages: dict, agent=Depends(get_agent)): def chat(messages: dict, agent=Depends(get_agent)):
""" """
@@ -16,7 +17,12 @@ def chat(messages: dict, agent = Depends(get_agent)):
dict: A dictionary containing the response message. dict: A dictionary containing the response message.
""" """
messages = [HumanMessage(content=messages["messages"])] messages = [
response_message = agent.invoke({"messages": messages}) HumanMessage(
content=messages["messages"]
)]
response_message = agent.invoke(
{"messages": messages}
)
return {"response": response_message} return {"response": response_message}

View File

@@ -9,6 +9,7 @@ import json
router = APIRouter() router = APIRouter()
async def send_whatsapp_message(request_data: SendMessageScheme): async def send_whatsapp_message(request_data: SendMessageScheme):
""" """
Envía un mensaje de texto a través de la API de WhatsApp. Envía un mensaje de texto a través de la API de WhatsApp.
@@ -35,8 +36,13 @@ async def send_whatsapp_message(request_data: SendMessageScheme):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
response = await client.post(endpoint, json=payload, headers=headers) response = await client.post(
endpoint,
json=payload,
headers=headers
)
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
except Exception as e: except Exception as e:
logger.error(f"Error enviando mensaje a WhatsApp: {e}") logger.error(f"Error enviando mensaje a WhatsApp: {e}")
@@ -74,7 +80,9 @@ async def webhook_chat(request: Request, agent = Depends(get_agent)):
messages = [HumanMessage(content=user_message)] messages = [HumanMessage(content=user_message)]
agent_response = await agent.ainvoke({"messages": messages}, config=config) agent_response = await agent.ainvoke(
{"messages": messages}, config=config
)
agent_response_content = agent_response["messages"][-1].content agent_response_content = agent_response["messages"][-1].content
clean_jid = thread_id.split('@')[0] clean_jid = thread_id.split('@')[0]

View File

@@ -16,10 +16,10 @@ async def lifespan(app: FastAPI):
await checkpointer.setup() await checkpointer.setup()
model_name = "anthropic" model_name = "anthropic"
model = deps.LLMFactory(model_name).get_model()
naliia_prompt = deps.get_prompt_template("NALIIA_PROMPT") naliia_prompt = deps.get_prompt_template("NALIIA_PROMPT")
naliia_tools = deps.NaliiaTools().get_tools() naliia_tools = deps.NaliiaTools().get_tools()
config = deps.AgentConfig(system_prompt=naliia_prompt) config = deps.AgentConfig(system_prompt=naliia_prompt)
model = deps.LLMFactory(model_name).get_model()
deps.agent_instance = deps.Agent( deps.agent_instance = deps.Agent(
model=model, model=model,
@@ -35,7 +35,9 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title="NaliiaBot API", title="NaliiaBot API",
description="API for NaliiaBot, a chatbot that provides customer service and related topics.", description=(
"API for NaliiaBot, a chatbot that provides "
"customer service and related topics."),
version="1.0.0", version="1.0.0",
lifespan=lifespan lifespan=lifespan
) )
@@ -43,10 +45,12 @@ app = FastAPI(
app.include_router(chat_router) app.include_router(chat_router)
app.include_router(chat_hook_router) app.include_router(chat_hook_router)
@app.get("/") @app.get("/")
def read_root(): def read_root():
return {"Hello": "World"} return {"Hello": "World"}
@app.get("/health") @app.get("/health")
def health_check(): def health_check():
return {"status": "ok"} return {"status": "ok"}

View File

@@ -1,10 +1,8 @@
import streamlit as st import streamlit as st
from agent_client import AgentClient from agent_client import AgentClient
from logger import logger import asyncio
import json import json
import httpx import httpx
import random
import time
async def main(): async def main():
@@ -18,7 +16,6 @@ async def main():
with st.chat_message(message["role"]): with st.chat_message(message["role"]):
st.markdown(message["content"]) st.markdown(message["content"])
if prompt := st.chat_input("What is up?"): if prompt := st.chat_input("What is up?"):
st.chat_message("user").markdown(prompt) st.chat_message("user").markdown(prompt)
st.session_state.messages.append({"role": "user", "content": prompt}) st.session_state.messages.append({"role": "user", "content": prompt})
@@ -26,10 +23,8 @@ async def main():
agent_client = AgentClient() agent_client = AgentClient()
response = await agent_client.send_message(prompt) response = await agent_client.send_message(prompt)
try: try:
response.raise_for_status() response.raise_for_status()
data = response.json()
except json.JSONDecodeError: except json.JSONDecodeError:
raise RuntimeError( raise RuntimeError(
f"Respuesta inválida (no JSON): {response.text}" f"Respuesta inválida (no JSON): {response.text}"
@@ -41,8 +36,10 @@ async def main():
raw_response = response.json()['response']['messages'][-1]['content'] raw_response = response.json()['response']['messages'][-1]['content']
with st.chat_message("assistant"): st.session_state.messages.append({
formated_response = st.markdown(raw_response) "role": "assistant",
st.session_state.messages.append({"role": "assistant", "content": raw_response}) "content": raw_response
})
if __name__ == "__main__": import asyncio; asyncio.run(main()) if __name__ == "__main__":
asyncio.run(main())

View File

@@ -6,13 +6,15 @@ from src.naliiabot.bot.factories.llm_factory import LLMFactory
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from naliiabotapi.main import app from naliiabotapi.main import app
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
async def async_client(): async def async_client():
"""Fixture para un cliente HTTP asíncrono.""" """Fixture para un cliente HTTP asíncrono."""
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=app), transport=ASGITransport(app=app),
base_url="http://localhost:8010") as client: base_url="http://localhost:8010"
) as client:
yield client yield client

View File

@@ -375,8 +375,9 @@ class TestIntegrationFlows:
assert "messages" in result assert "messages" in result
assert "llm_calls" in result assert "llm_calls" in result
assert result["llm_calls"] == 1 assert result["llm_calls"] == 1
assert len(result["messages"]) == 2 # Input + Response assert len(result["messages"]) == 2
assert result["messages"][-1].content == "¡Hola! ¿En qué puedo ayudarte?" assert result["messages"][-1].content == (
"¡Hola! ¿En qué puedo ayudarte?")
def test_tool_use_flow(self, agent_with_tool): def test_tool_use_flow(self, agent_with_tool):
""" """

View File

@@ -1,7 +1,6 @@
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from src.naliiabotapi.main import app from src.naliiabotapi.main import app
from langchain_core.messages import HumanMessage
@pytest.fixture @pytest.fixture
@@ -10,16 +9,19 @@ def client():
return client return client
def test_root(client): def test_root(client):
response = client.get("/") response = client.get("/")
assert response.status_code == 200 assert response.status_code == 200
def test_health(client): def test_health(client):
response = client.get("/health") response = client.get("/health")
assert response.status_code == 200 assert response.status_code == 200
def test_chat(client): def test_chat(client):
response = client.post("/chat", json={"messages": "Hello, how are you?"}) response = client.post("/chat", json={"messages": "Hello, how are you?"})

View File

@@ -26,7 +26,10 @@ class TestWebhookChatBot:
# Sobrescribir la dependencia del agente en la app # Sobrescribir la dependencia del agente en la app
app.dependency_overrides[get_agent] = lambda: mock_agent app.dependency_overrides[get_agent] = lambda: mock_agent
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://localhost:8010") as client: async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://localhost:8010"
) as client:
response = await client.post("/webhook", json=send_message_payload) response = await client.post("/webhook", json=send_message_payload)
# Limpiar override para no afectar a otros tests # Limpiar override para no afectar a otros tests