Merge branch 'additional-ai-tools'
This commit is contained in:
@@ -64,11 +64,14 @@ export async function scheduleMeeting({
|
||||
sendNotifications: true
|
||||
})
|
||||
|
||||
const inviteLink = event.data.htmlLink ?? undefined
|
||||
|
||||
return {
|
||||
success: true,
|
||||
eventId: event.data.id,
|
||||
htmlLink: event.data.htmlLink,
|
||||
message: `Meeting "${title}" scheduled for ${startTime.toLocaleString()}${visitorEmail ? `. Invite sent to ${visitorEmail}.` : '.'}`,
|
||||
htmlLink: inviteLink,
|
||||
inviteLink,
|
||||
message: `Meeting "${title}" scheduled for ${startTime.toLocaleString()}${visitorEmail ? `. Invite sent to ${visitorEmail}.` : '.'}${inviteLink ? ` Calendar invite: ${inviteLink}` : ''}`,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to schedule meeting:', error)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
||||
import { success, z } from 'zod'
|
||||
import { streamText, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { env } from '~/env'
|
||||
import { db } from '~/server/db'
|
||||
import { chatSession, chatMessage } from '~/server/dbschema/schema'
|
||||
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||
import { scheduleMeeting } from '~/app/actions/scheduleMeeting'
|
||||
import currentTime from '~/app/actions/currentTime';
|
||||
import { createChatTools } from '~/server/ai/tools'
|
||||
|
||||
const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY })
|
||||
|
||||
@@ -31,7 +29,15 @@ export async function POST(req: Request) {
|
||||
|
||||
if (!session) return new Response('Session not found', { status: 404 })
|
||||
|
||||
const systemPrompt = await servTrpc.chat.getSystemPrompt() || 'You are an AI recruiter assistant.'
|
||||
const configuredSystemPrompt = await servTrpc.chat.getSystemPrompt() || 'You are an AI recruiter assistant.'
|
||||
const systemPrompt = `${configuredSystemPrompt}
|
||||
|
||||
Runtime context:
|
||||
- Current server time: ${new Date().toISOString()}.
|
||||
- Default meeting timezone: Europe/Berlin.
|
||||
- For availability questions like "next open spot", call getAvailability once. It defaults to checking from now. Use nextAvailableSlot for the next opening, or the first item in availableSlots if needed. Do not call getAvailability again just to get more slots.
|
||||
- After scheduleMeeting succeeds, include the returned inviteLink or htmlLink in your response.
|
||||
- Do not calculate or invent calendar availability yourself.`
|
||||
const model = await servTrpc.chat.getModel()
|
||||
|
||||
// Save the latest user message
|
||||
@@ -50,42 +56,14 @@ export async function POST(req: Request) {
|
||||
model: openai(model),
|
||||
system: systemPrompt,
|
||||
messages: await convertToModelMessages(messages),
|
||||
tools: {
|
||||
scheduleMeeting: tool({
|
||||
description: 'Schedule a meeting with Gregor Lohaus and add it to his Google Calendar',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('Meeting title, make something up if not provided'),
|
||||
description: z.string().describe('Meeting description / agenda, make something up if not provided'),
|
||||
dateTime: z
|
||||
.string()
|
||||
.describe(
|
||||
'ISO 8601 datetime for the meeting start, e.g. 2025-04-01T10:00:00',
|
||||
),
|
||||
durationMinutes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(15)
|
||||
.max(120)
|
||||
.describe('Duration of the meeting in minutes, if none provided ask if 20 minutes is ok'),
|
||||
attendeeEmail: z
|
||||
.string()
|
||||
.email()
|
||||
.optional()
|
||||
.describe('Optional Email of the visitor to invite (if provided)'),
|
||||
attendeeName: z.string().optional().describe('Name of the visitor'),
|
||||
}),
|
||||
execute: async (input) => scheduleMeeting({ ...input }),
|
||||
}),
|
||||
getCurrentUnixTime: tool({
|
||||
description: 'Get the current unix time to reference for meeting dates',
|
||||
inputSchema: z.object({
|
||||
none: z.string().optional().describe("no inputs are needed")
|
||||
}),
|
||||
execute: async () => currentTime()
|
||||
})
|
||||
},
|
||||
stopWhen: stepCountIs(5),
|
||||
tools: createChatTools(),
|
||||
stopWhen: stepCountIs(2),
|
||||
onFinish: async ({ text, finishReason }) => {
|
||||
console.log('[ai:chat:onFinish]', {
|
||||
finishReason,
|
||||
hasText: Boolean(text),
|
||||
textLength: text.length,
|
||||
})
|
||||
if (text && finishReason === 'stop') {
|
||||
await db.insert(chatMessage).values({
|
||||
sessionId,
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import { ClientMdx } from "~/components/ClientMdx";
|
||||
|
||||
function toolLabel(type: string) {
|
||||
switch (type) {
|
||||
case "tool-searchSiteContent":
|
||||
return "Searching site content";
|
||||
case "tool-getRelevantExperience":
|
||||
return "Finding relevant experience";
|
||||
case "tool-getProjectDetails":
|
||||
return "Loading project details";
|
||||
case "tool-getAvailability":
|
||||
return "Checking availability";
|
||||
case "tool-getCurrentUnixTime":
|
||||
return "Checking current time";
|
||||
default:
|
||||
return "Using tool";
|
||||
}
|
||||
}
|
||||
|
||||
export const AssistantMessage = (props: { message: UIMessage }) => {
|
||||
let message = props.message;
|
||||
return (
|
||||
@@ -19,12 +36,12 @@ export const AssistantMessage = (props: { message: UIMessage }) => {
|
||||
)
|
||||
}
|
||||
if (part.type === 'tool-scheduleMeeting') {
|
||||
const toolPart = part as unknown as {
|
||||
type: 'tool-scheduleMeeting'
|
||||
state: string
|
||||
input: unknown
|
||||
output?: { success: boolean; message?: string; htmlLink?: string; error?: string }
|
||||
}
|
||||
const toolPart = part as unknown as {
|
||||
type: 'tool-scheduleMeeting'
|
||||
state: string
|
||||
input: unknown
|
||||
output?: { success: boolean; message?: string; htmlLink?: string; inviteLink?: string; error?: string }
|
||||
}
|
||||
if (toolPart.state === 'input-available' || toolPart.state === 'input-streaming') {
|
||||
return (
|
||||
<p key={i} className="text-xs opacity-70 italic">
|
||||
@@ -32,33 +49,62 @@ export const AssistantMessage = (props: { message: UIMessage }) => {
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (toolPart.state === 'output-available' && toolPart.output) {
|
||||
const result = toolPart.output
|
||||
return (
|
||||
<div key={i} className="text-xs mt-1 p-2 bg-background/20 rounded">
|
||||
{result.success ? (
|
||||
<span>
|
||||
✓ {result.message}{' '}
|
||||
{result.htmlLink && (
|
||||
<a
|
||||
href={result.htmlLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
View event
|
||||
</a>
|
||||
)}
|
||||
if (toolPart.state === 'output-available' && toolPart.output) {
|
||||
const result = toolPart.output
|
||||
const inviteLink = result.inviteLink ?? result.htmlLink
|
||||
return (
|
||||
<div key={i} className="text-xs mt-1 p-2 bg-background/20 rounded">
|
||||
{result.success ? (
|
||||
<span>
|
||||
✓ {result.message}{' '}
|
||||
{inviteLink && (
|
||||
<a
|
||||
href={inviteLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Open calendar invite
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span>✗ {result.error}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (part.type.startsWith('tool-')) {
|
||||
const toolPart = part as unknown as {
|
||||
type: string
|
||||
state: string
|
||||
output?: { success?: boolean; results?: unknown[]; matches?: unknown[]; availableSlots?: unknown[]; project?: unknown; error?: string }
|
||||
}
|
||||
if (toolPart.state === 'input-available' || toolPart.state === 'input-streaming') {
|
||||
return (
|
||||
<p key={i} className="text-xs opacity-70 italic">
|
||||
{toolLabel(toolPart.type)}...
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (toolPart.state === 'output-available') {
|
||||
const count = toolPart.output?.results?.length
|
||||
?? toolPart.output?.matches?.length
|
||||
?? toolPart.output?.availableSlots?.length
|
||||
return (
|
||||
<p key={i} className="text-xs opacity-70 italic">
|
||||
{toolPart.output?.success === false
|
||||
? (toolPart.output.error ?? `${toolLabel(toolPart.type)} failed`)
|
||||
: count != null
|
||||
? `${toolLabel(toolPart.type)} complete (${count})`
|
||||
: `${toolLabel(toolPart.type)} complete`}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ function addInitMessage(messageArray: UIMessage[]) {
|
||||
role: 'assistant',
|
||||
parts: [{
|
||||
type: 'text',
|
||||
text: "Hi im gregors ai assistant,you can ask me to provide general information or to schedule a meeting."
|
||||
text: "Hi, I'm Gregor's AI assistant. Ask me about his experience, projects, blog posts, or availability for a meeting."
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user