|
a |
|
b/breast-cancer-rag-app/backend/main.py |
|
|
1 |
import os |
|
|
2 |
import json |
|
|
3 |
from fastapi import FastAPI, HTTPException |
|
|
4 |
from pydantic import BaseModel |
|
|
5 |
from azure.identity import DefaultAzureCredential |
|
|
6 |
from azure.kusto.data import KustoClient, KustoConnectionStringBuilder |
|
|
7 |
from openai import AzureOpenAI |
|
|
8 |
from tenacity import retry, wait_random_exponential, stop_after_attempt |
|
|
9 |
from fastapi.middleware.cors import CORSMiddleware |
|
|
10 |
|
|
|
11 |
app = FastAPI() |
|
|
12 |
|
|
|
13 |
# Add CORS middleware |
|
|
14 |
app.add_middleware( |
|
|
15 |
CORSMiddleware, |
|
|
16 |
allow_origins=["*"], |
|
|
17 |
allow_methods=["*"], |
|
|
18 |
allow_headers=["*"], |
|
|
19 |
) |
|
|
20 |
|
|
|
21 |
# Configuration |
|
|
22 |
OPENAI_GPT4_DEPLOYMENT = os.getenv("OPENAI_GPT4_DEPLOYMENT") |
|
|
23 |
OPENAI_ENDPOINT = os.getenv("OPENAI_ENDPOINT") |
|
|
24 |
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") |
|
|
25 |
OPENAI_ADA_DEPLOYMENT = os.getenv("OPENAI_ADA_DEPLOYMENT") |
|
|
26 |
|
|
|
27 |
KUSTO_URI = os.getenv("KUSTO_URI") |
|
|
28 |
KUSTO_DATABASE = os.getenv("KUSTO_DATABASE") |
|
|
29 |
KUSTO_TABLE = os.getenv("KUSTO_TABLE") |
|
|
30 |
|
|
|
31 |
# Azure Authentication |
|
|
32 |
credential = DefaultAzureCredential() |
|
|
33 |
|
|
|
34 |
# # Kusto Client |
|
|
35 |
# kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication( |
|
|
36 |
# KUSTO_URI, |
|
|
37 |
# application_client_id=os.getenv("AZURE_CLIENT_ID"), |
|
|
38 |
# application_key=os.getenv("AZURE_CLIENT_SECRET"), |
|
|
39 |
# authority_id=os.getenv("AZURE_TENANT_ID") |
|
|
40 |
# ) |
|
|
41 |
|
|
|
42 |
# Use this universal approach that works with most versions |
|
|
43 |
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication( |
|
|
44 |
KUSTO_URI, |
|
|
45 |
os.getenv("AZURE_CLIENT_ID"), |
|
|
46 |
os.getenv("AZURE_CLIENT_SECRET"), |
|
|
47 |
os.getenv("AZURE_TENANT_ID") |
|
|
48 |
) |
|
|
49 |
|
|
|
50 |
kusto_client = KustoClient(kcsb) |
|
|
51 |
|
|
|
52 |
# OpenAI Client |
|
|
53 |
openai_client = AzureOpenAI( |
|
|
54 |
azure_endpoint=OPENAI_ENDPOINT, |
|
|
55 |
api_key=OPENAI_API_KEY, |
|
|
56 |
api_version="2023-09-01-preview" |
|
|
57 |
) |
|
|
58 |
|
|
|
59 |
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6)) |
|
|
60 |
def generate_embeddings(text: str) -> list: |
|
|
61 |
try: |
|
|
62 |
response = openai_client.embeddings.create( |
|
|
63 |
input=[text.replace("\n", " ")], |
|
|
64 |
model=OPENAI_ADA_DEPLOYMENT |
|
|
65 |
) |
|
|
66 |
return response.data[0].embedding |
|
|
67 |
except Exception as e: |
|
|
68 |
print(f"Embedding generation failed: {str(e)}") |
|
|
69 |
return None |
|
|
70 |
|
|
|
71 |
def execute_kusto_query(query: str) -> list: |
|
|
72 |
try: |
|
|
73 |
response = kusto_client.execute(KUSTO_DATABASE, query) |
|
|
74 |
return [row.to_dict() for row in response.primary_results[0]] |
|
|
75 |
except Exception as e: |
|
|
76 |
print(f"Kusto query failed: {str(e)}") |
|
|
77 |
return [] |
|
|
78 |
|
|
|
79 |
def query_biospecimen_data(question: str, top_results: int = 3) -> dict: |
|
|
80 |
embedding = generate_embeddings(question) |
|
|
81 |
if not embedding: |
|
|
82 |
return {"answer": "Embedding generation failed", "sources": []} |
|
|
83 |
|
|
|
84 |
embedding_str = "[" + ",".join(map(str, embedding)) + "]" |
|
|
85 |
kusto_query = f""" |
|
|
86 |
{KUSTO_TABLE} |
|
|
87 |
| extend similarity = cosine_similarity(embedding, dynamic({embedding_str})) |
|
|
88 |
| top {top_results} by similarity desc |
|
|
89 |
| project content, metadata, similarity |
|
|
90 |
""" |
|
|
91 |
|
|
|
92 |
results = execute_kusto_query(kusto_query) |
|
|
93 |
if not results: |
|
|
94 |
return {"answer": "No relevant records found", "sources": []} |
|
|
95 |
|
|
|
96 |
context = "\n".join([ |
|
|
97 |
f"Record {idx+1} (Similarity: {row['similarity']:.2f}):\n" |
|
|
98 |
f"Content: {row['content']}\n" |
|
|
99 |
f"Metadata: {json.dumps(row['metadata'])}\n" |
|
|
100 |
for idx, row in enumerate(results) |
|
|
101 |
]) |
|
|
102 |
|
|
|
103 |
try: |
|
|
104 |
response = openai_client.chat.completions.create( |
|
|
105 |
model=OPENAI_GPT4_DEPLOYMENT, |
|
|
106 |
messages=[ |
|
|
107 |
{"role": "system", "content": "You are a biomedical research assistant. " |
|
|
108 |
"Use only the provided context to answer questions about breast cancer biospecimens."}, |
|
|
109 |
{"role": "user", "content": f"Question: {question}\nContext:\n{context}"} |
|
|
110 |
], |
|
|
111 |
temperature=0.2, |
|
|
112 |
max_tokens=500 |
|
|
113 |
) |
|
|
114 |
answer = response.choices[0].message.content |
|
|
115 |
except Exception as e: |
|
|
116 |
answer = f"LLM processing error: {str(e)}" |
|
|
117 |
|
|
|
118 |
return { |
|
|
119 |
"answer": answer, |
|
|
120 |
"sources": [{ |
|
|
121 |
"content": r["content"], |
|
|
122 |
"metadata": r["metadata"], |
|
|
123 |
"similarity": round(r["similarity"], 2) |
|
|
124 |
} for r in results], |
|
|
125 |
"processing_time": f"{len(results)} results analyzed" |
|
|
126 |
} |
|
|
127 |
|
|
|
128 |
class QueryRequest(BaseModel): |
|
|
129 |
question: str |
|
|
130 |
top_results: int = 3 |
|
|
131 |
|
|
|
132 |
@app.post("/query") |
|
|
133 |
async def handle_query(request: QueryRequest): |
|
|
134 |
try: |
|
|
135 |
result = query_biospecimen_data(request.question, request.top_results) |
|
|
136 |
return result |
|
|
137 |
except Exception as e: |
|
|
138 |
raise HTTPException(status_code=500, detail=str(e)) |
|
|
139 |
|
|
|
140 |
@app.get("/health") |
|
|
141 |
async def health_check(): |
|
|
142 |
return {"status": "healthy", "service": "biospecimen-query"} |
|
|
143 |
|
|
|
144 |
if __name__ == "__main__": |
|
|
145 |
import uvicorn |
|
|
146 |
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 8000))) |