Skip to content

feat: Add BYOK (Bring Your Own Key) support for Groq API#145

Open
Nihallllll wants to merge 2 commits intoAOSSIE-Org:mainfrom
Nihallllll:feat/byok-integration
Open

feat: Add BYOK (Bring Your Own Key) support for Groq API#145
Nihallllll wants to merge 2 commits intoAOSSIE-Org:mainfrom
Nihallllll:feat/byok-integration

Conversation

@Nihallllll
Copy link

@Nihallllll Nihallllll commented Feb 27, 2026

What this PR does

This PR removes the dependency on a server-side GROQ_API_KEY environment variable
and replaces it with a BYOK (Bring Your Own Key) pattern — users supply their own
Groq API key through the frontend, which is sent securely with each request.


Screen.Recording.2026-02-26.224451.mp4

Why this is needed

Previously, the app required the developer/host to provide a single Groq API key
on the server. This meant:

  • The host bears all AI costs for every user
  • One shared key is a security and rate-limit risk
  • The app cannot be deployed publicly without exposing cost to the host

With BYOK, each user uses their own free Groq key. The server never stores any key.


Changes

Backend

  • routes.py — All 3 endpoints (/bias, /process, /chat) now read
    X-BYOK-Api-Key and X-BYOK-Model from request headers. Returns 401
    if the key is missing.
  • pipeline.pyrun_langgraph_workflow() accepts api_key and groq_model
    and injects them into the LangGraph state.
  • langgraph_builder.pyMyState TypedDict extended with groq_api_key and
    groq_model fields.
  • All LangGraph nodes (sentiment, generate_perspective, judge) — removed
    module-level hardcoded Groq clients; now create a per-request client from
    state["groq_api_key"].
  • fact_check.py — Fact-check failures are now soft/non-fatal. Pipeline
    continues with empty facts instead of aborting the whole analysis.
  • chat/llm_processing.py — Chat endpoint now accepts the full article text
    as additional context alongside Pinecone RAG results.

Frontend

  • components/settings-modal.tsx (new file) — Settings dialog for users to
    enter their Groq API key and choose a model. Key stored in localStorage.
    Auto-opens on first visit.
  • app/analyze/page.tsx — Added <SettingsModal /> to the header.
  • app/analyze/loading/page.tsx — Reads key/model from localStorage and attaches
    them as X-BYOK-Api-Key / X-BYOK-Model headers on API requests.
  • app/analyze/results/page.tsx — Chat requests also include BYOK headers and
    send full article text for richer answers.

Testing

Tested manually:

  • First visit triggers the settings modal
  • Key is validated (must start with gsk_)
  • Submitting an article URL with a valid key completes the full analysis pipeline
  • Submitting without a key returns a 401 error
  • Chat with article context returns better answers than before

Notes

  • The BACKEND URL in loading/page.tsx and results/page.tsx is currently
    hardcoded to http://localhost:7860. A follow-up can move this to
    NEXT_PUBLIC_API_URL env var for cleaner deployments.
  • Pinecone/embedding still uses server-side env vars (not BYOK'd) —
    intentional for now as embedding is infrastructure, not user AI calls.

Summary by CodeRabbit

  • New Features

    • Settings modal for configuring API keys and selecting AI models
    • Bring-your-own-key (BYOK) authentication support across all endpoints
  • Bug Fixes

    • Improved error handling for missing search results and incomplete data
    • Enhanced Unicode support for console output
    • Better input validation for bias detection
  • Chores

    • Added Google generative AI dependency

- Replace deprecated gemma2-9b-it model with llama-3.3-70b-versatile
- Add BYOK support: API key passed via X-BYOK-Api-Key request header
- Add groq_api_key and groq_model fields to LangGraph pipeline state
- Add SettingsModal component for users to configure their own Groq key
- Fix chat: send article_text as context so LLM can actually answer questions
- Fix chat: forward BYOK headers on chat requests
- Make fact-check failure non-fatal (soft failure continues with empty facts)
- Fix results page: add header with working Perspective → /analyze navigation link
- Switch frontend API base URL from hardcoded HF Space to localhost:7860
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces bring-your-own-key (BYOK) authentication with configurable model selection across the application. It transitions from module-level Groq client initialization using environment variables to per-call client creation with explicit api_key and groq_model parameters. A new frontend settings modal enables users to configure API credentials and model preferences, which are then propagated through HTTP headers to the backend and threaded through the LangGraph pipeline.

Changes

Cohort / File(s) Summary
Configuration & Logging
backend/app/.gitignore, backend/app/logging/logging_config.py
Added .env to gitignore; improved console handler with UTF-8 encoding support and fallback error handling.
Bias Detection Module
backend/app/modules/bias_detection/check_bias.py
Replaced global Groq client with per-call initialization; added api_key and groq_model parameters; introduced input validation for empty text.
Chat LLM Processing
backend/app/modules/chat/llm_processing.py
Migrated to per-call Groq client; extended signature with api_key, groq_model, and article_text; refactored context building with pinecone and article segments.
Facts Check Pipeline
backend/app/modules/facts_check/llm_processing.py, backend/app/utils/fact_check_utils.py
Moved Groq client initialization to per-call in both claim extraction and fact verification; replaced hardcoded model with configurable groq_model parameter; updated utility to pass api_key and model to verifier.
Web Search Guard
backend/app/modules/facts_check/web_search.py
Added guard to handle responses without "items" field, preventing KeyError on empty search results.
LangGraph Integration
backend/app/modules/langgraph_builder.py, backend/app/modules/langgraph_nodes/...
Extended MyState schema with groq_api_key and groq_model fields; refactored sentiment, judge, generate_perspective, and fact_check nodes to use per-call Groq initialization from state; changed fact_check error handling to soft failure (log and continue).
Pipeline & Routes
backend/app/modules/pipeline.py, backend/app/routes/routes.py
Updated pipeline signature to accept api_key and groq_model, threading them through LangGraph workflow; added HTTP authentication via x-byok-api-key header validation; added configurable groq_model header; extended ChatQuery with article_text field; all endpoints now require and pass API credentials.
Dependencies
backend/pyproject.toml
Added langchain-google-genai>=2.1.12 dependency.
Frontend Settings Modal
frontend/components/settings-modal.tsx
New component providing localStorage-backed API configuration UI; supports provider selection (Groq), API key input with visibility toggle, model selection from provider's supported models, and validation with inline error messaging.
Frontend Page Integration
frontend/app/page.tsx, frontend/app/analyze/page.tsx, frontend/app/analyze/loading/page.tsx, frontend/app/analyze/results/page.tsx
Integrated SettingsModal component in header; added BACKEND endpoint constant and centralized API calls; extended requests with X-BYOK-Api-Key and X-BYOK-Model headers sourced from settings; updated chat request payload with article_text field.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hop, hop—the settings modal now blooms,
As keys find their homes in localStorage rooms,
Per-call clients dance, no globals remain,
BYOK auth flows through the LangGraph lane! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding BYOK (Bring Your Own Key) support for Groq API, which is the primary objective across all backend and frontend modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/app/modules/facts_check/llm_processing.py (1)

143-148: ⚠️ Potential issue | 🟠 Major

Prevent UnboundLocalError when LLM JSON parsing fails.

If parsing fails on Line 145, parsed is undefined at Line 148.

Suggested fix
             try:
                 parsed = json.loads(content)
             except Exception as parse_err:
                 logger.error(f"LLM JSON parse error: {parse_err}")
+                parsed = {
+                    "verdict": "Unknown",
+                    "explanation": "Failed to parse verifier output",
+                    "original_claim": claim,
+                    "source_link": source,
+                }

             results_list.append(parsed)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/facts_check/llm_processing.py` around lines 143 - 148,
The JSON parsing try/except in llm_processing.py can leave `parsed` undefined if
json.loads(content) raises; update the except handler in the block around the
current try/except (the JSON parse around `parsed = json.loads(content)` and
`results_list.append(parsed)`) to either set `parsed` to a safe default (e.g.,
None or an empty dict) or skip appending when parsing fails, and keep logging
the error via `logger.error`; ensure `results_list.append(parsed)` only runs
when `parsed` is defined/valid to prevent UnboundLocalError.
backend/app/modules/facts_check/web_search.py (1)

35-40: ⚠️ Potential issue | 🟠 Major

Handle empty items arrays, not only missing keys.

Lines 35-36 avoid KeyError, but items: [] still raises IndexError at Line 38.

Suggested fix
-    if "items" not in res:
+    if not res.get("items"):
         return []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/facts_check/web_search.py` around lines 35 - 40, The code
currently checks only for the presence of the "items" key and then indexes
res["items"][0], which will raise IndexError when items is an empty list;
replace that logic by reading items = res.get("items", []) and then if not
items: return [] to handle empty lists (or None) safely, then populate first
using items[0]["title"], items[0]["link"], items[0]["snippet"]; update the block
that references res and first accordingly so it never indexes into an empty
items array.
frontend/app/analyze/results/page.tsx (1)

93-103: ⚠️ Potential issue | 🟠 Major

Add chat request error handling to prevent UI dead-ends.

If /api/chat returns 401/network error, this throws and leaves the chat flow without user feedback.

Proposed fix
-    const res = await axios.post(`${BACKEND}/api/chat`, {
-      message: message,
-      article_text: analysisData?.cleaned_text ?? "",
-    }, { headers: { "X-BYOK-Api-Key": getActiveApiKey() ?? "", "X-BYOK-Model": getActiveModel() ?? "" } });
-    const data = res.data;
-
-    console.log(data);
-
-    // 🔹 Step 2: Append LLM’s response
-    setMessages([...newMessages, { role: "assistant", content: data.answer }]);
+    try {
+      const apiKey = getActiveApiKey();
+      const model = getActiveModel();
+      const headers: Record<string, string> = {};
+      if (apiKey) headers["X-BYOK-Api-Key"] = apiKey;
+      if (model) headers["X-BYOK-Model"] = model;
+
+      const { data } = await axios.post(
+        `${BACKEND}/api/chat`,
+        {
+          message,
+          article_text: analysisData?.cleaned_text ?? "",
+        },
+        { headers }
+      );
+      setMessages([...newMessages, { role: "assistant", content: data.answer }]);
+    } catch (error) {
+      setMessages([
+        ...newMessages,
+        {
+          role: "assistant",
+          content: "I couldn’t reach the chat service. Check API settings and try again.",
+        },
+      ]);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` around lines 93 - 103, Wrap the
axios.post call that sends to `${BACKEND}/api/chat` in a try/catch around the
existing code in page.tsx (the block that creates `res` and `data`), detect HTTP
401 specifically via `err.response?.status === 401` and network/other errors via
lack of `err.response`, and in each case update UI state instead of letting the
exception bubble: call `setMessages` (or a dedicated error state) to append an
assistant/system message describing the error (e.g., "Authentication error:
please check API key" for 401, "Network error: please try again" for network
failures), ensure any loading flags are cleared, and optionally log the full
error to console for debugging; keep references to `newMessages`, `setMessages`,
`getActiveApiKey`, `getActiveModel`, and `analysisData` when implementing the
catch handling so the flow continues gracefully.
🧹 Nitpick comments (2)
backend/app/logging/logging_config.py (1)

28-32: Consider narrowing the exception type.

Catching bare Exception can mask unexpected errors. The likely failure modes here are OSError (if fileno() or open() fails) and AttributeError (if sys.stdout lacks fileno(), e.g., in certain IDEs or test harnesses).

♻️ Proposed fix
     try:
         utf8_stream = open(sys.stdout.fileno(), mode="w", encoding="utf-8", closefd=False)
         console_handler = logging.StreamHandler(utf8_stream)
-    except Exception:
+    except (OSError, AttributeError):
         console_handler = logging.StreamHandler(sys.stdout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/logging/logging_config.py` around lines 28 - 32, Replace the
broad "except Exception" with targeted exceptions: catch OSError and
AttributeError around the utf8_stream = open(sys.stdout.fileno(), ...) and
console_handler = logging.StreamHandler(utf8_stream) block so only expected
failures (fileno()/open issues or missing fileno) fall through to the fallback
console_handler = logging.StreamHandler(sys.stdout); keep the same fallback
behavior and ensure utf8_stream and console_handler symbols are used exactly as
in the diff.
backend/app/modules/pipeline.py (1)

67-70: Consider centralizing the default model constant.

"llama-3.3-70b-versatile" is duplicated across modules; a shared constant reduces drift and accidental mismatch.

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

In `@backend/app/modules/pipeline.py` around lines 67 - 70, Replace the hard-coded
model string in run_langgraph_workflow with a shared constant: define a single
exported constant (e.g., DEFAULT_GROQ_MODEL or GROQ_MODEL_DEFAULT) in your
central config/constants module and import it into this module, then change the
function signature of run_langgraph_workflow to use that constant as the default
for groq_model and update any other occurrences of "llama-3.3-70b-versatile" to
reference that same constant so all modules use the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/modules/bias_detection/check_bias.py`:
- Around line 36-38: The Groq client is being constructed before the try block
in check_bias, so initialization/validation errors escape your structured error
handling; move the Groq(api_key=api_key) call inside the try (or add a
surrounding try/except that wraps the instantiation) and handle exceptions there
(using the same error response path you use for subsequent calls), referencing
the check_bias function and the Groq client instantiation to ensure any
instantiation errors are caught and returned in the structured error response.

In `@backend/app/modules/chat/llm_processing.py`:
- Line 46: The current logger.debug call logs the full LLM context in
pinecone_context which can leak sensitive text and create huge logs; modify the
logging in llm_processing.py to avoid printing raw content by replacing the
logger.debug(f"Generated context for LLM:\n{pinecone_context}") call with a
metadata-only log: record the length/size (e.g., len(pinecone_context) or number
of chunks), an optional content hash (e.g., SHA256) or first N characters only
if needed for debugging, and any token/count metrics instead of the full
pinecone_context string; update the code paths that reference logger.debug and
ensure pinecone_context is never logged in full.

In `@backend/app/modules/facts_check/llm_processing.py`:
- Around line 39-40: The code instantiates Groq with state["groq_api_key"]
outside the try block which can raise KeyError before your structured error
handling runs; inside the claim extractor function, move the API key access into
the existing try (or guard it with state.get and raise a controlled error) and
then construct client = Groq(api_key=api_key) within that try so any missing-key
exception is caught and handled (or explicitly check for the key first and call
your error/log path if absent). Ensure you reference the same identifiers
(state, Groq, client) and preserve the existing error logging/handling flow.

In `@backend/app/modules/langgraph_builder.py`:
- Around line 55-56: The store_and_send node is leaking sensitive fields
(groq_api_key and groq_model) by logging the full state and returning it; update
the store_and_send function to (1) remove or redact these keys before logging
(or remove the debug log entirely) and (2) filter them out of the returned state
so the HTTP response from the /process pipeline does not include groq_api_key or
groq_model; locate the store_and_send implementation and replace the return
value with a sanitized dict that excludes "groq_api_key" and "groq_model" (and
ensure pipeline.py/routes.py continue to receive only the sanitized state).

In `@backend/app/modules/langgraph_nodes/fact_check.py`:
- Around line 35-38: The warning currently logs the raw upstream payload via
logger.warning(f"Fact-checking skipped (non-fatal): {error_message}") which can
leak sensitive provider text; change the logging in fact_check.py to emit a
sanitized/structured message instead (e.g., include a fixed description,
optional provider identifier or error code if available, and omit or truncate
the raw error_message) while preserving the soft-failure behavior that sets
verifications = [] so the pipeline continues; update the logger call to use the
sanitized fields rather than logging error_message verbatim.

In `@backend/app/modules/langgraph_nodes/generate_perspective.py`:
- Around line 55-60: The facts_str construction in generate_perspective.py is
using only the first line as an f-string so "{f['verdict']}" and
"{f['explanation']}" are not interpolated; update the list comprehension that
builds facts_str (the expression assigning facts_str) so each item is a single
f-string that includes f['original_claim'], f['verdict'], and f['explanation']
(or alternatively concatenate three f-strings per item) and ensure the
comprehension iterates over (facts or [])—i.e., replace the current mix of plain
and f-strings with a single f"Claim: {f['original_claim']}\nVerdict:
{f['verdict']}\nExplanation: {f['explanation']}" for f in (facts or []).

In `@backend/app/modules/langgraph_nodes/judge.py`:
- Around line 27-33: The ChatGroq instance is created before the try block and
uses direct indexing state["groq_api_key"], which can raise uncaught KeyError;
move the ChatGroq construction (groq_llm = ChatGroq(...)) inside the existing
try block and first validate the key (e.g., check "groq_api_key" in state and
that it is not empty) before instantiation; update references to groq_llm in
this scope so errors are caught and return the node-level error response on
failure.

In `@backend/app/modules/langgraph_nodes/sentiment.py`:
- Around line 28-29: The Groq client instantiation uses state["groq_api_key"]
outside the try block so a missing key raises KeyError before your error
handler; move the API-key access into the existing try/except (or use
state.get("groq_api_key") inside the try) so the client = Groq(api_key=...) call
is executed under the same error handling, and add a clear error/raise path if
the key is None—referencing the Groq constructor, state dict and the current try
block in sentiment.py.

In `@backend/app/routes/routes.py`:
- Around line 69-70: The call passes the entire scraper result dict (content)
into check_bias causing prompt issues; update the route to extract the text
field returned by run_scraper_pipeline (e.g., use content["cleaned_text"] or
content.get("cleaned_text")) and pass that string to check_bias along with
api_key and groq_model so check_bias receives raw article text rather than the
whole dict.
- Around line 94-95: In the /chat route, avoid blocking the event loop by
running the synchronous network calls in the thread pool: replace direct calls
to search_pinecone(query) and ask_llm(query, results, api_key, groq_model,
chat_request.article_text) with asyncio.to_thread(...) calls (i.e., await
asyncio.to_thread(search_pinecone, query) and await asyncio.to_thread(ask_llm,
query, results, api_key, groq_model, chat_request.article_text)); ensure asyncio
is imported if not already and preserve returned values as before.

In `@backend/pyproject.toml`:
- Line 16: Remove the unused dependency string "langchain-google-genai>=2.1.12"
from the project dependencies (the entry shown in the diff) and update the
project's dependency lockfile (poetry.lock or equivalent) so the removed package
is no longer pinned; ensure no import or usage of langchain_google_genai remains
in the codebase before committing the change.

In `@frontend/app/analyze/loading/page.tsx`:
- Line 20: The BACKEND constant in page.tsx is hardcoded to
"http://localhost:7860"; change it to read from a configurable client-side
environment variable (e.g. NEXT_PUBLIC_BACKEND) with a safe fallback to the
current origin/relative path so it works in staging/production and during local
dev; update the const BACKEND declaration to use process.env.NEXT_PUBLIC_BACKEND
(or equivalent runtime config) and ensure code that uses BACKEND (in this file)
handles undefined by building a URL from window.location.origin or using a
relative path so no hardcoded origin remains.

In `@frontend/app/analyze/results/page.tsx`:
- Line 25: Replace the hardcoded BACKEND constant in
frontend/app/analyze/results/page.tsx with an environment-configured value: read
the client-facing env var (e.g. process.env.NEXT_PUBLIC_BACKEND_URL or
NEXT_PUBLIC_API_URL) and use that for BACKEND, falling back to a sensible
default (such as a relative path) when the env var is absent; update any code
that imports/uses BACKEND to rely on this env-driven value so deployments can
configure the backend URL without changing source.

In `@frontend/components/settings-modal.tsx`:
- Around line 167-169: The privacy copy inside the settings-modal component (the
rounded info div in frontend/components/settings-modal.tsx) is incorrect; update
the text to accurately state that the API key is stored in the browser but may
be forwarded to selected AI providers via requests (for example via the
X-BYOK-Api-Key header) rather than claiming it is “never sent to our servers”;
locate the div rendering the message and replace the sentence with a concise,
factual line noting local storage of the key and that requests to providers may
include the key in headers so users understand it can be transmitted.
- Around line 109-114: The effect that runs on provider change unconditionally
calls setModel(current.defaultModel), which can overwrite a previously saved
activeModel in config; update the useEffect to first check for an existing saved
model (e.g., config.activeModel or a provider-scoped saved model) and only call
setModel(current.defaultModel) when no saved model exists or when the saved
model belongs to a different provider, leaving the saved activeModel intact;
keep the existing setShowKey and setError resets but do not clobber model if a
saved activeModel is present.

---

Outside diff comments:
In `@backend/app/modules/facts_check/llm_processing.py`:
- Around line 143-148: The JSON parsing try/except in llm_processing.py can
leave `parsed` undefined if json.loads(content) raises; update the except
handler in the block around the current try/except (the JSON parse around
`parsed = json.loads(content)` and `results_list.append(parsed)`) to either set
`parsed` to a safe default (e.g., None or an empty dict) or skip appending when
parsing fails, and keep logging the error via `logger.error`; ensure
`results_list.append(parsed)` only runs when `parsed` is defined/valid to
prevent UnboundLocalError.

In `@backend/app/modules/facts_check/web_search.py`:
- Around line 35-40: The code currently checks only for the presence of the
"items" key and then indexes res["items"][0], which will raise IndexError when
items is an empty list; replace that logic by reading items = res.get("items",
[]) and then if not items: return [] to handle empty lists (or None) safely,
then populate first using items[0]["title"], items[0]["link"],
items[0]["snippet"]; update the block that references res and first accordingly
so it never indexes into an empty items array.

In `@frontend/app/analyze/results/page.tsx`:
- Around line 93-103: Wrap the axios.post call that sends to
`${BACKEND}/api/chat` in a try/catch around the existing code in page.tsx (the
block that creates `res` and `data`), detect HTTP 401 specifically via
`err.response?.status === 401` and network/other errors via lack of
`err.response`, and in each case update UI state instead of letting the
exception bubble: call `setMessages` (or a dedicated error state) to append an
assistant/system message describing the error (e.g., "Authentication error:
please check API key" for 401, "Network error: please try again" for network
failures), ensure any loading flags are cleared, and optionally log the full
error to console for debugging; keep references to `newMessages`, `setMessages`,
`getActiveApiKey`, `getActiveModel`, and `analysisData` when implementing the
catch handling so the flow continues gracefully.

---

Nitpick comments:
In `@backend/app/logging/logging_config.py`:
- Around line 28-32: Replace the broad "except Exception" with targeted
exceptions: catch OSError and AttributeError around the utf8_stream =
open(sys.stdout.fileno(), ...) and console_handler =
logging.StreamHandler(utf8_stream) block so only expected failures
(fileno()/open issues or missing fileno) fall through to the fallback
console_handler = logging.StreamHandler(sys.stdout); keep the same fallback
behavior and ensure utf8_stream and console_handler symbols are used exactly as
in the diff.

In `@backend/app/modules/pipeline.py`:
- Around line 67-70: Replace the hard-coded model string in
run_langgraph_workflow with a shared constant: define a single exported constant
(e.g., DEFAULT_GROQ_MODEL or GROQ_MODEL_DEFAULT) in your central
config/constants module and import it into this module, then change the function
signature of run_langgraph_workflow to use that constant as the default for
groq_model and update any other occurrences of "llama-3.3-70b-versatile" to
reference that same constant so all modules use the single source of truth.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68a64c3 and 0392857.

⛔ Files ignored due to path filters (1)
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • backend/app/.gitignore
  • backend/app/logging/logging_config.py
  • backend/app/modules/bias_detection/check_bias.py
  • backend/app/modules/chat/llm_processing.py
  • backend/app/modules/facts_check/llm_processing.py
  • backend/app/modules/facts_check/web_search.py
  • backend/app/modules/langgraph_builder.py
  • backend/app/modules/langgraph_nodes/fact_check.py
  • backend/app/modules/langgraph_nodes/generate_perspective.py
  • backend/app/modules/langgraph_nodes/judge.py
  • backend/app/modules/langgraph_nodes/sentiment.py
  • backend/app/modules/pipeline.py
  • backend/app/routes/routes.py
  • backend/app/utils/fact_check_utils.py
  • backend/pyproject.toml
  • frontend/app/analyze/loading/page.tsx
  • frontend/app/analyze/page.tsx
  • frontend/app/analyze/results/page.tsx
  • frontend/app/page.tsx
  • frontend/components/settings-modal.tsx

Comment on lines +36 to 38
def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"):
client = Groq(api_key=api_key)
try:
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard client initialization inside error-handling path.

At Line 37, Groq(api_key=api_key) executes before the try, so init/validation errors bypass your structured error response.

Proposed fix
 def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"):
-    client = Groq(api_key=api_key)
     try:
+        if not api_key:
+            raise ValueError("Missing Groq API key")
+        client = Groq(api_key=api_key)
         logger.debug(f"Raw article text: {text}")
         logger.debug(f"JSON dump of text: {json.dumps(text)}")
📝 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
def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"):
client = Groq(api_key=api_key)
try:
def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"):
try:
if not api_key:
raise ValueError("Missing Groq API key")
client = Groq(api_key=api_key)
logger.debug(f"Raw article text: {text}")
logger.debug(f"JSON dump of text: {json.dumps(text)}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/bias_detection/check_bias.py` around lines 36 - 38, The
Groq client is being constructed before the try block in check_bias, so
initialization/validation errors escape your structured error handling; move the
Groq(api_key=api_key) call inside the try (or add a surrounding try/except that
wraps the instantiation) and handle exceptions there (using the same error
response path you use for subsequent calls), referencing the check_bias function
and the Groq client instantiation to ensure any instantiation errors are caught
and returned in the structured error response.

def ask_llm(question, docs, api_key: str, groq_model: str = "llama-3.3-70b-versatile", article_text: str = ""):
client = Groq(api_key=api_key)
pinecone_context = build_context(docs)
logger.debug(f"Generated context for LLM:\n{pinecone_context}")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging full LLM context payloads.

Line 46 logs raw context text, which can leak user/article content into logs and create very large log entries. Log size/metadata instead of full text.

🔧 Suggested change
-    logger.debug(f"Generated context for LLM:\n{pinecone_context}")
+    logger.debug("Generated LLM context from Pinecone notes (chars=%d)", len(pinecone_context))
📝 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
logger.debug(f"Generated context for LLM:\n{pinecone_context}")
logger.debug("Generated LLM context from Pinecone notes (chars=%d)", len(pinecone_context))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/chat/llm_processing.py` at line 46, The current
logger.debug call logs the full LLM context in pinecone_context which can leak
sensitive text and create huge logs; modify the logging in llm_processing.py to
avoid printing raw content by replacing the logger.debug(f"Generated context for
LLM:\n{pinecone_context}") call with a metadata-only log: record the length/size
(e.g., len(pinecone_context) or number of chunks), an optional content hash
(e.g., SHA256) or first N characters only if needed for debugging, and any
token/count metrics instead of the full pinecone_context string; update the code
paths that reference logger.debug and ensure pinecone_context is never logged in
full.

Comment on lines +39 to 40
client = Groq(api_key=state["groq_api_key"])
try:
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard API key access inside try in claim extractor.

Line 39 can throw KeyError before structured error handling starts.

Suggested fix
 def run_claim_extractor_sdk(state):
-    client = Groq(api_key=state["groq_api_key"])
     try:
+        api_key = state.get("groq_api_key")
+        if not api_key:
+            raise ValueError("Missing 'groq_api_key' in state")
+        client = Groq(api_key=api_key)
         text = state.get("cleaned_text")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/facts_check/llm_processing.py` around lines 39 - 40, The
code instantiates Groq with state["groq_api_key"] outside the try block which
can raise KeyError before your structured error handling runs; inside the claim
extractor function, move the API key access into the existing try (or guard it
with state.get and raise a controlled error) and then construct client =
Groq(api_key=api_key) within that try so any missing-key exception is caught and
handled (or explicitly check for the key first and call your error/log path if
absent). Ensure you reference the same identifiers (state, Groq, client) and
preserve the existing error logging/handling flow.

Comment on lines +55 to +56
groq_api_key: str
groq_model: str
Copy link
Contributor

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

🏁 Script executed:

# First, find and read the langgraph_builder.py file
fd langgraph_builder.py

Repository: AOSSIE-Org/Perspective

Length of output: 106


🏁 Script executed:

# Once we have the file, let's examine the context around lines 55-56
head -80 backend/app/modules/langgraph_builder.py | tail -40

Repository: AOSSIE-Org/Perspective

Length of output: 1105


🏁 Script executed:

# Search for groq_api_key usage throughout the codebase
rg -n 'groq_api_key' --type py -B2 -A2

Repository: AOSSIE-Org/Perspective

Length of output: 2957


🏁 Script executed:

# Search for groq_model usage
rg -n 'groq_model' --type py -B2 -A2

Repository: AOSSIE-Org/Perspective

Length of output: 7629


🏁 Script executed:

# Search for potential persistence/storage operations
rg -n 'store_and_send|JSONResponse|return\s+\{|insert|upsert|save|persist|log' backend/app/modules/langgraph_builder.py -B2 -A2

Repository: AOSSIE-Org/Perspective

Length of output: 1230


🏁 Script executed:

# Check for any JSON/dict serialization that might expose the key
rg -n 'json\.|dumps\|to_dict\|serialize' backend/app/modules/langgraph_builder.py -B2 -A2

Repository: AOSSIE-Org/Perspective

Length of output: 48


🏁 Script executed:

# Find store_and_send.py
fd store_and_send.py

Repository: AOSSIE-Org/Perspective

Length of output: 119


🏁 Script executed:

# Read the store_and_send implementation
cat backend/app/modules/langgraph_nodes/store_and_send.py

Repository: AOSSIE-Org/Perspective

Length of output: 1815


🏁 Script executed:

# Check routes.py to see what data is returned from API endpoints
cat backend/app/routes/routes.py

Repository: AOSSIE-Org/Perspective

Length of output: 3748


🏁 Script executed:

# Search for any logging statements that might expose groq_api_key
rg -n 'logger\.|print\(' backend/app/modules/langgraph_builder.py backend/app/modules/langgraph_nodes/ backend/app/modules/pipeline.py -A2

Repository: AOSSIE-Org/Perspective

Length of output: 3450


🏁 Script executed:

# Search for return statements that might include the entire state
rg -n 'return\s+\{|return\s+result|return\s+data' backend/app/routes/routes.py -B3 -A3

Repository: AOSSIE-Org/Perspective

Length of output: 750


🏁 Script executed:

# Find chunk_rag_data.py
fd chunk_rag_data.py

Repository: AOSSIE-Org/Perspective

Length of output: 116


🏁 Script executed:

# Read the chunk_rag_data implementation
cat backend/app/modules/vector_store/chunk_rag_data.py

Repository: AOSSIE-Org/Perspective

Length of output: 3488


🏁 Script executed:

# Check what fields are actually needed by chunk_rag_data
rg -n 'state\[|state\.get' backend/app/modules/vector_store/chunk_rag_data.py

Repository: AOSSIE-Org/Perspective

Length of output: 48


Strip groq_api_key and groq_model from state before returning from store_and_send node.

The API key is currently exposed in two critical ways:

  1. Logged in store_and_send.py (line 29): logger.debug(f"Received state for vector storage: {state}") logs the entire state including the API key to application logs.

  2. Returned to clients in HTTP response: The /process endpoint returns the entire workflow state (via pipeline.pyroutes.py line 84), which includes groq_api_key in the JSON response body. This violates BYOK principles by leaking user credentials to clients.

Fix: In store_and_send.py line 53, filter the returned state before returning:

return {k: v for k, v in state.items() if k not in ("groq_api_key", "groq_model")}, "status": "success"}

Also remove the debug log at line 29 or exclude sensitive fields from it.

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

In `@backend/app/modules/langgraph_builder.py` around lines 55 - 56, The
store_and_send node is leaking sensitive fields (groq_api_key and groq_model) by
logging the full state and returning it; update the store_and_send function to
(1) remove or redact these keys before logging (or remove the debug log
entirely) and (2) filter them out of the returned state so the HTTP response
from the /process pipeline does not include groq_api_key or groq_model; locate
the store_and_send implementation and replace the return value with a sanitized
dict that excludes "groq_api_key" and "groq_model" (and ensure
pipeline.py/routes.py continue to receive only the sanitized state).

Comment on lines +35 to +38
# Soft failure — web search quota/key issue. Continue with empty facts
# so the rest of the pipeline (generate_perspective, store_and_send) still runs.
logger.warning(f"Fact-checking skipped (non-fatal): {error_message}")
verifications = []
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging raw fact-check error payloads.

Line 37 logs error_message verbatim. This can leak sensitive upstream details into logs. Prefer a sanitized/structured message without raw provider text.

Suggested fix
-            logger.warning(f"Fact-checking skipped (non-fatal): {error_message}")
+            logger.warning("Fact-checking skipped (non-fatal). reason=%s", "upstream_fact_check_failure")
📝 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
# Soft failure — web search quota/key issue. Continue with empty facts
# so the rest of the pipeline (generate_perspective, store_and_send) still runs.
logger.warning(f"Fact-checking skipped (non-fatal): {error_message}")
verifications = []
# Soft failure — web search quota/key issue. Continue with empty facts
# so the rest of the pipeline (generate_perspective, store_and_send) still runs.
logger.warning("Fact-checking skipped (non-fatal). reason=%s", "upstream_fact_check_failure")
verifications = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/langgraph_nodes/fact_check.py` around lines 35 - 38, The
warning currently logs the raw upstream payload via
logger.warning(f"Fact-checking skipped (non-fatal): {error_message}") which can
leak sensitive provider text; change the logging in fact_check.py to emit a
sanitized/structured message instead (e.g., include a fixed description,
optional provider identifier or error code if available, and omit or truncate
the raw error_message) while preserving the soft-failure behavior that sets
verifications = [] so the pipeline continues; update the logger call to use the
sanitized fields rather than logging error_message verbatim.

"groq>=0.28.0",
"langchain>=0.3.25",
"langchain-community>=0.3.25",
"langchain-google-genai>=2.1.12",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for actual usages of langchain-google-genai in Python sources
rg -n --type=py -C2 'langchain_google_genai|ChatGoogleGenerativeAI|GoogleGenerativeAI'

Repository: AOSSIE-Org/Perspective

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Also search for imports from langchain packages to get broader context
rg -n --type=py 'from langchain.*import|import langchain' | head -50

Repository: AOSSIE-Org/Perspective

Length of output: 428


🏁 Script executed:

#!/bin/bash
# Check the pyproject.toml to see all dependencies and any optional groups
cat backend/pyproject.toml

Repository: AOSSIE-Org/Perspective

Length of output: 780


🏁 Script executed:

#!/bin/bash
# Check for any references to google or genai (case-insensitive) in Python files
# to catch indirect references or commented code
rg -in --type=py 'google.*genai|genai.*google'

Repository: AOSSIE-Org/Perspective

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check if there are any imports in __init__.py or similar that might re-export it
fd -t f '\.py$' | xargs grep -l 'google_genai\|GoogleGenerativeAI' 2>/dev/null || echo "No matches found"

Repository: AOSSIE-Org/Perspective

Length of output: 82


Remove the unused langchain-google-genai dependency from backend/pyproject.toml.

The langchain-google-genai package (line 16) is not imported or used anywhere in the codebase. Since the PR's focus is on Groq BYOK integration (which uses langchain_groq), this dependency should be removed to reduce unnecessary supply-chain surface area.

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

In `@backend/pyproject.toml` at line 16, Remove the unused dependency string
"langchain-google-genai>=2.1.12" from the project dependencies (the entry shown
in the diff) and update the project's dependency lockfile (poetry.lock or
equivalent) so the removed package is no longer pinned; ensure no import or
usage of langchain_google_genai remains in the codebase before committing the
change.

import { getActiveApiKey, getActiveModel } from "@/components/settings-modal";

// const backend_url = process.env.NEXT_PUBLIC_API_URL;
const BACKEND = "http://localhost:7860";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hardcoded backend origin in client code.

http://localhost:7860 at Line 20 will fail outside local dev and is likely to break production/staging.

Proposed fix
- const BACKEND = "http://localhost:7860";
+ const BACKEND =
+   process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860";
📝 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
const BACKEND = "http://localhost:7860";
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` at line 20, The BACKEND constant in
page.tsx is hardcoded to "http://localhost:7860"; change it to read from a
configurable client-side environment variable (e.g. NEXT_PUBLIC_BACKEND) with a
safe fallback to the current origin/relative path so it works in
staging/production and during local dev; update the const BACKEND declaration to
use process.env.NEXT_PUBLIC_BACKEND (or equivalent runtime config) and ensure
code that uses BACKEND (in this file) handles undefined by building a URL from
window.location.origin or using a relative path so no hardcoded origin remains.

import axios from "axios";
import { getActiveApiKey, getActiveModel } from "@/components/settings-modal";

const BACKEND = "http://localhost:7860";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use environment-configured backend URL instead of localhost literal.

Hardcoding http://localhost:7860 in client code creates immediate deployment fragility.

Proposed fix
- const BACKEND = "http://localhost:7860";
+ const BACKEND =
+   process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860";
📝 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
const BACKEND = "http://localhost:7860";
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` at line 25, Replace the hardcoded
BACKEND constant in frontend/app/analyze/results/page.tsx with an
environment-configured value: read the client-facing env var (e.g.
process.env.NEXT_PUBLIC_BACKEND_URL or NEXT_PUBLIC_API_URL) and use that for
BACKEND, falling back to a sensible default (such as a relative path) when the
env var is absent; update any code that imports/uses BACKEND to rely on this
env-driven value so deployments can configure the backend URL without changing
source.

Comment on lines +109 to +114
useEffect(() => {
const current = PROVIDERS[provider as keyof typeof PROVIDERS];
setModel(current.defaultModel);
setShowKey(false);
setError("");
}, [provider]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Saved model can be overwritten during initialization.

The provider effect resets model to default unconditionally; this can wipe a previously saved activeModel from config on mount.

Proposed fix
+ const [configLoaded, setConfigLoaded] = useState(false);

  useEffect(() => {
    const config = loadApiConfig();
    if (config) {
      const savedProvider = config.activeProvider in PROVIDERS ? config.activeProvider : "groq";
      setProvider(savedProvider);
      setModel(config.activeModel || PROVIDERS.groq.defaultModel);
      setKeys((prev) => ({ ...prev, ...config.keys }));
    }
+   setConfigLoaded(true);
    if (!isConfigured() || forceOpen) {
      setFirstRun(!isConfigured());
      setOpen(true);
    }
  }, [forceOpen]);

  useEffect(() => {
+   if (!configLoaded) return;
    const current = PROVIDERS[provider as keyof typeof PROVIDERS];
    setModel(current.defaultModel);
    setShowKey(false);
    setError("");
- }, [provider]);
+ }, [provider, configLoaded]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/settings-modal.tsx` around lines 109 - 114, The effect
that runs on provider change unconditionally calls
setModel(current.defaultModel), which can overwrite a previously saved
activeModel in config; update the useEffect to first check for an existing saved
model (e.g., config.activeModel or a provider-scoped saved model) and only call
setModel(current.defaultModel) when no saved model exists or when the saved
model belongs to a different provider, leaving the saved activeModel intact;
keep the existing setShowKey and setError resets but do not clobber model if a
saved activeModel is present.

Comment on lines +167 to +169
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-800 p-3 text-sm text-blue-800 dark:text-blue-200">
To use Perspective, select a free AI provider and paste your API key. Your key is stored only in your browser — never sent to our servers.
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

First-run privacy copy is factually incorrect.

Line 168 says the key is “never sent to our servers”, but requests explicitly forward it in X-BYOK-Api-Key. This is a compliance/privacy messaging risk.

Suggested wording
- To use Perspective, select a free AI provider and paste your API key. Your key is stored only in your browser — never sent to our servers.
+ To use Perspective, select a free AI provider and paste your API key. Your key is stored in your browser and only forwarded with your analysis requests; it is not persisted server-side.
📝 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
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-800 p-3 text-sm text-blue-800 dark:text-blue-200">
To use Perspective, select a free AI provider and paste your API key. Your key is stored only in your browser never sent to our servers.
</div>
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-800 p-3 text-sm text-blue-800 dark:text-blue-200">
To use Perspective, select a free AI provider and paste your API key. Your key is stored in your browser and only forwarded with your analysis requests; it is not persisted server-side.
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/settings-modal.tsx` around lines 167 - 169, The privacy
copy inside the settings-modal component (the rounded info div in
frontend/components/settings-modal.tsx) is incorrect; update the text to
accurately state that the API key is stored in the browser but may be forwarded
to selected AI providers via requests (for example via the X-BYOK-Api-Key
header) rather than claiming it is “never sent to our servers”; locate the div
rendering the message and replace the sentence with a concise, factual line
noting local storage of the key and that requests to providers may include the
key in headers so users understand it can be transmitted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant