Skip to content

Commit 0fa57ec

Browse files
Merge pull request #137 from Dharya4242/feat/onboarding-focused-workflow
feat: route onboarding flow through MCP-backed GitHub toolkit
2 parents 1b68d85 + 92bfe30 commit 0fa57ec

File tree

13 files changed

+810
-31
lines changed

13 files changed

+810
-31
lines changed

backend/app/agents/devrel/github/tools/general_github_help.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ async def handle_general_github_help(query: str, llm) -> Dict[str, Any]:
4747
"query": query,
4848
"error": str(e),
4949
"message": "Failed to provide general GitHub help"
50-
}
50+
}

backend/app/agents/devrel/nodes/gather_context.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
22
from datetime import datetime
3-
from typing import Dict, Any
3+
from typing import Any, Dict
4+
45
from app.agents.state import AgentState
6+
from app.services.auth.management import get_or_create_user_by_discord
57
from app.database.supabase.services import ensure_user_exists, get_conversation_context
68

79
logger = logging.getLogger(__name__)
@@ -28,8 +30,29 @@ async def gather_context_node(state: AgentState) -> Dict[str, Any]:
2830
"timestamp": datetime.now().isoformat()
2931
}
3032

33+
profile_data: Dict[str, Any] = dict(state.user_profile or {})
34+
35+
if state.platform.lower() == "discord":
36+
author = state.context.get("author", {}) or {}
37+
discord_id = author.get("id") or state.user_id
38+
display_name = author.get("display_name") or author.get("global_name") or author.get("name") or author.get("username")
39+
discord_username = author.get("username") or author.get("name") or author.get("display_name")
40+
avatar_url = author.get("avatar") or author.get("avatar_url")
41+
42+
if discord_id:
43+
try:
44+
user = await get_or_create_user_by_discord(
45+
discord_id=str(discord_id),
46+
display_name=str(display_name or discord_username or discord_id),
47+
discord_username=str(discord_username or display_name or discord_id),
48+
avatar_url=avatar_url,
49+
)
50+
profile_data = user.model_dump()
51+
except Exception as exc: # pragma: no cover - graceful degradation
52+
logger.warning("Failed to refresh Discord user profile for %s: %s", discord_id, exc)
53+
3154
context_data = {
32-
"user_profile": {"user_id": state.user_id, "platform": state.platform},
55+
"user_profile": profile_data or {"user_id": state.user_id, "platform": state.platform},
3356
"conversation_context": len(state.messages) + 1, # +1 for the new message
3457
"session_info": {"session_id": state.session_id},
3558
"user_uuid": user_uuid
@@ -63,9 +86,14 @@ async def gather_context_node(state: AgentState) -> Dict[str, Any]:
6386

6487
updated_context = {**state.context, **context_data}
6588

66-
return {
89+
result: Dict[str, Any] = {
6790
"messages": [new_message],
6891
"context": updated_context,
6992
"current_task": "context_gathered",
70-
"last_interaction_time": datetime.now()
93+
"last_interaction_time": datetime.now(),
7194
}
95+
96+
if profile_data:
97+
result["user_profile"] = profile_data
98+
99+
return result
Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,67 @@
11
import logging
2+
from typing import Any, Dict
3+
4+
from app.agents.devrel.onboarding.workflow import (
5+
OnboardingStage,
6+
run_onboarding_flow,
7+
)
28
from app.agents.state import AgentState
39

410
logger = logging.getLogger(__name__)
511

6-
async def handle_onboarding_node(state: AgentState) -> AgentState:
7-
"""Handle onboarding requests"""
12+
def _latest_text(state: AgentState) -> str:
13+
if state.messages:
14+
return state.messages[-1].get("content", "").lower()
15+
return state.context.get("original_message", "").lower()
16+
17+
async def handle_onboarding_node(state: AgentState) -> Dict[str, Any]:
18+
"""Handle onboarding requests via the multi-stage onboarding workflow."""
819
logger.info(f"Handling onboarding for session {state.session_id}")
920

21+
text = _latest_text(state)
22+
23+
# Try to derive verification state if present in context/user_profile
24+
is_verified = False
25+
github_username = None
26+
try:
27+
profile = state.user_profile or {}
28+
ctx_profile = state.context.get("user_profile", {})
29+
is_verified = bool(profile.get("is_verified") or ctx_profile.get("is_verified"))
30+
github_username = profile.get("github_username") or ctx_profile.get("github_username")
31+
except Exception:
32+
pass
33+
34+
flow_result, updated_state = run_onboarding_flow(
35+
state=state,
36+
latest_message=text,
37+
is_verified=is_verified,
38+
github_username=github_username,
39+
)
40+
41+
task_result: Dict[str, Any] = {
42+
"type": "onboarding",
43+
"stage": flow_result.stage.value,
44+
"status": flow_result.status,
45+
"welcome_message": flow_result.welcome_message,
46+
"final_message": flow_result.final_message,
47+
"actions": flow_result.actions,
48+
"is_verified": flow_result.is_verified,
49+
"capability_sections": flow_result.capability_sections,
50+
}
51+
52+
if flow_result.route_hint:
53+
task_result["route_hint"] = flow_result.route_hint
54+
if flow_result.handoff:
55+
task_result["handoff"] = flow_result.handoff
56+
if flow_result.next_tool:
57+
task_result["next_tool"] = flow_result.next_tool
58+
if flow_result.metadata:
59+
task_result["metadata"] = flow_result.metadata
60+
61+
current_task = f"onboarding_{flow_result.stage.value}"
62+
1063
return {
11-
"task_result": {
12-
"type": "onboarding",
13-
"action": "welcome_and_guide",
14-
"next_steps": ["setup_environment", "first_contribution", "join_community"]
15-
},
16-
"current_task": "onboarding_handled"
64+
"task_result": task_result,
65+
"current_task": current_task,
66+
"onboarding_state": updated_state,
1767
}

backend/app/agents/devrel/nodes/react_supervisor.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,41 @@ async def react_supervisor_node(state: AgentState, llm) -> Dict[str, Any]:
1717
tool_results = state.context.get("tool_results", [])
1818
iteration_count = state.context.get("iteration_count", 0)
1919

20+
forced_action = state.context.get("force_next_tool")
21+
if forced_action:
22+
logger.info(
23+
"Supervisor auto-routing to %s for session %s", forced_action, state.session_id
24+
)
25+
decision = {
26+
"action": forced_action,
27+
"reasoning": "Auto-routed by onboarding workflow",
28+
"thinking": "",
29+
}
30+
updated_context = {**state.context}
31+
updated_context.pop("force_next_tool", None)
32+
updated_context["supervisor_decision"] = decision
33+
updated_context["iteration_count"] = iteration_count + 1
34+
return {
35+
"context": updated_context,
36+
"current_task": f"supervisor_forced_{forced_action}",
37+
}
38+
39+
if state.context.get("force_complete"):
40+
logger.info("Supervisor forcing completion for session %s", state.session_id)
41+
decision = {
42+
"action": "complete",
43+
"reasoning": "Auto-complete after onboarding hand-off",
44+
"thinking": "",
45+
}
46+
updated_context = {**state.context}
47+
updated_context.pop("force_complete", None)
48+
updated_context["supervisor_decision"] = decision
49+
updated_context["iteration_count"] = iteration_count + 1
50+
return {
51+
"context": updated_context,
52+
"current_task": "supervisor_forced_complete",
53+
}
54+
2055
prompt = REACT_SUPERVISOR_PROMPT.format(
2156
latest_message=latest_message,
2257
platform=state.platform,

backend/app/agents/devrel/onboarding/__init__.py

Whitespace-only changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Shared onboarding messaging primitives used across channels."""
2+
from __future__ import annotations
3+
4+
from typing import Dict, List, Optional
5+
6+
# Structured capability sections pulled from design docs so UI and LLM share copy
7+
CAPABILITY_SECTIONS: List[Dict[str, List[str]]] = [
8+
{
9+
"title": "Explore Our Projects",
10+
"examples": [
11+
"Show me our most active repositories.",
12+
"Give me an overview of the 'Devr.AI-backend' repo.",
13+
],
14+
},
15+
{
16+
"title": "Find Ways to Contribute",
17+
"examples": [
18+
"Are there any 'good first issues' available?",
19+
"Find issues with the 'bug' label.",
20+
],
21+
},
22+
{
23+
"title": "Answer Project Questions",
24+
"examples": [
25+
"How do I set up the local development environment?",
26+
"What's the process for submitting a pull request?",
27+
],
28+
},
29+
]
30+
31+
CAPABILITIES_INTRO_TEXT = (
32+
"You're all set! As the Devr.AI assistant, my main purpose is to help you "
33+
"navigate and contribute to our projects. Here's a look at what you can ask "
34+
"me to do."
35+
)
36+
CAPABILITIES_OUTRO_TEXT = "Feel free to ask me anything related to the project. What's on your mind?"
37+
38+
39+
def render_capabilities_text() -> str:
40+
"""Render the capabilities message as plain text for chat responses."""
41+
lines: List[str] = [CAPABILITIES_INTRO_TEXT, ""]
42+
for section in CAPABILITY_SECTIONS:
43+
lines.append(f"{section['title']}:")
44+
for example in section["examples"]:
45+
lines.append(f"- \"{example}\"")
46+
lines.append("")
47+
lines.append(CAPABILITIES_OUTRO_TEXT)
48+
return "\n".join(lines).strip()
49+
50+
51+
def build_new_user_welcome() -> str:
52+
"""Welcome copy when verification is still pending."""
53+
return (
54+
"👋 Welcome to the Devr.AI community! I'm here to help you get started on your contributor journey.\n\n"
55+
"To give you the best recommendations for repositories and issues, I first need to link your GitHub account. "
56+
"This one-time step helps me align tasks with your profile.\n\n"
57+
"Here's how to verify:\n"
58+
"- Run `/verify_github` to start verification right away.\n"
59+
"- Use `/verification_status` to see if you're already linked.\n"
60+
"- Use `/help` anytime to explore everything I can assist with.\n\n"
61+
"Would you like to verify your GitHub account now or skip this step for now? You can always do it later."
62+
)
63+
64+
65+
def build_verified_welcome(github_username: Optional[str] = None) -> str:
66+
"""Welcome copy for returning verified contributors."""
67+
greeting = "👋 Welcome back to the Devr.AI community!"
68+
if github_username:
69+
greeting += f" I see `{github_username}` is already linked, which is great."
70+
else:
71+
greeting += " I see your GitHub account is already verified, which is great."
72+
return (
73+
f"{greeting}\n\nHow can I help you get started today? Ask me for repository overviews, issues to work on, or project guidance whenever you're ready."
74+
)
75+
76+
77+
def build_encourage_verification_message(reminder_count: int = 0) -> str:
78+
"""Reminder copy for users who haven't verified yet but want to explore."""
79+
reminder_prefix = "Quick reminder" if reminder_count else "No worries"
80+
return (
81+
f"{reminder_prefix} — linking your GitHub unlocks personalized suggestions. "
82+
"Run `/verify_github` when you're ready, and `/verification_status` to check progress.\n\n"
83+
"While you set that up, I can still show you what's happening across the organization. "
84+
"Ask for repository highlights, open issues, or anything else you're curious about."
85+
)
86+
87+
88+
def build_verified_capabilities_intro(github_username: Optional[str] = None) -> str:
89+
"""Intro text shown right before the capability menu for verified users."""
90+
if github_username:
91+
return (
92+
f"Awesome — `{github_username}` is linked! You're all set to explore. "
93+
"Here's a quick menu of what I can help you with right away."
94+
)
95+
return (
96+
"Great! Your GitHub account is connected and I'm ready to tailor suggestions. "
97+
"Here are the top things I can help with."
98+
)

0 commit comments

Comments
 (0)