37-AN commited on
Commit
a33458e
·
0 Parent(s):

Initial commit - Personal RAG Assistant with Hugging Face integration

Browse files
.gitattributes ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *.tflite filter=lfs diff=lfs merge=lfs -text
29
+ *.tgz filter=lfs diff=lfs merge=lfs -text
30
+ *.wasm filter=lfs diff=lfs merge=lfs -text
31
+ *.xz filter=lfs diff=lfs merge=lfs -text
32
+ *.zip filter=lfs diff=lfs merge=lfs -text
33
+ *.zst filter=lfs diff=lfs merge=lfs -text
34
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+ .env
27
+
28
+ # Data directories
29
+ data/
30
+
31
+ # IDE files
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+
37
+ # Streamlit
38
+ .streamlit/
39
+
40
+ # Logs
41
+ *.log
42
+
43
+ # OS specific
44
+ .DS_Store
45
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install required system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ git \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for better caching
12
+ COPY requirements.txt .
13
+
14
+ # Install Python dependencies
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy the rest of the application
18
+ COPY . .
19
+
20
+ # Create necessary directories
21
+ RUN mkdir -p data/documents data/vector_db
22
+
23
+ # Set environment variable to avoid TOKENIZERS_PARALLELISM warning
24
+ ENV TOKENIZERS_PARALLELISM=false
25
+
26
+ # Expose the Streamlit port
27
+ EXPOSE 8501
28
+
29
+ # Set the entrypoint command to run the Streamlit app
30
+ CMD ["streamlit", "run", "app/ui/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Personal AI Assistant with RAG (Hugging Face Edition)
2
+
3
+ A powerful personal AI assistant built with LangChain, integrating Retrieval-Augmented Generation (RAG) with a vector database (Qdrant) for improved contextual awareness and memory. This version uses Hugging Face models and can be deployed to Hugging Face Spaces for free hosting.
4
+
5
+ [![Open In Spaces](https://huggingface.co/datasets/huggingface/badges/resolve/main/open-in-spaces-sm.svg)](https://huggingface.co/spaces)
6
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com)
7
+
8
+ ## Features
9
+
10
+ - Large Language Model integration using Hugging Face's free models
11
+ - RAG-based memory system with vector database storage
12
+ - Document ingestion pipeline for various file types
13
+ - Simple web UI built with Streamlit
14
+ - Conversation history tracking and retrieval
15
+ - Free deployment on Hugging Face Spaces
16
+
17
+ ## Project Structure
18
+
19
+ ```
20
+ .
21
+ ├── README.md
22
+ ├── requirements.txt
23
+ ├── .env.example
24
+ ├── app.py # Main entry point for Hugging Face Spaces
25
+ ├── space.py # Hugging Face Spaces SDK integration
26
+ ├── app/
27
+ │ ├── main.py # FastAPI application entry point
28
+ │ ├── config.py # Configuration settings
29
+ │ ├── ui/
30
+ │ │ └── streamlit_app.py # Streamlit web interface
31
+ │ ├── core/
32
+ │ │ ├── llm.py # LLM integration (Hugging Face)
33
+ │ │ ├── memory.py # RAG and vector store integration
34
+ │ │ ├── agent.py # Agent orchestration
35
+ │ │ └── ingestion.py # Document processing pipeline
36
+ │ └── utils/
37
+ │ └── helpers.py # Utility functions
38
+ └── data/
39
+ ├── documents/ # Store for uploaded documents
40
+ └── vector_db/ # Local vector database storage
41
+ ```
42
+
43
+ ## Setup
44
+
45
+ 1. Clone this repository
46
+ 2. Install dependencies:
47
+ ```
48
+ pip install -r requirements.txt
49
+ ```
50
+ 3. Copy `.env.example` to `.env` and fill in your Hugging Face API keys (optional)
51
+ 4. Start the Streamlit UI:
52
+ ```
53
+ streamlit run app/ui/streamlit_app.py
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ 1. Upload documents through the web interface
59
+ 2. Chat with your assistant, which can now reference your documents
60
+ 3. The assistant will automatically leverage your document knowledge to provide more personalized responses
61
+
62
+ ## Deployment to Hugging Face Spaces
63
+
64
+ This app can be easily deployed to Hugging Face Spaces for free hosting:
65
+
66
+ 1. Create a Hugging Face account at [huggingface.co](https://huggingface.co)
67
+ 2. Set environment variables:
68
+ ```
69
+ export HF_USERNAME=your-username
70
+ export HF_TOKEN=your-huggingface-token
71
+ export SPACE_NAME=personal-rag-assistant # optional
72
+ ```
73
+ 3. Run the deployment script:
74
+ ```
75
+ python space.py
76
+ ```
77
+ 4. Visit your deployed app at `https://huggingface.co/spaces/{your-username}/{space-name}`
78
+
79
+ Alternatively, you can manually create a new Space on Hugging Face and link it to your GitHub repository.
80
+
81
+ ## Models Used
82
+
83
+ This implementation uses the following free models from Hugging Face:
84
+
85
+ - LLM: [google/flan-t5-large](https://huggingface.co/google/flan-t5-large) - A powerful instruction-tuned model
86
+ - Embeddings: [sentence-transformers/all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) - Efficient embedding model
87
+
88
+ You can change these in the `.env` file.
89
+
90
+ ## Extending
91
+
92
+ - Add more document loaders in `ingestion.py`
93
+ - Integrate additional tools in `agent.py`
94
+ - Customize the UI in `streamlit_app.py`
95
+ - Switch to a different LLM in `llm.py` and `.env`
app.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Main entry point for Hugging Face Spaces deployment.
4
+ This file starts the Streamlit UI when deployed to Hugging Face Spaces.
5
+ """
6
+ import subprocess
7
+ import os
8
+ import sys
9
+
10
+ # Make sure the app directory is in the path
11
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
12
+
13
+ # Create necessary directories
14
+ os.makedirs('data/documents', exist_ok=True)
15
+ os.makedirs('data/vector_db', exist_ok=True)
16
+
17
+ # Run the Streamlit app
18
+ subprocess.run(["streamlit", "run", "app/ui/streamlit_app.py"])
app/config.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from pathlib import Path
4
+
5
+ # Load environment variables
6
+ env_path = Path('.') / '.env'
7
+ load_dotenv(dotenv_path=env_path)
8
+
9
+ # API Keys
10
+ HF_API_KEY = os.getenv('HF_API_KEY', '')
11
+
12
+ # LLM Configuration
13
+ LLM_MODEL = os.getenv('LLM_MODEL', 'google/flan-t5-large')
14
+ EMBEDDING_MODEL = os.getenv('EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2')
15
+
16
+ # Vector Database
17
+ VECTOR_DB_PATH = os.getenv('VECTOR_DB_PATH', './data/vector_db')
18
+ COLLECTION_NAME = os.getenv('COLLECTION_NAME', 'personal_assistant')
19
+
20
+ # Application Settings
21
+ DEFAULT_TEMPERATURE = float(os.getenv('DEFAULT_TEMPERATURE', 0.7))
22
+ CHUNK_SIZE = int(os.getenv('CHUNK_SIZE', 1000))
23
+ CHUNK_OVERLAP = int(os.getenv('CHUNK_OVERLAP', 200))
24
+ MAX_TOKENS = int(os.getenv('MAX_TOKENS', 512))
25
+
26
+ # Create a template .env file if it doesn't exist
27
+ def create_env_example():
28
+ if not os.path.exists('.env.example'):
29
+ with open('.env.example', 'w') as f:
30
+ f.write("""# API Keys
31
+ HF_API_KEY=your_huggingface_api_key_here
32
+
33
+ # LLM Configuration
34
+ LLM_MODEL=google/flan-t5-large # Free model with good performance
35
+ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
36
+
37
+ # Vector Database
38
+ VECTOR_DB_PATH=./data/vector_db
39
+ COLLECTION_NAME=personal_assistant
40
+
41
+ # Application Settings
42
+ DEFAULT_TEMPERATURE=0.7
43
+ CHUNK_SIZE=1000
44
+ CHUNK_OVERLAP=200
45
+ MAX_TOKENS=512
46
+ """)
app/core/agent.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ from typing import List, Dict, Any
4
+ from langchain.prompts import PromptTemplate
5
+
6
+ # Add project root to path for imports
7
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
8
+ from app.core.memory import MemoryManager
9
+ from app.core.llm import get_llm
10
+
11
+ class AssistantAgent:
12
+ """Orchestrates the assistant's functionality, managing RAG and tools."""
13
+
14
+ def __init__(self):
15
+ self.memory_manager = MemoryManager()
16
+ self.rag_chain = self.memory_manager.create_rag_chain()
17
+ self.llm = get_llm()
18
+
19
+ # Define a system prompt template
20
+ self.system_template = """You are a personal AI assistant that helps the user with their tasks and questions.
21
+ You have access to the user's documents and notes through a retrieval system.
22
+ When answering questions, leverage this knowledge base to provide specific, factual information.
23
+ If the answer is not in the provided context, acknowledge that and give the best general answer you can.
24
+
25
+ Context from the user's documents:
26
+ {context}
27
+
28
+ Chat History:
29
+ {chat_history}
30
+
31
+ User: {question}
32
+ Assistant:"""
33
+
34
+ self.rag_prompt = PromptTemplate(
35
+ input_variables=["context", "chat_history", "question"],
36
+ template=self.system_template
37
+ )
38
+
39
+ def query(self, question: str) -> Dict[str, Any]:
40
+ """Process a user query and return a response."""
41
+ # Use the RAG chain to get an answer
42
+ response = self.rag_chain({"question": question})
43
+
44
+ # Extract the answer and source documents
45
+ answer = response["answer"]
46
+ source_docs = response["source_documents"] if "source_documents" in response else []
47
+
48
+ # Format source documents for display
49
+ sources = []
50
+ for doc in source_docs:
51
+ metadata = doc.metadata
52
+ sources.append({
53
+ "content": doc.page_content[:100] + "..." if len(doc.page_content) > 100 else doc.page_content,
54
+ "source": metadata.get("source", "Unknown"),
55
+ "file_name": metadata.get("file_name", "Unknown"),
56
+ "page": metadata.get("page", "N/A") if "page" in metadata else None
57
+ })
58
+
59
+ return {
60
+ "answer": answer,
61
+ "sources": sources
62
+ }
63
+
64
+ def add_conversation_to_memory(self, question: str, answer: str):
65
+ """Add a conversation exchange to the memory for future context."""
66
+ # Create metadata for the conversation
67
+ metadata = {
68
+ "type": "conversation",
69
+ "question": question
70
+ }
71
+
72
+ # Add the exchange to the vector store
73
+ self.memory_manager.add_texts([answer], [metadata])
app/core/ingestion.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from typing import List, Dict, Any
4
+ from langchain.document_loaders import (
5
+ PyPDFLoader,
6
+ TextLoader,
7
+ CSVLoader
8
+ )
9
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
10
+
11
+ # Add project root to path for imports
12
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
13
+ from app.config import CHUNK_SIZE, CHUNK_OVERLAP
14
+ from app.core.memory import MemoryManager
15
+
16
+ class DocumentProcessor:
17
+ """Processes documents for ingestion into the vector database."""
18
+
19
+ def __init__(self, memory_manager: MemoryManager):
20
+ self.memory_manager = memory_manager
21
+ self.text_splitter = RecursiveCharacterTextSplitter(
22
+ chunk_size=CHUNK_SIZE,
23
+ chunk_overlap=CHUNK_OVERLAP
24
+ )
25
+
26
+ def process_file(self, file_path: str) -> List[str]:
27
+ """Process a file and return a list of document chunks."""
28
+ if not os.path.exists(file_path):
29
+ raise FileNotFoundError(f"File not found: {file_path}")
30
+
31
+ # Get the file extension
32
+ _, extension = os.path.splitext(file_path)
33
+ extension = extension.lower()
34
+
35
+ # Load the file using the appropriate loader
36
+ if extension == '.pdf':
37
+ loader = PyPDFLoader(file_path)
38
+ elif extension == '.txt':
39
+ loader = TextLoader(file_path)
40
+ elif extension == '.csv':
41
+ loader = CSVLoader(file_path)
42
+ else:
43
+ raise ValueError(f"Unsupported file type: {extension}")
44
+
45
+ # Load and split the documents
46
+ documents = loader.load()
47
+ chunks = self.text_splitter.split_documents(documents)
48
+
49
+ return chunks
50
+
51
+ def ingest_file(self, file_path: str, metadata: Dict[str, Any] = None) -> List[str]:
52
+ """Ingest a file into the vector database."""
53
+ # Process the file
54
+ chunks = self.process_file(file_path)
55
+
56
+ # Add metadata to each chunk
57
+ if metadata is None:
58
+ metadata = {}
59
+
60
+ # Add file path to metadata
61
+ base_metadata = {
62
+ "source": file_path,
63
+ "file_name": os.path.basename(file_path)
64
+ }
65
+ base_metadata.update(metadata)
66
+
67
+ # Prepare chunks and metadatas
68
+ texts = [chunk.page_content for chunk in chunks]
69
+ metadatas = []
70
+
71
+ for i, chunk in enumerate(chunks):
72
+ chunk_metadata = base_metadata.copy()
73
+ if hasattr(chunk, 'metadata'):
74
+ chunk_metadata.update(chunk.metadata)
75
+ chunk_metadata["chunk_id"] = i
76
+ metadatas.append(chunk_metadata)
77
+
78
+ # Store in vector database
79
+ ids = self.memory_manager.add_texts(texts, metadatas)
80
+
81
+ return ids
82
+
83
+ def ingest_text(self, text: str, metadata: Dict[str, Any] = None) -> List[str]:
84
+ """Ingest raw text into the vector database."""
85
+ if metadata is None:
86
+ metadata = {}
87
+
88
+ # Split the text
89
+ chunks = self.text_splitter.split_text(text)
90
+
91
+ # Prepare metadatas
92
+ metadatas = []
93
+ for i in range(len(chunks)):
94
+ chunk_metadata = metadata.copy()
95
+ chunk_metadata["chunk_id"] = i
96
+ chunk_metadata["source"] = "direct_input"
97
+ metadatas.append(chunk_metadata)
98
+
99
+ # Store in vector database
100
+ ids = self.memory_manager.add_texts(chunks, metadatas)
101
+
102
+ return ids
app/core/llm.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.llms import HuggingFaceHub
2
+ from langchain_community.embeddings import HuggingFaceEmbeddings
3
+ from langchain.chains import LLMChain
4
+ from langchain.prompts import PromptTemplate
5
+ import sys
6
+ import os
7
+
8
+ # Add project root to path for imports
9
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
10
+ from app.config import HF_API_KEY, LLM_MODEL, EMBEDDING_MODEL, DEFAULT_TEMPERATURE, MAX_TOKENS
11
+
12
+ def get_llm():
13
+ """Initialize and return the language model."""
14
+ if not HF_API_KEY:
15
+ # Can still work without API key but with rate limits
16
+ print("Warning: Hugging Face API key not set. Using models without authentication.")
17
+
18
+ llm = HuggingFaceHub(
19
+ huggingfacehub_api_token=HF_API_KEY,
20
+ repo_id=LLM_MODEL,
21
+ model_kwargs={
22
+ "temperature": DEFAULT_TEMPERATURE,
23
+ "max_length": MAX_TOKENS
24
+ }
25
+ )
26
+
27
+ return llm
28
+
29
+ def get_embeddings():
30
+ """Initialize and return the embeddings model."""
31
+ # SentenceTransformers can be used locally without an API key
32
+ return HuggingFaceEmbeddings(
33
+ model_name=EMBEDDING_MODEL
34
+ )
35
+
36
+ def get_chat_model():
37
+ """
38
+ Create a chat-like interface using a regular LLM.
39
+ This is necessary because many free HF models don't have chat interfaces.
40
+ """
41
+ llm = get_llm()
42
+
43
+ # Create a chat-like prompt template
44
+ chat_template = """
45
+ Context: {context}
46
+
47
+ Chat History:
48
+ {chat_history}
49
+
50
+ User: {question}
51
+ AI Assistant:
52
+ """
53
+
54
+ prompt = PromptTemplate(
55
+ input_variables=["context", "chat_history", "question"],
56
+ template=chat_template
57
+ )
58
+
59
+ # Create a chain
60
+ return LLMChain(llm=llm, prompt=prompt)
app/core/memory.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from langchain.vectorstores import Qdrant
4
+ from langchain.chains import ConversationalRetrievalChain
5
+ from langchain.memory import ConversationBufferMemory
6
+ from qdrant_client import QdrantClient
7
+ from qdrant_client.models import Distance, VectorParams
8
+
9
+ # Add project root to path for imports
10
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
+ from app.config import VECTOR_DB_PATH, COLLECTION_NAME
12
+ from app.core.llm import get_llm, get_embeddings, get_chat_model
13
+
14
+ class MemoryManager:
15
+ """Manages the RAG memory system using a vector database."""
16
+
17
+ def __init__(self):
18
+ self.embeddings = get_embeddings()
19
+ self.llm = get_llm()
20
+ self.chat_model = get_chat_model()
21
+ self.client = self._init_qdrant_client()
22
+ self.vectorstore = self._init_vector_store()
23
+ self.memory = ConversationBufferMemory(
24
+ memory_key="chat_history",
25
+ return_messages=True
26
+ )
27
+
28
+ def _init_qdrant_client(self):
29
+ """Initialize the Qdrant client."""
30
+ os.makedirs(VECTOR_DB_PATH, exist_ok=True)
31
+ return QdrantClient(path=VECTOR_DB_PATH)
32
+
33
+ def _init_vector_store(self):
34
+ """Initialize the vector store."""
35
+ collections = self.client.get_collections().collections
36
+ collection_names = [collection.name for collection in collections]
37
+
38
+ # Get vector dimension from the embedding model
39
+ vector_size = len(self.embeddings.embed_query("test"))
40
+
41
+ if COLLECTION_NAME not in collection_names:
42
+ self.client.create_collection(
43
+ collection_name=COLLECTION_NAME,
44
+ vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE),
45
+ )
46
+
47
+ return Qdrant(
48
+ client=self.client,
49
+ collection_name=COLLECTION_NAME,
50
+ embeddings=self.embeddings
51
+ )
52
+
53
+ def get_retriever(self):
54
+ """Get the retriever for RAG."""
55
+ return self.vectorstore.as_retriever(
56
+ search_type="similarity",
57
+ search_kwargs={"k": 5}
58
+ )
59
+
60
+ def create_rag_chain(self):
61
+ """Create a RAG chain for question answering."""
62
+ # Using the chat model created with the regular LLM
63
+ return ConversationalRetrievalChain.from_llm(
64
+ llm=self.llm,
65
+ retriever=self.get_retriever(),
66
+ memory=self.memory,
67
+ return_source_documents=True
68
+ )
69
+
70
+ def add_texts(self, texts, metadatas=None):
71
+ """Add texts to the vector store."""
72
+ return self.vectorstore.add_texts(texts=texts, metadatas=metadatas)
73
+
74
+ def similarity_search(self, query, k=5):
75
+ """Perform a similarity search."""
76
+ return self.vectorstore.similarity_search(query, k=k)
app/main.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import uvicorn
4
+ from fastapi import FastAPI, HTTPException, Depends, Request, File, UploadFile
5
+ from fastapi.responses import JSONResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from pydantic import BaseModel
8
+ from typing import List, Dict, Any, Optional
9
+ import tempfile
10
+
11
+ # Add project root to path for imports
12
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+ from app.core.agent import AssistantAgent
14
+ from app.core.ingestion import DocumentProcessor
15
+ from app.utils.helpers import get_document_path
16
+ from app.config import create_env_example
17
+
18
+ # Create .env.example file if it doesn't exist
19
+ create_env_example()
20
+
21
+ # Create FastAPI app
22
+ app = FastAPI(
23
+ title="Personal AI Assistant API",
24
+ description="API for a personal AI assistant with RAG capabilities",
25
+ version="1.0.0"
26
+ )
27
+
28
+ # Add CORS middleware
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=["*"],
32
+ allow_credentials=True,
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
+
37
+ # Initialize the agent and document processor
38
+ agent = AssistantAgent()
39
+ document_processor = DocumentProcessor(agent.memory_manager)
40
+
41
+ # Define request and response models
42
+ class QueryRequest(BaseModel):
43
+ query: str
44
+
45
+ class QueryResponse(BaseModel):
46
+ answer: str
47
+ sources: List[Dict[str, Any]]
48
+
49
+ class TextIngestionRequest(BaseModel):
50
+ text: str
51
+ metadata: Optional[Dict[str, Any]] = None
52
+
53
+ # Define API endpoints
54
+ @app.get("/")
55
+ async def root():
56
+ return {"message": "Welcome to the Personal AI Assistant API"}
57
+
58
+ @app.post("/query", response_model=QueryResponse)
59
+ async def query(request: QueryRequest):
60
+ """Query the assistant with a question."""
61
+ try:
62
+ response = agent.query(request.query)
63
+
64
+ # Add the conversation to memory
65
+ agent.add_conversation_to_memory(request.query, response["answer"])
66
+
67
+ return response
68
+ except Exception as e:
69
+ raise HTTPException(status_code=500, detail=str(e))
70
+
71
+ @app.post("/ingest/text")
72
+ async def ingest_text(request: TextIngestionRequest):
73
+ """Ingest text into the knowledge base."""
74
+ try:
75
+ metadata = request.metadata or {}
76
+
77
+ # Add the text to the knowledge base
78
+ ids = document_processor.ingest_text(request.text, metadata)
79
+
80
+ return {"message": "Text ingested successfully", "ids": ids}
81
+ except Exception as e:
82
+ raise HTTPException(status_code=500, detail=str(e))
83
+
84
+ @app.post("/ingest/file")
85
+ async def ingest_file(file: UploadFile = File(...)):
86
+ """Ingest a file into the knowledge base."""
87
+ try:
88
+ # Create a temporary file
89
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file.filename.split('.')[-1]}") as tmp:
90
+ content = await file.read()
91
+ tmp.write(content)
92
+ tmp_path = tmp.name
93
+
94
+ # Get a path to store the document
95
+ doc_path = get_document_path(file.filename)
96
+
97
+ # Copy the file to the documents directory
98
+ with open(doc_path, "wb") as f:
99
+ # Seek to the beginning of the file
100
+ await file.seek(0)
101
+ content = await file.read()
102
+ f.write(content)
103
+
104
+ # Ingest the document
105
+ metadata = {"original_name": file.filename}
106
+ ids = document_processor.ingest_file(tmp_path, metadata)
107
+
108
+ # Clean up the temporary file
109
+ os.unlink(tmp_path)
110
+
111
+ return {"message": f"File {file.filename} ingested successfully", "ids": ids}
112
+ except Exception as e:
113
+ raise HTTPException(status_code=500, detail=str(e))
114
+
115
+ # Run the application
116
+ if __name__ == "__main__":
117
+ uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
app/ui/streamlit_app.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ from datetime import datetime
6
+ from typing import List, Dict, Any
7
+
8
+ # Add project root to path for imports
9
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
10
+ from app.core.agent import AssistantAgent
11
+ from app.core.ingestion import DocumentProcessor
12
+ from app.utils.helpers import get_document_path, format_sources, save_conversation
13
+ from app.config import LLM_MODEL, EMBEDDING_MODEL
14
+
15
+ # Set page config
16
+ st.set_page_config(
17
+ page_title="Personal AI Assistant (Hugging Face)",
18
+ page_icon="🤗",
19
+ layout="wide"
20
+ )
21
+
22
+ # Initialize session state variables
23
+ if "messages" not in st.session_state:
24
+ st.session_state.messages = []
25
+
26
+ if "agent" not in st.session_state:
27
+ st.session_state.agent = AssistantAgent()
28
+
29
+ if "document_processor" not in st.session_state:
30
+ st.session_state.document_processor = DocumentProcessor(st.session_state.agent.memory_manager)
31
+
32
+ # App title
33
+ st.title("🤗 Personal AI Assistant (Hugging Face)")
34
+
35
+ # Create a sidebar for uploading documents and settings
36
+ with st.sidebar:
37
+ st.header("Upload Documents")
38
+ uploaded_file = st.file_uploader("Choose a file", type=["pdf", "txt", "csv"])
39
+
40
+ if uploaded_file is not None:
41
+ # Create a temporary file
42
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp:
43
+ tmp.write(uploaded_file.getvalue())
44
+ tmp_path = tmp.name
45
+
46
+ if st.button("Process Document"):
47
+ with st.spinner("Processing document..."):
48
+ try:
49
+ # Get a path to store the document
50
+ doc_path = get_document_path(uploaded_file.name)
51
+
52
+ # Copy the file to the documents directory
53
+ with open(doc_path, "wb") as f:
54
+ f.write(uploaded_file.getvalue())
55
+
56
+ # Ingest the document
57
+ st.session_state.document_processor.ingest_file(tmp_path, {"original_name": uploaded_file.name})
58
+
59
+ # Clean up the temporary file
60
+ os.unlink(tmp_path)
61
+
62
+ st.success(f"Document {uploaded_file.name} processed successfully!")
63
+ except Exception as e:
64
+ st.error(f"Error processing document: {str(e)}")
65
+
66
+ st.header("Raw Text Input")
67
+ text_input = st.text_area("Enter text to add to the knowledge base")
68
+
69
+ if st.button("Add Text"):
70
+ if text_input:
71
+ with st.spinner("Adding text to knowledge base..."):
72
+ try:
73
+ # Create metadata
74
+ metadata = {
75
+ "type": "manual_input",
76
+ "timestamp": str(datetime.now())
77
+ }
78
+
79
+ # Ingest the text
80
+ st.session_state.document_processor.ingest_text(text_input, metadata)
81
+
82
+ st.success("Text added to knowledge base successfully!")
83
+ except Exception as e:
84
+ st.error(f"Error adding text: {str(e)}")
85
+
86
+ # Display model information
87
+ st.header("Models")
88
+ st.write(f"**LLM**: [{LLM_MODEL}](https://huggingface.co/{LLM_MODEL})")
89
+ st.write(f"**Embeddings**: [{EMBEDDING_MODEL}](https://huggingface.co/{EMBEDDING_MODEL})")
90
+
91
+ # Add Hugging Face deployment info
92
+ st.header("Deployment")
93
+ st.write("This app can be easily deployed to [Hugging Face Spaces](https://huggingface.co/spaces) for free hosting.")
94
+
95
+ # Link to Hugging Face
96
+ st.markdown("""
97
+ <div style="text-align: center; margin-top: 20px;">
98
+ <a href="https://huggingface.co" target="_blank">
99
+ <img src="https://huggingface.co/front/assets/huggingface_logo.svg" width="200" alt="Hugging Face">
100
+ </a>
101
+ </div>
102
+ """, unsafe_allow_html=True)
103
+
104
+ # Display chat messages
105
+ for message in st.session_state.messages:
106
+ with st.chat_message(message["role"]):
107
+ st.write(message["content"])
108
+
109
+ # Display sources if available
110
+ if message["role"] == "assistant" and "sources" in message:
111
+ with st.expander("View Sources"):
112
+ sources = message["sources"]
113
+ if sources:
114
+ for i, source in enumerate(sources, 1):
115
+ st.write(f"{i}. {source['file_name']}" + (f" (Page {source['page']})" if source.get('page') else ""))
116
+ st.text(source['content'])
117
+ else:
118
+ st.write("No specific sources used.")
119
+
120
+ # Chat input
121
+ if prompt := st.chat_input("Ask a question..."):
122
+ # Add user message to chat history
123
+ st.session_state.messages.append({"role": "user", "content": prompt})
124
+
125
+ # Display user message
126
+ with st.chat_message("user"):
127
+ st.write(prompt)
128
+
129
+ # Generate response
130
+ with st.chat_message("assistant"):
131
+ with st.spinner("Thinking..."):
132
+ response = st.session_state.agent.query(prompt)
133
+ answer = response["answer"]
134
+ sources = response["sources"]
135
+
136
+ # Display the response
137
+ st.write(answer)
138
+
139
+ # Display sources in an expander
140
+ with st.expander("View Sources"):
141
+ if sources:
142
+ for i, source in enumerate(sources, 1):
143
+ st.write(f"{i}. {source['file_name']}" + (f" (Page {source['page']})" if source.get('page') else ""))
144
+ st.text(source['content'])
145
+ else:
146
+ st.write("No specific sources used.")
147
+
148
+ # Save conversation
149
+ save_conversation(prompt, answer, sources)
150
+
151
+ # Add assistant response to chat history
152
+ st.session_state.messages.append({
153
+ "role": "assistant",
154
+ "content": answer,
155
+ "sources": sources
156
+ })
157
+
158
+ # Update the agent's memory
159
+ st.session_state.agent.add_conversation_to_memory(prompt, answer)
160
+
161
+ # Add a footer
162
+ st.markdown("---")
163
+ st.markdown("Built with LangChain, Hugging Face, and Qdrant")
164
+
165
+ if __name__ == "__main__":
166
+ # This is used when running the file directly
167
+ pass
app/utils/helpers.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from datetime import datetime
4
+ from typing import List, Dict, Any
5
+
6
+ def sanitize_filename(filename: str) -> str:
7
+ """Sanitize a filename by removing invalid characters."""
8
+ # Replace invalid characters with underscores
9
+ invalid_chars = '<>:"/\\|?*'
10
+ for char in invalid_chars:
11
+ filename = filename.replace(char, '_')
12
+ return filename
13
+
14
+ def get_document_path(filename: str) -> str:
15
+ """Get the path to store a document."""
16
+ # Get the documents directory
17
+ docs_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'data', 'documents')
18
+
19
+ # Create the directory if it doesn't exist
20
+ os.makedirs(docs_dir, exist_ok=True)
21
+
22
+ # Sanitize the filename
23
+ filename = sanitize_filename(filename)
24
+
25
+ # Add a timestamp to make the filename unique
26
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
27
+ base, ext = os.path.splitext(filename)
28
+ unique_filename = f"{base}_{timestamp}{ext}"
29
+
30
+ return os.path.join(docs_dir, unique_filename)
31
+
32
+ def format_sources(sources: List[Dict[str, Any]]) -> str:
33
+ """Format source documents for display."""
34
+ if not sources:
35
+ return "No sources found."
36
+
37
+ formatted = []
38
+ for i, source in enumerate(sources, 1):
39
+ source_str = f"{i}. {source['file_name']} "
40
+ if source.get('page'):
41
+ source_str += f"(Page {source['page']}) "
42
+ formatted.append(source_str)
43
+
44
+ return "\n".join(formatted)
45
+
46
+ def save_conversation(question: str, answer: str, sources: List[Dict[str, Any]]) -> str:
47
+ """Save a conversation to a file."""
48
+ # Create a directory for conversations
49
+ conv_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'data', 'conversations')
50
+ os.makedirs(conv_dir, exist_ok=True)
51
+
52
+ # Create a filename based on the timestamp and first few words of the question
53
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
54
+ question_slug = "_".join(question.split()[:5]).lower()
55
+ question_slug = sanitize_filename(question_slug)
56
+ filename = f"{timestamp}_{question_slug}.txt"
57
+
58
+ # Format the conversation
59
+ formatted_sources = format_sources(sources)
60
+ content = f"Question: {question}\n\nAnswer: {answer}\n\nSources:\n{formatted_sources}\n"
61
+
62
+ # Save the conversation
63
+ filepath = os.path.join(conv_dir, filename)
64
+ with open(filepath, 'w') as f:
65
+ f.write(content)
66
+
67
+ return filepath
huggingface-space.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ title: Personal AI Assistant with RAG
2
+ emoji: 🤗
3
+ colorFrom: indigo
4
+ colorTo: purple
5
+ sdk: docker
6
+ app_port: 8501
7
+ pinned: true
8
+ license: mit
9
+ duplicated_from: huggingface/transformers-examples
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ langchain==0.1.3
2
+ langchain-community==0.0.16
3
+ huggingface-hub==0.20.2
4
+ transformers==4.36.2
5
+ sentence-transformers==2.2.2
6
+ numpy==1.26.3
7
+ qdrant-client==1.7.0
8
+ fastapi==0.104.1
9
+ uvicorn==0.24.0
10
+ python-dotenv==1.0.0
11
+ pydantic==2.5.2
12
+ tiktoken==0.5.2
13
+ pypdf==3.17.1
14
+ streamlit==1.29.0
15
+ torch==2.1.2
run.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ import os
3
+ import sys
4
+ import argparse
5
+ import subprocess
6
+
7
+ def setup_environment():
8
+ """Check if the environment is set up correctly."""
9
+ # Check if .env file exists
10
+ if not os.path.exists('.env'):
11
+ if os.path.exists('.env.example'):
12
+ print("Warning: .env file not found. Creating from .env.example...")
13
+ with open('.env.example', 'r') as example, open('.env', 'w') as env:
14
+ env.write(example.read())
15
+ print("Created .env file. Please edit it with your API keys and settings.")
16
+ sys.exit(1)
17
+ else:
18
+ print("Error: Neither .env nor .env.example file found.")
19
+ sys.exit(1)
20
+
21
+ # Create necessary directories
22
+ os.makedirs('data/documents', exist_ok=True)
23
+ os.makedirs('data/vector_db', exist_ok=True)
24
+
25
+ def run_api():
26
+ """Run the FastAPI server."""
27
+ print("Starting API server...")
28
+ subprocess.run(["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"])
29
+
30
+ def run_ui():
31
+ """Run the Streamlit UI."""
32
+ print("Starting Streamlit UI...")
33
+ subprocess.run(["streamlit", "run", "app/ui/streamlit_app.py"])
34
+
35
+ def main():
36
+ parser = argparse.ArgumentParser(description="Run the Personal AI Assistant")
37
+ parser.add_argument('--api', action='store_true', help='Run the FastAPI server')
38
+ parser.add_argument('--ui', action='store_true', help='Run the Streamlit UI')
39
+ args = parser.parse_args()
40
+
41
+ setup_environment()
42
+
43
+ if args.api:
44
+ run_api()
45
+ elif args.ui:
46
+ run_ui()
47
+ else:
48
+ print("Please specify either --api or --ui")
49
+ print("Examples:")
50
+ print(" python run.py --api # Run the API server")
51
+ print(" python run.py --ui # Run the Streamlit UI")
52
+
53
+ if __name__ == "__main__":
54
+ main()
space.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Register this app with Hugging Face Spaces SDK.
4
+ This file is used for deploying the app to Hugging Face Spaces.
5
+ """
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from huggingface_hub import SpaceHardware, SpaceStage, SpaceSDK
10
+
11
+ def create_space():
12
+ """Create or update a Hugging Face Space."""
13
+ # Get the Space name or use a default
14
+ space_name = os.environ.get("SPACE_NAME", "personal-rag-assistant")
15
+ owner = os.environ.get("HF_USERNAME")
16
+
17
+ if not owner:
18
+ print("Please set the HF_USERNAME environment variable to your Hugging Face username.")
19
+ sys.exit(1)
20
+
21
+ # Initialize the SDK
22
+ sdk = SpaceSDK(
23
+ space_id=f"{owner}/{space_name}",
24
+ token=os.environ.get("HF_TOKEN")
25
+ )
26
+
27
+ # Check if space exists, if not create it
28
+ try:
29
+ space_info = sdk.get_space_runtime()
30
+ print(f"Space {owner}/{space_name} exists.")
31
+ exists = True
32
+ except Exception:
33
+ exists = False
34
+
35
+ # Create or update the space
36
+ if not exists:
37
+ print(f"Creating new space: {owner}/{space_name}")
38
+ sdk.create_space(
39
+ space_hardware=SpaceHardware.CPU_BASIC,
40
+ space_storage=1,
41
+ space_sleep_time=3600, # 1 hour of inactivity before sleep
42
+ space_stage=SpaceStage.RUNNING,
43
+ )
44
+
45
+ print(f"Space URL: https://huggingface.co/spaces/{owner}/{space_name}")
46
+ return sdk
47
+
48
+ if __name__ == "__main__":
49
+ create_space()