caarleexx commited on
Commit
cd34c51
·
verified ·
1 Parent(s): d720cdb

Upload 2 files

Browse files
processors/processor_base.py CHANGED
@@ -1,268 +1,282 @@
1
- """
2
- Classe Base Refatorada - Sem simulações, apenas LLM real
3
- """
4
- from abc import ABC, abstractmethod
5
- from typing import Dict, Any, Optional
6
- from datetime import datetime
7
  import logging
8
- import json
 
 
 
9
 
10
- from processors.config_loader import get_config_loader, SpecialistConfig
 
 
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
 
15
  class ProcessorBase(ABC):
16
- """
17
- Classe abstrata base para processadores
18
 
19
- MUDANÇAS:
20
- - Carrega configuração do YAML
21
- - Não tem métodos auxiliares hardcoded
22
- - Todos os dados vêm do LLM
23
- """
24
-
25
- def __init__(
26
- self,
27
- specialist_id: int,
28
- llm_manager=None
29
- ):
30
  """
 
 
31
  Args:
32
- specialist_id: ID do especialista (1-9)
33
- llm_manager: LLMManager para chamadas LLM
 
 
34
  """
35
- self.specialist_id = specialist_id
36
- self.llm_manager = llm_manager
37
-
38
- # Carregar configuração do YAML
39
- config_loader = get_config_loader()
40
- self.config: Optional[SpecialistConfig] = config_loader.get_specialist(specialist_id)
41
-
42
- if not self.config:
43
- raise ValueError(f"Especialista {specialist_id} não encontrado na configuração")
44
-
45
- self.specialist_name = self.config.name
46
- self.execution_time = 0
47
- self.confidence_score = 0
48
- self.errors = []
49
- self.warnings = []
50
-
51
- logger.info(
52
- f"✅ {self.specialist_name} inicializado "
53
- f"(provider={self.config.llm_config.provider}, "
54
- f"model={self.config.llm_config.model})"
55
- )
56
 
57
- async def process(self, acordao_data: Dict[str, Any]) -> Dict[str, Any]:
 
 
58
  """
59
- Processa acórdão usando LLM REAL
60
 
61
  Args:
62
- acordao_data: Dados do acórdão
63
 
64
  Returns:
65
- Resultado do processamento
66
  """
67
- if not self.llm_manager:
68
- raise ValueError("❌ LLMManager não configurado")
69
-
70
- start_time = datetime.now()
71
-
72
  try:
73
- # 1. Formatar prompt do usuário com dados do acórdão
74
- user_prompt = self.config.format_user_prompt(
75
- tribunal=acordao_data.get('tribunal', 'N/A'),
76
- numero_processo=acordao_data.get('numero_processo', 'N/A'),
77
- ementa=acordao_data.get('ementa', ''),
78
- integra=acordao_data.get('integra', ''),
79
- classe_processual=acordao_data.get('classe_processual', 'N/A')
80
- )
81
-
82
- # 2. Chamar LLM Manager
83
- response = await self.llm_manager.generate(
84
- provider=self.config.llm_config.provider,
85
- model=self.config.llm_config.model,
86
- system_prompt=self.config.system_prompt,
87
- user_prompt=user_prompt,
88
- temperature=self.config.llm_config.temperature,
89
- max_tokens=self.config.llm_config.max_tokens
90
  )
91
 
92
- # 3. Extrair resultado (compatível com GroqResponse ou dict)
93
- result_text = None
94
-
95
- # Verificar se é GroqResponse (objeto com atributo content)
96
- if hasattr(response, 'content'):
97
- result_text = response.content
98
- logger.debug(f"[{self.specialist_name}] Resposta GroqResponse extraída")
99
-
100
- # Verificar se é dict com estrutura padrão
101
- elif isinstance(response, dict):
102
- if response.get('status') == 'success':
103
- # Pode estar em 'result', 'content' ou 'response'
104
- result_text = (
105
- response.get('result') or
106
- response.get('content') or
107
- response.get('response')
108
- )
109
- logger.debug(f"[{self.specialist_name}] Resposta dict extraída")
110
- else:
111
- error_msg = response.get('error', 'Erro desconhecido')
112
- raise ValueError(f"Erro na chamada LLM: {error_msg}")
113
-
114
- # Tipo inesperado
115
- else:
116
- raise ValueError(
117
- f"Tipo de resposta inesperado: {type(response).__name__}. "
118
- f"Esperado: GroqResponse ou dict"
119
- )
120
-
121
- if not result_text:
122
- raise ValueError("Resposta vazia do LLM")
123
-
124
- # Parse JSON
125
- result = self._parse_json_response(result_text)
126
-
127
- # Validar
128
- if not self.validate(result):
129
- self.add_warning("Resultado não passou na validação completa")
130
-
131
- # Calcular confiança
132
- self.confidence_score = self._calculate_confidence(result)
133
-
134
- # 4. Pós-processar
135
- self.execution_time = (datetime.now() - start_time).total_seconds()
136
-
137
- return self.post_process(result)
138
 
139
  except Exception as e:
140
- self.add_error(f"Erro no processamento: {e}")
141
- self.execution_time = (datetime.now() - start_time).total_seconds()
142
 
143
- return {
144
- "specialist_id": self.specialist_id,
145
- "specialist_name": self.specialist_name,
146
- "status": "error",
147
- "error": str(e),
148
- "execution_time": self.execution_time,
149
- "timestamp": datetime.now().isoformat()
150
- }
 
 
 
 
151
 
152
- def _parse_json_response(self, text: str) -> Dict[str, Any]:
153
  """
154
- Parse resposta JSON do LLM (com limpeza se necessário)
155
 
156
  Args:
157
- text: Texto da resposta
158
 
159
  Returns:
160
- Dict parseado
161
  """
162
- try:
163
- # Tentar parse direto
164
- return json.loads(text)
165
- except json.JSONDecodeError:
166
- # Limpar markdown code blocks
167
- text = text.strip()
168
-
169
- if '```json' in text:
170
- text = text.split('```json', 1)[1].split('```', 1)[0].strip()
171
- elif '```' in text:
172
- text = text.split('```', 1)[1].split('```', 1)[0].strip()
173
-
174
- # Tentar parse novamente
175
- try:
176
- return json.loads(text)
177
- except json.JSONDecodeError as e:
178
- self.add_error(f"Erro ao parsear JSON: {e}")
179
- logger.error(f"[{self.specialist_name}] Resposta inválida: {text[:200]}...")
180
- raise ValueError(f"Resposta não é JSON válido: {e}")
181
-
182
- def validate(self, result: Dict[str, Any]) -> bool:
183
  """
184
- Valida resultado contra schema
185
 
186
  Args:
187
- result: Resultado a validar
188
 
189
  Returns:
190
- True se válido
 
 
 
 
191
  """
192
  try:
193
- # Validação básica: verificar campos required do schema
194
- schema = self.config.schema
195
- required_fields = schema.get('required', [])
 
 
 
196
 
197
- for field in required_fields:
198
- if field not in result:
199
- self.add_warning(f"Campo obrigatório ausente: {field}")
200
- return False
 
 
 
201
 
202
- return True
 
 
 
 
203
 
204
  except Exception as e:
205
- self.add_error(f"Erro na validação: {e}")
206
- return False
 
 
 
 
 
 
 
 
207
 
208
- def _calculate_confidence(self, result: Dict[str, Any]) -> int:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  """
210
- Calcula score de confiança baseado no resultado
 
 
 
 
 
 
 
211
 
212
  Returns:
213
- Score 0-100
214
  """
215
- # Lógica simples: quanto mais campos preenchidos, maior confiança
216
- schema = self.config.schema
217
- required_fields = schema.get('required', [])
218
 
219
- if not required_fields:
220
- return 85 # Padrão
221
 
222
- filled = sum(1 for field in required_fields if result.get(field))
223
- confidence = int((filled / len(required_fields)) * 100)
224
 
225
- return min(confidence, 100)
 
 
 
 
 
 
 
 
 
 
226
 
227
- def post_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
228
- """Pós-processa resultado"""
229
  return {
230
- "specialist_id": self.specialist_id,
231
- "specialist_name": self.specialist_name,
232
- "result": result,
233
- "execution_time": self.execution_time,
234
- "confidence_score": self.confidence_score,
235
- "errors": self.errors,
236
- "warnings": self.warnings,
237
- "timestamp": datetime.now().isoformat(),
238
- "config": {
239
- "provider": self.config.llm_config.provider,
240
- "model": self.config.llm_config.model,
241
- "temperature": self.config.llm_config.temperature
242
- }
243
  }
244
 
245
- def add_error(self, error_msg: str):
246
- """Adiciona erro"""
247
- self.errors.append(error_msg)
248
- logger.error(f"[{self.specialist_name}] {error_msg}")
249
 
250
- def add_warning(self, warning_msg: str):
251
- """Adiciona aviso"""
252
- self.warnings.append(warning_msg)
253
- logger.warning(f"[{self.specialist_name}] {warning_msg}")
 
 
254
 
255
- def get_schema(self) -> Dict[str, Any]:
256
- """Retorna schema do especialista"""
257
- return self.config.schema
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
- @property
260
- def info(self) -> Dict[str, Any]:
261
- """Retorna informações sobre o processador"""
262
  return {
263
- "specialist_id": self.specialist_id,
264
- "specialist_name": self.specialist_name,
265
- "provider": self.config.llm_config.provider,
266
- "model": self.config.llm_config.model,
267
- "schema": self.config.schema
268
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Classe base para processadores com injeção automática de schemas."""
2
+
 
 
 
 
3
  import logging
4
+ from abc import ABC, abstractmethod
5
+ from typing import Dict, Optional, Any
6
+ from pathlib import Path
7
+ import sys
8
 
9
+ # Importa schema_loader
10
+ sys.path.insert(0, str(Path(__file__).parent.parent))
11
+ from llm.schema_loader import get_schema_loader
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
 
16
  class ProcessorBase(ABC):
17
+ """Classe base para todos os processadores com schema injection."""
 
18
 
19
+ def __init__(self,
20
+ specialist_name: str,
21
+ llm_client: Any,
22
+ system_prompt: str,
23
+ config: Optional[Dict] = None):
 
 
 
 
 
 
24
  """
25
+ Inicializa processador base.
26
+
27
  Args:
28
+ specialist_name: Nome do especialista (usado para carregar schema)
29
+ llm_client: Cliente LLM (GroqClient, OpenAIClient, etc)
30
+ system_prompt: Prompt base do sistema
31
+ config: Configurações adicionais
32
  """
33
+ self.specialist_name = specialist_name
34
+ self.llm_client = llm_client
35
+ self.config = config or {}
36
+
37
+ # 🔥 INJEÇÃO AUTOMÁTICA DE SCHEMA
38
+ self.schema_loader = get_schema_loader()
39
+ self.system_prompt = self._enhance_system_prompt(system_prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ logger.info(f"✅ {self.__class__.__name__} inicializado (specialist={specialist_name})")
42
+
43
+ def _enhance_system_prompt(self, base_prompt: str) -> str:
44
  """
45
+ Injeta schema JSON no system prompt.
46
 
47
  Args:
48
+ base_prompt: Prompt original
49
 
50
  Returns:
51
+ Prompt com schema injetado
52
  """
 
 
 
 
 
53
  try:
54
+ enhanced = self.schema_loader.inject_schema_in_prompt(
55
+ system_prompt=base_prompt,
56
+ specialist_name=self.specialist_name,
57
+ format_style="json" # Pode ser "json", "compact" ou "markdown"
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  )
59
 
60
+ logger.debug(f"✅ Schema injetado para {self.specialist_name}")
61
+ return enhanced
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  except Exception as e:
64
+ logger.warning(f"⚠️ Falha ao injetar schema: {e}, usando prompt original")
65
+ return base_prompt
66
 
67
+ @abstractmethod
68
+ async def process(self, data: Dict) -> Dict:
69
+ """
70
+ Processa dados com LLM.
71
+
72
+ Args:
73
+ data: Dados de entrada
74
+
75
+ Returns:
76
+ Resultado processado
77
+ """
78
+ pass
79
 
80
+ def validate_response(self, response: str) -> tuple:
81
  """
82
+ Valida resposta contra schema.
83
 
84
  Args:
85
+ response: Resposta JSON do LLM
86
 
87
  Returns:
88
+ (is_valid: bool, errors: list)
89
  """
90
+ return self.schema_loader.validate_response(response, self.specialist_name)
91
+
92
+ async def process_with_validation(self, data: Dict) -> Dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  """
94
+ Processa dados e valida resposta contra schema.
95
 
96
  Args:
97
+ data: Dados de entrada
98
 
99
  Returns:
100
+ Dict com {
101
+ "status": "success" | "error",
102
+ "result": resultado processado,
103
+ "validation_errors": lista de erros (se houver)
104
+ }
105
  """
106
  try:
107
+ # Processa
108
+ result = await self.process(data)
109
+
110
+ # Valida se resultado contém JSON
111
+ if 'content' in result:
112
+ is_valid, errors = self.validate_response(result['content'])
113
 
114
+ if not is_valid:
115
+ logger.warning(f"⚠️ Validação falhou para {self.specialist_name}: {errors}")
116
+ return {
117
+ "status": "validation_error",
118
+ "result": result,
119
+ "validation_errors": errors
120
+ }
121
 
122
+ return {
123
+ "status": "success",
124
+ "result": result,
125
+ "validation_errors": []
126
+ }
127
 
128
  except Exception as e:
129
+ logger.error(f"Erro no processamento de {self.specialist_name}: {e}")
130
+ return {
131
+ "status": "error",
132
+ "result": None,
133
+ "validation_errors": [str(e)]
134
+ }
135
+
136
+ def get_model_config(self) -> Dict:
137
+ """Retorna configuração do modelo LLM."""
138
+ return self.config.get('llm_config', {})
139
 
140
+ def get_temperature(self) -> float:
141
+ """Retorna temperatura configurada."""
142
+ return self.get_model_config().get('temperature', 0.7)
143
+
144
+ def get_max_tokens(self) -> Optional[int]:
145
+ """Retorna max_tokens configurado."""
146
+ return self.get_model_config().get('max_tokens')
147
+
148
+ def get_model_name(self) -> str:
149
+ """Retorna nome do modelo."""
150
+ return self.get_model_config().get('model', 'llama-3.3-70b-versatile')
151
+
152
+
153
+ # ============================================================================
154
+ # EXEMPLO DE USO: Processador Decisão
155
+ # ============================================================================
156
+
157
+ class ProcessorDecisao(ProcessorBase):
158
+ """Processador especializado em análise de decisão."""
159
+
160
+ def __init__(self, llm_client: Any, config: Optional[Dict] = None):
161
+ system_prompt = """Você é um especialista em análise de decisões judiciais.
162
+ Extraia com precisão: tipo de decisão, dispositivos, placar de votação,
163
+ prazos e efeitos da decisão."""
164
+
165
+ super().__init__(
166
+ specialist_name="decisao",
167
+ llm_client=llm_client,
168
+ system_prompt=system_prompt,
169
+ config=config
170
+ )
171
+
172
+ async def process(self, data: Dict) -> Dict:
173
  """
174
+ Processa análise de decisão.
175
+
176
+ Args:
177
+ data: Dict com {
178
+ "acordao_texto": texto do acórdão,
179
+ "numero_processo": número do processo,
180
+ ...
181
+ }
182
 
183
  Returns:
184
+ Dict com resultado do LLM
185
  """
186
+ # Monta user prompt
187
+ user_prompt = f"""Analise a seguinte decisão judicial:
 
188
 
189
+ PROCESSO: {data.get('numero_processo', 'N/A')}
 
190
 
191
+ TEXTO:
192
+ {data.get('acordao_texto', '')}
193
 
194
+ Extraia e retorne em JSON seguindo o schema fornecido."""
195
+
196
+ # Chama LLM com schema injetado automaticamente
197
+ response = self.llm_client.generate(
198
+ prompt=user_prompt,
199
+ system_prompt=self.system_prompt,
200
+ model=self.get_model_name(),
201
+ temperature=self.get_temperature(),
202
+ max_tokens=self.get_max_tokens(),
203
+ response_format={"type": "json_object"}
204
+ )
205
 
 
 
206
  return {
207
+ "content": response.content,
208
+ "tokens": response.total_tokens,
209
+ "model": response.model
 
 
 
 
 
 
 
 
 
 
210
  }
211
 
 
 
 
 
212
 
213
+ # ============================================================================
214
+ # EXEMPLO DE USO: Processador Fundamentação
215
+ # ============================================================================
216
+
217
+ class ProcessorFundamentacao(ProcessorBase):
218
+ """Processador especializado em análise de fundamentação jurídica."""
219
 
220
+ def __init__(self, llm_client: Any, config: Optional[Dict] = None):
221
+ system_prompt = """Você é um especialista em fundamentação jurídica.
222
+ Identifique e classifique: legislação citada, jurisprudência, doutrina e princípios."""
223
+
224
+ super().__init__(
225
+ specialist_name="fundamentacao",
226
+ llm_client=llm_client,
227
+ system_prompt=system_prompt,
228
+ config=config
229
+ )
230
+
231
+ async def process(self, data: Dict) -> Dict:
232
+ """Processa análise de fundamentação."""
233
+ user_prompt = f"""Analise a fundamentação jurídica:
234
+
235
+ TEXTO:
236
+ {data.get('acordao_texto', '')}
237
+
238
+ Extraia legislação, jurisprudência e doutrina citadas em JSON."""
239
+
240
+ response = self.llm_client.generate(
241
+ prompt=user_prompt,
242
+ system_prompt=self.system_prompt,
243
+ model=self.get_model_name(),
244
+ temperature=self.get_temperature(),
245
+ max_tokens=self.get_max_tokens(),
246
+ response_format={"type": "json_object"}
247
+ )
248
 
 
 
 
249
  return {
250
+ "content": response.content,
251
+ "tokens": response.total_tokens,
252
+ "model": response.model
 
 
253
  }
254
+
255
+
256
+ # ============================================================================
257
+ # FACTORY: Criador de processadores
258
+ # ============================================================================
259
+
260
+ def create_processor(specialist_type: str, llm_client: Any, config: Dict) -> ProcessorBase:
261
+ """
262
+ Factory para criar processador baseado no tipo.
263
+
264
+ Args:
265
+ specialist_type: Tipo do especialista ("decisao", "fundamentacao", etc)
266
+ llm_client: Cliente LLM
267
+ config: Configuração
268
+
269
+ Returns:
270
+ Instância do processador apropriado
271
+ """
272
+ processors = {
273
+ "decisao": ProcessorDecisao,
274
+ "fundamentacao": ProcessorFundamentacao,
275
+ # Adicione outros processadores aqui
276
+ }
277
+
278
+ processor_class = processors.get(specialist_type)
279
+ if not processor_class:
280
+ raise ValueError(f"Processador desconhecido: {specialist_type}")
281
+
282
+ return processor_class(llm_client=llm_client, config=config)
processors/schema_loader.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Schema Loader - Carrega e injeta schemas JSON nos system prompts."""
2
+
3
+ import os
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Dict, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SchemaLoader:
13
+ """Carrega schemas JSON e injeta em system prompts."""
14
+
15
+ # Mapeamento especialista -> arquivo schema
16
+ SCHEMA_MAP = {
17
+ "metadados": "metadados_schema.json",
18
+ "segmentacao": "segmentacao_schema.json",
19
+ "transcricao": "transcricao_schema.json",
20
+ "contexto": "contexto_schema.json",
21
+ "fundamentacao": "fundamentacao_schema.json",
22
+ "decisao": "decisao_schema.json",
23
+ "arquivo": "arquivo_schema.json",
24
+ "relatorio": "relatorio_schema.json",
25
+ "auditoria": "auditoria_schema.json",
26
+ }
27
+
28
+ def __init__(self, schemas_dir: Optional[str] = None):
29
+ """
30
+ Inicializa SchemaLoader.
31
+
32
+ Args:
33
+ schemas_dir: Diretório dos schemas (padrão: prompts/schemas/)
34
+ """
35
+ if schemas_dir is None:
36
+ # Tenta encontrar o diretório automaticamente
37
+ base_dir = Path(__file__).parent.parent
38
+ schemas_dir = base_dir / "prompts" / "schemas"
39
+
40
+ self.schemas_dir = Path(schemas_dir)
41
+
42
+ if not self.schemas_dir.exists():
43
+ logger.warning(f"⚠️ Diretório de schemas não encontrado: {self.schemas_dir}")
44
+ else:
45
+ logger.info(f"✅ SchemaLoader inicializado: {self.schemas_dir}")
46
+
47
+ # Cache de schemas carregados
48
+ self._cache: Dict[str, Dict] = {}
49
+
50
+ def load_schema(self, specialist_name: str) -> Optional[Dict]:
51
+ """
52
+ Carrega schema JSON de um especialista.
53
+
54
+ Args:
55
+ specialist_name: Nome do especialista (ex: "decisao", "metadados")
56
+
57
+ Returns:
58
+ Dict com schema JSON ou None se não encontrado
59
+ """
60
+ # Verifica cache
61
+ if specialist_name in self._cache:
62
+ return self._cache[specialist_name]
63
+
64
+ # Busca arquivo schema
65
+ schema_filename = self.SCHEMA_MAP.get(specialist_name.lower())
66
+ if not schema_filename:
67
+ logger.warning(f"⚠️ Schema não mapeado para especialista: {specialist_name}")
68
+ return None
69
+
70
+ schema_path = self.schemas_dir / schema_filename
71
+
72
+ if not schema_path.exists():
73
+ logger.warning(f"⚠️ Arquivo schema não encontrado: {schema_path}")
74
+ return None
75
+
76
+ try:
77
+ with open(schema_path, 'r', encoding='utf-8') as f:
78
+ schema = json.load(f)
79
+
80
+ # Armazena no cache
81
+ self._cache[specialist_name] = schema
82
+
83
+ logger.debug(f"✅ Schema carregado: {specialist_name}")
84
+ return schema
85
+
86
+ except Exception as e:
87
+ logger.error(f"❌ Erro ao carregar schema {schema_path}: {e}")
88
+ return None
89
+
90
+ def inject_schema_in_prompt(self,
91
+ system_prompt: str,
92
+ specialist_name: str,
93
+ format_style: str = "json") -> str:
94
+ """
95
+ Injeta schema JSON no system prompt.
96
+
97
+ Args:
98
+ system_prompt: Prompt original do sistema
99
+ specialist_name: Nome do especialista
100
+ format_style: Estilo de formatação ("json", "markdown", "compact")
101
+
102
+ Returns:
103
+ System prompt com schema injetado
104
+ """
105
+ schema = self.load_schema(specialist_name)
106
+
107
+ if not schema:
108
+ logger.warning(f"⚠️ Schema não disponível para {specialist_name}, retornando prompt original")
109
+ return system_prompt
110
+
111
+ # Formata schema conforme estilo
112
+ if format_style == "json":
113
+ schema_text = json.dumps(schema, indent=2, ensure_ascii=False)
114
+ elif format_style == "compact":
115
+ schema_text = json.dumps(schema, ensure_ascii=False)
116
+ elif format_style == "markdown":
117
+ schema_text = self._format_schema_markdown(schema)
118
+ else:
119
+ schema_text = json.dumps(schema, indent=2, ensure_ascii=False)
120
+
121
+ # Monta prompt final com schema
122
+ enhanced_prompt = f"""{system_prompt}
123
+
124
+ # JSON SCHEMA OBRIGATÓRIO
125
+
126
+ Você DEVE retornar sua resposta seguindo EXATAMENTE este JSON Schema:
127
+
128
+ ```json
129
+ {schema_text}
130
+ ```
131
+
132
+ IMPORTANTE:
133
+ - Retorne APENAS JSON válido
134
+ - Siga TODOS os campos required do schema
135
+ - Respeite os tipos de dados (string, integer, array, object)
136
+ - Valide enums e patterns quando especificados
137
+ - NÃO adicione comentários ou texto fora do JSON
138
+ - NÃO invente campos que não estão no schema
139
+ """
140
+
141
+ return enhanced_prompt
142
+
143
+ def _format_schema_markdown(self, schema: Dict) -> str:
144
+ """Formata schema em Markdown legível."""
145
+ lines = []
146
+
147
+ if 'title' in schema:
148
+ lines.append(f"## {schema['title']}")
149
+ if 'description' in schema:
150
+ lines.append(f"{schema['description']}
151
+ ")
152
+
153
+ if 'properties' in schema:
154
+ lines.append("### Campos:")
155
+ for field, props in schema['properties'].items():
156
+ field_type = props.get('type', 'any')
157
+ desc = props.get('description', '')
158
+ required = '(obrigatório)' if field in schema.get('required', []) else '(opcional)'
159
+ lines.append(f"- **{field}** ({field_type}) {required}: {desc}")
160
+
161
+ return '
162
+ '.join(lines)
163
+
164
+ def get_available_specialists(self) -> list:
165
+ """Retorna lista de especialistas com schemas disponíveis."""
166
+ return list(self.SCHEMA_MAP.keys())
167
+
168
+ def validate_response(self, response: str, specialist_name: str) -> tuple:
169
+ """
170
+ Valida resposta JSON contra schema.
171
+
172
+ Args:
173
+ response: Resposta JSON (string)
174
+ specialist_name: Nome do especialista
175
+
176
+ Returns:
177
+ (is_valid: bool, errors: list)
178
+ """
179
+ try:
180
+ from jsonschema import validate, ValidationError
181
+ except ImportError:
182
+ logger.warning("jsonschema não instalado, validação desabilitada")
183
+ return True, []
184
+
185
+ schema = self.load_schema(specialist_name)
186
+ if not schema:
187
+ return True, ["Schema não encontrado"]
188
+
189
+ try:
190
+ data = json.loads(response)
191
+ validate(instance=data, schema=schema)
192
+ return True, []
193
+ except json.JSONDecodeError as e:
194
+ return False, [f"JSON inválido: {e}"]
195
+ except ValidationError as e:
196
+ return False, [f"Validação falhou: {e.message}"]
197
+ except Exception as e:
198
+ return False, [f"Erro na validação: {e}"]
199
+
200
+
201
+ # ============================================================================
202
+ # SINGLETON GLOBAL (Opcional)
203
+ # ============================================================================
204
+
205
+ _global_loader: Optional[SchemaLoader] = None
206
+
207
+ def get_schema_loader() -> SchemaLoader:
208
+ """Retorna instância global do SchemaLoader (singleton)."""
209
+ global _global_loader
210
+ if _global_loader is None:
211
+ _global_loader = SchemaLoader()
212
+ return _global_loader