Skip to content

Commit

Permalink
Merge pull request #35 from JohnStrunk/cache
Browse files Browse the repository at this point in the history
Add issue cache & fix type errors
  • Loading branch information
mergify[bot] authored Apr 30, 2024
2 parents e087ddf + 2010c27 commit b7ebaee
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 65 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pylint = "*"
pytest = "*"
pytest-pylint = "*"
pytest-mypy = "*"
types-requests = "*"

[requires]
python_version = "3.12"
Expand Down
61 changes: 39 additions & 22 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from datetime import datetime

import requests
from atlassian import Jira
from atlassian import Jira # type: ignore

from jiraissues import Issue
from summarizer import get_issues_to_summarize, summarize_issue


def main():
"""Main function for the bot."""
parser = argparse.ArgumentParser(description="Summarizer bot")
parser.add_argument(
"-d",
Expand Down
101 changes: 66 additions & 35 deletions jiraissues.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,27 +78,7 @@ class Issue: # pylint: disable=too-many-instance-attributes
Represents a Jira issue as a proper object.
"""

client: Jira
"""The Jira client to use for API calls."""
key: str
"""The key of the issue."""
summary: str
"""The summary of the issue."""
description: str
"""The description of the issue."""
issue_type: str
"""The type of the issue."""
labels: List[str]
"""The labels on the issue."""
resolution: str
"""The resolution of the issue."""
status: str
"""The status of the issue."""
_changelog: Optional[List[ChangelogEntry]]
_comments: Optional[List[Comment]]
_related: Optional[List[RelatedIssue]]

def __init__(self, client: Jira, issue_key: str):
def __init__(self, client: Jira, issue_key: str) -> None:
self.client = client
self.key = issue_key

Expand All @@ -114,27 +94,27 @@ def __init__(self, client: Jira, issue_key: str):
data = _check(client.issue(issue_key, fields=",".join(fields)))

# Populate the fields
self.summary = data["fields"]["summary"]
self.description = data["fields"]["description"]
self.issue_type = data["fields"]["issuetype"]["name"]
self.status = data["fields"]["status"]["name"]
self.labels = data["fields"]["labels"]
self.resolution = (
self.summary: str = data["fields"]["summary"]
self.description: str = data["fields"]["description"]
self.issue_type: str = data["fields"]["issuetype"]["name"]
self.status: str = data["fields"]["status"]["name"]
self.labels: List[str] = data["fields"]["labels"]
self.resolution: str = (
data["fields"]["resolution"]["name"]
if data["fields"]["resolution"]
else "Unresolved"
)
self._changelog = None
self._comments = None
self._related = None
_logger.info(f"Retrieved issue: {self.key} - {self.summary}")
self._changelog: Optional[List[ChangelogEntry]] = None
self._comments: Optional[List[Comment]] = None
self._related: Optional[List[RelatedIssue]] = None
_logger.info("Retrieved issue: %s - %s", self.key, self.summary)

def __str__(self) -> str:
return f"{self.key}: {self.summary} ({self.status}/{self.resolution})"

def _fetch_changelog(self) -> List[ChangelogEntry]:
"""Fetch the changelog from the API."""
_logger.debug(f"Retrieving changelog for {self.key}")
_logger.debug("Retrieving changelog for %s", self.key)
log = _check(self.client.get_issue_changelog(self.key, start=0, limit=1000))
items: List[ChangelogEntry] = []
for entry in log["histories"]:
Expand Down Expand Up @@ -167,7 +147,7 @@ def changelog(self) -> List[ChangelogEntry]:

def _fetch_comments(self) -> List[Comment]:
"""Fetch the comments from the API."""
_logger.debug(f"Retrieving comments for {self.key}")
_logger.debug("Retrieving comments for %s", self.key)
comments = _check(self.client.issue(self.key, fields="comment"))["fields"][
"comment"
]["comments"]
Expand Down Expand Up @@ -198,7 +178,7 @@ def _fetch_related(self) -> List[RelatedIssue]:
"customfield_12313140",
"customfield_12318341",
]
_logger.debug(f"Retrieving related links for {self.key}")
_logger.debug("Retrieving related links for %s", self.key)
data = _check(self.client.issue(self.key, fields=",".join(fields)))
# Get the related issues
related: List[RelatedIssue] = []
Expand Down Expand Up @@ -293,10 +273,11 @@ def update_description(self, new_description: str) -> None:
Parameters:
- new_description: The new description to set.
"""
_logger.info(f"Sending updated description for {self.key} to server")
_logger.info("Sending updated description for %s to server", self.key)
fields = {"description": new_description}
self.client.update_issue_field(self.key, fields) # type: ignore
self.description = new_description
issue_cache.remove(self.key) # Invalidate any cached copy


def _check(response: Any) -> dict:
Expand All @@ -312,3 +293,53 @@ def _check(response: Any) -> dict:
if isinstance(response, dict):
return response
raise ValueError(f"Unexpected response: {response}")


class IssueCache:
"""
A cache of Jira issues to avoid fetching the same issue multiple times.
"""

def __init__(self) -> None:
self._cache: dict[str, Issue] = {}
self.hits = 0
self.tries = 0

def get_issue(self, client: Jira, key: str) -> Issue:
"""
Get an issue from the cache, or fetch it from the server if it's not
already cached.
Parameters:
- client: The Jira client to use for fetching the issue.
- key: The key of the issue to fetch.
Returns:
The issue object.
"""
self.tries += 1
if key not in self._cache:
_logger.debug("Cache miss: %s", key)
self._cache[key] = Issue(client, key)
else:
self.hits += 1
_logger.debug("Cache hit: %s", key)
return self._cache[key]

def remove(self, key: str) -> None:
"""
Remove an Issue from the cache.
Parameters:
- key: The key of the issue to remove.
"""
if key in self._cache:
del self._cache[key]

def clear(self) -> None:
"""Clear the cache."""
self._cache = {}


# The global cache of issues
issue_cache = IssueCache()
3 changes: 2 additions & 1 deletion summarize_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import logging
import os

from atlassian import Jira
from atlassian import Jira # type: ignore

from jiraissues import Issue
from summarizer import summarize_issue


def main():
"""Main function"""
parser = argparse.ArgumentParser(description="Summarize a JIRA issue")
parser.add_argument(
"-d",
Expand Down
13 changes: 7 additions & 6 deletions summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from datetime import datetime
from typing import List, Tuple

from atlassian import Jira
from atlassian import Jira # type: ignore
from genai import Client, Credentials
from genai.extensions.langchain import LangChainInterface
from genai.schema import DecodingMethod, TextGenerationParameters
Expand All @@ -34,6 +34,7 @@
_WRAP_COLUMN = 78


# pylint: disable=too-many-locals
def summarize_issue(
issue: Issue,
max_depth: int = 0,
Expand All @@ -58,11 +59,11 @@ def summarize_issue(
A string containing the summary
"""

_logger.info(f"Summarizing {issue.key}...")
_logger.info("Summarizing %s...", issue.key)
# If the current summary is up-to-date and we're not asked to regenerate it,
# return what's there
if not regenerate and is_summary_current(issue):
_logger.debug(f"Summary for {issue.key} is current, using that.")
_logger.debug("Summary for %s is current, using that.", issue.key)
return get_aisummary(issue.description)

# if we have not reached max-depth, summarize the child issues for inclusion in this summary
Expand Down Expand Up @@ -151,8 +152,8 @@ def summarize_issue(
Here is the summary in a few sentences:
"""

_logger.info(f"Summarizing {issue.key} via LLM")
_logger.debug(f"Prompt:\n{llm_prompt}")
_logger.info("Summarizing %s via LLM", issue.key)
_logger.debug("Prompt:\n%s", llm_prompt)

chat = _chat_model()
summary = chat.invoke(llm_prompt).strip()
Expand Down Expand Up @@ -310,7 +311,7 @@ def get_issues_to_summarize(
limit=50,
fields="key,updated",
)
if type(result) is not dict:
if not isinstance(result, dict):
return []
keys = [issue["key"] for issue in result["issues"]]
return keys

0 comments on commit b7ebaee

Please sign in to comment.