Artefacto 1 de 6 · Mes 1 Entregado · CLI en uso real

Texto caótico dentro, forma garantizada fuera.

Un mini-servicio de terminal que recibe una oferta de empleo en prosa y devuelve sus datos como JSON validado, llamando al LLM por SDK directo — sin frameworks de por medio. Primer artefacto de la ruta Data Science → AI Engineering: la llamada cruda, entendida antes de taparla con capas.

9campos en el molde
2SDKs · mismo patrón
0frameworks de por medio
1comando de terminal

La idea

De texto libre a datos manejables

Un LLM por defecto devuelve texto libre: distinto en cada llamada e inservible para un programa. Este artefacto fuerza una salida con forma fija —un molde Pydantic— y la valida antes de devolverla. Ahora el resultado sí es código: if oferta.salario_min >= 40000 funciona, porque el salario es un número, no un párrafo.

Definición de «hecho» Se ejecuta desde fuera del editor y devuelve JSON válido de forma fiable. No vive en un notebook.
El instrumento

La extracción, en vivo

La oferta de abajo es el caso real del repo: oferta_ejemplo.txt, y el JSON es resultado.json tal cual lo produjo la CLI. Pulsa Extraer y mira cómo cada dato se enciende en el texto y aterriza en su campo del molde — es exactamente lo que el artefacto hace en cada llamada.

$ python analizar_oferta.py oferta_ejemplo.txtcaso real del repo

Data Analyst Junior en Barcelona (presencial). Buscamos perfil con SQL, Power BI y Excel avanzado. No se requiere experiencia previa. Salario 24.000 EUR brutos/año. Contrato de prácticas con posibilidad de indefinido.

"puesto":"Data Analyst Junior",✓
"empresa":null,✓
"ubicacion":"Barcelona",✓
"modalidad":"presencial",✓
"seniority":"junior",✓
"stack":["SQL", "Power BI", "Excel"],✓
"salario_min":24000,✓
"salario_max":24000,✓
"contrato":"practicas"✓

9 campos validados contra el molde Oferta. Y los detalles finos: la empresa no aparece en el texto → null explícito, nunca un invento; una sola cifra de salario → mínimo y máximo iguales; «no se requiere experiencia» → seniority: "junior", inferido del contexto.

El patrón

Schema → parse → objeto validado

El molde define qué campos quiero; el SDK obliga al modelo a rellenarlo y lo valida. Lo único específico del dominio son los campos y sus descripciones — el motor es reutilizable para facturas, tickets o contratos.

analizar_oferta.py · el contrato de formaPydantic
class Oferta(BaseModel):
    puesto: str = Field(description="Título del puesto")
    stack: list[str] = Field(description="Tecnologías mencionadas")
    salario_min: int | None = Field(description="Salario mínimo EUR, o null")
    ...

respuesta = client.messages.parse(
    model="claude-opus-4-8",
    messages=[{"role": "user", "content": texto}],
    output_format=Oferta,             # ← se pasa el molde
)
oferta = respuesta.parsed_output      # ← objeto validado, no texto
Dos proveedores, mismo patrón

Claude y OpenAI, lado a lado

La misma extracción con los dos SDKs: cambian tres nombres de parámetro y el id del modelo; el concepto es idéntico. El SDK por debajo es casi igual — todo lo demás es conveniencia.

Anthropicclaude-opus-4-8
client.messages.parse(
  messages=[...],
  output_format=Oferta,
)
.parsed_output
OpenAIgpt-4.1
client.responses.parse(
  input=[...],
  text_format=Oferta,
)
.output_parsed
Lo que se entregó

Los tres criterios, cumplidos

Llamada directa al SDK, con OpenAI y con ClaudeSin framework de por medio; los dos proveedores comparados sobre el mismo molde.
Salida estructurada validada antes de devolverlaMolde Pydantic + validación nativa del SDK; null explícito cuando el dato no está, nunca un invento.
Empaquetado como CLI que de verdad usoargparse · entrada por argumento o fichero · redirección a JSON.
Python 3.13 Pydantic v2 anthropic SDK openai SDK argparse CLI .env · claves protegidas
Por qué este artefacto primero

Antes de tocar frameworks, monté la llamada cruda. Si solo dominas la capa de LangChain, dominas algo que puede desaparecer; si entiendes el SDK por debajo, todo lo demás es conveniencia.