Fizu123 commited on
Commit
1c29d49
·
verified ·
1 Parent(s): 0fa8dbd

Upload 16 files

Browse files
.env ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GEMINI_API_KEY="AIzaSyDIhHusksgq0-NDavuzEXw-GuumFNQeQLc"
2
+ QDRANT_URL=https://9e93ef90-73bd-4888-9073-5d9306f63035.us-east4-0.gcp.cloud.qdrant.io
3
+ QDRANT_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.-tM0TkZqigtSpE-GD4pPYpPWLhx2FKtxuBAHcnNnp8I
4
+
5
+ # OpenRouter API Keys for rate limit rotation
6
+ OPENROUTER_API_KEY_1="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"
7
+ OPENROUTER_API_KEY_2="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"
8
+ OPENROUTER_API_KEY_3="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"
9
+
10
+ QDRANT_HOST=localhost
11
+ QDRANT_PORT=6333
12
+ QDRANT_COLLECTION_NAME=physical_ai_book
13
+
14
+ # Main Model Configuration
15
+ OPENROUTER_MODEL=nvidia/nemotron-nano-12b-v2-vl:free
16
+ OPENROUTER_API_URL=https://openrouter.ai/api/v1/chat/completions
17
+
18
+ # Translation Configuration
19
+ TRANSLATION_API_KEY=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
20
+ TRANSLATION_MODEL=nvidia/nemotron-nano-12b-v2-vl:free
app/__pycache__/main.cpython-311.pyc ADDED
Binary file (7.81 kB). View file
 
app/core/__pycache__/database.cpython-311.pyc ADDED
Binary file (3.28 kB). View file
 
app/core/database.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ QDRANT_URL = os.getenv("QDRANT_URL")
8
+ QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
9
+ COLLECTION_NAME = "physical_ai_textbook"
10
+
11
+ if not QDRANT_URL or not QDRANT_API_KEY:
12
+ raise ValueError("QDRANT_URL and QDRANT_API_KEY must be set in the .env file")
13
+
14
+ # Ensure URL doesn't end with slash + handle if user put "https://" or not
15
+ if not QDRANT_URL.startswith("http"):
16
+ QDRANT_URL = f"https://{QDRANT_URL}"
17
+ QDRANT_URL = QDRANT_URL.rstrip("/")
18
+
19
+ HEADERS = {
20
+ "api-key": QDRANT_API_KEY,
21
+ "Content-Type": "application/json"
22
+ }
23
+
24
+ def init_db():
25
+ """
26
+ Initializes the Qdrant collection via REST API.
27
+ """
28
+ # Check if collection exists
29
+ check_url = f"{QDRANT_URL}/collections/{COLLECTION_NAME}"
30
+ response = requests.get(check_url, headers=HEADERS)
31
+
32
+ if response.status_code == 200:
33
+ print(f"Collection {COLLECTION_NAME} already exists.")
34
+ else:
35
+ print(f"Creating collection: {COLLECTION_NAME}")
36
+ # Create collection
37
+ create_url = f"{QDRANT_URL}/collections/{COLLECTION_NAME}"
38
+ payload = {
39
+ "vectors": {
40
+ "size": 768,
41
+ "distance": "Cosine"
42
+ }
43
+ }
44
+ resp = requests.put(create_url, headers=HEADERS, json=payload)
45
+ if resp.status_code == 200:
46
+ print("Collection created successfully.")
47
+ else:
48
+ print(f"Error creating collection: {resp.text}")
49
+
50
+ def search_points(vector, limit=5):
51
+ url = f"{QDRANT_URL}/collections/{COLLECTION_NAME}/points/search"
52
+ payload = {
53
+ "vector": vector,
54
+ "limit": limit,
55
+ "with_payload": True
56
+ }
57
+ response = requests.post(url, headers=HEADERS, json=payload)
58
+ if response.status_code == 200:
59
+ return response.json().get("result", [])
60
+ else:
61
+ print(f"Search Error: {response.text}")
62
+ return []
63
+
64
+ def upsert_points(points):
65
+ url = f"{QDRANT_URL}/collections/{COLLECTION_NAME}/points?wait=true"
66
+ payload = {
67
+ "points": points
68
+ }
69
+ response = requests.put(url, headers=HEADERS, json=payload)
70
+ if response.status_code != 200:
71
+ print(f"Upsert Error: {response.text}")
app/main.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
3
+ from pydantic import BaseModel
4
+ from typing import Optional
5
+ import os
6
+ from app.core.database import init_db
7
+ from app.services.document_processor import process_and_index_documents
8
+
9
+ @asynccontextmanager
10
+ async def lifespan(app: FastAPI):
11
+ # Startup
12
+ init_db()
13
+ yield
14
+ # Shutdown (if needed)
15
+
16
+ app = FastAPI(title="Physical AI Textbook RAG Chatbot", version="1.0.0", lifespan=lifespan)
17
+
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["http://localhost:3000", "http://localhost:3001", "http://localhost:3002", "http://localhost:8000", "*"],
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+
28
+ class AskRequest(BaseModel):
29
+ query: str
30
+ selected_text: Optional[str] = None
31
+ personalization_context: Optional[str] = None
32
+ translate_urdu: bool = False
33
+
34
+ class AskResponse(BaseModel):
35
+ answer: str
36
+ chapter: str
37
+ section: str
38
+ personalization_applied: bool
39
+ translated_urdu: bool
40
+
41
+ class TranslateRequest(BaseModel):
42
+ text: str
43
+
44
+ class TranslateResponse(BaseModel):
45
+ translated_text: str
46
+
47
+ @app.get("/")
48
+ async def root():
49
+ return {"message": "Welcome to the Physical AI RAG Chatbot API. Visit /docs for documentation."}
50
+
51
+ @app.get("/health")
52
+ async def health_check():
53
+ return {"status": "ok", "service": "Physical AI RAG Chatbot"}
54
+
55
+ class PersonalizeRequest(BaseModel):
56
+ text: str
57
+ software_background: str
58
+ hardware_experience: str
59
+
60
+ class PersonalizeResponse(BaseModel):
61
+ personalized_text: str
62
+
63
+ @app.post("/ask", response_model=AskResponse)
64
+ async def ask_question(request: AskRequest):
65
+ from app.services.chat_service import process_user_query # Import here to avoid circular dep if any
66
+
67
+ result = await process_user_query(
68
+ query=request.query,
69
+ selected_text=request.selected_text,
70
+ personalization=request.personalization_context,
71
+ translate_urdu=request.translate_urdu
72
+ )
73
+
74
+ return AskResponse(
75
+ answer=result["answer"],
76
+ chapter=result.get("chapter", "N/A"),
77
+ section=result.get("section", "N/A"),
78
+ personalization_applied=result["personalization_applied"],
79
+ translated_urdu=result["translated_urdu"]
80
+ )
81
+
82
+ @app.post("/translate", response_model=TranslateResponse)
83
+ async def translate_content(request: TranslateRequest):
84
+ from app.services.chat_service import translate_text
85
+ print(f"DEBUG: Received translation request with text length: {len(request.text)}")
86
+ print(f"DEBUG: First 100 chars: {request.text[:100]}...")
87
+ translated = await translate_text(request.text)
88
+ print(f"DEBUG: Translation result length: {len(translated)}")
89
+ print(f"DEBUG: Translation result preview: {translated[:100]}...")
90
+ return TranslateResponse(translated_text=translated)
91
+
92
+ @app.post("/personalize", response_model=PersonalizeResponse)
93
+ async def personalize(request: PersonalizeRequest):
94
+ from app.services.chat_service import personalize_content
95
+ result = await personalize_content(
96
+ text=request.text,
97
+ software_bg=request.software_background,
98
+ hardware_exp=request.hardware_experience
99
+ )
100
+ return PersonalizeResponse(personalized_text=result)
101
+
102
+
103
+
104
+ @app.post("/reload-documents")
105
+ async def reload_documents(background_tasks: BackgroundTasks):
106
+ # Path to book docs relative to this file
107
+ # current file is app/main.py. Working dir when running is usually backend/
108
+ # Docs are at ../book-docs/docs
109
+
110
+ # Robust path finding
111
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # backend/
112
+ # Need to go up from backend -> rag-chatbot -> physical_ai_book -> book-docs
113
+ docs_path = os.path.join(base_dir, "..", "..", "book-docs", "docs")
114
+ docs_path = os.path.abspath(docs_path)
115
+
116
+ if not os.path.exists(docs_path):
117
+ raise HTTPException(status_code=404, detail=f"Docs directory not found at {docs_path}")
118
+
119
+ # Trigger processing in background
120
+ background_tasks.add_task(process_and_index_documents, docs_path)
121
+ return {"status": "Indexing started in background. The chatbot will be fully ready in a few minutes."}
122
+
123
+ if __name__ == "__main__":
124
+ import uvicorn
125
+ uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
app/services/__pycache__/chat_service.cpython-311.pyc ADDED
Binary file (14.7 kB). View file
 
app/services/__pycache__/document_processor.cpython-311.pyc ADDED
Binary file (6.37 kB). View file
 
app/services/chat_service.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import time
4
+ import re
5
+ from typing import Dict, Any, Optional, List
6
+ from app.core.database import search_points
7
+ from app.services.document_processor import get_embedding
8
+
9
+ # OpenRouter Configuration
10
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "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")
11
+ OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
12
+
13
+ # Primary chatbot model
14
+ OPENROUTER_MODEL = "nvidia/nemotron-nano-12b-v2-vl:free"
15
+
16
+ # Add simple in-memory cache for search results
17
+ search_cache = {}
18
+
19
+ def search_documents(query: str, limit: int = 5) -> str:
20
+ # Create cache key from query and limit
21
+ cache_key = f"{query[:100]}_{limit}"
22
+
23
+ # Check if result is already cached
24
+ if cache_key in search_cache:
25
+ return search_cache[cache_key]
26
+
27
+ try:
28
+ query_vector = get_embedding(query)
29
+ hits = search_points(query_vector, limit)
30
+ context_text = ""
31
+ for hit in hits:
32
+ payload = hit.get("payload", {})
33
+ source = payload.get("source", "Unknown")
34
+ text = payload.get("text", "")
35
+ context_text += f"\n[Source: {source}]\n{text}\n"
36
+
37
+ # Cache the result
38
+ search_cache[cache_key] = context_text
39
+
40
+ return context_text
41
+ except Exception as e:
42
+ print(f"SEARCH ERROR: {e}")
43
+ return ""
44
+
45
+ # Add cache for answer generation
46
+ answer_cache = {}
47
+
48
+ def generate_answer(query: str, context: str, personalization: Optional[str] = None, translate_urdu: bool = False) -> Dict[str, Any]:
49
+ # Create cache key
50
+ cache_key = f"{query[:50]}_{len(context)}_{bool(personalization)}_{translate_urdu}"
51
+
52
+ # Check if result is already cached
53
+ if cache_key in answer_cache:
54
+ print(f"DEBUG: Using cached answer for query: {query[:50]}...")
55
+ return answer_cache[cache_key]
56
+
57
+ print(f"DEBUG: Generating answer for query: {query[:50]}...")
58
+
59
+ system_instruction = """
60
+ You are an AI assistant for the 'Physical AI & Humanoid Robotics' textbook.
61
+
62
+ Identity Rules:
63
+ - If anyone asks 'Who created you?', 'Who is your developer?', or about your origin, ALWAYS answer: "Mujhe Fiza ❤️ ne banaya hai, jo ek nihayat hi talented programmer, Full-Stack Web Developer, aur AI Automation Specialist hain. Unho ne mujhe bari mehnat aur lagan se develop kiya hai taake main aapki behtar rehnumai kar sakoon."
64
+
65
+ Guidance Rules:
66
+ 1. Answer using ONLY the provided context from the book.
67
+ 2. Tell the user: "Main sirf is book mein se aapki guide karungi." if they ask generic questions.
68
+ 3. If the user asks in English, answer in English.
69
+ 4. If the user asks in Roman Urdu (like 'kya haal hai', 'robot kya hai'), answer in Roman Urdu.
70
+ 5. Mention the chapter/section if available.
71
+ 6. If not in context, say: "Ye information book mein nahi hai."
72
+ """
73
+
74
+ # CRITICAL: If Urdu Mode is ON, strictly use Urdu Script (Rasm-ul-Khat)
75
+ if translate_urdu:
76
+ system_instruction += "\n\nCRITICAL: Urdu Mode is ON. You MUST provide the final response in beautiful Urdu Script (Rasm-ul-Khat), not Roman Urdu."
77
+
78
+ if personalization:
79
+ system_instruction += f"\n\nUser Context: {personalization}"
80
+
81
+ payload = {
82
+ "model": OPENROUTER_MODEL,
83
+ "messages": [
84
+ {"role": "system", "content": system_instruction},
85
+ {"role": "user", "content": f"Context: {context}\n\nQuestion: {query}"}
86
+ ],
87
+ "temperature": 0.4 # Slightly lower for more factual technical answers
88
+ }
89
+
90
+ headers = {
91
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
92
+ "Content-Type": "application/json",
93
+ "HTTP-Referer": "http://localhost:8000",
94
+ "X-Title": "Physical AI Book"
95
+ }
96
+
97
+ # Standard retry for 429 or connection issues
98
+ for attempt in range(3): # Reduced attempts for faster response
99
+ try:
100
+ print(f"DEBUG: Attempting AI Request {attempt + 1}")
101
+ response = requests.post(OPENROUTER_API_URL, json=payload, headers=headers, timeout=30) # Reduced timeout
102
+
103
+ if response.status_code == 200:
104
+ data = response.json()
105
+ if "choices" in data and len(data["choices"]) > 0:
106
+ answer_text = data["choices"][0]["message"]["content"]
107
+ print("DEBUG: AI Success")
108
+
109
+ result = {
110
+ "answer": answer_text,
111
+ "chapter": "Textbook",
112
+ "section": "Relevant Section",
113
+ "personalization_applied": bool(personalization),
114
+ "translated_urdu": translate_urdu
115
+ }
116
+
117
+ # Cache the result
118
+ answer_cache[cache_key] = result
119
+ return result
120
+ else:
121
+ print(f"DEBUG: Unexpected Response Format: {data}")
122
+
123
+ elif response.status_code == 429:
124
+ print(f"DEBUG: 429 Rate Limit. Waiting {3 * (attempt + 1)}s...") # Reduced wait time
125
+ time.sleep(3 * (attempt + 1))
126
+ continue
127
+ else:
128
+ print(f"DEBUG: API Error {response.status_code}: {response.text}")
129
+ time.sleep(1) # Reduced wait time
130
+ except Exception as e:
131
+ print(f"DEBUG: Request Exception: {str(e)}")
132
+ time.sleep(1) # Reduced wait time
133
+ continue
134
+
135
+ return {
136
+ "answer": "🤖 The AI is currently busy or reaching its limit. Please try again in 10-15 seconds.",
137
+ "chapter": "N/A", "section": "N/A", "personalization_applied": False, "translated_urdu": False
138
+ }
139
+
140
+ async def process_user_query(query: str, selected_text: Optional[str], personalization: Optional[str], translate_urdu: bool):
141
+ # Greeting logic
142
+ query_lower = query.lower().strip()
143
+ greetings = ['hello', 'hi', 'salam', 'hey', 'aoa', 'hy', 'helo']
144
+ creator_queries = ['who created you', 'who is your creator', 'who developed you', 'aapko kis ne banaya', 'tumhe kis ne banaya', 'creator']
145
+
146
+ if any(q in query_lower for q in creator_queries):
147
+ return {
148
+ "answer": "**Fiza ❤️** is a highly skilled and talented professional with expertise in:\n\n• **Senior Full-Stack Web Developer** - Specialized in modern web technologies\n• **AI Automation Specialist** - Creating intelligent systems and chatbots\n• **Machine Learning Engineer** - Developing AI solutions for complex problems\n• **Software Architect** - Designing scalable and efficient systems\n\nShe has dedicated considerable time and effort to create me, ensuring I can provide you with the best guidance for learning Physical AI & Humanoid Robotics. Her passion for technology and education shines through in every interaction I have with you! 🌟",
149
+ "chapter": "Identity", "section": "Creator", "personalization_applied": False, "translated_urdu": False
150
+ }
151
+
152
+ if any(g == query_lower or query_lower.startswith(g + " ") for g in greetings) and not selected_text:
153
+ return {
154
+ "answer": "👋 **السلام علیکم!** Welcome to the Physical AI & Humanoid Robotics Learning Assistant. I'm here to guide you through this comprehensive textbook on robotics, AI, and humanoid systems. You can ask me anything about:\n\n• ROS 2 and robotic control systems\n• Gazebo & Unity simulation\n• NVIDIA Isaac platform\n• Vision-Language-Action (VLA) systems\n• Humanoid robotics fundamentals\n\nWhat would you like to explore today? 🚀",
155
+ "chapter": "Intro", "section": "Welcome", "personalization_applied": False, "translated_urdu": False
156
+ }
157
+
158
+ if selected_text:
159
+ context = f"Selected Text from Book: {selected_text}"
160
+ else:
161
+ context = search_documents(query)
162
+ if not context.strip():
163
+ return {
164
+ "answer": "Maazrat, ye information is book mein cover nahi hai. Please robotics ya Physical AI se mutaliq sawal poochein.",
165
+ "chapter": "N/A", "section": "N/A", "personalization_applied": False, "translated_urdu": False
166
+ }
167
+
168
+ return generate_answer(query, context, personalization, translate_urdu)
169
+
170
+ async def translate_text(text: str) -> str:
171
+ """
172
+ Instantly translate full chapter text
173
+ Uses the specified model and API key for translation
174
+ """
175
+ import time
176
+ start_time = time.time()
177
+
178
+ # Back to the model you preferred
179
+ model = "nvidia/nemotron-nano-12b-v2-vl:free"
180
+ api_key = os.getenv("TRANSLATION_API_KEY", "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")
181
+ api_url = "https://openrouter.ai/api/v1/chat/completions"
182
+
183
+ # SIGNIFICANTLY INCREASED LENGTH as requested
184
+ max_length = 15000 # Increased to 15000 for full chapters
185
+ text_to_translate = text[:max_length]
186
+
187
+ prompt = f"""
188
+ Translate the following technical textbook content into professional Urdu script (Rasm-ul-Khat).
189
+ Maintain HTML tags and formatting. Respond with ONLY the translated Urdu HTML.
190
+ Keep the same structure and formatting as the original.
191
+
192
+ Content to translate:
193
+ {text_to_translate}
194
+ """
195
+
196
+ payload = {
197
+ "model": model,
198
+ "messages": [{"role": "user", "content": prompt}],
199
+ "temperature": 0.2,
200
+ "max_tokens": 8000, # Increased tokens for longer output
201
+ }
202
+
203
+ try:
204
+ headers = {
205
+ "Authorization": f"Bearer {api_key}",
206
+ "Content-Type": "application/json",
207
+ "HTTP-Referer": "http://localhost:8000",
208
+ "X-Title": "Physical AI Book Translation"
209
+ }
210
+
211
+ response = requests.post(api_url, json=payload, headers=headers, timeout=60)
212
+
213
+ if response.status_code == 200:
214
+ data = response.json()
215
+ if "choices" in data and len(data["choices"]) > 0:
216
+ content = data["choices"][0]["message"]["content"]
217
+ result = content.replace("```html", "").replace("```", "").strip()
218
+
219
+ end_time = time.time()
220
+ print(f"DEBUG: Translation completed in {end_time - start_time:.2f} seconds")
221
+
222
+ return result
223
+ else:
224
+ return "Translation failed: No response content"
225
+ elif response.status_code == 429:
226
+ return "Maazrat, OpenRouter ki limit khatam ho chuki hai."
227
+ else:
228
+ return "Translation failed due to API error."
229
+
230
+ except Exception as e:
231
+ print(f"DEBUG: Translation error: {str(e)}")
232
+ return f"Translation failed: {str(e)}"
233
+ async def personalize_content(text: str, software_bg: str, hardware_exp: str) -> str:
234
+ # Use the same model and API key as translation for consistency
235
+ p_model = os.getenv("OPENROUTER_MODEL", "nvidia/nemotron-nano-12b-v2-vl:free")
236
+ api_key = os.getenv("OPENROUTER_API_KEY_1", "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")
237
+ api_url = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1/chat/completions")
238
+
239
+ prompt = f"""
240
+ Personalize the following technical textbook content for a student with this profile:
241
+ - Software Background: {software_bg}
242
+ - Hardware Experience: {hardware_exp}
243
+
244
+ Guidelines:
245
+ 1. If they are Beginners, simplify technical jargon and add relatable analogies.
246
+ 2. If they are Experts, keep it concise and focus on advanced integration/ROS nodes.
247
+ 3. Maintain the original HTML structure and tags.
248
+ 4. Keep the output in English (unless the input is Urdu).
249
+
250
+ Content to Personalize:
251
+ {text[:4000]}
252
+ """
253
+
254
+ payload = {
255
+ "model": p_model,
256
+ "messages": [{"role": "user", "content": prompt}],
257
+ "temperature": 0.5,
258
+ "max_tokens": 2000,
259
+ "top_p": 0.9
260
+ }
261
+
262
+ try:
263
+ headers = {
264
+ "Authorization": f"Bearer {api_key}",
265
+ "Content-Type": "application/json",
266
+ "HTTP-Referer": "http://localhost:8000",
267
+ "X-Title": "Physical AI Book Personalization"
268
+ }
269
+
270
+ response = requests.post(api_url, json=payload, headers=headers, timeout=45)
271
+ if response.status_code == 200:
272
+ content = response.json()["choices"][0]["message"]["content"]
273
+ return content.replace("```html", "").replace("```", "").strip()
274
+ elif response.status_code == 429:
275
+ print("DEBUG: Personalization rate limited")
276
+ return "Personalization is temporarily unavailable due to rate limits. Displaying standard content."
277
+ else:
278
+ print(f"Personalization API Error: {response.status_code}")
279
+ return "Personalization failed. Displaying standard content."
280
+
281
+ except requests.exceptions.Timeout:
282
+ print("Personalization request timed out")
283
+ return "Personalization is taking longer than expected. Displaying standard content."
284
+ except Exception as e:
285
+ print(f"Personalization Error: {e}")
286
+ return "Personalization failed. Displaying standard content."
app/services/document_processor.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import time
4
+ from typing import List
5
+ import requests
6
+ import uuid
7
+ import json
8
+ from app.core.database import upsert_points
9
+
10
+ # Configure Gemini
11
+ GOOGLE_API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
12
+
13
+ if not GOOGLE_API_KEY:
14
+ raise ValueError("GEMINI_API_KEY must be set in .env")
15
+
16
+ # Using Gemini 1.5 Flash for Embeddings (REST API)
17
+ # Official Endpoint: https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent
18
+ EMBEDDING_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key={GOOGLE_API_KEY}"
19
+
20
+ def get_embedding(text: str) -> List[float]:
21
+ """
22
+ Generates embedding using Gemini REST API with retry logic for rate limits.
23
+ """
24
+ payload = {
25
+ "model": "models/text-embedding-004",
26
+ "content": {
27
+ "parts": [{"text": text}]
28
+ }
29
+ }
30
+
31
+ # Retry logic with exponential backoff
32
+ max_retries = 3
33
+ retry_delay = 1
34
+
35
+ for attempt in range(max_retries):
36
+ try:
37
+ response = requests.post(EMBEDDING_API_URL, json=payload, headers={"Content-Type": "application/json"}, timeout=30)
38
+
39
+ if response.status_code == 200:
40
+ data = response.json()
41
+ return data["embedding"]["values"]
42
+
43
+ elif response.status_code == 429:
44
+ # Rate limit - retry with backoff
45
+ if attempt < max_retries - 1:
46
+ print(f"Embedding rate limit. Retrying in {retry_delay}s...")
47
+ time.sleep(retry_delay)
48
+ retry_delay *= 2
49
+ continue
50
+ else:
51
+ raise Exception("Rate limit exceeded after retries")
52
+
53
+ else:
54
+ print(f"Embedding Error ({response.status_code}): {response.text}")
55
+ raise Exception(f"Failed to generate embedding: {response.status_code}")
56
+
57
+ except requests.exceptions.Timeout:
58
+ if attempt < max_retries - 1:
59
+ print(f"Embedding timeout. Retrying in {retry_delay}s...")
60
+ time.sleep(retry_delay)
61
+ retry_delay *= 2
62
+ continue
63
+ else:
64
+ raise Exception("Embedding request timed out after retries")
65
+
66
+ except Exception as e:
67
+ if attempt < max_retries - 1 and "rate limit" in str(e).lower():
68
+ time.sleep(retry_delay)
69
+ retry_delay *= 2
70
+ continue
71
+ raise
72
+
73
+ def load_markdown_files(docs_path: str) -> List[dict]:
74
+ files = []
75
+ search_path = os.path.join(docs_path, "**/*.md")
76
+ for filepath in glob.glob(search_path, recursive=True):
77
+ with open(filepath, 'r', encoding='utf-8') as f:
78
+ content = f.read()
79
+ filename = os.path.basename(filepath)
80
+ files.append({
81
+ "content": content,
82
+ "source": filename,
83
+ "path": filepath
84
+ })
85
+ return files
86
+
87
+ def chunk_text(text: str, chunk_size: int = 2000, overlap: int = 100) -> List[str]:
88
+ chunks = []
89
+ start = 0
90
+ while start < len(text):
91
+ end = start + chunk_size
92
+ chunk = text[start:end]
93
+ chunks.append(chunk)
94
+ start += (chunk_size - overlap)
95
+ return chunks
96
+
97
+ def process_and_index_documents(docs_path: str):
98
+ print(f"Loading documents from: {docs_path}")
99
+ documents = load_markdown_files(docs_path)
100
+ print(f"Found {len(documents)} markdown files.")
101
+
102
+ points_batch = []
103
+
104
+ for doc in documents:
105
+ chunks = chunk_text(doc["content"])
106
+
107
+ for i, chunk in enumerate(chunks):
108
+ try:
109
+ embedding = get_embedding(chunk)
110
+
111
+ # Create Point Structure for Qdrant REST API
112
+ point = {
113
+ "id": str(uuid.uuid4()),
114
+ "vector": embedding,
115
+ "payload": {
116
+ "text": chunk,
117
+ "source": doc["source"],
118
+ "path": doc["path"],
119
+ "chunk_id": i
120
+ }
121
+ }
122
+ points_batch.append(point)
123
+
124
+ # Upload in batches of 50 to avoid big payloads
125
+ if len(points_batch) >= 50:
126
+ upsert_points(points_batch)
127
+ points_batch = []
128
+ print(".", end="", flush=True)
129
+
130
+ except Exception as e:
131
+ print(f"Error processing chunk in {doc['source']}: {e}")
132
+
133
+ # Upload remaining
134
+ if points_batch:
135
+ upsert_points(points_batch)
136
+
137
+ print("\nUpload complete!")
138
+ return {"status": "success"}
debug_db.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ QDRANT_URL = os.getenv("QDRANT_URL")
8
+ QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
9
+ COLLECTION_NAME = "physical_ai_textbook"
10
+
11
+ if not QDRANT_URL.startswith("http"):
12
+ QDRANT_URL = f"https://{QDRANT_URL}"
13
+ QDRANT_URL = QDRANT_URL.rstrip("/")
14
+
15
+ HEADERS = {
16
+ "api-key": QDRANT_API_KEY,
17
+ "Content-Type": "application/json"
18
+ }
19
+
20
+ def check_collection():
21
+ print(f"Checking collection: {COLLECTION_NAME} at {QDRANT_URL}")
22
+ url = f"{QDRANT_URL}/collections/{COLLECTION_NAME}"
23
+ response = requests.get(url, headers=HEADERS)
24
+
25
+ if response.status_code == 200:
26
+ data = response.json()
27
+ print("Collection Info:")
28
+ print(f"Status: {data.get('status')}")
29
+ print(f"Points Count: {data.get('result', {}).get('points_count', 'Unknown')}")
30
+ print(f"Vectors Count: {data.get('result', {}).get('vectors_count', 'Unknown')}")
31
+ else:
32
+ print(f"Error accessing collection: {response.status_code} - {response.text}")
33
+
34
+ def test_search(query_text="physical ai"):
35
+ print(f"\nTesting search for: '{query_text}'")
36
+ # We need to generate an embedding first, but we can't easily do that here without the full app setup.
37
+ # However, we can check if the collection *has* points first.
38
+ pass
39
+
40
+ if __name__ == "__main__":
41
+ check_collection()
debug_search.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.services.document_processor import get_embedding
2
+ from app.core.database import search_points
3
+ import sys
4
+
5
+ def debug_search(query):
6
+ print(f"--- Debugging Search for: '{query}' ---")
7
+
8
+ # 1. Generate Embedding
9
+ print("Generating embedding...")
10
+ try:
11
+ vector = get_embedding(query)
12
+ print("Embedding generated successfully.")
13
+ except Exception as e:
14
+ print(f"FAILED to generate embedding: {e}")
15
+ return
16
+
17
+ # 2. Search Qdrant
18
+ print("Searching Qdrant...")
19
+ results = search_points(vector, limit=3)
20
+
21
+ print(f"Found {len(results)} matches.")
22
+
23
+ if not results:
24
+ print("NO MATCHES FOUND. Check Qdrant connection or data.")
25
+ return
26
+
27
+ for i, hit in enumerate(results):
28
+ score = hit.get("score", "N/A")
29
+ payload = hit.get("payload", {})
30
+ source = payload.get("source", "Unknown")
31
+ text = payload.get("text", "")[:200] # Show first 200 chars
32
+
33
+ print(f"\nMatch #{i+1} (Score: {score}):")
34
+ print(f"Source: {source}")
35
+ print(f"Text Snippet: {text}...")
36
+
37
+ if __name__ == "__main__":
38
+ query = "What is Physical AI?"
39
+ if len(sys.argv) > 1:
40
+ query = sys.argv[1]
41
+ debug_search(query)
list_models.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ GOOGLE_API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
8
+
9
+ def list_models():
10
+ url = f"https://generativelanguage.googleapis.com/v1beta/models?key={GOOGLE_API_KEY}"
11
+ response = requests.get(url)
12
+ if response.status_code == 200:
13
+ models = response.json().get('models', [])
14
+ print("Available models:")
15
+ for m in models:
16
+ if 'generateContent' in m['supportedGenerationMethods']:
17
+ print(f" - {m['name']}")
18
+ else:
19
+ print(f"Error listing models: {response.text}")
20
+
21
+ if __name__ == "__main__":
22
+ list_models()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-dotenv
4
+ requests
5
+ pydantic
test_api.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+
4
+ # Load the API key from environment
5
+ api_key = os.getenv("TRANSLATION_API_KEY", "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")
6
+ model = os.getenv("TRANSLATION_MODEL", "allenai/olmo-3.1-32b-think:free")
7
+
8
+ print(f"Testing API key: {api_key[:10]}...") # Only show first 10 chars for security
9
+ print(f"Testing model: {model}")
10
+
11
+ headers = {
12
+ "Authorization": f"Bearer {api_key}",
13
+ "Content-Type": "application/json",
14
+ }
15
+
16
+ payload = {
17
+ "model": model,
18
+ "messages": [
19
+ {"role": "user", "content": "Hello, how are you?"}
20
+ ]
21
+ }
22
+
23
+ try:
24
+ response = requests.post(
25
+ "https://openrouter.ai/api/v1/chat/completions",
26
+ json=payload,
27
+ headers=headers,
28
+ timeout=30
29
+ )
30
+
31
+ print(f"Status Code: {response.status_code}")
32
+ print(f"Response: {response.text}")
33
+
34
+ if response.status_code == 200:
35
+ print("\n✅ API key is working correctly!")
36
+ else:
37
+ print(f"\n❌ API returned error: {response.status_code}")
38
+ print("This might indicate:")
39
+ print("1. API key has reached daily limit")
40
+ print("2. Model is not available")
41
+ print("3. API key is invalid")
42
+
43
+ except Exception as e:
44
+ print(f"\n❌ Error making API request: {str(e)}")
test_rag.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import time
3
+
4
+ BASE_URL = "http://127.0.0.1:8000"
5
+
6
+ def test_api():
7
+ print("--- Testing RAG Chatbot API ---")
8
+
9
+ # 1. Health Check
10
+ try:
11
+ response = requests.get(f"{BASE_URL}/health")
12
+ if response.status_code == 200:
13
+ print("[OK] Health Check Passed!")
14
+ else:
15
+ print(f"[FAIL] Health Check Failed: {response.text}")
16
+ return
17
+ except requests.exceptions.ConnectionError:
18
+ print("[FAIL] Could not connect to server. Is it running?")
19
+ return
20
+
21
+ # 2. Reload Documents (Indexing) - SKIPPING TO AVOID RE-TRIGGERING
22
+ print("\nSkipping Indexing for this test run...")
23
+ # try:
24
+ # response = requests.post(f"{BASE_URL}/reload-documents")
25
+ # if response.status_code == 200:
26
+ # print(f"[OK] Indexing Response: {response.json()}")
27
+ # else:
28
+ # print(f"[FAIL] Indexing Failed: {response.text}")
29
+ # except Exception as e:
30
+ # print(f"[FAIL] Error during indexing: {e}")
31
+
32
+ # 3. Ask a Question
33
+ print("\nAsking: 'What is Physical AI?'...")
34
+ payload = {
35
+ "query": "What is Physical AI?",
36
+ "translate_urdu": False
37
+ }
38
+
39
+ try:
40
+ start_time = time.time()
41
+ response = requests.post(f"{BASE_URL}/ask", json=payload)
42
+ duration = time.time() - start_time
43
+
44
+ if response.status_code == 200:
45
+ data = response.json()
46
+ print(f"[OK] Answer ({duration:.2f}s):")
47
+ print(f"Answer: {data['answer']}")
48
+ print(f"Sources: {data['chapter']} / {data['section']}")
49
+ else:
50
+ print(f"[FAIL] Ask Failed: {response.text}")
51
+
52
+ except Exception as e:
53
+ print(f"[FAIL] Error during asking: {e}")
54
+
55
+ if __name__ == "__main__":
56
+ test_api()
57
+
58
+
test_translation.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+
4
+ # Load the API key from environment
5
+ api_key = os.getenv("TRANSLATION_API_KEY", "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")
6
+ model = os.getenv("TRANSLATION_MODEL", "allenai/olmo-3.1-32b-think:free")
7
+
8
+ print(f"Testing translation with API key: {api_key[:10]}...")
9
+ print(f"Testing model: {model}")
10
+
11
+ headers = {
12
+ "Authorization": f"Bearer {api_key}",
13
+ "Content-Type": "application/json",
14
+ }
15
+
16
+ # Simple translation prompt
17
+ payload = {
18
+ "model": model,
19
+ "messages": [
20
+ {"role": "user", "content": "Translate this to Urdu: Hello, how are you?"}
21
+ ],
22
+ "temperature": 0.3
23
+ }
24
+
25
+ try:
26
+ response = requests.post(
27
+ "https://openrouter.ai/api/v1/chat/completions",
28
+ json=payload,
29
+ headers=headers,
30
+ timeout=30
31
+ )
32
+
33
+ print(f"Status Code: {response.status_code}")
34
+
35
+ if response.status_code == 200:
36
+ data = response.json()
37
+ if "choices" in data and len(data["choices"]) > 0:
38
+ content = data["choices"][0]["message"]["content"]
39
+ print(f"Translation Response: {content}")
40
+ print("\n✅ Translation API call successful!")
41
+ else:
42
+ print(f"\n❌ No choices in response: {data}")
43
+ else:
44
+ print(f"Response: {response.text}")
45
+ print(f"\n❌ Translation API returned error: {response.status_code}")
46
+
47
+ except Exception as e:
48
+ print(f"\n❌ Error making translation request: {str(e)}")