diff --git a/README.md b/README.md index 5d62c270..9ab2d6fe 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Each kit includes configuration instructions, environment variables/lamatic-conf || | **🧑‍💼 Assistant Kits** | Create context-aware helpers for users, customers, and team members | | | [`/kits/assistant`](./kits/assistant) | | **Grammar Assistant** | A chrome extension to check grammar corrections across your selection. | Available | | [`/kits/assistant/grammar-extension`](./kits/assistant/grammar-extension) | +| **Legal Assistant** | Research legal questions against a Lamatic-connected legal corpus with citations, next steps, and a standing disclaimer. | Available | | [`/kits/assistant/legal-assistant`](./kits/assistant/legal-assistant) | || | **💬 Embed Kits** | Seamlessly integrate AI agents into apps, websites, and workflows | | | [`/kits/embed`](./kits/embed) | | **Chatbot** | A Next.js starter kit for chatbot using Lamatic Flows. | Available | [![Live Demo](https://img.shields.io/badge/Live%20Demo-black?style=for-the-badge)](https://agent-kit-embedded-chat.vercel.app) | [`/kits/embed/chat`](./kits/embed/chat) | diff --git a/kits/assistant/legal-assistant/.env.example b/kits/assistant/legal-assistant/.env.example new file mode 100644 index 00000000..259eaedf --- /dev/null +++ b/kits/assistant/legal-assistant/.env.example @@ -0,0 +1,4 @@ +ASSISTANT_LEGAL_CHATBOT=ASSISTANT_LEGAL_CHATBOT_FLOW_ID +LAMATIC_API_KEY=LAMATIC_API_KEY +LAMATIC_API_URL=LAMATIC_API_URL +LAMATIC_PROJECT_ID=LAMATIC_PROJECT_ID diff --git a/kits/assistant/legal-assistant/.gitignore b/kits/assistant/legal-assistant/.gitignore new file mode 100644 index 00000000..56e16272 --- /dev/null +++ b/kits/assistant/legal-assistant/.gitignore @@ -0,0 +1,31 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +.next-dev*.log + +# env files +.env +.env.local +.env.*.local +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/kits/assistant/legal-assistant/README.md b/kits/assistant/legal-assistant/README.md new file mode 100644 index 00000000..c163fff6 --- /dev/null +++ b/kits/assistant/legal-assistant/README.md @@ -0,0 +1,92 @@ +# Legal Assistant + +Legal Assistant is a Lamatic-powered research kit for answering legal questions against a connected legal corpus. It is built for statutes, regulations, case summaries, internal policy libraries, or legal memos that you have already indexed in Lamatic. + +The UI asks for 3 things: + +1. **Jurisdiction** so the answer is framed in the right legal system. +2. **Context** so the assistant sees the facts and procedural posture. +3. **Question** so the flow can return a research-style answer. + +Every response is framed as informational only and pushes for citations plus practical next steps. + +## Prerequisites + +- Node.js 18+ +- A Lamatic project with the bundled legal RAG flow imported and deployed +- A Lamatic-backed legal knowledge base or vector store connected to that flow + +## Environment Variables + +Create a `.env` file in this directory: + +```bash +ASSISTANT_LEGAL_CHATBOT="your-deployed-flow-id" +LAMATIC_API_URL="https://your-org.lamatic.dev" +LAMATIC_PROJECT_ID="your-project-id" +LAMATIC_API_KEY="your-api-key" +``` + +## Lamatic Setup + +Import `flows/legal-rag-chatbot/` into Lamatic Studio, connect the RAG node to your legal corpus, then deploy it and copy the deployed flow ID into `ASSISTANT_LEGAL_CHATBOT`. + +The flow is meant to sit on top of legal source material such as: + +- public statutes and regulations +- internal policy manuals +- legal knowledge bases +- case summaries or research notes + +## Run Locally + +```bash +cd kits/assistant/legal-assistant +npm install +cp .env.example .env +npm run dev +``` + +Then open `http://localhost:3000`. + +## API Route + +The frontend posts to `POST /api/legal` with: + +```json +{ + "jurisdiction": "California", + "context": "Commercial lease, 2 months unpaid rent, no prior default notices.", + "question": "What notice is typically required before termination?" +} +``` + +The route packages that into a Lamatic `chatMessage`, executes the deployed legal RAG flow, and returns: + +```json +{ + "answer": "Research-style legal response...", + "disclaimer": "Informational only, not legal advice...", + "jurisdiction": "California" +} +``` + +## Project Structure + +```text +kits/assistant/legal-assistant/ +├── app/ +│ ├── api/legal/route.ts +│ ├── layout.tsx +│ └── page.tsx +├── flows/ +│ └── legal-rag-chatbot/ +├── .env.example +├── config.json +├── package.json +└── README.md +``` + +## Important Note + +This kit is for legal research support. It should not be presented as legal advice, and the bundled UI keeps that disclaimer visible by default. diff --git a/kits/assistant/legal-assistant/app/api/legal/route.ts b/kits/assistant/legal-assistant/app/api/legal/route.ts new file mode 100644 index 00000000..e611f3df --- /dev/null +++ b/kits/assistant/legal-assistant/app/api/legal/route.ts @@ -0,0 +1,209 @@ +import { NextRequest, NextResponse } from "next/server" + +const query = ` + query ExecuteWorkflow($workflowId: String!, $chatMessage: String) { + executeWorkflow( + workflowId: $workflowId + payload: { chatMessage: $chatMessage } + ) { + status + result + } + } +` + +const DISCLAIMER = + "Informational only, not legal advice. Verify the answer against current law in your jurisdiction and consult a qualified attorney before acting." + +type LamaticIssue = { + message?: string +} + +type LamaticGraphqlResponse = { + data?: { + executeWorkflow?: { + status?: string + result?: unknown + } + } + errors?: LamaticIssue[] + message?: string +} + +function getGraphqlUrl(apiUrl: string) { + const trimmed = apiUrl.trim().replace(/\/+$/, "") + return trimmed.endsWith("/graphql") ? trimmed : `${trimmed}/graphql` +} + +function extractAnswer(result: unknown): string { + if (typeof result === "string") { + return result.trim() + } + + if (!result || typeof result !== "object") { + return "" + } + + const record = result as Record + const candidateKeys = [ + "answer", + "response", + "content", + "generatedResponse", + "modelResponse", + ] + + for (const key of candidateKeys) { + const value = record[key] + if (typeof value === "string" && value.trim()) { + return value.trim() + } + } + + return JSON.stringify(result, null, 2) +} + +export async function POST(req: NextRequest) { + try { + const body = await req.json() + const question = typeof body?.question === "string" ? body.question.trim() : "" + const jurisdiction = + typeof body?.jurisdiction === "string" ? body.jurisdiction.trim() : "" + const context = typeof body?.context === "string" ? body.context.trim() : "" + + if (!question) { + return NextResponse.json( + { error: "A legal question is required." }, + { status: 400 } + ) + } + + const lamaticApiKey = process.env.LAMATIC_API_KEY?.trim() + const lamaticApiUrl = process.env.LAMATIC_API_URL?.trim() + const lamaticProjectId = process.env.LAMATIC_PROJECT_ID?.trim() + const workflowId = process.env.ASSISTANT_LEGAL_CHATBOT?.trim() + + const missingEnv = [ + !lamaticApiKey && "LAMATIC_API_KEY", + !lamaticApiUrl && "LAMATIC_API_URL", + !lamaticProjectId && "LAMATIC_PROJECT_ID", + !workflowId && "ASSISTANT_LEGAL_CHATBOT", + ].filter(Boolean) + + if (missingEnv.length > 0) { + return NextResponse.json( + { + error: `Missing required Lamatic environment variables: ${missingEnv.join( + ", " + )}.`, + }, + { status: 500 } + ) + } + + const combinedPrompt = [ + jurisdiction ? `Jurisdiction: ${jurisdiction}` : "Jurisdiction: unspecified", + context ? `Context: ${context}` : null, + `Question: ${question}`, + "Answer for informational purposes only. Cite the relevant law, statute, regulation, or source when possible. Close with practical next steps and a short reminder that this is not legal advice.", + ] + .filter(Boolean) + .join("\n\n") + + const res = await fetch(getGraphqlUrl(lamaticApiUrl), { + method: "POST", + headers: { + Authorization: `Bearer ${lamaticApiKey}`, + "Content-Type": "application/json", + "x-project-id": lamaticProjectId, + }, + body: JSON.stringify({ + query, + variables: { + workflowId, + chatMessage: combinedPrompt, + }, + }), + signal: AbortSignal.timeout(60_000), + }) + + const raw = await res.text() + const trimmed = raw.trim() + let data: LamaticGraphqlResponse | null = null + + if (trimmed) { + try { + data = JSON.parse(trimmed) as LamaticGraphqlResponse + } catch { + data = null + } + } + + if (!res.ok) { + const upstreamMessage = + data?.errors + ?.map((error) => error?.message) + .filter(Boolean) + .join("; ") || + data?.message || + (trimmed.startsWith("<") + ? "Lamatic returned HTML instead of JSON." + : "Lamatic returned an unsuccessful response.") + + return NextResponse.json( + { error: `Lamatic request failed (${res.status}): ${upstreamMessage}` }, + { status: 502 } + ) + } + + if (!data || typeof data !== "object") { + return NextResponse.json( + { error: "Lamatic returned an invalid non-JSON response." }, + { status: 502 } + ) + } + + if (Array.isArray(data.errors) && data.errors.length > 0) { + const message = data.errors + .map((error) => error?.message) + .filter(Boolean) + .join("; ") + + return NextResponse.json( + { error: message || "Lamatic returned GraphQL errors." }, + { status: 502 } + ) + } + + const execution = data.data?.executeWorkflow + + if (execution?.status?.toLowerCase() === "error") { + return NextResponse.json( + { error: "Lamatic reported an execution error." }, + { status: 502 } + ) + } + + const answer = extractAnswer(execution?.result) + + if (!answer) { + return NextResponse.json( + { error: "Lamatic returned an empty legal response." }, + { status: 502 } + ) + } + + return NextResponse.json({ + answer, + disclaimer: DISCLAIMER, + jurisdiction: jurisdiction || "Unspecified", + }) + } catch (error) { + console.error("Legal route failed", error) + + return NextResponse.json( + { error: "The legal assistant route failed before it could complete the request." }, + { status: 500 } + ) + } +} diff --git a/kits/assistant/legal-assistant/app/globals.css b/kits/assistant/legal-assistant/app/globals.css new file mode 100644 index 00000000..10f1f3ea --- /dev/null +++ b/kits/assistant/legal-assistant/app/globals.css @@ -0,0 +1,11 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: sans-serif; + background: #fff; + color: #111; +} \ No newline at end of file diff --git a/kits/assistant/legal-assistant/app/layout.tsx b/kits/assistant/legal-assistant/app/layout.tsx new file mode 100644 index 00000000..97907f21 --- /dev/null +++ b/kits/assistant/legal-assistant/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next" +import "./globals.css" + +export const metadata: Metadata = { + title: "Legal Assistant", + description: "Lamatic-powered legal research assistant with citations, next steps, and a standing disclaimer.", +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/kits/assistant/legal-assistant/app/page.tsx b/kits/assistant/legal-assistant/app/page.tsx new file mode 100644 index 00000000..da7cc55d --- /dev/null +++ b/kits/assistant/legal-assistant/app/page.tsx @@ -0,0 +1,404 @@ +"use client" + +import { type FormEvent, useState } from "react" + +type LegalResponse = { + answer: string + disclaimer: string + jurisdiction: string +} + +type LegalApiResponse = { + error?: string + answer?: string + disclaimer?: string + jurisdiction?: string +} + +export default function LegalAssistantPage() { + const [question, setQuestion] = useState("") + const [jurisdiction, setJurisdiction] = useState("") + const [context, setContext] = useState("") + const [loading, setLoading] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState("") + + async function handleSubmit(event: FormEvent) { + event.preventDefault() + + if (!question.trim()) { + setError("Please enter a legal question before submitting.") + return + } + + setLoading(true) + setError("") + setResult(null) + + try { + const res = await fetch("/api/legal", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ question, jurisdiction, context }), + }) + + const raw = await res.text() + const trimmed = raw.trim() + let data: LegalApiResponse | null = null + + if (trimmed) { + try { + data = JSON.parse(trimmed) as LegalApiResponse + } catch { + data = null + } + } + + if (!res.ok) { + if (typeof data?.error === "string" && data.error) { + throw new Error(data.error) + } + + throw new Error(`Legal assistant request failed with status ${res.status}.`) + } + + if (!data || typeof data.answer !== "string") { + throw new Error("The legal assistant returned an invalid response.") + } + + setResult({ + answer: data.answer, + disclaimer: + typeof data.disclaimer === "string" && data.disclaimer + ? data.disclaimer + : "Informational only, not legal advice.", + jurisdiction: + typeof data.jurisdiction === "string" && data.jurisdiction + ? data.jurisdiction + : "Unspecified", + }) + } catch (requestError: unknown) { + setError( + requestError instanceof Error + ? requestError.message + : "The legal assistant request failed." + ) + } finally { + setLoading(false) + } + } + + return ( + <> +
+
+
+
+ +
+
+ Lamatic Legal Assistant +

Legal research, without pretending to be your lawyer.

+

+ Ask jurisdiction-specific questions against your Lamatic-connected + legal corpus and get an informational answer that pushes for citations, + practical next steps, and a clear non-advice disclaimer. +

+
+ + Route + /api/legal + + + Flow + RAG chat over legal materials + + + Jurisdiction + {jurisdiction.trim() || "Unspecified"} + +
+
+ +
+
+
+ Intake +

Frame the legal issue

+
+ Informational Only +
+ +
+ + +