Lawrence121 commited on
Commit
5e03436
·
1 Parent(s): 446aa93

feat: switch to Groq LLaMA 3 free API

Browse files
Files changed (2) hide show
  1. app.py +131 -586
  2. requirements.txt +4 -3
app.py CHANGED
@@ -4,31 +4,24 @@ import tempfile
4
  import gc
5
  import base64
6
  import time
7
- import json
8
  from datetime import datetime
9
 
10
  from crewai import Agent, Crew, Process, Task
11
  from crewai_tools import SerperDevTool
 
12
  from src.agentic_rag.tools.custom_tool import DocumentSearchTool
13
 
14
- # ===========================
15
- # Page Config — must be first
16
- # ===========================
17
  st.set_page_config(
18
  page_title="NeuralDocs — Agentic RAG",
19
- page_icon="⬡",
20
  layout="wide",
21
  initial_sidebar_state="expanded"
22
  )
23
 
24
- # ===========================
25
- # Global CSS — Unified Dark Theme
26
- # ===========================
27
  st.markdown("""
28
  <style>
29
  @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&family=Figtree:wght@300;400;500;600&display=swap');
30
 
31
- /* ── ROOT TOKENS ── */
32
  :root {
33
  --bg-base: #080c12;
34
  --bg-surface: #0d1520;
@@ -53,29 +46,23 @@ st.markdown("""
53
  --radius-sm: 6px;
54
  --radius-md: 12px;
55
  --radius-lg: 18px;
56
- --shadow-card: 0 4px 24px rgba(0,0,0,0.4);
57
- --shadow-glow: 0 0 32px rgba(0,229,255,0.08);
58
  }
59
 
60
- /* ── GLOBAL RESET ── */
61
  html, body, [class*="css"] {
62
  font-family: var(--font-body);
63
  background-color: var(--bg-base) !important;
64
  color: var(--text-primary) !important;
65
  }
66
 
67
- /* ── HIDE STREAMLIT CHROME ── */
68
  #MainMenu, footer, header { visibility: hidden; }
69
  .stDeployButton { display: none; }
70
 
71
- /* ── MAIN CONTAINER ── */
72
  .main .block-container {
73
  background: var(--bg-base);
74
  padding: 1.5rem 2rem 3rem;
75
  max-width: 1100px;
76
  }
77
 
78
- /* ── APP HEADER ── */
79
  .app-header {
80
  display: flex;
81
  align-items: center;
@@ -89,43 +76,27 @@ html, body, [class*="css"] {
89
  background: linear-gradient(135deg, var(--accent), var(--accent2));
90
  border-radius: 10px;
91
  display: flex; align-items: center; justify-content: center;
92
- font-size: 18px; font-weight: 800;
93
- color: #000;
94
- flex-shrink: 0;
95
- box-shadow: 0 0 20px var(--accent-glow);
96
  }
97
  .app-title {
98
  font-family: var(--font-display);
99
- font-size: 1.5rem;
100
- font-weight: 800;
101
- letter-spacing: -0.5px;
102
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%);
103
- -webkit-background-clip: text;
104
- -webkit-text-fill-color: transparent;
105
- background-clip: text;
106
  line-height: 1;
107
  }
108
  .app-subtitle {
109
- font-size: 0.7rem;
110
- color: var(--text-muted);
111
- letter-spacing: 2px;
112
- text-transform: uppercase;
113
- font-family: var(--font-mono);
114
- margin-top: 3px;
115
  }
116
  .header-badge {
117
  margin-left: auto;
118
- background: var(--accent-glow);
119
- border: 1px solid var(--accent-dim);
120
- color: var(--accent);
121
- font-family: var(--font-mono);
122
- font-size: 0.65rem;
123
- padding: 4px 10px;
124
- border-radius: 20px;
125
- letter-spacing: 1px;
126
  }
127
 
128
- /* ── SIDEBAR ── */
129
  [data-testid="stSidebar"] {
130
  background: var(--bg-surface) !important;
131
  border-right: 1px solid var(--border) !important;
@@ -135,377 +106,98 @@ html, body, [class*="css"] {
135
  padding: 1.5rem 1.2rem;
136
  }
137
  .sidebar-section-label {
138
- font-family: var(--font-mono);
139
- font-size: 0.6rem;
140
- letter-spacing: 3px;
141
- text-transform: uppercase;
142
- color: var(--text-muted);
143
- margin: 1.2rem 0 0.6rem;
144
- padding-bottom: 0.4rem;
145
- border-bottom: 1px solid var(--border);
146
- }
147
- .sidebar-title {
148
- font-family: var(--font-display);
149
- font-size: 1.1rem;
150
- font-weight: 700;
151
- color: var(--text-primary);
152
- margin-bottom: 0.2rem;
153
  }
154
 
155
- /* ── STATUS PILLS ── */
156
  .status-pill {
157
- display: inline-flex;
158
- align-items: center;
159
- gap: 6px;
160
- padding: 5px 12px;
161
- border-radius: 20px;
162
- font-family: var(--font-mono);
163
- font-size: 0.7rem;
164
- letter-spacing: 0.5px;
165
- margin: 6px 0;
166
  }
167
- .status-ready { background: rgba(0,200,150,0.1); border: 1px solid var(--success); color: var(--success); }
168
- .status-waiting { background: rgba(255,183,0,0.1); border: 1px solid var(--warning); color: var(--warning); }
169
- .status-error { background: rgba(255,77,109,0.1); border: 1px solid var(--danger); color: var(--danger); }
170
  .status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
171
  .status-dot.pulse { animation: pulse 1.5s infinite; }
172
  @keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
173
 
174
- /* ── METRIC CARDS ── */
175
- .metrics-row {
176
- display: grid;
177
- grid-template-columns: 1fr 1fr;
178
- gap: 8px;
179
- margin: 10px 0;
180
- }
181
- .metric-card {
182
- background: var(--bg-elevated);
183
- border: 1px solid var(--border);
184
- border-radius: var(--radius-md);
185
- padding: 10px 14px;
186
- }
187
- .metric-value {
188
- font-family: var(--font-display);
189
- font-size: 1.3rem;
190
- font-weight: 700;
191
- color: var(--accent);
192
- line-height: 1;
193
- }
194
- .metric-label {
195
- font-family: var(--font-mono);
196
- font-size: 0.6rem;
197
- color: var(--text-muted);
198
- letter-spacing: 1px;
199
- text-transform: uppercase;
200
- margin-top: 3px;
201
- }
202
 
203
- /* ── SOURCE BADGE ── */
204
  .source-badge {
205
- display: inline-flex;
206
- align-items: center;
207
- gap: 5px;
208
- background: var(--bg-elevated);
209
- border: 1px solid var(--border);
210
- border-radius: 4px;
211
- padding: 2px 8px;
212
- font-family: var(--font-mono);
213
- font-size: 0.65rem;
214
- color: var(--text-secondary);
215
- margin: 2px 3px;
216
  }
217
- .source-badge.pdf { border-color: var(--accent2); color: #b39ddb; background: rgba(124,92,252,0.08); }
218
- .source-badge.web { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); }
219
 
220
- /* ── CHAT MESSAGES ── */
221
  .stChatMessage {
222
- background: var(--bg-surface) !important;
223
- border: 1px solid var(--border) !important;
224
- border-radius: var(--radius-lg) !important;
225
- padding: 1rem 1.2rem !important;
226
- margin-bottom: 10px !important;
227
- }
228
- [data-testid="stChatMessageContent"] p {
229
- font-family: var(--font-body);
230
- font-size: 0.9rem;
231
- line-height: 1.7;
232
- color: var(--text-primary);
233
- }
234
- [data-testid="stChatMessageContent"] code {
235
- font-family: var(--font-mono);
236
- background: var(--bg-elevated);
237
- border: 1px solid var(--border);
238
- border-radius: 4px;
239
- padding: 1px 6px;
240
- font-size: 0.82rem;
241
- color: var(--accent);
242
  }
 
243
 
244
- /* ── FOLLOW-UP QUESTION CHIPS ── */
245
- .followup-container {
246
- margin: 12px 0 6px;
247
- }
248
- .followup-label {
249
- font-family: var(--font-mono);
250
- font-size: 0.6rem;
251
- letter-spacing: 2px;
252
- text-transform: uppercase;
253
- color: var(--text-muted);
254
- margin-bottom: 6px;
255
- }
256
- .followup-chips {
257
- display: flex;
258
- flex-wrap: wrap;
259
- gap: 6px;
260
- }
261
- .followup-chip {
262
- background: var(--bg-elevated);
263
- border: 1px solid var(--border-bright);
264
- border-radius: 20px;
265
- padding: 5px 12px;
266
- font-size: 0.78rem;
267
- color: var(--text-secondary);
268
- cursor: pointer;
269
- transition: all 0.2s;
270
- font-family: var(--font-body);
271
- }
272
- .followup-chip:hover {
273
- border-color: var(--accent);
274
- color: var(--accent);
275
- background: var(--accent-glow);
276
- }
277
-
278
- /* ── ANSWER METADATA BAR ── */
279
  .answer-meta {
280
- display: flex;
281
- align-items: center;
282
- gap: 12px;
283
- padding: 8px 0 4px;
284
- border-top: 1px solid var(--border);
285
- margin-top: 10px;
286
- flex-wrap: wrap;
287
- }
288
- .answer-meta-item {
289
- display: flex;
290
- align-items: center;
291
- gap: 4px;
292
- font-family: var(--font-mono);
293
- font-size: 0.65rem;
294
- color: var(--text-muted);
295
- }
296
- .confidence-bar-wrap {
297
- display: flex;
298
- align-items: center;
299
- gap: 6px;
300
- }
301
- .confidence-bar {
302
- width: 60px;
303
- height: 4px;
304
- background: var(--border);
305
- border-radius: 2px;
306
- overflow: hidden;
307
- }
308
- .confidence-fill {
309
- height: 100%;
310
- border-radius: 2px;
311
- background: linear-gradient(90deg, var(--accent2), var(--accent));
312
  }
 
 
 
 
313
 
314
- /* ── CHAT INPUT ── */
315
  [data-testid="stChatInput"] {
316
- background: var(--bg-elevated) !important;
317
- border: 1px solid var(--border-bright) !important;
318
- border-radius: var(--radius-lg) !important;
319
- }
320
- [data-testid="stChatInput"]:focus-within {
321
- border-color: var(--accent) !important;
322
- box-shadow: 0 0 0 3px var(--accent-glow) !important;
323
- }
324
- [data-testid="stChatInput"] textarea {
325
- color: var(--text-primary) !important;
326
- font-family: var(--font-body) !important;
327
- font-size: 0.9rem !important;
328
- background: transparent !important;
329
  }
 
 
330
 
331
- /* ── BUTTONS ── */
332
  .stButton > button {
333
- background: var(--bg-elevated) !important;
334
- border: 1px solid var(--border-bright) !important;
335
- color: var(--text-secondary) !important;
336
- border-radius: var(--radius-sm) !important;
337
- font-family: var(--font-mono) !important;
338
- font-size: 0.72rem !important;
339
- letter-spacing: 0.5px !important;
340
- transition: all 0.2s !important;
341
- padding: 0.35rem 0.8rem !important;
342
- }
343
- .stButton > button:hover {
344
- border-color: var(--accent) !important;
345
- color: var(--accent) !important;
346
- background: var(--accent-glow) !important;
347
  }
 
348
 
349
- /* ── FILE UPLOADER ── */
350
  [data-testid="stFileUploader"] {
351
- background: var(--bg-elevated) !important;
352
- border: 1px dashed var(--border-bright) !important;
353
- border-radius: var(--radius-md) !important;
354
- padding: 8px !important;
355
- }
356
- [data-testid="stFileUploader"]:hover {
357
- border-color: var(--accent) !important;
358
- }
359
- [data-testid="stFileUploader"] label {
360
- color: var(--text-secondary) !important;
361
- font-size: 0.8rem !important;
362
  }
363
 
364
- /* ── SPINNER ── */
365
- .stSpinner > div {
366
- border-top-color: var(--accent) !important;
 
367
  }
 
 
368
 
369
- /* ── DIVIDER ── */
370
- hr { border-color: var(--border) !important; }
 
371
 
372
- /* ── SCROLLBAR ── */
373
  ::-webkit-scrollbar { width: 4px; }
374
  ::-webkit-scrollbar-track { background: var(--bg-base); }
375
  ::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
376
- ::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); }
377
-
378
- /* ── EMPTY STATE ── */
379
- .empty-state {
380
- display: flex;
381
- flex-direction: column;
382
- align-items: center;
383
- justify-content: center;
384
- padding: 3rem 1rem;
385
- text-align: center;
386
- gap: 12px;
387
- }
388
- .empty-icon {
389
- font-size: 2.5rem;
390
- opacity: 0.3;
391
- }
392
- .empty-text {
393
- font-family: var(--font-body);
394
- color: var(--text-muted);
395
- font-size: 0.9rem;
396
- max-width: 320px;
397
- line-height: 1.6;
398
- }
399
- .empty-hint {
400
- font-family: var(--font-mono);
401
- font-size: 0.68rem;
402
- color: var(--text-muted);
403
- letter-spacing: 1px;
404
- }
405
-
406
- /* ── SUGGESTION PROMPTS ── */
407
- .suggestion-grid {
408
- display: grid;
409
- grid-template-columns: 1fr 1fr;
410
- gap: 8px;
411
- margin-top: 1rem;
412
- max-width: 560px;
413
- }
414
- .suggestion-card {
415
- background: var(--bg-surface);
416
- border: 1px solid var(--border);
417
- border-radius: var(--radius-md);
418
- padding: 10px 14px;
419
- cursor: pointer;
420
- transition: all 0.2s;
421
- }
422
- .suggestion-card:hover {
423
- border-color: var(--accent);
424
- background: var(--accent-glow);
425
- }
426
- .suggestion-icon { font-size: 1rem; margin-bottom: 4px; }
427
- .suggestion-text {
428
- font-size: 0.78rem;
429
- color: var(--text-secondary);
430
- font-family: var(--font-body);
431
- line-height: 1.4;
432
- }
433
-
434
- /* ── HISTORY ITEM ── */
435
- .history-item {
436
- background: var(--bg-elevated);
437
- border: 1px solid var(--border);
438
- border-left: 3px solid var(--accent-dim);
439
- border-radius: var(--radius-sm);
440
- padding: 8px 10px;
441
- margin: 4px 0;
442
- cursor: pointer;
443
- transition: all 0.15s;
444
- }
445
- .history-item:hover {
446
- border-left-color: var(--accent);
447
- background: var(--bg-hover);
448
- }
449
- .history-q {
450
- font-size: 0.78rem;
451
- color: var(--text-secondary);
452
- white-space: nowrap;
453
- overflow: hidden;
454
- text-overflow: ellipsis;
455
- font-family: var(--font-body);
456
- }
457
- .history-time {
458
- font-family: var(--font-mono);
459
- font-size: 0.6rem;
460
- color: var(--text-muted);
461
- margin-top: 2px;
462
- }
463
-
464
- /* ── SUCCESS / INFO ALERTS ── */
465
- .stSuccess, .stInfo, .stWarning {
466
- background: var(--bg-elevated) !important;
467
- border-radius: var(--radius-md) !important;
468
- }
469
-
470
- /* ── SELECTBOX ── */
471
- [data-testid="stSelectbox"] > div {
472
- background: var(--bg-elevated) !important;
473
- border: 1px solid var(--border-bright) !important;
474
- border-radius: var(--radius-sm) !important;
475
- color: var(--text-primary) !important;
476
- }
477
-
478
- /* ── PROGRESS ── */
479
- .stProgress > div > div {
480
- background: linear-gradient(90deg, var(--accent2), var(--accent)) !important;
481
- }
482
  </style>
483
  """, unsafe_allow_html=True)
484
 
485
-
486
- # ===========================
487
- # Session State Init
488
- # ===========================
489
  defaults = {
490
- "messages": [],
491
- "pdf_tool": None,
492
- "crew": None,
493
- "pdf_name": None,
494
- "total_queries": 0,
495
- "pdf_hits": 0,
496
- "web_hits": 0,
497
- "query_log": [], # [{q, ts, source}]
498
- "pending_followups": [], # follow-up suggestions for latest answer
499
- "pending_prompt": None, # queued prompt from chip click
500
  }
501
  for k, v in defaults.items():
502
  if k not in st.session_state:
503
  st.session_state[k] = v
504
 
505
-
506
- # ===========================
507
- # Helpers
508
- # ===========================
509
  def reset_chat():
510
  st.session_state.messages = []
511
  st.session_state.pending_followups = []
@@ -516,128 +208,85 @@ def full_reset():
516
  st.session_state[k] = defaults[k]
517
  gc.collect()
518
 
519
- def display_pdf(file_bytes: bytes, file_name: str):
520
  b64 = base64.b64encode(file_bytes).decode("utf-8")
521
- st.markdown(f"""
522
- <iframe src="data:application/pdf;base64,{b64}"
523
- width="100%" height="480px" type="application/pdf"
524
- style="border:1px solid var(--border); border-radius:var(--radius-md); background:var(--bg-elevated);">
525
- </iframe>
526
- """, unsafe_allow_html=True)
527
 
528
- def detect_source(result_text: str) -> str:
529
- """Heuristic: guess whether answer came primarily from PDF or web."""
530
  web_signals = ["according to", "website", "http", "search result", "online", "found on"]
531
- if any(s in result_text.lower() for s in web_signals):
532
- return "web"
533
- return "pdf"
534
-
535
- def estimate_confidence(result_text: str) -> int:
536
- """Simple heuristic confidence 0–100."""
537
- if "i'm sorry" in result_text.lower() or "couldn't find" in result_text.lower():
538
- return 20
539
- if len(result_text) > 400:
540
- return 88
541
- if len(result_text) > 150:
542
- return 72
543
  return 55
544
 
545
- def generate_followups(query: str, answer: str) -> list[str]:
546
- """Generate 3 contextual follow-up questions from the answer."""
547
- lines = answer.strip().split(".")
548
- followups = []
549
- # Simple keyword-driven suggestions
550
  keywords = [w for w in query.lower().split() if len(w) > 4]
551
- starters = [
552
  f"Can you elaborate on {keywords[0] if keywords else 'this'} in more detail?",
553
- f"What are the practical applications of this?",
554
- f"How does this compare to similar approaches?",
555
- f"What are the limitations or challenges here?",
556
- f"Can you give a concrete example?",
557
  ]
558
- return starters[:3]
559
 
 
 
 
 
 
 
560
 
561
- # ===========================
562
- # Crew Factory
563
- # ===========================
564
  def create_crew(pdf_tool):
 
565
  web_search_tool = SerperDevTool()
566
  tools = [t for t in [pdf_tool, web_search_tool] if t]
567
 
568
  retriever = Agent(
569
  role="Retrieve relevant information to answer the user query: {query}",
570
- goal=(
571
- "Retrieve the most relevant information from the available sources "
572
- "for the user query: {query}. Always try the PDF search tool first. "
573
- "Fall back to web search only if the PDF search yields nothing useful."
574
- ),
575
- backstory=(
576
- "You're a meticulous analyst with a keen eye for detail. "
577
- "You understand user queries: {query} and retrieve knowledge "
578
- "from the most suitable knowledge base."
579
- ),
580
- verbose=True,
581
- tools=tools,
582
  )
583
 
584
  synthesizer = Agent(
585
- role="Response synthesizer for the user query: {query}",
586
- goal=(
587
- "Synthesize retrieved information into a concise, well-structured response "
588
- "for the query: {query}. If nothing was found, say so clearly."
589
- ),
590
- backstory="You're a skilled communicator who turns complex information into clear answers.",
591
- verbose=True,
592
  )
593
 
594
  retrieval_task = Task(
595
- description="Retrieve the most relevant information for the user query: {query}",
596
- expected_output="The most relevant information as retrieved from the sources.",
597
  agent=retriever,
598
  )
599
 
600
  response_task = Task(
601
- description="Synthesize the final response for the user query: {query}",
602
- expected_output=(
603
- "A concise, coherent response based on retrieved information for: {query}. "
604
- "If nothing was found, say: \"I'm sorry, I couldn't find the information you're looking for.\""
605
- ),
606
  agent=synthesizer,
607
  )
608
 
609
  return Crew(
610
  agents=[retriever, synthesizer],
611
  tasks=[retrieval_task, response_task],
612
- process=Process.sequential,
613
- verbose=True,
614
  )
615
 
616
-
617
- # ===========================
618
- # SIDEBAR
619
- # ===========================
620
  with st.sidebar:
621
- # Brand
622
  st.markdown("""
623
- <div style="padding: 0.2rem 0 1rem;">
624
- <div style="display:flex; align-items:center; gap:10px; margin-bottom:4px;">
625
- <div style="width:30px;height:30px;background:linear-gradient(135deg,#00e5ff,#7c5cfc);
626
- border-radius:8px;display:flex;align-items:center;justify-content:center;
627
- font-weight:800;color:#000;font-size:14px;">⬡</div>
628
- <span style="font-family:'Syne',sans-serif;font-size:1rem;font-weight:700;
629
- background:linear-gradient(135deg,#00e5ff,#7c5cfc);
630
- -webkit-background-clip:text;-webkit-text-fill-color:transparent;">NeuralDocs</span>
631
- </div>
632
- <div style="font-family:'JetBrains Mono',monospace;font-size:0.6rem;
633
- letter-spacing:2px;color:#3d5a75;text-transform:uppercase;">
634
- Agentic RAG · v2.0
635
  </div>
 
636
  </div>
637
  """, unsafe_allow_html=True)
638
 
639
  st.markdown('<div class="sidebar-section-label">📄 Document</div>', unsafe_allow_html=True)
640
-
641
  uploaded_file = st.file_uploader("Upload PDF", type=["pdf"], label_visibility="collapsed")
642
 
643
  if uploaded_file is not None:
@@ -649,28 +298,16 @@ with st.sidebar:
649
  with st.spinner("Indexing document…"):
650
  st.session_state.pdf_tool = DocumentSearchTool(file_path=path)
651
  st.session_state.pdf_name = uploaded_file.name
652
- st.session_state.crew = None # force rebuild
653
  st.session_state.messages = []
654
  st.session_state.pending_followups = []
655
 
656
- st.markdown(f"""
657
- <div class="status-pill status-ready">
658
- <span class="status-dot"></span>
659
- {uploaded_file.name[:28]}{'…' if len(uploaded_file.name)>28 else ''}
660
- </div>
661
- """, unsafe_allow_html=True)
662
-
663
  with st.expander("Preview PDF", expanded=False):
664
  display_pdf(uploaded_file.getvalue(), uploaded_file.name)
665
  else:
666
- st.markdown("""
667
- <div class="status-pill status-waiting">
668
- <span class="status-dot pulse"></span>
669
- No document loaded
670
- </div>
671
- """, unsafe_allow_html=True)
672
 
673
- # ── METRICS (Extended Feature #1) ──
674
  st.markdown('<div class="sidebar-section-label">📊 Session Stats</div>', unsafe_allow_html=True)
675
  total = st.session_state.total_queries or 0
676
  pdf_h = st.session_state.pdf_hits or 0
@@ -680,209 +317,117 @@ with st.sidebar:
680
 
681
  st.markdown(f"""
682
  <div class="metrics-row">
683
- <div class="metric-card">
684
- <div class="metric-value">{total}</div>
685
- <div class="metric-label">Queries</div>
686
- </div>
687
- <div class="metric-card">
688
- <div class="metric-value" style="color:var(--success)">{pdf_pct}%</div>
689
- <div class="metric-label">PDF hits</div>
690
- </div>
691
  </div>
692
  <div class="metrics-row">
693
- <div class="metric-card">
694
- <div class="metric-value" style="color:var(--accent2)">{web_pct}%</div>
695
- <div class="metric-label">Web hits</div>
696
- </div>
697
- <div class="metric-card">
698
- <div class="metric-value" style="color:var(--warning)">{len(st.session_state.messages)//2}</div>
699
- <div class="metric-label">Exchanges</div>
700
- </div>
701
  </div>
702
  """, unsafe_allow_html=True)
703
 
704
- # ── QUERY HISTORY (Extended Feature #2) ──
705
  if st.session_state.query_log:
706
  st.markdown('<div class="sidebar-section-label">🕓 Query History</div>', unsafe_allow_html=True)
707
  for item in reversed(st.session_state.query_log[-6:]):
708
  src_icon = "🌐" if item["source"] == "web" else "📄"
709
- st.markdown(f"""
710
- <div class="history-item">
711
- <div class="history-q">{src_icon} {item['q'][:48]}{'…' if len(item['q'])>48 else ''}</div>
712
- <div class="history-time">{item['ts']}</div>
713
- </div>
714
- """, unsafe_allow_html=True)
715
 
716
  st.markdown('<div class="sidebar-section-label">⚙ Controls</div>', unsafe_allow_html=True)
717
  col1, col2 = st.columns(2)
718
- with col1:
719
- st.button("Clear Chat", on_click=reset_chat, use_container_width=True)
720
- with col2:
721
- st.button("Full Reset", on_click=full_reset, use_container_width=True)
722
-
723
 
724
- # ===========================
725
- # MAIN AREA
726
- # ===========================
727
-
728
- # Header
729
  st.markdown("""
730
  <div class="app-header">
731
- <div class="app-logo">⬡</div>
732
  <div>
733
  <div class="app-title">NeuralDocs</div>
734
  <div class="app-subtitle">Agentic RAG · PDF + Web Intelligence</div>
735
  </div>
736
- <div class="header-badge">CREWAI POWERED</div>
737
  </div>
738
  """, unsafe_allow_html=True)
739
 
740
- # ── Empty state ──
741
  if not st.session_state.messages:
742
  st.markdown("""
743
  <div class="empty-state">
744
- <div class="empty-icon">⬡</div>
745
- <div class="empty-text">
746
- Upload a PDF in the sidebar, then ask anything.<br>
747
- The agent searches your document first — then the web.
748
- </div>
749
- <div class="empty-hint">PDF · WEB SEARCH · SYNTHESIS</div>
750
- </div>
751
- <div style="display:flex;justify-content:center;">
752
- <div class="suggestion-grid">
753
- <div class="suggestion-card">
754
- <div class="suggestion-icon">📖</div>
755
- <div class="suggestion-text">Summarize the key points of this document</div>
756
- </div>
757
- <div class="suggestion-card">
758
- <div class="suggestion-icon">🔍</div>
759
- <div class="suggestion-text">What are the main findings or conclusions?</div>
760
- </div>
761
- <div class="suggestion-card">
762
- <div class="suggestion-icon">🌐</div>
763
- <div class="suggestion-text">Search the web for the latest news on this topic</div>
764
- </div>
765
- <div class="suggestion-card">
766
- <div class="suggestion-icon">⚖️</div>
767
- <div class="suggestion-text">Compare this with current industry standards</div>
768
- </div>
769
- </div>
770
  </div>
771
  """, unsafe_allow_html=True)
772
 
773
- # ── Chat history ──
774
  for i, message in enumerate(st.session_state.messages):
775
  with st.chat_message(message["role"]):
776
  st.markdown(message["content"])
777
-
778
- # Show metadata + follow-ups only on last assistant message
779
- if (message["role"] == "assistant"
780
- and i == len(st.session_state.messages) - 1
781
- and st.session_state.pending_followups):
782
-
783
  conf = estimate_confidence(message["content"])
784
- src = detect_source(message["content"])
785
- src_label = "Web Search" if src == "web" else "PDF Document"
786
- src_class = src
787
-
788
  st.markdown(f"""
789
  <div class="answer-meta">
790
- <span class="source-badge {src_class}">
791
- {'🌐' if src == 'web' else '📄'} {src_label}
792
- </span>
793
  <div class="answer-meta-item confidence-bar-wrap">
794
  <span>Confidence</span>
795
- <div class="confidence-bar">
796
- <div class="confidence-fill" style="width:{conf}%"></div>
797
- </div>
798
  <span>{conf}%</span>
799
  </div>
800
- <div class="answer-meta-item">
801
- 🕐 {st.session_state.query_log[-1]['ts'] if st.session_state.query_log else '—'}
802
- </div>
803
  </div>
804
  """, unsafe_allow_html=True)
805
-
806
- # Follow-up chips
807
- st.markdown("""
808
- <div class="followup-label">💡 FOLLOW-UP SUGGESTIONS</div>
809
- """, unsafe_allow_html=True)
810
-
811
  cols = st.columns(len(st.session_state.pending_followups))
812
  for j, (col, q) in enumerate(zip(cols, st.session_state.pending_followups)):
813
  with col:
814
  if st.button(q, key=f"followup_{i}_{j}", use_container_width=True):
815
  st.session_state.pending_prompt = q
816
 
817
- # ───────────────────────────────────────────
818
- # Handle queued follow-up prompt
819
- # ───────────────────────────────────────────
820
  if st.session_state.pending_prompt:
821
  prompt = st.session_state.pending_prompt
822
  st.session_state.pending_prompt = None
823
  st.session_state.pending_followups = []
824
-
825
  st.session_state.messages.append({"role": "user", "content": prompt})
826
- with st.chat_message("user"):
827
- st.markdown(prompt)
828
-
829
- if st.session_state.crew is None:
830
- st.session_state.crew = create_crew(st.session_state.pdf_tool)
831
-
832
  with st.chat_message("assistant"):
833
  placeholder = st.empty()
834
- full_response = ""
835
  with st.spinner("Researching…"):
836
  result = st.session_state.crew.kickoff(inputs={"query": prompt}).raw
837
-
838
  for i, line in enumerate(result.split("\n")):
839
  full_response += line + ("\n" if i < len(result.split("\n")) - 1 else "")
840
  placeholder.markdown(full_response + "▌")
841
- time.sleep(0.12)
842
  placeholder.markdown(full_response)
843
-
844
  src = detect_source(result)
845
- ts = datetime.now().strftime("%H:%M")
846
  st.session_state.total_queries += 1
847
  if src == "pdf": st.session_state.pdf_hits += 1
848
- else: st.session_state.web_hits += 1
849
  st.session_state.query_log.append({"q": prompt, "ts": ts, "source": src})
850
  st.session_state.messages.append({"role": "assistant", "content": result})
851
  st.session_state.pending_followups = generate_followups(prompt, result)
852
  st.rerun()
853
 
854
- # ───────────────────────────────────────────
855
- # Primary chat input
856
- # ───────────────────────────────────────────
857
  prompt = st.chat_input("Ask anything about your document or search the web…")
858
-
859
  if prompt:
860
  st.session_state.pending_followups = []
861
  st.session_state.messages.append({"role": "user", "content": prompt})
862
-
863
- with st.chat_message("user"):
864
- st.markdown(prompt)
865
-
866
- if st.session_state.crew is None:
867
- st.session_state.crew = create_crew(st.session_state.pdf_tool)
868
-
869
  with st.chat_message("assistant"):
870
  placeholder = st.empty()
871
- full_response = ""
872
  with st.spinner("Researching…"):
873
  result = st.session_state.crew.kickoff(inputs={"query": prompt}).raw
874
-
875
  for i, line in enumerate(result.split("\n")):
876
  full_response += line + ("\n" if i < len(result.split("\n")) - 1 else "")
877
  placeholder.markdown(full_response + "▌")
878
- time.sleep(0.12)
879
  placeholder.markdown(full_response)
880
-
881
  src = detect_source(result)
882
- ts = datetime.now().strftime("%H:%M")
883
  st.session_state.total_queries += 1
884
  if src == "pdf": st.session_state.pdf_hits += 1
885
- else: st.session_state.web_hits += 1
886
  st.session_state.query_log.append({"q": prompt, "ts": ts, "source": src})
887
  st.session_state.messages.append({"role": "assistant", "content": result})
888
  st.session_state.pending_followups = generate_followups(prompt, result)
 
4
  import gc
5
  import base64
6
  import time
 
7
  from datetime import datetime
8
 
9
  from crewai import Agent, Crew, Process, Task
10
  from crewai_tools import SerperDevTool
11
+ from langchain_groq import ChatGroq
12
  from src.agentic_rag.tools.custom_tool import DocumentSearchTool
13
 
 
 
 
14
  st.set_page_config(
15
  page_title="NeuralDocs — Agentic RAG",
16
+ page_icon="🧠",
17
  layout="wide",
18
  initial_sidebar_state="expanded"
19
  )
20
 
 
 
 
21
  st.markdown("""
22
  <style>
23
  @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&family=Figtree:wght@300;400;500;600&display=swap');
24
 
 
25
  :root {
26
  --bg-base: #080c12;
27
  --bg-surface: #0d1520;
 
46
  --radius-sm: 6px;
47
  --radius-md: 12px;
48
  --radius-lg: 18px;
 
 
49
  }
50
 
 
51
  html, body, [class*="css"] {
52
  font-family: var(--font-body);
53
  background-color: var(--bg-base) !important;
54
  color: var(--text-primary) !important;
55
  }
56
 
 
57
  #MainMenu, footer, header { visibility: hidden; }
58
  .stDeployButton { display: none; }
59
 
 
60
  .main .block-container {
61
  background: var(--bg-base);
62
  padding: 1.5rem 2rem 3rem;
63
  max-width: 1100px;
64
  }
65
 
 
66
  .app-header {
67
  display: flex;
68
  align-items: center;
 
76
  background: linear-gradient(135deg, var(--accent), var(--accent2));
77
  border-radius: 10px;
78
  display: flex; align-items: center; justify-content: center;
79
+ font-size: 18px; font-weight: 800; color: #000; flex-shrink: 0;
 
 
 
80
  }
81
  .app-title {
82
  font-family: var(--font-display);
83
+ font-size: 1.5rem; font-weight: 800; letter-spacing: -0.5px;
 
 
84
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%);
85
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
 
 
86
  line-height: 1;
87
  }
88
  .app-subtitle {
89
+ font-size: 0.7rem; color: var(--text-muted);
90
+ letter-spacing: 2px; text-transform: uppercase;
91
+ font-family: var(--font-mono); margin-top: 3px;
 
 
 
92
  }
93
  .header-badge {
94
  margin-left: auto;
95
+ background: var(--accent-glow); border: 1px solid var(--accent-dim);
96
+ color: var(--accent); font-family: var(--font-mono);
97
+ font-size: 0.65rem; padding: 4px 10px; border-radius: 20px; letter-spacing: 1px;
 
 
 
 
 
98
  }
99
 
 
100
  [data-testid="stSidebar"] {
101
  background: var(--bg-surface) !important;
102
  border-right: 1px solid var(--border) !important;
 
106
  padding: 1.5rem 1.2rem;
107
  }
108
  .sidebar-section-label {
109
+ font-family: var(--font-mono); font-size: 0.6rem;
110
+ letter-spacing: 3px; text-transform: uppercase;
111
+ color: var(--text-muted); margin: 1.2rem 0 0.6rem;
112
+ padding-bottom: 0.4rem; border-bottom: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
 
115
  .status-pill {
116
+ display: inline-flex; align-items: center; gap: 6px;
117
+ padding: 5px 12px; border-radius: 20px;
118
+ font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.5px; margin: 6px 0;
 
 
 
 
 
 
119
  }
120
+ .status-ready { background: rgba(0,200,150,0.1); border: 1px solid var(--success); color: var(--success); }
121
+ .status-waiting { background: rgba(255,183,0,0.1); border: 1px solid var(--warning); color: var(--warning); }
 
122
  .status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
123
  .status-dot.pulse { animation: pulse 1.5s infinite; }
124
  @keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
125
 
126
+ .metrics-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 10px 0; }
127
+ .metric-card { background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 10px 14px; }
128
+ .metric-value { font-family: var(--font-display); font-size: 1.3rem; font-weight: 700; color: var(--accent); line-height: 1; }
129
+ .metric-label { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-muted); letter-spacing: 1px; text-transform: uppercase; margin-top: 3px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
 
131
  .source-badge {
132
+ display: inline-flex; align-items: center; gap: 5px;
133
+ background: var(--bg-elevated); border: 1px solid var(--border);
134
+ border-radius: 4px; padding: 2px 8px;
135
+ font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-secondary); margin: 2px 3px;
 
 
 
 
 
 
 
136
  }
137
+ .source-badge.pdf { border-color: var(--accent2); color: #b39ddb; background: rgba(124,92,252,0.08); }
138
+ .source-badge.web { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); }
139
 
 
140
  .stChatMessage {
141
+ background: var(--bg-surface) !important; border: 1px solid var(--border) !important;
142
+ border-radius: var(--radius-lg) !important; padding: 1rem 1.2rem !important; margin-bottom: 10px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
+ [data-testid="stChatMessageContent"] p { font-family: var(--font-body); font-size: 0.9rem; line-height: 1.7; color: var(--text-primary); }
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  .answer-meta {
147
+ display: flex; align-items: center; gap: 12px;
148
+ padding: 8px 0 4px; border-top: 1px solid var(--border); margin-top: 10px; flex-wrap: wrap;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
+ .answer-meta-item { display: flex; align-items: center; gap: 4px; font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-muted); }
151
+ .confidence-bar-wrap { display: flex; align-items: center; gap: 6px; }
152
+ .confidence-bar { width: 60px; height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
153
+ .confidence-fill { height: 100%; border-radius: 2px; background: linear-gradient(90deg, var(--accent2), var(--accent)); }
154
 
 
155
  [data-testid="stChatInput"] {
156
+ background: var(--bg-elevated) !important; border: 1px solid var(--border-bright) !important; border-radius: var(--radius-lg) !important;
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
+ [data-testid="stChatInput"]:focus-within { border-color: var(--accent) !important; }
159
+ [data-testid="stChatInput"] textarea { color: var(--text-primary) !important; font-family: var(--font-body) !important; font-size: 0.9rem !important; background: transparent !important; }
160
 
 
161
  .stButton > button {
162
+ background: var(--bg-elevated) !important; border: 1px solid var(--border-bright) !important;
163
+ color: var(--text-secondary) !important; border-radius: var(--radius-sm) !important;
164
+ font-family: var(--font-mono) !important; font-size: 0.72rem !important; transition: all 0.2s !important;
 
 
 
 
 
 
 
 
 
 
 
165
  }
166
+ .stButton > button:hover { border-color: var(--accent) !important; color: var(--accent) !important; background: var(--accent-glow) !important; }
167
 
 
168
  [data-testid="stFileUploader"] {
169
+ background: var(--bg-elevated) !important; border: 1px dashed var(--border-bright) !important; border-radius: var(--radius-md) !important;
 
 
 
 
 
 
 
 
 
 
170
  }
171
 
172
+ .history-item {
173
+ background: var(--bg-elevated); border: 1px solid var(--border);
174
+ border-left: 3px solid var(--accent-dim); border-radius: var(--radius-sm);
175
+ padding: 8px 10px; margin: 4px 0;
176
  }
177
+ .history-q { font-size: 0.78rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
178
+ .history-time { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-muted); margin-top: 2px; }
179
 
180
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 3rem 1rem; text-align: center; gap: 12px; }
181
+ .empty-icon { font-size: 2.5rem; opacity: 0.3; }
182
+ .empty-text { font-family: var(--font-body); color: var(--text-muted); font-size: 0.9rem; max-width: 320px; line-height: 1.6; }
183
 
184
+ hr { border-color: var(--border) !important; }
185
  ::-webkit-scrollbar { width: 4px; }
186
  ::-webkit-scrollbar-track { background: var(--bg-base); }
187
  ::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  </style>
189
  """, unsafe_allow_html=True)
190
 
191
+ # Session State
 
 
 
192
  defaults = {
193
+ "messages": [], "pdf_tool": None, "crew": None, "pdf_name": None,
194
+ "total_queries": 0, "pdf_hits": 0, "web_hits": 0,
195
+ "query_log": [], "pending_followups": [], "pending_prompt": None,
 
 
 
 
 
 
 
196
  }
197
  for k, v in defaults.items():
198
  if k not in st.session_state:
199
  st.session_state[k] = v
200
 
 
 
 
 
201
  def reset_chat():
202
  st.session_state.messages = []
203
  st.session_state.pending_followups = []
 
208
  st.session_state[k] = defaults[k]
209
  gc.collect()
210
 
211
+ def display_pdf(file_bytes, file_name):
212
  b64 = base64.b64encode(file_bytes).decode("utf-8")
213
+ st.markdown(f'<iframe src="data:application/pdf;base64,{b64}" width="100%" height="480px" style="border:1px solid var(--border);border-radius:var(--radius-md);"></iframe>', unsafe_allow_html=True)
 
 
 
 
 
214
 
215
+ def detect_source(result_text):
 
216
  web_signals = ["according to", "website", "http", "search result", "online", "found on"]
217
+ return "web" if any(s in result_text.lower() for s in web_signals) else "pdf"
218
+
219
+ def estimate_confidence(result_text):
220
+ if "i'm sorry" in result_text.lower() or "couldn't find" in result_text.lower(): return 20
221
+ if len(result_text) > 400: return 88
222
+ if len(result_text) > 150: return 72
 
 
 
 
 
 
223
  return 55
224
 
225
+ def generate_followups(query, answer):
 
 
 
 
226
  keywords = [w for w in query.lower().split() if len(w) > 4]
227
+ return [
228
  f"Can you elaborate on {keywords[0] if keywords else 'this'} in more detail?",
229
+ "What are the practical applications of this?",
230
+ "What are the limitations or challenges here?",
 
 
231
  ]
 
232
 
233
+ def get_llm():
234
+ return ChatGroq(
235
+ api_key=os.environ.get("GROQ_API_KEY"),
236
+ model="llama3-70b-8192",
237
+ temperature=0.1,
238
+ )
239
 
 
 
 
240
  def create_crew(pdf_tool):
241
+ llm = get_llm()
242
  web_search_tool = SerperDevTool()
243
  tools = [t for t in [pdf_tool, web_search_tool] if t]
244
 
245
  retriever = Agent(
246
  role="Retrieve relevant information to answer the user query: {query}",
247
+ goal="Retrieve the most relevant information from available sources for: {query}. Try PDF first, fall back to web search.",
248
+ backstory="You are a meticulous analyst with a keen eye for detail.",
249
+ verbose=True, tools=tools, llm=llm,
 
 
 
 
 
 
 
 
 
250
  )
251
 
252
  synthesizer = Agent(
253
+ role="Response synthesizer for: {query}",
254
+ goal="Synthesize retrieved information into a concise, well-structured response for: {query}.",
255
+ backstory="You are a skilled communicator who turns complex information into clear answers.",
256
+ verbose=True, llm=llm,
 
 
 
257
  )
258
 
259
  retrieval_task = Task(
260
+ description="Retrieve the most relevant information for: {query}",
261
+ expected_output="The most relevant information retrieved from available sources.",
262
  agent=retriever,
263
  )
264
 
265
  response_task = Task(
266
+ description="Synthesize the final response for: {query}",
267
+ expected_output="A concise, coherent response based on retrieved information for: {query}.",
 
 
 
268
  agent=synthesizer,
269
  )
270
 
271
  return Crew(
272
  agents=[retriever, synthesizer],
273
  tasks=[retrieval_task, response_task],
274
+ process=Process.sequential, verbose=True,
 
275
  )
276
 
277
+ # Sidebar
 
 
 
278
  with st.sidebar:
 
279
  st.markdown("""
280
+ <div style="padding:0.2rem 0 1rem;">
281
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:4px;">
282
+ <div style="width:30px;height:30px;background:linear-gradient(135deg,#00e5ff,#7c5cfc);border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:800;color:#000;font-size:14px;">🧠</div>
283
+ <span style="font-family:'Syne',sans-serif;font-size:1rem;font-weight:700;background:linear-gradient(135deg,#00e5ff,#7c5cfc);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">NeuralDocs</span>
 
 
 
 
 
 
 
 
284
  </div>
285
+ <div style="font-family:'JetBrains Mono',monospace;font-size:0.6rem;letter-spacing:2px;color:#3d5a75;text-transform:uppercase;">Agentic RAG · Groq + LLaMA 3</div>
286
  </div>
287
  """, unsafe_allow_html=True)
288
 
289
  st.markdown('<div class="sidebar-section-label">📄 Document</div>', unsafe_allow_html=True)
 
290
  uploaded_file = st.file_uploader("Upload PDF", type=["pdf"], label_visibility="collapsed")
291
 
292
  if uploaded_file is not None:
 
298
  with st.spinner("Indexing document…"):
299
  st.session_state.pdf_tool = DocumentSearchTool(file_path=path)
300
  st.session_state.pdf_name = uploaded_file.name
301
+ st.session_state.crew = None
302
  st.session_state.messages = []
303
  st.session_state.pending_followups = []
304
 
305
+ st.markdown(f'<div class="status-pill status-ready"><span class="status-dot"></span>{uploaded_file.name[:28]}{"…" if len(uploaded_file.name)>28 else ""}</div>', unsafe_allow_html=True)
 
 
 
 
 
 
306
  with st.expander("Preview PDF", expanded=False):
307
  display_pdf(uploaded_file.getvalue(), uploaded_file.name)
308
  else:
309
+ st.markdown('<div class="status-pill status-waiting"><span class="status-dot pulse"></span>No document loaded</div>', unsafe_allow_html=True)
 
 
 
 
 
310
 
 
311
  st.markdown('<div class="sidebar-section-label">📊 Session Stats</div>', unsafe_allow_html=True)
312
  total = st.session_state.total_queries or 0
313
  pdf_h = st.session_state.pdf_hits or 0
 
317
 
318
  st.markdown(f"""
319
  <div class="metrics-row">
320
+ <div class="metric-card"><div class="metric-value">{total}</div><div class="metric-label">Queries</div></div>
321
+ <div class="metric-card"><div class="metric-value" style="color:var(--success)">{pdf_pct}%</div><div class="metric-label">PDF hits</div></div>
 
 
 
 
 
 
322
  </div>
323
  <div class="metrics-row">
324
+ <div class="metric-card"><div class="metric-value" style="color:var(--accent2)">{web_pct}%</div><div class="metric-label">Web hits</div></div>
325
+ <div class="metric-card"><div class="metric-value" style="color:var(--warning)">{len(st.session_state.messages)//2}</div><div class="metric-label">Exchanges</div></div>
 
 
 
 
 
 
326
  </div>
327
  """, unsafe_allow_html=True)
328
 
 
329
  if st.session_state.query_log:
330
  st.markdown('<div class="sidebar-section-label">🕓 Query History</div>', unsafe_allow_html=True)
331
  for item in reversed(st.session_state.query_log[-6:]):
332
  src_icon = "🌐" if item["source"] == "web" else "📄"
333
+ st.markdown(f'<div class="history-item"><div class="history-q">{src_icon} {item["q"][:48]}{"…" if len(item["q"])>48 else ""}</div><div class="history-time">{item["ts"]}</div></div>', unsafe_allow_html=True)
 
 
 
 
 
334
 
335
  st.markdown('<div class="sidebar-section-label">⚙ Controls</div>', unsafe_allow_html=True)
336
  col1, col2 = st.columns(2)
337
+ with col1: st.button("Clear Chat", on_click=reset_chat, use_container_width=True)
338
+ with col2: st.button("Full Reset", on_click=full_reset, use_container_width=True)
 
 
 
339
 
340
+ # Main
 
 
 
 
341
  st.markdown("""
342
  <div class="app-header">
343
+ <div class="app-logo">🧠</div>
344
  <div>
345
  <div class="app-title">NeuralDocs</div>
346
  <div class="app-subtitle">Agentic RAG · PDF + Web Intelligence</div>
347
  </div>
348
+ <div class="header-badge">GROQ · LLAMA 3</div>
349
  </div>
350
  """, unsafe_allow_html=True)
351
 
 
352
  if not st.session_state.messages:
353
  st.markdown("""
354
  <div class="empty-state">
355
+ <div class="empty-icon">🧠</div>
356
+ <div class="empty-text">Upload a PDF in the sidebar, then ask anything.<br>The agent searches your document first — then the web.</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  </div>
358
  """, unsafe_allow_html=True)
359
 
 
360
  for i, message in enumerate(st.session_state.messages):
361
  with st.chat_message(message["role"]):
362
  st.markdown(message["content"])
363
+ if message["role"] == "assistant" and i == len(st.session_state.messages) - 1 and st.session_state.pending_followups:
 
 
 
 
 
364
  conf = estimate_confidence(message["content"])
365
+ src = detect_source(message["content"])
 
 
 
366
  st.markdown(f"""
367
  <div class="answer-meta">
368
+ <span class="source-badge {src}">{'🌐 Web' if src == 'web' else '📄 PDF'}</span>
 
 
369
  <div class="answer-meta-item confidence-bar-wrap">
370
  <span>Confidence</span>
371
+ <div class="confidence-bar"><div class="confidence-fill" style="width:{conf}%"></div></div>
 
 
372
  <span>{conf}%</span>
373
  </div>
 
 
 
374
  </div>
375
  """, unsafe_allow_html=True)
376
+ st.markdown('<div style="font-family:var(--font-mono);font-size:0.6rem;color:var(--text-muted);margin:8px 0 4px;">💡 FOLLOW-UP SUGGESTIONS</div>', unsafe_allow_html=True)
 
 
 
 
 
377
  cols = st.columns(len(st.session_state.pending_followups))
378
  for j, (col, q) in enumerate(zip(cols, st.session_state.pending_followups)):
379
  with col:
380
  if st.button(q, key=f"followup_{i}_{j}", use_container_width=True):
381
  st.session_state.pending_prompt = q
382
 
 
 
 
383
  if st.session_state.pending_prompt:
384
  prompt = st.session_state.pending_prompt
385
  st.session_state.pending_prompt = None
386
  st.session_state.pending_followups = []
 
387
  st.session_state.messages.append({"role": "user", "content": prompt})
388
+ with st.chat_message("user"): st.markdown(prompt)
389
+ if st.session_state.crew is None: st.session_state.crew = create_crew(st.session_state.pdf_tool)
 
 
 
 
390
  with st.chat_message("assistant"):
391
  placeholder = st.empty()
 
392
  with st.spinner("Researching…"):
393
  result = st.session_state.crew.kickoff(inputs={"query": prompt}).raw
394
+ full_response = ""
395
  for i, line in enumerate(result.split("\n")):
396
  full_response += line + ("\n" if i < len(result.split("\n")) - 1 else "")
397
  placeholder.markdown(full_response + "▌")
398
+ time.sleep(0.08)
399
  placeholder.markdown(full_response)
 
400
  src = detect_source(result)
401
+ ts = datetime.now().strftime("%H:%M")
402
  st.session_state.total_queries += 1
403
  if src == "pdf": st.session_state.pdf_hits += 1
404
+ else: st.session_state.web_hits += 1
405
  st.session_state.query_log.append({"q": prompt, "ts": ts, "source": src})
406
  st.session_state.messages.append({"role": "assistant", "content": result})
407
  st.session_state.pending_followups = generate_followups(prompt, result)
408
  st.rerun()
409
 
 
 
 
410
  prompt = st.chat_input("Ask anything about your document or search the web…")
 
411
  if prompt:
412
  st.session_state.pending_followups = []
413
  st.session_state.messages.append({"role": "user", "content": prompt})
414
+ with st.chat_message("user"): st.markdown(prompt)
415
+ if st.session_state.crew is None: st.session_state.crew = create_crew(st.session_state.pdf_tool)
 
 
 
 
 
416
  with st.chat_message("assistant"):
417
  placeholder = st.empty()
 
418
  with st.spinner("Researching…"):
419
  result = st.session_state.crew.kickoff(inputs={"query": prompt}).raw
420
+ full_response = ""
421
  for i, line in enumerate(result.split("\n")):
422
  full_response += line + ("\n" if i < len(result.split("\n")) - 1 else "")
423
  placeholder.markdown(full_response + "▌")
424
+ time.sleep(0.08)
425
  placeholder.markdown(full_response)
 
426
  src = detect_source(result)
427
+ ts = datetime.now().strftime("%H:%M")
428
  st.session_state.total_queries += 1
429
  if src == "pdf": st.session_state.pdf_hits += 1
430
+ else: st.session_state.web_hits += 1
431
  st.session_state.query_log.append({"q": prompt, "ts": ts, "source": src})
432
  st.session_state.messages.append({"role": "assistant", "content": result})
433
  st.session_state.pending_followups = generate_followups(prompt, result)
requirements.txt CHANGED
@@ -1,8 +1,9 @@
1
  streamlit==1.32.0
2
- crewai==0.80.0
3
- crewai-tools==0.14.0
4
  markitdown>=0.0.1
5
  chonkie>=0.1.0
6
  qdrant-client>=1.7.3
7
  openai>=1.30.0
8
- pydantic>=2.0.0
 
 
1
  streamlit==1.32.0
2
+ crewai==0.86.0
3
+ crewai-tools>=0.17.0
4
  markitdown>=0.0.1
5
  chonkie>=0.1.0
6
  qdrant-client>=1.7.3
7
  openai>=1.30.0
8
+ langchain-groq>=0.1.0
9
+ groq>=0.4.0