Fix lint errors

This commit is contained in:
2026-02-20 04:59:56 +00:00
parent faa04a0d01
commit 595abd6cd3
19 changed files with 442 additions and 321 deletions

View File

@@ -1,5 +1,8 @@
"""Conversation manager service for orchestrating user conversations."""
import logging
from datetime import datetime, timedelta
import re
from datetime import UTC, datetime, timedelta
from uuid import uuid4
from capa_de_integracion.config import Settings
@@ -10,6 +13,7 @@ from capa_de_integracion.models import (
DetectIntentResponse,
QueryResult,
)
from capa_de_integracion.models.notification import NotificationSession
from .dlp_service import DLPService
from .firestore_service import FirestoreService
@@ -46,10 +50,10 @@ class ConversationManagerService:
logger.info("ConversationManagerService initialized successfully")
async def manage_conversation(
async def manage_conversation( # noqa: PLR0915
self, request: ConversationRequest,
) -> DetectIntentResponse:
"""Main entry point for managing conversations.
"""Manage conversation flow and return response.
Args:
request: External conversation request from client
@@ -82,10 +86,11 @@ class ConversationManagerService:
# Step 2: Check for pantallaContexto in existing session
if session.pantallaContexto:
# Check if pantallaContexto is stale (10 minutes)
if self._is_pantalla_context_valid(session.lastModified):
if self._is_pantalla_context_valid(session.last_modified):
logger.info(
f"Detected 'pantallaContexto' in session: {session.pantallaContexto}. "
f"Delegating to QuickReplies flow.",
"Detected 'pantallaContexto' in session: %s. "
"Delegating to QuickReplies flow.",
session.pantallaContexto,
)
response = await self._manage_quick_reply_conversation(
request, session.pantallaContexto,
@@ -95,62 +100,64 @@ class ConversationManagerService:
user_entry = ConversationEntry(
entity="user",
type="CONVERSACION",
timestamp=datetime.now(),
timestamp=datetime.now(UTC),
text=request.mensaje,
parameters=None,
canal=getattr(request, "canal", None),
)
await self.firestore_service.save_entry(
session.sessionId, user_entry,
session.session_id, user_entry,
)
# Save quick reply response to Firestore
response_text = (
response.queryResult.responseText
if response.queryResult
response.query_result.response_text
if response.query_result
else ""
) or ""
assistant_entry = ConversationEntry(
entity="assistant",
type="CONVERSACION",
timestamp=datetime.now(),
timestamp=datetime.now(UTC),
text=response_text,
parameters=None,
canal=getattr(request, "canal", None),
)
await self.firestore_service.save_entry(
session.sessionId, assistant_entry,
session.session_id, assistant_entry,
)
# Update session with last message and timestamp
session.lastMessage = response_text
session.lastModified = datetime.now()
session.last_message = response_text
session.last_modified = datetime.now(UTC)
await self.firestore_service.save_session(session)
await self.redis_service.save_session(session)
return response
else:
logger.info(
"Detected STALE 'pantallaContexto'. Ignoring and proceeding with normal flow.",
"Detected STALE 'pantallaContexto'. "
"Ignoring and proceeding with normal flow.",
)
# Step 3: Continue with standard conversation flow
nickname = request.usuario.nickname
logger.info(
f"Primary Check (Redis): Looking up session for phone: {telefono}",
"Primary Check (Redis): Looking up session for phone: %s",
telefono,
)
# Step 3a: Load conversation history from Firestore
entries = await self.firestore_service.get_entries(
session.sessionId,
session.session_id,
limit=self.settings.conversation_context_message_limit,
)
logger.info(f"Loaded {len(entries)} conversation entries from Firestore")
logger.info("Loaded %s conversation entries from Firestore", len(entries))
# Step 3b: Retrieve active notifications for this user
notifications = await self._get_active_notifications(telefono)
logger.info(f"Retrieved {len(notifications)} active notifications")
logger.info("Retrieved %s active notifications", len(notifications))
# Step 3c: Prepare context for RAG service
messages = await self._prepare_rag_messages(
@@ -165,36 +172,37 @@ class ConversationManagerService:
logger.info("Sending query to RAG service")
assistant_response = await self.rag_service.query(messages)
logger.info(
f"Received response from RAG service: {assistant_response[:100]}...",
"Received response from RAG service: %s...",
assistant_response[:100],
)
# Step 3e: Save user message to Firestore
user_entry = ConversationEntry(
entity="user",
type="CONVERSACION",
timestamp=datetime.now(),
timestamp=datetime.now(UTC),
text=request.mensaje,
parameters=None,
canal=getattr(request, "canal", None),
)
await self.firestore_service.save_entry(session.sessionId, user_entry)
await self.firestore_service.save_entry(session.session_id, user_entry)
logger.info("Saved user message to Firestore")
# Step 3f: Save assistant response to Firestore
assistant_entry = ConversationEntry(
entity="assistant",
type="LLM",
timestamp=datetime.now(),
timestamp=datetime.now(UTC),
text=assistant_response,
parameters=None,
canal=getattr(request, "canal", None),
)
await self.firestore_service.save_entry(session.sessionId, assistant_entry)
await self.firestore_service.save_entry(session.session_id, assistant_entry)
logger.info("Saved assistant response to Firestore")
# Step 3g: Update session with last message and timestamp
session.lastMessage = assistant_response
session.lastModified = datetime.now()
session.last_message = assistant_response
session.last_modified = datetime.now(UTC)
await self.firestore_service.save_session(session)
await self.redis_service.save_session(session)
logger.info("Updated session in Firestore and Redis")
@@ -202,26 +210,26 @@ class ConversationManagerService:
# Step 3h: Mark notifications as processed if any were included
if notifications:
await self._mark_notifications_as_processed(telefono)
logger.info(f"Marked {len(notifications)} notifications as processed")
logger.info("Marked %s notifications as processed", len(notifications))
# Step 3i: Return response object
return DetectIntentResponse(
responseId=str(uuid4()),
queryResult=QueryResult(
responseText=assistant_response,
response_id=str(uuid4()),
query_result=QueryResult(
response_text=assistant_response,
parameters=None,
),
quick_replies=None,
)
except Exception as e:
logger.error(f"Error managing conversation: {e!s}", exc_info=True)
except Exception:
logger.exception("Error managing conversation")
raise
def _is_pantalla_context_valid(self, last_modified: datetime) -> bool:
"""Check if pantallaContexto is still valid (not stale)."""
time_diff = datetime.now() - last_modified
time_diff = datetime.now(UTC) - last_modified
return time_diff < timedelta(minutes=self.SCREEN_CONTEXT_TIMEOUT_MINUTES)
async def _manage_quick_reply_conversation(
@@ -234,7 +242,7 @@ class ConversationManagerService:
# If no questions available, delegate to normal conversation flow
if not quick_reply_screen.preguntas:
logger.warning(f"No quick replies found for screen: {screen_id}.")
logger.warning("No quick replies found for screen: %s.", screen_id)
return None
# Match user message to a quick reply question
@@ -245,19 +253,20 @@ class ConversationManagerService:
# Simple matching: check if question title matches user message
if pregunta.titulo.lower().strip() == user_message_lower:
matched_answer = pregunta.respuesta
logger.info(f"Matched quick reply: {pregunta.titulo}")
logger.info("Matched quick reply: %s", pregunta.titulo)
break
# If no match, use first question as default or delegate to normal flow
if not matched_answer:
logger.warning(
f"No matching quick reply found for message: '{request.mensaje}'.",
"No matching quick reply found for message: '%s'.",
request.mensaje,
)
# Create response with the matched quick reply answer
return DetectIntentResponse(
responseId=str(uuid4()),
queryResult=QueryResult(responseText=matched_answer, parameters=None),
response_id=str(uuid4()),
query_result=QueryResult(response_text=matched_answer, parameters=None),
quick_replies=quick_reply_screen,
)
@@ -281,8 +290,6 @@ class ConversationManagerService:
# If not in Redis, try Firestore
if not notification_session:
# Firestore uses phone as document ID for notifications
from capa_de_integracion.models.notification import NotificationSession
doc_ref = self.firestore_service.db.collection(
self.firestore_service.notifications_collection,
).document(telefono)
@@ -294,20 +301,19 @@ class ConversationManagerService:
# Filter for active notifications only
if notification_session and notification_session.notificaciones:
return [
active_notifications = [
notif
for notif in notification_session.notificaciones
if notif.status == "active"
]
else:
active_notifications = []
except Exception:
logger.exception("Error retrieving notifications for %s", telefono)
return []
except Exception as e:
logger.error(
f"Error retrieving notifications for {telefono}: {e!s}",
exc_info=True,
)
return []
else:
return active_notifications
async def _prepare_rag_messages(
self,
@@ -339,7 +345,10 @@ class ConversationManagerService:
messages.append(
{
"role": "system",
"content": f"Historial de conversación:\n{conversation_context}",
"content": (
f"Historial de conversación:\n"
f"{conversation_context}"
),
},
)
@@ -353,7 +362,10 @@ class ConversationManagerService:
messages.append(
{
"role": "system",
"content": f"Notificaciones pendientes para el usuario:\n{notifications_text}",
"content": (
f"Notificaciones pendientes para el usuario:\n"
f"{notifications_text}"
),
},
)
@@ -382,17 +394,17 @@ class ConversationManagerService:
# Update or delete from Redis
await self.redis_service.delete_notification_session(telefono)
logger.info(f"Marked notifications as processed for {telefono}")
logger.info("Marked notifications as processed for %s", telefono)
except Exception as e:
logger.error(
f"Error marking notifications as processed for {telefono}: {e!s}",
exc_info=True,
except Exception:
logger.exception(
"Error marking notifications as processed for %s",
telefono,
)
def _format_conversation_history(
self,
session: ConversationSession,
session: ConversationSession, # noqa: ARG002
entries: list[ConversationEntry],
) -> str:
"""Format conversation history with business rule limits.
@@ -414,7 +426,7 @@ class ConversationManagerService:
return ""
# Filter by date (30 days)
cutoff_date = datetime.now() - timedelta(
cutoff_date = datetime.now(UTC) - timedelta(
days=self.settings.conversation_context_days_limit,
)
recent_entries = [
@@ -445,7 +457,7 @@ class ConversationManagerService:
if not entries:
return ""
MAX_BYTES = 50 * 1024 # 50KB
max_bytes = 50 * 1024 # 50KB
formatted_messages = [self._format_entry(entry) for entry in entries]
# Build from newest to oldest
@@ -456,7 +468,7 @@ class ConversationManagerService:
message_line = message + "\n"
message_bytes = len(message_line.encode("utf-8"))
if current_size + message_bytes > MAX_BYTES:
if current_size + message_bytes > max_bytes:
break
text_block.insert(0, message_line)
@@ -481,8 +493,6 @@ class ConversationManagerService:
content = entry.text
if entry.entity == "assistant":
# Remove trailing JSON artifacts like {...}
import re
content = re.sub(r"\s*\{.*\}\s*$", "", content).strip()
return prefix + content