mirror of
https://github.com/hwchase17/langchain.git
synced 2025-06-27 08:58:48 +00:00
feat(community): add oauth2 support for Jira toolkit (#30684)
**Description:** add support for oauth2 in Jira tool by adding the possibility to pass a dictionary with oauth parameters. I also adapted the documentation to show this new behavior
This commit is contained in:
parent
b6fe7e8c10
commit
48cf7c838d
@ -14,10 +14,12 @@
|
||||
"## Installation and setup\n",
|
||||
"\n",
|
||||
"To use this tool, you must first set as environment variables:\n",
|
||||
" JIRA_API_TOKEN\n",
|
||||
" JIRA_USERNAME\n",
|
||||
" JIRA_INSTANCE_URL\n",
|
||||
" JIRA_CLOUD"
|
||||
" JIRA_INSTANCE_URL,\n",
|
||||
" JIRA_CLOUD\n",
|
||||
"\n",
|
||||
"You have the choice between two authentication methods:\n",
|
||||
"- API token authentication: set the JIRA_API_TOKEN (and JIRA_USERNAME if needed) environment variables\n",
|
||||
"- OAuth2.0 authentication: set the JIRA_OAUTH2 environment variable as a dict having as fields \"client_id\" and \"token\" which is a dict containing at least \"access_token\" and \"token_type\""
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -79,6 +81,12 @@
|
||||
"from langchain_openai import OpenAI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3c925f1468696e4c",
|
||||
"metadata": {},
|
||||
"source": "For authentication with API token"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
@ -109,6 +117,27 @@
|
||||
"os.environ[\"JIRA_CLOUD\"] = \"True\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "325ea81fb416aac6",
|
||||
"metadata": {},
|
||||
"source": "For authentication with a Oauth2.0"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "917e83e3a764d91a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"os.environ[\"JIRA_OAUTH2\"] = (\n",
|
||||
" '{\"client_id\": \"123\", \"token\": {\"access_token\": \"abc\", \"token_type\": \"bearer\"}}'\n",
|
||||
")\n",
|
||||
"os.environ[\"JIRA_INSTANCE_URL\"] = \"https://jira.atlassian.com\"\n",
|
||||
"os.environ[\"OPENAI_API_KEY\"] = \"xyz\"\n",
|
||||
"os.environ[\"JIRA_CLOUD\"] = \"True\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
@ -215,15 +244,15 @@
|
||||
"text": [
|
||||
"\n",
|
||||
"\n",
|
||||
"\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
|
||||
"\u001b[32;1m\u001b[1;3m I need to create an issue in project PW\n",
|
||||
"\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n",
|
||||
"\u001B[32;1m\u001B[1;3m I need to create an issue in project PW\n",
|
||||
"Action: Create Issue\n",
|
||||
"Action Input: {\"summary\": \"Make more fried rice\", \"description\": \"Reminder to make more fried rice\", \"issuetype\": {\"name\": \"Task\"}, \"priority\": {\"name\": \"Low\"}, \"project\": {\"key\": \"PW\"}}\u001b[0m\n",
|
||||
"Observation: \u001b[38;5;200m\u001b[1;3mNone\u001b[0m\n",
|
||||
"Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n",
|
||||
"Final Answer: A new issue has been created in project PW with the summary \"Make more fried rice\" and description \"Reminder to make more fried rice\".\u001b[0m\n",
|
||||
"Action Input: {\"summary\": \"Make more fried rice\", \"description\": \"Reminder to make more fried rice\", \"issuetype\": {\"name\": \"Task\"}, \"priority\": {\"name\": \"Low\"}, \"project\": {\"key\": \"PW\"}}\u001B[0m\n",
|
||||
"Observation: \u001B[38;5;200m\u001B[1;3mNone\u001B[0m\n",
|
||||
"Thought:\u001B[32;1m\u001B[1;3m I now know the final answer\n",
|
||||
"Final Answer: A new issue has been created in project PW with the summary \"Make more fried rice\" and description \"Reminder to make more fried rice\".\u001B[0m\n",
|
||||
"\n",
|
||||
"\u001b[1m> Finished chain.\u001b[0m\n"
|
||||
"\u001B[1m> Finished chain.\u001B[0m\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ JIRA_ISSUE_CREATE_PROMPT = """
|
||||
|
||||
JIRA_GET_ALL_PROJECTS_PROMPT = """
|
||||
This tool is a wrapper around atlassian-python-api's Jira project API,
|
||||
useful when you need to fetch all the projects the user has access to, find out how many projects there are, or as an intermediary step that involv searching by projects.
|
||||
useful when you need to fetch all the projects the user has access to, find out how many projects there are, or as an intermediary step that involve searching by projects.
|
||||
there is no input to this tool.
|
||||
"""
|
||||
|
||||
|
@ -1,19 +1,51 @@
|
||||
"""Util that calls Jira."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from langchain_core.utils import get_from_dict_or_env
|
||||
from pydantic import BaseModel, ConfigDict, model_validator
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class JiraOauth2Token(TypedDict):
|
||||
"""Jira OAuth2 token."""
|
||||
|
||||
access_token: str
|
||||
"""Jira OAuth2 access token."""
|
||||
token_type: str
|
||||
"""Jira OAuth2 token type ('bearer' or other)."""
|
||||
|
||||
|
||||
class JiraOauth2(TypedDict):
|
||||
"""Jira OAuth2."""
|
||||
|
||||
client_id: str
|
||||
"""Jira OAuth2 client ID."""
|
||||
token: JiraOauth2Token
|
||||
"""Jira OAuth2 token."""
|
||||
|
||||
|
||||
# TODO: think about error handling, more specific api specs, and jql/project limits
|
||||
class JiraAPIWrapper(BaseModel):
|
||||
"""Wrapper for Jira API."""
|
||||
"""
|
||||
Wrapper for Jira API. You can connect to Jira with either an API token or OAuth2.
|
||||
- with API token, you need to provide the JIRA_USERNAME and JIRA_API_TOKEN
|
||||
environment variables or arguments.
|
||||
ex: JIRA_USERNAME=your_username JIRA_API_TOKEN=your_api_token
|
||||
- with OAuth2, you need to provide the JIRA_OAUTH2 environment variable or
|
||||
argument as a dict having as fields "client_id" and "token" which is
|
||||
a dict containing at least "access_token" and "token_type".
|
||||
ex: JIRA_OAUTH2='{"client_id": "your_client_id", "token":
|
||||
{"access_token": "your_access_token","token_type": "bearer"}}'
|
||||
"""
|
||||
|
||||
jira: Any = None #: :meta private:
|
||||
confluence: Any = None
|
||||
jira_username: Optional[str] = None
|
||||
jira_api_token: Optional[str] = None
|
||||
"""Jira API token when you choose to connect to Jira with api token."""
|
||||
jira_oauth2: Optional[Union[JiraOauth2, str]] = None
|
||||
"""Jira OAuth2 token when you choose to connect to Jira with oauth2."""
|
||||
jira_instance_url: Optional[str] = None
|
||||
jira_cloud: Optional[bool] = None
|
||||
|
||||
@ -31,10 +63,30 @@ class JiraAPIWrapper(BaseModel):
|
||||
values["jira_username"] = jira_username
|
||||
|
||||
jira_api_token = get_from_dict_or_env(
|
||||
values, "jira_api_token", "JIRA_API_TOKEN"
|
||||
values, "jira_api_token", "JIRA_API_TOKEN", default=""
|
||||
)
|
||||
values["jira_api_token"] = jira_api_token
|
||||
|
||||
jira_oauth2 = get_from_dict_or_env(
|
||||
values, "jira_oauth2", "JIRA_OAUTH2", default=""
|
||||
)
|
||||
values["jira_oauth2"] = jira_oauth2
|
||||
|
||||
if jira_oauth2 and isinstance(jira_oauth2, str):
|
||||
try:
|
||||
import json
|
||||
|
||||
jira_oauth2 = json.loads(jira_oauth2)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"json is not installed. Please install it with `pip install json`"
|
||||
)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
raise ValueError(
|
||||
f"The format of the JIRA_OAUTH2 string is "
|
||||
f"not a valid dictionary: {e}"
|
||||
)
|
||||
|
||||
jira_instance_url = get_from_dict_or_env(
|
||||
values, "jira_instance_url", "JIRA_INSTANCE_URL"
|
||||
)
|
||||
@ -47,6 +99,12 @@ class JiraAPIWrapper(BaseModel):
|
||||
jira_cloud = jira_cloud_str.lower() == "true"
|
||||
values["jira_cloud"] = jira_cloud
|
||||
|
||||
if jira_api_token and jira_oauth2:
|
||||
raise ValueError(
|
||||
"You have to provide either a jira_api_token or a jira_oauth2. "
|
||||
"Not both."
|
||||
)
|
||||
|
||||
try:
|
||||
from atlassian import Confluence, Jira
|
||||
except ImportError:
|
||||
@ -55,26 +113,38 @@ class JiraAPIWrapper(BaseModel):
|
||||
"Please install it with `pip install atlassian-python-api`"
|
||||
)
|
||||
|
||||
if jira_username == "":
|
||||
jira = Jira(
|
||||
url=jira_instance_url,
|
||||
token=jira_api_token,
|
||||
cloud=jira_cloud,
|
||||
)
|
||||
else:
|
||||
jira = Jira(
|
||||
if jira_api_token:
|
||||
if jira_username == "":
|
||||
jira = Jira(
|
||||
url=jira_instance_url,
|
||||
token=jira_api_token,
|
||||
cloud=jira_cloud,
|
||||
)
|
||||
else:
|
||||
jira = Jira(
|
||||
url=jira_instance_url,
|
||||
username=jira_username,
|
||||
password=jira_api_token,
|
||||
cloud=jira_cloud,
|
||||
)
|
||||
|
||||
confluence = Confluence(
|
||||
url=jira_instance_url,
|
||||
username=jira_username,
|
||||
password=jira_api_token,
|
||||
cloud=jira_cloud,
|
||||
)
|
||||
|
||||
confluence = Confluence(
|
||||
url=jira_instance_url,
|
||||
username=jira_username,
|
||||
password=jira_api_token,
|
||||
cloud=jira_cloud,
|
||||
)
|
||||
elif jira_oauth2:
|
||||
jira = Jira(
|
||||
url=jira_instance_url,
|
||||
oauth2=jira_oauth2,
|
||||
cloud=jira_cloud,
|
||||
)
|
||||
confluence = Confluence(
|
||||
url=jira_instance_url,
|
||||
oauth2=jira_oauth2,
|
||||
cloud=jira_cloud,
|
||||
)
|
||||
|
||||
values["jira"] = jira
|
||||
values["confluence"] = confluence
|
||||
@ -97,7 +167,7 @@ class JiraAPIWrapper(BaseModel):
|
||||
except Exception:
|
||||
assignee = "None"
|
||||
rel_issues = {}
|
||||
for related_issue in issue["fields"]["issuelinks"]:
|
||||
for related_issue in issue["fields"].get("issuelinks", []):
|
||||
if "inwardIssue" in related_issue.keys():
|
||||
rel_type = related_issue["type"]["inward"]
|
||||
rel_key = related_issue["inwardIssue"]["key"]
|
||||
@ -126,8 +196,8 @@ class JiraAPIWrapper(BaseModel):
|
||||
id = project["id"]
|
||||
key = project["key"]
|
||||
name = project["name"]
|
||||
type = project["projectTypeKey"]
|
||||
style = project.get("style", None)
|
||||
type = project.get("projectTypeKey")
|
||||
style = project.get("style")
|
||||
parsed.append(
|
||||
{"id": id, "key": key, "name": name, "type": type, "style": style}
|
||||
)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@ -60,3 +61,24 @@ class TestJiraAPIWrapper:
|
||||
password="test_token",
|
||||
cloud=False,
|
||||
)
|
||||
|
||||
def test_jira_api_wrapper_with_oauth_dict(self, mock_jira: MagicMock) -> None:
|
||||
oauth_dict = {
|
||||
"client_id": "test_client_id",
|
||||
"token": {
|
||||
"access_token": "test_access_token",
|
||||
"token_type": "test_token_type",
|
||||
},
|
||||
}
|
||||
oauth_string = json.dumps(oauth_dict)
|
||||
|
||||
JiraAPIWrapper(
|
||||
jira_oauth2=oauth_string,
|
||||
jira_instance_url="https://test.atlassian.net",
|
||||
jira_cloud=False,
|
||||
)
|
||||
mock_jira.assert_called_once_with(
|
||||
url="https://test.atlassian.net",
|
||||
oauth2={"client": None, **oauth_dict},
|
||||
cloud=False,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user