Adds `Jira.GetAvailableAtlassianClouds` tool, which provides a list of clouds available (checking which Clouds were actually authorized by the current auth token). Refactors the interface of every tool to accept an `atlassian_cloud_id` argument (when not provided, try to get a unique cloud ID - if multiple are available, raises a Retryable error with the list of Clouds available instructing to select one). Gets rid of all caching. Now storing the global semaphore to the context object. The global semaphore is important because some tools depend on others, and each tool instantiates its own Jira HTTP client. Storing the semaphore in the context object ensures that all HTTP clients will respect the concurrency limit. Removes from tool responses the Atlassian URLs linking to objects in the Jira GUI (users, projects, issues, etc. We do not keep track of the cloud name anymore, which is required to build the objects' URLs. Extends/refactors unit tests accordingly. Evals checking LLM behavior when: - a cloud ID is explicitly mentioned in the prompt; - no cloud ID is mentioned; - a "multiple clouds available" error is raised and the user is prompted to pick one; - user request triggers another tool call after having previously picked a cloud ID (in the same chat context);
46 lines
1.5 KiB
Python
46 lines
1.5 KiB
Python
import asyncio
|
|
from typing import Annotated
|
|
|
|
import httpx
|
|
from arcade_tdk import ToolContext, tool
|
|
from arcade_tdk.auth import Atlassian
|
|
|
|
from arcade_jira.constants import JIRA_MAX_CONCURRENT_REQUESTS
|
|
from arcade_jira.utils import check_if_cloud_is_authorized
|
|
|
|
|
|
@tool(requires_auth=Atlassian(scopes=["read:jira-user"]))
|
|
async def get_available_atlassian_clouds(
|
|
context: ToolContext,
|
|
) -> Annotated[dict[str, list[dict[str, str]]], "Available Atlassian Clouds"]:
|
|
"""Get available Atlassian Clouds."""
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get(
|
|
"https://api.atlassian.com/oauth/token/accessible-resources",
|
|
headers={"Authorization": f"Bearer {context.get_auth_token_or_empty()}"},
|
|
)
|
|
|
|
verified_clouds = response.json()
|
|
cloud_ids_seen = set()
|
|
unique_clouds = []
|
|
|
|
for cloud in verified_clouds:
|
|
if cloud["id"] not in cloud_ids_seen:
|
|
unique_clouds.append({
|
|
"atlassian_cloud_id": cloud["id"],
|
|
"atlassian_cloud_name": cloud["name"],
|
|
"atlassian_cloud_url": cloud["url"],
|
|
})
|
|
cloud_ids_seen.add(cloud["id"])
|
|
|
|
semaphore = asyncio.Semaphore(JIRA_MAX_CONCURRENT_REQUESTS)
|
|
|
|
verified_clouds = await asyncio.gather(*[
|
|
check_if_cloud_is_authorized(context, cloud, semaphore) for cloud in unique_clouds
|
|
])
|
|
|
|
return {
|
|
"clouds_available": [
|
|
cloud_available for cloud_available in verified_clouds if cloud_available is not False
|
|
]
|
|
}
|