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.
@@ -0,0 +1,961 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Feedback Agent for AtendentePro.
4
+
5
+ Handles user feedback, questions, complaints, and suggestions through
6
+ a ticket-based system with email notifications.
7
+
8
+ This is a universal module for all customer service systems,
9
+ allowing users to:
10
+ - Register questions (dúvidas)
11
+ - Send feedback
12
+ - File complaints (reclamações)
13
+ - Submit suggestions (sugestões)
14
+ - Give compliments (elogios)
15
+ - Report problems
16
+
17
+ All tickets are tracked with unique protocol numbers and can be
18
+ configured with custom email templates per client.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import re
25
+ import smtplib
26
+ import uuid
27
+ from datetime import datetime
28
+ from email.mime.text import MIMEText
29
+ from email.mime.multipart import MIMEMultipart
30
+ from typing import Optional, List, Dict, Any, TYPE_CHECKING, Callable
31
+ from dataclasses import dataclass, field
32
+ from enum import Enum
33
+
34
+ from agents import Agent, function_tool
35
+
36
+ from atendentepro.config import RECOMMENDED_PROMPT_PREFIX
37
+ from atendentepro.models import ContextNote
38
+ from atendentepro.prompts.feedback import (
39
+ get_feedback_prompt,
40
+ FeedbackPromptBuilder,
41
+ FEEDBACK_INTRO,
42
+ DEFAULT_TICKET_TYPES,
43
+ )
44
+
45
+ if TYPE_CHECKING:
46
+ from atendentepro.guardrails import GuardrailCallable
47
+
48
+
49
+ # Type alias for the Feedback Agent
50
+ FeedbackAgent = Agent[ContextNote]
51
+
52
+
53
+ # =============================================================================
54
+ # Enums for Ticket Properties
55
+ # =============================================================================
56
+
57
+ class TicketType(str, Enum):
58
+ """Default ticket types."""
59
+ DUVIDA = "duvida"
60
+ FEEDBACK = "feedback"
61
+ RECLAMACAO = "reclamacao"
62
+ SUGESTAO = "sugestao"
63
+ ELOGIO = "elogio"
64
+ PROBLEMA = "problema"
65
+ OUTRO = "outro"
66
+
67
+
68
+ class TicketPriority(str, Enum):
69
+ """Ticket priority levels."""
70
+ BAIXA = "baixa"
71
+ NORMAL = "normal"
72
+ ALTA = "alta"
73
+ URGENTE = "urgente"
74
+
75
+
76
+ class TicketStatus(str, Enum):
77
+ """Ticket status states."""
78
+ ABERTO = "aberto"
79
+ EM_ANDAMENTO = "em_andamento"
80
+ AGUARDANDO = "aguardando_usuario"
81
+ RESOLVIDO = "resolvido"
82
+ FECHADO = "fechado"
83
+ CANCELADO = "cancelado"
84
+
85
+
86
+ # =============================================================================
87
+ # Ticket Data Model
88
+ # =============================================================================
89
+
90
+ @dataclass
91
+ class Ticket:
92
+ """Represents a feedback/support ticket."""
93
+ protocolo: str
94
+ tipo: str
95
+ descricao: str
96
+ email_usuario: str
97
+ nome_usuario: str = ""
98
+ telefone_usuario: str = ""
99
+ prioridade: str = "normal"
100
+ status: str = "aberto"
101
+ categoria: str = ""
102
+ data_criacao: datetime = field(default_factory=datetime.now)
103
+ data_atualizacao: datetime = field(default_factory=datetime.now)
104
+ resposta: Optional[str] = None
105
+ atendente: Optional[str] = None
106
+ historico: List[Dict[str, Any]] = field(default_factory=list)
107
+
108
+ def adicionar_historico(self, acao: str, detalhes: str = "") -> None:
109
+ """Add entry to ticket history."""
110
+ self.historico.append({
111
+ "data": datetime.now().isoformat(),
112
+ "acao": acao,
113
+ "detalhes": detalhes,
114
+ })
115
+ self.data_atualizacao = datetime.now()
116
+
117
+
118
+ # =============================================================================
119
+ # Storage (in-memory, replace with DB in production)
120
+ # =============================================================================
121
+
122
+ _tickets_storage: Dict[str, Ticket] = {}
123
+
124
+ # Protocol prefix (can be customized per client)
125
+ _protocol_prefix: str = "TKT"
126
+
127
+ # Email configuration
128
+ _email_config: Dict[str, Any] = {
129
+ "enabled": True,
130
+ "brand_color": "#4A90D9",
131
+ "brand_name": "Atendimento",
132
+ "sla_message": "Retornaremos em até 24h úteis.",
133
+ }
134
+
135
+
136
+ def configure_feedback_storage(
137
+ protocol_prefix: str = "TKT",
138
+ email_brand_color: str = "#4A90D9",
139
+ email_brand_name: str = "Atendimento",
140
+ email_sla_message: str = "Retornaremos em até 24h úteis.",
141
+ ) -> None:
142
+ """
143
+ Configure feedback storage settings.
144
+
145
+ Args:
146
+ protocol_prefix: Prefix for ticket protocols (e.g., "SAC", "TKT", "SUP")
147
+ email_brand_color: Hex color for email template branding
148
+ email_brand_name: Brand name for email template
149
+ email_sla_message: SLA message shown in confirmation email
150
+ """
151
+ global _protocol_prefix, _email_config
152
+ _protocol_prefix = protocol_prefix
153
+ _email_config.update({
154
+ "brand_color": email_brand_color,
155
+ "brand_name": email_brand_name,
156
+ "sla_message": email_sla_message,
157
+ })
158
+
159
+
160
+ def _gerar_protocolo() -> str:
161
+ """Generate unique ticket protocol."""
162
+ timestamp = datetime.now().strftime("%Y%m%d")
163
+ unique_id = uuid.uuid4().hex[:6].upper()
164
+ return f"{_protocol_prefix}-{timestamp}-{unique_id}"
165
+
166
+
167
+ def _salvar_ticket(ticket: Ticket) -> None:
168
+ """Save ticket to storage."""
169
+ _tickets_storage[ticket.protocolo.upper()] = ticket
170
+
171
+
172
+ def _buscar_ticket(protocolo: str) -> Optional[Ticket]:
173
+ """Find ticket by protocol."""
174
+ return _tickets_storage.get(protocolo.upper())
175
+
176
+
177
+ def _listar_tickets(email: Optional[str] = None, status: Optional[str] = None) -> List[Ticket]:
178
+ """List tickets, optionally filtered."""
179
+ tickets = list(_tickets_storage.values())
180
+
181
+ if email:
182
+ tickets = [t for t in tickets if t.email_usuario.lower() == email.lower()]
183
+
184
+ if status:
185
+ tickets = [t for t in tickets if t.status.lower() == status.lower()]
186
+
187
+ return sorted(tickets, key=lambda x: x.data_criacao, reverse=True)
188
+
189
+
190
+ # =============================================================================
191
+ # Email Functions
192
+ # =============================================================================
193
+
194
+ # SMTP configuration from environment
195
+ SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
196
+ SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
197
+ SMTP_USER = os.getenv("SMTP_USER", "")
198
+ SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
199
+ SMTP_FROM = os.getenv("SMTP_FROM", "sac@empresa.com")
200
+ FEEDBACK_EMAIL_DESTINO = os.getenv("FEEDBACK_EMAIL_DESTINO", "")
201
+
202
+
203
+ def _validar_email(email: str) -> bool:
204
+ """Validate email format."""
205
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
206
+ return bool(re.match(pattern, email))
207
+
208
+
209
+ def _enviar_email(
210
+ destinatario: str,
211
+ assunto: str,
212
+ corpo_html: str,
213
+ corpo_texto: Optional[str] = None,
214
+ ) -> bool:
215
+ """
216
+ Send email via SMTP.
217
+
218
+ Returns:
219
+ True if sent successfully, False otherwise
220
+ """
221
+ if not SMTP_USER or not SMTP_PASSWORD:
222
+ print(f"[Feedback] ⚠️ SMTP não configurado - email não enviado para {destinatario}")
223
+ return False
224
+
225
+ try:
226
+ msg = MIMEMultipart("alternative")
227
+ msg["Subject"] = assunto
228
+ msg["From"] = SMTP_FROM
229
+ msg["To"] = destinatario
230
+
231
+ if corpo_texto:
232
+ part_texto = MIMEText(corpo_texto, "plain", "utf-8")
233
+ msg.attach(part_texto)
234
+
235
+ part_html = MIMEText(corpo_html, "html", "utf-8")
236
+ msg.attach(part_html)
237
+
238
+ with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
239
+ server.starttls()
240
+ server.login(SMTP_USER, SMTP_PASSWORD)
241
+ server.send_message(msg)
242
+
243
+ print(f"[Feedback] ✅ Email enviado para {destinatario}")
244
+ return True
245
+
246
+ except Exception as e:
247
+ print(f"[Feedback] ❌ Erro ao enviar email: {e}")
248
+ return False
249
+
250
+
251
+ def _gerar_email_confirmacao(ticket: Ticket) -> str:
252
+ """Generate HTML email template for ticket confirmation."""
253
+ tipo_display = {
254
+ "duvida": "Dúvida",
255
+ "feedback": "Feedback",
256
+ "reclamacao": "Reclamação",
257
+ "sugestao": "Sugestão",
258
+ "elogio": "Elogio",
259
+ "problema": "Problema",
260
+ "outro": "Outro",
261
+ }.get(ticket.tipo.lower(), ticket.tipo.title())
262
+
263
+ prioridade_display = {
264
+ "baixa": "🟢 Baixa",
265
+ "normal": "🟡 Normal",
266
+ "alta": "🟠 Alta",
267
+ "urgente": "🔴 Urgente",
268
+ }.get(ticket.prioridade.lower(), ticket.prioridade.title())
269
+
270
+ nome = ticket.nome_usuario or "Cliente"
271
+ brand_color = _email_config.get("brand_color", "#4A90D9")
272
+ brand_name = _email_config.get("brand_name", "Atendimento")
273
+ sla_message = _email_config.get("sla_message", "Retornaremos em breve.")
274
+
275
+ return f"""
276
+ <!DOCTYPE html>
277
+ <html>
278
+ <head>
279
+ <meta charset="UTF-8">
280
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
281
+ </head>
282
+ <body style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 0 auto; background-color: #f5f5f5; padding: 20px;">
283
+ <div style="background-color: {brand_color}; color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
284
+ <h1 style="margin: 0; font-size: 24px;">{brand_name}</h1>
285
+ <p style="margin: 10px 0 0 0; opacity: 0.9;">Ticket Registrado com Sucesso</p>
286
+ </div>
287
+
288
+ <div style="background-color: white; padding: 30px; border-radius: 0 0 10px 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
289
+ <p style="font-size: 16px; color: #333;">Olá <strong>{nome}</strong>,</p>
290
+
291
+ <p style="color: #666;">Recebemos sua solicitação e ela foi registrada com sucesso!</p>
292
+
293
+ <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0; border-left: 4px solid {brand_color};">
294
+ <table style="width: 100%; border-collapse: collapse;">
295
+ <tr>
296
+ <td style="padding: 8px 0; color: #666;">📋 Protocolo:</td>
297
+ <td style="padding: 8px 0; font-weight: bold; color: {brand_color};">{ticket.protocolo}</td>
298
+ </tr>
299
+ <tr>
300
+ <td style="padding: 8px 0; color: #666;">📌 Tipo:</td>
301
+ <td style="padding: 8px 0;">{tipo_display}</td>
302
+ </tr>
303
+ <tr>
304
+ <td style="padding: 8px 0; color: #666;">⚡ Prioridade:</td>
305
+ <td style="padding: 8px 0;">{prioridade_display}</td>
306
+ </tr>
307
+ <tr>
308
+ <td style="padding: 8px 0; color: #666;">📅 Data:</td>
309
+ <td style="padding: 8px 0;">{ticket.data_criacao.strftime('%d/%m/%Y às %H:%M')}</td>
310
+ </tr>
311
+ </table>
312
+ </div>
313
+
314
+ <div style="background-color: #fff; padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; margin: 20px 0;">
315
+ <p style="color: #666; margin: 0 0 10px 0; font-weight: bold;">📝 Descrição:</p>
316
+ <p style="color: #333; margin: 0; line-height: 1.6;">{ticket.descricao}</p>
317
+ </div>
318
+
319
+ <p style="color: #666; font-size: 14px;">{sla_message}</p>
320
+
321
+ <p style="color: #888; font-size: 13px; margin-top: 25px;">
322
+ 💡 Guarde o protocolo <strong>{ticket.protocolo}</strong> para acompanhamento.
323
+ </p>
324
+
325
+ <hr style="border: none; border-top: 1px solid #eee; margin: 25px 0;">
326
+
327
+ <p style="color: #999; font-size: 12px; text-align: center; margin: 0;">
328
+ Este email foi enviado automaticamente. Por favor, não responda.
329
+ </p>
330
+ </div>
331
+ </body>
332
+ </html>
333
+ """
334
+
335
+
336
+ def _gerar_email_equipe(ticket: Ticket) -> str:
337
+ """Generate HTML email template for team notification."""
338
+ tipo_display = {
339
+ "duvida": "❓ Dúvida",
340
+ "feedback": "💬 Feedback",
341
+ "reclamacao": "📢 Reclamação",
342
+ "sugestao": "💡 Sugestão",
343
+ "elogio": "⭐ Elogio",
344
+ "problema": "⚠️ Problema",
345
+ "outro": "📋 Outro",
346
+ }.get(ticket.tipo.lower(), ticket.tipo.title())
347
+
348
+ prioridade_colors = {
349
+ "baixa": "#28a745",
350
+ "normal": "#ffc107",
351
+ "alta": "#fd7e14",
352
+ "urgente": "#dc3545",
353
+ }
354
+ prioridade_color = prioridade_colors.get(ticket.prioridade.lower(), "#6c757d")
355
+
356
+ brand_color = _email_config.get("brand_color", "#4A90D9")
357
+ brand_name = _email_config.get("brand_name", "Atendimento")
358
+
359
+ return f"""
360
+ <!DOCTYPE html>
361
+ <html>
362
+ <head><meta charset="UTF-8"></head>
363
+ <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
364
+ <div style="background-color: {brand_color}; color: white; padding: 20px; text-align: center;">
365
+ <h2 style="margin: 0;">🎫 Novo Ticket - {brand_name}</h2>
366
+ </div>
367
+
368
+ <div style="padding: 20px; background-color: #f9f9f9;">
369
+ <div style="background-color: white; padding: 20px; border-radius: 8px; margin-bottom: 15px;">
370
+ <h3 style="margin: 0 0 15px 0; color: #333;">
371
+ {tipo_display}
372
+ <span style="background-color: {prioridade_color}; color: white; padding: 3px 10px; border-radius: 4px; font-size: 12px; margin-left: 10px;">
373
+ {ticket.prioridade.upper()}
374
+ </span>
375
+ </h3>
376
+
377
+ <table style="width: 100%;">
378
+ <tr>
379
+ <td style="padding: 5px 0; color: #666; width: 120px;">Protocolo:</td>
380
+ <td style="padding: 5px 0; font-weight: bold;">{ticket.protocolo}</td>
381
+ </tr>
382
+ <tr>
383
+ <td style="padding: 5px 0; color: #666;">Nome:</td>
384
+ <td style="padding: 5px 0;">{ticket.nome_usuario or 'Não informado'}</td>
385
+ </tr>
386
+ <tr>
387
+ <td style="padding: 5px 0; color: #666;">Email:</td>
388
+ <td style="padding: 5px 0;"><a href="mailto:{ticket.email_usuario}">{ticket.email_usuario}</a></td>
389
+ </tr>
390
+ <tr>
391
+ <td style="padding: 5px 0; color: #666;">Telefone:</td>
392
+ <td style="padding: 5px 0;">{ticket.telefone_usuario or 'Não informado'}</td>
393
+ </tr>
394
+ <tr>
395
+ <td style="padding: 5px 0; color: #666;">Data:</td>
396
+ <td style="padding: 5px 0;">{ticket.data_criacao.strftime('%d/%m/%Y às %H:%M')}</td>
397
+ </tr>
398
+ </table>
399
+ </div>
400
+
401
+ <div style="background-color: white; padding: 20px; border-radius: 8px;">
402
+ <h4 style="margin: 0 0 10px 0; color: #666;">📝 Descrição:</h4>
403
+ <p style="margin: 0; color: #333; line-height: 1.6; white-space: pre-wrap;">{ticket.descricao}</p>
404
+ </div>
405
+ </div>
406
+ </body>
407
+ </html>
408
+ """
409
+
410
+
411
+ # =============================================================================
412
+ # Feedback Tools
413
+ # =============================================================================
414
+
415
+ @function_tool
416
+ def criar_ticket(
417
+ tipo: str,
418
+ descricao: str,
419
+ email_usuario: str,
420
+ nome_usuario: str = "",
421
+ telefone_usuario: str = "",
422
+ prioridade: str = "normal",
423
+ categoria: str = "",
424
+ ) -> str:
425
+ """
426
+ Cria um ticket de feedback, dúvida, reclamação ou sugestão.
427
+
428
+ IMPORTANTE: Esta ferramenta registra solicitações para análise posterior.
429
+ Use quando o usuário quer:
430
+ - Tirar uma dúvida que precisa de pesquisa
431
+ - Enviar feedback sobre produto/serviço
432
+ - Fazer uma reclamação formal
433
+ - Dar uma sugestão de melhoria
434
+ - Fazer um elogio
435
+ - Reportar um problema técnico
436
+
437
+ Args:
438
+ tipo: Tipo do ticket. Valores aceitos:
439
+ - "duvida": Pergunta que precisa de pesquisa
440
+ - "feedback": Opinião sobre produto/serviço
441
+ - "reclamacao": Reclamação formal
442
+ - "sugestao": Sugestão de melhoria
443
+ - "elogio": Elogio ou agradecimento
444
+ - "problema": Problema técnico ou bug
445
+ descricao: Descrição detalhada da solicitação (mínimo 10 caracteres)
446
+ email_usuario: Email do usuário para resposta e acompanhamento
447
+ nome_usuario: Nome do usuário (opcional, mas recomendado)
448
+ telefone_usuario: Telefone para contato alternativo (opcional)
449
+ prioridade: Nível de urgência. Valores aceitos:
450
+ - "baixa": Pode aguardar
451
+ - "normal": Atendimento padrão (default)
452
+ - "alta": Requer atenção prioritária
453
+ - "urgente": Crítico, precisa de ação imediata
454
+ categoria: Categoria adicional do ticket (opcional)
455
+
456
+ Returns:
457
+ Confirmação com número do protocolo e detalhes
458
+ """
459
+ # Validar tipo
460
+ tipos_validos = ["duvida", "feedback", "reclamacao", "sugestao", "elogio", "problema", "outro"]
461
+ tipo_norm = tipo.lower().strip().replace("ã", "a").replace("ç", "c")
462
+
463
+ if tipo_norm not in tipos_validos:
464
+ return f"""❌ **Tipo inválido:** {tipo}
465
+
466
+ 📋 **Tipos aceitos:**
467
+ - `duvida` - Pergunta que precisa de pesquisa
468
+ - `feedback` - Opinião sobre produto/serviço
469
+ - `reclamacao` - Reclamação formal
470
+ - `sugestao` - Sugestão de melhoria
471
+ - `elogio` - Elogio ou agradecimento
472
+ - `problema` - Problema técnico"""
473
+
474
+ # Validar descrição
475
+ if not descricao or len(descricao.strip()) < 10:
476
+ return "❌ **Descrição muito curta.** Por favor, forneça mais detalhes (mínimo 10 caracteres)."
477
+
478
+ # Validar email
479
+ email_norm = email_usuario.strip().lower()
480
+ if not _validar_email(email_norm):
481
+ return f"❌ **Email inválido:** {email_usuario}\n\nPor favor, informe um email válido para que possamos responder."
482
+
483
+ # Validar prioridade
484
+ prioridades_validas = ["baixa", "normal", "alta", "urgente"]
485
+ prioridade_norm = prioridade.lower().strip()
486
+ if prioridade_norm not in prioridades_validas:
487
+ prioridade_norm = "normal"
488
+
489
+ # Auto-classificar prioridade para reclamações
490
+ if tipo_norm == "reclamacao" and prioridade_norm == "normal":
491
+ prioridade_norm = "alta"
492
+
493
+ # Criar ticket
494
+ protocolo = _gerar_protocolo()
495
+ ticket = Ticket(
496
+ protocolo=protocolo,
497
+ tipo=tipo_norm,
498
+ descricao=descricao.strip(),
499
+ email_usuario=email_norm,
500
+ nome_usuario=nome_usuario.strip() if nome_usuario else "",
501
+ telefone_usuario=telefone_usuario.strip() if telefone_usuario else "",
502
+ prioridade=prioridade_norm,
503
+ status="aberto",
504
+ categoria=categoria.strip() if categoria else "",
505
+ )
506
+
507
+ ticket.adicionar_historico(
508
+ "Ticket criado",
509
+ f"Tipo: {tipo_norm}, Prioridade: {prioridade_norm}"
510
+ )
511
+
512
+ _salvar_ticket(ticket)
513
+
514
+ # Ícones
515
+ icone_tipo = {
516
+ "duvida": "❓",
517
+ "feedback": "💬",
518
+ "reclamacao": "📢",
519
+ "sugestao": "💡",
520
+ "elogio": "⭐",
521
+ "problema": "⚠️",
522
+ "outro": "📋",
523
+ }.get(tipo_norm, "📋")
524
+
525
+ icone_prioridade = {
526
+ "baixa": "🟢",
527
+ "normal": "🟡",
528
+ "alta": "🟠",
529
+ "urgente": "🔴",
530
+ }.get(prioridade_norm, "🟡")
531
+
532
+ tipo_display = {
533
+ "duvida": "Dúvida",
534
+ "feedback": "Feedback",
535
+ "reclamacao": "Reclamação",
536
+ "sugestao": "Sugestão",
537
+ "elogio": "Elogio",
538
+ "problema": "Problema",
539
+ "outro": "Outro",
540
+ }.get(tipo_norm, tipo_norm.title())
541
+
542
+ return f"""
543
+ ✅ **Ticket Criado com Sucesso!**
544
+
545
+ ═══════════════════════════════════════
546
+ 📋 **Protocolo:** {protocolo}
547
+ ═══════════════════════════════════════
548
+
549
+ {icone_tipo} **Tipo:** {tipo_display}
550
+ {icone_prioridade} **Prioridade:** {prioridade_norm.upper()}
551
+ 📅 **Data:** {ticket.data_criacao.strftime('%d/%m/%Y às %H:%M')}
552
+
553
+ 👤 **Dados de Contato:**
554
+ - Nome: {nome_usuario or 'Não informado'}
555
+ - Email: {email_norm}
556
+ - Telefone: {telefone_usuario or 'Não informado'}
557
+
558
+ 📝 **Descrição:**
559
+ {descricao[:200]}{'...' if len(descricao) > 200 else ''}
560
+
561
+ ═══════════════════════════════════════
562
+
563
+ 💡 **Guarde o protocolo {protocolo}** para consultar o status.
564
+
565
+ 📧 Use `enviar_email_confirmacao("{protocolo}")` para enviar confirmação ao usuário.
566
+ """
567
+
568
+
569
+ @function_tool
570
+ def enviar_email_confirmacao(protocolo: str) -> str:
571
+ """
572
+ Envia email de confirmação do ticket para o usuário.
573
+
574
+ IMPORTANTE: Chame esta ferramenta APÓS criar o ticket para
575
+ enviar a confirmação oficial por email.
576
+
577
+ Args:
578
+ protocolo: Número do protocolo do ticket (ex: TKT-20240106-ABC123)
579
+
580
+ Returns:
581
+ Confirmação do envio ou mensagem de erro
582
+ """
583
+ ticket = _buscar_ticket(protocolo)
584
+
585
+ if not ticket:
586
+ return f"""❌ **Ticket não encontrado:** {protocolo}
587
+
588
+ Verifique se o número do protocolo está correto.
589
+ Formato esperado: {_protocol_prefix}-YYYYMMDD-XXXXXX"""
590
+
591
+ # Enviar para usuário
592
+ assunto = f"[{_email_config.get('brand_name', 'Atendimento')}] Ticket {ticket.protocolo} - Recebemos sua solicitação"
593
+ corpo_html = _gerar_email_confirmacao(ticket)
594
+
595
+ enviado_usuario = _enviar_email(ticket.email_usuario, assunto, corpo_html)
596
+
597
+ # Enviar para equipe (se configurado)
598
+ enviado_equipe = False
599
+ if FEEDBACK_EMAIL_DESTINO:
600
+ assunto_equipe = f"[NOVO TICKET] {ticket.tipo.upper()} - {ticket.protocolo}"
601
+ corpo_equipe = _gerar_email_equipe(ticket)
602
+ enviado_equipe = _enviar_email(FEEDBACK_EMAIL_DESTINO, assunto_equipe, corpo_equipe)
603
+
604
+ # Registrar no histórico
605
+ ticket.adicionar_historico(
606
+ "Email de confirmação",
607
+ f"Usuário: {'✅' if enviado_usuario else '❌'}, Equipe: {'✅' if enviado_equipe else '❌ (não configurado)' if not FEEDBACK_EMAIL_DESTINO else '❌'}"
608
+ )
609
+ _salvar_ticket(ticket)
610
+
611
+ if enviado_usuario:
612
+ return f"""✅ **Email de confirmação enviado!**
613
+
614
+ 📧 Destinatário: {ticket.email_usuario}
615
+ 📋 Protocolo: {ticket.protocolo}
616
+ {"📧 Equipe também notificada!" if enviado_equipe else ""}
617
+
618
+ O usuário receberá o email em alguns minutos.
619
+ """
620
+ else:
621
+ return f"""⚠️ **Email não enviado** (SMTP não configurado)
622
+
623
+ 📋 Protocolo: {ticket.protocolo}
624
+ 📧 Destinatário: {ticket.email_usuario}
625
+
626
+ 💡 Para habilitar envio de emails, configure as variáveis de ambiente:
627
+ - SMTP_HOST
628
+ - SMTP_PORT
629
+ - SMTP_USER
630
+ - SMTP_PASSWORD
631
+ - SMTP_FROM
632
+
633
+ O ticket foi criado e pode ser consultado pelo protocolo.
634
+ """
635
+
636
+
637
+ @function_tool
638
+ def consultar_ticket(protocolo: str) -> str:
639
+ """
640
+ Consulta detalhes e status de um ticket pelo protocolo.
641
+
642
+ Args:
643
+ protocolo: Número do protocolo (ex: TKT-20240106-ABC123, SAC-20240106-XYZ789)
644
+
645
+ Returns:
646
+ Detalhes completos do ticket ou mensagem de não encontrado
647
+ """
648
+ ticket = _buscar_ticket(protocolo)
649
+
650
+ if not ticket:
651
+ return f"""❌ **Ticket não encontrado:** {protocolo}
652
+
653
+ Verifique se o número do protocolo está correto.
654
+ Formato esperado: {_protocol_prefix}-YYYYMMDD-XXXXXX
655
+
656
+ 💡 Use `listar_meus_tickets("email@exemplo.com")` para ver todos os seus tickets.
657
+ """
658
+
659
+ # Ícones
660
+ icone_tipo = {
661
+ "duvida": "❓",
662
+ "feedback": "💬",
663
+ "reclamacao": "📢",
664
+ "sugestao": "💡",
665
+ "elogio": "⭐",
666
+ "problema": "⚠️",
667
+ "outro": "📋",
668
+ }.get(ticket.tipo.lower(), "📋")
669
+
670
+ icone_status = {
671
+ "aberto": "🟡",
672
+ "em_andamento": "🔵",
673
+ "aguardando_usuario": "🟠",
674
+ "resolvido": "🟢",
675
+ "fechado": "⚫",
676
+ "cancelado": "🔴",
677
+ }.get(ticket.status.lower(), "⚪")
678
+
679
+ icone_prioridade = {
680
+ "baixa": "🟢",
681
+ "normal": "🟡",
682
+ "alta": "🟠",
683
+ "urgente": "🔴",
684
+ }.get(ticket.prioridade.lower(), "🟡")
685
+
686
+ tipo_display = ticket.tipo.replace("_", " ").title()
687
+ status_display = ticket.status.replace("_", " ").upper()
688
+
689
+ resultado = f"""
690
+ 📋 **Consulta de Ticket**
691
+
692
+ ═══════════════════════════════════════
693
+ 🔖 **Protocolo:** {ticket.protocolo}
694
+ {icone_status} **Status:** {status_display}
695
+ ═══════════════════════════════════════
696
+
697
+ {icone_tipo} **Tipo:** {tipo_display}
698
+ {icone_prioridade} **Prioridade:** {ticket.prioridade.upper()}
699
+
700
+ 👤 **Solicitante:**
701
+ - Nome: {ticket.nome_usuario or 'Não informado'}
702
+ - Email: {ticket.email_usuario}
703
+ - Telefone: {ticket.telefone_usuario or 'Não informado'}
704
+
705
+ 📅 **Datas:**
706
+ - Criado: {ticket.data_criacao.strftime('%d/%m/%Y às %H:%M')}
707
+ - Atualizado: {ticket.data_atualizacao.strftime('%d/%m/%Y às %H:%M')}
708
+
709
+ 📝 **Descrição:**
710
+ {ticket.descricao}
711
+ """
712
+
713
+ if ticket.resposta:
714
+ resultado += f"""
715
+ 💬 **Resposta:**
716
+ {ticket.resposta}
717
+ """
718
+
719
+ if ticket.atendente:
720
+ resultado += f"\n👨‍💼 **Atendente:** {ticket.atendente}"
721
+
722
+ if ticket.historico:
723
+ resultado += "\n\n📜 **Histórico:**\n"
724
+ for h in ticket.historico[-5:]: # Últimos 5 registros
725
+ data = h.get("data", "")[:16].replace("T", " ")
726
+ resultado += f"- [{data}] {h.get('acao', '')}"
727
+ if h.get("detalhes"):
728
+ resultado += f" - {h.get('detalhes')}"
729
+ resultado += "\n"
730
+
731
+ return resultado
732
+
733
+
734
+ @function_tool
735
+ def listar_meus_tickets(email: str, status: str = "") -> str:
736
+ """
737
+ Lista todos os tickets de um usuário pelo email.
738
+
739
+ Args:
740
+ email: Email do usuário
741
+ status: Filtrar por status (opcional): aberto, em_andamento, resolvido, fechado
742
+
743
+ Returns:
744
+ Lista de tickets ou mensagem se não houver nenhum
745
+ """
746
+ if not _validar_email(email):
747
+ return f"❌ **Email inválido:** {email}"
748
+
749
+ tickets = _listar_tickets(email=email.lower(), status=status if status else None)
750
+
751
+ if not tickets:
752
+ msg = f"📭 **Nenhum ticket encontrado** para {email}"
753
+ if status:
754
+ msg += f" com status '{status}'"
755
+ return msg
756
+
757
+ resultado = f"""
758
+ 📋 **Tickets de {email}**
759
+
760
+ Total: {len(tickets)} ticket(s)
761
+ {'Status: ' + status.upper() if status else ''}
762
+
763
+ ═══════════════════════════════════════
764
+ """
765
+
766
+ for t in tickets[:10]: # Mostrar até 10
767
+ icone_status = {
768
+ "aberto": "🟡",
769
+ "em_andamento": "🔵",
770
+ "aguardando_usuario": "🟠",
771
+ "resolvido": "🟢",
772
+ "fechado": "⚫",
773
+ "cancelado": "🔴",
774
+ }.get(t.status.lower(), "⚪")
775
+
776
+ icone_tipo = {
777
+ "duvida": "❓",
778
+ "feedback": "💬",
779
+ "reclamacao": "📢",
780
+ "sugestao": "💡",
781
+ "elogio": "⭐",
782
+ "problema": "⚠️",
783
+ }.get(t.tipo.lower(), "📋")
784
+
785
+ resultado += f"""
786
+ {icone_status} **{t.protocolo}** {icone_tipo}
787
+ 📅 {t.data_criacao.strftime('%d/%m/%Y')} | {t.tipo.title()} | {t.status.replace('_', ' ').title()}
788
+ 📝 {t.descricao[:50]}{'...' if len(t.descricao) > 50 else ''}
789
+ """
790
+
791
+ if len(tickets) > 10:
792
+ resultado += f"\n... e mais {len(tickets) - 10} ticket(s)"
793
+
794
+ resultado += "\n═══════════════════════════════════════"
795
+ resultado += "\n💡 Use `consultar_ticket(\"PROTOCOLO\")` para ver detalhes."
796
+
797
+ return resultado
798
+
799
+
800
+ @function_tool
801
+ def atualizar_ticket(
802
+ protocolo: str,
803
+ status: str = "",
804
+ resposta: str = "",
805
+ atendente: str = "",
806
+ ) -> str:
807
+ """
808
+ Atualiza o status ou adiciona resposta a um ticket.
809
+
810
+ Args:
811
+ protocolo: Número do protocolo do ticket
812
+ status: Novo status (aberto, em_andamento, aguardando_usuario, resolvido, fechado, cancelado)
813
+ resposta: Resposta ou comentário a adicionar
814
+ atendente: Nome do atendente responsável
815
+
816
+ Returns:
817
+ Confirmação da atualização
818
+ """
819
+ ticket = _buscar_ticket(protocolo)
820
+
821
+ if not ticket:
822
+ return f"❌ **Ticket não encontrado:** {protocolo}"
823
+
824
+ atualizacoes = []
825
+
826
+ if status:
827
+ status_validos = ["aberto", "em_andamento", "aguardando_usuario", "resolvido", "fechado", "cancelado"]
828
+ status_norm = status.lower().strip().replace(" ", "_")
829
+ if status_norm in status_validos:
830
+ status_anterior = ticket.status
831
+ ticket.status = status_norm
832
+ ticket.adicionar_historico("Status alterado", f"{status_anterior} → {status_norm}")
833
+ atualizacoes.append(f"Status: {status_norm.upper()}")
834
+
835
+ if resposta:
836
+ ticket.resposta = resposta.strip()
837
+ ticket.adicionar_historico("Resposta adicionada", resposta[:50] + "...")
838
+ atualizacoes.append("Resposta adicionada")
839
+
840
+ if atendente:
841
+ ticket.atendente = atendente.strip()
842
+ ticket.adicionar_historico("Atendente atribuído", atendente)
843
+ atualizacoes.append(f"Atendente: {atendente}")
844
+
845
+ if not atualizacoes:
846
+ return "⚠️ Nenhuma atualização fornecida. Informe status, resposta ou atendente."
847
+
848
+ _salvar_ticket(ticket)
849
+
850
+ return f"""✅ **Ticket atualizado!**
851
+
852
+ 📋 **Protocolo:** {ticket.protocolo}
853
+
854
+ 📝 **Atualizações:**
855
+ {chr(10).join('- ' + a for a in atualizacoes)}
856
+
857
+ 📅 **Atualizado em:** {ticket.data_atualizacao.strftime('%d/%m/%Y às %H:%M')}
858
+ """
859
+
860
+
861
+ # =============================================================================
862
+ # List of Tools
863
+ # =============================================================================
864
+
865
+ FEEDBACK_TOOLS = [
866
+ criar_ticket,
867
+ enviar_email_confirmacao,
868
+ consultar_ticket,
869
+ listar_meus_tickets,
870
+ atualizar_ticket,
871
+ ]
872
+
873
+
874
+ # =============================================================================
875
+ # Instruções movidas para atendentepro/prompts/feedback.py
876
+ # =============================================================================
877
+
878
+ # DEFAULT_FEEDBACK_INSTRUCTIONS, FEEDBACK_INTRO e DEFAULT_TICKET_TYPES
879
+ # estão em prompts/feedback.py
880
+
881
+
882
+ # =============================================================================
883
+ # Create Feedback Agent
884
+ # =============================================================================
885
+
886
+ def create_feedback_agent(
887
+ protocol_prefix: str = "TKT",
888
+ email_brand_color: str = "#4A90D9",
889
+ email_brand_name: str = "Atendimento",
890
+ email_sla_message: str = "Retornaremos em até 24h úteis.",
891
+ ticket_types: Optional[List[str]] = None,
892
+ handoffs: Optional[List] = None,
893
+ tools: Optional[List] = None,
894
+ guardrails: Optional[List["GuardrailCallable"]] = None,
895
+ name: str = "Feedback Agent",
896
+ custom_instructions: Optional[str] = None,
897
+ ) -> FeedbackAgent:
898
+ """
899
+ Create a Feedback Agent for collecting user feedback, questions, and complaints.
900
+
901
+ The feedback agent handles:
902
+ - Questions (dúvidas) that need research
903
+ - Product/service feedback
904
+ - Formal complaints (reclamações)
905
+ - Improvement suggestions
906
+ - Compliments (elogios)
907
+ - Technical problems
908
+
909
+ All interactions are tracked with unique protocol numbers.
910
+
911
+ Args:
912
+ protocol_prefix: Prefix for ticket protocols (e.g., "SAC", "TKT", "SUP").
913
+ email_brand_color: Hex color for email branding (e.g., "#660099").
914
+ email_brand_name: Brand name shown in emails.
915
+ email_sla_message: SLA message shown in confirmation emails.
916
+ ticket_types: Custom list of allowed ticket types (default: all).
917
+ handoffs: List of agents to hand off to.
918
+ tools: Additional custom tools.
919
+ guardrails: List of input guardrails.
920
+ name: Agent name.
921
+ custom_instructions: Override default instructions.
922
+
923
+ Returns:
924
+ Configured Feedback Agent instance.
925
+
926
+ Example:
927
+ >>> feedback = create_feedback_agent(
928
+ ... protocol_prefix="SAC",
929
+ ... email_brand_color="#660099",
930
+ ... email_brand_name="Vivo Empresas",
931
+ ... )
932
+ """
933
+ # Configure storage settings
934
+ configure_feedback_storage(
935
+ protocol_prefix=protocol_prefix,
936
+ email_brand_color=email_brand_color,
937
+ email_brand_name=email_brand_name,
938
+ email_sla_message=email_sla_message,
939
+ )
940
+
941
+ # Build instructions using prompt builder
942
+ if custom_instructions:
943
+ instructions = f"{RECOMMENDED_PROMPT_PREFIX} {custom_instructions}"
944
+ else:
945
+ # Usar o prompt builder do módulo prompts
946
+ instructions = f"{RECOMMENDED_PROMPT_PREFIX}\n{get_feedback_prompt(ticket_types=ticket_types, protocol_prefix=protocol_prefix, brand_name=email_brand_name, sla_message=email_sla_message)}"
947
+
948
+ # Combine tools
949
+ agent_tools = list(FEEDBACK_TOOLS)
950
+ if tools:
951
+ agent_tools.extend(tools)
952
+
953
+ return Agent[ContextNote](
954
+ name=name,
955
+ handoff_description="Registra dúvidas, feedbacks, reclamações, sugestões e elogios dos usuários através de tickets com protocolo.",
956
+ instructions=instructions,
957
+ tools=agent_tools,
958
+ handoffs=handoffs or [],
959
+ input_guardrails=guardrails or [],
960
+ )
961
+