Spaces:
Runtime error
Runtime error
ParaAIV3.1
Browse files- config/pipeline_config.yaml +62 -0
- core/__init__.py +18 -0
- core/base_specialist.py +309 -0
- core/context_builder.py +79 -0
- core/orchestrator.py +285 -0
- core/validator.py +160 -0
- processors/processor_manager.py +71 -197
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 -
|
|
|
|
| 3 |
"""
|
| 4 |
import logging
|
| 5 |
-
import
|
| 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.
|
| 12 |
-
from processors.processor_decisao import ProcessorDecisao
|
| 13 |
from processors.processor_fundamentacao import ProcessorFundamentacao
|
| 14 |
-
from processors.
|
| 15 |
from processors.processor_arquivo import ProcessorArquivo
|
| 16 |
-
from processors.
|
| 17 |
-
from processors.processor_auditoria import ProcessorAuditoria
|
| 18 |
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
| 21 |
|
| 22 |
class ProcessorManager:
|
| 23 |
"""
|
| 24 |
-
Gerenciador
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
-
|
| 28 |
-
-
|
| 29 |
-
-
|
| 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 |
-
#
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
logger.info(
|
| 56 |
-
f"✅ ProcessorManager:
|
| 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 (
|
| 67 |
-
|
| 68 |
-
|
| 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 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 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 (
|
| 136 |
-
|
| 137 |
-
|
| 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 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 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 |
-
"""
|
| 239 |
-
return self.
|
| 240 |
|
| 241 |
def get_all_processors(self) -> Dict[int, Any]:
|
| 242 |
-
"""Retorna todos os
|
| 243 |
-
return self.
|
| 244 |
|
| 245 |
-
def get_processors_info(self) ->
|
| 246 |
-
"""Retorna informações sobre todos os
|
| 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()
|
|
|
|
|
|
|
|
|