mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
fix: wire suggested task prompts for V1 (#12787)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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: [
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user