create-panal-agent 0.20.0 → 0.20.1

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.20.0",
3
+ "version": "0.20.1",
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",
@@ -438,9 +438,22 @@ export function comoAdjunto(nombre: string): string {
438
438
  const PIDE_UN_NOMBRE =
439
439
  'You name files. Given a client request, reply with ONLY a file name for the deliverable.\n' +
440
440
  'Two to five words. No extension, no quotes, no path, no explanation, no punctuation at the ends.\n' +
441
- 'Name the SUBJECT the work is about, never the action asked for: for "write the test cases for a ' +
442
- 'function that divides two integers" answer "division de dos enteros", not "escribir casos de prueba".\n' +
443
- 'Write it in the SAME language the client wrote in, in their own script. Do not translate it to English.';
441
+ 'Name the SUBJECT the work is about, never the action asked for.\n' +
442
+ // El ejemplo de antes enseñaba lo contrario de lo que pedía: a una petición
443
+ // en INGLÉS le contestaba en español («division de dos enteros»), y el
444
+ // encargo #95, escrito en inglés, salió como «contactos-en-json». Ahora hay
445
+ // un ejemplo por idioma, cada uno respondido en el suyo.
446
+ 'Write it in the language of the client\'s INSTRUCTIONS — what they ask for — not the language of ' +
447
+ 'the data they paste or of field names they spell out, and in their own script. No file-format ' +
448
+ 'words (JSON, PDF, CSV). Examples: "write the test cases for a function that divides two integers" ' +
449
+ '-> "division of two integers"; "escribe los casos de prueba de una función que divide dos enteros" ' +
450
+ '-> "division de dos enteros".\n' +
451
+ // Decirlo no bastaba: con los datos en español y las instrucciones en
452
+ // inglés, el modelo seguía contestando en español. Obligarle a nombrar
453
+ // primero el idioma de las instrucciones es lo que lo corrige (medido).
454
+ 'First decide the language of the instructions. Answer in exactly two lines:\n' +
455
+ 'LANG: <ISO code of the instructions\' language>\n' +
456
+ 'NAME: <the file name, in that language>';
444
457
 
445
458
  /**
446
459
  * El nombre del archivo que se entrega, sacado del TEMA del encargo.
@@ -461,7 +474,79 @@ const PIDE_UN_NOMBRE =
461
474
  * devuelve algo que no sirve, se usa el nombre de siempre. Nombrar un archivo
462
475
  * no puede impedir entregarlo: el pago ya está bloqueado.
463
476
  */
464
- export async function nombreDelTema(brief: string, deReserva: string): Promise<string> {
477
+ /**
478
+ * De «LANG: en\nNAME: contact list», la línea del nombre.
479
+ *
480
+ * Sin la etiqueta —un modelo que no siga el formato— se queda la respuesta
481
+ * entera, y `comoNombre` se queda con su primera línea, como antes.
482
+ */
483
+ export function lineaDelNombre(crudo: string): string {
484
+ const m = crudo.match(/^\s*NAME\s*:\s*(.+)$/im);
485
+ return m ? m[1]! : crudo;
486
+ }
487
+
488
+ /**
489
+ * EL IDIOMA DE LAS INSTRUCCIONES, sin los datos delante.
490
+ *
491
+ * Pedirle al modelo «escribe en el idioma del cliente» no funciona cuando el
492
+ * encargo trae datos en otro idioma: medido el 2026-09-28, con órdenes en
493
+ * inglés y una lista de contactos en español, las claves salían en español
494
+ * más de la mitad de las veces, y un detector que veía el encargo entero
495
+ * acertaba 19 de 30 (tomaba un encargo en francés por español, y uno en chino
496
+ * por inglés). El mismo detector con SOLO el primer párrafo acertó 39 de 40.
497
+ *
498
+ * El primer párrafo es lo que va antes de la primera línea en blanco, que es
499
+ * como se escribe casi siempre un encargo: primero qué hay que hacer, después
500
+ * los datos. Si es demasiado corto para decir nada, se usan los primeros 300
501
+ * caracteres.
502
+ */
503
+ /**
504
+ * El nombre del idioma, en inglés, para decírselo al modelo.
505
+ *
506
+ * Con el código solo («"en"») el modelo seguía nombrando en español un encargo
507
+ * en inglés que citaba campos en español; con «Write the NAME in English» y el
508
+ * aviso de que ni los datos ni los campos citados deciden, acertó 12 de 12.
509
+ */
510
+ const NOMBRES_DE_IDIOMA: Record<string, string> = {
511
+ en: 'English', es: 'Spanish', pt: 'Portuguese', fr: 'French', de: 'German', it: 'Italian',
512
+ nl: 'Dutch', ca: 'Catalan', zh: 'Chinese', ja: 'Japanese', ko: 'Korean', hi: 'Hindi',
513
+ bn: 'Bengali', ur: 'Urdu', ar: 'Arabic', fa: 'Persian', he: 'Hebrew', ru: 'Russian',
514
+ uk: 'Ukrainian', pl: 'Polish', tr: 'Turkish', vi: 'Vietnamese', id: 'Indonesian', th: 'Thai',
515
+ el: 'Greek', sv: 'Swedish',
516
+ };
517
+ export function nombreDeIdioma(codigo: string): string {
518
+ return NOMBRES_DE_IDIOMA[codigo] ?? `the language with ISO code "${codigo}"`;
519
+ }
520
+
521
+ const PIDE_EL_IDIOMA =
522
+ 'You detect the language of a request. Reply with ONLY the ISO 639-1 code (two letters) of the ' +
523
+ 'language the text is written in. Ignore field names in quotes. Nothing else.';
524
+
525
+ export function parrafoDeInstrucciones(brief: string): string {
526
+ const limpio = stripFilesManifest(brief).trim();
527
+ const primero = limpio.split(/\n\s*\n/)[0]!.trim();
528
+ return primero.length >= 12 ? primero : limpio.slice(0, 300);
529
+ }
530
+
531
+ /** El código ISO del idioma en que están escritas las instrucciones, o null si no se pudo saber. */
532
+ export async function idiomaDeLasInstrucciones(brief: string): Promise<string | null> {
533
+ try {
534
+ const cfg = resolverLlm(process.env);
535
+ const r = await llmChat(
536
+ { ...cfg, timeoutMs: 15_000, maxRetries: 1 },
537
+ { system: PIDE_EL_IDIOMA, user: parrafoDeInstrucciones(brief) },
538
+ );
539
+ const codigo = r.trim().toLowerCase().replace(/[^a-z]/g, '');
540
+ return /^[a-z]{2}$/.test(codigo) ? codigo : null;
541
+ } catch {
542
+ return null;
543
+ }
544
+ }
545
+
546
+ export async function nombreDelTema(brief: string, deReserva: string, idioma?: string | null): Promise<string> {
547
+ // El idioma se decide con el párrafo de las instrucciones, no con el encargo
548
+ // entero: ver `idiomaDeLasInstrucciones`.
549
+ idioma = idioma ?? (await idiomaDeLasInstrucciones(brief));
465
550
  try {
466
551
  const cfg = resolverLlm(process.env);
467
552
  const respuesta = await llmChat(
@@ -489,9 +574,19 @@ export async function nombreDelTema(brief: string, deReserva: string): Promise<s
489
574
  // Sin el manifiesto, por lo mismo que en `formatoPedido`: son 1.500
490
575
  // caracteres de presupuesto y un hash de 64 ocupa sitio sin decir nada
491
576
  // del tema. Con adjuntos cortos llegaba a colarse entero.
492
- { system: PIDE_UN_NOMBRE, user: stripFilesManifest(brief).trim().slice(0, 1_500) },
577
+ {
578
+ system: PIDE_UN_NOMBRE,
579
+ // Si quien llama ya sabe el idioma de las instrucciones, se le dice: no
580
+ // hay que volver a adivinarlo, que es donde el modelo se equivoca.
581
+ user:
582
+ stripFilesManifest(brief).trim().slice(0, 1_500) +
583
+ (idioma
584
+ ? `\n\n(Write the NAME in ${nombreDeIdioma(idioma)}, even if the data or the quoted field names ` +
585
+ `are in another language. Answer LANG: ${idioma}.)`
586
+ : ''),
587
+ },
493
588
  );
494
- const nombre = comoNombre(respuesta);
589
+ const nombre = comoNombre(lineaDelNombre(respuesta));
495
590
  if (nombre) return nombre;
496
591
  console.warn(`[salida] el modelo no dio un nombre usable; el archivo va como «${deReserva}»`);
497
592
  } catch (err) {