Skip to content

Add Kaggle models integration plan and scaffold - #346

Open
MasumRab wants to merge 9 commits into
mainfrom
jules-kaggle-gemma-integration-9750286105324430980
Open

Add Kaggle models integration plan and scaffold#346
MasumRab wants to merge 9 commits into
mainfrom
jules-kaggle-gemma-integration-9750286105324430980

Conversation

@MasumRab

@MasumRab MasumRab commented Feb 28, 2026

Copy link
Copy Markdown
Owner
  • Update docs/tasks/06_GEMMA_INTEGRATION.md to include steps for Kagglehub and KerasNLP.
  • Add KaggleGemmaClient reference implementation to backend/examples/gemma_providers.py.

PR created automatically by Jules for task 9750286105324430980 started by @MasumRab

Summary by Sourcery

Add a Kaggle-based Gemma client example and extend the Gemma integration plan to cover Kaggle Models and related configuration.

New Features:

  • Introduce KaggleGemmaClient example for running local Gemma models downloaded via Kagglehub and executed with KerasNLP.

Enhancements:

  • Expand Gemma integration task documentation to include Kaggle Models dependencies, configuration options, and client integration steps.

Summary by CodeRabbit

  • New Features

    • Configurable research settings (max loops) and improved deep research agent for richer results
    • Filesystem-based tools for reading/writing/listing files from the agent
    • Search improvements with tuned fallback behavior for more reliable results
  • Bug Fixes

    • Hardened environment validation and startup checks for required keys
    • Improved error handling and safer streaming/error responses
  • Chores

    • Added CI workflows and local pre-commit hooks; standardized formatting tooling (Prettier) across frontend/backend

*   Update docs/tasks/06_GEMMA_INTEGRATION.md to include steps for Kagglehub and KerasNLP.
*   Add KaggleGemmaClient reference implementation to backend/examples/gemma_providers.py.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@trunk-io

trunk-io Bot commented Feb 28, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

@sourcery-ai

sourcery-ai Bot commented Feb 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a Kaggle-based Gemma client example using Kagglehub + KerasNLP and updates the Gemma integration task doc to include Kaggle as a first-class provider with required dependencies and configuration keys.

Sequence diagram for KaggleGemmaClient initialization and text generation

sequenceDiagram
    actor Developer
    participant App as Application
    participant Config as AppConfig
    participant Client as KaggleGemmaClient
    participant OS as os_environ
    participant KH as kagglehub
    participant KCfg as keras_config
    participant KNLP as keras_nlp_models
    participant LLM as GemmaCausalLM

    Developer->>Config: Set GEMMA_PROVIDER=kaggle
    Developer->>Config: Set GEMMA_MODEL_NAME=google/gemma-2/keras/gemma2-2b-en
    Developer->>OS: Set KAGGLE_USERNAME and KAGGLE_KEY

    App->>Client: new KaggleGemmaClient(model_handle)
    activate Client
    Client->>OS: get(KAGGLE_USERNAME), get(KAGGLE_KEY)
    OS-->>Client: values or None
    Client->>Client: print warning if missing
    Client->>KH: model_download(model_handle)
    KH-->>Client: model_path
    Client->>KCfg: set_floatx(bfloat16)
    KCfg-->>Client: ok
    Client->>KNLP: GemmaCausalLM.from_preset(model_path)
    KNLP-->>Client: LLM instance
    Client->>Client: store llm
    deactivate Client
    App-->>Developer: KaggleGemmaClient ready

    Developer->>App: Request completion(prompt)
    App->>Client: generate(prompt, max_length)
    activate Client
    Client->>LLM: generate(prompt, max_length)
    LLM-->>Client: generated_text
    Client-->>App: generated_text
    deactivate Client
    App-->>Developer: generated_text
Loading

Class diagram for KaggleGemmaClient Gemma provider

classDiagram
    class KaggleGemmaClient {
        - str model_handle
        - str model_path
        - any llm
        + KaggleGemmaClient(model_handle: str)
        + generate(prompt: str, max_length: int, **kwargs) str
    }

    class kagglehub {
        + model_download(model_handle: str) str
    }

    class keras_nlp_models {
        + GemmaCausalLM_from_preset(preset: str) any
    }

    class keras_config {
        + set_floatx(dtype: str) void
    }

    KaggleGemmaClient ..> kagglehub : uses
    KaggleGemmaClient ..> keras_nlp_models : uses
    KaggleGemmaClient ..> keras_config : configures
Loading

File-Level Changes

Change Details Files
Introduce KaggleGemmaClient example that downloads Gemma models via Kagglehub and runs them with KerasNLP.
  • Add KaggleGemmaClient class that authenticates against Kaggle (via env vars), downloads a Gemma model with kagglehub, configures keras floatx, and instantiates a GemmaCausalLM from a local preset path.
  • Implement a simple generate method on KaggleGemmaClient that wraps GemmaCausalLM.generate with a prompt and max_length parameter.
  • Handle missing dependencies by raising an ImportError with guidance to install kagglehub, keras, and keras-nlp.
backend/examples/gemma_providers.py
Extend Gemma integration documentation to cover Kaggle-based models and configuration.
  • Document additional Python dependencies needed for Kaggle Models integration (kagglehub, keras, keras-nlp).
  • Update GEMMA_PROVIDER and GEMMA_MODEL_NAME examples to include the Kaggle provider and a Kaggle Gemma handle.
  • Add KAGGLE_USERNAME and KAGGLE_KEY as required configuration for Kagglehub authentication.
  • Add a new checklist step to integrate KaggleGemmaClient using kagglehub and keras_nlp for direct inference.
docs/tasks/06_GEMMA_INTEGRATION.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@MasumRab, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c59180d-f14c-48be-8dfd-d9fbcb1a64cc

📥 Commits

Reviewing files that changed from the base of the PR and between b1e198e and 235dc5a.

📒 Files selected for processing (9)
  • .github/workflows/validate-env.yml
  • backend/examples/gemma_providers.py
  • backend/src/agent/deep_search_agent.py
  • backend/src/agent/graph.py
  • backend/src/agent/nodes.py
  • backend/src/evaluation/metrics.py
  • backend/src/rag/chroma_store.py
  • backend/src/search/providers/tavily_adapter.py
  • frontend/src/components/ChatMessagesView.tsx

Walkthrough

This PR adds CI/workflow and pre-commit configs, modernizes type hints, refactors and extends the agent/research/RAG/search stacks (new DeepResearchAgent, kg_enrich, compression, dual-write RAG/Chroma support), expands config/.env examples (LangSmith placeholders, MAX_RESEARCH_LOOPS), introduces new Gemma providers, and applies broad frontend stylistic and UI refinements.

Changes

Cohort / File(s) Summary
CI / Pre-commit
.github/workflows/pr-check.yml, .github/workflows/push-check.yml, .github/workflows/validate-env.yml, .pre-commit-config.yaml, ruff.toml
Add PR and push GitHub Actions (frontend tests/ESLint/TS checks, backend ruff checks), env validation workflow (GEMINI_API_KEY presence), and local pre-commit hooks; update ruff config and per-file ignores.
Environment & Config
.env.example, backend/.env.example, backend/src/config/app_config.py, backend/src/config/validation.py
Convert active env vars to commented guidance, add MAX_RESEARCH_LOOPS and LangSmith placeholders, expand AppConfig with RAG/Chroma/search/model/provider settings and new fields.
Deep Research / Graphs / Orchestration
backend/src/agent/deep_search_agent.py, backend/src/agent/graph.py, backend/src/agent/graph_builder.py, backend/src/agent/graphs/*, backend/src/agent/orchestration.py
Introduce DeepResearchAgent and WebSearcher; restructure graph wiring (kg_enrich, compression_node, research_subgraph, reflection_router, supervisor/linear variants); update orchestration types and Tool/Agent registries.
RAG / Persistence / Chroma
backend/src/agent/rag.py, backend/src/rag/chroma_store.py, backend/src/agent/rag_nodes.py, backend/src/agent/mcp_persistence.py
Add dual-write/chroma support, embedding consistency wrapper, new EvidenceChunk fields (relevance_score, timestamp, chunk_id), enhanced retrieval/ingest/audit/prune APIs and guards.
LLM / Gemma Clients & Providers
backend/src/agent/gemma_client.py, backend/examples/gemma_providers.py, backend/src/agent/models.py, backend/src/agent/llm_client.py
Add KaggleGemmaClient example, VertexAIGemmaClient improvements, is_gemma/is_gemini helpers, GemmaAdapter typing/behavior clarifications and robust invoke handling.
Agent Nodes, Tools & MCP
backend/src/agent/nodes.py, backend/src/agent/mcp_*, backend/src/agent/mcp_server.py, backend/src/agent/tool_adapter.py, backend/src/agent/tools_and_schemas.py
Add Pydantic models (SearchInput, EvidenceItem/List), rate-limited LLM factory, stronger MCP server (FilesystemMCPServer) with safety limits, improved parse_tool_calls and tool schema handling.
Search Providers & Router
backend/src/search/*.py, backend/src/search/providers/*
Thread-safe lazy provider init, primary/fallback provider resolution and tuned/untuned attempts; update adapters (Bing/Brave/Google/DuckDuckGo/Tavily) with env/key handling and richer logging.
Rate Limiting, State, Utilities
backend/src/agent/rate_limiter.py, backend/src/agent/state.py, backend/src/agent/utils.py, backend/src/agent/_graph.py, backend/src/agent/app.py
Typing modernizations, ContextWindowManager signature updates, 1s cleanup throttle for RateLimiter, new Todo.result, improved multi-message research-topic extraction and docstrings/refinements.
Observability & Benchmarks
backend/src/observability/*.py, backend/src/evaluation/*
Langfuse handler guards, observe_span behavior updates; benchmark enhancements (tqdm, better error handling, docstrings).
Frontend: UI, Tests, Formatting
frontend/src/**, frontend/.prettierrc.json, frontend/package.json, frontend/eslint.config.js
Wide stylistic normalization (single quotes, remove semicolons), add Prettier and scripts, update ESLint ignores; UI changes: InputForm -> extracted InputControls, expanded model/effort selects, ChatMessagesView link target/accessibility tweak, ArtifactView/ActivityTimeline refactors; many test updates to match DOM mocks.
Lint/Style Utilities & Small Scripts
backend/pyproject.toml, patch_rag.py
Add per-file lint ignore entries and a small patch_rag.py script to fix a rag.py merge-conflict-style signature block.

Sequence Diagram

sequenceDiagram
  participant Client as Client/UI
  participant Agent as DeepResearchAgent
  participant LLM as LLM (Gemma/Gemini)
  participant Search as WebSearcher/SearchProviders
  participant RAG as RAG/VectorStore (FAISS/Chroma)
  participant Synth as Synthesizer

  Client->>Agent: run(topic)
  Agent->>LLM: _plan_queries(topic) (prompt -> JSON queries)
  LLM-->>Agent: list of queries
  Agent->>Search: search(query) [parallel per query]
  Search-->>Agent: results (title,url,snippet,content)
  Agent->>RAG: ingest_research_results(results, subgoal_id)
  RAG-->>Agent: ingestion confirmations / embeddings
  Agent->>LLM: synthesize results (SYNTHESIS_PROMPT + context)
  LLM-->>Agent: synthesized answer (with citations)
  Agent-->>Client: final answer + sources
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nimble paws,
New graphs and searches, shiny laws,
I planted prompts and fetched the web’s light,
Stored bits in vectors through moonlit night,
Now the project hums — a rabbit’s small delight! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add Kaggle models integration plan and scaffold' accurately reflects the primary changes: documentation updates for Kaggle integration and a new KaggleGemmaClient example implementation.
Docstring Coverage ✅ Passed Docstring coverage is 97.24% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jules-kaggle-gemma-integration-9750286105324430980

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.

@mergify

mergify Bot commented Feb 28, 2026

Copy link
Copy Markdown

🧪 CI Insights

Here's what we observed from your CI run for 8171dc9.

🟢 All jobs passed!

But CI Insights is watching 👀

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Consider avoiding side-effectful work (downloading the model, global keras.config.set_floatx) in the constructor; a lazy-loading pattern or explicit load() method would give callers more control over when these expensive/global operations occur.
  • Replace print statements in KaggleGemmaClient with the project’s logging mechanism so logs can be controlled consistently across providers and environments.
  • Ensure KaggleGemmaClient.generate returns the same type/shape as other Gemma clients (e.g., plain string vs tensor/list) so the higher-level LLMClient-style interface can treat all providers uniformly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider avoiding side-effectful work (downloading the model, global `keras.config.set_floatx`) in the constructor; a lazy-loading pattern or explicit `load()` method would give callers more control over when these expensive/global operations occur.
- Replace `print` statements in `KaggleGemmaClient` with the project’s logging mechanism so logs can be controlled consistently across providers and environments.
- Ensure `KaggleGemmaClient.generate` returns the same type/shape as other Gemma clients (e.g., plain string vs tensor/list) so the higher-level `LLMClient`-style interface can treat all providers uniformly.

## Individual Comments

### Comment 1
<location path="backend/examples/gemma_providers.py" line_range="252" />
<code_context>
+            print(f"Model path: {self.model_path}")
+
+            # Set floatx for efficiency if desired
+            keras.config.set_floatx("bfloat16")
+
+            # Initialize the causal language model via Keras NLP
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid mutating global floatx inside a library-style client, or make it configurable.

Calling `keras.config.set_floatx("bfloat16")` in the constructor changes global process-wide config and may unexpectedly affect other code that relies on the default float type. Instead, consider accepting the desired dtype as a constructor parameter so callers can opt in, or omit this change and rely on existing global/environment configuration.

Suggested implementation:

```python
            # Download the model weights and assets via kagglehub
            print(f"Downloading/Locating model {model_handle} via kagglehub...")
            self.model_path = kagglehub.model_download(model_handle)
            print(f"Model path: {self.model_path}")

            # Initialize the causal language model via Keras NLP
            # Since keras_nlp uses presets, we load it using the downloaded path
            print(f"Loading Gemma model via Keras NLP from {self.model_path}...")
            # Note: For custom downloaded paths, from_preset can point to a local directory
            self.llm = keras_nlp.models.GemmaCausalLM.from_preset(self.model_path)

```

If you want to make the dtype configurable instead of relying solely on global/environment configuration, you can:
1. Add a constructor parameter to the `KaggleGemmaClient` (e.g., `dtype: str | None = None`) and store it as `self.dtype`.
2. Document that callers can configure Keras globally before instantiating this client (e.g., `keras.config.set_floatx("bfloat16")` in application code), or use presets that already specify the desired dtype.
</issue_to_address>

### Comment 2
<location path="backend/examples/gemma_providers.py" line_range="274" />
<code_context>
+            max_length: Maximum length of the generated sequence.
+        """
+        # KerasNLP GemmaCausalLM 'generate' takes the prompt and max_length
+        output = self.llm.generate(prompt, max_length=max_length)
+        return output
</code_context>
<issue_to_address>
**issue (bug_risk):** Forward `**kwargs` to the underlying `llm.generate` to avoid silently discarding options.

`**kwargs` are accepted but not passed to `self.llm.generate`, so caller-specified options (e.g., temperature, top_p) are silently ignored. Please update the call to `self.llm.generate(prompt, max_length=max_length, **kwargs)` so all generation parameters are honored.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread backend/examples/gemma_providers.py Outdated
Comment thread backend/examples/gemma_providers.py Outdated
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@mergify

mergify Bot commented Feb 28, 2026

Copy link
Copy Markdown

👋 MasumRab your PR is conflicting and needs to be updated to be merged

Resolve formatting conflicts and address PR comments on KaggleGemmaClient implementation.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@MasumRab

Copy link
Copy Markdown
Owner Author

@jules pull lates update and address comments and conflits
Please address the comments from this code review:

Overall Comments

  • Consider avoiding side-effectful work (downloading the model, global keras.config.set_floatx) in the constructor; a lazy-loading pattern or explicit load() method would give callers more control over when these expensive/global operations occur.
  • Replace print statements in KaggleGemmaClient with the project’s logging mechanism so logs can be controlled consistently across providers and environments.
  • Ensure KaggleGemmaClient.generate returns the same type/shape as other Gemma clients (e.g., plain string vs tensor/list) so the higher-level LLMClient-style interface can treat all providers uniformly.

Individual Comments

Comment 1

+ print(f"Model path: {self.model_path}") + + # Set floatx for efficiency if desired + keras.config.set_floatx("bfloat16") + + # Initialize the causal language model via Keras NLP **suggestion (bug_risk):** Avoid mutating global floatx inside a library-style client, or make it configurable.

Calling keras.config.set_floatx("bfloat16") in the constructor changes global process-wide config and may unexpectedly affect other code that relies on the default float type. Instead, consider accepting the desired dtype as a constructor parameter so callers can opt in, or omit this change and rely on existing global/environment configuration.

Suggested implementation:

            # Download the model weights and assets via kagglehub
            print(f"Downloading/Locating model {model_handle} via kagglehub...")
            self.model_path = kagglehub.model_download(model_handle)
            print(f"Model path: {self.model_path}")

            # Initialize the causal language model via Keras NLP
            # Since keras_nlp uses presets, we load it using the downloaded path
            print(f"Loading Gemma model via Keras NLP from {self.model_path}...")
            # Note: For custom downloaded paths, from_preset can point to a local directory
            self.llm = keras_nlp.models.GemmaCausalLM.from_preset(self.model_path)

If you want to make the dtype configurable instead of relying solely on global/environment configuration, you can:

  1. Add a constructor parameter to the KaggleGemmaClient (e.g., dtype: str | None = None) and store it as self.dtype.
  2. Document that callers can configure Keras globally before instantiating this client (e.g., keras.config.set_floatx("bfloat16") in application code), or use presets that already specify the desired dtype.
    </issue_to_address>

Comment 2

+ max_length: Maximum length of the generated sequence. + """ + # KerasNLP GemmaCausalLM 'generate' takes the prompt and max_length + output = self.llm.generate(prompt, max_length=max_length) + return output **issue (bug_risk):** Forward `**kwargs` to the underlying `llm.generate` to avoid silently discarding options.

**kwargs are accepted but not passed to self.llm.generate, so caller-specified options (e.g., temperature, top_p) are silently ignored. Please update the call to self.llm.generate(prompt, max_length=max_length, **kwargs) so all generation parameters are honored.
</issue_to_address>

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (15)
backend/src/search/providers/google_adapter.py (1)

17-20: ⚠️ Potential issue | 🟡 Minor

Client is instantiated even when api_key is None.

Unlike TavilyAdapter which conditionally creates the client only when the API key exists, this adapter always instantiates Client(api_key=self.api_key) even after logging a warning that the key is missing. This may cause an immediate exception or a confusing deferred failure.

Consider guarding the client creation:

Suggested fix
         self.api_key = api_key or os.getenv("GEMINI_API_KEY")
         if not self.api_key:
             logger.warning("GEMINI_API_KEY not found. Google Search may fail.")
-        self.client = Client(api_key=self.api_key)
+            self.client = None
+        else:
+            self.client = Client(api_key=self.api_key)

Then add an early return in search:

def search(self, ...):
    if not self.client:
        logger.error("Google Search client not initialized (missing API key).")
        return []
    # ... rest of method
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/google_adapter.py` around lines 17 - 20, The
adapter currently instantiates Client(api_key=self.api_key) even when api_key is
None, risking exceptions; change GoogleAdapter to only create self.client when
self.api_key is truthy (mirroring TavilyAdapter), e.g. set self.client = None if
no key and otherwise Client(...), and update the search method to early-return
an empty list (and log an error) when self.client is None; reference symbols:
Client, self.api_key, search, and TavilyAdapter.
backend/src/agent/state.py (1)

23-31: ⚠️ Potential issue | 🔴 Critical

The two Todo definitions are intentional but reveal a critical bug in the transformation logic.

The Todo in state.py (TypedDict with id, task, status, result) is correctly separated from the Todo in tools_and_schemas.py (Pydantic with title, description, status) as they serve different purposes—one for LLM schema validation, one for internal state.

However, the conversion between them is inconsistent and buggy:

  • generate_plan (line 382) correctly maps: item.title"task", item.status"status", adds "result": None
  • update_plan (line 860) incorrectly maps: item.title"title", item.description"description", adds "query" — none of which match the expected state.Todo schema

This inconsistency will cause the plan_todos in update_plan to contain dict objects that don't conform to the state.Todo structure, leading to potential runtime errors when the state's todo_list is accessed.

Align the field mappings in update_plan (line 860+) to match the correct transformation used in generate_plan.

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

In `@backend/src/agent/state.py` around lines 23 - 31, The update_plan function
currently builds plan_todos with the LLM/Pydantic fields (e.g.,
item.title→"title", item.description→"description", adding "query"), which
mismatches the internal state.Todo shape; change update_plan's mapping to mirror
generate_plan: map item.title → "task", item.status → "status", set "result":
None (and include "id" if available or generate one), producing dicts that
conform to state.Todo so plan_todos and todo_list consume the same schema as
generate_plan.
backend/src/agent/rag.py (2)

211-212: ⚠️ Potential issue | 🔴 Critical

Chroma-only mode can crash on uninitialized next_id.

start_id = self.next_id executes even when FAISS is disabled, but self.next_id is only created in the FAISS init branch.

Proposed fix
-        start_id = self.next_id
+        start_id = self.next_id if self.use_faiss else 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/rag.py` around lines 211 - 212, The line "start_id =
self.next_id" assumes self.next_id exists even when FAISS is disabled; update
the initialization logic so self.next_id is always defined (e.g., set
self.next_id = 0 in the non-FAISS initialization branch) or guard the assignment
before use (e.g., in the method containing start_id = self.next_id, check
hasattr(self, 'next_id') and fallback to 0), ensuring any code paths in the
class (e.g., the RAG constructor or the method that sets start_id) reference a
valid next_id regardless of whether FAISS was initialized.

520-547: ⚠️ Potential issue | 🟠 Major

Stub config currently routes traffic into a guaranteed failure path.

_RAGConfig.enabled = True routes to RAG, but create_rag_tool() always returns None. This creates predictable error logging and wasted hops each run.

Proposed fix
 class _RAGConfig:
-    enabled = True
-    enable_fallback = True
+    enabled = False
+    enable_fallback = False
     max_documents = 5
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/rag.py` around lines 520 - 547, The config currently
advertises RAG as enabled but the legacy stub always returns None, causing
guaranteed failures; change _RAGConfig.enabled default to False and make
is_rag_enabled() return rag_config.enabled (not a hardcoded True) so runtime
checks reflect config, and update create_rag_tool(resources) to only log a
warning or attempt creation when rag_config.enabled is True (otherwise return
None silently or with a debug-level message); reference _RAGConfig.enabled,
rag_config, is_rag_enabled, and create_rag_tool when making these fixes.
backend/src/agent/configuration.py (1)

31-35: ⚠️ Potential issue | 🟡 Minor

answer_model description is inconsistent with the configured default.

Line 32 defaults to DEFAULT_ANSWER_MODEL (currently Gemma), but Line 34 says it uses Gemini 2.5 Pro. This metadata will mislead users and UI consumers.

🔧 Proposed fix
     answer_model: str = Field(
         default=DEFAULT_ANSWER_MODEL,
         json_schema_extra={
-            "description": "The name of the language model to use for the agent's answer. Uses Gemini 2.5 Pro for highest quality synthesis with advanced reasoning."
+            "description": "The name of the language model to use for the agent's answer. Defaults to the configured DEFAULT_ANSWER_MODEL."
         },
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/configuration.py` around lines 31 - 35, The
json_schema_extra description for the Field answer_model currently claims "Uses
Gemini 2.5 Pro", which is inconsistent with the configured DEFAULT_ANSWER_MODEL;
update the description in the answer_model Field (where json_schema_extra is
defined) to accurately reflect the actual default (DEFAULT_ANSWER_MODEL) or
change DEFAULT_ANSWER_MODEL to match the described model—ensure the description
references the DEFAULT_ANSWER_MODEL symbol or the correct model name so
UI/consumers are not misled.
backend/src/agent/models.py (1)

193-198: ⚠️ Potential issue | 🟡 Minor

Tighten Gemini detection to avoid model-family misclassification.

Line 198 currently treats any model containing "google" as Gemini. That can route non-Gemini Google models through Gemini-specific handling.

🔧 Proposed fix
 def is_gemini_model(model_name: str) -> bool:
     """Check if the model is a Gemini model (supports native tool binding)."""
     if not model_name:
         return False
     name = model_name.lower()
-    return "gemini" in name or "google" in name
+    return "gemini" in name
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/models.py` around lines 193 - 198, The current
is_gemini_model function is too broad because it treats any model_name
containing "google" as Gemini; update is_gemini_model to only return True when
the name clearly indicates the Gemini family by keeping the "gemini" check and
replacing the naive "google" substring check with a stricter match (e.g., use a
regex to match "google" as a vendor token or as a prefix with a delimiter such
as '-', '/', ':' — for example match r'(^|[^a-zA-Z0-9])google(?=[^a-zA-Z0-9]|$)'
or require model_name.startswith("google-")/startswith("google/") ) so
non-Google models that merely contain the string "google" are not misclassified.
backend/src/agent/kg.py (1)

49-53: ⚠️ Potential issue | 🟠 Major

Allowlist domain check is too permissive (substring match).

any(allowed in domain ...) admits unintended domains. Use parsed host comparison with exact/suffix matching.

💡 Proposed fix
+from urllib.parse import urlparse
@@
-        domain = url.split("//")[-1].split("/")[0]
-
-        if not any(allowed in domain for allowed in app_config.kg_allowlist):
+        host = (urlparse(url).hostname or "").lower()
+        allowlist = tuple(d.lower() for d in app_config.kg_allowlist)
+        if not any(host == d or host.endswith(f".{d}") for d in allowlist):
             continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/kg.py` around lines 49 - 53, The current allowlist uses
substring matching on domain (see url_match, domain, app_config.kg_allowlist)
which permits unintended hosts; instead parse the URL host (use
urllib.parse.urlparse and hostname/netloc) and compare against the allowlist
using exact equality or a proper suffix check that enforces dot-boundaries
(e.g., allowed == hostname or hostname.endswith("." + allowed)) to avoid
substring matches that cross label boundaries and strip ports before comparison;
replace the any(allowed in domain ...) logic with this hostname-based
exact/suffix matching.
backend/src/agent/router.py (1)

24-34: ⚠️ Potential issue | 🟠 Major

agent_mode selection is effectively unreachable with current config parsing.

Configuration.from_runnable_config only materializes declared model fields. Since agent_mode is not part of that model (per provided backend/src/agent/configuration.py snippet), this branch defaults to "parallel" and never routes to linear/supervisor.

💡 Proposed fix
 def select_agent(state: OverallState, config: RunnableConfig) -> str:
-    configurable = Configuration.from_runnable_config(config)
-    # Default to parallel if not specified
-    mode = getattr(configurable, "agent_mode", "parallel")
+    configurable = config.get("configurable", {}) if config else {}
+    mode = str(configurable.get("agent_mode", "parallel")).lower()

     if mode == "linear":
         return "linear_agent"
     elif mode == "supervisor":
         return "supervisor_agent"
     else:
         return "parallel_agent"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/router.py` around lines 24 - 34, select_agent currently
reads agent_mode from Configuration.from_runnable_config which never includes
agent_mode, so it always falls back to "parallel"; fix by reading agent_mode
from the original RunnableConfig first (e.g., check getattr(config,
"agent_mode", None)) and only fall back to Configuration.from_runnable_config or
the default if not present, or alternatively add agent_mode as a declared field
on the Configuration model so Configuration.from_runnable_config(materializes
it); update select_agent to use the config-provided agent_mode (or the updated
Configuration) to correctly route to "linear_agent" or "supervisor_agent".
backend/src/agent/graphs/supervisor.py (1)

43-53: ⚠️ Potential issue | 🟠 Major

compress_context cannot prevent context bloat with additive reducer semantics.

web_research_result is configured with operator.add (additive reducer), so any returned list appends to existing state rather than replacing it. The function deduplicates results before returning, but the deduplication is negated when the additive reducer applies. For example:

  • Existing state: [A, B]
  • Function dedupes and returns: [A, B, C]
  • After additive: [A, B, A, B, C] ← duplicates reappear

This defeats the node's stated purpose ("prevent context bloat" and "instead of appending to an ever-growing list"). Either replace the additive reducer with a custom reducer that truly replaces state, or redesign the node to work with append semantics.

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

In `@backend/src/agent/graphs/supervisor.py` around lines 43 - 53, The current
compress_context logic deduplicates combined = current_results + new_results but
returns the full unique_results which, given the node's operator.add reducer,
will be appended back and reintroduce duplicates; fix by either changing the
reducer for the web_research_result node to a replacing reducer (so returned
unique_results replaces prior state) or change compress_context to return only
the delta (items from validated_web_research_result not already present in
web_research_result) so additive semantics don't re-add prior items; locate the
code handling state.get("validated_web_research_result") and
state.get("web_research_result") in compress_context (and respect
app_config.compression_enabled) and implement one of these two options.
backend/src/observability/langfuse.py (1)

121-129: ⚠️ Potential issue | 🟡 Minor

Use bare raise to preserve traceback context.

raise e resets the traceback to this handler line, making it harder to trace where the exception actually originated. Since this is a simple re-raise with no exception transformation, use bare raise instead to keep the original stack intact.

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

In `@backend/src/observability/langfuse.py` around lines 121 - 129, In the except
Exception as e handler (the block that comments about Langfuse's observe()
re-raising exceptions), replace the current "raise e" with a bare "raise" so the
original traceback is preserved when re-raising the exception from the
observe()/yield path; keep the surrounding comment and behavior but use bare
raise to avoid losing the original stack context.
backend/src/agent/utils.py (1)

13-22: ⚠️ Potential issue | 🟠 Major

Handle dict-style messages in get_research_topic.

Line [14] assumes .content exists; dict-backed messages can fail here. Also, dict entries are ignored in the multi-message loop, which can drop context.

💡 Suggested fix
 def get_research_topic(messages: List[AnyMessage]) -> str:
     """Get the research topic from the messages."""
+    if not messages:
+        return ""
+
     # check if request has a history and combine the messages into a single string
     if len(messages) == 1:
-        research_topic = messages[-1].content
+        message = messages[-1]
+        if isinstance(message, dict):
+            return str(message.get("content", ""))
+        return str(getattr(message, "content", ""))
     else:
         research_topic = ""
         for message in messages:
-            if isinstance(message, HumanMessage):
+            if isinstance(message, dict):
+                role = str(message.get("role", "user")).lower()
+                prefix = "Assistant" if role == "assistant" else "User"
+                research_topic += f"{prefix}: {message.get('content', '')}\n"
+            elif isinstance(message, HumanMessage):
                 research_topic += f"User: {message.content}\n"
             elif isinstance(message, AIMessage):
                 research_topic += f"Assistant: {message.content}\n"
     return research_topic
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/utils.py` around lines 13 - 22, get_research_topic
currently assumes messages are objects with a .content attribute and ignores
dict-style messages, which can raise exceptions and drop context; update the
logic in get_research_topic to handle both object messages (HumanMessage,
AIMessage) and dict messages by checking isinstance(message, dict) and
extracting content via message.get('content') (and optionally role via
message.get('role') to map to "User"/"Assistant"), and build the research_topic
string for every message type so no entries are skipped and no attribute errors
occur.
backend/src/agent/orchestration.py (2)

120-128: ⚠️ Potential issue | 🟠 Major

Gate Tavily tool registration on API key availability.

Currently this can register a tool that fails at first invocation when TAVILY_API_KEY is absent.

Proposed guard
-            from agent.research_tools import TAVILY_AVAILABLE, tavily_search_multiple
+            from agent.research_tools import (
+                TAVILY_AVAILABLE,
+                get_tavily_api_key,
+                tavily_search_multiple,
+            )

             if TAVILY_AVAILABLE:
-                self.register(
-                    "tavily_search",
-                    tavily_search_multiple,
-                    description="Deep web search using Tavily API",
-                    category="search",
-                )
+                try:
+                    get_tavily_api_key()
+                except ValueError:
+                    logger.info("TAVILY_API_KEY not set; tavily_search tool not registered")
+                else:
+                    self.register(
+                        "tavily_search",
+                        tavily_search_multiple,
+                        description="Deep web search using Tavily API",
+                        category="search",
+                    )

Based on learnings Integrate tavily web research tool for enhanced capabilities (requires API key configuration).

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

In `@backend/src/agent/orchestration.py` around lines 120 - 128, The current
registration of the Tavily tool may succeed even when the API key is missing;
update the guard around registering tavily_search_multiple so it only registers
when both TAVILY_AVAILABLE and the Tavily API key/config is present (e.g., check
the environment/config var used for the key such as TAVILY_API_KEY or the helper
that validates it) before calling self.register("tavily_search",
tavily_search_multiple, ...). Ensure the check is performed in orchestration.py
near the existing TAVILY_AVAILABLE import so registration is skipped if the key
is absent.

333-348: ⚠️ Potential issue | 🟠 Major

Coordinator can choose use_tool, but router never returns tools.

This makes tool routing dead even when the coordinator explicitly selects a tool.

Proposed routing fix
-def create_task_router(agents: AgentPool):
+def create_task_router(agents: AgentPool, tools: ToolRegistry | None = None):
@@
     def router(state: OverallState) -> str:
         decision = state.get("coordinator_decision", "delegate_agent")
         target = state.get("coordinator_target", "planner")
@@
+        if decision == "use_tool" and tools and target in tools.get_tool_names():
+            return "tools"
+
         # Map target to node name
         if target in agents.get_agent_names():
             return f"agent_{target}"
-
-        return "agent_planner"  # Default
+        agent_names = agents.get_agent_names()
+        return f"agent_{agent_names[0]}" if agent_names else "finalize"
@@
     builder.add_conditional_edges(
         "coordinator",
-        create_task_router(agents),
+        create_task_router(agents, tools),
         route_targets,
     )

Also applies to: 383-399, 473-477

backend/src/config/app_config.py (1)

58-63: ⚠️ Potential issue | 🟠 Major

Avoid dual CORS config fields with different parsing semantics.

cors_origins and CORS_ORIGINS both read CORS_ORIGINS, but they can produce different values (empty entries/whitespace handling differ). This creates two sources of truth for a security-critical setting.

Proposed consolidation
-    cors_origins: Tuple[str, ...] = tuple(
-        filter(None, os.getenv("CORS_ORIGINS", "http://localhost:5173").split(","))
-    )
+    cors_origins: Tuple[str, ...] = tuple(
+        origin.strip()
+        for origin in os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",")
+        if origin.strip()
+    )
@@
-    # Security Configuration
-    CORS_ORIGINS: List[str] = field(
-        default_factory=lambda: os.getenv(
-            "CORS_ORIGINS", "http://localhost:5173"
-        ).split(",")
-    )
+    `@property`
+    def CORS_ORIGINS(self) -> List[str]:
+        # Backward-compatible alias for older call sites
+        return list(self.cors_origins)

Also applies to: 80-84

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

In `@backend/src/config/app_config.py` around lines 58 - 63, The file defines two
CORS parsing locations that read the same env var with different semantics
causing inconsistent values; consolidate parsing by implementing a single helper
(e.g., normalize_env_list) and use it for cors_origins and allowed_hosts so both
read their respective env vars with identical trimming/empty-entry filtering
behavior; update references to cors_origins and allowed_hosts to call that
helper (and apply the same fix to the similar code at the other occurrence
around the block referenced on lines 80-84) so whitespace is stripped and empty
entries are removed consistently.
backend/src/agent/tools_and_schemas.py (1)

101-104: ⚠️ Potential issue | 🟡 Minor

Use project logger in MCP error path instead of print.

print on Line 103 bypasses centralized logging controls and makes production diagnostics inconsistent.

Proposed logging fix
+import logging
@@
+logger = logging.getLogger(__name__)
@@
     except Exception as e:
         # Log error or return empty list so app startup doesn't crash on optional tool load
-        print(f"Error loading MCP tools: {e}")
+        logger.exception("Error loading MCP tools: %s", e)
         return []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/tools_and_schemas.py` around lines 101 - 104, Replace the
print in the MCP tool error handler with the project logger: in the except
Exception as e block (the "Error loading MCP tools" path) call the module's
logger (e.g., logger.exception or logger.error with exc_info=True) to log the
message and stack trace instead of print, and ensure the module imports/obtains
that logger (e.g., from your logging setup or use process_logger if available)
before returning [].
🟡 Minor comments (12)
backend/src/search/providers/duckduckgo_adapter.py-22-23 (1)

22-23: ⚠️ Potential issue | 🟡 Minor

Default value for region differs from base class signature.

The base class SearchProvider.search defines region: str | None = None, but this implementation uses region: str | None = "wt-wt". This signature mismatch could cause unexpected behavior when callers rely on the abstract interface's default.

Since line 33 already handles None"wt-wt" conversion, consider aligning the signature with the base class:

Suggested fix
     def search(
         self,
         query: str,
         max_results: int = 5,
-        region: str | None = "wt-wt",
+        region: str | None = None,
         time_range: str | None = None,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/duckduckgo_adapter.py` around lines 22 - 23, The
duckduckgo_adapter.SearchProvider.search implementation sets region: str | None
= "wt-wt" which conflicts with the base class SearchProvider.search default of
None; change the parameter default to region: str | None = None and rely on the
existing None → "wt-wt" conversion inside the function (around the code at line
handling region fallback) so the subclass signature matches the abstract
interface and callers see consistent defaults.
backend/src/search/providers/bing_adapter.py-54-60 (1)

54-60: ⚠️ Potential issue | 🟡 Minor

Handle missing 'y' (year) time range mapping.

The base class documents time_range supporting "d", "w", "m", and "y", but this implementation only maps the first three. When time_range == "y", the freshness parameter is silently ignored. While Bing's API doesn't provide a native "Year" freshness value, you should explicitly handle this case by omitting the freshness parameter (which searches all time).

        if time_range:
            if time_range == "d":
                params["freshness"] = "Day"
            elif time_range == "w":
                params["freshness"] = "Week"
            elif time_range == "m":
                params["freshness"] = "Month"
+            elif time_range == "y":
+                # Bing doesn't have a direct "Year" option; omit freshness to search all time
+                pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/bing_adapter.py` around lines 54 - 60, The
mapping for time_range in backend/src/search/providers/bing_adapter.py currently
handles "d","w","m" but omits "y"; update the logic around the params dict (the
time_range variable and params["freshness"] mapping) to explicitly handle
time_range == "y" by leaving out the freshness parameter (i.e., do not set
params["freshness"] for "y" or remove it if previously set) so that a year
request falls back to searching all time.
frontend/src/hooks/useAgentState.ts-112-114 (1)

112-114: ⚠️ Potential issue | 🟡 Minor

Normalize unknown error shapes before storing error state.

error.message is not guaranteed for non-Error throw values; this can silently set undefined and hide failures.

Suggested fix
-    onError: (error: any) => {
-      setError(error.message)
-    },
+    onError: (error: any) => {
+      setError(error?.message ?? String(error))
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/hooks/useAgentState.ts` around lines 112 - 114, The onError
handler in the useAgentState hook currently does setError(error.message) which
can become undefined for non-Error throw values; change the handler to normalize
the thrown value before storing it by deriving a safe string (e.g., check for
error?.message, error?.toString(), or fall back to String(error)) and pass that
normalized message into setError so unknown error shapes never set undefined.
Reference: the onError callback in useAgentState and the setError state setter.
frontend/src/components/ArtifactView.tsx-116-120 (1)

116-120: ⚠️ Potential issue | 🟡 Minor

Handle clipboard failures explicitly in copy action.

navigator.clipboard.writeText returns a promise that can reject; currently this can fail silently with an unhandled rejection.

Suggested fix
-    const handleCopy = () => {
-      navigator.clipboard.writeText(content)
-      setCopied(true)
-      setTimeout(() => setCopied(false), 2000)
-    }
+    const handleCopy = async () => {
+      try {
+        await navigator.clipboard.writeText(content)
+        setCopied(true)
+        setTimeout(() => setCopied(false), 2000)
+      } catch {
+        setCopied(false)
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ArtifactView.tsx` around lines 116 - 120, The
handleCopy function calls navigator.clipboard.writeText(content) but ignores the
returned promise; update handleCopy to await or .then() the writeText call, only
call setCopied(true) on success, and catch failures to handle them (e.g., set an
error state, call a provided onError/logging function, or fallback to a legacy
copy method), ensuring navigator.clipboard.writeText rejection is not left
unhandled; reference the handleCopy function, the content variable, and
setCopied state when implementing this change.
backend/src/agent/rag.py-129-131 (1)

129-131: ⚠️ Potential issue | 🟡 Minor

Warning message contradicts the active code path.

This branch runs only when Chroma is available and initialized, but the log says Chroma is missing.

Proposed fix
-            logger.warning(
-                "Dual write enabled but ChromaDB is missing. Writing to FAISS only."
-            )
+            logger.info("Initialized Chroma store at %s", persist_path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/rag.py` around lines 129 - 131, The warning message in
rag.py ("Dual write enabled but ChromaDB is missing. Writing to FAISS only.")
contradicts the branch it sits in (this branch executes when Chroma is
available); update the log in the logger.warning call to accurately describe the
active path (e.g., indicate dual-write is enabled and writes will go to both
FAISS and Chroma/ChromaDB), keeping the log call at the same location so it
reflects the real state used by the dual-write logic.
backend/src/agent/deep_search_agent.py-75-79 (1)

75-79: ⚠️ Potential issue | 🟡 Minor

Tighten JSON boundary detection before parsing.

The current end-index check can pass malformed bracket ranges and rely on broad exception fallback.

Proposed fix
-            start = response.find('[')
-            end = response.rfind(']') + 1
-            if start != -1 and end != -1:
-                return json.loads(response[start:end])
+            start = response.find('[')
+            end = response.rfind(']')
+            if start != -1 and end != -1 and end > start:
+                return json.loads(response[start : end + 1])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/deep_search_agent.py` around lines 75 - 79, Tighten the
JSON boundary detection before parsing by ensuring the found ']' comes after the
found '[' and by attempting to parse within a safe try/except; specifically, in
the block that computes start = response.find('[') and end = response.rfind(']')
and currently returns json.loads(response[start:end]) or fallback return
[topic], change the checks to require start != -1, end != -1 and end > start,
build the slice including the closing bracket (end+1) and wrap json.loads(...)
in a try/except that falls back to return [topic] on any parsing error so
malformed bracket ranges or bad JSON won't raise.
frontend/src/components/__tests__/ArtifactView.test.tsx-41-53 (1)

41-53: ⚠️ Potential issue | 🟡 Minor

Use label-based button queries instead of positional indices, but the suggested diff needs refinement.

Lines 41, 51, and 61 couple tests to button DOM order; this is fragile since the maximize button (hidden md:flex) may not exist on all screen sizes. The suggested refactor improves two buttons but has issues:

  • Copy button: Use screen.getByRole('button', { name: /copy content/i })
  • Close button: Use screen.getByRole('button', { name: /close/i })
  • Maximize button: The aria-label is dynamic ("Maximize""Restore size" after toggle), so the suggested pattern won't reliably find it post-click. Use getByRole('button', { name: /maximize|restore size/i }) or the stable title attribute instead.
  • className assertions: The suggested diff removes md:w-1/2 / left-0 checks entirely, but these assertions are necessary to verify the maximize actually works—not just that the button exists.

Also applies to: 61-70

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

In `@frontend/src/components/__tests__/ArtifactView.test.tsx` around lines 41 -
53, The tests in ArtifactView.test.tsx are brittle because they select buttons
by position; update the tests to query by accessible names instead: replace
positional queries with screen.getByRole('button', { name: /copy content/i })
for the copy button and screen.getByRole('button', { name: /close/i }) for the
close button; for the maximize toggle use a tolerant matcher like
screen.getByRole('button', { name: /maximize|restore size/i }) or query by a
stable title attribute to handle the dynamic aria-label; after clicking the
maximize/restore control assert the actual DOM layout changes by checking the
container or element className (e.g., verify presence/absence of md:w-1/2 and
left-0) rather than only asserting the button exists, and update any tests
referencing buttons[0] / buttons[buttons.length - 1] to use these named queries
and class assertions for ArtifactView.
.github/workflows/validate-env.yml-27-30 (1)

27-30: ⚠️ Potential issue | 🟡 Minor

Backend env check should validate key assignment, not just string presence.

Line 29 can pass on comments or incidental text. Use an assignment-style regex (optionally commented) for a more reliable guard.

🔧 Proposed fix
       - name: Check backend/.env.example has GEMINI_API_KEY
         run: |
-          if ! grep -q "GEMINI_API_KEY" backend/.env.example; then
+          if ! grep -Eq "^[[:space:]#]*GEMINI_API_KEY=" backend/.env.example; then
             echo "ERROR: backend/.env.example is missing GEMINI_API_KEY."
             exit 1
           fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/validate-env.yml around lines 27 - 30, The current check
only looks for the string GEMINI_API_KEY and can hit comments; update the grep
to use an assignment-style regex that matches a key assignment (optionally
commented). Replace the simple grep in the "Check backend/.env.example has
GEMINI_API_KEY" step with a regex check such as using grep -Eq and pattern
'^\s*(#\s*)?GEMINI_API_KEY\s*=' so the script only passes when GEMINI_API_KEY is
actually assigned (or an assignment is commented out).
backend/src/agent/persistence.py-13-14 (1)

13-14: ⚠️ Potential issue | 🟡 Minor

Sanitized thread IDs can collapse to an empty filename stem.

If thread_id contains only filtered characters, all such requests map to plans/.json.

💡 Proposed fix
 def _get_plan_path(thread_id: str) -> str:
@@
     safe_id = "".join(c for c in thread_id if c.isalnum() or c in ("-", "_"))
+    if not safe_id:
+        raise ValueError(
+            "thread_id must include at least one alphanumeric, '-' or '_' character"
+        )
     return os.path.join(PLAN_DIR, f"{safe_id}.json")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/persistence.py` around lines 13 - 14, The sanitization can
yield an empty safe_id (so all such calls map to "plans/.json"); in the function
that builds the filename (using thread_id and safe_id and returning
os.path.join(PLAN_DIR, f"{safe_id}.json")), detect when safe_id == "" and
replace it with a safe fallback (e.g., a UUID, a hashed version of the original
thread_id, or raise a clear ValueError) before joining the path; update any
callers or docs if you choose a deterministic fallback so other code can locate
the file reliably.
backend/src/agent/persistence.py-28-28 (1)

28-28: ⚠️ Potential issue | 🟡 Minor

updated_at is computed from the old file mtime, not the current save operation.

This stores stale metadata on updates. Set it to current write time (or compute mtime after writing).

💡 Proposed fix
+import time
@@
-        "updated_at": os.path.getmtime(path) if os.path.exists(path) else None,
+        "updated_at": time.time(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/persistence.py` at line 28, The current code sets
"updated_at" from os.path.getmtime(path) which may reflect the previous file
mtime; update the save/update logic in backend/src/agent/persistence.py so
"updated_at" is assigned the actual current write time by either calling
os.path.getmtime(path) after the file write completes (and verifying
os.path.exists(path)) or by using time.time() at the moment of the successful
write; locate the code that writes to the file (references to path and the
"updated_at" dict key) and replace the stale mtime read with a post-write mtime
or current timestamp.
backend/src/agent/mcp_server.py-172-185 (1)

172-185: ⚠️ Potential issue | 🟡 Minor

Report actual bytes written, not character count.

Line [184] currently returns len(content), which is chars. For UTF-8 multibyte text, this misreports written bytes.

💡 Suggested fix
         return ToolResult(
-            success=True, data={"path": path, "bytes_written": len(content)}
+            success=True, data={"path": path, "bytes_written": len(content_bytes)}
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/mcp_server.py` around lines 172 - 185, The code in the
save/write routine currently reports bytes_written using len(content) (character
count), which misreports UTF-8 multibyte data; change it to compute
bytes_written = len(content_bytes) (the already-created UTF-8 encoded bytes) and
return that value in the ToolResult (replace the use of len(content) with
bytes_written) after p.write_text(..., encoding="utf-8") in the same block
(refer to variables content_bytes, p.write_text, and the ToolResult return).
frontend/src/components/ChatMessagesView.tsx-232-238 (1)

232-238: ⚠️ Potential issue | 🟡 Minor

Copy flow assumes message.id always exists, but rendering already handles missing IDs.

The non-null assertion at Line 237 can push undefined into copy-state tracking for messages that use fallback keys.

Proposed safe ID threading
 interface AiMessageBubbleProps {
   message: Message
+  messageId: string
@@
 const AiMessageBubble: React.FC<AiMessageBubbleProps> = memo(
   ({
     message,
+    messageId,
@@
               typeof message.content === 'string'
                 ? message.content
                 : JSON.stringify(message.content),
-              message.id!
+              messageId
             )
           }
@@
 interface MessageItemProps {
   message: Message
+  messageId: string
@@
   ({
     message,
+    messageId,
@@
             <AiMessageBubble
               message={message}
+              messageId={messageId}
@@
           {messages.map((message, index) => {
             const isLast = index === messages.length - 1
+            const messageId = message.id ?? `msg-${index}`
@@
-                key={message.id || `msg-${index}`}
+                key={messageId}
                 message={message}
+                messageId={messageId}

Also applies to: 305-312, 512-513

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

In `@frontend/src/components/ChatMessagesView.tsx` around lines 232 - 238, The
copy handler call currently uses a non-null assertion on message.id
(handleCopy(..., message.id!)), which can pass undefined into your copy-state
tracking; change the call to pass the same stable key used for rendering instead
of forcing non-null—e.g., replace message.id! with (message.id ?? fallbackKey)
where fallbackKey is the fallback/render key you already derive for the message
(or the same stable key variable used as the React key), and remove the non-null
assertion; apply the same pattern to the other handleCopy sites that mirror this
call so copy-state always receives a defined, stable identifier.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 228f445 and 351f364.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (100)
  • .env.example
  • .github/workflows/pr-check.yml
  • .github/workflows/push-check.yml
  • .github/workflows/validate-env.yml
  • .pre-commit-config.yaml
  • backend/.env.example
  • backend/examples/gemma_providers.py
  • backend/pyproject.toml
  • backend/src/agent/__init__.py
  • backend/src/agent/_graph.py
  • backend/src/agent/app.py
  • backend/src/agent/configuration.py
  • backend/src/agent/deep_search_agent.py
  • backend/src/agent/gemma_client.py
  • backend/src/agent/graph.py
  • backend/src/agent/graph_builder.py
  • backend/src/agent/graphs/linear.py
  • backend/src/agent/graphs/parallel.py
  • backend/src/agent/graphs/planning.py
  • backend/src/agent/graphs/supervisor.py
  • backend/src/agent/graphs/upstream.py
  • backend/src/agent/kg.py
  • backend/src/agent/llm_client.py
  • backend/src/agent/mcp_client.py
  • backend/src/agent/mcp_config.py
  • backend/src/agent/mcp_persistence.py
  • backend/src/agent/mcp_server.py
  • backend/src/agent/memory_tools.py
  • backend/src/agent/models.py
  • backend/src/agent/nodes.py
  • backend/src/agent/orchestration.py
  • backend/src/agent/persistence.py
  • backend/src/agent/planning_router.py
  • backend/src/agent/rag.py
  • backend/src/agent/rag_nodes.py
  • backend/src/agent/rate_limiter.py
  • backend/src/agent/registry.py
  • backend/src/agent/research_tools.py
  • backend/src/agent/router.py
  • backend/src/agent/scoping_schema.py
  • backend/src/agent/security.py
  • backend/src/agent/state.py
  • backend/src/agent/tool_adapter.py
  • backend/src/agent/tools_and_schemas.py
  • backend/src/agent/utils.py
  • backend/src/config/__init__.py
  • backend/src/config/app_config.py
  • backend/src/config/validation.py
  • backend/src/evaluation/bench.py
  • backend/src/evaluation/data.py
  • backend/src/evaluation/deep_research_bench.py
  • backend/src/evaluation/metrics.py
  • backend/src/evaluation/mle_bench.py
  • backend/src/observability/config.py
  • backend/src/observability/langfuse.py
  • backend/src/rag/chroma_store.py
  • backend/src/search/__init__.py
  • backend/src/search/provider.py
  • backend/src/search/providers/bing_adapter.py
  • backend/src/search/providers/brave_adapter.py
  • backend/src/search/providers/duckduckgo_adapter.py
  • backend/src/search/providers/google_adapter.py
  • backend/src/search/providers/tavily_adapter.py
  • backend/src/search/router.py
  • docs/tasks/06_GEMMA_INTEGRATION.md
  • frontend/.prettierrc.json
  • frontend/eslint.config.js
  • frontend/package.json
  • frontend/src/App.tsx
  • frontend/src/components/ActivityTimeline.test.tsx
  • frontend/src/components/ActivityTimeline.tsx
  • frontend/src/components/ArtifactView.test.tsx
  • frontend/src/components/ArtifactView.tsx
  • frontend/src/components/ChatMessagesView.test.tsx
  • frontend/src/components/ChatMessagesView.tsx
  • frontend/src/components/ChatMessagesView_Accessibility.test.tsx
  • frontend/src/components/ChatMessagesView_Log.test.tsx
  • frontend/src/components/ChatMessagesView_Markdown.test.tsx
  • frontend/src/components/InputForm.test.tsx
  • frontend/src/components/InputForm.tsx
  • frontend/src/components/WelcomeScreen.tsx
  • frontend/src/components/__tests__/ActivityTimeline_Semantics.test.tsx
  • frontend/src/components/__tests__/ArtifactView.test.tsx
  • frontend/src/components/__tests__/WelcomeScreen.test.tsx
  • frontend/src/components/ui/badge.tsx
  • frontend/src/components/ui/button.tsx
  • frontend/src/components/ui/card.tsx
  • frontend/src/components/ui/input.tsx
  • frontend/src/components/ui/scroll-area.tsx
  • frontend/src/components/ui/select.tsx
  • frontend/src/components/ui/tabs.tsx
  • frontend/src/components/ui/textarea.tsx
  • frontend/src/global.css
  • frontend/src/hooks/useAgentState.test.ts
  • frontend/src/hooks/useAgentState.ts
  • frontend/src/lib/utils.ts
  • frontend/src/main.tsx
  • frontend/src/test/setup.ts
  • patch_rag.py
  • ruff.toml
💤 Files with no reviewable changes (1)
  • frontend/src/hooks/useAgentState.test.ts

Comment thread .github/workflows/validate-env.yml
Comment thread backend/examples/gemma_providers.py Outdated
Comment thread backend/src/agent/deep_search_agent.py Outdated
Comment thread backend/src/agent/deep_search_agent.py Outdated
Comment on lines +94 to +97
for query in queries:
results = self.searcher.search(query)
for r in results:
notes.append(f"Source: {r.get('url', 'unknown')}\n{r.get('content', '')}")

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 blocking sync calls inside async research flow.

self.searcher.search(query) is synchronous and runs on the event loop thread. Offload to threads (or async provider) to prevent loop stalls.

Proposed fix
-        # Parallel search
-        for query in queries:
-            results = self.searcher.search(query)
-            for r in results:
-                notes.append(f"Source: {r.get('url', 'unknown')}\n{r.get('content', '')}")
+        # Parallel search
+        search_tasks = [asyncio.to_thread(self.searcher.search, query) for query in queries]
+        search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
+        for query, result in zip(queries, search_results):
+            if isinstance(result, Exception):
+                logger.error("Search failed for '%s': %s", query, result)
+                continue
+            for r in result:
+                notes.append(f"Source: {r.get('url', 'unknown')}\n{r.get('content', '')}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/deep_search_agent.py` around lines 94 - 97, The loop in
DeepSearchAgent (in deep_search_agent.py) calls the synchronous
self.searcher.search(query) inside an async flow, which can block the event
loop; change that call to run in a thread via asyncio.to_thread (or
loop.run_in_executor) so the blocking work is offloaded — e.g., replace results
= self.searcher.search(query) with results = await
asyncio.to_thread(self.searcher.search, query) (import asyncio at top) and keep
the rest of the logic that appends to notes unchanged.

Comment thread backend/src/agent/graph.py
Comment thread backend/src/agent/rag.py
Comment on lines 286 to +289
if read_source == "chroma" and self.use_chroma and CHROMA_AVAILABLE:
return self.retrieve_from_chroma(query, top_k, query_embedding=query_embedding)
return self.retrieve_from_chroma(
query, top_k, query_embedding=query_embedding
)

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

Chroma retrieval path ignores subgoal_filter and min_score.

This breaks contract parity with FAISS retrieval and can return cross-subgoal / below-threshold chunks.

Proposed fix
-        if read_source == "chroma" and self.use_chroma and CHROMA_AVAILABLE:
-            return self.retrieve_from_chroma(
-                query, top_k, query_embedding=query_embedding
-            )
+        if read_source == "chroma" and self.use_chroma and CHROMA_AVAILABLE:
+            chroma_results = self.retrieve_from_chroma(
+                query, top_k=top_k * 2, query_embedding=query_embedding
+            )
+            filtered: List[Tuple[EvidenceChunk, float]] = []
+            for evidence, score in chroma_results:
+                if subgoal_filter and evidence.subgoal_id != subgoal_filter:
+                    continue
+                if score < min_score:
+                    continue
+                filtered.append((evidence, score))
+            return filtered[:top_k]
📝 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
if read_source == "chroma" and self.use_chroma and CHROMA_AVAILABLE:
return self.retrieve_from_chroma(query, top_k, query_embedding=query_embedding)
return self.retrieve_from_chroma(
query, top_k, query_embedding=query_embedding
)
if read_source == "chroma" and self.use_chroma and CHROMA_AVAILABLE:
chroma_results = self.retrieve_from_chroma(
query, top_k=top_k * 2, query_embedding=query_embedding
)
filtered: List[Tuple[EvidenceChunk, float]] = []
for evidence, score in chroma_results:
if subgoal_filter and evidence.subgoal_id != subgoal_filter:
continue
if score < min_score:
continue
filtered.append((evidence, score))
return filtered[:top_k]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/rag.py` around lines 286 - 289, The Chroma branch currently
calls retrieve_from_chroma without forwarding subgoal_filter and min_score,
causing it to ignore those constraints; update the call in the read_source ==
"chroma" branch to pass subgoal_filter and min_score (e.g.,
self.retrieve_from_chroma(query, top_k, query_embedding=query_embedding,
subgoal_filter=subgoal_filter, min_score=min_score)) and, if needed, adjust
retrieve_from_chroma's signature/implementation to accept and apply
subgoal_filter and min_score the same way FAISS retrieval does so results
respect subgoal membership and score threshold.

Comment on lines 18 to 21
def router_node(state: OverallState, config: RunnableConfig):
"""Routes to the appropriate sub-agent based on configuration."""
# We simply pass the state through; routing happens in the conditional edge.
return {"messages": state.get("messages", [])}

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

Router pass-through is mutating messages instead of preserving state.

Returning {"messages": state.get("messages", [])} will re-emit existing messages and can duplicate them when reducers append state updates. Return an empty update from the router node instead.

💡 Proposed fix
 def router_node(state: OverallState, config: RunnableConfig):
     """Routes to the appropriate sub-agent based on configuration."""
-    # We simply pass the state through; routing happens in the conditional edge.
-    return {"messages": state.get("messages", [])}
+    # Routing happens in the conditional edge; no state mutation needed here.
+    return {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/router.py` around lines 18 - 21, The router_node function
is incorrectly re-emitting existing messages by returning {"messages":
state.get("messages", [])}, which can cause duplicate messages; modify
router_node (function symbol: router_node, types: OverallState, RunnableConfig)
to return an empty update (e.g., an empty dict) instead of returning the
messages so the router does not mutate or re-emit state—ensure you do not modify
or copy state inside router_node and simply return no-op update.

Comment on lines +261 to 264
# Check if path is protected.
# If protected_paths is empty, nothing is rate-limited (explicit opt-in).
is_protected = any(path.startswith(prefix) for prefix in self.protected_paths)

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

Default path check currently turns rate limiting off globally.

Line 263 returns False when protected_paths is empty (the default), so no requests are rate-limited. That conflicts with the constructor contract and weakens DoS protection.

🔒 Proposed fix
-        # Check if path is protected.
-        # If protected_paths is empty, nothing is rate-limited (explicit opt-in).
-        is_protected = any(path.startswith(prefix) for prefix in self.protected_paths)
+        # If protected_paths is empty, rate-limit all paths.
+        is_protected = (
+            True
+            if not self.protected_paths
+            else any(path.startswith(prefix) for prefix in self.protected_paths)
+        )
🤖 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 261 - 264, The current check sets
is_protected = any(path.startswith(prefix) for prefix in self.protected_paths)
which returns False when self.protected_paths is empty and effectively disables
rate limiting; change the logic so an empty self.protected_paths means “protect
all paths” (e.g., if not self.protected_paths: is_protected = True else evaluate
the startswith any-check) to satisfy the constructor contract and restore DoS
protection for self.protected_paths and the is_protected variable.

Comment on lines 157 to +169
if isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except Exception:
pass
except json.JSONDecodeError:
logger.warning("Could not parse tool arguments as JSON; using raw string")

import uuid

call_id = f"call_{uuid.uuid4().hex[:8]}"

tool_calls.append({
"name": name,
"args": arguments,
"id": call_id,
"type": "tool_call"
})
tool_calls.append(
{"name": name, "args": arguments, "id": call_id, "type": "tool_call"}
)

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

Drop malformed string arguments instead of passing them through.

If argument JSON parsing fails, the current code preserves a raw string. That can break downstream logic that expects dict arguments.

💡 Suggested fix
             if isinstance(arguments, str):
                 try:
                     arguments = json.loads(arguments)
                 except json.JSONDecodeError:
                     logger.warning("Could not parse tool arguments as JSON; using raw string")
+                    continue
+
+            if not isinstance(arguments, dict):
+                continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/tool_adapter.py` around lines 157 - 169, When
json.loads(arguments) raises json.JSONDecodeError in the parsing block around
the variable arguments, do not keep the raw string; instead replace arguments
with an empty dict (arguments = {}) or otherwise normalize to a dict before
appending to tool_calls so downstream logic that expects dicts won't break.
Update the except block (where logger.warning is called) to set arguments = {}
and include the original malformed value in the warning via logger.warning so
callers can see what failed; keep the rest of the logic that builds call_id and
appends to tool_calls unchanged (references: arguments, json.loads,
logger.warning, call_id, tool_calls, name).

Comment thread backend/src/evaluation/metrics.py
@coderabbitai

coderabbitai Bot commented Mar 1, 2026

Copy link
Copy Markdown

Note

Docstrings generation - SUCCESS
Generated docstrings and committed to branch jules-kaggle-gemma-integration-9750286105324430980 (commit: b1e198e436a708e3b6292d1eb1f8b5ed1598dcd6)

Docstrings generation was requested by @MasumRab.

The following files were modified:

* `backend/examples/gemma_providers.py`
* `backend/src/agent/_graph.py`
* `backend/src/agent/app.py`
* `backend/src/agent/configuration.py`
* `backend/src/agent/deep_search_agent.py`
* `backend/src/agent/gemma_client.py`
* `backend/src/agent/graph.py`
* `backend/src/agent/graph_builder.py`
* `backend/src/agent/graphs/linear.py`
* `backend/src/agent/graphs/supervisor.py`
* `backend/src/agent/kg.py`
* `backend/src/agent/llm_client.py`
* `backend/src/agent/mcp_client.py`
* `backend/src/agent/mcp_config.py`
* `backend/src/agent/mcp_persistence.py`
* `backend/src/agent/mcp_server.py`
* `backend/src/agent/memory_tools.py`
* `backend/src/agent/models.py`
* `backend/src/agent/nodes.py`
* `backend/src/agent/orchestration.py`
* `backend/src/agent/persistence.py`
* `backend/src/agent/planning_router.py`
* `backend/src/agent/rag.py`
* `backend/src/agent/rag_nodes.py`
* `backend/src/agent/rate_limiter.py`
* `backend/src/agent/registry.py`
* `backend/src/agent/research_tools.py`
* `backend/src/agent/router.py`
* `backend/src/agent/security.py`
* `backend/src/agent/state.py`
* `backend/src/agent/tool_adapter.py`
* `backend/src/agent/tools_and_schemas.py`
* `backend/src/agent/utils.py`
* `backend/src/config/app_config.py`
* `backend/src/config/validation.py`
* `backend/src/evaluation/bench.py`
* `backend/src/evaluation/data.py`
* `backend/src/evaluation/metrics.py`
* `backend/src/evaluation/mle_bench.py`
* `backend/src/observability/config.py`
* `backend/src/observability/langfuse.py`
* `backend/src/rag/chroma_store.py`
* `backend/src/search/provider.py`
* `backend/src/search/providers/bing_adapter.py`
* `backend/src/search/providers/brave_adapter.py`
* `backend/src/search/providers/duckduckgo_adapter.py`
* `backend/src/search/providers/google_adapter.py`
* `backend/src/search/providers/tavily_adapter.py`
* `backend/src/search/router.py`
* `frontend/src/App.tsx`
* `frontend/src/components/ChatMessagesView.tsx`
* `frontend/src/components/ui/badge.tsx`
* `frontend/src/components/ui/button.tsx`
* `frontend/src/components/ui/card.tsx`
* `frontend/src/components/ui/input.tsx`
* `frontend/src/components/ui/scroll-area.tsx`
* `frontend/src/components/ui/select.tsx`
* `frontend/src/components/ui/tabs.tsx`
* `frontend/src/components/ui/textarea.tsx`
* `frontend/src/hooks/useAgentState.ts`
* `frontend/src/lib/utils.ts`

These files were kept as they were:
* `backend/src/evaluation/deep_research_bench.py`

These files were ignored:
* `frontend/src/components/ActivityTimeline.test.tsx`
* `frontend/src/components/ArtifactView.test.tsx`
* `frontend/src/components/ChatMessagesView.test.tsx`
* `frontend/src/components/ChatMessagesView_Accessibility.test.tsx`
* `frontend/src/components/ChatMessagesView_Log.test.tsx`
* `frontend/src/components/ChatMessagesView_Markdown.test.tsx`
* `frontend/src/components/InputForm.test.tsx`
* `frontend/src/components/__tests__/ActivityTimeline_Semantics.test.tsx`
* `frontend/src/components/__tests__/ArtifactView.test.tsx`
* `frontend/src/components/__tests__/WelcomeScreen.test.tsx`
* `frontend/src/hooks/useAgentState.test.ts`

These file types are not supported:
* `.env.example`
* `.github/workflows/pr-check.yml`
* `.github/workflows/push-check.yml`
* `.github/workflows/validate-env.yml`
* `.pre-commit-config.yaml`
* `backend/.env.example`
* `backend/pyproject.toml`
* `docs/tasks/06_GEMMA_INTEGRATION.md`
* `frontend/.prettierrc.json`
* `frontend/package.json`
* `frontend/src/global.css`
* `ruff.toml`
@sonarqubecloud

sonarqubecloud Bot commented Mar 1, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (10)
backend/src/observability/langfuse.py (1)

125-133: ⚠️ Potential issue | 🟡 Minor

Re-raise with bare raise to keep original traceback.

Current raise e loses stack fidelity from the original failure site.

♻️ Proposed fix
-    except Exception as e:
+    except Exception:
@@
-        raise e
+        raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/observability/langfuse.py` around lines 125 - 133, In the
exception handler in backend/src/observability/langfuse.py (the except Exception
as e block that currently does "raise e"), replace the explicit re-raise with a
bare "raise" so the original traceback is preserved; keep the surrounding
comments/logic intact and do not add additional exception handling so observe()
exceptions continue to bubble with their original stack trace.
backend/src/search/providers/tavily_adapter.py (1)

103-106: ⚠️ Potential issue | 🟡 Minor

Use bare raise to preserve the original traceback.

Re-raising with raise e truncates useful stack context.

♻️ Proposed fix
-        except Exception as e:
+        except Exception as e:
             logger.error(f"Tavily Search failed: {e}")
             # If tuned failed, we could try basic, but the router handles fallback.
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/tavily_adapter.py` around lines 103 - 106, In
the exception handler inside tavily_adapter (the try/except block that logs
"Tavily Search failed"), replace the current re-raise using "raise e" with a
bare "raise" so the original traceback is preserved; update the except block
where logger.error(f"Tavily Search failed: {e}") is called to use bare raise
after logging.
backend/src/search/providers/google_adapter.py (2)

85-87: ⚠️ Potential issue | 🟡 Minor

Preserve traceback when propagating errors.

Use bare raise instead of raise e to keep original stack context.

♻️ Proposed fix
-        except Exception as e:
+        except Exception as e:
             logger.error(f"Google Search failed: {e}")
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/google_adapter.py` around lines 85 - 87, In the
except block that currently does "except Exception as e: logger.error(f'Google
Search failed: {e}') raise e" in google_adapter.py, replace the "raise e" with a
bare "raise" so the original traceback is preserved when re-raising the
exception; keep the logger.error call but do not change the caught variable name
(e) so the message remains the same.

22-26: ⚠️ Potential issue | 🟠 Major

Guard client construction and fix exception handling pattern.

The SDK will raise a ValueError during Client() initialization if api_key is None and GOOGLE_API_KEY/GEMINI_API_KEY are not set, causing the adapter to fail at setup time rather than gracefully. For defensive programming, guard the client creation to fail fast with clear messaging.

Additionally, line 87 uses the anti-pattern raise e, which should be replaced with a bare raise to preserve the exception chain.

♻️ Proposed fix
         self.api_key = api_key or os.getenv("GEMINI_API_KEY")
         if not self.api_key:
             logger.warning("GEMINI_API_KEY not found. Google Search may fail.")
-        self.client = Client(api_key=self.api_key)
+        self.client = Client(api_key=self.api_key) if self.api_key else None
         except Exception as e:
             logger.error(f"Google Search failed: {e}")
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/google_adapter.py` around lines 22 - 26, Guard
Client construction in GoogleAdapter.__init__: check for a non-empty api_key
(use api_key or os.getenv("GEMINI_API_KEY") as already assigned) and if missing
log an error and raise a clear ValueError before calling Client(...) instead of
letting Client() raise; only construct self.client =
Client(api_key=self.api_key) when api_key is present. Also replace the
anti-pattern "raise e" in the method that currently re-raises exceptions
(referenced near the handler that catches exceptions around line 87) with a bare
"raise" to preserve the original traceback/exception chain.
backend/src/rag/chroma_store.py (1)

82-88: ⚠️ Potential issue | 🟠 Major

Protect reserved metadata keys from being overridden.

Current merge order allows e.metadata to overwrite canonical fields like subgoal_id and source_url, which can break retrieval semantics.

♻️ Proposed fix
             meta = {
+                **e.metadata,
                 "source_url": e.source_url,
                 "subgoal_id": e.subgoal_id,
                 "relevance_score": float(e.relevance_score),
                 "timestamp": float(e.timestamp),
-                **e.metadata,
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/rag/chroma_store.py` around lines 82 - 88, The merge currently
does meta = { ..., **e.metadata } which lets user metadata override reserved
canonical fields (source_url, subgoal_id, relevance_score, timestamp) and breaks
semantics; fix by preventing overrides—either merge e.metadata first and then
set the canonical fields last (so canonical values win) or filter e.metadata to
remove reserved keys before merging (e.g., drop keys "source_url", "subgoal_id",
"relevance_score", "timestamp"), and update the meta construction in
chroma_store.py (the meta dict creation that references e.metadata and fields
like source_url, subgoal_id, relevance_score, timestamp) accordingly.
backend/src/agent/tools_and_schemas.py (1)

139-141: ⚠️ Potential issue | 🟡 Minor

Use os.makedirs(workspace_path, exist_ok=True) for robustness.

The current code checks existence then creates, which has a TOCTOU race condition if multiple processes run concurrently. Using exist_ok=True is atomic and handles this safely.

Proposed fix
-    if not os.path.exists(workspace_path):
-        os.makedirs(workspace_path)
+    os.makedirs(workspace_path, exist_ok=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/tools_and_schemas.py` around lines 139 - 141, Replace the
two-step existence check and creation for the workspace directory with a single
atomic call; remove the os.path.exists(workspace_path) conditional and the
os.makedirs(workspace_path) call and instead call os.makedirs(workspace_path,
exist_ok=True) where workspace_path is defined in tools_and_schemas.py to avoid
TOCTOU races when creating the workspace directory.
backend/src/agent/mcp_persistence.py (1)

34-49: ⚠️ Potential issue | 🟡 Minor

Avoid false success on missing thread_id.

At Lines 47-49, this returns success even when save_plan() may no-op for an empty thread_id. Add an explicit upfront validation in this wrapper.

💡 Proposed fix
 def save_thread_plan(
     thread_id: str, todo_list: List[Dict[str, Any]], artifacts: Dict[str, Any]
 ) -> str:
@@
+    if not thread_id:
+        return "Error saving plan: missing thread_id"
+
     try:
         save_plan(thread_id, todo_list, artifacts)
         return f"Plan saved successfully for thread {thread_id}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/mcp_persistence.py` around lines 34 - 49, The wrapper
save_thread_plan should validate the thread_id before calling save_plan to avoid
returning a false success when save_plan no-ops for an empty id; in
save_thread_plan check that thread_id is non-empty/valid (e.g., if not
thread_id: raise ValueError("thread_id is required") or return an error
message), then proceed to call save_plan(thread_id, todo_list, artifacts) and
return the success string only after validation and successful save; reference
the save_thread_plan function and the save_plan call when making the change.
backend/src/agent/configuration.py (1)

88-97: ⚠️ Potential issue | 🟠 Major

Add compatibility mapping for legacy config keys.

At Line 96, only exact field names are read. Existing callers using model, num_queries, or max_loops will be ignored and silently fall back to defaults.

💡 Proposed fix
-        configurable = (
-            config["configurable"] if config and "configurable" in config else {}
-        )
+        configurable = (
+            dict(config["configurable"]) if config and "configurable" in config else {}
+        )
+
+        legacy_aliases = {
+            "model": "query_generator_model",
+            "num_queries": "number_of_initial_queries",
+            "max_loops": "max_research_loops",
+        }
+        for legacy_key, new_key in legacy_aliases.items():
+            if legacy_key in configurable and new_key not in configurable:
+                configurable[new_key] = configurable[legacy_key]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/configuration.py` around lines 88 - 97, The loop that reads
values from configurable/os.environ only checks exact field names (using
cls.model_fields), so legacy keys like "model", "num_queries", and "max_loops"
are ignored; add a compatibility mapping step before the for-loop that
translates legacy keys to current field names (e.g., map "model" -> "base_model"
or the current field name, "num_queries" -> "num_retrievals" or the current
field, and "max_loops" -> "max_iterations") and use that mapped dictionary when
looking up values in configurable and environment, updating the lookup in the
loop that references cls.model_fields and configurable so legacy keys are
honored.
backend/src/agent/graph.py (1)

43-43: 🛠️ Refactor suggestion | 🟠 Major

Replace print() with logger for consistency.

The module uses print() for MCP-related messages while other parts of the codebase use the logging module. This creates inconsistent log levels and formatting.

Suggested fix
+import logging
+
+logger = logging.getLogger(__name__)
+
 # ...

 try:
     validate(mcp_settings)
 except ValueError as e:
-    print(f"WARN: MCP Configuration invalid: {e}")
+    logger.warning(f"MCP Configuration invalid: {e}")

 # ...

 if mcp_settings.enabled:
-    print(f"INFO: MCP Enabled with endpoint {mcp_settings.endpoint}")
+    logger.info(f"MCP Enabled with endpoint {mcp_settings.endpoint}")

Also applies to: 67-67

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

In `@backend/src/agent/graph.py` at line 43, Replace the plain print() calls used
for MCP configuration errors in graph.py with the module logger: obtain or use
the existing logger (e.g. logger = logging.getLogger(__name__)) and change the
print(f"WARN: MCP Configuration invalid: {e}") occurrences to
logger.warning("MCP Configuration invalid: %s", e) (and similarly replace the
other print occurrence). Ensure both instances in the file that currently call
print() for MCP-related messages are updated to use logger.warning for
consistent log levels and formatting.
backend/src/agent/rag.py (1)

170-199: ⚠️ Potential issue | 🟠 Major

retrieve_from_chroma should accept and forward subgoal_filter and min_score to maintain parity with FAISS retrieval.

The method signature lacks subgoal_filter and min_score parameters, and the call to self.chroma.retrieve() on line 199 doesn't pass them. Since ChromaStore.retrieve already supports these parameters (per relevant code snippet), forwarding them here is cleaner than post-hoc filtering in the caller.

Proposed fix
     def retrieve_from_chroma(
-        self, query: str, top_k: int, query_embedding: List[float] | None = None
+        self, query: str, top_k: int, query_embedding: List[float] | None = None,
+        subgoal_filter: str | None = None, min_score: float = 0.0,
     ) -> List[Tuple[EvidenceChunk, float]]:
         ...
         # Map ChromaEvidenceChunk back to EvidenceChunk
-        results = self.chroma.retrieve(query, top_k=top_k, query_embedding=embedding)
+        results = self.chroma.retrieve(
+            query, top_k=top_k, query_embedding=embedding,
+            subgoal_filter=subgoal_filter, min_score=min_score,
+        )
         return [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/rag.py` around lines 170 - 199, The retrieve_from_chroma
method currently omits subgoal_filter and min_score and doesn't forward them to
the Chroma store; update the function signature of retrieve_from_chroma to
accept subgoal_filter: str | None = None and min_score: float | None = None (in
addition to query_embedding), update the docstring to document these params, and
pass them through to self.chroma.retrieve(...) so the call becomes
self.chroma.retrieve(query, top_k=top_k, query_embedding=embedding,
subgoal_filter=subgoal_filter, min_score=min_score); keep existing behavior when
those args are None.
♻️ Duplicate comments (7)
backend/src/evaluation/metrics.py (1)

1-8: ⚠️ Potential issue | 🔴 Critical

logger is still undefined and will crash exception handling paths.

Line 241 and Line 286 call logger.warning(...), but no module-level logger is defined in this file. This raises NameError exactly when fallback handling is needed.

🐛 Proposed fix
 import json
+import logging
 import re
 from collections import Counter
 from difflib import SequenceMatcher
 from typing import Dict, List

 import numpy as np
+
+logger = logging.getLogger(__name__)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/evaluation/metrics.py` around lines 1 - 8, Add a module-level
logger so the existing exception paths that call logger.warning(...) won't raise
NameError: import the logging module at top and create logger =
logging.getLogger(__name__) (or getLogger("evaluation.metrics")); ensure any
existing logger.warning(...) uses that module-level logger and remove any
undefined local references. This change will fix the calls around the existing
warning sites (the logger.warning usages in the file) without altering the
surrounding exception handling logic.
backend/examples/gemma_providers.py (1)

252-279: ⚠️ Potential issue | 🟠 Major

Lazy model initialization is still not thread-safe.

Concurrent generate() calls can race through _lazy_load() and trigger duplicate model download/load work.

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

In `@backend/examples/gemma_providers.py` around lines 252 - 279, The lazy loader
in _lazy_load can race when generate() is called concurrently; add a
per-instance lock (e.g., self._load_lock) created in __init__ and use
double-checked locking inside _lazy_load: first check if self.llm is not None
and return, then acquire self._load_lock, re-check self.llm, perform the
kagglehub download and keras_nlp load only while holding the lock, set
self.model_path and self.llm, and finally release the lock; this prevents
duplicate downloads/loads while preserving the existing self.llm check and
behavior.
backend/src/agent/router.py (1)

18-26: ⚠️ Potential issue | 🟠 Major

Router node should not re-emit messages.

At Line 26, returning existing messages can duplicate entries through reducers. Router should return a no-op update.

💡 Proposed fix
 def router_node(state: OverallState, config: RunnableConfig):
@@
-    # We simply pass the state through; routing happens in the conditional edge.
-    return {"messages": state.get("messages", [])}
+    # Routing happens in the conditional edge; no state mutation here.
+    return {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/router.py` around lines 18 - 26, The router_node currently
re-emits the existing "messages" by returning {"messages": state.get("messages",
[])}, which causes duplicate entries; update router_node (function name:
router_node, types: OverallState, RunnableConfig) to perform a no-op update
instead (e.g., return an empty dict or otherwise avoid returning the "messages"
key) so routing decisions happen via edges only and reducers won't get duplicate
messages.
backend/src/agent/persistence.py (1)

47-49: ⚠️ Potential issue | 🟠 Major

Replace print exception handling with module logger.

At Lines 48 and 72, print bypasses structured logging and stack-trace handling in backend runtime paths.

💡 Proposed fix
 import json
+import logging
 import os
 from typing import Any, Dict, List

 PLAN_DIR = "plans"
+logger = logging.getLogger(__name__)
@@
-    except Exception as e:
-        print(f"Error saving plan for thread {thread_id}: {e}")
+    except Exception:
+        logger.exception("Error saving plan for thread %s", thread_id)
@@
-    except Exception as e:
-        print(f"Error loading plan for thread {thread_id}: {e}")
+    except Exception:
+        logger.exception("Error loading plan for thread %s", thread_id)
         return None

Also applies to: 71-73

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

In `@backend/src/agent/persistence.py` around lines 47 - 49, Replace the bare
print calls in the exception handlers in backend/src/agent/persistence.py with
structured logging: ensure a module-level logger (e.g., logger =
logging.getLogger(__name__)) is defined and change the except blocks that now do
print(f"Error saving plan for thread {thread_id}: {e}") to use
logger.exception(f"Error saving plan for thread {thread_id}") (or
logger.error(..., exc_info=True)) so the error message and stack trace are
captured; apply the same replacement to the other except block that prints
errors (the one referencing thread_id / plan persistence).
backend/src/agent/orchestration.py (1)

510-530: ⚠️ Potential issue | 🔴 Critical

tools=None is accepted but dereferenced unconditionally.

If tools is None, line 566 (tools.get_tools()) will raise AttributeError. Add a default initialization similar to agents.

Proposed fix
 def build_orchestrated_graph(
     tools: ToolRegistry | None = None,
     agents: AgentPool | None = None,
     coordinator_model: str = GEMINI_PRO,
     name: str = "orchestrated-agent",
 ) -> StateGraph:
+    if tools is None:
+        tools = ToolRegistry()
     if agents is None:
         agents = AgentPool()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/orchestration.py` around lines 510 - 530, The function
build_orchestrated_graph accepts tools=None but later calls tools.get_tools(),
causing an AttributeError when tools is None; mirror the agents handling by
initializing a default ToolRegistry when tools is None (e.g., set tools =
ToolRegistry()) before any dereference, so subsequent uses like
tools.get_tools() or references to tools are safe; update the beginning of
build_orchestrated_graph to ensure tools is a valid ToolRegistry instance
whenever it is used.
backend/src/agent/deep_search_agent.py (2)

128-132: ⚠️ Potential issue | 🟠 Major

Avoid blocking sync calls inside async research flow.

self.searcher.search(query) is synchronous and runs on the event loop thread. Offload to threads to prevent loop stalls.

Proposed fix
         # Parallel search
-        for query in queries:
-            results = self.searcher.search(query)
-            for r in results:
-                notes.append(f"Source: {r.get('url', 'unknown')}\n{r.get('content', '')}")
+        search_tasks = [asyncio.to_thread(self.searcher.search, query) for query in queries]
+        search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
+        for query, result in zip(queries, search_results):
+            if isinstance(result, Exception):
+                logger.error("Search failed for '%s': %s", query, result)
+                continue
+            for r in result:
+                notes.append(f"Source: {r.get('url', 'unknown')}\n{r.get('content', '')}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/deep_search_agent.py` around lines 128 - 132, The loop in
deep_search_agent.py is calling the synchronous searcher.search(query) inside an
async flow, blocking the event loop; change the code that iterates queries to
offload the blocking call to a thread (e.g., use asyncio.to_thread or
loop.run_in_executor) when invoking searcher.search, await the threaded task,
then process results into notes as before (references: method using
self.searcher.search and the loop that appends to notes). Ensure exceptions from
the threaded call are propagated/handled and preserve existing extraction of
'url' and 'content' from each result.

58-60: ⚠️ Potential issue | 🟠 Major

Do not fabricate search evidence when the provider is unavailable.

Returning "dummy" results introduces false citations into downstream synthesis. Return an empty list (or explicit error metadata) instead.

Proposed fix
         if not self.tool:
-            logger.warning("Search tool not initialized. Returning dummy results.")
-            return [{"url": "dummy", "content": f"Dummy result for '{query}'"}]
+            logger.warning("Search tool not initialized; returning no results.")
+            return []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/deep_search_agent.py` around lines 58 - 60, The current
branch in the method that checks self.tool logs a warning and returns fabricated
dummy results (logger.warning + returning [{"url": "dummy", ...}]); change this
to not fabricate evidence by returning an empty list ([]) or a clear error
object instead of dummy results, and update the logger.warning message to
reflect that no results are available because the search provider (self.tool) is
uninitialized; ensure callers of this method can handle the empty list or error
metadata returned.
🧹 Nitpick comments (19)
frontend/src/components/ChatMessagesView.tsx (1)

291-308: Comparator currently re-renders all items on copy changes.

On Line 303, direct copiedMessageId comparison forces every MessageItem to re-render whenever any message is copied. Compare per-item copied state instead.

💡 Proposed optimization
 function areMessageItemPropsEqual(prev: MessageItemProps, next: MessageItemProps) {
@@
   const prevActivity = prev.message.id ? prev.historicalActivities[prev.message.id] : undefined
   const nextActivity = next.message.id ? next.historicalActivities[next.message.id] : undefined
+  const prevIsCopied = prev.copiedMessageId != null && prev.copiedMessageId === prev.message.id
+  const nextIsCopied = next.copiedMessageId != null && next.copiedMessageId === next.message.id

   return (
@@
-    prev.copiedMessageId === next.copiedMessageId &&
+    prevIsCopied === nextIsCopied &&
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ChatMessagesView.tsx` around lines 291 - 308, The
comparator areMessageItemPropsEqual currently compares the global
copiedMessageId which causes every MessageItem to re-render when any message is
copied; change the copied check to a per-item boolean by comparing whether
prev.copiedMessageId === prev.message.id and next.copiedMessageId ===
next.message.id (i.e., compute prevIsCopied and nextIsCopied) and use that in
the return instead of prev.copiedMessageId === next.copiedMessageId so only the
affected MessageItem re-renders.
backend/src/evaluation/bench.py (1)

221-258: Avoid recomputing Pass@1 in the same query evaluation.

Line 253 recomputes pass_at_1_accuracy(...) even though the same metric is already computed earlier in the same method. Reuse the first result to reduce duplicate work.

♻️ Proposed refactor
         # Compute metrics
+        pass_at_1_result = self.metrics.pass_at_1_accuracy(
+            generated_answer=generated_answer,
+            reference_answer=reference["reference_answer"],
+            key_facts=reference["key_facts"],
+        )
+
         results = {
             "query_id": query_id,
             "query": query,
             "generated_answer": generated_answer,
             # Metric 1: Pass@1 Accuracy
-            "pass_at_1": self.metrics.pass_at_1_accuracy(
-                generated_answer=generated_answer,
-                reference_answer=reference["reference_answer"],
-                key_facts=reference["key_facts"],
-            ),
+            "pass_at_1": pass_at_1_result,
             # Metric 2: Evidence Quality
             "evidence_quality": self.metrics.evidence_quality_score(
                 retrieved_docs=retrieved_docs,
                 required_sources=reference["required_sources"],
                 min_evidence_count=reference["min_evidence_count"],
@@
             "context_efficiency": self.metrics.context_efficiency(
                 final_answer_length=len(generated_answer),
                 total_context_length=len(context_used),
-                answer_quality_score=self.metrics.pass_at_1_accuracy(
-                    generated_answer,
-                    reference["reference_answer"],
-                    reference["key_facts"],
-                )["score"],
+                answer_quality_score=pass_at_1_result["score"],
             ),
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/evaluation/bench.py` around lines 221 - 258, The code currently
calls self.metrics.pass_at_1_accuracy(...) twice (once for "pass_at_1" and again
when computing context_efficiency), so change it to call pass_at_1_accuracy once
into a local variable (e.g., pass_at_1_result =
self.metrics.pass_at_1_accuracy(...)), assign that variable to the "pass_at_1"
field and pass pass_at_1_result["score"] to context_efficiency (instead of
recomputing), leaving other metric calls unchanged; reference the functions
pass_at_1_accuracy and context_efficiency and the variables generated_answer and
reference["reference_answer"]/reference["key_facts"] to locate where to make the
change.
backend/src/agent/utils.py (1)

29-34: Non-human/non-assistant messages are silently dropped.

In multi-message mode, message types outside the two explicit branches are omitted, which can lose context in the assembled topic string.

♻️ Proposed fix
         for message in messages:
             if isinstance(message, HumanMessage):
                 research_topic += f"User: {message.content}\n"
             elif isinstance(message, AIMessage):
                 research_topic += f"Assistant: {message.content}\n"
+            else:
+                research_topic += f"{message.content}\n"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/utils.py` around lines 29 - 34, The loop that builds
research_topic currently only handles HumanMessage and AIMessage so other
message types get dropped; in the messages iteration (the for message in
messages loop) add a fallback branch that appends non-human/non-assistant
messages to research_topic (include the message class name and its content or a
safe stringified fallback) so types other than HumanMessage and AIMessage are
preserved; update the logic around the research_topic construction to use
message.__class__.__name__ and getattr(message, "content", str(message)) in the
fallback.
backend/examples/gemma_providers.py (1)

300-301: Normalize generate() output to ensure consistent string return across initialization variants.

With GemmaCausalLM.from_preset(), a preprocessor is attached by default, so generate() returns str for single prompts. However, calling str() directly is redundant and will produce unwanted text representation if the preprocessor is ever disabled (preprocessor=None) or if batch inputs are passed. The proposed fix is partially correct but incomplete—it handles list and numpy arrays but not the dict case that occurs with preprocessor=None.

         output = self.llm.generate(prompt, max_length=max_length, **kwargs)
-        return str(output)
+        if isinstance(output, str):
+            return output
+        if isinstance(output, (list, tuple)) and output:
+            return str(output[0])
+        if isinstance(output, dict) and "token_ids" in output:
+            # Handle preprocessor=None case (raw token output)
+            token_ids = output["token_ids"]
+            if hasattr(token_ids, "numpy"):
+                token_ids = token_ids.numpy()
+            if hasattr(token_ids, "tolist"):
+                token_ids = token_ids.tolist()
+            return str(token_ids)
+        if hasattr(output, "numpy"):
+            arr = output.numpy()
+            if hasattr(arr, "tolist"):
+                arr = arr.tolist()
+            if isinstance(arr, list) and arr:
+                return str(arr[0])
+        return str(output)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/examples/gemma_providers.py` around lines 300 - 301, The generate()
return value should be normalized to a consistent string: in the wrapper where
you call self.llm.generate (used by GemmaCausalLM.from_preset and other
initializers), detect common output shapes and convert them to a single
string—if the result is a list or numpy.ndarray, take the first element (or join
elements for multi-input cases if you intend batch-to-single behavior); if it is
a dict (happens when preprocessor=None), extract the text field(s) (e.g.,
'generated_text' or 'text') or take the first value; otherwise stringify safely.
Update the code around the output = self.llm.generate(...) return str(output) to
perform these type checks and normalize to a single string before returning.
backend/src/agent/tools_and_schemas.py (1)

148-154: Add docstrings to read_file_wrapper and write_file_wrapper for consistency.

The list_directory_wrapper has a docstring, but the other two wrappers do not. Consider adding brief docstrings for consistency.

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

In `@backend/src/agent/tools_and_schemas.py` around lines 148 - 154, Add concise
docstrings to both read_file_wrapper and write_file_wrapper matching the style
of list_directory_wrapper: one-line summary of purpose, brief param descriptions
for path (and content for write_file_wrapper), and return description indicating
it returns file contents or an error string on failure; place the docstrings
immediately under each async def to maintain consistency with existing wrappers
and usage.
backend/src/config/app_config.py (2)

40-48: Inconsistent use of default_factory vs direct assignment.

Lines 40-48 use field(default_factory=lambda: int(...)) while other fields (e.g., line 14) use direct os.getenv() calls. Both patterns work, but the inconsistency is confusing. For a frozen dataclass where fields are set once at instantiation, direct assignment suffices:

Proposed simplification
     # Budgets & Performance
-    token_budget: int = field(
-        default_factory=lambda: int(os.getenv("TOKEN_BUDGET", "50000"))
-    )
-    call_budget: int = field(
-        default_factory=lambda: int(os.getenv("CALL_BUDGET", "50"))
-    )
-    latency_target_ms: int = field(
-        default_factory=lambda: int(os.getenv("LATENCY_TARGET_MS", "8000"))
-    )
+    token_budget: int = int(os.getenv("TOKEN_BUDGET", "50000"))
+    call_budget: int = int(os.getenv("CALL_BUDGET", "50"))
+    latency_target_ms: int = int(os.getenv("LATENCY_TARGET_MS", "8000"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/config/app_config.py` around lines 40 - 48, The token_budget,
call_budget, and latency_target_ms fields use field(default_factory=...) while
other fields use direct assignment; simplify by replacing these with direct
assignments using int(os.getenv(...)) (e.g., set token_budget =
int(os.getenv("TOKEN_BUDGET", "50000"))), keeping behavior identical for the
frozen dataclass and referencing the same symbols token_budget, call_budget, and
latency_target_ms so instantiation remains unchanged.

100-102: Use bare raise instead of raise e.

Consistent with other files, prefer bare raise to preserve the full exception traceback.

Proposed fix
         except ValueError as e:
             logger.error(f"Configuration loading failed: {e}")
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/config/app_config.py` around lines 100 - 102, The except block
currently does "except ValueError as e: logger.error(...); raise e" which
re-raises the exception with a new traceback; replace the explicit "raise e"
with a bare "raise" so the original traceback is preserved — update the except
ValueError as e handler in app_config.py (the block that logs "Configuration
loading failed: {e}") to use bare raise.
backend/src/search/providers/bing_adapter.py (1)

114-116: Use bare raise instead of raise e.

Using raise e instead of bare raise can obscure the original traceback in some Python versions. Prefer bare raise to preserve the full exception context.

Proposed fix
         except Exception as e:
             logger.error(f"Bing Search failed: {e}")
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/bing_adapter.py` around lines 114 - 116, In the
except block inside the Bing adapter (where it currently does "except Exception
as e:" and calls logger.error(f\"Bing Search failed: {e}\")), replace the
explicit "raise e" with a bare "raise" so the original traceback/context is
preserved; keep the logger.error call but change the re-raise to a bare raise to
maintain full exception chaining in the function that performs the Bing search.
backend/src/agent/llm_client.py (2)

64-66: Use bare raise instead of raise e.

Consistent with other files in this PR, prefer bare raise to preserve the full exception traceback.

Proposed fix
     except Exception as e:
         logger.warning(f"LLM call failed (attempting retry): {e}")
-        raise e
+        raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/llm_client.py` around lines 64 - 66, In the except
Exception handler in backend/src/agent/llm_client.py (the block that logs "LLM
call failed (attempting retry): {e}" and currently does "raise e"), change the
re-raise to a bare "raise" so the original traceback is preserved; locate the
except Exception as e block surrounding the LLM call (the code that uses
logger.warning) and replace the explicit re-raise with a bare re-raise.

88-91: Consider adding a comment explaining the deferred import.

The import inside __init__ is likely to avoid circular dependencies. Adding a brief comment would clarify this intent for future maintainers.

Proposed clarification
+        # Deferred import to avoid circular dependency with tool_adapter
         from agent.tool_adapter import (
             GEMMA_TOOL_INSTRUCTION,
             format_tools_to_json_schema,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/llm_client.py` around lines 88 - 91, Add a brief inline
comment above the deferred import in LLMAgent.__init__ (the import of
GEMMA_TOOL_INSTRUCTION and format_tools_to_json_schema from agent.tool_adapter)
stating that the import is deferred to avoid circular imports and to ensure
dependencies (e.g., agent.tool_adapter) are only loaded at runtime; reference
the symbols GEMMA_TOOL_INSTRUCTION and format_tools_to_json_schema and mention
that this prevents import-time cycles when initializing the LLMAgent.
backend/src/agent/tool_adapter.py (1)

186-188: Move uuid import to module level.

The import uuid statement is inside the loop, which runs on every iteration. While Python caches imports, this is non-idiomatic. Move the import to the top of the file with other imports.

Proposed fix

At the top of the file (after line 6):

import uuid

Then update line 186-188:

-            import uuid
-
             call_id = f"call_{uuid.uuid4().hex[:8]}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/tool_adapter.py` around lines 186 - 188, The uuid import is
inside the loop where call_id is created (call_id =
f"call_{uuid.uuid4().hex[:8]}"); move the import to module level with the other
imports by adding a top-of-file "import uuid" and remove the inline "import
uuid" so the code uses uuid.uuid4() without importing inside the loop.
backend/src/search/providers/brave_adapter.py (2)

56-57: Inconsistent error handling compared to BingAdapter.

BraveSearchAdapter.search raises ValueError when API key is missing, while BingAdapter.search returns an empty list. Consider aligning the behavior for consistency across search providers, or document this intentional difference clearly.

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

In `@backend/src/search/providers/brave_adapter.py` around lines 56 - 57,
BraveSearchAdapter.search currently raises ValueError when self.api_key is
missing, which is inconsistent with BingAdapter.search that returns an empty
list; update BraveSearchAdapter.search to mirror BingAdapter.search behavior by
returning an empty list ([]) when self.api_key is falsy, or if you prefer
raising an error, update BingAdapter.search to raise the same ValueError —
change the conditional in BraveSearchAdapter.search (the check of self.api_key)
to return [] instead of raising, and ensure any callers expecting a list are
unaffected.

93-95: Use bare raise instead of raise e.

Same as BingAdapter - prefer bare raise to preserve the full exception traceback.

Proposed fix
         except Exception as e:
             logger.error(f"Brave Search failed: {e}")
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/brave_adapter.py` around lines 93 - 95, The
except block that currently logs with logger.error(f"Brave Search failed: {e}")
and then does "raise e" should be changed to re-raise the original exception
with a bare "raise" to preserve the full traceback; locate the except handling
in brave_adapter.py (the block that logs "Brave Search failed") and replace the
explicit re-raise with a bare raise while keeping the existing logging call.
backend/src/agent/models.py (1)

105-111: Unify valid-model definitions to avoid drift.

is_valid_model() and ALL_VALID_MODELS currently duplicate the same model set. Keep one source of truth so future model additions don’t diverge.

♻️ Proposed refactor
 def is_valid_model(model_name: str) -> bool:
@@
-    valid_models = {
-        GEMINI_FLASH,
-        GEMINI_FLASH_LITE,
-        GEMINI_PRO,
-        GEMMA_2_27B_IT,
-        GEMMA_3_27B_IT,
-    }
+    valid_models = set(ALL_VALID_MODELS)

Also applies to: 176-182

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

In `@backend/src/agent/models.py` around lines 105 - 111, The code duplicates the
same model set in multiple places (valid_models, ALL_VALID_MODELS, and
is_valid_model()), causing drift; refactor to a single source of truth by
defining one canonical constant (e.g., ALL_VALID_MODELS or VALID_MODELS) and
have is_valid_model() and any other consumers reference that constant instead of
duplicating the set; update references in the file (including the blocks around
valid_models and the is_valid_model() implementation) to use the unified
constant and remove the redundant set definitions.
backend/src/search/providers/duckduckgo_adapter.py (1)

71-73: Bare raise e is redundant.

Using raise e instead of bare raise prevents the interpreter from preserving the original traceback context in some edge cases. Consider using bare raise.

Suggested fix
         except Exception as e:
             logger.error(f"DuckDuckGo search failed: {e}")
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/providers/duckduckgo_adapter.py` around lines 71 - 73, The
except block in the DuckDuckGo adapter (see the exception handler in
duckduckgo_adapter.py, around the logger.error call) currently uses "raise e"
which can lose original traceback context; change it to a bare "raise" so the
original exception traceback is preserved while still logging the error (i.e.,
keep logger.error(f\"DuckDuckGo search failed: {e}\") and replace "raise e" with
a bare "raise").
backend/src/search/router.py (1)

144-148: Fallback provider lacks retry logic.

The primary provider gets two attempts (tuned=True, then tuned=False), but the fallback provider only gets a single attempt with default parameters. If the fallback also experiences a transient error, it will fail without retry.

Consider applying the same retry pattern to the fallback for consistency, or document this as intentional (single-shot fallback to avoid excessive latency).

Optional: Apply retry logic to fallback
                     if fallback_provider:
-                        # Fallback gets the same retry logic or just a single shot?
-                        # For simplicity, fallback is usually single shot untuned or standard.
-                        # Let's try standard (tuned=True default)
-                        return fallback_provider.search(query, max_results=max_results)
+                        try:
+                            return fallback_provider.search(query, max_results=max_results, tuned=True)
+                        except Exception as e3:
+                            logger.warning(f"Fallback search failed (tuned=True): {e3}")
+                            return fallback_provider.search(query, max_results=max_results, tuned=False)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/search/router.py` around lines 144 - 148, The
fallback_provider.search call currently uses a single default invocation; apply
the same two-attempt retry pattern as the primary provider by calling
fallback_provider.search(query, max_results=max_results, tuned=True) first and
if that raises/returns an error then call fallback_provider.search(query,
max_results=max_results, tuned=False) before propagating the error; update the
error handling in the same function (where fallback_provider is used) to mirror
the primary provider's try/except/logic so transient failures on fallback are
retried once.
backend/src/agent/gemma_client.py (1)

92-94: Bare raise e is redundant.

Same pattern as in duckduckgo_adapter.py. Using bare raise preserves the original traceback better.

Suggested fix
         except Exception as e:
             logger.error(f"Vertex AI prediction failed: {e}")
-            raise e
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/gemma_client.py` around lines 92 - 94, In the exception
handler inside gemma_client.py (the except Exception as e block handling Vertex
AI prediction failures), replace the redundant "raise e" with a bare "raise" so
the original traceback is preserved; update the handler that currently logs via
logger.error(f"Vertex AI prediction failed: {e}") to re-raise the exception
using "raise" instead of re-raising the bound variable (mirroring the fix
applied in duckduckgo_adapter.py).
backend/src/agent/nodes.py (1)

992-993: Remove redundant import json statement.

json is already imported at module level (line 14). This local import is unnecessary.

♻️ Proposed fix
                 # Heuristic: Find first { and last }
-                import json
-
                 start = content.find("{")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/nodes.py` around lines 992 - 993, Remove the redundant
local import of json in backend/src/agent/nodes.py: delete the inner "import
json" (the one shown in the diff) since json is already imported at module
level; ensure no other local references rely on a re-import and run
tests/linters to confirm the unused import is gone.
backend/src/agent/app.py (1)

354-360: Replace traceback.print_exc() with structured logging.

Using traceback.print_exc() writes to stderr which may not be captured by the project's logging infrastructure. For consistent, configurable logging across environments, use logger.exception() which automatically includes the traceback.

♻️ Proposed fix
         except Exception:
-            traceback.print_exc()
+            logger.exception("Error during SSE stream processing")
             # Security: Don't leak exception details to client
             yield (
                 f"event: error\n"

Apply the same pattern at lines 390 and 427.

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

In `@backend/src/agent/app.py` around lines 354 - 360, Replace
traceback.print_exc() with structured logging by calling logger.exception(...)
so the traceback is captured by the project's logging system; keep the same SSE
error yield payload (the f"event: error..." block) to avoid leaking details to
clients. Specifically, in the except Exception handlers that produce the SSE
error response (the generator/function containing the yield of the error event),
remove traceback.print_exc() and call logger.exception("Stream processing
error") or similar contextual message. Apply the same replacement to the other
identical except Exception blocks in this module that emit the SSE error (the
two other catch blocks with the same yield pattern).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 351f364 and b1e198e.

📒 Files selected for processing (61)
  • backend/examples/gemma_providers.py
  • backend/src/agent/_graph.py
  • backend/src/agent/app.py
  • backend/src/agent/configuration.py
  • backend/src/agent/deep_search_agent.py
  • backend/src/agent/gemma_client.py
  • backend/src/agent/graph.py
  • backend/src/agent/graph_builder.py
  • backend/src/agent/graphs/linear.py
  • backend/src/agent/graphs/supervisor.py
  • backend/src/agent/kg.py
  • backend/src/agent/llm_client.py
  • backend/src/agent/mcp_client.py
  • backend/src/agent/mcp_config.py
  • backend/src/agent/mcp_persistence.py
  • backend/src/agent/mcp_server.py
  • backend/src/agent/memory_tools.py
  • backend/src/agent/models.py
  • backend/src/agent/nodes.py
  • backend/src/agent/orchestration.py
  • backend/src/agent/persistence.py
  • backend/src/agent/planning_router.py
  • backend/src/agent/rag.py
  • backend/src/agent/rag_nodes.py
  • backend/src/agent/rate_limiter.py
  • backend/src/agent/registry.py
  • backend/src/agent/research_tools.py
  • backend/src/agent/router.py
  • backend/src/agent/security.py
  • backend/src/agent/state.py
  • backend/src/agent/tool_adapter.py
  • backend/src/agent/tools_and_schemas.py
  • backend/src/agent/utils.py
  • backend/src/config/app_config.py
  • backend/src/config/validation.py
  • backend/src/evaluation/bench.py
  • backend/src/evaluation/data.py
  • backend/src/evaluation/metrics.py
  • backend/src/evaluation/mle_bench.py
  • backend/src/observability/config.py
  • backend/src/observability/langfuse.py
  • backend/src/rag/chroma_store.py
  • backend/src/search/provider.py
  • backend/src/search/providers/bing_adapter.py
  • backend/src/search/providers/brave_adapter.py
  • backend/src/search/providers/duckduckgo_adapter.py
  • backend/src/search/providers/google_adapter.py
  • backend/src/search/providers/tavily_adapter.py
  • backend/src/search/router.py
  • frontend/src/App.tsx
  • frontend/src/components/ChatMessagesView.tsx
  • frontend/src/components/ui/badge.tsx
  • frontend/src/components/ui/button.tsx
  • frontend/src/components/ui/card.tsx
  • frontend/src/components/ui/input.tsx
  • frontend/src/components/ui/scroll-area.tsx
  • frontend/src/components/ui/select.tsx
  • frontend/src/components/ui/tabs.tsx
  • frontend/src/components/ui/textarea.tsx
  • frontend/src/hooks/useAgentState.ts
  • frontend/src/lib/utils.ts
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/components/ui/select.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • frontend/src/lib/utils.ts
  • backend/src/evaluation/mle_bench.py
  • backend/src/agent/security.py
  • frontend/src/App.tsx
  • backend/src/config/validation.py
  • backend/src/observability/config.py
  • frontend/src/hooks/useAgentState.ts
  • backend/src/agent/graphs/linear.py
  • frontend/src/components/ui/input.tsx

Comment on lines 383 to 390
while finish_reason == "length":
continuation = llm.invoke("Please continue from where you left off:\n" + final_answer[-500:])
continuation = llm.invoke(
"Please continue from where you left off:\n" + final_answer[-500:]
)
final_answer += "\n" + getattr(continuation, "content", str(continuation))
response_metadata = getattr(continuation, "response_metadata", {})
finish_reason = response_metadata.get("finish_reason")
time.sleep(2)

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

Potential infinite loop in truncated response handling.

The while finish_reason == "length" loop will continue indefinitely if the model keeps returning truncated responses. Consider adding a maximum iteration limit to prevent runaway loops.

Suggested fix: Add iteration limit
     # Handle truncated responses
     response_metadata = getattr(result, "response_metadata", {})
     finish_reason = response_metadata.get("finish_reason")
+    continuation_count = 0
+    max_continuations = 5
-    while finish_reason == "length":
+    while finish_reason == "length" and continuation_count < max_continuations:
+        continuation_count += 1
         continuation = llm.invoke(
             "Please continue from where you left off:\n" + final_answer[-500:]
         )
         final_answer += "\n" + getattr(continuation, "content", str(continuation))
         response_metadata = getattr(continuation, "response_metadata", {})
         finish_reason = response_metadata.get("finish_reason")
         time.sleep(2)
+
+    if continuation_count >= max_continuations:
+        logger.warning("Max continuations reached; answer may be truncated.")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/_graph.py` around lines 383 - 390, The loop that handles
truncated LLM responses (while finish_reason == "length") can run forever if the
model keeps returning length-truncated continuations; add a maximum retry
counter (e.g., max_retries) and increment it each iteration, breaking the loop
and logging or raising an error after exceeding the limit. Modify the loop
around llm.invoke, continuation, final_answer and response_metadata to track
attempts, use a configurable cap, and ensure you still append the last
continuation before breaking so final_answer remains consistent.

Comment thread backend/src/agent/graph_builder.py Outdated
Comment on lines +66 to +67
) -> StateGraph:
"""Build a customized agent graph based on feature flags.

Args:
enable_planning: Include planning mode for user review (default: False)
enable_reflection: Include reflection loop for follow-up queries (default: True)
enable_validation: Include web result validation (default: True)
enable_compression: Include result compression (default: False)
enable_rag: Include RAG retrieval node (default: False)
enable_kg: Include Knowledge Graph enrichment (default: False)
parallel_search: Use parallel Send for web research (default: True)
name: Name for the compiled graph

"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n backend/src/agent/graph_builder.py | head -80

Repository: MasumRab/gemini-fullstack-langgraph-quickstart

Length of output: 3261


🏁 Script executed:

cat backend/src/agent/graph_builder.py | sed -n '160,180p' | cat -n

Repository: MasumRab/gemini-fullstack-langgraph-quickstart

Length of output: 764


🏁 Script executed:

find . -name "*.txt" -o -name "*.toml" -o -name "*.cfg" | grep -E "(requirements|pyproject|setup)" | head -5

Repository: MasumRab/gemini-fullstack-langgraph-quickstart

Length of output: 114


🌐 Web query:

langgraph 0.2.6 StateGraph.compile() return type CompiledStateGraph

💡 Result:

In LangGraph Python v0.2.6, StateGraph.compile() returns a CompiledStateGraph instance (the implementation constructs CompiledStateGraph(...) and returns it after validate()), even though some docs/type hints describe it more generically as a CompiledGraph. [1]

So you can treat the return value as:

  • Runtime type: CompiledStateGraph
  • Type relationship: CompiledStateGraph is a specialized “compiled graph” (i.e., it behaves like a compiled runnable graph / CompiledGraph-style object). [1]

Typing example (Python):

from langgraph.graph import StateGraph
from langgraph.graph.state import CompiledStateGraph  # where available in your version

compiled: CompiledStateGraph = StateGraph(State).compile()

Source: LangGraph StateGraph.compile() reference + shown source snippet returning CompiledStateGraph. [1]

References:
[1] (baihezi.com)

Citations:


Return annotation should match the compiled graph type.

build_graph() returns builder.compile(...), which is a CompiledStateGraph instance in langgraph 0.2.6, not a raw StateGraph. Update the type annotation to reflect the actual return type.

♻️ Fix
 from langgraph.graph import END, START, StateGraph
+from langgraph.graph.state import CompiledStateGraph
@@
-) -> StateGraph:
+) -> CompiledStateGraph:

Also applies to: 172-172

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

In `@backend/src/agent/graph_builder.py` around lines 66 - 67, The return type
annotation on build_graph (and the other occurrence noted) is incorrect: change
the annotated return type from StateGraph to CompiledStateGraph to match
builder.compile(...)'s actual return value; import CompiledStateGraph from the
langgraph package (or the module where it's exported) and update the function
signatures (e.g., build_graph) to use CompiledStateGraph so the type matches the
compiled graph instance.

Comment thread backend/src/agent/kg.py
Comment on lines +44 to +46
results = state.get("validated_web_research_result", []) or state.get(
"web_research_result", []
)

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

Do not fall back to unvalidated results when validated output is empty.

At Line 44, using or means an empty validated_web_research_result falls back to raw web_research_result. That can bypass validation filtering and enrich KG with content that was intentionally rejected.

💡 Proposed fix
-    results = state.get("validated_web_research_result", []) or state.get(
-        "web_research_result", []
-    )
+    if "validated_web_research_result" in state:
+        results = state.get("validated_web_research_result") or []
+    else:
+        results = state.get("web_research_result", [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/kg.py` around lines 44 - 46, The current assignment to
results uses "or" so an empty validated_web_research_result will fall back to
web_research_result; change the logic to prefer the validated key only if it
exists in state (even if it's an empty list) and otherwise fall back to
web_research_result. Concretely, replace the expression that computes results
with a conditional that checks for the presence of the
"validated_web_research_result" key in state (e.g., using "if
'validated_web_research_result' in state") and assigns
state["validated_web_research_result"] when present, otherwise assigns
state.get("web_research_result", []); reference the results variable and the
keys "validated_web_research_result" and "web_research_result" to locate where
to change it.

Comment on lines +226 to 229
p.write_text(content, encoding="utf-8")
return ToolResult(
success=True,
data={"path": path, "bytes_written": len(content)}
success=True, data={"path": path, "bytes_written": len(content)}
)

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 | 🟡 Minor

bytes_written reports character count, not actual bytes written.

Line 228 uses len(content) which counts characters, but content_bytes (which may differ for multi-byte UTF-8 characters) was already computed on line 216. For accuracy, report the actual bytes written.

🔧 Proposed fix
             p.write_text(content, encoding="utf-8")
             return ToolResult(
-                success=True, data={"path": path, "bytes_written": len(content)}
+                success=True, data={"path": path, "bytes_written": len(content_bytes)}
             )
📝 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
p.write_text(content, encoding="utf-8")
return ToolResult(
success=True,
data={"path": path, "bytes_written": len(content)}
success=True, data={"path": path, "bytes_written": len(content)}
)
p.write_text(content, encoding="utf-8")
return ToolResult(
success=True, data={"path": path, "bytes_written": len(content_bytes)}
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/mcp_server.py` around lines 226 - 229, The return value
currently reports character count using len(content); change it to report actual
bytes written by using the already-computed content_bytes size (e.g., use
len(content_bytes) or content_bytes.__len__()) in the ToolResult for
bytes_written so multi-byte UTF-8 characters are counted correctly; update the
return in the function/method that calls p.write_text (referenced in
mcp_server.py around the write block) to use content_bytes instead of content.

Comment on lines 282 to +287
async def start(self):
# In a real MCP server, this would start stdio/sse transport.
# For this implementation (in-process usage in notebooks), it's a no-op
# or sets up internal state.
pass
"""Start the MCP server.

In a real MCP server, this would start stdio/sse transport.
For this implementation (in-process usage), it is intentionally a no-op.
"""

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

Missing method body causes IndentationError.

The start method has a docstring but no executable statement. Python requires at least a pass statement for empty method bodies.

🐛 Proposed fix
     async def start(self):
         """Start the MCP server.

         In a real MCP server, this would start stdio/sse transport.
         For this implementation (in-process usage), it is intentionally a no-op.
         """
+        pass
📝 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
async def start(self):
# In a real MCP server, this would start stdio/sse transport.
# For this implementation (in-process usage in notebooks), it's a no-op
# or sets up internal state.
pass
"""Start the MCP server.
In a real MCP server, this would start stdio/sse transport.
For this implementation (in-process usage), it is intentionally a no-op.
"""
async def start(self):
"""Start the MCP server.
In a real MCP server, this would start stdio/sse transport.
For this implementation (in-process usage), it is intentionally a no-op.
"""
pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/agent/mcp_server.py` around lines 282 - 287, The async start
method currently contains only a docstring and no executable statements, which
raises an IndentationError; add a minimal body (e.g., a single pass or return)
inside the async def start(self): method in backend/src/agent/mcp_server.py (the
start coroutine) so the method is syntactically valid while preserving the
intended no-op behavior.

Comment thread backend/src/evaluation/metrics.py
Comment thread backend/src/rag/chroma_store.py Outdated
Comment thread backend/src/search/providers/tavily_adapter.py
Comment thread frontend/src/components/ChatMessagesView.tsx Outdated
Comment thread frontend/src/components/ChatMessagesView.tsx
@MasumRab

MasumRab commented Jun 9, 2026

Copy link
Copy Markdown
Owner Author

@jules Resolve conflicts: git fetch origin && git rebase origin/main && git push --force-with-lease

Avoid full repo diff - focus only on your changed paths.
Report when ready.

Copy link
Copy Markdown
Owner Author

Jules Session Analysis: This PR has merge conflicts. Recommended fix: git fetch origin && git rebase origin/main. Resolve conflicts and push. Let me know if you need help!

@MasumRab

Copy link
Copy Markdown
Owner Author

@jules your gemini and gemma integration changes may need to be updated

Resolved conflicts:
- For docs/tasks/06_GEMMA_INTEGRATION.md: kept PR version with Kaggle integration
- For backend/examples/gemma_providers.py: kept PR version with KaggleGemmaClient
- For all other files: accepted upstream/main version

Co-authored-by: openhands <openhands@all-hands.dev>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gemini-fullstack-langgraph-quickstart Ready Ready Preview Jul 30, 2026 7:24pm

- Add thread-safety to KaggleGemmaClient with double-checked locking
- Fix validate-env.yml path pattern to include subdirectories
- Fix deep_search_agent to return empty list instead of dummy results
- Use asyncio.to_thread for non-blocking search calls in deep_search_agent
- Add dedicated scoping_wait node in graph.py
- Apply confidence_threshold in metrics.py when counting completed subgoals
- Coerce metadata values to float in chroma_store.py
- Populate raw_content field in SearchResult for tavily_adapter
- Fix copy button gating and timeout race in ChatMessagesView.tsx

Co-authored-by: openhands <openhands@all-hands.dev>
@mergify

mergify Bot commented Jul 30, 2026

Copy link
Copy Markdown

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 📃 Configuration Change Requirements

Mergify configuration change

  • check-success = Configuration changed

…h fixes

- Add thread-safety to KaggleGemmaClient with double-checked locking
- Fix validate-env.yml path pattern to include subdirectories
- Fix deep_search_agent to return empty list instead of dummy results
- Use asyncio.to_thread for non-blocking search calls in deep_search_agent
- Add dedicated scoping_wait node in graph.py
- Apply confidence_threshold in metrics.py when counting completed subgoals
- Coerce metadata values to float in chroma_store.py
- Populate raw_content field in SearchResult for tavily_adapter
- Fix copy button gating and timeout race in ChatMessagesView.tsx

Co-authored-by: openhands <openhands@all-hands.dev>
The SubGoal class was accidentally removed during merge conflict
resolution but is still referenced in the research() method.

Co-authored-by: openhands <openhands@all-hands.dev>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
30.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants