2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import type { UserList } from "~/src/types/user";
import { env } from "../env.mjs";
/**
* Creates a booking for a user by event type, times, and timezone.
*/
const createBooking = async ({
apiKey,
userId,
users,
eventTypeId,
start,
end,
timeZone,
language,
invite,
}: {
apiKey: string;
userId: number;
users: UserList;
eventTypeId: number;
start: string;
end: string;
timeZone: string;
language: string;
invite: number;
title?: string;
status?: string;
}): Promise<string | Error | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings?${urlParams.toString()}`;
const user = users.find((u) => u.id === invite);
if (!user) {
return { error: `User with id ${invite} not found to invite` };
}
const responses = {
id: invite.toString(),
name: user.username,
email: user.email,
};
const response = await fetch(url, {
body: JSON.stringify({
end,
eventTypeId,
language,
metadata: {},
responses,
start,
timeZone,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
// Let GPT handle this. This will happen when wrong event type id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return {
error: data.message,
};
}
return "Booking created";
};
const createBookingTool = (apiKey: string, userId: number, users: UserList) => {
return new DynamicStructuredTool({
description: "Creates a booking on the primary user's calendar.",
func: async ({ eventTypeId, start, end, timeZone, language, invite, title, status }) => {
return JSON.stringify(
await createBooking({
apiKey,
userId,
users,
end,
eventTypeId,
language,
invite,
start,
status,
timeZone,
title,
})
);
},
name: "createBooking",
schema: z.object({
end: z
.string()
.describe("This should correspond to the event type's length, unless otherwise specified."),
eventTypeId: z.number(),
language: z.string(),
invite: z.number().describe("External user id to invite."),
start: z.string(),
status: z.string().optional().describe("ACCEPTED, PENDING, CANCELLED or REJECTED"),
timeZone: z.string(),
title: z.string().optional(),
}),
});
};
export default createBookingTool;

View File

@@ -0,0 +1,66 @@
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Cancels a booking for a user by ID with reason.
*/
const cancelBooking = async ({
apiKey,
id,
reason,
}: {
apiKey: string;
id: string;
reason: string;
}): Promise<string | { error: string }> => {
const params = {
apiKey,
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings/${id}/cancel?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({ reason }),
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return "Booking cancelled";
};
const cancelBookingTool = (apiKey: string) => {
return new DynamicStructuredTool({
description: "Cancel a booking",
func: async ({ id, reason }) => {
return JSON.stringify(
await cancelBooking({
apiKey,
id,
reason,
})
);
},
name: "cancelBooking",
schema: z.object({
id: z.string(),
reason: z.string(),
}),
});
};
export default cancelBookingTool;

View File

@@ -0,0 +1,77 @@
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { Availability } from "../types/availability";
/**
* Fetches availability for a user by date range and event type.
*/
export const fetchAvailability = async ({
apiKey,
userId,
dateFrom,
dateTo,
}: {
apiKey: string;
userId: number;
dateFrom: string;
dateTo: string;
}): Promise<Partial<Availability> | { error: string }> => {
const params: { [k: string]: string } = {
apiKey,
userId: userId.toString(),
dateFrom,
dateTo,
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/availability?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return {
busy: data.busy,
dateRanges: data.dateRanges,
timeZone: data.timeZone,
workingHours: data.workingHours,
};
};
const getAvailabilityTool = (apiKey: string) => {
return new DynamicStructuredTool({
description: "Get availability of users within range.",
func: async ({ userIds, dateFrom, dateTo }) => {
return JSON.stringify(
await Promise.all(
userIds.map(
async (userId) =>
await fetchAvailability({
userId: userId,
apiKey,
dateFrom,
dateTo,
})
)
)
);
},
name: "getAvailability",
schema: z.object({
userIds: z.array(z.number()).describe("The users to fetch availability for."),
dateFrom: z.string(),
dateTo: z.string(),
}),
});
};
export default getAvailabilityTool;

View File

@@ -0,0 +1,75 @@
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { Booking } from "../types/booking";
import { BOOKING_STATUS } from "../types/booking";
/**
* Fetches bookings for a user by date range.
*/
const fetchBookings = async ({
apiKey,
userId,
from,
to,
}: {
apiKey: string;
userId: number;
from: string;
to: string;
}): Promise<Booking[] | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
dateFrom: from,
dateTo: to,
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
const bookings: Booking[] = data.bookings
.filter((booking: Booking) => {
const notCancelled = booking.status !== BOOKING_STATUS.CANCELLED;
return notCancelled;
})
.map(({ endTime, eventTypeId, id, startTime, status, title }: Booking) => ({
endTime,
eventTypeId,
id,
startTime,
status,
title,
}));
return bookings;
};
const getBookingsTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Get bookings for the primary user between two dates.",
func: async ({ from, to }) => {
return JSON.stringify(await fetchBookings({ apiKey, userId, from, to }));
},
name: "getBookings",
schema: z.object({
from: z.string().describe("ISO 8601 datetime string"),
to: z.string().describe("ISO 8601 datetime string"),
}),
});
};
export default getBookingsTool;

View File

@@ -0,0 +1,59 @@
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { EventType } from "../types/eventType";
/**
* Fetches event types by user ID.
*/
export const fetchEventTypes = async ({ apiKey, userId }: { apiKey: string; userId?: number }) => {
const params: Record<string, string> = {
apiKey,
};
if (userId) {
params["userId"] = userId.toString();
}
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/event-types?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return data.event_types.map((eventType: EventType) => ({
id: eventType.id,
slug: eventType.slug,
length: eventType.length,
title: eventType.title,
}));
};
const getEventTypesTool = (apiKey: string) => {
return new DynamicStructuredTool({
description: "Get a user's event type IDs. Usually necessary to book a meeting.",
func: async ({ userId }) => {
return JSON.stringify(
await fetchEventTypes({
apiKey,
userId,
})
);
},
name: "getEventTypes",
schema: z.object({
userId: z.number().optional().describe("The user ID. Defaults to the primary user's ID."),
}),
});
};
export default getEventTypesTool;

View File

@@ -0,0 +1,124 @@
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "~/src/env.mjs";
import type { User, UserList } from "~/src/types/user";
import sendEmail from "~/src/utils/sendEmail";
export const sendBookingEmail = async ({
user,
agentEmail,
subject,
to,
message,
eventTypeSlug,
slots,
date,
}: {
apiKey: string;
user: User;
users: UserList;
agentEmail: string;
subject: string;
to: string;
message: string;
eventTypeSlug: string;
slots?: {
time: string;
text: string;
}[];
date: {
date: string;
text: string;
};
}) => {
// const url = `${env.FRONTEND_URL}/${user.username}/${eventTypeSlug}?date=${date}`;
const timeUrls = slots?.map(({ time, text }) => {
return {
url: `${env.FRONTEND_URL}/${user.username}/${eventTypeSlug}?slot=${time}`,
text,
};
});
const dateUrl = {
url: `${env.FRONTEND_URL}/${user.username}/${eventTypeSlug}?date=${date.date}`,
text: date.text,
};
await sendEmail({
subject,
to,
cc: user.email,
from: agentEmail,
text: message
.split("[[[Slots]]]")
.join(timeUrls?.map(({ url, text }) => `${text}: ${url}`).join("\n"))
.split("[[[Link]]]")
.join(`${dateUrl.text}: ${dateUrl.url}`),
html: message
.split("\n")
.join("<br>")
.split("[[[Slots]]]")
.join(timeUrls?.map(({ url, text }) => `<a href="${url}">${text}</a>`).join("<br>"))
.split("[[[Link]]]")
.join(`<a href="${dateUrl.url}">${dateUrl.text}</a>`),
});
return "Booking link sent";
};
const sendBookingEmailTool = (apiKey: string, user: User, users: UserList, agentEmail: string) => {
return new DynamicStructuredTool({
description:
"Send a booking link via email. Useful for scheduling with non cal users. Be confident, suggesting a good date/time with a fallback to a link to select a date/time.",
func: async ({ message, subject, to, eventTypeSlug, slots, date }) => {
return JSON.stringify(
await sendBookingEmail({
apiKey,
user,
users,
agentEmail,
subject,
to,
message,
eventTypeSlug,
slots,
date,
})
);
},
name: "sendBookingEmail",
schema: z.object({
message: z
.string()
.describe(
"A polite and professional email with an intro and signature at the end. Specify you are the AI booking assistant of the primary user. Use [[[Slots]]] and a fallback [[[Link]]] to inject good times and 'see all times' into messages"
),
subject: z.string(),
to: z
.string()
.describe("email address to send the booking link to. Primary user is automatically CC'd"),
eventTypeSlug: z.string().describe("the slug of the event type to book"),
slots: z
.array(
z.object({
time: z.string().describe("YYYY-MM-DDTHH:mm in UTC"),
text: z.string().describe("minimum readable label. Ex. 4pm."),
})
)
.optional()
.describe("Time slots the external user can click"),
date: z
.object({
date: z.string().describe("YYYY-MM-DD"),
text: z.string().describe('"See all times" or similar'),
})
.describe(
"A booking link that allows the external user to select a date / time. Should be a fallback to time slots"
),
}),
});
};
export default sendBookingEmailTool;

View File

@@ -0,0 +1,85 @@
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Edits a booking for a user by booking ID with new times, title, description, or status.
*/
const editBooking = async ({
apiKey,
userId,
id,
startTime, // In the docs it says start, but it's startTime: https://cal.com/docs/enterprise-features/api/api-reference/bookings#edit-an-existing-booking.
endTime, // Same here: it says end but it's endTime.
title,
description,
status,
}: {
apiKey: string;
userId: number;
id: string;
startTime?: string;
endTime?: string;
title?: string;
description?: string;
status?: string;
}): Promise<string | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings/${id}?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({ description, endTime, startTime, status, title }),
headers: {
"Content-Type": "application/json",
},
method: "PATCH",
});
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return "Booking edited";
};
const editBookingTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Edit a booking",
func: async ({ description, endTime, id, startTime, status, title }) => {
return JSON.stringify(
await editBooking({
apiKey,
userId,
description,
endTime,
id,
startTime,
status,
title,
})
);
},
name: "editBooking",
schema: z.object({
description: z.string().optional(),
endTime: z.string().optional(),
id: z.string(),
startTime: z.string().optional(),
status: z.string().optional(),
title: z.string().optional(),
}),
});
};
export default editBookingTool;