726 lines
33 KiB
Python
726 lines
33 KiB
Python
"""
|
|||
|
|
Copyright 2024-2026 ReplyPier
|
||
|
|
|
||
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
you may not use this file except in compliance with the License.
|
||
|
|
You may obtain a copy of the License at
|
||
|
|
|
||
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
|
||
|
|
Unless required by applicable law or agreed to in writing, software
|
||
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
|
See the License for the specific language governing permissions and
|
||
|
|
limitations under the License.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Optional, Dict, Any, List
|
||
|
|
from agno.tools import Toolkit
|
||
|
|
from app.core.config import settings
|
||
|
|
from app.core.logger import get_logger
|
||
|
|
from app.services.jira import (
|
||
|
|
JiraService,
|
||
|
|
jira_accessible_resources_url,
|
||
|
|
jira_rest_url,
|
||
|
|
jira_token_url,
|
||
|
|
)
|
||
|
|
from app.repositories.session_to_agent import SessionToAgentRepository
|
||
|
|
from app.api.jira import CreateJiraIssueModel
|
||
|
|
from app.database import SessionLocal
|
||
|
|
from app.models.organization import Organization
|
||
|
|
from app.models.agent import Agent
|
||
|
|
from uuid import UUID
|
||
|
|
|
||
|
|
logger = get_logger(__name__)
|
||
|
|
|
||
|
|
class JiraTools(Toolkit):
|
||
|
|
def __init__(self, agent_id: str, org_id: str, session_id: str):
|
||
|
|
super().__init__(name="jira_tools")
|
||
|
|
self.agent_id = agent_id
|
||
|
|
self.org_id = org_id
|
||
|
|
self.session_id = session_id
|
||
|
|
self.jira_service = JiraService()
|
||
|
|
|
||
|
|
# Register the functions
|
||
|
|
self.register(self.create_jira_ticket)
|
||
|
|
self.register(self.get_ticket_status)
|
||
|
|
self.register(self.check_existing_ticket)
|
||
|
|
|
||
|
|
def create_jira_ticket(
|
||
|
|
self,
|
||
|
|
summary: str,
|
||
|
|
description: str,
|
||
|
|
priority: Optional[str] = "Medium"
|
||
|
|
) -> str:
|
||
|
|
"""
|
||
|
|
Create a Jira ticket for the current session or update an existing one.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
summary (str): The summary/title of the ticket.
|
||
|
|
description (str): The detailed description of the ticket.
|
||
|
|
priority (str, optional): The priority level of the ticket (Highest, High, Medium, Low, Lowest). Defaults to "Medium".
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: JSON string with information about the created or updated ticket including ticket ID and status.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
import json
|
||
|
|
import requests
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
|
||
|
|
# Check if a ticket already exists for this session
|
||
|
|
existing_ticket_str = self.check_existing_ticket()
|
||
|
|
existing_ticket = json.loads(existing_ticket_str)
|
||
|
|
existing_ticket_id = None
|
||
|
|
is_update = False
|
||
|
|
|
||
|
|
if existing_ticket.get("exists", False):
|
||
|
|
existing_ticket_id = existing_ticket.get("ticket_id")
|
||
|
|
is_update = True
|
||
|
|
logger.info(f"Found existing ticket {existing_ticket_id} for session {self.session_id}, will update it")
|
||
|
|
|
||
|
|
# Use context manager for database operations
|
||
|
|
with SessionLocal() as db:
|
||
|
|
# Get the agent's Jira configuration
|
||
|
|
try:
|
||
|
|
agent_uuid = UUID(str(self.agent_id))
|
||
|
|
agent = db.query(Agent).filter(Agent.id == agent_uuid).first()
|
||
|
|
except (ValueError, TypeError) as e:
|
||
|
|
logger.error(f"Invalid agent ID format: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Invalid agent ID format: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
if not agent:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "Agent not found"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Get the organization
|
||
|
|
try:
|
||
|
|
org_uuid = UUID(str(self.org_id))
|
||
|
|
organization = db.query(Organization).filter(
|
||
|
|
Organization.id == org_uuid
|
||
|
|
).first()
|
||
|
|
except (ValueError, TypeError) as e:
|
||
|
|
logger.error(f"Invalid organization ID format: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Invalid organization ID format: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
if not organization:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "Organization not found"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Get the agent's Jira configuration
|
||
|
|
from app.models.jira import AgentJiraConfig
|
||
|
|
jira_config = db.query(AgentJiraConfig).filter(
|
||
|
|
AgentJiraConfig.agent_id == str(self.agent_id)
|
||
|
|
).first()
|
||
|
|
|
||
|
|
if not jira_config or not jira_config.enabled:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "Jira integration is not enabled for this agent"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Create the issue data
|
||
|
|
issue_data = CreateJiraIssueModel(
|
||
|
|
projectKey=jira_config.project_key,
|
||
|
|
issueTypeId=jira_config.issue_type_id,
|
||
|
|
summary=summary,
|
||
|
|
description=description,
|
||
|
|
priority=priority,
|
||
|
|
chatId=str(self.session_id) # Ensure session_id is a string
|
||
|
|
)
|
||
|
|
|
||
|
|
# Get the Jira token
|
||
|
|
from app.models.jira import JiraToken
|
||
|
|
token = db.query(JiraToken).filter(
|
||
|
|
JiraToken.organization_id == organization.id
|
||
|
|
).first()
|
||
|
|
|
||
|
|
if not token:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "No Jira connection found"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Check if token is valid and refresh if needed
|
||
|
|
is_valid = self.jira_service.validate_token(token)
|
||
|
|
if not is_valid:
|
||
|
|
# Implement synchronous token refresh
|
||
|
|
try:
|
||
|
|
client_id = settings.JIRA_CLIENT_ID
|
||
|
|
client_secret = settings.JIRA_CLIENT_SECRET
|
||
|
|
|
||
|
|
if not client_id or not client_secret:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "Jira client credentials not configured"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Refresh token using requests
|
||
|
|
refresh_data = {
|
||
|
|
"grant_type": "refresh_token",
|
||
|
|
"client_id": client_id,
|
||
|
|
"client_secret": client_secret,
|
||
|
|
"refresh_token": token.refresh_token
|
||
|
|
}
|
||
|
|
|
||
|
|
refresh_response = requests.post(
|
||
|
|
jira_token_url(),
|
||
|
|
data=refresh_data
|
||
|
|
)
|
||
|
|
|
||
|
|
if refresh_response.status_code != 200:
|
||
|
|
logger.error(f"Failed to refresh token: {refresh_response.text}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to refresh Jira token: {refresh_response.text}"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Parse response and update token
|
||
|
|
token_data = refresh_response.json()
|
||
|
|
|
||
|
|
# Update token in database
|
||
|
|
token.access_token = token_data["access_token"]
|
||
|
|
token.refresh_token = token_data.get("refresh_token", token.refresh_token)
|
||
|
|
token.token_type = token_data["token_type"]
|
||
|
|
token.expires_at = datetime.utcnow() + timedelta(seconds=token_data["expires_in"])
|
||
|
|
|
||
|
|
db.commit()
|
||
|
|
logger.info("Successfully refreshed Jira token")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error refreshing Jira token: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Error refreshing Jira token: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Create the issue directly using the REST API
|
||
|
|
headers = {
|
||
|
|
"Authorization": f"Bearer {token.access_token}",
|
||
|
|
"Accept": "application/json",
|
||
|
|
"Content-Type": "application/json"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Check if priority field is available for this project and issue type
|
||
|
|
priority_available = False
|
||
|
|
try:
|
||
|
|
# Get the create metadata to check available fields
|
||
|
|
create_meta_url = jira_rest_url(str(token.cloud_id), f"issue/createmeta?projectKeys={issue_data.projectKey}&issuetypeIds={issue_data.issueTypeId}&expand=projects.issuetypes.fields")
|
||
|
|
meta_response = requests.get(create_meta_url, headers=headers)
|
||
|
|
|
||
|
|
if meta_response.status_code == 200:
|
||
|
|
meta_data = meta_response.json()
|
||
|
|
# Navigate through the response to find the priority field
|
||
|
|
if meta_data.get("projects") and len(meta_data["projects"]) > 0:
|
||
|
|
project = meta_data["projects"][0]
|
||
|
|
if project.get("issuetypes") and len(project["issuetypes"]) > 0:
|
||
|
|
issue_type = project["issuetypes"][0]
|
||
|
|
if issue_type.get("fields") and "priority" in issue_type["fields"]:
|
||
|
|
priority_available = True
|
||
|
|
logger.info("Priority field is available for this project and issue type")
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Failed to check if priority field is available: {e}")
|
||
|
|
# Continue without setting priority
|
||
|
|
|
||
|
|
# Prepare the issue data for the API
|
||
|
|
api_data = {
|
||
|
|
"fields": {
|
||
|
|
"summary": issue_data.summary,
|
||
|
|
"description": {
|
||
|
|
"type": "doc",
|
||
|
|
"version": 1,
|
||
|
|
"content": [
|
||
|
|
{
|
||
|
|
"type": "paragraph",
|
||
|
|
"content": [
|
||
|
|
{
|
||
|
|
"type": "text",
|
||
|
|
"text": issue_data.description
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Only include project and issuetype for new tickets, not updates
|
||
|
|
if not is_update:
|
||
|
|
api_data["fields"]["project"] = {"key": issue_data.projectKey}
|
||
|
|
api_data["fields"]["issuetype"] = {"id": issue_data.issueTypeId}
|
||
|
|
|
||
|
|
# Add priority if provided and available
|
||
|
|
if priority_available and issue_data.priority:
|
||
|
|
# Map string priority to Jira priority
|
||
|
|
priority_map = {
|
||
|
|
"Highest": "1",
|
||
|
|
"High": "2",
|
||
|
|
"Medium": "3",
|
||
|
|
"Low": "4",
|
||
|
|
"Lowest": "5"
|
||
|
|
}
|
||
|
|
priority_id = priority_map.get(issue_data.priority, "3") # Default to Medium
|
||
|
|
api_data["fields"]["priority"] = {"id": priority_id}
|
||
|
|
logger.info(f"Setting priority to {issue_data.priority} (ID: {priority_id})")
|
||
|
|
else:
|
||
|
|
logger.info("Priority field is not available or not provided, skipping")
|
||
|
|
|
||
|
|
try:
|
||
|
|
if is_update:
|
||
|
|
# Get the existing ticket details first to get the current description
|
||
|
|
existing_ticket_url = jira_rest_url(str(token.cloud_id), f"issue/{existing_ticket_id}")
|
||
|
|
existing_ticket_response = requests.get(existing_ticket_url, headers=headers)
|
||
|
|
|
||
|
|
if existing_ticket_response.status_code == 200:
|
||
|
|
existing_ticket_data = existing_ticket_response.json()
|
||
|
|
existing_description = ""
|
||
|
|
|
||
|
|
# Extract the existing description text
|
||
|
|
if existing_ticket_data.get("fields", {}).get("description"):
|
||
|
|
description_field = existing_ticket_data["fields"]["description"]
|
||
|
|
if isinstance(description_field, dict) and description_field.get("content"):
|
||
|
|
# Extract text from Atlassian Document Format
|
||
|
|
for content in description_field.get("content", []):
|
||
|
|
if content.get("type") == "paragraph" and content.get("content"):
|
||
|
|
for text_node in content.get("content", []):
|
||
|
|
if text_node.get("type") == "text":
|
||
|
|
existing_description += text_node.get("text", "")
|
||
|
|
existing_description += "\n\n"
|
||
|
|
elif isinstance(description_field, str):
|
||
|
|
existing_description = description_field
|
||
|
|
|
||
|
|
# Append the new description to the existing one
|
||
|
|
combined_description = existing_description.strip()
|
||
|
|
if combined_description:
|
||
|
|
combined_description += "\n\n--- Update " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " ---\n\n"
|
||
|
|
combined_description += description
|
||
|
|
|
||
|
|
# Update the description in the API data
|
||
|
|
api_data["fields"]["description"] = {
|
||
|
|
"type": "doc",
|
||
|
|
"version": 1,
|
||
|
|
"content": [
|
||
|
|
{
|
||
|
|
"type": "paragraph",
|
||
|
|
"content": [
|
||
|
|
{
|
||
|
|
"type": "text",
|
||
|
|
"text": combined_description
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(f"Appending new description to existing ticket {existing_ticket_id}")
|
||
|
|
else:
|
||
|
|
error_msg = f"Failed to get existing ticket details: {existing_ticket_response.text}"
|
||
|
|
logger.warning(error_msg)
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": error_msg
|
||
|
|
})
|
||
|
|
|
||
|
|
# Update existing ticket
|
||
|
|
url = jira_rest_url(str(token.cloud_id), f"issue/{existing_ticket_id}")
|
||
|
|
response = requests.put(url, headers=headers, json=api_data)
|
||
|
|
|
||
|
|
if response.status_code not in (200, 204):
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to update Jira ticket: {response.text}"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Get updated ticket details
|
||
|
|
get_response = requests.get(url, headers=headers)
|
||
|
|
if get_response.status_code != 200:
|
||
|
|
logger.warning(f"Failed to get updated ticket details: {get_response.text}")
|
||
|
|
issue_result = {"key": existing_ticket_id}
|
||
|
|
else:
|
||
|
|
issue_result = get_response.json()
|
||
|
|
|
||
|
|
action_message = "updated"
|
||
|
|
else:
|
||
|
|
# Create new ticket
|
||
|
|
url = jira_rest_url(str(token.cloud_id), "issue")
|
||
|
|
response = requests.post(url, headers=headers, json=api_data)
|
||
|
|
|
||
|
|
if response.status_code not in (200, 201):
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to create Jira ticket: {response.text}"
|
||
|
|
})
|
||
|
|
|
||
|
|
issue_result = response.json()
|
||
|
|
action_message = "created"
|
||
|
|
|
||
|
|
# Get the site URL from the token
|
||
|
|
site_url = token.site_url
|
||
|
|
if not site_url:
|
||
|
|
# Try to get the domain from the token
|
||
|
|
domain = token.domain
|
||
|
|
if domain:
|
||
|
|
site_url = f"https://{domain}.atlassian.net"
|
||
|
|
else:
|
||
|
|
# Try to get the site URL from the cloud resources
|
||
|
|
try:
|
||
|
|
resources_url = jira_accessible_resources_url()
|
||
|
|
resources_response = requests.get(resources_url, headers={
|
||
|
|
"Authorization": f"Bearer {token.access_token}",
|
||
|
|
"Accept": "application/json"
|
||
|
|
})
|
||
|
|
|
||
|
|
if resources_response.status_code == 200:
|
||
|
|
resources = resources_response.json()
|
||
|
|
for resource in resources:
|
||
|
|
if resource.get("id") == str(token.cloud_id):
|
||
|
|
site_url = resource.get("url", "")
|
||
|
|
break
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Failed to get site URL from resources: {e}")
|
||
|
|
|
||
|
|
# Construct the ticket URL
|
||
|
|
ticket_key = issue_result["key"]
|
||
|
|
if site_url:
|
||
|
|
# Remove trailing slash if present
|
||
|
|
if site_url.endswith("/"):
|
||
|
|
site_url = site_url[:-1]
|
||
|
|
|
||
|
|
# Construct the URL using the site URL
|
||
|
|
ticket_url = f"{site_url}/browse/{ticket_key}"
|
||
|
|
else:
|
||
|
|
# Get the organization name for the URL
|
||
|
|
org_name = organization.name.lower().replace(" ", "-") if organization.name else "organization"
|
||
|
|
# Use the Atlassian Cloud URL format
|
||
|
|
ticket_url = f"https://{org_name}.atlassian.net/browse/{ticket_key}"
|
||
|
|
|
||
|
|
# Update the session with ticket information
|
||
|
|
session_repo = SessionToAgentRepository(db)
|
||
|
|
try:
|
||
|
|
# Use the combined description for updates, or just the new description for new tickets
|
||
|
|
final_description = combined_description if is_update and 'combined_description' in locals() else description
|
||
|
|
|
||
|
|
session_repo.update_session(
|
||
|
|
str(self.session_id), # Ensure session_id is a string
|
||
|
|
{
|
||
|
|
"ticket_id": issue_result["key"],
|
||
|
|
"ticket_status": "Updated" if is_update else "Created",
|
||
|
|
"ticket_summary": summary,
|
||
|
|
"ticket_description": final_description,
|
||
|
|
"integration_type": "JIRA",
|
||
|
|
"ticket_priority": priority if priority_available else None,
|
||
|
|
"ticket_url": ticket_url
|
||
|
|
}
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to update session with ticket information: {e}")
|
||
|
|
# Continue since the ticket was created/updated successfully
|
||
|
|
|
||
|
|
return json.dumps({
|
||
|
|
"success": True,
|
||
|
|
"message": f"Ticket {action_message} successfully: {issue_result['key']}",
|
||
|
|
"ticket_id": issue_result["key"],
|
||
|
|
"ticket_url": ticket_url,
|
||
|
|
"was_updated": is_update
|
||
|
|
})
|
||
|
|
|
||
|
|
except requests.exceptions.RequestException as e:
|
||
|
|
logger.error(f"Request error {'updating' if is_update else 'creating'} Jira ticket: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Request error {'updating' if is_update else 'creating'} Jira ticket: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error {'updating' if 'is_update' in locals() and is_update else 'creating'} Jira ticket: {str(e)}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Error {'updating' if 'is_update' in locals() and is_update else 'creating'} Jira ticket: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
def get_ticket_status(self, ticket_id: Optional[str] = None) -> str:
|
||
|
|
"""
|
||
|
|
Get the status of a Jira ticket.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
ticket_id (str, optional): The ID of the ticket to check. If not provided, will check for a ticket associated with the current session.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: JSON string with information about the ticket including its status.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
import json
|
||
|
|
import requests
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
|
||
|
|
# Use context manager for database operations
|
||
|
|
with SessionLocal() as db:
|
||
|
|
# If no ticket ID is provided, check if there's a ticket for this session
|
||
|
|
if not ticket_id:
|
||
|
|
session_repo = SessionToAgentRepository(db)
|
||
|
|
try:
|
||
|
|
session = session_repo.get_session(str(self.session_id))
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to get session: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to get session: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
if not session or not session.ticket_id:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "No ticket found for this session"
|
||
|
|
})
|
||
|
|
|
||
|
|
ticket_id = session.ticket_id
|
||
|
|
|
||
|
|
# Get the organization
|
||
|
|
try:
|
||
|
|
org_uuid = UUID(str(self.org_id))
|
||
|
|
organization = db.query(Organization).filter(
|
||
|
|
Organization.id == org_uuid
|
||
|
|
).first()
|
||
|
|
except (ValueError, TypeError) as e:
|
||
|
|
logger.error(f"Invalid organization ID format: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Invalid organization ID format: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
if not organization:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "Organization not found"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Get the ticket status from Jira
|
||
|
|
from app.models.jira import JiraToken
|
||
|
|
token = db.query(JiraToken).filter(
|
||
|
|
JiraToken.organization_id == organization.id
|
||
|
|
).first()
|
||
|
|
|
||
|
|
if not token:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "No Jira connection found"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Check if token is valid and refresh if needed
|
||
|
|
is_valid = self.jira_service.validate_token(token)
|
||
|
|
if not is_valid:
|
||
|
|
# Implement synchronous token refresh
|
||
|
|
try:
|
||
|
|
client_id = settings.JIRA_CLIENT_ID
|
||
|
|
client_secret = settings.JIRA_CLIENT_SECRET
|
||
|
|
|
||
|
|
if not client_id or not client_secret:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": "Jira client credentials not configured"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Refresh token using requests
|
||
|
|
refresh_data = {
|
||
|
|
"grant_type": "refresh_token",
|
||
|
|
"client_id": client_id,
|
||
|
|
"client_secret": client_secret,
|
||
|
|
"refresh_token": token.refresh_token
|
||
|
|
}
|
||
|
|
|
||
|
|
refresh_response = requests.post(
|
||
|
|
jira_token_url(),
|
||
|
|
data=refresh_data
|
||
|
|
)
|
||
|
|
|
||
|
|
if refresh_response.status_code != 200:
|
||
|
|
logger.error(f"Failed to refresh token: {refresh_response.text}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to refresh Jira token: {refresh_response.text}"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Parse response and update token
|
||
|
|
token_data = refresh_response.json()
|
||
|
|
|
||
|
|
# Update token in database
|
||
|
|
token.access_token = token_data["access_token"]
|
||
|
|
token.refresh_token = token_data.get("refresh_token", token.refresh_token)
|
||
|
|
token.token_type = token_data["token_type"]
|
||
|
|
token.expires_at = datetime.utcnow() + timedelta(seconds=token_data["expires_in"])
|
||
|
|
|
||
|
|
db.commit()
|
||
|
|
logger.info("Successfully refreshed Jira token")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error refreshing Jira token: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Error refreshing Jira token: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Get the issue
|
||
|
|
headers = {
|
||
|
|
"Authorization": f"Bearer {token.access_token}",
|
||
|
|
"Accept": "application/json"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Ensure ticket_id is a string before using it in the URL
|
||
|
|
ticket_id_str = str(ticket_id) if ticket_id else ""
|
||
|
|
|
||
|
|
url = jira_rest_url(str(token.cloud_id), f"issue/{ticket_id_str}")
|
||
|
|
|
||
|
|
try:
|
||
|
|
response = requests.get(url, headers=headers)
|
||
|
|
except requests.exceptions.RequestException as e:
|
||
|
|
logger.error(f"Failed to make request to Jira API: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to make request to Jira API: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
if response.status_code != 200:
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to get Jira ticket: {response.text}"
|
||
|
|
})
|
||
|
|
|
||
|
|
try:
|
||
|
|
issue_data = response.json()
|
||
|
|
except ValueError as e:
|
||
|
|
logger.error(f"Failed to parse Jira response: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Failed to parse Jira response: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Get the site URL from the token
|
||
|
|
site_url = token.site_url
|
||
|
|
if not site_url:
|
||
|
|
# Try to get the domain from the token
|
||
|
|
domain = token.domain
|
||
|
|
if domain:
|
||
|
|
site_url = f"https://{domain}.atlassian.net"
|
||
|
|
else:
|
||
|
|
# Try to get the site URL from the cloud resources
|
||
|
|
try:
|
||
|
|
resources_url = jira_accessible_resources_url()
|
||
|
|
resources_response = requests.get(resources_url, headers={
|
||
|
|
"Authorization": f"Bearer {token.access_token}",
|
||
|
|
"Accept": "application/json"
|
||
|
|
})
|
||
|
|
|
||
|
|
if resources_response.status_code == 200:
|
||
|
|
resources = resources_response.json()
|
||
|
|
for resource in resources:
|
||
|
|
if resource.get("id") == str(token.cloud_id):
|
||
|
|
site_url = resource.get("url", "")
|
||
|
|
break
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Failed to get site URL from resources: {e}")
|
||
|
|
|
||
|
|
# Construct the ticket URL
|
||
|
|
if site_url:
|
||
|
|
# Remove trailing slash if present
|
||
|
|
if site_url.endswith("/"):
|
||
|
|
site_url = site_url[:-1]
|
||
|
|
|
||
|
|
# Construct the URL using the site URL
|
||
|
|
ticket_url = f"{site_url}/browse/{ticket_id_str}"
|
||
|
|
else:
|
||
|
|
# Get the organization name for the URL
|
||
|
|
org_name = organization.name.lower().replace(" ", "-") if organization.name else "organization"
|
||
|
|
# Use the Atlassian Cloud URL format
|
||
|
|
ticket_url = f"https://{org_name}.atlassian.net/browse/{ticket_id_str}"
|
||
|
|
|
||
|
|
return json.dumps({
|
||
|
|
"success": True,
|
||
|
|
"ticket_id": ticket_id_str,
|
||
|
|
"ticket_status": issue_data.get("fields", {}).get("status", {}).get("name", "Unknown"),
|
||
|
|
"ticket_summary": issue_data.get("fields", {}).get("summary", ""),
|
||
|
|
"ticket_description": issue_data.get("fields", {}).get("description", ""),
|
||
|
|
"ticket_priority": issue_data.get("fields", {}).get("priority", {}).get("name", ""),
|
||
|
|
"ticket_url": ticket_url
|
||
|
|
})
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting Jira ticket status: {str(e)}")
|
||
|
|
return json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"message": f"Error getting Jira ticket status: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
def check_existing_ticket(self) -> str:
|
||
|
|
"""
|
||
|
|
Check if a Jira ticket already exists for the current session.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: JSON string with information about whether a ticket exists.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
import json
|
||
|
|
|
||
|
|
# Use context manager for database operations
|
||
|
|
with SessionLocal() as db:
|
||
|
|
# Get the session
|
||
|
|
session_repo = SessionToAgentRepository(db)
|
||
|
|
try:
|
||
|
|
session = session_repo.get_session(str(self.session_id))
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to get session: {e}")
|
||
|
|
return json.dumps({
|
||
|
|
"exists": False,
|
||
|
|
"message": f"Failed to get session: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
if not session or not session.ticket_id:
|
||
|
|
return json.dumps({
|
||
|
|
"exists": False,
|
||
|
|
"message": "No ticket found for this session"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Get the ticket status
|
||
|
|
ticket_status_str = self.get_ticket_status(session.ticket_id)
|
||
|
|
ticket_status = json.loads(ticket_status_str)
|
||
|
|
|
||
|
|
if not ticket_status.get("success", False):
|
||
|
|
return json.dumps({
|
||
|
|
"exists": True,
|
||
|
|
"ticket_id": session.ticket_id,
|
||
|
|
"ticket_status": session.ticket_status,
|
||
|
|
"ticket_summary": session.ticket_summary,
|
||
|
|
"ticket_description": session.ticket_description,
|
||
|
|
"ticket_priority": session.ticket_priority,
|
||
|
|
"message": "Ticket exists in the database but could not be retrieved from Jira"
|
||
|
|
})
|
||
|
|
|
||
|
|
return json.dumps({
|
||
|
|
"exists": True,
|
||
|
|
"ticket_id": ticket_status.get("ticket_id"),
|
||
|
|
"ticket_status": ticket_status.get("ticket_status"),
|
||
|
|
"ticket_summary": ticket_status.get("ticket_summary"),
|
||
|
|
"ticket_description": ticket_status.get("ticket_description"),
|
||
|
|
"ticket_priority": ticket_status.get("ticket_priority"),
|
||
|
|
"ticket_url": ticket_status.get("ticket_url"),
|
||
|
|
"message": "Ticket exists"
|
||
|
|
})
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error checking existing ticket: {str(e)}")
|
||
|
|
return json.dumps({
|
||
|
|
"exists": False,
|
||
|
|
"message": f"Error checking existing ticket: {str(e)}"
|
||
|
|
})
|
||
|
|
|
||
|
|
# Private async methods that will be called by the public synchronous methods
|
||
|
|
|
||
|
|
# Note: We've removed the async _check_existing_ticket method and replaced it with a synchronous implementation
|