|
|
import streamlit as st |
|
|
import os |
|
|
import warnings |
|
|
from dotenv import load_dotenv |
|
|
from google import genai |
|
|
from typing import Annotated, Literal |
|
|
from typing_extensions import TypedDict |
|
|
from langgraph.graph.message import add_messages |
|
|
from langgraph.graph import StateGraph, START, END |
|
|
from langgraph.prebuilt import ToolNode |
|
|
from langchain_google_genai import ChatGoogleGenerativeAI |
|
|
from langchain_core.tools import tool |
|
|
from langchain_core.messages import HumanMessage, AIMessage |
|
|
from google.genai import types |
|
|
|
|
|
|
|
|
warnings.filterwarnings('ignore') |
|
|
|
|
|
|
|
|
load_dotenv() |
|
|
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") |
|
|
|
|
|
if GOOGLE_API_KEY: |
|
|
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY |
|
|
|
|
|
|
|
|
client = None |
|
|
|
|
|
|
|
|
class GameState(TypedDict): |
|
|
messages: Annotated[list, add_messages] |
|
|
order: list[str] |
|
|
finished: bool |
|
|
|
|
|
|
|
|
GAMEBOT_SYSINT = ( |
|
|
"system", |
|
|
"You are GameBot, an interactive board game expert system. A human will talk to you about " |
|
|
"various board games and you will answer any questions about game rules, strategies, and history " |
|
|
"(and only about board games - no off-topic discussion, but you can chat about the games and their history).\n\n" |
|
|
"You must refuse to discuss any other type of game, including:\n" |
|
|
"- video games (e.g., Mario Kart, Call of Duty)\n" |
|
|
"- mobile games\n" |
|
|
"- sports or outdoor games\n" |
|
|
"- gambling and casino games\n\n" |
|
|
|
|
|
"If the user mentions a non-board game, politely respond that you only cover board games.\n" |
|
|
"Do NOT call any tool for non-board games.\n\n" |
|
|
|
|
|
"You have access to the function `get_list_game()` to list available games.\n" |
|
|
"If a user asks about a game not in the list, do not tell them it's unavailable. " |
|
|
"Instead, print the game_name and call the `search_game_online(game_name)` tool to search online and help them.\n" |
|
|
|
|
|
"If the game is in your list (for example: 'Chess'), feel free to answer detailed questions about its rules, pieces, or strategy using your own knowledge.\n\n" |
|
|
|
|
|
"Be proactive and helpful. Respond in the user's language.\n" |
|
|
"Keep your responses concise and friendly.\n\n" |
|
|
|
|
|
"If any of the tools are unavailable, you can break the fourth wall and tell the user " |
|
|
"that they have not implemented them yet and should keep reading to do so." |
|
|
) |
|
|
|
|
|
WELCOME_MSG = "Welcome to the GameBot expert system! How may I assist you with board games today?" |
|
|
|
|
|
|
|
|
@tool |
|
|
def get_list_game() -> str: |
|
|
"""Provide the latest up-to-date menu.""" |
|
|
return """ |
|
|
Game: |
|
|
Monopoly |
|
|
Scrabble |
|
|
Chess |
|
|
Cluedo |
|
|
Uno |
|
|
Mahjong |
|
|
Mikado |
|
|
""" |
|
|
|
|
|
@tool |
|
|
def search_game_online(game_name: str) -> str: |
|
|
"""Search online information about a board game if not found in the local list.""" |
|
|
prompt = f"Can you give me a short description of the board game '{game_name}'?" |
|
|
response = client.models.generate_content( |
|
|
model='gemini-2.0-flash', |
|
|
contents=prompt, |
|
|
config=types.GenerateContentConfig( |
|
|
tools=[types.Tool(google_search=types.GoogleSearch())], |
|
|
), |
|
|
) |
|
|
return response.candidates[0].content.parts[0].text |
|
|
|
|
|
|
|
|
@st.cache_resource |
|
|
def initialize_graph(): |
|
|
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash") |
|
|
tools = [get_list_game, search_game_online] |
|
|
tool_node = ToolNode(tools) |
|
|
llm_with_tools = llm.bind_tools(tools) |
|
|
|
|
|
def maybe_route_to_tools(state: GameState) -> Literal["tools", END]: |
|
|
if not (msgs := state.get("messages", [])): |
|
|
raise ValueError(f"No messages found when parsing state: {state}") |
|
|
msg = msgs[-1] |
|
|
if hasattr(msg, "tool_calls") and len(msg.tool_calls) > 0: |
|
|
return "tools" |
|
|
else: |
|
|
return END |
|
|
|
|
|
def chatbot_with_tools(state: GameState) -> GameState: |
|
|
defaults = {"order": [], "finished": False} |
|
|
if state["messages"]: |
|
|
new_output = llm_with_tools.invoke([GAMEBOT_SYSINT] + state["messages"]) |
|
|
else: |
|
|
new_output = AIMessage(content=WELCOME_MSG) |
|
|
return defaults | state | {"messages": [new_output]} |
|
|
|
|
|
graph_builder = StateGraph(GameState) |
|
|
graph_builder.add_node("chatbot", chatbot_with_tools) |
|
|
graph_builder.add_node("tools", tool_node) |
|
|
|
|
|
graph_builder.add_conditional_edges("chatbot", maybe_route_to_tools) |
|
|
graph_builder.add_edge("tools", "chatbot") |
|
|
graph_builder.add_edge(START, "chatbot") |
|
|
|
|
|
return graph_builder.compile() |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="GameBot - Board Game Expert", |
|
|
page_icon="π²", |
|
|
layout="centered" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
.main { |
|
|
background-color: #f0f2f6; |
|
|
} |
|
|
.stChatMessage { |
|
|
background-color: blue; |
|
|
border-radius: 10px; |
|
|
padding: 10px; |
|
|
margin: 5px 0; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.title("π² GameBot - Board Game Expert") |
|
|
st.markdown("*Your AI assistant for board game rules, strategies, and history*") |
|
|
|
|
|
|
|
|
if not GOOGLE_API_KEY: |
|
|
st.error("β οΈ Please set your GOOGLE_API_KEY in the .env file") |
|
|
st.info("Create a `.env` file with: `GOOGLE_API_KEY=your_api_key_here`") |
|
|
st.stop() |
|
|
|
|
|
|
|
|
client = genai.Client(api_key=GOOGLE_API_KEY) |
|
|
|
|
|
|
|
|
if "messages" not in st.session_state: |
|
|
st.session_state.messages = [] |
|
|
st.session_state.graph_state = {"messages": [], "order": [], "finished": False} |
|
|
|
|
|
|
|
|
try: |
|
|
graph = initialize_graph() |
|
|
except Exception as e: |
|
|
st.error(f"Error initializing the chatbot: {str(e)}") |
|
|
st.stop() |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.header("βΉοΈ About GameBot") |
|
|
st.markdown(""" |
|
|
GameBot can help you with: |
|
|
- π List of available games |
|
|
- π Game rules and instructions |
|
|
- π― Strategies and tips |
|
|
- π Search for games online |
|
|
""") |
|
|
|
|
|
st.header("π€ How it works ?") |
|
|
st.markdown(""" |
|
|
Want to know more about your favorite board games? Try asking about: |
|
|
Monopoly, Scrabble, Chess, Clue, Uno, Mahjong, Mikado⦠and more! \n |
|
|
π¬ Just send your questions in the chat!\n |
|
|
π« GameBot only covers board games β no video games or online games. |
|
|
""") |
|
|
|
|
|
if st.button("π Clear Chat", use_container_width=True): |
|
|
st.session_state.messages = [] |
|
|
st.session_state.graph_state = {"messages": [], "order": [], "finished": False} |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
if len(st.session_state.messages) == 0: |
|
|
with st.chat_message("assistant", avatar="π²"): |
|
|
st.markdown(WELCOME_MSG) |
|
|
|
|
|
|
|
|
for message in st.session_state.messages: |
|
|
avatar = "π²" if message["role"] == "assistant" else "π€" |
|
|
with st.chat_message(message["role"], avatar=avatar): |
|
|
st.markdown(message["content"]) |
|
|
|
|
|
|
|
|
if prompt := st.chat_input("Ask me about board games..."): |
|
|
|
|
|
st.session_state.messages.append({"role": "user", "content": prompt}) |
|
|
with st.chat_message("user", avatar="π€"): |
|
|
st.markdown(prompt) |
|
|
|
|
|
|
|
|
st.session_state.graph_state["messages"].append(HumanMessage(content=prompt)) |
|
|
|
|
|
|
|
|
with st.chat_message("assistant", avatar="π²"): |
|
|
with st.spinner("Thinking..."): |
|
|
try: |
|
|
config = {"recursion_limit": 100} |
|
|
result = graph.invoke(st.session_state.graph_state, config) |
|
|
st.session_state.graph_state = result |
|
|
|
|
|
|
|
|
last_message = result["messages"][-1] |
|
|
response = last_message.content |
|
|
|
|
|
st.markdown(response) |
|
|
st.session_state.messages.append({"role": "assistant", "content": response}) |
|
|
|
|
|
except Exception as e: |
|
|
error_msg = f"Sorry, I encountered an error: {str(e)}" |
|
|
st.error(error_msg) |
|
|
st.session_state.messages.append({"role": "assistant", "content": error_msg}) |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown( |
|
|
"<div style='text-align: center; color: gray;'> First try on AI agent ! Have fun testing this application.π</div>", |
|
|
unsafe_allow_html=True |
|
|
) |