diff --git a/jus-ia-start-kit/.env.example b/jus-ia-start-kit/.env.example index d16c37dd2..928181c61 100644 --- a/jus-ia-start-kit/.env.example +++ b/jus-ia-start-kit/.env.example @@ -1,7 +1,11 @@ -# LLM Provider (openai or anthropic) -LLM_PROVIDER=openai -LLM_API_KEY=sk-your-api-key -LLM_MODEL=gpt-4o-mini +# LLM Provider (openrouter, openai, or anthropic) +LLM_PROVIDER=openrouter +LLM_API_KEY=sk-or-v1-your-openrouter-key +LLM_MODEL=google/gemini-2.5-flash +LLM_BASE_URL=https://openrouter.ai/api/v1 + +# Alternative: use OPENROUTER_API_KEY directly +# OPENROUTER_API_KEY=sk-or-v1-your-openrouter-key # Server PORT=3000 diff --git a/jus-ia-start-kit/package.json b/jus-ia-start-kit/package.json index 88210afed..4a76a4959 100644 --- a/jus-ia-start-kit/package.json +++ b/jus-ia-start-kit/package.json @@ -5,12 +5,12 @@ "type": "module", "scripts": { "dev": "concurrently \"npm run dev:server\" \"npm run dev:css\"", - "dev:server": "tsx watch src/server.ts", + "dev:server": "tsx watch --env-file=.env src/server.ts", "dev:css": "npx @tailwindcss/cli -i src/styles/input.css -o src/public/css/app.css --watch", "build": "npm run build:css && npm run build:server", "build:server": "tsc", "build:css": "npx @tailwindcss/cli -i src/styles/input.css -o src/public/css/app.css --minify", - "start": "node dist/server.js", + "start": "node --env-file=.env dist/server.js", "test": "vitest run", "test:watch": "vitest" }, diff --git a/jus-ia-start-kit/src/__tests__/routes.test.ts b/jus-ia-start-kit/src/__tests__/routes.test.ts new file mode 100644 index 000000000..c3e1d0b3c --- /dev/null +++ b/jus-ia-start-kit/src/__tests__/routes.test.ts @@ -0,0 +1,233 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import Fastify from "fastify"; +import fastifyView from "@fastify/view"; +import fastifyFormbody from "@fastify/formbody"; +import nunjucks from "nunjucks"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { homeRoutes } from "../routes/home.js"; +import { flowRoutes } from "../routes/flow.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const templatesDir = join(__dirname, "..", "templates"); + +async function buildApp() { + const app = Fastify({ logger: false }); + + await app.register(fastifyFormbody); + await app.register(fastifyView, { + engine: { nunjucks }, + templates: templatesDir, + }); + await app.register(homeRoutes); + await app.register(flowRoutes); + + return app; +} + +describe("Routes Integration", () => { + let app: Awaited>; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("GET /", () => { + it("returns 200", async () => { + const response = await app.inject({ method: "GET", url: "/" }); + expect(response.statusCode).toBe(200); + }); + + it("contains page content", async () => { + const response = await app.inject({ method: "GET", url: "/" }); + expect(response.body).toContain("Jus IA"); + }); + }); + + describe("GET /health", () => { + it("returns ok status", async () => { + const response = await app.inject({ method: "GET", url: "/health" }); + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ status: "ok" }); + }); + }); + + describe("POST /selecionar", () => { + it("returns 200 for trabalhista", async () => { + const response = await app.inject({ + method: "POST", + url: "/selecionar", + payload: { tipo_tarefa: "peticao-inicial", area: "trabalhista" }, + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("Horas Extras"); + }); + + it("returns 200 for civel", async () => { + const response = await app.inject({ + method: "POST", + url: "/selecionar", + payload: { tipo_tarefa: "peticao-inicial", area: "civel" }, + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("Cobrança"); + }); + + it("returns not-available for unknown area", async () => { + const response = await app.inject({ + method: "POST", + url: "/selecionar", + payload: { tipo_tarefa: "peticao-inicial", area: "penal" }, + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("disponível"); + }); + }); + + describe("GET /:area/:subtipo (deep links)", () => { + it("returns 200 for trabalhista/horas-extras", async () => { + const response = await app.inject({ + method: "GET", + url: "/trabalhista/horas-extras", + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("Dados do Caso"); + }); + + it("returns 200 for civel/cobranca", async () => { + const response = await app.inject({ + method: "GET", + url: "/civel/cobranca", + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("Dados da Dívida"); + }); + + it("returns not-available for unknown flow", async () => { + const response = await app.inject({ + method: "GET", + url: "/trabalhista/nao-existe", + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("disponível"); + }); + }); + + describe("POST /:area/iniciar", () => { + it("starts horas-extras flow", async () => { + const response = await app.inject({ + method: "POST", + url: "/trabalhista/iniciar", + payload: { subtipo: "horas-extras", _tipo_tarefa: "peticao-inicial" }, + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("Dados do Caso"); + }); + + it("starts cobranca flow", async () => { + const response = await app.inject({ + method: "POST", + url: "/civel/iniciar", + payload: { subtipo: "cobranca", _tipo_tarefa: "peticao-inicial" }, + }); + expect(response.statusCode).toBe(200); + }); + + it("returns not-available for unknown subtipo", async () => { + const response = await app.inject({ + method: "POST", + url: "/trabalhista/iniciar", + payload: { subtipo: "nao-existe" }, + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("disponível"); + }); + }); + + describe("POST /:area/:subtipo/step/:stepNumber (step submission)", () => { + it("advances from step 1 to step 2 in horas-extras", async () => { + const response = await app.inject({ + method: "POST", + url: "/trabalhista/horas-extras/step/1", + payload: { + _area: "trabalhista", + _subtipo: "horas-extras", + _tipo_tarefa: "peticao-inicial", + _step: "1", + _total_steps: "5", + _responses: "{}", + empregador_tipo: "Pessoa Jurídica (empresa)", + regime: "CLT", + jornada_contratual: "44h semanais", + data_inicio: "2020-01-01", + data_fim: "2024-01-01", + ainda_empregado: "Não", + horas_extras_semana: "5 a 10 horas", + banco_horas: "Não", + }, + }); + expect(response.statusCode).toBe(200); + expect(response.body).toContain("Detalhes do Caso"); + }); + + it("shows preview after final step in horas-extras", async () => { + const responses = JSON.stringify({ + empregador_tipo: "Pessoa Jurídica (empresa)", + regime: "CLT", + jornada_contratual: "44h semanais", + data_inicio: "2020-01-01", + data_fim: "2024-01-01", + ainda_empregado: "Não", + horas_extras_semana: "5 a 10 horas", + banco_horas: "Não", + }); + + const response = await app.inject({ + method: "POST", + url: "/trabalhista/horas-extras/step/2", + payload: { + _area: "trabalhista", + _subtipo: "horas-extras", + _tipo_tarefa: "peticao-inicial", + _step: "2", + _total_steps: "5", + _responses: responses, + registro_ponto: "Sim, eletrônico", + testemunhas: "Sim", + pagamento_parcial: "Não, nenhum pagamento", + }, + }); + expect(response.statusCode).toBe(200); + // Should contain the assembled prompt with CLT reference + expect(response.body).toContain("CLT"); + }); + }); + + describe("All registered flows have valid deep links", () => { + const flows = [ + "/trabalhista/horas-extras", + "/trabalhista/rescisao-indireta", + "/trabalhista/dano-moral", + "/trabalhista/acumulo-funcao", + "/trabalhista/contestacao", + "/civel/cobranca", + "/civel/indenizacao", + "/civel/obrigacao-fazer", + "/civel/contestacao", + "/civel/contrato", + ]; + + for (const flowUrl of flows) { + it(`GET ${flowUrl} returns 200`, async () => { + const response = await app.inject({ method: "GET", url: flowUrl }); + expect(response.statusCode).toBe(200); + }); + } + }); +}); diff --git a/jus-ia-start-kit/src/config/flows.ts b/jus-ia-start-kit/src/config/flows.ts index 688c91d18..cd26b1733 100644 --- a/jus-ia-start-kit/src/config/flows.ts +++ b/jus-ia-start-kit/src/config/flows.ts @@ -1,5 +1,16 @@ import type { FlowConfig } from "../flows/types.js"; +// Trabalhista flows import { horasExtrasFlow } from "../flows/trabalhista/horas-extras.js"; +import { rescisaoIndiretaFlow } from "../flows/trabalhista/rescisao-indireta.js"; +import { danoMoralFlow } from "../flows/trabalhista/dano-moral.js"; +import { acumuloFuncaoFlow } from "../flows/trabalhista/acumulo-funcao.js"; +import { contestacaoTrabalhistaFlow } from "../flows/trabalhista/contestacao.js"; +// Cível flows +import { cobrancaFlow } from "../flows/civel/cobranca.js"; +import { indenizacaoFlow } from "../flows/civel/indenizacao.js"; +import { obrigacaoFazerFlow } from "../flows/civel/obrigacao-fazer.js"; +import { contestacaoCivelFlow } from "../flows/civel/contestacao.js"; +import { contratoRevisaoFlow } from "../flows/civel/contrato.js"; /** Registry of all available flows */ const flowRegistry: Map = new Map(); @@ -9,9 +20,19 @@ function registerFlow(flow: FlowConfig): void { flowRegistry.set(key, flow); } -// Register all flows +// Register all trabalhista flows registerFlow(horasExtrasFlow); -// TODO: Register remaining 9 flows as they are built +registerFlow(rescisaoIndiretaFlow); +registerFlow(danoMoralFlow); +registerFlow(acumuloFuncaoFlow); +registerFlow(contestacaoTrabalhistaFlow); + +// Register all cível flows +registerFlow(cobrancaFlow); +registerFlow(indenizacaoFlow); +registerFlow(obrigacaoFazerFlow); +registerFlow(contestacaoCivelFlow); +registerFlow(contratoRevisaoFlow); /** Get a flow by area/subtipo */ export function getFlow(area: string, subtipo: string): FlowConfig | undefined { diff --git a/jus-ia-start-kit/src/config/index.ts b/jus-ia-start-kit/src/config/index.ts index f0b65fa2d..03ccb0251 100644 --- a/jus-ia-start-kit/src/config/index.ts +++ b/jus-ia-start-kit/src/config/index.ts @@ -3,9 +3,13 @@ export const config = { host: process.env.HOST || "0.0.0.0", nodeEnv: process.env.NODE_ENV || "development", llm: { - provider: process.env.LLM_PROVIDER || "openai", - apiKey: process.env.LLM_API_KEY || "", - model: process.env.LLM_MODEL || "gpt-4o-mini", + provider: (process.env.LLM_PROVIDER || "openrouter") as + | "openrouter" + | "openai" + | "anthropic", + apiKey: process.env.LLM_API_KEY || process.env.OPENROUTER_API_KEY || "", + model: process.env.LLM_MODEL || "google/gemini-2.5-flash", + baseUrl: process.env.LLM_BASE_URL || "https://openrouter.ai/api/v1", }, analyticsId: process.env.ANALYTICS_ID || "", sentryDsn: process.env.SENTRY_DSN || "", diff --git a/jus-ia-start-kit/src/flows/civel/cobranca.ts b/jus-ia-start-kit/src/flows/civel/cobranca.ts new file mode 100644 index 000000000..887bafd6c --- /dev/null +++ b/jus-ia-start-kit/src/flows/civel/cobranca.ts @@ -0,0 +1,179 @@ +import type { FlowConfig } from "../types.js"; + +export const cobrancaFlow: FlowConfig = { + area: "civel", + areaLabel: "Cível", + subtipo: "cobranca", + subtipoLabel: "Cobrança", + tipoTarefa: "peticao-inicial", + steps: [ + { + stepNumber: 1, + title: "Dados da Dívida", + requiresLlm: false, + groups: [ + { + title: "Credor e Devedor", + questions: [ + { + id: "relacao_partes", + text: "Qual a relação entre as partes?", + type: "select", + options: [ + "Contrato de prestação de serviço", + "Empréstimo pessoal", + "Venda de produto", + "Aluguel", + "Cheque devolvido", + "Nota promissória", + "Outro", + ], + required: true, + }, + { + id: "tipo_devedor", + text: "O devedor é pessoa física ou jurídica?", + type: "select", + options: ["PF", "PJ"], + required: true, + }, + ], + }, + { + title: "Valor e Prazo", + questions: [ + { + id: "faixa_valor", + text: "Qual a faixa de valor da dívida?", + type: "select", + options: [ + "Até R$ 5 mil", + "R$ 5 a 20 mil", + "R$ 20 a 50 mil", + "Acima de R$ 50 mil", + ], + required: true, + }, + { + id: "data_vencimento", + text: "Data de vencimento da dívida", + type: "date", + required: true, + }, + { + id: "tentou_cobranca", + text: "Já tentou cobrar antes?", + type: "select", + options: [ + "Sim, extrajudicialmente", + "Sim, com protesto", + "Não", + ], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Documentação", + requiresLlm: true, + groups: [ + { + title: "Provas da Dívida", + questions: [ + { + id: "documentos", + text: "Quais documentos possui?", + type: "multiselect", + options: [ + "Contrato assinado", + "Nota promissória", + "Cheque", + "Notas fiscais", + "Comprovantes de transferência", + "Mensagens", + "E-mails", + "Nenhum documento formal", + ], + required: true, + }, + { + id: "divida_reconhecida", + text: "A dívida é reconhecida pelo devedor?", + type: "select", + options: [ + "Sim, devedor reconhece", + "Parcialmente", + "Não, devedor contesta", + ], + required: true, + }, + ], + }, + { + title: "Situação Atual", + questions: [ + { + id: "parcelas_pagas", + text: "Houve pagamento parcial?", + type: "select", + options: [ + "Nenhuma", + "Algumas parcelas", + "Maioria paga, faltam poucas", + ], + required: true, + }, + { + id: "possui_garantia", + text: "Há garantia vinculada à dívida?", + type: "select", + options: [ + "Sim, com garantia real", + "Sim, com fiador", + "Não", + ], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma petição inicial de ação de cobrança com os seguintes dados: + +**Partes:** +- Credor: [a ser preenchido pelo advogado] +- Devedor: {{tipo_devedor}} + +**Relação entre as partes:** +- Origem: {{relacao_partes}} +- Faixa de valor: {{faixa_valor}} +- Data de vencimento: {{data_vencimento}} +- Cobrança prévia: {{tentou_cobranca}} + +**Documentação e provas:** +- Documentos disponíveis: {{documentos}} +- Reconhecimento da dívida: {{divida_reconhecida}} +- Parcelas pagas: {{parcelas_pagas}} +- Garantia: {{possui_garantia}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 318 do CPC (procedimento comum) +- Art. 319 do CPC (requisitos da petição inicial) +- Art. 784 do CPC (títulos executivos extrajudiciais) +- Art. 397 do CC (mora) + +Inclua pedidos de: pagamento do valor principal, juros de mora, correção monetária, custas processuais e honorários advocatícios.`, + + legalReferences: [ + "art. 318 CPC", + "art. 319 CPC", + "art. 784 CPC", + "art. 397 CC", + ], +}; diff --git a/jus-ia-start-kit/src/flows/civel/contestacao.ts b/jus-ia-start-kit/src/flows/civel/contestacao.ts new file mode 100644 index 000000000..634e3e791 --- /dev/null +++ b/jus-ia-start-kit/src/flows/civel/contestacao.ts @@ -0,0 +1,182 @@ +import type { FlowConfig } from "../types.js"; + +export const contestacaoCivelFlow: FlowConfig = { + area: "civel", + areaLabel: "Cível", + subtipo: "contestacao", + subtipoLabel: "Contestação Cível", + tipoTarefa: "contestacao", + steps: [ + { + stepNumber: 1, + title: "Dados do Processo", + requiresLlm: false, + groups: [ + { + title: "Processo", + questions: [ + { + id: "numero_processo", + text: "Número do processo", + type: "text", + required: true, + }, + { + id: "vara_tribunal", + text: "Vara e tribunal", + type: "text", + required: true, + }, + { + id: "tipo_acao_autor", + text: "Qual o tipo de ação movida pelo autor?", + type: "select", + options: [ + "Cobrança", + "Indenização", + "Obrigação de fazer", + "Revisional", + "Consignação", + "Outra", + ], + required: true, + }, + ], + }, + { + title: "Partes", + questions: [ + { + id: "posicao_cliente", + text: "Posição do cliente no processo", + type: "select", + options: [ + "Réu pessoa física", + "Réu pessoa jurídica", + ], + required: true, + }, + { + id: "valor_causa", + text: "Valor da causa", + type: "select", + options: [ + "Até R$ 20 mil", + "R$ 20 a 50 mil", + "R$ 50 a 100 mil", + "Acima de R$ 100 mil", + ], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Estratégia de Defesa", + requiresLlm: true, + groups: [ + { + title: "Preliminares", + questions: [ + { + id: "preliminares", + text: "Quais preliminares deseja alegar?", + type: "multiselect", + options: [ + "Inépcia da inicial", + "Ilegitimidade passiva", + "Falta de interesse de agir", + "Incompetência", + "Litispendência", + "Coisa julgada", + "Prescrição", + "Decadência", + "Nenhuma", + ], + required: true, + }, + { + id: "merito_defesa", + text: "Qual a tese principal de defesa no mérito?", + type: "select", + options: [ + "Fato não ocorreu", + "Fato ocorreu diferente", + "Inexistência de dano", + "Culpa exclusiva do autor", + "Caso fortuito ou força maior", + "Pagamento já realizado", + ], + required: true, + }, + ], + }, + { + title: "Provas", + questions: [ + { + id: "documentos_defesa", + text: "Quais documentos possui para defesa?", + type: "multiselect", + options: [ + "Contrato", + "Comprovantes de pagamento", + "E-mails", + "Protocolo", + "Fotos", + "Testemunhas", + "Perícia", + "Nenhum", + ], + required: true, + }, + { + id: "pedido_reconvencao", + text: "Deseja fazer pedido reconvencional?", + type: "select", + options: ["Sim", "Não"], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma contestação cível com os seguintes dados: + +**Processo:** +- Número: {{numero_processo}} +- Vara/Tribunal: {{vara_tribunal}} +- Tipo de ação: {{tipo_acao_autor}} +- Valor da causa: {{valor_causa}} + +**Partes:** +- Contestante: {{posicao_cliente}} + +**Estratégia de defesa:** +- Preliminares: {{preliminares}} +- Tese de mérito: {{merito_defesa}} + +**Provas e pedidos:** +- Documentos: {{documentos_defesa}} +- Reconvenção: {{pedido_reconvencao}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 335 do CPC (prazo para contestação) +- Art. 336 do CPC (ônus de impugnar especificamente) +- Art. 337 do CPC (preliminares de contestação) +- Art. 343 do CPC (reconvenção) + +Inclua: preliminares aplicáveis, impugnação específica dos fatos, defesa de mérito, requerimento de provas e pedidos finais.`, + + legalReferences: [ + "art. 335 CPC", + "art. 336 CPC", + "art. 337 CPC", + "art. 343 CPC", + ], +}; diff --git a/jus-ia-start-kit/src/flows/civel/contrato.ts b/jus-ia-start-kit/src/flows/civel/contrato.ts new file mode 100644 index 000000000..845663ce7 --- /dev/null +++ b/jus-ia-start-kit/src/flows/civel/contrato.ts @@ -0,0 +1,196 @@ +import type { FlowConfig } from "../types.js"; + +export const contratoRevisaoFlow: FlowConfig = { + area: "civel", + areaLabel: "Cível", + subtipo: "contrato", + subtipoLabel: "Contrato (Revisão)", + tipoTarefa: "contrato", + steps: [ + { + stepNumber: 1, + title: "Dados do Contrato", + requiresLlm: false, + groups: [ + { + title: "Tipo de Contrato", + questions: [ + { + id: "tipo_contrato", + text: "Qual o tipo de contrato?", + type: "select", + options: [ + "Aluguel", + "Prestação de serviço", + "Financiamento", + "Empréstimo", + "Seguro", + "Outro", + ], + required: true, + }, + { + id: "partes_contrato", + text: "Quais as partes do contrato?", + type: "select", + options: ["PF x PF", "PF x PJ", "PJ x PJ"], + required: true, + }, + { + id: "data_contrato", + text: "Data de assinatura do contrato", + type: "date", + required: true, + }, + { + id: "vigencia", + text: "Situação do contrato", + type: "select", + options: ["Em vigor", "Encerrado", "Rescindido"], + required: true, + }, + ], + }, + { + title: "Problema", + questions: [ + { + id: "problema_principal", + text: "Qual o problema principal do contrato?", + type: "select", + options: [ + "Cláusula abusiva", + "Reajuste abusivo", + "Descumprimento", + "Vício de consentimento", + "Onerosidade excessiva", + "Outro", + ], + required: true, + }, + { + id: "tentou_negociar", + text: "Tentou negociar com a outra parte?", + type: "select", + options: ["Sim, sem sucesso", "Não"], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Detalhes da Revisão", + requiresLlm: true, + groups: [ + { + title: "Cláusulas", + questions: [ + { + id: "clausulas_contestadas", + text: "Cláusulas que deseja revisar", + type: "text", + placeholder: "Quais cláusulas deseja revisar?", + required: true, + }, + { + id: "valor_atual", + text: "Valor atual do contrato", + type: "text", + placeholder: "Valor mensal ou total atual", + required: true, + }, + { + id: "valor_pretendido", + text: "Valor pretendido", + type: "text", + placeholder: "Valor que considera justo", + required: true, + }, + { + id: "fundamentacao", + text: "Fundamentação legal principal", + type: "select", + options: [ + "CDC - relação de consumo", + "CC - onerosidade excessiva", + "CC - lesão", + "Lei do Inquilinato", + "Outro", + ], + required: true, + }, + ], + }, + { + title: "Documentação", + questions: [ + { + id: "documentos", + text: "Quais documentos possui?", + type: "multiselect", + options: [ + "Contrato original", + "Aditivos", + "Comprovantes de pagamento", + "Notificações", + "Extratos", + "Nenhum", + ], + required: true, + }, + { + id: "pedido_tutela", + text: "Deseja pedir tutela de urgência?", + type: "select", + options: [ + "Sim, com tutela de urgência", + "Não", + ], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma petição inicial de ação revisional de contrato com os seguintes dados: + +**Contrato:** +- Tipo: {{tipo_contrato}} +- Partes: {{partes_contrato}} +- Data: {{data_contrato}} +- Vigência: {{vigencia}} + +**Problema:** +- Problema principal: {{problema_principal}} +- Tentativa de negociação: {{tentou_negociar}} + +**Revisão pretendida:** +- Cláusulas contestadas: {{clausulas_contestadas}} +- Valor atual: {{valor_atual}} +- Valor pretendido: {{valor_pretendido}} +- Fundamentação: {{fundamentacao}} + +**Documentação e pedidos:** +- Documentos: {{documentos}} +- Tutela de urgência: {{pedido_tutela}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 317 do CC (correção do valor da prestação) +- Art. 478 do CC (resolução por onerosidade excessiva) +- Art. 51 do CDC (nulidade de cláusulas abusivas) +- Art. 6º, V do CDC (modificação de cláusulas desproporcionais) + +Inclua pedidos de: revisão das cláusulas abusivas, adequação dos valores, tutela de urgência (se aplicável), custas processuais e honorários advocatícios.`, + + legalReferences: [ + "art. 317 CC", + "art. 478 CC", + "art. 51 CDC", + "art. 6º, V CDC", + ], +}; diff --git a/jus-ia-start-kit/src/flows/civel/indenizacao.ts b/jus-ia-start-kit/src/flows/civel/indenizacao.ts new file mode 100644 index 000000000..8f8787cd9 --- /dev/null +++ b/jus-ia-start-kit/src/flows/civel/indenizacao.ts @@ -0,0 +1,192 @@ +import type { FlowConfig } from "../types.js"; + +export const indenizacaoFlow: FlowConfig = { + area: "civel", + areaLabel: "Cível", + subtipo: "indenizacao", + subtipoLabel: "Indenização", + tipoTarefa: "peticao-inicial", + steps: [ + { + stepNumber: 1, + title: "Dados do Fato", + requiresLlm: false, + groups: [ + { + title: "Tipo de Dano", + questions: [ + { + id: "tipo_indenizacao", + text: "Qual o tipo de indenização pretendida?", + type: "select", + options: [ + "Dano material", + "Dano moral", + "Danos estéticos", + "Lucros cessantes", + "Dano material e moral", + ], + required: true, + }, + { + id: "origem", + text: "Qual a origem do dano?", + type: "select", + options: [ + "Acidente de trânsito", + "Erro médico", + "Relação de consumo", + "Relação contratual", + "Ato ilícito", + "Outro", + ], + required: true, + }, + { + id: "data_fato", + text: "Data do fato", + type: "date", + required: true, + }, + ], + }, + { + title: "Partes", + questions: [ + { + id: "tipo_responsavel", + text: "O responsável é pessoa física, jurídica ou poder público?", + type: "select", + options: ["PF", "PJ", "Poder Público"], + required: true, + }, + { + id: "relacao_com_responsavel", + text: "Qual a relação com o responsável?", + type: "select", + options: [ + "Consumidor", + "Contratante", + "Terceiro", + "Paciente", + "Outro", + ], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Detalhes e Provas", + requiresLlm: true, + groups: [ + { + title: "Extensão do Dano", + questions: [ + { + id: "descricao_dano", + text: "Descrição do dano sofrido", + type: "text", + placeholder: "Descreva brevemente o dano sofrido", + required: true, + }, + { + id: "valor_estimado", + text: "Valor estimado da indenização", + type: "select", + options: [ + "Até R$ 10 mil", + "R$ 10 a 50 mil", + "R$ 50 a 100 mil", + "Acima de R$ 100 mil", + ], + required: true, + }, + { + id: "consequencias", + text: "Quais as consequências do dano?", + type: "multiselect", + options: [ + "Gastos médicos", + "Perda de renda", + "Dano psicológico", + "Dano estético", + "Perda de bem material", + "Outro", + ], + required: true, + }, + ], + }, + { + title: "Provas", + questions: [ + { + id: "provas", + text: "Quais provas possui?", + type: "multiselect", + options: [ + "Boletim de ocorrência", + "Laudos médicos", + "Fotos", + "Vídeos", + "Testemunhas", + "Notas fiscais", + "Orçamentos", + "Contratos", + "Nenhuma", + ], + required: true, + }, + { + id: "tentou_acordo", + text: "Tentou acordo extrajudicial?", + type: "select", + options: ["Sim, sem sucesso", "Não"], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma petição inicial de ação de indenização por responsabilidade civil com os seguintes dados: + +**Partes:** +- Autor: [a ser preenchido pelo advogado] +- Réu: {{tipo_responsavel}} +- Relação: {{relacao_com_responsavel}} + +**Fato gerador:** +- Origem: {{origem}} +- Data do fato: {{data_fato}} +- Tipo de indenização: {{tipo_indenizacao}} + +**Extensão do dano:** +- Descrição: {{descricao_dano}} +- Valor estimado: {{valor_estimado}} +- Consequências: {{consequencias}} + +**Provas disponíveis:** +- Provas: {{provas}} +- Tentativa de acordo: {{tentou_acordo}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 186 do CC (ato ilícito) +- Art. 927 do CC (obrigação de reparar o dano) +- Art. 944 do CC (indenização mede-se pela extensão do dano) +- Art. 14 do CDC (responsabilidade do fornecedor) + +Inclua pedidos de: indenização por danos materiais e/ou morais, juros de mora, correção monetária, custas processuais e honorários advocatícios.`, + + legalReferences: [ + "art. 186 CC", + "art. 927 CC", + "art. 944 CC", + "art. 14 CDC", + ], +}; diff --git a/jus-ia-start-kit/src/flows/civel/index.ts b/jus-ia-start-kit/src/flows/civel/index.ts index 79c6eaecb..956192057 100644 --- a/jus-ia-start-kit/src/flows/civel/index.ts +++ b/jus-ia-start-kit/src/flows/civel/index.ts @@ -2,10 +2,10 @@ export const civelArea = { value: "civel", label: "Cível", subtipos: [ - { value: "cobranca", label: "Cobrança", available: false }, - { value: "indenizacao", label: "Indenização", available: false }, - { value: "obrigacao-fazer", label: "Obrigação de Fazer", available: false }, - { value: "contestacao", label: "Contestação Cível", available: false }, - { value: "contrato", label: "Contrato (Revisão)", available: false }, + { value: "cobranca", label: "Cobrança" }, + { value: "indenizacao", label: "Indenização" }, + { value: "obrigacao-fazer", label: "Obrigação de Fazer" }, + { value: "contestacao", label: "Contestação Cível" }, + { value: "contrato", label: "Contrato (Revisão)" }, ], }; diff --git a/jus-ia-start-kit/src/flows/civel/obrigacao-fazer.ts b/jus-ia-start-kit/src/flows/civel/obrigacao-fazer.ts new file mode 100644 index 000000000..309e4f4e5 --- /dev/null +++ b/jus-ia-start-kit/src/flows/civel/obrigacao-fazer.ts @@ -0,0 +1,182 @@ +import type { FlowConfig } from "../types.js"; + +export const obrigacaoFazerFlow: FlowConfig = { + area: "civel", + areaLabel: "Cível", + subtipo: "obrigacao-fazer", + subtipoLabel: "Obrigação de Fazer", + tipoTarefa: "peticao-inicial", + steps: [ + { + stepNumber: 1, + title: "Dados da Obrigação", + requiresLlm: false, + groups: [ + { + title: "Origem", + questions: [ + { + id: "origem_obrigacao", + text: "Qual a origem da obrigação?", + type: "select", + options: [ + "Contrato", + "Decisão judicial anterior", + "Lei ou regulamento", + "Relação de consumo", + "Outro", + ], + required: true, + }, + { + id: "tipo_obrigante", + text: "Quem deveria cumprir a obrigação?", + type: "select", + options: ["PF", "PJ", "Poder Público"], + required: true, + }, + ], + }, + { + title: "Obrigação", + questions: [ + { + id: "descricao_obrigacao", + text: "Descrição da obrigação", + type: "text", + placeholder: "O que deveria ser feito ou entregue?", + required: true, + }, + { + id: "prazo_original", + text: "Prazo original para cumprimento", + type: "date", + required: true, + }, + { + id: "urgencia", + text: "Qual o grau de urgência?", + type: "select", + options: [ + "Urgente - risco de dano irreparável", + "Moderada", + "Baixa - pode aguardar rito normal", + ], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Detalhes", + requiresLlm: true, + groups: [ + { + title: "Inadimplência", + questions: [ + { + id: "notificou", + text: "Notificou o obrigado?", + type: "select", + options: [ + "Sim, com AR", + "Sim, por e-mail", + "Sim, verbal", + "Não", + ], + required: true, + }, + { + id: "motivo_recusa", + text: "Qual o motivo da recusa ou inadimplência?", + type: "select", + options: [ + "Alega impossibilidade", + "Ignora pedidos", + "Contesta obrigação", + "Desconhecido", + ], + required: true, + }, + { + id: "prejuizo", + text: "Qual o prejuízo causado?", + type: "text", + placeholder: "Qual o prejuízo pela não realização?", + required: true, + }, + ], + }, + { + title: "Provas", + questions: [ + { + id: "documentos", + text: "Quais documentos possui?", + type: "multiselect", + options: [ + "Contrato", + "Notificação", + "E-mails", + "Mensagens", + "Protocolo de atendimento", + "Fotos", + "Nenhum", + ], + required: true, + }, + { + id: "pedido_tutela", + text: "Deseja pedir tutela de urgência?", + type: "select", + options: [ + "Sim, com tutela de urgência", + "Não, apenas rito normal", + ], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma petição inicial de ação de obrigação de fazer com os seguintes dados: + +**Partes:** +- Autor: [a ser preenchido pelo advogado] +- Réu: {{tipo_obrigante}} + +**Obrigação:** +- Origem: {{origem_obrigacao}} +- Descrição: {{descricao_obrigacao}} +- Prazo original: {{prazo_original}} +- Urgência: {{urgencia}} + +**Inadimplência:** +- Notificação: {{notificou}} +- Motivo da recusa: {{motivo_recusa}} +- Prejuízo: {{prejuizo}} + +**Provas e pedidos:** +- Documentos: {{documentos}} +- Tutela de urgência: {{pedido_tutela}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 497 do CPC (tutela específica das obrigações de fazer) +- Art. 536 do CPC (cumprimento de sentença de obrigação de fazer) +- Art. 537 do CPC (multa periódica - astreintes) +- Art. 300 do CPC (tutela de urgência) + +Inclua pedidos de: cumprimento da obrigação de fazer, fixação de astreintes em caso de descumprimento, tutela de urgência (se aplicável), custas processuais e honorários advocatícios.`, + + legalReferences: [ + "art. 497 CPC", + "art. 536 CPC", + "art. 537 CPC", + "art. 300 CPC", + ], +}; diff --git a/jus-ia-start-kit/src/flows/trabalhista/acumulo-funcao.ts b/jus-ia-start-kit/src/flows/trabalhista/acumulo-funcao.ts new file mode 100644 index 000000000..4bf1a8c06 --- /dev/null +++ b/jus-ia-start-kit/src/flows/trabalhista/acumulo-funcao.ts @@ -0,0 +1,187 @@ +import type { FlowConfig } from "../types.js"; + +export const acumuloFuncaoFlow: FlowConfig = { + area: "trabalhista", + areaLabel: "Trabalhista", + subtipo: "acumulo-funcao", + subtipoLabel: "Acúmulo de Função", + tipoTarefa: "peticao-inicial", + steps: [ + { + stepNumber: 1, + title: "Dados do Vínculo", + requiresLlm: false, + groups: [ + { + title: "Contrato", + questions: [ + { + id: "cargo_registrado", + text: "Qual o cargo registrado em carteira?", + type: "text", + placeholder: "Ex: Auxiliar Administrativo", + required: true, + }, + { + id: "cargo_exercido", + text: "Qual o cargo efetivamente exercido?", + type: "text", + placeholder: "Ex: Auxiliar Administrativo + Financeiro", + required: true, + }, + { + id: "regime", + text: "Qual o regime de trabalho?", + type: "select", + options: ["CLT", "PJ", "Autônomo", "Temporário"], + required: true, + }, + { + id: "tempo_servico", + text: "Quanto tempo de serviço?", + type: "select", + options: [ + "Menos de 1 ano", + "1 a 3 anos", + "3 a 5 anos", + "Mais de 5 anos", + ], + required: true, + }, + ], + }, + { + title: "Remuneração", + questions: [ + { + id: "faixa_salarial", + text: "Qual a faixa salarial?", + type: "select", + options: [ + "Até 2 SM", + "2 a 5 SM", + "5 a 10 SM", + "Acima de 10 SM", + ], + required: true, + }, + { + id: "recebeu_adicional", + text: "Recebeu algum adicional pelo acúmulo?", + type: "select", + options: ["Sim", "Não"], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Detalhes do Acúmulo", + requiresLlm: true, + groups: [ + { + title: "Funções Acumuladas", + questions: [ + { + id: "funcoes_extras", + text: "Descreva as funções exercidas além do cargo contratado", + type: "text", + placeholder: "Descreva as funções exercidas além do cargo contratado", + required: true, + }, + { + id: "inicio_acumulo", + text: "Quando começou o acúmulo de função?", + type: "select", + options: [ + "Desde a contratação", + "Após promoção", + "Após saída de colega", + "Após reestruturação", + ], + required: true, + }, + { + id: "frequencia_acumulo", + text: "Com que frequência exerce as funções extras?", + type: "select", + options: [ + "Diariamente", + "Várias vezes por semana", + "Eventualmente", + ], + required: true, + }, + ], + }, + { + title: "Provas", + questions: [ + { + id: "provas_disponiveis", + text: "Quais provas estão disponíveis?", + type: "multiselect", + options: [ + "E-mails com atribuições", + "Testemunhas", + "Descrição de cargo", + "Contracheques", + "Prints de sistemas", + "Nenhuma", + ], + required: true, + }, + { + id: "outros_exercem", + text: "Há outro funcionário específico para a função acumulada?", + type: "select", + options: [ + "Sim, há funcionário específico", + "Sim, havia antes", + "Não sei", + ], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma petição inicial trabalhista de adicional por acúmulo de função com os seguintes dados: + +**Partes:** +- Reclamante: [a ser preenchido pelo advogado] +- Reclamada: [a ser preenchido pelo advogado] + +**Vínculo empregatício:** +- Cargo registrado: {{cargo_registrado}} +- Cargo efetivamente exercido: {{cargo_exercido}} +- Regime: {{regime}} +- Tempo de serviço: {{tempo_servico}} +- Faixa salarial: {{faixa_salarial}} +- Recebeu adicional: {{recebeu_adicional}} + +**Detalhes do acúmulo:** +- Funções extras exercidas: {{funcoes_extras}} +- Início do acúmulo: {{inicio_acumulo}} +- Frequência: {{frequencia_acumulo}} + +**Provas:** +- Provas disponíveis: {{provas_disponiveis}} +- Existência de funcionário específico para a função: {{outros_exercem}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 456, parágrafo único da CLT (condição do contrato de trabalho) +- Art. 468 da CLT (alteração contratual lesiva) + +Inclua pedidos de: pagamento de adicional por acúmulo de função com reflexos em férias + 1/3, 13º salário, FGTS, DSR, e honorários advocatícios.`, + + legalReferences: [ + "art. 456, parágrafo único CLT", + "art. 468 CLT", + ], +}; diff --git a/jus-ia-start-kit/src/flows/trabalhista/contestacao.ts b/jus-ia-start-kit/src/flows/trabalhista/contestacao.ts new file mode 100644 index 000000000..504e72b05 --- /dev/null +++ b/jus-ia-start-kit/src/flows/trabalhista/contestacao.ts @@ -0,0 +1,181 @@ +import type { FlowConfig } from "../types.js"; + +export const contestacaoTrabalhistaFlow: FlowConfig = { + area: "trabalhista", + areaLabel: "Trabalhista", + subtipo: "contestacao", + subtipoLabel: "Contestação Trabalhista", + tipoTarefa: "contestacao", + steps: [ + { + stepNumber: 1, + title: "Dados do Processo", + requiresLlm: false, + groups: [ + { + title: "Processo", + questions: [ + { + id: "numero_processo", + text: "Qual o número do processo?", + type: "text", + placeholder: "0000000-00.0000.0.00.0000", + required: true, + }, + { + id: "vara_tribunal", + text: "Qual a vara e tribunal?", + type: "text", + placeholder: "Ex: 1ª Vara do Trabalho de São Paulo", + required: true, + }, + { + id: "tipo_acao", + text: "Qual o tipo de ação?", + type: "select", + options: [ + "Reclamatória trabalhista", + "Ação de consignação", + "Inquérito para apuração de falta grave", + "Outra", + ], + required: true, + }, + ], + }, + { + title: "Partes", + questions: [ + { + id: "porte_empresa", + text: "Qual o porte da empresa reclamada?", + type: "select", + options: [ + "Microempresa", + "Pequena empresa", + "Média empresa", + "Grande empresa", + ], + required: true, + }, + { + id: "segmento", + text: "Qual o segmento da empresa?", + type: "text", + placeholder: "Ex: Comércio, Indústria, Serviços", + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Pedidos do Reclamante", + requiresLlm: true, + groups: [ + { + title: "Pedidos a Contestar", + questions: [ + { + id: "pedidos_principais", + text: "Quais os principais pedidos do reclamante?", + type: "multiselect", + options: [ + "Horas extras", + "Verbas rescisórias", + "Dano moral", + "Vínculo empregatício", + "Equiparação salarial", + "Adicional de insalubridade", + "Desvio de função", + "Outro", + ], + required: true, + }, + { + id: "valor_causa", + text: "Qual o valor da causa?", + type: "select", + options: [ + "Até R$ 20 mil", + "R$ 20 a 50 mil", + "R$ 50 a 100 mil", + "Acima de R$ 100 mil", + ], + required: true, + }, + ], + }, + { + title: "Defesa", + questions: [ + { + id: "teses_defesa", + text: "Quais teses de defesa serão utilizadas?", + type: "multiselect", + options: [ + "Prescrição", + "Ausência de provas", + "Acordo coletivo válido", + "Justa causa comprovada", + "Inexistência de vínculo", + "Pagamento correto", + "Culpa do reclamante", + ], + required: true, + }, + { + id: "documentos_empresa", + text: "Quais documentos a empresa possui?", + type: "multiselect", + options: [ + "Controle de ponto", + "Contracheques", + "Contrato de trabalho", + "TRCT", + "Acordo coletivo", + "Regulamento interno", + "Nenhum", + ], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma contestação trabalhista com os seguintes dados: + +**Processo:** +- Número: {{numero_processo}} +- Vara/Tribunal: {{vara_tribunal}} +- Tipo de ação: {{tipo_acao}} + +**Reclamada:** +- Porte: {{porte_empresa}} +- Segmento: {{segmento}} + +**Pedidos do reclamante a contestar:** +- Pedidos principais: {{pedidos_principais}} +- Valor da causa: {{valor_causa}} + +**Teses de defesa:** +- Teses: {{teses_defesa}} +- Documentos disponíveis: {{documentos_empresa}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 818 da CLT (ônus da prova) +- Art. 373 do CPC (distribuição do ônus da prova) +- Art. 769 da CLT (aplicação subsidiária do CPC) + +Inclua preliminares pertinentes, conteste cada pedido individualmente com fundamentos fáticos e jurídicos, e formule pedidos finais de improcedência total dos pedidos do reclamante, com condenação em honorários advocatícios sucumbenciais.`, + + legalReferences: [ + "art. 818 CLT", + "art. 373 CPC", + "art. 769 CLT", + ], +}; diff --git a/jus-ia-start-kit/src/flows/trabalhista/dano-moral.ts b/jus-ia-start-kit/src/flows/trabalhista/dano-moral.ts new file mode 100644 index 000000000..1335745cf --- /dev/null +++ b/jus-ia-start-kit/src/flows/trabalhista/dano-moral.ts @@ -0,0 +1,202 @@ +import type { FlowConfig } from "../types.js"; + +export const danoMoralFlow: FlowConfig = { + area: "trabalhista", + areaLabel: "Trabalhista", + subtipo: "dano-moral", + subtipoLabel: "Dano Moral", + tipoTarefa: "peticao-inicial", + steps: [ + { + stepNumber: 1, + title: "Dados do Vínculo", + requiresLlm: false, + groups: [ + { + title: "Relação de Trabalho", + questions: [ + { + id: "empregador_tipo", + text: "O empregador é pessoa jurídica ou física?", + type: "select", + options: ["Pessoa Jurídica (empresa)", "Pessoa Física"], + required: true, + }, + { + id: "regime", + text: "Qual o regime de trabalho?", + type: "select", + options: ["CLT", "PJ", "Autônomo", "Temporário"], + required: true, + }, + { + id: "cargo", + text: "Qual o cargo exercido?", + type: "text", + required: true, + }, + { + id: "tempo_servico", + text: "Quanto tempo de serviço?", + type: "select", + options: [ + "Menos de 1 ano", + "1 a 3 anos", + "3 a 5 anos", + "Mais de 5 anos", + ], + required: true, + }, + ], + }, + { + title: "Remuneração", + questions: [ + { + id: "faixa_salarial", + text: "Qual a faixa salarial?", + type: "select", + options: [ + "Até 2 SM", + "2 a 5 SM", + "5 a 10 SM", + "Acima de 10 SM", + ], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Detalhes do Dano", + requiresLlm: true, + groups: [ + { + title: "Caracterização do Dano", + questions: [ + { + id: "tipo_dano", + text: "Qual o tipo de dano sofrido?", + type: "select", + options: [ + "Assédio moral", + "Assédio sexual", + "Discriminação", + "Acidente de trabalho", + "Exposição indevida", + "Revista íntima", + "Outro", + ], + required: true, + }, + { + id: "frequencia", + text: "Qual a frequência das ocorrências?", + type: "select", + options: [ + "Episódio único", + "Recorrente", + "Sistemático", + ], + required: true, + }, + { + id: "periodo_ocorrencias", + text: "Por quanto tempo ocorreram os fatos?", + type: "select", + options: [ + "Menos de 3 meses", + "3 a 6 meses", + "6 a 12 meses", + "Mais de 1 ano", + ], + required: true, + }, + ], + }, + { + title: "Impacto e Provas", + questions: [ + { + id: "consequencias", + text: "Quais consequências o dano causou?", + type: "multiselect", + options: [ + "Afastamento médico", + "Tratamento psicológico", + "Perda salarial", + "Danos à reputação", + "Nenhuma consequência formal", + ], + required: true, + }, + { + id: "provas_disponiveis", + text: "Quais provas estão disponíveis?", + type: "multiselect", + options: [ + "Mensagens", + "E-mails", + "Testemunhas", + "Laudos médicos", + "Câmeras", + "Documentos", + "Nenhuma", + ], + required: true, + }, + { + id: "registrou_ocorrencia", + text: "Registrou a ocorrência formalmente?", + type: "select", + options: [ + "Sim, BO", + "Sim, RH ou ouvidoria", + "Não", + ], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma petição inicial trabalhista de indenização por dano moral com os seguintes dados: + +**Partes:** +- Reclamante: [a ser preenchido pelo advogado] +- Reclamada: {{empregador_tipo}} + +**Vínculo empregatício:** +- Regime: {{regime}} +- Cargo: {{cargo}} +- Tempo de serviço: {{tempo_servico}} +- Faixa salarial: {{faixa_salarial}} + +**Caracterização do dano:** +- Tipo de dano: {{tipo_dano}} +- Frequência: {{frequencia}} +- Período das ocorrências: {{periodo_ocorrencias}} + +**Impacto e provas:** +- Consequências: {{consequencias}} +- Provas disponíveis: {{provas_disponiveis}} +- Registro de ocorrência: {{registrou_ocorrencia}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Arts. 223-A a 223-G da CLT (dano extrapatrimonial nas relações de trabalho) +- Art. 186 do Código Civil (ato ilícito) +- Art. 927 do Código Civil (obrigação de reparar o dano) + +Inclua pedidos de: indenização por dano moral com arbitramento judicial do valor, considerando a gravidade da ofensa, a condição econômica das partes e o caráter pedagógico, além de honorários advocatícios.`, + + legalReferences: [ + "art. 223-A a 223-G CLT", + "art. 186 CC", + "art. 927 CC", + ], +}; diff --git a/jus-ia-start-kit/src/flows/trabalhista/index.ts b/jus-ia-start-kit/src/flows/trabalhista/index.ts index 45a79ce2a..3fcc90bd7 100644 --- a/jus-ia-start-kit/src/flows/trabalhista/index.ts +++ b/jus-ia-start-kit/src/flows/trabalhista/index.ts @@ -3,9 +3,9 @@ export const trabalhistaArea = { label: "Trabalhista", subtipos: [ { value: "horas-extras", label: "Horas Extras" }, - { value: "rescisao-indireta", label: "Rescisão Indireta", available: false }, - { value: "dano-moral", label: "Dano Moral", available: false }, - { value: "acumulo-funcao", label: "Acúmulo de Função", available: false }, - { value: "contestacao", label: "Contestação Trabalhista", available: false }, + { value: "rescisao-indireta", label: "Rescisão Indireta" }, + { value: "dano-moral", label: "Dano Moral" }, + { value: "acumulo-funcao", label: "Acúmulo de Função" }, + { value: "contestacao", label: "Contestação Trabalhista" }, ], }; diff --git a/jus-ia-start-kit/src/flows/trabalhista/rescisao-indireta.ts b/jus-ia-start-kit/src/flows/trabalhista/rescisao-indireta.ts new file mode 100644 index 000000000..7ab016a51 --- /dev/null +++ b/jus-ia-start-kit/src/flows/trabalhista/rescisao-indireta.ts @@ -0,0 +1,192 @@ +import type { FlowConfig } from "../types.js"; + +export const rescisaoIndiretaFlow: FlowConfig = { + area: "trabalhista", + areaLabel: "Trabalhista", + subtipo: "rescisao-indireta", + subtipoLabel: "Rescisão Indireta", + tipoTarefa: "peticao-inicial", + steps: [ + { + stepNumber: 1, + title: "Dados do Vínculo", + requiresLlm: false, + groups: [ + { + title: "Relação de Trabalho", + questions: [ + { + id: "regime", + text: "Qual o regime de trabalho?", + type: "select", + options: ["CLT", "PJ", "Autônomo", "Temporário"], + required: true, + }, + { + id: "cargo", + text: "Qual o cargo exercido?", + type: "text", + required: true, + }, + { + id: "tempo_servico", + text: "Quanto tempo de serviço?", + type: "select", + options: [ + "Menos de 1 ano", + "1 a 3 anos", + "3 a 5 anos", + "Mais de 5 anos", + ], + required: true, + }, + ], + }, + { + title: "Remuneração", + questions: [ + { + id: "salario_tipo", + text: "Qual o tipo de salário?", + type: "select", + options: ["Fixo", "Comissão", "Misto"], + required: true, + }, + { + id: "faixa_salarial", + text: "Qual a faixa salarial?", + type: "select", + options: [ + "Até 2 SM", + "2 a 5 SM", + "5 a 10 SM", + "Acima de 10 SM", + ], + required: true, + }, + ], + }, + ], + }, + { + stepNumber: 2, + title: "Motivos da Rescisão", + requiresLlm: true, + groups: [ + { + title: "Falta Grave do Empregador", + questions: [ + { + id: "motivo_principal", + text: "Qual o motivo principal da rescisão indireta?", + type: "select", + options: [ + "Atraso reiterado de salários", + "Não recolhimento de FGTS", + "Assédio moral", + "Desvio de função", + "Redução salarial", + "Condições de trabalho inadequadas", + ], + required: true, + }, + { + id: "motivos_adicionais", + text: "Há motivos adicionais?", + type: "multiselect", + options: [ + "Atraso reiterado de salários", + "Não recolhimento de FGTS", + "Assédio moral", + "Desvio de função", + "Redução salarial", + "Condições de trabalho inadequadas", + ], + required: false, + }, + { + id: "inicio_problemas", + text: "Há quanto tempo os problemas começaram?", + type: "select", + options: [ + "Menos de 3 meses", + "3 a 6 meses", + "6 a 12 meses", + "Mais de 1 ano", + ], + required: true, + }, + ], + }, + { + title: "Evidências", + questions: [ + { + id: "provas_disponiveis", + text: "Quais provas estão disponíveis?", + type: "multiselect", + options: [ + "Contracheques", + "Extratos FGTS", + "Mensagens", + "Testemunhas", + "E-mails", + "Fotos ou vídeos", + "Documentos médicos", + "Nenhuma", + ], + required: true, + }, + { + id: "comunicou_empregador", + text: "Comunicou o empregador sobre os problemas?", + type: "select", + options: [ + "Sim, formalmente", + "Sim, verbalmente", + "Não", + ], + required: true, + }, + ], + }, + ], + }, + ], + promptTemplate: `Elabore uma petição inicial trabalhista de rescisão indireta do contrato de trabalho com os seguintes dados: + +**Partes:** +- Reclamante: [a ser preenchido pelo advogado] +- Reclamada: [a ser preenchido pelo advogado] + +**Vínculo empregatício:** +- Regime: {{regime}} +- Cargo: {{cargo}} +- Tempo de serviço: {{tempo_servico}} +- Tipo de salário: {{salario_tipo}} +- Faixa salarial: {{faixa_salarial}} + +**Motivos da rescisão indireta:** +- Motivo principal: {{motivo_principal}} +- Motivos adicionais: {{motivos_adicionais}} +- Início dos problemas: {{inicio_problemas}} + +**Evidências:** +- Provas disponíveis: {{provas_disponiveis}} +- Comunicação ao empregador: {{comunicou_empregador}} + +{{#refinement_context}} + +**Fundamente com base em:** +- Art. 483 da CLT (hipóteses de rescisão indireta) +- Art. 487 da CLT (aviso prévio) +- Súmula 13 do TST (mora salarial) + +Inclua pedidos de: reconhecimento da rescisão indireta, pagamento de verbas rescisórias como dispensa sem justa causa (saldo de salário, aviso prévio, 13º proporcional, férias + 1/3, FGTS + 40%), guias para seguro-desemprego, e honorários advocatícios.`, + + legalReferences: [ + "art. 483 CLT", + "art. 487 CLT", + "Súmula 13 TST", + ], +}; diff --git a/jus-ia-start-kit/src/services/__tests__/flow-engine.test.ts b/jus-ia-start-kit/src/services/__tests__/flow-engine.test.ts new file mode 100644 index 000000000..d86a9458b --- /dev/null +++ b/jus-ia-start-kit/src/services/__tests__/flow-engine.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from "vitest"; +import { calculateTotalSteps, getVisualStep, isStepComplete } from "../flow-engine.js"; +import type { FlowConfig, FlowStep } from "../../flows/types.js"; + +const mockFlow: FlowConfig = { + area: "trabalhista", + areaLabel: "Trabalhista", + subtipo: "horas-extras", + subtipoLabel: "Horas Extras", + tipoTarefa: "peticao-inicial", + steps: [ + { stepNumber: 1, title: "Step 1", groups: [], requiresLlm: false }, + { stepNumber: 2, title: "Step 2", groups: [], requiresLlm: true }, + ], + promptTemplate: "", + legalReferences: [], +}; + +describe("calculateTotalSteps", () => { + it("includes selection screens when no deep link", () => { + expect(calculateTotalSteps(mockFlow, false)).toBe(5); // 2 selection + 2 flow + 1 preview + }); + + it("excludes selection screens with deep link", () => { + expect(calculateTotalSteps(mockFlow, true)).toBe(3); // 0 selection + 2 flow + 1 preview + }); +}); + +describe("getVisualStep", () => { + it("adds offset for non-deep-link", () => { + expect(getVisualStep(mockFlow, 1, false)).toBe(3); // offset 2 + step 1 + }); + + it("no offset for deep link", () => { + expect(getVisualStep(mockFlow, 1, true)).toBe(1); + }); +}); + +describe("isStepComplete", () => { + const step: FlowStep = { + stepNumber: 1, + title: "Test", + requiresLlm: false, + groups: [ + { + title: "Group 1", + questions: [ + { id: "q1", text: "Question 1", type: "select", options: ["A", "B"], required: true }, + { id: "q2", text: "Question 2", type: "text", required: false }, + ], + }, + ], + }; + + it("returns true when all required questions answered", () => { + expect(isStepComplete(step, { q1: "A" })).toBe(true); + }); + + it("returns false when required question missing", () => { + expect(isStepComplete(step, {})).toBe(false); + }); + + it("returns false when required answer is empty", () => { + expect(isStepComplete(step, { q1: "" })).toBe(false); + }); + + it("returns true when optional question missing", () => { + expect(isStepComplete(step, { q1: "A" })).toBe(true); + }); +}); diff --git a/jus-ia-start-kit/src/services/__tests__/llm-client.test.ts b/jus-ia-start-kit/src/services/__tests__/llm-client.test.ts new file mode 100644 index 000000000..a6dcbda11 --- /dev/null +++ b/jus-ia-start-kit/src/services/__tests__/llm-client.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { getRefinementQuestions } from "../llm-client.js"; +import type { FlowState } from "../../flows/types.js"; + +const mockState: FlowState = { + area: "trabalhista", + subtipo: "horas-extras", + tipoTarefa: "peticao-inicial", + currentStep: 1, + totalSteps: 5, + responses: { regime: "CLT" }, +}; + +describe("getRefinementQuestions", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("returns empty array when no API key configured", async () => { + const result = await getRefinementQuestions(mockState, "Trabalhista", "Horas Extras"); + expect(result).toEqual([]); + }); + + it("returns empty array on fetch failure (graceful fallback)", async () => { + // Even if somehow config had a key, network failure should return [] + const result = await getRefinementQuestions(mockState, "Trabalhista", "Horas Extras"); + expect(Array.isArray(result)).toBe(true); + }); +}); diff --git a/jus-ia-start-kit/src/services/__tests__/prompt-builder.test.ts b/jus-ia-start-kit/src/services/__tests__/prompt-builder.test.ts new file mode 100644 index 000000000..e0f08df7f --- /dev/null +++ b/jus-ia-start-kit/src/services/__tests__/prompt-builder.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from "vitest"; +import { buildPrompt } from "../prompt-builder.js"; +import type { FlowConfig, FlowState } from "../../flows/types.js"; + +const mockFlow: FlowConfig = { + area: "trabalhista", + areaLabel: "Trabalhista", + subtipo: "horas-extras", + subtipoLabel: "Horas Extras", + tipoTarefa: "peticao-inicial", + steps: [], + promptTemplate: "Regime: {{regime}}, Jornada: {{jornada_contratual}}, Extra: {{horas_extras_semana}}", + legalReferences: ["art. 59 CLT"], +}; + +const mockState: FlowState = { + area: "trabalhista", + subtipo: "horas-extras", + tipoTarefa: "peticao-inicial", + currentStep: 2, + totalSteps: 5, + responses: { + regime: "CLT", + jornada_contratual: "44h semanais", + horas_extras_semana: "5 a 10 horas", + }, +}; + +describe("buildPrompt", () => { + it("interpolates template variables", () => { + const result = buildPrompt(mockFlow, mockState); + expect(result.text).toContain("CLT"); + expect(result.text).toContain("44h semanais"); + expect(result.text).toContain("5 a 10 horas"); + expect(result.text).not.toContain("{{"); + }); + + it("returns legal references", () => { + const result = buildPrompt(mockFlow, mockState); + expect(result.legalReferences).toEqual(["art. 59 CLT"]); + }); + + it("calculates char count", () => { + const result = buildPrompt(mockFlow, mockState); + expect(result.charCount).toBe(result.text.length); + }); + + it("determines fitsInUrl correctly for short prompt", () => { + const result = buildPrompt(mockFlow, mockState); + expect(result.fitsInUrl).toBe(true); + expect(result.encodedUrl).toBeDefined(); + }); + + it("determines fitsInUrl correctly for long prompt", () => { + const longFlow = { + ...mockFlow, + promptTemplate: "A".repeat(2000), + }; + const result = buildPrompt(longFlow, mockState); + expect(result.fitsInUrl).toBe(false); + expect(result.encodedUrl).toBeUndefined(); + }); + + it("removes unreplaced variables", () => { + const flowWithExtra = { + ...mockFlow, + promptTemplate: "{{regime}} - {{campo_inexistente}}", + }; + const result = buildPrompt(flowWithExtra, mockState); + expect(result.text).not.toContain("{{campo_inexistente}}"); + expect(result.text).toContain("CLT"); + }); + + it("handles array responses by joining with comma", () => { + const stateWithArray = { + ...mockState, + responses: { ...mockState.responses, provas: ["Doc1", "Doc2"] }, + }; + const flowWithArray = { + ...mockFlow, + promptTemplate: "Provas: {{provas}}", + }; + const result = buildPrompt(flowWithArray, stateWithArray); + expect(result.text).toContain("Doc1, Doc2"); + }); +}); diff --git a/jus-ia-start-kit/src/services/__tests__/url-builder.test.ts b/jus-ia-start-kit/src/services/__tests__/url-builder.test.ts new file mode 100644 index 000000000..3f57f2729 --- /dev/null +++ b/jus-ia-start-kit/src/services/__tests__/url-builder.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest"; +import { buildRedirectUrl, getJusIaDirectUrl } from "../url-builder.js"; + +describe("buildRedirectUrl", () => { + it("builds correct URL with encoded prompt", () => { + const url = buildRedirectUrl("teste prompt"); + expect(url).toContain("https://ia.jusbrasil.com.br/conversa?q="); + expect(url).toContain("teste%20prompt"); + expect(url.endsWith("&send")).toBe(true); + }); + + it("encodes special characters", () => { + const url = buildRedirectUrl("art. 59 da CLT (limite)"); + expect(url).toContain(encodeURIComponent("art. 59 da CLT (limite)")); + }); +}); + +describe("getJusIaDirectUrl", () => { + it("returns base Jus IA URL", () => { + expect(getJusIaDirectUrl()).toBe("https://ia.jusbrasil.com.br/conversa"); + }); +}); diff --git a/jus-ia-start-kit/src/services/llm-client.ts b/jus-ia-start-kit/src/services/llm-client.ts index 41973d76b..e7a62a32c 100644 --- a/jus-ia-start-kit/src/services/llm-client.ts +++ b/jus-ia-start-kit/src/services/llm-client.ts @@ -67,13 +67,26 @@ Gere perguntas de refinamento para capturar nuances deste caso.`; signal: controller.signal, }); } else { - // Default: OpenAI-compatible - response = await fetch("https://api.openai.com/v1/chat/completions", { + // OpenRouter (default) and OpenAI use the same OpenAI-compatible API + const baseUrl = + config.llm.provider === "openrouter" + ? config.llm.baseUrl + : "https://api.openai.com/v1"; + + const headers: Record = { + "Content-Type": "application/json", + Authorization: `Bearer ${config.llm.apiKey}`, + }; + + // OpenRouter-specific headers + if (config.llm.provider === "openrouter") { + headers["HTTP-Referer"] = "https://jus-ia-start-kit.app"; + headers["X-Title"] = "Jus IA Start Kit"; + } + + response = await fetch(`${baseUrl}/chat/completions`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.llm.apiKey}`, - }, + headers, body: JSON.stringify({ model: config.llm.model, messages: [ @@ -99,6 +112,7 @@ Gere perguntas de refinamento para capturar nuances deste caso.`; if (config.llm.provider === "anthropic") { content = data.content?.[0]?.text || "{}"; } else { + // OpenRouter and OpenAI both use the same response format content = data.choices?.[0]?.message?.content || "{}"; } diff --git a/jus-ia-start-kit/src/utils/__tests__/parse-flow-state.test.ts b/jus-ia-start-kit/src/utils/__tests__/parse-flow-state.test.ts new file mode 100644 index 000000000..5fb94fd0e --- /dev/null +++ b/jus-ia-start-kit/src/utils/__tests__/parse-flow-state.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect } from "vitest"; +import { parseFlowState, serializeFlowState } from "../parse-flow-state.js"; + +describe("parseFlowState", () => { + it("parses basic form body", () => { + const state = parseFlowState({ + _area: "trabalhista", + _subtipo: "horas-extras", + _tipo_tarefa: "peticao-inicial", + _step: "1", + _total_steps: "5", + _responses: "{}", + regime: "CLT", + }); + expect(state.area).toBe("trabalhista"); + expect(state.subtipo).toBe("horas-extras"); + expect(state.currentStep).toBe(1); + expect(state.responses.regime).toBe("CLT"); + }); + + it("merges existing responses with new ones", () => { + const state = parseFlowState({ + _area: "trabalhista", + _subtipo: "horas-extras", + _step: "2", + _total_steps: "5", + _responses: JSON.stringify({ regime: "CLT", jornada: "44h" }), + testemunhas: "Sim", + }); + expect(state.responses.regime).toBe("CLT"); + expect(state.responses.jornada).toBe("44h"); + expect(state.responses.testemunhas).toBe("Sim"); + }); + + it("handles invalid JSON in _responses", () => { + const state = parseFlowState({ + _area: "civel", + _subtipo: "cobranca", + _step: "1", + _total_steps: "4", + _responses: "invalid-json", + campo: "valor", + }); + expect(state.responses.campo).toBe("valor"); + }); + + it("handles empty body gracefully", () => { + const state = parseFlowState({}); + expect(state.area).toBe(""); + expect(state.currentStep).toBe(1); + expect(Object.keys(state.responses)).toHaveLength(0); + }); + + it("sanitizes input values", () => { + const state = parseFlowState({ + _area: "trabalhista", + _subtipo: "test", + _step: "1", + _total_steps: "3", + _responses: "{}", + campo: "Hello", + }); + expect(state.responses.campo).toBe("alert('xss')Hello"); + }); + + it("handles array values", () => { + const state = parseFlowState({ + _area: "trabalhista", + _subtipo: "test", + _step: "1", + _total_steps: "3", + _responses: "{}", + provas: ["Doc1", "Doc2"], + }); + expect(state.responses.provas).toEqual(["Doc1", "Doc2"]); + }); +}); + +describe("serializeFlowState", () => { + it("serializes responses to JSON", () => { + const json = serializeFlowState({ + area: "trabalhista", + subtipo: "horas-extras", + tipoTarefa: "peticao-inicial", + currentStep: 1, + totalSteps: 5, + responses: { regime: "CLT", jornada: "44h" }, + }); + const parsed = JSON.parse(json); + expect(parsed.regime).toBe("CLT"); + expect(parsed.jornada).toBe("44h"); + }); +}); diff --git a/jus-ia-start-kit/src/utils/__tests__/sanitize.test.ts b/jus-ia-start-kit/src/utils/__tests__/sanitize.test.ts new file mode 100644 index 000000000..43266cd10 --- /dev/null +++ b/jus-ia-start-kit/src/utils/__tests__/sanitize.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import { sanitizeText, validateSelect } from "../sanitize.js"; + +describe("sanitizeText", () => { + it("strips HTML tags", () => { + expect(sanitizeText("bold text")).toBe("bold text"); + }); + it("trims whitespace", () => { + expect(sanitizeText(" hello ")).toBe("hello"); + }); + it("caps at MAX_INPUT_LENGTH", () => { + const long = "a".repeat(600); + expect(sanitizeText(long).length).toBe(500); + }); + it("handles empty string", () => { + expect(sanitizeText("")).toBe(""); + }); + it("passes through clean text", () => { + expect(sanitizeText("Texto limpo")).toBe("Texto limpo"); + }); + it("strips nested HTML", () => { + expect(sanitizeText("

test

")).toBe("test"); + }); +}); + +describe("validateSelect", () => { + const options = ["CLT", "PJ", "Autônomo"]; + it("returns true for valid option", () => { + expect(validateSelect("CLT", options)).toBe(true); + }); + it("returns false for invalid option", () => { + expect(validateSelect("Outro", options)).toBe(false); + }); + it("returns false for empty string", () => { + expect(validateSelect("", options)).toBe(false); + }); +}); diff --git a/jus-ia-start-kit/vitest.config.ts b/jus-ia-start-kit/vitest.config.ts new file mode 100644 index 000000000..3f824fb95 --- /dev/null +++ b/jus-ia-start-kit/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + }, +});