diff --git a/.env.example b/.env.example index dd809b5..4dff16a 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,39 @@ # Only set this if you need to override the auto-detection (e.g., reverse proxy scenarios). API_URL=http://localhost:5055 +# API CLIENT TIMEOUT (in seconds) +# Controls how long the frontend/Streamlit UI waits for API responses +# Increase this if you're using slow AI providers or hardware (Ollama on CPU, remote LM Studio, etc.) +# Default: 300 seconds (5 minutes) - sufficient for most transformation/insight operations +# +# Common scenarios: +# - Fast cloud APIs (OpenAI, Anthropic): 300 seconds is more than enough +# - Local Ollama on GPU: 300 seconds should work fine +# - Local Ollama on CPU: Consider 600 seconds (10 minutes) or more +# - Remote LM Studio over slow network: Consider 900 seconds (15 minutes) +# - Very large documents: May need 900+ seconds +# +# API_CLIENT_TIMEOUT=300 + +# ESPERANTO LLM TIMEOUT (in seconds) +# Controls the timeout for AI model API calls at the Esperanto library level +# This is separate from API_CLIENT_TIMEOUT and applies to the actual LLM provider requests +# Only increase this if you're experiencing timeouts during model inference itself +# Default: 60 seconds (built into Esperanto) +# +# Important: This should generally be LOWER than API_CLIENT_TIMEOUT to allow proper error handling +# +# Common scenarios: +# - Fast cloud APIs (OpenAI, Anthropic, Groq): 60 seconds is sufficient +# - Local Ollama with small models: 120-180 seconds may help +# - Local Ollama with large models on CPU: 300+ seconds +# - Remote or self-hosted LLMs: 180-300 seconds depending on hardware +# +# Note: If transformations complete but you see timeout errors, increase API_CLIENT_TIMEOUT first. +# Only increase ESPERANTO_LLM_TIMEOUT if the model itself is timing out during inference. +# +# ESPERANTO_LLM_TIMEOUT=60 + # SECURITY # Set this to protect your Open Notebook instance with a password (for public hosting) # OPEN_NOTEBOOK_PASSWORD= diff --git a/api/client.py b/api/client.py index d628b25..016d31c 100644 --- a/api/client.py +++ b/api/client.py @@ -15,7 +15,24 @@ class APIClient: def __init__(self, base_url: Optional[str] = None): self.base_url = base_url or os.getenv("API_BASE_URL", "http://127.0.0.1:5055") - self.timeout = 30.0 + # Timeout increased to 5 minutes (300s) to accommodate slow LLM operations + # (transformations, insights) on slower hardware (Ollama, LM Studio, remote APIs) + # Configurable via API_CLIENT_TIMEOUT environment variable (in seconds) + timeout_str = os.getenv("API_CLIENT_TIMEOUT", "300.0") + try: + timeout_value = float(timeout_str) + # Validate timeout is within reasonable bounds (30s - 3600s / 1 hour) + if timeout_value < 30: + logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too low, using minimum of 30s") + timeout_value = 30.0 + elif timeout_value > 3600: + logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too high, using maximum of 3600s") + timeout_value = 3600.0 + self.timeout = timeout_value + except ValueError: + logger.error(f"Invalid API_CLIENT_TIMEOUT value '{timeout_str}', using default 300s") + self.timeout = 300.0 + # Add authentication header if password is set self.headers = {} password = os.getenv("OPEN_NOTEBOOK_PASSWORD") @@ -117,9 +134,9 @@ class APIClient: "answer_model": answer_model, "final_answer_model": final_answer_model, } - # Use 5 minute timeout for long-running ask operations + # Use configured timeout for long-running ask operations return self._make_request( - "POST", "/api/search/ask/simple", json=data, timeout=300.0 + "POST", "/api/search/ask/simple", json=data, timeout=self.timeout ) # Models API methods @@ -199,9 +216,9 @@ class APIClient: "input_text": input_text, "model_id": model_id, } - # Use extended timeout for transformation operations + # Use configured timeout for transformation operations return self._make_request( - "POST", "/api/transformations/execute", json=data, timeout=120.0 + "POST", "/api/transformations/execute", json=data, timeout=self.timeout ) # Notes API methods @@ -251,8 +268,8 @@ class APIClient: "item_type": item_type, "async_processing": async_processing, } - # Use extended timeout for embedding operations - return self._make_request("POST", "/api/embed", json=data, timeout=120.0) + # Use configured timeout for embedding operations + return self._make_request("POST", "/api/embed", json=data, timeout=self.timeout) def rebuild_embeddings( self, @@ -261,15 +278,20 @@ class APIClient: include_notes: bool = True, include_insights: bool = True ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: - """Rebuild embeddings in bulk.""" + """Rebuild embeddings in bulk. + + Note: This operation can take a long time for large databases. + Consider increasing API_CLIENT_TIMEOUT to 600-900s for bulk rebuilds. + """ data = { "mode": mode, "include_sources": include_sources, "include_notes": include_notes, "include_insights": include_insights, } - # Use extended timeout for rebuild operations (up to 10 minutes) - return self._make_request("POST", "/api/embeddings/rebuild", json=data, timeout=600.0) + # Use double the configured timeout for bulk rebuild operations (or configured value if already high) + rebuild_timeout = max(self.timeout, min(self.timeout * 2, 3600.0)) + return self._make_request("POST", "/api/embeddings/rebuild", json=data, timeout=rebuild_timeout) def get_rebuild_status(self, command_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get status of a rebuild operation.""" @@ -347,8 +369,8 @@ class APIClient: if transformations: data["transformations"] = transformations - # Use 5 minute timeout for source creation (especially PDF processing with OCR) - return self._make_request("POST", "/api/sources/json", json=data, timeout=300.0) + # Use configured timeout for source creation (especially PDF processing with OCR) + return self._make_request("POST", "/api/sources/json", json=data, timeout=self.timeout) def get_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific source.""" diff --git a/docs/troubleshooting/common-issues.md b/docs/troubleshooting/common-issues.md index 2195233..82deaa2 100644 --- a/docs/troubleshooting/common-issues.md +++ b/docs/troubleshooting/common-issues.md @@ -195,6 +195,72 @@ This document covers the most frequently encountered issues when installing, con - Use lower-tier models for testing - Check provider rate limits +### API Timeout Errors During Transformations + +**Problem**: Timeout errors when running transformations or generating insights, even though the operation completes successfully. + +**Symptoms**: +- "timeout of 30000ms exceeded" in React frontend +- "Failed to connect to API: timed out" in Streamlit UI +- Transformation completes after a few minutes, but error appears after 30-60 seconds +- Common with local models (Ollama), remote LM Studio, or slow hardware + +**Solutions**: + +1. **Increase API client timeout** (recommended): + ```bash + # Add to your .env file + API_CLIENT_TIMEOUT=600 # 10 minutes (600 seconds) + ``` + + This controls how long the frontend/UI waits for API responses. Default is 300 seconds (5 minutes). + +2. **Adjust timeout based on your setup**: + ```bash + # Fast cloud APIs (OpenAI, Anthropic, Groq) + API_CLIENT_TIMEOUT=300 # 5 minutes (default) + + # Local Ollama on GPU + API_CLIENT_TIMEOUT=600 # 10 minutes + + # Local Ollama on CPU or slow hardware + API_CLIENT_TIMEOUT=1200 # 20 minutes + + # Remote LM Studio over slow network + API_CLIENT_TIMEOUT=900 # 15 minutes + ``` + +3. **Increase LLM provider timeout if needed**: + ```bash + # Add to your .env file if the model itself is timing out + ESPERANTO_LLM_TIMEOUT=180 # 3 minutes (default is 60s) + ``` + + Only increase this if you see errors during actual model inference, not just HTTP timeouts. + +4. **Use faster models for testing**: + - Test with cloud APIs first to verify setup + - Try smaller local models (e.g., `gemma2:2b` instead of `llama3:70b`) + - Preload models before running transformations: `ollama run model-name` + +5. **Restart services after configuration changes**: + ```bash + # For Docker + docker compose down + docker compose up -d + + # For source installation + make stop-all + make start-all + ``` + +**Important Notes**: +- `API_CLIENT_TIMEOUT` should be HIGHER than `ESPERANTO_LLM_TIMEOUT` for proper error handling +- If transformations complete successfully after refresh, you only need to increase `API_CLIENT_TIMEOUT` +- First time running a model may be slower due to model loading + +**Related GitHub Issue**: [#131](https://github.com/lfnovo/open-notebook/issues/131) + ### Memory and Performance Issues **Problem**: Application running slowly or crashing due to memory issues. diff --git a/docs/troubleshooting/faq.md b/docs/troubleshooting/faq.md index 04bde6e..1a7ae80 100644 --- a/docs/troubleshooting/faq.md +++ b/docs/troubleshooting/faq.md @@ -338,6 +338,27 @@ tar -xzf backup-20240101.tar.gz ## Troubleshooting +### Why do I get timeout errors even though transformations complete successfully? + +**Cause**: The default client timeout (5 minutes) may be too short for slow AI providers or hardware. + +**Quick fix**: +```bash +# Add to your .env file +API_CLIENT_TIMEOUT=600 # 10 minutes for slow hardware +``` + +**When this happens**: +- Using local Ollama models on CPU +- Using remote LM Studio over slow network +- First transformation after starting (model loading) +- Very large documents +- Slower hardware configurations + +**Detailed solutions**: See [Common Issues - API Timeout Errors](./common-issues.md#api-timeout-errors-during-transformations) + +**Note**: If transformations complete after you refresh the page, you only need to increase `API_CLIENT_TIMEOUT`, not `ESPERANTO_LLM_TIMEOUT`. + ### My question isn't answered here. What should I do? 1. **Check the troubleshooting guide**: [Common Issues](./common-issues.md) diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 65fbb07..8599b74 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -3,8 +3,12 @@ import { getApiUrl } from '@/lib/config' // API client with runtime-configurable base URL // The base URL is fetched from the API config endpoint on first request +// Timeout increased to 5 minutes (300000ms = 300s) to accommodate slow LLM operations +// (transformations, insights generation) especially on slower hardware (Ollama, LM Studio) +// Note: Frontend uses milliseconds (300000ms), backend uses seconds (300s) - both equal 5 minutes +// To configure: Set API_CLIENT_TIMEOUT=600 in .env for 10 minutes (600s = 600000ms) export const apiClient = axios.create({ - timeout: 30000, + timeout: 300000, // 300 seconds = 5 minutes headers: { 'Content-Type': 'application/json', }, diff --git a/pyproject.toml b/pyproject.toml index 2da49c5..3c94dd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "open-notebook" -version = "1.0.5" +version = "1.0.6" description = "An open source implementation of a research assistant, inspired by Google Notebook LM" authors = [ {name = "Luis Novo", email = "lfnovo@gmail.com"} @@ -55,6 +55,7 @@ dev = [ "types-requests>=2.32.0.20241016", "ipywidgets>=8.1.5", "pre-commit>=4.0.1", + "pytest>=8.0.0", ] [build-system] diff --git a/uv.lock b/uv.lock index f091184..e2b8de5 100644 --- a/uv.lock +++ b/uv.lock @@ -1232,6 +1232,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "ipykernel" version = "7.0.1" @@ -2199,7 +2208,7 @@ wheels = [ [[package]] name = "open-notebook" -version = "1.0.5" +version = "1.0.6" source = { editable = "." } dependencies = [ { name = "ai-prompter" }, @@ -2238,6 +2247,7 @@ dev = [ { name = "ipywidgets" }, { name = "mypy" }, { name = "pre-commit" }, + { name = "pytest" }, { name = "ruff" }, { name = "types-requests" }, ] @@ -2276,6 +2286,7 @@ requires-dist = [ { name = "podcast-creator", specifier = ">=0.7.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.1" }, { name = "pydantic", specifier = ">=2.9.2" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.5" }, { name = "surreal-commands", specifier = ">=1.0.13" }, @@ -2589,6 +2600,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "podcast-creator" version = "0.7.0" @@ -2936,6 +2956,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"