3
3
import os
4
4
import sys
5
5
import traceback
6
- from typing import Optional
6
+ from pathlib import Path
7
7
8
8
from pydantic import BaseModel , Field
9
9
19
19
from beeai_framework .tools import Tool
20
20
from beeai_framework .tools .search .duckduckgo import DuckDuckGoSearchTool
21
21
from beeai_framework .tools .think import ThinkTool
22
+ from beeai_framework .workflows import Workflow
22
23
23
- from constants import COMMIT_PREFIX , BRANCH_PREFIX
24
+ import tasks
25
+ from constants import COMMIT_PREFIX
24
26
from observability import setup_observability
25
27
from tools .commands import RunShellCommandTool
28
+ from tools .specfile import AddChangelogEntryTool
29
+ from tools .text import CreateTool , InsertTool , StrReplaceTool , ViewTool
26
30
from triage_agent import RebaseData , ErrorData
27
- from utils import get_agent_execution_config , mcp_tools , redis_client , get_git_finalization_steps
31
+ from utils import get_agent_execution_config , mcp_tools , redis_client , run_tool
28
32
29
33
logger = logging .getLogger (__name__ )
30
34
31
35
32
36
class InputSchema (BaseModel ):
37
+ local_clone : Path = Field (description = "Path to the local clone of forked dist-git repository" )
33
38
package : str = Field (description = "Package to update" )
39
+ dist_git_branch : str = Field (description = "dist-git branch to update" )
34
40
version : str = Field (description = "Version to update to" )
35
41
jira_issue : str = Field (description = "Jira issue to reference as resolved" )
36
- dist_git_branch : str = Field (description = "Git branch in dist-git to be updated" )
37
- gitlab_user : str = Field (
38
- description = "Name of the GitLab user" ,
39
- default = os .getenv ("GITLAB_USER" , "rhel-packaging-agent" ),
40
- )
41
- git_url : str = Field (
42
- description = "URL of the git repository" ,
43
- default = "https://gitlab.com/redhat/centos-stream/rpms" ,
44
- )
45
- git_repo_basepath : str = Field (
46
- description = "Base path for cloned git repos" ,
47
- default = os .getenv ("GIT_REPO_BASEPATH" ),
48
- )
49
42
50
43
51
44
class OutputSchema (BaseModel ):
52
45
success : bool = Field (description = "Whether the rebase was successfully completed" )
53
46
status : str = Field (description = "Rebase status" )
54
- mr_url : Optional [ str ] = Field (description = "URL to the opened merge request" )
55
- error : Optional [ str ] = Field (description = "Specific details about an error" )
47
+ mr_url : str | None = Field (description = "URL to the opened merge request" )
48
+ error : str | None = Field (description = "Specific details about an error" )
56
49
57
50
58
51
def render_prompt (input : InputSchema ) -> str :
59
52
template = """
60
- You are an AI Agent tasked to rebase a CentOS package to a newer version following the exact workflow.
53
+ You are an AI Agent tasked to rebase a package to a newer version following the exact workflow.
61
54
62
55
A couple of rules that you must follow and useful information for you:
63
- * All packages are in separate Git repositories under the Gitlab project {{ git_url }}
64
- * You can find the package at {{ git_url }}/{{ package }}
65
- * Use {{ gitlab_user }} as the GitLab user.
66
- * Work only in a temporary directory that you can create with the mktemp tool.
67
- * You can find packaging guidelines at https://docs.fedoraproject.org/en-US/packaging-guidelines/
56
+ * You can find packaging guidelines at https://docs.fedoraproject.org/en-US/packaging-guidelines/.
68
57
* You can find the RPM packaging guide at https://rpm-packaging-guide.github.io/.
69
- * Do not run the `centpkg new-sources` command for now (testing purposes), just write down the commands you would run.
70
-
71
- IMPORTANT GUIDELINES:
72
- - **Tool Usage**: You have run_shell_command tool available - use it directly!
73
- - **Command Execution Rules**:
74
- - Use run_shell_command tool for ALL command execution
75
- - If a command shows "no output" or empty STDOUT, that is a VALID result - do not retry
76
- - Commands that succeed with no output are normal - report success
77
- - **Git Configuration**: Always configure git user name and email before any git operations
58
+ * IMPORTANT: Do not run the `centpkg new-sources` command for now (testing purposes), just write down
59
+ the commands you would run.
78
60
79
61
Follow exactly these steps:
80
62
81
- 1. Find the location of the {{ package }} package at {{ git_url }}. Always use the {{ dist_git_branch }} branch.
63
+ 1. You will find the cloned dist-git repository of the {{ package }} package in {{ local_clone }}.
64
+ It is your current working directory, do not `cd` anywhere else.
82
65
83
- 2. Check if the {{ package }} was not already updated to version {{ version }}. That means comparing
84
- the current version and provided version.
85
- * The current version of the package can be found in the 'Version' field of the RPM . spec file.
66
+ 2. Check if the {{ package }} was not already updated to version {{ version }}. That means comparing
67
+ the current version with the provided version.
68
+ * The current version of the package can be found in the 'Version' field of the spec file.
86
69
* If there is nothing to update, print a message and exit. Otherwise follow the instructions below.
87
- * Do not clone any repository for detecting the version in .spec file.
88
-
89
- 3. Create a local Git repository by following these steps:
90
- * Create a fork of the {{ package }} package using the `fork_repository` tool.
91
- * Clone the fork using git and HTTPS into a temporary directory under {{ git_repo_basepath }}.
92
70
93
- 4. Update the {{ package }} to the newer version:
94
- * Create a new Git branch named `automated-package-update-{{ version }}`.
71
+ 3. Update the {{ package }} to the newer version:
95
72
* Update the local package by:
96
- * Updating the 'Version' and 'Release' fields in the . spec file as needed (or corresponding macros) ,
73
+ * Updating the 'Version' and 'Release' fields (or corresponding macros) in the spec file as needed,
97
74
following packaging documentation.
98
- * Make sure the format of the . spec file remains the same.
99
- * Updating macros related to update (e.g., 'commit') if present and necessary; examine the file's history
75
+ * Make sure the format of the spec file remains the same.
76
+ * Updating macros related to update (e.g., 'commit') if present and necessary; examine the file history
100
77
to see how updates are typically done.
101
78
* You might need to check some information in upstream repository, e.g. the commit SHA of the new version.
102
79
* Creating a changelog entry, referencing the Jira issue as "Resolves: {{ jira_issue }}".
103
80
* Downloading sources using `spectool -g -S {{ package }}.spec` (you might need to copy local sources,
104
- e.g. if the . spec file loads some macros from them, to a directory where spectool expects them).
81
+ e.g. if the spec file loads some macros from them, to a directory where ` spectool` expects them).
105
82
* Uploading the new sources using `centpkg --release {{ dist_git_branch }} new-sources`.
106
83
* IMPORTANT: Only performing changes relevant to the version update: Do not rename variables,
107
- comment out existing lines, or alter if-else branches in the . spec file.
84
+ comment out existing lines, or alter if-else branches in the spec file.
108
85
109
- 5 . Verify and adjust the changes:
110
- * Use `rpmlint` to validate your . spec file changes and fix any new errors it identifies.
111
- * Generate the SRPM using `rpmbuild -bs` (ensure your . spec file and source files are correctly
86
+ 4 . Verify and adjust the changes:
87
+ * Use `rpmlint` to validate your spec file changes and fix any new errors it identifies.
88
+ * Generate the SRPM using `rpmbuild -bs` (ensure your spec file and source files are correctly
112
89
copied to the build environment as required by the command).
113
90
114
- 6. {{ rebase_git_steps }}
115
-
116
91
Report the status of the rebase operation including:
117
92
- Whether the package was already up to date
118
93
- Any errors encountered during the process
119
94
- The URL of the created merge request if successful
120
95
- Any validation issues found with rpmlint
121
96
"""
122
-
123
- # Define template function that can be called from the template
124
- def rebase_git_steps (data : dict ) -> str :
125
- input_data = InputSchema .model_validate (data )
126
- return get_git_finalization_steps (
127
- package = input_data .package ,
128
- jira_issue = input_data .jira_issue ,
129
- commit_title = f"{ COMMIT_PREFIX } Update to version { input_data .version } " ,
130
- files_to_commit = "*.spec" ,
131
- branch_name = f"{ BRANCH_PREFIX } -{ input_data .version } " ,
132
- git_url = input_data .git_url ,
133
- dist_git_branch = input_data .dist_git_branch ,
134
- )
135
-
136
- return PromptTemplate (
137
- PromptTemplateInput (schema = InputSchema , template = template , functions = {"rebase_git_steps" : rebase_git_steps })
138
- ).render (input )
97
+ return PromptTemplate (PromptTemplateInput (schema = InputSchema , template = template )).render (input )
139
98
140
99
141
100
async def main () -> None :
@@ -144,28 +103,112 @@ async def main() -> None:
144
103
setup_observability (os .getenv ("COLLECTOR_ENDPOINT" ))
145
104
146
105
async with mcp_tools (os .getenv ("MCP_GATEWAY_URL" )) as gateway_tools :
147
- agent = RequirementAgent (
106
+ rebase_agent = RequirementAgent (
148
107
llm = ChatModel .from_name (os .getenv ("CHAT_MODEL" )),
149
- tools = [ThinkTool (), RunShellCommandTool (), DuckDuckGoSearchTool ()]
150
- + [
151
- t
152
- for t in gateway_tools
153
- if t .name in ("fork_repository" , "open_merge_request" , "push_to_remote_repository" )
108
+ tools = [
109
+ ThinkTool (),
110
+ RunShellCommandTool (),
111
+ DuckDuckGoSearchTool (),
112
+ CreateTool (),
113
+ ViewTool (),
114
+ InsertTool (),
115
+ StrReplaceTool (),
116
+ AddChangelogEntryTool (),
154
117
],
155
118
memory = UnconstrainedMemory (),
156
119
requirements = [
157
120
ConditionalRequirement (ThinkTool , force_after = Tool , consecutive_allowed = False ),
158
121
],
159
122
middlewares = [GlobalTrajectoryMiddleware (pretty = True )],
123
+ role = "Red Hat Enterprise Linux developer" ,
124
+ instructions = [
125
+ "Use the `think` tool to reason through complex decisions and document your approach." ,
126
+ "Preserve existing formatting and style conventions in RPM spec files and patch headers." ,
127
+ "Use `rpmlint *.spec` to check for packaging issues and address any NEW errors" ,
128
+ "Ignore pre-existing rpmlint warnings unless they're related to your changes" ,
129
+ "Run `centpkg prep` to verify all patches apply cleanly during build preparation" ,
130
+ "Generate an SRPM using `centpkg srpm` command to ensure complete build readiness" ,
131
+ "* IMPORTANT: Only perform changes relevant to the rebase update" ,
132
+ ],
160
133
)
161
134
162
- async def run (input ):
163
- response = await agent .run (
164
- prompt = render_prompt (input ),
165
- expected_output = OutputSchema ,
166
- execution = get_agent_execution_config (),
135
+ class State (BaseModel ):
136
+ jira_issue : str
137
+ package : str
138
+ dist_git_branch : str
139
+ version : str
140
+ local_clone : Path | None = Field (default = None )
141
+ update_branch : str | None = Field (default = None )
142
+ fork_url : str | None = Field (default = None )
143
+ rebase_result : OutputSchema | None = Field (default = None )
144
+ merge_request_url : str | None = Field (default = None )
145
+
146
+ workflow = Workflow (State )
147
+
148
+ async def fork_and_prepare_dist_git (state ):
149
+ state .local_clone , state .update_branch , state .fork_url = await tasks .fork_and_prepare_dist_git (
150
+ jira_issue = state .jira_issue ,
151
+ package = state .package ,
152
+ dist_git_branch = state .dist_git_branch ,
153
+ available_tools = gateway_tools ,
167
154
)
168
- return OutputSchema .model_validate_json (response .answer .text )
155
+ return "run_rebase_agent"
156
+
157
+ async def run_rebase_agent (state ):
158
+ cwd = Path .cwd ()
159
+ try :
160
+ # make things easier for the LLM
161
+ os .chdir (state .local_clone )
162
+ response = await rebase_agent .run (
163
+ prompt = render_prompt (
164
+ InputSchema (
165
+ local_clone = state .local_clone ,
166
+ package = state .package ,
167
+ dist_git_branch = state .dist_git_branch ,
168
+ version = state .version ,
169
+ jira_issue = state .jira_issue ,
170
+ ),
171
+ ),
172
+ expected_output = OutputSchema ,
173
+ execution = get_agent_execution_config (),
174
+ )
175
+ state .rebase_result = OutputSchema .model_validate_json (response .answer .text )
176
+ finally :
177
+ os .chdir (cwd )
178
+ if state .rebase_result .success :
179
+ return "commit_push_and_open_mr"
180
+ else :
181
+ return Workflow .END
182
+
183
+ async def commit_push_and_open_mr (state ):
184
+ state .merge_request_url = await tasks .commit_push_and_open_mr (
185
+ local_clone = state .local_clone ,
186
+ files_to_commit = "*.spec" ,
187
+ commit_message = f"{ COMMIT_PREFIX } Update to version { state .version } " ,
188
+ fork_url = state .fork_url ,
189
+ dist_git_branch = state .dist_git_branch ,
190
+ update_branch = state .update_branch ,
191
+ mr_title = f"{ COMMIT_PREFIX } Update to version { state .version } " ,
192
+ mr_description = "TODO" ,
193
+ available_tools = gateway_tools ,
194
+ commit_only = os .getenv ("DRY_RUN" , "False" ).lower () == "true" ,
195
+ )
196
+ return Workflow .END
197
+
198
+ workflow .add_step ("fork_and_prepare_dist_git" , fork_and_prepare_dist_git )
199
+ workflow .add_step ("run_rebase_agent" , run_rebase_agent )
200
+ workflow .add_step ("commit_push_and_open_mr" , commit_push_and_open_mr )
201
+
202
+ async def run_workflow (package , dist_git_branch , version , jira_issue ):
203
+ response = await workflow .run (
204
+ State (
205
+ package = package ,
206
+ dist_git_branch = dist_git_branch ,
207
+ version = version ,
208
+ jira_issue = jira_issue ,
209
+ ),
210
+ )
211
+ return response .state
169
212
170
213
if (
171
214
(package := os .getenv ("PACKAGE" , None ))
@@ -174,14 +217,13 @@ async def run(input):
174
217
and (branch := os .getenv ("BRANCH" , None ))
175
218
):
176
219
logger .info ("Running in direct mode with environment variables" )
177
- input = InputSchema (
220
+ state = await run_workflow (
178
221
package = package ,
222
+ dist_git_branch = branch ,
179
223
version = version ,
180
224
jira_issue = jira_issue ,
181
- dist_git_branch = branch ,
182
225
)
183
- output = await run (input )
184
- logger .info (f"Direct run completed: { output .model_dump_json (indent = 4 )} " )
226
+ logger .info (f"Direct run completed: { state .rebase_result .model_dump_json (indent = 4 )} " )
185
227
return
186
228
187
229
class Task (BaseModel ):
@@ -211,13 +253,6 @@ class Task(BaseModel):
211
253
f"attempt: { task .attempts + 1 } "
212
254
)
213
255
214
- input = InputSchema (
215
- package = rebase_data .package ,
216
- version = rebase_data .version ,
217
- jira_issue = rebase_data .jira_issue ,
218
- dist_git_branch = rebase_data .branch ,
219
- )
220
-
221
256
async def retry (task , error ):
222
257
task .attempts += 1
223
258
if task .attempts < max_retries :
@@ -235,21 +270,26 @@ async def retry(task, error):
235
270
236
271
try :
237
272
logger .info (f"Starting rebase processing for { rebase_data .jira_issue } " )
238
- output = await run (input )
273
+ state = await run_workflow (
274
+ package = rebase_data .package ,
275
+ dist_git_branch = rebase_data .branch ,
276
+ version = rebase_data .version ,
277
+ jira_issue = rebase_data .jira_issue ,
278
+ )
239
279
logger .info (
240
- f"Rebase processing completed for { rebase_data .jira_issue } , " f"success: { output .success } "
280
+ f"Rebase processing completed for { rebase_data .jira_issue } , " f"success: { state . rebase_result .success } "
241
281
)
242
282
except Exception as e :
243
283
error = "" .join (traceback .format_exception (e ))
244
284
logger .error (f"Exception during rebase processing for { rebase_data .jira_issue } : { error } " )
245
- await retry (task , ErrorData (details = error , jira_issue = input .jira_issue ).model_dump_json ())
285
+ await retry (task , ErrorData (details = error , jira_issue = rebase_data .jira_issue ).model_dump_json ())
246
286
else :
247
- if output .success :
287
+ if state . rebase_result .success :
248
288
logger .info (f"Rebase successful for { rebase_data .jira_issue } , " f"adding to completed list" )
249
- await redis .lpush ("completed_rebase_list" , output .model_dump_json ())
289
+ await redis .lpush ("completed_rebase_list" , state . rebase_result .model_dump_json ())
250
290
else :
251
- logger .warning (f"Rebase failed for { rebase_data .jira_issue } : { output .error } " )
252
- await retry (task , output .error )
291
+ logger .warning (f"Rebase failed for { rebase_data .jira_issue } : { state . rebase_result .error } " )
292
+ await retry (task , state . rebase_result .error )
253
293
254
294
255
295
if __name__ == "__main__" :
0 commit comments