-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add Render Free Tier Lite Mode deployment and optimizations #352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Deploying to Render (Free Tier) | ||
|
|
||
| This guide covers how to deploy the Gemini Fullstack Agent to Render using the **Free Tier** (512MB RAM, 0.1 CPU). | ||
|
|
||
| Due to the limited resources of the Free Tier, we use a "Lite Mode" configuration that disables heavy local computations (like local RAG embeddings or Ollama inference) and relies entirely on cloud APIs (Google Gemini). | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| 1. A [Render.com](https://render.com) account. | ||
| 2. A Google Gemini API Key. | ||
| 3. Your code pushed to a GitHub or GitLab repository connected to Render. | ||
|
|
||
| ## Why Manual Web Service Deployment? | ||
|
|
||
| Render occasionally attempts to charge or block deployments initialized via `render.yaml` (Blueprints) if they detect you are trying to bypass plan restrictions or simply because Blueprints are sometimes considered a paid feature depending on your account state. | ||
|
|
||
| **Creating a manual Web Service** bypasses this completely and guarantees you can use your Free Tier allowance. | ||
|
|
||
| --- | ||
|
|
||
| ## Deployment Steps | ||
|
|
||
| ### 1. Create a New Web Service | ||
| 1. Log in to your Render dashboard. | ||
| 2. Click **New +** and select **Web Service**. | ||
| 3. Choose **Build and deploy from a Git repository**. | ||
| 4. Connect your GitHub/GitLab repository and select it. | ||
|
|
||
| ### 2. Configure the Service | ||
|
|
||
| Fill out the form with the following details exactly: | ||
|
|
||
| * **Name:** `gemini-fullstack-agent` (or your preferred name) | ||
| * **Region:** (Select the one closest to you) | ||
| * **Branch:** `main` (or the branch you want to deploy) | ||
| * **Root Directory:** *(Leave blank! Do not put `backend` or `frontend` here)* | ||
| * **Runtime:** `Python 3` | ||
| * **Build Command:** | ||
| ```bash | ||
| ./setup_env.sh | ||
| ``` | ||
| * **Start Command:** | ||
| ```bash | ||
| cd backend && export PYTHONPATH=$PYTHONPATH:$(pwd)/src && python -m uvicorn agent.app:app --host 0.0.0.0 --port $PORT | ||
| ``` | ||
| * **Instance Type:** `Free` (512 MB RAM • 0.1 CPU) | ||
|
|
||
| ### 3. Configure Environment Variables | ||
|
|
||
| Scroll down to the **Environment Variables** section and click **Add Environment Variable**. You must add all of the following: | ||
|
|
||
| | Key | Value | Purpose | | ||
| | :--- | :--- | :--- | | ||
| | `RENDER` | `true` | Tells `setup_env.sh` to skip downloading heavy Playwright browsers. | | ||
| | `PYTHON_VERSION` | `3.12.0` | Forces Render to use Python 3.12 (required by `uv` and langgraph). | | ||
| | `NODE_VERSION` | `20.11.0` | Informs the build script which node version to use. | | ||
| | `GEMINI_API_KEY` | `your-gemini-api-key` | Your Google API Key for LLM reasoning. | | ||
| | `GEMMA_PROVIDER` | `google_genai` | Uses the API for Gemma tasks instead of local Ollama. | | ||
| | `RAG_ENABLED` | `false` | (Recommended) Disables local vector embeddings to save RAM. If you need RAG, set to `true` and set `RAG_EMBEDDING_PROVIDER=google_genai`. | | ||
| | `TRUST_PROXY_HEADERS` | `true` | Ensures rate-limiting works correctly behind Render's load balancer. | | ||
|
|
||
| ### 4. Deploy | ||
|
|
||
| Click **Create Web Service**. | ||
|
|
||
| Render will now run the `./setup_env.sh` script. This script is smart enough to detect the Render environment: | ||
| 1. It will download a standalone version of Node.js (since Render's Python runtime doesn't have it). | ||
| 2. It will install `uv` and sync the Python dependencies. | ||
| 3. It will install `pnpm` and build the React frontend for production. | ||
| 4. It will start the FastAPI backend using `uvicorn`, serving the built frontend on the `/app` route and the API on `/`. | ||
|
|
||
| ### 5. Access the App | ||
|
|
||
| Once the deployment is marked as **Live**, click the URL provided by Render (e.g., `https://gemini-fullstack-agent-abc1.onrender.com`). | ||
|
|
||
| Because it's a single-service architecture, navigating to the root URL will automatically redirect you to `/app`, where the frontend interface lives. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| * **Memory Limit Exceeded (OOM):** If the build or runtime fails due to memory limits, ensure `RAG_ENABLED=false` is set. The Free tier (512MB) struggles to load sentence-transformers or FAISS indexes. | ||
| * **Node.js Not Found Error:** If the build fails saying `node` or `pnpm` is not found, ensure you are using the exact `./setup_env.sh` as the build command, as it installs Node on the fly for the Python environment. | ||
| * **Rate Limiting Issues:** If you get "Too Many Requests" errors instantly, ensure `TRUST_PROXY_HEADERS=true` is set so the backend sees real client IPs, not just Render's internal proxy IP. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -110,13 +110,51 @@ def invoke(self, prompt: str, **kwargs) -> str: | |||||
| logger.error(f"Ollama call failed: {e}") | ||||||
| raise | ||||||
|
|
||||||
| class GoogleGenAIGemmaClient(GemmaClient): | ||||||
| """Client for Gemma models via Google GenAI API.""" | ||||||
|
|
||||||
| def __init__(self): | ||||||
| """Initialize Google GenAI client.""" | ||||||
| try: | ||||||
| from google import genai | ||||||
| except ImportError: | ||||||
| logger.error("google-genai not installed.") | ||||||
| raise ImportError("Please install 'google-genai' to use GoogleGenAIGemmaClient") | ||||||
|
|
||||||
| import os | ||||||
| self.api_key = os.getenv("GEMINI_API_KEY") | ||||||
| if not self.api_key: | ||||||
| logger.error("GEMINI_API_KEY environment variable is missing.") | ||||||
| raise ValueError("GEMINI_API_KEY is required for GoogleGenAIGemmaClient") | ||||||
|
|
||||||
| self.client = genai.Client(api_key=self.api_key) | ||||||
|
|
||||||
| # Use provided model or default to gemini-2.5-flash for free tier if trying to use gemma but not specified correctly | ||||||
| self.model_name = app_config.gemma_model_name | ||||||
| if not self.model_name or self.model_name == "gemma:7b": | ||||||
| self.model_name = "gemini-2.5-flash" # Fallback to gemini if gemma:7b (ollama default) is used | ||||||
|
|
||||||
| def invoke(self, prompt: str, **kwargs) -> str: | ||||||
| """Generate text completion.""" | ||||||
| try: | ||||||
| response = self.client.models.generate_content( | ||||||
| model=self.model_name, | ||||||
| contents=prompt, | ||||||
| ) | ||||||
| return response.text | ||||||
| except Exception as e: | ||||||
| logger.error(f"Google GenAI generation failed: {e}") | ||||||
| raise e | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
References
|
||||||
|
|
||||||
| def get_gemma_client() -> GemmaClient: | ||||||
| """Factory function to get the configured Gemma client.""" | ||||||
| provider = (app_config.gemma_provider or "ollama").lower() | ||||||
| if provider == "vertex": | ||||||
| return VertexAIGemmaClient() | ||||||
| elif provider == "ollama": | ||||||
| return OllamaGemmaClient() | ||||||
| elif provider == "google_genai": | ||||||
| return GoogleGenAIGemmaClient() | ||||||
| else: | ||||||
| logger.warning(f"Unknown or unsupported Gemma provider: {provider}. Defaulting to Ollama.") | ||||||
| return OllamaGemmaClient() | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import re | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (complexity): Consider removing the regex-based patch script and implementing the GoogleGenAIGemmaClient and updated factory directly in gemma_client.py instead. The indirection through a regex‑based patch script is unnecessary and brittle; you can keep the exact same functionality by modifying Instead of: # separate patch script
import re
with open('backend/src/agent/gemma_client.py', 'r') as f:
content = f.read()
# ... giant new_class string ...
content = content.replace("def get_gemma_client() -> GemmaClient:", new_class + "def get_gemma_client() -> GemmaClient:")
content = re.sub(r'def get_gemma_client\(\) -> GemmaClient:.*?return OllamaGemmaClient\(\)', factory_replacement, content, flags=re.DOTALL)
with open('backend/src/agent/gemma_client.py', 'w') as f:
f.write(content)add the class and factory directly in class GoogleGenAIGemmaClient(GemmaClient):
"""Client for Gemma models via Google GenAI API."""
def __init__(self):
"""Initialize Google GenAI client."""
try:
from google import genai
except ImportError:
logger.error("google-genai not installed.")
raise ImportError(
"Please install 'google-genai' to use GoogleGenAIGemmaClient"
)
import os
self.api_key = os.getenv("GEMINI_API_KEY")
if not self.api_key:
logger.error("GEMINI_API_KEY environment variable is missing.")
raise ValueError(
"GEMINI_API_KEY is required for GoogleGenAIGemmaClient"
)
self.client = genai.Client(api_key=self.api_key)
self.model_name = app_config.gemma_model_name
def invoke(self, prompt: str, **kwargs) -> str:
"""Generate text completion."""
try:
response = self.client.models.generate_content(
model=self.model_name,
contents=prompt,
)
return response.text
except Exception as e:
logger.error(f"Google GenAI generation failed: {e}")
raiseUpdate the factory function in the same file: def get_gemma_client() -> GemmaClient:
"""Factory function to get the configured Gemma client."""
provider = (app_config.gemma_provider or "ollama").lower()
if provider == "vertex":
return VertexAIGemmaClient()
elif provider == "ollama":
return OllamaGemmaClient()
elif provider == "google_genai":
return GoogleGenAIGemmaClient()
else:
logger.warning(
f"Unknown or unsupported Gemma provider: {provider}. "
"Defaulting to Ollama."
)
return OllamaGemmaClient()Then remove the patch script entirely. Benefits:
|
||
|
|
||
| with open('backend/src/agent/gemma_client.py', 'r') as f: | ||
| content = f.read() | ||
|
|
||
| new_class = """class GoogleGenAIGemmaClient(GemmaClient): | ||
| \"\"\"Client for Gemma models via Google GenAI API.\"\"\" | ||
|
|
||
| def __init__(self): | ||
| \"\"\"Initialize Google GenAI client.\"\"\" | ||
| try: | ||
| from google import genai | ||
| except ImportError: | ||
| logger.error("google-genai not installed.") | ||
| raise ImportError("Please install 'google-genai' to use GoogleGenAIGemmaClient") | ||
|
|
||
| import os | ||
| self.api_key = os.getenv("GEMINI_API_KEY") | ||
| if not self.api_key: | ||
| logger.error("GEMINI_API_KEY environment variable is missing.") | ||
| raise ValueError("GEMINI_API_KEY is required for GoogleGenAIGemmaClient") | ||
|
|
||
| self.client = genai.Client(api_key=self.api_key) | ||
| self.model_name = app_config.gemma_model_name | ||
|
|
||
| def invoke(self, prompt: str, **kwargs) -> str: | ||
| \"\"\"Generate text completion.\"\"\" | ||
| try: | ||
| response = self.client.models.generate_content( | ||
| model=self.model_name, | ||
| contents=prompt, | ||
| ) | ||
| return response.text | ||
| except Exception as e: | ||
| logger.error(f"Google GenAI generation failed: {e}") | ||
| raise e | ||
|
|
||
| """ | ||
|
|
||
| # Insert before get_gemma_client | ||
| content = content.replace("def get_gemma_client() -> GemmaClient:", new_class + "def get_gemma_client() -> GemmaClient:") | ||
|
|
||
| # Update factory method | ||
| factory_replacement = """def get_gemma_client() -> GemmaClient: | ||
| \"\"\"Factory function to get the configured Gemma client.\"\"\" | ||
| provider = (app_config.gemma_provider or "ollama").lower() | ||
| if provider == "vertex": | ||
| return VertexAIGemmaClient() | ||
| elif provider == "ollama": | ||
| return OllamaGemmaClient() | ||
| elif provider == "google_genai": | ||
| return GoogleGenAIGemmaClient() | ||
| else: | ||
| logger.warning(f"Unknown or unsupported Gemma provider: {provider}. Defaulting to Ollama.") | ||
| return OllamaGemmaClient()""" | ||
|
|
||
| content = re.sub(r'def get_gemma_client\(\) -> GemmaClient:.*?return OllamaGemmaClient\(\)', factory_replacement, content, flags=re.DOTALL) | ||
|
|
||
| with open('backend/src/agent/gemma_client.py', 'w') as f: | ||
| f.write(content) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import re | ||
|
|
||
| with open('backend/src/agent/gemma_client.py', 'r') as f: | ||
| content = f.read() | ||
|
|
||
| # Fix the duplicate else block | ||
| content = content.replace(""" else: | ||
| logger.warning(f"Unknown or unsupported Gemma provider: {provider}. Defaulting to Ollama.") | ||
| return OllamaGemmaClient() | ||
| else: | ||
| logger.warning(f"Unknown or unsupported Gemma provider: {provider}. Defaulting to Ollama.") | ||
| return OllamaGemmaClient()""", """ else: | ||
| logger.warning(f"Unknown or unsupported Gemma provider: {provider}. Defaulting to Ollama.") | ||
| return OllamaGemmaClient()""") | ||
|
|
||
|
|
||
| # Fix GoogleGenAIGemmaClient __init__ where ValueError is raised before setting api_key properly in exception message (not really an issue, but let's make it clean) | ||
| class_replacement = """class GoogleGenAIGemmaClient(GemmaClient): | ||
| \"\"\"Client for Gemma models via Google GenAI API.\"\"\" | ||
|
|
||
| def __init__(self): | ||
| \"\"\"Initialize Google GenAI client.\"\"\" | ||
| try: | ||
| from google import genai | ||
| except ImportError: | ||
| logger.error("google-genai not installed.") | ||
| raise ImportError("Please install 'google-genai' to use GoogleGenAIGemmaClient") | ||
|
|
||
| import os | ||
| self.api_key = os.getenv("GEMINI_API_KEY") | ||
| if not self.api_key: | ||
| logger.error("GEMINI_API_KEY environment variable is missing.") | ||
| raise ValueError("GEMINI_API_KEY is required for GoogleGenAIGemmaClient") | ||
|
|
||
| self.client = genai.Client(api_key=self.api_key) | ||
|
|
||
| # Use provided model or default to gemini-2.5-flash for free tier if trying to use gemma but not specified correctly | ||
| self.model_name = app_config.gemma_model_name | ||
| if not self.model_name or self.model_name == "gemma:7b": | ||
| self.model_name = "gemini-2.5-flash" # Fallback to gemini if gemma:7b (ollama default) is used | ||
|
|
||
| def invoke(self, prompt: str, **kwargs) -> str: | ||
| \"\"\"Generate text completion.\"\"\" | ||
| try: | ||
| response = self.client.models.generate_content( | ||
| model=self.model_name, | ||
| contents=prompt, | ||
| ) | ||
| return response.text | ||
| except Exception as e: | ||
| logger.error(f"Google GenAI generation failed: {e}") | ||
| raise e""" | ||
|
|
||
| content = re.sub(r'class GoogleGenAIGemmaClient.*?raise e', class_replacement, content, flags=re.DOTALL) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (complexity): Consider replacing the regex-based class rewrite and exact multi-line else-block replacement with more explicit, bounded patch regions and pattern-aware logic to make the patch safer and easier to maintain. The main complexity comes from regex‑patching an entire class body in another file. This is fragile and hard to maintain. You can keep the same behavior while making the transform explicit and safer. Two concrete ways to reduce complexity: 1. Use explicit region markers instead of a greedy regexInstead of: content = re.sub(
r'class GoogleGenAIGemmaClient.*?raise e',
class_replacement,
content,
flags=re.DOTALL,
)have # gemma_client.py
# BEGIN_GOOGLE_GENAI_GEMMA_CLIENT
class GoogleGenAIGemmaClient(GemmaClient):
...
# END_GOOGLE_GENAI_GEMMA_CLIENTThen your patch script becomes a simple, bounded replacement, not a file‑wide regex: start = '# BEGIN_GOOGLE_GENAI_GEMMA_CLIENT'
end = '# END_GOOGLE_GENAI_GEMMA_CLIENT'
start_idx = content.index(start) + len(start)
end_idx = content.index(end)
content = (
content[:start_idx]
+ '\n' + class_replacement.strip() + '\n'
+ content[end_idx:]
)This keeps the same end result but makes the patch:
2. Avoid full‑string duplicate block replacementThe duplicate For example, replace only the second needle = """ else:
logger.warning(f"Unknown or unsupported Gemma provider: {provider}. Defaulting to Ollama.")
return OllamaGemmaClient()
"""
occurrences = [m.start() for m in re.finditer(re.escape(needle), content)]
if len(occurrences) > 1:
# keep the first, remove extras
first = occurrences[0]
for pos in reversed(occurrences[1:]):
content = content[:pos] + content[pos + len(needle):]Functionality is unchanged (duplicate Both of these keep your “patching” approach but reduce the meta‑programming complexity and maintenance risk that the reviewer called out. |
||
|
|
||
|
|
||
| with open('backend/src/agent/gemma_client.py', 'w') as f: | ||
| f.write(content) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import re | ||
|
|
||
| with open('backend/src/agent/orchestration.py', 'r') as f: | ||
| content = f.read() | ||
|
|
||
| # Fix the bug in _load_default_tools where rag_tool.invoke is called but rag_tool doesn't have invoke | ||
| fix = """ # RAG retrieval | ||
| try: | ||
| from agent.rag import create_rag_tool, is_rag_enabled | ||
| if is_rag_enabled(): | ||
| rag_tool = create_rag_tool([]) | ||
| if rag_tool: | ||
| self.register( | ||
| "rag_search", | ||
| lambda query: rag_tool.retrieve(query), # Fixed missing invoke method | ||
| description="Search internal knowledge base", | ||
| category="search", | ||
| ) | ||
| except ImportError: | ||
| pass""" | ||
|
|
||
| content = re.sub(r' # RAG retrieval.*?pass', fix, content, flags=re.DOTALL) | ||
|
Check warning on line 22 in patch_orchestration.py
|
||
|
|
||
| with open('backend/src/agent/orchestration.py', 'w') as f: | ||
| f.write(content) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to PEP 8 style guidelines, imports should be placed at the top of the file. Please move
import osto the top-level of the module to improve code organization and readability.References