This commit is contained in:
Tat Dat Duong
2025-05-13 11:57:29 -07:00
parent 6ec0aaa141
commit 048c5f571e
4 changed files with 31 additions and 30 deletions
+18 -17
View File
@@ -2,14 +2,13 @@
This repository contains implementations of AI email assistants built using LangGraph.js, a library for building stateful, multi-actor applications with LLMs. It demonstrates how to create, test, and add features like Human-in-the-Loop (HITL) and persistent memory to an AI agent.
### Prerequisites
- Install the latest version of [JupyterLab or Jupyter Notebook](https://jupyter.org/install)
- Install [TS-Lab Typescript Kernel for Notebooks](https://github.com/yunabe/tslab#display-rich-objects)
- Node.js (v18 or higher recommended)
- A package manager (npm, yarn, or pnpm)
### 1. Install Dependencies
Clone the repository and install the necessary packages:
@@ -24,11 +23,12 @@ pnpm install
### 2. Environment Setup
Copy the `.env.example` file to `.env` file in the root of the project and add your OpenAI API key:
Copy the `.env.example` file to `.env` file in the root of the project and add your OpenAI API key:
```
OPENAI_API_KEY=your_api_key_here
```
it is recommended to also add your langsmith api key, and langsmith_project name to be able to analyze your graph traces in Langsmith.
### 3. Run the Agent
@@ -43,24 +43,25 @@ yarn agent
pnpm agent
```
### Typescript Notebooks
multiple interactive Typescript Jupyter notebooks to go through the process of creating these agentic features.
multiple interactive Typescript Jupyter notebooks to go through the process of creating these agentic features.
- `notebooks/langgraph_101.ipynb`
- `notebooks/agent.ipynb`
- `notebooks/hitl.ipynb`
- `notebooks/memory.ipynb`
- `notebooks/hitl.ipynb`
- `notebooks/memory.ipynb`
### Email Assistant Scripts
### Email Assistant Scripts
Three versions of the email assistant are available in the `src/` directory:
1. `email_assistant.ts`: A basic email assistant for triage and response.
2. `email_assistant_hitl.ts`: Extends the basic assistant with Human-in-the-Loop capabilities for reviewing and intervening in the agent's actions.
3. `email_assistant_hitl_memory.ts`: Further extends the HITL assistant with persistent memory to learn from user feedback and preferences.
There are also The notebooks assemble the agents in `src/` step by step. You can use **ts-lab kernel instead of the default python kernel** to execute the notebook code cells
## Table of Contents
- [Getting Started](#getting-started)
@@ -79,9 +80,6 @@ There are also The notebooks assemble the agents in `src/` step by step. You can
- [Testing Memory Features](#testing-memory-features)
- [Development & Debugging](#development--debugging)
## Project Structure
- `src/`: Contains the core TypeScript source code for the email assistants.
@@ -98,10 +96,10 @@ There are also The notebooks assemble the agents in `src/` step by step. You can
- `tsconfig.json`: TypeScript configuration.
- `.env`: Environment variables (gitignored).
- `notebooks/` contains the interactive notebooks for the email assistants
`0_langgraph_101.ipynb` - langgraph fundamentals
`1_agent.ipynb` - agentic email assistant
`2_hitl.ipynb` - Human in the Loop, Interrupts notebook
`3_memory.ipynb` - Human in the loop, with memory notebook
`0_langgraph_101.ipynb` - langgraph fundamentals
`1_agent.ipynb` - agentic email assistant
`2_hitl.ipynb` - Human in the Loop, Interrupts notebook
`3_memory.ipynb` - Human in the loop, with memory notebook
## Core Concepts & Workflow
@@ -151,6 +149,7 @@ The email assistants are built as stateful graphs using LangGraph.js.
Implemented in `email_assistant_hitl.ts` and `email_assistant_hitl_memory.ts`.
Interactive Notebooks available in `2_hitl.ipynb` and `3_memory.ipynb`
- **Interrupts**: The graph execution pauses at critical junctures:
- After triage if an email is marked `notify` (`triage_interrupt_handler` node).
- Before executing certain tool calls within the `response_agent` (`interrupt_handler` node).
@@ -200,6 +199,7 @@ Use this JSON structure as input when testing your agent in the studio:
### Understanding Node Outputs
#### Triage Router Node (`triage_router`)
Classifies emails. Expect output like:
```json
@@ -216,6 +216,7 @@ Classifies emails. Expect output like:
```
#### Response Agent Nodes (e.g., `llm_call`, `interrupt_handler`)
Handles response generation.
- `llm_call` output might include tool calls:
+3 -3
View File
@@ -32,7 +32,7 @@ ${content}
description:
"Write an email draft based on provided information. Use this when the user wants to compose a new email message.",
schema: emailSchema,
}
},
);
/**
@@ -76,7 +76,7 @@ Recommended action: ${priority === "High" ? "Respond immediately" : "Review when
description:
"Analyze and categorize an email by importance and type. Use this when evaluating how to handle incoming messages.",
schema: triageSchema,
}
},
);
/**
@@ -99,5 +99,5 @@ export const Done = tool(
description:
"Signal that you've completed the current task and no further actions are needed.",
schema: doneSchema,
}
},
);
+4 -4
View File
@@ -20,7 +20,7 @@ export const backgroundTool = tool(
name: "background",
description: "Get background information about the user",
schema: z.object({}).describe("This tool doesn't take any arguments"),
}
},
);
/**
@@ -36,7 +36,7 @@ export const calPreferencesTool = tool(
name: "cal_preferences",
description: "Get the user's calendar preferences",
schema: z.object({}).describe("This tool doesn't take any arguments"),
}
},
);
/**
@@ -52,7 +52,7 @@ export const responsePreferencesTool = tool(
name: "response_preferences",
description: "Get the user's response style preferences",
schema: z.object({}).describe("This tool doesn't take any arguments"),
}
},
);
/**
@@ -68,5 +68,5 @@ export const questionTool = tool(
schema: z.object({
content: z.string().describe("The question to ask the user"),
}),
}
},
);
+6 -6
View File
@@ -58,7 +58,7 @@ export function formatEmailMarkdown(
subject: string,
author: string,
to: string,
emailThread: string
emailThread: string,
): string {
return `## Email: ${subject}
@@ -206,13 +206,13 @@ export function extractToolCalls(messages: any[]): string[] {
// Handle plain objects
if (message.tool_calls && Array.isArray(message.tool_calls)) {
toolCallNames.push(
...message.tool_calls.map((call: any) => call.name.toLowerCase())
...message.tool_calls.map((call: any) => call.name.toLowerCase()),
);
}
// Handle class instances with toolCalls property
else if ("toolCalls" in message && Array.isArray(message.toolCalls)) {
toolCallNames.push(
...message.toolCalls.map((call: any) => call.name.toLowerCase())
...message.toolCalls.map((call: any) => call.name.toLowerCase()),
);
}
}
@@ -264,7 +264,7 @@ export function formatMessagesString(messages: BaseMessage[]): string {
const toolCallsStr = aiMessage.tool_calls
.map(
(tc: ToolCall) =>
`\n Tool: ${tc.name}\n Args: ${JSON.stringify(tc.args, null, 2)}`
`\n Tool: ${tc.name}\n Args: ${JSON.stringify(tc.args, null, 2)}`,
)
.join("\n");
content += `\n[Tool Calls: ${toolCallsStr}]`;
@@ -283,12 +283,12 @@ export function formatEmailOptional(
subject?: string,
author?: string,
to?: string,
emailThread?: string
emailThread?: string,
): string {
return formatEmailMarkdown(
subject ?? "No Subject",
author ?? "Unknown Sender",
to ?? "Unknown Recipient",
emailThread ?? ""
emailThread ?? "",
);
}