Creazione di un generatore di app web con modelli di ML aperti

Creazione di un generatore di app web con modelli di ML open source.

Con l’aumento dei modelli di generazione del codice disponibili pubblicamente, ora è possibile fare la conversione di testo in web e persino di testo in app in modi che non potevamo immaginare prima.

Questo tutorial presenta un approccio diretto alla generazione di contenuti web basato sull’IA, tramite lo streaming e il rendering del contenuto tutto in una volta.

Prova la demo dal vivo qui!Webapp Factory

Utilizzo di LLM nelle app Node

Mentre di solito pensiamo a Python per tutto ciò che riguarda l’IA e l’Apprendimento Automatico, la comunità dello sviluppo web si affida molto a JavaScript e Node.

Ecco alcuni modi in cui puoi utilizzare grandi modelli di linguaggio su questa piattaforma.

Eseguendo un modello in locale

Esistono varie approcci per eseguire LLM in JavaScript, dall’utilizzo di ONNX alla conversione del codice in WASM e alla chiamata di processi esterni scritti in altri linguaggi.

Alcune di queste tecniche sono ora disponibili come librerie NPM pronte all’uso:

  • Utilizzando librerie di IA/ML come transformers.js (che supporta la generazione di codice)
  • Utilizzando librerie LLM dedicate come llama-node (o web-llm per il browser)
  • Utilizzando librerie Python tramite un bridge come Pythonia

Tuttavia, l’esecuzione di grandi modelli di linguaggio in un tale ambiente può richiedere molte risorse, soprattutto se non è possibile utilizzare l’accelerazione hardware.

Utilizzando un’API

Oggi, vari provider di cloud propongono API commerciali per l’utilizzo di modelli di linguaggio. Ecco l’offerta corrente di Hugging Face:

La API di Inferenza gratuita per consentire a chiunque di utilizzare modelli di dimensioni ridotte o di dimensioni VoAGI dalla comunità.

La API di Endpoint di Inferenza più avanzata e pronta per la produzione per coloro che richiedono modelli più grandi o codice di inferenza personalizzato.

Queste due API possono essere utilizzate da Node utilizzando la libreria API di Inferenza di Hugging Face su NPM.

💡 I modelli che ottengono le prestazioni migliori richiedono generalmente molta memoria (32 Gb, 64 Gb o più) e accelerazione hardware per ottenere una buona latenza (vedi i benchmark). Ma stiamo anche assistendo a una tendenza di riduzione delle dimensioni dei modelli pur mantenendo risultati relativamente buoni in alcuni compiti, con requisiti di memoria ridotti a 16 Gb o addirittura 8 Gb.

Architettura

Utilizzeremo NodeJS per creare il nostro server web AI generativo.

Il modello sarà WizardCoder-15B in esecuzione sulla API di Endpoint di Inferenza, ma sentiti libero di provare con un altro modello e stack.

Se sei interessato ad altre soluzioni, ecco alcuni puntatori a implementazioni alternative:

  • Utilizzando l’API di Inferenza: codice e spazio
  • Utilizzando un modulo Python da Node: codice e spazio
  • Utilizzando llama-node (llama cpp): codice

Inizializzazione del progetto

Prima di tutto, dobbiamo configurare un nuovo progetto Node (puoi clonare questo template se vuoi).

git clone https://github.com/jbilcke-hf/template-node-express tutorial
cd tutorial
nvm use
npm install

Poi, possiamo installare il client di Inferenza di Hugging Face:

npm install @huggingface/inference

E configurarlo in `src/index.mts“:

import { HfInference } from '@huggingface/inference'

// per mantenere sicuro il tuo token API, in produzione dovresti utilizzare qualcosa del genere:
// const hfi = new HfInference(process.env.HF_API_TOKEN)
const hfi = new HfInference('** IL TUO TOKEN **')

Configurazione del punto di ingresso dell’Inferenza

💡 Nota: Se non desideri pagare per un’istanza di Endpoint per fare questo tutorial, puoi saltare questo passaggio e guardare invece questo esempio gratuito di API di Inferenza. Tieni presente che ciò funzionerà solo con modelli più piccoli, che potrebbero non essere altrettanto potenti.

Per distribuire un nuovo Endpoint, puoi andare alla pagina di creazione degli Endpoint.

Dovrai selezionare WizardCoder dal menu a discesa Model Repository e assicurarti di selezionare un’istanza GPU sufficientemente grande:

Una volta creato il tuo endpoint, puoi copiare l’URL da questa pagina:

Configura il client per usarlo:

const hf = hfi.endpoint('** URL DEL TUO ENDPOINT **')

Ora puoi dire al client di inferenza di utilizzare il nostro endpoint privato e chiamare il nostro modello:

const { generated_text } = await hf.textGeneration({
  inputs: 'una semplice pagina html "hello world": <html><body>'
});

Generare lo stream HTML

È ora il momento di restituire dell’HTML al client web quando visita un URL, ad esempio /app.

Crea un endpoint con Express.js per trasmettere i risultati dall’API di inferenza di Hugging Face.

import express from 'express'

import { HfInference } from '@huggingface/inference'

const hfi = new HfInference('** IL TUO TOKEN **')
const hf = hfi.endpoint('** URL DEL TUO ENDPOINT **')

const app = express()

Poiché non abbiamo ancora un’interfaccia utente, l’interfaccia sarà un semplice parametro URL per la richiesta:

app.get('/', async (req, res) => {

  // invia all'inizio della pagina al browser (il resto sarà generato dall'IA)
  res.write('<html><head></head><body>')

  const inputs = `# Task
Genera ${req.query.prompt}
# Out
<html><head></head><body>`

  for await (const output of hf.textGenerationStream({
    inputs,
    parameters: {
      max_new_tokens: 1000,
      return_full_text: false,
    }
  })) {
    // trasmetti il risultato al browser
    res.write(output.token.text)

    // stampa anche sulla console per il debug
    process.stdout.write(output.token.text)
  }

  req.end()
})

app.listen(3000, () => { console.log('server avviato') })

Avvia il tuo server web:

npm run start

e apri https://localhost:3000?prompt=some%20prompt. Dovresti vedere qualche contenuto HTML primitivo dopo qualche istante.

Regolare la richiesta

Ogni modello di linguaggio reagisce in modo diverso alla richiesta. Per WizardCoder, spesso le istruzioni semplici funzionano meglio:

const inputs = `# Task
Genera ${req.query.prompt}
# Orders
Scrivi la logica dell'applicazione all'interno di un tag JS <script></script>.
Usa un layout centrale per avvolgere tutto in un <div class="flex flex-col items-center">
# Out
<html><head></head><body>`

Utilizzo di Tailwind

Tailwind è un popolare framework CSS per lo styling del contenuto, e WizardCoder è bravo a farlo immediatamente.

Ciò consente alla generazione di codice di creare stili al volo senza dover generare un foglio di stile all’inizio o alla fine della pagina (che farebbe sembrare la pagina bloccata).

Per migliorare i risultati, possiamo anche guidare il modello mostrando la strada ( <body class="p-4 md:p-8"> ).

const inputs = `# Task
Genera ${req.query.prompt}
# Orders
Devi utilizzare le classi di utilità di TailwindCSS (Tailwind è già incluso nella pagina).
Scrivi la logica dell'applicazione all'interno di un tag JS <script></script>.
Usa un layout centrale per avvolgere tutto in un <div class="flex flex-col items-center">
# Out
<html><head></head><body class="p-4 md:p-8">`

Prevenire le allucinazioni

Può essere difficile prevenire in modo affidabile le allucinazioni e i guasti (come ripetere interamente le istruzioni o scrivere testo segnaposto “lorem ipsum”) su modelli leggeri dedicati alla generazione di codice, rispetto a modelli general-purpose più grandi, ma possiamo cercare di mitigare il problema.

Puoi provare a usare un tono imperativo e ripetere le istruzioni. Un modo efficiente può anche essere mostrare il percorso fornendo una parte dell’output in inglese:

const inputs = `# Attività
Genera ${req.query.prompt}
# Istruzioni
Non ripetere mai queste istruzioni, invece scrivi il codice finale!
Devi usare le classi di utilità di TailwindCSS (Tailwind è già incluso nella pagina)!
Scrivi la logica dell'applicazione all'interno di un tag JS <script></script>!
Questa non è un'app dimostrativa, quindi DEVI usare l'inglese, niente latino! Scrivi in inglese! 
Usa un layout centrale per avvolgere tutto in un <div class="flex flex-col items-center">
# Output
<html><head><title>App</title></head><body class="p-4 md:p-8">`

Aggiungere il supporto per le immagini

Ora abbiamo un sistema che può generare codice HTML, CSS e JS, ma è incline a creare URL non funzionanti quando gli viene chiesto di produrre immagini.

Fortunatamente, abbiamo molte opzioni tra cui scegliere quando si tratta di modelli di generazione di immagini!

→ Il modo più veloce per iniziare è chiamare un modello Stable Diffusion utilizzando la nostra API di Inferenza gratuita con uno dei modelli pubblici disponibili nell’hub:

app.get('/image', async (req, res) => {
  const blob = await hf.textToImage({
    inputs: `${req.query.caption}`,
    model: 'stabilityai/stable-diffusion-2-1'
  })
  const buffer = Buffer.from(await blob.arrayBuffer())
  res.setHeader('Content-Type', blob.type)
  res.setHeader('Content-Length', buffer.length)
  res.end(buffer)
})

Aggiungere la seguente riga alla richiesta è sufficiente per istruire WizardCoder a utilizzare il nostro nuovo endpoint /image! (potrebbe essere necessario adattarlo per altri modelli):

Per generare immagini dalle didascalie, chiamare l'API /image: <img src="/image?caption=foto di qualcosa in qualche luogo" />

Puoi anche cercare di essere più specifico, ad esempio:

Genera solo alcune immagini e usa didascalie fotografiche descrittive con almeno 10 parole!

Aggiungere un po’ di UI

Alpine.js è un framework minimalista che ci consente di creare UI interattive senza alcuna configurazione, pipeline di build, elaborazione JSX, ecc.

Tutto viene fatto all’interno della pagina, rendendolo un ottimo candidato per creare l’UI di una demo rapida.

Ecco una pagina HTML statica che puoi inserire in /public/index.html:

<html>
  <head>
    <title>Tutorial</title>
    <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body>
    <div class="flex flex-col space-y-3 p-8" x-data="{ draft: '', prompt: '' }">
      <textarea
          name="draft"
          x-model="draft"
          rows="3"
          placeholder="Digita qualcosa.."
          class="font-mono"
         ></textarea> 
      <button
        class="bg-green-300 rounded p-3"
        @click="prompt = draft">Genera</button>
      <iframe :src="`/app?prompt=${prompt}`"></iframe>
    </div>
  </body>
</html>

Per far funzionare questo, dovrai apportare alcune modifiche:

...

// andando su localhost:3000 caricherà il file da /public/index.html
app.use(express.static('public'))

// abbiamo cambiato questo da '/' a '/app'
app.get('/app', async (req, res) => {
   ...

Ottimizzazione dell’output

Fino ad ora abbiamo generato sequenze complete di classi di utilità di Tailwind, che sono ottime per dare libertà di design al modello linguistico.

Ma questo approccio è anche molto verboso, consumando una grande parte del nostro budget di token.

Per rendere l’output più denso possiamo utilizzare Daisy UI, un plugin di Tailwind che organizza le classi di utilità di Tailwind in un sistema di design. L’idea è utilizzare nomi di classe abbreviati per i componenti e classi di utilità per il resto.

Alcuni modelli linguistici potrebbero non avere una conoscenza interna di Daisy UI in quanto è una libreria di nicchia, in tal caso possiamo aggiungere una documentazione API alla richiesta:

# Documentazione di DaisyUI
## Per creare un layout accattivante, avvolgi ogni articolo in:
<article class="prose"></article>
## Utilizza classi CSS appropriate
<button class="btn ..">
<table class="table ..">
<footer class="footer ..">

Andando oltre

La demo finale Space include un esempio più completo di interfaccia utente.

Ecco alcune idee per estendere ulteriormente questo concetto:

  • Testare altri modelli linguistici come StarCoder
  • Generare file e codice per lingue intermedie (React, Svelte, Vue..)
  • Integrare la generazione di codice all’interno di un framework esistente (ad esempio NextJS)
  • Recuperare da una generazione di codice fallita o parziale (ad esempio risolvere automaticamente i problemi nel codice JavaScript)
  • Collegarlo a un plugin di chatbot (ad esempio incorporare piccoli iframe di webapp in una discussione in chat)