atendentepro 0.3.0__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
atendentepro/__init__.py CHANGED
@@ -42,7 +42,7 @@ Para obter um token de licença, entre em contato: contato@bemonkai.com
42
42
  Para mais informações, consulte a documentação.
43
43
  """
44
44
 
45
- __version__ = "0.3.0"
45
+ __version__ = "0.5.0"
46
46
  __author__ = "BeMonkAI"
47
47
  __email__ = "contato@bemonkai.com"
48
48
  __license__ = "Proprietary"
@@ -92,6 +92,8 @@ from atendentepro.agents import (
92
92
  create_confirmation_agent,
93
93
  create_usage_agent,
94
94
  create_onboarding_agent,
95
+ create_escalation_agent,
96
+ create_feedback_agent,
95
97
  TriageAgent,
96
98
  FlowAgent,
97
99
  InterviewAgent,
@@ -100,7 +102,12 @@ from atendentepro.agents import (
100
102
  ConfirmationAgent,
101
103
  UsageAgent,
102
104
  OnboardingAgent,
105
+ EscalationAgent,
106
+ FeedbackAgent,
103
107
  go_to_rag,
108
+ ESCALATION_TOOLS,
109
+ FEEDBACK_TOOLS,
110
+ configure_feedback_storage,
104
111
  )
105
112
 
106
113
  # Network
@@ -178,6 +185,8 @@ __all__ = [
178
185
  "create_confirmation_agent",
179
186
  "create_usage_agent",
180
187
  "create_onboarding_agent",
188
+ "create_escalation_agent",
189
+ "create_feedback_agent",
181
190
  "TriageAgent",
182
191
  "FlowAgent",
183
192
  "InterviewAgent",
@@ -186,7 +195,12 @@ __all__ = [
186
195
  "ConfirmationAgent",
187
196
  "UsageAgent",
188
197
  "OnboardingAgent",
198
+ "EscalationAgent",
199
+ "FeedbackAgent",
189
200
  "go_to_rag",
201
+ "ESCALATION_TOOLS",
202
+ "FEEDBACK_TOOLS",
203
+ "configure_feedback_storage",
190
204
  # Network
191
205
  "AgentNetwork",
192
206
  "create_standard_network",
@@ -14,6 +14,8 @@ from .knowledge import create_knowledge_agent, KnowledgeAgent, go_to_rag
14
14
  from .confirmation import create_confirmation_agent, ConfirmationAgent
15
15
  from .usage import create_usage_agent, UsageAgent
16
16
  from .onboarding import create_onboarding_agent, OnboardingAgent
17
+ from .escalation import create_escalation_agent, EscalationAgent, ESCALATION_TOOLS
18
+ from .feedback import create_feedback_agent, FeedbackAgent, FEEDBACK_TOOLS, configure_feedback_storage
17
19
 
18
20
  __all__ = [
19
21
  # Triage
@@ -41,5 +43,14 @@ __all__ = [
41
43
  # Onboarding
42
44
  "create_onboarding_agent",
43
45
  "OnboardingAgent",
46
+ # Escalation
47
+ "create_escalation_agent",
48
+ "EscalationAgent",
49
+ "ESCALATION_TOOLS",
50
+ # Feedback
51
+ "create_feedback_agent",
52
+ "FeedbackAgent",
53
+ "FEEDBACK_TOOLS",
54
+ "configure_feedback_storage",
44
55
  ]
45
56
 
@@ -52,8 +52,7 @@ def create_answer_agent(
52
52
  name=name,
53
53
  handoff_description=(
54
54
  "Agente responsável por sintetizar a resposta técnica final usando os dados coletados. "
55
- "Após concluir a orientação, deve acionar o handoff para o Triage Agent a fim de encerrar o caso. "
56
- "Se identificar lacunas de informação, orienta o que falta e aciona o handoff para o Interview Agent."
55
+ "Após concluir a orientação, deve acionar o handoff para o Triage Agent a fim de encerrar o caso."
57
56
  ),
58
57
  instructions=instructions,
59
58
  handoffs=handoffs or [],
@@ -0,0 +1,510 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Escalation Agent for AtendentePro.
4
+
5
+ Handles transfers to human support when:
6
+ - User explicitly requests
7
+ - Topic is not covered by the system
8
+ - Agent cannot resolve the issue
9
+ - User shows frustration or confusion
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import uuid
16
+ from datetime import datetime
17
+ from typing import List, Optional, Dict, Any, TYPE_CHECKING
18
+ from dataclasses import dataclass, field
19
+
20
+ from agents import Agent, function_tool
21
+
22
+ from atendentepro.config import RECOMMENDED_PROMPT_PREFIX
23
+ from atendentepro.models import ContextNote
24
+ from atendentepro.prompts.escalation import (
25
+ get_escalation_prompt,
26
+ EscalationPromptBuilder,
27
+ ESCALATION_INTRO,
28
+ DEFAULT_ESCALATION_TRIGGERS,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from atendentepro.guardrails import GuardrailCallable
33
+
34
+
35
+ # Type alias for the Escalation Agent
36
+ EscalationAgent = Agent[ContextNote]
37
+
38
+
39
+ # =============================================================================
40
+ # Configurações de Escalação
41
+ # =============================================================================
42
+
43
+ # Horário de atendimento padrão (pode ser sobrescrito via config)
44
+ DEFAULT_BUSINESS_HOURS = {
45
+ "start": 8,
46
+ "end": 18,
47
+ "days": [0, 1, 2, 3, 4], # Seg-Sex
48
+ }
49
+
50
+
51
+ # =============================================================================
52
+ # Storage de Escalações (em memória - substituir por DB em produção)
53
+ # =============================================================================
54
+
55
+ @dataclass
56
+ class Escalation:
57
+ """Representa uma escalação para atendimento humano."""
58
+ protocolo: str
59
+ motivo: str
60
+ categoria: str # solicitacao, frustração, topico_nao_coberto, incerteza
61
+ nome_usuario: str
62
+ contato: str
63
+ tipo_contato: str # telefone, email, whatsapp
64
+ resumo_conversa: str
65
+ prioridade: str # baixa, normal, alta, urgente
66
+ status: str # pendente, em_atendimento, concluido, cancelado
67
+ data_criacao: datetime = field(default_factory=datetime.now)
68
+ data_atualizacao: datetime = field(default_factory=datetime.now)
69
+ atendente: Optional[str] = None
70
+ observacoes: Optional[str] = None
71
+
72
+
73
+ # Storage em memória
74
+ _escalations_storage: Dict[str, Escalation] = {}
75
+
76
+
77
+ def _gerar_protocolo_escalacao() -> str:
78
+ """Gera um protocolo único para escalação."""
79
+ timestamp = datetime.now().strftime("%Y%m%d")
80
+ unique_id = uuid.uuid4().hex[:6].upper()
81
+ return f"ESC-{timestamp}-{unique_id}"
82
+
83
+
84
+ def _salvar_escalacao(escalation: Escalation) -> None:
85
+ """Salva escalação no storage."""
86
+ _escalations_storage[escalation.protocolo] = escalation
87
+
88
+
89
+ def _buscar_escalacao(protocolo: str) -> Optional[Escalation]:
90
+ """Busca escalação pelo protocolo."""
91
+ return _escalations_storage.get(protocolo.upper())
92
+
93
+
94
+ def _listar_escalacoes_pendentes() -> List[Escalation]:
95
+ """Lista escalações pendentes."""
96
+ return [
97
+ e for e in _escalations_storage.values()
98
+ if e.status == "pendente"
99
+ ]
100
+
101
+
102
+ # =============================================================================
103
+ # Funções de Notificação (implementar conforme necessidade)
104
+ # =============================================================================
105
+
106
+ def _notificar_equipe(escalation: Escalation) -> bool:
107
+ """
108
+ Notifica a equipe sobre nova escalação.
109
+
110
+ Em produção, integrar com:
111
+ - Email (SMTP)
112
+ - Slack/Teams
113
+ - Sistema de tickets
114
+ - Webhook
115
+ """
116
+ webhook_url = os.getenv("ESCALATION_WEBHOOK_URL")
117
+
118
+ if webhook_url:
119
+ try:
120
+ import requests
121
+ payload = {
122
+ "protocolo": escalation.protocolo,
123
+ "motivo": escalation.motivo,
124
+ "prioridade": escalation.prioridade,
125
+ "usuario": escalation.nome_usuario,
126
+ "contato": escalation.contato,
127
+ "resumo": escalation.resumo_conversa,
128
+ }
129
+ requests.post(webhook_url, json=payload, timeout=5)
130
+ return True
131
+ except Exception as e:
132
+ print(f"[Escalation] Erro ao notificar: {e}")
133
+ return False
134
+
135
+ # Modo simulação
136
+ print(f"[Escalation] Nova escalação: {escalation.protocolo}")
137
+ return True
138
+
139
+
140
+ def _verificar_disponibilidade() -> Dict[str, Any]:
141
+ """Verifica se atendimento humano está disponível."""
142
+ agora = datetime.now()
143
+ hora = agora.hour
144
+ dia = agora.weekday()
145
+
146
+ # Verificar variáveis de ambiente para horário customizado
147
+ hora_inicio = int(os.getenv("ESCALATION_HOUR_START", DEFAULT_BUSINESS_HOURS["start"]))
148
+ hora_fim = int(os.getenv("ESCALATION_HOUR_END", DEFAULT_BUSINESS_HOURS["end"]))
149
+
150
+ disponivel = dia in DEFAULT_BUSINESS_HOURS["days"] and hora_inicio <= hora < hora_fim
151
+
152
+ return {
153
+ "disponivel": disponivel,
154
+ "hora_atual": agora.strftime("%H:%M"),
155
+ "dia_semana": ["Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado", "Domingo"][dia],
156
+ "horario_atendimento": f"{hora_inicio:02d}:00 - {hora_fim:02d}:00",
157
+ "dias_atendimento": "Segunda a Sexta",
158
+ }
159
+
160
+
161
+ # =============================================================================
162
+ # Classificação Automática de Prioridade
163
+ # =============================================================================
164
+
165
+ def _classificar_prioridade(motivo: str, categoria: str) -> str:
166
+ """
167
+ Classifica automaticamente a prioridade baseado no motivo e categoria.
168
+ """
169
+ motivo_lower = motivo.lower()
170
+
171
+ # Palavras que indicam urgência
172
+ palavras_urgentes = [
173
+ "urgente", "emergência", "emergencia", "crítico", "critico",
174
+ "não funciona", "parou", "bloqueado", "cancelar", "prejuízo"
175
+ ]
176
+
177
+ palavras_alta = [
178
+ "reclamação", "reclamacao", "insatisfeito", "problema grave",
179
+ "já tentei", "terceira vez", "não resolve"
180
+ ]
181
+
182
+ # Verificar urgência
183
+ for palavra in palavras_urgentes:
184
+ if palavra in motivo_lower:
185
+ return "urgente"
186
+
187
+ # Verificar alta prioridade
188
+ for palavra in palavras_alta:
189
+ if palavra in motivo_lower:
190
+ return "alta"
191
+
192
+ # Frustração do usuário = alta
193
+ if categoria == "frustracao":
194
+ return "alta"
195
+
196
+ return "normal"
197
+
198
+
199
+ # =============================================================================
200
+ # Tools do Agente de Escalação
201
+ # =============================================================================
202
+
203
+ @function_tool
204
+ def verificar_horario_atendimento() -> str:
205
+ """
206
+ Verifica se o atendimento humano está disponível no momento atual.
207
+ Use esta ferramenta ANTES de registrar a escalação para informar o usuário.
208
+
209
+ Returns:
210
+ Status de disponibilidade do atendimento humano
211
+ """
212
+ info = _verificar_disponibilidade()
213
+
214
+ if info["disponivel"]:
215
+ return f"""✅ **Atendimento Humano DISPONÍVEL**
216
+
217
+ 📅 {info['dia_semana']}, {info['hora_atual']}
218
+ ⏰ Horário de atendimento: {info['horario_atendimento']}
219
+
220
+ Um atendente poderá retornar em breve após o registro."""
221
+ else:
222
+ return f"""⚠️ **Atendimento Humano FORA DO HORÁRIO**
223
+
224
+ 📅 {info['dia_semana']}, {info['hora_atual']}
225
+ ⏰ Horário de atendimento: {info['horario_atendimento']}
226
+ 📆 Dias: {info['dias_atendimento']}
227
+
228
+ Você pode deixar seus dados e retornaremos no próximo horário disponível."""
229
+
230
+
231
+ @function_tool
232
+ def registrar_escalacao(
233
+ motivo: str,
234
+ nome_usuario: str,
235
+ contato: str,
236
+ tipo_contato: str = "telefone",
237
+ resumo_conversa: str = "",
238
+ categoria: str = "solicitacao",
239
+ ) -> str:
240
+ """
241
+ Registra uma escalação para atendimento humano e notifica a equipe.
242
+
243
+ IMPORTANTE: Use esta ferramenta quando:
244
+ - O usuário solicitar falar com um humano
245
+ - O tópico não for coberto pelo sistema
246
+ - Não conseguir resolver o problema do usuário
247
+ - O usuário demonstrar frustração
248
+
249
+ Args:
250
+ motivo: Motivo da escalação (descrição do que o usuário precisa)
251
+ nome_usuario: Nome do usuário para contato
252
+ contato: Telefone, email ou WhatsApp do usuário
253
+ tipo_contato: Tipo de contato preferido ("telefone", "email", "whatsapp")
254
+ resumo_conversa: Breve resumo do que foi discutido antes da escalação
255
+ categoria: Categoria da escalação:
256
+ - "solicitacao": Usuário pediu para falar com humano
257
+ - "frustracao": Usuário demonstrou frustração
258
+ - "topico_nao_coberto": Assunto fora do escopo
259
+ - "incerteza": Agente não conseguiu resolver
260
+
261
+ Returns:
262
+ Confirmação com protocolo e próximos passos
263
+ """
264
+ # Validações
265
+ if not nome_usuario or len(nome_usuario.strip()) < 2:
266
+ return "❌ Por favor, informe seu nome completo para que possamos retornar."
267
+
268
+ if not contato or len(contato.strip()) < 5:
269
+ return "❌ Por favor, informe um contato válido (telefone, email ou WhatsApp)."
270
+
271
+ if not motivo or len(motivo.strip()) < 5:
272
+ return "❌ Por favor, descreva brevemente o motivo do contato."
273
+
274
+ # Normalizar tipo de contato
275
+ tipo_contato_norm = tipo_contato.lower().strip()
276
+ if tipo_contato_norm not in ["telefone", "email", "whatsapp"]:
277
+ tipo_contato_norm = "telefone"
278
+
279
+ # Normalizar categoria
280
+ categorias_validas = ["solicitacao", "frustracao", "topico_nao_coberto", "incerteza"]
281
+ categoria_norm = categoria.lower().strip()
282
+ if categoria_norm not in categorias_validas:
283
+ categoria_norm = "solicitacao"
284
+
285
+ # Classificar prioridade automaticamente
286
+ prioridade = _classificar_prioridade(motivo, categoria_norm)
287
+
288
+ # Criar escalação
289
+ escalation = Escalation(
290
+ protocolo=_gerar_protocolo_escalacao(),
291
+ motivo=motivo.strip(),
292
+ categoria=categoria_norm,
293
+ nome_usuario=nome_usuario.strip(),
294
+ contato=contato.strip(),
295
+ tipo_contato=tipo_contato_norm,
296
+ resumo_conversa=resumo_conversa.strip() if resumo_conversa else "",
297
+ prioridade=prioridade,
298
+ status="pendente",
299
+ )
300
+
301
+ # Salvar
302
+ _salvar_escalacao(escalation)
303
+
304
+ # Notificar equipe
305
+ notificado = _notificar_equipe(escalation)
306
+
307
+ # Verificar disponibilidade
308
+ disp = _verificar_disponibilidade()
309
+
310
+ # Ícones por prioridade
311
+ icone_prioridade = {
312
+ "baixa": "🟢",
313
+ "normal": "🟡",
314
+ "alta": "🟠",
315
+ "urgente": "🔴",
316
+ }.get(prioridade, "🟡")
317
+
318
+ # Ícones por tipo de contato
319
+ icone_contato = {
320
+ "telefone": "📞",
321
+ "email": "📧",
322
+ "whatsapp": "💬",
323
+ }.get(tipo_contato_norm, "📞")
324
+
325
+ resposta = f"""
326
+ ✅ **Escalação Registrada com Sucesso!**
327
+
328
+ ═══════════════════════════════════════
329
+ 📋 **Protocolo:** {escalation.protocolo}
330
+ ═══════════════════════════════════════
331
+
332
+ 👤 **Nome:** {escalation.nome_usuario}
333
+ {icone_contato} **Contato ({tipo_contato_norm}):** {escalation.contato}
334
+ {icone_prioridade} **Prioridade:** {prioridade.upper()}
335
+ 📅 **Data:** {escalation.data_criacao.strftime('%d/%m/%Y às %H:%M')}
336
+
337
+ 📝 **Motivo:**
338
+ {escalation.motivo}
339
+ """
340
+
341
+ if escalation.resumo_conversa:
342
+ resposta += f"""
343
+ 📄 **Resumo da conversa:**
344
+ {escalation.resumo_conversa[:200]}{'...' if len(escalation.resumo_conversa) > 200 else ''}
345
+ """
346
+
347
+ resposta += "\n═══════════════════════════════════════\n"
348
+
349
+ if disp["disponivel"]:
350
+ resposta += """
351
+ ⏳ **Próximos Passos:**
352
+ Um atendente humano entrará em contato em breve.
353
+ """
354
+ else:
355
+ resposta += f"""
356
+ ⏳ **Próximos Passos:**
357
+ Estamos fora do horário de atendimento ({disp['horario_atendimento']}).
358
+ Um atendente retornará no próximo dia útil.
359
+ """
360
+
361
+ resposta += f"""
362
+ 💡 **Dica:** Guarde o protocolo **{escalation.protocolo}** para acompanhamento.
363
+ """
364
+
365
+ if notificado:
366
+ resposta += "\n✅ Nossa equipe foi notificada."
367
+
368
+ return resposta
369
+
370
+
371
+ @function_tool
372
+ def consultar_escalacao(protocolo: str) -> str:
373
+ """
374
+ Consulta o status de uma escalação pelo protocolo.
375
+
376
+ Args:
377
+ protocolo: Número do protocolo (ex: ESC-20240105-ABC123)
378
+
379
+ Returns:
380
+ Status e detalhes da escalação
381
+ """
382
+ escalation = _buscar_escalacao(protocolo)
383
+
384
+ if not escalation:
385
+ return f"""❌ **Escalação não encontrada:** {protocolo}
386
+
387
+ Verifique se o número do protocolo está correto.
388
+ Formato esperado: ESC-YYYYMMDD-XXXXXX"""
389
+
390
+ # Ícones de status
391
+ icone_status = {
392
+ "pendente": "🟡",
393
+ "em_atendimento": "🔵",
394
+ "concluido": "🟢",
395
+ "cancelado": "⚫",
396
+ }.get(escalation.status, "⚪")
397
+
398
+ resposta = f"""
399
+ 📋 **Consulta de Escalação**
400
+
401
+ ═══════════════════════════════════════
402
+ 🔖 **Protocolo:** {escalation.protocolo}
403
+ {icone_status} **Status:** {escalation.status.upper().replace('_', ' ')}
404
+ ═══════════════════════════════════════
405
+
406
+ 👤 **Solicitante:** {escalation.nome_usuario}
407
+ 📞 **Contato:** {escalation.contato} ({escalation.tipo_contato})
408
+ ⚡ **Prioridade:** {escalation.prioridade.upper()}
409
+
410
+ 📅 **Criado em:** {escalation.data_criacao.strftime('%d/%m/%Y às %H:%M')}
411
+ 📅 **Atualizado em:** {escalation.data_atualizacao.strftime('%d/%m/%Y às %H:%M')}
412
+
413
+ 📝 **Motivo:**
414
+ {escalation.motivo}
415
+ """
416
+
417
+ if escalation.atendente:
418
+ resposta += f"\n👨‍💼 **Atendente:** {escalation.atendente}"
419
+
420
+ if escalation.observacoes:
421
+ resposta += f"\n\n📌 **Observações:**\n{escalation.observacoes}"
422
+
423
+ return resposta
424
+
425
+
426
+ # =============================================================================
427
+ # Lista de Tools
428
+ # =============================================================================
429
+
430
+ ESCALATION_TOOLS = [
431
+ verificar_horario_atendimento,
432
+ registrar_escalacao,
433
+ consultar_escalacao,
434
+ ]
435
+
436
+
437
+ # =============================================================================
438
+ # Triggers e Instruções movidos para atendentepro/prompts/escalation.py
439
+ # =============================================================================
440
+
441
+ # Re-exportar para compatibilidade
442
+ # DEFAULT_ESCALATION_TRIGGERS e ESCALATION_INTRO estão em prompts/escalation.py
443
+
444
+
445
+ # =============================================================================
446
+ # Criar Escalation Agent
447
+ # =============================================================================
448
+
449
+ def create_escalation_agent(
450
+ escalation_triggers: str = "",
451
+ escalation_channels: str = "",
452
+ handoffs: Optional[List] = None,
453
+ tools: Optional[List] = None,
454
+ guardrails: Optional[List["GuardrailCallable"]] = None,
455
+ name: str = "Escalation Agent",
456
+ custom_instructions: Optional[str] = None,
457
+ ) -> EscalationAgent:
458
+ """
459
+ Create an Escalation Agent instance.
460
+
461
+ The escalation agent handles transfers to human support when:
462
+ - User explicitly requests to talk to a human
463
+ - Topic is not covered by the automated system
464
+ - Agent cannot resolve the issue after attempts
465
+ - User shows frustration or confusion
466
+
467
+ This agent should be added as a handoff option to ALL other agents
468
+ in the network to ensure users can always escalate when needed.
469
+
470
+ Args:
471
+ escalation_triggers: Custom triggers for escalation (keywords, situations).
472
+ escalation_channels: Available contact channels description.
473
+ handoffs: List of agents to hand off to (usually triage to return).
474
+ tools: Additional tools (custom notifications, integrations).
475
+ guardrails: List of input guardrails.
476
+ name: Agent name.
477
+ custom_instructions: Optional custom instructions to override default.
478
+
479
+ Returns:
480
+ Configured Escalation Agent instance.
481
+
482
+ Example:
483
+ >>> escalation = create_escalation_agent(
484
+ ... escalation_channels="Telefone: 0800-123-4567 (Seg-Sex 8h-18h)",
485
+ ... handoffs=[triage],
486
+ ... )
487
+ >>> # Add to all agents
488
+ >>> triage.handoffs.append(escalation)
489
+ >>> flow.handoffs.append(escalation)
490
+ """
491
+ if custom_instructions:
492
+ instructions = f"{RECOMMENDED_PROMPT_PREFIX} {custom_instructions}"
493
+ else:
494
+ # Usar o prompt builder do módulo prompts
495
+ instructions = f"{RECOMMENDED_PROMPT_PREFIX}\n{get_escalation_prompt(escalation_triggers=escalation_triggers, escalation_channels=escalation_channels)}"
496
+
497
+ # Combinar tools padrão com customizadas
498
+ agent_tools = list(ESCALATION_TOOLS)
499
+ if tools:
500
+ agent_tools.extend(tools)
501
+
502
+ return Agent[ContextNote](
503
+ name=name,
504
+ handoff_description="Transfere para atendimento humano quando o agente não consegue resolver, o tópico não é coberto, ou o usuário solicita.",
505
+ instructions=instructions,
506
+ tools=agent_tools,
507
+ handoffs=handoffs or [],
508
+ input_guardrails=guardrails or [],
509
+ )
510
+