mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
fix-github
...
fix-conver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebcf6a1daa | ||
|
|
e4f48ffaec |
@@ -77,6 +77,7 @@ export interface Conversation {
|
||||
last_updated_at: string;
|
||||
created_at: string;
|
||||
status: ProjectStatus;
|
||||
needs_title_update?: boolean;
|
||||
}
|
||||
|
||||
export interface ResultSet<T> {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user