mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
feat: add commit_and_open_pr tool and custom agent prompting
This commit is contained in:
parent
8f4fd4a555
commit
b89ead0794
5 changed files with 228 additions and 17 deletions
|
|
@ -4,11 +4,80 @@ You are operating in a **remote Linux sandbox** at `{working_dir}`.
|
|||
|
||||
All code execution and file operations happen in this sandbox environment.
|
||||
|
||||
|
||||
|
||||
**Important:**
|
||||
- Use `{working_dir}` as your working directory for all operations
|
||||
|
||||
|
||||
|
||||
### Code Style
|
||||
|
||||
- NEVER add inline comments to code
|
||||
- Any docstrings on functions you add or modify must be VERY concise (1 line preferred)
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
If the repository needs dependency installation, run:
|
||||
```
|
||||
uv run poe install-deps
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Committing Changes and Opening Pull Requests
|
||||
|
||||
When you have completed your implementation, follow these steps in order:
|
||||
|
||||
1. **Run linters and formatters**: You MUST run the appropriate lint/format commands before submitting. Determine which languages are in the repo and run the corresponding commands:
|
||||
|
||||
**Python** (if repo contains `.py` files):
|
||||
- `make format` then `make lint`
|
||||
|
||||
**Frontend / TypeScript / JavaScript** (if repo contains `package.json`):
|
||||
- `yarn format` then `yarn lint`
|
||||
|
||||
**Go** (if repo contains `.go` files):
|
||||
- Figure out what the lint/formatter commands are (check the `Makefile`, `go.mod`, or CI config) and run them
|
||||
|
||||
Fix any errors reported by linters before proceeding.
|
||||
|
||||
2. **Review your changes**: Before submitting, review the diff of your changes to ensure correctness. Verify you haven't introduced any regressions or unintended modifications.
|
||||
|
||||
3. **Submit via `commit_and_open_pr` tool**: Call this tool as the final step. It will commit all changes, push to a branch, and create a pull request.
|
||||
|
||||
**PR Title** (keep under 70 characters):
|
||||
```
|
||||
<type>: <concise description> [closes {linear_project_id}-{linear_issue_number}]
|
||||
```
|
||||
Where type is one of: `fix` (bug fix), `feat` (new feature), `chore` (maintenance), `ci` (CI/CD)
|
||||
|
||||
**PR Body**:
|
||||
```
|
||||
## Description
|
||||
<Explain WHY this PR is needed, list the changes, and reference the Linear issue>
|
||||
|
||||
## Test Plan
|
||||
- [ ] <specific verification step>
|
||||
```
|
||||
|
||||
**Commit message**: Should be concise and focus on the "why" rather than the "what". If not provided, the PR title is used.
|
||||
|
||||
|
||||
|
||||
|
||||
Always call `commit_and_open_pr` as the final step once your implementation is complete and code quality checks pass.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def construct_system_prompt(working_dir: str) -> str:
|
||||
return SYSTEM_PROMPT.format(working_dir=working_dir)
|
||||
def construct_system_prompt(
|
||||
working_dir: str,
|
||||
linear_project_id: str = "",
|
||||
linear_issue_number: str = "",
|
||||
) -> str:
|
||||
return SYSTEM_PROMPT.format(
|
||||
working_dir=working_dir,
|
||||
linear_project_id=linear_project_id or "<PROJECT_ID>",
|
||||
linear_issue_number=linear_issue_number or "<ISSUE_NUMBER>",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,9 +30,10 @@ from deepagents.backends.sandbox import SandboxBackendProtocol
|
|||
|
||||
# Local import for encryption
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
from .encryption import decrypt_token
|
||||
from .prompt import construct_system_prompt
|
||||
from .tools import fetch_url, http_request
|
||||
from .tools import commit_and_open_pr, fetch_url, http_request
|
||||
|
||||
|
||||
def _get_langsmith_api_key() -> str | None:
|
||||
|
|
@ -456,6 +457,28 @@ async def post_to_linear_after_model( # noqa: PLR0911, PLR0912
|
|||
return None
|
||||
|
||||
|
||||
def _extract_pr_params_from_messages(messages: list) -> dict[str, str] | None:
|
||||
"""Extract PR title/body/commit_message from the last commit_and_open_pr tool result."""
|
||||
for msg in reversed(messages):
|
||||
if isinstance(msg, dict):
|
||||
content = msg.get("content", "")
|
||||
name = msg.get("name", "")
|
||||
else:
|
||||
content = getattr(msg, "content", "")
|
||||
name = getattr(msg, "name", "")
|
||||
|
||||
if name == "commit_and_open_pr" and content:
|
||||
import json as _json
|
||||
|
||||
try:
|
||||
parsed = _json.loads(content) if isinstance(content, str) else content
|
||||
if isinstance(parsed, dict) and "title" in parsed:
|
||||
return parsed
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@after_agent
|
||||
async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
||||
state: AgentState,
|
||||
|
|
@ -465,7 +488,6 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
|||
logger.info("After-agent middleware started")
|
||||
pr_url = None
|
||||
pr_number = None
|
||||
pr_title = "feat: Open SWE PR"
|
||||
|
||||
try:
|
||||
config = get_config()
|
||||
|
|
@ -485,6 +507,21 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
|||
linear_issue = configurable.get("linear_issue", {})
|
||||
linear_issue_id = linear_issue.get("id")
|
||||
|
||||
pr_params = _extract_pr_params_from_messages(messages)
|
||||
|
||||
if not pr_params:
|
||||
logger.info("No commit_and_open_pr tool call found, skipping PR creation")
|
||||
if linear_issue_id and last_message_content:
|
||||
comment = f""" **Agent Response**
|
||||
|
||||
{last_message_content}"""
|
||||
await comment_on_linear_issue(linear_issue_id, comment)
|
||||
return None
|
||||
|
||||
pr_title = pr_params.get("title", "feat: Open SWE PR")
|
||||
pr_body = pr_params.get("body", "Automated PR created by Open SWE agent.")
|
||||
commit_message = pr_params.get("commit_message", pr_title)
|
||||
|
||||
if not thread_id:
|
||||
if linear_issue_id and last_message_content:
|
||||
comment = f"""🤖 **Agent Response**
|
||||
|
|
@ -564,8 +601,9 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
|||
|
||||
await asyncio.to_thread(sandbox_backend.execute, f"cd {repo_dir} && git add -A")
|
||||
|
||||
safe_commit_msg = commit_message.replace("'", "'\\''")
|
||||
await asyncio.to_thread(
|
||||
sandbox_backend.execute, f'cd {repo_dir} && git commit -m "feat: Open SWE PR"'
|
||||
sandbox_backend.execute, f"cd {repo_dir} && git commit -m '{safe_commit_msg}'"
|
||||
)
|
||||
|
||||
encrypted_token = configurable.get("github_token_encrypted")
|
||||
|
|
@ -579,7 +617,6 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
|||
if remote_result.exit_code == 0:
|
||||
remote_url = remote_result.output.strip()
|
||||
if "github.com" in remote_url and "@" not in remote_url:
|
||||
# Convert https://github.com/owner/repo.git to https://git:token@github.com/owner/repo.git
|
||||
auth_url = remote_url.replace("https://", f"https://git:{github_token}@")
|
||||
await asyncio.to_thread(
|
||||
sandbox_backend.execute,
|
||||
|
|
@ -590,13 +627,9 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
|||
sandbox_backend.execute, f"cd {repo_dir} && git push origin {target_branch}"
|
||||
)
|
||||
|
||||
# Get default branch from GitHub API (most reliable method)
|
||||
base_branch = await get_github_default_branch(repo_owner, repo_name, github_token)
|
||||
logger.info("Using base branch: %s", base_branch)
|
||||
|
||||
pr_title = "feat: Open SWE PR"
|
||||
pr_body = "Automated PR created by Open SWE agent."
|
||||
|
||||
pr_url, pr_number = await create_github_pr(
|
||||
repo_owner=repo_owner,
|
||||
repo_name=repo_name,
|
||||
|
|
@ -911,13 +944,19 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
|||
|
||||
_SANDBOX_BACKENDS[thread_id] = sandbox_backend
|
||||
|
||||
linear_issue = config["configurable"].get("linear_issue", {})
|
||||
linear_project_id = linear_issue.get("linear_project_id", "")
|
||||
linear_issue_number = linear_issue.get("linear_issue_number", "")
|
||||
|
||||
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
||||
return create_deep_agent(
|
||||
model=ChatAnthropic(
|
||||
model="claude-opus-4-6", max_tokens=20_000
|
||||
), # TODO: Actually pass a model here
|
||||
system_prompt=construct_system_prompt(repo_dir),
|
||||
tools=[http_request, fetch_url],
|
||||
model=ChatAnthropic(model="claude-opus-4-6", max_tokens=20_000),
|
||||
system_prompt=construct_system_prompt(
|
||||
repo_dir,
|
||||
linear_project_id=linear_project_id,
|
||||
linear_issue_number=linear_issue_number,
|
||||
),
|
||||
tools=[http_request, fetch_url, commit_and_open_pr],
|
||||
backend=sandbox_backend,
|
||||
middleware=[
|
||||
check_message_queue_before_model,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from .commit_and_open_pr import commit_and_open_pr
|
||||
from .fetch_url import fetch_url
|
||||
from .http_request import http_request
|
||||
|
||||
__all__ = ["fetch_url", "http_request"]
|
||||
__all__ = ["commit_and_open_pr", "fetch_url", "http_request"]
|
||||
|
|
|
|||
92
apps/agent/agent/tools/commit_and_open_pr.py
Normal file
92
apps/agent/agent/tools/commit_and_open_pr.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def commit_and_open_pr(
|
||||
title: str,
|
||||
body: str,
|
||||
commit_message: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Commit all current changes and open a GitHub Pull Request.
|
||||
|
||||
You MUST call this tool when you have completed your work and want to
|
||||
submit your changes for review. This is the final step in your workflow.
|
||||
|
||||
Before calling this tool, ensure you have:
|
||||
1. Reviewed your changes for correctness
|
||||
2. Run `make format` and `make lint` if a Makefile exists in the repo root
|
||||
|
||||
## Title Format (REQUIRED — keep under 70 characters)
|
||||
|
||||
The PR title MUST follow this exact format:
|
||||
|
||||
<type>: <short description> [closes <PROJECT_ID>-<ISSUE_NUMBER>]
|
||||
|
||||
Where <type> is one of:
|
||||
- fix: for bug fixes
|
||||
- feat: for new features
|
||||
- chore: for maintenance tasks (deps, configs, cleanup)
|
||||
- ci: for CI/CD changes
|
||||
|
||||
The [closes ...] suffix links and auto-closes the Linear ticket.
|
||||
Use the linear_project_id and linear_issue_number from your context.
|
||||
|
||||
Examples:
|
||||
- "fix: resolve null pointer in user auth [closes AA-123]"
|
||||
- "feat: add dark mode toggle to settings [closes ENG-456]"
|
||||
- "chore: upgrade dependencies to latest versions [closes OPS-789]"
|
||||
|
||||
## Body Format (REQUIRED)
|
||||
|
||||
The PR body MUST follow this exact template:
|
||||
|
||||
## Description
|
||||
<Explain WHY this PR is needed. Include:
|
||||
- List of changes made
|
||||
- Reference to the Linear issue or design docs
|
||||
- Any context on the approach taken>
|
||||
|
||||
## Test Plan
|
||||
- [ ] <specific test step 1>
|
||||
- [ ] <specific test step 2>
|
||||
|
||||
Example body:
|
||||
|
||||
## Description
|
||||
Fixes the null pointer exception that occurs when a user without
|
||||
a profile attempts to authenticate. The root cause was a missing
|
||||
null check in the `getProfile` method.
|
||||
|
||||
Changes:
|
||||
- Added null check in `auth/getProfile.ts`
|
||||
- Added fallback default profile object
|
||||
- Updated related unit tests
|
||||
|
||||
Resolves AA-123
|
||||
|
||||
## Test Plan
|
||||
- [ ] Verify login works for users without profiles
|
||||
- [ ] Verify existing users are unaffected
|
||||
- [ ] Run `yarn test` and confirm all tests pass
|
||||
|
||||
## Commit Message
|
||||
|
||||
The commit message should be concise (1-2 sentences) and focus on the "why"
|
||||
rather than the "what". Summarize the nature of the changes: new feature,
|
||||
bug fix, refactoring, etc. If not provided, the PR title is used.
|
||||
|
||||
Args:
|
||||
title: PR title following the format above (e.g. "fix: resolve auth bug [closes AA-123]")
|
||||
body: PR description following the template above with ## Description and ## Test Plan
|
||||
commit_message: Optional git commit message. If not provided, the PR title is used.
|
||||
|
||||
Returns:
|
||||
Dictionary with the result of the operation including PR URL if successful.
|
||||
"""
|
||||
return {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"commit_message": commit_message or title,
|
||||
}
|
||||
|
|
@ -552,13 +552,23 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
|
|||
"When you're done, commit and push your changes."
|
||||
)
|
||||
|
||||
identifier = full_issue.get("identifier", "") or issue_data.get("identifier", "")
|
||||
linear_project_id = ""
|
||||
linear_issue_number = ""
|
||||
if identifier and "-" in identifier:
|
||||
parts = identifier.split("-", 1)
|
||||
linear_project_id = parts[0]
|
||||
linear_issue_number = parts[1]
|
||||
|
||||
configurable: dict[str, Any] = {
|
||||
"repo": repo_config,
|
||||
"linear_issue": {
|
||||
"id": issue_id,
|
||||
"title": title,
|
||||
"url": full_issue.get("url", "") or issue_data.get("url", ""),
|
||||
"identifier": full_issue.get("identifier", "") or issue_data.get("identifier", ""),
|
||||
"identifier": identifier,
|
||||
"linear_project_id": linear_project_id,
|
||||
"linear_issue_number": linear_issue_number,
|
||||
},
|
||||
}
|
||||
if github_token:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue