# IMPLEMENTACION - Wrappers y Ejemplos **Version:** 2.0 **Dependencia:** `S-CONTRACT.md` **Estado:** Referencia --- # 1. Introduccion Este documento proporciona implementaciones de referencia para: - Construccion de requests S-CONTRACT - Validacion de responses - Integracion con Alfred (n8n) --- # 2. ContractBuilder (Python) ## 2.1 Clase Principal ```python import uuid import hashlib from datetime import datetime, timezone from typing import Optional, Dict, Any, List from dataclasses import dataclass, asdict @dataclass class ContractBuilder: """ Constructor de requests S-CONTRACT v2.0. """ contract_version: str = "2.0" profile: str = "FULL" # Envelope trace_id: Optional[str] = None parent_trace_id: Optional[str] = None step_id: Optional[str] = None step_index: int = 1 total_steps: Optional[int] = None idempotency_key: Optional[str] = None ttl_ms: int = 30000 provider_timeout_ms: int = 15000 # Routing module: Optional[str] = None module_version: Optional[str] = None provider_preference: Optional[List[str]] = None fallback_chain: Optional[List[str]] = None max_fallback_level: int = 2 # Context lang: str = "es" mode: str = "strict" pii_filter: bool = False bandera_id: Optional[str] = None player_id: Optional[str] = None method_hash: Optional[str] = None human_readable: Optional[str] = None # Payload payload_type: str = "text" encoding: str = "utf-8" content: Optional[str] = None content_hash: Optional[str] = None schema_expected: Optional[str] = None # Batch is_batch: bool = False batch_id: Optional[str] = None item_index: Optional[int] = None items_total: int = 1 batch_mode: str = "SEQUENTIAL" # Storage input_location: str = "internal" input_ref: Optional[str] = None output_location: str = "internal" output_ref: Optional[str] = None persist_intermediate: bool = True retention_days: int = 30 # Security encryption_profile: str = "NONE" data_sensitivity: str = "LOW" key_vault_ref: Optional[str] = None pii_detected: bool = False gdpr_relevant: bool = False def __post_init__(self): # Auto-generate IDs if not provided if not self.trace_id: self.trace_id = str(uuid.uuid4()) if not self.step_id: self.step_id = str(uuid.uuid4()) def set_content(self, content: str) -> 'ContractBuilder': """Sets content and auto-calculates hash.""" self.content = content self.content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest() if not self.idempotency_key: self.idempotency_key = self.content_hash return self def for_module(self, module: str, version: str = "1.0") -> 'ContractBuilder': """Sets target module.""" self.module = module self.module_version = version return self def with_fallback(self, chain: List[str]) -> 'ContractBuilder': """Sets fallback chain.""" self.fallback_chain = chain return self def lite(self) -> 'ContractBuilder': """Switches to LITE profile.""" self.profile = "LITE" return self def build(self) -> Dict[str, Any]: """Builds the final request dictionary.""" timestamp = datetime.now(timezone.utc).isoformat() if self.profile == "LITE": return self._build_lite(timestamp) return self._build_full(timestamp) def _build_lite(self, timestamp: str) -> Dict[str, Any]: return { "contract_version": self.contract_version, "profile": "LITE", "envelope": { "trace_id": self.trace_id, "idempotency_key": self.idempotency_key }, "routing": { "module": self.module }, "context": { "lang": self.lang, "mode": self.mode }, "payload": { "type": self.payload_type, "encoding": self.encoding, "content": self.content } } def _build_full(self, timestamp: str) -> Dict[str, Any]: return { "contract_version": self.contract_version, "profile": "FULL", "envelope": { "trace_id": self.trace_id, "parent_trace_id": self.parent_trace_id, "step_id": self.step_id, "step_index": self.step_index, "total_steps": self.total_steps, "idempotency_key": self.idempotency_key, "timestamp_init": timestamp, "ttl_ms": self.ttl_ms, "provider_timeout_ms": self.provider_timeout_ms }, "routing": { "module": self.module, "version": self.module_version, "provider_preference": self.provider_preference, "fallback_chain": self.fallback_chain, "max_fallback_level": self.max_fallback_level }, "context": { "lang": self.lang, "mode": self.mode, "pii_filter": self.pii_filter, "bandera_id": self.bandera_id, "player_id": self.player_id, "method_hash": self.method_hash, "human_readable": self.human_readable }, "payload": { "type": self.payload_type, "encoding": self.encoding, "content": self.content, "content_hash": self.content_hash, "schema_expected": self.schema_expected }, "batch": { "is_batch": self.is_batch, "batch_id": self.batch_id, "item_index": self.item_index, "items_total": self.items_total, "batch_mode": self.batch_mode }, "storage": { "input_location": self.input_location, "input_ref": self.input_ref, "output_location": self.output_location, "output_ref": self.output_ref, "persist_intermediate": self.persist_intermediate, "retention_days": self.retention_days }, "security": { "encryption_profile": self.encryption_profile, "data_sensitivity": self.data_sensitivity, "key_vault_ref": self.key_vault_ref, "pii_detected": self.pii_detected, "gdpr_relevant": self.gdpr_relevant } } ``` ## 2.2 Uso ```python # Request LITE para clasificacion request = ( ContractBuilder() .for_module("CLASSIFIER") .set_content("Factura de Telefonica por 45.99 EUR") .lite() .build() ) # Request FULL con fallback request = ( ContractBuilder() .for_module("OCR_CORE", "1.0") .set_content(base64_image) .with_fallback(["OCR_LOCAL", "OCR_GROQ", "OCR_OPENAI"]) .build() ) ``` --- # 3. ResponseValidator (Python) ```python from typing import Dict, Any, List, Optional from dataclasses import dataclass @dataclass class ValidationResult: valid: bool errors: List[str] warnings: List[str] class ResponseValidator: """ Validador de responses S-CONTRACT v2.0. """ VALID_STATUS_CODES = {'SUCCESS', 'PARTIAL', 'ERROR', 'TIMEOUT', 'FALLBACK'} REQUIRED_LITE = {'contract_version', 'profile', 'envelope', 'status', 'result'} REQUIRED_FULL = REQUIRED_LITE | {'quality', 'metadata', 'storage', 'audit'} def validate(self, response: Dict[str, Any]) -> ValidationResult: errors = [] warnings = [] # Contract version if response.get('contract_version') != '2.0': warnings.append(f"Contract version mismatch: {response.get('contract_version')}") # Profile profile = response.get('profile', 'FULL') required = self.REQUIRED_LITE if profile == 'LITE' else self.REQUIRED_FULL # Required fields for field in required: if field not in response: errors.append(f"Missing required field: {field}") # Status validation status = response.get('status', {}) if isinstance(status, dict): code = status.get('code') if code not in self.VALID_STATUS_CODES: errors.append(f"Invalid status code: {code}") else: errors.append("Status must be an object with 'code' field") # Envelope validation envelope = response.get('envelope', {}) if not envelope.get('trace_id'): errors.append("Missing trace_id in envelope") if not envelope.get('idempotency_key'): warnings.append("Missing idempotency_key in envelope") # Quality validation (FULL profile) if profile == 'FULL': quality = response.get('quality', {}) confidence = quality.get('confidence') if confidence is not None: if not (0 <= confidence <= 1): errors.append(f"Confidence out of range: {confidence}") return ValidationResult( valid=len(errors) == 0, errors=errors, warnings=warnings ) ``` --- # 4. Integracion n8n (Alfred) ## 4.1 Nodo Function: Build Request ```javascript // n8n Function node: Build S-CONTRACT Request const crypto = require('crypto'); function buildRequest(input, module, profile = 'LITE') { const content = typeof input === 'string' ? input : JSON.stringify(input); const contentHash = crypto.createHash('sha256').update(content).digest('hex'); const traceId = $node.data.trace_id || crypto.randomUUID(); if (profile === 'LITE') { return { contract_version: '2.0', profile: 'LITE', envelope: { trace_id: traceId, idempotency_key: contentHash }, routing: { module }, context: { lang: 'es', mode: 'strict' }, payload: { type: 'text', encoding: 'utf-8', content: content } }; } return { contract_version: '2.0', profile: 'FULL', envelope: { trace_id: traceId, step_id: crypto.randomUUID(), step_index: 1, idempotency_key: contentHash, timestamp_init: new Date().toISOString(), ttl_ms: 30000 }, routing: { module, fallback_chain: $node.data.fallback_chain || [] }, context: { lang: 'es', mode: 'strict', player_id: $node.data.player_id, human_readable: $node.data.description }, payload: { type: 'text', encoding: 'utf-8', content: content, content_hash: contentHash }, batch: { is_batch: false, items_total: 1 }, storage: { input_location: 'internal', persist_intermediate: true, retention_days: 30 }, security: { encryption_profile: 'NONE', data_sensitivity: 'LOW' } }; } // Usage const request = buildRequest( $input.first().json.text, 'CLASSIFIER', 'LITE' ); return [{ json: request }]; ``` ## 4.2 Nodo Function: Validate Response ```javascript // n8n Function node: Validate S-CONTRACT Response function validateResponse(response) { const errors = []; const warnings = []; // Status check const status = response.status?.code; const validStatus = ['SUCCESS', 'PARTIAL', 'ERROR', 'TIMEOUT', 'FALLBACK']; if (!validStatus.includes(status)) { errors.push(`Invalid status: ${status}`); } // Envelope check if (!response.envelope?.trace_id) { errors.push('Missing trace_id'); } // Result check if (status === 'SUCCESS' && !response.result?.data) { warnings.push('SUCCESS status but no result data'); } return { valid: errors.length === 0, errors, warnings, trace_id: response.envelope?.trace_id, status: status, result: response.result?.data }; } const validation = validateResponse($input.first().json); if (!validation.valid) { throw new Error(`Contract validation failed: ${validation.errors.join(', ')}`); } return [{ json: validation }]; ``` --- # 5. Logging a SYS_LOG ```python import psycopg2 from psycopg2.extras import Json from datetime import datetime def log_to_syslog(conn, request: dict, response: dict): """ Inserta registro en SYS_LOG. """ envelope = request.get('envelope', {}) r_envelope = response.get('envelope', {}) status = response.get('status', {}) quality = response.get('quality', {}) metadata = response.get('metadata', {}) sql = """ INSERT INTO SYS_LOG ( trace_id, step_id, idempotency_key, step_index, step_type, profile, timestamp_created, timestamp_started, timestamp_completed, duration_ms, module_name, module_version, provider_used, fallback_level, model_id, status_code, input_hash, input_ref, input_type, output_hash, output_ref, confidence, coverage, tokens_input, tokens_output, cost_units ) VALUES ( %(trace_id)s, %(step_id)s, %(idempotency_key)s, %(step_index)s, %(step_type)s, %(profile)s, %(timestamp_created)s, %(timestamp_started)s, %(timestamp_completed)s, %(duration_ms)s, %(module_name)s, %(module_version)s, %(provider_used)s, %(fallback_level)s, %(model_id)s, %(status_code)s, %(input_hash)s, %(input_ref)s, %(input_type)s, %(output_hash)s, %(output_ref)s, %(confidence)s, %(coverage)s, %(tokens_input)s, %(tokens_output)s, %(cost_units)s ) """ params = { 'trace_id': envelope.get('trace_id'), 'step_id': envelope.get('step_id'), 'idempotency_key': envelope.get('idempotency_key'), 'step_index': envelope.get('step_index', 1), 'step_type': 'TRANSFORM', 'profile': request.get('profile', 'FULL'), 'timestamp_created': datetime.now(), 'timestamp_started': envelope.get('timestamp_init'), 'timestamp_completed': r_envelope.get('timestamp_end'), 'duration_ms': metadata.get('processing_ms'), 'module_name': request.get('routing', {}).get('module'), 'module_version': request.get('routing', {}).get('version'), 'provider_used': status.get('provider_used'), 'fallback_level': status.get('fallback_level_used', 0), 'model_id': metadata.get('model_id'), 'status_code': status.get('code'), 'input_hash': request.get('payload', {}).get('content_hash'), 'input_ref': request.get('storage', {}).get('input_ref'), 'input_type': request.get('payload', {}).get('type'), 'output_hash': response.get('storage', {}).get('output_hash'), 'output_ref': response.get('storage', {}).get('output_ref'), 'confidence': quality.get('confidence'), 'coverage': quality.get('coverage'), 'tokens_input': quality.get('tokens_input'), 'tokens_output': quality.get('tokens_output'), 'cost_units': metadata.get('cost_units') } with conn.cursor() as cur: cur.execute(sql, params) conn.commit() ``` --- # 6. Checklist de Implementacion - [ ] Implementar ContractBuilder en Python - [ ] Implementar ContractBuilder en JavaScript (n8n) - [ ] Implementar ResponseValidator - [ ] Crear funcion log_to_syslog - [ ] Configurar nodos n8n con templates - [ ] Probar flujo completo LITE - [ ] Probar flujo completo FULL - [ ] Probar fallback chain --- **Fin del Documento IMPLEMENTACION - Version 2.0** *Sistema GRACE - "Alfred Decide, GRACE Transforma"*