Files
IPSExtractor/application/prompts.py
T

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)