adding new agent

This commit is contained in:
j-broekhuizen
2025-10-10 12:08:49 -07:00
parent 62d44f8c3a
commit e52a437de0
34 changed files with 2250 additions and 0 deletions
@@ -0,0 +1,4 @@
OPENAI_API_KEY=""
LANGSMITH_TRACING="true"
LANGSMITH_API_KEY=""
LANGSMITH_PROJECT="ecommerce"
@@ -0,0 +1,5 @@
.env
__pycache__/
.venv/
.claude
uv.lock
@@ -0,0 +1,30 @@
# Ecommerce System
A simple multi-agent ecommerce system built with LangChain and LangGraph. A top-level supervisor routes customer queries to specialized business unit (BU) supervisors that focus on billing & payments, order management, and promotions & loyalty tasks.
## Project Structure
- `main_agent.py`: Main supervisor graph that orchestrates BU supervisors.
- `billing_and_payments/`, `order_management/`, `promotions_and_loyalty/`: BU supervisors and their tools.
- `prompts.py`: System prompts and tool descriptions used by the supervisors.
- `test_agent.ipynb`: Notebook you can use to experiment with the assistant end-to-end.
## Getting Started
1. Install dependencies:
```bash
uv sync
```
or use `pip install -e .` if you prefer standard tooling.
2. Create a `.env` file based on `.env.example` and set any API keys (for example `OPENAI_API_KEY`) and LangSmith settings.
## Usage
- From a Python shell or script, import `graph` from `main_agent.py` and call `await graph.ainvoke({"messages": [...]})` with your conversation history.
- Alternatively, open `test_agent.ipynb` to try the flow interactively.
## LangSmith Tracing
- `.env.example` enables LangSmith tracing by default (`LANGSMITH_TRACING="true"`).
- Set `LANGSMITH_API_KEY` with your workspace token and optionally adjust `LANGSMITH_PROJECT` (defaults to `ecommerce`) to group runs.
- Once configured, every invocation reported through LangSmith includes the full supervisor/tool call tree, which makes it easy to debug agent routing.
- See an example trace [here](https://smith.langchain.com/public/704635b2-8735-4d0d-890f-67c26a5aeae4/r)
## Notes
- LangGraph CLI support is included; run `langgraph dev` if you want to inspect the graph locally.
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import verify_pricing, calculate_price_adjustment
from .prompts import PRICING_VERIFICATION_PROMPT
from models import model
model_with_tools = model.bind_tools([verify_pricing, calculate_price_adjustment])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([verify_pricing, calculate_price_adjustment])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=PRICING_VERIFICATION_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,82 @@
"""System prompts for Billing & Payments BU agents"""
# Supervisor Prompt
BILLING_SUPERVISOR_PROMPT = """
You are the Billing & Payments Supervisor responsible for coordinating billing and payment-related customer inquiries.
You have access to three specialized agents:
1. **transaction_lookup_agent**: Use this to look up transaction details, payment information, and transaction status
2. **pricing_verification_agent**: Use this to verify prices, explain billing discrepancies, and calculate price adjustments
3. **refund_processing_agent**: Use this to process refunds for customers
Your responsibilities:
- Analyze the customer's billing or payment issue
- Route to the appropriate specialized agent(s) based on the request
- You may need to call multiple agents in sequence (e.g., first verify pricing, then process a refund)
- Synthesize results from agents into a clear, helpful response for the customer
Be thorough and ensure all aspects of the customer's billing inquiry are addressed.
"""
# Agent Tool Descriptions for Supervisor
TRANSACTION_LOOKUP_TOOL_DESC = """Look up transaction details and payment information for an order.
Use this when the customer needs to:
- Find transaction details by order ID
- Check payment method used
- Verify transaction status
- Get transaction ID or payment confirmation"""
PRICING_VERIFICATION_TOOL_DESC = """Verify pricing and explain billing discrepancies.
Use this when the customer needs to:
- Compare charged amount vs advertised price
- Understand pricing discrepancies (tax, shipping, fees)
- Calculate price adjustments for discounts or corrections
- Get detailed pricing breakdowns"""
REFUND_PROCESSING_TOOL_DESC = """Process refunds for customer orders.
Use this when the customer needs to:
- Process a refund
- Get refund confirmation details
- Understand refund timelines
- Receive refund amount and status"""
# Sub-Agent Prompts
TRANSACTION_LOOKUP_PROMPT = """
You are a Transaction Lookup Agent specializing in finding and retrieving transaction details for customer orders.
Your responsibilities:
- Look up transaction details by order ID
- Provide accurate transaction information including transaction ID, amount charged, payment method, and status
- Clearly communicate transaction details to help resolve billing inquiries
Be precise and thorough when looking up transaction information.
"""
PRICING_VERIFICATION_PROMPT = """
You are a Pricing Verification Agent specializing in verifying prices and identifying billing discrepancies.
Your responsibilities:
- Verify if charged amounts match advertised/expected prices
- Identify and explain pricing discrepancies (tax, shipping, fees, etc.)
- Calculate price adjustments when discounts or corrections need to be applied
- Provide detailed breakdowns of pricing components
Be thorough in explaining any differences between expected and actual charges.
When calculating adjustments, be precise with the math and clearly explain the breakdown.
"""
REFUND_PROCESSING_PROMPT = """
You are a Refund Processing Agent specializing in processing refunds for customer orders.
Your responsibilities:
- Process refunds for valid refund requests
- Confirm refund amounts and reasons
- Provide refund confirmation details and timelines
- Ensure customers understand when and how they will receive their refund
Always confirm the refund amount and reason before processing.
Be empathetic and clear about refund timelines and expectations.
"""
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import process_refund
from .prompts import REFUND_PROCESSING_PROMPT
from models import model
model_with_tools = model.bind_tools([process_refund])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([process_refund])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=REFUND_PROCESSING_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,88 @@
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from models import model
from .transaction_lookup_agent import graph as transaction_lookup_graph
from .pricing_verification_agent import graph as pricing_verification_graph
from .refund_processing_agent import graph as refund_processing_graph
from .prompts import (
BILLING_SUPERVISOR_PROMPT,
TRANSACTION_LOOKUP_TOOL_DESC,
PRICING_VERIFICATION_TOOL_DESC,
REFUND_PROCESSING_TOOL_DESC,
)
@tool(description=TRANSACTION_LOOKUP_TOOL_DESC)
async def transaction_lookup_agent(query: str) -> str:
result = await transaction_lookup_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
@tool(description=PRICING_VERIFICATION_TOOL_DESC)
async def pricing_verification_agent(query: str) -> str:
result = await pricing_verification_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
@tool(description=REFUND_PROCESSING_TOOL_DESC)
async def refund_processing_agent(query: str) -> str:
result = await refund_processing_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
# Bind tools to model
model_with_tools = model.bind_tools(
[transaction_lookup_agent, pricing_verification_agent, refund_processing_agent]
)
tools_node = ToolNode(
[transaction_lookup_agent, pricing_verification_agent, refund_processing_agent]
)
async def supervisor_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=BILLING_SUPERVISOR_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
# Build the supervisor graph
builder = StateGraph(State)
builder.add_node("supervisor_node", supervisor_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "supervisor_node")
builder.add_conditional_edges(
"supervisor_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "supervisor_node")
graph = builder.compile()
@@ -0,0 +1,156 @@
from langchain_core.tools import tool
from typing import Optional
@tool
def lookup_transaction(order_id: str) -> dict:
"""Look up transaction details by order ID.
Args:
order_id: The order ID to look up transaction for
Returns:
Dictionary with transaction details including transaction_id, amount_charged, payment_method
"""
# Mock data - in production this would query a payment gateway/database
mock_transactions = {
"ORD-12345": {
"transaction_id": "TXN-98765",
"order_id": "ORD-12345",
"amount_charged": 1299.00,
"currency": "USD",
"payment_method": "Visa ****1234",
"transaction_date": "2024-01-07",
"status": "completed"
},
"ORD-67890": {
"transaction_id": "TXN-54321",
"order_id": "ORD-67890",
"amount_charged": 599.99,
"currency": "USD",
"payment_method": "MasterCard ****5678",
"transaction_date": "2024-01-08",
"status": "completed"
}
}
transaction = mock_transactions.get(order_id)
if transaction:
return transaction
else:
return {
"error": f"No transaction found for order ID: {order_id}",
"order_id": order_id
}
@tool
def verify_pricing(order_id: str, expected_amount: float) -> dict:
"""Verify if the charged amount matches the expected price for an order.
Args:
order_id: The order ID to verify pricing for
expected_amount: The expected/advertised price
Returns:
Dictionary with verification results including discrepancy details
"""
# Mock data - in production this would check order details and pricing rules
mock_order_pricing = {
"ORD-12345": {
"order_id": "ORD-12345",
"advertised_price": 1199.00,
"charged_amount": 1299.00,
"discrepancy": 100.00,
"reason": "Tax and shipping not included in advertised price",
"breakdown": {
"base_price": 1199.00,
"tax": 95.92,
"shipping": 4.08
}
},
"ORD-67890": {
"order_id": "ORD-67890",
"advertised_price": 599.99,
"charged_amount": 599.99,
"discrepancy": 0.00,
"reason": "Price matches advertised amount",
"breakdown": {
"base_price": 549.99,
"tax": 50.00,
"shipping": 0.00
}
}
}
pricing_info = mock_order_pricing.get(order_id)
if pricing_info:
# Check if expected amount matches
if abs(pricing_info["advertised_price"] - expected_amount) < 0.01:
return pricing_info
else:
pricing_info["note"] = f"Expected amount {expected_amount} doesn't match our records"
return pricing_info
else:
return {
"error": f"No pricing information found for order ID: {order_id}",
"order_id": order_id,
"expected_amount": expected_amount
}
@tool
def process_refund(order_id: str, amount: float, reason: str) -> dict:
"""Process a refund for an order.
Args:
order_id: The order ID to process refund for
amount: The refund amount
reason: The reason for the refund
Returns:
Dictionary with refund processing results
"""
# Mock implementation - in production this would integrate with payment processor
return {
"refund_id": f"REF-{order_id[-5:]}",
"order_id": order_id,
"amount": amount,
"reason": reason,
"status": "processed",
"estimated_days": "3-5 business days",
"message": f"Refund of ${amount:.2f} has been processed successfully"
}
@tool
def calculate_price_adjustment(
original_amount: float,
discount_amount: float,
tax_rate: float = 0.08
) -> dict:
"""Calculate the final price after applying a discount and recalculating tax.
Args:
original_amount: The original charged amount
discount_amount: The discount amount to apply
tax_rate: Tax rate to apply (default 8%)
Returns:
Dictionary with adjusted pricing breakdown
"""
# Calculate new totals
base_with_discount = original_amount - discount_amount
new_tax = base_with_discount * tax_rate
new_total = base_with_discount + new_tax
adjustment_needed = original_amount - new_total
return {
"original_amount": original_amount,
"discount_applied": discount_amount,
"new_base": base_with_discount,
"new_tax": round(new_tax, 2),
"new_total": round(new_total, 2),
"adjustment_amount": round(adjustment_needed, 2),
"message": f"Customer should receive ${adjustment_needed:.2f} back"
}
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import lookup_transaction
from .prompts import TRANSACTION_LOOKUP_PROMPT
from models import model
model_with_tools = model.bind_tools([lookup_transaction])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([lookup_transaction])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=TRANSACTION_LOOKUP_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,9 @@
{
"dependencies": [
"."
],
"graphs": {
"agent": "./main_agent.py:graph"
},
"env": ".env"
}
@@ -0,0 +1,97 @@
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from models import model
from billing_and_payments.supervisor import graph as billing_payments_graph
from order_management.supervisor import graph as order_management_graph
from promotions_and_loyalty.supervisor import graph as promotions_loyalty_graph
from prompts import (
MAIN_SUPERVISOR_PROMPT,
BILLING_PAYMENTS_SUPERVISOR_DESC,
ORDER_MANAGEMENT_SUPERVISOR_DESC,
PROMOTIONS_LOYALTY_SUPERVISOR_DESC,
)
@tool(description=BILLING_PAYMENTS_SUPERVISOR_DESC)
async def billing_and_payments_supervisor(query: str) -> str:
"""Route billing, payment, pricing, and refund inquiries to the Billing & Payments BU."""
result = await billing_payments_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
@tool(description=ORDER_MANAGEMENT_SUPERVISOR_DESC)
async def order_management_supervisor(query: str) -> str:
"""Route order and shipping inquiries to the Order Management BU."""
result = await order_management_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
@tool(description=PROMOTIONS_LOYALTY_SUPERVISOR_DESC)
async def promotions_and_loyalty_supervisor(query: str) -> str:
"""Route promotional code, discount, and loyalty program inquiries to the Promotions & Loyalty BU."""
result = await promotions_loyalty_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
# Bind tools to model
model_with_tools = model.bind_tools([
billing_and_payments_supervisor,
order_management_supervisor,
promotions_and_loyalty_supervisor,
])
tools_node = ToolNode([
billing_and_payments_supervisor,
order_management_supervisor,
promotions_and_loyalty_supervisor,
])
async def main_supervisor_node(state: State) -> dict:
"""Main supervisor that routes customer inquiries to appropriate BU supervisors."""
result = await model_with_tools.ainvoke(
[SystemMessage(content=MAIN_SUPERVISOR_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
"""Determine if the supervisor needs to call more tools or end."""
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
# Build the main supervisor graph
builder = StateGraph(State)
builder.add_node("main_supervisor_node", main_supervisor_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "main_supervisor_node")
builder.add_conditional_edges(
"main_supervisor_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "main_supervisor_node")
graph = builder.compile()
@@ -0,0 +1,7 @@
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
# Central model configuration for all agents
model = ChatOpenAI(model="gpt-4o", temperature=0)
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import check_fulfillment_status
from .prompts import FULFILLMENT_PROMPT
from models import model
model_with_tools = model.bind_tools([check_fulfillment_status])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([check_fulfillment_status])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=FULFILLMENT_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import get_order_details, cancel_order, update_shipping_address
from .prompts import ORDER_STATUS_PROMPT
from models import model
model_with_tools = model.bind_tools([get_order_details, cancel_order, update_shipping_address])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([get_order_details, cancel_order, update_shipping_address])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=ORDER_STATUS_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,86 @@
"""System prompts for Order Management BU agents"""
# Supervisor Prompt
ORDER_MANAGEMENT_SUPERVISOR_PROMPT = """
You are the Order Management Supervisor responsible for coordinating all order and shipping-related customer inquiries.
You have access to three specialized agents:
1. **order_status_agent**: Use this to get order details, update orders, or cancel orders
2. **shipping_tracker_agent**: Use this to track shipments, get delivery estimates, and provide shipping updates
3. **fulfillment_agent**: Use this to check warehouse fulfillment status, picking progress, and readiness for shipment
Your responsibilities:
- Analyze the customer's order or shipping inquiry
- Route to the appropriate specialized agent(s) based on the request
- You may need to call multiple agents (e.g., check order status first, then track shipment)
- Synthesize results from agents into a clear, helpful response for the customer
Be thorough and ensure all aspects of the customer's order inquiry are addressed.
"""
# Agent Tool Descriptions for Supervisor
ORDER_STATUS_TOOL_DESC = """Get order details and manage order changes.
Use this when the customer needs to:
- View order details and items
- Check order status
- Cancel an order
- Update shipping address
- Get order confirmation information"""
SHIPPING_TRACKER_TOOL_DESC = """Track shipments and get delivery information.
Use this when the customer needs to:
- Track package location and status
- Get tracking numbers
- Check estimated delivery dates
- View shipping carrier information
- See shipment history"""
FULFILLMENT_TOOL_DESC = """Check warehouse fulfillment status and processing.
Use this when the customer needs to:
- Check if order is being picked/packed
- Understand warehouse processing status
- Know when order will be ready to ship
- Get detailed fulfillment stage information"""
# Sub-Agent Prompts
ORDER_STATUS_PROMPT = """
You are an Order Status Agent specializing in retrieving and managing order information.
Your responsibilities:
- Retrieve detailed order information including items, prices, and shipping details
- Help customers understand their order status
- Process order cancellations when requested
- Update shipping addresses if orders haven't shipped yet
- Provide clear order confirmations and updates
Be helpful and proactive in addressing order-related concerns. If an order cannot be modified, explain why clearly.
"""
SHIPPING_TRACKER_PROMPT = """
You are a Shipping Tracker Agent specializing in tracking packages and providing delivery information.
Your responsibilities:
- Track shipment locations and provide real-time updates
- Provide tracking numbers and carrier information
- Give accurate delivery estimates
- Explain shipping delays or issues clearly
- Provide detailed tracking history
Be precise with tracking information and set realistic expectations about delivery times.
"""
FULFILLMENT_PROMPT = """
You are a Fulfillment Agent specializing in warehouse operations and order processing.
Your responsibilities:
- Check order fulfillment status in the warehouse
- Provide information about picking, packing, and shipping readiness
- Explain warehouse processing stages
- Give estimates for when orders will be ready to ship
- Clarify any fulfillment delays or issues
Be transparent about warehouse processes and provide realistic timelines for order fulfillment.
"""
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import track_shipment, get_delivery_estimate
from .prompts import SHIPPING_TRACKER_PROMPT
from models import model
model_with_tools = model.bind_tools([track_shipment, get_delivery_estimate])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([track_shipment, get_delivery_estimate])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=SHIPPING_TRACKER_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,82 @@
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from models import model
from .order_status_agent import graph as order_status_graph
from .shipping_tracker_agent import graph as shipping_tracker_graph
from .fulfillment_agent import graph as fulfillment_graph
from .prompts import (
ORDER_MANAGEMENT_SUPERVISOR_PROMPT,
ORDER_STATUS_TOOL_DESC,
SHIPPING_TRACKER_TOOL_DESC,
FULFILLMENT_TOOL_DESC,
)
@tool(description=ORDER_STATUS_TOOL_DESC)
async def order_status_agent(query: str) -> str:
result = await order_status_graph.ainvoke({"messages": [HumanMessage(content=query)]})
return result["messages"][-1].content
@tool(description=SHIPPING_TRACKER_TOOL_DESC)
async def shipping_tracker_agent(query: str) -> str:
result = await shipping_tracker_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
@tool(description=FULFILLMENT_TOOL_DESC)
async def fulfillment_agent(query: str) -> str:
result = await fulfillment_graph.ainvoke({"messages": [HumanMessage(content=query)]})
return result["messages"][-1].content
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
# Bind tools to model
model_with_tools = model.bind_tools(
[order_status_agent, shipping_tracker_agent, fulfillment_agent]
)
tools_node = ToolNode([order_status_agent, shipping_tracker_agent, fulfillment_agent])
async def supervisor_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=ORDER_MANAGEMENT_SUPERVISOR_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
# Build the supervisor graph
builder = StateGraph(State)
builder.add_node("supervisor_node", supervisor_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "supervisor_node")
builder.add_conditional_edges(
"supervisor_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "supervisor_node")
graph = builder.compile()
@@ -0,0 +1,308 @@
from langchain_core.tools import tool
from typing import Optional
@tool
def get_order_details(order_id: str) -> dict:
"""Get detailed information about an order.
Args:
order_id: The order ID to look up
Returns:
Dictionary with order details including items, status, dates, and shipping info
"""
# Mock data - in production this would query an order database
mock_orders = {
"ORD-12345": {
"order_id": "ORD-12345",
"status": "processing",
"order_date": "2024-01-07",
"customer_id": "CUST-001",
"items": [
{
"item_id": "ITEM-456",
"name": "Laptop - Dell XPS 15",
"quantity": 1,
"price": 1199.00
}
],
"subtotal": 1199.00,
"total": 1299.00,
"shipping_address": "123 Main St, San Francisco, CA 94102",
"estimated_delivery": "2024-01-15"
},
"ORD-67890": {
"order_id": "ORD-67890",
"status": "shipped",
"order_date": "2024-01-08",
"customer_id": "CUST-002",
"items": [
{
"item_id": "ITEM-789",
"name": "Wireless Mouse",
"quantity": 2,
"price": 29.99
}
],
"subtotal": 59.98,
"total": 64.78,
"shipping_address": "456 Oak Ave, Portland, OR 97201",
"estimated_delivery": "2024-01-12"
}
}
order = mock_orders.get(order_id)
if order:
return order
else:
return {
"error": f"No order found with ID: {order_id}",
"order_id": order_id
}
@tool
def track_shipment(order_id: str) -> dict:
"""Track the shipping status and location of an order.
Args:
order_id: The order ID to track
Returns:
Dictionary with shipment tracking details including carrier, tracking number, and current location
"""
# Mock data - in production this would integrate with shipping carriers
mock_shipments = {
"ORD-12345": {
"order_id": "ORD-12345",
"status": "processing",
"tracking_number": None,
"carrier": None,
"current_location": "Warehouse - San Jose, CA",
"last_update": "2024-01-09T14:30:00Z",
"message": "Order is being prepared for shipment",
"tracking_history": [
{"date": "2024-01-07", "status": "Order received", "location": "Online"},
{"date": "2024-01-09", "status": "Processing", "location": "Warehouse - San Jose, CA"}
]
},
"ORD-67890": {
"order_id": "ORD-67890",
"status": "in_transit",
"tracking_number": "1Z999AA10123456784",
"carrier": "UPS",
"current_location": "Sacramento, CA",
"last_update": "2024-01-10T08:15:00Z",
"estimated_delivery": "2024-01-12",
"message": "Package is in transit to destination",
"tracking_history": [
{"date": "2024-01-08", "status": "Shipped", "location": "San Jose, CA"},
{"date": "2024-01-09", "status": "In transit", "location": "Oakland, CA"},
{"date": "2024-01-10", "status": "In transit", "location": "Sacramento, CA"}
]
}
}
shipment = mock_shipments.get(order_id)
if shipment:
return shipment
else:
return {
"error": f"No shipment information found for order ID: {order_id}",
"order_id": order_id
}
@tool
def check_fulfillment_status(order_id: str) -> dict:
"""Check the fulfillment status of an order in the warehouse.
Args:
order_id: The order ID to check fulfillment status for
Returns:
Dictionary with fulfillment details including picking, packing, and readiness status
"""
# Mock data - in production this would query warehouse management system
mock_fulfillment = {
"ORD-12345": {
"order_id": "ORD-12345",
"fulfillment_status": "picking_in_progress",
"warehouse_location": "Warehouse A - San Jose, CA",
"assigned_picker": "Picker-42",
"picking_progress": "60%",
"items_picked": 0,
"items_total": 1,
"estimated_ready_date": "2024-01-11",
"notes": "Item located in aisle 12. High-value item requires special handling.",
"stages": {
"received": True,
"picking": True,
"packing": False,
"ready_to_ship": False
}
},
"ORD-67890": {
"order_id": "ORD-67890",
"fulfillment_status": "shipped",
"warehouse_location": "Warehouse A - San Jose, CA",
"assigned_picker": "Picker-15",
"picking_progress": "100%",
"items_picked": 2,
"items_total": 2,
"ship_date": "2024-01-08",
"notes": "Order fulfilled and shipped successfully",
"stages": {
"received": True,
"picking": True,
"packing": True,
"ready_to_ship": True,
"shipped": True
}
}
}
fulfillment = mock_fulfillment.get(order_id)
if fulfillment:
return fulfillment
else:
return {
"error": f"No fulfillment information found for order ID: {order_id}",
"order_id": order_id
}
@tool
def cancel_order(order_id: str, reason: str) -> dict:
"""Cancel an order if it hasn't been shipped yet.
Args:
order_id: The order ID to cancel
reason: The reason for cancellation
Returns:
Dictionary with cancellation status and details
"""
# Mock implementation - in production this would update order database
# Check if order can be cancelled (not shipped yet)
mock_order_statuses = {
"ORD-12345": "processing",
"ORD-67890": "shipped"
}
status = mock_order_statuses.get(order_id)
if not status:
return {
"success": False,
"error": f"Order {order_id} not found",
"order_id": order_id
}
if status == "shipped":
return {
"success": False,
"order_id": order_id,
"message": "Order has already been shipped and cannot be cancelled. Please initiate a return instead.",
"status": status
}
return {
"success": True,
"order_id": order_id,
"cancellation_id": f"CANCEL-{order_id[-5:]}",
"reason": reason,
"refund_status": "pending",
"refund_timeline": "3-5 business days",
"message": f"Order {order_id} has been successfully cancelled. Refund will be processed within 3-5 business days."
}
@tool
def update_shipping_address(order_id: str, new_address: str) -> dict:
"""Update the shipping address for an order if it hasn't shipped yet.
Args:
order_id: The order ID to update
new_address: The new shipping address
Returns:
Dictionary with update status and confirmation
"""
# Mock implementation - in production this would update order database
mock_order_statuses = {
"ORD-12345": "processing",
"ORD-67890": "shipped"
}
status = mock_order_statuses.get(order_id)
if not status:
return {
"success": False,
"error": f"Order {order_id} not found",
"order_id": order_id
}
if status == "shipped":
return {
"success": False,
"order_id": order_id,
"message": "Order has already been shipped. Address cannot be changed. Please contact carrier for address correction.",
"status": status
}
return {
"success": True,
"order_id": order_id,
"old_address": "123 Main St, San Francisco, CA 94102",
"new_address": new_address,
"message": f"Shipping address for order {order_id} has been successfully updated."
}
@tool
def get_delivery_estimate(order_id: str) -> dict:
"""Get estimated delivery date for an order.
Args:
order_id: The order ID to get delivery estimate for
Returns:
Dictionary with delivery estimate details
"""
# Mock data - in production this would integrate with shipping systems
mock_estimates = {
"ORD-12345": {
"order_id": "ORD-12345",
"estimated_delivery_date": "2024-01-15",
"estimated_delivery_window": "Jan 15-17, 2024",
"shipping_method": "Standard Shipping (5-7 business days)",
"current_status": "Processing",
"can_expedite": True,
"expedite_options": [
{"method": "2-Day", "cost": 15.00, "delivery_date": "2024-01-12"},
{"method": "Overnight", "cost": 25.00, "delivery_date": "2024-01-11"}
]
},
"ORD-67890": {
"order_id": "ORD-67890",
"estimated_delivery_date": "2024-01-12",
"estimated_delivery_window": "Jan 12, 2024",
"shipping_method": "Standard Shipping (5-7 business days)",
"current_status": "In Transit",
"tracking_number": "1Z999AA10123456784",
"can_expedite": False,
"message": "Package is already in transit. Delivery estimate is based on current shipping progress."
}
}
estimate = mock_estimates.get(order_id)
if estimate:
return estimate
else:
return {
"error": f"No delivery estimate available for order ID: {order_id}",
"order_id": order_id
}
@@ -0,0 +1,65 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import (
check_loyalty_balance,
get_loyalty_history,
calculate_points_earned,
redeem_loyalty_points,
)
from .prompts import LOYALTY_POINTS_PROMPT
from models import model
model_with_tools = model.bind_tools([
check_loyalty_balance,
get_loyalty_history,
calculate_points_earned,
redeem_loyalty_points,
])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([
check_loyalty_balance,
get_loyalty_history,
calculate_points_earned,
redeem_loyalty_points,
])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=LOYALTY_POINTS_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import validate_promo_code
from .prompts import PROMO_VALIDATOR_PROMPT
from models import model
model_with_tools = model.bind_tools([validate_promo_code])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([validate_promo_code])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=PROMO_VALIDATOR_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,86 @@
"""System prompts for Promotions & Loyalty BU agents"""
# Supervisor Prompt
PROMOTIONS_SUPERVISOR_PROMPT = """
You are the Promotions & Loyalty Supervisor responsible for coordinating all promotional codes, discounts, and loyalty program inquiries.
You have access to three specialized agents:
1. **promo_code_validator_agent**: Use this to validate promotional codes and check eligibility
2. **retroactive_discount_agent**: Use this to apply discounts retroactively to existing orders
3. **loyalty_points_agent**: Use this to check loyalty balances, history, and redeem points
Your responsibilities:
- Analyze the customer's promotion or loyalty inquiry
- Route to the appropriate specialized agent(s) based on the request
- You may need to call multiple agents (e.g., validate promo code first, then apply it retroactively)
- Synthesize results from agents into a clear, helpful response for the customer
Be helpful and ensure customers understand how to maximize their savings and rewards.
"""
# Agent Tool Descriptions for Supervisor
PROMO_VALIDATOR_TOOL_DESC = """Validate promotional codes and check eligibility.
Use this when the customer needs to:
- Check if a promo code is valid
- Understand promo code terms and restrictions
- Get details about discount amounts and expiration dates
- Verify minimum purchase requirements"""
RETROACTIVE_DISCOUNT_TOOL_DESC = """Apply promotional discounts retroactively to existing orders.
Use this when the customer needs to:
- Apply a forgotten promo code to a recent order
- Get a refund for a discount they should have received
- Calculate adjusted order totals with discounts applied
- Process promotional adjustments"""
LOYALTY_POINTS_TOOL_DESC = """Manage loyalty points and rewards.
Use this when the customer needs to:
- Check their loyalty points balance
- View loyalty transaction history
- Redeem points for discounts
- Calculate points earned on purchases
- Understand tier benefits and status"""
# Sub-Agent Prompts
PROMO_VALIDATOR_PROMPT = """
You are a Promo Code Validator Agent specializing in validating promotional codes and explaining eligibility.
Your responsibilities:
- Validate promotional codes and check their status
- Explain discount amounts, types, and restrictions
- Clarify minimum purchase requirements and eligible categories
- Communicate expiration dates and usage limits clearly
- Help customers understand why codes may not work
Be clear about promotional terms and help customers maximize their savings.
"""
RETROACTIVE_DISCOUNT_PROMPT = """
You are a Retroactive Discount Agent specializing in applying promotional discounts to existing orders.
Your responsibilities:
- Apply promotional codes retroactively to recent orders
- Calculate refund amounts when discounts are applied
- Verify order eligibility for promotional discounts
- Process price adjustments and refunds
- Clearly explain the adjustment process and timeline
Be empathetic when customers forget to use promo codes and make the retroactive application process smooth.
"""
LOYALTY_POINTS_PROMPT = """
You are a Loyalty Points Agent specializing in managing customer loyalty accounts and rewards.
Your responsibilities:
- Check and report loyalty points balances and tier status
- Provide loyalty transaction history
- Process points redemptions for discounts
- Calculate points earned on purchases
- Explain tier benefits and how to advance to next tier
- Help customers maximize their loyalty rewards
Be enthusiastic about loyalty benefits and help customers understand the value of their rewards.
"""
@@ -0,0 +1,50 @@
from langchain_core.messages import SystemMessage
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from .tools import apply_retroactive_discount, validate_promo_code
from .prompts import RETROACTIVE_DISCOUNT_PROMPT
from models import model
model_with_tools = model.bind_tools([apply_retroactive_discount, validate_promo_code])
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
tools_node = ToolNode([apply_retroactive_discount, validate_promo_code])
async def llm_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=RETROACTIVE_DISCOUNT_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "llm_node")
builder.add_conditional_edges(
"llm_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "llm_node")
graph = builder.compile()
@@ -0,0 +1,88 @@
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph.message import AnyMessage, add_messages
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
from models import model
from .promo_code_validator_agent import graph as promo_validator_graph
from .retroactive_discount_agent import graph as retroactive_discount_graph
from .loyalty_points_agent import graph as loyalty_points_graph
from .prompts import (
PROMOTIONS_SUPERVISOR_PROMPT,
PROMO_VALIDATOR_TOOL_DESC,
RETROACTIVE_DISCOUNT_TOOL_DESC,
LOYALTY_POINTS_TOOL_DESC,
)
@tool(description=PROMO_VALIDATOR_TOOL_DESC)
async def promo_code_validator_agent(query: str) -> str:
result = await promo_validator_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
@tool(description=RETROACTIVE_DISCOUNT_TOOL_DESC)
async def retroactive_discount_agent(query: str) -> str:
result = await retroactive_discount_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
@tool(description=LOYALTY_POINTS_TOOL_DESC)
async def loyalty_points_agent(query: str) -> str:
result = await loyalty_points_graph.ainvoke(
{"messages": [HumanMessage(content=query)]}
)
return result["messages"][-1].content
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
# Bind tools to model
model_with_tools = model.bind_tools(
[promo_code_validator_agent, retroactive_discount_agent, loyalty_points_agent]
)
tools_node = ToolNode(
[promo_code_validator_agent, retroactive_discount_agent, loyalty_points_agent]
)
async def supervisor_node(state: State) -> dict:
result = await model_with_tools.ainvoke(
[SystemMessage(content=PROMOTIONS_SUPERVISOR_PROMPT)] + state["messages"]
)
return {"messages": [result]}
def should_continue(state: State):
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return "end"
else:
return "continue"
# Build the supervisor graph
builder = StateGraph(State)
builder.add_node("supervisor_node", supervisor_node)
builder.add_node("tools_node", tools_node)
builder.add_edge(START, "supervisor_node")
builder.add_conditional_edges(
"supervisor_node",
should_continue,
{
"continue": "tools_node",
"end": END,
},
)
builder.add_edge("tools_node", "supervisor_node")
graph = builder.compile()
@@ -0,0 +1,360 @@
from langchain_core.tools import tool
from typing import Optional
@tool
def validate_promo_code(promo_code: str, order_id: Optional[str] = None) -> dict:
"""Validate a promotional code and check if it's eligible for use.
Args:
promo_code: The promotional code to validate
order_id: Optional order ID to check code applicability
Returns:
Dictionary with validation results including discount amount, expiration, and eligibility
"""
# Mock data - in production this would query a promotions database
mock_promo_codes = {
"FALL50": {
"code": "FALL50",
"valid": True,
"discount_type": "fixed",
"discount_amount": 50.00,
"description": "$50 off orders over $500",
"minimum_purchase": 500.00,
"expiration_date": "2024-12-31",
"max_uses": 1000,
"uses_remaining": 842,
"eligible_categories": ["Electronics", "Computers", "Accessories"]
},
"SAVE20": {
"code": "SAVE20",
"valid": True,
"discount_type": "percentage",
"discount_amount": 20.0,
"description": "20% off all orders",
"minimum_purchase": 0,
"expiration_date": "2024-11-30",
"max_uses": 5000,
"uses_remaining": 3214,
"eligible_categories": ["All"]
},
"EXPIRED10": {
"code": "EXPIRED10",
"valid": False,
"reason": "This promotional code has expired",
"expiration_date": "2024-01-01"
}
}
promo_code_upper = promo_code.upper()
promo = mock_promo_codes.get(promo_code_upper)
if promo:
return promo
else:
return {
"code": promo_code,
"valid": False,
"reason": f"Promotional code '{promo_code}' not found or is invalid"
}
@tool
def apply_retroactive_discount(order_id: str, promo_code: str) -> dict:
"""Apply a promotional discount retroactively to an already-placed order.
Args:
order_id: The order ID to apply the discount to
promo_code: The promotional code to apply
Returns:
Dictionary with application results including adjusted price and refund amount
"""
# Mock implementation - in production this would update order and process adjustment
# First validate the promo code
promo_validation = validate_promo_code(promo_code, order_id)
if not promo_validation.get("valid"):
return {
"success": False,
"order_id": order_id,
"promo_code": promo_code,
"error": promo_validation.get("reason", "Invalid promotional code")
}
# Mock order data
mock_orders = {
"ORD-12345": {
"order_id": "ORD-12345",
"original_total": 1299.00,
"items_subtotal": 1199.00,
"eligible_for_promo": True
}
}
order = mock_orders.get(order_id)
if not order:
return {
"success": False,
"order_id": order_id,
"error": f"Order {order_id} not found"
}
# Check eligibility
if not order.get("eligible_for_promo"):
return {
"success": False,
"order_id": order_id,
"promo_code": promo_code,
"error": "This order is not eligible for promotional discounts"
}
# Calculate discount
discount_amount = 0
if promo_validation["discount_type"] == "fixed":
discount_amount = promo_validation["discount_amount"]
elif promo_validation["discount_type"] == "percentage":
discount_amount = order["items_subtotal"] * (promo_validation["discount_amount"] / 100)
# Check minimum purchase requirement
if order["items_subtotal"] < promo_validation.get("minimum_purchase", 0):
return {
"success": False,
"order_id": order_id,
"promo_code": promo_code,
"error": f"Order does not meet minimum purchase requirement of ${promo_validation.get('minimum_purchase', 0):.2f}"
}
new_total = order["original_total"] - discount_amount
refund_amount = discount_amount
return {
"success": True,
"order_id": order_id,
"promo_code": promo_code,
"original_total": order["original_total"],
"discount_amount": round(discount_amount, 2),
"new_total": round(new_total, 2),
"refund_amount": round(refund_amount, 2),
"refund_method": "Original payment method",
"refund_timeline": "3-5 business days",
"message": f"Promotional code {promo_code} has been applied successfully. You will receive a refund of ${refund_amount:.2f}."
}
@tool
def check_loyalty_balance(customer_id: str) -> dict:
"""Check the loyalty points balance for a customer.
Args:
customer_id: The customer ID to check loyalty points for
Returns:
Dictionary with loyalty balance, tier status, and points expiration info
"""
# Mock data - in production this would query a loyalty database
mock_loyalty_accounts = {
"CUST-001": {
"customer_id": "CUST-001",
"points_balance": 2450,
"points_pending": 120,
"tier": "Gold",
"tier_benefits": [
"Free shipping on all orders",
"Early access to sales",
"Birthday bonus points"
],
"next_tier": "Platinum",
"points_to_next_tier": 550,
"points_expiring_soon": {
"amount": 200,
"expiration_date": "2024-03-31"
},
"lifetime_points_earned": 8920
},
"CUST-002": {
"customer_id": "CUST-002",
"points_balance": 550,
"points_pending": 0,
"tier": "Silver",
"tier_benefits": [
"Free shipping on orders over $50",
"Exclusive member discounts"
],
"next_tier": "Gold",
"points_to_next_tier": 450,
"points_expiring_soon": None,
"lifetime_points_earned": 1230
}
}
loyalty_account = mock_loyalty_accounts.get(customer_id)
if loyalty_account:
return loyalty_account
else:
return {
"error": f"No loyalty account found for customer ID: {customer_id}",
"customer_id": customer_id
}
@tool
def get_loyalty_history(customer_id: str) -> dict:
"""Get the transaction history for a customer's loyalty account.
Args:
customer_id: The customer ID to get loyalty history for
Returns:
Dictionary with loyalty transaction history including earned and redeemed points
"""
# Mock data - in production this would query loyalty transaction history
mock_loyalty_history = {
"CUST-001": {
"customer_id": "CUST-001",
"transactions": [
{
"date": "2024-01-07",
"type": "earned",
"points": 120,
"description": "Purchase - Order ORD-12345",
"order_id": "ORD-12345"
},
{
"date": "2024-01-05",
"type": "redeemed",
"points": -500,
"description": "Redeemed for $25 discount",
"order_id": "ORD-11111"
},
{
"date": "2024-01-01",
"type": "earned",
"points": 50,
"description": "New Year Bonus Points",
"order_id": None
},
{
"date": "2023-12-28",
"type": "earned",
"points": 200,
"description": "Purchase - Order ORD-11000",
"order_id": "ORD-11000"
}
],
"total_earned": 8920,
"total_redeemed": 6470,
"current_balance": 2450
},
"CUST-002": {
"customer_id": "CUST-002",
"transactions": [
{
"date": "2024-01-08",
"type": "earned",
"points": 60,
"description": "Purchase - Order ORD-67890",
"order_id": "ORD-67890"
},
{
"date": "2023-12-15",
"type": "earned",
"points": 100,
"description": "Purchase - Order ORD-55555",
"order_id": "ORD-55555"
}
],
"total_earned": 1230,
"total_redeemed": 680,
"current_balance": 550
}
}
history = mock_loyalty_history.get(customer_id)
if history:
return history
else:
return {
"error": f"No loyalty history found for customer ID: {customer_id}",
"customer_id": customer_id
}
@tool
def calculate_points_earned(order_total: float, customer_tier: str = "Silver") -> dict:
"""Calculate loyalty points that would be earned for a purchase.
Args:
order_total: The order total amount
customer_tier: The customer's loyalty tier (Silver, Gold, Platinum)
Returns:
Dictionary with points calculation breakdown
"""
# Mock calculation - in production this would use actual loyalty program rules
tier_multipliers = {
"Silver": 1.0,
"Gold": 1.5,
"Platinum": 2.0
}
base_points_per_dollar = 1
multiplier = tier_multipliers.get(customer_tier, 1.0)
base_points = int(order_total * base_points_per_dollar)
bonus_points = int(base_points * (multiplier - 1.0))
total_points = base_points + bonus_points
return {
"order_total": order_total,
"customer_tier": customer_tier,
"base_points": base_points,
"tier_multiplier": multiplier,
"bonus_points": bonus_points,
"total_points_earned": total_points,
"points_breakdown": f"${order_total:.2f} x {base_points_per_dollar} point per dollar x {multiplier}x tier bonus = {total_points} points"
}
@tool
def redeem_loyalty_points(customer_id: str, points_to_redeem: int) -> dict:
"""Redeem loyalty points for a discount or reward.
Args:
customer_id: The customer ID redeeming points
points_to_redeem: The number of points to redeem
Returns:
Dictionary with redemption details including discount value
"""
# Mock implementation - in production this would update loyalty account
# Check balance first
balance_info = check_loyalty_balance(customer_id)
if "error" in balance_info:
return balance_info
current_balance = balance_info["points_balance"]
if points_to_redeem > current_balance:
return {
"success": False,
"customer_id": customer_id,
"error": f"Insufficient points. You have {current_balance} points but tried to redeem {points_to_redeem} points."
}
# Calculate discount value (100 points = $5)
points_per_dollar = 20
discount_value = points_to_redeem / points_per_dollar
new_balance = current_balance - points_to_redeem
return {
"success": True,
"customer_id": customer_id,
"points_redeemed": points_to_redeem,
"discount_value": round(discount_value, 2),
"previous_balance": current_balance,
"new_balance": new_balance,
"redemption_code": f"LOYALTY-{customer_id[-3:]}-{points_to_redeem}",
"message": f"Successfully redeemed {points_to_redeem} points for ${discount_value:.2f} discount. Your new balance is {new_balance} points."
}
@@ -0,0 +1,74 @@
"""System prompts for Main Customer Support Agent"""
# Main Supervisor Prompt
MAIN_SUPERVISOR_PROMPT = """
You are the Main Customer Support Supervisor responsible for coordinating all customer support inquiries across the organization.
You have access to three Business Unit (BU) supervisors, each managing specialized teams:
1. **billing_and_payments_supervisor**: Use this for all billing, payment, pricing, and refund-related inquiries
- Transaction lookups and payment information
- Pricing verification and billing discrepancies
- Refund processing
2. **order_management_supervisor**: Use this for all order and shipping-related inquiries
- Order status and details
- Shipment tracking and delivery information
- Warehouse fulfillment status
- Order modifications and cancellations
3. **promotions_and_loyalty_supervisor**: Use this for all promotional codes, discounts, and loyalty program inquiries
- Promotional code validation
- Retroactive discount applications
- Loyalty points balance and redemption
- Tier benefits and rewards
Your responsibilities:
- Analyze the customer's inquiry and identify which BU(s) are needed
- Route the request to the appropriate BU supervisor(s)
- You may need to coordinate between multiple BUs (e.g., apply a promo code retroactively AND process a refund)
- Synthesize responses from different BUs into a cohesive, customer-friendly answer
- Ensure the customer's complete issue is resolved, not just individual parts
Guidelines:
- Always be empathetic and customer-focused
- If the inquiry spans multiple BUs, coordinate them in the right order
- Provide clear, actionable information to the customer
- Set proper expectations about timelines and next steps
Example routing scenarios:
- "My card was charged $1,299 but the website said $1,199" -> billing_and_payments_supervisor
- "Where is my order?" -> order_management_supervisor
- "Can I use promo code FALL50?" -> promotions_and_loyalty_supervisor
- "I forgot to use my promo code and was overcharged" -> promotions_and_loyalty_supervisor THEN billing_and_payments_supervisor
"""
# BU Supervisor Tool Descriptions
BILLING_PAYMENTS_SUPERVISOR_DESC = """Handle billing, payment, pricing, and refund inquiries.
Use this when the customer needs help with:
- Transaction details and payment information
- Pricing verification and billing discrepancies
- Refunds and payment adjustments
- Understanding charges on their card
- Processing payment-related corrections"""
ORDER_MANAGEMENT_SUPERVISOR_DESC = """Handle order and shipping inquiries.
Use this when the customer needs help with:
- Checking order status and details
- Tracking shipments and packages
- Getting delivery estimates
- Understanding warehouse fulfillment status
- Canceling or modifying orders
- Updating shipping addresses"""
PROMOTIONS_LOYALTY_SUPERVISOR_DESC = """Handle promotional codes, discounts, and loyalty program inquiries.
Use this when the customer needs help with:
- Validating promotional codes
- Applying discounts retroactively to orders
- Checking loyalty points balance
- Redeeming loyalty rewards
- Understanding tier benefits
- Getting promotional discount information"""
@@ -0,0 +1,24 @@
[project]
name = "langgraph-101"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"azure-identity>=1.25.0",
"ipython>=9.6.0",
"langchain>=1.0.0a9",
"langchain-anthropic>=1.0.0a2",
"langchain-community>=0.3.30",
"langchain-openai>=0.3.34",
"langgraph>=0.6.8",
"langgraph-cli[inmem]>=0.4.2",
"langsmith>=0.4.32",
"notebook>=7.4.7",
"openai>=2.1.0",
"openevals>=0.1.0",
"protobuf>=3.20.3",
"pyppeteer>=2.0.0",
"python-dotenv>=1.1.1",
"requests>=2.32.5",
]
File diff suppressed because one or more lines are too long