From 0491357fefef12ba1fbd53f7fe380164430df033 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Thu, 17 Apr 2025 15:09:28 -0400 Subject: [PATCH] [Refactor]: Collapse initial user message for cloud resolver (#7871) Co-authored-by: openhands --- .../components/file-operations.test.tsx | 11 +++++- frontend/src/api/open-hands.types.ts | 3 ++ .../features/chat/expandable-message.tsx | 1 + .../src/components/features/chat/messages.tsx | 35 +++++++++++++++++-- frontend/src/i18n/declaration.ts | 1 + frontend/src/i18n/translation.json | 15 ++++++++ .../github/templates/issue_comment_prompt.j2 | 20 +++++++++++ .../github/templates/issue_labeled_prompt.j2 | 12 +++++++ .../github/templates/pr_update_prompt.j2 | 25 +++++++++++++ .../runtime/impl/remote/remote_runtime.py | 1 + .../server/data_models/conversation_info.py | 2 ++ .../server/routes/manage_conversations.py | 7 ++-- .../data_models/conversation_metadata.py | 10 +++++- 13 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 openhands/integrations/github/templates/issue_comment_prompt.j2 create mode 100644 openhands/integrations/github/templates/issue_labeled_prompt.j2 create mode 100644 openhands/integrations/github/templates/pr_update_prompt.j2 diff --git a/frontend/__tests__/components/file-operations.test.tsx b/frontend/__tests__/components/file-operations.test.tsx index c0c3543933..322e0f6266 100644 --- a/frontend/__tests__/components/file-operations.test.tsx +++ b/frontend/__tests__/components/file-operations.test.tsx @@ -1,9 +1,18 @@ import { render, screen } from "@testing-library/react"; -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { Messages } from "#/components/features/chat/messages"; import type { Message } from "#/message"; import { renderWithProviders } from "test-utils"; +// Mock the useParams hook to provide a conversationId +vi.mock("react-router", async () => { + const actual = await vi.importActual("react-router"); + return { + ...actual, + useParams: () => ({ conversationId: "test-conversation-id" }), + }; +}); + describe("File Operations Messages", () => { it("should show success indicator for successful file read operation", () => { const messages: Message[] = [ diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts index c175c8c42d..2fa41da1b4 100644 --- a/frontend/src/api/open-hands.types.ts +++ b/frontend/src/api/open-hands.types.ts @@ -70,6 +70,8 @@ export interface AuthenticateResponse { error?: string; } +export type ConversationTrigger = "resolver" | "gui"; + export interface Conversation { conversation_id: string; title: string; @@ -77,6 +79,7 @@ export interface Conversation { last_updated_at: string; created_at: string; status: ProjectStatus; + trigger?: ConversationTrigger; } export interface ResultSet { diff --git a/frontend/src/components/features/chat/expandable-message.tsx b/frontend/src/components/features/chat/expandable-message.tsx index ac7e00c1a5..5fe974b073 100644 --- a/frontend/src/components/features/chat/expandable-message.tsx +++ b/frontend/src/components/features/chat/expandable-message.tsx @@ -53,6 +53,7 @@ export function ExpandableMessage({ }); useEffect(() => { + // If we have a translation ID, process it if (id && i18n.exists(id)) { let processedObservation = observation; let processedAction = action; diff --git a/frontend/src/components/features/chat/messages.tsx b/frontend/src/components/features/chat/messages.tsx index 4556985ad4..f98270650c 100644 --- a/frontend/src/components/features/chat/messages.tsx +++ b/frontend/src/components/features/chat/messages.tsx @@ -4,6 +4,9 @@ import { ChatMessage } from "#/components/features/chat/chat-message"; import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons"; import { ImageCarousel } from "../images/image-carousel"; import { ExpandableMessage } from "./expandable-message"; +import { useUserConversation } from "#/hooks/query/use-user-conversation"; +import { useConversation } from "#/context/conversation-context"; +import { I18nKey } from "#/i18n/declaration"; interface MessagesProps { messages: Message[]; @@ -11,13 +14,38 @@ interface MessagesProps { } export const Messages: React.FC = React.memo( - ({ messages, isAwaitingUserConfirmation }) => - messages.map((message, index) => { + ({ messages, isAwaitingUserConfirmation }) => { + const { conversationId } = useConversation(); + const { data: conversation } = useUserConversation(conversationId || null); + + // Check if conversation metadata has trigger=resolver + const isResolverTrigger = conversation?.trigger === "resolver"; + + return messages.map((message, index) => { const shouldShowConfirmationButtons = messages.length - 1 === index && message.sender === "assistant" && isAwaitingUserConfirmation; + const isFirstUserMessageWithResolverTrigger = + index === 0 && message.sender === "user" && isResolverTrigger; + + // Special case: First user message with resolver trigger + if (isFirstUserMessageWithResolverTrigger) { + return ( +
+ + {message.imageUrls && message.imageUrls.length > 0 && ( + + )} +
+ ); + } + if (message.type === "error" || message.type === "action") { return (
@@ -46,7 +74,8 @@ export const Messages: React.FC = React.memo( {shouldShowConfirmationButtons && } ); - }), + }); + }, ); Messages.displayName = "Messages"; diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index 3da66a63b0..35a2c8cdfb 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -1,5 +1,6 @@ // this file generate by script, don't modify it manually!!! export enum I18nKey { + CHAT$RESOLVER_INSTRUCTIONS = "CHAT$RESOLVER_INSTRUCTIONS", SETTINGS$ADVANCED = "SETTINGS$ADVANCED", SETTINGS$BASE_URL = "SETTINGS$BASE_URL", SETTINGS$AGENT = "SETTINGS$AGENT", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 020040abb7..6a016e333f 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -1,4 +1,19 @@ { + "CHAT$RESOLVER_INSTRUCTIONS": { + "en": "Resolver Instructions", + "ja": "リゾルバの指示", + "zh-CN": "解析器指令", + "zh-TW": "解析器指令", + "ko-KR": "리졸버 지침", + "no": "Resolver-instruksjoner", + "it": "Istruzioni del resolver", + "pt": "Instruções do resolvedor", + "es": "Instrucciones del resolvedor", + "ar": "تعليمات المحلل", + "fr": "Instructions du résolveur", + "tr": "Çözümleyici Talimatları", + "de": "Resolver-Anweisungen" + }, "SETTINGS$ADVANCED": { "en": "Advanced", "ja": "詳細設定", diff --git a/openhands/integrations/github/templates/issue_comment_prompt.j2 b/openhands/integrations/github/templates/issue_comment_prompt.j2 new file mode 100644 index 0000000000..b7cda1d31f --- /dev/null +++ b/openhands/integrations/github/templates/issue_comment_prompt.j2 @@ -0,0 +1,20 @@ +You are requested to fix issue number #{{ issue_number }} in a repository. + +A comment on the issue has been addressed to you. + +# Comment +{{ issue_comment }} + + +# Steps to Handle the Comment + +1. Address the comment. Use the GitHub API to read issue title, body, and comments if you need more context +2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed +3. Run the tests, and if they pass you are done! +4. You do NOT need to write new tests if there are only changes to documentation or configuration files. + +When you're done, make sure to + +1. Use the `GITHUB_TOKEN` environment variable and GitHub API to open a new PR +2. The PR description should mention that it "fixes" or "closes" the issue number +3. Make sure to leave the following sentence at the end of the PR description: `@{{ username }} can click here to [continue refining the PR]({{ conversation_url }})` \ No newline at end of file diff --git a/openhands/integrations/github/templates/issue_labeled_prompt.j2 b/openhands/integrations/github/templates/issue_labeled_prompt.j2 new file mode 100644 index 0000000000..d9fc4ef1d4 --- /dev/null +++ b/openhands/integrations/github/templates/issue_labeled_prompt.j2 @@ -0,0 +1,12 @@ +Please fix a issue number #{{ issue_number }} in your repository. Do the following + +1. Read the issue body and comments using the Github API +2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed +3. Run the tests, and if they pass you are done! +4. You do NOT need to write new tests if there are only changes to documentation or configuration files. + +When you're done, make sure to + +1. Use the `GITHUB_TOKEN` environment variable and GitHub API to open a new PR +2. The PR description should mention that it "fixes" or "closes" the issue number +3. Make sure to leave the following sentence at the end of the PR description: `@{{ username }} can click here to [continue refining the PR]({{ conversation_url }})` \ No newline at end of file diff --git a/openhands/integrations/github/templates/pr_update_prompt.j2 b/openhands/integrations/github/templates/pr_update_prompt.j2 new file mode 100644 index 0000000000..5401857be9 --- /dev/null +++ b/openhands/integrations/github/templates/pr_update_prompt.j2 @@ -0,0 +1,25 @@ +You are checked out to branch {{ branch_name }}, which has an open PR #{{ pr_number }}. +A comment on the PR (below) has been addressed to you. Do NOT respond to this comment via the GitHub API. + +{% if file_location %} The comment is on the file `{{ file_location }}`{% endif %}. + +# Comment +{{ pr_comment }} + + +# Steps to Handle the Comment + +## Understand the PR Context +Use the GitHub API to: + 1. Retrieve the diff against main to understand the changes + 2. Fetch the PR body and the linked issue for context + +## Process the Comment +If it's a question: + 1. Answer the question asked + 2. DO NOT leave any comments on the PR + +If it requests a code update: + 1. Modify the code accordingly in the current branch + 2. Push the changes to update the PR + 3. DO NOT leave any comments on the PR \ No newline at end of file diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index 6f7aa37210..610351c7b4 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -474,3 +474,4 @@ class RemoteRuntime(ActionExecutionClient): def _stop_if_closed(self, retry_state: tenacity.RetryCallState) -> bool: return self._runtime_closed + \ No newline at end of file diff --git a/openhands/server/data_models/conversation_info.py b/openhands/server/data_models/conversation_info.py index c6f9523c62..1fe5262672 100644 --- a/openhands/server/data_models/conversation_info.py +++ b/openhands/server/data_models/conversation_info.py @@ -1,6 +1,7 @@ from dataclasses import dataclass, field from datetime import datetime, timezone +from openhands.storage.data_models.conversation_metadata import ConversationTrigger from openhands.storage.data_models.conversation_status import ConversationStatus @@ -16,4 +17,5 @@ class ConversationInfo: last_updated_at: datetime | None = None status: ConversationStatus = ConversationStatus.STOPPED selected_repository: str | None = None + trigger: ConversationTrigger | None = None created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) diff --git a/openhands/server/routes/manage_conversations.py b/openhands/server/routes/manage_conversations.py index 087719e07c..0d1bb84fbc 100644 --- a/openhands/server/routes/manage_conversations.py +++ b/openhands/server/routes/manage_conversations.py @@ -33,7 +33,7 @@ from openhands.server.shared import ( file_store, ) from openhands.server.types import LLMAuthenticationError, MissingSettingsError -from openhands.storage.data_models.conversation_metadata import ConversationMetadata +from openhands.storage.data_models.conversation_metadata import ConversationMetadata, ConversationTrigger from openhands.storage.data_models.conversation_status import ConversationStatus from openhands.utils.async_utils import wait_all from openhands.utils.conversation_summary import generate_conversation_title @@ -57,7 +57,8 @@ async def _create_new_conversation( initial_user_msg: str | None, image_urls: list[str] | None, replay_json: str | None, - attach_convo_id: bool = False, + conversation_trigger: ConversationTrigger = ConversationTrigger.GUI, + attach_convo_id: bool = False ): logger.info( 'Creating conversation', @@ -108,6 +109,7 @@ async def _create_new_conversation( logger.info(f'Saving metadata for conversation {conversation_id}') await conversation_store.save_metadata( ConversationMetadata( + trigger=conversation_trigger, conversation_id=conversation_id, title=conversation_title, user_id=user_id, @@ -388,6 +390,7 @@ async def _get_conversation_info( if not title: title = get_default_conversation_title(conversation.conversation_id) return ConversationInfo( + trigger=conversation.trigger, conversation_id=conversation.conversation_id, title=title, last_updated_at=conversation.last_updated_at, diff --git a/openhands/storage/data_models/conversation_metadata.py b/openhands/storage/data_models/conversation_metadata.py index c7e4cd4508..c47fdb8631 100644 --- a/openhands/storage/data_models/conversation_metadata.py +++ b/openhands/storage/data_models/conversation_metadata.py @@ -1,9 +1,16 @@ from dataclasses import dataclass, field from datetime import datetime, timezone +from enum import Enum +from openhands.integrations.service_types import ProviderType + + +class ConversationTrigger(Enum): + RESOLVER = 'resolver' + GUI = 'gui' @dataclass -class ConversationMetadata: +class ConversationMetadata: conversation_id: str github_user_id: str | None selected_repository: str | None @@ -11,6 +18,7 @@ class ConversationMetadata: selected_branch: str | None = None title: str | None = None last_updated_at: datetime | None = None + trigger: ConversationTrigger | None = None created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) # Cost and token metrics accumulated_cost: float = 0.0