devai-api/app/gitlab_utils.py (100 lines of code) (raw):
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import gitlab
import os
from git import Repo
from langchain.agents import AgentType, initialize_agent
from langchain_community.agent_toolkits.gitlab.toolkit import GitLabToolkit
from langchain_community.utilities.gitlab import GitLabAPIWrapper
from google.cloud.aiplatform import telemetry
from langchain_google_vertexai import ChatVertexAI
from vertexai.generative_models import GenerativeModel
from .github_utils import delete_folder
from .constants import USER_AGENT, MODEL_NAME
from .file_processor import format_files_as_string
LLM_INSTRUCTION_TEMPLATE = """You are principal software engineer and given requirements below for implementation.
You must follow rules when generating implementation:
- you must use existing codebase from the context below
- in your response, generate complete source code files and not diffs
- in your response, include full filepath with name before file content, excluding repository name
- must return response using sample format
REQUIREMENTS:
{prompt}
SAMPLE RESPONSE FORMAT:
menu-service/src/main/java/org/google/demo/Menu.java
OLD <<<<
existing code from the context below for the file
>>>> OLD
NEW <<<<
new generated code by LLM
>>>> NEW
CONTEXT:
{codebase}
"""
PR_PROMPT_TEMPLATE = """Create GitLab merge request using provided details below.
Update existing files or Create new files, commit them and push them to opened merge request.
DETAILS:
{response_text}
"""
class MergeRequestError(Exception):
"""Custom exception for merge request creation."""
pass
def _create_branch() -> str:
"""Creates a new branch in GitLab for a merge request.
Returns:
str: The name of the newly created branch.
"""
gitlab_url = os.environ["GITLAB_URL"]
gitlab_base_branch = os.environ["GITLAB_BASE_BRANCH"]
gitlab_repo_name = os.environ["GITLAB_REPOSITORY"]
gitlab_access_token = os.environ["GITLAB_PERSONAL_ACCESS_TOKEN"]
gl = gitlab.Gitlab(gitlab_url, private_token=gitlab_access_token)
project = gl.projects.get(gitlab_repo_name)
new_branch_name = f'feature/generated-{datetime.datetime.now().strftime("%m%d%Y-%H%M")}'
project.branches.create({'branch': new_branch_name, 'ref': gitlab_base_branch})
print("Created new branch:", new_branch_name)
return new_branch_name
def _clone_repo(repo_name: str) -> Repo:
"""Clones a GitLab repository to the local file system.
Args:
repo_name (str): The name of the repository to clone.
Returns:
Repo: The cloned repository object.
"""
gitlab_repo_name = os.environ["GITLAB_REPOSITORY"]
gitlab_access_token = os.environ["GITLAB_PERSONAL_ACCESS_TOKEN"]
repo = Repo.clone_from(
f"https://oauth2:{gitlab_access_token}@gitlab.com/{gitlab_repo_name}.git",
repo_name,
)
return repo
def _init_agent(new_gitlab_branch: str):
"""Initializes an agent with the GitLab toolkit.
Args:
new_gitlab_branch (str): The name of the branch to use for the agent.
Returns:
Agent: The initialized agent.
"""
gitlab = GitLabAPIWrapper(gitlab_branch=new_gitlab_branch)
toolkit = GitLabToolkit.from_gitlab_api_wrapper(gitlab)
with telemetry.tool_context_manager(USER_AGENT):
llm = ChatVertexAI(model_name=MODEL_NAME,
convert_system_message_to_human=True,
temperature=0.2,
max_output_tokens=8192)
agent = initialize_agent(
toolkit.get_tools(),
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
handle_parsing_errors=True,
max_iterations=10,
return_intermediate_steps=True,
early_stopping_method="generate",
)
return agent
def _generate_llm_instructions(prompt: str, codebase: str) -> str:
"""Generates instructions for the Language Model (LLM).
Args:
prompt (str): The user prompt.
codebase (str): The codebase context.
Returns:
str: The formatted instructions for the LLM.
"""
return LLM_INSTRUCTION_TEMPLATE.format(prompt=prompt, codebase=codebase)
def _get_llm_response(instructions: str, repo_name: str) -> str:
"""Sends instructions to the LLM and retrieves its response.
Args:
instructions (str): The instructions for the LLM.
repo_name (str): The name of the repository.
Returns:
str: The response from the LLM.
"""
code_chat_model = GenerativeModel(MODEL_NAME)
with telemetry.tool_context_manager(USER_AGENT):
code_chat = code_chat_model.start_chat(response_validation=False)
response = code_chat.send_message(instructions)
# Remove repo name from the response
return response.text.replace(f"{repo_name}/", "")
def _create_gitlab_merge_request(response_text: str, agent) -> None:
"""Creates a GitLab merge request using the provided response text and agent.
Args:
response_text (str): The response text to use for the merge request.
agent: The agent to use for creating the merge request.
"""
pr_prompt = PR_PROMPT_TEMPLATE.format(response_text=response_text)
agent.invoke(pr_prompt)
def create_merge_request(prompt: str) -> str:
"""Creates a new GitLab merge request.
Args:
prompt (str): The prompt describing the changes to be made.
Returns:
str: The implementation details returned by the LLM.
Raises:
MergeRequestError: If an error occurs during the merge request creation.
"""
_, repo_name = get_repo_details()
try:
delete_folder(repo_name)
_clone_repo(repo_name)
codebase = load_codebase(repo_name, prompt)
instructions = _generate_llm_instructions(prompt, codebase)
implementation_details = _get_llm_response(instructions, repo_name)
new_gitlab_branch = _create_branch()
agent = _init_agent(new_gitlab_branch)
_create_gitlab_merge_request(implementation_details, agent)
return implementation_details
except Exception as e:
raise MergeRequestError(f"Failed to create merge request: {e}") from e
finally:
delete_folder(repo_name)
def get_repo_details() -> tuple[str, str]:
"""Extracts the repository owner and name from the GITLAB_REPOSITORY environment variable.
Returns:
tuple[str, str]: A tuple containing the repository owner and name.
"""
gitlab_repo_name = os.environ["GITLAB_REPOSITORY"]
repo = gitlab_repo_name.split("/")
return (repo[0], repo[1])
def load_codebase(repo_name: str, prompt: str) -> str:
"""Loads the codebase from the specified repository.
Args:
repo_name (str): The name of the repository.
prompt (str): The user prompt.
Returns:
str: The formatted codebase as a string.
"""
# Defaults to repo root
service = ""
if "menu service" in prompt.lower():
service = "menu-service"
if "customer service" in prompt.lower():
service = "customer-service/src"
if "customer ui" in prompt.lower():
service = "customer-ui/src"
if "inventory service" in prompt.lower():
service = "inventory-service/spanner"
if "order-service" in prompt.lower():
service = "order-service"
code_path = f"{repo_name}/{service}"
return format_files_as_string(code_path)