"""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)