github.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import random
  2. import string
  3. from dataclasses import dataclass
  4. from typing import TYPE_CHECKING
  5. import requests
  6. from opendevin import config
  7. from opendevin.observation import AgentErrorObservation, Observation
  8. from opendevin.observation.message import AgentMessageObservation
  9. from opendevin.observation.run import CmdOutputObservation
  10. from opendevin.schema import ActionType
  11. from opendevin.schema.config import ConfigType
  12. from .base import ExecutableAction
  13. if TYPE_CHECKING:
  14. from opendevin.controller import AgentController
  15. @dataclass
  16. class GitHubPushAction(ExecutableAction):
  17. """This pushes the current branch to github.
  18. To use this, you need to set the GITHUB_TOKEN environment variable.
  19. The agent will return a message with a URL that you can click to make a pull
  20. request.
  21. Attributes:
  22. owner: The owner of the source repo
  23. repo: The name of the source repo
  24. branch: The branch to push
  25. action: The action identifier
  26. """
  27. owner: str
  28. repo: str
  29. branch: str
  30. action: str = ActionType.PUSH
  31. async def run(self, controller: 'AgentController') -> Observation:
  32. github_token = config.get(ConfigType.GITHUB_TOKEN)
  33. if not github_token:
  34. return AgentErrorObservation(
  35. 'GITHUB_TOKEN is not set'
  36. )
  37. # Create a random short string to use as a temporary remote
  38. random_remote = ''.join(
  39. ['opendevin_temp_'] + random.choices(string.ascii_lowercase, k=5)
  40. )
  41. # Set the temporary remote
  42. new_url = f'https://{github_token}@github.com/{self.owner}/{self.repo}.git'
  43. command = f'git remote add {random_remote} {new_url}'
  44. remote_add_result = controller.action_manager.run_command(
  45. command, background=False
  46. )
  47. if (
  48. not isinstance(remote_add_result, CmdOutputObservation)
  49. or remote_add_result.exit_code != 0
  50. ):
  51. return remote_add_result
  52. # Push the branch to the temporary remote
  53. command = f'git push {random_remote} {self.branch}'
  54. push_result = controller.action_manager.run_command(command, background=False)
  55. # Delete the temporary remote
  56. command = f'git remote remove {random_remote}'
  57. remote_remove_result = controller.action_manager.run_command(
  58. command, background=False
  59. )
  60. if (
  61. not isinstance(remote_remove_result, CmdOutputObservation)
  62. or remote_remove_result.exit_code != 0
  63. ):
  64. return remote_remove_result
  65. return push_result
  66. @property
  67. def message(self) -> str:
  68. return f'Pushing branch {self.branch} to {self.owner}/{self.repo}'
  69. @dataclass
  70. class GitHubSendPRAction(ExecutableAction):
  71. """An action to send a github PR.
  72. To use this, you need to set the GITHUB_TOKEN environment variable.
  73. Attributes:
  74. owner: The owner of the source repo
  75. repo: The name of the source repo
  76. title: The title of the PR
  77. head: The branch to send the PR from
  78. head_repo: The repo to send the PR from
  79. base: The branch to send the PR to
  80. body: The body of the PR
  81. """
  82. owner: str
  83. repo: str
  84. title: str
  85. head: str
  86. head_repo: str | None
  87. base: str
  88. body: str | None
  89. action: str = ActionType.SEND_PR
  90. async def run(self, controller: 'AgentController') -> Observation:
  91. github_token = config.get(ConfigType.GITHUB_TOKEN)
  92. if not github_token:
  93. return AgentErrorObservation(
  94. 'GITHUB_TOKEN is not set'
  95. )
  96. # API URL to create the pull request
  97. url = f'https://api.github.com/repos/{self.owner}/{self.repo}/pulls'
  98. # Headers to authenticate and request JSON responses
  99. headers = {
  100. 'Authorization': f'token {github_token}',
  101. 'Accept': 'application/vnd.github.v3+json',
  102. }
  103. # Data for the pull request
  104. data = {
  105. 'title': self.title,
  106. 'head': self.head,
  107. 'head_repo': self.head_repo,
  108. 'base': self.base,
  109. 'body': self.body,
  110. }
  111. data = {k: v for k, v in data.items() if v is not None}
  112. # Make the request
  113. response = requests.post(url, headers=headers, json=data)
  114. # Check for errors
  115. if response.status_code == 201:
  116. return AgentMessageObservation(
  117. 'Pull request created successfully!\n'
  118. f'Pull request URL:{response.json()["html_url"]}'
  119. )
  120. else:
  121. return AgentErrorObservation(
  122. 'Failed to create pull request\n'
  123. f'Status code: {response.status_code}\n'
  124. f'Response: {response.text}'
  125. )
  126. @property
  127. def message(self) -> str:
  128. return (
  129. f'Sending PR from {self.head_repo}:{self.head} to '
  130. f'{self.owner}:{self.base}'
  131. )