Build an AI App

Structured Extraction

Run the dev server and navigate to the /extraction route:

pnpm run dev

You should see an input field where you can enter unstructured text about an appointment. Let's build a system to extract structured information from this input using the generateObject function.

Create a new file called actions.ts in the extraction directory. In it, define a new server action called extractAppointment. This action will take in one argument, input, which is a string. Import and call generateObject and return the resulting object generation.

app/(4-extraction)/extraction/actions.ts
"use server";
 
import { generateObject } from "ai";
 
export const extractAppointment = async (input: string) => {
  const result = await generateObject();
  return result.object;
};

Pass in a model and a prompt. In this case, we want the model to extract appointment information from the input text. We'll use the gpt-4o-mini model for this task.

app/(4-extraction)/extraction/actions.ts
"use server";
 
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
 
export const extractAppointment = async (input: string) => {
  const result = await generateObject({
    model: openai("gpt-4o-mini"), 
    prompt: "Extract appointment info for the following input: " + input, 
  });
  return result.object;
};

Now, let's define a schema for the exact information we want to extract.

app/(4-extraction)/extraction/actions.ts
"use server";
 
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
 
export const extractAppointment = async (input: string) => {
  const result = await generateObject({
    model: openai("gpt-4o-mini"),
    prompt: "Extract appointment info for the following input: " + input,
    schema: z.object({ 
      title: z.string(), 
      startTime: z.string().nullable(), 
      endTime: z.string().nullable(), 
      attendees: z.array(z.string()).nullable(), 
      location: z.string().nullable(), 
      date: z.string(), 
    }), 
  });
  return result.object;
};

Import the CalendarAppointment type and create a new state variable to store the extracted appointment details.

app/(4-extraction)/extraction/page.tsx
"use client";
 
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { AppointmentDetails, CalendarAppointment } from "./calendar-appointment"; 
 
export default function Page() {
  const [loading, setLoading] = useState(false);
  const [appointment, setAppointment] =
    useState<AppointmentDetails | null>(null);  
 
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    // extract appointment
    setLoading(false);
  };
 
  return (
    <div className="max-w-lg mx-auto px-4 py-8">
      <div className="flex flex-col gap-6">
        <Card>
          <CardHeader>
            <CardTitle>Extract Appointment</CardTitle>
          </CardHeader>
          <CardContent>
            <form onSubmit={handleSubmit} className="space-y-4">
              <Input
                name="appointment"
                placeholder="Enter appointment details..."
                className="w-full"
              />
              <Button type="submit" className="w-full" disabled={loading}>
                {loading ? "Extracting..." : "Extract Appointment"}
              </Button>
            </form>
          </CardContent>
        </Card>
        <CalendarAppointment appointment={null} />
      </div>
    </div>
  );
}

Import the newly created extractAppointment action and call it. Pass in the input value from the form and update the extracted appointment state with the awaited resulting value.

app/(4-extraction)/extraction/page.tsx
"use client";
 
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { AppointmentDetails, CalendarAppointment } from "./calendar-appointment";
import { extractAppointment } from "./actions";
 
export default function Page() {
  const [loading, setLoading] = useState(false);
  const [appointment, setAppointment] =
    useState<AppointmentDetails | null>(null);
 
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    const formData = new FormData(e.target as HTMLFormElement); 
    const input = formData.get("appointment") as string; 
    setAppointment(await extractAppointment(input)); 
    setLoading(false);
  };
 
  return (
    <div className="max-w-lg mx-auto px-4 py-8">
      <div className="flex flex-col gap-6">
        <Card>
          <CardHeader>
            <CardTitle>Extract Appointment</CardTitle>
          </CardHeader>
          <CardContent>
            <form onSubmit={handleSubmit} className="space-y-4">
              <Input
                name="appointment"
                placeholder="Enter appointment details..."
                className="w-full"
              />
              <Button type="submit" className="w-full" disabled={loading}>
                {loading ? "Extracting..." : "Extract Appointment"}
              </Button>
            </form>
          </CardContent>
        </Card>
        <CalendarAppointment appointment={null} />
      </div>
    </div>
  );
}

Pass the appointment to the CalendarAppointment component as props.

app/(4-extraction)/extraction/page.tsx
"use client";
 
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { AppointmentDetails, CalendarAppointment } from "./calendar-appointment";
import { extractAppointment } from "./actions";
 
export default function Page() {
  const [loading, setLoading] = useState(false);
  const [appointment, setAppointment] =
    useState<AppointmentDetails | null>(null);
 
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    const formData = new FormData(e.target as HTMLFormElement);
    const input = formData.get("appointment") as string;
    setAppointment(await extractAppointment(input));
    setLoading(false);
  };
 
  return (
    <div className="max-w-lg mx-auto px-4 py-8">
      <div className="flex flex-col gap-6">
        <Card>
          <CardHeader>
            <CardTitle>Extract Appointment</CardTitle>
          </CardHeader>
          <CardContent>
            <form onSubmit={handleSubmit} className="space-y-4">
              <Input
                name="appointment"
                placeholder="Enter appointment details..."
                className="w-full"
              />
              <Button type="submit" className="w-full" disabled={loading}>
                {loading ? "Extracting..." : "Extract Appointment"}
              </Button>
            </form>
          </CardContent>
        </Card>
        <CalendarAppointment appointment={appointment} />
      </div>
    </div>
  );
}

Test the extraction functionality:

  1. Run the dev server if it's not already running:
    pnpm run dev
  2. Navigate to the /extraction route in your browser.
  3. Enter an unstructured appointment text in the input field, such as:
Meeting with Guillermo Rauch about Next Conf Keynote Practice tomorrow at 2pm at Vercel HQ
  1. Click the "Submit" button and observe the structured appointment details being displayed.

You should now see the extracted appointment information rendered in a structured format using the CalendarAppointment component.

But notice that the date and time format isn't great. And the appointment name isn't perfect either. Let's add some Zod descriptions to clarify the expected format of the extracted fields.

Update the action.ts file with descriptions for the extracted fields:

app/(4-extraction)/extraction/actions.ts
"use server";
 
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
 
export const extractAppointment = async (input: string) => {
  const result = await generateObject({
    model: openai("gpt-4o-mini"),
    prompt: "Extract appointment info for the following input: " + input,
    schema: z.object({
      title: z.string().describe("The title of the event. This should be the main purpose of the event. No need to mention names. Clean up formatting (capitalise)."), 
      startTime: z.string().nullable().describe("format HH:MM"), 
      endTime: z.string().nullable().describe("format HH:MM - note: default meeting duration is 1 hour"), 
      attendees: z.array(z.string()).nullable().describe("comma separated list of attendees"), 
      location: z.string().nullable(),
      date: z.string().describe("Today's date is: " + new Date().toISOString().split("T")[0]), 
    }),
  });
  return result.object;
};

Try the following input again and see the improvement in the output.

Meeting with Guillermo Rauch about Next Conf Keynote Practice tomorrow at 2pm at Vercel HQ