-
-
Notifications
You must be signed in to change notification settings - Fork 138
[feat]: langsmith tracing integration #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
smokeyScraper
wants to merge
20
commits into
AOSSIE-Org:main
from
smokeyScraper:langsmith_integration
Closed
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
b32dd28
poetry updates
smokeyScraper 74db2c2
scoping codebase
smokeyScraper 2e5c7f1
add global configuration
smokeyScraper 6488370
add base agent and classification router
smokeyScraper 7e8544b
add tools
smokeyScraper ea6c235
add orchestration logic
smokeyScraper 529f22c
update config dependency
smokeyScraper 613d168
implement devrel agent and coordinator
smokeyScraper aaa9c8b
implement discord bot
smokeyScraper 23fe6bf
main.py and poetry updates
smokeyScraper 67858f8
poetry update
smokeyScraper 3d37fae
fix: replace sync call and use lazy logging
smokeyScraper cb3499a
refactor: discordBot to discord_bot
smokeyScraper 89051f3
refactor: coderabbit refactorings
smokeyScraper c64cf95
refactor: coderabbit refactoring
smokeyScraper 2182e84
refactor: modularizing codebase
smokeyScraper a5e6897
[chore]: update .env
smokeyScraper 8c12787
[chore]: poetry updates
smokeyScraper d985b25
[chore]: update config.py to support langsmith
smokeyScraper 2354f5d
[feat]: add langsmith tracing
smokeyScraper File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,22 @@ | ||
| SUPABASE_URL= | ||
| SUPABASE_SERVICE_ROLE_KEY= | ||
| # API Configuration | ||
| # PORT=8000 | ||
| # CORS_ORIGINS=http://localhost:3000 | ||
|
|
||
| CORS_ORIGINS=http://localhost:3000 | ||
| GITHUB_TOKEN= | ||
| # SUPABASE_URL= | ||
| # SUPABASE_SERVICE_ROLE_KEY= | ||
|
|
||
| DISCORD_BOT_TOKEN= | ||
| # ENABLE_DISCORD_BOT=true | ||
|
|
||
| # EMBEDDING_MODEL=BAAI/bge-small-en-v1.5 | ||
| # EMBEDDING_MAX_BATCH_SIZE=32 | ||
| # EMBEDDING_DEVICE=cpu | ||
|
|
||
| GEMINI_API_KEY= | ||
| TAVILY_API_KEY= | ||
|
|
||
| # Langsmith | ||
| LANGSMITH_TRACING= | ||
| LANGSMITH_ENDPOINT= | ||
| LANGSMITH_API_KEY= | ||
| LANGSMITH_PROJECT= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from .devrel.agent import DevRelAgent | ||
| from .shared.base_agent import BaseAgent, AgentState | ||
| from .shared.classification_router import ClassificationRouter | ||
|
|
||
| __all__ = [ | ||
| "DevRelAgent", | ||
| "BaseAgent", | ||
| "AgentState", | ||
| "ClassificationRouter" | ||
| ] |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import logging | ||
| from typing import Dict, Any | ||
| from functools import partial | ||
| from langgraph.graph import StateGraph, END | ||
| from langchain_google_genai import ChatGoogleGenerativeAI | ||
| from ..shared.base_agent import BaseAgent, AgentState | ||
| from ..shared.classification_router import MessageCategory | ||
| from .tools.search_tool import TavilySearchTool | ||
| from .tools.faq_tool import FAQTool | ||
| from app.core.config import settings | ||
| from .nodes.classify_intent_node import classify_intent_node | ||
| from .nodes.gather_context_node import gather_context_node | ||
| from .nodes.handle_faq_node import handle_faq_node | ||
| from .nodes.handle_web_search_node import handle_web_search_node | ||
| from .nodes.handle_technical_support_node import handle_technical_support_node | ||
| from .nodes.handle_onboarding_node import handle_onboarding_node | ||
| from .nodes.generate_response_node import generate_response_node | ||
| from langsmith import traceable | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| class DevRelAgent(BaseAgent): | ||
| """DevRel LangGraph Agent for community support and engagement""" | ||
|
|
||
| def __init__(self, config: Dict[str, Any] = None): | ||
| self.config = config or {} | ||
| self.llm = ChatGoogleGenerativeAI( | ||
| model=settings.devrel_agent_model, | ||
| temperature=0.3, | ||
| google_api_key=settings.gemini_api_key | ||
| ) | ||
| self.search_tool = TavilySearchTool() | ||
| self.faq_tool = FAQTool() | ||
| super().__init__("DevRelAgent", self.config) | ||
|
|
||
| def _build_graph(self): | ||
| """Build the DevRel agent workflow graph""" | ||
| workflow = StateGraph(AgentState) | ||
|
|
||
| # Add nodes | ||
| workflow.add_node("classify_intent", partial(classify_intent_node, llm=self.llm)) | ||
| workflow.add_node("gather_context", gather_context_node) | ||
| workflow.add_node("handle_faq", partial(handle_faq_node, faq_tool=self.faq_tool)) | ||
| workflow.add_node("handle_web_search", partial( | ||
| handle_web_search_node, search_tool=self.search_tool, llm=self.llm)) | ||
| workflow.add_node("handle_technical_support", handle_technical_support_node) | ||
| workflow.add_node("handle_onboarding", handle_onboarding_node) | ||
| workflow.add_node("generate_response", partial(generate_response_node, llm=self.llm)) | ||
|
|
||
| # Add edges | ||
| workflow.add_edge("classify_intent", "gather_context") | ||
| workflow.add_conditional_edges( | ||
| "gather_context", | ||
| self._route_to_handler, | ||
| { | ||
| MessageCategory.FAQ: "handle_faq", | ||
| MessageCategory.WEB_SEARCH: "handle_web_search", | ||
| MessageCategory.ONBOARDING: "handle_onboarding", | ||
| MessageCategory.TECHNICAL_SUPPORT: "handle_technical_support", | ||
| MessageCategory.COMMUNITY_ENGAGEMENT: "handle_technical_support", | ||
| MessageCategory.DOCUMENTATION: "handle_technical_support", | ||
| MessageCategory.BUG_REPORT: "handle_technical_support", | ||
| MessageCategory.FEATURE_REQUEST: "handle_technical_support", | ||
| "default": "handle_technical_support" | ||
| } | ||
| ) | ||
|
|
||
| # All handlers lead to response generation | ||
| for node in ["handle_faq", "handle_web_search", "handle_technical_support", "handle_onboarding"]: | ||
| workflow.add_edge(node, "generate_response") | ||
|
|
||
| workflow.add_edge("generate_response", END) | ||
|
|
||
| # Set entry point | ||
| workflow.set_entry_point("classify_intent") | ||
|
|
||
| self.graph = workflow.compile() | ||
|
|
||
| @traceable(name="devrel_agent_execution", run_type="chain") | ||
| async def run(self, initial_state: AgentState) -> AgentState: | ||
| """Execute the agent workflow""" | ||
| return await super().run(initial_state) | ||
|
|
||
| def _route_to_handler(self, state: AgentState) -> str: | ||
| """Route to the appropriate handler based on intent""" | ||
| intent = state.context.get("intent", MessageCategory.TECHNICAL_SUPPORT) | ||
| logger.info(f"Routing based on intent: {intent} for session {state.session_id}") | ||
|
|
||
| # Mapping from MessageCategory enum to string keys used in add_conditional_edges | ||
| route_map = { | ||
| MessageCategory.FAQ: "faq", | ||
| MessageCategory.WEB_SEARCH: "web_search", | ||
| MessageCategory.ONBOARDING: "onboarding", | ||
| MessageCategory.TECHNICAL_SUPPORT: "technical_support", | ||
| MessageCategory.COMMUNITY_ENGAGEMENT: "technical_support", | ||
| MessageCategory.DOCUMENTATION: "technical_support", | ||
| MessageCategory.BUG_REPORT: "technical_support", | ||
| MessageCategory.FEATURE_REQUEST: "technical_support" | ||
| } | ||
|
|
||
| return route_map.get(intent, "technical_support") | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import logging | ||
| from app.agents.shared.state import AgentState | ||
| from app.agents.shared.classification_router import ClassificationRouter | ||
| from langchain_google_genai import ChatGoogleGenerativeAI | ||
| from langsmith import traceable | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| @traceable(name="classify_intent_node", run_type="tool") | ||
| async def classify_intent_node(state: AgentState, llm: ChatGoogleGenerativeAI) -> AgentState: | ||
| """Classify the user's intent and needs""" | ||
| logger.info(f"Classifying intent for session {state.session_id}") | ||
|
|
||
| # Get the latest message | ||
| latest_message = "" | ||
| if state.messages: | ||
| latest_message = state.messages[-1].get("content", "") | ||
| elif state.context.get("original_message"): | ||
| latest_message = state.context["original_message"] | ||
|
|
||
| # Use classification router | ||
| router = ClassificationRouter(llm) | ||
|
|
||
| classification = await router.classify_message(latest_message, state.context) | ||
|
|
||
| # Update state with classification | ||
| state.context.update({ | ||
| "classification": classification, | ||
| "intent": classification["category"], | ||
| "priority": classification["priority"] | ||
| }) | ||
|
|
||
| state.current_task = "intent_classified" | ||
| return state |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import logging | ||
| from app.agents.shared.state import AgentState | ||
| from langsmith import traceable | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| @traceable(name="gather_context_node", run_type="tool") | ||
| async def gather_context_node(state: AgentState) -> AgentState: | ||
| """Gather additional context for the user and their request""" | ||
| logger.info(f"Gathering context for session {state.session_id}") | ||
|
|
||
| # TODO: Add context gathering from databases | ||
| # Currently, context is simple | ||
| # In production, query databases for user history, etc. | ||
| context_data = { | ||
| "user_profile": {"user_id": state.user_id, "platform": state.platform}, | ||
| "conversation_context": len(state.messages), | ||
| "session_info": {"session_id": state.session_id} | ||
| } | ||
|
|
||
| state.context.update(context_data) | ||
| state.current_task = "context_gathered" | ||
| return state |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import logging | ||
| from typing import Dict, Any | ||
| from app.agents.shared.state import AgentState | ||
| from langchain_core.messages import HumanMessage | ||
| from ..prompts.base_prompt import GENERAL_LLM_RESPONSE_PROMPT | ||
| from langsmith import traceable | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| @traceable(name="create_search_response", run_type="tool") | ||
| async def _create_search_response(task_result: Dict[str, Any]) -> str: | ||
| """Create a response string from search results.""" | ||
| query = task_result.get("query") | ||
| results = task_result.get("results", []) | ||
| if not results: | ||
| return f"I couldn't find any information for '{query}'. You might want to try rephrasing your search." | ||
|
|
||
| response_parts = [f"Here's what I found for '{query}':"] | ||
| for i, result in enumerate(results[:3]): | ||
| title = result.get('title', 'N/A') | ||
| snippet = result.get('snippet', 'N/A') | ||
| url = result.get('url', '#') | ||
| result_line = f"{i+1}. {title}: {snippet}" | ||
| response_parts.append(result_line) | ||
| response_parts.append(f" (Source: {url})") | ||
| response_parts.append("You can ask me to search again with a different query if these aren't helpful.") | ||
| return "\n".join(response_parts) | ||
|
|
||
| @traceable(name="create_llm_response", run_type="tool") | ||
| async def _create_llm_response(state: AgentState, task_result: Dict[str, Any], llm) -> str: | ||
| """Generate a response using the LLM based on the current state and task result.""" | ||
| logger.info(f"Creating LLM response 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"] | ||
|
|
||
| conversation_history_str = "\n".join([ | ||
| f"{msg.get('type', 'unknown')}: {msg.get('content', '')}" | ||
| for msg in state.conversation_history[-5:] | ||
| ]) | ||
| current_context_str = str(state.context) | ||
| task_type_str = str(task_result.get("type", "N/A")) | ||
| task_details_str = str(task_result) | ||
|
|
||
| try: | ||
| prompt = GENERAL_LLM_RESPONSE_PROMPT.format( | ||
| latest_message=latest_message, | ||
| conversation_history=conversation_history_str, | ||
| current_context=current_context_str, | ||
| task_type=task_type_str, | ||
| task_details=task_details_str | ||
| ) | ||
| except KeyError as e: | ||
| logger.error(f"Missing key in GENERAL_LLM_RESPONSE_PROMPT: {e}") | ||
| return "Error: Response template formatting error." | ||
|
|
||
| response = await llm.ainvoke([HumanMessage(content=prompt)]) | ||
| return response.content.strip() | ||
|
|
||
| @traceable(name="generate_response_node", run_type="tool") | ||
| async def generate_response_node(state: AgentState, llm) -> AgentState: | ||
| """Generate final response to user""" | ||
| logger.info(f"Generating response for session {state.session_id}") | ||
| task_result = state.task_result or {} | ||
|
|
||
| if task_result.get("type") == "faq": | ||
| state.final_response = task_result.get("response", "I don't have a specific answer for that question.") | ||
| elif task_result.get("type") == "web_search": | ||
| response = await _create_search_response(task_result) | ||
| state.final_response = response | ||
| else: | ||
| # Pass the llm instance to _create_llm_response | ||
| response = await _create_llm_response(state, task_result, llm) | ||
| state.final_response = response | ||
|
|
||
| state.current_task = "response_generated" | ||
| return state |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import logging | ||
| from app.agents.shared.state import AgentState | ||
| from langsmith import traceable | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| @traceable(name="handle_faq_node", run_type="tool") | ||
| async def handle_faq_node(state: AgentState, faq_tool) -> AgentState: | ||
| """Handle FAQ requests""" | ||
| logger.info(f"Handling 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"] | ||
|
|
||
| # faq_tool will be passed from the agent, similar to llm for classify_intent | ||
| faq_response = await faq_tool.get_response(latest_message) | ||
|
|
||
| state.task_result = { | ||
| "type": "faq", | ||
| "response": faq_response, | ||
| "source": "faq_database" | ||
| } | ||
|
|
||
| state.current_task = "faq_handled" | ||
| return state |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import logging | ||
| from app.agents.shared.state import AgentState | ||
| from langsmith import traceable | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| @traceable(name="handle_onboarding_node", run_type="tool") | ||
| async def handle_onboarding_node(state: AgentState) -> AgentState: | ||
| """Handle onboarding requests""" | ||
| logger.info(f"Handling onboarding for session {state.session_id}") | ||
|
|
||
| state.task_result = { | ||
| "type": "onboarding", | ||
| "action": "welcome_and_guide", | ||
| "next_steps": ["setup_environment", "first_contribution", "join_community"] | ||
| } | ||
|
|
||
| state.current_task = "onboarding_handled" | ||
| return state |
19 changes: 19 additions & 0 deletions
19
backend/app/agents/devrel/nodes/handle_technical_support_node.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import logging | ||
| from app.agents.shared.state import AgentState | ||
| from langsmith import traceable | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| @traceable(name="handle_technical_support_node", run_type="tool") | ||
| async def handle_technical_support_node(state: AgentState) -> AgentState: | ||
| """Handle technical support requests""" | ||
| logger.info(f"Handling technical support for session {state.session_id}") | ||
|
|
||
| state.task_result = { | ||
| "type": "technical_support", | ||
| "action": "provide_guidance", | ||
| "requires_human_review": False | ||
| } | ||
|
|
||
| state.current_task = "technical_support_handled" | ||
| return state |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import logging | ||
| from app.agents.shared.state import AgentState | ||
| from langchain_core.messages import HumanMessage | ||
| from ..prompts.search_prompt import EXTRACT_SEARCH_QUERY_PROMPT | ||
| from langsmith import traceable | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| @traceable(name="extract_search_query", run_type="tool") | ||
| async def _extract_search_query(message: str, llm) -> str: | ||
| """Extract a concise search query from the user's message.""" | ||
| logger.info(f"Extracting search query from: {message[:100]}") | ||
| try: | ||
| prompt = EXTRACT_SEARCH_QUERY_PROMPT.format(message=message) | ||
| except KeyError as e: | ||
| logger.error(f"Missing key in EXTRACT_SEARCH_QUERY_PROMPT: {e}") | ||
| return message # Fallback | ||
| response = await llm.ainvoke([HumanMessage(content=prompt)]) | ||
| search_query = response.content.strip() | ||
| logger.info(f"Extracted search query: {search_query}") | ||
| return search_query | ||
|
|
||
| @traceable(name="handle_web_search_node", run_type="tool") | ||
| async def handle_web_search_node(state: AgentState, search_tool, llm) -> AgentState: | ||
| """Handle web search requests""" | ||
| logger.info(f"Handling web search 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"] | ||
|
|
||
| search_query = await _extract_search_query(latest_message, llm) | ||
| search_results = await search_tool.search(search_query) | ||
|
|
||
| state.task_result = { | ||
| "type": "web_search", | ||
| "query": search_query, | ||
| "results": search_results, | ||
| "source": "tavily_search" | ||
| } | ||
| state.tools_used.append("tavily_search") | ||
| state.current_task = "web_search_handled" | ||
| return state |
Empty file.
Empty file.
Empty file.
Empty file.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There appears to be a mismatch in how conditional edges are defined. The
_route_to_handlermethod (lines 84-101) returns string keys (e.g., "faq", "web_search") based on theintent. However, the dictionary provided toadd_conditional_edges(lines 55-65) usesMessageCategoryenum objects (e.g.,MessageCategory.FAQ) as keys for most routes, instead of the strings returned by_route_to_handler.According to LangGraph documentation, the keys in the mapping dictionary for
add_conditional_edgesmust be the string outputs from the conditional function (_route_to_handlerin this case).This mismatch will likely cause the routing to not work as expected for enum-keyed routes, potentially always falling back to a default or failing if the returned string key isn't found.
Could you update the keys in the
add_conditional_edgesdictionary to be the string values that_route_to_handlerreturns (e.g., use "faq" instead ofMessageCategory.FAQ)? The "default" key should also match the default string returned by_route_to_handler(which is "technical_support").Style Guide References