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

From Go APIs to AI-Featured Frontends

reading.progression 23 min de lecture

Description de Des API Go aux frontends dotés d'IA

Construction d'API Modernes avec Go, OpenAPI et CUE

Introduction

Dans ce cours, nous explorerons la puissante combinaison de Go, OpenAPI et CUE pour construire des API robustes, typées et bien documentées. Cette approche moderne tire parti des forces de chaque technologie pour créer des implémentations d'API maintenables et validées.

Technologies Clés

Langage de Programmation Go

Go offre une excellente base pour la construction de services web grâce à :

  • Sa solide bibliothèque standard

  • Sa concurrence intégrée

  • Ses excellentes caractéristiques de performance

  • Sa syntaxe claire et maintenable

  • Son riche écosystème d'outils et de packages

Le Package HTTP de Go

Le package net/http sert à la fois de :

Côté Serveur

  • Implémentation native de serveur HTTP

  • Routage et gestion des requêtes

  • Support des middlewares

  • Utilitaires d'écriture de réponses

Côté Client

  • Opérations client HTTP

  • Composition des requêtes

  • Gestion des réponses

  • Mise en commun des connexions

Langage CUE

CUE (Configure, Unify, Execute) apporte plusieurs capacités cruciales :

Définition de Schéma

  • Définitions de type sûr

  • Spécifications de contraintes

  • Composition et héritage

  • Valeurs par défaut

Génération de Schéma JSON

  • Génération de schéma OpenAPI 3.0

  • Validation de type

  • Génération de documentation

  • Application des contrats

Validation à l'exécution

  • Validation des requêtes/réponses

  • Vérifications de cohérence des données

  • Messagerie d'erreur

  • Assertions de type

Objectifs

  • Concevoir des API en utilisant les spécifications OpenAPI 3.0

  • Définir des schémas en utilisant le puissant système de contraintes de CUE

  • Générer de la documentation OpenAPI à partir des définitions CUE

  • Implémenter des API en utilisant la bibliothèque standard de Go

  • Valider les requêtes et les réponses en utilisant CUE

  • Créer des implémentations d'API maintenables et de type sûr

Pourquoi cette combinaison ?

Sécurité des types

  • Typage statique de Go

  • Système de contraintes de CUE

  • Définitions de schémas d'OpenAPI

Expérience Développeur

  • Séparation claire des préoccupations

  • Validation automatisée

  • API auto-documentées

  • Formats adaptés aux outils

Avantages de la Maintenance

  • Source unique de vérité pour les types

  • Génération de schémas automatisée

  • Validation à l'exécution

  • Définitions de contrats claires

Guide d'Installation des Outils

Configuration de l'Environnement de Développement

Installation de Goenv

macOS (avec Homebrew)
brew install goenv
Linux/Unix
git clone https://github.com/go-nv/goenv.git ~/.goenv
Configurer le Shell (ajouter à ~/.bashrc ou ~/.zshrc)
export GOENV_ROOT="$['HOME']/.goenv"
export PATH="$['GOENV_ROOT']/bin:$['PATH']"
eval "$(goenv init -)"

Installation de Go avec Goenv

Lister les versions disponibles
goenv install --list
Installer la dernière version stable
goenv install 1.23.3
Définir la version globale de Go
goenv global 1.23.3
Vérifier l'installation
go version

Installer CUE

Utiliser Go install
go install cuelang.org/go/cmd/cue@latest
Vérifier l'installation de CUE
cue version

Le package Go net/http

Le package Gonet/httpfournit des implémentations de client et de serveur HTTP. Il est utilisé pour effectuer des appels d'API et construire des serveurs web.

Fonctionnalités clés

  • Prise en charge de HTTP/1.1, HTTP2

  • Prise en charge de TLS (HTTPS)

  • Abstraction client et serveur HTTP

  • Prise en charge du routage viahttp.ServeMux

Client HTTP

Le typehttp.Clientest utilisé pour effectuer des requêtes HTTP.

Créer une requête 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))
}

Création d'une requête 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)
}

Serveur HTTP

Le typehttp.Serverest utilisé pour implémenter des serveurs HTTP.

Serveur HTTP de base

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)
}

Utilisation de http.ServeMux pour le routage

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()
}

Documentation du package Http

https://pkg.go.dev/net/http

Analyse de JSON avec le package GJSON

Aperçu

  • Analyseur JSON rapide pour Go

  • Utilise la syntaxe de chemin pour extraire des valeurs de JSON

  • Aucune sérialisation/désérialisation requise

  • Zéro dépendances

  • Thread-safe

Fonctionnalités Clés

Syntaxe des Chemins

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

Types Pris en Charge

  • Chaînes de caractères

  • Nombres

  • Booléens

  • Null

  • Tableaux

  • Objets

Performances

  • Analyse très rapide

  • Pas de surcharge d'allocation

  • Opère directement sur []byte

Opérations Courantes

value := gjson.Get(json, "path.to.value")
value.String()  // Get as string
value.Int()     // Get as integer
value.Array()   // Get as array
value.Map()     // Get as map

Modificateurs

  • @reverse: Inverser le tableau

  • @flatten: Aplatir le tableau

  • @join: Joindre les éléments du tableau

  • @valid: Valider le JSON

Cas d'Utilisation

  • Analyse des réponses d'API

  • Gestion de la configuration

  • Extraction de données JSON

  • Opérations JSON critiques en termes de performances

Liens de Documentation GJSON

Dépôt GitHub

https://github.com/tidwall/gjson

Documentation GoDoc

https://pkg.go.dev/github.com/tidwall/gjson

Démo 1 : Interagir avec l'API gpt-4o d'OpenAI depuis Go

var (
    apiURL = "https://api.openai.com/v1/chat/completions" // remplacer par le point de terminaison réel
    apiKey = os.Getenv("OPENAI_API_KEY")
    imagePath = "assets/from-go-apis-to-ai-enhanced-frontends.webp"
    prompt = "Décrivez le paysage de l'image."
)

fmt.Println(fmt.Sprintf("Extraction d'informations de : %v\n", imagePath))
// Charger l'image et l'encoder en base64
imageBytes, err := os.ReadFile(imagePath)
if err != nil {
    fmt.Println("Erreur lors de la lecture du fichier image :", 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()

```go
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)

Langage CUE (Configure Unify Execute)

Aperçu

CUE est un langage de configuration et de contrainte créé par Google pour aider à gérer les configurations complexes et la validation des données.

Concepts Clés

Modèle de Données

  • Modèle de monde ouvert

  • Héritage et composition

  • Fortement typé

  • Déclaratif

Fonctionnalités Clés

Contraintes de Type

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

Contraintes de Valeur

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

Intégration avec Go

Balises de structure Go

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

Exemple de validation

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()
}

Cas d'utilisation courants

Gestion de la configuration

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

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

Validation des données

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

Génération OpenAPI

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

Outillage

Ligne de commande

  • cue eval

  • cue fmt

  • cue vet

  • cue export

Support IDE

  • Extension VSCode

  • Plugin GoLand/IntelliJ

Bonnes pratiques

Définition de schéma

  • Utiliser le préfixe # pour les définitions

  • Garder les contraintes simples et claires

  • Utiliser des noms significatifs

Gestion des erreurs

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

Ressources

Documentation officielle

Ressources d'apprentissage

  • Tutoriels officiels

  • Exemples GitHub

  • Guides de la communauté

Modèles courants

Valeurs par défaut

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

Composition

#Base: {
    version: string
}

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

Relation avec Go

Points d'intégration

  • API Go directe

  • Prise en charge des balises de structure

  • Génération de code

  • Validation à l'exécution

Avantages

  • Sécurité des types

  • Validation de schéma

  • Gestion de la configuration

  • Modélisation des données

Cas d'utilisation

  • Définitions d'API

  • Fichiers de configuration

  • Validation des données

  • Génération de schémas

Messages d'erreur

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

Liens de documentation du langage CUE

Ressources officielles

Ressources GitHub

Ressources d'apprentissage

Démo 2 : API de base avec Go et Cue

Les Entités

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

Entités en tant que schéma JSON / Yaml :

rm -rf contracts/basic_schema.yaml # supprimer si déjà existant

cue def contracts/user.cue -o contracts/basic_schema.yaml --out openapi+yaml # générer le schéma

cat contracts/basic_schema.yaml # afficher le contenu du fichier résultant


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 ]+$


## Le contrat OpenAPI :

En améliorant la sortie précédente, nous incluons manuellement la définition des `paths`.

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 ]+$


## Le serveur :

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   string`json:"id"`
	Name string`json:"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))
}

Testons cette API de base :

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
: Résultat du scénario de chemin heureux :
:
: {"id":"1","name":"John Doe"}
:
:
: Résultat du scénario d'échec de validation :
:
: name: invalid value "1234" (out of bound =~"^ [A-Za-z ]+$")

Démo 3 : Interface utilisateur Swagger

Conversion de Yaml en JSON

mkdir -p demos/demo3

go run cli.go y2j contracts/demo2.yaml demos/demo3/openapi.json
: Conversion réussie de contracts/demo2.yaml en demos/demo3/openapi.json

Serveur mis à jour

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   string`json:"id"`
	Name string`json:"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() {
    // Points de terminaison de l'API
	http.HandleFunc("POST /users", userHandler)

// Servir la spécification 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 l'interface utilisateur 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("Serveur démarrant sur http://localhost:8080")
    log.Printf("Documentation de l'API disponible sur http://localhost:8080/docs")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

const swaggerTemplate =`

Documentation de l'API

`

Démo 4 : Récupération d'informations sur les images

Points forts

Gestion du type d'image

Le hook React récupère le type d'image directement à partir de l'objet File de l'image téléchargée et définit un champ caché

Gestion de la taille de l'image

La taille de l'image est appliquée dans l'API via les longueurs de chaîne min et max.

Un calcul approximatif de la taille base64 d'une chaîne de 10 Mo :

  • Tout d'abord, convertissez 10 Mo en octets :

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

  • Appliquez la formule : 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

Ainsi, une chaîne de 10 Mo sera d'environ 13,98 Mo une fois encodée en base64 (13 981 016 octets ≈ 13,98 Mo)

Les Entités

import "strings"

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

// Invite d'image
	prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"

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

// Statut de téléchargement de l'image
#ImageUploadStatus: {
	// Identifiant unique
	id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Invite d'image
	prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"

// Statut de téléchargement de l'image
	status: string & strings.MinRunes(3) & strings.MaxRunes(300) & =~"^ [A-Za-z0-9 -_.]+$"
}

Entités en tant que schéma JSON / Yaml :

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

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

cat contracts/image.yaml # show the contents of the resulting file
openapi: 3.0.0
info:
  title: Généré par cue.
  version: aucune version
paths: {}
components:
  schemas:
    ImageUpload:
      description: Contrat de téléchargement d'image
      type: object
      required:
        - id
        - prompt
        - blob
      properties:
        id:
          description: Identifiant unique
          type: string
          pattern: ^ [0-9a-zA-Z -]{36}$
        prompt:
          description: Invite d'image
          type: string
          allOf:
            - pattern: ^.{3,100}$
            - pattern: ^ [A-Za-z0-9 -_.]+$
        blob:
          description: Image encodée en Base64
          type: string
          minLength: 3
          maxLength: 13900000
          pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
    ImageUploadStatus:
      description: Statut de téléchargement d'image
      type: object
      required:
        - id
        - prompt
  • status
    properties:
    id:
    description: Identifiant unique
    type: string
    pattern: ^ [0-9a-zA-Z -]{36}$
    prompt:
    description: Invite d'image
    type: string
    minLength: 3
    maxLength: 100
    pattern: ^ [A-Za-z0-9 -.]+$
    status:
    description: Statut de téléchargement de l'image
    type: string
    minLength: 3
    maxLength: 300
    pattern: ^ [A-Za-z0-9 -
    .]+$

## Le contrat OpenAPI :

Nous incluons manuellement la définition de `paths` dans la sortie précédente.

openapi: 3.0.0
info:
title: Contrat de téléchargement d'image
version: 1.0.0
paths:
/extract-image-info:
post:
summary: Extraire les informations de l'image
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUpload'
responses:
'200':
description: Image traitée avec succès
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
'400':
description: Échec du traitement de l'image
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
components:
schemas:
ImageUpload:
description: Contrat de téléchargement d'image
type: object
required:
- id
- prompt
- blob


properties:
        id:
          description: Identifiant unique
          type: string
          pattern: ^ [0-9a-zA-Z -]{36}$
        prompt:
          description: Invite d'image
          type: string
          allOf:
            - pattern: ^.{3,100}$
            - pattern: ^ [A-Za-z0-9 -_.]+$
        blob:
          description: Image encodée en Base64
          type: string
          minLength: 3
          maxLength: 13900000
          pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
    ImageUploadStatus:
      description: Statut de téléchargement de l'image
      type: object
      required:
        - id
        - prompt
        - status
      properties:
        id:
          description: Identifiant unique
          type: string
          pattern: ^ [0-9a-zA-Z -]{36}$
        prompt:
          description: Invite d'image
          type: string
          minLength: 3

maxLength: 100
          pattern: ^ [A-Za-z0-9 -_.]+$
        status:
          description: Statut de téléchargement de l'image
          type: string
          minLength: 3
          maxLength: 300
          pattern: ^ [A-Za-z0-9 -_.]+$

Conversion de Yaml en JSON

mkdir -p demos/demo4

go run cli.go y2j contracts/demo4.yaml demos/demo4/openapi.json
: Converti avec succès contracts/demo4.yaml en demos/demo4/openapi.json

Le serveur :

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" // remplacer par le point de terminaison réel
	apiKey = os.Getenv("OPENAI_API_KEY")
)

//go:embed *
var content embed.FS

type ImageUpload struct {
	ID     string`json:"id"`
	Prompt string`json:"prompt"`
	Blob   string`json:"blob"`
}

type ImageUploadStatus struct {
	ID     string`json:"id"`
	Prompt string`json:"prompt"`
	Status string`json:"status"`
}

const schema =`

import "strings"

// Contrat de téléchargement d'image
#ImageUpload: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Invite d'image
prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"

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

// Statut de téléchargement d'image
#ImageUploadStatus: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Invite d'image
prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"

// Statut de téléchargement d'image
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() {
// 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 =`

Documentation 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
}


# Démo 5 : Triple Streaming



sequenceDiagram
participant LLM as Large Language Model
participant BE as Backend
participant UI as UI Layer
participant BR as Browser

BR->>UI: 1. Requête (invite/interaction)
UI->>BE: 2. Transférer l'invite
BE->>LLM: 3. Appeler le LLM avec stream=true
Note right of LLM: Le LLM génère
des jetons de manière incrémentielle
LLM-->>BE: 4. Diffuser les jetons partiels
Note right of BE: Le backend relaie les jetons
au fur et à mesure de leur arrivée
BE-->>UI: 5. Diffuser les jetons partiels
UI-->>BR: 6. Pousser les jetons partiels vers le navigateur
Note right of BR: Le navigateur met à jour l'interface utilisateur en temps réel,
affichant les jetons au fur et à mesure de leur diffusion
## Entités

import "strings"

// Contrat de téléchargement d'image
#ImageUpload: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Invite d'image
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"

// Diffusion activée
stream: bool

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

// Contrat d'informations sur l'image
#ImageInfo: {
// Informations sur l'image
info: string
}


## Entités en tant que schéma JSON / Yaml :

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

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

cat contracts/image2.yaml # show the contents of the resulting file


openapi: 3.0.0
info:
title: Generated by cue.
version: no version
paths: {}
components:
schemas:
ImageInfo:
description: Contrat d'informations d'image
type: object
required:
- info
properties:
info:
description: Informations d'image
type: string
ImageUpload:
description: Contrat de téléchargement d'image
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Flux activé
type: boolean
blob:
description: Image encodée en Base64


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

Le contrat OpenAPI :

Nous incluons manuellement la définition depathsà la sortie précédente.

openapi: 3.0.0
info:
  title: Généré par cue.
  version: no version
paths:
  /extract-image-info:
    post:
      summary: Extraire les informations de l'image
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $['ref']: '#/components/schemas/ImageUpload'
      responses:
        '200':
          description: Image traitée avec succès
          content:
            application/json:
              schema:
                $['ref']: '#/components/schemas/ImageInfo'
        '400':
          description: Le traitement de l'image a échoué
          content:
            application/json:
              schema:
                $['ref']: '#/components/schemas/ImageInfo'
components:
  schemas:
    ImageInfo:
      description: Contrat d'informations sur l'image
      type: object
      required:
        - info
      properties:
        info:
          description: Informations sur l'image

type: string
ImageUpload:
description: Contrat de téléchargement d'image
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Flux activé
type: boolean
blob:
description: Image encodée en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$


## Conversion de Yaml en JSON

mkdir -p demos/demo5

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


: Conversion réussie de contracts/demo5.yaml en demos/demo5/openapi.json


## Le serveur :

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" // remplacer par le point de terminaison réel
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"

// Contrat de téléchargement d'image
#ImageUpload: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"

// Invite d'image
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"

// Flux activé
stream: bool

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

// Contrat d'informations sur l'image
#ImageInfo: {
// Informations sur l'image
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 {
// Définir les en-têtes CORS pour autoriser toutes les origines. Vous voudrez peut-être restreindre cela à des origines spécifiques dans un environnement de production.
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")
// Définir le type de contenu sur 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 =`

Documentation de l'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 making request: %w", err)
}
defer resp.Body.Close()

// Vérifier le code de statut non-OK
if resp.StatusCode != http.StatusOK {
respBody, _ := bufio.NewReader(resp.Body).ReadString('\n')
fmt.Printf("Non-OK status code: %d\nBody: %s\n", resp.StatusCode, respBody)
return fmt.Errorf("Non-OK status code: %d", resp.StatusCode)
}

// Utiliser bufio pour lire la réponse ligne par ligne
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()

// OpenAI diffuse chaque morceau préfixé par "data:"
if !strings.HasPrefix(line, "data:") {
continue
}

// "data: [DONE]" indique la fin du flux
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
}

// Analyse chaque morceau à l'aide de gjson
result := gjson.Parse(jsonData)

// Le contenu pertinent se trouve dans "choices" -> tableau -> "delta" -> "content"
// ex., result.Get("choices.0.delta.content")
content := ""
for _, choice := range result.Get("choices").Array() {
contentDelta := choice.Get("delta.content").String()
content += contentDelta
}
// Affiche le morceau de contenu (le cas échéant)
if content != "" {
fmt.Print(content)
fmt.Fprintf(w, "data: %s\n\n", content)
w.(http.Flusher).Flush()
}

// Met en pause un instant pour simuler le temps de traitement
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
}


# Références

- [Thérapie cognitivo-comportementale pour guérir la phobie des serveurs](https://x.com/dhh/status/1841876620141068560)