fix(ux): Refinamento final do onboarding e correção de loop

Porque foi feita essa alteração?
Resolução de Bug e Melhoria de UX.
- Foi removido o tom excessivamente animado e os exemplos robóticos do onboarding.
- Corrigido o loop de registro onde o usuário não era encontrado no Qdrant logo após o upsert (adicionado cache local em memória).
- Implementado regex flexível para capturar nome e empresa com separadores variados (vírgula, ponto, hífen, espaço).
- Removidas saudações duplicadas no fluxo de análise.
- Melhorado o tratamento de erros na resposta final do agente.

Quais testes foram feitos?
- Execução de pytest tests/test_onboarding.py (12 testes passando).
- Teste manual via Telegram validando o fluxo de registro sem vírgula e a transição direta para o agente.

A alteração gerou um novo teste que precisa ser implementado no pipeline de testes?
Não. Os testes unitários existentes foram atualizados para refletir a mudança nos templates de mensagem, garantindo que o fluxo continue validado.
This commit is contained in:
João Pedro Toledo Goncalves 2026-02-02 18:01:28 -03:00
parent 14865c049f
commit a0476886ab
3 changed files with 56 additions and 45 deletions

View File

@ -20,47 +20,38 @@ logger = logging.getLogger("ArthurOnboarding")
# Brazil timezone # Brazil timezone
BRAZIL_TZ = ZoneInfo("America/Sao_Paulo") BRAZIL_TZ = ZoneInfo("America/Sao_Paulo")
# Welcome message templates - will be randomized # Welcome message templates - will be randomized (NO examples - too robotic)
WELCOME_TEMPLATES = [ WELCOME_TEMPLATES = [
( (
"Olá! {greeting}. 👋\n\n" "Olá! {greeting}. 👋\n\n"
"Essa parece ser a primeira vez que nos falamos. " "Essa parece ser a primeira vez que nos falamos. "
"Pode me confirmar seu <b>Nome Completo</b> e <b>Empresa</b>, por favor?\n\n" "Pode me confirmar seu <b>nome</b> e <b>empresa</b>?"
"Exemplo: <i>João Silva, iT Guys</i>"
), ),
( (
"{greeting}! 👋\n\n" "{greeting}! 👋\n\n"
"Prazer em conhecê-lo! Para que eu possa te atender melhor, " "Prazer! Para te atender melhor, "
"me conta: qual é o seu <b>nome</b> e de qual <b>empresa</b> você fala?\n\n" "me conta seu <b>nome</b> e de qual <b>empresa</b> você fala?"
"Pode responder assim: <i>Maria Santos, Empresa XYZ</i>"
), ),
( (
"E aí! {greeting}. 😊\n\n" "{greeting}. 👋\n\n"
"Ainda não temos seu cadastro por aqui. " "Ainda não temos seu cadastro. "
"Me passa seu <b>nome completo</b> e a <b>empresa</b> que você representa?\n\n" "Qual seu <b>nome</b> e <b>empresa</b>?"
"Formato: <i>Nome Sobrenome, Sua Empresa</i>"
), ),
] ]
# Registration confirmation templates # Registration confirmation templates (natural, conversational)
REGISTRATION_SUCCESS_TEMPLATES = [ REGISTRATION_SUCCESS_TEMPLATES = [
( (
"Perfeito, <b>{first_name}</b>! Obrigado pelas informações. ✅\n\n" "Certo, <b>{first_name}</b>. ✅\n"
"Você está vinculado à empresa <b>{company}</b>.\n" "Você está na <b>{company}</b>. O que houve?"
"Agora pode me descrever problemas técnicos que eu analisarei.\n\n"
"Como posso te ajudar hoje?"
), ),
( (
"Show, <b>{first_name}</b>! Cadastro feito com sucesso. ✅\n\n" "Ok <b>{first_name}</b>, te encontrei aqui na <b>{company}</b>. ✅\n"
"Te encontrei aqui como parte da <b>{company}</b>.\n" "Como posso ajudar?"
"Estou pronto pra ajudar com qualquer questão técnica!\n\n"
"O que você precisa hoje?"
), ),
( (
"Muito obrigado, <b>{first_name}</b>! Tudo certo. ✅\n\n" "Pronto, <b>{first_name}</b>. ✅\n"
"Seu perfil está associado à <b>{company}</b>.\n" "Associei você à <b>{company}</b>. Qual o problema?"
"Pode contar comigo para análises técnicas e suporte.\n\n"
"Em que posso ajudar?"
), ),
] ]
@ -122,10 +113,14 @@ class OnboardingManager:
""" """
COLLECTION_NAME = "arthur_users" COLLECTION_NAME = "arthur_users"
VECTOR_SIZE = 384
def __init__(self): def __init__(self):
self._qdrant = get_qdrant_client() self._qdrant = get_qdrant_client()
self._pending_registrations: Dict[str, Dict[str, Any]] = {} self._pending_registrations: Dict[str, Dict[str, Any]] = {}
# Local cache to avoid Qdrant lookup issues
self._registered_users: Dict[str, UserProfile] = {}
self._collection_ready = False
logger.info("OnboardingManager initialized") logger.info("OnboardingManager initialized")
def get_time_greeting(self) -> str: def get_time_greeting(self) -> str:
@ -156,6 +151,10 @@ class OnboardingManager:
Returns: Returns:
True if user is registered, False otherwise True if user is registered, False otherwise
""" """
# Check local cache first (faster and more reliable)
if telegram_id in self._registered_users:
return self._registered_users[telegram_id].status == UserStatus.REGISTERED
profile = await self.get_user_profile(telegram_id) profile = await self.get_user_profile(telegram_id)
return profile is not None and profile.status == UserStatus.REGISTERED return profile is not None and profile.status == UserStatus.REGISTERED
@ -199,7 +198,11 @@ class OnboardingManager:
Returns: Returns:
Dict with name, company, last_ticket_summary (if any) Dict with name, company, last_ticket_summary (if any)
""" """
profile = await self.get_user_profile(telegram_id) # Check local cache first
if telegram_id in self._registered_users:
profile = self._registered_users[telegram_id]
else:
profile = await self.get_user_profile(telegram_id)
if profile is None: if profile is None:
return None return None
@ -287,6 +290,9 @@ class OnboardingManager:
except Exception as e: except Exception as e:
logger.error(f"Failed to store user profile: {e}") logger.error(f"Failed to store user profile: {e}")
# Add to local cache (IMPORTANT: fixes the loop issue)
self._registered_users[telegram_id] = profile
# Clean up pending registration # Clean up pending registration
self._pending_registrations.pop(telegram_id, None) self._pending_registrations.pop(telegram_id, None)

View File

@ -140,20 +140,24 @@ class TelegramListener:
telegram_id = str(user.id) telegram_id = str(user.id)
message = update.message.text.strip() message = update.message.text.strip()
# Parse "Name, Company" format # Parse "Name, Company" or "Name. Company" or "Name - Company" format
match = re.match(r'^(.+?),\s*(.+)$', message) # More flexible: accepts comma, period, or hyphen as separator
match = re.match(r'^(.+?)[,\.\-]\s*(.+)$', message)
if not match: if not match:
await update.message.reply_text( # Try splitting by last space if no separator found
"📝 Por favor, informe no formato:\n" parts = message.rsplit(' ', 1)
"<i>Nome Completo, Empresa</i>\n\n" if len(parts) == 2 and len(parts[0]) > 2 and len(parts[1]) > 2:
"Exemplo: João Silva, iT Guys", name, company = parts[0].strip(), parts[1].strip()
parse_mode="HTML" else:
) await update.message.reply_text(
return "Não entendi. Qual seu <b>nome</b> e <b>empresa</b>?",
parse_mode="HTML"
name = match.group(1).strip() )
company = match.group(2).strip() return
else:
name = match.group(1).strip()
company = match.group(2).strip()
# Check if company is a client # Check if company is a client
tenant = await self._financial.get_tenant_by_name(company) tenant = await self._financial.get_tenant_by_name(company)
@ -220,10 +224,9 @@ class TelegramListener:
greeting = self._onboarding.get_time_greeting() greeting = self._onboarding.get_time_greeting()
first_name = user_ctx.get("name", "você").split()[0] first_name = user_ctx.get("name", "você").split()[0]
# Send acknowledgment # Send acknowledgment (NO greeting here - already greeted in registration)
await update.message.reply_text( await update.message.reply_text(
f"👋 {greeting}, {first_name}!\n" "Um momento...",
"🔍 Analisando sua solicitação...",
parse_mode="HTML" parse_mode="HTML"
) )
@ -248,12 +251,14 @@ class TelegramListener:
await self._onboarding.update_last_contact(telegram_id, ticket_summary) await self._onboarding.update_last_contact(telegram_id, ticket_summary)
# Format response # Format response
if result.success: if result.success and result.context.final_response:
response = f"✅ Análise Concluída\n\n{result.context.final_response}" response = result.context.final_response
elif result.context.error:
response = f"⚠️ Houve um problema: {result.context.error}"
else: else:
response = f"❌ Erro na Análise\n\n{result.context.error}" response = "Desculpe, não consegui processar sua solicitação. Pode reformular?"
await update.message.reply_text(response) await update.message.reply_text(response, parse_mode="HTML")
except Exception as e: except Exception as e:
logger.error(f"Error processing telegram message: {e}") logger.error(f"Error processing telegram message: {e}")

View File

@ -124,8 +124,8 @@ class TestStartRegistration:
result = onboarding.start_registration("123456") result = onboarding.start_registration("123456")
assert "Boa tarde" in result assert "Boa tarde" in result
assert "Nome Completo" in result assert "nome" in result.lower()
assert "Empresa" in result assert "empresa" in result.lower()
def test_marks_user_as_pending(self, onboarding): def test_marks_user_as_pending(self, onboarding):
"""Should mark user as pending registration.""" """Should mark user as pending registration."""