V1 GitHub resolver fixes (#12199)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra
2026-01-06 11:33:54 -08:00
committed by GitHub
parent d053a3d363
commit 9686ee02f3
13 changed files with 267 additions and 244 deletions

View File

@@ -310,7 +310,7 @@ class GithubManager(Manager):
f'[GitHub] Created conversation {conversation_id} for user {user_info.username}' f'[GitHub] Created conversation {conversation_id} for user {user_info.username}'
) )
if not github_view.v1: if not github_view.v1_enabled:
# Create a GithubCallbackProcessor # Create a GithubCallbackProcessor
processor = GithubCallbackProcessor( processor = GithubCallbackProcessor(
github_view=github_view, github_view=github_view,

View File

@@ -1,11 +1,12 @@
import logging import logging
import os
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
import httpx import httpx
from github import Auth, Github, GithubIntegration from github import Auth, Github, GithubIntegration
from integrations.utils import CONVERSATION_URL, get_summary_instruction
from pydantic import Field from pydantic import Field
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
from openhands.agent_server.models import AskAgentRequest, AskAgentResponse from openhands.agent_server.models import AskAgentRequest, AskAgentResponse
from openhands.app_server.event_callback.event_callback_models import ( from openhands.app_server.event_callback.event_callback_models import (
@@ -20,8 +21,6 @@ from openhands.app_server.event_callback.util import (
ensure_conversation_found, ensure_conversation_found,
ensure_running_sandbox, ensure_running_sandbox,
get_agent_server_url_from_sandbox, get_agent_server_url_from_sandbox,
get_conversation_url,
get_prompt_template,
) )
from openhands.sdk import Event from openhands.sdk import Event
from openhands.sdk.event import ConversationStateUpdateEvent from openhands.sdk.event import ConversationStateUpdateEvent
@@ -34,7 +33,6 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
github_view_data: dict[str, Any] = Field(default_factory=dict) github_view_data: dict[str, Any] = Field(default_factory=dict)
should_request_summary: bool = Field(default=True) should_request_summary: bool = Field(default=True)
should_extract: bool = Field(default=True)
inline_pr_comment: bool = Field(default=False) inline_pr_comment: bool = Field(default=False)
async def __call__( async def __call__(
@@ -64,7 +62,12 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
self.should_request_summary = False self.should_request_summary = False
try: try:
_logger.info(f'[GitHub V1] Requesting summary {conversation_id}')
summary = await self._request_summary(conversation_id) summary = await self._request_summary(conversation_id)
_logger.info(
f'[GitHub V1] Posting summary {conversation_id}',
extra={'summary': summary},
)
await self._post_summary_to_github(summary) await self._post_summary_to_github(summary)
return EventCallbackResult( return EventCallbackResult(
@@ -82,12 +85,12 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
# Check if we have installation ID and credentials before posting # Check if we have installation ID and credentials before posting
if ( if (
self.github_view_data.get('installation_id') self.github_view_data.get('installation_id')
and os.getenv('GITHUB_APP_CLIENT_ID') and GITHUB_APP_CLIENT_ID
and os.getenv('GITHUB_APP_PRIVATE_KEY') and GITHUB_APP_PRIVATE_KEY
): ):
await self._post_summary_to_github( await self._post_summary_to_github(
f'OpenHands encountered an error: **{str(e)}**.\n\n' f'OpenHands encountered an error: **{str(e)}**.\n\n'
f'[See the conversation]({get_conversation_url().format(conversation_id)})' f'[See the conversation]({CONVERSATION_URL.format(conversation_id)})'
'for more information.' 'for more information.'
) )
except Exception as post_error: except Exception as post_error:
@@ -115,16 +118,11 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
f'Missing installation ID for GitHub payload: {self.github_view_data}' f'Missing installation ID for GitHub payload: {self.github_view_data}'
) )
github_app_client_id = os.getenv('GITHUB_APP_CLIENT_ID', '').strip() if not GITHUB_APP_CLIENT_ID or not GITHUB_APP_PRIVATE_KEY:
github_app_private_key = os.getenv('GITHUB_APP_PRIVATE_KEY', '').replace(
'\\n', '\n'
)
if not github_app_client_id or not github_app_private_key:
raise ValueError('GitHub App credentials are not configured') raise ValueError('GitHub App credentials are not configured')
github_integration = GithubIntegration( github_integration = GithubIntegration(
auth=Auth.AppAuth(github_app_client_id, github_app_private_key), auth=Auth.AppAuth(GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY),
) )
token_data = github_integration.get_access_token(installation_id) token_data = github_integration.get_access_token(installation_id)
return token_data.token return token_data.token
@@ -274,16 +272,16 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
app_conversation_info.sandbox_id, app_conversation_info.sandbox_id,
) )
assert sandbox.session_api_key is not None, ( assert (
f'No session API key for sandbox: {sandbox.id}' sandbox.session_api_key is not None
) ), f'No session API key for sandbox: {sandbox.id}'
# 3. URL + instruction # 3. URL + instruction
agent_server_url = get_agent_server_url_from_sandbox(sandbox) agent_server_url = get_agent_server_url_from_sandbox(sandbox)
agent_server_url = get_agent_server_url_from_sandbox(sandbox) agent_server_url = get_agent_server_url_from_sandbox(sandbox)
# Prepare message based on agent state # Prepare message based on agent state
message_content = get_prompt_template('summary_prompt.j2') message_content = get_summary_instruction()
# Ask the agent and return the response text # Ask the agent and return the response text
return await self._ask_question( return await self._ask_question(

View File

@@ -140,7 +140,10 @@ class GithubIssue(ResolverViewInterface):
title: str title: str
description: str description: str
previous_comments: list[Comment] previous_comments: list[Comment]
v1: bool v1_enabled: bool
def _get_branch_name(self) -> str | None:
return getattr(self, 'branch_name', None)
async def _load_resolver_context(self): async def _load_resolver_context(self):
github_service = GithubServiceImpl( github_service = GithubServiceImpl(
@@ -188,23 +191,27 @@ class GithubIssue(ResolverViewInterface):
async def initialize_new_conversation(self) -> ConversationMetadata: async def initialize_new_conversation(self) -> ConversationMetadata:
# FIXME: Handle if initialize_conversation returns None # FIXME: Handle if initialize_conversation returns None
v1_enabled = await get_user_v1_enabled_setting(self.user_info.keycloak_user_id) self.v1_enabled = await get_user_v1_enabled_setting(
logger.info( self.user_info.keycloak_user_id
f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {v1_enabled}'
) )
if v1_enabled: logger.info(
f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {self.v1_enabled}'
)
if self.v1_enabled:
# Create dummy conversationm metadata # Create dummy conversationm metadata
# Don't save to conversation store # Don't save to conversation store
# V1 conversations are stored in a separate table # V1 conversations are stored in a separate table
self.conversation_id = uuid4().hex
return ConversationMetadata( return ConversationMetadata(
conversation_id=uuid4().hex, selected_repository=self.full_repo_name conversation_id=self.conversation_id,
selected_repository=self.full_repo_name,
) )
conversation_metadata: ConversationMetadata = await initialize_conversation( # type: ignore[assignment] conversation_metadata: ConversationMetadata = await initialize_conversation( # type: ignore[assignment]
user_id=self.user_info.keycloak_user_id, user_id=self.user_info.keycloak_user_id,
conversation_id=None, conversation_id=None,
selected_repository=self.full_repo_name, selected_repository=self.full_repo_name,
selected_branch=None, selected_branch=self._get_branch_name(),
conversation_trigger=ConversationTrigger.RESOLVER, conversation_trigger=ConversationTrigger.RESOLVER,
git_provider=ProviderType.GITHUB, git_provider=ProviderType.GITHUB,
) )
@@ -218,25 +225,18 @@ class GithubIssue(ResolverViewInterface):
conversation_metadata: ConversationMetadata, conversation_metadata: ConversationMetadata,
saas_user_auth: UserAuth, saas_user_auth: UserAuth,
): ):
v1_enabled = await get_user_v1_enabled_setting(self.user_info.keycloak_user_id)
logger.info( logger.info(
f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {v1_enabled}' f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {self.v1_enabled}'
)
if v1_enabled:
try:
# Use V1 app conversation service
await self._create_v1_conversation(
jinja_env, saas_user_auth, conversation_metadata
)
return
except Exception as e:
logger.warning(f'Error checking V1 settings, falling back to V0: {e}')
# Use existing V0 conversation service
await self._create_v0_conversation(
jinja_env, git_provider_tokens, conversation_metadata
) )
if self.v1_enabled:
# Use V1 app conversation service
await self._create_v1_conversation(
jinja_env, saas_user_auth, conversation_metadata
)
else:
await self._create_v0_conversation(
jinja_env, git_provider_tokens, conversation_metadata
)
async def _create_v0_conversation( async def _create_v0_conversation(
self, self,
@@ -294,6 +294,7 @@ class GithubIssue(ResolverViewInterface):
system_message_suffix=conversation_instructions, system_message_suffix=conversation_instructions,
initial_message=initial_message, initial_message=initial_message,
selected_repository=self.full_repo_name, selected_repository=self.full_repo_name,
selected_branch=self._get_branch_name(),
git_provider=ProviderType.GITHUB, git_provider=ProviderType.GITHUB,
title=f'GitHub Issue #{self.issue_number}: {self.title}', title=f'GitHub Issue #{self.issue_number}: {self.title}',
trigger=ConversationTrigger.RESOLVER, trigger=ConversationTrigger.RESOLVER,
@@ -318,11 +319,9 @@ class GithubIssue(ResolverViewInterface):
f'Failed to start V1 conversation: {task.detail}' f'Failed to start V1 conversation: {task.detail}'
) )
self.v1 = True
def _create_github_v1_callback_processor(self): def _create_github_v1_callback_processor(self):
"""Create a V1 callback processor for GitHub integration.""" """Create a V1 callback processor for GitHub integration."""
from openhands.app_server.event_callback.github_v1_callback_processor import ( from integrations.github.github_v1_callback_processor import (
GithubV1CallbackProcessor, GithubV1CallbackProcessor,
) )
@@ -390,31 +389,6 @@ class GithubPRComment(GithubIssueComment):
return user_instructions, conversation_instructions return user_instructions, conversation_instructions
async def initialize_new_conversation(self) -> ConversationMetadata:
v1_enabled = await get_user_v1_enabled_setting(self.user_info.keycloak_user_id)
logger.info(
f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {v1_enabled}'
)
if v1_enabled:
# Create dummy conversationm metadata
# Don't save to conversation store
# V1 conversations are stored in a separate table
return ConversationMetadata(
conversation_id=uuid4().hex, selected_repository=self.full_repo_name
)
conversation_metadata: ConversationMetadata = await initialize_conversation( # type: ignore[assignment]
user_id=self.user_info.keycloak_user_id,
conversation_id=None,
selected_repository=self.full_repo_name,
selected_branch=self.branch_name,
conversation_trigger=ConversationTrigger.RESOLVER,
git_provider=ProviderType.GITHUB,
)
self.conversation_id = conversation_metadata.conversation_id
return conversation_metadata
@dataclass @dataclass
class GithubInlinePRComment(GithubPRComment): class GithubInlinePRComment(GithubPRComment):
@@ -830,7 +804,7 @@ class GithubFactory:
title='', title='',
description='', description='',
previous_comments=[], previous_comments=[],
v1=False, v1_enabled=False,
) )
elif GithubFactory.is_issue_comment(message): elif GithubFactory.is_issue_comment(message):
@@ -856,7 +830,7 @@ class GithubFactory:
title='', title='',
description='', description='',
previous_comments=[], previous_comments=[],
v1=False, v1_enabled=False,
) )
elif GithubFactory.is_pr_comment(message): elif GithubFactory.is_pr_comment(message):
@@ -898,7 +872,7 @@ class GithubFactory:
title='', title='',
description='', description='',
previous_comments=[], previous_comments=[],
v1=False, v1_enabled=False,
) )
elif GithubFactory.is_inline_pr_comment(message): elif GithubFactory.is_inline_pr_comment(message):
@@ -932,7 +906,7 @@ class GithubFactory:
title='', title='',
description='', description='',
previous_comments=[], previous_comments=[],
v1=False, v1_enabled=False,
) )
else: else:

View File

@@ -79,7 +79,10 @@ ENABLE_V1_GITHUB_RESOLVER = (
) )
OPENHANDS_RESOLVER_TEMPLATES_DIR = 'openhands/integrations/templates/resolver/' OPENHANDS_RESOLVER_TEMPLATES_DIR = (
os.getenv('OPENHANDS_RESOLVER_TEMPLATES_DIR')
or 'openhands/integrations/templates/resolver/'
)
jinja_env = Environment(loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR)) jinja_env = Environment(loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR))

View File

@@ -10,12 +10,14 @@ Covers:
- Low-level helper methods - Low-level helper methods
""" """
import os
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4 from uuid import uuid4
import httpx import httpx
import pytest import pytest
from integrations.github.github_v1_callback_processor import (
GithubV1CallbackProcessor,
)
from openhands.app_server.app_conversation.app_conversation_models import ( from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationInfo, AppConversationInfo,
@@ -24,9 +26,6 @@ from openhands.app_server.event_callback.event_callback_models import EventCallb
from openhands.app_server.event_callback.event_callback_result_models import ( from openhands.app_server.event_callback.event_callback_result_models import (
EventCallbackResultStatus, EventCallbackResultStatus,
) )
from openhands.app_server.event_callback.github_v1_callback_processor import (
GithubV1CallbackProcessor,
)
from openhands.app_server.sandbox.sandbox_models import ( from openhands.app_server.sandbox.sandbox_models import (
ExposedUrl, ExposedUrl,
SandboxInfo, SandboxInfo,
@@ -198,30 +197,27 @@ class TestGithubV1CallbackProcessor:
# Successful paths # Successful paths
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@patch.dict( @patch(
os.environ, 'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
{ 'test_client_id',
'GITHUB_APP_CLIENT_ID': 'test_client_id', )
'GITHUB_APP_PRIVATE_KEY': 'test_private_key', @patch(
}, 'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'test_private_key',
) )
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@patch('openhands.app_server.config.get_httpx_client') @patch('openhands.app_server.config.get_httpx_client')
@patch( @patch('integrations.github.github_v1_callback_processor.get_summary_instruction')
'openhands.app_server.event_callback.github_v1_callback_processor.get_prompt_template' @patch('integrations.github.github_v1_callback_processor.Auth')
) @patch('integrations.github.github_v1_callback_processor.GithubIntegration')
@patch('openhands.app_server.event_callback.github_v1_callback_processor.Auth') @patch('integrations.github.github_v1_callback_processor.Github')
@patch(
'openhands.app_server.event_callback.github_v1_callback_processor.GithubIntegration'
)
@patch('openhands.app_server.event_callback.github_v1_callback_processor.Github')
async def test_successful_callback_execution( async def test_successful_callback_execution(
self, self,
mock_github, mock_github,
mock_github_integration, mock_github_integration,
mock_auth, mock_auth,
mock_get_prompt_template, mock_get_summary_instruction,
mock_get_httpx_client, mock_get_httpx_client,
mock_get_sandbox_service, mock_get_sandbox_service,
mock_get_app_conversation_info_service, mock_get_app_conversation_info_service,
@@ -242,7 +238,7 @@ class TestGithubV1CallbackProcessor:
mock_sandbox_info, mock_sandbox_info,
) )
mock_get_prompt_template.return_value = 'Please provide a summary' mock_get_summary_instruction.return_value = 'Please provide a summary'
# Auth.AppAuth mock # Auth.AppAuth mock
mock_app_auth_instance = MagicMock() mock_app_auth_instance = MagicMock()
@@ -293,28 +289,25 @@ class TestGithubV1CallbackProcessor:
assert kwargs['headers']['X-Session-API-Key'] == 'test_api_key' assert kwargs['headers']['X-Session-API-Key'] == 'test_api_key'
assert kwargs['json']['question'] == 'Please provide a summary' assert kwargs['json']['question'] == 'Please provide a summary'
@patch.dict( @patch(
os.environ, 'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
{ 'test_client_id',
'GITHUB_APP_CLIENT_ID': 'test_client_id', )
'GITHUB_APP_PRIVATE_KEY': 'test_private_key', @patch(
}, 'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'test_private_key',
) )
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@patch('openhands.app_server.config.get_httpx_client') @patch('openhands.app_server.config.get_httpx_client')
@patch( @patch('integrations.github.github_v1_callback_processor.get_summary_instruction')
'openhands.app_server.event_callback.github_v1_callback_processor.get_prompt_template' @patch('integrations.github.github_v1_callback_processor.GithubIntegration')
) @patch('integrations.github.github_v1_callback_processor.Github')
@patch(
'openhands.app_server.event_callback.github_v1_callback_processor.GithubIntegration'
)
@patch('openhands.app_server.event_callback.github_v1_callback_processor.Github')
async def test_successful_inline_pr_comment( async def test_successful_inline_pr_comment(
self, self,
mock_github, mock_github,
mock_github_integration, mock_github_integration,
mock_get_prompt_template, mock_get_summary_instruction,
mock_get_httpx_client, mock_get_httpx_client,
mock_get_sandbox_service, mock_get_sandbox_service,
mock_get_app_conversation_info_service, mock_get_app_conversation_info_service,
@@ -334,7 +327,7 @@ class TestGithubV1CallbackProcessor:
mock_sandbox_info, mock_sandbox_info,
) )
mock_get_prompt_template.return_value = 'Please provide a summary' mock_get_summary_instruction.return_value = 'Please provide a summary'
mock_token_data = MagicMock() mock_token_data = MagicMock()
mock_token_data.token = 'test_access_token' mock_token_data.token = 'test_access_token'
@@ -367,6 +360,7 @@ class TestGithubV1CallbackProcessor:
# Error paths # Error paths
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@patch('integrations.github.github_v1_callback_processor.get_summary_instruction')
@patch('openhands.app_server.config.get_httpx_client') @patch('openhands.app_server.config.get_httpx_client')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@@ -375,6 +369,7 @@ class TestGithubV1CallbackProcessor:
mock_get_app_conversation_info_service, mock_get_app_conversation_info_service,
mock_get_sandbox_service, mock_get_sandbox_service,
mock_get_httpx_client, mock_get_httpx_client,
mock_get_summary_instruction,
conversation_state_update_event, conversation_state_update_event,
event_callback, event_callback,
mock_app_conversation_info, mock_app_conversation_info,
@@ -393,6 +388,8 @@ class TestGithubV1CallbackProcessor:
mock_sandbox_info, mock_sandbox_info,
) )
mock_get_summary_instruction.return_value = 'Please provide a summary'
result = await processor( result = await processor(
conversation_id=conversation_id, conversation_id=conversation_id,
callback=event_callback, callback=event_callback,
@@ -403,7 +400,15 @@ class TestGithubV1CallbackProcessor:
assert result.status == EventCallbackResultStatus.ERROR assert result.status == EventCallbackResultStatus.ERROR
assert 'Missing installation ID' in result.detail assert 'Missing installation ID' in result.detail
@patch.dict(os.environ, {}, clear=True) @patch(
'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
'',
)
@patch(
'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'',
)
@patch('integrations.github.github_v1_callback_processor.get_summary_instruction')
@patch('openhands.app_server.config.get_httpx_client') @patch('openhands.app_server.config.get_httpx_client')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@@ -412,6 +417,7 @@ class TestGithubV1CallbackProcessor:
mock_get_app_conversation_info_service, mock_get_app_conversation_info_service,
mock_get_sandbox_service, mock_get_sandbox_service,
mock_get_httpx_client, mock_get_httpx_client,
mock_get_summary_instruction,
github_callback_processor, github_callback_processor,
conversation_state_update_event, conversation_state_update_event,
event_callback, event_callback,
@@ -428,6 +434,8 @@ class TestGithubV1CallbackProcessor:
mock_sandbox_info, mock_sandbox_info,
) )
mock_get_summary_instruction.return_value = 'Please provide a summary'
result = await github_callback_processor( result = await github_callback_processor(
conversation_id=conversation_id, conversation_id=conversation_id,
callback=event_callback, callback=event_callback,
@@ -438,12 +446,13 @@ class TestGithubV1CallbackProcessor:
assert result.status == EventCallbackResultStatus.ERROR assert result.status == EventCallbackResultStatus.ERROR
assert 'GitHub App credentials are not configured' in result.detail assert 'GitHub App credentials are not configured' in result.detail
@patch.dict( @patch(
os.environ, 'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
{ 'test_client_id',
'GITHUB_APP_CLIENT_ID': 'test_client_id', )
'GITHUB_APP_PRIVATE_KEY': 'test_private_key', @patch(
}, 'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'test_private_key',
) )
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@@ -489,22 +498,21 @@ class TestGithubV1CallbackProcessor:
assert result.status == EventCallbackResultStatus.ERROR assert result.status == EventCallbackResultStatus.ERROR
assert 'Sandbox not running' in result.detail assert 'Sandbox not running' in result.detail
@patch.dict( @patch(
os.environ, 'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
{ 'test_client_id',
'GITHUB_APP_CLIENT_ID': 'test_client_id', )
'GITHUB_APP_PRIVATE_KEY': 'test_private_key', @patch(
}, 'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'test_private_key',
) )
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@patch('openhands.app_server.config.get_httpx_client') @patch('openhands.app_server.config.get_httpx_client')
@patch( @patch('integrations.github.github_v1_callback_processor.get_summary_instruction')
'openhands.app_server.event_callback.github_v1_callback_processor.get_prompt_template'
)
async def test_agent_server_http_error( async def test_agent_server_http_error(
self, self,
mock_get_prompt_template, mock_get_summary_instruction,
mock_get_httpx_client, mock_get_httpx_client,
mock_get_sandbox_service, mock_get_sandbox_service,
mock_get_app_conversation_info_service, mock_get_app_conversation_info_service,
@@ -525,7 +533,7 @@ class TestGithubV1CallbackProcessor:
mock_sandbox_info, mock_sandbox_info,
) )
mock_get_prompt_template.return_value = 'Please provide a summary' mock_get_summary_instruction.return_value = 'Please provide a summary'
mock_httpx_client = mock_get_httpx_client.return_value.__aenter__.return_value mock_httpx_client = mock_get_httpx_client.return_value.__aenter__.return_value
mock_response = MagicMock() mock_response = MagicMock()
@@ -547,22 +555,21 @@ class TestGithubV1CallbackProcessor:
assert result.status == EventCallbackResultStatus.ERROR assert result.status == EventCallbackResultStatus.ERROR
assert 'Failed to send message to agent server' in result.detail assert 'Failed to send message to agent server' in result.detail
@patch.dict( @patch(
os.environ, 'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
{ 'test_client_id',
'GITHUB_APP_CLIENT_ID': 'test_client_id', )
'GITHUB_APP_PRIVATE_KEY': 'test_private_key', @patch(
}, 'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'test_private_key',
) )
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@patch('openhands.app_server.config.get_httpx_client') @patch('openhands.app_server.config.get_httpx_client')
@patch( @patch('integrations.github.github_v1_callback_processor.get_summary_instruction')
'openhands.app_server.event_callback.github_v1_callback_processor.get_prompt_template'
)
async def test_agent_server_timeout( async def test_agent_server_timeout(
self, self,
mock_get_prompt_template, mock_get_summary_instruction,
mock_get_httpx_client, mock_get_httpx_client,
mock_get_sandbox_service, mock_get_sandbox_service,
mock_get_app_conversation_info_service, mock_get_app_conversation_info_service,
@@ -582,7 +589,7 @@ class TestGithubV1CallbackProcessor:
mock_sandbox_info, mock_sandbox_info,
) )
mock_get_prompt_template.return_value = 'Please provide a summary' mock_get_summary_instruction.return_value = 'Please provide a summary'
mock_httpx_client = mock_get_httpx_client.return_value.__aenter__.return_value mock_httpx_client = mock_get_httpx_client.return_value.__aenter__.return_value
mock_httpx_client.post.side_effect = httpx.TimeoutException('Request timeout') mock_httpx_client.post.side_effect = httpx.TimeoutException('Request timeout')
@@ -607,7 +614,14 @@ class TestGithubV1CallbackProcessor:
with pytest.raises(ValueError, match='Missing installation ID'): with pytest.raises(ValueError, match='Missing installation ID'):
processor._get_installation_access_token() processor._get_installation_access_token()
@patch.dict(os.environ, {}, clear=True) @patch(
'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
'',
)
@patch(
'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'',
)
def test_get_installation_access_token_missing_credentials( def test_get_installation_access_token_missing_credentials(
self, github_callback_processor self, github_callback_processor
): ):
@@ -616,17 +630,16 @@ class TestGithubV1CallbackProcessor:
): ):
github_callback_processor._get_installation_access_token() github_callback_processor._get_installation_access_token()
@patch.dict(
os.environ,
{
'GITHUB_APP_CLIENT_ID': 'test_client_id',
'GITHUB_APP_PRIVATE_KEY': 'test_private_key\\nwith_newlines',
},
)
@patch('openhands.app_server.event_callback.github_v1_callback_processor.Auth')
@patch( @patch(
'openhands.app_server.event_callback.github_v1_callback_processor.GithubIntegration' 'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
'test_client_id',
) )
@patch(
'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'test_private_key\nwith_newlines',
)
@patch('integrations.github.github_v1_callback_processor.Auth')
@patch('integrations.github.github_v1_callback_processor.GithubIntegration')
def test_get_installation_access_token_success( def test_get_installation_access_token_success(
self, mock_github_integration, mock_auth, github_callback_processor self, mock_github_integration, mock_auth, github_callback_processor
): ):
@@ -649,7 +662,7 @@ class TestGithubV1CallbackProcessor:
mock_github_integration.assert_called_once_with(auth=mock_app_auth_instance) mock_github_integration.assert_called_once_with(auth=mock_app_auth_instance)
mock_integration_instance.get_access_token.assert_called_once_with(12345) mock_integration_instance.get_access_token.assert_called_once_with(12345)
@patch('openhands.app_server.event_callback.github_v1_callback_processor.Github') @patch('integrations.github.github_v1_callback_processor.Github')
async def test_post_summary_to_github_issue_comment( async def test_post_summary_to_github_issue_comment(
self, mock_github, github_callback_processor self, mock_github, github_callback_processor
): ):
@@ -672,7 +685,7 @@ class TestGithubV1CallbackProcessor:
mock_repo.get_issue.assert_called_once_with(number=42) mock_repo.get_issue.assert_called_once_with(number=42)
mock_issue.create_comment.assert_called_once_with('Test summary') mock_issue.create_comment.assert_called_once_with('Test summary')
@patch('openhands.app_server.event_callback.github_v1_callback_processor.Github') @patch('integrations.github.github_v1_callback_processor.Github')
async def test_post_summary_to_github_pr_comment( async def test_post_summary_to_github_pr_comment(
self, mock_github, github_callback_processor_inline self, mock_github, github_callback_processor_inline
): ):
@@ -708,14 +721,15 @@ class TestGithubV1CallbackProcessor:
with pytest.raises(RuntimeError, match='Missing GitHub credentials'): with pytest.raises(RuntimeError, match='Missing GitHub credentials'):
await github_callback_processor._post_summary_to_github('Test summary') await github_callback_processor._post_summary_to_github('Test summary')
@patch.dict( @patch(
os.environ, 'integrations.github.github_v1_callback_processor.GITHUB_APP_CLIENT_ID',
{ 'test_client_id',
'GITHUB_APP_CLIENT_ID': 'test_client_id',
'GITHUB_APP_PRIVATE_KEY': 'test_private_key',
'WEB_HOST': 'test.example.com',
},
) )
@patch(
'integrations.github.github_v1_callback_processor.GITHUB_APP_PRIVATE_KEY',
'test_private_key',
)
@patch('integrations.github.github_v1_callback_processor.get_summary_instruction')
@patch('openhands.app_server.config.get_httpx_client') @patch('openhands.app_server.config.get_httpx_client')
@patch('openhands.app_server.config.get_sandbox_service') @patch('openhands.app_server.config.get_sandbox_service')
@patch('openhands.app_server.config.get_app_conversation_info_service') @patch('openhands.app_server.config.get_app_conversation_info_service')
@@ -724,6 +738,7 @@ class TestGithubV1CallbackProcessor:
mock_get_app_conversation_info_service, mock_get_app_conversation_info_service,
mock_get_sandbox_service, mock_get_sandbox_service,
mock_get_httpx_client, mock_get_httpx_client,
mock_get_summary_instruction,
github_callback_processor, github_callback_processor,
conversation_state_update_event, conversation_state_update_event,
event_callback, event_callback,
@@ -741,13 +756,14 @@ class TestGithubV1CallbackProcessor:
mock_sandbox_info, mock_sandbox_info,
) )
mock_httpx_client.post.side_effect = Exception('Simulated agent server error') mock_httpx_client.post.side_effect = Exception('Simulated agent server error')
mock_get_summary_instruction.return_value = 'Please provide a summary'
with ( with (
patch( patch(
'openhands.app_server.event_callback.github_v1_callback_processor.GithubIntegration' 'integrations.github.github_v1_callback_processor.GithubIntegration'
) as mock_github_integration, ) as mock_github_integration,
patch( patch(
'openhands.app_server.event_callback.github_v1_callback_processor.Github' 'integrations.github.github_v1_callback_processor.Github'
) as mock_github, ) as mock_github,
): ):
mock_integration = MagicMock() mock_integration = MagicMock()

View File

@@ -86,12 +86,12 @@ class TestGithubV1ConversationRouting(TestCase):
def setUp(self): def setUp(self):
"""Set up test fixtures.""" """Set up test fixtures."""
# Create a proper UserData instance instead of MagicMock # Create a proper UserData instance instead of MagicMock
user_data = UserData( self.user_data = UserData(
user_id=123, username='testuser', keycloak_user_id='test-keycloak-id' user_id=123, username='testuser', keycloak_user_id='test-keycloak-id'
) )
# Create a mock raw_payload # Create a mock raw_payload
raw_payload = Message( self.raw_payload = Message(
source=SourceType.GITHUB, source=SourceType.GITHUB,
message={ message={
'payload': { 'payload': {
@@ -101,8 +101,10 @@ class TestGithubV1ConversationRouting(TestCase):
}, },
) )
self.github_issue = GithubIssue( def _create_github_issue(self):
user_info=user_data, """Create a GithubIssue instance for testing."""
return GithubIssue(
user_info=self.user_data,
full_repo_name='test/repo', full_repo_name='test/repo',
issue_number=123, issue_number=123,
installation_id=456, installation_id=456,
@@ -110,35 +112,72 @@ class TestGithubV1ConversationRouting(TestCase):
should_extract=True, should_extract=True,
send_summary_instruction=False, send_summary_instruction=False,
is_public_repo=True, is_public_repo=True,
raw_payload=raw_payload, raw_payload=self.raw_payload,
uuid='test-uuid', uuid='test-uuid',
title='Test Issue', title='Test Issue',
description='Test issue description', description='Test issue description',
previous_comments=[], previous_comments=[],
v1=False, v1_enabled=False,
) )
@pytest.mark.asyncio @pytest.mark.asyncio
@patch('integrations.github.github_view.initialize_conversation')
@patch('integrations.github.github_view.get_user_v1_enabled_setting') @patch('integrations.github.github_view.get_user_v1_enabled_setting')
async def test_initialize_sets_v1_enabled_from_setting_when_false(
self, mock_get_v1_setting, mock_initialize_conversation
):
"""Test that initialize_new_conversation sets v1_enabled from get_user_v1_enabled_setting."""
mock_get_v1_setting.return_value = False
mock_initialize_conversation.return_value = MagicMock(
conversation_id='new-conversation-id'
)
github_issue = self._create_github_issue()
await github_issue.initialize_new_conversation()
# Verify get_user_v1_enabled_setting was called with correct user ID
mock_get_v1_setting.assert_called_once_with('test-keycloak-id')
# Verify v1_enabled was set to False
self.assertFalse(github_issue.v1_enabled)
@pytest.mark.asyncio
@patch('integrations.github.github_view.get_user_v1_enabled_setting')
async def test_initialize_sets_v1_enabled_from_setting_when_true(
self, mock_get_v1_setting
):
"""Test that initialize_new_conversation sets v1_enabled to True when setting returns True."""
mock_get_v1_setting.return_value = True
github_issue = self._create_github_issue()
await github_issue.initialize_new_conversation()
# Verify get_user_v1_enabled_setting was called with correct user ID
mock_get_v1_setting.assert_called_once_with('test-keycloak-id')
# Verify v1_enabled was set to True
self.assertTrue(github_issue.v1_enabled)
@pytest.mark.asyncio
@patch.object(GithubIssue, '_create_v0_conversation') @patch.object(GithubIssue, '_create_v0_conversation')
@patch.object(GithubIssue, '_create_v1_conversation') @patch.object(GithubIssue, '_create_v1_conversation')
async def test_create_new_conversation_routes_to_v0_when_disabled( async def test_create_new_conversation_routes_to_v0_when_disabled(
self, mock_create_v1, mock_create_v0, mock_get_v1_setting self, mock_create_v1, mock_create_v0
): ):
"""Test that conversation creation routes to V0 when v1_enabled is False.""" """Test that conversation creation routes to V0 when v1_enabled is False."""
# Mock v1_enabled as False
mock_get_v1_setting.return_value = False
mock_create_v0.return_value = None mock_create_v0.return_value = None
mock_create_v1.return_value = None mock_create_v1.return_value = None
github_issue = self._create_github_issue()
github_issue.v1_enabled = False
# Mock parameters # Mock parameters
jinja_env = MagicMock() jinja_env = MagicMock()
git_provider_tokens = MagicMock() git_provider_tokens = MagicMock()
conversation_metadata = MagicMock() conversation_metadata = MagicMock()
saas_user_auth = MagicMock()
# Call the method # Call the method
await self.github_issue.create_new_conversation( await github_issue.create_new_conversation(
jinja_env, git_provider_tokens, conversation_metadata jinja_env, git_provider_tokens, conversation_metadata, saas_user_auth
) )
# Verify V0 was called and V1 was not # Verify V0 was called and V1 was not
@@ -148,62 +187,31 @@ class TestGithubV1ConversationRouting(TestCase):
mock_create_v1.assert_not_called() mock_create_v1.assert_not_called()
@pytest.mark.asyncio @pytest.mark.asyncio
@patch('integrations.github.github_view.get_user_v1_enabled_setting')
@patch.object(GithubIssue, '_create_v0_conversation') @patch.object(GithubIssue, '_create_v0_conversation')
@patch.object(GithubIssue, '_create_v1_conversation') @patch.object(GithubIssue, '_create_v1_conversation')
async def test_create_new_conversation_routes_to_v1_when_enabled( async def test_create_new_conversation_routes_to_v1_when_enabled(
self, mock_create_v1, mock_create_v0, mock_get_v1_setting self, mock_create_v1, mock_create_v0
): ):
"""Test that conversation creation routes to V1 when v1_enabled is True.""" """Test that conversation creation routes to V1 when v1_enabled is True."""
# Mock v1_enabled as True
mock_get_v1_setting.return_value = True
mock_create_v0.return_value = None mock_create_v0.return_value = None
mock_create_v1.return_value = None mock_create_v1.return_value = None
github_issue = self._create_github_issue()
github_issue.v1_enabled = True
# Mock parameters # Mock parameters
jinja_env = MagicMock() jinja_env = MagicMock()
git_provider_tokens = MagicMock() git_provider_tokens = MagicMock()
conversation_metadata = MagicMock() conversation_metadata = MagicMock()
saas_user_auth = MagicMock()
# Call the method # Call the method
await self.github_issue.create_new_conversation( await github_issue.create_new_conversation(
jinja_env, git_provider_tokens, conversation_metadata jinja_env, git_provider_tokens, conversation_metadata, saas_user_auth
) )
# Verify V1 was called and V0 was not # Verify V1 was called and V0 was not
mock_create_v1.assert_called_once_with( mock_create_v1.assert_called_once_with(
jinja_env, git_provider_tokens, conversation_metadata jinja_env, saas_user_auth, conversation_metadata
) )
mock_create_v0.assert_not_called() mock_create_v0.assert_not_called()
@pytest.mark.asyncio
@patch('integrations.github.github_view.get_user_v1_enabled_setting')
@patch.object(GithubIssue, '_create_v0_conversation')
@patch.object(GithubIssue, '_create_v1_conversation')
async def test_create_new_conversation_fallback_on_v1_setting_error(
self, mock_create_v1, mock_create_v0, mock_get_v1_setting
):
"""Test that conversation creation falls back to V0 when _create_v1_conversation fails."""
# Mock v1_enabled as True so V1 is attempted
mock_get_v1_setting.return_value = True
# Mock _create_v1_conversation to raise an exception
mock_create_v1.side_effect = Exception('V1 conversation creation failed')
mock_create_v0.return_value = None
# Mock parameters
jinja_env = MagicMock()
git_provider_tokens = MagicMock()
conversation_metadata = MagicMock()
# Call the method
await self.github_issue.create_new_conversation(
jinja_env, git_provider_tokens, conversation_metadata
)
# Verify V1 was attempted first, then V0 was called as fallback
mock_create_v1.assert_called_once_with(
jinja_env, git_provider_tokens, conversation_metadata
)
mock_create_v0.assert_called_once_with(
jinja_env, git_provider_tokens, conversation_metadata
)

View File

@@ -14,6 +14,7 @@ from openhands.app_server.sandbox.sandbox_models import SandboxStatus
from openhands.integrations.service_types import ProviderType from openhands.integrations.service_types import ProviderType
from openhands.sdk.conversation.state import ConversationExecutionStatus from openhands.sdk.conversation.state import ConversationExecutionStatus
from openhands.sdk.llm import MetricsSnapshot from openhands.sdk.llm import MetricsSnapshot
from openhands.sdk.utils.models import OpenHandsModel
from openhands.storage.data_models.conversation_metadata import ConversationTrigger from openhands.storage.data_models.conversation_metadata import ConversationTrigger
@@ -91,7 +92,7 @@ class AppConversationPage(BaseModel):
next_page_id: str | None = None next_page_id: str | None = None
class AppConversationStartRequest(BaseModel): class AppConversationStartRequest(OpenHandsModel):
"""Start conversation request object. """Start conversation request object.
Although a user can go directly to the sandbox and start conversations, they Although a user can go directly to the sandbox and start conversations, they
@@ -142,7 +143,7 @@ class AppConversationStartTaskSortOrder(Enum):
UPDATED_AT_DESC = 'UPDATED_AT_DESC' UPDATED_AT_DESC = 'UPDATED_AT_DESC'
class AppConversationStartTask(BaseModel): class AppConversationStartTask(OpenHandsModel):
"""Object describing the start process for an app conversation. """Object describing the start process for an app conversation.
Because starting an app conversation can be slow (And can involve starting a sandbox), Because starting an app conversation can be slow (And can involve starting a sandbox),
@@ -167,7 +168,7 @@ class AppConversationStartTask(BaseModel):
updated_at: datetime = Field(default_factory=utc_now) updated_at: datetime = Field(default_factory=utc_now)
class AppConversationStartTaskPage(BaseModel): class AppConversationStartTaskPage(OpenHandsModel):
items: list[AppConversationStartTask] items: list[AppConversationStartTask]
next_page_id: str | None = None next_page_id: str | None = None

View File

@@ -135,7 +135,7 @@ class SQLAppConversationStartTaskService(AppConversationStartTaskService):
if has_more: if has_more:
rows = rows[:limit] rows = rows[:limit]
items = [AppConversationStartTask(**row2dict(row)) for row in rows] items = [AppConversationStartTask.model_validate(row2dict(row)) for row in rows]
# Calculate next page ID # Calculate next page ID
next_page_id = None next_page_id = None
@@ -196,7 +196,7 @@ class SQLAppConversationStartTaskService(AppConversationStartTaskService):
# Return tasks in the same order as requested, with None for missing ones # Return tasks in the same order as requested, with None for missing ones
return [ return [
( (
AppConversationStartTask(**row2dict(tasks_by_id[task_id])) AppConversationStartTask.model_validate(row2dict(tasks_by_id[task_id]))
if task_id in tasks_by_id if task_id in tasks_by_id
else None else None
) )
@@ -218,7 +218,7 @@ class SQLAppConversationStartTaskService(AppConversationStartTaskService):
result = await self.session.execute(query) result = await self.session.execute(query)
stored_task = result.scalar_one_or_none() stored_task = result.scalar_one_or_none()
if stored_task: if stored_task:
return AppConversationStartTask(**row2dict(stored_task)) return AppConversationStartTask.model_validate(row2dict(stored_task))
return None return None
async def save_app_conversation_start_task( async def save_app_conversation_start_task(

View File

@@ -9,7 +9,6 @@ with the discriminated union system used by Pydantic for validation.
# Import base classes and processors without circular dependencies # Import base classes and processors without circular dependencies
from .event_callback_models import EventCallbackProcessor, LoggingCallbackProcessor from .event_callback_models import EventCallbackProcessor, LoggingCallbackProcessor
from .github_v1_callback_processor import GithubV1CallbackProcessor
# Note: SetTitleCallbackProcessor is not imported here to avoid circular imports # Note: SetTitleCallbackProcessor is not imported here to avoid circular imports
# It will be registered when imported elsewhere in the application # It will be registered when imported elsewhere in the application
@@ -17,5 +16,4 @@ from .github_v1_callback_processor import GithubV1CallbackProcessor
__all__ = [ __all__ = [
'EventCallbackProcessor', 'EventCallbackProcessor',
'LoggingCallbackProcessor', 'LoggingCallbackProcessor',
'GithubV1CallbackProcessor',
] ]

View File

@@ -90,7 +90,7 @@ class SQLEventCallbackService(EventCallbackService):
self.db_session.add(stored_callback) self.db_session.add(stored_callback)
await self.db_session.commit() await self.db_session.commit()
await self.db_session.refresh(stored_callback) await self.db_session.refresh(stored_callback)
return EventCallback(**row2dict(stored_callback)) return EventCallback.model_validate(row2dict(stored_callback))
async def get_event_callback(self, id: UUID) -> EventCallback | None: async def get_event_callback(self, id: UUID) -> EventCallback | None:
"""Get a single event callback, returning None if not found.""" """Get a single event callback, returning None if not found."""
@@ -98,7 +98,7 @@ class SQLEventCallbackService(EventCallbackService):
result = await self.db_session.execute(stmt) result = await self.db_session.execute(stmt)
stored_callback = result.scalar_one_or_none() stored_callback = result.scalar_one_or_none()
if stored_callback: if stored_callback:
return EventCallback(**row2dict(stored_callback)) return EventCallback.model_validate(row2dict(stored_callback))
return None return None
async def delete_event_callback(self, id: UUID) -> bool: async def delete_event_callback(self, id: UUID) -> bool:
@@ -173,7 +173,9 @@ class SQLEventCallbackService(EventCallbackService):
next_page_id = str(offset + limit) next_page_id = str(offset + limit)
# Convert stored callbacks to domain models # Convert stored callbacks to domain models
callbacks = [EventCallback(**row2dict(cb)) for cb in stored_callbacks] callbacks = [
EventCallback.model_validate(row2dict(cb)) for cb in stored_callbacks
]
return EventCallbackPage(items=callbacks, next_page_id=next_page_id) return EventCallbackPage(items=callbacks, next_page_id=next_page_id)
async def save_event_callback(self, event_callback: EventCallback) -> EventCallback: async def save_event_callback(self, event_callback: EventCallback) -> EventCallback:
@@ -202,7 +204,9 @@ class SQLEventCallbackService(EventCallbackService):
result = await self.db_session.execute(query) result = await self.db_session.execute(query)
stored_callbacks = result.scalars().all() stored_callbacks = result.scalars().all()
if stored_callbacks: if stored_callbacks:
callbacks = [EventCallback(**row2dict(cb)) for cb in stored_callbacks] callbacks = [
EventCallback.model_validate(row2dict(cb)) for cb in stored_callbacks
]
await asyncio.gather( await asyncio.gather(
*[ *[
self.execute_callback(conversation_id, callback, event) self.execute_callback(conversation_id, callback, event)

View File

@@ -18,15 +18,6 @@ if TYPE_CHECKING:
) )
def get_conversation_url() -> str:
from openhands.app_server.config import get_global_config
web_url = get_global_config().web_url
conversation_prefix = 'conversations/{}'
conversation_url = f'{web_url}/{conversation_prefix}'
return conversation_url
def ensure_conversation_found( def ensure_conversation_found(
app_conversation_info: AppConversationInfo | None, conversation_id: UUID app_conversation_info: AppConversationInfo | None, conversation_id: UUID
) -> AppConversationInfo: ) -> AppConversationInfo:
@@ -68,14 +59,3 @@ def get_agent_server_url_from_sandbox(sandbox: SandboxInfo) -> str:
) from None ) from None
return replace_localhost_hostname_for_docker(agent_server_url) return replace_localhost_hostname_for_docker(agent_server_url)
def get_prompt_template(template_name: str) -> str:
from jinja2 import Environment, FileSystemLoader
jinja_env = Environment(
loader=FileSystemLoader('openhands/integrations/templates/resolver/')
)
summary_instruction_template = jinja_env.get_template(template_name)
summary_instruction = summary_instruction_template.render()
return summary_instruction

View File

@@ -790,7 +790,7 @@ class RemoteSandboxServiceInjector(SandboxServiceInjector):
# This is primarily used for local development rather than production # This is primarily used for local development rather than production
config = get_global_config() config = get_global_config()
web_url = config.web_url web_url = config.web_url
if web_url is None: if web_url is None or 'localhost' in web_url:
global polling_task global polling_task
if polling_task is None: if polling_task is None:
polling_task = asyncio.create_task( polling_task = asyncio.create_task(

View File

@@ -0,0 +1,41 @@
from unittest.mock import MagicMock
from uuid import UUID, uuid4
import pytest
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartRequest,
)
from openhands.app_server.event_callback.event_callback_models import (
EventCallback,
EventCallbackProcessor,
)
from openhands.app_server.event_callback.event_callback_result_models import (
EventCallbackResult,
EventCallbackResultStatus,
)
from openhands.sdk import Event
@pytest.mark.asyncio
async def test_app_conversation_start_request_polymorphism():
class MyCallbackProcessor(EventCallbackProcessor):
async def __call__(
self,
conversation_id: UUID,
callback: EventCallback,
event: Event,
) -> EventCallbackResult | None:
return EventCallbackResult(
status=EventCallbackResultStatus.SUCCESS,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail='Live long and prosper!',
)
req = AppConversationStartRequest(processors=[MyCallbackProcessor()])
assert len(req.processors) == 1
processor = req.processors[0]
result = await processor(uuid4(), MagicMock(id=uuid4()), MagicMock(id=str(uuid4())))
assert result.detail == 'Live long and prosper!'