# Project export: Compass: Your Personal Caseworker

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: Making it easy to navigate and maximize access to government services through conversational and task-performing agents. Compass remembers your information to easily apply for benefits across domains.
- Devpost: https://devpost.com/software/compass-xiatk7
- GitHub: https://github.com/martinezdavid12/2025_ai_hacks
- Demo: https://2025-ai-hacks.vercel.app/
- Video: https://www.youtube.com/embed/hhssCFUa30c?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — David Martinez (8 commits)

## Devpost submission (written by the team)

### Inspiration

Millions of Californians (and ~40 million Americans nationwide) qualify for aid yet never finish the paperwork. Surveys show 47% find government applications “overly complex,” and >$140 B in benefits go unclaimed every year.¹ We watched friends juggle five portals, print 30-page PDFs, and re-type the same SSN half a dozen times. Compass was born to shrink that gap: one friendly conversation that figures out every program you qualify for and hands you finished applications—no clipboard, no jargon, no wait-list. What It Does Compass is a voice-first digital case-worker for public benefits (CalFresh, Medi-Cal, Alameda County IHSS today). Right now, we focus on public benefits assistance programs for those in need, helping low-income families, the elderly, college students, and more figure out which assistance programs they qualify for and quickly complete all of the applications at once. Conversational Agent (Vapi + Claude 4) Greets the user, gains consent, and runs an adaptive screening interview. Memory & Eligibility Agent (Letta) Stores answers, matches them against a JSON rule catalog, returns eligible programs and any still-missing fields. Creative Use - Letta replaces Redis/Postgres, acting both as the long-term memory layer and short-term retrieval layer per person, updated automatically through conversation history and without schemas Document-Filling Agent (Claude 4 PDF prompt) Maps normalized field names → PDF / HTML IDs, fills every field (checkboxes included), and returns ready-to-submit files. Result: Speak once → receive fully populated PDFs with zero duplicate questions even when multiple programs need the same data. How We Built It Vapi orchestrates the conversation flow with multi-step nodes powered by Anthropic's Claude 4 LLMs (Anthropic's Claude Opus 4) handle natural language + validation and guide Q&A; system prompts enforce accessible, welcoming tone. Letta parses the catalog’s eligibility requirements to return matching programs, determines what information we still need to complete relevant applications, and stores memory about the user. Form filler uses Claude Opus 4 to automatically fill out PDF forms, matching user data to text and checkbox inputs. Front-end Built in Next.js, using Vapi for real-time speech transcription. Dashboards for easy access and navigation. We built everything in less than 24 hours!! 🏃💨 Challenges We Faced Mapping 50+ form fields to conversation-friendly variable names without losing fidelity and without duplicating questions to the user. Validating ambiguous answers (“uh… maybe monthly?”) and re-asking the user politely. Cross-agent memory sync between Vapi and Letta. Checkbox hell: we spent two hours getting our form-filler to accurately interact with checkboxes! Rate-limits and exhausted credits at 4 am, forcing us to come up with creative ways to keep moving. Accomplishments We’re Proud Of End-to-end demo: speak to Compass → instant, fully populated CalFresh PDF. Zero duplicate questions, even when multiple programs need the same data. Plug-and-play program catalogs (including program name, description, eligibility requirements, and application requirements) are stored in a JSON file after LLM processing/information extraction, so adding new programs is a single file change. Accessibility: works over a low-data phone call or web interface. What We Learned Prompt design for structured extraction is half the battle, but still is super important for guiding LLM-enabled functionality. Users love hearing “let me fill that out for you” instead of “go to this portal”. PDFs are relics, but they’re not going away soon in government. Automation bridges this gap! What’s Next Expand catalog to more assistance programs housing vouchers, LIHEAP, child-care subsidies, to more geographies (counties/states), to more domains (construction permitting, procurement contracting, etc.) Multilingual flows with automatic language detection to expand language access, particularly for immigrants where English may not be their first language SMS fallback for alternate ways to interact with the agent. Document upload & OCR to attach pay stubs or IDs. Caseworker dashboard so humans (whether a caseworker, secretary, or college student) can more easily override or approve submissions.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 67 recognized source files, 186 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Python (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (74 of 74)

```
.DS_Store
.gitignore
app/.DS_Store
app/api/analyze-pdf/route.ts
app/api/chat/route.ts
app/api/vapi-speak/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
components.json
components/form-filler-app.tsx
components/form-state-panel.tsx
components/PdfPreview.tsx
components/theme-provider.tsx
components/ui/accordion.tsx
components/ui/alert-dialog.tsx
components/ui/alert.tsx
components/ui/aspect-ratio.tsx
components/ui/avatar.tsx
components/ui/badge.tsx
components/ui/breadcrumb.tsx
components/ui/button.tsx
components/ui/calendar.tsx
components/ui/card.tsx
components/ui/carousel.tsx
components/ui/chart.tsx
components/ui/checkbox.tsx
components/ui/collapsible.tsx
components/ui/command.tsx
components/ui/context-menu.tsx
components/ui/dialog.tsx
components/ui/drawer.tsx
components/ui/dropdown-menu.tsx
components/ui/form.tsx
components/ui/hover-card.tsx
components/ui/input-otp.tsx
components/ui/input.tsx
components/ui/label.tsx
components/ui/menubar.tsx
components/ui/navigation-menu.tsx
components/ui/pagination.tsx
components/ui/popover.tsx
components/ui/progress.tsx
components/ui/radio-group.tsx
components/ui/resizable.tsx
components/ui/scroll-area.tsx
components/ui/select.tsx
components/ui/separator.tsx
components/ui/sheet.tsx
components/ui/sidebar.tsx
components/ui/skeleton.tsx
components/ui/slider.tsx
components/ui/sonner.tsx
components/ui/switch.tsx
components/ui/table.tsx
components/ui/tabs.tsx
components/ui/textarea.tsx
components/ui/toast.tsx
components/ui/toaster.tsx
components/ui/toggle-group.tsx
components/ui/toggle.tsx
components/ui/tooltip.tsx
components/ui/use-mobile.tsx
components/ui/use-toast.ts
hooks/use-mobile.tsx
hooks/use-toast.ts
lib/forms.ts
lib/utils.ts
next.config.mjs
package.json
postcss.config.mjs
styles/globals.css
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @hookform/resolvers@^3.9.1, @letta-ai/vercel-ai-sdk-provider@latest, @radix-ui/react-accordion@1.2.2, @radix-ui/react-alert-dialog@1.1.4, @radix-ui/react-aspect-ratio@1.1.1, @radix-ui/react-avatar@1.1.2, @radix-ui/react-checkbox@1.1.3, @radix-ui/react-collapsible@1.1.2, @radix-ui/react-context-menu@2.2.4, @radix-ui/react-dialog@1.1.4, @radix-ui/react-dropdown-menu@2.1.4, @radix-ui/react-hover-card@1.1.4, @radix-ui/react-label@2.1.1, @radix-ui/react-menubar@1.1.4, @radix-ui/react-navigation-menu@1.2.3, @radix-ui/react-popover@1.1.4, @radix-ui/react-progress@1.1.1, @radix-ui/react-radio-group@1.2.2, @radix-ui/react-scroll-area@1.2.2, @radix-ui/react-select@2.1.4, @radix-ui/react-separator@1.1.1, @radix-ui/react-slider@1.2.2, @radix-ui/react-slot@1.1.1, @radix-ui/react-switch@1.1.2, @radix-ui/react-tabs@1.1.2, @radix-ui/react-toast@1.2.4, @radix-ui/react-toggle@1.1.1, @radix-ui/react-toggle-group@1.1.1, @radix-ui/react-tooltip@1.1.6, @types/node@^22, @types/react@^19, @types/react-dom@^19, @vapi-ai/web@^2.3.6, ai@latest, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@1.0.4, date-fns@4.1.0, embla-carousel-react@8.5.1, input-otp@1.4.1, lucide-react@^0.454.0, next@15.2.4, next-themes@^0.4.4, pdf-lib@^1.17.1, pdfjs-dist@^5.3.31, postcss@^8.5, react@^19, react-day-picker@8.10.1, react-dom@^19, react-hook-form@^7.54.1, react-markdown@latest, react-resizable-panels@^2.1.7, recharts@2.15.0, sonner@^1.7.1, tailwind-merge@^2.5.5, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, typescript@^5, uuid@^11.1.0, vaul@^0.9.6, zod@^3.24.1

### Recent commits (newest first)

- done perhaps
- asda
- asdasd
- asdasdas
- added pdf preview
- asda
- updated pdf thing
- inital commit

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "my-v0-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "lint": "next lint",
    "start": "next start"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.9.1",
    "@letta-ai/vercel-ai-sdk-provider": "latest",
    "@radix-ui/react-accordion": "1.2.2",
    "@radix-ui/react-alert-dialog": "1.1.4",
    "@radix-ui/react-aspect-ratio": "1.1.1",
    "@radix-ui/react-avatar": "1.1.2",
    "@radix-ui/react-checkbox": "1.1.3",
    "@radix-ui/react-collapsible": "1.1.2",
    "@radix-ui/react-context-menu": "2.2.4",
    "@radix-ui/react-dialog": "1.1.4",
    "@radix-ui/react-dropdown-menu": "2.1.4",
    "@radix-ui/react-hover-card": "1.1.4",
    "@radix-ui/react-label": "2.1.1",
    "@radix-ui/react-menubar": "1.1.4",
    "@radix-ui/react-navigation-menu": "1.2.3",
    "@radix-ui/react-popover": "1.1.4",
    "@radix-ui/react-progress": "1.1.1",
    "@radix-ui/react-radio-group": "1.2.2",
    "@radix-ui/react-scroll-area": "1.2.2",
    "@radix-ui/react-select": "2.1.4",
    "@radix-ui/react-separator": "1.1.1",
    "@radix-ui/react-slider": "1.2.2",
    "@radix-ui/react-slot": "1.1.1",
    "@radix-ui/react-switch": "1.1.2",
    "@radix-ui/react-tabs": "1.1.2",
    "@radix-ui/react-toast": "1.2.4",
    "@radix-ui/react-toggle": "1.1.1",
    "@radix-ui/react-toggle-group": "1.1.1",
    "@radix-ui/react-tooltip": "1.1.6",
    "@vapi-ai/web": "^2.3.6",
    "ai": "latest",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.0.4",
    "date-fns": "4.1.0",
    "embla-carousel-react": "8.5.1",
    "input-otp": "1.4.1",
    "lucide-react": "^0.454.0",
    "next": "15.2.4",
    "next-themes": "^0.4.4",
    "pdf-lib": "^1.17.1",
    "pdfjs-dist": "^5.3.31",
    "react": "^19",
    "react-day-picker": "8.10.1",
    "react-dom": "^19",
    "react-hook-form": "^7.54.1",
    "react-markdown": "latest",
    "react-resizable-panels": "^2.1.7",
    "recharts": "2.15.0",
    "sonner": "^1.7.1",
    "tailwind-merge": "^2.5.5",
    "tailwindcss-animate": "^1.0.7",
    "uuid": "^11.1.0",
    "vaul": "^0.9.6",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^22",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "postcss": "^8.5",
    "tailwindcss": "^3.4.17",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'AssistAgent',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

```

### app/page.tsx

```typescript
import FormFillerApp from "@/components/form-filler-app"

export default function HomePage() {
  return (
    // Added padding to the main container for better spacing from viewport edges
    <main className="h-screen w-full bg-background p-4 md:p-6 lg:p-8">
      <FormFillerApp />
    </main>
  )
}

```

### app/api/vapi-speak/route.ts

```typescript
import { NextResponse } from "next/server"

export async function POST(req: Request) {
  const { message } = await req.json()

  const vapiRes = await fetch("https://api.vapi.ai/v1/tts/speak", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VAPI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      voice: "ava", // or any supported voice like "charlie", "maya", etc.
      text: message,
    }),
  })

  if (!vapiRes.ok) {
    const errorText = await vapiRes.text()
    console.error("VAPI error:", errorText)
    return NextResponse.json({ error: "VAPI request failed" }, { status: 500 })
  }

  return NextResponse.json({ status: "ok" })
}

```

### app/api/chat/route.ts

```typescript
// No changes needed from the version that correctly parses the Vercel AI SDK stream.
// Ensure this file exists and is correctly implemented as in previous steps.
import { lettaCloud } from "@letta-ai/vercel-ai-sdk-provider"
import { streamText } from "ai"

// export const runtime = "edge" // Optional

export async function POST(req: Request) {
  try {
    const { prompt } = (await req.json()) as { prompt: string }

    if (!process.env.LETTA_AGENT_ID_SOCIAL_AGENT) {
      throw new Error("LETTA_AGENT_ID_SOCIAL_AGENT is not set.")
    }
    // lettaCloud uses LETTA_API_KEY from environment by default
    const result = await streamText({
      model: lettaCloud(process.env.LETTA_AGENT_ID_SOCIAL_AGENT),
      prompt: prompt,
    })
    return result.toDataStreamResponse()
  } catch (error: any) {
    console.error("[Letta Chat API Error]", error)
    return new Response(JSON.stringify({ error: error.message || "Failed to process chat message." }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    })
  }
}

```

### app/api/analyze-pdf/route.ts

```typescript
import { lettaCloud } from "@letta-ai/vercel-ai-sdk-provider"
import { PDFDocument } from "pdf-lib"

export const config = {
  runtime: "nodejs",
}

export async function POST(req: Request) {
  try {
    const formData = await req.formData()
    const pdfFile = formData.get("pdf") as File | null
    const prompt = formData.get("prompt") as string | null

    if (!pdfFile) {
      return new Response(JSON.stringify({ error: "No PDF file provided." }), { status: 400 })
    }

    if (!prompt) {
      return new Response(JSON.stringify({ error: "No prompt provided with PDF." }), { status: 400 })
    }

    const agentId = process.env.LETTA_AGENT_ID_SOCIAL_AGENT
    if (!agentId) {
      throw new Error("LETTA_AGENT_ID_SOCIAL_AGENT is not set.")
    }

    const fileBuffer = await pdfFile.arrayBuffer()
    let fields: { name: string; type: string }[] = []
    let textNote = ""

    try {
      const pdfDoc = await PDFDocument.load(fileBuffer)

      try {
        const form = pdfDoc.getForm()
        fields = form.getFields().map((field) => ({
          name: field.getName(),
          type: field.constructor.name,
        }))
      } catch {
        console.warn("[Letta PDF] No AcroForm fields found in PDF.")
      }

      // Note: pdf-lib cannot extract page text; add a placeholder if needed.
      textNote = "(Note: Full text extraction is not available in this mode. Only form fields are accessible.)"
    } catch (err) {
      console.warn("[Letta PDF] Failed to parse PDF:", err)
    }

    const messagesForLetta = [
      {
        role: "user",
        content: `${prompt}\n\n${textNote}`,
      },
    ]

    const stream = await lettaCloud.client.agents.messages.createStream(agentId, {
      messages: messagesForLetta,
      streamTokens: true,
    })

    const fieldInfoPrefix = `@@PDF_FIELDS:${JSON.stringify(fields)}\n`

    const readableStream = new ReadableStream({
      async start(controller) {
        controller.enqueue(new TextEncoder().encode(fieldInfoPrefix))

        for await (const chunk of stream) {
          if (chunk.messageType === "assistant_message" && chunk.content) {
            controller.enqueue(new TextEncoder().encode(chunk.content))
          }
        }

        controller.close()
      },
    })

    return new Response(readableStream, {
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        "Transfer-Encoding": "chunked",
      },
    })
  } catch (error: any) {
    console.error("[Letta PDF API Error]", error)
    return new Response(JSON.stringify({ error: error.message || "Failed to process PDF." }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    })
  }
}

```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss";

const config: Config = {
    darkMode: ["class"],
    content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "*.{js,ts,jsx,tsx,mdx}"
  ],
  theme: {
  	extend: {
  		colors: {
  			background: 'hsl(var(--background))',
  			foreground: 'hsl(var(--foreground))',
  			card: {
  				DEFAULT: 'hsl(var(--card))',
  				foreground: 'hsl(var(--card-foreground))'
  			},
  			popover: {
  				DEFAULT: 'hsl(var(--popover))',
  				foreground: 'hsl(var(--popover-foreground))'
  			},
  			primary: {
  				DEFAULT: 'hsl(var(--primary))',
  				foreground: 'hsl(var(--primary-foreground))'
  			},
  			secondary: {
  				DEFAULT: 'hsl(var(--secondary))',
  				foreground: 'hsl(var(--secondary-foreground))'
  			},
  			muted: {
  				DEFAULT: 'hsl(var(--muted))',
  				foreground: 'hsl(var(--muted-foreground))'
  			},
  			accent: {
  				DEFAULT: 'hsl(var(--accent))',
  				foreground: 'hsl(var(--accent-foreground))'
  			},
  			destructive: {
  				DEFAULT: 'hsl(var(--destructive))',
  				foreground: 'hsl(var(--destructive-foreground))'
  			},
  			border: 'hsl(var(--border))',
  			input: 'hsl(var(--input))',
  			ring: 'hsl(var(--ring))',
  			chart: {
  				'1': 'hsl(var(--chart-1))',
  				'2': 'hsl(var(--chart-2))',
  				'3': 'hsl(var(--chart-3))',
  				'4': 'hsl(var(--chart-4))',
  				'5': 'hsl(var(--chart-5))'
  			},
  			sidebar: {
  				DEFAULT: 'hsl(var(--sidebar-background))',
  				foreground: 'hsl(var(--sidebar-foreground))',
  				primary: 'hsl(var(--sidebar-primary))',
  				'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
  				accent: 'hsl(var(--sidebar-accent))',
  				'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
  				border: 'hsl(var(--sidebar-border))',
  				ring: 'hsl(var(--sidebar-ring))'
  			}
  		},
  		borderRadius: {
  			lg: 'var(--radius)',
  			md: 'calc(var(--radius) - 2px)',
  			sm: 'calc(var(--radius) - 4px)'
  		},
  		keyframes: {
  			'accordion-down': {
  				from: {
  					height: '0'
  				},
  				to: {
  					height: 'var(--radix-accordion-content-height)'
  				}
  			},
  			'accordion-up': {
  				from: {
  					height: 'var(--radix-accordion-content-height)'
  				},
  				to: {
  					height: '0'
  				}
  			}
  		},
  		animation: {
  			'accordion-down': 'accordion-down 0.2s ease-out',
  			'accordion-up': 'accordion-up 0.2s ease-out'
  		}
  	}
  },
  plugins: [require("tailwindcss-animate")],
};
export default config;

```

### lib/utils.ts

```typescript
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

```

### components/theme-provider.tsx

```typescript
'use client'

import * as React from 'react'
import {
  ThemeProvider as NextThemesProvider,
  type ThemeProviderProps,
} from 'next-themes'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

```

### hooks/use-mobile.tsx

```typescript
import * as React from "react"

const MOBILE_BREAKPOINT = 768

export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return !!isMobile
}

```

[57 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]