├── database ├── README.md ├── food_orders.db ├── The New Complete Book of Foos.pdf ├── db_manager_sample_use.py ├── requirements.txt └── db_manager.py ├── sample-solution ├── ChatFood-Mobin.mp4 ├── README.md └── final-graph.jpeg ├── ChatFood - Project Description (Fa).pdf ├── LICENSE └── README.md /database/README.md: -------------------------------------------------------------------------------- 1 | Database files 2 | -------------------------------------------------------------------------------- /sample-solution/ChatFood-Mobin.mp4: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample-solution/README.md: -------------------------------------------------------------------------------- 1 | This is a sample solution by Mobin Tirafkan (one the class students). 2 | -------------------------------------------------------------------------------- /database/food_orders.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohammadi-milad-mim/ChatFood/HEAD/database/food_orders.db -------------------------------------------------------------------------------- /sample-solution/final-graph.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohammadi-milad-mim/ChatFood/HEAD/sample-solution/final-graph.jpeg -------------------------------------------------------------------------------- /ChatFood - Project Description (Fa).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohammadi-milad-mim/ChatFood/HEAD/ChatFood - Project Description (Fa).pdf -------------------------------------------------------------------------------- /database/The New Complete Book of Foos.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohammadi-milad-mim/ChatFood/HEAD/database/The New Complete Book of Foos.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Milad Mohammadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /database/db_manager_sample_use.py: -------------------------------------------------------------------------------- 1 | # Import the functions from db_manager.py 2 | from db_manager import food_search, cancel_order, comment_order, check_order_status 3 | 4 | 5 | # Example Search by food name 6 | matching_foods = food_search(food_name="Pizza") 7 | print("Search by food name:") 8 | for food in matching_foods: 9 | print(food) 10 | 11 | # Example Search by restaurant name 12 | matching_foods = food_search(restaurant_name="Pizza") 13 | print("\nSearch by restaurant name:") 14 | for food in matching_foods: 15 | print(food) 16 | 17 | # Example Search by both 18 | matching_foods = food_search(food_name="Pizza", restaurant_name="Pizza Place") 19 | print("\nSearch by both:") 20 | for food in matching_foods: 21 | print(food) 22 | 23 | # Example Cancel Order 24 | order_id, phone_number = 1, "123-456-7890" 25 | cancel_result = cancel_order(order_id, phone_number) 26 | print(f"\nCancel Order Result: {cancel_result}") 27 | 28 | # Example Comment on Order 29 | order_id, person_name = 1, "Alice" 30 | comment_result = comment_order(order_id, person_name, "New comment for this order.") 31 | print(f"\nComment Order Result: {comment_result}") 32 | 33 | # Example Check Order Status 34 | order_id = 10 35 | status_result = check_order_status(order_id) 36 | print(f"\nCheck Order Status Result: {status_result}") 37 | 38 | -------------------------------------------------------------------------------- /database/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.2.1 2 | aiohappyeyeballs==2.4.4 3 | aiohttp==3.11.11 4 | aiosignal==1.3.2 5 | annotated-types==0.7.0 6 | anyio==4.7.0 7 | asyncer==0.0.7 8 | attrs==24.3.0 9 | bidict==0.23.1 10 | cachetools==5.5.0 11 | certifi==2024.12.14 12 | chainlit==1.3.2 13 | charset-normalizer==3.4.1 14 | chevron==0.14.0 15 | click==8.1.8 16 | colorama==0.4.6 17 | dataclasses-json==0.6.7 18 | Deprecated==1.2.15 19 | distro==1.9.0 20 | fastapi==0.115.6 21 | filetype==1.2.0 22 | frozenlist==1.5.0 23 | google-ai-generativelanguage==0.6.10 24 | google-api-core==2.24.0 25 | google-api-python-client==2.157.0 26 | google-auth==2.37.0 27 | google-auth-httplib2==0.2.0 28 | google-generativeai==0.8.3 29 | googleapis-common-protos==1.66.0 30 | greenlet==3.1.1 31 | grpcio==1.68.1 32 | grpcio-status==1.68.1 33 | h11==0.14.0 34 | httpcore==1.0.7 35 | httplib2==0.22.0 36 | httpx==0.28.1 37 | idna==3.10 38 | importlib_metadata==8.5.0 39 | Jinja2==3.1.5 40 | jiter==0.8.2 41 | jsonpatch==1.33 42 | jsonpointer==3.0.0 43 | langchain==0.3.13 44 | langchain-core==0.3.28 45 | langchain-google-genai==2.0.7 46 | langchain-openai==0.2.14 47 | langchain-text-splitters==0.3.4 48 | langchain-together==0.2.0 49 | langgraph==0.2.60 50 | langgraph-checkpoint==2.0.9 51 | langgraph-sdk==0.1.48 52 | langsmith==0.2.7 53 | Lazify==0.4.0 54 | Levenshtein==0.26.1 55 | literalai==0.0.623 56 | MarkupSafe==3.0.2 57 | marshmallow==3.23.2 58 | msgpack==1.1.0 59 | multidict==6.1.0 60 | mypy-extensions==1.0.0 61 | nest-asyncio==1.6.0 62 | numpy==1.26.4 63 | openai==1.59.2 64 | opentelemetry-api==1.28.2 65 | opentelemetry-exporter-otlp==1.28.2 66 | opentelemetry-exporter-otlp-proto-common==1.28.2 67 | opentelemetry-exporter-otlp-proto-grpc==1.28.2 68 | opentelemetry-exporter-otlp-proto-http==1.28.2 69 | opentelemetry-instrumentation==0.49b2 70 | opentelemetry-proto==1.28.2 71 | opentelemetry-sdk==1.28.2 72 | opentelemetry-semantic-conventions==0.49b2 73 | orjson==3.10.13 74 | packaging==23.2 75 | propcache==0.2.1 76 | proto-plus==1.25.0 77 | protobuf==5.29.2 78 | pyasn1==0.6.1 79 | pyasn1_modules==0.4.1 80 | pydantic==2.10.1 81 | pydantic_core==2.27.1 82 | PyJWT==2.10.1 83 | pyparsing==3.2.1 84 | python-dotenv==1.0.1 85 | python-engineio==4.11.2 86 | python-multipart==0.0.9 87 | python-socketio==5.12.1 88 | PyYAML==6.0.2 89 | RapidFuzz==3.11.0 90 | regex==2024.11.6 91 | requests==2.32.3 92 | requests-toolbelt==1.0.0 93 | rsa==4.9 94 | simple-websocket==1.1.0 95 | sniffio==1.3.1 96 | SQLAlchemy==2.0.36 97 | starlette==0.41.3 98 | syncer==2.0.3 99 | tenacity==9.0.0 100 | tiktoken==0.8.0 101 | tomli==2.2.1 102 | tqdm==4.67.1 103 | typing-inspect==0.9.0 104 | typing_extensions==4.12.2 105 | uptrace==1.28.2 106 | uritemplate==4.1.1 107 | urllib3==2.3.0 108 | uvicorn==0.25.0 109 | watchfiles==0.20.0 110 | wrapt==1.17.0 111 | wsproto==1.2.0 112 | yarl==1.18.3 113 | zipp==3.21.0 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatFood: A LLM-powered Agentic Food Ordering Assistant 2 | 3 | ## 📌 Overview 4 | ChatFood is an intelligent chatbot designed as part of an NLP course project using the **LangGraph** framework. It acts as a virtual assistant in a hypothetical food ordering application, offering users various services, including: 5 | 6 | - 📖 **Providing general and specialized food-related information** 7 | - 🍽️ **Recommending dishes based on user preferences** 8 | - 🔍 **Searching for available foods in restaurants** 9 | - 📦 **Tracking and canceling orders** 10 | - 🛠️ **Managing user interactions through an intuitive UI (Chainlit)** 11 | 12 | This project allows students to gain hands-on experience in chatbot development, **multi-agent architectures, RAG-based retrieval, LangGraph workflows, and database management**. 13 | 14 | ## 🎯 Objectives 15 | This project aims to: 16 | - Introduce **LangGraph** for designing an intelligent chatbot with agent-based reasoning. 17 | - Demonstrate the integration of tools like **Tavily**, **LlamaParse**, **LanceDB**, and **Chainlit**. 18 | - Teach best practices in **chatbot architecture**, **LLM-driven interactions**, and **structured output processing**. 19 | - Encourage students to explore **document-based retrieval (RAG)** and **multi-stage reasoning** using **ReAct** and **Plan-and-Execute** architectures. 20 | 21 | ## 🚀 Features 22 | ### ✅ Foods and Meals QA (Information Retrieval) 23 | - Uses **Hybrid RAG (Retrieval-Augmented Generation)** to provide detailed food-related knowledge. 24 | - Retrieves structured information from **a knowledge base (book corpus) or online sources** (via Tavily API). 25 | 26 | ### ✅ Food Recommendations 27 | - Implements **multi-step reasoning** to suggest relevant dishes based on user preferences. 28 | - Ensures that recommended dishes are available in listed restaurants. 29 | 30 | ### ✅ Order Management 31 | - Users can **check order status**, **cancel an order**, and **leave a review**. 32 | - The chatbot interacts with a **SQLite-based order database**. 33 | 34 | ### ✅ Food Search 35 | - Allows users to **search for foods in restaurants** by name or category. 36 | - Uses **natural language search** with **fuzzy matching**. 37 | 38 | ### ✅ Chainlit UI 39 | - Provides a **web-based chat interface** using **Chainlit**. 40 | - Supports real-time interaction with chatbot responses. 41 | 42 | ## 🏗️ Architecture 43 | ChatFood is built using **LangGraph**, leveraging its graph-based workflow to create an **efficient and scalable agentic chatbot**. The system consists of: 44 | - **Food Info Agent**: Handles food-related queries (via RAG & Tavily API). 45 | - **Recommendation Agent**: Suggests food items based on user inputs. 46 | - **Order Manager**: Manages orders (checking status, cancellations, and reviews). 47 | - **Food Search Module**: Retrieves food details from the database. 48 | 49 | 📌 **Graph View of the ChatFood Workflow:** 50 | ![Graph View](sample-solution/final-graph.jpeg) 51 | 52 | ## 📽️ Demo Video 53 | Here is a video demo of the project (by Mobin Tirafkan): 54 | 55 | 56 | https://github.com/user-attachments/assets/f7a67270-803b-41cb-9264-2c82f3b39718 57 | 58 | 59 | ## ⚙️ Tech Stack 60 | - **LangGraph** (for workflow orchestration) 61 | - **LanceDB** (for vector storage & retrieval) 62 | - **Tavily API** (for real-time web search) 63 | - **LlamaParse** (for document parsing) 64 | - **SQLite** (for order management database) 65 | - **Chainlit** (for UI interface) 66 | 67 | ## 🛠️ Installation & Setup 68 | ```sh 69 | # Clone the repository 70 | git clone https://github.com/your-repo/chatfood.git 71 | cd chatfood 72 | 73 | # Create a virtual environment (optional but recommended) 74 | python -m venv env 75 | source env/bin/activate # On Windows use 'env\Scripts\activate' 76 | 77 | # Install dependencies 78 | pip install -r requirements.txt 79 | 80 | # Run the chatbot in the terminal 81 | python chatfood.py 82 | 83 | # Run with Chainlit UI 84 | chainlit run chatfood_ui.py 85 | ``` 86 | 87 | ## 🏁 Usage Guide 88 | - **To get general food information:** 89 | - "What is Sushi?" 90 | - "Is eating yogurt with kebab unhealthy?" 91 | - **To search for food availability:** 92 | - "Which restaurants serve Ghormeh Sabzi?" 93 | - "How much is a Pepperoni Pizza at Milad Restaurant?" 94 | - **To track an order:** 95 | - "What is the status of my order #456?" 96 | - **To cancel an order:** 97 | - "Cancel my order #123." 98 | - **To get food recommendations:** 99 | - "I want a spicy fast food option. What do you suggest?" 100 | 101 | ## 📚 Project Structure 102 | ``` 103 | 📂 chatfood/ 104 | ├── 📜 README.md # Project documentation 105 | ├── 📜 requirements.txt # Dependencies 106 | ├── 📜 chatfood.py # Main chatbot script 107 | ├── 📜 chatfood_ui.py # Chainlit UI 108 | ├── 📂 sample-solutions/ # Graph view & demo video 109 | ├── 📂 db/ # SQLite database for orders & food items 110 | ├── 📂 utils/ # Helper functions 111 | ├── 📂 models/ # LangGraph nodes & logic 112 | ``` 113 | 114 | ## 🏆 Bonus Features 115 | 💡 **(Optional Advanced Features)** 116 | - **Optimized Long Conversations**: Implements trimming, summarization, or filtering for chat history. 117 | - **Streaming Responses**: Supports **incremental response updates** for better UX. 118 | - **Human-in-the-loop (HITL)**: Implements **explicit confirmations for sensitive actions** like order cancellation. 119 | 120 | ## 🤝 Contributions & Support 121 | This project is open to contributions! Feel free to submit **issues**, **pull requests**, or share feedback. 122 | 123 | 📩 For questions, reach out via **[GitHub Issues](https://github.com/your-repo/chatfood/issues)**. 124 | 125 | --- 126 | 🔗 **Developed as part of an NLP Course at Tehran University.** 127 | -------------------------------------------------------------------------------- /database/db_manager.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import Levenshtein 3 | import atexit 4 | 5 | 6 | # atexit.register(lambda: connection.close()) 7 | 8 | 9 | 10 | def food_search(food_name=None, restaurant_name=None, max_distance=1): 11 | """ 12 | Search for foods based on food_name, restaurant_name, or both using edit distance. 13 | :param connection: SQLite database connection 14 | :param food_name: Food name to search for (optional) 15 | :param restaurant_name: Restaurant name to search for (optional) 16 | :param max_distance: Maximum allowed edit distance for a match 17 | :return: List of matching foods 18 | """ 19 | connection = sqlite3.connect('food_orders.db') 20 | 21 | cursor = connection.cursor() 22 | cursor.execute("SELECT id, food_name, food_category, restaurant_name, price FROM foods") 23 | results = cursor.fetchall() 24 | 25 | matches = [] 26 | for food_id, db_food_name, food_category, db_restaurant_name, db_price in results: 27 | food_name_distance = float('inf') 28 | restaurant_name_distance = float('inf') 29 | 30 | if food_name: 31 | food_name_distance_1 = Levenshtein.distance(food_name.lower(), db_food_name.lower(), weights=(0, 1, 1)) 32 | food_name_distance_2 = Levenshtein.distance(food_name.lower(), db_food_name.lower(), weights=(1, 0, 1)) 33 | food_name_distance_3 = Levenshtein.distance(food_name.lower(), db_food_name.lower(), weights=(1, 1, 1)) 34 | food_name_distance = min(food_name_distance_1, food_name_distance_2, food_name_distance_3) 35 | 36 | 37 | if restaurant_name: 38 | restaurant_name_distance_1 = Levenshtein.distance(restaurant_name.lower(), db_restaurant_name.lower(), weights=(0, 1, 1)) 39 | restaurant_name_distance_2 = Levenshtein.distance(restaurant_name.lower(), db_restaurant_name.lower(), weights=(1, 0, 1)) 40 | restaurant_name_distance_3 = Levenshtein.distance(restaurant_name.lower(), db_restaurant_name.lower(), weights=(1, 1, 1)) 41 | 42 | restaurant_name_distance = min(restaurant_name_distance_1, restaurant_name_distance_2, restaurant_name_distance_3) 43 | 44 | if food_name and restaurant_name: 45 | if food_name_distance <= max_distance and restaurant_name_distance <= max_distance: 46 | matches.append({ 47 | 'id': food_id, 48 | 'food_name': db_food_name, 49 | 'food_category': food_category, 50 | 'restaurant_name': db_restaurant_name, 51 | 'price': db_price, 52 | 'edit_distance': min(food_name_distance, restaurant_name_distance) 53 | }) 54 | elif food_name: 55 | if food_name_distance <= max_distance: 56 | matches.append({ 57 | 'id': food_id, 58 | 'food_name': db_food_name, 59 | 'food_category': food_category, 60 | 'restaurant_name': db_restaurant_name, 61 | 'price': db_price, 62 | 'edit_distance': food_name_distance 63 | }) 64 | elif restaurant_name: 65 | if restaurant_name_distance <= max_distance: 66 | matches.append({ 67 | 'id': food_id, 68 | 'food_name': db_food_name, 69 | 'food_category': food_category, 70 | 'restaurant_name': db_restaurant_name, 71 | 'price': db_price, 72 | 'edit_distance': restaurant_name_distance 73 | }) 74 | 75 | matches.sort(key=lambda x: x['edit_distance']) 76 | connection.close() 77 | return matches 78 | 79 | 80 | def cancel_order(order_id, phone_number): 81 | """ 82 | Cancel an order if its status is 'preparation'. 83 | :param connection: SQLite database connection 84 | :param order_id: ID of the order to cancel 85 | :return: Result message 86 | """ 87 | connection = sqlite3.connect('food_orders.db') 88 | cursor = connection.cursor() 89 | 90 | cursor.execute("SELECT status FROM food_orders WHERE id = ? AND person_phone_number = ?", (order_id,phone_number)) 91 | result = cursor.fetchone() 92 | 93 | if result is None: 94 | return f"Order ID {order_id} from {phone_number} does not exist." 95 | 96 | current_status = result[0] 97 | 98 | if current_status == "preparation": 99 | cursor.execute("UPDATE food_orders SET status = 'canceled' WHERE id = ?", (order_id,)) 100 | connection.commit() 101 | connection.close() 102 | return f"Order ID {order_id} from {phone_number} has been successfully canceled." 103 | else: 104 | connection.close() 105 | return f"Order ID {order_id} from {phone_number} cannot be canceled as it is in '{current_status}' status." 106 | 107 | 108 | def comment_order(order_id, person_name ,comment): 109 | """ 110 | Add or overwrite a comment for an order. 111 | :param connection: SQLite database connection 112 | :param order_id: ID of the order to comment on 113 | :param comment: The comment to add or overwrite 114 | :return: Result message 115 | """ 116 | connection = sqlite3.connect('food_orders.db') 117 | cursor = connection.cursor() 118 | 119 | cursor.execute("SELECT id FROM food_orders WHERE id = ?", (order_id,)) 120 | result = cursor.fetchone() 121 | 122 | if result is None: 123 | return f"Order ID {order_id} does not exist." 124 | 125 | cursor.execute("UPDATE food_orders SET comment = ? WHERE id = ?", (comment, order_id)) 126 | connection.commit() 127 | connection.close() 128 | return f"Comment for Order ID {order_id} from {person_name} has been updated." 129 | 130 | 131 | def check_order_status(order_id): 132 | """ 133 | Check the status of an order. 134 | :param connection: SQLite database connection 135 | :param order_id: ID of the order to check 136 | :return: Order status or an error message 137 | """ 138 | connection = sqlite3.connect('food_orders.db') 139 | cursor = connection.cursor() 140 | 141 | cursor.execute("SELECT status FROM food_orders WHERE id = ?", (order_id,)) 142 | result = cursor.fetchone() 143 | connection.close() 144 | if result is None: 145 | return f"Order ID {order_id} does not exist." 146 | 147 | return f"Order ID {order_id} from is currently in '{result[0]}' status." --------------------------------------------------------------------------------