docs: major documentation restructure and cleanup for v0.2.0
Documentation consolidation: - Merge docs/contributing/*.md into docs/development.md - Merge docs/reference/internals/*.md into docs/rpc-development.md - Move rpc-ui-reference.md to docs/rpc-reference.md - Consolidate examples/ into docs/examples/ (6 files total) - Remove getting-started.md (content in README) - Remove docs/README.md (navigation implicit) Cleanup: - Remove AGENTS.md (redundant with CLAUDE.md) - Remove RELEASING.md (merged into docs/development.md) - Remove .gemini/ and .github/copilot-instructions.md - Remove investigation files and artifacts - Add gitignore for auto-generated CLAUDE.md files Version bump: 0.1.4 → 0.2.0 (new features per stability.md) Final structure: docs/ ├── cli-reference.md # User docs ├── python-api.md ├── configuration.md ├── troubleshooting.md ├── stability.md ├── development.md # Contributor docs (merged) ├── rpc-development.md # RPC docs (merged) ├── rpc-reference.md ├── examples/ # Consolidated examples └── designs/ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3de99fb928
commit
fbc4fd5de7
59 changed files with 820 additions and 5790 deletions
|
|
@ -1,44 +0,0 @@
|
|||
# Code Review Style Guide
|
||||
|
||||
This project is an unofficial Python client for Google NotebookLM using undocumented RPC APIs.
|
||||
|
||||
## Key Review Focus Areas
|
||||
|
||||
### Async Patterns
|
||||
- All API methods should be `async` and use `await` properly
|
||||
- Use `async with` for context managers (e.g., `NotebookLMClient`)
|
||||
- Avoid blocking calls in async code
|
||||
|
||||
### RPC Layer (`src/notebooklm/rpc/`)
|
||||
- Method IDs in `types.py` are undocumented and may change
|
||||
- Parameter positions are critical - verify against existing patterns
|
||||
- Source ID nesting varies: `[id]`, `[[id]]`, `[[[id]]]`, `[[[[id]]]]`
|
||||
|
||||
### Error Handling
|
||||
- Use specific exception types from `types.py` (e.g., `SourceProcessingError`, `SourceTimeoutError`)
|
||||
- Don't catch generic `Exception` unless re-raising
|
||||
|
||||
### Type Annotations
|
||||
- All public APIs should have type hints
|
||||
- Use `dataclasses` for data structures (see `types.py`)
|
||||
|
||||
### Testing
|
||||
- Unit tests: no network calls, mock at RPC layer
|
||||
- Integration tests: mock HTTP responses
|
||||
- E2E tests: marked with `@pytest.mark.e2e`, require auth
|
||||
|
||||
### CLI (`src/notebooklm/cli/`)
|
||||
- Use Click decorators consistently
|
||||
- Follow existing command patterns in `helpers.py`
|
||||
|
||||
## Code Style
|
||||
|
||||
- Formatter: `ruff format`
|
||||
- Linter: `ruff check`
|
||||
- Python: 3.10+
|
||||
- Follow PEP-8 with 88-character line limit (Black-compatible)
|
||||
|
||||
## What NOT to Flag
|
||||
|
||||
- Undocumented RPC method IDs (these are intentionally opaque)
|
||||
- `# type: ignore` comments on RPC responses (necessary for untyped Google APIs)
|
||||
39
.github/copilot-instructions.md
vendored
39
.github/copilot-instructions.md
vendored
|
|
@ -1,39 +0,0 @@
|
|||
# Code Review Guidelines for notebooklm-py
|
||||
|
||||
## Project Context
|
||||
|
||||
This is an unofficial Python client for Google NotebookLM using undocumented RPC APIs. The codebase uses async/await patterns extensively with httpx for HTTP.
|
||||
|
||||
## Security
|
||||
|
||||
- Flag any hardcoded credentials, API keys, or tokens
|
||||
- Check for proper CSRF token handling in RPC calls
|
||||
- Ensure cookies and auth data are not logged or exposed
|
||||
- Verify input validation for user-provided URLs and content
|
||||
|
||||
## Python Patterns
|
||||
|
||||
- Require type hints for public API methods in `client.py` and `_*.py` files
|
||||
- Prefer `async with` context managers for HTTP client lifecycle
|
||||
- Use `httpx.AsyncClient` consistently (not requests or aiohttp)
|
||||
- Check for proper exception handling in async code
|
||||
|
||||
## Architecture
|
||||
|
||||
- RPC method IDs in `rpc/types.py` are the source of truth
|
||||
- Verify parameter nesting matches existing patterns (some need `[id]`, others `[[id]]`, etc.)
|
||||
- CLI commands should use Click decorators consistently
|
||||
- Client methods should be in the appropriate namespace (`notebooks`, `sources`, `artifacts`, `chat`)
|
||||
|
||||
## Testing
|
||||
|
||||
- New features should have corresponding tests
|
||||
- Mock HTTP responses in integration tests, don't call real APIs
|
||||
- E2E tests are separate and require authentication
|
||||
|
||||
## Style
|
||||
|
||||
- Line length limit is 100 characters
|
||||
- Use double quotes for strings
|
||||
- Imports should be sorted (ruff handles this)
|
||||
- No trailing whitespace
|
||||
7
.github/workflows/CLAUDE.md
vendored
7
.github/workflows/CLAUDE.md
vendored
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -19,6 +19,16 @@ captured_rpcs/
|
|||
.sisyphus/
|
||||
.claude/
|
||||
|
||||
# Auto-generated by claude-mem plugin
|
||||
**/CLAUDE.md
|
||||
!/CLAUDE.md
|
||||
|
||||
# Investigation artifacts
|
||||
investigate*.py
|
||||
INVESTIGATION*.md
|
||||
investigation_output/
|
||||
downloads/
|
||||
|
||||
# VCR cassettes - committed after security review
|
||||
# Cassettes are scrubbed of sensitive data (cookies, tokens, user IDs, emails)
|
||||
# See tests/vcr_config.py for scrubbing patterns
|
||||
|
|
|
|||
115
AGENTS.md
115
AGENTS.md
|
|
@ -1,115 +0,0 @@
|
|||
# AGENTS.md
|
||||
|
||||
Guidelines for AI agents working on `notebooklm-py`.
|
||||
|
||||
**IMPORTANT:** Follow documentation rules in [CONTRIBUTING.md](CONTRIBUTING.md) - especially the file creation and naming conventions.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
See [CLAUDE.md](CLAUDE.md) for full project context. Essential commands:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate # Always activate venv first
|
||||
pytest # Run tests
|
||||
pip install -e ".[all]" # Install in dev mode
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Type Annotations (Python 3.10+)
|
||||
```python
|
||||
def process(items: list[str]) -> dict[str, Any]: ...
|
||||
async def query(notebook_id: str, source_ids: Optional[list[str]] = None): ...
|
||||
|
||||
# Use TYPE_CHECKING for circular imports
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from ..api_client import NotebookLMClient
|
||||
```
|
||||
|
||||
### Async Patterns
|
||||
```python
|
||||
# All client methods are async - use namespaced APIs
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
notebooks = await client.notebooks.list()
|
||||
await client.sources.add_url(nb_id, url)
|
||||
result = await client.chat.ask(nb_id, question)
|
||||
```
|
||||
|
||||
### Data Structures
|
||||
```python
|
||||
@dataclass
|
||||
class Notebook:
|
||||
id: str
|
||||
title: str
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: list[Any]) -> "Notebook": ...
|
||||
```
|
||||
|
||||
### Enums for Constants
|
||||
```python
|
||||
class RPCMethod(str, Enum):
|
||||
LIST_NOTEBOOKS = "wXbhsf"
|
||||
|
||||
class AudioFormat(int, Enum):
|
||||
DEEP_DIVE = 1
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```python
|
||||
class RPCError(Exception):
|
||||
def __init__(self, message: str, rpc_id: Optional[str] = None, code: Optional[Any] = None):
|
||||
self.rpc_id, self.code = rpc_id, code
|
||||
super().__init__(message)
|
||||
|
||||
raise RPCError(f"No result found for RPC ID: {rpc_id}", rpc_id=rpc_id)
|
||||
raise ValueError(f"Invalid YouTube URL: {url}") # For validation
|
||||
```
|
||||
|
||||
### Docstrings
|
||||
```python
|
||||
def decode_response(raw_response: str, rpc_id: str, allow_null: bool = False) -> Any:
|
||||
"""Complete decode pipeline: strip prefix -> parse chunks -> extract result.
|
||||
|
||||
Args:
|
||||
raw_response: Raw response text from batchexecute
|
||||
rpc_id: RPC method ID to extract result for
|
||||
allow_null: If True, return None instead of raising when null
|
||||
|
||||
Returns:
|
||||
Decoded result data
|
||||
|
||||
Raises:
|
||||
RPCError: If RPC returned an error or result not found
|
||||
"""
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
```python
|
||||
# Class-based for related tests
|
||||
class TestDecodeResponse:
|
||||
def test_full_decode_pipeline(self): ...
|
||||
|
||||
# Markers
|
||||
@pytest.mark.e2e # End-to-end (requires auth)
|
||||
@pytest.mark.slow # Long-running (audio/video)
|
||||
@pytest.mark.asyncio # Async tests
|
||||
|
||||
# Async tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notebooks(self, client):
|
||||
notebooks = await client.notebooks.list()
|
||||
assert isinstance(notebooks, list)
|
||||
```
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Suppress type errors with `# type: ignore`
|
||||
- Commit `.env` files or credentials
|
||||
- Add dependencies without updating `pyproject.toml`
|
||||
- Change RPC method IDs without verifying via network capture
|
||||
- Delete or modify e2e tests without running them
|
||||
- Create documentation files without following CONTRIBUTING.md rules
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2026-01-13
|
||||
|
||||
### Added
|
||||
- **Source fulltext extraction** - Retrieve the complete indexed text content of any source
|
||||
- New `client.sources.get_fulltext(notebook_id, source_id)` Python API
|
||||
|
|
@ -17,6 +19,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Each reference includes `source_id`, `cited_text`, `start_char`, `end_char`, `chunk_id`
|
||||
- Use `notebooklm ask "question" --json` to see references in CLI output
|
||||
- **Source status helper** - New `source_status_to_str()` function for consistent status display
|
||||
- **Quiz and flashcard downloads** - Export interactive study materials in multiple formats
|
||||
- New `download quiz` and `download flashcards` CLI commands
|
||||
- Supports JSON, Markdown, and HTML output formats via `--format` flag
|
||||
- Python API: `client.artifacts.download_quiz()` and `client.artifacts.download_flashcards()`
|
||||
- **Extended artifact downloads** - Download additional artifact types
|
||||
- New `download report` command (exports as Markdown)
|
||||
- New `download mind-map` command (exports as JSON)
|
||||
- New `download data-table` command (exports as CSV)
|
||||
- All download commands support `--all`, `--latest`, `--name`, and `--artifact` selection options
|
||||
|
||||
### Fixed
|
||||
- **Regional Google domain authentication** - SID cookie extraction now works with regional Google domains (e.g., google.co.uk, google.de) in addition to google.com
|
||||
- **URL hostname validation** - Use proper URL parsing instead of string operations for security
|
||||
|
||||
### Changed
|
||||
- **Pre-commit checks** - Added mypy type checking to required pre-commit workflow
|
||||
|
|
@ -139,12 +154,13 @@ This is the initial public release of `notebooklm-py`. While core functionality
|
|||
|
||||
### Known Issues
|
||||
|
||||
- **RPC method IDs may change**: Google can update their internal APIs at any time, breaking this library. Check the [RPC Capture](docs/reference/internals/rpc-capture.md) for how to identify and update method IDs.
|
||||
- **RPC method IDs may change**: Google can update their internal APIs at any time, breaking this library. Check the [RPC Development Guide](docs/rpc-development.md) for how to identify and update method IDs.
|
||||
- **Rate limiting**: Heavy usage may trigger Google's rate limits. Add delays between bulk operations.
|
||||
- **Authentication expiry**: CSRF tokens expire after some time. Re-run `notebooklm login` if you encounter auth errors.
|
||||
- **Large file uploads**: Files over 50MB may fail or timeout. Split large documents if needed.
|
||||
|
||||
[Unreleased]: https://github.com/teng-lin/notebooklm-py/compare/v0.1.4...HEAD
|
||||
[Unreleased]: https://github.com/teng-lin/notebooklm-py/compare/v0.2.0...HEAD
|
||||
[0.2.0]: https://github.com/teng-lin/notebooklm-py/compare/v0.1.4...v0.2.0
|
||||
[0.1.4]: https://github.com/teng-lin/notebooklm-py/compare/v0.1.3...v0.1.4
|
||||
[0.1.3]: https://github.com/teng-lin/notebooklm-py/compare/v0.1.2...v0.1.3
|
||||
[0.1.2]: https://github.com/teng-lin/notebooklm-py/compare/v0.1.1...v0.1.2
|
||||
|
|
|
|||
|
|
@ -178,12 +178,13 @@ Commands are organized as:
|
|||
## Documentation
|
||||
|
||||
All docs use lowercase-kebab naming in `docs/`:
|
||||
- `docs/getting-started.md` - Installation and first workflow
|
||||
- `docs/cli-reference.md` - CLI commands
|
||||
- `docs/python-api.md` - Python API reference
|
||||
- `docs/configuration.md` - Storage and settings
|
||||
- `docs/troubleshooting.md` - Known issues
|
||||
- `docs/contributing/` - Architecture, debugging, testing guides
|
||||
- `docs/development.md` - Architecture, testing, releasing
|
||||
- `docs/rpc-development.md` - RPC capture and debugging
|
||||
- `docs/rpc-reference.md` - RPC payload structures
|
||||
|
||||
## When to Suggest CLI vs API
|
||||
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ Design decisions should be captured where they're most useful, not in separate d
|
|||
|------|--------|---------|
|
||||
| Root GitHub files | `UPPERCASE.md` | `README.md`, `CONTRIBUTING.md` |
|
||||
| Agent files | `UPPERCASE.md` | `CLAUDE.md`, `AGENTS.md` |
|
||||
| Folder README | `README.md` | `docs/README.md` (GitHub auto-renders) |
|
||||
| All other docs/ files | `lowercase-kebab.md` | `getting-started.md`, `cli-reference.md` |
|
||||
| Subfolder README | `README.md` | `docs/examples/README.md` |
|
||||
| All other docs/ files | `lowercase-kebab.md` | `cli-reference.md`, `contributing.md` |
|
||||
| Scratch files | `YYYY-MM-DD-context.md` | `2026-01-06-debug-auth.md` |
|
||||
|
||||
### Status Headers
|
||||
|
|
@ -130,7 +130,7 @@ Agents should ignore files marked `Deprecated`.
|
|||
|
||||
1. **Link, Don't Copy** - Reference README.md sections instead of repeating commands. Prevents drift between docs.
|
||||
|
||||
2. **Scoped Instructions** - See `docs/README.md` for folder-specific documentation rules.
|
||||
2. **Scoped Instructions** - Subfolders like `docs/examples/` may have their own README.md with folder-specific rules.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -138,19 +138,14 @@ Agents should ignore files marked `Deprecated`.
|
|||
|
||||
```
|
||||
docs/
|
||||
├── README.md # Folder-specific rules
|
||||
├── getting-started.md # Installation and first workflow
|
||||
├── cli-reference.md # CLI command reference
|
||||
├── python-api.md # Python API reference
|
||||
├── configuration.md # Storage and settings
|
||||
├── troubleshooting.md # Common issues and solutions
|
||||
├── stability.md # API versioning policy
|
||||
├── development.md # Architecture, testing, releasing
|
||||
├── rpc-development.md # RPC capture and debugging
|
||||
├── rpc-reference.md # RPC payload structures
|
||||
├── examples/ # Runnable example scripts
|
||||
├── contributing/ # Contributor guides
|
||||
│ ├── architecture.md # Code structure
|
||||
│ ├── debugging.md # Network capture guide
|
||||
│ └── testing.md # Running tests
|
||||
├── reference/
|
||||
│ └── internals/ # Reverse engineering notes
|
||||
└── scratch/ # Temporary agent work (disposable)
|
||||
└── YYYY-MM-DD-context.md
|
||||
└── designs/ # Design decisions (ADRs)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
# Flashcard & Quiz Download Investigation Findings
|
||||
|
||||
## Summary
|
||||
|
||||
**Successfully discovered how to download quiz and flashcard content from NotebookLM.**
|
||||
|
||||
The key discovery is a new RPC method `v9rmvd` (not previously documented) that returns the full quiz/flashcard content as a self-contained HTML document with embedded JSON data.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. New RPC Method: `v9rmvd`
|
||||
|
||||
- **RPC ID**: `v9rmvd`
|
||||
- **Parameters**: `[artifact_id]`
|
||||
- **Purpose**: Fetch quiz/flashcard content for display
|
||||
- **Returns**: Full artifact data with HTML at position `[9][0]`
|
||||
|
||||
### 2. Response Structure
|
||||
|
||||
```
|
||||
[
|
||||
[0] artifact_id
|
||||
[1] title
|
||||
[2] type (4 = quiz/flashcard)
|
||||
[3] [[source_ids]]
|
||||
[4] status
|
||||
[5-8] null
|
||||
[9] [html_content, metadata] <-- The actual content!
|
||||
[10] timestamps
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Position `[9][0]` contains a complete HTML document with:
|
||||
- Embedded CSS/fonts
|
||||
- JavaScript for interactivity
|
||||
- **JSON data with all questions/answers**
|
||||
|
||||
### 3. Quiz Data Structure
|
||||
|
||||
The HTML contains a `<script>` tag with quiz data in this format:
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "What is the core philosophy of the project?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "The model itself is the agent...",
|
||||
"rationale": "This aligns with the project's philosophy...",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "The number and variety of tools...",
|
||||
"rationale": "While tools are necessary...",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "Consider the stated ratio of importance..."
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Flashcard Data Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"f": "What is the fundamental loop that every coding agent is based on?",
|
||||
"b": "A loop where the model calls tools until it's done, and the results are appended to the message history."
|
||||
}
|
||||
```
|
||||
|
||||
- `f` = front (question)
|
||||
- `b` = back (answer)
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Add RPC Method
|
||||
Add to `src/notebooklm/rpc/types.py`:
|
||||
```python
|
||||
GET_ARTIFACT_CONTENT = "v9rmvd" # Fetch quiz/flashcard/interactive content
|
||||
```
|
||||
|
||||
### Step 2: Create Content Fetcher
|
||||
In `_artifacts.py`, add method to fetch content:
|
||||
```python
|
||||
async def _get_artifact_content(self, artifact_id: str) -> list | None:
|
||||
"""Fetch full artifact content including HTML for interactive types."""
|
||||
return await self._core.rpc_call(
|
||||
RPCMethod.GET_ARTIFACT_CONTENT,
|
||||
[artifact_id],
|
||||
source_path=f"/notebook/{self._current_notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Parse Quiz/Flashcard Data
|
||||
Extract JSON from HTML response:
|
||||
```python
|
||||
import re
|
||||
import json
|
||||
|
||||
def _parse_quiz_from_html(html: str) -> list[dict]:
|
||||
"""Extract quiz questions from embedded HTML."""
|
||||
# Find the JSON data embedded in the HTML
|
||||
match = re.search(r'"questions"\s*:\s*\[(.*?)\]\s*}', html, re.DOTALL)
|
||||
if match:
|
||||
return json.loads(f"[{match.group(1)}]")
|
||||
return []
|
||||
|
||||
def _parse_flashcards_from_html(html: str) -> list[dict]:
|
||||
"""Extract flashcard data from embedded HTML."""
|
||||
# Similar pattern for flashcards
|
||||
match = re.search(r'"cards"\s*:\s*\[(.*?)\]\s*}', html, re.DOTALL)
|
||||
if match:
|
||||
return json.loads(f"[{match.group(1)}]")
|
||||
return []
|
||||
```
|
||||
|
||||
### Step 4: Download Methods
|
||||
```python
|
||||
async def download_quiz(
|
||||
self,
|
||||
notebook_id: str,
|
||||
output_path: Path,
|
||||
artifact_id: str | None = None,
|
||||
format: str = "json" # json, markdown, html
|
||||
) -> Path:
|
||||
"""Download quiz questions."""
|
||||
|
||||
async def download_flashcards(
|
||||
self,
|
||||
notebook_id: str,
|
||||
output_path: Path,
|
||||
artifact_id: str | None = None,
|
||||
format: str = "json" # json, markdown, html
|
||||
) -> Path:
|
||||
"""Download flashcard deck."""
|
||||
```
|
||||
|
||||
### Step 5: Export Formats
|
||||
|
||||
**JSON Export:**
|
||||
```json
|
||||
{
|
||||
"title": "Agent Quiz",
|
||||
"questions": [
|
||||
{
|
||||
"question": "...",
|
||||
"options": [{"text": "...", "correct": true}, ...],
|
||||
"hint": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Markdown Export:**
|
||||
```markdown
|
||||
# Agent Quiz
|
||||
|
||||
## Question 1
|
||||
What is the core philosophy of the project?
|
||||
|
||||
- [ ] The agent's performance is primarily determined by clever engineering
|
||||
- [x] The model itself is the agent, and the surrounding code's main job is to provide tools
|
||||
|
||||
**Hint:** Consider the stated ratio of importance...
|
||||
```
|
||||
|
||||
**HTML Export:**
|
||||
Return the original HTML for interactive use.
|
||||
|
||||
## Test Data
|
||||
|
||||
- Notebook: `167481cd-23a3-4331-9a45-c8948900bf91` (Claude Code High School)
|
||||
- Quiz ID: `a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767` (Agent Quiz)
|
||||
- Flashcard ID: `173255d8-12b3-4c67-b925-a76ce6c71735` (Agent Flashcards)
|
||||
|
||||
## Files Created During Investigation
|
||||
|
||||
- `investigate_v9rmvd_direct.py` - Working script to fetch quiz/flashcard content
|
||||
- `investigation_output/quiz_content_v9rmvd.json` - Full quiz response
|
||||
- `investigation_output/flashcard_content_v9rmvd.json` - Full flashcard response
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Add `GET_ARTIFACT_CONTENT = "v9rmvd"` to rpc/types.py
|
||||
2. Implement `download_quiz()` and `download_flashcards()` in `_artifacts.py`
|
||||
3. Add CLI commands for downloading
|
||||
4. Write unit tests with mock data
|
||||
5. Test with real API (e2e tests)
|
||||
17
README.md
17
README.md
|
|
@ -48,7 +48,6 @@ pip install notebooklm-py
|
|||
pip install "notebooklm-py[browser]"
|
||||
playwright install chromium
|
||||
```
|
||||
See [Installation](#installation) for options.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -109,12 +108,12 @@ asyncio.run(main())
|
|||
### Agent Skills (Claude Code)
|
||||
|
||||
```bash
|
||||
# Install the skill
|
||||
# Install via CLI or ask Claude Code to do it
|
||||
notebooklm skill install
|
||||
|
||||
# Then use natural language in Claude Code:
|
||||
# Then use natural language:
|
||||
# "Create a podcast about quantum computing"
|
||||
# "Summarize these URLs into a notebook"
|
||||
# "Download the quiz as markdown"
|
||||
# "/notebooklm generate video"
|
||||
```
|
||||
|
||||
|
|
@ -127,12 +126,11 @@ notebooklm skill install
|
|||
| **Chat** | Questions, conversation history, custom personas |
|
||||
| **Generation** | Audio podcasts, video, slides, quizzes, flashcards, reports, infographics, mind maps |
|
||||
| **Research** | Web and Drive research agents with auto-import |
|
||||
| **Downloads** | Audio, video, slides, infographics |
|
||||
| **Downloads** | Audio, video, slides, infographics, reports, mind maps, data tables, quizzes, flashcards |
|
||||
| **Agent Skills** | Claude Code skill for LLM-driven automation |
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Getting Started](docs/getting-started.md)** - Installation and first workflow
|
||||
- **[CLI Reference](docs/cli-reference.md)** - Complete command documentation
|
||||
- **[Python API](docs/python-api.md)** - Full API reference
|
||||
- **[Configuration](docs/configuration.md)** - Storage and settings
|
||||
|
|
@ -141,10 +139,9 @@ notebooklm skill install
|
|||
|
||||
### For Contributors
|
||||
|
||||
- **[Architecture](docs/contributing/architecture.md)** - Code structure
|
||||
- **[Testing](docs/contributing/testing.md)** - Running and writing tests
|
||||
- **[RPC Capture](docs/reference/internals/rpc-capture.md)** - Protocol reference and capture guides
|
||||
- **[Debugging](docs/contributing/debugging.md)** - Network capture guide
|
||||
- **[Development Guide](docs/development.md)** - Architecture, testing, and releasing
|
||||
- **[RPC Development](docs/rpc-development.md)** - Protocol capture and debugging
|
||||
- **[RPC Reference](docs/rpc-reference.md)** - Payload structures
|
||||
- **[Changelog](CHANGELOG.md)** - Version history and release notes
|
||||
- **[Security](SECURITY.md)** - Security policy and credential handling
|
||||
|
||||
|
|
|
|||
50
RELEASING.md
50
RELEASING.md
|
|
@ -1,50 +0,0 @@
|
|||
# Releasing notebooklm-py
|
||||
|
||||
## Pre-Release Checklist
|
||||
|
||||
### Code Quality
|
||||
- [ ] `mypy src/notebooklm` - 0 errors
|
||||
- [ ] `pytest tests/unit tests/integration -v` - 100% pass
|
||||
- [ ] `coverage report` - >= 70% coverage (target: 80%)
|
||||
- [ ] No TODO/FIXME in critical paths
|
||||
|
||||
### Packaging
|
||||
- [ ] Version bumped in `pyproject.toml`
|
||||
- [ ] Version bumped in `src/notebooklm/__init__.py`
|
||||
- [ ] CHANGELOG.md updated with release date
|
||||
- [ ] GitHub URLs correct (not "clinet")
|
||||
- [ ] License file present
|
||||
|
||||
### Documentation
|
||||
- [ ] README examples work copy-paste
|
||||
- [ ] CLI help text accurate (`notebooklm --help`)
|
||||
- [ ] Python API docs match actual signatures
|
||||
|
||||
### Manual Testing
|
||||
- [ ] `pip install .` succeeds
|
||||
- [ ] `notebooklm login` opens browser
|
||||
- [ ] `notebooklm list` shows notebooks
|
||||
- [ ] `notebooklm create "Test"` succeeds
|
||||
- [ ] `notebooklm source add <url>` succeeds
|
||||
- [ ] `notebooklm ask "question"` returns answer
|
||||
|
||||
### PyPI Publishing
|
||||
1. Test on TestPyPI first:
|
||||
```bash
|
||||
hatch build
|
||||
hatch publish -r test
|
||||
```
|
||||
2. Verify at https://test.pypi.org/project/notebooklm-py/
|
||||
3. Publish to PyPI:
|
||||
```bash
|
||||
hatch publish
|
||||
```
|
||||
|
||||
## Version Bumping
|
||||
|
||||
1. Update version in `pyproject.toml`
|
||||
2. Update version in `src/notebooklm/__init__.py`
|
||||
3. Update CHANGELOG.md
|
||||
4. Commit: `git commit -m "chore: bump version to X.Y.Z"`
|
||||
5. Tag: `git tag vX.Y.Z`
|
||||
6. Push: `git push && git push --tags`
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# Documentation Folder
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-08
|
||||
|
||||
This folder contains all project documentation. AI agents must follow the rules in `/CONTRIBUTING.md`.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose | File Format |
|
||||
|--------|---------|-------------|
|
||||
| `contributing/` | Contributor guides | `lowercase-kebab.md` |
|
||||
| `examples/` | Runnable example scripts | `lowercase-kebab.py` |
|
||||
| `reference/internals/` | Reverse engineering notes | `lowercase-kebab.md` |
|
||||
| `designs/` | Approved design decisions | `lowercase-kebab.md` |
|
||||
| `scratch/` | Temporary agent work | `YYYY-MM-DD-context.md` |
|
||||
|
||||
## Rules for This Folder
|
||||
|
||||
1. **Root files are defined** - Only the files listed in "Top-Level Files" belong at `docs/` root. All other docs go in subfolders.
|
||||
|
||||
2. **Reference docs are stable** - Only update `reference/` files when fixing errors or adding significant new information.
|
||||
|
||||
3. **Designs are living docs** - Files in `designs/` document architectural decisions. Delete when implementation is complete and design is captured elsewhere (code comments, PRs).
|
||||
|
||||
4. **Scratch is temporary** - Files in `scratch/` can be deleted after 30 days. Always use date prefix.
|
||||
|
||||
## Top-Level Files
|
||||
|
||||
- `getting-started.md` - Installation and first workflow
|
||||
- `cli-reference.md` - Complete CLI command reference
|
||||
- `python-api.md` - Python API reference with examples
|
||||
- `configuration.md` - Storage, environment variables, settings
|
||||
- `troubleshooting.md` - Common errors and known issues
|
||||
- `releasing.md` - PyPI release checklist and process
|
||||
|
|
@ -1,379 +0,0 @@
|
|||
# Adding New RPC Methods
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-07
|
||||
|
||||
Step-by-step guide for adding new RPC methods to `notebooklm-py`.
|
||||
|
||||
## When You Need This
|
||||
|
||||
- **New feature**: NotebookLM adds a feature you want to support
|
||||
- **Method ID changed**: Google updated an RPC ID (causes `RPCError: No result found`)
|
||||
- **Payload changed**: Existing method returns unexpected results
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
1. Capture → 2. Decode → 3. Implement → 4. Test → 5. Document
|
||||
```
|
||||
|
||||
| Step | Tools | Output |
|
||||
|------|-------|--------|
|
||||
| Capture | Chrome DevTools or Playwright | Raw request/response |
|
||||
| Decode | Browser console or Python | RPC ID + params structure |
|
||||
| Implement | Code editor | types.py + _*.py changes |
|
||||
| Test | pytest | Unit + integration + E2E tests |
|
||||
| Document | Markdown | Update rpc-ui-reference.md |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Capture the RPC Call
|
||||
|
||||
Use one of these approaches (see [rpc-capture.md](../reference/internals/rpc-capture.md) for details):
|
||||
|
||||
### Quick: Chrome DevTools
|
||||
|
||||
1. Open NotebookLM in Chrome
|
||||
2. Open DevTools → Network tab
|
||||
3. Filter by `batchexecute`
|
||||
4. Perform the action in the UI
|
||||
5. Click the request → copy from Headers and Payload tabs
|
||||
|
||||
### Systematic: Playwright
|
||||
|
||||
```python
|
||||
# See rpc-capture.md for full setup
|
||||
async def capture_action(page, action_name):
|
||||
# Clear, perform action, capture request
|
||||
...
|
||||
```
|
||||
|
||||
### What to Capture
|
||||
|
||||
From the request:
|
||||
- **URL `rpcids` parameter**: The 6-character RPC ID (e.g., `wXbhsf`)
|
||||
- **`f.req` body**: URL-encoded payload
|
||||
|
||||
From the response:
|
||||
- **Response body**: Starts with `)]}'\n`, contains result
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Decode the Payload
|
||||
|
||||
### Decode f.req in Browser Console
|
||||
|
||||
```javascript
|
||||
// Paste the f.req value
|
||||
const encoded = "[[...encoded data...]]";
|
||||
const decoded = decodeURIComponent(encoded);
|
||||
const outer = JSON.parse(decoded);
|
||||
|
||||
console.log("RPC ID:", outer[0][0][0]);
|
||||
console.log("Params:", JSON.parse(outer[0][0][1]));
|
||||
```
|
||||
|
||||
### Decode in Python
|
||||
|
||||
```python
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
|
||||
def decode_f_req(encoded: str) -> dict:
|
||||
decoded = unquote(encoded)
|
||||
outer = json.loads(decoded)
|
||||
inner = outer[0][0]
|
||||
return {
|
||||
"rpc_id": inner[0],
|
||||
"params": json.loads(inner[1]) if inner[1] else None,
|
||||
}
|
||||
```
|
||||
|
||||
### Understand the Structure
|
||||
|
||||
RPC params are **position-sensitive arrays**. Document each position:
|
||||
|
||||
```python
|
||||
# Example: ADD_SOURCE for URL
|
||||
params = [
|
||||
[[None, None, [url], None, None, None, None, None]], # 0: Source data, URL at [2]
|
||||
notebook_id, # 1: Notebook ID
|
||||
[2], # 2: Fixed flag
|
||||
None, # 3: Optional settings
|
||||
None, # 4: Optional settings
|
||||
]
|
||||
```
|
||||
|
||||
Key patterns to identify:
|
||||
- **Nested source IDs**: `[id]`, `[[id]]`, `[[[id]]]` - check existing methods
|
||||
- **Fixed flags**: Arrays like `[2]`, `[1]` that don't change
|
||||
- **Optional positions**: Often `None`
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Implement
|
||||
|
||||
### 3a. Add RPC Method ID
|
||||
|
||||
Edit `src/notebooklm/rpc/types.py`:
|
||||
|
||||
```python
|
||||
class RPCMethod(str, Enum):
|
||||
# ... existing methods ...
|
||||
|
||||
# Add new method with descriptive name
|
||||
NEW_FEATURE = "abc123" # 6-char ID from capture
|
||||
```
|
||||
|
||||
### 3b. Implement Client Method
|
||||
|
||||
Choose the appropriate API file:
|
||||
- `_notebooks.py` - Notebook operations
|
||||
- `_sources.py` - Source operations
|
||||
- `_artifacts.py` - Artifact/generation operations
|
||||
- `_chat.py` - Chat operations
|
||||
- `_notes.py` - Note operations
|
||||
- `_research.py` - Research operations
|
||||
|
||||
Add the method:
|
||||
|
||||
```python
|
||||
async def new_feature(
|
||||
self,
|
||||
notebook_id: str,
|
||||
some_param: str,
|
||||
optional_param: Optional[str] = None,
|
||||
) -> SomeResult:
|
||||
"""Short description of what this does.
|
||||
|
||||
Args:
|
||||
notebook_id: The notebook ID.
|
||||
some_param: Description.
|
||||
optional_param: Description.
|
||||
|
||||
Returns:
|
||||
Description of return value.
|
||||
|
||||
Raises:
|
||||
RPCError: If the RPC call fails.
|
||||
"""
|
||||
# Build params array matching captured structure
|
||||
params = [
|
||||
some_param, # Position 0
|
||||
notebook_id, # Position 1
|
||||
[2], # Position 2: Fixed flag
|
||||
]
|
||||
|
||||
# Make RPC call
|
||||
result = await self._core.rpc_call(
|
||||
RPCMethod.NEW_FEATURE,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
)
|
||||
|
||||
# Parse response into dataclass
|
||||
if result is None:
|
||||
return None
|
||||
return SomeResult.from_api_response(result)
|
||||
```
|
||||
|
||||
### 3c. Add Dataclass (if needed)
|
||||
|
||||
Edit `src/notebooklm/types.py`:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class SomeResult:
|
||||
id: str
|
||||
title: str
|
||||
# ... other fields
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: list[Any]) -> "SomeResult":
|
||||
"""Parse API response array into dataclass."""
|
||||
return cls(
|
||||
id=data[0],
|
||||
title=data[1],
|
||||
# Map positions to fields
|
||||
)
|
||||
```
|
||||
|
||||
### 3d. Add CLI Command (if needed)
|
||||
|
||||
Edit appropriate CLI file in `src/notebooklm/cli/`:
|
||||
|
||||
```python
|
||||
@some_group.command("new-feature")
|
||||
@click.argument("param")
|
||||
@click.pass_context
|
||||
@async_command
|
||||
async def new_feature_cmd(ctx, param: str):
|
||||
"""Short description."""
|
||||
nb_id = get_notebook_id(ctx)
|
||||
async with get_client(ctx) as client:
|
||||
result = await client.some_api.new_feature(nb_id, param)
|
||||
console.print(f"Result: {result}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Test
|
||||
|
||||
### 4a. Unit Test (encoding)
|
||||
|
||||
`tests/unit/test_encoder.py`:
|
||||
|
||||
```python
|
||||
def test_encode_new_feature():
|
||||
params = ["value", "notebook_id", [2]]
|
||||
result = encode_rpc_request(RPCMethod.NEW_FEATURE, params)
|
||||
|
||||
assert "abc123" in result # RPC ID
|
||||
assert "value" in result
|
||||
```
|
||||
|
||||
### 4b. Integration Test (mocked response)
|
||||
|
||||
`tests/integration/test_new_feature.py`:
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_feature(mock_client):
|
||||
mock_response = ["result_id", "Result Title"]
|
||||
|
||||
with patch('notebooklm._core.ClientCore.rpc_call', new_callable=AsyncMock) as mock:
|
||||
mock.return_value = mock_response
|
||||
|
||||
result = await mock_client.some_api.new_feature("nb_id", "param")
|
||||
|
||||
assert result.id == "result_id"
|
||||
assert result.title == "Result Title"
|
||||
```
|
||||
|
||||
### 4c. E2E Test (real API)
|
||||
|
||||
`tests/e2e/test_new_feature_e2e.py`:
|
||||
|
||||
```python
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_feature_e2e(client, read_only_notebook_id):
|
||||
"""Test new feature against real API."""
|
||||
# Use read_only_notebook_id for read-only tests
|
||||
# Use temp_notebook for CRUD tests
|
||||
# Use generation_notebook_id for artifact generation tests
|
||||
# See docs/contributing/testing.md for fixture guidance
|
||||
result = await client.some_api.new_feature(read_only_notebook_id, "param")
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
# Unit + integration
|
||||
pytest tests/unit tests/integration -v
|
||||
|
||||
# E2E (requires auth)
|
||||
pytest tests/e2e/test_new_feature_e2e.py -m e2e -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Document
|
||||
|
||||
Update `docs/reference/internals/rpc-ui-reference.md`:
|
||||
|
||||
```markdown
|
||||
### NEW_FEATURE (`abc123`)
|
||||
|
||||
**Purpose:** Short description
|
||||
|
||||
**Params:**
|
||||
```python
|
||||
params = [
|
||||
some_value, # 0: Description
|
||||
notebook_id, # 1: Notebook ID
|
||||
[2], # 2: Fixed flag
|
||||
]
|
||||
```
|
||||
|
||||
**Response:** Description of response structure
|
||||
|
||||
**Source:** `_some_api.py:123`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Wrong nesting level
|
||||
|
||||
Different methods need different source ID nesting:
|
||||
|
||||
```python
|
||||
# Single: [source_id]
|
||||
# Double: [[source_id]]
|
||||
# Triple: [[[source_id]]]
|
||||
```
|
||||
|
||||
Check similar methods for the pattern.
|
||||
|
||||
### Position sensitivity
|
||||
|
||||
Params are arrays, not dicts. Position matters:
|
||||
|
||||
```python
|
||||
# WRONG - missing position 2
|
||||
params = [value, notebook_id, settings]
|
||||
|
||||
# RIGHT - explicit None for unused positions
|
||||
params = [value, notebook_id, None, settings]
|
||||
```
|
||||
|
||||
### Forgetting source_path
|
||||
|
||||
Some methods require `source_path` for routing:
|
||||
|
||||
```python
|
||||
# Without source_path - may fail
|
||||
await self._core.rpc_call(RPCMethod.X, params)
|
||||
|
||||
# With source_path - correct
|
||||
await self._core.rpc_call(
|
||||
RPCMethod.X,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
)
|
||||
```
|
||||
|
||||
### Response parsing
|
||||
|
||||
API returns nested arrays. Print raw response first:
|
||||
|
||||
```python
|
||||
result = await self._core.rpc_call(...)
|
||||
print(f"DEBUG: {result}") # See actual structure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Captured RPC ID and params structure
|
||||
- [ ] Added to `RPCMethod` enum in `rpc/types.py`
|
||||
- [ ] Implemented method in appropriate `_*.py` file
|
||||
- [ ] Added dataclass if needed in `types.py`
|
||||
- [ ] Added CLI command if needed
|
||||
- [ ] Unit test for encoding
|
||||
- [ ] Integration test with mock
|
||||
- [ ] E2E test (manual verification OK for rare operations)
|
||||
- [ ] Updated `rpc-ui-reference.md`
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [RPC Capture Guide](../reference/internals/rpc-capture.md) - Detailed capture methodology
|
||||
- [RPC UI Reference](../reference/internals/rpc-ui-reference.md) - Existing payloads
|
||||
- [Debugging Guide](debugging.md) - Troubleshooting RPC issues
|
||||
- [Testing Guide](testing.md) - Test patterns and fixtures
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
# Architecture Guide
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-07
|
||||
|
||||
Overview of the `notebooklm-py` codebase structure and design decisions.
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
src/notebooklm/
|
||||
├── __init__.py # Public exports
|
||||
├── client.py # NotebookLMClient main class
|
||||
├── auth.py # Authentication handling
|
||||
├── types.py # Dataclasses and type definitions
|
||||
├── _core.py # Core HTTP/RPC infrastructure
|
||||
├── _notebooks.py # NotebooksAPI implementation
|
||||
├── _sources.py # SourcesAPI implementation
|
||||
├── _artifacts.py # ArtifactsAPI implementation
|
||||
├── _chat.py # ChatAPI implementation
|
||||
├── _research.py # ResearchAPI implementation
|
||||
├── _notes.py # NotesAPI implementation
|
||||
├── rpc/ # RPC protocol layer
|
||||
│ ├── __init__.py
|
||||
│ ├── types.py # RPCMethod enum and constants
|
||||
│ ├── encoder.py # Request encoding
|
||||
│ └── decoder.py # Response parsing
|
||||
└── cli/ # CLI implementation
|
||||
├── __init__.py # CLI package exports
|
||||
├── helpers.py # Shared utilities
|
||||
├── options.py # Common Click options
|
||||
├── grouped.py # Grouped command utilities
|
||||
├── session.py # login, use, status, clear
|
||||
├── notebook.py # list, create, delete, rename, etc.
|
||||
├── source.py # source add, list, delete, etc.
|
||||
├── artifact.py # artifact list, get, delete, etc.
|
||||
├── generate.py # generate audio, video, etc.
|
||||
├── download.py # download audio, video, etc.
|
||||
├── download_helpers.py # Download utility functions
|
||||
├── chat.py # ask, configure, history
|
||||
├── note.py # note create, list, etc.
|
||||
├── research.py # research status, wait
|
||||
└── skill.py # skill install, status, uninstall
|
||||
```
|
||||
|
||||
## Layered Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI Layer │
|
||||
│ cli/session.py, cli/notebook.py, cli/generate.py, etc. │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ Client Layer │
|
||||
│ NotebookLMClient → NotebooksAPI, SourcesAPI, ArtifactsAPI │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ Core Layer │
|
||||
│ ClientCore → _rpc_call(), HTTP client │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ RPC Layer │
|
||||
│ encoder.py, decoder.py, types.py (RPCMethod) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layer Responsibilities
|
||||
|
||||
**CLI Layer (`cli/`)**
|
||||
- User-facing commands
|
||||
- Input validation and formatting
|
||||
- Output rendering with Rich
|
||||
- Context management (active notebook)
|
||||
|
||||
**Client Layer (`client.py`, `_*.py`)**
|
||||
- High-level Python API
|
||||
- Domain-specific methods (`notebooks.create()`, `sources.add_url()`)
|
||||
- Returns typed dataclasses (`Notebook`, `Source`, `Artifact`)
|
||||
- Wraps Core layer RPC calls
|
||||
|
||||
**Core Layer (`_core.py`)**
|
||||
- HTTP client management (`httpx.AsyncClient`)
|
||||
- Request counter (`_reqid_counter`)
|
||||
- RPC call abstraction
|
||||
- Authentication handling
|
||||
|
||||
**RPC Layer (`rpc/`)**
|
||||
- Protocol constants and enums
|
||||
- Request encoding (`encode_rpc_request()`)
|
||||
- Response parsing (`decode_response()`)
|
||||
- No HTTP/networking logic
|
||||
|
||||
## Key Files
|
||||
|
||||
### `client.py`
|
||||
|
||||
The main public interface. Users import `NotebookLMClient` from here.
|
||||
|
||||
```python
|
||||
class NotebookLMClient:
|
||||
notebooks: NotebooksAPI
|
||||
sources: SourcesAPI
|
||||
artifacts: ArtifactsAPI
|
||||
chat: ChatAPI
|
||||
research: ResearchAPI
|
||||
notes: NotesAPI
|
||||
```
|
||||
|
||||
Design decisions:
|
||||
- Namespaced APIs for organization
|
||||
- Async context manager pattern
|
||||
- `from_storage()` factory for easy initialization
|
||||
|
||||
### `_core.py`
|
||||
|
||||
Infrastructure shared by all API classes:
|
||||
|
||||
```python
|
||||
class ClientCore:
|
||||
auth: AuthTokens
|
||||
_client: httpx.AsyncClient
|
||||
_reqid_counter: int
|
||||
|
||||
async def rpc_call(method, params, ...) -> Any
|
||||
async def open() / close()
|
||||
```
|
||||
|
||||
All `_*.py` API classes receive a `ClientCore` instance.
|
||||
|
||||
### `_*.py` Files
|
||||
|
||||
Underscore prefix indicates internal modules (not for direct import by users).
|
||||
|
||||
Pattern:
|
||||
```python
|
||||
class SomeAPI:
|
||||
def __init__(self, core: ClientCore):
|
||||
self._core = core
|
||||
|
||||
async def some_method(self, ...):
|
||||
params = [...] # Build RPC params
|
||||
result = await self._core.rpc_call(RPCMethod.SOME, params)
|
||||
return SomeType.from_api_response(result)
|
||||
```
|
||||
|
||||
### `rpc/types.py`
|
||||
|
||||
**This is THE source of truth for RPC constants.**
|
||||
|
||||
```python
|
||||
class RPCMethod(str, Enum):
|
||||
LIST_NOTEBOOKS = "wXbhsf"
|
||||
CREATE_NOTEBOOK = "CCqFvf"
|
||||
# ... all method IDs
|
||||
```
|
||||
|
||||
When Google changes method IDs, only this file needs updating.
|
||||
|
||||
### `types.py`
|
||||
|
||||
Domain dataclasses:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Notebook:
|
||||
id: str
|
||||
title: str
|
||||
created_at: Optional[datetime]
|
||||
sources_count: int
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: list) -> "Notebook":
|
||||
# Parse nested list structure
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why Underscore Prefixes?
|
||||
|
||||
Files like `_notebooks.py` use underscore prefixes to:
|
||||
1. Signal they're internal implementation
|
||||
2. Keep public API clean (`from notebooklm import NotebookLMClient`)
|
||||
3. Allow refactoring without breaking imports
|
||||
|
||||
### Why Namespaced APIs?
|
||||
|
||||
Instead of `client.list_notebooks()`, we use `client.notebooks.list()`:
|
||||
- Groups related methods
|
||||
- Mirrors UI organization
|
||||
- Scales better as API grows
|
||||
- Tab completion friendly
|
||||
|
||||
### Why Async?
|
||||
|
||||
Google's API can be slow. Async allows:
|
||||
- Concurrent operations
|
||||
- Non-blocking downloads
|
||||
- Integration with async frameworks (FastAPI, etc.)
|
||||
|
||||
### Why No Service Layer?
|
||||
|
||||
Originally had `NotebookService(client)` pattern. Removed because:
|
||||
- Extra indirection without value
|
||||
- Users had to instantiate both client and services
|
||||
- Namespaced APIs (`client.notebooks`) achieve same organization
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### New RPC Method
|
||||
|
||||
1. Capture network traffic (see `docs/contributing/debugging.md`)
|
||||
2. Add to `rpc/types.py`:
|
||||
```python
|
||||
NEW_METHOD = "AbCdEf"
|
||||
```
|
||||
3. Add to appropriate `_*.py` API class
|
||||
4. Add dataclass to `types.py` if needed
|
||||
5. Add CLI command if user-facing
|
||||
|
||||
### New API Class
|
||||
|
||||
1. Create `_newfeature.py`:
|
||||
```python
|
||||
class NewFeatureAPI:
|
||||
def __init__(self, core: ClientCore):
|
||||
self._core = core
|
||||
```
|
||||
2. Add to `client.py`:
|
||||
```python
|
||||
self.newfeature = NewFeatureAPI(self._core)
|
||||
```
|
||||
3. Export types from `__init__.py`
|
||||
|
||||
### New CLI Command
|
||||
|
||||
1. Add to appropriate `cli/*.py` file
|
||||
2. Register in `cli/__init__.py` or `notebooklm_cli.py`
|
||||
3. Follow existing patterns (Click decorators, async handling)
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
See `docs/contributing/testing.md` for details.
|
||||
|
||||
- Unit tests: Mock `ClientCore.rpc_call()`
|
||||
- Integration tests: Mock HTTP responses
|
||||
- E2E tests: Real API calls (require auth)
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
# Debugging Guide
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-07
|
||||
|
||||
How to debug issues and reverse engineer new NotebookLM features.
|
||||
|
||||
## Capturing Network Traffic
|
||||
|
||||
### Chrome DevTools Setup
|
||||
|
||||
1. Open Chrome DevTools (F12)
|
||||
2. Go to **Network** tab
|
||||
3. Check "Preserve log" to keep requests across navigations
|
||||
4. Filter by: `batchexecute` (for RPC calls)
|
||||
|
||||
### Capturing an RPC Call
|
||||
|
||||
1. Clear network log
|
||||
2. Perform action in NotebookLM UI
|
||||
3. Find the `batchexecute` request
|
||||
4. Examine:
|
||||
- **URL params**: `rpcids=METHOD_ID`
|
||||
- **Request body**: `f.req=...` (URL-encoded)
|
||||
- **Response**: Starts with `)]}'\n`
|
||||
|
||||
### Decoding the Request
|
||||
|
||||
The `f.req` parameter contains URL-encoded JSON:
|
||||
|
||||
```python
|
||||
import urllib.parse
|
||||
import json
|
||||
|
||||
# Copy the f.req value from DevTools
|
||||
encoded = "..."
|
||||
|
||||
# Decode
|
||||
decoded = urllib.parse.unquote(encoded)
|
||||
print(decoded)
|
||||
|
||||
# Parse JSON
|
||||
data = json.loads(decoded)
|
||||
# Structure: [[[rpc_id, params_json, null, "generic"]]]
|
||||
|
||||
# The params are themselves JSON-encoded
|
||||
inner_params = json.loads(data[0][0][1])
|
||||
print(json.dumps(inner_params, indent=2))
|
||||
```
|
||||
|
||||
### Decoding the Response
|
||||
|
||||
Responses have an anti-XSSI prefix and chunked format:
|
||||
|
||||
```python
|
||||
response_text = """)]}'
|
||||
|
||||
123
|
||||
["wrb.fr","wXbhsf","[[\\"abc123\\",\\"My Notebook\\"]]"]
|
||||
|
||||
45
|
||||
["di",42]
|
||||
"""
|
||||
|
||||
# Step 1: Remove prefix
|
||||
lines = response_text.split('\n')
|
||||
lines = [l for l in lines if not l.startswith(')]}\'')]
|
||||
|
||||
# Step 2: Find wrb.fr chunk
|
||||
for i, line in enumerate(lines):
|
||||
if '"wrb.fr"' in line:
|
||||
chunk = json.loads(line)
|
||||
method_id = chunk[1]
|
||||
result_json = chunk[2]
|
||||
result = json.loads(result_json)
|
||||
print(f"Method: {method_id}")
|
||||
print(f"Result: {result}")
|
||||
```
|
||||
|
||||
## Common Debugging Scenarios
|
||||
|
||||
### "Session Expired" Errors
|
||||
|
||||
**Symptoms:**
|
||||
- `RPCError` mentioning unauthorized
|
||||
- Redirects to login page
|
||||
|
||||
**Debug:**
|
||||
```python
|
||||
# Check if CSRF token is present
|
||||
print(client.auth.csrf_token)
|
||||
|
||||
# Try refreshing
|
||||
await client.refresh_auth()
|
||||
print(client.auth.csrf_token) # Should be new value
|
||||
```
|
||||
|
||||
**Solution:** Re-run `notebooklm login`
|
||||
|
||||
### RPC Method Returns None
|
||||
|
||||
**Symptoms:**
|
||||
- Method completes but returns `None`
|
||||
- No error raised
|
||||
|
||||
**Debug:**
|
||||
```python
|
||||
# Add logging to see raw response
|
||||
from notebooklm.rpc import decode_response
|
||||
|
||||
# In your test code:
|
||||
raw_response = await http_client.post(...)
|
||||
print("Raw:", raw_response.text[:500])
|
||||
|
||||
result = decode_response(raw_response.text, "METHOD_ID")
|
||||
print("Parsed:", result)
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
- Rate limiting (Google returns empty result)
|
||||
- Wrong RPC method ID
|
||||
- Incorrect parameter structure
|
||||
|
||||
### Parameter Order Issues
|
||||
|
||||
RPC parameters are position-sensitive:
|
||||
|
||||
```python
|
||||
# Wrong - audio will fail
|
||||
params = [
|
||||
[2], notebook_id, [
|
||||
None, None, 1,
|
||||
source_ids,
|
||||
# ... missing positional elements
|
||||
]
|
||||
]
|
||||
|
||||
# Correct - all positions filled
|
||||
params = [
|
||||
[2], notebook_id, [
|
||||
None, None, 1,
|
||||
source_ids,
|
||||
None, None, # Required placeholders
|
||||
[None, [instructions, length, None, sources, lang, None, format]]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
**Debug:** Compare your params with captured traffic byte-by-byte.
|
||||
|
||||
### Nested List Depth
|
||||
|
||||
Source IDs have different nesting requirements:
|
||||
|
||||
```python
|
||||
# Single nesting (some methods)
|
||||
["source_id"]
|
||||
|
||||
# Double nesting
|
||||
[["source_id"]]
|
||||
|
||||
# Triple nesting (artifact generation)
|
||||
[[["source_id"]]]
|
||||
|
||||
# Quad nesting (get_source_guide)
|
||||
[[[["source_id"]]]]
|
||||
```
|
||||
|
||||
**Debug:** Capture working traffic and count brackets.
|
||||
|
||||
## RPC Tracing
|
||||
|
||||
### Using Debug Mode
|
||||
|
||||
The client has built-in RPC debugging via environment variable:
|
||||
|
||||
```bash
|
||||
# Enable debug output for all RPC calls
|
||||
NOTEBOOKLM_DEBUG_RPC=1 notebooklm <command>
|
||||
```
|
||||
|
||||
This shows:
|
||||
```
|
||||
DEBUG: Looking for RPC ID: wXbhsf
|
||||
DEBUG: Found RPC IDs in response: ['wXbhsf']
|
||||
```
|
||||
|
||||
This is especially useful when:
|
||||
- A method suddenly stops working (ID may have changed)
|
||||
- You're implementing a new method and want to verify the ID
|
||||
- Debugging parameter structure issues
|
||||
|
||||
### Adding Custom Logging
|
||||
|
||||
For more detailed debugging, add logging to `_core.py`:
|
||||
|
||||
```python
|
||||
async def rpc_call(self, method, params, ...):
|
||||
import json
|
||||
print(f"=== RPC Call: {method} ===")
|
||||
print(f"Params: {json.dumps(params, indent=2)}")
|
||||
|
||||
# ... existing code ...
|
||||
|
||||
print(f"Response status: {response.status_code}")
|
||||
print(f"Response preview: {response.text[:500]}")
|
||||
|
||||
result = decode_response(response.text, method)
|
||||
print(f"Decoded result: {result}")
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
### Comparing with Browser
|
||||
|
||||
To verify your implementation matches the browser:
|
||||
|
||||
1. Capture browser request with DevTools
|
||||
2. Save the decoded params
|
||||
3. Run your code with logging
|
||||
4. Compare params structure
|
||||
5. Check for differences in:
|
||||
- Nesting depth
|
||||
- Null placeholder positions
|
||||
- String vs integer types
|
||||
|
||||
## Testing RPC Changes
|
||||
|
||||
### Quick Test Script
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from notebooklm import NotebookLMClient
|
||||
|
||||
async def test_method():
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
# Test the method
|
||||
try:
|
||||
result = await client.notebooks.list()
|
||||
print(f"Success: {result}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
asyncio.run(test_method())
|
||||
```
|
||||
|
||||
### Mocking for Unit Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notebooks():
|
||||
mock_response = [[["nb123", "Test Notebook", None, [[1234567890]], 3]]]
|
||||
|
||||
with patch.object(ClientCore, 'rpc_call', new_callable=AsyncMock) as mock:
|
||||
mock.return_value = mock_response
|
||||
|
||||
client = NotebookLMClient(mock_auth)
|
||||
await client.__aenter__()
|
||||
|
||||
notebooks = await client.notebooks.list()
|
||||
|
||||
assert len(notebooks) == 1
|
||||
assert notebooks[0].title == "Test Notebook"
|
||||
```
|
||||
|
||||
## Reverse Engineering New Features
|
||||
|
||||
### Process
|
||||
|
||||
1. **Identify the feature** in NotebookLM UI
|
||||
2. **Capture traffic** while using it
|
||||
3. **Document the RPC ID** from URL params
|
||||
4. **Decode request payload**
|
||||
5. **Decode response structure**
|
||||
6. **Implement and test**
|
||||
|
||||
### Documentation Template
|
||||
|
||||
When discovering a new method, document:
|
||||
|
||||
```markdown
|
||||
## NEW_METHOD (RPC ID: XyZ123)
|
||||
|
||||
**Purpose:** What it does
|
||||
|
||||
**Request:**
|
||||
```python
|
||||
params = [
|
||||
# Document each position
|
||||
position_0, # What this is
|
||||
position_1, # What this is
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```python
|
||||
[
|
||||
result_data, # Structure description
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Any quirks or gotchas
|
||||
- Related methods
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're stuck:
|
||||
|
||||
1. Check existing implementations in `_*.py` files
|
||||
2. Look at test files for expected structures
|
||||
3. Compare with RPC reference in `docs/reference/internals/rpc-ui-reference.md`
|
||||
4. Follow the [Adding RPC Methods Guide](adding-rpc-methods.md) for implementation steps
|
||||
5. Open an issue with:
|
||||
- What you're trying to do
|
||||
- Captured request/response (sanitized)
|
||||
- Error messages
|
||||
|
|
@ -1,628 +0,0 @@
|
|||
# Testing Guide
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-08
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running ANY E2E tests, you must complete this setup:
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
uv pip install -e ".[dev]"
|
||||
|
||||
# Or using pip
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
### 2. Authenticate with NotebookLM
|
||||
|
||||
```bash
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
This opens a browser, logs into your Google account, and stores cookies in `~/.notebooklm/storage_state.json`.
|
||||
|
||||
Verify with:
|
||||
```bash
|
||||
notebooklm status
|
||||
```
|
||||
|
||||
### 3. Create Your Read-Only Test Notebook (REQUIRED)
|
||||
|
||||
**You MUST create a personal test notebook** for read-only tests. Tests will exit with an error if not configured.
|
||||
|
||||
1. Go to [NotebookLM](https://notebooklm.google.com)
|
||||
2. Create a new notebook (e.g., "E2E Test Notebook")
|
||||
3. Add multiple sources:
|
||||
- At least one text/paste source
|
||||
- At least one URL source
|
||||
- Optionally: PDF, YouTube video
|
||||
4. **Generate artifacts** (at least one of each type for full test coverage):
|
||||
- Audio overview (try different formats: Deep Dive, Brief)
|
||||
- Video overview
|
||||
- Quiz
|
||||
- Flashcards
|
||||
- Infographic
|
||||
- Slide deck
|
||||
- Report (Briefing Doc, Study Guide, or Blog Post)
|
||||
|
||||
*Tip: Generate multiple artifacts of the same type with different customizations to test download selection.*
|
||||
|
||||
5. Copy the notebook ID from the URL: `notebooklm.google.com/notebook/YOUR_NOTEBOOK_ID`
|
||||
6. Create your `.env` file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env and set your notebook ID
|
||||
```
|
||||
|
||||
Or set the environment variable directly:
|
||||
```bash
|
||||
export NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID="your-notebook-id-here"
|
||||
```
|
||||
|
||||
**Note:** The generation notebook is auto-created on first run and stored in `NOTEBOOKLM_HOME/generation_notebook_id` (default: `~/.notebooklm/generation_notebook_id`). You don't need to configure it manually.
|
||||
|
||||
### 4. Verify Setup
|
||||
|
||||
```bash
|
||||
# Should pass - unit tests don't need auth
|
||||
pytest tests/unit/
|
||||
|
||||
# Should pass - uses your test notebook
|
||||
pytest tests/e2e -m readonly -v
|
||||
```
|
||||
|
||||
If tests skip with "no auth stored" or fail with permission errors, your setup is incomplete.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Rate Limiting
|
||||
|
||||
NotebookLM has undocumented API rate limits. Running many tests in sequence (especially generation tests) can trigger rate limiting.
|
||||
|
||||
**How it works:**
|
||||
- Generation tests use `assert_generation_started()` helper
|
||||
- Rate-limited tests are **SKIPPED** (not failed) - you'll see `SKIPPED (Rate limited by API)`
|
||||
- The API returns `USER_DISPLAYABLE_ERROR` when rate limited, detected via `result.is_rate_limited`
|
||||
|
||||
**Strategies:**
|
||||
- Run `readonly` tests for quick validation (minimal API calls)
|
||||
- Skip `variants` to reduce generation API calls
|
||||
- Wait a few minutes between full test runs for rate limits to reset
|
||||
- Check test output for skipped tests - many skips indicate you've hit rate limits
|
||||
|
||||
---
|
||||
|
||||
## VCR Testing (Recorded HTTP Cassettes)
|
||||
|
||||
VCR.py records HTTP interactions to YAML "cassettes" for deterministic replay. This helps with:
|
||||
|
||||
- **Rate limit protection**: Record once, replay infinitely without hitting API
|
||||
- **Offline testing**: Run tests without network access
|
||||
- **Faster tests**: Replay is instant, no network latency
|
||||
- **Regression testing**: Detect API changes by comparing against recorded responses
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Recording mode**: Real HTTP requests are made and responses saved to cassettes
|
||||
2. **Replay mode** (default): Requests are intercepted and recorded responses returned
|
||||
|
||||
### Recording Cassettes
|
||||
|
||||
Cassettes are **not committed** to the repository (gitignored). Each developer records their own:
|
||||
|
||||
```bash
|
||||
# Set notebook IDs (same as e2e tests)
|
||||
export NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID="your-id"
|
||||
export NOTEBOOKLM_GENERATION_NOTEBOOK_ID="your-id"
|
||||
|
||||
# Record cassettes
|
||||
NOTEBOOKLM_VCR_RECORD=1 pytest tests/integration/test_vcr_*.py -v
|
||||
```
|
||||
|
||||
Cassettes are saved to `tests/cassettes/` (gitignored).
|
||||
|
||||
### Running VCR Tests
|
||||
|
||||
```bash
|
||||
# With cassettes - tests run using recorded responses
|
||||
pytest tests/integration/test_vcr_*.py
|
||||
|
||||
# Without cassettes - tests skip gracefully
|
||||
pytest # VCR tests skipped, other tests pass
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
All sensitive data is automatically scrubbed before recording:
|
||||
|
||||
| Data Type | Example | Scrubbed To |
|
||||
|-----------|---------|-------------|
|
||||
| Session cookies | `SID=abc123` | `SID=SCRUBBED` |
|
||||
| CSRF tokens | `SNlM0e: "token"` | `SNlM0e: "SCRUBBED_CSRF"` |
|
||||
| User emails | `user@gmail.com` | `SCRUBBED_EMAIL@example.com` |
|
||||
| User IDs | `105603816177942538178` | `SCRUBBED_USER_ID` |
|
||||
| API keys | `AIzaSy...` | `SCRUBBED_API_KEY` |
|
||||
|
||||
**Never commit cassettes** - they may contain data specific to your account.
|
||||
|
||||
### Backing Up Cassettes
|
||||
|
||||
To preserve your recorded cassettes:
|
||||
|
||||
```bash
|
||||
# Backup
|
||||
cp -r tests/cassettes ~/.notebooklm-py/vcr-cassettes/
|
||||
|
||||
# Restore
|
||||
cp ~/.notebooklm-py/vcr-cassettes/*.yaml tests/cassettes/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Run unit + integration tests (default, no auth needed)
|
||||
pytest
|
||||
|
||||
# Run E2E tests (requires setup above)
|
||||
pytest tests/e2e -m readonly # Read-only tests only (minimal API calls)
|
||||
pytest tests/e2e -m "not variants" # Skip generation parameter variants
|
||||
pytest tests/e2e # All tests (variants skipped by default)
|
||||
pytest tests/e2e --include-variants # ALL tests including parameter variants
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # RPC response builders, VCR marker
|
||||
├── vcr_config.py # VCR.py configuration and scrubbing
|
||||
├── cassettes/ # Recorded HTTP responses (gitignored)
|
||||
├── unit/ # No network, fast
|
||||
│ ├── cli/ # CLI command tests
|
||||
│ ├── test_decoder.py
|
||||
│ ├── test_encoder.py
|
||||
│ └── ...
|
||||
├── integration/ # Mocked HTTP + VCR tests
|
||||
│ ├── conftest.py # Mock auth tokens, VCR skip logic
|
||||
│ ├── test_vcr_*.py # VCR recorded tests (skip if no cassettes)
|
||||
│ └── test_*.py # pytest-httpx mocked tests
|
||||
└── e2e/ # Real API
|
||||
├── conftest.py # Auth, fixtures, cleanup
|
||||
├── test_generation.py # All artifact generation tests
|
||||
├── test_artifacts.py # Artifact CRUD/list operations
|
||||
├── test_downloads.py # Download operations
|
||||
├── test_sources.py # Source operations
|
||||
└── ...
|
||||
```
|
||||
|
||||
## E2E Fixtures
|
||||
|
||||
### Which Fixture Do I Need?
|
||||
|
||||
| I want to... | Use | Why |
|
||||
|--------------|-----|-----|
|
||||
| List/download existing artifacts | `read_only_notebook_id` | Your notebook with pre-made content |
|
||||
| Add/delete sources or notes | `temp_notebook` | Isolated, auto-cleanup, has content |
|
||||
| Generate audio/video/quiz | `generation_notebook_id` | Auto-created, has content, CI-aware cleanup |
|
||||
|
||||
### `read_only_notebook_id` (Your Test Notebook - REQUIRED)
|
||||
|
||||
Returns `NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID` env var. **Tests will exit with an error if not set.**
|
||||
|
||||
Your notebook must have:
|
||||
- Multiple sources (text, URL, etc.)
|
||||
- Pre-generated artifacts (audio, quiz, etc.)
|
||||
|
||||
```python
|
||||
@pytest.mark.readonly
|
||||
async def test_list_artifacts(self, client, read_only_notebook_id):
|
||||
artifacts = await client.artifacts.list(read_only_notebook_id)
|
||||
assert isinstance(artifacts, list)
|
||||
```
|
||||
|
||||
### `temp_notebook`
|
||||
|
||||
Fresh notebook per test. Automatically deleted after test completes (even on failure).
|
||||
|
||||
```python
|
||||
async def test_add_source(self, client, temp_notebook):
|
||||
source = await client.sources.add_url(temp_notebook.id, "https://example.com")
|
||||
assert source.id is not None
|
||||
```
|
||||
|
||||
### `generation_notebook_id`
|
||||
|
||||
Notebook for generation tests. Auto-created on first run if not configured via env var.
|
||||
|
||||
```python
|
||||
async def test_generate_quiz(self, client, generation_notebook_id):
|
||||
result = await client.artifacts.generate_quiz(generation_notebook_id)
|
||||
assert result is not None
|
||||
assert result.task_id # Generation returns immediately with task_id
|
||||
```
|
||||
|
||||
**Lifecycle:**
|
||||
- Auto-created if `NOTEBOOKLM_GENERATION_NOTEBOOK_ID` not set
|
||||
- Artifacts/notes cleaned BEFORE tests (clean starting state)
|
||||
- **In CI (CI=true):** Notebook deleted after tests to avoid orphans
|
||||
- **Locally:** Notebook persists, ID stored in `NOTEBOOKLM_HOME/generation_notebook_id`
|
||||
|
||||
## Test Markers
|
||||
|
||||
All markers defined in `pyproject.toml`:
|
||||
|
||||
| Marker | Purpose |
|
||||
|--------|---------|
|
||||
| `readonly` | Read-only tests against user's test notebook |
|
||||
| `variants` | Generation parameter variants (skip to reduce API calls) |
|
||||
|
||||
### Understanding Generation Tests
|
||||
|
||||
Generation tests (audio, video, quiz, etc.) call the API and receive a `task_id` immediately - they do **not** wait for the artifact to complete. This means:
|
||||
|
||||
- Tests are fast (single API call each)
|
||||
- The main concern is **rate limiting**, not execution time
|
||||
- Running many generation tests in sequence triggers rate limits
|
||||
|
||||
### Variant Testing
|
||||
|
||||
Each artifact type has multiple parameter options (format, style, difficulty, etc.). To balance coverage and API quota:
|
||||
|
||||
- **Default test:** Tests with default parameters (always runs)
|
||||
- **Variant tests:** Test other parameter combinations (skipped by default)
|
||||
|
||||
**Variants are skipped by default** to reduce API calls. Use `--include-variants` to run them:
|
||||
|
||||
```bash
|
||||
pytest tests/e2e # Skips variants
|
||||
pytest tests/e2e --include-variants # Includes variants
|
||||
```
|
||||
|
||||
```python
|
||||
# Runs by default - tests that generation works
|
||||
async def test_generate_audio_default(self, client, generation_notebook_id):
|
||||
result = await client.artifacts.generate_audio(generation_notebook_id)
|
||||
assert result.task_id, "Expected non-empty task_id"
|
||||
assert result.status in ("pending", "in_progress")
|
||||
assert result.error is None
|
||||
|
||||
# Skipped by default - tests parameter encoding
|
||||
@pytest.mark.variants
|
||||
async def test_generate_audio_brief(self, client, generation_notebook_id):
|
||||
result = await client.artifacts.generate_audio(
|
||||
generation_notebook_id,
|
||||
audio_format=AudioFormat.BRIEF,
|
||||
)
|
||||
assert result.task_id, "Expected non-empty task_id"
|
||||
```
|
||||
|
||||
## E2E Fixtures Reference
|
||||
|
||||
From `tests/e2e/conftest.py`:
|
||||
|
||||
### Authentication
|
||||
|
||||
```python
|
||||
@pytest.fixture(scope="session")
|
||||
def auth_cookies() -> dict[str, str]:
|
||||
"""Load cookies from ~/.notebooklm/storage_state.json"""
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def auth_tokens(auth_cookies) -> AuthTokens:
|
||||
"""Fetch CSRF + session ID from NotebookLM homepage"""
|
||||
|
||||
@pytest.fixture
|
||||
async def client(auth_tokens) -> NotebookLMClient:
|
||||
"""Fresh client per test"""
|
||||
```
|
||||
|
||||
### Notebooks
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def read_only_notebook_id() -> str:
|
||||
"""Returns NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID (required)"""
|
||||
|
||||
@pytest.fixture
|
||||
async def temp_notebook(client) -> Notebook:
|
||||
"""Create notebook with content, auto-delete after test"""
|
||||
|
||||
@pytest.fixture
|
||||
async def generation_notebook_id(client) -> str:
|
||||
"""Auto-created notebook for generation tests (deleted in CI)"""
|
||||
```
|
||||
|
||||
### Decorators
|
||||
|
||||
```python
|
||||
@requires_auth # Skip test if no auth stored
|
||||
```
|
||||
|
||||
### Helpers
|
||||
|
||||
```python
|
||||
from .conftest import assert_generation_started
|
||||
|
||||
# Use in generation tests - skips on rate limiting instead of failing
|
||||
result = await client.artifacts.generate_audio(notebook_id)
|
||||
assert_generation_started(result, "Audio") # Skips if rate limited
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set via `.env` file (recommended) or shell export:
|
||||
|
||||
```bash
|
||||
# Required: Your read-only test notebook with sources and artifacts
|
||||
NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID=your-notebook-id-here
|
||||
|
||||
# Optional: Explicit generation notebook ID (auto-created if not set)
|
||||
# NOTEBOOKLM_GENERATION_NOTEBOOK_ID=your-generation-notebook-id
|
||||
```
|
||||
|
||||
See `.env.example` for the template.
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
Need network?
|
||||
├── No → tests/unit/
|
||||
├── Mocked → tests/integration/
|
||||
└── Real API → tests/e2e/
|
||||
└── What notebook?
|
||||
├── Read-only → read_only_notebook_id + @pytest.mark.readonly
|
||||
├── CRUD → temp_notebook
|
||||
└── Generation → generation_notebook_id
|
||||
└── Parameter variant? → add @pytest.mark.variants
|
||||
```
|
||||
|
||||
### Example: New Generation Test
|
||||
|
||||
Add generation tests to `tests/e2e/test_generation.py`:
|
||||
|
||||
```python
|
||||
from .conftest import requires_auth, assert_generation_started
|
||||
|
||||
@requires_auth
|
||||
class TestNewArtifact:
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_new_artifact_default(self, client, generation_notebook_id):
|
||||
result = await client.artifacts.generate_new(generation_notebook_id)
|
||||
# Use helper - skips test if rate limited, fails on other errors
|
||||
assert_generation_started(result, "NewArtifact")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.variants
|
||||
async def test_generate_new_artifact_with_options(self, client, generation_notebook_id):
|
||||
result = await client.artifacts.generate_new(
|
||||
generation_notebook_id,
|
||||
option=SomeOption.VALUE,
|
||||
)
|
||||
assert_generation_started(result, "NewArtifact")
|
||||
```
|
||||
|
||||
Note: Generation tests only need `client` and `generation_notebook_id`. Cleanup is automatic.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
NotebookLM has undocumented rate limits. Running many generation tests in sequence can trigger rate limiting:
|
||||
|
||||
- Rate-limited tests are **skipped** (not failed) via `assert_generation_started()` helper
|
||||
- You'll see `SKIPPED (Rate limited by API)` in test output
|
||||
- Many skipped tests = you've hit rate limits, wait and retry
|
||||
|
||||
**Strategies:**
|
||||
- **Run `readonly` tests first:** `pytest tests/e2e -m readonly` (minimal API calls)
|
||||
- **Skip variants:** `pytest tests/e2e -m "not variants"` (fewer generation calls)
|
||||
- **Wait between runs:** Rate limits reset after a few minutes
|
||||
|
||||
---
|
||||
|
||||
## Rate Limit Optimization
|
||||
|
||||
### Understanding Rate Limits
|
||||
|
||||
NotebookLM appears to have multiple rate limit mechanisms:
|
||||
|
||||
| Limit Type | Scope | Notes |
|
||||
|------------|-------|-------|
|
||||
| **Per-type daily quota** | Per artifact type | e.g., limited audio generations per day |
|
||||
| **Burst rate limit** | Per time window | e.g., generations per 5 minutes |
|
||||
| **Account-wide** | All operations | Undocumented overall limits |
|
||||
|
||||
**Key insight:** Rate limits primarily affect **artifact generation**, not notebook/source creation.
|
||||
|
||||
### Current Optimizations
|
||||
|
||||
The test suite is designed to minimize rate limit issues:
|
||||
|
||||
1. **Spread across artifact types** - Default tests cover 9 different artifact types (audio, video, quiz, flashcards, infographic, slide_deck, data_table, mind_map, study_guide). This distributes load across per-type quotas.
|
||||
|
||||
2. **Variants skipped by default** - Only ~10 generation tests run by default. The 20+ variant tests (testing parameter combinations) require `--include-variants`.
|
||||
|
||||
3. **Graceful rate limit handling** - `assert_generation_started()` detects rate limiting and skips tests instead of failing.
|
||||
|
||||
4. **Combined mutation tests** - `test_artifacts.py` combines poll/rename/wait operations into one test, and spreads across artifact types (flashcards for mutations, quiz for delete).
|
||||
|
||||
### Default Generation Tests (10 calls)
|
||||
|
||||
```
|
||||
audio_default, audio_brief → 2 audio calls
|
||||
video_default → 1 video call
|
||||
quiz_default → 1 quiz call
|
||||
flashcards_default → 1 flashcards call
|
||||
infographic_default → 1 infographic call
|
||||
slide_deck_default → 1 slide_deck call
|
||||
data_table_default → 1 data_table call
|
||||
mind_map → 1 mind_map call (sync)
|
||||
study_guide → 1 study_guide call
|
||||
```
|
||||
|
||||
This spread is optimal for per-type rate limits.
|
||||
|
||||
### Generation Notebook Design
|
||||
|
||||
The `generation_notebook_id` fixture uses a hybrid approach to balance:
|
||||
- **Local development**: Notebook persists for post-test verification
|
||||
- **CI environments**: Auto-cleanup to avoid orphaned notebooks
|
||||
|
||||
**How it works:**
|
||||
|
||||
| Environment | Behavior |
|
||||
|-------------|----------|
|
||||
| `NOTEBOOKLM_GENERATION_NOTEBOOK_ID` set | Uses provided ID, never deleted |
|
||||
| Local (no `CI` env var) | Auto-creates once, stores ID, persists across runs |
|
||||
| CI (`CI=true`) | Auto-creates, deletes after tests complete |
|
||||
|
||||
Artifacts and notes are always cleaned BEFORE tests to ensure a clean starting state.
|
||||
|
||||
### Future Considerations
|
||||
|
||||
If rate limiting becomes more severe, consider:
|
||||
|
||||
1. **Add delays between generation tests:**
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
async def rate_limit_delay():
|
||||
yield
|
||||
await asyncio.sleep(2) # Delay after each test
|
||||
```
|
||||
|
||||
2. **CI scheduling** - Spread test runs over time rather than running all at once.
|
||||
|
||||
3. **Tiered test runs** - Run readonly tests in CI, full suite nightly.
|
||||
|
||||
## CI/CD Setup
|
||||
|
||||
### GitHub Actions Workflows
|
||||
|
||||
The project has two CI workflows:
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `test.yml` | Push/PR to main | Unit tests, linting, type checking |
|
||||
| `nightly.yml` | Daily at 6 AM UTC | E2E tests with real API |
|
||||
|
||||
### Automatic CI (test.yml)
|
||||
|
||||
Runs automatically on every push and PR:
|
||||
|
||||
1. **Quality Job** - Ruff linting + mypy type checking (runs once)
|
||||
2. **Test Job** - Unit/integration tests on Ubuntu and macOS across Python 3.10-3.14
|
||||
|
||||
No setup required - this works out of the box.
|
||||
|
||||
### Nightly E2E Tests (nightly.yml)
|
||||
|
||||
Runs E2E tests daily against the real NotebookLM API. Requires repository secrets.
|
||||
|
||||
#### Step 1: Get Your Storage State
|
||||
|
||||
```bash
|
||||
# Make sure you're logged in
|
||||
notebooklm login
|
||||
|
||||
# Copy the storage state content
|
||||
cat ~/.notebooklm/storage_state.json
|
||||
```
|
||||
|
||||
#### Step 2: Add the Secrets to GitHub
|
||||
|
||||
1. Go to your repository on GitHub
|
||||
2. Click **Settings** → **Secrets and variables** → **Actions**
|
||||
3. Add **two** repository secrets:
|
||||
|
||||
**Secret 1: Auth JSON**
|
||||
- **Name:** `NOTEBOOKLM_AUTH_JSON`
|
||||
- **Value:** Paste the entire JSON content from step 1
|
||||
|
||||
**Secret 2: Read-Only Notebook ID**
|
||||
- **Name:** `NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID`
|
||||
- **Value:** Your test notebook ID (from URL: `notebooklm.google.com/notebook/YOUR_ID`)
|
||||
|
||||
#### Step 3: Test the Workflow
|
||||
|
||||
```bash
|
||||
# Trigger manually to verify it works
|
||||
gh workflow run nightly.yml
|
||||
|
||||
# Check the run status
|
||||
gh run list --workflow=nightly.yml
|
||||
```
|
||||
|
||||
### Maintaining CI Secrets
|
||||
|
||||
| Task | Frequency | Action |
|
||||
|------|-----------|--------|
|
||||
| Refresh credentials | Every 1-2 weeks | Run `notebooklm login`, update secret |
|
||||
| Check nightly results | Daily | Review Actions tab for failures |
|
||||
| Update secret after expiry | When E2E tests fail with auth errors | Repeat steps 1-2 |
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- Use a **dedicated test Google account** (not personal)
|
||||
- The secret is encrypted and never exposed in logs
|
||||
- Only the main repository can access secrets (forks cannot)
|
||||
- Session cookies expire naturally, requiring periodic refresh
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tests skip with "no auth stored"
|
||||
|
||||
Run `notebooklm login` and complete browser authentication.
|
||||
|
||||
### Tests fail with permission errors
|
||||
|
||||
Your `NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID` may be invalid or you don't own it. Verify:
|
||||
```bash
|
||||
echo $NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID
|
||||
notebooklm list # Should show your notebooks
|
||||
```
|
||||
|
||||
### Tests hang or timeout
|
||||
|
||||
- **CSRF token expired:** Run `notebooklm login` again.
|
||||
- **Download tests:** May timeout if browser auth expired. Clear profile and re-login:
|
||||
```bash
|
||||
rm -rf ~/.notebooklm/browser_profile && notebooklm login
|
||||
```
|
||||
|
||||
### Rate limiting / Many tests skipped
|
||||
|
||||
NotebookLM has undocumented rate limits. If you see many tests `SKIPPED (Rate limited by API)`:
|
||||
|
||||
1. **This is expected behavior** - tests skip instead of fail when rate limited
|
||||
2. **Wait and retry:** Rate limits reset after a few minutes
|
||||
3. **Reduce API calls:** Use `pytest tests/e2e -m "not variants"` (fewer generation calls)
|
||||
4. **Run single tests:** `pytest tests/e2e/test_generation.py::TestAudioGeneration::test_generate_audio_default`
|
||||
|
||||
Note: The `assert_generation_started()` helper detects rate limiting via `USER_DISPLAYABLE_ERROR` from the API and skips the test gracefully.
|
||||
|
||||
### "CSRF token invalid" or 403 errors
|
||||
|
||||
Your session expired. Re-authenticate:
|
||||
```bash
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
### "NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID not set" error
|
||||
|
||||
This is expected - E2E tests require your own notebook. Follow Prerequisites step 3 to create one.
|
||||
|
||||
### Too many artifacts accumulating
|
||||
|
||||
- `temp_notebook`: Automatically deleted after each test
|
||||
- `generation_notebook_id` in CI: Notebook deleted after tests
|
||||
- `generation_notebook_id` locally: Artifacts cleaned before each run, notebook persists (ID stored in `NOTEBOOKLM_HOME/generation_notebook_id`)
|
||||
- If tests are interrupted, orphaned notebooks may remain - clean up manually in the NotebookLM UI
|
||||
276
docs/development.md
Normal file
276
docs/development.md
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# Contributing Guide
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-13
|
||||
|
||||
This guide covers everything you need to contribute to `notebooklm-py`: architecture overview, testing, and releasing.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Package Structure
|
||||
|
||||
```
|
||||
src/notebooklm/
|
||||
├── __init__.py # Public exports
|
||||
├── client.py # NotebookLMClient main class
|
||||
├── auth.py # Authentication handling
|
||||
├── types.py # Dataclasses and type definitions
|
||||
├── _core.py # Core HTTP/RPC infrastructure
|
||||
├── _notebooks.py # NotebooksAPI implementation
|
||||
├── _sources.py # SourcesAPI implementation
|
||||
├── _artifacts.py # ArtifactsAPI implementation
|
||||
├── _chat.py # ChatAPI implementation
|
||||
├── _research.py # ResearchAPI implementation
|
||||
├── _notes.py # NotesAPI implementation
|
||||
├── rpc/ # RPC protocol layer
|
||||
│ ├── __init__.py
|
||||
│ ├── types.py # RPCMethod enum and constants
|
||||
│ ├── encoder.py # Request encoding
|
||||
│ └── decoder.py # Response parsing
|
||||
└── cli/ # CLI implementation
|
||||
├── __init__.py # CLI package exports
|
||||
├── helpers.py # Shared utilities
|
||||
├── session.py # login, use, status, clear
|
||||
├── notebook.py # list, create, delete, rename
|
||||
├── source.py # source add, list, delete
|
||||
├── artifact.py # artifact list, get, delete
|
||||
├── generate.py # generate audio, video, etc.
|
||||
├── download.py # download audio, video, etc.
|
||||
├── chat.py # ask, configure, history
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI Layer │
|
||||
│ cli/session.py, cli/notebook.py, cli/generate.py, etc. │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ Client Layer │
|
||||
│ NotebookLMClient → NotebooksAPI, SourcesAPI, ArtifactsAPI │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ Core Layer │
|
||||
│ ClientCore → _rpc_call(), HTTP client │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ RPC Layer │
|
||||
│ encoder.py, decoder.py, types.py (RPCMethod) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layer Responsibilities
|
||||
|
||||
| Layer | Files | Responsibility |
|
||||
|-------|-------|----------------|
|
||||
| **CLI** | `cli/*.py` | User commands, input validation, Rich output |
|
||||
| **Client** | `client.py`, `_*.py` | High-level Python API, returns typed dataclasses |
|
||||
| **Core** | `_core.py` | HTTP client, request counter, RPC abstraction |
|
||||
| **RPC** | `rpc/*.py` | Protocol encoding/decoding, method IDs |
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
**Why underscore prefixes?** Files like `_notebooks.py` are internal implementation. Public API stays clean (`from notebooklm import NotebookLMClient`).
|
||||
|
||||
**Why namespaced APIs?** `client.notebooks.list()` instead of `client.list_notebooks()` - better organization, scales well, tab-completion friendly.
|
||||
|
||||
**Why async?** Google's API can be slow. Async enables concurrent operations and non-blocking downloads.
|
||||
|
||||
### Adding New Features
|
||||
|
||||
**New RPC Method:**
|
||||
1. Capture traffic (see [RPC Development Guide](rpc-development.md))
|
||||
2. Add to `rpc/types.py`: `NEW_METHOD = "AbCdEf"`
|
||||
3. Implement in appropriate `_*.py` API class
|
||||
4. Add dataclass to `types.py` if needed
|
||||
5. Add CLI command if user-facing
|
||||
|
||||
**New API Class:**
|
||||
1. Create `_newfeature.py` with `NewFeatureAPI` class
|
||||
2. Add to `client.py`: `self.newfeature = NewFeatureAPI(self._core)`
|
||||
3. Export types from `__init__.py`
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
uv pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
2. **Authenticate:**
|
||||
```bash
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
3. **Create read-only test notebook** (required for E2E tests):
|
||||
- Create notebook at [NotebookLM](https://notebooklm.google.com)
|
||||
- Add multiple sources (text, URL, etc.)
|
||||
- Generate artifacts (audio, quiz, etc.)
|
||||
- Set env var: `export NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID="your-id"`
|
||||
|
||||
### Quick Reference
|
||||
|
||||
```bash
|
||||
# Unit + integration tests (no auth needed)
|
||||
pytest
|
||||
|
||||
# E2E tests (requires auth + test notebook)
|
||||
pytest tests/e2e -m readonly # Read-only tests only
|
||||
pytest tests/e2e -m "not variants" # Skip parameter variants
|
||||
pytest tests/e2e --include-variants # All tests including variants
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # No network, fast, mock everything
|
||||
├── integration/ # Mocked HTTP responses + VCR cassettes
|
||||
└── e2e/ # Real API calls (requires auth)
|
||||
```
|
||||
|
||||
### E2E Fixtures
|
||||
|
||||
| Fixture | Use Case |
|
||||
|---------|----------|
|
||||
| `read_only_notebook_id` | List/download existing artifacts |
|
||||
| `temp_notebook` | Add/delete sources (auto-cleanup) |
|
||||
| `generation_notebook_id` | Generate artifacts (CI-aware cleanup) |
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
NotebookLM has undocumented rate limits. Generation tests may be skipped when rate limited:
|
||||
- Use `pytest tests/e2e -m readonly` for quick validation
|
||||
- Wait a few minutes between full test runs
|
||||
- `SKIPPED (Rate limited by API)` is expected behavior, not failure
|
||||
|
||||
### VCR Testing (Recorded HTTP)
|
||||
|
||||
Record HTTP interactions for offline/deterministic replay:
|
||||
|
||||
```bash
|
||||
# Record cassettes (not committed to repo)
|
||||
NOTEBOOKLM_VCR_RECORD=1 pytest tests/integration/test_vcr_*.py -v
|
||||
|
||||
# Run with recorded responses
|
||||
pytest tests/integration/test_vcr_*.py
|
||||
```
|
||||
|
||||
Sensitive data (cookies, tokens, emails) is automatically scrubbed.
|
||||
|
||||
### Writing New Tests
|
||||
|
||||
```
|
||||
Need network?
|
||||
├── No → tests/unit/
|
||||
├── Mocked → tests/integration/
|
||||
└── Real API → tests/e2e/
|
||||
└── What notebook?
|
||||
├── Read-only → read_only_notebook_id + @pytest.mark.readonly
|
||||
├── CRUD → temp_notebook
|
||||
└── Generation → generation_notebook_id
|
||||
└── Parameter variant? → add @pytest.mark.variants
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Releasing
|
||||
|
||||
### Pre-release Checklist
|
||||
|
||||
- [ ] All tests pass: `pytest`
|
||||
- [ ] E2E readonly tests pass: `pytest tests/e2e -m readonly`
|
||||
- [ ] No uncommitted changes: `git status`
|
||||
- [ ] On `main` branch with latest changes
|
||||
- [ ] Version updated in `src/notebooklm/__init__.py`
|
||||
- [ ] CHANGELOG.md updated
|
||||
|
||||
### Release Steps
|
||||
|
||||
1. **Update version** in `src/notebooklm/__init__.py`:
|
||||
```python
|
||||
__version__ = "X.Y.Z"
|
||||
```
|
||||
|
||||
2. **Update CHANGELOG.md** with release notes
|
||||
|
||||
3. **Commit and push:**
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: release vX.Y.Z"
|
||||
git push
|
||||
```
|
||||
|
||||
4. **Test on TestPyPI:**
|
||||
```bash
|
||||
python -m build
|
||||
twine upload --repository testpypi dist/*
|
||||
pip install --index-url https://test.pypi.org/simple/ \
|
||||
--extra-index-url https://pypi.org/simple notebooklm-py
|
||||
notebooklm --version
|
||||
```
|
||||
|
||||
5. **Create release tag:**
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
This triggers GitHub Actions to publish to PyPI automatically.
|
||||
|
||||
### Versioning Policy
|
||||
|
||||
| Change Type | Bump | Example |
|
||||
|-------------|------|---------|
|
||||
| RPC method ID fixes | PATCH | 0.1.0 → 0.1.1 |
|
||||
| Bug fixes | PATCH | 0.1.1 → 0.1.2 |
|
||||
| New features (backward compatible) | MINOR | 0.1.2 → 0.2.0 |
|
||||
| Breaking API changes | MAJOR | 0.2.0 → 1.0.0 |
|
||||
|
||||
See [stability.md](stability.md) for full versioning policy.
|
||||
|
||||
---
|
||||
|
||||
## CI/CD
|
||||
|
||||
### Workflows
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `test.yml` | Push/PR | Unit tests, linting, type checking |
|
||||
| `nightly.yml` | Daily 6 AM UTC | E2E tests with real API |
|
||||
| `publish.yml` | Tag push | Publish to PyPI |
|
||||
|
||||
### Setting Up Nightly E2E Tests
|
||||
|
||||
1. Get storage state: `cat ~/.notebooklm/storage_state.json`
|
||||
2. Add GitHub secrets:
|
||||
- `NOTEBOOKLM_AUTH_JSON`: Storage state JSON
|
||||
- `NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID`: Your test notebook ID
|
||||
|
||||
### Maintaining Secrets
|
||||
|
||||
| Task | Frequency |
|
||||
|------|-----------|
|
||||
| Refresh credentials | Every 1-2 weeks |
|
||||
| Check nightly results | Daily |
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Check existing implementations in `_*.py` files
|
||||
- Look at test files for expected structures
|
||||
- See [RPC Development Guide](rpc-development.md) for protocol details
|
||||
- Open an issue with captured request/response (sanitized)
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
# Getting Started
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-09
|
||||
|
||||
This guide walks you through installing and using `notebooklm-py` for the first time.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10 or higher
|
||||
- A Google account with access to [NotebookLM](https://notebooklm.google.com)
|
||||
|
||||
## Installation
|
||||
|
||||
### Basic Installation (CLI + Python API)
|
||||
|
||||
```bash
|
||||
pip install notebooklm-py
|
||||
```
|
||||
|
||||
### With Browser Login Support (Recommended for first-time setup)
|
||||
|
||||
```bash
|
||||
pip install "notebooklm-py[browser]"
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
The browser extra installs Playwright, which is **only required for `notebooklm login`**. All other commands and the Python API use standard HTTP requests via `httpx`.
|
||||
|
||||
> **Headless/Remote Machines:** If you've already authenticated on another machine, copy `~/.notebooklm/storage_state.json` and use the basic installation without Playwright. See [Configuration - Headless Servers](configuration.md#headless-servers--containers).
|
||||
|
||||
### Development Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/teng-lin/notebooklm-py.git
|
||||
cd notebooklm-py
|
||||
pip install -e ".[all]"
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
> **See also:** [Configuration](configuration.md) for custom storage paths and environment settings.
|
||||
|
||||
## Authentication
|
||||
|
||||
NotebookLM uses Google's internal APIs, which require valid session cookies. The CLI provides a browser-based login flow:
|
||||
|
||||
```bash
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Open a Chromium browser window
|
||||
2. Navigate to NotebookLM
|
||||
3. Wait for you to log in with your Google account
|
||||
4. Save your session to `~/.notebooklm/storage_state.json`
|
||||
|
||||
**Note:** The browser uses a persistent profile at `~/.notebooklm/browser_profile/` to avoid Google's bot detection. This makes it appear as a regular browser installation.
|
||||
|
||||
**Custom Locations:** Set `NOTEBOOKLM_HOME` to store all configuration files in a different directory:
|
||||
```bash
|
||||
export NOTEBOOKLM_HOME=/custom/path
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
See [Configuration](configuration.md) for multiple accounts, CI/CD setup, and more options.
|
||||
|
||||
### Verifying Authentication
|
||||
|
||||
After login, verify your session works:
|
||||
|
||||
```bash
|
||||
notebooklm list
|
||||
```
|
||||
|
||||
This should show your existing notebooks (or an empty list if you're new to NotebookLM).
|
||||
|
||||
## Your First Workflow
|
||||
|
||||
Let's create a notebook, add a source, and generate a podcast.
|
||||
|
||||
### Step 1: Create a Notebook
|
||||
|
||||
```bash
|
||||
notebooklm create "My First Notebook"
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Created notebook: abc123def456
|
||||
Title: My First Notebook
|
||||
```
|
||||
|
||||
### Step 2: Set the Active Notebook
|
||||
|
||||
```bash
|
||||
notebooklm use abc123def456
|
||||
```
|
||||
|
||||
You can use partial IDs - `notebooklm use abc` will match `abc123def456`.
|
||||
|
||||
### Step 3: Add a Source
|
||||
|
||||
```bash
|
||||
notebooklm source add "https://en.wikipedia.org/wiki/Artificial_intelligence"
|
||||
```
|
||||
|
||||
The CLI auto-detects URLs, YouTube links, and local files:
|
||||
- URLs: `source add "https://example.com/article"`
|
||||
- YouTube: `source add "https://youtube.com/watch?v=..."`
|
||||
- Files: `source add "./document.pdf"`
|
||||
|
||||
### Step 4: Chat with Your Sources
|
||||
|
||||
```bash
|
||||
notebooklm ask "What are the key themes in this article?"
|
||||
```
|
||||
|
||||
### Step 5: Generate a Podcast
|
||||
|
||||
```bash
|
||||
notebooklm generate audio "Focus on the history and future predictions" --wait
|
||||
```
|
||||
|
||||
This generates a podcast and waits for completion (takes 2-5 minutes).
|
||||
|
||||
### Step 6: Download the Result
|
||||
|
||||
```bash
|
||||
notebooklm download audio ./my-podcast.mp3
|
||||
```
|
||||
|
||||
## Using the Python API
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from notebooklm import NotebookLMClient
|
||||
|
||||
async def main():
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
# List notebooks
|
||||
notebooks = await client.notebooks.list()
|
||||
print(f"Found {len(notebooks)} notebooks")
|
||||
|
||||
# Create a notebook
|
||||
nb = await client.notebooks.create("API Test")
|
||||
print(f"Created: {nb.id}")
|
||||
|
||||
# Add a source
|
||||
await client.sources.add_url(nb.id, "https://example.com")
|
||||
|
||||
# Ask a question
|
||||
result = await client.chat.ask(nb.id, "Summarize this content")
|
||||
print(result.answer)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Claude Code Integration
|
||||
|
||||
If you use [Claude Code](https://claude.ai/code), you can install a skill for natural language automation:
|
||||
|
||||
```bash
|
||||
notebooklm skill install
|
||||
```
|
||||
|
||||
After installation, Claude recognizes NotebookLM commands via:
|
||||
- Explicit: `/notebooklm list`, `/notebooklm generate audio`
|
||||
- Natural language: "Create a podcast about quantum computing", "Summarize these URLs"
|
||||
|
||||
Check installation status:
|
||||
```bash
|
||||
notebooklm skill status
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [CLI Reference](cli-reference.md) - Complete command documentation
|
||||
- [Python API](python-api.md) - Full API reference
|
||||
- [Configuration](configuration.md) - Storage and environment settings
|
||||
- [Troubleshooting](troubleshooting.md) - Common issues and solutions
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
# RPC Capture Guide
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-07
|
||||
**Purpose:** How to capture and reverse-engineer NotebookLM RPC calls
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
NotebookLM uses Google's `batchexecute` RPC protocol. This guide explains how to capture and decode RPC calls for different use cases.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
| Term | Description |
|
||||
|------|-------------|
|
||||
| **batchexecute** | Google's internal RPC protocol endpoint |
|
||||
| **RPC ID** | 6-character identifier (e.g., `wXbhsf`, `s0tc2d`) |
|
||||
| **f.req** | URL-encoded JSON payload |
|
||||
| **at** | CSRF token (SNlM0e value) |
|
||||
| **Anti-XSSI** | `)]}'` prefix on responses |
|
||||
|
||||
### Protocol Flow
|
||||
|
||||
```
|
||||
1. Build request: [[[rpc_id, json_params, null, "generic"]]]
|
||||
2. Encode to f.req parameter
|
||||
3. POST to /_/LabsTailwindUi/data/batchexecute
|
||||
4. Strip )]}' prefix from response
|
||||
5. Parse chunked JSON, extract result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Choose Your Approach
|
||||
|
||||
| If you are... | Use this approach |
|
||||
|---------------|-------------------|
|
||||
| **Reporting a bug** or doing quick investigation | [Manual Capture](#manual-capture-for-bug-reports) |
|
||||
| **Building library features** or systematic capture | [Playwright Automation](#playwright-automation-for-developers) |
|
||||
| **An LLM agent** discovering new methods | [LLM Discovery Workflow](#llm-discovery-workflow) |
|
||||
|
||||
---
|
||||
|
||||
## Manual Capture (For Bug Reports)
|
||||
|
||||
**Best for:** Quick investigation, bug reports, one-off captures
|
||||
|
||||
### Step 1: Setup DevTools
|
||||
|
||||
1. Open Chrome → Navigate to `https://notebooklm.google.com/`
|
||||
2. Open DevTools (`F12` or `Cmd+Option+I`)
|
||||
3. Go to **Network** tab
|
||||
4. Configure:
|
||||
- [x] **Preserve log** (prevents clearing on navigation)
|
||||
- [x] **Disable cache** (ensures fresh requests)
|
||||
5. Filter by: `batchexecute`
|
||||
|
||||
### Step 2: Capture One Action
|
||||
|
||||
**CRITICAL**: Perform ONE action at a time to isolate the exact RPC call.
|
||||
|
||||
```
|
||||
1. Clear network log immediately before action
|
||||
2. Perform the UI action (e.g., rename notebook)
|
||||
3. Wait for request to complete (check status code 200)
|
||||
4. DO NOT perform any other actions
|
||||
```
|
||||
|
||||
### Step 3: Document the Request
|
||||
|
||||
From the batchexecute request:
|
||||
|
||||
**Headers Tab:**
|
||||
- `rpcids` query parameter = RPC method ID
|
||||
|
||||
**Payload Tab:**
|
||||
- `f.req` = URL-encoded payload (decode this)
|
||||
- `at` = CSRF token
|
||||
|
||||
**Response Tab:**
|
||||
- Starts with `)]}'\n` (anti-XSSI prefix)
|
||||
- Followed by chunked JSON
|
||||
|
||||
### Step 4: Decode Payload
|
||||
|
||||
**Manual decode in browser console:**
|
||||
```javascript
|
||||
const encoded = "..."; // Paste f.req value
|
||||
const decoded = decodeURIComponent(encoded);
|
||||
const outer = JSON.parse(decoded);
|
||||
console.log("RPC ID:", outer[0][0][0]);
|
||||
console.log("Params:", JSON.parse(outer[0][0][1]));
|
||||
```
|
||||
|
||||
**Include in bug report:**
|
||||
- RPC ID (e.g., `wXbhsf`)
|
||||
- Decoded params (JSON format)
|
||||
- Error message or unexpected behavior
|
||||
- Response if relevant
|
||||
|
||||
---
|
||||
|
||||
## Playwright Automation (For Developers)
|
||||
|
||||
**Best for:** Systematic RPC capture, building new features, CI/CD integration
|
||||
|
||||
### Setup
|
||||
|
||||
```python
|
||||
from playwright.async_api import async_playwright
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import unquote
|
||||
|
||||
async def setup_capture_session():
|
||||
"""Initialize Playwright with network interception."""
|
||||
playwright = await async_playwright().start()
|
||||
browser = await playwright.chromium.launch_persistent_context(
|
||||
user_data_dir="./browser_state",
|
||||
headless=False,
|
||||
)
|
||||
page = browser.pages[0] if browser.pages else await browser.new_page()
|
||||
|
||||
# Storage for captured RPCs
|
||||
captured_rpcs = []
|
||||
|
||||
# Intercept batchexecute requests
|
||||
async def handle_request(request):
|
||||
if "batchexecute" in request.url:
|
||||
post_data = request.post_data
|
||||
if post_data and "f.req" in post_data:
|
||||
# Use proper URL parsing (handles encoded & and = in values)
|
||||
from urllib.parse import parse_qs
|
||||
params = parse_qs(post_data)
|
||||
f_req = params.get("f.req", [None])[0]
|
||||
if f_req:
|
||||
decoded = decode_f_req(f_req)
|
||||
captured_rpcs.append({
|
||||
"timestamp": time.time(),
|
||||
"url": request.url,
|
||||
"rpc_id": decoded["rpc_id"],
|
||||
"params": decoded["params"],
|
||||
})
|
||||
|
||||
page.on("request", handle_request)
|
||||
return page, captured_rpcs
|
||||
|
||||
|
||||
def decode_f_req(encoded: str) -> dict:
|
||||
"""Decode f.req parameter to extract RPC details."""
|
||||
decoded = unquote(encoded)
|
||||
outer = json.loads(decoded)
|
||||
inner = outer[0][0]
|
||||
return {
|
||||
"rpc_id": inner[0],
|
||||
"params": json.loads(inner[1]),
|
||||
"raw": inner,
|
||||
}
|
||||
```
|
||||
|
||||
### Triggering Actions
|
||||
|
||||
```python
|
||||
async def trigger_action(page, captured_rpcs, action_type: str, **kwargs):
|
||||
"""Trigger a specific UI action and return captured RPC."""
|
||||
captured_rpcs.clear()
|
||||
|
||||
if action_type == "list_notebooks":
|
||||
await page.goto("https://notebooklm.google.com/")
|
||||
await page.wait_for_selector("mat-card", timeout=10000)
|
||||
|
||||
elif action_type == "create_notebook":
|
||||
await page.click("button:has-text('Create new')")
|
||||
await page.wait_for_url("**/notebook/**", timeout=10000)
|
||||
|
||||
elif action_type == "add_source_url":
|
||||
url = kwargs.get("url")
|
||||
await page.click("button:has-text('Add source')")
|
||||
await page.wait_for_selector("[role='dialog']", timeout=5000)
|
||||
await page.click("button:has-text('Website')")
|
||||
await page.fill("textarea[placeholder*='links']", url)
|
||||
await page.click("button:has-text('Insert')")
|
||||
await page.wait_for_timeout(5000)
|
||||
|
||||
return list(captured_rpcs)
|
||||
```
|
||||
|
||||
### Example Capture Session
|
||||
|
||||
```python
|
||||
async def capture_new_method():
|
||||
"""Example: Discover the RPC for a new action."""
|
||||
page, captured = await setup_capture_session()
|
||||
|
||||
# Authenticate if needed
|
||||
await page.goto("https://notebooklm.google.com/")
|
||||
|
||||
# Capture the action
|
||||
rpcs = await trigger_action(page, captured, "create_notebook")
|
||||
|
||||
# Print results
|
||||
for rpc in rpcs:
|
||||
print(f"RPC ID: {rpc['rpc_id']}")
|
||||
print(f"Params: {json.dumps(rpc['params'], indent=2)}")
|
||||
|
||||
await browser.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LLM Discovery Workflow
|
||||
|
||||
**Best for:** AI agents discovering new RPC methods, adaptive exploration
|
||||
|
||||
### Context for LLM
|
||||
|
||||
When investigating NotebookLM RPC calls, use this context:
|
||||
|
||||
```
|
||||
NotebookLM Protocol Facts:
|
||||
- Endpoint: /_/LabsTailwindUi/data/batchexecute
|
||||
- RPC IDs are 6-character strings (e.g., "wXbhsf")
|
||||
- Payload is triple-nested: [[[rpc_id, json_params, null, "generic"]]]
|
||||
- Response has )]}' anti-XSSI prefix
|
||||
- Parameters are position-sensitive arrays
|
||||
|
||||
Source of Truth:
|
||||
- Canonical RPC IDs: src/notebooklm/rpc/types.py
|
||||
- Payload structures: docs/reference/internals/rpc-ui-reference.md
|
||||
```
|
||||
|
||||
### Discovery Prompt Template
|
||||
|
||||
Use this when discovering a new RPC method:
|
||||
|
||||
```
|
||||
Task: Discover the RPC call for [ACTION_NAME]
|
||||
|
||||
Steps:
|
||||
1. Identify the UI element that triggers this action
|
||||
2. Set up network interception for batchexecute
|
||||
3. Trigger the UI action
|
||||
4. Capture the RPC request
|
||||
|
||||
Document:
|
||||
- RPC ID (6-character string)
|
||||
- Payload structure with parameter positions
|
||||
- Any source ID nesting patterns (single/double/triple)
|
||||
- Response structure
|
||||
|
||||
Reference: See rpc-ui-reference.md for existing patterns to follow.
|
||||
```
|
||||
|
||||
### Validation Workflow
|
||||
|
||||
After discovering a new RPC:
|
||||
|
||||
```python
|
||||
async def validate_rpc_call(rpc_id: str, params: list, expected_action: str):
|
||||
"""Validate an RPC call works correctly."""
|
||||
from notebooklm import NotebookLMClient
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
result = await client._rpc_call(RPCMethod(rpc_id), params)
|
||||
|
||||
assert result is not None, f"RPC {rpc_id} returned None"
|
||||
|
||||
return {
|
||||
"rpc_id": rpc_id,
|
||||
"action": expected_action,
|
||||
"status": "verified",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Parameter Position Sensitivity
|
||||
|
||||
NotebookLM RPC calls are **position-sensitive** - parameters must be at exact array indices.
|
||||
|
||||
**Example: ADD_SOURCE (URL)**
|
||||
```python
|
||||
params = [
|
||||
[[None, None, None, None, None, None, None, [url], None, None, 1]], # Position 0
|
||||
notebook_id, # Position 1
|
||||
[2], # Position 2
|
||||
[1, None, None, None, None, None, None, None, None, None, [1]], # Position 3
|
||||
]
|
||||
```
|
||||
|
||||
### Source ID Nesting
|
||||
|
||||
Different methods require different nesting levels:
|
||||
|
||||
| Nesting | Example | Used By |
|
||||
|---------|---------|---------|
|
||||
| Single | `source_id` | Simple lookups |
|
||||
| Double | `[[source_id]]` | DELETE_SOURCE, UPDATE_SOURCE |
|
||||
| Triple | `[[[source_id]]]` | CREATE_ARTIFACT source lists |
|
||||
|
||||
### Response Parsing
|
||||
|
||||
```python
|
||||
import json
|
||||
import re
|
||||
|
||||
def parse_response(text: str, rpc_id: str):
|
||||
"""Parse batchexecute response."""
|
||||
# Strip anti-XSSI prefix
|
||||
if text.startswith(")]}'"):
|
||||
text = re.sub(r"^\)\]\}'\r?\n", "", text)
|
||||
|
||||
# Find wrb.fr chunk for our RPC ID
|
||||
for line in text.split("\n"):
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
if chunk[0] == "wrb.fr" and chunk[1] == rpc_id:
|
||||
result = chunk[2]
|
||||
return json.loads(result) if isinstance(result, str) else result
|
||||
except (json.JSONDecodeError, IndexError):
|
||||
continue
|
||||
return None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Request Returns Null (No Error)
|
||||
|
||||
**Cause:** Payload format is close but not exact.
|
||||
|
||||
**Fix:** Capture exact browser request and compare byte-by-byte:
|
||||
```python
|
||||
import json
|
||||
your_params = [...]
|
||||
captured_params = [...]
|
||||
print(json.dumps(your_params, separators=(",", ":")))
|
||||
print(json.dumps(captured_params, separators=(",", ":")))
|
||||
```
|
||||
|
||||
### RPC ID Not Found in Response
|
||||
|
||||
**Cause:** Wrong RPC ID or different chunk format.
|
||||
|
||||
**Fix:** Log all chunks to find the result:
|
||||
```python
|
||||
for line in response.split("\n"):
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
print(f"Type: {chunk[0]}, ID: {chunk[1] if len(chunk) > 1 else 'N/A'}")
|
||||
except:
|
||||
continue
|
||||
```
|
||||
|
||||
### CSRF Token Expired
|
||||
|
||||
**Symptoms:** 403 errors or authentication failures.
|
||||
|
||||
**Fix:** Re-fetch tokens via browser or refresh storage state:
|
||||
```python
|
||||
await client.refresh_auth()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New RPC Methods
|
||||
|
||||
See **[Adding RPC Methods Guide](../../contributing/adding-rpc-methods.md)** for the complete step-by-step workflow.
|
||||
|
||||
Quick summary:
|
||||
1. Capture traffic using methodology above
|
||||
2. Decode payload and identify parameter positions
|
||||
3. Add to `rpc/types.py` and implement in `_*.py`
|
||||
4. Test with unit, integration, and E2E tests
|
||||
5. Document in `rpc-ui-reference.md`
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
# Releasing to PyPI
|
||||
|
||||
This document describes how to release a new version of `notebooklm-py` to PyPI.
|
||||
|
||||
## Pre-release Checklist
|
||||
|
||||
- [ ] All tests pass: `pytest`
|
||||
- [ ] E2E readonly tests pass: `pytest tests/e2e -m readonly`
|
||||
- [ ] No uncommitted changes: `git status`
|
||||
- [ ] On `main` branch with latest changes
|
||||
|
||||
## Release Steps
|
||||
|
||||
### 1. Update Version
|
||||
|
||||
Edit `pyproject.toml` and update the version number:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "notebooklm-py"
|
||||
version = "X.Y.Z" # Update this
|
||||
```
|
||||
|
||||
Follow [semantic versioning](https://semver.org/):
|
||||
- **MAJOR** (X): Breaking API changes
|
||||
- **MINOR** (Y): New features, backward compatible
|
||||
- **PATCH** (Z): Bug fixes, backward compatible
|
||||
|
||||
### 2. Commit Version Bump
|
||||
|
||||
```bash
|
||||
git add pyproject.toml
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. Test on TestPyPI
|
||||
|
||||
Build and upload to TestPyPI first:
|
||||
|
||||
```bash
|
||||
# Install build tools
|
||||
pip install build twine
|
||||
|
||||
# Build the package
|
||||
python -m build
|
||||
|
||||
# Upload to TestPyPI
|
||||
twine upload --repository testpypi dist/*
|
||||
```
|
||||
|
||||
Test the installation (uses TestPyPI for the package, PyPI for dependencies):
|
||||
|
||||
```bash
|
||||
uv pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple notebooklm-py
|
||||
```
|
||||
|
||||
Or with pip:
|
||||
|
||||
```bash
|
||||
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple notebooklm-py
|
||||
```
|
||||
|
||||
Verify it works:
|
||||
|
||||
```bash
|
||||
notebooklm --version
|
||||
notebooklm --help
|
||||
```
|
||||
|
||||
### 4. Create Release Tag
|
||||
|
||||
Once TestPyPI verification passes:
|
||||
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
This triggers the GitHub Actions workflow (`.github/workflows/publish.yml`) which automatically publishes to PyPI using trusted publishing.
|
||||
|
||||
### 5. Verify PyPI Release
|
||||
|
||||
After the workflow completes:
|
||||
|
||||
```bash
|
||||
# Install from PyPI
|
||||
pip install --upgrade notebooklm-py
|
||||
|
||||
# Verify
|
||||
notebooklm --version
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### TestPyPI upload fails
|
||||
|
||||
Ensure you have a TestPyPI account and API token:
|
||||
1. Create account at https://test.pypi.org/account/register/
|
||||
2. Create API token at https://test.pypi.org/manage/account/token/
|
||||
3. Configure in `~/.pypirc` or use `twine upload --username __token__ --password <token>`
|
||||
|
||||
### GitHub Actions publish fails
|
||||
|
||||
Ensure trusted publishing is configured on PyPI:
|
||||
1. Go to https://pypi.org/manage/project/notebooklm-py/settings/publishing/
|
||||
2. Add publisher with:
|
||||
- Owner: `teng-lin`
|
||||
- Repository: `notebooklm-py`
|
||||
- Workflow: `publish.yml`
|
||||
|
||||
### Version already exists
|
||||
|
||||
PyPI versions are immutable. If you need to fix something:
|
||||
1. Yank the bad version (optional): `twine yank notebooklm-py X.Y.Z`
|
||||
2. Bump to next patch version and release again
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Full release flow
|
||||
pytest # Run tests
|
||||
vim pyproject.toml # Bump version
|
||||
git add pyproject.toml && git commit -m "chore: bump version to X.Y.Z"
|
||||
git push
|
||||
python -m build # Build
|
||||
twine upload --repository testpypi dist/* # Upload to TestPyPI
|
||||
uv pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple notebooklm-py # Test
|
||||
git tag vX.Y.Z && git push origin vX.Y.Z # Release to PyPI
|
||||
```
|
||||
478
docs/rpc-development.md
Normal file
478
docs/rpc-development.md
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
# RPC Development Guide
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-01-13
|
||||
|
||||
This guide covers everything about NotebookLM's RPC protocol: capturing calls, debugging issues, and implementing new methods.
|
||||
|
||||
---
|
||||
|
||||
## Protocol Overview
|
||||
|
||||
NotebookLM uses Google's `batchexecute` RPC protocol.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
| Term | Description |
|
||||
|------|-------------|
|
||||
| **batchexecute** | Google's internal RPC endpoint |
|
||||
| **RPC ID** | 6-character identifier (e.g., `wXbhsf`, `s0tc2d`) |
|
||||
| **f.req** | URL-encoded JSON payload |
|
||||
| **at** | CSRF token (SNlM0e value) |
|
||||
| **Anti-XSSI** | `)]}'` prefix on responses |
|
||||
|
||||
### Protocol Flow
|
||||
|
||||
```
|
||||
1. Build request: [[[rpc_id, json_params, null, "generic"]]]
|
||||
2. Encode to f.req parameter
|
||||
3. POST to /_/LabsTailwindUi/data/batchexecute
|
||||
4. Strip )]}' prefix from response
|
||||
5. Parse chunked JSON, extract result
|
||||
```
|
||||
|
||||
### Source of Truth
|
||||
|
||||
- **RPC method IDs:** `src/notebooklm/rpc/types.py`
|
||||
- **Payload structures:** `docs/rpc-reference.md`
|
||||
|
||||
---
|
||||
|
||||
## Capturing RPC Calls
|
||||
|
||||
### Manual Capture (Chrome DevTools)
|
||||
|
||||
Best for quick investigation and bug reports.
|
||||
|
||||
1. Open Chrome → Navigate to `https://notebooklm.google.com/`
|
||||
2. Open DevTools (`F12` or `Cmd+Option+I`)
|
||||
3. Go to **Network** tab
|
||||
4. Configure:
|
||||
- [x] **Preserve log**
|
||||
- [x] **Disable cache**
|
||||
5. Filter by: `batchexecute`
|
||||
6. **Perform ONE action** (isolate the exact RPC call)
|
||||
7. Click the request to inspect
|
||||
|
||||
**From the request:**
|
||||
- **Headers tab → URL `rpcids`**: The RPC method ID
|
||||
- **Payload tab → `f.req`**: URL-encoded payload
|
||||
- **Response tab**: Starts with `)]}'` prefix
|
||||
|
||||
### Decoding the Payload
|
||||
|
||||
**Browser console:**
|
||||
```javascript
|
||||
const encoded = "..."; // Paste f.req value
|
||||
const decoded = decodeURIComponent(encoded);
|
||||
const outer = JSON.parse(decoded);
|
||||
console.log("RPC ID:", outer[0][0][0]);
|
||||
console.log("Params:", JSON.parse(outer[0][0][1]));
|
||||
```
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
|
||||
def decode_f_req(encoded: str) -> dict:
|
||||
decoded = unquote(encoded)
|
||||
outer = json.loads(decoded)
|
||||
inner = outer[0][0]
|
||||
return {
|
||||
"rpc_id": inner[0],
|
||||
"params": json.loads(inner[1]) if inner[1] else None,
|
||||
}
|
||||
```
|
||||
|
||||
### Playwright Automation
|
||||
|
||||
Best for systematic capture and CI integration.
|
||||
|
||||
```python
|
||||
from playwright.async_api import async_playwright
|
||||
import json
|
||||
from urllib.parse import unquote, parse_qs
|
||||
|
||||
async def setup_capture_session():
|
||||
playwright = await async_playwright().start()
|
||||
browser = await playwright.chromium.launch_persistent_context(
|
||||
user_data_dir="./browser_state",
|
||||
headless=False,
|
||||
)
|
||||
page = browser.pages[0] if browser.pages else await browser.new_page()
|
||||
captured_rpcs = []
|
||||
|
||||
async def handle_request(request):
|
||||
if "batchexecute" in request.url:
|
||||
post_data = request.post_data
|
||||
if post_data and "f.req" in post_data:
|
||||
params = parse_qs(post_data)
|
||||
f_req = params.get("f.req", [None])[0]
|
||||
if f_req:
|
||||
decoded = decode_f_req(f_req)
|
||||
captured_rpcs.append(decoded)
|
||||
|
||||
page.on("request", handle_request)
|
||||
return page, captured_rpcs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Issues
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
```bash
|
||||
# See what RPC IDs the server returns
|
||||
NOTEBOOKLM_DEBUG_RPC=1 notebooklm <command>
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
DEBUG: Looking for RPC ID: Ljjv0c
|
||||
DEBUG: Found RPC IDs in response: ['Ljjv0c']
|
||||
```
|
||||
|
||||
If IDs don't match, the method ID has changed - report it in a GitHub issue.
|
||||
|
||||
### Common Scenarios
|
||||
|
||||
#### "Session Expired" Errors
|
||||
|
||||
```python
|
||||
# Check CSRF token
|
||||
print(client.auth.csrf_token)
|
||||
|
||||
# Refresh auth
|
||||
await client.refresh_auth()
|
||||
```
|
||||
|
||||
**Solution:** Re-run `notebooklm login`
|
||||
|
||||
#### RPC Method Returns None
|
||||
|
||||
**Causes:**
|
||||
- Rate limiting (Google returns empty result)
|
||||
- Wrong RPC method ID
|
||||
- Incorrect parameter structure
|
||||
|
||||
**Debug:**
|
||||
```python
|
||||
from notebooklm.rpc import decode_response
|
||||
|
||||
raw_response = await http_client.post(...)
|
||||
print("Raw:", raw_response.text[:500])
|
||||
|
||||
result = decode_response(raw_response.text, "METHOD_ID")
|
||||
print("Parsed:", result)
|
||||
```
|
||||
|
||||
#### Parameter Order Issues
|
||||
|
||||
RPC parameters are **position-sensitive**:
|
||||
|
||||
```python
|
||||
# WRONG - missing positional elements
|
||||
params = [value, notebook_id]
|
||||
|
||||
# RIGHT - all positions filled
|
||||
params = [value, notebook_id, None, None, settings]
|
||||
```
|
||||
|
||||
**Debug:** Compare your params with captured traffic byte-by-byte.
|
||||
|
||||
#### Nested List Depth
|
||||
|
||||
Source IDs have different nesting requirements:
|
||||
|
||||
```python
|
||||
# Single nesting (some methods)
|
||||
["source_id"]
|
||||
|
||||
# Double nesting
|
||||
[["source_id"]]
|
||||
|
||||
# Triple nesting (artifact generation)
|
||||
[[["source_id"]]]
|
||||
|
||||
# Quad nesting (get_source_guide)
|
||||
[[[["source_id"]]]]
|
||||
```
|
||||
|
||||
**Debug:** Capture working traffic and count brackets.
|
||||
|
||||
### Response Parsing
|
||||
|
||||
```python
|
||||
import json
|
||||
import re
|
||||
|
||||
def parse_response(text: str, rpc_id: str):
|
||||
"""Parse batchexecute response."""
|
||||
# Strip anti-XSSI prefix
|
||||
if text.startswith(")]}'"):
|
||||
text = re.sub(r"^\)\]\}'\r?\n", "", text)
|
||||
|
||||
# Find wrb.fr chunk for our RPC ID
|
||||
for line in text.split("\n"):
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
if chunk[0] == "wrb.fr" and chunk[1] == rpc_id:
|
||||
result = chunk[2]
|
||||
return json.loads(result) if isinstance(result, str) else result
|
||||
except (json.JSONDecodeError, IndexError):
|
||||
continue
|
||||
return None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New RPC Methods
|
||||
|
||||
### Workflow
|
||||
|
||||
```
|
||||
1. Capture → 2. Decode → 3. Implement → 4. Test → 5. Document
|
||||
```
|
||||
|
||||
### Step 1: Capture
|
||||
|
||||
Use Chrome DevTools or Playwright (see above).
|
||||
|
||||
**What to capture:**
|
||||
- RPC ID from URL `rpcids` parameter
|
||||
- Decoded `f.req` payload
|
||||
- Response structure
|
||||
|
||||
### Step 2: Decode
|
||||
|
||||
Document each position in the params array:
|
||||
|
||||
```python
|
||||
# Example: ADD_SOURCE for URL
|
||||
params = [
|
||||
[[None, None, [url], None, None, None, None, None]], # 0: Source data
|
||||
notebook_id, # 1: Notebook ID
|
||||
[2], # 2: Fixed flag
|
||||
None, # 3: Optional settings
|
||||
]
|
||||
```
|
||||
|
||||
Key patterns:
|
||||
- **Nested source IDs:** Count brackets carefully
|
||||
- **Fixed flags:** Arrays like `[2]`, `[1]` that don't change
|
||||
- **Optional positions:** Often `None`
|
||||
|
||||
### Step 3: Implement
|
||||
|
||||
**Add RPC method ID** (`src/notebooklm/rpc/types.py`):
|
||||
```python
|
||||
class RPCMethod(str, Enum):
|
||||
NEW_METHOD = "AbCdEf" # 6-char ID from capture
|
||||
```
|
||||
|
||||
**Add client method** (appropriate `_*.py` file):
|
||||
```python
|
||||
async def new_method(self, notebook_id: str, param: str) -> SomeResult:
|
||||
"""Short description.
|
||||
|
||||
Args:
|
||||
notebook_id: The notebook ID.
|
||||
param: Description.
|
||||
|
||||
Returns:
|
||||
Description of return value.
|
||||
"""
|
||||
params = [
|
||||
param, # Position 0
|
||||
notebook_id, # Position 1
|
||||
[2], # Position 2: Fixed flag
|
||||
]
|
||||
|
||||
result = await self._core.rpc_call(
|
||||
RPCMethod.NEW_METHOD,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
return SomeResult.from_api_response(result)
|
||||
```
|
||||
|
||||
**Add dataclass if needed** (`src/notebooklm/types.py`):
|
||||
```python
|
||||
@dataclass
|
||||
class SomeResult:
|
||||
id: str
|
||||
title: str
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: list[Any]) -> "SomeResult":
|
||||
return cls(id=data[0], title=data[1])
|
||||
```
|
||||
|
||||
### Step 4: Test
|
||||
|
||||
**Unit test** (`tests/unit/`):
|
||||
```python
|
||||
def test_encode_new_method():
|
||||
params = ["value", "notebook_id", [2]]
|
||||
result = encode_rpc_request(RPCMethod.NEW_METHOD, params)
|
||||
assert "AbCdEf" in result
|
||||
```
|
||||
|
||||
**Integration test** (`tests/integration/`):
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_method(mock_client):
|
||||
mock_response = ["result_id", "Result Title"]
|
||||
with patch('notebooklm._core.ClientCore.rpc_call', new_callable=AsyncMock) as mock:
|
||||
mock.return_value = mock_response
|
||||
result = await mock_client.some_api.new_method("nb_id", "param")
|
||||
assert result.id == "result_id"
|
||||
```
|
||||
|
||||
**E2E test** (`tests/e2e/`):
|
||||
```python
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_method_e2e(client, read_only_notebook_id):
|
||||
result = await client.some_api.new_method(read_only_notebook_id, "param")
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
### Step 5: Document
|
||||
|
||||
Update `docs/rpc-reference.md`:
|
||||
|
||||
```markdown
|
||||
### NEW_METHOD (`AbCdEf`)
|
||||
|
||||
**Purpose:** Short description
|
||||
|
||||
**Params:**
|
||||
```python
|
||||
params = [
|
||||
some_value, # 0: Description
|
||||
notebook_id, # 1: Notebook ID
|
||||
[2], # 2: Fixed flag
|
||||
]
|
||||
```
|
||||
|
||||
**Response:** Description of response structure
|
||||
|
||||
**Source:** `_some_api.py:123`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Wrong nesting level
|
||||
|
||||
Different methods need different source ID nesting. Check similar methods.
|
||||
|
||||
### Position sensitivity
|
||||
|
||||
Params are arrays, not dicts. Position matters:
|
||||
|
||||
```python
|
||||
# WRONG - missing position 2
|
||||
params = [value, notebook_id, settings]
|
||||
|
||||
# RIGHT - explicit None for unused positions
|
||||
params = [value, notebook_id, None, settings]
|
||||
```
|
||||
|
||||
### Forgetting source_path
|
||||
|
||||
Some methods require `source_path` for routing:
|
||||
|
||||
```python
|
||||
# May fail without source_path
|
||||
await self._core.rpc_call(RPCMethod.X, params)
|
||||
|
||||
# Correct
|
||||
await self._core.rpc_call(
|
||||
RPCMethod.X,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
)
|
||||
```
|
||||
|
||||
### Response parsing
|
||||
|
||||
API returns nested arrays. Print raw response first:
|
||||
|
||||
```python
|
||||
result = await self._core.rpc_call(...)
|
||||
print(f"DEBUG: {result}") # See actual structure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Captured RPC ID and params structure
|
||||
- [ ] Added to `RPCMethod` enum in `rpc/types.py`
|
||||
- [ ] Implemented method in appropriate `_*.py` file
|
||||
- [ ] Added dataclass if needed in `types.py`
|
||||
- [ ] Added CLI command if needed
|
||||
- [ ] Unit test for encoding
|
||||
- [ ] Integration test with mock
|
||||
- [ ] E2E test (manual verification OK for rare operations)
|
||||
- [ ] Updated `rpc-reference.md`
|
||||
|
||||
---
|
||||
|
||||
## LLM Agent Workflow
|
||||
|
||||
For AI agents discovering new RPC methods:
|
||||
|
||||
### Context
|
||||
|
||||
```
|
||||
NotebookLM Protocol Facts:
|
||||
- Endpoint: /_/LabsTailwindUi/data/batchexecute
|
||||
- RPC IDs are 6-character strings (e.g., "wXbhsf")
|
||||
- Payload: [[[rpc_id, json_params, null, "generic"]]]
|
||||
- Response has )]}' anti-XSSI prefix
|
||||
- Parameters are position-sensitive arrays
|
||||
|
||||
Source of Truth:
|
||||
- Canonical RPC IDs: src/notebooklm/rpc/types.py
|
||||
- Payload structures: docs/rpc-reference.md
|
||||
```
|
||||
|
||||
### Discovery Prompt Template
|
||||
|
||||
```
|
||||
Task: Discover the RPC call for [ACTION_NAME]
|
||||
|
||||
Steps:
|
||||
1. Identify the UI element that triggers this action
|
||||
2. Set up network interception for batchexecute
|
||||
3. Trigger the UI action
|
||||
4. Capture the RPC request
|
||||
|
||||
Document:
|
||||
- RPC ID (6-character string)
|
||||
- Payload structure with parameter positions
|
||||
- Source ID nesting pattern
|
||||
- Response structure
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
```python
|
||||
async def validate_rpc_call(rpc_id: str, params: list, expected_action: str):
|
||||
from notebooklm import NotebookLMClient
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
result = await client._rpc_call(RPCMethod(rpc_id), params)
|
||||
|
||||
assert result is not None, f"RPC {rpc_id} returned None"
|
||||
return {"rpc_id": rpc_id, "action": expected_action, "status": "verified"}
|
||||
```
|
||||
|
|
@ -116,7 +116,7 @@ When Google changes their internal APIs:
|
|||
- Error message (especially any RPC error codes)
|
||||
- Which operation failed
|
||||
- When it started failing
|
||||
3. See [RPC Capture Guide](reference/internals/rpc-capture.md) for debugging
|
||||
3. See [RPC Development Guide](rpc-development.md) for debugging
|
||||
|
||||
### Self-Recovery
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
{
|
||||
"title": "Agent Flashcards",
|
||||
"cards": [
|
||||
{
|
||||
"front": "What is the core philosophy of the 'learn-claude-code' project?",
|
||||
"back": "Modern AI agents work because the model is trained to be an agent; our job is to give it tools and stay out of the way."
|
||||
},
|
||||
{
|
||||
"front": "What is the fundamental loop that every coding agent, like Claude Code, is based on?",
|
||||
"back": "A loop where the model calls tools until it's done, and the results of those tools are appended to the message history for the next iteration."
|
||||
},
|
||||
{
|
||||
"front": "According to the 'learn-claude-code' philosophy, the model is _____% of the agent, and the code is _____% of the agent.",
|
||||
"back": "80, 20"
|
||||
},
|
||||
{
|
||||
"front": "What is the core insight of the v0 `v0_bash_agent.py`?",
|
||||
"back": "Bash is all you need; a single `bash` tool is sufficient to provide full agent capability, including reading, writing, and executing."
|
||||
},
|
||||
{
|
||||
"front": "In `v0_bash_agent.py`, how is the concept of a subagent implemented without a dedicated 'Task' tool?",
|
||||
"back": "By recursively calling itself via a bash command (e.g., `python v0_bash_agent.py \"subtask\"`), which spawns an isolated process with a fresh context."
|
||||
},
|
||||
{
|
||||
"front": "What core insight does `v1_basic_agent.py` demonstrate?",
|
||||
"back": "The concept of 'Model as Agent,' where the model is the primary decision-maker, and the code just provides tools and runs the execution loop."
|
||||
},
|
||||
{
|
||||
"front": "What are the four essential tools introduced in `v1_basic_agent.py` that cover 90% of coding use cases?",
|
||||
"back": "`bash`, `read_file`, `write_file`, and `edit_file`."
|
||||
},
|
||||
{
|
||||
"front": "In an agent system, what is the purpose of the `read_file` tool?",
|
||||
"back": "To read the contents of an existing file, allowing the agent to understand code."
|
||||
},
|
||||
{
|
||||
"front": "Which tool in `v1_basic_agent.py` is used for surgical changes to existing code by replacing exact text?",
|
||||
"back": "The `edit_file` tool."
|
||||
},
|
||||
{
|
||||
"front": "What problem in multi-step tasks does `v2_todo_agent.py` aim to solve?",
|
||||
"back": "Context Fade, where the model loses focus or forgets steps in a complex plan because the plan is not explicitly tracked."
|
||||
},
|
||||
{
|
||||
"front": "What new tool is introduced in `v2_todo_agent.py` to enable structured planning?",
|
||||
"back": "The `TodoWrite` tool, which allows the agent to maintain and update a visible task list."
|
||||
},
|
||||
{
|
||||
"front": "What is the key design insight behind the constraints (e.g., max 20 items, one `in_progress` task) in the `TodoManager`?",
|
||||
"back": "Structure constrains AND enables; the constraints force focus and make complex task completion possible by providing scaffolding."
|
||||
},
|
||||
{
|
||||
"front": "In the `TodoManager`, what is the purpose of the `activeForm` field for a task item?",
|
||||
"back": "It provides a present-tense description of the action being performed for the task currently marked as `in_progress`."
|
||||
},
|
||||
{
|
||||
"front": "What problem arises when a single agent performs large exploration tasks before acting, as addressed in v3?",
|
||||
"back": "Context Pollution, where the agent's history fills with exploration details, leaving little room for the primary task."
|
||||
},
|
||||
{
|
||||
"front": "How does the v3 subagent mechanism solve the problem of context pollution?",
|
||||
"back": "It spawns child agents with isolated contexts for subtasks, so the main agent only receives a clean summary as a result."
|
||||
},
|
||||
{
|
||||
"front": "What is the name of the tool introduced in v3 to spawn child agents?",
|
||||
"back": "The `Task` tool."
|
||||
},
|
||||
{
|
||||
"front": "In the v3 subagent design, what is the purpose of the `AGENT_TYPES` registry?",
|
||||
"back": "To define different types of agents (e.g., 'explore', 'code', 'plan') with specific capabilities, prompts, and tool access."
|
||||
},
|
||||
{
|
||||
"front": "What is the key to context isolation when a subagent is executed in v3?",
|
||||
"back": "The subagent is started with a fresh, empty message history (`sub_messages = []`), so it does not inherit the parent's context."
|
||||
},
|
||||
{
|
||||
"front": "In the v3 `AGENT_TYPES` registry, what is the key difference in tool permissions for an 'explore' agent versus a 'code' agent?",
|
||||
"back": "The 'explore' agent has read-only tools (like `bash` and `read_file`), while the 'code' agent has access to all tools, including those that write files."
|
||||
},
|
||||
{
|
||||
"front": "What is the core insight of the v4 skills mechanism?",
|
||||
"back": "Skills are knowledge packages, not tools; they teach the agent HOW to do something, rather than just giving it a new capability."
|
||||
},
|
||||
{
|
||||
"front": "In v4, a Tool is what the model _____, while a Skill is how the model _____ to do something.",
|
||||
"back": "CAN do, KNOWS"
|
||||
},
|
||||
{
|
||||
"front": "What is the paradigm shift that skills embody, moving away from traditional AI development?",
|
||||
"back": "Knowledge Externalization, where knowledge is stored in editable documents (`SKILL.md`) instead of being locked inside model parameters."
|
||||
},
|
||||
{
|
||||
"front": "What is the main advantage of Knowledge Externalization over traditional model fine-tuning?",
|
||||
"back": "Anyone can teach the model a new skill by editing a text file, without needing ML expertise, training data, or GPU clusters."
|
||||
},
|
||||
{
|
||||
"front": "What is the standard file format for defining a skill in the v4 agent?",
|
||||
"back": "A `SKILL.md` file containing YAML frontmatter for metadata and a Markdown body for instructions."
|
||||
},
|
||||
{
|
||||
"front": "What are the two required metadata fields in a `SKILL.md` file's YAML frontmatter?",
|
||||
"back": "`name` and `description`."
|
||||
},
|
||||
{
|
||||
"front": "What is the concept of 'Progressive Disclosure' in the v4 skills mechanism?",
|
||||
"back": "Loading knowledge in layers: first, lightweight metadata is always available, and second, the detailed skill body is loaded only when triggered."
|
||||
},
|
||||
{
|
||||
"front": "What is the name of the tool introduced in v4 that allows the model to load domain expertise on-demand?",
|
||||
"back": "The `Skill` tool."
|
||||
},
|
||||
{
|
||||
"front": "What is the critical implementation detail for how skill content is injected into the conversation to preserve the prompt cache?",
|
||||
"back": "The skill content is returned as a `tool_result` (part of a user message), not injected into the system prompt."
|
||||
},
|
||||
{
|
||||
"front": "Why is it a bad practice to inject dynamic information into the system prompt on each turn of an agent loop?",
|
||||
"back": "It invalidates the KV Cache, as the entire message prefix changes, leading to re-computation and drastically increased costs (20-50x)."
|
||||
},
|
||||
{
|
||||
"front": "What is the KV Cache in the context of LLMs?",
|
||||
"back": "A mechanism that stores the computed key-value states of previous tokens in a sequence so they don't need to be re-calculated for subsequent tokens."
|
||||
},
|
||||
{
|
||||
"front": "A cache hit for an LLM API call requires that the new request has an _____ with the previous request.",
|
||||
"back": "exact prefix match"
|
||||
},
|
||||
{
|
||||
"front": "Which of these is a cache-breaking anti-pattern in agent development: append-only messages or message compression?",
|
||||
"back": "Message compression, as it modifies past history and invalidates the cache from the point of replacement."
|
||||
},
|
||||
{
|
||||
"front": "To optimize for cost and performance with LLM APIs, you should treat the conversation history as an _____, not an editable document.",
|
||||
"back": "append-only log"
|
||||
},
|
||||
{
|
||||
"front": "In the `learn-claude-code` project, how does using Skills represent a shift from 'training AI' to 'educating AI'?",
|
||||
"back": "It turns implicit knowledge that required training into explicit, human-readable documents that can be written, version-controlled, and shared."
|
||||
},
|
||||
{
|
||||
"front": "In v3, the pattern `Main Agent -> Subagent A -> Subagent B` is described as a strategy of _____.",
|
||||
"back": "Divide and conquer"
|
||||
},
|
||||
{
|
||||
"front": "What is the purpose of the `safe_path` function in the provided agent code?",
|
||||
"back": "It's a security measure to ensure the file path provided by the model stays within the project's working directory."
|
||||
},
|
||||
{
|
||||
"front": "In the v1 agent's `run_bash` function, what is one reason a command might be blocked?",
|
||||
"back": "It is considered dangerous, containing patterns like `rm -rf /` or `sudo`."
|
||||
},
|
||||
{
|
||||
"front": "What does the v3 `get_tools_for_agent` function do?",
|
||||
"back": "It filters the list of available tools based on the specified `agent_type` to enforce capability restrictions."
|
||||
},
|
||||
{
|
||||
"front": "Why do subagents in the v3 demo not get access to the `Task` tool?",
|
||||
"back": "To prevent the possibility of infinite recursion (a subagent spawning another subagent)."
|
||||
},
|
||||
{
|
||||
"front": "In v4, what information does the `SkillLoader` class initially load from all `SKILL.md` files at startup?",
|
||||
"back": "Only the metadata (name and description) from the YAML frontmatter, to keep the initial context lean."
|
||||
},
|
||||
{
|
||||
"front": "How does the v4 agent provide the model with hints about a skill's available resources, such as scripts or reference documents?",
|
||||
"back": "When a skill's content is loaded, it includes a list of files found in optional subdirectories like `scripts/` and `references/`."
|
||||
},
|
||||
{
|
||||
"front": "What is the primary difference between the `write_file` and `edit_file` tools?",
|
||||
"back": "`write_file` creates or completely overwrites a file, while `edit_file` performs a surgical replacement of specific text within an existing file."
|
||||
},
|
||||
{
|
||||
"front": "The core agent loop `while True: response = model(messages, tools) ...` demonstrates that the _____ controls the loop.",
|
||||
"back": "model"
|
||||
},
|
||||
{
|
||||
"front": "In v3, what is the role of a 'plan' agent type?",
|
||||
"back": "To analyze a codebase and produce a numbered implementation plan without modifying any files."
|
||||
},
|
||||
{
|
||||
"front": "The v4 `SkillLoader`'s `parse_skill_md` function uses a regular expression to separate the _____ from the Markdown body.",
|
||||
"back": "YAML frontmatter"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
# Agent Flashcards
|
||||
|
||||
## Card 1
|
||||
|
||||
**Q:** What is the core philosophy of the 'learn-claude-code' project?
|
||||
|
||||
**A:** Modern AI agents work because the model is trained to be an agent; our job is to give it tools and stay out of the way.
|
||||
|
||||
---
|
||||
|
||||
## Card 2
|
||||
|
||||
**Q:** What is the fundamental loop that every coding agent, like Claude Code, is based on?
|
||||
|
||||
**A:** A loop where the model calls tools until it's done, and the results of those tools are appended to the message history for the next iteration.
|
||||
|
||||
---
|
||||
|
||||
## Card 3
|
||||
|
||||
**Q:** According to the 'learn-claude-code' philosophy, the model is _____% of the agent, and the code is _____% of the agent.
|
||||
|
||||
**A:** 80, 20
|
||||
|
||||
---
|
||||
|
||||
## Card 4
|
||||
|
||||
**Q:** What is the core insight of the v0 `v0_bash_agent.py`?
|
||||
|
||||
**A:** Bash is all you need; a single `bash` tool is sufficient to provide full agent capability, including reading, writing, and executing.
|
||||
|
||||
---
|
||||
|
||||
## Card 5
|
||||
|
||||
**Q:** In `v0_bash_agent.py`, how is the concept of a subagent implemented without a dedicated 'Task' tool?
|
||||
|
||||
**A:** By recursively calling itself via a bash command (e.g., `python v0_bash_agent.py "subtask"`), which spawns an isolated process with a fresh context.
|
||||
|
||||
---
|
||||
|
||||
## Card 6
|
||||
|
||||
**Q:** What core insight does `v1_basic_agent.py` demonstrate?
|
||||
|
||||
**A:** The concept of 'Model as Agent,' where the model is the primary decision-maker, and the code just provides tools and runs the execution loop.
|
||||
|
||||
---
|
||||
|
||||
## Card 7
|
||||
|
||||
**Q:** What are the four essential tools introduced in `v1_basic_agent.py` that cover 90% of coding use cases?
|
||||
|
||||
**A:** `bash`, `read_file`, `write_file`, and `edit_file`.
|
||||
|
||||
---
|
||||
|
||||
## Card 8
|
||||
|
||||
**Q:** In an agent system, what is the purpose of the `read_file` tool?
|
||||
|
||||
**A:** To read the contents of an existing file, allowing the agent to understand code.
|
||||
|
||||
---
|
||||
|
||||
## Card 9
|
||||
|
||||
**Q:** Which tool in `v1_basic_agent.py` is used for surgical changes to existing code by replacing exact text?
|
||||
|
||||
**A:** The `edit_file` tool.
|
||||
|
||||
---
|
||||
|
||||
## Card 10
|
||||
|
||||
**Q:** What problem in multi-step tasks does `v2_todo_agent.py` aim to solve?
|
||||
|
||||
**A:** Context Fade, where the model loses focus or forgets steps in a complex plan because the plan is not explicitly tracked.
|
||||
|
||||
---
|
||||
|
||||
## Card 11
|
||||
|
||||
**Q:** What new tool is introduced in `v2_todo_agent.py` to enable structured planning?
|
||||
|
||||
**A:** The `TodoWrite` tool, which allows the agent to maintain and update a visible task list.
|
||||
|
||||
---
|
||||
|
||||
## Card 12
|
||||
|
||||
**Q:** What is the key design insight behind the constraints (e.g., max 20 items, one `in_progress` task) in the `TodoManager`?
|
||||
|
||||
**A:** Structure constrains AND enables; the constraints force focus and make complex task completion possible by providing scaffolding.
|
||||
|
||||
---
|
||||
|
||||
## Card 13
|
||||
|
||||
**Q:** In the `TodoManager`, what is the purpose of the `activeForm` field for a task item?
|
||||
|
||||
**A:** It provides a present-tense description of the action being performed for the task currently marked as `in_progress`.
|
||||
|
||||
---
|
||||
|
||||
## Card 14
|
||||
|
||||
**Q:** What problem arises when a single agent performs large exploration tasks before acting, as addressed in v3?
|
||||
|
||||
**A:** Context Pollution, where the agent's history fills with exploration details, leaving little room for the primary task.
|
||||
|
||||
---
|
||||
|
||||
## Card 15
|
||||
|
||||
**Q:** How does the v3 subagent mechanism solve the problem of context pollution?
|
||||
|
||||
**A:** It spawns child agents with isolated contexts for subtasks, so the main agent only receives a clean summary as a result.
|
||||
|
||||
---
|
||||
|
||||
## Card 16
|
||||
|
||||
**Q:** What is the name of the tool introduced in v3 to spawn child agents?
|
||||
|
||||
**A:** The `Task` tool.
|
||||
|
||||
---
|
||||
|
||||
## Card 17
|
||||
|
||||
**Q:** In the v3 subagent design, what is the purpose of the `AGENT_TYPES` registry?
|
||||
|
||||
**A:** To define different types of agents (e.g., 'explore', 'code', 'plan') with specific capabilities, prompts, and tool access.
|
||||
|
||||
---
|
||||
|
||||
## Card 18
|
||||
|
||||
**Q:** What is the key to context isolation when a subagent is executed in v3?
|
||||
|
||||
**A:** The subagent is started with a fresh, empty message history (`sub_messages = []`), so it does not inherit the parent's context.
|
||||
|
||||
---
|
||||
|
||||
## Card 19
|
||||
|
||||
**Q:** In the v3 `AGENT_TYPES` registry, what is the key difference in tool permissions for an 'explore' agent versus a 'code' agent?
|
||||
|
||||
**A:** The 'explore' agent has read-only tools (like `bash` and `read_file`), while the 'code' agent has access to all tools, including those that write files.
|
||||
|
||||
---
|
||||
|
||||
## Card 20
|
||||
|
||||
**Q:** What is the core insight of the v4 skills mechanism?
|
||||
|
||||
**A:** Skills are knowledge packages, not tools; they teach the agent HOW to do something, rather than just giving it a new capability.
|
||||
|
||||
---
|
||||
|
||||
## Card 21
|
||||
|
||||
**Q:** In v4, a Tool is what the model _____, while a Skill is how the model _____ to do something.
|
||||
|
||||
**A:** CAN do, KNOWS
|
||||
|
||||
---
|
||||
|
||||
## Card 22
|
||||
|
||||
**Q:** What is the paradigm shift that skills embody, moving away from traditional AI development?
|
||||
|
||||
**A:** Knowledge Externalization, where knowledge is stored in editable documents (`SKILL.md`) instead of being locked inside model parameters.
|
||||
|
||||
---
|
||||
|
||||
## Card 23
|
||||
|
||||
**Q:** What is the main advantage of Knowledge Externalization over traditional model fine-tuning?
|
||||
|
||||
**A:** Anyone can teach the model a new skill by editing a text file, without needing ML expertise, training data, or GPU clusters.
|
||||
|
||||
---
|
||||
|
||||
## Card 24
|
||||
|
||||
**Q:** What is the standard file format for defining a skill in the v4 agent?
|
||||
|
||||
**A:** A `SKILL.md` file containing YAML frontmatter for metadata and a Markdown body for instructions.
|
||||
|
||||
---
|
||||
|
||||
## Card 25
|
||||
|
||||
**Q:** What are the two required metadata fields in a `SKILL.md` file's YAML frontmatter?
|
||||
|
||||
**A:** `name` and `description`.
|
||||
|
||||
---
|
||||
|
||||
## Card 26
|
||||
|
||||
**Q:** What is the concept of 'Progressive Disclosure' in the v4 skills mechanism?
|
||||
|
||||
**A:** Loading knowledge in layers: first, lightweight metadata is always available, and second, the detailed skill body is loaded only when triggered.
|
||||
|
||||
---
|
||||
|
||||
## Card 27
|
||||
|
||||
**Q:** What is the name of the tool introduced in v4 that allows the model to load domain expertise on-demand?
|
||||
|
||||
**A:** The `Skill` tool.
|
||||
|
||||
---
|
||||
|
||||
## Card 28
|
||||
|
||||
**Q:** What is the critical implementation detail for how skill content is injected into the conversation to preserve the prompt cache?
|
||||
|
||||
**A:** The skill content is returned as a `tool_result` (part of a user message), not injected into the system prompt.
|
||||
|
||||
---
|
||||
|
||||
## Card 29
|
||||
|
||||
**Q:** Why is it a bad practice to inject dynamic information into the system prompt on each turn of an agent loop?
|
||||
|
||||
**A:** It invalidates the KV Cache, as the entire message prefix changes, leading to re-computation and drastically increased costs (20-50x).
|
||||
|
||||
---
|
||||
|
||||
## Card 30
|
||||
|
||||
**Q:** What is the KV Cache in the context of LLMs?
|
||||
|
||||
**A:** A mechanism that stores the computed key-value states of previous tokens in a sequence so they don't need to be re-calculated for subsequent tokens.
|
||||
|
||||
---
|
||||
|
||||
## Card 31
|
||||
|
||||
**Q:** A cache hit for an LLM API call requires that the new request has an _____ with the previous request.
|
||||
|
||||
**A:** exact prefix match
|
||||
|
||||
---
|
||||
|
||||
## Card 32
|
||||
|
||||
**Q:** Which of these is a cache-breaking anti-pattern in agent development: append-only messages or message compression?
|
||||
|
||||
**A:** Message compression, as it modifies past history and invalidates the cache from the point of replacement.
|
||||
|
||||
---
|
||||
|
||||
## Card 33
|
||||
|
||||
**Q:** To optimize for cost and performance with LLM APIs, you should treat the conversation history as an _____, not an editable document.
|
||||
|
||||
**A:** append-only log
|
||||
|
||||
---
|
||||
|
||||
## Card 34
|
||||
|
||||
**Q:** In the `learn-claude-code` project, how does using Skills represent a shift from 'training AI' to 'educating AI'?
|
||||
|
||||
**A:** It turns implicit knowledge that required training into explicit, human-readable documents that can be written, version-controlled, and shared.
|
||||
|
||||
---
|
||||
|
||||
## Card 35
|
||||
|
||||
**Q:** In v3, the pattern `Main Agent -> Subagent A -> Subagent B` is described as a strategy of _____.
|
||||
|
||||
**A:** Divide and conquer
|
||||
|
||||
---
|
||||
|
||||
## Card 36
|
||||
|
||||
**Q:** What is the purpose of the `safe_path` function in the provided agent code?
|
||||
|
||||
**A:** It's a security measure to ensure the file path provided by the model stays within the project's working directory.
|
||||
|
||||
---
|
||||
|
||||
## Card 37
|
||||
|
||||
**Q:** In the v1 agent's `run_bash` function, what is one reason a command might be blocked?
|
||||
|
||||
**A:** It is considered dangerous, containing patterns like `rm -rf /` or `sudo`.
|
||||
|
||||
---
|
||||
|
||||
## Card 38
|
||||
|
||||
**Q:** What does the v3 `get_tools_for_agent` function do?
|
||||
|
||||
**A:** It filters the list of available tools based on the specified `agent_type` to enforce capability restrictions.
|
||||
|
||||
---
|
||||
|
||||
## Card 39
|
||||
|
||||
**Q:** Why do subagents in the v3 demo not get access to the `Task` tool?
|
||||
|
||||
**A:** To prevent the possibility of infinite recursion (a subagent spawning another subagent).
|
||||
|
||||
---
|
||||
|
||||
## Card 40
|
||||
|
||||
**Q:** In v4, what information does the `SkillLoader` class initially load from all `SKILL.md` files at startup?
|
||||
|
||||
**A:** Only the metadata (name and description) from the YAML frontmatter, to keep the initial context lean.
|
||||
|
||||
---
|
||||
|
||||
## Card 41
|
||||
|
||||
**Q:** How does the v4 agent provide the model with hints about a skill's available resources, such as scripts or reference documents?
|
||||
|
||||
**A:** When a skill's content is loaded, it includes a list of files found in optional subdirectories like `scripts/` and `references/`.
|
||||
|
||||
---
|
||||
|
||||
## Card 42
|
||||
|
||||
**Q:** What is the primary difference between the `write_file` and `edit_file` tools?
|
||||
|
||||
**A:** `write_file` creates or completely overwrites a file, while `edit_file` performs a surgical replacement of specific text within an existing file.
|
||||
|
||||
---
|
||||
|
||||
## Card 43
|
||||
|
||||
**Q:** The core agent loop `while True: response = model(messages, tools) ...` demonstrates that the _____ controls the loop.
|
||||
|
||||
**A:** model
|
||||
|
||||
---
|
||||
|
||||
## Card 44
|
||||
|
||||
**Q:** In v3, what is the role of a 'plan' agent type?
|
||||
|
||||
**A:** To analyze a codebase and produce a numbered implementation plan without modifying any files.
|
||||
|
||||
---
|
||||
|
||||
## Card 45
|
||||
|
||||
**Q:** The v4 `SkillLoader`'s `parse_skill_md` function uses a regular expression to separate the _____ from the Markdown body.
|
||||
|
||||
**A:** YAML frontmatter
|
||||
|
||||
---
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
{
|
||||
"title": "Agent Quiz",
|
||||
"questions": [
|
||||
{
|
||||
"question": "What is the core philosophy of the `learn-claude-code` project, as stated in its documentation?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "The agent's performance is primarily determined by clever engineering and complex code loops.",
|
||||
"rationale": "The project's philosophy suggests that the code itself is only 20% of the solution, with the model's inherent capabilities being the most significant factor.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The model itself is the agent, and the surrounding code's main job is to provide it with tools and manage the execution loop.",
|
||||
"rationale": "This aligns with the project's \"Model as Agent\" philosophy, emphasizing that the model's training to act as an agent is the key, not intricate code.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "The number and variety of tools available to the agent are the most critical factors for its success.",
|
||||
"rationale": "While tools are necessary, the core philosophy emphasizes the model's role as the decision-maker, not just the quantity of tools it can access.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "Effective AI agents must rely solely on their internal, pre-trained knowledge without using external tools.",
|
||||
"rationale": "The entire project is built around the concept of an agent that repeatedly interacts with its environment using tools.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "Consider the stated ratio of importance between the model and the code in the project's philosophy."
|
||||
},
|
||||
{
|
||||
"question": "In the `v0_bash_agent.py` implementation, how is the concept of a subagent achieved?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "By using a dedicated `Task` tool that spawns a child agent from a registry.",
|
||||
"rationale": "The `Task` tool and an agent registry are features introduced in the more advanced `v3` version, not the minimalist `v0`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By recursively executing the script as a new, isolated process via a `bash` command.",
|
||||
"rationale": "This method leverages process isolation at the operating system level to create a subagent with a fresh context, which is the key insight of the `v0` agent.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "By creating a new thread within the parent process to handle the sub-task.",
|
||||
"rationale": "While a valid programming technique, the `v0` agent uses process spawning, not multithreading, to ensure complete context isolation.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By instantiating a new agent class within the code that maintains a separate history list.",
|
||||
"rationale": "The `v0` implementation is much simpler and relies on an external shell command rather than internal class structures for sub-tasking.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "Think about how this minimalist agent uses its single tool to delegate work."
|
||||
},
|
||||
{
|
||||
"question": "What primary problem is the `TodoWrite` tool in `v2_todo_agent.py` designed to solve for the agent?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "The agent polluting its context window with excessive file content during exploration.",
|
||||
"rationale": "This issue, known as 'context pollution', is the primary problem addressed by the subagent mechanism in `v3`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The agent's inability to write new files or modify existing ones.",
|
||||
"rationale": "File writing and editing capabilities are fundamental tools provided in the `v1` basic agent.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The model losing focus or forgetting the overall plan during complex, multi-step tasks.",
|
||||
"rationale": "The `TodoWrite` tool makes the agent's plan explicit and persistent, addressing 'context fade' and helping it track progress.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "The agent not knowing which type of specialized subagent to use for a task.",
|
||||
"rationale": "The concept of specialized subagents is introduced in `v3`, whereas the `TodoWrite` tool is about managing a single agent's workflow.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "This tool helps make the agent's internal thought process visible and persistent."
|
||||
},
|
||||
{
|
||||
"question": "According to the v3 documentation, what is the main advantage of using subagents?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "It allows the agent to learn new domain-specific expertise from `SKILL.md` files.",
|
||||
"rationale": "Loading expertise from external files is the key feature of the `v4` skills mechanism.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It provides a mechanism for structured planning and tracking task completion.",
|
||||
"rationale": "Structured planning through a visible list is the core concept introduced with the `TodoManager` in `v2`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It enables the agent to run multiple `bash` commands in parallel for faster execution.",
|
||||
"rationale": "While parallelism is mentioned as a possibility, the primary benefit highlighted is the management of the agent's focus.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It isolates the context of a sub-task, preventing the main agent's history from becoming polluted.",
|
||||
"rationale": "This is the central purpose of subagents in `v3`: to handle tasks like codebase exploration in a separate context so the main agent remains focused.",
|
||||
"isCorrect": true
|
||||
}
|
||||
],
|
||||
"hint": "Consider the problem that arises when an agent reads many large files to prepare for a small change."
|
||||
},
|
||||
{
|
||||
"question": "The v4 documentation draws a clear distinction between 'Tools' and 'Skills'. What is this distinction?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "Tools define what an agent *can do* (its capabilities), while Skills define *how* an agent knows to perform a task (its expertise).",
|
||||
"rationale": "This correctly captures the essence of the distinction: Tools are about action/capability, whereas Skills are about knowledge/expertise.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "Tools are built-in functionalities, while Skills are third-party plugins that must be downloaded.",
|
||||
"rationale": "The documentation describes skills as locally stored, human-editable files, not necessarily as plugins from a marketplace.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "Tools are used for general-purpose tasks like reading files, while Skills are only for code generation.",
|
||||
"rationale": "Skills provide domain expertise for a wide range of tasks, such as PDF processing or code review, not just code generation.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "Tools are implemented in Python code, whereas Skills are implemented using shell scripts.",
|
||||
"rationale": "Skills are primarily human-readable Markdown documents that provide instructions; they are not themselves executable scripts.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "Think about the difference between having a hammer and knowing how to build a house."
|
||||
},
|
||||
{
|
||||
"question": "What is the key benefit of the \"Knowledge Externalization\" paradigm introduced in v4?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "It allows the agent's knowledge to be version-controlled, audited, and edited in plain text by anyone, without requiring model training.",
|
||||
"rationale": "This concept democratizes the process of 'teaching' an agent, moving from expensive, expertise-heavy model training to simple document editing.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "It forces the agent to write all its internal thoughts to an external log file for easier debugging.",
|
||||
"rationale": "While logging is a development practice, Knowledge Externalization is about how the agent acquires and uses expertise, not how it logs its actions.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It caches the model's large parameter weights in the local file system to speed up agent initialization.",
|
||||
"rationale": "This describes a form of model caching, which is a different concept from externalizing domain knowledge into editable documents.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It requires the agent to use external web APIs for all information, preventing it from using outdated internal knowledge.",
|
||||
"rationale": "Knowledge Externalization refers to loading structured expertise from local `SKILL.md` files, not necessarily relying on web APIs.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "This paradigm shift makes customizing an agent's expertise as easy as editing a text document."
|
||||
},
|
||||
{
|
||||
"question": "To maintain cost-efficiency by preserving the KV Cache, how does the `v4_skills_agent.py` inject skill content into the conversation?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "By dynamically updating the system prompt with the new skill information before each model call.",
|
||||
"rationale": "The documentation explicitly identifies this as a costly anti-pattern because modifying the system prompt invalidates the entire cache.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By appending the skill content as a `tool_result` message, which doesn't alter the preceding message history.",
|
||||
"rationale": "This append-only method ensures the prefix of the conversation remains unchanged, allowing the provider to reuse the cached computations.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "By performing a lightweight fine-tuning operation on the model in real-time.",
|
||||
"rationale": "Real-time fine-tuning is computationally expensive and is the 'traditional' approach that the skills mechanism is designed to avoid.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By storing the skill content in a separate memory buffer that is not part of the model's main context.",
|
||||
"rationale": "For the model to use the knowledge, it must be part of its context; the key is *how* it's added to that context.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "The correct method avoids changing the beginning or middle of the conversation history."
|
||||
},
|
||||
{
|
||||
"question": "Which of these is NOT one of the four essential tools introduced in the `v1_basic_agent.py` to cover most coding use cases?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "`bash`",
|
||||
"rationale": "`bash` is included as the gateway tool for running any command-line operation.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "`read_file`",
|
||||
"rationale": "`read_file` is included as an essential tool for understanding existing code.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "`TodoWrite`",
|
||||
"rationale": "`TodoWrite` is the specialized tool for structured planning introduced in the `v2` agent.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "`edit_file`",
|
||||
"rationale": "`edit_file` is included for making surgical changes to existing files.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "The first version of the agent focused on core capabilities for interacting with the file system and shell, not on complex planning."
|
||||
},
|
||||
{
|
||||
"question": "What is the fundamental logic of the \"core agent loop\" described throughout the project?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "The agent first creates a complete, unchangeable plan and then executes each step in sequence.",
|
||||
"rationale": "The agent loop is iterative and reactive; the result of one tool call informs the model's decision for the next one.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The model makes a tool call, the tool's result is added to the history, and this cycle repeats until the model stops calling tools.",
|
||||
"rationale": "This accurately describes the iterative process where the model is the decision-maker, acting in a loop until the task is complete.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "The user provides a precise sequence of tools for the agent to execute.",
|
||||
"rationale": "In this agent model, the user provides a high-level goal, and the AI model itself decides which tools to use and in what order.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The agent's code analyzes the prompt and selects a single, optimal tool to resolve the entire request.",
|
||||
"rationale": "The power of the agent comes from its ability to use multiple tools sequentially over several turns, not from picking just one.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "It is a repetitive cycle of thinking, acting, and observing the results."
|
||||
},
|
||||
{
|
||||
"question": "In the `v3` subagent mechanism, what is the purpose of the `AGENT_TYPES` registry?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "To define different subagent archetypes with specific roles, prompts, and restricted toolsets.",
|
||||
"rationale": "This registry allows the main agent to spawn specialized subagents, such as a read-only 'explore' agent or a full-powered 'code' agent.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "To list all the available skills and their descriptions for the `v4` agent.",
|
||||
"rationale": "The listing of skills is handled by the `SkillLoader` class, which is a concept from `v4`, not `v3`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "To keep a real-time log of all active subagent processes and their current status.",
|
||||
"rationale": "The registry is a static configuration defining agent types, not a dynamic process manager that tracks running instances.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "To provide the input schema and validation rules for the `TodoWrite` tool.",
|
||||
"rationale": "A tool's input schema is defined within the tool's own JSON definition, not in the agent type registry.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "This configuration allows the main agent to choose the right kind of 'specialist' for a sub-task."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
# Agent Quiz
|
||||
|
||||
## Question 1
|
||||
What is the core philosophy of the `learn-claude-code` project, as stated in its documentation?
|
||||
|
||||
- [ ] The agent's performance is primarily determined by clever engineering and complex code loops.
|
||||
- [x] The model itself is the agent, and the surrounding code's main job is to provide it with tools and manage the execution loop.
|
||||
- [ ] The number and variety of tools available to the agent are the most critical factors for its success.
|
||||
- [ ] Effective AI agents must rely solely on their internal, pre-trained knowledge without using external tools.
|
||||
|
||||
**Hint:** Consider the stated ratio of importance between the model and the code in the project's philosophy.
|
||||
|
||||
## Question 2
|
||||
In the `v0_bash_agent.py` implementation, how is the concept of a subagent achieved?
|
||||
|
||||
- [ ] By using a dedicated `Task` tool that spawns a child agent from a registry.
|
||||
- [x] By recursively executing the script as a new, isolated process via a `bash` command.
|
||||
- [ ] By creating a new thread within the parent process to handle the sub-task.
|
||||
- [ ] By instantiating a new agent class within the code that maintains a separate history list.
|
||||
|
||||
**Hint:** Think about how this minimalist agent uses its single tool to delegate work.
|
||||
|
||||
## Question 3
|
||||
What primary problem is the `TodoWrite` tool in `v2_todo_agent.py` designed to solve for the agent?
|
||||
|
||||
- [ ] The agent polluting its context window with excessive file content during exploration.
|
||||
- [ ] The agent's inability to write new files or modify existing ones.
|
||||
- [x] The model losing focus or forgetting the overall plan during complex, multi-step tasks.
|
||||
- [ ] The agent not knowing which type of specialized subagent to use for a task.
|
||||
|
||||
**Hint:** This tool helps make the agent's internal thought process visible and persistent.
|
||||
|
||||
## Question 4
|
||||
According to the v3 documentation, what is the main advantage of using subagents?
|
||||
|
||||
- [ ] It allows the agent to learn new domain-specific expertise from `SKILL.md` files.
|
||||
- [ ] It provides a mechanism for structured planning and tracking task completion.
|
||||
- [ ] It enables the agent to run multiple `bash` commands in parallel for faster execution.
|
||||
- [x] It isolates the context of a sub-task, preventing the main agent's history from becoming polluted.
|
||||
|
||||
**Hint:** Consider the problem that arises when an agent reads many large files to prepare for a small change.
|
||||
|
||||
## Question 5
|
||||
The v4 documentation draws a clear distinction between 'Tools' and 'Skills'. What is this distinction?
|
||||
|
||||
- [x] Tools define what an agent *can do* (its capabilities), while Skills define *how* an agent knows to perform a task (its expertise).
|
||||
- [ ] Tools are built-in functionalities, while Skills are third-party plugins that must be downloaded.
|
||||
- [ ] Tools are used for general-purpose tasks like reading files, while Skills are only for code generation.
|
||||
- [ ] Tools are implemented in Python code, whereas Skills are implemented using shell scripts.
|
||||
|
||||
**Hint:** Think about the difference between having a hammer and knowing how to build a house.
|
||||
|
||||
## Question 6
|
||||
What is the key benefit of the "Knowledge Externalization" paradigm introduced in v4?
|
||||
|
||||
- [x] It allows the agent's knowledge to be version-controlled, audited, and edited in plain text by anyone, without requiring model training.
|
||||
- [ ] It forces the agent to write all its internal thoughts to an external log file for easier debugging.
|
||||
- [ ] It caches the model's large parameter weights in the local file system to speed up agent initialization.
|
||||
- [ ] It requires the agent to use external web APIs for all information, preventing it from using outdated internal knowledge.
|
||||
|
||||
**Hint:** This paradigm shift makes customizing an agent's expertise as easy as editing a text document.
|
||||
|
||||
## Question 7
|
||||
To maintain cost-efficiency by preserving the KV Cache, how does the `v4_skills_agent.py` inject skill content into the conversation?
|
||||
|
||||
- [ ] By dynamically updating the system prompt with the new skill information before each model call.
|
||||
- [x] By appending the skill content as a `tool_result` message, which doesn't alter the preceding message history.
|
||||
- [ ] By performing a lightweight fine-tuning operation on the model in real-time.
|
||||
- [ ] By storing the skill content in a separate memory buffer that is not part of the model's main context.
|
||||
|
||||
**Hint:** The correct method avoids changing the beginning or middle of the conversation history.
|
||||
|
||||
## Question 8
|
||||
Which of these is NOT one of the four essential tools introduced in the `v1_basic_agent.py` to cover most coding use cases?
|
||||
|
||||
- [ ] `bash`
|
||||
- [ ] `read_file`
|
||||
- [x] `TodoWrite`
|
||||
- [ ] `edit_file`
|
||||
|
||||
**Hint:** The first version of the agent focused on core capabilities for interacting with the file system and shell, not on complex planning.
|
||||
|
||||
## Question 9
|
||||
What is the fundamental logic of the "core agent loop" described throughout the project?
|
||||
|
||||
- [ ] The agent first creates a complete, unchangeable plan and then executes each step in sequence.
|
||||
- [x] The model makes a tool call, the tool's result is added to the history, and this cycle repeats until the model stops calling tools.
|
||||
- [ ] The user provides a precise sequence of tools for the agent to execute.
|
||||
- [ ] The agent's code analyzes the prompt and selects a single, optimal tool to resolve the entire request.
|
||||
|
||||
**Hint:** It is a repetitive cycle of thinking, acting, and observing the results.
|
||||
|
||||
## Question 10
|
||||
In the `v3` subagent mechanism, what is the purpose of the `AGENT_TYPES` registry?
|
||||
|
||||
- [x] To define different subagent archetypes with specific roles, prompts, and restricted toolsets.
|
||||
- [ ] To list all the available skills and their descriptions for the `v4` agent.
|
||||
- [ ] To keep a real-time log of all active subagent processes and their current status.
|
||||
- [ ] To provide the input schema and validation rules for the `TodoWrite` tool.
|
||||
|
||||
**Hint:** This configuration allows the main agent to choose the right kind of 'specialist' for a sub-task.
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
{
|
||||
"title": "Agent Quiz",
|
||||
"questions": [
|
||||
{
|
||||
"question": "What is the core philosophy of the `learn-claude-code` project, as stated in its documentation?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "The agent's performance is primarily determined by clever engineering and complex code loops.",
|
||||
"rationale": "The project's philosophy suggests that the code itself is only 20% of the solution, with the model's inherent capabilities being the most significant factor.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The model itself is the agent, and the surrounding code's main job is to provide it with tools and manage the execution loop.",
|
||||
"rationale": "This aligns with the project's \"Model as Agent\" philosophy, emphasizing that the model's training to act as an agent is the key, not intricate code.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "The number and variety of tools available to the agent are the most critical factors for its success.",
|
||||
"rationale": "While tools are necessary, the core philosophy emphasizes the model's role as the decision-maker, not just the quantity of tools it can access.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "Effective AI agents must rely solely on their internal, pre-trained knowledge without using external tools.",
|
||||
"rationale": "The entire project is built around the concept of an agent that repeatedly interacts with its environment using tools.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "Consider the stated ratio of importance between the model and the code in the project's philosophy."
|
||||
},
|
||||
{
|
||||
"question": "In the `v0_bash_agent.py` implementation, how is the concept of a subagent achieved?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "By using a dedicated `Task` tool that spawns a child agent from a registry.",
|
||||
"rationale": "The `Task` tool and an agent registry are features introduced in the more advanced `v3` version, not the minimalist `v0`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By recursively executing the script as a new, isolated process via a `bash` command.",
|
||||
"rationale": "This method leverages process isolation at the operating system level to create a subagent with a fresh context, which is the key insight of the `v0` agent.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "By creating a new thread within the parent process to handle the sub-task.",
|
||||
"rationale": "While a valid programming technique, the `v0` agent uses process spawning, not multithreading, to ensure complete context isolation.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By instantiating a new agent class within the code that maintains a separate history list.",
|
||||
"rationale": "The `v0` implementation is much simpler and relies on an external shell command rather than internal class structures for sub-tasking.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "Think about how this minimalist agent uses its single tool to delegate work."
|
||||
},
|
||||
{
|
||||
"question": "What primary problem is the `TodoWrite` tool in `v2_todo_agent.py` designed to solve for the agent?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "The agent polluting its context window with excessive file content during exploration.",
|
||||
"rationale": "This issue, known as 'context pollution', is the primary problem addressed by the subagent mechanism in `v3`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The agent's inability to write new files or modify existing ones.",
|
||||
"rationale": "File writing and editing capabilities are fundamental tools provided in the `v1` basic agent.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The model losing focus or forgetting the overall plan during complex, multi-step tasks.",
|
||||
"rationale": "The `TodoWrite` tool makes the agent's plan explicit and persistent, addressing 'context fade' and helping it track progress.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "The agent not knowing which type of specialized subagent to use for a task.",
|
||||
"rationale": "The concept of specialized subagents is introduced in `v3`, whereas the `TodoWrite` tool is about managing a single agent's workflow.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "This tool helps make the agent's internal thought process visible and persistent."
|
||||
},
|
||||
{
|
||||
"question": "According to the v3 documentation, what is the main advantage of using subagents?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "It allows the agent to learn new domain-specific expertise from `SKILL.md` files.",
|
||||
"rationale": "Loading expertise from external files is the key feature of the `v4` skills mechanism.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It provides a mechanism for structured planning and tracking task completion.",
|
||||
"rationale": "Structured planning through a visible list is the core concept introduced with the `TodoManager` in `v2`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It enables the agent to run multiple `bash` commands in parallel for faster execution.",
|
||||
"rationale": "While parallelism is mentioned as a possibility, the primary benefit highlighted is the management of the agent's focus.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It isolates the context of a sub-task, preventing the main agent's history from becoming polluted.",
|
||||
"rationale": "This is the central purpose of subagents in `v3`: to handle tasks like codebase exploration in a separate context so the main agent remains focused.",
|
||||
"isCorrect": true
|
||||
}
|
||||
],
|
||||
"hint": "Consider the problem that arises when an agent reads many large files to prepare for a small change."
|
||||
},
|
||||
{
|
||||
"question": "The v4 documentation draws a clear distinction between 'Tools' and 'Skills'. What is this distinction?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "Tools define what an agent *can do* (its capabilities), while Skills define *how* an agent knows to perform a task (its expertise).",
|
||||
"rationale": "This correctly captures the essence of the distinction: Tools are about action/capability, whereas Skills are about knowledge/expertise.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "Tools are built-in functionalities, while Skills are third-party plugins that must be downloaded.",
|
||||
"rationale": "The documentation describes skills as locally stored, human-editable files, not necessarily as plugins from a marketplace.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "Tools are used for general-purpose tasks like reading files, while Skills are only for code generation.",
|
||||
"rationale": "Skills provide domain expertise for a wide range of tasks, such as PDF processing or code review, not just code generation.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "Tools are implemented in Python code, whereas Skills are implemented using shell scripts.",
|
||||
"rationale": "Skills are primarily human-readable Markdown documents that provide instructions; they are not themselves executable scripts.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "Think about the difference between having a hammer and knowing how to build a house."
|
||||
},
|
||||
{
|
||||
"question": "What is the key benefit of the \"Knowledge Externalization\" paradigm introduced in v4?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "It allows the agent's knowledge to be version-controlled, audited, and edited in plain text by anyone, without requiring model training.",
|
||||
"rationale": "This concept democratizes the process of 'teaching' an agent, moving from expensive, expertise-heavy model training to simple document editing.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "It forces the agent to write all its internal thoughts to an external log file for easier debugging.",
|
||||
"rationale": "While logging is a development practice, Knowledge Externalization is about how the agent acquires and uses expertise, not how it logs its actions.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It caches the model's large parameter weights in the local file system to speed up agent initialization.",
|
||||
"rationale": "This describes a form of model caching, which is a different concept from externalizing domain knowledge into editable documents.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "It requires the agent to use external web APIs for all information, preventing it from using outdated internal knowledge.",
|
||||
"rationale": "Knowledge Externalization refers to loading structured expertise from local `SKILL.md` files, not necessarily relying on web APIs.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "This paradigm shift makes customizing an agent's expertise as easy as editing a text document."
|
||||
},
|
||||
{
|
||||
"question": "To maintain cost-efficiency by preserving the KV Cache, how does the `v4_skills_agent.py` inject skill content into the conversation?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "By dynamically updating the system prompt with the new skill information before each model call.",
|
||||
"rationale": "The documentation explicitly identifies this as a costly anti-pattern because modifying the system prompt invalidates the entire cache.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By appending the skill content as a `tool_result` message, which doesn't alter the preceding message history.",
|
||||
"rationale": "This append-only method ensures the prefix of the conversation remains unchanged, allowing the provider to reuse the cached computations.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "By performing a lightweight fine-tuning operation on the model in real-time.",
|
||||
"rationale": "Real-time fine-tuning is computationally expensive and is the 'traditional' approach that the skills mechanism is designed to avoid.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "By storing the skill content in a separate memory buffer that is not part of the model's main context.",
|
||||
"rationale": "For the model to use the knowledge, it must be part of its context; the key is *how* it's added to that context.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "The correct method avoids changing the beginning or middle of the conversation history."
|
||||
},
|
||||
{
|
||||
"question": "Which of these is NOT one of the four essential tools introduced in the `v1_basic_agent.py` to cover most coding use cases?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "`bash`",
|
||||
"rationale": "`bash` is included as the gateway tool for running any command-line operation.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "`read_file`",
|
||||
"rationale": "`read_file` is included as an essential tool for understanding existing code.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "`TodoWrite`",
|
||||
"rationale": "`TodoWrite` is the specialized tool for structured planning introduced in the `v2` agent.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "`edit_file`",
|
||||
"rationale": "`edit_file` is included for making surgical changes to existing files.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "The first version of the agent focused on core capabilities for interacting with the file system and shell, not on complex planning."
|
||||
},
|
||||
{
|
||||
"question": "What is the fundamental logic of the \"core agent loop\" described throughout the project?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "The agent first creates a complete, unchangeable plan and then executes each step in sequence.",
|
||||
"rationale": "The agent loop is iterative and reactive; the result of one tool call informs the model's decision for the next one.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The model makes a tool call, the tool's result is added to the history, and this cycle repeats until the model stops calling tools.",
|
||||
"rationale": "This accurately describes the iterative process where the model is the decision-maker, acting in a loop until the task is complete.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "The user provides a precise sequence of tools for the agent to execute.",
|
||||
"rationale": "In this agent model, the user provides a high-level goal, and the AI model itself decides which tools to use and in what order.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "The agent's code analyzes the prompt and selects a single, optimal tool to resolve the entire request.",
|
||||
"rationale": "The power of the agent comes from its ability to use multiple tools sequentially over several turns, not from picking just one.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "It is a repetitive cycle of thinking, acting, and observing the results."
|
||||
},
|
||||
{
|
||||
"question": "In the `v3` subagent mechanism, what is the purpose of the `AGENT_TYPES` registry?",
|
||||
"answerOptions": [
|
||||
{
|
||||
"text": "To define different subagent archetypes with specific roles, prompts, and restricted toolsets.",
|
||||
"rationale": "This registry allows the main agent to spawn specialized subagents, such as a read-only 'explore' agent or a full-powered 'code' agent.",
|
||||
"isCorrect": true
|
||||
},
|
||||
{
|
||||
"text": "To list all the available skills and their descriptions for the `v4` agent.",
|
||||
"rationale": "The listing of skills is handled by the `SkillLoader` class, which is a concept from `v4`, not `v3`.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "To keep a real-time log of all active subagent processes and their current status.",
|
||||
"rationale": "The registry is a static configuration defining agent types, not a dynamic process manager that tracks running instances.",
|
||||
"isCorrect": false
|
||||
},
|
||||
{
|
||||
"text": "To provide the input schema and validation rules for the `TodoWrite` tool.",
|
||||
"rationale": "A tool's input schema is defined within the tool's own JSON definition, not in the agent type registry.",
|
||||
"isCorrect": false
|
||||
}
|
||||
],
|
||||
"hint": "This configuration allows the main agent to choose the right kind of 'specialist' for a sub-task."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"""Example: Upload files to NotebookLM using native file upload.
|
||||
|
||||
This example demonstrates how to use the native file upload feature
|
||||
to add documents to NotebookLM without local text extraction.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.services import NotebookService, SourceService
|
||||
|
||||
|
||||
async def main():
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
notebook_svc = NotebookService(client)
|
||||
source_svc = SourceService(client)
|
||||
|
||||
notebook = await notebook_svc.create("File Upload Demo")
|
||||
print(f"Created notebook: {notebook.id} - {notebook.title}")
|
||||
|
||||
print("\n1. Uploading a PDF file...")
|
||||
pdf_source = await source_svc.add_file(notebook.id, "research_paper.pdf")
|
||||
print(f" Uploaded: {pdf_source.id} - {pdf_source.title}")
|
||||
|
||||
print("\n2. Uploading a markdown file...")
|
||||
md_source = await source_svc.add_file(
|
||||
notebook.id, "notes.md", mime_type="text/markdown"
|
||||
)
|
||||
print(f" Uploaded: {md_source.id} - {md_source.title}")
|
||||
|
||||
print("\n3. Uploading a text file (auto-detected MIME type)...")
|
||||
txt_source = await source_svc.add_file(notebook.id, "documentation.txt")
|
||||
print(f" Uploaded: {txt_source.id} - {txt_source.title}")
|
||||
|
||||
print("\n4. Uploading a Word document...")
|
||||
docx_source = await source_svc.add_file(
|
||||
notebook.id,
|
||||
"report.docx",
|
||||
mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
)
|
||||
print(f" Uploaded: {docx_source.id} - {docx_source.title}")
|
||||
|
||||
print(f"\nAll files uploaded successfully to notebook {notebook.id}!")
|
||||
|
||||
print("\n5. Querying the notebook...")
|
||||
response = await client.query(
|
||||
notebook.id, "Summarize the key points from all uploaded documents"
|
||||
)
|
||||
print(f"\nAI Response:\n{response['answer']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
"""Example: Generate an Audio Overview (podcast) from URL sources.
|
||||
|
||||
This example demonstrates a complete podcast generation workflow:
|
||||
1. Create a notebook
|
||||
2. Add URL sources
|
||||
3. Generate an audio podcast
|
||||
4. Wait for completion with progress updates
|
||||
5. Download the result
|
||||
|
||||
Prerequisites:
|
||||
- Authentication configured via `notebooklm login` CLI command
|
||||
- Valid Google account with NotebookLM access
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from notebooklm import NotebookLMClient, AudioFormat, AudioLength
|
||||
|
||||
|
||||
async def main():
|
||||
"""Generate a podcast from web sources."""
|
||||
|
||||
# Connect to NotebookLM using stored authentication
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
# Step 1: Create a new notebook for our content
|
||||
print("Creating notebook...")
|
||||
notebook = await client.notebooks.create("AI Research Podcast")
|
||||
print(f"Created notebook: {notebook.id}")
|
||||
|
||||
# Step 2: Add URL sources to the notebook
|
||||
# The AI will use these sources to generate the podcast
|
||||
urls = [
|
||||
"https://en.wikipedia.org/wiki/Artificial_intelligence",
|
||||
"https://en.wikipedia.org/wiki/Machine_learning",
|
||||
]
|
||||
|
||||
print("\nAdding sources...")
|
||||
for url in urls:
|
||||
source = await client.sources.add_url(notebook.id, url)
|
||||
print(f" Added: {source.title or url}")
|
||||
|
||||
# Step 3: Generate the audio overview (podcast)
|
||||
# Options:
|
||||
# audio_format: DEEP_DIVE (default), BRIEF, CRITIQUE, or DEBATE
|
||||
# audio_length: SHORT, DEFAULT, or LONG
|
||||
# instructions: Custom guidance for the AI hosts
|
||||
print("\nStarting podcast generation...")
|
||||
generation = await client.artifacts.generate_audio(
|
||||
notebook.id,
|
||||
audio_format=AudioFormat.DEEP_DIVE,
|
||||
audio_length=AudioLength.DEFAULT,
|
||||
instructions="Focus on practical applications and recent breakthroughs",
|
||||
)
|
||||
print(f"Generation started with task ID: {generation.task_id}")
|
||||
|
||||
# Step 4: Wait for completion with progress polling
|
||||
# This can take 2-5 minutes for a full podcast
|
||||
print("\nWaiting for generation to complete...")
|
||||
print("(This typically takes 2-5 minutes)")
|
||||
|
||||
try:
|
||||
final_status = await client.artifacts.wait_for_completion(
|
||||
notebook.id,
|
||||
generation.task_id,
|
||||
initial_interval=5.0, # Start checking every 5 seconds
|
||||
max_interval=15.0, # Max 15 seconds between checks
|
||||
timeout=600.0, # 10 minute timeout
|
||||
)
|
||||
|
||||
if final_status.is_complete:
|
||||
print("Podcast generation complete!")
|
||||
|
||||
# Step 5: Download the audio file
|
||||
output_path = "ai_podcast.mp4" # Audio is in MP4 container
|
||||
print(f"\nDownloading to {output_path}...")
|
||||
|
||||
await client.artifacts.download_audio(
|
||||
notebook.id,
|
||||
output_path,
|
||||
artifact_id=generation.task_id,
|
||||
)
|
||||
print(f"Downloaded successfully: {output_path}")
|
||||
|
||||
elif final_status.is_failed:
|
||||
print(f"Generation failed: {final_status.error}")
|
||||
|
||||
except TimeoutError:
|
||||
print("Generation timed out - it may still be processing")
|
||||
print("Check the NotebookLM web UI for status")
|
||||
|
||||
# Optional: List all audio artifacts in the notebook
|
||||
print("\nAll audio artifacts:")
|
||||
audios = await client.artifacts.list_audio(notebook.id)
|
||||
for audio in audios:
|
||||
status = "Ready" if audio.is_completed else "Processing"
|
||||
print(f" - {audio.title} ({status})")
|
||||
|
||||
# Cleanup note: The notebook persists after this script ends.
|
||||
# Delete it manually or use: await client.notebooks.delete(notebook.id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
"""Example: Use the Research API to discover and import sources.
|
||||
|
||||
This example demonstrates the research workflow:
|
||||
1. Start a research session (fast or deep mode)
|
||||
2. Poll for completion
|
||||
3. Review discovered sources
|
||||
4. Import selected sources into the notebook
|
||||
|
||||
The Research API searches the web or Google Drive for relevant sources
|
||||
based on your query, providing AI-curated results.
|
||||
|
||||
Prerequisites:
|
||||
- Authentication configured via `notebooklm login` CLI command
|
||||
- Valid Google account with NotebookLM access
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from notebooklm import NotebookLMClient
|
||||
|
||||
|
||||
async def main():
|
||||
"""Demonstrate web research and source import."""
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
# Create a notebook for the research
|
||||
print("Creating notebook...")
|
||||
notebook = await client.notebooks.create("Climate Research")
|
||||
print(f"Created notebook: {notebook.id}")
|
||||
|
||||
# =====================================================================
|
||||
# Fast Research Mode
|
||||
# =====================================================================
|
||||
# Fast research returns results quickly (10-30 seconds)
|
||||
# Best for getting a quick overview of available sources
|
||||
|
||||
print("\n--- Fast Research Mode ---")
|
||||
print("Starting fast web research...")
|
||||
|
||||
task = await client.research.start(
|
||||
notebook.id,
|
||||
query="climate change mitigation strategies",
|
||||
source="web", # "web" or "drive"
|
||||
mode="fast", # "fast" or "deep"
|
||||
)
|
||||
|
||||
if not task:
|
||||
print("Failed to start research")
|
||||
return
|
||||
|
||||
print(f"Research task started: {task['task_id']}")
|
||||
|
||||
# Poll for results
|
||||
print("Polling for results...")
|
||||
max_attempts = 30
|
||||
for attempt in range(max_attempts):
|
||||
result = await client.research.poll(notebook.id)
|
||||
|
||||
if result["status"] == "completed":
|
||||
print(
|
||||
f"\nResearch complete! Found {len(result.get('sources', []))} sources"
|
||||
)
|
||||
break
|
||||
elif result["status"] == "in_progress":
|
||||
print(f" Still searching... (attempt {attempt + 1})")
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
print(f" Status: {result['status']}")
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
print("Research timed out")
|
||||
return
|
||||
|
||||
# Display discovered sources
|
||||
sources = result.get("sources", [])
|
||||
print("\nDiscovered sources:")
|
||||
for i, src in enumerate(sources[:10], 1): # Show first 10
|
||||
title = src.get("title", "Untitled")
|
||||
url = src.get("url", "")
|
||||
print(f" {i}. {title}")
|
||||
if url:
|
||||
print(f" {url[:60]}...")
|
||||
|
||||
# Display AI summary if available
|
||||
summary = result.get("summary", "")
|
||||
if summary:
|
||||
print(f"\nAI Summary:\n{summary[:500]}...")
|
||||
|
||||
# Import selected sources (first 3 for this example)
|
||||
sources_to_import = sources[:3]
|
||||
if sources_to_import:
|
||||
print(f"\nImporting {len(sources_to_import)} sources...")
|
||||
imported = await client.research.import_sources(
|
||||
notebook.id,
|
||||
task["task_id"],
|
||||
sources_to_import,
|
||||
)
|
||||
|
||||
print("Imported sources:")
|
||||
for src in imported:
|
||||
print(f" - {src['title']} (ID: {src['id']})")
|
||||
|
||||
# =====================================================================
|
||||
# Deep Research Mode (Web only)
|
||||
# =====================================================================
|
||||
# Deep research takes longer (1-3 minutes) but provides:
|
||||
# - More comprehensive source discovery
|
||||
# - Detailed analysis and synthesis
|
||||
# - Higher quality source recommendations
|
||||
|
||||
print("\n--- Deep Research Mode ---")
|
||||
print("Starting deep web research...")
|
||||
|
||||
deep_task = await client.research.start(
|
||||
notebook.id,
|
||||
query="renewable energy policy effectiveness",
|
||||
source="web",
|
||||
mode="deep", # Deep mode for thorough research
|
||||
)
|
||||
|
||||
if not deep_task:
|
||||
print("Failed to start deep research")
|
||||
return
|
||||
|
||||
print(f"Deep research task started: {deep_task['task_id']}")
|
||||
print("Deep research takes 1-3 minutes...")
|
||||
|
||||
# Poll with longer intervals for deep research
|
||||
max_attempts = 60
|
||||
for attempt in range(max_attempts):
|
||||
result = await client.research.poll(notebook.id)
|
||||
|
||||
if result["status"] == "completed":
|
||||
print(f"\nDeep research complete!")
|
||||
sources = result.get("sources", [])
|
||||
print(f"Found {len(sources)} sources")
|
||||
|
||||
# Deep research often includes more detailed summaries
|
||||
if result.get("summary"):
|
||||
print(f"\nResearch synthesis:\n{result['summary'][:800]}...")
|
||||
break
|
||||
elif result["status"] == "in_progress":
|
||||
if attempt % 5 == 0: # Log every 5 attempts
|
||||
print(f" Deep analysis in progress... ({attempt * 3}s)")
|
||||
await asyncio.sleep(3)
|
||||
else:
|
||||
print("Deep research timed out")
|
||||
|
||||
# Verify final notebook contents
|
||||
print("\n--- Final Notebook Sources ---")
|
||||
all_sources = await client.sources.list(notebook.id)
|
||||
for src in all_sources:
|
||||
print(f" - {src.title} ({src.source_type})")
|
||||
|
||||
print(f"\nTotal sources in notebook: {len(all_sources)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to try ACT_ON_SOURCES for quiz/flashcard content."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod
|
||||
|
||||
|
||||
async def investigate_act_on_sources():
|
||||
"""Try ACT_ON_SOURCES RPC with different action types."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
flashcard_id = "173255d8-12b3-4c67-b925-a76ce6c71735"
|
||||
|
||||
# Get source IDs from the notebook
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
source_ids = await client._core.get_source_ids(notebook_id)
|
||||
print(f"Notebook has {len(source_ids)} sources")
|
||||
source_ids_nested = [[[sid]] for sid in source_ids]
|
||||
|
||||
# Actions to try - based on what we know works for mind maps
|
||||
actions_to_try = [
|
||||
"get_quiz",
|
||||
"get_flashcards",
|
||||
"quiz",
|
||||
"flashcard",
|
||||
"flashcards",
|
||||
"quiz_content",
|
||||
"flashcard_content",
|
||||
"get_artifact_content",
|
||||
]
|
||||
|
||||
for action in actions_to_try:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Trying action: {action}")
|
||||
|
||||
params = [
|
||||
source_ids_nested,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
[action, [["[CONTEXT]", ""]], ""],
|
||||
None,
|
||||
[2, None, [1]],
|
||||
]
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.ACT_ON_SOURCES,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f" SUCCESS! Type: {type(result)}")
|
||||
print(f" Preview: {str(result)[:500]}...")
|
||||
with open(f"investigation_output/act_on_sources_{action}.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
else:
|
||||
print(" No result (None)")
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
# Also try with artifact ID included
|
||||
print(f"\n\n{'='*60}")
|
||||
print("Trying with artifact ID in params...")
|
||||
|
||||
artifact_params = [
|
||||
source_ids_nested,
|
||||
flashcard_id, # Include artifact ID
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
["get_content", [["[CONTEXT]", ""]], ""],
|
||||
None,
|
||||
[2, None, [1]],
|
||||
]
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.ACT_ON_SOURCES,
|
||||
artifact_params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
if result:
|
||||
print(f" SUCCESS! {str(result)[:500]}...")
|
||||
else:
|
||||
print(" No result (None)")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_act_on_sources())
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to examine each artifact position in detail."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod
|
||||
|
||||
|
||||
async def investigate_artifact_positions():
|
||||
"""Examine each position in flashcard/quiz artifact data in detail."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
# Get raw artifacts data
|
||||
params = [[2], notebook_id, 'NOT artifact.status = "ARTIFACT_STATUS_SUGGESTED"']
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.LIST_ARTIFACTS,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if not result:
|
||||
print("No artifacts found")
|
||||
return
|
||||
|
||||
artifacts_data = result[0] if isinstance(result[0], list) else result
|
||||
|
||||
# Find flashcard artifact
|
||||
flashcard_art = None
|
||||
quiz_art = None
|
||||
|
||||
for art in artifacts_data:
|
||||
if isinstance(art, list) and len(art) > 2 and art[2] == 4:
|
||||
variant = None
|
||||
if len(art) > 9 and isinstance(art[9], list) and len(art[9]) > 1:
|
||||
if isinstance(art[9][1], list) and len(art[9][1]) > 0:
|
||||
variant = art[9][1][0]
|
||||
if variant == 1:
|
||||
flashcard_art = art
|
||||
elif variant == 2:
|
||||
quiz_art = art
|
||||
|
||||
if flashcard_art:
|
||||
print("=" * 80)
|
||||
print("FLASHCARD ARTIFACT STRUCTURE")
|
||||
print("=" * 80)
|
||||
print(f"\nTitle: {flashcard_art[1]}")
|
||||
print(f"Total length: {len(flashcard_art)} positions")
|
||||
print()
|
||||
|
||||
for i, item in enumerate(flashcard_art):
|
||||
print(f"\n[{i}] ", end="")
|
||||
if item is None:
|
||||
print("None")
|
||||
elif isinstance(item, str):
|
||||
if len(item) > 100:
|
||||
print(f"String ({len(item)} chars): {item[:100]}...")
|
||||
else:
|
||||
print(f"String: {repr(item)}")
|
||||
elif isinstance(item, (int, float, bool)):
|
||||
print(f"{type(item).__name__}: {item}")
|
||||
elif isinstance(item, list):
|
||||
print(f"List[{len(item)}]:")
|
||||
# Show nested structure
|
||||
for j, sub in enumerate(item[:5]): # First 5 items
|
||||
if isinstance(sub, list):
|
||||
print(f" [{j}] List[{len(sub)}]: {str(sub)[:100]}...")
|
||||
elif isinstance(sub, str):
|
||||
print(f" [{j}] String: {repr(sub[:50]) if len(sub) > 50 else repr(sub)}...")
|
||||
else:
|
||||
print(f" [{j}] {type(sub).__name__}: {str(sub)[:100]}")
|
||||
if len(item) > 5:
|
||||
print(f" ... and {len(item) - 5} more items")
|
||||
elif isinstance(item, dict):
|
||||
print(f"Dict[{len(item)}]: {list(item.keys())[:5]}...")
|
||||
|
||||
# Also check if there's a quiz with more data
|
||||
if quiz_art:
|
||||
print("\n" + "=" * 80)
|
||||
print("QUIZ ARTIFACT STRUCTURE")
|
||||
print("=" * 80)
|
||||
print(f"\nTitle: {quiz_art[1]}")
|
||||
|
||||
# Look specifically at positions that might hold content
|
||||
interesting_positions = [6, 7, 8, 9, 10, 11, 12, 13]
|
||||
for i in interesting_positions:
|
||||
if i < len(quiz_art) and quiz_art[i] is not None:
|
||||
item = quiz_art[i]
|
||||
print(f"\n[{i}] ", end="")
|
||||
if isinstance(item, list):
|
||||
print(f"List[{len(item)}]:")
|
||||
# Deeper exploration
|
||||
print(f" Full content: {json.dumps(item, indent=2, default=str)[:500]}...")
|
||||
else:
|
||||
print(f"{type(item).__name__}: {item}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_artifact_positions())
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to try EXPORT_ARTIFACT for quiz/flashcard content."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod, ExportType
|
||||
|
||||
|
||||
async def investigate_export():
|
||||
"""Try EXPORT_ARTIFACT RPC for quiz/flashcard content."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
flashcard_id = "173255d8-12b3-4c67-b925-a76ce6c71735"
|
||||
quiz_id = "a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767"
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
print("Testing EXPORT_ARTIFACT (Krh3pd) for flashcards...")
|
||||
|
||||
# Try export to Docs
|
||||
params = [None, flashcard_id, None, "Flashcard Export", 1] # ExportType.DOCS = 1
|
||||
print(f"\nParams: {params}")
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.EXPORT_ARTIFACT,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"Got result! Type: {type(result)}")
|
||||
print(f"Result: {json.dumps(result, indent=2, default=str)[:1000]}")
|
||||
with open("investigation_output/export_flashcard_result.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
else:
|
||||
print("No result (None)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
print("\n\nTesting EXPORT_ARTIFACT for quiz...")
|
||||
params = [None, quiz_id, None, "Quiz Export", 1]
|
||||
print(f"Params: {params}")
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.EXPORT_ARTIFACT,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"Got result! Type: {type(result)}")
|
||||
print(f"Result: {json.dumps(result, indent=2, default=str)[:1000]}")
|
||||
with open("investigation_output/export_quiz_result.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
else:
|
||||
print("No result (None)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_export())
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to analyze flashcard and quiz artifact structures.
|
||||
|
||||
This script:
|
||||
1. Lists all type 4 (quiz/flashcard) artifacts in a notebook
|
||||
2. Dumps the raw API response for analysis
|
||||
3. Identifies where content/download URLs might be stored
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path for development
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod
|
||||
|
||||
|
||||
def deep_search(obj, depth=0, path="", results=None):
|
||||
"""Recursively search for string content that might be flashcard/quiz data."""
|
||||
if results is None:
|
||||
results = []
|
||||
|
||||
max_depth = 20
|
||||
if depth > max_depth:
|
||||
return results
|
||||
|
||||
if isinstance(obj, str):
|
||||
# Look for JSON-like content
|
||||
if len(obj) > 50 and ("{" in obj or "[" in obj):
|
||||
results.append((path, "potential_json", obj[:200] + "..." if len(obj) > 200 else obj))
|
||||
# Look for URLs
|
||||
elif obj.startswith("http"):
|
||||
results.append((path, "url", obj))
|
||||
elif isinstance(obj, list):
|
||||
for i, item in enumerate(obj):
|
||||
deep_search(item, depth + 1, f"{path}[{i}]", results)
|
||||
elif isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
deep_search(v, depth + 1, f"{path}.{k}", results)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def investigate_notebook(notebook_id: str | None = None):
|
||||
"""Investigate flashcard and quiz artifact structures in a notebook."""
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
# Get list of notebooks if no ID provided
|
||||
if not notebook_id:
|
||||
notebooks = await client.notebooks.list()
|
||||
if not notebooks:
|
||||
print("No notebooks found")
|
||||
return
|
||||
|
||||
print("Available notebooks:")
|
||||
for i, nb in enumerate(notebooks):
|
||||
print(f" {i+1}. {nb.title} ({nb.id})")
|
||||
|
||||
# Use first notebook
|
||||
notebook_id = notebooks[0].id
|
||||
print(f"\nUsing notebook: {notebooks[0].title}")
|
||||
|
||||
# Get raw artifacts data
|
||||
params = [[2], notebook_id, 'NOT artifact.status = "ARTIFACT_STATUS_SUGGESTED"']
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.LIST_ARTIFACTS,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if not result or not isinstance(result, list):
|
||||
print("No artifacts found")
|
||||
return
|
||||
|
||||
artifacts_data = result[0] if isinstance(result[0], list) else result
|
||||
|
||||
# Find type 4 (quiz/flashcard) artifacts
|
||||
type4_artifacts = [
|
||||
a for a in artifacts_data
|
||||
if isinstance(a, list) and len(a) > 2 and a[2] == 4
|
||||
]
|
||||
|
||||
if not type4_artifacts:
|
||||
print("No quiz/flashcard artifacts found (type 4)")
|
||||
print(f"\nFound {len(artifacts_data)} total artifacts")
|
||||
for art in artifacts_data:
|
||||
if isinstance(art, list) and len(art) > 2:
|
||||
print(f" - Type {art[2]}: {art[1] if len(art) > 1 else 'unknown'}")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(type4_artifacts)} quiz/flashcard artifacts:")
|
||||
|
||||
for i, art in enumerate(type4_artifacts):
|
||||
# Determine variant
|
||||
variant = None
|
||||
if len(art) > 9 and isinstance(art[9], list) and len(art[9]) > 1:
|
||||
if isinstance(art[9][1], list) and len(art[9][1]) > 0:
|
||||
variant = art[9][1][0]
|
||||
|
||||
variant_name = "flashcards" if variant == 1 else "quiz" if variant == 2 else "unknown"
|
||||
title = art[1] if len(art) > 1 else "untitled"
|
||||
status = art[4] if len(art) > 4 else "unknown"
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Artifact {i+1}: {title}")
|
||||
print(f" Type: {variant_name} (variant={variant})")
|
||||
print(f" Status: {status} (3=completed)")
|
||||
print(f" ID: {art[0]}")
|
||||
print(f" Array length: {len(art)}")
|
||||
|
||||
# Dump structure overview
|
||||
print(f"\n Structure overview:")
|
||||
for idx, item in enumerate(art):
|
||||
if item is None:
|
||||
item_repr = "None"
|
||||
elif isinstance(item, str):
|
||||
item_repr = f"str[{len(item)}]: {item[:50]}..." if len(item) > 50 else f"str: {item}"
|
||||
elif isinstance(item, list):
|
||||
item_repr = f"list[{len(item)}]"
|
||||
elif isinstance(item, dict):
|
||||
item_repr = f"dict[{len(item)}]"
|
||||
else:
|
||||
item_repr = f"{type(item).__name__}: {item}"
|
||||
print(f" [{idx}] {item_repr}")
|
||||
|
||||
# Deep search for interesting content
|
||||
interesting = deep_search(art)
|
||||
if interesting:
|
||||
print(f"\n Interesting content found:")
|
||||
for path, content_type, content in interesting[:20]: # Limit output
|
||||
print(f" {path} ({content_type}): {content[:100]}...")
|
||||
|
||||
# Save full artifact data
|
||||
output_dir = Path("investigation_output")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
filename = f"artifact_{variant_name}_{i+1}.json"
|
||||
with open(output_dir / filename, "w") as f:
|
||||
json.dump(art, f, indent=2, default=str)
|
||||
print(f"\n Full data saved to: {output_dir / filename}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point."""
|
||||
notebook_id = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
await investigate_notebook(notebook_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to try fetching individual artifact content.
|
||||
|
||||
Tests the GET_ARTIFACT RPC (BnLyuf) to see if it returns quiz/flashcard questions.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod
|
||||
|
||||
|
||||
async def investigate_get_artifact():
|
||||
"""Try GET_ARTIFACT RPC to fetch flashcard/quiz content."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91" # High School notebook
|
||||
flashcard_id = "173255d8-12b3-4c67-b925-a76ce6c71735" # Agent Flashcards
|
||||
quiz_id = "a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767" # Agent Quiz
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
print("Testing GET_ARTIFACT RPC (BnLyuf)...")
|
||||
|
||||
# Try different parameter formats
|
||||
param_formats = [
|
||||
("Format 1: [id]", [flashcard_id]),
|
||||
("Format 2: [[id]]", [[flashcard_id]]),
|
||||
("Format 3: [nb_id, id]", [notebook_id, flashcard_id]),
|
||||
("Format 4: [id, nb_id]", [flashcard_id, notebook_id]),
|
||||
("Format 5: [[2], id]", [[2], flashcard_id]),
|
||||
("Format 6: [[2], nb_id, id]", [[2], notebook_id, flashcard_id]),
|
||||
]
|
||||
|
||||
for desc, params in param_formats:
|
||||
print(f"\n{desc}: {params}")
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.GET_ARTIFACT,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
if result:
|
||||
print(f" SUCCESS! Result type: {type(result)}")
|
||||
if isinstance(result, list):
|
||||
print(f" Length: {len(result)}")
|
||||
# Save result
|
||||
with open("investigation_output/get_artifact_result.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
print(" Saved to get_artifact_result.json")
|
||||
return result
|
||||
else:
|
||||
print(f" Result: {result}")
|
||||
else:
|
||||
print(" No result (None)")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
# Also try LIST_ARTIFACTS_ALT
|
||||
print("\n\nTesting LIST_ARTIFACTS_ALT RPC (LfTXoe)...")
|
||||
alt_param_formats = [
|
||||
("Format 1: [nb_id]", [notebook_id]),
|
||||
("Format 2: [[2], nb_id]", [[2], notebook_id]),
|
||||
("Format 3: [nb_id, id]", [notebook_id, flashcard_id]),
|
||||
]
|
||||
|
||||
for desc, params in alt_param_formats:
|
||||
print(f"\n{desc}: {params}")
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.LIST_ARTIFACTS_ALT,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
if result:
|
||||
print(f" SUCCESS! Result type: {type(result)}")
|
||||
if isinstance(result, list):
|
||||
print(f" Length: {len(result)}")
|
||||
with open("investigation_output/list_artifacts_alt_result.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
print(" Saved to list_artifacts_alt_result.json")
|
||||
else:
|
||||
print(f" Result: {result}")
|
||||
else:
|
||||
print(" No result (None)")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_get_artifact())
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to check GET_NOTEBOOK for embedded flashcard/quiz data."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod
|
||||
|
||||
|
||||
def find_content(obj, depth=0, path="", max_depth=25):
|
||||
"""Recursively search for flashcard/quiz-like content."""
|
||||
if depth > max_depth:
|
||||
return []
|
||||
|
||||
results = []
|
||||
|
||||
if isinstance(obj, str):
|
||||
lower = obj.lower()
|
||||
# Look for quiz/flashcard-related strings
|
||||
if any(kw in lower for kw in ["question", "answer", "correct", "option", "front", "back"]):
|
||||
if len(obj) > 20: # Skip short strings
|
||||
results.append((path, obj[:200]))
|
||||
# Look for JSON content
|
||||
if obj.startswith("{") or obj.startswith("["):
|
||||
try:
|
||||
parsed = json.loads(obj)
|
||||
if isinstance(parsed, (dict, list)) and len(str(parsed)) > 100:
|
||||
results.append((path + " (JSON)", str(parsed)[:300]))
|
||||
except:
|
||||
pass
|
||||
elif isinstance(obj, list):
|
||||
for i, item in enumerate(obj):
|
||||
results.extend(find_content(item, depth + 1, f"{path}[{i}]"))
|
||||
elif isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
results.extend(find_content(v, depth + 1, f"{path}.{k}"))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def investigate_notebook_detail():
|
||||
"""Fetch notebook detail and look for quiz/flashcard content."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
print("Fetching GET_NOTEBOOK...")
|
||||
|
||||
params = [[notebook_id]]
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.GET_NOTEBOOK,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"Got notebook data, length: {len(str(result))} chars")
|
||||
|
||||
# Save full response
|
||||
with open("investigation_output/notebook_detail.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
print("Saved to investigation_output/notebook_detail.json")
|
||||
|
||||
# Search for quiz/flashcard content
|
||||
print("\nSearching for quiz/flashcard content...")
|
||||
findings = find_content(result)
|
||||
if findings:
|
||||
print(f"\nFound {len(findings)} potential matches:")
|
||||
for path, content in findings[:30]:
|
||||
print(f"\n {path}:")
|
||||
print(f" {content}")
|
||||
else:
|
||||
print("No quiz/flashcard keywords found in notebook data")
|
||||
else:
|
||||
print("No result from GET_NOTEBOOK")
|
||||
|
||||
# Also try to fetch conversation history - maybe quiz state is stored there
|
||||
print("\n\nFetching GET_CONVERSATION_HISTORY...")
|
||||
try:
|
||||
params = [notebook_id]
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.GET_CONVERSATION_HISTORY,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
if result:
|
||||
print(f"Got conversation history, length: {len(str(result))} chars")
|
||||
with open("investigation_output/conversation_history.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
print("Saved to investigation_output/conversation_history.json")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_notebook_detail())
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to try POLL_STUDIO for detailed artifact data."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod
|
||||
|
||||
|
||||
async def investigate_poll_studio():
|
||||
"""Try POLL_STUDIO RPC with different parameter formats."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
flashcard_id = "173255d8-12b3-4c67-b925-a76ce6c71735"
|
||||
quiz_id = "a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767"
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
print("Testing POLL_STUDIO RPC (gArtLc) for flashcards...")
|
||||
|
||||
# Standard format from existing code
|
||||
params = [flashcard_id, notebook_id, [2]]
|
||||
print(f"\nParams: {params}")
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.POLL_STUDIO,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"Got result! Type: {type(result)}, Length: {len(result) if isinstance(result, (list, dict, str)) else 'N/A'}")
|
||||
with open("investigation_output/poll_studio_flashcard.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
print("Saved to poll_studio_flashcard.json")
|
||||
print(f"\nResult preview: {str(result)[:500]}...")
|
||||
else:
|
||||
print("No result (None)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
print("\n\nTesting POLL_STUDIO RPC for quiz...")
|
||||
params = [quiz_id, notebook_id, [2]]
|
||||
print(f"Params: {params}")
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.POLL_STUDIO,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"Got result! Type: {type(result)}, Length: {len(result) if isinstance(result, (list, dict, str)) else 'N/A'}")
|
||||
with open("investigation_output/poll_studio_quiz.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str)
|
||||
print("Saved to poll_studio_quiz.json")
|
||||
print(f"\nResult preview: {str(result)[:500]}...")
|
||||
else:
|
||||
print("No result (None)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Try with different modes/flags
|
||||
print("\n\nTrying with extended params...")
|
||||
extended_params = [
|
||||
[flashcard_id, notebook_id, [2], None, True],
|
||||
[flashcard_id, notebook_id, [2], 1],
|
||||
[[2], flashcard_id, notebook_id],
|
||||
]
|
||||
|
||||
for params in extended_params:
|
||||
print(f"\nParams: {params}")
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.POLL_STUDIO,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
if result:
|
||||
print(f" SUCCESS! {str(result)[:200]}...")
|
||||
else:
|
||||
print(" No result")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_poll_studio())
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to try fetching quiz/flashcard content via chat-like requests."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
from notebooklm.rpc import RPCMethod
|
||||
|
||||
|
||||
async def investigate_quiz_via_chat():
|
||||
"""Try to fetch quiz/flashcard content through various approaches."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
flashcard_id = "173255d8-12b3-4c67-b925-a76ce6c71735"
|
||||
quiz_id = "a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767"
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
# First let's check if the chat API can retrieve quiz content
|
||||
print("Attempting to retrieve quiz content via chat...\n")
|
||||
|
||||
# Try using an artifact-related prompt through chat.ask()
|
||||
try:
|
||||
print("Testing: Ask about flashcard content")
|
||||
result = await client.chat.ask(
|
||||
notebook_id,
|
||||
"Show me the flashcards from this notebook"
|
||||
)
|
||||
print(f"Response: {result.answer[:500]}...")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}\n")
|
||||
|
||||
# Try to see if there's a specific RPC for getting artifact content
|
||||
# Let's look at what methods are available in CREATE_ARTIFACT / GET_ARTIFACT style
|
||||
print("\nTrying CREATE_VIDEO RPC with artifact ID to get content...")
|
||||
|
||||
# The xpWGLf RPC is CREATE_ARTIFACT - maybe it has a "get" mode
|
||||
try:
|
||||
params = [
|
||||
[2],
|
||||
notebook_id,
|
||||
[
|
||||
flashcard_id, # Try passing existing artifact ID
|
||||
None,
|
||||
4, # Type 4 = quiz/flashcard
|
||||
]
|
||||
]
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.CREATE_ARTIFACT,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
if result:
|
||||
print(f"CREATE_ARTIFACT result: {json.dumps(result, indent=2, default=str)[:500]}")
|
||||
else:
|
||||
print("No result")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Check the streaming endpoint format - maybe quiz content comes through there
|
||||
print("\n\nChecking if quiz/flashcard has special rendering requirements...")
|
||||
|
||||
# Get all artifacts and look more carefully at type 4
|
||||
params = [[2], notebook_id, 'NOT artifact.status = "ARTIFACT_STATUS_SUGGESTED"']
|
||||
result = await client._core.rpc_call(
|
||||
RPCMethod.LIST_ARTIFACTS,
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
artifacts_data = result[0] if isinstance(result[0], list) else result
|
||||
|
||||
for art in artifacts_data:
|
||||
if isinstance(art, list) and len(art) > 2 and art[2] == 4:
|
||||
print(f"\nType 4 artifact: {art[1]}")
|
||||
print(f" Full raw data ({len(art)} elements):")
|
||||
# Save complete data for offline analysis
|
||||
with open(f"investigation_output/type4_full_{art[0][:8]}.json", "w") as f:
|
||||
json.dump(art, f, indent=2, default=str)
|
||||
print(f" Saved to investigation_output/type4_full_{art[0][:8]}.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_quiz_via_chat())
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Investigation script to test the v9rmvd RPC for quiz/flashcard content."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from notebooklm import NotebookLMClient
|
||||
|
||||
|
||||
async def investigate_v9rmvd():
|
||||
"""Test the v9rmvd RPC to fetch quiz/flashcard content."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
quiz_id = "a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767" # Agent Quiz
|
||||
flashcard_id = "173255d8-12b3-4c67-b925-a76ce6c71735" # Agent Flashcards
|
||||
|
||||
async with await NotebookLMClient.from_storage() as client:
|
||||
print("Testing v9rmvd RPC for quiz content...")
|
||||
print(f"Quiz ID: {quiz_id}")
|
||||
|
||||
# Based on the curl, the params are just [artifact_id]
|
||||
params = [quiz_id]
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
"v9rmvd", # New RPC ID we discovered!
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"\n✓ SUCCESS! Got quiz content!")
|
||||
print(f"Result type: {type(result)}")
|
||||
if isinstance(result, list):
|
||||
print(f"Result length: {len(result)}")
|
||||
|
||||
# Save full result
|
||||
with open("investigation_output/quiz_content_v9rmvd.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str, ensure_ascii=False)
|
||||
print("\nFull result saved to: investigation_output/quiz_content_v9rmvd.json")
|
||||
|
||||
# Preview the content
|
||||
print(f"\nResult preview:\n{json.dumps(result, indent=2, default=str, ensure_ascii=False)[:2000]}")
|
||||
else:
|
||||
print("No result (None)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("Testing v9rmvd RPC for flashcard content...")
|
||||
print(f"Flashcard ID: {flashcard_id}")
|
||||
|
||||
params = [flashcard_id]
|
||||
|
||||
try:
|
||||
result = await client._core.rpc_call(
|
||||
"v9rmvd",
|
||||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"\n✓ SUCCESS! Got flashcard content!")
|
||||
print(f"Result type: {type(result)}")
|
||||
|
||||
# Save full result
|
||||
with open("investigation_output/flashcard_content_v9rmvd.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str, ensure_ascii=False)
|
||||
print("\nFull result saved to: investigation_output/flashcard_content_v9rmvd.json")
|
||||
|
||||
# Preview
|
||||
print(f"\nResult preview:\n{json.dumps(result, indent=2, default=str, ensure_ascii=False)[:2000]}")
|
||||
else:
|
||||
print("No result (None)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(investigate_v9rmvd())
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Direct HTTP call to test v9rmvd RPC."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlencode, quote
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
import httpx
|
||||
from notebooklm.auth import AuthTokens
|
||||
from notebooklm.rpc.decoder import decode_response
|
||||
|
||||
BATCHEXECUTE_URL = "https://notebooklm.google.com/_/LabsTailwindUi/data/batchexecute"
|
||||
|
||||
|
||||
def encode_rpc_manual(rpc_id: str, params: list) -> str:
|
||||
"""Manually encode RPC request."""
|
||||
params_json = json.dumps(params)
|
||||
inner = [rpc_id, params_json, None, "generic"]
|
||||
outer = [[inner]]
|
||||
encoded = json.dumps(outer, separators=(",", ":"))
|
||||
return f"f.req={quote(encoded)}"
|
||||
|
||||
|
||||
async def test_v9rmvd():
|
||||
"""Make direct HTTP call to v9rmvd RPC."""
|
||||
|
||||
notebook_id = "167481cd-23a3-4331-9a45-c8948900bf91"
|
||||
quiz_id = "a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767"
|
||||
flashcard_id = "173255d8-12b3-4c67-b925-a76ce6c71735"
|
||||
|
||||
# Load auth
|
||||
auth = await AuthTokens.from_storage()
|
||||
|
||||
# Build URL
|
||||
url_params = {
|
||||
"rpcids": "v9rmvd",
|
||||
"source-path": f"/notebook/{notebook_id}",
|
||||
"f.sid": auth.session_id,
|
||||
"hl": "en",
|
||||
"_reqid": "123456",
|
||||
"rt": "c",
|
||||
}
|
||||
url = f"{BATCHEXECUTE_URL}?{urlencode(url_params)}"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
"X-Same-Domain": "1",
|
||||
"Referer": "https://notebooklm.google.com/",
|
||||
}
|
||||
|
||||
cookies = httpx.Cookies()
|
||||
for name, value in auth.cookies.items():
|
||||
cookies.set(name, value, domain="notebooklm.google.com")
|
||||
|
||||
async with httpx.AsyncClient(cookies=cookies, follow_redirects=True) as client:
|
||||
print("="*60)
|
||||
print("Testing QUIZ content...")
|
||||
print(f"Quiz ID: {quiz_id}")
|
||||
|
||||
# Build request body - params is [artifact_id]
|
||||
params = [quiz_id]
|
||||
body = encode_rpc_manual("v9rmvd", params)
|
||||
body += f"&at={auth.csrf_token}"
|
||||
|
||||
print(f"URL: {url}")
|
||||
print(f"Body: {body[:200]}...")
|
||||
|
||||
response = await client.post(url, content=body, headers=headers)
|
||||
print(f"Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
result = decode_response(response.text, "v9rmvd", allow_null=True)
|
||||
print(f"\n✓ SUCCESS!")
|
||||
print(f"Result type: {type(result)}")
|
||||
|
||||
# Save full result
|
||||
with open("investigation_output/quiz_content_v9rmvd.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str, ensure_ascii=False)
|
||||
print("Saved to: investigation_output/quiz_content_v9rmvd.json")
|
||||
|
||||
# Pretty print preview
|
||||
print(f"\nPreview:\n{json.dumps(result, indent=2, ensure_ascii=False)[:3000]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Decode error: {e}")
|
||||
print(f"Raw response: {response.text[:2000]}")
|
||||
else:
|
||||
print(f"Error: {response.text[:500]}")
|
||||
|
||||
# Test flashcard
|
||||
print("\n" + "="*60)
|
||||
print("Testing FLASHCARD content...")
|
||||
print(f"Flashcard ID: {flashcard_id}")
|
||||
|
||||
params = [flashcard_id]
|
||||
body = encode_rpc_manual("v9rmvd", params)
|
||||
body += f"&at={auth.csrf_token}"
|
||||
|
||||
response = await client.post(url, content=body, headers=headers)
|
||||
print(f"Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
result = decode_response(response.text, "v9rmvd", allow_null=True)
|
||||
print(f"\n✓ SUCCESS!")
|
||||
|
||||
with open("investigation_output/flashcard_content_v9rmvd.json", "w") as f:
|
||||
json.dump(result, f, indent=2, default=str, ensure_ascii=False)
|
||||
print("Saved to: investigation_output/flashcard_content_v9rmvd.json")
|
||||
|
||||
print(f"\nPreview:\n{json.dumps(result, indent=2, ensure_ascii=False)[:3000]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Decode error: {e}")
|
||||
print(f"Raw response: {response.text[:2000]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_v9rmvd())
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
[
|
||||
"173255d8-12b3-4c67-b925-a76ce6c71735",
|
||||
"Agent Flashcards",
|
||||
4,
|
||||
[
|
||||
[
|
||||
[
|
||||
"a474cd35-6c21-4e72-94a0-c38b5491b449"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"735fcfef-9fbd-4c89-9789-6a9760587bec"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"4d3f7b07-e9e6-43d3-ab8b-184fa27a9f1e"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"a5ec927b-12eb-45d1-989f-12eb3db4ce53"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"c361a555-5c2d-42e2-94d0-a65da95be660"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"48f71a82-08d3-46fa-a37f-d657fb2f0723"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"d6ce2ec3-f98a-4529-acd5-08bff271cb3b"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"ef358221-3904-4dbc-be6f-e1e8dea63954"
|
||||
]
|
||||
]
|
||||
],
|
||||
3,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
"",
|
||||
[
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
"en",
|
||||
null,
|
||||
null,
|
||||
[
|
||||
2,
|
||||
2
|
||||
],
|
||||
null,
|
||||
true
|
||||
]
|
||||
],
|
||||
[
|
||||
1768311102,
|
||||
683814000
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
1768311058,
|
||||
331340000
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1
|
||||
]
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
[
|
||||
"a0e4dca6-3bb0-4ed0-aea1-dfa91cf89767",
|
||||
"Agent Quiz",
|
||||
4,
|
||||
[
|
||||
[
|
||||
[
|
||||
"a474cd35-6c21-4e72-94a0-c38b5491b449"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"735fcfef-9fbd-4c89-9789-6a9760587bec"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"4d3f7b07-e9e6-43d3-ab8b-184fa27a9f1e"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"a5ec927b-12eb-45d1-989f-12eb3db4ce53"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"c361a555-5c2d-42e2-94d0-a65da95be660"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"48f71a82-08d3-46fa-a37f-d657fb2f0723"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"d6ce2ec3-f98a-4529-acd5-08bff271cb3b"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"ef358221-3904-4dbc-be6f-e1e8dea63954"
|
||||
]
|
||||
]
|
||||
],
|
||||
3,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
"",
|
||||
[
|
||||
2,
|
||||
null,
|
||||
null,
|
||||
"en",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
2,
|
||||
2
|
||||
],
|
||||
true
|
||||
]
|
||||
],
|
||||
[
|
||||
1768346092,
|
||||
144365000
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
1768346040,
|
||||
459674000
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1
|
||||
]
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
[
|
||||
"835a5938-4a60-43a0-b2c3-6f01b435664a",
|
||||
"Agent Architecture Quiz",
|
||||
4,
|
||||
[
|
||||
[
|
||||
[
|
||||
"a474cd35-6c21-4e72-94a0-c38b5491b449"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"735fcfef-9fbd-4c89-9789-6a9760587bec"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"4d3f7b07-e9e6-43d3-ab8b-184fa27a9f1e"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"a5ec927b-12eb-45d1-989f-12eb3db4ce53"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"c361a555-5c2d-42e2-94d0-a65da95be660"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"48f71a82-08d3-46fa-a37f-d657fb2f0723"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"d6ce2ec3-f98a-4529-acd5-08bff271cb3b"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"ef358221-3904-4dbc-be6f-e1e8dea63954"
|
||||
]
|
||||
]
|
||||
],
|
||||
3,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
"",
|
||||
[
|
||||
2,
|
||||
null,
|
||||
null,
|
||||
"en",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
2,
|
||||
2
|
||||
],
|
||||
true
|
||||
]
|
||||
],
|
||||
[
|
||||
1768311105,
|
||||
796819000
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
1768311059,
|
||||
696595000
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "notebooklm-py"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
description = "Unofficial Python client for Google NotebookLM API"
|
||||
dynamic = ["readme"]
|
||||
requires-python = ">=3.10"
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
|
|
@ -18,7 +18,7 @@ from ._logging import configure_logging
|
|||
|
||||
configure_logging()
|
||||
|
||||
__version__ = "0.1.3"
|
||||
__version__ = "0.2.0"
|
||||
|
||||
# Public API: Authentication
|
||||
from .auth import DEFAULT_STORAGE_PATH, AuthTokens
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
18
uv.lock
18
uv.lock
|
|
@ -432,7 +432,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "notebooklm-py"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
|
|
@ -448,6 +448,7 @@ all = [
|
|||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-httpx" },
|
||||
{ name = "pytest-rerunfailures" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "ruff" },
|
||||
{ name = "vcrpy" },
|
||||
|
|
@ -461,6 +462,7 @@ dev = [
|
|||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-httpx" },
|
||||
{ name = "pytest-rerunfailures" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "ruff" },
|
||||
{ name = "vcrpy" },
|
||||
|
|
@ -477,6 +479,7 @@ requires-dist = [
|
|||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
|
||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },
|
||||
{ name = "pytest-httpx", marker = "extra == 'dev'", specifier = ">=0.30.0" },
|
||||
{ name = "pytest-rerunfailures", marker = "extra == 'dev'", specifier = ">=14.0" },
|
||||
{ name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0.0" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" },
|
||||
|
|
@ -610,6 +613,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e2/d2/1eb1ea9c84f0d2033eb0b49675afdc71aa4ea801b74615f00f3c33b725e3/pytest_httpx-0.36.0-py3-none-any.whl", hash = "sha256:bd4c120bb80e142df856e825ec9f17981effb84d159f9fa29ed97e2357c3a9c8", size = 20229, upload-time = "2025-12-02T16:34:56.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-rerunfailures"
|
||||
version = "16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/04/71e9520551fc8fe2cf5c1a1842e4e600265b0815f2016b7c27ec85688682/pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e", size = 30889, upload-time = "2025-10-10T07:06:01.238Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/54/60eabb34445e3db3d3d874dc1dfa72751bfec3265bd611cb13c8b290adea/pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86", size = 14093, upload-time = "2025-10-10T07:06:00.019Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
|
|
|
|||
Loading…
Reference in a new issue