Files
HermesMessages/backend/app/modules/bot_engine/service.py

156 lines
5.2 KiB
Python
Raw Normal View History

import json
import logging
from datetime import date, time
import anthropic
import redis.asyncio as aioredis
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.modules.bot_engine.prompt import build_system_prompt
from app.modules.bot_engine.schemas import BotResponse, CollectedData, ConversationContext
from app.modules.business.models import Business
2026-04-29 09:39:56 -05:00
from app.modules.business.service import get_business_config, list_services, list_table_types
from app.modules.calendar.service import get_available_slots, get_available_slots_for_party
from app.modules.reservations.schemas import ReservationCreate
from app.modules.reservations.service import create_reservation
from app.modules.whatsapp.client import send_text_message
logger = logging.getLogger(__name__)
CONTEXT_TTL = 1800 # 30 minutos
MODEL = "claude-sonnet-4-20250514"
_anthropic = anthropic.AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
def _context_key(business_id: int, phone: str) -> str:
return f"conv:{business_id}:{phone}"
async def _load_context(redis: aioredis.Redis, business_id: int, phone: str) -> ConversationContext:
raw = await redis.get(_context_key(business_id, phone))
if raw:
return ConversationContext.model_validate_json(raw)
return ConversationContext(phone=phone, business_id=business_id)
async def _save_context(redis: aioredis.Redis, context: ConversationContext) -> None:
await redis.setex(
_context_key(context.business_id, context.phone),
CONTEXT_TTL,
context.model_dump_json(),
)
async def _clear_context(redis: aioredis.Redis, business_id: int, phone: str) -> None:
await redis.delete(_context_key(business_id, phone))
async def _call_claude(system_prompt: str, messages: list[dict]) -> BotResponse:
response = await _anthropic.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt,
messages=messages,
)
raw_text = response.content[0].text.strip()
try:
data = json.loads(raw_text)
return BotResponse.model_validate(data)
except Exception:
logger.warning("Claude devolvió JSON inválido: %s", raw_text)
return BotResponse(
message=raw_text,
action="collect_more",
collected_data=CollectedData(),
)
async def _handle_create_reservation(
db: AsyncSession,
redis: aioredis.Redis,
business: Business,
phone: str,
bot_response: BotResponse,
) -> None:
cd = bot_response.collected_data
if not all([cd.client_name, cd.date, cd.time_start, cd.party_size]):
return
await create_reservation(
db=db,
redis=redis,
business_id=business.id,
data=ReservationCreate(
client_name=cd.client_name,
client_phone=phone,
date=date.fromisoformat(cd.date),
time_start=time.fromisoformat(cd.time_start),
party_size=cd.party_size,
source="whatsapp",
),
)
await _clear_context(redis, business.id, phone)
async def process_message(
db: AsyncSession,
phone: str,
text: str,
business: Business,
) -> None:
redis: aioredis.Redis = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
try:
context = await _load_context(redis, business.id, phone)
config = await get_business_config(db, business.id)
2026-04-29 09:39:56 -05:00
table_types = await list_table_types(db, business.id)
services = await list_services(db, business.id)
availability = None
if context.collected_data.date:
try:
2026-04-29 09:39:56 -05:00
party_size = context.collected_data.party_size or 1
if table_types:
availability = await get_available_slots_for_party(
db, redis, business.id,
date.fromisoformat(context.collected_data.date),
party_size,
)
else:
availability = await get_available_slots(
db, redis, business.id, date.fromisoformat(context.collected_data.date)
)
except Exception:
pass
2026-04-29 09:39:56 -05:00
system_prompt = build_system_prompt(business, config, availability, context, table_types, services)
context.messages.append({"role": "user", "content": text})
bot_response = await _call_claude(system_prompt, context.messages)
context.messages.append({"role": "assistant", "content": bot_response.message})
context.collected_data = bot_response.collected_data
if bot_response.action == "create_reservation":
await _handle_create_reservation(db, redis, business, phone, bot_response)
elif bot_response.action == "cancel":
await _clear_context(redis, business.id, phone)
else:
await _save_context(redis, context)
await send_text_message(
phone_number_id=business.whatsapp_phone_number_id,
access_token=business.whatsapp_access_token,
to=phone,
text=bot_response.message,
)
except Exception as exc:
logger.exception("Error procesando mensaje de %s: %s", phone, exc)
finally:
await redis.aclose()