atendentepro 0.6.6__py3-none-any.whl → 0.6.8__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/agents/escalation.py +68 -9
- atendentepro/agents/feedback.py +117 -12
- atendentepro/network.py +83 -4
- atendentepro/templates/__init__.py +12 -0
- atendentepro/templates/manager.py +139 -0
- {atendentepro-0.6.6.dist-info → atendentepro-0.6.8.dist-info}/METADATA +1 -1
- {atendentepro-0.6.6.dist-info → atendentepro-0.6.8.dist-info}/RECORD +11 -11
- {atendentepro-0.6.6.dist-info → atendentepro-0.6.8.dist-info}/WHEEL +0 -0
- {atendentepro-0.6.6.dist-info → atendentepro-0.6.8.dist-info}/entry_points.txt +0 -0
- {atendentepro-0.6.6.dist-info → atendentepro-0.6.8.dist-info}/licenses/LICENSE +0 -0
- {atendentepro-0.6.6.dist-info → atendentepro-0.6.8.dist-info}/top_level.txt +0 -0
|
@@ -47,6 +47,13 @@ DEFAULT_BUSINESS_HOURS = {
|
|
|
47
47
|
"days": [0, 1, 2, 3, 4], # Seg-Sex
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
# Configurable business hours (None = use DEFAULT_BUSINESS_HOURS)
|
|
51
|
+
_configured_business_hours: Optional[Dict[str, Any]] = None
|
|
52
|
+
|
|
53
|
+
# Configurable priority keywords (None = use defaults)
|
|
54
|
+
_priority_keywords_urgent: Optional[List[str]] = None
|
|
55
|
+
_priority_keywords_high: Optional[List[str]] = None
|
|
56
|
+
|
|
50
57
|
|
|
51
58
|
# =============================================================================
|
|
52
59
|
# Storage de Escalações (em memória - substituir por DB em produção)
|
|
@@ -139,22 +146,46 @@ def _notificar_equipe(escalation: Escalation) -> bool:
|
|
|
139
146
|
|
|
140
147
|
def _verificar_disponibilidade() -> Dict[str, Any]:
|
|
141
148
|
"""Verifica se atendimento humano está disponível."""
|
|
149
|
+
global _configured_business_hours
|
|
142
150
|
agora = datetime.now()
|
|
143
151
|
hora = agora.hour
|
|
144
152
|
dia = agora.weekday()
|
|
145
153
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
# Use configured business hours or fall back to defaults
|
|
155
|
+
if _configured_business_hours:
|
|
156
|
+
business_hours = _configured_business_hours
|
|
157
|
+
hora_inicio = business_hours.get("start", DEFAULT_BUSINESS_HOURS["start"])
|
|
158
|
+
hora_fim = business_hours.get("end", DEFAULT_BUSINESS_HOURS["end"])
|
|
159
|
+
days = business_hours.get("days", DEFAULT_BUSINESS_HOURS["days"])
|
|
160
|
+
else:
|
|
161
|
+
# Verificar variáveis de ambiente para horário customizado
|
|
162
|
+
hora_inicio = int(os.getenv("ESCALATION_HOUR_START", DEFAULT_BUSINESS_HOURS["start"]))
|
|
163
|
+
hora_fim = int(os.getenv("ESCALATION_HOUR_END", DEFAULT_BUSINESS_HOURS["end"]))
|
|
164
|
+
days = DEFAULT_BUSINESS_HOURS["days"]
|
|
165
|
+
|
|
166
|
+
# Ensure days is a list of integers
|
|
167
|
+
if isinstance(days, list):
|
|
168
|
+
days_int = [d if isinstance(d, int) else int(d) for d in days]
|
|
169
|
+
else:
|
|
170
|
+
days_int = DEFAULT_BUSINESS_HOURS["days"]
|
|
171
|
+
|
|
172
|
+
disponivel = dia in days_int and hora_inicio <= hora < hora_fim
|
|
149
173
|
|
|
150
|
-
|
|
174
|
+
# Format days for display
|
|
175
|
+
day_names = ["Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado", "Domingo"]
|
|
176
|
+
if len(days_int) == 1:
|
|
177
|
+
dias_atendimento = day_names[days_int[0]]
|
|
178
|
+
elif len(days_int) == 5 and days_int == [0, 1, 2, 3, 4]:
|
|
179
|
+
dias_atendimento = "Segunda a Sexta"
|
|
180
|
+
else:
|
|
181
|
+
dias_atendimento = ", ".join(day_names[d] for d in sorted(days_int))
|
|
151
182
|
|
|
152
183
|
return {
|
|
153
184
|
"disponivel": disponivel,
|
|
154
185
|
"hora_atual": agora.strftime("%H:%M"),
|
|
155
|
-
"dia_semana": [
|
|
186
|
+
"dia_semana": day_names[dia],
|
|
156
187
|
"horario_atendimento": f"{hora_inicio:02d}:00 - {hora_fim:02d}:00",
|
|
157
|
-
"dias_atendimento":
|
|
188
|
+
"dias_atendimento": dias_atendimento,
|
|
158
189
|
}
|
|
159
190
|
|
|
160
191
|
|
|
@@ -166,15 +197,16 @@ def _classificar_prioridade(motivo: str, categoria: str) -> str:
|
|
|
166
197
|
"""
|
|
167
198
|
Classifica automaticamente a prioridade baseado no motivo e categoria.
|
|
168
199
|
"""
|
|
200
|
+
global _priority_keywords_urgent, _priority_keywords_high
|
|
169
201
|
motivo_lower = motivo.lower()
|
|
170
202
|
|
|
171
|
-
#
|
|
172
|
-
palavras_urgentes = [
|
|
203
|
+
# Use configured keywords or defaults
|
|
204
|
+
palavras_urgentes = _priority_keywords_urgent or [
|
|
173
205
|
"urgente", "emergência", "emergencia", "crítico", "critico",
|
|
174
206
|
"não funciona", "parou", "bloqueado", "cancelar", "prejuízo"
|
|
175
207
|
]
|
|
176
208
|
|
|
177
|
-
palavras_alta = [
|
|
209
|
+
palavras_alta = _priority_keywords_high or [
|
|
178
210
|
"reclamação", "reclamacao", "insatisfeito", "problema grave",
|
|
179
211
|
"já tentei", "terceira vez", "não resolve"
|
|
180
212
|
]
|
|
@@ -449,6 +481,9 @@ ESCALATION_TOOLS = [
|
|
|
449
481
|
def create_escalation_agent(
|
|
450
482
|
escalation_triggers: str = "",
|
|
451
483
|
escalation_channels: str = "",
|
|
484
|
+
business_hours: Optional[Dict[str, Any]] = None,
|
|
485
|
+
priority_keywords_urgent: Optional[List[str]] = None,
|
|
486
|
+
priority_keywords_high: Optional[List[str]] = None,
|
|
452
487
|
handoffs: Optional[List] = None,
|
|
453
488
|
tools: Optional[List] = None,
|
|
454
489
|
guardrails: Optional[List["GuardrailCallable"]] = None,
|
|
@@ -472,6 +507,9 @@ def create_escalation_agent(
|
|
|
472
507
|
Args:
|
|
473
508
|
escalation_triggers: Custom triggers for escalation (keywords, situations).
|
|
474
509
|
escalation_channels: Available contact channels description.
|
|
510
|
+
business_hours: Optional dict with "start", "end", and "days" (list of weekday numbers 0-6 or day names).
|
|
511
|
+
priority_keywords_urgent: Optional list of keywords that indicate urgent priority.
|
|
512
|
+
priority_keywords_high: Optional list of keywords that indicate high priority.
|
|
475
513
|
handoffs: List of agents to hand off to (usually triage to return).
|
|
476
514
|
tools: Additional tools (custom notifications, integrations).
|
|
477
515
|
guardrails: List of input guardrails.
|
|
@@ -492,6 +530,27 @@ def create_escalation_agent(
|
|
|
492
530
|
>>> triage.handoffs.append(escalation)
|
|
493
531
|
>>> flow.handoffs.append(escalation)
|
|
494
532
|
"""
|
|
533
|
+
global _configured_business_hours, _priority_keywords_urgent, _priority_keywords_high
|
|
534
|
+
|
|
535
|
+
# Configure business hours
|
|
536
|
+
if business_hours:
|
|
537
|
+
# Convert day names to weekday numbers if needed
|
|
538
|
+
days = business_hours.get("days", [])
|
|
539
|
+
if days and isinstance(days[0], str):
|
|
540
|
+
day_map = {
|
|
541
|
+
"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
|
|
542
|
+
"friday": 4, "saturday": 5, "sunday": 6,
|
|
543
|
+
}
|
|
544
|
+
days_int = [day_map.get(day.lower(), day) for day in days if day.lower() in day_map]
|
|
545
|
+
business_hours = {**business_hours, "days": days_int}
|
|
546
|
+
_configured_business_hours = business_hours
|
|
547
|
+
else:
|
|
548
|
+
_configured_business_hours = None
|
|
549
|
+
|
|
550
|
+
# Configure priority keywords
|
|
551
|
+
_priority_keywords_urgent = priority_keywords_urgent
|
|
552
|
+
_priority_keywords_high = priority_keywords_high
|
|
553
|
+
|
|
495
554
|
if custom_instructions:
|
|
496
555
|
instructions = f"{RECOMMENDED_PROMPT_PREFIX} {custom_instructions}"
|
|
497
556
|
else:
|
atendentepro/agents/feedback.py
CHANGED
|
@@ -30,6 +30,7 @@ from email.mime.multipart import MIMEMultipart
|
|
|
30
30
|
from typing import Optional, List, Dict, Any, TYPE_CHECKING, Callable
|
|
31
31
|
from dataclasses import dataclass, field
|
|
32
32
|
from enum import Enum
|
|
33
|
+
from pathlib import Path
|
|
33
34
|
|
|
34
35
|
from agents import Agent, function_tool
|
|
35
36
|
|
|
@@ -116,14 +117,22 @@ class Ticket:
|
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
# =============================================================================
|
|
119
|
-
# Storage (in-memory
|
|
120
|
+
# Storage (in-memory + JSON file persistence)
|
|
120
121
|
# =============================================================================
|
|
121
122
|
|
|
123
|
+
import json
|
|
124
|
+
|
|
122
125
|
_tickets_storage: Dict[str, Ticket] = {}
|
|
123
126
|
|
|
124
127
|
# Protocol prefix (can be customized per client)
|
|
125
128
|
_protocol_prefix: str = "TKT"
|
|
126
129
|
|
|
130
|
+
# Allowed ticket types (None = accept any type)
|
|
131
|
+
_allowed_ticket_types: Optional[List[str]] = None
|
|
132
|
+
|
|
133
|
+
# Storage path for persistence
|
|
134
|
+
_storage_path: Optional[Path] = None
|
|
135
|
+
|
|
127
136
|
# Email configuration
|
|
128
137
|
_email_config: Dict[str, Any] = {
|
|
129
138
|
"enabled": True,
|
|
@@ -138,6 +147,8 @@ def configure_feedback_storage(
|
|
|
138
147
|
email_brand_color: str = "#4A90D9",
|
|
139
148
|
email_brand_name: str = "Atendimento",
|
|
140
149
|
email_sla_message: str = "Retornaremos em até 24h úteis.",
|
|
150
|
+
allowed_ticket_types: Optional[List[str]] = None,
|
|
151
|
+
storage_path: Optional[Path] = None,
|
|
141
152
|
) -> None:
|
|
142
153
|
"""
|
|
143
154
|
Configure feedback storage settings.
|
|
@@ -147,14 +158,102 @@ def configure_feedback_storage(
|
|
|
147
158
|
email_brand_color: Hex color for email template branding
|
|
148
159
|
email_brand_name: Brand name for email template
|
|
149
160
|
email_sla_message: SLA message shown in confirmation email
|
|
161
|
+
allowed_ticket_types: List of allowed ticket types (None = accept any)
|
|
162
|
+
storage_path: Path to JSON file for persistence (None = use default or env var)
|
|
150
163
|
"""
|
|
151
|
-
global _protocol_prefix, _email_config
|
|
164
|
+
global _protocol_prefix, _email_config, _allowed_ticket_types, _storage_path
|
|
152
165
|
_protocol_prefix = protocol_prefix
|
|
153
166
|
_email_config.update({
|
|
154
167
|
"brand_color": email_brand_color,
|
|
155
168
|
"brand_name": email_brand_name,
|
|
156
169
|
"sla_message": email_sla_message,
|
|
157
170
|
})
|
|
171
|
+
_allowed_ticket_types = allowed_ticket_types
|
|
172
|
+
|
|
173
|
+
# Set storage path from parameter, env var, or default
|
|
174
|
+
if storage_path:
|
|
175
|
+
_storage_path = Path(storage_path)
|
|
176
|
+
else:
|
|
177
|
+
env_path = os.getenv("FEEDBACK_STORAGE_PATH")
|
|
178
|
+
if env_path:
|
|
179
|
+
_storage_path = Path(env_path)
|
|
180
|
+
else:
|
|
181
|
+
# Default: current working directory
|
|
182
|
+
_storage_path = Path.cwd() / "feedback_tickets.json"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _get_storage_path() -> Path:
|
|
186
|
+
"""Get the storage path, initializing default if needed."""
|
|
187
|
+
global _storage_path
|
|
188
|
+
if _storage_path is None:
|
|
189
|
+
env_path = os.getenv("FEEDBACK_STORAGE_PATH")
|
|
190
|
+
if env_path:
|
|
191
|
+
_storage_path = Path(env_path)
|
|
192
|
+
else:
|
|
193
|
+
_storage_path = Path.cwd() / "feedback_tickets.json"
|
|
194
|
+
return _storage_path
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _load_tickets_from_file() -> None:
|
|
198
|
+
"""Load tickets from JSON file into memory."""
|
|
199
|
+
global _tickets_storage
|
|
200
|
+
storage_path = _get_storage_path()
|
|
201
|
+
|
|
202
|
+
if not storage_path.exists():
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
with open(storage_path, "r", encoding="utf-8") as f:
|
|
207
|
+
data = json.load(f)
|
|
208
|
+
|
|
209
|
+
tickets_data = data.get("tickets", {})
|
|
210
|
+
_tickets_storage = {}
|
|
211
|
+
|
|
212
|
+
for protocolo, ticket_data in tickets_data.items():
|
|
213
|
+
# Convert dict back to Ticket dataclass
|
|
214
|
+
ticket_data["data_criacao"] = datetime.fromisoformat(ticket_data["data_criacao"])
|
|
215
|
+
ticket_data["data_atualizacao"] = datetime.fromisoformat(ticket_data["data_atualizacao"])
|
|
216
|
+
_tickets_storage[protocolo.upper()] = Ticket(**ticket_data)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(f"[Feedback] ⚠️ Erro ao carregar tickets do arquivo: {e}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _save_tickets_to_file() -> None:
|
|
222
|
+
"""Save tickets from memory to JSON file."""
|
|
223
|
+
global _tickets_storage
|
|
224
|
+
storage_path = _get_storage_path()
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
# Convert tickets to dict for JSON serialization
|
|
228
|
+
tickets_data = {}
|
|
229
|
+
for protocolo, ticket in _tickets_storage.items():
|
|
230
|
+
ticket_dict = {
|
|
231
|
+
"protocolo": ticket.protocolo,
|
|
232
|
+
"tipo": ticket.tipo,
|
|
233
|
+
"descricao": ticket.descricao,
|
|
234
|
+
"email_usuario": ticket.email_usuario,
|
|
235
|
+
"nome_usuario": ticket.nome_usuario,
|
|
236
|
+
"telefone_usuario": ticket.telefone_usuario,
|
|
237
|
+
"prioridade": ticket.prioridade,
|
|
238
|
+
"status": ticket.status,
|
|
239
|
+
"categoria": ticket.categoria,
|
|
240
|
+
"data_criacao": ticket.data_criacao.isoformat(),
|
|
241
|
+
"data_atualizacao": ticket.data_atualizacao.isoformat(),
|
|
242
|
+
"resposta": ticket.resposta,
|
|
243
|
+
"atendente": ticket.atendente,
|
|
244
|
+
"historico": ticket.historico,
|
|
245
|
+
}
|
|
246
|
+
tickets_data[protocolo.upper()] = ticket_dict
|
|
247
|
+
|
|
248
|
+
data = {"tickets": tickets_data}
|
|
249
|
+
|
|
250
|
+
# Ensure directory exists
|
|
251
|
+
storage_path.parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
|
|
253
|
+
with open(storage_path, "w", encoding="utf-8") as f:
|
|
254
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print(f"[Feedback] ⚠️ Erro ao salvar tickets no arquivo: {e}")
|
|
158
257
|
|
|
159
258
|
|
|
160
259
|
def _gerar_protocolo() -> str:
|
|
@@ -165,17 +264,25 @@ def _gerar_protocolo() -> str:
|
|
|
165
264
|
|
|
166
265
|
|
|
167
266
|
def _salvar_ticket(ticket: Ticket) -> None:
|
|
168
|
-
"""Save ticket to storage."""
|
|
267
|
+
"""Save ticket to storage (memory + file)."""
|
|
169
268
|
_tickets_storage[ticket.protocolo.upper()] = ticket
|
|
269
|
+
_save_tickets_to_file()
|
|
170
270
|
|
|
171
271
|
|
|
172
272
|
def _buscar_ticket(protocolo: str) -> Optional[Ticket]:
|
|
173
273
|
"""Find ticket by protocol."""
|
|
274
|
+
# Load from file if storage is empty
|
|
275
|
+
if not _tickets_storage:
|
|
276
|
+
_load_tickets_from_file()
|
|
174
277
|
return _tickets_storage.get(protocolo.upper())
|
|
175
278
|
|
|
176
279
|
|
|
177
280
|
def _listar_tickets(email: Optional[str] = None, status: Optional[str] = None) -> List[Ticket]:
|
|
178
281
|
"""List tickets, optionally filtered."""
|
|
282
|
+
# Load from file if storage is empty
|
|
283
|
+
if not _tickets_storage:
|
|
284
|
+
_load_tickets_from_file()
|
|
285
|
+
|
|
179
286
|
tickets = list(_tickets_storage.values())
|
|
180
287
|
|
|
181
288
|
if email:
|
|
@@ -456,20 +563,17 @@ def criar_ticket(
|
|
|
456
563
|
Returns:
|
|
457
564
|
Confirmação com número do protocolo e detalhes
|
|
458
565
|
"""
|
|
459
|
-
# Validar tipo
|
|
460
|
-
tipos_validos = ["duvida", "feedback", "reclamacao", "sugestao", "elogio", "problema", "outro"]
|
|
566
|
+
# Validar tipo (se allowed_ticket_types estiver configurado)
|
|
461
567
|
tipo_norm = tipo.lower().strip().replace("ã", "a").replace("ç", "c")
|
|
462
568
|
|
|
463
|
-
if tipo_norm not in
|
|
569
|
+
if _allowed_ticket_types is not None and tipo_norm not in _allowed_ticket_types:
|
|
570
|
+
tipos_list = "\n".join(f"- `{t}`" for t in _allowed_ticket_types)
|
|
464
571
|
return f"""❌ **Tipo inválido:** {tipo}
|
|
465
572
|
|
|
466
573
|
📋 **Tipos aceitos:**
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
- `sugestao` - Sugestão de melhoria
|
|
471
|
-
- `elogio` - Elogio ou agradecimento
|
|
472
|
-
- `problema` - Problema técnico"""
|
|
574
|
+
{tipos_list}
|
|
575
|
+
|
|
576
|
+
Por favor, escolha um dos tipos acima."""
|
|
473
577
|
|
|
474
578
|
# Validar descrição
|
|
475
579
|
if not descricao or len(descricao.strip()) < 10:
|
|
@@ -940,6 +1044,7 @@ def create_feedback_agent(
|
|
|
940
1044
|
email_brand_color=email_brand_color,
|
|
941
1045
|
email_brand_name=email_brand_name,
|
|
942
1046
|
email_sla_message=email_sla_message,
|
|
1047
|
+
allowed_ticket_types=ticket_types,
|
|
943
1048
|
)
|
|
944
1049
|
|
|
945
1050
|
# Build instructions using prompt builder
|
atendentepro/network.py
CHANGED
|
@@ -123,6 +123,9 @@ from atendentepro.templates import (
|
|
|
123
123
|
load_confirmation_config,
|
|
124
124
|
load_onboarding_config,
|
|
125
125
|
load_access_config,
|
|
126
|
+
load_feedback_config,
|
|
127
|
+
load_escalation_config,
|
|
128
|
+
load_answer_config,
|
|
126
129
|
AccessConfig,
|
|
127
130
|
AccessFilterConfig,
|
|
128
131
|
)
|
|
@@ -449,6 +452,39 @@ def create_standard_network(
|
|
|
449
452
|
confirmation_template = ""
|
|
450
453
|
confirmation_format = ""
|
|
451
454
|
|
|
455
|
+
# Load feedback config
|
|
456
|
+
try:
|
|
457
|
+
feedback_config = load_feedback_config(client)
|
|
458
|
+
feedback_protocol_prefix = feedback_config.protocol_prefix
|
|
459
|
+
feedback_email_config = feedback_config.email
|
|
460
|
+
feedback_ticket_types = feedback_config.get_ticket_type_names()
|
|
461
|
+
except FileNotFoundError:
|
|
462
|
+
feedback_protocol_prefix = feedback_protocol_prefix # Use parameter default
|
|
463
|
+
feedback_email_config = {}
|
|
464
|
+
feedback_ticket_types = None
|
|
465
|
+
|
|
466
|
+
# Load escalation config
|
|
467
|
+
escalation_config_loaded = False
|
|
468
|
+
try:
|
|
469
|
+
escalation_config = load_escalation_config(client)
|
|
470
|
+
escalation_business_hours = escalation_config.business_hours
|
|
471
|
+
escalation_priority_keywords = escalation_config.priority
|
|
472
|
+
escalation_triggers = escalation_config.triggers
|
|
473
|
+
escalation_channels_config = escalation_config.channels
|
|
474
|
+
escalation_config_loaded = True
|
|
475
|
+
except FileNotFoundError:
|
|
476
|
+
escalation_business_hours = None
|
|
477
|
+
escalation_priority_keywords = {}
|
|
478
|
+
escalation_triggers = {}
|
|
479
|
+
escalation_channels_config = None
|
|
480
|
+
|
|
481
|
+
# Load answer config
|
|
482
|
+
try:
|
|
483
|
+
answer_config = load_answer_config(client)
|
|
484
|
+
answer_template = answer_config.get_answer_template()
|
|
485
|
+
except FileNotFoundError:
|
|
486
|
+
answer_template = ""
|
|
487
|
+
|
|
452
488
|
# Load guardrails
|
|
453
489
|
try:
|
|
454
490
|
set_guardrails_client(client, templates_root=templates_root)
|
|
@@ -518,7 +554,7 @@ def create_standard_network(
|
|
|
518
554
|
answer = None
|
|
519
555
|
if include_answer and is_agent_allowed("answer"):
|
|
520
556
|
answer = create_answer_agent(
|
|
521
|
-
answer_template=
|
|
557
|
+
answer_template=answer_template,
|
|
522
558
|
guardrails=get_guardrails_for_agent("Answer Agent", templates_root),
|
|
523
559
|
style_instructions=get_full_instructions("answer"),
|
|
524
560
|
single_reply=get_single_reply_for_agent("answer"),
|
|
@@ -561,8 +597,44 @@ def create_standard_network(
|
|
|
561
597
|
# Create escalation agent if requested and allowed
|
|
562
598
|
escalation = None
|
|
563
599
|
if include_escalation and is_agent_allowed("escalation"):
|
|
600
|
+
# Format escalation triggers from config
|
|
601
|
+
triggers_text = ""
|
|
602
|
+
if escalation_triggers:
|
|
603
|
+
trigger_parts = []
|
|
604
|
+
if escalation_triggers.get("explicit_request"):
|
|
605
|
+
trigger_parts.append(f"Pedidos explícitos: {', '.join(escalation_triggers['explicit_request'])}")
|
|
606
|
+
if escalation_triggers.get("frustration"):
|
|
607
|
+
trigger_parts.append(f"Indicadores de frustração: {', '.join(escalation_triggers['frustration'])}")
|
|
608
|
+
if escalation_triggers.get("topics_requiring_human"):
|
|
609
|
+
trigger_parts.append(f"Tópicos que requerem humano: {', '.join(escalation_triggers['topics_requiring_human'])}")
|
|
610
|
+
triggers_text = "\n".join(trigger_parts) if trigger_parts else ""
|
|
611
|
+
|
|
612
|
+
# Format escalation channels from config or use parameter
|
|
613
|
+
if escalation_config_loaded and escalation_channels_config:
|
|
614
|
+
# Format from config dict
|
|
615
|
+
channel_parts = []
|
|
616
|
+
if escalation_channels_config.get("phone", {}).get("enabled"):
|
|
617
|
+
phone = escalation_channels_config["phone"]
|
|
618
|
+
channel_parts.append(f"Telefone: {phone.get('number', '')} ({phone.get('hours', '')})")
|
|
619
|
+
if escalation_channels_config.get("email", {}).get("enabled"):
|
|
620
|
+
email = escalation_channels_config["email"]
|
|
621
|
+
channel_parts.append(f"Email: {email.get('address', '')} ({email.get('sla', '')})")
|
|
622
|
+
if escalation_channels_config.get("whatsapp", {}).get("enabled"):
|
|
623
|
+
whatsapp = escalation_channels_config["whatsapp"]
|
|
624
|
+
channel_parts.append(f"WhatsApp: {whatsapp.get('number', '')} ({whatsapp.get('hours', '')})")
|
|
625
|
+
if channel_parts:
|
|
626
|
+
channels_text = " | ".join(channel_parts)
|
|
627
|
+
else:
|
|
628
|
+
channels_text = escalation_channels # Use parameter default
|
|
629
|
+
else:
|
|
630
|
+
channels_text = escalation_channels # Use parameter default
|
|
631
|
+
|
|
564
632
|
escalation = create_escalation_agent(
|
|
565
|
-
|
|
633
|
+
escalation_triggers=triggers_text,
|
|
634
|
+
escalation_channels=channels_text if isinstance(channels_text, str) else escalation_channels,
|
|
635
|
+
business_hours=escalation_business_hours,
|
|
636
|
+
priority_keywords_urgent=escalation_priority_keywords.get("urgent"),
|
|
637
|
+
priority_keywords_high=escalation_priority_keywords.get("high"),
|
|
566
638
|
tools=get_filtered_tools_for_agent("escalation", tools.get("escalation", [])),
|
|
567
639
|
guardrails=get_guardrails_for_agent("Escalation Agent", templates_root),
|
|
568
640
|
style_instructions=get_full_instructions("escalation"),
|
|
@@ -572,10 +644,17 @@ def create_standard_network(
|
|
|
572
644
|
# Create feedback agent if requested and allowed
|
|
573
645
|
feedback = None
|
|
574
646
|
if include_feedback and is_agent_allowed("feedback"):
|
|
647
|
+
# Use email config from YAML if available, otherwise use parameters
|
|
648
|
+
email_color = feedback_email_config.get("brand_color", feedback_brand_color)
|
|
649
|
+
email_name = feedback_email_config.get("brand_name", feedback_brand_name)
|
|
650
|
+
email_sla = feedback_email_config.get("sla_message", "Retornaremos em até 24h úteis.")
|
|
651
|
+
|
|
575
652
|
feedback = create_feedback_agent(
|
|
576
653
|
protocol_prefix=feedback_protocol_prefix,
|
|
577
|
-
email_brand_color=
|
|
578
|
-
email_brand_name=
|
|
654
|
+
email_brand_color=email_color,
|
|
655
|
+
email_brand_name=email_name,
|
|
656
|
+
email_sla_message=email_sla,
|
|
657
|
+
ticket_types=feedback_ticket_types,
|
|
579
658
|
tools=get_filtered_tools_for_agent("feedback", tools.get("feedback", [])),
|
|
580
659
|
guardrails=get_guardrails_for_agent("Feedback Agent", templates_root),
|
|
581
660
|
style_instructions=get_full_instructions("feedback"),
|
|
@@ -17,6 +17,9 @@ from .manager import (
|
|
|
17
17
|
load_style_config,
|
|
18
18
|
load_single_reply_config,
|
|
19
19
|
load_access_config,
|
|
20
|
+
load_feedback_config,
|
|
21
|
+
load_escalation_config,
|
|
22
|
+
load_answer_config,
|
|
20
23
|
FlowConfig,
|
|
21
24
|
InterviewConfig,
|
|
22
25
|
TriageConfig,
|
|
@@ -32,6 +35,9 @@ from .manager import (
|
|
|
32
35
|
DataSourceConfig,
|
|
33
36
|
DataSourceColumn,
|
|
34
37
|
DocumentConfig,
|
|
38
|
+
FeedbackConfig,
|
|
39
|
+
EscalationConfig,
|
|
40
|
+
AnswerConfig,
|
|
35
41
|
)
|
|
36
42
|
|
|
37
43
|
__all__ = [
|
|
@@ -50,6 +56,9 @@ __all__ = [
|
|
|
50
56
|
"load_style_config",
|
|
51
57
|
"load_single_reply_config",
|
|
52
58
|
"load_access_config",
|
|
59
|
+
"load_feedback_config",
|
|
60
|
+
"load_escalation_config",
|
|
61
|
+
"load_answer_config",
|
|
53
62
|
"FlowConfig",
|
|
54
63
|
"InterviewConfig",
|
|
55
64
|
"TriageConfig",
|
|
@@ -65,5 +74,8 @@ __all__ = [
|
|
|
65
74
|
"DataSourceConfig",
|
|
66
75
|
"DataSourceColumn",
|
|
67
76
|
"DocumentConfig",
|
|
77
|
+
"FeedbackConfig",
|
|
78
|
+
"EscalationConfig",
|
|
79
|
+
"AnswerConfig",
|
|
68
80
|
]
|
|
69
81
|
|
|
@@ -561,6 +561,112 @@ class AccessConfig(BaseModel):
|
|
|
561
561
|
return self.tool_access.get(tool_name)
|
|
562
562
|
|
|
563
563
|
|
|
564
|
+
class FeedbackConfig(BaseModel):
|
|
565
|
+
"""Configuration model for Feedback Agent."""
|
|
566
|
+
|
|
567
|
+
protocol_prefix: str = "TKT"
|
|
568
|
+
ticket_types: List[Dict[str, Any]] = Field(default_factory=list)
|
|
569
|
+
priorities: List[Dict[str, Any]] = Field(default_factory=list)
|
|
570
|
+
email: Dict[str, Any] = Field(default_factory=dict)
|
|
571
|
+
messages: Dict[str, str] = Field(default_factory=dict)
|
|
572
|
+
auto_priority: Dict[str, Any] = Field(default_factory=dict)
|
|
573
|
+
categories: List[str] = Field(default_factory=list)
|
|
574
|
+
|
|
575
|
+
@classmethod
|
|
576
|
+
@lru_cache(maxsize=4)
|
|
577
|
+
def load(cls, path: Path) -> "FeedbackConfig":
|
|
578
|
+
"""Load feedback configuration from YAML file."""
|
|
579
|
+
if not path.exists():
|
|
580
|
+
raise FileNotFoundError(f"Feedback config not found at {path}")
|
|
581
|
+
|
|
582
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
583
|
+
data = yaml.safe_load(f) or {}
|
|
584
|
+
|
|
585
|
+
return cls(
|
|
586
|
+
protocol_prefix=data.get("protocol_prefix", "TKT"),
|
|
587
|
+
ticket_types=data.get("ticket_types", []),
|
|
588
|
+
priorities=data.get("priorities", []),
|
|
589
|
+
email=data.get("email", {}),
|
|
590
|
+
messages=data.get("messages", {}),
|
|
591
|
+
auto_priority=data.get("auto_priority", {}),
|
|
592
|
+
categories=data.get("categories", []),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
def get_ticket_type_names(self) -> List[str]:
|
|
596
|
+
"""Get list of allowed ticket type names."""
|
|
597
|
+
return [t.get("name", "") for t in self.ticket_types if t.get("name")]
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class EscalationConfig(BaseModel):
|
|
601
|
+
"""Configuration model for Escalation Agent."""
|
|
602
|
+
|
|
603
|
+
triggers: Dict[str, Any] = Field(default_factory=dict)
|
|
604
|
+
channels: Dict[str, Any] = Field(default_factory=dict)
|
|
605
|
+
business_hours: Dict[str, Any] = Field(default_factory=dict)
|
|
606
|
+
priority: Dict[str, Any] = Field(default_factory=dict)
|
|
607
|
+
messages: Dict[str, str] = Field(default_factory=dict)
|
|
608
|
+
notifications: Dict[str, Any] = Field(default_factory=dict)
|
|
609
|
+
|
|
610
|
+
@classmethod
|
|
611
|
+
@lru_cache(maxsize=4)
|
|
612
|
+
def load(cls, path: Path) -> "EscalationConfig":
|
|
613
|
+
"""Load escalation configuration from YAML file."""
|
|
614
|
+
if not path.exists():
|
|
615
|
+
raise FileNotFoundError(f"Escalation config not found at {path}")
|
|
616
|
+
|
|
617
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
618
|
+
data = yaml.safe_load(f) or {}
|
|
619
|
+
|
|
620
|
+
return cls(
|
|
621
|
+
triggers=data.get("triggers", {}),
|
|
622
|
+
channels=data.get("channels", {}),
|
|
623
|
+
business_hours=data.get("business_hours", {}),
|
|
624
|
+
priority=data.get("priority", {}),
|
|
625
|
+
messages=data.get("messages", {}),
|
|
626
|
+
notifications=data.get("notifications", {}),
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def get_business_hours_days(self) -> List[int]:
|
|
630
|
+
"""Convert YAML day names to weekday numbers (0=Monday, 6=Sunday)."""
|
|
631
|
+
day_map = {
|
|
632
|
+
"monday": 0,
|
|
633
|
+
"tuesday": 1,
|
|
634
|
+
"wednesday": 2,
|
|
635
|
+
"thursday": 3,
|
|
636
|
+
"friday": 4,
|
|
637
|
+
"saturday": 5,
|
|
638
|
+
"sunday": 6,
|
|
639
|
+
}
|
|
640
|
+
days = self.business_hours.get("days", [])
|
|
641
|
+
return [day_map.get(day.lower(), day) for day in days if isinstance(day, str) and day.lower() in day_map]
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
class AnswerConfig(BaseModel):
|
|
645
|
+
"""Configuration model for Answer Agent."""
|
|
646
|
+
|
|
647
|
+
instructions: str = ""
|
|
648
|
+
response_format: Dict[str, Any] = Field(default_factory=dict)
|
|
649
|
+
|
|
650
|
+
@classmethod
|
|
651
|
+
@lru_cache(maxsize=4)
|
|
652
|
+
def load(cls, path: Path) -> "AnswerConfig":
|
|
653
|
+
"""Load answer configuration from YAML file."""
|
|
654
|
+
if not path.exists():
|
|
655
|
+
raise FileNotFoundError(f"Answer config not found at {path}")
|
|
656
|
+
|
|
657
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
658
|
+
data = yaml.safe_load(f) or {}
|
|
659
|
+
|
|
660
|
+
return cls(
|
|
661
|
+
instructions=data.get("instructions", ""),
|
|
662
|
+
response_format=data.get("response_format", {}),
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
def get_answer_template(self) -> str:
|
|
666
|
+
"""Get answer template from instructions."""
|
|
667
|
+
return self.instructions
|
|
668
|
+
|
|
669
|
+
|
|
564
670
|
class OnboardingConfig(BaseModel):
|
|
565
671
|
"""Configuration model for Onboarding Agent."""
|
|
566
672
|
|
|
@@ -694,6 +800,21 @@ class TemplateManager:
|
|
|
694
800
|
folder = self.get_template_folder(client)
|
|
695
801
|
return AccessConfig.load(folder / "access_config.yaml")
|
|
696
802
|
|
|
803
|
+
def load_feedback_config(self, client: Optional[str] = None) -> FeedbackConfig:
|
|
804
|
+
"""Load feedback configuration for the specified client."""
|
|
805
|
+
folder = self.get_template_folder(client)
|
|
806
|
+
return FeedbackConfig.load(folder / "feedback_config.yaml")
|
|
807
|
+
|
|
808
|
+
def load_escalation_config(self, client: Optional[str] = None) -> EscalationConfig:
|
|
809
|
+
"""Load escalation configuration for the specified client."""
|
|
810
|
+
folder = self.get_template_folder(client)
|
|
811
|
+
return EscalationConfig.load(folder / "escalation_config.yaml")
|
|
812
|
+
|
|
813
|
+
def load_answer_config(self, client: Optional[str] = None) -> AnswerConfig:
|
|
814
|
+
"""Load answer configuration for the specified client."""
|
|
815
|
+
folder = self.get_template_folder(client)
|
|
816
|
+
return AnswerConfig.load(folder / "answer_config.yaml")
|
|
817
|
+
|
|
697
818
|
def clear_caches(self) -> None:
|
|
698
819
|
"""Clear all configuration caches."""
|
|
699
820
|
FlowConfig.load.cache_clear()
|
|
@@ -705,6 +826,9 @@ class TemplateManager:
|
|
|
705
826
|
StyleConfig.load.cache_clear()
|
|
706
827
|
SingleReplyConfig.load.cache_clear()
|
|
707
828
|
AccessConfig.load.cache_clear()
|
|
829
|
+
FeedbackConfig.load.cache_clear()
|
|
830
|
+
EscalationConfig.load.cache_clear()
|
|
831
|
+
AnswerConfig.load.cache_clear()
|
|
708
832
|
|
|
709
833
|
|
|
710
834
|
def get_template_manager() -> TemplateManager:
|
|
@@ -808,3 +932,18 @@ def load_access_config(client: Optional[str] = None) -> AccessConfig:
|
|
|
808
932
|
"""Load access configuration for the specified client."""
|
|
809
933
|
return get_template_manager().load_access_config(client)
|
|
810
934
|
|
|
935
|
+
|
|
936
|
+
def load_feedback_config(client: Optional[str] = None) -> FeedbackConfig:
|
|
937
|
+
"""Load feedback configuration for the specified client."""
|
|
938
|
+
return get_template_manager().load_feedback_config(client)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def load_escalation_config(client: Optional[str] = None) -> EscalationConfig:
|
|
942
|
+
"""Load escalation configuration for the specified client."""
|
|
943
|
+
return get_template_manager().load_escalation_config(client)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def load_answer_config(client: Optional[str] = None) -> AnswerConfig:
|
|
947
|
+
"""Load answer configuration for the specified client."""
|
|
948
|
+
return get_template_manager().load_answer_config(client)
|
|
949
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atendentepro
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.8
|
|
4
4
|
Summary: Framework de orquestração de agentes IA com tom e estilo customizáveis. Integra documentos (RAG), APIs e bancos de dados em uma plataforma inteligente multi-agente.
|
|
5
5
|
Author-email: BeMonkAI <contato@monkai.com.br>
|
|
6
6
|
Maintainer-email: BeMonkAI <contato@monkai.com.br>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
atendentepro/README.md,sha256=TAXl5GRjhSwz_I-Dx_eN5JOIcUxuE_dz31iMZ_-OnRY,45390
|
|
2
2
|
atendentepro/__init__.py,sha256=VtJoW9tQ-n-hipu0Lp4gPNWJir1QATouA_CoIl_uY3A,6220
|
|
3
3
|
atendentepro/license.py,sha256=rlPtysXNqAzEQkP2VjUAVu_nMndhPgfKv1yN2ruUYVI,17570
|
|
4
|
-
atendentepro/network.py,sha256=
|
|
4
|
+
atendentepro/network.py,sha256=292buwWLglU7ISv5UL9sOXMBU2MObZc31TUtPZZQYuY,33719
|
|
5
5
|
atendentepro/agents/__init__.py,sha256=OcPhG1Dp6xe49B5YIti4HVmaZDoDIrFLfRa8GmI4jpQ,1638
|
|
6
6
|
atendentepro/agents/answer.py,sha256=S6wTchNSTMc0h6d89A4jAzoqecFPVqHUrr55-rCM-p4,2494
|
|
7
7
|
atendentepro/agents/confirmation.py,sha256=bQmIiDaxaCDIWJ3Fxz8h3AHW4kHhwbWSmyLX4y3dtls,2900
|
|
8
|
-
atendentepro/agents/escalation.py,sha256=
|
|
9
|
-
atendentepro/agents/feedback.py,sha256=
|
|
8
|
+
atendentepro/agents/escalation.py,sha256=pAS1HANW1jMCBZHekVxvuN9NZ_z9IpAYWbfUzH0Leuw,20540
|
|
9
|
+
atendentepro/agents/feedback.py,sha256=cQd35y2-u93GRmu36ErAMyQEinxvNUaDGblGTGrWCfw,37486
|
|
10
10
|
atendentepro/agents/flow.py,sha256=8SsAQ_f-daOM25EVBTMKGSUI9ywG3y4B0bFcCnrUfo0,2645
|
|
11
11
|
atendentepro/agents/interview.py,sha256=3eWHUw63OJuVpHH-QYtdL8SadRgevNLv2N8Esa4Za2c,2722
|
|
12
12
|
atendentepro/agents/knowledge.py,sha256=Xew_qhAdBrm3oVRvHhKh2oi6aURyeIKU5CtgfAHBYEI,11054
|
|
@@ -30,15 +30,15 @@ atendentepro/prompts/interview.py,sha256=9zVGA8zSmm6pBx2i1LPGNJIPuLlz7PZKRJE_PC6
|
|
|
30
30
|
atendentepro/prompts/knowledge.py,sha256=B3BOyAvzQlwAkR51gc-B6XbQLtwIZlwGP1ofhZ4cFbk,4644
|
|
31
31
|
atendentepro/prompts/onboarding.py,sha256=78fSIh2ifsGeoav8DV41_jnyU157c0dtggJujcDvW4U,6093
|
|
32
32
|
atendentepro/prompts/triage.py,sha256=bSdEVheGy03r5P6MQuv7NwhN2_wrt0mK80F9f_LskRU,1283
|
|
33
|
-
atendentepro/templates/__init__.py,sha256=
|
|
34
|
-
atendentepro/templates/manager.py,sha256=
|
|
33
|
+
atendentepro/templates/__init__.py,sha256=T6UdrTdsqkHMs2dH4W7RG264mj21SdYYWgnuyiIVxEM,1863
|
|
34
|
+
atendentepro/templates/manager.py,sha256=gPj8zPgN3iSHNACaonJU_aSFdevBJ1gpnlHvaiewAHg,34351
|
|
35
35
|
atendentepro/utils/__init__.py,sha256=x2yMUueBilWmI2qASSGbFREeyu0a65r3TuDt0euxNAU,1244
|
|
36
36
|
atendentepro/utils/openai_client.py,sha256=R0ns7SU36vTgploq14-QJMTke1pPxcAXlENDeoHU0L4,4552
|
|
37
37
|
atendentepro/utils/tracing.py,sha256=kpTPw1PF4rR1qq1RyBnAaPIQIJRka4RF8MfG_JrRJ7U,8486
|
|
38
38
|
atendentepro/utils/user_loader.py,sha256=J8wd-XF2PZg_i1ped8FI8nmGjmUafXROWJa1tYxSDMI,10623
|
|
39
|
-
atendentepro-0.6.
|
|
40
|
-
atendentepro-0.6.
|
|
41
|
-
atendentepro-0.6.
|
|
42
|
-
atendentepro-0.6.
|
|
43
|
-
atendentepro-0.6.
|
|
44
|
-
atendentepro-0.6.
|
|
39
|
+
atendentepro-0.6.8.dist-info/licenses/LICENSE,sha256=TF6CdXxePoT9DXtPnCejiU5mUwWzrFzd1iyWJyoMauA,983
|
|
40
|
+
atendentepro-0.6.8.dist-info/METADATA,sha256=oRNLhZMOr1RrV75bpeyXv_SSvnNStE7IZqhPkNjmqk8,45468
|
|
41
|
+
atendentepro-0.6.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
42
|
+
atendentepro-0.6.8.dist-info/entry_points.txt,sha256=OP0upzqJF3MLS6VX-M-5BfUwx5YLJO2sJ3YBAp4e6yI,89
|
|
43
|
+
atendentepro-0.6.8.dist-info/top_level.txt,sha256=BFasD4SMmgDUmWKlTIZ1PeuukoRBhyiMIz8umKWVCcs,13
|
|
44
|
+
atendentepro-0.6.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|