Skip to content

Commit 9c6e298

Browse files
committed
Add upstream-search tool and instruct triage agent to use it
UpstreamSearchTool makes it easy for the triage agent to work with upstream-search REST API and helps with URL format whenever possible to reduce unnecessary trials and errors when accessing related commits. Resolves: packit/jotnar#221
1 parent 9f57b03 commit 9c6e298

File tree

4 files changed

+144
-1
lines changed

4 files changed

+144
-1
lines changed

agents/tools/upstream_search.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import os
2+
import asyncio
3+
import aiohttp
4+
import logging
5+
from enum import Enum
6+
from urllib.parse import urlparse
7+
8+
from pydantic import BaseModel, Field
9+
10+
from beeai_framework.context import RunContext
11+
from beeai_framework.emitter import Emitter
12+
from beeai_framework.tools import JSONToolOutput, Tool, ToolRunOptions, ToolError
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class UpstreamSearchResult(Enum):
18+
FOUND = "found"
19+
NOT_FOUND = "not_found"
20+
NOT_POSSIBLE = "not_possible"
21+
22+
23+
class UpstreamSearchToolInput(BaseModel):
24+
project: str = Field(
25+
description="name of the upstream project which should be searched through")
26+
description: str = Field(
27+
description="description of issue for which fixing commit will be looked for")
28+
date: str | None = Field(
29+
description="date in iso format after which the commit was created")
30+
31+
32+
class UpstreamSearchToolResult(BaseModel):
33+
result: UpstreamSearchResult = Field(
34+
description="result of the tool invocation")
35+
repository_url: str | None = Field(
36+
description="url of repository where commits reside")
37+
related_commits: list[str] | None = Field(
38+
description="commits related to given description")
39+
40+
41+
class UpstreamSearchToolOutput(JSONToolOutput[UpstreamSearchToolResult]):
42+
pass
43+
44+
45+
class UpstreamSearchTool(Tool[UpstreamSearchToolInput, ToolRunOptions, UpstreamSearchToolOutput]):
46+
name = "upstream_search"
47+
description = """
48+
Search through upstream project's git repository and finds commits related to
49+
provided description and optionally allows to filter commits made after provided date.
50+
51+
If the tool was successful, 'result' is set to 'found' which means that commits
52+
'related_commits'in repository 'repository_url' are the ones which should be related to
53+
provided description.
54+
55+
If the tool was unsuccessful to find commits for this particular query, 'result'
56+
is set to 'not_found'.
57+
58+
If the tool can not be used for this particular project, 'result' is set to 'not_possible'.
59+
"""
60+
input_schema = UpstreamSearchToolInput
61+
62+
def _create_emitter(self) -> Emitter:
63+
return Emitter.root().child(
64+
namespace=["tool", "commands", self.name],
65+
creator=self,
66+
)
67+
68+
async def _run(
69+
self, tool_input: UpstreamSearchToolInput,
70+
options: ToolRunOptions | None, context: RunContext) -> UpstreamSearchToolOutput:
71+
try:
72+
timeout = aiohttp.ClientTimeout(total=30)
73+
repos = []
74+
commits = []
75+
async with aiohttp.ClientSession(timeout=timeout) as session:
76+
async with session.get(f"{os.environ['UPSTREAM_SEARCH_API_URL']}/find_repository",
77+
params={"name": tool_input.project}) as response:
78+
if response.status != 200:
79+
logger.debug("Searching did not yield repo. status %d response %s",
80+
response.status, await response.text())
81+
return UpstreamSearchToolOutput(UpstreamSearchToolResult(
82+
result=UpstreamSearchResult.NOT_POSSIBLE,
83+
repository_url=None,
84+
related_commits=None
85+
))
86+
repos = await response.json()
87+
88+
# until we have solid reference to upstream repository through for example VCS
89+
# spec file tag, this is the best we can do
90+
post_params = {"url": repos[0], "text": tool_input.description}
91+
if tool_input.date is not None:
92+
post_params["date"] = tool_input.date
93+
async with session.post(f"{os.environ['UPSTREAM_SEARCH_API_URL']}/find_commit",
94+
json=post_params, timeout=240) as response:
95+
if response.status != 200:
96+
logger.debug("Searching did not yield commits. status %d response %s",
97+
response.status, await response.text())
98+
return UpstreamSearchToolOutput(UpstreamSearchToolResult(
99+
result=UpstreamSearchResult.NOT_FOUND,
100+
repository_url=None,
101+
related_commits=None
102+
))
103+
commits = await response.json()
104+
except asyncio.TimeoutError:
105+
raise ToolError("Timeout occured while contacting upstream-search backend")
106+
except Exception as e:
107+
raise ToolError(f"Unexpected internal error occured while contacting backend {e}")
108+
109+
def get_patch_url(commit):
110+
parsed_url = urlparse(repos[0])
111+
if not parsed_url.path.endswith(".git"):
112+
return commit
113+
if parsed_url.hostname.startswith("gitlab"):
114+
prefix = "/-"
115+
elif parsed_url.hostname.startswith("github"):
116+
prefix = ""
117+
else:
118+
return commit
119+
path = f"{prefix}/commit/{commit}.patch"
120+
return parsed_url._replace(path=parsed_url.path.replace(".git", path)).geturl()
121+
commits = [get_patch_url(commit) for commit in commits]
122+
123+
return UpstreamSearchToolOutput(UpstreamSearchToolResult(
124+
result=UpstreamSearchResult.FOUND,
125+
repository_url=repos[0],
126+
related_commits=commits
127+
))

agents/triage_agent.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from tools.commands import RunShellCommandTool
4545
from tools.patch_validator import PatchValidatorTool
4646
from tools.version_mapper import VersionMapperTool
47+
from tools.upstream_search import UpstreamSearchTool
4748
from utils import get_agent_execution_config, get_chat_model, get_tool_call_checker_config, mcp_tools, run_tool
4849

4950
logger = logging.getLogger(__name__)
@@ -201,6 +202,17 @@ def render_prompt(input: InputSchema) -> str:
201202
202203
* Even if the Jira issue provides a direct link to a fix, you need to validate it
203204
* When no direct link is provided, you must proactively search for fixes - do not give up easily
205+
* Try to use upstream_search tool to find out commits related to the issue.
206+
- The description you will use should be 1-2 sentences long and include implementation
207+
details, keywords, function names or any other helpful information.
208+
- The description should be like a command for example `Fix`, `Add` etc.
209+
- If the tool gives you list of URLs use them without any change.
210+
- Use release date of upstream version used in RHEL if you know it.
211+
- If the tool says it can not be used for this project, or it encounters internal error,
212+
do not try to use it again and proceed with different approach.
213+
- If you run out of commits to check, use different approach, do not give up. Inability
214+
of the tool to find proper fix does not mean it does not exist, search bug trackers
215+
and version control system.
204216
* Using the details from your analysis, search these sources:
205217
- Bug Trackers (for fixed bugs matching the issue summary and description)
206218
- Git / Version Control (for commit messages, using keywords, CVE IDs, function names, etc.)
@@ -298,7 +310,8 @@ async def run_workflow(jira_issue):
298310
name="TriageAgent",
299311
llm=get_chat_model(),
300312
tool_call_checker=get_tool_call_checker_config(),
301-
tools=[ThinkTool(), RunShellCommandTool(), PatchValidatorTool(), VersionMapperTool()]
313+
tools=[ThinkTool(), RunShellCommandTool(), PatchValidatorTool(),
314+
VersionMapperTool(), UpstreamSearchTool()]
302315
+ [t for t in gateway_tools if t.name in ["get_jira_details", "set_jira_fields"]],
303316
memory=UnconstrainedMemory(),
304317
requirements=[
@@ -310,6 +323,7 @@ async def run_workflow(jira_issue):
310323
only_success_invocations=False,
311324
),
312325
ConditionalRequirement("get_jira_details", min_invocations=1),
326+
ConditionalRequirement(UpstreamSearchTool, only_after="get_jira_details"),
313327
ConditionalRequirement(RunShellCommandTool, only_after="get_jira_details"),
314328
ConditionalRequirement(PatchValidatorTool, only_after="get_jira_details"),
315329
ConditionalRequirement("set_jira_fields", only_after="get_jira_details"),

compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Environment variables shared by all agents
22
x-beeai-env: &beeai-env
33
MCP_GATEWAY_URL: http://mcp-gateway:8000/sse
4+
UPSTREAM_SEARCH_API_URL: http://upstream-search.hosted.upshift.rdu2.redhat.com:80/v1
45
REDIS_URL: redis://valkey:6379/0
56
COLLECTOR_ENDPOINT: http://phoenix:6006/v1/traces
67
GIT_REPO_BASEPATH: /git-repos

openshift/configmap-endpoints-env.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ apiVersion: v1
22
data:
33
COLLECTOR_ENDPOINT: http://phoenix:6006/v1/traces
44
MCP_GATEWAY_URL: http://mcp-gateway:8000/sse
5+
UPSTREAM_SEARCH_API_URL: http://upstream-search.hosted.upshift.rdu2.redhat.com:80/v1
56
REDIS_URL: redis://valkey:6379/0
67
immutable: false
78
kind: ConfigMap

0 commit comments

Comments
 (0)