Skip to content

Commit 39abb92

Browse files
author
hfhoffman1144
committed
first langgraph commit
1 parent 11f745c commit 39abb92

File tree

10 files changed

+1432
-0
lines changed

10 files changed

+1432
-0
lines changed

python-langgraph/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# LangGraph: Build Stateful AI Agents in Python
2+
3+
This repo contains the source code for [LangGraph: Build Stateful AI Agents in Python](https://realpython.com/langgraph-build-stateful-ai-agents-in-python/)
4+
5+
## Setup
6+
7+
LangGraph is available on [PyPI](https://pypi.org/), and you can install it with [pip](https://realpython.com/what-is-pip/). Open a terminal or command prompt, create a new virtual environment, and then run the following command to install LangGraph:
8+
9+
```console
10+
(venv) $ python -m pip install langgraph
11+
```
12+
13+
This command will install the latest version of LangGraph from PyPI onto your machine. To verify that the installation was successful, start a [Python REPL](https://realpython.com/python-repl/) and import LangGraph:
14+
15+
```pycon
16+
>>> import langgraph
17+
```
18+
19+
If the import runs without error, then you've successfully installed LangGraph. You'll also need a few more libraries for this tutorial:
20+
21+
```console
22+
(venv) $ python -m pip install langchain-openai pydantic[email]
23+
```
24+
25+
You'll use `langchain-openai` to interact with OpenAI LLMs, but keep in mind you can use any LLM provider you like with LangGraph and LangChain. You'll use [`pydantic`](https://realpython.com/python-pydantic/) to validate the information your agent parses from emails.
26+
27+
Before moving forward, if you choose to use OpenAI, make sure you're signed up for an OpenAI account and you have a valid [API key](https://openai.com/api/). You'll need to set the following [environment variable](https://en.wikipedia.org/wiki/Environment_variable) before running any examples in this tutorial:
28+
29+
```dotenv
30+
OPENAI_API_KEY=<YOUR-OPENAI-API-KEY>
31+
```
32+
33+
Note that while LangGraph was made by the creators of LangChain, and the two libraries are highly compatible, it is possible to use LangGraph without LangChain. However, it's more common to use LangChain and LangGraph together, and you'll see throughout this tutorial how they compliment each other.
34+
35+
With that, you've installed all the dependencies you'll need for this tutorial, and you're ready to create your LangGraph email processor. You'll start by going through an overview of LangChain chains and exploring LangGraph's core concept - the state graph.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from langchain_openai import ChatOpenAI
2+
from langchain_core.prompts import ChatPromptTemplate
3+
from pydantic import BaseModel, Field
4+
5+
6+
class BinaryAnswer(BaseModel):
7+
is_true: bool = Field(
8+
description="""Whether the answer to the question is yes or no.
9+
True if yes otherwise false."""
10+
)
11+
12+
13+
binary_question_prompt = ChatPromptTemplate.from_messages(
14+
[
15+
(
16+
"system",
17+
"""
18+
Answer this question as True for "yes" and False for "no".
19+
No other answers are allowed:
20+
{question}
21+
""",
22+
)
23+
]
24+
)
25+
26+
binary_question_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
27+
28+
BINARY_QUESTION_CHAIN = (
29+
binary_question_prompt
30+
| binary_question_model.with_structured_output(BinaryAnswer)
31+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from langchain_openai import ChatOpenAI
2+
from langchain_core.prompts import ChatPromptTemplate
3+
from pydantic import BaseModel, Field
4+
5+
6+
class EscalationCheck(BaseModel):
7+
needs_escalation: bool = Field(
8+
description="""Whether the notice requires escalation according
9+
to specified criteria"""
10+
)
11+
12+
13+
escalation_prompt = ChatPromptTemplate.from_messages(
14+
[
15+
(
16+
"system",
17+
"""
18+
Determine whether the following notice received from a regulatory
19+
body requires immediate escalation. Immediate escalation is
20+
required when {escalation_criteria}.
21+
Here's the notice message:
22+
{message}
23+
""",
24+
)
25+
]
26+
)
27+
28+
escalation_check_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
29+
30+
ESCALATION_CHECK_CHAIN = (
31+
escalation_prompt
32+
| escalation_check_model.with_structured_output(EscalationCheck)
33+
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from langchain_openai import ChatOpenAI
2+
from langchain_core.prompts import ChatPromptTemplate
3+
from pydantic import BaseModel, EmailStr, Field
4+
from datetime import date
5+
6+
7+
class NoticeEmailExtract(BaseModel):
8+
date_of_notice: date | None = Field(
9+
default=None,
10+
description="""The date of the notice (if any) reformatted
11+
to match YYYY-mm-dd""",
12+
)
13+
entity_name: str | None = Field(
14+
default=None,
15+
description="""The name of the entity sending the notice (if present
16+
in the message)""",
17+
)
18+
entity_phone: str | None = Field(
19+
default=None,
20+
description="""The phone number of the entity sending the notice
21+
(if present in the message)""",
22+
)
23+
entity_email: EmailStr | None = Field(
24+
default=None,
25+
description="""The email of the entity sending the notice
26+
(if present in the message)""",
27+
)
28+
project_id: int | None = Field(
29+
default=None,
30+
description="""The project ID (if present in the message) -
31+
must be an integer""",
32+
)
33+
site_location: str | None = Field(
34+
default=None,
35+
description="""The site location of the project (if present
36+
in the message)""",
37+
)
38+
violation_type: str | None = Field(
39+
default=None,
40+
description="""The type of violation (if present in the
41+
message)""",
42+
)
43+
required_changes: str | None = Field(
44+
default=None,
45+
description="""The required changes specified by the entity
46+
(if present in the message)""",
47+
)
48+
compliance_deadline: date | None = Field(
49+
default=None,
50+
description="""The date that the company must comply (if any)
51+
reformatted to match YYYY-mm-dd""",
52+
)
53+
max_potential_fine: float | None = Field(
54+
default=None,
55+
description="""The maximum potential fine
56+
(if any)""",
57+
)
58+
59+
60+
info_parse_prompt = ChatPromptTemplate.from_messages(
61+
[
62+
(
63+
"system",
64+
"""
65+
Parse the date of notice, sending entity name, sending entity
66+
phone, sending entity email, project id, site location, violation
67+
type, required changes, compliance deadline, and maximum potential
68+
fine from the message. If any of the fields aren't present, don't
69+
populate them. Try to cast dates into the YYYY-mm-dd format. Don't
70+
populate fields if they're not present in the message.
71+
Here's the notice message:
72+
{message}
73+
""",
74+
)
75+
]
76+
)
77+
78+
notice_parser_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
79+
80+
NOTICE_PARSER_CHAIN = (
81+
info_parse_prompt
82+
| notice_parser_model.with_structured_output(NoticeEmailExtract)
83+
)

python-langgraph/example_emails.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
EMAILS = [
2+
# Email 1
3+
"""
4+
5+
Hey Betsy,
6+
Here's your invoice for $1000 for the cookies you ordered.
7+
""",
8+
# Email 2
9+
"""
10+
11+
Hi Paul,
12+
We have an issue with the HVAC system your team installed in
13+
apartment 1235. We'd like to request maintenance or a refund.
14+
Thanks,
15+
Terrance
16+
""",
17+
# Email 3
18+
"""
19+
Date: January 10, 2025
20+
From: City of Los Angeles Building and Safety Department
21+
To: West Coast Development, project 345678123 - Sunset Luxury
22+
Condominiums
23+
Location: Los Angeles, CA
24+
Following an inspection of your site at 456 Sunset Boulevard, we have
25+
identified the following building code violations:
26+
Electrical Wiring: Exposed wiring was found in the underground parking
27+
garage, posing a safety hazard. Fire Safety: Insufficient fire
28+
extinguishers were available across multiple floors of the structure
29+
under construction.
30+
Structural Integrity: The temporary support beams in the eastern wing
31+
do not meet the load-bearing standards specified in local building codes.
32+
Required Corrective Actions:
33+
Replace or properly secure exposed wiring to meet electrical safety
34+
standards. Install additional fire extinguishers in compliance with
35+
fire code requirements. Reinforce or replace temporary support beams
36+
to ensure structural stability. Deadline for Compliance: Violations
37+
must be addressed no later than February 5,
38+
2025. Failure to comply may result in
39+
a stop-work order and additional fines.
40+
Contact: For questions or to schedule a re-inspection, please contact
41+
the Building and Safety Department at
42+
(555) 456-7890 or email [email protected].
43+
""",
44+
]
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import time
2+
import logging
3+
from langchain_core.messages import AIMessage
4+
from langchain_core.tools import tool
5+
from langgraph.prebuilt import ToolNode
6+
from langchain_openai import ChatOpenAI
7+
from langgraph.graph import StateGraph, MessagesState, START, END
8+
from chains.notice_extraction import NoticeEmailExtract
9+
from graphs.notice_extraction import NOTICE_EXTRACTION_GRAPH
10+
11+
logging.basicConfig(
12+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
13+
)
14+
LOGGER = logging.getLogger(__name__)
15+
16+
17+
@tool
18+
def forward_email(email_message: str, send_to_email: str) -> bool:
19+
"""
20+
Forward an email_message to the address of sent_to_email. Returns
21+
true if the email was successful otherwise it wil return false. Note
22+
that this tool only forwards the email to an internal department -
23+
it does not reply to the sender.
24+
"""
25+
26+
LOGGER.info(f"Forwarding the email to {send_to_email}...")
27+
time.sleep(2)
28+
LOGGER.info("Email forwarded!")
29+
30+
return True
31+
32+
33+
@tool
34+
def send_wrong_email_notification_to_sender(
35+
sender_email: str, correct_department: str
36+
):
37+
"""
38+
Send an email back to the sender informing them that
39+
they have the wrong address. The email should be sent
40+
to the correct_department.
41+
"""
42+
43+
LOGGER.info(f"Sending wrong email notification to {sender_email}...")
44+
time.sleep(2)
45+
LOGGER.info("Email sent!")
46+
47+
return True
48+
49+
50+
@tool
51+
def extract_notice_data(
52+
email: str, escalation_criteria: str
53+
) -> NoticeEmailExtract:
54+
"""
55+
Extract structured fields from a regulatory notice.
56+
This should be used when the email message comes from
57+
a regulatory body or auditor regarding a property or
58+
construction site that the company works on.
59+
escalation_criteria is a description of which kinds of
60+
notices require immediate escalation.
61+
After calling this tool, you don't need to call any others.
62+
"""
63+
64+
LOGGER.info("Calling the email notice extraction graph...")
65+
66+
initial_state = {
67+
"notice_message": email,
68+
"notice_email_extract": None,
69+
"critical_fields_missing": False,
70+
"escalation_text_criteria": escalation_criteria,
71+
"escalation_dollar_criteria": 100_000,
72+
"requires_escalation": False,
73+
"escalation_emails": ["[email protected]", "[email protected]"],
74+
}
75+
76+
results = NOTICE_EXTRACTION_GRAPH.invoke(initial_state)
77+
return results["notice_email_extract"]
78+
79+
80+
@tool
81+
def determine_email_action(email: str) -> str:
82+
"""
83+
Call to determine which action should be taken
84+
for an email. Only use when the other tools don't seem
85+
relevant for the email task. Do not call this tool if
86+
you've already called extract_notice_data.
87+
"""
88+
89+
instructions = """
90+
If the email appears to be an invoice of any kind or related to
91+
billing, forward the email to the billing and invoices team:
92+
93+
and send a wrong email notice back to the sender. The correct department is
94+
95+
If the email appears to be from a customer, forward to [email protected],
96+
[email protected], and [email protected]. Be sure to forward it to all three
97+
emails listed.
98+
Send a wrong email notice back to the
99+
customer and let them know the correct department is [email protected].
100+
For any other emails, please send a wrong email notification and try to
101+
infer the correct department from one of [email protected],
102+
103+
104+
"""
105+
106+
return instructions
107+
108+
109+
tools = [
110+
determine_email_action,
111+
forward_email,
112+
send_wrong_email_notification_to_sender,
113+
extract_notice_data,
114+
]
115+
tool_node = ToolNode(tools)
116+
117+
EMAIL_AGENT_MODEL = ChatOpenAI(model="gpt-4o-mini", temperature=0).bind_tools(
118+
tools
119+
)
120+
121+
122+
def call_agent_model_node(state: MessagesState) -> dict[str, list[AIMessage]]:
123+
"""Node to call the email agent model"""
124+
messages = state["messages"]
125+
response = EMAIL_AGENT_MODEL.invoke(messages)
126+
return {"messages": [response]}
127+
128+
129+
def route_agent_graph_edge(state: MessagesState) -> str:
130+
"""Determine whether to call more tools or exit the graph"""
131+
last_message = state["messages"][-1]
132+
if last_message.tool_calls:
133+
return "email_tools"
134+
return END
135+
136+
137+
workflow = StateGraph(MessagesState)
138+
139+
workflow.add_node("email_agent", call_agent_model_node)
140+
workflow.add_node("email_tools", tool_node)
141+
142+
workflow.add_edge(START, "email_agent")
143+
workflow.add_conditional_edges(
144+
"email_agent", route_agent_graph_edge, ["email_tools", END]
145+
)
146+
workflow.add_edge("email_tools", "email_agent")
147+
148+
email_agent_graph = workflow.compile()

0 commit comments

Comments
 (0)