huang_de_jun commited on
Commit
3b6c6f8
·
1 Parent(s): 99383fe

feat: initial MVP of Ask The Right Question app

Browse files

- Add audio transcription via OpenAI Whisper API
- Add context accumulation system for tracking talk content
- Add research tools via Tavily API (web search, news,
fact-check)
- Add Claude agent with tool use for question generation
- Add Gradio 6 UI with microphone input and text paste options
- Add question categorization (Clarification, Depth, Connection,
etc.)
- Configure for HuggingFace Spaces deployment

Files changed (15) hide show
  1. .env.example +8 -0
  2. .gitattributes +0 -35
  3. .gitignore +51 -0
  4. .python-version +1 -0
  5. README.md +151 -5
  6. app.py +319 -0
  7. main.py +6 -0
  8. pyproject.toml +16 -0
  9. requirements.txt +5 -0
  10. src/__init__.py +1 -0
  11. src/agent.py +358 -0
  12. src/context.py +161 -0
  13. src/research.py +147 -0
  14. src/transcription.py +56 -0
  15. uv.lock +0 -0
.env.example ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # OpenAI API Key (for Whisper transcription)
2
+ OPENAI_API_KEY=your_openai_api_key_here
3
+
4
+ # Anthropic API Key (for Claude agent)
5
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
6
+
7
+ # Tavily API Key (for web search/research)
8
+ TAVILY_API_KEY=your_tavily_api_key_here
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+
28
+ # Environment variables
29
+ .env
30
+
31
+ # IDE
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
40
+
41
+ # Logs
42
+ *.log
43
+
44
+ # Testing
45
+ .pytest_cache/
46
+ .coverage
47
+ htmlcov/
48
+
49
+ # Misc
50
+ *.bak
51
+ *.tmp
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
README.md CHANGED
@@ -1,13 +1,159 @@
1
  ---
2
  title: Ask The Right Question
3
- emoji: 📊
4
- colorFrom: blue
5
  colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
- short_description: Listen to a talk and suggest quality questions to ask
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Ask The Right Question
3
+ emoji: 🎤
4
+ colorFrom: purple
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: "5.50.0"
8
  app_file: app.py
9
  pinned: false
10
+ tags:
11
+ - mcp-in-action-track-consumer
12
+ - agents
13
+ - question-generation
14
+ - q-and-a
15
+ - critical-thinking
16
  ---
17
 
18
+ # Ask The Right Question
19
+
20
+ > AI-powered assistant that helps you ask insightful questions during Q&A sessions at conferences, classes, and talks.
21
+
22
+ ## The Problem
23
+
24
+ You're at a conference, a class, or a talk. The speaker finishes and opens the floor for questions. Your mind goes blank. You want to:
25
+ - Ask something meaningful that shows you were engaged
26
+ - Make a connection with the speaker
27
+ - Actually learn something valuable from the Q&A
28
+
29
+ But coming up with good questions on the spot is hard.
30
+
31
+ ## The Solution
32
+
33
+ **Ask The Right Question** listens to the talk and helps you generate thoughtful, well-researched questions. It:
34
+
35
+ 1. **Captures Context** - Record audio snippets or paste notes from the talk
36
+ 2. **Researches** - Automatically researches the speaker's background, fact-checks claims, and finds relevant trends
37
+ 3. **Generates Questions** - Creates insightful questions with explanations of *why* they're good
38
+ 4. **Teaches Critical Thinking** - Helps you understand what makes a question valuable
39
+
40
+ ## Features
41
+
42
+ ### Core Features (MVP)
43
+ - Live microphone capture with OpenAI Whisper transcription
44
+ - Context accumulation as the talk progresses
45
+ - AI-powered question generation with Claude
46
+ - Web research via Tavily (speaker background, fact-checking, trends)
47
+ - Question categorization (Clarification, Depth, Connection, Challenge, Practical, Forward)
48
+ - Explanation of why each question is valuable
49
+
50
+ ### Stretch Goals
51
+ - Fill-in-the-blank questions for learning
52
+ - Question ranking exercises
53
+ - Gamification to build critical thinking skills
54
+
55
+ ## How It Works
56
+
57
+ ```
58
+ +----------------+ +------------------+ +-----------------+
59
+ | Microphone | --> | Transcription | --> | Context Store |
60
+ | (Gradio) | | (OpenAI Whisper)| | (Accumulating) |
61
+ +----------------+ +------------------+ +--------+--------+
62
+ |
63
+ +--------------------------------+--------+
64
+ | v |
65
+ | +-------------+ +----------------+ |
66
+ | | Tavily API | | Claude Agent | |
67
+ | | - Web Search| -> | Question Gen | |
68
+ | | - News | +-------+--------+ |
69
+ | | - Fact Check| | |
70
+ | +-------------+ | |
71
+ | Agent Loop | |
72
+ +-----------------------------+-----------+
73
+ |
74
+ v
75
+ +-----------------+
76
+ | Gradio UI |
77
+ | - Questions |
78
+ | - Explanations|
79
+ +-----------------+
80
+ ```
81
+
82
+ ## Question Categories
83
+
84
+ | Category | Description | Example |
85
+ |----------|-------------|---------|
86
+ | **CLARIFICATION** | Seeks to understand better | "Could you elaborate on what you meant by...?" |
87
+ | **DEPTH** | Explores a topic more deeply | "What's the underlying mechanism behind...?" |
88
+ | **CONNECTION** | Connects to other fields | "How does this relate to developments in...?" |
89
+ | **CHALLENGE** | Probes assumptions respectfully | "Have you considered the alternative view that...?" |
90
+ | **PRACTICAL** | Asks about real-world application | "How would this work in practice for...?" |
91
+ | **FORWARD** | Explores future implications | "Where do you see this heading in the next...?" |
92
+
93
+ ## Tech Stack
94
+
95
+ - **Frontend**: Gradio 6
96
+ - **Transcription**: OpenAI Whisper API
97
+ - **Agent LLM**: Claude (Anthropic)
98
+ - **Research**: Tavily API (web search, news, fact-checking)
99
+ - **Hosting**: HuggingFace Spaces
100
+
101
+ ## Setup
102
+
103
+ ### Environment Variables
104
+
105
+ Create a `.env` file with:
106
+
107
+ ```bash
108
+ OPENAI_API_KEY=your_openai_api_key
109
+ ANTHROPIC_API_KEY=your_anthropic_api_key
110
+ TAVILY_API_KEY=your_tavily_api_key
111
+ ```
112
+
113
+ ### Local Development
114
+
115
+ ```bash
116
+ # Clone the repo
117
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/ask-the-right-question
118
+
119
+ # Install dependencies
120
+ uv sync
121
+
122
+ # Run the app
123
+ uv run python app.py
124
+ ```
125
+
126
+ ## Usage
127
+
128
+ 1. **Record Audio**: Click the microphone button to record snippets of the talk
129
+ 2. **Or Paste Text**: Manually paste notes or transcript
130
+ 3. **Add Speaker Info**: (Optional) Enter the speaker's name for background research
131
+ 4. **Generate Questions**: Click the button to get AI-generated questions
132
+ 5. **Learn**: Read the explanations to understand why each question is valuable
133
+
134
+ ## Demo Video
135
+
136
+ [Coming soon]
137
+
138
+ ## Social Media Post
139
+
140
+ [Link to social media post]
141
+
142
+ ## Team
143
+
144
+ - [Your Name](https://huggingface.co/YOUR_USERNAME)
145
+
146
+ ## Acknowledgments
147
+
148
+ Built for the HuggingFace MCP Hackathon (November 2024)
149
+
150
+ Powered by:
151
+ - [Anthropic Claude](https://anthropic.com) - AI reasoning and question generation
152
+ - [OpenAI Whisper](https://openai.com) - Audio transcription
153
+ - [Tavily](https://tavily.com) - AI-optimized web search
154
+ - [Gradio](https://gradio.app) - UI framework
155
+ - [HuggingFace](https://huggingface.co) - Hosting
156
+
157
+ ## License
158
+
159
+ MIT
app.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ask The Right Question - AI-powered Q&A Assistant
3
+
4
+ An app that listens to talks/presentations and helps you generate
5
+ insightful questions during Q&A sessions.
6
+ """
7
+
8
+ import os
9
+ import gradio as gr
10
+ from dotenv import load_dotenv
11
+
12
+ from src.transcription import transcribe_audio
13
+ from src.context import ConversationContext
14
+ from src.agent import analyze_and_generate_questions, extract_topics_and_claims
15
+
16
+ # Load environment variables
17
+ load_dotenv()
18
+
19
+ # Global context (per session in production, use gr.State)
20
+ context = ConversationContext()
21
+
22
+
23
+ def process_audio(audio_path: str, state: dict) -> tuple[str, str, dict]:
24
+ """
25
+ Process audio input and update transcript.
26
+
27
+ Args:
28
+ audio_path: Path to recorded audio file
29
+ state: Current session state
30
+
31
+ Returns:
32
+ Tuple of (transcript, status message, updated state)
33
+ """
34
+ if not audio_path:
35
+ return state.get("transcript", ""), "No audio received", state
36
+
37
+ try:
38
+ # Transcribe the audio
39
+ new_text = transcribe_audio(audio_path)
40
+
41
+ if new_text.strip():
42
+ # Get context from state
43
+ ctx = state.get("context")
44
+ if ctx is None:
45
+ ctx = ConversationContext()
46
+ state["context"] = ctx
47
+
48
+ # Add to context
49
+ ctx.add_transcript(new_text)
50
+
51
+ # Update transcript display
52
+ full_transcript = ctx.get_full_transcript()
53
+ state["transcript"] = full_transcript
54
+
55
+ # Get context summary
56
+ summary = ctx.get_context_summary()
57
+ status = f"Transcribed {summary['num_segments']} segments ({summary['transcript_length']} chars)"
58
+
59
+ # Auto-extract topics if enough context
60
+ if ctx.has_enough_context(min_words=50) and len(ctx.identified_topics) == 0:
61
+ extract_topics_and_claims(ctx)
62
+ if ctx.identified_topics:
63
+ status += f" | Topics: {', '.join(ctx.identified_topics[:3])}"
64
+
65
+ return full_transcript, status, state
66
+
67
+ except Exception as e:
68
+ return state.get("transcript", ""), f"Error: {str(e)}", state
69
+
70
+ return state.get("transcript", ""), "Processing...", state
71
+
72
+
73
+ def generate_questions(
74
+ speaker_name: str,
75
+ focus_area: str,
76
+ num_questions: int,
77
+ state: dict
78
+ ) -> tuple[str, str, dict]:
79
+ """
80
+ Generate questions based on accumulated context.
81
+
82
+ Args:
83
+ speaker_name: Optional speaker name
84
+ focus_area: Optional focus area
85
+ num_questions: Number of questions to generate
86
+ state: Current session state
87
+
88
+ Returns:
89
+ Tuple of (questions HTML, research summary, updated state)
90
+ """
91
+ ctx = state.get("context")
92
+ if ctx is None or not ctx.has_enough_context(min_words=30):
93
+ return "Need more context. Please record more of the talk.", "", state
94
+
95
+ try:
96
+ result = analyze_and_generate_questions(
97
+ context=ctx,
98
+ speaker_name=speaker_name,
99
+ num_questions=int(num_questions),
100
+ focus_area=focus_area
101
+ )
102
+
103
+ # Format questions as HTML
104
+ questions_html = f"<h3>Analysis</h3><p>{result.get('analysis', '')}</p>"
105
+ questions_html += "<h3>Suggested Questions</h3>"
106
+
107
+ for i, q in enumerate(result.get("questions", []), 1):
108
+ category = q.get("category", "GENERAL")
109
+ question = q.get("question", "")
110
+ reasoning = q.get("reasoning", "")
111
+
112
+ questions_html += f"""
113
+ <div style="margin: 10px 0; padding: 15px; border-left: 4px solid #667eea; background: #f8f9fa; border-radius: 4px;">
114
+ <div style="font-size: 12px; color: #667eea; font-weight: bold; margin-bottom: 5px;">
115
+ {category}
116
+ </div>
117
+ <div style="font-size: 16px; font-weight: 500; margin-bottom: 8px;">
118
+ {i}. {question}
119
+ </div>
120
+ <div style="font-size: 14px; color: #666; font-style: italic;">
121
+ Why this is good: {reasoning}
122
+ </div>
123
+ </div>
124
+ """
125
+
126
+ research_summary = result.get("research_summary", "")
127
+
128
+ return questions_html, research_summary, state
129
+
130
+ except Exception as e:
131
+ return f"Error generating questions: {str(e)}", "", state
132
+
133
+
134
+ def clear_session(state: dict) -> tuple[str, str, str, str, dict]:
135
+ """Clear all session data."""
136
+ state["context"] = ConversationContext()
137
+ state["transcript"] = ""
138
+ return "", "", "", "Session cleared", state
139
+
140
+
141
+ def add_manual_context(text: str, state: dict) -> tuple[str, str, dict]:
142
+ """Add manually typed context."""
143
+ if not text.strip():
144
+ return state.get("transcript", ""), "No text provided", state
145
+
146
+ ctx = state.get("context")
147
+ if ctx is None:
148
+ ctx = ConversationContext()
149
+ state["context"] = ctx
150
+
151
+ ctx.add_transcript(text)
152
+ full_transcript = ctx.get_full_transcript()
153
+ state["transcript"] = full_transcript
154
+
155
+ # Extract topics
156
+ if ctx.has_enough_context(min_words=50):
157
+ extract_topics_and_claims(ctx)
158
+
159
+ summary = ctx.get_context_summary()
160
+ status = f"Added context. Total: {summary['transcript_length']} chars"
161
+
162
+ return full_transcript, status, state
163
+
164
+
165
+ # Custom CSS
166
+ custom_css = """
167
+ .main-container {
168
+ max-width: 1200px;
169
+ margin: 0 auto;
170
+ }
171
+ .header {
172
+ text-align: center;
173
+ padding: 20px;
174
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
175
+ color: white;
176
+ border-radius: 10px;
177
+ margin-bottom: 20px;
178
+ }
179
+ .question-card {
180
+ background: #f8f9fa;
181
+ border-radius: 8px;
182
+ padding: 15px;
183
+ margin: 10px 0;
184
+ border-left: 4px solid #667eea;
185
+ }
186
+ """
187
+
188
+ # Build the Gradio interface
189
+ with gr.Blocks(css=custom_css, title="Ask The Right Question") as demo:
190
+ # Session state
191
+ state = gr.State({"context": ConversationContext(), "transcript": ""})
192
+
193
+ # Header
194
+ gr.HTML("""
195
+ <div class="header">
196
+ <h1>Ask The Right Question</h1>
197
+ <p>AI-powered assistant to help you ask insightful questions during Q&A sessions</p>
198
+ </div>
199
+ """)
200
+
201
+ with gr.Row():
202
+ # Left column - Input
203
+ with gr.Column(scale=1):
204
+ gr.Markdown("### 1. Capture the Talk")
205
+
206
+ with gr.Tab("Record Audio"):
207
+ audio_input = gr.Audio(
208
+ sources=["microphone"],
209
+ type="filepath",
210
+ label="Record from microphone",
211
+ streaming=False
212
+ )
213
+ audio_status = gr.Textbox(label="Status", interactive=False)
214
+
215
+ with gr.Tab("Paste Text"):
216
+ manual_text = gr.Textbox(
217
+ label="Paste transcript or notes",
218
+ placeholder="Paste any context from the talk here...",
219
+ lines=5
220
+ )
221
+ add_text_btn = gr.Button("Add Context", variant="secondary")
222
+
223
+ gr.Markdown("### 2. Transcript")
224
+ transcript_display = gr.Textbox(
225
+ label="Accumulated Transcript",
226
+ lines=10,
227
+ interactive=False,
228
+ placeholder="Transcript will appear here as you record..."
229
+ )
230
+
231
+ clear_btn = gr.Button("Clear Session", variant="stop")
232
+
233
+ # Right column - Questions
234
+ with gr.Column(scale=1):
235
+ gr.Markdown("### 3. Generate Questions")
236
+
237
+ with gr.Row():
238
+ speaker_name = gr.Textbox(
239
+ label="Speaker Name (optional)",
240
+ placeholder="e.g., Dr. Jane Smith",
241
+ scale=2
242
+ )
243
+ num_questions = gr.Slider(
244
+ minimum=3,
245
+ maximum=10,
246
+ value=5,
247
+ step=1,
248
+ label="Number of Questions",
249
+ scale=1
250
+ )
251
+
252
+ focus_area = gr.Textbox(
253
+ label="Focus Area (optional)",
254
+ placeholder="e.g., practical applications, future trends..."
255
+ )
256
+
257
+ generate_btn = gr.Button("Generate Questions", variant="primary", size="lg")
258
+
259
+ gr.Markdown("### Suggested Questions")
260
+ questions_output = gr.HTML(
261
+ value="<p style='color: #666; text-align: center; padding: 40px;'>Questions will appear here after you record some context and click 'Generate Questions'</p>"
262
+ )
263
+
264
+ with gr.Accordion("Research Summary", open=False):
265
+ research_output = gr.Textbox(
266
+ label="Research conducted by the AI",
267
+ lines=5,
268
+ interactive=False
269
+ )
270
+
271
+ # Footer
272
+ gr.Markdown("""
273
+ ---
274
+ **How to use:**
275
+ 1. Click the microphone to record parts of a talk or paste text directly
276
+ 2. The AI will accumulate context and identify topics
277
+ 3. Optionally enter the speaker's name for background research
278
+ 4. Click "Generate Questions" to get AI-suggested questions with explanations
279
+
280
+ **Tips for great questions:**
281
+ - Record key moments, not the entire talk
282
+ - Add speaker name for personalized research
283
+ - Use focus area to target specific themes
284
+
285
+ ---
286
+ Built for the Hugging Face MCP Hackathon | Powered by Claude, OpenAI Whisper, and Tavily
287
+ """)
288
+
289
+ # Event handlers
290
+ audio_input.change(
291
+ fn=process_audio,
292
+ inputs=[audio_input, state],
293
+ outputs=[transcript_display, audio_status, state]
294
+ )
295
+
296
+ add_text_btn.click(
297
+ fn=add_manual_context,
298
+ inputs=[manual_text, state],
299
+ outputs=[transcript_display, audio_status, state]
300
+ ).then(
301
+ fn=lambda: "",
302
+ outputs=[manual_text]
303
+ )
304
+
305
+ generate_btn.click(
306
+ fn=generate_questions,
307
+ inputs=[speaker_name, focus_area, num_questions, state],
308
+ outputs=[questions_output, research_output, state]
309
+ )
310
+
311
+ clear_btn.click(
312
+ fn=clear_session,
313
+ inputs=[state],
314
+ outputs=[transcript_display, questions_output, research_output, audio_status, state]
315
+ )
316
+
317
+
318
+ if __name__ == "__main__":
319
+ demo.launch()
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Entry point for Ask The Right Question app."""
2
+
3
+ from app import demo
4
+
5
+ if __name__ == "__main__":
6
+ demo.launch()
pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ask-the-right-question"
3
+ version = "0.1.0"
4
+ description = "AI-powered Q&A assistant that helps you ask insightful questions during talks and presentations"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "anthropic>=0.75.0",
9
+ "gradio>=5.50.0",
10
+ "openai>=2.8.1",
11
+ "python-dotenv>=1.2.1",
12
+ "tavily-python>=0.7.13",
13
+ ]
14
+
15
+ [project.scripts]
16
+ ask-the-right-question = "app:demo.launch"
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ anthropic>=0.75.0
2
+ gradio>=5.50.0
3
+ openai>=2.8.1
4
+ python-dotenv>=1.2.1
5
+ tavily-python>=0.7.13
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Ask The Right Question - AI-powered Q&A assistant
src/agent.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Question generation agent using Claude."""
2
+
3
+ import os
4
+ import json
5
+ from anthropic import Anthropic
6
+
7
+ from .context import ConversationContext
8
+ from .research import search_web, search_news, research_speaker, fact_check, get_topic_trends
9
+
10
+
11
+ def get_anthropic_client() -> Anthropic:
12
+ """Get Anthropic client with API key from environment."""
13
+ api_key = os.getenv("ANTHROPIC_API_KEY")
14
+ if not api_key:
15
+ raise ValueError("ANTHROPIC_API_KEY environment variable not set")
16
+ return Anthropic(api_key=api_key)
17
+
18
+
19
+ SYSTEM_PROMPT = """You are an expert at helping people ask insightful, thoughtful questions during Q&A sessions at conferences, classes, and talks.
20
+
21
+ Your role is to:
22
+ 1. Analyze the conversation/talk context provided
23
+ 2. Identify key themes, claims, and areas worth exploring
24
+ 3. Generate high-quality questions that:
25
+ - Show genuine engagement with the material
26
+ - Demonstrate critical thinking
27
+ - Could lead to valuable insights
28
+ - Are respectful and constructive
29
+ - Help the asker make a positive impression
30
+
31
+ Question Categories:
32
+ - CLARIFICATION: Questions that seek to understand something better
33
+ - DEPTH: Questions that explore a topic more deeply
34
+ - CONNECTION: Questions that connect ideas to other fields or experiences
35
+ - CHALLENGE: Respectful questions that probe assumptions or claims
36
+ - PRACTICAL: Questions about real-world application
37
+ - FORWARD: Questions about future implications or directions
38
+
39
+ When generating questions, consider:
40
+ - The speaker's expertise and background
41
+ - Recent trends and developments in the field
42
+ - Claims that could be fact-checked or explored further
43
+ - Connections to current events or other domains
44
+
45
+ You have access to research tools. Use them to:
46
+ - Research the speaker's background
47
+ - Fact-check interesting claims
48
+ - Find recent trends in the topic
49
+ - Discover relevant news or developments
50
+
51
+ Always explain WHY a question is good - this helps users learn to ask better questions themselves."""
52
+
53
+
54
+ TOOLS = [
55
+ {
56
+ "name": "search_web",
57
+ "description": "Search the web for information on any topic. Use this to research context, verify claims, or find relevant information.",
58
+ "input_schema": {
59
+ "type": "object",
60
+ "properties": {
61
+ "query": {
62
+ "type": "string",
63
+ "description": "The search query"
64
+ }
65
+ },
66
+ "required": ["query"]
67
+ }
68
+ },
69
+ {
70
+ "name": "search_news",
71
+ "description": "Search for recent news articles on a topic. Use this to find current events or recent developments.",
72
+ "input_schema": {
73
+ "type": "object",
74
+ "properties": {
75
+ "query": {
76
+ "type": "string",
77
+ "description": "The news search query"
78
+ },
79
+ "days": {
80
+ "type": "integer",
81
+ "description": "How many days back to search (default: 7)",
82
+ "default": 7
83
+ }
84
+ },
85
+ "required": ["query"]
86
+ }
87
+ },
88
+ {
89
+ "name": "research_speaker",
90
+ "description": "Research a speaker's background, expertise, and recent activity.",
91
+ "input_schema": {
92
+ "type": "object",
93
+ "properties": {
94
+ "speaker_name": {
95
+ "type": "string",
96
+ "description": "Name of the speaker"
97
+ },
98
+ "topic": {
99
+ "type": "string",
100
+ "description": "Optional topic context"
101
+ }
102
+ },
103
+ "required": ["speaker_name"]
104
+ }
105
+ },
106
+ {
107
+ "name": "fact_check",
108
+ "description": "Fact-check a specific claim or statement made in the talk.",
109
+ "input_schema": {
110
+ "type": "object",
111
+ "properties": {
112
+ "claim": {
113
+ "type": "string",
114
+ "description": "The claim to verify"
115
+ }
116
+ },
117
+ "required": ["claim"]
118
+ }
119
+ },
120
+ {
121
+ "name": "get_topic_trends",
122
+ "description": "Get recent trends and developments in a specific topic area.",
123
+ "input_schema": {
124
+ "type": "object",
125
+ "properties": {
126
+ "topic": {
127
+ "type": "string",
128
+ "description": "The topic to research"
129
+ }
130
+ },
131
+ "required": ["topic"]
132
+ }
133
+ }
134
+ ]
135
+
136
+
137
+ def execute_tool(tool_name: str, tool_input: dict) -> str:
138
+ """Execute a tool and return the result as a string."""
139
+ try:
140
+ if tool_name == "search_web":
141
+ result = search_web(tool_input["query"])
142
+ elif tool_name == "search_news":
143
+ result = search_news(tool_input["query"], days=tool_input.get("days", 7))
144
+ elif tool_name == "research_speaker":
145
+ result = research_speaker(
146
+ tool_input["speaker_name"],
147
+ topic=tool_input.get("topic", "")
148
+ )
149
+ elif tool_name == "fact_check":
150
+ result = fact_check(tool_input["claim"])
151
+ elif tool_name == "get_topic_trends":
152
+ result = get_topic_trends(tool_input["topic"])
153
+ else:
154
+ return f"Unknown tool: {tool_name}"
155
+
156
+ # Extract relevant info from result
157
+ if isinstance(result, dict):
158
+ if "answer" in result:
159
+ return f"Summary: {result['answer']}\n\nSources: {json.dumps(result.get('results', [])[:3], indent=2)}"
160
+ return json.dumps(result, indent=2)
161
+ return str(result)
162
+
163
+ except Exception as e:
164
+ return f"Error executing {tool_name}: {str(e)}"
165
+
166
+
167
+ def analyze_and_generate_questions(
168
+ context: ConversationContext,
169
+ speaker_name: str = "",
170
+ num_questions: int = 5,
171
+ focus_area: str = ""
172
+ ) -> dict:
173
+ """
174
+ Analyze context and generate insightful questions.
175
+
176
+ Args:
177
+ context: The conversation context
178
+ speaker_name: Optional speaker name to research
179
+ num_questions: Number of questions to generate
180
+ focus_area: Optional area to focus questions on
181
+
182
+ Returns:
183
+ Dictionary with questions and analysis
184
+ """
185
+ client = get_anthropic_client()
186
+
187
+ # Build the user message
188
+ user_message = f"""Based on the following context from a talk/presentation, generate {num_questions} insightful questions.
189
+
190
+ {context.to_prompt_context()}
191
+
192
+ """
193
+
194
+ if speaker_name:
195
+ user_message += f"\nSpeaker name: {speaker_name} (please research their background)"
196
+
197
+ if focus_area:
198
+ user_message += f"\nFocus area: {focus_area}"
199
+
200
+ user_message += """
201
+
202
+ Please:
203
+ 1. First, use the research tools to gather additional context (speaker background, fact-check claims, find trends)
204
+ 2. Then generate questions with explanations of why each is valuable
205
+ 3. Categorize each question (CLARIFICATION, DEPTH, CONNECTION, CHALLENGE, PRACTICAL, FORWARD)
206
+ 4. Format your response as a JSON object with this structure:
207
+ {
208
+ "analysis": "Brief analysis of the talk's key themes",
209
+ "questions": [
210
+ {
211
+ "question": "The question text",
212
+ "category": "CATEGORY",
213
+ "reasoning": "Why this is a good question",
214
+ "based_on": "What context/research this was based on"
215
+ }
216
+ ],
217
+ "research_summary": "Summary of research conducted"
218
+ }"""
219
+
220
+ messages = [{"role": "user", "content": user_message}]
221
+
222
+ # Agent loop with tool use
223
+ max_iterations = 10
224
+ iteration = 0
225
+
226
+ while iteration < max_iterations:
227
+ iteration += 1
228
+
229
+ response = client.messages.create(
230
+ model="claude-sonnet-4-20250514",
231
+ max_tokens=4096,
232
+ system=SYSTEM_PROMPT,
233
+ tools=TOOLS,
234
+ messages=messages
235
+ )
236
+
237
+ # Check if we need to handle tool use
238
+ if response.stop_reason == "tool_use":
239
+ # Process tool calls
240
+ tool_results = []
241
+ assistant_content = response.content
242
+
243
+ for block in response.content:
244
+ if block.type == "tool_use":
245
+ tool_result = execute_tool(block.name, block.input)
246
+ tool_results.append({
247
+ "type": "tool_result",
248
+ "tool_use_id": block.id,
249
+ "content": tool_result
250
+ })
251
+
252
+ # Store research in context
253
+ if block.name in ["search_web", "search_news", "fact_check", "get_topic_trends"]:
254
+ context.add_research(
255
+ query=str(block.input),
256
+ summary=tool_result[:500] # First 500 chars
257
+ )
258
+
259
+ # Add assistant response and tool results to messages
260
+ messages.append({"role": "assistant", "content": assistant_content})
261
+ messages.append({"role": "user", "content": tool_results})
262
+
263
+ else:
264
+ # No more tool use, extract final response
265
+ final_text = ""
266
+ for block in response.content:
267
+ if hasattr(block, "text"):
268
+ final_text += block.text
269
+
270
+ # Try to parse JSON from response
271
+ try:
272
+ # Find JSON in response
273
+ json_start = final_text.find("{")
274
+ json_end = final_text.rfind("}") + 1
275
+ if json_start >= 0 and json_end > json_start:
276
+ json_str = final_text[json_start:json_end]
277
+ result = json.loads(json_str)
278
+
279
+ # Store questions in context
280
+ for q in result.get("questions", []):
281
+ context.add_question(
282
+ question=q["question"],
283
+ category=q.get("category", "general"),
284
+ reasoning=q.get("reasoning", "")
285
+ )
286
+
287
+ return result
288
+ except json.JSONDecodeError:
289
+ pass
290
+
291
+ # Return raw text if JSON parsing fails
292
+ return {
293
+ "analysis": "Could not parse structured response",
294
+ "questions": [{"question": final_text, "category": "general", "reasoning": ""}],
295
+ "research_summary": ""
296
+ }
297
+
298
+ return {
299
+ "analysis": "Max iterations reached",
300
+ "questions": [],
301
+ "research_summary": ""
302
+ }
303
+
304
+
305
+ def extract_topics_and_claims(context: ConversationContext) -> dict:
306
+ """
307
+ Extract key topics and claims from the transcript.
308
+
309
+ Args:
310
+ context: The conversation context
311
+
312
+ Returns:
313
+ Dictionary with topics and claims
314
+ """
315
+ client = get_anthropic_client()
316
+
317
+ transcript = context.get_full_transcript()
318
+ if not transcript:
319
+ return {"topics": [], "claims": []}
320
+
321
+ response = client.messages.create(
322
+ model="claude-sonnet-4-20250514",
323
+ max_tokens=1024,
324
+ messages=[{
325
+ "role": "user",
326
+ "content": f"""Analyze this transcript and extract:
327
+ 1. Key topics being discussed
328
+ 2. Notable claims or statements that could be fact-checked or explored
329
+
330
+ Transcript:
331
+ {transcript}
332
+
333
+ Respond in JSON format:
334
+ {{
335
+ "topics": ["topic1", "topic2", ...],
336
+ "claims": ["claim1", "claim2", ...]
337
+ }}"""
338
+ }]
339
+ )
340
+
341
+ try:
342
+ text = response.content[0].text
343
+ json_start = text.find("{")
344
+ json_end = text.rfind("}") + 1
345
+ if json_start >= 0 and json_end > json_start:
346
+ result = json.loads(text[json_start:json_end])
347
+
348
+ # Update context
349
+ for topic in result.get("topics", []):
350
+ context.add_topic(topic)
351
+ for claim in result.get("claims", []):
352
+ context.add_claim(claim)
353
+
354
+ return result
355
+ except (json.JSONDecodeError, IndexError):
356
+ pass
357
+
358
+ return {"topics": [], "claims": []}
src/context.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Context accumulation and management for conversation tracking."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class TranscriptSegment:
10
+ """A segment of transcribed text."""
11
+ text: str
12
+ timestamp: datetime = field(default_factory=datetime.now)
13
+
14
+
15
+ @dataclass
16
+ class SpeakerInfo:
17
+ """Information about the speaker."""
18
+ name: str = ""
19
+ background: str = ""
20
+ expertise: list[str] = field(default_factory=list)
21
+ recent_activity: str = ""
22
+
23
+
24
+ @dataclass
25
+ class ResearchResult:
26
+ """A research result from web search."""
27
+ query: str
28
+ summary: str
29
+ sources: list[dict] = field(default_factory=list)
30
+ timestamp: datetime = field(default_factory=datetime.now)
31
+
32
+
33
+ class ConversationContext:
34
+ """Manages accumulated context from a talk/conversation."""
35
+
36
+ def __init__(self):
37
+ self.transcript_segments: list[TranscriptSegment] = []
38
+ self.speaker_info: Optional[SpeakerInfo] = None
39
+ self.research_results: list[ResearchResult] = []
40
+ self.identified_topics: list[str] = []
41
+ self.key_claims: list[str] = []
42
+ self.generated_questions: list[dict] = []
43
+ self.session_start: datetime = datetime.now()
44
+
45
+ def add_transcript(self, text: str) -> None:
46
+ """Add a new transcript segment."""
47
+ if text.strip():
48
+ self.transcript_segments.append(TranscriptSegment(text=text.strip()))
49
+
50
+ def get_full_transcript(self) -> str:
51
+ """Get the complete transcript as a single string."""
52
+ return " ".join(seg.text for seg in self.transcript_segments)
53
+
54
+ def get_recent_transcript(self, num_segments: int = 5) -> str:
55
+ """Get the most recent transcript segments."""
56
+ recent = self.transcript_segments[-num_segments:]
57
+ return " ".join(seg.text for seg in recent)
58
+
59
+ def set_speaker(self, name: str, background: str = "", expertise: list[str] = None,
60
+ recent_activity: str = "") -> None:
61
+ """Set speaker information."""
62
+ self.speaker_info = SpeakerInfo(
63
+ name=name,
64
+ background=background,
65
+ expertise=expertise or [],
66
+ recent_activity=recent_activity
67
+ )
68
+
69
+ def add_research(self, query: str, summary: str, sources: list[dict] = None) -> None:
70
+ """Add a research result."""
71
+ self.research_results.append(ResearchResult(
72
+ query=query,
73
+ summary=summary,
74
+ sources=sources or []
75
+ ))
76
+
77
+ def add_topic(self, topic: str) -> None:
78
+ """Add an identified topic."""
79
+ if topic not in self.identified_topics:
80
+ self.identified_topics.append(topic)
81
+
82
+ def add_claim(self, claim: str) -> None:
83
+ """Add a key claim from the talk."""
84
+ if claim not in self.key_claims:
85
+ self.key_claims.append(claim)
86
+
87
+ def add_question(self, question: str, category: str = "general",
88
+ reasoning: str = "") -> None:
89
+ """Add a generated question."""
90
+ self.generated_questions.append({
91
+ "question": question,
92
+ "category": category,
93
+ "reasoning": reasoning,
94
+ "timestamp": datetime.now().isoformat()
95
+ })
96
+
97
+ def get_context_summary(self) -> dict:
98
+ """Get a summary of all accumulated context."""
99
+ return {
100
+ "transcript_length": len(self.get_full_transcript()),
101
+ "num_segments": len(self.transcript_segments),
102
+ "speaker": self.speaker_info.name if self.speaker_info else None,
103
+ "topics": self.identified_topics,
104
+ "claims_count": len(self.key_claims),
105
+ "research_count": len(self.research_results),
106
+ "questions_generated": len(self.generated_questions),
107
+ "session_duration": (datetime.now() - self.session_start).seconds
108
+ }
109
+
110
+ def has_enough_context(self, min_words: int = 100) -> bool:
111
+ """Check if we have enough context to start generating questions."""
112
+ transcript = self.get_full_transcript()
113
+ word_count = len(transcript.split())
114
+ return word_count >= min_words
115
+
116
+ def to_prompt_context(self) -> str:
117
+ """Format context for use in LLM prompts."""
118
+ parts = []
119
+
120
+ # Transcript
121
+ transcript = self.get_full_transcript()
122
+ if transcript:
123
+ parts.append(f"## Transcript\n{transcript}")
124
+
125
+ # Speaker info
126
+ if self.speaker_info and self.speaker_info.name:
127
+ speaker_section = f"## Speaker: {self.speaker_info.name}"
128
+ if self.speaker_info.background:
129
+ speaker_section += f"\nBackground: {self.speaker_info.background}"
130
+ if self.speaker_info.expertise:
131
+ speaker_section += f"\nExpertise: {', '.join(self.speaker_info.expertise)}"
132
+ if self.speaker_info.recent_activity:
133
+ speaker_section += f"\nRecent Activity: {self.speaker_info.recent_activity}"
134
+ parts.append(speaker_section)
135
+
136
+ # Topics
137
+ if self.identified_topics:
138
+ parts.append(f"## Topics Discussed\n" + "\n".join(f"- {t}" for t in self.identified_topics))
139
+
140
+ # Key claims
141
+ if self.key_claims:
142
+ parts.append(f"## Key Claims Made\n" + "\n".join(f"- {c}" for c in self.key_claims))
143
+
144
+ # Research
145
+ if self.research_results:
146
+ research_section = "## Research Findings"
147
+ for r in self.research_results[-3:]: # Last 3 research results
148
+ research_section += f"\n\n### {r.query}\n{r.summary}"
149
+ parts.append(research_section)
150
+
151
+ return "\n\n".join(parts)
152
+
153
+ def clear(self) -> None:
154
+ """Reset all context."""
155
+ self.transcript_segments = []
156
+ self.speaker_info = None
157
+ self.research_results = []
158
+ self.identified_topics = []
159
+ self.key_claims = []
160
+ self.generated_questions = []
161
+ self.session_start = datetime.now()
src/research.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Research tools using Tavily API for web search and content extraction."""
2
+
3
+ import os
4
+ from tavily import TavilyClient
5
+
6
+
7
+ def get_tavily_client() -> TavilyClient:
8
+ """Get Tavily client with API key from environment."""
9
+ api_key = os.getenv("TAVILY_API_KEY")
10
+ if not api_key:
11
+ raise ValueError("TAVILY_API_KEY environment variable not set")
12
+ return TavilyClient(api_key=api_key)
13
+
14
+
15
+ def search_web(query: str, max_results: int = 5, search_depth: str = "basic") -> dict:
16
+ """
17
+ Search the web for information.
18
+
19
+ Args:
20
+ query: Search query
21
+ max_results: Maximum number of results
22
+ search_depth: "basic" or "advanced"
23
+
24
+ Returns:
25
+ Search results with title, url, and content
26
+ """
27
+ client = get_tavily_client()
28
+
29
+ response = client.search(
30
+ query=query,
31
+ search_depth=search_depth,
32
+ max_results=max_results,
33
+ include_answer=True
34
+ )
35
+
36
+ return response
37
+
38
+
39
+ def search_news(query: str, max_results: int = 5, days: int = 7) -> dict:
40
+ """
41
+ Search for recent news articles.
42
+
43
+ Args:
44
+ query: Search query
45
+ max_results: Maximum number of results
46
+ days: How many days back to search
47
+
48
+ Returns:
49
+ News search results
50
+ """
51
+ client = get_tavily_client()
52
+
53
+ response = client.search(
54
+ query=query,
55
+ topic="news",
56
+ days=days,
57
+ max_results=max_results,
58
+ include_answer=True
59
+ )
60
+
61
+ return response
62
+
63
+
64
+ def research_speaker(speaker_name: str, topic: str = "") -> dict:
65
+ """
66
+ Research a speaker's background.
67
+
68
+ Args:
69
+ speaker_name: Name of the speaker
70
+ topic: Optional topic context
71
+
72
+ Returns:
73
+ Combined research results
74
+ """
75
+ client = get_tavily_client()
76
+
77
+ # Search for speaker background
78
+ query = f"{speaker_name} professional background expertise"
79
+ if topic:
80
+ query += f" {topic}"
81
+
82
+ background = client.search(
83
+ query=query,
84
+ search_depth="advanced",
85
+ max_results=5,
86
+ include_answer=True
87
+ )
88
+
89
+ # Search for recent news/activity
90
+ news_query = f"{speaker_name} recent news talks publications"
91
+ recent = client.search(
92
+ query=news_query,
93
+ topic="news",
94
+ days=30,
95
+ max_results=3,
96
+ include_answer=True
97
+ )
98
+
99
+ return {
100
+ "background": background,
101
+ "recent_activity": recent
102
+ }
103
+
104
+
105
+ def fact_check(claim: str) -> dict:
106
+ """
107
+ Fact-check a claim or statement.
108
+
109
+ Args:
110
+ claim: The claim to verify
111
+
112
+ Returns:
113
+ Search results related to the claim
114
+ """
115
+ client = get_tavily_client()
116
+
117
+ response = client.search(
118
+ query=f"fact check: {claim}",
119
+ search_depth="advanced",
120
+ max_results=5,
121
+ include_answer=True
122
+ )
123
+
124
+ return response
125
+
126
+
127
+ def get_topic_trends(topic: str) -> dict:
128
+ """
129
+ Get recent trends and developments in a topic.
130
+
131
+ Args:
132
+ topic: The topic to research
133
+
134
+ Returns:
135
+ Recent developments and trends
136
+ """
137
+ client = get_tavily_client()
138
+
139
+ response = client.search(
140
+ query=f"{topic} recent developments trends 2024 2025",
141
+ topic="news",
142
+ days=30,
143
+ max_results=5,
144
+ include_answer=True
145
+ )
146
+
147
+ return response
src/transcription.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio transcription using OpenAI Whisper API."""
2
+
3
+ import os
4
+ from openai import OpenAI
5
+
6
+
7
+ def get_openai_client() -> OpenAI:
8
+ """Get OpenAI client with API key from environment."""
9
+ api_key = os.getenv("OPENAI_API_KEY")
10
+ if not api_key:
11
+ raise ValueError("OPENAI_API_KEY environment variable not set")
12
+ return OpenAI(api_key=api_key)
13
+
14
+
15
+ def transcribe_audio(audio_path: str) -> str:
16
+ """
17
+ Transcribe audio file using OpenAI Whisper API.
18
+
19
+ Args:
20
+ audio_path: Path to audio file (mp3, wav, webm, etc.)
21
+
22
+ Returns:
23
+ Transcribed text
24
+ """
25
+ client = get_openai_client()
26
+
27
+ with open(audio_path, "rb") as audio_file:
28
+ transcription = client.audio.transcriptions.create(
29
+ model="whisper-1",
30
+ file=audio_file,
31
+ response_format="text"
32
+ )
33
+
34
+ return transcription
35
+
36
+
37
+ def transcribe_audio_chunk(audio_data: bytes, filename: str = "audio.webm") -> str:
38
+ """
39
+ Transcribe audio data directly from bytes.
40
+
41
+ Args:
42
+ audio_data: Raw audio bytes
43
+ filename: Filename hint for format detection
44
+
45
+ Returns:
46
+ Transcribed text
47
+ """
48
+ client = get_openai_client()
49
+
50
+ transcription = client.audio.transcriptions.create(
51
+ model="whisper-1",
52
+ file=(filename, audio_data),
53
+ response_format="text"
54
+ )
55
+
56
+ return transcription
uv.lock ADDED
The diff for this file is too large to render. See raw diff