mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 09:34:46 +08:00
feat(ia): Execuções diz se o Jev decidiu ou só observou
A linha do Jev dizia "Medido pelo Jev. Se ele está em observação, quem decide é a IA de sempre." — não dizia o que aconteceu naquela mensagem. O worker passa a gravar origem_da_escolha "jev" quando a nota dele decidiu e "jev_observacao" quando decidiu a IA de sempre, e Execuções diz o fato: "O Jev decidiu." / "O Jev observou; quem decidiu foi a IA de sempre." A linha da medição sai depois da decisão: antes dela o worker não sabia — em observação com a IA de sempre caída, é a nota do Jev que decide. Sem CHECK na coluna (texto livre desde a 0128): nenhuma migration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015PWGNXXzCQURHMALcEMmkC
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e4c193bdf6
commit
fcf0456853
@@ -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();
|
||||
|
||||
@@ -144,7 +144,7 @@ export async function GET(req: NextRequest): Promise<Response> {
|
||||
? (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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,7 +34,7 @@ export const O_QUE_FAZER_DO_JEV: Readonly<Record<`jev_${MotivoComRede}`, string>
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
|
||||
@@ -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<OrigemDaEscolha, string> = {
|
||||
"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.",
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -265,11 +265,21 @@ export async function processSentiment(event: EventRow): Promise<SentimentResult
|
||||
: null;
|
||||
|
||||
if (clima?.ok) {
|
||||
await fecharAvisoDoJev(admin, event.organization_id);
|
||||
}
|
||||
/**
|
||||
* A linha da medição do Jev sai DEPOIS da decisão, com a origem do que
|
||||
* aconteceu: "jev" quando a nota dele decidiu, "jev_observacao" quando a IA
|
||||
* de sempre decidiu. Antes da decisão a linha não sabia — em observação com a
|
||||
* IA de sempre caída, é a nota do Jev que decide.
|
||||
*/
|
||||
const registrarMedicaoDoJev = (decidiu: boolean): void => {
|
||||
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<SentimentResult
|
||||
}),
|
||||
finish_reason: null,
|
||||
});
|
||||
await fecharAvisoDoJev(admin, event.organization_id);
|
||||
}
|
||||
};
|
||||
|
||||
const jevFalhouNaRede = clima !== null && !clima.ok && clima.tentouRede ? clima : null;
|
||||
/** `reservaMediu` é o que o corpo do aviso afirma — por isso só se sabe no fim. */
|
||||
@@ -413,6 +422,8 @@ export async function processSentiment(event: EventRow): Promise<SentimentResult
|
||||
}
|
||||
}
|
||||
|
||||
registrarMedicaoDoJev(decisao.engine === "jev");
|
||||
|
||||
// ── Merge sentiment into messages.metadata ────────────────────────────
|
||||
const existingMetadata = (message.metadata as Record<string, unknown> | null) ?? {};
|
||||
const updatedMetadata = {
|
||||
|
||||
Reference in New Issue
Block a user