Spaces:
Sleeping
Sleeping
Commit
·
f871fed
0
Parent(s):
FIX: SurrealDB 2.0 migration syntax and Frontend/CORS link
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +63 -0
- .env.example +259 -0
- .env.railway +125 -0
- .github/ISSUE_TEMPLATE/bug_report.yml +104 -0
- .github/ISSUE_TEMPLATE/config.yml +11 -0
- .github/ISSUE_TEMPLATE/feature_request.yml +65 -0
- .github/ISSUE_TEMPLATE/installation_issue.yml +148 -0
- .github/pull_request_template.md +107 -0
- .github/workflows/build-and-release.yml +298 -0
- .github/workflows/build-dev.yml +157 -0
- .github/workflows/claude-code-review.yml +75 -0
- .github/workflows/claude.yml +59 -0
- .gitignore +137 -0
- .python-version +1 -0
- .railwayignore +75 -0
- .worktreeinclude +5 -0
- DEPLOY_NOW.md +201 -0
- Dockerfile +66 -0
- Dockerfile.huggingface +66 -0
- Dockerfile.railway +80 -0
- Dockerfile.single +98 -0
- FINAL_RAILWAY_VARS.md +59 -0
- FREE_TIER_QUICK.md +104 -0
- FREE_TIER_SETUP.md +375 -0
- FRONTEND_CONNECTION_GUIDE.md +215 -0
- HUGGINGFACE_DEPLOYMENT.md +197 -0
- LICENSE +17 -0
- Makefile +201 -0
- QUICK_FIX.md +99 -0
- RAILWAY.md +197 -0
- RAILWAY_CHECKLIST.md +164 -0
- RAILWAY_FIXES.md +196 -0
- README.md +9 -0
- README_HUGGINGFACE.md +53 -0
- YOUR_RAILWAY_CONFIG.md +286 -0
- api/__init__.py +0 -0
- api/auth.py +100 -0
- api/chat_service.py +174 -0
- api/client.py +473 -0
- api/command_service.py +92 -0
- api/context_service.py +32 -0
- api/embedding_service.py +25 -0
- api/episode_profiles_service.py +104 -0
- api/insights_service.py +84 -0
- api/main.py +151 -0
- api/models.py +430 -0
- api/models_service.py +100 -0
- api/notebook_service.py +87 -0
- api/notes_service.py +100 -0
- api/podcast_api_service.py +125 -0
.dockerignore
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
notebooks/
|
| 2 |
+
data/
|
| 3 |
+
.uploads/
|
| 4 |
+
.venv/
|
| 5 |
+
.env
|
| 6 |
+
sqlite-db/
|
| 7 |
+
temp/
|
| 8 |
+
google-credentials.json
|
| 9 |
+
docker-compose*
|
| 10 |
+
.docker_data/
|
| 11 |
+
docs/
|
| 12 |
+
surreal_data/
|
| 13 |
+
surreal-data/
|
| 14 |
+
notebook_data/
|
| 15 |
+
temp/
|
| 16 |
+
*.env
|
| 17 |
+
.git/
|
| 18 |
+
.github/
|
| 19 |
+
|
| 20 |
+
# Frontend build artifacts and dependencies
|
| 21 |
+
frontend/node_modules/
|
| 22 |
+
frontend/.next/
|
| 23 |
+
frontend/.env.local
|
| 24 |
+
|
| 25 |
+
# Cache directories (recursive patterns)
|
| 26 |
+
**/__pycache__/
|
| 27 |
+
**/.mypy_cache/
|
| 28 |
+
**/.ruff_cache/
|
| 29 |
+
**/.pytest_cache/
|
| 30 |
+
**/*.pyc
|
| 31 |
+
**/*.pyo
|
| 32 |
+
**/*.pyd
|
| 33 |
+
.coverage
|
| 34 |
+
.coverage.*
|
| 35 |
+
htmlcov/
|
| 36 |
+
.tox/
|
| 37 |
+
.nox/
|
| 38 |
+
.cache/
|
| 39 |
+
nosetests.xml
|
| 40 |
+
coverage.xml
|
| 41 |
+
*.cover
|
| 42 |
+
*.py,cover
|
| 43 |
+
.hypothesis/
|
| 44 |
+
|
| 45 |
+
# IDE and editor files
|
| 46 |
+
.vscode/
|
| 47 |
+
.idea/
|
| 48 |
+
*.swp
|
| 49 |
+
*.swo
|
| 50 |
+
*~
|
| 51 |
+
|
| 52 |
+
# OS files
|
| 53 |
+
.DS_Store
|
| 54 |
+
.DS_Store?
|
| 55 |
+
._*
|
| 56 |
+
.Spotlight-V100
|
| 57 |
+
.Trashes
|
| 58 |
+
ehthumbs.db
|
| 59 |
+
Thumbs.db
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
.quarentena/
|
| 63 |
+
surreal_single_data/
|
.env.example
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# API CONFIGURATION
|
| 3 |
+
# URL where the API can be accessed by the browser
|
| 4 |
+
# This setting allows the frontend to connect to the API at runtime (no rebuild needed!)
|
| 5 |
+
#
|
| 6 |
+
# IMPORTANT: Do NOT include /api at the end - it will be added automatically!
|
| 7 |
+
#
|
| 8 |
+
# Common scenarios:
|
| 9 |
+
# - Docker on localhost: http://localhost:5055 (default, works for most cases)
|
| 10 |
+
# - Docker on LAN/remote server: http://192.168.1.100:5055 or http://your-server-ip:5055
|
| 11 |
+
# - Behind reverse proxy with custom domain: https://your-domain.com
|
| 12 |
+
# - Behind reverse proxy with subdomain: https://api.your-domain.com
|
| 13 |
+
#
|
| 14 |
+
# Examples for reverse proxy users:
|
| 15 |
+
# - API_URL=https://notebook.example.com (frontend will call https://notebook.example.com/api/*)
|
| 16 |
+
# - API_URL=https://api.example.com (frontend will call https://api.example.com/api/*)
|
| 17 |
+
#
|
| 18 |
+
# Note: If not set, the system will auto-detect based on the incoming request.
|
| 19 |
+
# Only set this if you need to override the auto-detection (e.g., reverse proxy scenarios).
|
| 20 |
+
API_URL=http://localhost:5055
|
| 21 |
+
|
| 22 |
+
# INTERNAL API URL (Server-Side)
|
| 23 |
+
# URL where Next.js server-side should proxy API requests (via rewrites)
|
| 24 |
+
# This is DIFFERENT from API_URL which is used by the browser client
|
| 25 |
+
#
|
| 26 |
+
# INTERNAL_API_URL is used by Next.js rewrites to forward /api/* requests to the FastAPI backend
|
| 27 |
+
# API_URL is used by the browser to know where to make API calls
|
| 28 |
+
#
|
| 29 |
+
# Default: http://localhost:5055 (single-container deployment - both services on same host)
|
| 30 |
+
# Override for multi-container: INTERNAL_API_URL=http://api-service:5055
|
| 31 |
+
#
|
| 32 |
+
# Common scenarios:
|
| 33 |
+
# - Single container (default): Don't set - defaults to http://localhost:5055
|
| 34 |
+
# - Multi-container Docker Compose: INTERNAL_API_URL=http://api:5055 (use service name)
|
| 35 |
+
# - Kubernetes/advanced networking: INTERNAL_API_URL=http://api-service.namespace.svc.cluster.local:5055
|
| 36 |
+
#
|
| 37 |
+
# Why two variables?
|
| 38 |
+
# - API_URL: External/public URL that browsers use (can be https://your-domain.com)
|
| 39 |
+
# - INTERNAL_API_URL: Internal container networking URL (usually http://localhost:5055 or service name)
|
| 40 |
+
#
|
| 41 |
+
# INTERNAL_API_URL=http://localhost:5055
|
| 42 |
+
|
| 43 |
+
# API CLIENT TIMEOUT (in seconds)
|
| 44 |
+
# Controls how long the frontend/Streamlit UI waits for API responses
|
| 45 |
+
# Increase this if you're using slow AI providers or hardware (Ollama on CPU, remote LM Studio, etc.)
|
| 46 |
+
# Default: 300 seconds (5 minutes) - sufficient for most transformation/insight operations
|
| 47 |
+
#
|
| 48 |
+
# Common scenarios:
|
| 49 |
+
# - Fast cloud APIs (OpenAI, Anthropic): 300 seconds is more than enough
|
| 50 |
+
# - Local Ollama on GPU: 300 seconds should work fine
|
| 51 |
+
# - Local Ollama on CPU: Consider 600 seconds (10 minutes) or more
|
| 52 |
+
# - Remote LM Studio over slow network: Consider 900 seconds (15 minutes)
|
| 53 |
+
# - Very large documents: May need 900+ seconds
|
| 54 |
+
#
|
| 55 |
+
# API_CLIENT_TIMEOUT=300
|
| 56 |
+
|
| 57 |
+
# ESPERANTO LLM TIMEOUT (in seconds)
|
| 58 |
+
# Controls the timeout for AI model API calls at the Esperanto library level
|
| 59 |
+
# This is separate from API_CLIENT_TIMEOUT and applies to the actual LLM provider requests
|
| 60 |
+
# Only increase this if you're experiencing timeouts during model inference itself
|
| 61 |
+
# Default: 60 seconds (built into Esperanto)
|
| 62 |
+
#
|
| 63 |
+
# Important: This should generally be LOWER than API_CLIENT_TIMEOUT to allow proper error handling
|
| 64 |
+
#
|
| 65 |
+
# Common scenarios:
|
| 66 |
+
# - Fast cloud APIs (OpenAI, Anthropic, Groq): 60 seconds is sufficient
|
| 67 |
+
# - Local Ollama with small models: 120-180 seconds may help
|
| 68 |
+
# - Local Ollama with large models on CPU: 300+ seconds
|
| 69 |
+
# - Remote or self-hosted LLMs: 180-300 seconds depending on hardware
|
| 70 |
+
#
|
| 71 |
+
# Note: If transformations complete but you see timeout errors, increase API_CLIENT_TIMEOUT first.
|
| 72 |
+
# Only increase ESPERANTO_LLM_TIMEOUT if the model itself is timing out during inference.
|
| 73 |
+
#
|
| 74 |
+
# ESPERANTO_LLM_TIMEOUT=60
|
| 75 |
+
|
| 76 |
+
# SSL VERIFICATION CONFIGURATION
|
| 77 |
+
# Configure SSL certificate verification for local AI providers (Ollama, LM Studio, etc.)
|
| 78 |
+
# behind reverse proxies with self-signed certificates
|
| 79 |
+
#
|
| 80 |
+
# Option 1: Custom CA Bundle (recommended for self-signed certs)
|
| 81 |
+
# Point to your CA certificate file to verify SSL while using custom certificates
|
| 82 |
+
# ESPERANTO_SSL_CA_BUNDLE=/path/to/your/ca-bundle.pem
|
| 83 |
+
#
|
| 84 |
+
# Option 2: Disable SSL Verification (development only)
|
| 85 |
+
# WARNING: Disabling SSL verification exposes you to man-in-the-middle attacks
|
| 86 |
+
# Only use in trusted development/testing environments
|
| 87 |
+
# ESPERANTO_SSL_VERIFY=false
|
| 88 |
+
|
| 89 |
+
# SECURITY
|
| 90 |
+
# Set this to protect your Open Notebook instance with a password (for public hosting)
|
| 91 |
+
# OPEN_NOTEBOOK_PASSWORD=
|
| 92 |
+
|
| 93 |
+
# OPENAI
|
| 94 |
+
# OPENAI_API_KEY=
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# ANTHROPIC
|
| 98 |
+
# ANTHROPIC_API_KEY=
|
| 99 |
+
|
| 100 |
+
# GEMINI
|
| 101 |
+
# this is the best model for long context and podcast generation
|
| 102 |
+
# GOOGLE_API_KEY=
|
| 103 |
+
# GEMINI_API_BASE_URL= # Optional: Override default endpoint (for Vertex AI, proxies, etc.)
|
| 104 |
+
|
| 105 |
+
# VERTEXAI
|
| 106 |
+
# VERTEX_PROJECT=my-google-cloud-project-name
|
| 107 |
+
# GOOGLE_APPLICATION_CREDENTIALS=./google-credentials.json
|
| 108 |
+
# VERTEX_LOCATION=us-east5
|
| 109 |
+
|
| 110 |
+
# MISTRAL
|
| 111 |
+
# MISTRAL_API_KEY=
|
| 112 |
+
|
| 113 |
+
# DEEPSEEK
|
| 114 |
+
# DEEPSEEK_API_KEY=
|
| 115 |
+
|
| 116 |
+
# OLLAMA
|
| 117 |
+
# OLLAMA_API_BASE="http://10.20.30.20:11434"
|
| 118 |
+
|
| 119 |
+
# OPEN ROUTER
|
| 120 |
+
# OPENROUTER_BASE_URL="https://openrouter.ai/api/v1"
|
| 121 |
+
# OPENROUTER_API_KEY=
|
| 122 |
+
|
| 123 |
+
# GROQ
|
| 124 |
+
# GROQ_API_KEY=
|
| 125 |
+
|
| 126 |
+
# XAI
|
| 127 |
+
# XAI_API_KEY=
|
| 128 |
+
|
| 129 |
+
# ELEVENLABS
|
| 130 |
+
# Used only by the podcast feature
|
| 131 |
+
# ELEVENLABS_API_KEY=
|
| 132 |
+
|
| 133 |
+
# TTS BATCH SIZE
|
| 134 |
+
# Controls concurrent TTS requests for podcast generation (default: 5)
|
| 135 |
+
# Lower values reduce provider load but increase generation time
|
| 136 |
+
# Recommended: OpenAI=5, ElevenLabs=2, Google=4, Custom=1
|
| 137 |
+
# TTS_BATCH_SIZE=2
|
| 138 |
+
|
| 139 |
+
# VOYAGE AI
|
| 140 |
+
# VOYAGE_API_KEY=
|
| 141 |
+
|
| 142 |
+
# OPENAI COMPATIBLE ENDPOINTS
|
| 143 |
+
# Generic configuration (applies to all modalities: language, embedding, STT, TTS)
|
| 144 |
+
# OPENAI_COMPATIBLE_BASE_URL=http://localhost:1234/v1
|
| 145 |
+
# OPENAI_COMPATIBLE_API_KEY=
|
| 146 |
+
|
| 147 |
+
# Mode-specific configuration (overrides generic if set)
|
| 148 |
+
# Use these when you want different endpoints for different capabilities
|
| 149 |
+
# OPENAI_COMPATIBLE_BASE_URL_LLM=http://localhost:1234/v1
|
| 150 |
+
# OPENAI_COMPATIBLE_API_KEY_LLM=
|
| 151 |
+
# OPENAI_COMPATIBLE_BASE_URL_EMBEDDING=http://localhost:8080/v1
|
| 152 |
+
# OPENAI_COMPATIBLE_API_KEY_EMBEDDING=
|
| 153 |
+
# OPENAI_COMPATIBLE_BASE_URL_STT=http://localhost:9000/v1
|
| 154 |
+
# OPENAI_COMPATIBLE_API_KEY_STT=
|
| 155 |
+
# OPENAI_COMPATIBLE_BASE_URL_TTS=http://localhost:9000/v1
|
| 156 |
+
# OPENAI_COMPATIBLE_API_KEY_TTS=
|
| 157 |
+
|
| 158 |
+
# AZURE OPENAI
|
| 159 |
+
# Generic configuration (applies to all modalities: language, embedding, STT, TTS)
|
| 160 |
+
# AZURE_OPENAI_API_KEY=
|
| 161 |
+
# AZURE_OPENAI_ENDPOINT=
|
| 162 |
+
# AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
| 163 |
+
|
| 164 |
+
# Mode-specific configuration (overrides generic if set)
|
| 165 |
+
# Use these when you want different deployments for different AI capabilities
|
| 166 |
+
# AZURE_OPENAI_API_KEY_LLM=
|
| 167 |
+
# AZURE_OPENAI_ENDPOINT_LLM=
|
| 168 |
+
# AZURE_OPENAI_API_VERSION_LLM=
|
| 169 |
+
|
| 170 |
+
# AZURE_OPENAI_API_KEY_EMBEDDING=
|
| 171 |
+
# AZURE_OPENAI_ENDPOINT_EMBEDDING=
|
| 172 |
+
# AZURE_OPENAI_API_VERSION_EMBEDDING=
|
| 173 |
+
|
| 174 |
+
# AZURE_OPENAI_API_KEY_STT=
|
| 175 |
+
# AZURE_OPENAI_ENDPOINT_STT=
|
| 176 |
+
# AZURE_OPENAI_API_VERSION_STT=
|
| 177 |
+
|
| 178 |
+
# AZURE_OPENAI_API_KEY_TTS=
|
| 179 |
+
# AZURE_OPENAI_ENDPOINT_TTS=
|
| 180 |
+
# AZURE_OPENAI_API_VERSION_TTS=
|
| 181 |
+
|
| 182 |
+
# USE THIS IF YOU WANT TO DEBUG THE APP ON LANGSMITH
|
| 183 |
+
# LANGCHAIN_TRACING_V2=true
|
| 184 |
+
# LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
|
| 185 |
+
# LANGCHAIN_API_KEY=
|
| 186 |
+
# LANGCHAIN_PROJECT="Open Notebook"
|
| 187 |
+
|
| 188 |
+
# CONNECTION DETAILS FOR YOUR SURREAL DB
|
| 189 |
+
# New format (preferred) - WebSocket URL
|
| 190 |
+
SURREAL_URL="ws://surrealdb/rpc:8000"
|
| 191 |
+
SURREAL_USER="root"
|
| 192 |
+
SURREAL_PASSWORD="root"
|
| 193 |
+
SURREAL_NAMESPACE="open_notebook"
|
| 194 |
+
SURREAL_DATABASE="staging"
|
| 195 |
+
|
| 196 |
+
# RETRY CONFIGURATION (surreal-commands v1.2.0+)
|
| 197 |
+
# Global defaults for all background commands unless explicitly overridden at command level
|
| 198 |
+
# These settings help commands automatically recover from transient failures like:
|
| 199 |
+
# - Database transaction conflicts during concurrent operations
|
| 200 |
+
# - Network timeouts when calling external APIs
|
| 201 |
+
# - Rate limits from LLM/embedding providers
|
| 202 |
+
# - Temporary resource unavailability
|
| 203 |
+
|
| 204 |
+
# Enable/disable retry globally (default: true)
|
| 205 |
+
# Set to false to disable retries for all commands (useful for debugging)
|
| 206 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 207 |
+
|
| 208 |
+
# Maximum retry attempts before giving up (default: 3)
|
| 209 |
+
# Database operations use 5 attempts (defined per-command)
|
| 210 |
+
# API calls use 3 attempts (defined per-command)
|
| 211 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 212 |
+
|
| 213 |
+
# Wait strategy between retry attempts (default: exponential_jitter)
|
| 214 |
+
# Options: exponential_jitter, exponential, fixed, random
|
| 215 |
+
# - exponential_jitter: Recommended - prevents thundering herd during DB conflicts
|
| 216 |
+
# - exponential: Good for API rate limits (predictable backoff)
|
| 217 |
+
# - fixed: Use for quick recovery scenarios
|
| 218 |
+
# - random: Use when you want unpredictable retry timing
|
| 219 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 220 |
+
|
| 221 |
+
# Minimum wait time between retries in seconds (default: 1)
|
| 222 |
+
# Database conflicts: 1 second (fast retry for transient issues)
|
| 223 |
+
# API rate limits: 5 seconds (wait for quota reset)
|
| 224 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 225 |
+
|
| 226 |
+
# Maximum wait time between retries in seconds (default: 30)
|
| 227 |
+
# Database conflicts: 30 seconds maximum
|
| 228 |
+
# API rate limits: 120 seconds maximum (defined per-command)
|
| 229 |
+
# Total retry time won't exceed max_attempts * wait_max
|
| 230 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 231 |
+
|
| 232 |
+
# WORKER CONCURRENCY
|
| 233 |
+
# Maximum number of concurrent tasks in the worker pool (default: 5)
|
| 234 |
+
# This affects the likelihood of database transaction conflicts during batch operations
|
| 235 |
+
#
|
| 236 |
+
# Tuning guidelines based on deployment size:
|
| 237 |
+
# - Resource-constrained (low CPU/memory): 1-2 workers
|
| 238 |
+
# Reduces conflicts and resource usage, but slower processing
|
| 239 |
+
#
|
| 240 |
+
# - Normal deployment (balanced): 5 workers (RECOMMENDED)
|
| 241 |
+
# Good balance between throughput and conflict rate
|
| 242 |
+
# Retry logic handles occasional conflicts gracefully
|
| 243 |
+
#
|
| 244 |
+
# - Large instances (high CPU/memory): 10-20 workers
|
| 245 |
+
# Higher throughput but more frequent DB conflicts
|
| 246 |
+
# Relies heavily on retry logic with jittered backoff
|
| 247 |
+
#
|
| 248 |
+
# Note: Higher concurrency increases vectorization speed but also increases
|
| 249 |
+
# SurrealDB transaction conflicts. The retry logic with exponential-jitter
|
| 250 |
+
# backoff ensures operations complete successfully even at high concurrency.
|
| 251 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 252 |
+
|
| 253 |
+
# OPEN_NOTEBOOK_PASSWORD=
|
| 254 |
+
|
| 255 |
+
# FIRECRAWL - Get a key at https://firecrawl.dev/
|
| 256 |
+
FIRECRAWL_API_KEY=
|
| 257 |
+
|
| 258 |
+
# JINA - Get a key at https://jina.ai/
|
| 259 |
+
JINA_API_KEY=
|
.env.railway
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Railway Deployment Environment Variables
|
| 2 |
+
# Copy these to your Railway service's Variables section
|
| 3 |
+
|
| 4 |
+
# ============================================
|
| 5 |
+
# DATABASE CONNECTION (Single Container)
|
| 6 |
+
# ============================================
|
| 7 |
+
# Use 127.0.0.1 for Railway single-container deployment
|
| 8 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 9 |
+
SURREAL_USER=root
|
| 10 |
+
SURREAL_PASSWORD=root
|
| 11 |
+
SURREAL_NAMESPACE=test
|
| 12 |
+
SURREAL_DATABASE=test
|
| 13 |
+
|
| 14 |
+
# ============================================
|
| 15 |
+
# API CONFIGURATION
|
| 16 |
+
# ============================================
|
| 17 |
+
# INTERNAL_API_URL: Used by Next.js server-side to proxy to FastAPI
|
| 18 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 19 |
+
|
| 20 |
+
# API_URL: Public URL - SET THIS AFTER FIRST DEPLOY
|
| 21 |
+
# Replace YOUR_RAILWAY_APP_URL with your actual Railway app URL
|
| 22 |
+
# Format: https://your-app-name.up.railway.app (no /api at the end)
|
| 23 |
+
API_URL=https://YOUR_RAILWAY_APP_URL
|
| 24 |
+
|
| 25 |
+
# ============================================
|
| 26 |
+
# WORKER & RETRY CONFIGURATION
|
| 27 |
+
# ============================================
|
| 28 |
+
# Background worker concurrency (default: 5)
|
| 29 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 30 |
+
|
| 31 |
+
# Retry configuration for resilient background tasks
|
| 32 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 33 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 34 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 35 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 36 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 37 |
+
|
| 38 |
+
# ============================================
|
| 39 |
+
# AI MODEL API KEYS (Configured for FREE tier)
|
| 40 |
+
# ============================================
|
| 41 |
+
|
| 42 |
+
# Groq (for chat, transformations, insights - FREE)
|
| 43 |
+
GROQ_API_KEY=your_groq_api_key_here
|
| 44 |
+
|
| 45 |
+
# Google Gemini (for embeddings, long context - FREE)
|
| 46 |
+
GOOGLE_API_KEY=your_google_api_key_here
|
| 47 |
+
|
| 48 |
+
# Llama (if using via Ollama or another provider)
|
| 49 |
+
# If using Ollama locally/remote, set the base URL:
|
| 50 |
+
# OLLAMA_API_BASE=http://your-ollama-host:11434
|
| 51 |
+
|
| 52 |
+
# OpenAI (optional - for GPT models, embeddings, TTS)
|
| 53 |
+
# OPENAI_API_KEY=sk-your_openai_key_here
|
| 54 |
+
|
| 55 |
+
# Anthropic (optional - for Claude models)
|
| 56 |
+
# ANTHROPIC_API_KEY=sk-ant-your_anthropic_key_here
|
| 57 |
+
|
| 58 |
+
# Mistral (optional - for Mistral models)
|
| 59 |
+
# MISTRAL_API_KEY=your_mistral_key_here
|
| 60 |
+
|
| 61 |
+
# DeepSeek (optional - for DeepSeek models)
|
| 62 |
+
# DEEPSEEK_API_KEY=your_deepseek_key_here
|
| 63 |
+
|
| 64 |
+
# XAI (optional - for Grok models)
|
| 65 |
+
# XAI_API_KEY=your_xai_key_here
|
| 66 |
+
|
| 67 |
+
# OpenRouter (optional - access multiple models via one API)
|
| 68 |
+
# OPENROUTER_API_KEY=your_openrouter_key_here
|
| 69 |
+
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
| 70 |
+
|
| 71 |
+
# ============================================
|
| 72 |
+
# PODCAST FEATURES (Optional)
|
| 73 |
+
# ============================================
|
| 74 |
+
# ElevenLabs for high-quality text-to-speech
|
| 75 |
+
# ELEVENLABS_API_KEY=your_elevenlabs_key_here
|
| 76 |
+
|
| 77 |
+
# TTS batch size (adjust based on provider)
|
| 78 |
+
# OpenAI/Google: 5, ElevenLabs: 2, Custom: 1
|
| 79 |
+
# TTS_BATCH_SIZE=5
|
| 80 |
+
|
| 81 |
+
# ============================================
|
| 82 |
+
# EMBEDDINGS (Optional - if not using default)
|
| 83 |
+
# ============================================
|
| 84 |
+
# Voyage AI for advanced embeddings
|
| 85 |
+
# VOYAGE_API_KEY=your_voyage_key_here
|
| 86 |
+
|
| 87 |
+
# ============================================
|
| 88 |
+
# WEB SCRAPING (Optional)
|
| 89 |
+
# ============================================
|
| 90 |
+
# Firecrawl for enhanced web scraping
|
| 91 |
+
# FIRECRAWL_API_KEY=your_firecrawl_key_here
|
| 92 |
+
|
| 93 |
+
# Jina AI for web reading and embeddings
|
| 94 |
+
# JINA_API_KEY=your_jina_key_here
|
| 95 |
+
|
| 96 |
+
# ============================================
|
| 97 |
+
# SECURITY (Optional but Recommended)
|
| 98 |
+
# ============================================
|
| 99 |
+
# Protect your instance with a password for public hosting
|
| 100 |
+
# OPEN_NOTEBOOK_PASSWORD=your_secure_password_here
|
| 101 |
+
|
| 102 |
+
# ============================================
|
| 103 |
+
# ADVANCED: TIMEOUT CONFIGURATION (Optional)
|
| 104 |
+
# ============================================
|
| 105 |
+
# Only adjust these if you experience timeout issues
|
| 106 |
+
|
| 107 |
+
# API client timeout (seconds) - how long frontend waits for responses
|
| 108 |
+
# Default: 300 (5 minutes)
|
| 109 |
+
# Increase for slow models or large documents
|
| 110 |
+
# API_CLIENT_TIMEOUT=300
|
| 111 |
+
|
| 112 |
+
# LLM provider timeout (seconds) - how long to wait for AI model response
|
| 113 |
+
# Default: 60 seconds
|
| 114 |
+
# Increase for slow local models (Ollama on CPU, etc.)
|
| 115 |
+
# ESPERANTO_LLM_TIMEOUT=60
|
| 116 |
+
|
| 117 |
+
# ============================================
|
| 118 |
+
# NOTES FOR RAILWAY DEPLOYMENT
|
| 119 |
+
# ============================================
|
| 120 |
+
# 1. PORT variable is automatically set by Railway - DO NOT override it
|
| 121 |
+
# 2. Railway will expose your app on the PORT it assigns (usually 8080)
|
| 122 |
+
# 3. Set API_URL AFTER your first deploy when you get your Railway domain
|
| 123 |
+
# 4. Use 127.0.0.1 (not localhost) for internal connections
|
| 124 |
+
# 5. Keep database and API settings as-is for single container deployment
|
| 125 |
+
|
.github/ISSUE_TEMPLATE/bug_report.yml
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 🐛 Bug Report
|
| 2 |
+
description: Report a bug or unexpected behavior (app is running but misbehaving)
|
| 3 |
+
title: "[Bug]: "
|
| 4 |
+
labels: ["bug", "needs-triage"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
Thanks for reporting a bug! Please fill out the information below to help us understand and fix the issue.
|
| 10 |
+
|
| 11 |
+
**Note**: If you're having installation or setup issues, please use the "Installation Issue" template instead.
|
| 12 |
+
|
| 13 |
+
- type: textarea
|
| 14 |
+
id: what-happened
|
| 15 |
+
attributes:
|
| 16 |
+
label: What did you do when it broke?
|
| 17 |
+
description: Describe the steps you took that led to the bug
|
| 18 |
+
placeholder: |
|
| 19 |
+
1. I went to the Notebooks page
|
| 20 |
+
2. I clicked on "Create New Notebook"
|
| 21 |
+
3. I filled in the form and clicked "Save"
|
| 22 |
+
4. Then the error occurred...
|
| 23 |
+
validations:
|
| 24 |
+
required: true
|
| 25 |
+
|
| 26 |
+
- type: textarea
|
| 27 |
+
id: how-broke
|
| 28 |
+
attributes:
|
| 29 |
+
label: How did it break?
|
| 30 |
+
description: What happened that was unexpected? What did you expect to happen instead?
|
| 31 |
+
placeholder: |
|
| 32 |
+
Expected: The notebook should be created and I should see it in the list
|
| 33 |
+
Actual: I got an error message saying "Failed to create notebook"
|
| 34 |
+
validations:
|
| 35 |
+
required: true
|
| 36 |
+
|
| 37 |
+
- type: textarea
|
| 38 |
+
id: logs-screenshots
|
| 39 |
+
attributes:
|
| 40 |
+
label: Logs or Screenshots
|
| 41 |
+
description: |
|
| 42 |
+
Please provide any error messages, logs, or screenshots that might help us understand the issue.
|
| 43 |
+
|
| 44 |
+
**How to get logs:**
|
| 45 |
+
- Docker: `docker compose logs -f open_notebook`
|
| 46 |
+
- Check browser console (F12 → Console tab)
|
| 47 |
+
placeholder: |
|
| 48 |
+
Paste logs here or drag and drop screenshots.
|
| 49 |
+
|
| 50 |
+
Error messages, stack traces, or browser console errors are very helpful!
|
| 51 |
+
validations:
|
| 52 |
+
required: false
|
| 53 |
+
|
| 54 |
+
- type: dropdown
|
| 55 |
+
id: version
|
| 56 |
+
attributes:
|
| 57 |
+
label: Open Notebook Version
|
| 58 |
+
description: Which version are you using?
|
| 59 |
+
options:
|
| 60 |
+
- v1-latest (Docker)
|
| 61 |
+
- v1-latest-single (Docker)
|
| 62 |
+
- Latest from main branch
|
| 63 |
+
- Other (please specify in additional context)
|
| 64 |
+
validations:
|
| 65 |
+
required: true
|
| 66 |
+
|
| 67 |
+
- type: textarea
|
| 68 |
+
id: environment
|
| 69 |
+
attributes:
|
| 70 |
+
label: Environment
|
| 71 |
+
description: What environment are you running in?
|
| 72 |
+
placeholder: |
|
| 73 |
+
- OS: Ubuntu 22.04 / Windows 11 / macOS 14
|
| 74 |
+
- Browser: Chrome 120
|
| 75 |
+
validations:
|
| 76 |
+
required: false
|
| 77 |
+
|
| 78 |
+
- type: textarea
|
| 79 |
+
id: additional-context
|
| 80 |
+
attributes:
|
| 81 |
+
label: Additional Context
|
| 82 |
+
description: Any other information that might be helpful
|
| 83 |
+
placeholder: "This started happening after I upgraded to v1.5.0..."
|
| 84 |
+
validations:
|
| 85 |
+
required: false
|
| 86 |
+
|
| 87 |
+
- type: checkboxes
|
| 88 |
+
id: willing-to-contribute
|
| 89 |
+
attributes:
|
| 90 |
+
label: Contribution
|
| 91 |
+
description: Would you like to work on fixing this bug?
|
| 92 |
+
options:
|
| 93 |
+
- label: I am a developer and would like to work on fixing this issue (pending maintainer approval)
|
| 94 |
+
required: false
|
| 95 |
+
|
| 96 |
+
- type: markdown
|
| 97 |
+
attributes:
|
| 98 |
+
value: |
|
| 99 |
+
---
|
| 100 |
+
**Next Steps:**
|
| 101 |
+
1. A maintainer will review your bug report
|
| 102 |
+
2. If you checked the box above and want to fix it, please propose your solution approach
|
| 103 |
+
3. Wait for assignment before starting development
|
| 104 |
+
4. See our [Contributing Guide](https://github.com/lfnovo/open-notebook/blob/main/CONTRIBUTING.md) for more details
|
.github/ISSUE_TEMPLATE/config.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blank_issues_enabled: false
|
| 2 |
+
contact_links:
|
| 3 |
+
- name: 💬 Discord Community
|
| 4 |
+
url: https://discord.gg/37XJPXfz2w
|
| 5 |
+
about: Get help from the community and share ideas
|
| 6 |
+
- name: 🤖 Installation Assistant (ChatGPT)
|
| 7 |
+
url: https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant
|
| 8 |
+
about: CustomGPT that knows all our docs. Really useful. Try it.
|
| 9 |
+
- name: 📚 Documentation
|
| 10 |
+
url: https://github.com/lfnovo/open-notebook/tree/main/docs
|
| 11 |
+
about: Browse our comprehensive documentation
|
.github/ISSUE_TEMPLATE/feature_request.yml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: ✨ Feature Suggestion
|
| 2 |
+
description: Suggest a new feature or improvement for Open Notebook
|
| 3 |
+
title: "[Feature]: "
|
| 4 |
+
labels: ["enhancement", "needs-triage"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
Thanks for taking the time to suggest a feature! Your ideas help make Open Notebook better for everyone.
|
| 10 |
+
|
| 11 |
+
- type: textarea
|
| 12 |
+
id: feature-description
|
| 13 |
+
attributes:
|
| 14 |
+
label: Feature Description
|
| 15 |
+
description: What feature would you like to see added or improved?
|
| 16 |
+
placeholder: "I would like to be able to..."
|
| 17 |
+
validations:
|
| 18 |
+
required: true
|
| 19 |
+
|
| 20 |
+
- type: textarea
|
| 21 |
+
id: why-helpful
|
| 22 |
+
attributes:
|
| 23 |
+
label: Why would this be helpful?
|
| 24 |
+
description: Explain how this feature would benefit you and other users
|
| 25 |
+
placeholder: "This would help because..."
|
| 26 |
+
validations:
|
| 27 |
+
required: true
|
| 28 |
+
|
| 29 |
+
- type: textarea
|
| 30 |
+
id: proposed-solution
|
| 31 |
+
attributes:
|
| 32 |
+
label: Proposed Solution (Optional)
|
| 33 |
+
description: If you have ideas on how to implement this feature, please share them
|
| 34 |
+
placeholder: "This could be implemented by..."
|
| 35 |
+
validations:
|
| 36 |
+
required: false
|
| 37 |
+
|
| 38 |
+
- type: textarea
|
| 39 |
+
id: additional-context
|
| 40 |
+
attributes:
|
| 41 |
+
label: Additional Context
|
| 42 |
+
description: Any other context, screenshots, or examples that might be helpful
|
| 43 |
+
placeholder: "For example, other tools do this by..."
|
| 44 |
+
validations:
|
| 45 |
+
required: false
|
| 46 |
+
|
| 47 |
+
- type: checkboxes
|
| 48 |
+
id: willing-to-contribute
|
| 49 |
+
attributes:
|
| 50 |
+
label: Contribution
|
| 51 |
+
description: Would you like to work on implementing this feature?
|
| 52 |
+
options:
|
| 53 |
+
- label: I am a developer and would like to work on implementing this feature (pending maintainer approval)
|
| 54 |
+
required: false
|
| 55 |
+
|
| 56 |
+
- type: markdown
|
| 57 |
+
attributes:
|
| 58 |
+
value: |
|
| 59 |
+
---
|
| 60 |
+
**Next Steps:**
|
| 61 |
+
1. A maintainer will review your feature request
|
| 62 |
+
2. If approved and you checked the box above, the issue will be assigned to you
|
| 63 |
+
3. Please wait for assignment before starting development
|
| 64 |
+
4. See our [Contributing Guide](https://github.com/lfnovo/open-notebook/blob/main/CONTRIBUTING.md) for more details
|
| 65 |
+
|
.github/ISSUE_TEMPLATE/installation_issue.yml
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 🔧 Installation Issue
|
| 2 |
+
description: Report problems with installation, setup, or connectivity
|
| 3 |
+
title: "[Install]: "
|
| 4 |
+
labels: ["installation", "needs-triage"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
## ⚠️ Before You Continue
|
| 10 |
+
|
| 11 |
+
**Please try these resources first:**
|
| 12 |
+
|
| 13 |
+
1. 🤖 **[Installation Assistant ChatGPT](https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant)** - Our AI assistant can help you troubleshoot most installation issues instantly!
|
| 14 |
+
|
| 15 |
+
2. 📚 **[Installation Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/getting-started/installation.md)** - Comprehensive setup instructions
|
| 16 |
+
|
| 17 |
+
3. 🐋 **[Docker Deployment Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/deployment/docker.md)** - Detailed Docker setup
|
| 18 |
+
|
| 19 |
+
4. 🦙 **Ollama Issues?** Read our [Ollama Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/features/ollama.md) first
|
| 20 |
+
|
| 21 |
+
5. 💬 **[Discord Community](https://discord.gg/37XJPXfz2w)** - Get real-time help from the community
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
If you've tried the above and still need help, please fill out the form below with as much detail as possible.
|
| 26 |
+
|
| 27 |
+
- type: dropdown
|
| 28 |
+
id: installation-method
|
| 29 |
+
attributes:
|
| 30 |
+
label: Installation Method
|
| 31 |
+
description: How are you trying to install Open Notebook?
|
| 32 |
+
options:
|
| 33 |
+
- Docker (single container - v1-latest-single)
|
| 34 |
+
- Docker (multi-container - docker-compose)
|
| 35 |
+
- Local development (make start-all)
|
| 36 |
+
- Other (please specify below)
|
| 37 |
+
validations:
|
| 38 |
+
required: true
|
| 39 |
+
|
| 40 |
+
- type: textarea
|
| 41 |
+
id: issue-description
|
| 42 |
+
attributes:
|
| 43 |
+
label: What is the issue?
|
| 44 |
+
description: Describe the installation or setup problem you're experiencing
|
| 45 |
+
placeholder: |
|
| 46 |
+
Example: "I can't connect to the database" or "The container won't start" or "Getting 404 errors when accessing the UI"
|
| 47 |
+
validations:
|
| 48 |
+
required: true
|
| 49 |
+
|
| 50 |
+
- type: textarea
|
| 51 |
+
id: logs
|
| 52 |
+
attributes:
|
| 53 |
+
label: Logs
|
| 54 |
+
description: |
|
| 55 |
+
Please provide relevant logs. **This is very important for diagnosing issues!**
|
| 56 |
+
|
| 57 |
+
**How to get logs:**
|
| 58 |
+
- Docker single container: `docker logs open-notebook`
|
| 59 |
+
- Docker Compose: `docker compose logs -f`
|
| 60 |
+
- Specific service: `docker compose logs -f open_notebook`
|
| 61 |
+
placeholder: |
|
| 62 |
+
Paste your logs here. Include the full error message and stack trace if available.
|
| 63 |
+
render: shell
|
| 64 |
+
validations:
|
| 65 |
+
required: false
|
| 66 |
+
|
| 67 |
+
- type: textarea
|
| 68 |
+
id: docker-compose
|
| 69 |
+
attributes:
|
| 70 |
+
label: Docker Compose Configuration
|
| 71 |
+
description: |
|
| 72 |
+
If using Docker Compose, please paste your `docker-compose.yml` file here.
|
| 73 |
+
|
| 74 |
+
**⚠️ IMPORTANT: Redact any sensitive information (API keys, passwords, etc.)**
|
| 75 |
+
placeholder: |
|
| 76 |
+
services:
|
| 77 |
+
open_notebook:
|
| 78 |
+
image: lfnovo/open_notebook:v1-latest-single
|
| 79 |
+
ports:
|
| 80 |
+
- "8502:8502"
|
| 81 |
+
- "5055:5055"
|
| 82 |
+
environment:
|
| 83 |
+
- OPENAI_API_KEY=sk-***REDACTED***
|
| 84 |
+
...
|
| 85 |
+
render: yaml
|
| 86 |
+
validations:
|
| 87 |
+
required: false
|
| 88 |
+
|
| 89 |
+
- type: textarea
|
| 90 |
+
id: env-file
|
| 91 |
+
attributes:
|
| 92 |
+
label: Environment File
|
| 93 |
+
description: |
|
| 94 |
+
If using an `.env` or `docker.env` file, please paste it here.
|
| 95 |
+
|
| 96 |
+
**⚠️ IMPORTANT: REDACT ALL API KEYS AND PASSWORDS!**
|
| 97 |
+
placeholder: |
|
| 98 |
+
SURREAL_URL=ws://surrealdb:8000/rpc
|
| 99 |
+
SURREAL_USER=root
|
| 100 |
+
SURREAL_PASSWORD=***REDACTED***
|
| 101 |
+
OPENAI_API_KEY=sk-***REDACTED***
|
| 102 |
+
ANTHROPIC_API_KEY=sk-ant-***REDACTED***
|
| 103 |
+
render: shell
|
| 104 |
+
validations:
|
| 105 |
+
required: false
|
| 106 |
+
|
| 107 |
+
- type: textarea
|
| 108 |
+
id: system-info
|
| 109 |
+
attributes:
|
| 110 |
+
label: System Information
|
| 111 |
+
description: Tell us about your setup
|
| 112 |
+
placeholder: |
|
| 113 |
+
- Operating System: Ubuntu 22.04 / Windows 11 / macOS 14
|
| 114 |
+
- Docker version: `docker --version`
|
| 115 |
+
- Docker Compose version: `docker compose version`
|
| 116 |
+
- Architecture: amd64 / arm64 (Apple Silicon)
|
| 117 |
+
- Available disk space: `df -h`
|
| 118 |
+
- Available memory: `free -h` (Linux) or Activity Monitor (Mac)
|
| 119 |
+
validations:
|
| 120 |
+
required: false
|
| 121 |
+
|
| 122 |
+
- type: textarea
|
| 123 |
+
id: additional-context
|
| 124 |
+
attributes:
|
| 125 |
+
label: Additional Context
|
| 126 |
+
description: Any other information that might be helpful
|
| 127 |
+
placeholder: |
|
| 128 |
+
- Are you behind a corporate proxy or firewall?
|
| 129 |
+
- Are you using a VPN?
|
| 130 |
+
- Have you made any custom modifications?
|
| 131 |
+
- Did this work before and suddenly break?
|
| 132 |
+
validations:
|
| 133 |
+
required: false
|
| 134 |
+
|
| 135 |
+
- type: checkboxes
|
| 136 |
+
id: checklist
|
| 137 |
+
attributes:
|
| 138 |
+
label: Pre-submission Checklist
|
| 139 |
+
description: Please confirm you've tried these steps
|
| 140 |
+
options:
|
| 141 |
+
- label: I tried the [Installation Assistant ChatGPT](https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant)
|
| 142 |
+
required: false
|
| 143 |
+
- label: I read the relevant documentation ([Installation Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/getting-started/installation.md) or [Ollama Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/features/ollama.md))
|
| 144 |
+
required: false
|
| 145 |
+
- label: I searched existing issues to see if this was already reported
|
| 146 |
+
required: true
|
| 147 |
+
- label: I redacted all sensitive information (API keys, passwords, etc.)
|
| 148 |
+
required: true
|
.github/pull_request_template.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Description
|
| 2 |
+
|
| 3 |
+
<!-- Provide a clear and concise description of what this PR does -->
|
| 4 |
+
|
| 5 |
+
## Related Issue
|
| 6 |
+
|
| 7 |
+
<!-- This PR should be linked to an approved issue. If not, please create an issue first. -->
|
| 8 |
+
|
| 9 |
+
Fixes #<!-- issue number -->
|
| 10 |
+
|
| 11 |
+
## Type of Change
|
| 12 |
+
|
| 13 |
+
<!-- Mark the relevant option with an "x" -->
|
| 14 |
+
|
| 15 |
+
- [ ] Bug fix (non-breaking change that fixes an issue)
|
| 16 |
+
- [ ] New feature (non-breaking change that adds functionality)
|
| 17 |
+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
| 18 |
+
- [ ] Documentation update
|
| 19 |
+
- [ ] Code refactoring (no functional changes)
|
| 20 |
+
- [ ] Performance improvement
|
| 21 |
+
- [ ] Test coverage improvement
|
| 22 |
+
|
| 23 |
+
## How Has This Been Tested?
|
| 24 |
+
|
| 25 |
+
<!-- Describe the tests you ran and/or how you verified your changes work -->
|
| 26 |
+
|
| 27 |
+
- [ ] Tested locally with Docker
|
| 28 |
+
- [ ] Tested locally with development setup
|
| 29 |
+
- [ ] Added new unit tests
|
| 30 |
+
- [ ] Existing tests pass (`uv run pytest`)
|
| 31 |
+
- [ ] Manual testing performed (describe below)
|
| 32 |
+
|
| 33 |
+
**Test Details:**
|
| 34 |
+
<!-- Describe your testing approach -->
|
| 35 |
+
|
| 36 |
+
## Design Alignment
|
| 37 |
+
|
| 38 |
+
<!-- This section helps ensure your PR aligns with our project vision -->
|
| 39 |
+
|
| 40 |
+
**Which design principles does this PR support?** (See [DESIGN_PRINCIPLES.md](../DESIGN_PRINCIPLES.md))
|
| 41 |
+
|
| 42 |
+
- [ ] Privacy First
|
| 43 |
+
- [ ] Simplicity Over Features
|
| 44 |
+
- [ ] API-First Architecture
|
| 45 |
+
- [ ] Multi-Provider Flexibility
|
| 46 |
+
- [ ] Extensibility Through Standards
|
| 47 |
+
- [ ] Async-First for Performance
|
| 48 |
+
|
| 49 |
+
**Explanation:**
|
| 50 |
+
<!-- Brief explanation of how your changes align with these principles -->
|
| 51 |
+
|
| 52 |
+
## Checklist
|
| 53 |
+
|
| 54 |
+
<!-- Mark completed items with an "x" -->
|
| 55 |
+
|
| 56 |
+
### Code Quality
|
| 57 |
+
- [ ] My code follows PEP 8 style guidelines (Python)
|
| 58 |
+
- [ ] My code follows TypeScript best practices (Frontend)
|
| 59 |
+
- [ ] I have added type hints to my code (Python)
|
| 60 |
+
- [ ] I have added JSDoc comments where appropriate (TypeScript)
|
| 61 |
+
- [ ] I have performed a self-review of my code
|
| 62 |
+
- [ ] I have commented my code, particularly in hard-to-understand areas
|
| 63 |
+
- [ ] My changes generate no new warnings or errors
|
| 64 |
+
|
| 65 |
+
### Testing
|
| 66 |
+
- [ ] I have added tests that prove my fix is effective or that my feature works
|
| 67 |
+
- [ ] New and existing unit tests pass locally with my changes
|
| 68 |
+
- [ ] I ran linting: `make ruff` or `ruff check . --fix`
|
| 69 |
+
- [ ] I ran type checking: `make lint` or `uv run python -m mypy .`
|
| 70 |
+
|
| 71 |
+
### Documentation
|
| 72 |
+
- [ ] I have updated the relevant documentation in `/docs` (if applicable)
|
| 73 |
+
- [ ] I have added/updated docstrings for new/modified functions
|
| 74 |
+
- [ ] I have updated the API documentation (if API changes were made)
|
| 75 |
+
- [ ] I have added comments to complex logic
|
| 76 |
+
|
| 77 |
+
### Database Changes
|
| 78 |
+
- [ ] I have created migration scripts for any database schema changes (in `/migrations`)
|
| 79 |
+
- [ ] Migration includes both up and down scripts
|
| 80 |
+
- [ ] Migration has been tested locally
|
| 81 |
+
|
| 82 |
+
### Breaking Changes
|
| 83 |
+
- [ ] This PR includes breaking changes
|
| 84 |
+
- [ ] I have documented the migration path for users
|
| 85 |
+
- [ ] I have updated MIGRATION.md (if applicable)
|
| 86 |
+
|
| 87 |
+
## Screenshots (if applicable)
|
| 88 |
+
|
| 89 |
+
<!-- Add screenshots for UI changes -->
|
| 90 |
+
|
| 91 |
+
## Additional Context
|
| 92 |
+
|
| 93 |
+
<!-- Add any other context about the PR here -->
|
| 94 |
+
|
| 95 |
+
## Pre-Submission Verification
|
| 96 |
+
|
| 97 |
+
Before submitting, please verify:
|
| 98 |
+
|
| 99 |
+
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md)
|
| 100 |
+
- [ ] I have read [DESIGN_PRINCIPLES.md](../DESIGN_PRINCIPLES.md)
|
| 101 |
+
- [ ] This PR addresses an approved issue that was assigned to me
|
| 102 |
+
- [ ] I have not included unrelated changes in this PR
|
| 103 |
+
- [ ] My PR title follows conventional commits format (e.g., "feat: add user authentication")
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
**Thank you for contributing to Open Notebook!** 🎉
|
.github/workflows/build-and-release.yml
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build and Release
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
push_latest:
|
| 7 |
+
description: 'Also push v1-latest tags'
|
| 8 |
+
required: true
|
| 9 |
+
default: false
|
| 10 |
+
type: boolean
|
| 11 |
+
release:
|
| 12 |
+
types: [published]
|
| 13 |
+
|
| 14 |
+
permissions:
|
| 15 |
+
contents: read
|
| 16 |
+
packages: write
|
| 17 |
+
|
| 18 |
+
env:
|
| 19 |
+
GHCR_IMAGE: ghcr.io/lfnovo/open-notebook
|
| 20 |
+
DOCKERHUB_IMAGE: lfnovo/open_notebook
|
| 21 |
+
|
| 22 |
+
jobs:
|
| 23 |
+
extract-version:
|
| 24 |
+
runs-on: ubuntu-latest
|
| 25 |
+
outputs:
|
| 26 |
+
version: ${{ steps.version.outputs.version }}
|
| 27 |
+
has_dockerhub_secrets: ${{ steps.check.outputs.has_dockerhub_secrets }}
|
| 28 |
+
steps:
|
| 29 |
+
- name: Checkout
|
| 30 |
+
uses: actions/checkout@v4
|
| 31 |
+
|
| 32 |
+
- name: Extract version from pyproject.toml
|
| 33 |
+
id: version
|
| 34 |
+
run: |
|
| 35 |
+
VERSION=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
|
| 36 |
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
| 37 |
+
echo "Extracted version: $VERSION"
|
| 38 |
+
|
| 39 |
+
- name: Check for Docker Hub credentials
|
| 40 |
+
id: check
|
| 41 |
+
env:
|
| 42 |
+
SECRET_DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
| 43 |
+
SECRET_DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
| 44 |
+
run: |
|
| 45 |
+
if [[ -n ""$SECRET_DOCKER_USERNAME"" && -n ""$SECRET_DOCKER_PASSWORD"" ]]; then
|
| 46 |
+
echo "has_dockerhub_secrets=true" >> $GITHUB_OUTPUT
|
| 47 |
+
echo "Docker Hub credentials available"
|
| 48 |
+
else
|
| 49 |
+
echo "has_dockerhub_secrets=false" >> $GITHUB_OUTPUT
|
| 50 |
+
echo "Docker Hub credentials not available - will only push to GHCR"
|
| 51 |
+
fi
|
| 52 |
+
|
| 53 |
+
build-regular:
|
| 54 |
+
needs: extract-version
|
| 55 |
+
runs-on: ubuntu-latest
|
| 56 |
+
steps:
|
| 57 |
+
- name: Checkout
|
| 58 |
+
uses: actions/checkout@v4
|
| 59 |
+
|
| 60 |
+
- name: Free up disk space
|
| 61 |
+
run: |
|
| 62 |
+
sudo rm -rf /usr/share/dotnet
|
| 63 |
+
sudo rm -rf /usr/local/lib/android
|
| 64 |
+
sudo rm -rf /opt/ghc
|
| 65 |
+
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
| 66 |
+
sudo docker image prune --all --force
|
| 67 |
+
df -h
|
| 68 |
+
|
| 69 |
+
- name: Set up Docker Buildx
|
| 70 |
+
uses: docker/setup-buildx-action@v3
|
| 71 |
+
|
| 72 |
+
- name: Login to GitHub Container Registry
|
| 73 |
+
uses: docker/login-action@v3
|
| 74 |
+
with:
|
| 75 |
+
registry: ghcr.io
|
| 76 |
+
username: ${{ github.actor }}
|
| 77 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 78 |
+
|
| 79 |
+
- name: Login to Docker Hub
|
| 80 |
+
if: needs.extract-version.outputs.has_dockerhub_secrets == 'true'
|
| 81 |
+
uses: docker/login-action@v3
|
| 82 |
+
with:
|
| 83 |
+
username: ${{ secrets.DOCKER_USERNAME }}
|
| 84 |
+
password: ${{ secrets.DOCKER_PASSWORD }}
|
| 85 |
+
|
| 86 |
+
- name: Cache Docker layers
|
| 87 |
+
uses: actions/cache@v3
|
| 88 |
+
with:
|
| 89 |
+
path: /tmp/.buildx-cache
|
| 90 |
+
key: ${{ runner.os }}-buildx-regular-${{ github.sha }}
|
| 91 |
+
restore-keys: |
|
| 92 |
+
${{ runner.os }}-buildx-regular-
|
| 93 |
+
|
| 94 |
+
- name: Prepare Docker tags for regular build
|
| 95 |
+
id: tags-regular
|
| 96 |
+
env:
|
| 97 |
+
ENV_GHCR_IMAGE: ${{ env.GHCR_IMAGE }}
|
| 98 |
+
GITHUB_EVENT_INPUTS_PUSH_LATEST: ${{ github.event.inputs.push_latest }}
|
| 99 |
+
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
| 100 |
+
GITHUB_EVENT_RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
|
| 101 |
+
ENV_DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
|
| 102 |
+
run: |
|
| 103 |
+
TAGS=""$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}"
|
| 104 |
+
|
| 105 |
+
# Determine if we should push latest tags
|
| 106 |
+
PUSH_LATEST=""$GITHUB_EVENT_INPUTS_PUSH_LATEST""
|
| 107 |
+
if [[ -z "$PUSH_LATEST" ]]; then
|
| 108 |
+
PUSH_LATEST="false"
|
| 109 |
+
fi
|
| 110 |
+
|
| 111 |
+
# Add GHCR latest tag if requested or for non-prerelease releases
|
| 112 |
+
if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
|
| 113 |
+
TAGS="${TAGS},"$ENV_GHCR_IMAGE":v1-latest"
|
| 114 |
+
fi
|
| 115 |
+
|
| 116 |
+
# Add Docker Hub tags if credentials available
|
| 117 |
+
if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
|
| 118 |
+
TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}"
|
| 119 |
+
|
| 120 |
+
if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
|
| 121 |
+
TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":v1-latest"
|
| 122 |
+
fi
|
| 123 |
+
fi
|
| 124 |
+
|
| 125 |
+
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
| 126 |
+
echo "Generated tags: ${TAGS}"
|
| 127 |
+
|
| 128 |
+
- name: Build and push regular image
|
| 129 |
+
uses: docker/build-push-action@v5
|
| 130 |
+
with:
|
| 131 |
+
context: .
|
| 132 |
+
file: ./Dockerfile
|
| 133 |
+
platforms: linux/amd64,linux/arm64
|
| 134 |
+
push: true
|
| 135 |
+
tags: ${{ steps.tags-regular.outputs.tags }}
|
| 136 |
+
cache-from: type=local,src=/tmp/.buildx-cache
|
| 137 |
+
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
| 138 |
+
|
| 139 |
+
- name: Move cache
|
| 140 |
+
run: |
|
| 141 |
+
rm -rf /tmp/.buildx-cache
|
| 142 |
+
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
| 143 |
+
|
| 144 |
+
build-single:
|
| 145 |
+
needs: extract-version
|
| 146 |
+
runs-on: ubuntu-latest
|
| 147 |
+
steps:
|
| 148 |
+
- name: Checkout
|
| 149 |
+
uses: actions/checkout@v4
|
| 150 |
+
|
| 151 |
+
- name: Free up disk space
|
| 152 |
+
run: |
|
| 153 |
+
sudo rm -rf /usr/share/dotnet
|
| 154 |
+
sudo rm -rf /usr/local/lib/android
|
| 155 |
+
sudo rm -rf /opt/ghc
|
| 156 |
+
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
| 157 |
+
sudo docker image prune --all --force
|
| 158 |
+
df -h
|
| 159 |
+
|
| 160 |
+
- name: Set up Docker Buildx
|
| 161 |
+
uses: docker/setup-buildx-action@v3
|
| 162 |
+
|
| 163 |
+
- name: Login to GitHub Container Registry
|
| 164 |
+
uses: docker/login-action@v3
|
| 165 |
+
with:
|
| 166 |
+
registry: ghcr.io
|
| 167 |
+
username: ${{ github.actor }}
|
| 168 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 169 |
+
|
| 170 |
+
- name: Login to Docker Hub
|
| 171 |
+
if: needs.extract-version.outputs.has_dockerhub_secrets == 'true'
|
| 172 |
+
uses: docker/login-action@v3
|
| 173 |
+
with:
|
| 174 |
+
username: ${{ secrets.DOCKER_USERNAME }}
|
| 175 |
+
password: ${{ secrets.DOCKER_PASSWORD }}
|
| 176 |
+
|
| 177 |
+
- name: Cache Docker layers
|
| 178 |
+
uses: actions/cache@v3
|
| 179 |
+
with:
|
| 180 |
+
path: /tmp/.buildx-cache-single
|
| 181 |
+
key: ${{ runner.os }}-buildx-single-${{ github.sha }}
|
| 182 |
+
restore-keys: |
|
| 183 |
+
${{ runner.os }}-buildx-single-
|
| 184 |
+
|
| 185 |
+
- name: Prepare Docker tags for single build
|
| 186 |
+
id: tags-single
|
| 187 |
+
env:
|
| 188 |
+
ENV_GHCR_IMAGE: ${{ env.GHCR_IMAGE }}
|
| 189 |
+
GITHUB_EVENT_INPUTS_PUSH_LATEST: ${{ github.event.inputs.push_latest }}
|
| 190 |
+
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
| 191 |
+
GITHUB_EVENT_RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
|
| 192 |
+
ENV_DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
|
| 193 |
+
run: |
|
| 194 |
+
TAGS=""$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}-single"
|
| 195 |
+
|
| 196 |
+
# Determine if we should push latest tags
|
| 197 |
+
PUSH_LATEST=""$GITHUB_EVENT_INPUTS_PUSH_LATEST""
|
| 198 |
+
if [[ -z "$PUSH_LATEST" ]]; then
|
| 199 |
+
PUSH_LATEST="false"
|
| 200 |
+
fi
|
| 201 |
+
|
| 202 |
+
# Add GHCR latest tag if requested or for non-prerelease releases
|
| 203 |
+
if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
|
| 204 |
+
TAGS="${TAGS},"$ENV_GHCR_IMAGE":v1-latest-single"
|
| 205 |
+
fi
|
| 206 |
+
|
| 207 |
+
# Add Docker Hub tags if credentials available
|
| 208 |
+
if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
|
| 209 |
+
TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}-single"
|
| 210 |
+
|
| 211 |
+
if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
|
| 212 |
+
TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":v1-latest-single"
|
| 213 |
+
fi
|
| 214 |
+
fi
|
| 215 |
+
|
| 216 |
+
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
| 217 |
+
echo "Generated tags: ${TAGS}"
|
| 218 |
+
|
| 219 |
+
- name: Build and push single-container image
|
| 220 |
+
uses: docker/build-push-action@v5
|
| 221 |
+
with:
|
| 222 |
+
context: .
|
| 223 |
+
file: ./Dockerfile.single
|
| 224 |
+
platforms: linux/amd64,linux/arm64
|
| 225 |
+
push: true
|
| 226 |
+
tags: ${{ steps.tags-single.outputs.tags }}
|
| 227 |
+
cache-from: type=local,src=/tmp/.buildx-cache-single
|
| 228 |
+
cache-to: type=local,dest=/tmp/.buildx-cache-single-new,mode=max
|
| 229 |
+
|
| 230 |
+
- name: Move cache
|
| 231 |
+
run: |
|
| 232 |
+
rm -rf /tmp/.buildx-cache-single
|
| 233 |
+
mv /tmp/.buildx-cache-single-new /tmp/.buildx-cache-single
|
| 234 |
+
|
| 235 |
+
summary:
|
| 236 |
+
needs: [extract-version, build-regular, build-single]
|
| 237 |
+
runs-on: ubuntu-latest
|
| 238 |
+
if: always()
|
| 239 |
+
steps:
|
| 240 |
+
- name: Build Summary
|
| 241 |
+
env:
|
| 242 |
+
GITHUB_EVENT_INPUTS_PUSH_LATEST_____FALSE_: ${{ github.event.inputs.push_latest || 'false' }}
|
| 243 |
+
ENV_GHCR_IMAGE: ${{ env.GHCR_IMAGE }}
|
| 244 |
+
ENV_DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
|
| 245 |
+
GITHUB_EVENT_INPUTS_PUSH_LATEST: ${{ github.event.inputs.push_latest }}
|
| 246 |
+
run: |
|
| 247 |
+
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
| 248 |
+
echo "**Version:** ${{ needs.extract-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
| 249 |
+
echo "**Push v1-Latest:** "$GITHUB_EVENT_INPUTS_PUSH_LATEST_____FALSE_"" >> $GITHUB_STEP_SUMMARY
|
| 250 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 251 |
+
echo "### Registries:" >> $GITHUB_STEP_SUMMARY
|
| 252 |
+
echo "✅ **GHCR:** \`"$ENV_GHCR_IMAGE"\`" >> $GITHUB_STEP_SUMMARY
|
| 253 |
+
if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
|
| 254 |
+
echo "✅ **Docker Hub:** \`"$ENV_DOCKERHUB_IMAGE"\`" >> $GITHUB_STEP_SUMMARY
|
| 255 |
+
else
|
| 256 |
+
echo "⏭️ **Docker Hub:** Skipped (credentials not configured)" >> $GITHUB_STEP_SUMMARY
|
| 257 |
+
fi
|
| 258 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 259 |
+
echo "### Images Built:" >> $GITHUB_STEP_SUMMARY
|
| 260 |
+
|
| 261 |
+
if [[ "${{ needs.build-regular.result }}" == "success" ]]; then
|
| 262 |
+
echo "✅ **Regular (GHCR):** \`"$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
|
| 263 |
+
if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
|
| 264 |
+
echo "✅ **Regular v1-Latest (GHCR):** \`"$ENV_GHCR_IMAGE":v1-latest\`" >> $GITHUB_STEP_SUMMARY
|
| 265 |
+
fi
|
| 266 |
+
if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
|
| 267 |
+
echo "✅ **Regular (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
|
| 268 |
+
if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
|
| 269 |
+
echo "✅ **Regular v1-Latest (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":v1-latest\`" >> $GITHUB_STEP_SUMMARY
|
| 270 |
+
fi
|
| 271 |
+
fi
|
| 272 |
+
elif [[ "${{ needs.build-regular.result }}" == "skipped" ]]; then
|
| 273 |
+
echo "⏭️ **Regular:** Skipped" >> $GITHUB_STEP_SUMMARY
|
| 274 |
+
else
|
| 275 |
+
echo "❌ **Regular:** Failed" >> $GITHUB_STEP_SUMMARY
|
| 276 |
+
fi
|
| 277 |
+
|
| 278 |
+
if [[ "${{ needs.build-single.result }}" == "success" ]]; then
|
| 279 |
+
echo "✅ **Single (GHCR):** \`"$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}-single\`" >> $GITHUB_STEP_SUMMARY
|
| 280 |
+
if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
|
| 281 |
+
echo "✅ **Single v1-Latest (GHCR):** \`"$ENV_GHCR_IMAGE":v1-latest-single\`" >> $GITHUB_STEP_SUMMARY
|
| 282 |
+
fi
|
| 283 |
+
if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
|
| 284 |
+
echo "✅ **Single (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}-single\`" >> $GITHUB_STEP_SUMMARY
|
| 285 |
+
if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
|
| 286 |
+
echo "✅ **Single v1-Latest (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":v1-latest-single\`" >> $GITHUB_STEP_SUMMARY
|
| 287 |
+
fi
|
| 288 |
+
fi
|
| 289 |
+
elif [[ "${{ needs.build-single.result }}" == "skipped" ]]; then
|
| 290 |
+
echo "⏭️ **Single:** Skipped" >> $GITHUB_STEP_SUMMARY
|
| 291 |
+
else
|
| 292 |
+
echo "❌ **Single:** Failed" >> $GITHUB_STEP_SUMMARY
|
| 293 |
+
fi
|
| 294 |
+
|
| 295 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 296 |
+
echo "### Platforms:" >> $GITHUB_STEP_SUMMARY
|
| 297 |
+
echo "- linux/amd64" >> $GITHUB_STEP_SUMMARY
|
| 298 |
+
echo "- linux/arm64" >> $GITHUB_STEP_SUMMARY
|
.github/workflows/build-dev.yml
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Development Build
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
branches: [ main ]
|
| 6 |
+
push:
|
| 7 |
+
branches: [ main ]
|
| 8 |
+
paths-ignore:
|
| 9 |
+
- '**.md'
|
| 10 |
+
- 'docs/**'
|
| 11 |
+
- 'notebooks/**'
|
| 12 |
+
- '.github/workflows/claude*.yml'
|
| 13 |
+
workflow_dispatch:
|
| 14 |
+
inputs:
|
| 15 |
+
dockerfile:
|
| 16 |
+
description: 'Dockerfile to test'
|
| 17 |
+
required: true
|
| 18 |
+
default: 'both'
|
| 19 |
+
type: choice
|
| 20 |
+
options:
|
| 21 |
+
- both
|
| 22 |
+
- regular
|
| 23 |
+
- single
|
| 24 |
+
platform:
|
| 25 |
+
description: 'Platform to build'
|
| 26 |
+
required: true
|
| 27 |
+
default: 'linux/amd64'
|
| 28 |
+
type: choice
|
| 29 |
+
options:
|
| 30 |
+
- linux/amd64
|
| 31 |
+
- linux/arm64
|
| 32 |
+
- linux/amd64,linux/arm64
|
| 33 |
+
|
| 34 |
+
env:
|
| 35 |
+
REGISTRY: docker.io
|
| 36 |
+
IMAGE_NAME: lfnovo/open_notebook
|
| 37 |
+
|
| 38 |
+
jobs:
|
| 39 |
+
extract-version:
|
| 40 |
+
runs-on: ubuntu-latest
|
| 41 |
+
outputs:
|
| 42 |
+
version: ${{ steps.version.outputs.version }}
|
| 43 |
+
steps:
|
| 44 |
+
- name: Checkout
|
| 45 |
+
uses: actions/checkout@v4
|
| 46 |
+
|
| 47 |
+
- name: Extract version from pyproject.toml
|
| 48 |
+
id: version
|
| 49 |
+
run: |
|
| 50 |
+
VERSION=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
|
| 51 |
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
| 52 |
+
echo "Extracted version: $VERSION"
|
| 53 |
+
|
| 54 |
+
test-build-regular:
|
| 55 |
+
needs: extract-version
|
| 56 |
+
runs-on: ubuntu-latest
|
| 57 |
+
if: github.event.inputs.dockerfile == 'regular' || github.event.inputs.dockerfile == 'both' || github.event_name != 'workflow_dispatch'
|
| 58 |
+
steps:
|
| 59 |
+
- name: Checkout
|
| 60 |
+
uses: actions/checkout@v4
|
| 61 |
+
|
| 62 |
+
- name: Set up Docker Buildx
|
| 63 |
+
uses: docker/setup-buildx-action@v3
|
| 64 |
+
|
| 65 |
+
- name: Cache Docker layers
|
| 66 |
+
uses: actions/cache@v3
|
| 67 |
+
with:
|
| 68 |
+
path: /tmp/.buildx-cache-dev
|
| 69 |
+
key: ${{ runner.os }}-buildx-dev-regular-${{ github.sha }}
|
| 70 |
+
restore-keys: |
|
| 71 |
+
${{ runner.os }}-buildx-dev-regular-
|
| 72 |
+
|
| 73 |
+
- name: Build regular image (test only)
|
| 74 |
+
uses: docker/build-push-action@v5
|
| 75 |
+
with:
|
| 76 |
+
context: .
|
| 77 |
+
file: ./Dockerfile
|
| 78 |
+
platforms: ${{ github.event.inputs.platform || 'linux/amd64' }}
|
| 79 |
+
push: false
|
| 80 |
+
tags: ${{ env.IMAGE_NAME }}:${{ needs.extract-version.outputs.version }}-dev-regular
|
| 81 |
+
cache-from: type=local,src=/tmp/.buildx-cache-dev
|
| 82 |
+
cache-to: type=local,dest=/tmp/.buildx-cache-dev-new,mode=max
|
| 83 |
+
|
| 84 |
+
- name: Move cache
|
| 85 |
+
run: |
|
| 86 |
+
rm -rf /tmp/.buildx-cache-dev
|
| 87 |
+
mv /tmp/.buildx-cache-dev-new /tmp/.buildx-cache-dev
|
| 88 |
+
|
| 89 |
+
test-build-single:
|
| 90 |
+
needs: extract-version
|
| 91 |
+
runs-on: ubuntu-latest
|
| 92 |
+
if: github.event.inputs.dockerfile == 'single' || github.event.inputs.dockerfile == 'both' || github.event_name != 'workflow_dispatch'
|
| 93 |
+
steps:
|
| 94 |
+
- name: Checkout
|
| 95 |
+
uses: actions/checkout@v4
|
| 96 |
+
|
| 97 |
+
- name: Set up Docker Buildx
|
| 98 |
+
uses: docker/setup-buildx-action@v3
|
| 99 |
+
|
| 100 |
+
- name: Cache Docker layers
|
| 101 |
+
uses: actions/cache@v3
|
| 102 |
+
with:
|
| 103 |
+
path: /tmp/.buildx-cache-dev-single
|
| 104 |
+
key: ${{ runner.os }}-buildx-dev-single-${{ github.sha }}
|
| 105 |
+
restore-keys: |
|
| 106 |
+
${{ runner.os }}-buildx-dev-single-
|
| 107 |
+
|
| 108 |
+
- name: Build single-container image (test only)
|
| 109 |
+
uses: docker/build-push-action@v5
|
| 110 |
+
with:
|
| 111 |
+
context: .
|
| 112 |
+
file: ./Dockerfile.single
|
| 113 |
+
platforms: ${{ github.event.inputs.platform || 'linux/amd64' }}
|
| 114 |
+
push: false
|
| 115 |
+
tags: ${{ env.IMAGE_NAME }}:${{ needs.extract-version.outputs.version }}-dev-single
|
| 116 |
+
cache-from: type=local,src=/tmp/.buildx-cache-dev-single
|
| 117 |
+
cache-to: type=local,dest=/tmp/.buildx-cache-dev-single-new,mode=max
|
| 118 |
+
|
| 119 |
+
- name: Move cache
|
| 120 |
+
run: |
|
| 121 |
+
rm -rf /tmp/.buildx-cache-dev-single
|
| 122 |
+
mv /tmp/.buildx-cache-dev-single-new /tmp/.buildx-cache-dev-single
|
| 123 |
+
|
| 124 |
+
summary:
|
| 125 |
+
needs: [extract-version, test-build-regular, test-build-single]
|
| 126 |
+
runs-on: ubuntu-latest
|
| 127 |
+
if: always()
|
| 128 |
+
steps:
|
| 129 |
+
- name: Development Build Summary
|
| 130 |
+
run: |
|
| 131 |
+
echo "## Development Build Summary" >> $GITHUB_STEP_SUMMARY
|
| 132 |
+
echo "**Version:** ${{ needs.extract-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
| 133 |
+
echo "**Platform:** ${{ github.event.inputs.platform || 'linux/amd64' }}" >> $GITHUB_STEP_SUMMARY
|
| 134 |
+
echo "**Dockerfile:** ${{ github.event.inputs.dockerfile || 'both' }}" >> $GITHUB_STEP_SUMMARY
|
| 135 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 136 |
+
echo "### Results:" >> $GITHUB_STEP_SUMMARY
|
| 137 |
+
|
| 138 |
+
if [[ "${{ needs.test-build-regular.result }}" == "success" ]]; then
|
| 139 |
+
echo "✅ **Regular Dockerfile:** Build successful" >> $GITHUB_STEP_SUMMARY
|
| 140 |
+
elif [[ "${{ needs.test-build-regular.result }}" == "skipped" ]]; then
|
| 141 |
+
echo "⏭️ **Regular Dockerfile:** Skipped" >> $GITHUB_STEP_SUMMARY
|
| 142 |
+
else
|
| 143 |
+
echo "❌ **Regular Dockerfile:** Build failed" >> $GITHUB_STEP_SUMMARY
|
| 144 |
+
fi
|
| 145 |
+
|
| 146 |
+
if [[ "${{ needs.test-build-single.result }}" == "success" ]]; then
|
| 147 |
+
echo "✅ **Single Dockerfile:** Build successful" >> $GITHUB_STEP_SUMMARY
|
| 148 |
+
elif [[ "${{ needs.test-build-single.result }}" == "skipped" ]]; then
|
| 149 |
+
echo "⏭️ **Single Dockerfile:** Skipped" >> $GITHUB_STEP_SUMMARY
|
| 150 |
+
else
|
| 151 |
+
echo "❌ **Single Dockerfile:** Build failed" >> $GITHUB_STEP_SUMMARY
|
| 152 |
+
fi
|
| 153 |
+
|
| 154 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 155 |
+
echo "### Notes:" >> $GITHUB_STEP_SUMMARY
|
| 156 |
+
echo "- This is a development build (no images pushed to registry)" >> $GITHUB_STEP_SUMMARY
|
| 157 |
+
echo "- For production releases, use the 'Build and Release' workflow" >> $GITHUB_STEP_SUMMARY
|
.github/workflows/claude-code-review.yml
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Claude Code Review
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
types: [opened, synchronize]
|
| 6 |
+
# Optional: Only run on specific file changes
|
| 7 |
+
# paths:
|
| 8 |
+
# - "src/**/*.ts"
|
| 9 |
+
# - "src/**/*.tsx"
|
| 10 |
+
# - "src/**/*.js"
|
| 11 |
+
# - "src/**/*.jsx"
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
claude-review:
|
| 15 |
+
# Optional: Filter by PR author
|
| 16 |
+
# if: |
|
| 17 |
+
# github.event.pull_request.user.login == 'external-contributor' ||
|
| 18 |
+
# github.event.pull_request.user.login == 'new-developer' ||
|
| 19 |
+
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
| 20 |
+
|
| 21 |
+
runs-on: ubuntu-latest
|
| 22 |
+
permissions:
|
| 23 |
+
contents: read
|
| 24 |
+
pull-requests: read
|
| 25 |
+
issues: read
|
| 26 |
+
id-token: write
|
| 27 |
+
|
| 28 |
+
steps:
|
| 29 |
+
- name: Checkout repository
|
| 30 |
+
uses: actions/checkout@v4
|
| 31 |
+
with:
|
| 32 |
+
fetch-depth: 1
|
| 33 |
+
|
| 34 |
+
- name: Run Claude Code Review
|
| 35 |
+
id: claude-review
|
| 36 |
+
uses: anthropics/claude-code-action@beta
|
| 37 |
+
with:
|
| 38 |
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
| 39 |
+
|
| 40 |
+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
| 41 |
+
# model: "claude-opus-4-20250514"
|
| 42 |
+
|
| 43 |
+
# Direct prompt for automated review (no @claude mention needed)
|
| 44 |
+
direct_prompt: |
|
| 45 |
+
Please review this pull request and provide feedback on:
|
| 46 |
+
- Code quality and best practices
|
| 47 |
+
- Potential bugs or issues
|
| 48 |
+
- Performance considerations
|
| 49 |
+
- Security concerns
|
| 50 |
+
- Test coverage
|
| 51 |
+
|
| 52 |
+
Be constructive and helpful in your feedback.
|
| 53 |
+
|
| 54 |
+
# Optional: Customize review based on file types
|
| 55 |
+
# direct_prompt: |
|
| 56 |
+
# Review this PR focusing on:
|
| 57 |
+
# - For TypeScript files: Type safety and proper interface usage
|
| 58 |
+
# - For API endpoints: Security, input validation, and error handling
|
| 59 |
+
# - For React components: Performance, accessibility, and best practices
|
| 60 |
+
# - For tests: Coverage, edge cases, and test quality
|
| 61 |
+
|
| 62 |
+
# Optional: Different prompts for different authors
|
| 63 |
+
# direct_prompt: |
|
| 64 |
+
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
|
| 65 |
+
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
|
| 66 |
+
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
|
| 67 |
+
|
| 68 |
+
# Optional: Add specific tools for running tests or linting
|
| 69 |
+
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
|
| 70 |
+
|
| 71 |
+
# Optional: Skip review for certain conditions
|
| 72 |
+
# if: |
|
| 73 |
+
# !contains(github.event.pull_request.title, '[skip-review]') &&
|
| 74 |
+
# !contains(github.event.pull_request.title, '[WIP]')
|
| 75 |
+
|
.github/workflows/claude.yml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Claude Code
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
issue_comment:
|
| 5 |
+
types: [created]
|
| 6 |
+
pull_request_review_comment:
|
| 7 |
+
types: [created]
|
| 8 |
+
issues:
|
| 9 |
+
types: [opened, assigned]
|
| 10 |
+
pull_request_review:
|
| 11 |
+
types: [submitted]
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
claude:
|
| 15 |
+
if: |
|
| 16 |
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
| 17 |
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
| 18 |
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
| 19 |
+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
| 20 |
+
runs-on: ubuntu-latest
|
| 21 |
+
permissions:
|
| 22 |
+
contents: read
|
| 23 |
+
pull-requests: read
|
| 24 |
+
issues: read
|
| 25 |
+
id-token: write
|
| 26 |
+
steps:
|
| 27 |
+
- name: Checkout repository
|
| 28 |
+
uses: actions/checkout@v4
|
| 29 |
+
with:
|
| 30 |
+
fetch-depth: 1
|
| 31 |
+
|
| 32 |
+
- name: Run Claude Code
|
| 33 |
+
id: claude
|
| 34 |
+
uses: anthropics/claude-code-action@beta
|
| 35 |
+
with:
|
| 36 |
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
| 37 |
+
|
| 38 |
+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
| 39 |
+
# model: "claude-opus-4-20250514"
|
| 40 |
+
|
| 41 |
+
# Optional: Customize the trigger phrase (default: @claude)
|
| 42 |
+
# trigger_phrase: "/claude"
|
| 43 |
+
|
| 44 |
+
# Optional: Trigger when specific user is assigned to an issue
|
| 45 |
+
# assignee_trigger: "claude-bot"
|
| 46 |
+
|
| 47 |
+
# Optional: Allow Claude to run specific commands
|
| 48 |
+
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
| 49 |
+
|
| 50 |
+
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
| 51 |
+
# custom_instructions: |
|
| 52 |
+
# Follow our coding standards
|
| 53 |
+
# Ensure all new code has tests
|
| 54 |
+
# Use TypeScript for new files
|
| 55 |
+
|
| 56 |
+
# Optional: Custom environment variables for Claude
|
| 57 |
+
# claude_env: |
|
| 58 |
+
# NODE_ENV: test
|
| 59 |
+
|
.gitignore
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
prompts/patterns/user/
|
| 3 |
+
/notebooks/
|
| 4 |
+
data/
|
| 5 |
+
.uploads/
|
| 6 |
+
sqlite-db/
|
| 7 |
+
surreal-data/
|
| 8 |
+
surreal_data/
|
| 9 |
+
docker.env
|
| 10 |
+
!setup_guide/docker.env
|
| 11 |
+
notebook_data/
|
| 12 |
+
# Python-specific
|
| 13 |
+
*.py[cod]
|
| 14 |
+
__pycache__/
|
| 15 |
+
*.so
|
| 16 |
+
todo.md
|
| 17 |
+
temp/
|
| 18 |
+
google-credentials.json
|
| 19 |
+
# Distribution / packaging
|
| 20 |
+
.Python
|
| 21 |
+
build/
|
| 22 |
+
develop-eggs/
|
| 23 |
+
dist/
|
| 24 |
+
downloads/
|
| 25 |
+
eggs/
|
| 26 |
+
.eggs/
|
| 27 |
+
/lib/
|
| 28 |
+
/lib64/
|
| 29 |
+
parts/
|
| 30 |
+
sdist/
|
| 31 |
+
var/
|
| 32 |
+
wheels/
|
| 33 |
+
share/python-wheels/
|
| 34 |
+
*.egg-info/
|
| 35 |
+
.installed.cfg
|
| 36 |
+
*.egg
|
| 37 |
+
|
| 38 |
+
# PyInstaller
|
| 39 |
+
*.manifest
|
| 40 |
+
*.spec
|
| 41 |
+
|
| 42 |
+
# Installer logs
|
| 43 |
+
pip-log.txt
|
| 44 |
+
pip-delete-this-directory.txt
|
| 45 |
+
|
| 46 |
+
# Unit test / coverage reports
|
| 47 |
+
htmlcov/
|
| 48 |
+
.tox/
|
| 49 |
+
.nox/
|
| 50 |
+
.coverage
|
| 51 |
+
.coverage.*
|
| 52 |
+
.cache
|
| 53 |
+
nosetests.xml
|
| 54 |
+
coverage.xml
|
| 55 |
+
*.cover
|
| 56 |
+
*.py,cover
|
| 57 |
+
.hypothesis/
|
| 58 |
+
.pytest_cache/
|
| 59 |
+
|
| 60 |
+
# Jupyter Notebook
|
| 61 |
+
.ipynb_checkpoints
|
| 62 |
+
|
| 63 |
+
# IPython
|
| 64 |
+
profile_default/
|
| 65 |
+
ipython_config.py
|
| 66 |
+
|
| 67 |
+
# Environments
|
| 68 |
+
.env
|
| 69 |
+
.venv
|
| 70 |
+
env/
|
| 71 |
+
venv/
|
| 72 |
+
ENV/
|
| 73 |
+
env.bak/
|
| 74 |
+
venv.bak/
|
| 75 |
+
|
| 76 |
+
# PyCharm
|
| 77 |
+
.idea/
|
| 78 |
+
|
| 79 |
+
# VS Code
|
| 80 |
+
.vscode/
|
| 81 |
+
|
| 82 |
+
# Spyder project settings
|
| 83 |
+
.spyderproject
|
| 84 |
+
.spyproject
|
| 85 |
+
|
| 86 |
+
# Rope project settings
|
| 87 |
+
.ropeproject
|
| 88 |
+
|
| 89 |
+
# mkdocs documentation
|
| 90 |
+
/site
|
| 91 |
+
|
| 92 |
+
# mypy
|
| 93 |
+
.mypy_cache/
|
| 94 |
+
.dmypy.json
|
| 95 |
+
dmypy.json
|
| 96 |
+
|
| 97 |
+
# Pyre type checker
|
| 98 |
+
.pyre/
|
| 99 |
+
|
| 100 |
+
# pytype static type analyzer
|
| 101 |
+
.pytype/
|
| 102 |
+
|
| 103 |
+
# Cython debug symbols
|
| 104 |
+
cython_debug/
|
| 105 |
+
|
| 106 |
+
# macOS
|
| 107 |
+
.DS_Store
|
| 108 |
+
|
| 109 |
+
# Windows
|
| 110 |
+
Thumbs.db
|
| 111 |
+
ehthumbs.db
|
| 112 |
+
desktop.ini
|
| 113 |
+
|
| 114 |
+
# Linux
|
| 115 |
+
*~
|
| 116 |
+
|
| 117 |
+
# Log files
|
| 118 |
+
*.log
|
| 119 |
+
|
| 120 |
+
# Database files
|
| 121 |
+
*.db
|
| 122 |
+
*.sqlite3
|
| 123 |
+
|
| 124 |
+
.quarentena
|
| 125 |
+
|
| 126 |
+
claude-logs/
|
| 127 |
+
.claude/sessions
|
| 128 |
+
**/claude-logs
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
docs/custom_gpt
|
| 132 |
+
doc_exports/
|
| 133 |
+
|
| 134 |
+
specs/
|
| 135 |
+
.claude
|
| 136 |
+
|
| 137 |
+
.playwright-mcp/
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
.railwayignore
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Railway Build Optimization - Ignore unnecessary files
|
| 2 |
+
|
| 3 |
+
# Development
|
| 4 |
+
.git/
|
| 5 |
+
.github/
|
| 6 |
+
.vscode/
|
| 7 |
+
*.md
|
| 8 |
+
!RAILWAY.md
|
| 9 |
+
!README.md
|
| 10 |
+
docs/
|
| 11 |
+
.env.example
|
| 12 |
+
.env.railway
|
| 13 |
+
.gitignore
|
| 14 |
+
|
| 15 |
+
# Data directories (will be empty in repo anyway)
|
| 16 |
+
data/
|
| 17 |
+
notebook_data/
|
| 18 |
+
surreal_data/
|
| 19 |
+
surreal_single_data/
|
| 20 |
+
|
| 21 |
+
# Python
|
| 22 |
+
__pycache__/
|
| 23 |
+
*.py[cod]
|
| 24 |
+
*$py.class
|
| 25 |
+
*.so
|
| 26 |
+
.Python
|
| 27 |
+
*.egg-info/
|
| 28 |
+
.pytest_cache/
|
| 29 |
+
.mypy_cache/
|
| 30 |
+
.ruff_cache/
|
| 31 |
+
|
| 32 |
+
# Node
|
| 33 |
+
node_modules/
|
| 34 |
+
.next/
|
| 35 |
+
.turbo/
|
| 36 |
+
out/
|
| 37 |
+
build/
|
| 38 |
+
dist/
|
| 39 |
+
|
| 40 |
+
# IDEs
|
| 41 |
+
.idea/
|
| 42 |
+
*.swp
|
| 43 |
+
*.swo
|
| 44 |
+
*~
|
| 45 |
+
.DS_Store
|
| 46 |
+
|
| 47 |
+
# Scripts not needed in production
|
| 48 |
+
start-dev.ps1
|
| 49 |
+
start-production.ps1
|
| 50 |
+
stop-services.ps1
|
| 51 |
+
diagnose.ps1
|
| 52 |
+
Makefile
|
| 53 |
+
|
| 54 |
+
# Alternative docker files
|
| 55 |
+
Dockerfile
|
| 56 |
+
Dockerfile.single
|
| 57 |
+
docker-compose*.yml
|
| 58 |
+
supervisord.conf
|
| 59 |
+
supervisord.single.conf
|
| 60 |
+
|
| 61 |
+
# Tests
|
| 62 |
+
tests/
|
| 63 |
+
*.test.ts
|
| 64 |
+
*.test.js
|
| 65 |
+
*.spec.ts
|
| 66 |
+
*.spec.js
|
| 67 |
+
|
| 68 |
+
# CI/CD
|
| 69 |
+
.gitlab-ci.yml
|
| 70 |
+
|
| 71 |
+
# Logs
|
| 72 |
+
*.log
|
| 73 |
+
npm-debug.log*
|
| 74 |
+
yarn-debug.log*
|
| 75 |
+
yarn-error.log*
|
.worktreeinclude
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.env.local
|
| 3 |
+
.env.*
|
| 4 |
+
**/.claude/settings.local.json
|
| 5 |
+
CLAUDE.local.md
|
DEPLOY_NOW.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎯 DEPLOY NOW - Everything Ready!
|
| 2 |
+
|
| 3 |
+
## ✅ What's Configured
|
| 4 |
+
|
| 5 |
+
I've set up everything to work exactly like your localhost, using your FREE API keys:
|
| 6 |
+
|
| 7 |
+
- ✅ **Groq API**: `gsk_3pLc...kvfC` - For chat, transformations, insights
|
| 8 |
+
- ✅ **Gemini API**: `AIzaS...ep_0` - For embeddings, search, long context
|
| 9 |
+
- ✅ **Database**: test namespace/database (same as localhost)
|
| 10 |
+
- ✅ **All settings**: Same retry/worker config as your working setup
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 🚀 STEP 1: Copy Railway Variables
|
| 15 |
+
|
| 16 |
+
Go to Railway Dashboard → Your Service → Variables, and paste ALL of these:
|
| 17 |
+
|
| 18 |
+
```plaintext
|
| 19 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 20 |
+
SURREAL_USER=root
|
| 21 |
+
SURREAL_PASSWORD=root
|
| 22 |
+
SURREAL_NAMESPACE=test
|
| 23 |
+
SURREAL_DATABASE=test
|
| 24 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 25 |
+
API_URL=http://localhost:5055
|
| 26 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 27 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 28 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 29 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 30 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 31 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 32 |
+
GROQ_API_KEY=<YOUR_GROQ_KEY_HERE>
|
| 33 |
+
GOOGLE_API_KEY=<YOUR_GEMINI_KEY_HERE>
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
**Note**: We'll update `API_URL` after first deploy.
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## 🚀 STEP 2: Push Code
|
| 41 |
+
|
| 42 |
+
```powershell
|
| 43 |
+
cd c:\sem6-real\studyrocket\notebookllm\open-notebook
|
| 44 |
+
git add .
|
| 45 |
+
git commit -m "Add Railway deployment with FREE tier (Groq + Gemini)"
|
| 46 |
+
git push origin main
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## 🚀 STEP 3: Wait for Deploy
|
| 52 |
+
|
| 53 |
+
Railway will:
|
| 54 |
+
1. Build the Docker image (~5-10 minutes)
|
| 55 |
+
2. Start all services (SurrealDB, API, Worker, Frontend)
|
| 56 |
+
3. Run migrations (0 → 18)
|
| 57 |
+
4. Expose your app on a public URL
|
| 58 |
+
|
| 59 |
+
**Watch the logs** in Railway dashboard for:
|
| 60 |
+
```
|
| 61 |
+
✓ Ready in XXXms
|
| 62 |
+
INFO: Application startup complete
|
| 63 |
+
Migrations completed successfully. Database is now at version 18
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## 🚀 STEP 4: Update API_URL
|
| 69 |
+
|
| 70 |
+
1. Find your Railway domain in the dashboard (e.g., `https://se-production-1a2b.up.railway.app`)
|
| 71 |
+
2. Update the `API_URL` variable:
|
| 72 |
+
```plaintext
|
| 73 |
+
API_URL=https://se-production-1a2b.up.railway.app
|
| 74 |
+
```
|
| 75 |
+
3. Railway will auto-redeploy (~1 minute)
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## ✅ STEP 5: Test Everything
|
| 80 |
+
|
| 81 |
+
Visit your app: `https://your-railway-domain.up.railway.app`
|
| 82 |
+
|
| 83 |
+
**Test these features:**
|
| 84 |
+
- ✅ Create a notebook
|
| 85 |
+
- ✅ Upload a document (tests embeddings)
|
| 86 |
+
- ✅ Search documents (tests Gemini embeddings)
|
| 87 |
+
- ✅ Chat with documents (tests Groq LLM)
|
| 88 |
+
- ✅ Generate insights (tests transformations)
|
| 89 |
+
- ✅ Create notes
|
| 90 |
+
|
| 91 |
+
**Skip for now:**
|
| 92 |
+
- ⏸️ Podcast generation (you'll configure later)
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## 🎉 What You'll Have
|
| 97 |
+
|
| 98 |
+
### Working Features (FREE):
|
| 99 |
+
- ✅ Chat using Groq Llama 3.1 70B
|
| 100 |
+
- ✅ Document embeddings using Gemini
|
| 101 |
+
- ✅ Semantic search using Gemini
|
| 102 |
+
- ✅ Transformations using Groq
|
| 103 |
+
- ✅ Insights using Groq
|
| 104 |
+
- ✅ Long context (1M tokens!) using Gemini
|
| 105 |
+
- ✅ All for **$0/month** (AI costs)
|
| 106 |
+
|
| 107 |
+
### Railway Costs:
|
| 108 |
+
- First month: **FREE** ($5 credit)
|
| 109 |
+
- After: **$5-10/month** (just hosting)
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## 🔧 Models Available
|
| 114 |
+
|
| 115 |
+
In the UI, you can select from:
|
| 116 |
+
|
| 117 |
+
### Groq Models (FREE):
|
| 118 |
+
- `llama-3.1-70b-versatile` - Best for complex tasks
|
| 119 |
+
- `llama-3.1-8b-instant` - Fast for simple tasks
|
| 120 |
+
- `mixtral-8x7b-32768` - Alternative option
|
| 121 |
+
|
| 122 |
+
### Gemini Models (FREE):
|
| 123 |
+
- `gemini-1.5-flash` - Fast, FREE
|
| 124 |
+
- `gemini-1.5-pro` - 1M context, FREE tier
|
| 125 |
+
- `text-embedding-004` - Embeddings
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 🆘 If Something Goes Wrong
|
| 130 |
+
|
| 131 |
+
### Build Fails
|
| 132 |
+
→ Check Railway logs for error message
|
| 133 |
+
→ Ensure all files are committed (especially migrations/18.surrealql)
|
| 134 |
+
|
| 135 |
+
### Services Won't Start
|
| 136 |
+
→ Check `SURREAL_URL=ws://127.0.0.1:8000/rpc` (not localhost!)
|
| 137 |
+
→ Verify both API keys are set correctly
|
| 138 |
+
|
| 139 |
+
### Can't Access App
|
| 140 |
+
→ Wait 2-3 minutes after deploy
|
| 141 |
+
→ Check `API_URL` is set to your Railway domain
|
| 142 |
+
→ Try incognito/private browser window
|
| 143 |
+
|
| 144 |
+
### Features Don't Work
|
| 145 |
+
→ Groq models: Check chat works in UI
|
| 146 |
+
→ Gemini embeddings: Try uploading a document
|
| 147 |
+
→ If API key issues: Regenerate keys at provider dashboards
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
## 📊 Your Setup Summary
|
| 152 |
+
|
| 153 |
+
| Component | Configuration | Status |
|
| 154 |
+
|-----------|--------------|--------|
|
| 155 |
+
| **Database** | SurrealDB (embedded) | ✅ Ready |
|
| 156 |
+
| **API** | FastAPI on port 5055 | ✅ Ready |
|
| 157 |
+
| **Frontend** | Next.js on port 8080 | ✅ Ready |
|
| 158 |
+
| **Worker** | Background tasks | ✅ Ready |
|
| 159 |
+
| **LLM** | Groq Llama 3.1 | ✅ FREE |
|
| 160 |
+
| **Embeddings** | Gemini | ✅ FREE |
|
| 161 |
+
| **Hosting** | Railway | ✅ $5-10/mo |
|
| 162 |
+
| **Podcasts** | Not configured | ⏸️ Later |
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
## 🎊 Next Steps After Deploy
|
| 167 |
+
|
| 168 |
+
1. ✅ Test all features (except podcasts)
|
| 169 |
+
2. ✅ Upload some test documents
|
| 170 |
+
3. ✅ Try searching and chatting
|
| 171 |
+
4. ✅ Generate some insights
|
| 172 |
+
5. ⏸️ Configure podcasts later when needed
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## 💰 Cost Tracking
|
| 177 |
+
|
| 178 |
+
Track your FREE tier usage:
|
| 179 |
+
- **Groq**: https://console.groq.com/dashboard
|
| 180 |
+
- **Gemini**: https://console.cloud.google.com/apis/dashboard
|
| 181 |
+
- **Railway**: https://railway.app/dashboard
|
| 182 |
+
|
| 183 |
+
All providers show FREE tier limits and usage!
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## 🚀 Ready to Deploy!
|
| 188 |
+
|
| 189 |
+
Everything is configured. Just run:
|
| 190 |
+
|
| 191 |
+
```powershell
|
| 192 |
+
git add .
|
| 193 |
+
git commit -m "Railway deployment ready with FREE tier keys"
|
| 194 |
+
git push origin main
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
Then watch Railway build and deploy! 🎉
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
**Questions?** Everything should work exactly like your localhost setup, but on Railway! The same models, same features (minus podcasts), all working with your FREE API keys.
|
Dockerfile
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Set PYTHONPATH to include /app
|
| 6 |
+
ENV PYTHONPATH=/app
|
| 7 |
+
|
| 8 |
+
# Set Hugging Face cache directories (writable in HF Spaces)
|
| 9 |
+
ENV HF_HOME=/tmp
|
| 10 |
+
ENV TRANSFORMERS_CACHE=/tmp
|
| 11 |
+
ENV SENTENCE_TRANSFORMERS_HOME=/tmp
|
| 12 |
+
|
| 13 |
+
# Install system dependencies
|
| 14 |
+
RUN apt-get update && apt-get install -y \
|
| 15 |
+
curl \
|
| 16 |
+
build-essential \
|
| 17 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
+
|
| 19 |
+
# Install SurrealDB
|
| 20 |
+
RUN curl -sSf https://install.surrealdb.com | sh
|
| 21 |
+
|
| 22 |
+
# Copy requirements.txt for dependency installation
|
| 23 |
+
COPY requirements.txt ./
|
| 24 |
+
|
| 25 |
+
# Install Python dependencies from requirements.txt
|
| 26 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 27 |
+
pip install --no-cache-dir -r requirements.txt
|
| 28 |
+
|
| 29 |
+
# Explicitly ensure surreal-commands is installed (belt-and-suspenders approach)
|
| 30 |
+
RUN pip install --no-cache-dir surreal-commands>=1.2.0
|
| 31 |
+
|
| 32 |
+
# Pre-download sentence-transformers model at build time
|
| 33 |
+
# This will be cached in the Docker image
|
| 34 |
+
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"
|
| 35 |
+
|
| 36 |
+
# Copy application code
|
| 37 |
+
COPY api/ ./api/
|
| 38 |
+
COPY open_notebook/ ./open_notebook/
|
| 39 |
+
COPY commands/ ./commands/
|
| 40 |
+
COPY migrations/ ./migrations/
|
| 41 |
+
COPY prompts/ ./prompts/
|
| 42 |
+
COPY run_api.py ./
|
| 43 |
+
COPY start.sh ./
|
| 44 |
+
|
| 45 |
+
# Make start script executable
|
| 46 |
+
RUN chmod +x start.sh
|
| 47 |
+
|
| 48 |
+
# Set environment variables for SurrealDB connection
|
| 49 |
+
ENV SURREAL_URL=ws://localhost:8000/rpc
|
| 50 |
+
ENV SURREAL_ADDRESS=localhost
|
| 51 |
+
ENV SURREAL_PORT=8000
|
| 52 |
+
ENV SURREAL_USER=root
|
| 53 |
+
ENV SURREAL_PASS=root
|
| 54 |
+
ENV SURREAL_NAMESPACE=open_notebook
|
| 55 |
+
ENV SURREAL_DATABASE=main
|
| 56 |
+
|
| 57 |
+
# Set API configuration for Hugging Face Spaces
|
| 58 |
+
ENV API_HOST=0.0.0.0
|
| 59 |
+
ENV API_PORT=7860
|
| 60 |
+
ENV API_RELOAD=false
|
| 61 |
+
|
| 62 |
+
# Expose Hugging Face Spaces port
|
| 63 |
+
EXPOSE 7860
|
| 64 |
+
|
| 65 |
+
# Run the start script
|
| 66 |
+
CMD ["./start.sh"]
|
Dockerfile.huggingface
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Set PYTHONPATH to include /app
|
| 6 |
+
ENV PYTHONPATH=/app
|
| 7 |
+
|
| 8 |
+
# Set Hugging Face cache directories (writable in HF Spaces)
|
| 9 |
+
ENV HF_HOME=/tmp
|
| 10 |
+
ENV TRANSFORMERS_CACHE=/tmp
|
| 11 |
+
ENV SENTENCE_TRANSFORMERS_HOME=/tmp
|
| 12 |
+
|
| 13 |
+
# Install system dependencies
|
| 14 |
+
RUN apt-get update && apt-get install -y \
|
| 15 |
+
curl \
|
| 16 |
+
build-essential \
|
| 17 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
+
|
| 19 |
+
# Install SurrealDB
|
| 20 |
+
RUN curl -sSf https://install.surrealdb.com | sh
|
| 21 |
+
|
| 22 |
+
# Copy requirements.txt for dependency installation
|
| 23 |
+
COPY requirements.txt ./
|
| 24 |
+
|
| 25 |
+
# Install Python dependencies from requirements.txt
|
| 26 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 27 |
+
pip install --no-cache-dir -r requirements.txt
|
| 28 |
+
|
| 29 |
+
# Explicitly ensure surreal-commands is installed (belt-and-suspenders approach)
|
| 30 |
+
RUN pip install --no-cache-dir surreal-commands>=1.2.0
|
| 31 |
+
|
| 32 |
+
# Pre-download sentence-transformers model at build time
|
| 33 |
+
# This will be cached in the Docker image
|
| 34 |
+
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"
|
| 35 |
+
|
| 36 |
+
# Copy application code
|
| 37 |
+
COPY api/ ./api/
|
| 38 |
+
COPY open_notebook/ ./open_notebook/
|
| 39 |
+
COPY commands/ ./commands/
|
| 40 |
+
COPY migrations/ ./migrations/
|
| 41 |
+
COPY prompts/ ./prompts/
|
| 42 |
+
COPY run_api.py ./
|
| 43 |
+
COPY start.sh ./
|
| 44 |
+
|
| 45 |
+
# Make start script executable
|
| 46 |
+
RUN chmod +x start.sh
|
| 47 |
+
|
| 48 |
+
# Set environment variables for SurrealDB connection
|
| 49 |
+
ENV SURREAL_URL=ws://localhost:8000/rpc
|
| 50 |
+
ENV SURREAL_ADDRESS=localhost
|
| 51 |
+
ENV SURREAL_PORT=8000
|
| 52 |
+
ENV SURREAL_USER=root
|
| 53 |
+
ENV SURREAL_PASS=root
|
| 54 |
+
ENV SURREAL_NAMESPACE=open_notebook
|
| 55 |
+
ENV SURREAL_DATABASE=main
|
| 56 |
+
|
| 57 |
+
# Set API configuration for Hugging Face Spaces
|
| 58 |
+
ENV API_HOST=0.0.0.0
|
| 59 |
+
ENV API_PORT=7860
|
| 60 |
+
ENV API_RELOAD=false
|
| 61 |
+
|
| 62 |
+
# Expose Hugging Face Spaces port
|
| 63 |
+
EXPOSE 7860
|
| 64 |
+
|
| 65 |
+
# Run the start script
|
| 66 |
+
CMD ["./start.sh"]
|
Dockerfile.railway
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Railway-optimized Dockerfile for Open Notebook
|
| 2 |
+
# Uses single-container architecture with all services
|
| 3 |
+
|
| 4 |
+
# Build stage
|
| 5 |
+
FROM python:3.12-slim-bookworm AS builder
|
| 6 |
+
|
| 7 |
+
# Install uv
|
| 8 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 9 |
+
|
| 10 |
+
# Install build dependencies
|
| 11 |
+
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
| 12 |
+
gcc g++ git make curl \
|
| 13 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 14 |
+
&& apt-get install -y nodejs \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# Build optimization
|
| 18 |
+
ENV MAKEFLAGS="-j$(nproc)" \
|
| 19 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 20 |
+
PYTHONUNBUFFERED=1 \
|
| 21 |
+
UV_COMPILE_BYTECODE=1 \
|
| 22 |
+
UV_LINK_MODE=copy
|
| 23 |
+
|
| 24 |
+
WORKDIR /app
|
| 25 |
+
|
| 26 |
+
# Install Python dependencies
|
| 27 |
+
COPY pyproject.toml uv.lock ./
|
| 28 |
+
COPY open_notebook/__init__.py ./open_notebook/__init__.py
|
| 29 |
+
RUN uv sync --frozen --no-dev
|
| 30 |
+
|
| 31 |
+
# Copy application code
|
| 32 |
+
COPY . /app
|
| 33 |
+
|
| 34 |
+
# Build frontend
|
| 35 |
+
WORKDIR /app/frontend
|
| 36 |
+
RUN npm ci && npm run build
|
| 37 |
+
|
| 38 |
+
WORKDIR /app
|
| 39 |
+
|
| 40 |
+
# Runtime stage
|
| 41 |
+
FROM python:3.12-slim-bookworm AS runtime
|
| 42 |
+
|
| 43 |
+
# Install runtime dependencies
|
| 44 |
+
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
| 45 |
+
ffmpeg supervisor curl \
|
| 46 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 47 |
+
&& apt-get install -y nodejs \
|
| 48 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 49 |
+
|
| 50 |
+
# Install SurrealDB
|
| 51 |
+
RUN curl --proto '=https' --tlsv1.2 -sSf https://install.surrealdb.com | sh
|
| 52 |
+
|
| 53 |
+
# Install uv
|
| 54 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 55 |
+
|
| 56 |
+
WORKDIR /app
|
| 57 |
+
|
| 58 |
+
# Copy from builder
|
| 59 |
+
COPY --from=builder /app/.venv /app/.venv
|
| 60 |
+
COPY --from=builder /app /app
|
| 61 |
+
|
| 62 |
+
# Set environment
|
| 63 |
+
ENV PORT=8080 \
|
| 64 |
+
PYTHONUNBUFFERED=1
|
| 65 |
+
|
| 66 |
+
# Expose ports
|
| 67 |
+
EXPOSE 8080 5055
|
| 68 |
+
|
| 69 |
+
# Create directories
|
| 70 |
+
RUN mkdir -p /mydata /app/data /var/log/supervisor
|
| 71 |
+
|
| 72 |
+
# Fix script permissions
|
| 73 |
+
RUN sed -i 's/\r$//' /app/scripts/wait-for-api.sh && \
|
| 74 |
+
chmod +x /app/scripts/wait-for-api.sh
|
| 75 |
+
|
| 76 |
+
# Copy supervisord config
|
| 77 |
+
COPY supervisord.railway.conf /app/supervisord.conf
|
| 78 |
+
|
| 79 |
+
# Start supervisord
|
| 80 |
+
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
|
Dockerfile.single
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build stage
|
| 2 |
+
FROM python:3.12-slim-bookworm AS builder
|
| 3 |
+
|
| 4 |
+
# Install uv using the official method
|
| 5 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 6 |
+
|
| 7 |
+
# Install system dependencies required for building certain Python packages
|
| 8 |
+
# Add Node.js 20.x LTS for building frontend
|
| 9 |
+
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
| 10 |
+
gcc g++ git make \
|
| 11 |
+
curl \
|
| 12 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 13 |
+
&& apt-get install -y nodejs \
|
| 14 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
+
|
| 16 |
+
# Set build optimization environment variables
|
| 17 |
+
ENV MAKEFLAGS="-j$(nproc)"
|
| 18 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 19 |
+
ENV PYTHONUNBUFFERED=1
|
| 20 |
+
ENV UV_COMPILE_BYTECODE=1
|
| 21 |
+
ENV UV_LINK_MODE=copy
|
| 22 |
+
|
| 23 |
+
# Set the working directory in the container to /app
|
| 24 |
+
WORKDIR /app
|
| 25 |
+
|
| 26 |
+
# Copy dependency files and minimal package structure first for better layer caching
|
| 27 |
+
COPY pyproject.toml uv.lock ./
|
| 28 |
+
COPY open_notebook/__init__.py ./open_notebook/__init__.py
|
| 29 |
+
|
| 30 |
+
# Install dependencies with optimizations (this layer will be cached unless dependencies change)
|
| 31 |
+
RUN uv sync --frozen --no-dev
|
| 32 |
+
|
| 33 |
+
# Copy the rest of the application code
|
| 34 |
+
COPY . /app
|
| 35 |
+
|
| 36 |
+
# Install frontend dependencies and build
|
| 37 |
+
WORKDIR /app/frontend
|
| 38 |
+
RUN npm ci
|
| 39 |
+
RUN npm run build
|
| 40 |
+
|
| 41 |
+
# Return to app root
|
| 42 |
+
WORKDIR /app
|
| 43 |
+
|
| 44 |
+
# Runtime stage
|
| 45 |
+
FROM python:3.12-slim-bookworm AS runtime
|
| 46 |
+
|
| 47 |
+
# Install runtime system dependencies including curl for SurrealDB installation
|
| 48 |
+
# Add Node.js 20.x LTS for running frontend
|
| 49 |
+
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
| 50 |
+
ffmpeg \
|
| 51 |
+
supervisor \
|
| 52 |
+
curl \
|
| 53 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 54 |
+
&& apt-get install -y nodejs \
|
| 55 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 56 |
+
|
| 57 |
+
# Install SurrealDB
|
| 58 |
+
RUN curl --proto '=https' --tlsv1.2 -sSf https://install.surrealdb.com | sh
|
| 59 |
+
|
| 60 |
+
# Install uv using the official method
|
| 61 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 62 |
+
|
| 63 |
+
# Set the working directory in the container to /app
|
| 64 |
+
WORKDIR /app
|
| 65 |
+
|
| 66 |
+
# Copy the virtual environment from builder stage
|
| 67 |
+
COPY --from=builder /app/.venv /app/.venv
|
| 68 |
+
|
| 69 |
+
# Copy the application code
|
| 70 |
+
COPY --from=builder /app /app
|
| 71 |
+
|
| 72 |
+
# Default PORT if not provided
|
| 73 |
+
ENV PORT=8502
|
| 74 |
+
|
| 75 |
+
# Expose ports for Frontend and API
|
| 76 |
+
EXPOSE 8502 5055
|
| 77 |
+
|
| 78 |
+
# Copy single-container supervisord configuration
|
| 79 |
+
COPY supervisord.single.conf /app/supervisord.conf
|
| 80 |
+
|
| 81 |
+
# Create log directories
|
| 82 |
+
RUN mkdir -p /var/log/supervisor
|
| 83 |
+
|
| 84 |
+
# Fix line endings for startup script
|
| 85 |
+
RUN sed -i 's/\r$//' /app/scripts/wait-for-api.sh
|
| 86 |
+
RUN chmod +x /app/scripts/wait-for-api.sh
|
| 87 |
+
|
| 88 |
+
# Runtime API URL Configuration
|
| 89 |
+
# The API_URL environment variable can be set at container runtime to configure
|
| 90 |
+
# where the frontend should connect to the API. This allows the same Docker image
|
| 91 |
+
# to work in different deployment scenarios without rebuilding.
|
| 92 |
+
#
|
| 93 |
+
# If not set, the system will auto-detect based on incoming requests.
|
| 94 |
+
# Set API_URL when using reverse proxies or custom domains.
|
| 95 |
+
#
|
| 96 |
+
# Example: docker run -e API_URL=https://your-domain.com/api ...
|
| 97 |
+
|
| 98 |
+
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
|
FINAL_RAILWAY_VARS.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 FINAL RAILWAY VARIABLES - Ready to Deploy
|
| 2 |
+
|
| 3 |
+
## Copy ALL of these to Railway Dashboard → Variables
|
| 4 |
+
|
| 5 |
+
```plaintext
|
| 6 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 7 |
+
SURREAL_USER=root
|
| 8 |
+
SURREAL_PASSWORD=root
|
| 9 |
+
SURREAL_NAMESPACE=test
|
| 10 |
+
SURREAL_DATABASE=test
|
| 11 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 12 |
+
API_URL=https://YOUR_RAILWAY_DOMAIN_HERE
|
| 13 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 14 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 15 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 16 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 17 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 18 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 19 |
+
GROQ_API_KEY=<YOUR_GROQ_KEY_HERE>
|
| 20 |
+
GOOGLE_API_KEY=<YOUR_GEMINI_KEY_HERE>
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## After First Deploy
|
| 24 |
+
|
| 25 |
+
Once you get your Railway domain (like `https://se-production-xxxx.up.railway.app`):
|
| 26 |
+
|
| 27 |
+
```plaintext
|
| 28 |
+
API_URL=https://your-actual-railway-domain.up.railway.app
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## ✅ What Will Work
|
| 34 |
+
|
| 35 |
+
With these keys, these features will work:
|
| 36 |
+
|
| 37 |
+
- ✅ **Chat** - Using Groq Llama models
|
| 38 |
+
- ✅ **Transformations** - Using Groq Llama models
|
| 39 |
+
- ✅ **Embeddings/Search** - Using Gemini embeddings
|
| 40 |
+
- ✅ **Long Context** - Using Gemini 1.5 Pro (1M tokens!)
|
| 41 |
+
- ✅ **Insights** - Using Groq models
|
| 42 |
+
- ✅ **Knowledge Graph** - Using embeddings
|
| 43 |
+
- ✅ **Document Upload** - Using embeddings for vectorization
|
| 44 |
+
|
| 45 |
+
## ⏸️ What You'll Set Up Later
|
| 46 |
+
|
| 47 |
+
- ⏸️ **Podcasts** - You'll configure this later (needs TTS setup)
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## 🚀 Deploy Now
|
| 52 |
+
|
| 53 |
+
```powershell
|
| 54 |
+
git add .
|
| 55 |
+
git commit -m "Add FREE tier config with Groq + Gemini API keys"
|
| 56 |
+
git push origin main
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Railway will auto-deploy and everything except podcasts will work! 🎉
|
FREE_TIER_QUICK.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🆓 FREE TIER - Railway Variables (Copy-Paste Ready)
|
| 2 |
+
|
| 3 |
+
## ⚡ QUICK SETUP
|
| 4 |
+
|
| 5 |
+
### Step 1: Get FREE API Keys
|
| 6 |
+
|
| 7 |
+
1. **Groq** (FREE LLM): https://console.groq.com/keys
|
| 8 |
+
2. **Gemini** (FREE Embeddings/TTS): https://makersuite.google.com/app/apikey
|
| 9 |
+
|
| 10 |
+
### Step 2: Set These in Railway Variables
|
| 11 |
+
|
| 12 |
+
```plaintext
|
| 13 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 14 |
+
SURREAL_USER=root
|
| 15 |
+
SURREAL_PASSWORD=root
|
| 16 |
+
SURREAL_NAMESPACE=test
|
| 17 |
+
SURREAL_DATABASE=test
|
| 18 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 19 |
+
API_URL=https://YOUR_RAILWAY_DOMAIN_HERE
|
| 20 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 21 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 22 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 23 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 24 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 25 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 26 |
+
GROQ_API_KEY=paste_your_groq_key_here
|
| 27 |
+
GOOGLE_API_KEY=paste_your_gemini_key_here
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### Step 3: Push Code
|
| 31 |
+
|
| 32 |
+
```powershell
|
| 33 |
+
git add .
|
| 34 |
+
git commit -m "Switch to 100% free tier models (Groq + Gemini)"
|
| 35 |
+
git push origin main
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### Step 4: After Deploy
|
| 39 |
+
|
| 40 |
+
Update `API_URL` with your actual Railway domain.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## 🎯 WHAT CHANGED
|
| 45 |
+
|
| 46 |
+
### Podcast Models (Migration 18)
|
| 47 |
+
|
| 48 |
+
**Before (BROKEN & PAID):**
|
| 49 |
+
- Outline: `openai/gpt-5-mini` ← Model doesn't exist!
|
| 50 |
+
- Transcript: `openai/gpt-5-mini` ← Model doesn't exist!
|
| 51 |
+
- TTS: `openai/gpt-4o-mini-tts` ← Model doesn't exist!
|
| 52 |
+
- **Cost**: Would fail + $15-30/month if fixed
|
| 53 |
+
|
| 54 |
+
**After (FREE & WORKING):**
|
| 55 |
+
- Outline: `groq/llama-3.1-8b-instant` ← Fast, FREE
|
| 56 |
+
- Transcript: `groq/llama-3.1-70b-versatile` ← Smart, FREE
|
| 57 |
+
- TTS: `google/gemini-1.5-flash` ← FREE
|
| 58 |
+
- **Cost**: $0/month!
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## 📊 FREE TIER LIMITS
|
| 63 |
+
|
| 64 |
+
| Provider | Free Limit | Good For |
|
| 65 |
+
|----------|-----------|----------|
|
| 66 |
+
| **Groq** | 30 req/min<br>~7000 req/day | Chat, Transformations, Podcasts |
|
| 67 |
+
| **Gemini** | 60 req/min<br>1500 req/day | Embeddings, Long Context, TTS |
|
| 68 |
+
| **Railway** | $5 credit/month | Hosting (costs $5-10/month) |
|
| 69 |
+
|
| 70 |
+
**Total Cost**: ~$5-10/month (just hosting!)
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## ✅ Verification
|
| 75 |
+
|
| 76 |
+
After deploy, check logs for:
|
| 77 |
+
```
|
| 78 |
+
Migrations completed successfully. Database is now at version 18
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Then test:
|
| 82 |
+
- ✅ Chat works with Groq
|
| 83 |
+
- ✅ Search works with Gemini embeddings
|
| 84 |
+
- ✅ Podcasts work with Groq + Gemini TTS
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## 🆘 If It Fails
|
| 89 |
+
|
| 90 |
+
### "Migration 18 not found"
|
| 91 |
+
→ Make sure you committed `migrations/18.surrealql`
|
| 92 |
+
|
| 93 |
+
### "GROQ_API_KEY not set"
|
| 94 |
+
→ Get free key: https://console.groq.com/keys
|
| 95 |
+
|
| 96 |
+
### "GOOGLE_API_KEY not set"
|
| 97 |
+
→ Get free key: https://makersuite.google.com/app/apikey
|
| 98 |
+
|
| 99 |
+
### "Model not found"
|
| 100 |
+
→ Migration 18 probably didn't run. Check logs.
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
**Read `FREE_TIER_SETUP.md` for detailed explanation!**
|
FREE_TIER_SETUP.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🆓 FREE TIER CONFIGURATION - Complete Guide
|
| 2 |
+
|
| 3 |
+
## Your Current Hardcoded Models (From migrations/7.surrealql)
|
| 4 |
+
|
| 5 |
+
I analyzed your codebase and found these hardcoded models:
|
| 6 |
+
|
| 7 |
+
### Podcast Features:
|
| 8 |
+
- **Outline Model**: `gpt-5-mini` (typo - should be `gpt-4o-mini`)
|
| 9 |
+
- **Transcript Model**: `gpt-5-mini` (typo - should be `gpt-4o-mini`)
|
| 10 |
+
- **TTS Model**: `gpt-4o-mini-tts`
|
| 11 |
+
- **Providers**: OpenAI
|
| 12 |
+
|
| 13 |
+
**Note**: `gpt-5-mini` is likely a typo in your migration file. OpenAI's model is called `gpt-4o-mini`.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 🎯 100% FREE TIER STRATEGY
|
| 18 |
+
|
| 19 |
+
To run this project completely free, you need to use **ONLY free-tier providers with generous limits**:
|
| 20 |
+
|
| 21 |
+
### Best Free Providers (Confirmed Free Tiers):
|
| 22 |
+
|
| 23 |
+
1. **Groq** - BEST for FREE LLM
|
| 24 |
+
- ✅ Completely FREE (generous rate limits)
|
| 25 |
+
- ✅ Very fast inference
|
| 26 |
+
- ✅ Models: `llama-3.1-70b-versatile`, `llama-3.1-8b-instant`, `mixtral-8x7b-32768`
|
| 27 |
+
- ✅ No credit card required
|
| 28 |
+
|
| 29 |
+
2. **Google Gemini** - BEST for embeddings & long context
|
| 30 |
+
- ✅ FREE tier: 60 requests/minute
|
| 31 |
+
- ✅ Models: `gemini-1.5-flash`, `gemini-1.5-pro` (1M context!)
|
| 32 |
+
- ✅ Embeddings included FREE
|
| 33 |
+
- ✅ No credit card required initially
|
| 34 |
+
|
| 35 |
+
3. **OpenAI** - NOT FREE (but you have credit)
|
| 36 |
+
- ❌ Requires payment (but $5-18 free credit for new accounts)
|
| 37 |
+
- ⚠️ `gpt-4o-mini` costs $0.15/1M input tokens, $0.60/1M output
|
| 38 |
+
- ⚠️ TTS costs extra
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 📋 RAILWAY VARIABLES - 100% FREE CONFIGURATION
|
| 43 |
+
|
| 44 |
+
Copy these EXACT variables to your Railway dashboard:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
# ============================================
|
| 48 |
+
# DATABASE (Keep as-is)
|
| 49 |
+
# ============================================
|
| 50 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 51 |
+
SURREAL_USER=root
|
| 52 |
+
SURREAL_PASSWORD=root
|
| 53 |
+
SURREAL_NAMESPACE=test
|
| 54 |
+
SURREAL_DATABASE=test
|
| 55 |
+
|
| 56 |
+
# ============================================
|
| 57 |
+
# API CONFIGURATION
|
| 58 |
+
# ============================================
|
| 59 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 60 |
+
API_URL=https://YOUR_RAILWAY_DOMAIN_HERE
|
| 61 |
+
|
| 62 |
+
# ============================================
|
| 63 |
+
# WORKER & RETRY (Keep as-is)
|
| 64 |
+
# ============================================
|
| 65 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 66 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 67 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 68 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 69 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 70 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 71 |
+
|
| 72 |
+
# ============================================
|
| 73 |
+
# FREE TIER AI PROVIDERS
|
| 74 |
+
# ============================================
|
| 75 |
+
|
| 76 |
+
# Groq - FREE (Best for LLMs - Chat, Transformations)
|
| 77 |
+
# Get FREE key at: https://console.groq.com/keys
|
| 78 |
+
GROQ_API_KEY=your_groq_api_key_here
|
| 79 |
+
|
| 80 |
+
# Google Gemini - FREE (Best for Embeddings & Long Context)
|
| 81 |
+
# Get FREE key at: https://makersuite.google.com/app/apikey
|
| 82 |
+
GOOGLE_API_KEY=your_google_gemini_key_here
|
| 83 |
+
|
| 84 |
+
# ============================================
|
| 85 |
+
# OPTIONAL: If you have OpenAI credit
|
| 86 |
+
# ============================================
|
| 87 |
+
# OPENAI_API_KEY=sk-your_key_if_you_have_credit
|
| 88 |
+
|
| 89 |
+
# ============================================
|
| 90 |
+
# DO NOT SET - These are paid services
|
| 91 |
+
# ============================================
|
| 92 |
+
# ANTHROPIC_API_KEY= # Claude - PAID
|
| 93 |
+
# ELEVENLABS_API_KEY= # TTS - PAID
|
| 94 |
+
# MISTRAL_API_KEY= # Mistral - PAID
|
| 95 |
+
# DEEPSEEK_API_KEY= # DeepSeek - PAID
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## ⚙️ CODE CHANGES REQUIRED
|
| 101 |
+
|
| 102 |
+
### Fix Migration 7 (Podcast Models)
|
| 103 |
+
|
| 104 |
+
Your migration file has `gpt-5-mini` which doesn't exist. You need to change it to use **FREE Groq models**:
|
| 105 |
+
|
| 106 |
+
**File**: `migrations/7.surrealql`
|
| 107 |
+
|
| 108 |
+
**Change these lines:**
|
| 109 |
+
|
| 110 |
+
```sql
|
| 111 |
+
-- BEFORE (Uses paid OpenAI):
|
| 112 |
+
outline_provider: "openai",
|
| 113 |
+
outline_model: "gpt-5-mini", # ← This is wrong (gpt-5 doesn't exist)
|
| 114 |
+
transcript_provider: "openai",
|
| 115 |
+
transcript_model: "gpt-5-mini",
|
| 116 |
+
|
| 117 |
+
-- AFTER (Uses FREE Groq):
|
| 118 |
+
outline_provider: "groq",
|
| 119 |
+
outline_model: "llama-3.1-8b-instant", # ← Fast & FREE
|
| 120 |
+
transcript_provider: "groq",
|
| 121 |
+
transcript_model: "llama-3.1-70b-versatile", # ← Smart & FREE
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
**All 3 episode profiles need this change:**
|
| 125 |
+
1. `tech_discussion`
|
| 126 |
+
2. `solo_expert`
|
| 127 |
+
3. `business_analysis`
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
## 🎤 TTS (Text-to-Speech) Problem
|
| 132 |
+
|
| 133 |
+
**Issue**: Your migrations use `gpt-4o-mini-tts` which is **NOT FREE** and **DOESN'T EXIST** as a model name.
|
| 134 |
+
|
| 135 |
+
OpenAI TTS models are:
|
| 136 |
+
- `tts-1` (costs $15/1M characters)
|
| 137 |
+
- `tts-1-hd` (costs $30/1M characters)
|
| 138 |
+
|
| 139 |
+
### FREE TTS Options:
|
| 140 |
+
|
| 141 |
+
1. **Google Gemini MultiModal** (BEST FREE OPTION)
|
| 142 |
+
- Use `gemini-1.5-flash` for audio generation
|
| 143 |
+
- FREE tier included
|
| 144 |
+
|
| 145 |
+
2. **Disable TTS** (if you don't need podcasts)
|
| 146 |
+
- Remove podcast functionality to stay 100% free
|
| 147 |
+
|
| 148 |
+
3. **Keep OpenAI TTS** (if you have credit)
|
| 149 |
+
- Will use your free credit (~500K-1M characters)
|
| 150 |
+
|
| 151 |
+
### Recommended: Change to Google TTS (FREE)
|
| 152 |
+
|
| 153 |
+
**File**: `migrations/7.surrealql`
|
| 154 |
+
|
| 155 |
+
```sql
|
| 156 |
+
-- BEFORE (Paid OpenAI TTS):
|
| 157 |
+
tts_provider: "openai",
|
| 158 |
+
tts_model: "gpt-4o-mini-tts", # ← Doesn't exist, costs money
|
| 159 |
+
|
| 160 |
+
-- AFTER (FREE Google TTS):
|
| 161 |
+
tts_provider: "google",
|
| 162 |
+
tts_model: "gemini-1.5-flash", # ← FREE
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
---
|
| 166 |
+
|
| 167 |
+
## 🔧 EXACT CHANGES TO MAKE
|
| 168 |
+
|
| 169 |
+
### Step 1: Update Migration File
|
| 170 |
+
|
| 171 |
+
**File**: `c:\sem6-real\studyrocket\notebookllm\open-notebook\migrations\18.surrealql` (create NEW migration)
|
| 172 |
+
|
| 173 |
+
```sql
|
| 174 |
+
-- Migration 18: Switch to FREE tier models (Groq + Gemini)
|
| 175 |
+
|
| 176 |
+
-- Update all episode profiles to use FREE Groq models
|
| 177 |
+
UPDATE episode_profile:tech_discussion SET
|
| 178 |
+
outline_provider = "groq",
|
| 179 |
+
outline_model = "llama-3.1-8b-instant",
|
| 180 |
+
transcript_provider = "groq",
|
| 181 |
+
transcript_model = "llama-3.1-70b-versatile";
|
| 182 |
+
|
| 183 |
+
UPDATE episode_profile:solo_expert SET
|
| 184 |
+
outline_provider = "groq",
|
| 185 |
+
outline_model = "llama-3.1-8b-instant",
|
| 186 |
+
transcript_provider = "groq",
|
| 187 |
+
transcript_model = "llama-3.1-70b-versatile";
|
| 188 |
+
|
| 189 |
+
UPDATE episode_profile:business_analysis SET
|
| 190 |
+
outline_provider = "groq",
|
| 191 |
+
outline_model = "llama-3.1-8b-instant",
|
| 192 |
+
transcript_provider = "groq",
|
| 193 |
+
transcript_model = "llama-3.1-70b-versatile";
|
| 194 |
+
|
| 195 |
+
-- Update all speaker profiles to use FREE Google TTS
|
| 196 |
+
UPDATE speaker_profile:tech_experts SET
|
| 197 |
+
tts_provider = "google",
|
| 198 |
+
tts_model = "gemini-1.5-flash";
|
| 199 |
+
|
| 200 |
+
UPDATE speaker_profile:solo_expert SET
|
| 201 |
+
tts_provider = "google",
|
| 202 |
+
tts_model = "gemini-1.5-flash";
|
| 203 |
+
|
| 204 |
+
UPDATE speaker_profile:business_panel SET
|
| 205 |
+
tts_provider = "google",
|
| 206 |
+
tts_model = "gemini-1.5-flash";
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
**File**: `c:\sem6-real\studyrocket\notebookllm\open-notebook\migrations\18_down.surrealql`
|
| 210 |
+
|
| 211 |
+
```sql
|
| 212 |
+
-- Migration 18 Down: Revert to original OpenAI models
|
| 213 |
+
|
| 214 |
+
UPDATE episode_profile:tech_discussion SET
|
| 215 |
+
outline_provider = "openai",
|
| 216 |
+
outline_model = "gpt-4o-mini",
|
| 217 |
+
transcript_provider = "openai",
|
| 218 |
+
transcript_model = "gpt-4o-mini";
|
| 219 |
+
|
| 220 |
+
UPDATE episode_profile:solo_expert SET
|
| 221 |
+
outline_provider = "openai",
|
| 222 |
+
outline_model = "gpt-4o-mini",
|
| 223 |
+
transcript_provider = "openai",
|
| 224 |
+
transcript_model = "gpt-4o-mini";
|
| 225 |
+
|
| 226 |
+
UPDATE episode_profile:business_analysis SET
|
| 227 |
+
outline_provider = "openai",
|
| 228 |
+
outline_model = "gpt-4o-mini",
|
| 229 |
+
transcript_provider = "openai",
|
| 230 |
+
transcript_model = "gpt-4o-mini";
|
| 231 |
+
|
| 232 |
+
UPDATE speaker_profile:tech_experts SET
|
| 233 |
+
tts_provider = "openai",
|
| 234 |
+
tts_model = "tts-1";
|
| 235 |
+
|
| 236 |
+
UPDATE speaker_profile:solo_expert SET
|
| 237 |
+
tts_provider = "openai",
|
| 238 |
+
tts_model = "tts-1";
|
| 239 |
+
|
| 240 |
+
UPDATE speaker_profile:business_panel SET
|
| 241 |
+
tts_provider = "openai",
|
| 242 |
+
tts_model = "tts-1";
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
### Step 2: Register Migration 18
|
| 246 |
+
|
| 247 |
+
**File**: `open_notebook/database/async_migrate.py`
|
| 248 |
+
|
| 249 |
+
Add migration 18 to the list (after line with migration 17):
|
| 250 |
+
|
| 251 |
+
```python
|
| 252 |
+
AsyncMigration.from_file("migrations/18.surrealql"),
|
| 253 |
+
|
| 254 |
+
# In down_migrations:
|
| 255 |
+
AsyncMigration.from_file("migrations/18_down.surrealql"),
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
---
|
| 259 |
+
|
| 260 |
+
## 📊 FREE TIER LIMITS
|
| 261 |
+
|
| 262 |
+
### Groq (LLM)
|
| 263 |
+
- **Rate Limit**: 30 requests/minute
|
| 264 |
+
- **Daily**: Generous (thousands of requests)
|
| 265 |
+
- **Models**: Llama 3.1 70B, Llama 3.1 8B, Mixtral
|
| 266 |
+
- **Context**: 8K-128K tokens depending on model
|
| 267 |
+
- **Cost**: $0 (100% FREE)
|
| 268 |
+
|
| 269 |
+
### Google Gemini (Embeddings + LLM + TTS)
|
| 270 |
+
- **Rate Limit**: 60 requests/minute (FREE tier)
|
| 271 |
+
- **Daily**: 1,500 requests/day (FREE tier)
|
| 272 |
+
- **Models**: Gemini 1.5 Flash, Gemini 1.5 Pro
|
| 273 |
+
- **Context**: Up to 1 MILLION tokens!
|
| 274 |
+
- **Cost**: $0 (FREE tier, then pay-as-you-go)
|
| 275 |
+
|
| 276 |
+
### Railway Hosting
|
| 277 |
+
- **Free**: $5 credit/month (hobby plan)
|
| 278 |
+
- **Usage**: ~$5-10/month for this app
|
| 279 |
+
- **Result**: First month FREE, then ~$5-10/month
|
| 280 |
+
|
| 281 |
+
---
|
| 282 |
+
|
| 283 |
+
## 🎯 MODEL USAGE BY FEATURE
|
| 284 |
+
|
| 285 |
+
Based on my analysis, here's what each feature uses:
|
| 286 |
+
|
| 287 |
+
| Feature | Current Model | FREE Alternative |
|
| 288 |
+
|---------|--------------|------------------|
|
| 289 |
+
| **Chat** | User-selected | Groq: `llama-3.1-70b-versatile` |
|
| 290 |
+
| **Transformations** | User-selected | Groq: `llama-3.1-70b-versatile` |
|
| 291 |
+
| **Embeddings** | User-selected | Gemini: `text-embedding-004` |
|
| 292 |
+
| **Large Context** | User-selected | Gemini: `gemini-1.5-pro` (1M context!) |
|
| 293 |
+
| **Podcast Outline** | `gpt-5-mini` (broken) | Groq: `llama-3.1-8b-instant` |
|
| 294 |
+
| **Podcast Transcript** | `gpt-5-mini` (broken) | Groq: `llama-3.1-70b-versatile` |
|
| 295 |
+
| **TTS (Podcast Audio)** | `gpt-4o-mini-tts` (doesn't exist) | Google: `gemini-1.5-flash` |
|
| 296 |
+
| **Search** | Embeddings model | Gemini: `text-embedding-004` |
|
| 297 |
+
| **Insights** | Transformation model | Groq: `llama-3.1-70b-versatile` |
|
| 298 |
+
|
| 299 |
+
---
|
| 300 |
+
|
| 301 |
+
## ✅ DEPLOYMENT CHECKLIST
|
| 302 |
+
|
| 303 |
+
### Before Pushing Code:
|
| 304 |
+
|
| 305 |
+
- [ ] Create `migrations/18.surrealql` (use FREE models)
|
| 306 |
+
- [ ] Create `migrations/18_down.surrealql` (rollback)
|
| 307 |
+
- [ ] Update `async_migrate.py` to include migration 18
|
| 308 |
+
- [ ] Get FREE Groq API key from https://console.groq.com/keys
|
| 309 |
+
- [ ] Get FREE Gemini API key from https://makersuite.google.com/app/apikey
|
| 310 |
+
|
| 311 |
+
### Railway Variables:
|
| 312 |
+
|
| 313 |
+
- [ ] Set `GROQ_API_KEY` (your FREE key)
|
| 314 |
+
- [ ] Set `GOOGLE_API_KEY` (your FREE key)
|
| 315 |
+
- [ ] Set `SURREAL_URL=ws://127.0.0.1:8000/rpc` (not localhost!)
|
| 316 |
+
- [ ] Set `INTERNAL_API_URL=http://127.0.0.1:5055`
|
| 317 |
+
- [ ] Keep all retry/worker settings as-is
|
| 318 |
+
- [ ] **DO NOT** set `OPENAI_API_KEY` (unless you have credit)
|
| 319 |
+
|
| 320 |
+
### After Deploy:
|
| 321 |
+
|
| 322 |
+
- [ ] Check logs for "Migrations completed successfully. Database is now at version 18"
|
| 323 |
+
- [ ] Test chat with Groq models
|
| 324 |
+
- [ ] Test embeddings with Gemini
|
| 325 |
+
- [ ] Test podcast generation (if needed)
|
| 326 |
+
- [ ] Monitor FREE tier usage in Groq/Gemini dashboards
|
| 327 |
+
|
| 328 |
+
---
|
| 329 |
+
|
| 330 |
+
## 💰 COST BREAKDOWN
|
| 331 |
+
|
| 332 |
+
### Monthly Costs (FREE TIER):
|
| 333 |
+
|
| 334 |
+
| Service | Cost |
|
| 335 |
+
|---------|------|
|
| 336 |
+
| **Groq LLM** | $0 (FREE) |
|
| 337 |
+
| **Gemini API** | $0 (FREE tier) |
|
| 338 |
+
| **Railway Hosting** | $5-10/month |
|
| 339 |
+
| **Domain** (optional) | $10-15/year |
|
| 340 |
+
| **Total** | **$5-10/month** |
|
| 341 |
+
|
| 342 |
+
### If You Exceed FREE Tiers:
|
| 343 |
+
|
| 344 |
+
- **Groq**: Still free (very generous limits)
|
| 345 |
+
- **Gemini**: $0.35 per 1M tokens (very cheap)
|
| 346 |
+
- **Worst case**: $10-20/month total
|
| 347 |
+
|
| 348 |
+
---
|
| 349 |
+
|
| 350 |
+
## 🚨 WARNINGS
|
| 351 |
+
|
| 352 |
+
1. **`gpt-5-mini` doesn't exist** - This will cause errors if OpenAI is called
|
| 353 |
+
2. **`gpt-4o-mini-tts` doesn't exist** - TTS will fail without migration
|
| 354 |
+
3. **Migration 18 is REQUIRED** - Old data uses broken model names
|
| 355 |
+
4. **Test locally first** - Run migrations on local DB before Railway
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## 🎉 BENEFITS OF FREE TIER SETUP
|
| 360 |
+
|
| 361 |
+
✅ **$0/month for AI** (only pay for Railway hosting)
|
| 362 |
+
✅ **Fast inference** with Groq (faster than OpenAI!)
|
| 363 |
+
✅ **1M token context** with Gemini (vs 128K for GPT-4)
|
| 364 |
+
✅ **No credit card needed** for Groq/Gemini free tiers
|
| 365 |
+
✅ **Scalable** - Upgrade to paid tiers if needed later
|
| 366 |
+
|
| 367 |
+
---
|
| 368 |
+
|
| 369 |
+
## 📞 SUPPORT
|
| 370 |
+
|
| 371 |
+
Get your FREE API keys:
|
| 372 |
+
- 🔥 **Groq**: https://console.groq.com/keys
|
| 373 |
+
- 🌟 **Gemini**: https://makersuite.google.com/app/apikey
|
| 374 |
+
|
| 375 |
+
Questions? Check the main docs or Discord!
|
FRONTEND_CONNECTION_GUIDE.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Frontend Connection to Hugging Face Backend
|
| 2 |
+
|
| 3 |
+
## ✅ Configuration Complete!
|
| 4 |
+
|
| 5 |
+
Your Next.js frontend is now configured to connect to your Hugging Face Space backend.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 📋 Changes Made
|
| 10 |
+
|
| 11 |
+
### 1. ✅ Frontend Environment Configuration
|
| 12 |
+
|
| 13 |
+
**Created:** [`frontend/.env.local`](frontend/.env.local)
|
| 14 |
+
|
| 15 |
+
```env
|
| 16 |
+
NEXT_PUBLIC_API_URL=https://baveshraam-open-notebook.hf.space
|
| 17 |
+
INTERNAL_API_URL=https://baveshraam-open-notebook.hf.space
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
This tells the frontend where to find your deployed backend API.
|
| 21 |
+
|
| 22 |
+
### 2. ✅ API Client Already Configured
|
| 23 |
+
|
| 24 |
+
**File:** [`frontend/src/lib/api/client.ts`](frontend/src/lib/api/client.ts)
|
| 25 |
+
|
| 26 |
+
The API client already uses dynamic configuration:
|
| 27 |
+
- ✅ Reads from `process.env.NEXT_PUBLIC_API_URL`
|
| 28 |
+
- ✅ Falls back to auto-detection
|
| 29 |
+
- ✅ Ultimate fallback to `http://127.0.0.1:5055`
|
| 30 |
+
|
| 31 |
+
**No changes needed!** The existing code will automatically use your `.env.local` settings.
|
| 32 |
+
|
| 33 |
+
### 3. ✅ CORS Configuration Updated
|
| 34 |
+
|
| 35 |
+
**File:** [`api/main.py`](api/main.py)
|
| 36 |
+
|
| 37 |
+
Updated CORS to allow requests from:
|
| 38 |
+
- ✅ `http://localhost:3000` (local development)
|
| 39 |
+
- ✅ `http://127.0.0.1:3000` (local development)
|
| 40 |
+
- ✅ `https://baveshraam-open-notebook.hf.space` (your HF Space)
|
| 41 |
+
- ✅ `*` (wildcard - allows any origin)
|
| 42 |
+
|
| 43 |
+
### 4. ✅ Hardcoded URLs Checked
|
| 44 |
+
|
| 45 |
+
All hardcoded `localhost:5055` references in the frontend are:
|
| 46 |
+
- ✅ **Fallback defaults only** (when env vars not set)
|
| 47 |
+
- ✅ **Example text in error messages** (documentation)
|
| 48 |
+
- ✅ **No action needed** - proper fallback behavior
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## 🎯 Next Steps
|
| 53 |
+
|
| 54 |
+
### Step 1: Deploy Backend Changes to Hugging Face
|
| 55 |
+
|
| 56 |
+
The CORS update needs to be deployed to your Hugging Face Space:
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
git add api/main.py
|
| 60 |
+
git commit -m "Update CORS for frontend connection"
|
| 61 |
+
git push
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
Wait for Hugging Face to rebuild (check the Space logs).
|
| 65 |
+
|
| 66 |
+
### Step 2: Start Your Frontend Locally
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
cd frontend
|
| 70 |
+
npm install # If not already done
|
| 71 |
+
npm run dev
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
Your frontend will start on `http://localhost:3000` and connect to:
|
| 75 |
+
- **Backend API:** `https://baveshraam-open-notebook.hf.space`
|
| 76 |
+
|
| 77 |
+
### Step 3: Test the Connection
|
| 78 |
+
|
| 79 |
+
1. **Open browser:** http://localhost:3000
|
| 80 |
+
2. **Check browser console** for API connection messages
|
| 81 |
+
3. **Try creating a notebook** or any API-dependent feature
|
| 82 |
+
4. **Check Network tab** to verify requests go to your HF Space URL
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## 🔍 How It Works
|
| 87 |
+
|
| 88 |
+
### Configuration Priority
|
| 89 |
+
|
| 90 |
+
The frontend uses a smart fallback system:
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
1. Runtime config from /config endpoint (uses .env.local)
|
| 94 |
+
↓
|
| 95 |
+
2. Build-time NEXT_PUBLIC_API_URL
|
| 96 |
+
↓
|
| 97 |
+
3. Auto-detection from browser URL
|
| 98 |
+
↓
|
| 99 |
+
4. Fallback to http://127.0.0.1:5055
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### Environment Variables
|
| 103 |
+
|
| 104 |
+
| Variable | Used By | Purpose |
|
| 105 |
+
|----------|---------|---------|
|
| 106 |
+
| `NEXT_PUBLIC_API_URL` | Browser | Client-side API calls |
|
| 107 |
+
| `INTERNAL_API_URL` | Next.js Server | Server-side proxying |
|
| 108 |
+
|
| 109 |
+
### URL Structure
|
| 110 |
+
|
| 111 |
+
The frontend automatically constructs API URLs:
|
| 112 |
+
|
| 113 |
+
- Base URL: `https://baveshraam-open-notebook.hf.space`
|
| 114 |
+
- API Endpoint: `/api` (added automatically)
|
| 115 |
+
- Full API URL: `https://baveshraam-open-notebook.hf.space/api`
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## 🛠️ Troubleshooting
|
| 120 |
+
|
| 121 |
+
### Issue: "Failed to fetch" or CORS errors
|
| 122 |
+
|
| 123 |
+
**Solution:**
|
| 124 |
+
1. Verify backend is running: https://baveshraam-open-notebook.hf.space/health
|
| 125 |
+
2. Check backend logs for CORS rejections
|
| 126 |
+
3. Ensure CORS changes were deployed (check git commit)
|
| 127 |
+
|
| 128 |
+
### Issue: Frontend still connects to localhost
|
| 129 |
+
|
| 130 |
+
**Solution:**
|
| 131 |
+
1. Verify `.env.local` file exists in `frontend/` directory
|
| 132 |
+
2. Restart Next.js dev server: `npm run dev`
|
| 133 |
+
3. Check browser console for config messages
|
| 134 |
+
4. Clear browser cache and reload
|
| 135 |
+
|
| 136 |
+
### Issue: 404 errors on /api/* endpoints
|
| 137 |
+
|
| 138 |
+
**Solution:**
|
| 139 |
+
1. Check that backend is running: https://baveshraam-open-notebook.hf.space/api/config
|
| 140 |
+
2. Verify the URL doesn't have double `/api/api/`
|
| 141 |
+
3. Check Next.js rewrite configuration in `next.config.ts`
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
## 📝 Environment Files Reference
|
| 146 |
+
|
| 147 |
+
### `.env.local` (active configuration)
|
| 148 |
+
Your current deployment settings.
|
| 149 |
+
|
| 150 |
+
### `.env.local.example` (template)
|
| 151 |
+
Copy this when deploying to new environments.
|
| 152 |
+
|
| 153 |
+
### `.env.example` (backend configuration)
|
| 154 |
+
Backend environment variables (already configured on HF Space).
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## 🎉 Success Indicators
|
| 159 |
+
|
| 160 |
+
You'll know the connection works when:
|
| 161 |
+
|
| 162 |
+
1. ✅ Browser console shows: `✅ [Config] Successfully loaded API config`
|
| 163 |
+
2. ✅ Network tab shows requests to `baveshraam-open-notebook.hf.space`
|
| 164 |
+
3. ✅ No CORS errors in browser console
|
| 165 |
+
4. ✅ Features like "Create Notebook" work correctly
|
| 166 |
+
5. ✅ Health check responds: https://baveshraam-open-notebook.hf.space/health
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
|
| 170 |
+
## 🚀 Deploy Frontend (Optional)
|
| 171 |
+
|
| 172 |
+
When ready to deploy your frontend:
|
| 173 |
+
|
| 174 |
+
### Vercel / Netlify
|
| 175 |
+
Add environment variables in dashboard:
|
| 176 |
+
```
|
| 177 |
+
NEXT_PUBLIC_API_URL=https://baveshraam-open-notebook.hf.space
|
| 178 |
+
INTERNAL_API_URL=https://baveshraam-open-notebook.hf.space
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
### Docker
|
| 182 |
+
```bash
|
| 183 |
+
docker build -t open-notebook-frontend ./frontend
|
| 184 |
+
docker run -p 3000:3000 \
|
| 185 |
+
-e NEXT_PUBLIC_API_URL=https://baveshraam-open-notebook.hf.space \
|
| 186 |
+
-e INTERNAL_API_URL=https://baveshraam-open-notebook.hf.space \
|
| 187 |
+
open-notebook-frontend
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## 📚 Architecture Overview
|
| 193 |
+
|
| 194 |
+
```
|
| 195 |
+
┌─────────────────────┐
|
| 196 |
+
│ Browser │
|
| 197 |
+
│ localhost:3000 │
|
| 198 |
+
└──────────┬──────────┘
|
| 199 |
+
│ NEXT_PUBLIC_API_URL
|
| 200 |
+
↓
|
| 201 |
+
┌─────────────────────────────────────┐
|
| 202 |
+
│ Hugging Face Space Backend │
|
| 203 |
+
│ baveshraam-open-notebook.hf.space │
|
| 204 |
+
│ │
|
| 205 |
+
│ ┌──────────┐ ┌──────────┐ │
|
| 206 |
+
│ │ FastAPI │ ←──→ │ SurrealDB│ │
|
| 207 |
+
│ └──────────┘ └──────────┘ │
|
| 208 |
+
└─────────────────────────────────────┘
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
---
|
| 212 |
+
|
| 213 |
+
## 🎊 You're All Set!
|
| 214 |
+
|
| 215 |
+
Your frontend is now ready to connect to your Hugging Face deployed backend. Start the frontend with `npm run dev` and test away!
|
HUGGINGFACE_DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to deploy Open Notebook to Hugging Face Spaces using the Docker SDK.
|
| 4 |
+
|
| 5 |
+
## Files Created
|
| 6 |
+
|
| 7 |
+
1. **`Dockerfile.huggingface`** - Docker configuration for single-container deployment
|
| 8 |
+
2. **`start.sh`** - Startup script that launches SurrealDB and FastAPI
|
| 9 |
+
3. **`open_notebook/database/connection.py`** - SurrealDB connection with retry logic
|
| 10 |
+
4. **`requirements.txt`** - Python dependencies extracted from pyproject.toml
|
| 11 |
+
5. **`README_HUGGINGFACE.md`** - Hugging Face Spaces README
|
| 12 |
+
|
| 13 |
+
## Deployment Steps
|
| 14 |
+
|
| 15 |
+
### 1. Rename Dockerfile
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
# Rename the Hugging Face Dockerfile to the standard name
|
| 19 |
+
mv Dockerfile.huggingface Dockerfile
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### 2. Create Hugging Face Space
|
| 23 |
+
|
| 24 |
+
1. Go to [huggingface.co/new-space](https://huggingface.co/new-space)
|
| 25 |
+
2. Choose:
|
| 26 |
+
- **Space name**: `open-notebook` (or your preferred name)
|
| 27 |
+
- **License**: MIT
|
| 28 |
+
- **SDK**: Docker
|
| 29 |
+
- **Visibility**: Public or Private
|
| 30 |
+
|
| 31 |
+
### 3. Push Code to Hugging Face
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
# Add Hugging Face remote
|
| 35 |
+
git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/open-notebook
|
| 36 |
+
|
| 37 |
+
# Push to Hugging Face
|
| 38 |
+
git push hf main
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 4. Configure Secrets
|
| 42 |
+
|
| 43 |
+
In your Hugging Face Space settings, add these secrets:
|
| 44 |
+
|
| 45 |
+
**Required (add at least one)**:
|
| 46 |
+
- `OPENAI_API_KEY` - Your OpenAI API key
|
| 47 |
+
- `ANTHROPIC_API_KEY` - Your Anthropic (Claude) API key
|
| 48 |
+
- `GOOGLE_API_KEY` - Your Google (Gemini) API key
|
| 49 |
+
|
| 50 |
+
**Optional**:
|
| 51 |
+
- `GROQ_API_KEY` - Groq API key
|
| 52 |
+
- `MISTRAL_API_KEY` - Mistral API key
|
| 53 |
+
|
| 54 |
+
### 5. Wait for Build
|
| 55 |
+
|
| 56 |
+
Hugging Face will automatically build your Docker container. This takes about 10-15 minutes.
|
| 57 |
+
|
| 58 |
+
## Important Notes
|
| 59 |
+
|
| 60 |
+
### Port Configuration
|
| 61 |
+
|
| 62 |
+
Hugging Face Spaces requires port **7860**. The Dockerfile is configured to use this port.
|
| 63 |
+
|
| 64 |
+
### In-Memory Storage
|
| 65 |
+
|
| 66 |
+
This deployment uses SurrealDB in **memory mode** (`memory`). This means:
|
| 67 |
+
- ✅ Fast performance
|
| 68 |
+
- ✅ No disk space issues
|
| 69 |
+
- ❌ Data is lost when container restarts
|
| 70 |
+
- ❌ Not suitable for production
|
| 71 |
+
|
| 72 |
+
For persistent storage, modify `start.sh`:
|
| 73 |
+
```bash
|
| 74 |
+
# Change from:
|
| 75 |
+
surreal start --log debug --user root --pass root memory &
|
| 76 |
+
|
| 77 |
+
# To:
|
| 78 |
+
surreal start --log debug --user root --pass root file://data/database.db &
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### Retry Logic
|
| 82 |
+
|
| 83 |
+
The connection module (`open_notebook/database/connection.py`) includes:
|
| 84 |
+
- **5 retry attempts** with exponential backoff
|
| 85 |
+
- **2-second initial delay**, increasing with each retry
|
| 86 |
+
- Ensures SurrealDB is ready before FastAPI starts accepting requests
|
| 87 |
+
|
| 88 |
+
### Resource Limits
|
| 89 |
+
|
| 90 |
+
Hugging Face Spaces free tier:
|
| 91 |
+
- **2 CPU cores**
|
| 92 |
+
- **16GB RAM**
|
| 93 |
+
- **50GB disk** (ephemeral)
|
| 94 |
+
|
| 95 |
+
The sentence-transformer model (`all-MiniLM-L6-v2`) is pre-downloaded during build to avoid startup delays.
|
| 96 |
+
|
| 97 |
+
## Testing Your Deployment
|
| 98 |
+
|
| 99 |
+
Once deployed, test these endpoints:
|
| 100 |
+
|
| 101 |
+
```bash
|
| 102 |
+
# Health check
|
| 103 |
+
curl https://YOUR_USERNAME-open-notebook.hf.space/health
|
| 104 |
+
|
| 105 |
+
# API documentation
|
| 106 |
+
https://YOUR_USERNAME-open-notebook.hf.space/docs
|
| 107 |
+
|
| 108 |
+
# Create a notebook
|
| 109 |
+
curl -X POST https://YOUR_USERNAME-open-notebook.hf.space/api/notebooks \
|
| 110 |
+
-H "Content-Type: application/json" \
|
| 111 |
+
-d '{"name": "Test Notebook", "description": "My first notebook"}'
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## Troubleshooting
|
| 115 |
+
|
| 116 |
+
### Build Fails
|
| 117 |
+
|
| 118 |
+
Check the build logs in Hugging Face. Common issues:
|
| 119 |
+
- **Missing dependencies**: Verify all packages in requirements.txt
|
| 120 |
+
- **SurrealDB install fails**: Check internet connectivity during build
|
| 121 |
+
- **Out of memory**: Reduce the size of pre-downloaded models
|
| 122 |
+
|
| 123 |
+
### Runtime Errors
|
| 124 |
+
|
| 125 |
+
Check the runtime logs:
|
| 126 |
+
- **"Connection refused"**: SurrealDB didn't start - increase wait time in start.sh
|
| 127 |
+
- **"Database migration failed"**: Check SURREAL_* environment variables
|
| 128 |
+
- **"Model not found"**: Ensure sentence-transformers model downloaded during build
|
| 129 |
+
|
| 130 |
+
### Performance Issues
|
| 131 |
+
|
| 132 |
+
On free tier:
|
| 133 |
+
- Limit concurrent requests
|
| 134 |
+
- Use lighter LLM models (Gemini, GPT-3.5-turbo)
|
| 135 |
+
- Reduce chunk size for embeddings
|
| 136 |
+
- Enable caching for repeated queries
|
| 137 |
+
|
| 138 |
+
## Upgrading to Persistent Storage
|
| 139 |
+
|
| 140 |
+
To use external SurrealDB with persistent storage:
|
| 141 |
+
|
| 142 |
+
1. Deploy SurrealDB separately (Railway, Fly.io, etc.)
|
| 143 |
+
2. Update environment variables in Hugging Face settings:
|
| 144 |
+
```
|
| 145 |
+
SURREAL_URL=wss://your-surrealdb-instance.com/rpc
|
| 146 |
+
SURREAL_USER=your_username
|
| 147 |
+
SURREAL_PASS=your_password
|
| 148 |
+
```
|
| 149 |
+
3. Remove SurrealDB startup from `start.sh`:
|
| 150 |
+
```bash
|
| 151 |
+
#!/bin/bash
|
| 152 |
+
set -e
|
| 153 |
+
echo "Starting FastAPI application on port 7860..."
|
| 154 |
+
exec uvicorn api.main:app --host 0.0.0.0 --port 7860
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
## Alternative Deployment: Split Services
|
| 158 |
+
|
| 159 |
+
For better performance, consider splitting frontend and backend:
|
| 160 |
+
|
| 161 |
+
**Backend Space** (this configuration):
|
| 162 |
+
- Python Docker SDK
|
| 163 |
+
- FastAPI + SurrealDB
|
| 164 |
+
- Port 7860
|
| 165 |
+
|
| 166 |
+
**Frontend Space** (separate):
|
| 167 |
+
- Node.js SDK or Static
|
| 168 |
+
- Next.js frontend
|
| 169 |
+
- Points to backend API
|
| 170 |
+
|
| 171 |
+
## Cost Optimization
|
| 172 |
+
|
| 173 |
+
**Free Tier Recommendations**:
|
| 174 |
+
- Use Google Gemini (free tier: 60 requests/min)
|
| 175 |
+
- Pre-generate embeddings during low traffic
|
| 176 |
+
- Implement request queuing
|
| 177 |
+
- Cache LLM responses
|
| 178 |
+
|
| 179 |
+
**Paid Tier Benefits** ($9/month):
|
| 180 |
+
- No cold starts
|
| 181 |
+
- More CPU/RAM
|
| 182 |
+
- Persistent storage
|
| 183 |
+
- Custom domains
|
| 184 |
+
|
| 185 |
+
## Security Considerations
|
| 186 |
+
|
| 187 |
+
1. **Never commit API keys** - Use Hugging Face Secrets
|
| 188 |
+
2. **Enable authentication** - Modify `api/auth.py` to add user login
|
| 189 |
+
3. **Rate limiting** - Add rate limits to prevent abuse
|
| 190 |
+
4. **CORS configuration** - Restrict allowed origins in production
|
| 191 |
+
5. **Input validation** - All file uploads should be validated
|
| 192 |
+
|
| 193 |
+
## Support
|
| 194 |
+
|
| 195 |
+
- **Discord**: [https://discord.gg/37XJPXfz2w](https://discord.gg/37XJPXfz2w)
|
| 196 |
+
- **GitHub Issues**: [https://github.com/baveshraam/software-eng-proj/issues](https://github.com/baveshraam/software-eng-proj/issues)
|
| 197 |
+
- **Documentation**: [https://www.open-notebook.ai](https://www.open-notebook.ai)
|
LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
Copyright (c) 2024 Luis Novo
|
| 3 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 4 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 5 |
+
in the Software without restriction, including without limitation the rights
|
| 6 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 7 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 8 |
+
furnished to do so, subject to the following conditions:
|
| 9 |
+
The above copyright notice and this permission notice shall be included in all
|
| 10 |
+
copies or substantial portions of the Software.
|
| 11 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 12 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 13 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 14 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 15 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 16 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 17 |
+
SOFTWARE.
|
Makefile
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: run frontend check ruff database lint api start-all stop-all status clean-cache worker worker-start worker-stop worker-restart
|
| 2 |
+
.PHONY: docker-buildx-prepare docker-buildx-clean docker-buildx-reset
|
| 3 |
+
.PHONY: docker-push docker-push-latest docker-release tag export-docs
|
| 4 |
+
|
| 5 |
+
# Get version from pyproject.toml
|
| 6 |
+
VERSION := $(shell grep -m1 version pyproject.toml | cut -d'"' -f2)
|
| 7 |
+
|
| 8 |
+
# Image names for both registries
|
| 9 |
+
DOCKERHUB_IMAGE := lfnovo/open_notebook
|
| 10 |
+
GHCR_IMAGE := ghcr.io/lfnovo/open-notebook
|
| 11 |
+
|
| 12 |
+
# Build platforms
|
| 13 |
+
PLATFORMS := linux/amd64,linux/arm64
|
| 14 |
+
|
| 15 |
+
database:
|
| 16 |
+
docker compose up -d surrealdb
|
| 17 |
+
|
| 18 |
+
run:
|
| 19 |
+
@echo "⚠️ Warning: Starting frontend only. For full functionality, use 'make start-all'"
|
| 20 |
+
cd frontend && npm run dev
|
| 21 |
+
|
| 22 |
+
frontend:
|
| 23 |
+
cd frontend && npm run dev
|
| 24 |
+
|
| 25 |
+
lint:
|
| 26 |
+
uv run python -m mypy .
|
| 27 |
+
|
| 28 |
+
ruff:
|
| 29 |
+
ruff check . --fix
|
| 30 |
+
|
| 31 |
+
# === Docker Build Setup ===
|
| 32 |
+
docker-buildx-prepare:
|
| 33 |
+
@docker buildx inspect multi-platform-builder >/dev/null 2>&1 || \
|
| 34 |
+
docker buildx create --use --name multi-platform-builder --driver docker-container
|
| 35 |
+
@docker buildx use multi-platform-builder
|
| 36 |
+
|
| 37 |
+
docker-buildx-clean:
|
| 38 |
+
@echo "🧹 Cleaning up buildx builders..."
|
| 39 |
+
@docker buildx rm multi-platform-builder 2>/dev/null || true
|
| 40 |
+
@docker ps -a | grep buildx_buildkit | awk '{print $$1}' | xargs -r docker rm -f 2>/dev/null || true
|
| 41 |
+
@echo "✅ Buildx cleanup complete!"
|
| 42 |
+
|
| 43 |
+
docker-buildx-reset: docker-buildx-clean docker-buildx-prepare
|
| 44 |
+
@echo "✅ Buildx reset complete!"
|
| 45 |
+
|
| 46 |
+
# === Docker Build Targets ===
|
| 47 |
+
|
| 48 |
+
# Build and push version tags ONLY (no latest) for both regular and single images
|
| 49 |
+
docker-push: docker-buildx-prepare
|
| 50 |
+
@echo "📤 Building and pushing version $(VERSION) to both registries..."
|
| 51 |
+
@echo "🔨 Building regular image..."
|
| 52 |
+
docker buildx build --pull \
|
| 53 |
+
--platform $(PLATFORMS) \
|
| 54 |
+
--progress=plain \
|
| 55 |
+
-t $(DOCKERHUB_IMAGE):$(VERSION) \
|
| 56 |
+
-t $(GHCR_IMAGE):$(VERSION) \
|
| 57 |
+
--push \
|
| 58 |
+
.
|
| 59 |
+
@echo "🔨 Building single-container image..."
|
| 60 |
+
docker buildx build --pull \
|
| 61 |
+
--platform $(PLATFORMS) \
|
| 62 |
+
--progress=plain \
|
| 63 |
+
-f Dockerfile.single \
|
| 64 |
+
-t $(DOCKERHUB_IMAGE):$(VERSION)-single \
|
| 65 |
+
-t $(GHCR_IMAGE):$(VERSION)-single \
|
| 66 |
+
--push \
|
| 67 |
+
.
|
| 68 |
+
@echo "✅ Pushed version $(VERSION) to both registries (latest NOT updated)"
|
| 69 |
+
@echo " 📦 Docker Hub:"
|
| 70 |
+
@echo " - $(DOCKERHUB_IMAGE):$(VERSION)"
|
| 71 |
+
@echo " - $(DOCKERHUB_IMAGE):$(VERSION)-single"
|
| 72 |
+
@echo " 📦 GHCR:"
|
| 73 |
+
@echo " - $(GHCR_IMAGE):$(VERSION)"
|
| 74 |
+
@echo " - $(GHCR_IMAGE):$(VERSION)-single"
|
| 75 |
+
|
| 76 |
+
# Update v1-latest tags to current version (both regular and single images)
|
| 77 |
+
docker-push-latest: docker-buildx-prepare
|
| 78 |
+
@echo "📤 Updating v1-latest tags to version $(VERSION)..."
|
| 79 |
+
@echo "🔨 Building regular image with latest tag..."
|
| 80 |
+
docker buildx build --pull \
|
| 81 |
+
--platform $(PLATFORMS) \
|
| 82 |
+
--progress=plain \
|
| 83 |
+
-t $(DOCKERHUB_IMAGE):$(VERSION) \
|
| 84 |
+
-t $(DOCKERHUB_IMAGE):v1-latest \
|
| 85 |
+
-t $(GHCR_IMAGE):$(VERSION) \
|
| 86 |
+
-t $(GHCR_IMAGE):v1-latest \
|
| 87 |
+
--push \
|
| 88 |
+
.
|
| 89 |
+
@echo "🔨 Building single-container image with latest tag..."
|
| 90 |
+
docker buildx build --pull \
|
| 91 |
+
--platform $(PLATFORMS) \
|
| 92 |
+
--progress=plain \
|
| 93 |
+
-f Dockerfile.single \
|
| 94 |
+
-t $(DOCKERHUB_IMAGE):$(VERSION)-single \
|
| 95 |
+
-t $(DOCKERHUB_IMAGE):v1-latest-single \
|
| 96 |
+
-t $(GHCR_IMAGE):$(VERSION)-single \
|
| 97 |
+
-t $(GHCR_IMAGE):v1-latest-single \
|
| 98 |
+
--push \
|
| 99 |
+
.
|
| 100 |
+
@echo "✅ Updated v1-latest to version $(VERSION)"
|
| 101 |
+
@echo " 📦 Docker Hub:"
|
| 102 |
+
@echo " - $(DOCKERHUB_IMAGE):$(VERSION) → v1-latest"
|
| 103 |
+
@echo " - $(DOCKERHUB_IMAGE):$(VERSION)-single → v1-latest-single"
|
| 104 |
+
@echo " 📦 GHCR:"
|
| 105 |
+
@echo " - $(GHCR_IMAGE):$(VERSION) → v1-latest"
|
| 106 |
+
@echo " - $(GHCR_IMAGE):$(VERSION)-single → v1-latest-single"
|
| 107 |
+
|
| 108 |
+
# Full release: push version AND update latest tags
|
| 109 |
+
docker-release: docker-push-latest
|
| 110 |
+
@echo "✅ Full release complete for version $(VERSION)"
|
| 111 |
+
|
| 112 |
+
tag:
|
| 113 |
+
@version=$$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/'); \
|
| 114 |
+
echo "Creating tag v$$version"; \
|
| 115 |
+
git tag "v$$version"; \
|
| 116 |
+
git push origin "v$$version"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
dev:
|
| 120 |
+
docker compose -f docker-compose.dev.yml up --build
|
| 121 |
+
|
| 122 |
+
full:
|
| 123 |
+
docker compose -f docker-compose.full.yml up --build
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
api:
|
| 127 |
+
uv run run_api.py
|
| 128 |
+
|
| 129 |
+
# === Worker Management ===
|
| 130 |
+
.PHONY: worker worker-start worker-stop worker-restart
|
| 131 |
+
|
| 132 |
+
worker: worker-start
|
| 133 |
+
|
| 134 |
+
worker-start:
|
| 135 |
+
@echo "Starting surreal-commands worker..."
|
| 136 |
+
uv run --env-file .env surreal-commands-worker --import-modules commands
|
| 137 |
+
|
| 138 |
+
worker-stop:
|
| 139 |
+
@echo "Stopping surreal-commands worker..."
|
| 140 |
+
pkill -f "surreal-commands-worker" || true
|
| 141 |
+
|
| 142 |
+
worker-restart: worker-stop
|
| 143 |
+
@sleep 2
|
| 144 |
+
@$(MAKE) worker-start
|
| 145 |
+
|
| 146 |
+
# === Service Management ===
|
| 147 |
+
start-all:
|
| 148 |
+
@echo "🚀 Starting Open Notebook (Database + API + Worker + Frontend)..."
|
| 149 |
+
@echo "📊 Starting SurrealDB..."
|
| 150 |
+
@docker compose -f docker-compose.dev.yml up -d surrealdb
|
| 151 |
+
@sleep 3
|
| 152 |
+
@echo "🔧 Starting API backend..."
|
| 153 |
+
@uv run run_api.py &
|
| 154 |
+
@sleep 3
|
| 155 |
+
@echo "⚙️ Starting background worker..."
|
| 156 |
+
@uv run --env-file .env surreal-commands-worker --import-modules commands &
|
| 157 |
+
@sleep 2
|
| 158 |
+
@echo "🌐 Starting Next.js frontend..."
|
| 159 |
+
@echo "✅ All services started!"
|
| 160 |
+
@echo "📱 Frontend: http://localhost:3000"
|
| 161 |
+
@echo "🔗 API: http://localhost:5055"
|
| 162 |
+
@echo "📚 API Docs: http://localhost:5055/docs"
|
| 163 |
+
cd frontend && npm run dev
|
| 164 |
+
|
| 165 |
+
stop-all:
|
| 166 |
+
@echo "🛑 Stopping all Open Notebook services..."
|
| 167 |
+
@pkill -f "next dev" || true
|
| 168 |
+
@pkill -f "surreal-commands-worker" || true
|
| 169 |
+
@pkill -f "run_api.py" || true
|
| 170 |
+
@pkill -f "uvicorn api.main:app" || true
|
| 171 |
+
@docker compose down
|
| 172 |
+
@echo "✅ All services stopped!"
|
| 173 |
+
|
| 174 |
+
status:
|
| 175 |
+
@echo "📊 Open Notebook Service Status:"
|
| 176 |
+
@echo "Database (SurrealDB):"
|
| 177 |
+
@docker compose ps surrealdb 2>/dev/null || echo " ❌ Not running"
|
| 178 |
+
@echo "API Backend:"
|
| 179 |
+
@pgrep -f "run_api.py\|uvicorn api.main:app" >/dev/null && echo " ✅ Running" || echo " ❌ Not running"
|
| 180 |
+
@echo "Background Worker:"
|
| 181 |
+
@pgrep -f "surreal-commands-worker" >/dev/null && echo " ✅ Running" || echo " ❌ Not running"
|
| 182 |
+
@echo "Next.js Frontend:"
|
| 183 |
+
@pgrep -f "next dev" >/dev/null && echo " ✅ Running" || echo " ❌ Not running"
|
| 184 |
+
|
| 185 |
+
# === Documentation Export ===
|
| 186 |
+
export-docs:
|
| 187 |
+
@echo "📚 Exporting documentation..."
|
| 188 |
+
@uv run python scripts/export_docs.py
|
| 189 |
+
@echo "✅ Documentation export complete!"
|
| 190 |
+
|
| 191 |
+
# === Cleanup ===
|
| 192 |
+
clean-cache:
|
| 193 |
+
@echo "🧹 Cleaning cache directories..."
|
| 194 |
+
@find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
| 195 |
+
@find . -name ".mypy_cache" -type d -exec rm -rf {} + 2>/dev/null || true
|
| 196 |
+
@find . -name ".ruff_cache" -type d -exec rm -rf {} + 2>/dev/null || true
|
| 197 |
+
@find . -name ".pytest_cache" -type d -exec rm -rf {} + 2>/dev/null || true
|
| 198 |
+
@find . -name "*.pyc" -type f -delete 2>/dev/null || true
|
| 199 |
+
@find . -name "*.pyo" -type f -delete 2>/dev/null || true
|
| 200 |
+
@find . -name "*.pyd" -type f -delete 2>/dev/null || true
|
| 201 |
+
@echo "✅ Cache directories cleaned!"
|
QUICK_FIX.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎯 Railway Quick Fix - Copy & Paste Ready
|
| 2 |
+
|
| 3 |
+
## Variables to CHANGE in Railway Dashboard
|
| 4 |
+
|
| 5 |
+
### 1. Fix SURREAL_URL (CRITICAL)
|
| 6 |
+
```bash
|
| 7 |
+
# ❌ Remove or change this:
|
| 8 |
+
SURREAL_URL=ws://localhost:8000/rpc
|
| 9 |
+
|
| 10 |
+
# ✅ Use this instead:
|
| 11 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
### 2. Add INTERNAL_API_URL (CRITICAL)
|
| 15 |
+
```bash
|
| 16 |
+
# ✅ Add this new variable:
|
| 17 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
### 3. Update API_URL After First Deploy
|
| 21 |
+
```bash
|
| 22 |
+
# ❌ Current:
|
| 23 |
+
API_URL=http://localhost:5055
|
| 24 |
+
|
| 25 |
+
# ✅ Change to YOUR Railway domain (after you get it):
|
| 26 |
+
API_URL=https://your-app-production-xxxx.up.railway.app
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## Variables to ADD in Railway Dashboard
|
| 30 |
+
|
| 31 |
+
### 4. Add Your AI API Keys
|
| 32 |
+
```bash
|
| 33 |
+
# Google Gemini (Required for your setup)
|
| 34 |
+
GOOGLE_API_KEY=paste_your_gemini_key_here
|
| 35 |
+
|
| 36 |
+
# Groq (Required for your setup)
|
| 37 |
+
GROQ_API_KEY=paste_your_groq_key_here
|
| 38 |
+
|
| 39 |
+
# If using Ollama for Llama models (Optional)
|
| 40 |
+
OLLAMA_API_BASE=http://your-ollama-host:11434
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Variables to KEEP (Don't Change)
|
| 44 |
+
|
| 45 |
+
These are already correct in your Railway:
|
| 46 |
+
```bash
|
| 47 |
+
✅ SURREAL_USER=root
|
| 48 |
+
✅ SURREAL_PASSWORD=root
|
| 49 |
+
✅ SURREAL_NAMESPACE=test
|
| 50 |
+
✅ SURREAL_DATABASE=test
|
| 51 |
+
✅ SURREAL_COMMANDS_MAX_TASKS=5
|
| 52 |
+
✅ SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 53 |
+
✅ SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 54 |
+
✅ SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 55 |
+
✅ SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 56 |
+
✅ SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Complete Variable List for Railway
|
| 60 |
+
|
| 61 |
+
Copy this entire block and set in Railway Variables:
|
| 62 |
+
|
| 63 |
+
```plaintext
|
| 64 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 65 |
+
SURREAL_USER=root
|
| 66 |
+
SURREAL_PASSWORD=root
|
| 67 |
+
SURREAL_NAMESPACE=test
|
| 68 |
+
SURREAL_DATABASE=test
|
| 69 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 70 |
+
API_URL=https://YOUR_RAILWAY_DOMAIN_HERE
|
| 71 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 72 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 73 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 74 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 75 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 76 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 77 |
+
GOOGLE_API_KEY=your_gemini_api_key
|
| 78 |
+
GROQ_API_KEY=your_groq_api_key
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
## Deployment Steps
|
| 82 |
+
|
| 83 |
+
1. **Update Railway Variables** (see above)
|
| 84 |
+
2. **Push Code**:
|
| 85 |
+
```powershell
|
| 86 |
+
git add .
|
| 87 |
+
git commit -m "Fix Railway config and add migrations 15-17"
|
| 88 |
+
git push origin main
|
| 89 |
+
```
|
| 90 |
+
3. **Wait for Deploy** (Railway auto-deploys from GitHub)
|
| 91 |
+
4. **Get Railway Domain** from Railway Dashboard
|
| 92 |
+
5. **Update API_URL** with your actual domain
|
| 93 |
+
6. **Verify** at `https://your-domain/api/health`
|
| 94 |
+
|
| 95 |
+
## That's It!
|
| 96 |
+
|
| 97 |
+
Your deployment should work after these changes.
|
| 98 |
+
|
| 99 |
+
Questions? Read `YOUR_RAILWAY_CONFIG.md` for detailed explanations.
|
RAILWAY.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚂 Railway Deployment Guide for Open Notebook
|
| 2 |
+
|
| 3 |
+
## Prerequisites
|
| 4 |
+
|
| 5 |
+
- A [Railway](https://railway.app/) account
|
| 6 |
+
- At least one AI API key (OpenAI, Anthropic, etc.)
|
| 7 |
+
|
| 8 |
+
## Quick Deploy
|
| 9 |
+
|
| 10 |
+
### Option 1: Deploy from GitHub (Recommended)
|
| 11 |
+
|
| 12 |
+
1. **Fork this repository** to your GitHub account
|
| 13 |
+
|
| 14 |
+
2. **Create a new Railway project:**
|
| 15 |
+
- Go to [Railway](https://railway.app/)
|
| 16 |
+
- Click "New Project"
|
| 17 |
+
- Select "Deploy from GitHub repo"
|
| 18 |
+
- Choose your forked repository
|
| 19 |
+
|
| 20 |
+
3. **Configure Environment Variables:**
|
| 21 |
+
|
| 22 |
+
Go to your Railway service → Variables tab and add:
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
# Required Variables
|
| 26 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 27 |
+
SURREAL_USER=root
|
| 28 |
+
SURREAL_PASSWORD=root
|
| 29 |
+
SURREAL_NAMESPACE=open_notebook
|
| 30 |
+
SURREAL_DATABASE=production
|
| 31 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 32 |
+
|
| 33 |
+
# Add your AI API key (at least one required)
|
| 34 |
+
OPENAI_API_KEY=sk-your-key-here
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
4. **After first deployment, set the API_URL:**
|
| 38 |
+
|
| 39 |
+
Once Railway generates your public URL (e.g., `https://your-app.up.railway.app`):
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
API_URL=https://your-app.up.railway.app
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
5. **Configure Railway Settings:**
|
| 46 |
+
- Go to Settings → Networking
|
| 47 |
+
- Make sure port 8080 is exposed (Railway should auto-detect this)
|
| 48 |
+
- Health check path: `/api/health`
|
| 49 |
+
|
| 50 |
+
6. **Redeploy** after adding the API_URL
|
| 51 |
+
|
| 52 |
+
### Option 2: Deploy with Railway CLI
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# Install Railway CLI
|
| 56 |
+
npm i -g @railway/cli
|
| 57 |
+
|
| 58 |
+
# Login to Railway
|
| 59 |
+
railway login
|
| 60 |
+
|
| 61 |
+
# Link to project (or create new)
|
| 62 |
+
railway link
|
| 63 |
+
|
| 64 |
+
# Set environment variables
|
| 65 |
+
railway variables set SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 66 |
+
railway variables set SURREAL_USER=root
|
| 67 |
+
railway variables set SURREAL_PASSWORD=root
|
| 68 |
+
railway variables set SURREAL_NAMESPACE=open_notebook
|
| 69 |
+
railway variables set SURREAL_DATABASE=production
|
| 70 |
+
railway variables set INTERNAL_API_URL=http://127.0.0.1:5055
|
| 71 |
+
railway variables set OPENAI_API_KEY=sk-your-key-here
|
| 72 |
+
|
| 73 |
+
# Deploy
|
| 74 |
+
railway up
|
| 75 |
+
|
| 76 |
+
# Get your app URL
|
| 77 |
+
railway domain
|
| 78 |
+
|
| 79 |
+
# Set API_URL with your domain
|
| 80 |
+
railway variables set API_URL=https://your-app.up.railway.app
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## Architecture
|
| 84 |
+
|
| 85 |
+
This deployment uses the **single-container** architecture:
|
| 86 |
+
- ✅ SurrealDB (embedded database)
|
| 87 |
+
- ✅ FastAPI backend (port 5055)
|
| 88 |
+
- ✅ Background worker
|
| 89 |
+
- ✅ Next.js frontend (port 8080)
|
| 90 |
+
|
| 91 |
+
All services run in one container managed by Supervisord.
|
| 92 |
+
|
| 93 |
+
## Troubleshooting
|
| 94 |
+
|
| 95 |
+
### Build Fails
|
| 96 |
+
|
| 97 |
+
**Issue:** Build timeout or memory issues
|
| 98 |
+
|
| 99 |
+
**Solution:**
|
| 100 |
+
- Increase Railway instance size temporarily during build
|
| 101 |
+
- Or use pre-built Docker image:
|
| 102 |
+
```dockerfile
|
| 103 |
+
FROM lfnovo/open_notebook:v1-latest-single
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### Service Won't Start
|
| 107 |
+
|
| 108 |
+
**Issue:** Container restarts continuously
|
| 109 |
+
|
| 110 |
+
**Solution:** Check logs for:
|
| 111 |
+
1. Missing environment variables (especially `SURREAL_URL`)
|
| 112 |
+
2. Database connection errors
|
| 113 |
+
3. Frontend build issues
|
| 114 |
+
|
| 115 |
+
### Can't Access the App
|
| 116 |
+
|
| 117 |
+
**Issue:** Railway shows running but can't access
|
| 118 |
+
|
| 119 |
+
**Solution:**
|
| 120 |
+
1. Verify `API_URL` is set to your Railway domain
|
| 121 |
+
2. Check that port 8080 is exposed in Railway settings
|
| 122 |
+
3. Wait 2-3 minutes after deployment for all services to start
|
| 123 |
+
|
| 124 |
+
### Database Migration Errors
|
| 125 |
+
|
| 126 |
+
**Issue:** Migration fails on startup
|
| 127 |
+
|
| 128 |
+
**Solution:**
|
| 129 |
+
- Ensure all required migrations files exist (1-17)
|
| 130 |
+
- Check database connection settings
|
| 131 |
+
- View logs: `railway logs` or in Railway dashboard
|
| 132 |
+
|
| 133 |
+
## Environment Variables Reference
|
| 134 |
+
|
| 135 |
+
| Variable | Required | Default | Description |
|
| 136 |
+
|----------|----------|---------|-------------|
|
| 137 |
+
| `SURREAL_URL` | Yes | - | Database WebSocket URL |
|
| 138 |
+
| `SURREAL_USER` | Yes | - | Database username |
|
| 139 |
+
| `SURREAL_PASSWORD` | Yes | - | Database password |
|
| 140 |
+
| `SURREAL_NAMESPACE` | Yes | - | Database namespace |
|
| 141 |
+
| `SURREAL_DATABASE` | Yes | - | Database name |
|
| 142 |
+
| `INTERNAL_API_URL` | Yes | - | Internal API endpoint |
|
| 143 |
+
| `API_URL` | Yes | - | Public API URL (your Railway domain) |
|
| 144 |
+
| `OPENAI_API_KEY` | Yes* | - | OpenAI API key (*or another AI provider) |
|
| 145 |
+
| `ANTHROPIC_API_KEY` | No | - | Anthropic API key |
|
| 146 |
+
| `GOOGLE_API_KEY` | No | - | Google AI API key |
|
| 147 |
+
| `OPEN_NOTEBOOK_PASSWORD` | No | - | Optional app password protection |
|
| 148 |
+
|
| 149 |
+
## Persistent Data
|
| 150 |
+
|
| 151 |
+
Railway provides **ephemeral storage**, meaning:
|
| 152 |
+
- ⚠️ Database data will be lost on redeploys
|
| 153 |
+
- ⚠️ Uploaded files will be lost on redeploys
|
| 154 |
+
|
| 155 |
+
For production use, consider:
|
| 156 |
+
1. Using Railway's **Volume** feature for `/mydata` (database)
|
| 157 |
+
2. Using external storage (S3, Cloudinary) for uploads
|
| 158 |
+
3. Or deploying to a platform with persistent storage
|
| 159 |
+
|
| 160 |
+
## Performance Tips
|
| 161 |
+
|
| 162 |
+
1. **Start with Hobby plan** ($5/month) for testing
|
| 163 |
+
2. **Upgrade if needed** based on usage
|
| 164 |
+
3. **Monitor memory usage** - increase if services crash
|
| 165 |
+
4. **Use CDN** for faster frontend loading (Railway Pro)
|
| 166 |
+
|
| 167 |
+
## Cost Estimation
|
| 168 |
+
|
| 169 |
+
- **Hobby Plan**: ~$5-10/month (includes some usage)
|
| 170 |
+
- **Pro Plan**: ~$20-30/month (higher limits)
|
| 171 |
+
- Plus: AI API costs (pay per use)
|
| 172 |
+
|
| 173 |
+
Railway charges for:
|
| 174 |
+
- CPU time
|
| 175 |
+
- Memory usage
|
| 176 |
+
- Bandwidth
|
| 177 |
+
|
| 178 |
+
The single-container deployment is optimized to minimize costs.
|
| 179 |
+
|
| 180 |
+
## Support
|
| 181 |
+
|
| 182 |
+
- 📖 [Full Documentation](../README.md)
|
| 183 |
+
- 💬 [Discord Community](https://discord.gg/37XJPXfz2w)
|
| 184 |
+
- 🐛 [GitHub Issues](https://github.com/lfnovo/open-notebook/issues)
|
| 185 |
+
- 🚂 [Railway Docs](https://docs.railway.app/)
|
| 186 |
+
|
| 187 |
+
## Alternative Deployment Options
|
| 188 |
+
|
| 189 |
+
If Railway doesn't work for you, consider:
|
| 190 |
+
|
| 191 |
+
- **Docker** - Self-hosted on any VPS (DigitalOcean, Linode, etc.)
|
| 192 |
+
- **AWS ECS/Fargate** - Managed containers
|
| 193 |
+
- **Google Cloud Run** - Serverless containers
|
| 194 |
+
- **Azure Container Instances** - Pay-per-use containers
|
| 195 |
+
- **Fly.io** - Similar to Railway
|
| 196 |
+
|
| 197 |
+
See the main [Deployment Guide](../docs/deployment/index.md) for more options.
|
RAILWAY_CHECKLIST.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Railway Deployment Quick Checklist
|
| 2 |
+
|
| 3 |
+
## Pre-Deployment
|
| 4 |
+
- [ ] Fork repository to your GitHub account
|
| 5 |
+
- [ ] Have at least one AI API key ready (OpenAI, Anthropic, etc.)
|
| 6 |
+
- [ ] Have Railway account created
|
| 7 |
+
|
| 8 |
+
## Step 1: Push Code
|
| 9 |
+
```bash
|
| 10 |
+
git add .
|
| 11 |
+
git commit -m "Add Railway deployment fixes"
|
| 12 |
+
git push origin main
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
## Step 2: Create Railway Project
|
| 16 |
+
- [ ] Go to https://railway.app/
|
| 17 |
+
- [ ] Click "New Project"
|
| 18 |
+
- [ ] Select "Deploy from GitHub repo"
|
| 19 |
+
- [ ] Choose your forked repository
|
| 20 |
+
- [ ] Railway will start building automatically
|
| 21 |
+
|
| 22 |
+
## Step 3: Set Environment Variables (CRITICAL!)
|
| 23 |
+
|
| 24 |
+
Go to Railway → Your Service → Variables tab
|
| 25 |
+
|
| 26 |
+
### Required Variables (Set BEFORE first successful deploy):
|
| 27 |
+
```bash
|
| 28 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 29 |
+
SURREAL_USER=root
|
| 30 |
+
SURREAL_PASSWORD=root
|
| 31 |
+
SURREAL_NAMESPACE=open_notebook
|
| 32 |
+
SURREAL_DATABASE=production
|
| 33 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 34 |
+
OPENAI_API_KEY=sk-your-actual-openai-key
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
- [ ] All required variables set
|
| 38 |
+
- [ ] Wait for build to complete
|
| 39 |
+
- [ ] Note your Railway domain (e.g., `https://yourapp-production.up.railway.app`)
|
| 40 |
+
|
| 41 |
+
## Step 4: Set API_URL (AFTER getting domain)
|
| 42 |
+
```bash
|
| 43 |
+
API_URL=https://yourapp-production.up.railway.app
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
- [ ] API_URL variable added with YOUR actual Railway domain
|
| 47 |
+
- [ ] Redeploy triggered (automatic after adding variable)
|
| 48 |
+
|
| 49 |
+
## Step 5: Configure Railway Settings
|
| 50 |
+
- [ ] Go to Settings → Networking
|
| 51 |
+
- [ ] Verify port 8080 is exposed (should auto-detect)
|
| 52 |
+
- [ ] Health check path: `/api/health`
|
| 53 |
+
|
| 54 |
+
## Step 6: Verify Deployment
|
| 55 |
+
|
| 56 |
+
### Check These URLs:
|
| 57 |
+
- [ ] `https://yourapp.up.railway.app/` → Should show Open Notebook UI
|
| 58 |
+
- [ ] `https://yourapp.up.railway.app/api/health` → Should return `{"status":"ok"}`
|
| 59 |
+
- [ ] `https://yourapp.up.railway.app/api/docs` → Should show API documentation
|
| 60 |
+
|
| 61 |
+
### Check Railway Logs:
|
| 62 |
+
- [ ] All 4 services started: surrealdb, api, worker, frontend
|
| 63 |
+
- [ ] No error messages (warnings are OK)
|
| 64 |
+
- [ ] Migrations completed successfully
|
| 65 |
+
- [ ] Frontend shows "Ready in XXms"
|
| 66 |
+
|
| 67 |
+
## Step 7: Test Functionality
|
| 68 |
+
- [ ] Create a new notebook
|
| 69 |
+
- [ ] Upload a test document
|
| 70 |
+
- [ ] Try chat functionality
|
| 71 |
+
- [ ] Generate a podcast (optional)
|
| 72 |
+
|
| 73 |
+
## Common Issues & Quick Fixes
|
| 74 |
+
|
| 75 |
+
### ❌ Build Timeout
|
| 76 |
+
**Solution:** Upgrade to Railway Hobby plan ($5/month) for longer build times
|
| 77 |
+
|
| 78 |
+
### ❌ Services Keep Restarting
|
| 79 |
+
**Solution:** Check environment variables are set correctly, especially `SURREAL_URL`
|
| 80 |
+
|
| 81 |
+
### ❌ Frontend Can't Connect to API
|
| 82 |
+
**Solution:** Ensure `API_URL` is set to your actual Railway domain (with https://)
|
| 83 |
+
|
| 84 |
+
### ❌ Out of Memory
|
| 85 |
+
**Solution:** Upgrade Railway plan (single container needs ~2GB RAM)
|
| 86 |
+
|
| 87 |
+
### ❌ "Database Connection Failed"
|
| 88 |
+
**Solution:**
|
| 89 |
+
1. Check `SURREAL_URL=ws://127.0.0.1:8000/rpc` (note: 127.0.0.1, not localhost)
|
| 90 |
+
2. Verify SurrealDB service is running in logs
|
| 91 |
+
|
| 92 |
+
## Environment Variables Checklist
|
| 93 |
+
|
| 94 |
+
### Required (App Won't Work Without These):
|
| 95 |
+
- [ ] `SURREAL_URL`
|
| 96 |
+
- [ ] `SURREAL_USER`
|
| 97 |
+
- [ ] `SURREAL_PASSWORD`
|
| 98 |
+
- [ ] `SURREAL_NAMESPACE`
|
| 99 |
+
- [ ] `SURREAL_DATABASE`
|
| 100 |
+
- [ ] `INTERNAL_API_URL`
|
| 101 |
+
- [ ] `API_URL` (add after first deploy)
|
| 102 |
+
- [ ] At least one AI API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)
|
| 103 |
+
|
| 104 |
+
### Optional (Add As Needed):
|
| 105 |
+
- [ ] `ANTHROPIC_API_KEY` - For Claude models
|
| 106 |
+
- [ ] `GOOGLE_API_KEY` - For Gemini models
|
| 107 |
+
- [ ] `GROQ_API_KEY` - For Groq models
|
| 108 |
+
- [ ] `MISTRAL_API_KEY` - For Mistral models
|
| 109 |
+
- [ ] `OPEN_NOTEBOOK_PASSWORD` - For password protection
|
| 110 |
+
- [ ] `FIRECRAWL_API_KEY` - For enhanced web scraping
|
| 111 |
+
- [ ] `JINA_API_KEY` - For advanced embeddings
|
| 112 |
+
|
| 113 |
+
## Success Indicators
|
| 114 |
+
|
| 115 |
+
Your deployment is successful when you see in Railway logs:
|
| 116 |
+
```
|
| 117 |
+
✓ Ready in XXXms
|
| 118 |
+
INFO: Application startup complete.
|
| 119 |
+
INFO: Uvicorn running on http://0.0.0.0:5055
|
| 120 |
+
Migrations completed successfully. Database is now at version 17
|
| 121 |
+
All services entered RUNNING state
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
## Cost Estimation
|
| 125 |
+
|
| 126 |
+
**Railway Hobby Plan**: ~$5-10/month
|
| 127 |
+
- Includes $5 usage credit
|
| 128 |
+
- Covers single container deployment
|
| 129 |
+
- Sufficient for testing and small-scale use
|
| 130 |
+
|
| 131 |
+
**Plus AI API Costs**: Pay-per-use
|
| 132 |
+
- OpenAI: ~$0.002-0.06 per 1K tokens
|
| 133 |
+
- Anthropic: Similar pricing
|
| 134 |
+
- Varies by model and usage
|
| 135 |
+
|
| 136 |
+
## Support
|
| 137 |
+
|
| 138 |
+
Need help?
|
| 139 |
+
- 📖 Read [RAILWAY.md](./RAILWAY.md) for detailed guide
|
| 140 |
+
- 💬 Join [Discord](https://discord.gg/37XJPXfz2w)
|
| 141 |
+
- 🐛 Report [GitHub Issues](https://github.com/PremKxmar/se/issues)
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
## After Successful Deployment
|
| 146 |
+
|
| 147 |
+
1. **Bookmark your Railway app URL**
|
| 148 |
+
2. **Set up volume** (in Railway) for `/mydata` to persist database
|
| 149 |
+
3. **Monitor usage** in Railway dashboard
|
| 150 |
+
4. **Configure more AI providers** as needed
|
| 151 |
+
5. **Secure with password** by setting `OPEN_NOTEBOOK_PASSWORD`
|
| 152 |
+
|
| 153 |
+
## Development Workflow
|
| 154 |
+
|
| 155 |
+
To update your deployed app:
|
| 156 |
+
1. Make changes locally
|
| 157 |
+
2. Test with `docker compose up` or `npm run dev`
|
| 158 |
+
3. Commit and push to GitHub
|
| 159 |
+
4. Railway auto-deploys (if enabled)
|
| 160 |
+
5. Verify in Railway logs
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
**Pro Tip:** Copy this checklist and check off items as you complete them!
|
RAILWAY_FIXES.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Railway Deployment Fixes - Summary
|
| 2 |
+
|
| 3 |
+
## Issues Identified
|
| 4 |
+
|
| 5 |
+
### 1. ⚠️ Next.js Standalone Configuration Conflict
|
| 6 |
+
**Problem:** The logs showed:
|
| 7 |
+
```
|
| 8 |
+
⚠ "next start" does not work with "output: standalone" configuration.
|
| 9 |
+
Use "node .next/standalone/server.js" instead.
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
**Root Cause:** `frontend/next.config.ts` had `output: "standalone"` enabled, but the startup command used `npm run start` which calls `next start`.
|
| 13 |
+
|
| 14 |
+
**Fix Applied:**
|
| 15 |
+
- Disabled standalone mode in `next.config.ts` for Railway deployment
|
| 16 |
+
- This allows standard `next start` command to work properly
|
| 17 |
+
|
| 18 |
+
### 2. 📦 Missing Database Migrations (15, 16, 17)
|
| 19 |
+
**Problem:** Migration files 15-17 existed but weren't registered in the migration manager, causing potential schema inconsistencies.
|
| 20 |
+
|
| 21 |
+
**Fix Applied:**
|
| 22 |
+
- Updated `open_notebook/database/async_migrate.py` to include migrations 15, 16, and 17
|
| 23 |
+
- Added both up and down migration files
|
| 24 |
+
|
| 25 |
+
### 3. 🔧 Railway-Specific Configuration Missing
|
| 26 |
+
**Problem:** No Railway-specific configuration files, making deployment harder and less optimized.
|
| 27 |
+
|
| 28 |
+
**Fix Applied:**
|
| 29 |
+
- Created `railway.json` for Railway build configuration
|
| 30 |
+
- Created `Dockerfile.railway` optimized for Railway
|
| 31 |
+
- Created `supervisord.railway.conf` with proper PORT env variable handling
|
| 32 |
+
- Created `.env.railway` template with all required variables
|
| 33 |
+
- Created `RAILWAY.md` comprehensive deployment guide
|
| 34 |
+
|
| 35 |
+
### 4. 🌐 Port Configuration for Railway
|
| 36 |
+
**Problem:** Railway assigns dynamic PORT, but the config wasn't flexible enough.
|
| 37 |
+
|
| 38 |
+
**Fix Applied:**
|
| 39 |
+
- Updated supervisord to use `%(ENV_PORT)s` to read Railway's PORT variable
|
| 40 |
+
- Ensured frontend binds to the correct port (8080 by default, or Railway's PORT)
|
| 41 |
+
|
| 42 |
+
## Files Created
|
| 43 |
+
|
| 44 |
+
1. **railway.json** - Railway deployment configuration
|
| 45 |
+
2. **Dockerfile.railway** - Railway-optimized Docker build
|
| 46 |
+
3. **supervisord.railway.conf** - Railway-specific supervisor config
|
| 47 |
+
4. **.env.railway** - Environment variable template for Railway
|
| 48 |
+
5. **RAILWAY.md** - Complete deployment guide for Railway users
|
| 49 |
+
|
| 50 |
+
## Files Modified
|
| 51 |
+
|
| 52 |
+
1. **frontend/next.config.ts** - Disabled standalone output for Railway
|
| 53 |
+
2. **open_notebook/database/async_migrate.py** - Added migrations 15, 16, 17
|
| 54 |
+
3. **supervisord.single.conf** - Fixed frontend startup command
|
| 55 |
+
|
| 56 |
+
## Deployment Success Indicators
|
| 57 |
+
|
| 58 |
+
From your logs, the deployment was actually **mostly successful**:
|
| 59 |
+
- ✅ SurrealDB started correctly
|
| 60 |
+
- ✅ API server started on port 5055
|
| 61 |
+
- ✅ Worker started successfully
|
| 62 |
+
- ✅ Frontend built and started on port 8080
|
| 63 |
+
- ✅ All migrations (1-14) ran successfully
|
| 64 |
+
- ✅ All services entered RUNNING state
|
| 65 |
+
|
| 66 |
+
The warning about standalone mode was **not blocking deployment**, but could cause issues in production.
|
| 67 |
+
|
| 68 |
+
## What Was Actually Wrong?
|
| 69 |
+
|
| 70 |
+
Looking at your logs more carefully, there's **NO ERROR** - the deployment was successful!
|
| 71 |
+
|
| 72 |
+
The confusion might be:
|
| 73 |
+
1. The supervisor warning about running as root (not critical)
|
| 74 |
+
2. The Next.js standalone warning (now fixed)
|
| 75 |
+
3. Missing pytesseract module (optional OCR feature)
|
| 76 |
+
|
| 77 |
+
These are **warnings**, not errors. The app should be working.
|
| 78 |
+
|
| 79 |
+
## How to Deploy to Railway Now
|
| 80 |
+
|
| 81 |
+
### Step 1: Push Changes to GitHub
|
| 82 |
+
```bash
|
| 83 |
+
cd c:\sem6-real\studyrocket\notebookllm\open-notebook
|
| 84 |
+
git add .
|
| 85 |
+
git commit -m "Add Railway deployment configuration and fixes"
|
| 86 |
+
git push origin main
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Step 2: Configure Railway Environment Variables
|
| 90 |
+
|
| 91 |
+
In Railway dashboard, add these variables:
|
| 92 |
+
|
| 93 |
+
**Required:**
|
| 94 |
+
```env
|
| 95 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 96 |
+
SURREAL_USER=root
|
| 97 |
+
SURREAL_PASSWORD=root
|
| 98 |
+
SURREAL_NAMESPACE=open_notebook
|
| 99 |
+
SURREAL_DATABASE=production
|
| 100 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 101 |
+
OPENAI_API_KEY=your_actual_key_here
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Step 3: Set API_URL After First Deploy
|
| 105 |
+
|
| 106 |
+
After Railway generates your domain (e.g., `https://your-app-production-xxxx.up.railway.app`):
|
| 107 |
+
|
| 108 |
+
```env
|
| 109 |
+
API_URL=https://your-app-production-xxxx.up.railway.app
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
Then redeploy.
|
| 113 |
+
|
| 114 |
+
### Step 4: Verify Deployment
|
| 115 |
+
|
| 116 |
+
Check these endpoints:
|
| 117 |
+
- `https://your-app.up.railway.app/` - Frontend UI
|
| 118 |
+
- `https://your-app.up.railway.app/api/health` - API health check
|
| 119 |
+
- `https://your-app.up.railway.app/api/docs` - API documentation
|
| 120 |
+
|
| 121 |
+
## Troubleshooting
|
| 122 |
+
|
| 123 |
+
### If Build Times Out
|
| 124 |
+
Railway free tier has build time limits. Solutions:
|
| 125 |
+
1. Upgrade to Hobby plan ($5/month)
|
| 126 |
+
2. Use pre-built image: `FROM lfnovo/open_notebook:v1-latest-single`
|
| 127 |
+
|
| 128 |
+
### If App Crashes After Deploy
|
| 129 |
+
1. Check Railway logs for actual errors
|
| 130 |
+
2. Verify all environment variables are set
|
| 131 |
+
3. Wait 2-3 minutes - services need time to start
|
| 132 |
+
|
| 133 |
+
### If Frontend Can't Connect to API
|
| 134 |
+
1. Ensure `API_URL` is set to your Railway domain
|
| 135 |
+
2. Check that port 8080 is exposed (Railway auto-detects)
|
| 136 |
+
3. Verify `INTERNAL_API_URL=http://127.0.0.1:5055`
|
| 137 |
+
|
| 138 |
+
## Testing Locally
|
| 139 |
+
|
| 140 |
+
Before pushing to Railway, test with Docker:
|
| 141 |
+
|
| 142 |
+
```powershell
|
| 143 |
+
# Build Railway Dockerfile
|
| 144 |
+
docker build -f Dockerfile.railway -t open-notebook-railway .
|
| 145 |
+
|
| 146 |
+
# Run with Railway-like environment
|
| 147 |
+
docker run -p 8080:8080 -p 5055:5055 `
|
| 148 |
+
-e PORT=8080 `
|
| 149 |
+
-e SURREAL_URL=ws://127.0.0.1:8000/rpc `
|
| 150 |
+
-e SURREAL_USER=root `
|
| 151 |
+
-e SURREAL_PASSWORD=root `
|
| 152 |
+
-e SURREAL_NAMESPACE=open_notebook `
|
| 153 |
+
-e SURREAL_DATABASE=production `
|
| 154 |
+
-e INTERNAL_API_URL=http://127.0.0.1:5055 `
|
| 155 |
+
-e API_URL=http://localhost:8080 `
|
| 156 |
+
-e OPENAI_API_KEY=your_key `
|
| 157 |
+
open-notebook-railway
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
Access at: http://localhost:8080
|
| 161 |
+
|
| 162 |
+
## Next Steps
|
| 163 |
+
|
| 164 |
+
1. ✅ **Commit and push** all changes to GitHub
|
| 165 |
+
2. ✅ **Configure environment variables** in Railway
|
| 166 |
+
3. ✅ **Deploy** from GitHub in Railway
|
| 167 |
+
4. ✅ **Set API_URL** after getting your domain
|
| 168 |
+
5. ✅ **Redeploy** to apply API_URL
|
| 169 |
+
6. ✅ **Test** all functionality
|
| 170 |
+
|
| 171 |
+
## Additional Notes
|
| 172 |
+
|
| 173 |
+
- **Database persistence**: Railway containers are ephemeral. For production, consider:
|
| 174 |
+
- Using Railway Volumes for `/mydata` (database storage)
|
| 175 |
+
- Exporting/importing data periodically
|
| 176 |
+
- Using external database (more expensive)
|
| 177 |
+
|
| 178 |
+
- **Costs**: Railway charges for:
|
| 179 |
+
- CPU usage
|
| 180 |
+
- Memory usage
|
| 181 |
+
- Bandwidth
|
| 182 |
+
- Start with Hobby plan ($5/mo) for testing
|
| 183 |
+
|
| 184 |
+
- **Performance**: Single container runs 4 services, so:
|
| 185 |
+
- May need 2GB+ RAM for smooth operation
|
| 186 |
+
- Consider upgrading Railway plan if services crash
|
| 187 |
+
|
| 188 |
+
## Support Resources
|
| 189 |
+
|
| 190 |
+
- 📖 [RAILWAY.md](./RAILWAY.md) - Full Railway deployment guide
|
| 191 |
+
- 💬 [Discord Community](https://discord.gg/37XJPXfz2w)
|
| 192 |
+
- 🐛 [GitHub Issues](https://github.com/lfnovo/open-notebook/issues)
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
**Important:** Your deployment logs show the app **IS WORKING**. The issues were warnings, not blocking errors. These fixes will make the deployment more robust and eliminate the warnings.
|
README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Open Notebook
|
| 3 |
+
emoji: 📓
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
+
---
|
README_HUGGINGFACE.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Open Notebook
|
| 3 |
+
emoji: 📓
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Open Notebook - Hugging Face Spaces Deployment
|
| 12 |
+
|
| 13 |
+
An open source, privacy-focused alternative to Google's Notebook LM!
|
| 14 |
+
|
| 15 |
+
This Space runs Open Notebook with a FastAPI backend and embedded SurrealDB database.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- 🔒 **Control your data** - Keep your research private and secure
|
| 20 |
+
- 🤖 **Choose your AI models** - Support for 16+ providers including OpenAI, Anthropic, Ollama, and more
|
| 21 |
+
- 📚 **Organize multi-modal content** - PDFs, videos, audio, web pages
|
| 22 |
+
- 🎙️ **Generate professional podcasts** - Advanced multi-speaker podcast generation
|
| 23 |
+
- 🔍 **Search intelligently** - Full-text and vector search across all your content
|
| 24 |
+
- 💬 **Chat with context** - AI conversations powered by your research
|
| 25 |
+
|
| 26 |
+
## Configuration
|
| 27 |
+
|
| 28 |
+
To use this Space, you need to set the following secrets in the Space settings:
|
| 29 |
+
|
| 30 |
+
### Required API Keys (set at least one):
|
| 31 |
+
- `OPENAI_API_KEY` - For OpenAI models (GPT-4, GPT-3.5)
|
| 32 |
+
- `ANTHROPIC_API_KEY` - For Claude models
|
| 33 |
+
- `GOOGLE_API_KEY` - For Gemini models
|
| 34 |
+
|
| 35 |
+
### Optional Configuration:
|
| 36 |
+
- `GROQ_API_KEY` - For Groq models
|
| 37 |
+
- `MISTRAL_API_KEY` - For Mistral models
|
| 38 |
+
|
| 39 |
+
## Usage
|
| 40 |
+
|
| 41 |
+
Once deployed, access the API at:
|
| 42 |
+
- API Documentation: `https://your-space-name.hf.space/docs`
|
| 43 |
+
- Health Check: `https://your-space-name.hf.space/health`
|
| 44 |
+
|
| 45 |
+
## Note
|
| 46 |
+
|
| 47 |
+
This deployment uses in-memory SurrealDB storage. Data will be lost when the Space restarts.
|
| 48 |
+
For persistent storage, consider using an external SurrealDB instance.
|
| 49 |
+
|
| 50 |
+
## Learn More
|
| 51 |
+
|
| 52 |
+
- [GitHub Repository](https://github.com/baveshraam/software-eng-proj)
|
| 53 |
+
- [Original Project](https://github.com/lfnovo/open-notebook)
|
YOUR_RAILWAY_CONFIG.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚂 Your Railway Configuration - Ready to Deploy
|
| 2 |
+
|
| 3 |
+
## Your Current Setup Analysis
|
| 4 |
+
|
| 5 |
+
Based on your existing Railway variables, your configuration uses:
|
| 6 |
+
- ✅ **Database**: `test` namespace and database (not production)
|
| 7 |
+
- ✅ **Multiple AI Providers**: Gemini (Google), Groq, Llama
|
| 8 |
+
- ✅ **Worker Configuration**: 5 concurrent tasks with retry logic
|
| 9 |
+
- ✅ **Proper retry settings**: Exponential jitter for reliability
|
| 10 |
+
|
| 11 |
+
## STEP 1: Current Railway Variables (What You Already Have)
|
| 12 |
+
|
| 13 |
+
These are already set in your Railway deployment:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
# Database - Already Configured ✅
|
| 17 |
+
SURREAL_URL=ws://localhost:8000/rpc # ⚠️ NEEDS FIX (see below)
|
| 18 |
+
SURREAL_USER=root
|
| 19 |
+
SURREAL_PASSWORD=root
|
| 20 |
+
SURREAL_NAMESPACE=test
|
| 21 |
+
SURREAL_DATABASE=test
|
| 22 |
+
|
| 23 |
+
# API URL - Already Configured ✅
|
| 24 |
+
API_URL=http://localhost:5055 # ⚠️ NEEDS UPDATE (see below)
|
| 25 |
+
|
| 26 |
+
# Worker & Retry - Already Configured ✅
|
| 27 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 28 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 29 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 30 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 31 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 32 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## STEP 2: Critical Fixes Needed
|
| 36 |
+
|
| 37 |
+
### Fix 1: Change `localhost` to `127.0.0.1` for SURREAL_URL
|
| 38 |
+
|
| 39 |
+
**In Railway Dashboard → Variables:**
|
| 40 |
+
|
| 41 |
+
❌ **Current (WRONG):**
|
| 42 |
+
```bash
|
| 43 |
+
SURREAL_URL=ws://localhost:8000/rpc
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
✅ **Change to (CORRECT):**
|
| 47 |
+
```bash
|
| 48 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
**Why?** Railway's container networking requires `127.0.0.1` instead of `localhost`.
|
| 52 |
+
|
| 53 |
+
### Fix 2: Add INTERNAL_API_URL
|
| 54 |
+
|
| 55 |
+
**Add this new variable in Railway:**
|
| 56 |
+
```bash
|
| 57 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
**Why?** Next.js needs this for server-side API proxying.
|
| 61 |
+
|
| 62 |
+
### Fix 3: Update API_URL to Your Railway Domain
|
| 63 |
+
|
| 64 |
+
**After your first successful deploy:**
|
| 65 |
+
|
| 66 |
+
❌ **Current:**
|
| 67 |
+
```bash
|
| 68 |
+
API_URL=http://localhost:5055
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
✅ **Change to YOUR Railway domain:**
|
| 72 |
+
```bash
|
| 73 |
+
API_URL=https://your-app-production-xxxx.up.railway.app
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**How to find your Railway domain:**
|
| 77 |
+
1. Go to Railway Dashboard → Your Service
|
| 78 |
+
2. Look at the "Deployments" tab
|
| 79 |
+
3. Copy the domain (e.g., `https://se-production-1234.up.railway.app`)
|
| 80 |
+
4. Paste it as the API_URL value (without `/api` at the end)
|
| 81 |
+
|
| 82 |
+
## STEP 3: Add Your AI API Keys
|
| 83 |
+
|
| 84 |
+
You mentioned using **Gemini, Groq, and Llama**. Add these variables:
|
| 85 |
+
|
| 86 |
+
### For Google Gemini (Required)
|
| 87 |
+
```bash
|
| 88 |
+
GOOGLE_API_KEY=your_actual_gemini_api_key
|
| 89 |
+
```
|
| 90 |
+
Get it at: https://makersuite.google.com/app/apikey
|
| 91 |
+
|
| 92 |
+
### For Groq (Required)
|
| 93 |
+
```bash
|
| 94 |
+
GROQ_API_KEY=your_actual_groq_api_key
|
| 95 |
+
```
|
| 96 |
+
Get it at: https://console.groq.com/keys
|
| 97 |
+
|
| 98 |
+
### For Llama via Ollama (If applicable)
|
| 99 |
+
If you're running Ollama somewhere accessible:
|
| 100 |
+
```bash
|
| 101 |
+
OLLAMA_API_BASE=http://your-ollama-host:11434
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
**OR** if using Llama via Groq (most common):
|
| 105 |
+
- No extra configuration needed - Groq provides Llama models
|
| 106 |
+
|
| 107 |
+
### Optional: Other Providers
|
| 108 |
+
If you want to add more providers later:
|
| 109 |
+
```bash
|
| 110 |
+
# OpenAI (optional)
|
| 111 |
+
OPENAI_API_KEY=sk-your_key
|
| 112 |
+
|
| 113 |
+
# Anthropic Claude (optional)
|
| 114 |
+
ANTHROPIC_API_KEY=sk-ant-your_key
|
| 115 |
+
|
| 116 |
+
# Mistral (optional)
|
| 117 |
+
MISTRAL_API_KEY=your_key
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## STEP 4: Complete Railway Variables List
|
| 121 |
+
|
| 122 |
+
Copy this EXACT configuration to Railway:
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
# ============================================
|
| 126 |
+
# DATABASE (Keep as-is)
|
| 127 |
+
# ============================================
|
| 128 |
+
SURREAL_URL=ws://127.0.0.1:8000/rpc
|
| 129 |
+
SURREAL_USER=root
|
| 130 |
+
SURREAL_PASSWORD=root
|
| 131 |
+
SURREAL_NAMESPACE=test
|
| 132 |
+
SURREAL_DATABASE=test
|
| 133 |
+
|
| 134 |
+
# ============================================
|
| 135 |
+
# API CONFIGURATION
|
| 136 |
+
# ============================================
|
| 137 |
+
INTERNAL_API_URL=http://127.0.0.1:5055
|
| 138 |
+
API_URL=https://YOUR-RAILWAY-DOMAIN.up.railway.app
|
| 139 |
+
|
| 140 |
+
# ============================================
|
| 141 |
+
# WORKER & RETRY (Keep as-is)
|
| 142 |
+
# ============================================
|
| 143 |
+
SURREAL_COMMANDS_MAX_TASKS=5
|
| 144 |
+
SURREAL_COMMANDS_RETRY_ENABLED=true
|
| 145 |
+
SURREAL_COMMANDS_RETRY_MAX_ATTEMPTS=3
|
| 146 |
+
SURREAL_COMMANDS_RETRY_WAIT_STRATEGY=exponential_jitter
|
| 147 |
+
SURREAL_COMMANDS_RETRY_WAIT_MIN=1
|
| 148 |
+
SURREAL_COMMANDS_RETRY_WAIT_MAX=30
|
| 149 |
+
|
| 150 |
+
# ============================================
|
| 151 |
+
# AI PROVIDERS (ADD YOUR KEYS)
|
| 152 |
+
# ============================================
|
| 153 |
+
GOOGLE_API_KEY=your_actual_gemini_key
|
| 154 |
+
GROQ_API_KEY=your_actual_groq_key
|
| 155 |
+
|
| 156 |
+
# Optional: If using Ollama for Llama
|
| 157 |
+
# OLLAMA_API_BASE=http://your-ollama-host:11434
|
| 158 |
+
|
| 159 |
+
# Optional: Other providers
|
| 160 |
+
# OPENAI_API_KEY=sk-your_key
|
| 161 |
+
# ANTHROPIC_API_KEY=sk-ant-your_key
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
## STEP 5: Deploy Order
|
| 165 |
+
|
| 166 |
+
### A. Before Redeploying - Set These First:
|
| 167 |
+
1. ✅ Change `SURREAL_URL` to use `127.0.0.1`
|
| 168 |
+
2. ✅ Add `INTERNAL_API_URL=http://127.0.0.1:5055`
|
| 169 |
+
3. ✅ Add `GOOGLE_API_KEY` (your Gemini key)
|
| 170 |
+
4. ✅ Add `GROQ_API_KEY` (your Groq key)
|
| 171 |
+
5. ⏸️ Keep `API_URL` as is for now (update after deploy)
|
| 172 |
+
|
| 173 |
+
### B. Push Code Changes:
|
| 174 |
+
```powershell
|
| 175 |
+
cd c:\sem6-real\studyrocket\notebookllm\open-notebook
|
| 176 |
+
git add .
|
| 177 |
+
git commit -m "Fix Railway deployment configuration"
|
| 178 |
+
git push origin main
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
### C. After Successful Deploy:
|
| 182 |
+
1. ✅ Copy your Railway domain
|
| 183 |
+
2. ✅ Update `API_URL` to your Railway domain
|
| 184 |
+
3. ✅ Railway will auto-redeploy
|
| 185 |
+
|
| 186 |
+
## STEP 6: Verification Checklist
|
| 187 |
+
|
| 188 |
+
After deployment completes, verify:
|
| 189 |
+
|
| 190 |
+
- [ ] Service shows "RUNNING" in Railway
|
| 191 |
+
- [ ] Check logs: "Application startup complete"
|
| 192 |
+
- [ ] Check logs: "Migrations completed successfully. Database is now at version 17"
|
| 193 |
+
- [ ] Visit `https://your-domain.up.railway.app/` → Should load UI
|
| 194 |
+
- [ ] Visit `https://your-domain.up.railway.app/api/health` → Should return `{"status":"ok"}`
|
| 195 |
+
- [ ] Try creating a notebook in the UI
|
| 196 |
+
- [ ] Test AI features (chat, generation)
|
| 197 |
+
|
| 198 |
+
## Common Issues Specific to Your Setup
|
| 199 |
+
|
| 200 |
+
### Issue: "Database Connection Failed"
|
| 201 |
+
**Cause:** Using `localhost` instead of `127.0.0.1`
|
| 202 |
+
**Solution:** Change `SURREAL_URL=ws://127.0.0.1:8000/rpc`
|
| 203 |
+
|
| 204 |
+
### Issue: "Unable to Connect to API Server"
|
| 205 |
+
**Cause:** `INTERNAL_API_URL` not set or `API_URL` pointing to localhost
|
| 206 |
+
**Solution:**
|
| 207 |
+
- Set `INTERNAL_API_URL=http://127.0.0.1:5055`
|
| 208 |
+
- Set `API_URL=https://your-railway-domain.up.railway.app`
|
| 209 |
+
|
| 210 |
+
### Issue: "AI Model Not Available"
|
| 211 |
+
**Cause:** API keys not set or incorrect
|
| 212 |
+
**Solution:**
|
| 213 |
+
- Verify `GOOGLE_API_KEY` is set correctly
|
| 214 |
+
- Verify `GROQ_API_KEY` is set correctly
|
| 215 |
+
- Check API key validity at provider dashboards
|
| 216 |
+
|
| 217 |
+
### Issue: "Migrations Stuck at Version 14"
|
| 218 |
+
**Cause:** Code changes not deployed
|
| 219 |
+
**Solution:**
|
| 220 |
+
- Ensure you pushed the latest code with migrations 15-17
|
| 221 |
+
- Check Railway logs for migration errors
|
| 222 |
+
- Verify all migration files exist in the repo
|
| 223 |
+
|
| 224 |
+
## Model Configuration by Provider
|
| 225 |
+
|
| 226 |
+
Based on your setup, here's which models you can use:
|
| 227 |
+
|
| 228 |
+
### Via Gemini (GOOGLE_API_KEY)
|
| 229 |
+
- ✅ `gemini-pro` - General purpose
|
| 230 |
+
- ✅ `gemini-pro-vision` - Image understanding
|
| 231 |
+
- ✅ `gemini-1.5-pro` - Long context (1M tokens)
|
| 232 |
+
- ✅ `gemini-1.5-flash` - Fast & efficient
|
| 233 |
+
- ✅ Text embeddings via Gemini
|
| 234 |
+
|
| 235 |
+
### Via Groq (GROQ_API_KEY)
|
| 236 |
+
- ✅ `llama-3.1-70b-versatile` - Best Llama model
|
| 237 |
+
- ✅ `llama-3.1-8b-instant` - Fast Llama
|
| 238 |
+
- ✅ `llama3-70b-8192` - Older Llama version
|
| 239 |
+
- ✅ `mixtral-8x7b-32768` - Mixtral model
|
| 240 |
+
- ✅ `gemma2-9b-it` - Google's Gemma
|
| 241 |
+
|
| 242 |
+
### Via Ollama (if configured)
|
| 243 |
+
- ✅ Any locally installed model
|
| 244 |
+
- ✅ `llama3:latest`, `llama3.1:latest`
|
| 245 |
+
- ✅ `mistral:latest`, `mixtral:latest`
|
| 246 |
+
- ✅ Custom models
|
| 247 |
+
|
| 248 |
+
## Cost Estimation for Your Setup
|
| 249 |
+
|
| 250 |
+
### Gemini (Google)
|
| 251 |
+
- **Free Tier**: 60 requests/minute
|
| 252 |
+
- **Paid**: $0.50 per 1M input tokens (very affordable)
|
| 253 |
+
- **Best for**: Long context, embeddings, general use
|
| 254 |
+
|
| 255 |
+
### Groq
|
| 256 |
+
- **Free Tier**: Generous free tier
|
| 257 |
+
- **Paid**: Very competitive pricing
|
| 258 |
+
- **Best for**: Fast inference, Llama models
|
| 259 |
+
|
| 260 |
+
### Total Monthly Cost (Estimated)
|
| 261 |
+
- **Light use** (testing): $0-5/month
|
| 262 |
+
- **Medium use** (regular): $10-30/month
|
| 263 |
+
- **Heavy use** (production): $50-100/month
|
| 264 |
+
|
| 265 |
+
Plus Railway hosting: ~$5-10/month
|
| 266 |
+
|
| 267 |
+
## Next Steps
|
| 268 |
+
|
| 269 |
+
1. **Update variables** in Railway as shown above
|
| 270 |
+
2. **Push code** to GitHub
|
| 271 |
+
3. **Wait for deploy** (5-10 minutes)
|
| 272 |
+
4. **Update API_URL** with your Railway domain
|
| 273 |
+
5. **Test all features** with your AI providers
|
| 274 |
+
|
| 275 |
+
## Support
|
| 276 |
+
|
| 277 |
+
If you encounter issues:
|
| 278 |
+
1. Check Railway logs: Dashboard → Deployments → View Logs
|
| 279 |
+
2. Look for specific error messages
|
| 280 |
+
3. Verify all environment variables are set
|
| 281 |
+
4. Test API keys at provider dashboards
|
| 282 |
+
5. Join Discord for help: https://discord.gg/37XJPXfz2w
|
| 283 |
+
|
| 284 |
+
---
|
| 285 |
+
|
| 286 |
+
**Your setup is nearly perfect!** Just make the three fixes above (127.0.0.1, INTERNAL_API_URL, and API_URL) and add your AI keys, then you're good to go! 🚀
|
api/__init__.py
ADDED
|
File without changes
|
api/auth.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
from fastapi import HTTPException, Request
|
| 5 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
| 6 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 7 |
+
from starlette.responses import JSONResponse
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class PasswordAuthMiddleware(BaseHTTPMiddleware):
|
| 11 |
+
"""
|
| 12 |
+
Middleware to check password authentication for all API requests.
|
| 13 |
+
Only active when OPEN_NOTEBOOK_PASSWORD environment variable is set.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
def __init__(self, app, excluded_paths: Optional[list] = None):
|
| 17 |
+
super().__init__(app)
|
| 18 |
+
self.password = os.environ.get("OPEN_NOTEBOOK_PASSWORD")
|
| 19 |
+
self.excluded_paths = excluded_paths or ["/", "/health", "/docs", "/openapi.json", "/redoc"]
|
| 20 |
+
|
| 21 |
+
async def dispatch(self, request: Request, call_next):
|
| 22 |
+
# Skip authentication if no password is set
|
| 23 |
+
if not self.password:
|
| 24 |
+
return await call_next(request)
|
| 25 |
+
|
| 26 |
+
# Skip authentication for excluded paths
|
| 27 |
+
if request.url.path in self.excluded_paths:
|
| 28 |
+
return await call_next(request)
|
| 29 |
+
|
| 30 |
+
# Skip authentication for CORS preflight requests (OPTIONS)
|
| 31 |
+
if request.method == "OPTIONS":
|
| 32 |
+
return await call_next(request)
|
| 33 |
+
|
| 34 |
+
# Check authorization header
|
| 35 |
+
auth_header = request.headers.get("Authorization")
|
| 36 |
+
|
| 37 |
+
if not auth_header:
|
| 38 |
+
return JSONResponse(
|
| 39 |
+
status_code=401,
|
| 40 |
+
content={"detail": "Missing authorization header"},
|
| 41 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Expected format: "Bearer {password}"
|
| 45 |
+
try:
|
| 46 |
+
scheme, credentials = auth_header.split(" ", 1)
|
| 47 |
+
if scheme.lower() != "bearer":
|
| 48 |
+
raise ValueError("Invalid authentication scheme")
|
| 49 |
+
except ValueError:
|
| 50 |
+
return JSONResponse(
|
| 51 |
+
status_code=401,
|
| 52 |
+
content={"detail": "Invalid authorization header format"},
|
| 53 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# Check password
|
| 57 |
+
if credentials != self.password:
|
| 58 |
+
return JSONResponse(
|
| 59 |
+
status_code=401,
|
| 60 |
+
content={"detail": "Invalid password"},
|
| 61 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Password is correct, proceed with the request
|
| 65 |
+
response = await call_next(request)
|
| 66 |
+
return response
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# Optional: HTTPBearer security scheme for OpenAPI documentation
|
| 70 |
+
security = HTTPBearer(auto_error=False)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def check_api_password(credentials: Optional[HTTPAuthorizationCredentials] = None) -> bool:
|
| 74 |
+
"""
|
| 75 |
+
Utility function to check API password.
|
| 76 |
+
Can be used as a dependency in individual routes if needed.
|
| 77 |
+
"""
|
| 78 |
+
password = os.environ.get("OPEN_NOTEBOOK_PASSWORD")
|
| 79 |
+
|
| 80 |
+
# No password set, allow access
|
| 81 |
+
if not password:
|
| 82 |
+
return True
|
| 83 |
+
|
| 84 |
+
# No credentials provided
|
| 85 |
+
if not credentials:
|
| 86 |
+
raise HTTPException(
|
| 87 |
+
status_code=401,
|
| 88 |
+
detail="Missing authorization",
|
| 89 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Check password
|
| 93 |
+
if credentials.credentials != password:
|
| 94 |
+
raise HTTPException(
|
| 95 |
+
status_code=401,
|
| 96 |
+
detail="Invalid password",
|
| 97 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return True
|
api/chat_service.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chat service for API operations.
|
| 3 |
+
Provides async interface for chat functionality.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
import httpx
|
| 9 |
+
from loguru import logger
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ChatService:
|
| 13 |
+
"""Service for chat-related API operations"""
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
self.base_url = os.getenv("API_BASE_URL", "http://127.0.0.1:5055")
|
| 17 |
+
# Add authentication header if password is set
|
| 18 |
+
self.headers = {}
|
| 19 |
+
password = os.getenv("OPEN_NOTEBOOK_PASSWORD")
|
| 20 |
+
if password:
|
| 21 |
+
self.headers["Authorization"] = f"Bearer {password}"
|
| 22 |
+
|
| 23 |
+
async def get_sessions(self, notebook_id: str) -> List[Dict[str, Any]]:
|
| 24 |
+
"""Get all chat sessions for a notebook"""
|
| 25 |
+
try:
|
| 26 |
+
async with httpx.AsyncClient() as client:
|
| 27 |
+
response = await client.get(
|
| 28 |
+
f"{self.base_url}/api/chat/sessions",
|
| 29 |
+
params={"notebook_id": notebook_id},
|
| 30 |
+
headers=self.headers
|
| 31 |
+
)
|
| 32 |
+
response.raise_for_status()
|
| 33 |
+
return response.json()
|
| 34 |
+
except Exception as e:
|
| 35 |
+
logger.error(f"Error fetching chat sessions: {str(e)}")
|
| 36 |
+
raise
|
| 37 |
+
|
| 38 |
+
async def create_session(
|
| 39 |
+
self,
|
| 40 |
+
notebook_id: str,
|
| 41 |
+
title: Optional[str] = None,
|
| 42 |
+
model_override: Optional[str] = None,
|
| 43 |
+
) -> Dict[str, Any]:
|
| 44 |
+
"""Create a new chat session"""
|
| 45 |
+
try:
|
| 46 |
+
data: Dict[str, Any] = {"notebook_id": notebook_id}
|
| 47 |
+
if title is not None:
|
| 48 |
+
data["title"] = title
|
| 49 |
+
if model_override is not None:
|
| 50 |
+
data["model_override"] = model_override
|
| 51 |
+
|
| 52 |
+
async with httpx.AsyncClient() as client:
|
| 53 |
+
response = await client.post(
|
| 54 |
+
f"{self.base_url}/api/chat/sessions",
|
| 55 |
+
json=data,
|
| 56 |
+
headers=self.headers
|
| 57 |
+
)
|
| 58 |
+
response.raise_for_status()
|
| 59 |
+
return response.json()
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"Error creating chat session: {str(e)}")
|
| 62 |
+
raise
|
| 63 |
+
|
| 64 |
+
async def get_session(self, session_id: str) -> Dict[str, Any]:
|
| 65 |
+
"""Get a specific session with messages"""
|
| 66 |
+
try:
|
| 67 |
+
async with httpx.AsyncClient() as client:
|
| 68 |
+
response = await client.get(
|
| 69 |
+
f"{self.base_url}/api/chat/sessions/{session_id}",
|
| 70 |
+
headers=self.headers
|
| 71 |
+
)
|
| 72 |
+
response.raise_for_status()
|
| 73 |
+
return response.json()
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logger.error(f"Error fetching session: {str(e)}")
|
| 76 |
+
raise
|
| 77 |
+
|
| 78 |
+
async def update_session(
|
| 79 |
+
self,
|
| 80 |
+
session_id: str,
|
| 81 |
+
title: Optional[str] = None,
|
| 82 |
+
model_override: Optional[str] = None,
|
| 83 |
+
) -> Dict[str, Any]:
|
| 84 |
+
"""Update session properties"""
|
| 85 |
+
try:
|
| 86 |
+
data: Dict[str, Any] = {}
|
| 87 |
+
if title is not None:
|
| 88 |
+
data["title"] = title
|
| 89 |
+
if model_override is not None:
|
| 90 |
+
data["model_override"] = model_override
|
| 91 |
+
|
| 92 |
+
if not data:
|
| 93 |
+
raise ValueError("At least one field must be provided to update a session")
|
| 94 |
+
|
| 95 |
+
async with httpx.AsyncClient() as client:
|
| 96 |
+
response = await client.put(
|
| 97 |
+
f"{self.base_url}/api/chat/sessions/{session_id}",
|
| 98 |
+
json=data,
|
| 99 |
+
headers=self.headers
|
| 100 |
+
)
|
| 101 |
+
response.raise_for_status()
|
| 102 |
+
return response.json()
|
| 103 |
+
except Exception as e:
|
| 104 |
+
logger.error(f"Error updating session: {str(e)}")
|
| 105 |
+
raise
|
| 106 |
+
|
| 107 |
+
async def delete_session(self, session_id: str) -> Dict[str, Any]:
|
| 108 |
+
"""Delete a chat session"""
|
| 109 |
+
try:
|
| 110 |
+
async with httpx.AsyncClient() as client:
|
| 111 |
+
response = await client.delete(
|
| 112 |
+
f"{self.base_url}/api/chat/sessions/{session_id}",
|
| 113 |
+
headers=self.headers
|
| 114 |
+
)
|
| 115 |
+
response.raise_for_status()
|
| 116 |
+
return response.json()
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"Error deleting session: {str(e)}")
|
| 119 |
+
raise
|
| 120 |
+
|
| 121 |
+
async def execute_chat(
|
| 122 |
+
self,
|
| 123 |
+
session_id: str,
|
| 124 |
+
message: str,
|
| 125 |
+
context: Dict[str, Any],
|
| 126 |
+
model_override: Optional[str] = None,
|
| 127 |
+
) -> Dict[str, Any]:
|
| 128 |
+
"""Execute a chat request"""
|
| 129 |
+
try:
|
| 130 |
+
data = {
|
| 131 |
+
"session_id": session_id,
|
| 132 |
+
"message": message,
|
| 133 |
+
"context": context
|
| 134 |
+
}
|
| 135 |
+
if model_override is not None:
|
| 136 |
+
data["model_override"] = model_override
|
| 137 |
+
|
| 138 |
+
# Short connect timeout (10s), long read timeout (10 min) for Ollama/local LLMs
|
| 139 |
+
timeout = httpx.Timeout(connect=10.0, read=600.0, write=30.0, pool=10.0)
|
| 140 |
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
| 141 |
+
response = await client.post(
|
| 142 |
+
f"{self.base_url}/api/chat/execute",
|
| 143 |
+
json=data,
|
| 144 |
+
headers=self.headers
|
| 145 |
+
)
|
| 146 |
+
response.raise_for_status()
|
| 147 |
+
return response.json()
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"Error executing chat: {str(e)}")
|
| 150 |
+
raise
|
| 151 |
+
|
| 152 |
+
async def build_context(self, notebook_id: str, context_config: Dict[str, Any]) -> Dict[str, Any]:
|
| 153 |
+
"""Build context for a notebook"""
|
| 154 |
+
try:
|
| 155 |
+
data = {
|
| 156 |
+
"notebook_id": notebook_id,
|
| 157 |
+
"context_config": context_config
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
async with httpx.AsyncClient() as client:
|
| 161 |
+
response = await client.post(
|
| 162 |
+
f"{self.base_url}/api/chat/context",
|
| 163 |
+
json=data,
|
| 164 |
+
headers=self.headers
|
| 165 |
+
)
|
| 166 |
+
response.raise_for_status()
|
| 167 |
+
return response.json()
|
| 168 |
+
except Exception as e:
|
| 169 |
+
logger.error(f"Error building context: {str(e)}")
|
| 170 |
+
raise
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# Global instance
|
| 174 |
+
chat_service = ChatService()
|
api/client.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API client for Open Notebook API.
|
| 3 |
+
This module provides a client interface to interact with the Open Notebook API.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from typing import Any, Dict, List, Optional, Union
|
| 8 |
+
|
| 9 |
+
import httpx
|
| 10 |
+
from loguru import logger
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class APIClient:
|
| 14 |
+
"""Client for Open Notebook API."""
|
| 15 |
+
|
| 16 |
+
def __init__(self, base_url: Optional[str] = None):
|
| 17 |
+
self.base_url = base_url or os.getenv("API_BASE_URL", "http://127.0.0.1:5055")
|
| 18 |
+
# Timeout increased to 5 minutes (300s) to accommodate slow LLM operations
|
| 19 |
+
# (transformations, insights) on slower hardware (Ollama, LM Studio, remote APIs)
|
| 20 |
+
# Configurable via API_CLIENT_TIMEOUT environment variable (in seconds)
|
| 21 |
+
timeout_str = os.getenv("API_CLIENT_TIMEOUT", "300.0")
|
| 22 |
+
try:
|
| 23 |
+
timeout_value = float(timeout_str)
|
| 24 |
+
# Validate timeout is within reasonable bounds (30s - 3600s / 1 hour)
|
| 25 |
+
if timeout_value < 30:
|
| 26 |
+
logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too low, using minimum of 30s")
|
| 27 |
+
timeout_value = 30.0
|
| 28 |
+
elif timeout_value > 3600:
|
| 29 |
+
logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too high, using maximum of 3600s")
|
| 30 |
+
timeout_value = 3600.0
|
| 31 |
+
self.timeout = timeout_value
|
| 32 |
+
except ValueError:
|
| 33 |
+
logger.error(f"Invalid API_CLIENT_TIMEOUT value '{timeout_str}', using default 300s")
|
| 34 |
+
self.timeout = 300.0
|
| 35 |
+
|
| 36 |
+
# Add authentication header if password is set
|
| 37 |
+
self.headers = {}
|
| 38 |
+
password = os.getenv("OPEN_NOTEBOOK_PASSWORD")
|
| 39 |
+
if password:
|
| 40 |
+
self.headers["Authorization"] = f"Bearer {password}"
|
| 41 |
+
|
| 42 |
+
def _make_request(
|
| 43 |
+
self, method: str, endpoint: str, timeout: Optional[float] = None, **kwargs
|
| 44 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 45 |
+
"""Make HTTP request to the API."""
|
| 46 |
+
url = f"{self.base_url}{endpoint}"
|
| 47 |
+
request_timeout = timeout if timeout is not None else self.timeout
|
| 48 |
+
|
| 49 |
+
# Merge headers
|
| 50 |
+
headers = kwargs.get("headers", {})
|
| 51 |
+
headers.update(self.headers)
|
| 52 |
+
kwargs["headers"] = headers
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
with httpx.Client(timeout=request_timeout) as client:
|
| 56 |
+
response = client.request(method, url, **kwargs)
|
| 57 |
+
response.raise_for_status()
|
| 58 |
+
return response.json()
|
| 59 |
+
except httpx.RequestError as e:
|
| 60 |
+
logger.error(f"Request error for {method} {url}: {str(e)}")
|
| 61 |
+
raise ConnectionError(f"Failed to connect to API: {str(e)}")
|
| 62 |
+
except httpx.HTTPStatusError as e:
|
| 63 |
+
logger.error(
|
| 64 |
+
f"HTTP error {e.response.status_code} for {method} {url}: {e.response.text}"
|
| 65 |
+
)
|
| 66 |
+
raise RuntimeError(
|
| 67 |
+
f"API request failed: {e.response.status_code} - {e.response.text}"
|
| 68 |
+
)
|
| 69 |
+
except Exception as e:
|
| 70 |
+
logger.error(f"Unexpected error for {method} {url}: {str(e)}")
|
| 71 |
+
raise
|
| 72 |
+
|
| 73 |
+
# Notebooks API methods
|
| 74 |
+
def get_notebooks(
|
| 75 |
+
self, archived: Optional[bool] = None, order_by: str = "updated desc"
|
| 76 |
+
) -> List[Dict[Any, Any]]:
|
| 77 |
+
"""Get all notebooks."""
|
| 78 |
+
params: Dict[str, Any] = {"order_by": order_by}
|
| 79 |
+
if archived is not None:
|
| 80 |
+
params["archived"] = str(archived).lower()
|
| 81 |
+
|
| 82 |
+
result = self._make_request("GET", "/api/notebooks", params=params)
|
| 83 |
+
return result if isinstance(result, list) else [result]
|
| 84 |
+
|
| 85 |
+
def create_notebook(self, name: str, description: str = "") -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 86 |
+
"""Create a new notebook."""
|
| 87 |
+
data = {"name": name, "description": description}
|
| 88 |
+
return self._make_request("POST", "/api/notebooks", json=data)
|
| 89 |
+
|
| 90 |
+
def get_notebook(self, notebook_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 91 |
+
"""Get a specific notebook."""
|
| 92 |
+
return self._make_request("GET", f"/api/notebooks/{notebook_id}")
|
| 93 |
+
|
| 94 |
+
def update_notebook(self, notebook_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 95 |
+
"""Update a notebook."""
|
| 96 |
+
return self._make_request("PUT", f"/api/notebooks/{notebook_id}", json=updates)
|
| 97 |
+
|
| 98 |
+
def delete_notebook(self, notebook_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 99 |
+
"""Delete a notebook."""
|
| 100 |
+
return self._make_request("DELETE", f"/api/notebooks/{notebook_id}")
|
| 101 |
+
|
| 102 |
+
# Search API methods
|
| 103 |
+
def search(
|
| 104 |
+
self,
|
| 105 |
+
query: str,
|
| 106 |
+
search_type: str = "text",
|
| 107 |
+
limit: int = 100,
|
| 108 |
+
search_sources: bool = True,
|
| 109 |
+
search_notes: bool = True,
|
| 110 |
+
minimum_score: float = 0.2,
|
| 111 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 112 |
+
"""Search the knowledge base."""
|
| 113 |
+
data = {
|
| 114 |
+
"query": query,
|
| 115 |
+
"type": search_type,
|
| 116 |
+
"limit": limit,
|
| 117 |
+
"search_sources": search_sources,
|
| 118 |
+
"search_notes": search_notes,
|
| 119 |
+
"minimum_score": minimum_score,
|
| 120 |
+
}
|
| 121 |
+
return self._make_request("POST", "/api/search", json=data)
|
| 122 |
+
|
| 123 |
+
def ask_simple(
|
| 124 |
+
self,
|
| 125 |
+
question: str,
|
| 126 |
+
strategy_model: str,
|
| 127 |
+
answer_model: str,
|
| 128 |
+
final_answer_model: str,
|
| 129 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 130 |
+
"""Ask the knowledge base a question (simple, non-streaming)."""
|
| 131 |
+
data = {
|
| 132 |
+
"question": question,
|
| 133 |
+
"strategy_model": strategy_model,
|
| 134 |
+
"answer_model": answer_model,
|
| 135 |
+
"final_answer_model": final_answer_model,
|
| 136 |
+
}
|
| 137 |
+
# Use configured timeout for long-running ask operations
|
| 138 |
+
return self._make_request(
|
| 139 |
+
"POST", "/api/search/ask/simple", json=data, timeout=self.timeout
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# Models API methods
|
| 143 |
+
def get_models(self, model_type: Optional[str] = None) -> List[Dict[Any, Any]]:
|
| 144 |
+
"""Get all models with optional type filtering."""
|
| 145 |
+
params = {}
|
| 146 |
+
if model_type:
|
| 147 |
+
params["type"] = model_type
|
| 148 |
+
result = self._make_request("GET", "/api/models", params=params)
|
| 149 |
+
return result if isinstance(result, list) else [result]
|
| 150 |
+
|
| 151 |
+
def create_model(self, name: str, provider: str, model_type: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 152 |
+
"""Create a new model."""
|
| 153 |
+
data = {
|
| 154 |
+
"name": name,
|
| 155 |
+
"provider": provider,
|
| 156 |
+
"type": model_type,
|
| 157 |
+
}
|
| 158 |
+
return self._make_request("POST", "/api/models", json=data)
|
| 159 |
+
|
| 160 |
+
def delete_model(self, model_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 161 |
+
"""Delete a model."""
|
| 162 |
+
return self._make_request("DELETE", f"/api/models/{model_id}")
|
| 163 |
+
|
| 164 |
+
def get_default_models(self) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 165 |
+
"""Get default model assignments."""
|
| 166 |
+
return self._make_request("GET", "/api/models/defaults")
|
| 167 |
+
|
| 168 |
+
def update_default_models(self, **defaults) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 169 |
+
"""Update default model assignments."""
|
| 170 |
+
return self._make_request("PUT", "/api/models/defaults", json=defaults)
|
| 171 |
+
|
| 172 |
+
# Transformations API methods
|
| 173 |
+
def get_transformations(self) -> List[Dict[Any, Any]]:
|
| 174 |
+
"""Get all transformations."""
|
| 175 |
+
result = self._make_request("GET", "/api/transformations")
|
| 176 |
+
return result if isinstance(result, list) else [result]
|
| 177 |
+
|
| 178 |
+
def create_transformation(
|
| 179 |
+
self,
|
| 180 |
+
name: str,
|
| 181 |
+
title: str,
|
| 182 |
+
description: str,
|
| 183 |
+
prompt: str,
|
| 184 |
+
apply_default: bool = False,
|
| 185 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 186 |
+
"""Create a new transformation."""
|
| 187 |
+
data = {
|
| 188 |
+
"name": name,
|
| 189 |
+
"title": title,
|
| 190 |
+
"description": description,
|
| 191 |
+
"prompt": prompt,
|
| 192 |
+
"apply_default": apply_default,
|
| 193 |
+
}
|
| 194 |
+
return self._make_request("POST", "/api/transformations", json=data)
|
| 195 |
+
|
| 196 |
+
def get_transformation(self, transformation_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 197 |
+
"""Get a specific transformation."""
|
| 198 |
+
return self._make_request("GET", f"/api/transformations/{transformation_id}")
|
| 199 |
+
|
| 200 |
+
def update_transformation(self, transformation_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 201 |
+
"""Update a transformation."""
|
| 202 |
+
return self._make_request(
|
| 203 |
+
"PUT", f"/api/transformations/{transformation_id}", json=updates
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
def delete_transformation(self, transformation_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 207 |
+
"""Delete a transformation."""
|
| 208 |
+
return self._make_request("DELETE", f"/api/transformations/{transformation_id}")
|
| 209 |
+
|
| 210 |
+
def execute_transformation(
|
| 211 |
+
self, transformation_id: str, input_text: str, model_id: str
|
| 212 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 213 |
+
"""Execute a transformation on input text."""
|
| 214 |
+
data = {
|
| 215 |
+
"transformation_id": transformation_id,
|
| 216 |
+
"input_text": input_text,
|
| 217 |
+
"model_id": model_id,
|
| 218 |
+
}
|
| 219 |
+
# Use configured timeout for transformation operations
|
| 220 |
+
return self._make_request(
|
| 221 |
+
"POST", "/api/transformations/execute", json=data, timeout=self.timeout
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Notes API methods
|
| 225 |
+
def get_notes(self, notebook_id: Optional[str] = None) -> List[Dict[Any, Any]]:
|
| 226 |
+
"""Get all notes with optional notebook filtering."""
|
| 227 |
+
params = {}
|
| 228 |
+
if notebook_id:
|
| 229 |
+
params["notebook_id"] = notebook_id
|
| 230 |
+
result = self._make_request("GET", "/api/notes", params=params)
|
| 231 |
+
return result if isinstance(result, list) else [result]
|
| 232 |
+
|
| 233 |
+
def create_note(
|
| 234 |
+
self,
|
| 235 |
+
content: str,
|
| 236 |
+
title: Optional[str] = None,
|
| 237 |
+
note_type: str = "human",
|
| 238 |
+
notebook_id: Optional[str] = None,
|
| 239 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 240 |
+
"""Create a new note."""
|
| 241 |
+
data = {
|
| 242 |
+
"content": content,
|
| 243 |
+
"note_type": note_type,
|
| 244 |
+
}
|
| 245 |
+
if title:
|
| 246 |
+
data["title"] = title
|
| 247 |
+
if notebook_id:
|
| 248 |
+
data["notebook_id"] = notebook_id
|
| 249 |
+
return self._make_request("POST", "/api/notes", json=data)
|
| 250 |
+
|
| 251 |
+
def get_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 252 |
+
"""Get a specific note."""
|
| 253 |
+
return self._make_request("GET", f"/api/notes/{note_id}")
|
| 254 |
+
|
| 255 |
+
def update_note(self, note_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 256 |
+
"""Update a note."""
|
| 257 |
+
return self._make_request("PUT", f"/api/notes/{note_id}", json=updates)
|
| 258 |
+
|
| 259 |
+
def delete_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 260 |
+
"""Delete a note."""
|
| 261 |
+
return self._make_request("DELETE", f"/api/notes/{note_id}")
|
| 262 |
+
|
| 263 |
+
# Embedding API methods
|
| 264 |
+
def embed_content(self, item_id: str, item_type: str, async_processing: bool = False) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 265 |
+
"""Embed content for vector search."""
|
| 266 |
+
data = {
|
| 267 |
+
"item_id": item_id,
|
| 268 |
+
"item_type": item_type,
|
| 269 |
+
"async_processing": async_processing,
|
| 270 |
+
}
|
| 271 |
+
# Use configured timeout for embedding operations
|
| 272 |
+
return self._make_request("POST", "/api/embed", json=data, timeout=self.timeout)
|
| 273 |
+
|
| 274 |
+
def rebuild_embeddings(
|
| 275 |
+
self,
|
| 276 |
+
mode: str = "existing",
|
| 277 |
+
include_sources: bool = True,
|
| 278 |
+
include_notes: bool = True,
|
| 279 |
+
include_insights: bool = True
|
| 280 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 281 |
+
"""Rebuild embeddings in bulk.
|
| 282 |
+
|
| 283 |
+
Note: This operation can take a long time for large databases.
|
| 284 |
+
Consider increasing API_CLIENT_TIMEOUT to 600-900s for bulk rebuilds.
|
| 285 |
+
"""
|
| 286 |
+
data = {
|
| 287 |
+
"mode": mode,
|
| 288 |
+
"include_sources": include_sources,
|
| 289 |
+
"include_notes": include_notes,
|
| 290 |
+
"include_insights": include_insights,
|
| 291 |
+
}
|
| 292 |
+
# Use double the configured timeout for bulk rebuild operations (or configured value if already high)
|
| 293 |
+
rebuild_timeout = max(self.timeout, min(self.timeout * 2, 3600.0))
|
| 294 |
+
return self._make_request("POST", "/api/embeddings/rebuild", json=data, timeout=rebuild_timeout)
|
| 295 |
+
|
| 296 |
+
def get_rebuild_status(self, command_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 297 |
+
"""Get status of a rebuild operation."""
|
| 298 |
+
return self._make_request("GET", f"/api/embeddings/rebuild/{command_id}/status")
|
| 299 |
+
|
| 300 |
+
# Settings API methods
|
| 301 |
+
def get_settings(self) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 302 |
+
"""Get all application settings."""
|
| 303 |
+
return self._make_request("GET", "/api/settings")
|
| 304 |
+
|
| 305 |
+
def update_settings(self, **settings) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 306 |
+
"""Update application settings."""
|
| 307 |
+
return self._make_request("PUT", "/api/settings", json=settings)
|
| 308 |
+
|
| 309 |
+
# Context API methods
|
| 310 |
+
def get_notebook_context(
|
| 311 |
+
self, notebook_id: str, context_config: Optional[Dict] = None
|
| 312 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 313 |
+
"""Get context for a notebook."""
|
| 314 |
+
data: Dict[str, Any] = {"notebook_id": notebook_id}
|
| 315 |
+
if context_config:
|
| 316 |
+
data["context_config"] = context_config
|
| 317 |
+
result = self._make_request(
|
| 318 |
+
"POST", f"/api/notebooks/{notebook_id}/context", json=data
|
| 319 |
+
)
|
| 320 |
+
return result if isinstance(result, dict) else {}
|
| 321 |
+
|
| 322 |
+
# Sources API methods
|
| 323 |
+
def get_sources(self, notebook_id: Optional[str] = None) -> List[Dict[Any, Any]]:
|
| 324 |
+
"""Get all sources with optional notebook filtering."""
|
| 325 |
+
params = {}
|
| 326 |
+
if notebook_id:
|
| 327 |
+
params["notebook_id"] = notebook_id
|
| 328 |
+
result = self._make_request("GET", "/api/sources", params=params)
|
| 329 |
+
return result if isinstance(result, list) else [result]
|
| 330 |
+
|
| 331 |
+
def create_source(
|
| 332 |
+
self,
|
| 333 |
+
notebook_id: Optional[str] = None,
|
| 334 |
+
notebooks: Optional[List[str]] = None,
|
| 335 |
+
source_type: str = "text",
|
| 336 |
+
url: Optional[str] = None,
|
| 337 |
+
file_path: Optional[str] = None,
|
| 338 |
+
content: Optional[str] = None,
|
| 339 |
+
title: Optional[str] = None,
|
| 340 |
+
transformations: Optional[List[str]] = None,
|
| 341 |
+
embed: bool = False,
|
| 342 |
+
delete_source: bool = False,
|
| 343 |
+
async_processing: bool = False,
|
| 344 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 345 |
+
"""Create a new source."""
|
| 346 |
+
data = {
|
| 347 |
+
"type": source_type,
|
| 348 |
+
"embed": embed,
|
| 349 |
+
"delete_source": delete_source,
|
| 350 |
+
"async_processing": async_processing,
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
# Handle backward compatibility for notebook_id vs notebooks
|
| 354 |
+
if notebooks:
|
| 355 |
+
data["notebooks"] = notebooks
|
| 356 |
+
elif notebook_id:
|
| 357 |
+
data["notebook_id"] = notebook_id
|
| 358 |
+
else:
|
| 359 |
+
raise ValueError("Either notebook_id or notebooks must be provided")
|
| 360 |
+
|
| 361 |
+
if url:
|
| 362 |
+
data["url"] = url
|
| 363 |
+
if file_path:
|
| 364 |
+
data["file_path"] = file_path
|
| 365 |
+
if content:
|
| 366 |
+
data["content"] = content
|
| 367 |
+
if title:
|
| 368 |
+
data["title"] = title
|
| 369 |
+
if transformations:
|
| 370 |
+
data["transformations"] = transformations
|
| 371 |
+
|
| 372 |
+
# Use configured timeout for source creation (especially PDF processing with OCR)
|
| 373 |
+
return self._make_request("POST", "/api/sources/json", json=data, timeout=self.timeout)
|
| 374 |
+
|
| 375 |
+
def get_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 376 |
+
"""Get a specific source."""
|
| 377 |
+
return self._make_request("GET", f"/api/sources/{source_id}")
|
| 378 |
+
|
| 379 |
+
def get_source_status(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 380 |
+
"""Get processing status for a source."""
|
| 381 |
+
return self._make_request("GET", f"/api/sources/{source_id}/status")
|
| 382 |
+
|
| 383 |
+
def update_source(self, source_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 384 |
+
"""Update a source."""
|
| 385 |
+
return self._make_request("PUT", f"/api/sources/{source_id}", json=updates)
|
| 386 |
+
|
| 387 |
+
def delete_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 388 |
+
"""Delete a source."""
|
| 389 |
+
return self._make_request("DELETE", f"/api/sources/{source_id}")
|
| 390 |
+
|
| 391 |
+
# Insights API methods
|
| 392 |
+
def get_source_insights(self, source_id: str) -> List[Dict[Any, Any]]:
|
| 393 |
+
"""Get all insights for a specific source."""
|
| 394 |
+
result = self._make_request("GET", f"/api/sources/{source_id}/insights")
|
| 395 |
+
return result if isinstance(result, list) else [result]
|
| 396 |
+
|
| 397 |
+
def get_insight(self, insight_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 398 |
+
"""Get a specific insight."""
|
| 399 |
+
return self._make_request("GET", f"/api/insights/{insight_id}")
|
| 400 |
+
|
| 401 |
+
def delete_insight(self, insight_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 402 |
+
"""Delete a specific insight."""
|
| 403 |
+
return self._make_request("DELETE", f"/api/insights/{insight_id}")
|
| 404 |
+
|
| 405 |
+
def save_insight_as_note(
|
| 406 |
+
self, insight_id: str, notebook_id: Optional[str] = None
|
| 407 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 408 |
+
"""Convert an insight to a note."""
|
| 409 |
+
data = {}
|
| 410 |
+
if notebook_id:
|
| 411 |
+
data["notebook_id"] = notebook_id
|
| 412 |
+
return self._make_request(
|
| 413 |
+
"POST", f"/api/insights/{insight_id}/save-as-note", json=data
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
def create_source_insight(
|
| 417 |
+
self, source_id: str, transformation_id: str, model_id: Optional[str] = None
|
| 418 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 419 |
+
"""Create a new insight for a source by running a transformation."""
|
| 420 |
+
data = {"transformation_id": transformation_id}
|
| 421 |
+
if model_id:
|
| 422 |
+
data["model_id"] = model_id
|
| 423 |
+
return self._make_request(
|
| 424 |
+
"POST", f"/api/sources/{source_id}/insights", json=data
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
# Episode Profiles API methods
|
| 428 |
+
def get_episode_profiles(self) -> List[Dict[Any, Any]]:
|
| 429 |
+
"""Get all episode profiles."""
|
| 430 |
+
result = self._make_request("GET", "/api/episode-profiles")
|
| 431 |
+
return result if isinstance(result, list) else [result]
|
| 432 |
+
|
| 433 |
+
def get_episode_profile(self, profile_name: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 434 |
+
"""Get a specific episode profile by name."""
|
| 435 |
+
return self._make_request("GET", f"/api/episode-profiles/{profile_name}")
|
| 436 |
+
|
| 437 |
+
def create_episode_profile(
|
| 438 |
+
self,
|
| 439 |
+
name: str,
|
| 440 |
+
description: str = "",
|
| 441 |
+
speaker_config: str = "",
|
| 442 |
+
outline_provider: str = "",
|
| 443 |
+
outline_model: str = "",
|
| 444 |
+
transcript_provider: str = "",
|
| 445 |
+
transcript_model: str = "",
|
| 446 |
+
default_briefing: str = "",
|
| 447 |
+
num_segments: int = 5,
|
| 448 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 449 |
+
"""Create a new episode profile."""
|
| 450 |
+
data = {
|
| 451 |
+
"name": name,
|
| 452 |
+
"description": description,
|
| 453 |
+
"speaker_config": speaker_config,
|
| 454 |
+
"outline_provider": outline_provider,
|
| 455 |
+
"outline_model": outline_model,
|
| 456 |
+
"transcript_provider": transcript_provider,
|
| 457 |
+
"transcript_model": transcript_model,
|
| 458 |
+
"default_briefing": default_briefing,
|
| 459 |
+
"num_segments": num_segments,
|
| 460 |
+
}
|
| 461 |
+
return self._make_request("POST", "/api/episode-profiles", json=data)
|
| 462 |
+
|
| 463 |
+
def update_episode_profile(self, profile_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 464 |
+
"""Update an episode profile."""
|
| 465 |
+
return self._make_request("PUT", f"/api/episode-profiles/{profile_id}", json=updates)
|
| 466 |
+
|
| 467 |
+
def delete_episode_profile(self, profile_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 468 |
+
"""Delete an episode profile."""
|
| 469 |
+
return self._make_request("DELETE", f"/api/episode-profiles/{profile_id}")
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
# Global client instance
|
| 473 |
+
api_client = APIClient()
|
api/command_service.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Optional
|
| 2 |
+
|
| 3 |
+
from loguru import logger
|
| 4 |
+
from surreal_commands import get_command_status, submit_command
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CommandService:
|
| 8 |
+
"""Generic service layer for command operations"""
|
| 9 |
+
|
| 10 |
+
@staticmethod
|
| 11 |
+
async def submit_command_job(
|
| 12 |
+
module_name: str, # Actually app_name for surreal-commands
|
| 13 |
+
command_name: str,
|
| 14 |
+
command_args: Dict[str, Any],
|
| 15 |
+
context: Optional[Dict[str, Any]] = None,
|
| 16 |
+
) -> str:
|
| 17 |
+
"""Submit a generic command job for background processing"""
|
| 18 |
+
try:
|
| 19 |
+
# Ensure command modules are imported before submitting
|
| 20 |
+
# This is needed because submit_command validates against local registry
|
| 21 |
+
try:
|
| 22 |
+
import commands.podcast_commands # noqa: F401
|
| 23 |
+
except ImportError as import_err:
|
| 24 |
+
logger.error(f"Failed to import command modules: {import_err}")
|
| 25 |
+
raise ValueError("Command modules not available")
|
| 26 |
+
|
| 27 |
+
# surreal-commands expects: submit_command(app_name, command_name, args)
|
| 28 |
+
cmd_id = submit_command(
|
| 29 |
+
module_name, # This is actually the app name (e.g., "open_notebook")
|
| 30 |
+
command_name, # Command name (e.g., "process_text")
|
| 31 |
+
command_args, # Input data
|
| 32 |
+
)
|
| 33 |
+
# Convert RecordID to string if needed
|
| 34 |
+
if not cmd_id:
|
| 35 |
+
raise ValueError("Failed to get cmd_id from submit_command")
|
| 36 |
+
cmd_id_str = str(cmd_id)
|
| 37 |
+
logger.info(
|
| 38 |
+
f"Submitted command job: {cmd_id_str} for {module_name}.{command_name}"
|
| 39 |
+
)
|
| 40 |
+
return cmd_id_str
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error(f"Failed to submit command job: {e}")
|
| 44 |
+
raise
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
async def get_command_status(job_id: str) -> Dict[str, Any]:
|
| 48 |
+
"""Get status of any command job"""
|
| 49 |
+
try:
|
| 50 |
+
status = await get_command_status(job_id)
|
| 51 |
+
return {
|
| 52 |
+
"job_id": job_id,
|
| 53 |
+
"status": status.status if status else "unknown",
|
| 54 |
+
"result": status.result if status else None,
|
| 55 |
+
"error_message": getattr(status, "error_message", None)
|
| 56 |
+
if status
|
| 57 |
+
else None,
|
| 58 |
+
"created": str(status.created)
|
| 59 |
+
if status and hasattr(status, "created") and status.created
|
| 60 |
+
else None,
|
| 61 |
+
"updated": str(status.updated)
|
| 62 |
+
if status and hasattr(status, "updated") and status.updated
|
| 63 |
+
else None,
|
| 64 |
+
"progress": getattr(status, "progress", None) if status else None,
|
| 65 |
+
}
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logger.error(f"Failed to get command status: {e}")
|
| 68 |
+
raise
|
| 69 |
+
|
| 70 |
+
@staticmethod
|
| 71 |
+
async def list_command_jobs(
|
| 72 |
+
module_filter: Optional[str] = None,
|
| 73 |
+
command_filter: Optional[str] = None,
|
| 74 |
+
status_filter: Optional[str] = None,
|
| 75 |
+
limit: int = 50,
|
| 76 |
+
) -> List[Dict[str, Any]]:
|
| 77 |
+
"""List command jobs with optional filtering"""
|
| 78 |
+
# This will be implemented with proper SurrealDB queries
|
| 79 |
+
# For now, return empty list as this is foundation phase
|
| 80 |
+
return []
|
| 81 |
+
|
| 82 |
+
@staticmethod
|
| 83 |
+
async def cancel_command_job(job_id: str) -> bool:
|
| 84 |
+
"""Cancel a running command job"""
|
| 85 |
+
try:
|
| 86 |
+
# Implementation depends on surreal-commands cancellation support
|
| 87 |
+
# For now, just log the attempt
|
| 88 |
+
logger.info(f"Attempting to cancel job: {job_id}")
|
| 89 |
+
return True
|
| 90 |
+
except Exception as e:
|
| 91 |
+
logger.error(f"Failed to cancel command job: {e}")
|
| 92 |
+
raise
|
api/context_service.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Context service layer using API.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Optional, Union
|
| 6 |
+
|
| 7 |
+
from loguru import logger
|
| 8 |
+
|
| 9 |
+
from api.client import api_client
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ContextService:
|
| 13 |
+
"""Service layer for context operations using API."""
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
logger.info("Using API for context operations")
|
| 17 |
+
|
| 18 |
+
def get_notebook_context(
|
| 19 |
+
self,
|
| 20 |
+
notebook_id: str,
|
| 21 |
+
context_config: Optional[Dict] = None
|
| 22 |
+
) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 23 |
+
"""Get context for a notebook."""
|
| 24 |
+
result = api_client.get_notebook_context(
|
| 25 |
+
notebook_id=notebook_id,
|
| 26 |
+
context_config=context_config
|
| 27 |
+
)
|
| 28 |
+
return result
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Global service instance
|
| 32 |
+
context_service = ContextService()
|
api/embedding_service.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Embedding service layer using API.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Union
|
| 6 |
+
|
| 7 |
+
from loguru import logger
|
| 8 |
+
|
| 9 |
+
from api.client import api_client
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class EmbeddingService:
|
| 13 |
+
"""Service layer for embedding operations using API."""
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
logger.info("Using API for embedding operations")
|
| 17 |
+
|
| 18 |
+
def embed_content(self, item_id: str, item_type: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
|
| 19 |
+
"""Embed content for vector search."""
|
| 20 |
+
result = api_client.embed_content(item_id=item_id, item_type=item_type)
|
| 21 |
+
return result
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Global service instance
|
| 25 |
+
embedding_service = EmbeddingService()
|
api/episode_profiles_service.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Episode profiles service layer using API.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
from loguru import logger
|
| 8 |
+
|
| 9 |
+
from api.client import api_client
|
| 10 |
+
from open_notebook.domain.podcast import EpisodeProfile
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class EpisodeProfilesService:
|
| 14 |
+
"""Service layer for episode profiles operations using API."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
logger.info("Using API for episode profiles operations")
|
| 18 |
+
|
| 19 |
+
def get_all_episode_profiles(self) -> List[EpisodeProfile]:
|
| 20 |
+
"""Get all episode profiles."""
|
| 21 |
+
profiles_data = api_client.get_episode_profiles()
|
| 22 |
+
# Convert API response to EpisodeProfile objects
|
| 23 |
+
profiles = []
|
| 24 |
+
for profile_data in profiles_data:
|
| 25 |
+
profile = EpisodeProfile(
|
| 26 |
+
name=profile_data["name"],
|
| 27 |
+
description=profile_data.get("description", ""),
|
| 28 |
+
speaker_config=profile_data["speaker_config"],
|
| 29 |
+
outline_provider=profile_data["outline_provider"],
|
| 30 |
+
outline_model=profile_data["outline_model"],
|
| 31 |
+
transcript_provider=profile_data["transcript_provider"],
|
| 32 |
+
transcript_model=profile_data["transcript_model"],
|
| 33 |
+
default_briefing=profile_data["default_briefing"],
|
| 34 |
+
num_segments=profile_data["num_segments"]
|
| 35 |
+
)
|
| 36 |
+
profile.id = profile_data["id"]
|
| 37 |
+
profiles.append(profile)
|
| 38 |
+
return profiles
|
| 39 |
+
|
| 40 |
+
def get_episode_profile(self, profile_name: str) -> EpisodeProfile:
|
| 41 |
+
"""Get a specific episode profile by name."""
|
| 42 |
+
profile_response = api_client.get_episode_profile(profile_name)
|
| 43 |
+
profile_data = profile_response if isinstance(profile_response, dict) else profile_response[0]
|
| 44 |
+
profile = EpisodeProfile(
|
| 45 |
+
name=profile_data["name"],
|
| 46 |
+
description=profile_data.get("description", ""),
|
| 47 |
+
speaker_config=profile_data["speaker_config"],
|
| 48 |
+
outline_provider=profile_data["outline_provider"],
|
| 49 |
+
outline_model=profile_data["outline_model"],
|
| 50 |
+
transcript_provider=profile_data["transcript_provider"],
|
| 51 |
+
transcript_model=profile_data["transcript_model"],
|
| 52 |
+
default_briefing=profile_data["default_briefing"],
|
| 53 |
+
num_segments=profile_data["num_segments"]
|
| 54 |
+
)
|
| 55 |
+
profile.id = profile_data["id"]
|
| 56 |
+
return profile
|
| 57 |
+
|
| 58 |
+
def create_episode_profile(
|
| 59 |
+
self,
|
| 60 |
+
name: str,
|
| 61 |
+
description: str = "",
|
| 62 |
+
speaker_config: str = "",
|
| 63 |
+
outline_provider: str = "",
|
| 64 |
+
outline_model: str = "",
|
| 65 |
+
transcript_provider: str = "",
|
| 66 |
+
transcript_model: str = "",
|
| 67 |
+
default_briefing: str = "",
|
| 68 |
+
num_segments: int = 5,
|
| 69 |
+
) -> EpisodeProfile:
|
| 70 |
+
"""Create a new episode profile."""
|
| 71 |
+
profile_response = api_client.create_episode_profile(
|
| 72 |
+
name=name,
|
| 73 |
+
description=description,
|
| 74 |
+
speaker_config=speaker_config,
|
| 75 |
+
outline_provider=outline_provider,
|
| 76 |
+
outline_model=outline_model,
|
| 77 |
+
transcript_provider=transcript_provider,
|
| 78 |
+
transcript_model=transcript_model,
|
| 79 |
+
default_briefing=default_briefing,
|
| 80 |
+
num_segments=num_segments,
|
| 81 |
+
)
|
| 82 |
+
profile_data = profile_response if isinstance(profile_response, dict) else profile_response[0]
|
| 83 |
+
profile = EpisodeProfile(
|
| 84 |
+
name=profile_data["name"],
|
| 85 |
+
description=profile_data.get("description", ""),
|
| 86 |
+
speaker_config=profile_data["speaker_config"],
|
| 87 |
+
outline_provider=profile_data["outline_provider"],
|
| 88 |
+
outline_model=profile_data["outline_model"],
|
| 89 |
+
transcript_provider=profile_data["transcript_provider"],
|
| 90 |
+
transcript_model=profile_data["transcript_model"],
|
| 91 |
+
default_briefing=profile_data["default_briefing"],
|
| 92 |
+
num_segments=profile_data["num_segments"]
|
| 93 |
+
)
|
| 94 |
+
profile.id = profile_data["id"]
|
| 95 |
+
return profile
|
| 96 |
+
|
| 97 |
+
def delete_episode_profile(self, profile_id: str) -> bool:
|
| 98 |
+
"""Delete an episode profile."""
|
| 99 |
+
api_client.delete_episode_profile(profile_id)
|
| 100 |
+
return True
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# Global service instance
|
| 104 |
+
episode_profiles_service = EpisodeProfilesService()
|
api/insights_service.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Insights service layer using API.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
from loguru import logger
|
| 8 |
+
|
| 9 |
+
from api.client import api_client
|
| 10 |
+
from open_notebook.domain.notebook import Note, SourceInsight
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class InsightsService:
|
| 14 |
+
"""Service layer for insights operations using API."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
logger.info("Using API for insights operations")
|
| 18 |
+
|
| 19 |
+
def get_source_insights(self, source_id: str) -> List[SourceInsight]:
|
| 20 |
+
"""Get all insights for a specific source."""
|
| 21 |
+
insights_data = api_client.get_source_insights(source_id)
|
| 22 |
+
# Convert API response to SourceInsight objects
|
| 23 |
+
insights = []
|
| 24 |
+
for insight_data in insights_data:
|
| 25 |
+
insight = SourceInsight(
|
| 26 |
+
insight_type=insight_data["insight_type"],
|
| 27 |
+
content=insight_data["content"],
|
| 28 |
+
)
|
| 29 |
+
insight.id = insight_data["id"]
|
| 30 |
+
insight.created = insight_data["created"]
|
| 31 |
+
insight.updated = insight_data["updated"]
|
| 32 |
+
insights.append(insight)
|
| 33 |
+
return insights
|
| 34 |
+
|
| 35 |
+
def get_insight(self, insight_id: str) -> SourceInsight:
|
| 36 |
+
"""Get a specific insight."""
|
| 37 |
+
insight_response = api_client.get_insight(insight_id)
|
| 38 |
+
insight_data = insight_response if isinstance(insight_response, dict) else insight_response[0]
|
| 39 |
+
insight = SourceInsight(
|
| 40 |
+
insight_type=insight_data["insight_type"],
|
| 41 |
+
content=insight_data["content"],
|
| 42 |
+
)
|
| 43 |
+
insight.id = insight_data["id"]
|
| 44 |
+
insight.created = insight_data["created"]
|
| 45 |
+
insight.updated = insight_data["updated"]
|
| 46 |
+
# Note: source_id from API response is not stored; use await insight.get_source() if needed
|
| 47 |
+
return insight
|
| 48 |
+
|
| 49 |
+
def delete_insight(self, insight_id: str) -> bool:
|
| 50 |
+
"""Delete a specific insight."""
|
| 51 |
+
api_client.delete_insight(insight_id)
|
| 52 |
+
return True
|
| 53 |
+
|
| 54 |
+
def save_insight_as_note(self, insight_id: str, notebook_id: Optional[str] = None) -> Note:
|
| 55 |
+
"""Convert an insight to a note."""
|
| 56 |
+
note_response = api_client.save_insight_as_note(insight_id, notebook_id)
|
| 57 |
+
note_data = note_response if isinstance(note_response, dict) else note_response[0]
|
| 58 |
+
note = Note(
|
| 59 |
+
title=note_data["title"],
|
| 60 |
+
content=note_data["content"],
|
| 61 |
+
note_type=note_data["note_type"],
|
| 62 |
+
)
|
| 63 |
+
note.id = note_data["id"]
|
| 64 |
+
note.created = note_data["created"]
|
| 65 |
+
note.updated = note_data["updated"]
|
| 66 |
+
return note
|
| 67 |
+
|
| 68 |
+
def create_source_insight(self, source_id: str, transformation_id: str, model_id: Optional[str] = None) -> SourceInsight:
|
| 69 |
+
"""Create a new insight for a source by running a transformation."""
|
| 70 |
+
insight_response = api_client.create_source_insight(source_id, transformation_id, model_id)
|
| 71 |
+
insight_data = insight_response if isinstance(insight_response, dict) else insight_response[0]
|
| 72 |
+
insight = SourceInsight(
|
| 73 |
+
insight_type=insight_data["insight_type"],
|
| 74 |
+
content=insight_data["content"],
|
| 75 |
+
)
|
| 76 |
+
insight.id = insight_data["id"]
|
| 77 |
+
insight.created = insight_data["created"]
|
| 78 |
+
insight.updated = insight_data["updated"]
|
| 79 |
+
# Note: source_id from API response is not stored; use await insight.get_source() if needed
|
| 80 |
+
return insight
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# Global service instance
|
| 84 |
+
insights_service = InsightsService()
|
api/main.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Load environment variables
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
load_dotenv()
|
| 4 |
+
|
| 5 |
+
from contextlib import asynccontextmanager
|
| 6 |
+
|
| 7 |
+
from fastapi import FastAPI
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from loguru import logger
|
| 10 |
+
|
| 11 |
+
from api.auth import PasswordAuthMiddleware
|
| 12 |
+
from api.routers import (
|
| 13 |
+
auth,
|
| 14 |
+
chat,
|
| 15 |
+
config,
|
| 16 |
+
context,
|
| 17 |
+
embedding,
|
| 18 |
+
embedding_rebuild,
|
| 19 |
+
episode_profiles,
|
| 20 |
+
insights,
|
| 21 |
+
knowledge_graph,
|
| 22 |
+
models,
|
| 23 |
+
monitoring,
|
| 24 |
+
notebooks,
|
| 25 |
+
notes,
|
| 26 |
+
ocr,
|
| 27 |
+
podcasts,
|
| 28 |
+
quiz,
|
| 29 |
+
research,
|
| 30 |
+
search,
|
| 31 |
+
settings,
|
| 32 |
+
source_chat,
|
| 33 |
+
sources,
|
| 34 |
+
speaker_profiles,
|
| 35 |
+
study_plans,
|
| 36 |
+
transformations,
|
| 37 |
+
diagrams,
|
| 38 |
+
)
|
| 39 |
+
from api.routers import commands as commands_router
|
| 40 |
+
from open_notebook.database.async_migrate import AsyncMigrationManager
|
| 41 |
+
|
| 42 |
+
# Import commands to register them in the API process
|
| 43 |
+
try:
|
| 44 |
+
|
| 45 |
+
logger.info("Commands imported in API process")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
logger.error(f"Failed to import commands in API process: {e}")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@asynccontextmanager
|
| 51 |
+
async def lifespan(app: FastAPI):
|
| 52 |
+
"""
|
| 53 |
+
Lifespan event handler for the FastAPI application.
|
| 54 |
+
Runs database migrations automatically on startup.
|
| 55 |
+
"""
|
| 56 |
+
# Startup: Run database migrations
|
| 57 |
+
logger.info("Starting API initialization...")
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
migration_manager = AsyncMigrationManager()
|
| 61 |
+
current_version = await migration_manager.get_current_version()
|
| 62 |
+
logger.info(f"Current database version: {current_version}")
|
| 63 |
+
|
| 64 |
+
if await migration_manager.needs_migration():
|
| 65 |
+
logger.warning("Database migrations are pending. Running migrations...")
|
| 66 |
+
await migration_manager.run_migration_up()
|
| 67 |
+
new_version = await migration_manager.get_current_version()
|
| 68 |
+
logger.success(f"Migrations completed successfully. Database is now at version {new_version}")
|
| 69 |
+
else:
|
| 70 |
+
logger.info("Database is already at the latest version. No migrations needed.")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"CRITICAL: Database migration failed: {str(e)}")
|
| 73 |
+
logger.exception(e)
|
| 74 |
+
# Fail fast - don't start the API with an outdated database schema
|
| 75 |
+
raise RuntimeError(f"Failed to run database migrations: {str(e)}") from e
|
| 76 |
+
|
| 77 |
+
logger.success("API initialization completed successfully")
|
| 78 |
+
|
| 79 |
+
# Yield control to the application
|
| 80 |
+
yield
|
| 81 |
+
|
| 82 |
+
# Shutdown: cleanup if needed
|
| 83 |
+
logger.info("API shutdown complete")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
app = FastAPI(
|
| 87 |
+
title="Open Notebook API",
|
| 88 |
+
description="API for Open Notebook - Research Assistant",
|
| 89 |
+
version="0.2.2",
|
| 90 |
+
lifespan=lifespan,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# Add password authentication middleware first
|
| 94 |
+
# Exclude /api/auth/status and /api/config from authentication
|
| 95 |
+
app.add_middleware(PasswordAuthMiddleware, excluded_paths=["/", "/health", "/docs", "/openapi.json", "/redoc", "/api/auth/status", "/api/config"])
|
| 96 |
+
|
| 97 |
+
# Add CORS middleware last (so it processes first)
|
| 98 |
+
# Allow requests from:
|
| 99 |
+
# - localhost development (http://localhost:3000)
|
| 100 |
+
# - Hugging Face Space backend (https://baveshraam-open-notebook.hf.space)
|
| 101 |
+
# - Any frontend deployment (can be restricted further in production)
|
| 102 |
+
app.add_middleware(
|
| 103 |
+
CORSMiddleware,
|
| 104 |
+
allow_origins=[
|
| 105 |
+
"http://localhost:3000",
|
| 106 |
+
"http://127.0.0.1:3000",
|
| 107 |
+
"https://baveshraam-open-notebook.hf.space",
|
| 108 |
+
"*" # Allow all origins - can be restricted later
|
| 109 |
+
],
|
| 110 |
+
allow_credentials=True,
|
| 111 |
+
allow_methods=["*"],
|
| 112 |
+
allow_headers=["*"],
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Include routers
|
| 116 |
+
app.include_router(auth.router, prefix="/api", tags=["auth"])
|
| 117 |
+
app.include_router(config.router, prefix="/api", tags=["config"])
|
| 118 |
+
app.include_router(notebooks.router, prefix="/api", tags=["notebooks"])
|
| 119 |
+
app.include_router(search.router, prefix="/api", tags=["search"])
|
| 120 |
+
app.include_router(models.router, prefix="/api", tags=["models"])
|
| 121 |
+
app.include_router(transformations.router, prefix="/api", tags=["transformations"])
|
| 122 |
+
app.include_router(notes.router, prefix="/api", tags=["notes"])
|
| 123 |
+
app.include_router(embedding.router, prefix="/api", tags=["embedding"])
|
| 124 |
+
app.include_router(embedding_rebuild.router, prefix="/api/embeddings", tags=["embeddings"])
|
| 125 |
+
app.include_router(settings.router, prefix="/api", tags=["settings"])
|
| 126 |
+
app.include_router(context.router, prefix="/api", tags=["context"])
|
| 127 |
+
app.include_router(sources.router, prefix="/api", tags=["sources"])
|
| 128 |
+
app.include_router(insights.router, prefix="/api", tags=["insights"])
|
| 129 |
+
app.include_router(commands_router.router, prefix="/api", tags=["commands"])
|
| 130 |
+
app.include_router(podcasts.router, prefix="/api", tags=["podcasts"])
|
| 131 |
+
app.include_router(episode_profiles.router, prefix="/api", tags=["episode-profiles"])
|
| 132 |
+
app.include_router(speaker_profiles.router, prefix="/api", tags=["speaker-profiles"])
|
| 133 |
+
app.include_router(chat.router, prefix="/api", tags=["chat"])
|
| 134 |
+
app.include_router(source_chat.router, prefix="/api", tags=["source-chat"])
|
| 135 |
+
app.include_router(quiz.router, prefix="/api", tags=["quiz"])
|
| 136 |
+
app.include_router(research.router, prefix="/api", tags=["research"])
|
| 137 |
+
app.include_router(knowledge_graph.router, prefix="/api", tags=["knowledge-graph"])
|
| 138 |
+
app.include_router(monitoring.router, prefix="/api", tags=["monitoring"])
|
| 139 |
+
app.include_router(ocr.router, prefix="/api", tags=["ocr"])
|
| 140 |
+
app.include_router(study_plans.router, prefix="/api", tags=["study-plans"])
|
| 141 |
+
app.include_router(diagrams.router, prefix="/api", tags=["diagrams"])
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@app.get("/")
|
| 145 |
+
async def root():
|
| 146 |
+
return {"message": "Open Notebook API is running"}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@app.get("/health")
|
| 150 |
+
async def health():
|
| 151 |
+
return {"status": "healthy"}
|
api/models.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Notebook models
|
| 7 |
+
class NotebookCreate(BaseModel):
|
| 8 |
+
name: str = Field(..., description="Name of the notebook")
|
| 9 |
+
description: str = Field(default="", description="Description of the notebook")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class NotebookUpdate(BaseModel):
|
| 13 |
+
name: Optional[str] = Field(None, description="Name of the notebook")
|
| 14 |
+
description: Optional[str] = Field(None, description="Description of the notebook")
|
| 15 |
+
archived: Optional[bool] = Field(
|
| 16 |
+
None, description="Whether the notebook is archived"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class NotebookResponse(BaseModel):
|
| 21 |
+
id: str
|
| 22 |
+
name: str
|
| 23 |
+
description: str
|
| 24 |
+
archived: bool
|
| 25 |
+
created: str
|
| 26 |
+
updated: str
|
| 27 |
+
source_count: int
|
| 28 |
+
note_count: int
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Search models
|
| 32 |
+
class SearchRequest(BaseModel):
|
| 33 |
+
query: str = Field(..., description="Search query")
|
| 34 |
+
type: Literal["text", "vector"] = Field("text", description="Search type")
|
| 35 |
+
limit: int = Field(100, description="Maximum number of results", le=1000)
|
| 36 |
+
search_sources: bool = Field(True, description="Include sources in search")
|
| 37 |
+
search_notes: bool = Field(True, description="Include notes in search")
|
| 38 |
+
minimum_score: float = Field(
|
| 39 |
+
0.2, description="Minimum score for vector search", ge=0, le=1
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class SearchResponse(BaseModel):
|
| 44 |
+
results: List[Dict[str, Any]] = Field(..., description="Search results")
|
| 45 |
+
total_count: int = Field(..., description="Total number of results")
|
| 46 |
+
search_type: str = Field(..., description="Type of search performed")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class AskRequest(BaseModel):
|
| 50 |
+
question: str = Field(..., description="Question to ask the knowledge base")
|
| 51 |
+
strategy_model: str = Field(..., description="Model ID for query strategy")
|
| 52 |
+
answer_model: str = Field(..., description="Model ID for individual answers")
|
| 53 |
+
final_answer_model: str = Field(..., description="Model ID for final answer")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class DirectAskRequest(BaseModel):
|
| 57 |
+
"""Request for direct AI queries (without RAG)"""
|
| 58 |
+
question: str = Field(..., description="Question to ask AI")
|
| 59 |
+
model_id: Optional[str] = Field(None, description="Model ID to use (optional)")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class AskResponse(BaseModel):
|
| 63 |
+
answer: str = Field(..., description="Final answer from the knowledge base")
|
| 64 |
+
question: str = Field(..., description="Original question")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# Models API models
|
| 68 |
+
class ModelCreate(BaseModel):
|
| 69 |
+
name: str = Field(..., description="Model name (e.g., gpt-5-mini, claude, gemini)")
|
| 70 |
+
provider: str = Field(
|
| 71 |
+
..., description="Provider name (e.g., openai, anthropic, gemini)"
|
| 72 |
+
)
|
| 73 |
+
type: str = Field(
|
| 74 |
+
...,
|
| 75 |
+
description="Model type (language, embedding, text_to_speech, speech_to_text)",
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class ModelResponse(BaseModel):
|
| 80 |
+
id: str
|
| 81 |
+
name: str
|
| 82 |
+
provider: str
|
| 83 |
+
type: str
|
| 84 |
+
created: str
|
| 85 |
+
updated: str
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class DefaultModelsResponse(BaseModel):
|
| 89 |
+
default_chat_model: Optional[str] = None
|
| 90 |
+
default_transformation_model: Optional[str] = None
|
| 91 |
+
large_context_model: Optional[str] = None
|
| 92 |
+
default_text_to_speech_model: Optional[str] = None
|
| 93 |
+
default_speech_to_text_model: Optional[str] = None
|
| 94 |
+
default_embedding_model: Optional[str] = None
|
| 95 |
+
default_tools_model: Optional[str] = None
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class ProviderAvailabilityResponse(BaseModel):
|
| 99 |
+
available: List[str] = Field(..., description="List of available providers")
|
| 100 |
+
unavailable: List[str] = Field(..., description="List of unavailable providers")
|
| 101 |
+
supported_types: Dict[str, List[str]] = Field(
|
| 102 |
+
..., description="Provider to supported model types mapping"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# Transformations API models
|
| 107 |
+
class TransformationCreate(BaseModel):
|
| 108 |
+
name: str = Field(..., description="Transformation name")
|
| 109 |
+
title: str = Field(..., description="Display title for the transformation")
|
| 110 |
+
description: str = Field(
|
| 111 |
+
..., description="Description of what this transformation does"
|
| 112 |
+
)
|
| 113 |
+
prompt: str = Field(..., description="The transformation prompt")
|
| 114 |
+
apply_default: bool = Field(
|
| 115 |
+
False, description="Whether to apply this transformation by default"
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class TransformationUpdate(BaseModel):
|
| 120 |
+
name: Optional[str] = Field(None, description="Transformation name")
|
| 121 |
+
title: Optional[str] = Field(
|
| 122 |
+
None, description="Display title for the transformation"
|
| 123 |
+
)
|
| 124 |
+
description: Optional[str] = Field(
|
| 125 |
+
None, description="Description of what this transformation does"
|
| 126 |
+
)
|
| 127 |
+
prompt: Optional[str] = Field(None, description="The transformation prompt")
|
| 128 |
+
apply_default: Optional[bool] = Field(
|
| 129 |
+
None, description="Whether to apply this transformation by default"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class TransformationResponse(BaseModel):
|
| 134 |
+
id: str
|
| 135 |
+
name: str
|
| 136 |
+
title: str
|
| 137 |
+
description: str
|
| 138 |
+
prompt: str
|
| 139 |
+
apply_default: bool
|
| 140 |
+
created: str
|
| 141 |
+
updated: str
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class TransformationExecuteRequest(BaseModel):
|
| 145 |
+
model_config = ConfigDict(protected_namespaces=())
|
| 146 |
+
|
| 147 |
+
transformation_id: str = Field(
|
| 148 |
+
..., description="ID of the transformation to execute"
|
| 149 |
+
)
|
| 150 |
+
input_text: str = Field(..., description="Text to transform")
|
| 151 |
+
model_id: str = Field(..., description="Model ID to use for the transformation")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class TransformationExecuteResponse(BaseModel):
|
| 155 |
+
model_config = ConfigDict(protected_namespaces=())
|
| 156 |
+
|
| 157 |
+
output: str = Field(..., description="Transformed text")
|
| 158 |
+
transformation_id: str = Field(..., description="ID of the transformation used")
|
| 159 |
+
model_id: str = Field(..., description="Model ID used")
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# Default Prompt API models
|
| 163 |
+
class DefaultPromptResponse(BaseModel):
|
| 164 |
+
transformation_instructions: str = Field(
|
| 165 |
+
..., description="Default transformation instructions"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class DefaultPromptUpdate(BaseModel):
|
| 170 |
+
transformation_instructions: str = Field(
|
| 171 |
+
..., description="Default transformation instructions"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# Notes API models
|
| 176 |
+
class NoteCreate(BaseModel):
|
| 177 |
+
title: Optional[str] = Field(None, description="Note title")
|
| 178 |
+
content: str = Field(..., description="Note content")
|
| 179 |
+
note_type: Optional[str] = Field("human", description="Type of note (human, ai)")
|
| 180 |
+
notebook_id: Optional[str] = Field(
|
| 181 |
+
None, description="Notebook ID to add the note to"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class NoteUpdate(BaseModel):
|
| 186 |
+
title: Optional[str] = Field(None, description="Note title")
|
| 187 |
+
content: Optional[str] = Field(None, description="Note content")
|
| 188 |
+
note_type: Optional[str] = Field(None, description="Type of note (human, ai)")
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class NoteResponse(BaseModel):
|
| 192 |
+
id: str
|
| 193 |
+
title: Optional[str]
|
| 194 |
+
content: Optional[str]
|
| 195 |
+
note_type: Optional[str]
|
| 196 |
+
created: str
|
| 197 |
+
updated: str
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# Embedding API models
|
| 201 |
+
class EmbedRequest(BaseModel):
|
| 202 |
+
item_id: str = Field(..., description="ID of the item to embed")
|
| 203 |
+
item_type: str = Field(..., description="Type of item (source, note)")
|
| 204 |
+
async_processing: bool = Field(
|
| 205 |
+
False, description="Process asynchronously in background"
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class EmbedResponse(BaseModel):
|
| 210 |
+
success: bool = Field(..., description="Whether embedding was successful")
|
| 211 |
+
message: str = Field(..., description="Result message")
|
| 212 |
+
item_id: str = Field(..., description="ID of the item that was embedded")
|
| 213 |
+
item_type: str = Field(..., description="Type of item that was embedded")
|
| 214 |
+
command_id: Optional[str] = Field(
|
| 215 |
+
None, description="Command ID for async processing"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# Rebuild request/response models
|
| 220 |
+
class RebuildRequest(BaseModel):
|
| 221 |
+
mode: Literal["existing", "all"] = Field(
|
| 222 |
+
...,
|
| 223 |
+
description="Rebuild mode: 'existing' only re-embeds items with embeddings, 'all' embeds everything",
|
| 224 |
+
)
|
| 225 |
+
include_sources: bool = Field(True, description="Include sources in rebuild")
|
| 226 |
+
include_notes: bool = Field(True, description="Include notes in rebuild")
|
| 227 |
+
include_insights: bool = Field(True, description="Include insights in rebuild")
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
class RebuildResponse(BaseModel):
|
| 231 |
+
command_id: str = Field(..., description="Command ID to track progress")
|
| 232 |
+
total_items: int = Field(..., description="Estimated number of items to process")
|
| 233 |
+
message: str = Field(..., description="Status message")
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
class RebuildProgress(BaseModel):
|
| 237 |
+
processed: int = Field(..., description="Number of items processed")
|
| 238 |
+
total: int = Field(..., description="Total items to process")
|
| 239 |
+
percentage: float = Field(..., description="Progress percentage")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class RebuildStats(BaseModel):
|
| 243 |
+
sources: int = Field(0, description="Sources processed")
|
| 244 |
+
notes: int = Field(0, description="Notes processed")
|
| 245 |
+
insights: int = Field(0, description="Insights processed")
|
| 246 |
+
failed: int = Field(0, description="Failed items")
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class RebuildStatusResponse(BaseModel):
|
| 250 |
+
command_id: str = Field(..., description="Command ID")
|
| 251 |
+
status: str = Field(..., description="Status: queued, running, completed, failed")
|
| 252 |
+
progress: Optional[RebuildProgress] = None
|
| 253 |
+
stats: Optional[RebuildStats] = None
|
| 254 |
+
started_at: Optional[str] = None
|
| 255 |
+
completed_at: Optional[str] = None
|
| 256 |
+
error_message: Optional[str] = None
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# Settings API models
|
| 260 |
+
class SettingsResponse(BaseModel):
|
| 261 |
+
default_content_processing_engine_doc: Optional[str] = None
|
| 262 |
+
default_content_processing_engine_url: Optional[str] = None
|
| 263 |
+
default_embedding_option: Optional[str] = None
|
| 264 |
+
auto_delete_files: Optional[str] = None
|
| 265 |
+
youtube_preferred_languages: Optional[List[str]] = None
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class SettingsUpdate(BaseModel):
|
| 269 |
+
default_content_processing_engine_doc: Optional[str] = None
|
| 270 |
+
default_content_processing_engine_url: Optional[str] = None
|
| 271 |
+
default_embedding_option: Optional[str] = None
|
| 272 |
+
auto_delete_files: Optional[str] = None
|
| 273 |
+
youtube_preferred_languages: Optional[List[str]] = None
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# Sources API models
|
| 277 |
+
class AssetModel(BaseModel):
|
| 278 |
+
file_path: Optional[str] = None
|
| 279 |
+
url: Optional[str] = None
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
class SourceCreate(BaseModel):
|
| 283 |
+
# Backward compatibility: support old single notebook_id
|
| 284 |
+
notebook_id: Optional[str] = Field(
|
| 285 |
+
None, description="Notebook ID to add the source to (deprecated, use notebooks)"
|
| 286 |
+
)
|
| 287 |
+
# New multi-notebook support
|
| 288 |
+
notebooks: Optional[List[str]] = Field(
|
| 289 |
+
None, description="List of notebook IDs to add the source to"
|
| 290 |
+
)
|
| 291 |
+
# Required fields
|
| 292 |
+
type: str = Field(..., description="Source type: link, upload, or text")
|
| 293 |
+
url: Optional[str] = Field(None, description="URL for link type")
|
| 294 |
+
file_path: Optional[str] = Field(None, description="File path for upload type")
|
| 295 |
+
content: Optional[str] = Field(None, description="Text content for text type")
|
| 296 |
+
title: Optional[str] = Field(None, description="Source title")
|
| 297 |
+
transformations: Optional[List[str]] = Field(
|
| 298 |
+
default_factory=list, description="Transformation IDs to apply"
|
| 299 |
+
)
|
| 300 |
+
embed: bool = Field(False, description="Whether to embed content for vector search")
|
| 301 |
+
delete_source: bool = Field(
|
| 302 |
+
False, description="Whether to delete uploaded file after processing"
|
| 303 |
+
)
|
| 304 |
+
# New async processing support
|
| 305 |
+
async_processing: bool = Field(
|
| 306 |
+
False, description="Whether to process source asynchronously"
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
@model_validator(mode="after")
|
| 310 |
+
def validate_notebook_fields(self):
|
| 311 |
+
# Ensure only one of notebook_id or notebooks is provided
|
| 312 |
+
if self.notebook_id is not None and self.notebooks is not None:
|
| 313 |
+
raise ValueError(
|
| 314 |
+
"Cannot specify both 'notebook_id' and 'notebooks'. Use 'notebooks' for multi-notebook support."
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
# Convert single notebook_id to notebooks array for internal processing
|
| 318 |
+
if self.notebook_id is not None:
|
| 319 |
+
self.notebooks = [self.notebook_id]
|
| 320 |
+
# Keep notebook_id for backward compatibility in response
|
| 321 |
+
|
| 322 |
+
# Set empty array if no notebooks specified (allow sources without notebooks)
|
| 323 |
+
if self.notebooks is None:
|
| 324 |
+
self.notebooks = []
|
| 325 |
+
|
| 326 |
+
return self
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
class SourceUpdate(BaseModel):
|
| 330 |
+
title: Optional[str] = Field(None, description="Source title")
|
| 331 |
+
topics: Optional[List[str]] = Field(None, description="Source topics")
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
class SourceResponse(BaseModel):
|
| 335 |
+
id: str
|
| 336 |
+
title: Optional[str]
|
| 337 |
+
topics: Optional[List[str]]
|
| 338 |
+
asset: Optional[AssetModel]
|
| 339 |
+
full_text: Optional[str]
|
| 340 |
+
embedded: bool
|
| 341 |
+
embedded_chunks: int
|
| 342 |
+
file_available: Optional[bool] = None
|
| 343 |
+
created: str
|
| 344 |
+
updated: str
|
| 345 |
+
# New fields for async processing
|
| 346 |
+
command_id: Optional[str] = None
|
| 347 |
+
status: Optional[str] = None
|
| 348 |
+
processing_info: Optional[Dict] = None
|
| 349 |
+
# Notebook associations
|
| 350 |
+
notebooks: Optional[List[str]] = None
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
class SourceListResponse(BaseModel):
|
| 354 |
+
id: str
|
| 355 |
+
title: Optional[str]
|
| 356 |
+
topics: Optional[List[str]]
|
| 357 |
+
asset: Optional[AssetModel]
|
| 358 |
+
embedded: bool # Boolean flag indicating if source has embeddings
|
| 359 |
+
embedded_chunks: int # Number of embedded chunks
|
| 360 |
+
insights_count: int
|
| 361 |
+
created: str
|
| 362 |
+
updated: str
|
| 363 |
+
file_available: Optional[bool] = None
|
| 364 |
+
# Status fields for async processing
|
| 365 |
+
command_id: Optional[str] = None
|
| 366 |
+
status: Optional[str] = None
|
| 367 |
+
processing_info: Optional[Dict[str, Any]] = None
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# Context API models
|
| 371 |
+
class ContextConfig(BaseModel):
|
| 372 |
+
sources: Dict[str, str] = Field(
|
| 373 |
+
default_factory=dict, description="Source inclusion config {source_id: level}"
|
| 374 |
+
)
|
| 375 |
+
notes: Dict[str, str] = Field(
|
| 376 |
+
default_factory=dict, description="Note inclusion config {note_id: level}"
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
class ContextRequest(BaseModel):
|
| 381 |
+
notebook_id: str = Field(..., description="Notebook ID to get context for")
|
| 382 |
+
context_config: Optional[ContextConfig] = Field(
|
| 383 |
+
None, description="Context configuration"
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
class ContextResponse(BaseModel):
|
| 388 |
+
notebook_id: str
|
| 389 |
+
sources: List[Dict[str, Any]] = Field(..., description="Source context data")
|
| 390 |
+
notes: List[Dict[str, Any]] = Field(..., description="Note context data")
|
| 391 |
+
total_tokens: Optional[int] = Field(None, description="Estimated token count")
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
# Insights API models
|
| 395 |
+
class SourceInsightResponse(BaseModel):
|
| 396 |
+
id: str
|
| 397 |
+
source_id: str
|
| 398 |
+
insight_type: str
|
| 399 |
+
content: str
|
| 400 |
+
created: str
|
| 401 |
+
updated: str
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
class SaveAsNoteRequest(BaseModel):
|
| 405 |
+
notebook_id: Optional[str] = Field(None, description="Notebook ID to add note to")
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
class CreateSourceInsightRequest(BaseModel):
|
| 409 |
+
model_config = ConfigDict(protected_namespaces=())
|
| 410 |
+
|
| 411 |
+
transformation_id: str = Field(..., description="ID of transformation to apply")
|
| 412 |
+
model_id: Optional[str] = Field(
|
| 413 |
+
None, description="Model ID (uses default if not provided)"
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# Source status response
|
| 418 |
+
class SourceStatusResponse(BaseModel):
|
| 419 |
+
status: Optional[str] = Field(None, description="Processing status")
|
| 420 |
+
message: str = Field(..., description="Descriptive message about the status")
|
| 421 |
+
processing_info: Optional[Dict[str, Any]] = Field(
|
| 422 |
+
None, description="Detailed processing information"
|
| 423 |
+
)
|
| 424 |
+
command_id: Optional[str] = Field(None, description="Command ID if available")
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
# Error response
|
| 428 |
+
class ErrorResponse(BaseModel):
|
| 429 |
+
error: str
|
| 430 |
+
message: str
|
api/models_service.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Models service layer using API.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
from loguru import logger
|
| 8 |
+
|
| 9 |
+
from api.client import api_client
|
| 10 |
+
from open_notebook.domain.models import DefaultModels, Model
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ModelsService:
|
| 14 |
+
"""Service layer for models operations using API."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
logger.info("Using API for models operations")
|
| 18 |
+
|
| 19 |
+
def get_all_models(self, model_type: Optional[str] = None) -> List[Model]:
|
| 20 |
+
"""Get all models with optional type filtering."""
|
| 21 |
+
models_data = api_client.get_models(model_type=model_type)
|
| 22 |
+
# Convert API response to Model objects
|
| 23 |
+
models = []
|
| 24 |
+
for model_data in models_data:
|
| 25 |
+
model = Model(
|
| 26 |
+
name=model_data["name"],
|
| 27 |
+
provider=model_data["provider"],
|
| 28 |
+
type=model_data["type"],
|
| 29 |
+
)
|
| 30 |
+
model.id = model_data["id"]
|
| 31 |
+
model.created = model_data["created"]
|
| 32 |
+
model.updated = model_data["updated"]
|
| 33 |
+
models.append(model)
|
| 34 |
+
return models
|
| 35 |
+
|
| 36 |
+
def create_model(self, name: str, provider: str, model_type: str) -> Model:
|
| 37 |
+
"""Create a new model."""
|
| 38 |
+
response = api_client.create_model(name, provider, model_type)
|
| 39 |
+
model_data = response if isinstance(response, dict) else response[0]
|
| 40 |
+
model = Model(
|
| 41 |
+
name=model_data["name"],
|
| 42 |
+
provider=model_data["provider"],
|
| 43 |
+
type=model_data["type"],
|
| 44 |
+
)
|
| 45 |
+
model.id = model_data["id"]
|
| 46 |
+
model.created = model_data["created"]
|
| 47 |
+
model.updated = model_data["updated"]
|
| 48 |
+
return model
|
| 49 |
+
|
| 50 |
+
def delete_model(self, model_id: str) -> bool:
|
| 51 |
+
"""Delete a model."""
|
| 52 |
+
api_client.delete_model(model_id)
|
| 53 |
+
return True
|
| 54 |
+
|
| 55 |
+
def get_default_models(self) -> DefaultModels:
|
| 56 |
+
"""Get default model assignments."""
|
| 57 |
+
response = api_client.get_default_models()
|
| 58 |
+
defaults_data = response if isinstance(response, dict) else response[0]
|
| 59 |
+
defaults = DefaultModels()
|
| 60 |
+
|
| 61 |
+
# Set the values from API response
|
| 62 |
+
defaults.default_chat_model = defaults_data.get("default_chat_model")
|
| 63 |
+
defaults.default_transformation_model = defaults_data.get("default_transformation_model")
|
| 64 |
+
defaults.large_context_model = defaults_data.get("large_context_model")
|
| 65 |
+
defaults.default_text_to_speech_model = defaults_data.get("default_text_to_speech_model")
|
| 66 |
+
defaults.default_speech_to_text_model = defaults_data.get("default_speech_to_text_model")
|
| 67 |
+
defaults.default_embedding_model = defaults_data.get("default_embedding_model")
|
| 68 |
+
defaults.default_tools_model = defaults_data.get("default_tools_model")
|
| 69 |
+
|
| 70 |
+
return defaults
|
| 71 |
+
|
| 72 |
+
def update_default_models(self, defaults: DefaultModels) -> DefaultModels:
|
| 73 |
+
"""Update default model assignments."""
|
| 74 |
+
updates = {
|
| 75 |
+
"default_chat_model": defaults.default_chat_model,
|
| 76 |
+
"default_transformation_model": defaults.default_transformation_model,
|
| 77 |
+
"large_context_model": defaults.large_context_model,
|
| 78 |
+
"default_text_to_speech_model": defaults.default_text_to_speech_model,
|
| 79 |
+
"default_speech_to_text_model": defaults.default_speech_to_text_model,
|
| 80 |
+
"default_embedding_model": defaults.default_embedding_model,
|
| 81 |
+
"default_tools_model": defaults.default_tools_model,
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
response = api_client.update_default_models(**updates)
|
| 85 |
+
defaults_data = response if isinstance(response, dict) else response[0]
|
| 86 |
+
|
| 87 |
+
# Update the defaults object with the response
|
| 88 |
+
defaults.default_chat_model = defaults_data.get("default_chat_model")
|
| 89 |
+
defaults.default_transformation_model = defaults_data.get("default_transformation_model")
|
| 90 |
+
defaults.large_context_model = defaults_data.get("large_context_model")
|
| 91 |
+
defaults.default_text_to_speech_model = defaults_data.get("default_text_to_speech_model")
|
| 92 |
+
defaults.default_speech_to_text_model = defaults_data.get("default_speech_to_text_model")
|
| 93 |
+
defaults.default_embedding_model = defaults_data.get("default_embedding_model")
|
| 94 |
+
defaults.default_tools_model = defaults_data.get("default_tools_model")
|
| 95 |
+
|
| 96 |
+
return defaults
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# Global service instance
|
| 100 |
+
models_service = ModelsService()
|
api/notebook_service.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Notebook service layer using API.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
from loguru import logger
|
| 8 |
+
|
| 9 |
+
from api.client import api_client
|
| 10 |
+
from open_notebook.domain.notebook import Notebook
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class NotebookService:
|
| 14 |
+
"""Service layer for notebook operations using API."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
logger.info("Using API for notebook operations")
|
| 18 |
+
|
| 19 |
+
def get_all_notebooks(self, order_by: str = "updated desc") -> List[Notebook]:
|
| 20 |
+
"""Get all notebooks."""
|
| 21 |
+
notebooks_data = api_client.get_notebooks(order_by=order_by)
|
| 22 |
+
# Convert API response to Notebook objects
|
| 23 |
+
notebooks = []
|
| 24 |
+
for nb_data in notebooks_data:
|
| 25 |
+
nb = Notebook(
|
| 26 |
+
name=nb_data["name"],
|
| 27 |
+
description=nb_data["description"],
|
| 28 |
+
archived=nb_data["archived"],
|
| 29 |
+
)
|
| 30 |
+
nb.id = nb_data["id"]
|
| 31 |
+
nb.created = nb_data["created"]
|
| 32 |
+
nb.updated = nb_data["updated"]
|
| 33 |
+
notebooks.append(nb)
|
| 34 |
+
return notebooks
|
| 35 |
+
|
| 36 |
+
def get_notebook(self, notebook_id: str) -> Optional[Notebook]:
|
| 37 |
+
"""Get a specific notebook."""
|
| 38 |
+
response = api_client.get_notebook(notebook_id)
|
| 39 |
+
nb_data = response if isinstance(response, dict) else response[0]
|
| 40 |
+
nb = Notebook(
|
| 41 |
+
name=nb_data["name"],
|
| 42 |
+
description=nb_data["description"],
|
| 43 |
+
archived=nb_data["archived"],
|
| 44 |
+
)
|
| 45 |
+
nb.id = nb_data["id"]
|
| 46 |
+
nb.created = nb_data["created"]
|
| 47 |
+
nb.updated = nb_data["updated"]
|
| 48 |
+
return nb
|
| 49 |
+
|
| 50 |
+
def create_notebook(self, name: str, description: str = "") -> Notebook:
|
| 51 |
+
"""Create a new notebook."""
|
| 52 |
+
response = api_client.create_notebook(name, description)
|
| 53 |
+
nb_data = response if isinstance(response, dict) else response[0]
|
| 54 |
+
nb = Notebook(
|
| 55 |
+
name=nb_data["name"],
|
| 56 |
+
description=nb_data["description"],
|
| 57 |
+
archived=nb_data["archived"],
|
| 58 |
+
)
|
| 59 |
+
nb.id = nb_data["id"]
|
| 60 |
+
nb.created = nb_data["created"]
|
| 61 |
+
nb.updated = nb_data["updated"]
|
| 62 |
+
return nb
|
| 63 |
+
|
| 64 |
+
def update_notebook(self, notebook: Notebook) -> Notebook:
|
| 65 |
+
"""Update a notebook."""
|
| 66 |
+
updates = {
|
| 67 |
+
"name": notebook.name,
|
| 68 |
+
"description": notebook.description,
|
| 69 |
+
"archived": notebook.archived,
|
| 70 |
+
}
|
| 71 |
+
response = api_client.update_notebook(notebook.id or "", **updates)
|
| 72 |
+
nb_data = response if isinstance(response, dict) else response[0]
|
| 73 |
+
# Update the notebook object with the response
|
| 74 |
+
notebook.name = nb_data["name"]
|
| 75 |
+
notebook.description = nb_data["description"]
|
| 76 |
+
notebook.archived = nb_data["archived"]
|
| 77 |
+
notebook.updated = nb_data["updated"]
|
| 78 |
+
return notebook
|
| 79 |
+
|
| 80 |
+
def delete_notebook(self, notebook: Notebook) -> bool:
|
| 81 |
+
"""Delete a notebook."""
|
| 82 |
+
api_client.delete_notebook(notebook.id or "")
|
| 83 |
+
return True
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# Global service instance
|
| 87 |
+
notebook_service = NotebookService()
|
api/notes_service.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Notes service layer using API.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
from loguru import logger
|
| 8 |
+
|
| 9 |
+
from api.client import api_client
|
| 10 |
+
from open_notebook.domain.notebook import Note
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class NotesService:
|
| 14 |
+
"""Service layer for notes operations using API."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
logger.info("Using API for notes operations")
|
| 18 |
+
|
| 19 |
+
def get_all_notes(self, notebook_id: Optional[str] = None) -> List[Note]:
|
| 20 |
+
"""Get all notes with optional notebook filtering."""
|
| 21 |
+
notes_data = api_client.get_notes(notebook_id=notebook_id)
|
| 22 |
+
# Convert API response to Note objects
|
| 23 |
+
notes = []
|
| 24 |
+
for note_data in notes_data:
|
| 25 |
+
note = Note(
|
| 26 |
+
title=note_data["title"],
|
| 27 |
+
content=note_data["content"],
|
| 28 |
+
note_type=note_data["note_type"],
|
| 29 |
+
)
|
| 30 |
+
note.id = note_data["id"]
|
| 31 |
+
note.created = note_data["created"]
|
| 32 |
+
note.updated = note_data["updated"]
|
| 33 |
+
notes.append(note)
|
| 34 |
+
return notes
|
| 35 |
+
|
| 36 |
+
def get_note(self, note_id: str) -> Note:
|
| 37 |
+
"""Get a specific note."""
|
| 38 |
+
note_response = api_client.get_note(note_id)
|
| 39 |
+
note_data = note_response if isinstance(note_response, dict) else note_response[0]
|
| 40 |
+
note = Note(
|
| 41 |
+
title=note_data["title"],
|
| 42 |
+
content=note_data["content"],
|
| 43 |
+
note_type=note_data["note_type"],
|
| 44 |
+
)
|
| 45 |
+
note.id = note_data["id"]
|
| 46 |
+
note.created = note_data["created"]
|
| 47 |
+
note.updated = note_data["updated"]
|
| 48 |
+
return note
|
| 49 |
+
|
| 50 |
+
def create_note(
|
| 51 |
+
self,
|
| 52 |
+
content: str,
|
| 53 |
+
title: Optional[str] = None,
|
| 54 |
+
note_type: str = "human",
|
| 55 |
+
notebook_id: Optional[str] = None
|
| 56 |
+
) -> Note:
|
| 57 |
+
"""Create a new note."""
|
| 58 |
+
note_response = api_client.create_note(
|
| 59 |
+
content=content,
|
| 60 |
+
title=title,
|
| 61 |
+
note_type=note_type,
|
| 62 |
+
notebook_id=notebook_id
|
| 63 |
+
)
|
| 64 |
+
note_data = note_response if isinstance(note_response, dict) else note_response[0]
|
| 65 |
+
note = Note(
|
| 66 |
+
title=note_data["title"],
|
| 67 |
+
content=note_data["content"],
|
| 68 |
+
note_type=note_data["note_type"],
|
| 69 |
+
)
|
| 70 |
+
note.id = note_data["id"]
|
| 71 |
+
note.created = note_data["created"]
|
| 72 |
+
note.updated = note_data["updated"]
|
| 73 |
+
return note
|
| 74 |
+
|
| 75 |
+
def update_note(self, note: Note) -> Note:
|
| 76 |
+
"""Update a note."""
|
| 77 |
+
updates = {
|
| 78 |
+
"title": note.title,
|
| 79 |
+
"content": note.content,
|
| 80 |
+
"note_type": note.note_type,
|
| 81 |
+
}
|
| 82 |
+
note_response = api_client.update_note(note.id or "", **updates)
|
| 83 |
+
note_data = note_response if isinstance(note_response, dict) else note_response[0]
|
| 84 |
+
|
| 85 |
+
# Update the note object with the response
|
| 86 |
+
note.title = note_data["title"]
|
| 87 |
+
note.content = note_data["content"]
|
| 88 |
+
note.note_type = note_data["note_type"]
|
| 89 |
+
note.updated = note_data["updated"]
|
| 90 |
+
|
| 91 |
+
return note
|
| 92 |
+
|
| 93 |
+
def delete_note(self, note_id: str) -> bool:
|
| 94 |
+
"""Delete a note."""
|
| 95 |
+
api_client.delete_note(note_id)
|
| 96 |
+
return True
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# Global service instance
|
| 100 |
+
notes_service = NotesService()
|
api/podcast_api_service.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Podcast service layer using API client.
|
| 3 |
+
This replaces direct httpx calls in the Streamlit pages.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Any, Dict, List
|
| 7 |
+
|
| 8 |
+
from loguru import logger
|
| 9 |
+
|
| 10 |
+
from api.client import api_client
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class PodcastAPIService:
|
| 14 |
+
"""Service layer for podcast operations using API client."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
logger.info("Using API client for podcast operations")
|
| 18 |
+
|
| 19 |
+
# Episode methods
|
| 20 |
+
def get_episodes(self) -> List[Dict[Any, Any]]:
|
| 21 |
+
"""Get all podcast episodes."""
|
| 22 |
+
result = api_client._make_request("GET", "/api/podcasts/episodes")
|
| 23 |
+
return result if isinstance(result, list) else [result]
|
| 24 |
+
|
| 25 |
+
def delete_episode(self, episode_id: str) -> bool:
|
| 26 |
+
"""Delete a podcast episode."""
|
| 27 |
+
try:
|
| 28 |
+
api_client._make_request("DELETE", f"/api/podcasts/episodes/{episode_id}")
|
| 29 |
+
return True
|
| 30 |
+
except Exception as e:
|
| 31 |
+
logger.error(f"Failed to delete episode: {e}")
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
# Episode Profile methods
|
| 35 |
+
def get_episode_profiles(self) -> List[Dict]:
|
| 36 |
+
"""Get all episode profiles."""
|
| 37 |
+
return api_client.get_episode_profiles()
|
| 38 |
+
|
| 39 |
+
def create_episode_profile(self, profile_data: Dict) -> bool:
|
| 40 |
+
"""Create a new episode profile."""
|
| 41 |
+
try:
|
| 42 |
+
api_client.create_episode_profile(**profile_data)
|
| 43 |
+
return True
|
| 44 |
+
except Exception as e:
|
| 45 |
+
logger.error(f"Failed to create episode profile: {e}")
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
def update_episode_profile(self, profile_id: str, profile_data: Dict) -> bool:
|
| 49 |
+
"""Update an episode profile."""
|
| 50 |
+
try:
|
| 51 |
+
api_client.update_episode_profile(profile_id, **profile_data)
|
| 52 |
+
return True
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logger.error(f"Failed to update episode profile: {e}")
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
def delete_episode_profile(self, profile_id: str) -> bool:
|
| 58 |
+
"""Delete an episode profile."""
|
| 59 |
+
try:
|
| 60 |
+
api_client.delete_episode_profile(profile_id)
|
| 61 |
+
return True
|
| 62 |
+
except Exception as e:
|
| 63 |
+
logger.error(f"Failed to delete episode profile: {e}")
|
| 64 |
+
return False
|
| 65 |
+
|
| 66 |
+
def duplicate_episode_profile(self, profile_id: str) -> bool:
|
| 67 |
+
"""Duplicate an episode profile."""
|
| 68 |
+
try:
|
| 69 |
+
api_client._make_request(
|
| 70 |
+
"POST", f"/api/episode-profiles/{profile_id}/duplicate"
|
| 71 |
+
)
|
| 72 |
+
return True
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"Failed to duplicate episode profile: {e}")
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
# Speaker Profile methods
|
| 78 |
+
def get_speaker_profiles(self) -> List[Dict[Any, Any]]:
|
| 79 |
+
"""Get all speaker profiles."""
|
| 80 |
+
result = api_client._make_request("GET", "/api/speaker-profiles")
|
| 81 |
+
return result if isinstance(result, list) else [result]
|
| 82 |
+
|
| 83 |
+
def create_speaker_profile(self, profile_data: Dict) -> bool:
|
| 84 |
+
"""Create a new speaker profile."""
|
| 85 |
+
try:
|
| 86 |
+
api_client._make_request("POST", "/api/speaker-profiles", json=profile_data)
|
| 87 |
+
return True
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Failed to create speaker profile: {e}")
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
def update_speaker_profile(self, profile_id: str, profile_data: Dict) -> bool:
|
| 93 |
+
"""Update a speaker profile."""
|
| 94 |
+
try:
|
| 95 |
+
api_client._make_request(
|
| 96 |
+
"PUT", f"/api/speaker-profiles/{profile_id}", json=profile_data
|
| 97 |
+
)
|
| 98 |
+
return True
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.error(f"Failed to update speaker profile: {e}")
|
| 101 |
+
return False
|
| 102 |
+
|
| 103 |
+
def delete_speaker_profile(self, profile_id: str) -> bool:
|
| 104 |
+
"""Delete a speaker profile."""
|
| 105 |
+
try:
|
| 106 |
+
api_client._make_request("DELETE", f"/api/speaker-profiles/{profile_id}")
|
| 107 |
+
return True
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.error(f"Failed to delete speaker profile: {e}")
|
| 110 |
+
return False
|
| 111 |
+
|
| 112 |
+
def duplicate_speaker_profile(self, profile_id: str) -> bool:
|
| 113 |
+
"""Duplicate a speaker profile."""
|
| 114 |
+
try:
|
| 115 |
+
api_client._make_request(
|
| 116 |
+
"POST", f"/api/speaker-profiles/{profile_id}/duplicate"
|
| 117 |
+
)
|
| 118 |
+
return True
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logger.error(f"Failed to duplicate speaker profile: {e}")
|
| 121 |
+
return False
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# Global service instance
|
| 125 |
+
podcast_api_service = PodcastAPIService()
|