Nikhil Pravin Pise commited on
Commit
40cdc42
Β·
1 Parent(s): 57b1d14

feat: Switch to Groq as primary LLM provider

Browse files
Files changed (9) hide show
  1. GRADIO_APP.md +1 -1
  2. MCP_SERVER.md +1 -0
  3. README.md +6 -4
  4. app.py +72 -34
  5. requirements.txt +1 -0
  6. server.py +63 -19
  7. src/config.py +2 -0
  8. src/knowledge.py +70 -41
  9. src/shared_config.py +3 -1
GRADIO_APP.md CHANGED
@@ -58,7 +58,7 @@ Subtle translucency (`backdrop-filter: blur(12px)`) creates a sense of depth. Th
58
 
59
  ### 🎧 The Narrator (Sonic Mode)
60
  - **Design**: Modeled after premium music players
61
- - **Tech**: Gemini 2.0 Flash summarization + ElevenLabs TTS
62
  - **Feature**: Natural sentence endings for clean audio
63
 
64
  ### 🧠 The Analyst (Intelligence)
 
58
 
59
  ### 🎧 The Narrator (Sonic Mode)
60
  - **Design**: Modeled after premium music players
61
+ - **Tech**: Groq Llama 3.1 summarization + ElevenLabs TTS
62
  - **Feature**: Natural sentence endings for clean audio
63
 
64
  ### 🧠 The Analyst (Intelligence)
MCP_SERVER.md CHANGED
@@ -87,6 +87,7 @@ Add to your `claude_desktop_config.json`:
87
  "command": "python",
88
  "args": ["c:/Dev/Medium-Agent/medium-mcp/server.py"],
89
  "env": {
 
90
  "GEMINI_API_KEY": "your-key",
91
  "ELEVENLABS_API_KEY": "your-key",
92
  "OPENAI_API_KEY": "your-key"
 
87
  "command": "python",
88
  "args": ["c:/Dev/Medium-Agent/medium-mcp/server.py"],
89
  "env": {
90
+ "GROQ_API_KEY": "your-key",
91
  "GEMINI_API_KEY": "your-key",
92
  "ELEVENLABS_API_KEY": "your-key",
93
  "OPENAI_API_KEY": "your-key"
README.md CHANGED
@@ -52,7 +52,7 @@ By combining **Agentic Workflows**, **Neural Audio**, and the **Model Context Pr
52
 
53
  ### 🎨 UI Enhancements
54
  - **🏠 Hero Tab**: Beautiful landing page with feature overview and gradient design
55
- - **🎧 Improved Audio**: Gemini 2.0 Flash for summarization with natural sentence endings
56
  - **πŸ“Š Enhanced Intelligence**: Streamlined analyst reports with PDF export
57
 
58
  ---
@@ -101,7 +101,7 @@ The system operates as an autonomous research pipeline:
101
  1. **User** asks a question via the **Project Aether UI**
102
  2. **Scout Agent** searches DuckDuckGo & RSS for fresh signals
103
  3. **Reader Agent** extracts clean text, bypassing paywalls
104
- 4. **Analyst Agent** (Gemini 2.0 Flash) synthesizes professional reports
105
  5. **Sonic Agent** (ElevenLabs) converts content to podcast audio
106
 
107
  ---
@@ -112,7 +112,8 @@ The system operates as an autonomous research pipeline:
112
  | :--- | :--- |
113
  | **[ElevenLabs](https://elevenlabs.io/)** | Neural TTS for Sonic Mode |
114
  | **[OpenAI](https://openai.com/)** | TTS fallback & embeddings |
115
- | **[Google Gemini 2.0](https://deepmind.google/technologies/gemini/)** | Intelligence engine for Analyst |
 
116
  | **[Gradio](https://gradio.app/)** | Project Aether web interface |
117
  | **[Playwright](https://playwright.dev/)** | Headless browser scraping |
118
 
@@ -172,7 +173,8 @@ python app.py
172
 
173
  Built with ❀️ for the **Open Source AI Community**.
174
 
175
- * **Google DeepMind**: Gemini 2.0 Flash
 
176
  * **Anthropic**: MCP Standard
177
  * **Hugging Face**: Platform & Infrastructure
178
  * **ElevenLabs**: Neural Voice Technology
 
52
 
53
  ### 🎨 UI Enhancements
54
  - **🏠 Hero Tab**: Beautiful landing page with feature overview and gradient design
55
+ - **🎧 Improved Audio**: Groq Llama for summarization with natural sentence endings
56
  - **πŸ“Š Enhanced Intelligence**: Streamlined analyst reports with PDF export
57
 
58
  ---
 
101
  1. **User** asks a question via the **Project Aether UI**
102
  2. **Scout Agent** searches DuckDuckGo & RSS for fresh signals
103
  3. **Reader Agent** extracts clean text, bypassing paywalls
104
+ 4. **Analyst Agent** (Groq Llama 3.3) synthesizes professional reports
105
  5. **Sonic Agent** (ElevenLabs) converts content to podcast audio
106
 
107
  ---
 
112
  | :--- | :--- |
113
  | **[ElevenLabs](https://elevenlabs.io/)** | Neural TTS for Sonic Mode |
114
  | **[OpenAI](https://openai.com/)** | TTS fallback & embeddings |
115
+ | **[Groq](https://console.groq.com/)** | Primary LLM (fastest inference) |
116
+ | **[Google Gemini](https://deepmind.google/technologies/gemini/)** | Backup LLM + Vision/Embeddings |
117
  | **[Gradio](https://gradio.app/)** | Project Aether web interface |
118
  | **[Playwright](https://playwright.dev/)** | Headless browser scraping |
119
 
 
173
 
174
  Built with ❀️ for the **Open Source AI Community**.
175
 
176
+ * **Groq**: Lightning-fast LLM inference
177
+ * **Google DeepMind**: Gemini for Vision/Embeddings
178
  * **Anthropic**: MCP Standard
179
  * **Hugging Face**: Platform & Infrastructure
180
  * **ElevenLabs**: Neural Voice Technology
app.py CHANGED
@@ -56,8 +56,10 @@ from src.service import ScraperService
56
  from src.html_renderer import render_full_page, BASE_TEMPLATE as RENDERER_TEMPLATE
57
  from src.config import MCPConfig
58
  from elevenlabs_voices import ELEVENLABS_VOICES, VOICE_CATEGORIES, get_voice_id
59
- # Import Gemini for Analyst
60
  import google.generativeai as genai
 
 
61
 
62
  # ============================================================================
63
  # PROJECT AETHER: VISUAL SYSTEM (ENHANCED)
@@ -976,21 +978,14 @@ async def generate_audio(url, voice, summarize, max_chars):
976
  if not text or len(text) < 50:
977
  return '<div style="background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); border-radius: 12px; padding: 16px; text-align: center; margin-top: 16px;"><span style="color: #fca5a5; font-weight: 600; font-size: 14px;">⚠️ Article too short</span></div>', None
978
 
979
- # Summarize with heavily guardrailed prompt
980
  if summarize != "none":
 
981
  gemini_key = os.environ.get("GEMINI_API_KEY")
982
- if gemini_key:
983
- try:
984
- gr.Info("Summarizing for audio...")
985
- genai.configure(api_key=gemini_key)
986
- # Use gemini-2.0-flash as primary, fallback to 1.5 if needed
987
- try:
988
- model = genai.GenerativeModel('gemini-2.0-flash')
989
- except:
990
- model = genai.GenerativeModel('gemini-1.5-flash-latest')
991
-
992
- # HEAVILY GUARDRAILED PROMPT
993
- prompt = f"""You are a professional podcast narrator. Your ONLY task is to summarize the following article for audio narration.
994
 
995
  STRICT RULES:
996
  1. Output ONLY plain English text suitable for text-to-speech
@@ -1010,26 +1005,53 @@ Article Content:
1010
 
1011
  Write a clean, narration-ready summary that ends with a proper concluding sentence:"""
1012
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1013
  response = await model.generate_content_async(prompt)
1014
  summary = response.text.strip()
1015
 
1016
- # Validate response - reject if it contains gibberish markers
1017
  if summary and len(summary) > 50:
1018
- # Additional cleaning of Gemini output
1019
  summary = clean_text_for_audio(summary)
1020
-
1021
- # Reject if too short after cleaning or contains obvious issues
1022
  if len(summary) > 50 and not any(bad in summary.lower() for bad in ['```', 'http://', 'https://', '**', '##']):
1023
  text = summary
 
1024
  else:
1025
  gr.Warning("Summary had issues, using cleaned original")
1026
- text = text[:max_chars]
1027
- else:
1028
- text = text[:max_chars]
1029
  except Exception as e:
1030
- gr.Warning(f"Summarization failed: {str(e)[:50]}")
1031
- text = text[:max_chars]
1032
- else:
 
1033
  text = text[:max_chars]
1034
 
1035
  # Final safety check - ensure text is clean for TTS
@@ -1092,11 +1114,12 @@ async def analyst_report(topic):
1092
  if not topic:
1093
  return "Please enter a topic."
1094
 
 
1095
  gemini_key = os.environ.get("GEMINI_API_KEY")
1096
  openai_key = os.environ.get("OPENAI_API_KEY")
1097
 
1098
- if not gemini_key and not openai_key:
1099
- return "⚠️ Error: No AI API keys found. Set GEMINI_API_KEY or OPENAI_API_KEY in your .env file."
1100
 
1101
  max_articles = 5
1102
 
@@ -1152,11 +1175,25 @@ Articles:
1152
  gr.Info("Analyst: Synthesizing report...")
1153
  report_content = ""
1154
 
1155
- # 4. Try Gemini first
1156
- if gemini_key:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1157
  try:
1158
  genai.configure(api_key=gemini_key)
1159
- # Use gemini-2.0-flash as primary, fallback to 1.5 if needed
1160
  try:
1161
  model = genai.GenerativeModel('gemini-2.0-flash')
1162
  except:
@@ -1167,7 +1204,7 @@ Articles:
1167
  except Exception as e:
1168
  gr.Warning(f"Gemini failed: {str(e)[:100]}, trying OpenAI...")
1169
 
1170
- # 5. Fallback to OpenAI
1171
  if not report_content and openai_key:
1172
  try:
1173
  from openai import AsyncOpenAI
@@ -1286,9 +1323,10 @@ async def export_report_pdf():
1286
 
1287
  def render_settings():
1288
  keys = {
 
1289
  "ElevenLabs": "ELEVENLABS_API_KEY",
1290
- "Gemini": "GEMINI_API_KEY",
1291
- "OpenAI": "OPENAI_API_KEY"
1292
  }
1293
 
1294
  html = "<h3>System Status</h3>"
@@ -1382,7 +1420,7 @@ with gr.Blocks(title="Project Aether") as demo:
1382
  <div style="width: 44px; height: 44px; background: linear-gradient(135deg, #10b981, #34d399); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px;">🧠</div>
1383
  <h3 style="margin: 0; color: #fff; font-size: 1.1rem; font-weight: 600;">AI Analyst</h3>
1384
  </div>
1385
- <p style="color: #a1a1aa; font-size: 0.9rem; margin: 0; line-height: 1.6;">Generate comprehensive intelligence reports. AI-powered synthesis using Gemini & GPT-4.</p>
1386
  </div>
1387
 
1388
  <!-- Settings Card -->
@@ -1391,7 +1429,7 @@ with gr.Blocks(title="Project Aether") as demo:
1391
  <div style="width: 44px; height: 44px; background: linear-gradient(135deg, #6366f1, #818cf8); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px;">βš™οΈ</div>
1392
  <h3 style="margin: 0; color: #fff; font-size: 1.1rem; font-weight: 600;">Configuration</h3>
1393
  </div>
1394
- <p style="color: #a1a1aa; font-size: 0.9rem; margin: 0; line-height: 1.6;">Manage API keys and system status. Connect ElevenLabs, Gemini, and OpenAI.</p>
1395
  </div>
1396
 
1397
  </div>
 
56
  from src.html_renderer import render_full_page, BASE_TEMPLATE as RENDERER_TEMPLATE
57
  from src.config import MCPConfig
58
  from elevenlabs_voices import ELEVENLABS_VOICES, VOICE_CATEGORIES, get_voice_id
59
+ # Import Gemini for Analyst (backup)
60
  import google.generativeai as genai
61
+ # Import Groq for primary LLM
62
+ from groq import Groq
63
 
64
  # ============================================================================
65
  # PROJECT AETHER: VISUAL SYSTEM (ENHANCED)
 
978
  if not text or len(text) < 50:
979
  return '<div style="background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); border-radius: 12px; padding: 16px; text-align: center; margin-top: 16px;"><span style="color: #fca5a5; font-weight: 600; font-size: 14px;">⚠️ Article too short</span></div>', None
980
 
981
+ # Summarize with Groq (PRIMARY) or Gemini (BACKUP)
982
  if summarize != "none":
983
+ groq_key = os.environ.get("GROQ_API_KEY")
984
  gemini_key = os.environ.get("GEMINI_API_KEY")
985
+ summarize_success = False
986
+
987
+ # HEAVILY GUARDRAILED PROMPT (shared between providers)
988
+ prompt = f"""You are a professional podcast narrator. Your ONLY task is to summarize the following article for audio narration.
 
 
 
 
 
 
 
 
989
 
990
  STRICT RULES:
991
  1. Output ONLY plain English text suitable for text-to-speech
 
1005
 
1006
  Write a clean, narration-ready summary that ends with a proper concluding sentence:"""
1007
 
1008
+ # Try Groq first (PRIMARY - fastest)
1009
+ if groq_key and not summarize_success:
1010
+ try:
1011
+ gr.Info("Summarizing for audio with Groq...")
1012
+ client = Groq(api_key=groq_key)
1013
+ response = client.chat.completions.create(
1014
+ model="llama-3.1-8b-instant", # Fast model for summarization
1015
+ messages=[{"role": "user", "content": prompt}],
1016
+ max_tokens=500,
1017
+ temperature=0.7
1018
+ )
1019
+ summary = response.choices[0].message.content.strip()
1020
+
1021
+ # Validate response
1022
+ if summary and len(summary) > 50:
1023
+ summary = clean_text_for_audio(summary)
1024
+ if len(summary) > 50 and not any(bad in summary.lower() for bad in ['```', 'http://', 'https://', '**', '##']):
1025
+ text = summary
1026
+ summarize_success = True
1027
+ except Exception as e:
1028
+ gr.Warning(f"Groq failed: {str(e)[:50]}, trying Gemini...")
1029
+
1030
+ # Fallback to Gemini (BACKUP)
1031
+ if gemini_key and not summarize_success:
1032
+ try:
1033
+ gr.Info("Summarizing for audio with Gemini...")
1034
+ genai.configure(api_key=gemini_key)
1035
+ try:
1036
+ model = genai.GenerativeModel('gemini-2.0-flash')
1037
+ except:
1038
+ model = genai.GenerativeModel('gemini-1.5-flash-latest')
1039
+
1040
  response = await model.generate_content_async(prompt)
1041
  summary = response.text.strip()
1042
 
 
1043
  if summary and len(summary) > 50:
 
1044
  summary = clean_text_for_audio(summary)
 
 
1045
  if len(summary) > 50 and not any(bad in summary.lower() for bad in ['```', 'http://', 'https://', '**', '##']):
1046
  text = summary
1047
+ summarize_success = True
1048
  else:
1049
  gr.Warning("Summary had issues, using cleaned original")
 
 
 
1050
  except Exception as e:
1051
+ gr.Warning(f"Gemini also failed: {str(e)[:50]}")
1052
+
1053
+ # Final fallback: truncate original
1054
+ if not summarize_success:
1055
  text = text[:max_chars]
1056
 
1057
  # Final safety check - ensure text is clean for TTS
 
1114
  if not topic:
1115
  return "Please enter a topic."
1116
 
1117
+ groq_key = os.environ.get("GROQ_API_KEY")
1118
  gemini_key = os.environ.get("GEMINI_API_KEY")
1119
  openai_key = os.environ.get("OPENAI_API_KEY")
1120
 
1121
+ if not groq_key and not gemini_key and not openai_key:
1122
+ return "⚠️ Error: No AI API keys found. Set GROQ_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY in your .env file."
1123
 
1124
  max_articles = 5
1125
 
 
1175
  gr.Info("Analyst: Synthesizing report...")
1176
  report_content = ""
1177
 
1178
+ # 4. Try Groq first (PRIMARY - fastest)
1179
+ if groq_key:
1180
+ try:
1181
+ client = Groq(api_key=groq_key)
1182
+ response = client.chat.completions.create(
1183
+ model="llama-3.3-70b-versatile", # Best model for synthesis
1184
+ messages=[{"role": "user", "content": prompt}],
1185
+ max_tokens=2000,
1186
+ temperature=0.7
1187
+ )
1188
+ report_content = response.choices[0].message.content
1189
+ gr.Info("Report generated via Groq")
1190
+ except Exception as e:
1191
+ gr.Warning(f"Groq failed: {str(e)[:100]}, trying Gemini...")
1192
+
1193
+ # 5. Fallback to Gemini
1194
+ if not report_content and gemini_key:
1195
  try:
1196
  genai.configure(api_key=gemini_key)
 
1197
  try:
1198
  model = genai.GenerativeModel('gemini-2.0-flash')
1199
  except:
 
1204
  except Exception as e:
1205
  gr.Warning(f"Gemini failed: {str(e)[:100]}, trying OpenAI...")
1206
 
1207
+ # 6. Fallback to OpenAI
1208
  if not report_content and openai_key:
1209
  try:
1210
  from openai import AsyncOpenAI
 
1323
 
1324
  def render_settings():
1325
  keys = {
1326
+ "Groq": "GROQ_API_KEY", # PRIMARY LLM
1327
  "ElevenLabs": "ELEVENLABS_API_KEY",
1328
+ "Gemini": "GEMINI_API_KEY", # BACKUP LLM
1329
+ "OpenAI": "OPENAI_API_KEY" # BACKUP LLM
1330
  }
1331
 
1332
  html = "<h3>System Status</h3>"
 
1420
  <div style="width: 44px; height: 44px; background: linear-gradient(135deg, #10b981, #34d399); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px;">🧠</div>
1421
  <h3 style="margin: 0; color: #fff; font-size: 1.1rem; font-weight: 600;">AI Analyst</h3>
1422
  </div>
1423
+ <p style="color: #a1a1aa; font-size: 0.9rem; margin: 0; line-height: 1.6;">Generate comprehensive intelligence reports. AI-powered synthesis using Groq & GPT-4.</p>
1424
  </div>
1425
 
1426
  <!-- Settings Card -->
 
1429
  <div style="width: 44px; height: 44px; background: linear-gradient(135deg, #6366f1, #818cf8); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px;">βš™οΈ</div>
1430
  <h3 style="margin: 0; color: #fff; font-size: 1.1rem; font-weight: 600;">Configuration</h3>
1431
  </div>
1432
+ <p style="color: #a1a1aa; font-size: 0.9rem; margin: 0; line-height: 1.6;">Manage API keys and system status. Connect ElevenLabs, Groq, and OpenAI.</p>
1433
  </div>
1434
 
1435
  </div>
requirements.txt CHANGED
@@ -32,6 +32,7 @@ fastmcp>=0.2.0
32
  # ============================================================================
33
  google-generativeai>=0.3.0
34
  openai>=1.3.0
 
35
 
36
  # ============================================================================
37
  # Text-to-Speech
 
32
  # ============================================================================
33
  google-generativeai>=0.3.0
34
  openai>=1.3.0
35
+ groq>=0.13.0
36
 
37
  # ============================================================================
38
  # Text-to-Speech
server.py CHANGED
@@ -28,6 +28,9 @@ from elevenlabs_voices import ELEVENLABS_VOICES, get_voice_id, get_voices_info,
28
  from src.service import ScraperService
29
  from src.html_renderer import render_article_html, render_full_page
30
 
 
 
 
31
 
32
  # ============================================================================
33
  # LIFESPAN MANAGEMENT
@@ -234,7 +237,7 @@ async def get_server_stats() -> str:
234
  "35+ total domains"
235
  ],
236
  "tts_providers": ["elevenlabs", "edge-tts", "openai"],
237
- "ai_providers": ["gemini", "openai"]
238
  }, ensure_ascii=False)
239
 
240
 
@@ -545,14 +548,11 @@ async def medium_cast(
545
  )
546
 
547
  if should_summarize and summarize != "none":
548
- try:
549
- import google.generativeai as genai
550
- gemini_key = os.environ.get("GEMINI_API_KEY")
551
- if gemini_key:
552
- genai.configure(api_key=gemini_key)
553
- gemini_model = genai.GenerativeModel('gemini-2.5-flash')
554
-
555
- prompt = f"""You are creating a quick audio summary for busy professionals. In EXACTLY {max_chars} characters or less, give the ONE most valuable insight or actionable takeaway from this article.
556
 
557
  Format: Start with the key insight, then briefly explain why it matters.
558
  Style: Conversational, engaging, like a smart friend sharing a tip.
@@ -564,14 +564,43 @@ Article Content:
564
  {text[:8000]}
565
 
566
  Your {max_chars}-character summary (make every word count):"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
 
568
  response = await gemini_model.generate_content_async(prompt)
569
  text = response.text.strip()[:max_chars]
 
570
  if ctx:
571
- await ctx.info(f"Summarized: {original_length} -> {len(text)} chars")
572
- except Exception as e:
573
- if ctx:
574
- await ctx.warning(f"Summarization failed: {e}, using truncation")
 
 
 
575
  text = text[:max_chars]
576
  else:
577
  # Just truncate to model limit
@@ -741,15 +770,14 @@ async def medium_synthesize(topic: str, max_articles: int = 5, ctx: Context = No
741
  Returns:
742
  Synthesized research report
743
  """
744
- import google.generativeai as genai
745
-
746
  app = get_app_context(ctx)
747
 
 
748
  gemini_key = os.environ.get("GEMINI_API_KEY")
749
  openai_key = os.environ.get("OPENAI_API_KEY")
750
 
751
- if not gemini_key and not openai_key:
752
- return "Error: Neither GEMINI_API_KEY nor OPENAI_API_KEY is set."
753
 
754
  # Scrape articles
755
  if ctx:
@@ -792,9 +820,25 @@ Articles:
792
  {context_text}
793
  """
794
 
795
- # Try Gemini first
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  if gemini_key:
797
  try:
 
798
  genai.configure(api_key=gemini_key)
799
  model = genai.GenerativeModel('gemini-2.0-flash')
800
  response = await model.generate_content_async(prompt)
@@ -814,7 +858,7 @@ Articles:
814
  )
815
  return response.choices[0].message.content
816
  except Exception as e:
817
- return f"Error: Both Gemini and OpenAI failed. {e}"
818
 
819
  return "Error: No AI service available."
820
 
 
28
  from src.service import ScraperService
29
  from src.html_renderer import render_article_html, render_full_page
30
 
31
+ # LLM imports
32
+ from groq import Groq
33
+
34
 
35
  # ============================================================================
36
  # LIFESPAN MANAGEMENT
 
237
  "35+ total domains"
238
  ],
239
  "tts_providers": ["elevenlabs", "edge-tts", "openai"],
240
+ "ai_providers": ["groq", "gemini", "openai"]
241
  }, ensure_ascii=False)
242
 
243
 
 
548
  )
549
 
550
  if should_summarize and summarize != "none":
551
+ groq_key = os.environ.get("GROQ_API_KEY")
552
+ gemini_key = os.environ.get("GEMINI_API_KEY")
553
+ summarize_success = False
554
+
555
+ prompt = f"""You are creating a quick audio summary for busy professionals. In EXACTLY {max_chars} characters or less, give the ONE most valuable insight or actionable takeaway from this article.
 
 
 
556
 
557
  Format: Start with the key insight, then briefly explain why it matters.
558
  Style: Conversational, engaging, like a smart friend sharing a tip.
 
564
  {text[:8000]}
565
 
566
  Your {max_chars}-character summary (make every word count):"""
567
+
568
+ # Try Groq first (PRIMARY - fastest)
569
+ if groq_key and not summarize_success:
570
+ try:
571
+ client = Groq(api_key=groq_key)
572
+ response = client.chat.completions.create(
573
+ model="llama-3.1-8b-instant", # Fast model for summarization
574
+ messages=[{"role": "user", "content": prompt}],
575
+ max_tokens=500,
576
+ temperature=0.7
577
+ )
578
+ text = response.choices[0].message.content.strip()[:max_chars]
579
+ summarize_success = True
580
+ if ctx:
581
+ await ctx.info(f"Summarized with Groq: {original_length} -> {len(text)} chars")
582
+ except Exception as e:
583
+ if ctx:
584
+ await ctx.warning(f"Groq failed: {e}, trying Gemini...")
585
+
586
+ # Fallback to Gemini (BACKUP)
587
+ if gemini_key and not summarize_success:
588
+ try:
589
+ import google.generativeai as genai
590
+ genai.configure(api_key=gemini_key)
591
+ gemini_model = genai.GenerativeModel('gemini-2.0-flash')
592
 
593
  response = await gemini_model.generate_content_async(prompt)
594
  text = response.text.strip()[:max_chars]
595
+ summarize_success = True
596
  if ctx:
597
+ await ctx.info(f"Summarized with Gemini: {original_length} -> {len(text)} chars")
598
+ except Exception as e:
599
+ if ctx:
600
+ await ctx.warning(f"Gemini also failed: {e}, using truncation")
601
+
602
+ # Final fallback: truncation
603
+ if not summarize_success:
604
  text = text[:max_chars]
605
  else:
606
  # Just truncate to model limit
 
770
  Returns:
771
  Synthesized research report
772
  """
 
 
773
  app = get_app_context(ctx)
774
 
775
+ groq_key = os.environ.get("GROQ_API_KEY")
776
  gemini_key = os.environ.get("GEMINI_API_KEY")
777
  openai_key = os.environ.get("OPENAI_API_KEY")
778
 
779
+ if not groq_key and not gemini_key and not openai_key:
780
+ return "Error: No AI API keys set (GROQ_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY)."
781
 
782
  # Scrape articles
783
  if ctx:
 
820
  {context_text}
821
  """
822
 
823
+ # Try Groq first (PRIMARY - fastest)
824
+ if groq_key:
825
+ try:
826
+ client = Groq(api_key=groq_key)
827
+ response = client.chat.completions.create(
828
+ model="llama-3.3-70b-versatile", # Best model for synthesis
829
+ messages=[{"role": "user", "content": prompt}],
830
+ max_tokens=2000,
831
+ temperature=0.7
832
+ )
833
+ return response.choices[0].message.content
834
+ except Exception as e:
835
+ if ctx:
836
+ await ctx.warning(f"Groq failed: {e}")
837
+
838
+ # Fallback to Gemini
839
  if gemini_key:
840
  try:
841
+ import google.generativeai as genai
842
  genai.configure(api_key=gemini_key)
843
  model = genai.GenerativeModel('gemini-2.0-flash')
844
  response = await model.generate_content_async(prompt)
 
858
  )
859
  return response.choices[0].message.content
860
  except Exception as e:
861
+ return f"Error: All providers failed. Last error: {e}"
862
 
863
  return "Error: No AI service available."
864
 
src/config.py CHANGED
@@ -28,6 +28,7 @@ class Config:
28
  DB_PATH = ":memory:" if os.getenv("SPACE_ID") else os.path.join(BASE_DIR, "articles.db")
29
 
30
  # API Keys (from shared config)
 
31
  GEMINI_API_KEY = _shared.gemini_api_key or os.getenv("GEMINI_API_KEY")
32
 
33
  # Scraping Settings (from shared config)
@@ -81,6 +82,7 @@ class Config:
81
  @classmethod
82
  def reload_config(cls):
83
  cls._shared = SharedConfig.from_env()
 
84
  cls.GEMINI_API_KEY = cls._shared.gemini_api_key or os.getenv("GEMINI_API_KEY")
85
  cls.TIMEOUT_MS = cls._shared.default_timeout * 1000
86
  cls.MAX_WORKERS = int(os.getenv("MAX_WORKERS", cls._shared.max_workers))
 
28
  DB_PATH = ":memory:" if os.getenv("SPACE_ID") else os.path.join(BASE_DIR, "articles.db")
29
 
30
  # API Keys (from shared config)
31
+ GROQ_API_KEY = _shared.groq_api_key or os.getenv("GROQ_API_KEY")
32
  GEMINI_API_KEY = _shared.gemini_api_key or os.getenv("GEMINI_API_KEY")
33
 
34
  # Scraping Settings (from shared config)
 
82
  @classmethod
83
  def reload_config(cls):
84
  cls._shared = SharedConfig.from_env()
85
+ cls.GROQ_API_KEY = cls._shared.groq_api_key or os.getenv("GROQ_API_KEY")
86
  cls.GEMINI_API_KEY = cls._shared.gemini_api_key or os.getenv("GEMINI_API_KEY")
87
  cls.TIMEOUT_MS = cls._shared.default_timeout * 1000
88
  cls.MAX_WORKERS = int(os.getenv("MAX_WORKERS", cls._shared.max_workers))
src/knowledge.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  import google.generativeai as genai
 
3
  from typing import Dict, Any, List, Optional
4
  import json
5
 
@@ -8,51 +9,79 @@ import logging
8
 
9
  logger = logging.getLogger("KnowledgeGraph")
10
 
11
- # Configure Gemini
12
- if Config.GEMINI_API_KEY:
13
- genai.configure(api_key=Config.GEMINI_API_KEY)
14
-
15
  def extract_knowledge_graph(text: str) -> Optional[Dict[str, Any]]:
16
  """
17
- Uses Gemini to extract a Knowledge Graph (Concepts & Relationships) from text.
18
  Returns a JSON object with 'concepts' and 'relationships'.
19
  """
20
- if not Config.GEMINI_API_KEY:
21
- logger.warning("GEMINI_API_KEY not set. Skipping Knowledge Graph extraction.")
 
 
 
22
  return None
23
 
24
- try:
25
- model = genai.GenerativeModel('gemini-1.5-flash')
26
-
27
- prompt = f"""
28
- Analyze the following text and extract a Knowledge Graph.
29
- Identify key "Concepts" (entities, technologies, ideas) and "Relationships" between them.
30
-
31
- Return ONLY a valid JSON object with this structure:
32
- {{
33
- "concepts": [
34
- {{"id": "concept_name", "type": "technology/person/idea", "description": "short definition"}}
35
- ],
36
- "relationships": [
37
- {{"source": "concept_name", "target": "concept_name", "relation": "uses/created/is_a"}}
38
- ]
39
- }}
40
-
41
- Text:
42
- {text[:10000]} # Limit text length to avoid token limits
43
- """
44
-
45
- response = model.generate_content(prompt)
46
-
47
- # Clean up response (remove markdown code blocks if present)
48
- content = response.text.strip()
49
- if content.startswith("```json"):
50
- content = content[7:]
51
- if content.endswith("```"):
52
- content = content[:-3]
53
 
54
- return json.loads(content)
55
-
56
- except Exception as e:
57
- print(f"Error extracting Knowledge Graph: {e}")
58
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import google.generativeai as genai
3
+ from groq import Groq
4
  from typing import Dict, Any, List, Optional
5
  import json
6
 
 
9
 
10
  logger = logging.getLogger("KnowledgeGraph")
11
 
 
 
 
 
12
  def extract_knowledge_graph(text: str) -> Optional[Dict[str, Any]]:
13
  """
14
+ Uses Groq (primary) or Gemini (backup) to extract a Knowledge Graph (Concepts & Relationships) from text.
15
  Returns a JSON object with 'concepts' and 'relationships'.
16
  """
17
+ groq_key = os.environ.get("GROQ_API_KEY") or Config.GROQ_API_KEY
18
+ gemini_key = os.environ.get("GEMINI_API_KEY") or Config.GEMINI_API_KEY
19
+
20
+ if not groq_key and not gemini_key:
21
+ logger.warning("No LLM API key set (GROQ_API_KEY or GEMINI_API_KEY). Skipping Knowledge Graph extraction.")
22
  return None
23
 
24
+ prompt = f"""
25
+ Analyze the following text and extract a Knowledge Graph.
26
+ Identify key "Concepts" (entities, technologies, ideas) and "Relationships" between them.
27
+
28
+ Return ONLY a valid JSON object with this structure:
29
+ {{
30
+ "concepts": [
31
+ {{"id": "concept_name", "type": "technology/person/idea", "description": "short definition"}}
32
+ ],
33
+ "relationships": [
34
+ {{"source": "concept_name", "target": "concept_name", "relation": "uses/created/is_a"}}
35
+ ]
36
+ }}
37
+
38
+ Text:
39
+ {text[:10000]}
40
+ """
41
+
42
+ # Try Groq first (PRIMARY - fastest)
43
+ if groq_key:
44
+ try:
45
+ client = Groq(api_key=groq_key)
46
+ response = client.chat.completions.create(
47
+ model="llama-3.3-70b-versatile",
48
+ messages=[{"role": "user", "content": prompt}],
49
+ max_tokens=2000,
50
+ temperature=0.3
51
+ )
52
+ content = response.choices[0].message.content.strip()
53
 
54
+ # Clean up response (remove markdown code blocks if present)
55
+ if content.startswith("```json"):
56
+ content = content[7:]
57
+ if content.startswith("```"):
58
+ content = content[3:]
59
+ if content.endswith("```"):
60
+ content = content[:-3]
61
+
62
+ return json.loads(content.strip())
63
+ except Exception as e:
64
+ logger.warning(f"Groq failed for Knowledge Graph: {e}, trying Gemini...")
65
+
66
+ # Fallback to Gemini (BACKUP)
67
+ if gemini_key:
68
+ try:
69
+ genai.configure(api_key=gemini_key)
70
+ model = genai.GenerativeModel('gemini-1.5-flash')
71
+
72
+ response = model.generate_content(prompt)
73
+
74
+ # Clean up response (remove markdown code blocks if present)
75
+ content = response.text.strip()
76
+ if content.startswith("```json"):
77
+ content = content[7:]
78
+ if content.endswith("```"):
79
+ content = content[:-3]
80
+
81
+ return json.loads(content)
82
+
83
+ except Exception as e:
84
+ logger.error(f"Gemini also failed for Knowledge Graph: {e}")
85
+ return None
86
+
87
+ return None
src/shared_config.py CHANGED
@@ -33,6 +33,7 @@ class SharedConfig:
33
  # ========================================================================
34
  # API Keys
35
  # ========================================================================
 
36
  gemini_api_key: Optional[str] = None
37
  openai_api_key: Optional[str] = None
38
  elevenlabs_api_key: Optional[str] = None
@@ -128,6 +129,7 @@ class SharedConfig:
128
  max_concurrency=get_env("MAX_CONCURRENCY", 5, int),
129
 
130
  # API Keys
 
131
  gemini_api_key=get_env("GEMINI_API_KEY"),
132
  openai_api_key=get_env("OPENAI_API_KEY"),
133
  elevenlabs_api_key=get_env("ELEVENLABS_API_KEY"),
@@ -179,7 +181,7 @@ class SharedConfig:
179
  safe_dict = self.to_dict()
180
 
181
  # Mask sensitive keys
182
- sensitive_keys = ['gemini_api_key', 'openai_api_key', 'elevenlabs_api_key']
183
  for key in sensitive_keys:
184
  if safe_dict.get(key):
185
  safe_dict[key] = safe_dict[key][:8] + "..." if safe_dict[key] else None
 
33
  # ========================================================================
34
  # API Keys
35
  # ========================================================================
36
+ groq_api_key: Optional[str] = None
37
  gemini_api_key: Optional[str] = None
38
  openai_api_key: Optional[str] = None
39
  elevenlabs_api_key: Optional[str] = None
 
129
  max_concurrency=get_env("MAX_CONCURRENCY", 5, int),
130
 
131
  # API Keys
132
+ groq_api_key=get_env("GROQ_API_KEY"),
133
  gemini_api_key=get_env("GEMINI_API_KEY"),
134
  openai_api_key=get_env("OPENAI_API_KEY"),
135
  elevenlabs_api_key=get_env("ELEVENLABS_API_KEY"),
 
181
  safe_dict = self.to_dict()
182
 
183
  # Mask sensitive keys
184
+ sensitive_keys = ['groq_api_key', 'gemini_api_key', 'openai_api_key', 'elevenlabs_api_key']
185
  for key in sensitive_keys:
186
  if safe_dict.get(key):
187
  safe_dict[key] = safe_dict[key][:8] + "..." if safe_dict[key] else None