116 lines
5.1 KiB
Python
116 lines
5.1 KiB
Python
"""Builder para prompts de extracción.
|
|
|
|
Centraliza la construcción de instrucciones reducidas en base a:
|
|
- selección de campos (paths de schema)
|
|
- schema de referencia (ruta->tipo)
|
|
- perfil (invoice/prenuevaeps)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Dict, List, Any
|
|
|
|
from .profiles import get_profile
|
|
|
|
|
|
class PromptBuilder:
|
|
@staticmethod
|
|
def build(selected_fields: List[str], schema: Dict[str, str], profile_name: str | None = None) -> str:
|
|
profile = get_profile(profile_name or "invoice")
|
|
field_schema = {k: schema[k] for k in selected_fields if k in schema}
|
|
json_prompt: Dict[str, Any] = {
|
|
"meta": {
|
|
"perfil": profile.get("name", "invoice"),
|
|
"descripcion": "Extracción estructurada desde OCR de facturas (sin datos personales reales)",
|
|
},
|
|
"instrucciones": [
|
|
"Extrae únicamente los campos solicitados respetando jerarquía y listas",
|
|
"Devuelve EXCLUSIVAMENTE un JSON válido y bien formado, sin texto adicional",
|
|
"No inventes claves nuevas ni cambies nombres existentes",
|
|
"Si un campo no existe, usa null o [] según corresponda",
|
|
"Ten especial cuidado en no confundir el número de autorización con el código"
|
|
],
|
|
"campos_requeridos": selected_fields,
|
|
"schema_referencia": field_schema,
|
|
"reglas_estrictas_nomenclatura": [
|
|
"Usa EXACTAMENTE los nombres listados en 'campos_requeridos'",
|
|
"No utilices sinónimos ni variantes (no cambies tildes ni mayúsculas/minúsculas)",
|
|
"Mantén la estructura de rutas: 'items/*/subcampo' => {'items': [{'subcampo': ...}]}",
|
|
"No cambies nombres de claves por 'equivalentes'",
|
|
"Si un campo no existe, devuelve null (o []) usando el MISMO nombre solicitado",
|
|
],
|
|
"salida": {
|
|
"tipo": "json_object",
|
|
"raiz": None,
|
|
},
|
|
"nota_listas": "Si se pide 'items/*/subtotal', la salida debe ser {'items': [{'subtotal': ...}, {'subtotal': ...}]}",
|
|
}
|
|
return json.dumps(json_prompt, indent=2, ensure_ascii=False)
|
|
|
|
|
|
@staticmethod
|
|
def build_normalization_messages(profile_name: str, template: Dict[str, Any], source_json: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
system_txt = (
|
|
"Eres un asistente que normaliza un JSON a un ESQUEMA CANÓNICO dado. "
|
|
"Debes devolver EXCLUSIVAMENTE un JSON que respete exactamente la estructura y nombres del 'template'. "
|
|
"Reglas: (1) No inventes claves; (2) No uses sinónimos ni variantes; (3) Respeta listas (si 'items' es lista, devuelve un array de objetos); "
|
|
"(4) Si falta un valor, usa null o [] según corresponda; (5) No incluyas texto extra ni comentarios."
|
|
)
|
|
user_obj = {
|
|
"perfil": profile_name,
|
|
"template": template,
|
|
"source_json": source_json,
|
|
"instrucciones": (
|
|
"Mapea los valores del 'source_json' al 'template'. Si hay claves equivalentes en el source, "
|
|
"elige siempre los nombres del template. No devuelvas rutas con '/', sino objetos anidados."
|
|
),
|
|
}
|
|
return [
|
|
{"role": "system", "content": system_txt},
|
|
{"role": "user", "content": json.dumps(user_obj, ensure_ascii=False)},
|
|
]
|
|
|
|
@staticmethod
|
|
def build_for_type(
|
|
doc_type_name: str,
|
|
selected_paths_for_type: List[str],
|
|
schema_for_type: Dict[str, str],
|
|
profile_name: str | None = None,
|
|
) -> str:
|
|
"""Construye un prompt reducido para un Tipo de Documento específico.
|
|
|
|
doc_type_name se usa en meta para trazabilidad; no afecta el esquema.
|
|
"""
|
|
profile = get_profile(profile_name or "invoice")
|
|
field_schema = {k: schema_for_type.get(k) for k in selected_paths_for_type if k in schema_for_type}
|
|
json_prompt: Dict[str, Any] = {
|
|
"meta": {
|
|
"perfil": profile.get("name", "invoice"),
|
|
"tipo_documento": doc_type_name,
|
|
"descripcion": "Extracción estructurada desde OCR (sin datos personales reales)",
|
|
},
|
|
"instrucciones": [
|
|
"Extrae únicamente los campos solicitados respetando jerarquía y listas",
|
|
"Devuelve EXCLUSIVAMENTE un JSON válido y bien formado, sin texto adicional",
|
|
"No inventes claves nuevas ni cambies nombres existentes",
|
|
"Si un campo no existe, usa null o [] según corresponda",
|
|
"Ten especial cuidado en no confundir el número de autorización con el código"
|
|
],
|
|
"campos_requeridos": selected_paths_for_type,
|
|
"schema_referencia": field_schema,
|
|
"reglas_estrictas_nomenclatura": [
|
|
"Usa EXACTAMENTE los nombres listados en 'campos_requeridos'",
|
|
"No utilices sinónimos ni variantes (no cambies tildes ni mayúsculas/minúsculas)",
|
|
"Mantén la estructura de rutas: 'items/*/subcampo' => {'items': [{'subcampo': ...}]}",
|
|
"No cambies nombres de claves por 'equivalentes'",
|
|
"Si un campo no existe, devuelve null (o []) usando el MISMO nombre solicitado",
|
|
],
|
|
"salida": {
|
|
"tipo": "json_object",
|
|
"raiz": None,
|
|
},
|
|
"nota_listas": "Si se pide 'items/*/subtotal', la salida debe ser {'items': [{'subtotal': ...}, {'subtotal': ...}]}",
|
|
}
|
|
return json.dumps(json_prompt, indent=2, ensure_ascii=False)
|
|
|