diff --git a/app/api/v1/ai/runs/route.test.ts b/app/api/v1/ai/runs/route.test.ts index 272c83cb7..7ed9ce500 100644 --- a/app/api/v1/ai/runs/route.test.ts +++ b/app/api/v1/ai/runs/route.test.ts @@ -134,10 +134,22 @@ describe("GET /api/v1/ai/runs", () => { expect(falha.consequencia).toBe(PONTO_POR_ID.get("sentiment_classify")?.sintomaDeFalha); expect(falha.oQueFazer).toBe(O_QUE_FAZER_DO_JEV.jev_credencial_invalida); - // "Medido pelo Jev" seria falso na única linha em que ninguém mediu. + // "O Jev decidiu" seria falso na única linha em que ninguém mediu. expect(falha.porQueEsteModelo).toBe(JEV_FALHOU_SEM_RESERVA); }); + it("a linha do Jev diz o fato: decidiu, ou só observou", async () => { + linhas = [ + linha({ provider: "typesafe", model: "typesafe/jev-1.13.0", origem_da_escolha: "jev" }), + linha({ provider: "typesafe", model: "typesafe/jev-1.13.0", origem_da_escolha: "jev_observacao" }), + ]; + const { corpo } = await pedir("?provider=typesafe"); + expect(corpo.data.execucoes.map((e: { porQueEsteModelo: string }) => e.porQueEsteModelo)).toEqual([ + "O Jev decidiu.", + "O Jev observou; quem decidiu foi a IA de sempre.", + ]); + }); + it("observação com a IA de sempre caída: a falha dela não afirma consequência que não houve", async () => { linhas = [linha({ status: "erro", error_code: "provedor_indisponivel", origem_da_escolha: "jev_cobriu" })]; const { corpo } = await pedir(); diff --git a/app/api/v1/ai/runs/route.ts b/app/api/v1/ai/runs/route.ts index fe2057523..c33e2d2bc 100644 --- a/app/api/v1/ai/runs/route.ts +++ b/app/api/v1/ai/runs/route.ts @@ -144,7 +144,7 @@ export async function GET(req: NextRequest): Promise { ? (ponto?.sintomaDeFalha ?? null) : null, oQueFazer: l.status === "erro" ? (O_QUE_FAZER[l.error_code ?? ""] ?? null) : null, - // A linha de falha do Jev só existe quando ninguém mediu: "Medido pelo Jev" + // A linha de falha do Jev só existe quando ninguém mediu: "O Jev decidiu" // seria falso justamente nela. porQueEsteModelo: l.origem_da_escolha === "jev" && l.status === "erro" diff --git a/lib/ai/decisao/textos.test.ts b/lib/ai/decisao/textos.test.ts index 572b7f780..dde1d5d2b 100644 --- a/lib/ai/decisao/textos.test.ts +++ b/lib/ai/decisao/textos.test.ts @@ -22,6 +22,7 @@ const TEXTOS = [ ...Object.values(O_QUE_FAZER_DO_JEV), ...Object.values(AVISO_DO_JEV), EXPLICACAO_DA_ORIGEM.jev, + EXPLICACAO_DA_ORIGEM.jev_observacao, EXPLICACAO_DA_ORIGEM.reserva_do_jev, EXPLICACAO_DA_ORIGEM.jev_cobriu, JEV_FALHOU_SEM_RESERVA, diff --git a/lib/ai/decisao/textos.ts b/lib/ai/decisao/textos.ts index b455efe7c..f22203e20 100644 --- a/lib/ai/decisao/textos.ts +++ b/lib/ai/decisao/textos.ts @@ -34,7 +34,7 @@ export const O_QUE_FAZER_DO_JEV: Readonly /** * O "por que este modelo" da linha de FALHA do Jev em Execuções — a única que - * ele grava, quando ninguém mediu. A frase da origem `jev` ("Medido pelo Jev") + * ele grava, quando ninguém mediu. A frase da origem `jev` ("O Jev decidiu.") * seria falsa justamente nela. */ export const JEV_FALHOU_SEM_RESERVA = diff --git a/lib/ai/pontos/resolver.ts b/lib/ai/pontos/resolver.ts index 5202648ec..54a267d8d 100644 --- a/lib/ai/pontos/resolver.ts +++ b/lib/ai/pontos/resolver.ts @@ -45,8 +45,10 @@ export type OrigemDaEscolha = | "variavel_de_ambiente" | "herdado_de_quem_chamou" | "padrao_da_organizacao" - /** O Jev mediu (decidindo, ou observando ao lado da IA de sempre). */ + /** O Jev mediu e a nota dele decidiu. Também a linha de falha do Jev (ver Execuções). */ | "jev" + /** Observação: o Jev mediu ao lado da IA de sempre, e quem decidiu foi ela. */ + | "jev_observacao" /** O Jev estava ligado e não respondeu: a IA de sempre mediu no lugar dele. */ | "reserva_do_jev" /** Observação: a IA de sempre falhou, e a nota do Jev, já medida, decidiu. */ @@ -60,9 +62,10 @@ export const EXPLICACAO_DA_ORIGEM: Record = { "Herdado de quem disparou a chamada — o agente publicado, ou o roteador de intenção.", padrao_da_organizacao: "Usando o padrão da organização.", fixo_do_produto: "O produto resolve este ponto sozinho — não há modelo a escolher.", - // Uma frase para os dois modos: em observação o Jev mede e NÃO decide, e - // "o Jev decidiu" seria falso justamente no modo de comparar os dois. - jev: "Medido pelo Jev. Se ele está em observação, quem decide é a IA de sempre.", + // Duas origens, uma por desfecho: a frase única ("se ele está em observação, + // quem decide é…") não dizia o que aconteceu NAQUELA mensagem. + jev: "O Jev decidiu.", + jev_observacao: "O Jev observou; quem decidiu foi a IA de sempre.", reserva_do_jev: "O Jev não respondeu; a IA de sempre mediu no lugar dele.", jev_cobriu: "A IA de sempre falhou, mas o Jev já tinha medido esta mensagem: nada se perdeu.", }; diff --git a/lib/i18n/dicionario.ts b/lib/i18n/dicionario.ts index e200483d4..1c6b732e0 100644 --- a/lib/i18n/dicionario.ts +++ b/lib/i18n/dicionario.ts @@ -3283,9 +3283,8 @@ export const DICIONARIO: Traducoes = { es: "El producto resuelve este punto por sí solo; no hay modelo que elegir.", }, // O Jev em IA › Execuções e na Central (`lib/ai/decisao/textos.ts`). - "Medido pelo Jev. Se ele está em observação, quem decide é a IA de sempre.": { - es: "Medido por Jev. Si está en observación, quien decide es la IA de siempre.", - }, + "O Jev decidiu.": { es: "Jev decidió." }, + "O Jev observou; quem decidiu foi a IA de sempre.": { es: "Jev observó; quien decidió fue la IA de siempre." }, "O Jev não respondeu; a IA de sempre mediu no lugar dele.": { es: "Jev no respondió; la IA de siempre midió en su lugar.", }, diff --git a/tests/unit/clima-da-conversa-no-worker.test.ts b/tests/unit/clima-da-conversa-no-worker.test.ts index c88c9985d..0d8fa8e5a 100644 --- a/tests/unit/clima-da-conversa-no-worker.test.ts +++ b/tests/unit/clima-da-conversa-no-worker.test.ts @@ -462,7 +462,8 @@ describe("o Jev no worker de clima", () => { sentiment_jev_model: "jev-1.13.0", }); expect(linhasDoJev(banco)).toHaveLength(1); - expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: "jev" }); + // Execuções diz o que aconteceu NESTA mensagem: o Jev observou, não decidiu. + expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: "jev_observacao" }); expect(linhasDaIaDeSempre(banco)).toHaveLength(1); expect(linhasDaIaDeSempre(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: null }); expect(alertas(rpcs)[0]!["p_payload"]).toMatchObject({ sentiment_engine: "llm" }); @@ -539,7 +540,8 @@ describe("o Jev no worker de clima", () => { expect(resultado).toEqual({ skipped: false, sentiment_score: 0 }); expect(generateObject).toHaveBeenCalledTimes(1); - expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok" }); + // Em observação, mas foi a nota dele que decidiu: a linha diz "jev", não "jev_observacao". + expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: "jev" }); expect(linhasDaIaDeSempre(banco)[0], "a falha da IA de sempre segue visível em Execuções").toMatchObject({ status: "erro", // Sem consequência na tela: o Jev já tinha medido. diff --git a/workers/ai-sentiment-worker.ts b/workers/ai-sentiment-worker.ts index 37608f1ce..ae3d391dd 100644 --- a/workers/ai-sentiment-worker.ts +++ b/workers/ai-sentiment-worker.ts @@ -265,11 +265,21 @@ export async function processSentiment(event: EventRow): Promise { + if (!clima?.ok) return; logInvocation({ ...comum, provider: "typesafe", model: `typesafe/${clima.modelo}`, - origem_da_escolha: "jev", + origem_da_escolha: decidiu ? "jev" : "jev_observacao", prompt_tokens: clima.tokensDeEntrada, completion_tokens: clima.tokensDeSaida, latency_ms: clima.latenciaMs, @@ -284,8 +294,7 @@ export async function processSentiment(event: EventRow): Promise | null) ?? {}; const updatedMetadata = {