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

539 lines
16 KiB
Markdown

# 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"*