atendentepro 0.3.0__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.
Files changed (39) hide show
  1. atendentepro/README.md +890 -0
  2. atendentepro/__init__.py +215 -0
  3. atendentepro/agents/__init__.py +45 -0
  4. atendentepro/agents/answer.py +62 -0
  5. atendentepro/agents/confirmation.py +69 -0
  6. atendentepro/agents/flow.py +64 -0
  7. atendentepro/agents/interview.py +68 -0
  8. atendentepro/agents/knowledge.py +296 -0
  9. atendentepro/agents/onboarding.py +65 -0
  10. atendentepro/agents/triage.py +57 -0
  11. atendentepro/agents/usage.py +56 -0
  12. atendentepro/config/__init__.py +19 -0
  13. atendentepro/config/settings.py +134 -0
  14. atendentepro/guardrails/__init__.py +21 -0
  15. atendentepro/guardrails/manager.py +419 -0
  16. atendentepro/license.py +502 -0
  17. atendentepro/models/__init__.py +21 -0
  18. atendentepro/models/context.py +21 -0
  19. atendentepro/models/outputs.py +118 -0
  20. atendentepro/network.py +325 -0
  21. atendentepro/prompts/__init__.py +35 -0
  22. atendentepro/prompts/answer.py +114 -0
  23. atendentepro/prompts/confirmation.py +124 -0
  24. atendentepro/prompts/flow.py +112 -0
  25. atendentepro/prompts/interview.py +123 -0
  26. atendentepro/prompts/knowledge.py +135 -0
  27. atendentepro/prompts/onboarding.py +146 -0
  28. atendentepro/prompts/triage.py +42 -0
  29. atendentepro/templates/__init__.py +51 -0
  30. atendentepro/templates/manager.py +530 -0
  31. atendentepro/utils/__init__.py +19 -0
  32. atendentepro/utils/openai_client.py +154 -0
  33. atendentepro/utils/tracing.py +71 -0
  34. atendentepro-0.3.0.dist-info/METADATA +306 -0
  35. atendentepro-0.3.0.dist-info/RECORD +39 -0
  36. atendentepro-0.3.0.dist-info/WHEEL +5 -0
  37. atendentepro-0.3.0.dist-info/entry_points.txt +2 -0
  38. atendentepro-0.3.0.dist-info/licenses/LICENSE +25 -0
  39. atendentepro-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,502 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AtendentePro - Sistema de Licenciamento
4
+
5
+ Este módulo gerencia a validação de tokens de acesso para uso da biblioteca.
6
+
7
+ Uso:
8
+ from atendentepro import activate
9
+
10
+ # Ativar com token
11
+ activate("seu-token-de-acesso")
12
+
13
+ # Agora pode usar a biblioteca normalmente
14
+ from atendentepro import create_standard_network
15
+ """
16
+
17
+ import os
18
+ import hashlib
19
+ import hmac
20
+ import time
21
+ import json
22
+ from typing import Optional
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ # Chave secreta para validação local (pode ser alterada em produção)
27
+ _SECRET_KEY = "atendentepro-bemonkai-2024"
28
+
29
+ # Estado global de ativação
30
+ _license_state = {
31
+ "activated": False,
32
+ "token": None,
33
+ "expiration": None,
34
+ "features": [],
35
+ "organization": None,
36
+ }
37
+
38
+
39
+ @dataclass
40
+ class LicenseInfo:
41
+ """Informações da licença ativa."""
42
+ valid: bool
43
+ organization: Optional[str] = None
44
+ expiration: Optional[str] = None
45
+ features: list = None
46
+ message: str = ""
47
+
48
+ def __post_init__(self):
49
+ if self.features is None:
50
+ self.features = []
51
+
52
+
53
+ class LicenseError(Exception):
54
+ """Erro de licenciamento."""
55
+ pass
56
+
57
+
58
+ class LicenseNotActivatedError(LicenseError):
59
+ """Biblioteca não foi ativada com um token válido."""
60
+
61
+ def __init__(self):
62
+ super().__init__(
63
+ "\n\n"
64
+ "╔══════════════════════════════════════════════════════════════╗\n"
65
+ "║ ATENDENTEPRO - LICENÇA NÃO ATIVADA ║\n"
66
+ "╠══════════════════════════════════════════════════════════════╣\n"
67
+ "║ ║\n"
68
+ "║ A biblioteca AtendentePro requer ativação para uso. ║\n"
69
+ "║ ║\n"
70
+ "║ Para ativar, use: ║\n"
71
+ "║ ║\n"
72
+ "║ from atendentepro import activate ║\n"
73
+ "║ activate('seu-token-de-acesso') ║\n"
74
+ "║ ║\n"
75
+ "║ Ou defina a variável de ambiente: ║\n"
76
+ "║ ║\n"
77
+ "║ export ATENDENTEPRO_LICENSE_KEY='seu-token' ║\n"
78
+ "║ ║\n"
79
+ "║ Para obter um token, entre em contato: ║\n"
80
+ "║ 📧 contato@bemonkai.com ║\n"
81
+ "║ ║\n"
82
+ "╚══════════════════════════════════════════════════════════════╝\n"
83
+ )
84
+
85
+
86
+ class LicenseExpiredError(LicenseError):
87
+ """Token de licença expirado."""
88
+
89
+ def __init__(self, expiration: str):
90
+ super().__init__(
91
+ f"\n\n"
92
+ f"╔══════════════════════════════════════════════════════════════╗\n"
93
+ f"║ ATENDENTEPRO - LICENÇA EXPIRADA ║\n"
94
+ f"╠══════════════════════════════════════════════════════════════╣\n"
95
+ f"║ ║\n"
96
+ f"║ Sua licença expirou em: {expiration:<36}║\n"
97
+ f"║ ║\n"
98
+ f"║ Para renovar, entre em contato: ║\n"
99
+ f"║ 📧 contato@bemonkai.com ║\n"
100
+ f"║ ║\n"
101
+ f"╚══════════════════════════════════════════════════════════════╝\n"
102
+ )
103
+
104
+
105
+ class InvalidTokenError(LicenseError):
106
+ """Token inválido."""
107
+
108
+ def __init__(self):
109
+ super().__init__(
110
+ "\n\n"
111
+ "╔══════════════════════════════════════════════════════════════╗\n"
112
+ "║ ATENDENTEPRO - TOKEN INVÁLIDO ║\n"
113
+ "╠══════════════════════════════════════════════════════════════╣\n"
114
+ "║ ║\n"
115
+ "║ O token fornecido não é válido. ║\n"
116
+ "║ ║\n"
117
+ "║ Verifique se o token está correto ou entre em contato: ║\n"
118
+ "║ 📧 contato@bemonkai.com ║\n"
119
+ "║ ║\n"
120
+ "╚══════════════════════════════════════════════════════════════╝\n"
121
+ )
122
+
123
+
124
+ def _generate_token(
125
+ organization: str,
126
+ expiration_timestamp: int = None,
127
+ features: list = None,
128
+ secret_key: str = None
129
+ ) -> str:
130
+ """
131
+ Gera um token de licença.
132
+
133
+ USO INTERNO - Para gerar tokens para clientes.
134
+
135
+ Args:
136
+ organization: Nome da organização
137
+ expiration_timestamp: Unix timestamp de expiração (None = sem expiração)
138
+ features: Lista de features habilitadas
139
+ secret_key: Chave secreta para assinatura
140
+
141
+ Returns:
142
+ Token codificado em base64
143
+ """
144
+ import base64
145
+
146
+ if secret_key is None:
147
+ secret_key = _SECRET_KEY
148
+
149
+ if features is None:
150
+ features = ["full"]
151
+
152
+ # Payload do token
153
+ payload = {
154
+ "org": organization,
155
+ "exp": expiration_timestamp,
156
+ "feat": features,
157
+ "v": 1, # versão do token
158
+ }
159
+
160
+ # Serializar payload
161
+ payload_json = json.dumps(payload, separators=(",", ":"))
162
+ payload_b64 = base64.urlsafe_b64encode(payload_json.encode()).decode()
163
+
164
+ # Gerar assinatura
165
+ signature = hmac.new(
166
+ secret_key.encode(),
167
+ payload_b64.encode(),
168
+ hashlib.sha256
169
+ ).hexdigest()[:16]
170
+
171
+ # Token final: payload.signature
172
+ token = f"ATP_{payload_b64}.{signature}"
173
+
174
+ return token
175
+
176
+
177
+ def _validate_token_local(token: str, secret_key: str = None) -> LicenseInfo:
178
+ """
179
+ Valida um token localmente.
180
+
181
+ Args:
182
+ token: Token de licença
183
+ secret_key: Chave secreta para validação
184
+
185
+ Returns:
186
+ LicenseInfo com informações da validação
187
+ """
188
+ import base64
189
+
190
+ if secret_key is None:
191
+ secret_key = _SECRET_KEY
192
+
193
+ try:
194
+ # Verificar formato
195
+ if not token.startswith("ATP_"):
196
+ return LicenseInfo(valid=False, message="Formato de token inválido")
197
+
198
+ token_body = token[4:] # Remover "ATP_"
199
+
200
+ if "." not in token_body:
201
+ return LicenseInfo(valid=False, message="Token malformado")
202
+
203
+ payload_b64, signature = token_body.rsplit(".", 1)
204
+
205
+ # Verificar assinatura
206
+ expected_signature = hmac.new(
207
+ secret_key.encode(),
208
+ payload_b64.encode(),
209
+ hashlib.sha256
210
+ ).hexdigest()[:16]
211
+
212
+ if not hmac.compare_digest(signature, expected_signature):
213
+ return LicenseInfo(valid=False, message="Assinatura inválida")
214
+
215
+ # Decodificar payload
216
+ try:
217
+ payload_json = base64.urlsafe_b64decode(payload_b64).decode()
218
+ payload = json.loads(payload_json)
219
+ except Exception:
220
+ return LicenseInfo(valid=False, message="Payload inválido")
221
+
222
+ # Verificar expiração
223
+ expiration = payload.get("exp")
224
+ expiration_str = None
225
+
226
+ if expiration is not None:
227
+ expiration_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(expiration))
228
+ if time.time() > expiration:
229
+ return LicenseInfo(
230
+ valid=False,
231
+ expiration=expiration_str,
232
+ message="Token expirado"
233
+ )
234
+
235
+ # Token válido
236
+ return LicenseInfo(
237
+ valid=True,
238
+ organization=payload.get("org", "Unknown"),
239
+ expiration=expiration_str,
240
+ features=payload.get("feat", ["full"]),
241
+ message="Token válido"
242
+ )
243
+
244
+ except Exception as e:
245
+ return LicenseInfo(valid=False, message=f"Erro na validação: {str(e)}")
246
+
247
+
248
+ def _validate_token_online(token: str, api_url: str = None) -> LicenseInfo:
249
+ """
250
+ Valida um token online através de API.
251
+
252
+ Args:
253
+ token: Token de licença
254
+ api_url: URL da API de validação
255
+
256
+ Returns:
257
+ LicenseInfo com informações da validação
258
+ """
259
+ # Implementação futura para validação online
260
+ # Por enquanto, faz validação local
261
+ return _validate_token_local(token)
262
+
263
+
264
+ def activate(
265
+ token: str = None,
266
+ validate_online: bool = False,
267
+ silent: bool = False
268
+ ) -> LicenseInfo:
269
+ """
270
+ Ativa a biblioteca AtendentePro com um token de licença.
271
+
272
+ Args:
273
+ token: Token de licença. Se não fornecido, tenta usar
274
+ a variável de ambiente ATENDENTEPRO_LICENSE_KEY
275
+ validate_online: Se True, valida o token online (requer internet)
276
+ silent: Se True, não imprime mensagens de sucesso
277
+
278
+ Returns:
279
+ LicenseInfo com informações da licença
280
+
281
+ Raises:
282
+ InvalidTokenError: Se o token for inválido
283
+ LicenseExpiredError: Se o token estiver expirado
284
+
285
+ Exemplo:
286
+ >>> from atendentepro import activate
287
+ >>> activate("ATP_eyJvcmciOiJNaW5oYUVtcHJlc2EiLCJleHAiOm51bGwsImZlYXQiOlsiZnVsbCJdLCJ2IjoxfQ.abc123")
288
+ ✅ AtendentePro ativado para: MinhaEmpresa
289
+ """
290
+ global _license_state
291
+
292
+ # Tentar obter token de variável de ambiente
293
+ if token is None:
294
+ token = os.environ.get("ATENDENTEPRO_LICENSE_KEY")
295
+
296
+ if token is None:
297
+ raise LicenseNotActivatedError()
298
+
299
+ # Validar token
300
+ if validate_online:
301
+ license_info = _validate_token_online(token)
302
+ else:
303
+ license_info = _validate_token_local(token)
304
+
305
+ if not license_info.valid:
306
+ if "expirado" in license_info.message.lower():
307
+ raise LicenseExpiredError(license_info.expiration or "Data desconhecida")
308
+ raise InvalidTokenError()
309
+
310
+ # Atualizar estado global
311
+ _license_state["activated"] = True
312
+ _license_state["token"] = token
313
+ _license_state["expiration"] = license_info.expiration
314
+ _license_state["features"] = license_info.features
315
+ _license_state["organization"] = license_info.organization
316
+
317
+ if not silent:
318
+ exp_msg = f" (expira: {license_info.expiration})" if license_info.expiration else " (sem expiração)"
319
+ print(f"✅ AtendentePro ativado para: {license_info.organization}{exp_msg}")
320
+
321
+ return license_info
322
+
323
+
324
+ def deactivate():
325
+ """Desativa a biblioteca (útil para testes)."""
326
+ global _license_state
327
+ _license_state = {
328
+ "activated": False,
329
+ "token": None,
330
+ "expiration": None,
331
+ "features": [],
332
+ "organization": None,
333
+ }
334
+
335
+
336
+ def is_activated() -> bool:
337
+ """Verifica se a biblioteca está ativada."""
338
+ return _license_state["activated"]
339
+
340
+
341
+ def get_license_info() -> LicenseInfo:
342
+ """Retorna informações da licença atual."""
343
+ if not _license_state["activated"]:
344
+ return LicenseInfo(valid=False, message="Não ativado")
345
+
346
+ return LicenseInfo(
347
+ valid=True,
348
+ organization=_license_state["organization"],
349
+ expiration=_license_state["expiration"],
350
+ features=_license_state["features"],
351
+ message="Ativo"
352
+ )
353
+
354
+
355
+ def require_activation():
356
+ """
357
+ Decorator/função para verificar se a biblioteca está ativada.
358
+
359
+ Raises:
360
+ LicenseNotActivatedError: Se não estiver ativada
361
+ """
362
+ # Primeiro, tentar ativar automaticamente via variável de ambiente
363
+ if not _license_state["activated"]:
364
+ env_token = os.environ.get("ATENDENTEPRO_LICENSE_KEY")
365
+ if env_token:
366
+ try:
367
+ activate(env_token, silent=True)
368
+ except LicenseError:
369
+ pass
370
+
371
+ if not _license_state["activated"]:
372
+ raise LicenseNotActivatedError()
373
+
374
+
375
+ def has_feature(feature: str) -> bool:
376
+ """Verifica se uma feature específica está habilitada."""
377
+ if not _license_state["activated"]:
378
+ return False
379
+
380
+ features = _license_state.get("features", [])
381
+ return "full" in features or feature in features
382
+
383
+
384
+ # ============================================================================
385
+ # UTILITÁRIO PARA GERAR TOKENS (USO ADMINISTRATIVO)
386
+ # ============================================================================
387
+
388
+ def generate_license_token(
389
+ organization: str,
390
+ days_valid: int = None,
391
+ features: list = None
392
+ ) -> str:
393
+ """
394
+ Gera um token de licença para uma organização.
395
+
396
+ USO ADMINISTRATIVO - Para gerar tokens para clientes.
397
+
398
+ Args:
399
+ organization: Nome da organização/cliente
400
+ days_valid: Dias de validade (None = sem expiração)
401
+ features: Lista de features ["full", "basic", "knowledge", etc]
402
+
403
+ Returns:
404
+ Token de licença
405
+
406
+ Exemplo:
407
+ >>> generate_license_token("MinhaEmpresa", days_valid=365)
408
+ 'ATP_eyJvcmciOiJNaW5oYUVtcHJlc2EiLCJleHAiOjE3MzU2ODkw...'
409
+ """
410
+ expiration = None
411
+ if days_valid is not None:
412
+ expiration = int(time.time()) + (days_valid * 24 * 60 * 60)
413
+
414
+ return _generate_token(
415
+ organization=organization,
416
+ expiration_timestamp=expiration,
417
+ features=features or ["full"]
418
+ )
419
+
420
+
421
+ # ============================================================================
422
+ # AUTO-ATIVAÇÃO VIA VARIÁVEL DE AMBIENTE
423
+ # ============================================================================
424
+
425
+ def _try_auto_activate():
426
+ """Tenta ativar automaticamente via variável de ambiente."""
427
+ env_token = os.environ.get("ATENDENTEPRO_LICENSE_KEY")
428
+ if env_token and not _license_state["activated"]:
429
+ try:
430
+ activate(env_token, silent=True)
431
+ except LicenseError:
432
+ pass # Silenciosamente ignora erros de auto-ativação
433
+
434
+
435
+ # Tentar auto-ativar ao importar o módulo
436
+ _try_auto_activate()
437
+
438
+
439
+ # ============================================================================
440
+ # CLI PARA GERAR TOKENS
441
+ # ============================================================================
442
+
443
+ def _cli_generate_token():
444
+ """
445
+ Ponto de entrada CLI para gerar tokens.
446
+
447
+ Uso:
448
+ atendentepro-generate-token "MinhaEmpresa" --days 365
449
+ """
450
+ import argparse
451
+
452
+ parser = argparse.ArgumentParser(
453
+ prog="atendentepro-generate-token",
454
+ description="Gera tokens de licença para o AtendentePro"
455
+ )
456
+ parser.add_argument(
457
+ "organization",
458
+ help="Nome da organização/cliente"
459
+ )
460
+ parser.add_argument(
461
+ "--days",
462
+ type=int,
463
+ default=None,
464
+ help="Dias de validade (padrão: sem expiração)"
465
+ )
466
+ parser.add_argument(
467
+ "--features",
468
+ default="full",
469
+ help="Features habilitadas, separadas por vírgula (padrão: full)"
470
+ )
471
+
472
+ args = parser.parse_args()
473
+
474
+ # Processar features
475
+ features = [f.strip() for f in args.features.split(",")]
476
+
477
+ # Gerar token
478
+ token = generate_license_token(
479
+ organization=args.organization,
480
+ days_valid=args.days,
481
+ features=features
482
+ )
483
+
484
+ # Validar o token gerado
485
+ info = _validate_token_local(token)
486
+
487
+ print("\n" + "=" * 70)
488
+ print("TOKEN DE LICENÇA ATENDENTEPRO")
489
+ print("=" * 70)
490
+ print(f"\n📋 Organização: {info.organization}")
491
+ print(f"⏰ Expiração: {info.expiration or 'Sem expiração'}")
492
+ print(f"🎯 Features: {', '.join(info.features)}")
493
+ print(f"\n🔑 Token:\n")
494
+ print(token)
495
+ print("\n" + "=" * 70)
496
+ print("\n📌 Para usar, adicione ao ambiente ou código:")
497
+ print(f'\nexport ATENDENTEPRO_LICENSE_KEY="{token}"')
498
+ print("\nou no código Python:")
499
+ print(f'\nfrom atendentepro import activate')
500
+ print(f'activate("{token}")')
501
+ print("\n" + "=" * 70 + "\n")
502
+
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Models module for AtendentePro library."""
3
+
4
+ from .context import ContextNote
5
+ from .outputs import (
6
+ FlowTopic,
7
+ FlowOutput,
8
+ InterviewOutput,
9
+ KnowledgeToolResult,
10
+ GuardrailValidationOutput,
11
+ )
12
+
13
+ __all__ = [
14
+ "ContextNote",
15
+ "FlowTopic",
16
+ "FlowOutput",
17
+ "InterviewOutput",
18
+ "KnowledgeToolResult",
19
+ "GuardrailValidationOutput",
20
+ ]
21
+
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Context models for AtendentePro agents."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class ContextNote(BaseModel):
10
+ """
11
+ Shared context model used across all agents.
12
+
13
+ Contains structured summaries generated by previous agents
14
+ to guide subsequent handoffs.
15
+ """
16
+
17
+ handoff_summaries: dict[str, dict] = Field(
18
+ default_factory=dict,
19
+ description="Resumos estruturados gerados por agentes anteriores para orientar próximos handoffs.",
20
+ )
21
+
@@ -0,0 +1,118 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Output models for AtendentePro agents."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import Enum
7
+ from typing import List, Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class FlowTopic(str, Enum):
13
+ """Enumeration of available flow topics."""
14
+
15
+ GENERIC = "generic"
16
+ CUSTOM = "custom"
17
+
18
+ @classmethod
19
+ def from_label(cls, label: str) -> "FlowTopic":
20
+ """Create a topic from a label string."""
21
+ normalized = label.lower().replace(" ", "_").replace("-", "_")
22
+ for topic in cls:
23
+ if topic.value == normalized:
24
+ return topic
25
+ return cls.CUSTOM
26
+
27
+
28
+ class FlowOutput(BaseModel):
29
+ """Output model for the Flow Agent."""
30
+
31
+ topic: FlowTopic = Field(
32
+ description="O tópico identificado para o fluxo de atendimento."
33
+ )
34
+ confidence: float = Field(
35
+ default=0.0,
36
+ ge=0.0,
37
+ le=1.0,
38
+ description="Nível de confiança na classificação do tópico (0-1)."
39
+ )
40
+ reasoning: str = Field(
41
+ default="",
42
+ description="Explicação do raciocínio para a classificação."
43
+ )
44
+
45
+
46
+ class InterviewOutput(BaseModel):
47
+ """Output model for the Interview Agent."""
48
+
49
+ topic: str = Field(
50
+ description="O tópico sendo entrevistado."
51
+ )
52
+ questions_asked: List[str] = Field(
53
+ default_factory=list,
54
+ description="Lista de perguntas já realizadas."
55
+ )
56
+ answers_collected: dict = Field(
57
+ default_factory=dict,
58
+ description="Respostas coletadas do usuário."
59
+ )
60
+ is_complete: bool = Field(
61
+ default=False,
62
+ description="Se a entrevista foi completada."
63
+ )
64
+ missing_info: List[str] = Field(
65
+ default_factory=list,
66
+ description="Informações ainda faltando."
67
+ )
68
+
69
+
70
+ class KnowledgeToolResult(BaseModel):
71
+ """Output model for Knowledge Agent RAG tool."""
72
+
73
+ answer: str = Field(
74
+ description="Resposta sintetizada usando o contexto recuperado."
75
+ )
76
+ context: str = Field(
77
+ description="Trechos dos documentos utilizados para resposta."
78
+ )
79
+ sources: List[str] = Field(
80
+ default_factory=list,
81
+ description="Documentos consultados."
82
+ )
83
+ confidence: float = Field(
84
+ default=0.0,
85
+ description="Confiança média estimada a partir da similaridade dos trechos."
86
+ )
87
+
88
+
89
+ class GuardrailValidationOutput(BaseModel):
90
+ """Output model for guardrail validation."""
91
+
92
+ is_in_scope: bool = Field(
93
+ description="Se a mensagem está dentro do escopo do agente."
94
+ )
95
+ reasoning: str = Field(
96
+ description="Explicação do raciocínio para a decisão."
97
+ )
98
+
99
+
100
+ class AnswerOutput(BaseModel):
101
+ """Output model for the Answer Agent."""
102
+
103
+ response: str = Field(
104
+ description="Resposta final sintetizada para o usuário."
105
+ )
106
+ sources_used: List[str] = Field(
107
+ default_factory=list,
108
+ description="Fontes utilizadas para compor a resposta."
109
+ )
110
+ follow_up_needed: bool = Field(
111
+ default=False,
112
+ description="Se é necessário acompanhamento adicional."
113
+ )
114
+ follow_up_reason: Optional[str] = Field(
115
+ default=None,
116
+ description="Razão para o acompanhamento, se necessário."
117
+ )
118
+