mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 05:43:27 +00:00
feat: add linear_comment tool, remove auto-comment middleware (#994)
* feat: add linear_comment tool, remove auto-comment middleware * ⏺ feat: pass triggering user name to linear_comment tool for @mentions * linting * Apply suggestion from @bracesproul Co-authored-by: Brace Sproul <braceasproul@gmail.com> * Apply suggestion from @bracesproul Co-authored-by: Brace Sproul <braceasproul@gmail.com> * feat: add ticket_id arg to linear_comment tool and include it in agent prompt * Apply suggestions from code review --------- Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
parent
4c94404b76
commit
5bced68e72
8 changed files with 80 additions and 248 deletions
|
|
@ -1,11 +1,9 @@
|
||||||
from .check_message_queue import check_message_queue_before_model
|
from .check_message_queue import check_message_queue_before_model
|
||||||
from .open_pr import open_pr_if_needed
|
from .open_pr import open_pr_if_needed
|
||||||
from .post_to_linear import post_to_linear_after_model
|
|
||||||
from .tool_error_handler import ToolErrorMiddleware
|
from .tool_error_handler import ToolErrorMiddleware
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ToolErrorMiddleware",
|
"ToolErrorMiddleware",
|
||||||
"check_message_queue_before_model",
|
"check_message_queue_before_model",
|
||||||
"open_pr_if_needed",
|
"open_pr_if_needed",
|
||||||
"post_to_linear_after_model",
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
"""After-agent middleware that creates a GitHub PR and comments on Linear.
|
"""After-agent middleware that creates a GitHub PR if needed.
|
||||||
|
|
||||||
Runs once after the agent finishes. If the agent called the
|
Runs once after the agent finishes as a safety net. If the agent called
|
||||||
``commit_and_open_pr`` tool, this middleware commits any remaining changes,
|
``commit_and_open_pr`` and it already succeeded, this is a no-op. Otherwise it
|
||||||
pushes to a feature branch, opens a GitHub PR, and posts a summary comment
|
commits any remaining changes, pushes to a feature branch, and opens a GitHub PR.
|
||||||
back to the originating Linear issue.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -31,8 +30,6 @@ from ..utils.github import (
|
||||||
git_has_unpushed_commits,
|
git_has_unpushed_commits,
|
||||||
git_push,
|
git_push,
|
||||||
)
|
)
|
||||||
from ..utils.linear import comment_on_linear_issue
|
|
||||||
from ..utils.messages import extract_text_content
|
|
||||||
from ..utils.sandbox_state import get_sandbox_backend
|
from ..utils.sandbox_state import get_sandbox_backend
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -63,10 +60,8 @@ async def open_pr_if_needed(
|
||||||
state: AgentState,
|
state: AgentState,
|
||||||
runtime: Runtime,
|
runtime: Runtime,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Middleware that commits/pushes changes and comments on Linear after agent runs."""
|
"""Middleware that commits/pushes changes after agent runs if `commit_and_open_pr` tool didn't."""
|
||||||
logger.info("After-agent middleware started")
|
logger.info("After-agent middleware started")
|
||||||
pr_url = None
|
|
||||||
pr_number = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
@ -74,62 +69,15 @@ async def open_pr_if_needed(
|
||||||
thread_id = configurable.get("thread_id")
|
thread_id = configurable.get("thread_id")
|
||||||
logger.debug("Middleware running for thread %s", thread_id)
|
logger.debug("Middleware running for thread %s", thread_id)
|
||||||
|
|
||||||
last_message_content = ""
|
|
||||||
messages = state.get("messages", [])
|
messages = state.get("messages", [])
|
||||||
if messages:
|
|
||||||
last_message = messages[-1]
|
|
||||||
if isinstance(last_message, dict):
|
|
||||||
last_message_content = extract_text_content(last_message.get("content", ""))
|
|
||||||
elif hasattr(last_message, "content"):
|
|
||||||
last_message_content = extract_text_content(last_message.content)
|
|
||||||
|
|
||||||
linear_issue = configurable.get("linear_issue", {})
|
|
||||||
linear_issue_id = linear_issue.get("id")
|
|
||||||
|
|
||||||
pr_payload = _extract_pr_params_from_messages(messages)
|
pr_payload = _extract_pr_params_from_messages(messages)
|
||||||
|
|
||||||
if not pr_payload:
|
if not pr_payload:
|
||||||
logger.info("No commit_and_open_pr tool call found, skipping PR creation")
|
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
|
return None
|
||||||
|
|
||||||
if "success" in pr_payload:
|
if "success" in pr_payload:
|
||||||
pr_url = pr_payload.get("pr_url")
|
# Tool already handled commit/push/PR creation
|
||||||
pr_existing = bool(pr_payload.get("pr_existing", False))
|
|
||||||
error = pr_payload.get("error")
|
|
||||||
if linear_issue_id and last_message_content:
|
|
||||||
if pr_url:
|
|
||||||
header = "Pull Request Updated" if pr_existing else "Pull Request Created"
|
|
||||||
action = "updated the existing" if pr_existing else "created a"
|
|
||||||
comment = f"""**{header}**
|
|
||||||
|
|
||||||
I've {action} pull request to address this issue:
|
|
||||||
|
|
||||||
{pr_url}
|
|
||||||
|
|
||||||
---
|
|
||||||
**Agent Response**
|
|
||||||
|
|
||||||
{last_message_content}"""
|
|
||||||
elif error:
|
|
||||||
comment = f"""**Pull Request Error**
|
|
||||||
|
|
||||||
{error}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Agent Response**
|
|
||||||
|
|
||||||
{last_message_content}"""
|
|
||||||
else:
|
|
||||||
comment = f""" **Agent Response**
|
|
||||||
|
|
||||||
{last_message_content}"""
|
|
||||||
await comment_on_linear_issue(linear_issue_id, comment)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
pr_title = pr_payload.get("title", "feat: Open SWE PR")
|
pr_title = pr_payload.get("title", "feat: Open SWE PR")
|
||||||
|
|
@ -137,11 +85,6 @@ I've {action} pull request to address this issue:
|
||||||
commit_message = pr_payload.get("commit_message", pr_title)
|
commit_message = pr_payload.get("commit_message", pr_title)
|
||||||
|
|
||||||
if not thread_id:
|
if not thread_id:
|
||||||
if linear_issue_id and last_message_content:
|
|
||||||
comment = f"""🤖 **Agent Response**
|
|
||||||
|
|
||||||
{last_message_content}"""
|
|
||||||
await comment_on_linear_issue(linear_issue_id, comment)
|
|
||||||
raise ValueError("No thread_id found in config")
|
raise ValueError("No thread_id found in config")
|
||||||
|
|
||||||
repo_config = configurable.get("repo", {})
|
repo_config = configurable.get("repo", {})
|
||||||
|
|
@ -149,15 +92,9 @@ I've {action} pull request to address this issue:
|
||||||
repo_name = repo_config.get("name")
|
repo_name = repo_config.get("name")
|
||||||
|
|
||||||
sandbox_backend = await get_sandbox_backend(thread_id)
|
sandbox_backend = await get_sandbox_backend(thread_id)
|
||||||
|
|
||||||
repo_dir = f"/workspace/{repo_name}"
|
repo_dir = f"/workspace/{repo_name}"
|
||||||
|
|
||||||
if not sandbox_backend or not repo_dir:
|
if not sandbox_backend or not repo_dir:
|
||||||
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
|
return None
|
||||||
|
|
||||||
has_uncommitted_changes = await asyncio.to_thread(
|
has_uncommitted_changes = await asyncio.to_thread(
|
||||||
|
|
@ -173,17 +110,11 @@ I've {action} pull request to address this issue:
|
||||||
|
|
||||||
if not has_changes:
|
if not has_changes:
|
||||||
logger.info("No changes detected, skipping PR creation")
|
logger.info("No changes detected, 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
|
return None
|
||||||
|
|
||||||
logger.info("Changes detected, preparing PR for thread %s", thread_id)
|
logger.info("Changes detected, preparing PR for thread %s", thread_id)
|
||||||
|
|
||||||
current_branch = await asyncio.to_thread(git_current_branch, sandbox_backend, repo_dir)
|
current_branch = await asyncio.to_thread(git_current_branch, sandbox_backend, repo_dir)
|
||||||
|
|
||||||
target_branch = f"open-swe/{thread_id}"
|
target_branch = f"open-swe/{thread_id}"
|
||||||
|
|
||||||
if current_branch != target_branch:
|
if current_branch != target_branch:
|
||||||
|
|
@ -212,7 +143,7 @@ I've {action} pull request to address this issue:
|
||||||
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_url, pr_number, pr_existing = await create_github_pr(
|
await create_github_pr(
|
||||||
repo_owner=repo_owner,
|
repo_owner=repo_owner,
|
||||||
repo_name=repo_name,
|
repo_name=repo_name,
|
||||||
github_token=github_token,
|
github_token=github_token,
|
||||||
|
|
@ -222,48 +153,8 @@ I've {action} pull request to address this issue:
|
||||||
body=pr_body,
|
body=pr_body,
|
||||||
)
|
)
|
||||||
|
|
||||||
linear_issue = configurable.get("linear_issue", {})
|
|
||||||
linear_issue_id = linear_issue.get("id")
|
|
||||||
|
|
||||||
if linear_issue_id and last_message_content:
|
|
||||||
if pr_url:
|
|
||||||
header = "Pull Request Updated" if pr_existing else "Pull Request Created"
|
|
||||||
action = "updated the existing" if pr_existing else "created a"
|
|
||||||
comment = f"""**{header}**
|
|
||||||
|
|
||||||
I've {action} pull request to address this issue:
|
|
||||||
|
|
||||||
**[PR #{pr_number}: {pr_title}]({pr_url})**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
🤖 **Agent Response**
|
|
||||||
|
|
||||||
{last_message_content}"""
|
|
||||||
else:
|
|
||||||
comment = f""" **Agent Response**
|
|
||||||
|
|
||||||
{last_message_content}"""
|
|
||||||
await comment_on_linear_issue(linear_issue_id, comment)
|
|
||||||
|
|
||||||
logger.info("After-agent middleware completed successfully")
|
logger.info("After-agent middleware completed successfully")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception("Error in after-agent middleware")
|
logger.exception("Error in after-agent middleware")
|
||||||
try:
|
|
||||||
config = get_config()
|
|
||||||
configurable = config.get("configurable", {})
|
|
||||||
linear_issue = configurable.get("linear_issue", {})
|
|
||||||
linear_issue_id = linear_issue.get("id")
|
|
||||||
if linear_issue_id:
|
|
||||||
error_comment = f""" **Agent Error**
|
|
||||||
|
|
||||||
An error occurred while processing this issue:
|
|
||||||
|
|
||||||
```
|
|
||||||
{type(e).__name__}: {e}
|
|
||||||
```"""
|
|
||||||
await comment_on_linear_issue(linear_issue_id, error_comment)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to post error comment to Linear")
|
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
"""After-model middleware that posts AI responses to Linear.
|
|
||||||
|
|
||||||
Posts the first AI text response back to the originating Linear issue so
|
|
||||||
stakeholders can see progress without leaving Linear.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from langchain.agents.middleware import after_model
|
|
||||||
from langgraph.config import get_config
|
|
||||||
from langgraph.runtime import Runtime
|
|
||||||
|
|
||||||
from ..utils.linear import comment_on_linear_issue
|
|
||||||
from ..utils.messages import extract_text_content
|
|
||||||
from .check_message_queue import LinearNotifyState
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
MIN_MESSAGES_FOR_PREV_CHECK = 2
|
|
||||||
|
|
||||||
|
|
||||||
@after_model(state_schema=LinearNotifyState)
|
|
||||||
async def post_to_linear_after_model( # noqa: PLR0911, PLR0912
|
|
||||||
state: LinearNotifyState,
|
|
||||||
runtime: Runtime, # noqa: ARG001
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Middleware that posts AI responses to Linear after each model call.
|
|
||||||
|
|
||||||
Only posts if:
|
|
||||||
- This is a Linear-triggered conversation (has linear_issue in config)
|
|
||||||
- There's exactly 1 human message (initial request)
|
|
||||||
- The previous message was from human (not a tool result)
|
|
||||||
- The AI response has text content (not just tool calls)
|
|
||||||
- The message hasn't already been sent (tracked via linear_messages_sent_count)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
config = get_config()
|
|
||||||
configurable = config.get("configurable", {})
|
|
||||||
|
|
||||||
linear_issue = configurable.get("linear_issue", {})
|
|
||||||
linear_issue_id = linear_issue.get("id")
|
|
||||||
|
|
||||||
if not linear_issue_id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
messages = state.get("messages", [])
|
|
||||||
if not messages:
|
|
||||||
return None
|
|
||||||
|
|
||||||
sent_count = state.get("linear_messages_sent_count", 0)
|
|
||||||
|
|
||||||
human_message_count = 0
|
|
||||||
for msg in messages:
|
|
||||||
if isinstance(msg, dict):
|
|
||||||
role = msg.get("role", "")
|
|
||||||
else:
|
|
||||||
role = getattr(msg, "type", "") or getattr(msg, "role", "")
|
|
||||||
if role in ("human", "user"):
|
|
||||||
human_message_count += 1
|
|
||||||
|
|
||||||
if human_message_count != 1:
|
|
||||||
return None
|
|
||||||
|
|
||||||
last_message = messages[-1]
|
|
||||||
if isinstance(last_message, dict):
|
|
||||||
role = last_message.get("role", "")
|
|
||||||
content = last_message.get("content", "")
|
|
||||||
else:
|
|
||||||
role = getattr(last_message, "type", "") or getattr(last_message, "role", "")
|
|
||||||
content = getattr(last_message, "content", "")
|
|
||||||
|
|
||||||
if role not in ("ai", "assistant"):
|
|
||||||
return None
|
|
||||||
|
|
||||||
ai_message_count = 0
|
|
||||||
for msg in messages:
|
|
||||||
if isinstance(msg, dict):
|
|
||||||
r = msg.get("role", "")
|
|
||||||
else:
|
|
||||||
r = getattr(msg, "type", "") or getattr(msg, "role", "")
|
|
||||||
if r in ("ai", "assistant"):
|
|
||||||
ai_message_count += 1
|
|
||||||
|
|
||||||
if ai_message_count <= sent_count:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if len(messages) >= MIN_MESSAGES_FOR_PREV_CHECK:
|
|
||||||
prev_message = messages[-2]
|
|
||||||
if isinstance(prev_message, dict):
|
|
||||||
prev_role = prev_message.get("role", "")
|
|
||||||
else:
|
|
||||||
prev_role = getattr(prev_message, "type", "") or getattr(prev_message, "role", "")
|
|
||||||
|
|
||||||
if prev_role not in ("human", "user"):
|
|
||||||
return None
|
|
||||||
|
|
||||||
content_text = extract_text_content(content)
|
|
||||||
if not content_text:
|
|
||||||
return None
|
|
||||||
|
|
||||||
comment = f"""🤖 **Agent Response**
|
|
||||||
|
|
||||||
{content_text}"""
|
|
||||||
logger.info("Posting AI response to Linear issue %s", linear_issue_id)
|
|
||||||
success = await comment_on_linear_issue(linear_issue_id, comment)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
logger.info("Successfully posted to Linear")
|
|
||||||
return {"linear_messages_sent_count": ai_message_count}
|
|
||||||
logger.warning("Failed to post to Linear")
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Error in post_to_linear_after_model")
|
|
||||||
return None
|
|
||||||
|
|
@ -37,12 +37,20 @@ TASK_EXECUTION_SECTION = """---
|
||||||
|
|
||||||
### Task Execution
|
### Task Execution
|
||||||
|
|
||||||
When working on a task, follow this order:
|
If you make changes, call `linear_comment` to notify the user of the changes. For questions or status updates, call `linear_comment` with your answer.
|
||||||
|
|
||||||
|
For tasks that require code changes, follow this order:
|
||||||
|
|
||||||
1. **Understand** — Read the issue/task carefully. Explore relevant files before making any changes.
|
1. **Understand** — Read the issue/task carefully. Explore relevant files before making any changes.
|
||||||
2. **Implement** — Make focused, minimal changes. Do not modify code outside the scope of the task.
|
2. **Implement** — Make focused, minimal changes. Do not modify code outside the scope of the task.
|
||||||
3. **Verify** — Run tests and linters to confirm correctness before submitting.
|
3. **Verify** — Run tests and linters to confirm correctness before submitting.
|
||||||
4. **Submit** — Call `commit_and_open_pr` as the final step."""
|
4. **Submit** — Call `commit_and_open_pr`.
|
||||||
|
5. **Comment** — Call `linear_comment` with a summary and the PR link.
|
||||||
|
|
||||||
|
For questions or status checks (no code changes needed):
|
||||||
|
|
||||||
|
1. **Answer** — Gather the information needed to respond.
|
||||||
|
2. **Comment** — Call `linear_comment` with your answer. Never leave a question unanswered."""
|
||||||
|
|
||||||
|
|
||||||
TOOL_USAGE_SECTION = """---
|
TOOL_USAGE_SECTION = """---
|
||||||
|
|
@ -59,7 +67,10 @@ Fetches a URL and converts HTML to markdown. Use for web pages. Synthesize the c
|
||||||
Make HTTP requests (GET, POST, PUT, DELETE, etc.) to APIs. Use this for API calls with custom headers, methods, params, or request bodies — not for fetching web pages.
|
Make HTTP requests (GET, POST, PUT, DELETE, etc.) to APIs. Use this for API calls with custom headers, methods, params, or request bodies — not for fetching web pages.
|
||||||
|
|
||||||
#### `commit_and_open_pr`
|
#### `commit_and_open_pr`
|
||||||
Commits all changes, pushes to a branch, and opens a **draft** GitHub PR. If a PR already exists for the branch, it is updated instead of recreated. Always the final step."""
|
Commits all changes, pushes to a branch, and opens a **draft** GitHub PR. If a PR already exists for the branch, it is updated instead of recreated.
|
||||||
|
|
||||||
|
#### `linear_comment`
|
||||||
|
Posts a comment to a Linear ticket given a `ticket_id`. Call this **after** `commit_and_open_pr` to notify stakeholders that the work is done and include the PR link. You can tag Linear users with `@username` (their Linear display name). Example: "I've completed the implementation and opened a PR: <pr_url>. Hey @username, let me know if you have any feedback!"."""
|
||||||
|
|
||||||
|
|
||||||
TOOL_BEST_PRACTICES_SECTION = """---
|
TOOL_BEST_PRACTICES_SECTION = """---
|
||||||
|
|
@ -189,7 +200,21 @@ When you have completed your implementation, follow these steps in order:
|
||||||
|
|
||||||
**Commit message**: Concise, focusing on the "why" rather than the "what". If not provided, the PR title is used.
|
**Commit message**: Concise, focusing 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 implementation is complete and code quality checks pass."""
|
4. **Comment on the Linear ticket** via `linear_comment` immediately after `commit_and_open_pr` succeeds. Include:
|
||||||
|
- A brief summary of what was done
|
||||||
|
- The PR link returned by `commit_and_open_pr`
|
||||||
|
- An `@mention` of the user who triggered the task by their Linear display name
|
||||||
|
|
||||||
|
Example comment:
|
||||||
|
```
|
||||||
|
@username, I've completed the implementation and opened a PR: <pr_url>
|
||||||
|
|
||||||
|
Here's a summary of the changes:
|
||||||
|
- <change 1>
|
||||||
|
- <change 2>
|
||||||
|
```
|
||||||
|
|
||||||
|
Always call `commit_and_open_pr` followed by `linear_comment` once implementation is complete and code quality checks pass."""
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROMPT = (
|
SYSTEM_PROMPT = (
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,9 @@ from .middleware import (
|
||||||
ToolErrorMiddleware,
|
ToolErrorMiddleware,
|
||||||
check_message_queue_before_model,
|
check_message_queue_before_model,
|
||||||
open_pr_if_needed,
|
open_pr_if_needed,
|
||||||
post_to_linear_after_model,
|
|
||||||
)
|
)
|
||||||
from .prompt import construct_system_prompt
|
from .prompt import construct_system_prompt
|
||||||
from .tools import commit_and_open_pr, fetch_url, http_request
|
from .tools import commit_and_open_pr, fetch_url, http_request, linear_comment
|
||||||
from .utils.model import make_model
|
from .utils.model import make_model
|
||||||
|
|
||||||
client = get_client()
|
client = get_client()
|
||||||
|
|
@ -368,12 +367,11 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
linear_issue_number=linear_issue_number,
|
linear_issue_number=linear_issue_number,
|
||||||
agents_md=agents_md,
|
agents_md=agents_md,
|
||||||
),
|
),
|
||||||
tools=[http_request, fetch_url, commit_and_open_pr],
|
tools=[http_request, fetch_url, commit_and_open_pr, linear_comment],
|
||||||
backend=sandbox_backend,
|
backend=sandbox_backend,
|
||||||
middleware=[
|
middleware=[
|
||||||
ToolErrorMiddleware(),
|
ToolErrorMiddleware(),
|
||||||
check_message_queue_before_model,
|
check_message_queue_before_model,
|
||||||
post_to_linear_after_model,
|
|
||||||
open_pr_if_needed,
|
open_pr_if_needed,
|
||||||
],
|
],
|
||||||
).with_config(config)
|
).with_config(config)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from .commit_and_open_pr import commit_and_open_pr
|
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
|
||||||
|
from .linear_comment import linear_comment
|
||||||
|
|
||||||
__all__ = ["commit_and_open_pr", "fetch_url", "http_request"]
|
__all__ = ["commit_and_open_pr", "fetch_url", "http_request", "linear_comment"]
|
||||||
|
|
|
||||||
26
apps/agent/agent/tools/linear_comment.py
Normal file
26
apps/agent/agent/tools/linear_comment.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..utils.linear import comment_on_linear_issue
|
||||||
|
|
||||||
|
|
||||||
|
def linear_comment(comment_body: str, ticket_id: str) -> dict[str, Any]:
|
||||||
|
"""Post a comment to a Linear issue.
|
||||||
|
|
||||||
|
Use this tool to communicate progress and completion to stakeholders on Linear.
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- After calling `commit_and_open_pr`, post a comment on the Linear ticket to let
|
||||||
|
stakeholders know the task is complete and include the PR link. For example:
|
||||||
|
"I've completed the implementation and opened a PR: <pr_url>"
|
||||||
|
- When answering a question or sharing an update (no code changes needed).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
comment_body: Markdown-formatted comment text to post to the Linear issue.
|
||||||
|
ticket_id: The Linear issue UUID to post the comment to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with 'success' (bool) key.
|
||||||
|
"""
|
||||||
|
success = asyncio.run(comment_on_linear_issue(ticket_id, comment_body))
|
||||||
|
return {"success": success}
|
||||||
|
|
@ -663,13 +663,23 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
|
||||||
triggering_comment_id or "<missing-id>",
|
triggering_comment_id or "<missing-id>",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
identifier = full_issue.get("identifier", "") or issue_data.get("identifier", "")
|
||||||
|
|
||||||
|
triggered_by_line = f"## Triggered by: {user_name}\n\n" if user_name else ""
|
||||||
|
tag_instruction = (
|
||||||
|
f"When calling linear_comment, tag @{user_name} if you are asking them a question, need their input, or are notifying them of something important (e.g. a completed PR). For simple answers, tagging is not required."
|
||||||
|
if user_name
|
||||||
|
else ""
|
||||||
|
)
|
||||||
prompt = (
|
prompt = (
|
||||||
f"Please work on the following issue:\n\n"
|
f"Please work on the following issue:\n\n"
|
||||||
f"## Title: {title}\n\n"
|
f"## Title: {title}\n\n"
|
||||||
|
f"{triggered_by_line}"
|
||||||
|
f"## Linear Ticket: {identifier} - Ticket ID: {issue_id}\n\n"
|
||||||
f"## Description:\n{description}\n"
|
f"## Description:\n{description}\n"
|
||||||
f"{comments_text}\n\n"
|
f"{comments_text}\n\n"
|
||||||
"Please analyze this issue and implement the necessary changes. "
|
f"Please analyze this issue and implement the necessary changes. "
|
||||||
"When you're done, commit and push your changes."
|
f"When you're done, commit and push your changes. {tag_instruction}"
|
||||||
)
|
)
|
||||||
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]
|
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]
|
||||||
if image_urls:
|
if image_urls:
|
||||||
|
|
@ -684,7 +694,6 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
|
||||||
content_blocks.append(image_block)
|
content_blocks.append(image_block)
|
||||||
logger.info("Built %d content block(s) for prompt", len(content_blocks))
|
logger.info("Built %d content block(s) for prompt", len(content_blocks))
|
||||||
|
|
||||||
identifier = full_issue.get("identifier", "") or issue_data.get("identifier", "")
|
|
||||||
linear_project_id = ""
|
linear_project_id = ""
|
||||||
linear_issue_number = ""
|
linear_issue_number = ""
|
||||||
if identifier and "-" in identifier:
|
if identifier and "-" in identifier:
|
||||||
|
|
@ -701,6 +710,7 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
|
||||||
"identifier": identifier,
|
"identifier": identifier,
|
||||||
"linear_project_id": linear_project_id,
|
"linear_project_id": linear_project_id,
|
||||||
"linear_issue_number": linear_issue_number,
|
"linear_issue_number": linear_issue_number,
|
||||||
|
"triggering_user_name": user_name or "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if github_token:
|
if github_token:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue