Carlex22 commited on
Commit
f1e8dc8
·
1 Parent(s): 1ef5356

ParaAIV3.1

Browse files
config/pipeline_config.yaml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/config/pipeline_config.yaml
2
+ # =============================================================================
3
+ # Pipeline Configuration V13.6 - Orquestração em Fases com Dependências
4
+ # =============================================================================
5
+
6
+ pipeline:
7
+ name: "Para.AI V13.6 Pipeline"
8
+ version: "v13.6"
9
+ description: "Pipeline de processamento de acórdãos TJPR em 6 fases"
10
+
11
+ # Fases sequenciais com dependências explícitas
12
+ phases:
13
+ - id: 1
14
+ name: "Segmentação"
15
+ description: "Segmenta inteiro teor em RELATÓRIO + FUNDAMENTAÇÃO + DISPOSITIVO"
16
+ parallel: false
17
+ specialists: [1]
18
+ depends_on: []
19
+
20
+ - id: 2
21
+ name: "Metadados"
22
+ description: "Extrai metadados estruturais do acórdão"
23
+ parallel: false
24
+ specialists: [2]
25
+ depends_on: [1]
26
+
27
+ - id: 3
28
+ name: "Classificação"
29
+ description: "Classifica ramo do direito e assuntos"
30
+ parallel: false
31
+ specialists: [3]
32
+ depends_on: [2]
33
+
34
+ - id: 4
35
+ name: "Tripartite"
36
+ description: "Extrai RELATÓRIO + FUNDAMENTAÇÃO + DECISÃO em paralelo"
37
+ parallel: true
38
+ specialists: [4, 5, 6]
39
+ depends_on: [1, 2, 3]
40
+
41
+ - id: 5
42
+ name: "Arquivista"
43
+ description: "Análise meta-cognitiva e avaliação de qualidade"
44
+ parallel: false
45
+ specialists: [7]
46
+ depends_on: [4]
47
+
48
+ - id: 6
49
+ name: "Validação"
50
+ description: "Validação jsonschema e cálculo de completude"
51
+ parallel: false
52
+ specialists: []
53
+ depends_on: [5]
54
+
55
+ # Configuração Global
56
+ global_config:
57
+ max_workers: 3
58
+ default_timeout: 120
59
+ batch_size: 50
60
+ enable_retry: true
61
+ max_retries: 3
62
+ retry_delay: 2
core/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/__init__.py
2
+ """
3
+ Core Module - Componentes principais da pipeline V13.6
4
+ """
5
+
6
+ from .orchestrator import PipelineOrchestrator
7
+ from .base_specialist import BaseSpecialist
8
+ from .context_builder import ContextBuilder
9
+ from .validator import SchemaValidator
10
+
11
+ __all__ = [
12
+ 'PipelineOrchestrator',
13
+ 'BaseSpecialist',
14
+ 'ContextBuilder',
15
+ 'SchemaValidator'
16
+ ]
17
+
18
+ __version__ = "13.6.0"
core/base_specialist.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/base_specialist.py
2
+ """
3
+ Base Specialist - Classe base refatorada para todos os especialistas V13.6
4
+ MUDANÇAS DO V13.1:
5
+ - Context injection obrigatório
6
+ - Validação com jsonschema
7
+ - Prompts ultra-simples (<1000 chars)
8
+ - Retry logic automático
9
+ """
10
+ import logging
11
+ import json
12
+ import yaml
13
+ from typing import Dict, Any, Optional
14
+ from datetime import datetime
15
+ from abc import ABC, abstractmethod
16
+
17
+ from api.utils.logger import setup_logger
18
+
19
+ logger = setup_logger(__name__)
20
+
21
+
22
+ class BaseSpecialist(ABC):
23
+ """
24
+ Classe base para todos os especialistas V13.6
25
+
26
+ REGRAS DE OURO:
27
+ 1. NUNCA pedir mais de 10 campos por especialista
28
+ 2. SEMPRE exibir JSON parcial no prompt (context injection)
29
+ 3. SEMPRE validar output com jsonschema
30
+ 4. SEMPRE logar input + output (debug)
31
+ 5. NUNCA usar placeholders ou dados simulados
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ specialist_id: int,
37
+ config_path: str,
38
+ llm_manager,
39
+ schema_validator=None
40
+ ):
41
+ """
42
+ Args:
43
+ specialist_id: ID único do especialista (1-9)
44
+ config_path: Caminho para specialists_v13_6.yaml
45
+ llm_manager: Instância do LLMManager
46
+ schema_validator: Validador de schema (opcional)
47
+ """
48
+ self.id = specialist_id
49
+ self.llm_manager = llm_manager
50
+ self.schema_validator = schema_validator
51
+
52
+ # Carregar configuração do especialista
53
+ self.config = self._load_config(specialist_id, config_path)
54
+ self.name = self.config.get('name', f'Specialist_{specialist_id}')
55
+
56
+ # Carregar prompt template
57
+ self.prompt_template = self._load_prompt_template()
58
+
59
+ # Configuração LLM
60
+ self.llm_config = self.config.get('llm_config', {})
61
+
62
+ # Retry logic
63
+ self.max_retries = self.config.get('max_retries', 3)
64
+ self.retry_delay = self.config.get('retry_delay', 2)
65
+
66
+ logger.info(f"✅ {self.name} (ID {self.id}) inicializado")
67
+
68
+ def _load_config(self, specialist_id: int, config_path: str) -> Dict[str, Any]:
69
+ """Carrega configuração do especialista do YAML"""
70
+ try:
71
+ with open(config_path, 'r', encoding='utf-8') as f:
72
+ full_config = yaml.safe_load(f)
73
+
74
+ specialist_config = full_config.get('specialists', {}).get(specialist_id, {})
75
+
76
+ if not specialist_config:
77
+ raise ValueError(f"Configuração não encontrada para especialista {specialist_id}")
78
+
79
+ return specialist_config
80
+
81
+ except Exception as e:
82
+ logger.error(f"❌ Erro ao carregar config: {e}")
83
+ raise
84
+
85
+ def _load_prompt_template(self) -> str:
86
+ """Carrega template de prompt do arquivo .txt"""
87
+ prompt_file = self.config.get('prompt_file', '')
88
+
89
+ if not prompt_file:
90
+ logger.warning(f"⚠️ Prompt file não configurado para {self.name}")
91
+ return ""
92
+
93
+ try:
94
+ with open(prompt_file, 'r', encoding='utf-8') as f:
95
+ return f.read()
96
+ except Exception as e:
97
+ logger.error(f"❌ Erro ao carregar prompt: {e}")
98
+ return ""
99
+
100
+ async def process(
101
+ self,
102
+ input_data: Dict[str, Any],
103
+ context: Optional[Dict[str, Any]] = None
104
+ ) -> Dict[str, Any]:
105
+ """
106
+ Processa input com context injection e retry logic
107
+
108
+ Args:
109
+ input_data: Dados brutos do acórdão
110
+ context: JSON parcial já preenchido por especialistas anteriores
111
+
112
+ Returns:
113
+ Resultado parcial (apenas os campos deste especialista)
114
+ """
115
+ logger.info(f"🤖 {self.name} processando...")
116
+
117
+ for attempt in range(self.max_retries):
118
+ try:
119
+ # 1. Construir prompt com context injection
120
+ prompt = self._build_prompt(input_data, context)
121
+
122
+ # 2. Log do prompt (debug)
123
+ logger.debug(f" 📝 Prompt ({len(prompt)} chars): {prompt[:200]}...")
124
+
125
+ # 3. Chamar LLM
126
+ response = await self._call_llm(prompt)
127
+
128
+ # 4. Parse JSON
129
+ result = self._parse_response(response)
130
+
131
+ # 5. Validar schema (se configurado)
132
+ if self.schema_validator:
133
+ is_valid, errors = self._validate_output(result)
134
+ if not is_valid:
135
+ logger.warning(f" ⚠️ Validação falhou: {errors[:2]}")
136
+ if attempt < self.max_retries - 1:
137
+ continue # Retry
138
+
139
+ # 6. Log de sucesso
140
+ logger.info(f" ✅ {self.name} completou: {len(result)} campos")
141
+
142
+ return result
143
+
144
+ except Exception as e:
145
+ logger.error(f" ❌ Tentativa {attempt + 1}/{self.max_retries} falhou: {e}")
146
+
147
+ if attempt == self.max_retries - 1:
148
+ # Última tentativa falhou - retornar estrutura vazia
149
+ logger.error(f" ❌ {self.name} FALHOU após {self.max_retries} tentativas")
150
+ return self._get_empty_structure()
151
+
152
+ return self._get_empty_structure()
153
+
154
+ def _build_prompt(
155
+ self,
156
+ input_data: Dict[str, Any],
157
+ context: Optional[Dict[str, Any]] = None
158
+ ) -> str:
159
+ """
160
+ Constrói prompt final com substituição de variáveis e context injection
161
+
162
+ NOVIDADE V13.6:
163
+ - Exibe JSON parcial já preenchido
164
+ - LLM entende que deve "completar", não "gerar do zero"
165
+ """
166
+ prompt = self.prompt_template
167
+
168
+ # Substituir variáveis básicas
169
+ prompt = prompt.replace('{ementa}', input_data.get('ementa', ''))
170
+ prompt = prompt.replace('{inteiro_teor}', input_data.get('inteiro_teor', '')[:10000]) # Limitar
171
+
172
+ # Substituir seções específicas (se disponíveis no context)
173
+ if context:
174
+ secoes = context.get('secoes_originais', {})
175
+ prompt = prompt.replace(
176
+ '{RELATORIO_texto}',
177
+ secoes.get('RELATORIO_texto_completo', '')[:5000]
178
+ )
179
+ prompt = prompt.replace(
180
+ '{FUNDAMENTACAO_texto}',
181
+ secoes.get('FUNDAMENTACAO_texto_completo', '')[:5000]
182
+ )
183
+ prompt = prompt.replace(
184
+ '{DISPOSITIVO_texto}',
185
+ secoes.get('DISPOSITIVO_texto_completo', '')[:3000]
186
+ )
187
+
188
+ # Substituir metadados
189
+ if 'metadados' in context:
190
+ metadados_str = json.dumps(context['metadados'], indent=2, ensure_ascii=False)
191
+ prompt = prompt.replace('{metadados}', metadados_str[:1000])
192
+
193
+ # CONTEXT INJECTION: Exibir JSON parcial já preenchido
194
+ if context:
195
+ # Remover campos muito grandes para não explodir o prompt
196
+ context_compact = self._compact_context(context)
197
+
198
+ context_str = json.dumps(context_compact, indent=2, ensure_ascii=False)
199
+
200
+ prompt += f"""
201
+
202
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
203
+ JSON JÁ PREENCHIDO (contexto dos especialistas anteriores):
204
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
205
+
206
+ {context_str}
207
+
208
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
209
+ IMPORTANTE:
210
+ - NÃO repita os campos acima
211
+ - Complete APENAS os campos NOVOS da sua responsabilidade
212
+ - Retorne SOMENTE JSON válido
213
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
214
+ """
215
+
216
+ return prompt
217
+
218
+ def _compact_context(self, context: Dict[str, Any]) -> Dict[str, Any]:
219
+ """Remove campos muito grandes do contexto para não explodir o prompt"""
220
+ compact = {}
221
+
222
+ for key, value in context.items():
223
+ if key in ['secoes_originais']:
224
+ # Não incluir textos completos (já foram substituídos)
225
+ continue
226
+
227
+ if isinstance(value, str) and len(value) > 500:
228
+ # Truncar strings muito longas
229
+ compact[key] = value[:500] + "..."
230
+ else:
231
+ compact[key] = value
232
+
233
+ return compact
234
+
235
+ async def _call_llm(self, prompt: str) -> str:
236
+ """
237
+ Chama LLM via LLMManager
238
+
239
+ NOTA: Adaptar para API específica do llm_manager
240
+ """
241
+ try:
242
+ # Assumindo que llm_manager tem método generate()
243
+ response = await self.llm_manager.generate(
244
+ system_prompt=self.config.get('system_prompt', ''),
245
+ user_prompt=prompt,
246
+ temperature=self.llm_config.get('temperature', 0.3),
247
+ max_tokens=self.llm_config.get('max_tokens', 2000),
248
+ model=self.llm_config.get('model', 'groq/llama-3-70b')
249
+ )
250
+
251
+ return response
252
+
253
+ except Exception as e:
254
+ logger.error(f"❌ Erro ao chamar LLM: {e}")
255
+ raise
256
+
257
+ def _parse_response(self, response: str) -> Dict[str, Any]:
258
+ """Parse da resposta JSON do LLM"""
259
+ try:
260
+ # Tentar extrair JSON (pode vir com markdown)
261
+ response_clean = response.strip()
262
+
263
+ # Remover markdown code blocks se existir
264
+ if response_clean.startswith('```'):
265
+ lines = response_clean.split('
266
+ ')
267
+ response_clean = '
268
+ '.join(lines[1:-1]) # Remove primeira e última linha
269
+
270
+ result = json.loads(response_clean)
271
+ return result
272
+
273
+ except json.JSONDecodeError as e:
274
+ logger.error(f"❌ Erro ao parsear JSON: {e}")
275
+ logger.error(f"Response: {response[:500]}")
276
+ raise
277
+
278
+ def _validate_output(self, result: Dict[str, Any]) -> tuple[bool, list]:
279
+ """Valida output contra schema parcial do especialista"""
280
+ if not self.schema_validator:
281
+ return True, []
282
+
283
+ schema_file = self.config.get('schema_file', '')
284
+ if not schema_file:
285
+ return True, []
286
+
287
+ try:
288
+ return self.schema_validator.validate_partial(result, schema_file)
289
+ except Exception as e:
290
+ logger.error(f"❌ Erro na validação: {e}")
291
+ return False, [str(e)]
292
+
293
+ @abstractmethod
294
+ def _get_empty_structure(self) -> Dict[str, Any]:
295
+ """
296
+ Retorna estrutura vazia em caso de falha total
297
+ Deve ser implementado por cada especialista
298
+ """
299
+ pass
300
+
301
+ def get_info(self) -> Dict[str, Any]:
302
+ """Retorna informações sobre o especialista"""
303
+ return {
304
+ 'id': self.id,
305
+ 'name': self.name,
306
+ 'enabled': self.config.get('enabled', True),
307
+ 'llm_model': self.llm_config.get('model', 'unknown'),
308
+ 'max_retries': self.max_retries
309
+ }
core/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
core/orchestrator.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/orchestrator.py
2
+ """
3
+ Pipeline Orchestrator - Orquestrador principal V13.6
4
+ Substitui processor_manager.py com arquitetura em fases e dependências explícitas
5
+ """
6
+ import logging
7
+ import asyncio
8
+ from typing import Dict, Any, List, Optional
9
+ from datetime import datetime
10
+ import json
11
+
12
+ from core.context_builder import ContextBuilder
13
+ from core.validator import SchemaValidator
14
+ from api.utils.logger import setup_logger
15
+
16
+ logger = setup_logger(__name__)
17
+
18
+
19
+ class PipelineOrchestrator:
20
+ """
21
+ Orquestrador de pipeline em fases com dependências explícitas
22
+
23
+ MUDANÇAS DO V13.1:
24
+ - Fases sequenciais com dependências explícitas (DAG)
25
+ - Context injection: especialista N recebe output de N-1
26
+ - Validação jsonschema após cada fase
27
+ - Logs detalhados por fase/especialista
28
+ - Suporte a execução paralela controlada (FASE 4)
29
+ """
30
+
31
+ def __init__(self, config: Dict[str, Any], llm_manager, specialists: Dict[int, Any]):
32
+ """
33
+ Args:
34
+ config: Configuração da pipeline (de pipeline_config.yaml)
35
+ llm_manager: Instância do LLMManager
36
+ specialists: Dicionário {id: instância do especialista}
37
+ """
38
+ self.config = config
39
+ self.llm_manager = llm_manager
40
+ self.specialists = specialists
41
+ self.context_builder = ContextBuilder()
42
+ self.validator = SchemaValidator('schemas/protocolo_v13_6_schema.json')
43
+
44
+ # Organizar fases
45
+ self.phases = self._organize_phases()
46
+
47
+ logger.info(
48
+ f"✅ PipelineOrchestrator inicializado: "
49
+ f"{len(self.phases)} fases, {len(self.specialists)} especialistas"
50
+ )
51
+
52
+ def _organize_phases(self) -> List[Dict[str, Any]]:
53
+ """Organiza fases em ordem de execução respeitando dependências"""
54
+ phases = self.config['pipeline']['phases']
55
+ # Ordenar por id (já está em ordem correta no YAML)
56
+ return sorted(phases, key=lambda p: p['id'])
57
+
58
+ async def process_acordao(
59
+ self,
60
+ acordao_bruto: Dict[str, Any],
61
+ fase_inicial: int = 1,
62
+ fase_final: Optional[int] = None
63
+ ) -> Dict[str, Any]:
64
+ """
65
+ Processa acórdão através de todas as fases da pipeline
66
+
67
+ Args:
68
+ acordao_bruto: Dados brutos do acórdão (ementa, inteiro_teor, etc)
69
+ fase_inicial: Fase inicial (default: 1)
70
+ fase_final: Fase final (default: None = todas)
71
+
72
+ Returns:
73
+ JSON completo conforme Protocolo V13.6
74
+ """
75
+ start_time = datetime.now()
76
+
77
+ # Inicializar resultado acumulado
78
+ resultado = {
79
+ "protocolo_versao": "v13.6",
80
+ "id_manifestacao": acordao_bruto.get('id', 0),
81
+ "metadados_processamento": {
82
+ "protocolo_origem": "v13.6",
83
+ "data_processamento": start_time.isoformat(),
84
+ "versao_preprocessador": "v13.6.0",
85
+ "campos_enriquecidos": [],
86
+ "tempo_processamento_segundos": None
87
+ },
88
+ "campos_futuros": {
89
+ "relatorio_transcript_exato": None,
90
+ "fundamentacao_transcript_exato": None,
91
+ "dispositivo_transcript_exato": None,
92
+ "embeddings_metadata": None,
93
+ "tags_embedding_baldes": None
94
+ }
95
+ }
96
+
97
+ logger.info(f"🚀 Iniciando pipeline para acórdão ID {resultado['id_manifestacao']}")
98
+
99
+ # Executar fases
100
+ fase_final = fase_final or len(self.phases)
101
+ fases_para_executar = [p for p in self.phases if fase_inicial <= p['id'] <= fase_final]
102
+
103
+ for phase in fases_para_executar:
104
+ phase_id = phase['id']
105
+ phase_name = phase['name']
106
+ is_parallel = phase.get('parallel', False)
107
+
108
+ logger.info(f"📍 FASE {phase_id}: {phase_name} (parallel={is_parallel})")
109
+
110
+ try:
111
+ if is_parallel and len(phase.get('specialists', [])) > 1:
112
+ # Execução paralela
113
+ resultado = await self._run_phase_parallel(
114
+ phase=phase,
115
+ current_result=resultado,
116
+ input_data=acordao_bruto
117
+ )
118
+ else:
119
+ # Execução sequencial
120
+ resultado = await self._run_phase_sequential(
121
+ phase=phase,
122
+ current_result=resultado,
123
+ input_data=acordao_bruto
124
+ )
125
+
126
+ logger.info(f"✅ FASE {phase_id} concluída")
127
+
128
+ except Exception as e:
129
+ logger.error(f"❌ Erro na FASE {phase_id} ({phase_name}): {e}")
130
+ resultado['metadados_processamento']['alertas_qualidade'] = resultado['metadados_processamento'].get('alertas_qualidade', []) + [f"Erro na fase {phase_id}: {str(e)}"]
131
+ # Continuar para próximas fases (não travar tudo)
132
+
133
+ # Validação final (FASE 6)
134
+ if fase_final >= 6:
135
+ is_valid, errors = self.validator.validate(resultado)
136
+ if not is_valid:
137
+ logger.warning(f"⚠️ Validação final: {len(errors)} erros encontrados")
138
+ resultado['metadados_processamento']['alertas_validacao'] = errors[:5] # Max 5
139
+
140
+ # Calcular tempo total
141
+ end_time = datetime.now()
142
+ resultado['metadados_processamento']['tempo_processamento_segundos'] = (end_time - start_time).total_seconds()
143
+
144
+ logger.info(
145
+ f"✅ Pipeline completa: {resultado['metadados_processamento']['tempo_processamento_segundos']:.2f}s"
146
+ )
147
+
148
+ return resultado
149
+
150
+ async def _run_phase_sequential(
151
+ self,
152
+ phase: Dict[str, Any],
153
+ current_result: Dict[str, Any],
154
+ input_data: Dict[str, Any]
155
+ ) -> Dict[str, Any]:
156
+ """Executa fase sequencialmente (um especialista por vez)"""
157
+ specialist_ids = phase.get('specialists', [])
158
+
159
+ for spec_id in specialist_ids:
160
+ specialist = self.specialists.get(spec_id)
161
+ if not specialist:
162
+ logger.warning(f"⚠️ Especialista {spec_id} não encontrado")
163
+ continue
164
+
165
+ # Context injection: passar JSON parcial já preenchido
166
+ context = self.context_builder.build_context(
167
+ current_result=current_result,
168
+ specialist_id=spec_id
169
+ )
170
+
171
+ logger.info(f" 🤖 Executando Especialista {spec_id}: {specialist.__class__.__name__}")
172
+
173
+ # Executar especialista
174
+ try:
175
+ partial_result = await specialist.process(
176
+ input_data=input_data,
177
+ context=context
178
+ )
179
+
180
+ # Mesclar resultado parcial no resultado acumulado
181
+ current_result = self._merge_results(current_result, partial_result)
182
+
183
+ # Registrar campo enriquecido
184
+ campos_novos = list(partial_result.keys())
185
+ current_result['metadados_processamento']['campos_enriquecidos'].extend(campos_novos)
186
+
187
+ logger.info(f" ✅ Especialista {spec_id} completou: {len(campos_novos)} campos")
188
+
189
+ except Exception as e:
190
+ logger.error(f" ❌ Erro no Especialista {spec_id}: {e}")
191
+ raise
192
+
193
+ return current_result
194
+
195
+ async def _run_phase_parallel(
196
+ self,
197
+ phase: Dict[str, Any],
198
+ current_result: Dict[str, Any],
199
+ input_data: Dict[str, Any]
200
+ ) -> Dict[str, Any]:
201
+ """Executa fase em paralelo (múltiplos especialistas simultaneamente)"""
202
+ specialist_ids = phase.get('specialists', [])
203
+
204
+ # Preparar tarefas paralelas
205
+ tasks = []
206
+ for spec_id in specialist_ids:
207
+ specialist = self.specialists.get(spec_id)
208
+ if not specialist:
209
+ logger.warning(f"⚠️ Especialista {spec_id} não encontrado")
210
+ continue
211
+
212
+ # Context injection
213
+ context = self.context_builder.build_context(
214
+ current_result=current_result,
215
+ specialist_id=spec_id
216
+ )
217
+
218
+ logger.info(f" 🤖 Agendando Especialista {spec_id} (paralelo)")
219
+
220
+ # Criar tarefa async
221
+ task = specialist.process(input_data=input_data, context=context)
222
+ tasks.append((spec_id, task))
223
+
224
+ # Executar em paralelo
225
+ results = await asyncio.gather(*[t[1] for t in tasks], return_exceptions=True)
226
+
227
+ # Processar resultados
228
+ for (spec_id, _), result in zip(tasks, results):
229
+ if isinstance(result, Exception):
230
+ logger.error(f" ❌ Erro no Especialista {spec_id}: {result}")
231
+ continue
232
+
233
+ # Mesclar resultado
234
+ current_result = self._merge_results(current_result, result)
235
+ campos_novos = list(result.keys())
236
+ current_result['metadados_processamento']['campos_enriquecidos'].extend(campos_novos)
237
+
238
+ logger.info(f" ✅ Especialista {spec_id} completou: {len(campos_novos)} campos")
239
+
240
+ return current_result
241
+
242
+ def _merge_results(self, current: Dict[str, Any], partial: Dict[str, Any]) -> Dict[str, Any]:
243
+ """
244
+ Mescla resultado parcial no resultado acumulado
245
+
246
+ Estratégia:
247
+ - Campos de primeiro nível: sobrescrever
248
+ - Campos aninhados: deep merge
249
+ """
250
+ for key, value in partial.items():
251
+ if key in ['metadados_processamento', 'campos_futuros']:
252
+ # Não sobrescrever metadados de controle
253
+ continue
254
+
255
+ if isinstance(value, dict) and key in current and isinstance(current[key], dict):
256
+ # Deep merge
257
+ current[key].update(value)
258
+ else:
259
+ # Sobrescrever
260
+ current[key] = value
261
+
262
+ return current
263
+
264
+ def get_phase_info(self, phase_id: int) -> Optional[Dict[str, Any]]:
265
+ """Retorna informações sobre uma fase específica"""
266
+ for phase in self.phases:
267
+ if phase['id'] == phase_id:
268
+ return phase
269
+ return None
270
+
271
+ def get_pipeline_status(self) -> Dict[str, Any]:
272
+ """Retorna status atual da pipeline"""
273
+ return {
274
+ 'total_phases': len(self.phases),
275
+ 'total_specialists': len(self.specialists),
276
+ 'phases': [
277
+ {
278
+ 'id': p['id'],
279
+ 'name': p['name'],
280
+ 'parallel': p.get('parallel', False),
281
+ 'specialists_count': len(p.get('specialists', []))
282
+ }
283
+ for p in self.phases
284
+ ]
285
+ }
core/validator.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##PARA.AI/core/validator.py
2
+ """
3
+ Schema Validator - Validação com jsonschema V13.6
4
+ """
5
+ import logging
6
+ import json
7
+ from typing import Dict, Any, Tuple, List
8
+
9
+ try:
10
+ import jsonschema
11
+ from jsonschema import validate, ValidationError
12
+ HAS_JSONSCHEMA = True
13
+ except ImportError:
14
+ HAS_JSONSCHEMA = False
15
+ logging.warning("⚠️ jsonschema não instalado. Validação desabilitada.")
16
+
17
+ from api.utils.logger import setup_logger
18
+
19
+ logger = setup_logger(__name__)
20
+
21
+
22
+ class SchemaValidator:
23
+ """
24
+ Validador de schema JSON usando jsonschema
25
+
26
+ MUDANÇA DO V13.1:
27
+ - V13.1: Método validate() existia mas não fazia nada
28
+ - V13.6: Validação real com jsonschema
29
+ """
30
+
31
+ def __init__(self, schema_path: str):
32
+ """
33
+ Args:
34
+ schema_path: Caminho para protocolo_v13_6_schema.json
35
+ """
36
+ self.schema_path = schema_path
37
+ self.schema = self._load_schema(schema_path)
38
+ self.enabled = HAS_JSONSCHEMA
39
+
40
+ if not self.enabled:
41
+ logger.warning("⚠️ SchemaValidator inicializado SEM jsonschema (validação desabilitada)")
42
+ else:
43
+ logger.info(f"✅ SchemaValidator inicializado: {schema_path}")
44
+
45
+ def _load_schema(self, schema_path: str) -> Dict[str, Any]:
46
+ """Carrega schema JSON"""
47
+ try:
48
+ with open(schema_path, 'r', encoding='utf-8') as f:
49
+ return json.load(f)
50
+ except FileNotFoundError:
51
+ logger.error(f"❌ Schema não encontrado: {schema_path}")
52
+ return {}
53
+ except json.JSONDecodeError as e:
54
+ logger.error(f"❌ Erro ao parsear schema: {e}")
55
+ return {}
56
+
57
+ def validate(self, data: Dict[str, Any]) -> Tuple[bool, List[str]]:
58
+ """
59
+ Valida dados completos contra schema V13.6
60
+
61
+ Args:
62
+ data: JSON completo para validar
63
+
64
+ Returns:
65
+ (is_valid, errors)
66
+ """
67
+ if not self.enabled:
68
+ return True, []
69
+
70
+ if not self.schema:
71
+ logger.warning("⚠️ Schema vazio, validação ignorada")
72
+ return True, []
73
+
74
+ try:
75
+ validate(instance=data, schema=self.schema)
76
+ logger.info("✅ Validação completa: SUCESSO")
77
+ return True, []
78
+
79
+ except ValidationError as e:
80
+ error_msg = f"{e.message} (campo: {'.'.join(map(str, e.path))})"
81
+ logger.warning(f"⚠️ Validação falhou: {error_msg}")
82
+ return False, [error_msg]
83
+
84
+ except Exception as e:
85
+ logger.error(f"❌ Erro na validação: {e}")
86
+ return False, [str(e)]
87
+
88
+ def validate_partial(
89
+ self,
90
+ data: Dict[str, Any],
91
+ partial_schema_path: str
92
+ ) -> Tuple[bool, List[str]]:
93
+ """
94
+ Valida dados parciais contra schema de um especialista
95
+
96
+ Args:
97
+ data: JSON parcial (output de um especialista)
98
+ partial_schema_path: Caminho para schema parcial (ex: fase1_segmentacao.json)
99
+
100
+ Returns:
101
+ (is_valid, errors)
102
+ """
103
+ if not self.enabled:
104
+ return True, []
105
+
106
+ try:
107
+ with open(partial_schema_path, 'r', encoding='utf-8') as f:
108
+ partial_schema = json.load(f)
109
+
110
+ validate(instance=data, schema=partial_schema)
111
+ logger.debug(f"✅ Validação parcial ({partial_schema_path}): SUCESSO")
112
+ return True, []
113
+
114
+ except FileNotFoundError:
115
+ logger.warning(f"⚠️ Schema parcial não encontrado: {partial_schema_path}")
116
+ return True, [] # Ignorar se schema não existe
117
+
118
+ except ValidationError as e:
119
+ error_msg = f"{e.message} (campo: {'.'.join(map(str, e.path))})"
120
+ logger.warning(f"⚠️ Validação parcial falhou: {error_msg}")
121
+ return False, [error_msg]
122
+
123
+ except Exception as e:
124
+ logger.error(f"❌ Erro na validação parcial: {e}")
125
+ return False, [str(e)]
126
+
127
+ def validate_required_fields(self, data: Dict[str, Any]) -> Tuple[bool, List[str]]:
128
+ """
129
+ Valida apenas campos obrigatórios (verificação rápida)
130
+
131
+ Returns:
132
+ (is_valid, missing_fields)
133
+ """
134
+ if not self.schema:
135
+ return True, []
136
+
137
+ required_fields = self.schema.get('required', [])
138
+ missing = []
139
+
140
+ for field in required_fields:
141
+ if field not in data:
142
+ missing.append(field)
143
+
144
+ if missing:
145
+ logger.warning(f"⚠️ Campos obrigatórios faltando: {missing}")
146
+ return False, missing
147
+
148
+ return True, []
149
+
150
+ def get_schema_info(self) -> Dict[str, Any]:
151
+ """Retorna informações sobre o schema"""
152
+ if not self.schema:
153
+ return {}
154
+
155
+ return {
156
+ 'title': self.schema.get('title', 'Unknown'),
157
+ 'version': self.schema.get('description', '').split('DATA:')[-1].strip() if 'DATA:' in self.schema.get('description', '') else 'Unknown',
158
+ 'required_fields': self.schema.get('required', []),
159
+ 'total_properties': len(self.schema.get('properties', {}))
160
+ }
processors/processor_manager.py CHANGED
@@ -1,33 +1,35 @@
 
1
  """
2
- Processor Manager - Gerencia os 9 especialistas com batch e paralelo
 
3
  """
4
  import logging
5
- import asyncio
6
  from typing import Dict, Any, List, Optional
7
- from datetime import datetime
8
 
 
 
 
 
9
  from processors.processor_metadados import ProcessorMetadados
10
  from processors.processor_segmentacao import ProcessorSegmentacao
11
- from processors.processor_transcricao import ProcessorTranscricao
12
- from processors.processor_decisao import ProcessorDecisao
13
  from processors.processor_fundamentacao import ProcessorFundamentacao
14
- from processors.processor_contexto import ProcessorContexto
15
  from processors.processor_arquivo import ProcessorArquivo
16
- from processors.processor_relatorio import ProcessorRelatorio
17
- from processors.processor_auditoria import ProcessorAuditoria
18
 
19
  logger = logging.getLogger(__name__)
20
 
21
 
22
  class ProcessorManager:
23
  """
24
- Gerenciador dos 9 especialistas
25
-
26
- MUDANÇAS:
27
- - Todos os especialistas usam LLM real
28
- - Suporte a batch processing
29
- - Execução paralela otimizada
30
- - Sem simulações
31
  """
32
 
33
  def __init__(self, llm_manager, max_workers: int = 3):
@@ -39,22 +41,30 @@ class ProcessorManager:
39
  self.llm_manager = llm_manager
40
  self.max_workers = max_workers
41
 
42
- # Inicializar os 9 especialistas com LLM Manager
43
- self.processors = {
44
- 1: ProcessorMetadados(llm_manager),
45
- 2: ProcessorSegmentacao(llm_manager),
46
- 3: ProcessorTranscricao(llm_manager),
47
- 4: ProcessorDecisao(llm_manager),
48
- 5: ProcessorFundamentacao(llm_manager),
49
- 6: ProcessorContexto(llm_manager),
50
- 7: ProcessorArquivo(llm_manager),
51
- 8: ProcessorRelatorio(llm_manager),
52
- 9: ProcessorAuditoria(llm_manager)
 
 
53
  }
54
 
 
 
 
 
 
 
 
55
  logger.info(
56
- f"✅ ProcessorManager: 9 especialistas inicializados "
57
- f"(max_workers={max_workers})"
58
  )
59
 
60
  async def process_acordao_sequential(
@@ -63,188 +73,52 @@ class ProcessorManager:
63
  specialist_ids: Optional[List[int]] = None
64
  ) -> Dict[str, Any]:
65
  """
66
- Processa acórdão sequencialmente (um especialista por vez)
67
-
68
- Args:
69
- acordao_data: Dados do acórdão
70
- specialist_ids: IDs dos especialistas a executar (None = todos)
71
-
72
- Returns:
73
- Resultado consolidado
74
  """
75
- if specialist_ids is None:
76
- specialist_ids = list(self.processors.keys())
77
-
78
- results = []
79
- start_time = datetime.now()
80
-
81
- logger.info(
82
- f"🔄 Processamento SEQUENCIAL: {len(specialist_ids)} especialistas"
83
- )
84
-
85
- for spec_id in specialist_ids:
86
- processor = self.processors.get(spec_id)
87
- if processor:
88
- logger.info(f" ⚙️ Executando especialista {spec_id}: {processor.specialist_name}")
89
- try:
90
- result = await processor.process(acordao_data)
91
- results.append(result)
92
- logger.info(
93
- f" ✅ {processor.specialist_name} concluído em "
94
- f"{result.get('execution_time', 0):.2f}s"
95
- )
96
- except Exception as e:
97
- logger.error(f" ❌ Erro no especialista {spec_id}: {e}")
98
- results.append({
99
- "specialist_id": spec_id,
100
- "status": "error",
101
- "error": str(e)
102
- })
103
-
104
- execution_time = (datetime.now() - start_time).total_seconds()
105
-
106
- # Contar sucessos e falhas
107
- successful = sum(1 for r in results if r.get('status') != 'error')
108
- failed = len(results) - successful
109
-
110
- logger.info(
111
- f"✅ Processamento concluído: {successful} sucessos, "
112
- f"{failed} falhas em {execution_time:.2f}s"
113
  )
114
 
115
- return {
116
- "acordao_id": acordao_data.get('acordao_id', 'unknown'),
117
- "status": "success" if failed == 0 else "partial",
118
- "mode": "sequential",
119
- "specialists_results": results,
120
- "statistics": {
121
- "total": len(results),
122
- "successful": successful,
123
- "failed": failed
124
- },
125
- "execution_time": execution_time,
126
- "timestamp": datetime.now().isoformat()
127
- }
128
-
129
  async def process_acordao_parallel(
130
  self,
131
  acordao_data: Dict[str, Any],
132
  specialist_ids: Optional[List[int]] = None
133
  ) -> Dict[str, Any]:
134
  """
135
- Processa acórdão em paralelo (todos os especialistas ao mesmo tempo)
136
-
137
- Args:
138
- acordao_data: Dados do acórdão
139
- specialist_ids: IDs dos especialistas a executar (None = todos)
140
-
141
- Returns:
142
- Resultado consolidado
143
  """
144
- if specialist_ids is None:
145
- specialist_ids = list(self.processors.keys())
146
-
147
- start_time = datetime.now()
148
-
149
- logger.info(
150
- f"⚡ Processamento PARALELO: {len(specialist_ids)} especialistas"
151
- )
152
-
153
- # Criar tasks para execução paralela
154
- tasks = []
155
- for spec_id in specialist_ids:
156
- processor = self.processors.get(spec_id)
157
- if processor:
158
- logger.info(f" 📤 Agendando especialista {spec_id}: {processor.specialist_name}")
159
- tasks.append(processor.process(acordao_data))
160
-
161
- # Executar todas as tasks em paralelo
162
- results = await asyncio.gather(*tasks, return_exceptions=True)
163
-
164
- # Processar resultados e exceções
165
- processed_results = []
166
- for i, result in enumerate(results):
167
- if isinstance(result, Exception):
168
- logger.error(f" ❌ Exceção no especialista {specialist_ids[i]}: {result}")
169
- processed_results.append({
170
- "specialist_id": specialist_ids[i],
171
- "status": "error",
172
- "error": str(result)
173
- })
174
- else:
175
- processed_results.append(result)
176
-
177
- execution_time = (datetime.now() - start_time).total_seconds()
178
-
179
- # Contar sucessos e falhas
180
- successful = sum(1 for r in processed_results if r.get('status') != 'error')
181
- failed = len(processed_results) - successful
182
-
183
- logger.info(
184
- f"✅ Processamento paralelo concluído: {successful} sucessos, "
185
- f"{failed} falhas em {execution_time:.2f}s"
186
  )
187
 
188
- return {
189
- "acordao_id": acordao_data.get('acordao_id', 'unknown'),
190
- "status": "success" if failed == 0 else "partial",
191
- "mode": "parallel",
192
- "specialists_results": processed_results,
193
- "statistics": {
194
- "total": len(processed_results),
195
- "successful": successful,
196
- "failed": failed
197
- },
198
- "execution_time": execution_time,
199
- "timestamp": datetime.now().isoformat()
200
- }
201
-
202
- async def process_acordao_batch(
203
- self,
204
- acordao_data: Dict[str, Any],
205
- specialist_ids: Optional[List[int]] = None,
206
- batch_id: Optional[str] = None
207
- ) -> Dict[str, Any]:
208
- """
209
- Processa acórdão usando batch processing (via Groq Batch API)
210
-
211
- Args:
212
- acordao_data: Dados do acórdão
213
- specialist_ids: IDs dos especialistas a executar (None = todos)
214
- batch_id: ID do batch (gerado automaticamente se None)
215
-
216
- Returns:
217
- Resultado consolidado
218
- """
219
- if specialist_ids is None:
220
- specialist_ids = list(self.processors.keys())
221
-
222
- if batch_id is None:
223
- batch_id = f"batch_{acordao_data.get('acordao_id', 'unknown')}_{int(datetime.now().timestamp())}"
224
-
225
- start_time = datetime.now()
226
-
227
- logger.info(
228
- f"📦 Processamento BATCH: {len(specialist_ids)} especialistas "
229
- f"(batch_id={batch_id})"
230
- )
231
-
232
- # TODO: Implementar batch processing
233
- # Por enquanto, usar paralelo como fallback
234
- logger.warning("⚠️ Batch processing ainda não implementado, usando paralelo")
235
- return await self.process_acordao_parallel(acordao_data, specialist_ids)
236
-
237
  def get_processor(self, specialist_id: int):
238
- """Obtém processador específico"""
239
- return self.processors.get(specialist_id)
240
 
241
  def get_all_processors(self) -> Dict[int, Any]:
242
- """Retorna todos os processadores"""
243
- return self.processors
244
 
245
- def get_processors_info(self) -> List[Dict[str, Any]]:
246
- """Retorna informações sobre todos os processadores"""
247
- return [
248
- processor.info
249
- for processor in self.processors.values()
250
- ]
 
1
+ ##PARA.AI/processors/processor_manager.py
2
  """
3
+ Processor Manager - REFATORADO para usar PipelineOrchestrator V13.6
4
+ MUDANÇA: Este arquivo agora é apenas um wrapper para manter compatibilidade
5
  """
6
  import logging
7
+ import yaml
8
  from typing import Dict, Any, List, Optional
 
9
 
10
+ from core.orchestrator import PipelineOrchestrator
11
+ from core.validator import SchemaValidator
12
+
13
+ # Importar especialistas (mantidos do V13.1, serão refatorados na FASE 2)
14
  from processors.processor_metadados import ProcessorMetadados
15
  from processors.processor_segmentacao import ProcessorSegmentacao
16
+ from processors.processor_relatorio import ProcessorRelatorio
 
17
  from processors.processor_fundamentacao import ProcessorFundamentacao
18
+ from processors.processor_decisao import ProcessorDecisao
19
  from processors.processor_arquivo import ProcessorArquivo
20
+ from processors.processor_contexto import ProcessorContexto
 
21
 
22
  logger = logging.getLogger(__name__)
23
 
24
 
25
  class ProcessorManager:
26
  """
27
+ Gerenciador de processadores - WRAPPER para PipelineOrchestrator
28
+
29
+ MUDANÇA V13.6:
30
+ - Este arquivo agora delega para PipelineOrchestrator
31
+ - Mantém compatibilidade com código existente
32
+ - Especialistas serão migrados gradualmente para novo formato
 
33
  """
34
 
35
  def __init__(self, llm_manager, max_workers: int = 3):
 
41
  self.llm_manager = llm_manager
42
  self.max_workers = max_workers
43
 
44
+ # Carregar configuração da pipeline
45
+ with open('config/pipeline_config.yaml', 'r', encoding='utf-8') as f:
46
+ self.pipeline_config = yaml.safe_load(f)
47
+
48
+ # Inicializar especialistas (mapeamento temporário V13.1 → V13.6)
49
+ self.specialists = {
50
+ 1: ProcessorSegmentacao(llm_manager), # Segmentador
51
+ 2: ProcessorMetadados(llm_manager), # Metadados
52
+ 3: ProcessorContexto(llm_manager), # Classificador (renomear depois)
53
+ 4: ProcessorRelatorio(llm_manager), # Relatório
54
+ 5: ProcessorFundamentacao(llm_manager), # Fundamentação
55
+ 6: ProcessorDecisao(llm_manager), # Dispositivo
56
+ 7: ProcessorArquivo(llm_manager), # Arquivista
57
  }
58
 
59
+ # Inicializar orquestrador V13.6
60
+ self.orchestrator = PipelineOrchestrator(
61
+ config=self.pipeline_config,
62
+ llm_manager=llm_manager,
63
+ specialists=self.specialists
64
+ )
65
+
66
  logger.info(
67
+ f"✅ ProcessorManager V13.6: Orquestração refatorada com {len(self.specialists)} especialistas"
 
68
  )
69
 
70
  async def process_acordao_sequential(
 
73
  specialist_ids: Optional[List[int]] = None
74
  ) -> Dict[str, Any]:
75
  """
76
+ Processa acórdão sequencialmente (compatibilidade V13.1)
77
+
78
+ MUDANÇA: Delega para PipelineOrchestrator
 
 
 
 
 
79
  """
80
+ logger.info("🔄 process_acordao_sequential() delegando para PipelineOrchestrator")
81
+
82
+ # Se specialist_ids fornecido, processar apenas fases específicas
83
+ if specialist_ids:
84
+ fase_inicial = min(specialist_ids)
85
+ fase_final = max(specialist_ids)
86
+ else:
87
+ fase_inicial = 1
88
+ fase_final = 6
89
+
90
+ return await self.orchestrator.process_acordao(
91
+ acordao_bruto=acordao_data,
92
+ fase_inicial=fase_inicial,
93
+ fase_final=fase_final
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  )
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  async def process_acordao_parallel(
97
  self,
98
  acordao_data: Dict[str, Any],
99
  specialist_ids: Optional[List[int]] = None
100
  ) -> Dict[str, Any]:
101
  """
102
+ Processa acórdão em paralelo (compatibilidade V13.1)
103
+
104
+ MUDANÇA: Paralelo é controlado por fase no V13.6
 
 
 
 
 
105
  """
106
+ logger.info("🔄 process_acordao_parallel() delegando para PipelineOrchestrator")
107
+
108
+ return await self.orchestrator.process_acordao(
109
+ acordao_bruto=acordao_data,
110
+ fase_inicial=1,
111
+ fase_final=6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  )
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  def get_processor(self, specialist_id: int):
115
+ """Retorna especialista específico (compatibilidade V13.1)"""
116
+ return self.specialists.get(specialist_id)
117
 
118
  def get_all_processors(self) -> Dict[int, Any]:
119
+ """Retorna todos os especialistas (compatibilidade V13.1)"""
120
+ return self.specialists
121
 
122
+ def get_processors_info(self) -> Dict[str, Any]:
123
+ """Retorna informações sobre todos os especialistas"""
124
+ return self.orchestrator.get_pipeline_status()