Spaces:
Sleeping
Sleeping
ego commited on
Commit ·
ba7bcd3
1
Parent(s): ea8f8db
1.0
Browse files- .env +2 -0
- .streamlit/config.toml +5 -0
- Dockerfile +1 -1
- __pycache__/prompts.cpython-312.pyc +0 -0
- agent_workflow.png +0 -0
- app.py +44 -27
- core/__pycache__/__init__.cpython-312.pyc +0 -0
- core/__pycache__/graph.cpython-312.pyc +0 -0
- core/__pycache__/map_reduce.cpython-312.pyc +0 -0
- core/__pycache__/models.cpython-312.pyc +0 -0
- core/__pycache__/pdf_processer.cpython-312.pyc +0 -0
- core/__pycache__/podcast.cpython-312.pyc +0 -0
- core/__pycache__/visualizer.cpython-312.pyc +0 -0
- core/graph.py +18 -12
- core/models.py +50 -1
- core/pdf_processer.py +0 -1
- core/podcast.py +5 -49
- core/visualizer.py +2 -2
- .gitignore → gitignore +0 -0
- prompts.py +56 -41
.env
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GOOGLE_API_KEY=AIzaSyBapGOjPJR58TlTMYcnz7G1jP8fsJXZ1Tg
|
| 2 |
+
NV_API_KEY=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
|
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[server]
|
| 2 |
+
headless = true
|
| 3 |
+
|
| 4 |
+
[browser]
|
| 5 |
+
gatherUsageStats = false
|
Dockerfile
CHANGED
|
@@ -8,4 +8,4 @@ RUN apt-get update && apt-get install -y graphviz
|
|
| 8 |
|
| 9 |
COPY . .
|
| 10 |
|
| 11 |
-
CMD ["streamlit", "run", "app.py", "--server.port", "7860", "--server.address", "0.0.0.0"]
|
|
|
|
| 8 |
|
| 9 |
COPY . .
|
| 10 |
|
| 11 |
+
CMD ["streamlit", "run", "app.py", "--server.port", "7860", "--server.address", "0.0.0.0","--server.enableCORS=false", "--server.enableXsrfProtection=false"]
|
__pycache__/prompts.cpython-312.pyc
ADDED
|
Binary file (6.97 kB). View file
|
|
|
agent_workflow.png
ADDED
|
app.py
CHANGED
|
@@ -74,6 +74,8 @@ if "deep_summary" not in st.session_state:
|
|
| 74 |
st.session_state.deep_summary = None
|
| 75 |
if "graph_dot" not in st.session_state:
|
| 76 |
st.session_state.graph_dot = None
|
|
|
|
|
|
|
| 77 |
|
| 78 |
def switch_page(page_name):
|
| 79 |
st.session_state.page = page_name
|
|
@@ -244,7 +246,7 @@ def view_summary_dialog(text):
|
|
| 244 |
|
| 245 |
@dialog_decorator("Knowledge Graph Visualization", width="large")
|
| 246 |
def view_graph_dialog(dot_code):
|
| 247 |
-
st.graphviz_chart(dot_code, width=
|
| 248 |
st.caption("Right-click -> 'Open Image in New Tab' to zoom/download.")
|
| 249 |
|
| 250 |
def show_app():
|
|
@@ -288,6 +290,7 @@ def show_app():
|
|
| 288 |
st.session_state.full_text = ""
|
| 289 |
st.session_state.processed_files = set()
|
| 290 |
st.session_state.upload_status = ""
|
|
|
|
| 291 |
st.session_state.uploader_key += 1
|
| 292 |
st.rerun()
|
| 293 |
|
|
@@ -301,6 +304,10 @@ def show_app():
|
|
| 301 |
# Chat History
|
| 302 |
for msg in st.session_state.messages:
|
| 303 |
with st.chat_message(msg["role"]):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
st.markdown(msg["content"])
|
| 305 |
|
| 306 |
# User Input
|
|
@@ -313,22 +320,26 @@ def show_app():
|
|
| 313 |
if st.session_state.agent:
|
| 314 |
# Container for intermediate thought process
|
| 315 |
with st.status("Agent Reasoning...", expanded=True) as status:
|
|
|
|
| 316 |
|
| 317 |
def graph_callback(node_name, state):
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
status.write(f"🔍 **Retrieving** context for query: *'{state.get('current_query', '...')}'*")
|
| 322 |
elif node_name == "generate":
|
| 323 |
-
|
| 324 |
elif node_name == "reflect":
|
| 325 |
score = state.get("reflection_score")
|
| 326 |
if score == "yes":
|
| 327 |
-
|
| 328 |
else:
|
| 329 |
-
|
| 330 |
elif node_name == "rewrite_query":
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
| 333 |
result = st.session_state.agent.run(prompt, callback=graph_callback)
|
| 334 |
status.update(label="Response Ready", state="complete", expanded=False)
|
|
@@ -336,11 +347,15 @@ def show_app():
|
|
| 336 |
response = result["generation"]
|
| 337 |
|
| 338 |
# Show debug steps comfortably (Optional redundant info, maybe keep for final stats)
|
| 339 |
-
with st.expander("
|
| 340 |
st.write(f"**Reflected:** {result.get('reflection_score')} | **Total Iter:** {result.get('iterations')}")
|
| 341 |
|
| 342 |
st.markdown(response)
|
| 343 |
-
st.session_state.messages.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
else:
|
| 345 |
st.warning("Please upload a PDF first.")
|
| 346 |
|
|
@@ -365,22 +380,24 @@ def show_app():
|
|
| 365 |
|
| 366 |
# Podcast Tool
|
| 367 |
with st.expander("🎧 Podcast", expanded=False):
|
| 368 |
-
if st.
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
| 384 |
|
| 385 |
# Knowledge Graph Tool
|
| 386 |
with st.expander("🕸️ Knowledge Graph", expanded=False):
|
|
|
|
| 74 |
st.session_state.deep_summary = None
|
| 75 |
if "graph_dot" not in st.session_state:
|
| 76 |
st.session_state.graph_dot = None
|
| 77 |
+
if "podcast_audio" not in st.session_state:
|
| 78 |
+
st.session_state.podcast_audio = None
|
| 79 |
|
| 80 |
def switch_page(page_name):
|
| 81 |
st.session_state.page = page_name
|
|
|
|
| 246 |
|
| 247 |
@dialog_decorator("Knowledge Graph Visualization", width="large")
|
| 248 |
def view_graph_dialog(dot_code):
|
| 249 |
+
st.graphviz_chart(dot_code, width="stretch")
|
| 250 |
st.caption("Right-click -> 'Open Image in New Tab' to zoom/download.")
|
| 251 |
|
| 252 |
def show_app():
|
|
|
|
| 290 |
st.session_state.full_text = ""
|
| 291 |
st.session_state.processed_files = set()
|
| 292 |
st.session_state.upload_status = ""
|
| 293 |
+
st.session_state.podcast_audio = None
|
| 294 |
st.session_state.uploader_key += 1
|
| 295 |
st.rerun()
|
| 296 |
|
|
|
|
| 304 |
# Chat History
|
| 305 |
for msg in st.session_state.messages:
|
| 306 |
with st.chat_message(msg["role"]):
|
| 307 |
+
if "thoughts" in msg and msg["thoughts"]:
|
| 308 |
+
with st.expander("⛓️ Reasoning Log", expanded=False):
|
| 309 |
+
for log in msg["thoughts"]:
|
| 310 |
+
st.write(log)
|
| 311 |
st.markdown(msg["content"])
|
| 312 |
|
| 313 |
# User Input
|
|
|
|
| 320 |
if st.session_state.agent:
|
| 321 |
# Container for intermediate thought process
|
| 322 |
with st.status("Agent Reasoning...", expanded=True) as status:
|
| 323 |
+
thoughts = []
|
| 324 |
|
| 325 |
def graph_callback(node_name, state):
|
| 326 |
+
msg = ""
|
| 327 |
+
if node_name == "retrieve":
|
| 328 |
+
msg = f"🔍 **Retrieving** context for query: *'{state.get('current_query', '...')}'*"
|
|
|
|
| 329 |
elif node_name == "generate":
|
| 330 |
+
msg = "🧠 **Generating** answer..."
|
| 331 |
elif node_name == "reflect":
|
| 332 |
score = state.get("reflection_score")
|
| 333 |
if score == "yes":
|
| 334 |
+
msg = "✅ **Reflection Passed**: Answer is grounded."
|
| 335 |
else:
|
| 336 |
+
msg = "❌ **Reflection Failed**: Hallucination/Irrelevance detected."
|
| 337 |
elif node_name == "rewrite_query":
|
| 338 |
+
msg = f"🔄 **Rewriting Query** to improve results..."
|
| 339 |
+
|
| 340 |
+
if msg:
|
| 341 |
+
status.write(msg)
|
| 342 |
+
thoughts.append(msg)
|
| 343 |
|
| 344 |
result = st.session_state.agent.run(prompt, callback=graph_callback)
|
| 345 |
status.update(label="Response Ready", state="complete", expanded=False)
|
|
|
|
| 347 |
response = result["generation"]
|
| 348 |
|
| 349 |
# Show debug steps comfortably (Optional redundant info, maybe keep for final stats)
|
| 350 |
+
with st.expander("📊 Final Stats", expanded=False):
|
| 351 |
st.write(f"**Reflected:** {result.get('reflection_score')} | **Total Iter:** {result.get('iterations')}")
|
| 352 |
|
| 353 |
st.markdown(response)
|
| 354 |
+
st.session_state.messages.append({
|
| 355 |
+
"role": "assistant",
|
| 356 |
+
"content": response,
|
| 357 |
+
"thoughts": thoughts
|
| 358 |
+
})
|
| 359 |
else:
|
| 360 |
st.warning("Please upload a PDF first.")
|
| 361 |
|
|
|
|
| 380 |
|
| 381 |
# Podcast Tool
|
| 382 |
with st.expander("🎧 Podcast", expanded=False):
|
| 383 |
+
if not st.session_state.podcast_audio:
|
| 384 |
+
if st.button("Generate Audio"):
|
| 385 |
+
briefing = ensure_deep_summary()
|
| 386 |
+
with st.spinner("Scripting & Synthesizing..."):
|
| 387 |
+
p_gen = PodcastGenerator()
|
| 388 |
+
script = p_gen.generate_audio_script(briefing)
|
| 389 |
+
audio_path = p_gen.generate_audio_file(script)
|
| 390 |
+
if audio_path:
|
| 391 |
+
st.session_state.podcast_audio = audio_path
|
| 392 |
+
st.rerun()
|
| 393 |
+
else:
|
| 394 |
+
st.error("Audio generation failed.")
|
| 395 |
+
else:
|
| 396 |
+
st.success("Podcast Ready!")
|
| 397 |
+
st.audio(st.session_state.podcast_audio)
|
| 398 |
+
if st.button("🔄 Regenerate Podcast"):
|
| 399 |
+
st.session_state.podcast_audio = None
|
| 400 |
+
st.rerun()
|
| 401 |
|
| 402 |
# Knowledge Graph Tool
|
| 403 |
with st.expander("🕸️ Knowledge Graph", expanded=False):
|
core/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (122 Bytes). View file
|
|
|
core/__pycache__/graph.cpython-312.pyc
ADDED
|
Binary file (6.37 kB). View file
|
|
|
core/__pycache__/map_reduce.cpython-312.pyc
ADDED
|
Binary file (1.99 kB). View file
|
|
|
core/__pycache__/models.cpython-312.pyc
ADDED
|
Binary file (3.7 kB). View file
|
|
|
core/__pycache__/pdf_processer.cpython-312.pyc
ADDED
|
Binary file (4.13 kB). View file
|
|
|
core/__pycache__/podcast.cpython-312.pyc
ADDED
|
Binary file (3.09 kB). View file
|
|
|
core/__pycache__/visualizer.cpython-312.pyc
ADDED
|
Binary file (1.35 kB). View file
|
|
|
core/graph.py
CHANGED
|
@@ -2,7 +2,7 @@ from typing import TypedDict, List
|
|
| 2 |
from langgraph.graph import StateGraph, END
|
| 3 |
from langchain_core.documents import Document
|
| 4 |
from core.models import get_llm
|
| 5 |
-
from prompts import RAG_PROMPT, REFLECTION_PROMPT, REWRITE_PROMPT
|
| 6 |
from langchain_core.output_parsers import StrOutputParser
|
| 7 |
|
| 8 |
class GraphState(TypedDict):
|
|
@@ -19,12 +19,6 @@ class RAGAgent:
|
|
| 19 |
self.llm = get_llm()
|
| 20 |
self.app = self.build_graph()
|
| 21 |
|
| 22 |
-
def expand_query(self, state: GraphState):
|
| 23 |
-
question = state["question"]
|
| 24 |
-
chain = QUERY_EXPANSION_PROMPT | self.llm | StrOutputParser()
|
| 25 |
-
expanded_query = chain.invoke({"question": question})
|
| 26 |
-
return {"current_query": expanded_query}
|
| 27 |
-
|
| 28 |
def retrieve(self, state: GraphState):
|
| 29 |
query = state["current_query"]
|
| 30 |
docs = self.retriever.invoke(query)
|
|
@@ -44,9 +38,17 @@ class RAGAgent:
|
|
| 44 |
def reflect(self, state: GraphState):
|
| 45 |
question = state["question"]
|
| 46 |
generation = state["generation"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
chain = REFLECTION_PROMPT | self.llm | StrOutputParser()
|
| 49 |
-
score = chain.invoke({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
# Normalize score
|
| 52 |
normalized_score = "yes" if "yes" in score.lower() else "no"
|
|
@@ -54,9 +56,15 @@ class RAGAgent:
|
|
| 54 |
|
| 55 |
def rewrite_query(self, state: GraphState):
|
| 56 |
question = state["question"]
|
|
|
|
|
|
|
| 57 |
|
| 58 |
chain = REWRITE_PROMPT | self.llm | StrOutputParser()
|
| 59 |
-
new_query = chain.invoke({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
return {"current_query": new_query, "iterations": state["iterations"] + 1}
|
| 62 |
|
|
@@ -73,15 +81,13 @@ class RAGAgent:
|
|
| 73 |
workflow = StateGraph(GraphState)
|
| 74 |
|
| 75 |
# Define Nodes
|
| 76 |
-
workflow.add_node("expand_query", self.expand_query)
|
| 77 |
workflow.add_node("retrieve", self.retrieve)
|
| 78 |
workflow.add_node("generate", self.generate)
|
| 79 |
workflow.add_node("reflect", self.reflect)
|
| 80 |
workflow.add_node("rewrite_query", self.rewrite_query)
|
| 81 |
|
| 82 |
# Build Edges
|
| 83 |
-
workflow.set_entry_point("
|
| 84 |
-
workflow.add_edge("expand_query", "retrieve")
|
| 85 |
workflow.add_edge("retrieve", "generate")
|
| 86 |
workflow.add_edge("generate", "reflect")
|
| 87 |
|
|
|
|
| 2 |
from langgraph.graph import StateGraph, END
|
| 3 |
from langchain_core.documents import Document
|
| 4 |
from core.models import get_llm
|
| 5 |
+
from prompts import RAG_PROMPT, REFLECTION_PROMPT, REWRITE_PROMPT
|
| 6 |
from langchain_core.output_parsers import StrOutputParser
|
| 7 |
|
| 8 |
class GraphState(TypedDict):
|
|
|
|
| 19 |
self.llm = get_llm()
|
| 20 |
self.app = self.build_graph()
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
def retrieve(self, state: GraphState):
|
| 23 |
query = state["current_query"]
|
| 24 |
docs = self.retriever.invoke(query)
|
|
|
|
| 38 |
def reflect(self, state: GraphState):
|
| 39 |
question = state["question"]
|
| 40 |
generation = state["generation"]
|
| 41 |
+
docs = state["documents"]
|
| 42 |
+
|
| 43 |
+
# Format context so the reflector can check for grounding
|
| 44 |
+
context = "\n\n".join([f"[Source: {doc.metadata.get('filename', 'Unknown')}] {doc.page_content}" for doc in docs])
|
| 45 |
|
| 46 |
chain = REFLECTION_PROMPT | self.llm | StrOutputParser()
|
| 47 |
+
score = chain.invoke({
|
| 48 |
+
"context": context,
|
| 49 |
+
"question": question,
|
| 50 |
+
"generation": generation
|
| 51 |
+
})
|
| 52 |
|
| 53 |
# Normalize score
|
| 54 |
normalized_score = "yes" if "yes" in score.lower() else "no"
|
|
|
|
| 56 |
|
| 57 |
def rewrite_query(self, state: GraphState):
|
| 58 |
question = state["question"]
|
| 59 |
+
previous_query = state["current_query"]
|
| 60 |
+
failed_gen = state["generation"]
|
| 61 |
|
| 62 |
chain = REWRITE_PROMPT | self.llm | StrOutputParser()
|
| 63 |
+
new_query = chain.invoke({
|
| 64 |
+
"question": question,
|
| 65 |
+
"previous_query": previous_query,
|
| 66 |
+
"generation": failed_gen
|
| 67 |
+
})
|
| 68 |
|
| 69 |
return {"current_query": new_query, "iterations": state["iterations"] + 1}
|
| 70 |
|
|
|
|
| 81 |
workflow = StateGraph(GraphState)
|
| 82 |
|
| 83 |
# Define Nodes
|
|
|
|
| 84 |
workflow.add_node("retrieve", self.retrieve)
|
| 85 |
workflow.add_node("generate", self.generate)
|
| 86 |
workflow.add_node("reflect", self.reflect)
|
| 87 |
workflow.add_node("rewrite_query", self.rewrite_query)
|
| 88 |
|
| 89 |
# Build Edges
|
| 90 |
+
workflow.set_entry_point("retrieve")
|
|
|
|
| 91 |
workflow.add_edge("retrieve", "generate")
|
| 92 |
workflow.add_edge("generate", "reflect")
|
| 93 |
|
core/models.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
import os
|
| 2 |
import streamlit as st
|
| 3 |
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
| 4 |
-
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
|
|
|
| 5 |
|
| 6 |
def get_llm(model_name: str = "nvidia/nemotron-3-nano-30b-a3b"):
|
| 7 |
"""
|
|
@@ -37,3 +38,51 @@ def get_embeddings():
|
|
| 37 |
raise ValueError("GOOGLE_API_KEY not found in environment or secrets.")
|
| 38 |
|
| 39 |
return GoogleGenerativeAIEmbeddings(model="models/embedding-001", google_api_key=api_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import streamlit as st
|
| 3 |
from langchain_nvidia_ai_endpoints import ChatNVIDIA
|
| 4 |
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI
|
| 5 |
+
from google import genai
|
| 6 |
|
| 7 |
def get_llm(model_name: str = "nvidia/nemotron-3-nano-30b-a3b"):
|
| 8 |
"""
|
|
|
|
| 38 |
raise ValueError("GOOGLE_API_KEY not found in environment or secrets.")
|
| 39 |
|
| 40 |
return GoogleGenerativeAIEmbeddings(model="models/embedding-001", google_api_key=api_key)
|
| 41 |
+
|
| 42 |
+
from google.genai import types
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def generate_podcast_audio(script_text: str):
|
| 46 |
+
"""
|
| 47 |
+
Calls Gemini TTS with multi-speaker configuration.
|
| 48 |
+
Returns raw audio data.
|
| 49 |
+
"""
|
| 50 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 51 |
+
if not api_key and "GOOGLE_API_KEY" in st.secrets:
|
| 52 |
+
api_key = st.secrets["GOOGLE_API_KEY"]
|
| 53 |
+
|
| 54 |
+
client = genai.Client(api_key=api_key)
|
| 55 |
+
|
| 56 |
+
response = client.models.generate_content(
|
| 57 |
+
model="gemini-2.5-flash-preview-tts",
|
| 58 |
+
contents=f"Generate a podcast dialogue audio. \n\n{script_text}",
|
| 59 |
+
config=types.GenerateContentConfig(
|
| 60 |
+
response_modalities=["AUDIO"],
|
| 61 |
+
speech_config=types.SpeechConfig(
|
| 62 |
+
multi_speaker_voice_config=types.MultiSpeakerVoiceConfig(
|
| 63 |
+
speaker_voice_configs=[
|
| 64 |
+
types.SpeakerVoiceConfig(
|
| 65 |
+
speaker='Alex',
|
| 66 |
+
voice_config=types.VoiceConfig(
|
| 67 |
+
prebuilt_voice_config=types.PrebuiltVoiceConfig(
|
| 68 |
+
voice_name='Kore',
|
| 69 |
+
)
|
| 70 |
+
)
|
| 71 |
+
),
|
| 72 |
+
types.SpeakerVoiceConfig(
|
| 73 |
+
speaker='Jamie',
|
| 74 |
+
voice_config=types.VoiceConfig(
|
| 75 |
+
prebuilt_voice_config=types.PrebuiltVoiceConfig(
|
| 76 |
+
voice_name='Puck',
|
| 77 |
+
)
|
| 78 |
+
)
|
| 79 |
+
),
|
| 80 |
+
]
|
| 81 |
+
)
|
| 82 |
+
)
|
| 83 |
+
)
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
if response.candidates and response.candidates[0].content.parts:
|
| 87 |
+
return response.candidates[0].content.parts[0].inline_data.data
|
| 88 |
+
return None
|
core/pdf_processer.py
CHANGED
|
@@ -76,7 +76,6 @@ class PDFProcessor:
|
|
| 76 |
def get_retriever(self):
|
| 77 |
if not self.vector_store:
|
| 78 |
raise ValueError("Vector store not initialized. Upload a PDF first.")
|
| 79 |
-
# return self.vector_store.as_retriever(search_type="mmr", search_kwargs={"k": 5})
|
| 80 |
return self.vector_store.as_retriever(search_type="mmr", search_kwargs={"k": 5})
|
| 81 |
def get_full_text(self):
|
| 82 |
return "\n\n".join([doc.page_content for doc in self.documents])
|
|
|
|
| 76 |
def get_retriever(self):
|
| 77 |
if not self.vector_store:
|
| 78 |
raise ValueError("Vector store not initialized. Upload a PDF first.")
|
|
|
|
| 79 |
return self.vector_store.as_retriever(search_type="mmr", search_kwargs={"k": 5})
|
| 80 |
def get_full_text(self):
|
| 81 |
return "\n\n".join([doc.page_content for doc in self.documents])
|
core/podcast.py
CHANGED
|
@@ -1,13 +1,9 @@
|
|
| 1 |
-
from core.models import get_llm
|
| 2 |
from prompts import PODCAST_AUDIO_PROMPT
|
| 3 |
from langchain_core.prompts import ChatPromptTemplate
|
| 4 |
from langchain_core.output_parsers import StrOutputParser
|
| 5 |
import tempfile
|
| 6 |
-
import os
|
| 7 |
-
from google import genai
|
| 8 |
-
from google.genai import types
|
| 9 |
import wave
|
| 10 |
-
import streamlit as st
|
| 11 |
|
| 12 |
class PodcastGenerator:
|
| 13 |
def __init__(self):
|
|
@@ -25,52 +21,12 @@ class PodcastGenerator:
|
|
| 25 |
|
| 26 |
def generate_audio_file(self, script_text):
|
| 27 |
"""
|
| 28 |
-
Uses
|
| 29 |
"""
|
| 30 |
-
api_key = os.getenv("GOOGLE_API_KEY")
|
| 31 |
-
if not api_key and "GOOGLE_API_KEY" in st.secrets:
|
| 32 |
-
api_key = st.secrets["GOOGLE_API_KEY"]
|
| 33 |
-
|
| 34 |
-
if not api_key:
|
| 35 |
-
return None
|
| 36 |
-
|
| 37 |
-
client = genai.Client(api_key=api_key)
|
| 38 |
-
|
| 39 |
try:
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
config=types.GenerateContentConfig(
|
| 44 |
-
response_modalities=["AUDIO"],
|
| 45 |
-
speech_config=types.SpeechConfig(
|
| 46 |
-
multi_speaker_voice_config=types.MultiSpeakerVoiceConfig(
|
| 47 |
-
speaker_voice_configs=[
|
| 48 |
-
types.SpeakerVoiceConfig(
|
| 49 |
-
speaker='Alex',
|
| 50 |
-
voice_config=types.VoiceConfig(
|
| 51 |
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(
|
| 52 |
-
voice_name='Kore',
|
| 53 |
-
)
|
| 54 |
-
)
|
| 55 |
-
),
|
| 56 |
-
types.SpeakerVoiceConfig(
|
| 57 |
-
speaker='Jamie',
|
| 58 |
-
voice_config=types.VoiceConfig(
|
| 59 |
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(
|
| 60 |
-
voice_name='Puck',
|
| 61 |
-
)
|
| 62 |
-
)
|
| 63 |
-
),
|
| 64 |
-
]
|
| 65 |
-
)
|
| 66 |
-
)
|
| 67 |
-
)
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
# Extract audio data
|
| 71 |
-
if response.candidates and response.candidates[0].content.parts:
|
| 72 |
-
data = response.candidates[0].content.parts[0].inline_data.data
|
| 73 |
-
|
| 74 |
# Use NamedTemporaryFile to get a unique name, then close it immediately
|
| 75 |
# so wave.open can re-open it for writing.
|
| 76 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
|
|
|
| 1 |
+
from core.models import get_llm, generate_podcast_audio
|
| 2 |
from prompts import PODCAST_AUDIO_PROMPT
|
| 3 |
from langchain_core.prompts import ChatPromptTemplate
|
| 4 |
from langchain_core.output_parsers import StrOutputParser
|
| 5 |
import tempfile
|
|
|
|
|
|
|
|
|
|
| 6 |
import wave
|
|
|
|
| 7 |
|
| 8 |
class PodcastGenerator:
|
| 9 |
def __init__(self):
|
|
|
|
| 21 |
|
| 22 |
def generate_audio_file(self, script_text):
|
| 23 |
"""
|
| 24 |
+
Uses centralized Gemini TTS logic.
|
| 25 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
try:
|
| 27 |
+
data = generate_podcast_audio(script_text)
|
| 28 |
+
|
| 29 |
+
if data:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# Use NamedTemporaryFile to get a unique name, then close it immediately
|
| 31 |
# so wave.open can re-open it for writing.
|
| 32 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
core/visualizer.py
CHANGED
|
@@ -3,7 +3,7 @@ from langchain_core.prompts import PromptTemplate
|
|
| 3 |
from langchain_core.output_parsers import StrOutputParser
|
| 4 |
import graphviz
|
| 5 |
import re
|
| 6 |
-
from prompts import
|
| 7 |
|
| 8 |
class KnowledgeGraphGenerator:
|
| 9 |
def __init__(self):
|
|
@@ -13,7 +13,7 @@ class KnowledgeGraphGenerator:
|
|
| 13 |
# Text is now the "Deep Summary", so no need to truncate.
|
| 14 |
input_text = text
|
| 15 |
|
| 16 |
-
chain =
|
| 17 |
dot_code = chain.invoke({"text": input_text})
|
| 18 |
|
| 19 |
# Cleanup markdown if present
|
|
|
|
| 3 |
from langchain_core.output_parsers import StrOutputParser
|
| 4 |
import graphviz
|
| 5 |
import re
|
| 6 |
+
from prompts import GRAPH_PROMPT
|
| 7 |
|
| 8 |
class KnowledgeGraphGenerator:
|
| 9 |
def __init__(self):
|
|
|
|
| 13 |
# Text is now the "Deep Summary", so no need to truncate.
|
| 14 |
input_text = text
|
| 15 |
|
| 16 |
+
chain = GRAPH_PROMPT | self.llm | StrOutputParser()
|
| 17 |
dot_code = chain.invoke({"text": input_text})
|
| 18 |
|
| 19 |
# Cleanup markdown if present
|
.gitignore → gitignore
RENAMED
|
File without changes
|
prompts.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
| 1 |
from langchain_core.prompts import ChatPromptTemplate
|
| 2 |
|
| 3 |
# RAG Generation Prompt
|
| 4 |
-
RAG_SYSTEM = """You are
|
| 5 |
If the context does not contain the answer, say "I cannot answer this based on the document."
|
| 6 |
|
| 7 |
Requirements:
|
| 8 |
1. Use academic tone.
|
| 9 |
-
2.
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
RAG_HUMAN = """Context:
|
| 13 |
{context}
|
|
@@ -24,13 +29,17 @@ RAG_PROMPT = ChatPromptTemplate.from_messages([
|
|
| 24 |
|
| 25 |
# Reflection Prompt
|
| 26 |
REFLECTION_SYSTEM = """You are a senior editor grading an AI-generated answer.
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
Output exactly "yes" if the answer is grounded and relevant.
|
| 30 |
-
Output "no" if the answer
|
| 31 |
-
IMPORTANT: If the answer says "I cannot answer" or "context does not contain", YOU MUST OUTPUT "no"."""
|
| 32 |
|
| 33 |
-
REFLECTION_HUMAN = """
|
|
|
|
|
|
|
|
|
|
| 34 |
Generated Answer: {generation}
|
| 35 |
|
| 36 |
Current Answer Quality status:"""
|
|
@@ -41,59 +50,61 @@ REFLECTION_PROMPT = ChatPromptTemplate.from_messages([
|
|
| 41 |
])
|
| 42 |
|
| 43 |
# Query Rewrite Prompt
|
| 44 |
-
REWRITE_SYSTEM = """You are a query optimizer. The previous search query failed to retrieve
|
| 45 |
-
|
|
|
|
| 46 |
Output ONLY the rewritten query string."""
|
| 47 |
|
| 48 |
REWRITE_HUMAN = """Original Question: {question}
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
Rewritten Query:"""
|
| 51 |
|
| 52 |
REWRITE_PROMPT = ChatPromptTemplate.from_messages([
|
| 53 |
("system", REWRITE_SYSTEM),
|
| 54 |
("human", REWRITE_HUMAN)
|
| 55 |
])
|
| 56 |
|
| 57 |
-
# Query Expansion Prompt (Pre-retrieval)
|
| 58 |
-
QUERY_EXPANSION_SYSTEM = """You are a research assistant.
|
| 59 |
-
Rewrite the user's query to be more effective for vector retrieval (RAG).
|
| 60 |
-
- Add 2-3 relevant academic keywords, synonyms, or related concepts.
|
| 61 |
-
- Keep the original intent and core subject intact.
|
| 62 |
-
- Output ONLY the rewritten/expanded query string. No explanations."""
|
| 63 |
-
|
| 64 |
-
QUERY_EXPANSION_HUMAN = "Original Query: {question}\n\nExpanded Query:"
|
| 65 |
-
|
| 66 |
-
QUERY_EXPANSION_PROMPT = ChatPromptTemplate.from_messages([
|
| 67 |
-
("system", QUERY_EXPANSION_SYSTEM),
|
| 68 |
-
("human", QUERY_EXPANSION_HUMAN)
|
| 69 |
-
])
|
| 70 |
-
|
| 71 |
# Podcast Prompts
|
| 72 |
# Summary/Podcast Map Prompts
|
| 73 |
-
SUMMARY_MAP_SYSTEM = """
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
SUMMARY_MAP_HUMAN = """Text Chunk:
|
| 77 |
{text}
|
| 78 |
|
| 79 |
-
Summary:"""
|
| 80 |
|
| 81 |
SUMMARY_MAP_PROMPT = ChatPromptTemplate.from_messages([
|
| 82 |
("system", SUMMARY_MAP_SYSTEM),
|
| 83 |
("human", SUMMARY_MAP_HUMAN)
|
| 84 |
])
|
| 85 |
|
| 86 |
-
SUMMARY_REDUCE_SYSTEM = """
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
SUMMARY_REDUCE_PROMPT = ChatPromptTemplate.from_messages([
|
| 95 |
("system", SUMMARY_REDUCE_SYSTEM),
|
| 96 |
-
("human", "Summaries:\n{text}\n\
|
| 97 |
])
|
| 98 |
|
| 99 |
PODCAST_AUDIO_SYSTEM = """You are producing a podcast script.
|
|
@@ -123,9 +134,8 @@ PODCAST_AUDIO_PROMPT = ChatPromptTemplate.from_messages([
|
|
| 123 |
])
|
| 124 |
|
| 125 |
|
| 126 |
-
|
| 127 |
# Knowledge Graph Prompt
|
| 128 |
-
|
| 129 |
Your goal is to extract a DEEP hierarchical structure and key relationships from the provided text and represent them as a CLEAN, multi-level Knowledge Graph using DOT syntax.
|
| 130 |
|
| 131 |
CRITICAL INSTRUCTIONS:
|
|
@@ -135,7 +145,7 @@ CRITICAL INSTRUCTIONS:
|
|
| 135 |
4. DESCRIPTIVE RELATIONSHIPS: Every edge MUST have a unique, descriptive label (e.g., "implements", "results in", "validates"). AVOID using the same generic label like "includes" for multiple edges in the same branch.
|
| 136 |
5. AVOID SPIDER WEBS: Focus on hierarchical flow (Root -> Child -> Grandchild) rather than lateral cross-connections.
|
| 137 |
6. HIERARCHICAL LAYOUT: Use 'rankdir=LR' (Left-to-Right).
|
| 138 |
-
7. CONCISE LABELS: Keep node names and labels short (
|
| 139 |
|
| 140 |
Output ONLY the raw DOT code. No markdown code blocks.
|
| 141 |
|
|
@@ -154,9 +164,14 @@ digraph G {{
|
|
| 154 |
"Category B" -> "Step 1" [label="baseline"];
|
| 155 |
"Step 1" -> "Validation Method" [label="criteria"];
|
| 156 |
"Validation Method" -> "Metric X" [label="output"];
|
| 157 |
-
}}
|
| 158 |
|
| 159 |
-
Text to Analyze:
|
| 160 |
{text}
|
| 161 |
|
| 162 |
DOT Code:"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from langchain_core.prompts import ChatPromptTemplate
|
| 2 |
|
| 3 |
# RAG Generation Prompt
|
| 4 |
+
RAG_SYSTEM = """You are a research assistant. Answer the user's question based strictly on the provided context.
|
| 5 |
If the context does not contain the answer, say "I cannot answer this based on the document."
|
| 6 |
|
| 7 |
Requirements:
|
| 8 |
1. Use academic tone.
|
| 9 |
+
2. **In-text Citations:** Use Unicode Superscript Numbers (¹, ², ³, ⁴, ⁵, ⁶, ⁷, ⁸, ⁹, ¹⁰) strictly. Place them immediately after the punctuation or relevant phrase.
|
| 10 |
+
- Do NOT use `[^1]` (Markdown footnotes) or `[1]` (Brackets).
|
| 11 |
+
- Example: ...at compile time¹.
|
| 12 |
+
3. **References Section:** At the very end, include a section titled "References".
|
| 13 |
+
4. **Reference Format:** List the citations sequentially using normal numbers.
|
| 14 |
+
- Format: `1. Document: <filename>, Page: <page>`
|
| 15 |
+
5. Be concise but comprehensive."""
|
| 16 |
|
| 17 |
RAG_HUMAN = """Context:
|
| 18 |
{context}
|
|
|
|
| 29 |
|
| 30 |
# Reflection Prompt
|
| 31 |
REFLECTION_SYSTEM = """You are a senior editor grading an AI-generated answer.
|
| 32 |
+
Evaluate if the answer is:
|
| 33 |
+
1. GROUNDED: Is the answer supported by the facts in the provided Context?
|
| 34 |
+
2. RELEVANT: Does it actually answer the User Question?
|
| 35 |
|
| 36 |
+
Output exactly "yes" if the answer is both grounded and relevant.
|
| 37 |
+
Output "no" if the answer contains information NOT in the context, is irrelevant, or if the assistant says it cannot answer."""
|
|
|
|
| 38 |
|
| 39 |
+
REFLECTION_HUMAN = """Context:
|
| 40 |
+
{context}
|
| 41 |
+
|
| 42 |
+
User Question: {question}
|
| 43 |
Generated Answer: {generation}
|
| 44 |
|
| 45 |
Current Answer Quality status:"""
|
|
|
|
| 50 |
])
|
| 51 |
|
| 52 |
# Query Rewrite Prompt
|
| 53 |
+
REWRITE_SYSTEM = """You are a query optimizer. The previous search query failed to retrieve documents that could fully answer the question.
|
| 54 |
+
Analyze the original question, the previous query used, and the failed answer to understand what was missing or misunderstood.
|
| 55 |
+
Rewrite the pursuit into a new, improved search query that is more specific and uses better technical keywords.
|
| 56 |
Output ONLY the rewritten query string."""
|
| 57 |
|
| 58 |
REWRITE_HUMAN = """Original Question: {question}
|
| 59 |
+
Previous Query: {previous_query}
|
| 60 |
+
Failed Answer: {generation}
|
| 61 |
|
| 62 |
+
Improved Rewritten Query:"""
|
| 63 |
|
| 64 |
REWRITE_PROMPT = ChatPromptTemplate.from_messages([
|
| 65 |
("system", REWRITE_SYSTEM),
|
| 66 |
("human", REWRITE_HUMAN)
|
| 67 |
])
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# Podcast Prompts
|
| 70 |
# Summary/Podcast Map Prompts
|
| 71 |
+
SUMMARY_MAP_SYSTEM = """You are a precision-oriented research analyst.
|
| 72 |
+
Extract atomic facts and technical details from this segment into dense bullet points.
|
| 73 |
+
|
| 74 |
+
STRICT CONSTRAINT: Maximum 500 words total.
|
| 75 |
+
Requirements:
|
| 76 |
+
1. Highlight core concepts and key relationships (e.g., "A influences B").
|
| 77 |
+
2. Maintain technical accuracy and preserve specialized terminology.
|
| 78 |
+
3. Be concise: avoid introductory phrases, focus on pure data/logic."""
|
| 79 |
|
| 80 |
SUMMARY_MAP_HUMAN = """Text Chunk:
|
| 81 |
{text}
|
| 82 |
|
| 83 |
+
Atomic Fact Summary:"""
|
| 84 |
|
| 85 |
SUMMARY_MAP_PROMPT = ChatPromptTemplate.from_messages([
|
| 86 |
("system", SUMMARY_MAP_SYSTEM),
|
| 87 |
("human", SUMMARY_MAP_HUMAN)
|
| 88 |
])
|
| 89 |
|
| 90 |
+
SUMMARY_REDUCE_SYSTEM = """You are a Senior Knowledge Architect.
|
| 91 |
+
Synthesize the provided segment summaries into a cohesive, high-density "Master Strategic Briefing".
|
| 92 |
+
|
| 93 |
+
STRICT CONSTRAINT: Total length must be between 1200 and 1800 words for a comprehensive deep-dive.
|
| 94 |
+
|
| 95 |
+
Synthesis Strategy:
|
| 96 |
+
1. ELIMINATE REDUNDANCY: Group similar findings from different segments together.
|
| 97 |
+
2. LOGICAL MAPPING: Establish clear connections between methodology, results, and implications across the entire document.
|
| 98 |
+
3. STRUCTURE: Use professional H2/H3 headers.
|
| 99 |
+
4. TARGET SECTIONS:
|
| 100 |
+
- Main Themes & Scope
|
| 101 |
+
- Technical Methodology & Contributions
|
| 102 |
+
- Primary Findings & Evidence
|
| 103 |
+
- Critical Implications & "So What?" analysis."""
|
| 104 |
|
| 105 |
SUMMARY_REDUCE_PROMPT = ChatPromptTemplate.from_messages([
|
| 106 |
("system", SUMMARY_REDUCE_SYSTEM),
|
| 107 |
+
("human", "Segment Summaries:\n{text}\n\nExecute the Master Strategic Briefing Summary:")
|
| 108 |
])
|
| 109 |
|
| 110 |
PODCAST_AUDIO_SYSTEM = """You are producing a podcast script.
|
|
|
|
| 134 |
])
|
| 135 |
|
| 136 |
|
|
|
|
| 137 |
# Knowledge Graph Prompt
|
| 138 |
+
GRAPH_SYSTEM = """You are an expert at visualizing complex knowledge information.
|
| 139 |
Your goal is to extract a DEEP hierarchical structure and key relationships from the provided text and represent them as a CLEAN, multi-level Knowledge Graph using DOT syntax.
|
| 140 |
|
| 141 |
CRITICAL INSTRUCTIONS:
|
|
|
|
| 145 |
4. DESCRIPTIVE RELATIONSHIPS: Every edge MUST have a unique, descriptive label (e.g., "implements", "results in", "validates"). AVOID using the same generic label like "includes" for multiple edges in the same branch.
|
| 146 |
5. AVOID SPIDER WEBS: Focus on hierarchical flow (Root -> Child -> Grandchild) rather than lateral cross-connections.
|
| 147 |
6. HIERARCHICAL LAYOUT: Use 'rankdir=LR' (Left-to-Right).
|
| 148 |
+
7. CONCISE LABELS: Keep node names and labels short (less than 5 words).
|
| 149 |
|
| 150 |
Output ONLY the raw DOT code. No markdown code blocks.
|
| 151 |
|
|
|
|
| 164 |
"Category B" -> "Step 1" [label="baseline"];
|
| 165 |
"Step 1" -> "Validation Method" [label="criteria"];
|
| 166 |
"Validation Method" -> "Metric X" [label="output"];
|
| 167 |
+
}}"""
|
| 168 |
|
| 169 |
+
GRAPH_HUMAN = """Text to Analyze:
|
| 170 |
{text}
|
| 171 |
|
| 172 |
DOT Code:"""
|
| 173 |
+
|
| 174 |
+
GRAPH_PROMPT = ChatPromptTemplate.from_messages([
|
| 175 |
+
("system", GRAPH_SYSTEM),
|
| 176 |
+
("human", GRAPH_HUMAN)
|
| 177 |
+
])
|