-
Notifications
You must be signed in to change notification settings - Fork 92
feat: add customer support agent #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+25
to
+28
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden env-file ignores to prevent accidental secret commits Line 25–28 currently miss variants like Proposed fix-# local env files
-.env*.local
-.env
+# local env files
+.env*
+!.env.example📝 Committable suggestion
Suggested change
|
||||||||||||||||
| # vercel | ||||||||||||||||
| .vercel | ||||||||||||||||
|
|
||||||||||||||||
| # typescript | ||||||||||||||||
| *.tsbuildinfo | ||||||||||||||||
| next-env.d.ts | ||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1 @@ | ||||
| force=true | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Committed .npmrc files under support-triage:"
fd '^\.npmrc$' kits/automation/support-triage -x sh -c 'echo "--- $1"; cat "$1"' sh {}
echo
echo "Other explicit force usage under the kit:"
rg -n --hidden --glob '!**/node_modules/**' '\bforce\s*=\s*true\b|\bnpm\b.*\s--force\b' kits/automation/support-triage || trueRepository: Lamatic/AgentKit Length of output: 329 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Searching for .npmrc files under support-triage (including hidden files):"
fd -H '\.npmrc$' kits/automation/support-triage -x sh -c 'echo "File: $1"; cat "$1"' sh {} || echo "No .npmrc found"
echo
echo "Checking for npm force-related configurations:"
rg -H -n 'force\s*=\s*true|npm.*--force' kits/automation/support-triage --glob '!**/node_modules/**' || echo "No force configurations found"Repository: Lamatic/AgentKit Length of output: 266 🌐 Web query:
💡 Result: The force=true parameter in an .npmrc file (or --force CLI flag, or npm_config_force=true environment variable) sets npm's force configuration to true. This removes various protections against side effects, mistakes, performance issues, and malicious input. Specific behaviors include: - Allows unpublishing of entire packages (not just single versions). - Allows clobbering existing values in npm pkg set. - Implicitly sets --yes during npm init. - Allows installing packages with conflicting peerDependencies in the root project. - Allows npm audit fix to install modules outside stated dependency ranges (including SemVer-major changes). - Overrides engine-strict to allow installing packages requiring different Node.js versions. - Lifecycle script failures do not block progress. - Publishing clobbers previously published versions. - Skips cache when requesting from the registry. - Prevents checks against clobbering non-npm files. It can be set in project .npmrc, user ~/.npmrc, global config, or via CLI/env. Using force=true triggers a warning "Recommended protections disabled" on many commands, which can be suppressed with loglevel=error in .npmrc. Default is false; use cautiously as it bypasses safety checks. Citations:
Remove This project-level setting applies to all npm invocations in the kit and bypasses critical safety checks. Specifically, it allows installations with conflicting Proposed fix-force=true📝 Committable suggestion
Suggested change
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| "use server" | ||
|
|
||
| import { lamaticClient } from "@/lib/lamatic-client" | ||
| import { config } from "../orchestrate.js" | ||
|
|
||
| type SchemaToType<T> = { | ||
| [K in keyof T]: T[K] extends "string" | ||
| ? string | ||
| : T[K] extends "number" | ||
| ? number | ||
| : T[K] extends "boolean" | ||
| ? boolean | ||
| : never | ||
| } | ||
|
|
||
| type SupportTriageInput = SchemaToType<typeof config.flows.supportTriage.inputSchema> | ||
| type SupportTriageOutput = SchemaToType<typeof config.flows.supportTriage.outputSchema> | ||
|
|
||
| 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 | ||
| } | ||
|
Comment on lines
+21
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject invalid workflow output instead of silently coercing it. Lines 21-31 turn anything other than exact Suggested fix-function parseBoolean(value: unknown) {
+function parseBoolean(value: unknown): boolean | null {
if (typeof value === "boolean") {
return value
}
if (typeof value === "string") {
- return value.toLowerCase() === "true"
+ const normalized = value.trim().toLowerCase()
+ if (normalized === "true") return true
+ if (normalized === "false") return false
}
- return false
+ return null
}
@@
- return {
+ const severity = String(result.severity ?? "").toLowerCase()
+ const possibleDuplicate = parseBoolean(result.possible_duplicate)
+ const slaRisk = parseBoolean(result.sla_risk)
+
+ if (
+ !["low", "medium", "high", "critical"].includes(severity) ||
+ possibleDuplicate === null ||
+ slaRisk === null
+ ) {
+ throw new Error("Workflow returned an invalid triage payload.")
+ }
+
+ return {
success: true,
result: {
category: String(result.category ?? ""),
- severity: String(result.severity ?? ""),
+ severity,
priority_reason: String(result.priority_reason ?? ""),
- possible_duplicate: parseBoolean(result.possible_duplicate),
+ possible_duplicate: possibleDuplicate,
recommended_owner: String(result.recommended_owner ?? ""),
- sla_risk: parseBoolean(result.sla_risk),
+ sla_risk: slaRisk,
escalation_summary: String(result.escalation_summary ?? ""),
},
}Also applies to: 64-74 |
||
|
|
||
| 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<string, unknown>, | ||
| ) | ||
|
|
||
| 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)}`) | ||
|
Comment on lines
+60
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep raw Lamatic payloads out of returned errors. Line 61 embeds the whole workflow response in the thrown error, and Lines 76-90 return that message to the browser on the generic path. If Lamatic echoes ticket text or internal metadata, this leaks it directly into the UI. Throw a generic message and log only sanitized diagnostics server-side. Suggested fix- if (!result) {
- throw new Error(`No result returned from workflow. Response shape: ${JSON.stringify(response)}`)
- }
+ if (!result) {
+ console.error("Support triage workflow returned no result", {
+ workflowId: supportTriageFlow.workflowId,
+ hasResponse: Boolean(response),
+ hasRawResult: Boolean(rawResult),
+ })
+ throw new Error("Support triage workflow returned no result.")
+ }Also applies to: 76-90 |
||
| } | ||
|
|
||
| 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, | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <html lang="en"> | ||
| <body className={`font-sans antialiased`}> | ||
| {children} | ||
| <Analytics /> | ||
| </body> | ||
| </html> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix typo in documentation.
The phrase "understax nd it quickly" contains a typo and should be "understand it quickly". This makes the documentation unprofessional and harder to read.
Additionally, this file modification appears unrelated to the PR's stated purpose of adding a Support Triage Agent kit. Please verify whether this change was intentional.
📝 Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 LanguageTool
[grammar] ~15-~15: Ensure spelling is correct
Context: ..., and enough polish for someone else to understax nd it quickly. ## ✅ What a strong submiss...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)