fix: wire suggested task prompts for V1 (#12787)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Engel Nyst
2026-02-16 23:57:32 +01:00
committed by GitHub
parent a9afafa991
commit b06b9eedac
7 changed files with 207 additions and 6 deletions

View File

@@ -0,0 +1,104 @@
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, expect, it, vi } from "vitest";
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { SuggestedTask } from "#/utils/types";
vi.mock("#/hooks/query/use-settings", async () => {
const actual = await vi.importActual<typeof import("#/hooks/query/use-settings")>(
"#/hooks/query/use-settings",
);
return {
...actual,
useSettings: vi.fn().mockReturnValue({
data: {
v1_enabled: true,
},
isLoading: false,
}),
};
});
vi.mock("#/hooks/use-tracking", () => ({
useTracking: () => ({
trackConversationCreated: vi.fn(),
}),
}));
describe("useCreateConversation", () => {
it("passes suggested tasks to the V1 create conversation API", async () => {
const createConversationSpy = vi
.spyOn(V1ConversationService, "createConversation")
.mockResolvedValue({
id: "task-id",
created_by_user_id: null,
status: "READY",
detail: null,
app_conversation_id: null,
sandbox_id: null,
agent_server_url: "http://agent-server.local",
request: {
sandbox_id: null,
initial_message: {
role: "user",
content: [{ type: "text", text: "Please address the comments" }],
},
processors: [],
llm_model: null,
selected_repository: null,
selected_branch: null,
git_provider: "github",
suggested_task: null,
title: null,
trigger: null,
pr_number: [],
parent_conversation_id: null,
agent_type: "default",
},
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
const { result } = renderHook(() => useCreateConversation(), {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
const suggestedTask: SuggestedTask = {
git_provider: "github",
issue_number: 42,
repo: "owner/repo",
title: "Resolve comments",
task_type: "UNRESOLVED_COMMENTS",
};
await result.current.mutateAsync({
query: "Please address the comments",
repository: {
name: "owner/repo",
gitProvider: "github",
branch: "main",
},
conversationInstructions: "Focus on review comments",
suggestedTask,
});
await waitFor(() => {
expect(createConversationSpy).toHaveBeenCalledWith(
"owner/repo",
"github",
"Please address the comments",
"main",
"Focus on review comments",
suggestedTask,
undefined,
undefined,
undefined,
);
});
});
});

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import { openHands } from "../open-hands-axios";
import { ConversationTrigger, GetVSCodeUrlResponse } from "../open-hands.types";
import { Provider } from "#/types/settings";
import { SuggestedTask } from "#/utils/types";
import { buildHttpBaseUrl } from "#/utils/websocket-url";
import { buildSessionHeaders } from "#/utils/utils";
import type {
@@ -61,6 +62,7 @@ class V1ConversationService {
initialUserMsg?: string,
selected_branch?: string,
conversationInstructions?: string,
suggestedTask?: SuggestedTask,
trigger?: ConversationTrigger,
parent_conversation_id?: string,
agent_type?: "default" | "plan",
@@ -69,14 +71,15 @@ class V1ConversationService {
selected_repository: selectedRepository,
git_provider,
selected_branch,
suggested_task: suggestedTask,
title: conversationInstructions,
trigger,
parent_conversation_id: parent_conversation_id || null,
agent_type,
};
// Add initial message if provided
if (initialUserMsg) {
// suggested_task implies the backend will construct the initial_message
if (!suggestedTask && initialUserMsg) {
body.initial_message = {
role: "user",
content: [

View File

@@ -1,6 +1,7 @@
import { ConversationTrigger } from "../open-hands.types";
import { Provider } from "#/types/settings";
import { V1SandboxStatus } from "../sandbox-service/sandbox-service.types";
import { Provider } from "#/types/settings";
import { SuggestedTask } from "#/utils/types";
// V1 Metrics Types
export interface V1TokenUsage {
@@ -47,6 +48,7 @@ export interface V1AppConversationStartRequest {
selected_repository?: string | null;
selected_branch?: string | null;
git_provider?: Provider | null;
suggested_task?: SuggestedTask | null;
title?: string | null;
trigger?: ConversationTrigger | null;
pr_number?: number[];

View File

@@ -61,7 +61,8 @@ export const useCreateConversation = () => {
query,
repository?.branch,
conversationInstructions,
undefined, // trigger - will be set by backend
suggestedTask,
undefined, // trigger - set by backend when applicable
parentConversationId,
agentType,
);

View File

@@ -11,7 +11,7 @@ from openhands.app_server.event_callback.event_callback_models import (
EventCallbackProcessor,
)
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
from openhands.integrations.service_types import ProviderType
from openhands.integrations.service_types import ProviderType, SuggestedTask
from openhands.sdk.conversation.state import ConversationExecutionStatus
from openhands.sdk.llm import MetricsSnapshot
from openhands.sdk.plugin import PluginSource
@@ -150,6 +150,7 @@ class AppConversationStartRequest(OpenHandsModel):
selected_repository: str | None = None
selected_branch: str | None = None
git_provider: ProviderType | None = None
suggested_task: SuggestedTask | None = None
title: str | None = None
trigger: ConversationTrigger | None = None
pr_number: list[int] = Field(default_factory=list)

View File

@@ -18,6 +18,7 @@ from openhands.agent_server.models import (
ConversationInfo,
SendMessageRequest,
StartConversationRequest,
TextContent,
)
from openhands.app_server.app_conversation.app_conversation_info_service import (
AppConversationInfoService,
@@ -78,6 +79,7 @@ from openhands.app_server.utils.llm_metadata import (
)
from openhands.experiments.experiment_manager import ExperimentManagerImpl
from openhands.integrations.provider import ProviderType
from openhands.integrations.service_types import SuggestedTask
from openhands.sdk import Agent, AgentContext, LocalWorkspace
from openhands.sdk.llm import LLM
from openhands.sdk.plugin import PluginSource
@@ -85,6 +87,7 @@ from openhands.sdk.secret import LookupSecret, SecretValue, StaticSecret
from openhands.sdk.utils.paging import page_iterator
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
from openhands.server.types import AppMode
from openhands.storage.data_models.conversation_metadata import ConversationTrigger
from openhands.tools.preset.default import (
get_default_tools,
)
@@ -209,6 +212,8 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
)
self._inherit_configuration_from_parent(request, parent_info)
self._apply_suggested_task(request)
task = AppConversationStartTask(
created_by_user_id=user_id,
request=request,
@@ -569,6 +574,33 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
if not request.llm_model and parent_info.llm_model:
request.llm_model = parent_info.llm_model
def _apply_suggested_task(self, request: AppConversationStartRequest) -> None:
"""Apply suggested task defaults to the start request."""
suggested_task: SuggestedTask | None = request.suggested_task
if not suggested_task:
return
if request.initial_message is not None:
raise ValueError(
'initial_message cannot be provided when suggested_task is present'
)
prompt = suggested_task.get_prompt_for_task()
if not prompt:
raise ValueError(
f'Suggested task returned empty prompt for task type {suggested_task.task_type}'
)
request.initial_message = SendMessageRequest(
role='user',
content=[TextContent(text=prompt)],
)
request.trigger = ConversationTrigger.SUGGESTED_TASK
if not request.selected_repository:
request.selected_repository = suggested_task.repo
if not request.git_provider:
request.git_provider = suggested_task.git_provider
def _compute_plan_path(
self,
working_dir: str,

View File

@@ -14,6 +14,7 @@ from pydantic import SecretStr
from openhands.agent_server.models import (
SendMessageRequest,
StartConversationRequest,
TextContent,
)
from openhands.app_server.app_conversation.app_conversation_models import (
AgentType,
@@ -32,12 +33,14 @@ from openhands.app_server.sandbox.sandbox_models import (
from openhands.app_server.sandbox.sandbox_spec_models import SandboxSpecInfo
from openhands.app_server.user.user_context import UserContext
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.integrations.service_types import SuggestedTask, TaskType
from openhands.sdk import Agent, Event
from openhands.sdk.llm import LLM
from openhands.sdk.secret import LookupSecret, StaticSecret
from openhands.sdk.workspace import LocalWorkspace
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
from openhands.server.types import AppMode
from openhands.storage.data_models.conversation_metadata import ConversationTrigger
# Env var used by openhands SDK LLM to skip context-window validation (e.g. for gpt-4 in tests)
_ALLOW_SHORT_CONTEXT_WINDOWS = 'ALLOW_SHORT_CONTEXT_WINDOWS'
@@ -112,7 +115,62 @@ class TestLiveStatusAppConversationService:
self.mock_sandbox.id = uuid4()
self.mock_sandbox.status = SandboxStatus.RUNNING
@pytest.mark.asyncio
def test_apply_suggested_task_sets_prompt_and_trigger(self):
"""Test suggested task prompts populate initial message and trigger."""
suggested_task = SuggestedTask(
git_provider=ProviderType.GITHUB,
task_type=TaskType.UNRESOLVED_COMMENTS,
repo='owner/repo',
issue_number=42,
title='Handle review comments',
)
request = AppConversationStartRequest(suggested_task=suggested_task)
self.service._apply_suggested_task(request)
assert request.initial_message is not None
assert (
request.initial_message.content[0].text
== suggested_task.get_prompt_for_task()
)
assert request.trigger == ConversationTrigger.SUGGESTED_TASK
assert request.selected_repository == suggested_task.repo
assert request.git_provider == suggested_task.git_provider
def test_apply_suggested_task_raises_if_initial_message_present(self):
suggested_task = SuggestedTask(
repo='foo/bar',
git_provider=ProviderType.GITHUB,
title='Some title',
task_type=TaskType.OPEN_ISSUE,
issue_number=123,
)
request = AppConversationStartRequest(
suggested_task=suggested_task,
initial_message=SendMessageRequest(
role='user',
content=[TextContent(text='User provided message')],
),
)
with pytest.raises(ValueError, match='initial_message cannot be provided'):
self.service._apply_suggested_task(request)
def test_apply_suggested_task_raises_if_prompt_empty(self):
suggested_task = SuggestedTask(
repo='foo/bar',
git_provider=ProviderType.GITHUB,
title='Some title',
task_type=TaskType.OPEN_ISSUE,
issue_number=123,
)
request = AppConversationStartRequest(suggested_task=suggested_task)
with patch.object(SuggestedTask, 'get_prompt_for_task', return_value=''):
with pytest.raises(ValueError, match='empty prompt'):
self.service._apply_suggested_task(request)
async def test_setup_secrets_for_git_providers_no_provider_tokens(self):
"""Test _setup_secrets_for_git_providers with no provider tokens."""
# Arrange