Spaces:
Runtime error
Runtime error
Julien Simon commited on
Commit Β·
ab96bbb
0
Parent(s):
Initial version
Browse files- .gitattributes +36 -0
- README.md +95 -0
- app.py +136 -0
- demo.py +280 -0
- requirements.txt +11 -0
.gitattributes
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Conductor RAG
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "5.23.1"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Conductor RAG - Document Question-Answering System
|
| 13 |
+
π A Retrieval-Augmented Generation (RAG) powered chat interface for document Q&A using Arcee Conductor
|
| 14 |
+
|
| 15 |
+
## Overview
|
| 16 |
+
This application provides an interactive chat interface that allows users to ask questions about their documents. It combines the power of Large Language Models with document retrieval to provide accurate, source-backed answers.
|
| 17 |
+
|
| 18 |
+
## Features
|
| 19 |
+
- **RAG-Powered Responses**: Leverages document context to provide accurate, factual answers
|
| 20 |
+
- **Flexible Query Modes**: Switch between RAG and vanilla LLM responses
|
| 21 |
+
- **Source Citations**: Automatically includes relevant document sources and page numbers
|
| 22 |
+
- **Interactive Interface**: Clean, user-friendly Gradio-based chat interface
|
| 23 |
+
- **Context Visibility**: View the retrieved document chunks used to generate responses
|
| 24 |
+
|
| 25 |
+
## Technical Details
|
| 26 |
+
- Built with Langchain and Gradio
|
| 27 |
+
- Uses Arcee Conductor API for LLM capabilities
|
| 28 |
+
- Document embedding via BAAI/bge-small-en-v1.5
|
| 29 |
+
- ChromaDB for vector storage
|
| 30 |
+
- Supports PDF document processing
|
| 31 |
+
|
| 32 |
+
## Included Papers
|
| 33 |
+
The following research papers are included in the `pdf` directory:
|
| 34 |
+
|
| 35 |
+
- [arXiv:2306.13649v3](https://arxiv.org/abs/2306.13649)
|
| 36 |
+
- [arXiv:2309.16609v1](https://arxiv.org/abs/2309.16609)
|
| 37 |
+
- [arXiv:2312.06795v1](https://arxiv.org/abs/2312.06795)
|
| 38 |
+
- [arXiv:2403.19522v1](https://arxiv.org/abs/2403.19522)
|
| 39 |
+
- [arXiv:2405.04434v5](https://arxiv.org/abs/2405.04434)
|
| 40 |
+
- [arXiv:2406.11617v1](https://arxiv.org/abs/2406.11617)
|
| 41 |
+
- [arXiv:2410.21228v1](https://arxiv.org/abs/2410.21228)
|
| 42 |
+
- [arXiv:2411.05059v2](https://arxiv.org/abs/2411.05059)
|
| 43 |
+
- [arXiv:2501.09223v1](https://arxiv.org/abs/2501.09223)
|
| 44 |
+
- [arXiv:2501.12948v1](https://arxiv.org/abs/2501.12948)
|
| 45 |
+
- [arXiv:2503.04872v1](https://arxiv.org/abs/2503.04872)
|
| 46 |
+
|
| 47 |
+
## Deployment
|
| 48 |
+
This application is hosted as a Hugging Face Space. Configuration details can be found in the [spaces config reference](https://huggingface.co/docs/hub/spaces-config-reference).
|
| 49 |
+
|
| 50 |
+
## Creating Your Own Hugging Face Space Using CLI
|
| 51 |
+
|
| 52 |
+
You can easily deploy this application as your own Hugging Face Space using the Hugging Face CLI. Follow these steps:
|
| 53 |
+
|
| 54 |
+
1. **Install the Hugging Face CLI**:
|
| 55 |
+
```bash
|
| 56 |
+
pip install huggingface_hub
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
2. **Login to Hugging Face**:
|
| 60 |
+
```bash
|
| 61 |
+
huggingface-cli login
|
| 62 |
+
```
|
| 63 |
+
You'll be prompted to enter your Hugging Face token, which you can find in your account settings.
|
| 64 |
+
|
| 65 |
+
3. **Clone this Repository**:
|
| 66 |
+
```bash
|
| 67 |
+
git clone https://github.com/username/conductor-rag.git
|
| 68 |
+
cd conductor-rag
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
4. **Create a New Space**:
|
| 72 |
+
```bash
|
| 73 |
+
huggingface-cli repo create conductor-rag-your-name --type space --space-sdk gradio
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
5. **Add Your Environment Variables**:
|
| 77 |
+
The application uses the following environment variables, which you need to set in the Space settings:
|
| 78 |
+
- `OPENAI_API_KEY`: Your Arcee Conductor API key
|
| 79 |
+
|
| 80 |
+
7. **Push Your Code to the Space**:
|
| 81 |
+
```bash
|
| 82 |
+
git remote add space https://huggingface.co/spaces/your-username/conductor-rag
|
| 83 |
+
git push space main
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
8. **Add Your PDF Documents**:
|
| 87 |
+
You can either add PDFs directly to the repository before pushing, or upload them later through git.
|
| 88 |
+
|
| 89 |
+
9. **Monitor Deployment**:
|
| 90 |
+
Visit `https://huggingface.co/spaces/your-username/conductor-rag-your-name` to see your Space being built and deployed.
|
| 91 |
+
|
| 92 |
+
Your Space will automatically build and deploy the application. Once complete, you can access it via the provided URL and share it with others.
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
Built with π using Arcee Conductor
|
app.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from demo import create_llm, create_embeddings, load_or_create_vectorstore, create_qa_chain
|
| 3 |
+
|
| 4 |
+
def initialize_chain():
|
| 5 |
+
"""Initialize the RAG chain and return it."""
|
| 6 |
+
llm = create_llm()
|
| 7 |
+
embeddings = create_embeddings()
|
| 8 |
+
vectorstore = load_or_create_vectorstore(embeddings)
|
| 9 |
+
return create_qa_chain(llm, vectorstore)
|
| 10 |
+
|
| 11 |
+
def chat_response(message, history, query_type):
|
| 12 |
+
"""Process chat messages and return responses using either RAG or vanilla LLM.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
message (str): The user's input message
|
| 16 |
+
history (list): List of previous message-response pairs
|
| 17 |
+
query_type (str): Either "RAG" or "Vanilla LLM"
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
str: The formatted response text including sources for RAG queries
|
| 21 |
+
"""
|
| 22 |
+
# Convert Gradio history to list of tuples
|
| 23 |
+
chat_history = [(msg, resp) for msg, resp in history] if history else []
|
| 24 |
+
|
| 25 |
+
if query_type == "RAG":
|
| 26 |
+
result = qa_chain.invoke({
|
| 27 |
+
"question": message,
|
| 28 |
+
"chat_history": chat_history
|
| 29 |
+
})
|
| 30 |
+
response_text = result["answer"]
|
| 31 |
+
|
| 32 |
+
# Add source citations if available
|
| 33 |
+
if result.get("source_documents"):
|
| 34 |
+
sources = []
|
| 35 |
+
seen_sources = set()
|
| 36 |
+
|
| 37 |
+
# Deduplicate sources while preserving order
|
| 38 |
+
for doc in result["source_documents"]:
|
| 39 |
+
source = doc.metadata.get('source', 'Unknown')
|
| 40 |
+
page = doc.metadata.get('page', 'unknown')
|
| 41 |
+
source_key = f"{source}:{page}"
|
| 42 |
+
|
| 43 |
+
if source_key not in seen_sources:
|
| 44 |
+
sources.append(f"- {source}, page {page}")
|
| 45 |
+
seen_sources.add(source_key)
|
| 46 |
+
|
| 47 |
+
if sources:
|
| 48 |
+
response_text += "\n\nSources:\n" + "\n".join(sources)
|
| 49 |
+
else:
|
| 50 |
+
# Use vanilla LLM for direct responses
|
| 51 |
+
result = llm.invoke(message)
|
| 52 |
+
response_text = result.content
|
| 53 |
+
|
| 54 |
+
return response_text
|
| 55 |
+
|
| 56 |
+
# Initialize components globally
|
| 57 |
+
llm = create_llm()
|
| 58 |
+
qa_chain = initialize_chain()
|
| 59 |
+
|
| 60 |
+
# Create the Gradio interface
|
| 61 |
+
with gr.Blocks() as demo:
|
| 62 |
+
# UI Setup
|
| 63 |
+
gr.Markdown("# RAG-Powered Document Chat with Arcee Conductor")
|
| 64 |
+
gr.Markdown("Ask questions about your documents. The system will provide answers based on the content of your PDFs.")
|
| 65 |
+
|
| 66 |
+
rag_enabled = gr.Checkbox(
|
| 67 |
+
value=True,
|
| 68 |
+
label="Enable RAG",
|
| 69 |
+
info="Toggle between RAG-powered document search or vanilla LLM responses"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
chatbot = gr.Chatbot()
|
| 73 |
+
context_box = gr.Textbox(
|
| 74 |
+
label="Retrieved Context",
|
| 75 |
+
interactive=False,
|
| 76 |
+
visible=True,
|
| 77 |
+
lines=5
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# Input controls
|
| 81 |
+
with gr.Row():
|
| 82 |
+
msg = gr.Textbox(label="Query", scale=8)
|
| 83 |
+
with gr.Column(scale=1):
|
| 84 |
+
submit = gr.Button("Submit")
|
| 85 |
+
clear = gr.Button("Clear")
|
| 86 |
+
|
| 87 |
+
def respond(message, chat_history, is_rag_enabled):
|
| 88 |
+
"""Handle user messages and generate responses.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
message (str): User input message
|
| 92 |
+
chat_history (list): Previous conversation history
|
| 93 |
+
is_rag_enabled (bool): Whether to use RAG or vanilla LLM
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
tuple: (cleared message, updated history, context, RAG state)
|
| 97 |
+
"""
|
| 98 |
+
if not message:
|
| 99 |
+
return "", chat_history, "", is_rag_enabled
|
| 100 |
+
|
| 101 |
+
query_type = "RAG" if is_rag_enabled else "Vanilla LLM"
|
| 102 |
+
bot_message = chat_response(message, chat_history, query_type)
|
| 103 |
+
|
| 104 |
+
# Create new history to avoid modifying the original
|
| 105 |
+
new_history = list(chat_history)
|
| 106 |
+
new_history.append((message, bot_message))
|
| 107 |
+
|
| 108 |
+
# Get relevant context for RAG queries
|
| 109 |
+
context = ""
|
| 110 |
+
if is_rag_enabled and qa_chain.retriever:
|
| 111 |
+
docs = qa_chain.retriever.get_relevant_documents(message)
|
| 112 |
+
context = "\n\n".join(doc.page_content for doc in docs)
|
| 113 |
+
|
| 114 |
+
return "", new_history, context, is_rag_enabled
|
| 115 |
+
|
| 116 |
+
def update_context_visibility(is_rag_enabled):
|
| 117 |
+
"""Update context box visibility based on RAG toggle."""
|
| 118 |
+
return gr.update(visible=is_rag_enabled, value="" if not is_rag_enabled else None)
|
| 119 |
+
|
| 120 |
+
# Wire up event handlers
|
| 121 |
+
msg.submit(respond, [msg, chatbot, rag_enabled], [msg, chatbot, context_box, rag_enabled])
|
| 122 |
+
submit.click(respond, [msg, chatbot, rag_enabled], [msg, chatbot, context_box, rag_enabled])
|
| 123 |
+
clear.click(lambda: [[], "", True], None, [chatbot, context_box, rag_enabled], queue=False)
|
| 124 |
+
rag_enabled.change(update_context_visibility, rag_enabled, context_box)
|
| 125 |
+
|
| 126 |
+
# Add example queries
|
| 127 |
+
gr.Examples(
|
| 128 |
+
examples=[
|
| 129 |
+
"Tell me about Arcee Fusion.",
|
| 130 |
+
"How does deepseek-R1 differ from deepseek-v3?"
|
| 131 |
+
],
|
| 132 |
+
inputs=msg
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
if __name__ == "__main__":
|
| 136 |
+
demo.launch(share=False)
|
demo.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import glob
|
| 3 |
+
# Set tokenizers parallelism before importing other libraries
|
| 4 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 5 |
+
|
| 6 |
+
from langchain_openai import ChatOpenAI
|
| 7 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 8 |
+
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
|
| 9 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 10 |
+
from langchain_chroma import Chroma
|
| 11 |
+
from langchain.chains import ConversationalRetrievalChain
|
| 12 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 13 |
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
| 14 |
+
import time
|
| 15 |
+
|
| 16 |
+
# Constants
|
| 17 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 18 |
+
OPENAI_URL = "https://conductor.arcee.ai/v1"
|
| 19 |
+
OPENAI_MODEL = "auto"
|
| 20 |
+
CHROMA_PATH = "vectorstore"
|
| 21 |
+
PDF_PATH = "pdf"
|
| 22 |
+
|
| 23 |
+
def create_llm(streaming=False):
|
| 24 |
+
"""Initialize the OpenAI language model.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
streaming (bool): Whether to enable response streaming
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
ChatOpenAI: Configured language model instance
|
| 31 |
+
"""
|
| 32 |
+
return ChatOpenAI(
|
| 33 |
+
model=OPENAI_MODEL,
|
| 34 |
+
openai_api_key=OPENAI_API_KEY,
|
| 35 |
+
base_url=OPENAI_URL,
|
| 36 |
+
streaming=streaming
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def create_embeddings():
|
| 40 |
+
"""Initialize the embedding model."""
|
| 41 |
+
return HuggingFaceEmbeddings(
|
| 42 |
+
model_name="BAAI/bge-small-en-v1.5",
|
| 43 |
+
model_kwargs={'device': 'cpu'},
|
| 44 |
+
encode_kwargs={'normalize_embeddings': True}
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
def get_text_splitter():
|
| 48 |
+
"""Create text splitter with optimal settings."""
|
| 49 |
+
return RecursiveCharacterTextSplitter(
|
| 50 |
+
chunk_size=512,
|
| 51 |
+
chunk_overlap=129,
|
| 52 |
+
length_function=len,
|
| 53 |
+
add_start_index=True,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
def get_pdf_files():
|
| 57 |
+
"""Get list of PDF files from the specified directory."""
|
| 58 |
+
if not os.path.exists(PDF_PATH):
|
| 59 |
+
os.makedirs(PDF_PATH)
|
| 60 |
+
return []
|
| 61 |
+
return list(glob.glob(os.path.join(PDF_PATH, "*.pdf")))
|
| 62 |
+
|
| 63 |
+
def filter_metadata(doc):
|
| 64 |
+
"""Filter out unwanted sections from documents.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
doc: Document object with metadata
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
bool: True if document should be kept, False if filtered out
|
| 71 |
+
"""
|
| 72 |
+
skip_sections = {
|
| 73 |
+
'references',
|
| 74 |
+
'acknowledgments',
|
| 75 |
+
'appendix'
|
| 76 |
+
}
|
| 77 |
+
section = doc.metadata.get('section', '').lower()
|
| 78 |
+
return not any(s in section for s in skip_sections)
|
| 79 |
+
|
| 80 |
+
def process_documents(documents, text_splitter):
|
| 81 |
+
"""Process and filter documents into chunks."""
|
| 82 |
+
chunks = text_splitter.split_documents(documents)
|
| 83 |
+
return [chunk for chunk in chunks if filter_metadata(chunk)]
|
| 84 |
+
|
| 85 |
+
def load_or_create_vectorstore(embeddings):
|
| 86 |
+
"""Load existing vectorstore or create a new one."""
|
| 87 |
+
if os.path.exists(CHROMA_PATH):
|
| 88 |
+
return handle_existing_vectorstore(embeddings)
|
| 89 |
+
return create_new_vectorstore(embeddings)
|
| 90 |
+
|
| 91 |
+
def handle_existing_vectorstore(embeddings):
|
| 92 |
+
"""Handle loading and updating existing vectorstore.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
embeddings: Embedding model instance
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
Chroma: Loaded and potentially updated vectorstore
|
| 99 |
+
|
| 100 |
+
Exits if no PDF files are found.
|
| 101 |
+
"""
|
| 102 |
+
print("Loading existing Chroma database...")
|
| 103 |
+
vectorstore = Chroma(
|
| 104 |
+
persist_directory=CHROMA_PATH,
|
| 105 |
+
embedding_function=embeddings
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
current_pdfs = get_pdf_files()
|
| 109 |
+
if not current_pdfs:
|
| 110 |
+
print("No PDF files found in directory.")
|
| 111 |
+
exit(1)
|
| 112 |
+
|
| 113 |
+
collection = vectorstore.get()
|
| 114 |
+
processed_files = {
|
| 115 |
+
meta.get('source')
|
| 116 |
+
for meta in collection['metadatas']
|
| 117 |
+
if meta and meta.get('source')
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
new_pdfs = [pdf for pdf in current_pdfs if pdf not in processed_files]
|
| 121 |
+
|
| 122 |
+
if new_pdfs:
|
| 123 |
+
update_vectorstore(vectorstore, new_pdfs, processed_files)
|
| 124 |
+
else:
|
| 125 |
+
print("No new PDF files to process.")
|
| 126 |
+
|
| 127 |
+
return vectorstore
|
| 128 |
+
|
| 129 |
+
def update_vectorstore(vectorstore, new_pdfs, processed_files):
|
| 130 |
+
"""Update existing vectorstore with new documents."""
|
| 131 |
+
print(f"Found {len(new_pdfs)} new PDF files to process...")
|
| 132 |
+
loader = DirectoryLoader(PDF_PATH, glob="**/*.pdf", loader_cls=PyPDFLoader)
|
| 133 |
+
documents = loader.load()
|
| 134 |
+
new_documents = [doc for doc in documents if doc.metadata.get('source') not in processed_files]
|
| 135 |
+
|
| 136 |
+
filtered_chunks = process_documents(new_documents, get_text_splitter())
|
| 137 |
+
if filtered_chunks:
|
| 138 |
+
print("Adding new documents to existing database...")
|
| 139 |
+
vectorstore.add_documents(filtered_chunks)
|
| 140 |
+
print("Database updated successfully!")
|
| 141 |
+
|
| 142 |
+
def create_new_vectorstore(embeddings):
|
| 143 |
+
"""Create a new vectorstore from documents."""
|
| 144 |
+
print("Creating new Chroma database...")
|
| 145 |
+
pdf_files = get_pdf_files()
|
| 146 |
+
if not pdf_files:
|
| 147 |
+
print(f"No PDF files found in '{PDF_PATH}' directory!")
|
| 148 |
+
print(f"Please add your PDF files to the '{PDF_PATH}' directory and run again.")
|
| 149 |
+
exit(1)
|
| 150 |
+
|
| 151 |
+
print(f"Found {len(pdf_files)} PDF files to process...")
|
| 152 |
+
print("(This may take a while as documents need to be processed and embedded)")
|
| 153 |
+
|
| 154 |
+
os.makedirs(CHROMA_PATH, exist_ok=True)
|
| 155 |
+
|
| 156 |
+
loader = DirectoryLoader(PDF_PATH, glob="**/*.pdf", loader_cls=PyPDFLoader)
|
| 157 |
+
documents = loader.load()
|
| 158 |
+
filtered_chunks = process_documents(documents, get_text_splitter())
|
| 159 |
+
|
| 160 |
+
return Chroma.from_documents(
|
| 161 |
+
documents=filtered_chunks,
|
| 162 |
+
embedding=embeddings,
|
| 163 |
+
persist_directory=CHROMA_PATH
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
def create_qa_chain(llm, vectorstore):
|
| 167 |
+
"""Create the question-answering chain."""
|
| 168 |
+
prompt_template = """Answer the question using your own knowledge and the provided context.
|
| 169 |
+
|
| 170 |
+
Context:
|
| 171 |
+
{context}
|
| 172 |
+
|
| 173 |
+
Question: {question}
|
| 174 |
+
|
| 175 |
+
Previous conversation:
|
| 176 |
+
{chat_history}
|
| 177 |
+
|
| 178 |
+
Answer:"""
|
| 179 |
+
|
| 180 |
+
prompt = ChatPromptTemplate.from_template(prompt_template)
|
| 181 |
+
|
| 182 |
+
return ConversationalRetrievalChain.from_llm(
|
| 183 |
+
llm=llm,
|
| 184 |
+
retriever=vectorstore.as_retriever(
|
| 185 |
+
search_type="similarity",
|
| 186 |
+
search_kwargs={'k': 3}
|
| 187 |
+
),
|
| 188 |
+
return_source_documents=True,
|
| 189 |
+
combine_docs_chain_kwargs={'prompt': prompt},
|
| 190 |
+
chain_type="stuff",
|
| 191 |
+
verbose=True
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
def get_vanilla_response(llm, query):
|
| 195 |
+
"""Get response from vanilla chain without RAG with streaming."""
|
| 196 |
+
streaming_llm = create_llm(streaming=True)
|
| 197 |
+
|
| 198 |
+
vanilla_prompt = ChatPromptTemplate.from_template("""
|
| 199 |
+
Question: {question}
|
| 200 |
+
|
| 201 |
+
Instructions:
|
| 202 |
+
- If you don't know the answer, say so
|
| 203 |
+
- Be concise and clear
|
| 204 |
+
- Only state what you're confident about
|
| 205 |
+
|
| 206 |
+
Answer:""")
|
| 207 |
+
|
| 208 |
+
chain = vanilla_prompt | streaming_llm
|
| 209 |
+
|
| 210 |
+
print("\n=== Vanilla Response (No RAG) ===")
|
| 211 |
+
|
| 212 |
+
try:
|
| 213 |
+
print("Answer: ", end="", flush=True)
|
| 214 |
+
for chunk in chain.stream({"question": query}):
|
| 215 |
+
print(chunk.content, end="", flush=True)
|
| 216 |
+
print() # New line after streaming completes
|
| 217 |
+
|
| 218 |
+
except Exception as e:
|
| 219 |
+
print(f"\nError getting vanilla response: {str(e)}")
|
| 220 |
+
|
| 221 |
+
def get_rag_response(qa_chain, query, chat_history):
|
| 222 |
+
"""Get response from RAG-powered chain with streaming.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
qa_chain: The QA chain instance
|
| 226 |
+
query (str): User's question
|
| 227 |
+
chat_history (list): Previous conversation history
|
| 228 |
+
"""
|
| 229 |
+
print("\n=== RAG Response ===")
|
| 230 |
+
|
| 231 |
+
try:
|
| 232 |
+
result = qa_chain.invoke({
|
| 233 |
+
"question": query,
|
| 234 |
+
"chat_history": chat_history
|
| 235 |
+
})
|
| 236 |
+
|
| 237 |
+
# Print answer with streaming
|
| 238 |
+
print("\nAnswer: ", end="", flush=True)
|
| 239 |
+
for chunk in result["answer"]:
|
| 240 |
+
print(chunk, end="", flush=True)
|
| 241 |
+
print()
|
| 242 |
+
|
| 243 |
+
# Print unique sources
|
| 244 |
+
if result.get("source_documents"):
|
| 245 |
+
print("\nSources:")
|
| 246 |
+
seen_sources = set()
|
| 247 |
+
for doc in result["source_documents"]:
|
| 248 |
+
source = doc.metadata.get('source', 'Unknown')
|
| 249 |
+
page = doc.metadata.get('page', 'unknown')
|
| 250 |
+
source_key = f"{source}:{page}"
|
| 251 |
+
|
| 252 |
+
if source_key not in seen_sources:
|
| 253 |
+
print(f"- {source}, page {page}")
|
| 254 |
+
seen_sources.add(source_key)
|
| 255 |
+
|
| 256 |
+
except Exception as e:
|
| 257 |
+
print(f"\nError: {str(e)}")
|
| 258 |
+
|
| 259 |
+
def main():
|
| 260 |
+
"""Main execution function."""
|
| 261 |
+
# Create streaming LLM for RAG
|
| 262 |
+
llm = create_llm(streaming=True)
|
| 263 |
+
embeddings = create_embeddings()
|
| 264 |
+
vectorstore = load_or_create_vectorstore(embeddings)
|
| 265 |
+
qa_chain = create_qa_chain(llm, vectorstore)
|
| 266 |
+
|
| 267 |
+
chat_history = []
|
| 268 |
+
query = "Tell me about Arcee Fusion?"
|
| 269 |
+
|
| 270 |
+
# Get both vanilla and RAG responses
|
| 271 |
+
get_vanilla_response(llm, query)
|
| 272 |
+
get_rag_response(qa_chain, query, chat_history)
|
| 273 |
+
|
| 274 |
+
if __name__ == "__main__":
|
| 275 |
+
main()
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
langchain
|
| 2 |
+
langchain-openai
|
| 3 |
+
langchain-huggingface
|
| 4 |
+
langchain-chroma
|
| 5 |
+
sentence-transformers
|
| 6 |
+
torch
|
| 7 |
+
transformers
|
| 8 |
+
langchain-community
|
| 9 |
+
chromadb
|
| 10 |
+
pypdf
|
| 11 |
+
gradio
|