539 lines
16 KiB
Markdown
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"*
|