Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ async def handle_general_github_help(query: str, llm) -> Dict[str, Any]:
"query": query,
"error": str(e),
"message": "Failed to provide general GitHub help"
}
}
36 changes: 32 additions & 4 deletions backend/app/agents/devrel/nodes/gather_context.py
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__)
Expand All @@ -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)
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

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.

-                logger.warning("Failed to refresh Discord user profile for %s: %s", discord_id, exc)
+                logger.warning("Failed to refresh Discord user profile", extra={"discord_id": str(discord_id)}, exc_info=exc)
🤖 Prompt for AI Agents
In backend/app/agents/devrel/nodes/gather_context.py around line 52, the
logger.warning call embeds the raw discord_id into the message text; instead
remove the discord_id from the formatted message and pass it as a structured
field (e.g., via the logger's extra/context parameter) or redact/mask it if you
must include it in the message, and include the exception via exc_info or a
separate field so the log message reads a generic error and the PII is stored
only in structured metadata handled by the log sink.


Comment on lines +51 to +53
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

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

‼️ 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
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)
🧰 Tools
🪛 Ruff (0.14.1)

51-51: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In backend/app/agents/devrel/nodes/gather_context.py around lines 51 to 53, the
current broad except Exception block will swallow asyncio.CancelledError; modify
the handler to re-raise CancelledError while keeping graceful degradation for
other exceptions — import asyncio if not present, change the exception handling
to either explicitly catch asyncio.CancelledError and re-raise it, or inspect
the caught exception and re-raise if isinstance(exc, asyncio.CancelledError),
and then log other exceptions as before.

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
Expand Down Expand Up @@ -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
66 changes: 58 additions & 8 deletions backend/app/agents/devrel/nodes/handlers/onboarding.py
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,
}
35 changes: 35 additions & 0 deletions backend/app/agents/devrel/nodes/react_supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,41 @@ async def react_supervisor_node(state: AgentState, llm) -> Dict[str, Any]:
tool_results = state.context.get("tool_results", [])
iteration_count = state.context.get("iteration_count", 0)

forced_action = state.context.get("force_next_tool")
if forced_action:
logger.info(
"Supervisor auto-routing to %s for session %s", forced_action, state.session_id
)
decision = {
"action": forced_action,
"reasoning": "Auto-routed by onboarding workflow",
"thinking": "",
}
updated_context = {**state.context}
updated_context.pop("force_next_tool", None)
updated_context["supervisor_decision"] = decision
updated_context["iteration_count"] = iteration_count + 1
return {
"context": updated_context,
"current_task": f"supervisor_forced_{forced_action}",
}

if state.context.get("force_complete"):
logger.info("Supervisor forcing completion for session %s", state.session_id)
decision = {
"action": "complete",
"reasoning": "Auto-complete after onboarding hand-off",
"thinking": "",
}
updated_context = {**state.context}
updated_context.pop("force_complete", None)
updated_context["supervisor_decision"] = decision
updated_context["iteration_count"] = iteration_count + 1
return {
"context": updated_context,
"current_task": "supervisor_forced_complete",
}

prompt = REACT_SUPERVISOR_PROMPT.format(
latest_message=latest_message,
platform=state.platform,
Expand Down
Empty file.
98 changes: 98 additions & 0 deletions backend/app/agents/devrel/onboarding/messages.py
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."
)
Loading