diff --git a/community-chatbot/scripts/backend/api/routes/github_routes.py b/community-chatbot/scripts/backend/api/routes/github_routes.py new file mode 100644 index 00000000..eaa0ade6 --- /dev/null +++ b/community-chatbot/scripts/backend/api/routes/github_routes.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter, HTTPException +from backend.models.schemas import ChatRequest, ChatResponse +from backend.core.state import github_store +from backend.services.github_service import get_github_agent + +router = APIRouter(tags=["github"]) + +@router.post("/chat", response_model=ChatResponse) +async def chat_with_github_agent(request: ChatRequest): + try: + agent_executor = get_github_agent() + except Exception as e: + raise HTTPException(status_code=500, detail=f"Agent not initialized: {e}") + + session_id = request.session_id + user_message = request.message + + # Add user message to state + github_store.add_message(session_id, ("user", user_message)) + chat_history = github_store.get_session(session_id) + + try: + events = agent_executor.stream( + {"messages": chat_history}, + stream_mode="values" + ) + + last_msg = None + for event in events: + last_msg = event["messages"][-1] + + if last_msg: + response_content = last_msg.content + github_store.add_message(session_id, ("assistant", response_content)) + else: + response_content = "Sorry, I couldn't process your request." + + return ChatResponse(response=response_content, session_id=session_id) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error processing request: {str(e)}") + +@router.get("/sessions") +async def get_sessions(): + return {"sessions": github_store.get_all_session_ids()} + +@router.delete("/sessions/{session_id}") +async def clear_session(session_id: str): + if github_store.clear_session(session_id): + return {"message": f"Session {session_id} cleared"} + raise HTTPException(status_code=404, detail="Session not found") diff --git a/community-chatbot/scripts/backend/api/routes/jira_routes.py b/community-chatbot/scripts/backend/api/routes/jira_routes.py new file mode 100644 index 00000000..a3c915a4 --- /dev/null +++ b/community-chatbot/scripts/backend/api/routes/jira_routes.py @@ -0,0 +1,53 @@ +from fastapi import APIRouter, HTTPException +from backend.models.schemas import JiraQueryRequest, JiraQueryResponse +from backend.services.jira_service import get_jira_service + +router = APIRouter(tags=["jira"]) + +@router.options("/query") +async def options_jira_query(): + return {} + +@router.post("/query", response_model=JiraQueryResponse) +async def query_jira(request: JiraQueryRequest): + try: + service = get_jira_service() + if request.use_fallback: + result = service.intelligent_agent_run(request.query) + else: + try: + response = service.run_agent(request.query) + result = { + "response": response, + "method_used": "agent", + "query_used": request.query, + "success": True + } + except Exception as e: + result = { + "response": f"Agent failed: {str(e)}", + "method_used": "agent", + "query_used": request.query, + "success": False + } + return JiraQueryResponse(**result) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/direct-jql") +async def direct_jql_query(jql_query: str): + try: + service = get_jira_service() + result = service.direct_jql_query(jql_query) + return {"jql_query": jql_query, "result": result, "success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/generate-jql") +async def generate_jql(natural_query: str): + try: + service = get_jira_service() + generated_jql = service.generate_jql(natural_query) + return {"natural_query": natural_query, "generated_jql": generated_jql, "success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/community-chatbot/scripts/backend/api/routes/slack_routes.py b/community-chatbot/scripts/backend/api/routes/slack_routes.py new file mode 100644 index 00000000..bb7cd993 --- /dev/null +++ b/community-chatbot/scripts/backend/api/routes/slack_routes.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, HTTPException +from backend.models.schemas import ChatRequest, ChatResponse, ConversationHistoryResponse, ChatMessageSchema +from backend.core.state import slack_store +from backend.services.slack_service import get_slack_agent + +router = APIRouter(tags=["slack"]) + +@router.post("/chat", response_model=ChatResponse) +async def chat_endpoint(request: ChatRequest): + try: + session_id = request.session_id + + # Get or create agent + agent = slack_store.get_agent(session_id) + if not agent: + agent = get_slack_agent() + slack_store.set_agent(session_id, agent) + + slack_store.add_message(session_id, ("user", request.message)) + chat_history = slack_store.get_session(session_id) + + events = agent.stream({"messages": chat_history}, stream_mode="values") + + final_response = "" + final_event = None + + for event in events: + final_event = event + message = event["messages"][-1] + if hasattr(message, 'content') and message.type == "ai": + if not (hasattr(message, 'tool_calls') and message.tool_calls): + final_response = message.content + + if final_event: + slack_store.update_session(session_id, final_event["messages"]) + + if not final_response: + final_response = "I processed your request, but didn't generate a text response." + + return ChatResponse(response=str(final_response), session_id=session_id) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/conversations/{session_id}", response_model=ConversationHistoryResponse) +async def get_conversation(session_id: str): + history = slack_store.get_session(session_id) + formatted = [] + + for message in history: + if hasattr(message, 'type') and hasattr(message, 'content'): + formatted.append(ChatMessageSchema(role=message.type, content=str(message.content))) + elif isinstance(message, tuple) and len(message) == 2: + formatted.append(ChatMessageSchema(role=message[0], content=str(message[1]))) + else: + formatted.append(ChatMessageSchema(role="unknown", content=str(message))) + + return ConversationHistoryResponse(session_id=session_id, messages=formatted) + +@router.delete("/conversations/{session_id}") +async def clear_conversation(session_id: str): + slack_store.clear_session(session_id) + return {"message": f"Conversation {session_id} cleared"} diff --git a/community-chatbot/scripts/backend/core/config.py b/community-chatbot/scripts/backend/core/config.py new file mode 100644 index 00000000..67e9d64f --- /dev/null +++ b/community-chatbot/scripts/backend/core/config.py @@ -0,0 +1,34 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +from functools import lru_cache +from typing import Optional + +class Settings(BaseSettings): + # App Settings + APP_NAME: str = "Community AI Backend" + APP_VERSION: str = "1.0.0" + API_V1_STR: str = "/api/v1" + + # OpenAI + OPENAI_API_KEY: Optional[str] = None + + # GitHub App Settings + GITHUB_APP_ID: Optional[str] = None + GITHUB_REPOSITORY: Optional[str] = None + GITHUB_BRANCH: Optional[str] = None + GITHUB_BASE_BRANCH: Optional[str] = None + GITHUB_APP_PRIVATE_KEY: Optional[str] = None + + # Jira Settings + JIRA_API_TOKEN: Optional[str] = None + JIRA_USERNAME: Optional[str] = None + JIRA_INSTANCE_URL: Optional[str] = None + JIRA_CLOUD: Optional[bool] = None + + # Slack Settings + SLACK_BOT_TOKEN: Optional[str] = None + + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") + +@lru_cache() +def get_settings() -> Settings: + return Settings() diff --git a/community-chatbot/scripts/backend/core/state.py b/community-chatbot/scripts/backend/core/state.py new file mode 100644 index 00000000..88d8b6e6 --- /dev/null +++ b/community-chatbot/scripts/backend/core/state.py @@ -0,0 +1,56 @@ +from typing import Dict, List, Any +from threading import Lock + +class MemoryStore: + """Thread-safe in-memory store for chat sessions and agents.""" + def __init__(self): + self._chat_sessions: Dict[str, List[Any]] = {} + self._agents: Dict[str, Any] = {} + self._lock = Lock() + + def get_session(self, session_id: str) -> List[Any]: + with self._lock: + if session_id not in self._chat_sessions: + self._chat_sessions[session_id] = [] + return list(self._chat_sessions[session_id]) # Return a shallow copy + + def update_session(self, session_id: str, messages: List[Any]): + with self._lock: + self._chat_sessions[session_id] = messages + + def add_message(self, session_id: str, message: Any): + with self._lock: + if session_id not in self._chat_sessions: + self._chat_sessions[session_id] = [] + self._chat_sessions[session_id].append(message) + + def clear_session(self, session_id: str) -> bool: + with self._lock: + popped = False + if session_id in self._chat_sessions: + del self._chat_sessions[session_id] + popped = True + if session_id in self._agents: + del self._agents[session_id] + return popped + + def get_agent(self, session_id: str) -> Any: + with self._lock: + return self._agents.get(session_id) + + def set_agent(self, session_id: str, agent: Any): + with self._lock: + self._agents[session_id] = agent + + def delete_agent(self, session_id: str): + with self._lock: + if session_id in self._agents: + del self._agents[session_id] + + def get_all_session_ids(self) -> List[str]: + with self._lock: + return list(self._chat_sessions.keys()) + +# Global stores +github_store = MemoryStore() +slack_store = MemoryStore() diff --git a/community-chatbot/scripts/backend/main.py b/community-chatbot/scripts/backend/main.py new file mode 100644 index 00000000..831d22bd --- /dev/null +++ b/community-chatbot/scripts/backend/main.py @@ -0,0 +1,56 @@ +import uvicorn +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from backend.core.config import get_settings +from backend.api.routes import github_routes, jira_routes, slack_routes +from backend.services.jira_service import get_jira_service +from backend.services.github_service import get_github_agent +from backend.core.state import slack_store + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Eager initialization during startup + try: + print("Initializing agents... (This may take a moment)") + # Pre-warm caches by invoking factory for Singletons + get_jira_service() + get_github_agent() + + # Original slack initialized a default conversation memory list + slack_store.get_session("1") + print("Agents initialized successfully.") + except Exception as e: + print(f"Warning: Failed to fully initialize some agents (Missing env vars?): {e}") + # Server won't crash here so that other valid routes can still be tested + yield + +settings = get_settings() + +app = FastAPI( + title=settings.APP_NAME, + version=settings.APP_VERSION, + lifespan=lifespan +) + +# Unified global CORS setup +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Register routes with prefixes inside the unified API schema +app.include_router(github_routes.router, prefix=f"{settings.API_V1_STR}/github") +app.include_router(jira_routes.router, prefix=f"{settings.API_V1_STR}/jira") +app.include_router(slack_routes.router, prefix=f"{settings.API_V1_STR}/slack") + +@app.get(f"{settings.API_V1_STR}/health") +async def root_health_check(): + return {"status": "healthy"} + +if __name__ == "__main__": + uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/community-chatbot/scripts/backend/models/schemas.py b/community-chatbot/scripts/backend/models/schemas.py new file mode 100644 index 00000000..cbd3f46d --- /dev/null +++ b/community-chatbot/scripts/backend/models/schemas.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from typing import List, Dict, Any, Optional + +# Generic Chat Models (used by GitHub and Slack) +class ChatRequest(BaseModel): + message: str + session_id: str = "default" + +class ChatResponse(BaseModel): + response: str + session_id: str + +# Jira Specific Models +class JiraQueryRequest(BaseModel): + query: str + use_fallback: bool = True + +class JiraQueryResponse(BaseModel): + response: str + query_used: str + method_used: str + success: bool + +# Conversation History Models (used by Slack endpoint GET /conversations/{id}) +class ChatMessageSchema(BaseModel): + role: str + content: str + +class ConversationHistoryResponse(BaseModel): + session_id: str + messages: List[ChatMessageSchema] diff --git a/community-chatbot/scripts/backend/services/github_service.py b/community-chatbot/scripts/backend/services/github_service.py new file mode 100644 index 00000000..6a02e0ee --- /dev/null +++ b/community-chatbot/scripts/backend/services/github_service.py @@ -0,0 +1,39 @@ +import os +import re +from functools import lru_cache +from langgraph.prebuilt import create_react_agent +from langchain.chat_models import init_chat_model +from langchain_community.agent_toolkits.github.toolkit import GitHubToolkit +from langchain_community.utilities.github import GitHubAPIWrapper + +def sanitize_tool_name(name: str) -> str: + """Convert tool name to a valid function name.""" + name = name.lower().replace("'", "").replace("'", "") + name = re.sub(r"[^a-zA-Z0-9_-]+", "_", name) + return name.strip("_") + +@lru_cache() +def get_github_agent(): + """Initialize the GitHub agent with tools (Singleton).""" + required_vars = [ + "OPENAI_API_KEY", + "GITHUB_APP_ID", + "GITHUB_REPOSITORY", + "GITHUB_BRANCH", + "GITHUB_BASE_BRANCH", + "GITHUB_APP_PRIVATE_KEY" + ] + + missing_vars = [var for var in required_vars if not os.getenv(var)] + if missing_vars: + raise ValueError(f"Missing required env vars for GitHub agent: {', '.join(missing_vars)}") + + github = GitHubAPIWrapper() + toolkit = GitHubToolkit.from_github_api_wrapper(github) + + tools = toolkit.get_tools() + for tool in tools: + tool.name = sanitize_tool_name(tool.name) + + llm = init_chat_model("gpt-4o-mini", model_provider="openai") + return create_react_agent(llm, tools) diff --git a/community-chatbot/scripts/backend/services/jira_service.py b/community-chatbot/scripts/backend/services/jira_service.py new file mode 100644 index 00000000..05b7877e --- /dev/null +++ b/community-chatbot/scripts/backend/services/jira_service.py @@ -0,0 +1,158 @@ +import os +from functools import lru_cache +from langchain.agents import initialize_agent, AgentType +from langchain_community.utilities.jira import JiraAPIWrapper +from langchain_community.agent_toolkits.jira.toolkit import JiraToolkit +from langchain_openai import ChatOpenAI +from langchain.prompts import PromptTemplate + +class JiraService: + def __init__(self): + self.jira_wrapper = None + self.jira_agent = None + self.llm = None + self.jql_generation_prompt = None + self.summarization_prompt = None + self._initialize_components() + + def _initialize_components(self): + print("Initializing Jira and LangChain components...") + self.jira_wrapper = JiraAPIWrapper() + toolkit = JiraToolkit.from_jira_api_wrapper(self.jira_wrapper) + tools = toolkit.get_tools() + + self.llm = ChatOpenAI(temperature=0, model="gpt-4-turbo-preview") + + agent_kwargs = { + "prefix": """You are a specialized Jira assistant. +You MUST use the provided tools to answer questions about Jira. +Do NOT answer any questions from your own knowledge. +If a user's query seems like a general knowledge question, you MUST assume it refers to data within Jira. + +*** CRITICAL JQL RULE *** +When filtering by a field with a string value that contains spaces (like a person's name, a project name, or a summary), you MUST enclose the value in single or double quotes. +CORRECT: assignee = 'Aru Sharma' +INCORRECT: assignee = Aru Sharma +CORRECT: summary ~ '"New Login Button"' +INCORRECT: summary ~ 'New Login Button' + +Always format your response as a Thought, an Action, and an Action Input. +Begin!""" + } + + self.jira_agent = initialize_agent( + tools, + self.llm, + agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, + verbose=True, + handle_parsing_errors=True, + agent_kwargs=agent_kwargs, + ) + + self.jql_generation_prompt = PromptTemplate.from_template( + """You are an expert in Jira Query Language (JQL). Your sole task is to convert a user's natural language request into a valid JQL query. +You must only respond with the JQL query string and nothing else. + +--- Important JQL Syntax Rules --- +1. **Quoting:** Any string value containing spaces or special characters MUST be enclosed in single ('') or double ("") quotes. + - Example for a name: `assignee = 'Aru Sharma'` + - Example for a search phrase: `summary ~ '"Detailed new feature"'` +2. **Usernames:** When searching for an assignee, it is best to use their name in quotes. + +--- Examples --- +User Request: "find all tickets in the 'PROJ' project" +JQL Query: project = 'PROJ' + +User Request: "show me all open bugs in the 'Mobile' project assigned to Aru Sharma" +JQL Query: project = 'Mobile' AND issuetype = 'Bug' AND status = 'Open' AND assignee = 'Aru Sharma' + +User Request: "what were the top 5 highest priority issues created last week?" +JQL Query: created >= -7d ORDER BY priority DESC + +Now, convert the following user request into a JQL query. + +User Request: "{user_query}" +JQL Query:""" + ) + + self.summarization_prompt = PromptTemplate.from_template( + """You are a helpful assistant. The user asked the following question: + +"{user_query}" + +An AI agent attempted to answer this but failed. As a fallback, we ran a JQL query and got the following raw Jira issue data. +Please analyze this data and provide a clear, concise, and helpful answer to the user's original question. If the data seems irrelevant or empty, state that you couldn't find relevant information. + +JSON Data: +{json_data} + +Based on the data, answer the user's question. +""" + ) + + def run_agent(self, query: str): + return self.jira_agent.run(query) + + def intelligent_agent_run(self, query: str): + try: + print("--- Attempting main agent execution ---") + response = self.run_agent(query) + return { + "response": response, + "method_used": "agent", + "query_used": query, + "success": True + } + except Exception as e: + print(f"\n--- Agent failed, switching to intelligent fallback mode ---\nError: {e}\n") + print("Generating JQL from natural language...") + jql_generation_chain = self.jql_generation_prompt | self.llm + generated_jql = jql_generation_chain.invoke({"user_query": query}).content + generated_jql = generated_jql.strip().strip("'\"") + print(f"Dynamically Generated JQL: '{generated_jql}'") + + print("Executing generated JQL query...") + try: + fallback_data = self.jira_wrapper.run(mode="jql", query=generated_jql) + + if not fallback_data: + return { + "response": "The generated JQL query ran successfully but returned no issues. Please try rephrasing your request or be more specific.", + "method_used": "fallback", + "query_used": generated_jql, + "success": True + } + + print("Summarizing JQL results for the user...") + summarization_chain = self.summarization_prompt | self.llm + final_response = summarization_chain.invoke({ + "user_query": query, + "json_data": fallback_data + }).content + + return { + "response": final_response, + "method_used": "fallback", + "query_used": generated_jql, + "success": True + } + except Exception as fallback_e: + print(f"Fallback JQL execution also failed: {fallback_e}") + return { + "response": f"I'm sorry, I couldn't process your request. Both the primary agent and the fallback query failed. The last error was: {fallback_e}", + "method_used": "failed", + "query_used": query, + "success": False + } + + def direct_jql_query(self, query: str): + return self.jira_wrapper.run(mode="jql", query=query) + + def generate_jql(self, natural_query: str) -> str: + jql_generation_chain = self.jql_generation_prompt | self.llm + generated_jql = jql_generation_chain.invoke({"user_query": natural_query}).content + return generated_jql.strip().strip("'\"") + +@lru_cache() +def get_jira_service() -> JiraService: + return JiraService() diff --git a/community-chatbot/scripts/backend/services/slack_service.py b/community-chatbot/scripts/backend/services/slack_service.py new file mode 100644 index 00000000..04d49be9 --- /dev/null +++ b/community-chatbot/scripts/backend/services/slack_service.py @@ -0,0 +1,14 @@ +import os +from langchain_community.agent_toolkits import SlackToolkit +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import create_react_agent + +def get_slack_agent(): + """Create a new stateful Slack Agent instance.""" + if not os.getenv("OPENAI_API_KEY") or not os.getenv("SLACK_BOT_TOKEN"): + raise ValueError("Missing OPENAI_API_KEY or SLACK_BOT_TOKEN environment variables") + + llm = ChatOpenAI(model="gpt-4o-mini") + toolkit = SlackToolkit() + tools = toolkit.get_tools() + return create_react_agent(llm, tools) diff --git a/community-chatbot/scripts/github_agent.py b/community-chatbot/scripts/github_agent.py deleted file mode 100644 index 95c10860..00000000 --- a/community-chatbot/scripts/github_agent.py +++ /dev/null @@ -1,156 +0,0 @@ -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -from typing import List, Dict, Any -import os -from dotenv import load_dotenv -from langgraph.prebuilt import create_react_agent -from langchain.chat_models import init_chat_model -from langchain_community.agent_toolkits.github.toolkit import GitHubToolkit -from langchain_community.utilities.github import GitHubAPIWrapper -import re -import asyncio -from contextlib import asynccontextmanager - -load_dotenv() - -# os.environ['GITHUB_APP_PRIVATE_KEY'] = """ Replace with the actual private key """ - -# Pydantic models for request/response -class ChatMessage(BaseModel): - message: str - session_id: str = "default" - -class ChatResponse(BaseModel): - response: str - session_id: str - -# Global variables -agent_executor = None -chat_sessions = {} - -def sanitize_tool_name(name: str) -> str: - """Convert tool name to a valid function name.""" - name = name.lower().replace("'", "").replace("'", "") - name = re.sub(r"[^a-zA-Z0-9_-]+", "_", name) - return name.strip("_") - -def initialize_agent(): - """Initialize the GitHub agent with tools.""" - global agent_executor - - # Verify required environment variables are loaded - required_vars = [ - "OPENAI_API_KEY", - "GITHUB_APP_ID", - "GITHUB_REPOSITORY", - "GITHUB_BRANCH", - "GITHUB_BASE_BRANCH", - "GITHUB_APP_PRIVATE_KEY" - ] - - missing_vars = [var for var in required_vars if not os.getenv(var)] - if missing_vars: - raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}") - - # Initialize GitHub wrapper and toolkit - github = GitHubAPIWrapper() - toolkit = GitHubToolkit.from_github_api_wrapper(github) - - # Get and sanitize tools - tools = toolkit.get_tools() - for tool in tools: - tool.name = sanitize_tool_name(tool.name) - - # Initialize LLM and create agent - llm = init_chat_model("gpt-4o-mini", model_provider="openai") - agent_executor = create_react_agent(llm, tools) - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup - try: - initialize_agent() - print("GitHub Agent initialized successfully") - except Exception as e: - print(f"Failed to initialize agent: {e}") - raise - yield - # Shutdown - pass - -# Create FastAPI app -app = FastAPI( - title="GitHub Agent API", - description="A FastAPI backend for GitHub LangGraph agent", - version="1.0.0", - lifespan=lifespan -) - -@app.get("/") -async def root(): - return {"message": "GitHub Agent API is running!"} - -@app.post("/chat", response_model=ChatResponse) -async def chat_with_agent(request: ChatMessage): - """Chat with the GitHub agent.""" - global agent_executor, chat_sessions - - if agent_executor is None: - raise HTTPException(status_code=500, detail="Agent not initialized") - - session_id = request.session_id - user_message = request.message - - # Initialize session if it doesn't exist - if session_id not in chat_sessions: - chat_sessions[session_id] = [] - - # Add user message to chat history - chat_sessions[session_id].append(("user", user_message)) - - try: - # Stream events from the agent - events = agent_executor.stream( - {"messages": chat_sessions[session_id]}, - stream_mode="values" - ) - - # Get the last message from the agent - last_msg = None - for event in events: - last_msg = event["messages"][-1] - - if last_msg: - response_content = last_msg.content - # Add assistant response to chat history - chat_sessions[session_id].append(("assistant", response_content)) - else: - response_content = "Sorry, I couldn't process your request." - - return ChatResponse(response=response_content, session_id=session_id) - - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error processing request: {str(e)}") - -@app.get("/sessions") -async def get_sessions(): - """Get all active chat sessions.""" - return {"sessions": list(chat_sessions.keys())} - -@app.delete("/sessions/{session_id}") -async def clear_session(session_id: str): - """Clear a specific chat session.""" - if session_id in chat_sessions: - del chat_sessions[session_id] - return {"message": f"Session {session_id} cleared"} - else: - raise HTTPException(status_code=404, detail="Session not found") - -@app.get("/health") -async def health_check(): - """Health check endpoint.""" - return {"status": "healthy", "agent_initialized": agent_executor is not None} - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8003) diff --git a/community-chatbot/scripts/jira.py b/community-chatbot/scripts/jira.py deleted file mode 100644 index 0902b576..00000000 --- a/community-chatbot/scripts/jira.py +++ /dev/null @@ -1,286 +0,0 @@ -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -from typing import Optional -from contextlib import asynccontextmanager -import os -from fastapi.middleware.cors import CORSMiddleware -import json -from dotenv import load_dotenv -from langchain.agents import initialize_agent, AgentType -from langchain_community.utilities.jira import JiraAPIWrapper -from langchain_community.agent_toolkits.jira.toolkit import JiraToolkit -from langchain_openai import ChatOpenAI -from langchain.prompts import PromptTemplate - -load_dotenv() -openai_api_key = os.getenv("OPENAI_API_KEY") - -# Pydantic models -class JiraQueryRequest(BaseModel): - query: str - use_fallback: bool = True - -class JiraQueryResponse(BaseModel): - response: str - query_used: str - method_used: str - success: bool - -# Create the lifespan context manager -@asynccontextmanager -async def lifespan(app: FastAPI): - # Initialize components on startup - initialize_jira_components() - yield - # Clean up on shutdown (if needed) - -# Define app with lifespan -app = FastAPI(title="Jira Agent API", version="1.0.0", lifespan=lifespan) - -# Add CORS middleware IMMEDIATELY after app definition -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Global variables -jira_agent = None -jira_wrapper = None -llm = None -jql_generation_prompt = None -summarization_prompt = None - -def initialize_jira_components(): - """Initialize all Jira and LangChain components""" - global jira_agent, jira_wrapper, llm, jql_generation_prompt, summarization_prompt - - print("Initializing Jira and LangChain components...") - - # Initialize Jira components - jira_wrapper = JiraAPIWrapper() - toolkit = JiraToolkit.from_jira_api_wrapper(jira_wrapper) - tools = toolkit.get_tools() - - # Initialize LLM - llm = ChatOpenAI(temperature=0, model="gpt-4-turbo-preview") - - # Agent configuration - agent_kwargs = { - "prefix": """You are a specialized Jira assistant. -You MUST use the provided tools to answer questions about Jira. -Do NOT answer any questions from your own knowledge. -If a user's query seems like a general knowledge question, you MUST assume it refers to data within Jira. - -*** CRITICAL JQL RULE *** -When filtering by a field with a string value that contains spaces (like a person's name, a project name, or a summary), you MUST enclose the value in single or double quotes. -CORRECT: assignee = 'Aru Sharma' -INCORRECT: assignee = Aru Sharma -CORRECT: summary ~ '"New Login Button"' -INCORRECT: summary ~ 'New Login Button' - -Always format your response as a Thought, an Action, and an Action Input. -Begin!""" - } - - # Initialize agent - jira_agent = initialize_agent( - tools, - llm, - agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, - verbose=True, - handle_parsing_errors=True, - agent_kwargs=agent_kwargs, - ) - - # Initialize prompts - jql_generation_prompt = PromptTemplate.from_template( - """You are an expert in Jira Query Language (JQL). Your sole task is to convert a user's natural language request into a valid JQL query. -You must only respond with the JQL query string and nothing else. - ---- Important JQL Syntax Rules --- -1. **Quoting:** Any string value containing spaces or special characters MUST be enclosed in single ('') or double ("") quotes. - - Example for a name: `assignee = 'Aru Sharma'` - - Example for a search phrase: `summary ~ '"Detailed new feature"'` -2. **Usernames:** When searching for an assignee, it is best to use their name in quotes. - ---- Examples --- -User Request: "find all tickets in the 'PROJ' project" -JQL Query: project = 'PROJ' - -User Request: "show me all open bugs in the 'Mobile' project assigned to Aru Sharma" -JQL Query: project = 'Mobile' AND issuetype = 'Bug' AND status = 'Open' AND assignee = 'Aru Sharma' - -User Request: "what were the top 5 highest priority issues created last week?" -JQL Query: created >= -7d ORDER BY priority DESC - -Now, convert the following user request into a JQL query. - -User Request: "{user_query}" -JQL Query:""" - ) - - summarization_prompt = PromptTemplate.from_template( - """You are a helpful assistant. The user asked the following question: - -"{user_query}" - -An AI agent attempted to answer this but failed. As a fallback, we ran a JQL query and got the following raw Jira issue data. -Please analyze this data and provide a clear, concise, and helpful answer to the user's original question. If the data seems irrelevant or empty, state that you couldn't find relevant information. - -JSON Data: -{json_data} - -Based on the data, answer the user's question. -""" - ) - -def intelligent_agent_run(query: str): - """ - Tries to run the main agent. If it fails, it uses an LLM to generate - a JQL query from the user's input and executes that instead. - """ - try: - print("--- Attempting main agent execution ---") - response = jira_agent.run(query) - return { - "response": response, - "method_used": "agent", - "query_used": query, - "success": True - } - except Exception as e: - print("\n--- Agent failed, switching to intelligent fallback mode ---") - print(f"Error: {e}\n") - - # Use the LLM to generate a JQL query from the user's original query - print("Generating JQL from natural language...") - jql_generation_chain = jql_generation_prompt | llm - generated_jql = jql_generation_chain.invoke({"user_query": query}).content - generated_jql = generated_jql.strip().strip("'\"") - print(f"Dynamically Generated JQL: '{generated_jql}'") - - # Execute the generated JQL query - print("Executing generated JQL query...") - try: - fallback_data = jira_wrapper.run(mode="jql", query=generated_jql) - - if not fallback_data: - return { - "response": "The generated JQL query ran successfully but returned no issues. Please try rephrasing your request or be more specific.", - "method_used": "fallback", - "query_used": generated_jql, - "success": True - } - - # Use the LLM to summarize the results for the user - print("Summarizing JQL results for the user...") - summarization_chain = summarization_prompt | llm - - final_response = summarization_chain.invoke({ - "user_query": query, - "json_data": fallback_data - }).content - - return { - "response": final_response, - "method_used": "fallback", - "query_used": generated_jql, - "success": True - } - - except Exception as fallback_e: - print(f"Fallback JQL execution also failed: {fallback_e}") - return { - "response": f"I'm sorry, I couldn't process your request. Both the primary agent and the fallback query failed. The last error was: {fallback_e}", - "method_used": "failed", - "query_used": query, - "success": False - } -@app.options("/jira/query") -async def options_jira_query(): - return {} - -@app.post("/jira/query", response_model=JiraQueryResponse) -async def query_jira(request: JiraQueryRequest): - """Main endpoint to query Jira using natural language""" - try: - if not jira_agent: - raise HTTPException(status_code=500, detail="Jira agent not initialized") - - if request.use_fallback: - result = intelligent_agent_run(request.query) - else: - # Use only the main agent without fallback - try: - response = jira_agent.run(request.query) - result = { - "response": response, - "method_used": "agent", - "query_used": request.query, - "success": True - } - except Exception as e: - result = { - "response": f"Agent failed: {str(e)}", - "method_used": "agent", - "query_used": request.query, - "success": False - } - - return JiraQueryResponse(**result) - - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@app.post("/jira/direct-jql") -async def direct_jql_query(jql_query: str): - """Direct JQL query endpoint""" - try: - if not jira_wrapper: - raise HTTPException(status_code=500, detail="Jira wrapper not initialized") - - result = jira_wrapper.run(mode="jql", query=jql_query) - return { - "jql_query": jql_query, - "result": result, - "success": True - } - - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/jira/generate-jql") -async def generate_jql(natural_query: str): - """Generate JQL from natural language""" - try: - if not llm or not jql_generation_prompt: - raise HTTPException(status_code=500, detail="Components not initialized") - - jql_generation_chain = jql_generation_prompt | llm - generated_jql = jql_generation_chain.invoke({"user_query": natural_query}).content - generated_jql = generated_jql.strip().strip("'\"") - - return { - "natural_query": natural_query, - "generated_jql": generated_jql, - "success": True - } - - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/health") -async def health_check(): - """Health check endpoint""" - return { - "status": "healthy", - "jira_initialized": jira_agent is not None, - "wrapper_initialized": jira_wrapper is not None - } - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/community-chatbot/scripts/run_backend.sh b/community-chatbot/scripts/run_backend.sh index 2181b7c9..6d20179c 100644 --- a/community-chatbot/scripts/run_backend.sh +++ b/community-chatbot/scripts/run_backend.sh @@ -1,9 +1,4 @@ #!/bin/bash -python scripts/github_agent.py & - -python scripts/jira.py & - -python scripts/slack.py & - -echo "All servers started!" \ No newline at end of file +echo "Starting Unified Community AI Backend..." +uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload \ No newline at end of file diff --git a/community-chatbot/scripts/slack.py b/community-chatbot/scripts/slack.py deleted file mode 100644 index 5d77b984..00000000 --- a/community-chatbot/scripts/slack.py +++ /dev/null @@ -1,163 +0,0 @@ -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -from typing import List, Dict, Any -import asyncio -from dotenv import load_dotenv -from langchain_community.agent_toolkits import SlackToolkit -from langchain_openai import ChatOpenAI -from langgraph.prebuilt import create_react_agent -import os -from fastapi.middleware.cors import CORSMiddleware # Add this import - -load_dotenv() -openai_api_key = os.getenv("OPENAI_API_KEY") -slack_bot_token = os.getenv("SLACK_BOT_TOKEN") - -# Pydantic models for request/response -class ChatMessage(BaseModel): - role: str # "user" or "agent" - content: str - -class ChatRequest(BaseModel): - message: str - conversation_id: str = "default" # To maintain separate conversations - -class ChatResponse(BaseModel): - response: str - conversation_id: str - -# FastAPI app -app = FastAPI(title="Slack Agent API", version="1.0.0") - -# Add CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # In production, replace with your frontend URL - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Global variables to store agents and conversations -agents: Dict[str, Any] = {} -conversations: Dict[str, List] = {} - -# Initialize the agent -def initialize_agent(): - llm = ChatOpenAI(model="gpt-4o-mini") - toolkit = SlackToolkit() - tools = toolkit.get_tools() - return create_react_agent(llm, tools) - -@app.on_event("startup") -async def startup_event(): - """Initialize the agent when the server starts""" - global agents - agents["default"] = initialize_agent() - # Initialize the default conversation - conversations["1"] = [] # Add this line to create the default conversation - -@app.post("/chat", response_model=ChatResponse) -async def chat_endpoint(request: ChatRequest): - """Main chat endpoint""" - try: - # Get or create conversation - if request.conversation_id not in conversations: - conversations[request.conversation_id] = [] - - # Get the agent (create if doesn't exist) - if request.conversation_id not in agents: - agents[request.conversation_id] = initialize_agent() - - agent = agents[request.conversation_id] - conversation = conversations[request.conversation_id] - - # Add user message to conversation - conversation.append(("user", request.message)) - - # Get agent response - events = agent.stream({"messages": conversation}, stream_mode="values") - - # Process the stream to get the final response - final_response = "" - final_event = None - - for event in events: - final_event = event - message = event["messages"][-1] - if hasattr(message, 'content') and message.type == "ai": - # Check if it's not a tool call - if not (hasattr(message, 'tool_calls') and message.tool_calls): - final_response = message.content - - # Update conversation with the final state - if final_event: - conversations[request.conversation_id] = final_event["messages"] - - # If no response found, provide a default - if not final_response: - final_response = "I processed your request, but didn't generate a text response." - - if not isinstance(final_response, str): - final_response = str(final_response) - - return ChatResponse( - response=final_response, - conversation_id=request.conversation_id - ) - - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/conversations/{conversation_id}") -async def get_conversation(conversation_id: str): - """Get conversation history""" - if conversation_id not in conversations: - # Instead of 404, create an empty conversation - conversations[conversation_id] = [] - - # Convert conversation to readable format - formatted_conversation = [] - - for message in conversations[conversation_id]: - # Handle different message types - if hasattr(message, 'type') and hasattr(message, 'content'): - # LangChain message object - formatted_conversation.append({ - "role": message.type, - "content": str(message.content) - }) - elif isinstance(message, tuple) and len(message) == 2: - # Tuple format (role, content) - role, content = message - formatted_conversation.append({ - "role": role, - "content": str(content) - }) - else: - # Fallback for other formats - formatted_conversation.append({ - "role": "unknown", - "content": str(message) - }) - - return {"conversation_id": conversation_id, "messages": formatted_conversation} - -@app.delete("/conversations/{conversation_id}") -async def clear_conversation(conversation_id: str): - """Clear a specific conversation""" - if conversation_id in conversations: - conversations[conversation_id] = [] - if conversation_id in agents and conversation_id != "default": - del agents[conversation_id] - - return {"message": f"Conversation {conversation_id} cleared"} - -@app.get("/health") -async def health_check(): - """Health check endpoint""" - return {"status": "healthy"} - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file