tecuts commited on
Commit
a79844e
·
verified ·
1 Parent(s): 5cb21e4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -24
app.py CHANGED
@@ -3,12 +3,38 @@ import json
3
  import requests
4
  from datetime import datetime
5
  from typing import List, Dict, Optional
6
- from fastapi import FastAPI, Request, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from openai import OpenAI
9
  import logging
10
- from fastapi.responses import StreamingResponse
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # --- Configure Logging ---
14
  logging.basicConfig(level=logging.INFO)
@@ -157,9 +183,14 @@ app = FastAPI(title="AI Chatbot with Enhanced Search", version="2.0.0")
157
 
158
  app.add_middleware(
159
  CORSMiddleware,
160
- allow_origins=["*"], # In production, specify actual origins
 
 
 
 
 
161
  allow_credentials=True,
162
- allow_methods=["*"],
163
  allow_headers=["*"],
164
  )
165
 
@@ -221,51 +252,154 @@ def should_use_search(message: str) -> bool:
221
 
222
  # --- Enhanced Chatbot Endpoint ---
223
  @app.post("/chat")
224
- async def chat_endpoint(request: Request):
225
  if not client:
226
  raise HTTPException(status_code=500, detail="LLM client not configured")
 
227
  try:
228
  data = await request.json()
229
  user_message = data.get("message", "").strip()
230
- use_search = data.get("use_search", data.get("user_search"))
 
 
 
 
 
 
 
 
 
 
 
231
  conversation_history = data.get("history", [])
232
-
 
 
 
233
  if not user_message:
234
  raise HTTPException(status_code=400, detail="No message provided")
235
 
 
236
  if use_search is None:
237
  use_search = should_use_search(user_message)
 
 
 
238
 
 
239
  current_date = datetime.now().strftime("%Y-%m-%d")
 
240
  if use_search:
241
  system_content = SYSTEM_PROMPT_WITH_SEARCH.format(current_date=current_date)
242
  else:
243
  system_content = SYSTEM_PROMPT_NO_SEARCH.format(current_date=current_date)
 
244
  system_message = {"role": "system", "content": system_content}
245
  messages = [system_message] + conversation_history + [{"role": "user", "content": user_message}]
 
246
  llm_kwargs = {
247
- "model": "unsloth/Qwen3-30B-A3B-GGUF",
248
- "temperature": 0.7,
249
  "messages": messages,
250
- "max_tokens": 2000,
251
- "stream": True, # <--- Enable streaming
252
  }
 
253
  if use_search:
 
254
  llm_kwargs["tools"] = available_tools
255
- llm_kwargs["tool_choice"] = "auto"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- # Streaming generator
258
- def stream_llm_response():
259
- response = client.chat.completions.create(**llm_kwargs)
260
- for chunk in response:
261
- # Each chunk is an object, get the content delta
262
- if hasattr(chunk.choices[0].delta, "content"):
263
- content = chunk.choices[0].delta.content
264
- if content:
265
- yield content
 
266
 
267
- # Return as streaming response
268
- return StreamingResponse(stream_llm_response(), media_type="text/plain")
 
 
 
 
 
 
 
 
 
269
 
270
  except HTTPException:
271
  raise
@@ -276,7 +410,6 @@ async def chat_endpoint(request: Request):
276
  logger.error(f"Unexpected error in /chat endpoint: {e}")
277
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
278
 
279
-
280
  # --- Health Check Endpoint ---
281
  @app.get("/")
282
  async def root():
 
3
  import requests
4
  from datetime import datetime
5
  from typing import List, Dict, Optional
6
+ from fastapi import FastAPI, Request, HTTPException, Depends
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from openai import OpenAI
9
  import logging
 
10
 
11
+ # --- Security Helper Functions ---
12
+ def verify_origin(request: Request):
13
+ """Verify that the request comes from an allowed origin for /chat endpoint"""
14
+ origin = request.headers.get("origin")
15
+ referer = request.headers.get("referer")
16
+
17
+ allowed_origins = [
18
+ "https://chrunos.com",
19
+ "https://www.chrunos.com"
20
+ ]
21
+
22
+ # Allow localhost for development (you can remove this in production)
23
+ if origin and any(origin.startswith(local) for local in ["http://localhost:", "http://127.0.0.1:"]):
24
+ return True
25
+
26
+ # Check origin header
27
+ if origin in allowed_origins:
28
+ return True
29
+
30
+ # Check referer header as fallback
31
+ if referer and any(referer.startswith(allowed) for allowed in allowed_origins):
32
+ return True
33
+
34
+ raise HTTPException(
35
+ status_code=403,
36
+ detail="Access denied: This endpoint is only accessible from chrunos.com"
37
+ )
38
 
39
  # --- Configure Logging ---
40
  logging.basicConfig(level=logging.INFO)
 
183
 
184
  app.add_middleware(
185
  CORSMiddleware,
186
+ allow_origins=[
187
+ "https://chrunos.com",
188
+ "https://www.chrunos.com",
189
+ "http://localhost:3000", # For local development
190
+ "http://localhost:8000", # For local development
191
+ ],
192
  allow_credentials=True,
193
+ allow_methods=["GET", "POST", "OPTIONS"],
194
  allow_headers=["*"],
195
  )
196
 
 
252
 
253
  # --- Enhanced Chatbot Endpoint ---
254
  @app.post("/chat")
255
+ async def chat_endpoint(request: Request, _: None = Depends(verify_origin)):
256
  if not client:
257
  raise HTTPException(status_code=500, detail="LLM client not configured")
258
+
259
  try:
260
  data = await request.json()
261
  user_message = data.get("message", "").strip()
262
+
263
+ # Support both 'use_search' and 'user_search' parameter names for flexibility
264
+ use_search = data.get("use_search")
265
+ if use_search is None:
266
+ use_search = data.get("user_search") # Alternative parameter name
267
+
268
+ # Allow client to specify temperature (with validation)
269
+ temperature = data.get("temperature", 0.7) # Default to 0.7
270
+ if not isinstance(temperature, (int, float)) or temperature < 0 or temperature > 2:
271
+ logger.warning(f"Invalid temperature value: {temperature}, defaulting to 0.7")
272
+ temperature = 0.7
273
+
274
  conversation_history = data.get("history", [])
275
+
276
+ # Debug logging for request parameters
277
+ logger.info(f"Request parameters - message length: {len(user_message)}, use_search: {use_search}, temperature: {temperature}, history length: {len(conversation_history)}")
278
+
279
  if not user_message:
280
  raise HTTPException(status_code=400, detail="No message provided")
281
 
282
+ # Auto-decide search usage if not specified
283
  if use_search is None:
284
  use_search = should_use_search(user_message)
285
+ logger.info(f"Auto-decided search usage: {use_search}")
286
+ else:
287
+ logger.info(f"Manual search setting: {use_search}")
288
 
289
+ # Prepare messages with appropriate system prompt based on search availability
290
  current_date = datetime.now().strftime("%Y-%m-%d")
291
+
292
  if use_search:
293
  system_content = SYSTEM_PROMPT_WITH_SEARCH.format(current_date=current_date)
294
  else:
295
  system_content = SYSTEM_PROMPT_NO_SEARCH.format(current_date=current_date)
296
+
297
  system_message = {"role": "system", "content": system_content}
298
  messages = [system_message] + conversation_history + [{"role": "user", "content": user_message}]
299
+
300
  llm_kwargs = {
301
+ "model": "unsloth/Qwen3-30B-A3B-GGUF",
302
+ "temperature": temperature, # Use client-specified temperature
303
  "messages": messages,
304
+ "max_tokens": 2000 # Ensure comprehensive responses
 
305
  }
306
+
307
  if use_search:
308
+ logger.info("Search is ENABLED - tools will be available to the model")
309
  llm_kwargs["tools"] = available_tools
310
+ llm_kwargs["tool_choice"] = "auto" # Consider using "required" for testing
311
+ else:
312
+ logger.info("Search is DISABLED - no tools available")
313
+
314
+ # First LLM call
315
+ logger.info(f"Making LLM request with tools: {bool(use_search)}, temperature: {temperature}")
316
+ llm_response = client.chat.completions.create(**llm_kwargs)
317
+ tool_calls = llm_response.choices[0].message.tool_calls
318
+ source_links = []
319
+
320
+ # Debug: Log tool call information
321
+ if tool_calls:
322
+ logger.info(f"LLM made {len(tool_calls)} tool calls")
323
+ for i, call in enumerate(tool_calls):
324
+ logger.info(f"Tool call {i+1}: {call.function.name} with args: {call.function.arguments}")
325
+ else:
326
+ logger.info("LLM did not make any tool calls")
327
+ if use_search:
328
+ logger.warning("Search was enabled but LLM chose not to use search tools - this might indicate the query doesn't require current information")
329
+
330
+ if tool_calls:
331
+ logger.info(f"Processing {len(tool_calls)} tool calls")
332
+ tool_outputs = []
333
+
334
+ for tool_call in tool_calls:
335
+ if tool_call.function.name == "google_search":
336
+ try:
337
+ function_args = json.loads(tool_call.function.arguments)
338
+ search_query = function_args.get("query", "").strip()
339
+
340
+ if search_query:
341
+ logger.info(f"Executing search for: {search_query}")
342
+ search_results = google_search_tool([search_query], num_results=5)
343
+
344
+ # Collect source links for response
345
+ for result in search_results:
346
+ source_links.append({
347
+ "title": result["source_title"],
348
+ "url": result["url"],
349
+ "domain": result["domain"]
350
+ })
351
+
352
+ # Format results for LLM
353
+ formatted_results = format_search_results_for_llm(search_results)
354
+ tool_outputs.append({
355
+ "tool_call_id": tool_call.id,
356
+ "output": formatted_results
357
+ })
358
+ else:
359
+ logger.warning("Empty search query in tool call")
360
+ tool_outputs.append({
361
+ "tool_call_id": tool_call.id,
362
+ "output": "Error: Empty search query provided."
363
+ })
364
+
365
+ except json.JSONDecodeError as e:
366
+ logger.error(f"Failed to parse tool call arguments: {e}")
367
+ tool_outputs.append({
368
+ "tool_call_id": tool_call.id,
369
+ "output": "Error: Failed to parse search parameters."
370
+ })
371
+
372
+ # Continue conversation with search results
373
+ messages.append(llm_response.choices[0].message)
374
+ for output_item in tool_outputs:
375
+ messages.append({
376
+ "role": "tool",
377
+ "tool_call_id": output_item["tool_call_id"],
378
+ "content": output_item["output"]
379
+ })
380
 
381
+ # Final response generation with search context
382
+ final_response = client.chat.completions.create(
383
+ model="unsloth/Qwen3-30B-A3B-GGUF",
384
+ temperature=temperature, # Use same temperature for consistency
385
+ messages=messages,
386
+ max_tokens=2000
387
+ )
388
+ final_chatbot_response = final_response.choices[0].message.content
389
+ else:
390
+ final_chatbot_response = llm_response.choices[0].message.content
391
 
392
+ # Enhanced response structure
393
+ response_data = {
394
+ "response": final_chatbot_response,
395
+ "sources": source_links,
396
+ "search_used": bool(tool_calls),
397
+ "temperature": temperature, # Include temperature in response for debugging
398
+ "timestamp": datetime.now().isoformat()
399
+ }
400
+
401
+ logger.info(f"Chat response generated successfully. Search used: {bool(tool_calls)}, Temperature: {temperature}")
402
+ return response_data
403
 
404
  except HTTPException:
405
  raise
 
410
  logger.error(f"Unexpected error in /chat endpoint: {e}")
411
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
412
 
 
413
  # --- Health Check Endpoint ---
414
  @app.get("/")
415
  async def root():