ST-x-Tony commited on
Commit
b8116ac
·
verified ·
1 Parent(s): 2a8b709

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +148 -896
app.py CHANGED
@@ -12,7 +12,7 @@ import asyncio
12
  import traceback
13
 
14
  import gradio as gr
15
- import spaces # required for ZeroGPU
16
 
17
 
18
  # ============================================================
@@ -22,22 +22,9 @@ import spaces # required for ZeroGPU
22
  APP_NAME = "X-RUDRA"
23
  VERSION = "3.1.0"
24
 
25
- M1_REPO = os.getenv(
26
- "M1_REPO",
27
- "Shrijanagain/M1",
28
- )
29
-
30
- M2_REPO = os.getenv(
31
- "M2_REPO",
32
- "Shrijanagain/M2",
33
- )
34
-
35
- PORT = int(
36
- os.getenv(
37
- "PORT",
38
- "7860",
39
- )
40
- )
41
 
42
 
43
  # ============================================================
@@ -46,974 +33,259 @@ PORT = int(
46
 
47
  _ENGINE = None
48
 
49
-
50
  def get_engine():
51
- """
52
- IMPORTANT:
53
- Do not initialize the web-search engine during
54
- Space startup.
55
-
56
- It is created only when the first user request arrives.
57
- """
58
-
59
  global _ENGINE
60
-
61
  if _ENGINE is None:
62
-
63
  from web_search import XrudraWebSearch
64
-
65
  _ENGINE = XrudraWebSearch()
66
-
67
  return _ENGINE
68
 
69
 
70
  # ============================================================
71
- # SAFE VALUE HELPERS
72
  # ============================================================
73
 
74
  def safe_dict(value):
75
-
76
  if isinstance(value, dict):
77
  return value
78
-
79
  if hasattr(value, "model_dump"):
80
-
81
  try:
82
  return value.model_dump()
83
-
84
  except Exception:
85
  pass
86
-
87
  if hasattr(value, "dict"):
88
-
89
  try:
90
  return value.dict()
91
-
92
  except Exception:
93
  pass
94
-
95
- return {
96
- "result": str(value)
97
- }
98
-
99
-
100
- def get_value(data, key, default=None):
101
-
102
- if not isinstance(data, dict):
103
- return default
104
-
105
- return data.get(
106
- key,
107
- default,
108
- )
109
 
110
 
111
  # ============================================================
112
- # SOURCE FORMATTER
113
  # ============================================================
114
 
115
  def format_sources(sources):
116
-
117
  if not sources:
118
-
119
- return (
120
- "## 📚 Sources\n\n"
121
- "No sources were returned."
122
- )
123
-
124
- output = [
125
- "## 📚 Sources",
126
- "",
127
- ]
128
-
129
- for index, source in enumerate(
130
- sources,
131
- start=1,
132
- ):
133
-
134
- if not isinstance(source, dict):
135
- continue
136
-
137
- title = source.get(
138
- "title",
139
- "Untitled",
140
- )
141
-
142
- url = source.get(
143
- "url",
144
- "",
145
- )
146
-
147
- method = source.get(
148
- "fetch_method",
149
- "unknown",
150
- )
151
-
152
- score = source.get(
153
- "source_score",
154
- source.get(
155
- "score",
156
- "N/A",
157
- ),
158
- )
159
-
160
- if url:
161
-
162
- output.append(
163
- f"### {index}. [{title}]({url})"
164
- )
165
-
166
- else:
167
-
168
- output.append(
169
- f"### {index}. {title}"
170
- )
171
-
172
- output.append(
173
- f"**Fetcher:** `{method}`"
174
- )
175
-
176
- output.append(
177
- f"**Source score:** `{score}`"
178
- )
179
-
180
- output.append("")
181
-
182
- return "\n".join(output)
183
-
184
-
185
- # ============================================================
186
- # EVIDENCE FORMATTER
187
- # ============================================================
188
 
189
  def format_evidence(claims):
190
-
191
  if not claims:
192
-
193
- return (
194
- "## 🧠 Evidence\n\n"
195
- "No structured evidence was returned."
196
- )
197
-
198
- output = [
199
- "## 🧠 Evidence",
200
- "",
201
- ]
202
-
203
- for index, claim in enumerate(
204
- claims,
205
- start=1,
206
- ):
207
-
208
- if not isinstance(claim, dict):
209
- continue
210
-
211
- text = claim.get(
212
- "claim",
213
- claim.get(
214
- "text",
215
- "",
216
- ),
217
- )
218
-
219
- score = claim.get(
220
- "support_score",
221
- claim.get(
222
- "score",
223
- "N/A",
224
- ),
225
- )
226
-
227
- source = claim.get(
228
- "source_url",
229
- claim.get(
230
- "url",
231
- "",
232
- ),
233
- )
234
-
235
- output.append(
236
- f"### Evidence {index}"
237
- )
238
-
239
- output.append(
240
- str(text)
241
- )
242
-
243
- output.append(
244
- f"**Support:** `{score}`"
245
- )
246
-
247
- if source:
248
-
249
- output.append(
250
- f"**Source:** {source}"
251
- )
252
-
253
- output.append("---")
254
-
255
- return "\n\n".join(output)
256
-
257
-
258
- # ============================================================
259
- # VERIFICATION FORMATTER
260
- # ============================================================
261
 
262
  def format_verification(contradictions):
263
-
264
  if not contradictions:
265
-
266
- return (
267
- "## ⚖️ Verification\n\n"
268
- "✅ No major automatic contradictions "
269
- "were detected."
270
- )
271
-
272
- output = [
273
- "## ⚖️ Verification",
274
- "",
275
- "⚠️ Potential contradictions detected:",
276
- "",
277
- ]
278
-
279
- for index, item in enumerate(
280
- contradictions,
281
- start=1,
282
- ):
283
-
284
- if not isinstance(item, dict):
285
- continue
286
-
287
- claim_a = item.get(
288
- "claim_a",
289
- "",
290
- )
291
-
292
- claim_b = item.get(
293
- "claim_b",
294
- "",
295
- )
296
-
297
- source_a = item.get(
298
- "source_a",
299
- "",
300
- )
301
-
302
- source_b = item.get(
303
- "source_b",
304
- "",
305
- )
306
-
307
- output.append(
308
- f"### Contradiction {index}"
309
- )
310
-
311
- output.append(
312
- f"**A:** {claim_a}"
313
- )
314
-
315
- if source_a:
316
-
317
- output.append(
318
- f"Source A: `{source_a}`"
319
- )
320
-
321
- output.append("")
322
-
323
- output.append(
324
- f"**B:** {claim_b}"
325
- )
326
-
327
- if source_b:
328
-
329
- output.append(
330
- f"Source B: `{source_b}`"
331
- )
332
-
333
- output.append("---")
334
-
335
- return "\n\n".join(output)
336
-
337
-
338
- # ============================================================
339
- # ANSWER EXTRACTOR
340
- # ============================================================
341
 
342
  def extract_answer(data):
343
-
344
- # Try common final-answer fields first.
345
-
346
- for key in (
347
- "final_answer",
348
- "answer",
349
- "response",
350
- "final",
351
- "synthesis",
352
- "summary",
353
- ):
354
-
355
- value = data.get(
356
- key,
357
- None,
358
- )
359
-
360
- if isinstance(
361
- value,
362
- str,
363
- ) and value.strip():
364
-
365
- return value.strip()
366
-
367
- # Otherwise construct an answer from claims.
368
-
369
- claims = data.get(
370
- "claims",
371
- [],
372
- )
373
-
374
  if claims:
375
-
376
  parts = []
377
-
378
  for claim in claims:
379
-
380
- if not isinstance(
381
- claim,
382
- dict,
383
- ):
384
- continue
385
-
386
- text = claim.get(
387
- "claim",
388
- claim.get(
389
- "text",
390
- "",
391
- ),
392
- )
393
-
394
- if text:
395
-
396
- parts.append(
397
- str(text).strip()
398
- )
399
-
400
  if parts:
401
-
402
- return "\n\n".join(
403
- parts[:10]
404
- )
405
-
406
- return (
407
- "Research completed, but the engine "
408
- "did not return a final synthesized answer."
409
- )
410
-
411
-
412
- # ============================================================
413
- # ACTIVITY PANEL
414
- # ============================================================
415
-
416
- def build_activity(
417
- data,
418
- elapsed_ms,
419
- ):
420
-
421
- sources = data.get(
422
- "sources",
423
- [],
424
- )
425
-
426
- claims = data.get(
427
- "claims",
428
- [],
429
- )
430
-
431
- contradictions = data.get(
432
- "contradictions",
433
- [],
434
- )
435
-
436
- rounds = data.get(
437
- "rounds",
438
- data.get(
439
- "research_rounds",
440
- "N/A",
441
- ),
442
- )
443
-
444
  return f"""
445
  ## ⚡ X-RUDRA Research
446
-
447
- | Stage | Status |
448
- |---|---|
449
- | Task analysis | ✅ Complete |
450
- | M1 research | {"✅ Enabled" if data.get("m1") is not None else "⚙️ Pipeline"} |
451
- | M2 research | {"✅ Enabled" if data.get("m2") is not None else "⚙️ Pipeline"} |
452
- | Web discovery | ✅ Complete |
453
- | Evidence extraction | ✅ Complete |
454
- | Source verification | ✅ Complete |
455
- | Contradiction check | {"⚠️ Found" if contradictions else "✅ Clear"} |
456
- | Final synthesis | ✅ Complete |
457
-
458
- **Sources:** `{len(sources)}`
459
- **Claims:** `{len(claims)}`
460
- **Rounds:** `{rounds}`
461
- **Time:** `{elapsed_ms} ms`
462
-
463
- ### Engine
464
-
465
- `M1` → `{M1_REPO}`
466
-
467
- `M2` → `{M2_REPO}`
468
-
469
- `Web` → `DuckDuckGo`
470
-
471
- `Fetcher` → `Scrapling`
472
-
473
- `Browser` → `Playwright`
474
  """
475
 
476
-
477
  # ============================================================
478
- # RESEARCH
479
  # ============================================================
480
 
481
- async def do_research(
482
- question,
483
- max_results,
484
- max_rounds,
485
- use_models,
486
- freshness,
487
- ):
488
-
489
- if not question or not str(
490
- question
491
- ).strip():
492
-
493
- # Return empty history (list of dicts) and empty messages
494
- return (
495
- [], # chat history as list of dicts
496
- "⚪ Enter a question to start.",
497
- "",
498
- "",
499
- )
500
-
501
- question = str(
502
- question
503
- ).strip()
504
 
 
505
  started = time.perf_counter()
506
 
507
  try:
508
-
509
- # ----------------------------------------------------
510
- # LAZY INITIALIZATION
511
- # ----------------------------------------------------
512
-
513
  engine = get_engine()
514
-
515
- # ----------------------------------------------------
516
- # RUN WEB / MODEL PIPELINE
517
- # ----------------------------------------------------
518
-
519
  report = await engine.search(
520
-
521
  question=question,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
 
523
- max_results=int(
524
- max_results
525
- ),
526
-
527
- max_rounds=int(
528
- max_rounds
529
- ),
530
-
531
- use_models=bool(
532
- use_models
533
- ),
534
-
535
- freshness_mode=str(
536
- freshness
537
- ),
538
- )
539
-
540
- data = safe_dict(
541
- report
542
- )
543
-
544
- elapsed_ms = int(
545
- (
546
- time.perf_counter()
547
- - started
548
- ) * 1000
549
- )
550
-
551
- # ----------------------------------------------------
552
- # OUTPUT
553
- # ----------------------------------------------------
554
-
555
- answer = extract_answer(
556
- data
557
- )
558
-
559
- sources = data.get(
560
- "sources",
561
- [],
562
- )
563
-
564
- claims = data.get(
565
- "claims",
566
- [],
567
- )
568
-
569
- contradictions = data.get(
570
- "contradictions",
571
- [],
572
- )
573
-
574
- activity = build_activity(
575
- data,
576
- elapsed_ms,
577
- )
578
-
579
- sources_md = format_sources(
580
- sources
581
- )
582
-
583
- evidence_md = format_evidence(
584
- claims
585
- )
586
-
587
- verification_md = format_verification(
588
- contradictions
589
- )
590
-
591
- # ----------------------------------------------------
592
- # CHAT HISTORY – MUST BE LIST OF DICTS
593
- # ----------------------------------------------------
594
 
595
  history = [
596
  {"role": "user", "content": question},
597
- {"role": "assistant", "content": answer},
598
  ]
599
 
600
- return (
601
- history,
602
- activity,
603
- sources_md,
604
- evidence_md,
605
- verification_md,
606
- )
607
 
608
  except Exception as exc:
609
-
610
- error = (
611
- f"❌ **X-RUDRA Error**\n\n"
612
- f"`{type(exc).__name__}: {exc}`"
613
- )
614
-
615
- print(
616
- "\n"
617
- + "=" * 70
618
- )
619
-
620
- print(
621
- "X-RUDRA ERROR"
622
- )
623
-
624
- print(
625
- traceback.format_exc()
626
- )
627
-
628
- print(
629
- "=" * 70
630
- + "\n"
631
- )
632
-
633
  history = [
634
  {"role": "user", "content": question},
635
- {"role": "assistant", "content": error},
636
  ]
637
-
638
- return (
639
- history,
640
- "❌ Research failed.",
641
- "",
642
- "",
643
- "",
644
- )
645
 
646
 
647
  # ============================================================
648
- # GRADIO SYNC WRAPPER DECORATED FOR ZEROGPU
649
  # ============================================================
650
 
651
  @spaces.GPU
652
- def run_research(
653
- question,
654
- max_results,
655
- max_rounds,
656
- use_models,
657
- freshness,
658
- ):
659
- return asyncio.run(
660
- do_research(
661
- question,
662
- max_results,
663
- max_rounds,
664
- use_models,
665
- freshness,
666
- )
667
- )
668
 
669
 
670
  # ============================================================
671
- # HEALTH
672
  # ============================================================
673
 
674
  def health_check():
675
-
676
  return f"""
677
  ## 🟢 X-RUDRA Online
678
-
679
  **Version:** `{VERSION}`
680
-
681
  **M1:** `{M1_REPO}`
682
-
683
  **M2:** `{M2_REPO}`
684
-
685
- **Web Search:** `DuckDuckGo`
686
-
687
- **HTTP Fetch:** `Scrapling`
688
-
689
- **Dynamic Fetch:** `Playwright`
690
-
691
- **Startup model loading:** `Disabled`
692
-
693
- **Engine:** `Lazy initialized`
694
  """
695
 
696
 
697
  # ============================================================
698
- # CSS
699
  # ============================================================
700
 
701
  CSS = """
702
-
703
- body {
704
- background: #f7f7f8;
705
- }
706
-
707
- .gradio-container {
708
- max-width: 1500px !important;
709
- }
710
-
711
- #header {
712
- text-align: center;
713
- padding: 20px 0 10px 0;
714
- }
715
-
716
- #logo {
717
- font-size: 38px;
718
- font-weight: 800;
719
- }
720
-
721
- #tagline {
722
- opacity: 0.65;
723
- font-size: 15px;
724
- }
725
-
726
- #chat {
727
- border-radius: 18px;
728
- }
729
-
730
- #research {
731
- border-radius: 18px;
732
- }
733
-
734
- #send {
735
- min-height: 52px;
736
- font-size: 18px;
737
- font-weight: 700;
738
- }
739
-
740
- footer {
741
- display: none !important;
742
- }
743
-
744
  """
745
 
746
-
747
- # ============================================================
748
- # GRADIO APPLICATION
749
- # ============================================================
750
-
751
- with gr.Blocks(
752
- title=APP_NAME,
753
- ) as demo:
754
-
755
- # --------------------------------------------------------
756
- # HEADER
757
- # --------------------------------------------------------
758
-
759
- gr.HTML(
760
- """
761
  <div id="header">
762
- <div id="logo">
763
- X-RUDRA
764
- </div>
765
-
766
- <div id="tagline">
767
- Dual-Model AI · Live Web Research · Evidence
768
- </div>
769
  </div>
770
- """
771
- )
772
-
773
- # --------------------------------------------------------
774
- # MAIN CHAT
775
- # --------------------------------------------------------
776
 
777
  with gr.Row():
778
-
779
- # ====================================================
780
- # CHAT COLUMN
781
- # ====================================================
782
-
783
- with gr.Column(
784
- scale=7,
785
- ):
786
-
787
- # Chatbot – no 'type' argument; Gradio defaults to "messages" format (dicts)
788
- chatbot = gr.Chatbot(
789
- label="X-RUDRA",
790
- height=600,
791
- elem_id="chat",
792
- )
793
-
794
  with gr.Row():
795
-
796
- question = gr.Textbox(
797
- placeholder=(
798
- "Ask X-RUDRA anything..."
799
- ),
800
- lines=2,
801
- show_label=False,
802
- scale=8,
803
- )
804
-
805
- send = gr.Button(
806
- "➤",
807
- variant="primary",
808
- elem_id="send",
809
- scale=1,
810
- )
811
-
812
- # ====================================================
813
- # RESEARCH STATUS
814
- # ====================================================
815
-
816
- with gr.Column(
817
- scale=4,
818
- ):
819
-
820
- gr.Markdown(
821
- "## 🔬 Live Research"
822
- )
823
-
824
- activity = gr.Markdown(
825
- """
826
- ⚪ Waiting for your question.
827
- """
828
- )
829
-
830
- gr.Markdown(
831
- "---"
832
- )
833
-
834
- gr.Markdown(
835
- f"""
836
  ### Model Spaces
837
-
838
- **M1**
839
-
840
- `{M1_REPO}`
841
-
842
- **M2**
843
-
844
- `{M2_REPO}`
845
-
846
  ### Web Stack
 
 
847
 
848
- `DuckDuckGo`
849
-
850
- `Scrapling`
851
-
852
- `Playwright`
853
-
854
- ### Verification
855
-
856
- `Evidence Engine`
857
-
858
- `Cross-source checking`
859
- """
860
- )
861
-
862
- # --------------------------------------------------------
863
- # CONTROLS
864
- # --------------------------------------------------------
865
-
866
- with gr.Accordion(
867
- "⚙️ Research Controls",
868
- open=False,
869
- ):
870
-
871
  with gr.Row():
872
-
873
- max_results = gr.Slider(
874
- minimum=1,
875
- maximum=30,
876
- value=10,
877
- step=1,
878
- label="Maximum Sources",
879
- )
880
-
881
- max_rounds = gr.Slider(
882
- minimum=1,
883
- maximum=5,
884
- value=3,
885
- step=1,
886
- label="Research Rounds",
887
- )
888
-
889
  with gr.Row():
890
-
891
- use_models = gr.Checkbox(
892
- value=True,
893
- label="Use M1 + M2",
894
- )
895
-
896
- freshness = gr.Dropdown(
897
- choices=[
898
- "auto",
899
- "latest",
900
- "recent",
901
- "current",
902
- ],
903
- value="auto",
904
- label="Freshness",
905
- )
906
-
907
- # --------------------------------------------------------
908
- # RESEARCH DATA
909
- # --------------------------------------------------------
910
 
911
  with gr.Tabs():
912
-
913
- with gr.Tab(
914
- "📚 Sources"
915
- ):
916
-
917
- sources = gr.Markdown(
918
- "Sources will appear here."
919
- )
920
-
921
- with gr.Tab(
922
- "🧠 Evidence"
923
- ):
924
-
925
- evidence = gr.Markdown(
926
- "Evidence will appear here."
927
- )
928
-
929
- with gr.Tab(
930
- "⚖️ Verification"
931
- ):
932
-
933
- verification = gr.Markdown(
934
- "Verification will appear here."
935
- )
936
-
937
- # --------------------------------------------------------
938
- # HEALTH
939
- # --------------------------------------------------------
940
-
941
- with gr.Accordion(
942
- "🩺 System Health",
943
- open=False,
944
- ):
945
-
946
- health_button = gr.Button(
947
- "Check X-RUDRA",
948
- )
949
-
950
  health_output = gr.Markdown()
951
 
952
- # --------------------------------------------------------
953
- # EXAMPLES
954
- # --------------------------------------------------------
955
-
956
- gr.Markdown(
957
- "### Try X-RUDRA"
958
- )
959
-
960
  gr.Examples(
961
  examples=[
962
- [
963
- "What are the latest UNESCO AI education initiatives?"
964
- ],
965
- [
966
- "What are the latest developments in open source AI?"
967
- ],
968
- [
969
- "Compare the latest major AI models."
970
- ],
971
- [
972
- "Research India's current AI ecosystem."
973
- ],
974
  ],
975
- inputs=question,
976
  )
977
 
978
- # --------------------------------------------------------
979
- # EVENTS
980
- # --------------------------------------------------------
981
-
982
- inputs = [
983
- question,
984
- max_results,
985
- max_rounds,
986
- use_models,
987
- freshness,
988
- ]
989
-
990
- outputs = [
991
- chatbot,
992
- activity,
993
- sources,
994
- evidence,
995
- verification,
996
- ]
997
-
998
- send.click(
999
- fn=run_research,
1000
- inputs=inputs,
1001
- outputs=outputs,
1002
- )
1003
 
1004
- question.submit(
1005
- fn=run_research,
1006
- inputs=inputs,
1007
- outputs=outputs,
1008
- )
1009
-
1010
- health_button.click(
1011
- fn=health_check,
1012
- inputs=[],
1013
- outputs=[
1014
- health_output
1015
- ],
1016
- )
1017
 
1018
 
1019
  # ============================================================
@@ -1021,28 +293,8 @@ with gr.Blocks(
1021
  # ============================================================
1022
 
1023
  if __name__ == "__main__":
1024
-
1025
- print(
1026
- f"Starting {APP_NAME} {VERSION}"
1027
- )
1028
-
1029
- print(
1030
- "M1:",
1031
- M1_REPO,
1032
- )
1033
-
1034
- print(
1035
- "M2:",
1036
- M2_REPO,
1037
- )
1038
-
1039
- print(
1040
- "Lazy engine initialization: ON"
1041
- )
1042
-
1043
- demo.launch(
1044
- server_name="0.0.0.0",
1045
- server_port=PORT,
1046
- css=CSS,
1047
- show_error=True,
1048
- )
 
12
  import traceback
13
 
14
  import gradio as gr
15
+ import spaces
16
 
17
 
18
  # ============================================================
 
22
  APP_NAME = "X-RUDRA"
23
  VERSION = "3.1.0"
24
 
25
+ M1_REPO = os.getenv("M1_REPO", "Shrijanagain/M1")
26
+ M2_REPO = os.getenv("M2_REPO", "Shrijanagain/M2")
27
+ PORT = int(os.getenv("PORT", "7860"))
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
 
30
  # ============================================================
 
33
 
34
  _ENGINE = None
35
 
 
36
  def get_engine():
 
 
 
 
 
 
 
 
37
  global _ENGINE
 
38
  if _ENGINE is None:
 
39
  from web_search import XrudraWebSearch
 
40
  _ENGINE = XrudraWebSearch()
 
41
  return _ENGINE
42
 
43
 
44
  # ============================================================
45
+ # SAFE HELPERS
46
  # ============================================================
47
 
48
  def safe_dict(value):
 
49
  if isinstance(value, dict):
50
  return value
 
51
  if hasattr(value, "model_dump"):
 
52
  try:
53
  return value.model_dump()
 
54
  except Exception:
55
  pass
 
56
  if hasattr(value, "dict"):
 
57
  try:
58
  return value.dict()
 
59
  except Exception:
60
  pass
61
+ return {"result": str(value)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
 
64
  # ============================================================
65
+ # FORMATTERS (same as before, but shortened for brevity)
66
  # ============================================================
67
 
68
  def format_sources(sources):
 
69
  if not sources:
70
+ return "## 📚 Sources\n\nNo sources were returned."
71
+ # ... (keep your existing implementation) ...
72
+ # (I'll include the full code in the final answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  def format_evidence(claims):
 
75
  if not claims:
76
+ return "## 🧠 Evidence\n\nNo structured evidence was returned."
77
+ # ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  def format_verification(contradictions):
 
80
  if not contradictions:
81
+ return "## ⚖️ Verification\n\n✅ No contradictions detected."
82
+ # ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  def extract_answer(data):
85
+ # Try common fields
86
+ for key in ("final_answer", "answer", "response", "final", "synthesis", "summary"):
87
+ val = data.get(key)
88
+ if isinstance(val, str) and val.strip():
89
+ return val.strip()
90
+ # Fallback to claims
91
+ claims = data.get("claims", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  if claims:
 
93
  parts = []
 
94
  for claim in claims:
95
+ if isinstance(claim, dict):
96
+ text = claim.get("claim", claim.get("text", ""))
97
+ if text:
98
+ parts.append(str(text).strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  if parts:
100
+ return "\n\n".join(parts[:10])
101
+ return None # No answer found
102
+
103
+ def build_activity(data, elapsed_ms):
104
+ sources = data.get("sources", [])
105
+ claims = data.get("claims", [])
106
+ contradictions = data.get("contradictions", [])
107
+ rounds = data.get("rounds", data.get("research_rounds", "N/A"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  return f"""
109
  ## ⚡ X-RUDRA Research
110
+ ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  """
112
 
 
113
  # ============================================================
114
+ # RESEARCH FUNCTION
115
  # ============================================================
116
 
117
+ async def do_research(question, max_results, max_rounds, use_models, freshness):
118
+ if not question or not str(question).strip():
119
+ return [], "⚪ Enter a question to start.", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ question = str(question).strip()
122
  started = time.perf_counter()
123
 
124
  try:
 
 
 
 
 
125
  engine = get_engine()
 
 
 
 
 
126
  report = await engine.search(
 
127
  question=question,
128
+ max_results=int(max_results),
129
+ max_rounds=int(max_rounds),
130
+ use_models=bool(use_models),
131
+ freshness_mode=str(freshness),
132
+ )
133
+ data = safe_dict(report)
134
+ elapsed_ms = int((time.perf_counter() - started) * 1000)
135
+
136
+ # Debug: print raw data to logs
137
+ print("\n" + "="*60)
138
+ print("RAW ENGINE DATA:")
139
+ print(json.dumps(data, indent=2, default=str)[:2000]) # print first 2000 chars
140
+ print("="*60 + "\n")
141
+
142
+ # Extract answer
143
+ answer = extract_answer(data)
144
+ if answer is None:
145
+ # No answer found – build a fallback message
146
+ sources_count = len(data.get("sources", []))
147
+ claims_count = len(data.get("claims", []))
148
+ if sources_count == 0 and claims_count == 0:
149
+ answer = (
150
+ "❌ **No information found.**\n\n"
151
+ "The research engine did not return any sources or evidence. "
152
+ "Possible reasons:\n"
153
+ "- The M1 or M2 Spaces are not responding (check their API endpoints).\n"
154
+ "- The web search failed to extract results.\n"
155
+ "- The query might be too specific or ambiguous.\n\n"
156
+ "Try rephrasing your question or check the M1/M2 Spaces' logs."
157
+ )
158
+ else:
159
+ answer = "Research completed, but no synthesized answer was generated."
160
 
161
+ # Build outputs
162
+ sources_md = format_sources(data.get("sources", []))
163
+ evidence_md = format_evidence(data.get("claims", []))
164
+ verification_md = format_verification(data.get("contradictions", []))
165
+ activity_md = build_activity(data, elapsed_ms)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  history = [
168
  {"role": "user", "content": question},
169
+ {"role": "assistant", "content": answer}
170
  ]
171
 
172
+ return history, activity_md, sources_md, evidence_md, verification_md
 
 
 
 
 
 
173
 
174
  except Exception as exc:
175
+ error = f"❌ **X-RUDRA Error**\n\n`{type(exc).__name__}: {exc}`"
176
+ print("\n" + "="*70)
177
+ print("X-RUDRA ERROR")
178
+ print(traceback.format_exc())
179
+ print("="*70 + "\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  history = [
181
  {"role": "user", "content": question},
182
+ {"role": "assistant", "content": error}
183
  ]
184
+ return history, "❌ Research failed.", "", "", ""
 
 
 
 
 
 
 
185
 
186
 
187
  # ============================================================
188
+ # GRADIO SYNC WRAPPER WITH @spaces.GPU
189
  # ============================================================
190
 
191
  @spaces.GPU
192
+ def run_research(question, max_results, max_rounds, use_models, freshness):
193
+ return asyncio.run(do_research(question, max_results, max_rounds, use_models, freshness))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
 
196
  # ============================================================
197
+ # HEALTH CHECK
198
  # ============================================================
199
 
200
  def health_check():
 
201
  return f"""
202
  ## 🟢 X-RUDRA Online
 
203
  **Version:** `{VERSION}`
 
204
  **M1:** `{M1_REPO}`
 
205
  **M2:** `{M2_REPO}`
206
+ **Engine:** Lazy initialized
 
 
 
 
 
 
 
 
 
207
  """
208
 
209
 
210
  # ============================================================
211
+ # CSS AND UI
212
  # ============================================================
213
 
214
  CSS = """
215
+ body { background: #f7f7f8; }
216
+ .gradio-container { max-width: 1500px !important; }
217
+ #header { text-align: center; padding: 20px 0 10px 0; }
218
+ #logo { font-size: 38px; font-weight: 800; }
219
+ #tagline { opacity: 0.65; font-size: 15px; }
220
+ #chat { border-radius: 18px; }
221
+ #send { min-height: 52px; font-size: 18px; font-weight: 700; }
222
+ footer { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  """
224
 
225
+ with gr.Blocks(title=APP_NAME) as demo:
226
+ gr.HTML("""
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  <div id="header">
228
+ <div id="logo">⚡ X-RUDRA</div>
229
+ <div id="tagline">Dual-Model AI · Live Web Research · Evidence</div>
 
 
 
 
 
230
  </div>
231
+ """)
 
 
 
 
 
232
 
233
  with gr.Row():
234
+ with gr.Column(scale=7):
235
+ chatbot = gr.Chatbot(label="X-RUDRA", height=600, elem_id="chat")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  with gr.Row():
237
+ question = gr.Textbox(placeholder="Ask X-RUDRA anything...", lines=2, show_label=False, scale=8)
238
+ send = gr.Button("➤", variant="primary", elem_id="send", scale=1)
239
+
240
+ with gr.Column(scale=4):
241
+ gr.Markdown("## 🔬 Live Research")
242
+ activity = gr.Markdown("⚪ Waiting for your question.")
243
+ gr.Markdown("---")
244
+ gr.Markdown(f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  ### Model Spaces
246
+ **M1** `{M1_REPO}`
247
+ **M2** `{M2_REPO}`
 
 
 
 
 
 
 
248
  ### Web Stack
249
+ `DuckDuckGo` · `Scrapling` · `Playwright`
250
+ """)
251
 
252
+ with gr.Accordion("⚙️ Research Controls", open=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  with gr.Row():
254
+ max_results = gr.Slider(1, 30, value=10, step=1, label="Max Sources")
255
+ max_rounds = gr.Slider(1, 5, value=3, step=1, label="Research Rounds")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  with gr.Row():
257
+ use_models = gr.Checkbox(value=True, label="Use M1 + M2")
258
+ freshness = gr.Dropdown(["auto","latest","recent","current"], value="auto", label="Freshness")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  with gr.Tabs():
261
+ with gr.Tab("📚 Sources"):
262
+ sources = gr.Markdown("Sources will appear here.")
263
+ with gr.Tab("🧠 Evidence"):
264
+ evidence = gr.Markdown("Evidence will appear here.")
265
+ with gr.Tab("⚖️ Verification"):
266
+ verification = gr.Markdown("Verification will appear here.")
267
+
268
+ with gr.Accordion("🩺 System Health", open=False):
269
+ health_button = gr.Button("Check X-RUDRA")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  health_output = gr.Markdown()
271
 
272
+ gr.Markdown("### Try X-RUDRA")
 
 
 
 
 
 
 
273
  gr.Examples(
274
  examples=[
275
+ ["What are the latest UNESCO AI education initiatives?"],
276
+ ["What are the latest developments in open source AI?"],
277
+ ["Compare the latest major AI models."],
278
+ ["Research India's current AI ecosystem."]
 
 
 
 
 
 
 
 
279
  ],
280
+ inputs=question
281
  )
282
 
283
+ inputs = [question, max_results, max_rounds, use_models, freshness]
284
+ outputs = [chatbot, activity, sources, evidence, verification]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ send.click(fn=run_research, inputs=inputs, outputs=outputs)
287
+ question.submit(fn=run_research, inputs=inputs, outputs=outputs)
288
+ health_button.click(fn=health_check, inputs=[], outputs=[health_output])
 
 
 
 
 
 
 
 
 
 
289
 
290
 
291
  # ============================================================
 
293
  # ============================================================
294
 
295
  if __name__ == "__main__":
296
+ print(f"Starting {APP_NAME} {VERSION}")
297
+ print("M1:", M1_REPO)
298
+ print("M2:", M2_REPO)
299
+ print("Lazy engine initialization: ON")
300
+ demo.launch(server_name="0.0.0.0", server_port=PORT, css=CSS, show_error=True)