diff --git a/examples/README.md b/examples/README.md index 42f524309..c65098ffa 100644 --- a/examples/README.md +++ b/examples/README.md @@ -125,6 +125,11 @@ it is useful when you want to quickly do ranking experiments without rewriting a * [MLCon](https://mlconference.ai/machine-learning-advanced-development/adaptive-incontext-learning/) * [data science connect COLLIDE](https://datasciconnect.com/events/collide/agenda/) +### Agentic Chatbot using Vespa + +[![logo](/assets/vespa-logomark-tiny.png) agentic-streamlit-chatbot](agentic-streamlit-chatbot) This sample Streamlit application demonstrates how to use [LangGraph](https://www.langchain.com/langgraph) agentic framework to develop an E-commerce chatbot using Vespa as a retrieval tool. + + For any questions, please register at the Vespa Slack and discuss in the general channel. ---- diff --git a/examples/agentic-streamlit-chatbot/README.md b/examples/agentic-streamlit-chatbot/README.md new file mode 100644 index 000000000..5a953ac3b --- /dev/null +++ b/examples/agentic-streamlit-chatbot/README.md @@ -0,0 +1,66 @@ +# Prerequisites + + +- You need a python virtual environment. For this demo, `Python 3.13.1` was used, but any Python environment 3.11+ should work. +- You will need [Vespa CLI](https://docs.vespa.ai/en/vespa-cli.html) that you can deploy on MacOS with `brew install vespa-cli` +- Libraries dependencies can be installed with: `pip install -R requirements.txt` +- Sign-up with [Tavily](https://tavily.com/) and Get an API key. +- Spin-up a Vespa Cloud [Trial](https://vespa.ai/free-trial) account: + - Login to the account you just created and create a tenant at [console.vespa-cloud.com](https://console.vespa-cloud.com/). + - Save the tenant name. +- A Valid OpenAI API key. Note that you have the option to use any other LLM, which may support Langgraph tools binding. +- Git clone the repo `https://github.com/vespa-engine/system-test.git` +- The ecommerce_hybrid_search app will be deployed. For more information about the app, please review the [README.md](https://github.com/vespa-engine/system-test/blob/master/tests/performance/ecommerce_hybrid_search/dataprep/README.md). You do not have to follow the data prep steps there. Follow the instructions below instead. +- Uncompress the data file: `zstd -d data/vespa_update-96k.json.zst` + + + +# Deploy the Ecommerce Vespa Application + + +- In the system-test repo you just cloned, navigate to `tests/performance/ecommerce_hybrid_search/app` +- Choose a name for your app. For example `ecommercebot` +- Follow instructions in the [**Getting Started**](https://cloud.vespa.ai/en/getting-started) document. Please note the following as you go through the documentation: + - You will need your tenant name you created previously. + - When adding the public certificates with `vespa auth cert`, it will give you the absolute path of the certificate and the private key. Please note them down. + - To feed the application, return to the original directory and run: + ``` + vespa feed data/vespa_update-96k.json + ``` +- You can test the following query from the Vespa CLI: + ``` + vespa query "select id, category, title, price from sources * where default contains 'screwdriver'" + ``` + - You will need the URL of your Vespa application. Run the following command: + ``` + vespa status + ``` + This should return you an output like: + ``` + Container container at https://xxxxx.yyyyy.z.vespa-app.cloud/ is ready + ``` + Note down the URL. + + # Configure and Launch your Streamlit Application + + A template for `secrets.toml` file to store streamlit secrets has been provided. Please create a subdirectory `.streamlit` and copy the template there. + + Update all the fields with all the information collected previously and save the file as `secrets.toml` + + Launch your streamlit application: + ``` + streamlit run streamlit_vespa_app.py + ``` + # Testing the Application + + You can try a mix of questions like: + + `What is the weather in Toronto ?` + + Followed by: + + `I'm looking for a screwdriver` + + And then: + + `Which one do you recommend to fix a watch?` diff --git a/examples/agentic-streamlit-chatbot/Vespa-logo-dark-RGB.png b/examples/agentic-streamlit-chatbot/Vespa-logo-dark-RGB.png new file mode 100644 index 000000000..a8966a698 Binary files /dev/null and b/examples/agentic-streamlit-chatbot/Vespa-logo-dark-RGB.png differ diff --git a/examples/agentic-streamlit-chatbot/data/vespa_update-96k.json.zst b/examples/agentic-streamlit-chatbot/data/vespa_update-96k.json.zst new file mode 100644 index 000000000..e6fe40d7f Binary files /dev/null and b/examples/agentic-streamlit-chatbot/data/vespa_update-96k.json.zst differ diff --git a/examples/agentic-streamlit-chatbot/requirements.txt b/examples/agentic-streamlit-chatbot/requirements.txt new file mode 100644 index 000000000..3b3490193 --- /dev/null +++ b/examples/agentic-streamlit-chatbot/requirements.txt @@ -0,0 +1,6 @@ +pyvespa +langchain_openai +langchain_core +langchain_community +langgraph +streamlit \ No newline at end of file diff --git a/examples/agentic-streamlit-chatbot/streamlit_vespa_app.py b/examples/agentic-streamlit-chatbot/streamlit_vespa_app.py new file mode 100644 index 000000000..16053bf5b --- /dev/null +++ b/examples/agentic-streamlit-chatbot/streamlit_vespa_app.py @@ -0,0 +1,141 @@ + +import streamlit as st +import getpass +import os + +from vespa.application import Vespa +from vespa.io import VespaResponse, VespaQueryResponse +import json + +from langchain_community.tools.tavily_search import TavilySearchResults + +from langchain_openai import ChatOpenAI + +from langgraph.graph import MessagesState +from langchain_core.messages import AIMessage, HumanMessage, SystemMessage + +from langgraph.graph import START, StateGraph +from langgraph.prebuilt import tools_condition, ToolNode + +from PIL import Image + +st.title(":shopping_trolley: Vespa.ai Shopping Assistant") + +# Load an image (ensure the file is in the correct path) +icon = Image.open("Vespa-logo-dark-RGB.png") + +# Display the image in the sidebar +st.sidebar.image(icon, width=500) + +# Fetch secrets +OPENAI_API_KEY = st.secrets["api_keys"]["llm"] +TAVILY_API_KEY = st.secrets["api_keys"]["tavily"] +VESPA_URL = st.secrets["vespa"]["url"] +PUBLIC_CERT_PATH = st.secrets["vespa"]["public_cert_path"] +PRIVATE_KEY_PATH = st.secrets["vespa"]["private_key_path"] + +#if not os.environ.get("OPENAI_API_KEY"): +os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY +#if not os.environ.get("TAVILY_API_KEY"): +os.environ["TAVILY_API_KEY"] = TAVILY_API_KEY + + +def VespaRetriever(UserQuery: str) -> str: + """Retrieves all items for sale matching the user query. + + Args: + UserQuery: User Query for items they are looking for. + """ + + vespa_app = Vespa(url=VESPA_URL, + cert=PUBLIC_CERT_PATH, + key=PRIVATE_KEY_PATH) + + with vespa_app.syncio(connections=1) as session: + query = UserQuery + response: VespaQueryResponse = session.query( + yql="select id, category, title, average_rating, price from sources * where userQuery()", + query=query, + hits=5, + ranking="hybrid", + ) + assert response.is_successful() + + # Extract only the 'fields' content from each entry + filtered_data = [hit["fields"] for hit in response.hits] + + # Convert to a JSON string + json_string = json.dumps(filtered_data, indent=1) + + return json_string + +TavilySearch = TavilySearchResults(max_results=2) + +tools = [TavilySearch, VespaRetriever] + +llm = ChatOpenAI(model="gpt-4o") +llm_with_tools = llm.bind_tools(tools) + +# System message +sys_msg = SystemMessage(content="You are a helpful sales assistant willing to answer any user questions about items to sell. You will try your best to provide all the information regarding an item for sale to a customer.") + +# Node +def assistant(state: MessagesState): + return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]} + +# Graph +builder = StateGraph(MessagesState) + +# Define nodes: these do the work +builder.add_node("assistant", assistant) +builder.add_node("tools", ToolNode(tools)) + +# Define edges: these determine how the control flow moves +builder.add_edge(START, "assistant") +builder.add_conditional_edges( + "assistant", + # If the latest message (result) from assistant is a tool call -> tools_condition routes to tools + # If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END + tools_condition, +) +builder.add_edge("tools", "assistant") + +react_graph = builder.compile() + +#Initialize the chat messages history +if "messages" not in st.session_state.keys(): + st.session_state.messages = [ + {"role": "assistant", "content": "Hello, I'm your Vespa Shopping Assistant using an agentic architecture based on LangGraph. How can I assist you today ?"} + ] + +#Prompt the user input and save +if prompt := st.chat_input(): + st.session_state.messages.append({"role": "user", "content": prompt}) + +#Display the existing chat messages +for message in st.session_state.messages: + with st.chat_message(message["role"]): + st.write(message["content"]) + +#If last message is not from the assistant, we need to generate a new response +if st.session_state.messages[-1]["role"] != "assistant": + question = st.session_state.messages[-1]["content"].replace('?','') + + message_list = [] + messages = react_graph.invoke({"messages": st.session_state.messages}, stream_mode="values") + print(messages) + + for m in messages['messages']: + message_list.append(m) + + print(message_list) + + response_text = next( + (msg.content for msg in reversed(messages['messages']) if isinstance(msg, AIMessage)), + None + ) + with st.chat_message("assistant"): + st.write("Response: ", response_text) + + # **Add the assistant response to session state** + st.session_state.messages.append({"role": "assistant", "content": response_text}) diff --git a/examples/agentic-streamlit-chatbot/template-secrets.toml b/examples/agentic-streamlit-chatbot/template-secrets.toml new file mode 100644 index 000000000..3fd201af4 --- /dev/null +++ b/examples/agentic-streamlit-chatbot/template-secrets.toml @@ -0,0 +1,9 @@ +# .streamlit/secrets.toml + +[vespa] +url = "https://.vespa-app.cloud/" +public_cert_path = "PATH/data-plane-public-cert.pem" +private_key_path = "PATH/data-plane-private-key.pem" +[api_keys] +llm = "sk-" +tavily = "tvly-" \ No newline at end of file