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
- .env.example +8 -0
- .gitattributes +0 -35
- .gitignore +51 -0
- .python-version +1 -0
- README.md +151 -5
- app.py +319 -0
- main.py +6 -0
- pyproject.toml +16 -0
- requirements.txt +5 -0
- src/__init__.py +1 -0
- src/agent.py +358 -0
- src/context.py +161 -0
- src/research.py +147 -0
- src/transcription.py +56 -0
- 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:
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|