generated from langchain-ai/new-langgraphjs-project
-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
74d1398
commit ac4488f
Showing
17 changed files
with
726 additions
and
190 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,104 +1,50 @@ | ||
/** | ||
* Starter LangGraph.js Template | ||
* Make this code your own! | ||
*/ | ||
import { StateGraph } from "@langchain/langgraph"; | ||
import { RunnableConfig } from "@langchain/core/runnables"; | ||
import { StateAnnotation } from "./state.js"; | ||
import { END, START, StateGraph } from "@langchain/langgraph"; | ||
import { GraphAnnotation } from "./state.js"; | ||
import { ingestData } from "./nodes/ingest-data.js"; | ||
import { generateLinkedinPost } from "./nodes/generate-post/linkedin.js"; | ||
import { generateTwitterPost } from "./nodes/generate-post/twitter.js"; | ||
import { identifyContentGraph } from "./subgraphs/identify-content/graph.js"; | ||
import { schedulePosts } from "./nodes/schedule-posts.js"; | ||
|
||
/** | ||
* Define a node, these do the work of the graph and should have most of the logic. | ||
* Must return a subset of the properties set in StateAnnotation. | ||
* @param state The current state of the graph. | ||
* @param config Extra parameters passed into the state graph. | ||
* @returns Some subset of parameters of the graph state, used to update the state | ||
* for the edges and nodes executed next. | ||
*/ | ||
const callModel = async ( | ||
state: typeof StateAnnotation.State, | ||
_config: RunnableConfig, | ||
): Promise<typeof StateAnnotation.Update> => { | ||
/** | ||
* Do some work... (e.g. call an LLM) | ||
* For example, with LangChain you could do something like: | ||
* | ||
* ```bash | ||
* $ npm i @langchain/anthropic | ||
* ``` | ||
* | ||
* ```ts | ||
* import { ChatAnthropic } from "@langchain/anthropic"; | ||
* const model = new ChatAnthropic({ | ||
* model: "claude-3-5-sonnet-20240620", | ||
* apiKey: process.env.ANTHROPIC_API_KEY, | ||
* }); | ||
* const res = await model.invoke(state.messages); | ||
* ``` | ||
* | ||
* Or, with an SDK directly: | ||
* | ||
* ```bash | ||
* $ npm i openai | ||
* ``` | ||
* | ||
* ```ts | ||
* import OpenAI from "openai"; | ||
* const openai = new OpenAI({ | ||
* apiKey: process.env.OPENAI_API_KEY, | ||
* }); | ||
* | ||
* const chatCompletion = await openai.chat.completions.create({ | ||
* messages: [{ | ||
* role: state.messages[0]._getType(), | ||
* content: state.messages[0].content, | ||
* }], | ||
* model: "gpt-4o-mini", | ||
* }); | ||
* ``` | ||
*/ | ||
console.log("Current state:", state); | ||
return { | ||
messages: [ | ||
{ | ||
role: "assistant", | ||
content: `Hi there! How are you?`, | ||
}, | ||
], | ||
}; | ||
}; | ||
|
||
/** | ||
* Routing function: Determines whether to continue research or end the builder. | ||
* This function decides if the gathered information is satisfactory or if more research is needed. | ||
* | ||
* @param state - The current state of the research builder | ||
* @returns Either "callModel" to continue research or END to finish the builder | ||
*/ | ||
export const route = ( | ||
state: typeof StateAnnotation.State, | ||
): "__end__" | "callModel" => { | ||
if (state.messages.length > 0) { | ||
return "__end__"; | ||
function routeAfterIdentifyContent( | ||
state: typeof GraphAnnotation.State, | ||
): "generateLinkedinPost" | typeof END { | ||
if (state.relevantProducts && state.relevantProducts.length > 0) { | ||
return "generateLinkedinPost"; | ||
} | ||
// Loop back | ||
return "callModel"; | ||
}; | ||
return END; | ||
} | ||
|
||
const builder = new StateGraph(GraphAnnotation) | ||
// Ingests posts from Slack channel. | ||
.addNode("ingestData", ingestData) | ||
// Subgraph which identifies content is relevant to LangChain products, | ||
// and if so it generates a report on the content. | ||
.addNode("identifyContent", identifyContentGraph) | ||
// Generates a post on the content for LinkedIn. | ||
.addNode("generateLinkedinPost", generateLinkedinPost) | ||
// Generates a post on the content for Twitter. | ||
.addNode("generateTwitterPost", generateTwitterPost) | ||
// Interrupts the node for human in the loop, then schedules the | ||
// post for Twitter/LinkedIn. | ||
.addNode("schedulePosts", schedulePosts) | ||
|
||
// Finally, create the graph itself. | ||
const builder = new StateGraph(StateAnnotation) | ||
// Add the nodes to do the work. | ||
// Chaining the nodes together in this way | ||
// updates the types of the StateGraph instance | ||
// so you have static type checking when it comes time | ||
// to add the edges. | ||
.addNode("callModel", callModel) | ||
// Regular edges mean "always transition to node B after node A is done" | ||
// The "__start__" and "__end__" nodes are "virtual" nodes that are always present | ||
// and represent the beginning and end of the builder. | ||
.addEdge("__start__", "callModel") | ||
// Conditional edges optionally route to different nodes (or end) | ||
.addConditionalEdges("callModel", route); | ||
// Start node | ||
.addEdge(START, "ingestData") | ||
.addEdge("ingestData", "identifyContent") | ||
// Route to post generators or end node. | ||
.addConditionalEdges("identifyContent", routeAfterIdentifyContent, [ | ||
"generateLinkedinPost", | ||
END, | ||
]) | ||
// After generating the LinkedIn post, generate the Twitter post. | ||
.addEdge("generateLinkedinPost", "generateTwitterPost") | ||
// Finally, schedule the post. This will also throw an interrupt | ||
// so a human can edit the post before scheduling. | ||
.addEdge("generateTwitterPost", "schedulePosts") | ||
// Finish after generating the Twitter post. | ||
.addEdge("schedulePosts", END); | ||
|
||
export const graph = builder.compile(); | ||
|
||
graph.name = "New Agent"; | ||
graph.name = "Social Media Agent"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { LangGraphRunnableConfig } from "@langchain/langgraph"; | ||
import { GraphAnnotation } from "../../state.js"; | ||
|
||
export async function generateLinkedinPost( | ||
_state: typeof GraphAnnotation.State, | ||
_config: LangGraphRunnableConfig, | ||
): Promise<Partial<typeof GraphAnnotation.State>> { | ||
throw new Error("Not implemented"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { LangGraphRunnableConfig } from "@langchain/langgraph"; | ||
import { GraphAnnotation } from "../../state.js"; | ||
|
||
export async function generateTwitterPost( | ||
_state: typeof GraphAnnotation.State, | ||
_config: LangGraphRunnableConfig, | ||
): Promise<Partial<typeof GraphAnnotation.State>> { | ||
throw new Error("Not implemented"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { GraphAnnotation } from "../state.js"; | ||
import { LangGraphRunnableConfig } from "@langchain/langgraph"; | ||
import { SlackMessageFetcher } from "../../clients/slack.js"; | ||
import { extractUrlsFromSlackText } from "../utils.js"; | ||
|
||
const getChannelIdFromConfig = async ( | ||
config: LangGraphRunnableConfig, | ||
): Promise<string | undefined> => { | ||
if (config.configurable?.slack.channelName) { | ||
const client = new SlackMessageFetcher({ | ||
channelName: config.configurable.slack.channelName, | ||
}); | ||
return await client.getChannelId(); | ||
} | ||
return config.configurable?.slack.channelId; | ||
}; | ||
|
||
export async function ingestData( | ||
_state: typeof GraphAnnotation.State, | ||
config: LangGraphRunnableConfig, | ||
): Promise<Partial<typeof GraphAnnotation.State>> { | ||
const channelId = await getChannelIdFromConfig(config); | ||
if (!channelId) { | ||
throw new Error("Channel ID not found"); | ||
} | ||
|
||
const client = new SlackMessageFetcher({ | ||
channelId: channelId, | ||
}); | ||
|
||
const recentMessages = await client.fetchLast24HoursMessages(); | ||
const messagesWithUrls = recentMessages.flatMap((msg) => { | ||
const links = extractUrlsFromSlackText(msg.text); | ||
if (!links.length) { | ||
return []; | ||
} | ||
return { | ||
...msg, | ||
links, | ||
}; | ||
}); | ||
|
||
return { | ||
slackMessages: messagesWithUrls, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { LangGraphRunnableConfig } from "@langchain/langgraph"; | ||
import { GraphAnnotation } from "../state.js"; | ||
|
||
export async function schedulePosts( | ||
_state: typeof GraphAnnotation.State, | ||
_config: LangGraphRunnableConfig, | ||
): Promise<Partial<typeof GraphAnnotation.State>> { | ||
// Call `interrupt` here first | ||
throw new Error("Not implemented"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,59 +1,33 @@ | ||
import { BaseMessage, BaseMessageLike } from "@langchain/core/messages"; | ||
import { Annotation, messagesStateReducer } from "@langchain/langgraph"; | ||
import { Annotation, MessagesAnnotation } from "@langchain/langgraph"; | ||
import { SimpleSlackMessage } from "../clients/slack.js"; | ||
|
||
/** | ||
* A graph's StateAnnotation defines three main things: | ||
* 1. The structure of the data to be passed between nodes (which "channels" to read from/write to and their types) | ||
* 2. Default values for each field | ||
* 3. Reducers for the state's. Reducers are functions that determine how to apply updates to the state. | ||
* See [Reducers](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#reducers) for more information. | ||
*/ | ||
export type LangChainProduct = "langchain" | "langgraph" | "langsmith"; | ||
export type SimpleSlackMessageWithLinks = SimpleSlackMessage & { | ||
links: string[]; | ||
}; | ||
|
||
// This is the primary state of your agent, where you can store any information | ||
export const StateAnnotation = Annotation.Root({ | ||
export const GraphAnnotation = Annotation.Root({ | ||
...MessagesAnnotation.spec, | ||
/** | ||
* Messages track the primary execution state of the agent. | ||
* | ||
* Typically accumulates a pattern of: | ||
* | ||
* 1. HumanMessage - user input | ||
* 2. AIMessage with .tool_calls - agent picking tool(s) to use to collect | ||
* information | ||
* 3. ToolMessage(s) - the responses (or errors) from the executed tools | ||
* | ||
* (... repeat steps 2 and 3 as needed ...) | ||
* 4. AIMessage without .tool_calls - agent responding in unstructured | ||
* format to the user. | ||
* | ||
* 5. HumanMessage - user responds with the next conversational turn. | ||
* | ||
* (... repeat steps 2-5 as needed ... ) | ||
* | ||
* Merges two lists of messages or message-like objects with role and content, | ||
* updating existing messages by ID. | ||
* | ||
* Message-like objects are automatically coerced by `messagesStateReducer` into | ||
* LangChain message classes. If a message does not have a given id, | ||
* LangGraph will automatically assign one. | ||
* | ||
* By default, this ensures the state is "append-only", unless the | ||
* new message has the same ID as an existing message. | ||
* | ||
* Returns: | ||
* A new list of messages with the messages from \`right\` merged into \`left\`. | ||
* If a message in \`right\` has the same ID as a message in \`left\`, the | ||
* message from \`right\` will replace the message from \`left\`.` | ||
* The Slack messages to use for the content. | ||
*/ | ||
messages: Annotation<BaseMessage[], BaseMessageLike[]>({ | ||
reducer: messagesStateReducer, | ||
default: () => [], | ||
}), | ||
slackMessages: Annotation<SimpleSlackMessageWithLinks[]>, | ||
/** | ||
* Feel free to add additional attributes to your state as needed. | ||
* Common examples include retrieved documents, extracted entities, API connections, etc. | ||
* | ||
* For simple fields whose value should be overwritten by the return value of a node, | ||
* you don't need to define a reducer or default. | ||
* The LangChain product(s) this content is relevant to. | ||
* Undefined if it is not relevant to any product. | ||
*/ | ||
// additionalField: Annotation<string>, | ||
relevantProducts: Annotation<LangChainProduct[] | undefined>, | ||
/** | ||
* A report generated on the content. Will be used in the main | ||
* graph when generating the post about this content. | ||
*/ | ||
report: Annotation<string>, | ||
/** | ||
* The content of the linkedin post. | ||
*/ | ||
linkedinPost: Annotation<string>, | ||
/** | ||
* The content of the tweet. | ||
*/ | ||
twitterPost: Annotation<string>, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { END, START, StateGraph } from "@langchain/langgraph"; | ||
import { GraphAnnotation } from "./state.js"; | ||
import { verifyLangChainContent } from "./nodes/verify-langchain-content.js"; | ||
import { generateContentReport } from "./nodes/generate-content-report.js"; | ||
|
||
function routeIsLangChainContent( | ||
state: typeof GraphAnnotation.State, | ||
): "generateContentReport" | typeof END { | ||
if (state.relevantProducts && state.relevantProducts.length > 0) { | ||
return "generateContentReport"; | ||
} | ||
return END; | ||
} | ||
|
||
// Finally, create the graph itself. | ||
const identifyContentBuilder = new StateGraph(GraphAnnotation) | ||
.addNode("verifyLangChainContent", verifyLangChainContent) | ||
.addNode("generateContentReport", generateContentReport) | ||
// Start node | ||
.addEdge(START, "verifyLangChainContent") | ||
// Router conditional edge | ||
.addConditionalEdges("verifyLangChainContent", routeIsLangChainContent, [ | ||
"generateContentReport", | ||
END, | ||
]) | ||
// End graph after generating report. | ||
.addEdge("generateContentReport", END); | ||
|
||
export const identifyContentGraph = identifyContentBuilder.compile(); | ||
|
||
identifyContentGraph.name = "Identify Content Subgraph"; |
9 changes: 9 additions & 0 deletions
9
src/agent/subgraphs/identify-content/nodes/generate-content-report.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { LangGraphRunnableConfig } from "@langchain/langgraph"; | ||
import { GraphAnnotation } from "../state.js"; | ||
|
||
export async function generateContentReport( | ||
_state: typeof GraphAnnotation.State, | ||
_config: LangGraphRunnableConfig, | ||
): Promise<Partial<typeof GraphAnnotation.State>> { | ||
throw new Error("Not implemented"); | ||
} |
9 changes: 9 additions & 0 deletions
9
src/agent/subgraphs/identify-content/nodes/verify-langchain-content.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { LangGraphRunnableConfig } from "@langchain/langgraph"; | ||
import { GraphAnnotation } from "../state.js"; | ||
|
||
export async function verifyLangChainContent( | ||
_state: typeof GraphAnnotation.State, | ||
_config: LangGraphRunnableConfig, | ||
): Promise<Partial<typeof GraphAnnotation.State>> { | ||
throw new Error("Not implemented"); | ||
} |
Oops, something went wrong.