mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
Fix typing: make Message a dict instead of dict | str (#13144)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -200,22 +200,26 @@ class GithubManager(Manager):
|
||||
self._add_reaction(github_view, 'eyes', installation_token)
|
||||
await self.start_job(github_view)
|
||||
|
||||
async def send_message(self, message: Message, github_view: ResolverViewInterface):
|
||||
installation_token = await self.token_manager.load_org_token(
|
||||
async def send_message(self, message: str, github_view: ResolverViewInterface):
|
||||
"""Send a message to GitHub.
|
||||
|
||||
Args:
|
||||
message: The message content to send (plain text string)
|
||||
github_view: The GitHub view object containing issue/PR/comment info
|
||||
"""
|
||||
installation_token = self.token_manager.load_org_token(
|
||||
github_view.installation_id
|
||||
)
|
||||
if not installation_token:
|
||||
logger.warning('Missing installation token')
|
||||
return
|
||||
|
||||
outgoing_message = message.message
|
||||
|
||||
if isinstance(github_view, GithubInlinePRComment):
|
||||
with Github(auth=Auth.Token(installation_token)) as github_client:
|
||||
repo = github_client.get_repo(github_view.full_repo_name)
|
||||
pr = repo.get_pull(github_view.issue_number)
|
||||
pr.create_review_comment_reply(
|
||||
comment_id=github_view.comment_id, body=outgoing_message
|
||||
comment_id=github_view.comment_id, body=message
|
||||
)
|
||||
|
||||
elif (
|
||||
@@ -226,7 +230,7 @@ class GithubManager(Manager):
|
||||
with Github(auth=Auth.Token(installation_token)) as github_client:
|
||||
repo = github_client.get_repo(github_view.full_repo_name)
|
||||
issue = repo.get_issue(number=github_view.issue_number)
|
||||
issue.create_comment(outgoing_message)
|
||||
issue.create_comment(message)
|
||||
|
||||
else:
|
||||
logger.warning('Unsupported location')
|
||||
@@ -245,7 +249,7 @@ class GithubManager(Manager):
|
||||
)
|
||||
|
||||
try:
|
||||
msg_info = None
|
||||
msg_info: str = ''
|
||||
|
||||
try:
|
||||
user_info = github_view.user_info
|
||||
@@ -361,15 +365,13 @@ class GithubManager(Manager):
|
||||
|
||||
msg_info = get_session_expired_message(user_info.username)
|
||||
|
||||
msg = self.create_outgoing_message(msg_info)
|
||||
await self.send_message(msg, github_view)
|
||||
await self.send_message(msg_info, github_view)
|
||||
|
||||
except Exception:
|
||||
logger.exception('[Github]: Error starting job')
|
||||
msg = self.create_outgoing_message(
|
||||
msg='Uh oh! There was an unexpected error starting the job :('
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(', github_view
|
||||
)
|
||||
await self.send_message(msg, github_view)
|
||||
|
||||
try:
|
||||
await self.data_collector.save_data(github_view)
|
||||
|
||||
@@ -121,12 +121,11 @@ class GitlabManager(Manager):
|
||||
# Check if the user has write access to the repository
|
||||
return has_write_access
|
||||
|
||||
async def send_message(self, message: Message, gitlab_view: ResolverViewInterface):
|
||||
"""
|
||||
Send a message to GitLab based on the view type.
|
||||
async def send_message(self, message: str, gitlab_view: ResolverViewInterface):
|
||||
"""Send a message to GitLab based on the view type.
|
||||
|
||||
Args:
|
||||
message: The message to send
|
||||
message: The message content to send (plain text string)
|
||||
gitlab_view: The GitLab view object containing issue/PR/comment info
|
||||
"""
|
||||
keycloak_user_id = gitlab_view.user_info.keycloak_user_id
|
||||
@@ -138,8 +137,6 @@ class GitlabManager(Manager):
|
||||
external_auth_id=keycloak_user_id
|
||||
)
|
||||
|
||||
outgoing_message = message.message
|
||||
|
||||
if isinstance(gitlab_view, GitlabInlineMRComment) or isinstance(
|
||||
gitlab_view, GitlabMRComment
|
||||
):
|
||||
@@ -147,7 +144,7 @@ class GitlabManager(Manager):
|
||||
gitlab_view.project_id,
|
||||
gitlab_view.issue_number,
|
||||
gitlab_view.discussion_id,
|
||||
message.message,
|
||||
message,
|
||||
)
|
||||
|
||||
elif isinstance(gitlab_view, GitlabIssueComment):
|
||||
@@ -155,14 +152,14 @@ class GitlabManager(Manager):
|
||||
gitlab_view.project_id,
|
||||
gitlab_view.issue_number,
|
||||
gitlab_view.discussion_id,
|
||||
outgoing_message,
|
||||
message,
|
||||
)
|
||||
elif isinstance(gitlab_view, GitlabIssue):
|
||||
await gitlab_service.reply_to_issue(
|
||||
gitlab_view.project_id,
|
||||
gitlab_view.issue_number,
|
||||
None, # no discussion id, issue is tagged
|
||||
outgoing_message,
|
||||
message,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -262,12 +259,10 @@ class GitlabManager(Manager):
|
||||
msg_info = get_session_expired_message(user_info.username)
|
||||
|
||||
# Send the acknowledgment message
|
||||
msg = self.create_outgoing_message(msg_info)
|
||||
await self.send_message(msg, gitlab_view)
|
||||
await self.send_message(msg_info, gitlab_view)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'[GitLab] Error starting job: {str(e)}')
|
||||
msg = self.create_outgoing_message(
|
||||
msg='Uh oh! There was an unexpected error starting the job :('
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(', gitlab_view
|
||||
)
|
||||
await self.send_message(msg, gitlab_view)
|
||||
|
||||
@@ -341,17 +341,25 @@ class JiraManager(Manager):
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
message: Message,
|
||||
message: str,
|
||||
issue_key: str,
|
||||
jira_cloud_id: str,
|
||||
svc_acc_email: str,
|
||||
svc_acc_api_key: str,
|
||||
):
|
||||
"""Send a comment to a Jira issue."""
|
||||
"""Send a comment to a Jira issue.
|
||||
|
||||
Args:
|
||||
message: The message content to send (plain text string)
|
||||
issue_key: The Jira issue key (e.g., 'PROJ-123')
|
||||
jira_cloud_id: The Jira Cloud ID
|
||||
svc_acc_email: Service account email for authentication
|
||||
svc_acc_api_key: Service account API key for authentication
|
||||
"""
|
||||
url = (
|
||||
f'{JIRA_CLOUD_API_URL}/{jira_cloud_id}/rest/api/2/issue/{issue_key}/comment'
|
||||
)
|
||||
data = {'body': message.message}
|
||||
data = {'body': message}
|
||||
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
|
||||
response = await client.post(
|
||||
url, auth=(svc_acc_email, svc_acc_api_key), json=data
|
||||
@@ -366,7 +374,7 @@ class JiraManager(Manager):
|
||||
view.jira_workspace.svc_acc_api_key
|
||||
)
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=msg),
|
||||
msg,
|
||||
issue_key=view.payload.issue_key,
|
||||
jira_cloud_id=view.jira_workspace.jira_cloud_id,
|
||||
svc_acc_email=view.jira_workspace.svc_acc_email,
|
||||
@@ -388,7 +396,7 @@ class JiraManager(Manager):
|
||||
try:
|
||||
api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=error_msg),
|
||||
error_msg,
|
||||
issue_key=payload.issue_key,
|
||||
jira_cloud_id=workspace.jira_cloud_id,
|
||||
svc_acc_email=workspace.svc_acc_email,
|
||||
|
||||
@@ -418,7 +418,7 @@ class JiraDcManager(Manager):
|
||||
jira_dc_view.jira_dc_workspace.svc_acc_api_key
|
||||
)
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=msg_info),
|
||||
msg_info,
|
||||
issue_key=jira_dc_view.job_context.issue_key,
|
||||
base_api_url=jira_dc_view.job_context.base_api_url,
|
||||
svc_acc_api_key=api_key,
|
||||
@@ -456,12 +456,19 @@ class JiraDcManager(Manager):
|
||||
return title, description
|
||||
|
||||
async def send_message(
|
||||
self, message: Message, issue_key: str, base_api_url: str, svc_acc_api_key: str
|
||||
self, message: str, issue_key: str, base_api_url: str, svc_acc_api_key: str
|
||||
):
|
||||
"""Send message/comment to Jira DC issue."""
|
||||
"""Send message/comment to Jira DC issue.
|
||||
|
||||
Args:
|
||||
message: The message content to send (plain text string)
|
||||
issue_key: The Jira issue key (e.g., 'PROJ-123')
|
||||
base_api_url: The base API URL for the Jira DC instance
|
||||
svc_acc_api_key: Service account API key for authentication
|
||||
"""
|
||||
url = f'{base_api_url}/rest/api/2/issue/{issue_key}/comment'
|
||||
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
|
||||
data = {'body': message.message}
|
||||
data = {'body': message}
|
||||
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
|
||||
response = await client.post(url, headers=headers, json=data)
|
||||
response.raise_for_status()
|
||||
@@ -481,7 +488,7 @@ class JiraDcManager(Manager):
|
||||
try:
|
||||
api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=error_msg),
|
||||
error_msg,
|
||||
issue_key=job_context.issue_key,
|
||||
base_api_url=job_context.base_api_url,
|
||||
svc_acc_api_key=api_key,
|
||||
@@ -502,7 +509,7 @@ class JiraDcManager(Manager):
|
||||
)
|
||||
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=comment_msg),
|
||||
comment_msg,
|
||||
issue_key=jira_dc_view.job_context.issue_key,
|
||||
base_api_url=jira_dc_view.job_context.base_api_url,
|
||||
svc_acc_api_key=api_key,
|
||||
|
||||
@@ -408,7 +408,7 @@ class LinearManager(Manager):
|
||||
linear_view.linear_workspace.svc_acc_api_key
|
||||
)
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=msg_info),
|
||||
msg_info,
|
||||
linear_view.job_context.issue_id,
|
||||
api_key,
|
||||
)
|
||||
@@ -473,8 +473,14 @@ class LinearManager(Manager):
|
||||
|
||||
return title, description
|
||||
|
||||
async def send_message(self, message: Message, issue_id: str, api_key: str):
|
||||
"""Send message/comment to Linear issue."""
|
||||
async def send_message(self, message: str, issue_id: str, api_key: str):
|
||||
"""Send message/comment to Linear issue.
|
||||
|
||||
Args:
|
||||
message: The message content to send (plain text string)
|
||||
issue_id: The Linear issue ID to comment on
|
||||
api_key: The Linear API key for authentication
|
||||
"""
|
||||
query = """
|
||||
mutation CommentCreate($input: CommentCreateInput!) {
|
||||
commentCreate(input: $input) {
|
||||
@@ -485,7 +491,7 @@ class LinearManager(Manager):
|
||||
}
|
||||
}
|
||||
"""
|
||||
variables = {'input': {'issueId': issue_id, 'body': message.message}}
|
||||
variables = {'input': {'issueId': issue_id, 'body': message}}
|
||||
return await self._query_api(query, variables, api_key)
|
||||
|
||||
async def _send_error_comment(
|
||||
@@ -498,9 +504,7 @@ class LinearManager(Manager):
|
||||
|
||||
try:
|
||||
api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=error_msg), issue_id, api_key
|
||||
)
|
||||
await self.send_message(error_msg, issue_id, api_key)
|
||||
except Exception as e:
|
||||
logger.error(f'[Linear] Failed to send error comment: {str(e)}')
|
||||
|
||||
@@ -517,7 +521,7 @@ class LinearManager(Manager):
|
||||
)
|
||||
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg=comment_msg),
|
||||
comment_msg,
|
||||
linear_view.job_context.issue_id,
|
||||
api_key,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from integrations.models import Message, SourceType
|
||||
|
||||
@@ -12,14 +13,15 @@ class Manager(ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def send_message(self, message: Message):
|
||||
"Send message to integration from Openhands server"
|
||||
def send_message(self, message: str, *args: Any, **kwargs: Any):
|
||||
"""Send message to integration from OpenHands server.
|
||||
|
||||
Args:
|
||||
message: The message content to send (plain text string).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def start_job(self):
|
||||
"Kick off a job with openhands agent"
|
||||
raise NotImplementedError
|
||||
|
||||
def create_outgoing_message(self, msg: str | dict, ephemeral: bool = False):
|
||||
return Message(source=SourceType.OPENHANDS, message=msg, ephemeral=ephemeral)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -16,8 +17,16 @@ class SourceType(str, Enum):
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""Message model for incoming webhook payloads from integrations.
|
||||
|
||||
Note: This model is intended for INCOMING messages only.
|
||||
For outgoing messages (e.g., sending comments to GitHub/GitLab),
|
||||
pass strings directly to the send_message methods instead of
|
||||
wrapping them in a Message object.
|
||||
"""
|
||||
|
||||
source: SourceType
|
||||
message: str | dict
|
||||
message: dict[str, Any]
|
||||
ephemeral: bool = False
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
from integrations.manager import Manager
|
||||
@@ -202,9 +203,7 @@ class SlackManager(Manager):
|
||||
msg = self.login_link.format(link)
|
||||
|
||||
logger.info('slack_not_yet_authenticated')
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(msg, ephemeral=True), slack_view
|
||||
)
|
||||
await self.send_message(msg, slack_view, ephemeral=True)
|
||||
return
|
||||
|
||||
if not await self.is_job_requested(message, slack_view):
|
||||
@@ -212,27 +211,40 @@ class SlackManager(Manager):
|
||||
|
||||
await self.start_job(slack_view)
|
||||
|
||||
async def send_message(self, message: Message, slack_view: SlackViewInterface):
|
||||
async def send_message(
|
||||
self,
|
||||
message: str | dict[str, Any],
|
||||
slack_view: SlackViewInterface,
|
||||
ephemeral: bool = False,
|
||||
):
|
||||
"""Send a message to Slack.
|
||||
|
||||
Args:
|
||||
message: The message content. Can be a string (for simple text) or
|
||||
a dict with 'text' and 'blocks' keys (for structured messages).
|
||||
slack_view: The Slack view object containing channel/thread info.
|
||||
ephemeral: If True, send as an ephemeral message visible only to the user.
|
||||
"""
|
||||
client = AsyncWebClient(token=slack_view.bot_access_token)
|
||||
if message.ephemeral and isinstance(message.message, str):
|
||||
if ephemeral and isinstance(message, str):
|
||||
await client.chat_postEphemeral(
|
||||
channel=slack_view.channel_id,
|
||||
markdown_text=message.message,
|
||||
markdown_text=message,
|
||||
user=slack_view.slack_user_id,
|
||||
thread_ts=slack_view.thread_ts,
|
||||
)
|
||||
elif message.ephemeral and isinstance(message.message, dict):
|
||||
elif ephemeral and isinstance(message, dict):
|
||||
await client.chat_postEphemeral(
|
||||
channel=slack_view.channel_id,
|
||||
user=slack_view.slack_user_id,
|
||||
thread_ts=slack_view.thread_ts,
|
||||
text=message.message['text'],
|
||||
blocks=message.message['blocks'],
|
||||
text=message['text'],
|
||||
blocks=message['blocks'],
|
||||
)
|
||||
else:
|
||||
await client.chat_postMessage(
|
||||
channel=slack_view.channel_id,
|
||||
markdown_text=message.message,
|
||||
markdown_text=message,
|
||||
thread_ts=slack_view.message_ts,
|
||||
)
|
||||
|
||||
@@ -279,10 +291,7 @@ class SlackManager(Manager):
|
||||
repos, slack_view.message_ts, slack_view.thread_ts
|
||||
),
|
||||
}
|
||||
await self.send_message(
|
||||
self.create_outgoing_message(repo_selection_msg, ephemeral=True),
|
||||
slack_view,
|
||||
)
|
||||
await self.send_message(repo_selection_msg, slack_view, ephemeral=True)
|
||||
|
||||
return False
|
||||
|
||||
@@ -368,9 +377,10 @@ class SlackManager(Manager):
|
||||
except StartingConvoException as e:
|
||||
msg_info = str(e)
|
||||
|
||||
await self.send_message(self.create_outgoing_message(msg_info), slack_view)
|
||||
await self.send_message(msg_info, slack_view)
|
||||
|
||||
except Exception:
|
||||
logger.exception('[Slack]: Error starting job')
|
||||
msg = 'Uh oh! There was an unexpected error starting the job :('
|
||||
await self.send_message(self.create_outgoing_message(msg), slack_view)
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(', slack_view
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
|
||||
from integrations.github.github_manager import GithubManager
|
||||
from integrations.github.github_view import GithubViewType
|
||||
from integrations.models import Message, SourceType
|
||||
from integrations.utils import (
|
||||
extract_summary_from_conversation_manager,
|
||||
get_summary_instruction,
|
||||
@@ -35,16 +34,12 @@ class GithubCallbackProcessor(ConversationCallbackProcessor):
|
||||
send_summary_instruction: bool = True
|
||||
|
||||
async def _send_message_to_github(self, message: str) -> None:
|
||||
"""
|
||||
Send a message to GitHub.
|
||||
"""Send a message to GitHub.
|
||||
|
||||
Args:
|
||||
message: The message content to send to GitHub
|
||||
"""
|
||||
try:
|
||||
# Create a message object for GitHub
|
||||
message_obj = Message(source=SourceType.OPENHANDS, message=message)
|
||||
|
||||
# Get the token manager
|
||||
token_manager = TokenManager()
|
||||
|
||||
@@ -53,8 +48,8 @@ class GithubCallbackProcessor(ConversationCallbackProcessor):
|
||||
|
||||
github_manager = GithubManager(token_manager, GitHubDataCollector())
|
||||
|
||||
# Send the message
|
||||
await github_manager.send_message(message_obj, self.github_view)
|
||||
# Send the message directly as a string
|
||||
await github_manager.send_message(message, self.github_view)
|
||||
|
||||
logger.info(
|
||||
f'[GitHub] Sent summary message to {self.github_view.full_repo_name}#{self.github_view.issue_number}'
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
|
||||
from integrations.gitlab.gitlab_manager import GitlabManager
|
||||
from integrations.gitlab.gitlab_view import GitlabViewType
|
||||
from integrations.models import Message, SourceType
|
||||
from integrations.utils import (
|
||||
extract_summary_from_conversation_manager,
|
||||
get_summary_instruction,
|
||||
@@ -28,8 +27,7 @@ gitlab_manager = GitlabManager(token_manager)
|
||||
|
||||
|
||||
class GitlabCallbackProcessor(ConversationCallbackProcessor):
|
||||
"""
|
||||
Processor for sending conversation summaries to GitLab.
|
||||
"""Processor for sending conversation summaries to GitLab.
|
||||
|
||||
This processor is used to send summaries of conversations to GitLab
|
||||
when agent state changes occur.
|
||||
@@ -39,22 +37,18 @@ class GitlabCallbackProcessor(ConversationCallbackProcessor):
|
||||
send_summary_instruction: bool = True
|
||||
|
||||
async def _send_message_to_gitlab(self, message: str) -> None:
|
||||
"""
|
||||
Send a message to GitLab.
|
||||
"""Send a message to GitLab.
|
||||
|
||||
Args:
|
||||
message: The message content to send to GitLab
|
||||
"""
|
||||
try:
|
||||
# Create a message object for GitHub
|
||||
message_obj = Message(source=SourceType.OPENHANDS, message=message)
|
||||
|
||||
# Get the token manager
|
||||
token_manager = TokenManager()
|
||||
gitlab_manager = GitlabManager(token_manager)
|
||||
|
||||
# Send the message
|
||||
await gitlab_manager.send_message(message_obj, self.gitlab_view)
|
||||
# Send the message directly as a string
|
||||
await gitlab_manager.send_message(message, self.gitlab_view)
|
||||
|
||||
logger.info(
|
||||
f'[GitLab] Sent summary message to {self.gitlab_view.full_repo_name}#{self.gitlab_view.issue_number}'
|
||||
|
||||
@@ -37,8 +37,7 @@ class JiraCallbackProcessor(ConversationCallbackProcessor):
|
||||
workspace_name: str
|
||||
|
||||
async def _send_comment_to_jira(self, message: str) -> None:
|
||||
"""
|
||||
Send a comment to Jira issue.
|
||||
"""Send a comment to Jira issue.
|
||||
|
||||
Args:
|
||||
message: The message content to send to Jira
|
||||
@@ -59,8 +58,9 @@ class JiraCallbackProcessor(ConversationCallbackProcessor):
|
||||
# Decrypt API key
|
||||
api_key = jira_manager.token_manager.decrypt_text(workspace.svc_acc_api_key)
|
||||
|
||||
# Send comment directly as a string
|
||||
await jira_manager.send_message(
|
||||
jira_manager.create_outgoing_message(msg=message),
|
||||
message,
|
||||
issue_key=self.issue_key,
|
||||
jira_cloud_id=workspace.jira_cloud_id,
|
||||
svc_acc_email=workspace.svc_acc_email,
|
||||
|
||||
@@ -37,8 +37,7 @@ class JiraDcCallbackProcessor(ConversationCallbackProcessor):
|
||||
base_api_url: str
|
||||
|
||||
async def _send_comment_to_jira_dc(self, message: str) -> None:
|
||||
"""
|
||||
Send a comment to Jira DC issue.
|
||||
"""Send a comment to Jira DC issue.
|
||||
|
||||
Args:
|
||||
message: The message content to send to Jira DC
|
||||
@@ -61,8 +60,9 @@ class JiraDcCallbackProcessor(ConversationCallbackProcessor):
|
||||
workspace.svc_acc_api_key
|
||||
)
|
||||
|
||||
# Send comment directly as a string
|
||||
await jira_dc_manager.send_message(
|
||||
jira_dc_manager.create_outgoing_message(msg=message),
|
||||
message,
|
||||
issue_key=self.issue_key,
|
||||
base_api_url=self.base_api_url,
|
||||
svc_acc_api_key=api_key,
|
||||
|
||||
@@ -36,8 +36,7 @@ class LinearCallbackProcessor(ConversationCallbackProcessor):
|
||||
workspace_name: str
|
||||
|
||||
async def _send_comment_to_linear(self, message: str) -> None:
|
||||
"""
|
||||
Send a comment to Linear issue.
|
||||
"""Send a comment to Linear issue.
|
||||
|
||||
Args:
|
||||
message: The message content to send to Linear
|
||||
@@ -60,9 +59,9 @@ class LinearCallbackProcessor(ConversationCallbackProcessor):
|
||||
workspace.svc_acc_api_key
|
||||
)
|
||||
|
||||
# Send comment
|
||||
# Send comment directly as a string
|
||||
await linear_manager.send_message(
|
||||
linear_manager.create_outgoing_message(msg=message),
|
||||
message,
|
||||
self.issue_id,
|
||||
api_key,
|
||||
)
|
||||
|
||||
@@ -26,8 +26,7 @@ slack_manager = SlackManager(token_manager)
|
||||
|
||||
|
||||
class SlackCallbackProcessor(ConversationCallbackProcessor):
|
||||
"""
|
||||
Processor for sending conversation summaries to Slack.
|
||||
"""Processor for sending conversation summaries to Slack.
|
||||
|
||||
This processor is used to send summaries of conversations to Slack channels
|
||||
when agent state changes occur.
|
||||
@@ -41,14 +40,13 @@ class SlackCallbackProcessor(ConversationCallbackProcessor):
|
||||
last_user_msg_id: int | None = None
|
||||
|
||||
async def _send_message_to_slack(self, message: str) -> None:
|
||||
"""
|
||||
Send a message to Slack using the conversation_manager's send_to_event_stream method.
|
||||
"""Send a message to Slack.
|
||||
|
||||
Args:
|
||||
message: The message content to send to Slack
|
||||
"""
|
||||
try:
|
||||
# Create a message object for Slack
|
||||
# Create a message object for Slack view creation (incoming message format)
|
||||
message_obj = Message(
|
||||
source=SourceType.SLACK,
|
||||
message={
|
||||
@@ -67,9 +65,8 @@ class SlackCallbackProcessor(ConversationCallbackProcessor):
|
||||
slack_view = SlackFactory.create_slack_view_from_payload(
|
||||
message_obj, slack_user, saas_user_auth
|
||||
)
|
||||
await slack_manager.send_message(
|
||||
slack_manager.create_outgoing_message(message), slack_view
|
||||
)
|
||||
# Send the message directly as a string
|
||||
await slack_manager.send_message(message, slack_view)
|
||||
|
||||
logger.info(
|
||||
f'[Slack] Sent summary message to channel {self.channel_id} '
|
||||
|
||||
@@ -7,7 +7,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from integrations.jira.jira_manager import JiraManager
|
||||
from integrations.jira.jira_payload import JiraEventType, JiraWebhookPayload
|
||||
from integrations.models import Message, SourceType
|
||||
|
||||
from openhands.server.types import (
|
||||
LLMAuthenticationError,
|
||||
@@ -274,9 +273,8 @@ class TestSendMessage:
|
||||
return_value=mock_response
|
||||
)
|
||||
|
||||
message = Message(source=SourceType.JIRA, message='Test message')
|
||||
result = await jira_manager.send_message(
|
||||
message,
|
||||
'Test message',
|
||||
'PROJ-123',
|
||||
'cloud-123',
|
||||
'service@test.com',
|
||||
|
||||
@@ -738,7 +738,7 @@ class TestStartJob:
|
||||
# Should send error message about re-login
|
||||
jira_dc_manager.send_message.assert_called_once()
|
||||
call_args = jira_dc_manager.send_message.call_args[0]
|
||||
assert 'Please re-login' in call_args[0].message
|
||||
assert 'Please re-login' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_llm_authentication_error(
|
||||
@@ -763,7 +763,7 @@ class TestStartJob:
|
||||
# Should send error message about LLM API key
|
||||
jira_dc_manager.send_message.assert_called_once()
|
||||
call_args = jira_dc_manager.send_message.call_args[0]
|
||||
assert 'valid LLM API key' in call_args[0].message
|
||||
assert 'valid LLM API key' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_session_expired_error(
|
||||
@@ -788,8 +788,8 @@ class TestStartJob:
|
||||
# Should send error message about session expired
|
||||
jira_dc_manager.send_message.assert_called_once()
|
||||
call_args = jira_dc_manager.send_message.call_args[0]
|
||||
assert 'session has expired' in call_args[0].message
|
||||
assert 'login again' in call_args[0].message
|
||||
assert 'session has expired' in call_args[0]
|
||||
assert 'login again' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_unexpected_error(
|
||||
@@ -814,7 +814,7 @@ class TestStartJob:
|
||||
# Should send generic error message
|
||||
jira_dc_manager.send_message.assert_called_once()
|
||||
call_args = jira_dc_manager.send_message.call_args[0]
|
||||
assert 'unexpected error' in call_args[0].message
|
||||
assert 'unexpected error' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_send_message_fails(
|
||||
@@ -943,9 +943,8 @@ class TestSendMessage:
|
||||
return_value=mock_response
|
||||
)
|
||||
|
||||
message = Message(source=SourceType.JIRA_DC, message='Test message')
|
||||
result = await jira_dc_manager.send_message(
|
||||
message, 'PROJ-123', 'https://jira.company.com', 'bearer_token'
|
||||
'Test message', 'PROJ-123', 'https://jira.company.com', 'bearer_token'
|
||||
)
|
||||
|
||||
assert result == {'id': 'comment_id'}
|
||||
@@ -1014,7 +1013,7 @@ class TestSendRepoSelectionComment:
|
||||
|
||||
jira_dc_manager.send_message.assert_called_once()
|
||||
call_args = jira_dc_manager.send_message.call_args[0]
|
||||
assert 'which repository to work with' in call_args[0].message
|
||||
assert 'which repository to work with' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_repo_selection_comment_send_fails(
|
||||
|
||||
@@ -802,7 +802,7 @@ class TestStartJob:
|
||||
# Should send error message about re-login
|
||||
linear_manager.send_message.assert_called_once()
|
||||
call_args = linear_manager.send_message.call_args[0]
|
||||
assert 'Please re-login' in call_args[0].message
|
||||
assert 'Please re-login' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_llm_authentication_error(
|
||||
@@ -828,7 +828,7 @@ class TestStartJob:
|
||||
# Should send error message about LLM API key
|
||||
linear_manager.send_message.assert_called_once()
|
||||
call_args = linear_manager.send_message.call_args[0]
|
||||
assert 'valid LLM API key' in call_args[0].message
|
||||
assert 'valid LLM API key' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_session_expired_error(
|
||||
@@ -854,8 +854,8 @@ class TestStartJob:
|
||||
# Should send error message about session expired
|
||||
linear_manager.send_message.assert_called_once()
|
||||
call_args = linear_manager.send_message.call_args[0]
|
||||
assert 'session has expired' in call_args[0].message
|
||||
assert 'login again' in call_args[0].message
|
||||
assert 'session has expired' in call_args[0]
|
||||
assert 'login again' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_unexpected_error(
|
||||
@@ -881,7 +881,7 @@ class TestStartJob:
|
||||
# Should send generic error message
|
||||
linear_manager.send_message.assert_called_once()
|
||||
call_args = linear_manager.send_message.call_args[0]
|
||||
assert 'unexpected error' in call_args[0].message
|
||||
assert 'unexpected error' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_job_send_message_fails(
|
||||
@@ -1049,8 +1049,9 @@ class TestSendMessage:
|
||||
|
||||
linear_manager._query_api = AsyncMock(return_value=mock_response)
|
||||
|
||||
message = Message(source=SourceType.LINEAR, message='Test message')
|
||||
result = await linear_manager.send_message(message, 'issue_id', 'api_key')
|
||||
result = await linear_manager.send_message(
|
||||
'Test message', 'issue_id', 'api_key'
|
||||
)
|
||||
|
||||
assert result == mock_response
|
||||
linear_manager._query_api.assert_called_once()
|
||||
@@ -1114,7 +1115,7 @@ class TestSendRepoSelectionComment:
|
||||
|
||||
linear_manager.send_message.assert_called_once()
|
||||
call_args = linear_manager.send_message.call_args[0]
|
||||
assert 'which repository to work with' in call_args[0].message
|
||||
assert 'which repository to work with' in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_repo_selection_comment_send_fails(
|
||||
|
||||
@@ -34,7 +34,6 @@ async def test_send_comment_to_jira_success(mock_jira_manager, processor):
|
||||
)
|
||||
mock_jira_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_jira_manager.send_message = AsyncMock()
|
||||
mock_jira_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
# Action
|
||||
await processor._send_comment_to_jira('This is a summary.')
|
||||
@@ -130,7 +129,6 @@ async def test_call_sends_summary_to_jira(
|
||||
return_value=mock_workspace
|
||||
)
|
||||
mock_jira_manager.send_message = AsyncMock()
|
||||
mock_jira_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
with patch(
|
||||
'server.conversation_callback_processor.jira_callback_processor.asyncio.create_task'
|
||||
@@ -200,7 +198,6 @@ async def test_send_comment_to_jira_api_error(mock_jira_manager, processor):
|
||||
)
|
||||
mock_jira_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_jira_manager.send_message = AsyncMock(side_effect=Exception('API Error'))
|
||||
mock_jira_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
# Action - should not raise exception, but handle it gracefully
|
||||
await processor._send_comment_to_jira('This is a summary.')
|
||||
@@ -328,18 +325,15 @@ async def test_send_comment_to_jira_message_construction(mock_jira_manager, proc
|
||||
)
|
||||
mock_jira_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_jira_manager.send_message = AsyncMock()
|
||||
mock_outgoing_message = MagicMock()
|
||||
mock_jira_manager.create_outgoing_message.return_value = mock_outgoing_message
|
||||
|
||||
test_message = 'This is a test summary message.'
|
||||
|
||||
# Action
|
||||
await processor._send_comment_to_jira(test_message)
|
||||
|
||||
# Assert
|
||||
mock_jira_manager.create_outgoing_message.assert_called_once_with(msg=test_message)
|
||||
# Assert - send_message now receives the string directly
|
||||
mock_jira_manager.send_message.assert_called_once_with(
|
||||
mock_outgoing_message,
|
||||
test_message,
|
||||
issue_key='TEST-123',
|
||||
jira_cloud_id='cloud123',
|
||||
svc_acc_email='service@test.com',
|
||||
@@ -386,7 +380,6 @@ async def test_call_creates_background_task_for_sending(
|
||||
return_value=mock_workspace
|
||||
)
|
||||
mock_jira_manager.send_message = AsyncMock()
|
||||
mock_jira_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
with patch(
|
||||
'server.conversation_callback_processor.jira_callback_processor.asyncio.create_task'
|
||||
|
||||
@@ -32,7 +32,6 @@ async def test_send_comment_to_jira_dc_success(mock_jira_dc_manager, processor):
|
||||
)
|
||||
mock_jira_dc_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_jira_dc_manager.send_message = AsyncMock()
|
||||
mock_jira_dc_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
# Action
|
||||
await processor._send_comment_to_jira_dc('This is a summary.')
|
||||
@@ -125,7 +124,6 @@ async def test_call_sends_summary_to_jira_dc(
|
||||
return_value=mock_workspace
|
||||
)
|
||||
mock_jira_dc_manager.send_message = AsyncMock()
|
||||
mock_jira_dc_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
with patch(
|
||||
'server.conversation_callback_processor.jira_dc_callback_processor.asyncio.create_task'
|
||||
@@ -200,7 +198,6 @@ async def test_send_comment_to_jira_dc_api_error(mock_jira_dc_manager, processor
|
||||
)
|
||||
mock_jira_dc_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_jira_dc_manager.send_message = AsyncMock(side_effect=Exception('API Error'))
|
||||
mock_jira_dc_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
# Action - should not raise exception, but handle it gracefully
|
||||
await processor._send_comment_to_jira_dc('This is a summary.')
|
||||
@@ -328,20 +325,15 @@ async def test_send_comment_to_jira_dc_message_construction(
|
||||
)
|
||||
mock_jira_dc_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_jira_dc_manager.send_message = AsyncMock()
|
||||
mock_outgoing_message = MagicMock()
|
||||
mock_jira_dc_manager.create_outgoing_message.return_value = mock_outgoing_message
|
||||
|
||||
test_message = 'This is a test summary message.'
|
||||
|
||||
# Action
|
||||
await processor._send_comment_to_jira_dc(test_message)
|
||||
|
||||
# Assert
|
||||
mock_jira_dc_manager.create_outgoing_message.assert_called_once_with(
|
||||
msg=test_message
|
||||
)
|
||||
# Assert - send_message now receives the string directly
|
||||
mock_jira_dc_manager.send_message.assert_called_once_with(
|
||||
mock_outgoing_message,
|
||||
test_message,
|
||||
issue_key='TEST-123',
|
||||
base_api_url='https://test-jira-dc.company.com',
|
||||
svc_acc_api_key='decrypted_key',
|
||||
@@ -384,7 +376,6 @@ async def test_call_creates_background_task_for_sending(
|
||||
return_value=mock_workspace
|
||||
)
|
||||
mock_jira_dc_manager.send_message = AsyncMock()
|
||||
mock_jira_dc_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
with patch(
|
||||
'server.conversation_callback_processor.jira_dc_callback_processor.asyncio.create_task'
|
||||
|
||||
@@ -32,7 +32,6 @@ async def test_send_comment_to_linear_success(mock_linear_manager, processor):
|
||||
)
|
||||
mock_linear_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_linear_manager.send_message = AsyncMock()
|
||||
mock_linear_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
# Action
|
||||
await processor._send_comment_to_linear('This is a summary.')
|
||||
@@ -125,7 +124,6 @@ async def test_call_sends_summary_to_linear(
|
||||
return_value=mock_workspace
|
||||
)
|
||||
mock_linear_manager.send_message = AsyncMock()
|
||||
mock_linear_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
with patch(
|
||||
'server.conversation_callback_processor.linear_callback_processor.asyncio.create_task'
|
||||
@@ -200,7 +198,6 @@ async def test_send_comment_to_linear_api_error(mock_linear_manager, processor):
|
||||
)
|
||||
mock_linear_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_linear_manager.send_message = AsyncMock(side_effect=Exception('API Error'))
|
||||
mock_linear_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
# Action - should not raise exception, but handle it gracefully
|
||||
await processor._send_comment_to_linear('This is a summary.')
|
||||
@@ -328,20 +325,15 @@ async def test_send_comment_to_linear_message_construction(
|
||||
)
|
||||
mock_linear_manager.token_manager.decrypt_text.return_value = 'decrypted_key'
|
||||
mock_linear_manager.send_message = AsyncMock()
|
||||
mock_outgoing_message = MagicMock()
|
||||
mock_linear_manager.create_outgoing_message.return_value = mock_outgoing_message
|
||||
|
||||
test_message = 'This is a test summary message.'
|
||||
|
||||
# Action
|
||||
await processor._send_comment_to_linear(test_message)
|
||||
|
||||
# Assert
|
||||
mock_linear_manager.create_outgoing_message.assert_called_once_with(
|
||||
msg=test_message
|
||||
)
|
||||
# Assert - send_message now receives the string directly
|
||||
mock_linear_manager.send_message.assert_called_once_with(
|
||||
mock_outgoing_message,
|
||||
test_message,
|
||||
'TEST-123', # issue_id
|
||||
'decrypted_key', # api_key
|
||||
)
|
||||
@@ -383,7 +375,6 @@ async def test_call_creates_background_task_for_sending(
|
||||
return_value=mock_workspace
|
||||
)
|
||||
mock_linear_manager.send_message = AsyncMock()
|
||||
mock_linear_manager.create_outgoing_message.return_value = MagicMock()
|
||||
|
||||
with patch(
|
||||
'server.conversation_callback_processor.linear_callback_processor.asyncio.create_task'
|
||||
|
||||
@@ -234,7 +234,6 @@ class TestSlackCallbackProcessor:
|
||||
mock_slack_user = MagicMock()
|
||||
mock_saas_user_auth = MagicMock()
|
||||
mock_slack_view = MagicMock()
|
||||
mock_outgoing_message = MagicMock()
|
||||
|
||||
# Mock the authenticate_user method on slack_manager
|
||||
mock_slack_manager.authenticate_user = AsyncMock(
|
||||
@@ -248,9 +247,6 @@ class TestSlackCallbackProcessor:
|
||||
mock_slack_factory.create_slack_view_from_payload.return_value = (
|
||||
mock_slack_view
|
||||
)
|
||||
mock_slack_manager.create_outgoing_message.return_value = (
|
||||
mock_outgoing_message
|
||||
)
|
||||
mock_slack_manager.send_message = AsyncMock()
|
||||
|
||||
# Call the method
|
||||
@@ -283,12 +279,9 @@ class TestSlackCallbackProcessor:
|
||||
)
|
||||
assert message_call.message['team_id'] == slack_callback_processor.team_id
|
||||
|
||||
# Verify the slack manager methods were called correctly
|
||||
mock_slack_manager.create_outgoing_message.assert_called_once_with(
|
||||
'Test message'
|
||||
)
|
||||
# Verify send_message was called with the string directly
|
||||
mock_slack_manager.send_message.assert_called_once_with(
|
||||
mock_outgoing_message, mock_slack_view
|
||||
'Test message', mock_slack_view
|
||||
)
|
||||
|
||||
@patch('server.conversation_callback_processor.slack_callback_processor.logger')
|
||||
|
||||
Reference in New Issue
Block a user