From 9b0a5da839256ca32076509a98c30d9854ab55ff Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Fri, 8 Aug 2025 18:03:03 +0400 Subject: [PATCH] Use EventStore directly in remember prompt; merge client services (#10143) Co-authored-by: openhands --- .../api/file-service/file-service.api.test.ts | 10 +-- .../chat/launch-microagent-modal.test.tsx | 2 - .../src/api/file-service/file-service.api.ts | 66 ---------------- .../api/file-service/file-service.types.ts | 5 -- .../api/memory-service/memory-service.api.ts | 21 ----- frontend/src/api/open-hands.ts | 77 +++++++++++++++++-- frontend/src/api/open-hands.types.ts | 6 ++ .../src/hooks/mutation/use-upload-files.ts | 4 +- .../src/hooks/query/use-get-microagents.ts | 4 +- .../src/hooks/query/use-microagent-prompt.ts | 4 +- .../server/routes/manage_conversations.py | 31 ++++---- 11 files changed, 102 insertions(+), 128 deletions(-) delete mode 100644 frontend/src/api/file-service/file-service.api.ts delete mode 100644 frontend/src/api/file-service/file-service.types.ts delete mode 100644 frontend/src/api/memory-service/memory-service.api.ts diff --git a/frontend/__tests__/api/file-service/file-service.api.test.ts b/frontend/__tests__/api/file-service/file-service.api.test.ts index 12c0cd9a2e..05b517be18 100644 --- a/frontend/__tests__/api/file-service/file-service.api.test.ts +++ b/frontend/__tests__/api/file-service/file-service.api.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { FileService } from "#/api/file-service/file-service.api"; +import OpenHands from "#/api/open-hands"; import { FILE_VARIANTS_1, FILE_VARIANTS_2, @@ -10,20 +10,20 @@ import { * You can find the mock handlers in `frontend/src/mocks/file-service-handlers.ts`. */ -describe("FileService", () => { +describe("OpenHands File API", () => { it("should get a list of files", async () => { - await expect(FileService.getFiles("test-conversation-id")).resolves.toEqual( + await expect(OpenHands.getFiles("test-conversation-id")).resolves.toEqual( FILE_VARIANTS_1, ); await expect( - FileService.getFiles("test-conversation-id-2"), + OpenHands.getFiles("test-conversation-id-2"), ).resolves.toEqual(FILE_VARIANTS_2); }); it("should get content of a file", async () => { await expect( - FileService.getFile("test-conversation-id", "file1.txt"), + OpenHands.getFile("test-conversation-id", "file1.txt"), ).resolves.toEqual("Content of file1.txt"); }); }); diff --git a/frontend/__tests__/components/features/chat/launch-microagent-modal.test.tsx b/frontend/__tests__/components/features/chat/launch-microagent-modal.test.tsx index 41238fdb6e..6593475987 100644 --- a/frontend/__tests__/components/features/chat/launch-microagent-modal.test.tsx +++ b/frontend/__tests__/components/features/chat/launch-microagent-modal.test.tsx @@ -3,8 +3,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { LaunchMicroagentModal } from "#/components/features/chat/microagent/launch-microagent-modal"; -import { MemoryService } from "#/api/memory-service/memory-service.api"; -import { FileService } from "#/api/file-service/file-service.api"; import { I18nKey } from "#/i18n/declaration"; vi.mock("react-router", async () => ({ diff --git a/frontend/src/api/file-service/file-service.api.ts b/frontend/src/api/file-service/file-service.api.ts deleted file mode 100644 index 12f83ddcd0..0000000000 --- a/frontend/src/api/file-service/file-service.api.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { openHands } from "../open-hands-axios"; -import { GetFilesResponse, GetFileResponse } from "./file-service.types"; -import { getConversationUrl } from "../conversation.utils"; -import { FileUploadSuccessResponse } from "../open-hands.types"; - -export class FileService { - /** - * Retrieve the list of files available in the workspace - * @param conversationId ID of the conversation - * @param path Path to list files from. If provided, it lists all the files in the given path - * @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace - */ - static async getFiles( - conversationId: string, - path?: string, - ): Promise { - const url = `${getConversationUrl(conversationId)}/list-files`; - const { data } = await openHands.get(url, { - params: { path }, - }); - - return data; - } - - /** - * Retrieve the content of a file - * @param conversationId ID of the conversation - * @param path Full path of the file to retrieve - * @returns Code content of the file - */ - static async getFile(conversationId: string, path: string): Promise { - const url = `${getConversationUrl(conversationId)}/select-file`; - const { data } = await openHands.get(url, { - params: { file: path }, - }); - - return data.code; - } - - /** - * Upload multiple files to the workspace - * @param conversationId ID of the conversation - * @param files List of files. - * @returns list of uploaded files, list of skipped files - */ - static async uploadFiles( - conversationId: string, - files: File[], - ): Promise { - const formData = new FormData(); - for (const file of files) { - formData.append("files", file); - } - const url = `${getConversationUrl(conversationId)}/upload-files`; - const response = await openHands.post( - url, - formData, - { - headers: { - "Content-Type": "multipart/form-data", - }, - }, - ); - return response.data; - } -} diff --git a/frontend/src/api/file-service/file-service.types.ts b/frontend/src/api/file-service/file-service.types.ts deleted file mode 100644 index 1ccbe46faa..0000000000 --- a/frontend/src/api/file-service/file-service.types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type GetFilesResponse = string[]; - -export interface GetFileResponse { - code: string; -} diff --git a/frontend/src/api/memory-service/memory-service.api.ts b/frontend/src/api/memory-service/memory-service.api.ts deleted file mode 100644 index 0818894c58..0000000000 --- a/frontend/src/api/memory-service/memory-service.api.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { openHands } from "../open-hands-axios"; - -interface GetPromptResponse { - status: string; - prompt: string; -} - -export class MemoryService { - static async getPrompt( - conversationId: string, - eventId: number, - ): Promise { - const { data } = await openHands.get( - `/api/conversations/${conversationId}/remember_prompt`, - { - params: { event_id: eventId }, - }, - ); - return data.prompt; - } -} diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index f2ce45ff8f..92e5120838 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -15,6 +15,9 @@ import { GetMicroagentPromptResponse, CreateMicroagent, MicroagentContentResponse, + FileUploadSuccessResponse, + GetFilesResponse, + GetFileResponse, } from "./open-hands.types"; import { openHands } from "./open-hands-axios"; import { ApiSettings, PostApiSettings, Provider } from "#/types/settings"; @@ -618,12 +621,11 @@ class OpenHands { conversationId: string, eventId: number, ): Promise { - const { data } = await openHands.get( - `/api/conversations/${conversationId}/remember_prompt`, - { - params: { event_id: eventId }, - }, - ); + const url = `${this.getConversationUrl(conversationId)}/remember-prompt`; + const { data } = await openHands.get(url, { + params: { event_id: eventId }, + headers: this.getConversationHeaders(), + }); return data.prompt; } @@ -640,6 +642,69 @@ class OpenHands { return data; } + /** + * Retrieve the list of files available in the workspace + * @param conversationId ID of the conversation + * @param path Path to list files from. If provided, it lists all the files in the given path + * @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace + */ + static async getFiles( + conversationId: string, + path?: string, + ): Promise { + const url = `${this.getConversationUrl(conversationId)}/list-files`; + const { data } = await openHands.get(url, { + params: { path }, + headers: this.getConversationHeaders(), + }); + + return data; + } + + /** + * Retrieve the content of a file + * @param conversationId ID of the conversation + * @param path Full path of the file to retrieve + * @returns Code content of the file + */ + static async getFile(conversationId: string, path: string): Promise { + const url = `${this.getConversationUrl(conversationId)}/select-file`; + const { data } = await openHands.get(url, { + params: { file: path }, + headers: this.getConversationHeaders(), + }); + + return data.code; + } + + /** + * Upload multiple files to the workspace + * @param conversationId ID of the conversation + * @param files List of files. + * @returns list of uploaded files, list of skipped files + */ + static async uploadFiles( + conversationId: string, + files: File[], + ): Promise { + const formData = new FormData(); + for (const file of files) { + formData.append("files", file); + } + const url = `${this.getConversationUrl(conversationId)}/upload-files`; + const response = await openHands.post( + url, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + ...this.getConversationHeaders(), + }, + }, + ); + return response.data; + } + /** * Get the user installation IDs * @param provider The provider to get installation IDs for (github, bitbucket, etc.) diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts index a402cdade8..35e8391fa9 100644 --- a/frontend/src/api/open-hands.types.ts +++ b/frontend/src/api/open-hands.types.ts @@ -158,3 +158,9 @@ export interface MicroagentContentResponse { git_provider: Provider; triggers: string[]; } + +export type GetFilesResponse = string[]; + +export interface GetFileResponse { + code: string; +} diff --git a/frontend/src/hooks/mutation/use-upload-files.ts b/frontend/src/hooks/mutation/use-upload-files.ts index 6118291473..b59f3b3739 100644 --- a/frontend/src/hooks/mutation/use-upload-files.ts +++ b/frontend/src/hooks/mutation/use-upload-files.ts @@ -1,11 +1,11 @@ import { useMutation } from "@tanstack/react-query"; -import { FileService } from "#/api/file-service/file-service.api"; +import OpenHands from "#/api/open-hands"; export const useUploadFiles = () => useMutation({ mutationKey: ["upload-files"], mutationFn: (variables: { conversationId: string; files: File[] }) => - FileService.uploadFiles(variables.conversationId!, variables.files), + OpenHands.uploadFiles(variables.conversationId!, variables.files), onSuccess: async () => {}, meta: { disableToast: true, diff --git a/frontend/src/hooks/query/use-get-microagents.ts b/frontend/src/hooks/query/use-get-microagents.ts index 0b7642d2e8..097473b170 100644 --- a/frontend/src/hooks/query/use-get-microagents.ts +++ b/frontend/src/hooks/query/use-get-microagents.ts @@ -1,13 +1,13 @@ import { useQuery } from "@tanstack/react-query"; import { useConversationId } from "../use-conversation-id"; -import { FileService } from "#/api/file-service/file-service.api"; +import OpenHands from "#/api/open-hands"; export const useGetMicroagents = (microagentDirectory: string) => { const { conversationId } = useConversationId(); return useQuery({ queryKey: ["files", "microagents", conversationId, microagentDirectory], - queryFn: () => FileService.getFiles(conversationId!, microagentDirectory), + queryFn: () => OpenHands.getFiles(conversationId!, microagentDirectory), enabled: !!conversationId, select: (data) => data.map((fileName) => fileName.replace(microagentDirectory, "")), diff --git a/frontend/src/hooks/query/use-microagent-prompt.ts b/frontend/src/hooks/query/use-microagent-prompt.ts index 05b4f655c3..ac9275a18e 100644 --- a/frontend/src/hooks/query/use-microagent-prompt.ts +++ b/frontend/src/hooks/query/use-microagent-prompt.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { MemoryService } from "#/api/memory-service/memory-service.api"; +import OpenHands from "#/api/open-hands"; import { useConversationId } from "../use-conversation-id"; export const useMicroagentPrompt = (eventId: number) => { @@ -7,7 +7,7 @@ export const useMicroagentPrompt = (eventId: number) => { return useQuery({ queryKey: ["memory", "prompt", conversationId, eventId], - queryFn: () => MemoryService.getPrompt(conversationId!, eventId), + queryFn: () => OpenHands.getMicroagentPrompt(conversationId!, eventId), enabled: !!conversationId, staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 15, // 15 minutes diff --git a/openhands/server/routes/manage_conversations.py b/openhands/server/routes/manage_conversations.py index 62d33c7126..776f8e52a1 100644 --- a/openhands/server/routes/manage_conversations.py +++ b/openhands/server/routes/manage_conversations.py @@ -16,11 +16,11 @@ from openhands.events.action import ( NullAction, ) from openhands.events.event_filter import EventFilter +from openhands.events.event_store import EventStore from openhands.events.observation import ( AgentStateChangedObservation, NullObservation, ) -from openhands.events.stream import EventStream from openhands.integrations.provider import ( PROVIDER_TOKEN_TYPE, ProviderHandler, @@ -44,11 +44,11 @@ from openhands.server.services.conversation_service import ( create_new_conversation, setup_init_convo_settings, ) -from openhands.server.session.conversation import ServerConversation from openhands.server.shared import ( ConversationStoreImpl, config, conversation_manager, + file_store, ) from openhands.server.types import LLMAuthenticationError, MissingSettingsError from openhands.server.user_auth import ( @@ -60,7 +60,7 @@ from openhands.server.user_auth import ( get_user_settings_store, ) from openhands.server.user_auth.user_auth import AuthType -from openhands.server.utils import get_conversation as get_conversation_object +from openhands.server.utils import get_conversation as get_conversation_metadata from openhands.server.utils import get_conversation_store from openhands.storage.conversation.conversation_store import ConversationStore from openhands.storage.data_models.conversation_metadata import ( @@ -331,23 +331,20 @@ async def delete_conversation( return True -@app.get('/conversations/{conversation_id}/remember_prompt') +@app.get('/conversations/{conversation_id}/remember-prompt') async def get_prompt( + conversation_id: str, event_id: int, user_settings: SettingsStore = Depends(get_user_settings_store), - conversation: ServerConversation | None = Depends(get_conversation_object), + metadata: ConversationMetadata = Depends(get_conversation_metadata), ): - if conversation is None: - return JSONResponse( - status_code=404, - content={'error': 'Conversation not found.'}, - ) - - # get event stream for the conversation - event_stream = conversation.event_stream + # get event store for the conversation + event_store = EventStore( + sid=conversation_id, file_store=file_store, user_id=metadata.user_id + ) # retrieve the relevant events - stringified_events = _get_contextual_events(event_stream, event_id) + stringified_events = _get_contextual_events(event_store, event_id) # generate a prompt settings = await user_settings.load() @@ -551,7 +548,7 @@ async def stop_conversation( ) -def _get_contextual_events(event_stream: EventStream, event_id: int) -> str: +def _get_contextual_events(event_store: EventStore, event_id: int) -> str: # find the specified events to learn from # Get X events around the target event context_size = 4 @@ -567,7 +564,7 @@ def _get_contextual_events(event_stream: EventStream, event_id: int) -> str: ) # the types of events that can be in an agent's history # from event_id - context_size to event_id.. - context_before = event_stream.search_events( + context_before = event_store.search_events( start_id=event_id, filter=agent_event_filter, reverse=True, @@ -575,7 +572,7 @@ def _get_contextual_events(event_stream: EventStream, event_id: int) -> str: ) # from event_id to event_id + context_size + 1 - context_after = event_stream.search_events( + context_after = event_store.search_events( start_id=event_id + 1, filter=agent_event_filter, limit=context_size + 1,