File size: 6,858 Bytes
b60a96f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
"""
Optimized RAG Chatbot - Memory Efficient!
Uses TinyLlama with batch processing
"""

import requests
import json
import numpy as np
from typing import List, Dict
import time

class OptimizedRAGChatbot:
    def __init__(self, model_name="tinyllama"):
        self.model_name = model_name
        self.ollama_url = "http://localhost:11434/api/generate"
        self.documents = []
        self.embeddings = []
        self.chunk_size = 300  # Smaller chunks
        self.chunk_overlap = 30
        self.max_context_length = 200  # Limit context size
        
        print(f"πŸ€– Optimized RAG with {model_name}")
        print(f"πŸ“Š Memory efficient mode: ON")
    
    def add_document(self, text: str, metadata: Dict = None):
        """Add document with smaller chunks"""
        chunks = self._chunk_text(text)
        
        for chunk in chunks:
            self.documents.append({
                'text': chunk[:200],  # Truncate long chunks
                'metadata': metadata or {}
            })
        
        # Simple embedding (no heavy computation)
        self._generate_simple_embeddings()
        
        print(f"βœ… Added {len(chunks)} chunks")
        print(f"πŸ“š Total: {len(self.documents)} chunks")
    
    def _chunk_text(self, text: str) -> List[str]:
        """Split text into smaller chunks"""
        words = text.split()
        chunks = []
        current_chunk = []
        current_size = 0
        
        for word in words:
            current_chunk.append(word)
            current_size += len(word) + 1
            
            if current_size > self.chunk_size:
                chunks.append(' '.join(current_chunk))
                current_chunk = []
                current_size = 0
        
        if current_chunk:
            chunks.append(' '.join(current_chunk))
        
        return chunks
    
    def _generate_simple_embeddings(self):
        """Simple TF-IDF style embeddings"""
        self.embeddings = []
        
        for doc in self.documents:
            # Create simple word frequency embedding
            words = doc['text'].lower().split()
            embedding = np.zeros(50)  # Smaller dimension
            
            for word in words[:20]:  # Limit words per document
                hash_val = abs(hash(word)) % 50
                embedding[hash_val] += 1
            
            # Normalize
            norm = np.linalg.norm(embedding)
            if norm > 0:
                embedding = embedding / norm
            
            self.embeddings.append(embedding)
    
    def _get_query_embedding(self, query: str) -> np.ndarray:
        """Simple query embedding"""
        words = query.lower().split()
        embedding = np.zeros(50)
        
        for word in words[:10]:  # Limit query words
            hash_val = abs(hash(word)) % 50
            embedding[hash_val] += 1
        
        norm = np.linalg.norm(embedding)
        if norm > 0:
            embedding = embedding / norm
        
        return embedding
    
    def retrieve(self, query: str, top_k: int = 2) -> List[Dict]:
        """Retrieve only top relevant chunks"""
        if not self.documents:
            return []
        
        query_embedding = self._get_query_embedding(query)
        
        # Compute similarities
        scores = []
        for i, doc_embedding in enumerate(self.embeddings):
            similarity = np.dot(query_embedding, doc_embedding)
            scores.append((i, similarity))
        
        # Sort and return top_k
        scores.sort(key=lambda x: x[1], reverse=True)
        
        results = []
        for i, score in scores[:top_k]:
            results.append({
                'text': self.documents[i]['text'],
                'score': float(score),
                'metadata': self.documents[i]['metadata']
            })
        
        return results
    
    def ask(self, question: str) -> Dict:
        """Answer with minimal context"""
        # Retrieve relevant chunks
        results = self.retrieve(question)
        
        if not results:
            return {
                'question': question,
                'answer': "I need more information. Please add documents.",
                'context': [],
                'sources': []
            }
        
        # Limit context to avoid memory issues
        context = results[0]['text']  # Only use top result
        if len(context) > 200:
            context = context[:200] + "..."
        
        # Generate response with TinyLlama (lightweight prompt)
        prompt = f"""Context: {context}

Question: {question}

Short answer:"""
        
        try:
            response = requests.post(
                self.ollama_url,
                json={
                    "model": self.model_name,
                    "prompt": prompt,
                    "stream": False,
                    "temperature": 0.5,  # Lower temperature = faster
                    "max_tokens": 50,    # Shorter responses
                    "options": {
                        "num_ctx": 512   # Smaller context window
                    }
                },
                timeout=30
            )
            
            if response.status_code == 200:
                answer = response.json()['response'].strip()
                # Truncate if too long
                if len(answer) > 200:
                    answer = answer[:200] + "..."
            else:
                answer = "Error generating response"
                
        except Exception as e:
            answer = f"Error: {str(e)[:100]}"
        
        return {
            'question': question,
            'answer': answer,
            'context': [context],
            'sources': [r.get('metadata', {}).get('source', 'Unknown') for r in results[:1]]
        }

# Test with sample
if __name__ == "__main__":
    print("πŸ§ͺ Testing Optimized RAG...")
    
    # Sample document
    doc = """
    Artificial Intelligence is the simulation of human intelligence processes by machines.
    Machine Learning enables systems to learn from data without explicit programming.
    Deep Learning uses neural networks with multiple layers for pattern recognition.
    Natural Language Processing helps computers understand human language.
    Computer Vision allows machines to interpret visual information.
    Reinforcement Learning trains agents through rewards and punishments.
    """
    
    bot = OptimizedRAGChatbot("tinyllama")
    bot.add_document(doc, {'source': 'AI Basics'})
    
    questions = [
        "What is AI?",
        "What is Machine Learning?",
        "What is Deep Learning?"
    ]
    
    for q in questions:
        print(f"\n❓ {q}")
        result = bot.ask(q)
        print(f"πŸ€– {result['answer']}")
        print("-" * 40)
    
    print("\nβœ… Test complete! Memory optimized.")