Carlex22 commited on
Commit
a7f77a7
·
1 Parent(s): 1f24745

Revert "ParaAIV3.1"

Browse files

This reverts commit bc33fdf0aa1d0fe36eb6b469cc18e729fe53198f.

.backups/pre_fix_413/context_builder.py DELETED
@@ -1,79 +0,0 @@
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 DELETED
@@ -1,334 +0,0 @@
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 DELETED
@@ -1,79 +0,0 @@
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 DELETED
@@ -1,334 +0,0 @@
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 DELETED
@@ -1,79 +0,0 @@
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 DELETED
@@ -1,334 +0,0 @@
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 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,53 +22,15 @@ 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,20 +62,10 @@ class ContextBuilder:
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
 
 
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
  """
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
  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
 
core/payload_manager.py DELETED
@@ -1,203 +0,0 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
{old_files/llm → llm}/schema_loader.py RENAMED
File without changes
{old_files/processors → processors}/processor_auditoria.py RENAMED
File without changes
processors/processor_base.py CHANGED
@@ -1,58 +1,138 @@
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,159 +140,195 @@ class ProcessorBase(ABC):
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  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
+ }
{old_files/processors → processors}/processor_transcricao.py RENAMED
File without changes