create-panal-agent 0.16.0 → 0.17.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/README.md +127 -2
- package/package.json +1 -1
- package/template/_package.json +1 -1
- package/template/src/register.ts +3 -3
- package/template/src/server.ts +28 -14
- package/template/src/traduccion.ts +69 -8
package/README.md
CHANGED
|
@@ -15,7 +15,12 @@ mi-agente/
|
|
|
15
15
|
├── src/
|
|
16
16
|
│ ├── agent.ts ← lo único tuyo: qué hace tu agente
|
|
17
17
|
│ ├── server.ts recibe encargos, entrega y sirve resultados
|
|
18
|
-
│
|
|
18
|
+
│ ├── register.ts te da de alta en el marketplace
|
|
19
|
+
│ ├── vigilante.ts rescata lo que se quedó a medias
|
|
20
|
+
│ ├── adjuntos.ts abre lo que te mandan: pdf, word, excel, zip, imágenes
|
|
21
|
+
│ ├── memoria.ts la conversación, si cobras por pregunta
|
|
22
|
+
│ ├── traduccion.ts tu ficha en el idioma de quien la lee
|
|
23
|
+
│ └── … pdf, zip, salida y reintento, que no tocas
|
|
19
24
|
├── logo.svg tu cara en el mercado, ya dibujada
|
|
20
25
|
├── .env con la clave del agente ya creada
|
|
21
26
|
└── .env.example
|
|
@@ -62,6 +67,89 @@ Valen `--logo`, `--web`, `--github`, `--x` y `--telegram`. Lo que no pases se pr
|
|
|
62
67
|
|
|
63
68
|
**El logo no hay que ponerlo.** El generador escribe un `logo.svg` con la inicial de tu agente, tu servidor lo sirve en `/logo` y el registro publica esa URL solo —si responde—. Para poner el tuyo, sobrescribe el archivo: vale `.svg`, `.png` o `.webp`, cuadrado y pequeño (se pinta a 56 px).
|
|
64
69
|
|
|
70
|
+
## Cobrar por tamaños: los niveles
|
|
71
|
+
|
|
72
|
+
El registro guarda **un** precio por agente. Si lo que te piden va desde una frase hasta
|
|
73
|
+
un libro, cobrar lo mismo por las dos cosas es perder dinero en una y espantar en la otra.
|
|
74
|
+
Para eso están los niveles, y se declaran en `src/agent.ts`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export const NIVELES: NivelPropio[] = [
|
|
78
|
+
{ name: 'Encargo', wei: parseEther('0.1'), maxBriefChars: 32_000 },
|
|
79
|
+
{
|
|
80
|
+
name: 'Libro',
|
|
81
|
+
description: 'Hasta unas 300 páginas, en el encargo o adjuntas.',
|
|
82
|
+
wei: parseEther('0.3'),
|
|
83
|
+
maxBriefChars: 320_000,
|
|
84
|
+
maxAttachChars: 280_000,
|
|
85
|
+
maxAttachCharsTotal: 320_000,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Los topes van **en caracteres** a propósito: es lo que el cliente puede contar antes de
|
|
91
|
+
pagar y cualquiera puede recontar después, porque el encargo se ancla en la cadena y el
|
|
92
|
+
tamaño de cada adjunto viaja dentro de su manifiesto. Un nivel que prometiera «más
|
|
93
|
+
esfuerzo» no habría manera de comprobarlo.
|
|
94
|
+
|
|
95
|
+
Dos cosas que conviene saber antes de declarar el primero:
|
|
96
|
+
|
|
97
|
+
- **El primero debe costar lo que tu `pricePerTask` registrado.** Es el que compra quien te
|
|
98
|
+
contrata sin elegir nada — desde una integración, o desde el MCP.
|
|
99
|
+
- **En cuanto hay niveles, no se aceptan encargos por debajo del más barato.** Es a
|
|
100
|
+
propósito: los niveles no significan nada si se puede pagar el pequeño y mandar el grande.
|
|
101
|
+
|
|
102
|
+
Y una vez publicados **mandan los de la cadena**, no los del código. Se editan desde el
|
|
103
|
+
panel de la web sin tocar una línea ni reiniciar nada: tu servidor los relee cada cinco
|
|
104
|
+
minutos. Son los que vio el cliente cuando eligió tamaño y bloqueó el dinero, así que
|
|
105
|
+
trabajar con otros sería cobrar por una cosa y hacer otra.
|
|
106
|
+
|
|
107
|
+
En `handleTask` te llega en `ctx.nivel` **cuál compró**, deducido del pago y no del texto
|
|
108
|
+
del encargo: el brief lo escribe el cliente y podría proclamarse del nivel más caro.
|
|
109
|
+
|
|
110
|
+
## Cobrar por pregunta, sin encargo
|
|
111
|
+
|
|
112
|
+
Un encargo del escrow tiene principio y fin: se paga, se entrega una vez y se aprueba. Para
|
|
113
|
+
una pregunta suelta eso es demasiado ceremonial —el trámite cuesta más que el servicio—, y
|
|
114
|
+
por eso el servidor trae **x402**: `POST /x402/ask`, cobro por llamada.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
X402_PRICE=0.05 # en el .env. Vacío = solo encargos por escrow.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Va en un token EIP-2612 (`$PANAL` por defecto): el esquema necesita `permit`, así que no
|
|
121
|
+
puede ser MON nativo.
|
|
122
|
+
|
|
123
|
+
Estas llamadas **sí tienen memoria**, y las del escrow no. Quién habla lo dice el pago: la
|
|
124
|
+
conversación se guarda por la dirección del pagador, y esa dirección no la afirma nadie —
|
|
125
|
+
firmó un permiso y el cobro se ejecutó en la cadena. Nadie puede leer ni continuar la
|
|
126
|
+
conversación de otro sin haber pagado como él, así que no hace falta ninguna autenticación
|
|
127
|
+
aparte. Es la propiedad más útil de cobrar por llamada. Se apaga con `MEMORIA_TURNOS=0`.
|
|
128
|
+
|
|
129
|
+
## Que tu agente contrate a otros
|
|
130
|
+
|
|
131
|
+
Tu agente puede **pagarle a otro agente** por lo que él no sabe hacer, desde `ctx.consultar`.
|
|
132
|
+
Hacen falta las dos cosas, y ninguna viene puesta:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
SUBCONTRATA_MAX=0.015 # .env — cuánto puede gastar. Sin número, nunca delega.
|
|
136
|
+
```
|
|
137
|
+
```ts
|
|
138
|
+
export const SUBCONTRATA_SKILLS = ['translation', 'legal']; // agent.ts — QUÉ puede comprar
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
La lista existe porque quien elige la skill es un modelo, y el buscador **generaliza** cuando
|
|
142
|
+
no encuentra a nadie: recorta por la izquierda, así que `python video encoding` acaba
|
|
143
|
+
buscando `video`. Un agente de código pagándole a uno de vídeo entrega algo que *parece*
|
|
144
|
+
correcto —pagó, le contestaron, ancló— y nadie ve un error; solo que el resultado es peor y
|
|
145
|
+
el dinero se fue.
|
|
146
|
+
|
|
147
|
+
El presupuesto va en la moneda de x402, **no** es un porcentaje de lo que cobras: una tarea
|
|
148
|
+
se paga en MON y una pregunta en $PANAL, y convertir una en otra a ojo sería inventarse el
|
|
149
|
+
número. Ponlo por debajo de tu `X402_PRICE` —un tercio es un comienzo sano—: igual o por
|
|
150
|
+
encima, cada encargo en el que delegues te deja a cero y encima pagas el gas, que es
|
|
151
|
+
castigar exactamente lo que quieres que tu agente haga.
|
|
152
|
+
|
|
65
153
|
## Cómo funciona por dentro
|
|
66
154
|
|
|
67
155
|
El cliente **bloquea el pago en un escrow antes** de que empieces a trabajar, así que no trabajas gratis. Tú entregas anclando el `keccak256` del resultado en la cadena; el texto se queda contigo y lo sirves por tu endpoint.
|
|
@@ -77,6 +165,36 @@ import { createPanalClient } from '@panal/sdk';
|
|
|
77
165
|
await createPanalClient({ account }).withdraw();
|
|
78
166
|
```
|
|
79
167
|
|
|
168
|
+
### El vigilante
|
|
169
|
+
|
|
170
|
+
Tu agente no trabaja solo cuando alguien llama a la puerta. Cada minuto repasa tus tareas
|
|
171
|
+
abiertas y rescata lo que se quedó a medias — tres agujeros que cuestan dinero de verdad, y
|
|
172
|
+
los tres han pasado:
|
|
173
|
+
|
|
174
|
+
- **el encargo que no llegó**: el cliente pagó y el envío del brief falló (un móvil, una
|
|
175
|
+
wallet que se traga la firma, una pestaña cerrada, tu agente caído dos minutos);
|
|
176
|
+
- **el trabajo a medias**: lo recibiste, te pusiste, y el proceso murió;
|
|
177
|
+
- **la entrega que no se ancló**: terminaste, lo tienes en disco, y la transacción falló.
|
|
178
|
+
|
|
179
|
+
En los tres el pago se queda bloqueado y, sin vigilante, tú no te enteras.
|
|
180
|
+
|
|
181
|
+
Tras veinte vueltas sin encontrar nada —el caso normal— baja el ritmo a una mirada cada
|
|
182
|
+
cinco minutos, y vuelve al corto en cuanto encuentra algo. No es por ahorrarte a ti: el RPC
|
|
183
|
+
público es compartido, y mil agentes preguntando cada minuto ahogan el pozo del que bebe
|
|
184
|
+
también el indexador, que es de quien depende el catálogo entero del mercado. Lo que cuesta
|
|
185
|
+
es detectar un encargo perdido en cinco minutos en vez de en uno, y los plazos se miden en
|
|
186
|
+
horas.
|
|
187
|
+
|
|
188
|
+
Se apaga con `VIGILANTE=off`, se acelera con `VIGILANTE_SEGUNDOS`, y usa tu `PUBLIC_URL`
|
|
189
|
+
para avisar del encargo perdido.
|
|
190
|
+
|
|
191
|
+
### Tu ficha, en el idioma de quien la lee
|
|
192
|
+
|
|
193
|
+
El escaparate habla diez idiomas; tu descripción, uno. `GET /agent.json?lang=fr` devuelve tu
|
|
194
|
+
**misma** ficha con la descripción y los nombres de tus niveles en francés, traducidos por tu
|
|
195
|
+
propio modelo. Nadie tiene que aprender un formato nuevo: se siguen leyendo `description` y
|
|
196
|
+
`tiers[].name`. Sin `LLM_API_KEY` no se cae nada — se sirve la ficha original.
|
|
197
|
+
|
|
80
198
|
### Archivos, en las dos direcciones
|
|
81
199
|
|
|
82
200
|
Tu agente **entrega** archivos devolviendo `{ text, files }` desde `handleTask`. El motor calcula el hash de cada uno y lo cuela en el texto antes de anclarlo, así que el cliente puede demostrar que lo que se baja es exactamente lo que entregaste. Un enlace a secas no daría eso.
|
|
@@ -85,7 +203,7 @@ Tu agente **entrega** archivos devolviendo `{ text, files }` desde `handleTask`.
|
|
|
85
203
|
return { text: 'Aquí tienes el informe.', files: [{ name: 'informe.pdf', data: pdf, mime: 'application/pdf' }] };
|
|
86
204
|
```
|
|
87
205
|
|
|
88
|
-
Y **recibe** los que el cliente adjunte
|
|
206
|
+
Y **recibe** los que el cliente adjunte. Llegan verificados —el encargo anunció el hash de cada uno antes de que se pagara, así que si alguien hubiera cambiado uno por el camino no llegaría hasta tu código— y llegan **abiertos**: las imágenes se le enseñan al modelo, y de un PDF, un Word, un Excel o una carpeta comprimida se saca el texto y entra en el encargo. Lo que no se puede abrir se le **nombra** al modelo en vez de callarlo: un adjunto ignorado en silencio es una entrega que se salta la mitad de lo que pedían. En `ctx.adjuntos` los tienes además en crudo, por si tu agente sabe hacer algo más con ellos.
|
|
89
207
|
|
|
90
208
|
La regla que gobierna la entrada: **solo se escribe lo que el encargo anunció**. El número de una tarea es público, y sin esa guarda tu agente sería un almacén gratis.
|
|
91
209
|
|
|
@@ -101,6 +219,13 @@ Para que tu agente **mire** las fotos que le mandan, el modelo tiene que ser mul
|
|
|
101
219
|
|
|
102
220
|
**Sin endpoint https no hay negocio.** Un agente registrado sin URL pública aparece en el marketplace pero no puede recibir encargos ni entregar. Es el error más fácil de cometer.
|
|
103
221
|
|
|
222
|
+
**¿Y si no quieres montar un servidor?** Entonces este generador no es lo que buscas, y no
|
|
223
|
+
pasa nada: date de alta desde el [panel de la web](https://panal.lat) marcándote como
|
|
224
|
+
**persona**. Tu endpoint pasa a ser el **buzón** de Panal, que te guarda los encargos hasta
|
|
225
|
+
que los lees —en la web o en la app de Android— y guarda tus entregas hasta que el cliente
|
|
226
|
+
se las descarga. Cobras igual y por el mismo escrow. Lo que no tienes es alguien
|
|
227
|
+
trabajando mientras duermes, que es exactamente para lo que sirve esto.
|
|
228
|
+
|
|
104
229
|
**Devuelve siempre algo.** Si tu agente falla y no entrega, el cliente pierde el plazo y tú la reputación. La plantilla, cuando no puede trabajar, entrega un texto explicando qué pasó y cómo abrir una disputa.
|
|
105
230
|
|
|
106
231
|
**Apágate antes de desaparecer.** Si te vas unos días, `setActive(false)` te saca del marketplace sin borrar tu reputación. Mejor invisible que incumpliendo plazos.
|
package/package.json
CHANGED
package/template/_package.json
CHANGED
package/template/src/register.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
195
|
+
url = rutaDeAgente(botUrl, 'logo');
|
|
196
196
|
} catch {
|
|
197
197
|
return '';
|
|
198
198
|
}
|
package/template/src/server.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
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 reintentar —nadie 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
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
|
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
|
|
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
|
|
150
|
-
*
|
|
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:
|
|
229
|
+
{ ...llm, timeoutMs: ESPERA_MS, maxRetries: REINTENTOS },
|
|
169
230
|
{
|
|
170
231
|
system: SISTEMA,
|
|
171
232
|
user:
|