primocode 8.22.0 → 8.22.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/lib/act.js +41 -1
- package/lib/referencia.js +145 -18
- package/package.json +1 -1
package/lib/act.js
CHANGED
|
@@ -349,6 +349,12 @@ async function runAct(userPrompt, opts) {
|
|
|
349
349
|
// Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
|
|
350
350
|
const jaChamado = repeticao.novo();
|
|
351
351
|
let voltasEmFalso = 0;
|
|
352
|
+
// Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
|
|
353
|
+
// para de rodar e devolve ORDEM DE SEGUIR: sem isto o agente entrava em
|
|
354
|
+
// ciclo — buscava, não achava, lia "tente outra consulta", buscava de
|
|
355
|
+
// novo — até a cota do minuto acabar e o modelo responder vazio. Foi
|
|
356
|
+
// assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
|
|
357
|
+
let pesquisasSecas = 0;
|
|
352
358
|
let rodadasParadas = 0;
|
|
353
359
|
|
|
354
360
|
const messages = [];
|
|
@@ -515,9 +521,22 @@ async function runAct(userPrompt, opts) {
|
|
|
515
521
|
// o modelo grátis engasgando. Cair direto no "parei sem concluir"
|
|
516
522
|
// jogava a tarefa fora quando repetir a MESMA rodada quase sempre
|
|
517
523
|
// resolve — visto em produção no primeiro pedido de vídeo.
|
|
518
|
-
if (!finalText && respostasVazias <
|
|
524
|
+
if (!finalText && respostasVazias < 3 && !PARAR) {
|
|
519
525
|
respostasVazias++;
|
|
520
526
|
if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo respondeu vazio — tentando de novo'));
|
|
527
|
+
// Repetir o MESMO contexto costuma produzir o mesmo vazio: se
|
|
528
|
+
// o turno anterior já engasgou, o que falta é um empurrão
|
|
529
|
+
// curto e concreto. A partir da segunda vez, entra um pedido
|
|
530
|
+
// explícito de fechamento — barato em tokens (é justamente
|
|
531
|
+
// quando a cota está no fim) e suficiente para o modelo
|
|
532
|
+
// escrever a resposta ou chamar finish.
|
|
533
|
+
if (respostasVazias >= 2) {
|
|
534
|
+
messages.push({
|
|
535
|
+
role: 'user',
|
|
536
|
+
content: 'Sua última resposta veio vazia. Não pesquise mais nem explique o processo: '
|
|
537
|
+
+ 'faça agora a entrega pedida com o que você já tem e finalize.',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
521
540
|
iteration--;
|
|
522
541
|
continue;
|
|
523
542
|
}
|
|
@@ -616,6 +635,22 @@ async function runAct(userPrompt, opts) {
|
|
|
616
635
|
temPasta = true;
|
|
617
636
|
}
|
|
618
637
|
|
|
638
|
+
if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
|
|
639
|
+
voltasEmFalso++;
|
|
640
|
+
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
641
|
+
console.log(ident(profundidade) + c.muted(' ⎿ ') + c.warn('3 pesquisas sem resultado — siga com o que já tem'));
|
|
642
|
+
messages.push({
|
|
643
|
+
role: 'tool',
|
|
644
|
+
tool_call_id: tc.id,
|
|
645
|
+
content: 'PARE DE PESQUISAR. Três buscas seguidas não trouxeram fonte nenhuma; '
|
|
646
|
+
+ 'mais uma vai gastar a cota e devolver o mesmo nada. FAÇA AGORA a entrega '
|
|
647
|
+
+ 'pedida com o que você já sabe. Se faltar um fato específico (número, data, '
|
|
648
|
+
+ 'preço, nome de pessoa), NÃO invente: escreva a peça sem ele e avise o '
|
|
649
|
+
+ 'usuário no final, em uma linha, o que não deu para confirmar.',
|
|
650
|
+
});
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
619
654
|
algoRodou = true;
|
|
620
655
|
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
621
656
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
@@ -626,6 +661,11 @@ async function runAct(userPrompt, opts) {
|
|
|
626
661
|
// não corrige um teste quebrado noutro lugar; antes isto zerava
|
|
627
662
|
// a lista inteira e o agente conseguia dizer "concluído" com uma
|
|
628
663
|
// falha real ainda pendente.
|
|
664
|
+
if (tc.name === 'pesquisar_referencia') {
|
|
665
|
+
if (result && result.ok === false) pesquisasSecas++;
|
|
666
|
+
else pesquisasSecas = 0; // achou fonte: o ciclo se rompeu
|
|
667
|
+
}
|
|
668
|
+
|
|
629
669
|
if (result && result.ok === false) {
|
|
630
670
|
falhasPendentes.push({
|
|
631
671
|
tool: tc.name,
|
package/lib/referencia.js
CHANGED
|
@@ -244,21 +244,116 @@ function relevante(cand, palavras) {
|
|
|
244
244
|
* Foi o que salvou este caso — parisgroup.ai abre e tem o texto todo, enquanto
|
|
245
245
|
* DuckDuckGo devolvia 202 e o Bing devolvia Paris, a cidade.
|
|
246
246
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
247
|
+
* Reconhece nome de DUAS palavras com maiúscula ("Paris Group") e também de
|
|
248
|
+
* UMA palavra, mesmo em minúscula — que é como as pessoas digitam no meio de
|
|
249
|
+
* uma frase. Exigir duas maiúsculas custou caro: para "crie um vídeo para a
|
|
250
|
+
* tostudy, plataforma de aprendizagem com IA" a função devolvia lista vazia,
|
|
251
|
+
* o buscador não conhecia o nome, a pesquisa falhava — e o agente repetia
|
|
252
|
+
* buscas até a tarefa morrer, com tostudy.ai no ar o tempo todo.
|
|
250
253
|
*/
|
|
254
|
+
|
|
255
|
+
// Palavras que aparecem em qualquer pedido e NUNCA são o nome da marca. Sem
|
|
256
|
+
// esta peneira o chute viraria plataforma.com, aprendizagem.com, video.com.
|
|
257
|
+
const NAO_E_MARCA = new Set([
|
|
258
|
+
'plataforma', 'plataformas', 'empresa', 'startup', 'site', 'oficial', 'aplicativo', 'app',
|
|
259
|
+
'aprendizagem', 'aprendizado', 'ensino', 'educacao', 'educacional', 'curso', 'cursos', 'escola',
|
|
260
|
+
'video', 'videos', 'reels', 'anuncio', 'campanha', 'marketing', 'institucional', 'apresentacao',
|
|
261
|
+
'inteligencia', 'artificial', 'brasil', 'brasileira', 'sobre', 'para', 'como', 'uma', 'que',
|
|
262
|
+
'crie', 'criar', 'faca', 'fazer', 'quero', 'preciso', 'estudo', 'estudos', 'tecnologia',
|
|
263
|
+
'software', 'sistema', 'servico', 'servicos', 'produto', 'produtos', 'negocio', 'digital',
|
|
264
|
+
'documentacao', 'tutorial', 'exemplo', 'exemplos', 'guia', 'referencia',
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
function _slug(txt) {
|
|
268
|
+
return String(txt || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
269
|
+
.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
270
|
+
}
|
|
271
|
+
|
|
251
272
|
function sitesProvaveis(consulta) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
273
|
+
const texto = String(consulta || '');
|
|
274
|
+
const marcas = [];
|
|
275
|
+
|
|
276
|
+
// Entre aspas é a forma de a pessoa dizer "o nome é ESTE". Vem primeiro.
|
|
277
|
+
const citado = /["“'”]([^"“'”]{3,40})["“'”]/.exec(texto);
|
|
278
|
+
if (citado) marcas.push({ rotulo: citado[1].trim(), slug: _slug(citado[1]) });
|
|
279
|
+
|
|
280
|
+
const duplo = /\b([A-ZÀ-Ý][\wÀ-ɏ]{1,})\s+([A-ZÀ-Ý][\wÀ-ɏ]{1,})\b/.exec(texto);
|
|
281
|
+
if (duplo) marcas.push({ rotulo: `${duplo[1]} ${duplo[2]}`, slug: _slug(duplo[1] + duplo[2]) });
|
|
282
|
+
|
|
283
|
+
// Marca de UMA palavra só é chutada quando a consulta dá SINAL de que
|
|
284
|
+
// fala de um negócio ("plataforma", "empresa", "app", "site"...) ou põe o
|
|
285
|
+
// nome entre aspas. Sem essa exigência, "react hooks tutorial" viraria
|
|
286
|
+
// react.com e "como fazer um bolo" viraria fazer.com — dez buscas
|
|
287
|
+
// inúteis para cada pesquisa técnica.
|
|
288
|
+
const SINAL_DE_MARCA = /\b(plataforma|empresa|startup|site|marca|neg[óo]cio|produto|servi[çc]o|app|aplicativo|institucional|cliente|fabricante|loja|ag[êe]ncia|ecommerce|e-commerce|saas)\b/i;
|
|
289
|
+
const temSinal = SINAL_DE_MARCA.test(texto) || !!citado;
|
|
290
|
+
if (temSinal) {
|
|
291
|
+
const util = (bruto) => {
|
|
292
|
+
const s = _slug(bruto);
|
|
293
|
+
return (s.length >= 4 && s.length <= 24 && !NAO_E_MARCA.has(s) && !/^\d+$/.test(s)) ? s : null;
|
|
294
|
+
};
|
|
295
|
+
// O nome vem quase sempre depois de artigo ou preposição: "um mouse
|
|
296
|
+
// DA multilaser", "vídeo para A tostudy". Pegar simplesmente a
|
|
297
|
+
// primeira palavra não-genérica escolhia o verbo do pedido
|
|
298
|
+
// ("anunciar") e chutava anunciar.ai.
|
|
299
|
+
const depoisDeArtigo = [...texto.matchAll(/\b(?:d[aeo]s?|[ao]|n[ao]s?|à)\s+([A-Za-zÀ-ÿ][\wÀ-ÿ-]{3,23})\b/gi)];
|
|
300
|
+
for (const m of depoisDeArtigo) {
|
|
301
|
+
const s = util(m[1]);
|
|
302
|
+
if (s) { marcas.push({ rotulo: m[1], slug: s }); break; }
|
|
303
|
+
}
|
|
304
|
+
// Só quando o artigo não entregou nada: senão o verbo do pedido
|
|
305
|
+
// ("anunciar") entraria junto e gastaria dez sondas à toa.
|
|
306
|
+
if (!marcas.length) {
|
|
307
|
+
for (const bruto of texto.split(/[\s,.;:!?()]+/)) {
|
|
308
|
+
const s = util(bruto);
|
|
309
|
+
if (s) { marcas.push({ rotulo: bruto, slug: s }); break; }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const vistos = new Set();
|
|
315
|
+
const saida = [];
|
|
316
|
+
for (const m of marcas.slice(0, 2)) {
|
|
317
|
+
if (!m.slug || m.slug.length < 4 || vistos.has(m.slug)) continue;
|
|
318
|
+
vistos.add(m.slug);
|
|
319
|
+
for (const tld of ['ai', 'com', 'com.br', 'io', 'co']) {
|
|
320
|
+
// Com e sem "www": um resolve onde o outro não, e quem decide é a
|
|
321
|
+
// hospedagem. Como estes candidatos passam pela sonda antes de
|
|
322
|
+
// entrar na fila de leitura, tentar os dois não custa vaga.
|
|
323
|
+
saida.push({ titulo: `${m.rotulo} — site oficial`, url: `https://${m.slug}.${tld}`, nota: 'site provável da empresa' });
|
|
324
|
+
saida.push({ titulo: `${m.rotulo} — site oficial`, url: `https://www.${m.slug}.${tld}`, nota: 'site provável da empresa' });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return saida;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Sonda os domínios chutados e devolve só os que responderam, com o HTML já
|
|
332
|
+
* na mão. Sem isto, dez chutes entrariam na fila de leitura e expulsariam os
|
|
333
|
+
* resultados de verdade do buscador — e a maioria deles nem existe.
|
|
334
|
+
*/
|
|
335
|
+
async function chutesVivos(chutes, limite = 2) {
|
|
336
|
+
if (!chutes.length) return { vivos: [], html: new Map() };
|
|
337
|
+
// 12 s, não 6: a sonda roda em paralelo e a home de uma empresa hoje pesa
|
|
338
|
+
// centenas de KB. Com 6 s o site CERTO chegava atrasado, era descartado
|
|
339
|
+
// como morto, e a pesquisa falhava com o site no ar — medido.
|
|
340
|
+
const respostas = await Promise.all(chutes.slice(0, 12).map(async (c) => {
|
|
341
|
+
const html = await pegar(c.url, { timeout: 12000 });
|
|
342
|
+
return html && html.length > 500 ? { cand: c, html } : null;
|
|
261
343
|
}));
|
|
344
|
+
const vivos = [];
|
|
345
|
+
const html = new Map();
|
|
346
|
+
const dominios = new Set();
|
|
347
|
+
for (const r of respostas.filter(Boolean)) {
|
|
348
|
+
// www.x.ai e x.ai são o mesmo site: vale o primeiro que respondeu.
|
|
349
|
+
const raiz = r.cand.url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
|
|
350
|
+
if (dominios.has(raiz)) continue;
|
|
351
|
+
dominios.add(raiz);
|
|
352
|
+
vivos.push(r.cand);
|
|
353
|
+
html.set(r.cand.url, r.html);
|
|
354
|
+
if (vivos.length >= limite) break;
|
|
355
|
+
}
|
|
356
|
+
return { vivos, html };
|
|
262
357
|
}
|
|
263
358
|
|
|
264
359
|
// Links que não são referência de nada — resultado de busca sujo.
|
|
@@ -460,7 +555,17 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
460
555
|
// O site provável da empresa vai NA FRENTE: quando a consulta nomeia um
|
|
461
556
|
// negócio, o site dele é a melhor fonte que existe, e não depende de
|
|
462
557
|
// buscador nenhum estar de bom humor.
|
|
463
|
-
|
|
558
|
+
// Os chutes de domínio são SONDADOS antes de entrar na fila: só os que
|
|
559
|
+
// respondem ocupam vaga. São dez palpites (5 TLDs x com/sem www) e a
|
|
560
|
+
// maioria não existe — enfileirar todos expulsaria os resultados reais
|
|
561
|
+
// do buscador. A sonda roda em paralelo e o HTML já volta aproveitado.
|
|
562
|
+
const palpites = sitesProvaveis(termo);
|
|
563
|
+
const { vivos, html: htmlChutes } = await chutesVivos(palpites);
|
|
564
|
+
// A sonda ORDENA, não faz porteiro. Ela é uma corrida contra o relógio
|
|
565
|
+
// contra home pesada e rede lenta, e perdê-la não pode significar perder
|
|
566
|
+
// o site certo: quando nada responde a tempo, os primeiros palpites vão
|
|
567
|
+
// para a fila mesmo assim e o laço de leitura tenta de novo, sem pressa.
|
|
568
|
+
const chutes = vivos.length ? vivos : palpites.slice(0, 3);
|
|
464
569
|
const palavras = palavrasDaConsulta(termo);
|
|
465
570
|
const brutos = [...chutes, ...web, ...so, ...docsConhecidas(termo)];
|
|
466
571
|
|
|
@@ -481,7 +586,15 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
481
586
|
provavelCausa: descartados
|
|
482
587
|
? `Vieram ${descartados} link(s), mas nenhum era sobre "${termo}" — o buscador não achou o assunto e devolveu página aleatória. Fonte errada é pior que fonte nenhuma, então foram descartadas.`
|
|
483
588
|
: 'Os buscadores costumam recusar chamadas automáticas (bloqueio ou limite por excesso de buscas seguidas), e não a sua conexão.',
|
|
484
|
-
|
|
589
|
+
// "Tente outra consulta" era um convite ao ciclo: o agente
|
|
590
|
+
// reformulava a busca, falhava igual, e repetia até a cota acabar.
|
|
591
|
+
// A ordem agora é explícita — no máximo UMA nova tentativa, e por
|
|
592
|
+
// um caminho DIFERENTE (o site direto), senão segue sem a fonte.
|
|
593
|
+
comoCorrigir: 'NÃO repita esta busca com outras palavras — o buscador não conhece o assunto e '
|
|
594
|
+
+ 'vai devolver o mesmo nada. Se souber (ou conseguir deduzir) o site, tente UMA vez '
|
|
595
|
+
+ 'browser_open + browser_read nele. Se também não abrir, SIGA COM A TAREFA usando o que '
|
|
596
|
+
+ 'você já sabe e avise ao usuário, no final, qual fato não deu para confirmar. Nunca invente '
|
|
597
|
+
+ 'dado sobre empresa, número, data, preço ou pessoa.',
|
|
485
598
|
};
|
|
486
599
|
}
|
|
487
600
|
|
|
@@ -491,7 +604,9 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
491
604
|
const chutesLidos = [];
|
|
492
605
|
for (const cand of candidatos) {
|
|
493
606
|
if (lidas.length >= paginas) break;
|
|
494
|
-
|
|
607
|
+
// A sonda já baixou o HTML dos chutes: reaproveita em vez de pedir
|
|
608
|
+
// a mesma página duas vezes.
|
|
609
|
+
const html = htmlChutes.get(cand.url) || await pegar(cand.url);
|
|
495
610
|
if (!html) continue;
|
|
496
611
|
const texto = textoDe(html);
|
|
497
612
|
if (texto.length < 200) continue;
|
|
@@ -502,7 +617,17 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
502
617
|
// da página, sim. Sem esta prova o agente escreveria sobre a errada.
|
|
503
618
|
const ehChute = cand.nota === 'site provável da empresa';
|
|
504
619
|
if (ehChute) {
|
|
505
|
-
|
|
620
|
+
let nota = quantoBate({ titulo: texto.slice(0, 4000), url: cand.url }, palavras);
|
|
621
|
+
// O NOME DA MARCA no domínio, confirmado no texto da página, já é
|
|
622
|
+
// prova — e é a prova mais forte que existe. Exigir duas palavras
|
|
623
|
+
// genéricas da consulta ("plataforma", "aprendizagem") reprovava o
|
|
624
|
+
// site certo: tostudy.ai abria, dizia "ToStudy" na primeira linha,
|
|
625
|
+
// e mesmo assim era descartado por não repetir o vocabulário do
|
|
626
|
+
// pedido. O agente então repetia buscas até a tarefa morrer.
|
|
627
|
+
const marca = (cand.url.match(/^https?:\/\/(?:www\.)?([a-z0-9-]+)\./i) || [])[1] || '';
|
|
628
|
+
const textoLimpo = texto.slice(0, 8000).normalize('NFD')
|
|
629
|
+
.replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
630
|
+
if (marca.length >= 4 && textoLimpo.includes(marca.toLowerCase())) nota = Math.max(nota, 2);
|
|
506
631
|
if (nota < Math.min(2, palavras.length)) continue;
|
|
507
632
|
// Passar não basta: entre dois domínios do MESMO nome, fica o que
|
|
508
633
|
// fala mais do assunto. parisgroup.ai e parisgroup.co os dois
|
|
@@ -532,8 +657,10 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
532
657
|
error: 'achei os links mas não consegui abrir nenhum',
|
|
533
658
|
consulta: termo,
|
|
534
659
|
links: candidatos.map((c) => c.url),
|
|
535
|
-
comoCorrigir: 'Tente browser_open + browser_read num desses links
|
|
536
|
-
+ '
|
|
660
|
+
comoCorrigir: 'Tente browser_open + browser_read num desses links — UMA vez. Não refaça a busca '
|
|
661
|
+
+ 'com outras palavras: os links são estes mesmos. Se nada abrir, SIGA COM A TAREFA e diga ao '
|
|
662
|
+
+ 'usuário, no final, que não conseguiu confirmar os fatos — NUNCA invente dado sobre empresa, '
|
|
663
|
+
+ 'data, preço ou pessoa.',
|
|
537
664
|
};
|
|
538
665
|
}
|
|
539
666
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.22.
|
|
3
|
+
"version": "8.22.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|