mirror of
https://github.com/langchain-ai/cookbooks.git
synced 2026-07-01 20:44:05 -04:00
adding new agent
This commit is contained in:
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -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.
|
||||
+50
@@ -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.
|
||||
"""
|
||||
+50
@@ -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()
|
||||
+88
@@ -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"
|
||||
}
|
||||
+50
@@ -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)
|
||||
+50
@@ -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()
|
||||
+50
@@ -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.
|
||||
"""
|
||||
+50
@@ -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
|
||||
}
|
||||
+65
@@ -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()
|
||||
+50
@@ -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()
|
||||
+86
@@ -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.
|
||||
"""
|
||||
+50
@@ -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()
|
||||
+88
@@ -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()
|
||||
+360
@@ -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
Reference in New Issue
Block a user