umar8902 commited on
Commit
8553e64
ยท
verified ยท
1 Parent(s): cf86927

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +379 -179
app.py CHANGED
@@ -6,13 +6,13 @@ import logging
6
  import io
7
  import json
8
  import requests
 
9
  from flask import Flask, request, jsonify, Response
10
  from flask_cors import CORS
11
  from datetime import datetime
12
  from dotenv import load_dotenv
13
  from llama_cpp import Llama
14
  from huggingface_hub import hf_hub_download
15
- from duckduckgo_search import DDGS
16
  import pdfplumber
17
  import docx
18
  from PIL import Image
@@ -31,11 +31,11 @@ app = Flask(__name__)
31
  CORS(app)
32
 
33
  # --- Configuration & Model Selection ---
34
- OLLAMA_URL = "http://localhost:11434" # Default for local/colab
35
  HF_FALLBACK_REPO = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF"
36
  HF_FALLBACK_FILE = "tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
37
 
38
- # Load Fallback Local Model (Hugging Face)
39
  llm_fallback = None
40
  try:
41
  logger.info("Loading Hugging Face fallback model...")
@@ -78,151 +78,251 @@ Maintain a helpful, modern, and production-ready chat style (like ChatGPT).
78
  Current Date: {date}
79
  """
80
 
81
- # --- Tool Brain Logic ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  def detect_tools(message):
83
- """
84
- Improved intent detection for tool routing.
85
- """
86
- message = message.lower()
87
  tools = []
88
-
89
- # Search Intent
90
  search_keywords = [
91
  "search", "latest", "news", "find out", "who is", "what is the current",
92
  "research", "browse", "today", "current", "facts", "data", "tell me about",
93
- "what happened", "weather", "stock", "price", "how many", "who won"
 
 
 
 
 
94
  ]
 
 
95
  if any(kw in message for kw in search_keywords):
96
  tools.append("search")
97
-
 
 
 
 
 
 
 
 
 
 
98
  # Image Intent
99
- if any(kw in message for kw in ["image", "draw", "visualize", "photo", "paint"]) or \
100
- ("create" in message and "image" in message) or \
101
- ("make" in message and "image" in message):
102
  tools.append("image")
103
-
 
 
 
 
 
104
  return tools
105
 
106
- # --- Internet Search Feature ---
107
- def web_search(query: str, max_results: int = 5) -> list:
108
- """Search the web using DuckDuckGo v7.5.0 syntax. No API key required."""
109
- try:
110
- from duckduckgo_search import DDGS
111
- with DDGS() as ddgs:
112
- results = list(ddgs.text(query, max_results=max_results))
113
- return results
114
- except Exception as e:
115
- logger.error(f"Search failed: {str(e)}")
116
- return [{"error": f"Search failed: {str(e)}"}]
117
 
118
- def format_results(data: dict) -> str:
119
  """Format search results with markdown citations."""
120
- if not data or not data.get("results"):
121
- return "No results found."
122
-
123
- results = data["results"]
124
- if isinstance(results[0], dict) and "error" in results[0]:
125
- return results[0]["error"]
126
-
127
- formatted = [f"Source: {data.get('engine', 'Unknown')}\n"]
 
 
128
  for i, r in enumerate(results, 1):
129
  title = r.get("title", "No title")
130
  url = r.get("href", "#")
131
- body = r.get("body", "No description")[:300]
132
- formatted.append(f"{i}. [{title}]({url})\n {body}")
133
- return "\n\n".join(formatted)
134
-
135
- def perform_search(query):
136
- try:
137
- # Clean query for better search results
138
- search_query = query.lower().replace('search for', '').replace('find out about', '').replace('/search', '').strip()
139
- logger.info(f"๐Ÿ” Searching for: {search_query}")
140
-
141
- results = web_search(search_query)
142
-
143
- if results and not (isinstance(results[0], dict) and "error" in results[0]):
144
- logger.info(f"โœ… Found {len(results)} results")
145
- return {"engine": "DuckDuckGo", "results": results}, None
146
- else:
147
- error_msg = results[0]["error"] if results and "error" in results[0] else "No results found."
148
- logger.warning(f"โŒ Search failed: {error_msg}")
149
- return None, error_msg
150
-
151
- except Exception as e:
152
- logger.error(f"โš ๏ธ Global Search Error: {str(e)}")
153
- return None, str(e)
154
-
155
- # --- Image Generation Feature ---
156
- @app.route('/api/image', methods=['POST'])
157
- def generate_image():
158
- data = request.json or {}
159
- prompt = data.get("prompt", "").strip()
160
- if not prompt:
161
- return jsonify({"error": "No prompt provided"}), 400
162
-
163
- try:
164
- seed = int(time.time())
165
- width = data.get("width", 1024)
166
- height = data.get("height", 1024)
167
- image_url = f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?width={width}&height={height}&seed={seed}&nologo=true"
168
-
169
- return jsonify({
170
- "status": "success",
171
- "image_url": image_url,
172
- "prompt": prompt
173
- })
174
- except Exception as e:
175
- logger.error(f"Image generation failed: {e}")
176
- return jsonify({"error": "Image generation failed"}), 500
177
-
178
- # --- File Analysis Feature ---
179
- @app.route('/api/analyze-file', methods=['POST'])
180
- def analyze_file():
181
- if 'file' not in request.files:
182
- return jsonify({"error": "No file uploaded"}), 400
183
-
184
- file = request.files['file']
185
- filename = file.filename
186
- ext = filename.split('.')[-1].lower()
187
-
188
- try:
189
- text_content = ""
190
- if ext == 'pdf':
191
- with pdfplumber.open(file) as pdf:
192
- text_content = "\n".join([page.extract_text() for page in pdf.pages if page.extract_text()])
193
- elif ext == 'docx':
194
- doc = docx.Document(file)
195
- text_content = "\n".join([para.text for para in doc.paragraphs])
196
- elif ext == 'txt':
197
- text_content = file.read().decode('utf-8', errors='ignore')
198
- elif ext in ['png', 'jpg', 'jpeg', 'webp']:
199
- img = Image.open(file)
200
- text_content = pytesseract.image_to_string(img)
201
- else:
202
- return jsonify({"error": f"Unsupported file type: {ext}"}), 400
203
-
204
- if not text_content.strip():
205
- return jsonify({"error": "Could not extract text from the file."}), 400
206
-
207
- truncated_text = text_content[:4000]
208
- return jsonify({
209
- "filename": filename,
210
- "content": truncated_text,
211
- "full_length": len(text_content),
212
- "status": "success"
213
- })
214
- except Exception as e:
215
- logger.error(f"File analysis failed: {e}")
216
- return jsonify({"error": f"Failed to process file: {str(e)}"}), 500
217
 
218
- # --- Time Bot Logic ---
219
- def get_time_response(message):
220
- message = message.lower()
221
- time_keywords = ["time", "date", "today", "now", "clock", "day"]
222
- if any(re.search(rf"\b{word}\b", message) for word in time_keywords):
223
- now = datetime.now()
224
- return f"๐Ÿ•’ The current time is **{now.strftime('%H:%M:%S')}**.\n๐Ÿ“… Today is **{now.strftime('%A, %Y-%m-%d')}**."
225
- return None
226
 
227
  @app.route('/')
228
  def home():
@@ -243,6 +343,37 @@ def login():
243
  data = request.json or {}
244
  return jsonify({"user": {"username": data.get("username", "Guest"), "messages_used": 0}}), 200
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  @app.route('/api/chat', methods=['POST'])
247
  def chat():
248
  data = request.json or {}
@@ -251,7 +382,7 @@ def chat():
251
  memory = data.get("memory", {})
252
  mode = data.get("mode", "instant")
253
  is_deep_research = data.get("deep_research", False)
254
-
255
  if not message:
256
  return jsonify({"error": "No message"}), 400
257
 
@@ -263,15 +394,20 @@ def chat():
263
  # 2. Tool Detection
264
  tools_needed = detect_tools(message)
265
  tool_context = ""
266
-
267
  if "search" in tools_needed:
268
- search_results, search_error = perform_search(message)
269
- if search_results:
270
- tool_context += f"\n\n[Web Search Results]:\n{format_results(search_results)}"
271
- elif search_error:
272
- tool_context += f"\n\n[Search Error]: {search_error}"
 
 
 
273
  else:
274
- tool_context += "\n\n[Search Results]: No relevant information found on the web."
 
 
275
 
276
  # 3. Dynamic Prompting
277
  current_date = datetime.now().strftime("%Y-%m-%d")
@@ -282,16 +418,19 @@ def chat():
282
  "research": "Act as a professional researcher. Provide deep, structured answers with logical sections.",
283
  "creative": "Act as a creative writer. Use evocative language and focus on ideation."
284
  }
285
-
286
  selected_mode_instruction = mode_instructions.get(mode, mode_instructions["instant"])
287
  dynamic_system_prompt = SYSTEM_PROMPT.format(date=current_date)
288
- dynamic_system_prompt += f"\n[Active Mode]: {mode.upper()} - {selected_mode_instruction}"
289
-
 
290
  if is_deep_research:
291
- dynamic_system_prompt += "\n[DEEP RESEARCH ACTIVE]: Use a multi-step reasoning approach. Analyze thoroughly."
 
292
 
293
  if memory:
294
- dynamic_system_prompt += f"\n[User Memory]: {json.dumps(memory)}"
 
295
 
296
  full_prompt = f"{message}{tool_context}"
297
 
@@ -305,7 +444,7 @@ def chat():
305
  ollama_role = "assistant" if role == "ai" else "user"
306
  messages.append({"role": ollama_role, "content": h.get('content', '')})
307
  messages.append({"role": "user", "content": full_prompt})
308
-
309
  response = requests.post(
310
  f"{OLLAMA_URL}/api/chat",
311
  json={
@@ -317,7 +456,7 @@ def chat():
317
  timeout=5,
318
  stream=True
319
  )
320
-
321
  if response.status_code == 200:
322
  for line in response.iter_lines():
323
  if line:
@@ -328,7 +467,8 @@ def chat():
328
  "source": "colab",
329
  "provider": "Ollama (Phi-3)",
330
  "tools_used": tools_needed
331
- }) + "\n"
 
332
  return
333
 
334
  except Exception as colab_err:
@@ -337,19 +477,26 @@ def chat():
337
  # Step B: Fallback to Local HF Model
338
  if llm_fallback:
339
  try:
340
- llama_prompt = f"<|system|>\n{dynamic_system_prompt}</s>\n"
 
 
341
  for h in history:
342
  role = h.get('role', 'user')
343
- llama_prompt += f"<|{role}|>\n{h.get('content', '')}</s>\n"
344
- llama_prompt += f"<|user|>\n{full_prompt}</s>\n<|assistant|>\n"
345
-
 
 
 
 
 
346
  output = llm_fallback(
347
  llama_prompt,
348
  max_tokens=512,
349
  stop=["</s>", "<|user|>"],
350
  stream=True
351
  )
352
-
353
  for chunk in output:
354
  token = chunk["choices"][0]["text"]
355
  if token:
@@ -358,48 +505,101 @@ def chat():
358
  "source": "hf",
359
  "provider": "Nexa Local (HF Fallback)",
360
  "tools_used": tools_needed
361
- }) + "\n"
 
362
  except Exception as hf_err:
363
  logger.error(f"HF Fallback failed: {hf_err}")
364
- yield json.dumps({"error": "All backends failed"}) + "\n"
 
365
  else:
366
- yield json.dumps({"error": "No backend available"}) + "\n"
 
367
 
368
  return Response(generate(), mimetype='application/x-ndjson')
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  @app.route('/api/status', methods=['GET'])
371
  def status_check():
372
  return jsonify({
373
  "status": "online",
374
  "backend": "Nexa AI Unified",
375
  "timestamp": time.ctime(),
376
- "fallback_loaded": llm_fallback is not None
 
377
  })
378
 
379
- @app.route('/api/search', methods=['POST'])
380
- def search_api():
381
- try:
382
- data = request.json or {}
383
- query = data.get("query", "").strip()
384
- if not query:
385
- return jsonify({"error": "No query provided"}), 400
386
-
387
- search_data, error = perform_search(query)
388
- if error and not search_data:
389
- return jsonify({"response": error, "results": []})
390
- if not search_data:
391
- return jsonify({"response": "No results found.", "results": []})
392
-
393
- return jsonify({
394
- "response": format_results(search_data),
395
- "results": search_data["results"],
396
- "engine": search_data["engine"],
397
- "status": "success"
398
- })
399
- except Exception as e:
400
- logger.error(f"Search API crashed: {e}")
401
- return jsonify({"error": "Search API internal error", "response": f"Internal error: {str(e)}"}), 500
402
-
403
  @app.errorhandler(500)
404
  def internal_error(error):
405
  return jsonify({"error": "Internal Server Error", "message": "An unexpected error occurred on the server."}), 500
 
6
  import io
7
  import json
8
  import requests
9
+ import urllib.parse
10
  from flask import Flask, request, jsonify, Response
11
  from flask_cors import CORS
12
  from datetime import datetime
13
  from dotenv import load_dotenv
14
  from llama_cpp import Llama
15
  from huggingface_hub import hf_hub_download
 
16
  import pdfplumber
17
  import docx
18
  from PIL import Image
 
31
  CORS(app)
32
 
33
  # --- Configuration & Model Selection ---
34
+ OLLAMA_URL = "http://localhost:11434"
35
  HF_FALLBACK_REPO = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF"
36
  HF_FALLBACK_FILE = "tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
37
 
38
+ # Load Fallback Local Model
39
  llm_fallback = None
40
  try:
41
  logger.info("Loading Hugging Face fallback model...")
 
78
  Current Date: {date}
79
  """
80
 
81
+ # ============ BULLETPROOF SEARCH SYSTEM ============
82
+
83
+ class SearchManager:
84
+ """Multi-engine search with automatic fallback. Never fails completely."""
85
+
86
+ def __init__(self):
87
+ self.last_search_time = 0
88
+ self.min_delay = 2 # seconds between searches
89
+ self.user_agents = [
90
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.0",
91
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.0",
92
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.0",
93
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0"
94
+ ]
95
+
96
+ def _get_headers(self):
97
+ return {"User-Agent": random.choice(self.user_agents)}
98
+
99
+ def _rate_limit(self):
100
+ """Ensure minimum delay between searches to avoid blocking."""
101
+ elapsed = time.time() - self.last_search_time
102
+ if elapsed < self.min_delay:
103
+ time.sleep(self.min_delay - elapsed)
104
+ self.last_search_time = time.time()
105
+
106
+ def search_duckduckgo(self, query: str, max_results: int = 5) -> list:
107
+ """Primary search: DuckDuckGo (fastest, most reliable)."""
108
+ try:
109
+ self._rate_limit()
110
+ from duckduckgo_search import DDGS
111
+
112
+ # Try with custom headers and timeout
113
+ with DDGS(headers=self._get_headers(), timeout=15) as ddgs:
114
+ results = list(ddgs.text(query, max_results=max_results))
115
+ if results:
116
+ logger.info(f"โœ… DuckDuckGo: {len(results)} results")
117
+ return [{"title": r["title"], "href": r["href"], "body": r["body"]}
118
+ for r in results]
119
+ except Exception as e:
120
+ logger.warning(f"DuckDuckGo failed: {e}")
121
+ return []
122
+
123
+ def search_bing(self, query: str, max_results: int = 5) -> list:
124
+ """Fallback 1: Bing HTML scraping."""
125
+ try:
126
+ self._rate_limit()
127
+ encoded_query = urllib.parse.quote(query)
128
+ url = f"https://www.bing.com/search?q={encoded_query}"
129
+
130
+ response = requests.get(url, headers=self._get_headers(), timeout=10)
131
+ response.raise_for_status()
132
+
133
+ # Extract results using regex patterns
134
+ html = response.text
135
+ results = []
136
+
137
+ # Pattern 1: Standard Bing results
138
+ import re
139
+
140
+ # Find all result blocks
141
+ result_blocks = re.findall(
142
+ r'<li class="b_algo"[^>]*>(.*?)</li>',
143
+ html,
144
+ re.DOTALL
145
+ )
146
+
147
+ for block in result_blocks[:max_results]:
148
+ # Extract title and URL
149
+ title_match = re.search(r'<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>', block, re.DOTALL)
150
+ if title_match:
151
+ href = title_match.group(1)
152
+ title = re.sub(r'<[^>]+>', '', title_match.group(2)).strip()
153
+
154
+ # Extract snippet
155
+ snippet_match = re.search(r'<p[^>]*>(.*?)</p>', block, re.DOTALL)
156
+ body = re.sub(r'<[^>]+>', '', snippet_match.group(1)).strip() if snippet_match else ""
157
+
158
+ # Clean up URL
159
+ if href.startswith('/'):
160
+ href = 'https://www.bing.com' + href
161
+
162
+ results.append({"title": title, "href": href, "body": body})
163
+
164
+ if results:
165
+ logger.info(f"โœ… Bing: {len(results)} results")
166
+ return results
167
+
168
+ except Exception as e:
169
+ logger.warning(f"Bing failed: {e}")
170
+ return []
171
+
172
+ def search_wikipedia(self, query: str, max_results: int = 3) -> list:
173
+ """Fallback 2: Wikipedia API (never blocks, always free)."""
174
+ try:
175
+ self._rate_limit()
176
+ url = "https://en.wikipedia.org/w/api.php"
177
+ params = {
178
+ "action": "query",
179
+ "list": "search",
180
+ "srsearch": query,
181
+ "format": "json",
182
+ "srlimit": max_results
183
+ }
184
+ response = requests.get(url, params=params, headers=self._get_headers(), timeout=10)
185
+ data = response.json()
186
+
187
+ results = []
188
+ for item in data.get("query", {}).get("search", []):
189
+ title = item["title"]
190
+ page_url = f"https://en.wikipedia.org/wiki/{title.replace(' ', '_')}"
191
+ snippet = re.sub(r'<span class="searchmatch">|</span>', '', item["snippet"])
192
+ results.append({"title": title, "href": page_url, "body": snippet})
193
+
194
+ if results:
195
+ logger.info(f"โœ… Wikipedia: {len(results)} results")
196
+ return results
197
+
198
+ except Exception as e:
199
+ logger.warning(f"Wikipedia failed: {e}")
200
+ return []
201
+
202
+ def search(self, query: str, max_results: int = 5) -> dict:
203
+ """
204
+ Master search function. Tries engines in order until one succeeds.
205
+ Returns: {"engine": str, "results": list, "success": bool}
206
+ """
207
+ if not query or not query.strip():
208
+ return {"engine": "None", "results": [], "success": False, "error": "Empty query"}
209
+
210
+ logger.info(f"๐Ÿ” Searching: {query}")
211
+
212
+ # Engine 1: DuckDuckGo
213
+ results = self.search_duckduckgo(query, max_results)
214
+ if results:
215
+ return {"engine": "DuckDuckGo", "results": results, "success": True, "error": None}
216
+
217
+ # Engine 2: Bing
218
+ results = self.search_bing(query, max_results)
219
+ if results:
220
+ return {"engine": "Bing", "results": results, "success": True, "error": None}
221
+
222
+ # Engine 3: Wikipedia
223
+ results = self.search_wikipedia(query, max_results)
224
+ if results:
225
+ return {"engine": "Wikipedia", "results": results, "success": True, "error": None}
226
+
227
+ # All failed
228
+ logger.error("โŒ All search engines failed")
229
+ return {
230
+ "engine": "None",
231
+ "results": [],
232
+ "success": False,
233
+ "error": "All search engines are temporarily unavailable. Please try again in a few minutes."
234
+ }
235
+
236
+ # Initialize search manager
237
+ search_manager = SearchManager()
238
+
239
+ # ============ TOOL DETECTION ============
240
+
241
  def detect_tools(message):
242
+ """Improved intent detection for tool routing."""
243
+ message = message.lower().strip()
 
 
244
  tools = []
245
+
246
+ # Search Intent - comprehensive keyword list
247
  search_keywords = [
248
  "search", "latest", "news", "find out", "who is", "what is the current",
249
  "research", "browse", "today", "current", "facts", "data", "tell me about",
250
+ "what happened", "weather", "stock", "price", "how many", "who won",
251
+ "temperature", "forecast", "update", "recent", "now", "happening",
252
+ "look up", "check", "get info", "information about", "details on",
253
+ "what's", "whats", "how's", "hows", "where is", "when did", "why did",
254
+ "did ", "does ", "is ", "are ", "was ", "were ", "will ", "can ", "could ",
255
+ "2024", "2025", "2026", "yesterday", "tomorrow", "this week", "this month"
256
  ]
257
+
258
+ # Check for search keywords
259
  if any(kw in message for kw in search_keywords):
260
  tools.append("search")
261
+
262
+ # Question patterns that likely need search (who, what, when, where, why, how)
263
+ question_starters = ["who ", "what ", "when ", "where ", "why ", "how "]
264
+ if any(message.startswith(kw) for kw in question_starters):
265
+ # Only add if not already added
266
+ if "search" not in tools:
267
+ # Check if it's a factual question (not personal/opinion)
268
+ personal_words = ["you", "your", "we", "our", "i ", "my ", "me "]
269
+ if not any(pw in message for pw in personal_words):
270
+ tools.append("search")
271
+
272
  # Image Intent
273
+ image_keywords = ["image", "draw", "visualize", "photo", "paint", "picture", "generate image"]
274
+ if any(kw in message for kw in image_keywords) or ("create" in message and "image" in message) or ("make" in message and "image" in message):
 
275
  tools.append("image")
276
+
277
+ # File analysis intent
278
+ file_keywords = ["pdf", "document", "file", "analyze file", "read file", "upload"]
279
+ if any(kw in message for kw in file_keywords):
280
+ tools.append("file")
281
+
282
  return tools
283
 
284
+ # ============ FORMATTING ============
 
 
 
 
 
 
 
 
 
 
285
 
286
+ def format_search_results(data: dict) -> str:
287
  """Format search results with markdown citations."""
288
+ if not data.get("success"):
289
+ error = data.get("error", "Search failed")
290
+ return f"โš ๏ธ {error}"
291
+
292
+ results = data.get("results", [])
293
+ if not results:
294
+ return "No results found for this query."
295
+
296
+ formatted = [f"๐Ÿ“ก Source: {data['engine']}
297
+ "]
298
  for i, r in enumerate(results, 1):
299
  title = r.get("title", "No title")
300
  url = r.get("href", "#")
301
+ body = r.get("body", "No description")
302
+ # Clean up body text
303
+ body = re.sub(r'\s+', ' ', body).strip()[:300]
304
+ formatted.append(f"{i}. [{title}]({url})
305
+ {body}")
306
+
307
+ return "
308
+
309
+ ".join(formatted)
310
+
311
+ def clean_search_query(query: str) -> str:
312
+ """Clean and optimize query for better search results."""
313
+ # Remove common prefixes
314
+ prefixes = [
315
+ "search for", "find out about", "look up", "check",
316
+ "tell me about", "what is", "who is", "where is",
317
+ "/search", "search:", "find:"
318
+ ]
319
+ query = query.lower().strip()
320
+ for prefix in prefixes:
321
+ if query.startswith(prefix):
322
+ query = query[len(prefix):].strip()
323
+ return query
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
+ # ============ API ROUTES ============
 
 
 
 
 
 
 
326
 
327
  @app.route('/')
328
  def home():
 
343
  data = request.json or {}
344
  return jsonify({"user": {"username": data.get("username", "Guest"), "messages_used": 0}}), 200
345
 
346
+ @app.route('/api/search', methods=['POST'])
347
+ def search_api():
348
+ """Dedicated search endpoint with bulletproof fallback."""
349
+ try:
350
+ data = request.json or {}
351
+ query = data.get("query", "").strip()
352
+ max_results = data.get("max_results", 5)
353
+
354
+ if not query:
355
+ return jsonify({"error": "No query provided", "success": False}), 400
356
+
357
+ # Perform search with fallback
358
+ search_data = search_manager.search(query, max_results)
359
+
360
+ return jsonify({
361
+ "success": search_data["success"],
362
+ "query": query,
363
+ "engine": search_data["engine"],
364
+ "results": search_data["results"],
365
+ "formatted": format_search_results(search_data),
366
+ "result_count": len(search_data["results"])
367
+ })
368
+
369
+ except Exception as e:
370
+ logger.error(f"Search API error: {e}")
371
+ return jsonify({
372
+ "error": "Search service temporarily unavailable",
373
+ "success": False,
374
+ "query": data.get("query", "") if 'data' in dir() else ""
375
+ }), 500
376
+
377
  @app.route('/api/chat', methods=['POST'])
378
  def chat():
379
  data = request.json or {}
 
382
  memory = data.get("memory", {})
383
  mode = data.get("mode", "instant")
384
  is_deep_research = data.get("deep_research", False)
385
+
386
  if not message:
387
  return jsonify({"error": "No message"}), 400
388
 
 
394
  # 2. Tool Detection
395
  tools_needed = detect_tools(message)
396
  tool_context = ""
397
+
398
  if "search" in tools_needed:
399
+ search_query = clean_search_query(message)
400
+ search_data = search_manager.search(search_query, max_results=5)
401
+
402
+ if search_data["success"]:
403
+ tool_context += f"
404
+
405
+ [Web Search Results from {search_data['engine']}]:
406
+ {format_search_results(search_data)}"
407
  else:
408
+ tool_context += f"
409
+
410
+ [Search Status]: {search_data.get('error', 'Search temporarily unavailable')}"
411
 
412
  # 3. Dynamic Prompting
413
  current_date = datetime.now().strftime("%Y-%m-%d")
 
418
  "research": "Act as a professional researcher. Provide deep, structured answers with logical sections.",
419
  "creative": "Act as a creative writer. Use evocative language and focus on ideation."
420
  }
421
+
422
  selected_mode_instruction = mode_instructions.get(mode, mode_instructions["instant"])
423
  dynamic_system_prompt = SYSTEM_PROMPT.format(date=current_date)
424
+ dynamic_system_prompt += f"
425
+ [Active Mode]: {mode.upper()} - {selected_mode_instruction}"
426
+
427
  if is_deep_research:
428
+ dynamic_system_prompt += "
429
+ [DEEP RESEARCH ACTIVE]: Use a multi-step reasoning approach. Analyze thoroughly."
430
 
431
  if memory:
432
+ dynamic_system_prompt += f"
433
+ [User Memory]: {json.dumps(memory)}"
434
 
435
  full_prompt = f"{message}{tool_context}"
436
 
 
444
  ollama_role = "assistant" if role == "ai" else "user"
445
  messages.append({"role": ollama_role, "content": h.get('content', '')})
446
  messages.append({"role": "user", "content": full_prompt})
447
+
448
  response = requests.post(
449
  f"{OLLAMA_URL}/api/chat",
450
  json={
 
456
  timeout=5,
457
  stream=True
458
  )
459
+
460
  if response.status_code == 200:
461
  for line in response.iter_lines():
462
  if line:
 
467
  "source": "colab",
468
  "provider": "Ollama (Phi-3)",
469
  "tools_used": tools_needed
470
+ }) + "
471
+ "
472
  return
473
 
474
  except Exception as colab_err:
 
477
  # Step B: Fallback to Local HF Model
478
  if llm_fallback:
479
  try:
480
+ llama_prompt = f"<|system|>
481
+ {dynamic_system_prompt}</s>
482
+ "
483
  for h in history:
484
  role = h.get('role', 'user')
485
+ llama_prompt += f"<|{role}|>
486
+ {h.get('content', '')}</s>
487
+ "
488
+ llama_prompt += f"<|user|>
489
+ {full_prompt}</s>
490
+ <|assistant|>
491
+ "
492
+
493
  output = llm_fallback(
494
  llama_prompt,
495
  max_tokens=512,
496
  stop=["</s>", "<|user|>"],
497
  stream=True
498
  )
499
+
500
  for chunk in output:
501
  token = chunk["choices"][0]["text"]
502
  if token:
 
505
  "source": "hf",
506
  "provider": "Nexa Local (HF Fallback)",
507
  "tools_used": tools_needed
508
+ }) + "
509
+ "
510
  except Exception as hf_err:
511
  logger.error(f"HF Fallback failed: {hf_err}")
512
+ yield json.dumps({"error": "All backends failed"}) + "
513
+ "
514
  else:
515
+ yield json.dumps({"error": "No backend available"}) + "
516
+ "
517
 
518
  return Response(generate(), mimetype='application/x-ndjson')
519
 
520
+ # --- Time Bot Logic ---
521
+ def get_time_response(message):
522
+ message = message.lower()
523
+ time_keywords = ["time", "date", "today", "now", "clock", "day"]
524
+ if any(re.search(rf"\b{word}\b", message) for word in time_keywords):
525
+ now = datetime.now()
526
+ return f"๐Ÿ•’ The current time is **{now.strftime('%H:%M:%S')}**.
527
+ ๐Ÿ“… Today is **{now.strftime('%A, %Y-%m-%d')}**."
528
+ return None
529
+
530
+ # --- Image Generation Feature ---
531
+ @app.route('/api/image', methods=['POST'])
532
+ def generate_image():
533
+ data = request.json or {}
534
+ prompt = data.get("prompt", "").strip()
535
+ if not prompt:
536
+ return jsonify({"error": "No prompt provided"}), 400
537
+
538
+ try:
539
+ seed = int(time.time())
540
+ width = data.get("width", 1024)
541
+ height = data.get("height", 1024)
542
+ image_url = f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?width={width}&height={height}&seed={seed}&nologo=true"
543
+
544
+ return jsonify({
545
+ "status": "success",
546
+ "image_url": image_url,
547
+ "prompt": prompt
548
+ })
549
+ except Exception as e:
550
+ logger.error(f"Image generation failed: {e}")
551
+ return jsonify({"error": "Image generation failed"}), 500
552
+
553
+ # --- File Analysis Feature ---
554
+ @app.route('/api/analyze-file', methods=['POST'])
555
+ def analyze_file():
556
+ if 'file' not in request.files:
557
+ return jsonify({"error": "No file uploaded"}), 400
558
+
559
+ file = request.files['file']
560
+ filename = file.filename
561
+ ext = filename.split('.')[-1].lower()
562
+
563
+ try:
564
+ text_content = ""
565
+ if ext == 'pdf':
566
+ with pdfplumber.open(file) as pdf:
567
+ text_content = "\n".join([page.extract_text() for page in pdf.pages if page.extract_text()])
568
+ elif ext == 'docx':
569
+ doc = docx.Document(file)
570
+ text_content = "\n".join([para.text for para in doc.paragraphs])
571
+ elif ext == 'txt':
572
+ text_content = file.read().decode('utf-8', errors='ignore')
573
+ elif ext in ['png', 'jpg', 'jpeg', 'webp']:
574
+ img = Image.open(file)
575
+ text_content = pytesseract.image_to_string(img)
576
+ else:
577
+ return jsonify({"error": f"Unsupported file type: {ext}"}), 400
578
+
579
+ if not text_content.strip():
580
+ return jsonify({"error": "Could not extract text from the file."}), 400
581
+
582
+ truncated_text = text_content[:4000]
583
+ return jsonify({
584
+ "filename": filename,
585
+ "content": truncated_text,
586
+ "full_length": len(text_content),
587
+ "status": "success"
588
+ })
589
+ except Exception as e:
590
+ logger.error(f"File analysis failed: {e}")
591
+ return jsonify({"error": f"Failed to process file: {str(e)}"}), 500
592
+
593
  @app.route('/api/status', methods=['GET'])
594
  def status_check():
595
  return jsonify({
596
  "status": "online",
597
  "backend": "Nexa AI Unified",
598
  "timestamp": time.ctime(),
599
+ "fallback_loaded": llm_fallback is not None,
600
+ "search_engines": ["DuckDuckGo", "Bing", "Wikipedia"]
601
  })
602
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  @app.errorhandler(500)
604
  def internal_error(error):
605
  return jsonify({"error": "Internal Server Error", "message": "An unexpected error occurred on the server."}), 500