diff --git a/CHALLENGE.md b/CHALLENGE.md index 5b1ecc3f..12f84132 100644 --- a/CHALLENGE.md +++ b/CHALLENGE.md @@ -12,7 +12,7 @@ This repository explains the challenge for internship candidates applying to Lam Candidates are asked to choose a **unique problem statement**, build a practical contribution, and share it through a pull request and a short walkthrough video. -The goal is not to make the biggest project possible. The goal is to make something focused, useful, and well explained. A strong submission shows good judgment, clear thinking, and enough polish for someone else to understand it quickly. +The goal is not to make the biggest project possible. The goal is to make something focused, useful, and well explained. A strong submission shows good judgment, clear thinking, and enough polish for someone else to understax nd it quickly. ## ✅ What a strong submission looks like diff --git a/kits/automation/support-triage/.gitignore b/kits/automation/support-triage/.gitignore new file mode 100644 index 00000000..8ccc8748 --- /dev/null +++ b/kits/automation/support-triage/.gitignore @@ -0,0 +1,34 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/kits/automation/support-triage/.npmrc b/kits/automation/support-triage/.npmrc new file mode 100644 index 00000000..5e3cde50 --- /dev/null +++ b/kits/automation/support-triage/.npmrc @@ -0,0 +1 @@ +force=true diff --git a/kits/automation/support-triage/README.md b/kits/automation/support-triage/README.md new file mode 100644 index 00000000..ab497bfd --- /dev/null +++ b/kits/automation/support-triage/README.md @@ -0,0 +1,79 @@ +# Support Triage Agent + +**Support Triage Agent** is a Lamatic-powered automation kit that helps support teams review incoming tickets faster and more consistently. It accepts raw support requests, classifies the issue, assigns severity, estimates duplicate risk, recommends the likely owner, flags SLA risk, and returns a concise escalation summary. + +## Problem It Solves + +Support teams often spend too much time manually reviewing noisy inbound tickets before any real troubleshooting begins. This kit turns one support request into a structured triage result that is easier to route, prioritize, and escalate. + +## What The Kit Returns + +For each ticket, the flow returns: + +- `category` +- `severity` +- `priority_reason` +- `possible_duplicate` +- `recommended_owner` +- `sla_risk` +- `escalation_summary` + +## Lamatic Setup + +1. Create a project in [Lamatic](https://lamatic.ai). +2. Create and deploy a flow named `support-triage`. +3. Copy the deployed flow ID and your Lamatic API credentials. +4. Export the flow into [`flows/support-triage`](/home/kumarsaurabh27d/lamatic/AgentKit/kits/automation/support-triage/flows/support-triage). + +## Environment Variables + +Create `.env.local` with: + +```env +FLOW_SUPPORT_TRIAGE="your-flow-id" +LAMATIC_API_URL="https://api.lamatic.ai/v1/..." +LAMATIC_PROJECT_ID="proj_xxxxxxxxxxxx" +LAMATIC_API_KEY="lam_xxxxxxxxxxxx" +``` + +## Local Development + +```bash +npm install +cp .env.example .env.local +npm run dev +``` + +Open `http://localhost:3000`. + +## Example Input + +```json +{ + "ticket_text": "Our enterprise team cannot access the dashboard and customers are reporting failures.", + "customer_tier": "enterprise", + "channel": "email", + "created_at": "2026-03-21T10:30:00Z", + "past_ticket_context": "Two similar dashboard timeout complaints were reported earlier today." +} +``` + +## Example Output + +```json +{ + "category": "Access / Dashboard Issue", + "severity": "high", + "priority_reason": "Enterprise customer reports a customer-facing access failure.", + "possible_duplicate": true, + "recommended_owner": "Platform Support", + "sla_risk": true, + "escalation_summary": "Enterprise customer reports dashboard access failures affecting end users. Similar timeout complaints were seen earlier today. Immediate investigation is recommended." +} +``` + +## Repository Notes + +- Main kit UI: [`app/page.tsx`](/home/kumarsaurabh27d/lamatic/AgentKit/kits/automation/support-triage/app/page.tsx) +- Server action: [`actions/orchestrate.ts`](/home/kumarsaurabh27d/lamatic/AgentKit/kits/automation/support-triage/actions/orchestrate.ts) +- Exported Lamatic flow: [`flows/support-triage/config.json`](/home/kumarsaurabh27d/lamatic/AgentKit/kits/automation/support-triage/flows/support-triage/config.json) diff --git a/kits/automation/support-triage/actions/orchestrate.ts b/kits/automation/support-triage/actions/orchestrate.ts new file mode 100644 index 00000000..cb417bec --- /dev/null +++ b/kits/automation/support-triage/actions/orchestrate.ts @@ -0,0 +1,93 @@ +"use server" + +import { lamaticClient } from "@/lib/lamatic-client" +import { config } from "../orchestrate.js" + +type SchemaToType = { + [K in keyof T]: T[K] extends "string" + ? string + : T[K] extends "number" + ? number + : T[K] extends "boolean" + ? boolean + : never +} + +type SupportTriageInput = SchemaToType +type SupportTriageOutput = SchemaToType + +const supportTriageFlow = config.flows.supportTriage + +function parseBoolean(value: unknown) { + if (typeof value === "boolean") { + return value + } + + if (typeof value === "string") { + return value.toLowerCase() === "true" + } + + return false +} + +export async function executeSupportTriage( + input: SupportTriageInput, +): Promise<{ + success: boolean + result?: SupportTriageOutput + error?: string +}> { + try { + if (!supportTriageFlow.workflowId) { + throw Error("Workflow not found in config.") + } + + const workflowInput = Object.keys(supportTriageFlow.inputSchema).reduce( + (acc, key) => { + acc[key] = input[key as keyof SupportTriageInput] + return acc + }, + {} as Record, + ) + + const response = await lamaticClient.executeFlow(supportTriageFlow.workflowId, workflowInput) + const rawResult = response?.result + const result = + rawResult?.output ?? + rawResult?.result ?? + rawResult + + if (!result) { + throw new Error(`No result returned from workflow. Response shape: ${JSON.stringify(response)}`) + } + + return { + success: true, + result: { + category: String(result.category ?? ""), + severity: String(result.severity ?? ""), + priority_reason: String(result.priority_reason ?? ""), + possible_duplicate: parseBoolean(result.possible_duplicate), + recommended_owner: String(result.recommended_owner ?? ""), + sla_risk: parseBoolean(result.sla_risk), + escalation_summary: String(result.escalation_summary ?? ""), + }, + } + } catch (error) { + let errorMessage = "Unknown error occurred" + if (error instanceof Error) { + errorMessage = error.message + if (error.message.includes("fetch failed")) { + errorMessage = + "Network error: Unable to connect to the service. Please check your internet connection and try again." + } else if (error.message.includes("API key")) { + errorMessage = "Authentication error: Please check your API configuration." + } + } + + return { + success: false, + error: errorMessage, + } + } +} diff --git a/kits/automation/support-triage/app/globals.css b/kits/automation/support-triage/app/globals.css new file mode 100644 index 00000000..dc2aea17 --- /dev/null +++ b/kits/automation/support-triage/app/globals.css @@ -0,0 +1,125 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: 'Geist', 'Geist Fallback'; + --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/kits/automation/support-triage/app/layout.tsx b/kits/automation/support-triage/app/layout.tsx new file mode 100644 index 00000000..4489638d --- /dev/null +++ b/kits/automation/support-triage/app/layout.tsx @@ -0,0 +1,45 @@ +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const _geist = Geist({ subsets: ["latin"] }); +const _geistMono = Geist_Mono({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: 'v0 App', + description: 'Created with v0', + generator: 'v0.app', + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/kits/automation/support-triage/app/page.tsx b/kits/automation/support-triage/app/page.tsx new file mode 100644 index 00000000..7502b84a --- /dev/null +++ b/kits/automation/support-triage/app/page.tsx @@ -0,0 +1,284 @@ +"use client" + +import type React from "react" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Card } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Loader2, Copy, Check, RotateCcw } from "lucide-react" +import { executeSupportTriage } from "@/actions/orchestrate" +import { Header } from "@/components/header" + +type TriageResult = { + category: string + severity: string + priority_reason: string + possible_duplicate: boolean + recommended_owner: string + sla_risk: boolean + escalation_summary: string +} + +const defaultForm = { + ticket_text: "", + customer_tier: "enterprise", + channel: "email", + created_at: "", + past_ticket_context: "", +} + +export default function SupportTriagePage() { + const [form, setForm] = useState(defaultForm) + const [isLoading, setIsLoading] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState("") + const [copied, setCopied] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!form.ticket_text.trim()) { + setError("Please provide a support ticket description.") + return + } + + setIsLoading(true) + setError("") + setResult(null) + setCopied(false) + + try { + const response = await executeSupportTriage({ + ...form, + created_at: form.created_at || new Date().toISOString(), + }) + + if (response.success) { + setResult(response.result ?? null) + } else { + setError(response.error || "Triage failed") + } + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred") + } finally { + setIsLoading(false) + } + } + + const handleReset = () => { + setResult(null) + setForm(defaultForm) + setError("") + setCopied(false) + } + + const handleCopy = async () => { + const textToCopy = JSON.stringify(result, null, 2) + + try { + await navigator.clipboard.writeText(textToCopy) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error("Failed to copy:", err) + } + } + + const setField = (field: keyof typeof defaultForm, value: string) => { + setForm((current) => ({ ...current, [field]: value })) + } + + return ( +
+
+ +
+
+
+
+

+ Support +
+ Triage Agent +

+

+ Turn raw support requests into a structured triage result with severity, owner guidance, duplicate + hints, and an escalation summary. +

+
+ + +
+
+ +