Skip to content

Conversation

@PrithvijitBose
Copy link

@PrithvijitBose PrithvijitBose commented Aug 11, 2025

🌟 Feature Implementation: Dynamic FAQ Handler

Problem Solved

  • Eliminates manual, repetitive responses to foundational organizational questions
  • Replaces static FAQ databases with dynamic, real-time information retrieval
  • Provides automated public-facing representation for the organization

Key Features Implemented

Smart Query Routing: ReAct Supervisor intelligently identifies organizational queries
Dynamic Web Search: Real-time information retrieval from organization's public presence
Automated Synthesis: Converts search results into comprehensive, user-friendly responses
Source Attribution: Provides links for users to explore topics further

Testing Results

Successfully tested with query: "How can i contribute to AOSSIE Projects?"

  • ✅ Correct supervisor routing to FAQ Handler
  • ✅ Dynamic search query refinement
  • ✅ Successful web search execution
  • ✅ Comprehensive response generation with sources

Technical Implementation

  • Integrated with existing DevRel Agent architecture
  • Uses ReAct Supervisor for intelligent routing
  • Implements web search via refined query processing
  • Maintains consistency with existing tool patterns

Ready for review and production deployment.

Closes #97

bandicam.2025-08-10.19-47-44-389.mp4

Summary by CodeRabbit

  • New Features

    • Dynamic FAQ: live web search plus AI query refinement and synthesis delivering concise answers with up to three source citations and graceful fallbacks.
  • Enhancements

    • Supervisor: stricter iteration/history limits, validated actions, clearer decisions, and improved error handling/logging; new API to record tool results.
    • GitHub toolkit: returns structured error results on failure.
    • FAQ flow: site-restricted searches, query refinement, synthesis, formatted sources, and richer logging.
  • Configuration

    • New organization identity settings and example env entries for official handles.
  • Chores

    • Removed several scripts’ automatic run-on-exec behavior and test script auto-run.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

Walkthrough

Replaces the static FAQ handler with a site-restricted web-search + LLM pipeline, updates call sites to pass search_tool and llm, adds org identity settings and env placeholders, hardens the ReAct supervisor with caps/validation and tool-result handling, and removes script/test auto-run entry points.

Changes

Cohort / File(s) Summary
Agent Graph Wiring
backend/app/agents/devrel/agent.py
Instantiate FAQ tool with the agent LLM and register the FAQ handler using a partial that supplies search_tool and llm (replacing prior faq_tool wiring).
FAQ Handler (node)
backend/app/agents/devrel/nodes/handlers/faq.py
Reworked into a dynamic, multi-step FAQ handler that accepts search_tool and llm; adds ORG_NAME and OFFICIAL_HANDLES, site-restriction logic, query refinement, synthesis, formatting, fallbacks, logging; signature changed to handle_faq_node(state, search_tool, llm).
FAQ Tool (implementation)
backend/app/agents/devrel/tools/faq_tool.py
Replaced static FAQ with FAQTool(llm) implementing prompt loading, site-filter building, query refinement, web-search orchestration, synthesis, formatting with top sources, fallback generation, and get_response; added compatibility wrapper handle_faq_node.
Tool Wrappers / Callsites
backend/app/agents/devrel/tool_wrappers.py
Updated faq_handler_tool_node signature to accept/forward search_tool and llm and import from tools.faq_tool; refactored latest-message extraction and added exception handling + structured error response for github_toolkit.
ReAct Supervisor
backend/app/agents/devrel/nodes/react_supervisor.py
Added MAX_ITERATIONS, MAX_CONVERSATION_HISTORY, VALID_ACTIONS, state validation helpers, robust THINK/ACT/REASON parsing, iteration caps and completion/error responses, safe tool-results serialization, and new public add_tool_result.
Prompts
backend/app/agents/devrel/prompts/faq_prompt.py
Added REFINEMENT_PROMPT and SYNTHESIS_PROMPT templates used by the FAQ refinement/synthesis pipeline.
Settings & Env
backend/app/core/config/settings.py, env.example
Added org_name, org_website, org_github, org_twitter fields (with env aliases) and OFFICIAL_HANDLE_1/2/3 placeholders in env.example.
DB Scripts / Tests (entry-point removal)
backend/app/database/weaviate/scripts/create_schemas.py, .../populate_db.py, tests/test_embedding_service.py
Removed if __name__ == "__main__": auto-run guards from scripts/tests; functions remain defined but no longer auto-executed.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DevRelAgent
  participant ReactSupervisor
  participant FAQHandler
  participant SearchTool
  participant LLM

  User->>DevRelAgent: Ask org-level question
  DevRelAgent->>ReactSupervisor: react_supervisor_node(state)
  ReactSupervisor-->>DevRelAgent: Decision: faq_handler
  DevRelAgent->>FAQHandler: handle_faq_node(state, search_tool, llm)
  FAQHandler->>LLM: Refine query (with org/site filters)
  LLM-->>FAQHandler: Refined query
  FAQHandler->>SearchTool: Web search (site_filters)
  SearchTool-->>FAQHandler: Search results
  FAQHandler->>LLM: Synthesize answer + sources
  LLM-->>FAQHandler: Synthesized answer
  FAQHandler-->>DevRelAgent: Formatted response + sources
  DevRelAgent-->>User: Answer with citations
Loading
sequenceDiagram
  participant DevRelAgent
  participant ReactSupervisor
  participant LLM

  DevRelAgent->>ReactSupervisor: react_supervisor_node(state)
  ReactSupervisor->>ReactSupervisor: Validate state, enforce caps
  ReactSupervisor->>LLM: Supervisor prompt (context + tool_results)
  LLM-->>ReactSupervisor: THINK / ACT / REASON
  ReactSupervisor->>ReactSupervisor: Parse & validate action (guarded)
  ReactSupervisor-->>DevRelAgent: Decision, updated iteration_count or completion
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • smokeyScraper
  • chandansgowda

Poem

I twitch my whiskers, hop through docs and sites,
I fetch the truths beneath the official lights.
I stitch three links, refine and synthesize,
A rabbit’s answer, clear and concise. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The pull request includes several out-of-scope changes unrelated to the FAQ Handler feature. The github_toolkit error handling enhancement in backend/app/agents/devrel/tool_wrappers.py, while potentially beneficial, is not part of issue #97 requirements. Additionally, removal of main entry points from backend/app/database/weaviate/scripts/create_schemas.py, backend/app/database/weaviate/scripts/populate_db.py, and removal of unittest.main() from tests/test_embedding_service.py are infrastructure cleanup changes unrelated to the Dynamic FAQ Handler implementation. While the ReAct Supervisor enhancements [react_supervisor.py] provide necessary infrastructure for routing and state management, the breadth of those changes (MAX_ITERATIONS, MAX_CONVERSATION_HISTORY, add_tool_result function, etc.) extends beyond minimal support for FAQ integration. Consider isolating out-of-scope changes into separate pull requests: move github_toolkit error handling to its own PR, move database script entry point removals to a separate cleanup PR, and move unittest.main() removal to a test infrastructure PR. Evaluate whether all ReAct Supervisor enhancements are truly necessary for FAQ Handler integration, or defer non-essential supervisory improvements to a follow-up PR focused on general agent robustness improvements.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Dynamic FAQ Handler" clearly and concisely captures the primary change in this pull request: replacing the static FAQ approach with a dynamic, web-search-powered FAQ handler. The title is specific and meaningful enough for someone scanning commit history to quickly understand the core objective, without being vague or generic. It directly reflects the feature implementation described in the PR objectives and linked issue.
Linked Issues Check ✅ Passed The implementation comprehensively addresses all coding objectives from issue #97. The changes introduce a specialized FAQ Handler that leverages web search via TavilySearchTool [backend/app/agents/devrel/tools/faq_tool.py], dynamically searches organization-owned sources using site filters based on org_website, org_github, and org_twitter [backend/app/core/config/settings.py, faq_tool.py], implements query refinement and answer synthesis via _refine_faq_query and _synthesize_faq_response methods, and provides source attribution through _format_faq_response. The ReAct Supervisor integration [react_supervisor.py] enables proper routing of organizational queries to the FAQ handler, and the infrastructure supports a dynamic single-source-of-truth approach as required.
Docstring Coverage ✅ Passed Docstring coverage is 92.86% 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

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (9)
env.example (1)

29-29: Trailing whitespace on MAX_RETRIES

Remove the trailing space to avoid lint churn.

-MAX_RETRIES=3 
+MAX_RETRIES=3
backend/app/agents/devrel/nodes/handlers/faq.py (2)

114-181: Prompts: lower log verbosity for user content and queries

Sensitive user content and generated prompts are logged at INFO. Consider downgrading to DEBUG to reduce PII exposure in logs.

-    logger.info(f"[FAQ_HANDLER] Refined query: {refined_query}")
+    logger.debug(f"[FAQ_HANDLER] Refined query: {refined_query}")
...
-    logger.info("[FAQ_HANDLER] Step 3-4: Synthesizing search results into FAQ response")
+    logger.debug("[FAQ_HANDLER] Step 3-4: Synthesizing search results into FAQ response")
...
-    logger.info(f"[FAQ_HANDLER] Synthesized FAQ response: {synthesized_answer[:100]}...")
+    logger.debug(f"[FAQ_HANDLER] Synthesized FAQ response: {synthesized_answer[:100]}...")

214-238: Inline test harness in production module

The main test harness is useful locally but should live under tests/ to avoid accidental execution and to keep modules import-safe.

-# Example usage for testing
-if __name__ == "__main__":
-    ...
-    asyncio.run(test_faq_handler())
+# Consider moving the inline test harness to tests/ with an async test case.
backend/app/agents/devrel/nodes/react_supervisor.py (6)

167-171: Use timezone-aware timestamps in tool results

Naive datetime.now() is ambiguous; prefer UTC-aware timestamps for consistent logs and auditing.

Apply this diff:

-from datetime import datetime
+from datetime import datetime, timezone
@@
-            "timestamp": datetime.now().isoformat()
+            "timestamp": datetime.now(timezone.utc).isoformat()

Also applies to: 3-3


179-187: Deduplicate tools_used and handle None robustly

Avoid unbounded duplicates in tools_used and guard against None.

Apply this diff:

-        tools_used = getattr(state, 'tools_used', []) + [tool_name]
+        existing_tools = getattr(state, 'tools_used', []) or []
+        tools_used = list(dict.fromkeys([*existing_tools, tool_name]))

85-114: Make THINK/ACT/REASON parsing case-insensitive and tolerant to format variations

Current parsing requires exact uppercase keywords with no space before colon. LLMs often vary casing/spacing (e.g., "Act:", "REASON :"). Normalize case and split on the first colon.

Apply this diff:

-        for line in response.strip().split("\n"):
-            line = line.strip()
+        for line in response.strip().split("\n"):
+            line = line.strip()
+            line_upper = line.upper()
             if not line:
                 continue
@@
-            if line.startswith("THINK:"):
+            if line_upper.startswith("THINK"):
                 if current_section and content_buffer:
                     decision[current_section] = " ".join(content_buffer)
                 current_section = "thinking"
-                content_buffer = [line.replace("THINK:", "").strip()]
+                content_buffer = [line.split(":", 1)[1].strip()] if ":" in line else []
@@
-            elif line.startswith("ACT:"):
+            elif line_upper.startswith("ACT"):
                 if current_section and content_buffer:
                     decision[current_section] = " ".join(content_buffer)
-                action = line.replace("ACT:", "").strip().lower()
+                action = line.split(":", 1)[1].strip().lower() if ":" in line else ""
                 if action in VALID_ACTIONS:
                     decision["action"] = action
                 else:
                     logger.warning(f"Invalid action '{action}', defaulting to 'complete'")
                     decision["action"] = "complete"
                 current_section = None
                 content_buffer = []
@@
-            elif line.startswith("REASON:"):
+            elif line_upper.startswith("REASON"):
                 if current_section and content_buffer:
                     decision[current_section] = " ".join(content_buffer)
                 current_section = "reasoning"
-                content_buffer = [line.replace("REASON:", "").strip()]
+                content_buffer = [line.split(":", 1)[1].strip()] if ":" in line else []
@@
-        if decision["action"] not in VALID_ACTIONS:
+        if decision["action"] not in VALID_ACTIONS:
             logger.warning(f"Final validation failed for action '{decision['action']}', defaulting to 'complete'")
             decision["action"] = "complete"

Also applies to: 120-123


227-238: Strengthen state validation (types) to fail fast on malformed state

Minimal checks are good, but validating types prevents subtle downstream errors.

Apply this diff:

     try:
         if not state:
             return False
         if not getattr(state, 'session_id', None):
             logger.error("Invalid state: missing session_id")
             return False
-        if not hasattr(state, 'context'):
+        if not hasattr(state, 'context'):
             logger.error("Invalid state: missing context")
             return False
+        if not isinstance(state.context, dict):
+            logger.error("Invalid state: context must be a dict")
+            return False
+        if hasattr(state, 'messages') and state.messages is not None and not isinstance(state.messages, list):
+            logger.error("Invalid state: messages must be a list")
+            return False
         return True

57-66: Consider not storing raw supervisor_thinking unless in debug mode

Storing full LLM internal thoughts can leak sensitive content and inflate state size. Keep decision, but gate raw "supervisor_thinking" behind a debug flag or truncate.

If you want to keep it, consider:

  • Truncating to N chars (e.g., 1k).
  • Only storing when an env var like DEVREL_DEBUG is true.

Also applies to: 244-273


73-128: Add focused unit tests for decision parsing, iteration bounds, and tool result merging

These flows are critical to routing correctness and robustness.

Proposed test coverage:

  • _parse_supervisor_decision handles: empty response, mixed-case tags, extra spaces, invalid action fallback, multi-line THINK/REASON accumulation.
  • react_supervisor_node iteration cap triggers completion when iteration_count >= MAX_ITERATIONS.
  • supervisor_decision_router respects the same bound (>=) and defaults to "complete" on invalid actions.
  • add_tool_result trims history to 20, uses UTC timestamps, and deduplicates tools_used.

I can draft pytest tests if helpful. Would you like me to open a new issue or add a test file in this PR?

Also applies to: 130-151, 153-189

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4ff6eb and 303c637.

📒 Files selected for processing (6)
  • backend/app/agents/devrel/agent.py (1 hunks)
  • backend/app/agents/devrel/nodes/handlers/faq.py (1 hunks)
  • backend/app/agents/devrel/nodes/react_supervisor.py (1 hunks)
  • backend/app/agents/devrel/tool_wrappers.py (1 hunks)
  • backend/app/agents/devrel/tools/faq_tool.py (1 hunks)
  • env.example (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
backend/app/agents/devrel/agent.py (2)
backend/app/agents/devrel/tool_wrappers.py (1)
  • faq_handler_tool_node (18-24)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/devrel/nodes/react_supervisor.py (2)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/agents/devrel/nodes/generate_response.py (1)
  • _get_latest_message (97-101)
backend/app/agents/devrel/nodes/handlers/faq.py (4)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/devrel/tools/faq_tool.py (1)
  • search (35-37)
backend/app/agents/base_agent.py (1)
  • run (22-54)
backend/app/agents/devrel/tool_wrappers.py (5)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/devrel/nodes/handlers/faq.py (1)
  • handle_faq_node (27-59)
backend/app/agents/devrel/nodes/react_supervisor.py (1)
  • add_tool_result (153-192)
backend/app/agents/devrel/nodes/handlers/onboarding.py (1)
  • handle_onboarding_node (6-17)
🔇 Additional comments (4)
backend/app/agents/devrel/tools/faq_tool.py (1)

43-43: Fuzzy-match simplification LGTM

The inlined intersection check is concise and equivalent.

backend/app/agents/devrel/tool_wrappers.py (1)

37-41: Minor readability improvement LGTM

The latest_message one-liner is concise and clear.

backend/app/agents/devrel/nodes/handlers/faq.py (1)

87-106: Ignore string‐shape normalization; search always returns a list of dicts
The current DuckDuckGoSearchTool.search implementation (and any other active tool) always returns a List[Dict[str, Any]] (or an empty list on error). There is no code path that yields a raw string or single dict, so the added normalization isn’t needed.

Likely an incorrect or invalid review comment.

backend/app/agents/devrel/nodes/react_supervisor.py (1)

130-147: Action Name Consistency Confirmed

All routing action strings are used uniformly across the codebase, so there are no dead routes:

  • VALID_ACTIONS in backend/app/agents/devrel/nodes/react_supervisor.py
  • Workflow mapping in backend/app/agents/devrel/agent.py (lines 66–69)
  • Tool wrappers in backend/app/agents/devrel/tool_wrappers.py
  • Prompt definitions in backend/app/agents/devrel/prompts/react_prompt.py

No changes required.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
env.example (1)

31-35: Broken interpolation: ORG_ vars are not defined; OFFICIAL_HANDLE_ will expand to empty/literal strings**

Define ORG_NAME and ORG_WEBSITE/ORG_GITHUB/ORG_TWITTER before referencing them. Otherwise, the FAQ handler may generate queries like site:${ORG_WEBSITE} or no site filters at all.

Apply this diff:

 # Agent Configuration (optional)
 DEVREL_AGENT_MODEL=gemini-2.5-flash
 GITHUB_AGENT_MODEL=gemini-2.5-flash
 CLASSIFICATION_AGENT_MODEL=gemini-2.0-flash
 AGENT_TIMEOUT=30
-MAX_RETRIES=3 
+MAX_RETRIES=3
 
-# Explicit handles for faq.py
+# Organization identity and official sources (consumed by Settings and FAQ handler)
+ORG_NAME=AOSSIE
+ORG_WEBSITE=https://aossie.org
+ORG_GITHUB=https://github.com/AOSSIE-Org
+ORG_TWITTER=https://twitter.com/aossie_org
+
+# Explicit handles for faq.py (map to ORG_* above)
 OFFICIAL_HANDLE_1=${ORG_WEBSITE}
 OFFICIAL_HANDLE_2=${ORG_GITHUB}
 OFFICIAL_HANDLE_3=${ORG_TWITTER}
backend/app/agents/devrel/nodes/handlers/faq.py (1)

13-16: Good: removed import-time logging.basicConfig

Avoiding global logging configuration in a library module is the right call. Let the application configure logging.

backend/app/agents/devrel/nodes/react_supervisor.py (1)

61-66: Good: guard len() against None latest_message

Using len(latest_message or "") avoids TypeError.

🧹 Nitpick comments (5)
env.example (1)

29-29: Nit: trailing space after MAX_RETRIES

Trim the trailing space to avoid accidental parsing issues.

-MAX_RETRIES=3 
+MAX_RETRIES=3
backend/app/agents/devrel/nodes/handlers/faq.py (3)

53-69: Site filters: robust domain extraction; consider native include_domains when available

Domain parsing with urlparse is solid. Two improvement suggestions:

  • Prefer passing include_domains (or equivalent) to the search tool if supported, falling back to query-time site: tokens only when necessary.
  • For broad engine compatibility, concatenating site filters with whitespace is usually sufficient; the literal AND inside the query can be brittle depending on the backend parser.

If you want, I can update the wrapper/tool to optionally accept include_domains and branch accordingly.


108-112: Query composition: avoid brittle "AND" in the free-text string

Some providers do not honor boolean operators in the query string. Safer composition:

-        if site_filters:
-            refined_query = f"({refined_query}) AND ({site_filters})"
+        if site_filters:
+            refined_query = f"{refined_query} {site_filters}"

241-264: Keep test harness out of production modules

The main AsyncMock harness is handy, but consider moving this to a proper test under tests/ with pytest-asyncio to keep modules clean.

backend/app/agents/devrel/nodes/react_supervisor.py (1)

186-191: Prefer timezone-aware timestamps

Use UTC ISO timestamps to avoid ambiguity.

-from datetime import datetime
+from datetime import datetime, timezone
@@
-            "timestamp": datetime.now().isoformat()
+            "timestamp": datetime.now(timezone.utc).isoformat()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 303c637 and d46e0fc.

📒 Files selected for processing (7)
  • backend/app/agents/devrel/agent.py (1 hunks)
  • backend/app/agents/devrel/nodes/handlers/faq.py (1 hunks)
  • backend/app/agents/devrel/nodes/react_supervisor.py (1 hunks)
  • backend/app/agents/devrel/tool_wrappers.py (1 hunks)
  • backend/app/agents/devrel/tools/faq_tool.py (1 hunks)
  • backend/app/core/config/settings.py (1 hunks)
  • env.example (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/app/agents/devrel/tools/faq_tool.py
  • backend/app/agents/devrel/tool_wrappers.py
  • backend/app/agents/devrel/agent.py
🧰 Additional context used
🪛 Ruff (0.12.2)
backend/app/agents/devrel/nodes/react_supervisor.py

180-180: Local variable tool_results_str is assigned to but never used

Remove assignment to unused variable tool_results_str

(F841)

🔇 Additional comments (5)
backend/app/agents/devrel/nodes/handlers/faq.py (2)

41-52: Nice early-exit guard for empty message

Good defensive check and graceful fallback without invoking the LLM or search.


216-221: Sanitize titles/URLs downstream to prevent UI XSS

Since titles/urls come from the web, ensure the renderer escapes markdown/html. If you control the UI, sanitize before rendering.

backend/app/agents/devrel/nodes/react_supervisor.py (3)

33-37: Iteration cap guard in node looks good

Node prevents generating decisions once the cap is hit. Keep as-is.


38-47: Good: safely serializing tool_results for prompt construction

Using default=str and fallback prevents crashes from non-serializable entries.

Also applies to: 54-55


199-208: Actionable Directive: Verify State Merger’s Handling of tools_used

To confirm whether top‐level tools_used is merged into AgentState.tools_used or ignored, please locate and inspect the code that applies a node’s return-dict to the AgentState. For example, search for where output = node.run(...) is consumed and how it updates state.context and state.tools_used.

Suggested search commands:

# Find where node outputs are merged into AgentState
rg -n "state.context" -n backend/app/agents
rg -n "tools_used" -n backend/app/agents
rg -n "AgentState" -n backend/app/agents

Once you’ve identified whether tools_used must be returned at top level or embedded in context, update react_supervisor.py accordingly:

If the merger only looks at context:

-    tools_used = getattr(state, 'tools_used', []) + [tool_name]
-
-    return {
-        "context": {
-            **state.context,
-            "tool_results": tool_results
-        },
-        "tools_used": tools_used,
-        "current_task": f"completed_{tool_name}"
-    }
+    tools_used = state.context.get("tools_used", []) + [tool_name]
+
+    return {
+        "context": {
+            **state.context,
+            "tool_results": tool_results,
+            "tools_used": tools_used
+        },
+        "current_task": f"completed_{tool_name}"
+    }

If the merger respects top-level tools_used, no change is needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
backend/app/agents/devrel/nodes/handlers/faq.py (3)

5-5: Centralized org settings usage looks good

Switching to app_settings and dropping ad-hoc env parsing aligns config to a single source of truth. Good cleanup.

Also applies to: 12-15


8-9: Good: no global logging configuration at import time

Using a module logger without basicConfig avoids clobbering app-level logging config.


30-40: Early exit on empty user message

Solid guardrail to avoid unnecessary LLM/search calls and return a helpful fallback.

🧹 Nitpick comments (6)
backend/app/agents/devrel/nodes/handlers/faq.py (6)

42-57: Tighten site scoping for GitHub/Twitter using inurl to reduce noise

Current site filters restrict by domain only (e.g., github.com), which can be very broad. Optionally add inurl constraints for handles where a subpath is known (GitHub org, Twitter handle) to bias results toward the org space while still staying provider-agnostic.

Apply this diff:

-    from urllib.parse import urlparse
-    domains = []
+    from urllib.parse import urlparse
+    filters = set()
     for u in OFFICIAL_HANDLES:
         try:
             parsed = urlparse(u)
-            domain = parsed.netloc or parsed.path  # handles bare domains
-            if domain:
-                domains.append(domain)
+            domain = parsed.netloc or parsed.path  # handles bare domains
+            if not domain:
+                continue
+            filters.add(f"site:{domain}")
+            path = (parsed.path or "").strip("/")
+            # Add path scoping for common hubs to reduce noise
+            if path and (domain.endswith("github.com") or domain.endswith("twitter.com")):
+                filters.add(f"site:{domain} inurl:{path}")
         except Exception:
             continue
-    site_filters = " OR ".join([f"site:{d}" for d in domains])
+    site_filters = " OR ".join(filters)

192-195: Reduce verbosity and potential leakage in logs

Log synthesized content preview at debug, not info.

Apply this diff:

-    logger.info(f"[FAQ_HANDLER] Synthesized FAQ response: {synthesized_answer[:100]}...")
+    logger.debug(f"[FAQ_HANDLER] Synthesized FAQ response: {synthesized_answer[:100]}...")

67-74: Consider returning structured sources alongside the formatted answer

For richer UIs, expose sources as structured metadata (e.g., array of {title, url}) in task_result rather than only embedding Markdown links in the string. This enables downstream rendering and analytics without parsing.

Follow-up: have _dynamic_faq_process return both text and the top-N sources, or compute sources in handle_faq_node before returning task_result.


17-21: Optional: add lightweight Protocols for search_tool and llm

Helps static checking and refactoring safety without tying to specific implementations.

Add near the imports:

from typing import Protocol, runtime_checkable, Sequence
from langchain_core.messages import BaseMessage

@runtime_checkable
class SupportsSearch(Protocol):
    async def search(self, query: str) -> List[Dict[str, Any]]: ...

@runtime_checkable
class SupportsChatInvoke(Protocol):
    async def ainvoke(self, messages: Sequence[BaseMessage]) -> Any: ...

Then annotate:

async def handle_faq_node(state: AgentState, search_tool: SupportsSearch, llm: SupportsChatInvoke) -> dict:
    ...
async def _dynamic_faq_process(..., search_tool: SupportsSearch, llm: SupportsChatInvoke, ...): ...

229-253: Move module-level test harness to proper tests

Keep modules import-safe. Relocate main harness to tests or a sample notebook/script.

Apply this diff to remove it here:

-# Example usage for testing
-if __name__ == "__main__":
-    import asyncio
-    from unittest.mock import AsyncMock
-
-    class MockState:
-        session_id = "test_session"
-        messages = [{"content": "What projects does your organization have?"}]
-        context = {}
-
-    async def test_faq_handler():
-        mock_state = MockState()
-        mock_search_tool = AsyncMock()
-        mock_search_tool.search.return_value = [
-            {"title": "Project A", "content": "Details about Project A.", "url": "https://aossie.org/projects/a"},
-            {"title": "Project B", "content": "Details about Project B.", "url": "https://aossie.org/projects/b"},
-        ]
-        mock_llm = AsyncMock()
-        mock_llm.ainvoke.return_value = AsyncMock(content="We have Project A and Project B focusing on AI and Web.")
-
-        response = await handle_faq_node(mock_state, mock_search_tool, mock_llm)
-        print("FAQ Handler response:")
-        print(response)
-
-    asyncio.run(test_faq_handler())

97-101: Make query composition provider-agnostic by using whitespace instead of explicit AND

Update the FAQ handler’s query concatenation to avoid hard-coding AND, relying on implicit AND (whitespace) which is supported more broadly:

--- a/backend/app/agents/devrel/nodes/handlers/faq.py
+++ b/backend/app/agents/devrel/nodes/handlers/faq.py
@@ -98,7 +98,7 @@ async def _dynamic_faq_process(
-        if site_filters:
-            refined_query = f"({refined_query}) AND ({site_filters})"
+        if site_filters:
+            refined_query = f"{refined_query} {site_filters}"
        logger.info(f"[FAQ_HANDLER] Refined and filtered query: {refined_query}")

This change removes the explicit AND, allowing providers that treat whitespace as an implicit AND to handle the query correctly. Please verify that each search tool implementation you use (e.g. DuckDuckGoSearchTool, TavilySearchTool) supports boolean operators and grouping as expected.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d46e0fc and 7743d07.

📒 Files selected for processing (2)
  • backend/app/agents/devrel/nodes/handlers/faq.py (1 hunks)
  • backend/app/agents/devrel/nodes/react_supervisor.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/agents/devrel/nodes/react_supervisor.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/app/agents/devrel/nodes/handlers/faq.py (3)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/base_agent.py (1)
  • run (22-54)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
backend/app/core/config/settings.py (1)

55-58: Pydantic v2: class Config is ignored; use SettingsConfigDict

In pydantic v2/pydantic-settings v2, the nested Config class is no-op. Switch to model_config so env_file/extra/case sensitivity actually take effect.

Apply this diff:

+from pydantic_settings import SettingsConfigDict
@@
-    class Config:
-        env_file = ".env"
-        extra = "ignore"  # to prevent errors from extra env variables
+    # pydantic-settings v2 configuration
+    model_config = SettingsConfigDict(
+        env_file=".env",
+        extra="ignore",
+        case_sensitive=False,  # allow ORG_NAME or org_name
+    )
🧹 Nitpick comments (1)
backend/app/core/config/settings.py (1)

42-42: Nit: remove emoji from code comments for consistency

Avoid emojis in source comments to keep style consistent with the rest of the file.

Apply this diff:

-    # ✅ New org info fields (pulled from .env automatically)
+    # Organization info fields (populated from environment)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7743d07 and d44156d.

📒 Files selected for processing (1)
  • backend/app/core/config/settings.py (1 hunks)
🔇 Additional comments (1)
backend/app/core/config/settings.py (1)

42-47: LGTM: centralizing org identity in Settings

Adding org_* fields here aligns with the goal to make branding/handles a single source of truth for the dynamic FAQ flow. This addresses prior feedback about avoiding separate env parsing in faq.py.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
backend/app/agents/devrel/nodes/handlers/faq.py (1)

42-58: Normalize handles to bare domains for reliable site: filtering

As written, Line 49 may capture path segments (e.g., "github.com/AOSSIE-Org") when OFFICIAL_HANDLES contain bare values without a scheme, which would lead to invalid site: filters. Strip path segments and "www." to ensure domains only.

Apply this diff within the loop:

-                domain = parsed.netloc or parsed.path  # handles bare domains
-                if domain:
-                    domains.append(domain)
+                domain = parsed.netloc or parsed.path  # handles bare domains
+                if domain:
+                    # Keep only host, drop any path and "www." prefix
+                    domain = domain.split("/")[0]
+                    if domain.startswith("www."):
+                        domain = domain[4:]
+                    if domain:
+                        domains.append(domain)

Optionally dedupe before building filters:

-        site_filters = " OR ".join([f"site:{d}" for d in domains])
+        domains = list(dict.fromkeys(domains))  # preserve order, remove dups
+        site_filters = " OR ".join([f"site:{d}" for d in domains])
🧹 Nitpick comments (4)
backend/app/agents/devrel/nodes/handlers/faq.py (4)

24-28: Make sure you’re using the latest human/user message, not potentially the last assistant message

If state.messages ends with an assistant reply, Line 26 may pick up the assistant’s content. Prefer selecting the most recent human/user message.

If your messages include a role/type, consider:

-    if state.messages:
-        latest_message = state.messages[-1].get("content", "")
+    if state.messages:
+        for msg in reversed(state.messages):
+            role = (msg.get("role") or msg.get("type") or "user").lower()
+            if role in ("user", "human"):
+                latest_message = msg.get("content", "")
+                break

Please confirm whether your message dicts include role/type so we can finalize the patch accordingly.


200-213: Channel-aware formatting for sources

The Markdown/emoji formatting on Lines 207-212 is nice but may render poorly on some channels. Consider a settings toggle (e.g., faq_markdown_enabled) or falling back to plain text when markdown is unsupported.

For a plain-text fallback:

-        formatted_response += "\n\n**📚 Sources:**"
+        formatted_response += "\n\nSources:"
-            formatted_response += f"\n{i+1}. [{title}]({url})"
+            formatted_response += f"\n{i+1}. {title} - {url}"

231-255: Move the main test harness to a proper test to keep production modules clean

Keeping executable harnesses in production modules is unconventional and easy to forget. Move this to tests/ with AsyncMock-based unit tests.

I can port this into pytest/asyncio tests that cover:

  • Empty-message fallback
  • Successful dynamic flow (query refinement, search, synthesis)
  • Search error path
  • No-results path
  • Source formatting cap (top 3)
    Want me to push a test module?

17-21: Stronger typing via Protocols for search_tool and llm

Using Any everywhere weakens static guarantees. Introduce minimal Protocols to document the expected async interface.

Add (outside the selected range):

from typing import Protocol, runtime_checkable

@runtime_checkable
class AsyncSearchTool(Protocol):
    async def search(self, query: str) -> List[Dict[str, Any]]: ...

@runtime_checkable
class AsyncLLM(Protocol):
    async def ainvoke(self, messages: List[HumanMessage]) -> Any: ...

Then:

-async def handle_faq_node(state: AgentState, search_tool: Any, llm: Any) -> dict:
+async def handle_faq_node(state: AgentState, search_tool: AsyncSearchTool, llm: AsyncLLM) -> dict:

This keeps flexibility while improving correctness.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d44156d and cea5a23.

📒 Files selected for processing (1)
  • backend/app/agents/devrel/nodes/handlers/faq.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/app/agents/devrel/nodes/handlers/faq.py (3)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/base_agent.py (1)
  • run (22-54)
🔇 Additional comments (4)
backend/app/agents/devrel/nodes/handlers/faq.py (4)

8-10: Good: Logger configured without global basicConfig

Module uses a local logger and avoids configuring global logging at import-time. This aligns with best practices and prior review feedback.


12-15: Good: Centralized org config via app settings

Using app_settings for ORG_NAME and OFFICIAL_HANDLES prevents env drift and consolidates configuration.


30-41: Good: Early exit on empty message with graceful fallback

Short-circuiting avoids unnecessary tool/LLM invocations and returns a structured fallback.


97-101: Verify search_tool’s Boolean operator support

Please confirm whether the search_tool.search implementation reliably handles explicit Boolean operators (e.g., “AND”). Many web-search back ends ignore or misinterpret these operators, which can lead to unexpected results. If it does not:

  • In backend/app/agents/devrel/nodes/handlers/faq.py, inside _dynamic_faq_process (lines ~97–101), replace
    if site_filters:
        refined_query = f"({refined_query}) AND ({site_filters})"
    with a more search-friendly concatenation, for example:
    if site_filters:
        refined_query = f"{refined_query} {site_filters}"
  • Better yet, if your search tool exposes a structured API (e.g., search_tool.search(query, site_filters=domains)), switch to that instead of in-query filtering.

Comment on lines +94 to +101
logger.info(f"[FAQ_HANDLER] Step 1: Refining FAQ query for org '{org_name}'")
refined_query = await _refine_faq_query(message, llm, org_name)

# Append site filters for restricting to official handles
if site_filters:
refined_query = f"({refined_query}) AND ({site_filters})"
logger.info(f"[FAQ_HANDLER] Refined and filtered query: {refined_query}")

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Reduce user-content logging to DEBUG and truncate to lower PII exposure and noise

Refined queries and synthesized responses may contain user-provided content. Logging them at INFO can leak PII and increase noise.

Apply:

-        logger.info(f"[FAQ_HANDLER] Step 1: Refining FAQ query for org '{org_name}'")
+        logger.debug(f"[FAQ_HANDLER] Step 1: Refining FAQ query for org '{org_name}'")
@@
-        logger.info(f"[FAQ_HANDLER] Refined and filtered query: {refined_query}")
+        logger.debug(f"[FAQ_HANDLER] Refined and filtered query: {refined_query[:200]}{'...' if len(refined_query) > 200 else ''}")
@@
-    logger.info(f"[FAQ_HANDLER] Synthesized FAQ response: {synthesized_answer[:100]}...")
+    logger.debug(f"[FAQ_HANDLER] Synthesized FAQ response: {synthesized_answer[:100]}...")

If you need INFO-level observability, consider redacting with a sanitizer (emails, phone numbers) before logging.

Also applies to: 196-196

Comment on lines +103 to +108
logger.info(f"[FAQ_HANDLER] Step 2: Searching for: {refined_query}")
try:
search_results = await search_tool.search(refined_query)
except Exception as search_err:
logger.error(f"[FAQ_HANDLER] Search tool error: {search_err}")
return _generate_fallback_response(message, org_name)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add timeouts around external search calls to prevent hangs

Wrap the search call with asyncio.wait_for using configurable timeouts; otherwise a network stall can block the node.

Apply:

-        try:
-            search_results = await search_tool.search(refined_query)
+        try:
+            search_results = await asyncio.wait_for(
+                search_tool.search(refined_query),
+                timeout=FAQ_SEARCH_TIMEOUT
+            )

Add these near the top of the module (outside the selected range):

import asyncio  # at the imports

# Timeouts with safe defaults; override via settings if available
FAQ_SEARCH_TIMEOUT = getattr(app_settings, "faq_search_timeout", 10.0)
FAQ_LLM_TIMEOUT = getattr(app_settings, "faq_llm_timeout", 15.0)
🤖 Prompt for AI Agents
In backend/app/agents/devrel/nodes/handlers/faq.py around lines 103 to 108, the
external search call can hang; add an asyncio timeout: import asyncio at the top
and define FAQ_SEARCH_TIMEOUT = getattr(app_settings, "faq_search_timeout",
10.0) (and FAQ_LLM_TIMEOUT similarly if present), then replace the direct await
search_tool.search(refined_query) with await
asyncio.wait_for(search_tool.search(refined_query), FAQ_SEARCH_TIMEOUT); catch
asyncio.TimeoutError (and still catch general Exception) and log a
timeout-specific error before returning _generate_fallback_response(message,
org_name).

@PrithvijitBose PrithvijitBose force-pushed the FAQ-HANDLER branch 4 times, most recently from fe0cfe8 to de1246e Compare August 13, 2025 13:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
backend/app/agents/devrel/nodes/react_supervisor.py (2)

34-37: Fix iteration cap check to allow exactly MAX_ITERATIONS

The current check prevents routing the action decided on the MAX-th iteration. Since the node increments iteration_count after decision-making, you should use > instead of >= to allow exactly MAX_ITERATIONS to proceed.

-    if iteration_count >= MAX_ITERATIONS:
+    if iteration_count > MAX_ITERATIONS:
         logger.warning(f"Max iterations ({MAX_ITERATIONS}) reached for session {state.session_id}")
         return _create_completion_response(state, "Maximum iterations reached")

147-149: Align iteration boundary check with node logic

This router check should match the node's iteration check. Since you're incrementing before routing, use > to allow the MAX-th iteration's action to be routed.

-        if iteration_count > MAX_ITERATIONS:
+        if iteration_count >= MAX_ITERATIONS:
             logger.warning(f"Max iterations reached for session {state.session_id}")
             return "complete"

Wait, I notice the current code already uses > which is actually correct. The node should be changed to match this, not the other way around.

backend/app/agents/devrel/nodes/handlers/faq.py (1)

103-108: Add timeout for search operations

External search calls could hang indefinitely. Consider adding a timeout to prevent blocking.

Add at the top of the file:

import asyncio

# Configurable timeouts
FAQ_SEARCH_TIMEOUT = getattr(app_settings, "faq_search_timeout", 10.0)
FAQ_LLM_TIMEOUT = getattr(app_settings, "faq_llm_timeout", 15.0)

Then wrap the search call:

         try:
-            search_results = await search_tool.search(refined_query)
+            search_results = await asyncio.wait_for(
+                search_tool.search(refined_query),
+                timeout=FAQ_SEARCH_TIMEOUT
+            )
+        except asyncio.TimeoutError:
+            logger.error(f"[FAQ_HANDLER] Search timeout after {FAQ_SEARCH_TIMEOUT}s")
+            return _generate_fallback_response(message, org_name)
         except Exception as search_err:
🧹 Nitpick comments (2)
backend/app/agents/devrel/nodes/handlers/faq.py (2)

94-100: Consider adding DEBUG level for detailed query logging

The refined query may contain user-provided content. Consider using DEBUG level and truncating for privacy.

-        logger.info(f"[FAQ_HANDLER] Step 1: Refining FAQ query for org '{org_name}'")
+        logger.debug(f"[FAQ_HANDLER] Step 1: Refining FAQ query for org '{org_name}'")

         # ...
-        logger.info(f"[FAQ_HANDLER] Refined and filtered query: {refined_query}")
+        logger.debug(f"[FAQ_HANDLER] Refined and filtered query: {refined_query[:200]}{'...' if len(refined_query) > 200 else ''}")

232-255: Consider moving test code to a separate test file

The test harness in __main__ is useful for development but should ideally be in a separate test file for better organization.

Consider moving this to a file like tests/test_faq_handler.py to keep the production code clean and maintain separation of concerns.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe0cfe8 and b05f020.

📒 Files selected for processing (7)
  • backend/app/agents/devrel/agent.py (1 hunks)
  • backend/app/agents/devrel/nodes/handlers/faq.py (1 hunks)
  • backend/app/agents/devrel/nodes/react_supervisor.py (1 hunks)
  • backend/app/agents/devrel/tool_wrappers.py (1 hunks)
  • backend/app/agents/devrel/tools/faq_tool.py (1 hunks)
  • backend/app/core/config/settings.py (2 hunks)
  • env.example (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/app/agents/devrel/agent.py
  • backend/app/agents/devrel/tools/faq_tool.py
  • env.example
  • backend/app/core/config/settings.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-08T13:31:11.572Z
Learnt from: smokeyScraper
PR: AOSSIE-Org/Devr.AI#72
File: backend/app/agents/devrel/nodes/handle_web_search_node.py:31-42
Timestamp: 2025-06-08T13:31:11.572Z
Learning: In backend/app/agents/devrel/tools/search_tool.py, the TavilySearchTool.search() method has partial error handling for missing API key, AttributeError, ConnectionError, and TimeoutError, but lacks a comprehensive Exception catch-all block, so calling functions may still need additional error handling for other potential exceptions.

Applied to files:

  • backend/app/agents/devrel/nodes/handlers/faq.py
🧬 Code Graph Analysis (3)
backend/app/agents/devrel/nodes/handlers/faq.py (3)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/base_agent.py (1)
  • run (22-54)
backend/app/agents/devrel/nodes/react_supervisor.py (2)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/agents/devrel/nodes/generate_response.py (1)
  • _get_latest_message (97-101)
backend/app/agents/devrel/tool_wrappers.py (5)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/devrel/nodes/handlers/faq.py (1)
  • handle_faq_node (17-74)
backend/app/agents/devrel/nodes/react_supervisor.py (1)
  • add_tool_result (163-202)
backend/app/agents/devrel/nodes/handlers/onboarding.py (1)
  • handle_onboarding_node (6-17)
🔇 Additional comments (12)
backend/app/agents/devrel/nodes/react_supervisor.py (4)

62-62: Good defensive programming for None handling

The use of len(latest_message or '') properly guards against None values that could cause a TypeError.


205-212: Good error handling wrapper for utility function

The wrapper properly catches exceptions from the imported utility function and provides a safe fallback, preventing crashes in the supervisor node.


237-252: Comprehensive state validation

The validation function properly checks for essential state attributes with detailed error logging, which will help with debugging issues in production.


38-46: Remove unused variable tool_results_str

The variable tool_results_str is computed but never used in the prompt construction. The prompt uses the same serialization logic directly on line 54.

-    # Safely serialize tool_results for prompt usage
-    try:
-        if tool_results:
-            tool_results_str = json.dumps(tool_results, indent=2, default=str)
-        else:
-            tool_results_str = "No previous tool results"
-    except Exception as e:
-        logger.warning(f"Failed to serialize tool_results: {e}")
-        tool_results_str = str(tool_results) if tool_results else "No previous tool results"

Then update line 54 to safely serialize:

-        tool_results=tool_results_str
+        tool_results=json.dumps(tool_results, indent=2, default=str) if tool_results else "No previous tool results"

Likely an incorrect or invalid review comment.

backend/app/agents/devrel/tool_wrappers.py (3)

18-24: LGTM! Wrapper signature properly aligned with FAQ handler

The function signature correctly accepts search_tool and llm parameters and forwards them to the FAQ handler, which aligns with the dynamic FAQ implementation requirements.


37-41: Good refactoring for message extraction

The conditional expression cleanly handles both scenarios (messages present vs context fallback) in a readable way.


43-53: Excellent error handling for GitHub toolkit

The try-catch block properly captures toolkit errors and returns a structured error response that can be processed downstream, preventing crashes while maintaining observability.

backend/app/agents/devrel/nodes/handlers/faq.py (5)

13-14: Good use of centralized settings

Using app_settings for organization configuration ensures consistency across the application and avoids duplicate environment variable parsing.


30-41: Smart early exit for empty messages

The early return prevents unnecessary LLM and search API calls when there's no user input to process, improving efficiency.


43-57: Well-implemented domain extraction for site filters

The URL parsing logic properly extracts domains from URLs and handles both full URLs and bare domains, with good error handling for malformed URLs.


169-171: Good content truncation for token management

Limiting content to 500 characters prevents token explosion in LLM prompts while maintaining useful context.


216-228: Excellent user-friendly fallback message

The fallback response is informative, explains potential reasons for failure, and provides actionable next steps for users.

Comment on lines +194 to +195
response = await llm.ainvoke([HumanMessage(content=synthesis_prompt)])
synthesized_answer = response.content.strip()
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Apply timeout to LLM synthesis calls

Similar to search operations, LLM calls should have timeouts to prevent hanging.

-    response = await llm.ainvoke([HumanMessage(content=synthesis_prompt)])
+    response = await asyncio.wait_for(
+        llm.ainvoke([HumanMessage(content=synthesis_prompt)]),
+        timeout=FAQ_LLM_TIMEOUT
+    )

Add exception handling:

except asyncio.TimeoutError:
    logger.error(f"[FAQ_HANDLER] LLM timeout after {FAQ_LLM_TIMEOUT}s")
    return _generate_fallback_response(message, org_name)
🤖 Prompt for AI Agents
In backend/app/agents/devrel/nodes/handlers/faq.py around lines 194-195, wrap
the LLM synthesis call in asyncio.wait_for using the FAQ_LLM_TIMEOUT constant
and add exception handling: put the await llm.ainvoke(...) inside a try block
with response = await asyncio.wait_for(llm.ainvoke(...), FAQ_LLM_TIMEOUT), catch
asyncio.TimeoutError, log the timeout with logger.error(f"[FAQ_HANDLER] LLM
timeout after {FAQ_LLM_TIMEOUT}s") and return
_generate_fallback_response(message, org_name); ensure asyncio is imported if
not already.

Copy link
Contributor

@smokeyScraper smokeyScraper left a comment

Choose a reason for hiding this comment

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

The workflow seems fine to me. A lot of restructuring changes are needed. Please do the needful.
I'm up for suggestions on your implementation.

Thanks a lot @PrithvijitBose !

PS: Please do review the coderrabit suggestions too.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
backend/app/agents/devrel/tool_wrappers.py (1)

18-24: LGTM! Signature correctly aligned with dynamic FAQ handler.

The parameter rename from faq_tool to search_tool and the updated call to handle_faq_node correctly implement the past review feedback. This allows the wrapper to accept DuckDuckGoSearchTool and forward it to the dynamic FAQ handler.

🧹 Nitpick comments (1)
backend/app/agents/devrel/tool_wrappers.py (1)

37-41: Minor refactor: Conditional expression for message extraction.

The refactored conditional expression for extracting latest_message is more concise. Consider extracting this pattern into a helper function if it's used in multiple tool nodes to reduce duplication.

Example helper:

def extract_latest_message(state: AgentState) -> str:
    """Extract latest message from state"""
    return (
        state.messages[-1].get("content", "")
        if state.messages else
        state.context.get("original_message", "")
    )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b05f020 and 295065c.

📒 Files selected for processing (2)
  • backend/app/agents/devrel/agent.py (2 hunks)
  • backend/app/agents/devrel/tool_wrappers.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/agents/devrel/tool_wrappers.py (4)
backend/app/agents/devrel/nodes/handlers/faq.py (1)
  • handle_faq_node (17-74)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/agents/devrel/nodes/react_supervisor.py (1)
  • add_tool_result (163-202)
backend/app/agents/devrel/nodes/handlers/onboarding.py (1)
  • handle_onboarding_node (6-17)
backend/app/agents/devrel/agent.py (3)
backend/app/agents/devrel/tools/faq_tool.py (1)
  • FAQTool (6-43)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/devrel/tool_wrappers.py (1)
  • faq_handler_tool_node (18-24)

)
self.search_tool = DuckDuckGoSearchTool()
self.faq_tool = FAQTool()
self.faq_tool = FAQTool(search_tool=self.search_tool, llm=self.llm)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Remove unused FAQTool instance.

The self.faq_tool instance is created but never used. The workflow now calls handle_faq_node directly via the tool wrapper, bypassing the FAQTool class. This line (and potentially the import on line 9) can be removed.

Verify that FAQTool is not used elsewhere in the codebase:

If no other usage is found, apply this diff:

-from .tools.faq_tool import FAQTool
-        self.faq_tool = FAQTool(search_tool=self.search_tool, llm=self.llm)

🏁 Script executed:

#!/bin/bash
# Search for usage of self.faq_tool in the DevRel agent and related files
rg -n 'self\.faq_tool' --type py -g '!__pycache__' -g '!*.pyc'

Length of output: 113


🏁 Script executed:

#!/bin/bash
# Search for any usage of FAQTool instantiation or calls elsewhere
rg -n 'FAQTool\(' --type py -g '!__pycache__' -g '!*.pyc'

Length of output: 113


Remove unused FAQTool import and instantiation

-from .tools.faq_tool import FAQTool-        self.faq_tool = FAQTool(search_tool=self.search_tool, llm=self.llm)
📝 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
self.faq_tool = FAQTool(search_tool=self.search_tool, llm=self.llm)
--- a/backend/app/agents/devrel/agent.py
@@
-from .tools.faq_tool import FAQTool
@@ class DevrelAgent:
def __init__(self, config, llm, search_tool):
# existing initialization...
- self.faq_tool = FAQTool(search_tool=self.search_tool, llm=self.llm)
🤖 Prompt for AI Agents
In backend/app/agents/devrel/agent.py around line 31, the FAQTool is imported
and instantiated but not used; remove the unused import and delete the
instantiation (self.faq_tool = FAQTool(...)) to avoid dead code, and run
tests/linting to ensure no other references remain—if other code expects
faq_tool, either wire it up properly or remove those references as well.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
backend/app/agents/devrel/prompts/faq_prompt.py (1)

15-58: Normalize punctuation for linting

The templates include en dashes (“2–3”) and a curly apostrophe (“you’re”). Ruff flags these as ambiguous characters; replace them with ASCII hyphen-minus and straight apostrophe to keep lint clean.

-3. **Be informative yet concise (2–3 paragraphs max).**
+3. **Be informative yet concise (2-3 paragraphs max).**-3. Keep it concise (2–3 paragraphs).
+3. Keep it concise (2-3 paragraphs).-6. Avoid mentioning “search results” — write as though you’re providing expert knowledge.
+6. Avoid mentioning "search results" — write as though you're providing expert knowledge.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b05f020 and 5299f17.

📒 Files selected for processing (3)
  • backend/app/agents/devrel/agent.py (2 hunks)
  • backend/app/agents/devrel/prompts/faq_prompt.py (1 hunks)
  • backend/app/agents/devrel/tool_wrappers.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/agents/devrel/tool_wrappers.py (3)
backend/app/agents/devrel/nodes/handlers/faq.py (1)
  • handle_faq_node (17-74)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/agents/devrel/nodes/react_supervisor.py (1)
  • add_tool_result (163-202)
backend/app/agents/devrel/agent.py (3)
backend/app/agents/devrel/tools/faq_tool.py (1)
  • FAQTool (6-43)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/devrel/tool_wrappers.py (1)
  • faq_handler_tool_node (18-24)
🪛 Ruff (0.13.3)
backend/app/agents/devrel/prompts/faq_prompt.py

15-15: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


54-54: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


57-57: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

Comment on lines +12 to +57
### Instructions:
1. **Answer the user's question directly and conversationally.**
2. **Focus on the most relevant and recent information.**
3. **Be informative yet concise (2–3 paragraphs max).**
4. **If the search results don't fully answer the question, acknowledge that clearly.**
5. **Maintain a knowledgeable and professional tone** as if you are part of {org_name}.
6. **Do not mention "search results" or sources** — respond as if you know the information first-hand.
---
### Your Response:
"""
SYNTHESIS_PROMPT = """
You are an AI assistant representing **{org_name}**.
A user asked: "{message}"
Below are search results gathered from official sources about {org_name}.
Search Results:
{results_context}
---
### Step 1: Refine User Query
Refine the user's question to make it suitable for an internal FAQ or search engine query.
**Organization:** {org_name}
**Original User Question:** {message}
---
### Step 2: Synthesize the Information
Based on the refined query and search context, generate a clear, accurate, and conversational answer.
**Search Results Context:**
{results_context}
---
### Step 3: Write the Final Answer
1. Respond directly and conversationally.
2. Highlight the most relevant and updated facts.
3. Keep it concise (2–3 paragraphs).
4. Acknowledge any missing information if applicable.
5. Sound professional and informed about {org_name}.
6. Avoid mentioning “search results” — write as though you’re providing expert knowledge.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore source attribution in the FAQ prompts

The dynamic FAQ feature must return source links, but both prompts explicitly forbid mentioning sources. That contradicts the product requirement (“Provide source links for verification”) and will suppress citations at runtime. Update the instructions/response sections so the model is told to include the extracted sources (e.g., “Append a bulleted ‘Sources’ section with the URLs you used”).

🧰 Tools
🪛 Ruff (0.13.3)

15-15: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


54-54: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


57-57: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
backend/app/agents/devrel/prompts/faq_prompt.py (1)

24-62: Consider consolidating duplicative prompt logic.

Both REFINEMENT_PROMPT and SYNTHESIS_PROMPT contain similar instructions and formatting. The main difference is that SYNTHESIS_PROMPT adds a three-step structure. Consider whether both are necessary or if one can be parameterized to serve both purposes.

backend/app/agents/devrel/tools/faq_tool.py (4)

98-107: Improve exception logging for better debugging.

Use logging.exception() instead of logging.error() to automatically include the stack trace, which aids in debugging.

Apply this diff:

         except Exception as e:
-            logger.error(f"[FAQ_TOOL] Failed to handle FAQ: {e}")
+            logger.exception(f"[FAQ_TOOL] Failed to handle FAQ: {e}")
             return {

115-122: Consider logging URL parsing failures for observability.

While silently continuing on invalid URLs is reasonable, logging failures at debug level could help diagnose configuration issues.

             try:
                 parsed = urlparse(u)
                 domain = parsed.netloc or parsed.path
                 if domain:
                     domains.append(domain)
             except Exception:
+                logger.debug(f"[FAQ_TOOL] Failed to parse handle URL: {u}")
                 continue

146-148: Improve exception logging in search and pipeline error handlers.

Use logging.exception() to include stack traces for better debugging of search failures and pipeline errors.

Apply this diff:

             except Exception as err:
-                logger.error(f"[FAQ_TOOL] Search failed: {err}")
+                logger.exception(f"[FAQ_TOOL] Search failed: {err}")
                 return self._generate_fallback_response(message)
         except Exception as e:
-            logger.error(f"[FAQ_TOOL] Error in dynamic FAQ process: {e}")
+            logger.exception(f"[FAQ_TOOL] Error in dynamic FAQ process: {e}")
             return self._generate_fallback_response(message)

Also applies to: 162-164


167-175: Consider adding timeout to query refinement LLM call for consistency.

The synthesis step (line 198) uses asyncio.wait_for() with FAQ_LLM_TIMEOUT, but this refinement call does not. For consistency and resilience, consider adding a timeout here as well.

     async def _refine_faq_query(self, message: str) -> str:
         """Refine user's question into an optimized search query."""
         refinement_prompt = load_prompt("refinement_prompt").format(
             org_name=ORG_NAME, message=message
         )
-        response = await self.llm.ainvoke([HumanMessage(content=refinement_prompt)])
+        response = await asyncio.wait_for(
+            self.llm.ainvoke([HumanMessage(content=refinement_prompt)]),
+            timeout=FAQ_LLM_TIMEOUT
+        )
         refined_query = response.content.strip()
         logger.info(f"[FAQ_TOOL] Refined query: {refined_query}")
         return refined_query
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 295065c and 70f760f.

📒 Files selected for processing (5)
  • backend/app/agents/devrel/prompts/faq_prompt.py (1 hunks)
  • backend/app/agents/devrel/tools/faq_tool.py (1 hunks)
  • backend/app/database/weaviate/scripts/create_schemas.py (0 hunks)
  • backend/app/database/weaviate/scripts/populate_db.py (1 hunks)
  • tests/test_embedding_service.py (0 hunks)
💤 Files with no reviewable changes (2)
  • tests/test_embedding_service.py
  • backend/app/database/weaviate/scripts/create_schemas.py
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/agents/devrel/tools/faq_tool.py (2)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/agents/devrel/nodes/handlers/faq.py (6)
  • _generate_fallback_response (216-228)
  • _dynamic_faq_process (77-126)
  • _refine_faq_query (129-154)
  • _synthesize_faq_response (157-197)
  • _format_faq_response (200-213)
  • handle_faq_node (17-74)
🪛 Ruff (0.13.3)
backend/app/agents/devrel/prompts/faq_prompt.py

15-15: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


54-54: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


57-57: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

backend/app/agents/devrel/tools/faq_tool.py

39-41: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


39-41: Avoid specifying long messages outside the exception class

(TRY003)


89-96: Consider moving this statement to an else block

(TRY300)


98-98: Do not catch blind exception: Exception

(BLE001)


99-99: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


121-122: try-except-continue detected, consider logging the exception

(S112)


121-121: Do not catch blind exception: Exception

(BLE001)


146-146: Do not catch blind exception: Exception

(BLE001)


147-147: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


154-154: Comment contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF003)


162-162: Do not catch blind exception: Exception

(BLE001)


163-163: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


227-227: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

🔇 Additional comments (4)
backend/app/database/weaviate/scripts/populate_db.py (1)

305-305: Confirm all references to running populate_db.py are updated. No direct invocations or imports were found; manually verify CI/CD workflows, Makefiles, shell scripts, shebang calls, and documentation for any remaining calls.

backend/app/agents/devrel/tools/faq_tool.py (3)

11-18: LGTM! Clean configuration integration.

The organization settings and timeout configurations are properly initialized with sensible defaults.


198-201: LGTM! Proper timeout handling on LLM synthesis.

The use of asyncio.wait_for() with FAQ_LLM_TIMEOUT ensures the synthesis step doesn't hang indefinitely.


233-239: LGTM! Clean compatibility wrapper for migration.

The wrapper provides a smooth migration path for existing code while encouraging new code to use FAQTool.get_response() directly.

### Instructions:
1. **Answer the user's question directly and conversationally.**
2. **Focus on the most relevant and recent information.**
3. **Be informative yet concise (2–3 paragraphs max).**
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix ambiguous characters flagged by static analysis.

The prompts contain ambiguous Unicode characters that should be replaced with standard ASCII equivalents for consistency:

  • EN DASH (–) should be HYPHEN-MINUS (-)
  • RIGHT SINGLE QUOTATION MARK (') should be APOSTROPHE (')

Apply this diff to fix the ambiguous characters:

-3. **Be informative yet concise (2–3 paragraphs max).**
+3. **Be informative yet concise (2-3 paragraphs max).**
-3. Keep it concise (2–3 paragraphs).  
+3. Keep it concise (2-3 paragraphs).  
-6. Avoid mentioning "search results" — write as though you're providing expert knowledge.
+6. Avoid mentioning "search results" - write as though you're providing expert knowledge.

Also applies to: 54-54, 57-57

🧰 Tools
🪛 Ruff (0.13.3)

15-15: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)

🤖 Prompt for AI Agents
In backend/app/agents/devrel/prompts/faq_prompt.py around lines 15, 54 and 57,
replace ambiguous Unicode punctuation with ASCII equivalents: change EN DASH (–)
to HYPHEN-MINUS (-) and RIGHT SINGLE QUOTATION MARK (’) to APOSTROPHE (')
wherever they appear in the prompt strings; update the text so punctuation is
standard ASCII throughout and save the file.

FAQ_LLM_TIMEOUT = getattr(app_settings, "faq_llm_timeout", 15.0)

# Path to FAQ prompts
FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect directory name in path construction.

The path uses "prompt" but the actual directory is "prompts" (as evidenced by the import on line 36). This will cause file lookups to fail.

Apply this diff:

-FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
+FAQ_PROMPTS_PATH = Path(__file__).parent.parent / "prompts"

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/app/agents/devrel/tools/faq_tool.py around line 21, the Path is
constructed with the wrong directory name ("prompt"); change it to the correct
directory "prompts" so FAQ_PROMPTS_PATH = Path(__file__).parent / "prompts" /
"faq_prompt.py" (update the string only) to match the actual import and avoid
file lookup failures.

Comment on lines +34 to +41
# 2. Fallback: Load from Python module
try:
from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
return getattr(REFINEMENT_PROMPT, name)
except (ImportError, AttributeError):
raise FileNotFoundError(
f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical logic error in prompt loading fallback.

Line 37 attempts getattr(REFINEMENT_PROMPT, name) where REFINEMENT_PROMPT is a string constant, not a module. This will always fail with AttributeError.

Apply this diff to correctly load from the module:

     # 2. Fallback: Load from Python module
     try:
-        from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
-        return getattr(REFINEMENT_PROMPT, name)
+        from app.agents.devrel.prompts import faq_prompt
+        return getattr(faq_prompt, name.upper())
     except (ImportError, AttributeError):
         raise FileNotFoundError(
             f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
-        )
+        ) from None

Note: Also added from None to properly chain exceptions as suggested by static analysis.

📝 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
# 2. Fallback: Load from Python module
try:
from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
return getattr(REFINEMENT_PROMPT, name)
except (ImportError, AttributeError):
raise FileNotFoundError(
f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
)
# 2. Fallback: Load from Python module
try:
from app.agents.devrel.prompts import faq_prompt
return getattr(faq_prompt, name.upper())
except (ImportError, AttributeError):
raise FileNotFoundError(
f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
) from None
🧰 Tools
🪛 Ruff (0.13.3)

39-41: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


39-41: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In backend/app/agents/devrel/tools/faq_tool.py around lines 34 to 41, the
fallback attempts getattr(REFINEMENT_PROMPT, name) but REFINEMENT_PROMPT is a
string constant so getattr will always raise AttributeError; instead import the
module object (e.g., from app.agents.devrel.prompts import faq_prompt or use
importlib.import_module("app.agents.devrel.prompts.faq_prompt")) and call
getattr(faq_prompt, name) to retrieve the prompt, and when re-raising wrap the
original exception with "from None" removed and instead use "raise
FileNotFoundError(...) from e" so the exception chain is preserved.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5299f17 and 70f760f.

📒 Files selected for processing (4)
  • backend/app/agents/devrel/tools/faq_tool.py (1 hunks)
  • backend/app/database/weaviate/scripts/create_schemas.py (0 hunks)
  • backend/app/database/weaviate/scripts/populate_db.py (1 hunks)
  • tests/test_embedding_service.py (0 hunks)
💤 Files with no reviewable changes (2)
  • tests/test_embedding_service.py
  • backend/app/database/weaviate/scripts/create_schemas.py
✅ Files skipped from review due to trivial changes (1)
  • backend/app/database/weaviate/scripts/populate_db.py
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/agents/devrel/tools/faq_tool.py (2)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/agents/devrel/nodes/handlers/faq.py (6)
  • _generate_fallback_response (216-228)
  • _dynamic_faq_process (77-126)
  • _refine_faq_query (129-154)
  • _synthesize_faq_response (157-197)
  • _format_faq_response (200-213)
  • handle_faq_node (17-74)
🪛 Ruff (0.13.3)
backend/app/agents/devrel/tools/faq_tool.py

39-41: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


39-41: Avoid specifying long messages outside the exception class

(TRY003)


89-96: Consider moving this statement to an else block

(TRY300)


98-98: Do not catch blind exception: Exception

(BLE001)


99-99: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


121-122: try-except-continue detected, consider logging the exception

(S112)


121-121: Do not catch blind exception: Exception

(BLE001)


146-146: Do not catch blind exception: Exception

(BLE001)


147-147: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


154-154: Comment contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF003)


162-162: Do not catch blind exception: Exception

(BLE001)


163-163: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


227-227: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

Comment on lines +21 to +41
FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"

def load_prompt(name: str) -> str:
"""
Load a prompt from a .txt file or fallback to faq_prompt.py.
"""
prompt_path = FAQ_PROMPTS_PATH / f"{name}.txt"

# 1. Try to load from text file
if prompt_path.exists():
with open(prompt_path, "r", encoding="utf-8") as f:
return f.read()

# 2. Fallback: Load from Python module
try:
from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
return getattr(REFINEMENT_PROMPT, name)
except (ImportError, AttributeError):
raise FileNotFoundError(
f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix prompt loading so the dynamic FAQ pipeline can run

load_prompt currently can’t return anything: FAQ_PROMPTS_PATH points to the faq_prompt.py file, so prompt_path.exists() is always false, and the fallback tries getattr on the imported REFINEMENT_PROMPT string, immediately raising AttributeError. The first call to load_prompt("refinement_prompt") therefore throws a FileNotFoundError, causing _dynamic_faq_process to short‑circuit into the fallback response for every FAQ request. The new dynamic handler never actually searches or synthesizes results.

Please point the loader at the prompts directory and fetch the string constants from the module itself. One way:

-FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
+FAQ_PROMPTS_DIR = Path(__file__).parent.parent / "prompts"

 def load_prompt(name: str) -> str:
@@
-    prompt_path = FAQ_PROMPTS_PATH / f"{name}.txt"
+    prompt_path = FAQ_PROMPTS_DIR / f"{name}.txt"
@@
-        from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
-        return getattr(REFINEMENT_PROMPT, name)
-    except (ImportError, AttributeError):
+        from app.agents.devrel.prompts import faq_prompt as prompt_module
+        attr_name = name.upper()
+        if hasattr(prompt_module, attr_name):
+            return getattr(prompt_module, attr_name)
+    except ImportError as err:
+        raise FileNotFoundError(
+            f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
+        ) from err
+    raise FileNotFoundError(
+        f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
+    )

That restoration will let the refinement/synthesis prompts resolve and the dynamic FAQ flow execute as designed.

📝 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
FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
def load_prompt(name: str) -> str:
"""
Load a prompt from a .txt file or fallback to faq_prompt.py.
"""
prompt_path = FAQ_PROMPTS_PATH / f"{name}.txt"
# 1. Try to load from text file
if prompt_path.exists():
with open(prompt_path, "r", encoding="utf-8") as f:
return f.read()
# 2. Fallback: Load from Python module
try:
from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
return getattr(REFINEMENT_PROMPT, name)
except (ImportError, AttributeError):
raise FileNotFoundError(
f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
)
FAQ_PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
def load_prompt(name: str) -> str:
"""
Load a prompt from a .txt file or fallback to faq_prompt.py.
"""
prompt_path = FAQ_PROMPTS_DIR / f"{name}.txt"
# 1. Try to load from text file
if prompt_path.exists():
with open(prompt_path, "r", encoding="utf-8") as f:
return f.read()
# 2. Fallback: Load from Python module
- try:
- from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
- return getattr(REFINEMENT_PROMPT, name)
- except (ImportError, AttributeError):
- raise FileNotFoundError(
- f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
try:
from app.agents.devrel.prompts import faq_prompt as prompt_module
attr_name = name.upper()
if hasattr(prompt_module, attr_name):
return getattr(prompt_module, attr_name)
except ImportError as err:
raise FileNotFoundError(
f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
) from err
raise FileNotFoundError(
f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
)
🧰 Tools
🪛 Ruff (0.13.3)

39-41: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


39-41: Avoid specifying long messages outside the exception class

(TRY003)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (3)
backend/app/agents/devrel/prompts/faq_prompt.py (1)

1-62: Consider consolidating the two similar prompts.

Both REFINEMENT_PROMPT and SYNTHESIS_PROMPT contain very similar instructions (conversational tone, 2-3 paragraphs, don't mention sources, acknowledge gaps). The main difference is that SYNTHESIS_PROMPT includes explicit step headers.

Consider whether both are necessary or if a single, well-structured prompt could serve both purposes. This would reduce maintenance overhead and prevent instruction drift between the two.

backend/app/agents/devrel/tools/faq_tool.py (2)

99-99: Use logging.exception for better error diagnostics.

When logging errors in exception handlers, use logging.exception() instead of logging.error() to automatically include the stack trace.

Apply this diff:

         except Exception as e:
-            logger.error(f"[FAQ_TOOL] Failed to handle FAQ: {e}")
+            logger.exception(f"[FAQ_TOOL] Failed to handle FAQ: {e}")

Same issue on lines 147 and 163.


39-41: Chain exception properly with from clause.

When raising exceptions in an except block, use raise ... from err or raise ... from None to properly chain exceptions and preserve the original traceback.

Apply this diff:

     except (ImportError, AttributeError):
-        raise FileNotFoundError(
+        raise FileNotFoundError(
             f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
-        )
+        ) from None
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 295065c and 70f760f.

📒 Files selected for processing (5)
  • backend/app/agents/devrel/prompts/faq_prompt.py (1 hunks)
  • backend/app/agents/devrel/tools/faq_tool.py (1 hunks)
  • backend/app/database/weaviate/scripts/create_schemas.py (0 hunks)
  • backend/app/database/weaviate/scripts/populate_db.py (1 hunks)
  • tests/test_embedding_service.py (0 hunks)
💤 Files with no reviewable changes (2)
  • backend/app/database/weaviate/scripts/create_schemas.py
  • tests/test_embedding_service.py
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/agents/devrel/tools/faq_tool.py (3)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/services/embedding_service/service.py (1)
  • llm (55-68)
backend/app/agents/devrel/nodes/handlers/faq.py (6)
  • _generate_fallback_response (216-228)
  • _dynamic_faq_process (77-126)
  • _refine_faq_query (129-154)
  • _synthesize_faq_response (157-197)
  • _format_faq_response (200-213)
  • handle_faq_node (17-74)
🪛 Ruff (0.13.3)
backend/app/agents/devrel/tools/faq_tool.py

39-41: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


39-41: Avoid specifying long messages outside the exception class

(TRY003)


89-96: Consider moving this statement to an else block

(TRY300)


98-98: Do not catch blind exception: Exception

(BLE001)


99-99: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


121-122: try-except-continue detected, consider logging the exception

(S112)


121-121: Do not catch blind exception: Exception

(BLE001)


146-146: Do not catch blind exception: Exception

(BLE001)


147-147: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


154-154: Comment contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF003)


162-162: Do not catch blind exception: Exception

(BLE001)


163-163: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


227-227: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

backend/app/agents/devrel/prompts/faq_prompt.py

15-15: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


54-54: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


57-57: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

🔇 Additional comments (1)
backend/app/database/weaviate/scripts/populate_db.py (1)

305-305: Approve removal of __main__ guard: no internal references found
The deletion cleanly disables direct execution; no code, documentation, or CI workflows in the repo reference populate_db.py.

### Instructions:
1. **Answer the user's question directly and conversationally.**
2. **Focus on the most relevant and recent information.**
3. **Be informative yet concise (2–3 paragraphs max).**
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace ambiguous EN DASH with standard hyphen.

The string contains an EN DASH (–) which may cause text processing issues. Replace with a standard HYPHEN-MINUS (-).

Apply this diff:

-3. **Be informative yet concise (2–3 paragraphs max).**
+3. **Be informative yet concise (2-3 paragraphs max).**
📝 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
3. **Be informative yet concise (23 paragraphs max).**
3. **Be informative yet concise (2-3 paragraphs max).**
🧰 Tools
🪛 Ruff (0.13.3)

15-15: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)

🤖 Prompt for AI Agents
In backend/app/agents/devrel/prompts/faq_prompt.py around line 15, the text
"2–3" uses an EN DASH (U+2013); replace that character with a standard
hyphen-minus (U+002D) so it reads "2-3", updating the file encoding/commit as
needed.

### Step 3: Write the Final Answer
1. Respond directly and conversationally.
2. Highlight the most relevant and updated facts.
3. Keep it concise (2–3 paragraphs).
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace ambiguous EN DASH with standard hyphen.

The string contains an EN DASH (–) which may cause text processing issues. Replace with a standard HYPHEN-MINUS (-).

Apply this diff:

-3. Keep it concise (2–3 paragraphs).
+3. Keep it concise (2-3 paragraphs).
📝 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
3. Keep it concise (23 paragraphs).
3. Keep it concise (2-3 paragraphs).
🧰 Tools
🪛 Ruff (0.13.3)

54-54: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)

🤖 Prompt for AI Agents
In backend/app/agents/devrel/prompts/faq_prompt.py around line 54, replace the
ambiguous EN DASH (U+2013, “–”) used in the string "2–3 paragraphs" with a
standard hyphen-minus ("-") so it reads "2-3 paragraphs"; update the source text
accordingly (search and replace the character) and save the file with UTF-8
encoding to avoid text-processing issues.

3. Keep it concise (2–3 paragraphs).
4. Acknowledge any missing information if applicable.
5. Sound professional and informed about {org_name}.
6. Avoid mentioning “search results” — write as though you’re providing expert knowledge.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace ambiguous RIGHT SINGLE QUOTATION MARK with standard apostrophe.

The string contains a RIGHT SINGLE QUOTATION MARK (') which may cause text processing issues. Replace with a standard apostrophe (').

Apply this diff:

-6. Avoid mentioning "search results" — write as though you're providing expert knowledge.
+6. Avoid mentioning "search results" — write as though you're providing expert knowledge.
📝 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
6. Avoid mentioningsearch results” — write as though youre providing expert knowledge.
6. Avoid mentioningsearch results” — write as though you're providing expert knowledge.
🧰 Tools
🪛 Ruff (0.13.3)

57-57: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

🤖 Prompt for AI Agents
In backend/app/agents/devrel/prompts/faq_prompt.py around line 57, there is a
RIGHT SINGLE QUOTATION MARK character used in the string; replace that character
with the standard ASCII apostrophe (') so the string uses a normal apostrophe,
ensuring file encoding remains UTF-8 and updating any affected string literals
accordingly.

FAQ_LLM_TIMEOUT = getattr(app_settings, "faq_llm_timeout", 15.0)

# Path to FAQ prompts
FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: FAQ_PROMPTS_PATH points to a Python file, not a directory.

FAQ_PROMPTS_PATH is constructed as Path(__file__).parent / "prompt" / "faq_prompt.py", which results in a path ending in .py (a Python file). This breaks the path construction on line 27 where you append f"{name}.txt", creating invalid paths like .../prompt/faq_prompt.py/refinement_prompt.txt.

Fix by pointing to the prompts directory instead:

-FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
+FAQ_PROMPTS_PATH = Path(__file__).parent.parent / "prompts"
📝 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
FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
FAQ_PROMPTS_PATH = Path(__file__).parent.parent / "prompts"
🤖 Prompt for AI Agents
In backend/app/agents/devrel/tools/faq_tool.py around line 21, FAQ_PROMPTS_PATH
is set to the file path Path(__file__).parent / "prompt" / "faq_prompt.py" which
yields a .py file and breaks later when appending f"{name}.txt"; change it to
point to the prompts directory instead (e.g., Path(__file__).parent / "prompt")
so subsequent path joins produce valid files like
.../prompt/refinement_prompt.txt; update the constant accordingly and ensure any
usages that append filenames remain unchanged.

Comment on lines +36 to +37
from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
return getattr(REFINEMENT_PROMPT, name)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: getattr called on string constant instead of module.

Line 36 imports REFINEMENT_PROMPT (a string constant), then line 37 attempts getattr(REFINEMENT_PROMPT, name). Since REFINEMENT_PROMPT is a string, not a module, this will always raise AttributeError.

Fix by importing the module and accessing its attributes:

     # 2. Fallback: Load from Python module
     try:
-        from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
-        return getattr(REFINEMENT_PROMPT, name)
+        from app.agents.devrel.prompts import faq_prompt
+        # Convert snake_case name to UPPER_CASE constant name
+        constant_name = name.upper()
+        return getattr(faq_prompt, constant_name)
     except (ImportError, AttributeError):
📝 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
from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
return getattr(REFINEMENT_PROMPT, name)
# 2. Fallback: Load from Python module
try:
- from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
from app.agents.devrel.prompts import faq_prompt
# Convert snake_case name to UPPER_CASE constant name
constant_name = name.upper()
return getattr(faq_prompt, constant_name)
except (ImportError, AttributeError):
# handle missing module or constant
...
🤖 Prompt for AI Agents
In backend/app/agents/devrel/tools/faq_tool.py around lines 36-37, the code
imports REFINEMENT_PROMPT (a string constant) then calls
getattr(REFINEMENT_PROMPT, name), which will raise AttributeError; fix it by
importing the module object instead of the string constant (for example import
or from app.agents.devrel.prompts import faq_prompt or import
app.agents.devrel.prompts.faq_prompt as faq_prompt) and then call
getattr(faq_prompt, name); optionally keep a clear fallback or raise a
descriptive error if the attribute isn’t found.

Comment on lines 6 to 239
class FAQTool:
"""FAQ handling tool"""

# TODO: Add FAQ responses from a database to refer organization's FAQ and Repo's FAQ

def __init__(self):
self.faq_responses = {
"what is devr.ai": "Devr.AI is an AI-powered Developer Relations assistant that helps open-source communities by automating engagement, issue tracking, and providing intelligent support to developers.",
"how do i contribute": "You can contribute by visiting our GitHub repository, checking open issues, and submitting pull requests. We welcome all types of contributions including code, documentation, and bug reports.",
"what platforms does devr.ai support": "Devr.AI integrates with Discord, Slack, GitHub, and can be extended to other platforms. We use these integrations to provide seamless developer support across multiple channels.",
"who maintains devr.ai": "Devr.AI is maintained by an open-source community of developers passionate about improving developer relations and community engagement.",
"how do i report a bug": "You can report a bug by opening an issue on our GitHub repository. Please include detailed information about the bug, steps to reproduce it, and your environment.",
"how to get started": "To get started with Devr.AI: 1) Check our documentation, 2) Join our Discord community, 3) Explore the GitHub repository, 4) Try contributing to open issues.",
"what is langgraph": "LangGraph is a framework for building stateful, multi-actor applications with large language models. We use it to create intelligent agent workflows for our DevRel automation."
}

async def get_response(self, question: str) -> Optional[str]:
"""Get FAQ response for a question"""
question_lower = question.lower().strip()

# Direct match
if question_lower in self.faq_responses:
return self.faq_responses[question_lower]

# Fuzzy matching
for faq_key, response in self.faq_responses.items():
if self._is_similar_question(question_lower, faq_key):
return response

return None

def _is_similar_question(self, question: str, faq_key: str) -> bool:
"""Check if question is similar to FAQ key"""
# Simple keyword matching - in production, use better similarity
question_words = set(question.split())
faq_words = set(faq_key.split())

common_words = question_words.intersection(faq_words)
return len(common_words) >= 2 # At least 2 common words
"""
Dynamic FAQ handling tool integrating search and LLM synthesis.
Handles FAQ queries by refining search terms, fetching relevant data,
and synthesizing coherent responses with source citations.
"""

def __init__(self, search_tool: Any, llm: Any):
self.search_tool = search_tool
self.llm = llm

async def get_response(self, state: AgentState) -> Optional[Dict[str, Any]]:
"""
Fetch and synthesize a dynamic FAQ response using web search and LLM.
"""
logger.info(f"Handling FAQ for session {state.session_id}")

# Extract latest user message
latest_message = (
state.messages[-1].get("content", "")
if state.messages
else state.context.get("original_message", "")
)

if not latest_message:
logger.warning("[FAQ_TOOL] Empty user message — returning fallback")
return {
"task_result": {
"type": "faq",
"response": self._generate_fallback_response(latest_message),
"source": "dynamic_web_search",
},
"current_task": "faq_handled",
}

try:
# Build site filters from official handles
site_filters = self._build_site_filters()
logger.info(f"[FAQ_TOOL] Using site filters: {site_filters or '(none)'}")

# Process FAQ request
faq_response = await self._dynamic_faq_process(
latest_message, site_filters=site_filters
)

return {
"task_result": {
"type": "faq",
"response": faq_response,
"source": "dynamic_web_search",
},
"current_task": "faq_handled",
}

except Exception as e:
logger.error(f"[FAQ_TOOL] Failed to handle FAQ: {e}")
return {
"task_result": {
"type": "faq",
"response": f"Sorry, something went wrong while handling your FAQ request: {e}",
"source": "error",
},
"current_task": "faq_failed",
}


def _build_site_filters(self) -> str:
"""Construct search site filters from official handles."""
from urllib.parse import urlparse

domains = []
for u in OFFICIAL_HANDLES:
try:
parsed = urlparse(u)
domain = parsed.netloc or parsed.path
if domain:
domains.append(domain)
except Exception:
continue

return " OR ".join([f"site:{d}" for d in domains]) if domains else ""


async def _dynamic_faq_process(self, message: str, site_filters: str = "") -> str:
"""
Pipeline: refine → search → synthesize → format
"""
try:
# Step 1: Refine the query
logger.debug(f"[FAQ_TOOL] Refining query for org '{ORG_NAME}'")
refined_query = await self._refine_faq_query(message)

if site_filters:
refined_query = f"({refined_query}) AND ({site_filters})"
logger.debug(f"[FAQ_TOOL] Final query: {refined_query}")

# Step 2: Search
logger.info(f"[FAQ_TOOL] Searching for: {refined_query}")
try:
search_results = await asyncio.wait_for(
self.search_tool.search(refined_query), timeout=FAQ_SEARCH_TIMEOUT
)
except Exception as err:
logger.error(f"[FAQ_TOOL] Search failed: {err}")
return self._generate_fallback_response(message)

if not search_results:
logger.warning(f"[FAQ_TOOL] No results for: {refined_query}")
return self._generate_fallback_response(message)

# Step 3–4: Synthesize response
logger.info("[FAQ_TOOL] Synthesizing FAQ response")
synthesized = await self._synthesize_faq_response(message, search_results)

# Step 5: Format final response
logger.info("[FAQ_TOOL] Formatting response with citations")
return self._format_faq_response(synthesized, search_results)

except Exception as e:
logger.error(f"[FAQ_TOOL] Error in dynamic FAQ process: {e}")
return self._generate_fallback_response(message)


async def _refine_faq_query(self, message: str) -> str:
"""Refine user's question into an optimized search query."""
refinement_prompt = load_prompt("refinement_prompt").format(
org_name=ORG_NAME, message=message
)
response = await self.llm.ainvoke([HumanMessage(content=refinement_prompt)])
refined_query = response.content.strip()
logger.info(f"[FAQ_TOOL] Refined query: {refined_query}")
return refined_query


async def _synthesize_faq_response(
self, message: str, search_results: List[Dict[str, Any]]
) -> str:
"""Generate a synthesized answer from search results."""
# Build context (top 5 results)
results_context = ""
for i, result in enumerate(search_results[:5]):
title = result.get("title", "N/A")
content = result.get("content", "N/A")
if isinstance(content, str) and len(content) > 500:
content = content[:500] + "..."
url = result.get("url", "N/A")
results_context += f"\nResult {i+1}:\nTitle: {title}\nContent: {content}\nURL: {url}\n"

# Build synthesis prompt
synthesis_prompt = load_prompt("synthesis_prompt").format(
org_name=ORG_NAME, message=message, results_context=results_context
)

# LLM synthesis
response = await asyncio.wait_for(
self.llm.ainvoke([HumanMessage(content=synthesis_prompt)]),
timeout=FAQ_LLM_TIMEOUT,
)

synthesized_answer = response.content.strip()
logger.debug(f"[FAQ_TOOL] Synthesized response: {synthesized_answer[:100]}...")
return synthesized_answer


def _format_faq_response(
self, synthesized_answer: str, search_results: List[Dict[str, Any]]
) -> str:
"""Append top sources to the synthesized answer."""
formatted = synthesized_answer
if search_results:
formatted += "\n\n**📚 Sources:**"
for i, result in enumerate(search_results[:3]):
title = result.get("title", "Source")
url = result.get("url", "#")
formatted += f"\n{i+1}. [{title}]({url})"
return formatted

def _generate_fallback_response(self, message: str) -> str:
"""Return a friendly fallback message when no data is found."""
return (
f"I’d love to help you learn about {ORG_NAME}, but I couldn’t find current information for your question:\n"
f"“{message}\n\n"
"This might be because:\n"
"- The information isn’t publicly available yet\n"
"- The search terms need to be more specific\n"
"- There might be temporary connectivity issues\n\n"
f"Try asking a more specific question, or visit our official website and documentation for the most up-to-date info about {ORG_NAME}."
)

async def handle_faq_node(state: AgentState, search_tool: Any, llm: Any) -> dict:
"""
Legacy compatibility wrapper for backward support.
Use FAQTool.get_response() directly in new code.
"""
tool = FAQTool(search_tool, llm)
return await tool.get_response(state)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Significant code duplication with faq.py handler.

The FAQTool class in this file duplicates substantial logic from backend/app/agents/devrel/nodes/handlers/faq.py. Both implement:

  • _dynamic_faq_process with the same 5-step pipeline
  • _refine_faq_query with similar refinement logic
  • _synthesize_faq_response with identical synthesis approach
  • _format_faq_response for source citations
  • _generate_fallback_response for error handling

This violates DRY principles and creates maintenance burden. If business logic changes, both files must be updated.

Consider consolidating into a single implementation. Options:

  1. Keep FAQTool only: Remove the handler functions from faq.py and use FAQTool everywhere
  2. Keep handler only: Remove FAQTool and use the handler functions directly
  3. Composition: Have one delegate to the other

Based on the architecture, option 1 seems cleanest since FAQTool is a proper class with encapsulated state.

🧰 Tools
🪛 Ruff (0.13.3)

89-96: Consider moving this statement to an else block

(TRY300)


98-98: Do not catch blind exception: Exception

(BLE001)


99-99: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


121-122: try-except-continue detected, consider logging the exception

(S112)


121-121: Do not catch blind exception: Exception

(BLE001)


146-146: Do not catch blind exception: Exception

(BLE001)


147-147: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


154-154: Comment contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF003)


162-162: Do not catch blind exception: Exception

(BLE001)


163-163: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


224-224: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)


227-227: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

Comment on lines +169 to +170
refinement_prompt = load_prompt("refinement_prompt").format(
org_name=ORG_NAME, message=message
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify prompt name mapping between snake_case and UPPER_CASE.

The code calls load_prompt("refinement_prompt") and load_prompt("synthesis_prompt") (line 193), but the constants in faq_prompt.py are named REFINEMENT_PROMPT and SYNTHESIS_PROMPT. After fixing the load_prompt function, ensure the name mapping logic correctly converts between these formats.

Run this script to verify the prompt constants exist:


🏁 Script executed:

#!/bin/bash
# Check if the expected prompt constants exist in faq_prompt.py

rg -n "^(REFINEMENT_PROMPT|SYNTHESIS_PROMPT)\s*=" backend/app/agents/devrel/prompts/faq_prompt.py

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Locate load_prompt definition across the repository
rg -n "def load_prompt" -R .

Length of output: 404


🏁 Script executed:

#!/bin/bash
# Search for load_prompt definition in Python files
rg -n "def load_prompt" --type py

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Display implementation of load_prompt for mapping logic inspection
rg -n -C5 "def load_prompt" backend/app/agents/devrel/tools/faq_tool.py

Length of output: 359


🏁 Script executed:

#!/bin/bash
# Dump load_prompt implementation for mapping inspection
sed -n '23,120p' backend/app/agents/devrel/tools/faq_tool.py

Length of output: 3330


Fix fallback in load_prompt to correctly map snake_case to UPPER_CASE constants. The current code only imports REFINEMENT_PROMPT and does getattr(REFINEMENT_PROMPT, name) (which always fails). Instead import the faq_prompt module and use something like getattr(faq_prompt, name.upper()) so that "refinement_prompt"REFINEMENT_PROMPT and "synthesis_prompt"SYNTHESIS_PROMPT.

🤖 Prompt for AI Agents
In backend/app/agents/devrel/tools/faq_tool.py around lines 169-170, the call to
load_prompt currently tries to retrieve attributes from an imported
REFINEMENT_PROMPT object which always fails; change load_prompt to import the
faq_prompt module and lookup the constant by converting the snake_case name to
UPPER_CASE (e.g., use getattr(faq_prompt, name.upper())) so "refinement_prompt"
-> REFINEMENT_PROMPT and "synthesis_prompt" -> SYNTHESIS_PROMPT, and ensure the
module attribute is returned rather than attempting getattr on a single prompt
constant.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
backend/app/agents/devrel/tools/faq_tool.py (1)

22-42: Critical prompt loading issues remain unresolved.

The past review comments extensively documented two blocking defects that prevent the dynamic FAQ pipeline from functioning:

  1. Line 22: FAQ_PROMPTS_PATH points to the Python file faq_prompt.py rather than the prompts directory, so line 28's prompt_path = FAQ_PROMPTS_PATH / f"{name}.txt" constructs invalid paths like .../faq_prompt.py/refinement_prompt.txt.

  2. Lines 37-38: The fallback imports REFINEMENT_PROMPT (a string constant) then calls getattr(REFINEMENT_PROMPT, name), which always raises AttributeError because you cannot call getattr on a string.

Since load_prompt cannot return a valid prompt string, every call to _refine_faq_query (line 219) and _synthesize_faq_response (line 243) will throw FileNotFoundError, short-circuiting the dynamic FAQ flow into the fallback response for every request.

Apply this diff to fix both issues:

-FAQ_PROMPTS_PATH = Path(__file__).parent / "prompt" / "faq_prompt.py"
+FAQ_PROMPTS_DIR = Path(__file__).parent.parent / "prompts"

 def load_prompt(name: str) -> str:
     """
     Load a prompt from a .txt file or fallback to faq_prompt.py.
     """
-    prompt_path = FAQ_PROMPTS_PATH / f"{name}.txt"
+    prompt_path = FAQ_PROMPTS_DIR / f"{name}.txt"

     # 1. Try to load from text file
     if prompt_path.exists():
         with open(prompt_path, "r", encoding="utf-8") as f:
             return f.read()

     # 2. Fallback: Load from Python module
     try:
-        from app.agents.devrel.prompts.faq_prompt import REFINEMENT_PROMPT
-        return getattr(REFINEMENT_PROMPT, name)
-    except (ImportError, AttributeError):
+        from app.agents.devrel.prompts import faq_prompt as prompt_module
+        # Convert snake_case to UPPER_CASE constant name
+        constant_name = name.upper()
+        return getattr(prompt_module, constant_name)
+    except (ImportError, AttributeError) as err:
         raise FileNotFoundError(
             f"Prompt '{name}' not found in {prompt_path} or faq_prompt.py"
-        )
+        ) from err
🧹 Nitpick comments (5)
backend/app/agents/devrel/tools/faq_tool.py (5)

106-115: Improve exception handling and error exposure.

The broad Exception catch exposes the raw error message to the user (line 111), which may leak implementation details or sensitive information. Additionally, using logging.exception instead of logging.error would automatically include the stack trace.

Apply this diff to improve the exception handling:

     except Exception as e:
-        logger.error(f"[FAQ_TOOL] Failed to handle FAQ: {e}")
+        logger.exception("[FAQ_TOOL] Failed to handle FAQ")
         return {
             "task_result": {
                 "type": "faq",
-                "response": f"Sorry, something went wrong while handling your FAQ request: {e}",
+                "response": "I apologize, but I encountered an error while processing your FAQ request. Please try again or contact support if the issue persists.",
                 "source": "error",
             },
             "current_task": "faq_failed",
         }

118-132: Consider logging malformed official handles.

The silent try-except-continue (lines 124-130) makes it difficult to detect configuration issues if OFFICIAL_HANDLES contains malformed URLs. Adding debug-level logging would help administrators identify and fix invalid configurations.

Apply this diff to add debug logging:

         for u in OFFICIAL_HANDLES:
             try:
                 parsed = urlparse(u)
                 domain = parsed.netloc or parsed.path
                 if domain:
                     domains.append(domain)
-            except Exception:
-                continue
+            except Exception as e:
+                logger.debug(f"[FAQ_TOOL] Skipping malformed handle '{u}': {e}")
+                continue

159-161: Use logging.exception for automatic stack trace inclusion.

Replace logging.error with logging.exception to automatically capture the full exception context, which aids in debugging search failures.

Based on static analysis hints.

Apply this diff:

             except Exception as err:
-                logger.error(f"[FAQ_TOOL] Search failed: {err}")
+                logger.exception("[FAQ_TOOL] Search failed")
                 return self._generate_fallback_response(message)

175-177: Use logging.exception for automatic stack trace inclusion.

Replace logging.error with logging.exception to automatically capture the full exception context, which aids in debugging pipeline failures.

Based on static analysis hints.

Apply this diff:

         except Exception as e:
-            logger.error(f"[FAQ_TOOL] Error in dynamic FAQ process: {e}")
+            logger.exception("[FAQ_TOOL] Error in dynamic FAQ process")
             return self._generate_fallback_response(message)

188-212: Consider logging malformed URLs during filtering.

The silent try-except-continue blocks (lines 194-195, 211-212) make it difficult to detect when search results contain malformed URLs. Adding debug-level logging would help identify data quality issues.

Apply this diff to add debug logging:

         for handle in OFFICIAL_HANDLES:
             try:
                 parsed = urlparse(handle)
                 domain = parsed.netloc or parsed.path
                 if domain:
                     allowed_domains.add(domain.lower())
-            except Exception:
-                continue
+            except Exception as e:
+                logger.debug(f"[FAQ_TOOL] Skipping malformed handle in filtering '{handle}': {e}")
+                continue
         
         if not allowed_domains:
             return results
         
         # Filter results
         filtered = []
         for result in results:
             url = result.get("url", "")
             try:
                 parsed = urlparse(url)
                 result_domain = parsed.netloc.lower()
                 
                 # Check if domain matches any allowed domain
                 if any(
                     result_domain == allowed or result_domain.endswith(f".{allowed}")
                     for allowed in allowed_domains
                 ):
                     filtered.append(result)
-            except Exception:
-                continue
+            except Exception as e:
+                logger.debug(f"[FAQ_TOOL] Skipping result with malformed URL '{url}': {e}")
+                continue
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70f760f and ef640d2.

📒 Files selected for processing (2)
  • backend/app/agents/devrel/agent.py (2 hunks)
  • backend/app/agents/devrel/tools/faq_tool.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/agents/devrel/agent.py
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/agents/devrel/tools/faq_tool.py (3)
backend/app/agents/state.py (1)
  • AgentState (18-73)
backend/app/agents/devrel/tools/search_tool/tavilly.py (1)
  • TavilySearchTool (10-52)
backend/app/agents/devrel/nodes/handlers/faq.py (6)
  • _generate_fallback_response (216-228)
  • _dynamic_faq_process (77-126)
  • _refine_faq_query (129-154)
  • _synthesize_faq_response (157-197)
  • _format_faq_response (200-213)
  • handle_faq_node (17-74)
🪛 Ruff (0.14.0)
backend/app/agents/devrel/tools/faq_tool.py

40-42: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


40-42: Avoid specifying long messages outside the exception class

(TRY003)


97-104: Consider moving this statement to an else block

(TRY300)


106-106: Do not catch blind exception: Exception

(BLE001)


107-107: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


129-130: try-except-continue detected, consider logging the exception

(S112)


129-129: Do not catch blind exception: Exception

(BLE001)


159-159: Do not catch blind exception: Exception

(BLE001)


160-160: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


175-175: Do not catch blind exception: Exception

(BLE001)


176-176: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


194-195: try-except-continue detected, consider logging the exception

(S112)


194-194: Do not catch blind exception: Exception

(BLE001)


211-212: try-except-continue detected, consider logging the exception

(S112)


211-211: Do not catch blind exception: Exception

(BLE001)

Comment on lines +209 to +210
if any(allowed in result_domain or result_domain in allowed for allowed in allowed_domains):
filtered.append(result)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Domain matching logic may allow unintended subdomain matches.

The bidirectional substring check allowed in result_domain or result_domain in allowed could match unintended domains. For example, if allowed is "example.com", then "example.com.attacker.com" would match because "example.com" in "example.com.attacker.com" is true.

Apply this diff to use exact domain or proper subdomain matching:

                 # Check if domain matches any allowed domain
-                if any(allowed in result_domain or result_domain in allowed for allowed in allowed_domains):
+                # Match exact domain or proper subdomain (e.g., "blog.example.com" matches "example.com")
+                if any(
+                    result_domain == allowed or result_domain.endswith(f".{allowed}")
+                    for allowed in allowed_domains
+                ):
                     filtered.append(result)
📝 Committable suggestion

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

Suggested change
if any(allowed in result_domain or result_domain in allowed for allowed in allowed_domains):
filtered.append(result)
# Check if domain matches any allowed domain
# Match exact domain or proper subdomain (e.g., "blog.example.com" matches "example.com")
if any(
result_domain == allowed or result_domain.endswith(f".{allowed}")
for allowed in allowed_domains
):
filtered.append(result)
🤖 Prompt for AI Agents
In backend/app/agents/devrel/tools/faq_tool.py around lines 209-210, the current
bidirectional substring check can produce false positives (e.g., "example.com"
matching "example.com.attacker.com"); replace it with a proper domain check:
normalize both domains to lowercase and strip any trailing dots, then accept
when result_domain == allowed or result_domain.endswith("." + allowed) to allow
only exact matches or legitimate subdomains; update the condition accordingly
and ensure allowed_domains is iterated as before.

Comment on lines +283 to +289
async def handle_faq_node(state: AgentState, llm: Any) -> dict:
"""
Legacy compatibility wrapper for backward support.
Use FAQTool.get_response() directly in new code.
"""
tool = FAQTool(llm)
return await tool.get_response(state)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify signature compatibility with handler in faq.py.

The wrapper signature handle_faq_node(state: AgentState, llm: Any) differs from the handler in backend/app/agents/devrel/nodes/handlers/faq.py which expects handle_faq_node(state: AgentState, search_tool: Any, llm: Any). While the docstring indicates this is for "backward support," the signature mismatch could cause TypeError if callers pass search_tool.

Run this script to check all call sites:


🏁 Script executed:

#!/bin/bash
# Find all calls to handle_faq_node to verify parameter usage
rg -n -B2 -A2 'handle_faq_node\s*\(' --type=py

Length of output: 1813


Align handle_faq_node signature with callers
Update the wrapper in backend/app/agents/devrel/tools/faq_tool.py to:

async def handle_faq_node(state: AgentState, search_tool: Any, llm: Any) -> dict:
    """
    Legacy compatibility wrapper for backward support.
    Use FAQTool.get_response() directly in new code.
    """
    tool = FAQTool(llm)
    return await tool.get_response(state)

This matches calls in tool_wrappers.py and nodes/handlers/faq.py.

🤖 Prompt for AI Agents
In backend/app/agents/devrel/tools/faq_tool.py around lines 283 to 289, the
async wrapper handle_faq_node has the wrong signature (missing search_tool)
compared to its callers; update the function signature to accept (state:
AgentState, search_tool: Any, llm: Any) and leave the body unchanged so it
constructs FAQTool with llm and calls get_response(state); ensure imports/types
still satisfy the new parameter and run tests to confirm no other call sites
need changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
backend/app/agents/devrel/nodes/react_supervisor.py (2)

35-37: Single source of truth for iteration cap; drop router check.

Stop condition is enforced in react_supervisor_node (>=). Remove duplicate router check to avoid drift and off‑by‑one confusion.

@@
-    # Safety check for max iterations
-    if iteration_count >= MAX_ITERATIONS:
+    # Safety check for max iterations
+    if iteration_count >= MAX_ITERATIONS:
         logger.warning(f"Max iterations ({MAX_ITERATIONS}) reached for session {state.session_id}")
         return _create_completion_response(state, "Maximum iterations reached")
@@
-        iteration_count = state.context.get("iteration_count", 0)
-        if iteration_count > MAX_ITERATIONS:
-            logger.warning(f"Max iterations reached for session {state.session_id}")
-            return "complete"
+        iteration_count = state.context.get("iteration_count", 0)  # Node enforces cap

Also applies to: 183-187


251-271: History cap: confirm requirement vs summarization.

You cap to MAX_CONVERSATION_HISTORY=5. Product feedback suggested reverting reliance on caps due to working summarization and large context windows. Confirm desired behavior before merging.

Would you like to remove/relax the cap, or make it configurable via settings with a sensible default?

🧹 Nitpick comments (8)
backend/app/agents/devrel/tool_wrappers.py (2)

18-24: Type hints for clarity on tool and LLM dependencies.

Annotate parameters to document the contract and improve IDE help.

-async def faq_handler_tool_node(state: AgentState, search_tool, llm) -> Dict[str, Any]:
+async def faq_handler_tool_node(state: AgentState, search_tool: Any, llm: Any) -> Dict[str, Any]:

50-54: Reuse shared latest-message helper for consistency.

Use the existing util to avoid drift with other nodes.

+from .nodes.generate_response import _get_latest_message as get_latest_message
@@
-    latest_message = (
-        state.messages[-1].get("content", "")
-        if state.messages else
-        state.context.get("original_message", "")
-    )
+    latest_message = get_latest_message(state)
backend/app/agents/devrel/nodes/react_supervisor.py (6)

39-48: Bound tool_results included in the prompt to prevent prompt bloat.

Cap entries before serialization; keep most recent N.

-    try:
+    try:
+        # Limit to last 5 tool results to keep prompt small
+        if tool_results and isinstance(tool_results, list):
+            tool_results = tool_results[-5:]
         if tool_results:
             tool_results_str = json.dumps(tool_results, indent=2, default=str)
         else:
             tool_results_str = "No previous tool results"

115-117: Prefer logger.exception (and avoid f-strings) for tracebacks.

Cleaner and satisfies RUF010; includes stacktrace automatically.

-    except Exception as e:
-        logger.error(f"Error in react_supervisor_node: {e}", exc_info=True)
-        return _create_error_response(state, f"Supervisor error: {str(e)}")
+    except Exception:
+        logger.exception("Error in react_supervisor_node")
+        return _create_error_response(state, "Supervisor error")

155-160: Parser: accept REASONING: alias.

LLMs often emit REASONING:. Handle both.

-            elif line.startswith("REASON:"):
+            elif line.startswith(("REASON:", "REASONING:")):
                 if current_section and content_buffer:
                     decision[current_section] = " ".join(content_buffer)
-                current_section = "reasoning"
-                content_buffer = [line.replace("REASON:", "").strip()]
+                current_section = "reasoning"
+                content_buffer = [line.split(":", 1)[1].strip()]

226-235: Minor: UTC timestamps and list concat nit in add_tool_result.

Use timezone-aware ISO timestamps and avoid + concat (RUF005).

-from datetime import datetime
+from datetime import datetime, timezone
@@
-        tool_entry = {
+        tool_entry = {
             "tool": tool_name,
             "result": result,
             "iteration": state.context.get("iteration_count", 0),
-            "timestamp": datetime.now().isoformat()
+            "timestamp": datetime.now(timezone.utc).isoformat()
         }
@@
-        tools_used = getattr(state, 'tools_used', []) + [tool_name]
+        tools_used = [*getattr(state, 'tools_used', []), tool_name]

45-45: Broad except Exception used in many places.

Narrow where feasible or document rationale. For irreducible cases, prefer logger.exception for automatic tracebacks.

Also applies to: 116-116, 171-172, 195-197, 237-239, 247-249, 269-271, 287-289


98-103: Minor logging style.

Use parameterized logging to avoid eager f-string formatting.

-logger.debug(f"Current iteration: {iteration_count}")
-logger.debug(f"Latest message length: {len(latest_message or '')}")
-logger.info(f"ReAct Supervisor decision: {decision['action']}")
-logger.debug(f"Supervisor thinking: {decision.get('thinking', '')[:100]}...")
-logger.debug(f"Supervisor reasoning: {decision.get('reasoning', '')[:100]}...")
+logger.debug("Current iteration: %d", iteration_count)
+logger.debug("Latest message length: %d", len(latest_message or ""))
+logger.info("ReAct Supervisor decision: %s", decision["action"])
+logger.debug("Supervisor thinking: %s", decision.get("thinking", "")[:100] + "...")
+logger.debug("Supervisor reasoning: %s", decision.get("reasoning", "")[:100] + "...")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef640d2 and 345a68a.

📒 Files selected for processing (3)
  • backend/app/agents/devrel/nodes/react_supervisor.py (2 hunks)
  • backend/app/agents/devrel/tool_wrappers.py (3 hunks)
  • backend/app/core/config/settings.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/core/config/settings.py
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/agents/devrel/tool_wrappers.py (2)
backend/app/agents/devrel/tools/faq_tool.py (1)
  • handle_faq_node (283-289)
backend/app/agents/devrel/nodes/handlers/faq.py (1)
  • handle_faq_node (17-74)
backend/app/agents/devrel/nodes/react_supervisor.py (2)
backend/app/agents/state.py (1)
  • AgentState (18-77)
backend/app/agents/devrel/nodes/generate_response.py (1)
  • _get_latest_message (102-106)
🪛 Ruff (0.14.1)
backend/app/agents/devrel/nodes/react_supervisor.py

45-45: Do not catch blind exception: Exception

(BLE001)


117-117: Use explicit conversion flag

Replace with conversion flag

(RUF010)


193-193: Consider moving this statement to an else block

(TRY300)


226-226: Consider [*getattr(state, 'tools_used', []), tool_name] instead of concatenation

Replace with [*getattr(state, 'tools_used', []), tool_name]

(RUF005)


228-235: Consider moving this statement to an else block

(TRY300)


285-285: Consider moving this statement to an else block

(TRY300)

from app.agents.state import AgentState
from .nodes.react_supervisor import add_tool_result
from .nodes.handlers.faq import handle_faq_node
from app.agents.devrel.tools.faq_tool import handle_faq_node
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix import/signature mismatch for FAQ handler (runtime TypeError).

You import handle_faq_node from tools.faq_tool (signature: (state, llm)) but call it with (state, search_tool, llm). Import the handler that accepts search_tool, or adapt the call.

Apply one of the following:

Option A — import the dynamic handler:

-from app.agents.devrel.tools.faq_tool import handle_faq_node
+from .nodes.handlers.faq import handle_faq_node

Option B — keep tools.faq_tool and adapt call:

-    handler_result = await handle_faq_node(state, search_tool, llm)
+    handler_result = await handle_faq_node(state, llm)  # search_tool unused by tools.faq_tool wrapper

Prefer A to actually pass search_tool to the dynamic flow. Based on learnings

Run to confirm definitions/callers:

Also applies to: 22-23


🏁 Script executed:

#!/bin/bash
rg -nP "def\s+handle_faq_node\s*\(" -C2 backend/app/agents/devrel
rg -nP "\bhandle_faq_node\s*\(" -C2 backend/app/agents/devrel

Length of output: 2604


Fix import/signature mismatch for FAQ handler (runtime TypeError).

You import handle_faq_node from tools.faq_tool (signature: (state, llm)) but call it with (state, search_tool, llm). Import the dynamic handler instead:

-from app.agents.devrel.tools.faq_tool import handle_faq_node
+from app.agents.devrel.nodes.handlers.faq import handle_faq_node

This passes search_tool to the actual implementation that requires it.

📝 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
from app.agents.devrel.tools.faq_tool import handle_faq_node
from app.agents.devrel.nodes.handlers.faq import handle_faq_node
🤖 Prompt for AI Agents
In backend/app/agents/devrel/tool_wrappers.py around line 5, the file imports
handle_faq_node (signature (state, llm)) but the code calls it with (state,
search_tool, llm), causing a TypeError; replace the current import with the
dynamic FAQ handler/factory exported by app.agents.devrel.tools.faq_tool that
accepts the search_tool parameter (or returns a handler that closes over
search_tool), and update the import statement accordingly so the invoked
function receives (state, search_tool, llm).

@smokeyScraper
Copy link
Contributor

smokeyScraper commented Oct 25, 2025

@PrithvijitBose
image

Could you please look up and try the codebase once? I'm facing an error.

PS: please try restructuring a few of your commits, a lot of them are overlapping

Copy link
Contributor

@smokeyScraper smokeyScraper left a comment

Choose a reason for hiding this comment

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

Please try having a minimalist implementation throughout.

return ""


def _get_conversation_history(state: AgentState, max_messages: int = MAX_CONVERSATION_HISTORY) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

This function was already defined in the codebase.

return {"context": state.context if hasattr(state, 'context') else {}}


def _get_latest_message(state: AgentState) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

This function was already defined in the codebase.

Comment on lines +31 to +40
if not latest_message:
logger.warning("[FAQ_HANDLER] Empty latest user message; returning fallback")
return {
"task_result": {
"type": "faq",
"response": _generate_fallback_response(latest_message, ORG_NAME),
"source": "dynamic_web_search"
},
"current_task": "faq_handled"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure if this section is really required...

Copy link
Author

Choose a reason for hiding this comment

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

faq.py is not required now..i have merged its functions with faq_tool.py..Will remove the commit

return refined_query


async def _synthesize_faq_response(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this really required? We already have a response synthesizer prompt

return formatted_response


def _generate_fallback_response(message: str, org_name: str) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

Not required. In case of anomaly where there won't be any results the bot itself manages that scenario and responds well.

### Your Response:
"""
SYNTHESIS_PROMPT = """
Copy link
Contributor

Choose a reason for hiding this comment

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

This is duplicated in your PR.

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.

FEATURE REQUEST: Implement FAQ Handler with Web Search for Organizational Queries

2 participants