atendentepro 0.6.6__tar.gz → 0.6.8__tar.gz

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.
Files changed (48) hide show
  1. {atendentepro-0.6.6 → atendentepro-0.6.8}/CHANGELOG.md +42 -0
  2. {atendentepro-0.6.6 → atendentepro-0.6.8}/PKG-INFO +1 -1
  3. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/escalation.py +68 -9
  4. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/feedback.py +117 -12
  5. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/network.py +83 -4
  6. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/templates/__init__.py +12 -0
  7. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/templates/manager.py +139 -0
  8. {atendentepro-0.6.6 → atendentepro-0.6.8}/pyproject.toml +1 -1
  9. {atendentepro-0.6.6 → atendentepro-0.6.8}/LICENSE +0 -0
  10. {atendentepro-0.6.6 → atendentepro-0.6.8}/MANIFEST.in +0 -0
  11. {atendentepro-0.6.6 → atendentepro-0.6.8}/README.md +0 -0
  12. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/README.md +0 -0
  13. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/__init__.py +0 -0
  14. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/__init__.py +0 -0
  15. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/answer.py +0 -0
  16. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/confirmation.py +0 -0
  17. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/flow.py +0 -0
  18. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/interview.py +0 -0
  19. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/knowledge.py +0 -0
  20. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/onboarding.py +0 -0
  21. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/triage.py +0 -0
  22. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/agents/usage.py +0 -0
  23. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/config/__init__.py +0 -0
  24. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/config/settings.py +0 -0
  25. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/guardrails/__init__.py +0 -0
  26. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/guardrails/manager.py +0 -0
  27. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/license.py +0 -0
  28. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/models/__init__.py +0 -0
  29. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/models/context.py +0 -0
  30. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/models/outputs.py +0 -0
  31. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/__init__.py +0 -0
  32. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/answer.py +0 -0
  33. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/confirmation.py +0 -0
  34. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/escalation.py +0 -0
  35. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/feedback.py +0 -0
  36. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/flow.py +0 -0
  37. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/interview.py +0 -0
  38. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/knowledge.py +0 -0
  39. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/onboarding.py +0 -0
  40. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/prompts/triage.py +0 -0
  41. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/utils/__init__.py +0 -0
  42. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/utils/openai_client.py +0 -0
  43. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/utils/tracing.py +0 -0
  44. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro/utils/user_loader.py +0 -0
  45. {atendentepro-0.6.6 → atendentepro-0.6.8}/atendentepro.egg-info/SOURCES.txt +0 -0
  46. {atendentepro-0.6.6 → atendentepro-0.6.8}/requirements.txt +0 -0
  47. {atendentepro-0.6.6 → atendentepro-0.6.8}/setup.cfg +0 -0
  48. {atendentepro-0.6.6 → atendentepro-0.6.8}/setup.py +0 -0
@@ -5,6 +5,48 @@ All notable changes to AtendentePro will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.8] - 2025-02-03
9
+
10
+ ### Fixed
11
+ - **Import Error Fix**: Corrigido `ImportError` ao usar `include_feedback=True` em `create_standard_network`
12
+ - Adicionadas exportações faltantes em `atendentepro/templates/__init__.py`
13
+ - `load_feedback_config`, `load_escalation_config`, `load_answer_config` agora estão disponíveis
14
+ - Modelos `FeedbackConfig`, `EscalationConfig`, `AnswerConfig` agora são exportados corretamente
15
+
16
+ ## [0.6.7] - 2025-02-03
17
+
18
+ ### Fixed
19
+ - **Feedback Agent**: Correção crítica - configuração YAML agora é carregada e aplicada
20
+ - Removida validação hardcoded de tipos de ticket (agora configurável via YAML)
21
+ - Adicionada persistência de tickets em arquivo JSON (`feedback_tickets.json`)
22
+ - Tipos de ticket agora são validados contra `feedback_config.yaml`
23
+ - Configurações de email (brand_color, brand_name, sla_message) agora vêm do YAML
24
+ - **Escalation Agent**: Correção crítica - configuração YAML agora é carregada e aplicada
25
+ - Business hours agora configurável via `escalation_config.yaml`
26
+ - Keywords de prioridade (urgent/high) agora configuráveis via YAML
27
+ - Conversão automática de dias da semana (monday, tuesday, etc.) para números
28
+ - **Answer Agent**: Correção crítica - `answer_config.yaml` agora é carregado e usado
29
+ - Template de resposta agora vem do arquivo de configuração
30
+
31
+ ### Added
32
+ - **Novos modelos de configuração em TemplateManager**:
33
+ - `FeedbackConfig`: Modelo Pydantic para `feedback_config.yaml`
34
+ - `EscalationConfig`: Modelo Pydantic para `escalation_config.yaml`
35
+ - `AnswerConfig`: Modelo Pydantic para `answer_config.yaml`
36
+ - **Métodos de carregamento**:
37
+ - `load_feedback_config()`: Carrega configuração do Feedback Agent
38
+ - `load_escalation_config()`: Carrega configuração do Escalation Agent
39
+ - `load_answer_config()`: Carrega configuração do Answer Agent
40
+ - **Persistência de tickets**: Sistema de armazenamento em JSON para tickets do Feedback Agent
41
+ - Configurável via variável de ambiente `FEEDBACK_STORAGE_PATH`
42
+ - Carregamento automático ao iniciar
43
+ - Salvamento automático após cada criação/atualização
44
+
45
+ ### Changed
46
+ - **Feedback Agent**: `create_feedback_agent()` agora aceita `ticket_types` da configuração YAML
47
+ - **Escalation Agent**: `create_escalation_agent()` agora aceita `business_hours` e `priority_keywords` da configuração
48
+ - **Network**: `create_standard_network()` agora carrega e aplica configurações YAML automaticamente
49
+
8
50
  ## [0.6.6] - 2025-01-21
9
51
 
10
52
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atendentepro
3
- Version: 0.6.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>
@@ -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
- # Verificar variáveis de ambiente para horário customizado
147
- hora_inicio = int(os.getenv("ESCALATION_HOUR_START", DEFAULT_BUSINESS_HOURS["start"]))
148
- hora_fim = int(os.getenv("ESCALATION_HOUR_END", DEFAULT_BUSINESS_HOURS["end"]))
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
- disponivel = dia in DEFAULT_BUSINESS_HOURS["days"] and hora_inicio <= hora < hora_fim
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": ["Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado", "Domingo"][dia],
186
+ "dia_semana": day_names[dia],
156
187
  "horario_atendimento": f"{hora_inicio:02d}:00 - {hora_fim:02d}:00",
157
- "dias_atendimento": "Segunda a Sexta",
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
- # Palavras que indicam urgência
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:
@@ -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, replace with DB in production)
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 tipos_validos:
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
- - `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"""
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
@@ -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
- escalation_channels=escalation_channels,
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=feedback_brand_color,
578
- email_brand_name=feedback_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
  [project]
2
2
  name = "atendentepro"
3
- version = "0.6.6"
3
+ version = "0.6.8" # Trigger PyPI publish
4
4
  description = "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
  authors = [
6
6
  { name = "BeMonkAI", email = "contato@monkai.com.br" }
File without changes
File without changes
File without changes
File without changes
File without changes