Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first langgraph commit #631

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
23 changes: 23 additions & 0 deletions python-langgraph/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# LangGraph: Build Stateful AI Agents in Python

This repo contains the source code for [LangGraph: Build Stateful AI Agents in Python](https://realpython.com/langgraph-build-stateful-ai-agents-in-python/)

## Setup

Create a new virtual environment, and run the following command to install LangGraph and the additional requirements for this project:

```console
(venv) $ python -m pip install -r requirements.txt
```

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.

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:

```dotenv
OPENAI_API_KEY=<YOUR-OPENAI-API-KEY>
```

## Usage

TODO: A short note on how to run the project. You can tell them to go to the tutorial for more information.
martin-martin marked this conversation as resolved.
Show resolved Hide resolved
31 changes: 31 additions & 0 deletions python-langgraph/chains/binary_questions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field


class BinaryAnswer(BaseModel):
is_true: bool = Field(
description="""Whether the answer to the question is yes or no.
True if yes otherwise false."""
)


binary_question_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""
Answer this question as True for "yes" and False for "no".
No other answers are allowed:
{question}
""",
)
]
)

binary_question_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

BINARY_QUESTION_CHAIN = (
binary_question_prompt
| binary_question_model.with_structured_output(BinaryAnswer)
)
33 changes: 33 additions & 0 deletions python-langgraph/chains/escalation_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field


class EscalationCheck(BaseModel):
needs_escalation: bool = Field(
description="""Whether the notice requires escalation according
to specified criteria"""
)


escalation_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""
Determine whether the following notice received from a regulatory
body requires immediate escalation. Immediate escalation is
required when {escalation_criteria}.
Here's the notice message:
{message}
""",
)
]
)

escalation_check_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

ESCALATION_CHECK_CHAIN = (
escalation_prompt
| escalation_check_model.with_structured_output(EscalationCheck)
)
84 changes: 84 additions & 0 deletions python-langgraph/chains/notice_extraction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from datetime import date

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, EmailStr, Field


class NoticeEmailExtract(BaseModel):
date_of_notice: date | None = Field(
default=None,
description="""The date of the notice (if any) reformatted
to match YYYY-mm-dd""",
)
entity_name: str | None = Field(
default=None,
description="""The name of the entity sending the notice (if present
in the message)""",
)
entity_phone: str | None = Field(
default=None,
description="""The phone number of the entity sending the notice
(if present in the message)""",
)
entity_email: EmailStr | None = Field(
default=None,
description="""The email of the entity sending the notice
(if present in the message)""",
)
project_id: int | None = Field(
default=None,
description="""The project ID (if present in the message) -
must be an integer""",
)
site_location: str | None = Field(
default=None,
description="""The site location of the project (if present
in the message)""",
)
violation_type: str | None = Field(
default=None,
description="""The type of violation (if present in the
message)""",
)
required_changes: str | None = Field(
default=None,
description="""The required changes specified by the entity
(if present in the message)""",
)
compliance_deadline: date | None = Field(
default=None,
description="""The date that the company must comply (if any)
reformatted to match YYYY-mm-dd""",
)
max_potential_fine: float | None = Field(
default=None,
description="""The maximum potential fine
(if any)""",
)


info_parse_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""
Parse the date of notice, sending entity name, sending entity
phone, sending entity email, project id, site location, violation
type, required changes, compliance deadline, and maximum potential
fine from the message. If any of the fields aren't present, don't
populate them. Try to cast dates into the YYYY-mm-dd format. Don't
populate fields if they're not present in the message.
Here's the notice message:
{message}
""",
)
]
)

notice_parser_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

NOTICE_PARSER_CHAIN = (
info_parse_prompt
| notice_parser_model.with_structured_output(NoticeEmailExtract)
)
44 changes: 44 additions & 0 deletions python-langgraph/example_emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
EMAILS = [
# Email 1
"""
From: debby@stack.com
Hey Betsy,
Here's your invoice for $1000 for the cookies you ordered.
""",
# Email 2
"""
From: tdavid@companyxyz.com
Hi Paul,
We have an issue with the HVAC system your team installed in
apartment 1235. We'd like to request maintenance or a refund.
Thanks,
Terrance
""",
# Email 3
"""
Date: January 10, 2025
From: City of Los Angeles Building and Safety Department
To: West Coast Development, project 345678123 - Sunset Luxury
Condominiums
Location: Los Angeles, CA
Following an inspection of your site at 456 Sunset Boulevard, we have
identified the following building code violations:
Electrical Wiring: Exposed wiring was found in the underground parking
garage, posing a safety hazard. Fire Safety: Insufficient fire
extinguishers were available across multiple floors of the structure
under construction.
Structural Integrity: The temporary support beams in the eastern wing
do not meet the load-bearing standards specified in local building codes.
Required Corrective Actions:
Replace or properly secure exposed wiring to meet electrical safety
standards. Install additional fire extinguishers in compliance with
fire code requirements. Reinforce or replace temporary support beams
to ensure structural stability. Deadline for Compliance: Violations
must be addressed no later than February 5,
2025. Failure to comply may result in
a stop-work order and additional fines.
Contact: For questions or to schedule a re-inspection, please contact
the Building and Safety Department at
(555) 456-7890 or email inspections@lacity.gov.
""",
]
149 changes: 149 additions & 0 deletions python-langgraph/graphs/email_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import logging
import time

from chains.notice_extraction import NoticeEmailExtract
from graphs.notice_extraction import NOTICE_EXTRACTION_GRAPH
from langchain_core.messages import AIMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
LOGGER = logging.getLogger(__name__)


@tool
def forward_email(email_message: str, send_to_email: str) -> bool:
"""
Forward an email_message to the address of sent_to_email. Returns
true if the email was successful otherwise it wil return false. Note
that this tool only forwards the email to an internal department -
it does not reply to the sender.
"""

LOGGER.info(f"Forwarding the email to {send_to_email}...")
time.sleep(2)
LOGGER.info("Email forwarded!")

return True


@tool
def send_wrong_email_notification_to_sender(
sender_email: str, correct_department: str
):
"""
Send an email back to the sender informing them that
they have the wrong address. The email should be sent
to the correct_department.
"""

LOGGER.info(f"Sending wrong email notification to {sender_email}...")
time.sleep(2)
LOGGER.info("Email sent!")

return True


@tool
def extract_notice_data(
email: str, escalation_criteria: str
) -> NoticeEmailExtract:
"""
Extract structured fields from a regulatory notice.
This should be used when the email message comes from
a regulatory body or auditor regarding a property or
construction site that the company works on.
escalation_criteria is a description of which kinds of
notices require immediate escalation.
After calling this tool, you don't need to call any others.
"""

LOGGER.info("Calling the email notice extraction graph...")

initial_state = {
"notice_message": email,
"notice_email_extract": None,
"critical_fields_missing": False,
"escalation_text_criteria": escalation_criteria,
"escalation_dollar_criteria": 100_000,
"requires_escalation": False,
"escalation_emails": ["brog@abc.com", "bigceo@company.com"],
}

results = NOTICE_EXTRACTION_GRAPH.invoke(initial_state)
return results["notice_email_extract"]


@tool
def determine_email_action(email: str) -> str:
"""
Call to determine which action should be taken
for an email. Only use when the other tools don't seem
relevant for the email task. Do not call this tool if
you've already called extract_notice_data.
"""

instructions = """
If the email appears to be an invoice of any kind or related to
billing, forward the email to the billing and invoices team:
billing@company.com
and send a wrong email notice back to the sender. The correct department is
billing@company.com.
If the email appears to be from a customer, forward to support@company.com,
cdetuma@company.com, and ctu@abc.com. Be sure to forward it to all three
emails listed.
Send a wrong email notice back to the
customer and let them know the correct department is support@company.com.
For any other emails, please send a wrong email notification and try to
infer the correct department from one of billing@company.com,
support@company.com,
humanresources@company.com, and it@company.com.
"""

return instructions


tools = [
determine_email_action,
forward_email,
send_wrong_email_notification_to_sender,
extract_notice_data,
]
tool_node = ToolNode(tools)

EMAIL_AGENT_MODEL = ChatOpenAI(model="gpt-4o-mini", temperature=0).bind_tools(
tools
)


def call_agent_model_node(state: MessagesState) -> dict[str, list[AIMessage]]:
"""Node to call the email agent model"""
messages = state["messages"]
response = EMAIL_AGENT_MODEL.invoke(messages)
return {"messages": [response]}


def route_agent_graph_edge(state: MessagesState) -> str:
"""Determine whether to call more tools or exit the graph"""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "email_tools"
return END


workflow = StateGraph(MessagesState)

workflow.add_node("email_agent", call_agent_model_node)
workflow.add_node("email_tools", tool_node)

workflow.add_edge(START, "email_agent")
workflow.add_conditional_edges(
"email_agent", route_agent_graph_edge, ["email_tools", END]
)
workflow.add_edge("email_tools", "email_agent")

email_agent_graph = workflow.compile()
Loading