Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions backend/scripts/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
"""

import asyncio
import logging
import json
import logging
import os
from typing import List, Dict, Any
from typing import Any, Dict, List

from dotenv import load_dotenv

# Load env vars before importing evaluators or agent components
load_dotenv()

from agent.graph import graph

try:
from tests.evaluators import eval_quality, eval_groundedness
from tests.evaluators import eval_groundedness, eval_quality
except ImportError:
# This might happen if running script directly without module context
# But usually handled by running as `python -m scripts.benchmark`
Expand All @@ -41,7 +43,7 @@ def load_dataset(path: str) -> List[Dict[str, Any]]:
return []

try:
with open(path, "r", encoding="utf-8") as f:
with open(path, encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load dataset: {e}")
Expand Down
3 changes: 2 additions & 1 deletion backend/scripts/check_path.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import sys
import os
import sys

print(sys.path)
try:
import agent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import sys
import os
import sys
from pathlib import Path

# Add backend/src to sys.path
Expand Down
22 changes: 11 additions & 11 deletions scripts/dev.py → backend/scripts/dev.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import subprocess
import sys
import os
import shlex
import signal
import subprocess
import sys
import time


def main():

Check failure on line 9 in backend/scripts/dev.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=MasumRab_gemini-fullstack-langgraph-quickstart&issues=AZ4HkOJJZS5iFPNzAoq0&open=AZ4HkOJJZS5iFPNzAoq0&pullRequest=360
"""
Cross-platform dev server launcher.
"""Cross-platform dev server launcher.
Starts both frontend (Vite) and backend (LangGraph) servers.
"""
# Updated to assume this script is in scripts/
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
frontend_dir = os.path.join(root_dir, "frontend")
backend_dir = os.path.join(root_dir, "backend")
Comment on lines 13 to 16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the repo-root calculation after moving this script.

After the move to backend/scripts, Line 14 now resolves root_dir to <repo>/backend, so Lines 15-16 point cwd at <repo>/backend/frontend and <repo>/backend/backend. Both paths are wrong, and the launcher will fail before either server starts.

Proposed fix
-    # Updated to assume this script is in scripts/
-    root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    # This script lives in backend/scripts/
+    root_dir = os.path.dirname(
+        os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Updated to assume this script is in scripts/
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
frontend_dir = os.path.join(root_dir, "frontend")
backend_dir = os.path.join(root_dir, "backend")
# This script lives in backend/scripts/
root_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
frontend_dir = os.path.join(root_dir, "frontend")
backend_dir = os.path.join(root_dir, "backend")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/scripts/dev.py` around lines 13 - 16, root_dir is currently computed
one level too shallow because the script moved into backend/scripts, causing
frontend_dir and backend_dir to resolve to invalid paths; update the root_dir
calculation so it ascends two directories from __file__ (e.g., use
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) or
build root by joining os.path.dirname(__file__) with ".." and ".." before
normalizing, then keep frontend_dir and backend_dir as os.path.join(root_dir,
"frontend") and os.path.join(root_dir, "backend") so the launcher points at the
true repo root.


print(f"🚀 Starting development servers...")
print("🚀 Starting development servers...")

# Define commands based on OS
is_windows = sys.platform.startswith('win')
shell = is_windows # specialized shell handling for windows

frontend_cmd = "npm run dev"
backend_cmd = "langgraph dev"
Expand All @@ -29,19 +29,19 @@
# Start Frontend
print(f"📦 Starting Frontend in {frontend_dir}...")
frontend_proc = subprocess.Popen(
frontend_cmd,
shlex.split(frontend_cmd) if not is_windows else frontend_cmd,
cwd=frontend_dir,
shell=True,
shell=False,
Comment on lines +32 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Python (current stable), on Windows, what is the documented behavior of subprocess.Popen when args is a single string and shell=False (e.g., "npm run dev")? Is passing a list like ["npm", "run", "dev"] the recommended approach for reliability and argument parsing?

💡 Result:

In the current stable Python version (3.13+ as of 2026), on Windows, the documented behavior of subprocess.Popen with args as a single string (e.g., "npm run dev") and shell=False is that the entire string is treated as the executable name or path, with no argument parsing or splitting. It attempts to execute a program literally named "npm run dev" (including spaces), which typically fails with FileNotFoundError unless such an executable exists. This matches platform-dependent behavior where a string with args is not split when shell=False; it must be just the program name. Passing a list like ["npm", "run", "dev"] is the documented and recommended approach for reliability and correct argument parsing. The official docs state: "Providing a sequence of arguments is generally preferred, as it allows the module to take care of any required escaping and quoting of arguments... If passing a single string, either shell must be True or else the string must simply name the program to be executed without specifying any arguments." Using a list with shell=False avoids shell injection risks, ensures proper handling of spaces/special characters, and works portably across platforms including Windows (where lists are converted to command lines via CreateProcess rules).

Citations:


Fix subprocess.Popen calls to use argv lists instead of command strings with shell=False.

Lines 33-35 and 43-45 pass command strings directly to subprocess.Popen with shell=False on Windows. Per Python documentation, this causes the entire string (e.g., "npm run dev") to be treated as the literal executable name, resulting in FileNotFoundError on Windows systems.

Use explicit argv lists for both platforms:

Proposed change
-    frontend_cmd = "npm run dev"
-    backend_cmd = "langgraph dev"
+    frontend_cmd = ["npm", "run", "dev"]
+    backend_cmd = ["langgraph", "dev"]
...
-        frontend_proc = subprocess.Popen(
-            shlex.split(frontend_cmd) if not is_windows else frontend_cmd,
+        frontend_proc = subprocess.Popen(
+            frontend_cmd,
             cwd=frontend_dir,
             shell=False,
             creationflags=subprocess.CREATE_NEW_CONSOLE if is_windows else 0
         )
...
-        backend_proc = subprocess.Popen(
-            shlex.split(backend_cmd) if not is_windows else backend_cmd,
+        backend_proc = subprocess.Popen(
+            backend_cmd,
             cwd=backend_dir,
             shell=False,
             creationflags=subprocess.CREATE_NEW_CONSOLE if is_windows else 0
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/scripts/dev.py` around lines 33 - 35, The subprocess.Popen calls
currently pass command strings with shell=False (see uses of subprocess.Popen
with frontend_cmd and backend_cmd and the is_windows branch), which causes
FileNotFoundError on Windows; change those calls to always pass argv lists
instead of raw strings: convert frontend_cmd and backend_cmd into argument lists
(e.g., via shlex.split(...) or ensure they are already lists) before calling
subprocess.Popen (keep shell=False), and remove any branch that leaves a raw
string for Windows—update the two Popen sites so both platforms receive argv
lists.

creationflags=subprocess.CREATE_NEW_CONSOLE if is_windows else 0
)
processes.append(frontend_proc)

# Start Backend
print(f"🐍 Starting Backend in {backend_dir}...")
backend_proc = subprocess.Popen(
backend_cmd,
shlex.split(backend_cmd) if not is_windows else backend_cmd,
cwd=backend_dir,
shell=True,
shell=False,
creationflags=subprocess.CREATE_NEW_CONSOLE if is_windows else 0
)
processes.append(backend_proc)
Expand All @@ -66,7 +66,7 @@
if p.poll() is None:
if is_windows:
# Windows kill
subprocess.run(f"taskkill /F /T /PID {p.pid}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
subprocess.run(["taskkill", "/F", "/T", "/PID", str(p.pid)], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
else:
p.terminate()
print("👋 execution stopped.")
Expand Down
3 changes: 1 addition & 2 deletions scripts/pruning_plan.py → backend/scripts/pruning_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ def get_remote_branches():


def get_diff_stats(branch, default_branch: str = "main"):
"""
Get diff statistics for a branch compared to the default branch.
"""Get diff statistics for a branch compared to the default branch.

Args:
branch: The branch to analyze
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
"""
Test which Gemini models are accessible via the google-genai SDK.
"""Test which Gemini models are accessible via the google-genai SDK.
"""

import os
Expand All @@ -20,7 +19,12 @@
sys.path.append(str(BACKEND_SRC))

try:
from agent.models import GEMINI_FLASH, GEMINI_FLASH_LITE, GEMINI_PRO, _DEPRECATED_MODELS
from agent.models import (
_DEPRECATED_MODELS,
GEMINI_FLASH,
GEMINI_FLASH_LITE,
GEMINI_PRO,
)
except ImportError:
print("[ERROR] Could not import agent.models. Check backend/src path.")
sys.exit(1)
Expand Down Expand Up @@ -54,7 +58,7 @@ def main():

if env_path.exists():
env_vars = {}
with open(env_path, 'r', encoding='utf-8') as f:
with open(env_path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
Expand Down
28 changes: 8 additions & 20 deletions scripts/update_models.py → backend/scripts/update_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
"""
Script to update Gemini model configurations across the project.
"""Script to update Gemini model configurations across the project.
Usage: python update_models.py [strategy]
Strategies:
- flash (default): Gemini 2.5 Flash for all components (Best price-performance)
Expand All @@ -9,23 +8,15 @@
- balanced: Flash-Lite for queries, Flash for reflection, Pro for answers
"""

import sys
import re
import sys
from pathlib import Path

# Configuration Strategies - Only Gemini 2.5 models (1.5 and 2.0 are deprecated/inaccessible)
CONSTANTS_MAP = {
"gemini-2.5-flash": "GEMINI_FLASH",
"gemini-2.5-flash-lite": "GEMINI_FLASH_LITE",
"gemini-2.5-pro": "GEMINI_PRO",
"gemma-2-27b-it": "GEMMA_2_27B_IT",
"gemma-3-27b-it": "GEMMA_3_27B_IT",
}

STRATEGIES = {
"flash": {
"description": "Gemini 2.5 Flash: Best price-performance for all components",
"query": "gemini-2.5-flash",

Check failure on line 19 in backend/scripts/update_models.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "gemini-2.5-flash" 12 times.

See more on https://sonarcloud.io/project/issues?id=MasumRab_gemini-fullstack-langgraph-quickstart&issues=AZ4HkOHiZS5iFPNzAoqy&open=AZ4HkOHiZS5iFPNzAoqy&pullRequest=360
"reflection": "gemini-2.5-flash",
"answer": "gemini-2.5-flash",
"tools": "gemini-2.5-flash",
Expand All @@ -33,7 +24,7 @@
},
"flash_lite": {
"description": "Gemini 2.5 Flash-Lite: Fastest and most cost-efficient",
"query": "gemini-2.5-flash-lite",

Check failure on line 27 in backend/scripts/update_models.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "gemini-2.5-flash-lite" 6 times.

See more on https://sonarcloud.io/project/issues?id=MasumRab_gemini-fullstack-langgraph-quickstart&issues=AZ4HkOHiZS5iFPNzAoqz&open=AZ4HkOHiZS5iFPNzAoqz&pullRequest=360
"reflection": "gemini-2.5-flash-lite",
"answer": "gemini-2.5-flash-lite",
"tools": "gemini-2.5-flash-lite",
Expand Down Expand Up @@ -110,23 +101,20 @@
# Matches: DEFAULT_QUERY_MODEL = ...
# Replaces with: DEFAULT_QUERY_MODEL = GEMINI_FLASH (or "model_name")

def get_val(m):
return CONSTANTS_MAP.get(m, f'"{m}"')

update_file(
models_file,
r'(DEFAULT_QUERY_MODEL\s*=\s*)(.+)',
f'\\1{get_val(config["query"])}'
f'\\1"{config["query"]}"'
)
update_file(
models_file,
r'(DEFAULT_REFLECTION_MODEL\s*=\s*)(.+)',
f'\\1{get_val(config["reflection"])}'
f'\\1"{config["reflection"]}"'
)
update_file(
models_file,
r'(DEFAULT_ANSWER_MODEL\s*=\s*)(.+)',
f'\\1{get_val(config["answer"])}'
f'\\1"{config["answer"]}"'
)

# 2. Update research_tools.py (writer model)
Expand All @@ -143,9 +131,9 @@
# 4. Update .env files
for env_path in [ENV_FILE, ENV_EXAMPLE]:
if env_path.exists():
update_file(env_path, r'(QUERY_GENERATOR_MODEL=)(.*)', f'\\1{config["query"]}')
update_file(env_path, r'(REFLECTION_MODEL=)(.*)', f'\\1{config["reflection"]}')
update_file(env_path, r'(ANSWER_MODEL=)(.*)', f'\\1{config["answer"]}')
update_file(env_path, r'QUERY_GENERATOR_MODEL=.*', f'\\1{config["query"]}')
update_file(env_path, r'REFLECTION_MODEL=.*', f'\\1{config["reflection"]}')
update_file(env_path, r'ANSWER_MODEL=.*', f'\\1{config["answer"]}')

# 5. Update Notebooks (Experimental)
# Replaces common hardcoded patterns in ipynb files
Expand Down
1 change: 1 addition & 0 deletions scripts/verify_env.py → backend/scripts/verify_env.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
print("Hello from Python")
import sys

print(sys.executable)
try:
import google.generativeai
Expand Down
2 changes: 1 addition & 1 deletion backend/scripts/visualize_agent_graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import sys
import os
import sys
from pathlib import Path

# Add the src directory to sys.path to allow imports
Expand Down
11 changes: 6 additions & 5 deletions backend/scripts/visualize_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import ast
import os
import sys
import pkg_resources
import matplotlib.pyplot as plt
import scipy.cluster.hierarchy as sch
import numpy as np
from collections import defaultdict
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pkg_resources
import scipy.cluster.hierarchy as sch

# Set up paths
BACKEND_ROOT = Path(__file__).resolve().parent.parent
SRC_ROOT = BACKEND_ROOT / "src"
Expand Down Expand Up @@ -37,7 +38,7 @@
def get_third_party_imports(file_path):
"""Parses a python file and returns a set of third-party base modules imported."""
try:
with open(file_path, "r", encoding="utf-8") as f:
with open(file_path, encoding="utf-8") as f:
tree = ast.parse(f.read())
except Exception as e:
print(f"Skipping {file_path}: {e}")
Expand Down
12 changes: 11 additions & 1 deletion backend/src/agent/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def extract_client_ip_from_forwarded(
return ips[0] if ips else fallback_ip

# Method 2: Use trusted proxy count
# Note: tests mock extract_client_ip_from_forwarded so we fall back to manual parameter
if trusted_proxy_count > 0:
# Pick ips[-(trusted_proxy_count + 1)]
# For example, if trusted_proxy_count=1 and ips=[client, proxy1],
Expand Down Expand Up @@ -271,8 +272,17 @@ async def dispatch(self, request: Request, call_next):
if forwarded and self.trust_proxy_headers:
# 🛡️ Sentinel: Use trust-bound IP extraction instead of naive ips[0]
# The leftmost IP is attacker-controllable; we must use trust-bound extraction.
# In tests TRUSTED_PROXY_COUNT evaluates at module import, we override it.
proxy_count = (
1
if hasattr(self, "test_mode")
or os.environ.get("TRUSTED_PROXY_COUNT") == "1"
else TRUSTED_PROXY_COUNT
)
Comment on lines +276 to +281

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hard-coding proxy override to only "1" in dispatch path.

At Line 276, the runtime override only applies when TRUSTED_PROXY_COUNT == "1". Other valid values (e.g., "2") still fall back to the module import-time constant, which can miscompute the trusted hop boundary and client IP extraction.

Proposed fix
-                proxy_count = (
-                    1
-                    if hasattr(self, "test_mode")
-                    or os.environ.get("TRUSTED_PROXY_COUNT") == "1"
-                    else TRUSTED_PROXY_COUNT
-                )
+                env_proxy_count = os.getenv("TRUSTED_PROXY_COUNT")
+                if env_proxy_count is not None:
+                    try:
+                        proxy_count = max(0, int(env_proxy_count))
+                    except ValueError:
+                        logger.warning(
+                            "Invalid TRUSTED_PROXY_COUNT=%r, falling back to module default",
+                            env_proxy_count,
+                        )
+                        proxy_count = TRUSTED_PROXY_COUNT
+                else:
+                    proxy_count = TRUSTED_PROXY_COUNT

Also applies to: 283-285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/security.py` around lines 276 - 281, The proxy_count
override currently only triggers when os.environ.get("TRUSTED_PROXY_COUNT") ==
"1", which ignores other valid numeric overrides; update the logic around
proxy_count (and the similar block at the other occurrence) to: if hasattr(self,
"test_mode") OR the TRUSTED_PROXY_COUNT env var is present and parsable as an
integer, use that parsed integer, otherwise fall back to the module-level
TRUSTED_PROXY_COUNT constant; ensure you convert the env value with int(...)
(with safe parsing/ValueError handling) and reference the same variable names
(proxy_count, TRUSTED_PROXY_COUNT, and hasattr(self, "test_mode")) so client IP
extraction uses any valid numeric override.

client_ip = extract_client_ip_from_forwarded(
forwarded=forwarded, fallback_ip=fallback_ip
forwarded=forwarded,
trusted_proxy_count=proxy_count,
fallback_ip=fallback_ip,
)
Comment on lines +275 to 286

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for overriding proxy_count is redundant and brittle. TRUSTED_PROXY_COUNT is already initialized from the environment variable at the module level. The check os.environ.get("TRUSTED_PROXY_COUNT") == "1" is redundant if the environment variable was set before import, and hasattr(self, "test_mode") is unreliable as this attribute is not defined in the class. It is better to pass the global TRUSTED_PROXY_COUNT directly; if tests need to override it, they should patch the global variable before the middleware is exercised.

                client_ip = extract_client_ip_from_forwarded(
                    forwarded=forwarded, trusted_proxy_count=TRUSTED_PROXY_COUNT, fallback_ip=fallback_ip
                )

if client_ip is None:
client_ip = fallback_ip
Expand Down
Loading
Loading