Carlex22 commited on
Commit
3233c37
·
1 Parent(s): e895ba5

ParaAIV3.0

Browse files
api/config.py DELETED
@@ -1,58 +0,0 @@
1
- """
2
- API Configuration
3
- Environment variables and settings
4
- """
5
- from pydantic_settings import BaseSettings
6
- from pydantic import Field
7
- from typing import Optional
8
- from pathlib import Path
9
- import os
10
-
11
-
12
- class Settings(BaseSettings):
13
- """API Configuration Settings"""
14
-
15
- APP_NAME: str = "para.AI API"
16
- APP_VERSION: str = "3.0.0"
17
- DEBUG: bool = os.getenv("DEBUG", "true").lower() == "true"
18
-
19
- API_HOST: str = os.getenv("API_HOST", "0.0.0.0")
20
- API_PORT: int = int(os.getenv("API_PORT", "7860"))
21
-
22
- # Paths
23
- BASE_DIR: Path = Path(__file__).parent
24
- STORAGE_DIR: Path = BASE_DIR / "storage"
25
- TEMP_DIR: Path = STORAGE_DIR / "temp"
26
- OUTPUT_DIR: Path = STORAGE_DIR / "output"
27
- DOWNLOADS_DIR: Path = STORAGE_DIR / "downloads"
28
-
29
- DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/paraai")
30
- REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
31
-
32
- LLM_MODEL: str = os.getenv("LLM_MODEL", "gemini-1.5-pro-001")
33
- LLM_TEMPERATURE: float = float(os.getenv("LLM_TEMPERATURE", "1.3"))
34
- LLM_MAX_TOKENS: int = int(os.getenv("LLM_MAX_TOKENS", "14000"))
35
-
36
-
37
- MAX_WORKERS: int = int(os.getenv("MAX_WORKERS", "10"))
38
- BATCH_SIZE: int = int(os.getenv("BATCH_SIZE", "100"))
39
-
40
- CORS_ORIGINS: list[str] = [
41
- "http://localhost:7860",
42
- "https://*.hf.space",
43
- "*"
44
- ]
45
-
46
- LOG_LEVEL: str = os.getenv("LOG_LEVEL", "DEBUG")
47
-
48
- model_config = {
49
- "env_file": ".env",
50
- "case_sensitive": True
51
- }
52
-
53
-
54
- settings = Settings()
55
-
56
- # Criar diretórios de storage
57
- for directory in [settings.TEMP_DIR, settings.OUTPUT_DIR, settings.DOWNLOADS_DIR]:
58
- directory.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/core/__init__.py DELETED
@@ -1,8 +0,0 @@
1
- """Core processing modules"""
2
-
3
- """Core processing modules"""
4
- from api.core.processor_manager import ProcessorManager
5
- from api.core.file_handler import FileHandler
6
- from api.core.tar_generator import TARGenerator
7
-
8
- __all__ = ['ProcessorManager', 'FileHandler', 'TARGenerator']
 
 
 
 
 
 
 
 
 
api/core/file_handler.py DELETED
@@ -1,35 +0,0 @@
1
- """Manipulação de arquivos"""
2
- from pathlib import Path
3
- from api.config import settings
4
- import json
5
- from datetime import datetime
6
-
7
-
8
- class FileHandler:
9
- """Gerenciador de arquivos"""
10
-
11
- def save_temp_jsonl(self, batch_id: str, content: bytes) -> Path:
12
- """Salva JSONL temporário"""
13
- file_path = settings.TEMP_DIR / f"{batch_id}.jsonl"
14
- with open(file_path, 'wb') as f:
15
- f.write(content)
16
- return file_path
17
-
18
- def save_processed_results(self, batch_id: str, results: list) -> Path:
19
- """Salva resultados processados"""
20
- output_path = settings.OUTPUT_DIR / f"{batch_id}_processed.jsonl"
21
- with open(output_path, 'w', encoding='utf-8') as f:
22
- for result in results:
23
- f.write(json.dumps(result, ensure_ascii=False) + '
24
- ')
25
- return output_path
26
-
27
- def cleanup_temp(self, batch_id: str):
28
- """Limpa arquivos temporários"""
29
- temp_file = settings.TEMP_DIR / f"{batch_id}.jsonl"
30
- if temp_file.exists():
31
- temp_file.unlink()
32
-
33
- def get_temp_file(self, batch_id: str) -> Path:
34
- """Retorna caminho do arquivo temporário"""
35
- return settings.TEMP_DIR / f"{batch_id}.jsonl"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/core/init.py DELETED
@@ -1,6 +0,0 @@
1
- """Core processing modules"""
2
- from api.core.processor_manager import ProcessorManager
3
- from api.core.file_handler import FileHandler
4
- from api.core.tar_generator import TARGenerator
5
-
6
- __all__ = ['ProcessorManager', 'FileHandler', 'TARGenerator']
 
 
 
 
 
 
 
api/core/processor_manager.py DELETED
@@ -1,82 +0,0 @@
1
- """Orquestrador de 9 especialistas"""
2
- import asyncio
3
- from typing import Dict, List, Any, Optional
4
- from api.processors.metadados import ProcessorMetadados
5
- from api.processors.relatorio import ProcessorRelatorio
6
- from api.processors.fundamentacao import ProcessorFundamentacao
7
- from api.processors.decisao import ProcessorDecisao
8
- from api.processors.auditoria import ProcessorAuditoria
9
- from api.processors.arquivo_relacional import ProcessorArquivoRelacional
10
- from api.processors.segmentacao import ProcessorSegmentacao
11
- from api.processors.contexto import ProcessorContexto
12
- from api.processors.transcricao import ProcessorTranscricao
13
- import logging
14
-
15
- logger = logging.getLogger("para_ai")
16
-
17
- class ProcessorManager:
18
- """Gerenciador dos 9 especialistas"""
19
-
20
- def __init__(self):
21
- self.specialists = {
22
- 1: ProcessorMetadados(),
23
- 2: ProcessorRelatorio(),
24
- 3: ProcessorFundamentacao(),
25
- 4: ProcessorDecisao(),
26
- 5: ProcessorAuditoria(),
27
- 6: ProcessorArquivoRelacional(),
28
- 7: ProcessorSegmentacao(),
29
- 8: ProcessorContexto(),
30
- 9: ProcessorTranscricao(),
31
- }
32
- logger.info("✅ ProcessorManager inicializado com 9 especialistas")
33
-
34
- async def process_acordao_sequential(
35
- self,
36
- acordao_data: Dict[str, Any],
37
- specialist_ids: List[int]
38
- ) -> Dict[str, Any]:
39
- """Processa sequencialmente"""
40
- results = {}
41
-
42
- for spec_id in specialist_ids:
43
- specialist = self.specialists[spec_id]
44
- result = specialist.process(acordao_data)
45
- results[f"specialist_{spec_id}"] = result
46
-
47
- return {"status": "completed", "results": results}
48
-
49
- async def process_acordao_parallel(
50
- self,
51
- acordao_data: Dict[str, Any],
52
- specialist_ids: List[int]
53
- ) -> Dict[str, Any]:
54
- """Processa em paralelo"""
55
- tasks = []
56
- for spec_id in specialist_ids:
57
- specialist = self.specialists[spec_id]
58
- task = asyncio.to_thread(specialist.process, acordao_data)
59
- tasks.append(task)
60
-
61
- results_list = await asyncio.gather(*tasks)
62
-
63
- results = {}
64
- for spec_id, result in zip(specialist_ids, results_list):
65
- results[f"specialist_{spec_id}"] = result
66
-
67
- return {"status": "completed", "results": results}
68
-
69
- def get_specialist(self, spec_id: int):
70
- """Retorna especialista específico"""
71
- return self.specialists.get(spec_id)
72
-
73
- def get_specialists_info(self) -> List[Dict]:
74
- """Info de todos especialistas"""
75
- return [
76
- {
77
- "id": sid,
78
- "name": s.specialist_name,
79
- "description": s.__class__.__doc__
80
- }
81
- for sid, s in self.specialists.items()
82
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/core/tar_genetator.py DELETED
@@ -1,34 +0,0 @@
1
- """Gerador de arquivos TAR.GZ"""
2
- import tarfile
3
- from pathlib import Path
4
- from api.config import settings
5
- from datetime import datetime
6
-
7
- class TARGenerator:
8
- """Gerador de TAR.GZ para download"""
9
-
10
- def create_tar_gz(self, batch_id: str, source_file: Path) -> Path:
11
- """Cria TAR.GZ com resultados"""
12
- tar_path = settings.DOWNLOADS_DIR / f"{batch_id}.tar.gz"
13
-
14
- with tarfile.open(tar_path, 'w:gz') as tar:
15
- tar.add(source_file, arcname=source_file.name)
16
-
17
- return tar_path
18
-
19
- def get_tar_path(self, batch_id: str) -> Path:
20
- """Retorna caminho do TAR.GZ"""
21
- return settings.DOWNLOADS_DIR / f"{batch_id}.tar.gz"
22
-
23
- def list_available_downloads(self) -> list:
24
- """Lista downloads disponíveis"""
25
- downloads = []
26
- for tar_file in settings.DOWNLOADS_DIR.glob('*.tar.gz'):
27
- stat = tar_file.stat()
28
- downloads.append({
29
- 'batch_id': tar_file.stem,
30
- 'filename': tar_file.name,
31
- 'size_bytes': stat.st_size,
32
- 'created_at': datetime.fromtimestamp(stat.st_ctime).isoformat()
33
- })
34
- return downloads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/env.py DELETED
@@ -1,52 +0,0 @@
1
- from logging.config import fileConfig
2
- from sqlalchemy import engine_from_config
3
- from sqlalchemy import pool
4
- from alembic import context
5
- import os
6
- from database.models import Base
7
-
8
- config = context.config
9
-
10
- if config.config_file_name is not None:
11
- fileConfig(config.config_file_name)
12
-
13
- target_metadata = Base.metadata
14
-
15
-
16
- def run_migrations_offline() -> None:
17
- sqlalchemy_url = os.getenv('DATABASE_URL', 'postgresql://user:password@localhost:5432/para_ai')
18
-
19
- context.configure(
20
- url=sqlalchemy_url,
21
- target_metadata=target_metadata,
22
- literal_binds=True,
23
- dialect_opts={"paramstyle": "named"},
24
- )
25
-
26
- with context.begin_transaction():
27
- context.run_migrations()
28
-
29
-
30
- def run_migrations_online() -> None:
31
- configuration = config.get_section(config.config_ini_section)
32
- configuration["sqlalchemy.url"] = os.getenv('DATABASE_URL', 'postgresql://user:password@localhost:5432/para_ai')
33
-
34
- connectable = engine_from_config(
35
- configuration,
36
- prefix="sqlalchemy.",
37
- poolclass=pool.NullPool,
38
- )
39
-
40
- with connectable.connect() as connection:
41
- context.configure(
42
- connection=connection, target_metadata=target_metadata
43
- )
44
-
45
- with context.begin_transaction():
46
- context.run_migrations()
47
-
48
-
49
- if context.is_offline_mode():
50
- run_migrations_offline()
51
- else:
52
- run_migrations_online()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/main.py CHANGED
@@ -1,29 +1,89 @@
1
  """
2
- FastAPI Application Principal
 
3
  """
4
- from fastapi import FastAPI
5
  from fastapi.middleware.cors import CORSMiddleware
6
- from api.config import settings
7
- from api.routes import health_routes, debug_routes, status_routes
8
- from api.routes import test_routes, process_routes
9
- import logging
10
-
11
- # Setup logging
12
- logging.basicConfig(
13
- level=settings.LOG_LEVEL,
14
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
15
- )
16
- logger = logging.getLogger("para_ai")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- # Criar app
19
  app = FastAPI(
20
  title=settings.APP_NAME,
21
  version=settings.APP_VERSION,
22
- description="Sistema para.AI - 9 Especialistas LLM para análise jurisprudencial",
23
- docs_url="/docs" if settings.DEBUG else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  )
25
 
26
- # CORS
 
 
 
27
  app.add_middleware(
28
  CORSMiddleware,
29
  allow_origins=settings.CORS_ORIGINS,
@@ -31,28 +91,141 @@ app.add_middleware(
31
  allow_methods=["*"],
32
  allow_headers=["*"],
33
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- # Rotas
36
- app.include_router(health_routes.router, prefix="/health", tags=["Health"])
37
- app.include_router(test_routes.router, prefix="/test", tags=["Tests"])
38
- app.include_router(process_routes.router, prefix="/process", tags=["Process"])
39
- app.include_router(status_routes.router, prefix="/status", tags=["Status"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- if settings.DEBUG:
42
- app.include_router(debug_routes.router, prefix="/debug", tags=["Debug"])
 
 
 
 
 
43
 
44
 
45
- @app.get("/")
 
 
 
 
46
  async def root():
 
 
47
  return {
48
- "app": settings.APP_NAME,
49
  "version": settings.APP_VERSION,
50
- "status": "running",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  "endpoints": {
52
- "docs": "/docs",
53
- "health": "/health",
54
- "test_specialists": "/test/specialists",
55
- "process_jsonl": "/process/jsonl",
56
- "download": "/download/{batch_id}"
57
  }
58
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ FastAPI Application - para.AI v3.0
3
+ Endpoint principal para processamento de acórdãos jurisprudenciais
4
  """
5
+ from fastapi import FastAPI, Request
6
  from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.middleware.gzip import GZipMiddleware
8
+ from fastapi.responses import JSONResponse
9
+ from contextlib import asynccontextmanager
10
+ import time
11
+ from datetime import datetime
12
+
13
+ from api.config import get_settings
14
+ from api.utils.logger import api_logger
15
+ from api.utils.exceptions import ParaAIException
16
+
17
+ settings = get_settings()
18
+
19
+ # Variável global para tracking de uptime
20
+ app_start_time = datetime.now()
21
+
22
+
23
+ @asynccontextmanager
24
+ async def lifespan(app: FastAPI):
25
+ """Lifecycle events - startup e shutdown."""
26
+ # Startup
27
+ api_logger.info("=" * 70)
28
+ api_logger.info(f"🚀 Starting {settings.APP_NAME} v{settings.APP_VERSION}")
29
+ api_logger.info(f"📍 Environment: {settings.APP_ENV}")
30
+ api_logger.info(f"🐛 Debug mode: {settings.DEBUG}")
31
+ api_logger.info(f"🗄️ Database: {settings.DATABASE_URL.split('@')[-1] if '@' in settings.DATABASE_URL else 'N/A'}")
32
+ api_logger.info(f"📂 Files path: {settings.FILES_BASE_PATH}")
33
+ api_logger.info(f"🤖 LLM Providers:")
34
+ api_logger.info(f" - Groq: {'✅' if settings.GROQ_API_KEY else '❌'}")
35
+ api_logger.info(f" - OpenAI: {'✅' if settings.OPENAI_API_KEY else '❌'}")
36
+ api_logger.info(f" - Anthropic: {'✅' if settings.ANTHROPIC_API_KEY else '❌'}")
37
+ api_logger.info("=" * 70)
38
+
39
+ yield
40
+
41
+ # Shutdown
42
+ api_logger.info("🛑 Shutting down para.AI API")
43
+
44
 
 
45
  app = FastAPI(
46
  title=settings.APP_NAME,
47
  version=settings.APP_VERSION,
48
+ description="""
49
+ # para.AI - Análise Jurisprudencial com IA
50
+
51
+ Sistema completo para processamento automatizado de acórdãos com 9 especialistas IA.
52
+
53
+ ## Funcionalidades Principais
54
+
55
+ * 📤 **Upload JSONL**: Envie lotes de acórdãos para processamento
56
+ * 🤖 **9 Especialistas**: Análise por múltiplos processadores especializados
57
+ * 📦 **Download TAR.GZ**: Receba resultados compactados
58
+ * 🔍 **Debug Completo**: Teste cada componente isoladamente
59
+ * 📊 **Métricas**: Acompanhe performance e custos
60
+
61
+ ## Endpoints Principais
62
+
63
+ * `/api/v1/process/upload` - Upload e processamento
64
+ * `/api/v1/process/status/{task_id}` - Status da tarefa
65
+ * `/api/v1/process/download/{task_id}` - Download de resultados
66
+ * `/api/v1/health` - Health check
67
+
68
+ ## Debug e Testes
69
+
70
+ * `/api/v1/debug/*` - Informações de sistema
71
+ * `/api/v1/test/llm/*` - Testar LLMs
72
+ * `/api/v1/test/processors/*` - Testar processadores
73
+ * `/api/v1/test/database/*` - Testar banco de dados
74
+ * `/api/v1/test/files/*` - Testar gestão de arquivos
75
+ """,
76
+ docs_url="/api/docs",
77
+ redoc_url="/api/redoc",
78
+ openapi_url="/api/openapi.json",
79
+ lifespan=lifespan,
80
+ swagger_ui_parameters={"defaultModelsExpandDepth": -1}
81
  )
82
 
83
+ # ============================================================================
84
+ # MIDDLEWARES
85
+ # ============================================================================
86
+
87
  app.add_middleware(
88
  CORSMiddleware,
89
  allow_origins=settings.CORS_ORIGINS,
 
91
  allow_methods=["*"],
92
  allow_headers=["*"],
93
  )
94
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
95
+
96
+
97
+ @app.middleware("http")
98
+ async def log_requests(request: Request, call_next):
99
+ """Middleware para logging de todas as requisições."""
100
+ start_time = time.time()
101
+
102
+ # Log request
103
+ api_logger.info(f"➡️ {request.method} {request.url.path}")
104
+
105
+ # Process
106
+ response = await call_next(request)
107
+
108
+ # Log response
109
+ process_time = (time.time() - start_time) * 1000
110
+ api_logger.info(
111
+ f"⬅️ {request.method} {request.url.path} - "
112
+ f"Status: {response.status_code} - "
113
+ f"Time: {process_time:.2f}ms"
114
+ )
115
+
116
+ # Add headers
117
+ response.headers["X-Process-Time"] = f"{process_time:.2f}"
118
+ response.headers["X-API-Version"] = settings.APP_VERSION
119
+
120
+ return response
121
+
122
+
123
+ @app.middleware("http")
124
+ async def add_security_headers(request: Request, call_next):
125
+ """Middleware para adicionar headers de segurança."""
126
+ response = await call_next(request)
127
+ response.headers["X-Content-Type-Options"] = "nosniff"
128
+ response.headers["X-Frame-Options"] = "DENY"
129
+ response.headers["X-XSS-Protection"] = "1; mode=block"
130
+ return response
131
+
132
+
133
+ # ============================================================================
134
+ # EXCEPTION HANDLERS
135
+ # ============================================================================
136
 
137
+ @app.exception_handler(ParaAIException)
138
+ async def para_ai_exception_handler(request: Request, exc: ParaAIException):
139
+ """Handler para exceções customizadas do para.AI."""
140
+ api_logger.error(f"ParaAIException: {exc.message} - Details: {exc.details}")
141
+ return JSONResponse(
142
+ status_code=500,
143
+ content={
144
+ "error": exc.message,
145
+ "details": exc.details,
146
+ "type": exc.__class__.__name__,
147
+ "timestamp": datetime.now().isoformat()
148
+ }
149
+ )
150
+
151
+
152
+ @app.exception_handler(Exception)
153
+ async def global_exception_handler(request: Request, exc: Exception):
154
+ """Handler global para exceções não tratadas."""
155
+ api_logger.error(f"Unhandled exception: {str(exc)}", exc_info=True)
156
+ return JSONResponse(
157
+ status_code=500,
158
+ content={
159
+ "error": "Internal server error",
160
+ "detail": str(exc) if settings.DEBUG else "An unexpected error occurred",
161
+ "timestamp": datetime.now().isoformat()
162
+ }
163
+ )
164
+
165
+
166
+ # ============================================================================
167
+ # ROUTERS
168
+ # ============================================================================
169
+
170
+ # Import routers (fazemos lazy import para evitar dependências circulares)
171
+ from api.routers import health, processing, debug, llm, database, files, processors
172
 
173
+ app.include_router(health.router, prefix="/api/v1", tags=["🏥 Health"])
174
+ app.include_router(processing.router, prefix="/api/v1", tags=["🚀 Processing"])
175
+ app.include_router(debug.router, prefix="/api/v1/debug", tags=["🐛 Debug"])
176
+ app.include_router(llm.router, prefix="/api/v1/test/llm", tags=["🤖 LLM Testing"])
177
+ app.include_router(database.router, prefix="/api/v1/test/database", tags=["🗄️ Database Testing"])
178
+ app.include_router(files.router, prefix="/api/v1/test/files", tags=["📁 Files Testing"])
179
+ app.include_router(processors.router, prefix="/api/v1/test/processors", tags=["⚙️ Processors Testing"])
180
 
181
 
182
+ # ============================================================================
183
+ # ROOT ENDPOINTS
184
+ # ============================================================================
185
+
186
+ @app.get("/", include_in_schema=False)
187
  async def root():
188
+ """Root endpoint - informações básicas da API."""
189
+ uptime = (datetime.now() - app_start_time).total_seconds()
190
  return {
191
+ "name": settings.APP_NAME,
192
  "version": settings.APP_VERSION,
193
+ "environment": settings.APP_ENV,
194
+ "status": "online",
195
+ "uptime_seconds": uptime,
196
+ "docs": "/api/docs",
197
+ "health": "/api/v1/health",
198
+ "timestamp": datetime.now().isoformat()
199
+ }
200
+
201
+
202
+ @app.get("/api", include_in_schema=False)
203
+ async def api_root():
204
+ """API root - redireciona para docs."""
205
+ return {
206
+ "message": "para.AI API v3.0 - Acesse /api/docs para documentação completa",
207
+ "docs": "/api/docs",
208
  "endpoints": {
209
+ "health": "/api/v1/health",
210
+ "upload": "/api/v1/process/upload",
211
+ "debug": "/api/v1/debug/info"
 
 
212
  }
213
+ }
214
+
215
+
216
+ # ============================================================================
217
+ # MAIN (para execução direta)
218
+ # ============================================================================
219
+
220
+ if __name__ == "__main__":
221
+ import uvicorn
222
+
223
+ uvicorn.run(
224
+ "api.main:app",
225
+ host=settings.HOST,
226
+ port=settings.PORT,
227
+ reload=settings.DEBUG,
228
+ workers=1 if settings.DEBUG else settings.WORKERS,
229
+ log_level=settings.LOG_LEVEL.lower(),
230
+ access_log=True
231
+ )
api/routes/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- """Routes package"""
2
- from api.routes import health, test, process, download
3
 
4
- __all__ = ['health', 'test', 'process', 'download']
 
1
+ """Routers package"""
2
+ from . import health, processing, debug, llm, database, files, processors
3
 
4
+ __all__ = ["health", "processing", "debug", "llm", "database", "files", "processors"]
api/routes/debug.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Router de debug - informações de sistema e tasks"""
2
+ from fastapi import APIRouter
3
+ from api.config import get_settings
4
+ from api.utils.logger import setup_logger
5
+ import sys, os
6
+
7
+ router = APIRouter()
8
+ logger = setup_logger(__name__)
9
+ settings = get_settings()
10
+
11
+ @router.get("/info")
12
+ async def debug_info():
13
+ """Informações completas do sistema"""
14
+ return {
15
+ "python_version": sys.version,
16
+ "environment": settings.APP_ENV,
17
+ "debug_mode": settings.DEBUG,
18
+ "paths": {
19
+ "upload": settings.UPLOAD_PATH,
20
+ "output": settings.OUTPUT_PATH,
21
+ "files": settings.FILES_BASE_PATH
22
+ },
23
+ "llm_config": {
24
+ "default_provider": settings.DEFAULT_LLM_PROVIDER,
25
+ "providers_available": {
26
+ "groq": bool(settings.GROQ_API_KEY),
27
+ "openai": bool(settings.OPENAI_API_KEY),
28
+ "anthropic": bool(settings.ANTHROPIC_API_KEY)
29
+ }
30
+ }
31
+ }
32
+
33
+ @router.get("/tasks")
34
+ async def list_tasks():
35
+ """Lista todas as tasks"""
36
+ from api.routers.processing import processing_tasks
37
+ return {"total": len(processing_tasks), "tasks": list(processing_tasks.keys())}
api/routes/debug_routes.py DELETED
@@ -1,83 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- ParaAi - Debug Routes
4
- GET /debug/* - Endpoints de debug para testes
5
- """
6
-
7
- from fastapi import APIRouter, HTTPException, Query
8
- from datetime import datetime
9
-
10
- def create_debug_router(llm_client, db_manager, context_engine, files_manager, workers):
11
- """Factory para criar router de debug"""
12
- router = APIRouter()
13
-
14
- @router.get("/health")
15
- async def debug_health() -> Dict:
16
- """Health check detalhado"""
17
- return {
18
- "status": "healthy",
19
- "llm_client": "OK" if llm_client else "FAIL",
20
- "db_manager": "OK" if db_manager else "FAIL",
21
- "context_engine": "OK" if context_engine else "FAIL",
22
- "files_manager": "OK" if files_manager else "FAIL",
23
- "workers_count": len(workers),
24
- "timestamp": datetime.now().isoformat()
25
- }
26
-
27
- @router.get("/stats")
28
- async def debug_stats() -> Dict:
29
- """Estatísticas globais de todos os componentes"""
30
- return {
31
- "llm_client": llm_client.get_stats() if llm_client else {},
32
- "context_engine": context_engine.obter_stats() if context_engine else {},
33
- "files_manager": files_manager.obter_stats() if files_manager else {},
34
- "workers_count": len(workers)
35
- }
36
-
37
- @router.get("/llm-config")
38
- async def debug_llm_config() -> Dict:
39
- """Retorna configuração de especialistas LLM"""
40
- if not llm_client:
41
- raise HTTPException(status_code=503, detail="LLM Client não inicializado")
42
- return {
43
- "especialistas": llm_client.obter_config_especialistas()
44
- }
45
-
46
- @router.get("/test-llm-api")
47
- async def debug_test_llm_api(
48
- especialista: str = "metadados",
49
- systemprompt: str = "Você é um assistente útil."
50
- ) -> Dict:
51
- """Testa chamada de API LLM"""
52
- try:
53
- resultado = llm_client.processar_requisicao(
54
- especialista=especialista,
55
- systemprompt=systemprompt,
56
- userprompt="Qual é a capital do Brasil?",
57
- cache=False
58
- )
59
- return resultado
60
- except Exception as e:
61
- raise HTTPException(status_code=500, detail=str(e))
62
-
63
- @router.get("/test-especialista/{especialista}")
64
- async def debug_test_especialista(especialista: str) -> Dict:
65
- """Testa um especialista específico"""
66
- try:
67
- if especialista not in llm_client.especialistas:
68
- raise HTTPException(
69
- status_code=404,
70
- detail=f"Especialista {especialista} não encontrado"
71
- )
72
-
73
- resultado = llm_client.processar_requisicao(
74
- especialista=especialista,
75
- systemprompt=f"Você é um especialista em {especialista}.",
76
- userprompt="Analise este texto jurídico simples.",
77
- cache=False
78
- )
79
- return resultado
80
- except Exception as e:
81
- raise HTTPException(status_code=500, detail=str(e))
82
-
83
- return router
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/routes/download.py DELETED
@@ -1,30 +0,0 @@
1
- """Endpoints de download"""
2
- from fastapi import APIRouter, HTTPException
3
- from fastapi.responses import FileResponse
4
- from api.core.tar_generator import TARGenerator
5
- from pathlib import Path
6
-
7
- router = APIRouter()
8
- tar_gen = TARGenerator()
9
-
10
- @router.get("/{batch_id}")
11
- async def download_results(batch_id: str):
12
- """Download do TAR.GZ processado"""
13
- tar_path = tar_gen.get_tar_path(batch_id)
14
-
15
- if not tar_path.exists():
16
- raise HTTPException(
17
- status_code=404,
18
- detail=f"Arquivo não encontrado para batch_id: {batch_id}"
19
- )
20
-
21
- return FileResponse(
22
- path=str(tar_path),
23
- filename=tar_path.name,
24
- media_type='application/gzip'
25
- )
26
-
27
- @router.get("/list/all")
28
- async def list_downloads():
29
- """Lista todos os downloads disponíveis"""
30
- return tar_gen.list_available_downloads()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/routes/health.py CHANGED
@@ -1,35 +1,92 @@
1
- """Health check endpoints"""
 
 
2
  from fastapi import APIRouter
3
  from datetime import datetime
4
- from api.config import settings
 
 
 
 
 
5
 
6
  router = APIRouter()
 
 
 
 
7
 
8
- @router.get("/")
9
- async def health_check():
10
- """Health check básico"""
11
- return {
12
- "status": "healthy",
13
- "timestamp": datetime.now().isoformat(),
14
- "version": settings.APP_VERSION,
15
- "app": settings.APP_NAME
16
- }
17
 
18
- @router.get("/detailed")
19
- async def detailed_health():
20
- """Health check detalhado"""
21
- return {
22
- "status": "healthy",
23
- "timestamp": datetime.now().isoformat(),
24
- "version": settings.APP_VERSION,
25
- "config": {
26
- "max_workers": settings.MAX_WORKERS,
27
- "batch_size": settings.BATCH_SIZE,
28
- "debug": settings.DEBUG
29
- },
30
- "storage": {
31
- "temp_dir": str(settings.TEMP_DIR),
32
- "output_dir": str(settings.OUTPUT_DIR),
33
- "downloads_dir": str(settings.DOWNLOADS_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Router de health check e status do sistema
3
+ """
4
  from fastapi import APIRouter
5
  from datetime import datetime
6
+ import psutil
7
+ import os
8
+
9
+ from api.models.responses import HealthResponse
10
+ from api.config import get_settings
11
+ from api.utils.logger import setup_logger
12
 
13
  router = APIRouter()
14
+ logger = setup_logger(__name__)
15
+ settings = get_settings()
16
+
17
+ app_start_time = datetime.now()
18
 
 
 
 
 
 
 
 
 
 
19
 
20
+ @router.get("/health", response_model=HealthResponse)
21
+ async def health_check():
22
+ """
23
+ **Health check completo do sistema.**
24
+
25
+ Verifica:
26
+ - ✅ Database connectivity
27
+ - ✅ LLM providers availability
28
+ - ✅ File system access
29
+ - ✅ System metrics
30
+ """
31
+ services = {}
32
+
33
+ # Check Database
34
+ try:
35
+ from database.db_manager import get_db_manager
36
+ db = get_db_manager()
37
+ services['database'] = db.health_check()
38
+ except Exception as e:
39
+ logger.error(f"DB health check failed: {e}")
40
+ services['database'] = False
41
+
42
+ # Check LLM Providers
43
+ try:
44
+ services['llm_groq'] = bool(settings.GROQ_API_KEY)
45
+ services['llm_openai'] = bool(settings.OPENAI_API_KEY)
46
+ services['llm_anthropic'] = bool(settings.ANTHROPIC_API_KEY)
47
+ except Exception as e:
48
+ logger.error(f"LLM health check failed: {e}")
49
+ services.update({
50
+ 'llm_groq': False,
51
+ 'llm_openai': False,
52
+ 'llm_anthropic': False
53
+ })
54
+
55
+ # Check Files
56
+ from pathlib import Path
57
+ services['files_upload'] = Path(settings.UPLOAD_PATH).exists()
58
+ services['files_output'] = Path(settings.OUTPUT_PATH).exists()
59
+ services['files_base'] = Path(settings.FILES_BASE_PATH).exists()
60
+
61
+ # System metrics
62
+ try:
63
+ metrics = {
64
+ 'cpu_percent': psutil.cpu_percent(),
65
+ 'memory_percent': psutil.virtual_memory().percent,
66
+ 'disk_percent': psutil.disk_usage('/').percent,
67
+ 'process_count': len(psutil.pids())
68
  }
69
+ except:
70
+ metrics = {}
71
+
72
+ # Calcular uptime
73
+ uptime = (datetime.now() - app_start_time).total_seconds()
74
+
75
+ # Status geral
76
+ status = "healthy" if all(services.values()) else "degraded"
77
+
78
+ return HealthResponse(
79
+ status=status,
80
+ version=settings.APP_VERSION,
81
+ environment=settings.APP_ENV,
82
+ timestamp=datetime.now(),
83
+ uptime_seconds=uptime,
84
+ services=services,
85
+ metrics=metrics
86
+ )
87
+
88
+
89
+ @router.get("/ping")
90
+ async def ping():
91
+ """Simple ping endpoint."""
92
+ return {"status": "pong", "timestamp": datetime.now().isoformat()}
api/routes/process.py DELETED
@@ -1,98 +0,0 @@
1
- """Endpoints de processamento JSONL"""
2
- from fastapi import APIRouter, UploadFile, File, BackgroundTasks, HTTPException
3
- from api.api.schemas import ProcessResponse
4
- from api.core.processor_manager import ProcessorManager
5
- from api.core.file_handler import FileHandler
6
- from api.core.tar_generator import TARGenerator
7
- from datetime import datetime
8
- import json
9
- import uuid
10
-
11
- router = APIRouter()
12
- manager = ProcessorManager()
13
- file_handler = FileHandler()
14
- tar_gen = TARGenerator()
15
-
16
- @router.post("/jsonl", response_model=ProcessResponse)
17
- async def process_jsonl(
18
- file: UploadFile = File(...),
19
- background_tasks: BackgroundTasks = None,
20
- parallel: bool = True
21
- ):
22
- """
23
- Processa arquivo JSONL e gera TAR.GZ
24
-
25
- Args:
26
- file: Arquivo JSONL com acórdãos
27
- parallel: Processar em paralelo?
28
-
29
- Returns:
30
- Status do processamento com batch_id
31
- """
32
- if not file.filename.endswith('.jsonl'):
33
- raise HTTPException(status_code=400, detail="Arquivo deve ser .jsonl")
34
-
35
- # Gerar batch_id
36
- batch_id = f"batch_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
37
-
38
- # Ler arquivo
39
- content = await file.read()
40
- lines = content.decode('utf-8').split('\n')
41
- lines = [l.strip() for l in lines if l.strip()]
42
-
43
- total_records = len(lines)
44
-
45
- # Salvar arquivo temporário
46
- temp_file = file_handler.save_temp_jsonl(batch_id, content)
47
-
48
- # Processar em background
49
- background_tasks.add_task(
50
- process_batch_task,
51
- batch_id=batch_id,
52
- temp_file=temp_file,
53
- total_records=total_records,
54
- parallel=parallel
55
- )
56
-
57
- return ProcessResponse(
58
- batch_id=batch_id,
59
- status="processing",
60
- total_records=total_records,
61
- timestamp=datetime.now().isoformat(),
62
- message=f"Processamento iniciado. Use /download/{batch_id} após conclusão."
63
- )
64
-
65
- async def process_batch_task(batch_id: str, temp_file: str, total_records: int, parallel: bool):
66
- """Task de processamento em background"""
67
- import logging
68
- logger = logging.getLogger("para_ai")
69
-
70
- logger.info(f"🚀 Iniciando processamento batch {batch_id} ({total_records} registros)")
71
-
72
- try:
73
- # Ler registros
74
- with open(temp_file, 'r', encoding='utf-8') as f:
75
- registros = [json.loads(line) for line in f if line.strip()]
76
-
77
- # Processar cada registro
78
- resultados = []
79
- for idx, registro in enumerate(registros):
80
- logger.info(f"📄 Processando {idx+1}/{total_records}")
81
-
82
- if parallel:
83
- resultado = await manager.process_acordao_parallel(registro, [1,2,3,4,5,6,7,8,9])
84
- else:
85
- resultado = await manager.process_acordao_sequential(registro, [1,2,3,4,5,6,7,8,9])
86
-
87
- resultados.append(resultado)
88
-
89
- # Salvar resultados processados
90
- output_file = file_handler.save_processed_results(batch_id, resultados)
91
-
92
- # Gerar TAR.GZ
93
- tar_path = tar_gen.create_tar_gz(batch_id, output_file)
94
-
95
- logger.info(f"✅ Batch {batch_id} concluído: {tar_path}")
96
-
97
- except Exception as e:
98
- logger.error(f"❌ Erro no batch {batch_id}: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/routes/process_routes.py DELETED
@@ -1,135 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- ParaAi - Process Routes
4
- POST /process - Inicia processamento de lote
5
- """
6
-
7
- import json
8
- import logging
9
- from datetime import datetime
10
- from typing import Dict, List, Optional
11
- from pathlib import Path
12
-
13
- from fastapi import APIRouter, HTTPException, BackgroundTasks, UploadFile, File
14
- from pydantic import BaseModel
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
- class ProcessRequest(BaseModel):
19
- jsonl_path: str
20
- batch_id: Optional[str] = None
21
- num_workers: int = 10
22
- enable_cache: bool = True
23
-
24
- class ProcessResponse(BaseModel):
25
- status: str
26
- batch_id: str
27
- timestamp: str
28
-
29
- def create_process_router(llm_client, db_manager, context_engine, files_manager, workers):
30
- """Factory para criar router de processamento"""
31
- router = APIRouter()
32
-
33
- @router.post("/start")
34
- async def process_start(
35
- request: ProcessRequest,
36
- background_tasks: BackgroundTasks
37
- ) -> Dict:
38
- """
39
- Inicia processamento de lote de acórdãos
40
-
41
- Args:
42
- jsonl_path: Caminho para arquivo JSONL
43
- batch_id: ID único do lote (gerado se não fornecido)
44
- num_workers: Número de workers paralelos
45
- enable_cache: Se deve usar cache de LLM
46
-
47
- Returns:
48
- {batch_id, status, timestamp, total_records}
49
- """
50
- try:
51
- # Validar caminho
52
- jsonl_file = Path(request.jsonl_path)
53
- if not jsonl_file.exists():
54
- raise HTTPException(
55
- status_code=404,
56
- detail=f"Arquivo JSONL não encontrado: {request.jsonl_path}"
57
- )
58
-
59
- # Gerar batch_id
60
- batch_id = request.batch_id or f"batch_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
61
-
62
- # Contar registros
63
- total_records = sum(1 for _ in open(jsonl_file))
64
-
65
- logger.info(f"🚀 Iniciando processamento: batch_id={batch_id}, registros={total_records}")
66
-
67
- # Agendar processamento em background
68
- background_tasks.add_task(
69
- process_batch_task,
70
- batch_id=batch_id,
71
- jsonl_path=str(jsonl_file),
72
- num_workers=request.num_workers,
73
- enable_cache=request.enable_cache,
74
- llm_client=llm_client,
75
- db_manager=db_manager,
76
- context_engine=context_engine,
77
- files_manager=files_manager,
78
- workers=workers
79
- )
80
-
81
- return {
82
- "status": "accepted",
83
- "batch_id": batch_id,
84
- "total_records": total_records,
85
- "timestamp": datetime.now().isoformat()
86
- }
87
-
88
- except Exception as e:
89
- logger.error(f"❌ Erro ao iniciar processamento: {e}")
90
- raise HTTPException(status_code=500, detail=str(e))
91
-
92
- return router
93
-
94
- async def process_batch_task(
95
- batch_id: str,
96
- jsonl_path: str,
97
- num_workers: int,
98
- enable_cache: bool,
99
- llm_client,
100
- db_manager,
101
- context_engine,
102
- files_manager,
103
- workers
104
- ):
105
- """Task que executa processamento em background"""
106
- logger.info(f"📊 Iniciando processamento de batch {batch_id}")
107
-
108
- try:
109
- # Ler arquivo JSONL
110
- registros = []
111
- with open(jsonl_path, 'r', encoding='utf-8') as f:
112
- for linha in f:
113
- registros.append(json.loads(linha))
114
-
115
- logger.info(f"📋 {len(registros)} registros carregados")
116
-
117
- # Distribuir entre workers (round-robin)
118
- for idx, registro in enumerate(registros):
119
- worker_idx = idx % num_workers
120
- worker = workers[worker_idx]
121
-
122
- # Processar
123
- resultado = worker.processar_tarefa({
124
- 'id': f"{batch_id}_{idx}",
125
- 'chunk_id': f"chunk_{idx}",
126
- 'dados_originais': registro
127
- })
128
-
129
- logger.info(f"✅ Registro {idx} processado: {resultado.get('status')}")
130
-
131
- logger.info(f"🎉 Batch {batch_id} concluído")
132
-
133
- except Exception as e:
134
- logger.error(f"❌ Erro no processamento de batch {batch_id}: {e}")
135
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/routes/processing.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Router de processamento de acórdãos
3
+ ENDPOINT PRINCIPAL: Upload JSONL → Processamento → Download TAR.GZ
4
+ """
5
+ from fastapi import APIRouter, UploadFile, File, BackgroundTasks, HTTPException, Query
6
+ from fastapi.responses import FileResponse
7
+ import uuid
8
+ import json
9
+ from pathlib import Path
10
+ from datetime import datetime
11
+ import hashlib
12
+
13
+ from api.models.requests import ProcessingOptionsRequest
14
+ from api.models.responses import ProcessingResponse, ProcessingStatus, FileInfoResponse
15
+ from api.services.processing_service import ProcessingService
16
+ from api.utils.logger import setup_logger
17
+ from api.config import get_settings
18
+
19
+ router = APIRouter()
20
+ logger = setup_logger(__name__)
21
+ settings = get_settings()
22
+
23
+ # Storage de tasks (em produção usar Redis ou Database)
24
+ processing_tasks = {}
25
+
26
+
27
+ @router.post("/process/upload", response_model=ProcessingResponse, status_code=202)
28
+ async def upload_and_process(
29
+ background_tasks: BackgroundTasks,
30
+ file: UploadFile = File(..., description="Arquivo JSONL com acórdãos"),
31
+ llm_provider: str = Query("groq", description="Provedor LLM (groq/openai/anthropic)"),
32
+ model_type: str = Query("balanced", description="Tipo de modelo (fast/balanced/quality)"),
33
+ enable_parallel: bool = Query(True, description="Processar em paralelo"),
34
+ max_workers: int = Query(3, ge=1, le=10, description="Workers paralelos"),
35
+ save_to_db: bool = Query(False, description="Salvar resultados no banco")
36
+ ):
37
+ """
38
+ **Upload de arquivo JSONL e início do processamento em background.**
39
+
40
+ ## Fluxo:
41
+ 1. Upload do arquivo JSONL
42
+ 2. Validação do formato
43
+ 3. Criação de task de processamento
44
+ 4. Processamento em background (9 especialistas)
45
+ 5. Geração de arquivo TAR.GZ com resultados
46
+
47
+ ## Formato JSONL esperado:
48
+ ```json
49
+ {"acordao_id": "001", "tribunal": "TJPR", "ementa": "...", "integra": "..."}
50
+ {"acordao_id": "002", "tribunal": "TJSP", "ementa": "...", "integra": "..."}
51
+ ```
52
+
53
+ ## Response:
54
+ - **task_id**: ID único para consultar status
55
+ - **status**: Status inicial (pending)
56
+ - Use `/process/status/{task_id}` para acompanhar
57
+ - Use `/process/download/{task_id}` para baixar resultados
58
+ """
59
+
60
+ # Validar extensão
61
+ if not file.filename.endswith(('.jsonl', '.json')):
62
+ raise HTTPException(
63
+ status_code=400,
64
+ detail="Arquivo deve ser .jsonl ou .json"
65
+ )
66
+
67
+ # Validar tamanho
68
+ content = await file.read()
69
+ size_mb = len(content) / (1024 * 1024)
70
+ if size_mb > settings.MAX_UPLOAD_SIZE_MB:
71
+ raise HTTPException(
72
+ status_code=413,
73
+ detail=f"Arquivo muito grande: {size_mb:.2f}MB (máx: {settings.MAX_UPLOAD_SIZE_MB}MB)"
74
+ )
75
+
76
+ # Criar task ID
77
+ task_id = f"task-{uuid.uuid4()}"
78
+
79
+ # Criar diretórios
80
+ upload_dir = Path(settings.UPLOAD_PATH)
81
+ upload_dir.mkdir(parents=True, exist_ok=True)
82
+
83
+ # Salvar arquivo
84
+ file_path = upload_dir / f"{task_id}_{file.filename}"
85
+ file_path.write_bytes(content)
86
+
87
+ # Calcular hash
88
+ file_hash = hashlib.sha256(content).hexdigest()
89
+
90
+ # Validar e contar registros
91
+ total_records = 0
92
+ try:
93
+ for line_num, line in enumerate(content.decode('utf-8').strip().split('
94
+ '), 1):
95
+ if not line.strip():
96
+ continue
97
+ try:
98
+ record = json.loads(line)
99
+ # Validar campos obrigatórios
100
+ if 'ementa' not in record or 'integra' not in record:
101
+ raise HTTPException(
102
+ status_code=422,
103
+ detail=f"Linha {line_num}: campos obrigatórios ausentes (ementa, integra)"
104
+ )
105
+ total_records += 1
106
+ except json.JSONDecodeError as e:
107
+ raise HTTPException(
108
+ status_code=422,
109
+ detail=f"JSONL inválido na linha {line_num}: {str(e)}"
110
+ )
111
+ except UnicodeDecodeError:
112
+ raise HTTPException(
113
+ status_code=422,
114
+ detail="Arquivo deve estar em UTF-8"
115
+ )
116
+
117
+ # Criar response inicial
118
+ response = ProcessingResponse(
119
+ task_id=task_id,
120
+ status=ProcessingStatus.PENDING,
121
+ message=f"Processamento agendado para {total_records} registros",
122
+ total_records=total_records,
123
+ processed_records=0,
124
+ failed_records=0,
125
+ started_at=datetime.now()
126
+ )
127
+
128
+ # Armazenar task
129
+ processing_tasks[task_id] = response.dict()
130
+
131
+ # Adicionar metadados
132
+ processing_tasks[task_id]['metadata'] = {
133
+ 'filename': file.filename,
134
+ 'size_bytes': len(content),
135
+ 'hash_sha256': file_hash,
136
+ 'llm_provider': llm_provider,
137
+ 'model_type': model_type,
138
+ 'enable_parallel': enable_parallel,
139
+ 'max_workers': max_workers,
140
+ 'save_to_db': save_to_db
141
+ }
142
+
143
+ # Iniciar processamento em background
144
+ background_tasks.add_task(
145
+ process_acordaos_background,
146
+ task_id=task_id,
147
+ file_path=str(file_path),
148
+ llm_provider=llm_provider,
149
+ model_type=model_type,
150
+ enable_parallel=enable_parallel,
151
+ max_workers=max_workers,
152
+ save_to_db=save_to_db
153
+ )
154
+
155
+ logger.info(f"✅ Task {task_id} criada - {total_records} registros - {size_mb:.2f}MB")
156
+
157
+ return response
158
+
159
+
160
+ @router.get("/process/status/{task_id}", response_model=ProcessingResponse)
161
+ async def get_processing_status(task_id: str):
162
+ """
163
+ **Consulta status de processamento.**
164
+
165
+ Retorna informações atualizadas sobre a task:
166
+ - Status atual (pending/processing/completed/error)
167
+ - Progresso (processados/total)
168
+ - Tempo estimado restante
169
+ - URL de download (quando concluído)
170
+ """
171
+ if task_id not in processing_tasks:
172
+ raise HTTPException(
173
+ status_code=404,
174
+ detail=f"Task '{task_id}' não encontrada"
175
+ )
176
+
177
+ return ProcessingResponse(**processing_tasks[task_id])
178
+
179
+
180
+ @router.get("/process/list")
181
+ async def list_all_tasks():
182
+ """
183
+ **Lista todas as tasks de processamento.**
184
+
185
+ Útil para debug e monitoramento.
186
+ """
187
+ return {
188
+ "total": len(processing_tasks),
189
+ "tasks": [
190
+ {
191
+ "task_id": task_id,
192
+ "status": data["status"],
193
+ "progress": f"{data['processed_records']}/{data['total_records']}",
194
+ "started_at": data.get("started_at")
195
+ }
196
+ for task_id, data in processing_tasks.items()
197
+ ]
198
+ }
199
+
200
+
201
+ @router.get("/process/download/{task_id}")
202
+ async def download_result(task_id: str):
203
+ """
204
+ **Download do arquivo TAR.GZ com resultados.**
205
+
206
+ Disponível apenas quando status = "completed".
207
+
208
+ ## Conteúdo do arquivo:
209
+ - `{task_id}_results.json`: Resultados completos
210
+ - Análises de cada especialista
211
+ - Metadados do processamento
212
+ - Logs e métricas
213
+ """
214
+ if task_id not in processing_tasks:
215
+ raise HTTPException(
216
+ status_code=404,
217
+ detail=f"Task '{task_id}' não encontrada"
218
+ )
219
+
220
+ task = processing_tasks[task_id]
221
+
222
+ if task['status'] != ProcessingStatus.COMPLETED:
223
+ raise HTTPException(
224
+ status_code=400,
225
+ detail=f"Processamento ainda não concluído. Status atual: {task['status']}"
226
+ )
227
+
228
+ # Procurar arquivo
229
+ output_file = Path(settings.OUTPUT_PATH) / "archives" / f"{task_id}.tar.gz"
230
+
231
+ if not output_file.exists():
232
+ raise HTTPException(
233
+ status_code=404,
234
+ detail="Arquivo de resultado não encontrado"
235
+ )
236
+
237
+ logger.info(f"📦 Download iniciado: {task_id}")
238
+
239
+ return FileResponse(
240
+ path=str(output_file),
241
+ filename=f"para_ai_resultado_{task_id}.tar.gz",
242
+ media_type="application/gzip",
243
+ headers={
244
+ "Content-Disposition": f"attachment; filename=para_ai_resultado_{task_id}.tar.gz"
245
+ }
246
+ )
247
+
248
+
249
+ @router.delete("/process/{task_id}")
250
+ async def delete_task(task_id: str):
251
+ """
252
+ **Deleta uma task e seus arquivos.**
253
+
254
+ Útil para limpeza de tasks antigas.
255
+ """
256
+ if task_id not in processing_tasks:
257
+ raise HTTPException(
258
+ status_code=404,
259
+ detail=f"Task '{task_id}' não encontrada"
260
+ )
261
+
262
+ # Remover arquivos
263
+ upload_dir = Path(settings.UPLOAD_PATH)
264
+ output_dir = Path(settings.OUTPUT_PATH)
265
+
266
+ for file in upload_dir.glob(f"{task_id}_*"):
267
+ file.unlink()
268
+
269
+ for file in output_dir.glob(f"{task_id}*"):
270
+ file.unlink()
271
+
272
+ # Remover da memória
273
+ del processing_tasks[task_id]
274
+
275
+ logger.info(f"🗑️ Task deletada: {task_id}")
276
+
277
+ return {"message": f"Task {task_id} deletada com sucesso"}
278
+
279
+
280
+ # ============================================================================
281
+ # FUNÇÃO DE BACKGROUND
282
+ # ============================================================================
283
+
284
+ async def process_acordaos_background(
285
+ task_id: str,
286
+ file_path: str,
287
+ llm_provider: str,
288
+ model_type: str,
289
+ enable_parallel: bool,
290
+ max_workers: int,
291
+ save_to_db: bool
292
+ ):
293
+ """
294
+ Função executada em background para processar acórdãos.
295
+
296
+ Atualiza o status da task conforme progresso.
297
+ """
298
+ try:
299
+ # Atualizar status
300
+ processing_tasks[task_id]['status'] = ProcessingStatus.PROCESSING
301
+ processing_tasks[task_id]['message'] = "Processamento em andamento..."
302
+
303
+ logger.info(f"🚀 Iniciando processamento background: {task_id}")
304
+
305
+ # Inicializar serviço
306
+ service = ProcessingService()
307
+
308
+ # Processar
309
+ result = await service.process_jsonl_file(
310
+ file_path=file_path,
311
+ task_id=task_id,
312
+ llm_provider=llm_provider,
313
+ model_type=model_type,
314
+ enable_parallel=enable_parallel,
315
+ max_workers=max_workers
316
+ )
317
+
318
+ # Atualizar task com sucesso
319
+ processing_tasks[task_id].update({
320
+ 'status': ProcessingStatus.COMPLETED,
321
+ 'message': f"Processamento concluído com sucesso em {result['elapsed_seconds']:.2f}s",
322
+ 'processed_records': result['processed'],
323
+ 'failed_records': result['failed'],
324
+ 'completed_at': datetime.now(),
325
+ 'download_url': f"/api/v1/process/download/{task_id}",
326
+ 'result_metadata': {
327
+ 'archive_path': result['archive_path'],
328
+ 'hash_sha256': result['hash'],
329
+ 'elapsed_seconds': result['elapsed_seconds']
330
+ }
331
+ })
332
+
333
+ logger.info(f"✅ Task {task_id} concluída - {result['processed']} processados, {result['failed']} falhas")
334
+
335
+ except Exception as e:
336
+ # Atualizar task com erro
337
+ logger.error(f"❌ Erro na task {task_id}: {str(e)}", exc_info=True)
338
+ processing_tasks[task_id].update({
339
+ 'status': ProcessingStatus.ERROR,
340
+ 'message': f"Erro no processamento: {str(e)}",
341
+ 'completed_at': datetime.now(),
342
+ 'errors': [str(e)]
343
+ })
api/routes/status_routes.py DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- ParaAi - Status Routes
4
- GET /status/{batch_id} - Status do processamento
5
- """
6
-
7
- from fastapi import APIRouter, HTTPException
8
- from datetime import datetime
9
-
10
- def create_status_router(context_engine, files_manager):
11
- """Factory para criar router de status"""
12
- router = APIRouter()
13
-
14
- @router.get("/{batch_id}")
15
- async def get_status(batch_id: str) -> Dict:
16
- """Retorna status de um batch em processamento"""
17
- try:
18
- status = context_engine.obter_status_processamento(batch_id)
19
- if not status:
20
- raise HTTPException(
21
- status_code=404,
22
- detail=f"Batch {batch_id} não encontrado"
23
- )
24
- return status
25
- except Exception as e:
26
- raise HTTPException(status_code=500, detail=str(e))
27
-
28
- @router.get("/download/{batch_id}")
29
- async def get_download_url(batch_id: str) -> Dict:
30
- """Retorna URL de download do TAR.GZ"""
31
- try:
32
- arquivos = files_manager.listar_arquivos_download(limite=10)
33
- for arq in arquivos:
34
- if batch_id in arq['nome']:
35
- return {"url": arq['url']}
36
- raise HTTPException(
37
- status_code=404,
38
- detail=f"Arquivo para batch {batch_id} não encontrado"
39
- )
40
- except Exception as e:
41
- raise HTTPException(status_code=500, detail=str(e))
42
-
43
- return router
44
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/schemas.py DELETED
@@ -1,31 +0,0 @@
1
- """Pydantic schemas para request/response"""
2
- from pydantic import BaseModel, Field
3
- from typing import Optional, List, Dict, Any
4
- from datetime import datetime
5
-
6
- class ProcessJSONLRequest(BaseModel):
7
- """Request para processar JSONL"""
8
- batch_id: Optional[str] = None
9
- enable_specialists: List[int] = Field(default=[1, 2, 3, 4, 5, 6, 7, 8, 9])
10
- parallel: bool = True
11
-
12
- class ProcessResponse(BaseModel):
13
- """Response de processamento"""
14
- batch_id: str
15
- status: str
16
- total_records: int
17
- timestamp: str
18
- message: str
19
-
20
- class SpecialistTestRequest(BaseModel):
21
- """Request para testar especialista"""
22
- specialist_id: int = Field(ge=1, le=9)
23
- acordao_data: Dict[str, Any]
24
-
25
- class DownloadResponse(BaseModel):
26
- """Response de download"""
27
- batch_id: str
28
- filename: str
29
- size_bytes: int
30
- url: str
31
- created_at: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/utils/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Utilitários"""
 
 
api/utils/logger.py DELETED
@@ -1,20 +0,0 @@
1
- """Configuração de logging"""
2
- import logging
3
- import sys
4
- from api.config import settings
5
-
6
- def setup_logger():
7
- """Configura logger global"""
8
- logger = logging.getLogger("para_ai")
9
- logger.setLevel(settings.LOG_LEVEL)
10
-
11
- handler = logging.StreamHandler(sys.stdout)
12
- handler.setLevel(settings.LOG_LEVEL)
13
-
14
- formatter = logging.Formatter(
15
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
- )
17
- handler.setFormatter(formatter)
18
-
19
- logger.addHandler(handler)
20
- return logger
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/utils/validators.py DELETED
@@ -1,16 +0,0 @@
1
- """Validadores de dados"""
2
- from typing import Dict, Any
3
- import json
4
-
5
- def validate_jsonl_line(line: str) -> bool:
6
- """Valida se linha é JSON válido"""
7
- try:
8
- json.loads(line)
9
- return True
10
- except:
11
- return False
12
-
13
- def validate_acordao_data(data: Dict[str, Any]) -> bool:
14
- """Valida estrutura básica de acórdão"""
15
- required_fields = ['ementa', 'integra']
16
- return all(field in data for field in required_fields)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code_copy.py DELETED
@@ -1,71 +0,0 @@
1
- import os
2
- from pathlib import Path
3
-
4
- def consolidar_arquivos(diretorio_base='.', arquivo_saida='código.md'):
5
- """
6
- Itera por todos os arquivos de código em um diretório e subdiretórios,
7
- consolidando-os em um único arquivo Markdown.
8
-
9
- Args:
10
- diretorio_base: Diretório raiz para buscar arquivos (padrão: diretório atual)
11
- arquivo_saida: Nome do arquivo de saída (padrão: código.md)
12
- """
13
- # Extensões de arquivo para processar
14
- extensoes_validas = {'.py', '.sql', '.md', '.json'}
15
-
16
- # Lista para armazenar todos os arquivos encontrados
17
- arquivos_encontrados = []
18
-
19
- # Iterar por todos os arquivos no diretório e subdiretórios
20
- for root, dirs, files in os.walk(diretorio_base):
21
- for file in files:
22
- # Verificar se a extensão do arquivo está na lista
23
- if Path(file).suffix in extensoes_validas:
24
- caminho_completo = os.path.join(root, file)
25
- # Evitar processar o próprio arquivo de saída
26
- if file != arquivo_saida:
27
- arquivos_encontrados.append(caminho_completo)
28
-
29
- # Ordenar arquivos para melhor organização
30
- arquivos_encontrados.sort()
31
-
32
- # Escrever no arquivo de saída
33
- with open(arquivo_saida, 'w', encoding='utf-8') as f_out:
34
- f_out.write(f"# Consolidação de Código\n\n")
35
- f_out.write(f"Total de arquivos processados: {len(arquivos_encontrados)}\n")
36
- f_out.write("---\n\n")
37
-
38
- for caminho_arquivo in arquivos_encontrados:
39
- try:
40
- # Ler o conteúdo do arquivo
41
- with open(caminho_arquivo, 'r', encoding='utf-8') as f_in:
42
- conteudo = f_in.read()
43
-
44
- # Obter a extensão para o bloco de código
45
- extensao = Path(caminho_arquivo).suffix[1:] # Remove o ponto
46
-
47
- # Escrever no formato solicitado
48
- f_out.write(f"\n\n### **Código do arquivo salvo em {caminho_arquivo}\n**")
49
- f_out.write(f"```{extensao}\n")
50
- f_out.write(conteudo)
51
- # Garantir que termina com nova linha
52
- if not conteudo.endswith(''):
53
- f_out.write('')
54
- f_out.write("```\n")
55
- f_out.write("---\n")
56
-
57
- print(f"✓ Processado: {caminho_arquivo}")
58
-
59
- except Exception as e:
60
- print(f"✗ Erro ao processar {caminho_arquivo}: {e}")
61
-
62
- print(f"✓ Consolidação completa! Arquivo gerado: {arquivo_saida}")
63
- print(f"Total de arquivos: {len(arquivos_encontrados)}")
64
-
65
- if __name__ == "__main__":
66
- # Executar a função
67
- consolidar_arquivos()
68
-
69
- # Alternativas de uso:
70
- # consolidar_arquivos('./meu_projeto', 'todos_codigos.md') # Especificar diretório
71
- # consolidar_arquivos(arquivo_saida='backup_codigo.md') # Mudar nome da saída
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
copy_code.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ from collections import defaultdict
4
+
5
+ def normalize_path_for_filename(path):
6
+ """Normaliza o caminho do diretório para usar no nome do arquivo"""
7
+ normalized = path.replace(os.sep, '_').replace('/', '_').replace('\\', '_')
8
+ normalized = normalized.replace(':', '').replace('.', '_')
9
+ if normalized.startswith('_'):
10
+ normalized = normalized[1:]
11
+ if not normalized:
12
+ normalized = 'raiz'
13
+ return normalized
14
+
15
+ def get_language_tag(extension):
16
+ """Retorna a tag de linguagem para o bloco de código markdown"""
17
+ mapping = {
18
+ '.py': 'python',
19
+ '.sql': 'sql',
20
+ '.md': 'markdown',
21
+ '.json': 'json'
22
+ }
23
+ return mapping.get(extension, '')
24
+
25
+ def process_directory(root_dir='.', extensions=['.py', '.sql', '.md', '.json']):
26
+ """
27
+ Processa todos os arquivos do diretório e subdiretórios,
28
+ gerando um arquivo MD por pasta com os arquivos encontrados.
29
+ """
30
+ # Dicionário para agrupar arquivos por diretório
31
+ files_by_dir = defaultdict(list)
32
+
33
+ # Itera por todos os arquivos
34
+ for dirpath, dirnames, filenames in os.walk(root_dir):
35
+ for filename in filenames:
36
+ file_ext = os.path.splitext(filename)[1].lower()
37
+
38
+ # Verifica se a extensão está na lista
39
+ if file_ext in extensions:
40
+ full_path = os.path.join(dirpath, filename)
41
+ files_by_dir[dirpath].append((filename, full_path, file_ext))
42
+
43
+ # Gera um arquivo MD por diretório
44
+ for directory, files in files_by_dir.items():
45
+ # Normaliza o nome do diretório para o arquivo
46
+ normalized_dir = normalize_path_for_filename(directory)
47
+ output_filename = f'cópia_código_{normalized_dir}.md'
48
+
49
+ with open(output_filename, 'w', encoding='utf-8') as output_file:
50
+ output_file.write(f'# Arquivos do diretório: {directory}\n\n')
51
+
52
+ # Ordena arquivos por nome
53
+ files.sort(key=lambda x: x[0])
54
+
55
+ for filename, full_path, file_ext in files:
56
+ # Calcula o caminho relativo
57
+ rel_path = os.path.relpath(full_path, root_dir)
58
+
59
+ # Escreve o cabeçalho do arquivo
60
+ output_file.write(f'### {rel_path}\n')
61
+
62
+ # Lê o conteúdo do arquivo
63
+ try:
64
+ with open(full_path, 'r', encoding='utf-8') as f:
65
+ content = f.read()
66
+
67
+ # Escreve o bloco de código
68
+ lang_tag = get_language_tag(file_ext)
69
+ output_file.write(f'```{lang_tag}\n')
70
+ output_file.write(content)
71
+ if not content.endswith('\n'):
72
+ output_file.write('\n')
73
+ output_file.write('```\n')
74
+
75
+ except Exception as e:
76
+ output_file.write(f'```\n')
77
+ output_file.write(f'Erro ao ler arquivo: {e}\n')
78
+ output_file.write('```\n')
79
+
80
+ output_file.write('---\n\n')
81
+
82
+ print(f'✓ Criado: {output_filename} ({len(files)} arquivos)')
83
+
84
+ print(f'\nTotal: {len(files_by_dir)} diretórios processados')
85
+ return len(files_by_dir)
86
+
87
+ # Exemplo de uso:
88
+ # process_directory('.', ['.py', '.sql', '.md', '.json'])
89
+ #
90
+ # Ou especificando um diretório diferente:
91
+ process_directory('.', ['.py', '.sql', '.md', '.json'])