rkihacker commited on
Commit
1e679fd
·
verified ·
1 Parent(s): 4c88f38

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +45 -41
main.py CHANGED
@@ -12,7 +12,7 @@ import aiohttp
12
  from bs4 import BeautifulSoup
13
 
14
  # --- Configuration ---
15
- logging.basicConfig(level=logging.INFO)
16
  logger = logging.getLogger(__name__)
17
 
18
  load_dotenv()
@@ -21,22 +21,24 @@ LLM_API_KEY = os.getenv("LLM_API_KEY")
21
  if not LLM_API_KEY:
22
  raise RuntimeError("LLM_API_KEY must be set in a .env file.")
23
  else:
24
- logger.info(f"LLM API Key loaded successfully (starts with: {LLM_API_KEY[:4]}...).")
25
 
26
- # API URLs, Models, and context size limit
27
  SNAPZION_API_URL = "https://search.snapzion.com/get-snippets"
28
  LLM_API_URL = "https://api.typegpt.net/v1/chat/completions"
29
- LLM_MODEL = "gpt-4.1-mini" # Corrected model name from previous attempts
30
  MAX_CONTEXT_CHAR_LENGTH = 120000
31
 
32
  # Headers for external services
33
  SNAPZION_HEADERS = { 'Content-Type': 'application/json', 'User-Agent': 'AI-Deep-Research-Agent/1.0' }
34
  SCRAPING_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36' }
35
- # ***** CHANGE 1: Add a User-Agent to the LLM headers *****
 
36
  LLM_HEADERS = {
37
  "Authorization": f"Bearer {LLM_API_KEY}",
38
  "Content-Type": "application/json",
39
- "User-Agent": "AI-Deep-Research-Client/2.2"
 
40
  }
41
 
42
  # --- Pydantic Models ---
@@ -47,7 +49,7 @@ class DeepResearchRequest(BaseModel):
47
  app = FastAPI(
48
  title="AI Deep Research API",
49
  description="Provides streaming deep research completions.",
50
- version="2.2.0" # Version bump for critical bug fix
51
  )
52
 
53
  # --- Core Service Functions (Unchanged) ---
@@ -94,6 +96,7 @@ async def run_deep_research_stream(query: str) -> AsyncGenerator[str, None]:
94
  def format_sse(data: dict) -> str:
95
  return f"data: {json.dumps(data)}\n\n"
96
 
 
97
  try:
98
  async with aiohttp.ClientSession() as session:
99
  # Step 1: Generate Sub-Questions
@@ -104,63 +107,63 @@ async def run_deep_research_stream(query: str) -> AsyncGenerator[str, None]:
104
  "messages": [{ "role": "user", "content": f"You are a research planner. For the topic '{query}', create a JSON array of 3-4 key sub-questions for a research report. Respond ONLY with the JSON array. Example: [\"Question 1?\", \"Question 2?\"]" }]
105
  }
106
 
107
- # ***** CHANGE 2: Implement robust parsing for the API call *****
108
  try:
 
109
  async with session.post(LLM_API_URL, headers=LLM_HEADERS, json=sub_question_prompt, timeout=20) as response:
 
 
110
  if response.status != 200:
111
- error_text = await response.text()
112
- logger.error(f"LLM API for planning failed with status {response.status}: {error_text}")
113
- raise Exception(f"LLM API returned non-200 status: {response.status}")
114
 
115
- raw_response_text = await response.text()
116
- if not raw_response_text:
117
- raise Exception("LLM API returned an empty response.")
118
 
119
- result = json.loads(raw_response_text)
120
- llm_content = result['choices'][0]['message']['content']
 
 
 
 
 
121
  sub_questions = json.loads(llm_content)
 
122
  except Exception as e:
123
- logger.error(f"Failed to generate or parse research plan: {e}")
 
124
  yield format_sse({"event": "error", "data": f"Could not generate research plan. Reason: {e}"})
125
- return # Stop the process if planning fails
126
 
127
  yield format_sse({"event": "plan", "data": sub_questions})
128
 
129
- # (The rest of the logic remains the same)
130
- # Step 2: Concurrently research all sub-questions
131
  research_tasks = [search_and_scrape(session, sq) for sq in sub_questions]
132
- all_research_results = []
133
 
134
- for i, task in enumerate(asyncio.as_completed(research_tasks)):
135
- yield format_sse({"event": "status", "data": f"Researching: \"{sub_questions[i]}\""})
136
- result = await task
137
- all_research_results.append(result)
138
-
139
- # Step 3: Consolidate all context and sources
140
- yield format_sse({"event": "status", "data": "Consolidating research..."})
141
- full_context = "\n\n---\n\n".join(res[0] for res in all_research_results if res[0])
142
- all_sources = [source for res in all_research_results for source in res[1]]
143
- unique_sources = list({s['link']: s for s in all_sources}.values())
144
 
145
- if len(full_context) > MAX_CONTEXT_CHAR_LENGTH:
146
- logger.warning(f"Context is too long. Truncating from {len(full_context)} to {MAX_CONTEXT_CHAR_LENGTH} characters.")
147
- full_context = full_context[:MAX_CONTEXT_CHAR_LENGTH]
148
 
149
- if not full_context.strip():
150
  yield format_sse({"event": "error", "data": "Failed to gather any research context."})
151
  return
152
 
153
- # Step 4: Generate the final report with streaming
154
  yield format_sse({"event": "status", "data": "Generating final report..."})
155
- final_report_prompt = f'Synthesize the provided context into a comprehensive report on "{query}". Use the context exclusively. Structure the report with markdown.\n\n## Research Context ##\n{full_context}'
156
-
157
  final_report_payload = {"model": LLM_MODEL, "messages": [{"role": "user", "content": final_report_prompt}], "stream": True}
158
 
159
  async with session.post(LLM_API_URL, headers=LLM_HEADERS, json=final_report_payload) as response:
160
  if response.status != 200:
161
  error_text = await response.text()
162
  raise Exception(f"LLM API Error for final report: {response.status}, {error_text}")
163
-
164
  async for line in response.content:
165
  if line.strip():
166
  line_str = line.decode('utf-8').strip()
@@ -171,11 +174,12 @@ async def run_deep_research_stream(query: str) -> AsyncGenerator[str, None]:
171
  content = chunk.get("choices", [{}])[0].get("delta", {}).get("content")
172
  if content: yield format_sse({"event": "chunk", "data": content})
173
  except json.JSONDecodeError: continue
174
-
 
175
  yield format_sse({"event": "sources", "data": unique_sources})
176
 
177
  except Exception as e:
178
- logger.error(f"An error occurred during deep research: {e}")
179
  yield format_sse({"event": "error", "data": str(e)})
180
  finally:
181
  yield format_sse({"event": "done", "data": "Deep research complete."})
 
12
  from bs4 import BeautifulSoup
13
 
14
  # --- Configuration ---
15
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
16
  logger = logging.getLogger(__name__)
17
 
18
  load_dotenv()
 
21
  if not LLM_API_KEY:
22
  raise RuntimeError("LLM_API_KEY must be set in a .env file.")
23
  else:
24
+ logger.info(f"LLM API Key loaded successfully.")
25
 
26
+ # ***** CHANGE 1: Update constants to match your new API provider *****
27
  SNAPZION_API_URL = "https://search.snapzion.com/get-snippets"
28
  LLM_API_URL = "https://api.typegpt.net/v1/chat/completions"
29
+ LLM_MODEL = "gpt-4.1-mini"
30
  MAX_CONTEXT_CHAR_LENGTH = 120000
31
 
32
  # Headers for external services
33
  SNAPZION_HEADERS = { 'Content-Type': 'application/json', 'User-Agent': 'AI-Deep-Research-Agent/1.0' }
34
  SCRAPING_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36' }
35
+
36
+ # ***** CHANGE 2: Create more standard and robust headers for the LLM call *****
37
  LLM_HEADERS = {
38
  "Authorization": f"Bearer {LLM_API_KEY}",
39
  "Content-Type": "application/json",
40
+ "Accept": "application/json", # Explicitly request a JSON response
41
+ "User-Agent": "AI-Deep-Research-Client/2.3"
42
  }
43
 
44
  # --- Pydantic Models ---
 
49
  app = FastAPI(
50
  title="AI Deep Research API",
51
  description="Provides streaming deep research completions.",
52
+ version="2.3.0" # Version bump for advanced error handling
53
  )
54
 
55
  # --- Core Service Functions (Unchanged) ---
 
96
  def format_sse(data: dict) -> str:
97
  return f"data: {json.dumps(data)}\n\n"
98
 
99
+ raw_response_text_for_debugging = "" # Variable to hold response text for logging
100
  try:
101
  async with aiohttp.ClientSession() as session:
102
  # Step 1: Generate Sub-Questions
 
107
  "messages": [{ "role": "user", "content": f"You are a research planner. For the topic '{query}', create a JSON array of 3-4 key sub-questions for a research report. Respond ONLY with the JSON array. Example: [\"Question 1?\", \"Question 2?\"]" }]
108
  }
109
 
110
+ # ***** CHANGE 3: The most critical fix. Heavily reinforced error handling. *****
111
  try:
112
+ logger.info(f"Sending request to LLM for planning. Model: {LLM_MODEL}, URL: {LLM_API_URL}")
113
  async with session.post(LLM_API_URL, headers=LLM_HEADERS, json=sub_question_prompt, timeout=20) as response:
114
+ raw_response_text_for_debugging = await response.text()
115
+
116
  if response.status != 200:
117
+ logger.error(f"LLM API for planning failed! Status: {response.status}, Headers: {response.headers}, Body: {raw_response_text_for_debugging}")
118
+ raise Exception(f"LLM provider returned non-200 status: {response.status}")
 
119
 
120
+ if not raw_response_text_for_debugging:
121
+ raise Exception("LLM provider returned an empty response body.")
 
122
 
123
+ result = json.loads(raw_response_text_for_debugging)
124
+ llm_content = result.get('choices', [{}])[0].get('message', {}).get('content', '')
125
+
126
+ if not llm_content or not llm_content.strip().startswith('['):
127
+ logger.error(f"LLM did not return a valid JSON array string. Received: {llm_content}")
128
+ raise Exception("LLM failed to generate a valid research plan.")
129
+
130
  sub_questions = json.loads(llm_content)
131
+
132
  except Exception as e:
133
+ # This will now catch the JSON error and log the problematic text
134
+ logger.error(f"Failed to generate/parse research plan. Error: {e}. Raw API Response: '{raw_response_text_for_debugging}'")
135
  yield format_sse({"event": "error", "data": f"Could not generate research plan. Reason: {e}"})
136
+ return
137
 
138
  yield format_sse({"event": "plan", "data": sub_questions})
139
 
140
+ # (The rest of the logic remains the same, as it was not the point of failure)
 
141
  research_tasks = [search_and_scrape(session, sq) for sq in sub_questions]
142
+ yield format_sse({"event": "status", "data": f"Starting research on {len(sub_questions)} topics..."})
143
 
144
+ consolidated_context = ""
145
+ all_sources = []
146
+ for task in asyncio.as_completed(research_tasks):
147
+ context, sources = await task
148
+ if context: consolidated_context += context + "\n\n---\n\n"
149
+ if sources: all_sources.extend(sources)
 
 
 
 
150
 
151
+ yield format_sse({"event": "status", "data": "Consolidating research..."})
152
+ if len(consolidated_context) > MAX_CONTEXT_CHAR_LENGTH:
153
+ consolidated_context = consolidated_context[:MAX_CONTEXT_CHAR_LENGTH]
154
 
155
+ if not consolidated_context.strip():
156
  yield format_sse({"event": "error", "data": "Failed to gather any research context."})
157
  return
158
 
 
159
  yield format_sse({"event": "status", "data": "Generating final report..."})
160
+ final_report_prompt = f'Synthesize the provided context into a comprehensive report on "{query}". Use markdown. Context:\n{consolidated_context}'
 
161
  final_report_payload = {"model": LLM_MODEL, "messages": [{"role": "user", "content": final_report_prompt}], "stream": True}
162
 
163
  async with session.post(LLM_API_URL, headers=LLM_HEADERS, json=final_report_payload) as response:
164
  if response.status != 200:
165
  error_text = await response.text()
166
  raise Exception(f"LLM API Error for final report: {response.status}, {error_text}")
 
167
  async for line in response.content:
168
  if line.strip():
169
  line_str = line.decode('utf-8').strip()
 
174
  content = chunk.get("choices", [{}])[0].get("delta", {}).get("content")
175
  if content: yield format_sse({"event": "chunk", "data": content})
176
  except json.JSONDecodeError: continue
177
+
178
+ unique_sources = list({s['link']: s for s in all_sources}.values())
179
  yield format_sse({"event": "sources", "data": unique_sources})
180
 
181
  except Exception as e:
182
+ logger.error(f"A critical error occurred in the main research stream: {e}")
183
  yield format_sse({"event": "error", "data": str(e)})
184
  finally:
185
  yield format_sse({"event": "done", "data": "Deep research complete."})