Files
system-docs/v4-archive/contratos-comunes/docs/IMPLEMENTACION.md
2025-12-24 17:28:34 +00:00

16 KiB

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

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

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

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

// 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

// 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

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"