PraneshJs commited on
Commit
af3beff
·
verified ·
1 Parent(s): 934712a

Added gradio and removed unicorn

Browse files
Files changed (1) hide show
  1. app.py +141 -171
app.py CHANGED
@@ -1,230 +1,200 @@
1
  import os
2
  import asyncio
3
- import requests
4
  import urllib3
5
- from openai import AzureOpenAI
 
6
  from dotenv import load_dotenv
 
 
 
7
  from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
8
  from crawl4ai.content_filter_strategy import PruningContentFilter
9
  from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
10
- from fastapi import FastAPI, HTTPException
11
- from fastapi.staticfiles import StaticFiles
12
- from fastapi.middleware.cors import CORSMiddleware
13
- from pydantic import BaseModel
14
- import uvicorn
15
- import json
16
  import gradio as gr
17
 
18
- # Disable SSL warnings
19
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
20
 
 
21
  load_dotenv()
22
 
23
- # Initialize FastAPI app
24
- app = FastAPI(title="Search Assistant API")
25
-
26
- # Add CORS middleware
27
- app.add_middleware(
28
- CORSMiddleware,
29
- allow_origins=["*"], # In production, replace with specific origins
30
- allow_credentials=True,
31
- allow_methods=["*"],
32
- allow_headers=["*"],
33
- )
34
-
35
- # Mount static files
36
- # app.mount("/static", StaticFiles(directory="static"), name="static")
37
-
38
- # Initialize Azure OpenAI client
39
  client = AzureOpenAI(
40
  api_key=os.getenv("AZURE_OPENAI_KEY").strip(),
41
- api_version="2025-01-01-preview",
42
- azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT").strip()
43
  )
44
 
45
  SERPER_API_KEY = os.getenv("SERPER_API_KEY").strip()
46
- DEPLOYMENT_NAME = os.getenv("AZURE_OPENAI_DEPLOYMENT").strip()
47
-
48
- class SearchRequest(BaseModel):
49
- question: str
50
- mode: str = "quick" # "quick" or "deep"
51
-
52
- class SearchResponse(BaseModel):
53
- answer: str
54
- sources: list
55
- mode: str
56
- status: str = "success"
57
-
58
- def search_serper(query):
59
- headers = {
60
- "X-API-KEY": SERPER_API_KEY,
61
- "Content-Type": "application/json"
62
- }
63
  payload = {"q": query}
64
- response = requests.post("https://google.serper.dev/search", headers=headers, json=payload, verify=False)
65
- results = response.json()
66
-
67
- # Return both snippets and URLs for crawling
68
- search_results = []
69
- for result in results.get("organic", [])[:3]: # Limit to top 3 for crawling
70
- title = result.get("title", "")
71
- snippet = result.get("snippet", "")
72
- url = result.get("link", "")
73
- search_results.append({
74
- "title": title,
75
- "snippet": snippet,
76
- "url": url
77
  })
 
78
 
79
- return search_results
80
 
 
 
 
81
  async def crawl_to_markdown(url: str) -> str:
82
- """Crawl a URL and return its content as markdown."""
 
 
 
83
  try:
84
  browser_conf = BrowserConfig(headless=True, verbose=False)
85
  filter_strategy = PruningContentFilter()
86
  md_gen = DefaultMarkdownGenerator(content_filter=filter_strategy)
87
  run_conf = CrawlerRunConfig(markdown_generator=md_gen)
88
-
89
  async with AsyncWebCrawler(config=browser_conf) as crawler:
90
  result = await crawler.arun(url=url, config=run_conf)
91
  return result.markdown.fit_markdown or result.markdown.raw_markdown or ""
92
  except Exception as e:
93
- return f"Crawl error for {url}: {str(e)}"
94
 
95
- async def generate_answer_with_crawling(question):
96
- """Generate answer using search results and crawled content."""
 
 
 
 
 
 
 
97
  try:
98
- # 1. Get search results
99
  search_results = search_serper(question)
100
-
101
- # 2. Crawl each URL to get full content
102
- crawled_content = []
103
- for result in search_results:
104
- url = result["url"]
105
- title = result["title"]
106
-
107
- print(f"Crawling: {title} ({url})")
108
- markdown_content = await crawl_to_markdown(url)
109
-
110
- # Limit content to avoid token limits
111
- content_snippet = markdown_content[:2000] if markdown_content else result["snippet"]
112
- crawled_content.append(f"## {title}\nSource: {url}\n\n{content_snippet}\n\n")
113
-
114
- # 3. Combine all content for context
115
- full_context = "\n".join(crawled_content)
116
-
117
  messages = [
118
  {"role": "system", "content": "You are a helpful assistant that answers questions using detailed web content. Provide citations with URLs when possible."},
119
- {"role": "user", "content": f"Based on the following web content, answer the question. Include relevant citations.\n\nContent:\n{full_context}\n\nQuestion: {question}"}
120
  ]
121
-
122
- response = client.chat.completions.create(
123
  model=DEPLOYMENT_NAME,
124
  messages=messages,
125
  temperature=0.8,
126
- max_tokens=800
127
  )
128
- return response.choices[0].message.content, search_results
129
-
 
130
  except Exception as e:
131
- return f"Error: {str(e)}", []
132
 
133
- def generate_answer(question):
134
- """Original function using just search snippets."""
 
 
 
135
  search_results = search_serper(question)
136
-
137
  snippets = []
138
- for result in search_results:
139
- title = result["title"]
140
- snippet = result["snippet"]
141
- url = result["url"]
142
  snippets.append(f"{title}: {snippet} ({url})")
143
 
144
- context = "\n".join(snippets)
145
  messages = [
146
  {"role": "system", "content": "You are a helpful assistant that answers using real-time search context."},
147
  {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"}
148
  ]
149
- response = client.chat.completions.create(
150
  model=DEPLOYMENT_NAME,
151
  messages=messages,
152
  temperature=0.8,
153
- max_tokens=800
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  )
155
- return response.choices[0].message.content
156
-
157
- # API Endpoints
158
- @app.get("/")
159
- async def root():
160
- return {"status": "ok"}
161
-
162
-
163
- @app.post("/search")
164
- async def search_endpoint(request: SearchRequest):
165
- """Search endpoint that returns JSON response."""
166
- try:
167
- print(f"\n🔍 Search Request:")
168
- print(f"Question: {request.question}")
169
- print(f"Mode: {request.mode}")
170
-
171
- if request.mode == "deep":
172
- print("🕷️ Starting deep search with web crawling...")
173
- answer, sources = await generate_answer_with_crawling(request.question)
174
- else:
175
- print("⚡ Starting quick search...")
176
- answer = generate_answer(request.question)
177
- sources = search_serper(request.question)
178
-
179
- response_data = {
180
- "answer": answer,
181
- "sources": sources,
182
- "mode": request.mode,
183
- "status": "success"
184
- }
185
-
186
- print(f"\n📋 Response Data:")
187
- print(json.dumps(response_data, indent=2))
188
-
189
- return response_data
190
-
191
- except Exception as e:
192
- error_response = {
193
- "answer": f"Error: {str(e)}",
194
- "sources": [],
195
- "mode": request.mode,
196
- "status": "error"
197
- }
198
-
199
- print(f"\n❌ Error Response:")
200
- print(json.dumps(error_response, indent=2))
201
-
202
- raise HTTPException(status_code=500, detail=error_response)
203
-
204
- def gradio_search(question, mode):
205
- import requests
206
- try:
207
- resp = requests.post(
208
- "/search", # Internal call on Spaces
209
- json={"question": question, "mode": mode},
210
- timeout=60
211
- )
212
- data = resp.json()
213
- answer = data.get("answer", "")
214
- sources = data.get("sources", [])
215
- sources_md = "\n".join([f"- [{src['title']}]({src['url']})" for src in sources])
216
- return answer, sources_md
217
- except Exception as e:
218
- return f"Error: {e}", ""
219
 
220
- with gr.Blocks() as demo:
221
- gr.Markdown("# Search Assistant")
222
- question = gr.Textbox(label="Question", placeholder="Ask anything...")
223
- mode = gr.Radio(choices=["quick", "deep"], value="quick", label="Mode")
224
- answer = gr.Markdown(label="Answer")
225
- sources = gr.Markdown(label="Sources")
226
- btn = gr.Button("Search")
227
- btn.click(gradio_search, inputs=[question, mode], outputs=[answer, sources])
228
 
229
  if __name__ == "__main__":
230
- demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
 
 
1
  import os
2
  import asyncio
3
+ import json
4
  import urllib3
5
+ import requests
6
+
7
  from dotenv import load_dotenv
8
+ from openai import AzureOpenAI
9
+
10
+ # crawl4ai / Playwright
11
  from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
12
  from crawl4ai.content_filter_strategy import PruningContentFilter
13
  from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
14
+
 
 
 
 
 
15
  import gradio as gr
16
 
17
+ # --- Disable SSL warnings (keep if your SERPER endpoint dislikes verification) ---
18
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
19
 
20
+ # --- Load .env (also set these as HF Space Secrets) ---
21
  load_dotenv()
22
 
23
+ # --- Azure OpenAI client ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  client = AzureOpenAI(
25
  api_key=os.getenv("AZURE_OPENAI_KEY").strip(),
26
+ api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2025-01-01-preview"),
27
+ azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT").strip(),
28
  )
29
 
30
  SERPER_API_KEY = os.getenv("SERPER_API_KEY").strip()
31
+ DEPLOYMENT_NAME = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4.1").strip() # Your Azure model deployment name
32
+
33
+
34
+ # -------------------------
35
+ # Search (Serper) utilities
36
+ # -------------------------
37
+ def search_serper(query: str):
38
+ """
39
+ Returns a short list of {title, snippet, url} for the query.
40
+ """
41
+ if not SERPER_API_KEY:
42
+ raise RuntimeError("SERPER_API_KEY is not set")
43
+
44
+ headers = {"X-API-KEY": SERPER_API_KEY, "Content-Type": "application/json"}
 
 
 
45
  payload = {"q": query}
46
+
47
+ # verify=False because the original code disabled SSL warnings
48
+ resp = requests.post("https://google.serper.dev/search", headers=headers, json=payload, verify=False)
49
+ resp.raise_for_status()
50
+ results = resp.json()
51
+
52
+ out = []
53
+ for result in results.get("organic", [])[:3]:
54
+ out.append({
55
+ "title": result.get("title", ""),
56
+ "snippet": result.get("snippet", ""),
57
+ "url": result.get("link", "")
 
58
  })
59
+ return out
60
 
 
61
 
62
+ # -------------------------
63
+ # Crawl utilities
64
+ # -------------------------
65
  async def crawl_to_markdown(url: str) -> str:
66
+ """
67
+ Crawl a URL and return markdown (fallback to raw if needed).
68
+ Assumes Playwright + Chromium is available in the Docker image.
69
+ """
70
  try:
71
  browser_conf = BrowserConfig(headless=True, verbose=False)
72
  filter_strategy = PruningContentFilter()
73
  md_gen = DefaultMarkdownGenerator(content_filter=filter_strategy)
74
  run_conf = CrawlerRunConfig(markdown_generator=md_gen)
75
+
76
  async with AsyncWebCrawler(config=browser_conf) as crawler:
77
  result = await crawler.arun(url=url, config=run_conf)
78
  return result.markdown.fit_markdown or result.markdown.raw_markdown or ""
79
  except Exception as e:
80
+ return f"Crawl error for {url}: {e}"
81
 
82
+
83
+ # -------------------------
84
+ # LLM orchestration
85
+ # -------------------------
86
+ async def generate_answer_with_crawling(question: str):
87
+ """
88
+ Deep mode: search + crawl + synthesize with Azure OpenAI.
89
+ Returns (answer, sources_list)
90
+ """
91
  try:
 
92
  search_results = search_serper(question)
93
+
94
+ crawled_pieces = []
95
+ for r in search_results:
96
+ url = r["url"]
97
+ title = r["title"] or url
98
+ md = await crawl_to_markdown(url)
99
+
100
+ # Keep it small to avoid tokens blow-up
101
+ snippet = (md or r["snippet"])[:2000]
102
+ block = f"## {title}\nSource: {url}\n\n{snippet}\n\n"
103
+ crawled_pieces.append(block)
104
+
105
+ context = "\n".join(crawled_pieces) or "No crawl content available."
106
+
 
 
 
107
  messages = [
108
  {"role": "system", "content": "You are a helpful assistant that answers questions using detailed web content. Provide citations with URLs when possible."},
109
+ {"role": "user", "content": f"Based on the following web content, answer the question. Include relevant citations.\n\nContent:\n{context}\n\nQuestion: {question}"}
110
  ]
111
+
112
+ resp = client.chat.completions.create(
113
  model=DEPLOYMENT_NAME,
114
  messages=messages,
115
  temperature=0.8,
116
+ max_tokens=800,
117
  )
118
+ answer = resp.choices[0].message.content
119
+ return answer, search_results
120
+
121
  except Exception as e:
122
+ return f"Error (deep): {e}", []
123
 
124
+
125
+ def generate_answer_quick(question: str):
126
+ """
127
+ Quick mode: search snippets only + Azure OpenAI.
128
+ """
129
  search_results = search_serper(question)
 
130
  snippets = []
131
+ for r in search_results:
132
+ title = r["title"]
133
+ snippet = r["snippet"]
134
+ url = r["url"]
135
  snippets.append(f"{title}: {snippet} ({url})")
136
 
137
+ context = "\n".join(snippets) or "No search snippets available."
138
  messages = [
139
  {"role": "system", "content": "You are a helpful assistant that answers using real-time search context."},
140
  {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"}
141
  ]
142
+ resp = client.chat.completions.create(
143
  model=DEPLOYMENT_NAME,
144
  messages=messages,
145
  temperature=0.8,
146
+ max_tokens=800,
147
+ )
148
+ return resp.choices[0].message.content, search_results
149
+
150
+
151
+ # -------------------------
152
+ # Gradio function
153
+ # -------------------------
154
+ async def search_fn(question: str, mode: str):
155
+ """
156
+ Gradio-servable function. Returns:
157
+ - Markdown answer
158
+ - JSON of sources
159
+ """
160
+ mode = (mode or "quick").lower()
161
+ if not question.strip():
162
+ return "⚠️ Please enter a question.", "[]"
163
+
164
+ if mode == "deep":
165
+ answer, sources = await generate_answer_with_crawling(question)
166
+ else:
167
+ # run sync function in a thread so the Gradio loop is not blocked
168
+ answer, sources = await asyncio.to_thread(generate_answer_quick, question)
169
+
170
+ return answer, json.dumps(sources, indent=2)
171
+
172
+
173
+ # -------------------------
174
+ # Gradio UI
175
+ # -------------------------
176
+ with gr.Blocks(title="Search Assistant") as demo:
177
+ gr.Markdown("# 🔎 Search Assistant\nAsk a question. Pick **Quick** or **Deep** (crawls the top results).")
178
+
179
+ with gr.Row():
180
+ txt = gr.Textbox(label="Your question", placeholder="e.g., What's new in Python 3.12?", lines=3)
181
+ with gr.Row():
182
+ mode = gr.Radio(choices=["quick", "deep"], value="quick", label="Mode")
183
+ run_btn = gr.Button("Search")
184
+ with gr.Row():
185
+ answer_out = gr.Markdown(label="Answer")
186
+ with gr.Row():
187
+ sources_out = gr.JSON(label="Sources (top 3)")
188
+
189
+ run_btn.click(
190
+ fn=search_fn,
191
+ inputs=[txt, mode],
192
+ outputs=[answer_out, sources_out]
193
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ # Expose API (Gradio does this automatically). In Spaces:
196
+ # POST /run/predict with {"data": ["your question", "quick"]}
 
 
 
 
 
 
197
 
198
  if __name__ == "__main__":
199
+ # In HF Spaces Docker, Gradio is launched by this script.
200
+ demo.launch(share = False)