feat: Update docker-compose configuration to use environment variables for sensitive data, enhance testing documentation with new LaTeX files for capacity planning, and refine README for clarity on compilation and testing processes.

This commit is contained in:
erik
2026-07-17 08:32:00 -05:00
parent e47ef4f8c6
commit dc3fcc2280
89 changed files with 5331 additions and 1210 deletions
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Mock OpenAI-compatible para ST-EXT-01 (cola ARQ sin coste LLM real).
Endpoints:
GET /v1/models
POST /v1/chat/completions
Responde JSON fijo con delay configurable (default 50 ms).
Ejemplo:
python docs/plan_pruebas/scripts/llm_mock_server.py --port 8099 --delay-ms 50
Apuntar extraction/workers (Docker):
EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1
NORMALIZER_BASE_URL=http://host.docker.internal:8099/v1
EXTRACTOR_API_KEY=mock-key
ASYNC_QUEUE=true
"""
from __future__ import annotations
import argparse
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
MOCK_SCHEMA = {
"numero_factura": "str",
"fecha": "str",
"nit_prestador": "str",
"razon_social": "str",
"valor_total": "number",
}
MOCK_EXTRACTION = {
"numero_factura": "MOCK-0001",
"fecha": "2026-01-15",
"nit_prestador": "900000000",
"razon_social": "IPS MOCK ST-EXT-01",
"valor_total": 1000,
}
class Stats:
def __init__(self) -> None:
self.lock = threading.Lock()
self.completions = 0
self.models = 0
def inc(self, kind: str) -> None:
with self.lock:
if kind == "completions":
self.completions += 1
else:
self.models += 1
STATS = Stats()
def make_handler(delay_ms: float):
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt: str, *args) -> None: # quieter
pass
def _json(self, code: int, payload: dict) -> None:
body = json.dumps(payload).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None: # noqa: N802
path = urlparse(self.path).path.rstrip("/")
if path in ("/v1/models", "/models"):
STATS.inc("models")
self._json(
200,
{
"object": "list",
"data": [
{"id": "mock-llm", "object": "model", "owned_by": "st-ext-01"},
{"id": "gpt-5.4-mini", "object": "model", "owned_by": "st-ext-01"},
],
},
)
return
if path in ("/health", "/v1/health"):
self._json(200, {"status": "ok", "completions": STATS.completions})
return
self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}})
def do_POST(self) -> None: # noqa: N802
path = urlparse(self.path).path.rstrip("/")
length = int(self.headers.get("Content-Length") or 0)
raw = self.rfile.read(length) if length else b"{}"
try:
req = json.loads(raw.decode("utf-8") or "{}")
except Exception:
req = {}
if path not in ("/v1/chat/completions", "/chat/completions"):
self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}})
return
if delay_ms > 0:
time.sleep(delay_ms / 1000.0)
# Heurística: schema discovery suele pedir "campos" / schema; extracción pide factura.
messages = req.get("messages") or []
blob = json.dumps(messages, ensure_ascii=False).lower()
if "schema" in blob or "campos" in blob or "field" in blob or "descub" in blob:
content = json.dumps(MOCK_SCHEMA, ensure_ascii=False)
else:
content = json.dumps(MOCK_EXTRACTION, ensure_ascii=False)
model = req.get("model") or "mock-llm"
STATS.inc("completions")
self._json(
200,
{
"id": f"chatcmpl-mock-{STATS.completions}",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": content},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150,
},
},
)
return Handler
def main() -> int:
p = argparse.ArgumentParser(description="Mock OpenAI API for ST-EXT-01")
p.add_argument("--host", default="0.0.0.0")
p.add_argument("--port", type=int, default=int(__import__("os").environ.get("LLM_MOCK_PORT", "8099")))
p.add_argument("--delay-ms", type=float, default=float(__import__("os").environ.get("LLM_MOCK_DELAY_MS", "50")))
args = p.parse_args()
httpd = ThreadingHTTPServer((args.host, args.port), make_handler(args.delay_ms))
print(f"[llm-mock] http://{args.host}:{args.port}/v1 delay={args.delay_ms}ms")
print(f"[llm-mock] health: http://127.0.0.1:{args.port}/health")
print("[llm-mock] Ctrl+C para detener")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n[llm-mock] stopped")
return 0
if __name__ == "__main__":
raise SystemExit(main())