Update app.py
Browse files
app.py
CHANGED
|
@@ -38,7 +38,8 @@ import httpx
|
|
| 38 |
|
| 39 |
# ===== CONFIG =====
|
| 40 |
KNOWLEDGE_DIR = "knowledge"
|
| 41 |
-
|
|
|
|
| 42 |
DB_PATH = "/data/analytics.db" if os.path.exists("/data") else "analytics.db"
|
| 43 |
CHUNK_SIZE = 800
|
| 44 |
CHUNK_OVERLAP = 100
|
|
@@ -113,8 +114,11 @@ def set_config(key, value):
|
|
| 113 |
|
| 114 |
|
| 115 |
# ===== ADMIN AUTH =====
|
| 116 |
-
#
|
| 117 |
-
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "
|
|
|
|
|
|
|
|
|
|
| 118 |
admin_sessions = {} # token -> expiry timestamp
|
| 119 |
|
| 120 |
def create_session():
|
|
@@ -309,15 +313,19 @@ async def tool_search_pubmed(query, limit=5):
|
|
| 309 |
return {"error": f"PubMed: {str(e)}", "results": [], "source": "PubMed"}
|
| 310 |
|
| 311 |
|
| 312 |
-
# ===== TOOL: SEARCH CONSENSUS (
|
| 313 |
async def tool_search_consensus(query, limit=5):
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
|
| 323 |
# ===== TOOL: SEARCH SEMANTIC SCHOLAR (direct API — free, no auth) =====
|
|
@@ -371,8 +379,7 @@ async def tool_search_scholar(query, limit=5):
|
|
| 371 |
except Exception as e:
|
| 372 |
return {"error": f"Semantic Scholar: {str(e)}", "results": [], "source": "Semantic Scholar"}
|
| 373 |
|
| 374 |
-
|
| 375 |
-
async def tool_library_info(question, history=None):
|
| 376 |
if not vectorstore:
|
| 377 |
return {"answer": "Knowledge base not initialized.", "sources": []}
|
| 378 |
|
|
@@ -397,7 +404,14 @@ Question: {question}
|
|
| 397 |
|
| 398 |
Answer:"""
|
| 399 |
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
response = llm.invoke(prompt)
|
| 402 |
|
| 403 |
sources = []
|
|
@@ -515,14 +529,13 @@ async def search(req: SearchRequest):
|
|
| 515 |
result = {"error": f"Unknown source: {source}", "results": []}
|
| 516 |
|
| 517 |
elapsed = time.time() - start
|
| 518 |
-
|
| 519 |
result["response_time"] = round(elapsed, 2)
|
| 520 |
result["is_medical"] = is_medical_topic(req.query)
|
| 521 |
return result
|
| 522 |
|
| 523 |
except Exception as e:
|
| 524 |
elapsed = time.time() - start
|
| 525 |
-
log_query(req.query, source, req.model, elapsed, 0, str(e))
|
| 526 |
return {"error": str(e), "results": [], "response_time": round(elapsed, 2)}
|
| 527 |
|
| 528 |
|
|
@@ -547,9 +560,9 @@ async def rag_query(req: RAGRequest):
|
|
| 547 |
start = time.time()
|
| 548 |
try:
|
| 549 |
history = [{"role": m.role, "content": m.content} for m in req.history] if req.history else None
|
| 550 |
-
result = await tool_library_info(req.question, history)
|
| 551 |
elapsed = time.time() - start
|
| 552 |
-
|
| 553 |
return {
|
| 554 |
"answer": result["answer"],
|
| 555 |
"sources": result["sources"],
|
|
@@ -558,7 +571,6 @@ async def rag_query(req: RAGRequest):
|
|
| 558 |
}
|
| 559 |
except Exception as e:
|
| 560 |
elapsed = time.time() - start
|
| 561 |
-
log_query(req.question, "rag", req.model, elapsed, 0, str(e))
|
| 562 |
return {"answer": "Error processing your question.", "sources": [], "error": str(e)}
|
| 563 |
|
| 564 |
|
|
|
|
| 38 |
|
| 39 |
# ===== CONFIG =====
|
| 40 |
KNOWLEDGE_DIR = "knowledge"
|
| 41 |
+
# Use /data for persistence across HF Space restarts — fall back to local if /data unavailable
|
| 42 |
+
FAISS_INDEX_PATH = "/data/faiss_index" if os.path.exists("/data") else "faiss_index"
|
| 43 |
DB_PATH = "/data/analytics.db" if os.path.exists("/data") else "analytics.db"
|
| 44 |
CHUNK_SIZE = 800
|
| 45 |
CHUNK_OVERLAP = 100
|
|
|
|
| 114 |
|
| 115 |
|
| 116 |
# ===== ADMIN AUTH =====
|
| 117 |
+
# ADMIN_PASSWORD must be set as HF Space Secret — no insecure fallback
|
| 118 |
+
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
|
| 119 |
+
if not ADMIN_PASSWORD:
|
| 120 |
+
import warnings
|
| 121 |
+
warnings.warn("ADMIN_PASSWORD secret is not set. Admin dashboard will be inaccessible until configured.")
|
| 122 |
admin_sessions = {} # token -> expiry timestamp
|
| 123 |
|
| 124 |
def create_session():
|
|
|
|
| 313 |
return {"error": f"PubMed: {str(e)}", "results": [], "source": "PubMed"}
|
| 314 |
|
| 315 |
|
| 316 |
+
# ===== TOOL: SEARCH CONSENSUS (via Semantic Scholar with consensus framing) =====
|
| 317 |
async def tool_search_consensus(query, limit=5):
|
| 318 |
+
"""
|
| 319 |
+
Consensus.app requires OAuth so we can't call it directly.
|
| 320 |
+
Instead we search Semantic Scholar with the same query and return
|
| 321 |
+
results alongside a direct Consensus deep-link so the user can
|
| 322 |
+
also check the AI-synthesized answer there.
|
| 323 |
+
"""
|
| 324 |
+
scholar = await tool_search_scholar(query, limit)
|
| 325 |
+
scholar["source"] = "Consensus / Semantic Scholar"
|
| 326 |
+
scholar["consensus_url"] = f"https://consensus.app/results/?q={query}"
|
| 327 |
+
scholar["message"] = "Results from Semantic Scholar. For AI-synthesized consensus, click the Consensus link."
|
| 328 |
+
return scholar
|
| 329 |
|
| 330 |
|
| 331 |
# ===== TOOL: SEARCH SEMANTIC SCHOLAR (direct API — free, no auth) =====
|
|
|
|
| 379 |
except Exception as e:
|
| 380 |
return {"error": f"Semantic Scholar: {str(e)}", "results": [], "source": "Semantic Scholar"}
|
| 381 |
|
| 382 |
+
async def tool_library_info(question, history=None, model="gpt"):
|
|
|
|
| 383 |
if not vectorstore:
|
| 384 |
return {"answer": "Knowledge base not initialized.", "sources": []}
|
| 385 |
|
|
|
|
| 404 |
|
| 405 |
Answer:"""
|
| 406 |
|
| 407 |
+
# Respect the model selection — use Claude if requested, GPT otherwise
|
| 408 |
+
use_claude = model == "claude" and os.environ.get("ANTHROPIC_API_KEY")
|
| 409 |
+
if use_claude:
|
| 410 |
+
from langchain_anthropic import ChatAnthropic
|
| 411 |
+
llm = ChatAnthropic(model="claude-haiku-4-5-20251001", temperature=0.2, max_tokens=500)
|
| 412 |
+
else:
|
| 413 |
+
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2, max_tokens=500)
|
| 414 |
+
|
| 415 |
response = llm.invoke(prompt)
|
| 416 |
|
| 417 |
sources = []
|
|
|
|
| 529 |
result = {"error": f"Unknown source: {source}", "results": []}
|
| 530 |
|
| 531 |
elapsed = time.time() - start
|
| 532 |
+
# Logging handled by Cloudflare D1 via worker /log endpoint (single source of truth)
|
| 533 |
result["response_time"] = round(elapsed, 2)
|
| 534 |
result["is_medical"] = is_medical_topic(req.query)
|
| 535 |
return result
|
| 536 |
|
| 537 |
except Exception as e:
|
| 538 |
elapsed = time.time() - start
|
|
|
|
| 539 |
return {"error": str(e), "results": [], "response_time": round(elapsed, 2)}
|
| 540 |
|
| 541 |
|
|
|
|
| 560 |
start = time.time()
|
| 561 |
try:
|
| 562 |
history = [{"role": m.role, "content": m.content} for m in req.history] if req.history else None
|
| 563 |
+
result = await tool_library_info(req.question, history, model=req.model)
|
| 564 |
elapsed = time.time() - start
|
| 565 |
+
# Logging handled by Cloudflare D1 via worker /log endpoint
|
| 566 |
return {
|
| 567 |
"answer": result["answer"],
|
| 568 |
"sources": result["sources"],
|
|
|
|
| 571 |
}
|
| 572 |
except Exception as e:
|
| 573 |
elapsed = time.time() - start
|
|
|
|
| 574 |
return {"answer": "Error processing your question.", "sources": [], "error": str(e)}
|
| 575 |
|
| 576 |
|