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)))