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

View File

@@ -6,24 +6,22 @@ from langchain_core.runnables import RunnableConfig
from typing import Literal, Callable, Any, Union
from .schemas import MessagesState
from dataclasses import dataclass
import logging
@dataclass
class AgentConfig:
"""Configuración inmutable del agente para fácil testing."""
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
class Agent:
"""
Agente conversacional basado en LangGraph.
Soporta tools pasadas como lista de instancias.
"""
@@ -36,7 +34,7 @@ class Agent:
):
"""
Inicializa el agente.
Args:
model: Modelo LLM a usar
config: Configuración del agente (AgentConfig)
@@ -49,7 +47,7 @@ class Agent:
self._config = config or AgentConfig()
self._tools = tools or []
self._checkpointer = checkpointer or InMemorySaver()
self._tools_by_name: dict[str, Any] = {
tool.name: tool for tool in self._tools
}
@@ -73,7 +71,9 @@ class Agent:
"""Expone el grafo para inspección en tests."""
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.
"""
@@ -86,7 +86,9 @@ class Agent:
# Llamamos al método ainvoke del grafo compilado
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.
@@ -98,7 +100,7 @@ class Agent:
"""
if not config:
config : RunnableConfig = {"configurable": {"thread_id": "1"}}
config: RunnableConfig = {"configurable": {"thread_id": "1"}}
if "messages" not in state:
raise ValueError("State must contain 'messages' key")

View File

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

View File

@@ -1,67 +1,70 @@
import asyncio
from datetime import datetime
from fastmcp import Client
from langchain.tools import tool
from ..logger import logger
from .schemes import ScheduleSchema
MCP_SERVER_URL = "http://localhost:8000/mcp"
_MCP_CLIENT = Client(MCP_SERVER_URL)
class NaliiaTools:
def __init__(self):
self._session_active = False
def get_tools(self):
return [
self.verificar_usuario_registrado,
self.registrar_usuario_nuevo,
self.consultar_sedes,
self.consultar_agenda_disponible,
self.agendar_cita
self.get_current_datetime,
self.schedule_appointment
]
@tool
def verificar_usuario_registrado(phone: str) -> bool:
def get_current_datetime() -> datetime:
"""
Verifica si el usuario esta registrado.
Args:
phone: Numero de contacto del cliente ejemplo 30123334
Consulta la fecha actual, no se aceptan fechas
en el pasado con respecto a esta fecha.
"""
logger.info("Llamando a Verificar usuario.")
return datetime.now()
return False
@tool
def registrar_usuario_nuevo(full_name: str, phone: str) -> bool:
@tool(args_schema=ScheduleSchema)
def schedule_appointment(
schedule_date, schedule_time,
service_center, customer,
professional, description
) -> bool:
"""
En caso de que no se haya podido verificar el usuario, sera necesario
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.
Permite al Cliente al Agendar una 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

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 @@
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 naliiabot.bot.agent.agent import Agent
from psycopg_pool import AsyncConnectionPool
from psycopg.rows import dict_row
from ..logger import logger
from .settings import _settings
from .settings import _settings as st
_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
db_pool = None

View File

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

View File

@@ -9,6 +9,7 @@ import json
router = APIRouter()
async def send_whatsapp_message(request_data: SendMessageScheme):
"""
Envía un mensaje de texto a través de la API de WhatsApp.
@@ -32,11 +33,16 @@ async def send_whatsapp_message(request_data: SendMessageScheme):
"delay": delay,
"linkPreview": False
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(endpoint, json=payload, headers=headers)
response = await client.post(
endpoint,
json=payload,
headers=headers
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Error enviando mensaje a WhatsApp: {e}")
@@ -44,7 +50,7 @@ async def send_whatsapp_message(request_data: SendMessageScheme):
@router.post("/webhook")
async def webhook_chat(request: Request, agent = Depends(get_agent)):
async def webhook_chat(request: Request, agent=Depends(get_agent)):
logger.info("Received webhook request")
body = await request.json()
logger.info(f"Webhook payload: {json.dumps(body)}")
@@ -74,9 +80,11 @@ async def webhook_chat(request: Request, agent = Depends(get_agent)):
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
clean_jid = thread_id.split('@')[0]
await send_whatsapp_message(
@@ -87,10 +95,10 @@ async def webhook_chat(request: Request, agent = Depends(get_agent)):
jid=clean_jid,
text=agent_response_content,
delay=1200
))
))
except Exception as e:
logger.error(f"Error processing webhook: {e}")
return {"error": str(e)}
return {"status": "sent", "reply": agent_response_content}

View File

@@ -16,10 +16,10 @@ async def lifespan(app: FastAPI):
await checkpointer.setup()
model_name = "anthropic"
model = deps.LLMFactory(model_name).get_model()
naliia_prompt = deps.get_prompt_template("NALIIA_PROMPT")
naliia_tools = deps.NaliiaTools().get_tools()
config = deps.AgentConfig(system_prompt=naliia_prompt)
model = deps.LLMFactory(model_name).get_model()
deps.agent_instance = deps.Agent(
model=model,
@@ -35,7 +35,9 @@ async def lifespan(app: FastAPI):
app = FastAPI(
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",
lifespan=lifespan
)
@@ -43,10 +45,12 @@ app = FastAPI(
app.include_router(chat_router)
app.include_router(chat_hook_router)
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/health")
def health_check():
return {"status": "ok"}

View File

@@ -7,4 +7,4 @@ class SendMessageScheme(TypedDict):
apikey: str
jid: str
delay: int = 1200
text: str
text: str

View File

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

View File

@@ -4,4 +4,4 @@ logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)