primocode 8.8.0 → 8.9.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/bin/primocode.js +24 -47
- package/lib/referencia.js +29 -5
- package/package.json +1 -1
- package/studio/server.py +116 -5
package/bin/primocode.js
CHANGED
|
@@ -540,11 +540,11 @@ async function sendPrompt(userText) {
|
|
|
540
540
|
|
|
541
541
|
// O pedido passa por uma cabeça antes de virar trabalho: conserta o
|
|
542
542
|
// que está torto, diz qual é o entregável e o que conta como pronto.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
const passos = await dividirTarefa(
|
|
543
|
+
// UMA chamada faz as duas coisas: arruma o pedido torto e, se fizer
|
|
544
|
+
// sentido, divide em passos. Eram duas idas ao modelo por pedido, e
|
|
545
|
+
// num plano limitado por REQUISIÇÕES (o grátis da Groq são 30 por
|
|
546
|
+
// minuto) cada chamada extra custa tanto quanto token.
|
|
547
|
+
const passos = await dividirTarefa(prompt);
|
|
548
548
|
|
|
549
549
|
let r = null;
|
|
550
550
|
let jaFeito = '';
|
|
@@ -585,66 +585,43 @@ async function sendPrompt(userText) {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
/**
|
|
588
|
-
*
|
|
588
|
+
* Arruma o pedido e divide em passos — as duas coisas, ESCONDIDAS.
|
|
589
|
+
*
|
|
590
|
+
* ARRUMAR: gente pede curto e torto ("cria um vidro anunciando o founders
|
|
591
|
+
* ai"). Sem isso o agente adivinha, e adivinha errado. É oculto porque
|
|
592
|
+
* encanamento não é conversa: o usuário quer ver a coisa acontecendo, não um
|
|
593
|
+
* relatório do que a máquina entendeu — e não há o que aprovar, já que o
|
|
594
|
+
* refino não decide nada, só desambigua o que ele já disse.
|
|
589
595
|
*
|
|
590
|
-
*
|
|
591
|
-
* crie um vídeo" não cabe numa conversa
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
596
|
+
* DIVIDIR: o motivo é aritmético. Com 6.000 tokens por minuto, "pesquise
|
|
597
|
+
* sobre a X e crie um vídeo" não cabe numa conversa só — custo fixo ~3.700,
|
|
598
|
+
* spec do formato ~1.400, fatos da pesquisa o resto, e sobram 512 para o
|
|
599
|
+
* agente escrever. Em dois pedidos cada um cabe folgado, porque cada um leva
|
|
600
|
+
* só as ferramentas e o prompt do seu passo.
|
|
595
601
|
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
* está a fronteira barata, e pode partir num pedaço que ainda não cabe.
|
|
602
|
+
* As duas numa chamada só: elas leem o mesmo texto, e num plano limitado por
|
|
603
|
+
* requisições por minuto a segunda ida ao modelo custa tanto quanto token.
|
|
599
604
|
*
|
|
600
|
-
* Falhou?
|
|
605
|
+
* Falhou? O pedido do usuário, inteiro, como ele escreveu.
|
|
601
606
|
*/
|
|
602
607
|
async function dividirTarefa(pedido) {
|
|
603
608
|
const inteiro = [{ titulo: 'tarefa', pedido }];
|
|
609
|
+
if (!pedido || pedido.trim().length < 12) return inteiro;
|
|
610
|
+
const sp = new Spinner('Primo Code pensando').start();
|
|
604
611
|
try {
|
|
605
612
|
const r = await api.requestPlano(state.config.server, {
|
|
606
613
|
prompt: pedido, model: state.config.model, token: state.config.token,
|
|
607
614
|
});
|
|
615
|
+
sp.stop();
|
|
608
616
|
const passos = (r && Array.isArray(r.passos) && r.passos.length) ? r.passos : inteiro;
|
|
609
617
|
if (passos.length > 1) console.log(c.dim(` ⎿ ${passos[0].titulo}`));
|
|
610
618
|
return passos;
|
|
611
619
|
} catch {
|
|
620
|
+
sp.stop(); // preparar o pedido é melhoria, não requisito
|
|
612
621
|
return inteiro;
|
|
613
622
|
}
|
|
614
623
|
}
|
|
615
624
|
|
|
616
|
-
/**
|
|
617
|
-
* Estrutura o pedido antes de mandar o agente executar.
|
|
618
|
-
*
|
|
619
|
-
* Gente pede curto e torto — "cria um vidro anunciando o founders ai". Sem
|
|
620
|
-
* isto o agente adivinha, e adivinha errado: sai o genérico, ou ele pesquisa
|
|
621
|
-
* em círculo porque não sabe o que é "pronto". Uma passada barata antes de
|
|
622
|
-
* começar vira ordem de serviço: o que entregar, com que fatos, quando parar.
|
|
623
|
-
*
|
|
624
|
-
* É OCULTO de propósito. A primeira versão mostrava "entendi assim:" na tela,
|
|
625
|
-
* e o dono cortou: encanamento não é conversa. O usuário pediu uma coisa e
|
|
626
|
-
* quer ver a coisa acontecendo, não um relatório do que a máquina entendeu.
|
|
627
|
-
* O refino também não decide nada — só desambigua o que ele já disse —, então
|
|
628
|
-
* não há o que ele precise aprovar.
|
|
629
|
-
*
|
|
630
|
-
* Nunca bloqueia: se o refino falhar ou vier estranho, vale o pedido original.
|
|
631
|
-
*/
|
|
632
|
-
async function refinarPedido(cru) {
|
|
633
|
-
if (!cru || cru.trim().length < 12) return cru;
|
|
634
|
-
const sp = new Spinner('Primo Code pensando').start();
|
|
635
|
-
try {
|
|
636
|
-
const r = await api.requestRefino(state.config.server, {
|
|
637
|
-
prompt: cru, model: state.config.model, token: state.config.token,
|
|
638
|
-
});
|
|
639
|
-
sp.stop();
|
|
640
|
-
if (!r || !r.refinado || !r.prompt) return cru;
|
|
641
|
-
return r.prompt;
|
|
642
|
-
} catch {
|
|
643
|
-
sp.stop(); // refinar é melhoria, não requisito
|
|
644
|
-
return cru;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
625
|
/**
|
|
649
626
|
* Atualiza o resumo da conversa, se já compensar.
|
|
650
627
|
*
|
package/lib/referencia.js
CHANGED
|
@@ -224,13 +224,17 @@ const GENERICAS = new Set(['sobre', 'empresa', 'company', 'overview', 'about', '
|
|
|
224
224
|
* "paris" e não tem nada a ver). Consulta de uma palavra só se contenta com
|
|
225
225
|
* ela mesma.
|
|
226
226
|
*/
|
|
227
|
-
function
|
|
228
|
-
if (!palavras.length) return true;
|
|
227
|
+
function quantoBate(cand, palavras) {
|
|
229
228
|
const alvo = `${cand.titulo || ''} ${cand.url || ''}`
|
|
230
229
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
231
230
|
let batidas = 0;
|
|
232
231
|
for (const p of palavras) if (alvo.includes(p)) batidas++;
|
|
233
|
-
return batidas
|
|
232
|
+
return batidas;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function relevante(cand, palavras) {
|
|
236
|
+
if (!palavras.length) return true;
|
|
237
|
+
return quantoBate(cand, palavras) >= Math.min(2, palavras.length);
|
|
234
238
|
}
|
|
235
239
|
|
|
236
240
|
/**
|
|
@@ -482,6 +486,9 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
482
486
|
}
|
|
483
487
|
|
|
484
488
|
const lidas = [];
|
|
489
|
+
// Os chutes de domínio ficam de lado até o fim: só então dá para saber
|
|
490
|
+
// qual deles fala mais do assunto.
|
|
491
|
+
const chutesLidos = [];
|
|
485
492
|
for (const cand of candidatos) {
|
|
486
493
|
if (lidas.length >= paginas) break;
|
|
487
494
|
const html = await pegar(cand.url);
|
|
@@ -493,7 +500,16 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
493
500
|
// parisgroup.com (um grupo de varejo no exterior) e parisgroup.ai (a
|
|
494
501
|
// empresa que o usuário quis). O nome não distingue os dois — o texto
|
|
495
502
|
// da página, sim. Sem esta prova o agente escreveria sobre a errada.
|
|
496
|
-
|
|
503
|
+
const ehChute = cand.nota === 'site provável da empresa';
|
|
504
|
+
if (ehChute) {
|
|
505
|
+
const nota = quantoBate({ titulo: texto.slice(0, 4000), url: cand.url }, palavras);
|
|
506
|
+
if (nota < Math.min(2, palavras.length)) continue;
|
|
507
|
+
// Passar não basta: entre dois domínios do MESMO nome, fica o que
|
|
508
|
+
// fala mais do assunto. parisgroup.ai e parisgroup.co os dois
|
|
509
|
+
// "batiam", e o agente recebia as duas empresas como se fossem uma.
|
|
510
|
+
chutesLidos.push({ nota, item: { titulo: cand.titulo, url: cand.url, nota: cand.nota, trecho: texto.slice(0, MAX_TEXTO) } });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
497
513
|
lidas.push({
|
|
498
514
|
titulo: cand.titulo,
|
|
499
515
|
url: cand.url,
|
|
@@ -502,6 +518,14 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
502
518
|
});
|
|
503
519
|
}
|
|
504
520
|
|
|
521
|
+
// O melhor chute entra na frente: quando a empresa tem site, ele é a
|
|
522
|
+
// fonte mais confiável que existe sobre ela.
|
|
523
|
+
if (chutesLidos.length) {
|
|
524
|
+
chutesLidos.sort((a, b) => b.nota - a.nota);
|
|
525
|
+
lidas.unshift(chutesLidos[0].item);
|
|
526
|
+
if (lidas.length > paginas) lidas.length = paginas;
|
|
527
|
+
}
|
|
528
|
+
|
|
505
529
|
if (!lidas.length) {
|
|
506
530
|
return {
|
|
507
531
|
ok: false,
|
|
@@ -524,4 +548,4 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
524
548
|
}
|
|
525
549
|
|
|
526
550
|
module.exports = { pesquisar, buscar, stackoverflow, textoDe, buscarWikipedia, buscarRespostaDDG, pegar,
|
|
527
|
-
relevante, palavrasDaConsulta, sitesProvaveis, pareceCodigo };
|
|
551
|
+
relevante, quantoBate, palavrasDaConsulta, sitesProvaveis, pareceCodigo };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.9.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": {
|
package/studio/server.py
CHANGED
|
@@ -15,6 +15,7 @@ import mimetypes
|
|
|
15
15
|
import os
|
|
16
16
|
import re
|
|
17
17
|
import sys
|
|
18
|
+
import time
|
|
18
19
|
import unicodedata
|
|
19
20
|
import uuid
|
|
20
21
|
from datetime import datetime, timezone
|
|
@@ -461,6 +462,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
461
462
|
|
|
462
463
|
if p == "/api/voz":
|
|
463
464
|
return self.voz(urlparse(self.path).query)
|
|
465
|
+
if p == "/api/voz/status":
|
|
466
|
+
return self.voz_status()
|
|
464
467
|
|
|
465
468
|
if p == "/api/midia":
|
|
466
469
|
return self.js(listar_midia())
|
|
@@ -506,20 +509,128 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
506
509
|
if len(texto) > 4000:
|
|
507
510
|
texto = texto[:4000]
|
|
508
511
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
512
|
+
voz = (q.get("voz", [""])[0] or "").strip()
|
|
513
|
+
instrucao = (q.get("instrucao", [""])[0] or "").strip()
|
|
514
|
+
motor = (q.get("motor", [""])[0] or "").strip()
|
|
515
|
+
|
|
516
|
+
tipo = "audio/mpeg"
|
|
517
|
+
audio = b""
|
|
518
|
+
# 1) Voicebox, se o usuário tiver instalado: voz clonável, ilimitada e
|
|
519
|
+
# sem depender de nada na internet.
|
|
520
|
+
if motor != "google" and self._voicebox_vivo():
|
|
521
|
+
try:
|
|
522
|
+
audio, tipo = self._voz_voicebox(texto, voz, instrucao)
|
|
523
|
+
except Exception as e:
|
|
524
|
+
sys.stderr.write("[voz] Voicebox falhou (%s); indo de Google\n" % e)
|
|
525
|
+
audio = b""
|
|
526
|
+
# 2) Google: uma voz só, mas responde sempre.
|
|
527
|
+
if not audio:
|
|
528
|
+
try:
|
|
529
|
+
audio = b"".join(self._trecho_de_voz(t, idioma) for t in _picar(texto, 190))
|
|
530
|
+
except Exception as e: # rede, bloqueio, mudança do serviço
|
|
531
|
+
return self.js({"erro": "voz indisponível: %s" % e}, 502)
|
|
513
532
|
if not audio:
|
|
514
533
|
return self.js({"erro": "voz não devolveu áudio"}, 502)
|
|
515
534
|
|
|
516
535
|
self.send_response(200)
|
|
517
|
-
self.send_header("Content-Type",
|
|
536
|
+
self.send_header("Content-Type", tipo)
|
|
518
537
|
self.send_header("Content-Length", str(len(audio)))
|
|
519
538
|
self.send_header("Cache-Control", "no-store")
|
|
520
539
|
self.end_headers()
|
|
521
540
|
self.wfile.write(audio)
|
|
522
541
|
|
|
542
|
+
# ── Voicebox ───────────────────────────────────────────────────────
|
|
543
|
+
# https://voicebox.sh — app local, código aberto, de graça e sem limite,
|
|
544
|
+
# com clonagem de voz. É a melhor voz que existe para este uso e não
|
|
545
|
+
# depende de serviço nenhum na internet: roda na máquina do usuário.
|
|
546
|
+
#
|
|
547
|
+
# A ponte é FEITA AQUI, e não no navegador, por causa do CORS: o Voicebox
|
|
548
|
+
# só libera as portas dele (5173 e 17493), e o Studio serve em 7777 — do
|
|
549
|
+
# navegador o pedido morreria antes de sair.
|
|
550
|
+
#
|
|
551
|
+
# Não está instalado? A rota cai para o Google, e ninguém fica sem voz.
|
|
552
|
+
VOICEBOX = "http://127.0.0.1:17493"
|
|
553
|
+
|
|
554
|
+
def voz_status(self):
|
|
555
|
+
"""O que está disponível para narrar, para a interface não oferecer o
|
|
556
|
+
que não existe."""
|
|
557
|
+
vivo = self._voicebox_vivo()
|
|
558
|
+
vozes = []
|
|
559
|
+
if vivo:
|
|
560
|
+
try:
|
|
561
|
+
with urllib.request.urlopen(self.VOICEBOX + "/profiles", timeout=6) as r:
|
|
562
|
+
vozes = [{"id": v.get("id"), "nome": v.get("name"), "idioma": v.get("language")}
|
|
563
|
+
for v in json.loads(r.read().decode("utf-8"))]
|
|
564
|
+
except Exception:
|
|
565
|
+
vozes = []
|
|
566
|
+
return self.js({
|
|
567
|
+
"voicebox": {"ativo": vivo, "vozes": vozes, "site": "https://voicebox.sh"},
|
|
568
|
+
"google": {"ativo": True},
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
def _voicebox_vivo(self):
|
|
572
|
+
try:
|
|
573
|
+
with urllib.request.urlopen(self.VOICEBOX + "/health", timeout=2):
|
|
574
|
+
return True
|
|
575
|
+
except Exception:
|
|
576
|
+
return False
|
|
577
|
+
|
|
578
|
+
def _voicebox_perfil(self, pedido):
|
|
579
|
+
"""A voz a usar: a que o usuário pediu pelo nome, ou a primeira que
|
|
580
|
+
houver. Sem perfil não há geração — o Voicebox exige um."""
|
|
581
|
+
req = urllib.request.Request(self.VOICEBOX + "/profiles")
|
|
582
|
+
with urllib.request.urlopen(req, timeout=8) as r:
|
|
583
|
+
perfis = json.loads(r.read().decode("utf-8"))
|
|
584
|
+
if not perfis:
|
|
585
|
+
raise RuntimeError("Voicebox está no ar mas sem nenhuma voz criada")
|
|
586
|
+
if pedido:
|
|
587
|
+
alvo = pedido.strip().lower()
|
|
588
|
+
for p in perfis:
|
|
589
|
+
if alvo in (p.get("name") or "").lower() or alvo == p.get("id"):
|
|
590
|
+
return p
|
|
591
|
+
# Sem escolha explícita, prefere uma voz em português.
|
|
592
|
+
for p in perfis:
|
|
593
|
+
if (p.get("language") or "").startswith("pt"):
|
|
594
|
+
return p
|
|
595
|
+
return perfis[0]
|
|
596
|
+
|
|
597
|
+
def _voz_voicebox(self, texto, voz, instrucao):
|
|
598
|
+
perfil = self._voicebox_perfil(voz)
|
|
599
|
+
corpo = json.dumps({
|
|
600
|
+
"profile_id": perfil["id"],
|
|
601
|
+
"text": texto[:50000],
|
|
602
|
+
"language": "pt",
|
|
603
|
+
# A instrução de entrega é o que dá voz de cinema em vez de voz de
|
|
604
|
+
# leitura. O Voicebox aceita em linguagem natural, até 500 chars.
|
|
605
|
+
"instruct": (instrucao or "Narração institucional: voz grave, ritmo pausado, tom seguro.")[:500],
|
|
606
|
+
"max_chunk_chars": 800,
|
|
607
|
+
"crossfade_ms": 50,
|
|
608
|
+
"normalize": True,
|
|
609
|
+
}).encode("utf-8")
|
|
610
|
+
req = urllib.request.Request(self.VOICEBOX + "/generate", data=corpo,
|
|
611
|
+
headers={"Content-Type": "application/json"})
|
|
612
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
613
|
+
g = json.loads(r.read().decode("utf-8"))
|
|
614
|
+
gid = g.get("id")
|
|
615
|
+
if not gid:
|
|
616
|
+
raise RuntimeError("Voicebox não devolveu id de geração")
|
|
617
|
+
|
|
618
|
+
# /generate volta na hora com status "generating" e sintetiza em
|
|
619
|
+
# segundo plano — então é preciso esperar de verdade.
|
|
620
|
+
prazo = time.time() + 180
|
|
621
|
+
estado = g.get("status")
|
|
622
|
+
while estado in (None, "generating", "pending", "processing"):
|
|
623
|
+
if time.time() > prazo:
|
|
624
|
+
raise RuntimeError("Voicebox demorou demais")
|
|
625
|
+
time.sleep(1.0)
|
|
626
|
+
with urllib.request.urlopen(self.VOICEBOX + "/generate/%s/status" % gid, timeout=8) as r:
|
|
627
|
+
estado = (json.loads(r.read().decode("utf-8")) or {}).get("status")
|
|
628
|
+
if estado != "completed":
|
|
629
|
+
raise RuntimeError("Voicebox falhou (%s)" % estado)
|
|
630
|
+
|
|
631
|
+
with urllib.request.urlopen(self.VOICEBOX + "/audio/%s" % gid, timeout=30) as r:
|
|
632
|
+
return r.read(), (r.headers.get("Content-Type") or "audio/wav")
|
|
633
|
+
|
|
523
634
|
def _trecho_de_voz(self, texto, idioma):
|
|
524
635
|
url = "https://translate.google.com/translate_tts?" + urlencode({
|
|
525
636
|
"ie": "UTF-8", "q": texto, "tl": idioma, "client": "tw-ob", "total": 1, "idx": 0,
|