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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/agents/devrel/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self, config: Dict[str, Any] = None):
google_api_key=settings.gemini_api_key
)
self.search_tool = TavilySearchTool()
self.faq_tool = FAQTool()
self.faq_tool = FAQTool(search_tool=self.search_tool)
self.github_toolkit = GitHubToolkit()
self.checkpointer = InMemorySaver()
super().__init__("DevRelAgent", self.config)
Expand Down
92 changes: 79 additions & 13 deletions backend/app/agents/devrel/nodes/handlers/faq.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import logging
from typing import Dict, Any
from app.agents.state import AgentState

logger = logging.getLogger(__name__)

async def handle_faq_node(state: AgentState, faq_tool) -> dict:
"""Handle FAQ requests"""
async def handle_faq_node(state: AgentState, faq_tool) -> Dict[str, Any]:
"""Handle FAQ requests with enhanced organizational query support"""
logger.info(f"Handling FAQ for session {state.session_id}")

latest_message = ""
Expand All @@ -13,14 +14,79 @@ async def handle_faq_node(state: AgentState, faq_tool) -> dict:
elif state.context.get("original_message"):
latest_message = state.context["original_message"]

# faq_tool will be passed from the agent, similar to llm for classify_intent
faq_response = await faq_tool.get_response(latest_message)

return {
"task_result": {
"type": "faq",
"response": faq_response,
"source": "faq_database"
},
"current_task": "faq_handled"
}
try:
# Get enhanced response with metadata
enhanced_response = await faq_tool.get_enhanced_response(latest_message)

# Extract response details
response_text = enhanced_response.get("response")
response_type = enhanced_response.get("type", "unknown")
sources = enhanced_response.get("sources", [])
search_queries = enhanced_response.get("search_queries", [])

# Log the type of response for monitoring
logger.info(f"FAQ response type: {response_type} for session {state.session_id}")

if response_text:
# Successfully got a response
return {
"task_result": {
"type": response_type,
"response": response_text,
"source": enhanced_response.get("source", "enhanced_faq"),
"sources": sources,
"search_queries": search_queries,
"has_sources": len(sources) > 0
},
"current_task": "faq_handled",
"tools_used": ["enhanced_faq_tool"]
}
else:
# No response found
logger.info(f"No FAQ response found for: {latest_message[:100]}")
return {
"task_result": {
"type": "no_match",
"response": None,
"source": "enhanced_faq",
"sources": [],
"search_queries": [],
"has_sources": False
},
"current_task": "faq_no_match"
}

except Exception as e:
logger.error(f"Error in enhanced FAQ handler: {str(e)}")

# Fallback to simple response
try:
simple_response = await faq_tool.get_response(latest_message)
if simple_response:
return {
"task_result": {
"type": "fallback_faq",
"response": simple_response,
"source": "faq_fallback",
"sources": [],
"search_queries": [],
"has_sources": False
},
"current_task": "faq_handled"
}
except Exception as fallback_error:
logger.error(f"Fallback FAQ also failed: {str(fallback_error)}")

# Return error state
return {
"task_result": {
"type": "error",
"response": None,
"source": "error",
"error": str(e),
"sources": [],
"search_queries": [],
"has_sources": False
},
"current_task": "faq_error"
}
121 changes: 121 additions & 0 deletions backend/app/agents/devrel/nodes/handlers/organizational_faq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import logging
from typing import Dict, Any, List
from app.agents.state import AgentState
from langchain_core.messages import HumanMessage
from app.agents.devrel.prompts.organizational_faq_prompt import ORGANIZATIONAL_SYNTHESIS_PROMPT

logger = logging.getLogger(__name__)

async def handle_organizational_faq_node(
state: AgentState,
enhanced_faq_tool: Any,
llm: Any
) -> Dict[str, Any]:
"""Handle organizational FAQ requests with web search and LLM synthesis"""
logger.info(f"Handling organizational FAQ for session {state.session_id}")

latest_message = ""
if state.messages:
latest_message = state.messages[-1].get("content", "")
elif state.context.get("original_message"):
latest_message = state.context["original_message"]

# Get response from enhanced FAQ tool
faq_response = await enhanced_faq_tool.get_response(latest_message)

# If it's an organizational query, enhance with LLM synthesis
if faq_response.get("type") == "organizational_faq":
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think there is any need for segregation of FAQ types as long as they get answered, right? I mean the node itself should be able to determine out on it's own whether this search result is enough for answering on not?

search_results = faq_response.get("sources", [])

if search_results:
# Format search results for LLM
formatted_results = _format_search_results_for_llm(search_results)

# Use LLM to synthesize a better response
synthesis_prompt = ORGANIZATIONAL_SYNTHESIS_PROMPT.format(
question=latest_message,
search_results=formatted_results
)

try:
llm_response = await llm.ainvoke([HumanMessage(content=synthesis_prompt)])
synthesized_answer = llm_response.content.strip()

# Update the response with the synthesized answer
faq_response["response"] = synthesized_answer
faq_response["synthesis_method"] = "llm_enhanced"

logger.info(
f"Enhanced organizational response with LLM synthesis "
f"for session {state.session_id}"
)
except (ValueError, AttributeError, TypeError) as e:
logger.error(f"Error in LLM synthesis: {str(e)}")
# Keep the original response if LLM synthesis fails
faq_response["synthesis_method"] = "basic"
except Exception as e:
logger.error(f"Unexpected error in LLM synthesis: {str(e)}")
faq_response["synthesis_method"] = "basic"

return {
"task_result": {
"type": "organizational_faq",
"response": faq_response.get("response"),
"source": faq_response.get("source", "enhanced_faq"),
"sources": faq_response.get("sources", []),
"search_queries": faq_response.get("search_queries", []),
"synthesis_method": faq_response.get("synthesis_method", "none"),
"query_type": faq_response.get("type", "unknown")
},
"current_task": "organizational_faq_handled"
}

def _format_search_results_for_llm(search_results: List[Dict[str, Any]]) -> str:
"""Format search results for LLM synthesis"""
if not search_results:
return "No search results available."

formatted_parts = []
for i, result in enumerate(search_results, 1):
title = result.get('title', 'No title')
url = result.get('url', 'No URL')
content = result.get('content', 'No content available')

formatted_part = f"""
Result {i}:
Title: {title}
URL: {url}
Content: {content[:500]}{"..." if len(content) > 500 else ""}
"""
formatted_parts.append(formatted_part)

return "\n".join(formatted_parts)
Comment on lines +73 to +92
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 input validation for search results.

The function should validate the structure of search results to prevent runtime errors.

 def _format_search_results_for_llm(search_results: List[Dict[str, Any]]) -> str:
     """Format search results for LLM synthesis"""
     if not search_results:
         return "No search results available."
+    
+    if not isinstance(search_results, list):
+        logger.warning("Search results is not a list, returning empty message")
+        return "No search results available."

     formatted_parts = []
     for i, result in enumerate(search_results, 1):
+        if not isinstance(result, dict):
+            logger.warning(f"Invalid search result format at index {i-1}")
+            continue
+            
         title = result.get('title', 'No title')
         url = result.get('url', 'No URL')
         content = result.get('content', 'No content available')
📝 Committable suggestion

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

Suggested change
def _format_search_results_for_llm(search_results: List[Dict[str, Any]]) -> str:
"""Format search results for LLM synthesis"""
if not search_results:
return "No search results available."
formatted_parts = []
for i, result in enumerate(search_results, 1):
title = result.get('title', 'No title')
url = result.get('url', 'No URL')
content = result.get('content', 'No content available')
formatted_part = f"""
Result {i}:
Title: {title}
URL: {url}
Content: {content[:500]}{"..." if len(content) > 500 else ""}
"""
formatted_parts.append(formatted_part)
return "\n".join(formatted_parts)
def _format_search_results_for_llm(search_results: List[Dict[str, Any]]) -> str:
"""Format search results for LLM synthesis"""
if not search_results:
return "No search results available."
if not isinstance(search_results, list):
logger.warning("Search results is not a list, returning empty message")
return "No search results available."
formatted_parts = []
for i, result in enumerate(search_results, 1):
if not isinstance(result, dict):
logger.warning(f"Invalid search result format at index {i-1}")
continue
title = result.get('title', 'No title')
url = result.get('url', 'No URL')
content = result.get('content', 'No content available')
formatted_part = f"""
Result {i}:
Title: {title}
URL: {url}
Content: {content[:500]}{"..." if len(content) > 500 else ""}
"""
formatted_parts.append(formatted_part)
return "\n".join(formatted_parts)
🤖 Prompt for AI Agents
In backend/app/agents/devrel/nodes/handlers/organizational_faq.py around lines
85 to 104, the function _format_search_results_for_llm lacks input validation
for the search_results parameter. Add validation to ensure search_results is a
list of dictionaries and each dictionary contains the expected keys ('title',
'url', 'content') with appropriate types. If validation fails, handle it
gracefully by either returning a default message or raising a clear error to
prevent runtime exceptions.


def create_organizational_response(task_result: Dict[str, Any]) -> 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 very much sure on this too. This seems very much like aligning a separate function for a separate response, but I think the LLM can directly align the response.

"""Create a user-friendly response string from organizational FAQ results"""
response = task_result.get("response", "")
sources = task_result.get("sources", [])
query_type = task_result.get("query_type", "")

if not response:
return ("I couldn't find specific information about that. Please try rephrasing your "
"question or check our official documentation.")

# Start with the main response
response_parts = [response]

# Add sources if available
if sources:
response_parts.append("\n\n**Sources:**")
for i, source in enumerate(sources[:3], 1):
title = source.get('title', 'Source')
url = source.get('url', '')
response_parts.append(f"{i}. [{title}]({url})")

# Add helpful footer for organizational queries
if query_type == "organizational_faq":
response_parts.append(
"\n\nFor more information, you can also visit our official website or GitHub repository."
)

return "\n".join(response_parts)
101 changes: 101 additions & 0 deletions backend/app/agents/devrel/prompts/organizational_faq_prompt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Prompts for organizational FAQ handling and synthesis
Copy link
Contributor

Choose a reason for hiding this comment

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

These much of prompts are very much overhead. As of now, one query takes at max 4-5 calls for the model (with proper thinking, tool usage, results alignment, and whole workflow), and including all these would be very heavy on the API usage part. The interaction can go back and forth between two nodes (cycle) and does not need to be aligned in a flow-based way.


# Prompt for detecting organizational queries
ORGANIZATIONAL_QUERY_DETECTION_PROMPT = """You are an AI assistant that helps classify user questions.
Determine if the following question is asking about organizational information (about the company,
projects, mission, goals, etc.) or technical support.

User Question: "{question}"

Classification Guidelines:
- ORGANIZATIONAL: Questions about the company, its mission, projects, team, goals, platforms,
general information about what the organization does
- TECHNICAL: Questions about how to use the product, troubleshooting, implementation details,
contribution guidelines, specific feature requests

Examples of ORGANIZATIONAL questions:
Copy link
Contributor

Choose a reason for hiding this comment

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

These are very much aligned with DevR. We want a generalized handler as this product will be used by an organization that will have multiple repos and not just DevR.

- "What is Devr.AI?"
- "What projects does this organization work on?"
- "What are the main goals of Devr.AI?"
- "What platforms does Devr.AI support?"
- "Tell me about this organization"

Examples of TECHNICAL questions:
- "How do I contribute to the project?"
- "How do I report a bug?"
- "What is LangGraph?"
- "How do I get started with development?"

Respond with only: ORGANIZATIONAL or TECHNICAL"""

# Prompt for generating targeted search queries for organizational information
ORGANIZATIONAL_SEARCH_QUERY_GENERATION_PROMPT = """You are an AI assistant that helps generate effective
search queries. Based on the user's organizational question, generate 2-3 specific search queries that
would find relevant information about Devr.AI.

User Question: "{question}"

Guidelines for search queries:
1. Include "Devr.AI" in each query
Copy link
Contributor

Choose a reason for hiding this comment

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

any clarifications on this? Why DevR need to be in each query?

2. Focus on official sources (website, GitHub, documentation)
3. Be specific to the type of information requested
4. Avoid overly broad or generic terms

Generate search queries that would find information about:
- Official website content
- GitHub repositories and documentation
- Project descriptions and goals
- Platform integrations and capabilities

Format your response as a JSON list of strings:
["query1", "query2", "query3"]"""

# Enhanced synthesis prompt for organizational responses
ORGANIZATIONAL_SYNTHESIS_PROMPT = """You are the official AI representative for Devr.AI.
Your task is to provide a comprehensive, accurate, and helpful answer to the user's question about
our organization based on the search results provided.

User Question: "{question}"

Search Results:
{search_results}

Instructions:
1. **Accuracy First**: Only use information directly found in the search results
2. **Comprehensive Coverage**: Address all aspects of the user's question if information is available
3. **Professional Tone**: Maintain a friendly but professional tone appropriate for developer relations
4. **Structured Response**: Organize information logically with clear sections if needed
5. **Source Attribution**: If specific claims are made, they should be traceable to the search results
6. **Acknowledge Limitations**: If search results don't contain enough information, be honest about it
7. **Call to Action**: When appropriate, guide users to official resources for more information

Response Format:
- Start with a direct answer to the main question
- Provide supporting details from search results
- Include relevant examples or specifics when available
- End with helpful next steps or resources if appropriate

Avoid:
- Making up information not present in search results
- Being overly promotional or sales-like
- Providing outdated information
- Generic or vague responses

Response:"""

# Prompt for fallback responses when search results are insufficient
ORGANIZATIONAL_FALLBACK_PROMPT = """You are the AI representative for Devr.AI. The user asked an
organizational question, but the search results didn't provide sufficient information to answer
comprehensively.

User Question: "{question}"

Provide a helpful fallback response that:
1. Acknowledges their question
2. Provides any basic information you know about Devr.AI (AI-powered DevRel assistant)
3. Directs them to official sources for complete information
4. Maintains a helpful and professional tone

Keep the response concise but useful, and avoid making specific claims without evidence.

Response:"""
25 changes: 17 additions & 8 deletions backend/app/agents/devrel/prompts/react_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,28 @@
{tool_results}

AVAILABLE ACTIONS:
1. web_search - Search the web for external information
2. faq_handler - Answer common questions from knowledge base
3. onboarding - Welcome new users and guide exploration
1. web_search - Search the web for external information not related to our organization
2. faq_handler - Answer questions using knowledge base AND web search for organizational queries
3. onboarding - Welcome new users and guide exploration
4. github_toolkit - Handle GitHub operations (issues, PRs, repos, docs)
5. complete - Task is finished, format final response

ENHANCED FAQ HANDLER CAPABILITIES:
The faq_handler now has advanced capabilities for organizational queries:
- Detects questions about Devr.AI, our projects, mission, goals, and platforms
- Automatically searches the web for current organizational information
- Synthesizes responses from official sources (website, GitHub, docs)
- Provides static answers for technical FAQ questions
- Returns structured responses with source citations

THINK: Analyze the user's request and current context. What needs to be done?

Then choose ONE action:
- If you need external information or recent updates → web_search
- If this is a common question with a known answer → faq_handler
- If this is a new user needing guidance → onboarding
- If this involves GitHub repositories, issues, PRs, or code → github_toolkit
Choose ONE action based on these guidelines:
- If asking about Devr.AI organization, projects, mission, goals, or "what is..." → faq_handler
- If asking technical questions like "how to contribute", "report bugs" → faq_handler
- If you need external information unrelated to our organization → web_search
- If this is a new user needing general guidance → onboarding
- If this involves GitHub repositories, issues, PRs, or code operations → github_toolkit
- If you have enough information to fully answer → complete

Respond in this exact format:
Expand Down
Loading