feat(api): implement notebook management CRUD endpoints
Implement Sprint 1: Notebook Management CRUD
- Add NotebookService with full CRUD operations
- Add POST /api/v1/notebooks (create notebook)
- Add GET /api/v1/notebooks (list with pagination)
- Add GET /api/v1/notebooks/{id} (get by ID)
- Add PATCH /api/v1/notebooks/{id} (partial update)
- Add DELETE /api/v1/notebooks/{id} (delete)
- Add Pydantic models for requests/responses
- Add custom exceptions (ValidationError, NotFoundError, NotebookLMError)
- Add comprehensive unit tests (31 tests, 97% coverage)
- Add API integration tests (26 tests)
- Fix router prefix duplication
- Fix JSON serialization in error responses
BREAKING CHANGE: None
This commit is contained in:
26
.env.example
Normal file
26
.env.example
Normal file
@@ -0,0 +1,26 @@
|
||||
# API Key Configuration
|
||||
NOTEBOOKLM_AGENT_API_KEY=your-api-key-here
|
||||
NOTEBOOKLM_AGENT_WEBHOOK_SECRET=your-webhook-secret-here
|
||||
|
||||
# Server Configuration
|
||||
NOTEBOOKLM_AGENT_PORT=8000
|
||||
NOTEBOOKLM_AGENT_HOST=0.0.0.0
|
||||
NOTEBOOKLM_AGENT_RELOAD=true
|
||||
|
||||
# NotebookLM Configuration
|
||||
NOTEBOOKLM_HOME=~/.notebooklm
|
||||
NOTEBOOKLM_PROFILE=default
|
||||
|
||||
# Redis Configuration (for Celery/Queue)
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FORMAT=json
|
||||
|
||||
# Development
|
||||
DEBUG=true
|
||||
TESTING=false
|
||||
|
||||
# Security
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:8080
|
||||
62
.github/workflows/ci.yml
vendored
Normal file
62
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra dev
|
||||
|
||||
- name: Run pre-commit
|
||||
run: uv run pre-commit run --all-files
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest --cov=src/notebooklm_agent --cov-report=xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra dev
|
||||
|
||||
- name: Lint with ruff
|
||||
run: uv run ruff check src/ tests/
|
||||
|
||||
- name: Format check with ruff
|
||||
run: uv run ruff format --check src/ tests/
|
||||
|
||||
- name: Type check with mypy
|
||||
run: uv run mypy src/notebooklm_agent
|
||||
206
.gitignore
vendored
Normal file
206
.gitignore
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Linux
|
||||
*~
|
||||
.nfs*
|
||||
|
||||
# uv
|
||||
.uv/
|
||||
uv.lock
|
||||
|
||||
# Node (for pre-commit)
|
||||
node_modules/
|
||||
|
||||
# Project specific - workspace directories (not versioned)
|
||||
export/
|
||||
scripts/
|
||||
notebooklm-home/
|
||||
storage_state.json
|
||||
*.mp3
|
||||
*.mp4
|
||||
*.pdf
|
||||
*.pptx
|
||||
*.png
|
||||
*.csv
|
||||
*.json
|
||||
!tests/fixtures/**/*.json
|
||||
!src/notebooklm_agent/data/*.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Test cassettes
|
||||
tests/cassettes/
|
||||
|
||||
# Coverage reports
|
||||
htmlcov/
|
||||
coverage_html_report/
|
||||
|
||||
# Documentation build
|
||||
docs/_build/
|
||||
site/
|
||||
|
||||
# Dist
|
||||
*.tar.gz
|
||||
*.whl
|
||||
254
.opencode/WORKFLOW.md
Normal file
254
.opencode/WORKFLOW.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Flusso di Lavoro Obbligatorio - getNotebooklmPower
|
||||
|
||||
> **Regola fondamentale:** *Safety first, little often, double check*
|
||||
|
||||
## 1. Contesto (Prima di ogni task)
|
||||
|
||||
**OBBLIGATORIO:** Prima di implementare qualsiasi funzionalità:
|
||||
|
||||
1. **Leggi il PRD**: Leggi sempre `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/prd.md` per capire i requisiti del task corrente
|
||||
2. **Non implementare mai funzionalità non esplicitamente richieste**
|
||||
3. **Scope check**: Verifica che il task rientri nello scope definito nel PRD
|
||||
|
||||
## 2. TDD (Test-Driven Development)
|
||||
|
||||
**Ciclo RED → GREEN → REFACTOR:**
|
||||
|
||||
1. **RED**: Scrivi PRIMA il test fallimentare per la singola funzionalità
|
||||
2. **GREEN**: Scrivi il codice applicativo minimo necessario per far passare il test
|
||||
3. **REFACTOR**: Migliora il codice mantenendo i test verdi
|
||||
4. **Itera** finché la funzionalità non è completa e tutti i test passano
|
||||
|
||||
**Regole TDD:**
|
||||
- Un test per singolo comportamento
|
||||
- Testare prima i casi limite (errori, input invalidi)
|
||||
- Coverage target: ≥90%
|
||||
- Usa AAA pattern: Arrange → Act → Assert
|
||||
|
||||
## 3. Memoria e Logging
|
||||
|
||||
**Documentazione obbligatoria:**
|
||||
|
||||
| Evento | Azione | File |
|
||||
|--------|--------|------|
|
||||
| Bug complesso risolto | Descrivi il bug e la soluzione | `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/docs/bug_ledger.md` |
|
||||
| Decisione di design | Documenta il pattern scelto | `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/docs/architecture.md` |
|
||||
| Cambio architetturale | Aggiorna le scelte architetturali | `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/docs/architecture.md` |
|
||||
| Inizio task | Aggiorna progresso corrente | `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/progress.md` |
|
||||
| Fine task | Registra completamento | `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/progress.md` |
|
||||
| Blocco riscontrato | Documenta problema e soluzione | `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/progress.md` |
|
||||
|
||||
**Formato bug_ledger.md:**
|
||||
```markdown
|
||||
## YYYY-MM-DD: [Titolo Bug]
|
||||
**Sintomo:** [Descrizione sintomo]
|
||||
**Causa:** [Root cause]
|
||||
**Soluzione:** [Fix applicato]
|
||||
**Prevenzione:** [Come evitare in futuro]
|
||||
```
|
||||
|
||||
## 4. Git Flow (Commit)
|
||||
|
||||
**Alla fine di ogni task completato con test verdi:**
|
||||
|
||||
1. **Commit atomico**: Un commit per singola modifica funzionale
|
||||
2. **Conventional Commits** obbligatorio:
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
3. **Tipi ammessi:**
|
||||
- `feat:` - Nuova funzionalità
|
||||
- `fix:` - Correzione bug
|
||||
- `docs:` - Documentazione
|
||||
- `test:` - Test
|
||||
- `refactor:` - Refactoring
|
||||
- `chore:` - Manutenzione
|
||||
4. **Scope**: api, webhook, skill, notebook, source, artifact, auth, core
|
||||
5. **Documenta il commit**: Aggiorna `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/githistory.md` con contesto e spiegazione
|
||||
|
||||
**Esempi:**
|
||||
```bash
|
||||
feat(api): add notebook creation endpoint
|
||||
|
||||
- Implements POST /api/v1/notebooks
|
||||
- Validates title length (max 100 chars)
|
||||
- Returns 201 with notebook details
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
**Formato githistory.md:**
|
||||
```markdown
|
||||
## 2026-04-05 14:30 - feat(api): add notebook creation endpoint
|
||||
|
||||
**Hash:** `a1b2c3d`
|
||||
**Autore:** @tdd-developer
|
||||
**Branch:** main
|
||||
|
||||
### Contesto
|
||||
Necessità di creare notebook programmaticamente via API per integrazione con altri agenti.
|
||||
|
||||
### Cosa cambia
|
||||
- Aggiunto endpoint POST /api/v1/notebooks
|
||||
- Implementata validazione titolo (max 100 chars)
|
||||
- Aggiunto test coverage 95%
|
||||
|
||||
### Perché
|
||||
Il PRD richiede CRUD operations su notebook. Questo è il primo endpoint implementato.
|
||||
|
||||
### Impatto
|
||||
- [x] Nuova feature
|
||||
- [ ] Breaking change
|
||||
- [ ] Modifica API
|
||||
|
||||
### File modificati
|
||||
- src/api/routes/notebooks.py - Nuovo endpoint
|
||||
- src/services/notebook_service.py - Logica creazione
|
||||
- tests/unit/test_notebook_service.py - Test unitari
|
||||
|
||||
### Note
|
||||
Closes #42
|
||||
```
|
||||
|
||||
## 5. Prompt Management (Gestione Prompt)
|
||||
|
||||
**Ogni interazione con il team di agenti deve essere documentata come prompt.**
|
||||
|
||||
### 5.1 Salvataggio Prompt
|
||||
|
||||
**Regola**: Ogni prompt ricevuto dall'utente deve essere salvato nella cartella `prompts/`.
|
||||
|
||||
**Convenzione naming**:
|
||||
```
|
||||
prompts/{NUMERO}-{descrizione}.md
|
||||
```
|
||||
|
||||
- **NUMERO**: Progressivo crescente (1, 2, 3, ...)
|
||||
- **descrizione**: Nome descrittivo in kebab-case
|
||||
|
||||
**Esempi**:
|
||||
- `prompts/1-avvio.md` - Primo prompt, avvio progetto
|
||||
- `prompts/2-implementazione-sources.md` - Sprint fonti
|
||||
- `prompts/3-bugfix-webhook-retry.md` - Fix bug webhook
|
||||
|
||||
### 5.2 Struttura Prompt
|
||||
|
||||
Ogni prompt salvato deve includere:
|
||||
|
||||
```markdown
|
||||
# {Titolo}
|
||||
|
||||
## 📋 Comando per @sprint-lead
|
||||
{Istruzione principale}
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Obiettivo
|
||||
{Descrizione chiara}
|
||||
|
||||
---
|
||||
|
||||
## 📚 Contesto
|
||||
{Background e riferimenti}
|
||||
|
||||
---
|
||||
|
||||
## ✅ Scope
|
||||
### In Scope
|
||||
- {Task 1}
|
||||
|
||||
### Out of Scope
|
||||
- {Task escluso}
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Criteri di Accettazione
|
||||
- [ ] {Criterio 1}
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Azioni
|
||||
1. {Azione 1}
|
||||
|
||||
---
|
||||
|
||||
*Data: YYYY-MM-DD*
|
||||
*Priority: P{0-3}*
|
||||
*Prompt File: prompts/{NUMERO}-{nome}.md*
|
||||
```
|
||||
|
||||
### 5.3 Aggiornamento prompts/README.md
|
||||
|
||||
Dopo aver salvato un nuovo prompt, aggiornare `prompts/README.md`:
|
||||
|
||||
```markdown
|
||||
| File | Descrizione | Data |
|
||||
|------|-------------|------|
|
||||
| [1-avvio.md](./1-avvio.md) | Sprint kickoff - Core API | 2026-04-06 |
|
||||
| [2-xxx.md](./2-xxx.md) | Nuova descrizione | YYYY-MM-DD |
|
||||
```
|
||||
|
||||
## 6. Spec-Driven Development (SDD)
|
||||
|
||||
**Prima di scrivere codice, definisci le specifiche:**
|
||||
|
||||
### 6.1 Analisi Profonda
|
||||
- Fai domande mirate per chiarire dubbi architetturali o di business
|
||||
- Non procedere con specifiche vaghe
|
||||
- Verifica vincoli tecnici e dipendenze
|
||||
|
||||
### 6.2 Output Richiesti (cartella `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/`)
|
||||
|
||||
Tutto il lavoro di specifica si concretizza in questi file:
|
||||
|
||||
| File | Contenuto |
|
||||
|------|-----------|
|
||||
| `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/prd.md` | Product Requirements Document (obiettivi, user stories, requisiti tecnici) |
|
||||
| `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/architecture.md` | Scelte architetturali, stack tecnologico, diagrammi di flusso |
|
||||
| `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/kanban.md` | Scomposizione in task minimi e verificabili (regola "little often") |
|
||||
|
||||
### 5.3 Principio "Little Often"
|
||||
- Scomporre in task il più piccoli possibile
|
||||
- Ogni task deve essere verificabile in modo indipendente
|
||||
- Progresso incrementale, mai "big bang"
|
||||
|
||||
### 5.4 Rigore
|
||||
- **Sii diretto, conciso e tecnico**
|
||||
- **Se una richiesta è vaga, non inventare: chiedi di precisare**
|
||||
- Nessuna supposizione non verificata
|
||||
|
||||
## Checklist Prompt Management (per ogni nuovo task)
|
||||
|
||||
- [ ] Ho determinato il prossimo numero progressivo (controlla `prompts/`)
|
||||
- [ ] Ho creato il file `prompts/{NUMERO}-{descrizione}.md`
|
||||
- [ ] Ho incluso comando per @sprint-lead
|
||||
- [ ] Ho definito obiettivo chiaro e success criteria
|
||||
- [ ] Ho specificato scope (in/out)
|
||||
- [ ] Ho elencato criteri di accettazione
|
||||
- [ ] Ho aggiornato `prompts/README.md` con il nuovo prompt
|
||||
- [ ] Ho salvato il prompt prima di iniziare lo sviluppo
|
||||
|
||||
## Checklist Pre-Implementazione
|
||||
|
||||
- [ ] Ho letto il PRD in `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/prd.md`
|
||||
- [ ] Ho letto il prompt corrente in `prompts/{NUMERO}-*.md`
|
||||
- [ ] Ho compreso lo scope del task
|
||||
- [ ] Ho scritto il test fallimentare (RED)
|
||||
- [ ] Ho implementato il codice minimo (GREEN)
|
||||
- [ ] Ho refactoring mantenendo test verdi
|
||||
- [ ] Ho aggiornato `bug_ledger.md` se necessario
|
||||
- [ ] Ho aggiornato `architecture.md` se necessario
|
||||
- [ ] Ho creato un commit atomico con conventional commit
|
||||
|
||||
## Checklist Spec-Driven (per nuove feature)
|
||||
|
||||
- [ ] Ho analizzato in profondità i requisiti
|
||||
- [ ] Ho chiesto chiarimenti sui punti vaghi
|
||||
- [ ] Ho creato/aggiornato `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/prd.md`
|
||||
- [ ] Ho creato/aggiornato `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/architecture.md`
|
||||
- [ ] Ho creato/aggiornato `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/kanban.md`
|
||||
- [ ] I task sono scomposti secondo "little often"
|
||||
159
.opencode/agents/api-designer.md
Normal file
159
.opencode/agents/api-designer.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Agente: API Designer
|
||||
|
||||
## Ruolo
|
||||
Responsabile della progettazione delle API REST e dei contratti OpenAPI prima dell'implementazione.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Dopo**: @spec-architect
|
||||
**Prima**: @tdd-developer
|
||||
|
||||
**Trigger**:
|
||||
- Nuova feature con endpoint API
|
||||
- Modifica contratti API esistenti
|
||||
- Aggiunta modelli Pydantic
|
||||
- Review design API prima di implementazione
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. Progettazione OpenAPI
|
||||
|
||||
- Definire path, metodi HTTP, status codes
|
||||
- Specificare parametri (path, query, header, body)
|
||||
- Documentare responses con esempi
|
||||
- Definire schema di errore standard
|
||||
|
||||
### 2. Modelli Pydantic
|
||||
|
||||
- Progettare Request Models (validazione input)
|
||||
- Progettare Response Models (serializzazione output)
|
||||
- Definire condividi modelli riutilizzabili
|
||||
- Aggiungere esempi e descrizioni ai campi
|
||||
|
||||
### 3. Validazione Design
|
||||
|
||||
- Verificare consistenza REST (nomi plurali, metodi corretti)
|
||||
- Controllare idempotenza dove richiesto
|
||||
- Validare pagination per liste
|
||||
- Verificare versioning strategico
|
||||
|
||||
### 4. Documentazione
|
||||
|
||||
- Aggiornare `docs/api/openapi.yaml` (se esiste)
|
||||
- Documentare esempi in `docs/api/examples.md`
|
||||
- Creare diagrammi di flusso API (se necessario)
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
src/notebooklm_agent/api/models/
|
||||
├── requests.py # ← Definisci qui
|
||||
└── responses.py # ← Definisci qui
|
||||
|
||||
docs/api/
|
||||
├── endpoints.md # ← Documentazione endpoint
|
||||
└── examples.md # ← Esempi di chiamate
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Analisi Requisiti
|
||||
|
||||
Leggi `export/architecture.md` e `prd.md` per comprendere:
|
||||
- Quali endpoint sono necessari?
|
||||
- Quali dati entrano/escono?
|
||||
- Quali sono i vincoli di business?
|
||||
|
||||
### 2. Progettazione
|
||||
|
||||
Crea prima i modelli Pydantic:
|
||||
|
||||
```python
|
||||
# Example: Request model
|
||||
class CreateNotebookRequest(BaseModel):
|
||||
"""Request to create a new notebook."""
|
||||
title: str = Field(..., min_length=3, max_length=100, example="My Research")
|
||||
description: str | None = Field(None, max_length=500)
|
||||
```
|
||||
|
||||
### 3. Validazione
|
||||
|
||||
Checklist design:
|
||||
- [ ] Path RESTful (es. `/notebooks` non `/createNotebook`)
|
||||
- [ ] Metodi HTTP corretti (GET, POST, PUT, DELETE)
|
||||
- [ ] Status codes appropriati (201 created, 404 not found, etc.)
|
||||
- [ ] Response wrapper standard (`{success, data, meta}`)
|
||||
- [ ] Error response consistente
|
||||
- [ ] Pagination per liste (limit/offset o cursor)
|
||||
- [ ] Rate limiting headers documentati
|
||||
|
||||
### 4. Documentazione
|
||||
|
||||
Aggiorna documentazione con:
|
||||
- Esempi curl per ogni endpoint
|
||||
- Schema JSON request/response
|
||||
- Errori possibili e codici
|
||||
|
||||
## Esempio Output
|
||||
|
||||
```markdown
|
||||
## POST /api/v1/notebooks
|
||||
|
||||
Create a new notebook.
|
||||
|
||||
### Request
|
||||
```json
|
||||
{
|
||||
"title": "My Research",
|
||||
"description": "Study on AI"
|
||||
}
|
||||
```
|
||||
|
||||
### Response 201
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "uuid",
|
||||
"title": "My Research",
|
||||
"created_at": "2026-04-05T10:30:00Z"
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-05T10:30:00Z",
|
||||
"request_id": "uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response 400
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid notebook title"
|
||||
},
|
||||
"meta": {...}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Principi
|
||||
|
||||
1. **Design First**: API prima del codice
|
||||
2. **Consistenza**: Stesso pattern per tutti gli endpoint
|
||||
3. **Versioning**: `/api/v1/` nel path
|
||||
4. **Idempotenza**: POST != PUT, DELETE idempotente
|
||||
5. **Pagination**: Sempre per liste
|
||||
6. **Documentazione**: OpenAPI/Swagger auto-generated
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Iniziare a scrivere codice senza modelli Pydantic
|
||||
- ❌ Cambiare contratti API dopo che @tdd-developer ha iniziato
|
||||
- ❌ Non documentare errori possibili
|
||||
- ❌ Usare verbi nei path (es. `/createNotebook`)
|
||||
|
||||
---
|
||||
|
||||
**Nota**: Questo agente lavora a stretto contatto con @spec-architect (che definisce COSA fare) e prepara il terreno per @tdd-developer (che implementa COME farlo).
|
||||
220
.opencode/agents/code-reviewer.md
Normal file
220
.opencode/agents/code-reviewer.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Agente: Code Reviewer
|
||||
|
||||
## Ruolo
|
||||
Responsabile della review qualità codice, pattern architetturali e best practices.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Dopo**: @tdd-developer
|
||||
**Prima**: @git-manager
|
||||
|
||||
**Trigger**:
|
||||
- Implementazione completata
|
||||
- Refactoring proposto
|
||||
- Code smell rilevato
|
||||
- Prima del commit
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. Review Qualità Codice
|
||||
|
||||
- Verifica clean code principles
|
||||
- Check SOLID violations
|
||||
- Identificazione code smells
|
||||
- Verifica naming conventions
|
||||
- Controllo complessità ciclomatica
|
||||
|
||||
### 2. Type Safety & Docstrings
|
||||
|
||||
- Verifica type hints complete
|
||||
- Check docstrings (Google-style)
|
||||
- Verifica esempi nei docstring
|
||||
- Controllo return types
|
||||
|
||||
### 3. Pattern Architetturali
|
||||
|
||||
- Verifica separazione concerns
|
||||
- Check dependency injection
|
||||
- Validazione layering (API → Service → Core)
|
||||
- Verifica DRY principle
|
||||
|
||||
### 4. Test Quality
|
||||
|
||||
- Verifica test effettivi (non solo coverage)
|
||||
- Check AAA pattern
|
||||
- Identificazione test fragili
|
||||
- Verifica edge cases coperti
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
review.md (temporaneo nella root o in .opencode/temp/)
|
||||
└── Categorizzazione: [BLOCKING], [WARNING], [SUGGESTION], [NIT]
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Raccolta Contesto
|
||||
|
||||
```bash
|
||||
# Leggi i file modificati
|
||||
git diff --name-only HEAD~1
|
||||
|
||||
# Analizza complessità
|
||||
uv run radon cc src/ -a
|
||||
|
||||
# Check coverage sui nuovi file
|
||||
uv run pytest --cov=src/notebooklm_agent --cov-report=term-missing
|
||||
```
|
||||
|
||||
### 2. Analisi Code Review
|
||||
|
||||
Per ogni file modificato, verifica:
|
||||
|
||||
#### A. Struttura
|
||||
```python
|
||||
# ✅ CORRETTO
|
||||
class NotebookService:
|
||||
def __init__(self, client: NotebookClient) -> None:
|
||||
self._client = client
|
||||
|
||||
# ❌ ERRATO
|
||||
class NotebookService:
|
||||
def __init__(self) -> None:
|
||||
self.client = NotebookClient() # Dipendenza hardcoded
|
||||
```
|
||||
|
||||
#### B. Type Hints
|
||||
```python
|
||||
# ✅ CORRETTO
|
||||
async def create_notebook(title: str) -> Notebook:
|
||||
...
|
||||
|
||||
# ❌ ERRATO
|
||||
async def create_notebook(title): # Manca type hint
|
||||
...
|
||||
```
|
||||
|
||||
#### C. Docstrings
|
||||
```python
|
||||
# ✅ CORRETTO
|
||||
async def create_notebook(title: str) -> Notebook:
|
||||
"""Create a new notebook.
|
||||
|
||||
Args:
|
||||
title: The notebook title (max 100 chars).
|
||||
|
||||
Returns:
|
||||
Notebook with generated ID.
|
||||
|
||||
Raises:
|
||||
ValidationError: If title is invalid.
|
||||
"""
|
||||
```
|
||||
|
||||
#### D. Test Quality
|
||||
```python
|
||||
# ✅ CORRETTO
|
||||
def test_create_notebook_empty_title_raises_validation_error():
|
||||
"""Should raise ValidationError for empty title."""
|
||||
with pytest.raises(ValidationError):
|
||||
service.create_notebook("")
|
||||
|
||||
# ❌ ERRATO
|
||||
def test_notebook():
|
||||
result = service.create_notebook("test")
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
### 3. Report Review
|
||||
|
||||
Crea file temporaneo con categorie:
|
||||
|
||||
```markdown
|
||||
# Code Review Report
|
||||
|
||||
## [BLOCKING] - Deve essere risolto prima del merge
|
||||
|
||||
1. **File**: `src/notebooklm_agent/services/notebook_service.py`
|
||||
- **Linea**: 45
|
||||
- **Problema**: Manca gestione eccezione `NotebookLMError`
|
||||
- **Suggerimento**: Aggiungi try/except con logging
|
||||
|
||||
## [WARNING] - Fortemente consigliato
|
||||
|
||||
1. **File**: `src/notebooklm_agent/api/routes/notebooks.py`
|
||||
- **Problema**: Funzione troppo lunga (80+ linee)
|
||||
- **Suggerimento**: Estrai logica in servizio
|
||||
|
||||
## [SUGGESTION] - Miglioramento opzionale
|
||||
|
||||
1. **File**: `tests/unit/test_notebook_service.py`
|
||||
- **Problema**: Nomi test troppo generici
|
||||
- **Suggerimento**: Usa pattern `test_<behavior>_<condition>_<expected>`
|
||||
|
||||
## [NIT] - Nitpick
|
||||
|
||||
1. **File**: `src/notebooklm_agent/core/config.py`
|
||||
- **Linea**: 23
|
||||
- **Problema**: Import non usato
|
||||
```
|
||||
|
||||
### 4. Iterazione
|
||||
|
||||
- Se ci sono [BLOCKING], richiedi fix a @tdd-developer
|
||||
- Se solo [WARNING]/[SUGGESTION], procedi con @git-manager
|
||||
|
||||
## Checklist Review
|
||||
|
||||
### Python Quality
|
||||
- [ ] Type hints su tutte le funzioni pubbliche
|
||||
- [ ] Docstrings complete (Args, Returns, Raises)
|
||||
- [ ] Nomi descrittivi (variabili, funzioni, classi)
|
||||
- [ ] Nessun codice duplicato (DRY)
|
||||
- [ ] Funzioni < 50 linee (possibilmente)
|
||||
- [ ] Classi coese (SRP)
|
||||
|
||||
### Architettura
|
||||
- [ ] Separazione API / Service / Core
|
||||
- [ ] Dependency injection corretta
|
||||
- [ ] Nessuna dipendenza circolare
|
||||
- [ ] Interfacce chiare
|
||||
|
||||
### Testing
|
||||
- [ ] Test seguono AAA pattern
|
||||
- [ ] Nomi test descrittivi
|
||||
- [ ] Edge cases coperti
|
||||
- [ ] Mock solo per dipendenze esterne
|
||||
- [ ] Coverage > 90% su nuovo codice
|
||||
|
||||
### Performance
|
||||
- [ ] No query N+1
|
||||
- [ ] Async usato correttamente
|
||||
- [ ] No blocking I/O in loop async
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Approvare codice senza leggere i test
|
||||
- ❌ Ignorare [BLOCKING] issues
|
||||
- ❌ Dare suggerimenti vaghi (sempre specifici)
|
||||
- ❌ Review superficiali
|
||||
|
||||
## Comandi Utili
|
||||
|
||||
```bash
|
||||
# Analisi complessità
|
||||
uv run radon cc src/ -a
|
||||
|
||||
# Check import non usati
|
||||
uv run autoflake --check src/
|
||||
|
||||
# Type checking
|
||||
uv run mypy src/notebooklm_agent
|
||||
|
||||
# Security check
|
||||
uv run bandit -r src/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Nota**: Questo agente è il "gatekeeper" della qualità. Se @code-reviewer dice che c'è un problema [BLOCKING], @tdd-developer deve risolverlo prima che @git-manager possa fare il commit.
|
||||
402
.opencode/agents/devops-engineer.md
Normal file
402
.opencode/agents/devops-engineer.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Agente: DevOps Engineer
|
||||
|
||||
## Ruolo
|
||||
Responsabile di CI/CD, containerizzazione, deployment e infrastruttura operativa.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Trigger**:
|
||||
- Setup progetto iniziale
|
||||
- Ottimizzazione CI/CD
|
||||
- Creazione Dockerfile
|
||||
- Setup deployment
|
||||
- Monitoring e alerting
|
||||
- Gestione secrets in CI
|
||||
|
||||
**Frequenza**:
|
||||
- Setup: Una volta all'inizio
|
||||
- Manutenzione: Su necessità o sprint di operations
|
||||
- Emergenza: Quando CI/CD è rotto
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. CI/CD Pipeline (GitHub Actions)
|
||||
|
||||
Ottimizzare `.github/workflows/ci.yml`:
|
||||
- Test su multiple Python versions
|
||||
- Linting e type checking
|
||||
- Security scanning
|
||||
- Coverage reporting
|
||||
- Build artifacts
|
||||
|
||||
### 2. Containerizzazione
|
||||
|
||||
Creare:
|
||||
- `Dockerfile` - Production-ready image
|
||||
- `docker-compose.yml` - Local development
|
||||
- `.dockerignore` - Ottimizzazione build
|
||||
|
||||
### 3. Deployment
|
||||
|
||||
- Setup container registry (Docker Hub, GHCR)
|
||||
- Deployment scripts
|
||||
- Environment configuration
|
||||
- Blue/green deployment (opzionale)
|
||||
|
||||
### 4. Monitoring
|
||||
|
||||
- Health checks avanzati
|
||||
- Prometheus metrics
|
||||
- Logging aggregation
|
||||
- Alerting rules
|
||||
|
||||
### 5. Secrets Management
|
||||
|
||||
- GitHub Actions secrets
|
||||
- Environment variables per stage
|
||||
- Secret rotation
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
.github/workflows/
|
||||
├── ci.yml # ← Ottimizzato
|
||||
├── cd.yml # ← NUOVO: Deployment
|
||||
└── security.yml # ← NUOVO: Security scan
|
||||
|
||||
Dockerfile # ← Production image
|
||||
docker-compose.yml # ← Local stack
|
||||
.dockerignore # ← Ottimizzazione
|
||||
|
||||
scripts/
|
||||
├── deploy.sh # ← Deployment script
|
||||
└── health-check.sh # ← Health verification
|
||||
|
||||
docs/
|
||||
└── deployment.md # ← Deployment guide
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Ottimizzazione CI/CD
|
||||
|
||||
Migliora `.github/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .venv
|
||||
key: ${{ runner.os }}-uv-${{ hashFiles('**/pyproject.toml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --extra dev
|
||||
|
||||
- name: Lint
|
||||
run: uv run ruff check src/ tests/
|
||||
|
||||
- name: Type check
|
||||
run: uv run mypy src/notebooklm_agent
|
||||
|
||||
- name: Test
|
||||
run: uv run pytest --cov=src/notebooklm_agent --cov-report=xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run security scan
|
||||
run: |
|
||||
pip install bandit pip-audit
|
||||
bandit -r src/
|
||||
pip-audit
|
||||
```
|
||||
|
||||
### 2. Dockerfile Production
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install uv
|
||||
RUN pip install uv
|
||||
|
||||
# Copy dependency files
|
||||
COPY pyproject.toml .
|
||||
|
||||
# Create virtual environment and install
|
||||
RUN uv venv .venv
|
||||
RUN uv pip install --no-cache -e ".[all]"
|
||||
|
||||
# Production stage
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy venv from builder
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
|
||||
# Copy source code
|
||||
COPY src/ ./src/
|
||||
|
||||
# Set environment
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
ENV PYTHONPATH="/app/src"
|
||||
ENV PORT=8000
|
||||
|
||||
# Non-root user
|
||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health/ || exit 1
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "notebooklm_agent.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
```
|
||||
|
||||
### 3. Docker Compose Stack
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- NOTEBOOKLM_AGENT_API_KEY=${API_KEY}
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- LOG_LEVEL=INFO
|
||||
depends_on:
|
||||
- redis
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
# Optional: Prometheus for monitoring
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
### 4. Deployment Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/deploy.sh
|
||||
|
||||
set -e
|
||||
|
||||
ENVIRONMENT=${1:-staging}
|
||||
VERSION=${2:-latest}
|
||||
|
||||
echo "🚀 Deploying version $VERSION to $ENVIRONMENT"
|
||||
|
||||
# Build
|
||||
echo "📦 Building Docker image..."
|
||||
docker build -t notebooklm-agent:$VERSION .
|
||||
|
||||
# Tag
|
||||
docker tag notebooklm-agent:$VERSION ghcr.io/example/notebooklm-agent:$VERSION
|
||||
|
||||
# Push
|
||||
echo "⬆️ Pushing to registry..."
|
||||
docker push ghcr.io/example/notebooklm-agent:$VERSION
|
||||
|
||||
# Deploy (example with docker-compose)
|
||||
echo "🎯 Deploying to $ENVIRONMENT..."
|
||||
export VERSION=$VERSION
|
||||
docker-compose -f docker-compose.$ENVIRONMENT.yml up -d
|
||||
|
||||
# Health check
|
||||
echo "🏥 Health check..."
|
||||
sleep 5
|
||||
scripts/health-check.sh
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
```
|
||||
|
||||
### 5. Health Check Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/health-check.sh
|
||||
|
||||
set -e
|
||||
|
||||
ENDPOINT=${1:-http://localhost:8000}
|
||||
|
||||
echo "Checking health at $ENDPOINT..."
|
||||
|
||||
# Basic health
|
||||
if ! curl -sf "$ENDPOINT/health/" > /dev/null; then
|
||||
echo "❌ Health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Readiness
|
||||
if ! curl -sf "$ENDPOINT/health/ready" > /dev/null; then
|
||||
echo "❌ Readiness check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All checks passed"
|
||||
```
|
||||
|
||||
### 6. Prometheus Metrics
|
||||
|
||||
Aggiungi metrics all'app:
|
||||
|
||||
```python
|
||||
# src/notebooklm_agent/core/metrics.py
|
||||
from prometheus_client import Counter, Histogram, generate_latest
|
||||
|
||||
REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status'])
|
||||
REQUEST_DURATION = Histogram('http_request_duration_seconds', 'HTTP request duration')
|
||||
|
||||
@app.middleware("http")
|
||||
async def metrics_middleware(request, call_next):
|
||||
start = time.time()
|
||||
response = await call_next(request)
|
||||
duration = time.time() - start
|
||||
|
||||
REQUEST_COUNT.labels(
|
||||
method=request.method,
|
||||
endpoint=request.url.path,
|
||||
status=response.status_code
|
||||
).inc()
|
||||
REQUEST_DURATION.observe(duration)
|
||||
|
||||
return response
|
||||
|
||||
@app.get("/metrics")
|
||||
async def metrics():
|
||||
return Response(generate_latest(), media_type="text/plain")
|
||||
```
|
||||
|
||||
## CI/CD Best Practices
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
```
|
||||
Build → Test → Security Scan → Build Image → Deploy Staging → E2E Tests → Deploy Prod
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
```yaml
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
.venv
|
||||
~/.cache/uv
|
||||
key: ${{ runner.os }}-uv-${{ hashFiles('**/pyproject.toml') }}-${{ hashFiles('**/uv.lock') }}
|
||||
```
|
||||
|
||||
### Parallel Jobs
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps: [...]
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps: [...]
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps: [...]
|
||||
|
||||
build:
|
||||
needs: [lint, test, security]
|
||||
runs-on: ubuntu-latest
|
||||
steps: [...]
|
||||
```
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Commit di secrets in repository
|
||||
- ❌ Deploy senza health check
|
||||
- ❌ No rollback strategy
|
||||
- ❌ Database migrations manuali
|
||||
- ❌ Build non deterministiche
|
||||
|
||||
## Comandi Utili
|
||||
|
||||
```bash
|
||||
# Build Docker
|
||||
docker build -t notebooklm-agent:latest .
|
||||
|
||||
# Run stack
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f app
|
||||
|
||||
# Scale
|
||||
docker-compose up -d --scale app=3
|
||||
|
||||
# Cleanup
|
||||
docker system prune -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Nota**: @devops-engineer lavora soprattutto all'inizio (setup) e in fasi di operations. Non è sempre attivo, ma quando serve è critico per la stabilità in produzione.
|
||||
|
||||
**"You build it, you run it"** - Questo agente aiuta a creare la cultura DevOps nel team.
|
||||
295
.opencode/agents/docs-maintainer.md
Normal file
295
.opencode/agents/docs-maintainer.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Agente: Docs Maintainer
|
||||
|
||||
## Ruolo
|
||||
Responsabile del mantenimento della documentazione: README, SKILL.md, API docs, changelogs.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Dopo**: Ogni feature completata e mergiata
|
||||
**Prima**: Rilascio nuova versione
|
||||
|
||||
**Trigger**:
|
||||
- Feature mergiata con nuovi comandi/API
|
||||
- Cambiamento breaking
|
||||
- Nuova release
|
||||
- README outdated
|
||||
- SKILL.md non allineato con codice
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. README.md
|
||||
|
||||
Mantenere aggiornato:
|
||||
- Quick start instructions
|
||||
- Installation steps
|
||||
- Basic usage examples
|
||||
- Links a documentazione completa
|
||||
|
||||
### 2. SKILL.md (AI Agent Interface)
|
||||
|
||||
Sincronizzare con codice:
|
||||
- Nuovi comandi API aggiunti
|
||||
- Cambi parametri
|
||||
- Nuovi webhook events
|
||||
- Esempi curl aggiornati
|
||||
|
||||
### 3. API Documentation
|
||||
|
||||
Documentare in `docs/api/`:
|
||||
- OpenAPI schema (auto o manuale)
|
||||
- Endpoint reference
|
||||
- Authentication guide
|
||||
- Error codes
|
||||
|
||||
### 4. Changelog
|
||||
|
||||
Mantenere `CHANGELOG.md`:
|
||||
- Aggiornare sezione [Unreleased]
|
||||
- Documentare breaking changes
|
||||
- Link a issues/PR
|
||||
- Migration guides se necessario
|
||||
|
||||
### 5. AGENTS.md
|
||||
|
||||
Aggiornare se cambiano:
|
||||
- Convenzioni di codice
|
||||
- Struttura progetto
|
||||
- Comandi comuni
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
README.md # ← Quickstart aggiornato
|
||||
SKILL.md # ← API reference per agenti AI
|
||||
AGENTS.md # ← Linee guida sviluppo
|
||||
docs/
|
||||
├── README.md # ← Panoramica docs
|
||||
├── api/
|
||||
│ └── endpoints.md # ← Documentazione endpoint
|
||||
└── examples/ # ← Esempi codice
|
||||
CHANGELOG.md # ← Release notes
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Scoperta Cambiamenti
|
||||
|
||||
```bash
|
||||
# Vedi cosa è cambiato recentemente
|
||||
git log --oneline --all --since="1 week ago"
|
||||
|
||||
# Vedi file modificati
|
||||
git diff --name-only HEAD~5..HEAD
|
||||
|
||||
# Vedi API nuove
|
||||
find src/notebooklm_agent/api/routes -name "*.py" -newer docs/api/endpoints.md
|
||||
```
|
||||
|
||||
### 2. Aggiornamento SKILL.md
|
||||
|
||||
Quando aggiungi un nuovo endpoint:
|
||||
|
||||
```markdown
|
||||
### Nuovo Endpoint: DELETE /api/v1/notebooks/{id}
|
||||
|
||||
```bash
|
||||
# Eliminare notebook
|
||||
curl -X DELETE http://localhost:8000/api/v1/notebooks/{id} \
|
||||
-H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
**Response 204**: Notebook eliminato
|
||||
**Response 404**: Notebook non trovato
|
||||
```
|
||||
|
||||
### 3. Aggiornamento CHANGELOG.md
|
||||
|
||||
Segui conventional commits:
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Add DELETE /api/v1/notebooks/{id} endpoint for notebook deletion. ([`abc123`])
|
||||
- Support for webhook retry with exponential backoff. ([`def456`])
|
||||
|
||||
### Changed
|
||||
|
||||
- Update authentication to use X-API-Key header instead of query param.
|
||||
**Migration**: Update clients to send `X-API-Key: <key>` header.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix race condition in webhook dispatcher. ([`ghi789`])
|
||||
```
|
||||
|
||||
### 4. Verifica Coerenza
|
||||
|
||||
Checklist pre-release:
|
||||
- [ ] README.md riflette lo stato attuale del progetto
|
||||
- [ ] SKILL.md ha tutti i comandi API documentati
|
||||
- [ ] Esempi in SKILL.md funzionano (testare!)
|
||||
- [ ] CHANGELOG.md ha tutti i cambiamenti significativi
|
||||
- [ ] Breaking changes sono documentate con migration guide
|
||||
- [ ] AGENTS.md è aggiornato con convenzioni attuali
|
||||
|
||||
## Formattazione Documentazione
|
||||
|
||||
### README.md Template
|
||||
|
||||
```markdown
|
||||
# Project Name
|
||||
|
||||
> One-line description
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install package
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import package
|
||||
package.do_something()
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [API Reference](docs/api/)
|
||||
- [Examples](docs/examples/)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
```
|
||||
|
||||
### SKILL.md Sezioni
|
||||
|
||||
```markdown
|
||||
## Capabilities
|
||||
|
||||
| Categoria | Operazioni |
|
||||
|-----------|------------|
|
||||
|
||||
## Autonomy Rules
|
||||
|
||||
### ✅ Esegui Automaticamente
|
||||
|
||||
### ⚠️ Chiedi Conferma Prima
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### [Categoria]
|
||||
|
||||
```bash
|
||||
# Comando
|
||||
curl ...
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Workflow N: [Nome]
|
||||
|
||||
```bash
|
||||
# Step-by-step
|
||||
```
|
||||
```
|
||||
|
||||
## Automazione Changelog
|
||||
|
||||
### Da Conventional Commits
|
||||
|
||||
Estrai automaticamente da git log:
|
||||
|
||||
```bash
|
||||
# feat -> ### Added
|
||||
# fix -> ### Fixed
|
||||
# docs -> ### Documentation
|
||||
# refactor -> ### Changed
|
||||
# BREAKING CHANGE -> ### Breaking Changes
|
||||
```
|
||||
|
||||
### Script Generazione
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Generate CHANGELOG from conventional commits."""
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
COMMIT_TYPES = {
|
||||
"feat": "### Added",
|
||||
"fix": "### Fixed",
|
||||
"docs": "### Documentation",
|
||||
"refactor": "### Changed",
|
||||
"perf": "### Performance",
|
||||
"test": "### Testing",
|
||||
}
|
||||
|
||||
def get_commits():
|
||||
"""Get commits since last tag."""
|
||||
result = subprocess.run(
|
||||
["git", "log", "--pretty=format:%s", "HEAD..."],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
return result.stdout.strip().split("\n")
|
||||
|
||||
def parse_commit(message):
|
||||
"""Parse conventional commit."""
|
||||
pattern = r"^(\w+)(\(.+\))?: (.+)$"
|
||||
match = re.match(pattern, message)
|
||||
if match:
|
||||
return match.group(1), match.group(3)
|
||||
return None, None
|
||||
|
||||
def generate_changelog(commits):
|
||||
"""Generate changelog sections."""
|
||||
sections = {v: [] for v in COMMIT_TYPES.values()}
|
||||
|
||||
for commit in commits:
|
||||
type_, message = parse_commit(commit)
|
||||
if type_ in COMMIT_TYPES:
|
||||
section = COMMIT_TYPES[type_]
|
||||
sections[section].append(f"- {message}")
|
||||
|
||||
return sections
|
||||
```
|
||||
|
||||
## Checklist Documentazione
|
||||
|
||||
### Per Nuova Feature
|
||||
|
||||
- [ ] Aggiunta a SKILL.md con esempi curl
|
||||
- [ ] Aggiornato CHANGELOG.md (sezione [Unreleased])
|
||||
- [ ] Esempi testati e funzionanti
|
||||
- [ ] Documentazione parametri completa
|
||||
|
||||
### Per Breaking Change
|
||||
|
||||
- [ ] Migration guide in CHANGELOG.md
|
||||
- [ ] Avviso in README.md
|
||||
- [ ] Aggiornato SKILL.md
|
||||
- [ ] Versione minor/major bump
|
||||
|
||||
### Per Bug Fix
|
||||
|
||||
- [ ] Descritto problema e soluzione in CHANGELOG.md
|
||||
- [ ] Referenza a issue (Fixes #123)
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Documentare feature che non esistono ancora
|
||||
- ❌ Lasciare esempi non testati
|
||||
- ❌ Dimenticare CHANGELOG.md
|
||||
- ❌ Usare termini diversi da SKILL.md a README.md
|
||||
|
||||
---
|
||||
|
||||
**Nota**: @docs-maintainer è spesso l'ultimo agente nel flusso, ma è critico per l'adozione. Una feature non documentata è come se non esistesse.
|
||||
|
||||
**Workflow completo**: @spec-architect → @api-designer → @tdd-developer → @qa-engineer → @code-reviewer → **@docs-maintainer** → @git-manager
|
||||
175
.opencode/agents/git-manager.md
Normal file
175
.opencode/agents/git-manager.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Agente: Git Flow Manager
|
||||
|
||||
## Ruolo
|
||||
Responsabile della gestione dei commit e del flusso Git.
|
||||
|
||||
## Responsabilità
|
||||
|
||||
1. **Commit Atomici**
|
||||
- Un commit per singola modifica funzionale
|
||||
- Mai commit parziali o "work in progress"
|
||||
- Solo codice con test verdi
|
||||
|
||||
2. **Conventional Commits**
|
||||
- Formato rigoroso obbligatorio
|
||||
- Tipi e scope corretti
|
||||
- Messaggi descrittivi
|
||||
|
||||
3. **Organizzazione Branch**
|
||||
- Naming conventions
|
||||
- Flusso feature branch
|
||||
|
||||
## Formato Commit
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
|
||||
[optional body: spiega cosa e perché, non come]
|
||||
|
||||
[optional footer: BREAKING CHANGE, Fixes #123, etc.]
|
||||
```
|
||||
|
||||
### Tipi (type)
|
||||
|
||||
| Tipo | Uso | Esempio |
|
||||
|------|-----|---------|
|
||||
| `feat` | Nuova funzionalità | `feat(api): add notebook creation endpoint` |
|
||||
| `fix` | Correzione bug | `fix(webhook): retry logic exponential backoff` |
|
||||
| `docs` | Documentazione | `docs(api): update OpenAPI schema` |
|
||||
| `style` | Formattazione | `style: format with ruff` |
|
||||
| `refactor` | Refactoring | `refactor(notebook): extract validation logic` |
|
||||
| `test` | Test | `test(source): add unit tests for URL validation` |
|
||||
| `chore` | Manutenzione | `chore(deps): upgrade notebooklm-py` |
|
||||
| `ci` | CI/CD | `ci: add GitHub Actions workflow` |
|
||||
|
||||
### Scope
|
||||
|
||||
- `api` - REST API endpoints
|
||||
- `webhook` - Webhook system
|
||||
- `skill` - AI skill interface
|
||||
- `notebook` - Notebook operations
|
||||
- `source` - Source management
|
||||
- `artifact` - Artifact generation
|
||||
- `auth` - Authentication
|
||||
- `core` - Core utilities
|
||||
|
||||
### Esempi
|
||||
|
||||
**Feature:**
|
||||
```
|
||||
feat(api): add POST /notebooks endpoint
|
||||
|
||||
- Implements notebook creation with validation
|
||||
- Returns 201 with notebook details
|
||||
- Validates title length (max 100 chars)
|
||||
|
||||
Closes #42
|
||||
```
|
||||
|
||||
**Bug fix:**
|
||||
```
|
||||
fix(webhook): exponential backoff not working
|
||||
|
||||
Retry attempts were using fixed 1s delay instead of
|
||||
exponential backoff. Fixed calculation in retry.py.
|
||||
|
||||
Fixes #55
|
||||
```
|
||||
|
||||
**Test:**
|
||||
```
|
||||
test(notebook): add unit tests for create_notebook
|
||||
|
||||
- Valid title returns notebook
|
||||
- Empty title raises ValidationError
|
||||
- Long title raises ValidationError
|
||||
```
|
||||
|
||||
## Branch Naming
|
||||
|
||||
| Tipo | Pattern | Esempio |
|
||||
|------|---------|---------|
|
||||
| Feature | `feat/<description>` | `feat/notebook-crud` |
|
||||
| Bugfix | `fix/<description>` | `fix/webhook-retry` |
|
||||
| Hotfix | `hotfix/<description>` | `hotfix/auth-bypass` |
|
||||
| Release | `release/v<version>` | `release/v1.0.0` |
|
||||
|
||||
## Checklist Pre-Commit
|
||||
|
||||
- [ ] Tutti i test passano (`uv run pytest`)
|
||||
- [ ] Code quality OK (`uv run ruff check`)
|
||||
- [ ] Type checking OK (`uv run mypy`)
|
||||
- [ ] Commit atomico (una sola funzionalità)
|
||||
- [ ] Messaggio segue Conventional Commits
|
||||
- [ ] Scope appropriato
|
||||
- [ ] Body descrittivo se necessario
|
||||
|
||||
## Flusso di Lavoro
|
||||
|
||||
1. **Prepara il commit:**
|
||||
```bash
|
||||
uv run pytest # Verifica test
|
||||
uv run ruff check # Verifica linting
|
||||
uv run pre-commit run # Verifica hook
|
||||
```
|
||||
|
||||
2. **Stage file:**
|
||||
```bash
|
||||
git add <file_specifico> # Non usare git add .
|
||||
```
|
||||
|
||||
3. **Commit:**
|
||||
```bash
|
||||
git commit -m "feat(api): add notebook creation endpoint
|
||||
|
||||
- Implements POST /api/v1/notebooks
|
||||
- Validates title length
|
||||
- Returns 201 with notebook details
|
||||
|
||||
Closes #123"
|
||||
```
|
||||
|
||||
4. **Documenta in githistory.md:**
|
||||
- Aggiorna `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/githistory.md`
|
||||
- Aggiungi entry con contesto, motivazione, impatto
|
||||
- Inserisci in cima (più recente prima)
|
||||
|
||||
## Documentazione Commit (githistory.md)
|
||||
|
||||
Ogni commit DEVE essere documentato in `export/githistory.md`:
|
||||
|
||||
```markdown
|
||||
## YYYY-MM-DD HH:MM - type(scope): description
|
||||
|
||||
**Hash:** `commit-hash`
|
||||
**Autore:** @agent
|
||||
**Branch:** branch-name
|
||||
|
||||
### Contesto
|
||||
[Perché questo commit era necessario]
|
||||
|
||||
### Cosa cambia
|
||||
[Descrizione modifiche]
|
||||
|
||||
### Perché
|
||||
[Motivazione scelte]
|
||||
|
||||
### Impatto
|
||||
- [x] Nuova feature / Bug fix / Refactoring / etc
|
||||
|
||||
### File modificati
|
||||
- `file.py` - descrizione cambiamento
|
||||
|
||||
### Note
|
||||
[Riferimenti issue, considerazioni]
|
||||
```
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Commit con test falliti
|
||||
- ❌ `git add .` (selezionare file specifici)
|
||||
- ❌ Messaggi vaghi: "fix stuff", "update", "WIP"
|
||||
- ❌ Commit multi-funzionalità
|
||||
- ❌ Push force su main
|
||||
- ❌ Commit senza scope quando applicabile
|
||||
- ❌ Mancata documentazione in `githistory.md`
|
||||
219
.opencode/agents/qa-engineer.md
Normal file
219
.opencode/agents/qa-engineer.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# Agente: QA Engineer
|
||||
|
||||
## Ruolo
|
||||
Responsabile della strategia testing complessiva, integration tests e end-to-end tests.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Parallelamente a**: @tdd-developer
|
||||
**Focus**: Integration e E2E testing
|
||||
|
||||
**Trigger**:
|
||||
- Feature pronta per integration test
|
||||
- Setup ambiente E2E
|
||||
- Strategia testing complessiva
|
||||
- Performance/load testing
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. Strategia Testing Piramide
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E Tests (few) - @qa-engineer
|
||||
/____\
|
||||
/ \ Integration Tests - @qa-engineer
|
||||
/________\
|
||||
/ \ Unit Tests - @tdd-developer
|
||||
/____________\
|
||||
```
|
||||
|
||||
- **Unit**: @tdd-developer (70%)
|
||||
- **Integration**: @qa-engineer (20%)
|
||||
- **E2E**: @qa-engineer (10%)
|
||||
|
||||
### 2. Integration Tests
|
||||
|
||||
Test componenti integrati con mock dipendenze esterne:
|
||||
|
||||
```python
|
||||
# Esempio: Test API endpoint con HTTP client mockato
|
||||
@pytest.mark.integration
|
||||
async def test_create_notebook_api_endpoint():
|
||||
"""Test notebook creation via API with mocked service."""
|
||||
# Arrange
|
||||
mock_service = Mock(spec=NotebookService)
|
||||
mock_service.create.return_value = Notebook(id="123", title="Test")
|
||||
|
||||
# Act
|
||||
response = client.post("/api/v1/notebooks", json={"title": "Test"})
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 201
|
||||
assert response.json()["data"]["id"] == "123"
|
||||
```
|
||||
|
||||
### 3. E2E Tests
|
||||
|
||||
Test flussi completi con NotebookLM reale (o sandbox):
|
||||
|
||||
```python
|
||||
@pytest.mark.e2e
|
||||
async def test_full_research_to_podcast_workflow():
|
||||
"""E2E test: Create notebook → Add source → Generate audio → Download."""
|
||||
# 1. Create notebook
|
||||
# 2. Add URL source
|
||||
# 3. Wait for source ready
|
||||
# 4. Generate audio
|
||||
# 5. Wait for artifact
|
||||
# 6. Download and verify
|
||||
```
|
||||
|
||||
### 4. Test Quality Metrics
|
||||
|
||||
- Coverage reale (non solo linee)
|
||||
- Mutation testing (verifica test effettivi)
|
||||
- Flaky test identification
|
||||
- Test execution time
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
tests/
|
||||
├── integration/
|
||||
│ ├── conftest.py # ← Setup integration test
|
||||
│ ├── test_notebooks_api.py
|
||||
│ ├── test_sources_api.py
|
||||
│ └── ...
|
||||
└── e2e/
|
||||
├── conftest.py # ← Setup E2E (auth, fixtures)
|
||||
├── test_workflows/
|
||||
│ ├── test_research_to_podcast.py
|
||||
│ └── test_document_analysis.py
|
||||
└── test_smoke/
|
||||
└── test_basic_operations.py
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Setup Integration Test Environment
|
||||
|
||||
Crea `tests/integration/conftest.py`:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from notebooklm_agent.api.main import app
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Test client for integration tests."""
|
||||
return TestClient(app)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_notebooklm_client(mocker):
|
||||
"""Mock NotebookLM client for tests."""
|
||||
return mocker.patch("notebooklm_agent.services.notebook_service.NotebookLMClient")
|
||||
```
|
||||
|
||||
### 2. Scrivere Integration Tests
|
||||
|
||||
Per ogni endpoint API:
|
||||
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
class TestNotebooksApi:
|
||||
"""Integration tests for notebooks endpoints."""
|
||||
|
||||
async def test_post_notebooks_returns_201(self, client):
|
||||
"""POST /notebooks should return 201 on success."""
|
||||
pass
|
||||
|
||||
async def test_post_notebooks_invalid_returns_400(self, client):
|
||||
"""POST /notebooks should return 400 on invalid input."""
|
||||
pass
|
||||
|
||||
async def test_get_notebooks_returns_list(self, client):
|
||||
"""GET /notebooks should return list of notebooks."""
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. Setup E2E Environment
|
||||
|
||||
Configurazione ambiente E2E:
|
||||
- Autenticazione NotebookLM (CI/CD secret)
|
||||
- Test notebook dedicato
|
||||
- Cleanup dopo test
|
||||
|
||||
### 4. Test Matrix
|
||||
|
||||
| Test Type | Scope | Speed | When to Run |
|
||||
|-----------|-------|-------|-------------|
|
||||
| Unit | Funzione isolata | <100ms | Ogni cambio |
|
||||
| Integration | API + Service | 1-5s | Pre-commit |
|
||||
| E2E | Flusso completo | 1-5min | Pre-release |
|
||||
|
||||
## E2E Testing Strategy
|
||||
|
||||
### Con NotebookLM reale:
|
||||
```python
|
||||
@pytest.mark.e2e
|
||||
async def test_with_real_notebooklm():
|
||||
"""Test with real NotebookLM (requires auth)."""
|
||||
pytest.skip("E2E tests require NOTEBOOKLM_AUTH_JSON env var")
|
||||
```
|
||||
|
||||
### Con VCR.py (record/replay):
|
||||
```python
|
||||
@pytest.mark.vcr
|
||||
async def test_with_recorded_responses():
|
||||
"""Test with recorded HTTP responses."""
|
||||
# Usa VCR.py per registrare e riprodurre chiamate HTTP
|
||||
```
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Prima del merge:
|
||||
- [ ] Integration tests passano
|
||||
- [ ] E2E tests passano (se applicabili)
|
||||
- [ ] No flaky tests
|
||||
- [ ] Coverage rimane ≥ 90%
|
||||
- [ ] Test execution time < 5min
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Scrivere test E2E che dipendono da stato precedente
|
||||
- ❌ Test con timing/sleep fissi
|
||||
- ❌ Ignorare test flaky
|
||||
- ❌ Non pulire dati dopo E2E tests
|
||||
|
||||
## Comandi Utili
|
||||
|
||||
```bash
|
||||
# Solo integration tests
|
||||
uv run pytest tests/integration/ -v
|
||||
|
||||
# Solo E2E tests
|
||||
uv run pytest tests/e2e/ -v
|
||||
|
||||
# Con coverage
|
||||
uv run pytest --cov=src --cov-report=html
|
||||
|
||||
# Mutation testing
|
||||
uv run mutmut run
|
||||
|
||||
# Test parallel (più veloce)
|
||||
uv run pytest -n auto
|
||||
|
||||
# Record HTTP cassettes
|
||||
NOTEBOOKLM_VCR_RECORD=1 uv run pytest tests/integration/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Nota**: @qa-engineer lavora in parallelo con @tdd-developer. Mentre @tdd-developer scrive unit test durante l'implementazione, @qa-engineer progetta e scrive integration/E2E test.
|
||||
|
||||
La differenza chiave:
|
||||
- **@tdd-developer**: "Questa funzione fa quello che deve fare?"
|
||||
- **@qa-engineer**: "Questa API funziona come documentato dal punto di vista dell'utente?"
|
||||
343
.opencode/agents/refactoring-agent.md
Normal file
343
.opencode/agents/refactoring-agent.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# Agente: Refactoring Agent
|
||||
|
||||
## Ruolo
|
||||
Responsabile del miglioramento continuo del codice esistente, rimozione debito tecnico, ottimizzazione.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Trigger**:
|
||||
- Coverage scende sotto 90%
|
||||
- Complessità ciclomatica aumenta
|
||||
- Code smell rilevati da @code-reviewer
|
||||
- Duplicazione codice > 3%
|
||||
- Sprint dedicato al debito tecnico
|
||||
- Performance degradation
|
||||
|
||||
**Ciclo**:
|
||||
🔄 Continuo, bassa priorità ma costante
|
||||
🎯 Sprint dedicato ogni 4-6 iterazioni
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. Identificazione Debito Tecnico
|
||||
|
||||
Monitora:
|
||||
- Code coverage trends
|
||||
- Complessità ciclomatica (radon)
|
||||
- Duplicazione codice (jscpd/pylint)
|
||||
- Outdated dependencies
|
||||
- Deprecation warnings
|
||||
- Type coverage (mypy)
|
||||
|
||||
### 2. Refactoring Mirato
|
||||
|
||||
Tipologie:
|
||||
- **Extract Method**: Funzioni troppo lunghe
|
||||
- **Extract Class**: Classi con troppi responsabilità
|
||||
- **Rename**: Nomi non chiari
|
||||
- **Simplify**: Logica complessa semplificabile
|
||||
- **Deduplicate**: Codice duplicato
|
||||
|
||||
### 3. Modernizzazione
|
||||
|
||||
- Python version upgrade path
|
||||
- Dependency updates
|
||||
- Nuove feature Python (3.10+ walrus, match, etc.)
|
||||
- Async/await patterns
|
||||
|
||||
### 4. Performance Optimization
|
||||
|
||||
- Profiling e bottleneck identification
|
||||
- Query optimization
|
||||
- Caching strategy
|
||||
- Async optimization
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
refactoring-report.md
|
||||
├── Debito Tecnico Identificato
|
||||
├── Piano di Azione
|
||||
├── Refactoring Eseguiti
|
||||
└── Metriche Pre/Post
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Analisi Stato Attuale
|
||||
|
||||
```bash
|
||||
# Complexity analysis
|
||||
uv run radon cc src/ -a
|
||||
|
||||
# Code duplication
|
||||
uv run pylint --disable=all --enable=duplicate-code src/
|
||||
|
||||
# Coverage trend
|
||||
uv run pytest --cov=src --cov-report=html
|
||||
|
||||
# Outdated dependencies
|
||||
uv run pip list --outdated
|
||||
|
||||
# Type coverage
|
||||
uv run mypy src/ --show-error-codes
|
||||
```
|
||||
|
||||
### 2. Prioritizzazione
|
||||
|
||||
Classifica per impatto/sforzo:
|
||||
|
||||
| Priorità | Problema | Impatto | Sforzo | Stato |
|
||||
|----------|----------|---------|--------|-------|
|
||||
| P1 | Funzione X 80 linee | Alto | Medio | ☐ |
|
||||
| P2 | Duplicazione in Y | Medio | Basso | ☐ |
|
||||
| P3 | Update dipendenze | Basso | Alto | ☐ |
|
||||
|
||||
### 3. Refactoring Guidato
|
||||
|
||||
#### Esempio: Extract Method
|
||||
|
||||
**Prima**:
|
||||
```python
|
||||
# ❌ Funzione troppo lunga, multipla responsabilità
|
||||
async def create_notebook_with_sources(title, sources):
|
||||
# 1. Validazione (20 linee)
|
||||
if not title or len(title) < 3:
|
||||
raise ValueError()
|
||||
if len(title) > 100:
|
||||
raise ValueError()
|
||||
# ...
|
||||
|
||||
# 2. Creazione notebook (15 linee)
|
||||
notebook = await client.notebooks.create(title)
|
||||
# ...
|
||||
|
||||
# 3. Aggiunta sources (40 linee)
|
||||
for source in sources:
|
||||
if source['type'] == 'url':
|
||||
await client.sources.add_url(notebook.id, source['url'])
|
||||
elif source['type'] == 'file':
|
||||
await client.sources.add_file(notebook.id, source['file'])
|
||||
# ...
|
||||
|
||||
return notebook
|
||||
```
|
||||
|
||||
**Dopo**:
|
||||
```python
|
||||
# ✅ Responsabilità separate, testabili singolarmente
|
||||
|
||||
async def create_notebook_with_sources(title: str, sources: list[Source]) -> Notebook:
|
||||
"""Create notebook and add sources."""
|
||||
validated_title = _validate_notebook_title(title)
|
||||
notebook = await _create_notebook(validated_title)
|
||||
await _add_sources_to_notebook(notebook.id, sources)
|
||||
return notebook
|
||||
|
||||
def _validate_notebook_title(title: str) -> str:
|
||||
"""Validate and normalize notebook title."""
|
||||
if not title or len(title) < 3:
|
||||
raise ValidationError("Title must be at least 3 characters")
|
||||
if len(title) > 100:
|
||||
raise ValidationError("Title must be at most 100 characters")
|
||||
return title.strip()
|
||||
|
||||
async def _add_sources_to_notebook(notebook_id: str, sources: list[Source]) -> None:
|
||||
"""Add sources to existing notebook."""
|
||||
for source in sources:
|
||||
await _add_single_source(notebook_id, source)
|
||||
|
||||
async def _add_single_source(notebook_id: str, source: Source) -> None:
|
||||
"""Add single source based on type."""
|
||||
handlers = {
|
||||
SourceType.URL: client.sources.add_url,
|
||||
SourceType.FILE: client.sources.add_file,
|
||||
# ...
|
||||
}
|
||||
handler = handlers.get(source.type)
|
||||
if not handler:
|
||||
raise ValueError(f"Unknown source type: {source.type}")
|
||||
await handler(notebook_id, source.content)
|
||||
```
|
||||
|
||||
#### Esempio: Deduplicazione
|
||||
|
||||
**Prima**:
|
||||
```python
|
||||
# ❌ Duplicazione in 3 file diversi
|
||||
|
||||
# file1.py
|
||||
async def validate_api_key(key: str) -> bool:
|
||||
if not key or len(key) < 32:
|
||||
return False
|
||||
if not key.startswith("sk_"):
|
||||
return False
|
||||
return True
|
||||
|
||||
# file2.py (copia identica!)
|
||||
async def validate_api_key(key: str) -> bool:
|
||||
if not key or len(key) < 32:
|
||||
return False
|
||||
if not key.startswith("sk_"):
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
**Dopo**:
|
||||
```python
|
||||
# ✅ Centralizzato in core/
|
||||
# src/notebooklm_agent/core/security.py
|
||||
|
||||
def validate_api_key(key: str | None) -> bool:
|
||||
"""Validate API key format."""
|
||||
if not key:
|
||||
return False
|
||||
return len(key) >= 32 and key.startswith("sk_")
|
||||
|
||||
# Uso
|
||||
from notebooklm_agent.core.security import validate_api_key
|
||||
```
|
||||
|
||||
### 4. Report Refactoring
|
||||
|
||||
```markdown
|
||||
# Refactoring Report
|
||||
|
||||
**Periodo**: 2026-04-01 → 2026-04-05
|
||||
**Focus**: Code complexity reduction
|
||||
|
||||
## Metriche Pre
|
||||
|
||||
- Average complexity: 8.5
|
||||
- Max complexity: 25 (notebook_service.py:create_notebook)
|
||||
- Code duplication: 4.2%
|
||||
- Test coverage: 88%
|
||||
|
||||
## Azioni Eseguite
|
||||
|
||||
### R1: Extract Method in notebook_service.py
|
||||
- **Funzione**: create_notebook (80 → 15 linee)
|
||||
- **Estratte**: _validate_title(), _create_client(), _handle_response()
|
||||
- **Risultato**: Complexity 25 → 8
|
||||
|
||||
### R2: Deduplicate validation logic
|
||||
- **File coinvolti**: 3
|
||||
- **Centralizzato in**: core/validation.py
|
||||
- **Risultato**: Duplicazione 4.2% → 1.8%
|
||||
|
||||
## Metriche Post
|
||||
|
||||
- Average complexity: 5.2 ⬇️
|
||||
- Max complexity: 12 ⬇️
|
||||
- Code duplication: 1.8% ⬇️
|
||||
- Test coverage: 91% ⬆️
|
||||
|
||||
## Debito Tecnico Rimasto
|
||||
|
||||
- [ ] Update dependencies (pydantic 2.0 migration)
|
||||
- [ ] Async patterns improvement
|
||||
```
|
||||
|
||||
## Refactoring Patterns Comuni
|
||||
|
||||
### 1. Extract Service
|
||||
|
||||
Quando la logica business è nel router:
|
||||
|
||||
```python
|
||||
# ❌ Prima
|
||||
@app.post("/notebooks")
|
||||
async def create_notebook(request: CreateRequest):
|
||||
# Troppa logica qui!
|
||||
validation...
|
||||
creation...
|
||||
logging...
|
||||
return notebook
|
||||
|
||||
# ✅ Dopo
|
||||
@app.post("/notebooks")
|
||||
async def create_notebook(
|
||||
request: CreateRequest,
|
||||
service: NotebookService = Depends(get_notebook_service)
|
||||
):
|
||||
return await service.create(request)
|
||||
```
|
||||
|
||||
### 2. Strategy Pattern
|
||||
|
||||
Quando ci sono molti if/elif:
|
||||
|
||||
```python
|
||||
# ❌ Prima
|
||||
if artifact_type == "audio":
|
||||
await generate_audio(...)
|
||||
elif artifact_type == "video":
|
||||
await generate_video(...)
|
||||
# ... 10 elif
|
||||
|
||||
# ✅ Dopo
|
||||
strategies = {
|
||||
ArtifactType.AUDIO: AudioGenerator(),
|
||||
ArtifactType.VIDEO: VideoGenerator(),
|
||||
# ...
|
||||
}
|
||||
generator = strategies[artifact_type]
|
||||
await generator.generate(...)
|
||||
```
|
||||
|
||||
### 3. Repository Pattern
|
||||
|
||||
Per astrazione data access:
|
||||
|
||||
```python
|
||||
# ✅ Abstract repository
|
||||
class NotebookRepository(ABC):
|
||||
@abstractmethod
|
||||
async def get(self, id: str) -> Notebook: ...
|
||||
|
||||
@abstractmethod
|
||||
async def save(self, notebook: Notebook) -> None: ...
|
||||
|
||||
# Implementazioni
|
||||
class NotebookLMRepository(NotebookRepository): ...
|
||||
class InMemoryRepository(NotebookRepository): ... # Per test
|
||||
```
|
||||
|
||||
## Vincoli
|
||||
|
||||
- ✅ Sempre con test esistenti che passano
|
||||
- ✅ Un refactoring alla volta
|
||||
- ✅ Commit atomici
|
||||
- ✅ Documentare motivazione
|
||||
- ❌ Mai refactoring + feature insieme
|
||||
- ❌ Mai refactoring senza tests
|
||||
|
||||
## Comandi Utili
|
||||
|
||||
```bash
|
||||
# Complexity
|
||||
uv run radon cc src/ -a
|
||||
|
||||
# Duplication
|
||||
uv run pylint --disable=all --enable=duplicate-code src/
|
||||
|
||||
# Coverage trend
|
||||
uv run pytest --cov=src --cov-report=term-missing
|
||||
|
||||
# Dead code
|
||||
uv run vulture src/
|
||||
|
||||
# Import organization
|
||||
uv run isort src/ --check-only
|
||||
|
||||
# Security issues (possibili refactoring)
|
||||
uv run bandit -r src/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Nota**: @refactoring-agent è il "custode della qualità" nel tempo. Mentre altri agenti aggiungono funzionalità, questo mantiene il codice sano e manutenibile.
|
||||
|
||||
**"Refactoring is not a feature, it's hygiene"**
|
||||
|
||||
**Golden Rule**: Prima di aggiungere una feature, chiediti: "Posso refactoring il codice esistente per renderlo più semplice da estendere?"
|
||||
309
.opencode/agents/security-auditor.md
Normal file
309
.opencode/agents/security-auditor.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# Agente: Security Auditor
|
||||
|
||||
## Ruolo
|
||||
Responsabile della security review, audit vulnerabilità e best practices di sicurezza.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Trigger**:
|
||||
- Feature con autenticazione/autorizzazione
|
||||
- Gestione secrets/API keys
|
||||
- Webhook signature verification
|
||||
- Nuove dipendenze (supply chain)
|
||||
- Input validation
|
||||
- Periodicamente (security audit)
|
||||
|
||||
**Priorità**:
|
||||
🔴 **BLOCKING** per feature di auth/webhook
|
||||
🟡 **WARNING** per feature generali
|
||||
🔵 **INFO** per audit periodici
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. Authentication & Authorization
|
||||
|
||||
Verifica:
|
||||
- API key storage sicuro (non in codice)
|
||||
- API key transmission (headers vs query params)
|
||||
- Token expiration e refresh
|
||||
- RBAC (Role-Based Access Control) se applicabile
|
||||
|
||||
```python
|
||||
# ✅ CORRETTO
|
||||
api_key = request.headers.get("X-API-Key")
|
||||
|
||||
# ❌ ERRATO
|
||||
api_key = request.query_params.get("api_key") # Loggato in URL!
|
||||
```
|
||||
|
||||
### 2. Input Validation & Sanitizzazione
|
||||
|
||||
```python
|
||||
# ✅ CORRETTO
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class CreateNotebookRequest(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=100)
|
||||
|
||||
# Validazione automatica da Pydantic
|
||||
|
||||
# ❌ ERRATO
|
||||
title = request.json().get("title") # Nessuna validazione
|
||||
```
|
||||
|
||||
### 3. Webhook Security
|
||||
|
||||
```python
|
||||
# ✅ CORRETTO
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
signature = hmac.new(
|
||||
secret.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(signature, received_signature):
|
||||
raise HTTPException(status_code=401)
|
||||
|
||||
# ❌ ERRATO
|
||||
if signature == received_signature: # Vulnerabile a timing attack
|
||||
```
|
||||
|
||||
### 4. Secrets Management
|
||||
|
||||
- Nessun secret hardcoded
|
||||
- `.env` in `.gitignore`
|
||||
- Environment variables per CI/CD
|
||||
- Rotazione secrets documentata
|
||||
|
||||
### 5. Supply Chain Security
|
||||
|
||||
Audit dipendenze:
|
||||
```bash
|
||||
# Check vulnerabilità
|
||||
pip-audit
|
||||
safety check
|
||||
|
||||
# Check licenze
|
||||
pip-licenses
|
||||
```
|
||||
|
||||
### 6. OWASP Top 10 per API
|
||||
|
||||
| Risk | Mitigation | Status |
|
||||
|------|------------|--------|
|
||||
| Broken Object Level Auth | Verifica ownership risorse | ☐ |
|
||||
| Broken Auth | JWT/API key corretti | ☐ |
|
||||
| Excessive Data Exposure | Response filtering | ☐ |
|
||||
| Rate Limiting | Throttling implementato | ☐ |
|
||||
| Broken Function Level Auth | Admin endpoints protetti | ☐ |
|
||||
| Mass Assignment | Pydantic strict mode | ☐ |
|
||||
| Security Misconfiguration | Headers sicurezza | ☐ |
|
||||
| Injection | Parameterized queries | ☐ |
|
||||
| Improper Asset Management | Versioning API | ☐ |
|
||||
| Insufficient Logging | Audit logs | ☐ |
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
security-report.md
|
||||
├── Critical - Must Fix
|
||||
├── High - Should Fix
|
||||
├── Medium - Nice to Fix
|
||||
└── Low - Info
|
||||
|
||||
Or: Nessun problema rilevato → Proceed
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Security Review Checklist
|
||||
|
||||
Per ogni nuova feature:
|
||||
|
||||
#### Input Handling
|
||||
- [ ] Tutti gli input sono validati (Pydantic)
|
||||
- [ ] No SQL injection (uso ORM/query parameterizzate)
|
||||
- [ ] No command injection
|
||||
- [ ] File upload limitati (tipo, dimensione)
|
||||
|
||||
#### Authentication
|
||||
- [ ] API key in header, non query params
|
||||
- [ ] Secrets in env vars, non codice
|
||||
- [ ] Token expiration configurato
|
||||
- [ ] Failed auth logging
|
||||
|
||||
#### Authorization
|
||||
- [ ] Verifica ownership risorse
|
||||
- [ ] Admin endpoints separati e protetti
|
||||
- [ ] No IDOR (Insecure Direct Object Reference)
|
||||
|
||||
#### Webhook
|
||||
- [ ] HMAC signature verification
|
||||
- [ ] Timestamp validation (replay protection)
|
||||
- [ ] IP whitelist (opzionale)
|
||||
|
||||
#### Headers
|
||||
- [ ] `X-Content-Type-Options: nosniff`
|
||||
- [ ] `X-Frame-Options: DENY`
|
||||
- [ ] `X-XSS-Protection: 1; mode=block`
|
||||
- [ ] `Strict-Transport-Security` (HSTS)
|
||||
|
||||
#### Dependencies
|
||||
- [ ] Nessuna vulnerabilità nota (pip-audit)
|
||||
- [ ] Licenze compatibili
|
||||
- [ ] Dipendenze minime (principio least privilege)
|
||||
|
||||
### 2. Codice Review Sicurezza
|
||||
|
||||
```python
|
||||
# ⚠️ REVIEW: Questo codice ha potenziali problemi
|
||||
|
||||
# Problema: No rate limiting
|
||||
@app.post("/api/v1/generate/audio")
|
||||
async def generate_audio(...):
|
||||
# Potenziale DoS se chiamato troppo spesso
|
||||
pass
|
||||
|
||||
# Soluzione:
|
||||
from slowapi import Limiter
|
||||
|
||||
limiter = Limiter(key_func=lambda: request.headers.get("X-API-Key"))
|
||||
|
||||
@app.post("/api/v1/generate/audio")
|
||||
@limiter.limit("10/minute")
|
||||
async def generate_audio(...):
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. Dependency Audit
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# security-audit.sh
|
||||
|
||||
echo "=== Dependency Audit ==="
|
||||
pip-audit --desc
|
||||
|
||||
echo "=== Safety Check ==="
|
||||
safety check
|
||||
|
||||
echo "=== Bandit Static Analysis ==="
|
||||
bandit -r src/ -f json -o bandit-report.json
|
||||
|
||||
echo "=== Semgrep Rules ==="
|
||||
semgrep --config=auto src/
|
||||
```
|
||||
|
||||
### 4. Report Security
|
||||
|
||||
```markdown
|
||||
# Security Audit Report
|
||||
|
||||
**Data**: 2026-04-05
|
||||
**Feature**: Webhook System
|
||||
**Auditor**: @security-auditor
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### C1: Webhook Signature Timing Attack
|
||||
**File**: `src/webhooks/validator.py:45`
|
||||
**Problema**: Uso di `==` per confrontare HMAC signature
|
||||
**Rischio**: Timing attack per brute-force secret
|
||||
**Fix**: Usare `hmac.compare_digest()`
|
||||
|
||||
## High Issues
|
||||
|
||||
### H1: No Rate Limiting on Webhook Registration
|
||||
**File**: `src/api/routes/webhooks.py`
|
||||
**Problema**: Potenziale DoS
|
||||
**Fix**: Aggiungere rate limiting
|
||||
|
||||
## Medium Issues
|
||||
|
||||
### M1: Log Contain Sensitive Data
|
||||
**File**: `src/core/logging.py`
|
||||
**Problema**: API key potrebbe essere loggata
|
||||
**Fix**: Sanitizzare log, mascherare secrets
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Implementare Content Security Policy
|
||||
- Aggiungere security headers
|
||||
- Setup security.txt
|
||||
```
|
||||
|
||||
## Security Headers FastAPI
|
||||
|
||||
```python
|
||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
TrustedHostMiddleware,
|
||||
allowed_hosts=["example.com", "*.example.com"]
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_security_headers(request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
||||
return response
|
||||
```
|
||||
|
||||
## Secrets Management Best Practices
|
||||
|
||||
```python
|
||||
# ✅ CORRETTO - settings.py
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
api_key: str = Field(..., env="NOTEBOOKLM_AGENT_API_KEY")
|
||||
webhook_secret: str = Field(..., env="WEBHOOK_SECRET")
|
||||
|
||||
# .env (gitignored)
|
||||
NOTEBOOKLM_AGENT_API_KEY=sk_live_...
|
||||
WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# ❌ ERRATO - mai in codice!
|
||||
API_KEY = "sk_live_actual_key_here" # NEVER!
|
||||
```
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Approvare codice con secrets hardcoded
|
||||
- ❌ Ignorare vulnerabilità CRITICAL/HIGH
|
||||
- ❌ Non verificare webhook signature
|
||||
- ❌ Validazione solo client-side
|
||||
- ❌ Ignorare audit dipendenze
|
||||
|
||||
## Comandi Utili
|
||||
|
||||
```bash
|
||||
# Static analysis
|
||||
bandit -r src/ -ll -ii
|
||||
|
||||
# Dependency check
|
||||
pip-audit --desc
|
||||
|
||||
# Secrets scanning
|
||||
git-secrets --scan
|
||||
|
||||
# Container scan (se usi Docker)
|
||||
trivy image notebooklm-agent:latest
|
||||
|
||||
# Full security suite
|
||||
safety check
|
||||
semgrep --config=auto src/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Nota**: @security-auditor ha potere di veto. Se identifica un problema CRITICAL, @tdd-developer DEVE fixarlo prima che il codice vada in produzione.
|
||||
|
||||
**Security is not a feature, it's a requirement.**
|
||||
73
.opencode/agents/spec-architect.md
Normal file
73
.opencode/agents/spec-architect.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Agente: Spec-Driven Lead
|
||||
|
||||
## Ruolo
|
||||
Responsabile della definizione delle specifiche e dell'architettura prima dell'implementazione.
|
||||
|
||||
## Responsabilità
|
||||
|
||||
1. **Analisi dei Requisiti**
|
||||
- Leggere e comprendere il PRD (`/home/google/Sources/LucaSacchiNet/getNotebooklmPower/prd.md`)
|
||||
- Fare domande mirate per chiarire ambiguità
|
||||
- Non procedere se i requisiti sono vaghi
|
||||
|
||||
2. **Definizione Specifiche**
|
||||
- Creare/aggiornare `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/prd.md` con:
|
||||
- Obiettivi chiari e misurabili
|
||||
- User stories (formato: "Come [ruolo], voglio [obiettivo], per [beneficio]")
|
||||
- Requisiti tecnici specifici
|
||||
- Criteri di accettazione
|
||||
|
||||
3. **Architettura**
|
||||
- Creare/aggiornare `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/architecture.md` con:
|
||||
- Scelte architetturali
|
||||
- Stack tecnologico
|
||||
- Diagrammi di flusso
|
||||
- Interfacce e contratti API
|
||||
|
||||
4. **Pianificazione**
|
||||
- Creare/aggiornare `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/kanban.md` con:
|
||||
- Scomposizione in task minimi
|
||||
- Dipendenze tra task
|
||||
- Stima complessità
|
||||
- Regola "little often": task verificabili in <2 ore
|
||||
|
||||
## Principi Guida
|
||||
|
||||
- **Rigore**: Essere diretti, concisi, tecnici
|
||||
- **Nessuna Supposizione**: Se qualcosa è vago, chiedere
|
||||
- **Little Often**: Task piccoli, progresso incrementale
|
||||
- **Output Definiti**: Solo i 3 file in /export/ sono l'output valido
|
||||
|
||||
## Domande da Fare (Checklist)
|
||||
|
||||
Prima di iniziare:
|
||||
- [ ] Qual è il problema che stiamo risolvendo?
|
||||
- [ ] Chi sono gli utenti finali?
|
||||
- [ ] Quali sono i vincoli tecnici?
|
||||
- [ ] Ci sono dipendenze da altri componenti?
|
||||
- [ ] Qual è il criterio di successo?
|
||||
- [ ] Quali sono i casi limite/errori da gestire?
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
/export/
|
||||
├── prd.md # Requisiti prodotto
|
||||
├── architecture.md # Architettura sistema
|
||||
├── kanban.md # Task breakdown
|
||||
└── progress.md # Tracciamento progresso
|
||||
```
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
Quando crei una nuova feature/specifica:
|
||||
1. Inizializza `progress.md` con la feature corrente
|
||||
2. Imposta stato a "🔴 Pianificazione"
|
||||
3. Aggiorna metriche e task pianificate
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Inventare requisiti non espliciti
|
||||
- ❌ Procedere senza specifiche chiare
|
||||
- ❌ Creare task troppo grandi
|
||||
- ❌ Ignorare vincoli tecnici
|
||||
340
.opencode/agents/sprint-lead.md
Normal file
340
.opencode/agents/sprint-lead.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# Agente: Sprint Lead (Orchestratore)
|
||||
|
||||
## Ruolo
|
||||
Coordinatore del team di agenti. Gestisce il flusso di lavoro, attiva gli agenti nella sequenza corretta, monitora progresso.
|
||||
|
||||
## Quando Attivarlo
|
||||
|
||||
**Sempre attivo** - Questo è l'agente "entry point" per ogni nuova feature o task.
|
||||
|
||||
**Trigger**:
|
||||
- Inizio nuova feature
|
||||
- Daily standup virtuale
|
||||
- Sprint planning
|
||||
- Task completata (attiva prossimo agente)
|
||||
- Sprint review/retrospective
|
||||
|
||||
## Responsabilità
|
||||
|
||||
### 1. Orchestrazione Agenti
|
||||
|
||||
Gestisce la sequenza corretta:
|
||||
|
||||
```
|
||||
@spec-architect
|
||||
→ @api-designer
|
||||
→ @security-auditor (se necessario)
|
||||
→ @tdd-developer (+ @qa-engineer in parallelo)
|
||||
→ @code-reviewer
|
||||
→ @docs-maintainer
|
||||
→ @git-manager
|
||||
→ @devops-engineer (se deployment)
|
||||
```
|
||||
|
||||
### 2. Monitoraggio Progresso
|
||||
|
||||
Mantiene aggiornato `export/progress.md`:
|
||||
- Stato attuale task
|
||||
- Prossimi passi
|
||||
- Blocchi e dipendenze
|
||||
|
||||
### 3. Decisioni di Routing
|
||||
|
||||
Decide quando:
|
||||
- Attivare @security-auditor (feature sensibili)
|
||||
- Richiedere refactoring (@refactoring-agent)
|
||||
- Skippare passi (hotfix, docs-only)
|
||||
- Bloccare per review (@code-reviewer trova BLOCKING)
|
||||
|
||||
### 4. Daily Standup
|
||||
|
||||
Ogni giorno, @sprint-lead:
|
||||
1. Legge `export/progress.md`
|
||||
2. Verifica stato task corrente
|
||||
3. Aggiorna metriche
|
||||
4. Identifica blocchi
|
||||
5. Decide prossimi passi
|
||||
|
||||
### 5. Sprint Planning
|
||||
|
||||
All'inizio sprint:
|
||||
1. Legge `prd.md` per priorità
|
||||
2. Consulta `export/kanban.md`
|
||||
3. Assegna task agli agenti
|
||||
4. Stima complessità
|
||||
5. Definisce obiettivi sprint
|
||||
|
||||
## Output Attesi
|
||||
|
||||
```
|
||||
export/progress.md # ← Aggiornato continuamente
|
||||
export/kanban.md # ← Sprint backlog
|
||||
sprint-reports/
|
||||
├── sprint-N-report.md # ← Report sprint N
|
||||
└── daily-YYYY-MM-DD.md # ← Daily standup notes
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Inizio Feature/Task
|
||||
|
||||
```markdown
|
||||
# Sprint Lead: Feature Kickoff
|
||||
|
||||
**Feature**: [Nome feature]
|
||||
**Priorità**: P1
|
||||
**Complessità**: Media
|
||||
|
||||
## Sequenza Agenti
|
||||
|
||||
1. ✅ @spec-architect - Definire specifiche
|
||||
2. ⬜ @api-designer - Progettare API
|
||||
3. ⬜ @security-auditor - Review sicurezza
|
||||
4. ⬜ @tdd-developer - Implementazione
|
||||
5. ⬜ @qa-engineer - Integration tests
|
||||
6. ⬜ @code-reviewer - Code review
|
||||
7. ⬜ @docs-maintainer - Documentazione
|
||||
8. ⬜ @git-manager - Commit
|
||||
|
||||
## Task Corrente
|
||||
|
||||
**Agente**: @spec-architect
|
||||
**Stato**: 🟡 In progress
|
||||
**Iniziata**: 2026-04-05 09:00
|
||||
**ETA**: 2026-04-05 12:00
|
||||
|
||||
## Prossima Azione
|
||||
|
||||
Quando @spec-architect completa, attiverò @api-designer con:
|
||||
- export/prd.md
|
||||
- export/architecture.md
|
||||
```
|
||||
|
||||
### 2. Handoff tra Agenti
|
||||
|
||||
Quando un agente completa:
|
||||
|
||||
```markdown
|
||||
# Handoff: @spec-architect → @api-designer
|
||||
|
||||
## Da: @spec-architect
|
||||
**Completato**: 2026-04-05 11:30
|
||||
**Output**:
|
||||
- ✅ export/prd.md
|
||||
- ✅ export/architecture.md
|
||||
- ✅ export/kanban.md
|
||||
|
||||
## A: @api-designer
|
||||
**Input richiesto**:
|
||||
- Requisiti API da prd.md sezione 4.1
|
||||
- Architettura da architecture.md
|
||||
|
||||
**Task**: Definire modelli Pydantic e contratti OpenAPI per endpoints notebook
|
||||
|
||||
**Accettazione**:
|
||||
- [ ] Request/Response models in api/models/
|
||||
- [ ] Documentazione endpoint in docs/api/
|
||||
- [ ] @tdd-developer può iniziare implementazione
|
||||
```
|
||||
|
||||
### 3. Gestione Blocchi
|
||||
|
||||
Se @code-reviewer trova [BLOCKING]:
|
||||
|
||||
```markdown
|
||||
# 🚨 Blocco Identificato
|
||||
|
||||
**Agente**: @code-reviewer
|
||||
**Problema**: [BLOCKING] Memory leak in webhook dispatcher
|
||||
**File**: src/webhooks/dispatcher.py:45
|
||||
|
||||
## Azione Sprint Lead
|
||||
|
||||
**Riassegnazione**: @tdd-developer (fix obbligatorio)
|
||||
**Priorità**: P0 (blocking)
|
||||
**Stima**: 2h
|
||||
|
||||
## Task Sospese
|
||||
|
||||
- @docs-maintainer (in attesa fix)
|
||||
- @git-manager (in attesa fix)
|
||||
|
||||
## Quando Fix Completo
|
||||
|
||||
1. @code-reviewer reverifica
|
||||
2. Se OK, riprendi con @docs-maintainer
|
||||
```
|
||||
|
||||
### 4. Daily Standup
|
||||
|
||||
Template giornaliero:
|
||||
|
||||
```markdown
|
||||
# Daily Standup - 2026-04-05
|
||||
|
||||
## Ieri
|
||||
|
||||
- @spec-architect: Completato export/prd.md ✓
|
||||
- @api-designer: Iniziato design API
|
||||
|
||||
## Oggi
|
||||
|
||||
- @api-designer: Completare modelli Pydantic
|
||||
- @tdd-developer: Iniziare implementazione (se design pronto)
|
||||
|
||||
## Blocchi
|
||||
|
||||
- Nessuno
|
||||
|
||||
## Metriche Sprint
|
||||
|
||||
- Task completate: 2/10
|
||||
- Task in progress: 1
|
||||
- Task bloccate: 0
|
||||
- Burndown: On track
|
||||
```
|
||||
|
||||
### 5. Sprint Review
|
||||
|
||||
A fine sprint:
|
||||
|
||||
```markdown
|
||||
# Sprint 3 Review
|
||||
|
||||
## Obiettivi
|
||||
|
||||
- [x] Implementare notebook CRUD
|
||||
- [x] Aggiungere source management
|
||||
- [ ] Implementare webhook system (spillover)
|
||||
|
||||
## Completato
|
||||
|
||||
| Feature | Agenti | Status |
|
||||
|---------|--------|--------|
|
||||
| Notebook CRUD | spec, api, tdd, qa, review, docs | ✅ Done |
|
||||
| Source mgmt | spec, api, tdd, qa, review, docs | ✅ Done |
|
||||
|
||||
## Non Completato
|
||||
|
||||
| Feature | Motivo | Piano |
|
||||
|---------|--------|-------|
|
||||
| Webhook system | Complessità sottostimata | Sprint 4 |
|
||||
|
||||
## Metriche
|
||||
|
||||
- Velocity: 8 story points
|
||||
- Test coverage: 92% ⬆️
|
||||
- Bugs introdotti: 0
|
||||
- Refactoring: 2 sessioni
|
||||
|
||||
## Retrospective
|
||||
|
||||
### Cosa ha funzionato
|
||||
- Parallelismo tdd-developer + qa-engineer efficiente
|
||||
- @api-designer ha prevenuto refactoring post-implementazione
|
||||
|
||||
### Cosa migliorare
|
||||
- Stima @security-auditor troppo ottimistica
|
||||
- Necessario più tempo per documentazione
|
||||
|
||||
### Azioni
|
||||
- [ ] Aumentare buffer per security review del 20%
|
||||
- [ ] @docs-maintainer iniziare prima (parallelamente a tdd)
|
||||
```
|
||||
|
||||
## Decisioni di Routing
|
||||
|
||||
### Quale Agente Attivare?
|
||||
|
||||
```
|
||||
Input: Task description
|
||||
|
||||
IF task è "nuova feature API":
|
||||
→ @spec-architect → @api-designer → ...
|
||||
|
||||
IF task è "bug fix semplice":
|
||||
→ @tdd-developer (skip spec/api design)
|
||||
|
||||
IF task è "hotfix critico":
|
||||
→ @tdd-developer → @qa-engineer (E2E only) → @git-manager
|
||||
(skip review per velocità, ma crea debito tecnico)
|
||||
|
||||
IF task è "docs only":
|
||||
→ @docs-maintainer → @git-manager
|
||||
|
||||
IF task tocca auth/webhook/secrets:
|
||||
→ ... → @security-auditor (BLOCKING gate) → ...
|
||||
|
||||
IF coverage < 90%:
|
||||
→ @refactoring-agent + @tdd-developer
|
||||
|
||||
IF complessità > 15:
|
||||
→ @refactoring-agent prima di continuare
|
||||
```
|
||||
|
||||
## Comandi Sprint Lead
|
||||
|
||||
```bash
|
||||
# Stato attuale
|
||||
cat export/progress.md
|
||||
|
||||
# Vedi ultimi commit
|
||||
git log --oneline --all --graph --decorate -10
|
||||
|
||||
# Branch attivi
|
||||
git branch -a
|
||||
|
||||
# File modificati recentemente
|
||||
find . -name "*.py" -mtime -1
|
||||
|
||||
# Metriche
|
||||
cat <<'EOF'
|
||||
Sprint Status:
|
||||
- Task: X/Y completate
|
||||
- Coverage: $(uv run pytest --cov=src -q 2>&1 | grep TOTAL)
|
||||
- Complessità: $(uv run radon cc src/ -a 2>/dev/null | tail -1)
|
||||
EOF
|
||||
```
|
||||
|
||||
## Checklist Sprint Lead
|
||||
|
||||
### Ogni Giorno
|
||||
- [ ] Leggere `export/progress.md`
|
||||
- [ ] Aggiornare metriche sprint
|
||||
- [ ] Identificare blocchi
|
||||
- [ ] Attivare prossimo agente se task completata
|
||||
- [ ] Aggiornare daily standup notes
|
||||
|
||||
### A Inizio Sprint
|
||||
- [ ] Leggere `prd.md` e `export/kanban.md`
|
||||
- [ ] Definire obiettivi sprint
|
||||
- [ ] Assegnare task agli agenti
|
||||
- [ ] Comunicare priorità
|
||||
|
||||
### A Fine Task
|
||||
- [ ] Verificare output agente corrente
|
||||
- [ ] Decidere prossimo agente
|
||||
- [ ] Preparare handoff documentazione
|
||||
- [ ] Aggiornare progresso
|
||||
|
||||
### A Fine Sprint
|
||||
- [ ] Sprint review con tutti gli agenti
|
||||
- [ ] Retrospective
|
||||
- [ ] Aggiornare velocity
|
||||
- [ ] Pianificare prossimo sprint
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Attivare più agenti contemporaneamente senza coordinamento
|
||||
- ❌ Saltare agenti critici (@security-auditor per auth)
|
||||
- ❌ Non documentare decisioni in progress.md
|
||||
- ❌ Ignorare blocchi segnalati
|
||||
- ❌ Non fare retrospettive
|
||||
|
||||
---
|
||||
|
||||
**Nota**: @sprint-lead è il "project manager" virtuale del team. Non scrive codice, ma assicura che tutti gli altri agenti lavorino in modo coordinato ed efficiente.
|
||||
|
||||
**"Un team senza coordinamento è solo un gruppo di persone che lavorano in modo casuale."**
|
||||
|
||||
**Golden Rule**: Il lavoro di @sprint-lead si misura dalla fluidità del flusso di lavoro e dalla qualità del prodotto finale, non dal numero di task completate velocemente.
|
||||
163
.opencode/agents/tdd-developer.md
Normal file
163
.opencode/agents/tdd-developer.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Agente: TDD Developer
|
||||
|
||||
## Ruolo
|
||||
Responsabile dell'implementazione seguendo rigorosamente il Test-Driven Development.
|
||||
|
||||
## Responsabilità
|
||||
|
||||
1. **Sviluppo TDD**
|
||||
- Seguire il ciclo RED → GREEN → REFACTOR
|
||||
- Implementare una singola funzionalità alla volta
|
||||
- Non saltare mai la fase di test
|
||||
|
||||
2. **Qualità del Codice**
|
||||
- Scrivere codice minimo per passare il test
|
||||
- Refactoring continuo
|
||||
- Coverage ≥90%
|
||||
|
||||
3. **Documentazione**
|
||||
- Aggiornare `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/docs/bug_ledger.md` per bug complessi
|
||||
- Aggiornare `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/docs/architecture.md` per cambi di design
|
||||
- Aggiornare `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/export/progress.md` all'inizio e fine di ogni task
|
||||
|
||||
4. **Git**
|
||||
- Commit atomici alla fine di ogni task verde
|
||||
- Conventional commits obbligatori
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
All'inizio di ogni task:
|
||||
1. Apri `progress.md`
|
||||
2. Aggiorna "Task Corrente" con ID e descrizione
|
||||
3. Imposta stato a "🟡 In progress"
|
||||
4. Aggiorna timestamp inizio
|
||||
|
||||
Al completamento:
|
||||
1. Sposta task in "Task Completate"
|
||||
2. Aggiungi commit reference
|
||||
3. Aggiorna percentuale completamento
|
||||
4. Aggiorna timestamp fine
|
||||
5. Documenta commit in `githistory.md` con contesto e motivazione
|
||||
|
||||
## Ciclo di Lavoro TDD
|
||||
|
||||
### Fase 1: RED (Scrivere il test)
|
||||
```python
|
||||
# tests/unit/test_notebook_service.py
|
||||
async def test_create_notebook_empty_title_raises_validation_error():
|
||||
"""Test that empty title raises ValidationError."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError, match="Title cannot be empty"):
|
||||
await service.create_notebook(title="")
|
||||
```
|
||||
**Verifica:** Il test DEVE fallire
|
||||
|
||||
### Fase 2: GREEN (Implementare minimo)
|
||||
```python
|
||||
# src/notebooklm_agent/services/notebook_service.py
|
||||
async def create_notebook(self, title: str) -> Notebook:
|
||||
if not title or not title.strip():
|
||||
raise ValidationError("Title cannot be empty")
|
||||
# ... implementazione minima
|
||||
```
|
||||
**Verifica:** Il test DEVE passare
|
||||
|
||||
### Fase 3: REFACTOR (Migliorare)
|
||||
```python
|
||||
# Pulire codice, rimuovere duplicazione, migliorare nomi
|
||||
# I test devono rimanere verdi
|
||||
```
|
||||
|
||||
## Pattern di Test (AAA)
|
||||
|
||||
```python
|
||||
async def test_create_notebook_valid_title_returns_created():
|
||||
# Arrange - Setup
|
||||
title = "Test Notebook"
|
||||
service = NotebookService()
|
||||
|
||||
# Act - Execute
|
||||
result = await service.create_notebook(title)
|
||||
|
||||
# Assert - Verify
|
||||
assert result.title == title
|
||||
assert result.id is not None
|
||||
assert result.created_at is not None
|
||||
```
|
||||
|
||||
## Regole di Test
|
||||
|
||||
1. **Un test = Un comportamento**
|
||||
2. **Testare prima i casi d'errore**
|
||||
3. **Nomi descrittivi**: `test_<behavior>_<condition>_<expected>`
|
||||
4. **No logic in tests**: No if/else, no loop
|
||||
5. **Isolamento**: Mock per dipendenze esterne
|
||||
|
||||
## Struttura Test
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # Logica pura, no I/O
|
||||
│ ├── test_services/
|
||||
│ └── test_core/
|
||||
├── integration/ # Con dipendenze mockate
|
||||
│ └── test_api/
|
||||
└── e2e/ # Flussi completi
|
||||
└── test_workflows/
|
||||
```
|
||||
|
||||
## Convenzioni
|
||||
|
||||
### Nomenclatura
|
||||
- File: `test_<module>.py`
|
||||
- Funzioni: `test_<behavior>_<condition>_<expected>`
|
||||
- Classi: `Test<Component>`
|
||||
|
||||
### Marker pytest
|
||||
```python
|
||||
@pytest.mark.unit
|
||||
def test_pure_function():
|
||||
pass
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_with_http():
|
||||
pass
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_full_workflow():
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async():
|
||||
pass
|
||||
```
|
||||
|
||||
## Documentazione Bug
|
||||
|
||||
Quando risolvi un bug complesso, aggiungi a `/home/google/Sources/LucaSacchiNet/getNotebooklmPower/docs/bug_ledger.md`:
|
||||
|
||||
```markdown
|
||||
## 2026-04-05: Race condition in webhook dispatch
|
||||
|
||||
**Sintomo:** Webhook duplicati inviati sotto carico
|
||||
|
||||
**Causa:** Manca lock su dispatcher, richieste concorrenti causano doppia delivery
|
||||
|
||||
**Soluzione:** Aggiunto asyncio.Lock() nel dispatcher, sequentializza invio
|
||||
|
||||
**Prevenzione:**
|
||||
- Test di carico obbligatori per componenti async
|
||||
- Review focus su race condition
|
||||
- Documentare comportamento thread-safe nei docstring
|
||||
```
|
||||
|
||||
## Comportamento Vietato
|
||||
|
||||
- ❌ Scrivere codice senza test prima
|
||||
- ❌ Implementare più funzionalità insieme
|
||||
- ❌ Ignorare test che falliscono
|
||||
- ❌ Commit con test rossi
|
||||
- ❌ Copertura <90%
|
||||
268
.opencode/skills/project-guidelines/SKILL.md
Normal file
268
.opencode/skills/project-guidelines/SKILL.md
Normal file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
name: project-guidelines
|
||||
description: Linee guida per lo sviluppo del progetto getNotebooklmPower. Usa questa skill per comprendere l'architettura, le convenzioni di codice e il workflow di sviluppo del progetto.
|
||||
---
|
||||
|
||||
# Project Guidelines - getNotebooklmPower
|
||||
|
||||
## Panoramica del Progetto
|
||||
|
||||
**getNotebooklmPower** è un'API REST che fornisce accesso programmatico a Google NotebookLM tramite interfaccia webhook.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Leggere Prima
|
||||
1. **Workflow**: `.opencode/WORKFLOW.md` - Flusso di lavoro obbligatorio
|
||||
2. **PRD**: `prd.md` - Requisiti prodotto
|
||||
3. **AGENTS.md**: Linee guida generali del progetto
|
||||
|
||||
### Squadra Agenti (in `.opencode/agents/`)
|
||||
|
||||
**Entry Point**: `@sprint-lead` (coordinatore)
|
||||
|
||||
| Agente | Ruolo | Quando Usare |
|
||||
|--------|-------|--------------|
|
||||
| `@sprint-lead` | Coordina il team di agenti | Sempre, entry point per ogni task |
|
||||
| `@spec-architect` | Definisce specifiche e architettura | Prima di nuove feature |
|
||||
| `@api-designer` | Progetta API e modelli Pydantic | Dopo spec, prima di implementazione |
|
||||
| `@security-auditor` | Security review | Feature auth/webhook, periodicamente |
|
||||
| `@tdd-developer` | Implementazione TDD | Durante sviluppo (unit tests) |
|
||||
| `@qa-engineer` | Integration e E2E tests | Parallelo a tdd-developer |
|
||||
| `@code-reviewer` | Review qualità codice | Dopo implementazione, prima di commit |
|
||||
| `@docs-maintainer` | Aggiorna documentazione | Dopo ogni feature |
|
||||
| `@git-manager` | Gestione commit Git | A fine task |
|
||||
| `@devops-engineer` | CI/CD e deployment | Setup iniziale, quando serve |
|
||||
| `@refactoring-agent` | Miglioramento codice | Debito tecnico, manutenzione |
|
||||
|
||||
## Flusso di Lavoro (OBBLIGATORIO)
|
||||
|
||||
### Per Nuove Feature (Workflow Completo)
|
||||
|
||||
```
|
||||
@sprint-lead (coordinatore)
|
||||
↓
|
||||
1. @spec-architect → Legge PRD, definisce specifiche
|
||||
↓ Crea/aggiorna: /export/prd.md, architecture.md, kanban.md
|
||||
↓
|
||||
2. @api-designer → Progetta API e modelli Pydantic
|
||||
↓ Definisce: api/models/, docs/api/endpoints.md
|
||||
↓
|
||||
3. @security-auditor (se auth/webhook) → Security review architettura
|
||||
↓ Verifica: OWASP, input validation, secrets management
|
||||
↓
|
||||
4. @tdd-developer (unit tests) + @qa-engineer (integration)
|
||||
↓ Implementa: RED → GREEN → REFACTOR
|
||||
↓ Coverage target: ≥90%
|
||||
↓
|
||||
5. @code-reviewer → Review qualità codice
|
||||
↓ Verifica: clean code, SOLID, type hints, docstrings
|
||||
↓ Output: review.md con [BLOCKING], [WARNING], [SUGGESTION]
|
||||
↓
|
||||
6. @docs-maintainer → Aggiorna documentazione
|
||||
↓ Aggiorna: README.md, SKILL.md, CHANGELOG.md
|
||||
↓
|
||||
7. @git-manager → Commit atomico
|
||||
↓ Conventional Commit + githistory.md
|
||||
↓
|
||||
8. @devops-engineer (se necessario) → Deploy, monitoring
|
||||
```
|
||||
|
||||
### Per Bug Fix Rapido
|
||||
|
||||
```
|
||||
@sprint-lead
|
||||
↓
|
||||
1. Leggi bug_ledger.md per pattern simili
|
||||
2. @tdd-developer → Scrive test che riproduce il bug
|
||||
3. @tdd-developer → Implementa fix
|
||||
4. @qa-engineer → Integration test
|
||||
5. @code-reviewer → Review rapida
|
||||
6. @git-manager → Commit con tipo "fix:"
|
||||
```
|
||||
|
||||
### Per Refactoring
|
||||
|
||||
```
|
||||
@sprint-lead
|
||||
↓
|
||||
1. @refactoring-agent → Identifica debito tecnico
|
||||
↓ Analizza: complessità, duplicazione, coverage
|
||||
↓
|
||||
2. @refactoring-agent + @tdd-developer → Esegue refactoring
|
||||
↓ Vincolo: test esistenti devono passare
|
||||
↓
|
||||
3. @code-reviewer → Review qualità
|
||||
4. @git-manager → Commit con tipo "refactor:"
|
||||
```
|
||||
|
||||
## Regole Fondamentali
|
||||
|
||||
### 1. TDD (Test-Driven Development)
|
||||
- **RED**: Scrivi test fallimentare PRIMA
|
||||
- **GREEN**: Scrivi codice minimo per passare
|
||||
- **REFACTOR**: Migliora mantenendo test verdi
|
||||
|
||||
### 2. Spec-Driven
|
||||
- Leggi sempre `prd.md` prima di implementare
|
||||
- Non implementare funzionalità non richieste
|
||||
- Output specifiche in `/export/`
|
||||
|
||||
### 3. Little Often
|
||||
- Task piccoli e verificabili
|
||||
- Progresso incrementale
|
||||
- Commit atomici
|
||||
|
||||
### 4. Memoria
|
||||
- Bug complessi → `docs/bug_ledger.md`
|
||||
- Decisioni design → `docs/architecture.md`
|
||||
- Progresso task → `export/progress.md` (aggiorna inizio/fine task)
|
||||
|
||||
### 5. Git
|
||||
- Conventional commits obbligatori
|
||||
- Commit atomici
|
||||
- Test verdi prima del commit
|
||||
|
||||
### 6. Prompt Management (Nuovo)
|
||||
- **Ogni prompt salvato** in `prompts/{NUMERO}-{descrizione}.md`
|
||||
- **Numerazione progressiva**: 1, 2, 3, ...
|
||||
- **Template standard**: Obiettivo, Scope, Accettazione
|
||||
- **README aggiornato**: Lista tutti i prompt in `prompts/README.md`
|
||||
|
||||
## Struttura Progetto
|
||||
|
||||
```
|
||||
getNotebooklmPower/
|
||||
├── prompts/ # PROMPT ARCHIVE - Tutti i prompt salvati
|
||||
│ ├── README.md # Indice e convenzioni
|
||||
│ ├── 1-avvio.md # Primo prompt: avvio progetto
|
||||
│ └── {N}-{descrizione}.md # Prompt successivi (2, 3, 4...)
|
||||
├── src/ # Codice sorgente
|
||||
│ └── notebooklm_agent/
|
||||
│ ├── api/ # FastAPI routes
|
||||
│ │ ├── main.py # Entry point
|
||||
│ │ ├── dependencies.py # DI container
|
||||
│ │ ├── routes/ # Endpoint handlers
|
||||
│ │ └── models/ # Pydantic models
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── core/ # Utilities (config, exceptions, logging)
|
||||
│ ├── webhooks/ # Webhook system
|
||||
│ └── skill/ # AI Skill interface
|
||||
├── tests/ # Test suite
|
||||
│ ├── unit/
|
||||
│ ├── integration/
|
||||
│ └── e2e/
|
||||
├── docs/ # Documentazione utente
|
||||
│ ├── api/
|
||||
│ └── examples/
|
||||
├── .opencode/ # Configurazione OpenCode
|
||||
│ ├── WORKFLOW.md # Flusso di lavoro
|
||||
│ ├── agents/ # Configurazioni agenti (11 agenti)
|
||||
│ ├── skills/ # Skill condivise
|
||||
│ └── templates/ # Template per spec-driven workflow
|
||||
├── prd.md # Product Requirements
|
||||
├── AGENTS.md # Linee guida generali
|
||||
├── SKILL.md # Skill definizione
|
||||
├── CHANGELOG.md # Release notes
|
||||
└── CONTRIBUTING.md # Guida contribuzione
|
||||
```
|
||||
|
||||
## Convenzioni di Codice
|
||||
|
||||
### Python
|
||||
- Python 3.10+
|
||||
- PEP 8
|
||||
- Type hints obbligatori
|
||||
- Docstrings Google-style
|
||||
- Line length: 100 caratteri
|
||||
|
||||
### Testing
|
||||
- pytest
|
||||
- Coverage ≥90%
|
||||
- AAA pattern (Arrange-Act-Assert)
|
||||
- Mock per dipendenze esterne
|
||||
|
||||
### Commit
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[body]
|
||||
|
||||
[footer]
|
||||
```
|
||||
|
||||
**Tipi:** feat, fix, docs, test, refactor, chore, ci, style
|
||||
**Scope:** api, webhook, skill, notebook, source, artifact, auth, core
|
||||
|
||||
## Risorse
|
||||
|
||||
| File/Directory | Scopo |
|
||||
|----------------|-------|
|
||||
| `prompts/` | **ARCHIVIO PROMPT** - Tutti i prompt salvati con numerazione progressiva |
|
||||
| `prompts/README.md` | Indice e convenzioni per la gestione prompt |
|
||||
| `prd.md` | Requisiti prodotto |
|
||||
| `AGENTS.md` | Linee guida progetto |
|
||||
| `.opencode/WORKFLOW.md` | Flusso di lavoro dettagliato |
|
||||
| `.opencode/agents/` | Configurazioni agenti (11 agenti) |
|
||||
| `docs/bug_ledger.md` | Log bug risolti |
|
||||
| `docs/architecture.md` | Decisioni architetturali |
|
||||
| `export/progress.md` | Tracciamento progresso task |
|
||||
| `export/githistory.md` | Storico commit con contesto |
|
||||
| `CHANGELOG.md` | Changelog |
|
||||
| `CONTRIBUTING.md` | Guida contribuzione |
|
||||
|
||||
## Comandi Utili
|
||||
|
||||
```bash
|
||||
# Test
|
||||
uv run pytest # Tutti i test
|
||||
uv run pytest --cov # Con coverage
|
||||
uv run pytest tests/unit/ # Solo unit test
|
||||
|
||||
# Qualità
|
||||
uv run ruff check src/ # Linting
|
||||
uv run ruff format src/ # Formattazione
|
||||
uv run mypy src/ # Type checking
|
||||
|
||||
# Pre-commit
|
||||
uv run pre-commit run --all-files
|
||||
|
||||
# Server
|
||||
uv run fastapi dev src/notebooklm_agent/api/main.py
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
### Per Nuovi Prompt (prima di iniziare)
|
||||
- [ ] Ho determinato il prossimo numero (controlla `ls prompts/`)
|
||||
- [ ] Ho creato `prompts/{NUMERO}-{descrizione}.md` con template standard
|
||||
- [ ] Ho incluso comando per @sprint-lead
|
||||
- [ ] Ho definito obiettivo chiaro e criteri successo
|
||||
- [ ] Ho aggiornato `prompts/README.md` con il nuovo prompt
|
||||
|
||||
### Pre-Implementazione (coordinato da @sprint-lead)
|
||||
- [ ] @sprint-lead ha attivato il workflow corretto
|
||||
- [ ] Ho letto il prompt corrente in `prompts/{NUMERO}-*.md`
|
||||
- [ ] @spec-architect ha definito specifiche in `/export/`
|
||||
- [ ] Ho letto `prd.md` e `export/architecture.md`
|
||||
- [ ] Ho compreso lo scope e accettazione criteria
|
||||
|
||||
### Durante Implementazione
|
||||
- [ ] @api-designer ha definito contratti API (se applicabile)
|
||||
- [ ] @security-auditor ha approvato design (se auth/webhook)
|
||||
- [ ] Test scritto prima (RED)
|
||||
- [ ] Codice minimo (GREEN)
|
||||
- [ ] Refactoring (REFACTOR)
|
||||
- [ ] @qa-engineer ha scritto integration tests (parallelo)
|
||||
|
||||
### Post-Implementazione
|
||||
- [ ] Tutti i test passano (unit + integration)
|
||||
- [ ] Coverage ≥90%
|
||||
- [ ] @code-reviewer ha approvato (no [BLOCKING])
|
||||
- [ ] @docs-maintainer ha aggiornato documentazione
|
||||
- [ ] `CHANGELOG.md` aggiornato
|
||||
- [ ] @git-manager ha creato commit con conventional commits
|
||||
- [ ] @sprint-lead ha aggiornato `export/progress.md`
|
||||
|
||||
---
|
||||
|
||||
*Per dettagli su flusso di lavoro, vedere `.opencode/WORKFLOW.md`*
|
||||
29
.opencode/templates/architecture-adr.md
Normal file
29
.opencode/templates/architecture-adr.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
> Registro delle decisioni architetturali e dei pattern di design utilizzati.
|
||||
|
||||
## Formato
|
||||
|
||||
```markdown
|
||||
## [YYYY-MM-DD] - [Titolo Decisione]
|
||||
|
||||
### Contesto
|
||||
[Background e motivazione]
|
||||
|
||||
### Decisione
|
||||
[Cosa è stato deciso]
|
||||
|
||||
### Conseguenze
|
||||
- Positivo: [Benefici]
|
||||
- Negativo: [Trade-off]
|
||||
|
||||
### Alternative Considerate
|
||||
- [Alternativa 1]: [Perché scartata]
|
||||
- [Alternativa 2]: [Perché scartata]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
<!-- Aggiungere nuove decisioni qui in ordine cronologico crescente -->
|
||||
13
.opencode/templates/bug-ledger-entry.md
Normal file
13
.opencode/templates/bug-ledger-entry.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Bug Ledger Entry
|
||||
|
||||
> Template per documentare bug complessi risolti.
|
||||
|
||||
## YYYY-MM-DD: [Titolo Bug]
|
||||
|
||||
**Sintomo:** [Descrizione sintomo]
|
||||
|
||||
**Causa:** [Root cause]
|
||||
|
||||
**Soluzione:** [Fix applicato]
|
||||
|
||||
**Prevenzione:** [Come evitare in futuro]
|
||||
30
.opencode/templates/githistory-entry.md
Normal file
30
.opencode/templates/githistory-entry.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Git History Entry
|
||||
|
||||
> Template per documentare commit con contesto completo.
|
||||
|
||||
## YYYY-MM-DD HH:MM - type(scope): description
|
||||
|
||||
**Hash:** `commit-hash`
|
||||
**Autore:** @agent
|
||||
**Branch:** branch-name
|
||||
|
||||
### Contesto
|
||||
[Perché questo commit era necessario]
|
||||
|
||||
### Cosa cambia
|
||||
[Descrizione modifiche]
|
||||
|
||||
### Perché
|
||||
[Motivazione scelte]
|
||||
|
||||
### Impatto
|
||||
- [ ] Nuova feature
|
||||
- [ ] Bug fix
|
||||
- [ ] Refactoring
|
||||
- [ ] Breaking change
|
||||
|
||||
### File modificati
|
||||
- `file.py` - descrizione cambiamento
|
||||
|
||||
### Note
|
||||
[Riferimenti issue, considerazioni]
|
||||
98
.opencode/templates/progress-tracking.md
Normal file
98
.opencode/templates/progress-tracking.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Progress Tracking
|
||||
|
||||
> Tracciamento progresso sviluppo in tempo reale.
|
||||
|
||||
## 🎯 Sprint/Feature Corrente
|
||||
|
||||
**Feature:** `[Nome feature in sviluppo]`
|
||||
**Iniziata:** `YYYY-MM-DD`
|
||||
**Stato:** 🔴 Pianificazione / 🟡 In sviluppo / 🟢 Completata
|
||||
**Assegnato:** `@agent`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progresso Complessivo
|
||||
|
||||
| Area | Progresso | Stato |
|
||||
|------|-----------|-------|
|
||||
| API Core | 0/10 task | 🔴 Non iniziato |
|
||||
| Webhook System | 0/5 task | 🔴 Non iniziato |
|
||||
| AI Skill | 0/3 task | 🔴 Non iniziato |
|
||||
| Testing | 0/8 task | 🔴 Non iniziato |
|
||||
| Documentazione | 0/4 task | 🔴 Non iniziato |
|
||||
|
||||
**Completamento Totale:** 0%
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Attività in Corso
|
||||
|
||||
### Task Corrente: `[ID-XXX] - Titolo`
|
||||
|
||||
| Campo | Valore |
|
||||
|-------|--------|
|
||||
| **ID** | TASK-XXX |
|
||||
| **Descrizione** | [Breve descrizione] |
|
||||
| **Iniziata** | YYYY-MM-DD HH:MM |
|
||||
| **Assegnato** | @agent |
|
||||
| **Stato** | 🟡 In progress |
|
||||
| **Bloccata da** | Nessuna / TASK-YYY |
|
||||
| **Note** | [Eventuali ostacoli, decisioni] |
|
||||
|
||||
**Passi completati:**
|
||||
- [ ] Passo 1
|
||||
- [ ] Passo 2
|
||||
- [ ] Passo 3
|
||||
|
||||
---
|
||||
|
||||
## ✅ Task Completate (Oggi)
|
||||
|
||||
| ID | Task | Completata | Commit | Assegnato |
|
||||
|----|------|------------|--------|-----------|
|
||||
| | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 📅 Prossime Task
|
||||
|
||||
| Priority | ID | Task | Stima | Dipendenze |
|
||||
|----------|----|------|-------|------------|
|
||||
| P1 | | | | |
|
||||
| P2 | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Blocchi/Issue
|
||||
|
||||
| ID | Problema | Impatto | Soluzione Proposta | Stato |
|
||||
|----|----------|---------|-------------------|-------|
|
||||
| | | | | 🔴 Aperto |
|
||||
|
||||
---
|
||||
|
||||
## 📝 Decisioni Prese Oggi
|
||||
|
||||
| Data | Decisione | Motivazione | Impatto |
|
||||
|------|-----------|-------------|---------|
|
||||
| | | | |
|
||||
|
||||
---
|
||||
|
||||
## 📈 Metriche
|
||||
|
||||
### Sprint Corrente
|
||||
- **Task pianificate:** 0
|
||||
- **Task completate:** 0
|
||||
- **Task in progress:** 0
|
||||
- **Task bloccate:** 0
|
||||
|
||||
### Qualità
|
||||
- **Test Coverage:** 0%
|
||||
- **Test passanti:** 0/0
|
||||
- **Linting:** ✅ / ❌
|
||||
- **Type Check:** ✅ / ❌
|
||||
|
||||
---
|
||||
|
||||
*Ultimo aggiornamento: YYYY-MM-DD HH:MM*
|
||||
61
.pre-commit-config.yaml
Normal file
61
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
repos:
|
||||
# General file checks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude: ^(tests/fixtures/|docs/)
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^(tests/fixtures/|docs/)
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
- id: mixed-line-ending
|
||||
|
||||
# Ruff - Python linter and formatter
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
# Run the linter
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
files: ^(src/|tests/)
|
||||
# Run the formatter
|
||||
- id: ruff-format
|
||||
files: ^(src/|tests/)
|
||||
|
||||
# MyPy - Type checking
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
- pydantic>=2.0.0
|
||||
- pydantic-settings>=2.0.0
|
||||
- types-python-jose
|
||||
- types-passlib
|
||||
args: [--strict, --ignore-missing-imports]
|
||||
files: ^src/
|
||||
exclude: ^(tests/|scripts/)
|
||||
|
||||
# Commit message validation
|
||||
- repo: https://github.com/commitizen-tools/commitizen
|
||||
rev: v3.13.0
|
||||
hooks:
|
||||
- id: commitizen
|
||||
stages: [commit-msg]
|
||||
|
||||
# Security checks
|
||||
- repo: https://github.com/Yelp/detect-secrets
|
||||
rev: v1.4.0
|
||||
hooks:
|
||||
- id: detect-secrets
|
||||
args: ['--baseline', '.secrets.baseline']
|
||||
exclude: ^(tests/|docs/|\.env\.example)
|
||||
492
AGENTS.md
Normal file
492
AGENTS.md
Normal file
@@ -0,0 +1,492 @@
|
||||
# Repository Guidelines - NotebookLM Agent API
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-04-05
|
||||
**Project:** NotebookLM Agent API
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Structure & Module Organization
|
||||
|
||||
```
|
||||
├── src/
|
||||
│ └── notebooklm_agent/ # Main package
|
||||
│ ├── __init__.py
|
||||
│ ├── api/ # FastAPI application
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── main.py # FastAPI app entry
|
||||
│ │ ├── routes/ # API routes
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── notebooks.py
|
||||
│ │ │ ├── sources.py
|
||||
│ │ │ ├── chat.py
|
||||
│ │ │ ├── generate.py
|
||||
│ │ │ ├── artifacts.py
|
||||
│ │ │ └── webhooks.py
|
||||
│ │ ├── models/ # Pydantic models
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── requests.py
|
||||
│ │ │ └── responses.py
|
||||
│ │ └── dependencies.py # FastAPI dependencies
|
||||
│ ├── services/ # Business logic
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── notebook_service.py
|
||||
│ │ ├── source_service.py
|
||||
│ │ ├── chat_service.py
|
||||
│ │ ├── artifact_service.py
|
||||
│ │ └── webhook_service.py
|
||||
│ ├── core/ # Core utilities
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── config.py # Configuration management
|
||||
│ │ ├── exceptions.py # Custom exceptions
|
||||
│ │ ├── logging.py # Logging setup
|
||||
│ │ └── security.py # Security utilities
|
||||
│ ├── webhooks/ # Webhook system
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── dispatcher.py
|
||||
│ │ ├── validator.py
|
||||
│ │ └── retry.py
|
||||
│ └── skill/ # AI Skill interface
|
||||
│ ├── __init__.py
|
||||
│ └── handler.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── unit/ # Unit tests
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── test_services/
|
||||
│ │ └── test_core/
|
||||
│ ├── integration/ # Integration tests
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── test_api/
|
||||
│ └── e2e/ # End-to-end tests
|
||||
│ ├── __init__.py
|
||||
│ └── test_workflows/
|
||||
├── docs/ # Documentation
|
||||
│ ├── api/ # API documentation
|
||||
│ └── examples/ # Code examples
|
||||
├── scripts/ # Utility scripts
|
||||
├── .github/ # GitHub workflows
|
||||
├── pyproject.toml # Project configuration
|
||||
├── .pre-commit-config.yaml # Pre-commit hooks
|
||||
├── CHANGELOG.md # Changelog
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
├── SKILL.md # AI Agent skill definition
|
||||
└── prd.md # Product Requirements Document
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Development Environment Setup
|
||||
|
||||
### 2.1 Initial Setup
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone <repository-url>
|
||||
cd notebooklm-agent-api
|
||||
|
||||
# Create virtual environment with uv
|
||||
uv venv --python 3.10
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# or: .venv\Scripts\activate # Windows
|
||||
|
||||
# Install dependencies
|
||||
uv sync --extra dev --extra browser
|
||||
|
||||
# Install pre-commit hooks
|
||||
uv run pre-commit install
|
||||
|
||||
# Verify setup
|
||||
uv run pytest --version
|
||||
uv run python -c "import notebooklm_agent; print('OK')"
|
||||
```
|
||||
|
||||
### 2.2 Environment Variables
|
||||
Create `.env` file:
|
||||
```env
|
||||
# API Configuration
|
||||
NOTEBOOKLM_AGENT_API_KEY=your-api-key
|
||||
NOTEBOOKLM_AGENT_WEBHOOK_SECRET=your-webhook-secret
|
||||
NOTEBOOKLM_AGENT_PORT=8000
|
||||
NOTEBOOKLM_AGENT_HOST=0.0.0.0
|
||||
|
||||
# NotebookLM Configuration
|
||||
NOTEBOOKLM_HOME=~/.notebooklm
|
||||
NOTEBOOKLM_PROFILE=default
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FORMAT=json
|
||||
|
||||
# Development
|
||||
DEBUG=true
|
||||
TESTING=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Build, Test, and Development Commands
|
||||
|
||||
### 3.1 Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
uv run pytest
|
||||
|
||||
# Run with coverage
|
||||
uv run pytest --cov=src/notebooklm_agent --cov-report=term-missing
|
||||
|
||||
# Run specific test category
|
||||
uv run pytest tests/unit/
|
||||
uv run pytest tests/integration/
|
||||
uv run pytest tests/e2e/ -m e2e
|
||||
|
||||
# Run with verbose output
|
||||
uv run pytest -v
|
||||
|
||||
# Run failing tests only
|
||||
uv run pytest --lf
|
||||
|
||||
# Record VCR cassettes
|
||||
NOTEBOOKLM_VCR_RECORD=1 uv run pytest tests/integration/ -v
|
||||
```
|
||||
|
||||
### 3.2 Code Quality
|
||||
```bash
|
||||
# Run linter
|
||||
uv run ruff check src/ tests/
|
||||
|
||||
# Fix auto-fixable issues
|
||||
uv run ruff check --fix src/ tests/
|
||||
|
||||
# Format code
|
||||
uv run ruff format src/ tests/
|
||||
|
||||
# Type checking
|
||||
uv run mypy src/notebooklm_agent
|
||||
|
||||
# Run all quality checks
|
||||
uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run mypy src/notebooklm_agent
|
||||
```
|
||||
|
||||
### 3.3 Pre-commit
|
||||
```bash
|
||||
# Run all hooks on all files
|
||||
uv run pre-commit run --all-files
|
||||
|
||||
# Run specific hook
|
||||
uv run pre-commit run ruff --all-files
|
||||
uv run pre-commit run mypy --all-files
|
||||
```
|
||||
|
||||
### 3.4 Running the Application
|
||||
```bash
|
||||
# Development server with auto-reload
|
||||
uv run python -m notebooklm_agent.api.main --reload
|
||||
|
||||
# Production server
|
||||
uv run gunicorn notebooklm_agent.api.main:app -w 4 -k uvicorn.workers.UvicornWorker
|
||||
|
||||
# Using FastAPI CLI
|
||||
uv run fastapi dev src/notebooklm_agent/api/main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Coding Style & Naming Conventions
|
||||
|
||||
### 4.1 General Guidelines
|
||||
- **Python Version:** 3.10+
|
||||
- **Indentation:** 4 spaces
|
||||
- **Quotes:** Double quotes for strings
|
||||
- **Line Length:** 100 characters
|
||||
- **Type Hints:** Required for all function signatures
|
||||
|
||||
### 4.2 Naming Conventions
|
||||
| Element | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| Modules | snake_case | `notebook_service.py` |
|
||||
| Classes | PascalCase | `NotebookService` |
|
||||
| Functions | snake_case | `create_notebook()` |
|
||||
| Variables | snake_case | `notebook_id` |
|
||||
| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| Private | _prefix | `_internal_helper()` |
|
||||
|
||||
### 4.3 Import Order (enforced by ruff)
|
||||
```python
|
||||
# 1. Standard library
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
# 2. Third-party
|
||||
import httpx
|
||||
from fastapi import FastAPI
|
||||
|
||||
# 3. First-party
|
||||
from notebooklm_agent.core.config import Settings
|
||||
from notebooklm_agent.services.notebook_service import NotebookService
|
||||
```
|
||||
|
||||
### 4.4 Documentation
|
||||
- All public modules, classes, and functions must avere docstrings
|
||||
- Use Google-style docstrings
|
||||
- Include type information in docstrings when complesso
|
||||
|
||||
Example:
|
||||
```python
|
||||
async def create_notebook(
|
||||
title: str,
|
||||
description: Optional[str] = None
|
||||
) -> NotebookResponse:
|
||||
"""Create a new NotebookLM notebook.
|
||||
|
||||
Args:
|
||||
title: The notebook title (max 100 chars).
|
||||
description: Optional notebook description.
|
||||
|
||||
Returns:
|
||||
NotebookResponse with notebook details.
|
||||
|
||||
Raises:
|
||||
ValidationError: If title is invalid.
|
||||
NotebookLMError: If NotebookLM API fails.
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing Guidelines
|
||||
|
||||
### 5.1 Test Organization
|
||||
```
|
||||
tests/
|
||||
├── unit/ # Pure logic, no external calls
|
||||
├── integration/ # With mocked external APIs
|
||||
└── e2e/ # Full workflows, real APIs
|
||||
```
|
||||
|
||||
### 5.2 Test Naming
|
||||
- Test files: `test_<module_name>.py`
|
||||
- Test functions: `test_<behavior>_<condition>_<expected>`
|
||||
|
||||
Example:
|
||||
```python
|
||||
def test_create_notebook_valid_title_returns_created():
|
||||
...
|
||||
|
||||
def test_create_notebook_empty_title_raises_validation_error():
|
||||
...
|
||||
```
|
||||
|
||||
### 5.3 Test Structure (AAA Pattern)
|
||||
```python
|
||||
async def test_create_notebook_success():
|
||||
# Arrange
|
||||
title = "Test Notebook"
|
||||
service = NotebookService()
|
||||
|
||||
# Act
|
||||
result = await service.create_notebook(title)
|
||||
|
||||
# Assert
|
||||
assert result.title == title
|
||||
assert result.id is not None
|
||||
```
|
||||
|
||||
### 5.4 Markers
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_pure_function():
|
||||
...
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_with_http_client():
|
||||
...
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_full_workflow():
|
||||
...
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_function():
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Commit, PR, and Workflow
|
||||
|
||||
### 6.1 Conventional Commits
|
||||
Format: `<type>(<scope>): <description>`
|
||||
|
||||
**Types:**
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation only
|
||||
- `style`: Code style (formatting, semicolons, etc.)
|
||||
- `refactor`: Code refactoring
|
||||
- `test`: Adding or correcting tests
|
||||
- `chore`: Build process, dependencies
|
||||
- `ci`: CI/CD changes
|
||||
|
||||
**Scopes:**
|
||||
- `api`: REST API endpoints
|
||||
- `webhook`: Webhook system
|
||||
- `skill`: AI skill interface
|
||||
- `notebook`: Notebook operations
|
||||
- `source`: Source management
|
||||
- `artifact`: Artifact generation
|
||||
- `auth`: Authentication
|
||||
- `core`: Core utilities
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
feat(api): add webhook registration endpoint
|
||||
fix(webhook): retry logic exponential backoff
|
||||
refactor(notebook): extract validation logic
|
||||
test(source): add unit tests for URL validation
|
||||
docs(api): update OpenAPI schema
|
||||
chore(deps): upgrade notebooklm-py to 0.3.4
|
||||
```
|
||||
|
||||
### 6.2 Commit Message Format
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
|
||||
[optional body: explain what and why, not how]
|
||||
|
||||
[optional footer: BREAKING CHANGE, Fixes #123, etc.]
|
||||
```
|
||||
|
||||
### 6.3 Pull Request Process
|
||||
1. Create feature branch from `main`
|
||||
2. Make commits following conventional commits
|
||||
3. Ensure all tests pass: `uv run pytest`
|
||||
4. Ensure code quality: `uv run pre-commit run --all-files`
|
||||
5. Update CHANGELOG.md if applicable
|
||||
6. Create PR with template
|
||||
7. Require 1+ review approval
|
||||
8. Squash and merge
|
||||
|
||||
### 6.4 Branch Naming
|
||||
- Feature: `feat/description`
|
||||
- Bugfix: `fix/description`
|
||||
- Hotfix: `hotfix/description`
|
||||
- Release: `release/v1.0.0`
|
||||
|
||||
---
|
||||
|
||||
## 7. API Design Guidelines
|
||||
|
||||
### 7.1 RESTful Endpoints
|
||||
- Use nouns, not verbs: `/notebooks` not `/createNotebook`
|
||||
- Use plural nouns: `/notebooks` not `/notebook`
|
||||
- Use HTTP methods appropriately:
|
||||
- GET: Read
|
||||
- POST: Create
|
||||
- PUT/PATCH: Update
|
||||
- DELETE: Remove
|
||||
|
||||
### 7.2 Response Format
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... },
|
||||
"meta": {
|
||||
"timestamp": "2026-04-05T10:30:00Z",
|
||||
"request_id": "uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Error Format
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid notebook title",
|
||||
"details": [...]
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-05T10:30:00Z",
|
||||
"request_id": "uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Webhook Guidelines
|
||||
|
||||
### 8.1 Event Naming
|
||||
- Format: `<resource>.<action>`
|
||||
- Examples: `notebook.created`, `source.ready`, `artifact.completed`
|
||||
|
||||
### 8.2 Webhook Payload
|
||||
```json
|
||||
{
|
||||
"event": "source.ready",
|
||||
"timestamp": "2026-04-05T10:30:00Z",
|
||||
"data": {
|
||||
"notebook_id": "uuid",
|
||||
"source_id": "uuid",
|
||||
"source_title": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Security
|
||||
- Always sign with HMAC-SHA256
|
||||
- Include `X-Webhook-Signature` header
|
||||
- Verify signature before processing
|
||||
|
||||
---
|
||||
|
||||
## 9. Agent Notes
|
||||
|
||||
### 9.1 Parallel Agent Safety
|
||||
When running multiple agents:
|
||||
1. Use explicit notebook IDs: `-n <notebook_id>`
|
||||
2. Isolate with profiles: `NOTEBOOKLM_PROFILE=agent-$ID`
|
||||
3. Or isolate with home: `NOTEBOOKLM_HOME=/tmp/agent-$ID`
|
||||
|
||||
### 9.2 Common Commands
|
||||
```bash
|
||||
# Check status
|
||||
notebooklm status
|
||||
|
||||
# Verify auth
|
||||
notebooklm auth check
|
||||
|
||||
# Health check
|
||||
uv run python -c "from notebooklm_agent.api.main import health_check; print(health_check())"
|
||||
```
|
||||
|
||||
### 9.3 Troubleshooting
|
||||
```bash
|
||||
# Reset auth
|
||||
notebooklm login
|
||||
|
||||
# Clear cache
|
||||
rm -rf ~/.notebooklm/cache
|
||||
|
||||
# Verbose logging
|
||||
LOG_LEVEL=DEBUG uv run python -m notebooklm_agent.api.main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Resources
|
||||
|
||||
- **PRD:** `prd.md`
|
||||
- **Skill Definition:** `SKILL.md`
|
||||
- **Contributing:** `CONTRIBUTING.md`
|
||||
- **Changelog:** `CHANGELOG.md`
|
||||
- **API Docs:** `/docs` (when server running)
|
||||
- **OpenAPI Schema:** `/openapi.json`
|
||||
|
||||
---
|
||||
|
||||
**Maintained by:** NotebookLM Agent Team
|
||||
**Last Updated:** 2026-04-05
|
||||
111
CHANGELOG.md
Normal file
111
CHANGELOG.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Changelog
|
||||
|
||||
Tutti i cambiamenti significativi a questo progetto saranno documentati in questo file.
|
||||
|
||||
Il formato è basato su [Common Changelog](https://common-changelog.org/) e aderisce a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-04-05
|
||||
|
||||
### Added
|
||||
|
||||
- Initial project setup with Spec-Driven Development methodology
|
||||
- Product Requirements Document (PRD) defining all features
|
||||
- AGENTS.md with OpenCode development guidelines
|
||||
- SKILL.md for AI agent integration
|
||||
- CONTRIBUTING.md with conventional commits guidelines
|
||||
- Project configuration:
|
||||
- pyproject.toml with dependencies and tooling config
|
||||
- .pre-commit-config.yaml for code quality hooks
|
||||
- pytest configuration for TDD
|
||||
- ruff and mypy for linting and type checking
|
||||
- Documentation structure for API and examples
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
notebooklm-agent/
|
||||
├── src/notebooklm_agent/ # Main package (to be implemented)
|
||||
├── tests/ # Test suite (to be implemented)
|
||||
├── docs/ # Documentation
|
||||
├── scripts/ # Utility scripts
|
||||
├── .github/ # GitHub workflows (to be implemented)
|
||||
├── prd.md # Product Requirements Document
|
||||
├── AGENTS.md # OpenCode guidelines
|
||||
├── SKILL.md # AI agent skill definition
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
├── CHANGELOG.md # This file
|
||||
└── pyproject.toml # Project configuration
|
||||
```
|
||||
|
||||
### Development Methodology
|
||||
|
||||
- **Spec-Driven Development (SDD):** Requisiti definiti prima dell'implementazione
|
||||
- **Test Driven Development (TDD):** Red-Green-Refactor cycle
|
||||
- **Conventional Commits:** Standardizzazione messaggi commit
|
||||
- **Common Changelog:** Gestione versioni e cambiamenti
|
||||
|
||||
### Changed
|
||||
|
||||
- **Codebase reorganization:**
|
||||
- Removed `export/` directory (workspace temporaneo per spec-driven)
|
||||
- Removed `scripts/` directory (vuota)
|
||||
- Moved templates from `docs/` to `.opencode/templates/`
|
||||
- Updated `.gitignore` to exclude workspace directories
|
||||
|
||||
### Added
|
||||
|
||||
- **Core API implementation:**
|
||||
- `api/main.py` - FastAPI application entry point
|
||||
- `api/dependencies.py` - Dependency injection (API key auth, settings)
|
||||
- `api/routes/health.py` - Health check endpoints (/health, /ready, /live)
|
||||
- `core/logging.py` - Structured logging configuration with structlog
|
||||
|
||||
- **Templates for OpenCode agents** (`.opencode/templates/`):
|
||||
- `architecture-adr.md` - Architecture Decision Records template
|
||||
- `bug-ledger-entry.md` - Bug documentation template
|
||||
- `progress-tracking.md` - Task progress tracking template
|
||||
- `githistory-entry.md` - Git commit context template
|
||||
|
||||
- **Test suite expansion:**
|
||||
- `tests/unit/test_api/test_main.py` - API main module tests
|
||||
- `tests/unit/test_api/test_health.py` - Health endpoints tests
|
||||
- `tests/unit/test_core/test_logging.py` - Logging configuration tests
|
||||
|
||||
### Project Structure (Updated)
|
||||
|
||||
```
|
||||
notebooklm-agent/
|
||||
├── .opencode/
|
||||
│ ├── agents/ # Agent configurations
|
||||
│ ├── skills/ # Shared skills
|
||||
│ ├── templates/ # Templates for spec-driven workflow
|
||||
│ ├── WORKFLOW.md
|
||||
│ └── opencode.json
|
||||
├── docs/ # User documentation (cleaned)
|
||||
├── src/notebooklm_agent/
|
||||
│ ├── api/
|
||||
│ │ ├── main.py # FastAPI entry point
|
||||
│ │ ├── dependencies.py # DI container
|
||||
│ │ └── routes/health.py # Health endpoints
|
||||
│ └── core/logging.py # Logging setup
|
||||
└── tests/
|
||||
└── unit/test_api/ # API tests
|
||||
```
|
||||
|
||||
### Next Steps
|
||||
|
||||
- [x] ~~Implement core API structure~~ (Base structure done)
|
||||
- [ ] Add notebook management endpoints
|
||||
- [ ] Add source management endpoints
|
||||
- [ ] Add chat functionality
|
||||
- [ ] Add content generation endpoints
|
||||
- [ ] Add artifact management
|
||||
- [ ] Implement webhook system
|
||||
- [x] ~~Add comprehensive test suite~~ (Base tests added)
|
||||
- [x] ~~Set up CI/CD pipeline~~ (GitHub Actions configured)
|
||||
|
||||
---
|
||||
|
||||
**Note:** Questa è una versione iniziale di setup del progetto. Le funzionalità API saranno implementate nelle prossime release.
|
||||
403
CONTRIBUTING.md
Normal file
403
CONTRIBUTING.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# Contributing to NotebookLM Agent API
|
||||
|
||||
Grazie per il tuo interesse nel contribuire al progetto! Questo documento fornisce le linee guida per contribuire efficacemente.
|
||||
|
||||
## Indice
|
||||
|
||||
- [Codice di Condotta](#codice-di-condotta)
|
||||
- [Come Contribuire](#come-contribuire)
|
||||
- [Setup Ambiente di Sviluppo](#setup-ambiente-di-sviluppo)
|
||||
- [Workflow di Sviluppo](#workflow-di-sviluppo)
|
||||
- [Conventional Commits](#conventional-commits)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Testing](#testing)
|
||||
- [Changelog](#changelog)
|
||||
|
||||
---
|
||||
|
||||
## Codice di Condotta
|
||||
|
||||
Questo progetto aderisce al [Codice di Condotta](./CODE_OF_CONDUCT.md). Partecipando, ti impegni a mantenere un ambiente rispettoso e collaborativo.
|
||||
|
||||
---
|
||||
|
||||
## Come Contribuire
|
||||
|
||||
### Segnalare Bug
|
||||
|
||||
1. Verifica che il bug non sia già stato segnalato nelle [Issues](../../issues)
|
||||
2. Crea una nuova issue con:
|
||||
- Titolo descrittivo
|
||||
- Descrizione chiara del problema
|
||||
- Passi per riprodurre
|
||||
- Comportamento atteso vs attuale
|
||||
- Ambiente (OS, Python version, etc.)
|
||||
- Log o screenshot rilevanti
|
||||
|
||||
### Suggerire Feature
|
||||
|
||||
1. Crea una issue con label `enhancement`
|
||||
2. Descrivi chiaramente la feature e il valore aggiunto
|
||||
3. Discuti l'implementazione con i maintainer
|
||||
|
||||
### Contribuire al Codice
|
||||
|
||||
1. Fork del repository
|
||||
2. Crea un branch per la tua feature/fix
|
||||
3. Segui le [Conventional Commits](#conventional-commits)
|
||||
4. Assicurati che i test passino
|
||||
5. Aggiorna la documentazione
|
||||
6. Crea una Pull Request
|
||||
|
||||
---
|
||||
|
||||
## Setup Ambiente di Sviluppo
|
||||
|
||||
### Prerequisiti
|
||||
|
||||
- Python 3.10+
|
||||
- [uv](https://github.com/astral-sh/uv) per dependency management
|
||||
- Git
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clona il repository
|
||||
git clone https://github.com/example/notebooklm-agent.git
|
||||
cd notebooklm-agent
|
||||
|
||||
# Crea virtual environment
|
||||
uv venv --python 3.10
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# .venv\Scripts\activate # Windows
|
||||
|
||||
# Installa dipendenze
|
||||
uv sync --extra dev --extra browser
|
||||
|
||||
# Installa pre-commit hooks
|
||||
uv run pre-commit install
|
||||
|
||||
# Verifica setup
|
||||
uv run pytest --version
|
||||
uv run ruff --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow di Sviluppo
|
||||
|
||||
### 1. Creare un Branch
|
||||
|
||||
```bash
|
||||
# Aggiorna main
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Crea branch
|
||||
git checkout -b feat/nome-feature # Per feature
|
||||
git checkout -b fix/nome-bug # Per bugfix
|
||||
git checkout -b docs/nome-documentazione # Per docs
|
||||
```
|
||||
|
||||
### 2. Sviluppare con TDD
|
||||
|
||||
Segui il ciclo Red-Green-Refactor:
|
||||
|
||||
```bash
|
||||
# 1. Scrivi test (fallirà)
|
||||
# Crea tests/unit/test_nuova_feature.py
|
||||
|
||||
# 2. Esegui test (deve fallire)
|
||||
uv run pytest tests/unit/test_nuova_feature.py -v
|
||||
|
||||
# 3. Implementa codice minimo
|
||||
# Modifica src/notebooklm_agent/...
|
||||
|
||||
# 4. Esegui test (deve passare)
|
||||
uv run pytest tests/unit/test_nuova_feature.py -v
|
||||
|
||||
# 5. Refactor mantenendo test verdi
|
||||
# Migliora codice
|
||||
|
||||
# 6. Verifica tutti i test
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
### 3. Qualità del Codice
|
||||
|
||||
```bash
|
||||
# Linting
|
||||
uv run ruff check src/ tests/
|
||||
uv run ruff check --fix src/ tests/
|
||||
|
||||
# Formattazione
|
||||
uv run ruff format src/ tests/
|
||||
|
||||
# Type checking
|
||||
uv run mypy src/notebooklm_agent
|
||||
|
||||
# Pre-commit (esegue tutti i check)
|
||||
uv run pre-commit run --all-files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conventional Commits
|
||||
|
||||
Tutti i commit devono seguire la specifica [Conventional Commits](https://www.conventionalcommits.org/).
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
| Type | Descrizione |
|
||||
|------|-------------|
|
||||
| `feat` | Nuova feature |
|
||||
| `fix` | Bug fix |
|
||||
| `docs` | Cambiamenti alla documentazione |
|
||||
| `style` | Cambiamenti di stile (formattazione, punto e virgola, etc.) |
|
||||
| `refactor` | Refactoring codice |
|
||||
| `perf` | Miglioramenti performance |
|
||||
| `test` | Aggiunta o correzione test |
|
||||
| `chore` | Task di build, dipendenze, etc. |
|
||||
| `ci` | Cambiamenti CI/CD |
|
||||
|
||||
### Scopes
|
||||
|
||||
| Scope | Descrizione |
|
||||
|-------|-------------|
|
||||
| `api` | REST API endpoints |
|
||||
| `webhook` | Webhook system |
|
||||
| `skill` | AI skill interface |
|
||||
| `notebook` | Notebook operations |
|
||||
| `source` | Source management |
|
||||
| `artifact` | Artifact generation |
|
||||
| `auth` | Authentication |
|
||||
| `core` | Core utilities |
|
||||
| `deps` | Dependencies |
|
||||
| `config` | Configuration |
|
||||
|
||||
### Esempi
|
||||
|
||||
```bash
|
||||
# Feature
|
||||
git commit -m "feat(api): add webhook registration endpoint"
|
||||
git commit -m "feat(notebook): implement bulk create operation"
|
||||
|
||||
# Fix
|
||||
git commit -m "fix(webhook): retry logic exponential backoff"
|
||||
git commit -m "fix(auth): handle expired tokens correctly"
|
||||
|
||||
# Docs
|
||||
git commit -m "docs(api): update OpenAPI schema"
|
||||
git commit -m "docs(readme): add installation instructions"
|
||||
|
||||
# Test
|
||||
git commit -m "test(source): add unit tests for URL validation"
|
||||
git commit -m "test(webhook): add integration tests for retry logic"
|
||||
|
||||
# Refactor
|
||||
git commit -m "refactor(notebook): extract validation logic"
|
||||
git commit -m "refactor(core): improve error handling"
|
||||
|
||||
# Chore
|
||||
git commit -m "chore(deps): upgrade notebooklm-py to 0.3.4"
|
||||
git commit -m "chore(ci): add GitHub Actions workflow"
|
||||
```
|
||||
|
||||
### Commit con Body
|
||||
|
||||
Per commit complessi, usa il body:
|
||||
|
||||
```bash
|
||||
git commit -m "feat(api): add batch source import endpoint
|
||||
|
||||
- Support for multiple URLs in single request
|
||||
- Async processing with webhook notification
|
||||
- Rate limiting per user
|
||||
|
||||
Fixes #123"
|
||||
```
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
```bash
|
||||
git commit -m "feat(api)!: change response format for notebooks endpoint
|
||||
|
||||
BREAKING CHANGE: response now wrapped in 'data' key
|
||||
Migration guide: docs/migrations/v1-to-v2.md"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### 1. Prima di Creare PR
|
||||
|
||||
```bash
|
||||
# Aggiorna dal main
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout tuo-branch
|
||||
git rebase main
|
||||
|
||||
# Esegui test completi
|
||||
uv run pytest
|
||||
|
||||
# Verifica qualità
|
||||
uv run pre-commit run --all-files
|
||||
|
||||
# Aggiorna CHANGELOG.md se necessario
|
||||
```
|
||||
|
||||
### 2. Creare PR
|
||||
|
||||
1. Vai su GitHub e crea PR verso `main`
|
||||
2. Usa il template PR fornito
|
||||
3. Assicurati che:
|
||||
- Titolo segue conventional commits
|
||||
- Descrizione spiega cosa e perché
|
||||
- Test inclusi per nuove feature
|
||||
- Documentazione aggiornata
|
||||
- CHANGELOG.md aggiornato
|
||||
|
||||
### 3. Review Process
|
||||
|
||||
1. Richiedi review da almeno 1 maintainer
|
||||
2. Rispondi ai commenti
|
||||
3. Fai push delle correzioni
|
||||
4. Attendi approvazione
|
||||
|
||||
### 4. Merge
|
||||
|
||||
- Usa "Squash and Merge"
|
||||
- Il titolo del merge commit deve seguire conventional commits
|
||||
- Elimina il branch dopo il merge
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Eseguire Test
|
||||
|
||||
```bash
|
||||
# Tutti i test
|
||||
uv run pytest
|
||||
|
||||
# Con coverage
|
||||
uv run pytest --cov=src/notebooklm_agent --cov-report=term-missing
|
||||
|
||||
# Solo unit test
|
||||
uv run pytest tests/unit/ -m unit
|
||||
|
||||
# Solo integration test
|
||||
uv run pytest tests/integration/ -m integration
|
||||
|
||||
# Solo E2E (richiede auth)
|
||||
uv run pytest tests/e2e/ -m e2e
|
||||
|
||||
# Test specifico
|
||||
uv run pytest tests/unit/test_notebook_service.py::test_create_notebook -v
|
||||
|
||||
# Test falliti solo
|
||||
uv run pytest --lf
|
||||
|
||||
# Parallel (con pytest-xdist)
|
||||
uv run pytest -n auto
|
||||
```
|
||||
|
||||
### Scrivere Test
|
||||
|
||||
```python
|
||||
# tests/unit/test_example.py
|
||||
import pytest
|
||||
from notebooklm_agent.services.notebook_service import NotebookService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookService:
|
||||
"""Test suite for NotebookService."""
|
||||
|
||||
async def test_create_notebook_valid_title_returns_created(self):
|
||||
"""Should create notebook with valid title."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
title = "Test Notebook"
|
||||
|
||||
# Act
|
||||
result = await service.create_notebook(title)
|
||||
|
||||
# Assert
|
||||
assert result.title == title
|
||||
assert result.id is not None
|
||||
|
||||
async def test_create_notebook_empty_title_raises_validation_error(self):
|
||||
"""Should raise error for empty title."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError):
|
||||
await service.create_notebook("")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
Il progetto segue le specifiche [Common Changelog](https://common-changelog.org/).
|
||||
|
||||
### Aggiornare CHANGELOG.md
|
||||
|
||||
Quando crei una PR che cambia il comportamento visibile:
|
||||
|
||||
1. Aggiungi entry sotto la sezione "Unreleased"
|
||||
2. Usa i gruppi:
|
||||
- `### Added` - Nuove feature
|
||||
- `### Changed` - Cambiamenti a feature esistenti
|
||||
- `### Deprecated` - Feature deprecate
|
||||
- `### Removed` - Feature rimosse
|
||||
- `### Fixed` - Bug fix
|
||||
- `### Security` - Fix di sicurezza
|
||||
|
||||
### Esempio
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Add webhook retry mechanism with exponential backoff. ([`abc123`])
|
||||
- Support for batch source import via API. ([`def456`])
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix race condition in artifact status polling. ([`ghi789`])
|
||||
|
||||
## [0.1.0] - 2026-04-05
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release with core API functionality.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risorse
|
||||
|
||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
- [Common Changelog](https://common-changelog.org/)
|
||||
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
|
||||
- [Pytest Documentation](https://docs.pytest.org/)
|
||||
|
||||
---
|
||||
|
||||
Grazie per il tuo contributo! 🎉
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 NotebookLM Agent Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
176
README.md
Normal file
176
README.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# NotebookLM Agent API
|
||||
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://fastapi.tiangolo.com/)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
[](https://docs.pytest.org/)
|
||||
|
||||
> **API e Webhook Interface per Google NotebookLM Automation**
|
||||
|
||||
Questo progetto fornisce un'interfaccia API REST completa per Google NotebookLM, con supporto webhook per integrazione con altri agenti AI. Sviluppato seguendo le metodologie **Spec-Driven Development (SDD)** e **Test Driven Development (TDD)**.
|
||||
|
||||
## Caratteristiche
|
||||
|
||||
- **API REST Completa**: Gestione notebook, fonti, chat, generazione contenuti
|
||||
- **Webhook System**: Notifiche event-driven per integrazione multi-agent
|
||||
- **AI Skill**: Interfaccia nativa per OpenCode e altri agenti AI
|
||||
- **Qualità del Codice**: ≥90% test coverage, type hints, linting
|
||||
- **Metodologie**: SDD + TDD + Conventional Commits
|
||||
|
||||
## Requisiti
|
||||
|
||||
- Python 3.10+
|
||||
- [uv](https://github.com/astral-sh/uv) per dependency management
|
||||
- Account Google con accesso a NotebookLM
|
||||
|
||||
## Installazione
|
||||
|
||||
```bash
|
||||
# Clona il repository
|
||||
git clone https://github.com/example/notebooklm-agent.git
|
||||
cd notebooklm-agent
|
||||
|
||||
# Crea virtual environment
|
||||
uv venv --python 3.10
|
||||
source .venv/bin/activate
|
||||
|
||||
# Installa dipendenze
|
||||
uv sync --extra dev --extra browser
|
||||
|
||||
# Installa pre-commit hooks
|
||||
uv run pre-commit install
|
||||
|
||||
# Configura ambiente
|
||||
cp .env.example .env
|
||||
# Modifica .env con le tue configurazioni
|
||||
```
|
||||
|
||||
## Configurazione
|
||||
|
||||
Crea un file `.env`:
|
||||
|
||||
```env
|
||||
# API Configuration
|
||||
NOTEBOOKLM_AGENT_API_KEY=your-api-key
|
||||
NOTEBOOKLM_AGENT_WEBHOOK_SECRET=your-webhook-secret
|
||||
NOTEBOOKLM_AGENT_PORT=8000
|
||||
NOTEBOOKLM_AGENT_HOST=0.0.0.0
|
||||
|
||||
# NotebookLM Configuration
|
||||
NOTEBOOKLM_HOME=~/.notebooklm
|
||||
NOTEBOOKLM_PROFILE=default
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FORMAT=json
|
||||
```
|
||||
|
||||
## Autenticazione NotebookLM
|
||||
|
||||
```bash
|
||||
# Login a NotebookLM (prima volta)
|
||||
notebooklm login
|
||||
|
||||
# Verifica autenticazione
|
||||
notebooklm auth check
|
||||
```
|
||||
|
||||
## Avvio
|
||||
|
||||
```bash
|
||||
# Development server
|
||||
uv run fastapi dev src/notebooklm_agent/api/main.py
|
||||
|
||||
# Production server
|
||||
uv run gunicorn notebooklm_agent.api.main:app -w 4 -k uvicorn.workers.UvicornWorker
|
||||
```
|
||||
|
||||
L'API sarà disponibile su `http://localhost:8000`
|
||||
|
||||
- API docs: `http://localhost:8000/docs`
|
||||
- OpenAPI schema: `http://localhost:8000/openapi.json`
|
||||
|
||||
## Uso Rapido
|
||||
|
||||
```bash
|
||||
# Creare notebook
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Ricerca AI"}'
|
||||
|
||||
# Aggiungere fonte
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/sources \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "url", "url": "https://example.com"}'
|
||||
|
||||
# Generare podcast
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/audio \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"instructions": "Make it engaging"}'
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Esegui tutti i test
|
||||
uv run pytest
|
||||
|
||||
# Con coverage
|
||||
uv run pytest --cov=src/notebooklm_agent --cov-report=term-missing
|
||||
|
||||
# Solo unit test
|
||||
uv run pytest tests/unit/ -m unit
|
||||
|
||||
# Solo integration test
|
||||
uv run pytest tests/integration/ -m integration
|
||||
```
|
||||
|
||||
## Sviluppo
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Spec-Driven**: Definisci requisiti in `prd.md`
|
||||
2. **TDD**: Scrivi test → Implementa → Refactor
|
||||
3. **Conventional Commits**: Segui lo standard per i commit
|
||||
4. **Pre-commit**: I controlli automatici garantiscono qualità
|
||||
|
||||
### Struttura Progetto
|
||||
|
||||
```
|
||||
notebooklm-agent/
|
||||
├── src/notebooklm_agent/ # Codice sorgente
|
||||
├── tests/ # Test suite
|
||||
├── docs/ # Documentazione
|
||||
├── prd.md # Product Requirements
|
||||
├── AGENTS.md # Linee guida OpenCode
|
||||
├── SKILL.md # Definizione skill AI
|
||||
└── CONTRIBUTING.md # Guida contribuzione
|
||||
```
|
||||
|
||||
## Documentazione
|
||||
|
||||
- [PRD](./prd.md) - Product Requirements Document
|
||||
- [AGENTS.md](./AGENTS.md) - Linee guida per OpenCode
|
||||
- [SKILL.md](./SKILL.md) - Skill per agenti AI
|
||||
- [CONTRIBUTING.md](./CONTRIBUTING.md) - Come contribuire
|
||||
|
||||
## Stato del Progetto
|
||||
|
||||
⚠️ **Versione Iniziale**: Questo progetto è in fase di setup iniziale. Le funzionalità API sono pianificate per le prossime release.
|
||||
|
||||
Vedi [CHANGELOG.md](./CHANGELOG.md) per lo stato attuale e la roadmap.
|
||||
|
||||
## Licenza
|
||||
|
||||
MIT License - vedi [LICENSE](./LICENSE) per dettagli.
|
||||
|
||||
## Contributing
|
||||
|
||||
Vedi [CONTRIBUTING.md](./CONTRIBUTING.md) per le linee guida su come contribuire al progetto.
|
||||
|
||||
---
|
||||
|
||||
**Nota**: Questo è un progetto non ufficiale e non è affiliato con Google. Usa le API interne di NotebookLM che possono cambiare senza preavviso.
|
||||
560
SKILL.md
Normal file
560
SKILL.md
Normal file
@@ -0,0 +1,560 @@
|
||||
---
|
||||
name: notebooklm-agent
|
||||
description: API and webhook interface for Google NotebookLM automation. Full programmatic access including audio generation, video creation, quizzes, flashcards, and all NotebookLM Studio features. Integrates with other AI agents via REST API and webhooks.
|
||||
triggers:
|
||||
- /notebooklm-agent
|
||||
- /notebooklm
|
||||
- "create.*podcast"
|
||||
- "generate.*audio"
|
||||
- "create.*video"
|
||||
- "generate.*quiz"
|
||||
- "create.*flashcard"
|
||||
- "research.*notebook"
|
||||
- "webhook.*notebook"
|
||||
---
|
||||
|
||||
# NotebookLM Agent API Skill
|
||||
|
||||
Interfaccia agentica per Google NotebookLM tramite API REST e webhook. Automatizza la creazione di notebook, gestione fonti, generazione contenuti multi-formato (audio, video, slide, quiz, flashcard) e integrazione con altri agenti AI.
|
||||
|
||||
---
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Operazioni Supportate
|
||||
|
||||
| Categoria | Operazioni |
|
||||
|-----------|------------|
|
||||
| **Notebook** | Creare, listare, ottenere, aggiornare, eliminare |
|
||||
| **Fonti** | Aggiungere URL, PDF, YouTube, Drive, ricerca web |
|
||||
| **Chat** | Interrogare fonti, storico conversazioni |
|
||||
| **Generazione** | Audio (podcast), Video, Slide, Infografiche, Quiz, Flashcard, Report, Mappe mentali, Tabelle |
|
||||
| **Artifacts** | Monitorare stato, scaricare in vari formati |
|
||||
| **Webhook** | Registrare endpoint, ricevere notifiche eventi |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
### 1. Autenticazione NotebookLM
|
||||
|
||||
Prima di qualsiasi operazione, autenticarsi con NotebookLM:
|
||||
|
||||
```bash
|
||||
# Login browser (prima volta)
|
||||
notebooklm login
|
||||
|
||||
# Verifica autenticazione
|
||||
notebooklm auth check
|
||||
notebooklm list
|
||||
```
|
||||
|
||||
### 2. Avvio API Server
|
||||
|
||||
```bash
|
||||
# Avvia server API
|
||||
uv run fastapi dev src/notebooklm_agent/api/main.py
|
||||
|
||||
# Verifica salute
|
||||
http://localhost:8000/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Autonomy Rules
|
||||
|
||||
### ✅ Esegui Automaticamente (senza conferma)
|
||||
|
||||
| Operazione | Motivo |
|
||||
|------------|--------|
|
||||
| `GET /api/v1/notebooks` | Read-only |
|
||||
| `GET /api/v1/notebooks/{id}` | Read-only |
|
||||
| `GET /api/v1/notebooks/{id}/sources` | Read-only |
|
||||
| `GET /api/v1/notebooks/{id}/chat/history` | Read-only |
|
||||
| `GET /api/v1/notebooks/{id}/artifacts` | Read-only |
|
||||
| `GET /api/v1/notebooks/{id}/artifacts/{id}/status` | Read-only |
|
||||
| `GET /health` | Health check |
|
||||
| `POST /api/v1/webhooks/{id}/test` | Test non distruttivo |
|
||||
|
||||
### ⚠️ Chiedi Conferma Prima
|
||||
|
||||
| Operazione | Motivo |
|
||||
|------------|--------|
|
||||
| `POST /api/v1/notebooks` | Crea risorsa |
|
||||
| `DELETE /api/v1/notebooks/{id}` | Distruttivo |
|
||||
| `POST /api/v1/notebooks/{id}/sources` | Aggiunge dati |
|
||||
| `POST /api/v1/notebooks/{id}/generate/*` | Lungo, può fallire |
|
||||
| `GET /api/v1/notebooks/{id}/artifacts/{id}/download` | Scrive filesystem |
|
||||
| `POST /api/v1/webhooks` | Configura endpoint |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference API
|
||||
|
||||
### Notebook Operations
|
||||
|
||||
```bash
|
||||
# Creare notebook
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Ricerca AI", "description": "Studio sull\'intelligenza artificiale"}'
|
||||
|
||||
# Listare notebook
|
||||
curl http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-key"
|
||||
|
||||
# Ottenere notebook specifico
|
||||
curl http://localhost:8000/api/v1/notebooks/{notebook_id} \
|
||||
-H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
### Source Operations
|
||||
|
||||
```bash
|
||||
# Aggiungere URL
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/sources \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "url", "url": "https://example.com/article"}'
|
||||
|
||||
# Aggiungere PDF
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/sources \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "type=file" \
|
||||
-F "file=@/path/to/document.pdf"
|
||||
|
||||
# Ricerca web
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/sources/research \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "intelligenza artificiale 2026", "mode": "deep", "auto_import": true}'
|
||||
```
|
||||
|
||||
### Chat Operations
|
||||
|
||||
```bash
|
||||
# Inviare messaggio
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/chat \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Quali sono i punti chiave?", "include_references": true}'
|
||||
|
||||
# Ottenere storico
|
||||
curl http://localhost:8000/api/v1/notebooks/{id}/chat/history \
|
||||
-H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
### Content Generation
|
||||
|
||||
```bash
|
||||
# Generare podcast audio
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/audio \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"instructions": "Rendi il podcast coinvolgente e accessibile",
|
||||
"format": "deep-dive",
|
||||
"length": "long",
|
||||
"language": "it"
|
||||
}'
|
||||
|
||||
# Generare video
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/video \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"instructions": "Video esplicativo professionale",
|
||||
"style": "whiteboard",
|
||||
"language": "it"
|
||||
}'
|
||||
|
||||
# Generare quiz
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/quiz \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"difficulty": "medium",
|
||||
"quantity": "standard"
|
||||
}'
|
||||
|
||||
# Generare flashcards
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/flashcards \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"difficulty": "hard",
|
||||
"quantity": "more"
|
||||
}'
|
||||
|
||||
# Generare slide deck
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/slide-deck \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"format": "detailed",
|
||||
"length": "default"
|
||||
}'
|
||||
|
||||
# Generare infografica
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/infographic \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"orientation": "portrait",
|
||||
"detail": "detailed"
|
||||
}'
|
||||
|
||||
# Generare mappa mentale (instant)
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/mind-map \
|
||||
-H "X-API-Key: your-key"
|
||||
|
||||
# Generare tabella dati
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/data-table \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"description": "Confronta i diversi approcci di machine learning"
|
||||
}'
|
||||
```
|
||||
|
||||
### Artifact Management
|
||||
|
||||
```bash
|
||||
# Listare artifacts
|
||||
curl http://localhost:8000/api/v1/notebooks/{id}/artifacts \
|
||||
-H "X-API-Key: your-key"
|
||||
|
||||
# Controllare stato
|
||||
curl http://localhost:8000/api/v1/notebooks/{id}/artifacts/{artifact_id}/status \
|
||||
-H "X-API-Key: your-key"
|
||||
|
||||
# Attendere completamento
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/artifacts/{artifact_id}/wait \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"timeout": 1200}'
|
||||
|
||||
# Scaricare artifact
|
||||
curl http://localhost:8000/api/v1/notebooks/{id}/artifacts/{artifact_id}/download \
|
||||
-H "X-API-Key: your-key" \
|
||||
-o artifact.mp3
|
||||
```
|
||||
|
||||
### Webhook Management
|
||||
|
||||
```bash
|
||||
# Registrare webhook
|
||||
curl -X POST http://localhost:8000/api/v1/webhooks \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"url": "https://your-agent.com/webhook",
|
||||
"events": ["artifact.completed", "source.ready"],
|
||||
"secret": "your-webhook-secret"
|
||||
}'
|
||||
|
||||
# Listare webhook
|
||||
curl http://localhost:8000/api/v1/webhooks \
|
||||
-H "X-API-Key: your-key"
|
||||
|
||||
# Testare webhook
|
||||
curl -X POST http://localhost:8000/api/v1/webhooks/{webhook_id}/test \
|
||||
-H "X-API-Key: your-key"
|
||||
|
||||
# Rimuovere webhook
|
||||
curl -X DELETE http://localhost:8000/api/v1/webhooks/{webhook_id} \
|
||||
-H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Generation Options
|
||||
|
||||
### Audio (Podcast)
|
||||
|
||||
| Parametro | Valori | Default |
|
||||
|-----------|--------|---------|
|
||||
| `format` | `deep-dive`, `brief`, `critique`, `debate` | `deep-dive` |
|
||||
| `length` | `short`, `default`, `long` | `default` |
|
||||
| `language` | `en`, `it`, `es`, `fr`, `de`, ... | `en` |
|
||||
|
||||
### Video
|
||||
|
||||
| Parametro | Valori | Default |
|
||||
|-----------|--------|---------|
|
||||
| `format` | `explainer`, `brief` | `explainer` |
|
||||
| `style` | `auto`, `classic`, `whiteboard`, `kawaii`, `anime`, `watercolor`, `retro-print`, `heritage`, `paper-craft` | `auto` |
|
||||
| `language` | Codice lingua | `en` |
|
||||
|
||||
### Slide Deck
|
||||
|
||||
| Parametro | Valori | Default |
|
||||
|-----------|--------|---------|
|
||||
| `format` | `detailed`, `presenter` | `detailed` |
|
||||
| `length` | `default`, `short` | `default` |
|
||||
|
||||
### Infographic
|
||||
|
||||
| Parametro | Valori | Default |
|
||||
|-----------|--------|---------|
|
||||
| `orientation` | `landscape`, `portrait`, `square` | `landscape` |
|
||||
| `detail` | `concise`, `standard`, `detailed` | `standard` |
|
||||
| `style` | `auto`, `sketch-note`, `professional`, `bento-grid`, `editorial`, `instructional`, `bricks`, `clay`, `anime`, `kawaii`, `scientific` | `auto` |
|
||||
|
||||
### Quiz / Flashcards
|
||||
|
||||
| Parametro | Valori | Default |
|
||||
|-----------|--------|---------|
|
||||
| `difficulty` | `easy`, `medium`, `hard` | `medium` |
|
||||
| `quantity` | `fewer`, `standard`, `more` | `standard` |
|
||||
|
||||
---
|
||||
|
||||
## Workflow Comuni
|
||||
|
||||
### Workflow 1: Research to Podcast
|
||||
|
||||
```bash
|
||||
# 1. Creare notebook
|
||||
NOTEBOOK=$(curl -s -X POST http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "AI Research"}' | jq -r '.data.id')
|
||||
|
||||
# 2. Aggiungere fonti
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/$NOTEBOOK/sources \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "url", "url": "https://example.com/ai-article"}'
|
||||
|
||||
# 3. Ricerca web (opzionale)
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/$NOTEBOOK/sources/research \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "latest AI trends 2026", "mode": "deep", "auto_import": true}'
|
||||
|
||||
# 4. Generare podcast
|
||||
ARTIFACT=$(curl -s -X POST http://localhost:8000/api/v1/notebooks/$NOTEBOOK/generate/audio \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"instructions": "Make it engaging", "format": "deep-dive", "length": "long"}' | jq -r '.data.artifact_id')
|
||||
|
||||
# 5. Attendere completamento
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/$NOTEBOOK/artifacts/$ARTIFACT/wait \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"timeout": 1200}'
|
||||
|
||||
# 6. Scaricare
|
||||
curl http://localhost:8000/api/v1/notebooks/$NOTEBOOK/artifacts/$ARTIFACT/download \
|
||||
-H "X-API-Key: your-key" \
|
||||
-o podcast.mp3
|
||||
```
|
||||
|
||||
### Workflow 2: Document Analysis
|
||||
|
||||
```bash
|
||||
# Creare notebook e caricare PDF
|
||||
NOTEBOOK=$(curl -s -X POST http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Document Analysis"}' | jq -r '.data.id')
|
||||
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/$NOTEBOOK/sources \
|
||||
-H "X-API-Key: your-key" \
|
||||
-F "type=file" \
|
||||
-F "file=@document.pdf"
|
||||
|
||||
# Interrogare contenuto
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/$NOTEBOOK/chat \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Summarize the key points"}'
|
||||
|
||||
# Generare quiz
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/$NOTEBOOK/generate/quiz \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"difficulty": "medium"}'
|
||||
```
|
||||
|
||||
### Workflow 3: Webhook Integration
|
||||
|
||||
```bash
|
||||
# 1. Registrare webhook per ricevere notifiche
|
||||
curl -X POST http://localhost:8000/api/v1/webhooks \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"url": "https://my-agent.com/notebooklm-webhook",
|
||||
"events": ["artifact.completed", "artifact.failed", "source.ready"],
|
||||
"secret": "secure-webhook-secret"
|
||||
}'
|
||||
|
||||
# 2. Avviare generazione lunga
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks/{id}/generate/audio \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"instructions": "Create engaging podcast"}'
|
||||
|
||||
# 3. Il webhook riceverà:
|
||||
# {
|
||||
# "event": "artifact.completed",
|
||||
# "timestamp": "2026-04-05T10:30:00Z",
|
||||
# "data": {
|
||||
# "notebook_id": "...",
|
||||
# "artifact_id": "...",
|
||||
# "type": "audio",
|
||||
# "download_url": "..."
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response Formats
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "abc123...",
|
||||
"title": "My Notebook",
|
||||
"created_at": "2026-04-05T10:30:00Z"
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-05T10:30:00Z",
|
||||
"request_id": "req-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid notebook title",
|
||||
"details": [
|
||||
{"field": "title", "error": "Title must be at least 3 characters"}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-05T10:30:00Z",
|
||||
"request_id": "req-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webhook Events
|
||||
|
||||
### Event Types
|
||||
|
||||
| Evento | Descrizione | Payload |
|
||||
|--------|-------------|---------|
|
||||
| `notebook.created` | Nuovo notebook creato | `{notebook_id, title}` |
|
||||
| `source.added` | Nuova fonte aggiunta | `{notebook_id, source_id, type}` |
|
||||
| `source.ready` | Fonte indicizzata | `{notebook_id, source_id, title}` |
|
||||
| `source.error` | Errore indicizzazione | `{notebook_id, source_id, error}` |
|
||||
| `artifact.pending` | Generazione avviata | `{notebook_id, artifact_id, type}` |
|
||||
| `artifact.completed` | Generazione completata | `{notebook_id, artifact_id, type}` |
|
||||
| `artifact.failed` | Generazione fallita | `{notebook_id, artifact_id, error}` |
|
||||
| `research.completed` | Ricerca completata | `{notebook_id, sources_count}` |
|
||||
|
||||
### Webhook Security
|
||||
|
||||
Gli webhook includono header `X-Webhook-Signature` con HMAC-SHA256:
|
||||
|
||||
```python
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
signature = hmac.new(
|
||||
secret.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
# Verificare: signature == request.headers['X-Webhook-Signature']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Codice | Descrizione | Azione |
|
||||
|--------|-------------|--------|
|
||||
| `AUTH_ERROR` | Autenticazione fallita | Verificare API key |
|
||||
| `NOTEBOOKLM_AUTH_ERROR` | Sessione NotebookLM scaduta | Eseguire `notebooklm login` |
|
||||
| `VALIDATION_ERROR` | Dati input non validi | Correggere payload |
|
||||
| `NOT_FOUND` | Risorsa non trovata | Verificare ID |
|
||||
| `RATE_LIMITED` | Rate limit NotebookLM | Attendere e riprovare |
|
||||
| `GENERATION_FAILED` | Generazione fallita | Verificare fonti, riprovare |
|
||||
| `TIMEOUT` | Operazione timeout | Estendere timeout, riprovare |
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
Per operazioni che falliscono con `RATE_LIMITED`:
|
||||
- Attendere 5-10 minuti
|
||||
- Riprovare con exponential backoff
|
||||
- Massimo 3 retry
|
||||
|
||||
---
|
||||
|
||||
## Timing Guide
|
||||
|
||||
| Operazione | Tempo Tipico | Timeout Consigliato |
|
||||
|------------|--------------|---------------------|
|
||||
| Creazione notebook | <1s | 30s |
|
||||
| Aggiunta fonte URL | 10-60s | 120s |
|
||||
| Aggiunta PDF | 30s-10min | 600s |
|
||||
| Ricerca web (fast) | 30s-2min | 180s |
|
||||
| Ricerca web (deep) | 15-30min | 1800s |
|
||||
| Generazione quiz | 5-15min | 900s |
|
||||
| Generazione audio | 10-20min | 1200s |
|
||||
| Generazione video | 15-45min | 2700s |
|
||||
| Mind map | Istantaneo | n/a |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Usa webhook per operazioni lunghe** - Non bloccare l'agente in polling
|
||||
2. **Gestisci rate limits** - NotebookLM ha limiti aggressivi
|
||||
3. **Verifica firma webhook** - Sicurezza endpoint
|
||||
4. **Usa UUID completi** in automazione - Evita ambiguità
|
||||
5. **Isola contesti per agenti paralleli** - Usa profile o NOTEBOOKLM_HOME
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```bash
|
||||
# Verifica stato API
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Verifica autenticazione NotebookLM
|
||||
notebooklm auth check --test
|
||||
|
||||
# Log dettagliati
|
||||
LOG_LEVEL=DEBUG uv run fastapi dev src/notebooklm_agent/api/main.py
|
||||
|
||||
# Lista notebook per verificare
|
||||
curl http://localhost:8000/api/v1/notebooks -H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Skill Version:** 1.0.0
|
||||
**API Version:** v1
|
||||
**Last Updated:** 2026-04-05
|
||||
71
docs/README.md
Normal file
71
docs/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Documentation
|
||||
|
||||
Benvenuto nella documentazione di NotebookLM Agent API.
|
||||
|
||||
## Indice
|
||||
|
||||
- [API Reference](./api/) - Documentazione completa delle API (TODO)
|
||||
- [Examples](./examples/) - Esempi di utilizzo (TODO)
|
||||
|
||||
## Panoramica
|
||||
|
||||
NotebookLM Agent API fornisce:
|
||||
|
||||
1. **REST API** per gestire notebook, fonti, chat e generazione contenuti
|
||||
2. **Webhook System** per notifiche event-driven
|
||||
3. **AI Skill** per integrazione con agenti AI
|
||||
|
||||
## Endpoint Principali
|
||||
|
||||
### Notebook Management
|
||||
- `POST /api/v1/notebooks` - Creare notebook
|
||||
- `GET /api/v1/notebooks` - Listare notebook
|
||||
- `GET /api/v1/notebooks/{id}` - Ottenere notebook
|
||||
- `DELETE /api/v1/notebooks/{id}` - Eliminare notebook
|
||||
|
||||
### Source Management
|
||||
- `POST /api/v1/notebooks/{id}/sources` - Aggiungere fonte
|
||||
- `GET /api/v1/notebooks/{id}/sources` - Listare fonti
|
||||
- `POST /api/v1/notebooks/{id}/sources/research` - Ricerca web
|
||||
|
||||
### Content Generation
|
||||
- `POST /api/v1/notebooks/{id}/generate/audio` - Generare podcast
|
||||
- `POST /api/v1/notebooks/{id}/generate/video` - Generare video
|
||||
- `POST /api/v1/notebooks/{id}/generate/quiz` - Generare quiz
|
||||
- `POST /api/v1/notebooks/{id}/generate/flashcards` - Generare flashcard
|
||||
|
||||
### Webhooks
|
||||
- `POST /api/v1/webhooks` - Registrare webhook
|
||||
- `GET /api/v1/webhooks` - Listare webhook
|
||||
- `POST /api/v1/webhooks/{id}/test` - Testare webhook
|
||||
|
||||
## Autenticazione
|
||||
|
||||
Tutte le richieste API richiedono header `X-API-Key`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-api-key"
|
||||
```
|
||||
|
||||
## Webhook Security
|
||||
|
||||
I webhook includono firma HMAC-SHA256 nell'header `X-Webhook-Signature`:
|
||||
|
||||
```python
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
signature = hmac.new(
|
||||
secret.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
```
|
||||
|
||||
## Risorse
|
||||
|
||||
- [README](../README.md) - Panoramica progetto
|
||||
- [PRD](../prd.md) - Requisiti prodotto
|
||||
- [SKILL.md](../SKILL.md) - Skill per agenti AI
|
||||
- [CONTRIBUTING](../CONTRIBUTING.md) - Come contribuire
|
||||
512
docs/api/endpoints.md
Normal file
512
docs/api/endpoints.md
Normal file
@@ -0,0 +1,512 @@
|
||||
# API Endpoints Documentation
|
||||
|
||||
> NotebookLM Agent API - Endpoint Reference
|
||||
|
||||
**Version**: 0.1.0
|
||||
**Base URL**: `http://localhost:8000`
|
||||
**OpenAPI**: `/docs` (Swagger UI)
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests require an API key in the `X-API-Key` header:
|
||||
|
||||
```bash
|
||||
X-API-Key: your-api-key-here
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-api-key"
|
||||
```
|
||||
|
||||
### Error Response (401 Unauthorized)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "AUTH_ERROR",
|
||||
"message": "API key required",
|
||||
"details": null
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notebooks
|
||||
|
||||
### Create Notebook
|
||||
|
||||
Create a new notebook.
|
||||
|
||||
**Endpoint**: `POST /api/v1/notebooks`
|
||||
|
||||
#### Request
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "My Research Notebook",
|
||||
"description": "A collection of AI research papers"
|
||||
}'
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "string (required, min: 3, max: 100)",
|
||||
"description": "string (optional, max: 500)"
|
||||
}
|
||||
```
|
||||
|
||||
#### Success Response (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "My Research Notebook",
|
||||
"description": "A collection of AI research papers",
|
||||
"created_at": "2026-04-06T10:30:00Z",
|
||||
"updated_at": "2026-04-06T10:30:00Z"
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**400 Bad Request** - Validation Error
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid input data",
|
||||
"details": [
|
||||
{
|
||||
"field": "title",
|
||||
"error": "Title must be at least 3 characters"
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**401 Unauthorized** - Missing/Invalid API Key
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "AUTH_ERROR",
|
||||
"message": "API key required"
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**502 Bad Gateway** - NotebookLM API Error
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "NOTEBOOKLM_ERROR",
|
||||
"message": "External API error: rate limit exceeded"
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### List Notebooks
|
||||
|
||||
List all notebooks with pagination.
|
||||
|
||||
**Endpoint**: `GET /api/v1/notebooks`
|
||||
|
||||
#### Request
|
||||
|
||||
```bash
|
||||
# Basic request
|
||||
curl http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-api-key"
|
||||
|
||||
# With pagination
|
||||
curl "http://localhost:8000/api/v1/notebooks?limit=10&offset=0&sort=created_at&order=desc" \
|
||||
-H "X-API-Key: your-api-key"
|
||||
|
||||
# Sort by title ascending
|
||||
curl "http://localhost:8000/api/v1/notebooks?sort=title&order=asc" \
|
||||
-H "X-API-Key: your-api-key"
|
||||
```
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `limit` | integer | 20 | Max items per page (1-100) |
|
||||
| `offset` | integer | 0 | Items to skip |
|
||||
| `sort` | string | "created_at" | Sort field: `created_at`, `updated_at`, `title` |
|
||||
| `order` | string | "desc" | Sort order: `asc`, `desc` |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "My Research Notebook",
|
||||
"description": "A collection of AI research papers",
|
||||
"created_at": "2026-04-06T10:00:00Z",
|
||||
"updated_at": "2026-04-06T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"title": "Another Notebook",
|
||||
"description": null,
|
||||
"created_at": "2026-04-06T09:00:00Z",
|
||||
"updated_at": "2026-04-06T09:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 100,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"has_more": true
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**401 Unauthorized** - Missing/Invalid API Key
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "AUTH_ERROR",
|
||||
"message": "API key required"
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Notebook
|
||||
|
||||
Get a single notebook by ID.
|
||||
|
||||
**Endpoint**: `GET /api/v1/notebooks/{notebook_id}`
|
||||
|
||||
#### Request
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/api/v1/notebooks/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-H "X-API-Key: your-api-key"
|
||||
```
|
||||
|
||||
**Path Parameters**:
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `notebook_id` | UUID | Notebook unique identifier |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "My Research Notebook",
|
||||
"description": "A collection of AI research papers",
|
||||
"created_at": "2026-04-06T10:00:00Z",
|
||||
"updated_at": "2026-04-06T10:30:00Z"
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**404 Not Found** - Notebook doesn't exist
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Notebook with id '550e8400-e29b-41d4-a716-446655440000' not found"
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**400 Bad Request** - Invalid UUID format
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid notebook ID format"
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Notebook
|
||||
|
||||
Update an existing notebook (partial update).
|
||||
|
||||
**Endpoint**: `PATCH /api/v1/notebooks/{notebook_id}`
|
||||
|
||||
#### Request
|
||||
|
||||
```bash
|
||||
# Update title only
|
||||
curl -X PATCH http://localhost:8000/api/v1/notebooks/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Updated Title"
|
||||
}'
|
||||
|
||||
# Update description only
|
||||
curl -X PATCH http://localhost:8000/api/v1/notebooks/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"description": "Updated description"
|
||||
}'
|
||||
|
||||
# Update both
|
||||
curl -X PATCH http://localhost:8000/api/v1/notebooks/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Updated Title",
|
||||
"description": "Updated description"
|
||||
}'
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "string (optional, min: 3, max: 100)",
|
||||
"description": "string (optional, max: 500)"
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Only provided fields are updated. At least one field must be provided.
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "Updated Title",
|
||||
"description": "Updated description",
|
||||
"created_at": "2026-04-06T10:00:00Z",
|
||||
"updated_at": "2026-04-06T11:00:00Z"
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-04-06T11:00:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**400 Bad Request** - Validation Error
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid input data",
|
||||
"details": [
|
||||
{
|
||||
"field": "title",
|
||||
"error": "Title must be at least 3 characters"
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**404 Not Found** - Notebook doesn't exist
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Notebook with id '...' not found"
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete Notebook
|
||||
|
||||
Delete a notebook permanently.
|
||||
|
||||
**Endpoint**: `DELETE /api/v1/notebooks/{notebook_id}`
|
||||
|
||||
#### Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8000/api/v1/notebooks/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-H "X-API-Key: your-api-key"
|
||||
```
|
||||
|
||||
#### Success Response (204 No Content)
|
||||
|
||||
Empty body.
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**404 Not Found** - Notebook doesn't exist
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Notebook with id '...' not found"
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: This operation is idempotent. Deleting the same notebook twice returns 404 on the second attempt.
|
||||
|
||||
---
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Create and List Notebooks
|
||||
|
||||
```bash
|
||||
# 1. Create a notebook
|
||||
NOTEBOOK=$(curl -s -X POST http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "AI Research"}' | jq -r '.data.id')
|
||||
|
||||
# 2. List all notebooks
|
||||
curl http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: your-key"
|
||||
|
||||
# 3. Get specific notebook
|
||||
curl http://localhost:8000/api/v1/notebooks/$NOTEBOOK \
|
||||
-H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
### Update and Delete
|
||||
|
||||
```bash
|
||||
NOTEBOOK_ID="550e8400-e29b-41d4-a716-446655440000"
|
||||
|
||||
# Update title
|
||||
curl -X PATCH http://localhost:8000/api/v1/notebooks/$NOTEBOOK_ID \
|
||||
-H "X-API-Key: your-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "New Title"}'
|
||||
|
||||
# Delete notebook
|
||||
curl -X DELETE http://localhost:8000/api/v1/notebooks/$NOTEBOOK_ID \
|
||||
-H "X-API-Key: your-key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `VALIDATION_ERROR` | 400 | Input validation failed |
|
||||
| `AUTH_ERROR` | 401 | Authentication failed (missing/invalid API key) |
|
||||
| `NOT_FOUND` | 404 | Resource not found |
|
||||
| `RATE_LIMITED` | 429 | Rate limit exceeded |
|
||||
| `NOTEBOOKLM_ERROR` | 502 | External NotebookLM API error |
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
API requests are rate-limited to prevent abuse. Rate limit headers are included in responses:
|
||||
|
||||
```http
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1712400000
|
||||
```
|
||||
|
||||
If you exceed the rate limit, you'll receive a `429 Too Many Requests` response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "RATE_LIMITED",
|
||||
"message": "Rate limit exceeded. Try again in 60 seconds.",
|
||||
"details": [{"retry_after": 60}]
|
||||
},
|
||||
"meta": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Documentazione generata automaticamente da @api-designer*
|
||||
*Data: 2026-04-06*
|
||||
*Versione API: 0.1.0*
|
||||
472
prd.md
Normal file
472
prd.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# Product Requirements Document (PRD)
|
||||
|
||||
## NotebookLM Agent API
|
||||
|
||||
**Versione:** 1.0.0
|
||||
**Data:** 2026-04-05
|
||||
**Autore:** NotebookLM Agent Team
|
||||
**Status:** Draft
|
||||
|
||||
---
|
||||
|
||||
## 1. Panoramica del Prodotto
|
||||
|
||||
### 1.1 Nome del Prodotto
|
||||
**NotebookLM Agent API** - Un agente LLM che fornisce accesso programmatico a Google NotebookLM tramite interfaccia API con webhook.
|
||||
|
||||
### 1.2 Descrizione
|
||||
Sistema agentico basato su Python che integra Google NotebookLM tramite la libreria `notebooklm-py`, offrendo:
|
||||
- Interfaccia API REST per automazione
|
||||
- Webhook per integrazione con altri agenti AI
|
||||
- Skill nativa per agenti AI (OpenCode, Claude, Codex)
|
||||
- Metodologia Spec-Driven Development (SDD)
|
||||
- Test Driven Development (TDD)
|
||||
|
||||
### 1.3 Obiettivi Principali
|
||||
1. Fornire accesso programmatico completo a NotebookLM
|
||||
2. Creare un'interfaccia API standardizzata per integrazione multi-agent
|
||||
3. Supportare workflow automatizzati di ricerca e generazione contenuti
|
||||
4. Garantire qualità del codice attraverso TDD e SDD
|
||||
|
||||
---
|
||||
|
||||
## 2. Obiettivi
|
||||
|
||||
### 2.1 Obiettivi di Business
|
||||
- [ ] Creare un servizio API stabile per NotebookLM
|
||||
- [ ] Supportare integrazione con ecosistema AI multi-agent
|
||||
- [ ] Fornire webhook per event-driven architecture
|
||||
- [ ] Garantire manutenibilità attraverso test automatizzati (>90% coverage)
|
||||
|
||||
### 2.2 Obiettivi Utente
|
||||
- [ ] Creare notebook e gestire fonti programmaticamente
|
||||
- [ ] Generare contenuti (audio, video, quiz, flashcard) via API
|
||||
- [ ] Ricevere notifiche webhook su completamento operazioni
|
||||
- [ ] Integrare con altri agenti AI tramite API standard
|
||||
|
||||
### 2.3 Metriche di Successo (KPI)
|
||||
| Metrica | Target | Note |
|
||||
|---------|--------|------|
|
||||
| Code Coverage | ≥90% | Pytest + coverage |
|
||||
| API Uptime | ≥99.5% | Per operazioni core |
|
||||
| Response Time (API) | <500ms | Per operazioni sync |
|
||||
| Webhook Delivery | ≥99% | Con retry automatico |
|
||||
| Test Pass Rate | 100% | CI/CD gate |
|
||||
|
||||
---
|
||||
|
||||
## 3. Pubblico Target
|
||||
|
||||
### 3.1 Persona 1: AI Agent Developer
|
||||
- **Ruolo:** Sviluppatore di agenti AI
|
||||
- **Needs:** API stabile, webhook affidabili, documentazione chiara
|
||||
- **Frustrazioni:** API instabili, mancanza di webhook, documentazione scarsa
|
||||
- **Obiettivi:** Integrare NotebookLM nel proprio agente AI
|
||||
|
||||
### 3.2 Persona 2: Automation Engineer
|
||||
- **Ruolo:** Ingegnere automazione
|
||||
- **Needs:** Automazione research-to-content, batch processing
|
||||
- **Frustrazioni:** Processi manuali ripetitivi
|
||||
- **Obiettivi:** Pipeline automatizzate di ricerca e generazione
|
||||
|
||||
### 3.3 Persona 3: Content Creator
|
||||
- **Ruolo:** Creatore di contenuti
|
||||
- **Needs:** Generazione podcast/video da fonti multiple
|
||||
- **Frustrazioni:** Operazioni manuali su NotebookLM
|
||||
- **Obiettivi:** Workflow automatizzato research → content
|
||||
|
||||
---
|
||||
|
||||
## 4. Requisiti Funzionali
|
||||
|
||||
### 4.1 Core: API REST
|
||||
|
||||
#### REQ-001: Gestione Notebook
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** CRUD operazioni su notebook NotebookLM
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] POST /api/v1/notebooks - Creare notebook
|
||||
- [ ] GET /api/v1/notebooks - Listare notebook
|
||||
- [ ] GET /api/v1/notebooks/{id} - Ottenere dettagli
|
||||
- [ ] DELETE /api/v1/notebooks/{id} - Eliminare notebook
|
||||
- [ ] PATCH /api/v1/notebooks/{id} - Aggiornare notebook
|
||||
|
||||
**User Story:**
|
||||
*"Come sviluppatore, voglio creare e gestire notebook via API per automatizzare workflow"*
|
||||
|
||||
#### REQ-002: Gestione Fonti
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** Aggiungere e gestire fonti (URL, PDF, YouTube, Drive)
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] POST /api/v1/notebooks/{id}/sources - Aggiungere fonte
|
||||
- [ ] GET /api/v1/notebooks/{id}/sources - Listare fonti
|
||||
- [ ] DELETE /api/v1/notebooks/{id}/sources/{source_id} - Rimuovere fonte
|
||||
- [ ] GET /api/v1/notebooks/{id}/sources/{source_id}/fulltext - Ottenere testo indicizzato
|
||||
- [ ] POST /api/v1/notebooks/{id}/sources/research - Avviare ricerca web/Drive
|
||||
|
||||
**User Story:**
|
||||
*"Come utente, voglio aggiungere fonti programmaticamente per analisi bulk"*
|
||||
|
||||
#### REQ-003: Chat e Query
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** Interagire con i contenuti tramite chat
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] POST /api/v1/notebooks/{id}/chat - Inviare messaggio
|
||||
- [ ] GET /api/v1/notebooks/{id}/chat/history - Ottenere storico
|
||||
- [ ] POST /api/v1/notebooks/{id}/chat/save - Salvare risposta come nota
|
||||
|
||||
**User Story:**
|
||||
*"Come utente, voglio interrogare le fonti via API per estrarre insights"*
|
||||
|
||||
#### REQ-004: Generazione Contenuti
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** Generare tutti i tipi di contenuto NotebookLM
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/audio - Generare podcast
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/video - Generare video
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/slide-deck - Generare slide
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/infographic - Generare infografica
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/quiz - Generare quiz
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/flashcards - Generare flashcard
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/report - Generare report
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/mind-map - Generare mappa mentale
|
||||
- [ ] POST /api/v1/notebooks/{id}/generate/data-table - Generare tabella dati
|
||||
|
||||
**User Story:**
|
||||
*"Come creatore, voglio generare contenuti multi-formato automaticamente"*
|
||||
|
||||
#### REQ-005: Gestione Artifacts
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** Monitorare e scaricare contenuti generati
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] GET /api/v1/notebooks/{id}/artifacts - Listare artifacts
|
||||
- [ ] GET /api/v1/notebooks/{id}/artifacts/{artifact_id}/status - Controllare stato
|
||||
- [ ] GET /api/v1/notebooks/{id}/artifacts/{artifact_id}/download - Scaricare artifact
|
||||
- [ ] POST /api/v1/notebooks/{id}/artifacts/{artifact_id}/wait - Attendere completamento
|
||||
|
||||
**User Story:**
|
||||
*"Come utente, voglio scaricare contenuti generati in vari formati"*
|
||||
|
||||
### 4.2 Core: Webhook System
|
||||
|
||||
#### REQ-006: Webhook Management
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** Gestire endpoint webhook per notifiche eventi
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] POST /api/v1/webhooks - Registrare webhook
|
||||
- [ ] GET /api/v1/webhooks - Listare webhook registrati
|
||||
- [ ] DELETE /api/v1/webhooks/{id} - Rimuovere webhook
|
||||
- [ ] POST /api/v1/webhooks/{id}/test - Testare webhook
|
||||
|
||||
**User Story:**
|
||||
*"Come sviluppatore, voglio ricevere notifiche su eventi NotebookLM"*
|
||||
|
||||
#### REQ-007: Eventi Webhook
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** Inviare notifiche su eventi specifici
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] source.added - Nuova fonte aggiunta
|
||||
- [ ] source.ready - Fonte indicizzata e pronta
|
||||
- [ ] artifact.generated - Artifact generato con successo
|
||||
- [ ] artifact.failed - Generazione artifact fallita
|
||||
- [ ] research.completed - Ricerca completata
|
||||
- [ ] notebook.shared - Notebook condiviso
|
||||
|
||||
**User Story:**
|
||||
*"Come agente AI, voglio essere notificato quando un contenuto è pronto"*
|
||||
|
||||
#### REQ-008: Webhook Reliability
|
||||
**Priorità:** Media
|
||||
**Descrizione:** Garantire affidabilità delivery webhook
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] Retry automatico con exponential backoff
|
||||
- [ ] Firma HMAC per verifica autenticità
|
||||
- [ ] Timeout configurabile (default: 30s)
|
||||
- [ ] Delivery tracking con ID univoco
|
||||
|
||||
**User Story:**
|
||||
*"Come sviluppatore, voglio webhook affidabili con verifica sicurezza"*
|
||||
|
||||
### 4.3 Core: Skill AI
|
||||
|
||||
#### REQ-009: Skill OpenCode
|
||||
**Priorità:** Alta
|
||||
**Descrizione:** Skill nativa per OpenCode CLI
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] File SKILL.md conforme specifiche OpenCode
|
||||
- [ ] Comandi natural language supportati
|
||||
- [ ] Autonomy rules definite
|
||||
- [ ] Error handling documentato
|
||||
|
||||
**User Story:**
|
||||
*"Come utente OpenCode, voglio usare NotebookLM tramite comandi naturali"*
|
||||
|
||||
#### REQ-010: Multi-Agent Integration
|
||||
**Priorità:** Media
|
||||
**Descrizione:** Supporto per integrazione con altri agenti AI
|
||||
**Criteri di Accettazione:**
|
||||
- [ ] API Key authentication per agenti
|
||||
- [ ] Rate limiting per tenant
|
||||
- [ ] Isolamento risorse per agente
|
||||
|
||||
---
|
||||
|
||||
## 5. Requisiti Non Funzionali
|
||||
|
||||
### 5.1 Performance
|
||||
- API Response Time: <500ms per operazioni sincrone
|
||||
- Webhook Delivery: <5s dalla generazione evento
|
||||
- Throughput: 100 req/s per endpoint API
|
||||
- Connessioni concorrenti: ≥1000
|
||||
|
||||
### 5.2 Sicurezza
|
||||
- API Key authentication per tutti gli endpoint
|
||||
- HTTPS obbligatorio in produzione
|
||||
- HMAC signature per webhook
|
||||
- Sanitizzazione input (prevenzione injection)
|
||||
- Nessuna persistenza credenziali NotebookLM (usa storage_state.json)
|
||||
|
||||
### 5.3 Affidabilità
|
||||
- Uptime target: 99.5%
|
||||
- Retry automatico per operazioni fallite
|
||||
- Circuit breaker per API esterne (NotebookLM)
|
||||
- Graceful degradation su rate limiting
|
||||
|
||||
### 5.4 Scalabilità
|
||||
- Architettura stateless
|
||||
- Supporto per horizontal scaling
|
||||
- Queue-based processing per operazioni lunghe
|
||||
- Caching risultati dove appropriato
|
||||
|
||||
### 5.5 Monitoring
|
||||
- Logging strutturato (JSON)
|
||||
- Metrics (Prometheus-compatible)
|
||||
- Health check endpoints
|
||||
- Alerting su errori critici
|
||||
|
||||
---
|
||||
|
||||
## 6. Stack Tecnologico
|
||||
|
||||
### 6.1 Core Technologies
|
||||
| Componente | Tecnologia | Versione |
|
||||
|------------|------------|----------|
|
||||
| Language | Python | ≥3.10 |
|
||||
| Framework API | FastAPI | ≥0.100 |
|
||||
| Async | asyncio | built-in |
|
||||
| HTTP Client | httpx | ≥0.27 |
|
||||
| Validation | Pydantic | ≥2.0 |
|
||||
|
||||
### 6.2 NotebookLM Integration
|
||||
| Componente | Tecnologia | Note |
|
||||
|------------|------------|------|
|
||||
| NotebookLM Client | notebooklm-py | ≥0.3.4 |
|
||||
| Auth | playwright | Per browser login |
|
||||
| Storage | Local JSON | storage_state.json |
|
||||
|
||||
### 6.3 Testing & Quality
|
||||
| Componente | Tecnologia | Scopo |
|
||||
|------------|------------|-------|
|
||||
| Testing | pytest | Unit/Integration/E2E |
|
||||
| Coverage | pytest-cov | ≥90% target |
|
||||
| Linting | ruff | Code quality |
|
||||
| Type Check | mypy | Static typing |
|
||||
| Pre-commit | pre-commit | Git hooks |
|
||||
|
||||
### 6.4 DevOps
|
||||
| Componente | Tecnologia | Scopo |
|
||||
|------------|------------|-------|
|
||||
| Package | uv | Dependency management |
|
||||
| Build | hatchling | Package building |
|
||||
| Git | conventional commits | Standardizzazione commit |
|
||||
| Changelog | common-changelog | Gestione changelog |
|
||||
|
||||
---
|
||||
|
||||
## 7. Architettura
|
||||
|
||||
### 7.1 System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Clients │
|
||||
│ (OpenCode, Claude, Codex, Custom Agents, Direct API) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ NotebookLM Agent API │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ REST API │ │ Webhook │ │ Skill Interface │ │
|
||||
│ │ Layer │ │ Manager │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ Service │ │ Event │ │ Queue/Worker │ │
|
||||
│ │ Layer │ │ Bus │ │ (Celery/RQ) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ NotebookLM-py Client Library │
|
||||
│ (Async wrapper + RPC handling) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Google NotebookLM │
|
||||
│ (Undocumented Internal APIs) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.2 API Structure
|
||||
|
||||
```
|
||||
/api/v1/
|
||||
├── notebooks/
|
||||
│ ├── GET / # List
|
||||
│ ├── POST / # Create
|
||||
│ ├── GET /{id} # Get
|
||||
│ ├── DELETE /{id} # Delete
|
||||
│ ├── PATCH /{id} # Update
|
||||
│ ├── sources/
|
||||
│ │ ├── GET / # List sources
|
||||
│ │ ├── POST / # Add source
|
||||
│ │ ├── DELETE /{source_id} # Remove source
|
||||
│ │ ├── GET /{source_id}/fulltext
|
||||
│ │ └── POST /research # Web/Drive research
|
||||
│ ├── chat/
|
||||
│ │ ├── POST / # Send message
|
||||
│ │ ├── GET /history # Get history
|
||||
│ │ └── POST /save # Save as note
|
||||
│ ├── generate/
|
||||
│ │ ├── POST /audio
|
||||
│ │ ├── POST /video
|
||||
│ │ ├── POST /slide-deck
|
||||
│ │ ├── POST /infographic
|
||||
│ │ ├── POST /quiz
|
||||
│ │ ├── POST /flashcards
|
||||
│ │ ├── POST /report
|
||||
│ │ ├── POST /mind-map
|
||||
│ │ └── POST /data-table
|
||||
│ └── artifacts/
|
||||
│ ├── GET / # List
|
||||
│ ├── GET /{id}/status # Status
|
||||
│ ├── GET /{id}/download # Download
|
||||
│ └── POST /{id}/wait # Wait completion
|
||||
├── webhooks/
|
||||
│ ├── GET / # List
|
||||
│ ├── POST / # Register
|
||||
│ ├── DELETE /{id} # Remove
|
||||
│ └── POST /{id}/test # Test
|
||||
└── health/
|
||||
├── GET / # Health check
|
||||
└── GET /ready # Readiness probe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Metodologia di Sviluppo
|
||||
|
||||
### 8.1 Spec-Driven Development (SDD)
|
||||
1. **Specifica:** Definire requisiti e API contract prima del codice
|
||||
2. **Review:** Revisione specifica con stakeholder
|
||||
3. **Implementazione:** Sviluppo seguendo la spec
|
||||
4. **Validazione:** Verifica conformità alla spec
|
||||
|
||||
### 8.2 Test Driven Development (TDD)
|
||||
1. **Red:** Scrivere test che fallisce
|
||||
2. **Green:** Implementare codice minimo per passare il test
|
||||
3. **Refactor:** Migliorare codice mantenendo test pass
|
||||
|
||||
### 8.3 Testing Strategy
|
||||
| Livello | Scope | Tools | Coverage |
|
||||
|---------|-------|-------|----------|
|
||||
| Unit | Funzioni isolate | pytest, mock | ≥90% |
|
||||
| Integration | Componenti integrati | pytest, httpx | ≥80% |
|
||||
| E2E | Flussi completi | pytest, real API | Key paths |
|
||||
|
||||
### 8.4 Conventional Commits
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:** feat, fix, docs, style, refactor, test, chore, ci
|
||||
**Scopes:** api, webhook, skill, notebook, source, artifact, auth
|
||||
|
||||
---
|
||||
|
||||
## 9. Piano di Rilascio
|
||||
|
||||
### 9.1 Milestone
|
||||
|
||||
| Milestone | Data | Features | Stato |
|
||||
|-----------|------|----------|-------|
|
||||
| v0.1.0 | 2026-04-15 | Core API (notebook, source, chat) | Pianificato |
|
||||
| v0.2.0 | 2026-04-30 | Generazione contenuti + webhook | Pianificato |
|
||||
| v0.3.0 | 2026-05-15 | Skill OpenCode + multi-agent | Pianificato |
|
||||
| v1.0.0 | 2026-06-01 | Production ready + docs complete | Pianificato |
|
||||
|
||||
### 9.2 Roadmap
|
||||
- **Q2 2026:** Core features, stabilizzazione API
|
||||
- **Q3 2026:** Advanced features, performance optimization
|
||||
- **Q4 2026:** Enterprise features, scaling improvements
|
||||
|
||||
---
|
||||
|
||||
## 10. Analisi dei Rischi
|
||||
|
||||
| Rischio | Probabilità | Impatto | Mitigazione |
|
||||
|---------|-------------|---------|-------------|
|
||||
| API NotebookLM cambiano | Alta | Alto | Wrapper abstraction, monitoring |
|
||||
| Rate limiting agressivo | Media | Medio | Retry logic, queue-based processing |
|
||||
| Auth session scade | Media | Medio | Refresh automatico, alerting |
|
||||
| Webhook delivery fallisce | Media | Medio | Retry con backoff, dead letter queue |
|
||||
| Breaking changes notebooklm-py | Bassa | Alto | Version pinning, vendor tests |
|
||||
|
||||
---
|
||||
|
||||
## 11. Note e Assunzioni
|
||||
|
||||
### 11.1 Assunzioni
|
||||
- Utente ha account Google valido con accesso NotebookLM
|
||||
- `notebooklm-py` è installato e funzionante
|
||||
- Playwright configurato per autenticazione browser
|
||||
- Ambiente Python 3.10+ disponibile
|
||||
|
||||
### 11.2 Dipendenze Esterne
|
||||
- Google NotebookLM (undocumented APIs)
|
||||
- notebooklm-py library
|
||||
- Playwright (browser automation)
|
||||
|
||||
### 11.3 Vincoli
|
||||
- Nessuna persistenza server-side per credenziali
|
||||
- Rate limits di NotebookLM applicati
|
||||
- Alcune operazioni sono asincrone per natura
|
||||
|
||||
---
|
||||
|
||||
## 12. Approvazioni
|
||||
|
||||
| Ruolo | Nome | Firma | Data |
|
||||
|-------|------|-------|------|
|
||||
| Product Owner | | | |
|
||||
| Tech Lead | | | |
|
||||
| QA Lead | | | |
|
||||
|
||||
---
|
||||
|
||||
## 13. Cronologia Revisioni
|
||||
|
||||
| Versione | Data | Autore | Modifiche |
|
||||
|----------|------|--------|-----------|
|
||||
| 0.1.0 | 2026-04-05 | NotebookLM Agent Team | Bozza iniziale |
|
||||
|
||||
---
|
||||
|
||||
**Documento PRD**
|
||||
*Ultimo aggiornamento: 2026-04-05*
|
||||
224
prompts/1-avvio.md
Normal file
224
prompts/1-avvio.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Sprint Kickoff: Implementazione Core API - Notebook Management
|
||||
|
||||
## 📋 Comando per @sprint-lead
|
||||
|
||||
@sprint-lead Avvia il workflow completo per implementare la gestione notebook CRUD.
|
||||
Questo è lo sprint iniziale per rendere l'API funzionante.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Obiettivo Sprint
|
||||
|
||||
**Implementare l'API REST completa per la gestione dei notebook NotebookLM**,
|
||||
permettendo agli utenti di creare, leggere, aggiornare ed eliminare notebook via API.
|
||||
|
||||
**Success Criteria**:
|
||||
- Endpoint CRUD `/api/v1/notebooks` funzionanti con test ≥90% coverage
|
||||
- Documentazione SKILL.md aggiornata con esempi curl funzionanti
|
||||
- CI/CD pipeline verde su GitHub Actions
|
||||
|
||||
---
|
||||
|
||||
## 📚 Contesto & Background
|
||||
|
||||
### Stato Attuale
|
||||
- ✅ Setup progetto completo (struttura, tooling, CI/CD)
|
||||
- ✅ API base funzionante (health check, dependencies)
|
||||
- ✅ Core package (config, exceptions, logging)
|
||||
- ✅ Squadra agenti configurata e pronta
|
||||
|
||||
### Documentazione Riferimento
|
||||
- **PRD**: `prd.md` - Sezione 4.1 (Requisiti Notebook Management)
|
||||
- **Workflow**: `.opencode/WORKFLOW.md`
|
||||
- **Skill**: `.opencode/skills/project-guidelines/SKILL.md`
|
||||
- **API Design**: Da definire (@api-designer)
|
||||
- **NotebookLM-py docs**: https://github.com/teng-lin/notebooklm-py
|
||||
|
||||
### Requisiti dal PRD (REQ-001)
|
||||
- `POST /api/v1/notebooks` - Creare notebook
|
||||
- `GET /api/v1/notebooks` - Listare notebook (con pagination)
|
||||
- `GET /api/v1/notebooks/{id}` - Ottenere dettagli notebook
|
||||
- `PATCH /api/v1/notebooks/{id}` - Aggiornare notebook
|
||||
- `DELETE /api/v1/notebooks/{id}` - Eliminare notebook
|
||||
|
||||
---
|
||||
|
||||
## ✅ Scope (Incluso)
|
||||
|
||||
### In Scope
|
||||
1. **API Design** (@api-designer)
|
||||
- Modelli Pydantic (NotebookCreate, NotebookUpdate, NotebookResponse)
|
||||
- Query parameters per pagination (limit, offset)
|
||||
- Error response standard
|
||||
|
||||
2. **Implementazione** (@tdd-developer + @qa-engineer)
|
||||
- Service layer: `NotebookService` con integrazione notebooklm-py
|
||||
- API routes: CRUD endpoints in `api/routes/notebooks.py`
|
||||
- Unit tests: ≥90% coverage
|
||||
- Integration tests: Mock HTTP client
|
||||
|
||||
3. **Security** (@security-auditor)
|
||||
- API key validation su tutti gli endpoint
|
||||
- Input validation (Pydantic)
|
||||
- Rate limiting headers
|
||||
|
||||
4. **Documentazione** (@docs-maintainer)
|
||||
- Aggiornare SKILL.md con esempi curl
|
||||
- Aggiornare CHANGELOG.md
|
||||
- Creare `docs/api/endpoints.md`
|
||||
|
||||
5. **Quality Gates** (@code-reviewer)
|
||||
- Code review completa
|
||||
- Type hints check
|
||||
- No code smells
|
||||
|
||||
### Out of Scope (Prossimi Sprint)
|
||||
- Gestione fonti (sources) - Sprint 2
|
||||
- Chat functionality - Sprint 3
|
||||
- Content generation - Sprint 4
|
||||
- Webhook system - Sprint 5
|
||||
- E2E tests con NotebookLM reale (richiede auth)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Vincoli & Constraints
|
||||
|
||||
1. **Tecnici**
|
||||
- Python 3.10+ con type hints obbligatori
|
||||
- FastAPI + Pydantic v2
|
||||
- Coverage ≥90%
|
||||
- Async/await per I/O bound operations
|
||||
|
||||
2. **Architetturali**
|
||||
- Layering: API → Service → Core
|
||||
- Dependency injection
|
||||
- Nessuna logica business nei router
|
||||
|
||||
3. **NotebookLM-py**
|
||||
- Usare `NotebookLMClient.from_storage()` per auth
|
||||
- Gestire rate limiting con retry
|
||||
- Wrappare eccezioni in custom exceptions
|
||||
|
||||
4. **Tempo**
|
||||
- Stima: 3-5 giorni (complexity: media)
|
||||
- Daily standup virtuale ogni mattina
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Criteri di Accettazione (Definition of Done)
|
||||
|
||||
### Per @sprint-lead - Checklist Finale:
|
||||
- [ ] **@spec-architect**: Specifiche in `export/prd.md` e `export/architecture.md`
|
||||
- [ ] **@api-designer**: Modelli Pydantic in `api/models/`, API docs in `docs/api/`
|
||||
- [ ] **@security-auditor**: Security review completata, no [BLOCKING] issues
|
||||
- [ ] **@tdd-developer**: Unit tests passanti, coverage ≥90%
|
||||
- [ ] **@qa-engineer**: Integration tests passanti
|
||||
- [ ] **@code-reviewer**: Review approvata, no [BLOCKING] issues
|
||||
- [ ] **@docs-maintainer**: SKILL.md e CHANGELOG.md aggiornati
|
||||
- [ ] **@git-manager**: Commit atomici con conventional commits
|
||||
- [ ] **CI/CD**: Pipeline verde su GitHub Actions
|
||||
|
||||
### Test Manuale di Verifica:
|
||||
```bash
|
||||
# Avvia server
|
||||
uv run fastapi dev src/notebooklm_agent/api/main.py
|
||||
|
||||
# Test CRUD
|
||||
curl -X POST http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: test-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Test Notebook"}'
|
||||
|
||||
curl http://localhost:8000/api/v1/notebooks \
|
||||
-H "X-API-Key: test-key"
|
||||
|
||||
curl http://localhost:8000/api/v1/notebooks/{id} \
|
||||
-H "X-API-Key: test-key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Metriche Target
|
||||
|
||||
| Metrica | Target | Attuale |
|
||||
|---------|--------|---------|
|
||||
| API Endpoints | 5 CRUD | 1 (health) |
|
||||
| Test Coverage | ≥90% | ~60% |
|
||||
| Lines of Code | ~500 | ~200 |
|
||||
| Docs Completeness | 100% | 20% |
|
||||
| CI/CD Status | 🟢 Green | 🟡 Partial |
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Azioni Immediate per @sprint-lead
|
||||
|
||||
1. **Ora**: Attiva @spec-architect per analisi dettagliata
|
||||
- Leggere PRD sezione 4.1
|
||||
- Creare `export/kanban.md` con task breakdown
|
||||
- Definire interfacce NotebookService
|
||||
|
||||
2. **Quando @spec-architect completa**: Attiva @api-designer
|
||||
- Progettare modelli Pydantic
|
||||
- Definire schema OpenAPI
|
||||
- Documentare esempi
|
||||
|
||||
3. **Poi**: Attiva @tdd-developer + @qa-engineer in parallelo
|
||||
- RED: Scrivere test
|
||||
- GREEN: Implementare codice
|
||||
- REFACTOR: Migliorare
|
||||
|
||||
4. **Infine**: Quality gates e deploy
|
||||
- @code-reviewer → @docs-maintainer → @git-manager
|
||||
|
||||
---
|
||||
|
||||
## 📝 Note Aggiuntive
|
||||
|
||||
### Pattern da Seguire
|
||||
```python
|
||||
# Service Layer
|
||||
class NotebookService:
|
||||
async def create(self, data: NotebookCreate) -> Notebook:
|
||||
# Business logic qui
|
||||
pass
|
||||
|
||||
# API Layer
|
||||
@router.post("/")
|
||||
async def create_notebook(
|
||||
data: NotebookCreate,
|
||||
service: NotebookService = Depends(get_notebook_service)
|
||||
):
|
||||
return await service.create(data)
|
||||
```
|
||||
|
||||
### Gestione Errori
|
||||
Usare gerarchia eccezioni in `core/exceptions.py`:
|
||||
- `ValidationError` → HTTP 400
|
||||
- `NotFoundError` → HTTP 404
|
||||
- `AuthenticationError` → HTTP 401
|
||||
- `NotebookLMError` → HTTP 502 (bad gateway)
|
||||
|
||||
### Rate Limiting NotebookLM
|
||||
Il client notebooklm-py ha rate limiting aggressivo.
|
||||
Implementare retry con exponential backoff in service layer.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Call to Action
|
||||
|
||||
**@sprint-lead**:
|
||||
1. Inizializza `export/progress.md` con questo sprint
|
||||
2. Attiva **@spec-architect** per la fase di analisi
|
||||
3. Programma daily standup per domani mattina
|
||||
4. Monitora progresso e gestisci blocchi
|
||||
|
||||
**Team**: Seguire il workflow in `.opencode/WORKFLOW.md` e le linee guida in `.opencode/skills/project-guidelines/SKILL.md`.
|
||||
|
||||
**Obiettivo**: Notebook CRUD API production-ready entro venerdì! 🚀
|
||||
|
||||
---
|
||||
|
||||
*Data Inizio Sprint: 2026-04-06*
|
||||
*Sprint Lead: @sprint-lead*
|
||||
*Priority: P0 (Foundation)*
|
||||
*Prompt File: prompts/1-avvio.md*
|
||||
113
prompts/README.md
Normal file
113
prompts/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Prompts Directory
|
||||
|
||||
Questa cartella contiene tutti i prompt utilizzati per ingaggiare il team di agenti.
|
||||
|
||||
## Convenzione di Naming
|
||||
|
||||
I file prompt seguono la convenzione: `{NUMERO}-{NOME}.md`
|
||||
|
||||
- **NUMERO**: Numero progressivo crescente (1, 2, 3, ...)
|
||||
- **NOME**: Nome descrittivo del prompt (kebab-case)
|
||||
|
||||
## Lista Prompt
|
||||
|
||||
| File | Descrizione | Data |
|
||||
|------|-------------|------|
|
||||
| [1-avvio.md](./1-avvio.md) | Sprint kickoff - Implementazione Core API Notebook Management | 2026-04-06 |
|
||||
|
||||
## Come Aggiungere un Nuovo Prompt
|
||||
|
||||
1. Determina il prossimo numero progressivo (es: se l'ultimo è `3-xxx.md`, il prossimo sarà `4-`)
|
||||
2. Crea il file con nome descrittivo: `{NUMERO}-{descrizione}.md`
|
||||
3. Segui il template standard per i prompt (vedi sotto)
|
||||
4. Aggiorna questa README aggiungendo il nuovo prompt alla tabella
|
||||
|
||||
## Template Prompt Standard
|
||||
|
||||
```markdown
|
||||
# {Titolo Sprint/Task}
|
||||
|
||||
## 📋 Comando per @sprint-lead
|
||||
|
||||
@sprint-lead {istruzione specifica}
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Obiettivo
|
||||
|
||||
{Descrizione chiara dell'obiettivo}
|
||||
|
||||
**Success Criteria**:
|
||||
- {Criterio 1}
|
||||
- {Criterio 2}
|
||||
|
||||
---
|
||||
|
||||
## 📚 Contesto & Background
|
||||
|
||||
### Stato Attuale
|
||||
- {Stato attuale 1}
|
||||
- {Stato attuale 2}
|
||||
|
||||
### Documentazione Riferimento
|
||||
- **PRD**: `prd.md` - Sezione X
|
||||
- **Workflow**: `.opencode/WORKFLOW.md`
|
||||
- {Altri riferimenti}
|
||||
|
||||
---
|
||||
|
||||
## ✅ Scope (Incluso)
|
||||
|
||||
### In Scope
|
||||
1. {Task 1}
|
||||
2. {Task 2}
|
||||
|
||||
### Out of Scope
|
||||
- {Task escluso 1}
|
||||
- {Task escluso 2}
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Vincoli & Constraints
|
||||
|
||||
1. {Vincolo 1}
|
||||
2. {Vincolo 2}
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Criteri di Accettazione (Definition of Done)
|
||||
|
||||
- [ ] {Criterio 1}
|
||||
- [ ] {Criterio 2}
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Azioni Immediate
|
||||
|
||||
1. {Azione 1}
|
||||
2. {Azione 2}
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Call to Action
|
||||
|
||||
**@sprint-lead**: {Istruzioni specifiche}
|
||||
|
||||
**Team**: {Istruzioni per il team}
|
||||
|
||||
---
|
||||
|
||||
*Data: YYYY-MM-DD*
|
||||
*Priority: P{0-3}*
|
||||
*Prompt File: prompts/{NUMERO}-{nome}.md*
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
- I prompt sono versionati e tracciati
|
||||
- Ogni prompt rappresenta uno sprint, una feature o un task specifico
|
||||
- I prompt storici servono per:
|
||||
- Documentare decisioni passate
|
||||
- Riutilizzare pattern
|
||||
- Audit trail delle attività
|
||||
- Onboarding di nuovi agenti
|
||||
150
pyproject.toml
Normal file
150
pyproject.toml
Normal file
@@ -0,0 +1,150 @@
|
||||
[project]
|
||||
name = "notebooklm-agent"
|
||||
version = "0.1.0"
|
||||
description = "API and webhook interface for Google NotebookLM automation with AI agent integration"
|
||||
dynamic = ["readme"]
|
||||
requires-python = ">=3.10"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "NotebookLM Agent Team", email = "team@example.com"}
|
||||
]
|
||||
keywords = ["notebooklm", "api", "webhook", "ai", "agent", "automation"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
dependencies = [
|
||||
"fastapi>=0.100.0",
|
||||
"uvicorn[standard]>=0.23.0",
|
||||
"httpx>=0.27.0",
|
||||
"pydantic>=2.0.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"notebooklm-py>=0.3.4",
|
||||
"python-multipart>=0.0.6",
|
||||
"python-jose[cryptography]>=3.3.0",
|
||||
"passlib[bcrypt]>=1.7.4",
|
||||
"structlog>=24.1.0",
|
||||
"tenacity>=8.2.0",
|
||||
"celery>=5.3.0",
|
||||
"redis>=5.0.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/example/notebooklm-agent"
|
||||
Repository = "https://github.com/example/notebooklm-agent"
|
||||
Documentation = "https://github.com/example/notebooklm-agent#readme"
|
||||
Issues = "https://github.com/example/notebooklm-agent/issues"
|
||||
|
||||
[project.optional-dependencies]
|
||||
browser = ["playwright>=1.40.0"]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"pytest-httpx>=0.30.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
"pytest-rerunfailures>=14.0",
|
||||
"pytest-timeout>=2.3.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
"mypy>=1.0.0",
|
||||
"pre-commit>=4.5.1",
|
||||
"ruff==0.8.6",
|
||||
"vcrpy>=6.0.0",
|
||||
"httpx>=0.27.0",
|
||||
"respx>=0.21.0",
|
||||
]
|
||||
all = ["notebooklm-agent[browser,dev]"]
|
||||
|
||||
[project.scripts]
|
||||
notebooklm-agent = "notebooklm_agent.cli:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling", "hatch-fancy-pypi-readme"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata.hooks.fancy-pypi-readme]
|
||||
content-type = "text/markdown"
|
||||
|
||||
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
||||
path = "README.md"
|
||||
|
||||
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
||||
path = "CHANGELOG.md"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/notebooklm_agent"]
|
||||
force-include = {"SKILL.md" = "notebooklm_agent/data/SKILL.md", "AGENTS.md" = "notebooklm_agent/data/AGENTS.md"}
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
addopts = "--ignore=tests/e2e"
|
||||
timeout = 60
|
||||
markers = [
|
||||
"e2e: end-to-end tests requiring authentication (run with pytest tests/e2e -m e2e)",
|
||||
"integration: integration tests with mocked external APIs",
|
||||
"unit: pure unit tests",
|
||||
"slow: slow tests to run selectively",
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/notebooklm_agent"]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
fail_under = 90
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_return_any = false
|
||||
warn_unused_ignores = true
|
||||
disallow_untyped_defs = false
|
||||
check_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
files = ["src/notebooklm_agent"]
|
||||
exclude = ["tests/"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "notebooklm_agent.api.*"
|
||||
warn_return_any = false
|
||||
strict_optional = true
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
src = ["src", "tests"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"UP", # pyupgrade
|
||||
"SIM", # flake8-simplify
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by formatter)
|
||||
"B008", # function call in default argument (FastAPI uses this)
|
||||
"SIM102", # nested ifs - kept for readability
|
||||
"SIM105", # contextlib.suppress - explicit try/except clearer
|
||||
]
|
||||
per-file-ignores = {"src/notebooklm_agent/__init__.py" = ["E402"]}
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["notebooklm_agent"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
31
src/notebooklm_agent/__init__.py
Normal file
31
src/notebooklm_agent/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""NotebookLM Agent API - Unofficial Python API for Google NotebookLM automation.
|
||||
|
||||
This package provides a REST API and webhook interface for Google NotebookLM,
|
||||
enabling programmatic access to notebook management, source handling,
|
||||
content generation, and multi-agent integration.
|
||||
|
||||
Example:
|
||||
>>> from notebooklm_agent import NotebookAgent
|
||||
>>> agent = NotebookAgent()
|
||||
>>> notebook = await agent.create_notebook("My Research")
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "NotebookLM Agent Team"
|
||||
|
||||
# Core exports
|
||||
from notebooklm_agent.core.config import Settings
|
||||
from notebooklm_agent.core.exceptions import (
|
||||
NotebookLMAgentError,
|
||||
ValidationError,
|
||||
AuthenticationError,
|
||||
NotFoundError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Settings",
|
||||
"NotebookLMAgentError",
|
||||
"ValidationError",
|
||||
"AuthenticationError",
|
||||
"NotFoundError",
|
||||
]
|
||||
1
src/notebooklm_agent/api/__init__.py
Normal file
1
src/notebooklm_agent/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for API routes package
|
||||
63
src/notebooklm_agent/api/dependencies.py
Normal file
63
src/notebooklm_agent/api/dependencies.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""FastAPI dependencies for NotebookLM Agent API."""
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, status
|
||||
from fastapi.security import APIKeyHeader
|
||||
|
||||
from notebooklm_agent.core.config import Settings, get_settings
|
||||
from notebooklm_agent.core.exceptions import AuthenticationError
|
||||
|
||||
# Security scheme
|
||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
|
||||
|
||||
async def verify_api_key(
|
||||
api_key: Annotated[str | None, Depends(api_key_header)],
|
||||
settings: Annotated[Settings, Depends(get_settings)],
|
||||
) -> str:
|
||||
"""Verify API key from request header.
|
||||
|
||||
Args:
|
||||
api_key: API key from X-API-Key header.
|
||||
settings: Application settings.
|
||||
|
||||
Returns:
|
||||
Verified API key.
|
||||
|
||||
Raises:
|
||||
HTTPException: If API key is invalid or missing.
|
||||
"""
|
||||
if not settings.api_key:
|
||||
# If no API key is configured, allow all requests (development mode)
|
||||
return api_key or "dev"
|
||||
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="API key required",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
if api_key != settings.api_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid API key",
|
||||
)
|
||||
|
||||
return api_key
|
||||
|
||||
|
||||
async def get_current_settings() -> Settings:
|
||||
"""Get current application settings.
|
||||
|
||||
Returns:
|
||||
Application settings instance.
|
||||
"""
|
||||
return get_settings()
|
||||
|
||||
|
||||
# Type aliases for dependency injection
|
||||
VerifyAPIKey = Annotated[str, Depends(verify_api_key)]
|
||||
CurrentSettings = Annotated[Settings, Depends(get_current_settings)]
|
||||
74
src/notebooklm_agent/api/main.py
Normal file
74
src/notebooklm_agent/api/main.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""FastAPI application entry point for NotebookLM Agent API."""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from notebooklm_agent.api.routes import health, notebooks
|
||||
from notebooklm_agent.core.config import get_settings
|
||||
from notebooklm_agent.core.logging import setup_logging
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager.
|
||||
|
||||
Handles startup and shutdown events.
|
||||
"""
|
||||
# Startup
|
||||
setup_logging()
|
||||
yield
|
||||
# Shutdown
|
||||
|
||||
|
||||
def create_application() -> FastAPI:
|
||||
"""Create and configure FastAPI application.
|
||||
|
||||
Returns:
|
||||
Configured FastAPI application instance.
|
||||
"""
|
||||
app = FastAPI(
|
||||
title="NotebookLM Agent API",
|
||||
description="API and webhook interface for Google NotebookLM automation",
|
||||
version="0.1.0",
|
||||
docs_url="/docs" if not settings.is_production else None,
|
||||
redoc_url="/redoc" if not settings.is_production else None,
|
||||
openapi_url="/openapi.json",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
if settings.cors_origins:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(health.router, prefix="/health", tags=["health"])
|
||||
app.include_router(notebooks.router, prefix="/api/v1/notebooks", tags=["notebooks"])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_application()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint.
|
||||
|
||||
Returns:
|
||||
Basic API information.
|
||||
"""
|
||||
return {
|
||||
"name": "NotebookLM Agent API",
|
||||
"version": "0.1.0",
|
||||
"documentation": "/docs",
|
||||
}
|
||||
42
src/notebooklm_agent/api/models/__init__.py
Normal file
42
src/notebooklm_agent/api/models/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""API models for NotebookLM Agent API.
|
||||
|
||||
This package contains Pydantic models for request validation and response serialization.
|
||||
"""
|
||||
|
||||
from notebooklm_agent.api.models.requests import (
|
||||
NotebookCreate,
|
||||
NotebookListParams,
|
||||
NotebookUpdate,
|
||||
SourceCreate,
|
||||
)
|
||||
from notebooklm_agent.api.models.responses import (
|
||||
ApiResponse,
|
||||
ErrorDetail,
|
||||
HealthStatus,
|
||||
Notebook,
|
||||
NotebookDetail,
|
||||
PaginatedNotebooks,
|
||||
PaginatedSources,
|
||||
PaginationMeta,
|
||||
ResponseMeta,
|
||||
Source,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Request models
|
||||
"NotebookCreate",
|
||||
"NotebookUpdate",
|
||||
"NotebookListParams",
|
||||
"SourceCreate",
|
||||
# Response models
|
||||
"ApiResponse",
|
||||
"ErrorDetail",
|
||||
"ResponseMeta",
|
||||
"Notebook",
|
||||
"NotebookDetail",
|
||||
"PaginationMeta",
|
||||
"PaginatedNotebooks",
|
||||
"Source",
|
||||
"PaginatedSources",
|
||||
"HealthStatus",
|
||||
]
|
||||
231
src/notebooklm_agent/api/models/requests.py
Normal file
231
src/notebooklm_agent/api/models/requests.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""Request models for NotebookLM Agent API.
|
||||
|
||||
This module contains Pydantic models for API request validation.
|
||||
All models use Pydantic v2 syntax.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class NotebookCreate(BaseModel):
|
||||
"""Request model for creating a new notebook.
|
||||
|
||||
Attributes:
|
||||
title: The notebook title (3-100 characters).
|
||||
description: Optional notebook description (max 500 characters).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"title": "My Research Notebook",
|
||||
"description": "A collection of AI research papers and notes",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
title: str = Field(
|
||||
...,
|
||||
min_length=3,
|
||||
max_length=100,
|
||||
description="The notebook title",
|
||||
examples=["My Research Notebook", "AI Study Notes"],
|
||||
)
|
||||
description: str | None = Field(
|
||||
None,
|
||||
max_length=500,
|
||||
description="Optional notebook description",
|
||||
examples=["A collection of AI research papers"],
|
||||
)
|
||||
|
||||
@field_validator("title")
|
||||
@classmethod
|
||||
def title_not_empty(cls, v: str) -> str:
|
||||
"""Validate title is not empty or whitespace only."""
|
||||
if not v or not v.strip():
|
||||
raise ValueError("Title cannot be empty")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("description")
|
||||
@classmethod
|
||||
def description_optional(cls, v: str | None) -> str | None:
|
||||
"""Strip whitespace from description if provided."""
|
||||
if v is not None:
|
||||
return v.strip() or None
|
||||
return v
|
||||
|
||||
|
||||
class NotebookUpdate(BaseModel):
|
||||
"""Request model for updating an existing notebook.
|
||||
|
||||
All fields are optional to support partial updates (PATCH).
|
||||
Only provided fields will be updated.
|
||||
|
||||
Attributes:
|
||||
title: New notebook title (3-100 characters).
|
||||
description: New notebook description (max 500 characters).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"title": "Updated Research Notebook",
|
||||
"description": "Updated description",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
title: str | None = Field(
|
||||
None,
|
||||
min_length=3,
|
||||
max_length=100,
|
||||
description="New notebook title",
|
||||
examples=["Updated Research Notebook"],
|
||||
)
|
||||
description: str | None = Field(
|
||||
None,
|
||||
max_length=500,
|
||||
description="New notebook description",
|
||||
examples=["Updated description"],
|
||||
)
|
||||
|
||||
@field_validator("title")
|
||||
@classmethod
|
||||
def title_not_empty_if_provided(cls, v: str | None) -> str | None:
|
||||
"""Validate title is not empty if provided."""
|
||||
if v is not None:
|
||||
if not v.strip():
|
||||
raise ValueError("Title cannot be empty")
|
||||
return v.strip()
|
||||
return v
|
||||
|
||||
@field_validator("description")
|
||||
@classmethod
|
||||
def description_strip(cls, v: str | None) -> str | None:
|
||||
"""Strip whitespace from description if provided."""
|
||||
if v is not None:
|
||||
return v.strip() or None
|
||||
return v
|
||||
|
||||
|
||||
class NotebookListParams(BaseModel):
|
||||
"""Query parameters for listing notebooks.
|
||||
|
||||
Attributes:
|
||||
limit: Maximum number of items to return (1-100).
|
||||
offset: Number of items to skip.
|
||||
sort: Field to sort by.
|
||||
order: Sort order (ascending or descending).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"sort": "created_at",
|
||||
"order": "desc",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
limit: int = Field(
|
||||
20,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of items to return",
|
||||
examples=[20, 50, 100],
|
||||
)
|
||||
offset: int = Field(
|
||||
0,
|
||||
ge=0,
|
||||
description="Number of items to skip",
|
||||
examples=[0, 20, 40],
|
||||
)
|
||||
sort: str = Field(
|
||||
"created_at",
|
||||
pattern="^(created_at|updated_at|title)$",
|
||||
description="Field to sort by",
|
||||
examples=["created_at", "updated_at", "title"],
|
||||
)
|
||||
order: str = Field(
|
||||
"desc",
|
||||
pattern="^(asc|desc)$",
|
||||
description="Sort order",
|
||||
examples=["asc", "desc"],
|
||||
)
|
||||
|
||||
@field_validator("sort")
|
||||
@classmethod
|
||||
def validate_sort_field(cls, v: str) -> str:
|
||||
"""Validate sort field is allowed."""
|
||||
allowed = {"created_at", "updated_at", "title"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Sort field must be one of: {allowed}")
|
||||
return v
|
||||
|
||||
@field_validator("order")
|
||||
@classmethod
|
||||
def validate_order(cls, v: str) -> str:
|
||||
"""Validate order is asc or desc."""
|
||||
if v not in {"asc", "desc"}:
|
||||
raise ValueError("Order must be 'asc' or 'desc'")
|
||||
return v
|
||||
|
||||
|
||||
class SourceCreate(BaseModel):
|
||||
"""Request model for adding a source to a notebook.
|
||||
|
||||
Attributes:
|
||||
type: Type of source (url, file, etc.).
|
||||
url: URL for web sources.
|
||||
title: Optional title override.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"type": "url",
|
||||
"url": "https://example.com/article",
|
||||
"title": "Optional Title Override",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type: str = Field(
|
||||
...,
|
||||
description="Type of source",
|
||||
examples=["url", "file", "youtube"],
|
||||
)
|
||||
url: str | None = Field(
|
||||
None,
|
||||
description="URL for web sources",
|
||||
examples=["https://example.com/article"],
|
||||
)
|
||||
title: str | None = Field(
|
||||
None,
|
||||
max_length=200,
|
||||
description="Optional title override",
|
||||
examples=["Custom Title"],
|
||||
)
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
def validate_type(cls, v: str) -> str:
|
||||
"""Validate source type."""
|
||||
allowed = {"url", "file", "youtube", "drive"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"Type must be one of: {allowed}")
|
||||
return v
|
||||
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_url(cls, v: str | None, info: Any) -> str | None:
|
||||
"""Validate URL is provided for url type."""
|
||||
if info.data.get("type") == "url" and not v:
|
||||
raise ValueError("URL is required for type 'url'")
|
||||
return v
|
||||
420
src/notebooklm_agent/api/models/responses.py
Normal file
420
src/notebooklm_agent/api/models/responses.py
Normal file
@@ -0,0 +1,420 @@
|
||||
"""Response models for NotebookLM Agent API.
|
||||
|
||||
This module contains Pydantic models for API response serialization.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Generic, TypeVar
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ErrorDetail(BaseModel):
|
||||
"""Error detail information.
|
||||
|
||||
Attributes:
|
||||
code: Error code for programmatic handling.
|
||||
message: Human-readable error message.
|
||||
details: Additional error details (field errors, etc.).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid input data",
|
||||
"details": [{"field": "title", "error": "Title is too short"}],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
code: str = Field(
|
||||
...,
|
||||
description="Error code for programmatic handling",
|
||||
examples=["VALIDATION_ERROR", "NOT_FOUND", "AUTH_ERROR"],
|
||||
)
|
||||
message: str = Field(
|
||||
...,
|
||||
description="Human-readable error message",
|
||||
examples=["Invalid input data", "Notebook not found"],
|
||||
)
|
||||
details: list[dict[str, Any]] | None = Field(
|
||||
None,
|
||||
description="Additional error details",
|
||||
examples=[[{"field": "title", "error": "Title is too short"}]],
|
||||
)
|
||||
|
||||
|
||||
class ResponseMeta(BaseModel):
|
||||
"""Metadata for API responses.
|
||||
|
||||
Attributes:
|
||||
timestamp: ISO 8601 timestamp of the response.
|
||||
request_id: Unique identifier for the request.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
timestamp: datetime = Field(
|
||||
...,
|
||||
description="ISO 8601 timestamp of the response",
|
||||
examples=["2026-04-06T10:30:00Z"],
|
||||
)
|
||||
request_id: UUID = Field(
|
||||
...,
|
||||
description="Unique identifier for the request",
|
||||
examples=["550e8400-e29b-41d4-a716-446655440000"],
|
||||
)
|
||||
|
||||
|
||||
class ApiResponse(BaseModel, Generic[T]):
|
||||
"""Standard API response wrapper.
|
||||
|
||||
This wrapper is used for all API responses to ensure consistency.
|
||||
|
||||
Attributes:
|
||||
success: Whether the request was successful.
|
||||
data: Response data (None if error).
|
||||
error: Error details (None if success).
|
||||
meta: Response metadata.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"success": True,
|
||||
"data": {"id": "550e8400-e29b-41d4-a716-446655440000"},
|
||||
"error": None,
|
||||
"meta": {
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
success: bool = Field(
|
||||
...,
|
||||
description="Whether the request was successful",
|
||||
examples=[True, False],
|
||||
)
|
||||
data: T | None = Field(
|
||||
None,
|
||||
description="Response data (None if error)",
|
||||
)
|
||||
error: ErrorDetail | None = Field(
|
||||
None,
|
||||
description="Error details (None if success)",
|
||||
)
|
||||
meta: ResponseMeta = Field(
|
||||
...,
|
||||
description="Response metadata",
|
||||
)
|
||||
|
||||
|
||||
class Notebook(BaseModel):
|
||||
"""Notebook response model.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier (UUID).
|
||||
title: Notebook title.
|
||||
description: Optional notebook description.
|
||||
created_at: Creation timestamp.
|
||||
updated_at: Last update timestamp.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "My Research Notebook",
|
||||
"description": "A collection of AI research papers",
|
||||
"created_at": "2026-04-06T10:00:00Z",
|
||||
"updated_at": "2026-04-06T10:30:00Z",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
id: UUID = Field(
|
||||
...,
|
||||
description="Unique identifier (UUID)",
|
||||
examples=["550e8400-e29b-41d4-a716-446655440000"],
|
||||
)
|
||||
title: str = Field(
|
||||
...,
|
||||
description="Notebook title",
|
||||
examples=["My Research Notebook"],
|
||||
)
|
||||
description: str | None = Field(
|
||||
None,
|
||||
description="Optional notebook description",
|
||||
examples=["A collection of AI research papers"],
|
||||
)
|
||||
created_at: datetime = Field(
|
||||
...,
|
||||
description="Creation timestamp (ISO 8601)",
|
||||
examples=["2026-04-06T10:00:00Z"],
|
||||
)
|
||||
updated_at: datetime = Field(
|
||||
...,
|
||||
description="Last update timestamp (ISO 8601)",
|
||||
examples=["2026-04-06T10:30:00Z"],
|
||||
)
|
||||
|
||||
|
||||
class NotebookDetail(Notebook):
|
||||
"""Detailed notebook response with counts.
|
||||
|
||||
Extends Notebook with additional statistics.
|
||||
|
||||
Attributes:
|
||||
sources_count: Number of sources in the notebook.
|
||||
artifacts_count: Number of artifacts in the notebook.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "My Research Notebook",
|
||||
"description": "A collection of AI research papers",
|
||||
"created_at": "2026-04-06T10:00:00Z",
|
||||
"updated_at": "2026-04-06T10:30:00Z",
|
||||
"sources_count": 5,
|
||||
"artifacts_count": 2,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sources_count: int = Field(
|
||||
0,
|
||||
ge=0,
|
||||
description="Number of sources in the notebook",
|
||||
examples=[5, 10, 0],
|
||||
)
|
||||
artifacts_count: int = Field(
|
||||
0,
|
||||
ge=0,
|
||||
description="Number of artifacts in the notebook",
|
||||
examples=[2, 5, 0],
|
||||
)
|
||||
|
||||
|
||||
class PaginationMeta(BaseModel):
|
||||
"""Pagination metadata.
|
||||
|
||||
Attributes:
|
||||
total: Total number of items available.
|
||||
limit: Maximum items per page.
|
||||
offset: Current offset.
|
||||
has_more: Whether more items are available.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"total": 100,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"has_more": True,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
total: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
description="Total number of items available",
|
||||
examples=[100, 50, 0],
|
||||
)
|
||||
limit: int = Field(
|
||||
...,
|
||||
ge=1,
|
||||
description="Maximum items per page",
|
||||
examples=[20, 50],
|
||||
)
|
||||
offset: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
description="Current offset",
|
||||
examples=[0, 20, 40],
|
||||
)
|
||||
has_more: bool = Field(
|
||||
...,
|
||||
description="Whether more items are available",
|
||||
examples=[True, False],
|
||||
)
|
||||
|
||||
|
||||
class PaginatedNotebooks(BaseModel):
|
||||
"""Paginated list of notebooks.
|
||||
|
||||
Attributes:
|
||||
items: List of notebooks.
|
||||
pagination: Pagination metadata.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"items": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "Notebook 1",
|
||||
"created_at": "2026-04-06T10:00:00Z",
|
||||
"updated_at": "2026-04-06T10:30:00Z",
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 100,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"has_more": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
items: list[Notebook] = Field(
|
||||
...,
|
||||
description="List of notebooks",
|
||||
)
|
||||
pagination: PaginationMeta = Field(
|
||||
...,
|
||||
description="Pagination metadata",
|
||||
)
|
||||
|
||||
|
||||
class Source(BaseModel):
|
||||
"""Source response model.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier (UUID).
|
||||
notebook_id: Parent notebook ID.
|
||||
type: Source type (url, file, youtube, drive).
|
||||
title: Source title.
|
||||
url: Source URL (if applicable).
|
||||
status: Processing status.
|
||||
created_at: Creation timestamp.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"notebook_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"type": "url",
|
||||
"title": "Example Article",
|
||||
"url": "https://example.com/article",
|
||||
"status": "ready",
|
||||
"created_at": "2026-04-06T10:00:00Z",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
id: UUID = Field(
|
||||
...,
|
||||
description="Unique identifier (UUID)",
|
||||
examples=["550e8400-e29b-41d4-a716-446655440001"],
|
||||
)
|
||||
notebook_id: UUID = Field(
|
||||
...,
|
||||
description="Parent notebook ID",
|
||||
examples=["550e8400-e29b-41d4-a716-446655440000"],
|
||||
)
|
||||
type: str = Field(
|
||||
...,
|
||||
description="Source type",
|
||||
examples=["url", "file", "youtube", "drive"],
|
||||
)
|
||||
title: str = Field(
|
||||
...,
|
||||
description="Source title",
|
||||
examples=["Example Article"],
|
||||
)
|
||||
url: str | None = Field(
|
||||
None,
|
||||
description="Source URL (if applicable)",
|
||||
examples=["https://example.com/article"],
|
||||
)
|
||||
status: str = Field(
|
||||
...,
|
||||
description="Processing status",
|
||||
examples=["processing", "ready", "error"],
|
||||
)
|
||||
created_at: datetime = Field(
|
||||
...,
|
||||
description="Creation timestamp",
|
||||
examples=["2026-04-06T10:00:00Z"],
|
||||
)
|
||||
|
||||
|
||||
class PaginatedSources(BaseModel):
|
||||
"""Paginated list of sources.
|
||||
|
||||
Attributes:
|
||||
items: List of sources.
|
||||
pagination: Pagination metadata.
|
||||
"""
|
||||
|
||||
items: list[Source] = Field(
|
||||
...,
|
||||
description="List of sources",
|
||||
)
|
||||
pagination: PaginationMeta = Field(
|
||||
...,
|
||||
description="Pagination metadata",
|
||||
)
|
||||
|
||||
|
||||
class HealthStatus(BaseModel):
|
||||
"""Health check response.
|
||||
|
||||
Attributes:
|
||||
status: Health status (healthy, degraded, unhealthy).
|
||||
timestamp: Check timestamp.
|
||||
version: API version.
|
||||
service: Service name.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"status": "healthy",
|
||||
"timestamp": "2026-04-06T10:30:00Z",
|
||||
"version": "0.1.0",
|
||||
"service": "notebooklm-agent-api",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
status: str = Field(
|
||||
...,
|
||||
description="Health status",
|
||||
examples=["healthy", "degraded", "unhealthy"],
|
||||
)
|
||||
timestamp: datetime = Field(
|
||||
...,
|
||||
description="Check timestamp",
|
||||
examples=["2026-04-06T10:30:00Z"],
|
||||
)
|
||||
version: str = Field(
|
||||
...,
|
||||
description="API version",
|
||||
examples=["0.1.0"],
|
||||
)
|
||||
service: str = Field(
|
||||
...,
|
||||
description="Service name",
|
||||
examples=["notebooklm-agent-api"],
|
||||
)
|
||||
1
src/notebooklm_agent/api/routes/__init__.py
Normal file
1
src/notebooklm_agent/api/routes/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for routes package
|
||||
53
src/notebooklm_agent/api/routes/health.py
Normal file
53
src/notebooklm_agent/api/routes/health.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Health check routes for NotebookLM Agent API."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=dict[str, Any])
|
||||
async def health_check():
|
||||
"""Basic health check endpoint.
|
||||
|
||||
Returns:
|
||||
Health status information.
|
||||
"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"service": "notebooklm-agent-api",
|
||||
"version": "0.1.0",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/ready", response_model=dict[str, Any])
|
||||
async def readiness_check():
|
||||
"""Readiness probe for Kubernetes/Container orchestration.
|
||||
|
||||
Returns:
|
||||
Readiness status information.
|
||||
"""
|
||||
# TODO: Add actual readiness checks (DB connection, external services, etc.)
|
||||
return {
|
||||
"status": "ready",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"checks": {
|
||||
"api": "ok",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/live", response_model=dict[str, Any])
|
||||
async def liveness_check():
|
||||
"""Liveness probe for Kubernetes/Container orchestration.
|
||||
|
||||
Returns:
|
||||
Liveness status information.
|
||||
"""
|
||||
return {
|
||||
"status": "alive",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
427
src/notebooklm_agent/api/routes/notebooks.py
Normal file
427
src/notebooklm_agent/api/routes/notebooks.py
Normal file
@@ -0,0 +1,427 @@
|
||||
"""Notebook API routes.
|
||||
|
||||
This module contains API endpoints for notebook management.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from notebooklm_agent.api.models.requests import NotebookCreate, NotebookListParams, NotebookUpdate
|
||||
from notebooklm_agent.api.models.responses import (
|
||||
ApiResponse,
|
||||
Notebook,
|
||||
PaginatedNotebooks,
|
||||
ResponseMeta,
|
||||
)
|
||||
from notebooklm_agent.core.exceptions import NotebookLMError, NotFoundError, ValidationError
|
||||
from notebooklm_agent.services.notebook_service import NotebookService
|
||||
|
||||
router = APIRouter(tags=["notebooks"])
|
||||
|
||||
|
||||
async def get_notebook_service() -> NotebookService:
|
||||
"""Get notebook service instance.
|
||||
|
||||
Returns:
|
||||
NotebookService instance.
|
||||
"""
|
||||
return NotebookService()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=ApiResponse[Notebook],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new notebook",
|
||||
description="Create a new notebook with title and optional description.",
|
||||
)
|
||||
async def create_notebook(data: NotebookCreate):
|
||||
"""Create a new notebook.
|
||||
|
||||
Args:
|
||||
data: Notebook creation data.
|
||||
|
||||
Returns:
|
||||
Created notebook.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 for validation errors, 502 for external API errors.
|
||||
"""
|
||||
try:
|
||||
service = await get_notebook_service()
|
||||
notebook = await service.create(data.model_dump())
|
||||
|
||||
return ApiResponse(
|
||||
success=True,
|
||||
data=notebook,
|
||||
error=None,
|
||||
meta=ResponseMeta(
|
||||
timestamp=datetime.utcnow(),
|
||||
request_id=uuid4(),
|
||||
),
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": e.details,
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
except NotebookLMError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=ApiResponse[PaginatedNotebooks],
|
||||
summary="List notebooks",
|
||||
description="List all notebooks with pagination, sorting, and ordering.",
|
||||
)
|
||||
async def list_notebooks(
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
sort: str = "created_at",
|
||||
order: str = "desc",
|
||||
):
|
||||
"""List notebooks with pagination.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of items to return (1-100).
|
||||
offset: Number of items to skip.
|
||||
sort: Field to sort by (created_at, updated_at, title).
|
||||
order: Sort order (asc, desc).
|
||||
|
||||
Returns:
|
||||
Paginated list of notebooks.
|
||||
|
||||
Raises:
|
||||
HTTPException: 422 for invalid parameters, 502 for external API errors.
|
||||
"""
|
||||
# Validate query parameters
|
||||
params = NotebookListParams(limit=limit, offset=offset, sort=sort, order=order)
|
||||
|
||||
try:
|
||||
service = await get_notebook_service()
|
||||
result = await service.list(
|
||||
limit=params.limit,
|
||||
offset=params.offset,
|
||||
sort=params.sort,
|
||||
order=params.order,
|
||||
)
|
||||
|
||||
return ApiResponse(
|
||||
success=True,
|
||||
data=result,
|
||||
error=None,
|
||||
meta=ResponseMeta(
|
||||
timestamp=datetime.utcnow(),
|
||||
request_id=uuid4(),
|
||||
),
|
||||
)
|
||||
except NotebookLMError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{notebook_id}",
|
||||
response_model=ApiResponse[Notebook],
|
||||
summary="Get notebook",
|
||||
description="Get a notebook by ID.",
|
||||
)
|
||||
async def get_notebook(notebook_id: str):
|
||||
"""Get a notebook by ID.
|
||||
|
||||
Args:
|
||||
notebook_id: Notebook UUID.
|
||||
|
||||
Returns:
|
||||
Notebook details.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 for invalid UUID, 404 for not found, 502 for external API errors.
|
||||
"""
|
||||
# Validate UUID format
|
||||
try:
|
||||
from uuid import UUID
|
||||
|
||||
notebook_uuid = UUID(notebook_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid notebook ID format",
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
service = await get_notebook_service()
|
||||
notebook = await service.get(notebook_uuid)
|
||||
|
||||
return ApiResponse(
|
||||
success=True,
|
||||
data=notebook,
|
||||
error=None,
|
||||
meta=ResponseMeta(
|
||||
timestamp=datetime.utcnow(),
|
||||
request_id=uuid4(),
|
||||
),
|
||||
)
|
||||
except NotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
except NotebookLMError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{notebook_id}",
|
||||
response_model=ApiResponse[Notebook],
|
||||
summary="Update notebook",
|
||||
description="Update a notebook (partial update).",
|
||||
)
|
||||
async def update_notebook(notebook_id: str, data: NotebookUpdate):
|
||||
"""Update a notebook (partial update).
|
||||
|
||||
Args:
|
||||
notebook_id: Notebook UUID.
|
||||
data: Update data (title and/or description).
|
||||
|
||||
Returns:
|
||||
Updated notebook.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 for invalid data, 404 for not found, 502 for external API errors.
|
||||
"""
|
||||
# Validate UUID format
|
||||
try:
|
||||
from uuid import UUID
|
||||
|
||||
notebook_uuid = UUID(notebook_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid notebook ID format",
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
service = await get_notebook_service()
|
||||
notebook = await service.update(notebook_uuid, data.model_dump(exclude_unset=True))
|
||||
|
||||
return ApiResponse(
|
||||
success=True,
|
||||
data=notebook,
|
||||
error=None,
|
||||
meta=ResponseMeta(
|
||||
timestamp=datetime.utcnow(),
|
||||
request_id=uuid4(),
|
||||
),
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": e.details,
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
except NotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
except NotebookLMError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{notebook_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete notebook",
|
||||
description="Delete a notebook.",
|
||||
)
|
||||
async def delete_notebook(notebook_id: str):
|
||||
"""Delete a notebook.
|
||||
|
||||
Args:
|
||||
notebook_id: Notebook UUID.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 for invalid UUID, 404 for not found, 502 for external API errors.
|
||||
"""
|
||||
# Validate UUID format
|
||||
try:
|
||||
from uuid import UUID
|
||||
|
||||
notebook_uuid = UUID(notebook_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Invalid notebook ID format",
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
service = await get_notebook_service()
|
||||
await service.delete(notebook_uuid)
|
||||
# 204 No Content - no body returned
|
||||
except NotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
except NotebookLMError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"details": [],
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
},
|
||||
)
|
||||
26
src/notebooklm_agent/core/__init__.py
Normal file
26
src/notebooklm_agent/core/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Core utilities for NotebookLM Agent API."""
|
||||
|
||||
from notebooklm_agent.core.config import Settings, get_settings
|
||||
from notebooklm_agent.core.exceptions import (
|
||||
AuthenticationError,
|
||||
NotebookLMAgentError,
|
||||
NotebookLMError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
ValidationError,
|
||||
WebhookError,
|
||||
)
|
||||
from notebooklm_agent.core.logging import setup_logging
|
||||
|
||||
__all__ = [
|
||||
"Settings",
|
||||
"get_settings",
|
||||
"setup_logging",
|
||||
"NotebookLMAgentError",
|
||||
"ValidationError",
|
||||
"AuthenticationError",
|
||||
"NotFoundError",
|
||||
"NotebookLMError",
|
||||
"RateLimitError",
|
||||
"WebhookError",
|
||||
]
|
||||
76
src/notebooklm_agent/core/config.py
Normal file
76
src/notebooklm_agent/core/config.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Core configuration for NotebookLM Agent API."""
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings loaded from environment variables."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
# API Configuration
|
||||
api_key: str = Field(default="", alias="NOTEBOOKLM_AGENT_API_KEY")
|
||||
webhook_secret: str = Field(default="", alias="NOTEBOOKLM_AGENT_WEBHOOK_SECRET")
|
||||
port: int = Field(default=8000, alias="NOTEBOOKLM_AGENT_PORT")
|
||||
host: str = Field(default="0.0.0.0", alias="NOTEBOOKLM_AGENT_HOST")
|
||||
reload: bool = Field(default=False, alias="NOTEBOOKLM_AGENT_RELOAD")
|
||||
|
||||
# NotebookLM Configuration
|
||||
notebooklm_home: str = Field(default="~/.notebooklm", alias="NOTEBOOKLM_HOME")
|
||||
notebooklm_profile: str = Field(default="default", alias="NOTEBOOKLM_PROFILE")
|
||||
|
||||
# Redis Configuration
|
||||
redis_url: str = Field(default="redis://localhost:6379/0", alias="REDIS_URL")
|
||||
|
||||
# Logging
|
||||
log_level: str = Field(default="INFO", alias="LOG_LEVEL")
|
||||
log_format: str = Field(default="json", alias="LOG_FORMAT")
|
||||
|
||||
# Development
|
||||
debug: bool = Field(default=False, alias="DEBUG")
|
||||
testing: bool = Field(default=False, alias="TESTING")
|
||||
|
||||
# Security
|
||||
cors_origins: list[str] = Field(default_factory=list, alias="CORS_ORIGINS")
|
||||
|
||||
@field_validator("cors_origins", mode="before")
|
||||
@classmethod
|
||||
def parse_cors_origins(cls, v: Any) -> list[str]:
|
||||
"""Parse CORS origins from string or list."""
|
||||
if isinstance(v, str):
|
||||
return [origin.strip() for origin in v.split(",") if origin.strip()]
|
||||
return v if v else []
|
||||
|
||||
@field_validator("log_level")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
"""Validate log level is one of the allowed values."""
|
||||
allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
v_upper = v.upper()
|
||||
if v_upper not in allowed:
|
||||
raise ValueError(f"log_level must be one of {allowed}")
|
||||
return v_upper
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
"""Check if running in production mode."""
|
||||
return not self.debug and not self.testing
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""Get cached settings instance.
|
||||
|
||||
Returns:
|
||||
Settings instance loaded from environment.
|
||||
"""
|
||||
return Settings()
|
||||
59
src/notebooklm_agent/core/exceptions.py
Normal file
59
src/notebooklm_agent/core/exceptions.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Core exceptions for NotebookLM Agent API."""
|
||||
|
||||
|
||||
class NotebookLMAgentError(Exception):
|
||||
"""Base exception for all NotebookLM Agent errors."""
|
||||
|
||||
def __init__(self, message: str, code: str = "AGENT_ERROR") -> None:
|
||||
self.message = message
|
||||
self.code = code
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class ValidationError(NotebookLMAgentError):
|
||||
"""Raised when input validation fails."""
|
||||
|
||||
def __init__(self, message: str, details: list[dict] | None = None) -> None:
|
||||
super().__init__(message, "VALIDATION_ERROR")
|
||||
self.details = details or []
|
||||
|
||||
|
||||
class AuthenticationError(NotebookLMAgentError):
|
||||
"""Raised when authentication fails."""
|
||||
|
||||
def __init__(self, message: str = "Authentication failed") -> None:
|
||||
super().__init__(message, "AUTH_ERROR")
|
||||
|
||||
|
||||
class NotFoundError(NotebookLMAgentError):
|
||||
"""Raised when a requested resource is not found."""
|
||||
|
||||
def __init__(self, resource: str, resource_id: str) -> None:
|
||||
message = f"{resource} with id '{resource_id}' not found"
|
||||
super().__init__(message, "NOT_FOUND")
|
||||
self.resource = resource
|
||||
self.resource_id = resource_id
|
||||
|
||||
|
||||
class NotebookLMError(NotebookLMAgentError):
|
||||
"""Raised when NotebookLM API returns an error."""
|
||||
|
||||
def __init__(self, message: str, original_error: Exception | None = None) -> None:
|
||||
super().__init__(message, "NOTEBOOKLM_ERROR")
|
||||
self.original_error = original_error
|
||||
|
||||
|
||||
class RateLimitError(NotebookLMAgentError):
|
||||
"""Raised when rate limit is exceeded."""
|
||||
|
||||
def __init__(self, message: str = "Rate limit exceeded", retry_after: int | None = None) -> None:
|
||||
super().__init__(message, "RATE_LIMITED")
|
||||
self.retry_after = retry_after
|
||||
|
||||
|
||||
class WebhookError(NotebookLMAgentError):
|
||||
"""Raised when webhook operation fails."""
|
||||
|
||||
def __init__(self, message: str, webhook_id: str | None = None) -> None:
|
||||
super().__init__(message, "WEBHOOK_ERROR")
|
||||
self.webhook_id = webhook_id
|
||||
49
src/notebooklm_agent/core/logging.py
Normal file
49
src/notebooklm_agent/core/logging.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Logging configuration for NotebookLM Agent API."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from notebooklm_agent.core.config import Settings, get_settings
|
||||
|
||||
|
||||
def setup_logging(settings: Settings | None = None) -> None:
|
||||
"""Configure structured logging.
|
||||
|
||||
Args:
|
||||
settings: Application settings. If None, loads from environment.
|
||||
"""
|
||||
if settings is None:
|
||||
settings = get_settings()
|
||||
|
||||
# Configure structlog
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
structlog.processors.UnicodeDecoder(),
|
||||
structlog.processors.JSONRenderer() if settings.log_format == "json" else structlog.dev.ConsoleRenderer(),
|
||||
],
|
||||
context_class=dict,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
# Configure standard library logging
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
stream=sys.stdout,
|
||||
level=getattr(logging, settings.log_level),
|
||||
)
|
||||
|
||||
# Set third-party loggers to WARNING to reduce noise
|
||||
logging.getLogger("uvicorn").setLevel(logging.WARNING)
|
||||
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
||||
8
src/notebooklm_agent/services/__init__.py
Normal file
8
src/notebooklm_agent/services/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Services for NotebookLM Agent API.
|
||||
|
||||
This package contains business logic services.
|
||||
"""
|
||||
|
||||
from notebooklm_agent.services.notebook_service import NotebookService
|
||||
|
||||
__all__ = ["NotebookService"]
|
||||
240
src/notebooklm_agent/services/notebook_service.py
Normal file
240
src/notebooklm_agent/services/notebook_service.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Notebook service for business logic.
|
||||
|
||||
This module contains the NotebookService class which handles
|
||||
all business logic for notebook operations.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from notebooklm_agent.api.models.requests import NotebookCreate, NotebookUpdate
|
||||
from notebooklm_agent.api.models.responses import Notebook, PaginatedNotebooks, PaginationMeta
|
||||
from notebooklm_agent.core.exceptions import NotebookLMError, NotFoundError, ValidationError
|
||||
|
||||
|
||||
class NotebookService:
|
||||
"""Service for notebook operations.
|
||||
|
||||
This service handles all business logic for notebook CRUD operations,
|
||||
including validation, error handling, and integration with notebooklm-py.
|
||||
|
||||
Attributes:
|
||||
_client: The notebooklm-py client instance.
|
||||
"""
|
||||
|
||||
def __init__(self, client: Any = None) -> None:
|
||||
"""Initialize the notebook service.
|
||||
|
||||
Args:
|
||||
client: Optional notebooklm-py client instance.
|
||||
If not provided, will be created on first use.
|
||||
"""
|
||||
self._client = client
|
||||
|
||||
async def _get_client(self) -> Any:
|
||||
"""Get or create notebooklm-py client.
|
||||
|
||||
Returns:
|
||||
The notebooklm-py client instance.
|
||||
"""
|
||||
if self._client is None:
|
||||
# Lazy initialization - import here to avoid circular imports
|
||||
from notebooklm import NotebookLMClient
|
||||
|
||||
self._client = await NotebookLMClient.from_storage()
|
||||
return self._client
|
||||
|
||||
def _validate_title(self, title: str | None) -> str:
|
||||
"""Validate notebook title.
|
||||
|
||||
Args:
|
||||
title: The title to validate.
|
||||
|
||||
Returns:
|
||||
The validated title.
|
||||
|
||||
Raises:
|
||||
ValidationError: If title is invalid.
|
||||
"""
|
||||
if title is None:
|
||||
raise ValidationError("Title is required")
|
||||
|
||||
title = title.strip()
|
||||
if not title:
|
||||
raise ValidationError("Title cannot be empty")
|
||||
|
||||
if len(title) < 3:
|
||||
raise ValidationError("Title must be at least 3 characters")
|
||||
|
||||
if len(title) > 100:
|
||||
raise ValidationError("Title must be at most 100 characters")
|
||||
|
||||
return title
|
||||
|
||||
def _to_notebook_model(self, response: Any) -> Notebook:
|
||||
"""Convert notebooklm-py response to Notebook model.
|
||||
|
||||
Args:
|
||||
response: The notebooklm-py response object.
|
||||
|
||||
Returns:
|
||||
Notebook model instance.
|
||||
"""
|
||||
return Notebook(
|
||||
id=response.id,
|
||||
title=response.title,
|
||||
description=getattr(response, "description", None),
|
||||
created_at=getattr(response, "created_at", datetime.utcnow()),
|
||||
updated_at=getattr(response, "updated_at", datetime.utcnow()),
|
||||
)
|
||||
|
||||
async def create(self, data: dict[str, Any]) -> Notebook:
|
||||
"""Create a new notebook.
|
||||
|
||||
Args:
|
||||
data: Dictionary with title and optional description.
|
||||
|
||||
Returns:
|
||||
The created notebook.
|
||||
|
||||
Raises:
|
||||
ValidationError: If input data is invalid.
|
||||
NotebookLMError: If external API call fails.
|
||||
"""
|
||||
# Validate title
|
||||
title = self._validate_title(data.get("title"))
|
||||
|
||||
try:
|
||||
client = await self._get_client()
|
||||
response = await client.notebooks.create(title)
|
||||
return self._to_notebook_model(response)
|
||||
except Exception as e:
|
||||
raise NotebookLMError(f"Failed to create notebook: {e}") from e
|
||||
|
||||
async def list(
|
||||
self,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
sort: str = "created_at",
|
||||
order: str = "desc",
|
||||
) -> PaginatedNotebooks:
|
||||
"""List notebooks with pagination.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of items to return (1-100).
|
||||
offset: Number of items to skip.
|
||||
sort: Field to sort by (created_at, updated_at, title).
|
||||
order: Sort order (asc, desc).
|
||||
|
||||
Returns:
|
||||
Paginated list of notebooks.
|
||||
|
||||
Raises:
|
||||
NotebookLMError: If external API call fails.
|
||||
"""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
responses = await client.notebooks.list()
|
||||
|
||||
# Convert to models
|
||||
notebooks = [self._to_notebook_model(r) for r in responses]
|
||||
|
||||
# Apply pagination
|
||||
total = len(notebooks)
|
||||
paginated = notebooks[offset : offset + limit]
|
||||
|
||||
return PaginatedNotebooks(
|
||||
items=paginated,
|
||||
pagination=PaginationMeta(
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
has_more=(offset + limit) < total,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
raise NotebookLMError(f"Failed to list notebooks: {e}") from e
|
||||
|
||||
async def get(self, notebook_id: UUID) -> Notebook:
|
||||
"""Get a notebook by ID.
|
||||
|
||||
Args:
|
||||
notebook_id: The notebook UUID.
|
||||
|
||||
Returns:
|
||||
The notebook.
|
||||
|
||||
Raises:
|
||||
NotFoundError: If notebook doesn't exist.
|
||||
NotebookLMError: If external API call fails.
|
||||
"""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
response = await client.notebooks.get(str(notebook_id))
|
||||
return self._to_notebook_model(response)
|
||||
except Exception as e:
|
||||
if "not found" in str(e).lower():
|
||||
raise NotFoundError("Notebook", str(notebook_id)) from e
|
||||
raise NotebookLMError(f"Failed to get notebook: {e}") from e
|
||||
|
||||
async def update(self, notebook_id: UUID, data: dict[str, Any]) -> Notebook:
|
||||
"""Update a notebook.
|
||||
|
||||
Args:
|
||||
notebook_id: The notebook UUID.
|
||||
data: Dictionary with optional title and description.
|
||||
|
||||
Returns:
|
||||
The updated notebook.
|
||||
|
||||
Raises:
|
||||
ValidationError: If input data is invalid.
|
||||
NotFoundError: If notebook doesn't exist.
|
||||
NotebookLMError: If external API call fails.
|
||||
"""
|
||||
# Validate title if provided
|
||||
title = data.get("title")
|
||||
if title is not None:
|
||||
title = self._validate_title(title)
|
||||
|
||||
try:
|
||||
client = await self._get_client()
|
||||
|
||||
# Build update kwargs
|
||||
kwargs = {}
|
||||
if title is not None:
|
||||
kwargs["title"] = title
|
||||
if data.get("description") is not None:
|
||||
kwargs["description"] = data["description"]
|
||||
|
||||
if not kwargs:
|
||||
# No fields to update, just get current
|
||||
return await self.get(notebook_id)
|
||||
|
||||
response = await client.notebooks.update(str(notebook_id), **kwargs)
|
||||
return self._to_notebook_model(response)
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if "not found" in str(e).lower():
|
||||
raise NotFoundError("Notebook", str(notebook_id)) from e
|
||||
raise NotebookLMError(f"Failed to update notebook: {e}") from e
|
||||
|
||||
async def delete(self, notebook_id: UUID) -> None:
|
||||
"""Delete a notebook.
|
||||
|
||||
Args:
|
||||
notebook_id: The notebook UUID.
|
||||
|
||||
Raises:
|
||||
NotFoundError: If notebook doesn't exist.
|
||||
NotebookLMError: If external API call fails.
|
||||
"""
|
||||
try:
|
||||
client = await self._get_client()
|
||||
await client.notebooks.delete(str(notebook_id))
|
||||
except Exception as e:
|
||||
if "not found" in str(e).lower():
|
||||
raise NotFoundError("Notebook", str(notebook_id)) from e
|
||||
raise NotebookLMError(f"Failed to delete notebook: {e}") from e
|
||||
1
src/notebooklm_agent/skill/__init__.py
Normal file
1
src/notebooklm_agent/skill/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for skill package
|
||||
1
src/notebooklm_agent/webhooks/__init__.py
Normal file
1
src/notebooklm_agent/webhooks/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for webhooks package
|
||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for tests package
|
||||
1
tests/e2e/__init__.py
Normal file
1
tests/e2e/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for e2e tests package
|
||||
22
tests/e2e/test_workflows/test_full_workflow.py
Normal file
22
tests/e2e/test_workflows/test_full_workflow.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""E2E tests placeholder.
|
||||
|
||||
These tests will verify complete workflows with real NotebookLM API.
|
||||
Requires authentication and should be run manually.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestFullWorkflow:
|
||||
"""End-to-end workflow tests."""
|
||||
|
||||
async def test_research_to_podcast_workflow(self):
|
||||
"""Should complete full research to podcast workflow."""
|
||||
# TODO: Implement E2E test
|
||||
# 1. Create notebook
|
||||
# 2. Add sources
|
||||
# 3. Generate audio
|
||||
# 4. Wait for completion
|
||||
# 5. Download artifact
|
||||
pytest.skip("E2E tests require NotebookLM authentication")
|
||||
1
tests/integration/__init__.py
Normal file
1
tests/integration/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for integration tests package
|
||||
31
tests/integration/test_api/test_notebooks.py
Normal file
31
tests/integration/test_api/test_notebooks.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Integration tests placeholder.
|
||||
|
||||
These tests will verify the API endpoints with mocked external dependencies.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestApiHealth:
|
||||
"""Health check endpoint tests."""
|
||||
|
||||
async def test_health_endpoint_returns_ok(self):
|
||||
"""Should return OK status for health check."""
|
||||
# TODO: Implement when API is ready
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestNotebooksApi:
|
||||
"""Notebook API endpoint tests."""
|
||||
|
||||
async def test_create_notebook(self):
|
||||
"""Should create notebook via API."""
|
||||
# TODO: Implement when API is ready
|
||||
pass
|
||||
|
||||
async def test_list_notebooks(self):
|
||||
"""Should list notebooks via API."""
|
||||
# TODO: Implement when API is ready
|
||||
pass
|
||||
1
tests/unit/__init__.py
Normal file
1
tests/unit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for unit tests package
|
||||
1
tests/unit/test_api/__init__.py
Normal file
1
tests/unit/test_api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder for API tests package
|
||||
56
tests/unit/test_api/test_health.py
Normal file
56
tests/unit/test_api/test_health.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Tests for health routes."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from notebooklm_agent.api.main import app
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestHealthEndpoints:
|
||||
"""Test suite for health check endpoints."""
|
||||
|
||||
def test_health_check_returns_healthy(self):
|
||||
"""Should return healthy status."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.get("/health/")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
assert "timestamp" in data
|
||||
assert data["service"] == "notebooklm-agent-api"
|
||||
assert data["version"] == "0.1.0"
|
||||
|
||||
def test_readiness_check_returns_ready(self):
|
||||
"""Should return ready status."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.get("/health/ready")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "ready"
|
||||
assert "timestamp" in data
|
||||
assert "checks" in data
|
||||
|
||||
def test_liveness_check_returns_alive(self):
|
||||
"""Should return alive status."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.get("/health/live")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "alive"
|
||||
assert "timestamp" in data
|
||||
51
tests/unit/test_api/test_main.py
Normal file
51
tests/unit/test_api/test_main.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Tests for API main module."""
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from notebooklm_agent.api.main import app, create_application
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCreateApplication:
|
||||
"""Test suite for create_application function."""
|
||||
|
||||
def test_returns_fastapi_instance(self):
|
||||
"""Should return FastAPI application instance."""
|
||||
# Act
|
||||
application = create_application()
|
||||
|
||||
# Assert
|
||||
assert isinstance(application, FastAPI)
|
||||
assert application.title == "NotebookLM Agent API"
|
||||
assert application.version == "0.1.0"
|
||||
|
||||
def test_includes_health_router(self):
|
||||
"""Should include health check router."""
|
||||
# Act
|
||||
application = create_application()
|
||||
|
||||
# Assert
|
||||
routes = [route.path for route in application.routes]
|
||||
assert any("/health" in route for route in routes)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestRootEndpoint:
|
||||
"""Test suite for root endpoint."""
|
||||
|
||||
def test_root_returns_api_info(self):
|
||||
"""Should return API information."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.get("/")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "NotebookLM Agent API"
|
||||
assert data["version"] == "0.1.0"
|
||||
assert "/docs" in data["documentation"]
|
||||
639
tests/unit/test_api/test_notebooks.py
Normal file
639
tests/unit/test_api/test_notebooks.py
Normal file
@@ -0,0 +1,639 @@
|
||||
"""Tests for notebooks API routes.
|
||||
|
||||
TDD for DEV-002: POST /api/v1/notebooks
|
||||
TDD for DEV-003: GET /api/v1/notebooks
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from notebooklm_agent.api.main import app
|
||||
from notebooklm_agent.core.exceptions import ValidationError
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCreateNotebookEndpoint:
|
||||
"""Test suite for POST /api/v1/notebooks endpoint."""
|
||||
|
||||
def test_create_notebook_valid_data_returns_201(self):
|
||||
"""Should return 201 Created for valid notebook data."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.create.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.post(
|
||||
"/api/v1/notebooks",
|
||||
json={"title": "Test Notebook", "description": None},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["title"] == "Test Notebook"
|
||||
assert data["data"]["id"] == notebook_id
|
||||
|
||||
def test_create_notebook_with_description_returns_201(self):
|
||||
"""Should return 201 for notebook with description."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = "A description"
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.create.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.post(
|
||||
"/api/v1/notebooks",
|
||||
json={"title": "Test Notebook", "description": "A description"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["data"]["description"] == "A description"
|
||||
|
||||
def test_create_notebook_short_title_returns_400(self):
|
||||
"""Should return 400 for title too short."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_service.create.side_effect = ValidationError("Title must be at least 3 characters")
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.post(
|
||||
"/api/v1/notebooks",
|
||||
json={"title": "AB", "description": None},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error"]["code"] == "VALIDATION_ERROR"
|
||||
|
||||
def test_create_notebook_missing_title_returns_422(self):
|
||||
"""Should return 422 for missing title (Pydantic validation)."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.post(
|
||||
"/api/v1/notebooks",
|
||||
json={"description": "A description"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert "detail" in data
|
||||
|
||||
def test_create_notebook_empty_title_returns_400(self):
|
||||
"""Should return 400 for empty title."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_service.create.side_effect = ValidationError("Title cannot be empty")
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.post(
|
||||
"/api/v1/notebooks",
|
||||
json={"title": "", "description": None},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
|
||||
def test_create_notebook_response_has_meta(self):
|
||||
"""Should include meta in response."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.create.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.post(
|
||||
"/api/v1/notebooks",
|
||||
json={"title": "Test Notebook", "description": None},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "meta" in data
|
||||
assert "timestamp" in data["meta"]
|
||||
assert "request_id" in data["meta"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestListNotebooksEndpoint:
|
||||
"""Test suite for GET /api/v1/notebooks endpoint."""
|
||||
|
||||
def test_list_notebooks_returns_200(self):
|
||||
"""Should return 200 with list of notebooks."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_notebook = MagicMock()
|
||||
mock_notebook.id = str(uuid4())
|
||||
mock_notebook.title = "Notebook 1"
|
||||
mock_notebook.description = None
|
||||
mock_notebook.created_at = datetime.utcnow()
|
||||
mock_notebook.updated_at = datetime.utcnow()
|
||||
|
||||
mock_paginated = MagicMock()
|
||||
mock_paginated.items = [mock_notebook]
|
||||
mock_paginated.pagination.total = 1
|
||||
mock_paginated.pagination.limit = 20
|
||||
mock_paginated.pagination.offset = 0
|
||||
mock_paginated.pagination.has_more = False
|
||||
|
||||
mock_service.list.return_value = mock_paginated
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.get("/api/v1/notebooks")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]["items"]) == 1
|
||||
assert data["data"]["pagination"]["total"] == 1
|
||||
|
||||
def test_list_notebooks_with_pagination_returns_correct_page(self):
|
||||
"""Should return correct page with limit and offset."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_paginated = MagicMock()
|
||||
mock_paginated.items = []
|
||||
mock_paginated.pagination.total = 100
|
||||
mock_paginated.pagination.limit = 10
|
||||
mock_paginated.pagination.offset = 20
|
||||
mock_paginated.pagination.has_more = True
|
||||
|
||||
mock_service.list.return_value = mock_paginated
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.get("/api/v1/notebooks?limit=10&offset=20")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["pagination"]["limit"] == 10
|
||||
assert data["data"]["pagination"]["offset"] == 20
|
||||
assert data["data"]["pagination"]["has_more"] is True
|
||||
|
||||
def test_list_notebooks_with_sort_returns_sorted(self):
|
||||
"""Should sort notebooks by specified field."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_paginated = MagicMock()
|
||||
mock_paginated.items = []
|
||||
mock_paginated.pagination.total = 0
|
||||
mock_paginated.pagination.limit = 20
|
||||
mock_paginated.pagination.offset = 0
|
||||
mock_paginated.pagination.has_more = False
|
||||
|
||||
mock_service.list.return_value = mock_paginated
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.get("/api/v1/notebooks?sort=title&order=asc")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
mock_service.list.assert_called_once_with(limit=20, offset=0, sort="title", order="asc")
|
||||
|
||||
def test_list_notebooks_empty_list_returns_200(self):
|
||||
"""Should return 200 with empty list when no notebooks."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_paginated = MagicMock()
|
||||
mock_paginated.items = []
|
||||
mock_paginated.pagination.total = 0
|
||||
mock_paginated.pagination.limit = 20
|
||||
mock_paginated.pagination.offset = 0
|
||||
mock_paginated.pagination.has_more = False
|
||||
|
||||
mock_service.list.return_value = mock_paginated
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.get("/api/v1/notebooks")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["items"] == []
|
||||
assert data["data"]["pagination"]["total"] == 0
|
||||
|
||||
def test_list_notebooks_invalid_limit_returns_422(self):
|
||||
"""Should return 422 for invalid limit parameter."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.get("/api/v1/notebooks?limit=200")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_list_notebooks_invalid_sort_returns_422(self):
|
||||
"""Should return 422 for invalid sort parameter."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.get("/api/v1/notebooks?sort=invalid_field")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetNotebookEndpoint:
|
||||
"""Test suite for GET /api/v1/notebooks/{id} endpoint."""
|
||||
|
||||
def test_get_notebook_existing_id_returns_200(self):
|
||||
"""Should return 200 with notebook for existing ID."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = "A description"
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.get.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.get(f"/api/v1/notebooks/{notebook_id}")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["id"] == notebook_id
|
||||
assert data["data"]["title"] == "Test Notebook"
|
||||
|
||||
def test_get_notebook_nonexistent_id_returns_404(self):
|
||||
"""Should return 404 for non-existent notebook ID."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
from notebooklm_agent.core.exceptions import NotFoundError
|
||||
|
||||
mock_service.get.side_effect = NotFoundError("Notebook", notebook_id)
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.get(f"/api/v1/notebooks/{notebook_id}")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error"]["code"] == "NOT_FOUND"
|
||||
|
||||
def test_get_notebook_invalid_uuid_returns_400(self):
|
||||
"""Should return 400 for invalid UUID format."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.get("/api/v1/notebooks/invalid-uuid")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_get_notebook_response_has_meta(self):
|
||||
"""Should include meta in response."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.get.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.get(f"/api/v1/notebooks/{notebook_id}")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "meta" in data
|
||||
assert "timestamp" in data["meta"]
|
||||
assert "request_id" in data["meta"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDeleteNotebookEndpoint:
|
||||
"""Test suite for DELETE /api/v1/notebooks/{id} endpoint."""
|
||||
|
||||
def test_delete_notebook_valid_id_returns_204(self):
|
||||
"""Should return 204 No Content for valid notebook ID."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_service.delete.return_value = None
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.delete(f"/api/v1/notebooks/{notebook_id}")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 204
|
||||
assert response.content == b""
|
||||
|
||||
def test_delete_notebook_nonexistent_id_returns_404(self):
|
||||
"""Should return 404 for non-existent notebook ID."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
from notebooklm_agent.core.exceptions import NotFoundError
|
||||
|
||||
mock_service.delete.side_effect = NotFoundError("Notebook", notebook_id)
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.delete(f"/api/v1/notebooks/{notebook_id}")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert data["detail"]["success"] is False
|
||||
assert data["detail"]["error"]["code"] == "NOT_FOUND"
|
||||
|
||||
def test_delete_notebook_invalid_uuid_returns_400(self):
|
||||
"""Should return 400 for invalid UUID format."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.delete("/api/v1/notebooks/invalid-uuid")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["detail"]["success"] is False
|
||||
assert data["detail"]["error"]["code"] == "VALIDATION_ERROR"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUpdateNotebookEndpoint:
|
||||
"""Test suite for PATCH /api/v1/notebooks/{id} endpoint."""
|
||||
|
||||
def test_update_notebook_title_returns_200(self):
|
||||
"""Should return 200 with updated notebook when updating title."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Updated Title"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.update.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.patch(
|
||||
f"/api/v1/notebooks/{notebook_id}",
|
||||
json={"title": "Updated Title"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["title"] == "Updated Title"
|
||||
|
||||
def test_update_notebook_description_only_returns_200(self):
|
||||
"""Should return 200 when updating only description."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Original Title"
|
||||
mock_response.description = "New Description"
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.update.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.patch(
|
||||
f"/api/v1/notebooks/{notebook_id}",
|
||||
json={"description": "New Description"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["description"] == "New Description"
|
||||
|
||||
def test_update_notebook_both_fields_returns_200(self):
|
||||
"""Should return 200 when updating both title and description."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Updated Title"
|
||||
mock_response.description = "Updated Description"
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.update.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.patch(
|
||||
f"/api/v1/notebooks/{notebook_id}",
|
||||
json={"title": "Updated Title", "description": "Updated Description"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["title"] == "Updated Title"
|
||||
assert data["data"]["description"] == "Updated Description"
|
||||
|
||||
def test_update_notebook_nonexistent_id_returns_404(self):
|
||||
"""Should return 404 for non-existent notebook ID."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
from notebooklm_agent.core.exceptions import NotFoundError
|
||||
|
||||
mock_service.update.side_effect = NotFoundError("Notebook", notebook_id)
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.patch(
|
||||
f"/api/v1/notebooks/{notebook_id}",
|
||||
json={"title": "Updated Title"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error"]["code"] == "NOT_FOUND"
|
||||
|
||||
def test_update_notebook_invalid_title_returns_400(self):
|
||||
"""Should return 400 for invalid title."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
from notebooklm_agent.core.exceptions import ValidationError
|
||||
|
||||
mock_service.update.side_effect = ValidationError("Title too short")
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.patch(
|
||||
f"/api/v1/notebooks/{notebook_id}",
|
||||
json={"title": "AB"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert data["error"]["code"] == "VALIDATION_ERROR"
|
||||
|
||||
def test_update_notebook_invalid_uuid_returns_400(self):
|
||||
"""Should return 400 for invalid UUID format."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
|
||||
# Act
|
||||
response = client.patch(
|
||||
"/api/v1/notebooks/invalid-uuid",
|
||||
json={"title": "Updated Title"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_update_notebook_empty_body_returns_200(self):
|
||||
"""Should return 200 with unchanged notebook for empty body."""
|
||||
# Arrange
|
||||
client = TestClient(app)
|
||||
notebook_id = str(uuid4())
|
||||
|
||||
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
||||
mock_service = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = notebook_id
|
||||
mock_response.title = "Original Title"
|
||||
mock_response.description = "Original Description"
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_service.update.return_value = mock_response
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Act
|
||||
response = client.patch(
|
||||
f"/api/v1/notebooks/{notebook_id}",
|
||||
json={},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
102
tests/unit/test_core/test_config.py
Normal file
102
tests/unit/test_core/test_config.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Tests for core configuration."""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from notebooklm_agent.core.config import Settings, get_settings
|
||||
from notebooklm_agent.core.exceptions import NotebookLMAgentError, ValidationError
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSettings:
|
||||
"""Test suite for Settings configuration."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Should create settings with default values."""
|
||||
# Arrange & Act
|
||||
settings = Settings()
|
||||
|
||||
# Assert
|
||||
assert settings.port == 8000
|
||||
assert settings.host == "0.0.0.0"
|
||||
assert settings.log_level == "INFO"
|
||||
assert settings.debug is False
|
||||
assert settings.testing is False
|
||||
|
||||
def test_custom_values_from_env(self, monkeypatch):
|
||||
"""Should load custom values from environment variables."""
|
||||
# Arrange
|
||||
monkeypatch.setenv("NOTEBOOKLM_AGENT_PORT", "9000")
|
||||
monkeypatch.setenv("NOTEBOOKLM_AGENT_HOST", "127.0.0.1")
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
monkeypatch.setenv("DEBUG", "true")
|
||||
|
||||
# Act
|
||||
settings = Settings()
|
||||
|
||||
# Assert
|
||||
assert settings.port == 9000
|
||||
assert settings.host == "127.0.0.1"
|
||||
assert settings.log_level == "DEBUG"
|
||||
assert settings.debug is True
|
||||
|
||||
def test_cors_origins_parsing_from_string(self):
|
||||
"""Should parse CORS origins from comma-separated string."""
|
||||
# Arrange & Act
|
||||
settings = Settings(cors_origins="http://localhost:3000, http://localhost:8080")
|
||||
|
||||
# Assert
|
||||
assert settings.cors_origins == ["http://localhost:3000", "http://localhost:8080"]
|
||||
|
||||
def test_cors_origins_from_list(self):
|
||||
"""Should accept CORS origins as list."""
|
||||
# Arrange & Act
|
||||
origins = ["http://localhost:3000", "http://localhost:8080"]
|
||||
settings = Settings(cors_origins=origins)
|
||||
|
||||
# Assert
|
||||
assert settings.cors_origins == origins
|
||||
|
||||
def test_log_level_validation_valid(self):
|
||||
"""Should accept valid log levels."""
|
||||
# Arrange & Act & Assert
|
||||
for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
||||
settings = Settings(log_level=level)
|
||||
assert settings.log_level == level
|
||||
|
||||
def test_log_level_validation_invalid(self):
|
||||
"""Should reject invalid log levels."""
|
||||
# Arrange & Act & Assert
|
||||
with pytest.raises(ValueError, match="log_level must be one of"):
|
||||
Settings(log_level="INVALID")
|
||||
|
||||
def test_is_production_property(self):
|
||||
"""Should correctly identify production mode."""
|
||||
# Arrange & Act & Assert
|
||||
assert Settings(debug=False, testing=False).is_production is True
|
||||
assert Settings(debug=True, testing=False).is_production is False
|
||||
assert Settings(debug=False, testing=True).is_production is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetSettings:
|
||||
"""Test suite for get_settings function."""
|
||||
|
||||
def test_returns_settings_instance(self):
|
||||
"""Should return a Settings instance."""
|
||||
# Arrange & Act
|
||||
settings = get_settings()
|
||||
|
||||
# Assert
|
||||
assert isinstance(settings, Settings)
|
||||
|
||||
def test_caching(self):
|
||||
"""Should cache settings instance."""
|
||||
# Arrange & Act
|
||||
settings1 = get_settings()
|
||||
settings2 = get_settings()
|
||||
|
||||
# Assert - same instance due to lru_cache
|
||||
assert settings1 is settings2
|
||||
161
tests/unit/test_core/test_exceptions.py
Normal file
161
tests/unit/test_core/test_exceptions.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Tests for core exceptions."""
|
||||
|
||||
import pytest
|
||||
|
||||
from notebooklm_agent.core.exceptions import (
|
||||
AuthenticationError,
|
||||
NotebookLMAgentError,
|
||||
NotebookLMError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
ValidationError,
|
||||
WebhookError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookLMAgentError:
|
||||
"""Test suite for base exception."""
|
||||
|
||||
def test_default_code(self):
|
||||
"""Should have default error code."""
|
||||
# Arrange & Act
|
||||
error = NotebookLMAgentError("Test message")
|
||||
|
||||
# Assert
|
||||
assert error.code == "AGENT_ERROR"
|
||||
assert str(error) == "Test message"
|
||||
|
||||
def test_custom_code(self):
|
||||
"""Should accept custom error code."""
|
||||
# Arrange & Act
|
||||
error = NotebookLMAgentError("Test message", code="CUSTOM_ERROR")
|
||||
|
||||
# Assert
|
||||
assert error.code == "CUSTOM_ERROR"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestValidationError:
|
||||
"""Test suite for ValidationError."""
|
||||
|
||||
def test_default_details(self):
|
||||
"""Should have empty details by default."""
|
||||
# Arrange & Act
|
||||
error = ValidationError("Validation failed")
|
||||
|
||||
# Assert
|
||||
assert error.code == "VALIDATION_ERROR"
|
||||
assert error.details == []
|
||||
|
||||
def test_with_details(self):
|
||||
"""Should store validation details."""
|
||||
# Arrange
|
||||
details = [{"field": "title", "error": "Required"}]
|
||||
|
||||
# Act
|
||||
error = ValidationError("Validation failed", details=details)
|
||||
|
||||
# Assert
|
||||
assert error.details == details
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAuthenticationError:
|
||||
"""Test suite for AuthenticationError."""
|
||||
|
||||
def test_default_message(self):
|
||||
"""Should have default error message."""
|
||||
# Arrange & Act
|
||||
error = AuthenticationError()
|
||||
|
||||
# Assert
|
||||
assert error.code == "AUTH_ERROR"
|
||||
assert str(error) == "Authentication failed"
|
||||
|
||||
def test_custom_message(self):
|
||||
"""Should accept custom message."""
|
||||
# Arrange & Act
|
||||
error = AuthenticationError("Custom auth error")
|
||||
|
||||
# Assert
|
||||
assert str(error) == "Custom auth error"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotFoundError:
|
||||
"""Test suite for NotFoundError."""
|
||||
|
||||
def test_message_formatting(self):
|
||||
"""Should format message with resource info."""
|
||||
# Arrange & Act
|
||||
error = NotFoundError("Notebook", "abc123")
|
||||
|
||||
# Assert
|
||||
assert error.code == "NOT_FOUND"
|
||||
assert error.resource == "Notebook"
|
||||
assert error.resource_id == "abc123"
|
||||
assert "Notebook" in str(error)
|
||||
assert "abc123" in str(error)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookLMError:
|
||||
"""Test suite for NotebookLMError."""
|
||||
|
||||
def test_stores_original_error(self):
|
||||
"""Should store original exception."""
|
||||
# Arrange
|
||||
original = ValueError("Original error")
|
||||
|
||||
# Act
|
||||
error = NotebookLMError("NotebookLM failed", original_error=original)
|
||||
|
||||
# Assert
|
||||
assert error.code == "NOTEBOOKLM_ERROR"
|
||||
assert error.original_error is original
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestRateLimitError:
|
||||
"""Test suite for RateLimitError."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Should have default message and no retry_after."""
|
||||
# Arrange & Act
|
||||
error = RateLimitError()
|
||||
|
||||
# Assert
|
||||
assert error.code == "RATE_LIMITED"
|
||||
assert str(error) == "Rate limit exceeded"
|
||||
assert error.retry_after is None
|
||||
|
||||
def test_with_retry_after(self):
|
||||
"""Should store retry_after value."""
|
||||
# Arrange & Act
|
||||
error = RateLimitError(retry_after=60)
|
||||
|
||||
# Assert
|
||||
assert error.retry_after == 60
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWebhookError:
|
||||
"""Test suite for WebhookError."""
|
||||
|
||||
def test_without_webhook_id(self):
|
||||
"""Should work without webhook_id."""
|
||||
# Arrange & Act
|
||||
error = WebhookError("Webhook failed")
|
||||
|
||||
# Assert
|
||||
assert error.code == "WEBHOOK_ERROR"
|
||||
assert error.webhook_id is None
|
||||
|
||||
def test_with_webhook_id(self):
|
||||
"""Should store webhook_id."""
|
||||
# Arrange & Act
|
||||
error = WebhookError("Webhook failed", webhook_id="webhook123")
|
||||
|
||||
# Assert
|
||||
assert error.webhook_id == "webhook123"
|
||||
71
tests/unit/test_core/test_logging.py
Normal file
71
tests/unit/test_core/test_logging.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Tests for logging module."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import structlog
|
||||
|
||||
from notebooklm_agent.core.config import Settings
|
||||
from notebooklm_agent.core.logging import setup_logging
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSetupLogging:
|
||||
"""Test suite for setup_logging function."""
|
||||
|
||||
@patch("notebooklm_agent.core.logging.structlog.configure")
|
||||
@patch("notebooklm_agent.core.logging.logging.basicConfig")
|
||||
def test_configures_structlog(self, mock_basic_config, mock_structlog_configure):
|
||||
"""Should configure structlog with correct processors."""
|
||||
# Arrange
|
||||
settings = Settings(log_level="INFO", log_format="json")
|
||||
|
||||
# Act
|
||||
setup_logging(settings)
|
||||
|
||||
# Assert
|
||||
mock_structlog_configure.assert_called_once()
|
||||
call_args = mock_structlog_configure.call_args
|
||||
assert "processors" in call_args.kwargs
|
||||
|
||||
@patch("notebooklm_agent.core.logging.structlog.configure")
|
||||
@patch("notebooklm_agent.core.logging.logging.basicConfig")
|
||||
def test_uses_json_renderer_for_json_format(self, mock_basic_config, mock_structlog_configure):
|
||||
"""Should use JSONRenderer for json log format."""
|
||||
# Arrange
|
||||
settings = Settings(log_level="INFO", log_format="json")
|
||||
|
||||
# Act
|
||||
setup_logging(settings)
|
||||
|
||||
# Assert
|
||||
processors = mock_structlog_configure.call_args.kwargs["processors"]
|
||||
assert any("JSONRenderer" in str(p) for p in processors)
|
||||
|
||||
@patch("notebooklm_agent.core.logging.structlog.configure")
|
||||
@patch("notebooklm_agent.core.logging.logging.basicConfig")
|
||||
def test_uses_console_renderer_for_console_format(self, mock_basic_config, mock_structlog_configure):
|
||||
"""Should use ConsoleRenderer for console log format."""
|
||||
# Arrange
|
||||
settings = Settings(log_level="INFO", log_format="console")
|
||||
|
||||
# Act
|
||||
setup_logging(settings)
|
||||
|
||||
# Assert
|
||||
processors = mock_structlog_configure.call_args.kwargs["processors"]
|
||||
assert any("ConsoleRenderer" in str(p) for p in processors)
|
||||
|
||||
@patch("notebooklm_agent.core.logging.structlog.configure")
|
||||
@patch("notebooklm_agent.core.logging.logging.basicConfig")
|
||||
def test_sets_uvicorn_log_level(self, mock_basic_config, mock_structlog_configure):
|
||||
"""Should set uvicorn loggers to WARNING."""
|
||||
# Arrange
|
||||
settings = Settings(log_level="INFO", log_format="json")
|
||||
|
||||
# Act
|
||||
setup_logging(settings)
|
||||
|
||||
# Assert
|
||||
# basicConfig should be called
|
||||
mock_basic_config.assert_called_once()
|
||||
516
tests/unit/test_services/test_notebook_service.py
Normal file
516
tests/unit/test_services/test_notebook_service.py
Normal file
@@ -0,0 +1,516 @@
|
||||
"""Unit tests for NotebookService.
|
||||
|
||||
TDD Cycle: RED → GREEN → REFACTOR
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from notebooklm_agent.core.exceptions import (
|
||||
NotebookLMError,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
)
|
||||
from notebooklm_agent.services.notebook_service import NotebookService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookServiceInit:
|
||||
"""Test suite for NotebookService initialization and _get_client."""
|
||||
|
||||
async def test_get_client_returns_existing_client(self):
|
||||
"""Should return existing client if already initialized."""
|
||||
# Arrange
|
||||
mock_client = AsyncMock()
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act
|
||||
client = await service._get_client()
|
||||
|
||||
# Assert
|
||||
assert client == mock_client
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookServiceValidateTitle:
|
||||
"""Test suite for NotebookService._validate_title() method."""
|
||||
|
||||
def test_validate_title_none_raises_validation_error(self):
|
||||
"""Should raise ValidationError for None title."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError, match="Title is required"):
|
||||
service._validate_title(None)
|
||||
|
||||
def test_validate_title_too_short_raises_validation_error(self):
|
||||
"""Should raise ValidationError for title < 3 characters."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError, match="at least 3 characters"):
|
||||
service._validate_title("AB")
|
||||
|
||||
def test_validate_title_too_long_raises_validation_error(self):
|
||||
"""Should raise ValidationError for title > 100 characters."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
long_title = "A" * 101
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError, match="at most 100 characters"):
|
||||
service._validate_title(long_title)
|
||||
|
||||
def test_validate_title_exactly_100_chars_succeeds(self):
|
||||
"""Should accept title with exactly 100 characters."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
title = "A" * 100
|
||||
|
||||
# Act
|
||||
result = service._validate_title(title)
|
||||
|
||||
# Assert
|
||||
assert result == title
|
||||
|
||||
def test_validate_title_exactly_3_chars_succeeds(self):
|
||||
"""Should accept title with exactly 3 characters."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
|
||||
# Act
|
||||
result = service._validate_title("ABC")
|
||||
|
||||
# Assert
|
||||
assert result == "ABC"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookServiceCreate:
|
||||
"""Test suite for NotebookService.create() method."""
|
||||
|
||||
async def test_create_valid_title_returns_notebook(self):
|
||||
"""Should create notebook with valid title."""
|
||||
# Arrange
|
||||
mock_client = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = str(uuid4())
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_client.notebooks.create.return_value = mock_response
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": "Test Notebook", "description": None}
|
||||
|
||||
# Act
|
||||
result = await service.create(data)
|
||||
|
||||
# Assert
|
||||
assert result.title == "Test Notebook"
|
||||
assert result.id is not None
|
||||
mock_client.notebooks.create.assert_called_once_with("Test Notebook")
|
||||
|
||||
async def test_create_with_description_returns_notebook(self):
|
||||
"""Should create notebook with description."""
|
||||
# Arrange
|
||||
mock_client = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = str(uuid4())
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = "A description"
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_client.notebooks.create.return_value = mock_response
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": "Test Notebook", "description": "A description"}
|
||||
|
||||
# Act
|
||||
result = await service.create(data)
|
||||
|
||||
# Assert
|
||||
assert result.description == "A description"
|
||||
|
||||
async def test_create_empty_title_raises_validation_error(self):
|
||||
"""Should raise ValidationError for empty title."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
data = {"title": "", "description": None}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError, match="Title cannot be empty"):
|
||||
await service.create(data)
|
||||
|
||||
async def test_create_whitespace_title_raises_validation_error(self):
|
||||
"""Should raise ValidationError for whitespace-only title."""
|
||||
# Arrange
|
||||
service = NotebookService()
|
||||
data = {"title": " ", "description": None}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError):
|
||||
await service.create(data)
|
||||
|
||||
async def test_create_notebooklm_error_raises_notebooklm_error(self):
|
||||
"""Should raise NotebookLMError on external API error."""
|
||||
# Arrange
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.create.side_effect = Exception("API Error")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": "Test Notebook", "description": None}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotebookLMError):
|
||||
await service.create(data)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookServiceList:
|
||||
"""Test suite for NotebookService.list() method."""
|
||||
|
||||
async def test_list_returns_paginated_notebooks(self):
|
||||
"""Should return paginated list of notebooks."""
|
||||
# Arrange
|
||||
mock_client = AsyncMock()
|
||||
mock_notebook = MagicMock()
|
||||
mock_notebook.id = str(uuid4())
|
||||
mock_notebook.title = "Notebook 1"
|
||||
mock_notebook.description = None
|
||||
mock_notebook.created_at = datetime.utcnow()
|
||||
mock_notebook.updated_at = datetime.utcnow()
|
||||
mock_client.notebooks.list.return_value = [mock_notebook]
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act
|
||||
result = await service.list(limit=20, offset=0, sort="created_at", order="desc")
|
||||
|
||||
# Assert
|
||||
assert len(result.items) == 1
|
||||
assert result.items[0].title == "Notebook 1"
|
||||
assert result.pagination.total == 1
|
||||
assert result.pagination.limit == 20
|
||||
assert result.pagination.offset == 0
|
||||
|
||||
async def test_list_with_pagination_returns_correct_page(self):
|
||||
"""Should return correct page with offset."""
|
||||
# Arrange
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.list.return_value = []
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act
|
||||
result = await service.list(limit=10, offset=20, sort="created_at", order="desc")
|
||||
|
||||
# Assert
|
||||
assert result.pagination.limit == 10
|
||||
assert result.pagination.offset == 20
|
||||
|
||||
async def test_list_notebooklm_error_raises_notebooklm_error(self):
|
||||
"""Should raise NotebookLMError on external API error."""
|
||||
# Arrange
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.list.side_effect = Exception("API Error")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotebookLMError):
|
||||
await service.list(limit=20, offset=0, sort="created_at", order="desc")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookServiceGet:
|
||||
"""Test suite for NotebookService.get() method."""
|
||||
|
||||
async def test_get_existing_id_returns_notebook(self):
|
||||
"""Should return notebook for existing ID."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = str(notebook_id)
|
||||
mock_response.title = "Test Notebook"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_client.notebooks.get.return_value = mock_response
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act
|
||||
result = await service.get(notebook_id)
|
||||
|
||||
# Assert
|
||||
assert result.id == notebook_id
|
||||
assert result.title == "Test Notebook"
|
||||
mock_client.notebooks.get.assert_called_once_with(str(notebook_id))
|
||||
|
||||
async def test_get_nonexistent_id_raises_not_found(self):
|
||||
"""Should raise NotFoundError for non-existent ID."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.get.side_effect = Exception("Not found")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.get(notebook_id)
|
||||
|
||||
async def test_get_not_found_in_message_raises_not_found(self):
|
||||
"""Should raise NotFoundError when API message contains 'not found'."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.get.side_effect = Exception("resource not found in system")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.get(notebook_id)
|
||||
|
||||
async def test_get_other_error_raises_notebooklm_error(self):
|
||||
"""Should raise NotebookLMError on other API errors (line 179)."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.get.side_effect = Exception("Connection timeout")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotebookLMError, match="Failed to get notebook"):
|
||||
await service.get(notebook_id)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookServiceUpdate:
|
||||
"""Test suite for NotebookService.update() method."""
|
||||
|
||||
async def test_update_title_returns_updated_notebook(self):
|
||||
"""Should update title and return updated notebook."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = str(notebook_id)
|
||||
mock_response.title = "Updated Title"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_client.notebooks.update.return_value = mock_response
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": "Updated Title", "description": None}
|
||||
|
||||
# Act
|
||||
result = await service.update(notebook_id, data)
|
||||
|
||||
# Assert
|
||||
assert result.title == "Updated Title"
|
||||
mock_client.notebooks.update.assert_called_once()
|
||||
|
||||
async def test_update_description_only_returns_updated_notebook(self):
|
||||
"""Should update only description (partial update)."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = str(notebook_id)
|
||||
mock_response.title = "Original Title"
|
||||
mock_response.description = "New Description"
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_client.notebooks.update.return_value = mock_response
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": None, "description": "New Description"}
|
||||
|
||||
# Act
|
||||
result = await service.update(notebook_id, data)
|
||||
|
||||
# Assert
|
||||
assert result.description == "New Description"
|
||||
|
||||
async def test_update_nonexistent_id_raises_not_found(self):
|
||||
"""Should raise NotFoundError for non-existent ID."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.update.side_effect = Exception("Not found")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": "Updated Title", "description": None}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.update(notebook_id, data)
|
||||
|
||||
async def test_update_empty_data_calls_get_instead(self):
|
||||
"""Should call get() instead of update when no data provided."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = str(notebook_id)
|
||||
mock_response.title = "Original Title"
|
||||
mock_response.description = None
|
||||
mock_response.created_at = datetime.utcnow()
|
||||
mock_response.updated_at = datetime.utcnow()
|
||||
mock_client.notebooks.get.return_value = mock_response
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {} # Empty data
|
||||
|
||||
# Act
|
||||
result = await service.update(notebook_id, data)
|
||||
|
||||
# Assert
|
||||
assert result.title == "Original Title"
|
||||
mock_client.notebooks.update.assert_not_called()
|
||||
mock_client.notebooks.get.assert_called_once_with(str(notebook_id))
|
||||
|
||||
async def test_update_with_not_found_in_message_raises_not_found(self):
|
||||
"""Should raise NotFoundError when API returns 'not found' message."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.update.side_effect = Exception("notebook not found in database")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": "Updated Title"}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.update(notebook_id, data)
|
||||
|
||||
async def test_update_notebooklm_error_raises_notebooklm_error(self):
|
||||
"""Should raise NotebookLMError on other API errors."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.update.side_effect = Exception("Some other error")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {"title": "Updated Title"}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotebookLMError):
|
||||
await service.update(notebook_id, data)
|
||||
|
||||
async def test_update_reraises_not_found_error_directly(self):
|
||||
"""Should re-raise NotFoundError directly without wrapping (line 218)."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
# Simulate a NotFoundError raised during get() call in empty update case
|
||||
mock_client.notebooks.get.side_effect = NotFoundError("Notebook", str(notebook_id))
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
data = {} # Empty data triggers get() call
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotFoundError) as exc_info:
|
||||
await service.update(notebook_id, data)
|
||||
|
||||
assert "Notebook" in str(exc_info.value)
|
||||
|
||||
async def test_update_empty_title_raises_validation_error(self):
|
||||
"""Should raise ValidationError for empty title."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
service = NotebookService()
|
||||
data = {"title": "", "description": None}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError):
|
||||
await service.update(notebook_id, data)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNotebookServiceDelete:
|
||||
"""Test suite for NotebookService.delete() method."""
|
||||
|
||||
async def test_delete_existing_id_succeeds(self):
|
||||
"""Should delete notebook for existing ID."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.delete.return_value = None
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act
|
||||
await service.delete(notebook_id)
|
||||
|
||||
# Assert
|
||||
mock_client.notebooks.delete.assert_called_once_with(str(notebook_id))
|
||||
|
||||
async def test_delete_nonexistent_id_raises_not_found(self):
|
||||
"""Should raise NotFoundError for non-existent ID."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.delete.side_effect = Exception("Not found")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.delete(notebook_id)
|
||||
|
||||
async def test_delete_not_found_in_message_raises_not_found(self):
|
||||
"""Should raise NotFoundError when API message contains 'not found'."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.delete.side_effect = Exception("notebook not found")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.delete(notebook_id)
|
||||
|
||||
async def test_delete_notebooklm_error_raises_notebooklm_error(self):
|
||||
"""Should raise NotebookLMError on other API errors."""
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.delete.side_effect = Exception("Connection timeout")
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(NotebookLMError):
|
||||
await service.delete(notebook_id)
|
||||
|
||||
async def test_delete_is_idempotent(self):
|
||||
"""Delete should be idempotent (no error on second delete)."""
|
||||
# Note: This test documents expected behavior
|
||||
# Actual idempotency depends on notebooklm-py behavior
|
||||
# Arrange
|
||||
notebook_id = uuid4()
|
||||
mock_client = AsyncMock()
|
||||
mock_client.notebooks.delete.side_effect = [None, Exception("Not found")]
|
||||
|
||||
service = NotebookService(client=mock_client)
|
||||
|
||||
# Act - First delete should succeed
|
||||
await service.delete(notebook_id)
|
||||
|
||||
# Assert - Second delete should raise NotFoundError
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.delete(notebook_id)
|
||||
Reference in New Issue
Block a user