| Característica | OpenAI Direct | Azure OpenAI (MediSync) |
|---|---|---|
| Autenticación | Solo API Key | ✔ Managed Identity (sin secretos) |
| Residencia de datos | US primariamente | ✔ West Europe (Amsterdam) |
| Cumplimiento HIPAA | ✗ Sin BAA | ✔ BAA firmado con Microsoft |
| GDPR Art. 28 DPA | ✗ Limitado | ✔ Data Processing Agreement completo |
| Aislamiento VNET | ✗ No | ✔ Private endpoints en VNET MediSync |
| Filtrado de contenido | ✗ No | ✔ Configurable por deployment |
| Control de deployments | Compartido | ✔ Recursos propios con quota dedicada |
| Integración AKS | Manual | ✔ Workload Identity nativa |
| Monitoreo / alertas | Dashboard básico | ✔ Azure Monitor + Budget Alerts |
medisync-oai-prodSin API keys en código. El pod de AKS obtiene un token AAD via Workload Identity y lo intercambia por acceso a Cognitive Services.
# ✅ PRODUCCIÓN: Managed Identity — sin API keys en código ni variables de entorno from openai import AzureOpenAI from azure.identity import DefaultAzureCredential, get_bearer_token_provider from functools import lru_cache AZURE_ENDPOINT = "https://medisync-oai-prod.openai.azure.com/" API_VERSION = "2024-10-21" @lru_cache(maxsize=1) def get_azure_client() -> AzureOpenAI: """Singleton con Managed Identity. Funciona en AKS (Workload Identity) y localmente con `az login`. Nunca usa API keys.""" credential = DefaultAzureCredential() token_provider = get_bearer_token_provider( credential, "https://cognitiveservices.azure.com/.default" ) return AzureOpenAI( azure_endpoint=AZURE_ENDPOINT, azure_ad_token_provider=token_provider, api_version=API_VERSION, )
async def resumir_nota_clinica( nota_raw: str, especialidad: str = "medicina general" ) -> AsyncIterator[str]: client = get_azure_client() system = f"""Eres un asistente médico experto en {especialidad}. Extrae del texto clínico: motivo de consulta, antecedentes relevantes, exploración, diagnóstico y plan terapéutico. Formato SOAP estructurado.""" stream = client.chat.completions.create( model="gpt-4o-medisync", messages=[ {"role": "system", "content": system}, {"role": "user", "content": nota_raw} ], max_tokens=800, temperature=0.1, # Bajo: precisión clínica stream=True, ) for chunk in stream: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content
from openai import AzureOpenAI from azure.identity import DefaultAzureCredential import tempfile, pathlib def _whisper_client(): # Whisper usa api_version diferente credential = DefaultAzureCredential() token_prov = get_bearer_token_provider( credential, "https://cognitiveservices.azure.com/.default") return AzureOpenAI( azure_endpoint=AZURE_ENDPOINT, azure_ad_token_provider=token_prov, api_version="2024-06-01", ) def transcribir_consulta( audio_bytes: bytes, idioma: str = "es" ) -> str: client = _whisper_client() with tempfile.NamedTemporaryFile(suffix=".mp3") as tmp: tmp.write(audio_bytes); tmp.flush() with open(tmp.name, "rb") as f: result = client.audio.transcriptions.create( model="whisper-transcribe", file=f, language=idioma, response_format="verbose_json", prompt="Transcripción médica en español" ) return result.text
def vectorizar_sintomas(sintomas: list[str]) -> list[list[float]]: """Genera embeddings para búsqueda de pacientes similares.""" client = get_azure_client() response = client.embeddings.create( model="embed-clinico", input=sintomas, ) return [item.embedding for item in response.data] # Ejemplo real: buscar pacientes con síntomas similares def buscar_pacientes_similares( query: str, top_k: int = 5 ) -> list[dict]: [query_vec] = vectorizar_sintomas([query]) # Azure AI Search: vector search con filtro por clínica results = search_client.search( search_text=None, vector_queries=[{ "kind": "vector", "vector": query_vec, "fields": "embedding", "k": top_k, }], filter="clinica_id eq 'CL-42'" # aislamiento por tenant ) return [r for r in results]
TOOLS = [{ "type": "function", "function": { "name": "generar_informe_alta", "description": "Genera informe alta hospitalaria", "parameters": { "type": "object", "properties": { "diagnostico_principal": {"type": "string"}, "codigo_cie10": {"type": "string"}, "tratamiento_ambulatorio": { "type": "array", "items": {"type": "string"}}, "fecha_revision": {"type": "string", "format": "date"}, }, "required": ["diagnostico_principal", "codigo_cie10"] } } }] def generar_alta(historial: str) -> dict: client = get_azure_client() resp = client.chat.completions.create( model="gpt-4o-medisync", messages=[ {"role": "system", "content": "Médico experto. Extrae datos del alta."}, {"role": "user", "content": historial} ], tools=TOOLS, tool_choice="required" ) import json args = json.loads( resp.choices[0].message.tool_calls[0].function.arguments) return args # dict estructurado → BD MediSync
from openai import BadRequestError import logging, structlog log = structlog.get_logger() async def safe_completion(client, **kwargs) -> str: """Wrapper con manejo de content filter + retry + logging para HIPAA audit trail.""" try: response = client.chat.completions.create(**kwargs) return response.choices[0].message.content except BadRequestError as e: if e.code == "content_filter": # HIPAA: loguear el rechazo sin los datos del paciente log.warning( "content_filter_triggered", deployment=kwargs.get("model"), categories=str(e.error.innererror) if hasattr(e, 'error') else "unknown", request_id=e.request_id, ) raise ContentFilterError("Contenido rechazado por política de seguridad") raise # re-raise otros errores # Ejemplo de uso en endpoint FastAPI: @app.post("/api/v1/resumir-nota") async def resumir_nota(request: NotaRequest, user=Depends(verify_jwt)): client = get_azure_client() texto = await safe_completion( client, model="gpt-4o-medisync", messages=[{"role": "user", "content": request.nota}], max_tokens=600, temperature=0.1, ) return {"resumen": texto, "tokens_usados": ...}
// Hook React para streaming de resúmenes clínicos via Azure OpenAI (backend proxy) import { useState, useCallback } from 'react'; export function useResumenClinico() { const [resumen, setResumen] = useState(''); const [loading, setLoading] = useState(false); const resumir = useCallback(async (notaId: string) => { setLoading(true); setResumen(''); // El backend FastAPI hace la llamada a Azure con Managed Identity const res = await fetch(`/api/v1/resumir-nota/${notaId}`, { headers: { 'Authorization': `Bearer ${getJwt()}` }, }); const reader = res.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; setResumen(prev => prev + decoder.decode(value)); } setLoading(false); }, []); return { resumen, loading, resumir }; }