Skip to content
Draft
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
50 changes: 50 additions & 0 deletions community-chatbot/scripts/backend/api/routes/github_routes.py
Original file line number Diff line number Diff line change
@@ -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")
53 changes: 53 additions & 0 deletions community-chatbot/scripts/backend/api/routes/jira_routes.py
Original file line number Diff line number Diff line change
@@ -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))
62 changes: 62 additions & 0 deletions community-chatbot/scripts/backend/api/routes/slack_routes.py
Original file line number Diff line number Diff line change
@@ -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"}
34 changes: 34 additions & 0 deletions community-chatbot/scripts/backend/core/config.py
Original file line number Diff line number Diff line change
@@ -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()
56 changes: 56 additions & 0 deletions community-chatbot/scripts/backend/core/state.py
Original file line number Diff line number Diff line change
@@ -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()
56 changes: 56 additions & 0 deletions community-chatbot/scripts/backend/main.py
Original file line number Diff line number Diff line change
@@ -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)
31 changes: 31 additions & 0 deletions community-chatbot/scripts/backend/models/schemas.py
Original file line number Diff line number Diff line change
@@ -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]
39 changes: 39 additions & 0 deletions community-chatbot/scripts/backend/services/github_service.py
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading