File size: 13,014 Bytes
17d4361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf96cb
17d4361
cdf96cb
 
 
17d4361
 
 
 
 
 
 
 
 
 
 
 
 
cdf96cb
 
17d4361
cdf96cb
17d4361
 
 
 
 
cdf96cb
 
 
 
 
 
 
 
 
 
 
 
17d4361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf96cb
17d4361
 
 
 
 
cdf96cb
17d4361
cdf96cb
17d4361
 
 
 
 
 
cdf96cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17d4361
cdf96cb
 
 
 
17d4361
 
cdf96cb
17d4361
 
cdf96cb
17d4361
cdf96cb
 
17d4361
 
cdf96cb
17d4361
 
 
 
cdf96cb
17d4361
 
 
cdf96cb
17d4361
 
 
 
 
 
 
 
 
cdf96cb
17d4361
 
 
 
 
 
 
 
cdf96cb
17d4361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf96cb
17d4361
 
 
 
 
 
 
 
cdf96cb
17d4361
 
 
cdf96cb
17d4361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf96cb
 
17d4361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdf96cb
17d4361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import re
import time
import torch
from sentence_transformers import SentenceTransformer
import chromadb
import google.generativeai as genai
import gradio as gr

# Load embedding model with GPU support (optimized for QA tasks)
embedder = SentenceTransformer('multi-qa-mpnet-base-dot-v1', device='cuda' if torch.cuda.is_available() else 'cpu')

# Configure Gemini API
genai.configure(api_key="AIzaSyDXG4o4UnII5VFD1u5TaWgleG2kCfJ0Ofw")

# Initialize Gemini model
gemini_instance = genai.GenerativeModel('gemini-2.0-flash')

# Define document paths and labels (using .md files)
doc_paths = [
    '1002215.md',
    'cancers-15-00321.md',
    'ijo-57-06-1245.md'
]
doc_labels = [
    "Early-stage triple negative breast cancer: the therapeutic role of immunotherapy and the prognostic value of pathological complete response",
    "Immunotherapy for Triple-Negative Breast Cancer: Combination Strategies to Improve Outcome",
    "Triple‑negative breast cancer therapy: Current and future perspectives (Review)"
]

def extract_and_chunk_docs(doc_paths, doc_labels):
    segmented_docs = []
    doc_info = []
    
    for doc_path, doc_label in zip(doc_paths, doc_labels):
        try:
            with open(doc_path, 'r', encoding='utf-8') as md_file:
                full_text = md_file.read()
            
            if not full_text.strip():
                print(f"No content extracted from {doc_label}")
                segmented_docs.append([])
                doc_info.append({"label": doc_label, "gemini_structure": "No content extracted"})
                continue

            # Identify pages based on '-----'
            pages = full_text.split('-----')
            full_text_lines = []
            for page_num, page_content in enumerate(pages, 1):
                lines = page_content.split('\n')
                for line in lines:
                    line = line.strip()
                    if line:
                        full_text_lines.append({'text': line, 'page': page_num})
            
            text_for_gemini = "\n".join([entry['text'] for entry in full_text_lines])
            prompt = f"""You are an expert in analyzing research papers. Given the following text from a Markdown file, identify all potential section titles (e.g., Abstract, Introduction, Methods, Results, Discussion) and subsections (e.g., '2.1 Data Analysis'). Include headings that might define or characterize triple-negative breast cancer (TNBC), such as 'Definition,' 'Characteristics,' or similar, and explicitly include 'References' as a section title if present. Only headings starting with '## **' are sections. Return only the titles (without '## **'), one per line, without explanation.
            Text:
            {text_for_gemini}
            """
            
            max_attempts = 5
            for attempt in range(max_attempts):
                try:
                    response = gemini_instance.generate_content(prompt)
                    titles = response.text.strip().split('\n')
                    break
                except Exception as e:
                    if "429" in str(e):
                        delay = 2 ** attempt
                        print(f"Rate limit hit for {doc_label}. Waiting {delay} seconds...")
                        time.sleep(delay)
                    else:
                        print(f"Error identifying titles for {doc_label}: {e}")
                        segmented_docs.append([])
                        doc_info.append({"label": doc_label, "gemini_structure": f"Error: {e}"})
                        break
            else:
                print(f"Error for {doc_label}: Max retries exceeded.")
                segmented_docs.append([])
                doc_info.append({"label": doc_label, "gemini_structure": "Max retries exceeded"})
                continue
            
            titles = list(dict.fromkeys([title.strip() for title in titles if title.strip()]))
            print(f"Gemini Identified Titles for {doc_label}: {titles}")
            
            chunks = []
            current_chunk = ""
            current_title = "Unknown"
            
            for line_info in full_text_lines:
                line = line_info['text']
                page_num = line_info['page']  # Use page number from full_text_lines
                
                cleaned_line = re.sub(r'(?i)copyright.*|all\s*rights\s*reserved', '', line)
                cleaned_line = re.sub(r'\s+', ' ', cleaned_line).strip()
                if not cleaned_line:
                    continue
                
                if cleaned_line.startswith('## **'):
                    normalized_line = re.sub(r'^##\s*\*\*|\*\*', '', cleaned_line).strip()
                    matched_title = next((title for title in titles if normalized_line.lower() == title.lower()), None)
                    
                    if matched_title:
                        # Save the previous chunk if it exists and is not "References"
                        if current_chunk and current_title.lower() != "references":
                            chunks.append({
                                'text': current_chunk.strip(),
                                'page': page_num,  # Assign the current page
                                'section': current_title
                            })
                        current_title = matched_title
                        current_chunk = ""
                        print(f"Detected title on page {page_num}: '{current_title}'")
                        continue
                
                # Only add to chunk if current section is not "References"
                if current_title.lower() != "references":
                    current_chunk += " " + cleaned_line
                    if len(current_chunk.split()) > 100:
                        chunks.append({
                            'text': current_chunk.strip(),
                            'page': page_num,  # Assign the current page
                            'section': current_title
                        })
                        current_chunk = ""
            
            # Save the final chunk if it exists and is not "References"
            if current_chunk and current_title.lower() != "references":
                chunks.append({
                    'text': current_chunk.strip(),
                    'page': page_num,  # Assign the final page
                    'section': current_title
                })
            
            segmented_docs.append(chunks)
            doc_info.append({"label": doc_label, "gemini_structure": "Chunked by titles starting with '## **', excluding 'References'"})
            
            print(f"\n=== {doc_label} ===")
            print("Identified Section Titles:", titles)
            print("\nChunks (excluding References):")
            for i, chunk in enumerate(chunks):
                print(f"Chunk {i + 1}: [Page: {chunk['page']}, Section: '{chunk['section']}'] {chunk['text'][:100]}...")
        
        except Exception as e:
            print(f"Error processing {doc_label}: {e}")
            segmented_docs.append([])
            doc_info.append({"label": doc_label, "gemini_structure": f"Error: {str(e)}"})
    
    return segmented_docs, doc_info

def compute_segment_embeddings(segmented_docs):
    segment_embeddings = []
    for doc_segments in segmented_docs:
        if doc_segments:
            embeddings = embedder.encode(
                [seg['text'] for seg in doc_segments],
                convert_to_tensor=False,
                show_progress_bar=True,
                batch_size=32
            )
            segment_embeddings.append(embeddings)
        else:
            segment_embeddings.append([])
    return segment_embeddings

def save_to_vector_store(segmented_docs, segment_embeddings, doc_labels):
    db_instance = chromadb.Client()
    try:
        embeddings_store = db_instance.create_collection("research_docs_mpnet_run_b")
    except:
        embeddings_store = db_instance.get_collection("research_docs_mpnet_run_b")
    
    for i, (doc_segments, doc_embeds) in enumerate(zip(segmented_docs, segment_embeddings)):
        if doc_segments and doc_embeds.size > 0:
            for j, (segment, embed) in enumerate(zip(doc_segments, doc_embeds)):
                embeddings_store.add(
                    embeddings=[embed.tolist()],
                    documents=[segment['text']],
                    metadatas=[{
                        "label": doc_labels[i],
                        "page": segment['page'],  # Include page in metadata
                        "section": segment['section']
                    }],
                    ids=[f"{doc_labels[i]}seg{j}"]
                )
    return embeddings_store

def process_query(query, embeddings_store, doc_labels):
    query_embed = embedder.encode([query], convert_to_tensor=False)[0].tolist()
    query_results = embeddings_store.query(query_embeddings=[query_embed], n_results=3)  # Limit to top 3
    
    retrieved_contexts = []
    ref_citations = []
    
    for doc, meta in zip(query_results["documents"][0], query_results["metadatas"][0]):
        label = meta["label"]
        page = meta["page"]
        section = meta["section"]
        retrieved_contexts.append(doc)
        ref_citations.append(f"[Ref: {label}, Page: {page}, Section: '{section}']")
    
    combined_context = "\n".join(retrieved_contexts) if retrieved_contexts else "No relevant context found."
    citation_str = " | ".join(ref_citations) if ref_citations else "N/A"
    
    answer_prompt = f"""You are an AI assistant for research papers. Use only the provided context to answer the query concisely (1-2 sentences max). If the context lacks a clear answer, state so briefly.
    Context:
    {combined_context}
    Query: {query}
    Answer:"""
    
    max_attempts = 5
    for attempt in range(max_attempts):
        try:
            response = gemini_instance.generate_content(answer_prompt)
            response_text = response.text.strip()
            break
        except Exception as e:
            if "429" in str(e):
                delay = 2 ** attempt
                print(f"Rate limit for query '{query}'. Waiting {delay} seconds...")
                time.sleep(delay)
            else:
                response_text = f"Error generating answer: {e}"
                break
    else:
        response_text = "Error: Max retries exceeded."
    
    final_response = f"{response_text}\n\n*References*: {citation_str}"
    return final_response

def chatbot_response(message, history):
    response = process_query(message, embeddings_store, doc_labels)
    return history + [{"role": "user", "content": message}, {"role": "assistant", "content": response}]

# Custom theme (unchanged)
custom_theme = gr.themes.Soft(
    primary_hue="blue",
    secondary_hue="gray",
    neutral_hue="slate",
    text_size="lg",
    spacing_size="md",
    radius_size="lg",
).set(
    body_background_fill="#f0f4f8",
    body_text_color="#1e293b",
    input_background_fill="#ffffff",
    input_border_color="#cbd5e1",
    input_shadow="0 2px 4px rgba(0,0,0,0.1)",
    button_primary_background_fill="#3b82f6",
    button_primary_text_color="#ffffff",
    button_primary_background_fill_hover="#2563eb",
    block_title_text_color="#1e40af",
    block_border_color="#e2e8f0",
    block_background_fill="#ffffff",
)

# Initialize system
doc_segments, doc_metadata = extract_and_chunk_docs(doc_paths, doc_labels)
segment_embeds = compute_segment_embeddings(doc_segments)
embeddings_store = save_to_vector_store(doc_segments, segment_embeds, doc_labels)

# Custom CSS (simplified, no debug styling)
css = """
.header { text-align: center; margin-bottom: 20px; }
.gradio-container { max-width: 900px; margin: auto; }
.chatbot .prose { max-width: 100%; }
.chatbot .bubble-wrap:nth-child(even) { background-color: #dbeafe; color: #1e40af; }
.chatbot .bubble-wrap:nth-child(odd) { background-color: #f1f5f9; color: #1e293b; }
.chatbot .bubble { border-radius: 10px; padding: 10px; }
"""

# Gradio interface
with gr.Blocks(theme=custom_theme, css=css, title="Research Paper Chatbot") as interface:
    gr.Markdown("# Research Paper Chatbot\nAsk questions about the research papers and get concise, referenced answers.", elem_classes="header")
    chatbot = gr.Chatbot(label="Conversation", height=500, type="messages", avatar_images=(None, "https://png.pngtree.com/png-vector/20201224/ourmid/pngtree-future-intelligent-technology-robot-ai-png-image_2588803.jpg"))
    with gr.Row():
        with gr.Column(scale=8):
            msg = gr.Textbox(placeholder="Type your question here", show_label=False, container=False)
        with gr.Column(scale=2):
            submit_btn = gr.Button("Send", variant="primary")
    msg.submit(chatbot_response, [msg, chatbot], chatbot).then(lambda: "", None, msg)
    submit_btn.click(chatbot_response, [msg, chatbot], chatbot).then(lambda: "", None, msg)

if __name__ == "__main__":
    interface.launch()