-
Notifications
You must be signed in to change notification settings - Fork 55
feat: route onboarding flow through MCP-backed GitHub toolkit #137
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
Changes from all commits
707d33e
f5a9a80
7c6e9e6
7cca3ef
35ae5e6
abb6690
af72240
92bfe30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,9 @@ | ||||||||||||||
| import logging | ||||||||||||||
| from datetime import datetime | ||||||||||||||
| from typing import Dict, Any | ||||||||||||||
| from typing import Any, Dict | ||||||||||||||
|
|
||||||||||||||
| from app.agents.state import AgentState | ||||||||||||||
| from app.services.auth.management import get_or_create_user_by_discord | ||||||||||||||
| from app.database.supabase.services import ensure_user_exists, get_conversation_context | ||||||||||||||
|
|
||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||
|
|
@@ -28,8 +30,29 @@ async def gather_context_node(state: AgentState) -> Dict[str, Any]: | |||||||||||||
| "timestamp": datetime.now().isoformat() | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| profile_data: Dict[str, Any] = dict(state.user_profile or {}) | ||||||||||||||
|
|
||||||||||||||
| if state.platform.lower() == "discord": | ||||||||||||||
| author = state.context.get("author", {}) or {} | ||||||||||||||
| discord_id = author.get("id") or state.user_id | ||||||||||||||
| display_name = author.get("display_name") or author.get("global_name") or author.get("name") or author.get("username") | ||||||||||||||
| discord_username = author.get("username") or author.get("name") or author.get("display_name") | ||||||||||||||
| avatar_url = author.get("avatar") or author.get("avatar_url") | ||||||||||||||
|
|
||||||||||||||
| if discord_id: | ||||||||||||||
| try: | ||||||||||||||
| user = await get_or_create_user_by_discord( | ||||||||||||||
| discord_id=str(discord_id), | ||||||||||||||
| display_name=str(display_name or discord_username or discord_id), | ||||||||||||||
| discord_username=str(discord_username or display_name or discord_id), | ||||||||||||||
| avatar_url=avatar_url, | ||||||||||||||
| ) | ||||||||||||||
| profile_data = user.model_dump() | ||||||||||||||
| except Exception as exc: # pragma: no cover - graceful degradation | ||||||||||||||
| logger.warning("Failed to refresh Discord user profile for %s: %s", discord_id, exc) | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+51
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t swallow asyncio.CancelledError; narrow the exception handling Catching Exception will absorb cancellations in async tasks. Re-raise CancelledError and keep graceful degradation for others. +import asyncio
@@
- except Exception as exc: # pragma: no cover - graceful degradation
- logger.warning("Failed to refresh Discord user profile for %s: %s", discord_id, exc)
+ except asyncio.CancelledError:
+ raise
+ except Exception as exc: # pragma: no cover - graceful degradation
+ logger.warning("Failed to refresh Discord user profile", extra={"discord_id": str(discord_id)}, exc_info=exc)📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.14.1)51-51: Do not catch blind exception: (BLE001) 🤖 Prompt for AI Agents |
||||||||||||||
| context_data = { | ||||||||||||||
| "user_profile": {"user_id": state.user_id, "platform": state.platform}, | ||||||||||||||
| "user_profile": profile_data or {"user_id": state.user_id, "platform": state.platform}, | ||||||||||||||
| "conversation_context": len(state.messages) + 1, # +1 for the new message | ||||||||||||||
| "session_info": {"session_id": state.session_id}, | ||||||||||||||
| "user_uuid": user_uuid | ||||||||||||||
|
|
@@ -63,9 +86,14 @@ async def gather_context_node(state: AgentState) -> Dict[str, Any]: | |||||||||||||
|
|
||||||||||||||
| updated_context = {**state.context, **context_data} | ||||||||||||||
|
|
||||||||||||||
| return { | ||||||||||||||
| result: Dict[str, Any] = { | ||||||||||||||
| "messages": [new_message], | ||||||||||||||
| "context": updated_context, | ||||||||||||||
| "current_task": "context_gathered", | ||||||||||||||
| "last_interaction_time": datetime.now() | ||||||||||||||
| "last_interaction_time": datetime.now(), | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if profile_data: | ||||||||||||||
| result["user_profile"] = profile_data | ||||||||||||||
|
|
||||||||||||||
| return result | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,67 @@ | ||
| import logging | ||
| from typing import Any, Dict | ||
|
|
||
| from app.agents.devrel.onboarding.workflow import ( | ||
| OnboardingStage, | ||
| run_onboarding_flow, | ||
| ) | ||
| from app.agents.state import AgentState | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| async def handle_onboarding_node(state: AgentState) -> AgentState: | ||
| """Handle onboarding requests""" | ||
| def _latest_text(state: AgentState) -> str: | ||
| if state.messages: | ||
| return state.messages[-1].get("content", "").lower() | ||
| return state.context.get("original_message", "").lower() | ||
|
|
||
| async def handle_onboarding_node(state: AgentState) -> Dict[str, Any]: | ||
| """Handle onboarding requests via the multi-stage onboarding workflow.""" | ||
| logger.info(f"Handling onboarding for session {state.session_id}") | ||
|
|
||
| text = _latest_text(state) | ||
|
|
||
| # Try to derive verification state if present in context/user_profile | ||
| is_verified = False | ||
| github_username = None | ||
| try: | ||
| profile = state.user_profile or {} | ||
| ctx_profile = state.context.get("user_profile", {}) | ||
| is_verified = bool(profile.get("is_verified") or ctx_profile.get("is_verified")) | ||
| github_username = profile.get("github_username") or ctx_profile.get("github_username") | ||
| except Exception: | ||
| pass | ||
|
|
||
| flow_result, updated_state = run_onboarding_flow( | ||
| state=state, | ||
| latest_message=text, | ||
| is_verified=is_verified, | ||
| github_username=github_username, | ||
| ) | ||
|
|
||
| task_result: Dict[str, Any] = { | ||
| "type": "onboarding", | ||
| "stage": flow_result.stage.value, | ||
| "status": flow_result.status, | ||
| "welcome_message": flow_result.welcome_message, | ||
| "final_message": flow_result.final_message, | ||
| "actions": flow_result.actions, | ||
| "is_verified": flow_result.is_verified, | ||
| "capability_sections": flow_result.capability_sections, | ||
| } | ||
|
|
||
| if flow_result.route_hint: | ||
| task_result["route_hint"] = flow_result.route_hint | ||
| if flow_result.handoff: | ||
| task_result["handoff"] = flow_result.handoff | ||
| if flow_result.next_tool: | ||
| task_result["next_tool"] = flow_result.next_tool | ||
| if flow_result.metadata: | ||
| task_result["metadata"] = flow_result.metadata | ||
|
|
||
| current_task = f"onboarding_{flow_result.stage.value}" | ||
|
|
||
| return { | ||
| "task_result": { | ||
| "type": "onboarding", | ||
| "action": "welcome_and_guide", | ||
| "next_steps": ["setup_environment", "first_contribution", "join_community"] | ||
| }, | ||
| "current_task": "onboarding_handled" | ||
| "task_result": task_result, | ||
| "current_task": current_task, | ||
| "onboarding_state": updated_state, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| """Shared onboarding messaging primitives used across channels.""" | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Dict, List, Optional | ||
|
|
||
| # Structured capability sections pulled from design docs so UI and LLM share copy | ||
| CAPABILITY_SECTIONS: List[Dict[str, List[str]]] = [ | ||
| { | ||
| "title": "Explore Our Projects", | ||
| "examples": [ | ||
| "Show me our most active repositories.", | ||
| "Give me an overview of the 'Devr.AI-backend' repo.", | ||
| ], | ||
| }, | ||
| { | ||
| "title": "Find Ways to Contribute", | ||
| "examples": [ | ||
| "Are there any 'good first issues' available?", | ||
| "Find issues with the 'bug' label.", | ||
| ], | ||
| }, | ||
| { | ||
| "title": "Answer Project Questions", | ||
| "examples": [ | ||
| "How do I set up the local development environment?", | ||
| "What's the process for submitting a pull request?", | ||
| ], | ||
| }, | ||
| ] | ||
|
|
||
| CAPABILITIES_INTRO_TEXT = ( | ||
| "You're all set! As the Devr.AI assistant, my main purpose is to help you " | ||
| "navigate and contribute to our projects. Here's a look at what you can ask " | ||
| "me to do." | ||
| ) | ||
| CAPABILITIES_OUTRO_TEXT = "Feel free to ask me anything related to the project. What's on your mind?" | ||
|
|
||
|
|
||
| def render_capabilities_text() -> str: | ||
| """Render the capabilities message as plain text for chat responses.""" | ||
| lines: List[str] = [CAPABILITIES_INTRO_TEXT, ""] | ||
| for section in CAPABILITY_SECTIONS: | ||
| lines.append(f"{section['title']}:") | ||
| for example in section["examples"]: | ||
| lines.append(f"- \"{example}\"") | ||
| lines.append("") | ||
| lines.append(CAPABILITIES_OUTRO_TEXT) | ||
| return "\n".join(lines).strip() | ||
|
|
||
|
|
||
| def build_new_user_welcome() -> str: | ||
| """Welcome copy when verification is still pending.""" | ||
| return ( | ||
| "👋 Welcome to the Devr.AI community! I'm here to help you get started on your contributor journey.\n\n" | ||
| "To give you the best recommendations for repositories and issues, I first need to link your GitHub account. " | ||
| "This one-time step helps me align tasks with your profile.\n\n" | ||
| "Here's how to verify:\n" | ||
| "- Run `/verify_github` to start verification right away.\n" | ||
| "- Use `/verification_status` to see if you're already linked.\n" | ||
| "- Use `/help` anytime to explore everything I can assist with.\n\n" | ||
| "Would you like to verify your GitHub account now or skip this step for now? You can always do it later." | ||
| ) | ||
|
|
||
|
|
||
| def build_verified_welcome(github_username: Optional[str] = None) -> str: | ||
| """Welcome copy for returning verified contributors.""" | ||
| greeting = "👋 Welcome back to the Devr.AI community!" | ||
| if github_username: | ||
| greeting += f" I see `{github_username}` is already linked, which is great." | ||
| else: | ||
| greeting += " I see your GitHub account is already verified, which is great." | ||
| return ( | ||
| 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." | ||
| ) | ||
|
|
||
|
|
||
| def build_encourage_verification_message(reminder_count: int = 0) -> str: | ||
| """Reminder copy for users who haven't verified yet but want to explore.""" | ||
| reminder_prefix = "Quick reminder" if reminder_count else "No worries" | ||
| return ( | ||
| f"{reminder_prefix} — linking your GitHub unlocks personalized suggestions. " | ||
| "Run `/verify_github` when you're ready, and `/verification_status` to check progress.\n\n" | ||
| "While you set that up, I can still show you what's happening across the organization. " | ||
| "Ask for repository highlights, open issues, or anything else you're curious about." | ||
| ) | ||
|
|
||
|
|
||
| def build_verified_capabilities_intro(github_username: Optional[str] = None) -> str: | ||
| """Intro text shown right before the capability menu for verified users.""" | ||
| if github_username: | ||
| return ( | ||
| f"Awesome — `{github_username}` is linked! You're all set to explore. " | ||
| "Here's a quick menu of what I can help you with right away." | ||
| ) | ||
| return ( | ||
| "Great! Your GitHub account is connected and I'm ready to tailor suggestions. " | ||
| "Here are the top things I can help with." | ||
| ) |
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.
PII in logs: redact or move to structured fields
Avoid emitting raw discord_id in message text; keep it in extras and rely on log sinks for filtering.
🤖 Prompt for AI Agents