Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/components/docs/DocsTopBar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const t = {
reference: 'API Reference',
platform: 'Platform ↗',
site: 'nan.builders ↗',
home: 'NaN, back to the docs',
home: 'NaN, go to nan.builders',
menu: 'Toggle menu',
language: 'Language',
},
Expand All @@ -42,7 +42,7 @@ const t = {
reference: 'Referencia API',
platform: 'Plataforma ↗',
site: 'nan.builders ↗',
home: 'NaN, volver a los docs',
home: 'NaN, ir a nan.builders',
menu: 'Abrir o cerrar el menú',
language: 'Idioma',
},
Expand All @@ -57,7 +57,12 @@ const t = {
<path d="M3 6h18M3 12h18M3 18h18"></path>
</svg>
</button>
<a href={`${pfx}/docs`} class="docs-logo" aria-label={t.home}>
{/*
The wordmark goes to the site, not to the docs index. It is the brand
mark: clicking a logo is how you get out to the home page, and "Guides"
right next to it already covers going to the docs index.
*/}
<a href={`https://nan.builders${pfx}`} class="docs-logo" aria-label={t.home}>
<span class="docs-wm" aria-hidden="true"></span>
</a>
<span class="docs-top-tag">{t.docs}</span>
Expand Down
40 changes: 24 additions & 16 deletions src/components/docs/RateLimits.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,32 @@ import {
getRateLimitsConfig,
windowedModelBody,
windowedModelHeadline,
rateLimitsLabels,
} from '../../lib/rateLimits';

const { perKey, tokensPerMinuteByModel, requestsPerMinuteByModel, windowedModels } =
getRateLimitsConfig(env);

// This card is embedded from both the English and the Spanish guides, and MDX
// content cannot pass props down from the layout, so the locale is read off the
// route the page was rendered for.
const lang = Astro.url.pathname.startsWith('/es/') ? 'es' : 'en';

const T = rateLimitsLabels(lang);
---

<div class="rounded-xl border border-neutral-800/60 bg-[#0a0a0a] p-6 mb-6">
<p class="font-mono text-[10px] text-violet-400 uppercase tracking-widest mb-4">
rate limits per API key
{T.perKey}
</p>
<dl class="grid gap-3 sm:grid-cols-2">
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Requests / min</dt>
<dt class="text-neutral-500 uppercase tracking-wider">{T.requestsPerMin}</dt>
<dd class="text-neutral-200 text-right">{perKey.requestsPerMinute} rpm</dd>
</div>
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Max parallel</dt>
<dd class="text-neutral-200 text-right">{perKey.maxParallel} concurrent</dd>
<dt class="text-neutral-500 uppercase tracking-wider">{T.maxParallel}</dt>
<dd class="text-neutral-200 text-right">{perKey.maxParallel} {T.concurrent}</dd>
</div>
</dl>
</div>
Expand All @@ -37,37 +45,37 @@ const { perKey, tokensPerMinuteByModel, requestsPerMinuteByModel, windowedModels
windowedModels.map((m) => (
<div class="rounded-xl border border-violet-500/30 bg-violet-500/[0.06] p-6 mb-6">
<p class="font-mono text-[10px] text-violet-300 uppercase tracking-widest mb-4">
{m.model} · premium tier limits
{m.model} · {T.premium}
</p>
<dl class="grid gap-3 sm:grid-cols-2">
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Rolling {m.windowHours}h window</dt>
<dd class="text-neutral-200 text-right">{formatTokens(m.windowTokens)} tokens</dd>
<dt class="text-neutral-500 uppercase tracking-wider">{T.window(m.windowHours)}</dt>
<dd class="text-neutral-200 text-right">{formatTokens(m.windowTokens, lang)} tokens</dd>
</div>
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Allowance / billing period</dt>
<dd class="text-neutral-200 text-right">{formatTokens(m.periodCapTokens)} tokens</dd>
<dt class="text-neutral-500 uppercase tracking-wider">{T.allowance}</dt>
<dd class="text-neutral-200 text-right">{formatTokens(m.periodCapTokens, lang)} tokens</dd>
</div>
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Context window</dt>
<dd class="text-neutral-200 text-right">{formatTokens(m.contextTokens)} tokens</dd>
<dt class="text-neutral-500 uppercase tracking-wider">{T.context}</dt>
<dd class="text-neutral-200 text-right">{formatTokens(m.contextTokens, lang)} tokens</dd>
</div>
<div class="flex items-baseline justify-between gap-4 font-mono text-xs">
<dt class="text-neutral-500 uppercase tracking-wider">Concurrent requests</dt>
<dt class="text-neutral-500 uppercase tracking-wider">{T.concurrentRequests}</dt>
<dd class="text-neutral-200 text-right">{m.maxParallel}</dd>
</div>
</dl>
<p class="mt-4 font-mono text-[11px] leading-relaxed text-neutral-400">
<span class="text-violet-200">{windowedModelHeadline(m)}</span>{' '}
{windowedModelBody(m)}
<span class="text-violet-200">{windowedModelHeadline(m, lang)}</span>{' '}
{windowedModelBody(m, lang)}
</p>
</div>
))
}

<div class="rounded-xl border border-neutral-800/60 bg-[#0a0a0a] p-6 mb-6">
<p class="font-mono text-[10px] text-violet-400 uppercase tracking-widest mb-4">
tokens / min per model
{T.tokensPerModel}
</p>
<dl class="grid gap-3 sm:grid-cols-2">
{
Expand All @@ -83,7 +91,7 @@ const { perKey, tokensPerMinuteByModel, requestsPerMinuteByModel, windowedModels

<div class="rounded-xl border border-neutral-800/60 bg-[#0a0a0a] p-6 mb-14">
<p class="font-mono text-[10px] text-violet-400 uppercase tracking-widest mb-4">
requests / min per model
{T.requestsPerModel}
</p>
<dl class="grid gap-3 sm:grid-cols-2">
{
Expand Down
51 changes: 34 additions & 17 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,41 @@ import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const docsSchema = z.object({
title: z.string(),
description: z.string(),
order: z.number().int().min(0),
/*
* The heading the page appears under in the docs navigation.
*
* helmcode's nav writes the groups by hand in the layout; here they live in
* the data so adding a guide stays a matter of creating a file rather than
* also editing the layout, which is how these things drift apart. The order
* between groups comes from the lowest `order` in each, so there is no
* second list to maintain either.
*/
group: z.string().default('Guides'),
locale: z.string().default('es'),
});

const docs = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs' }),
schema: z.object({
title: z.string(),
description: z.string(),
order: z.number().int().min(0),
/*
* The heading the page appears under in the docs navigation.
*
* helmcode's nav writes the groups by hand in the layout; here they live in
* the data so adding a guide stays a matter of creating a file rather than
* also editing the layout, which is how these things drift apart. The order
* between groups comes from the lowest `order` in each, so there is no
* second list to maintain either.
*/
group: z.string().default('Guides'),
locale: z.string().default('es'),
}),
schema: docsSchema,
});

/*
* The Spanish guides live in their own directory rather than under a locale
* subfolder of `docs`.
*
* A `docs/en/…` + `docs/es/…` layout would turn every entry id into `en/intro`
* and the like, and SAFE_SLUG in src/lib/docsApi.ts rejects slashes: the
* manifest route throws on the first one, so /api/docs/manifest.json would
* answer 500 and the Discord bot would lose everything. Keeping English where
* it is leaves those slugs untouched.
*/
const docsEs = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs-es' }),
schema: docsSchema,
});

export const collections = { docs };
export const collections = { docs, docsEs };
116 changes: 116 additions & 0 deletions src/content/docs-es/agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
title: Agentes
description: "Despliega agentes de IA en una microVM aislada con QEMU: Hermes, terminal web, subida de ficheros y observabilidad."
order: 5
group: Guías
---

# Agentes.

NaN Cloud te permite desplegar agentes de IA en tu propia **microVM**: una máquina virtual ligera con QEMU y KVM, con su propio kernel, su propio sistema de ficheros y acceso root completo. Aislada del host y del resto de miembros. El primer tipo de agente disponible es **Hermes**.

> **¿Usas un agente que alojas tú?**
> Si ejecutas tu propio agente compatible con MCP en otro sitio, puedes enchufarle nuestras herramientas (como la búsqueda web) directamente, con la misma API key, a través de nuestro [servidor MCP](/es/docs/api#tag/mcp) remoto.

## Arquitectura

Cada agente corre dentro de su propia microVM de QEMU. En vez de compartir el kernel del host (como haría un contenedor normal), arranca con su propio kernel de Linux. La VM monta un disco ext4 de 20 GiB sobre un volumen persistente en modo bloque. Todo lo que hagas dentro (`apt install`, `pip install`, cambios en `/etc`, ficheros que subas) vive en ese disco y sobrevive a los reinicios.

El apagado es *limpio*: cuando reinicias o borras el agente, el sistema fuerza un `sync` y espera a que el journal de ext4 termine de volcarse antes de matar la VM. Sin corrupción.

## Hermes

Hermes es un agente de IA conversacional que se conecta a Telegram. Puedes chatear con él, pedirle que gestione notas, que ejecute comandos en su entorno, que genere webs y bastante más.

### 1. Crea un bot de Telegram

Necesitas un bot de Telegram. Abre Telegram, busca [@BotFather](https://core.telegram.org/bots/tutorial#obtain-your-bot-token) y sigue las instrucciones para crear uno nuevo. Copia el token que te dé.

### 2. Crea el agente

Entra en [cloud.nan.builders/agents/new](https://cloud.nan.builders/agents/new) y rellena: nombre, tipo (Hermes), el token de Telegram, el modelo y, opcionalmente, un *soul* (system prompt) que defina la personalidad de tu agente.

![Formulario de creación de agente](/docs/agents/create-agent-form.png)

### 3. Espera a que esté Running

Después de crear el agente, espera unos 30 segundos a que arranque la microVM, se formatee el disco por primera vez (`mkfs.ext4`) y se siembre el sistema de ficheros. El estado pasa a `Running` y Hermes a `Ready`.

### 4. Habla con tu agente

Busca tu bot en Telegram y mándale un mensaje. Hermes responderá con el modelo que hayas configurado.

![Conversación con Hermes en Telegram](/docs/agents/telegram-hermes-chat.jpg)

> **Tu agente está listo.**
> Con estos 4 pasos ya tienes Hermes funcionando. Lo que viene a continuación son funciones adicionales del panel del agente: terminal web, subida de ficheros, observabilidad, exposición HTTP, la UI de Hermes y la gestión de variables de entorno.

## Console: terminal web

La pestaña **Console** abre una terminal interactiva (`bash --login`) dentro de tu microVM, sin necesidad de configurar SSH. El stream va por WebSocket con xterm.js: se redimensiona sola al ajustar el panel, tiene una pastilla de estado arriba a la derecha y un botón de reconexión por si se cae la sesión.

Casos de uso típicos:

- Instalar paquetes: `apt update && apt install -y nginx`
- Revisar los logs internos del agente
- Mover a su sitio los ficheros que hayas subido
- Usar `htop`, `df -h`, `journalctl`, etc.

> **Límites operativos**
> 1 sesión simultánea por agente · 10 min de timeout por inactividad · 30 min de duración máxima por sesión.

## Files: subida de ficheros

La pestaña **Files** permite subir ficheros a la microVM arrastrándolos o desde el selector. Admite varios a la vez, con cola secuencial y barra de progreso en vivo con MiB/s. Los ficheros aterrizan en `/persist/uploads/` y desde ahí puedes moverlos con la Console.

- Tamaño máximo: **200 MiB** por fichero.
- Transporte: WebSocket con trozos de 256 KiB y backpressure de extremo a extremo.
- El nombre del fichero se sanea en servidor (sin path traversal).
- Listado en vivo de los ficheros subidos (se refresca cada 5s).

## Observabilidad

La pestaña **Observability** agrupa tres sub-pestañas:

- **Logs**: stream en vivo del stdout y stderr del agente por WebSocket. Búfer de las últimas 500 líneas en el cliente.
- **Events**: eventos del Pod de Kubernetes (BackOff, Scheduled, Pulled, Killing...) con tipo, motivo, mensaje, antigüedad y recuento. Se refresca solo cada 15s.
- **Metrics**: consumo real de CPU, RAM y disco frente a los límites configurados. CPU y RAM vía Prometheus (kubelet-cadvisor), disco con `df` dentro de la microVM (el sistema de ficheros es de modo bloque y kubelet no lo ve). Se refresca cada 10s.

## Web: exposición pública

La pestaña **Web** tiene dos sub-pestañas para exponer servicios HTTP del agente:

### HTTP

Cualquier servicio que tu agente sirva por HTTP (nginx, una API, un sitio estático) lo puedes exponer públicamente. Por ejemplo, pídele a Hermes que instale nginx con una página HTML propia:

![Pidiendo a Hermes que instale nginx con una página HTML propia](/docs/agents/telegram-nginx-setup.jpg)

En la pestaña **Web → HTTP**, pulsa **Enable HTTP**. Por defecto se expone el puerto `80`; si tu servicio escucha en otro, indícalo en **Container Port**. La plataforma genera una URL pública en `*.apps.nan.builders`.

![Web generada por Hermes vista desde la URL pública](/docs/agents/http-result.png)

### La UI de Hermes

Hermes incluye una UI web ligera ([nesquena/hermes-webui](https://github.com/nesquena/hermes-webui)) que corre siempre dentro del agente. Desde **Web → Hermes UI** puedes activar el acceso externo: la plataforma genera una URL del tipo `webui-<agente>-<usuario>.apps.nan.builders`, protegida por una contraseña por agente que se muestra en el panel.

## Variables de entorno

La pestaña **Env** te permite añadir, editar y borrar variables de entorno del agente sin tocar el Deployment. Útil para inyectar API keys de terceros, configurar el comportamiento de Hermes, etc.

Hay dos variables **protegidas** (solo se pueden editar, no borrar): `OPENAI_API_KEY` (tu key del clúster, que gestiona la plataforma) y `TELEGRAM_BOT_TOKEN`. El resto las puedes crear, editar o borrar libremente.

## Recursos y límites

Cada microVM se aprovisiona con:

| Recurso | Request | Límite |
|---|---|---|
| CPU | 200m | 1 vCPU |
| RAM | 512 Mi | 2 GiB |
| Disco | (sin request) | 20 GiB (PVC en modo bloque) |

La CPU y la RAM son los límites máximos de la microVM; el consumo real suele quedar muy por debajo. El disco es persistente: todo lo que instales o modifiques (paquetes, ficheros, configuraciones) se conserva entre reinicios. Si el disco se llena (por encima del 90%), libéralo desde la Console (`du -sh /persist/*`).

> **Límite actual**
> Ahora mismo cada miembro puede desplegar **1 agente en microVM**. Este límite se ampliará en versiones futuras.
Loading
Loading