Carlex22 commited on
Commit
bc33fdf
·
1 Parent(s): 2e3ae9a

ParaAIV3.1

Browse files
.backups/pre_fix_413/context_builder.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/context_builder.py
2
+ """
3
+ Context Builder - Constrói contexto para context injection V13.6
4
+ Monta JSON parcial que especialista N deve VER (mas não modificar)
5
+ """
6
+ import logging
7
+ from typing import Dict, Any, Optional
8
+
9
+ from api.utils.logger import setup_logger
10
+
11
+ logger = setup_logger(__name__)
12
+
13
+
14
+ class ContextBuilder:
15
+ """
16
+ Constrói contexto para cada especialista
17
+
18
+ REGRA:
19
+ - Especialista N vê output de especialistas 1..N-1
20
+ - Mas vê apenas os campos RELEVANTES (não tudo)
21
+ - Evita explodir o prompt com dados desnecessários
22
+ """
23
+
24
+ def __init__(self):
25
+ # Mapear quais campos cada especialista precisa ver
26
+ self.context_rules = {
27
+ 1: [], # Segmentador: não precisa de contexto
28
+ 2: ['secoes_originais'], # Metadados: vê segmentação
29
+ 3: ['metadados'], # Classificador: vê metadados
30
+ 4: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Relatório
31
+ 5: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Fundamentação
32
+ 6: ['metadados', 'secoes_originais'], # Dispositivo
33
+ 7: ['metadados', 'classificacao_tematica', 'RELATORIO', 'FUNDAMENTACAO', 'DECISAO'] # Arquivista
34
+ }
35
+
36
+ def build_context(
37
+ self,
38
+ current_result: Dict[str, Any],
39
+ specialist_id: int
40
+ ) -> Optional[Dict[str, Any]]:
41
+ """
42
+ Constrói contexto para um especialista específico
43
+
44
+ Args:
45
+ current_result: Resultado acumulado até agora
46
+ specialist_id: ID do especialista que vai receber o contexto
47
+
48
+ Returns:
49
+ Dicionário com campos relevantes ou None (se não precisa de contexto)
50
+ """
51
+ if specialist_id not in self.context_rules:
52
+ logger.warning(f"⚠️ Especialista {specialist_id} sem regra de contexto")
53
+ return None
54
+
55
+ fields_needed = self.context_rules[specialist_id]
56
+
57
+ if not fields_needed:
58
+ # Especialista não precisa de contexto
59
+ return None
60
+
61
+ # Montar contexto com apenas os campos necessários
62
+ context = {}
63
+
64
+ for field in fields_needed:
65
+ if field in current_result:
66
+ context[field] = current_result[field]
67
+
68
+ logger.debug(f"📦 Contexto para Especialista {specialist_id}: {list(context.keys())}")
69
+
70
+ return context if context else None
71
+
72
+ def add_context_rule(self, specialist_id: int, fields: list):
73
+ """Adiciona/modifica regra de contexto para um especialista"""
74
+ self.context_rules[specialist_id] = fields
75
+ logger.info(f"✅ Regra de contexto atualizada para Especialista {specialist_id}")
76
+
77
+ def get_context_rules(self) -> Dict[int, list]:
78
+ """Retorna todas as regras de contexto"""
79
+ return self.context_rules
.backups/pre_fix_413/processor_base.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Classe Base Refatorada - Com Schema Injection automático
3
+ Compatível com LLMManager do PARA.AI
4
+ """
5
+ from abc import ABC
6
+ from typing import Dict, Any, Optional
7
+ from datetime import datetime
8
+ import logging
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ from processors.config_loader import get_config_loader, SpecialistConfig
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ProcessorBase(ABC):
19
+ """
20
+ Classe abstrata base para processadores
21
+
22
+ FEATURES:
23
+ - Carrega configuração do YAML
24
+ - Schema injection automático no system_prompt
25
+ - Compatível com LLMManager (await llm_manager.generate())
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ specialist_id: int,
31
+ llm_manager=None
32
+ ):
33
+ """
34
+ Args:
35
+ specialist_id: ID do especialista (1-9)
36
+ llm_manager: LLMManager para chamadas LLM
37
+ """
38
+ self.specialist_id = specialist_id
39
+ self.llm_manager = llm_manager
40
+
41
+ # Carregar configuração do YAML
42
+ config_loader = get_config_loader()
43
+ self.config: Optional[SpecialistConfig] = config_loader.get_specialist(specialist_id)
44
+
45
+ if not self.config:
46
+ raise ValueError(f"Especialista {specialist_id} não encontrado na configuração")
47
+
48
+ self.specialist_name = self.config.name
49
+ self.execution_time = 0
50
+ self.confidence_score = 0
51
+ self.errors = []
52
+ self.warnings = []
53
+
54
+ # 🔥 NOVA FEATURE: Carrega schema JSON e injeta no system_prompt
55
+ self._inject_schema_in_prompt()
56
+
57
+ logger.info(
58
+ f"✅ {self.specialist_name} inicializado "
59
+ f"(provider={self.config.llm_config.provider}, "
60
+ f"model={self.config.llm_config.model})"
61
+ )
62
+
63
+ def _inject_schema_in_prompt(self):
64
+ """
65
+ 🔥 NOVA FUNÇÃO: Injeta schema JSON no system_prompt.
66
+
67
+ Carrega o schema do arquivo JSON correspondente e adiciona
68
+ ao final do system_prompt para garantir que o LLM retorne
69
+ JSON na estrutura correta.
70
+ """
71
+ try:
72
+ # Mapeamento de especialista para arquivo schema
73
+ schema_map = {
74
+ 1: "metadados_schema.json",
75
+ 2: "segmentacao_schema.json",
76
+ 3: "transcricao_schema.json",
77
+ 4: "contexto_schema.json",
78
+ 5: "fundamentacao_schema.json",
79
+ 6: "decisao_schema.json",
80
+ 7: "arquivo_schema.json",
81
+ 8: "relatorio_schema.json",
82
+ 9: "auditoria_schema.json",
83
+ }
84
+
85
+ schema_filename = schema_map.get(self.specialist_id)
86
+ if not schema_filename:
87
+ logger.warning(f"⚠️ Schema não mapeado para especialista {self.specialist_id}")
88
+ return
89
+
90
+ # Localiza diretório de schemas
91
+ base_dir = Path(__file__).parent.parent
92
+ schema_path = base_dir / "prompts" / "schemas" / schema_filename
93
+
94
+ if not schema_path.exists():
95
+ logger.warning(f"⚠️ Schema não encontrado: {schema_path}")
96
+ return
97
+
98
+ # Carrega schema JSON
99
+ with open(schema_path, 'r', encoding='utf-8') as f:
100
+ schema_dict = json.load(f)
101
+
102
+ # Formata schema para injeção
103
+ schema_json = json.dumps(schema_dict, indent=2, ensure_ascii=False)
104
+
105
+ # Injeta no system_prompt
106
+ schema_instruction = f"""
107
+
108
+ # JSON SCHEMA OBRIGATÓRIO
109
+
110
+ Você DEVE retornar sua resposta seguindo EXATAMENTE este JSON Schema:
111
+
112
+ ```json
113
+ {schema_json}
114
+ ```
115
+
116
+ REGRAS CRÍTICAS:
117
+ - Retorne APENAS JSON válido (sem comentários, sem texto adicional)
118
+ - Siga TODOS os campos marcados como "required" no schema
119
+ - Respeite os tipos de dados (string, integer, array, object, boolean)
120
+ - Valide enums quando especificados
121
+ - NÃO invente campos que não estão no schema
122
+ - Use formato ISO 8601 para datas (YYYY-MM-DD)
123
+ """
124
+
125
+ # Adiciona schema ao system_prompt
126
+ self.config.system_prompt = self.config.system_prompt + schema_instruction
127
+
128
+ logger.info(f"✅ Schema {schema_filename} injetado em {self.specialist_name}")
129
+
130
+ except Exception as e:
131
+ logger.warning(f"⚠️ Falha ao injetar schema: {e}")
132
+
133
+ async def process(self, acordao_data: Dict[str, Any]) -> Dict[str, Any]:
134
+ """
135
+ Processa acórdão usando LLM REAL via LLMManager.
136
+
137
+ Args:
138
+ acordao_data: Dados do acórdão
139
+
140
+ Returns:
141
+ Resultado do processamento
142
+ """
143
+ if not self.llm_manager:
144
+ raise ValueError("❌ LLMManager não configurado")
145
+
146
+ start_time = datetime.now()
147
+
148
+ try:
149
+ # 1. Formatar prompt do usuário com dados do acórdão
150
+ user_prompt = self.config.format_user_prompt(
151
+ tribunal=acordao_data.get('tribunal', 'N/A'),
152
+ numero_processo=acordao_data.get('numero_processo', 'N/A'),
153
+ ementa=acordao_data.get('ementa', ''),
154
+ integra=acordao_data.get('integra', ''),
155
+ classe_processual=acordao_data.get('classe_processual', 'N/A')
156
+ )
157
+
158
+ # 2. Chamar LLM Manager (system_prompt já tem schema injetado)
159
+ response = await self.llm_manager.generate(
160
+ provider=self.config.llm_config.provider,
161
+ model=self.config.llm_config.model,
162
+ system_prompt=self.config.system_prompt, # 🔥 Com schema injetado
163
+ user_prompt=user_prompt,
164
+ temperature=self.config.llm_config.temperature,
165
+ max_tokens=self.config.llm_config.max_tokens
166
+ )
167
+
168
+ # 3. Processar resposta do LLMManager
169
+ if response['status'] == 'success':
170
+ # response['result'] contém o dict do client: {'content': ..., 'tokens_input': ...}
171
+ llm_result = response['result']
172
+
173
+ # Extrair content e tokens
174
+ content = llm_result.get('content', '{}')
175
+ tokens_total = llm_result.get('total_tokens', 0)
176
+
177
+ # Parse JSON se for string
178
+ if isinstance(content, str):
179
+ try:
180
+ result = json.loads(content)
181
+ except json.JSONDecodeError as e:
182
+ logger.error(f"❌ JSON inválido: {e}")
183
+ logger.debug(f"Content recebido: {content[:500]}")
184
+ raise ValueError(f"Resposta não é JSON válido: {e}")
185
+ else:
186
+ result = content
187
+
188
+ # Validar
189
+ if not self.validate(result):
190
+ self.add_warning("Resultado não passou na validação completa")
191
+
192
+ # Calcular confiança
193
+ self.confidence_score = self._calculate_confidence(result)
194
+
195
+ else:
196
+ # Erro na chamada LLM
197
+ error_msg = response.get('error', 'Erro desconhecido')
198
+ raise ValueError(f"Erro na chamada LLM: {error_msg}")
199
+
200
+ # 4. Pós-processar
201
+ self.execution_time = (datetime.now() - start_time).total_seconds()
202
+
203
+ return self.post_process(result)
204
+
205
+ except Exception as e:
206
+ self.add_error(f"Erro no processamento: {e}")
207
+ self.execution_time = (datetime.now() - start_time).total_seconds()
208
+
209
+ return {
210
+ "specialist_id": self.specialist_id,
211
+ "specialist_name": self.specialist_name,
212
+ "status": "error",
213
+ "error": str(e),
214
+ "execution_time": self.execution_time,
215
+ "timestamp": datetime.now().isoformat()
216
+ }
217
+
218
+ def validate(self, result: Dict[str, Any]) -> bool:
219
+ """
220
+ Valida resultado contra schema.
221
+
222
+ Args:
223
+ result: Resultado a validar
224
+
225
+ Returns:
226
+ True se válido
227
+ """
228
+ try:
229
+ # Validação básica: verificar campos required do schema
230
+ schema = self.config.schema
231
+
232
+ # Verifica se schema existe e tem required fields
233
+ if not isinstance(schema, dict):
234
+ logger.warning(f"⚠️ Schema não é um dict para {self.specialist_name}")
235
+ return True
236
+
237
+ required_fields = schema.get('required', [])
238
+ if not required_fields:
239
+ logger.debug(f"✓ Schema sem campos required para {self.specialist_name}")
240
+ return True
241
+
242
+ # Verifica se result é dict
243
+ if not isinstance(result, dict):
244
+ self.add_warning(f"Resultado não é um dict: {type(result)}")
245
+ return False
246
+
247
+ # Verifica campos required
248
+ missing_fields = []
249
+ for field in required_fields:
250
+ if field not in result:
251
+ missing_fields.append(field)
252
+
253
+ if missing_fields:
254
+ self.add_warning(f"Campos obrigatórios ausentes: {', '.join(missing_fields)}")
255
+ return False
256
+
257
+ return True
258
+
259
+ except Exception as e:
260
+ self.add_error(f"Erro na validação: {e}")
261
+ return False
262
+
263
+ def _calculate_confidence(self, result: Dict[str, Any]) -> int:
264
+ """
265
+ Calcula score de confiança baseado no resultado.
266
+
267
+ Returns:
268
+ Score 0-100
269
+ """
270
+ try:
271
+ # Lógica simples: quanto mais campos preenchidos, maior confiança
272
+ schema = self.config.schema
273
+
274
+ if not isinstance(schema, dict):
275
+ return 85 # Padrão se schema inválido
276
+
277
+ required_fields = schema.get('required', [])
278
+
279
+ if not required_fields:
280
+ return 85 # Padrão
281
+
282
+ if not isinstance(result, dict):
283
+ return 0
284
+
285
+ filled = sum(1 for field in required_fields if result.get(field))
286
+ confidence = int((filled / len(required_fields)) * 100)
287
+
288
+ return min(confidence, 100)
289
+
290
+ except Exception:
291
+ return 85
292
+
293
+ def post_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
294
+ """Pós-processa resultado"""
295
+ return {
296
+ "specialist_id": self.specialist_id,
297
+ "specialist_name": self.specialist_name,
298
+ "result": result,
299
+ "execution_time": self.execution_time,
300
+ "confidence_score": self.confidence_score,
301
+ "errors": self.errors,
302
+ "warnings": self.warnings,
303
+ "timestamp": datetime.now().isoformat(),
304
+ "config": {
305
+ "provider": self.config.llm_config.provider,
306
+ "model": self.config.llm_config.model,
307
+ "temperature": self.config.llm_config.temperature
308
+ }
309
+ }
310
+
311
+ def add_error(self, error_msg: str):
312
+ """Adiciona erro"""
313
+ self.errors.append(error_msg)
314
+ logger.error(f"[{self.specialist_name}] {error_msg}")
315
+
316
+ def add_warning(self, warning_msg: str):
317
+ """Adiciona aviso"""
318
+ self.warnings.append(warning_msg)
319
+ logger.warning(f"[{self.specialist_name}] {warning_msg}")
320
+
321
+ def get_schema(self) -> Dict[str, Any]:
322
+ """Retorna schema do especialista"""
323
+ return self.config.schema
324
+
325
+ @property
326
+ def info(self) -> Dict[str, Any]:
327
+ """Retorna informações sobre o processador"""
328
+ return {
329
+ "specialist_id": self.specialist_id,
330
+ "specialist_name": self.specialist_name,
331
+ "provider": self.config.llm_config.provider,
332
+ "model": self.config.llm_config.model,
333
+ "schema": self.config.schema
334
+ }
.backups/pre_fix_413_20260116_230634/context_builder.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/context_builder.py
2
+ """
3
+ Context Builder - Constrói contexto para context injection V13.6
4
+ Monta JSON parcial que especialista N deve VER (mas não modificar)
5
+ """
6
+ import logging
7
+ from typing import Dict, Any, Optional
8
+
9
+ from api.utils.logger import setup_logger
10
+
11
+ logger = setup_logger(__name__)
12
+
13
+
14
+ class ContextBuilder:
15
+ """
16
+ Constrói contexto para cada especialista
17
+
18
+ REGRA:
19
+ - Especialista N vê output de especialistas 1..N-1
20
+ - Mas vê apenas os campos RELEVANTES (não tudo)
21
+ - Evita explodir o prompt com dados desnecessários
22
+ """
23
+
24
+ def __init__(self):
25
+ # Mapear quais campos cada especialista precisa ver
26
+ self.context_rules = {
27
+ 1: [], # Segmentador: não precisa de contexto
28
+ 2: ['secoes_originais'], # Metadados: vê segmentação
29
+ 3: ['metadados'], # Classificador: vê metadados
30
+ 4: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Relatório
31
+ 5: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Fundamentação
32
+ 6: ['metadados', 'secoes_originais'], # Dispositivo
33
+ 7: ['metadados', 'classificacao_tematica', 'RELATORIO', 'FUNDAMENTACAO', 'DECISAO'] # Arquivista
34
+ }
35
+
36
+ def build_context(
37
+ self,
38
+ current_result: Dict[str, Any],
39
+ specialist_id: int
40
+ ) -> Optional[Dict[str, Any]]:
41
+ """
42
+ Constrói contexto para um especialista específico
43
+
44
+ Args:
45
+ current_result: Resultado acumulado até agora
46
+ specialist_id: ID do especialista que vai receber o contexto
47
+
48
+ Returns:
49
+ Dicionário com campos relevantes ou None (se não precisa de contexto)
50
+ """
51
+ if specialist_id not in self.context_rules:
52
+ logger.warning(f"⚠️ Especialista {specialist_id} sem regra de contexto")
53
+ return None
54
+
55
+ fields_needed = self.context_rules[specialist_id]
56
+
57
+ if not fields_needed:
58
+ # Especialista não precisa de contexto
59
+ return None
60
+
61
+ # Montar contexto com apenas os campos necessários
62
+ context = {}
63
+
64
+ for field in fields_needed:
65
+ if field in current_result:
66
+ context[field] = current_result[field]
67
+
68
+ logger.debug(f"📦 Contexto para Especialista {specialist_id}: {list(context.keys())}")
69
+
70
+ return context if context else None
71
+
72
+ def add_context_rule(self, specialist_id: int, fields: list):
73
+ """Adiciona/modifica regra de contexto para um especialista"""
74
+ self.context_rules[specialist_id] = fields
75
+ logger.info(f"✅ Regra de contexto atualizada para Especialista {specialist_id}")
76
+
77
+ def get_context_rules(self) -> Dict[int, list]:
78
+ """Retorna todas as regras de contexto"""
79
+ return self.context_rules
.backups/pre_fix_413_20260116_230634/processor_base.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Classe Base Refatorada - Com Schema Injection automático
3
+ Compatível com LLMManager do PARA.AI
4
+ """
5
+ from abc import ABC
6
+ from typing import Dict, Any, Optional
7
+ from datetime import datetime
8
+ import logging
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ from processors.config_loader import get_config_loader, SpecialistConfig
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ProcessorBase(ABC):
19
+ """
20
+ Classe abstrata base para processadores
21
+
22
+ FEATURES:
23
+ - Carrega configuração do YAML
24
+ - Schema injection automático no system_prompt
25
+ - Compatível com LLMManager (await llm_manager.generate())
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ specialist_id: int,
31
+ llm_manager=None
32
+ ):
33
+ """
34
+ Args:
35
+ specialist_id: ID do especialista (1-9)
36
+ llm_manager: LLMManager para chamadas LLM
37
+ """
38
+ self.specialist_id = specialist_id
39
+ self.llm_manager = llm_manager
40
+
41
+ # Carregar configuração do YAML
42
+ config_loader = get_config_loader()
43
+ self.config: Optional[SpecialistConfig] = config_loader.get_specialist(specialist_id)
44
+
45
+ if not self.config:
46
+ raise ValueError(f"Especialista {specialist_id} não encontrado na configuração")
47
+
48
+ self.specialist_name = self.config.name
49
+ self.execution_time = 0
50
+ self.confidence_score = 0
51
+ self.errors = []
52
+ self.warnings = []
53
+
54
+ # 🔥 NOVA FEATURE: Carrega schema JSON e injeta no system_prompt
55
+ self._inject_schema_in_prompt()
56
+
57
+ logger.info(
58
+ f"✅ {self.specialist_name} inicializado "
59
+ f"(provider={self.config.llm_config.provider}, "
60
+ f"model={self.config.llm_config.model})"
61
+ )
62
+
63
+ def _inject_schema_in_prompt(self):
64
+ """
65
+ 🔥 NOVA FUNÇÃO: Injeta schema JSON no system_prompt.
66
+
67
+ Carrega o schema do arquivo JSON correspondente e adiciona
68
+ ao final do system_prompt para garantir que o LLM retorne
69
+ JSON na estrutura correta.
70
+ """
71
+ try:
72
+ # Mapeamento de especialista para arquivo schema
73
+ schema_map = {
74
+ 1: "metadados_schema.json",
75
+ 2: "segmentacao_schema.json",
76
+ 3: "transcricao_schema.json",
77
+ 4: "contexto_schema.json",
78
+ 5: "fundamentacao_schema.json",
79
+ 6: "decisao_schema.json",
80
+ 7: "arquivo_schema.json",
81
+ 8: "relatorio_schema.json",
82
+ 9: "auditoria_schema.json",
83
+ }
84
+
85
+ schema_filename = schema_map.get(self.specialist_id)
86
+ if not schema_filename:
87
+ logger.warning(f"⚠️ Schema não mapeado para especialista {self.specialist_id}")
88
+ return
89
+
90
+ # Localiza diretório de schemas
91
+ base_dir = Path(__file__).parent.parent
92
+ schema_path = base_dir / "prompts" / "schemas" / schema_filename
93
+
94
+ if not schema_path.exists():
95
+ logger.warning(f"⚠️ Schema não encontrado: {schema_path}")
96
+ return
97
+
98
+ # Carrega schema JSON
99
+ with open(schema_path, 'r', encoding='utf-8') as f:
100
+ schema_dict = json.load(f)
101
+
102
+ # Formata schema para injeção
103
+ schema_json = json.dumps(schema_dict, indent=2, ensure_ascii=False)
104
+
105
+ # Injeta no system_prompt
106
+ schema_instruction = f"""
107
+
108
+ # JSON SCHEMA OBRIGATÓRIO
109
+
110
+ Você DEVE retornar sua resposta seguindo EXATAMENTE este JSON Schema:
111
+
112
+ ```json
113
+ {schema_json}
114
+ ```
115
+
116
+ REGRAS CRÍTICAS:
117
+ - Retorne APENAS JSON válido (sem comentários, sem texto adicional)
118
+ - Siga TODOS os campos marcados como "required" no schema
119
+ - Respeite os tipos de dados (string, integer, array, object, boolean)
120
+ - Valide enums quando especificados
121
+ - NÃO invente campos que não estão no schema
122
+ - Use formato ISO 8601 para datas (YYYY-MM-DD)
123
+ """
124
+
125
+ # Adiciona schema ao system_prompt
126
+ self.config.system_prompt = self.config.system_prompt + schema_instruction
127
+
128
+ logger.info(f"✅ Schema {schema_filename} injetado em {self.specialist_name}")
129
+
130
+ except Exception as e:
131
+ logger.warning(f"⚠️ Falha ao injetar schema: {e}")
132
+
133
+ async def process(self, acordao_data: Dict[str, Any]) -> Dict[str, Any]:
134
+ """
135
+ Processa acórdão usando LLM REAL via LLMManager.
136
+
137
+ Args:
138
+ acordao_data: Dados do acórdão
139
+
140
+ Returns:
141
+ Resultado do processamento
142
+ """
143
+ if not self.llm_manager:
144
+ raise ValueError("❌ LLMManager não configurado")
145
+
146
+ start_time = datetime.now()
147
+
148
+ try:
149
+ # 1. Formatar prompt do usuário com dados do acórdão
150
+ user_prompt = self.config.format_user_prompt(
151
+ tribunal=acordao_data.get('tribunal', 'N/A'),
152
+ numero_processo=acordao_data.get('numero_processo', 'N/A'),
153
+ ementa=acordao_data.get('ementa', ''),
154
+ integra=acordao_data.get('integra', ''),
155
+ classe_processual=acordao_data.get('classe_processual', 'N/A')
156
+ )
157
+
158
+ # 2. Chamar LLM Manager (system_prompt já tem schema injetado)
159
+ response = await self.llm_manager.generate(
160
+ provider=self.config.llm_config.provider,
161
+ model=self.config.llm_config.model,
162
+ system_prompt=self.config.system_prompt, # 🔥 Com schema injetado
163
+ user_prompt=user_prompt,
164
+ temperature=self.config.llm_config.temperature,
165
+ max_tokens=self.config.llm_config.max_tokens
166
+ )
167
+
168
+ # 3. Processar resposta do LLMManager
169
+ if response['status'] == 'success':
170
+ # response['result'] contém o dict do client: {'content': ..., 'tokens_input': ...}
171
+ llm_result = response['result']
172
+
173
+ # Extrair content e tokens
174
+ content = llm_result.get('content', '{}')
175
+ tokens_total = llm_result.get('total_tokens', 0)
176
+
177
+ # Parse JSON se for string
178
+ if isinstance(content, str):
179
+ try:
180
+ result = json.loads(content)
181
+ except json.JSONDecodeError as e:
182
+ logger.error(f"❌ JSON inválido: {e}")
183
+ logger.debug(f"Content recebido: {content[:500]}")
184
+ raise ValueError(f"Resposta não é JSON válido: {e}")
185
+ else:
186
+ result = content
187
+
188
+ # Validar
189
+ if not self.validate(result):
190
+ self.add_warning("Resultado não passou na validação completa")
191
+
192
+ # Calcular confiança
193
+ self.confidence_score = self._calculate_confidence(result)
194
+
195
+ else:
196
+ # Erro na chamada LLM
197
+ error_msg = response.get('error', 'Erro desconhecido')
198
+ raise ValueError(f"Erro na chamada LLM: {error_msg}")
199
+
200
+ # 4. Pós-processar
201
+ self.execution_time = (datetime.now() - start_time).total_seconds()
202
+
203
+ return self.post_process(result)
204
+
205
+ except Exception as e:
206
+ self.add_error(f"Erro no processamento: {e}")
207
+ self.execution_time = (datetime.now() - start_time).total_seconds()
208
+
209
+ return {
210
+ "specialist_id": self.specialist_id,
211
+ "specialist_name": self.specialist_name,
212
+ "status": "error",
213
+ "error": str(e),
214
+ "execution_time": self.execution_time,
215
+ "timestamp": datetime.now().isoformat()
216
+ }
217
+
218
+ def validate(self, result: Dict[str, Any]) -> bool:
219
+ """
220
+ Valida resultado contra schema.
221
+
222
+ Args:
223
+ result: Resultado a validar
224
+
225
+ Returns:
226
+ True se válido
227
+ """
228
+ try:
229
+ # Validação básica: verificar campos required do schema
230
+ schema = self.config.schema
231
+
232
+ # Verifica se schema existe e tem required fields
233
+ if not isinstance(schema, dict):
234
+ logger.warning(f"⚠️ Schema não é um dict para {self.specialist_name}")
235
+ return True
236
+
237
+ required_fields = schema.get('required', [])
238
+ if not required_fields:
239
+ logger.debug(f"✓ Schema sem campos required para {self.specialist_name}")
240
+ return True
241
+
242
+ # Verifica se result é dict
243
+ if not isinstance(result, dict):
244
+ self.add_warning(f"Resultado não é um dict: {type(result)}")
245
+ return False
246
+
247
+ # Verifica campos required
248
+ missing_fields = []
249
+ for field in required_fields:
250
+ if field not in result:
251
+ missing_fields.append(field)
252
+
253
+ if missing_fields:
254
+ self.add_warning(f"Campos obrigatórios ausentes: {', '.join(missing_fields)}")
255
+ return False
256
+
257
+ return True
258
+
259
+ except Exception as e:
260
+ self.add_error(f"Erro na validação: {e}")
261
+ return False
262
+
263
+ def _calculate_confidence(self, result: Dict[str, Any]) -> int:
264
+ """
265
+ Calcula score de confiança baseado no resultado.
266
+
267
+ Returns:
268
+ Score 0-100
269
+ """
270
+ try:
271
+ # Lógica simples: quanto mais campos preenchidos, maior confiança
272
+ schema = self.config.schema
273
+
274
+ if not isinstance(schema, dict):
275
+ return 85 # Padrão se schema inválido
276
+
277
+ required_fields = schema.get('required', [])
278
+
279
+ if not required_fields:
280
+ return 85 # Padrão
281
+
282
+ if not isinstance(result, dict):
283
+ return 0
284
+
285
+ filled = sum(1 for field in required_fields if result.get(field))
286
+ confidence = int((filled / len(required_fields)) * 100)
287
+
288
+ return min(confidence, 100)
289
+
290
+ except Exception:
291
+ return 85
292
+
293
+ def post_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
294
+ """Pós-processa resultado"""
295
+ return {
296
+ "specialist_id": self.specialist_id,
297
+ "specialist_name": self.specialist_name,
298
+ "result": result,
299
+ "execution_time": self.execution_time,
300
+ "confidence_score": self.confidence_score,
301
+ "errors": self.errors,
302
+ "warnings": self.warnings,
303
+ "timestamp": datetime.now().isoformat(),
304
+ "config": {
305
+ "provider": self.config.llm_config.provider,
306
+ "model": self.config.llm_config.model,
307
+ "temperature": self.config.llm_config.temperature
308
+ }
309
+ }
310
+
311
+ def add_error(self, error_msg: str):
312
+ """Adiciona erro"""
313
+ self.errors.append(error_msg)
314
+ logger.error(f"[{self.specialist_name}] {error_msg}")
315
+
316
+ def add_warning(self, warning_msg: str):
317
+ """Adiciona aviso"""
318
+ self.warnings.append(warning_msg)
319
+ logger.warning(f"[{self.specialist_name}] {warning_msg}")
320
+
321
+ def get_schema(self) -> Dict[str, Any]:
322
+ """Retorna schema do especialista"""
323
+ return self.config.schema
324
+
325
+ @property
326
+ def info(self) -> Dict[str, Any]:
327
+ """Retorna informações sobre o processador"""
328
+ return {
329
+ "specialist_id": self.specialist_id,
330
+ "specialist_name": self.specialist_name,
331
+ "provider": self.config.llm_config.provider,
332
+ "model": self.config.llm_config.model,
333
+ "schema": self.config.schema
334
+ }
.backups/pre_fix_413_20260116_231356/context_builder.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/context_builder.py
2
+ """
3
+ Context Builder - Constrói contexto para context injection V13.6
4
+ Monta JSON parcial que especialista N deve VER (mas não modificar)
5
+ """
6
+ import logging
7
+ from typing import Dict, Any, Optional
8
+
9
+ from api.utils.logger import setup_logger
10
+
11
+ logger = setup_logger(__name__)
12
+
13
+
14
+ class ContextBuilder:
15
+ """
16
+ Constrói contexto para cada especialista
17
+
18
+ REGRA:
19
+ - Especialista N vê output de especialistas 1..N-1
20
+ - Mas vê apenas os campos RELEVANTES (não tudo)
21
+ - Evita explodir o prompt com dados desnecessários
22
+ """
23
+
24
+ def __init__(self):
25
+ # Mapear quais campos cada especialista precisa ver
26
+ self.context_rules = {
27
+ 1: [], # Segmentador: não precisa de contexto
28
+ 2: ['secoes_originais'], # Metadados: vê segmentação
29
+ 3: ['metadados'], # Classificador: vê metadados
30
+ 4: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Relatório
31
+ 5: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Fundamentação
32
+ 6: ['metadados', 'secoes_originais'], # Dispositivo
33
+ 7: ['metadados', 'classificacao_tematica', 'RELATORIO', 'FUNDAMENTACAO', 'DECISAO'] # Arquivista
34
+ }
35
+
36
+ def build_context(
37
+ self,
38
+ current_result: Dict[str, Any],
39
+ specialist_id: int
40
+ ) -> Optional[Dict[str, Any]]:
41
+ """
42
+ Constrói contexto para um especialista específico
43
+
44
+ Args:
45
+ current_result: Resultado acumulado até agora
46
+ specialist_id: ID do especialista que vai receber o contexto
47
+
48
+ Returns:
49
+ Dicionário com campos relevantes ou None (se não precisa de contexto)
50
+ """
51
+ if specialist_id not in self.context_rules:
52
+ logger.warning(f"⚠️ Especialista {specialist_id} sem regra de contexto")
53
+ return None
54
+
55
+ fields_needed = self.context_rules[specialist_id]
56
+
57
+ if not fields_needed:
58
+ # Especialista não precisa de contexto
59
+ return None
60
+
61
+ # Montar contexto com apenas os campos necessários
62
+ context = {}
63
+
64
+ for field in fields_needed:
65
+ if field in current_result:
66
+ context[field] = current_result[field]
67
+
68
+ logger.debug(f"📦 Contexto para Especialista {specialist_id}: {list(context.keys())}")
69
+
70
+ return context if context else None
71
+
72
+ def add_context_rule(self, specialist_id: int, fields: list):
73
+ """Adiciona/modifica regra de contexto para um especialista"""
74
+ self.context_rules[specialist_id] = fields
75
+ logger.info(f"✅ Regra de contexto atualizada para Especialista {specialist_id}")
76
+
77
+ def get_context_rules(self) -> Dict[int, list]:
78
+ """Retorna todas as regras de contexto"""
79
+ return self.context_rules
.backups/pre_fix_413_20260116_231356/processor_base.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Classe Base Refatorada - Com Schema Injection automático
3
+ Compatível com LLMManager do PARA.AI
4
+ """
5
+ from abc import ABC
6
+ from typing import Dict, Any, Optional
7
+ from datetime import datetime
8
+ import logging
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ from processors.config_loader import get_config_loader, SpecialistConfig
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ProcessorBase(ABC):
19
+ """
20
+ Classe abstrata base para processadores
21
+
22
+ FEATURES:
23
+ - Carrega configuração do YAML
24
+ - Schema injection automático no system_prompt
25
+ - Compatível com LLMManager (await llm_manager.generate())
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ specialist_id: int,
31
+ llm_manager=None
32
+ ):
33
+ """
34
+ Args:
35
+ specialist_id: ID do especialista (1-9)
36
+ llm_manager: LLMManager para chamadas LLM
37
+ """
38
+ self.specialist_id = specialist_id
39
+ self.llm_manager = llm_manager
40
+
41
+ # Carregar configuração do YAML
42
+ config_loader = get_config_loader()
43
+ self.config: Optional[SpecialistConfig] = config_loader.get_specialist(specialist_id)
44
+
45
+ if not self.config:
46
+ raise ValueError(f"Especialista {specialist_id} não encontrado na configuração")
47
+
48
+ self.specialist_name = self.config.name
49
+ self.execution_time = 0
50
+ self.confidence_score = 0
51
+ self.errors = []
52
+ self.warnings = []
53
+
54
+ # 🔥 NOVA FEATURE: Carrega schema JSON e injeta no system_prompt
55
+ self._inject_schema_in_prompt()
56
+
57
+ logger.info(
58
+ f"✅ {self.specialist_name} inicializado "
59
+ f"(provider={self.config.llm_config.provider}, "
60
+ f"model={self.config.llm_config.model})"
61
+ )
62
+
63
+ def _inject_schema_in_prompt(self):
64
+ """
65
+ 🔥 NOVA FUNÇÃO: Injeta schema JSON no system_prompt.
66
+
67
+ Carrega o schema do arquivo JSON correspondente e adiciona
68
+ ao final do system_prompt para garantir que o LLM retorne
69
+ JSON na estrutura correta.
70
+ """
71
+ try:
72
+ # Mapeamento de especialista para arquivo schema
73
+ schema_map = {
74
+ 1: "metadados_schema.json",
75
+ 2: "segmentacao_schema.json",
76
+ 3: "transcricao_schema.json",
77
+ 4: "contexto_schema.json",
78
+ 5: "fundamentacao_schema.json",
79
+ 6: "decisao_schema.json",
80
+ 7: "arquivo_schema.json",
81
+ 8: "relatorio_schema.json",
82
+ 9: "auditoria_schema.json",
83
+ }
84
+
85
+ schema_filename = schema_map.get(self.specialist_id)
86
+ if not schema_filename:
87
+ logger.warning(f"⚠️ Schema não mapeado para especialista {self.specialist_id}")
88
+ return
89
+
90
+ # Localiza diretório de schemas
91
+ base_dir = Path(__file__).parent.parent
92
+ schema_path = base_dir / "prompts" / "schemas" / schema_filename
93
+
94
+ if not schema_path.exists():
95
+ logger.warning(f"⚠️ Schema não encontrado: {schema_path}")
96
+ return
97
+
98
+ # Carrega schema JSON
99
+ with open(schema_path, 'r', encoding='utf-8') as f:
100
+ schema_dict = json.load(f)
101
+
102
+ # Formata schema para injeção
103
+ schema_json = json.dumps(schema_dict, indent=2, ensure_ascii=False)
104
+
105
+ # Injeta no system_prompt
106
+ schema_instruction = f"""
107
+
108
+ # JSON SCHEMA OBRIGATÓRIO
109
+
110
+ Você DEVE retornar sua resposta seguindo EXATAMENTE este JSON Schema:
111
+
112
+ ```json
113
+ {schema_json}
114
+ ```
115
+
116
+ REGRAS CRÍTICAS:
117
+ - Retorne APENAS JSON válido (sem comentários, sem texto adicional)
118
+ - Siga TODOS os campos marcados como "required" no schema
119
+ - Respeite os tipos de dados (string, integer, array, object, boolean)
120
+ - Valide enums quando especificados
121
+ - NÃO invente campos que não estão no schema
122
+ - Use formato ISO 8601 para datas (YYYY-MM-DD)
123
+ """
124
+
125
+ # Adiciona schema ao system_prompt
126
+ self.config.system_prompt = self.config.system_prompt + schema_instruction
127
+
128
+ logger.info(f"✅ Schema {schema_filename} injetado em {self.specialist_name}")
129
+
130
+ except Exception as e:
131
+ logger.warning(f"⚠️ Falha ao injetar schema: {e}")
132
+
133
+ async def process(self, acordao_data: Dict[str, Any]) -> Dict[str, Any]:
134
+ """
135
+ Processa acórdão usando LLM REAL via LLMManager.
136
+
137
+ Args:
138
+ acordao_data: Dados do acórdão
139
+
140
+ Returns:
141
+ Resultado do processamento
142
+ """
143
+ if not self.llm_manager:
144
+ raise ValueError("❌ LLMManager não configurado")
145
+
146
+ start_time = datetime.now()
147
+
148
+ try:
149
+ # 1. Formatar prompt do usuário com dados do acórdão
150
+ user_prompt = self.config.format_user_prompt(
151
+ tribunal=acordao_data.get('tribunal', 'N/A'),
152
+ numero_processo=acordao_data.get('numero_processo', 'N/A'),
153
+ ementa=acordao_data.get('ementa', ''),
154
+ integra=acordao_data.get('integra', ''),
155
+ classe_processual=acordao_data.get('classe_processual', 'N/A')
156
+ )
157
+
158
+ # 2. Chamar LLM Manager (system_prompt já tem schema injetado)
159
+ response = await self.llm_manager.generate(
160
+ provider=self.config.llm_config.provider,
161
+ model=self.config.llm_config.model,
162
+ system_prompt=self.config.system_prompt, # 🔥 Com schema injetado
163
+ user_prompt=user_prompt,
164
+ temperature=self.config.llm_config.temperature,
165
+ max_tokens=self.config.llm_config.max_tokens
166
+ )
167
+
168
+ # 3. Processar resposta do LLMManager
169
+ if response['status'] == 'success':
170
+ # response['result'] contém o dict do client: {'content': ..., 'tokens_input': ...}
171
+ llm_result = response['result']
172
+
173
+ # Extrair content e tokens
174
+ content = llm_result.get('content', '{}')
175
+ tokens_total = llm_result.get('total_tokens', 0)
176
+
177
+ # Parse JSON se for string
178
+ if isinstance(content, str):
179
+ try:
180
+ result = json.loads(content)
181
+ except json.JSONDecodeError as e:
182
+ logger.error(f"❌ JSON inválido: {e}")
183
+ logger.debug(f"Content recebido: {content[:500]}")
184
+ raise ValueError(f"Resposta não é JSON válido: {e}")
185
+ else:
186
+ result = content
187
+
188
+ # Validar
189
+ if not self.validate(result):
190
+ self.add_warning("Resultado não passou na validação completa")
191
+
192
+ # Calcular confiança
193
+ self.confidence_score = self._calculate_confidence(result)
194
+
195
+ else:
196
+ # Erro na chamada LLM
197
+ error_msg = response.get('error', 'Erro desconhecido')
198
+ raise ValueError(f"Erro na chamada LLM: {error_msg}")
199
+
200
+ # 4. Pós-processar
201
+ self.execution_time = (datetime.now() - start_time).total_seconds()
202
+
203
+ return self.post_process(result)
204
+
205
+ except Exception as e:
206
+ self.add_error(f"Erro no processamento: {e}")
207
+ self.execution_time = (datetime.now() - start_time).total_seconds()
208
+
209
+ return {
210
+ "specialist_id": self.specialist_id,
211
+ "specialist_name": self.specialist_name,
212
+ "status": "error",
213
+ "error": str(e),
214
+ "execution_time": self.execution_time,
215
+ "timestamp": datetime.now().isoformat()
216
+ }
217
+
218
+ def validate(self, result: Dict[str, Any]) -> bool:
219
+ """
220
+ Valida resultado contra schema.
221
+
222
+ Args:
223
+ result: Resultado a validar
224
+
225
+ Returns:
226
+ True se válido
227
+ """
228
+ try:
229
+ # Validação básica: verificar campos required do schema
230
+ schema = self.config.schema
231
+
232
+ # Verifica se schema existe e tem required fields
233
+ if not isinstance(schema, dict):
234
+ logger.warning(f"⚠️ Schema não é um dict para {self.specialist_name}")
235
+ return True
236
+
237
+ required_fields = schema.get('required', [])
238
+ if not required_fields:
239
+ logger.debug(f"✓ Schema sem campos required para {self.specialist_name}")
240
+ return True
241
+
242
+ # Verifica se result é dict
243
+ if not isinstance(result, dict):
244
+ self.add_warning(f"Resultado não é um dict: {type(result)}")
245
+ return False
246
+
247
+ # Verifica campos required
248
+ missing_fields = []
249
+ for field in required_fields:
250
+ if field not in result:
251
+ missing_fields.append(field)
252
+
253
+ if missing_fields:
254
+ self.add_warning(f"Campos obrigatórios ausentes: {', '.join(missing_fields)}")
255
+ return False
256
+
257
+ return True
258
+
259
+ except Exception as e:
260
+ self.add_error(f"Erro na validação: {e}")
261
+ return False
262
+
263
+ def _calculate_confidence(self, result: Dict[str, Any]) -> int:
264
+ """
265
+ Calcula score de confiança baseado no resultado.
266
+
267
+ Returns:
268
+ Score 0-100
269
+ """
270
+ try:
271
+ # Lógica simples: quanto mais campos preenchidos, maior confiança
272
+ schema = self.config.schema
273
+
274
+ if not isinstance(schema, dict):
275
+ return 85 # Padrão se schema inválido
276
+
277
+ required_fields = schema.get('required', [])
278
+
279
+ if not required_fields:
280
+ return 85 # Padrão
281
+
282
+ if not isinstance(result, dict):
283
+ return 0
284
+
285
+ filled = sum(1 for field in required_fields if result.get(field))
286
+ confidence = int((filled / len(required_fields)) * 100)
287
+
288
+ return min(confidence, 100)
289
+
290
+ except Exception:
291
+ return 85
292
+
293
+ def post_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
294
+ """Pós-processa resultado"""
295
+ return {
296
+ "specialist_id": self.specialist_id,
297
+ "specialist_name": self.specialist_name,
298
+ "result": result,
299
+ "execution_time": self.execution_time,
300
+ "confidence_score": self.confidence_score,
301
+ "errors": self.errors,
302
+ "warnings": self.warnings,
303
+ "timestamp": datetime.now().isoformat(),
304
+ "config": {
305
+ "provider": self.config.llm_config.provider,
306
+ "model": self.config.llm_config.model,
307
+ "temperature": self.config.llm_config.temperature
308
+ }
309
+ }
310
+
311
+ def add_error(self, error_msg: str):
312
+ """Adiciona erro"""
313
+ self.errors.append(error_msg)
314
+ logger.error(f"[{self.specialist_name}] {error_msg}")
315
+
316
+ def add_warning(self, warning_msg: str):
317
+ """Adiciona aviso"""
318
+ self.warnings.append(warning_msg)
319
+ logger.warning(f"[{self.specialist_name}] {warning_msg}")
320
+
321
+ def get_schema(self) -> Dict[str, Any]:
322
+ """Retorna schema do especialista"""
323
+ return self.config.schema
324
+
325
+ @property
326
+ def info(self) -> Dict[str, Any]:
327
+ """Retorna informações sobre o processador"""
328
+ return {
329
+ "specialist_id": self.specialist_id,
330
+ "specialist_name": self.specialist_name,
331
+ "provider": self.config.llm_config.provider,
332
+ "model": self.config.llm_config.model,
333
+ "schema": self.config.schema
334
+ }
core/context_builder.py CHANGED
@@ -1,14 +1,14 @@
1
  ##PARA.AI/core/context_builder.py
2
  """
3
- Context Builder - Constrói contexto para context injection V13.6
4
  Monta JSON parcial que especialista N deve VER (mas não modificar)
 
 
5
  """
6
  import logging
7
  from typing import Dict, Any, Optional
8
 
9
- from api.utils.logger import setup_logger
10
-
11
- logger = setup_logger(__name__)
12
 
13
 
14
  class ContextBuilder:
@@ -22,15 +22,53 @@ class ContextBuilder:
22
  """
23
 
24
  def __init__(self):
25
- # Mapear quais campos cada especialista precisa ver
 
26
  self.context_rules = {
27
  1: [], # Segmentador: não precisa de contexto
28
- 2: ['secoes_originais'], # Metadados: vê segmentação
29
- 3: ['metadados'], # Classificador: vê metadados
30
- 4: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Relatório
31
- 5: ['metadados', 'classificacao_tematica', 'secoes_originais'], # Fundamentação
32
- 6: ['metadados', 'secoes_originais'], # Dispositivo
33
- 7: ['metadados', 'classificacao_tematica', 'RELATORIO', 'FUNDAMENTACAO', 'DECISAO'] # Arquivista
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
 
36
  def build_context(
@@ -62,10 +100,20 @@ class ContextBuilder:
62
  context = {}
63
 
64
  for field in fields_needed:
 
65
  if field in current_result:
66
  context[field] = current_result[field]
67
-
68
- logger.debug(f"📦 Contexto para Especialista {specialist_id}: {list(context.keys())}")
 
 
 
 
 
 
 
 
 
69
 
70
  return context if context else None
71
 
 
1
  ##PARA.AI/core/context_builder.py
2
  """
3
+ Context Builder - Constrói contexto para context injection V13.6 CORRIGIDO
4
  Monta JSON parcial que especialista N deve VER (mas não modificar)
5
+
6
+ CORREÇÃO: Ajustado mapeamento de campos para compatibilidade V13.1
7
  """
8
  import logging
9
  from typing import Dict, Any, Optional
10
 
11
+ logger = logging.getLogger(__name__)
 
 
12
 
13
 
14
  class ContextBuilder:
 
22
  """
23
 
24
  def __init__(self):
25
+ # CORRIGIDO: Mapear quais campos cada especialista precisa ver
26
+ # Ajustado para os nomes de campos REAIS que os processadores V13.1 retornam
27
  self.context_rules = {
28
  1: [], # Segmentador: não precisa de contexto
29
+
30
+ 2: [ # Metadados: vê segmentação
31
+ 'secoes_originais',
32
+ 'RELATORIO_texto_completo',
33
+ 'FUNDAMENTACAO_texto_completo',
34
+ 'DISPOSITIVO_texto_completo',
35
+ ],
36
+
37
+ 3: [ # Classificador: vê metadados
38
+ 'metadados',
39
+ 'tribunal',
40
+ 'orgao_julgador',
41
+ 'classe_processual',
42
+ ],
43
+
44
+ 4: [ # Relatório: vê segmentação, metadados, classificação
45
+ 'metadados',
46
+ 'classificacao_tematica',
47
+ 'RELATORIO_texto_completo', # CORRIGIDO: nome correto do campo
48
+ ],
49
+
50
+ 5: [ # Fundamentação: vê segmentação, metadados, classificação
51
+ 'metadados',
52
+ 'classificacao_tematica',
53
+ 'FUNDAMENTACAO_texto_completo', # CORRIGIDO: nome correto
54
+ ],
55
+
56
+ 6: [ # Dispositivo: vê segmentação, metadados
57
+ 'metadados',
58
+ 'DISPOSITIVO_texto_completo', # CORRIGIDO: nome correto
59
+ 'ementa', # Adicionado: dispositivo precisa da ementa
60
+ ],
61
+
62
+ 7: [ # Arquivista: vê TUDO (análise meta-cognitiva)
63
+ 'metadados',
64
+ 'classificacao_tematica',
65
+ 'RELATORIO',
66
+ 'FUNDAMENTACAO',
67
+ 'DECISAO',
68
+ 'teses_fragmentadas',
69
+ 'teses_relator',
70
+ 'mapa_pedidos',
71
+ ]
72
  }
73
 
74
  def build_context(
 
100
  context = {}
101
 
102
  for field in fields_needed:
103
+ # CORRIGIDO: Buscar campo em diferentes níveis de aninhamento
104
  if field in current_result:
105
  context[field] = current_result[field]
106
+ elif 'secoes_originais' in current_result and field in current_result['secoes_originais']:
107
+ context[field] = current_result['secoes_originais'][field]
108
+ elif 'metadados' in current_result and isinstance(current_result['metadados'], dict):
109
+ if field in current_result['metadados']:
110
+ context[field] = current_result['metadados'][field]
111
+
112
+ # CORRIGIDO: Log mais informativo
113
+ if context:
114
+ logger.debug(f"📦 Contexto para Especialista {specialist_id}: {list(context.keys())} ({len(str(context))} chars)")
115
+ else:
116
+ logger.debug(f"📦 Contexto para Especialista {specialist_id}: vazio")
117
 
118
  return context if context else None
119
 
core/payload_manager.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/payload_manager.py
2
+ """
3
+ Payload Manager - Gerencia tamanho de payloads para evitar erro 413
4
+ NOVO: Criado para V13.6
5
+ """
6
+ import logging
7
+ from typing import Dict, Any, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class PayloadManager:
13
+ """
14
+ Gerenciador de payloads para evitar erro 413
15
+
16
+ LIMITES GROQ:
17
+ - Llama 3.1 70B: ~8k tokens contexto (~32k chars)
18
+ - Erro 413 ocorre em ~128k chars total
19
+
20
+ ESTRATÉGIA:
21
+ - Truncar cada seção para 15k chars
22
+ - Payload total máximo: 50k chars
23
+ """
24
+
25
+ # Limites conservadores
26
+ MAX_SECTION_CHARS = 15000
27
+ MAX_TOTAL_CHARS = 50000
28
+ MAX_CONTEXT_CHARS = 10000 # Para o contexto de especialistas anteriores
29
+
30
+ def __init__(self):
31
+ logger.info("✅ PayloadManager inicializado")
32
+
33
+ def truncate_text(
34
+ self,
35
+ text: str,
36
+ max_chars: int = None,
37
+ add_marker: bool = True
38
+ ) -> str:
39
+ """
40
+ Trunca texto de forma inteligente
41
+
42
+ Args:
43
+ text: Texto a truncar
44
+ max_chars: Máximo de caracteres (padrão: MAX_SECTION_CHARS)
45
+ add_marker: Se True, adiciona marcador de truncamento
46
+
47
+ Returns:
48
+ Texto truncado
49
+ """
50
+ if not text or not isinstance(text, str):
51
+ return ""
52
+
53
+ max_chars = max_chars or self.MAX_SECTION_CHARS
54
+
55
+ if len(text) <= max_chars:
56
+ return text
57
+
58
+ # Truncar
59
+ truncated = text[:max_chars]
60
+
61
+ # Encontrar último espaço para não cortar palavra
62
+ last_space = truncated.rfind(' ')
63
+ if last_space > max_chars * 0.9:
64
+ truncated = truncated[:last_space]
65
+
66
+ if add_marker:
67
+ truncated += "\n\n[... TEXTO TRUNCADO PARA REDUZIR PAYLOAD ...]"
68
+
69
+ logger.debug(f"✂️ Truncado: {len(text)} → {len(truncated)} chars")
70
+
71
+ return truncated
72
+
73
+ def prepare_acordao_for_llm(
74
+ self,
75
+ acordao_data: Dict[str, Any],
76
+ keep_full: Optional[list] = None
77
+ ) -> Dict[str, Any]:
78
+ """
79
+ Prepara acórdão para LLM truncando textos longos
80
+
81
+ Args:
82
+ acordao_data: Dados completos do acórdão
83
+ keep_full: Lista de campos para NÃO truncar (opcional)
84
+
85
+ Returns:
86
+ Dados preparados e truncados
87
+ """
88
+ keep_full = keep_full or []
89
+ prepared = acordao_data.copy()
90
+
91
+ # Campos de texto para truncar
92
+ text_fields = {
93
+ 'inteiro_teor': self.MAX_SECTION_CHARS,
94
+ 'ementa': 3000, # Ementa geralmente é curta
95
+ 'RELATORIO_texto_completo': self.MAX_SECTION_CHARS,
96
+ 'FUNDAMENTACAO_texto_completo': self.MAX_SECTION_CHARS,
97
+ 'DISPOSITIVO_texto_completo': self.MAX_SECTION_CHARS,
98
+ }
99
+
100
+ for field, max_chars in text_fields.items():
101
+ if field in keep_full:
102
+ continue
103
+
104
+ # Truncar em nível raiz
105
+ if field in prepared and isinstance(prepared[field], str):
106
+ prepared[field] = self.truncate_text(prepared[field], max_chars)
107
+
108
+ # Truncar em secoes_originais
109
+ if 'secoes_originais' in prepared and isinstance(prepared['secoes_originais'], dict):
110
+ if field in prepared['secoes_originais']:
111
+ if isinstance(prepared['secoes_originais'][field], str):
112
+ prepared['secoes_originais'][field] = self.truncate_text(
113
+ prepared['secoes_originais'][field],
114
+ max_chars
115
+ )
116
+
117
+ return prepared
118
+
119
+ def truncate_context(self, context: Dict[str, Any]) -> Dict[str, Any]:
120
+ """
121
+ Trunca contexto de especialistas anteriores
122
+
123
+ Args:
124
+ context: Contexto completo
125
+
126
+ Returns:
127
+ Contexto truncado
128
+ """
129
+ if not context:
130
+ return {}
131
+
132
+ truncated = {}
133
+
134
+ for key, value in context.items():
135
+ if isinstance(value, str):
136
+ # Truncar strings longas
137
+ truncated[key] = self.truncate_text(
138
+ value,
139
+ self.MAX_CONTEXT_CHARS,
140
+ add_marker=False
141
+ )
142
+ elif isinstance(value, dict):
143
+ # Recursivo para dicionários aninhados
144
+ truncated[key] = self.truncate_context(value)
145
+ elif isinstance(value, list):
146
+ # Para listas, truncar strings internas
147
+ truncated[key] = [
148
+ self.truncate_text(item, 1000, add_marker=False)
149
+ if isinstance(item, str)
150
+ else item
151
+ for item in value[:10] # Máximo 10 itens
152
+ ]
153
+ else:
154
+ # Outros tipos: copiar direto
155
+ truncated[key] = value
156
+
157
+ return truncated
158
+
159
+ def estimate_payload_size(self, data: Dict[str, Any]) -> int:
160
+ """
161
+ Estima tamanho do payload em caracteres
162
+
163
+ Args:
164
+ data: Dados a estimar
165
+
166
+ Returns:
167
+ Tamanho aproximado em caracteres
168
+ """
169
+ import json
170
+ try:
171
+ json_str = json.dumps(data, ensure_ascii=False)
172
+ return len(json_str)
173
+ except:
174
+ return len(str(data))
175
+
176
+ def is_payload_safe(self, data: Dict[str, Any]) -> bool:
177
+ """
178
+ Verifica se payload está dentro dos limites seguros
179
+
180
+ Args:
181
+ data: Dados a verificar
182
+
183
+ Returns:
184
+ True se seguro, False se muito grande
185
+ """
186
+ size = self.estimate_payload_size(data)
187
+
188
+ if size > self.MAX_TOTAL_CHARS:
189
+ logger.warning(f"⚠️ Payload muito grande: {size} chars (máx: {self.MAX_TOTAL_CHARS})")
190
+ return False
191
+
192
+ return True
193
+
194
+
195
+ # Singleton global
196
+ _payload_manager = None
197
+
198
+ def get_payload_manager() -> PayloadManager:
199
+ """Retorna instância singleton do PayloadManager"""
200
+ global _payload_manager
201
+ if _payload_manager is None:
202
+ _payload_manager = PayloadManager()
203
+ return _payload_manager
{llm → old_files/llm}/schema_loader.py RENAMED
File without changes
{processors → old_files/processors}/processor_auditoria.py RENAMED
File without changes
{processors → old_files/processors}/processor_transcricao.py RENAMED
File without changes
processors/processor_base.py CHANGED
@@ -1,138 +1,58 @@
 
1
  """
2
- Classe Base Refatorada - Com Schema Injection automático
3
- Compatível com LLMManager do PARA.AI
4
  """
5
- from abc import ABC
6
- from typing import Dict, Any, Optional
7
- from datetime import datetime
8
- import logging
9
  import json
10
- import os
11
- from pathlib import Path
12
-
13
- from processors.config_loader import get_config_loader, SpecialistConfig
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
 
18
  class ProcessorBase(ABC):
19
  """
20
- Classe abstrata base para processadores
21
 
22
- FEATURES:
23
- - Carrega configuração do YAML
24
- - Schema injection automático no system_prompt
25
- - Compatível com LLMManager (await llm_manager.generate())
26
  """
27
 
28
- def __init__(
29
- self,
30
- specialist_id: int,
31
- llm_manager=None
32
- ):
33
  """
34
  Args:
35
- specialist_id: ID do especialista (1-9)
36
- llm_manager: LLMManager para chamadas LLM
37
  """
38
- self.specialist_id = specialist_id
39
  self.llm_manager = llm_manager
 
 
40
 
41
- # Carregar configuração do YAML
42
- config_loader = get_config_loader()
43
- self.config: Optional[SpecialistConfig] = config_loader.get_specialist(specialist_id)
44
-
45
- if not self.config:
46
- raise ValueError(f"Especialista {specialist_id} não encontrado na configuração")
47
-
48
- self.specialist_name = self.config.name
49
- self.execution_time = 0
50
- self.confidence_score = 0
51
- self.errors = []
52
- self.warnings = []
53
-
54
- # 🔥 NOVA FEATURE: Carrega schema JSON e injeta no system_prompt
55
- self._inject_schema_in_prompt()
56
-
57
- logger.info(
58
- f"✅ {self.specialist_name} inicializado "
59
- f"(provider={self.config.llm_config.provider}, "
60
- f"model={self.config.llm_config.model})"
61
- )
62
-
63
- def _inject_schema_in_prompt(self):
64
- """
65
- 🔥 NOVA FUNÇÃO: Injeta schema JSON no system_prompt.
66
 
67
- Carrega o schema do arquivo JSON correspondente e adiciona
68
- ao final do system_prompt para garantir que o LLM retorne
69
- JSON na estrutura correta.
70
- """
71
  try:
72
- # Mapeamento de especialista para arquivo schema
73
- schema_map = {
74
- 1: "metadados_schema.json",
75
- 2: "segmentacao_schema.json",
76
- 3: "transcricao_schema.json",
77
- 4: "contexto_schema.json",
78
- 5: "fundamentacao_schema.json",
79
- 6: "decisao_schema.json",
80
- 7: "arquivo_schema.json",
81
- 8: "relatorio_schema.json",
82
- 9: "auditoria_schema.json",
83
- }
84
-
85
- schema_filename = schema_map.get(self.specialist_id)
86
- if not schema_filename:
87
- logger.warning(f"⚠️ Schema não mapeado para especialista {self.specialist_id}")
88
- return
89
-
90
- # Localiza diretório de schemas
91
- base_dir = Path(__file__).parent.parent
92
- schema_path = base_dir / "prompts" / "schemas" / schema_filename
93
-
94
- if not schema_path.exists():
95
- logger.warning(f"⚠️ Schema não encontrado: {schema_path}")
96
- return
97
-
98
- # Carrega schema JSON
99
- with open(schema_path, 'r', encoding='utf-8') as f:
100
- schema_dict = json.load(f)
101
-
102
- # Formata schema para injeção
103
- schema_json = json.dumps(schema_dict, indent=2, ensure_ascii=False)
104
-
105
- # Injeta no system_prompt
106
- schema_instruction = f"""
107
-
108
- # JSON SCHEMA OBRIGATÓRIO
109
-
110
- Você DEVE retornar sua resposta seguindo EXATAMENTE este JSON Schema:
111
-
112
- ```json
113
- {schema_json}
114
- ```
115
-
116
- REGRAS CRÍTICAS:
117
- - Retorne APENAS JSON válido (sem comentários, sem texto adicional)
118
- - Siga TODOS os campos marcados como "required" no schema
119
- - Respeite os tipos de dados (string, integer, array, object, boolean)
120
- - Valide enums quando especificados
121
- - NÃO invente campos que não estão no schema
122
- - Use formato ISO 8601 para datas (YYYY-MM-DD)
123
- """
124
-
125
- # Adiciona schema ao system_prompt
126
- self.config.system_prompt = self.config.system_prompt + schema_instruction
127
-
128
- logger.info(f"✅ Schema {schema_filename} injetado em {self.specialist_name}")
129
-
130
  except Exception as e:
131
- logger.warning(f"⚠️ Falha ao injetar schema: {e}")
 
132
 
 
133
  async def process(self, acordao_data: Dict[str, Any]) -> Dict[str, Any]:
134
  """
135
- Processa acórdão usando LLM REAL via LLMManager.
136
 
137
  Args:
138
  acordao_data: Dados do acórdão
@@ -140,195 +60,159 @@ REGRAS CRÍTICAS:
140
  Returns:
141
  Resultado do processamento
142
  """
143
- if not self.llm_manager:
144
- raise ValueError("❌ LLMManager não configurado")
145
 
146
- start_time = datetime.now()
 
 
147
 
148
- try:
149
- # 1. Formatar prompt do usuário com dados do acórdão
150
- user_prompt = self.config.format_user_prompt(
151
- tribunal=acordao_data.get('tribunal', 'N/A'),
152
- numero_processo=acordao_data.get('numero_processo', 'N/A'),
153
- ementa=acordao_data.get('ementa', ''),
154
- integra=acordao_data.get('integra', ''),
155
- classe_processual=acordao_data.get('classe_processual', 'N/A')
156
- )
157
 
158
- # 2. Chamar LLM Manager (system_prompt já tem schema injetado)
159
- response = await self.llm_manager.generate(
160
- provider=self.config.llm_config.provider,
161
- model=self.config.llm_config.model,
162
- system_prompt=self.config.system_prompt, # 🔥 Com schema injetado
163
- user_prompt=user_prompt,
164
- temperature=self.config.llm_config.temperature,
165
- max_tokens=self.config.llm_config.max_tokens
166
- )
167
 
168
- # 3. Processar resposta do LLMManager
169
- if response['status'] == 'success':
170
- # response['result'] contém o dict do client: {'content': ..., 'tokens_input': ...}
171
- llm_result = response['result']
 
172
 
173
- # Extrair content e tokens
174
- content = llm_result.get('content', '{}')
175
- tokens_total = llm_result.get('total_tokens', 0)
176
 
177
- # Parse JSON se for string
178
- if isinstance(content, str):
179
- try:
180
- result = json.loads(content)
181
- except json.JSONDecodeError as e:
182
- logger.error(f"❌ JSON inválido: {e}")
183
- logger.debug(f"Content recebido: {content[:500]}")
184
- raise ValueError(f"Resposta não é JSON válido: {e}")
185
- else:
186
- result = content
187
 
188
- # Validar
189
- if not self.validate(result):
190
- self.add_warning("Resultado não passou na validação completa")
191
 
192
- # Calcular confiança
193
- self.confidence_score = self._calculate_confidence(result)
 
 
194
 
195
- else:
196
- # Erro na chamada LLM
197
- error_msg = response.get('error', 'Erro desconhecido')
198
- raise ValueError(f"Erro na chamada LLM: {error_msg}")
199
 
200
- # 4. Pós-processar
201
- self.execution_time = (datetime.now() - start_time).total_seconds()
202
 
203
- return self.post_process(result)
204
 
205
- except Exception as e:
206
- self.add_error(f"Erro no processamento: {e}")
207
- self.execution_time = (datetime.now() - start_time).total_seconds()
208
-
209
- return {
210
- "specialist_id": self.specialist_id,
211
- "specialist_name": self.specialist_name,
212
- "status": "error",
213
- "error": str(e),
214
- "execution_time": self.execution_time,
215
- "timestamp": datetime.now().isoformat()
216
- }
217
-
218
- def validate(self, result: Dict[str, Any]) -> bool:
219
  """
220
- Valida resultado contra schema.
 
 
221
 
222
  Args:
223
- result: Resultado a validar
224
 
225
  Returns:
226
- True se válido
227
  """
228
- try:
229
- # Validação básica: verificar campos required do schema
230
- schema = self.config.schema
231
-
232
- # Verifica se schema existe e tem required fields
233
- if not isinstance(schema, dict):
234
- logger.warning(f"⚠️ Schema não é um dict para {self.specialist_name}")
235
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
- required_fields = schema.get('required', [])
238
- if not required_fields:
239
- logger.debug(f"✓ Schema sem campos required para {self.specialist_name}")
240
- return True
241
 
242
- # Verifica se result é dict
243
- if not isinstance(result, dict):
244
- self.add_warning(f"Resultado não é um dict: {type(result)}")
245
- return False
 
246
 
247
- # Verifica campos required
248
- missing_fields = []
249
- for field in required_fields:
250
- if field not in result:
251
- missing_fields.append(field)
 
 
 
 
 
 
 
 
 
252
 
253
- if missing_fields:
254
- self.add_warning(f"Campos obrigatórios ausentes: {', '.join(missing_fields)}")
255
- return False
256
 
257
- return True
 
 
 
 
 
 
 
258
 
259
  except Exception as e:
260
- self.add_error(f"Erro na validação: {e}")
261
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
- def _calculate_confidence(self, result: Dict[str, Any]) -> int:
264
  """
265
- Calcula score de confiança baseado no resultado.
 
 
 
 
266
 
267
  Returns:
268
- Score 0-100
269
  """
270
- try:
271
- # Lógica simples: quanto mais campos preenchidos, maior confiança
272
- schema = self.config.schema
273
-
274
- if not isinstance(schema, dict):
275
- return 85 # Padrão se schema inválido
276
-
277
- required_fields = schema.get('required', [])
278
-
279
- if not required_fields:
280
- return 85 # Padrão
281
-
282
- if not isinstance(result, dict):
283
- return 0
284
-
285
- filled = sum(1 for field in required_fields if result.get(field))
286
- confidence = int((filled / len(required_fields)) * 100)
287
-
288
- return min(confidence, 100)
289
-
290
- except Exception:
291
- return 85
292
-
293
- def post_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
294
- """Pós-processa resultado"""
295
- return {
296
- "specialist_id": self.specialist_id,
297
- "specialist_name": self.specialist_name,
298
- "result": result,
299
- "execution_time": self.execution_time,
300
- "confidence_score": self.confidence_score,
301
- "errors": self.errors,
302
- "warnings": self.warnings,
303
- "timestamp": datetime.now().isoformat(),
304
- "config": {
305
- "provider": self.config.llm_config.provider,
306
- "model": self.config.llm_config.model,
307
- "temperature": self.config.llm_config.temperature
308
- }
309
- }
310
-
311
- def add_error(self, error_msg: str):
312
- """Adiciona erro"""
313
- self.errors.append(error_msg)
314
- logger.error(f"[{self.specialist_name}] {error_msg}")
315
-
316
- def add_warning(self, warning_msg: str):
317
- """Adiciona aviso"""
318
- self.warnings.append(warning_msg)
319
- logger.warning(f"[{self.specialist_name}] {warning_msg}")
320
-
321
- def get_schema(self) -> Dict[str, Any]:
322
- """Retorna schema do especialista"""
323
- return self.config.schema
324
-
325
- @property
326
- def info(self) -> Dict[str, Any]:
327
- """Retorna informações sobre o processador"""
328
- return {
329
- "specialist_id": self.specialist_id,
330
- "specialist_name": self.specialist_name,
331
- "provider": self.config.llm_config.provider,
332
- "model": self.config.llm_config.model,
333
- "schema": self.config.schema
334
- }
 
1
+ ##PARA.AI/processors/processor_base.py
2
  """
3
+ ProcessorBase - Classe base para todos os processadores V13.1
4
+ CORRIGIDO: Adiciona truncamento de texto para evitar erro 413
5
  """
 
 
 
 
6
  import json
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from typing import Dict, Any, Optional, List
 
10
 
11
  logger = logging.getLogger(__name__)
12
 
13
 
14
  class ProcessorBase(ABC):
15
  """
16
+ Classe base para processadores V13.1
17
 
18
+ CORREÇÃO V13.6:
19
+ - Adiciona método truncate_text() para evitar erro 413
20
+ - Limita seções a 15000 chars cada
21
+ - Mantém compatibilidade total com código existente
22
  """
23
 
24
+ # NOVO: Limites de caracteres para evitar erro 413
25
+ MAX_SECTION_CHARS = 15000 # Máximo por seção de texto
26
+ MAX_TOTAL_CHARS = 50000 # Máximo total do payload
27
+
28
+ def __init__(self, llm_manager, config_path: str = None):
29
  """
30
  Args:
31
+ llm_manager: Instância do LLMManager
32
+ config_path: Caminho para arquivo de configuração (opcional)
33
  """
 
34
  self.llm_manager = llm_manager
35
+ self.config_path = config_path
36
+ self.config = self._load_config() if config_path else {}
37
 
38
+ logger.info(f"✅ {self.__class__.__name__} inicializado")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ def _load_config(self) -> Dict[str, Any]:
41
+ """Carrega configuração do processador"""
 
 
42
  try:
43
+ if self.config_path:
44
+ from processors.config_loader import ConfigLoader
45
+ loader = ConfigLoader(self.config_path)
46
+ return loader.get_config(self.__class__.__name__)
47
+ return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  except Exception as e:
49
+ logger.warning(f"⚠️ Erro ao carregar config: {e}")
50
+ return {}
51
 
52
+ @abstractmethod
53
  async def process(self, acordao_data: Dict[str, Any]) -> Dict[str, Any]:
54
  """
55
+ Processa um acórdão
56
 
57
  Args:
58
  acordao_data: Dados do acórdão
 
60
  Returns:
61
  Resultado do processamento
62
  """
63
+ pass
 
64
 
65
+ # =======================================================================
66
+ # NOVOS MÉTODOS - Correção erro 413
67
+ # =======================================================================
68
 
69
+ def truncate_text(self, text: str, max_chars: int = None) -> str:
70
+ """
71
+ Trunca texto para evitar erro 413 Payload Too Large
 
 
 
 
 
 
72
 
73
+ Args:
74
+ text: Texto a truncar
75
+ max_chars: Máximo de caracteres (padrão: MAX_SECTION_CHARS)
 
 
 
 
 
 
76
 
77
+ Returns:
78
+ Texto truncado
79
+ """
80
+ if not text:
81
+ return ""
82
 
83
+ max_chars = max_chars or self.MAX_SECTION_CHARS
 
 
84
 
85
+ if len(text) <= max_chars:
86
+ return text
 
 
 
 
 
 
 
 
87
 
88
+ # Truncar mantendo palavras completas
89
+ truncated = text[:max_chars]
 
90
 
91
+ # Encontrar último espaço para não cortar palavra
92
+ last_space = truncated.rfind(' ')
93
+ if last_space > max_chars * 0.9: # Se estiver próximo do fim
94
+ truncated = truncated[:last_space]
95
 
96
+ # Adicionar indicador de truncamento
97
+ truncated += "... [TEXTO TRUNCADO]"
 
 
98
 
99
+ logger.debug(f"✂️ Texto truncado: {len(text)} → {len(truncated)} chars")
 
100
 
101
+ return truncated
102
 
103
+ def prepare_sections_for_llm(self, acordao_data: Dict[str, Any]) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  """
105
+ Prepara seções truncadas para envio ao LLM
106
+
107
+ CORREÇÃO: Trunca cada seção individualmente para evitar erro 413
108
 
109
  Args:
110
+ acordao_data: Dados completos do acórdão
111
 
112
  Returns:
113
+ Dados com seções truncadas
114
  """
115
+ prepared_data = acordao_data.copy()
116
+
117
+ # Campos de texto que devem ser truncados
118
+ text_fields = [
119
+ 'inteiro_teor',
120
+ 'ementa',
121
+ 'RELATORIO_texto_completo',
122
+ 'FUNDAMENTACAO_texto_completo',
123
+ 'DISPOSITIVO_texto_completo',
124
+ ]
125
+
126
+ for field in text_fields:
127
+ # Verificar em nível raiz
128
+ if field in prepared_data and isinstance(prepared_data[field], str):
129
+ prepared_data[field] = self.truncate_text(prepared_data[field])
130
+
131
+ # Verificar em secoes_originais
132
+ if 'secoes_originais' in prepared_data:
133
+ if field in prepared_data['secoes_originais']:
134
+ if isinstance(prepared_data['secoes_originais'][field], str):
135
+ prepared_data['secoes_originais'][field] = self.truncate_text(
136
+ prepared_data['secoes_originais'][field]
137
+ )
138
+
139
+ return prepared_data
140
+
141
+ async def call_llm(
142
+ self,
143
+ prompt: str,
144
+ system_prompt: str = None,
145
+ acordao_data: Dict[str, Any] = None,
146
+ **kwargs
147
+ ) -> Optional[Dict[str, Any]]:
148
+ """
149
+ Chama LLM com truncamento automático de texto
150
 
151
+ CORREÇÃO: Trunca automaticamente antes de enviar
 
 
 
152
 
153
+ Args:
154
+ prompt: Prompt do usuário
155
+ system_prompt: Prompt do sistema (opcional)
156
+ acordao_data: Dados do acórdão (serão truncados automaticamente)
157
+ **kwargs: Argumentos adicionais
158
 
159
+ Returns:
160
+ Resposta parseada do LLM ou None
161
+ """
162
+ try:
163
+ # NOVO: Truncar dados antes de enviar
164
+ if acordao_data:
165
+ acordao_data = self.prepare_sections_for_llm(acordao_data)
166
+
167
+ # Chamar LLM normalmente
168
+ response = await self.llm_manager.chat(
169
+ prompt=prompt,
170
+ system_prompt=system_prompt,
171
+ **kwargs
172
+ )
173
 
174
+ if not response:
175
+ logger.warning(f"⚠️ LLM retornou resposta vazia")
176
+ return None
177
 
178
+ # Tentar parsear JSON
179
+ try:
180
+ result = json.loads(response)
181
+ return result
182
+ except json.JSONDecodeError as e:
183
+ logger.error(f"❌ Erro ao parsear resposta JSON: {e}")
184
+ logger.debug(f"Resposta: {response[:500]}...")
185
+ return None
186
 
187
  except Exception as e:
188
+ logger.error(f"Erro ao chamar LLM: {e}")
189
+ return None
190
+
191
+ def get_empty_structure(self) -> Dict[str, Any]:
192
+ """
193
+ Retorna estrutura vazia padrão
194
+ Pode ser sobrescrito por processadores específicos
195
+ """
196
+ return {}
197
+
198
+ def validate_result(self, result: Dict[str, Any]) -> bool:
199
+ """
200
+ Valida resultado básico
201
+ Pode ser sobrescrito por processadores específicos
202
+ """
203
+ return result is not None and isinstance(result, dict)
204
 
205
+ def merge_results(self, base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
206
  """
207
+ Mescla dois dicionários de resultados
208
+
209
+ Args:
210
+ base: Dicionário base
211
+ update: Dicionário com atualizações
212
 
213
  Returns:
214
+ Dicionário mesclado
215
  """
216
+ merged = base.copy()
217
+ merged.update(update)
218
+ return merged