tuhulab commited on
Commit
1172f79
·
1 Parent(s): 29ca557

Add comprehensive learning guide for Science Storyteller project

Browse files
Files changed (1) hide show
  1. LEARNING_GUIDE.md +1458 -0
LEARNING_GUIDE.md ADDED
@@ -0,0 +1,1458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Science Storyteller - Learning Guide
2
+
3
+ > **For developers new to async Python, OOP, and MCP protocol**
4
+ > A step-by-step guide to understanding the Science Storyteller codebase
5
+
6
+ ---
7
+
8
+ ## 📚 Table of Contents
9
+
10
+ 0. [Architecture](#architecture)
11
+ 1. [Learning Philosophy](#learning-philosophy)
12
+ 2. [Object-Oriented Programming Basics](#object-oriented-programming-basics)
13
+ 3. [Async/Await Deep Dive](#asyncawait-deep-dive)
14
+ 4. [Module-by-Module Learning Path](#module-by-module-learning-path)
15
+ 5. [Hands-On Exercises](#hands-on-exercises)
16
+ 6. [Common Patterns Explained](#common-patterns-explained)
17
+ 7. [Debugging Tips](#debugging-tips)
18
+ 8. [Further Resources](#further-resources)
19
+ 9. [Testing Strategy](#-testing-strategy)
20
+
21
+ ---
22
+
23
+ ## Architecture
24
+
25
+ This diagram shows how a user request flows through the system.
26
+
27
+ ```mermaid
28
+ graph TD
29
+ subgraph User Interface
30
+ A[Gradio UI]
31
+ end
32
+
33
+ subgraph Orchestration Layer
34
+ B(app.py: ScienceStoryteller)
35
+ end
36
+
37
+ subgraph Agent Layer
38
+ C[agents/research_agent.py]
39
+ D[agents/analysis_agent.py]
40
+ E[agents/audio_agent.py]
41
+ end
42
+
43
+ subgraph Tool Layer
44
+ F(mcp_tools/arxiv_tool.py)
45
+ G(mcp_tools/llm_tool.py)
46
+ H(ElevenLabs API)
47
+ end
48
+
49
+ subgraph External Services
50
+ I[arXiv MCP Server]
51
+ J[Anthropic Claude API]
52
+ K[ElevenLabs TTS Service]
53
+ end
54
+
55
+ A -- User Input (Topic) --> B
56
+ B -- 1. search(topic) --> C
57
+ C -- 2. search_papers(query) --> F
58
+ F -- 3. call_tool --> I
59
+ I -- 4. Paper Results --> F
60
+ F -- 5. Papers --> C
61
+ C -- 6. Papers --> B
62
+ B -- 7. summarize_and_script(paper) --> D
63
+ D -- 8. summarize_paper(paper) --> G
64
+ G -- 9. API Call --> J
65
+ J -- 10. Summary --> G
66
+ G -- 11. Summary --> D
67
+ D -- 12. Script --> B
68
+ B -- 13. text_to_speech(script) --> E
69
+ E -- 14. API Call --> H
70
+ H -- 15. API Call --> K
71
+ K -- 16. Audio MP3 --> H
72
+ H -- 17. Audio File Path --> E
73
+ E -- 18. Audio Path --> B
74
+ B -- 19. Results (Summary, Audio, etc.) --> A
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Python Function Basics
80
+
81
+ Functions are the primary way to group code into reusable blocks. Let's break down a function from our codebase: `utils/audio_processor.py`.
82
+
83
+ ```python
84
+ def process_audio_file(audio_path: str) -> Optional[str]:
85
+ """
86
+ Process and validate an audio file.
87
+
88
+ Args:
89
+ audio_path: Path to audio file
90
+
91
+ Returns:
92
+ Validated path or None if invalid
93
+ """
94
+ # ... function body ...
95
+ return str(path)
96
+ ```
97
+
98
+ ### Anatomy of a Function
99
+
100
+ Let's look at each part of the function definition:
101
+
102
+ 1. **`def` keyword**: This signals the start of a function definition.
103
+ 2. **Function Name**: `process_audio_file`. This is how you'll call the function later. It should be descriptive and follow the `snake_case` convention (all lowercase with underscores).
104
+ 3. **Parameters (in `()`)**: `(audio_path: str)`. These are the inputs the function accepts.
105
+ - `audio_path`: The name of the parameter.
106
+ - `: str`: This is a **type hint**. It tells developers that this function expects `audio_path` to be a string. It helps with code readability and catching errors.
107
+ 4. **Return Type Hint**: `-> Optional[str]`. This indicates what the function will return.
108
+ - `Optional[str]` means the function can return either a `str` (string) or `None`. This is very useful for functions that might not always have a valid result to give back.
109
+ 5. **Docstring**: The triple-quoted string `"""..."""` right after the definition. It explains the function's purpose, arguments (`Args`), and return value (`Returns`). This is essential for documentation.
110
+ 6. **Function Body**: The indented code block below the definition. This is where the function's logic is implemented.
111
+ 7. **`return` statement**: This keyword exits the function and passes back a value to whoever called it.
112
+
113
+ ### Why Use Functions?
114
+
115
+ - **Reusability**: Write code once and use it many times.
116
+ - **Modularity**: Break down complex problems into smaller, manageable pieces.
117
+ - **Readability**: Well-named functions make code easier to understand.
118
+
119
+ ---
120
+
121
+ ## Learning Philosophy
122
+
123
+ ### Why Learn Module-by-Module?
124
+
125
+ **Bottom-up approach** is recommended for this project:
126
+ 1. Start with simple utilities (pure Python functions)
127
+ 2. Progress to MCP tools (understand protocol basics)
128
+ 3. Study agents (business logic and coordination)
129
+ 4. Finally tackle orchestration (integration)
130
+
131
+ **Benefits:**
132
+ - ✅ Build confidence with simple concepts first
133
+ - ✅ Understand dependencies before integration
134
+ - ✅ Easier to debug when you know each piece
135
+ - ✅ Can test components independently
136
+
137
+ ### Learning vs Building Trade-off
138
+
139
+ For a hackathon project, you need to balance:
140
+ - **Deep understanding**: Takes time, prevents bugs
141
+ - **Quick delivery**: Ship working product by deadline
142
+
143
+ **Recommended approach for this project:**
144
+ - **Week 1**: Deep dive into 2-3 core modules
145
+ - **Week 2**: Implement and integrate
146
+ - **Week 3**: Test, polish, document
147
+
148
+ ---
149
+
150
+ ## Object-Oriented Programming Basics
151
+
152
+ ### What is a Class?
153
+
154
+ A **class** is a blueprint for creating objects. Think of it as a cookie cutter.
155
+
156
+ ```python
157
+ class ScienceStoryteller: # The blueprint
158
+ """Main orchestrator for the Science Storyteller workflow."""
159
+ ```
160
+
161
+ ### Creating Objects (Instantiation)
162
+
163
+ ```python
164
+ # Creating an object from the class
165
+ storyteller = ScienceStoryteller() # Now you have a specific storyteller object
166
+ ```
167
+
168
+ ### The `__init__` Method (Constructor)
169
+
170
+ The `__init__` method is called **automatically** when you create a new object.
171
+
172
+ ```python
173
+ class ScienceStoryteller:
174
+ def __init__(self): # Runs when ScienceStoryteller() is called
175
+ self.research_agent = ResearchAgent()
176
+ self.analysis_agent = AnalysisAgent()
177
+ self.audio_agent = AudioAgent()
178
+ ```
179
+
180
+ **Purpose:** Set up the initial state of your object.
181
+
182
+ **When it runs:**
183
+ ```python
184
+ storyteller = ScienceStoryteller() # __init__ runs here automatically
185
+ ```
186
+
187
+ ### Understanding `self`
188
+
189
+ `self` refers to **this particular object instance**.
190
+
191
+ ```python
192
+ class ScienceStoryteller:
193
+ def __init__(self):
194
+ self.research_agent = ResearchAgent() # Attach to THIS object
195
+
196
+ async def process_topic(self, topic: str):
197
+ papers = await self.research_agent.search(topic) # Use THIS object's agent
198
+ ```
199
+
200
+ **Why `self`?** So each object can have its own separate data.
201
+
202
+ ```python
203
+ storyteller1 = ScienceStoryteller() # Has its own research_agent
204
+ storyteller2 = ScienceStoryteller() # Has a different research_agent
205
+ ```
206
+
207
+ ### Attributes (Instance Variables)
208
+
209
+ **Attributes** store data that belongs to an object.
210
+
211
+ ```python
212
+ self.research_agent = ResearchAgent() # This is an attribute
213
+ self.analysis_agent = AnalysisAgent() # This is an attribute
214
+ ```
215
+
216
+ **Accessing attributes:**
217
+ ```python
218
+ async def process_topic(self, topic: str):
219
+ # Use the attributes we created in __init__
220
+ papers = await self.research_agent.search(topic)
221
+ best_paper = await self.analysis_agent.select_best(papers, topic)
222
+ ```
223
+
224
+ ### Methods (Functions in a Class)
225
+
226
+ **Methods** define what an object can **do**.
227
+
228
+ ```python
229
+ class ScienceStoryteller:
230
+ async def process_topic(self, topic: str): # This is a method
231
+ """Process a research topic into a podcast."""
232
+ # ... implementation ...
233
+
234
+ def _format_paper_info(self, paper: dict) -> str: # Another method
235
+ """Format paper metadata for display."""
236
+ # ... implementation ...
237
+ ```
238
+
239
+ **Key points:**
240
+ - First parameter is always `self`
241
+ - Called using dot notation: `storyteller.process_topic("AI")`
242
+ - Can access attributes: `self.research_agent`
243
+
244
+ ### Public vs Private Naming Convention
245
+
246
+ ```python
247
+ def process_topic(self, topic): # Public - no underscore
248
+ """Meant to be called from outside the class."""
249
+
250
+ def _format_paper_info(self, paper): # Private - starts with _
251
+ """Internal helper, not meant to be called externally."""
252
+ ```
253
+
254
+ **Convention (not enforced):**
255
+ - `method_name` → Public, part of the API
256
+ - `_method_name` → Private, internal use only
257
+
258
+ ### Complete Example
259
+
260
+ ```python
261
+ class ScienceStoryteller:
262
+ """Main orchestrator for the Science Storyteller workflow."""
263
+
264
+ # Constructor - runs when object is created
265
+ def __init__(self):
266
+ self.research_agent = ResearchAgent() # Attribute
267
+ self.analysis_agent = AnalysisAgent() # Attribute
268
+ self.audio_agent = AudioAgent() # Attribute
269
+
270
+ # Public method - main workflow
271
+ async def process_topic(self, topic: str):
272
+ papers = await self.research_agent.search(topic) # Use attribute
273
+ best_paper = await self.analysis_agent.select_best(papers)
274
+ paper_info = self._format_paper_info(best_paper) # Call private method
275
+ return paper_info
276
+
277
+ # Private method - internal helper
278
+ def _format_paper_info(self, paper: dict) -> str:
279
+ return f"**Title:** {paper.get('title', 'Unknown')}"
280
+
281
+ # Usage
282
+ storyteller = ScienceStoryteller() # Create object (__init__ runs)
283
+ result = await storyteller.process_topic("AlphaFold") # Call method
284
+ ```
285
+
286
+ ### Quick Reference
287
+
288
+ | Concept | Syntax | Purpose |
289
+ |---------|--------|---------|
290
+ | **Class** | `class ClassName:` | Blueprint for objects |
291
+ | **Object** | `obj = ClassName()` | Instance created from class |
292
+ | **Constructor** | `def __init__(self):` | Initialize object state |
293
+ | **Self** | `self.attribute` | Reference to current object |
294
+ | **Attribute** | `self.name = value` | Data stored in object |
295
+ | **Method** | `def method(self, args):` | Function belonging to class |
296
+ | **Public** | `def method(self):` | External API |
297
+ | **Private** | `def _method(self):` | Internal helper |
298
+
299
+ ---
300
+
301
+ ## Async/Await Deep Dive
302
+
303
+ ### Why Async? The Three Use Cases
304
+
305
+ Based on [RealPython's async guide](https://realpython.com/async-io-python/):
306
+
307
+ 1. **Writing pausable/resumable functions**
308
+ 2. **Managing I/O-bound tasks** (network, files, databases)
309
+ 3. **Improving performance** (handle multiple tasks concurrently)
310
+
311
+ **Science Storyteller uses all three!**
312
+
313
+ ### The Problem: Blocking I/O
314
+
315
+ **Without async (blocking):**
316
+ ```python
317
+ def process_topic_sync(topic):
318
+ papers = requests.get("arxiv_api") # ⏸️ BLOCKS for 5 seconds
319
+ summary = requests.post("claude_api") # ⏸️ BLOCKS for 10 seconds
320
+ audio = requests.post("elevenlabs_api") # ⏸️ BLOCKS for 60 seconds
321
+ return results # Total: 75 seconds of BLOCKING
322
+
323
+ # During blocking:
324
+ # ❌ UI freezes
325
+ # ❌ Progress bar can't update
326
+ # ❌ Other users can't be served
327
+ # ❌ Event loop is stuck
328
+ ```
329
+
330
+ **With async (non-blocking):**
331
+ ```python
332
+ async def process_topic(topic):
333
+ papers = await arxiv_tool.search() # ⏸️ Yields control for 5 seconds
334
+ summary = await llm_tool.summarize() # ⏸️ Yields control for 10 seconds
335
+ audio = await audio_tool.convert() # ⏸️ Yields control for 60 seconds
336
+ return results # Total: 75 seconds, but non-blocking
337
+
338
+ # During await:
339
+ # ✅ UI stays responsive
340
+ # ✅ Progress bar updates
341
+ # ✅ Other users can be served
342
+ # ✅ Event loop continues running
343
+ ```
344
+
345
+ ### Visualizing Blocking vs. Async
346
+
347
+ **Blocking (Sequential) Execution:**
348
+ ```
349
+ Request 1: [--arxiv--|----claude----|----------------audio----------------|]
350
+ Request 2: [--arxiv--|----claude----|---...
351
+ Time -----> 0s 5s 15s 75s 80s 90s
352
+ ```
353
+ - The UI is frozen for the entire 75s duration of Request 1.
354
+ - Request 2 must wait for Request 1 to completely finish.
355
+
356
+ **Async (Concurrent) Execution:**
357
+ ```
358
+ Request 1: [--arxiv--] ... [----claude----] ... [----------------audio----------------]
359
+ Request 2: [--arxiv--] ... [----claude----] ... [----------------audio----------------]
360
+ Time -----> 0s 1s 5s 6s 15s 16s 75s
361
+ ```
362
+ - When Request 1 `await`s `arxiv`, the event loop is free to start Request 2.
363
+ - Both requests run concurrently, sharing time during I/O waits. The UI remains responsive throughout.
364
+
365
+ ### How Async Works: The Event Loop
366
+
367
+ ```
368
+ ┌─────────────────────────────────────────┐
369
+ │ Python Asyncio Event Loop │
370
+ │ (Single thread, multiple tasks) │
371
+ └─────────────────────────────────────────┘
372
+ ↓ ↓ ↓
373
+ Task A Task B Task C
374
+ (User 1 req) (User 2 req) (User 3 req)
375
+ ```
376
+
377
+ **When `await` is hit:**
378
+ 1. Function **pauses** at that line
379
+ 2. Control **returns** to the event loop
380
+ 3. Event loop **runs other code** (updates UI, handles requests)
381
+ 4. When I/O completes, function **resumes** from where it paused
382
+
383
+ ### Single VM, Multiple Users
384
+
385
+ **Key insight:** On Hugging Face Spaces, **all users share one Python process**.
386
+
387
+ ```
388
+ Hugging Face Space (Single VM)
389
+ ├─ Python Process (port 7860)
390
+ │ └─ Event Loop
391
+ │ ├─ Task: User A (paused at await)
392
+ │ ├─ Task: User B (paused at await)
393
+ │ └─ Task: User C (paused at await)
394
+ ```
395
+
396
+ **Without async (sequential):**
397
+ ```
398
+ User A: 0-75s (completes at 75s)
399
+ User B: 75-150s (WAITS 75s, then runs 75s = 150s total)
400
+ User C: 150-225s (WAITS 150s, then runs 75s = 225s total)
401
+ ```
402
+
403
+ **With async (concurrent):**
404
+ ```
405
+ User A: 0-75s (completes at 75s)
406
+ User B: 1-76s (starts 1s later, runs concurrently = 76s total)
407
+ User C: 2-77s (starts 2s later, runs concurrently = 77s total)
408
+ ```
409
+
410
+ ### Performance Comparison
411
+
412
+ | Metric | Without Async | With Async |
413
+ |--------|--------------|------------|
414
+ | **User A wait** | 75s | 75s |
415
+ | **User B wait** | 150s | ~76s |
416
+ | **User C wait** | 225s | ~77s |
417
+ | **UI responsiveness** | Frozen | Live updates |
418
+ | **Progress tracking** | Can't update | Works |
419
+ | **Concurrent users** | Sequential | Interleaved |
420
+
421
+ ### Gradio + Async Integration
422
+
423
+ Gradio uses **FastAPI** internally, which is async-native:
424
+
425
+ ```python
426
+ # Gradio internals (simplified)
427
+ from fastapi import FastAPI
428
+
429
+ app = FastAPI()
430
+
431
+ @app.post("/api/predict")
432
+ async def predict(request):
433
+ result = await your_gradio_function(request.data)
434
+ return result
435
+ ```
436
+
437
+ **Why this matters:**
438
+ - `gr.Progress()` only works with async (sends WebSocket updates)
439
+ - Gradio's event loop can handle multiple users
440
+ - Your async functions integrate seamlessly
441
+
442
+ ### Async Syntax Rules
443
+
444
+ **Defining async functions:**
445
+ ```python
446
+ async def my_function(): # Note the 'async' keyword
447
+ result = await some_async_operation()
448
+ return result
449
+ ```
450
+
451
+ **Calling async functions:**
452
+ ```python
453
+ # From another async function:
454
+ result = await my_function()
455
+
456
+ # From synchronous code:
457
+ import asyncio
458
+ result = asyncio.run(my_function())
459
+ ```
460
+
461
+ **Common mistake:**
462
+ ```python
463
+ # ❌ Wrong - missing await
464
+ async def process():
465
+ result = some_async_function() # This returns a coroutine, not the result!
466
+
467
+ # ✅ Correct - with await
468
+ async def process():
469
+ result = await some_async_function() # This waits and gets the actual result
470
+ ```
471
+
472
+ ### The Async Chain in Science Storyteller
473
+
474
+ ```
475
+ app.py: process_topic (async)
476
+ ↓ await
477
+ agents/research_agent.py: search (async)
478
+ ↓ await
479
+ mcp_tools/arxiv_tool.py: search_papers (async)
480
+ ↓ await
481
+ session.call_tool() (MCP I/O)
482
+
483
+ [Network request to arXiv server]
484
+ ```
485
+
486
+ **Every step must be async** because:
487
+ - MCP communication uses async I/O
488
+ - Can't `await` inside a non-async function
489
+ - Event loop requires async all the way up
490
+
491
+ ---
492
+
493
+ ## Module-by-Module Learning Path
494
+
495
+ ### Level 1: Foundation (Start Here)
496
+
497
+ #### 1. `utils/audio_processor.py`
498
+
499
+ **What it does:** File system operations for audio files
500
+
501
+ **Key concepts:**
502
+ - Creating directories with `Path.mkdir()`
503
+ - Checking file sizes with `os.path.getsize()`
504
+ - Working with file paths
505
+
506
+ **Learning exercise:**
507
+ ```python
508
+ from utils.audio_processor import ensure_audio_dir, get_file_size_mb
509
+
510
+ # Create the audio directory
511
+ ensure_audio_dir()
512
+
513
+ # Check size of a file (if it exists)
514
+ # size = get_file_size_mb("assets/audio/podcast_123.mp3")
515
+ ```
516
+
517
+ **What to look for:**
518
+ - How does it handle file paths in a cross-platform way (`pathlib.Path`)?
519
+ - The use of `exist_ok=True` to prevent errors.
520
+ - Simple, pure functions that have no side effects other than interacting with the filesystem.
521
+
522
+ **Questions to answer:**
523
+ - Why use `Path` instead of strings for file paths?
524
+ - What happens if the directory already exists?
525
+ - How is file size converted from bytes to MB?
526
+
527
+ ---
528
+
529
+ #### 2. `utils/script_formatter.py`
530
+
531
+ **What it does:** Clean and format podcast scripts for TTS
532
+
533
+ **Key concepts:**
534
+ - String manipulation (`strip()`, `replace()`)
535
+ - Regular expressions (if used)
536
+ - Estimating audio duration from text
537
+
538
+ **Learning exercise:**
539
+ ```python
540
+ from utils.script_formatter import format_podcast_script, estimate_duration
541
+
542
+ script = """
543
+ Hello! This is a test.
544
+
545
+ With extra spaces and newlines.
546
+ """
547
+
548
+ cleaned = format_podcast_script(script)
549
+ duration = estimate_duration(cleaned)
550
+
551
+ print(f"Cleaned: {cleaned}")
552
+ print(f"Duration: {duration} seconds")
553
+ ```
554
+
555
+ **What to look for:**
556
+ - How simple string methods (`.strip()`, `.replace()`) are used for cleaning.
557
+ - The logic for `estimate_duration`: it's a heuristic, not an exact calculation.
558
+ - This is another example of pure functions that are easy to test.
559
+
560
+ **Questions to answer:**
561
+ - How does text length relate to audio duration?
562
+ - What characters need to be cleaned for TTS?
563
+ - Why estimate duration before generating audio?
564
+
565
+ ---
566
+
567
+ ### Level 2: MCP Tools (Core Hackathon Requirement)
568
+
569
+ #### 3. `mcp_tools/arxiv_tool.py`
570
+
571
+ **What it does:** Connects to arXiv MCP server to search papers
572
+
573
+ **Key concepts:**
574
+ - Model Context Protocol (MCP)
575
+ - Stdio transport (stdin/stdout communication)
576
+ - Async context managers (`__aenter__`, `__aexit__`)
577
+ - JSON-RPC messaging
578
+
579
+ **Important code sections:**
580
+
581
+ **Connection setup:**
582
+ ```python
583
+ server_params = StdioServerParameters(
584
+ command="npx",
585
+ args=["-y", "@blindnotation/arxiv-mcp-server"],
586
+ env=None
587
+ )
588
+
589
+ self.exit_stack = stdio_client(server_params)
590
+ stdio_transport = await self.exit_stack.__aenter__()
591
+ read_stream, write_stream = stdio_transport
592
+ self.session = ClientSession(read_stream, write_stream)
593
+ await self.session.__aenter__()
594
+ ```
595
+
596
+ **Calling tools:**
597
+ ```python
598
+ result = await self.session.call_tool(
599
+ "search_arxiv",
600
+ {
601
+ "query": query,
602
+ "max_results": max_results,
603
+ "sort_by": sort_by
604
+ }
605
+ )
606
+ ```
607
+
608
+ **Learning exercise:**
609
+ ```python
610
+ import asyncio
611
+ from mcp_tools.arxiv_tool import ArxivTool
612
+
613
+ async def explore_arxiv():
614
+ tool = ArxivTool()
615
+
616
+ # Connect to MCP server
617
+ connected = await tool.connect()
618
+ print(f"Connected: {connected}")
619
+
620
+ # Search for papers
621
+ papers = await tool.search_papers("quantum computing", max_results=3)
622
+ print(f"Found {len(papers)} papers:")
623
+
624
+ for paper in papers:
625
+ print(f"\n Title: {paper.get('title', 'N/A')}")
626
+ print(f" Authors: {paper.get('authors', [])[:2]}")
627
+
628
+ # Clean up
629
+ await tool.disconnect()
630
+
631
+ asyncio.run(explore_arxiv())
632
+ ```
633
+
634
+ **Questions to answer:**
635
+ - What is stdio transport and why use it?
636
+ - Why do we need both `exit_stack` and `session`?
637
+ - What happens if the MCP server crashes?
638
+ - How does `call_tool` send messages to the server?
639
+
640
+ **Deep dive topics:**
641
+ - JSON-RPC protocol format
642
+ - Async context managers (what `__aenter__` and `__aexit__` do)
643
+ - Process communication (pipes and streams)
644
+
645
+ ---
646
+
647
+ #### 4. `mcp_tools/llm_tool.py`
648
+
649
+ **What it does:** Calls Anthropic Claude API for summarization
650
+
651
+ **Key concepts:**
652
+ - HTTP API requests with async
653
+ - Prompt engineering
654
+ - API authentication
655
+ - Response parsing
656
+
657
+ **Important code sections:**
658
+
659
+ **API call:**
660
+ ```python
661
+ message = self.client.messages.create(
662
+ model=self.model,
663
+ max_tokens=max_tokens,
664
+ messages=[
665
+ {"role": "user", "content": prompt}
666
+ ]
667
+ )
668
+
669
+ summary = message.content[0].text
670
+ ```
671
+
672
+ **Learning exercise:**
673
+ ```python
674
+ import asyncio
675
+ from mcp_tools.llm_tool import LLMTool
676
+
677
+ async def test_llm():
678
+ tool = LLMTool() # Needs ANTHROPIC_API_KEY in .env
679
+
680
+ # Fake paper data
681
+ paper = {
682
+ "title": "Quantum Computing Fundamentals",
683
+ "summary": "This paper explores the basic principles of quantum computing...",
684
+ "authors": [{"name": "Alice"}, {"name": "Bob"}]
685
+ }
686
+
687
+ # Generate summary
688
+ summary = await tool.summarize_paper(paper, max_tokens=500)
689
+ print(f"Summary:\n{summary}")
690
+
691
+ asyncio.run(test_llm())
692
+ ```
693
+
694
+ **Questions to answer:**
695
+ - How is the prompt structured for summarization?
696
+ - What's the difference between `max_tokens` in the request and actual tokens used?
697
+ - How does prompt engineering affect output quality?
698
+ - What happens if the API returns an error?
699
+
700
+ ---
701
+
702
+ ### Level 3: Agents (Business Logic)
703
+
704
+ #### 5. `agents/research_agent.py`
705
+
706
+ **What it does:** Autonomous paper retrieval and search optimization
707
+
708
+ **Key concepts:**
709
+ - Query enhancement (autonomous planning)
710
+ - Fallback strategies (self-correction)
711
+ - Agent initialization and cleanup
712
+
713
+ **Autonomous behaviors:**
714
+ ```python
715
+ def _enhance_query(self, topic: str) -> str:
716
+ """
717
+ Autonomous planning - agent decides how to optimize search.
718
+ """
719
+ topic_lower = topic.lower()
720
+
721
+ enhancements = {
722
+ 'ai': 'artificial intelligence machine learning',
723
+ 'ml': 'machine learning',
724
+ 'quantum': 'quantum computing physics',
725
+ }
726
+
727
+ for key, value in enhancements.items():
728
+ if key in topic_lower and value not in topic_lower:
729
+ return f"{topic} {value}"
730
+
731
+ return topic
732
+ ```
733
+
734
+ **Self-correction:**
735
+ ```python
736
+ papers = await self.arxiv_tool.search_papers(enhanced_query)
737
+
738
+ if not papers:
739
+ # Fallback: try original query
740
+ papers = await self.arxiv_tool.search_papers(topic)
741
+ ```
742
+
743
+ **Learning exercise:**
744
+ ```python
745
+ from agents.research_agent import ResearchAgent
746
+
747
+ async def test_research():
748
+ agent = ResearchAgent()
749
+ await agent.initialize()
750
+
751
+ # Test query enhancement
752
+ original = "AI"
753
+ enhanced = agent._enhance_query(original)
754
+ print(f"Original: {original}")
755
+ print(f"Enhanced: {enhanced}")
756
+
757
+ # Test search
758
+ papers = await agent.search("AlphaFold", max_results=3)
759
+ print(f"\nFound {len(papers)} papers")
760
+
761
+ await agent.cleanup()
762
+
763
+ asyncio.run(test_research())
764
+ ```
765
+
766
+ **Questions to answer:**
767
+ - Why enhance queries? What problem does it solve?
768
+ - When should you use the fallback strategy?
769
+ - Why initialize and cleanup separately from `__init__`?
770
+
771
+ ---
772
+
773
+ #### 6. `agents/analysis_agent.py`
774
+
775
+ **What it does:** Paper analysis and podcast script generation
776
+
777
+ **Key concepts:**
778
+ - Paper selection (reasoning)
779
+ - LLM-based summarization
780
+ - Script generation with prompt engineering
781
+ - Fallback content for LLM failures
782
+
783
+ **Autonomous reasoning:**
784
+ ```python
785
+ async def select_best(self, papers: list, topic: str):
786
+ """
787
+ Reasoning - evaluate and select most relevant paper.
788
+ """
789
+ scored_papers = []
790
+ for paper in papers:
791
+ score = 0
792
+
793
+ # Has abstract
794
+ if paper.get('summary') or paper.get('abstract'):
795
+ score += 1
796
+
797
+ # Recent paper
798
+ pub_date = paper.get('published', '')
799
+ if '2024' in pub_date or '2023' in pub_date:
800
+ score += 2
801
+
802
+ scored_papers.append((score, paper))
803
+
804
+ scored_papers.sort(key=lambda x: x[0], reverse=True)
805
+ return scored_papers[0][1] if scored_papers else papers[0]
806
+ ```
807
+
808
+ **Learning exercise:**
809
+ ```python
810
+ from agents.analysis_agent import AnalysisAgent
811
+
812
+ async def test_analysis():
813
+ agent = AnalysisAgent()
814
+
815
+ # Mock paper data
816
+ papers = [
817
+ {"title": "Old Paper", "published": "2020-01-01", "summary": "..."},
818
+ {"title": "New Paper", "published": "2024-01-01", "summary": "..."},
819
+ ]
820
+
821
+ best = await agent.select_best(papers, "quantum computing")
822
+ print(f"Selected: {best['title']}")
823
+
824
+ asyncio.run(test_analysis())
825
+ ```
826
+
827
+ **Questions to answer:**
828
+ - What criteria determine "best" paper?
829
+ - Why fallback to template content instead of failing?
830
+ - How does prompt engineering affect script quality?
831
+
832
+ ---
833
+
834
+ #### 7. `agents/audio_agent.py`
835
+
836
+ **What it does:** Text-to-speech conversion via ElevenLabs
837
+
838
+ **Key concepts:**
839
+ - HTTP POST with binary response
840
+ - File I/O (saving MP3 bytes)
841
+ - API timeout handling
842
+ - Voice configuration
843
+
844
+ **Learning exercise:**
845
+ ```python
846
+ from agents.audio_agent import AudioAgent
847
+
848
+ async def test_audio():
849
+ agent = AudioAgent() # Needs ELEVENLABS_API_KEY
850
+
851
+ script = "Welcome to Science Storyteller. Today we explore quantum computing."
852
+
853
+ audio_path = await agent.text_to_speech(script)
854
+
855
+ if audio_path:
856
+ print(f"Audio saved to: {audio_path}")
857
+ else:
858
+ print("Audio generation failed")
859
+
860
+ asyncio.run(test_audio())
861
+ ```
862
+
863
+ **Questions to answer:**
864
+ - Why does TTS take so long (30-60 seconds)?
865
+ - What happens if the API times out?
866
+ - How are MP3 bytes different from text?
867
+
868
+ ---
869
+
870
+ ### Level 4: Orchestration (Integration)
871
+
872
+ #### 8. `app.py` - `ScienceStoryteller` Class
873
+
874
+ **What it does:** Coordinates all agents into a complete workflow
875
+
876
+ **Key concepts:**
877
+ - Orchestrator pattern
878
+ - Error recovery
879
+ - Progress tracking
880
+ - State management
881
+
882
+ **Learning exercise:**
883
+ ```python
884
+ from app import ScienceStoryteller
885
+
886
+ async def test_orchestrator():
887
+ storyteller = ScienceStoryteller()
888
+
889
+ # Test full workflow
890
+ result = await storyteller.process_topic("quantum entanglement")
891
+ summary, script, audio, paper_info, status = result
892
+
893
+ print(f"Status: {status}")
894
+ if summary:
895
+ print(f"Summary length: {len(summary)} chars")
896
+
897
+ asyncio.run(test_orchestrator())
898
+ ```
899
+
900
+ **Questions to answer:**
901
+ - How does the orchestrator handle partial failures?
902
+ - Why return a tuple instead of a dict?
903
+ - What's the role of `gr.Progress()`?
904
+
905
+ ---
906
+
907
+ #### 9. `app.py` - Gradio Interface
908
+
909
+ **What it does:** Web UI for user interaction
910
+
911
+ **Key concepts:**
912
+ - Gradio Blocks API
913
+ - Event handlers
914
+ - Async in Gradio
915
+ - UI layout
916
+
917
+ **Learning exercise:**
918
+ ```python
919
+ # Just run the app
920
+ python app.py
921
+
922
+ # Then interact with the UI to see the flow
923
+ ```
924
+
925
+ **Questions to answer:**
926
+ - How does Gradio handle async functions?
927
+ - What's the difference between `gr.Blocks` and `gr.Interface`?
928
+ - How are outputs mapped to UI components?
929
+
930
+ ---
931
+
932
+ ## Hands-On Exercises
933
+
934
+ ### Exercise 1: Test Individual Tools
935
+
936
+ **Goal:** Verify MCP connection works
937
+
938
+ ```python
939
+ # File: test_my_learning.py
940
+ import asyncio
941
+ from mcp_tools.arxiv_tool import ArxivTool
942
+
943
+ async def main():
944
+ print("Testing ArxivTool...")
945
+
946
+ tool = ArxivTool()
947
+ connected = await tool.connect()
948
+
949
+ if connected:
950
+ print("✓ Connected to MCP server")
951
+
952
+ papers = await tool.search_papers("AlphaFold", max_results=2)
953
+ print(f"✓ Found {len(papers)} papers")
954
+
955
+ for i, paper in enumerate(papers, 1):
956
+ print(f"\n{i}. {paper.get('title', 'N/A')}")
957
+
958
+ await tool.disconnect()
959
+ print("\n✓ Disconnected")
960
+ else:
961
+ print("✗ Failed to connect")
962
+
963
+ if __name__ == "__main__":
964
+ asyncio.run(main())
965
+ ```
966
+
967
+ Run: `python test_my_learning.py`
968
+
969
+ ---
970
+
971
+ ### Exercise 2: Trace the Async Chain
972
+
973
+ **Goal:** Understand how async calls propagate
974
+
975
+ Add print statements to trace execution:
976
+
977
+ ```python
978
+ # In arxiv_tool.py
979
+ async def search_papers(self, query: str, ...):
980
+ print(f"[ArxivTool] Starting search for: {query}")
981
+ result = await self.session.call_tool("search_arxiv", {...})
982
+ print(f"[ArxivTool] Search complete, parsing results...")
983
+ return papers
984
+
985
+ # In research_agent.py
986
+ async def search(self, topic: str, max_results: int = 5):
987
+ print(f"[ResearchAgent] Enhancing query: {topic}")
988
+ enhanced = self._enhance_query(topic)
989
+ print(f"[ResearchAgent] Enhanced to: {enhanced}")
990
+ papers = await self.arxiv_tool.search_papers(enhanced)
991
+ print(f"[ResearchAgent] Got {len(papers)} papers")
992
+ return papers
993
+ ```
994
+
995
+ Then run and watch the flow!
996
+
997
+ ---
998
+
999
+ ### Exercise 3: Mock External Dependencies
1000
+
1001
+ **Goal:** Test without API keys
1002
+
1003
+ ```python
1004
+ # test_mock.py
1005
+ from unittest.mock import AsyncMock, Mock
1006
+ from agents.research_agent import ResearchAgent
1007
+
1008
+ async def test_with_mock():
1009
+ agent = ResearchAgent()
1010
+
1011
+ # Mock the arxiv_tool to avoid real API calls
1012
+ agent.arxiv_tool.search_papers = AsyncMock(return_value=[
1013
+ {"title": "Fake Paper 1", "summary": "Test"},
1014
+ {"title": "Fake Paper 2", "summary": "Test"},
1015
+ ])
1016
+
1017
+ papers = await agent.search("test topic")
1018
+
1019
+ assert len(papers) == 2
1020
+ print(f"✓ Mock test passed: {len(papers)} papers")
1021
+
1022
+ asyncio.run(test_with_mock())
1023
+ ```
1024
+
1025
+ ---
1026
+
1027
+ ### Exercise 4: Build a Mini Version
1028
+
1029
+ **Goal:** Understand the workflow by simplifying
1030
+
1031
+ ```python
1032
+ # mini_storyteller.py
1033
+ import asyncio
1034
+
1035
+ class MiniStoryteller:
1036
+ """Simplified version to understand the flow"""
1037
+
1038
+ def __init__(self):
1039
+ print("📚 Initializing agents...")
1040
+ self.research = "ResearchAgent"
1041
+ self.analysis = "AnalysisAgent"
1042
+ self.audio = "AudioAgent"
1043
+
1044
+ async def process(self, topic):
1045
+ print(f"\n🔍 Step 1: Search for '{topic}'")
1046
+ await asyncio.sleep(1) # Simulate API call
1047
+ papers = ["Paper 1", "Paper 2"]
1048
+
1049
+ print(f"📝 Step 2: Select best paper")
1050
+ await asyncio.sleep(1)
1051
+ best = papers[0]
1052
+
1053
+ print(f"✍️ Step 3: Summarize '{best}'")
1054
+ await asyncio.sleep(1)
1055
+ summary = "This is a summary..."
1056
+
1057
+ print(f"🎙️ Step 4: Generate script")
1058
+ await asyncio.sleep(1)
1059
+ script = "Welcome to the podcast..."
1060
+
1061
+ print(f"🔊 Step 5: Convert to audio")
1062
+ await asyncio.sleep(2)
1063
+ audio = "podcast.mp3"
1064
+
1065
+ print(f"✅ Done!")
1066
+ return summary, script, audio
1067
+
1068
+ async def main():
1069
+ storyteller = MiniStoryteller()
1070
+ result = await storyteller.process("AlphaFold")
1071
+ print(f"\nResult: {result}")
1072
+
1073
+ asyncio.run(main())
1074
+ ```
1075
+
1076
+ ---
1077
+
1078
+ ## Common Patterns Explained
1079
+
1080
+ ### Pattern 1: Async Context Managers
1081
+
1082
+ **What you see:**
1083
+ ```python
1084
+ self.exit_stack = stdio_client(server_params)
1085
+ stdio_transport = await self.exit_stack.__aenter__()
1086
+ # ... use the connection ...
1087
+ await self.exit_stack.__aexit__(None, None, None)
1088
+ ```
1089
+
1090
+ **What it means:**
1091
+ - `__aenter__`: Setup (open connection, allocate resources)
1092
+ - `__aexit__`: Cleanup (close connection, free resources)
1093
+
1094
+ **Better syntax:**
1095
+ ```python
1096
+ async with stdio_client(server_params) as stdio_transport:
1097
+ # Connection is open here
1098
+ read_stream, write_stream = stdio_transport
1099
+ # ... use streams ...
1100
+ # Connection automatically closed when block exits
1101
+ ```
1102
+
1103
+ **Why the manual version in the code?**
1104
+ - Need to keep connection alive for multiple operations
1105
+ - Can't use `async with` because connection persists beyond one function call
1106
+
1107
+ ---
1108
+
1109
+ ### Pattern 2: Optional Parameters with Defaults
1110
+
1111
+ ```python
1112
+ async def search(self, topic: str, max_results: int = 5):
1113
+ """Search with default max_results"""
1114
+ ```
1115
+
1116
+ **Usage:**
1117
+ ```python
1118
+ # Use default
1119
+ papers = await agent.search("AI") # max_results=5
1120
+
1121
+ # Override default
1122
+ papers = await agent.search("AI", max_results=10)
1123
+ ```
1124
+
1125
+ ---
1126
+
1127
+ ### Pattern 3: Type Hints
1128
+
1129
+ ```python
1130
+ async def search_papers(
1131
+ self,
1132
+ query: str, # Must be a string
1133
+ max_results: int = 5, # Must be an int, defaults to 5
1134
+ sort_by: str = "relevance" # Must be a string, defaults to "relevance"
1135
+ ) -> List[Dict[str, Any]]: # Returns a list of dictionaries
1136
+ ```
1137
+
1138
+ **Benefits:**
1139
+ - Self-documenting code
1140
+ - IDE autocomplete
1141
+ - Type checking tools (mypy)
1142
+ - Easier to catch bugs
1143
+
1144
+ ---
1145
+
1146
+ ### Pattern 4: Dictionary `.get()` with Defaults
1147
+
1148
+ ```python
1149
+ title = paper.get('title', 'Unknown') # Returns 'Unknown' if 'title' key missing
1150
+ ```
1151
+
1152
+ **Why not just `paper['title']`?**
1153
+ - `paper['title']` → Raises `KeyError` if missing
1154
+ - `paper.get('title', 'Unknown')` → Returns default if missing (safer)
1155
+
1156
+ ---
1157
+
1158
+ ### Pattern 5: List Comprehension
1159
+
1160
+ ```python
1161
+ author_names = [
1162
+ author.get('name', '')
1163
+ for author in authors[:5]
1164
+ if isinstance(author, dict)
1165
+ ]
1166
+ ```
1167
+
1168
+ **Equivalent to:**
1169
+ ```python
1170
+ author_names = []
1171
+ for author in authors[:5]:
1172
+ if isinstance(author, dict):
1173
+ author_names.append(author.get('name', ''))
1174
+ ```
1175
+
1176
+ ---
1177
+
1178
+ ### Pattern 6: Try/Except for Error Handling
1179
+
1180
+ ```python
1181
+ try:
1182
+ result = await api_call()
1183
+ return result
1184
+ except Exception as e:
1185
+ logger.error(f"API error: {e}")
1186
+ return fallback_result()
1187
+ ```
1188
+
1189
+ **Why?**
1190
+ - External APIs can fail
1191
+ - Network can be unreliable
1192
+ - Graceful degradation instead of crashes
1193
+
1194
+ ---
1195
+
1196
+ ## Debugging Tips
1197
+
1198
+ ### Tip 1: Use Print Debugging
1199
+
1200
+ Add strategic print statements:
1201
+
1202
+ ```python
1203
+ async def search(self, topic: str):
1204
+ print(f"🔍 [DEBUG] Searching for: {topic}")
1205
+
1206
+ enhanced = self._enhance_query(topic)
1207
+ print(f"🔍 [DEBUG] Enhanced to: {enhanced}")
1208
+
1209
+ papers = await self.arxiv_tool.search_papers(enhanced)
1210
+ print(f"🔍 [DEBUG] Found {len(papers)} papers")
1211
+
1212
+ return papers
1213
+ ```
1214
+
1215
+ ---
1216
+
1217
+ ### Tip 2: Check Logs
1218
+
1219
+ The app uses Python's logging:
1220
+
1221
+ ```python
1222
+ logging.basicConfig(
1223
+ level=logging.INFO, # Change to DEBUG for more detail
1224
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
1225
+ )
1226
+ ```
1227
+
1228
+ Run with verbose logging:
1229
+ ```bash
1230
+ python app.py 2>&1 | tee app.log
1231
+ ```
1232
+
1233
+ ---
1234
+
1235
+ ### Tip 3: Use Python REPL
1236
+
1237
+ Test small pieces interactively:
1238
+
1239
+ ```bash
1240
+ $ python
1241
+ >>> from utils.script_formatter import estimate_duration
1242
+ >>> text = "Hello world, this is a test."
1243
+ >>> duration = estimate_duration(text)
1244
+ >>> print(duration)
1245
+ 5
1246
+ ```
1247
+
1248
+ ---
1249
+
1250
+ ### Tip 4: Check Environment Variables
1251
+
1252
+ ```bash
1253
+ # Verify API keys are set
1254
+ echo $ANTHROPIC_API_KEY
1255
+ echo $ELEVENLABS_API_KEY
1256
+
1257
+ # Or in Python
1258
+ import os
1259
+ print(os.getenv("ANTHROPIC_API_KEY"))
1260
+ ```
1261
+
1262
+ ---
1263
+
1264
+ ### Tip 5: Test Error Cases
1265
+
1266
+ ```python
1267
+ # Test with invalid input
1268
+ result = await storyteller.process_topic("") # Empty string
1269
+ result = await storyteller.process_topic("xyzinvalidtopic999") # No results
1270
+ ```
1271
+
1272
+ ---
1273
+
1274
+ ### Tip 6: Use Async Debugger
1275
+
1276
+ For complex async issues:
1277
+
1278
+ ```python
1279
+ import asyncio
1280
+ asyncio.run(my_function(), debug=True) # Enables debug mode
1281
+ ```
1282
+
1283
+ ---
1284
+
1285
+ ## Further Resources
1286
+
1287
+ ### Official Documentation
1288
+
1289
+ - **Python Async/Await**: [RealPython Guide](https://realpython.com/async-io-python/)
1290
+ - **MCP Protocol**: [Official Docs](https://modelcontextprotocol.io/)
1291
+ - **Anthropic Claude API**: [API Reference](https://docs.anthropic.com/claude/reference)
1292
+ - **Gradio**: [Documentation](https://www.gradio.app/docs)
1293
+ - **ElevenLabs**: [API Docs](https://elevenlabs.io/docs/api-reference)
1294
+
1295
+ ### Learning Paths
1296
+
1297
+ **If you're new to async:**
1298
+ 1. Read RealPython's async guide
1299
+ 2. Practice with simple async examples
1300
+ 3. Understand event loops
1301
+ 4. Study this project's async chain
1302
+
1303
+ **If you're new to OOP:**
1304
+ 1. Python classes tutorial
1305
+ 2. Understand `self` and `__init__`
1306
+ 3. Practice with simple class examples
1307
+ 4. Study `ScienceStoryteller` class
1308
+
1309
+ **If you're new to MCP:**
1310
+ 1. Read MCP specification
1311
+ 2. Understand stdio transport
1312
+ 3. Study `ArxivTool` implementation
1313
+ 4. Try building your own MCP tool
1314
+
1315
+ ### Practice Projects
1316
+
1317
+ **After understanding this codebase:**
1318
+
1319
+ 1. **Add a new MCP tool**: Try Semantic Scholar instead of arXiv
1320
+ 2. **Add a new agent**: Create a fact-checking agent
1321
+ 3. **Extend functionality**: Add multiple podcast voices
1322
+ 4. **Improve error handling**: Better retry logic
1323
+ 5. **Add caching**: Cache arXiv results for 24 hours
1324
+
1325
+ ---
1326
+
1327
+ ## Review Checklist
1328
+
1329
+ Before moving on, can you answer:
1330
+
1331
+ - [ ] What's the difference between a class and an object?
1332
+ - [ ] What does `self` refer to?
1333
+ - [ ] When does `__init__` run?
1334
+ - [ ] Why use `async`/`await`?
1335
+ - [ ] How does the event loop work?
1336
+ - [ ] What is MCP and why use it?
1337
+ - [ ] How do the three agents differ?
1338
+ - [ ] What does the orchestrator do?
1339
+ - [ ] How does Gradio integrate with async?
1340
+ - [ ] Where would you add error handling?
1341
+ - [ ] What is the difference between a unit and an integration test?
1342
+
1343
+ ---
1344
+
1345
+ ## Your Learning Journey
1346
+
1347
+ **Recommended 3-Week Plan:**
1348
+
1349
+ ### Week 1: Fundamentals
1350
+ - Day 1-2: OOP basics (`__init__`, `self`, methods)
1351
+ - Day 3-4: Async/await concepts
1352
+ - Day 5-7: Study `utils/` and `mcp_tools/`
1353
+
1354
+ ### Week 2: Implementation
1355
+ - Day 8-10: Understand all three agents
1356
+ - Day 11-12: Study orchestrator
1357
+ - Day 13-14: Explore Gradio interface
1358
+
1359
+ ### Week 3: Integration & Polish
1360
+ - Day 15-17: Test full workflow
1361
+ - Day 18-19: Fix bugs, improve error handling
1362
+ - Day 20-21: Polish UI, prepare demo
1363
+
1364
+ ---
1365
+
1366
+ **Remember:** Deep understanding takes time. Don't rush. Each module builds on the previous one. Master the basics before tackling integration!
1367
+
1368
+ ---
1369
+
1370
+ **Last Updated:** November 17, 2025
1371
+ **Version:** 1.0
1372
+ **For:** MCP's 1st Birthday Hackathon 2025
1373
+
1374
+ ---
1375
+
1376
+ ## 🧪 Testing Strategy
1377
+
1378
+ A good testing strategy is crucial for building reliable software. For this project, we can use a model called the "Testing Pyramid."
1379
+
1380
+ ### Unit Tests
1381
+
1382
+ **Definition:** Test individual components in isolation.
1383
+
1384
+ - **What to test:** Pure functions, methods with no external dependencies.
1385
+ - **Tools:** Python's built-in `unittest` or `pytest`.
1386
+ - **Example:**
1387
+ ```python
1388
+ import unittest
1389
+
1390
+ class TestArxivTool(unittest.TestCase):
1391
+ def test_search_papers(self):
1392
+ tool = ArxivTool()
1393
+ result = asyncio.run(tool.search_papers("AI"))
1394
+ self.assertGreater(len(result), 0)
1395
+ ```
1396
+
1397
+ ### Integration Tests
1398
+
1399
+ **Definition:** Test how components work together.
1400
+
1401
+ - **What to test:** Interactions between modules, like agent and tool communication.
1402
+ - **Tools:** `pytest` with async support.
1403
+ - **Example:**
1404
+ ```python
1405
+ async def test_agent_tool_integration():
1406
+ agent = ResearchAgent()
1407
+ await agent.initialize()
1408
+
1409
+ papers = await agent.search("AI")
1410
+ self.assertIsInstance(papers, list)
1411
+ self.assertGreater(len(papers), 0)
1412
+ ```
1413
+
1414
+ ### End-to-End Tests
1415
+
1416
+ **Definition:** Test the complete workflow from start to finish.
1417
+
1418
+ - **What to test:** User scenarios, like submitting a topic and receiving audio.
1419
+ - **Tools:** Gradio's built-in testing, Selenium for UI tests.
1420
+ - **Example:**
1421
+ ```python
1422
+ def test_gradio_interface(client):
1423
+ response = client.post("/api/predict", json={"data": "AI in healthcare"})
1424
+ assert response.status_code == 200
1425
+ assert "audio" in response.json()
1426
+ ```
1427
+
1428
+ ### Load Tests
1429
+
1430
+ **Definition:** Test system behavior under heavy load.
1431
+
1432
+ - **What to test:** How the system handles many requests at once.
1433
+ - **Tools:** Locust, JMeter.
1434
+ - **Example:**
1435
+ ```
1436
+ locust -f load_test.py
1437
+ ```
1438
+
1439
+ ### Security Tests
1440
+
1441
+ **Definition:** Identify vulnerabilities in the application.
1442
+
1443
+ - **What to test:** API security, data validation, authentication.
1444
+ - **Tools:** OWASP ZAP, Burp Suite.
1445
+ - **Example:**
1446
+ ```
1447
+ zap-cli quick-scan --self-contained --spider -r http://localhost:7860
1448
+ ```
1449
+
1450
+ ### Best Practices
1451
+
1452
+ - **Automate tests**: Use CI/CD pipelines to run tests automatically.
1453
+ - **Test coverage**: Aim for at least 80% coverage, but prioritize critical paths.
1454
+ - **Mock external services**: Use tools like `vcr.py` or `responses` to mock API calls.
1455
+ - **Data-driven tests**: Use parameterized tests to cover multiple scenarios.
1456
+ - **Regularly review and update tests**: As the code evolves, so should the tests.
1457
+
1458
+ ---