Spaces:
Sleeping
Sleeping
Nikhil Pravin Pise commited on
Commit Β·
40cdc42
1
Parent(s): 57b1d14
feat: Switch to Groq as primary LLM provider
Browse files- GRADIO_APP.md +1 -1
- MCP_SERVER.md +1 -0
- README.md +6 -4
- app.py +72 -34
- requirements.txt +1 -0
- server.py +63 -19
- src/config.py +2 -0
- src/knowledge.py +70 -41
- 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**:
|
| 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**:
|
| 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** (
|
| 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 |
-
| **[
|
|
|
|
| 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 |
-
* **
|
|
|
|
| 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
|
| 980 |
if summarize != "none":
|
|
|
|
| 981 |
gemini_key = os.environ.get("GEMINI_API_KEY")
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 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"
|
| 1031 |
-
|
| 1032 |
-
|
|
|
|
| 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
|
| 1156 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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
|
| 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,
|
| 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 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 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 |
-
|
| 573 |
-
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 753 |
|
| 754 |
# Scrape articles
|
| 755 |
if ctx:
|
|
@@ -792,9 +820,25 @@ Articles:
|
|
| 792 |
{context_text}
|
| 793 |
"""
|
| 794 |
|
| 795 |
-
# Try
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
return None
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
content =
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|