├── .gitignore ├── LICENSE ├── README.md ├── config.yml ├── docker-compose.yml ├── dockerfile-fastapi ├── dockerfile-gradio ├── img ├── chat-with-docs.png ├── chatbot_factory_line_pano.png ├── langsmith.png ├── localhost_6333_dashboard.png ├── localhost_7860_.png ├── localhost_8000_docs.png ├── process-docs.png └── scape.png ├── key.env.example ├── requirements-fastapi.txt ├── requirements-gradio.txt └── src ├── __init__.py ├── agent ├── __init__.py └── agent_handler.py ├── api ├── __init__.py ├── handlers.py ├── models.py └── routes.py ├── loader └── document.py ├── main.py ├── scraper ├── __init__.py └── scraper.py ├── template ├── prefix.txt ├── react_cot.txt └── suffix.txt ├── tools ├── __init__.py ├── doc_search.py └── setup.py ├── ui └── gradio_interface.py └── utils ├── __init__.py ├── config.py └── embedding_selector.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | 7 | # Virtual environment 8 | venv/ 9 | env/ 10 | *.venv 11 | *.env 12 | 13 | # Encrypted API key 14 | /api_key.txt 15 | /encrypted_api_key.bin 16 | 17 | # Log files 18 | *.log 19 | logs/ 20 | 21 | # User-specific files 22 | .DS_Store 23 | *.swp 24 | Thumbs.db 25 | 26 | # IDE/Editor specific files 27 | .vscode/ 28 | .idea/ 29 | *.sublime* 30 | *.sln 31 | *.suo 32 | *.cache 33 | *.log 34 | *.nupkg 35 | *.ncrunch* 36 | *.dotCover 37 | *.resharper* 38 | 39 | # Exclude encrypted API key files 40 | *.bin 41 | 42 | # Exclude configuration files 43 | alfred_config.sh 44 | 45 | # Exclude application data 46 | .data/ 47 | src/scraper/out/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kyle Tobin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 rag_bot: Adaptive Intelligence Chatbot 2 | ### A Next-Gen Stack for Agentic Tool-Using RAG Apps 3 | ![RagBot Factory](/img/chatbot_factory_line_pano.png) 4 | 5 | Welcome to `rag_bot`, a powerful solutions accelerator for building enterprise apps with adaptive intelligence. Rooted in a tool-using agenic architecture and retrieval augmented generation (RAG), `rag_bot` seamlessly integrates state-of-the-art open-source frameworks such as LangChain, LlamaIndex, and FastAPI. This platform is containerized, primed for Infrastructure as Code (IaC) deployments, and harnesses the scalability and advanced filtering capabilities of the Qdrant vector database. Dive into building intelligent applications with `rag_bot` at your side! 6 | 7 | ## 🎯 **Why `rag_bot`?** 8 | 9 | `rag_bot` isn't just a project; it's an innovation accelerator. With a focus on enabling developers like you, here's what you can achieve: 10 | - **Dynamic Conversations:** Powered by vast language models, your bots can hold enriched conversations like never before. 11 | - **Knowledge Access:** Retrieve pertinent information from custom knowledge reservoirs, making your bots more informed. 12 | - **Immediate Development:** You don't need to wait. You can dive right in! Load content into techdocs, test out chatting on that content, and then start pioneering custom tools for your use case! 13 | 14 | ## 🛠️ **Adapt. Customize. Innovate.** 15 | 16 | With `rag_bot`, you're not bound by limitations. The platform is your canvas: 17 | - **Data Integration:** Bring in new data sources, be it databases or internal documents. 18 | - **Model Experimentation:** Choose the language model that fits your narrative. 19 | - **Tool Development:** Extend the agent's capabilities. Integrate calendars, fetch weather data, or even add translation services. 20 | - **Prompt Refinement:** Customize the bot's persona, ensuring it aligns with your vision. 21 | - **Ready-to-Use & Extensible UI:** rag_bot features a basic Gradio UI for chat demos, primed for integration with your custom frontend. 22 | - **Seamless API Integration:** With FastAPI at its core, integrate effortlessly with other services. 23 | 24 | Your chatbot can be as unique as your vision. Customize, adapt, and let your creativity soar. 25 | 26 | ## 🚀 Dive In and Get Started 27 | 28 | Ready to revolutionize conversational agents? The subsequent sections guide you through the journey, right from prerequisites to deployment. Get to know the architecture, explore features, and tailor the platform to your needs. The world of advanced conversational agents awaits you. 29 | 30 | ### Prerequisites 31 | 32 | To use this project, you will need: 33 | 34 | - Docker and Docker Compose installed 35 | - Python 3.7+ 36 | - An OpenAI API key 37 | 38 | ### Setup 39 | 40 | To set up the project: 41 | 42 | 1. Clone this repository to your local machine. 43 | 44 | 2. Rename `key.env.example` to `key.env` and add your OpenAI API key. 45 | 46 | 3. Review `config.yml` and choose `openai` or `local` for your `Embedding_Type` 47 | 48 | 4. In `docker-compose.yml`, update the `volumes` path for `RAG_BOT_QDRANT` to a local folder where you want persistent storage for the vector database. 49 | 50 | 5. Create needed directories for persistant storage 51 | ```bash 52 | mkdir -p .data/qdrant/ 53 | ``` 54 | 55 | 6. Build the Docker images: 56 | ```bash 57 | docker-compose build 58 | ``` 59 | 7. Start the services: 60 | ```bash 61 | docker-compose up -d 62 | ``` 63 | The services will now be running. 64 | 65 | ## 🔥 **Hot-Loading in Docker** 66 | 67 | Hot-loading is enabled through Docker's volume mount feature and Uvicorn's reload capability. This allows for immediate reflection of local code changes in the Docker containers. 68 | 69 | ### Configuration Details: 70 | 71 | - **Docker Compose (`docker-compose.yml`):** 72 | - Local directories are mounted into the Docker containers. 73 | - Example volume mount configuration for a service: 74 | ```yaml 75 | volumes: 76 | - .:/app # Mounts the current directory to /app in the container 77 | ``` 78 | 79 | - **Dockerfile Configurations:** 80 | - Ensure your Dockerfile copies your application and its dependencies: 81 | ```Dockerfile 82 | COPY . /app 83 | RUN pip install -r requirements.txt 84 | ``` 85 | - For FastAPI applications, use the Uvicorn command with the `--reload` flag: 86 | ```Dockerfile 87 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] 88 | ``` 89 | 90 | ### Managing Configuration: 91 | 92 | - Modify volume mounts in `docker-compose.yml` to reflect your project structure. 93 | - Update the Uvicorn command in your Dockerfile according to your app's entry point. 94 | - Rebuild your Docker images after significant changes to the Dockerfile or dependencies. 95 | - Use `.dockerignore` to exclude unnecessary files from being copied into your Docker images. 96 | 97 | 98 | ## 🚢 **Deployment and Usage** 99 | Once the Docker containers are up and running, you can start interacting with the bot via: 100 | 101 | - The **interactive Swagger docs** at [http://localhost:8000/docs](http://localhost:8000/docs) 102 | - The **Gradio Chat Interface** at [http://localhost:7860](http://localhost:7860) 103 | - The **Qdrant Web Interface** at [http://localhost:6333/dashboard](http://localhost:6333/dashboard) 104 | 105 | ### Build the TechDocs Collection 106 | 1. **Scrape Documents:** 107 | - Go to the FastAPI server by navigating to the interactive Swagger docs at [http://localhost:8000/docs](http://localhost:8000/docs). 108 | - Use the `scrape` endpoint to scrape content from a specified URL. You will need to provide the URL you want to scrape in the request body. 109 | - The `scrape` endpoint will return the scraped content which will be processed in the next step. 110 | ![Scraper](img/scape.png) 111 | 112 | 2. **Create a Vector Index:** 113 | - Use the `process-documents` endpoint to create a vector index from the scraped content. 114 | - In case the `techdocs` collection does not exist, the `process-documents` endpoint will create one for you. 115 | - This endpoint will process the scraped documents, create a vector index, and load it into Qdrant. 116 | ![Process Documents](img/process-docs.png) 117 | 3. **Interact with Processed Documents:** 118 | - Now that the documents are processed and loaded into Qdrant, you can start interacting with them. 119 | - Try chatting with the documents via the Gradio Chat Interface at [http://localhost:7860](http://localhost:7860). 120 | - The bot should automatically check the `techdocs` collection for technical information while responding to your queries. If it doesn't, you can instruct the bot by typing "check techdocs" in the chat interface. 121 | ![TechDocs Chat](img/chat-with-docs.png) 122 | 123 | 124 | ## 🏗 **Architecture Overview** 125 | 126 | The rag_bot architecture consists of the following key components: 127 | 128 | - FastAPI - High performance REST API framework. Handles HTTP requests and routes them to application logic. 129 | - Gradio - Interface for interacting with the bot via GUI. 130 | - Qdrant - Vector database for storing document embeddings and enabling similarity search. 131 | - AgentHandler - Orchestrates the initialization and execution of the conversational agent. 132 | - Scraper - A tool that scrapes a web page and converts it to markdown. 133 | - Loader - A tool that loads content from the scrped_data directory to a VectorStoreIndex 134 | - Tools - Custom tools that extend the capabilities of the agent. 135 | 136 | ## ⚙️ **Bot Infrastructure** 137 | Let's take a closer look at some of the key bot infrastructure components: 138 | 139 | ### FastAPI 140 | FastAPI provides a robust web framework for handling the API routes and HTTP requests/responses. 141 | 142 | Some key advantages: 143 | 144 | - Built on modern Python standards like type hints and ASGI. 145 | - Extremely fast - benchmarked to be faster than NodeJS and Go. 146 | - Automatic interactive docs using OpenAPI standards. 147 | 148 | In this project, main.py initializes the application and sets up the /chat endpoint which is the gateway for users to interact with the bot. Functionality can be tested directly via the docs interface: 149 | 150 | ![FastAPI Docs](/img/localhost_8000_docs.png) 151 | 152 | ### Gradio 153 | Gradio serves as the interactive graphical interface allowing users to easily interact with the chatbot, providing a user-friendly way to visualize and test the bot's capabilities. 154 | 155 | ![Gradio Interface](img/localhost_7860_.png) 156 | 157 | ### Qdrant 158 | Qdrant is a vector database optimized for ultra-fast similarity search across large datasets. It is used in this project to store and index document embeddings, enabling the bot to quickly find relevant documents based on a search query or conversation context. 159 | 160 | ![Qdrant Dashboard](/img/localhost_6333_dashboard.png) 161 | 162 | ## 🔧 **Custom Components** 163 | ### AgentHandler 164 | 165 | `AgentHandler` is a central class, designed to initialize and manage the conversational agent within the `rag_bot` framework. It aims to provide developers with a clear and efficient way to handle the conversational agent's components and interactions. 166 | 167 | #### Initialization and Configuration 168 | - **`_initialize()`:** Orchestrates the initialization of all the components required for the agent, ensuring each element is set up correctly. 169 | - **`_setup_config_and_env()`:** Loads configurations and sets up environment variables, providing a context for the agent's operation. 170 | 171 | #### OpenAI Model Management 172 | - **`_setup_openai()`:** Initializes the OpenAI model based on loaded configurations. It includes error handling to log and raise exceptions if any issues occur during initialization. 173 | 174 | #### Memory Management 175 | - **`_setup_memory()`:** Establishes the conversation buffer memory for maintaining chat history, enabling contextual conversations. 176 | 177 | #### Prompt Template Management 178 | - **`_load_prompt_templates()`:** Loads the prompt templates that guide the agent's responses and handles exceptions during the loading process, logging errors for troubleshooting. 179 | 180 | #### Agent Executor Initialization 181 | - **`_initialize_agent_executor()`:** Initializes the `AgentExecutor`, setting up the `ZeroShotAgent` with proper configurations and tools. 182 | 183 | #### Tool Setup and Prompt Construction 184 | - **`_setup_tools() -> list`:** Initializes and returns the tools required for the `ZeroShotAgent`. 185 | - **`_setup_prompt_template() -> PromptTemplate`:** Constructs and returns the prompt template for the agent based on loaded templates and tools. 186 | 187 | #### Agent Setup and User Interaction 188 | - **`_setup_agent() -> AgentExecutor`:** Constructs and returns the `ZeroShotAgent` with all its configurations and tools. 189 | - **`chat_with_agent(user_input: str) -> str`:** Handles user input, manages interaction with the agent, and returns the agent's response, with error handling and logging. 190 | 191 | #### Singleton Instance Retrieval 192 | - **`get_agent_handler() -> AgentHandler`:** Returns the singleton instance of the `AgentHandler`, preventing unnecessary instantiations and initializations. 193 | 194 | #### Usage Example 195 | ```python 196 | agent_handler = get_agent_handler() 197 | response = agent_handler.chat_with_agent("How does photosynthesis work?") 198 | ``` 199 | 200 | 201 | ### Document Scraping Section 202 | 203 | The `scraper` module, located in `/app/src/scraper/scraper_main.py`, serves as a robust utility for extracting content from web pages and converting it into structured markdown format. This module is integral for enabling the framework to access and utilize information from a plethora of web sources. Below is a succinct overview focusing on its core functionalities and workflow for developers aiming to integrate and leverage this module effectively. 204 | 205 | #### Components: 206 | - **WebScraper Class:** 207 | - Inherits from the base Scraper class and implements the Singleton pattern to ensure a unique instance. 208 | - Orchestrates the entire scraping process, from fetching to parsing, and saving the content. 209 | - Leverages `ContentParser` to extract and convert meaningful data from HTML tags into markdown format. 210 | 211 | - **ContentParser Class:** 212 | - Designed to parse and convert meaningful content from supported HTML tags into markdown format. 213 | - Supports a variety of HTML tags including paragraphs, headers, list items, links, inline code, and code blocks. 214 | 215 | #### Workflow: 216 | 217 | 1. **URL Validation:** 218 | - The provided URL undergoes validation to ensure its correctness and accessibility. 219 | - If the URL is invalid, the process is terminated, and an error message is logged. 220 | 221 | 2. **Content Fetching:** 222 | - Content from the validated URL is fetched using HTTP requests. 223 | - Utilizes random user agents to mimic genuine user activity and avoid potential blocking by web servers. 224 | - If the content fetching fails, the process is halted, and an error message is logged. 225 | 226 | 3. **Content Parsing:** 227 | - The fetched content is parsed using BeautifulSoup, and the `ContentParser` class is employed to extract meaningful data. 228 | - The parsed data includes the title, metadata, and the content in markdown format. 229 | 230 | 4. **File Saving:** 231 | - The parsed content is saved to a file, the filename is generated using a hash of the URL. 232 | - The file is stored in a pre-configured data directory. 233 | - If the file saving fails, an error message is logged. 234 | 235 | 5. **Result Return:** 236 | - Upon the successful completion of the scraping process, a success message and the filepath of the saved content are returned. 237 | - If any step in the process fails, an appropriate error message is returned. 238 | 239 | #### Usage: 240 | Developers can initiate the scraping process by invoking the `run_web_scraper(url)` function with the desired URL. This function initializes a `WebScraper` instance and triggers the scraping process, returning a dictionary containing the outcome of the scraping process, including messages indicating success or failure and the location where the scraped data has been saved. 241 | 242 | #### Example: 243 | ```python 244 | result = run_web_scraper("http://example.com") 245 | if result and result.get("message") == "Scraping completed successfully": 246 | print(f"Scraping complete! Saved to {result['data']}") 247 | else: 248 | print(result["message"]) 249 | ``` 250 | 251 | ### Document Loader Section 252 | 253 | The `DocumentLoader` class, located within your project structure, is a pivotal component designed to load, embed, and index documents from a specified source directory into a Qdrant collection. This class is crucial for developers looking to manage and utilize a collection of documents efficiently within the framework. Below is a concise overview of its core functionalities and workflow to aid developers in integrating and leveraging this class effectively. 254 | 255 | #### Components: 256 | - **QdrantCollectionManager Class:** 257 | - Manages Qdrant collections, ensuring their existence or creating them as needed. 258 | - Interacts with the `QdrantClient` to perform operations on the collections. 259 | 260 | - **DocumentLoader Class:** 261 | - Initializes with a source directory, collection name, configuration, and embedding model. 262 | - Loads documents from the source directory and indexes them into the specified Qdrant collection. 263 | - Moves the loaded documents to an output directory after successful indexing. 264 | 265 | #### Workflow: 266 | 1. **Initialization:** 267 | - The `DocumentLoader` initializes with a specified source directory and collection name. 268 | - Loads configurations and sets up environment variables. 269 | - Initializes the embedding model and Qdrant client. 270 | - Ensures the existence of the specified Qdrant collection or creates it if it doesn’t exist. 271 | 272 | 2. **Document Loading and Indexing:** 273 | - Reads documents from the source directory using `SimpleDirectoryReader`. 274 | - Embeds and indexes the documents into the specified Qdrant collection using `VectorStoreIndex`. 275 | - If any error occurs during this process, it is logged, and the error is raised. 276 | 277 | 3. **File Movement:** 278 | - After successful loading and indexing, the documents are moved from the source directory to an output directory. 279 | - If the output directory doesn’t exist, it is created. 280 | 281 | #### Usage: 282 | Developers can instantiate the `DocumentLoader` class with the desired source directory and collection name and call the `load_documents` method to load, embed, and index the documents into the specified Qdrant collection. After successful indexing, the documents are moved to an output directory. 283 | 284 | #### Example: 285 | ```python 286 | document_loader = DocumentLoader(source_dir='/path/to/documents', collection='mycollection') 287 | index = document_loader.load_documents() # This will load, embed, and index the documents and then move them to the output directory. 288 | ``` 289 | 290 | ### Document Search Section 291 | 292 | The `DocumentSearch` class is a component of the framework that is designed to facilitate document searches within a specified collection using a vector store index. This class is integral for developers aiming to implement and leverage efficient document retrieval functionalities within the framework. Below is a succinct overview of its core functionalities and workflow to assist developers in understanding and integrating this class effectively. 293 | 294 | #### Components: 295 | - **DocumentSearch Class:** 296 | - Initializes with a specified collection name and user input query. 297 | - Sets up the vector store index and performs searches on it based on the user input query. 298 | - Handles exceptions and logs errors during the index setup and document search processes. 299 | 300 | #### Workflow: 301 | 1. **Initialization:** 302 | - The `DocumentSearch` initializes with a specified collection name and user input query. 303 | - Loads configurations and sets up environment variables. 304 | - Initializes the Qdrant client and embedding model. 305 | 306 | 2. **Index Setup:** 307 | - Sets up the vector store index for the specified collection using `QdrantVectorStore` and `ServiceContext`. 308 | - If any error occurs during this process, it is logged, and the error is raised. 309 | 310 | 3. **Document Search:** 311 | - Performs a search on the set up index based on the user input query using the query engine. 312 | - Logs the response received from querying the index. 313 | - If any error occurs during this process, it is logged, and the error is raised. 314 | 315 | #### Usage: 316 | Developers can instantiate the `DocumentSearch` class with the desired collection name and user input query and call the `search_documents` method to perform a search on the specified collection and retrieve documents based on the user input query. 317 | 318 | #### Example: 319 | ```python 320 | document_search = DocumentSearch(collection='mycollection', user_input='my query') 321 | response = document_search.search_documents() # This will perform a search on the specified collection and return the response. 322 | ``` 323 | 324 | ### Tools Module Overview 325 | 326 | The `tools` module is designed to enhance the agent's capabilities by integrating external libraries, APIs, and custom functionalities. It serves as a practical extension point for developers looking to customize and extend the agent's abilities. 327 | 328 | #### Key Features: 329 | - **Integration of External Libraries and APIs:** 330 | - The module allows for the incorporation of various libraries and APIs, enabling the agent to access and leverage external functionalities and data. 331 | 332 | - **Contextual Conversations:** 333 | - Tools like `DuckDuckGoSearch` and `DocumentSearch` enable the agent to access real-time, relevant information, allowing for more informed and context-aware conversations. 334 | 335 | #### Included Tools: 336 | 1. **DuckDuckGo Search Wrapper:** 337 | - Conducts DuckDuckGo searches to retrieve real-time search results programmatically. 338 | - Useful for obtaining current and relevant web information for user queries. 339 | 340 | 2. **Document Searcher:** 341 | - Queries specialized vector stores like ‘TechDocs’ for technical documentation. 342 | - Useful for addressing technical inquiries by providing relevant context and information. 343 | 344 | #### Customization and Extension: 345 | - Developers can modify existing tools or create new ones to meet specific needs, allowing for a high degree of customization and adaptability. 346 | 347 | #### Usage: 348 | The `ToolSetup` class is used to initialize and set up tools. Developers can leverage this class to equip the agent with a variety of tools that can be invoked based on the conversational context to enhance the agent's responses. 349 | 350 | #### Evolution: 351 | The `tools` module is dynamic and can be continually refined and expanded. Developers are encouraged to explore new integrations and enhancements to keep improving the agent's capabilities. 352 | 353 | ## 🛠️ **Prompt Engineering** 354 | 355 | Prompt Engineering is a pivotal process in developing conversational agents, focusing on optimizing the prompts sent to Language Models (LLMs) to elicit desired responses. It involves utilizing template files and leveraging platforms and resources to refine interactions with LLMs. 356 | 357 | Developers should leverage the template files and the LangSmith platform along with the additional resources to enhance the prompt engineering process, ensuring optimized interactions with LLMs and refined conversational experiences. 358 | 359 | ### Template Files 360 | The project incorporates three template files located in `/app/src/template` to define the interaction dynamics: 361 | 1. **prefix.txt:** Defines the bot's personality and tool access. 362 | 2. **react_cot.txt:** Outlines the chain of thought reasoning. 363 | 3. **suffix.txt:** Allocates space for the agent scratchpad, memory, and user input. 364 | 365 | ### LangSmith Platform 366 | [LangSmith](https://smith.langchain.com) is an integral platform designed to assist developers in building, debugging, testing, evaluating, and refining LLM-powered applications. It provides a suite of tools focusing on visibility, workflows, and extensibility, making it an indispensable resource for developing production-ready LLM applications. 367 | 368 | #### Key Features: 369 | - **Debugging:** Full visibility into prompts and responses, latency and token usage tracking, and a playground UI for tweaking prompts. 370 | - **Testing:** Allows the creation and running of chains/prompts over datasets for manual review. 371 | - **Evaluating:** Integrates with open-source evaluation modules. 372 | - **Monitoring:** Tracks system metrics and user interactions, and associates user feedback with model runs. 373 | - **Unified Platform:** Connects workflows and allows export of logs and datasets for integration with other tools. 374 | 375 | ![LangSmith](img/langsmith.png) 376 | 377 | ### Additional Resources 378 | Developers are encour## 🛠️ **Adapt. Customize. Innovate.** 379 | ```python 380 | document_search = DocumentSearch(collection='mycollection', user_input='my query') 381 | response = document_search.search_documents() # This will perform a search on the specified collection and return the response. 382 | ``` 383 | 384 | ## 🚧 **Customization and Extendability** 385 | While the project provides a solid architecture, there are ample opportunities for customization and extensibility: 386 | 387 | - Data Sources - Integrate additional knowledge sources like databases, internal company documents etc. 388 | - Models - Experiment with different language models based on your conversational requirements. 389 | - Tools - Build new tools to extend the agent's capabilitieaged to explore the following resources for more insights and guidance on prompt engineering: 390 | - [Prompting Engineering Guide](https://www.promptingguide.ai/): An educational project by DAIR.AI focusing on prompt engineering. 391 | - [LangChain Hub](https://smith.langchain.com/hub): A centralized platform for managing prompts. 392 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # /app/config.yml 2 | chatbot: 3 | name: "RAG_BOT" 4 | Key_File: "key.env" 5 | OpenAI: 6 | chat_temp: 0.0 7 | llm_temp: 0.0 8 | model: "gpt-4-1106-preview" 9 | Scraper: 10 | DATA_DIR: '/app/src/scraper/scraped_data' 11 | USER_AGENTS: 12 | - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36" 13 | - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/89.0" 14 | - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15" 15 | Qdrant: 16 | url: "http://RAG_BOT_QDRANT:6333" 17 | vector_size: "768" #768 for the local model, 1536 for OpenAI 18 | Embedding_Type: "local" #Can be 'local' or 'openai' 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # /app/docker-compose.yml 2 | version: '3.8' 3 | 4 | services: 5 | fastapi: 6 | build: 7 | context: . 8 | dockerfile: dockerfile-fastapi 9 | image: rag_bot_image_fastapi 10 | container_name: RAG_BOT_FASTAPI 11 | ports: 12 | - "8000:8000" 13 | volumes: 14 | - .:/app 15 | 16 | gradio: 17 | build: 18 | context: . 19 | dockerfile: dockerfile-gradio 20 | image: rag_bot_gradio 21 | container_name: RAG_BOT_GRADIO 22 | ports: 23 | - "7860:7860" 24 | volumes: 25 | - ./src/ui/:/src/ui/ 26 | 27 | qdrant: 28 | image: qdrant/qdrant 29 | container_name: RAG_BOT_QDRANT 30 | volumes: 31 | - qdrant_data:/qdrant/storage 32 | ports: 33 | - "6333:6333" 34 | environment: 35 | - QDRANT_WEB_SERVER_ADDRESS=0.0.0.0:6333 36 | 37 | volumes: 38 | qdrant_data: 39 | driver: local 40 | driver_opts: 41 | type: none 42 | o: bind 43 | device: ./.data/qdrant/ 44 | -------------------------------------------------------------------------------- /dockerfile-fastapi: -------------------------------------------------------------------------------- 1 | # /app/dockerfile-fastapi 2 | # Using Debian bullseye-slim as base image 3 | FROM python:3.11-slim-bullseye 4 | 5 | WORKDIR /app 6 | 7 | COPY ./requirements-fastapi.txt . 8 | RUN apt-get update && apt-get install -y --no-install-recommends git && \ 9 | apt-get clean && rm -rf /var/lib/apt/lists/* && \ 10 | pip install --no-cache-dir -r requirements-fastapi.txt 11 | 12 | COPY . . 13 | 14 | CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] 15 | 16 | 17 | -------------------------------------------------------------------------------- /dockerfile-gradio: -------------------------------------------------------------------------------- 1 | # /app/dockerfile-gradio 2 | # Using Debian bullseye-slim as base image 3 | FROM python:3.11-slim-bullseye 4 | 5 | WORKDIR /app 6 | 7 | COPY ./requirements-gradio.txt . 8 | RUN apt-get update && apt-get install -y --no-install-recommends git && \ 9 | apt-get clean && rm -rf /var/lib/apt/lists/* && \ 10 | pip install --no-cache-dir -r requirements-gradio.txt 11 | 12 | COPY src/ui/gradio_interface.py src/ui/gradio_interface.py 13 | 14 | CMD ["python", "src/ui/gradio_interface.py"] 15 | -------------------------------------------------------------------------------- /img/chat-with-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/chat-with-docs.png -------------------------------------------------------------------------------- /img/chatbot_factory_line_pano.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/chatbot_factory_line_pano.png -------------------------------------------------------------------------------- /img/langsmith.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/langsmith.png -------------------------------------------------------------------------------- /img/localhost_6333_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/localhost_6333_dashboard.png -------------------------------------------------------------------------------- /img/localhost_7860_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/localhost_7860_.png -------------------------------------------------------------------------------- /img/localhost_8000_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/localhost_8000_docs.png -------------------------------------------------------------------------------- /img/process-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/process-docs.png -------------------------------------------------------------------------------- /img/scape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/img/scape.png -------------------------------------------------------------------------------- /key.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | LLAMA_INDEX_CACHE_DIR=/app/src/data/llama_index_cache 3 | LANGCHAIN_TRACING_V2=true 4 | LANGCHAIN_ENDPOINT=https://api.smith.langchain.com 5 | LANGCHAIN_API_KEY= 6 | LANGCHAIN_PROJECT= -------------------------------------------------------------------------------- /requirements-fastapi.txt: -------------------------------------------------------------------------------- 1 | openai==1.6.1 2 | python-dotenv==1.0.0 3 | google-search-results==2.4.2 4 | pytest==7.4.4 5 | llama_index==0.9.23 6 | qdrant_client==1.7.0 7 | fastapi==0.108.0 8 | uvicorn==0.25 9 | bs4==0.0.1 10 | nltk==3.8.1 11 | langchain==0.1.0 12 | duckduckgo-search==4.1.1 13 | langchainhub==0.1.14 14 | langchain_openai==0.0.2 15 | sentence-transformers==2.2.2 -------------------------------------------------------------------------------- /requirements-gradio.txt: -------------------------------------------------------------------------------- 1 | gradio==4.13.0 2 | requests==2.31.0 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/src/__init__.py -------------------------------------------------------------------------------- /src/agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/src/agent/__init__.py -------------------------------------------------------------------------------- /src/agent/agent_handler.py: -------------------------------------------------------------------------------- 1 | # src/agent/agent_handler.py 2 | import logging 3 | import traceback 4 | from pathlib import Path 5 | 6 | from langchain.agents import AgentExecutor 7 | from langchain.agents.format_scratchpad import format_log_to_str 8 | from langchain.agents.output_parsers import ReActSingleInputOutputParser 9 | from langchain.memory import ConversationBufferMemory 10 | from langchain_core.prompts import PromptTemplate 11 | from langchain_openai import ChatOpenAI 12 | 13 | from src.tools.setup import ToolSetup 14 | from src.utils.config import load_config, setup_environment_variables 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | # Global variable to store the agent handler instance 19 | _agent_instance = None 20 | 21 | 22 | class AgentHandler: 23 | def __init__(self): 24 | self._initialize() 25 | 26 | def _initialize(self): 27 | self.CONFIG = load_config() 28 | setup_environment_variables(self.CONFIG) 29 | self.llm = ChatOpenAI(model=self.CONFIG["OpenAI"]["model"], temperature=self.CONFIG["OpenAI"]["llm_temp"]) 30 | self.memory = ConversationBufferMemory(memory_key="chat_history") 31 | self.tools = ToolSetup.setup_tools() 32 | self._load_prompt_templates() 33 | self.agent_executor = self._initialize_agent_executor() 34 | 35 | def _load_prompt_templates(self): 36 | """Load templates for the ZeroShotAgent's prompts.""" 37 | try: 38 | template_path = Path("/app/src/template") 39 | self.PROMPT_TEMPLATES = {file.stem: file.read_text() for file in template_path.iterdir() if file.suffix == '.txt'} 40 | except Exception as e: 41 | logging.error(f"Error loading prompt templates: {e}") 42 | raise 43 | 44 | def _setup_prompt_template(self) -> PromptTemplate: 45 | """ 46 | Construct and return the prompt template for the agent based on loaded templates and tools. 47 | 48 | Returns: 49 | PromptTemplate: The constructed prompt template. 50 | 51 | Raises: 52 | KeyError: If a required key is missing in self.PROMPT_TEMPLATES. 53 | """ 54 | tools = self.tools 55 | 56 | # Extracting the templates from self.PROMPT_TEMPLATES 57 | try: 58 | prefix = self.PROMPT_TEMPLATES["prefix"] 59 | react_cot = self.PROMPT_TEMPLATES["react_cot"] 60 | suffix = self.PROMPT_TEMPLATES["suffix"] 61 | except KeyError as e: 62 | logging.error(f"Missing key in PROMPT_TEMPLATES: {e}") 63 | raise 64 | 65 | # Constructing the tools string and tool_names string using list comprehension 66 | tools_str = '\n'.join(f"{tool.name}: {tool.description}" for tool in tools) 67 | tool_names_str = ', '.join(tool.name for tool in tools) 68 | 69 | # Replacing the placeholder with the actual tool names in the template strings 70 | react_cot = react_cot.replace("{tool_names}", tool_names_str) 71 | 72 | # Constructing the final template string 73 | final_template_str = f"{prefix}\n{tools_str}\n{react_cot}\n{suffix}" 74 | 75 | # Creating an instance of PromptTemplate 76 | prompt_template = PromptTemplate( 77 | input_variables=["chat_history", "input", "agent_scratchpad"], 78 | template=final_template_str 79 | ) 80 | 81 | return prompt_template 82 | 83 | def _initialize_agent_executor(self): 84 | prompt = self._setup_prompt_template() 85 | 86 | # Bind the llm with a stop condition 87 | llm_with_stop = self.llm.bind(stop=["\nObservation"]) 88 | 89 | # Correctly retrieving chat history from memory 90 | react_chain = ( 91 | { 92 | "input": lambda x: x["input"], 93 | "agent_scratchpad": lambda x: format_log_to_str(x["intermediate_steps"]), 94 | "chat_history": lambda x: self.memory.load_memory_variables(x).get('chat_history', '') # Correct retrieval of chat history 95 | } 96 | | prompt 97 | | llm_with_stop 98 | | ReActSingleInputOutputParser() 99 | ) 100 | 101 | return AgentExecutor(agent=react_chain, tools=self.tools, verbose=True) 102 | 103 | def chat_with_agent(self, user_input: str): 104 | try: 105 | response = self.agent_executor.invoke({'input': user_input}) 106 | 107 | if 'output' in response and hasattr(response['output'], 'response'): 108 | chat_response = response['output'].response # Extract the response text 109 | else: 110 | chat_response = "Unable to process your request." 111 | 112 | # Log the concise chat response 113 | logging.info(f"User input: '{user_input}' | Chatbot response: '{chat_response}'") 114 | return chat_response 115 | except Exception as e: 116 | tb_str = traceback.format_exception(None, e, e.__traceback__) 117 | logging.error(f"Chat error: {''.join(tb_str)}") 118 | return "An error occurred." 119 | 120 | 121 | def get_agent_handler(): 122 | # Singleton-like accessor for the AgentHandler instance 123 | global _agent_instance 124 | if _agent_instance is None: 125 | _agent_instance = AgentHandler() 126 | return _agent_instance 127 | -------------------------------------------------------------------------------- /src/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/src/api/__init__.py -------------------------------------------------------------------------------- /src/api/handlers.py: -------------------------------------------------------------------------------- 1 | # /app/src/api/handlers.py 2 | 3 | import logging 4 | 5 | from fastapi import Depends, HTTPException 6 | 7 | from src.agent.agent_handler import AgentHandler, get_agent_handler 8 | from src.api.models import (ChatInput, DocumentLoaderRequest, 9 | DocumentLoaderResponse, DocumentSearchRequest, 10 | ScrapeRequest) 11 | from src.loader.document import DocumentLoader 12 | from src.scraper.scraper import run_web_scraper 13 | from src.tools.doc_search import DocumentSearch 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | # ===== CHAT HANDLER ===== 19 | def handle_chat(data: ChatInput, agent: AgentHandler = Depends(get_agent_handler)): 20 | """Handles chat interactions with AgentHandler. 21 | 22 | Args: 23 | data (ChatInput): The user input data 24 | agent (AgentHandler): The AgentHandler instance 25 | 26 | Returns: 27 | dict: Response from agent 28 | """ 29 | response = agent.chat_with_agent(data.user_input) 30 | if isinstance(response, dict): 31 | # Extract just the string message from the response object 32 | return {"response": response["output"].response} 33 | else: 34 | # If response is already a string, return as is 35 | return {"response": response} 36 | 37 | 38 | # ===== WEB SCRAPER HANDLER ===== 39 | async def handle_scrape(data: ScrapeRequest): 40 | """ 41 | Initiates the web scraping process on the provided URL. 42 | 43 | This asynchronous function logs the triggering of the scrape endpoint 44 | and runs the web scraper on the URL provided in the ScrapeRequest object. 45 | 46 | Args: 47 | data (ScrapeRequest): The data containing the URL to be scraped. 48 | 49 | Returns: 50 | Any: The result of the web scraping process. 51 | """ 52 | logger = logging.getLogger("Scraper") 53 | logger.info("Scrape endpoint triggered.") 54 | 55 | # Run the web scraper and return the result. 56 | return run_web_scraper(data.url) 57 | 58 | 59 | # ===== DOCUMENT LOADER HANDLER ===== 60 | def handle_process_documents(data: DocumentLoaderRequest) -> DocumentLoaderResponse: 61 | """ 62 | Processes documents from a specified source directory and loads them into a collection. 63 | 64 | This function initiates a DocumentLoader with the specified source directory and 65 | collection name from the DocumentLoaderRequest object. It then loads the documents 66 | from the source directory into the specified collection. If there are any errors during 67 | the process, it raises an HTTPException. 68 | 69 | Args: 70 | data (DocumentLoaderRequest): The data containing the source directory and the 71 | collection name to which the documents should be loaded. 72 | 73 | Returns: 74 | DocumentLoaderResponse: A response object containing the status of the document 75 | loading process and an optional message. 76 | 77 | Raises: 78 | HTTPException: If there are any errors during the document loading process. 79 | """ 80 | try: 81 | processor = DocumentLoader(source_dir=data.source_dir, collection=data.collection) 82 | processor.load_documents() 83 | return DocumentLoaderResponse(status="success", message="Documents processed successfully") 84 | except Exception as e: 85 | logging.error(f"Error processing documents: {str(e)}") 86 | raise HTTPException(status_code=400, detail=str(e)) 87 | 88 | 89 | # ===== DOCUMENT SEARCHER HANDLER ===== 90 | def handle_document_search(data: DocumentSearchRequest) -> str: 91 | """Handles document search request and returns the raw response. 92 | 93 | This function receives a DocumentSearchRequest containing the collection 94 | name and user query input. It instantiates a DocumentSearch object and 95 | calls search_documents() to perform the actual search. 96 | 97 | The search_documents() method is returning a raw string response rather 98 | than a list of results. So this handler simply returns the raw string. 99 | 100 | No iteration or post-processing is done on the result string. The client 101 | must handle the raw response appropriately. 102 | 103 | Args: 104 | data (DocumentSearchRequest): The request data containing the collection and query. 105 | 106 | Returns: 107 | str: The raw response string from the DocumentSearch. 108 | 109 | Raises: 110 | HTTPException: If any exception occurs during the search process. The 111 | message will contain the underlying error details. 112 | 113 | Usage: 114 | 115 | ``` 116 | request_data = DocumentSearchRequest( 117 | collection="mydocs", 118 | user_input="hello world" 119 | ) 120 | 121 | response_string = handle_document_search(request_data) 122 | 123 | print(response_string) 124 | ``` 125 | """ 126 | try: 127 | document_search = DocumentSearch(collection=data.collection, user_input=data.user_input) 128 | 129 | results = document_search.search_documents() 130 | logging.debug(f"Raw results: {results}") 131 | 132 | return str(results) 133 | 134 | except Exception as e: 135 | logging.error(f"Error searching documents: {str(e)}") 136 | raise HTTPException(status_code=400, detail=str(e)) 137 | -------------------------------------------------------------------------------- /src/api/models.py: -------------------------------------------------------------------------------- 1 | # /app/src/api/models.py 2 | import logging 3 | from typing import Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | # === Chat Models === 11 | class ChatInput(BaseModel): 12 | """ 13 | Model representing the user input for chat interactions. 14 | 15 | Attributes: 16 | user_input (str): The input string from the user to the chat. 17 | """ 18 | user_input: str = "What are the basic steps to get rag_bot up and running?" 19 | 20 | 21 | class ChatOutput(BaseModel): 22 | """ 23 | Model representing the response from the chat agent. 24 | 25 | Attributes: 26 | response (str): The response string from the chat agent to the user. 27 | """ 28 | response: str 29 | 30 | 31 | # === Web Scraper Models === 32 | class ScrapeRequest(BaseModel): 33 | """ 34 | Model representing the request to initiate web scraping. 35 | 36 | Attributes: 37 | url (str): The URL of the web page to be scraped. 38 | """ 39 | url: str = 'https://github.com/kylejtobin/rag_bot' 40 | 41 | 42 | class ScrapeResponse(BaseModel): 43 | """ 44 | Model representing the response from the web scraping process. 45 | 46 | Attributes: 47 | message (str): The status message indicating the success or failure 48 | of the scraping process. 49 | data (Optional[str]): The scraped data from the web page if the 50 | scraping process is successful. None, if unsuccessful. 51 | """ 52 | message: str 53 | data: Optional[str] 54 | 55 | 56 | # === Document Loader Models === 57 | class DocumentLoaderResponse(BaseModel): 58 | """ 59 | Model representing the response from the document loading process. 60 | 61 | Attributes: 62 | status (str): The status of the document loading process. 63 | It will be 'success' if the documents are processed successfully. 64 | message (Optional[str]): Optional message field to convey any additional 65 | information or details about the process. 66 | """ 67 | status: str 68 | message: Optional[str] 69 | 70 | 71 | class DocumentLoaderRequest(BaseModel): 72 | """ 73 | Model representing the request to initiate document loading. 74 | 75 | Attributes: 76 | source_dir (str): The directory from where the documents are to be loaded. 77 | Default is set to the directory where scraped data is stored. 78 | collection (str): The name of the collection to which the documents 79 | should be loaded. Default is "techdocs". 80 | """ 81 | source_dir: str = '/app/src/scraper/scraped_data' 82 | collection: str = "techdocs" 83 | 84 | 85 | # === Document Search Models === 86 | class DocumentSearchRequest(BaseModel): 87 | """ 88 | Model representing the request to initiate document search. 89 | 90 | Attributes: 91 | collection (str): The name of the collection to be queried. 92 | user_input (str): The user input query for searching documents. 93 | """ 94 | collection: str 95 | user_input: str 96 | -------------------------------------------------------------------------------- /src/api/routes.py: -------------------------------------------------------------------------------- 1 | # /app/src/api/routes.py 2 | import logging 3 | 4 | from fastapi import APIRouter 5 | 6 | from src.agent.agent_handler import get_agent_handler 7 | from src.api.handlers import (handle_chat, handle_document_search, 8 | handle_process_documents, handle_scrape) 9 | from src.api.models import (ChatInput, ChatOutput, DocumentLoaderRequest, 10 | DocumentLoaderResponse, DocumentSearchRequest, 11 | ScrapeRequest, ScrapeResponse) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | # Initialize the router. 17 | router = APIRouter() 18 | 19 | 20 | # === Chat Endpoint === 21 | @router.post("/chat/", response_model=ChatOutput) 22 | def chat_endpoint(data: ChatInput): 23 | """ 24 | Endpoint to interact with the chat agent. 25 | 26 | This function receives user input, passes it to the chat handler, 27 | and returns the chat agent's response. 28 | 29 | Args: 30 | data (ChatInput): The user input data encapsulated in a ChatInput object. 31 | 32 | Returns: 33 | ChatOutput: The response from the chat agent encapsulated in a ChatOutput object. 34 | """ 35 | # Delegate to the chat handler and return the response. 36 | agent = get_agent_handler() 37 | return handle_chat(data, agent) 38 | 39 | 40 | # === Web Scraper Endpoint === 41 | @router.post("/scrape/", response_model=ScrapeResponse) 42 | async def scrape_endpoint(data: ScrapeRequest): 43 | """ 44 | Endpoint to initiate the web scraping process. 45 | 46 | This asynchronous function receives a URL, passes it to the scrape handler, 47 | and returns the result of the scraping process. 48 | 49 | Args: 50 | data (ScrapeRequest): The data containing the URL to be scraped. 51 | 52 | Returns: 53 | ScrapeResponse: The result of the scraping process encapsulated in a ScrapeResponse object. 54 | """ 55 | # Delegate to the scrape handler and return the response. 56 | return await handle_scrape(data) 57 | 58 | 59 | # === Document Loader Endpoint === 60 | @router.post("/process-documents/", response_model=DocumentLoaderResponse) 61 | def process_documents_endpoint(data: DocumentLoaderRequest) -> DocumentLoaderResponse: 62 | """ 63 | Endpoint to initiate the document loading process. 64 | 65 | This function receives the source directory and the collection name, 66 | passes them to the document loader handler, and returns the status of the 67 | processed files encapsulated in a DocumentLoaderResponse object. 68 | 69 | Args: 70 | data (DocumentLoaderRequest): The data containing the source directory 71 | and the collection name to which the documents should be loaded. 72 | 73 | Returns: 74 | DocumentLoaderResponse: A response object containing the status of the document 75 | loading process and an optional message. 76 | """ 77 | return handle_process_documents(data) 78 | 79 | 80 | # === Document Search Endpoint === 81 | @router.post("/search-documents/", response_model=str) 82 | def search_documents_endpoint(data: DocumentSearchRequest) -> str: 83 | """ 84 | Endpoint to initiate the document search process. 85 | 86 | Args: 87 | data (DocumentSearchRequest): The data containing the collection name and user input. 88 | 89 | Returns: 90 | DocumentSearchResponse: The result of the document search process. 91 | """ 92 | return handle_document_search(data) 93 | -------------------------------------------------------------------------------- /src/loader/document.py: -------------------------------------------------------------------------------- 1 | # /src/loader/document.py 2 | import logging 3 | import os 4 | import shutil 5 | 6 | from llama_index import (ServiceContext, SimpleDirectoryReader, StorageContext, 7 | VectorStoreIndex) 8 | from llama_index.vector_stores.qdrant import QdrantVectorStore 9 | from qdrant_client import QdrantClient 10 | 11 | from src.utils.config import load_config, setup_environment_variables 12 | from src.utils.embedding_selector import EmbeddingConfig, EmbeddingSelector 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class QdrantCollectionManager: 18 | 19 | @staticmethod 20 | def collection_exists(client: QdrantClient, collection_name: str) -> bool: 21 | try: 22 | client.get_collection(collection_name) 23 | return True 24 | except Exception: 25 | return False 26 | 27 | @staticmethod 28 | def create_collection(client: QdrantClient, collection_name: str, vector_size: int): 29 | client.recreate_collection( 30 | collection_name=collection_name, 31 | vectors_config={ 32 | "size": vector_size, 33 | "distance": "Cosine" 34 | } 35 | ) 36 | 37 | @staticmethod 38 | def ensure_collection(client: QdrantClient, collection_name: str, vector_size: int): 39 | if not QdrantCollectionManager.collection_exists(client, collection_name): 40 | QdrantCollectionManager.create_collection(client, collection_name, vector_size) 41 | 42 | 43 | class DocumentLoader: 44 | 45 | def __init__(self, source_dir='/app/src/scraper/scraped_data', collection="techdocs"): 46 | self.source_dir = source_dir 47 | self.collection_name = collection 48 | self.CONFIG = load_config() 49 | setup_environment_variables(self.CONFIG) 50 | self.embedding_config = EmbeddingConfig(type=self.CONFIG["Embedding_Type"]) 51 | self.embed_model = EmbeddingSelector(self.embedding_config).get_embedding_model() 52 | 53 | self.client = QdrantClient(url="http://RAG_BOT_QDRANT:6333") 54 | 55 | if not QdrantCollectionManager.collection_exists(self.client, collection): 56 | QdrantCollectionManager.create_collection(self.client, collection, self.CONFIG["Qdrant"]["vector_size"]) 57 | 58 | def load_documents(self): 59 | try: 60 | service_context = ServiceContext.from_defaults(embed_model=self.embed_model) 61 | documents = SimpleDirectoryReader(self.source_dir).load_data() 62 | vector_store = QdrantVectorStore(client=self.client, collection_name=self.collection_name) 63 | storage_context = StorageContext.from_defaults(vector_store=vector_store) 64 | index = VectorStoreIndex.from_documents(documents, storage_context=storage_context, service_context=service_context) 65 | 66 | # Move the files after successfully loading them to the vector index 67 | self.move_files_to_out() 68 | 69 | return index 70 | except Exception as e: 71 | logging.error(f"load_documents: Error - {str(e)}") 72 | raise e 73 | 74 | def move_files_to_out(self): 75 | out_dir = '/app/src/scraper/out' 76 | if not os.path.exists(out_dir): 77 | os.makedirs(out_dir) 78 | 79 | for filename in os.listdir(self.source_dir): 80 | file_path = os.path.join(self.source_dir, filename) 81 | if os.path.isfile(file_path): 82 | shutil.move(file_path, os.path.join(out_dir, filename)) 83 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # /app/src/main.py 2 | 3 | # Primary Components 4 | from fastapi import FastAPI 5 | 6 | # Internal Modules 7 | from src.api.routes import router 8 | from src.utils.config import load_config, setup_environment_variables 9 | from src.agent.agent_handler import get_agent_handler # Dependency function and AgentHandler for the application 10 | import logging 11 | import sys 12 | 13 | logger = logging.getLogger(__name__) 14 | logging.basicConfig( 15 | level=logging.INFO, 16 | format="%(levelname)s: %(name)s - %(funcName)s - %(message)s", 17 | stream=sys.stdout, 18 | ) 19 | 20 | # Load configuration and set up environment variables 21 | config = load_config() 22 | setup_environment_variables(config) 23 | 24 | 25 | # Initialize the FastAPI application 26 | app = FastAPI() 27 | 28 | 29 | @app.on_event("startup") 30 | async def startup_event(): 31 | """ 32 | Actions to be performed when the application starts up. 33 | Currently initializes the AgentHandler. Extend this function if more startup logic is needed. 34 | """ 35 | app.agent_instance = get_agent_handler() 36 | 37 | 38 | @app.on_event("shutdown") 39 | async def shutdown_event(): 40 | """ 41 | Cleanup actions to be performed when the application shuts down. 42 | Extend this function if any cleanup logic for components like AgentHandler is required. 43 | """ 44 | pass 45 | 46 | # Include the API router 47 | app.include_router(router) 48 | -------------------------------------------------------------------------------- /src/scraper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/src/scraper/__init__.py -------------------------------------------------------------------------------- /src/scraper/scraper.py: -------------------------------------------------------------------------------- 1 | # /app/src/scraper/scraper_main.py 2 | import hashlib 3 | import logging 4 | import os 5 | import random 6 | import sys 7 | from urllib.parse import urlparse 8 | 9 | import requests 10 | from bs4 import BeautifulSoup, Tag 11 | from requests.exceptions import RequestException 12 | 13 | from src.utils.config import load_config 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | # WebScraper Utilities 19 | def setup_logging(): 20 | """ 21 | Set up the logging configuration for the scraper. 22 | 23 | This function configures the logging module to output INFO and above logs 24 | to the stdout, while WARNING and above logs are redirected to stderr. 25 | It uses a custom format that prefixes each log message with "RAG_BOT" 26 | followed by the log timestamp, the log level, and the actual log message. 27 | 28 | Returns: 29 | bool: Always returns True to indicate successful setup. 30 | """ 31 | 32 | # Basic configuration for the logging module: 33 | # - Direct all logs to stdout 34 | # - Set the lowest level of logs to be captured as INFO 35 | # - Define a custom format for the logs 36 | logging.basicConfig( 37 | stream=sys.stdout, # Logs will go to stdout 38 | level=logging.INFO, 39 | format="RAG_BOT: %(asctime)s - %(levelname)s - %(message)s" 40 | ) 41 | 42 | # Create a separate handler for logs of level WARNING and above. 43 | # This handler will send these logs to stderr instead of stdout, 44 | # which is helpful to quickly identify warnings and errors. 45 | stderr_handler = logging.StreamHandler(sys.stderr) 46 | stderr_handler.setLevel(logging.WARNING) 47 | stderr_handler.setFormatter(logging.Formatter("RAG_BOT: %(asctime)s - %(levelname)s - %(message)s")) 48 | 49 | # Add the stderr handler to the root logger 50 | logging.getLogger().addHandler(stderr_handler) 51 | 52 | return True 53 | 54 | 55 | # WebScraper Base Class 56 | class Scraper: 57 | """ 58 | The Scraper class is designed to provide base functionality for web scraping tasks. 59 | 60 | Attributes: 61 | - logger (Logger): An instance of the logging.Logger class to facilitate event logging. 62 | - CONFIG (dict): Configuration data loaded from a configuration source (like a JSON file). 63 | 64 | Methods: 65 | - get_random_user_agent(): Return a random user agent from the list of user agents in the configuration. 66 | """ 67 | 68 | def __init__(self): 69 | """ 70 | Initialize the Scraper instance. 71 | 72 | This method initializes logging for the scraper and loads configuration data, 73 | which among other things, may contain a list of user agents to be used for scraping tasks. 74 | """ 75 | # Initialize logger for the Scraper class 76 | self.logger = logging.getLogger("Scraper") 77 | 78 | # Load configuration data for the scraper 79 | self.CONFIG = load_config() 80 | 81 | def get_random_user_agent(self): 82 | """ 83 | Fetch and return a random user agent string from the list specified in the configuration. 84 | 85 | The method chooses a user agent randomly from the list in the CONFIG attribute. Using 86 | random user agents can help mimic genuine user activity, potentially avoiding blocking 87 | by web servers during scraping tasks. 88 | 89 | Returns: 90 | - str: A randomly selected user agent string. 91 | """ 92 | return random.choice(self.CONFIG["Scraper"]["USER_AGENTS"]) 93 | 94 | 95 | # WebScraper Content Parser 96 | class ContentParser: 97 | """ 98 | A class designed to extract meaningful content from HTML tags and 99 | convert them into a markdown format. 100 | 101 | Supported HTML tags include paragraphs, headers (h1-h6), list items, links, 102 | inline code, and code blocks. 103 | """ 104 | 105 | # Dictionary that maps HTML tags to their corresponding extraction methods 106 | TAG_TYPES = { 107 | "p": "text", 108 | "h1": "header", 109 | "h2": "header", 110 | "h3": "header", 111 | "h4": "header", 112 | "h5": "header", 113 | "h6": "header", 114 | "li": "list_item", 115 | "a": "link", 116 | "code": "code", 117 | "pre": "pre" 118 | } 119 | 120 | def extract_content(self, element): 121 | """ 122 | Extract content from a given HTML element and its descendants, 123 | converting it into markdown format. 124 | 125 | Args: 126 | - element (Tag): A Beautiful Soup Tag object to extract content from. 127 | 128 | Returns: 129 | - str: The extracted content in markdown format. 130 | """ 131 | content_list = [] 132 | 133 | # Check if the element is a valid Beautiful Soup Tag 134 | if isinstance(element, Tag): 135 | 136 | # Get the type of tag (e.g., header, text, link) 137 | tag_type = self.TAG_TYPES.get(element.name) 138 | 139 | if tag_type: 140 | # Find the corresponding extraction method for the tag type 141 | content_method = getattr(self, f"_extract_{tag_type}_content") 142 | 143 | # Extract content from the tag 144 | content_list.append(content_method(element)) 145 | else: 146 | # If the tag isn't directly mapped in TAG_TYPES, 147 | # check its children for content extraction 148 | for child in element.children: 149 | content_list.append(self.extract_content(child)) 150 | 151 | # Join the extracted content list into a single markdown string 152 | return "\n\n".join(filter(None, content_list)) 153 | 154 | def _extract_text_content(self, element): 155 | """Extract content from a paragraph tag.""" 156 | return element.get_text(strip=True) 157 | 158 | def _extract_header_content(self, element): 159 | """Extract content from header tags (h1-h6) and convert to markdown format.""" 160 | return "#" * int(element.name[1]) + " " + element.get_text(strip=True) 161 | 162 | def _extract_list_item_content(self, element): 163 | """Extract content from list item tags and convert to markdown format.""" 164 | return "* " + element.get_text(strip=True) 165 | 166 | def _extract_link_content(self, element): 167 | """Extract content from anchor tags, converting them to markdown links.""" 168 | text = element.get_text(strip=True) 169 | href = element.get('href', '') 170 | return f"[{text}]({href})" 171 | 172 | def _extract_code_content(self, element): 173 | """Extract content from inline code tags and convert to markdown format.""" 174 | return f"`{element.get_text(strip=True)}`" 175 | 176 | def _extract_pre_content(self, element): 177 | """Extract content from code block tags and convert to markdown format.""" 178 | return f"```\n{element.get_text()}\n```" 179 | 180 | 181 | # WebScraper Class 182 | class SingletonMeta(type): 183 | """Metaclass for the Singleton design pattern. 184 | 185 | Ensures that only one instance of a class inheriting this metaclass exists. 186 | """ 187 | 188 | _instances = {} # Store created instances 189 | 190 | def __call__(cls, *args, **kwargs): 191 | if cls not in cls._instances: 192 | cls._instances[cls] = super().__call__(*args, **kwargs) 193 | return cls._instances[cls] 194 | 195 | 196 | class WebScraper(Scraper, metaclass=SingletonMeta): 197 | """Main WebScraper class for scraping content from websites and saving to a file. 198 | 199 | Inherits base scraper functionalities and uses a singleton pattern for unique instance handling. 200 | """ 201 | 202 | def __init__(self): 203 | super().__init__() 204 | self.logger = logging.getLogger(__name__) 205 | self.content_parser = ContentParser() 206 | self.CONFIG = load_config() 207 | self.setup_data_directory() 208 | 209 | def setup_data_directory(self): 210 | """Sets up the data directory for saving scraped content.""" 211 | if not os.path.exists(self.CONFIG["Scraper"]["DATA_DIR"]): 212 | os.makedirs(self.CONFIG["Scraper"]["DATA_DIR"]) 213 | self.logger.info("Data directory created successfully.") 214 | else: 215 | self.logger.info("Data directory already exists.") 216 | 217 | @staticmethod 218 | def is_valid_url(url): 219 | """Validates the given URL. 220 | 221 | Args: 222 | url (str): The URL to validate. 223 | 224 | Returns: 225 | bool: True if URL is valid, otherwise False. 226 | """ 227 | try: 228 | parsed_url = urlparse(url) 229 | return bool(parsed_url.netloc) and bool(parsed_url.scheme) 230 | except ValueError: 231 | return False 232 | 233 | def fetch_content(self, url): 234 | """Fetches content from the given URL using HTTP requests. 235 | 236 | Args: 237 | url (str): The URL to fetch content from. 238 | 239 | Returns: 240 | str: Fetched content if successful, otherwise None. 241 | """ 242 | self.logger.info(f"Attempting to fetch content from {url}") 243 | headers = {"User-Agent": self.get_random_user_agent()} 244 | try: 245 | response = requests.get(url, headers=headers, timeout=10) 246 | if 200 <= response.status_code < 300: 247 | return response.text 248 | else: 249 | self.logger.warning(f"Received a non-2xx status code ({response.status_code}) from {url}") 250 | return None 251 | except RequestException as e: 252 | self.logger.error(f"Error fetching content from {url}: {e}") 253 | return None 254 | 255 | def parse_content(self, content): 256 | """Parses the fetched content using BeautifulSoup and extracts the meaningful data. 257 | 258 | Args: 259 | content (str): The fetched web content. 260 | 261 | Returns: 262 | dict: A dictionary containing the parsed data. 263 | """ 264 | self.logger.info("Parsing the content.") 265 | soup = BeautifulSoup(content, 'html.parser') 266 | 267 | parsed_data = { 268 | "title": soup.title.string if soup.title else "", 269 | "metadata": { 270 | "description": soup.find("meta", attrs={"name": "description"})["content"] if soup.find("meta", attrs={"name": "description"}) else "" 271 | }, 272 | "content": self.content_parser.extract_content(soup.body) # Use ContentParser to extract content 273 | } 274 | 275 | self.logger.info("Content parsed successfully.") 276 | return parsed_data 277 | 278 | @staticmethod 279 | def generate_filename(url): 280 | """Generates a filename using a hash of the URL. 281 | 282 | Args: 283 | url (str): The URL to hash and generate the filename. 284 | 285 | Returns: 286 | str: The generated filename with a path. 287 | """ 288 | url_hash = hashlib.md5(url.encode()).hexdigest() 289 | return os.path.join(load_config()["Scraper"]["DATA_DIR"], f"{url_hash}.md") 290 | 291 | def save_to_file(self, url, content): 292 | """Saves the parsed content to a file. 293 | 294 | Args: 295 | url (str): The URL used to generate the filename. 296 | content (str): The parsed content to save. 297 | 298 | Returns: 299 | str: The filepath where content was saved if successful, otherwise None. 300 | """ 301 | self.logger.info("Saving content to file.") 302 | filepath = self.generate_filename(url) 303 | try: 304 | with open(filepath, 'w', encoding='utf-8') as f: 305 | f.write(content) 306 | self.logger.info(f"Content saved successfully to {filepath}.") 307 | except Exception as e: 308 | self.logger.error(f"Error saving content to {filepath}: {e}") 309 | return None 310 | return filepath 311 | 312 | def scrape_site(self, url): 313 | """Main method for orchestrating the entire scraping process. 314 | 315 | Args: 316 | url (str): The URL to scrape. 317 | 318 | Returns: 319 | dict: Contains a message indicating the outcome and, if successful, the filepath where content was saved. 320 | """ 321 | self.logger.info(f"Starting scraping for URL: {url}") 322 | 323 | if not self.is_valid_url(url): 324 | self.logger.error("Provided URL is invalid.") 325 | return {"message": "Invalid URL", "data": ""} 326 | 327 | content = self.fetch_content(url) 328 | if not content: 329 | self.logger.error("Failed to fetch content from URL.") 330 | return {"message": "Failed to fetch content from URL", "data": ""} 331 | 332 | parsed_data = self.parse_content(content) 333 | parsed_content = parsed_data["content"] 334 | 335 | filepath = self.save_to_file(url, parsed_content) 336 | if not filepath: 337 | self.logger.error("Failed to save content.") 338 | return {"message": "Failed to save content", "data": ""} 339 | 340 | self.logger.info("Scraping completed successfully.") 341 | return {"message": "Scraping completed successfully", "data": filepath} 342 | 343 | 344 | # WebScraper Main function 345 | def run_web_scraper(url): 346 | """ 347 | Initializes a WebScraper instance and triggers the scraping process for the given URL. 348 | 349 | Args: 350 | url (str): The URL of the website to be scraped. 351 | 352 | Returns: 353 | dict: A dictionary containing the result of the scraping process. 354 | This may include messages indicating success or failure and 355 | potentially where the scraped data has been saved. 356 | """ 357 | scraper = WebScraper() 358 | return scraper.scrape_site(url) 359 | 360 | 361 | if __name__ == "__main__": 362 | # Try setting up logging. Logging is essential to track the progress 363 | # and troubleshoot any issues that arise during the scraping process. 364 | if setup_logging(): 365 | 366 | # Prompt the user for the URL to be scraped 367 | url = input("Enter URL to scrape: ") 368 | 369 | # Begin the scraping process 370 | result = run_web_scraper(url) 371 | 372 | # If the scraping was successful, display a success message with 373 | # details about where the data was saved. Otherwise, display an error message. 374 | if result and result.get("message") == "Scraping completed successfully": 375 | print(f"Scraping complete! Saved to {result['data']}") 376 | else: 377 | print(result["message"]) 378 | 379 | # If logging setup fails, exit the program with an error message. 380 | else: 381 | print("Failed to set up logging. Exiting.") 382 | -------------------------------------------------------------------------------- /src/template/prefix.txt: -------------------------------------------------------------------------------- 1 | Your name is RAG_BOT. If asked to identify yourself, respond with your name. 2 | 3 | The sentiment of your language is kind, friendly, and virtuous. 4 | 5 | You are having a conversation with a human, answering the following questions as best you can. Consider all the context of your conversation before selecting tool use. You have access to the following tools: 6 | -------------------------------------------------------------------------------- /src/template/react_cot.txt: -------------------------------------------------------------------------------- 1 | 2 | Use the following format: 3 | Question: the input question you must answer 4 | Thought: you should always think about what to do 5 | Action: the action to take, consider these tools for help if they sound relevant to the need [{tool_names}] 6 | Action Input: the input to the action 7 | Observation: the result of the action 8 | ... (this Thought/Action/Action Input/Observation can repeat N times) 9 | Thought: I now know the final answer 10 | Final Answer: the final answer to the original input question 11 | -------------------------------------------------------------------------------- /src/template/suffix.txt: -------------------------------------------------------------------------------- 1 | Begin! 2 | 3 | {chat_history} 4 | Question: {input} 5 | {agent_scratchpad} -------------------------------------------------------------------------------- /src/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/src/tools/__init__.py -------------------------------------------------------------------------------- /src/tools/doc_search.py: -------------------------------------------------------------------------------- 1 | # /app/src/tools/doc_search.py 2 | import logging 3 | 4 | # Primary Components 5 | from llama_index import ServiceContext, VectorStoreIndex 6 | from llama_index.vector_stores.qdrant import QdrantVectorStore 7 | from qdrant_client import QdrantClient 8 | 9 | from src.utils.config import load_config, setup_environment_variables 10 | from src.utils.embedding_selector import EmbeddingConfig, EmbeddingSelector 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class DocumentSearch: 16 | """ 17 | Class to perform document searches using a vector store index. 18 | 19 | Attributes: 20 | - collection (str): Name of the collection to be queried. 21 | - query (str): User input query for searching documents. 22 | - CONFIG (dict): Loaded configuration settings. 23 | - client (QdrantClient): Client to interact with the Qdrant service. 24 | """ 25 | 26 | def __init__(self, query: str, collection: str): 27 | """ 28 | Initializes with collection name and user input. 29 | 30 | Parameters: 31 | - collection (str): Name of the collection to be queried. 32 | - query (str): User input query for searching documents. 33 | """ 34 | self.collection = collection 35 | self.query = query 36 | self.CONFIG = load_config() 37 | setup_environment_variables(self.CONFIG) 38 | self.client = QdrantClient(url="http://RAG_BOT_QDRANT:6333") 39 | # self.embed_model = OpenAIEmbedding() 40 | # self.embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/multi-qa-mpnet-base-dot-v1") 41 | self.embedding_config = EmbeddingConfig(type=self.CONFIG["Embedding_Type"]) 42 | self.embed_model = EmbeddingSelector(self.embedding_config).get_embedding_model() 43 | 44 | def setup_index(self) -> VectorStoreIndex: 45 | """ 46 | Sets up and returns the vector store index for the collection. 47 | 48 | Returns: 49 | - VectorStoreIndex: The set up vector store index. 50 | 51 | Raises: 52 | - Exception: Propagates any exceptions that occur during the index setup. 53 | """ 54 | try: 55 | vector_store = QdrantVectorStore(client=self.client, collection_name=self.collection) 56 | service_context = ServiceContext.from_defaults(embed_model=self.embed_model) 57 | index = VectorStoreIndex.from_vector_store(vector_store=vector_store, service_context=service_context) 58 | 59 | return index 60 | 61 | except Exception as e: 62 | logging.error(f"setup_index: Error - {str(e)}") 63 | raise e 64 | 65 | def search_documents(self): 66 | """ 67 | Searches and returns documents based on the user input query. 68 | 69 | Returns: 70 | - Any: The response received from querying the index. 71 | 72 | Raises: 73 | - Exception: Propagates any exceptions that occur during the document search. 74 | """ 75 | try: 76 | query_engine = (self.setup_index()).as_query_engine() 77 | response = query_engine.query(self.query) 78 | logging.info(f"search_documents: Response - {response}") 79 | 80 | return response 81 | 82 | except Exception as e: 83 | logging.error(f"search_documents: Error - {str(e)}") 84 | raise e 85 | -------------------------------------------------------------------------------- /src/tools/setup.py: -------------------------------------------------------------------------------- 1 | # /app/src/tools/setup.py 2 | import logging 3 | 4 | from langchain.pydantic_v1 import BaseModel, Field 5 | from langchain.tools import BaseTool 6 | from langchain_community.tools import DuckDuckGoSearchResults 7 | 8 | from src.tools.doc_search import DocumentSearch 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class SearchWebInput(BaseModel): 14 | query: str = Field(description="The search query") 15 | 16 | 17 | class SearchTechDocsInput(BaseModel): 18 | query: str = Field(description="The search query") 19 | collection: str = Field(default="techdocs", description="The document collection to search in") 20 | 21 | 22 | class SearchWebTool(BaseTool): 23 | name = "search_web" 24 | description = "Conducts DuckDuckGo searches." 25 | args_schema = SearchWebInput 26 | return_direct = True 27 | 28 | def _run(self, query: str, **kwargs) -> str: 29 | search = DuckDuckGoSearchResults() 30 | return search.run(query) 31 | 32 | 33 | class SearchTechDocsTool(BaseTool): 34 | name = "search_techdocs" 35 | description = "This tool enables the querying of a specialized vector store named ‘TechDocs,’ a repository where users archive valuable technical documentation they have encountered. It is particularly beneficial when engaging with technical subjects or when involved in coding activities. Utilize this search tool to scrutinize the vector store for pertinent context when addressing technical inquiries or tasks. If a term from the user input is unfamiliar but appears to be technical in nature, it is imperative to consult ‘TechDocs’ to ascertain whether relevant information or context is available therein. For your awareness, the information provided is sourced from ‘TechDocs,’ and we will refer to this source for any related queries." 36 | args_schema = SearchTechDocsInput 37 | return_direct = True 38 | 39 | def _run(self, query: str, collection: str = "techdocs", **kwargs) -> str: 40 | search = DocumentSearch(query, collection) 41 | results = search.search_documents() 42 | return results 43 | 44 | 45 | class ToolSetup: 46 | """ 47 | A class dedicated to the setup and initialization of tools used by the agent. 48 | """ 49 | 50 | @classmethod 51 | def setup_tools(cls) -> list: 52 | """ 53 | Initializes and returns a list of tools for the agent. 54 | Returns: 55 | - list: A list of initialized tools for agent's use. 56 | """ 57 | return [SearchWebTool(), SearchTechDocsTool()] 58 | -------------------------------------------------------------------------------- /src/ui/gradio_interface.py: -------------------------------------------------------------------------------- 1 | # /app/src/ui/gradio_interface.py 2 | import gradio as gr 3 | import requests 4 | 5 | FASTAPI_URL = "http://fastapi:8000/chat/" # This is the URL of your FastAPI service inside the Docker network 6 | 7 | 8 | def chat_with_bot(user_input: str) -> str: 9 | """Function to interface with Gradio that chats with the agent through the FastAPI service.""" 10 | response = requests.post(FASTAPI_URL, json={"user_input": user_input}) 11 | 12 | if response.status_code == 200: 13 | return response.json()["response"] 14 | else: 15 | return "Error communicating with the agent." 16 | 17 | 18 | # Gradio Interface 19 | iface = gr.Interface( 20 | fn=chat_with_bot, 21 | inputs="text", 22 | outputs="text" 23 | ) 24 | 25 | # If this script is run directly, launch the Gradio app 26 | if __name__ == "__main__": 27 | iface.launch(server_port=7860, server_name="0.0.0.0") 28 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylejtobin/rag_bot/a081c1e6a8f4032a9fd22841e57177c08419525f/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/config.py: -------------------------------------------------------------------------------- 1 | # /app/src/utils/config.py 2 | import logging 3 | from pathlib import Path 4 | 5 | import yaml 6 | from dotenv import load_dotenv 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def load_config() -> dict: 12 | """ 13 | Load the configuration from a YAML file located in the project root. 14 | 15 | Returns: 16 | dict: Configuration parameters from the YAML file. 17 | """ 18 | 19 | # Determine the project root based on the current file's location 20 | project_root = Path(__file__).resolve().parents[2] 21 | config_file_path = project_root / "config.yml" 22 | 23 | # Safely open and read the configuration file 24 | with open(config_file_path, 'r') as stream: 25 | try: 26 | config = yaml.safe_load(stream) 27 | return config 28 | except yaml.YAMLError as exc: 29 | # Log any errors that occur during YAML parsing 30 | print(exc) 31 | return {} # Return an empty dictionary if an error occurs 32 | 33 | 34 | def setup_environment_variables(config: dict): 35 | """ 36 | Load environment variables from the specified key file. 37 | 38 | Args: 39 | config (dict): Configuration containing the path to the key file. 40 | """ 41 | load_dotenv(config["Key_File"]) 42 | -------------------------------------------------------------------------------- /src/utils/embedding_selector.py: -------------------------------------------------------------------------------- 1 | # /app/src/utils/embedding_selector.py 2 | from pydantic import BaseModel 3 | from typing import Optional 4 | from src.utils.config import load_config 5 | 6 | CONFIG = load_config() 7 | 8 | 9 | # Pydantic model for configuration 10 | class EmbeddingConfig(BaseModel): 11 | type: str 12 | huggingface_model: Optional[str] = "sentence-transformers/multi-qa-mpnet-base-dot-v1" 13 | 14 | 15 | class EmbeddingSelector: 16 | def __init__(self, config: EmbeddingConfig): 17 | self.config = config 18 | 19 | def get_embedding_model(self): 20 | if self.config.type == "openai": 21 | from llama_index.embeddings import OpenAIEmbedding 22 | return OpenAIEmbedding() 23 | elif self.config.type == "local": 24 | from llama_index.embeddings import HuggingFaceEmbedding 25 | return HuggingFaceEmbedding(model_name=self.config.huggingface_model) 26 | else: 27 | raise ValueError(f"Unsupported embedding type: {self.config.type}") 28 | --------------------------------------------------------------------------------