-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat conversations CRUDS API #5775
Merged
Merged
Changes from all commits
Commits
Show all changes
75 commits
Select commit
Hold shift + click to select a range
74682ef
Conversation endpoint
tofarr fbae222
Merge branch 'main' into feat-search-conversations
tofarr dc82094
Added endpoint for conversation search
tofarr e3f9b7e
WIP
tofarr 3ac818b
WIP
tofarr 76f359c
Added specific types
tofarr 251b47f
Merge branch 'main' into feat-search-conversations
tofarr 413e1b7
Merge branch 'main' into feat-search-conversations
tofarr fe0acad
Metadata
tofarr 5edc93d
Search conversations endpoint
tofarr 8781657
Add unit tests for search_utils and implement page_id_to_offset
openhands-agent bdbce31
Made tests more generic
tofarr b182ba0
Removed something I don't know how it got in there
tofarr ae9ab6d
Now returning selected repository
tofarr 74a2068
Added get conversation endpoint
tofarr 9d43f1f
Merge branch 'main' into feat-search-conversations
tofarr 8d0896b
Lint fix
tofarr ede28d5
dded tests
tofarr 6b30f2a
Lint fixes
tofarr 4aeeb3f
Merge branch 'main' into feat-search-conversations
tofarr 7bd01d2
WIP
tofarr 0220b69
Ruff
tofarr 4172365
Lint fix
tofarr 4305335
WIP
tofarr a90dd21
Fix tests
tofarr a8379d7
WIP
tofarr e9d3f4b
WIP
tofarr 02cd435
WIP
tofarr c54a050
Merge branch 'main' into feat-search-conversations
tofarr d7ccd78
WIP
tofarr 64d8dc8
WIP
tofarr 392e1cc
Merge branch 'main' into feat-search-conversations
tofarr 77f0b57
Merge branch 'main' into feat-search-conversations
tofarr 9f6b63c
Merge branch 'feat-search-conversations' of github.com:All-Hands-AI/O…
tofarr 858d4cb
Ruff
tofarr 5b197b4
Merge branch 'main' into feat-search-conversations
tofarr 4181d09
WIP
tofarr c204108
Ruff
tofarr b1e161b
Merge branch 'main' into feat-search-conversations
tofarr 9d171b5
WIP
tofarr 2312dba
Merge branch 'main' into feat-search-conversations
tofarr 3707971
WIP
tofarr 8f70910
Merge branch 'main' into feat-search-conversations
tofarr bdd23da
WIP
tofarr 0e03376
Test fixes
tofarr d3d1b28
Ruff
tofarr c5f8850
Lint fix
tofarr 56ef8e3
Merge branch 'main' into feat-search-conversations
tofarr 1696987
Merge branch 'main' into feat-search-conversations
tofarr 3f4e87f
Handled case where there are no conversations
tofarr cf1d148
Implemented default title
tofarr 2d667c3
Moved as suggested
tofarr 215c633
Using put as post is for create
tofarr d2a80d9
Fixed typo
tofarr 5becacf
Added test for update conversation
tofarr ea5f583
Moved models as suggested by Enyst
tofarr c89f5f7
Renamed file for clarity
tofarr ba67047
Ruff
tofarr 0315069
Ruff
tofarr 2b80060
Ruff
tofarr 46260e6
Fixed broken tests
tofarr 92d7726
Now updating conversation metadata as events come in
tofarr d3b02e0
Lint fixes
tofarr e012059
Merge branch 'main' into feat-search-conversations
tofarr 792b2f9
Lint fix
tofarr 9cedca1
Test fixes
tofarr 0290a07
Shorter conversation name
tofarr 502f02a
Less utils
tofarr ca99db3
Using first 5 chars
tofarr f4a0645
Unified API as suggested by Stephan
tofarr fb634f0
Patch is most appropriate here
tofarr 78ac6aa
Fix body params
tofarr 881dc21
Merge branch 'main' into feat-search-conversations
tofarr d67380a
Merge branch 'main' into feat-search-conversations
tofarr 5b56f8c
Merge branch 'main' into feat-search-conversations
tofarr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
import uuid | ||
from datetime import datetime | ||
from typing import Callable | ||
|
||
from fastapi import APIRouter, Body, Request | ||
from fastapi.responses import JSONResponse | ||
from github import Github | ||
from pydantic import BaseModel | ||
|
||
from openhands.core.logger import openhands_logger as logger | ||
from openhands.events.stream import EventStreamSubscriber | ||
from openhands.server.routes.settings import ConversationStoreImpl, SettingsStoreImpl | ||
from openhands.server.session.conversation_init_data import ConversationInitData | ||
from openhands.server.shared import config, session_manager | ||
from openhands.storage.data_models.conversation_info import ConversationInfo | ||
from openhands.storage.data_models.conversation_info_result_set import ( | ||
ConversationInfoResultSet, | ||
) | ||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata | ||
from openhands.storage.data_models.conversation_status import ConversationStatus | ||
from openhands.utils.async_utils import ( | ||
GENERAL_TIMEOUT, | ||
call_async_from_sync, | ||
call_sync_from_async, | ||
wait_all, | ||
) | ||
|
||
app = APIRouter(prefix='/api') | ||
UPDATED_AT_CALLBACK_ID = 'updated_at_callback_id' | ||
|
||
|
||
class InitSessionRequest(BaseModel): | ||
github_token: str | None = None | ||
latest_event_id: int = -1 | ||
selected_repository: str | None = None | ||
args: dict | None = None | ||
|
||
|
||
@app.post('/conversations') | ||
async def new_conversation(request: Request, data: InitSessionRequest): | ||
"""Initialize a new session or join an existing one. | ||
After successful initialization, the client should connect to the WebSocket | ||
using the returned conversation ID | ||
""" | ||
logger.info('Initializing new conversation') | ||
github_token = data.github_token or '' | ||
|
||
logger.info('Loading settings') | ||
settings_store = await SettingsStoreImpl.get_instance(config, github_token) | ||
settings = await settings_store.load() | ||
logger.info('Settings loaded') | ||
|
||
session_init_args: dict = {} | ||
if settings: | ||
session_init_args = {**settings.__dict__, **session_init_args} | ||
|
||
session_init_args['github_token'] = github_token | ||
session_init_args['selected_repository'] = data.selected_repository | ||
conversation_init_data = ConversationInitData(**session_init_args) | ||
logger.info('Loading conversation store') | ||
conversation_store = await ConversationStoreImpl.get_instance(config, github_token) | ||
logger.info('Conversation store loaded') | ||
|
||
conversation_id = uuid.uuid4().hex | ||
while await conversation_store.exists(conversation_id): | ||
logger.warning(f'Collision on conversation ID: {conversation_id}. Retrying...') | ||
conversation_id = uuid.uuid4().hex | ||
logger.info(f'New conversation ID: {conversation_id}') | ||
|
||
user_id = '' | ||
if data.github_token: | ||
logger.info('Fetching Github user ID') | ||
with Github(data.github_token) as g: | ||
gh_user = await call_sync_from_async(g.get_user) | ||
user_id = gh_user.id | ||
|
||
logger.info(f'Saving metadata for conversation {conversation_id}') | ||
await conversation_store.save_metadata( | ||
ConversationMetadata( | ||
conversation_id=conversation_id, | ||
github_user_id=user_id, | ||
selected_repository=data.selected_repository, | ||
) | ||
) | ||
|
||
logger.info(f'Starting agent loop for conversation {conversation_id}') | ||
event_stream = await session_manager.maybe_start_agent_loop( | ||
conversation_id, conversation_init_data | ||
) | ||
try: | ||
event_stream.subscribe( | ||
EventStreamSubscriber.SERVER, | ||
_create_conversation_update_callback( | ||
data.github_token or '', conversation_id | ||
), | ||
UPDATED_AT_CALLBACK_ID, | ||
) | ||
except ValueError: | ||
pass # Already subscribed - take no action | ||
logger.info(f'Finished initializing conversation {conversation_id}') | ||
return JSONResponse(content={'status': 'ok', 'conversation_id': conversation_id}) | ||
|
||
|
||
@app.get('/conversations') | ||
async def search_conversations( | ||
request: Request, | ||
page_id: str | None = None, | ||
limit: int = 20, | ||
) -> ConversationInfoResultSet: | ||
github_token = getattr(request.state, 'github_token', '') or '' | ||
conversation_store = await ConversationStoreImpl.get_instance(config, github_token) | ||
conversation_metadata_result_set = await conversation_store.search(page_id, limit) | ||
conversation_ids = set( | ||
conversation.conversation_id | ||
for conversation in conversation_metadata_result_set.results | ||
) | ||
running_conversations = await session_manager.get_agent_loop_running( | ||
set(conversation_ids) | ||
) | ||
result = ConversationInfoResultSet( | ||
results=await wait_all( | ||
_get_conversation_info( | ||
conversation=conversation, | ||
is_running=conversation.conversation_id in running_conversations, | ||
) | ||
for conversation in conversation_metadata_result_set.results | ||
), | ||
next_page_id=conversation_metadata_result_set.next_page_id, | ||
) | ||
return result | ||
|
||
|
||
@app.get('/conversations/{conversation_id}') | ||
async def get_conversation( | ||
conversation_id: str, request: Request | ||
) -> ConversationInfo | None: | ||
github_token = getattr(request.state, 'github_token', '') or '' | ||
conversation_store = await ConversationStoreImpl.get_instance(config, github_token) | ||
try: | ||
metadata = await conversation_store.get_metadata(conversation_id) | ||
is_running = await session_manager.is_agent_loop_running(conversation_id) | ||
conversation_info = await _get_conversation_info(metadata, is_running) | ||
return conversation_info | ||
except FileNotFoundError: | ||
return None | ||
|
||
|
||
@app.patch('/conversations/{conversation_id}') | ||
async def update_conversation( | ||
request: Request, conversation_id: str, title: str = Body(embed=True) | ||
) -> bool: | ||
github_token = getattr(request.state, 'github_token', '') or '' | ||
conversation_store = await ConversationStoreImpl.get_instance(config, github_token) | ||
metadata = await conversation_store.get_metadata(conversation_id) | ||
if not metadata: | ||
return False | ||
metadata.title = title | ||
await conversation_store.save_metadata(metadata) | ||
return True | ||
|
||
|
||
@app.delete('/conversations/{conversation_id}') | ||
async def delete_conversation( | ||
conversation_id: str, | ||
request: Request, | ||
) -> bool: | ||
github_token = getattr(request.state, 'github_token', '') or '' | ||
conversation_store = await ConversationStoreImpl.get_instance(config, github_token) | ||
try: | ||
await conversation_store.get_metadata(conversation_id) | ||
except FileNotFoundError: | ||
return False | ||
is_running = await session_manager.is_agent_loop_running(conversation_id) | ||
if is_running: | ||
return False | ||
await conversation_store.delete_metadata(conversation_id) | ||
return True | ||
|
||
|
||
async def _get_conversation_info( | ||
conversation: ConversationMetadata, | ||
is_running: bool, | ||
) -> ConversationInfo | None: | ||
try: | ||
title = conversation.title | ||
if not title: | ||
title = f'Conversation {conversation.conversation_id[:5]}' | ||
return ConversationInfo( | ||
conversation_id=conversation.conversation_id, | ||
title=title, | ||
last_updated_at=conversation.last_updated_at, | ||
selected_repository=conversation.selected_repository, | ||
status=ConversationStatus.RUNNING | ||
if is_running | ||
else ConversationStatus.STOPPED, | ||
) | ||
except Exception: # type: ignore | ||
logger.warning( | ||
f'Error loading conversation: {conversation.conversation_id[:5]}', | ||
exc_info=True, | ||
stack_info=True, | ||
) | ||
return None | ||
|
||
|
||
def _create_conversation_update_callback( | ||
github_token: str, conversation_id: str | ||
) -> Callable: | ||
def callback(*args, **kwargs): | ||
call_async_from_sync( | ||
_update_timestamp_for_conversation, | ||
GENERAL_TIMEOUT, | ||
github_token, | ||
conversation_id, | ||
) | ||
|
||
return callback | ||
|
||
|
||
async def _update_timestamp_for_conversation(github_token: str, conversation_id: str): | ||
conversation_store = await ConversationStoreImpl.get_instance(config, github_token) | ||
conversation = await conversation_store.get_metadata(conversation_id) | ||
conversation.last_updated_at = datetime.now() | ||
await conversation_store.save_metadata(conversation) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did this change? /api/conversations/foo should have 4 parts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see--we don't need to attach the runtime for these.
Right now I think that's our main auth mechanism, so probably better to leave this as
3
? Worth refactoring in the future though