philip11 commited on
Commit
be454f3
Β·
verified Β·
1 Parent(s): 549a49a

Upload 17 files

Browse files
README.md CHANGED
@@ -1,19 +1,82 @@
1
  ---
2
- title: Agentic RAG Chatbot
3
- emoji: πŸš€
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Streamlit template space
12
  ---
 
13
 
14
- # Welcome to Streamlit!
 
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
 
 
 
 
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ app_file: ui/streamlit_app.py
3
+ sdk: streamlit
 
 
 
 
 
 
 
 
4
  ---
5
+ # Agentic RAG Chatbot
6
 
7
+ ## Project Description
8
+ This project implements an Agentic RAG (Retrieval Augmented Generation) Chatbot designed to answer questions based on uploaded documents. It leverages various agents for ingestion, retrieval, and LLM response generation, providing a conversational interface through a Streamlit application.
9
 
10
+ ## Features
11
+ - **Document Upload**: Supports PDF, PPTX, CSV, DOCX, TXT, and MD file formats.
12
+ - **Intelligent Retrieval**: Retrieves relevant information from uploaded documents to answer user queries.
13
+ - **Conversational Interface**: Interact with the chatbot through a user-friendly Streamlit UI.
14
+ - **Modular Agentic Architecture**: Built with distinct agents for better maintainability and scalability.
15
 
16
+ ## Hugging Face Spaces Deployment
17
+
18
+ To deploy this application on Hugging Face Spaces, follow these steps:
19
+
20
+ 1. **Create a New Space**: Go to [Hugging Face Spaces](https://huggingface.co/spaces/new) and create a new Space. Choose `Streamlit` as the SDK.
21
+ 2. **Upload Files**: Upload all project files and directories (`agents/`, `config/`, `core/`, `ui/`, `main.py`, `requirements.txt`, `ad.txt`, `PHILIP_SIMON_DEROCK.pdf`) to your Hugging Face Space repository.
22
+ 3. **Entry Point**: Ensure your main application file is named `app.py`. If you have a `main.py` like in this project, you will need to rename it to `app.py` or create an `app.py` that imports and runs your Streamlit app.
23
+ * In this project, `app.py` has been created to serve as the entry point, which calls `StreamlitApp` from `ui/streamlit_app.py`.
24
+ 4. **Dependencies**: The `requirements.txt` file specifies all necessary Python dependencies. Hugging Face Spaces will automatically install these when building your Space.
25
+ 5. **Environment Variables**: Set the `GOOGLE_API_KEY` environment variable in your Space settings.
26
+ * Go to your Space settings (usually `Settings` tab in your Space).
27
+ * Scroll down to "Repository secrets" or "Environment variables".
28
+ * Add a new secret/variable named `GOOGLE_API_KEY` with your actual Google API Key.
29
+
30
+ Your Space should now be ready to deploy and run the Agentic RAG Chatbot.
31
+
32
+ ## Local Setup (Optional)
33
+
34
+ To run this project locally, follow these steps:
35
+
36
+ 1. **Clone the repository**:
37
+ ```bash
38
+ git clone <repository_url>
39
+ cd Z
40
+ ```
41
+ 2. **Create a virtual environment** (recommended):
42
+ ```bash
43
+ python -m venv virtual_container
44
+ source virtual_container/bin/activate
45
+ ```
46
+ 3. **Install dependencies**:
47
+ ```bash
48
+ pip install -r requirements.txt
49
+ ```
50
+ 4. **Set Environment Variable**: Set your `GOOGLE_API_KEY`:
51
+ ```bash
52
+ export GOOGLE_API_KEY="your_google_api_key"
53
+ # For Windows (Command Prompt):
54
+ # set GOOGLE_API_KEY="your_google_api_key"
55
+ # For Windows (PowerShell):
56
+ # $env:GOOGLE_API_KEY="your_google_api_key"
57
+ ```
58
+ 5. **Run the Streamlit application**:
59
+ ```bash
60
+ streamlit run app.py
61
+ ```
62
+ The application will typically open in your web browser at `http://localhost:8501`.
63
+
64
+ ## Usage
65
+
66
+ 1. **Upload Documents**: Use the sidebar to upload PDF, PPTX, CSV, DOCX, TXT, or MD files.
67
+ 2. **Ask Questions**: Once documents are uploaded, type your questions in the chat input field to get answers based on the document content.
68
+
69
+ ## Project Structure
70
+
71
+ ```
72
+ Z/
73
+ β”œβ”€β”€ agents/ # Contains different agents (base, ingestion, llm_response, retrieval)
74
+ β”œβ”€β”€ config/ # Configuration settings
75
+ β”œβ”€β”€ core/ # Core functionalities like document parsing and vector store
76
+ β”œβ”€β”€ ui/ # Streamlit UI components
77
+ β”œβ”€β”€ app.py # Main entry point for the Streamlit application (for Hugging Face Spaces)
78
+ β”œβ”€β”€ main_local.py # Original main entry point (can be removed or kept for local dev)
79
+ β”œβ”€β”€ requirements.txt # Python dependencies
80
+ β”œβ”€β”€ README.md # Project README and deployment instructions
81
+ └── ... other files (ad.txt, PHILIP_SIMON_DEROCK.pdf)
82
+ ```
agents/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # agents/__init__.py
2
+ """Agentic RAG Chatbot - Agent Package"""
3
+
4
+ from .base_agent import BaseAgent
5
+ from .ingestion_agent import IngestionAgent
6
+ from .retrieval_agent import RetrievalAgent
7
+ from .llm_response_agent import LLMResponseAgent
8
+
9
+ __all__ = ['BaseAgent', 'IngestionAgent', 'RetrievalAgent', 'LLMResponseAgent']
agents/base_agent.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base Agent Class for MCP Communication"""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Any
5
+ from core.mcp import MCPMessage, message_bus
6
+
7
+ class BaseAgent(ABC):
8
+ """Abstract base class for all agents"""
9
+
10
+ def __init__(self, name: str):
11
+ self.name = name
12
+ self.message_bus = message_bus
13
+
14
+ # Subscribe to message bus
15
+ self.message_bus.subscribe(self.name, self.handle_message)
16
+
17
+ @abstractmethod
18
+ async def handle_message(self, message: MCPMessage) -> None:
19
+ """Handle incoming MCP messages"""
20
+ pass
21
+
22
+ async def send_message(self, receiver: str, msg_type, payload: Dict[str, Any],
23
+ trace_id: str = None) -> None:
24
+ """Send message to another agent"""
25
+ message = self.message_bus.create_message(
26
+ sender=self.name,
27
+ receiver=receiver,
28
+ msg_type=msg_type,
29
+ payload=payload,
30
+ trace_id=trace_id
31
+ )
32
+ await self.message_bus.send_message(message)
33
+
34
+ def log(self, message: str, level: str = "INFO") -> None:
35
+ """Simple logging mechanism"""
36
+ print(f"[{level}] {self.name}: {message}")
agents/ingestion_agent.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ingestion Agent for Document Processing"""
2
+
3
+ from typing import Dict, Any
4
+ from agents.base_agent import BaseAgent
5
+ from core.mcp import MCPMessage, MessageType
6
+ from core.document_parser import DocumentParser
7
+
8
+ class IngestionAgent(BaseAgent):
9
+ """Agent responsible for document ingestion and parsing"""
10
+
11
+ def __init__(self):
12
+ super().__init__("IngestionAgent")
13
+ self.parser = DocumentParser()
14
+
15
+ async def handle_message(self, message: MCPMessage) -> None:
16
+ """Handle incoming messages"""
17
+ try:
18
+ if message.type == MessageType.DOC_UPLOADED:
19
+ await self._process_document(message)
20
+ else:
21
+ self.log(f"Unhandled message type: {message.type}", "WARNING")
22
+ except Exception as e:
23
+ self.log(f"Error handling message: {str(e)}", "ERROR")
24
+ await self._send_error_message(message, str(e))
25
+
26
+ async def _process_document(self, message: MCPMessage) -> None:
27
+ """Process uploaded document"""
28
+ payload = message.payload
29
+ file_name = payload.get('file_name')
30
+ file_data = payload.get('file_data')
31
+
32
+ if not file_name or not file_data:
33
+ raise ValueError("Missing file_name or file_data in payload")
34
+
35
+ self.log(f"Processing document: {file_name}")
36
+
37
+ # Parse document into chunks
38
+ chunks = self.parser.parse_document(file_data, file_name)
39
+
40
+ self.log(f"Extracted {len(chunks)} chunks from {file_name}")
41
+
42
+ # Send chunks to RetrievalAgent
43
+ await self.send_message(
44
+ receiver="RetrievalAgent",
45
+ msg_type=MessageType.DOC_INGESTED,
46
+ payload={
47
+ "chunks": chunks,
48
+ "file_name": file_name,
49
+ "total_chunks": len(chunks)
50
+ },
51
+ trace_id=message.trace_id
52
+ )
53
+
54
+ async def _send_error_message(self, original_message: MCPMessage, error: str) -> None:
55
+ """Send error message back to sender"""
56
+ await self.send_message(
57
+ receiver=original_message.sender,
58
+ msg_type=MessageType.ERROR,
59
+ payload={
60
+ "error": error,
61
+ "original_message": original_message.to_dict()
62
+ },
63
+ trace_id=original_message.trace_id
64
+ )
agents/llm_response_agent.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM Response Agent for Answer Generation"""
2
+
3
+ from typing import Dict, Any
4
+ import google.generativeai as genai
5
+ from agents.base_agent import BaseAgent
6
+ from core.mcp import MCPMessage, MessageType
7
+ from config.settings import get_settings
8
+
9
+ class LLMResponseAgent(BaseAgent):
10
+ """Agent responsible for generating responses using LLM"""
11
+
12
+ def __init__(self):
13
+ super().__init__("LLMResponseAgent")
14
+ self.settings = get_settings()
15
+ self._initialize_llm()
16
+
17
+ def _initialize_llm(self) -> None:
18
+ """Initialize Google Gemini LLM"""
19
+ api_key = self.settings.get('GOOGLE_API_KEY')
20
+ if api_key:
21
+ genai.configure(api_key=api_key)
22
+ self.model = genai.GenerativeModel('gemini-1.5-flash-latest')
23
+ self.llm_available = True
24
+ self.log("Gemini LLM initialized")
25
+ else:
26
+ self.llm_available = False
27
+ self.log("No API key provided - using fallback responses", "WARNING")
28
+
29
+ async def handle_message(self, message: MCPMessage) -> None:
30
+ """Handle incoming messages"""
31
+ try:
32
+ if message.type == MessageType.USER_QUERY:
33
+ await self._process_user_query(message)
34
+ elif message.type == MessageType.RETRIEVAL_RESULT:
35
+ await self._generate_response(message)
36
+ else:
37
+ self.log(f"Unhandled message type: {message.type}", "WARNING")
38
+ except Exception as e:
39
+ self.log(f"Error handling message: {str(e)}", "ERROR")
40
+ await self._send_error_message(message, str(e))
41
+
42
+ async def _process_user_query(self, message: MCPMessage) -> None:
43
+ """Process user query and request retrieval"""
44
+ payload = message.payload
45
+ query = payload.get('query')
46
+
47
+ if not query:
48
+ raise ValueError("Missing query in user message")
49
+
50
+ self.log(f"Processing user query: {query}")
51
+
52
+ # Request retrieval from RetrievalAgent
53
+ await self.send_message(
54
+ receiver="RetrievalAgent",
55
+ msg_type=MessageType.RETRIEVAL_REQUEST,
56
+ payload={
57
+ "query": query,
58
+ "n_results": 5
59
+ },
60
+ trace_id=message.trace_id
61
+ )
62
+
63
+ async def _generate_response(self, message: MCPMessage) -> None:
64
+ """Generate response using LLM and retrieved context"""
65
+ payload = message.payload
66
+ query = payload.get('query')
67
+ retrieved_context = payload.get('retrieved_context', [])
68
+
69
+ self.log(f"Generating response for query with {len(retrieved_context)} context items")
70
+
71
+ # Generate response
72
+ if self.llm_available:
73
+ response = await self._generate_llm_response(query, retrieved_context)
74
+ else:
75
+ response = self._generate_fallback_response(query, retrieved_context)
76
+
77
+ # Extract source information
78
+ source_info = []
79
+ for context in retrieved_context:
80
+ source_entry = {"document": context.get('source', 'unknown')}
81
+
82
+ # Add location information
83
+ if 'page' in context:
84
+ source_entry['page'] = context['page']
85
+ elif 'slide' in context:
86
+ source_entry['slide'] = context['slide']
87
+ elif 'row' in context:
88
+ source_entry['row'] = context['row']
89
+ elif 'paragraph' in context:
90
+ source_entry['paragraph'] = context['paragraph']
91
+
92
+ source_info.append(source_entry)
93
+
94
+ # Send final response to UI
95
+ await self.send_message(
96
+ receiver="UI",
97
+ msg_type=MessageType.FINAL_RESPONSE,
98
+ payload={
99
+ "answer": response,
100
+ "source_info": source_info,
101
+ "query": query
102
+ },
103
+ trace_id=message.trace_id
104
+ )
105
+
106
+ async def _generate_llm_response(self, query: str, context: list) -> str:
107
+ """Generate response using Gemini LLM"""
108
+ # Prepare context text
109
+ context_text = "\n\n".join([
110
+ f"Source: {item.get('source', 'unknown')}\n{item['text']}"
111
+ for item in context
112
+ ])
113
+
114
+ # Create prompt
115
+ prompt = f"""
116
+ Based on the following context from uploaded documents, answer the user's question.
117
+ Be accurate, concise, and cite the sources when possible.
118
+
119
+ Context:
120
+ {context_text}
121
+
122
+ Question: {query}
123
+
124
+ Answer:
125
+ """
126
+
127
+ try:
128
+ response = self.model.generate_content(prompt)
129
+ return response.text
130
+ except Exception as e:
131
+ self.log(f"LLM generation error: {str(e)}", "ERROR")
132
+ return self._generate_fallback_response(query, context)
133
+
134
+ def _generate_fallback_response(self, query: str, context: list) -> str:
135
+ """Generate fallback response when LLM is not available"""
136
+ if not context:
137
+ return "I couldn't find relevant information in the uploaded documents to answer your question."
138
+
139
+ # Simple context-based response
140
+ context_snippets = []
141
+ for item in context[:3]: # Limit to top 3 results
142
+ source = item.get('source', 'unknown')
143
+ text = item['text'][:200] + "..." if len(item['text']) > 200 else item['text']
144
+ context_snippets.append(f"From {source}: {text}")
145
+
146
+ return f"Based on the uploaded documents, here are the most relevant passages:\n\n" + "\n\n".join(context_snippets)
147
+
148
+ async def _send_error_message(self, original_message: MCPMessage, error: str) -> None:
149
+ """Send error message to UI"""
150
+ await self.send_message(
151
+ receiver="UI",
152
+ msg_type=MessageType.ERROR,
153
+ payload={
154
+ "error": error,
155
+ "original_message": original_message.to_dict()
156
+ },
157
+ trace_id=original_message.trace_id
158
+ )
agents/retrieval_agent.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Retrieval Agent for Vector Search Operations"""
2
+
3
+ from typing import Dict, Any
4
+ from agents.base_agent import BaseAgent
5
+ from core.mcp import MCPMessage, MessageType
6
+ from core.vector_store import VectorStore
7
+
8
+ class RetrievalAgent(BaseAgent):
9
+ """Agent responsible for vector storage and retrieval"""
10
+
11
+ def __init__(self):
12
+ super().__init__("RetrievalAgent")
13
+ self.vector_store = VectorStore()
14
+
15
+ async def handle_message(self, message: MCPMessage) -> None:
16
+ """Handle incoming messages"""
17
+ try:
18
+ if message.type == MessageType.DOC_INGESTED:
19
+ await self._ingest_chunks(message)
20
+ elif message.type == MessageType.RETRIEVAL_REQUEST:
21
+ await self._perform_retrieval(message)
22
+ else:
23
+ self.log(f"Unhandled message type: {message.type}", "WARNING")
24
+ except Exception as e:
25
+ self.log(f"Error handling message: {str(e)}", "ERROR")
26
+ await self._send_error_message(message, str(e))
27
+
28
+ async def _ingest_chunks(self, message: MCPMessage) -> None:
29
+ """Ingest document chunks into vector store"""
30
+ payload = message.payload
31
+ chunks = payload.get('chunks', [])
32
+ file_name = payload.get('file_name')
33
+
34
+ self.log(f"Ingesting {len(chunks)} chunks from {file_name}")
35
+
36
+ # Add chunks to vector store
37
+ self.vector_store.add_documents(chunks)
38
+
39
+ # Log statistics
40
+ stats = self.vector_store.get_collection_stats()
41
+ self.log(f"Vector store now contains {stats['total_documents']} documents")
42
+
43
+ async def _perform_retrieval(self, message: MCPMessage) -> None:
44
+ """Perform semantic search and return results"""
45
+ payload = message.payload
46
+ query = payload.get('query')
47
+ n_results = payload.get('n_results', 5)
48
+
49
+ if not query:
50
+ raise ValueError("Missing query in retrieval request")
51
+
52
+ self.log(f"Performing retrieval for query: {query}")
53
+
54
+ # Search vector store
55
+ results = self.vector_store.search(query, n_results)
56
+
57
+ self.log(f"Found {len(results)} relevant chunks")
58
+
59
+ # Format context for LLM
60
+ retrieved_context = []
61
+ for result in results:
62
+ context_item = {
63
+ "text": result['text'],
64
+ "source": result.get('source', 'unknown'),
65
+ "score": result.get('score', 0.0)
66
+ }
67
+
68
+ # Add location metadata based on document type
69
+ if result.get('type') == 'pdf':
70
+ context_item['page'] = result.get('page')
71
+ elif result.get('type') == 'pptx':
72
+ context_item['slide'] = result.get('slide')
73
+ elif result.get('type') == 'csv':
74
+ context_item['row'] = result.get('row')
75
+ elif result.get('type') in ['docx', 'txt', 'md']:
76
+ context_item['paragraph'] = result.get('paragraph')
77
+
78
+ retrieved_context.append(context_item)
79
+
80
+ # Send results to LLMResponseAgent
81
+ await self.send_message(
82
+ receiver="LLMResponseAgent",
83
+ msg_type=MessageType.RETRIEVAL_RESULT,
84
+ payload={
85
+ "retrieved_context": retrieved_context,
86
+ "query": query,
87
+ "total_results": len(results)
88
+ },
89
+ trace_id=message.trace_id
90
+ )
91
+
92
+ async def _send_error_message(self, original_message: MCPMessage, error: str) -> None:
93
+ """Send error message back to sender"""
94
+ await self.send_message(
95
+ receiver=original_message.sender,
96
+ msg_type=MessageType.ERROR,
97
+ payload={
98
+ "error": error,
99
+ "original_message": original_message.to_dict()
100
+ },
101
+ trace_id=original_message.trace_id
102
+ )
103
+
104
+ def clear_store(self) -> None:
105
+ """Clear all documents from vector store"""
106
+ self.vector_store.clear()
107
+ self.log("Vector store cleared")
app.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+
4
+ # Add project root to Python path
5
+ # This is usually not necessary in Hugging Face Spaces if the structure is flat,
6
+ # but keeping it for robustness if subdirectories are needed.
7
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
8
+
9
+ from ui.streamlit_app import StreamlitApp
10
+
11
+ # Main function to run the application
12
+ def main():
13
+ print("πŸš€ Starting Agentic RAG Chatbot on Hugging Face Spaces...")
14
+ print("πŸ“ Make sure to set GOOGLE_API_KEY environment variable for full LLM functionality")
15
+
16
+ # Create and run the Streamlit app
17
+ app = StreamlitApp()
18
+ app.run()
19
+
20
+ if __name__ == "__main__":
21
+ main()
config/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # config/__init__.py
2
+ """Agentic RAG Chatbot - Configuration Package"""
3
+
4
+ from .settings import get_settings
5
+
6
+ __all__ = ['get_settings']
config/settings.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application Settings and Configuration"""
2
+
3
+ import os
4
+ from typing import Dict, Any
5
+
6
+ def get_settings() -> Dict[str, Any]:
7
+ """Get application settings from environment variables"""
8
+ return {
9
+ 'GOOGLE_API_KEY': os.environ.get('GOOGLE_API_KEY'),
10
+ 'EMBEDDING_MODEL': os.environ.get('EMBEDDING_MODEL', 'all-MiniLM-L6-v2'),
11
+ 'MAX_CHUNK_SIZE': int(os.environ.get('MAX_CHUNK_SIZE', '1000')),
12
+ 'RETRIEVAL_TOP_K': int(os.environ.get('RETRIEVAL_TOP_K', '5')),
13
+ 'DEBUG_MODE': os.environ.get('DEBUG_MODE', 'False').lower() == 'true'
14
+ }
core/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # core/__init__.py
2
+ """Agentic RAG Chatbot - Core Package"""
3
+
4
+ from .mcp import MCPMessage, MessageType, message_bus
5
+ from .document_parser import DocumentParser
6
+ from .vector_store import VectorStore
7
+
8
+ __all__ = ['MCPMessage', 'MessageType', 'message_bus', 'DocumentParser', 'VectorStore']
core/document_parser.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Document Parser for Multi-Format Support"""
2
+
3
+ import io
4
+ import PyPDF2
5
+ from pptx import Presentation
6
+ import pandas as pd
7
+ from docx import Document
8
+ import markdown
9
+ from typing import List, Dict, Any, Tuple
10
+
11
+ class DocumentParser:
12
+ """Unified document parsing for multiple formats"""
13
+
14
+ @staticmethod
15
+ def parse_pdf(file_data: bytes) -> List[Dict[str, Any]]:
16
+ """Parse PDF and extract text chunks"""
17
+ chunks = []
18
+ try:
19
+ pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_data))
20
+ for page_num, page in enumerate(pdf_reader.pages, 1):
21
+ text = page.extract_text()
22
+ if text.strip():
23
+ chunks.append({
24
+ "text": text.strip(),
25
+ "page": page_num,
26
+ "type": "pdf"
27
+ })
28
+ except Exception as e:
29
+ raise ValueError(f"PDF parsing error: {str(e)}")
30
+ return chunks
31
+
32
+ @staticmethod
33
+ def parse_pptx(file_data: bytes) -> List[Dict[str, Any]]:
34
+ """Parse PPTX and extract slide content"""
35
+ chunks = []
36
+ try:
37
+ prs = Presentation(io.BytesIO(file_data))
38
+ for slide_num, slide in enumerate(prs.slides, 1):
39
+ slide_text = []
40
+ for shape in slide.shapes:
41
+ if hasattr(shape, "text") and shape.text:
42
+ slide_text.append(shape.text)
43
+
44
+ if slide_text:
45
+ chunks.append({
46
+ "text": "\n".join(slide_text),
47
+ "slide": slide_num,
48
+ "type": "pptx"
49
+ })
50
+ except Exception as e:
51
+ raise ValueError(f"PPTX parsing error: {str(e)}")
52
+ return chunks
53
+
54
+ @staticmethod
55
+ def parse_csv(file_data: bytes) -> List[Dict[str, Any]]:
56
+ """Parse CSV and convert to text chunks"""
57
+ chunks = []
58
+ try:
59
+ df = pd.read_csv(io.StringIO(file_data.decode('utf-8')))
60
+
61
+ # Header chunk
62
+ chunks.append({
63
+ "text": f"CSV Headers: {', '.join(df.columns.tolist())}",
64
+ "row": 0,
65
+ "type": "csv"
66
+ })
67
+
68
+ # Row chunks (group by 10 rows for efficiency)
69
+ for i in range(0, len(df), 10):
70
+ chunk_df = df.iloc[i:i+10]
71
+ text_repr = chunk_df.to_string(index=False)
72
+ chunks.append({
73
+ "text": text_repr,
74
+ "row": i+1,
75
+ "type": "csv"
76
+ })
77
+ except Exception as e:
78
+ raise ValueError(f"CSV parsing error: {str(e)}")
79
+ return chunks
80
+
81
+ @staticmethod
82
+ def parse_docx(file_data: bytes) -> List[Dict[str, Any]]:
83
+ """Parse DOCX and extract paragraphs"""
84
+ chunks = []
85
+ try:
86
+ doc = Document(io.BytesIO(file_data))
87
+ for para_num, paragraph in enumerate(doc.paragraphs, 1):
88
+ if paragraph.text.strip():
89
+ chunks.append({
90
+ "text": paragraph.text.strip(),
91
+ "paragraph": para_num,
92
+ "type": "docx"
93
+ })
94
+ except Exception as e:
95
+ raise ValueError(f"DOCX parsing error: {str(e)}")
96
+ return chunks
97
+
98
+ @staticmethod
99
+ def parse_text(file_data: bytes, file_extension: str) -> List[Dict[str, Any]]:
100
+ """Parse TXT/MD files"""
101
+ chunks = []
102
+ try:
103
+ text = file_data.decode('utf-8')
104
+
105
+ if file_extension == '.md':
106
+ # Convert markdown to HTML then extract text
107
+ html = markdown.markdown(text)
108
+ text = html
109
+
110
+ # Split by paragraphs
111
+ paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
112
+ for para_num, paragraph in enumerate(paragraphs, 1):
113
+ chunks.append({
114
+ "text": paragraph,
115
+ "paragraph": para_num,
116
+ "type": file_extension[1:] # Remove dot
117
+ })
118
+ except Exception as e:
119
+ raise ValueError(f"Text parsing error: {str(e)}")
120
+ return chunks
121
+
122
+ @classmethod
123
+ def parse_document(cls, file_data: bytes, file_name: str) -> List[Dict[str, Any]]:
124
+ """Main parsing method - routes to appropriate parser"""
125
+ file_extension = file_name.lower().split('.')[-1]
126
+
127
+ parser_map = {
128
+ 'pdf': cls.parse_pdf,
129
+ 'pptx': cls.parse_pptx,
130
+ 'csv': cls.parse_csv,
131
+ 'docx': cls.parse_docx,
132
+ 'txt': lambda data: cls.parse_text(data, '.txt'),
133
+ 'md': lambda data: cls.parse_text(data, '.md')
134
+ }
135
+
136
+ if file_extension not in parser_map:
137
+ raise ValueError(f"Unsupported file format: {file_extension}")
138
+
139
+ chunks = parser_map[file_extension](file_data)
140
+
141
+ # Add source metadata to all chunks
142
+ for chunk in chunks:
143
+ chunk['source'] = file_name
144
+
145
+ return chunks
core/mcp.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model Context Protocol (MCP) Implementation for Agent Communication"""
2
+
3
+ import json
4
+ import uuid
5
+ from typing import Dict, Any, Optional
6
+ from dataclasses import dataclass, asdict
7
+ from enum import Enum
8
+ import asyncio
9
+ from collections import defaultdict
10
+
11
+ class MessageType(Enum):
12
+ DOC_UPLOADED = "DOC_UPLOADED"
13
+ DOC_INGESTED = "DOC_INGESTED"
14
+ USER_QUERY = "USER_QUERY"
15
+ RETRIEVAL_REQUEST = "RETRIEVAL_REQUEST"
16
+ RETRIEVAL_RESULT = "RETRIEVAL_RESULT"
17
+ FINAL_RESPONSE = "FINAL_RESPONSE"
18
+ ERROR = "ERROR"
19
+
20
+ @dataclass
21
+ class MCPMessage:
22
+ sender: str
23
+ receiver: str
24
+ type: MessageType
25
+ trace_id: str
26
+ payload: Dict[str, Any]
27
+
28
+ def to_dict(self) -> Dict[str, Any]:
29
+ return {
30
+ "sender": self.sender,
31
+ "receiver": self.receiver,
32
+ "type": self.type.value,
33
+ "trace_id": self.trace_id,
34
+ "payload": self.payload
35
+ }
36
+
37
+ class MCPMessageBus:
38
+ """In-memory message bus for agent communication"""
39
+
40
+ def __init__(self):
41
+ self._subscribers = defaultdict(list)
42
+ self._message_queue = asyncio.Queue()
43
+ self._handlers = {}
44
+
45
+ def subscribe(self, agent_name: str, handler_func):
46
+ """Subscribe an agent to receive messages"""
47
+ self._subscribers[agent_name].append(handler_func)
48
+ self._handlers[agent_name] = handler_func
49
+
50
+ async def send_message(self, message: MCPMessage):
51
+ """Send message to target agent"""
52
+ await self._message_queue.put(message)
53
+
54
+ # Direct delivery to handler
55
+ if message.receiver in self._handlers:
56
+ handler = self._handlers[message.receiver]
57
+ await handler(message)
58
+
59
+ def create_message(self, sender: str, receiver: str, msg_type: MessageType,
60
+ payload: Dict[str, Any], trace_id: Optional[str] = None) -> MCPMessage:
61
+ """Create standardized MCP message"""
62
+ return MCPMessage(
63
+ sender=sender,
64
+ receiver=receiver,
65
+ type=msg_type,
66
+ trace_id=trace_id or str(uuid.uuid4()),
67
+ payload=payload
68
+ )
69
+
70
+ # Global message bus instance
71
+ message_bus = MCPMessageBus()
core/vector_store.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Vector Store Implementation with ChromaDB"""
2
+
3
+ import chromadb
4
+ from chromadb.config import Settings
5
+ from sentence_transformers import SentenceTransformer
6
+ from typing import List, Dict, Any, Tuple
7
+ import uuid
8
+
9
+ class VectorStore:
10
+ """In-memory vector store using ChromaDB"""
11
+
12
+ def __init__(self, embedding_model: str = "all-MiniLM-L6-v2"):
13
+ # Initialize ChromaDB in memory
14
+ self.client = chromadb.Client(Settings(
15
+ allow_reset=True,
16
+ anonymized_telemetry=False
17
+ ))
18
+
19
+ # Initialize embedding model
20
+ self.embedding_model = SentenceTransformer(embedding_model)
21
+
22
+ # Create collection
23
+ self.collection = self.client.get_or_create_collection(
24
+ name="documents",
25
+ metadata={"hnsw:space": "cosine"}
26
+ )
27
+
28
+ def add_documents(self, chunks: List[Dict[str, Any]]) -> None:
29
+ """Add document chunks to vector store"""
30
+ if not chunks:
31
+ return
32
+
33
+ # Prepare data for ChromaDB
34
+ documents = []
35
+ metadatas = []
36
+ ids = []
37
+
38
+ for chunk in chunks:
39
+ # Generate unique ID
40
+ chunk_id = str(uuid.uuid4())
41
+
42
+ # Extract text for embedding
43
+ text = chunk['text']
44
+
45
+ # Prepare metadata (everything except text)
46
+ metadata = {k: v for k, v in chunk.items() if k != 'text'}
47
+
48
+ documents.append(text)
49
+ metadatas.append(metadata)
50
+ ids.append(chunk_id)
51
+
52
+ # Add to collection
53
+ self.collection.add(
54
+ documents=documents,
55
+ metadatas=metadatas,
56
+ ids=ids
57
+ )
58
+
59
+ def search(self, query: str, n_results: int = 5) -> List[Dict[str, Any]]:
60
+ """Search for relevant documents"""
61
+ if self.collection.count() == 0:
62
+ return []
63
+
64
+ # Perform similarity search
65
+ results = self.collection.query(
66
+ query_texts=[query],
67
+ n_results=min(n_results, self.collection.count())
68
+ )
69
+
70
+ # Format results
71
+ formatted_results = []
72
+ for i, doc in enumerate(results['documents'][0]):
73
+ metadata = results['metadatas'][0][i]
74
+
75
+ result = {
76
+ 'text': doc,
77
+ 'score': results['distances'][0][i] if 'distances' in results else 0.0,
78
+ **metadata
79
+ }
80
+ formatted_results.append(result)
81
+
82
+ return formatted_results
83
+
84
+ def get_collection_stats(self) -> Dict[str, Any]:
85
+ """Get statistics about the collection"""
86
+ return {
87
+ 'total_documents': self.collection.count(),
88
+ 'embedding_model': self.embedding_model.get_sentence_embedding_dimension()
89
+ }
90
+
91
+ def clear(self) -> None:
92
+ """Clear all documents from the store"""
93
+ self.client.reset()
main_local.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Main entry point for Agentic RAG Chatbot"""
2
+
3
+ import sys
4
+ import os
5
+
6
+ # Add project root to Python path
7
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
8
+
9
+ from ui.streamlit_app import StreamlitApp
10
+
11
+ def main():
12
+ """Main function to run the application"""
13
+ print("πŸš€ Starting Agentic RAG Chatbot...")
14
+ print("πŸ“ Make sure to set GOOGLE_API_KEY environment variable for full LLM functionality")
15
+ print("🌐 Access the application at: http://localhost:8501")
16
+
17
+ # Create and run the Streamlit app
18
+ app = StreamlitApp()
19
+ app.run()
20
+
21
+ if __name__ == "__main__":
22
+ main()
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.28.1
2
+ chromadb==0.5.3
3
+ sentence-transformers==2.7.0
4
+ pypdf2==3.0.1
5
+ python-pptx==0.6.22
6
+ pandas==1.5.3
7
+ python-docx==0.8.11
8
+ markdown==3.5.1
9
+ google-generativeai==0.7.1
10
+ numpy==1.24.3
11
+ python-dotenv
ui/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # ui/__init__.py
2
+ """Agentic RAG Chatbot - UI Package"""
3
+
4
+ from .streamlit_app import StreamlitApp
5
+
6
+ __all__ = ['StreamlitApp']
ui/streamlit_app.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit UI for Agentic RAG Chatbot"""
2
+
3
+ import streamlit as st
4
+ import asyncio
5
+ import uuid
6
+ from typing import Dict, Any, List
7
+ from agents.base_agent import BaseAgent
8
+ from core.mcp import MCPMessage, MessageType, message_bus
9
+
10
+ class UIAgent(BaseAgent):
11
+ """UI Agent for handling Streamlit interface"""
12
+
13
+ def __init__(self):
14
+ super().__init__("UI")
15
+ self.responses = {}
16
+ self.errors = {}
17
+
18
+ async def handle_message(self, message: MCPMessage) -> None:
19
+ """Handle incoming messages from other agents"""
20
+ trace_id = message.trace_id
21
+
22
+ if message.type == MessageType.FINAL_RESPONSE:
23
+ self.responses[trace_id] = message.payload
24
+ elif message.type == MessageType.ERROR:
25
+ self.errors[trace_id] = message.payload
26
+
27
+ async def upload_document(self, file_name: str, file_data: bytes) -> str:
28
+ """Upload document to ingestion agent"""
29
+ trace_id = str(uuid.uuid4())
30
+
31
+ await self.send_message(
32
+ receiver="IngestionAgent",
33
+ msg_type=MessageType.DOC_UPLOADED,
34
+ payload={
35
+ "file_name": file_name,
36
+ "file_data": file_data
37
+ },
38
+ trace_id=trace_id
39
+ )
40
+
41
+ return trace_id
42
+
43
+ async def ask_question(self, query: str) -> str:
44
+ """Send user query to LLM response agent"""
45
+ trace_id = str(uuid.uuid4())
46
+
47
+ await self.send_message(
48
+ receiver="LLMResponseAgent",
49
+ msg_type=MessageType.USER_QUERY,
50
+ payload={
51
+ "query": query
52
+ },
53
+ trace_id=trace_id
54
+ )
55
+
56
+ return trace_id
57
+
58
+ def get_response(self, trace_id: str) -> Dict[str, Any]:
59
+ """Get response for a trace ID"""
60
+ return self.responses.get(trace_id)
61
+
62
+ def get_error(self, trace_id: str) -> Dict[str, Any]:
63
+ """Get error for a trace ID"""
64
+ return self.errors.get(trace_id)
65
+
66
+ class StreamlitApp:
67
+ """Main Streamlit application"""
68
+
69
+ def __init__(self):
70
+ self.ui_agent = UIAgent()
71
+ self.init_session_state()
72
+
73
+ def init_session_state(self):
74
+ """Initialize Streamlit session state"""
75
+ if 'messages' not in st.session_state:
76
+ st.session_state.messages = []
77
+ if 'uploaded_files' not in st.session_state:
78
+ st.session_state.uploaded_files = []
79
+ if 'agents_initialized' not in st.session_state:
80
+ st.session_state.agents_initialized = False
81
+
82
+ def initialize_agents(self):
83
+ """Initialize all agents"""
84
+ if not st.session_state.agents_initialized:
85
+ from agents.ingestion_agent import IngestionAgent
86
+ from agents.retrieval_agent import RetrievalAgent
87
+ from agents.llm_response_agent import LLMResponseAgent
88
+
89
+ # Create agents
90
+ st.session_state.ingestion_agent = IngestionAgent()
91
+ st.session_state.retrieval_agent = RetrievalAgent()
92
+ st.session_state.llm_response_agent = LLMResponseAgent()
93
+
94
+ st.session_state.agents_initialized = True
95
+
96
+ def render_sidebar(self):
97
+ """Render sidebar with file upload and settings"""
98
+ st.sidebar.title("πŸ“ Document Upload")
99
+
100
+ uploaded_files = st.sidebar.file_uploader(
101
+ "Choose files",
102
+ type=['pdf', 'pptx', 'csv', 'docx', 'txt', 'md'],
103
+ accept_multiple_files=True
104
+ )
105
+
106
+ if uploaded_files:
107
+ for uploaded_file in uploaded_files:
108
+ if uploaded_file.name not in [f['name'] for f in st.session_state.uploaded_files]:
109
+ # Process file upload
110
+ file_data = uploaded_file.read()
111
+
112
+ # Upload to ingestion agent
113
+ trace_id = asyncio.run(self.ui_agent.upload_document(
114
+ uploaded_file.name, file_data
115
+ ))
116
+
117
+ # Add to session state
118
+ st.session_state.uploaded_files.append({
119
+ 'name': uploaded_file.name,
120
+ 'size': len(file_data),
121
+ 'trace_id': trace_id
122
+ })
123
+
124
+ st.sidebar.success(f"βœ… {uploaded_file.name} uploaded")
125
+
126
+ # Display uploaded files
127
+ if st.session_state.uploaded_files:
128
+ st.sidebar.subheader("πŸ“„ Uploaded Files")
129
+ for file_info in st.session_state.uploaded_files:
130
+ st.sidebar.text(f"β€’ {file_info['name']}")
131
+
132
+ # Settings
133
+ st.sidebar.subheader("βš™οΈ Settings")
134
+ if st.sidebar.button("Clear All Documents"):
135
+ st.session_state.uploaded_files = []
136
+ st.session_state.retrieval_agent.clear_store()
137
+ st.sidebar.success("Documents cleared")
138
+
139
+ def render_chat_interface(self):
140
+ """Render main chat interface"""
141
+ st.title("πŸ€– Agentic RAG Chatbot")
142
+ st.markdown("Upload documents and ask questions about their content!")
143
+
144
+ # Display chat messages
145
+ for message in st.session_state.messages:
146
+ with st.chat_message(message["role"]):
147
+ st.markdown(message["content"])
148
+
149
+ # Display sources if available
150
+ if message.get("sources"):
151
+ with st.expander("πŸ“š Sources"):
152
+ for source in message["sources"]:
153
+ source_text = f"**{source['document']}**"
154
+ if 'page' in source:
155
+ source_text += f" (Page {source['page']})"
156
+ elif 'slide' in source:
157
+ source_text += f" (Slide {source['slide']})"
158
+ elif 'row' in source:
159
+ source_text += f" (Row {source['row']})"
160
+ elif 'paragraph' in source:
161
+ source_text += f" (Paragraph {source['paragraph']})"
162
+
163
+ st.markdown(source_text)
164
+
165
+ # Chat input
166
+ if prompt := st.chat_input("Ask a question about your documents..."):
167
+ # Add user message
168
+ st.session_state.messages.append({"role": "user", "content": prompt})
169
+
170
+ # Display user message
171
+ with st.chat_message("user"):
172
+ st.markdown(prompt)
173
+
174
+ # Process question
175
+ with st.chat_message("assistant"):
176
+ with st.spinner("Thinking..."):
177
+ trace_id = asyncio.run(self.ui_agent.ask_question(prompt))
178
+
179
+ # Wait for response
180
+ response = None
181
+ error = None
182
+ max_attempts = 50
183
+ attempts = 0
184
+
185
+ while attempts < max_attempts:
186
+ response = self.ui_agent.get_response(trace_id)
187
+ error = self.ui_agent.get_error(trace_id)
188
+
189
+ if response or error:
190
+ break
191
+
192
+ asyncio.run(asyncio.sleep(0.1))
193
+ attempts += 1
194
+
195
+ if error:
196
+ error_msg = f"❌ Error: {error.get('error', 'Unknown error')}"
197
+ st.error(error_msg)
198
+ st.session_state.messages.append({
199
+ "role": "assistant",
200
+ "content": error_msg
201
+ })
202
+ elif response:
203
+ answer = response.get('answer', 'No answer generated')
204
+ sources = response.get('source_info', [])
205
+
206
+ st.markdown(answer)
207
+
208
+ # Display sources
209
+ if sources:
210
+ with st.expander("πŸ“š Sources"):
211
+ for source in sources:
212
+ source_text = f"**{source['document']}**"
213
+ if 'page' in source:
214
+ source_text += f" (Page {source['page']})"
215
+ elif 'slide' in source:
216
+ source_text += f" (Slide {source['slide']})"
217
+ elif 'row' in source:
218
+ source_text += f" (Row {source['row']})"
219
+ elif 'paragraph' in source:
220
+ source_text += f" (Paragraph {source['paragraph']})"
221
+
222
+ st.markdown(source_text)
223
+
224
+ st.session_state.messages.append({
225
+ "role": "assistant",
226
+ "content": answer,
227
+ "sources": sources
228
+ })
229
+ else:
230
+ timeout_msg = "⏰ Request timed out. Please try again."
231
+ st.error(timeout_msg)
232
+ st.session_state.messages.append({
233
+ "role": "assistant",
234
+ "content": timeout_msg
235
+ })
236
+
237
+ def run(self):
238
+ """Run the Streamlit application"""
239
+ st.set_page_config(
240
+ page_title="Agentic RAG Chatbot",
241
+ page_icon="πŸ€–",
242
+ layout="wide"
243
+ )
244
+
245
+ # CSS injection for black background and green text
246
+ st.markdown(
247
+ '''
248
+ <style>
249
+ html, body, [class*="st-"], [class*="css-"] {
250
+ background-color: #000000;
251
+ color: #00FF00; /* Retain green for general text */
252
+ }
253
+ h1 { /* Target for Streamlit titles */
254
+ color: red;
255
+ font-weight: bold; /* Make it thick */
256
+ }
257
+ </style>
258
+ ''',
259
+ unsafe_allow_html=True
260
+ )
261
+
262
+ self.initialize_agents()
263
+
264
+ self.render_sidebar()
265
+ self.render_chat_interface()
266
+
267
+ # Create and run the app
268
+ if __name__ == "__main__":
269
+ app = StreamlitApp()
270
+ app.run()