mirror of
https://github.com/langchain-ai/langgraph-fullstack-python.git
synced 2026-07-19 13:54:28 -04:00
merge
This commit is contained in:
+203
-49
@@ -1,22 +1,14 @@
|
||||
from fasthtml.common import (
|
||||
Button,
|
||||
Div,
|
||||
FastHTML,
|
||||
Form,
|
||||
Group,
|
||||
Hidden,
|
||||
Input,
|
||||
Link,
|
||||
Script,
|
||||
Titled,
|
||||
picolink,
|
||||
)
|
||||
from fasthtml.common import *
|
||||
import uuid
|
||||
from langgraph_sdk import get_client
|
||||
from starlette.responses import StreamingResponse
|
||||
from datetime import datetime
|
||||
from typing import TypedDict, Dict, List
|
||||
|
||||
# None means point to the local run loop
|
||||
# Initialize the LangGraph client
|
||||
langgraph_client = get_client()
|
||||
|
||||
# Set up the app, including daisyui and tailwind for the chat component
|
||||
# Define HTML headers for styling and client-side functionality
|
||||
hdrs = (
|
||||
picolink,
|
||||
Script(src="https://cdn.tailwindcss.com"),
|
||||
@@ -24,25 +16,55 @@ hdrs = (
|
||||
rel="stylesheet",
|
||||
href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css",
|
||||
),
|
||||
Script(src="https://unpkg.com/htmx.org@1.9.2/dist/htmx.min.js"),
|
||||
Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),
|
||||
)
|
||||
|
||||
# Create the FastHTML app with headers and default styling
|
||||
app = FastHTML(
|
||||
hdrs=hdrs, ct_hdr=True, cls="flex flex-row h-screen", live=True, debug=True
|
||||
)
|
||||
app = FastHTML(hdrs=hdrs, cls="p-4 max-w-lg mx-auto")
|
||||
|
||||
|
||||
# Chat message component (renders a chat bubble)
|
||||
def ChatMessage(msg, user):
|
||||
bubble_class = "chat-bubble-primary" if user else "chat-bubble-secondary"
|
||||
chat_class = "chat-end" if user else "chat-start"
|
||||
return Div(cls=f"chat {chat_class}")(
|
||||
Div("user" if user else "assistant", cls="chat-header"),
|
||||
Div(msg, cls=f"chat-bubble {bubble_class}"),
|
||||
Hidden(msg, name="messages"),
|
||||
# Define Thread type for clarity (though not strictly necessary in Python)
|
||||
class Thread(TypedDict):
|
||||
thread_id: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
metadata: dict
|
||||
status: str
|
||||
values: dict
|
||||
interrupts: Dict[str, List[dict]]
|
||||
|
||||
|
||||
# Function to get or set user_id via cookie
|
||||
def get_user_id(request):
|
||||
user_id = request.cookies.get("user_id")
|
||||
if not user_id:
|
||||
user_id = str(uuid.uuid4())
|
||||
return user_id
|
||||
|
||||
|
||||
# Function to render a chat message
|
||||
def ChatMessage(msg, idx):
|
||||
bubble_class = (
|
||||
"chat-bubble-primary" if msg["type"] == "human" else "chat-bubble-secondary"
|
||||
)
|
||||
chat_class = "chat-end" if msg["type"] == "human" else "chat-start"
|
||||
return Div(
|
||||
Div(msg["type"], cls="chat-header"),
|
||||
Div(
|
||||
msg["content"], id=f"chat-content-{idx}", cls=f"chat-bubble {bubble_class}"
|
||||
),
|
||||
id=f"chat-message-{idx}",
|
||||
cls=f"chat {chat_class}",
|
||||
)
|
||||
|
||||
|
||||
# The input field for the user message. Also used to clear the
|
||||
# input field after sending a message via an OOB swap
|
||||
# Function to create the chat input field
|
||||
def ChatInput():
|
||||
return Input(
|
||||
type="text",
|
||||
name="msg",
|
||||
id="msg-input",
|
||||
placeholder="Type a message",
|
||||
@@ -51,33 +73,165 @@ def ChatInput():
|
||||
)
|
||||
|
||||
|
||||
# The main screen
|
||||
@app.get("/")
|
||||
def index():
|
||||
page = Form(hx_post=send, hx_target="#chatlist", hx_swap="beforeend")(
|
||||
Div(id="chatlist", cls="chat-box h-[73vh] overflow-y-auto"),
|
||||
Div(cls="flex space-x-2 mt-2")(
|
||||
Group(ChatInput(), Button("Send", cls="btn btn-primary"))
|
||||
# Sidebar with conversation list
|
||||
async def ConversationList(user_id: str, current_thread_id: str):
|
||||
threads = await langgraph_client.threads.search(
|
||||
metadata={"user_id": user_id}, limit=50, offset=0
|
||||
)
|
||||
return Div(
|
||||
H2("Conversations", cls="text-lg font-bold mb-2"),
|
||||
Div(
|
||||
*[
|
||||
A(
|
||||
f"Thread {i+1} ({thread['created_at']})",
|
||||
href=f"/conversations/{thread['thread_id']}",
|
||||
cls="block p-2 hover:bg-gray-200 rounded"
|
||||
+ (
|
||||
" bg-gray-300"
|
||||
if thread["thread_id"] == current_thread_id
|
||||
else ""
|
||||
),
|
||||
)
|
||||
for i, thread in enumerate(threads)
|
||||
],
|
||||
cls="overflow-y-auto h-[calc(100vh-4rem)]",
|
||||
),
|
||||
cls="w-1/4 bg-gray-100 p-4 border-r",
|
||||
)
|
||||
return Titled("Chatbot Demo", page)
|
||||
|
||||
|
||||
# Handle the form submission
|
||||
@app.post("/send")
|
||||
async def send(msg: str, messages: list[str] = None):
|
||||
if not messages:
|
||||
# Root route: Redirect to a new conversation
|
||||
@app.route("/")
|
||||
def get(request):
|
||||
user_id = get_user_id(request)
|
||||
thread_id = str(uuid.uuid4())
|
||||
return Redirect(
|
||||
f"/conversations/{thread_id}",
|
||||
cookies=[("user_id", user_id, {"httponly": True, "max_age": 31536000})],
|
||||
)
|
||||
|
||||
|
||||
# Conversation route: Display the chat interface with sidebar
|
||||
@app.route("/conversations/{thread_id}")
|
||||
async def get(thread_id: str, request):
|
||||
user_id = get_user_id(request)
|
||||
|
||||
# Create thread with user_id in metadata
|
||||
await langgraph_client.threads.create(
|
||||
thread_id=thread_id, if_exists="do_nothing", metadata={"user_id": user_id}
|
||||
)
|
||||
|
||||
# Fetch thread state
|
||||
try:
|
||||
state = await langgraph_client.threads.get_state(thread_id)
|
||||
messages = state["values"]["messages"]
|
||||
except Exception:
|
||||
messages = []
|
||||
messages.append(msg.rstrip())
|
||||
result = await langgraph_client.runs.wait(
|
||||
None,
|
||||
"agent",
|
||||
input={"messages": messages},
|
||||
)
|
||||
response = result["messages"][-1]["content"]
|
||||
|
||||
return (
|
||||
ChatMessage(msg, True), # The user's message
|
||||
ChatMessage(response.strip(), False), # The chatbot's response
|
||||
ChatInput(),
|
||||
# New Thread button
|
||||
new_thread_button = Button(
|
||||
"New Thread",
|
||||
cls="btn btn-secondary btn-sm mr-2",
|
||||
hx_get="/new-thread",
|
||||
hx_swap="none",
|
||||
)
|
||||
|
||||
# Main chat content
|
||||
chat_content = Div(
|
||||
Div(
|
||||
Titled("Chatbot Demo", ""),
|
||||
new_thread_button,
|
||||
cls="flex justify-between items-center mb-4",
|
||||
),
|
||||
Div(
|
||||
*[ChatMessage(msg, i) for i, msg in enumerate(messages)],
|
||||
id="chatlist",
|
||||
cls="chat-box h-[calc(100vh-8rem)] overflow-y-auto",
|
||||
),
|
||||
Form(
|
||||
Group(ChatInput(), Button("Send", cls="btn btn-primary")),
|
||||
hx_post=f"/conversations/{thread_id}/send-message",
|
||||
hx_target="#chatlist",
|
||||
hx_swap="beforeend",
|
||||
cls="flex space-x-2 mt-2",
|
||||
),
|
||||
cls="flex-1 p-4",
|
||||
)
|
||||
|
||||
# Full page with sidebar and chat
|
||||
page = Div(
|
||||
await ConversationList(user_id, thread_id),
|
||||
chat_content,
|
||||
cls="flex w-full h-full",
|
||||
)
|
||||
|
||||
return Titled(
|
||||
"Chatbot Demo",
|
||||
page,
|
||||
cookies=[("user_id", user_id, {"httponly": True, "max_age": 31536000})],
|
||||
)
|
||||
|
||||
|
||||
# Route for creating a new thread
|
||||
@app.get("/new-thread")
|
||||
def new_thread(request):
|
||||
thread_id = str(uuid.uuid4())
|
||||
return Redirect(f"/conversations/{thread_id}")
|
||||
|
||||
|
||||
# Function to create an assistant message placeholder with SSE
|
||||
def AssistantMessagePlaceholder(thread_id, msg):
|
||||
content_id = f"assistant-content-{uuid.uuid4()}"
|
||||
return Div(
|
||||
Div("", id=content_id, cls="chat-bubble chat-bubble-secondary"),
|
||||
cls="chat chat-start",
|
||||
hx_ext="sse",
|
||||
sse_connect=f"/conversations/{thread_id}/get-message?msg={msg}",
|
||||
sse_swap="message",
|
||||
hx_target=f"#{content_id}",
|
||||
hx_swap="beforeend",
|
||||
)
|
||||
|
||||
|
||||
# Route to handle sending a new message
|
||||
@app.post("/conversations/{thread_id}/send-message")
|
||||
def send_message(thread_id: str, msg: str):
|
||||
user_msg_div = ChatMessage(
|
||||
{"type": "human", "content": msg}, f"user-{uuid.uuid4()}"
|
||||
)
|
||||
assistant_placeholder = AssistantMessagePlaceholder(thread_id, msg)
|
||||
return user_msg_div, assistant_placeholder, ChatInput()
|
||||
|
||||
|
||||
# Generator for streaming assistant responses
|
||||
async def message_generator(thread_id: str, msg: str):
|
||||
async for event in langgraph_client.runs.stream(
|
||||
thread_id,
|
||||
"agent",
|
||||
input={"messages": [{"type": "human", "content": msg}]},
|
||||
stream_mode="messages-tuple",
|
||||
multitask_strategy="interrupt",
|
||||
):
|
||||
if event.event == "messages":
|
||||
for chunk_msg in event.data:
|
||||
chunk = chunk_msg.get("content", "")
|
||||
if chunk.strip():
|
||||
yield f"event: message\ndata: {chunk}\n\n"
|
||||
yield "event: close\ndata:\n\n"
|
||||
|
||||
|
||||
# Route to stream assistant responses via SSE
|
||||
@app.get("/conversations/{thread_id}/get-message")
|
||||
async def get_message(thread_id: str, msg: str):
|
||||
return StreamingResponse(
|
||||
message_generator(thread_id, msg),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||
)
|
||||
|
||||
|
||||
# Run the app if executed directly
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
Reference in New Issue
Block a user