diff --git a/backend/.env.example b/backend/.env.example index ec5b1e6e..99f49f84 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,38 +2,41 @@ # PORT=8000 # CORS_ORIGINS=http://localhost:3000 -SUPABASE_URL="https://.supabase.co" -SUPABASE_KEY="ey...cnGQk" - -DISCORD_BOT_TOKEN="MTM5...5Vr7fw0m9PGAllLfyztCs" -ENABLE_DISCORD_BOT="true" - -GITHUB_TOKEN="ghp_...3IOhGP970vnjYK" # EMBEDDING_MODEL=BAAI/bge-small-en-v1.5 # EMBEDDING_MAX_BATCH_SIZE=32 # EMBEDDING_DEVICE=cpu +#1. backend only +BACKEND_URL=http://localhost:8000 + +#2. Discord-only mode (no GitHub, no Supabase) +DISCORD_BOT_TOKEN="your_discord_bot_token" +GEMINI_API_KEY="your_gemini_api_key" +GEMINI_MODEL=gemini-2.0-flash + +#3. Supabase setting (Discord + Github + Supabase) +SUPABASE_URL="https://...supabase.co" +SUPABASE_KEY="your_supabase_key" +GITHUB_TOKEN="your_github_token" + +#4. Full mode (FalkorDB + CodeGraph + Agents) # FalkorDB Configuration FALKORDB_HOST=localhost FALKORDB_PORT=6379 CODEGRAPH_BACKEND_URL=http://localhost:5000 -CODEGRAPH_SECRET_TOKEN=DevRAI_CodeGraph_Secret -GEMINI_MODEL=gemini-2.0-flash -SECRET_TOKEN=DevRAI_CodeGraph_Secret +SECRET_TOKEN="your_codegraph_secret" CODE_GRAPH_PUBLIC=1 FLASK_RUN_HOST=0.0.0.0 FLASK_RUN_PORT=5000 -BACKEND_URL="http://localhost:8000" - -GEMINI_API_KEY="AIz...ct527D5h-IJCRaXE" -TAVILY_API_KEY="tvly-dev-....1G2k3ivF56SJfWJ4" +RABBITMQ_URL=amqp://localhost:5672/ +TAVILY_API_KEY="your_tavily_api_key" # Langsmith LANGSMITH_TRACING="true" LANGSMITH_ENDPOINT="https://api.smith.langchain.com" -LANGSMITH_API_KEY="lsv2_pt...d9d989_953d073741" +LANGSMITH_API_KEY="your_langsmith_api_key" LANGSMITH_PROJECT="devr" ORG_NAME=AOSSIE diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 75059e88..2c38c6ea 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -6,6 +6,6 @@ - v1: Version 1 API endpoints """ -from .router import api_router +from .router import core_router, get_auth_router -__all__ = ["api_router"] +__all__ = ["core_router", "get_auth_router"] diff --git a/backend/app/api/router.py b/backend/app/api/router.py index 67cd1e56..6f2877b9 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -1,26 +1,31 @@ from fastapi import APIRouter -from .v1.auth import router as auth_router from .v1.health import router as health_router -from .v1.integrations import router as integrations_router -api_router = APIRouter() - -api_router.include_router( - auth_router, - prefix="/v1/auth", - tags=["Authentication"] -) - -api_router.include_router( +core_router = APIRouter() +# -------- Core Router --------------- +core_router.include_router( health_router, prefix="/v1", tags=["Health"] ) -api_router.include_router( - integrations_router, - prefix="/v1/integrations", - tags=["Integrations"] -) +# -------- Auth router -------- +def get_auth_router() -> APIRouter: + router= APIRouter() + from .v1.auth import router as auth_router + from .v1.integrations import router as integrations_router + router.include_router( + auth_router, + prefix="/v1/auth", + tags=["Authentication"] + ) + + router.include_router( + integrations_router, + prefix="/v1/integrations", + tags=["Integrations"] + ) + + return router -__all__ = ["api_router"] +__all__ = ["core_router", "get_auth_router"] diff --git a/backend/app/api/v1/health.py b/backend/app/api/v1/health.py index a60ef0a5..1713bb40 100644 --- a/backend/app/api/v1/health.py +++ b/backend/app/api/v1/health.py @@ -1,18 +1,28 @@ import logging -from fastapi import APIRouter, HTTPException, Depends -from app.database.weaviate.client import get_weaviate_client -from app.core.dependencies import get_app_instance -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from main import DevRAIApplication +from fastapi import APIRouter, HTTPException, Request +from app.core.config import settings router = APIRouter() logger = logging.getLogger(__name__) +async def get_weaviate_status() -> str: + if not settings.code_intelligence_enabled: + return "disabled" + + from app.database.weaviate.client import get_weaviate_client + async with get_weaviate_client() as client: + return "ready" if await client.is_ready() else "not_ready" + + +def get_discord_status(app_instance) -> str: + if not settings.discord_enabled: + return "disabled" + + bot = app_instance.discord_bot + return "running" if bot and not bot.is_closed() else "stopped" @router.get("/health") -async def health_check(app_instance: "DevRAIApplication" = Depends(get_app_instance)): +async def health_check(request: Request): """ General health check endpoint to verify services are running. @@ -20,16 +30,15 @@ async def health_check(app_instance: "DevRAIApplication" = Depends(get_app_insta dict: Status of the application and its services """ try: - async with get_weaviate_client() as client: - weaviate_ready = await client.is_ready() - + services = { + "weaviate": await get_weaviate_status(), + "discord": get_discord_status(request.app.state.app_instance), + } + return { "status": "healthy", - "services": { - "weaviate": "ready" if weaviate_ready else "not_ready", - "discord_bot": "running" if app_instance.discord_bot and not app_instance.discord_bot.is_closed() else "stopped" + "services": services, } - } except Exception as e: logger.error(f"Health check failed: {e}") raise HTTPException( @@ -45,13 +54,11 @@ async def health_check(app_instance: "DevRAIApplication" = Depends(get_app_insta async def weaviate_health(): """Check specifically Weaviate service health.""" try: - async with get_weaviate_client() as client: - is_ready = await client.is_ready() - return { "service": "weaviate", - "status": "ready" if is_ready else "not_ready" - } + "status": await get_weaviate_status(), + } + except Exception as e: logger.error(f"Weaviate health check failed: {e}") raise HTTPException( @@ -65,15 +72,14 @@ async def weaviate_health(): @router.get("/health/discord") -async def discord_health(app_instance: "DevRAIApplication" = Depends(get_app_instance)): +async def discord_health(request: Request): """Check specifically Discord bot health.""" try: - bot_status = "running" if app_instance.discord_bot and not app_instance.discord_bot.is_closed() else "stopped" - return { "service": "discord_bot", - "status": bot_status + "status": get_discord_status(request.app.state.app_instance), } + except Exception as e: logger.error(f"Discord bot health check failed: {e}") raise HTTPException( diff --git a/backend/app/core/config/settings.py b/backend/app/core/config/settings.py index 1349a02f..6a3f6433 100644 --- a/backend/app/core/config/settings.py +++ b/backend/app/core/config/settings.py @@ -2,23 +2,29 @@ from dotenv import load_dotenv from pydantic import field_validator, ConfigDict from typing import Optional +import os load_dotenv() class Settings(BaseSettings): + ## CORE (minimal code) + backend_url: str + + ## OPTIONAL + # Gemini LLM API Key - gemini_api_key: str = "" + gemini_api_key: Optional[str] = None # Tavily API Key - tavily_api_key: str = "" + tavily_api_key: Optional[str] = None # Platforms - github_token: str = "" - discord_bot_token: str = "" + github_token: Optional[str] = None + discord_bot_token: Optional[str] = None # DB configuration - supabase_url: str - supabase_key: str + supabase_url: Optional[str] = None + supabase_key: Optional[str] = None # LangSmith Tracing langsmith_tracing: bool = False @@ -32,27 +38,49 @@ class Settings(BaseSettings): classification_agent_model: str = "gemini-2.0-flash" agent_timeout: int = 30 max_retries: int = 3 + + # FalkorDB / CodeGraph + falkordb_host: Optional[str] = None + falkordb_port: Optional[str] = None + codegraph_backend_url: Optional[str] = None + secret_token: Optional[str] = None # RabbitMQ configuration rabbitmq_url: Optional[str] = None - # Backend URL - backend_url: str = "" - # Onboarding UX toggles onboarding_show_oauth_button: bool = True + # ------------------ + # Derived feature gates + # ------------------ + + @property + def discord_enabled(self) -> bool: + return bool(self.discord_bot_token) and bool(self.gemini_api_key) + + @property + def github_enabled(self) -> bool: + """ + GitHub verification + OAuth. + """ + return self.discord_enabled and bool(self.backend_url) and all([ + self.github_token, + self.supabase_url, + self.supabase_key, + ]) - @field_validator("supabase_url", "supabase_key", mode="before") - @classmethod - def _not_empty(cls, v, field): - if not v: - raise ValueError(f"{field.name} must be set") - return v - - model_config = ConfigDict( - env_file=".env", - extra="ignore" - ) # to prevent errors from extra env variables + @property + def code_intelligence_enabled(self) -> bool: + """ + Runs DevrAI in full mode. + """ + return self.github_enabled and all([ + self.rabbitmq_url, + self.falkordb_host, + self.falkordb_port, + self.codegraph_backend_url, + self.secret_token, + ]) settings = Settings() diff --git a/backend/app/database/supabase/client.py b/backend/app/database/supabase/client.py index b0f5fa7c..ff158c97 100644 --- a/backend/app/database/supabase/client.py +++ b/backend/app/database/supabase/client.py @@ -1,13 +1,25 @@ from app.core.config import settings from supabase._async.client import AsyncClient -supabase_client: AsyncClient = AsyncClient( - settings.supabase_url, - settings.supabase_key -) +_client: AsyncClient | None = None def get_supabase_client() -> AsyncClient: + global _client """ Returns a shared asynchronous Supabase client instance. """ - return supabase_client + if _client is None: + if not settings.supabase_url or not settings.supabase_key: + missing = [] + if not settings.supabase_url: + missing.append("SUPABASE_URL") + if not settings.supabase_key: + missing.append("SUPABASE_KEY") + raise RuntimeError( + f"Supabase misconfigured. Missing: {', '.join(missing)}" + ) + _client = AsyncClient( + settings.supabase_url, + settings.supabase_key, + ) + return _client diff --git a/backend/app/llm/chat.py b/backend/app/llm/chat.py new file mode 100644 index 00000000..49ada4ac --- /dev/null +++ b/backend/app/llm/chat.py @@ -0,0 +1,22 @@ +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_core.messages import HumanMessage +from app.core.config import settings + +async def chat_completion(prompt: str, context: dict | None = None) -> str: + """ + Stateless LLM chat used in discord-only mode. + No agents, no memory, no queue. + """ + # if not settings.llm_enabled: + # return "LLM responses are currently disabled." + + llm = ChatGoogleGenerativeAI( + model=settings.classification_agent_model, + temperature=0.7, + google_api_key=settings.gemini_api_key, + ) + + response = await llm.ainvoke( + [HumanMessage(content=prompt)] + ) + return response.content diff --git a/backend/integrations/discord/bot.py b/backend/integrations/discord/bot.py index dbb7c3a4..38d011da 100644 --- a/backend/integrations/discord/bot.py +++ b/backend/integrations/discord/bot.py @@ -1,16 +1,17 @@ import discord from discord.ext import commands import logging -from typing import Dict, Any, Optional -from app.core.orchestration.queue_manager import AsyncQueueManager, QueuePriority -from app.classification.classification_router import ClassificationRouter +from typing import Dict, Any, Optional,TYPE_CHECKING +from app.core.config import settings +if TYPE_CHECKING: + from app.core.orchestration.queue_manager import AsyncQueueManager, QueuePriority logger = logging.getLogger(__name__) class DiscordBot(commands.Bot): """Discord bot with LangGraph agent integration""" - def __init__(self, queue_manager: AsyncQueueManager, **kwargs): + def __init__(self, queue_manager: Optional["AsyncQueueManager"] = None, **kwargs): intents = discord.Intents.default() intents.message_content = True intents.guilds = True @@ -25,18 +26,29 @@ def __init__(self, queue_manager: AsyncQueueManager, **kwargs): ) self.queue_manager = queue_manager - self.classifier = ClassificationRouter() + self.classifier = None self.active_threads: Dict[str, str] = {} - self._register_queue_handlers() - - def _register_queue_handlers(self): - """Register handlers for queue messages""" - self.queue_manager.register_handler("discord_response", self._handle_agent_response) - + self._queue_handlers_registered = False + + def get_classifier(self): + if self.classifier is None: + from app.classification.classification_router import ClassificationRouter + self.classifier = ClassificationRouter() + return self.classifier + async def on_ready(self): """Bot ready event""" logger.info(f'Enhanced Discord bot logged in as {self.user}') print(f'Bot is ready! Logged in as {self.user}') + + if self.queue_manager and settings.code_intelligence_enabled: + if not self._queue_handlers_registered: + self.queue_manager.register_handler( + "discord_response", + self._handle_agent_response + ) + self._queue_handlers_registered = True + try: synced = await self.tree.sync() print(f"Synced {len(synced)} slash command(s)") @@ -48,11 +60,12 @@ async def on_message(self, message): if message.author == self.user: return - if message.interaction_metadata is not None: + # if message.interaction_metadata is not None: + if message.interaction is not None: return try: - triage_result = await self.classifier.should_process_message( + triage_result = await self.get_classifier().should_process_message( message.content, { "channel_id": str(message.channel.id), @@ -66,13 +79,57 @@ async def on_message(self, message): except Exception as e: logger.error(f"Error processing message: {str(e)}") - + async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): """This now handles both new requests and follow-ups in threads.""" + user_id = str(message.author.id) + thread_id = await self._get_or_create_thread(message, user_id) + + # UX is shared + if thread_id: + thread = self.get_channel(int(thread_id)) + if thread: + await thread.send("I'm processing your request, please hold on...") + + # 🔀 execution split + if self.queue_manager: + await self._handle_devrel_with_queue(message, triage_result, thread_id) + else: + await self._handle_devrel_direct(message, triage_result, thread_id) + + async def _handle_devrel_direct(self,message, triage_result, thread_id): try: - user_id = str(message.author.id) - thread_id = await self._get_or_create_thread(message, user_id) + response = await self._basic_discord_response(message.content) + if thread_id: + thread = self.get_channel(int(thread_id)) + if thread: + await thread.send(response) + else: + await message.channel.send(response) + + except Exception as e: + logger.exception("Basic Discord-only chat failed") + # Fall back to original channel if thread unavailable + try: + if thread_id: + thread = self.get_channel(int("thread_id")) + if thread: + await thread.send("Sorry, I ran into an issue answering that.") + return + await message.channel.send("Sorry, I ran into an issue answering that.") + except Exception: + logger.exception("Failed to send error message") + thread = self.get_channel(int(thread_id)) + await thread.send("Sorry, I ran into an issue answering that." + ) + + async def _handle_devrel_with_queue(self, message, triage_result, thread_id): + try: + from app.core.orchestration.queue_manager import QueuePriority + + user_id = str(message.author.id) + agent_message = { "type": "devrel_request", "id": f"discord_{message.id}", @@ -98,13 +155,6 @@ async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): priority = priority_map.get(triage_result.get("priority"), QueuePriority.MEDIUM) await self.queue_manager.enqueue(agent_message, priority) - # --- "PROCESSING" MESSAGE RESTORED --- - if thread_id: - thread = self.get_channel(int(thread_id)) - if thread: - await thread.send("I'm processing your request, please hold on...") - # ------------------------------------ - except Exception as e: logger.error(f"Error handling DevRel message: {str(e)}") @@ -128,7 +178,22 @@ async def _get_or_create_thread(self, message, user_id: str) -> Optional[str]: except Exception as e: logger.error(f"Failed to create thread: {e}") return str(message.channel.id) - + + async def _basic_discord_response(self, content: str) -> str: + """ + Stateless Discord-only reply. + No memory, no queue, no agent. + """ + from app.llm.chat import chat_completion + + return await chat_completion( + content, + context={ + "platform": "discord", + "mode": "discord_only", + }, + ) + async def _handle_agent_response(self, response_data: Dict[str, Any]): try: thread_id = response_data.get("thread_id") @@ -142,4 +207,4 @@ async def _handle_agent_response(self, response_data: Dict[str, Any]): else: logger.error(f"Thread {thread_id} not found for agent response") except Exception as e: - logger.error(f"Error handling agent response: {str(e)}") + logger.error(f"Error handling agent response: {str(e)}") \ No newline at end of file diff --git a/backend/integrations/discord/cogs.py b/backend/integrations/discord/cogs.py index 64fd0b7f..7eb05dec 100644 --- a/backend/integrations/discord/cogs.py +++ b/backend/integrations/discord/cogs.py @@ -3,36 +3,86 @@ import discord from discord import app_commands from discord.ext import commands, tasks +from typing import Optional, TYPE_CHECKING -from app.agents.devrel.onboarding.messages import ( - build_encourage_verification_message, - build_new_user_welcome, - build_verified_capabilities_intro, - build_verified_welcome, -) from app.core.config import settings -from app.core.orchestration.queue_manager import AsyncQueueManager, QueuePriority -from app.services.auth.management import get_or_create_user_by_discord -from app.services.auth.supabase import login_with_github -from app.services.auth.verification import create_verification_session, cleanup_expired_tokens -from app.services.codegraph.repo_service import RepoService from integrations.discord.bot import DiscordBot -from integrations.discord.views import OAuthView, OnboardingView, build_final_handoff_embed + +if TYPE_CHECKING: + from app.core.orchestration.queue_manager import AsyncQueueManager logger = logging.getLogger(__name__) +# ---interactions---- +async def send_github_unavailable(interaction: discord.Interaction): + embed = discord.Embed( + title="❌ GitHub Verification Unavailable", + description=( + "GitHub verification is currently disabled on this server.\n\n" + "You can continue using other features, or try again later." + ), + color=discord.Color.red(), + ) + if interaction.response.is_done(): + await interaction.followup.send(embed=embed, ephemeral=True) + else: + await interaction.response.send_message(embed=embed, ephemeral=True) + +async def falkor_unavailable(interaction: discord.Interaction): + embed = discord.Embed( + title="❌ Code Intelligence Unavailable", + description=( + "Repository indexing and code analysis are currently disabled.\n\n" + "**Possible reasons:**\n" + "• FalkorDB is not configured\n" + "• Background queue is disabled\n\n" + "You can still use basic DevRel and GitHub features." + ), + color=discord.Color.red(), + ) + # Safe send + if interaction.response.is_done(): + await interaction.followup.send(embed=embed, ephemeral=True) + else: + await interaction.response.send_message(embed=embed, ephemeral=True) + +# ---- user dms ---- +async def send_github_unavailable_dm(user: discord.abc.User): + embed = discord.Embed( + title="🔒 GitHub Verification Disabled", + description=( + "GitHub account linking is currently unavailable.\n\n" + "**Why this may happen:**\n" + "• GitHub OAuth is not configured\n" + "• Supabase is disabled\n\n" + "You can still use the bot for general help and questions." + ), + color=discord.Color.orange(), + ) + + try: + await user.send(embed=embed) + except discord.Forbidden: + # User has DMs disabled — silently ignore + pass + except Exception as e: + logger.exception(f"Failed to send GitHub unavailable DM to {user}: {e}") + + class DevRelCommands(commands.Cog): - def __init__(self, bot: DiscordBot, queue_manager: AsyncQueueManager): + def __init__(self, bot: DiscordBot, queue_manager: Optional["AsyncQueueManager"] = None): self.bot = bot self.queue = queue_manager def cog_load(self): """Called when the cog is loaded""" - self.cleanup_expired_tokens.start() + if settings.github_enabled: + self.cleanup_expired_tokens.start() def cog_unload(self): - self.cleanup_expired_tokens.cancel() + if settings.github_enabled and self.cleanup_expired_tokens.is_running(): + self.cleanup_expired_tokens.cancel() @tasks.loop(minutes=5) async def cleanup_expired_tokens(self): @@ -58,32 +108,70 @@ async def reset_thread(self, interaction: discord.Interaction): "user_id": user_id, "cleanup_reason": "manual_reset" } - await self.queue.enqueue(cleanup, QueuePriority.HIGH) self.bot.active_threads.pop(user_id, None) - await interaction.response.send_message("Your DevRel thread & memory have been reset!", ephemeral=True) + + if self.queue: + from app.core.orchestration.queue_manager import QueuePriority + await self.queue.enqueue(cleanup, QueuePriority.HIGH) + await interaction.response.send_message( + "Your DevRel thread has been reset!", + ephemeral=True, + ) + else: + await interaction.response.send_message( + "Your DevRel thread has been cleared locally. " + "(Full memory reset unavailable in current mode.)", + ephemeral=True, + ) @app_commands.command(name="help", description="Show DevRel assistant help.") async def help_devrel(self, interaction: discord.Interaction): embed = discord.Embed( title="DevRel Assistant Help", - description="I can help you with Devr.AI related questions!", + description="Available commands on this server:", color=discord.Color.blue() ) + embed.add_field( - name="Commands", + name="Basic", value=( "• `/reset` - Reset your DevRel thread and memory\n" - "• `/help` - Show this help message\n" - "• `/verify_github` - Link your GitHub account\n" - "• `/verification_status` - Check your verification status\n" + "• `/help` - Show this help message" ), inline=False ) + + if settings.github_enabled: + embed.add_field( + name="GitHub", + value=( + "• `/verify_github` - Link your GitHub account\n" + "• `/verification_status` - Check your verification status" + ), + inline=False + ) + if settings.code_intelligence_enabled: + embed.add_field( + name="Code Intelligence", + value=( + "• `/index_repository` - Index a GitHub repository\n" + "• `/delete_index` - Delete an indexed repository\n" + "• `/list_indexed_repos` - List your indexed repositories" + ), + inline=False + ) await interaction.response.send_message(embed=embed) @app_commands.command(name="verification_status", description="Check your GitHub verification status.") async def verification_status(self, interaction: discord.Interaction): + if not settings.github_enabled: + logger.info("Verification blocked: GitHub/Supabase not configured") + await send_github_unavailable(interaction) + return + + from integrations.discord.views import OAuthView, OnboardingView, build_final_handoff_embed + from app.services.auth.management import get_or_create_user_by_discord try: user_profile = await get_or_create_user_by_discord( discord_id=str(interaction.user.id), @@ -93,13 +181,13 @@ async def verification_status(self, interaction: discord.Interaction): ) if user_profile.is_verified and user_profile.github_id: embed = discord.Embed(title="✅ Verification Status", - color=discord.Color.green()) + color=discord.Color.green()) embed.add_field(name="GitHub Account", value=f"`{user_profile.github_username}`", inline=True) embed.add_field(name="Status", value="✅ Verified", inline=True) else: embed = discord.Embed(title="❌ Verification Status", - description="Your GitHub account is not linked.", - color=discord.Color.red()) + description="Your GitHub account is not linked.", + color=discord.Color.red()) embed.add_field(name="Next Steps", value="Use `/verify_github` to link your GitHub account.", inline=False) @@ -110,8 +198,17 @@ async def verification_status(self, interaction: discord.Interaction): @app_commands.command(name="verify_github", description="Link your GitHub account.") async def verify_github(self, interaction: discord.Interaction): + if not settings.github_enabled: + logger.info("Verification blocked: GitHub/Supabase not configured") + await send_github_unavailable(interaction) + return try: await interaction.response.defer(ephemeral=True) + + from integrations.discord.views import OAuthView, OnboardingView, build_final_handoff_embed + from app.services.auth.management import get_or_create_user_by_discord + from app.services.auth.supabase import login_with_github + from app.services.auth.verification import create_verification_session user_profile = await get_or_create_user_by_discord( discord_id=str(interaction.user.id), @@ -190,7 +287,14 @@ async def verify_github(self, interaction: discord.Interaction): @app_commands.describe(repository="GitHub URL or owner/repo (e.g., AOSSIE-Org/Devr.AI)") async def index_repository(self, interaction: discord.Interaction, repository: str): """Index a GitHub repository into FalkorDB code graph""" + if not settings.code_intelligence_enabled: + logger.info("Indexing blocked: FalkorDB not configured") + await falkor_unavailable(interaction) + return + await interaction.response.defer(thinking=True) + + from app.services.codegraph.repo_service import RepoService try: service = RepoService() @@ -317,8 +421,13 @@ async def _run_index_and_update(): @app_commands.describe(repository="Repository name (owner/repo)") async def delete_index(self, interaction: discord.Interaction, repository: str): """Delete a repository index""" + if not settings.code_intelligence_enabled: + logger.info("Deletion of index blocked: FalkorDB not configured") + await falkor_unavailable(interaction) + return + await interaction.response.defer(thinking=True) - + from app.services.codegraph.repo_service import RepoService try: service = RepoService() logger.info(f"Delete request from {interaction.user.id}: {repository}") @@ -364,8 +473,14 @@ async def delete_index(self, interaction: discord.Interaction, repository: str): @app_commands.command(name="list_indexed_repos", description="List your indexed repositories") async def list_indexed_repos(self, interaction: discord.Interaction): """List user's indexed repositories""" + if not settings.code_intelligence_enabled: + logger.info("list indexed repo blocked: FalkorDB not configured") + await falkor_unavailable(interaction) + return + await interaction.response.defer() - + from app.services.codegraph.repo_service import RepoService + try: service = RepoService() repos = await service.list_repos(str(interaction.user.id)) @@ -398,7 +513,8 @@ async def list_indexed_repos(self, interaction: discord.Interaction): async def setup(bot: commands.Bot): """This function is called by the bot to load the cog.""" - await bot.add_cog(DevRelCommands(bot, bot.queue_manager)) + queue = getattr(bot, "queue_manager", None) + await bot.add_cog(DevRelCommands(bot, queue)) await bot.add_cog(OnboardingCog(bot)) @@ -458,7 +574,22 @@ async def _send_onboarding_flow(self, user: discord.abc.User) -> str: - "dm_forbidden": cannot DM the user - "error": unexpected error (fallback attempted) """ + if not settings.github_enabled: + logger.info("Verification blocked: GitHub/Supabase not configured") + await send_github_unavailable_dm(user) + return "auth_unavailable" try: + from app.agents.devrel.onboarding.messages import ( + build_encourage_verification_message, + build_new_user_welcome, + build_verified_capabilities_intro, + build_verified_welcome, + ) + from integrations.discord.views import OnboardingView, build_final_handoff_embed + + from app.services.auth.verification import create_verification_session + from app.services.auth.supabase import login_with_github + from app.services.auth.management import get_or_create_user_by_discord # Ensure DB record exists profile = await get_or_create_user_by_discord( discord_id=str(user.id), diff --git a/backend/main.py b/backend/main.py index b7ad80a6..efd243a8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,13 +7,11 @@ from fastapi import FastAPI, Response from fastapi.middleware.cors import CORSMiddleware -from app.api.router import api_router +from app.api.router import core_router, get_auth_router from app.core.config import settings -from app.core.orchestration.agent_coordinator import AgentCoordinator -from app.core.orchestration.queue_manager import AsyncQueueManager -from app.database.weaviate.client import get_weaviate_client -from integrations.discord.bot import DiscordBot -from discord.ext import commands +# if settings.code_intelligence_enabled: +# from app.core.orchestration.queue_manager import AsyncQueueManager +# from app.database.weaviate.client import get_weaviate_client # DevRel commands are now loaded dynamically (commented out below) # from integrations.discord.cogs import DevRelCommands @@ -32,30 +30,65 @@ class DevRAIApplication: def __init__(self): """Initializes all services required by the application.""" self.weaviate_client = None - self.queue_manager = AsyncQueueManager() - self.agent_coordinator = AgentCoordinator(self.queue_manager) - self.discord_bot = DiscordBot(self.queue_manager) + self.discord_bot = None + self._discord_task = None + self.agent_coordinator = None + + # Discord always exists if enabled + if settings.discord_enabled: + from integrations.discord.bot import DiscordBot + from discord.ext import commands + self.discord_bot = DiscordBot(queue_manager=None) + self.queue_manager = None + + if settings.code_intelligence_enabled: + from app.core.orchestration.queue_manager import AsyncQueueManager + from app.database.weaviate.client import get_weaviate_client + from app.core.orchestration.agent_coordinator import AgentCoordinator + self.weaviate_client = get_weaviate_client() + + self.queue_manager = AsyncQueueManager() + self.discord_bot= DiscordBot(self.queue_manager) + self.agent_coordinator = AgentCoordinator(self.queue_manager) async def start_background_tasks(self): - """Starts the Discord bot and queue workers in the background.""" + """ + Start all enabled background services including the Discord bot, + queue workers, and code intelligence dependencies. + """ try: - logger.info("Starting background tasks (Discord Bot & Queue Manager)...") - - await self.test_weaviate_connection() - - await self.queue_manager.start(num_workers=3) - - # --- Load commands inside the async startup function --- - try: - await self.discord_bot.load_extension("integrations.discord.cogs") - except (ImportError, commands.ExtensionError) as e: - logger.error("Failed to load Discord cog extension: %s", e) - - # Start the bot as a background task. - asyncio.create_task( - self.discord_bot.start(settings.discord_bot_token) - ) - logger.info("Background tasks started successfully!") + # Discord Mode + if settings.discord_enabled: + # 1. Start queue + if settings.code_intelligence_enabled: + await self.queue_manager.start(num_workers=3) + + # 2. Discord + try: + await self.discord_bot.load_extension("integrations.discord.cogs") + self._discord_task = asyncio.create_task( + self.discord_bot.start(settings.discord_bot_token) + ) + except Exception as e: + logger.exception("Discord startup failed: %s",e) + self.discord_bot = None + + # Full Mode + if settings.code_intelligence_enabled: + # Weaviate + try: + await self.test_weaviate_connection() + except Exception as e: + logger.warning("Weaviate disabled: %s", e) + + + logger.info( + "Background services ready | queue=%s weaviate=%s discord=%s", + bool(settings.rabbitmq_url), + self.weaviate_client, + bool(self.discord_bot), + ) + except Exception as e: logger.error(f"Error during background task startup: {e}", exc_info=True) await self.stop_background_tasks() @@ -63,7 +96,11 @@ async def start_background_tasks(self): async def test_weaviate_connection(self): """Test Weaviate connection during startup.""" + if not settings.code_intelligence_enabled: + logger.info("Weaviate Is for Full Mode") + return try: + from app.database.weaviate.client import get_weaviate_client async with get_weaviate_client() as client: if await client.is_ready(): logger.info("Weaviate connection successful and ready") @@ -75,7 +112,7 @@ async def stop_background_tasks(self): """Stops all background tasks and connections gracefully.""" logger.info("Stopping background tasks and closing connections...") try: - if not self.discord_bot.is_closed(): + if settings.discord_enabled and self.discord_bot and not self.discord_bot.is_closed(): await self.discord_bot.close() logger.info("Discord bot has been closed.") except Exception as e: @@ -123,19 +160,52 @@ async def favicon(): """Return empty favicon to prevent 404 logs""" return Response(status_code=204) -api.include_router(api_router) +api.include_router(core_router) +if settings.github_enabled: + api.include_router(get_auth_router()) + if __name__ == "__main__": required_vars = [ - "DISCORD_BOT_TOKEN", "SUPABASE_URL", "SUPABASE_KEY", - "BACKEND_URL", "GEMINI_API_KEY", "TAVILY_API_KEY", "GITHUB_TOKEN" + "BACKEND_URL" ] + optional_vars = { + "supabase": ["SUPABASE_URL", "SUPABASE_KEY"], + "discord" : ["DISCORD_BOT_TOKEN"], + "llm":["GEMINI_API_KEY"], + "queue": ["RABBITMQ_URL"], + "search": ["TAVILY_API_KEY"], + "github": ["GITHUB_TOKEN"] + } missing_vars = [var for var in required_vars if not getattr(settings, var.lower(), None)] if missing_vars: - logger.error(f"Missing required environment variables: {', '.join(missing_vars)}") - sys.exit(1) + raise RuntimeError(f"Core backend misconfigured. Missing: {','.join(missing_vars)} ") + + capabilities = { + "discord_enabled": settings.discord_enabled, + "llm_enabled": bool(settings.gemini_api_key), + "github_enabled": settings.github_enabled, + "code_intelligence": settings.code_intelligence_enabled, + "search_enabled": bool(settings.tavily_api_key), + "queue_enabled": bool(settings.rabbitmq_url), + } + + if settings.code_intelligence_enabled: + mode = "full" + elif settings.github_enabled: + mode = "discord+github" + elif settings.discord_enabled: + mode = "discord" + else: + mode = "minimal" + + logger.info( + "Startup | mode=%s | capabilities=%s" , + mode, + {k: v for k, v in capabilities.items() if v}, + ) uvicorn.run( "__main__:api",