Skip to content

fix al workflow#26

Merged
Germanadjemian merged 5 commits intomainfrom
feature/fallback_inicial
Dec 14, 2025
Merged

fix al workflow#26
Germanadjemian merged 5 commits intomainfrom
feature/fallback_inicial

Conversation

@NicoWagner2005
Copy link
Copy Markdown
Collaborator

nodo condicional guard te lleva a fallback_inicial si malicious sino a parafraseo y el segundo guard si malicious te lleva a fallback_final sino termina

…i malicious sino a parafraseo y el segundo guard si malicious te lleva a fallback_final sino termina
Copilot AI review requested due to automatic review settings December 13, 2025 14:48
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Dec 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Simplified agent processing flow by streamlining intermediate steps for improved performance.
    • Enhanced prompt handling to use message history for better context awareness.
    • Improved paraphrasing and context generation with language model-driven processing.
    • Refined state management architecture for more efficient data handling.
    • Updated security validation to work with the new message-based input system.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Rewires the agent graph to replace fallback_inicial/generator/fallback_final with a single fallback node, changes routing via route_after_guard, shifts context and prompt handling to message-based LLM calls, and updates node imports/exports and state base type.

Changes

Cohort / File(s) Summary
Graph wiring
RAGManager/app/agents/graph.py
Removed fallback_inicial, generator, fallback_final; added fallback; guard now routes malicious → fallback, continue → parafraseo; context_builder now routes to guard; added fallback -> END; routing import updated (route_after_guard).
Node exports
RAGManager/app/agents/nodes/__init__.py
Replaced exports: removed fallback_inicial, generator, fallback_final; added fallback; imports adjusted.
Routing
RAGManager/app/agents/routing.py
Removed route_after_fallback_final; preserved route_after_guard.
Prompt sourcing (host & guard)
RAGManager/app/agents/nodes/agent_host.py, RAGManager/app/agents/nodes/guard.py
Both now extract prompt from last element of state["messages"] (message-based flow); guard logging switched to length-based log.
Context & paraphrase nodes (LLM-driven)
RAGManager/app/agents/nodes/context_builder.py, RAGManager/app/agents/nodes/parafraseo.py
Introduced SystemMessage/ChatOpenAI usage and global llm; context_builder builds system prompt and returns {"messages":[response]}; parafraseo uses LLM to set paraphrased_text.
New fallback node
RAGManager/app/agents/nodes/fallback.py
New LLM-based fallback that logs warning, prepends SystemMessage, invokes llm, and returns {"messages":[error_message]}.
Removed nodes
RAGManager/app/agents/nodes/fallback_final.py, .../fallback_inicial.py, .../generator.py
Deleted prior fallback and generator modules and their functions.
State model
RAGManager/app/agents/state.py
AgentState now subclasses MessagesState (message-based state) instead of TypedDict.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client as AgentHost
participant Guard
participant Parap as Parafraseo
participant Retriever
participant Context as ContextBuilder
participant Fallback
participant END

Client->>Guard: send last message
alt guard: malicious
    Guard->>Fallback: route malicious
    Fallback->>END: produce error messages -> END
else guard: continue
    Guard->>Parap: continue -> paraphrase (LLM)
    Parap->>Retriever: paraphrased -> retrieve context
    Retriever->>Context: pass chunks
    Context->>Guard: build system prompt, invoke LLM -> messages
    Guard->>Guard: route_after_guard decision
    alt second guard: malicious
        Guard->>Fallback: route malicious
        Fallback->>END: error -> END
    else
        Guard->>END: continue -> END
    end
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Files/areas to focus review on:

  • context_builder.py and fallback.py for LLM invocation correctness, return payload shape, and message format.
  • graph.py routing changes and consistency with renamed node exports.
  • state.py change to MessagesState — ensure consumers expecting TypedDict still work.
  • agent_host.py / guard.py prompt extraction assumptions (message list presence and types).

Possibly related PRs

Suggested reviewers

  • Germanadjemian

Poem

"I hopped through wires and nodes today,
Swapped old trails for one bright way.
Guard checks, a fallback hums its tune,
Messages dance beneath the moon.
— your rabbit, with a tiny spoon 🐇"

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'fix al workflow' is vague and does not clearly describe the specific changes made. While it references workflow modification, it lacks meaningful detail about what was actually changed. Use a more descriptive title that specifies the main change, such as 'Refactor agent graph workflow with consolidated fallback node' or 'Simplify guard routing to single fallback handler'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description is in Spanish and describes the intended workflow changes (guard routing logic), which aligns with the actual changes made in the pull request regarding guard nodes and routing.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/fallback_inicial

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to modify the agent workflow graph routing logic to implement a dual guard check system. The changes aim to route malicious requests to fallback nodes at two different points in the workflow: after the initial guard check and after the generator node. However, the implementation contains critical bugs that prevent the workflow from functioning correctly.

  • Modified the first guard node's conditional routing to send malicious requests to fallback_inicial instead of ending immediately, and valid requests to parafraseo instead of fallback_inicial
  • Removed the direct edge from fallback_inicial to parafraseo (since routing now handles this conditionally)
  • Changed the flow after generator to route back to guard for a second check instead of going to fallback_final
  • Attempted to add a second conditional routing from guard after the generator completes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 81 to 88
workflow.add_conditional_edges(
"fallback_final",
route_after_fallback_final,
"guard",
route_after_guard,
{
"risky": END, # End with error if risky
"continue": END, # End with final_response if valid
# Note: Final LLM is called within fallback_final node
"malicious": "fallback_inicial", # go to fallback_final if malicious
"continue": END, # if there's no error ends
},
)
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

This is a duplicate conditional edge definition for the "guard" node. Lines 58-65 already define conditional edges for "guard" with route_after_guard. In LangGraph, adding a second conditional edge to the same node will overwrite the first one, meaning the routing defined at lines 58-65 will be ignored and only this second definition will be active. This creates a logical error where the workflow cannot reach the parafraseo node at all, since the first guard check is effectively removed.

Copilot uses AI. Check for mistakes.
"risky": END, # End with error if risky
"continue": END, # End with final_response if valid
# Note: Final LLM is called within fallback_final node
"malicious": "fallback_inicial", # go to fallback_final if malicious
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The comment says "go to fallback_final if malicious" but the code routes to "fallback_inicial". Based on the PR description indicating the second guard should route to fallback_final if malicious, this appears to be a copy-paste error. The routing should likely be to "fallback_final" not "fallback_inicial".

Suggested change
"malicious": "fallback_inicial", # go to fallback_final if malicious
"malicious": "fallback_final", # go to fallback_final if malicious

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
RAGManager/app/agents/graph.py (2)

58-65: Duplicate conditional edges from "guard" node will cause the second definition to overwrite this one.

LangGraph's add_conditional_edges on the same source node (lines 58-65 and 81-88) will result in the second call overwriting the first. This means the routing defined here ("malicious": "fallback_inicial", "continue": "parafraseo") will be lost, and only the routing from lines 81-88 will be active.

To implement two distinct guard checks (one before parafraseo and one after generator), you need two separate guard nodes with different names (e.g., guard_inicial and guard_final).


23-32: Docstring is outdated and doesn't reflect the new flow.

The docstring still describes the old routing (e.g., "generator -> fallback_final" on line 31, guard routing to "END" on line 26). Update it to match the intended new workflow to avoid confusion for future maintainers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 298c6fd and 3ac24a4.

📒 Files selected for processing (1)
  • RAGManager/app/agents/graph.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
RAGManager/app/agents/graph.py (1)
RAGManager/app/agents/routing.py (1)
  • route_after_guard (6-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Upload results

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (2)
RAGManager/app/agents/graph.py (2)

24-35: fallback_inicial is a dead-end in the graph; add fallback_inicial -> parafraseo.
guard_inicial can route to fallback_inicial, but there’s no outgoing edge defined here; execution will stop there.

@@
     # parafraseo -> retriever
     workflow.add_edge("parafraseo", "retriever")
+
+    # fallback_inicial -> parafraseo
+    workflow.add_edge("fallback_inicial", "parafraseo")

Also applies to: 60-79


80-91: fallback_final is also a dead-end; add fallback_final -> END (or conditional edges).
guard_final can route to fallback_final, but fallback_final has no outgoing edge here.

@@
     workflow.add_conditional_edges(
         "guard_final",
         route_after_guard_final,
         {
             "risky": "fallback_final",  # go to fallback_final if PII detected
             "continue": END,  # if there's no error ends
         },
     )
+
+    # fallback_final -> END
+    workflow.add_edge("fallback_final", END)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ac24a4 and 2b0244c.

📒 Files selected for processing (5)
  • RAGManager/app/agents/graph.py (4 hunks)
  • RAGManager/app/agents/nodes/__init__.py (1 hunks)
  • RAGManager/app/agents/nodes/guard_final.py (1 hunks)
  • RAGManager/app/agents/nodes/guard_inicial.py (1 hunks)
  • RAGManager/app/agents/routing.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
RAGManager/app/agents/nodes/guard_final.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-46)
RAGManager/app/agents/nodes/guard_inicial.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-46)
RAGManager/app/agents/routing.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-46)
RAGManager/app/agents/nodes/__init__.py (4)
RAGManager/app/agents/nodes/guard_final.py (1)
  • guard_final (25-77)
RAGManager/app/agents/nodes/guard_inicial.py (1)
  • guard_inicial (24-75)
RAGManager/app/agents/nodes/parafraseo.py (1)
  • parafraseo (6-32)
RAGManager/app/agents/nodes/retriever.py (1)
  • retriever (6-32)
🔇 Additional comments (2)
RAGManager/app/agents/nodes/__init__.py (1)

8-23: Exports look consistent with the new nodes.

RAGManager/app/agents/graph.py (1)

83-91: Verify terminal output on the “continue -> END” path (likely final_response is unset).
If callers expect final_response (per AgentState), ending right after guard_final may return only generated_response. Ensure the API contract is consistent (e.g., set final_response = generated_response on the safe path or add a small finalize node).

Comment on lines +12 to +22
# Initialize Guard with DetectPII validator
# Note: The validator must be installed via: guardrails hub install hub://guardrails/detect_pii
# Also requires: presidio-analyzer and presidio-anonymizer
# Common PII entities: EMAIL_ADDRESS, PHONE_NUMBER, PERSON, LOCATION, CREDIT_CARD, SSN, etc.
# For full list see: https://microsoft.github.io/presidio/
_guard = Guard().use(
DetectPII(
pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER", "PERSON", "LOCATION", "CREDIT_CARD", "SSN"],
on_fail="noop", # Don't raise exceptions, handle via state flags
)
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check repository structure and find dependency files
find . -type f \( -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" -o -name "Pipfile" \) | head -20

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 139


🏁 Script executed:

# Examine the full guard_final.py file to see the actual code context
fd "guard_final.py" --type f

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 125


🏁 Script executed:

# Search for any logging statements that might expose PII
rg "logger|print|logging" RAGManager/app/agents/nodes/ -A 3 -B 1

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 6963


🏁 Script executed:

# Check RAGManager/pyproject.toml for dependency versions
cat RAGManager/pyproject.toml

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 1735


🏁 Script executed:

# Read the full guard_final.py file to see context
cat -n RAGManager/app/agents/nodes/guard_final.py

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 3525


Fix import-time initialization risks and dependency specification; remove PII exposure in logging.

  1. Guard/DetectPII imports at module level (lines 5-6) will crash app at import if Guardrails or hub validator aren't installed
  2. Presidio packages (presidio-analyzer, presidio-anonymizer) mentioned in comments (line 14) are missing from pyproject.toml dependencies; only "guardrails-ai>=0.5.10" is specified without version pinning
  3. Line 67 logs first 100 characters of generated_response which defeats PII detection by exposing the PII in logs: logger.warning(f"PII detected in generated response: {generated_response[:100]}...")
  4. Exception handler (lines 69-75) fails open by setting is_risky=False on any error, bypassing detection entirely

Defer Guard initialization to function scope, pin Guardrails and add missing Presidio dependencies to pyproject.toml, and remove response snippet from warning log (log detection fact only, not content).

🤖 Prompt for AI Agents
RAGManager/app/agents/nodes/guard_final.py lines ~12-22 (and related locations
5-6, 14, 67, 69-75): the module currently imports and initializes
Guard/DetectPII at import time, logs a snippet of generated_response (exposing
PII), and has an exception handler that fails open; fix by deferring import and
Guard/DetectPII initialization into the function that performs validation so
missing packages won’t crash at import, update pyproject.toml to pin
guardrails-ai to a specific compatible version and add presidio-analyzer and
presidio-anonymizer as dependencies, change the logger.warning to only note that
PII was detected without including any response content, and modify the
exception handling to fail closed (treat errors as risky or re-raise) and log
the exception details (not the response) so detection cannot be bypassed on
errors.

Comment on lines +23 to +37
def route_after_guard_final(state: AgentState) -> str:
"""
Route after Guard Final node validation.

Determines the next step based on whether the generated response was flagged as risky (PII detected).

Args:
state: Current agent state

Returns:
"risky" if PII is detected (is_risky=True), "continue" otherwise
"""
if state.get("is_risky", False):
return "risky"
return "continue"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: dedupe is_risky routing helper.
route_after_guard_final() and route_after_fallback_final() are identical; consider a single helper to avoid drift.

🤖 Prompt for AI Agents
In RAGManager/app/agents/routing.py around lines 23 to 37, the functions
route_after_guard_final and route_after_fallback_final are identical; replace
both with a single shared helper (e.g., route_after_final(state) or
route_after_risk_check(state)) and update all callers to use that helper, then
remove the duplicate function to avoid drift; ensure the new helper preserves
the same behavior (returns "risky" when state.get("is_risky", False) is true,
otherwise "continue") and run tests to verify no call sites were missed.

Copilot AI review requested due to automatic review settings December 14, 2025 00:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0244c and e71df9f.

📒 Files selected for processing (4)
  • RAGManager/app/agents/nodes/guard_final.py (1 hunks)
  • RAGManager/app/agents/nodes/guard_inicial.py (4 hunks)
  • RAGManager/app/core/config.py (2 hunks)
  • RAGManager/pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
RAGManager/app/agents/nodes/guard_inicial.py (2)
RAGManager/app/agents/nodes/guard_final.py (1)
  • _get_guard (21-34)
RAGManager/app/agents/state.py (1)
  • AgentState (6-46)
🪛 Ruff (0.14.8)
RAGManager/app/agents/nodes/guard_final.py

18-18: Remove quotes from type annotation

Remove quotes

(UP037)


21-21: Remove quotes from type annotation

Remove quotes

(UP037)

🔇 Additional comments (8)
RAGManager/app/core/config.py (1)

1-2: LGTM! Well-documented security configuration.

The new guardrails_fail_closed setting is properly structured with:

  • Appropriate default (False for backward compatibility)
  • Clear documentation explaining fail-closed vs fail-open behavior
  • Proper Field constraints and type hints

This gives operators explicit control over the security posture when Guardrails validation fails.

Also applies to: 43-48

RAGManager/app/agents/nodes/guard_inicial.py (3)

15-27: LGTM! Lazy initialization properly implemented.

The singleton pattern with _get_guard() correctly:

  • Defers Guard creation until first use (prevents import-time crashes)
  • Uses global for singleton caching
  • Configures DetectJailbreak with appropriate settings
  • Sets on_fail="noop" for manual validation result checking

This addresses the past review concern about import-time initialization risks.


54-74: LGTM! Validation logic is correct and privacy-safe.

The validation flow properly:

  • Validates using the lazy-loaded guard
  • Checks validation_passed flag correctly
  • Sets appropriate state flags (is_malicious, error_message)
  • Logs only metadata (prompt length) without exposing potentially sensitive content

This addresses the past review concern about logging user prompt snippets.


76-89: LGTM! Exception handling implements configurable fail-safe behavior.

The exception handler correctly:

  • Logs errors with full stack trace for debugging
  • Consults settings.guardrails_fail_closed for behavior control
  • Implements fail-closed (treat as malicious) when configured
  • Defaults to fail-open for backward compatibility
  • Includes clear comments explaining the security trade-offs

This addresses the past review concern about adding configurable fail-safe behavior.

RAGManager/app/agents/nodes/guard_final.py (4)

4-4: LGTM! Lazy initialization with proper TYPE_CHECKING pattern.

The implementation correctly:

  • Uses TYPE_CHECKING to avoid import-time crashes
  • Defers Guard/DetectPII imports to _get_guard() runtime
  • Configures DetectPII with appropriate PII entities
  • Uses on_fail="noop" for manual validation checking

Note on static analysis hints: The quotes on line 18 are necessary because Guard is only imported under TYPE_CHECKING. The quote on line 21 could be removed, but it's a minor style issue and doesn't affect functionality.

Also applies to: 8-9, 18-34


61-82: LGTM! Validation logic is correct and privacy-safe.

The PII detection flow properly:

  • Validates generated response using the lazy-loaded guard
  • Checks validation_passed flag correctly
  • Sets appropriate state flags (is_risky, error_message)
  • Logs only metadata (response length) without exposing detected PII

This addresses the past review concern about never logging detected PII content.


84-89: Consider making fail behavior consistent with guard_inicial.

The exception handler correctly implements fail-closed behavior (sets is_risky=True on errors), addressing the past review concern. However, guard_inicial uses the configurable settings.guardrails_fail_closed setting, while guard_final always fails closed.

For PII detection, fail-closed is the safer default. However, for consistency, consider whether guard_final should also respect the guardrails_fail_closed setting, or if the current hardcoded fail-closed behavior is intentional due to the higher sensitivity of output PII detection.

If consistency is desired, apply this diff:

     except Exception as e:
-        # Fail-closed: If validation fails due to error, treat as risky
-        # This prevents bypassing PII detection through errors
         logger.error("Error during PII detection", exc_info=True)
-        updated_state["is_risky"] = True
-        updated_state["error_message"] = f"PII detection error: {str(e)}"
+        
+        # Check if fail-closed mode is enabled
+        if settings.guardrails_fail_closed:
+            # Fail-closed: treat errors as risky to prevent bypassing detection
+            updated_state["is_risky"] = True
+            updated_state["error_message"] = f"PII detection error: {str(e)}"
+        else:
+            # Fail-open: allow responses when Guardrails fails (backward compatibility)
+            updated_state["is_risky"] = False
+            updated_state["error_message"] = None

Note: Given the sensitivity of PII in outputs, you may want to keep the current fail-closed behavior regardless of the setting. Document this decision if that's the case.


55-59: LGTM! Empty response handling is correct.

Treating empty responses as safe is appropriate since they cannot contain PII, and this is consistent with how guard_inicial handles empty prompts.

Copilot AI review requested due to automatic review settings December 14, 2025 02:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Collaborator

@Germanadjemian Germanadjemian left a comment

Choose a reason for hiding this comment

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

Request changes in Graph.py:
This is a duplicate conditional edge definition for the "guard" node. Lines 58-65 already define conditional edges for "guard" with route_after_guard. In LangGraph, adding a second conditional edge to the same node will overwrite the first one, meaning the routing defined at lines 58-65 will be ignored and only this second definition will be active. This creates a logical error where the workflow cannot reach the parafraseo node at all, since the first guard check is effectively removed.

Germanadjemian
Germanadjemian previously approved these changes Dec 14, 2025
@Germanadjemian Germanadjemian dismissed their stale review December 14, 2025 03:30

wrong review

Copy link
Copy Markdown
Collaborator

@Germanadjemian Germanadjemian left a comment

Choose a reason for hiding this comment

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

hacer cambios marcados en guar_final.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (6)
RAGManager/app/agents/nodes/agent_host.py (2)

4-9: Remove unused imports.

UUID, uuid4, settings, SessionLocal, ChatMessage, and ChatSession are no longer used after the DB-backed logic was removed.

Apply this diff:

 """Nodo 1: Agent Host - Entry point that saves initial context."""
 
 import logging
-from uuid import UUID, uuid4
 
 from app.agents.state import AgentState
-from app.core.config import settings
-from app.core.database_connection import SessionLocal
-from app.models.chat import ChatMessage, ChatSession
 
 logger = logging.getLogger(__name__)

15-30: Update the stale docstring.

The docstring describes PostgreSQL session creation, message persistence, and retrieval that no longer happen. Update it to reflect the current behavior (extracting initial context from the last message).

RAGManager/app/agents/nodes/parafraseo.py (1)

25-29: Remove the stale TODO comment.

The paraphrasing logic has been implemented below, so this TODO block is no longer applicable.

-    # TODO: Implement paraphrasing logic
-    # This should:
-    # 1. Use an LLM or paraphrasing model to rephrase the text
-    # 2. Improve clarity, adjust tone, or format as needed
-    # 3. Set paraphrased_text with the result
-
     # Paraphrase the last message using history
RAGManager/app/agents/nodes/context_builder.py (1)

34-36: Remove unused updated_state and paraphrased variables.

updated_state = state.copy() and paraphrased = state.get("paraphrased_text", "") are extracted but never used in the new implementation. This is dead code.

-    updated_state = state.copy()
-    paraphrased = state.get("paraphrased_text", "")
     chunks = state.get("relevant_chunks", [])
RAGManager/app/agents/state.py (1)

7-12: Update docstring to reflect the actual base class.

The docstring mentions "TypedDict" but the class now inherits from MessagesState. Update for accuracy:

     """
     State schema for the agent graph.

-    This TypedDict defines all the state variables that flow through
+    This class extends MessagesState and defines all the state variables that flow through
     the LangGraph nodes during agent execution.
     """
RAGManager/app/agents/graph.py (1)

53-60: Critical: Duplicate conditional edges on "guard" node will break routing.

LangGraph's add_conditional_edges called twice on the same node ("guard") will cause the second definition (lines 72-79) to overwrite the first (lines 53-60). This means:

  • The first guard check will use {malicious: fallback, continue: END} instead of {malicious: fallback, continue: parafraseo}
  • The parafraseo node will never be reached, breaking the entire workflow

You need separate guard nodes for initial and final validation. Based on guard_inicial.py in this PR, rename the guards accordingly:

 from app.agents.nodes import (
     agent_host,
     context_builder,
     fallback,
-    guard,
+    guard_inicial,
+    guard_final,  # You'll need to create/import this
     parafraseo,
     retriever,
 )
+from app.agents.routing import route_after_guard

 ...

-    workflow.add_node("guard", guard)
+    workflow.add_node("guard_inicial", guard_inicial)
+    workflow.add_node("guard_final", guard_final)

 ...

-    workflow.add_edge("agent_host", "guard")
+    workflow.add_edge("agent_host", "guard_inicial")

     workflow.add_conditional_edges(
-        "guard",
+        "guard_inicial",
         route_after_guard,
         {
             "malicious": "fallback",
             "continue": "parafraseo",
         },
     )

 ...

-    workflow.add_edge("context_builder", "guard")
+    workflow.add_edge("context_builder", "guard_final")

     workflow.add_conditional_edges(
-        "guard",
+        "guard_final",
         route_after_guard,  # Or a different routing function for final guard
         {
             "malicious": "fallback",
             "continue": END,
         },
     )

Also applies to: 71-79

♻️ Duplicate comments (1)
RAGManager/app/agents/graph.py (1)

20-29: Docstring describes non-existent nodes and incorrect flow.

The docstring references generator (Nodo 7) which is no longer in the graph, and the described flow doesn't match the actual implementation:

     """
     Create and configure the LangGraph agent graph.

     The graph implements the following flow:
     1. START -> agent_host (Nodo 1)
-    2. agent_host -> guard (Nodo 2)
-    3. guard -> [conditional] -> fallback (Nodo 3) or END
-    4. fallback -> parafraseo (Nodo 4)
+    2. agent_host -> guard_inicial (Nodo 2)
+    3. guard_inicial -> [conditional] -> fallback or parafraseo
+    4. parafraseo (Nodo 4)
     5. parafraseo -> retriever (Nodo 5)
     6. retriever -> context_builder (Nodo 6)
-    7. context_builder -> generator (Nodo 7)
-    8. generator -> fallback (Nodo 8)
-    9. fallback -> [conditional] -> END (with final_response) or END (with error)
+    7. context_builder -> guard_final
+    8. guard_final -> [conditional] -> fallback or END
+    9. fallback -> END

     Returns:
         Configured StateGraph instance ready for execution
     """
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a0d92b and bfe5417.

📒 Files selected for processing (12)
  • RAGManager/app/agents/graph.py (4 hunks)
  • RAGManager/app/agents/nodes/__init__.py (1 hunks)
  • RAGManager/app/agents/nodes/agent_host.py (1 hunks)
  • RAGManager/app/agents/nodes/context_builder.py (2 hunks)
  • RAGManager/app/agents/nodes/fallback.py (1 hunks)
  • RAGManager/app/agents/nodes/fallback_final.py (0 hunks)
  • RAGManager/app/agents/nodes/fallback_inicial.py (0 hunks)
  • RAGManager/app/agents/nodes/generator.py (0 hunks)
  • RAGManager/app/agents/nodes/guard_inicial.py (5 hunks)
  • RAGManager/app/agents/nodes/parafraseo.py (2 hunks)
  • RAGManager/app/agents/routing.py (1 hunks)
  • RAGManager/app/agents/state.py (2 hunks)
💤 Files with no reviewable changes (3)
  • RAGManager/app/agents/nodes/fallback_inicial.py
  • RAGManager/app/agents/nodes/fallback_final.py
  • RAGManager/app/agents/nodes/generator.py
🧰 Additional context used
🧬 Code graph analysis (3)
RAGManager/app/agents/nodes/guard_inicial.py (2)
RAGManager/app/agents/nodes/guard_final.py (1)
  • _get_guard (21-34)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
RAGManager/app/agents/graph.py (3)
RAGManager/app/agents/nodes/fallback.py (1)
  • fallback (16-41)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
RAGManager/app/agents/routing.py (1)
  • route_after_guard (6-20)
RAGManager/app/agents/nodes/fallback.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
🔇 Additional comments (6)
RAGManager/app/agents/routing.py (1)

6-20: LGTM!

The routing function correctly checks the is_malicious flag with a safe default. The removal of route_after_fallback_final addresses the previous duplicate code concern.

RAGManager/app/agents/nodes/__init__.py (1)

5-17: LGTM!

The package exports are correctly updated to reflect the consolidated fallback node, replacing the previous fallback_inicial, generator, and fallback_final exports.

RAGManager/app/agents/nodes/guard_inicial.py (2)

15-27: Lazy initialization pattern correctly implemented.

The _get_guard() function properly implements lazy initialization, addressing the previous review comment about preventing startup failures.


72-75: Logging and fail-closed behavior correctly implemented.

The logging now avoids PII leakage by only logging prompt length, and the configurable fail-closed/fail-open behavior via settings.guardrails_fail_closed addresses previous review concerns.

Also applies to: 77-89

RAGManager/app/agents/nodes/context_builder.py (1)

7-7: > Likely an incorrect or invalid review comment.

RAGManager/app/agents/state.py (1)

22-22: Remove this suggestion; the proposed syntax will not work with TypedDict.

The is_malicious and is_risky fields are safely initialized before use due to the execution order of the graph—the guard nodes always set these flags regardless of code path. Adding default values with = False syntax is not supported by TypedDict (which is what MessagesState is) and would not provide runtime defaults. The existing defensive .get("is_malicious", False) in the routing function provides extra safety if needed.

Likely an incorrect or invalid review comment.

Comment on lines +11 to +13
llm = ChatOpenAI(
model="gpt-5-nano",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Invalid model name gpt-5-nano.

Same issue as in parafraseo.py - this model doesn't exist and will cause a runtime error.

 llm = ChatOpenAI(
-    model="gpt-5-nano",
+    model="gpt-4o-mini",
 )
📝 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
llm = ChatOpenAI(
model="gpt-5-nano",
)
llm = ChatOpenAI(
model="gpt-4o-mini",
)
🤖 Prompt for AI Agents
RAGManager/app/agents/nodes/fallback.py around lines 11-13: the ChatOpenAI
instantiation uses an invalid model name "gpt-5-nano" which will raise at
runtime; change it to a valid model (e.g., "gpt-4o-mini" or your project’s
configured default/ENV model variable) and make it consistent with parafraseo.py
(use the same valid model or centralize the model name into a config/env var and
reference that here).

Comment on lines +27 to +28
Returns:
error_message
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Docstring and return value inconsistency.

The docstring states it returns error_message, but the function returns {"messages": [error_message]}. Additionally, the AgentState schema has an error_message field that isn't being set. Consider updating the return to set error_message for downstream nodes that may rely on it.

     error_message = llm.invoke(messages)
-    return {"messages": [error_message]}
+    return {"messages": [error_message], "error_message": error_message.content}

Also applies to: 41-41

🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/fallback.py around lines 27-28 (and also at line
41), the docstring claims the function returns error_message but the function
actually returns {"messages": [error_message]} and doesn't set
AgentState.error_message; update the function to (1) set the
AgentState.error_message field to the error string before returning, (2) return
a structure that matches the docstring (or update the docstring to reflect
returning both error_message and messages), and (3) ensure downstream callers
expect AgentState.error_message — i.e., assign state.error_message =
error_message and return either {"error_message": error_message, "messages":
[error_message]} or change the docstring to match the current return shape
consistently at both locations.

Comment on lines +35 to +40
messages = [
SystemMessage(
content="Your job is to generate an error message in user's language for the user explaining the database doesn't have the information to respond what the user asked"
)
] + state["messages"]
error_message = llm.invoke(messages)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

System prompt is misleading for malicious input handling.

The prompt instructs the LLM to say "the database doesn't have the information," but this node handles malicious prompts. This misleads users about why their request failed. Consider a prompt that politely declines without revealing detection logic.

     messages = [
         SystemMessage(
-            content="Your job is to generate an error message in user's language for the user explaining the database doesn't have the information to respond what the user asked"
+            content="Your job is to generate a polite error message in the user's language explaining that you cannot process this request. Do not reveal that it was flagged as malicious."
         )
     ] + state["messages"]
🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/fallback.py around lines 35-40, the SystemMessage
currently instructs the LLM to claim "the database doesn't have the information"
which is misleading for malicious or disallowed inputs; change the system prompt
to instead instruct the model to politely decline disallowed requests without
revealing detection logic (e.g., apologize, state it cannot assist with that
request, offer safe alternatives or resources), ensure the refusal is phrased in
the user's language, and keep the rest of the message flow intact so the node
returns a polite, non-revealing decline for malicious prompts.

Comment on lines +46 to +47
last_message = state["messages"][-1]
prompt = last_message.content if last_message else ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against empty messages list to prevent IndexError.

If state["messages"] is empty, state["messages"][-1] will raise an IndexError. Add a defensive check:

-    last_message = state["messages"][-1]
-    prompt = last_message.content if last_message else ""
+    messages = state.get("messages", [])
+    if not messages:
+        updated_state["is_malicious"] = False
+        updated_state["error_message"] = None
+        return updated_state
+    last_message = messages[-1]
+    prompt = getattr(last_message, "content", "") or ""

Using getattr also guards against messages that might not have a .content attribute.

🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/guard_inicial.py around lines 46-47, the code
directly indexes state["messages"][-1] which will raise IndexError if the list
is empty; change it to first check that state.get("messages") is a non-empty
list and then read the last item safely, e.g., verify messages and use
getattr(last_message, "content", "") to extract content, assigning an empty
string when messages is empty or the message lacks a content attribute.

from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-5-nano")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Invalid model name gpt-5-nano.

gpt-5-nano is not a valid OpenAI model. This will cause a runtime error when invoking the LLM. Use a valid model such as gpt-4o-mini or gpt-3.5-turbo.

-llm = ChatOpenAI(model="gpt-5-nano")
+llm = ChatOpenAI(model="gpt-4o-mini")
📝 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
llm = ChatOpenAI(model="gpt-5-nano")
llm = ChatOpenAI(model="gpt-4o-mini")
🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/parafraseo.py around line 7, the ChatOpenAI
instantiation uses an invalid model name `gpt-5-nano`; replace it with a
supported model (for example `gpt-4o-mini` or `gpt-3.5-turbo`) by updating the
model parameter to a valid OpenAI model string, and ensure any other code or
config that depends on model-specific options is adjusted accordingly.

Comment on lines +39 to +41
response = llm.invoke(messages)
updated_state = state.copy() # Create a copy of the state to update
updated_state["paraphrased_text"] = response.content
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add error handling for LLM invocation.

If llm.invoke() fails (e.g., network error, rate limit), the exception will propagate unhandled. Consider wrapping with try/except and setting an appropriate error state.

🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/parafraseo.py around lines 39-41, the call to
llm.invoke(messages) is unprotected and will raise uncaught exceptions; wrap the
invocation in a try/except block that catches broad LLM/network errors, log the
exception, and update a copied state with error details (e.g.,
updated_state["error"]="paraphrase_failed" and
updated_state["error_message"]=str(exc)) before returning or re-raising as
appropriate so the caller can handle failure gracefully.

Copy link
Copy Markdown

Copilot AI commented Dec 14, 2025

@NicoWagner2005 I've opened a new pull request, #28, to work on those changes. Once the pull request is ready, I'll request review from you.

@NicoWagner2005
Copy link
Copy Markdown
Collaborator Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown

Copilot AI commented Dec 14, 2025

@NicoWagner2005 I've opened a new pull request, #29, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
RAGManager/app/agents/nodes/context_builder.py (2)

34-36: Remove dead code and complete the implementation.

  • updated_state is created but never used.
  • paraphrased is extracted but never utilized, despite the TODO comments and docstring indicating that the enriched query should combine paraphrased_text with relevant_chunks.

Either complete the implementation to use paraphrased in the context, or remove the dead code.

-    updated_state = state.copy()
-    paraphrased = state.get("paraphrased_text", "")
     chunks = state.get("relevant_chunks", [])

Or incorporate paraphrased into the prompt:

     system_content = f"""You are a helpful assistant. Use the following context to answer the user's question.
 If the answer is not in the context, say you don't know.

+User's query: {paraphrased}
+
 Context:
 {context_section}"""

11-24: Docstring does not match implementation.

The docstring states the function returns "Updated state with enriched_query and primary_response set," but the function actually returns {"messages": [response]}. Update the docstring to reflect the actual behavior.

     """
     Context Builder node - Enriches query with retrieved context.

     This node:
     1. Takes paraphrased text and relevant chunks
     2. Builds an enriched query combining both
     3. Calls Primary LLM with the enriched query
     4. Gets response from Primary LLM

     Args:
         state: Agent state containing paraphrased_text and relevant_chunks

     Returns:
-        Updated state with enriched_query and primary_response set
+        Dict with 'messages' key containing the LLM response
     """
♻️ Duplicate comments (1)
RAGManager/app/agents/nodes/context_builder.py (1)

4-7: Move LLM initialization to lazy loading to prevent startup failures.

Instantiating ChatOpenAI at module scope can crash at import time if the API key is missing or the model is unavailable. Use lazy initialization as recommended in the previous review.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfe5417 and 27cc2cf.

📒 Files selected for processing (1)
  • RAGManager/app/agents/nodes/context_builder.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
RAGManager/app/agents/nodes/context_builder.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent

Context:
{context_section}"""

messages = [SystemMessage(content=system_content)] + state["messages"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use defensive access for state["messages"] to avoid KeyError.

Direct dictionary access will raise KeyError if messages is not present in the state. Use .get() with a default empty list for consistency with other state access patterns in this function.

-    messages = [SystemMessage(content=system_content)] + state["messages"]
+    messages = [SystemMessage(content=system_content)] + state.get("messages", [])
📝 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
messages = [SystemMessage(content=system_content)] + state["messages"]
messages = [SystemMessage(content=system_content)] + state.get("messages", [])
🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/context_builder.py around line 47, the code
directly accesses state["messages"] which can raise KeyError if messages is
missing; change to use state.get("messages", []) so it falls back to an empty
list (preserving existing behavior and matching other defensive access patterns
in this function), and ensure the resulting messages variable is a list before
concatenation.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Updated state with is_risky and error_message set
"""
updated_state = state.copy()
generated_response = state.get("generated_response", "")
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The guard_final node is trying to access "generated_response" from state, but the context_builder node now returns only a messages dict without setting "generated_response". The workflow needs to extract the response content from the last message in state["messages"] instead. Update this to use the last message's content: generated_response = state["messages"][-1].content if state.get("messages") else ""

Suggested change
generated_response = state.get("generated_response", "")
generated_response = state["messages"][-1].content if state.get("messages") else ""

Copilot uses AI. Check for mistakes.
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to respond what the user asked"
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Grammatical error in the error message. "what the user asked" should be "what the user asked for" or "the user's question".

Suggested change
"explaining the database doesn't have the information to respond what the user asked"
"explaining the database doesn't have the information to answer the user's question"

Copilot uses AI. Check for mistakes.
"typing-extensions>=4.15.0",
"uvicorn>=0.38.0",
"guardrails-ai>=0.5.10",
"guardrails-ai>=0.6.2",
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The dependency "langchain-openai" is missing from the dependencies list. This package is required for ChatOpenAI usage in multiple nodes (parafraseo.py, context_builder.py, fallback.py). Add "langchain-openai>=0.1.0" to the dependencies list.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
RAGManager/app/agents/nodes/fallback.py (3)

1-1: Update docstring to reflect unified fallback purpose.

The module docstring still refers only to "Fallback Inicial" but this node now serves as a general fallback handling both initial malicious prompts (from guard_inicial) and risky responses (from guard_final).

Apply this diff:

-"""Nodo 3: Fallback Inicial - Initial fallback processing."""
+"""Nodo 3: Fallback - Unified fallback processing for malicious prompts and risky responses."""

11-13: Invalid model name will cause runtime failure.

The model "gpt-5-nano" does not exist in OpenAI's model catalog and will raise an error when invoked.

Apply this diff:

 llm = ChatOpenAI(
-    model="gpt-5-nano",
+    model="gpt-4o-mini",
 )

28-29: Docstring and return value mismatch.

The docstring claims the function returns error_message, but it actually returns {"messages": [error_message]}. Additionally, the AgentState schema includes an error_message field that should be populated but isn't.

Update the return statement to also set the error_message field:

     error_message = llm.invoke(messages)
-    return {"messages": [error_message]}
+    return {
+        "messages": [error_message],
+        "error_message": error_message.content
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 871062a and 8fd90a8.

📒 Files selected for processing (1)
  • RAGManager/app/agents/nodes/fallback.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
RAGManager/app/agents/nodes/fallback.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)

Comment on lines +53 to +60
else:
logger.info(
"Fallback triggered: Generic fallback (no risky/malicious flag)"
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to respond what the user asked"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Generic fallback uses misleading error message.

The generic fallback branch (when neither is_risky nor is_malicious is set) uses the same misleading message as the malicious case, claiming "the database doesn't have the information." For a true generic fallback scenario, this explanation may be confusing or inaccurate.

Consider a more neutral message for unexpected fallback scenarios:

     else:
         logger.info(
             "Fallback triggered: Generic fallback (no risky/malicious flag)"
         )
         system_message_content = (
-            "Your job is to generate an error message in user's language for the user "
-            "explaining the database doesn't have the information to respond what the user asked"
+            "Your job is to generate a polite error message in the user's language "
+            "explaining that the request cannot be completed at this time and offering to help with something else"
         )
🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/fallback.py around lines 53 to 60, the generic
fallback branch logs and sets a system_message_content that incorrectly states
"the database doesn't have the information," which is misleading for
non-malicious/unflagged fallbacks; change the generic fallback to use a neutral,
user-facing message (e.g., apologize and state we cannot generate a confident
response or ask for clarification) and update the logger message accordingly so
it reflects an unexpected/unhandled fallback rather than implying missing
database content.

Comment on lines +62 to +67
messages = [
SystemMessage(content=system_message_content)
] + state["messages"]

error_message = llm.invoke(messages)
return {"messages": [error_message]}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add error handling for LLM invocation in fallback node.

The llm.invoke() call has no error handling. Since this is the fallback node (a last-resort error handler), if the LLM call fails due to network issues, API errors, or the invalid model name, the entire fallback will crash. Consider wrapping the invocation in try/except and returning a hardcoded fallback message on failure.

     messages = [
         SystemMessage(content=system_message_content)
     ] + state["messages"]
     
-    error_message = llm.invoke(messages)
+    try:
+        error_message = llm.invoke(messages)
+    except Exception as e:
+        logger.error(f"LLM invocation failed in fallback node: {e}")
+        from langchain_core.messages import AIMessage
+        error_message = AIMessage(
+            content="I'm sorry, but I cannot process your request at this time. Please try again later."
+        )
+    
     return {"messages": [error_message]}
📝 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
messages = [
SystemMessage(content=system_message_content)
] + state["messages"]
error_message = llm.invoke(messages)
return {"messages": [error_message]}
messages = [
SystemMessage(content=system_message_content)
] + state["messages"]
try:
error_message = llm.invoke(messages)
except Exception as e:
logger.error(f"LLM invocation failed in fallback node: {e}")
from langchain_core.messages import AIMessage
error_message = AIMessage(
content="I'm sorry, but I cannot process your request at this time. Please try again later."
)
return {"messages": [error_message]}
🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/fallback.py around lines 62-67, the
llm.invoke(messages) call is unprotected and can raise (network/API/model)
errors causing the fallback node to crash; wrap the llm.invoke call in a
try/except that catches Exception, log the exception (or attach a minimal safe
error string), and return a deterministic fallback response (e.g. a single
assistant/SystemMessage with a hardcoded apology/temporary-fallback text) in the
same {"messages": [...]} shape so the pipeline continues even if the LLM call
fails.

@NicoWagner2005
Copy link
Copy Markdown
Collaborator Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown

Copilot AI commented Dec 14, 2025

@NicoWagner2005 I've opened a new pull request, #30, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +23 to +29
2. Generates an error_message from llm to show the user

Args:
state: Agent state containing the prompt or initial context

Returns:
error_message
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The docstring states the function returns "error_message", but the actual return statement returns a dictionary with a "messages" key containing the LLM response. The docstring should accurately describe the return value format to match the implementation.

Suggested change
2. Generates an error_message from llm to show the user
Args:
state: Agent state containing the prompt or initial context
Returns:
error_message
2. Generates an error message from llm to show the user
Args:
state: Agent state containing the prompt or initial context
Returns:
dict: A dictionary with a "messages" key containing a list with the generated error message from the LLM.

Copilot uses AI. Check for mistakes.
)


# TO DO: implementar clase nodo fallback y inicializar el llm en el init
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Comment contains "TO DO" which should be written as "TODO" (one word, no space) for consistency with standard TODO comment conventions.

Suggested change
# TO DO: implementar clase nodo fallback y inicializar el llm en el init
# TODO: implementar clase nodo fallback y inicializar el llm en el init

Copilot uses AI. Check for mistakes.
finally:
if db is not None:
db.close()
initial_message = state["messages"][-1]
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The code accesses state["messages"][-1] without first checking if the messages list is empty. If state["messages"] is an empty list, this will raise an IndexError. Add a check to ensure the messages list is not empty before accessing the last element.

Suggested change
initial_message = state["messages"][-1]
initial_message = state["messages"][-1] if state["messages"] else None

Copilot uses AI. Check for mistakes.

def context_builder(state: AgentState) -> AgentState:

def context_builder(state: AgentState) -> dict:
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The function signature declares return type as "dict" but the docstring still states "Returns: Updated state with enriched_query and primary_response set". The docstring should be updated to reflect that the function now returns a dictionary with a "messages" key containing the LLM response, not the fields mentioned in the old documentation.

Copilot uses AI. Check for mistakes.
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-5-nano")
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The LLM is initialized at module level, which means it will be created immediately when the module is imported, even if the parafraseo function is never called. This could waste resources during startup or testing. Consider using lazy initialization with a function like _get_llm() similar to the pattern used in guard_inicial.py and guard_final.py.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +13
llm = ChatOpenAI(
model="gpt-5-nano",
)
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The LLM is initialized at module level, which means it will be created immediately when the module is imported, even if the fallback function is never called. This could waste resources during startup or testing. Consider using lazy initialization with a function like _get_llm() similar to the pattern used in guard_inicial.py and guard_final.py.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +68
llm = ChatOpenAI(
model="gpt-5-nano",
)


# TO DO: implementar clase nodo fallback y inicializar el llm en el init
def fallback(state: AgentState) -> AgentState:
"""
Fallback node - Performs fallback processing.

This node:
1. Alerts about malicious prompt or PII detection
2. Generates an error_message from llm to show the user

Args:
state: Agent state containing the prompt or initial context

Returns:
error_message
"""

# Check for PII/Risky content (from guard_final)
if state.get("is_risky"):
logger.warning(
"Defensive check triggered: PII/Risky content detected in response"
)
system_message_content = (
"Your job is to generate an error message in user's language explaining "
"that the response cannot be provided because it contains sensitive or private information."
)

# Check for Malicious prompt (from guard_inicial)
elif state.get("is_malicious"):
logger.warning(
"Defensive check triggered: Malicious prompt detected"
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to answer the user's question"
)

# Generic Fallback (neither risky nor malicious)
else:
logger.info(
"Fallback triggered: Generic fallback (no risky/malicious flag)"
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to answer the user's question"
)

messages = [
SystemMessage(content=system_message_content)
] + state["messages"]

error_message = llm.invoke(messages)
return {"messages": [error_message]}

Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The TODO comment suggests implementing a class for the fallback node and initializing the LLM in init. This is a valid concern since the current module-level LLM initialization is inconsistent with the lazy initialization pattern used in guard_inicial.py and guard_final.py. Consider addressing this TODO before merging, as it impacts the maintainability and consistency of the codebase.

Suggested change
llm = ChatOpenAI(
model="gpt-5-nano",
)
# TO DO: implementar clase nodo fallback y inicializar el llm en el init
def fallback(state: AgentState) -> AgentState:
"""
Fallback node - Performs fallback processing.
This node:
1. Alerts about malicious prompt or PII detection
2. Generates an error_message from llm to show the user
Args:
state: Agent state containing the prompt or initial context
Returns:
error_message
"""
# Check for PII/Risky content (from guard_final)
if state.get("is_risky"):
logger.warning(
"Defensive check triggered: PII/Risky content detected in response"
)
system_message_content = (
"Your job is to generate an error message in user's language explaining "
"that the response cannot be provided because it contains sensitive or private information."
)
# Check for Malicious prompt (from guard_inicial)
elif state.get("is_malicious"):
logger.warning(
"Defensive check triggered: Malicious prompt detected"
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to answer the user's question"
)
# Generic Fallback (neither risky nor malicious)
else:
logger.info(
"Fallback triggered: Generic fallback (no risky/malicious flag)"
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to answer the user's question"
)
messages = [
SystemMessage(content=system_message_content)
] + state["messages"]
error_message = llm.invoke(messages)
return {"messages": [error_message]}
class FallbackNode:
"""
Fallback node - Performs fallback processing.
This node:
1. Alerts about malicious prompt or PII detection
2. Generates an error_message from llm to show the user
"""
def __init__(self):
self.llm = ChatOpenAI(
model="gpt-5-nano",
)
def __call__(self, state: AgentState) -> AgentState:
"""
Args:
state: Agent state containing the prompt or initial context
Returns:
error_message
"""
# Check for PII/Risky content (from guard_final)
if state.get("is_risky"):
logger.warning(
"Defensive check triggered: PII/Risky content detected in response"
)
system_message_content = (
"Your job is to generate an error message in user's language explaining "
"that the response cannot be provided because it contains sensitive or private information."
)
# Check for Malicious prompt (from guard_inicial)
elif state.get("is_malicious"):
logger.warning(
"Defensive check triggered: Malicious prompt detected"
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to answer the user's question"
)
# Generic Fallback (neither risky nor malicious)
else:
logger.info(
"Fallback triggered: Generic fallback (no risky/malicious flag)"
)
system_message_content = (
"Your job is to generate an error message in user's language for the user "
"explaining the database doesn't have the information to answer the user's question"
)
messages = [
SystemMessage(content=system_message_content)
] + state["messages"]
error_message = self.llm.invoke(messages)
return {"messages": [error_message]}

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +47
last_message = state["messages"][-1]
prompt = last_message.content if last_message else ""
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The code accesses state["messages"][-1] without first checking if the messages list is empty. If state["messages"] is an empty list, this will raise an IndexError. Add a check to ensure the messages list is not empty before accessing the last element.

Suggested change
last_message = state["messages"][-1]
prompt = last_message.content if last_message else ""
if state["messages"]:
last_message = state["messages"][-1]
prompt = last_message.content if last_message else ""
else:
last_message = None
prompt = ""

Copilot uses AI. Check for mistakes.
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-5-nano")
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The LLM is initialized at module level, which means it will be created immediately when the module is imported, even if the context_builder function is never called. This could waste resources during startup or testing. Consider using lazy initialization with a function like _get_llm() similar to the pattern used in guard_inicial.py and guard_final.py.

Copilot uses AI. Check for mistakes.
@NicoWagner2005
Copy link
Copy Markdown
Collaborator Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown

Copilot AI commented Dec 14, 2025

@NicoWagner2005 I've opened a new pull request, #31, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI mentioned this pull request Dec 14, 2025
9 tasks
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
RAGManager/app/agents/nodes/agent_host.py (1)

14-26: Outdated docstring describes removed functionality.

The docstring still references PostgreSQL session handling, message persistence, and retrieval (steps 2-5), but the implementation now only extracts initial_context from the last message. Update it to reflect the simplified behavior.

     """
     Agent Host node - Entry point for the agent flow.

     This node:
-    1. Receives the initial prompt and optional chat_session_id
-    2. Creates or retrieves chat session from PostgreSQL
-    3. Saves the user's message to the chat session
-    4. Retrieves all chat messages for the session
-    5. Prepares state for validation
+    1. Receives incoming messages
+    2. Extracts the initial context from the last message

     Args:
-        state: Agent state containing the user prompt and optional chat_session_id
+        state: Agent state containing the messages list

     Returns:
-        Updated state with chat_session_id, chat_messages, and initial_context set
+        Updated state with initial_context set
     """
♻️ Duplicate comments (8)
RAGManager/app/agents/nodes/parafraseo.py (3)

8-16: Good: Lazy initialization pattern implemented.

The lazy initialization pattern addresses the past concern about module-level LLM creation wasting resources during startup or testing. This is an improvement.

However, the invalid model name gpt-5-nano at line 15 remains unaddressed from the previous review.


48-48: Missing error handling for LLM invocation.

The call to _get_llm().invoke(messages) remains unprotected and will raise uncaught exceptions on network errors or rate limits. This issue was flagged in the previous review.


49-52: Return value inconsistency with other workflow nodes.

The function returns the entire updated_state object instead of a dict with just the updated fields. This inconsistency with other nodes (like context_builder) was noted in the previous review, which suggested returning {"paraphrased_text": response.content} for consistency with the graph's expectation of partial state updates.

RAGManager/app/agents/nodes/context_builder.py (1)

54-54: Use defensive access for state["messages"].

Direct dictionary access will raise KeyError if messages is not present. This was flagged in a previous review.

-    messages = [SystemMessage(content=system_content)] + state["messages"]
+    messages = [SystemMessage(content=system_content)] + state.get("messages", [])
RAGManager/app/agents/nodes/fallback.py (3)

19-21: Invalid model name gpt-5-nano.

This was flagged in a previous review. The model gpt-5-nano does not exist and will cause a runtime error.

     _llm = ChatOpenAI(
-        model="gpt-5-nano",
+        model="gpt-4o-mini",
     )

55-58: Misleading error messages for malicious prompt handling.

The system prompts for both is_malicious and generic fallback cases claim "the database doesn't have the information," which is inaccurate. This was flagged in previous reviews. The malicious case should not reveal detection logic, and the generic case should use a neutral message.

Also applies to: 65-68


74-75: Add error handling for LLM invocation.

This was flagged in a previous review. Since this is the fallback node (last-resort error handler), if the LLM call fails, the entire fallback crashes. Wrap in try/except with a hardcoded fallback message.

RAGManager/app/agents/nodes/guard_final.py (1)

52-59: Consider returning only updated fields for MessagesState reducer pattern.

Creating a full copy of the state and modifying it works, but MessagesState supports a reducer pattern where nodes return only the changed fields. This approach is more efficient and aligns with the framework's design.

Refactor to return only the updated fields:

-    updated_state = state.copy()
-    generated_response = state["messages"][-1].content if state.get("messages") and len(state["messages"]) > 0 else ""
+    generated_response = state["messages"][-1].content if state.get("messages") and len(state["messages"]) > 0 else ""
 
     if not generated_response:
         # Empty response is considered safe
-        updated_state["is_risky"] = False
-        updated_state["error_message"] = None
-        return updated_state
+        return {"is_risky": False, "error_message": None}

Apply the same pattern to the rest of the function (lines 69-70, 74-78, 88-89).

Based on learnings, this aligns with MessagesState patterns used elsewhere in the codebase.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fd90a8 and ad5f7e8.

⛔ Files ignored due to path filters (1)
  • RAGManager/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • RAGManager/app/agents/nodes/agent_host.py (1 hunks)
  • RAGManager/app/agents/nodes/context_builder.py (3 hunks)
  • RAGManager/app/agents/nodes/fallback.py (1 hunks)
  • RAGManager/app/agents/nodes/guard_final.py (1 hunks)
  • RAGManager/app/agents/nodes/guard_inicial.py (1 hunks)
  • RAGManager/app/agents/nodes/parafraseo.py (2 hunks)
  • RAGManager/pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
RAGManager/app/agents/nodes/context_builder.py (3)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
RAGManager/app/agents/nodes/fallback.py (1)
  • _get_llm (15-22)
RAGManager/app/agents/nodes/parafraseo.py (1)
  • _get_llm (11-16)
RAGManager/app/agents/nodes/guard_inicial.py (2)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
RAGManager/app/agents/nodes/guard_final.py (1)
  • _get_guard (21-34)
RAGManager/app/agents/nodes/parafraseo.py (3)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
RAGManager/app/agents/nodes/context_builder.py (1)
  • _get_llm (11-16)
RAGManager/app/agents/nodes/fallback.py (1)
  • _get_llm (15-22)
RAGManager/app/agents/nodes/guard_final.py (2)
RAGManager/app/agents/state.py (1)
  • AgentState (6-47)
RAGManager/app/agents/nodes/guard_inicial.py (1)
  • _get_guard (17-27)
🪛 Ruff (0.14.8)
RAGManager/app/agents/nodes/guard_final.py

18-18: Remove quotes from type annotation

Remove quotes

(UP037)


21-21: Remove quotes from type annotation

Remove quotes

(UP037)

🔇 Additional comments (11)
RAGManager/app/agents/nodes/parafraseo.py (2)

3-4: LGTM!

The imports are appropriate for the paraphrase implementation using LangChain.


42-46: LGTM!

The system instruction clearly defines the paraphrasing task, and the message construction correctly prepends the system instruction to the conversation history.

RAGManager/app/agents/nodes/agent_host.py (1)

27-33: LGTM - Empty messages guard is in place.

The ternary check on line 28 properly handles empty message lists, and the initial_message.content if initial_message else "" on line 30 handles the None case. This addresses the previous IndexError concern.

RAGManager/pyproject.toml (2)

12-21: Dependency updates look good.

The additions address previous review feedback:

  • langchain-openai>=0.1.0 enables ChatOpenAI usage
  • guardrails-ai>=0.6.2 updates from the outdated 0.5.10
  • Presidio packages pinned to 2.2.360 for reproducibility

6-6: Remove or clarify the Python version requirement if possible.

requires-python = ">=3.14" is now a valid requirement as Python 3.14 has been released (October 2025) and is stable. However, ensure this minimum version is intentional—if the project can support Python 3.12 or 3.13, consider broadening the requirement to >=3.12 or >=3.13 for wider compatibility.

Likely an incorrect or invalid review comment.

RAGManager/app/agents/nodes/context_builder.py (1)

3-16: Lazy LLM initialization pattern is good.

The _get_llm() pattern correctly defers ChatOpenAI instantiation until first use, preventing import-time failures. This is consistent with other nodes.

RAGManager/app/agents/nodes/guard_inicial.py (4)

17-27: Lazy Guard initialization is well implemented.

The _get_guard() pattern correctly defers Guard instantiation, preventing import-time crashes if the validator isn't installed. Configuration is properly pulled from settings.


46-57: Empty messages handling is correct.

The guard against empty messages list properly prevents IndexError and treats empty prompts as safe. This addresses the previous review feedback.


76-79: Good - logging uses metadata only, no PII leakage.

The warning log uses extra={"prompt_len": len(prompt)} instead of logging prompt content, addressing the previous privacy concern.


81-94: Configurable fail-safe behavior is properly implemented.

The exception handling now respects settings.guardrails_fail_closed to choose between fail-closed (treat errors as malicious) and fail-open (allow through) behavior. This addresses the previous review feedback about making this configurable.

RAGManager/app/agents/nodes/guard_final.py (1)

61-91: LGTM! Previous critical security issues have been addressed.

The validation and exception handling logic is now correct:

PII logging fixed (lines 79-82): Logs only metadata (response_len), not response content
Fail-closed exception handling (lines 84-90): Sets is_risky=True on errors to prevent bypassing PII detection
Proper error propagation: Uses exc_info=True for detailed logging and includes exception details in error_message

The implementation correctly prevents PII exposure while maintaining robust error handling.

Comment on lines +70 to +72
messages = [
SystemMessage(content=system_message_content)
] + state["messages"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use defensive access for state["messages"].

Direct dictionary access state["messages"] will raise KeyError if the key is missing. Use .get() with a default for consistency with other state access patterns.

     messages = [
         SystemMessage(content=system_message_content)
-    ] + state["messages"]
+    ] + state.get("messages", [])
📝 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
messages = [
SystemMessage(content=system_message_content)
] + state["messages"]
messages = [
SystemMessage(content=system_message_content)
] + state.get("messages", [])
🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/fallback.py around lines 70-72, the code accesses
state["messages"] directly which can raise KeyError; replace that direct access
with defensive access using state.get("messages", []) (or wrap with list(...) /
ensure it's a list) so the messages concatenation becomes stable and consistent
with other state access patterns.

# Also requires: presidio-analyzer and presidio-anonymizer
# Common PII entities: EMAIL_ADDRESS, PHONE_NUMBER, PERSON, LOCATION, CREDIT_CARD, SSN, etc.
# For full list see: https://microsoft.github.io/presidio/
_guard: "Guard | None" = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove unnecessary quotes from type annotation.

The quotes around "Guard | None" are unnecessary when using the TYPE_CHECKING pattern. Modern Python (3.10+) with proper conditional imports supports the union syntax without quotes.

Apply this diff:

-_guard: "Guard | None" = None
+_guard: Guard | None = None
🧰 Tools
🪛 Ruff (0.14.8)

18-18: Remove quotes from type annotation

Remove quotes

(UP037)

🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/guard_final.py around line 18, the type
annotation uses unnecessary quotes: replace the quoted annotation "_guard:
\"Guard | None\" = None" with an unquoted annotation (e.g. _guard: Guard | None
= None) and ensure Guard is only imported inside a TYPE_CHECKING block or
otherwise conditionally to avoid runtime import issues.

_guard: "Guard | None" = None


def _get_guard() -> "Guard":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove unnecessary quotes from return type annotation.

Similar to line 18, the quotes around "Guard" in the return type are unnecessary with the TYPE_CHECKING pattern.

Apply this diff:

-def _get_guard() -> "Guard":
+def _get_guard() -> Guard:
🧰 Tools
🪛 Ruff (0.14.8)

21-21: Remove quotes from type annotation

Remove quotes

(UP037)

🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/guard_final.py around line 21, the return type
annotation uses quoted "Guard" which is unnecessary when using the TYPE_CHECKING
import pattern; remove the quotes so the signature reads def _get_guard() ->
Guard: and ensure the Guard symbol is imported under an if TYPE_CHECKING: block
(or otherwise available at runtime) to keep static typing intact.

Comment on lines +28 to +32
_guard = Guard().use(
DetectPII(
pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER", "PERSON", "LOCATION", "CREDIT_CARD", "SSN"],
on_fail="noop", # Don't raise exceptions, handle via state flags
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Pull PII configuration from settings for consistency and flexibility.

The PII entity list and on_fail behavior are hardcoded, unlike guard_inicial which uses settings.guardrails_jailbreak_threshold and settings.guardrails_device. Hardcoding these values creates maintenance burden and configuration inconsistency across guards.

Consider adding settings to the config module (e.g., guardrails_pii_entities, guardrails_pii_on_fail) and importing them here:

from app.core.config import settings

_guard = Guard().use(
    DetectPII(
        pii_entities=settings.guardrails_pii_entities,
        on_fail=settings.guardrails_pii_on_fail,
    )
)

This approach would:

  • Align with the pattern established in guard_inicial.py
  • Allow runtime configuration without code changes
  • Centralize security policy settings
🤖 Prompt for AI Agents
In RAGManager/app/agents/nodes/guard_final.py around lines 28 to 32, the
DetectPII guard is using a hardcoded list of pii_entities and a hardcoded
on_fail value which causes configuration inconsistency with other guards; update
the code to import settings from app.core.config and replace the hardcoded
values with settings.guardrails_pii_entities and settings.guardrails_pii_on_fail
(add those keys to the config if missing), ensuring the guard is created like
Guard().use(DetectPII(pii_entities=settings.guardrails_pii_entities,
on_fail=settings.guardrails_pii_on_fail)) so PII behavior is configurable at
runtime.

Copilot AI review requested due to automatic review settings December 14, 2025 18:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 14 comments.

Comments suppressed due to low confidence (1)

RAGManager/app/agents/nodes/guard_inicial.py:50

  • Variable last_message is not used.
        last_message = None

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.



def route_after_fallback_final(state: AgentState) -> str:
def route_after_guard_final(state: AgentState) -> str:
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The function name 'route_after_guard_final' is inconsistent with 'route_after_guard' (which doesn't use 'inicial' suffix). For consistency with the node naming convention where guard_inicial and guard_final are explicit, consider keeping the naming pattern. However, since 'route_after_guard' routes after 'guard_inicial', the asymmetry is acceptable. Consider adding a comment clarifying that 'route_after_guard' routes after 'guard_inicial' for better clarity.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +90
# Fail-closed: If validation fails due to error, treat as risky
# This prevents bypassing PII detection through errors
logger.error("Error during PII detection", exc_info=True)
updated_state["is_risky"] = True
updated_state["error_message"] = f"PII detection error: {str(e)}"

Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The guard_final node always fails closed on exceptions (treating validation errors as risky), but guard_inicial respects the guardrails_fail_closed configuration setting. This inconsistency could lead to unexpected behavior. Consider using the same fail-closed/fail-open logic from guard_inicial here as well, controlled by the guardrails_fail_closed setting, to provide consistent error handling behavior across both guard nodes.

Suggested change
# Fail-closed: If validation fails due to error, treat as risky
# This prevents bypassing PII detection through errors
logger.error("Error during PII detection", exc_info=True)
updated_state["is_risky"] = True
updated_state["error_message"] = f"PII detection error: {str(e)}"
# Respect guardrails_fail_closed setting for consistency with guard_inicial
fail_closed = state.get("guardrails_fail_closed", True)
logger.error("Error during PII detection", exc_info=True)
if fail_closed:
updated_state["is_risky"] = True
updated_state["error_message"] = f"PII detection error: {str(e)}"
else:
updated_state["is_risky"] = False
updated_state["error_message"] = None

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +15

from app.agents.state import AgentState

_llm: ChatOpenAI | None = None


def _get_llm() -> ChatOpenAI:
"""Lazy initialization of LLM instance."""
global _llm
if _llm is None:
_llm = ChatOpenAI(model="gpt-5-nano")
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The lazy initialization of the LLM instance using a global variable is not thread-safe. In a multi-threaded environment (like FastAPI with uvicorn), multiple threads could simultaneously check '_llm is None' and create multiple ChatOpenAI instances, potentially causing race conditions. Consider using threading.Lock or a thread-safe singleton pattern to ensure only one LLM instance is created.

Suggested change
from app.agents.state import AgentState
_llm: ChatOpenAI | None = None
def _get_llm() -> ChatOpenAI:
"""Lazy initialization of LLM instance."""
global _llm
if _llm is None:
_llm = ChatOpenAI(model="gpt-5-nano")
import threading
from app.agents.state import AgentState
_llm: ChatOpenAI | None = None
_llm_lock = threading.Lock()
def _get_llm() -> ChatOpenAI:
"""Lazy initialization of LLM instance (thread-safe)."""
global _llm
if _llm is None:
with _llm_lock:
if _llm is None:
_llm = ChatOpenAI(model="gpt-5-nano")

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +22
def _get_llm() -> ChatOpenAI:
"""Lazy initialization of LLM instance."""
global _llm
if _llm is None:
_llm = ChatOpenAI(
model="gpt-5-nano",
)
return _llm
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The lazy initialization of the LLM instance using a global variable is not thread-safe. In a multi-threaded environment (like FastAPI with uvicorn), multiple threads could simultaneously check '_llm is None' and create multiple ChatOpenAI instances, potentially causing race conditions. Consider using threading.Lock or a thread-safe singleton pattern to ensure only one LLM instance is created.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +15

from app.agents.state import AgentState

_llm: ChatOpenAI | None = None


def _get_llm() -> ChatOpenAI:
"""Lazy initialization of LLM instance."""
global _llm
if _llm is None:
_llm = ChatOpenAI(model="gpt-4o-mini")
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The lazy initialization of the LLM instance using a global variable is not thread-safe. In a multi-threaded environment (like FastAPI with uvicorn), multiple threads could simultaneously check '_llm is None' and create multiple ChatOpenAI instances, potentially causing race conditions. Consider using threading.Lock or a thread-safe singleton pattern to ensure only one LLM instance is created.

Suggested change
from app.agents.state import AgentState
_llm: ChatOpenAI | None = None
def _get_llm() -> ChatOpenAI:
"""Lazy initialization of LLM instance."""
global _llm
if _llm is None:
_llm = ChatOpenAI(model="gpt-4o-mini")
import threading
from app.agents.state import AgentState
_llm: ChatOpenAI | None = None
_llm_lock = threading.Lock()
def _get_llm() -> ChatOpenAI:
"""Lazy initialization of LLM instance (thread-safe)."""
global _llm
if _llm is None:
with _llm_lock:
if _llm is None:
_llm = ChatOpenAI(model="gpt-4o-mini")

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +27
def _get_guard() -> Guard:
global _guard
if _guard is None:
_guard = Guard().use(
DetectJailbreak(
threshold=settings.guardrails_jailbreak_threshold,
device=settings.guardrails_device,
on_fail="noop",
)
)
return _guard
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The lazy initialization of the Guard instance using a global variable is not thread-safe. In a multi-threaded environment (like FastAPI with uvicorn), multiple threads could simultaneously check '_guard is None' and create multiple Guard instances, potentially causing race conditions. Consider using threading.Lock or a thread-safe singleton pattern to ensure only one Guard instance is created.

Copilot uses AI. Check for mistakes.
"continue": END, # End with final_response if valid
# Note: Final LLM is called within fallback_final node
"risky": "fallback", # go to fallback if risky (PII detected)
"continue": END, # if there's no error ends
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The comment contains a grammatical error. It should read "if there's no error, ends" or better yet "End if no error is detected".

Suggested change
"continue": END, # if there's no error ends
"continue": END, # End if no error is detected

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
"""Guard node - alias to guard_inicial for backward compatibility."""

import logging
from app.agents.nodes.guard_inicial import guard_inicial as guard

from guardrails import Guard
from guardrails.hub import DetectJailbreak

from app.agents.state import AgentState
from app.core.config import settings

logger = logging.getLogger(__name__)

# Initialize Guard with DetectJailbreak validator
# Note: The validator must be installed via: guardrails hub install hub://guardrails/detect_jailbreak
_guard = Guard().use(
DetectJailbreak(
threshold=settings.guardrails_jailbreak_threshold,
device=settings.guardrails_device,
on_fail="noop", # Don't raise exceptions, handle via state flags
)
)


def guard(state: AgentState) -> AgentState:
"""
Guard node - Validates user input for malicious content using Guardrails DetectJailbreak.

This node:
1. Validates the prompt using Guardrails DetectJailbreak validator
2. Sets is_malicious flag if jailbreak attempt is detected
3. Sets error_message if malicious content is detected

Args:
state: Agent state containing the prompt

Returns:
Updated state with is_malicious and error_message set
"""
updated_state = state.copy()
prompt = state.get("prompt", "")

if not prompt:
# Empty prompt is considered safe
updated_state["is_malicious"] = False
updated_state["error_message"] = None
return updated_state

try:
# Validate the prompt using Guardrails
validation_result = _guard.validate(prompt)

# Check if validation passed
# The validator returns ValidationResult with outcome
# If validation fails, outcome will indicate failure
if validation_result.validation_passed:
updated_state["is_malicious"] = False
updated_state["error_message"] = None
logger.debug("Prompt passed jailbreak detection")
else:
# Jailbreak detected
updated_state["is_malicious"] = True
updated_state["error_message"] = (
"Jailbreak attempt detected. Your request contains content that violates security policies."
)
logger.warning(f"Jailbreak attempt detected in prompt: {prompt[:100]}...")

except Exception as e:
# If validation fails due to error, log it but don't block the request
# This is a safety measure - if Guardrails fails, we allow the request
# but log the error for monitoring
logger.error(f"Error during jailbreak detection: {e}")
updated_state["is_malicious"] = False
updated_state["error_message"] = None

return updated_state
__all__ = ["guard"]
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

This backward compatibility wrapper file imports guard_inicial as guard but doesn't expose it properly for the aliasing pattern shown. The all export is correct, but the import statement should explicitly rename it during import: 'from app.agents.nodes.guard_inicial import guard_inicial as guard' makes the aliasing clearer. The current approach creates an intermediate reference that may be confusing.

Copilot uses AI. Check for mistakes.
# 1. Combine paraphrased_text with relevant_chunks into enriched_query
# 2. Format the query appropriately (e.g., with system prompts, context sections)
# 3. Call Primary LLM with the enriched query
# 4. Store the LLM response in primary_response
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The TODO comment references outdated implementation steps that mention "primary_response" which is no longer part of the new workflow. Since the function now directly returns messages from the LLM, this TODO comment should be removed or updated to reflect the actual implementation.

Suggested change
# 4. Store the LLM response in primary_response

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +34
def _get_guard() -> "Guard":
"""Lazy initialization of Guard with DetectPII validator to prevent import-time crashes."""
global _guard
if _guard is None:
from guardrails import Guard
from guardrails.hub import DetectPII

_guard = Guard().use(
DetectPII(
pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER", "PERSON", "LOCATION", "CREDIT_CARD", "SSN"],
on_fail="noop", # Don't raise exceptions, handle via state flags
)
)
return _guard
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The lazy initialization of the Guard instance using a global variable is not thread-safe. In a multi-threaded environment (like FastAPI with uvicorn), multiple threads could simultaneously check '_guard is None' and create multiple Guard instances, potentially causing race conditions. Consider using threading.Lock or a thread-safe singleton pattern to ensure only one Guard instance is created.

Copilot uses AI. Check for mistakes.
@NicoWagner2005 NicoWagner2005 force-pushed the feature/fallback_inicial branch from ec0b13b to e869bed Compare December 14, 2025 19:31
Copilot AI review requested due to automatic review settings December 14, 2025 19:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
RAGManager/app/agents/state.py (1)

3-16: Verify MessagesState import/API and required-field strictness.
Given AgentState(MessagesState), confirm from langgraph.graph import MessagesState is correct for your pinned LangGraph version, and consider making workflow-populated flags optional (or ensure every node initializes them) to avoid “missing key” states at runtime.

In the LangGraph version used by this repo, is `MessagesState` available at `langgraph.graph.MessagesState`, and is subclassing it to add additional TypedDict fields the recommended pattern?
♻️ Duplicate comments (5)
RAGManager/app/agents/nodes/parafraseo.py (1)

4-41: Fix invalid model + avoid module-scope LLM init; add defensive messages access and invoke error handling.

 from app.agents.state import AgentState
 from langchain_core.messages import SystemMessage
 from langchain_openai import ChatOpenAI
+import threading
+import logging
+
+logger = logging.getLogger(__name__)
+
+_llm: ChatOpenAI | None = None
+_llm_lock = threading.Lock()
+
+def _get_llm() -> ChatOpenAI:
+    global _llm
+    if _llm is None:
+        with _llm_lock:
+            if _llm is None:
+                # TODO: prefer settings/env for model name
+                _llm = ChatOpenAI(model="gpt-4o-mini")
+    return _llm
-
-llm = ChatOpenAI(model="gpt-5-nano")
@@
-    messages = [SystemMessage(content=system_instruction)] + state["messages"]
+    messages = [SystemMessage(content=system_instruction)] + state.get("messages", [])
     
-    response = llm.invoke(messages)
-    updated_state = state.copy()  # Create a copy of the state to update
-    updated_state["paraphrased_text"] = response.content
-
-    return updated_state
+    try:
+        response = _get_llm().invoke(messages)
+        return {"paraphrased_text": response.content}
+    except Exception:
+        logger.exception("parafraseo llm.invoke failed")
+        return {"paraphrased_text": None, "error_message": "Paraphrasing failed. Please try again."}
What are the valid OpenAI model names supported by `langchain_openai.ChatOpenAI` in the versions used by this repo (and is `gpt-4o-mini` valid there)?
RAGManager/app/agents/nodes/context_builder.py (1)

4-52: Fix invalid model, return type mismatch, and unsafe state["messages"] access; update docstring to match behavior.

 from app.agents.state import AgentState
 from langchain_core.messages import SystemMessage
 from langchain_openai import ChatOpenAI
+import threading
+import logging
+
+logger = logging.getLogger(__name__)
 
-llm = ChatOpenAI(model="gpt-5-nano")
+_llm: ChatOpenAI | None = None
+_llm_lock = threading.Lock()
+
+def _get_llm() -> ChatOpenAI:
+    global _llm
+    if _llm is None:
+        with _llm_lock:
+            if _llm is None:
+                _llm = ChatOpenAI(model="gpt-4o-mini")
+    return _llm
 
 
-def context_builder(state: AgentState) -> AgentState:
+def context_builder(state: AgentState) -> dict:
@@
-    messages = [SystemMessage(content=system_content)] + state["messages"]
+    messages = [SystemMessage(content=system_content)] + state.get("messages", [])
@@
-    response = llm.invoke(messages)
+    try:
+        response = _get_llm().invoke(messages)
+    except Exception:
+        logger.exception("context_builder llm.invoke failed")
+        return {"messages": []}
 
     return {"messages": [response]}
RAGManager/app/agents/nodes/agent_host.py (1)

28-32: Guard against missing/empty messages before indexing.
state["messages"][-1] can raise KeyError/IndexError. Also consider returning only the changed keys (partial update) for consistency with other nodes.

 def agent_host(state: AgentState) -> AgentState:
@@
-    updated_state = state.copy()
-    initial_message = state["messages"][-1]
-    updated_state["initial_context"] = (
-        initial_message.content if initial_message else ""
-    )
-
-    return updated_state
+    messages = state.get("messages", [])
+    initial_context = messages[-1].content if messages else ""
+    return {"initial_context": initial_context}
RAGManager/app/agents/nodes/fallback.py (1)

11-41: Fallback must not crash: fix invalid model, avoid module-scope init, defensive messages, and handle llm.invoke failures.
Also, the current system prompt is misleading for malicious/risky handling and the docstring/return shape don’t match.

 import logging
+import threading
 
 from app.agents.state import AgentState
-from langchain_core.messages import SystemMessage
+from langchain_core.messages import SystemMessage, AIMessage
 from langchain_openai import ChatOpenAI
 
 logger = logging.getLogger(__name__)
 
-llm = ChatOpenAI(
-    model="gpt-5-nano",
-)
+_llm: ChatOpenAI | None = None
+_llm_lock = threading.Lock()
+
+def _get_llm() -> ChatOpenAI:
+    global _llm
+    if _llm is None:
+        with _llm_lock:
+            if _llm is None:
+                _llm = ChatOpenAI(model="gpt-4o-mini")
+    return _llm
 
-# TO DO: implementar clase nodo fallback y inicializar el llm en el init
+# TODO: implementar clase nodo fallback y inicializar el llm en el init
 def fallback(state: AgentState) -> AgentState:
@@
-    messages = [
-        SystemMessage(
-            content="Your job is to generate an error message in user's language for the user explaining the database doesn't have the information to respond what the user asked"
-        )
-    ] + state["messages"]
-    error_message = llm.invoke(messages)
-    return {"messages": [error_message]}
+    system_content = (
+        "Generate a brief, polite refusal in the user's language. "
+        "Do not mention policy checks or detection mechanisms."
+    )
+    messages = [SystemMessage(content=system_content)] + state.get("messages", [])
+    try:
+        error_msg = _get_llm().invoke(messages)
+    except Exception:
+        logger.exception("fallback llm.invoke failed")
+        error_msg = AIMessage(content="Sorry, I can’t help with that request.")
+    return {"messages": [error_msg], "error_message": getattr(error_msg, "content", None)}
In the versions used by this repo, what exceptions can `langchain_openai.ChatOpenAI.invoke()` raise and what is the recommended retry/timeout pattern?
RAGManager/app/agents/graph.py (1)

20-30: Split the guard node into distinct nodes for initial and final validation, and fix the docstring to match actual graph edges.

The graph has two add_conditional_edges("guard", ...) calls with conflicting routing mappings — the first routing (to parafraseo) becomes unreachable. Rename these to separate nodes (guard_initial and guard_final) so each can have its own conditional routing logic. Also update the docstring: it incorrectly mentions a "generator" node (which doesn't exist) and falsely claims generator -> fallback edges and fallback -> [conditional] routing; in reality, fallback unconditionally edges to END.

@@
-    workflow.add_node("guard", guard)
+    workflow.add_node("guard_initial", guard)
+    workflow.add_node("guard_final", guard)
@@
-    workflow.add_edge("agent_host", "guard")
+    workflow.add_edge("agent_host", "guard_initial")
@@
     workflow.add_conditional_edges(
-        "guard",
+        "guard_initial",
         route_after_guard,
         {
             "malicious": "fallback",
             "continue": "parafraseo",
         },
     )
@@
-    workflow.add_edge("context_builder", "guard")
+    workflow.add_edge("context_builder", "guard_final")
@@
     workflow.add_conditional_edges(
-        "guard",
+        "guard_final",
         route_after_guard,
         {
             "malicious": "fallback",
             "continue": END,
         },
     )

Update the docstring (lines 20–29) to reflect the actual graph: remove references to the non-existent generator node and correct fallback's unconditional edge to END.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad5f7e8 and e869bed.

📒 Files selected for processing (12)
  • RAGManager/app/agents/graph.py (4 hunks)
  • RAGManager/app/agents/nodes/__init__.py (1 hunks)
  • RAGManager/app/agents/nodes/agent_host.py (1 hunks)
  • RAGManager/app/agents/nodes/context_builder.py (2 hunks)
  • RAGManager/app/agents/nodes/fallback.py (1 hunks)
  • RAGManager/app/agents/nodes/fallback_final.py (0 hunks)
  • RAGManager/app/agents/nodes/fallback_inicial.py (0 hunks)
  • RAGManager/app/agents/nodes/generator.py (0 hunks)
  • RAGManager/app/agents/nodes/guard.py (1 hunks)
  • RAGManager/app/agents/nodes/parafraseo.py (2 hunks)
  • RAGManager/app/agents/routing.py (0 hunks)
  • RAGManager/app/agents/state.py (2 hunks)
💤 Files with no reviewable changes (4)
  • RAGManager/app/agents/nodes/generator.py
  • RAGManager/app/agents/routing.py
  • RAGManager/app/agents/nodes/fallback_inicial.py
  • RAGManager/app/agents/nodes/fallback_final.py
🧰 Additional context used
🧬 Code graph analysis (3)
RAGManager/app/agents/nodes/fallback.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-46)
RAGManager/app/agents/graph.py (3)
RAGManager/app/agents/nodes/fallback.py (1)
  • fallback (16-41)
RAGManager/app/agents/nodes/guard.py (1)
  • guard (24-76)
RAGManager/app/agents/routing.py (1)
  • route_after_guard (6-20)
RAGManager/app/agents/nodes/__init__.py (4)
RAGManager/app/agents/nodes/fallback.py (1)
  • fallback (16-41)
RAGManager/app/agents/nodes/guard.py (1)
  • guard (24-76)
RAGManager/app/agents/nodes/parafraseo.py (1)
  • parafraseo (10-43)
RAGManager/app/agents/nodes/retriever.py (1)
  • retriever (6-32)
🔇 Additional comments (1)
RAGManager/app/agents/nodes/__init__.py (1)

5-17: Exports look consistent with the new single fallback node.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"""
updated_state = state.copy()
prompt = state.get("prompt", "")
last_message = state["messages"][-1]
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The code accesses state["messages"][-1] without checking if the messages list exists or is non-empty, which could raise an IndexError if the state is not properly initialized. While the subsequent check handles empty prompt strings, it doesn't protect against a missing or empty messages list. Add a defensive check for the messages list before accessing it.

Suggested change
last_message = state["messages"][-1]
messages = state.get("messages")
last_message = messages[-1] if isinstance(messages, list) and messages else None

Copilot uses AI. Check for mistakes.
Comment on lines 31 to +47
paraphrased = state.get("paraphrased_text", "")
chunks = state.get("relevant_chunks", [])

# Build enriched query
context_section = "\n\n".join(chunks) if chunks else ""
enriched_query = f"{paraphrased}\n\nContext:\n{context_section}" if context_section else paraphrased
updated_state["enriched_query"] = enriched_query
# Build enriched query with context
context_section = "\n\n".join(chunks) if chunks else "No relevant context found."

system_content = f"""You are a helpful assistant. Use the following context to answer the user's question.
If the answer is not in the context, say you don't know.

Context:
{context_section}"""

messages = [SystemMessage(content=system_content)] + state["messages"]
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The function uses state.get("paraphrased_text", "") but this value is set by the parafraseo node and should always exist at this point in the workflow. However, it's never used in the actual LLM invocation - only the messages from state["messages"] and the context are used. This means the paraphrasing step is effectively ignored, which is likely a bug. Either use the paraphrased text in the query, or reconsider if the paraphrasing step is needed.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +29
3. guard -> [conditional] -> fallback (Nodo 3) or END
4. fallback -> parafraseo (Nodo 4)
5. parafraseo -> retriever (Nodo 5)
6. retriever -> context_builder (Nodo 6)
7. context_builder -> generator (Nodo 7)
8. generator -> fallback_final (Nodo 8)
9. fallback_final -> [conditional] -> END (with final_response) or END (with error)
8. generator -> fallback (Nodo 8)
9. fallback -> [conditional] -> END (with final_response) or END (with error)
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The documentation is outdated and does not match the actual workflow implementation. According to the code, after guard detects a malicious prompt, it goes to fallback and then END, not "fallback (Nodo 3) or END". The documentation should reflect that fallback is always a node that terminates the flow.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +2
"""Nodo 3: Fallback Inicial - Initial fallback processing."""

Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The module docstring incorrectly states "Nodo 3: Fallback Inicial" when this node is now a unified fallback that can be called from different points in the workflow (both after initial guard and after context_builder). The docstring should be updated to reflect the new unified purpose.

Suggested change
"""Nodo 3: Fallback Inicial - Initial fallback processing."""
"""Unified fallback node - Handles fallback processing from multiple workflow points.
This node can be invoked after the initial guard or after the context builder,
providing a consistent fallback mechanism across the workflow.
"""

Copilot uses AI. Check for mistakes.
coderabbitai[bot]
coderabbitai bot previously requested changes Dec 14, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
RAGManager/app/agents/nodes/agent_host.py (1)

31-44: prompt = state["messages"][-1] is unsafe (IndexError) and wrong type (BaseMessage), and you persist it as DB text.
This will crash on empty messages, and even when present you’re storing the message object into ChatMessage.message / initial_context instead of its .content. (This is also the previously-reported empty-messages issue, plus a new correctness bug.)

     updated_state = state.copy()
     
-    prompt = state["messages"][-1]
+    messages = state.get("messages") or []
+    last_message = messages[-1] if messages else None
+    prompt = last_message.content if last_message else ""
@@
         new_message = ChatMessage(
             session_id=chat_session.id,
             sender="user",
             message=prompt,
         )
@@
-        updated_state["initial_context"] = prompt
+        updated_state["initial_context"] = prompt

(After this change, prompt is guaranteed str, and empty messages won’t raise.)

Also applies to: 78-83, 113-117

RAGManager/app/agents/nodes/fallback.py (2)

11-13: model="gpt-5-nano" will likely fail at runtime — use a valid/configured model.
This is the same issue already flagged in prior threads. Centralize the model name in config/env and reuse it here.

Is `gpt-5-nano` a valid model identifier for ChatOpenAI (langchain_openai) as of December 2025? If not, what are the correct current model names?

15-41: Harden fallback: avoid KeyError/empty messages, fix misleading system prompt, set error_message, and wrap llm.invoke in try/except.
This consolidates multiple already-raised concerns that are still present (docstring/return mismatch, state["messages"], misleading “database doesn’t have info”, and fallback crashing if the LLM call fails).

 """Nodo 3: Fallback - Handles fallback processing from multiple workflow points."""
 
 import logging
+import threading
 
 from app.agents.state import AgentState
+from app.core.config import settings
-from langchain_core.messages import SystemMessage
+from langchain_core.messages import SystemMessage, AIMessage
 from langchain_openai import ChatOpenAI
 
 logger = logging.getLogger(__name__)
 
-llm = ChatOpenAI(
-    model="gpt-5-nano",
-)
+_llm: ChatOpenAI | None = None
+_llm_lock = threading.Lock()
+
+def _get_llm() -> ChatOpenAI:
+    global _llm
+    if _llm is None:
+        with _llm_lock:
+            if _llm is None:
+                model = getattr(settings, "openai_model", "gpt-4o-mini")
+                _llm = ChatOpenAI(model=model)
+    return _llm
 
-# TO DO: implementar clase nodo fallback y inicializar el llm en el init
+# TODO: implementar clase nodo fallback y inicializar el llm en el init
 def fallback(state: AgentState) -> AgentState:
@@
-    logger.warning(
-        "Defensive check triggered: Malicious prompt detected" 
-    )
+    logger.warning("Fallback handler triggered")
     
-    messages = [
-        SystemMessage(
-            content="Your job is to generate an error message in user's language for the user explaining the database doesn't have the information to respond what the user asked"
-        )
-    ] + state["messages"]
-    error_message = llm.invoke(messages)
-    return {"messages": [error_message]}
+    prior_messages = list(state.get("messages") or [])
+    system_message = SystemMessage(
+        content=(
+            "Generate a short, polite refusal in the user's language. "
+            "Do not mention security checks, jailbreak detection, or private/sensitive data. "
+            "Offer to help with a different request."
+        )
+    )
+    messages = [system_message] + prior_messages
+
+    try:
+        error_msg = _get_llm().invoke(messages)
+        error_text = getattr(error_msg, "content", "")
+    except Exception:
+        logger.exception("LLM invocation failed in fallback node")
+        error_msg = AIMessage(content="Sorry, I can’t help with that request right now.")
+        error_text = error_msg.content
+
+    return {"messages": [error_msg], "error_message": error_text}

Note: if you keep def fallback(...) -> AgentState while returning partial updates, you must make AgentState non-total / use NotRequired (see state.py comment).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e869bed and 228c23b.

📒 Files selected for processing (4)
  • RAGManager/app/agents/nodes/agent_host.py (1 hunks)
  • RAGManager/app/agents/nodes/fallback.py (1 hunks)
  • RAGManager/app/agents/nodes/guard.py (2 hunks)
  • RAGManager/app/agents/state.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
RAGManager/app/agents/nodes/fallback.py (1)
RAGManager/app/agents/state.py (1)
  • AgentState (6-50)

Comment on lines 39 to 43
updated_state = state.copy()
prompt = state.get("prompt", "")
messages = state.get("messages", [])
last_message = messages[-1] if messages else None
prompt = last_message.content if last_message else ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Good shift to messages-based prompt + length-only logging; harden messages type and exception logging.
If state["messages"] can be non-list/None, treat it as empty. Also log exceptions with stack trace for ops.

     updated_state = state.copy()
-    messages = state.get("messages", [])
+    messages = state.get("messages")
+    if not isinstance(messages, list):
+        messages = []
     last_message = messages[-1] if messages else None
     prompt = last_message.content if last_message else ""
@@
-        logger.error(f"Error during jailbreak detection: {e}")
+        logger.exception("Error during jailbreak detection")

Also applies to: 67-75

Comment on lines +3 to +6
from langgraph.graph import MessagesState


class AgentState(TypedDict):
class AgentState(MessagesState):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

langgraph.graph.MessagesState TypedDict optional fields best practices

💡 Result:

Short best-practices for optional fields in langgraph.graph.MessagesState (TypedDict)

  1. Use PEP 655 tools to express “may be absent” vs “may be None”:

    • Mark keys that may be omitted with typing.NotRequired (or make the whole TypedDict total=False and use Required for the few keys that must exist). Do NOT use Optional[T] to mean “may be omitted” because Optional means T | None. [1][2]
  2. For langgraph message state specifically:

    • Keep the messages key typed as a sequence/list of message objects (e.g., Sequence[BaseMessage] or list[MessageLikeRepresentation]) and annotate it with the reducer (add_messages / operator.add) as shown in the langgraph docs. If messages must always exist, keep it required; if it can be absent, mark it NotRequired. [5][6]
  3. If a field can be present but accept None, express that as type | None (or Optional[T] if you prefer), separate from NotRequired which controls presence. Example:

    • name: NotRequired[str | None] # may be omitted; if present may be None. [1]
  4. Prefer explicit totality for readability:

    • Use class Foo(TypedDict) with explicit NotRequired/Required for mixed keys, or class Foo(TypedDict, total=False) + Required[...] for the inverse common case. This keeps one type declaration and is supported by type checkers (PEP 655). [1][3]
  5. Keep runtime behavior and static typing aligned:

    • TypedDict annotations are enforced by static checkers only. If your runtime needs to validate presence/type, add runtime checks or pydantic/dataclass wrappers; don’t rely on TypedDict at runtime. [3]

Sources

  • PEP 655: Required / NotRequired for TypedDict fields. [1]
  • PEP 589 TypedDict / total semantics. [3]
  • langgraph / LangGraph docs and examples (MessagesState, Annotated usage, message typing). [5][6]

🏁 Script executed:

# First, let me inspect the full AgentState implementation (lines 14-50)
cd RAGManager && find . -name "state.py" -path "*/agents/*" -type f

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 104


🏁 Script executed:

# Check if there's a pyproject.toml or requirements file to see langgraph version
cd RAGManager && find . -name "pyproject.toml" -o -name "requirements.txt" -o -name "requirements*.txt" | head -5

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 99


🏁 Script executed:

cat RAGManager/pyproject.toml | grep -A 20 -B 2 langgraph

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 567


🏁 Script executed:

cat -n RAGManager/app/agents/state.py

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 2086


🌐 Web query:

langgraph MessagesState source code definition TypedDict

💡 Result:

The MessagesState TypedDict in LangGraph is a small prebuilt state that defines a single key "messages" typed as a list of AnyMessage and annotated to use the add_messages reducer. Example source/definition shown in the docs:

from typing_extensions import TypedDict
from typing import Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages

class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]

(Uses the add_messages reducer so message lists are appended/updated rather than overwritten.) [1][2]

Sources:

  • LangGraph docs — Graph API / MessagesState. [1]
  • LangGraph tutorial / examples showing MessagesState TypedDict. [2]

🏁 Script executed:

# Check if there are any imports or usage patterns that suggest how these fields are set
grep -r "is_malicious\|is_risky" RAGManager/app/agents/ --include="*.py" -B 2 -A 2

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 3386


🏁 Script executed:

# Look for node implementations to understand which fields are conditionally set
find RAGManager/app/agents -name "*.py" -type f | head -10

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 469


🏁 Script executed:

# Check fallback.py to see how is_risky is set
grep -A 10 -B 5 "is_risky" RAGManager/app/agents/nodes/fallback.py

Repository: ucudal/reto-xmas-2025-goland-ia-backend

Length of output: 65


Use NotRequired to mark fields that nodes may not set on every execution.

AgentState extends MessagesState (a TypedDict) but marks fields like is_malicious and is_risky as required when they are only set conditionally by specific nodes. Since LangGraph nodes return partial state updates, these fields must be marked NotRequired. The codebase already uses defensive .get("is_malicious", False) in routing logic, confirming fields may be absent.

Apply NotRequired to all fields set conditionally by nodes:

  • is_malicious: NotRequired[bool] (guard node only)
  • is_risky: NotRequired[bool] (fallback_final node only)
  • prompt, initial_context, chat_messages, adjusted_text, paraphrased_text, relevant_chunks, enriched_query, primary_response, generated_response, final_response — all NotRequired since set by intermediate nodes, not guaranteed at init

Keep messages required (inherited from MessagesState) and chat_session_id, user_id as NotRequired[str | None] if they may be absent in message-first flows.

@Germanadjemian Germanadjemian merged commit 6a5b4ad into main Dec 14, 2025
1 check passed
@Germanadjemian Germanadjemian deleted the feature/fallback_inicial branch December 14, 2025 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants