Compare commits

...

2 Commits

Author SHA1 Message Date
openhands
ebcf6a1daa Fix conversation title update to follow REST principles 2025-03-31 16:38:57 +00:00
openhands
e4f48ffaec Fix conversation title generation to follow REST principles 2025-03-31 16:28:32 +00:00
5 changed files with 71 additions and 64 deletions

View File

@@ -77,6 +77,7 @@ export interface Conversation {
last_updated_at: string;
created_at: string;
status: ProjectStatus;
needs_title_update?: boolean;
}
export interface ResultSet<T> {

View File

@@ -58,8 +58,14 @@ export const useSettings = () => {
// that would prepopulate the data to the cache and mess with expectations. Read more:
// https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#using-initialdata-to-prepopulate-a-query
if (query.error?.status === 404) {
// Return only the necessary properties to avoid excessive re-renders
return {
...query,
status: query.status,
error: query.error,
isLoading: query.isLoading,
isError: query.isError,
isSuccess: query.isSuccess,
refetch: query.refetch,
data: DEFAULT_SETTINGS,
};
}

View File

@@ -1,14 +1,11 @@
import { useEffect } from "react";
import { useParams } from "react-router";
import { useSelector, useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { useQueryClient } from "@tanstack/react-query";
import { useUpdateConversation } from "./mutation/use-update-conversation";
import { RootState } from "#/store";
import OpenHands from "#/api/open-hands";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
const defaultTitlePattern = /^Conversation [a-f0-9]+$/;
/**
* Hook that monitors for the first agent message and triggers title generation.
* This approach is more robust as it ensures the user message has been processed
@@ -18,7 +15,6 @@ export function useAutoTitle() {
const { conversationId } = useParams<{ conversationId: string }>();
const { data: conversation } = useUserConversation(conversationId ?? null);
const queryClient = useQueryClient();
const dispatch = useDispatch();
const { mutate: updateConversation } = useUpdateConversation();
const messages = useSelector((state: RootState) => state.chat.messages);
@@ -40,43 +36,34 @@ export function useAutoTitle() {
(message) => message.sender === "user",
);
// Check if we need to update the title
if (!hasAgentMessage || !hasUserMessage) {
return;
}
if (conversation.title && !defaultTitlePattern.test(conversation.title)) {
return;
}
updateConversation(
{
id: conversationId,
conversation: { title: "" },
},
{
onSuccess: async () => {
try {
const updatedConversation =
await OpenHands.getConversation(conversationId);
queryClient.setQueryData(
["user", "conversation", conversationId],
updatedConversation,
);
} catch (error) {
// If the conversation needs a title update or has a default title
if (conversation.needs_title_update) {
// Use the PATCH endpoint to update the title
updateConversation(
{
id: conversationId,
conversation: { title: "" },
},
{
onSuccess: () => {
// Invalidate the query to refresh the conversation data
queryClient.invalidateQueries({
queryKey: ["user", "conversation", conversationId],
});
}
},
},
},
);
);
}
}, [
messages,
conversationId,
conversation,
updateConversation,
queryClient,
dispatch,
]);
}

View File

@@ -17,3 +17,4 @@ class ConversationInfo:
status: ConversationStatus = ConversationStatus.STOPPED
selected_repository: str | None = None
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
needs_title_update: bool = False

View File

@@ -5,6 +5,7 @@ from fastapi import APIRouter, Body, Request, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from openhands.core.config.llm_config import LLMConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.message import MessageAction
from openhands.events.event import EventSource
@@ -34,6 +35,7 @@ from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
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
app = APIRouter(prefix='/api')
@@ -46,6 +48,11 @@ class InitSessionRequest(BaseModel):
replay_json: str | None = None
class ConversationUpdate(BaseModel):
title: str | None = None
selected_repository: str | None = None
async def _create_new_conversation(
user_id: str | None,
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
@@ -244,24 +251,19 @@ async def get_conversation(
metadata = await conversation_store.get_metadata(conversation_id)
is_running = await conversation_manager.is_agent_loop_running(conversation_id)
# Check if we need to update the title
# Check if we need to update the title but don't modify it in the GET request
needs_title_update = False
if is_running and metadata:
# Check if the title is a default title (contains the conversation ID)
if metadata.title and conversation_id[:5] in metadata.title:
# Generate a new title
new_title = await auto_generate_title(
conversation_id, get_user_id(request)
)
if new_title:
# Update the metadata
metadata.title = new_title
await conversation_store.save_metadata(metadata)
# Refresh metadata after update
metadata = await conversation_store.get_metadata(conversation_id)
needs_title_update = True
conversation_info = await _get_conversation_info(metadata, is_running)
# Add the needs_title_update flag to the response
if conversation_info:
conversation_info.needs_title_update = needs_title_update
return conversation_info
except FileNotFoundError:
return None
@@ -312,10 +314,6 @@ async def auto_generate_title(conversation_id: str, user_id: str | None) -> str:
if first_user_message:
# Try LLM-based title generation first
from openhands.core.config.llm_config import LLMConfig
from openhands.utils.conversation_summary import generate_conversation_title
# Get LLM config from user settings
try:
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
settings = await settings_store.load()
@@ -352,27 +350,40 @@ async def auto_generate_title(conversation_id: str, user_id: str | None) -> str:
@app.patch('/conversations/{conversation_id}')
async def update_conversation(
request: Request, conversation_id: str, title: str = Body(embed=True)
) -> bool:
user_id = get_user_id(request)
conversation_id: str,
conversation: ConversationUpdate,
request: Request,
) -> ConversationInfo | None:
conversation_store = await ConversationStoreImpl.get_instance(
config, user_id, get_github_user_id(request)
config, get_user_id(request), get_github_user_id(request)
)
metadata = await conversation_store.get_metadata(conversation_id)
if not metadata:
return False
try:
metadata = await conversation_store.get_metadata(conversation_id)
if metadata:
if conversation.title is not None:
# If title is empty string, auto-generate a title
if conversation.title == '':
new_title = await auto_generate_title(
conversation_id, get_user_id(request)
)
if new_title:
metadata.title = new_title
else:
metadata.title = get_default_conversation_title(conversation_id)
else:
metadata.title = conversation.title
# If title is empty or unspecified, auto-generate it
if not title or title.isspace():
title = await auto_generate_title(conversation_id, user_id)
if conversation.selected_repository is not None:
metadata.selected_repository = conversation.selected_repository
# If we still don't have a title, use the default
if not title or title.isspace():
title = get_default_conversation_title(conversation_id)
metadata.title = title
await conversation_store.save_metadata(metadata)
return True
await conversation_store.save_metadata(metadata)
is_running = await conversation_manager.is_agent_loop_running(conversation_id)
conversation_info = await _get_conversation_info(metadata, is_running)
if conversation_info:
conversation_info.needs_title_update = False # Reset the flag after update
return conversation_info
except FileNotFoundError:
return None
@app.delete('/conversations/{conversation_id}')
@@ -413,6 +424,7 @@ async def _get_conversation_info(
status=(
ConversationStatus.RUNNING if is_running else ConversationStatus.STOPPED
),
needs_title_update=False, # Default value, will be set by the GET endpoint if needed
)
except Exception as e:
logger.error(