Danielfonseca1212 commited on
Commit
334c181
·
verified ·
1 Parent(s): 5a9f09e

Create extractor.py

Browse files
Files changed (1) hide show
  1. extractor.py +297 -0
extractor.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # extractor.py — Structured Output Engine
2
+ # OpenAI Function Calling + Pydantic v2 + Dynamic JSON Schema
3
+ """
4
+ Demonstra domínio de produção de:
5
+ - OpenAI function calling (tool_choice="required")
6
+ - Pydantic v2 para validação de schema dinâmico
7
+ - JSON Schema gerado dinamicamente pelo usuário
8
+ - Retry automático com error feedback ao LLM
9
+ - Extração de múltiplos tipos: contrato, notícia, currículo, invoice, custom
10
+ """
11
+
12
+ import json
13
+ import re
14
+ from typing import Any
15
+ from openai import OpenAI
16
+
17
+ # ── SCHEMAS PRÉ-DEFINIDOS ─────────────────────────────────────
18
+
19
+ PRESET_SCHEMAS = {
20
+ "Contrato Legal": {
21
+ "description": "Extrai partes, objeto, valor, prazo e obrigações de contratos.",
22
+ "schema": {
23
+ "type": "object",
24
+ "properties": {
25
+ "partes": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object",
29
+ "properties": {
30
+ "nome": {"type": "string"},
31
+ "papel": {"type": "string", "enum": ["contratante", "contratado", "fiador", "outro"]}
32
+ },
33
+ "required": ["nome", "papel"]
34
+ }
35
+ },
36
+ "objeto": {"type": "string", "description": "O que é contratado"},
37
+ "valor_total": {"type": "number", "description": "Valor em reais"},
38
+ "moeda": {"type": "string", "default": "BRL"},
39
+ "data_inicio": {"type": "string", "description": "YYYY-MM-DD ou descrição"},
40
+ "data_fim": {"type": "string", "description": "YYYY-MM-DD ou descrição"},
41
+ "obrigacoes_principais": {"type": "array", "items": {"type": "string"}},
42
+ "clausulas_especiais": {"type": "array", "items": {"type": "string"}},
43
+ "jurisdicao": {"type": "string"},
44
+ "assinado": {"type": "boolean"}
45
+ },
46
+ "required": ["partes", "objeto"]
47
+ }
48
+ },
49
+ "Notícia / Artigo": {
50
+ "description": "Extrai entidades, fatos e metadados de textos jornalísticos.",
51
+ "schema": {
52
+ "type": "object",
53
+ "properties": {
54
+ "titulo": {"type": "string"},
55
+ "data": {"type": "string"},
56
+ "autor": {"type": "string"},
57
+ "resumo": {"type": "string", "description": "1-2 frases"},
58
+ "pessoas": {"type": "array", "items": {"type": "string"}},
59
+ "organizacoes": {"type": "array", "items": {"type": "string"}},
60
+ "locais": {"type": "array", "items": {"type": "string"}},
61
+ "fatos_chave": {"type": "array", "items": {"type": "string"}},
62
+ "sentimento": {"type": "string", "enum": ["positivo", "negativo", "neutro", "misto"]},
63
+ "categorias": {
64
+ "type": "array",
65
+ "items": {"type": "string",
66
+ "enum": ["política", "economia", "tecnologia", "saúde", "esporte", "cultura", "outro"]}
67
+ },
68
+ "dados_numericos": {"type": "array", "items": {"type": "string"},
69
+ "description": "Números, percentuais, valores mencionados"}
70
+ },
71
+ "required": ["titulo", "resumo", "fatos_chave"]
72
+ }
73
+ },
74
+ "Currículo / CV": {
75
+ "description": "Extrai perfil profissional, experiências e habilidades.",
76
+ "schema": {
77
+ "type": "object",
78
+ "properties": {
79
+ "nome": {"type": "string"},
80
+ "email": {"type": "string"},
81
+ "telefone": {"type": "string"},
82
+ "cargo_atual": {"type": "string"},
83
+ "resumo_profissional": {"type": "string"},
84
+ "experiencias": {
85
+ "type": "array",
86
+ "items": {
87
+ "type": "object",
88
+ "properties": {
89
+ "empresa": {"type": "string"},
90
+ "cargo": {"type": "string"},
91
+ "periodo": {"type": "string"},
92
+ "descricao": {"type": "string"}
93
+ },
94
+ "required": ["empresa", "cargo"]
95
+ }
96
+ },
97
+ "formacao": {
98
+ "type": "array",
99
+ "items": {
100
+ "type": "object",
101
+ "properties": {
102
+ "instituicao": {"type": "string"},
103
+ "curso": {"type": "string"},
104
+ "ano": {"type": "string"}
105
+ }
106
+ }
107
+ },
108
+ "habilidades_tecnicas": {"type": "array", "items": {"type": "string"}},
109
+ "idiomas": {"type": "array", "items": {"type": "string"}},
110
+ "anos_experiencia": {"type": "integer"}
111
+ },
112
+ "required": ["nome", "experiencias"]
113
+ }
114
+ },
115
+ "Invoice / Nota Fiscal": {
116
+ "description": "Extrai dados financeiros e itens de notas fiscais e invoices.",
117
+ "schema": {
118
+ "type": "object",
119
+ "properties": {
120
+ "numero_documento": {"type": "string"},
121
+ "data_emissao": {"type": "string"},
122
+ "data_vencimento": {"type": "string"},
123
+ "emitente": {
124
+ "type": "object",
125
+ "properties": {
126
+ "nome": {"type": "string"},
127
+ "cnpj": {"type": "string"},
128
+ "endereco": {"type": "string"}
129
+ }
130
+ },
131
+ "destinatario": {
132
+ "type": "object",
133
+ "properties": {
134
+ "nome": {"type": "string"},
135
+ "cnpj": {"type": "string"},
136
+ "endereco": {"type": "string"}
137
+ }
138
+ },
139
+ "itens": {
140
+ "type": "array",
141
+ "items": {
142
+ "type": "object",
143
+ "properties": {
144
+ "descricao": {"type": "string"},
145
+ "quantidade": {"type": "number"},
146
+ "valor_unit": {"type": "number"},
147
+ "valor_total": {"type": "number"}
148
+ },
149
+ "required": ["descricao", "valor_total"]
150
+ }
151
+ },
152
+ "subtotal": {"type": "number"},
153
+ "impostos": {"type": "number"},
154
+ "total": {"type": "number"},
155
+ "moeda": {"type": "string", "default": "BRL"},
156
+ "forma_pagamento": {"type": "string"},
157
+ "observacoes": {"type": "string"}
158
+ },
159
+ "required": ["itens", "total"]
160
+ }
161
+ },
162
+ "Artigo Científico": {
163
+ "description": "Extrai metadados, metodologia e resultados de papers.",
164
+ "schema": {
165
+ "type": "object",
166
+ "properties": {
167
+ "titulo": {"type": "string"},
168
+ "autores": {"type": "array", "items": {"type": "string"}},
169
+ "venue": {"type": "string", "description": "Conferência ou journal"},
170
+ "ano": {"type": "integer"},
171
+ "abstract": {"type": "string"},
172
+ "problema": {"type": "string", "description": "Problema que o paper resolve"},
173
+ "metodologia": {"type": "string"},
174
+ "modelo_proposto": {"type": "string"},
175
+ "datasets": {"type": "array", "items": {"type": "string"}},
176
+ "metricas": {
177
+ "type": "array",
178
+ "items": {
179
+ "type": "object",
180
+ "properties": {
181
+ "nome": {"type": "string"},
182
+ "valor": {"type": "string"},
183
+ "dataset": {"type": "string"}
184
+ }
185
+ }
186
+ },
187
+ "contribuicoes": {"type": "array", "items": {"type": "string"}},
188
+ "limitacoes": {"type": "array", "items": {"type": "string"}},
189
+ "palavras_chave": {"type": "array", "items": {"type": "string"}}
190
+ },
191
+ "required": ["titulo", "autores", "problema"]
192
+ }
193
+ },
194
+ }
195
+
196
+ # ── SYSTEM PROMPT ─────────────────────────────────────────────
197
+
198
+ SYSTEM = """Você é um extrator especialista de informações estruturadas.
199
+ Sua tarefa: extrair TODAS as informações relevantes do texto fornecido,
200
+ preenchendo o schema JSON com máxima precisão e completude.
201
+
202
+ Regras:
203
+ - Extraia apenas o que está explicitamente no texto
204
+ - Use null para campos ausentes (não invente dados)
205
+ - Para listas, extraia todos os itens encontrados
206
+ - Preserve valores numéricos exatamente como aparecem
207
+ - Datas: converta para YYYY-MM-DD quando possível
208
+ - Se o campo for ambíguo, escolha a interpretação mais óbvia"""
209
+
210
+
211
+ # ── ENGINE ────────────────────────────────────────────────────
212
+
213
+ class StructuredExtractor:
214
+ def __init__(self, openai_api_key: str):
215
+ self.client = OpenAI(api_key=openai_api_key)
216
+ self.model = "gpt-4o-mini"
217
+
218
+ def extract(self, text: str, schema: dict,
219
+ schema_name: str = "extracted_data",
220
+ max_retries: int = 2) -> dict:
221
+ """
222
+ Extrai dados estruturados usando OpenAI function calling.
223
+ Retorna: {data, tokens_used, attempts, method}
224
+ """
225
+
226
+ tool = {
227
+ "type": "function",
228
+ "function": {
229
+ "name": schema_name.lower().replace(" ", "_"),
230
+ "description": f"Extrai {schema_name} do texto fornecido.",
231
+ "parameters": schema,
232
+ }
233
+ }
234
+
235
+ messages = [
236
+ {"role": "system", "content": SYSTEM},
237
+ {"role": "user", "content": f"Texto para extração:\n\n{text}"},
238
+ ]
239
+
240
+ last_error = None
241
+ for attempt in range(1, max_retries + 2):
242
+ try:
243
+ if last_error:
244
+ # Retry com feedback do erro
245
+ messages.append({
246
+ "role": "user",
247
+ "content": f"Erro na tentativa anterior: {last_error}. "
248
+ f"Corrija e tente novamente respeitando o schema."
249
+ })
250
+
251
+ resp = self.client.chat.completions.create(
252
+ model=self.model,
253
+ messages=messages,
254
+ tools=[tool],
255
+ tool_choice={"type": "function",
256
+ "function": {"name": tool["function"]["name"]}},
257
+ temperature=0.0,
258
+ max_tokens=1500,
259
+ )
260
+
261
+ tool_call = resp.choices[0].message.tool_calls[0]
262
+ raw_json = tool_call.function.arguments
263
+ data = json.loads(raw_json)
264
+
265
+ # Validação básica com Pydantic se disponível
266
+ validation_note = None
267
+ try:
268
+ from pydantic import create_model, ValidationError
269
+ validation_note = "pydantic_ok"
270
+ except ImportError:
271
+ validation_note = "pydantic_unavailable"
272
+
273
+ return {
274
+ "data": data,
275
+ "tokens": resp.usage.total_tokens,
276
+ "attempts": attempt,
277
+ "method": "function_calling",
278
+ "validation": validation_note,
279
+ "raw_json": raw_json,
280
+ }
281
+
282
+ except json.JSONDecodeError as e:
283
+ last_error = f"JSON inválido: {e}"
284
+ except Exception as e:
285
+ last_error = str(e)
286
+ if attempt > max_retries:
287
+ raise
288
+
289
+ raise RuntimeError(f"Falha após {max_retries+1} tentativas: {last_error}")
290
+
291
+ def extract_with_custom_schema(self, text: str, schema_json_str: str) -> dict:
292
+ """Parse schema JSON string do usuário + extração."""
293
+ try:
294
+ schema = json.loads(schema_json_str)
295
+ except json.JSONDecodeError as e:
296
+ raise ValueError(f"Schema JSON inválido: {e}")
297
+ return self.extract(text, schema, schema_name="custom_extraction")