diff --git a/apps/ai/.env.example b/apps/ai/.env.example deleted file mode 100644 index e6effa0a1e..0000000000 --- a/apps/ai/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -BACKEND_URL=http://localhost:3002/api -# BACKEND_URL=https://api.cal.com/v1 -FRONTEND_URL=http://localhost:3000 -# FRONTEND_URL=https://cal.com - -APP_ID=cal-ai -APP_URL=http://localhost:3000/apps/cal-ai - -# This is for the onboard route. Which domain should we send emails from? -SENDER_DOMAIN=cal.ai - -# Used to verify requests from sendgrid. You can generate a new one with: `openssl rand -hex 32` -PARSE_KEY= - -OPENAI_API_KEY= - -# Optionally trace completions at https://smith.langchain.com -# LANGCHAIN_TRACING_V2=true -# LANGCHAIN_ENDPOINT= -# LANGCHAIN_API_KEY= -# LANGCHAIN_PROJECT= diff --git a/apps/ai/README.md b/apps/ai/README.md deleted file mode 100644 index 306bf07906..0000000000 --- a/apps/ai/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Cal.ai - -Welcome to [Cal.ai](https://cal.ai)! - -This app lets you chat with your calendar via email: - -- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?" -- List and rearrange your bookings eg. "clear my afternoon" -- Answer basic questions about your busiest times eg. "how does my Tuesday look?" - -The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings. - -_The AI agent can only choose from a set of tools, without ever seeing your API key._ - -Emails are cleaned and routed in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts) using [MailParser](https://nodemailer.com/extras/mailparser/). - -Incoming emails are routed by email address. Addresses are verified by [DKIM record](https://support.google.com/a/answer/174124?hl=en), making them hard to spoof. - -## Recognition - -Cal.ai - World's first open source AI scheduling assistant | Product Hunt Cal.ai - World's first open source AI scheduling assistant | Product Hunt - -## Getting Started - -### Development - -If you haven't yet, please run the [root setup](/README.md) steps. - -Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. Run `cp .env.example .env` in this folder to get started. You'll need: - -- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4 -- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys) -- A default sender email (for example, `me@dev.example.com`) -- The Cal.ai app's ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts)) -- A unique value for `PARSE_KEY` with `openssl rand -hex 32` - -To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`. - -### Agent Architecture - -The scheduling agent in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts) calls an LLM (in this case, GPT-4) in a loop to accomplish a multi-step task. We use an [OpenAI Functions agent](https://js.langchain.com/docs/modules/agents/agent_types/openai_functions_agent), which is fine-tuned to output text suited for passing to tools. - -Tools (eg. [`createBooking`](/apps/ai/src/tools/createBooking.ts)) are simply JavaScript methods wrapped by Zod schemas, telling the agent what format to output. - -Here is the full architecture: - -![Cal.ai architecture](/apps/ai/src/public/architecture.png) - -### Email Router - -To expose the AI app, you can use either [Tunnelmole](https://github.com/robbie-cahill/tunnelmole-client), an open source tunnelling tool; or [nGrok](https://ngrok.com/), a popular closed source tunnelling tool. - -For Tunnelmole, run `tmole 3005` (or the AI app's port number) in a new terminal. Please replace `3005` with the port number if it is different. In the output, you'll see two URLs, one http and a https (we recommend using the https url for privacy and security). To install Tunnelmole, use `curl -O https://install.tunnelmole.com/8dPBw/install && sudo bash install`. (On Windows, download [tmole.exe](https://tunnelmole.com/downloads/tmole.exe)) - -For nGrok, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install nGrok first. - -To forward incoming emails to the serverless function at `/agent`, we use [SendGrid's Inbound Parse](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook). - -1. Ensure you have a [SendGrid account](https://signup.sendgrid.com/) -2. Ensure you have an authenticated domain. Go to Settings > Sender Authentication > Authenticate. For DNS host, select `I'm not sure`. Click Next and add your domain, eg. `example.com`. Choose Manual Setup. You'll be given three CNAME records to add to your DNS settings, eg. in [Vercel Domains](https://vercel.com/dashboard/domains). After adding those records, click Verify. To troubleshoot, see the [full instructions](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication). -3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`. Set the priority to `10` if prompted. -4. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL. Choose your authenticated domain. -5. In the Destination URL field, use the Tunnelmole or ngrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.tunnelmole.net/api/receive?parseKey=ABC-123` or `https://abc.ngrok.io/api/receive?parseKey=ABC-123`. -6. Activate "POST the raw, full MIME message". -7. Send an email to `[anyUsername]@example.com`. You should see a ping on the Tunnelmole or ngrok listener and server. -8. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour. - -Please feel free to improve any part of this architecture! diff --git a/apps/ai/next-env.d.ts b/apps/ai/next-env.d.ts deleted file mode 100644 index 4f11a03dc6..0000000000 --- a/apps/ai/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/ai/next.config.js b/apps/ai/next.config.js deleted file mode 100644 index 747ded2324..0000000000 --- a/apps/ai/next.config.js +++ /dev/null @@ -1,24 +0,0 @@ -const withBundleAnalyzer = require("@next/bundle-analyzer"); - -const plugins = []; -plugins.push(withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" })); - -/** @type {import("next").NextConfig} */ -const nextConfig = { - async redirects() { - return [ - { - source: "/", - destination: "https://cal.com/ai", - permanent: true, - }, - ]; - }, - i18n: { - defaultLocale: "en", - locales: ["en"], - }, - reactStrictMode: true, -}; - -module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig); diff --git a/apps/ai/package.json b/apps/ai/package.json deleted file mode 100644 index 3bd4127264..0000000000 --- a/apps/ai/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@calcom/ai", - "version": "1.2.1", - "private": true, - "author": "Cal.com Inc.", - "dependencies": { - "@calcom/prisma": "*", - "@langchain/core": "^0.1.26", - "@langchain/openai": "^0.0.14", - "@t3-oss/env-nextjs": "^0.6.1", - "langchain": "^0.1.17", - "mailparser": "^3.6.5", - "next": "^13.5.4", - "supports-color": "8.1.1", - "zod": "^3.22.4" - }, - "devDependencies": { - "@types/mailparser": "^3.4.0" - }, - "scripts": { - "build": "next build", - "dev": "next dev -p 3005", - "format": "npx prettier . --write", - "lint": "eslint . --ext .ts,.js,.tsx,.jsx", - "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", - "start": "next start" - } -} diff --git a/apps/ai/src/app/api/agent/route.ts b/apps/ai/src/app/api/agent/route.ts deleted file mode 100644 index 0ffbfa36b0..0000000000 --- a/apps/ai/src/app/api/agent/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -import agent from "../../../utils/agent"; -import sendEmail from "../../../utils/sendEmail"; -import { verifyParseKey } from "../../../utils/verifyParseKey"; - -// Allow agent loop to run for up to 5 minutes -export const maxDuration = 300; - -/** - * Launches a LangChain agent to process an incoming email, - * then sends the response to the user. - */ -export const POST = async (request: NextRequest) => { - const verified = verifyParseKey(request.url); - - if (!verified) { - return new NextResponse("Unauthorized", { status: 401 }); - } - - const json = await request.json(); - - const { apiKey, userId, message, subject, user, users, replyTo: agentEmail } = json; - - if ((!message && !subject) || !user) { - return new NextResponse("Missing fields", { status: 400 }); - } - - try { - const response = await agent(`${subject}\n\n${message}`, { ...user }, users, apiKey, userId, agentEmail); - - // Send response to user - await sendEmail({ - subject: `Re: ${subject}`, - text: response.replace(/(?:\r\n|\r|\n)/g, "\n"), - to: user.email, - from: agentEmail, - }); - - return new NextResponse("ok"); - } catch (error) { - await sendEmail({ - subject: `Re: ${subject}`, - text: "Thanks for using Cal.ai! We're experiencing high demand and can't currently process your request. Please try again later.", - to: user.email, - from: agentEmail, - }); - - return new NextResponse( - (error as Error).message || "Something went wrong. Please try again or reach out for help.", - { status: 500 } - ); - } -}; diff --git a/apps/ai/src/app/api/onboard/route.ts b/apps/ai/src/app/api/onboard/route.ts deleted file mode 100644 index e4eaaef8cf..0000000000 --- a/apps/ai/src/app/api/onboard/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { NextRequest } from "next/server"; - -import prisma from "@calcom/prisma"; - -import { env } from "../../../env.mjs"; -import sendEmail from "../../../utils/sendEmail"; - -export const POST = async (request: NextRequest) => { - const { userId } = await request.json(); - - const user = await prisma.user.findUnique({ - select: { - email: true, - name: true, - username: true, - }, - where: { - id: userId, - }, - }); - - if (!user) { - return new Response("User not found", { status: 404 }); - } - - await sendEmail({ - subject: "Welcome to Cal AI", - to: user.email, - from: `${user.username}@${env.SENDER_DOMAIN}`, - text: `Hi ${ - user.name || `@${user.username}` - },\n\nI'm Cal AI, your personal booking assistant! I'll be here, 24/7 to help manage your busy schedule and find times to meet with the people you care about.\n\nHere are some things you can ask me:\n\n- "Book a meeting with @someone" (The @ symbol lets you tag Cal.com users)\n- "What meetings do I have today?" (I'll show you your schedule)\n- "Find a time for coffee with someone@gmail.com" (I'll intro and send them some good times)\n\nI'm still learning, so if you have any feedback, please tweet it to @calcom!\n\nRemember, you can always reach me here, at ${ - user.username - }@${ - env.SENDER_DOMAIN - }.\n\nLooking forward to working together (:\n\n- Cal AI, Your personal booking assistant`, - html: `Hi ${ - user.name || `@${user.username}` - },

I'm Cal AI, your personal booking assistant! I'll be here, 24/7 to help manage your busy schedule and find times to meet with the people you care about.

Here are some things you can ask me:

- "Book a meeting with @someone" (The @ symbol lets you tag Cal.com users)
- "What meetings do I have today?" (I'll show you your schedule)
- "Find a time for coffee with someone@gmail.com" (I'll intro and send them some good times)

I'm still learning, so if you have any feedback, please send it to @calcom on X!

Remember, you can always reach me here, at ${ - user.username - }@${env.SENDER_DOMAIN}.

Looking forward to working together (:

- Cal AI`, - }); - return new Response("OK", { status: 200 }); -}; diff --git a/apps/ai/src/app/api/receive/route.ts b/apps/ai/src/app/api/receive/route.ts deleted file mode 100644 index 68bbc51168..0000000000 --- a/apps/ai/src/app/api/receive/route.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { ParsedMail, Source } from "mailparser"; -import { simpleParser } from "mailparser"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; -import prisma from "@calcom/prisma"; - -import { env } from "../../../env.mjs"; -import { fetchAvailability } from "../../../tools/getAvailability"; -import { fetchEventTypes } from "../../../tools/getEventTypes"; -import { extractUsers } from "../../../utils/extractUsers"; -import getHostFromHeaders from "../../../utils/host"; -import now from "../../../utils/now"; -import sendEmail from "../../../utils/sendEmail"; -import { verifyParseKey } from "../../../utils/verifyParseKey"; - -// Allow receive loop to run for up to 30 seconds -// Why so long? the rate determining API call (getAvailability, getEventTypes) can take up to 15 seconds at peak times so we give it a little extra time to complete. -export const maxDuration = 30; - -/** - * Verifies email signature and app authorization, - * then hands off to booking agent. - */ -export const POST = async (request: NextRequest) => { - const verified = verifyParseKey(request.url); - - if (!verified) { - return new NextResponse("Unauthorized", { status: 401 }); - } - - const formData = await request.formData(); - const body = Object.fromEntries(formData); - const envelope = JSON.parse(body.envelope as string); - - const aiEmail = envelope.to[0]; - const subject = body.subject || ""; - - try { - await checkRateLimitAndThrowError({ - identifier: `ai:email:${envelope.from}`, - rateLimitingType: "ai", - }); - } catch (error) { - await sendEmail({ - subject: `Re: ${subject}`, - text: "Thanks for using Cal.ai! You've reached your daily limit. Please try again tomorrow.", - to: envelope.from, - from: aiEmail, - }); - - return new NextResponse("Exceeded rate limit", { status: 200 }); // Don't return 429 to avoid triggering retry logic in SendGrid - } - - // Parse email from mixed MIME type - const parsed: ParsedMail = await simpleParser(body.email as Source); - - if (!parsed.text && !parsed.subject) { - await sendEmail({ - subject: `Re: ${subject}`, - text: "Thanks for using Cal.ai! It looks like you forgot to include a message. Please try again.", - to: envelope.from, - from: aiEmail, - }); - return new NextResponse("Email missing text and subject", { status: 400 }); - } - - const user = await prisma.user.findUnique({ - select: { - email: true, - id: true, - username: true, - timeZone: true, - credentials: { - select: { - appId: true, - key: true, - }, - }, - }, - where: { email: envelope.from }, - }); - - // body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass} - const signature = (body.dkim as string).includes(" : pass"); - - // User is not a cal.com user or is using an unverified email. - if (!signature || !user) { - await sendEmail({ - html: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address and then install Cal.ai here: go.cal.com/ai.`, - subject: `Re: ${subject}`, - text: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`, - to: envelope.from, - from: aiEmail, - }); - - return new NextResponse("ok"); - } - - const credential = user.credentials.find((c) => c.appId === env.APP_ID)?.key; - - // User has not installed the app from the app store. Direct them to install it. - if (!(credential as { apiKey: string })?.apiKey) { - const url = env.APP_URL; - - await sendEmail({ - html: `Thanks for using Cal.ai! To get started, the app must be installed. Click this link to install it.`, - subject: `Re: ${subject}`, - text: `Thanks for using Cal.ai! To get started, the app must be installed. Click this link to install the Cal.ai app: ${url}`, - to: envelope.from, - from: aiEmail, - }); - - return new NextResponse("ok"); - } - - const { apiKey } = credential as { apiKey: string }; - - // Pre-fetch data relevant to most bookings. - const [eventTypes, availability, users] = await Promise.all([ - fetchEventTypes({ - apiKey, - }), - fetchAvailability({ - apiKey, - userId: user.id, - dateFrom: now(user.timeZone), - dateTo: now(user.timeZone), - }), - extractUsers(`${parsed.text} ${parsed.subject}`), - ]); - - if ("error" in availability) { - await sendEmail({ - subject: `Re: ${subject}`, - text: "Sorry, there was an error fetching your availability. Please try again.", - to: user.email, - from: aiEmail, - }); - console.error(availability.error); - return new NextResponse("Error fetching availability. Please try again.", { status: 400 }); - } - - if ("error" in eventTypes) { - await sendEmail({ - subject: `Re: ${subject}`, - text: "Sorry, there was an error fetching your event types. Please try again.", - to: user.email, - from: aiEmail, - }); - console.error(eventTypes.error); - return new NextResponse("Error fetching event types. Please try again.", { status: 400 }); - } - - const { workingHours } = availability; - - const appHost = getHostFromHeaders(request.headers); - - // Hand off to long-running agent endpoint to handle the email. (don't await) - fetch(`${appHost}/api/agent?parseKey=${env.PARSE_KEY}`, { - body: JSON.stringify({ - apiKey, - userId: user.id, - message: parsed.text || "", - subject: parsed.subject || "", - replyTo: aiEmail, - user: { - email: user.email, - eventTypes, - username: user.username, - timeZone: user.timeZone, - workingHours, - }, - users, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); - - await new Promise((r) => setTimeout(r, 1000)); - - return new NextResponse("ok"); -}; diff --git a/apps/ai/src/env.mjs b/apps/ai/src/env.mjs deleted file mode 100644 index 2596a26643..0000000000 --- a/apps/ai/src/env.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; - -export const env = createEnv({ - /** - * Specify your client-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. To expose them to the client, prefix them with - * `NEXT_PUBLIC_`. - */ - client: { - // NEXT_PUBLIC_CLIENTVAR: z.string().min(1), - }, - - /** - * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. - * middlewares) or client-side so we need to destruct manually. - */ - runtimeEnv: { - BACKEND_URL: process.env.BACKEND_URL, - FRONTEND_URL: process.env.FRONTEND_URL, - APP_ID: process.env.APP_ID, - APP_URL: process.env.APP_URL, - SENDER_DOMAIN: process.env.SENDER_DOMAIN, - PARSE_KEY: process.env.PARSE_KEY, - NODE_ENV: process.env.NODE_ENV, - OPENAI_API_KEY: process.env.OPENAI_API_KEY, - SENDGRID_API_KEY: process.env.SENDGRID_API_KEY, - DATABASE_URL: process.env.DATABASE_URL, - }, - - /** - * Specify your server-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. - */ - server: { - BACKEND_URL: z.string().url(), - FRONTEND_URL: z.string().url(), - APP_ID: z.string().min(1), - APP_URL: z.string().url(), - SENDER_DOMAIN: z.string().min(1), - PARSE_KEY: z.string().min(1), - NODE_ENV: z.enum(["development", "test", "production"]), - OPENAI_API_KEY: z.string().min(1), - SENDGRID_API_KEY: z.string().min(1), - DATABASE_URL: z.string().url(), - }, -}); diff --git a/apps/ai/src/public/architecture.png b/apps/ai/src/public/architecture.png deleted file mode 100644 index 52eedfe6f5..0000000000 Binary files a/apps/ai/src/public/architecture.png and /dev/null differ diff --git a/apps/ai/src/tools/createBooking.ts b/apps/ai/src/tools/createBooking.ts deleted file mode 100644 index 40bb4d0fc1..0000000000 --- a/apps/ai/src/tools/createBooking.ts +++ /dev/null @@ -1,121 +0,0 @@ -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 => { - 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; diff --git a/apps/ai/src/tools/deleteBooking.ts b/apps/ai/src/tools/deleteBooking.ts deleted file mode 100644 index 981cef8f58..0000000000 --- a/apps/ai/src/tools/deleteBooking.ts +++ /dev/null @@ -1,66 +0,0 @@ -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 => { - 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; diff --git a/apps/ai/src/tools/getAvailability.ts b/apps/ai/src/tools/getAvailability.ts deleted file mode 100644 index 26258e0c60..0000000000 --- a/apps/ai/src/tools/getAvailability.ts +++ /dev/null @@ -1,77 +0,0 @@ -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 | { 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; diff --git a/apps/ai/src/tools/getBookings.ts b/apps/ai/src/tools/getBookings.ts deleted file mode 100644 index 6976f31ecf..0000000000 --- a/apps/ai/src/tools/getBookings.ts +++ /dev/null @@ -1,75 +0,0 @@ -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 => { - 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; diff --git a/apps/ai/src/tools/getEventTypes.ts b/apps/ai/src/tools/getEventTypes.ts deleted file mode 100644 index c59df69c99..0000000000 --- a/apps/ai/src/tools/getEventTypes.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 = { - 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; diff --git a/apps/ai/src/tools/sendBookingEmail.ts b/apps/ai/src/tools/sendBookingEmail.ts deleted file mode 100644 index 2133f61769..0000000000 --- a/apps/ai/src/tools/sendBookingEmail.ts +++ /dev/null @@ -1,124 +0,0 @@ -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("
") - .split("[[[Slots]]]") - .join(timeUrls?.map(({ url, text }) => `${text}`).join("
")) - .split("[[[Link]]]") - .join(`${dateUrl.text}`), - }); - - 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; diff --git a/apps/ai/src/tools/updateBooking.ts b/apps/ai/src/tools/updateBooking.ts deleted file mode 100644 index eec462e897..0000000000 --- a/apps/ai/src/tools/updateBooking.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 => { - 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; diff --git a/apps/ai/src/types/availability.ts b/apps/ai/src/types/availability.ts deleted file mode 100644 index 62ed3b5ed5..0000000000 --- a/apps/ai/src/types/availability.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type Availability = { - busy: { - start: string; - end: string; - title?: string; - }[]; - timeZone: string; - dateRanges: { - start: string; - end: string; - }[]; - workingHours: { - days: number[]; - startTime: number; - endTime: number; - userId: number; - }[]; - dateOverrides: { - date: string; - startTime: number; - endTime: number; - userId: number; - }; - currentSeats: number; -}; diff --git a/apps/ai/src/types/booking.ts b/apps/ai/src/types/booking.ts deleted file mode 100644 index 91eaf4e322..0000000000 --- a/apps/ai/src/types/booking.ts +++ /dev/null @@ -1,23 +0,0 @@ -export enum BOOKING_STATUS { - ACCEPTED = "ACCEPTED", - PENDING = "PENDING", - CANCELLED = "CANCELLED", - REJECTED = "REJECTED", -} - -export type Booking = { - id: number; - userId: number; - description: string | null; - eventTypeId: number; - uid: string; - title: string; - startTime: string; - endTime: string; - attendees: { email: string; name: string; timeZone: string; locale: string }[] | null; - user: { email: string; name: string; timeZone: string; locale: string }[] | null; - payment: { id: number; success: boolean; paymentOption: string }[]; - metadata: object | null; - status: BOOKING_STATUS; - responses: { email: string; name: string; location: string } | null; -}; diff --git a/apps/ai/src/types/eventType.ts b/apps/ai/src/types/eventType.ts deleted file mode 100644 index b9de4cc230..0000000000 --- a/apps/ai/src/types/eventType.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type EventType = { - id: number; - title: string; - length: number; - metadata: object; - slug: string; - hosts: { - userId: number; - isFixed: boolean; - }[]; - hidden: boolean; - // ... -}; diff --git a/apps/ai/src/types/user.ts b/apps/ai/src/types/user.ts deleted file mode 100644 index e39b1b234b..0000000000 --- a/apps/ai/src/types/user.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { EventType } from "./eventType"; -import type { WorkingHours } from "./workingHours"; - -export type User = { - id: number; - email: string; - username: string; - timeZone: string; - eventTypes: EventType[]; - workingHours: WorkingHours[]; -}; - -export type UserList = { - id?: number; - email?: string; - username?: string; - type: "fromUsername" | "fromEmail"; -}[]; diff --git a/apps/ai/src/types/workingHours.ts b/apps/ai/src/types/workingHours.ts deleted file mode 100644 index e3a1905a45..0000000000 --- a/apps/ai/src/types/workingHours.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type WorkingHours = { - days: number[]; - startTime: number; - endTime: number; -}; diff --git a/apps/ai/src/utils/agent.ts b/apps/ai/src/utils/agent.ts deleted file mode 100644 index 4ef72f10cc..0000000000 --- a/apps/ai/src/utils/agent.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { ChatOpenAI } from "@langchain/openai"; -import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents"; - -import { env } from "../env.mjs"; -import createBookingIfAvailable from "../tools/createBooking"; -import deleteBooking from "../tools/deleteBooking"; -import getAvailability from "../tools/getAvailability"; -import getBookings from "../tools/getBookings"; -import sendBookingEmail from "../tools/sendBookingEmail"; -import updateBooking from "../tools/updateBooking"; -import type { EventType } from "../types/eventType"; -import type { User, UserList } from "../types/user"; -import type { WorkingHours } from "../types/workingHours"; -import now from "./now"; - -const gptModel = "gpt-4-0125-preview"; - -/** - * Core of the Cal.ai booking agent: a LangChain Agent Executor. - * Uses a toolchain to book meetings, list available slots, etc. - * Uses OpenAI functions to better enforce JSON-parsable output from the LLM. - */ -const agent = async ( - input: string, - user: User, - users: UserList, - apiKey: string, - userId: number, - agentEmail: string -) => { - const tools = [ - // getEventTypes(apiKey), - getAvailability(apiKey), - getBookings(apiKey, userId), - createBookingIfAvailable(apiKey, userId, users), - updateBooking(apiKey, userId), - deleteBooking(apiKey), - sendBookingEmail(apiKey, user, users, agentEmail), - ]; - - const model = new ChatOpenAI({ - modelName: gptModel, - openAIApiKey: env.OPENAI_API_KEY, - temperature: 0, - }); - - /** - * Initialize the agent executor with arguments. - */ - const prompt = - ChatPromptTemplate.fromTemplate(`You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email. - Make sure your final answers are definitive, complete and well formatted. - Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information. - Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone. - In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule." - If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds. - - The primary user's id is: ${userId} - The primary user's username is: ${user.username} - The current time in the primary user's timezone is: ${now(user.timeZone, { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - })} - The primary user's time zone is: ${user.timeZone} - The primary user's event types are: ${user.eventTypes - .map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`) - .join("\n")} - The primary user's working hours are: ${user.workingHours - .map( - (w: WorkingHours) => - `Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${ - w.startTime - }, End Time (minutes in UTC): ${w.endTime};` - ) - .join("\n")} - ${ - users.length - ? `The email references the following @usernames and emails: ${users - .map( - (u) => - `${ - (u.id ? `, id: ${u.id}` : "id: (non user)") + - (u.username - ? u.type === "fromUsername" - ? `, username: @${u.username}` - : ", username: REDACTED" - : ", (no username)") + - (u.email - ? u.type === "fromEmail" - ? `, email: ${u.email}` - : ", email: REDACTED" - : ", (no email)") - };` - ) - .join("\n")}` - : "" - }`); - const agent = await createOpenAIFunctionsAgent({ - llm: model, - prompt, - tools, - }); - - const executor = new AgentExecutor({ - agent, - tools, - returnIntermediateSteps: env.NODE_ENV === "development", - verbose: env.NODE_ENV === "development", - }); - - const result = await executor.invoke({ input }); - const { output } = result; - - return output; -}; - -export default agent; diff --git a/apps/ai/src/utils/context.ts b/apps/ai/src/utils/context.ts deleted file mode 100644 index 26864e73d6..0000000000 --- a/apps/ai/src/utils/context.ts +++ /dev/null @@ -1 +0,0 @@ -export const context = { apiKey: "", userId: "" }; diff --git a/apps/ai/src/utils/extractUsers.ts b/apps/ai/src/utils/extractUsers.ts deleted file mode 100644 index 10a6f9fe84..0000000000 --- a/apps/ai/src/utils/extractUsers.ts +++ /dev/null @@ -1,85 +0,0 @@ -import prisma from "@calcom/prisma"; - -import type { UserList } from "../types/user"; - -/* - * Extracts usernames (@Example) and emails (hi@example.com) from a string - */ -export const extractUsers = async (text: string) => { - const usernames = text - .match(/(? username.slice(1).toLowerCase()); - const emails = text - .match(/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/g) - ?.map((email) => email.toLowerCase()); - - const dbUsersFromUsernames = usernames - ? await prisma.user.findMany({ - select: { - id: true, - username: true, - email: true, - }, - where: { - username: { - in: usernames, - }, - }, - }) - : []; - - const usersFromUsernames = usernames - ? usernames.map((username) => { - const user = dbUsersFromUsernames.find((u) => u.username === username); - return user - ? { - username, - id: user.id, - email: user.email, - type: "fromUsername", - } - : { - username, - id: null, - email: null, - type: "fromUsername", - }; - }) - : []; - - const dbUsersFromEmails = emails - ? await prisma.user.findMany({ - select: { - id: true, - email: true, - username: true, - }, - where: { - email: { - in: emails, - }, - }, - }) - : []; - - const usersFromEmails = emails - ? emails.map((email) => { - const user = dbUsersFromEmails.find((u) => u.email === email); - return user - ? { - email, - id: user.id, - username: user.username, - type: "fromEmail", - } - : { - email, - id: null, - username: null, - type: "fromEmail", - }; - }) - : []; - - return [...usersFromUsernames, ...usersFromEmails] as UserList; -}; diff --git a/apps/ai/src/utils/host.ts b/apps/ai/src/utils/host.ts deleted file mode 100644 index f215d7e7d0..0000000000 --- a/apps/ai/src/utils/host.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextRequest } from "next/server"; - -const getHostFromHeaders = (headers: NextRequest["headers"]): string => { - return `https://${headers.get("host")}`; -}; - -export default getHostFromHeaders; diff --git a/apps/ai/src/utils/now.ts b/apps/ai/src/utils/now.ts deleted file mode 100644 index 98630d7bc4..0000000000 --- a/apps/ai/src/utils/now.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default function now(timeZone: string, options: Intl.DateTimeFormatOptions = {}) { - return new Date().toLocaleString("en-US", { - timeZone, - ...options, - }); -} diff --git a/apps/ai/src/utils/sendEmail.ts b/apps/ai/src/utils/sendEmail.ts deleted file mode 100644 index adbc9693e7..0000000000 --- a/apps/ai/src/utils/sendEmail.ts +++ /dev/null @@ -1,43 +0,0 @@ -import mail from "@sendgrid/mail"; - -const sendgridAPIKey = process.env.SENDGRID_API_KEY as string; - -/** - * Simply send an email by address, subject, and body. - */ -const send = async ({ - subject, - to, - cc, - from, - text, - html, -}: { - subject: string; - to: string | string[]; - cc?: string | string[]; - from: string; - text: string; - html?: string; -}): Promise => { - mail.setApiKey(sendgridAPIKey); - - const msg = { - to, - cc, - from: { - email: from, - name: "Cal.ai", - }, - text, - html, - subject, - }; - - const res = await mail.send(msg); - const success = !!res; - - return success; -}; - -export default send; diff --git a/apps/ai/src/utils/verifyParseKey.ts b/apps/ai/src/utils/verifyParseKey.ts deleted file mode 100644 index 5ac81ed78c..0000000000 --- a/apps/ai/src/utils/verifyParseKey.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { NextRequest } from "next/server"; - -import { env } from "../env.mjs"; - -/** - * Verifies that the request contains the correct parse key. - * env.PARSE_KEY must be configured as a query param in the sendgrid inbound parse settings. - */ -export const verifyParseKey = (url: NextRequest["url"]) => { - const verified = new URL(url).searchParams.get("parseKey") === env.PARSE_KEY; - - return verified; -}; diff --git a/apps/ai/tsconfig.json b/apps/ai/tsconfig.json deleted file mode 100644 index aae1b4e758..0000000000 --- a/apps/ai/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "extends": "@calcom/tsconfig/nextjs.json", - "compilerOptions": { - "strict": true, - "jsx": "preserve", - "baseUrl": ".", - "paths": { - "~/*": ["*"] - }, - "plugins": [ - { - "name": "next" - } - ] - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - "../../packages/types/window.d.ts" - ], - "exclude": ["node_modules"] -} diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index 4fea31051f..e623dbc919 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -11,7 +11,6 @@ export const InstallAppButtonMap = { vital: dynamic(() => import("./vital/components/InstallAppButton")), }; export const AppSettingsComponentsMap = { - "cal-ai": dynamic(() => import("./cal-ai/components/AppSettingsInterface")), "general-app-settings": dynamic(() => import("./templates/general-app-settings/components/AppSettingsInterface") ), diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 5023ca070e..09d7ae9954 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -9,7 +9,6 @@ import around_config_json from "./around/config.json"; import autocheckin_config_json from "./autocheckin/config.json"; import baa_for_hipaa_config_json from "./baa-for-hipaa/config.json"; import basecamp3_config_json from "./basecamp3/config.json"; -import cal_ai_config_json from "./cal-ai/config.json"; import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata"; import campfire_config_json from "./campfire/config.json"; import campsite_config_json from "./campsite/config.json"; @@ -102,7 +101,6 @@ export const appStoreMetadata = { autocheckin: autocheckin_config_json, "baa-for-hipaa": baa_for_hipaa_config_json, basecamp3: basecamp3_config_json, - "cal-ai": cal_ai_config_json, caldavcalendar: caldavcalendar__metadata_ts, campfire: campfire_config_json, campsite: campsite_config_json, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 6396cd1ca3..7c143a2d30 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -10,7 +10,6 @@ export const apiHandlers = { autocheckin: import("./autocheckin/api"), "baa-for-hipaa": import("./baa-for-hipaa/api"), basecamp3: import("./basecamp3/api"), - "cal-ai": import("./cal-ai/api"), caldavcalendar: import("./caldavcalendar/api"), campfire: import("./campfire/api"), campsite: import("./campsite/api"), diff --git a/packages/app-store/cal-ai/DESCRIPTION.md b/packages/app-store/cal-ai/DESCRIPTION.md deleted file mode 100644 index 6c045ee7c0..0000000000 --- a/packages/app-store/cal-ai/DESCRIPTION.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -items: - - 1.jpg - - 2.jpg - - 3.jpg - - 4.jpg - - 5.jpg ---- - -{DESCRIPTION} - -## Example questions: - -- "Book a meeting with @cal-user this week at a good time" -- "Find a time to meet with not-a-cal-user@gmail.com this week" -- "Move my meeting with [Someone's Name] to next week" -- "What does my week look like?" -- "When is my next meeting?" -- "What is my least busy day next week?" -- "Cancel the meeting with [Someone's Name]" -- "Reschedule the meeting with not-a-cal-user@gmail.com" -- "Is @cal-user busy on Friday at noon?" -- "Move my meeting at 2pm tomorrow to next week on Friday at 5pm" - -### Or, any of that in a thread: -- What meetings do I have this week? - - Meeting 1, Meeting 2 -- Cancel that first meeting - - -### Or, doing multiple things: -- Cancel all my meetings tomorrow then book a quick chat with @joe at noon, a long meeting with @jane at 1 (both tomorrow). Also see if Toby is down to grab dinner this week (toby@gmail.com). Finally send me my updated agenda of everything I have coming up in the next couple days. \ No newline at end of file diff --git a/packages/app-store/cal-ai/api/_getAdd.ts b/packages/app-store/cal-ai/api/_getAdd.ts deleted file mode 100644 index d8e13972c6..0000000000 --- a/packages/app-store/cal-ai/api/_getAdd.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultResponder } from "@calcom/lib/server"; - -import checkSession from "../../_utils/auth"; -import { withPaidAppRedirect } from "../../_utils/paid-apps"; -import appConfig from "../config.json"; - -export async function getHandler(req: NextApiRequest, res: NextApiResponse) { - const session = checkSession(req); - - const redirectUrl = await withPaidAppRedirect({ - appPaidMode: appConfig.paid.mode, - appSlug: appConfig.slug, - userId: session.user.id, - priceId: appConfig.paid.priceId, - }); - - if (!redirectUrl) { - return res.status(500).json({ message: "Failed to create Stripe checkout session" }); - } - - return { url: redirectUrl }; -} - -export default defaultResponder(getHandler); diff --git a/packages/app-store/cal-ai/api/_getCallback.ts b/packages/app-store/cal-ai/api/_getCallback.ts deleted file mode 100644 index e424d9bde7..0000000000 --- a/packages/app-store/cal-ai/api/_getCallback.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultResponder } from "@calcom/lib/server"; -import { createContext } from "@calcom/trpc/server/createContext"; -import { apiKeysRouter } from "@calcom/trpc/server/routers/viewer/apiKeys/_router"; - -import checkSession from "../../_utils/auth"; -import getInstalledAppPath from "../../_utils/getInstalledAppPath"; -import { checkInstalled, createDefaultInstallation } from "../../_utils/installation"; -import { withStripeCallback } from "../../_utils/paid-apps"; -import appConfig from "../config.json"; - -export async function getHandler(req: NextApiRequest, res: NextApiResponse) { - const session = checkSession(req); - const slug = appConfig.slug; - const appType = appConfig.type; - - const { checkoutId } = req.query as { checkoutId: string }; - if (!checkoutId) { - return { url: `/apps/installed?error=${JSON.stringify({ message: "No Stripe Checkout Session ID" })}` }; - } - - const { url } = await withStripeCallback(checkoutId, slug, async ({ checkoutSession }) => { - const ctx = await createContext({ req, res }); - const caller = apiKeysRouter.createCaller(ctx); - - const apiKey = await caller.create({ - note: "Cal.ai", - expiresAt: null, - appId: "cal-ai", - }); - - await checkInstalled(slug, session.user.id); - await createDefaultInstallation({ - appType, - user: session.user, - slug, - key: { - apiKey, - }, - subscriptionId: checkoutSession.subscription?.toString(), - billingCycleStart: new Date().getDate(), - paymentStatus: "active", - }); - - await fetch( - `${process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://cal.ai"}/api/onboard`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userId: session.user.id, - }), - } - ); - - return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "cal-ai" }) }; - }); - - return res.redirect(url); -} - -export default defaultResponder(getHandler); diff --git a/packages/app-store/cal-ai/api/add.ts b/packages/app-store/cal-ai/api/add.ts deleted file mode 100644 index 912126e232..0000000000 --- a/packages/app-store/cal-ai/api/add.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server"; - -export default defaultHandler({ - GET: import("./_getAdd"), -}); diff --git a/packages/app-store/cal-ai/api/callback.ts b/packages/app-store/cal-ai/api/callback.ts deleted file mode 100644 index 07586cf85f..0000000000 --- a/packages/app-store/cal-ai/api/callback.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server"; - -export default defaultHandler({ - GET: import("./_getCallback"), -}); diff --git a/packages/app-store/cal-ai/api/index.ts b/packages/app-store/cal-ai/api/index.ts deleted file mode 100644 index eb12c1b4ed..0000000000 --- a/packages/app-store/cal-ai/api/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as add } from "./add"; -export { default as callback } from "./callback"; diff --git a/packages/app-store/cal-ai/components/AppSettingsInterface.tsx b/packages/app-store/cal-ai/components/AppSettingsInterface.tsx deleted file mode 100644 index 8c40985f6e..0000000000 --- a/packages/app-store/cal-ai/components/AppSettingsInterface.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useSession } from "next-auth/react"; - -export function MailLink({ subject, body }: { subject: string; body?: string }) { - const { data } = useSession(); - - return ( - - {subject ? subject : body} - - ); -} -export default function AppSettings() { - return ( - <> -
-

Example questions:

-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-

Or, any of that in a thread:

-
    -
  • - What meetings do I have this week? -
      -
    • Meeting 1, Meeting 2
    • -
    -
  • -
  • Cancel that first meeting
  • -
-

Or, doing multiple things:

-
    -
  • - -
  • -
-
- - ); -} diff --git a/packages/app-store/cal-ai/config.json b/packages/app-store/cal-ai/config.json deleted file mode 100644 index c13c045312..0000000000 --- a/packages/app-store/cal-ai/config.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "/*": "Don't modify slug - If required, do it using cli edit command", - "name": "Cal.ai", - "slug": "cal-ai", - "type": "cal-ai_automation", - "logo": "icon.png", - "url": "https://cal.ai", - "variant": "automation", - "categories": ["automation"], - "publisher": "Cal.com, Inc.", - "email": "support@cal.com", - "description": "Cal.ai is your AI scheduling assistant. Get your personal email assistant (username@cal.ai) that you can forward emails to or have a conversation with. Cal.ai will automatically schedule meetings for you.", - "isTemplate": false, - "__createdUsingCli": true, - "__template": "basic", - "dirName": "cal-ai", - "isOAuth": false, - "paid": { - "priceInUsd": 8, - "priceId": "price_1O1ziDH8UDiwIftkDHp3MCTP", - "mode": "subscription" - } -} diff --git a/packages/app-store/cal-ai/index.ts b/packages/app-store/cal-ai/index.ts deleted file mode 100644 index d7f3602204..0000000000 --- a/packages/app-store/cal-ai/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as api from "./api"; diff --git a/packages/app-store/cal-ai/package.json b/packages/app-store/cal-ai/package.json deleted file mode 100644 index 3cde71d537..0000000000 --- a/packages/app-store/cal-ai/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "private": true, - "name": "@calcom/cal-ai", - "version": "0.0.0", - "main": "./index.ts", - "dependencies": { - "@calcom/lib": "*" - }, - "devDependencies": { - "@calcom/types": "*" - }, - "description": "Cal.ai is your AI scheduling assistant. Get your personal email assistant (username@cal.ai) that you can forward emails to or have a conversation with. Cal.ai will automatically schedule meetings for you." -} diff --git a/packages/app-store/cal-ai/static/1.jpg b/packages/app-store/cal-ai/static/1.jpg deleted file mode 100644 index 9792aedc37..0000000000 Binary files a/packages/app-store/cal-ai/static/1.jpg and /dev/null differ diff --git a/packages/app-store/cal-ai/static/2.jpg b/packages/app-store/cal-ai/static/2.jpg deleted file mode 100644 index 050acf433f..0000000000 Binary files a/packages/app-store/cal-ai/static/2.jpg and /dev/null differ diff --git a/packages/app-store/cal-ai/static/3.jpg b/packages/app-store/cal-ai/static/3.jpg deleted file mode 100644 index fab5b1ef18..0000000000 Binary files a/packages/app-store/cal-ai/static/3.jpg and /dev/null differ diff --git a/packages/app-store/cal-ai/static/4.jpg b/packages/app-store/cal-ai/static/4.jpg deleted file mode 100644 index 0b0fbbfde9..0000000000 Binary files a/packages/app-store/cal-ai/static/4.jpg and /dev/null differ diff --git a/packages/app-store/cal-ai/static/5.jpg b/packages/app-store/cal-ai/static/5.jpg deleted file mode 100644 index ccb8475bdd..0000000000 Binary files a/packages/app-store/cal-ai/static/5.jpg and /dev/null differ diff --git a/packages/app-store/cal-ai/static/icon.png b/packages/app-store/cal-ai/static/icon.png deleted file mode 100644 index c63758d416..0000000000 Binary files a/packages/app-store/cal-ai/static/icon.png and /dev/null differ