Component: JiraAgent - Natural Language Jira Interface
Module: gaia.agents.jira.agent
Inherits: Agent
Model: Qwen3-Coder-30B-A3B-Instruct-GGUF (default)
Overview
JiraAgent provides a natural language interface to Jira with automatic configuration discovery. It adapts to any Jira instance by discovering projects, issue types, statuses, and priorities dynamically, then translates natural language to JQL queries and API calls.
Key Features:
- Automatic Jira instance discovery
- Natural language to JQL translation
- Async API operations (aiohttp)
- Dynamic system prompt generation
- WebUI integration (Electron)
- Portable across Jira instances
Requirements
Functional Requirements
-
Jira Discovery
- Discover projects (keys, names)
- Discover issue types (excluding subtasks)
- Discover statuses
- Discover priorities
- Cache discovered configuration
-
Search Operations
- Natural language → JQL translation
- Execute JQL searches via API
- Parse and format results
- Handle pagination
-
CRUD Operations
- Create issues with validation
- Update issues (summary, description, priority, status)
- Proper error handling for invalid fields
-
Configuration Management
- Accept pre-discovered config
- Initialize() method for discovery
- get_config() to retrieve cached config
API Specification
JiraAgent Class
class JiraAgent(Agent):
"""Intelligent Jira agent with automatic configuration discovery."""
def __init__(
self,
jira_config: Dict[str, Any] = None,
max_steps: int = 10,
model_id: str = "Qwen3-Coder-30B-A3B-Instruct-GGUF",
silent_mode: bool = False,
**kwargs
):
"""
Initialize Jira agent.
Args:
jira_config: Pre-discovered Jira configuration {
"projects": [{"key": "PROJ", "name": "Project Name"}],
"issue_types": ["Bug", "Task", "Story"],
"statuses": ["To Do", "In Progress", "Done"],
"priorities": ["Highest", "High", "Medium", "Low"]
}
"""
pass
def initialize(self) -> Dict[str, Any]:
"""
Discover and cache Jira configuration.
Returns:
Discovered configuration dict
Raises:
Exception: If credentials invalid or API fails
"""
pass
def get_config(self) -> Optional[Dict[str, Any]]:
"""Get cached Jira configuration."""
pass
# Tools
@tool
def jira_search(
jql: str = "created >= -30d ORDER BY updated DESC",
max_results: int = None,
fields: str = None
) -> Dict[str, Any]:
"""Search Jira issues using JQL query."""
pass
@tool
def jira_create(
summary: str,
description: str = "",
issue_type: str = "Task",
priority: str = None,
project: str = None
) -> Dict[str, Any]:
"""Create new Jira issue."""
pass
@tool
def jira_update(
issue_key: str,
summary: str = None,
description: str = None,
priority: str = None,
status: str = None
) -> Dict[str, Any]:
"""Update existing Jira issue."""
pass
Environment Variables
# Required
ATLASSIAN_SITE_URL=https://company.atlassian.net
ATLASSIAN_API_KEY=your_api_token_here
ATLASSIAN_USER_EMAIL=[email protected]
Implementation Details
Discovery Process
async def _discover_jira_config(self) -> Dict[str, Any]:
"""Discover Jira instance configuration via API."""
site_url, api_key, user_email = self._get_jira_credentials()
auth_header = base64.b64encode(f"{user_email}:{api_key}".encode()).decode()
headers = {
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/json"
}
config = {
"projects": [],
"issue_types": [],
"statuses": [],
"priorities": []
}
async with aiohttp.ClientSession() as session:
# Get projects
async with session.get(f"{site_url}/rest/api/3/project", headers=headers) as resp:
projects = await resp.json()
config["projects"] = [{"key": p["key"], "name": p["name"]} for p in projects]
# Get issue types (exclude subtasks)
async with session.get(f"{site_url}/rest/api/3/issuetype", headers=headers) as resp:
types = await resp.json()
config["issue_types"] = [t["name"] for t in types if not t.get("subtask")]
# Get statuses
async with session.get(f"{site_url}/rest/api/3/status", headers=headers) as resp:
statuses = await resp.json()
config["statuses"] = [s["name"] for s in statuses]
# Get priorities
async with session.get(f"{site_url}/rest/api/3/priority", headers=headers) as resp:
priorities = await resp.json()
config["priorities"] = [p["name"] for p in priorities]
return config
Dynamic System Prompt
def _get_system_prompt(self) -> str:
"""Generate system prompt with discovered configuration."""
prompt = """You are a Jira assistant that responds ONLY in JSON format.
**JQL Basics:**
- Current user: assignee = currentUser()
- By key: key = "PROJ-123"
- Operators: AND, OR, NOT"""
# Add discovered configuration
if self._jira_config:
if self._jira_config.get("projects"):
keys = [p["key"] for p in self._jira_config["projects"]]
prompt += f"\n- Projects: {', '.join(keys)}"
if self._jira_config.get("issue_types"):
prompt += f"\n- Types: {', '.join(self._jira_config['issue_types'])}"
if self._jira_config.get("priorities"):
prompt += f"\n- Priorities: {', '.join(self._jira_config['priorities'])}"
if self._jira_config.get("statuses"):
prompt += f"\n- Statuses: {', '.join(self._jira_config['statuses'])}"
prompt += """
**JQL QUOTING RULES:**
- Single words: NO quotes → status = Done
- Multiple words: DOUBLE quotes → status = "In Progress"
- NEVER use single quotes
- Functions: NO quotes → assignee = currentUser()"""
return prompt
JQL Search Execution
async def _execute_jira_search_async(
self,
jql: str,
max_results: int = None,
fields: str = None
) -> Dict[str, Any]:
"""Execute Jira search API call."""
site_url, api_key, user_email = self._get_jira_credentials()
auth_header = base64.b64encode(f"{user_email}:{api_key}".encode()).decode()
headers = {
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/json"
}
params = {"jql": jql}
if max_results:
params["maxResults"] = max_results
if fields:
params["fields"] = fields
else:
params["fields"] = "key,summary,status,priority,issuetype,assignee"
async with aiohttp.ClientSession() as session:
url = f"{site_url}/rest/api/3/search/jql"
async with session.get(url, headers=headers, params=params) as response:
response.raise_for_status()
data = await response.json()
issues = []
for issue in data.get("issues", []):
fields = issue.get("fields", {})
issues.append({
"key": issue.get("key"),
"summary": fields.get("summary"),
"status": fields.get("status", {}).get("name"),
"priority": fields.get("priority", {}).get("name"),
"type": fields.get("issuetype", {}).get("name"),
"assignee": fields.get("assignee", {}).get("displayName", "Unassigned")
})
return {
"issues": issues,
"total": data.get("total", len(issues)),
"jql": jql
}
Testing Requirements
Unit Tests
# tests/agents/test_jira_agent.py
def test_jira_agent_initialization():
"""Test JiraAgent initializes."""
config = {
"projects": [{"key": "TEST", "name": "Test Project"}],
"issue_types": ["Bug", "Task"],
"statuses": ["To Do", "Done"],
"priorities": ["High", "Low"]
}
agent = JiraAgent(jira_config=config, silent_mode=True)
assert agent.get_config() == config
@pytest.mark.asyncio
async def test_jira_search():
"""Test JQL search execution."""
agent = JiraAgent(silent_mode=True)
# Mock aiohttp session
result = await agent._execute_jira_search_async("assignee = currentUser()")
assert "issues" in result
assert "total" in result
assert "jql" in result
def test_dynamic_system_prompt():
"""Test system prompt includes discovered config."""
config = {
"projects": [{"key": "PROJ", "name": "Project"}],
"issue_types": ["Bug"],
"priorities": ["High"]
}
agent = JiraAgent(jira_config=config, silent_mode=True)
prompt = agent._get_system_prompt()
assert "PROJ" in prompt
assert "Bug" in prompt
assert "High" in prompt
Usage Examples
Example 1: CLI Usage
# Set credentials
export ATLASSIAN_SITE_URL=https://company.atlassian.net
export ATLASSIAN_API_KEY=your_token
export ATLASSIAN_USER_EMAIL=you@company.com
# Use agent
gaia jira "Show me high priority bugs assigned to me"
Example 2: Python API
from gaia.agents.jira.agent import JiraAgent
# Initialize and discover
agent = JiraAgent()
config = agent.initialize()
print(f"Found {len(config['projects'])} projects")
# Natural language query
result = agent.process_query("Show my open issues")
print(result["result"])
# Use pre-discovered config (faster initialization)
config = {
"projects": [{"key": "GAIA", "name": "GAIA Project"}],
"issue_types": ["Bug", "Task", "Story"],
"statuses": ["To Do", "In Progress", "Done"],
"priorities": ["Highest", "High", "Medium", "Low"]
}
agent = JiraAgent(jira_config=config)
result = agent.process_query("Create a bug: Login fails")
Acceptance Criteria
JiraAgent Technical Specification