Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/polite-steaks-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/community": patch
---

add GoogleCalendarDeleteTool
18 changes: 15 additions & 3 deletions examples/src/langchain-classic/tools/google_calendar.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import { Calculator } from "@langchain/community/tools/calculator";
import {
GoogleCalendarCreateTool,
GoogleCalendarViewTool,
GoogleCalendarDeleteTool,
} from "@langchain/community/tools/google_calendar";
import { createAgent } from "langchain";

export async function run() {
const model = new ChatOpenAI({
Expand All @@ -30,10 +31,11 @@ export async function run() {
new Calculator(),
new GoogleCalendarCreateTool(googleCalendarParams),
new GoogleCalendarViewTool(googleCalendarParams),
new GoogleCalendarDeleteTool(googleCalendarParams),
];

const calendarAgent = createReactAgent({
llm: model,
const calendarAgent = createAgent({
model,
tools,
});

Expand All @@ -56,4 +58,14 @@ export async function run() {
// output: "You have no meetings this week between 8am and 8pm."
// }
console.log("View Result", viewResult);

const deleteInput = `Delete the meeting with John Doe next Friday at 4pm`;

const deleteResult = await calendarAgent.invoke({
messages: [{ role: "user", content: deleteInput }],
});
// Delete Result {
// output: "The meeting with John Doe on 29th September at 4pm has been deleted."
// }
console.log("Delete Result", deleteResult);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { z } from "zod/v3";
import { calendar_v3 } from "googleapis";
import { PromptTemplate } from "@langchain/core/prompts";
import { CallbackManagerForToolRun } from "@langchain/core/callbacks/manager";
import { BaseLanguageModel } from "@langchain/core/language_models/base";
import { DELETE_EVENT_PROMPT } from "../prompts/index.js";
import { getTimezoneOffsetInHours } from "../utils/get-timezone-offset-in-hours.js";

const deleteEventSchema = z.object({
event_id: z.string().optional(),
event_summary: z.string().optional(),
time_min: z.string().optional(),
time_max: z.string().optional(),
user_timezone: z.string(),
});

const deleteEvent = async (
calendarId: string,
eventId: string,
calendar: calendar_v3.Calendar
) => {
try {
await calendar.events.delete({
calendarId,
eventId,
});

return "Event deleted successfully";
} catch (error) {
return `An error occurred: ${error}`;
}
};

type RunDeleteEventParams = {
calendarId: string;
calendar: calendar_v3.Calendar;
model: BaseLanguageModel;
};

const runDeleteEvent = async (
query: string,
{ calendarId, calendar, model }: RunDeleteEventParams,
runManager?: CallbackManagerForToolRun
) => {
const prompt = new PromptTemplate({
template: DELETE_EVENT_PROMPT,
inputVariables: ["date", "query", "u_timezone", "dayName"],
});

if (!model?.withStructuredOutput) {
throw new Error("Model does not support structured output");
}

const deleteEventChain = prompt.pipe(
model.withStructuredOutput(deleteEventSchema)
);

const date = new Date().toISOString();
const u_timezone = getTimezoneOffsetInHours();
const dayName = new Date().toLocaleString("en-us", { weekday: "long" });

const output = await deleteEventChain.invoke(
{
query,
date,
u_timezone,
dayName,
},
runManager?.getChild()
);

const { event_id, event_summary, time_min, time_max } = output;

if (event_id) {
return deleteEvent(calendarId, event_id, calendar);
}

if (event_summary || (time_min && time_max)) {
try {
const response = await calendar.events.list({
calendarId,
timeMin: time_min,
timeMax: time_max,
q: event_summary,
singleEvents: true,
});

const items = response.data.items || [];

if (items.length === 0) {
return "No events found matching the description.";
}

if (items.length === 1 && items[0].id) {
return deleteEvent(calendarId, items[0].id, calendar);
}

if (items.length > 1) {
const eventsList = items
.map(
(event) =>
`- ${event.summary} (${
event.start?.dateTime || event.start?.date
})`
)
.join("\n");
return `Multiple events found. Please be more specific:\n${eventsList}`;
}
} catch (error) {
return `An error occurred while searching for the event: ${error}`;
}
}

return "Could not extract event details to delete. Please provide an event ID or a description with time.";
};

export { runDeleteEvent };
51 changes: 51 additions & 0 deletions libs/langchain-community/src/tools/google_calendar/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { CallbackManagerForToolRun } from "@langchain/core/callbacks/manager";
import { GoogleCalendarBase, GoogleCalendarAgentParams } from "./base.js";
import { runDeleteEvent } from "./commands/run-delete-events.js";
import { DELETE_TOOL_DESCRIPTION } from "./descriptions.js";

/**
* @example
* ```typescript
* const googleCalendarDeleteTool = new GoogleCalendarDeleteTool({
* credentials: {
* clientEmail: process.env.GOOGLE_CALENDAR_CLIENT_EMAIL,
* privateKey: process.env.GOOGLE_CALENDAR_PRIVATE_KEY,
* calendarId: process.env.GOOGLE_CALENDAR_CALENDAR_ID,
* },
* scopes: [
* "https://www.googleapis.com/auth/calendar",
* "https://www.googleapis.com/auth/calendar.events",
* ],
* model: new ChatOpenAI({ model: "gpt-4o-mini" }),
* });
* const deleteInput = `Delete the meeting with John at 3pm`;
* const deleteResult = await googleCalendarDeleteTool.invoke({
* input: deleteInput,
* });
* console.log("Delete Result", deleteResult);
* ```
*/
export class GoogleCalendarDeleteTool extends GoogleCalendarBase {
name = "google_calendar_delete";

description = DELETE_TOOL_DESCRIPTION;

constructor(fields: GoogleCalendarAgentParams) {
super(fields);
}

async _call(query: string, runManager?: CallbackManagerForToolRun) {
const calendar = await this.getCalendarClient();
const model = this.getModel();

return runDeleteEvent(
query,
{
calendar,
model,
calendarId: this.calendarId,
},
runManager
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ INPUT examples:
OUTPUT:
- title, start time, end time, attendees, description (if available)
`;

export const DELETE_TOOL_DESCRIPTION = `A tool for deleting Google Calendar events and meetings.
INPUT example:
"action": "google_calendar_delete",
"action_input": "delete the meeting with John at 3pm"

OUTPUT:
Output is a confirmation of a deleted event.
`;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { GoogleCalendarCreateTool } from "./create.js";
export { GoogleCalendarViewTool } from "./view.js";
export { GoogleCalendarDeleteTool } from "./delete.js";
export type { GoogleCalendarAgentParams } from "./base.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const DELETE_EVENT_PROMPT = `
Date format: YYYY-MM-DDThh:mm:ss+00:00
Based on this event description: "{query}", output a JSON string of the
following parameters. Do not include any other text or comments:
Today's datetime on UTC time {date}, it's {dayName} and timezone of the user {u_timezone},
take into account the timezone of the user and today's date.
1. event_id
2. event_summary
3. time_min
4. time_max
5. user_timezone
output:
`;
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { CREATE_EVENT_PROMPT } from "./create-event-prompt.js";
export { VIEW_EVENTS_PROMPT } from "./view-events-prompt.js";
export { DELETE_EVENT_PROMPT } from "./delete-event-prompt.js";
59 changes: 51 additions & 8 deletions libs/langchain-community/src/tools/tests/google_calendar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ChatResult } from "@langchain/core/outputs";
import {
GoogleCalendarCreateTool,
GoogleCalendarViewTool,
GoogleCalendarDeleteTool,
} from "../google_calendar/index.js";

jest.mock("googleapis", () => ({
Expand All @@ -18,14 +19,6 @@ jest.mock("@langchain/core/utils/env", () => ({
getEnvironmentVariable: () => "key",
}));

// jest.mock("../google_calendar/commands/run-create-events.js", () => ({
// runCreateEvent: jest.fn(),
// }));

// jest.mock("../google_calendar/commands/run-view-events.js", () => ({
// runViewEvents: jest.fn(),
// }));

class FakeLLM extends BaseChatModel {
_llmType() {
return "fake";
Expand Down Expand Up @@ -148,3 +141,53 @@ describe("GoogleCalendarViewTool", () => {
);
});
});

describe("GoogleCalendarDeleteTool", () => {
it("should be setup with correct parameters", async () => {
const params = {
credentials: {
clientEmail: "[email protected]",
privateKey: "privateKey",
calendarId: "calendarId",
},
model: new FakeLLM({}),
};

const instance = new GoogleCalendarDeleteTool(params);
expect(instance.name).toBe("google_calendar_delete");
});

it("should be setup with accessToken", async () => {
const params = {
credentials: {
accessToken: "accessToken",
calendarId: "calendarId",
},
model: new FakeLLM({}),
};

const instance = new GoogleCalendarDeleteTool(params);
expect(instance.name).toBe("google_calendar_delete");
});

it("should throw an error if missing credentials", async () => {
const params = {
credentials: {},
model: new FakeLLM({}),
};
expect(() => new GoogleCalendarDeleteTool(params)).toThrow(
"Missing GOOGLE_CALENDAR_CLIENT_EMAIL to interact with Google Calendar"
);
});

it("should throw an error if missing model", async () => {
const params = {
credentials: {
clientEmail: "test",
},
};
expect(() => new GoogleCalendarDeleteTool(params)).toThrow(
"Missing llm instance to interact with Google Calendar"
);
});
});
Loading