mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 09:13:27 +00:00
Merge pull request #902 from langchain-ai/yogesh/generated_PR_and_custom_prompting
feat: add commit_and_open_pr tool and custom agent prompting [closes #892, closes #887]
This commit is contained in:
commit
1c847d56ab
5 changed files with 230 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.
|
All code execution and file operations happen in this sandbox environment.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Important:**
|
**Important:**
|
||||||
- Use `{working_dir}` as your working directory for all operations
|
- 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:
|
def construct_system_prompt(
|
||||||
return SYSTEM_PROMPT.format(working_dir=working_dir)
|
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
|
# Local import for encryption
|
||||||
from langchain_anthropic import ChatAnthropic
|
from langchain_anthropic import ChatAnthropic
|
||||||
|
|
||||||
from .encryption import decrypt_token
|
from .encryption import decrypt_token
|
||||||
from .prompt import construct_system_prompt
|
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:
|
def _get_langsmith_api_key() -> str | None:
|
||||||
|
|
@ -456,6 +457,28 @@ async def post_to_linear_after_model( # noqa: PLR0911, PLR0912
|
||||||
return None
|
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
|
@after_agent
|
||||||
async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
||||||
state: AgentState,
|
state: AgentState,
|
||||||
|
|
@ -465,7 +488,6 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
||||||
logger.info("After-agent middleware started")
|
logger.info("After-agent middleware started")
|
||||||
pr_url = None
|
pr_url = None
|
||||||
pr_number = None
|
pr_number = None
|
||||||
pr_title = "feat: Open SWE PR"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
@ -485,6 +507,21 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
|
||||||
linear_issue = configurable.get("linear_issue", {})
|
linear_issue = configurable.get("linear_issue", {})
|
||||||
linear_issue_id = linear_issue.get("id")
|
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 not thread_id:
|
||||||
if linear_issue_id and last_message_content:
|
if linear_issue_id and last_message_content:
|
||||||
comment = f"""🤖 **Agent Response**
|
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")
|
await asyncio.to_thread(sandbox_backend.execute, f"cd {repo_dir} && git add -A")
|
||||||
|
|
||||||
|
safe_commit_msg = commit_message.replace("'", "'\\''")
|
||||||
await asyncio.to_thread(
|
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")
|
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:
|
if remote_result.exit_code == 0:
|
||||||
remote_url = remote_result.output.strip()
|
remote_url = remote_result.output.strip()
|
||||||
if "github.com" in remote_url and "@" not in remote_url:
|
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}@")
|
auth_url = remote_url.replace("https://", f"https://git:{github_token}@")
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
sandbox_backend.execute,
|
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}"
|
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)
|
base_branch = await get_github_default_branch(repo_owner, repo_name, github_token)
|
||||||
logger.info("Using base branch: %s", base_branch)
|
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(
|
pr_url, pr_number = await create_github_pr(
|
||||||
repo_owner=repo_owner,
|
repo_owner=repo_owner,
|
||||||
repo_name=repo_name,
|
repo_name=repo_name,
|
||||||
|
|
@ -911,13 +944,19 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
|
|
||||||
_SANDBOX_BACKENDS[thread_id] = sandbox_backend
|
_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)
|
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
||||||
return create_deep_agent(
|
return create_deep_agent(
|
||||||
model=ChatAnthropic(
|
model=ChatAnthropic(model="claude-opus-4-6", max_tokens=20_000),
|
||||||
model="claude-opus-4-6", max_tokens=20_000
|
system_prompt=construct_system_prompt(
|
||||||
), # TODO: Actually pass a model here
|
repo_dir,
|
||||||
system_prompt=construct_system_prompt(repo_dir),
|
linear_project_id=linear_project_id,
|
||||||
tools=[http_request, fetch_url],
|
linear_issue_number=linear_issue_number,
|
||||||
|
),
|
||||||
|
tools=[http_request, fetch_url, commit_and_open_pr],
|
||||||
backend=sandbox_backend,
|
backend=sandbox_backend,
|
||||||
middleware=[
|
middleware=[
|
||||||
check_message_queue_before_model,
|
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 .fetch_url import fetch_url
|
||||||
from .http_request import http_request
|
from .http_request import http_request
|
||||||
|
|
||||||
__all__ = ["fetch_url", "http_request"]
|
__all__ = ["commit_and_open_pr", "fetch_url", "http_request"]
|
||||||
|
|
|
||||||
94
apps/agent/agent/tools/commit_and_open_pr.py
Normal file
94
apps/agent/agent/tools/commit_and_open_pr.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
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 lowercase description> [closes <PROJECT_ID>-<ISSUE_NUMBER>]
|
||||||
|
|
||||||
|
The description MUST be entirely lowercase (no capital letters).
|
||||||
|
|
||||||
|
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."
|
"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] = {
|
configurable: dict[str, Any] = {
|
||||||
"repo": repo_config,
|
"repo": repo_config,
|
||||||
"linear_issue": {
|
"linear_issue": {
|
||||||
"id": issue_id,
|
"id": issue_id,
|
||||||
"title": title,
|
"title": title,
|
||||||
"url": full_issue.get("url", "") or issue_data.get("url", ""),
|
"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:
|
if github_token:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue