-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_buffer.py
More file actions
163 lines (144 loc) · 5.9 KB
/
Copy pathapi_buffer.py
File metadata and controls
163 lines (144 loc) · 5.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# api_buffer.py
"""
Módulo de buffer de sesión para JARVI 2.0.
Mantiene la asociación cliente ↔ thread_id en Redis,
almacena el estado de la conversación y coordina la persistencia final.
"""
import os
import json
import uuid
import asyncio
import logging
from typing import Optional, Dict, Any
from pydantic import BaseModel, Field
import redis.asyncio as redis
logger = logging.getLogger("jarvi.buffer")
# -------------------------------
# Modelo de Estado de Sesión
# -------------------------------
class SessionState(BaseModel):
"""Estado completo de la sesión del cliente."""
thread_id: str
whatsapp: str = ""
nombre: str = "Usuario"
contexto_tecnico: Dict[str, Any] = Field(default_factory=dict)
pasos_completados: list = Field(default_factory=list)
fase_actual: str = "inicio"
ultimo_mensaje: str = ""
metadata: Dict[str, Any] = Field(default_factory=dict)
# -------------------------------
# Clase Principal del Buffer
# -------------------------------
class APIBuffer:
def __init__(self):
self.redis_url = os.getenv("REDIS_URL")
if not self.redis_url:
raise RuntimeError("REDIS_URL no configurada")
self.ttl = int(os.getenv("REDIS_TTL", 604800)) # 7 días
self._redis = None
self._lock = asyncio.Lock()
async def _get_redis(self) -> redis.Redis:
"""Obtiene la conexión a Redis (lazy singleton)."""
if self._redis is None:
async with self._lock:
if self._redis is None:
self._redis = redis.from_url(
self.redis_url,
decode_responses=True,
max_connections=10
)
logger.info("Conexión a Redis establecida")
return self._redis
def _build_key(self, identifier: str) -> str:
"""Genera la clave para Redis."""
return f"session:{identifier}"
async def get_or_create_session(
self,
identifier: str,
metadata: Optional[Dict[str, Any]] = None
) -> SessionState:
"""
Obtiene una sesión existente por identificador (whatsapp/número)
o crea una nueva si no existe.
"""
redis_client = await self._get_redis()
key = self._build_key(identifier)
data = await redis_client.get(key)
if data:
try:
state = SessionState(**json.loads(data))
logger.debug(f"Sesión recuperada: {identifier} -> {state.thread_id}")
# Actualizar metadatos si se proporcionan nuevos
if metadata:
state.metadata.update(metadata)
return state
except Exception as e:
logger.error(f"Error al deserializar sesión: {e}")
# Crear nueva sesión
new_thread_id = str(uuid.uuid4())
new_state = SessionState(
thread_id=new_thread_id,
whatsapp=identifier if self._is_whatsapp(identifier) else "",
nombre=metadata.get("nombre", "Usuario") if metadata else "Usuario",
contexto_tecnico={},
metadata=metadata or {}
)
# Guardar en Redis
await redis_client.setex(
key,
self.ttl,
new_state.json()
)
logger.info(f"Nueva sesión creada: {identifier} -> {new_thread_id}")
return new_state
async def update_session(self, state: SessionState) -> bool:
"""
Actualiza el estado de la sesión en Redis.
Si el whatsapp cambió, actualiza también la clave.
"""
redis_client = await self._get_redis()
old_identifier = self._extract_identifier(state)
new_identifier = state.whatsapp or old_identifier
# Si el identificador cambió (por ejemplo, se obtuvo el número),
# eliminamos la clave antigua y creamos la nueva.
if new_identifier != old_identifier and old_identifier:
old_key = self._build_key(old_identifier)
await redis_client.delete(old_key)
logger.debug(f"Clave antigua eliminada: {old_key}")
key = self._build_key(new_identifier)
await redis_client.setex(key, self.ttl, state.json())
logger.debug(f"Sesión actualizada: {key}")
return True
async def finalize_session(self, state: SessionState) -> bool:
"""
Transfiere los datos del buffer a PostgreSQL (persistencia final)
y elimina la clave de Redis.
"""
# Aquí se podría invocar un worker o una función asíncrona
# que inserte en threads, audit_events, telemetry_events, etc.
# Por ahora, solo registramos la acción y borramos la clave.
redis_client = await self._get_redis()
key = self._build_key(state.whatsapp or self._extract_identifier(state))
await redis_client.delete(key)
logger.info(f"Sesión finalizada y eliminada: {key}")
# Llamar a función de persistencia (mock)
await self._persist_to_postgres(state)
return True
async def _persist_to_postgres(self, state: SessionState):
"""
(Mock) En una implementación real, aquí se insertan los datos
en las tablas de PostgreSQL (threads, audit_events, etc.).
"""
logger.info(f"Persistiendo thread_id {state.thread_id} en PostgreSQL")
# Ejemplo de inserción (usando asyncpg o SQLAlchemy)
# ...
def _extract_identifier(self, state: SessionState) -> str:
"""Extrae el identificador principal de la sesión."""
return state.whatsapp or state.metadata.get("whatsapp") or state.metadata.get("number") or ""
def _is_whatsapp(self, value: str) -> bool:
"""Verifica si el valor parece un número de WhatsApp (simple)."""
return len(value) >= 8 and value.isdigit()
# -------------------------------
# Instancia global (singleton)
# -------------------------------
api_buffer = APIBuffer()