mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
feat: Allow attaching/changing repository for existing conversations (#12671)
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com> Co-authored-by: hieptl <hieptl.developer@gmail.com>
This commit is contained in:
@@ -170,7 +170,15 @@ class AppConversationStartRequest(OpenHandsModel):
|
||||
|
||||
|
||||
class AppConversationUpdateRequest(BaseModel):
|
||||
public: bool
|
||||
"""Request model for updating conversation metadata.
|
||||
|
||||
All fields are optional - only provided fields will be updated.
|
||||
"""
|
||||
|
||||
public: bool | None = None
|
||||
selected_repository: str | None = None
|
||||
selected_branch: str | None = None
|
||||
git_provider: ProviderType | None = None
|
||||
|
||||
|
||||
class AppConversationStartTaskStatus(Enum):
|
||||
|
||||
@@ -1283,20 +1283,97 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
f'Successfully updated agent-server conversation {conversation_id} title to "{new_title}"'
|
||||
)
|
||||
|
||||
def _validate_repository_update(
|
||||
self,
|
||||
request: AppConversationUpdateRequest,
|
||||
existing_branch: str | None = None,
|
||||
) -> None:
|
||||
"""Validate repository-related fields in the update request.
|
||||
|
||||
Args:
|
||||
request: The update request containing fields to validate
|
||||
existing_branch: The conversation's current branch (if any)
|
||||
|
||||
Raises:
|
||||
ValueError: If validation fails
|
||||
"""
|
||||
# Check if repository is being set
|
||||
if 'selected_repository' in request.model_fields_set:
|
||||
repo = request.selected_repository
|
||||
if repo is not None:
|
||||
# Validate repository format (owner/repo)
|
||||
if '/' not in repo or repo.count('/') != 1:
|
||||
raise ValueError(
|
||||
f"Invalid repository format: '{repo}'. Expected 'owner/repo'."
|
||||
)
|
||||
|
||||
# Sanitize: check for dangerous characters
|
||||
if any(c in repo for c in [';', '&', '|', '$', '`', '\n', '\r']):
|
||||
raise ValueError(f"Invalid characters in repository name: '{repo}'")
|
||||
|
||||
# If setting a repository, branch should also be provided
|
||||
# (either in this request or already exists in conversation)
|
||||
if (
|
||||
'selected_branch' not in request.model_fields_set
|
||||
and existing_branch is None
|
||||
):
|
||||
_logger.warning(
|
||||
f'Repository {repo} set without branch in the same request '
|
||||
'and no existing branch in conversation'
|
||||
)
|
||||
else:
|
||||
# Repository is being removed (set to null)
|
||||
# Enforce consistency: branch and provider must also be cleared
|
||||
if 'selected_branch' in request.model_fields_set:
|
||||
if request.selected_branch is not None:
|
||||
raise ValueError(
|
||||
'When removing repository, branch must also be cleared'
|
||||
)
|
||||
if 'git_provider' in request.model_fields_set:
|
||||
if request.git_provider is not None:
|
||||
raise ValueError(
|
||||
'When removing repository, git_provider must also be cleared'
|
||||
)
|
||||
|
||||
# Validate branch if provided
|
||||
if 'selected_branch' in request.model_fields_set:
|
||||
branch = request.selected_branch
|
||||
if branch is not None:
|
||||
# Sanitize: check for dangerous characters
|
||||
if any(c in branch for c in [';', '&', '|', '$', '`', '\n', '\r', ' ']):
|
||||
raise ValueError(f"Invalid characters in branch name: '{branch}'")
|
||||
|
||||
async def update_app_conversation(
|
||||
self, conversation_id: UUID, request: AppConversationUpdateRequest
|
||||
) -> AppConversation | None:
|
||||
"""Update an app conversation and return it. Return None if the conversation
|
||||
did not exist.
|
||||
"""Update an app conversation and return it.
|
||||
|
||||
Return None if the conversation did not exist.
|
||||
|
||||
Only fields that are explicitly set in the request will be updated.
|
||||
This allows partial updates where only specific fields are modified.
|
||||
Fields can be set to None to clear them (e.g., removing a repository).
|
||||
|
||||
Raises:
|
||||
ValueError: If repository/branch validation fails
|
||||
"""
|
||||
info = await self.app_conversation_info_service.get_app_conversation_info(
|
||||
conversation_id
|
||||
)
|
||||
if info is None:
|
||||
return None
|
||||
for field_name in AppConversationUpdateRequest.model_fields:
|
||||
|
||||
# Validate repository-related fields before updating
|
||||
# Pass existing branch to avoid false warnings when only updating repository
|
||||
self._validate_repository_update(request, existing_branch=info.selected_branch)
|
||||
|
||||
# Only update fields that were explicitly provided in the request
|
||||
# This uses Pydantic's model_fields_set to detect which fields were set,
|
||||
# allowing us to distinguish between "not provided" and "explicitly set to None"
|
||||
for field_name in request.model_fields_set:
|
||||
value = getattr(request, field_name)
|
||||
setattr(info, field_name, value)
|
||||
|
||||
info = await self.app_conversation_info_service.save_app_conversation_info(info)
|
||||
conversations = await self._build_app_conversations([info])
|
||||
return conversations[0]
|
||||
|
||||
Reference in New Issue
Block a user