atendentepro 0.3.0__py3-none-any.whl → 0.5.2__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/README.md +651 -74
- atendentepro/__init__.py +17 -3
- atendentepro/agents/__init__.py +11 -0
- atendentepro/agents/answer.py +1 -2
- atendentepro/agents/escalation.py +510 -0
- atendentepro/agents/feedback.py +961 -0
- atendentepro/license.py +3 -3
- atendentepro/network.py +115 -15
- atendentepro/prompts/__init__.py +10 -0
- atendentepro/prompts/answer.py +2 -3
- atendentepro/prompts/escalation.py +190 -0
- atendentepro/prompts/feedback.py +232 -0
- {atendentepro-0.3.0.dist-info → atendentepro-0.5.2.dist-info}/METADATA +58 -44
- {atendentepro-0.3.0.dist-info → atendentepro-0.5.2.dist-info}/RECORD +18 -14
- {atendentepro-0.3.0.dist-info → atendentepro-0.5.2.dist-info}/licenses/LICENSE +3 -3
- {atendentepro-0.3.0.dist-info → atendentepro-0.5.2.dist-info}/WHEEL +0 -0
- {atendentepro-0.3.0.dist-info → atendentepro-0.5.2.dist-info}/entry_points.txt +0 -0
- {atendentepro-0.3.0.dist-info → atendentepro-0.5.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
|