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_TZ = ZoneInfo("America/Sao_Paulo")
# Welcome message templates - will be randomized
# Welcome message templates - will be randomized (NO examples - too robotic)
WELCOME_TEMPLATES = [
(
"Olá! {greeting}. 👋\n\n"
"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"
"Exemplo: <i>João Silva, iT Guys</i>"
"Pode me confirmar seu <b>nome</b> e <b>empresa</b>?"
),
(
"{greeting}! 👋\n\n"
"Prazer em conhecê-lo! Para que eu possa te atender melhor, "
"me conta: qual é o seu <b>nome</b> e de qual <b>empresa</b> você fala?\n\n"
"Pode responder assim: <i>Maria Santos, Empresa XYZ</i>"
"Prazer! Para te atender melhor, "
"me conta seu <b>nome</b> e de qual <b>empresa</b> você fala?"
),
(
"E aí! {greeting}. 😊\n\n"
"Ainda não temos seu cadastro por aqui. "
"Me passa seu <b>nome completo</b> e a <b>empresa</b> que você representa?\n\n"
"Formato: <i>Nome Sobrenome, Sua Empresa</i>"
"{greeting}. 👋\n\n"
"Ainda não temos seu cadastro. "
"Qual seu <b>nome</b> e <b>empresa</b>?"
),
]
# Registration confirmation templates
# Registration confirmation templates (natural, conversational)
REGISTRATION_SUCCESS_TEMPLATES = [
(
"Perfeito, <b>{first_name}</b>! Obrigado pelas informações. ✅\n\n"
"Você está vinculado à empresa <b>{company}</b>.\n"
"Agora pode me descrever problemas técnicos que eu analisarei.\n\n"
"Como posso te ajudar hoje?"
"Certo, <b>{first_name}</b>. ✅\n"
"Você está na <b>{company}</b>. O que houve?"
),
(
"Show, <b>{first_name}</b>! Cadastro feito com sucesso. ✅\n\n"
"Te encontrei aqui como parte da <b>{company}</b>.\n"
"Estou pronto pra ajudar com qualquer questão técnica!\n\n"
"O que você precisa hoje?"
"Ok <b>{first_name}</b>, te encontrei aqui na <b>{company}</b>. ✅\n"
"Como posso ajudar?"
),
(
"Muito obrigado, <b>{first_name}</b>! Tudo certo. ✅\n\n"
"Seu perfil está associado à <b>{company}</b>.\n"
"Pode contar comigo para análises técnicas e suporte.\n\n"
"Em que posso ajudar?"
"Pronto, <b>{first_name}</b>. ✅\n"
"Associei você à <b>{company}</b>. Qual o problema?"
),
]
@ -122,10 +113,14 @@ class OnboardingManager:
"""
COLLECTION_NAME = "arthur_users"
VECTOR_SIZE = 384
def __init__(self):
self._qdrant = get_qdrant_client()
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")
def get_time_greeting(self) -> str:
@ -156,6 +151,10 @@ class OnboardingManager:
Returns:
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)
return profile is not None and profile.status == UserStatus.REGISTERED
@ -199,6 +198,10 @@ class OnboardingManager:
Returns:
Dict with name, company, last_ticket_summary (if any)
"""
# 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:
@ -287,6 +290,9 @@ class OnboardingManager:
except Exception as 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
self._pending_registrations.pop(telegram_id, None)

View File

@ -140,18 +140,22 @@ class TelegramListener:
telegram_id = str(user.id)
message = update.message.text.strip()
# Parse "Name, Company" format
match = re.match(r'^(.+?),\s*(.+)$', message)
# Parse "Name, Company" or "Name. Company" or "Name - Company" format
# More flexible: accepts comma, period, or hyphen as separator
match = re.match(r'^(.+?)[,\.\-]\s*(.+)$', message)
if not match:
# Try splitting by last space if no separator found
parts = message.rsplit(' ', 1)
if len(parts) == 2 and len(parts[0]) > 2 and len(parts[1]) > 2:
name, company = parts[0].strip(), parts[1].strip()
else:
await update.message.reply_text(
"📝 Por favor, informe no formato:\n"
"<i>Nome Completo, Empresa</i>\n\n"
"Exemplo: João Silva, iT Guys",
"Não entendi. Qual seu <b>nome</b> e <b>empresa</b>?",
parse_mode="HTML"
)
return
else:
name = match.group(1).strip()
company = match.group(2).strip()
@ -220,10 +224,9 @@ class TelegramListener:
greeting = self._onboarding.get_time_greeting()
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(
f"👋 {greeting}, {first_name}!\n"
"🔍 Analisando sua solicitação...",
"Um momento...",
parse_mode="HTML"
)
@ -248,12 +251,14 @@ class TelegramListener:
await self._onboarding.update_last_contact(telegram_id, ticket_summary)
# Format response
if result.success:
response = f"✅ Análise Concluída\n\n{result.context.final_response}"
if result.success and result.context.final_response:
response = result.context.final_response
elif result.context.error:
response = f"⚠️ Houve um problema: {result.context.error}"
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:
logger.error(f"Error processing telegram message: {e}")

View File

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