mythaitts commited on
Commit
92e4fa4
Β·
verified Β·
1 Parent(s): e16af2c

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +392 -105
main.py CHANGED
@@ -1,16 +1,18 @@
1
  """
2
- AI-Powered Search API β€” Perplexity-style
3
  Built on SearXNG + LemonData (doubao-1.5-lite-32k)
4
  ---------------------------------------
5
  Endpoints:
6
- GET / - Welcome + API info
7
- GET /health - Health check
8
- GET /search - Raw SearXNG results
9
- GET /search/{engine} - Raw results from a specific engine
10
- GET /ai/search - AI-summarized search (any/specific engine)
11
- GET /ai/search/{engine} - AI search pinned to one engine
12
- POST /ai/ask - Q&A grounded in live web results
13
- GET /ai/news - AI news briefing
 
 
14
  """
15
 
16
  import os
@@ -21,7 +23,7 @@ from fastapi import FastAPI, Query, HTTPException, Depends, Path
21
  from fastapi.middleware.cors import CORSMiddleware
22
  from fastapi.security import APIKeyHeader
23
  from pydantic import BaseModel
24
- from typing import Optional, Literal
25
 
26
  # ─────────────────────────────────────────────
27
  # Config
@@ -36,17 +38,28 @@ AI_MODEL = "doubao-1.5-lite-32k"
36
  API_KEY = os.getenv("SEARCH_API_KEY", "")
37
  API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
38
 
 
39
  SUPPORTED_ENGINES = [
40
- "google", "bing", "duckduckgo", "wikipedia",
41
- "github", "youtube", "reddit", "twitter",
42
- "brave", "yahoo", "startpage"
43
  ]
44
 
 
 
 
 
 
 
 
 
 
 
45
  # ─────────────────────────────────────────────
46
  # Clients
47
  # ─────────────────────────────────────────────
48
 
49
- ai = OpenAI(api_key=LEMON_API_KEY, base_url=LEMON_BASE_URL)
50
 
51
  app = FastAPI(
52
  title="Synthex AI Search API",
@@ -57,15 +70,19 @@ Privacy-respecting search powered by **SearXNG** + **AI summarization**.
57
 
58
  ### Features
59
  - πŸ€– AI-powered summaries grounded in real-time web results
60
- - πŸ”Ž Choose any search engine: Google, Bing, DuckDuckGo, Reddit, YouTube & more
 
 
61
  - πŸ“° AI news briefings on any topic
62
  - ❓ Ask questions, get answers with sources
63
  - πŸ”’ No tracking, no profiling
64
 
65
  ### Engine shortcuts
66
- Use `/search/google`, `/search/bing`, `/ai/search/reddit` etc. to pin to a specific engine.
 
 
67
  """,
68
- version="2.0.0",
69
  docs_url="/docs",
70
  redoc_url="/redoc",
71
  )
@@ -84,7 +101,10 @@ app.add_middleware(
84
 
85
  async def verify_api_key(api_key: str = Depends(API_KEY_HEADER)):
86
  if API_KEY and api_key != API_KEY:
87
- raise HTTPException(status_code=403, detail="Invalid or missing API key. Pass it as X-API-Key header.")
 
 
 
88
  return api_key
89
 
90
  # ─────────────────────────────────────────────
@@ -96,15 +116,18 @@ class SearchResult(BaseModel):
96
  url: str
97
  snippet: str
98
  engine: Optional[str] = None
 
 
99
 
100
  class Latency(BaseModel):
101
- search_ms: float # time to fetch from SearXNG
102
- ai_ms: Optional[float] # time for AI summarization (None for raw endpoints)
103
- total_ms: float # end-to-end total
104
 
105
  class RawSearchResponse(BaseModel):
106
  query: str
107
  engine_used: str
 
108
  total_results: int
109
  results: list[SearchResult]
110
  latency: Latency
@@ -112,6 +135,7 @@ class RawSearchResponse(BaseModel):
112
  class AISearchResponse(BaseModel):
113
  query: str
114
  engine_used: str
 
115
  summary: str
116
  key_points: list[str]
117
  sources: list[SearchResult]
@@ -137,10 +161,33 @@ class NewsResponse(BaseModel):
137
  articles: list[SearchResult]
138
  latency: Latency
139
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  # ─────────────────────────────────────────────
141
  # Core helpers
142
  # ─────────────────────────────────────────────
143
 
 
 
 
 
 
 
 
 
 
 
 
144
  async def fetch_searxng(
145
  query: str,
146
  num_results: int = 5,
@@ -149,8 +196,8 @@ async def fetch_searxng(
149
  time_range: Optional[str] = None,
150
  engines: Optional[str] = None,
151
  ) -> tuple[list[SearchResult], float]:
152
- """Returns (results, search_ms)"""
153
- params = {
154
  "q": query,
155
  "format": "json",
156
  "language": language,
@@ -162,22 +209,24 @@ async def fetch_searxng(
162
  params["engines"] = engines
163
 
164
  t0 = time.perf_counter()
165
- async with httpx.AsyncClient(timeout=20) as client:
166
  try:
167
  resp = await client.get(f"{SEARXNG_BASE_URL}/search", params=params)
168
  resp.raise_for_status()
169
  except httpx.HTTPError as e:
170
- raise HTTPException(status_code=502, detail=f"SearXNG error: {str(e)}")
171
  search_ms = round((time.perf_counter() - t0) * 1000, 2)
172
 
173
  data = resp.json()
174
- results = []
175
  for item in data.get("results", [])[:num_results]:
176
  results.append(SearchResult(
177
  title=item.get("title", ""),
178
  url=item.get("url", ""),
179
- snippet=item.get("content", ""),
180
  engine=item.get("engine", engines or "mixed"),
 
 
181
  ))
182
  return results, search_ms
183
 
@@ -189,10 +238,10 @@ def build_context(results: list[SearchResult]) -> str:
189
  return "\n".join(lines)
190
 
191
 
192
- def ask_ai(prompt: str, max_tokens: int = 1500) -> tuple[str, float]:
193
- """Returns (text, ai_ms)"""
194
  t0 = time.perf_counter()
195
- response = ai.chat.completions.create(
196
  model=AI_MODEL,
197
  max_tokens=max_tokens,
198
  messages=[{"role": "user", "content": prompt}],
@@ -202,48 +251,67 @@ def ask_ai(prompt: str, max_tokens: int = 1500) -> tuple[str, float]:
202
 
203
 
204
  def parse_key_points(text: str) -> tuple[str, list[str]]:
205
- """Extract KEY POINTS section from AI response if present."""
206
- key_points = []
207
  summary = text
208
 
209
- if "KEY POINTS:" in text or "**Key Points" in text:
210
- parts = text.split("KEY POINTS:", 1) if "KEY POINTS:" in text else text.split("**Key Points", 1)
 
 
 
 
 
 
211
  summary = parts[0].strip()
212
  if len(parts) > 1:
213
  for line in parts[1].strip().split("\n"):
214
- line = line.strip().lstrip("-β€’*123456789. ")
215
  if line:
216
  key_points.append(line)
217
 
218
  return summary, key_points
219
 
220
  # ─────────────────────────────────────────────
221
- # Root
222
  # ─────────────────────────────────────────────
223
 
224
  @app.get("/", tags=["Info"])
225
  async def root():
226
  return {
227
  "name": "Synthex AI Search API",
228
- "version": "2.0.0",
229
  "status": "running",
230
  "docs": "/docs",
231
  "supported_engines": SUPPORTED_ENGINES,
 
232
  "endpoints": {
233
- "raw_search": "/search?q=query&engine=google",
234
- "engine_search": "/search/{engine}?q=query",
235
- "ai_search": "/ai/search?q=query&engine=bing",
236
- "ai_engine_search": "/ai/search/{engine}?q=query",
237
- "ai_ask": "POST /ai/ask",
238
- "ai_news": "/ai/news?topic=AI&time_range=day",
 
 
239
  }
240
  }
241
 
 
242
  @app.get("/health", tags=["Info"])
243
  async def health():
 
 
 
 
 
 
 
 
 
244
  return {
245
  "status": "ok",
246
- "searxng": SEARXNG_BASE_URL,
247
  "ai_model": AI_MODEL,
248
  "ai_provider": "LemonData (api.lemondata.cc)",
249
  "supported_engines": SUPPORTED_ENGINES,
@@ -253,201 +321,420 @@ async def health():
253
  # Raw Search Endpoints
254
  # ─────────────────────────────────────────────
255
 
256
- @app.get("/search", response_model=RawSearchResponse, tags=["Search"], dependencies=[Depends(verify_api_key)])
 
 
 
 
 
257
  async def raw_search(
258
  q: str = Query(..., description="Search query"),
259
- engine: str = Query("all", description=f"Engine to use: all, or one of {SUPPORTED_ENGINES}"),
260
  num_results: int = Query(5, ge=1, le=20),
261
  language: str = Query("en"),
 
262
  ):
263
- """Raw search results β€” no AI. Optionally pin to a specific engine."""
264
  t0 = time.perf_counter()
265
- results, search_ms = await fetch_searxng(q, num_results, language, engines=engine)
 
 
 
 
 
 
266
  return RawSearchResponse(
267
  query=q,
268
  engine_used=engine,
 
269
  total_results=len(results),
270
  results=results,
271
- latency=Latency(search_ms=search_ms, ai_ms=None, total_ms=round((time.perf_counter()-t0)*1000,2)),
 
 
 
272
  )
273
 
274
 
275
- @app.get("/search/{engine}", response_model=RawSearchResponse, tags=["Search"], dependencies=[Depends(verify_api_key)])
 
 
 
 
 
276
  async def raw_search_engine(
277
  engine: str = Path(..., description=f"Engine: {', '.join(SUPPORTED_ENGINES)}"),
278
  q: str = Query(..., description="Search query"),
279
  num_results: int = Query(5, ge=1, le=20),
280
  language: str = Query("en"),
 
281
  ):
282
- """Raw search pinned to a specific engine. e.g. /search/google?q=openai"""
283
  if engine not in SUPPORTED_ENGINES:
284
- raise HTTPException(status_code=400, detail=f"Unsupported engine '{engine}'. Choose from: {SUPPORTED_ENGINES}")
 
 
 
285
  t0 = time.perf_counter()
286
- results, search_ms = await fetch_searxng(q, num_results, language, engines=engine)
 
 
 
 
 
 
287
  return RawSearchResponse(
288
  query=q,
289
  engine_used=engine,
 
290
  total_results=len(results),
291
  results=results,
292
- latency=Latency(search_ms=search_ms, ai_ms=None, total_ms=round((time.perf_counter()-t0)*1000,2)),
 
 
 
293
  )
294
 
295
  # ─────────────────────────────────────────────
296
  # AI Search Endpoints
297
  # ─────────────────────────────────────────────
298
 
299
- @app.get("/ai/search", response_model=AISearchResponse, tags=["AI Search"], dependencies=[Depends(verify_api_key)])
 
 
 
 
 
300
  async def ai_search(
301
  q: str = Query(..., description="Search query"),
302
  engine: str = Query("all", description=f"Engine: all, or one of {SUPPORTED_ENGINES}"),
303
  num_results: int = Query(5, ge=1, le=10),
304
  language: str = Query("en"),
 
305
  ):
306
- """
307
- AI-enhanced search with deep summary, key points, and source citations.
308
- Optionally pin to a specific search engine.
309
- """
310
  t0 = time.perf_counter()
311
- results, search_ms = await fetch_searxng(q, num_results, language, engines=engine)
 
 
 
 
 
 
312
  if not results:
313
  raise HTTPException(status_code=404, detail="No results found.")
314
 
315
  context = build_context(results)
 
316
  raw, ai_ms = ask_ai(
317
- f"You are a Perplexity-style AI search assistant. A user searched for: '{q}'\n\n"
318
- f"Based on these search results, provide:\n"
319
- f"1. A concise summary (3-4 sentences) covering the most important information.\n"
320
- f"2. Then write 'KEY POINTS:' followed by 3-5 bullet points of the most critical facts.\n\n"
321
- f"Be specific, factual, and cite source numbers like [1], [2] inline.\n\n"
 
322
  f"Search Results:\n{context}",
323
- max_tokens=600,
324
  )
325
-
326
  summary, key_points = parse_key_points(raw)
327
- total_ms = round((time.perf_counter()-t0)*1000, 2)
328
  return AISearchResponse(
329
  query=q,
330
  engine_used=engine,
 
331
  summary=summary,
332
  key_points=key_points,
333
  sources=results,
334
- latency=Latency(search_ms=search_ms, ai_ms=ai_ms, total_ms=total_ms),
 
 
 
 
335
  )
336
 
337
 
338
- @app.get("/ai/search/{engine}", response_model=AISearchResponse, tags=["AI Search"], dependencies=[Depends(verify_api_key)])
 
 
 
 
 
339
  async def ai_search_engine(
340
  engine: str = Path(..., description=f"Engine: {', '.join(SUPPORTED_ENGINES)}"),
341
  q: str = Query(..., description="Search query"),
342
  num_results: int = Query(5, ge=1, le=10),
343
  language: str = Query("en"),
 
344
  ):
345
- """AI search pinned to a specific engine. e.g. /ai/search/reddit?q=best laptop 2025"""
346
  if engine not in SUPPORTED_ENGINES:
347
- raise HTTPException(status_code=400, detail=f"Unsupported engine '{engine}'. Choose from: {SUPPORTED_ENGINES}")
348
-
 
 
349
  t0 = time.perf_counter()
350
- results, search_ms = await fetch_searxng(q, num_results, language, engines=engine)
 
 
 
 
 
 
351
  if not results:
352
- raise HTTPException(status_code=404, detail=f"No results found from {engine}.")
353
 
354
  context = build_context(results)
355
  raw, ai_ms = ask_ai(
356
- f"You are a Perplexity-style AI search assistant using {engine.upper()} results. "
357
- f"A user searched for: '{q}'\n\n"
358
- f"Based on these {engine} search results, provide:\n"
359
  f"1. A thorough summary (3-4 sentences) with the most important information.\n"
360
- f"2. Then write 'KEY POINTS:' followed by 3-5 bullet points of critical facts.\n\n"
361
- f"Be specific, cite source numbers like [1], [2] inline.\n\n"
362
  f"Search Results:\n{context}",
363
- max_tokens=600,
364
  )
365
-
366
  summary, key_points = parse_key_points(raw)
367
- total_ms = round((time.perf_counter()-t0)*1000, 2)
368
  return AISearchResponse(
369
  query=q,
370
  engine_used=engine,
 
371
  summary=summary,
372
  key_points=key_points,
373
  sources=results,
374
- latency=Latency(search_ms=search_ms, ai_ms=ai_ms, total_ms=total_ms),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  )
376
 
377
  # ─────────────────────────────────────────────
378
  # AI Ask
379
  # ─────────────────────────────────────────────
380
 
381
- @app.post("/ai/ask", response_model=AskResponse, tags=["AI Search"], dependencies=[Depends(verify_api_key)])
 
 
 
 
 
382
  async def ai_ask(body: AskRequest):
383
  """
384
- Ask any question β€” AI searches the web and answers with sources.
385
- Optionally specify engine in the request body.
386
  """
387
  engine = body.engine or "all"
388
  t0 = time.perf_counter()
389
- results, search_ms = await fetch_searxng(body.question, body.num_results, body.language, engines=engine)
 
 
 
 
 
 
 
390
  if not results:
391
- raise HTTPException(status_code=404, detail="No results found.")
392
 
393
  context = build_context(results)
394
  answer, ai_ms = ask_ai(
395
- f"You are a helpful AI assistant. Answer this question using the search results below.\n"
396
  f"Be thorough, accurate, and helpful. Cite sources inline like [1], [2].\n"
397
- f"If results don't fully answer the question, clearly say so.\n\n"
398
  f"Question: {body.question}\n\n"
399
  f"Search Results:\n{context}",
400
- max_tokens=600,
401
  )
402
- total_ms = round((time.perf_counter()-t0)*1000, 2)
403
  return AskResponse(
404
  question=body.question,
405
  engine_used=engine,
406
  answer=answer,
407
  sources=results,
408
- latency=Latency(search_ms=search_ms, ai_ms=ai_ms, total_ms=total_ms),
 
 
 
 
409
  )
410
 
411
  # ─────────────────────────────────────────────
412
  # AI News
413
  # ─────────────────────────────────────────────
414
 
415
- @app.get("/ai/news", response_model=NewsResponse, tags=["AI Search"], dependencies=[Depends(verify_api_key)])
 
 
 
 
 
416
  async def ai_news(
417
- topic: str = Query(..., description="News topic"),
418
  time_range: str = Query("day", description="day | week | month"),
419
  engine: str = Query("all", description="Engine to use for news"),
420
  num_results: int = Query(5, ge=1, le=10),
421
  ):
422
- """AI news briefing β€” latest news on any topic, AI-summarized."""
423
  t0 = time.perf_counter()
424
  results, search_ms = await fetch_searxng(
425
  query=f"{topic} news",
426
  num_results=num_results,
427
  categories="news",
428
  time_range=time_range,
429
- engines=engine if engine != "all" else None,
430
  )
 
 
 
 
 
 
 
 
 
 
431
  if not results:
432
- results, search_ms = await fetch_searxng(f"{topic} latest news", num_results, engines=engine if engine != "all" else None)
433
 
434
  context = build_context(results)
435
  summary, ai_ms = ask_ai(
436
- f"You are a news briefing assistant. Summarize the latest news about '{topic}'.\n"
437
- f"Write 3-4 sentences covering the most important developments.\n"
438
- f"Be neutral, factual, and cite sources like [1], [2].\n\n"
439
- f"Articles:\n{context}"
 
 
440
  )
441
- total_ms = round((time.perf_counter()-t0)*1000, 2)
442
  return NewsResponse(
443
  topic=topic,
444
  engine_used=engine,
445
  summary=summary,
446
  articles=results,
447
- latency=Latency(search_ms=search_ms, ai_ms=ai_ms, total_ms=total_ms),
 
 
 
 
448
  )
449
 
450
 
 
 
 
 
451
  if __name__ == "__main__":
452
  import uvicorn
453
  uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
 
1
  """
2
+ Synthex AI Search API β€” Perplexity-style
3
  Built on SearXNG + LemonData (doubao-1.5-lite-32k)
4
  ---------------------------------------
5
  Endpoints:
6
+ GET / - Welcome + API info
7
+ GET /health - Health check
8
+ GET /search - Raw SearXNG results (any/specific engine)
9
+ GET /search/{engine} - Raw results from a specific engine
10
+ GET /ai/search - AI-summarized search (any/specific engine)
11
+ GET /ai/search/{engine} - AI search pinned to one engine
12
+ POST /ai/ask - Q&A grounded in live web results
13
+ GET /ai/news - AI news briefing
14
+ GET /ai/videos - AI-summarized YouTube/video search
15
+ GET /ai/code - AI-summarized GitHub code search
16
  """
17
 
18
  import os
 
23
  from fastapi.middleware.cors import CORSMiddleware
24
  from fastapi.security import APIKeyHeader
25
  from pydantic import BaseModel
26
+ from typing import Optional
27
 
28
  # ─────────────────────────────────────────────
29
  # Config
 
38
  API_KEY = os.getenv("SEARCH_API_KEY", "")
39
  API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
40
 
41
+ # All engines supported in settings.yml
42
  SUPPORTED_ENGINES = [
43
+ "google", "bing", "duckduckgo", "brave",
44
+ "wikipedia", "github", "github code",
45
+ "youtube", "reddit", "yahoo", "startpage", "dailymotion"
46
  ]
47
 
48
+ # Maps engine name β†’ correct SearXNG category
49
+ # If engine not in this map, defaults to "general"
50
+ ENGINE_CATEGORIES = {
51
+ "youtube": "videos",
52
+ "dailymotion": "videos",
53
+ "github code": "it",
54
+ "reddit": "social media",
55
+ "wikipedia": "general",
56
+ }
57
+
58
  # ─────────────────────────────────────────────
59
  # Clients
60
  # ─────────────────────────────────────────────
61
 
62
+ ai_client = OpenAI(api_key=LEMON_API_KEY, base_url=LEMON_BASE_URL)
63
 
64
  app = FastAPI(
65
  title="Synthex AI Search API",
 
70
 
71
  ### Features
72
  - πŸ€– AI-powered summaries grounded in real-time web results
73
+ - πŸ”Ž Multi-engine: Google, Bing, DuckDuckGo, Brave, Reddit, YouTube, GitHub & more
74
+ - πŸ“Ή Native YouTube video search with AI summaries
75
+ - πŸ’» GitHub code search with AI explanations
76
  - πŸ“° AI news briefings on any topic
77
  - ❓ Ask questions, get answers with sources
78
  - πŸ”’ No tracking, no profiling
79
 
80
  ### Engine shortcuts
81
+ Use `/search/google`, `/search/youtube`, `/ai/search/reddit` etc. to pin to a specific engine.
82
+ Use `/ai/videos` for YouTube-first video search.
83
+ Use `/ai/code` for GitHub code search.
84
  """,
85
+ version="3.0.0",
86
  docs_url="/docs",
87
  redoc_url="/redoc",
88
  )
 
101
 
102
  async def verify_api_key(api_key: str = Depends(API_KEY_HEADER)):
103
  if API_KEY and api_key != API_KEY:
104
+ raise HTTPException(
105
+ status_code=403,
106
+ detail="Invalid or missing API key. Pass it as X-API-Key header."
107
+ )
108
  return api_key
109
 
110
  # ─────────────────────────────────────────────
 
116
  url: str
117
  snippet: str
118
  engine: Optional[str] = None
119
+ thumbnail: Optional[str] = None # for video results
120
+ duration: Optional[str] = None # for video results
121
 
122
  class Latency(BaseModel):
123
+ search_ms: float
124
+ ai_ms: Optional[float] = None
125
+ total_ms: float
126
 
127
  class RawSearchResponse(BaseModel):
128
  query: str
129
  engine_used: str
130
+ category: str
131
  total_results: int
132
  results: list[SearchResult]
133
  latency: Latency
 
135
  class AISearchResponse(BaseModel):
136
  query: str
137
  engine_used: str
138
+ category: str
139
  summary: str
140
  key_points: list[str]
141
  sources: list[SearchResult]
 
161
  articles: list[SearchResult]
162
  latency: Latency
163
 
164
+ class VideoResponse(BaseModel):
165
+ query: str
166
+ summary: str
167
+ videos: list[SearchResult]
168
+ latency: Latency
169
+
170
+ class CodeResponse(BaseModel):
171
+ query: str
172
+ summary: str
173
+ results: list[SearchResult]
174
+ latency: Latency
175
+
176
  # ─────────────────────────────────────────────
177
  # Core helpers
178
  # ─────────────────────────────────────────────
179
 
180
+ def resolve_category(engines: Optional[str], override_category: Optional[str] = None) -> str:
181
+ """Pick the right SearXNG category for the given engine(s)."""
182
+ if override_category:
183
+ return override_category
184
+ if engines and engines != "all":
185
+ # Use the first engine in the list to determine category
186
+ first = engines.split(",")[0].strip().lower()
187
+ return ENGINE_CATEGORIES.get(first, "general")
188
+ return "general"
189
+
190
+
191
  async def fetch_searxng(
192
  query: str,
193
  num_results: int = 5,
 
196
  time_range: Optional[str] = None,
197
  engines: Optional[str] = None,
198
  ) -> tuple[list[SearchResult], float]:
199
+ """Fetch results from SearXNG. Returns (results, search_ms)."""
200
+ params: dict = {
201
  "q": query,
202
  "format": "json",
203
  "language": language,
 
209
  params["engines"] = engines
210
 
211
  t0 = time.perf_counter()
212
+ async with httpx.AsyncClient(timeout=25) as client:
213
  try:
214
  resp = await client.get(f"{SEARXNG_BASE_URL}/search", params=params)
215
  resp.raise_for_status()
216
  except httpx.HTTPError as e:
217
+ raise HTTPException(status_code=502, detail=f"SearXNG unreachable: {str(e)}")
218
  search_ms = round((time.perf_counter() - t0) * 1000, 2)
219
 
220
  data = resp.json()
221
+ results: list[SearchResult] = []
222
  for item in data.get("results", [])[:num_results]:
223
  results.append(SearchResult(
224
  title=item.get("title", ""),
225
  url=item.get("url", ""),
226
+ snippet=item.get("content", "") or item.get("description", ""),
227
  engine=item.get("engine", engines or "mixed"),
228
+ thumbnail=item.get("thumbnail") or item.get("img_src"),
229
+ duration=item.get("length") or item.get("duration"),
230
  ))
231
  return results, search_ms
232
 
 
238
  return "\n".join(lines)
239
 
240
 
241
+ def ask_ai(prompt: str, max_tokens: int = 800) -> tuple[str, float]:
242
+ """Call LemonData AI. Returns (text, ai_ms)."""
243
  t0 = time.perf_counter()
244
+ response = ai_client.chat.completions.create(
245
  model=AI_MODEL,
246
  max_tokens=max_tokens,
247
  messages=[{"role": "user", "content": prompt}],
 
251
 
252
 
253
  def parse_key_points(text: str) -> tuple[str, list[str]]:
254
+ """Extract KEY POINTS section from AI response."""
255
+ key_points: list[str] = []
256
  summary = text
257
 
258
+ marker = None
259
+ if "KEY POINTS:" in text:
260
+ marker = "KEY POINTS:"
261
+ elif "**Key Points" in text:
262
+ marker = "**Key Points"
263
+
264
+ if marker:
265
+ parts = text.split(marker, 1)
266
  summary = parts[0].strip()
267
  if len(parts) > 1:
268
  for line in parts[1].strip().split("\n"):
269
+ line = line.strip().lstrip("-β€’*123456789. ").strip("*")
270
  if line:
271
  key_points.append(line)
272
 
273
  return summary, key_points
274
 
275
  # ─────────────────────────────────────────────
276
+ # Root / Health
277
  # ─────────────────────────────────────────────
278
 
279
  @app.get("/", tags=["Info"])
280
  async def root():
281
  return {
282
  "name": "Synthex AI Search API",
283
+ "version": "3.0.0",
284
  "status": "running",
285
  "docs": "/docs",
286
  "supported_engines": SUPPORTED_ENGINES,
287
+ "engine_categories": ENGINE_CATEGORIES,
288
  "endpoints": {
289
+ "raw_search": "GET /search?q=query&engine=google",
290
+ "engine_search": "GET /search/{engine}?q=query",
291
+ "ai_search": "GET /ai/search?q=query&engine=brave",
292
+ "ai_engine_search": "GET /ai/search/{engine}?q=query",
293
+ "ai_videos": "GET /ai/videos?q=query",
294
+ "ai_code": "GET /ai/code?q=query",
295
+ "ai_ask": "POST /ai/ask",
296
+ "ai_news": "GET /ai/news?topic=AI&time_range=day",
297
  }
298
  }
299
 
300
+
301
  @app.get("/health", tags=["Info"])
302
  async def health():
303
+ # Ping SearXNG
304
+ searxng_status = "unreachable"
305
+ try:
306
+ async with httpx.AsyncClient(timeout=5) as client:
307
+ r = await client.get(f"{SEARXNG_BASE_URL}/")
308
+ searxng_status = "ok" if r.status_code == 200 else f"http_{r.status_code}"
309
+ except Exception:
310
+ pass
311
+
312
  return {
313
  "status": "ok",
314
+ "searxng": {"url": SEARXNG_BASE_URL, "status": searxng_status},
315
  "ai_model": AI_MODEL,
316
  "ai_provider": "LemonData (api.lemondata.cc)",
317
  "supported_engines": SUPPORTED_ENGINES,
 
321
  # Raw Search Endpoints
322
  # ─────────────────────────────────────────────
323
 
324
+ @app.get(
325
+ "/search",
326
+ response_model=RawSearchResponse,
327
+ tags=["Raw Search"],
328
+ dependencies=[Depends(verify_api_key)],
329
+ )
330
  async def raw_search(
331
  q: str = Query(..., description="Search query"),
332
+ engine: str = Query("all", description=f"Engine: all, or one of {SUPPORTED_ENGINES}"),
333
  num_results: int = Query(5, ge=1, le=20),
334
  language: str = Query("en"),
335
+ time_range: Optional[str] = Query(None, description="day | week | month | year"),
336
  ):
337
+ """Raw search results β€” no AI. Auto-selects correct category per engine."""
338
  t0 = time.perf_counter()
339
+ category = resolve_category(engine)
340
+ results, search_ms = await fetch_searxng(
341
+ q, num_results, language,
342
+ categories=category,
343
+ time_range=time_range,
344
+ engines=None if engine == "all" else engine,
345
+ )
346
  return RawSearchResponse(
347
  query=q,
348
  engine_used=engine,
349
+ category=category,
350
  total_results=len(results),
351
  results=results,
352
+ latency=Latency(
353
+ search_ms=search_ms,
354
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
355
+ ),
356
  )
357
 
358
 
359
+ @app.get(
360
+ "/search/{engine}",
361
+ response_model=RawSearchResponse,
362
+ tags=["Raw Search"],
363
+ dependencies=[Depends(verify_api_key)],
364
+ )
365
  async def raw_search_engine(
366
  engine: str = Path(..., description=f"Engine: {', '.join(SUPPORTED_ENGINES)}"),
367
  q: str = Query(..., description="Search query"),
368
  num_results: int = Query(5, ge=1, le=20),
369
  language: str = Query("en"),
370
+ time_range: Optional[str] = Query(None, description="day | week | month | year"),
371
  ):
372
+ """Raw search pinned to a specific engine. e.g. /search/youtube?q=python tutorial"""
373
  if engine not in SUPPORTED_ENGINES:
374
+ raise HTTPException(
375
+ status_code=400,
376
+ detail=f"Unsupported engine '{engine}'. Supported: {SUPPORTED_ENGINES}"
377
+ )
378
  t0 = time.perf_counter()
379
+ category = resolve_category(engine)
380
+ results, search_ms = await fetch_searxng(
381
+ q, num_results, language,
382
+ categories=category,
383
+ time_range=time_range,
384
+ engines=engine,
385
+ )
386
  return RawSearchResponse(
387
  query=q,
388
  engine_used=engine,
389
+ category=category,
390
  total_results=len(results),
391
  results=results,
392
+ latency=Latency(
393
+ search_ms=search_ms,
394
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
395
+ ),
396
  )
397
 
398
  # ─────────────────────────────────────────────
399
  # AI Search Endpoints
400
  # ─────────────────────────────────────────────
401
 
402
+ @app.get(
403
+ "/ai/search",
404
+ response_model=AISearchResponse,
405
+ tags=["AI Search"],
406
+ dependencies=[Depends(verify_api_key)],
407
+ )
408
  async def ai_search(
409
  q: str = Query(..., description="Search query"),
410
  engine: str = Query("all", description=f"Engine: all, or one of {SUPPORTED_ENGINES}"),
411
  num_results: int = Query(5, ge=1, le=10),
412
  language: str = Query("en"),
413
+ time_range: Optional[str] = Query(None, description="day | week | month | year"),
414
  ):
415
+ """AI-enhanced search: deep summary + key points + sources. Auto-routes engine category."""
 
 
 
416
  t0 = time.perf_counter()
417
+ category = resolve_category(engine)
418
+ results, search_ms = await fetch_searxng(
419
+ q, num_results, language,
420
+ categories=category,
421
+ time_range=time_range,
422
+ engines=None if engine == "all" else engine,
423
+ )
424
  if not results:
425
  raise HTTPException(status_code=404, detail="No results found.")
426
 
427
  context = build_context(results)
428
+ engine_label = engine.upper() if engine != "all" else "web"
429
  raw, ai_ms = ask_ai(
430
+ f"You are a Perplexity-style AI search assistant using {engine_label} results.\n"
431
+ f"User searched: '{q}'\n\n"
432
+ f"Provide:\n"
433
+ f"1. A concise summary (3-4 sentences) with the most important information.\n"
434
+ f"2. Then write exactly 'KEY POINTS:' on a new line, followed by 3-5 bullet points.\n\n"
435
+ f"Be specific and factual. Cite source numbers like [1], [2] inline.\n\n"
436
  f"Search Results:\n{context}",
 
437
  )
 
438
  summary, key_points = parse_key_points(raw)
 
439
  return AISearchResponse(
440
  query=q,
441
  engine_used=engine,
442
+ category=category,
443
  summary=summary,
444
  key_points=key_points,
445
  sources=results,
446
+ latency=Latency(
447
+ search_ms=search_ms,
448
+ ai_ms=ai_ms,
449
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
450
+ ),
451
  )
452
 
453
 
454
+ @app.get(
455
+ "/ai/search/{engine}",
456
+ response_model=AISearchResponse,
457
+ tags=["AI Search"],
458
+ dependencies=[Depends(verify_api_key)],
459
+ )
460
  async def ai_search_engine(
461
  engine: str = Path(..., description=f"Engine: {', '.join(SUPPORTED_ENGINES)}"),
462
  q: str = Query(..., description="Search query"),
463
  num_results: int = Query(5, ge=1, le=10),
464
  language: str = Query("en"),
465
+ time_range: Optional[str] = Query(None, description="day | week | month | year"),
466
  ):
467
+ """AI search pinned to one engine. e.g. /ai/search/brave?q=best python frameworks"""
468
  if engine not in SUPPORTED_ENGINES:
469
+ raise HTTPException(
470
+ status_code=400,
471
+ detail=f"Unsupported engine '{engine}'. Supported: {SUPPORTED_ENGINES}"
472
+ )
473
  t0 = time.perf_counter()
474
+ category = resolve_category(engine)
475
+ results, search_ms = await fetch_searxng(
476
+ q, num_results, language,
477
+ categories=category,
478
+ time_range=time_range,
479
+ engines=engine,
480
+ )
481
  if not results:
482
+ raise HTTPException(status_code=404, detail=f"No results from {engine}.")
483
 
484
  context = build_context(results)
485
  raw, ai_ms = ask_ai(
486
+ f"You are a Perplexity-style AI search assistant using {engine.upper()} results.\n"
487
+ f"User searched: '{q}'\n\n"
488
+ f"Provide:\n"
489
  f"1. A thorough summary (3-4 sentences) with the most important information.\n"
490
+ f"2. Then write exactly 'KEY POINTS:' on a new line, followed by 3-5 bullet points.\n\n"
491
+ f"Be specific and factual. Cite source numbers like [1], [2] inline.\n\n"
492
  f"Search Results:\n{context}",
 
493
  )
 
494
  summary, key_points = parse_key_points(raw)
 
495
  return AISearchResponse(
496
  query=q,
497
  engine_used=engine,
498
+ category=category,
499
  summary=summary,
500
  key_points=key_points,
501
  sources=results,
502
+ latency=Latency(
503
+ search_ms=search_ms,
504
+ ai_ms=ai_ms,
505
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
506
+ ),
507
+ )
508
+
509
+ # ─────────────────────────────────────────────
510
+ # AI Videos (YouTube-first)
511
+ # ─────────────────────────────────────────────
512
+
513
+ @app.get(
514
+ "/ai/videos",
515
+ response_model=VideoResponse,
516
+ tags=["AI Search"],
517
+ dependencies=[Depends(verify_api_key)],
518
+ )
519
+ async def ai_videos(
520
+ q: str = Query(..., description="Video search query"),
521
+ num_results: int = Query(5, ge=1, le=10),
522
+ time_range: Optional[str] = Query(None, description="day | week | month | year"),
523
+ ):
524
+ """
525
+ YouTube-first video search with AI summary.
526
+ Returns video titles, URLs, thumbnails, and durations where available.
527
+ """
528
+ t0 = time.perf_counter()
529
+
530
+ # Try YouTube first
531
+ results, search_ms = await fetch_searxng(
532
+ q, num_results,
533
+ categories="videos",
534
+ time_range=time_range,
535
+ engines="youtube",
536
+ )
537
+
538
+ # Fallback: search all video engines if YouTube returned nothing
539
+ if not results:
540
+ results, search_ms = await fetch_searxng(
541
+ q, num_results,
542
+ categories="videos",
543
+ time_range=time_range,
544
+ )
545
+
546
+ if not results:
547
+ raise HTTPException(status_code=404, detail="No video results found.")
548
+
549
+ context = build_context(results)
550
+ summary, ai_ms = ask_ai(
551
+ f"You are a helpful video search assistant.\n"
552
+ f"The user searched for videos about: '{q}'\n\n"
553
+ f"Based on these YouTube/video results, write a 2-3 sentence summary of what these videos cover "
554
+ f"and which ones look most useful. Mention video titles by name.\n\n"
555
+ f"Video Results:\n{context}",
556
+ max_tokens=400,
557
+ )
558
+ return VideoResponse(
559
+ query=q,
560
+ summary=summary,
561
+ videos=results,
562
+ latency=Latency(
563
+ search_ms=search_ms,
564
+ ai_ms=ai_ms,
565
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
566
+ ),
567
+ )
568
+
569
+ # ─────────────────────────────────────────────
570
+ # AI Code Search (GitHub-first)
571
+ # ─────────────────────────────────────────────
572
+
573
+ @app.get(
574
+ "/ai/code",
575
+ response_model=CodeResponse,
576
+ tags=["AI Search"],
577
+ dependencies=[Depends(verify_api_key)],
578
+ )
579
+ async def ai_code(
580
+ q: str = Query(..., description="Code / repo search query"),
581
+ num_results: int = Query(5, ge=1, le=10),
582
+ ):
583
+ """
584
+ GitHub Code search with AI explanation.
585
+ Great for finding repos, code snippets, and open-source projects.
586
+ """
587
+ t0 = time.perf_counter()
588
+ results, search_ms = await fetch_searxng(
589
+ q, num_results,
590
+ categories="it",
591
+ engines="github code",
592
+ )
593
+
594
+ # Fallback to github engine
595
+ if not results:
596
+ results, search_ms = await fetch_searxng(
597
+ q, num_results,
598
+ categories="it",
599
+ engines="github",
600
+ )
601
+
602
+ if not results:
603
+ raise HTTPException(status_code=404, detail="No code results found.")
604
+
605
+ context = build_context(results)
606
+ summary, ai_ms = ask_ai(
607
+ f"You are a developer-focused AI assistant.\n"
608
+ f"The user searched GitHub for: '{q}'\n\n"
609
+ f"Based on these GitHub results, write a 2-3 sentence summary covering what repos/code "
610
+ f"are available and which look most relevant. Mention repo names specifically.\n\n"
611
+ f"GitHub Results:\n{context}",
612
+ max_tokens=400,
613
+ )
614
+ return CodeResponse(
615
+ query=q,
616
+ summary=summary,
617
+ results=results,
618
+ latency=Latency(
619
+ search_ms=search_ms,
620
+ ai_ms=ai_ms,
621
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
622
+ ),
623
  )
624
 
625
  # ─────────────────────────────────────────────
626
  # AI Ask
627
  # ─────────────────────────────────────────────
628
 
629
+ @app.post(
630
+ "/ai/ask",
631
+ response_model=AskResponse,
632
+ tags=["AI Search"],
633
+ dependencies=[Depends(verify_api_key)],
634
+ )
635
  async def ai_ask(body: AskRequest):
636
  """
637
+ Ask any question β€” AI searches the web and answers with cited sources.
638
+ Specify engine in body to pin to a specific search provider.
639
  """
640
  engine = body.engine or "all"
641
  t0 = time.perf_counter()
642
+ category = resolve_category(engine)
643
+ results, search_ms = await fetch_searxng(
644
+ body.question,
645
+ body.num_results or 5,
646
+ body.language or "en",
647
+ categories=category,
648
+ engines=None if engine == "all" else engine,
649
+ )
650
  if not results:
651
+ raise HTTPException(status_code=404, detail="No results found for this question.")
652
 
653
  context = build_context(results)
654
  answer, ai_ms = ask_ai(
655
+ f"You are a helpful AI assistant. Answer the question below using the web search results provided.\n"
656
  f"Be thorough, accurate, and helpful. Cite sources inline like [1], [2].\n"
657
+ f"If results don't fully answer the question, clearly say what is and isn't covered.\n\n"
658
  f"Question: {body.question}\n\n"
659
  f"Search Results:\n{context}",
660
+ max_tokens=800,
661
  )
 
662
  return AskResponse(
663
  question=body.question,
664
  engine_used=engine,
665
  answer=answer,
666
  sources=results,
667
+ latency=Latency(
668
+ search_ms=search_ms,
669
+ ai_ms=ai_ms,
670
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
671
+ ),
672
  )
673
 
674
  # ─────────────────────────────────────────────
675
  # AI News
676
  # ─────────────────────────────────────────────
677
 
678
+ @app.get(
679
+ "/ai/news",
680
+ response_model=NewsResponse,
681
+ tags=["AI Search"],
682
+ dependencies=[Depends(verify_api_key)],
683
+ )
684
  async def ai_news(
685
+ topic: str = Query(..., description="News topic e.g. AI, crypto, sports"),
686
  time_range: str = Query("day", description="day | week | month"),
687
  engine: str = Query("all", description="Engine to use for news"),
688
  num_results: int = Query(5, ge=1, le=10),
689
  ):
690
+ """AI news briefing β€” latest news on any topic, AI-summarized with sources."""
691
  t0 = time.perf_counter()
692
  results, search_ms = await fetch_searxng(
693
  query=f"{topic} news",
694
  num_results=num_results,
695
  categories="news",
696
  time_range=time_range,
697
+ engines=None if engine == "all" else engine,
698
  )
699
+
700
+ # Fallback if news category returns nothing
701
+ if not results:
702
+ results, search_ms = await fetch_searxng(
703
+ f"{topic} latest news",
704
+ num_results,
705
+ categories="general",
706
+ engines=None if engine == "all" else engine,
707
+ )
708
+
709
  if not results:
710
+ raise HTTPException(status_code=404, detail=f"No news found for topic: {topic}")
711
 
712
  context = build_context(results)
713
  summary, ai_ms = ask_ai(
714
+ f"You are a neutral news briefing assistant.\n"
715
+ f"Summarize the latest news about '{topic}' in 3-4 sentences.\n"
716
+ f"Cover the most important developments. Be factual and balanced.\n"
717
+ f"Cite sources inline like [1], [2].\n\n"
718
+ f"Articles:\n{context}",
719
+ max_tokens=500,
720
  )
 
721
  return NewsResponse(
722
  topic=topic,
723
  engine_used=engine,
724
  summary=summary,
725
  articles=results,
726
+ latency=Latency(
727
+ search_ms=search_ms,
728
+ ai_ms=ai_ms,
729
+ total_ms=round((time.perf_counter() - t0) * 1000, 2)
730
+ ),
731
  )
732
 
733
 
734
+ # ─────────────────────────────────────────────
735
+ # Entry point
736
+ # ─────────────────────────────────────────────
737
+
738
  if __name__ == "__main__":
739
  import uvicorn
740
  uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)