Update main.py
Browse files
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
|
| 25 |
|
| 26 |
-
#
|
| 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 |
LLM_HEADERS = {
|
| 37 |
"Authorization": f"Bearer {LLM_API_KEY}",
|
| 38 |
"Content-Type": "application/json",
|
| 39 |
-
"
|
|
|
|
| 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.
|
| 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
|
| 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 |
-
|
| 112 |
-
|
| 113 |
-
raise Exception(f"LLM API returned non-200 status: {response.status}")
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
raise Exception("LLM API returned an empty response.")
|
| 118 |
|
| 119 |
-
result = json.loads(
|
| 120 |
-
llm_content = result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
sub_questions = json.loads(llm_content)
|
|
|
|
| 122 |
except Exception as e:
|
| 123 |
-
|
|
|
|
| 124 |
yield format_sse({"event": "error", "data": f"Could not generate research plan. Reason: {e}"})
|
| 125 |
-
return
|
| 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 |
-
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 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 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
|
| 149 |
-
if not
|
| 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
|
| 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"
|
| 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."})
|