create-panal-agent 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@panal/sdk": "^0.16.0",
13
+ "@panal/sdk": "^0.17.0",
14
14
  "unpdf": "^1.8.1",
15
15
  "dotenv": "^17.0.0",
16
16
  "tsx": "^4.19.0",
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import 'dotenv/config';
15
- import { createPanalClient, formatAgentMetadata, NATIVE_CURRENCY } from '@panal/sdk';
15
+ import { createPanalClient, formatAgentMetadata, NATIVE_CURRENCY, rutaDeAgente } from '@panal/sdk';
16
16
  import { privateKeyToAccount } from 'viem/accounts';
17
17
  import { createPublicClient, createWalletClient, formatEther, http, parseEther } from 'viem';
18
18
 
@@ -138,7 +138,7 @@ export function loQueFaltaDelPerfil(perfil: typeof PERFIL): string | null {
138
138
  async function compruebaEndpoint(botUrl: string, yo: string): Promise<string | null> {
139
139
  let url: string;
140
140
  try {
141
- url = new URL('/agent.json', botUrl).toString();
141
+ url = rutaDeAgente(botUrl, 'agent.json');
142
142
  } catch {
143
143
  return `PERFIL.botUrl no es una URL válida: ${botUrl}`;
144
144
  }
@@ -192,7 +192,7 @@ async function compruebaEndpoint(botUrl: string, yo: string): Promise<string | n
192
192
  async function logoQueSirves(botUrl: string): Promise<string> {
193
193
  let url: string;
194
194
  try {
195
- url = new URL('/logo', botUrl).toString();
195
+ url = rutaDeAgente(botUrl, 'logo');
196
196
  } catch {
197
197
  return '';
198
198
  }
@@ -61,7 +61,7 @@ import { privateKeyToAccount } from 'viem/accounts';
61
61
  import { isAddress, keccak256, parseEther, toBytes, verifyMessage } from 'viem';
62
62
  import type { Address } from 'viem';
63
63
  import { handleTask, NIVELES, SUBCONTRATA_SKILLS } from './agent.js';
64
- import { traducirFrases } from './traduccion.js';
64
+ import { frasesGuardadas, pedirTraduccion } from './traduccion.js';
65
65
  import type { AdjuntoRecibido, NivelPropio, TaskContext, TaskFile, TaskResult } from './agent.js';
66
66
  import { arrancarVigilante } from './vigilante.js';
67
67
  import { historialParaElModelo, recordarTurno, type Turno } from './memoria.js';
@@ -1264,25 +1264,36 @@ const server = createServer((req, res) => {
1264
1264
  /**
1265
1265
  * `?lang=fr`: la misma ficha con las frases en francés.
1266
1266
  *
1267
- * Traduce este agente con su propio modelo y guarda el resultado, así
1268
- * que la primera petición en cada idioma cuesta una llamada y las demás
1269
- * ninguna. Si falla —sin clave, sin cuota, sin redse sirve el original:
1270
- * no traducir no puede ser un error para quien pide la ficha.
1267
+ * SIN ESPERAR. Si el idioma ya está traducido se sirve traducido; si no,
1268
+ * se sirve el original y la traducción se encarga por detrás para la
1269
+ * próxima vez. Traducir aquí dentro obliga a no reintentarnadie espera
1270
+ * a un modelo con la tarjeta en blanco— y sin reintentos un 429 pasajero
1271
+ * dejaba ese idioma sin traducir para siempre.
1271
1272
  */
1272
1273
  const idioma = normalizarIdioma(url.searchParams.get('lang'));
1273
1274
  const nivelesFicha = NIVELES_OK.map(comoFicha);
1274
1275
  let descripcion = FICHA_TEXTO.description;
1276
+ /**
1277
+ * En qué idioma va lo que se sirve, y `null` si va en el original.
1278
+ *
1279
+ * Hay que DECIRLO, no dejarlo adivinar. Como la traducción va por detrás,
1280
+ * pedir `?lang=fr` antes de que esté lista devuelve la ficha original con
1281
+ * un 200 impecable: quien la guarde —el indexador lo hace— se queda con
1282
+ * el texto en inglés creyendo que es el francés, y como le llegaron los
1283
+ * diez idiomas da el trabajo por hecho y no vuelve nunca. Pasó en
1284
+ * mainnet: nueve de cada diez «traducciones» del catálogo eran el
1285
+ * original.
1286
+ */
1287
+ let servidoEn: string | null = null;
1275
1288
  if (idioma) {
1276
- const traducido = await traducirFrases(
1277
- {
1278
- description: descripcion,
1279
- tiers: NIVELES_OK.map((n) => ({ name: n.name ?? '', description: n.description ?? '' })),
1280
- },
1281
- idioma,
1282
- LLM_FICHA,
1283
- DATA_DIR,
1284
- );
1289
+ const frases = {
1290
+ description: descripcion,
1291
+ tiers: NIVELES_OK.map((n) => ({ name: n.name ?? '', description: n.description ?? '' })),
1292
+ };
1293
+ const traducido = frasesGuardadas(frases, idioma, DATA_DIR);
1294
+ if (!traducido) pedirTraduccion(frases, idioma, LLM_FICHA, DATA_DIR);
1285
1295
  if (traducido) {
1296
+ servidoEn = idioma;
1286
1297
  descripcion = traducido.description;
1287
1298
  traducido.tiers.forEach((t, i) => {
1288
1299
  const destino = nivelesFicha[i];
@@ -1318,6 +1329,9 @@ const server = createServer((req, res) => {
1318
1329
  // francés y traducirlo sería inventarle otro nombre a este agente.
1319
1330
  ...(FICHA_TEXTO.name ? { name: FICHA_TEXTO.name } : {}),
1320
1331
  ...(descripcion ? { description: descripcion } : {}),
1332
+ // Solo cuando se ha traducido de verdad. Ausente = esto va en el
1333
+ // idioma en que su dueño lo escribió, aunque lo hayas pedido en otro.
1334
+ ...(servidoEn ? { lang: servidoEn } : {}),
1321
1335
  endpoints: {
1322
1336
  base,
1323
1337
  postBrief: {
@@ -26,6 +26,22 @@
26
26
  * Diez idiomas son diez llamadas en toda la vida de una descripción. Traducir
27
27
  * cuatro frases es la llamada más barata que va a hacer tu agente.
28
28
  *
29
+ * NADIE ESPERA A QUE TRADUZCA
30
+ *
31
+ * La ficha se sirve SIEMPRE al momento. Si el idioma ya está guardado va
32
+ * traducida; si no, va original y la traducción se pide POR DETRÁS, para la
33
+ * próxima vez que alguien pregunte por ese idioma.
34
+ *
35
+ * Traducir dentro de la petición obliga a no reintentar, porque nadie va a
36
+ * esperar a un modelo con la tarjeta en blanco. Y sin reintentos un
37
+ * `429 Too Many Requests` —que en una cuenta compartida por cuatro agentes es
38
+ * lo normal, no la excepción— significa «esta ficha no se traduce»; como no se
39
+ * guarda nada, el siguiente que pregunte se come otro 429 y el idioma no llega
40
+ * a traducirse NUNCA. Comprobado contra los agentes de mainnet: la misma
41
+ * petición que falla con cero reintentos entra en cuanto se la deja insistir.
42
+ *
43
+ * Fuera de la petición sí se puede insistir, porque no hay nadie mirando.
44
+ *
29
45
  * CUANDO FALLA NO SE NOTA
30
46
  *
31
47
  * Si el modelo no contesta, se acabó la cuota o no hay `LLM_API_KEY`, se sirve
@@ -55,11 +71,24 @@ export interface Frases {
55
71
  const MAX_FRASE = 400;
56
72
 
57
73
  /**
58
- * Cuánto se espera. Corto a propósito: esto corre DENTRO de una petición de la
59
- * ficha, y una tarjeta que tarda medio minuto en pintar es una tarjeta rota.
60
- * Lo que no llegue a tiempo sale sin traducir y se traducirá en la siguiente.
74
+ * Cuánto se espera al modelo, y cuántas veces se insiste.
75
+ *
76
+ * Holgado porque esto ya NO corre dentro de la petición de la ficha: nadie está
77
+ * mirando. Los reintentos son lo que hace que la traducción llegue; sin ellos
78
+ * un 429 pasajero dejaba el idioma sin traducir para siempre.
79
+ */
80
+ const ESPERA_MS = 60_000;
81
+ const REINTENTOS = 4;
82
+
83
+ /**
84
+ * Los idiomas que se están traduciendo ahora mismo.
85
+ *
86
+ * El indexador pide los diez seguidos, y sin esta lista tres peticiones en
87
+ * francés llegadas antes de que vuelva la primera lanzarían tres traducciones
88
+ * idénticas: tres veces el gasto contra una cuenta que ya va justa de
89
+ * peticiones por minuto, para escribir el mismo archivo.
61
90
  */
62
- const ESPERA_MS = 20_000;
91
+ const enCurso = new Set<string>();
63
92
 
64
93
  /** La huella del texto original: si cambia, la traducción guardada ya no vale. */
65
94
  function huella(frases: Frases): string {
@@ -144,10 +173,42 @@ const SISTEMA =
144
173
  'stay short. Do not translate brand names, product names or code identifiers.';
145
174
 
146
175
  /**
147
- * Las frases de la ficha en otro idioma.
176
+ * Las frases ya traducidas, si están guardadas. NO llama a nadie.
177
+ *
178
+ * Esta es la que usa la ficha, y por eso es síncrona: contesta en microsegundos
179
+ * y no puede hacer esperar a quien pide `/agent.json`.
180
+ */
181
+ export function frasesGuardadas(frases: Frases, idioma: Idioma, dir: string): Frases | null {
182
+ return leerGuardado(dir, idioma, huella(frases));
183
+ }
184
+
185
+ /**
186
+ * Pide la traducción POR DETRÁS, para la próxima vez.
187
+ *
188
+ * No devuelve nada y no se espera: quien la llama ya ha servido la ficha
189
+ * original. Si sale bien queda guardada y la siguiente petición en ese idioma
190
+ * la encuentra hecha; si sale mal no se entera nadie y se reintentará.
191
+ */
192
+ export function pedirTraduccion(
193
+ frases: Frases,
194
+ idioma: Idioma,
195
+ llm: LlmConfig | null,
196
+ dir: string,
197
+ ): void {
198
+ if (!llm) return;
199
+ if (!frases.description.trim() && frases.tiers.length === 0) return;
200
+ const clave = `${idioma}-${huella(frases)}`;
201
+ if (enCurso.has(clave) || frasesGuardadas(frases, idioma, dir)) return;
202
+ enCurso.add(clave);
203
+ void traducirFrases(frases, idioma, llm, dir).finally(() => enCurso.delete(clave));
204
+ }
205
+
206
+ /**
207
+ * Las frases de la ficha en otro idioma, esperando al modelo.
148
208
  *
149
- * Devuelve `null` cuando no se ha podido traducir, y quien llama sirve el
150
- * original: no traducir NUNCA puede ser un error para el que pide la ficha.
209
+ * Devuelve `null` cuando no se ha podido traducir. La ficha NO la llama
210
+ * directamente —usa el par de arriba—; esta existe para las pruebas y para
211
+ * traducir a mano, donde sí se quiere el resultado.
151
212
  */
152
213
  export async function traducirFrases(
153
214
  frases: Frases,
@@ -165,7 +226,7 @@ export async function traducirFrases(
165
226
 
166
227
  try {
167
228
  const crudo = await llmChat(
168
- { ...llm, timeoutMs: ESPERA_MS, maxRetries: 0 },
229
+ { ...llm, timeoutMs: ESPERA_MS, maxRetries: REINTENTOS },
169
230
  {
170
231
  system: SISTEMA,
171
232
  user: