Ubuntu TechHive
from-go-apis-to-aifeatured-frontends.md
From Go APIs to AI-Featured Frontends
article.detalle

From Go APIs to AI-Featured Frontends

reading.progreso 23 min de lectura

Descripción de De las API de Go a los frontends con IA

Creación de APIs Modernas con Go, OpenAPI y CUE

Introducción

En este curso, exploraremos la potente combinación de Go, OpenAPI y CUE para construir APIs robustas, seguras en tipos y bien documentadas. Este enfoque moderno aprovecha las fortalezas de cada tecnología para crear implementaciones de API mantenibles y validadas.

Tecnologías Clave

Lenguaje de Programación Go

Go proporciona una excelente base para construir servicios web con su:

  • Potente biblioteca estándar

  • Concurrencia integrada

  • Excelentes características de rendimiento

  • Sintaxis clara y mantenible

  • Rico ecosistema de herramientas y paquetes

Paquete HTTP de Go

El paquete net/http sirve como ambos:

Lado del servidor

  • Implementación nativa de servidor HTTP

  • Enrutamiento y manejo de solicitudes

  • Soporte para middleware

  • Utilidades de escritura de respuestas

Lado del cliente

  • Operaciones de cliente HTTP

  • Composición de solicitudes

  • Manejo de respuestas

  • Agrupación de conexiones

Lenguaje CUE

CUE (Configure, Unify, Execute) aporta varias capacidades cruciales:

Definición de Esquemas

  • Definiciones con seguridad de tipo

  • Especificaciones de restricciones

  • Composición y herencia

  • Valores predeterminados

Generación de Esquemas JSON

  • Generación de esquemas OpenAPI 3.0

  • Validación de tipos

  • Generación de documentación

  • Aplicación de contratos

Validación en Tiempo de Ejecución

  • Validación de solicitudes/respuestas

  • Verificaciones de consistencia de datos

  • Mensajería de errores

  • Aserciones de tipo

Objetivos

  • Diseñar APIs utilizando especificaciones OpenAPI 3.0

  • Definir esquemas utilizando el potente sistema de restricciones de CUE

  • Generar documentación OpenAPI a partir de definiciones CUE

  • Implementar APIs utilizando la biblioteca estándar de Go

  • Validar solicitudes y respuestas utilizando CUE

  • Crear implementaciones de API mantenibles y con seguridad de tipo

¿Por qué esta combinación?

Seguridad de Tipo

  • Tipado estático de Go

  • Sistema de restricciones de CUE

  • Definiciones de esquema de OpenAPI

Experiencia del Desarrollador

  • Clara separación de responsabilidades

  • Validación automatizada

  • APIs autodocumentadas

  • Formatos amigables para herramientas

Beneficios de Mantenimiento

  • Fuente única de verdad para tipos

  • Generación automática de esquemas

  • Validación en tiempo de ejecución

  • Definiciones claras de contratos

Guía de Instalación de Herramientas

Configuración del Entorno de Desarrollo

Instalación de Goenv

macOS (usando Homebrew)
brew install goenv
Linux/Unix
git clone https://github.com/go-nv/goenv.git ~/.goenv
Configurar Shell (añadir a ~/.bashrc o ~/.zshrc)
export GOENV_ROOT="$['HOME']/.goenv"
export PATH="$['GOENV_ROOT']/bin:$['PATH']"
eval "$(goenv init -)"

Instalación de Go usando Goenv

Listar versiones disponibles
goenv install --list
Instalar la última versión estable
goenv install 1.23.3
Establecer la versión global de Go
goenv global 1.23.3
Verificar instalación
go version

Instalación de CUE

Usando Go install
go install cuelang.org/go/cmd/cue@latest
Verificar instalación de CUE
cue version

El paquete Go net/http

El paquete Gonet/httpproporciona implementaciones de cliente y servidor HTTP. Se utiliza para realizar llamadas a la API y construir servidores web.

Características clave

  • Soporte para HTTP/1.1, HTTP2

  • Soporte TLS (HTTPS)

  • Abstracción de cliente y servidor HTTP

  • Soporte de enrutamiento a través dehttp.ServeMux

Cliente HTTP

El tipohttp.Clientse utiliza para realizar solicitudes HTTP.

Creación de una solicitud HTTP GET

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}

fmt.Println("Response Body:", string(body))
}


### Creando una solicitud HTTP POST

package main

import (
"bytes"
"fmt"
"net/http"
)

func main() {
jsonData := []byte({"key": "value"})
resp, err := http.Post("https://api.example.com/data", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()

fmt.Println("Response Status:", resp.Status)
}


## Servidor HTTP

El tipo `http.Server` se utiliza para implementar servidores HTTP.

### Servidor HTTP Básico

package main

import (
"fmt"
"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}

func main() {
http.HandleFunc("/hello", helloHandler)
http.ListenAndServe(":8080", nil)
}


### Uso de http.ServeMux para enrutamiento

package main

import (
"fmt"
"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)

server := &http.Server{
Addr: ":8080",
Handler: mux,
}

server.ListenAndServe()
}


## Documentación del paquete Http

[https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)

# Análisis de JSON con el paquete GJSON

## Resumen

- Analizador JSON rápido para Go

- Utiliza sintaxis de rutas para extraer valores de JSON

- No requiere serialización/deserialización

- Cero dependencias

- Seguro para hilos

## Características Clave

### Sintaxis de Ruta

{"name": {"first": "John"}}
// Access with: "name.first" -> "John"


### Tipos Soportados

- Cadenas de texto

- Números

- Booleanos

- Nulo

- Arrays

- Objetos

### Rendimiento

- Análisis muy rápido

- Sin sobrecarga de asignación

- Opera directamente en []byte

## Operaciones Comunes

value := gjson.Get(json, "path.to.value")
value.String() // Obtener como cadena
value.Int() // Obtener como entero
value.Array() // Obtener como array
value.Map() // Obtener como mapa


## Modificadores

- @reverse: Invertir array

- @flatten: Aplanar array

- @join: Unir elementos de array

- @valid: Validar JSON

## Casos de Uso

- Análisis de respuestas de API

- Manejo de configuración

- Extracción de datos JSON

- Operaciones JSON críticas para el rendimiento

## Enlaces de Documentación de GJSON

### Repositorio de GitHub

[https://github.com/tidwall/gjson](https://github.com/tidwall/gjson)

### Documentación de GoDoc

[https://pkg.go.dev/github.com/tidwall/gjson](https://pkg.go.dev/github.com/tidwall/gjson)

# Demo 1: Interactuar con la API gpt-4o de OpenAI desde Go

var (
apiURL = "https://api.openai.com/v1/chat/completions" // reemplazar con el endpoint real
apiKey = os.Getenv("OPENAI_API_KEY")
imagePath = "assets/from-go-apis-to-ai-enhanced-frontends.webp"
prompt = "Describe el paisaje en la imagen."
)

fmt.Println(fmt.Sprintf("Extrayendo información de: %v\n", imagePath))
// Cargar imagen y codificar como base64
imageBytes, err := os.ReadFile(imagePath)
if err != nil {
fmt.Println("Error al leer el archivo de imagen:", err)
return
}
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)

requestBody, err := json.Marshal(map [string]any{
"model": "gpt-4o",
"max_tokens": 4096,
"messages": []map [string]any{
{
"role": "user",
"content": []map [string]any{
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": map [string]any{
"url": fmt.Sprintf("data:image/webp;base64, %s", imageBase64),
},
},
},
},
},
})
if err != nil {
fmt.Println("Error marshalling JSON:", err)
return
}

req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}

responseString := string(body)
// fmt.Println("Response:", responseString)

result := gjson.Get(responseString, "choices.0.message.content")

fmt.Println(result.Str)


# Lenguaje CUE (Configurar Unificar Ejecutar)

## Visión general

CUE es un lenguaje de configuración y restricciones creado por Google para ayudar a gestionar configuraciones complejas y la validación de datos.

## Conceptos clave

### Modelo de datos

- Modelo de mundo abierto

- Herencia y composición

- Fuertemente tipado

- Declarativo

### Características clave

#### Restricciones de tipo

#Person: {
name: string
age: int & >=0 & <=120
email: string & =~"^ [a-zA-Z0-9._%+-]+@ [a-zA-Z0-9.-]+\. [a-zA-Z]{2,}$"
}


#### Restricciones de valor

settings: {
timeout: int & >=0 & <=60
mode: "dev" | "prod" | "stage"
}


## Integración con Go

### Etiquetas de Struct de Go

type Person struct {
Name stringjson:"name" cue:"string"
Age int json:"age" cue:">=0 & <=120"
Email stringjson:"email"
}


### Ejemplo de Validación

import "cuelang.org/go/cue"

const schema = #Person: { name: string age: int & >=0 email: string }

func validate(data interface{}) error {
ctx := cue.context.New()
v := ctx.CompileString(schema)
return v.Validate()
}


## Casos de Uso Comunes

### Gestión de Configuración

#Database: {
host: string
port: int & >=1024 & <=65535
user: string
password: string
}

prod: #Database & {
host: "prod.db.example.com"
port: 5432
user: "admin"
}


### Validación de Datos

#APIConfig: {
endpoints: [...{
path: string & =~"^/"
method: "GET" | "POST" | "PUT" | "DELETE"
auth: bool | *true
}]
}


### Generación de OpenAPI

openapi: "3.0.0"
info: {
title: "My API"
version: "1.0.0"
}


## Herramientas

### Línea de Comandos

- cue eval

- cue fmt

- cue vet

- cue export

### Soporte IDE

- Extensión de VSCode

- Plugin de GoLand/IntelliJ

## Mejores Prácticas

### Definición de Esquema

- Usar el prefijo # para las definiciones

- Mantener las restricciones simples y claras

- Usar nombres significativos

### Manejo de Errores

if err := val.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}


## Recursos

### Documentación Oficial

- https://cuelang.org/docs/

- https://pkg.go.dev/cuelang.org/go/cue

### Recursos de Aprendizaje

- Tutoriales oficiales

- Ejemplos de GitHub

- Guías de la comunidad

## Patrones Comunes

### Valores Predeterminados

#Config: {
debug: bool | *false
port: int | *8080
}


### Composición

#Base: {
version: string
}

#Service: #Base & {
name: string
ports: [...int]
}


## Relación con Go

### Puntos de Integración

- API directa de Go

- Soporte para etiquetas de struct

- Generación de código

- Validación en tiempo de ejecución

### Beneficios

- Seguridad de tipos

- Validación de esquemas

- Gestión de configuración

- Modelado de datos

### Casos de Uso

- Definiciones de API

- Archivos de configuración

- Validación de datos

- Generación de esquemas

## Mensajes de Error

invalid value "foo" (does not match ">=0 & <=120")
conflicting values false and true
field "required" not allowed


## Enlaces a la Documentación del Lenguaje CUE

### Recursos Oficiales

- Sitio Web Principal: [https://cuelang.org/](https://cuelang.org/)

- Documentación Oficial: [https://cuelang.org/docs/](https://cuelang.org/docs/)

- Referencia de la API: [https://pkg.go.dev/cuelang.org/go/cue](https://pkg.go.dev/cuelang.org/go/cue)

### Recursos de GitHub

- Repositorio Principal: [https://github.com/cue-lang/cue](https://github.com/cue-lang/cue)

- CUE Playground: [https://cuelang.org/play/](https://cuelang.org/play/)

### Recursos de Aprendizaje

- Guía de Referencia: [https://cuelang.org/docs/references/](https://cuelang.org/docs/references/)

- Especificación: [https://cuelang.org/docs/references/spec/](https://cuelang.org/docs/references/spec/)

# Demo 2: API Básica con Go y Cue

## Las Entidades

#User: {
id: string
name: string & =~"^ [A-Za-z ]+$"
}


## Entidades como Esquema JSON / Yaml:

rm -rf contracts/basic_schema.yaml # delete if already exists

cue def contracts/user.cue -o contracts/basic_schema.yaml --out openapi+yaml # generate schema


cat contracts/basic_schema.yaml # mostrar el contenido del archivo resultante
openapi: 3.0.0
info:
  title: Generated by cue.
  version: no version
paths: {}
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
        name:
          type: string
          pattern: ^ [A-Za-z ]+$

El Contrato OpenAPI:

Mejorando la salida anterior, incluimos manualmente la definición depaths.

# basic_schema.yaml
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    post:
      summary: Create user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $['ref']: '#/components/schemas/User'
      responses:
        '200':
          description: User created
          content:
            application/json:
              schema:
                $['ref']: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
        name:
          type: string
          pattern: ^ [A-Za-z ]+$

El servidor:

package main

// basic_cueapi.go

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

"cuelang.org/go/cue"
	"cuelang.org/go/cue/cuecontext"
)

type User struct {
ID stringjson:"id"
Name stringjson:"name"
}

const schema = #User: { id: string name: string & =~"^ [A-Za-z ]+$" }

var ctx = cuecontext.New()
var userSchema = ctx.CompileString(schema)

func validateUser(u User) error {
val := ctx.Encode(u)
return val.Unify(userSchema.LookupPath(cue.ParsePath("#User"))).Err()
}

func userHandler(w http.ResponseWriter, r *http.Request) {

var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if err := validateUser(user); err != nil {
fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v", err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}

func main() {
http.HandleFunc("POST /users", userHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}


Probemos esta API básica:

echo "Happy Path Scenario Result:"
echo ""
curl -X POST localhost:8080/users -d '{"id":"1","name":"John Doe"}' # happy path test

echo ""
echo ""

echo "Fail Validation Scenario Result:"
echo ""
curl -X POST localhost:8080/users -d '{"id":"1","name":"1234"}' # fail the schema validation


: Happy Path Scenario Result:
:
: {"id":"1","name":"John Doe"}
:
:
: Fail Validation Scenario Result:
:
: name: invalid value "1234" (out of bound =~"^ [A-Za-z ]+$")


# Demo 3: Interfaz de usuario de Swagger

## Conversión de Yaml a JSON

mkdir -p demos/demo3

go run cli.go y2j contracts/demo2.yaml demos/demo3/openapi.json


: contracts/demo2.yaml convertido exitosamente a demos/demo3/openapi.json


## Servidor Actualizado

package main

import (
"embed"
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
)

//go:embed *
var content embed.FS

type User struct {
ID stringjson:"id"
Name stringjson:"name"
}

const schema = #User: { id: string name: string & =~"^ [A-Za-z ]+$" }

var ctx = cuecontext.New()
var userSchema = ctx.CompileString(schema)

func validateUser(u User) error {
val := ctx.Encode(u)
return val.Unify(userSchema.LookupPath(cue.ParsePath("#User"))).Err()
}

func userHandler(w http.ResponseWriter, r *http.Request) {

var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if err := validateUser(user); err != nil {
fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v", err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}

func main() {
// Puntos finales de la API
http.HandleFunc("POST /users", userHandler)

// Servir la especificación OpenAPI
http.HandleFunc("GET /openapi.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
spec, _ := content.ReadFile("openapi.json")
w.Write(spec)
})

// Servir la interfaz de usuario de Swagger
http.HandleFunc("GET /docs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
tmpl := template.Must(template.New("swagger").Parse(swaggerTemplate))
tmpl.Execute(w, nil)
})

log.Printf("Servidor iniciando en http://localhost:8080")
log.Printf("Documentación de la API disponible en http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}

const swaggerTemplate =`

Documentación de la API

`


# Demo 4: Recuperación de Información de Imagen

## Puntos Destacados

### Gestión del tipo de imagen

El hook de React recupera el tipo de imagen directamente del objeto `File` de la imagen subida y establece el campo oculto

### Gestión del tamaño de la imagen

El tamaño de la imagen se aplica en la API a través de la longitud de cadena `min` y `max`.

Un cálculo aproximado del tamaño base64 de una cadena de 10MB:

- Primero, convierte 10MB a bytes:

- 10MB = 10 * 1024* 1024 = 10,485,760 bytes

- Aplica la fórmula: ceil(n / 3) * 4

- n = 10,485,760

- 10,485,760 / 3 = 3,495,253.33...

- ceil(3,495,253.33...) = 3,495,254

- 3,495,254 * 4 = 13,981,016 bytes

Así, una cadena de `10MB` será aproximadamente `13.98MB` cuando se codifique en base64 (13,981,016 bytes  13.98MB)

## Las Entidades

import "strings"

// Image upload contract
#ImageUpload: {
// Unique identifier
id: string & =~"^ [0-9a-zA-Z -]{36}$"


// Indicación de imagen
	prompt: string & =~"^.{3,100}$" & =~"^ [A-Za-z0-9 -_.]+$"

// Imagen codificada en Base64
	blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}

// Estado de carga de imagen
#ImageUploadStatus: {
	// Identificador único
	id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Indicación de imagen
	prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"

// Estado de carga de imagen
	status: string & strings.MinRunes(3) & strings.MaxRunes(300) & =~"^ [A-Za-z0-9 -_.]+$"
}

Entidades como esquema JSON / Yaml:

rm -rf contracts/image.yaml # eliminar si ya existe

cue def contracts/image.cue -o contracts/image.yaml --out openapi+yaml # generar esquema

cat contracts/image.yaml # mostrar el contenido del archivo resultante
openapi: 3.0.0
info:
  title: Generado por cue.
  version: no version
paths: {}
components:
  schemas:
    ImageUpload:
      description: Contrato de carga de imagen
      type: object
      required:
        - id
        - prompt
        - blob
      properties:
        id:
          description: Identificador único
          type: string
          pattern: ^ [0-9a-zA-Z -]{36}$
        prompt:
          description: Mensaje de imagen
          type: string
          allOf:
            - pattern: ^.{3,100}$
            - pattern: ^ [A-Za-z0-9 -_.]+$
        blob:
          description: Imagen codificada en Base64
          type: string
          minLength: 3
          maxLength: 13900000
          pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
    ImageUploadStatus:
      description: Estado de carga de imagen
      type: object
      required:
        - id
        - prompt

- status
      properties:
        id:
          description: Identificador único
          type: string
          pattern: ^ [0-9a-zA-Z -]{36}$
        prompt:
          description: Mensaje de imagen
          type: string
          minLength: 3
          maxLength: 100
          pattern: ^ [A-Za-z0-9 -_.]+$
        status:
          description: Estado de carga de imagen
          type: string
          minLength: 3
          maxLength: 300
          pattern: ^ [A-Za-z0-9 -_.]+$

El Contrato OpenAPI:

Incluimos manualmente la definición depathsen la salida anterior.

openapi: 3.0.0
info:
  title: Contrato de carga de imagen
  version: 1.0.0
paths:
  /extract-image-info:
    post:
      summary: Extraer información de imagen
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $['ref']: '#/components/schemas/ImageUpload'
      responses:
        '200':
          description: Imagen procesada con éxito
          content:
            application/json:
              schema:
                $['ref']: '#/components/schemas/ImageUploadStatus'
        '400':
          description: Fallo en el procesamiento de la imagen
          content:
            application/json:
              schema:
                $['ref']: '#/components/schemas/ImageUploadStatus'
components:
  schemas:
    ImageUpload:
      description: Contrato de carga de imagen
      type: object
      required:
        - id
        - prompt
        - blob

properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
blob:
description: Imagen codificada en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
ImageUploadStatus:
description: Estado de carga de imagen
type: object
required:
- id
- prompt
- status
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
minLength: 3

maxLength: 100
pattern: ^ [A-Za-z0-9 -.]+$
status:
description: Estado de carga de la imagen
type: string
minLength: 3
maxLength: 300
pattern: ^ [A-Za-z0-9 -
.]+$


## Conversión de Yaml a JSON

mkdir -p demos/demo4

go run cli.go y2j contracts/demo4.yaml demos/demo4/openapi.json


: contracts/demo4.yaml convertido exitosamente a demos/demo4/openapi.json


## El servidor:

package main

import (
"bytes"
"embed"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/tidwall/gjson"
)

var (
apiURL = "https://api.openai.com/v1/chat/completions" // replace with actual endpoint
apiKey = os.Getenv("OPENAI_API_KEY")
)

//go:embed *
var content embed.FS

type ImageUpload struct {
ID stringjson:"id"
Prompt stringjson:"prompt"
Blob stringjson:"blob"
}

type ImageUploadStatus struct {
ID stringjson:"id"
Prompt stringjson:"prompt"
Status stringjson:"status"
}

const schema =`

import "strings"


// Contrato de carga de imagen
#ImageUpload: {
	// Identificador único
	id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Mensaje de imagen
	prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"

// Imagen codificada en Base64
	blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}

// Estado de carga de imagen
#ImageUploadStatus: {
	// Identificador único
	id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Mensaje de imagen
	prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"

// Estado de carga de imagen
	status: string & strings.MinRunes(3) & strings.MaxRunes(300) & =~"^ [A-Za-z0-9 -_.]+$"
}

`

var ctx = cuecontext.New()
var compiledSchema = ctx.CompileString(schema)

func validateImageUpload(p ImageUpload) error {
	val := ctx.Encode(p)
	return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUpload"))).Err()
}

func validateImageUploadStatus(p ImageUploadStatus) error {
	val := ctx.Encode(p)
	return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUploadStatus"))).Err()
}

func processImageUploadHandler(w http.ResponseWriter, r *http.Request) {

var (
		image  ImageUpload
		status ImageUploadStatus
	)
	if err := json.NewDecoder(r.Body).Decode(&image); err != nil {
		fmt.Println(fmt.Errorf("BAD_PAYLOAD:::: +%v", err))
		status = ImageUploadStatus{
			ID:     "bad-id",
			Prompt: "bad-prompt",
			Status: err.Error(),
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(status)
		return
	}

if err := validateImageUpload(image); err != nil {
		fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v with image size: %d", err, len(image.Blob)))
		status = ImageUploadStatus{
			ID:     "bad-id",
			Prompt: "bad-prompt",
			Status: err.Error(),
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(status)
		return
	}

if err, info := getInfoFromImage(image.Blob, image.Prompt); err != nil {
		fmt.Println(fmt.Errorf("INFO_RETRIEVAL_ERROR:::: +%v with image size: %d", err, len(image.Blob)))
		status = ImageUploadStatus{
			ID:     "bad-id",
			Prompt: "bad-prompt",
			Status: err.Error(),
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(status)
		return
	} else {
		w.Header().Set("Content-Type", "application/json")
		status = ImageUploadStatus{
			ID:     image.ID,
			Prompt: image.Prompt,
			Status: info,
		}
		fmt.Println(fmt.Sprintf("Extracted Image Data; +%v", status))
		json.NewEncoder(w).Encode(status)
	}
}

func main() {
	// Puntos finales de la API
	http.HandleFunc("POST /extract-image-info", processImageUploadHandler)

// Servir especificación OpenAPI
	http.HandleFunc("GET /openapi.json", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		spec, _ := content.ReadFile("openapi.json")
		w.Write(spec)
	})

// Servir interfaz de usuario de Swagger
	http.HandleFunc("GET /docs", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		tmpl := template.Must(template.New("swagger").Parse(swaggerTemplate))
		tmpl.Execute(w, nil)
	})

log.Printf("Servidor iniciando en http://localhost:8080")
	log.Printf("Documentación de la API disponible en http://localhost:8080/docs")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

const swaggerTemplate =`

Documentación de la API

`

func getInfoFromImage(imageUrl, prompt string) (error, string) {
	requestBody, err := json.Marshal(map [string]any{
		"model":      "gpt-4o",
		"max_tokens": 4096,
		"messages": []map [string]any{
			{
				"role": "user",
				"content": []map [string]any{
					{
						"type": "text",
						"text": prompt,
					},
					{
						"type": "image_url",
						"image_url": map [string]any{
							"url": imageUrl,
						},
					},
				},
			},
		},
	})
	if err != nil {
		return fmt.Errorf("Error marshalling JSON: %w", err), ""
	}

req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
	if err != nil {
		return fmt.Errorf("Error creating request: %w", err), ""
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("Error making request: %w", err), ""
	}
	defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("Error reading response body: %w", err), ""
	}

responseString := string(body)
	// fmt.Println("Response:", responseString)

result := gjson.Get(responseString, "choices.0.message.content")

response := result.Str
	fmt.Println("Response:", response)

return nil, response
}

Demostración 5: Transmisión Triple

sequenceDiagram
participant LLM as Modelo de Lenguaje Grande
participant BE as Backend
participant UI as Capa de Interfaz de Usuario
participant BR as Navegador

BR->>UI: 1. Solicitud (indicación/interacción)
UI->>BE: 2. Reenviar indicación
BE->>LLM: 3. Llamar a LLM con stream=true
Note right of LLM: LLM genera
tokens incrementalmente
LLM-->>BE: 4. Transmitir tokens parciales
Note right of BE: El backend retransmite los tokens
a medida que llegan
BE-->>UI: 5. Transmitir tokens parciales
UI-->>BR: 6. Enviar tokens parciales al navegador
Note right of BR: El navegador actualiza la interfaz de usuario en tiempo real,
mostrando los tokens a medida que se transmiten

Entidades

import "strings"

// Contrato de carga de imagen
#ImageUpload: {
	// Identificador único
	id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Indicación de imagen
	prompt: string & =~"^.{3,100}$" & =~"^ [A-Za-z0-9 -_.]+$"

// Transmisión habilitada
	stream: bool

// Imagen codificada en Base64
	blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}

// Contrato de información de imagen
#ImageInfo: {
	// Información de imagen
	info: string
}

## Entidades como esquema JSON / Yaml:

rm -rf contracts/image2.yaml # eliminar si ya existe

cue def contracts/image2.cue -o contracts/image2.yaml --out openapi+yaml # generar esquema

cat contracts/image2.yaml # mostrar el contenido del archivo resultante


openapi: 3.0.0
info:
title: Generado por cue.
version: sin versión
paths: {}
components:
schemas:
ImageInfo:
description: Contrato de información de imagen
type: object
required:
- info
properties:
info:
description: Información de imagen
type: string
ImageUpload:
description: Contrato de carga de imagen
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt de imagen
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Stream habilitado
type: boolean
blob:
description: Imagen codificada en Base64


type: string
          minLength: 3
          maxLength: 13900000
          pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$

El Contrato OpenAPI:

Incluimos manualmente la definición de paths en la salida anterior.

openapi: 3.0.0
info:
  title: Generated by cue.
  version: no version
paths:
  /extract-image-info:
    post:
      summary: Extraer información de la imagen
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $['ref']: '#/components/schemas/ImageUpload'
      responses:
        '200':
          description: Imagen procesada correctamente
          content:
            application/json:
              schema:
                $['ref']: '#/components/schemas/ImageInfo'
        '400':
          description: Falló el procesamiento de la imagen
          content:
            application/json:
              schema:
                $['ref']: '#/components/schemas/ImageInfo'
components:
  schemas:
    ImageInfo:
      description: Contrato de información de imagen
      type: object
      required:
        - info
      properties:
        info:
          description: Información de la imagen

type: string
ImageUpload:
description: Contrato de carga de imagen
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Transmisión habilitada
type: boolean
blob:
description: Imagen codificada en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$


## Conversión de Yaml a JSON

mkdir -p demos/demo5

go run cli.go y2j contracts/demo5.yaml demos/demo5/openapi.json


: contracts/demo5.yaml se ha convertido correctamente a demos/demo5/openapi.json


## El servidor:

package main

import (
"bufio"
"bytes"
"embed"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"strings"
"time"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/tidwall/gjson"
)

var (
apiURL = "https://api.openai.com/v1/chat/completions" // reemplazar con el endpoint real
apiKey = os.Getenv("OPENAI_API_KEY")
)

//go:embed *
var content embed.FS

type ImageUpload struct {
ID stringjson:"id"
Prompt stringjson:"prompt"
Stream bool json:"stream"
Blob stringjson:"blob"
}

type ImageInfo struct {
Info stringjson:"info"
}

const schema =`

import "strings"

// Contrato de carga de imagen
#ImageUpload: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Mensaje de imagen
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"

// Transmisión habilitada
stream: bool

// Imagen codificada en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}

// Contrato de información de imagen
#ImageInfo: {
// Información de imagen
info: string
}

`

var ctx = cuecontext.New()
var compiledSchema = ctx.CompileString(schema)

func validateImageUpload(p ImageUpload) error {
val := ctx.Encode(p)
return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUpload"))).Err()
}

func validateImageInfoStatus(p ImageInfo) error {
val := ctx.Encode(p)
return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUploadStatus"))).Err()
}

func processImageUploadHandler(w http.ResponseWriter, r *http.Request) {

var (
image ImageUpload
status ImageInfo
)
if err := json.NewDecoder(r.Body).Decode(&image); err != nil {
fmt.Println(fmt.Errorf("BAD_PAYLOAD:::: +%v", err))
status = ImageInfo{
Info: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
}

if err := validateImageUpload(image); err != nil {
fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v with image size: %d", err, len(image.Blob)))
status = ImageInfo{
Info: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
}

if image.Stream {
// Establece los encabezados CORS para permitir todos los orígenes. Es posible que desees restringir esto a orígenes específicos en un entorno de producción.
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
//w.Header().Set("Access-Control-Max-Age", "3600")
// Establece el tipo de contenido a text/event-stream
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

if err := getInfoFromImageStreaming(w, image.Blob, image.Prompt); err != nil {
fmt.Println(fmt.Errorf("INFO_RETRIEVAL_ERROR:::: +%v with image size: %d", err, len(image.Blob)))
fmt.Fprintf(w, "data: %s\n\n", err.Error())
w.(http.Flusher).Flush()
fmt.Fprintf(w, "data: %s\n\n", " [DONE]")
w.(http.Flusher).Flush()
return
}
} else {
if err, info := getInfoFromImage(image.Blob, image.Prompt); err != nil {
fmt.Println(fmt.Errorf("INFO_RETRIEVAL_ERROR:::: +%v with image size: %d", err, len(image.Blob)))
status = ImageInfo{
Info: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
} else {
w.Header().Set("Content-Type", "application/json")
status = ImageInfo{
Info: info,
}
fmt.Println(fmt.Sprintf("Extracted Image Data; +%v", status))

json.NewEncoder(w).Encode(status)
}
}
}

func main() {
// API endpoints
http.HandleFunc("POST /extract-image-info", processImageUploadHandler)

// Serve OpenAPI spec
http.HandleFunc("GET /openapi.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
spec, _ := content.ReadFile("openapi.json")
w.Write(spec)
})

// Serve Swagger UI
http.HandleFunc("GET /docs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
tmpl := template.Must(template.New("swagger").Parse(swaggerTemplate))
tmpl.Execute(w, nil)
})

log.Printf("Server starting on http://localhost:8080")
log.Printf("API documentation available at http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}

const swaggerTemplate =`

Documentación de la API

`

func getInfoFromImageStreaming(w http.ResponseWriter, imageUrl, prompt string) error {
requestBody, err := json.Marshal(map [string]any{
"model": "gpt-4o",
"max_tokens": 4096,
"messages": []map [string]any{
{
"role": "user",
"content": []map [string]any{
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": map [string]any{
"url": imageUrl,
},
},
},
},
},
"stream": true, // Enable streaming
})
if err != nil {
return fmt.Errorf("Error marshalling JSON: %w", err)
}

req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
if err != nil {
return fmt.Errorf("Error creating request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Error al realizar la solicitud: %w", err)
}
defer resp.Body.Close()

// Comprobar el código de estado no OK
if resp.StatusCode != http.StatusOK {
respBody, _ := bufio.NewReader(resp.Body).ReadString('\n')
fmt.Printf("Código de estado no OK: %d\nCuerpo: %s\n", resp.StatusCode, respBody)
return fmt.Errorf("Código de estado no OK: %d", resp.StatusCode)
}

// Usar bufio para leer la respuesta línea por línea
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()

// OpenAI transmite cada fragmento prefijado por "data:"
if !strings.HasPrefix(line, "data:") {
continue
}

// "data: [DONE]" indica el final del flujo
jsonData := strings.TrimPrefix(line, "data: ")
if jsonData == " [DONE]" {
fmt.Println("\n--- Stream finished ---")
fmt.Fprintf(w, "data: %s\n\n", " [DONE]")
w.(http.Flusher).Flush()
break
}

// Analizar cada fragmento usando gjson
result := gjson.Parse(jsonData)

// El contenido relevante está en "choices" -> array -> "delta" -> "content"
// p. ej., result.Get("choices.0.delta.content")
content := ""
for _, choice := range result.Get("choices").Array() {
contentDelta := choice.Get("delta.content").String()
content += contentDelta
}
// Imprimir el fragmento de contenido (si lo hay)
if content != "" {
fmt.Print(content)
fmt.Fprintf(w, "data: %s\n\n", content)
w.(http.Flusher).Flush()
}

// Esperar un poco para simular el tiempo de procesamiento
time.Sleep(100 * time.Millisecond)
}

if err := scanner.Err(); err != nil {
return fmt.Errorf("Error reading streamed response body: %w", err)
}

return nil
}

func getInfoFromImage(imageUrl, prompt string) (error, string) {
requestBody, err := json.Marshal(map [string]any{
"model": "gpt-4o",
"max_tokens": 4096,
"messages": []map [string]any{
{
"role": "user",
"content": []map [string]any{
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": map [string]any{
"url": imageUrl,
},
},
},
},
},
})
if err != nil {
return fmt.Errorf("Error marshalling JSON: %w", err), ""
}

req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
	if err != nil {
		return fmt.Errorf("Error creating request: %w", err), ""
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("Error making request: %w", err), ""
	}
	defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("Error reading response body: %w", err), ""
	}

responseString := string(body)
	// fmt.Println("Response:", responseString)

result := gjson.Get(responseString, "choices.0.message.content")

response := result.Str
	fmt.Println("Response:", response)

return nil, response
}

Referencias