diff --git a/agents-api/agents_api/autogen/openapi_model.py b/agents-api/agents_api/autogen/openapi_model.py index 22d916938..336f25c86 100644 --- a/agents-api/agents_api/autogen/openapi_model.py +++ b/agents-api/agents_api/autogen/openapi_model.py @@ -15,8 +15,11 @@ CreateOrUpdateAgentRequest = UpdateAgentRequest CreateOrUpdateUserRequest = UpdateUserRequest +CreateOrUpdateSessionRequest = CreateSessionRequest + ChatMLRole = Entry.model_fields["role"].annotation + def make_session( *, agents: list[UUID], @@ -46,4 +49,4 @@ def make_session( return cls( **data, **participants, - ) \ No newline at end of file + ) diff --git a/agents-api/agents_api/models/agent/list_agents.py b/agents-api/agents_api/models/agent/list_agents.py index 5eebb3a65..8dcb27606 100644 --- a/agents-api/agents_api/models/agent/list_agents.py +++ b/agents-api/agents_api/models/agent/list_agents.py @@ -34,9 +34,6 @@ def list_agents_query( offset: Number of agents to skip before starting to collect the result set. metadata_filter: Dictionary to filter agents based on metadata. client: Instance of CozoClient to execute the query. - - Returns: - A pandas DataFrame containing the query results. """ # Transforms the metadata_filter dictionary into a string representation for the datalog query. metadata_filter_str = ", ".join( diff --git a/agents-api/agents_api/models/agent/update_agent.py b/agents-api/agents_api/models/agent/update_agent.py index a32d041d8..af652a8e4 100644 --- a/agents-api/agents_api/models/agent/update_agent.py +++ b/agents-api/agents_api/models/agent/update_agent.py @@ -57,19 +57,6 @@ def update_agent_query( else [update_data["instructions"]] ) - assertion_query = """ - ?[developer_id, agent_id] := - *agents { - developer_id, - agent_id, - }, - developer_id = to_uuid($developer_id), - agent_id = to_uuid($agent_id), - - # Assertion to ensure the agent exists before updating. - :assert some - """ - # Construct the agent update part of the query with dynamic columns and values based on `update_data`. # Agent update query agent_update_cols, agent_update_vals = cozo_process_mutate_data( @@ -127,11 +114,9 @@ def update_agent_query( if len(default_settings) != 0: queries.insert(0, settings_update_query) - # Combine the assertion query with the update queries queries = [ verify_developer_id_query(developer_id), verify_developer_owns_resource_query(developer_id, "agents", agent_id=agent_id), - assertion_query, *queries, ] diff --git a/agents-api/agents_api/models/session/create_or_update_session.py b/agents-api/agents_api/models/session/create_or_update_session.py new file mode 100644 index 000000000..a5a3cf3ed --- /dev/null +++ b/agents-api/agents_api/models/session/create_or_update_session.py @@ -0,0 +1,144 @@ +from beartype import beartype + +from uuid import UUID + + +from ...autogen.openapi_model import ( + CreateOrUpdateSessionRequest, + ResourceUpdatedResponse, +) +from ...common.utils.cozo import cozo_process_mutate_data +from ..utils import ( + cozo_query, + verify_developer_id_query, + verify_developer_owns_resource_query, + wrap_in_class, +) + + +@wrap_in_class( + ResourceUpdatedResponse, + one=True, + transform=lambda d: { + "id": d["session_id"], + "updated_at": d.pop("updated_at")[0], + "jobs": [], + **d, + }, +) +@cozo_query(debug=True) +@beartype +def create_or_update_session_query( + *, + session_id: UUID, + developer_id: UUID, + create_or_update_session: CreateOrUpdateSessionRequest, +) -> tuple[str, dict]: + + create_or_update_session.metadata = create_or_update_session.metadata or {} + session_data = create_or_update_session.model_dump() + + user = session_data.pop("user") + agent = session_data.pop("agent") + users = session_data.pop("users") + agents = session_data.pop("agents") + + # Only one of agent or agents should be provided. + if agent and agents: + raise ValueError("Only one of 'agent' or 'agents' should be provided.") + + agents = agents or ([agent] if agent else []) + assert len(agents) > 0, "At least one agent must be provided." + + # Users are zero or more, so we default to an empty list if not provided. + if not (user or users): + users = [] + + else: + users = users or [user] + + participants = [ + *[("user", str(user)) for user in users], + *[("agent", str(agent)) for agent in agents], + ] + + # Construct the datalog query for creating a new session and its lookup. + clear_lookup_query = """ + input[session_id] <- [[$session_id]] + ?[session_id, participant_id, participant_type] := + input[session_id], + *session_lookup { + session_id, + participant_type, + participant_id, + }, + + :delete session_lookup { + session_id, + participant_type, + participant_id, + } + """ + + lookup_query = """ + # This section creates a new session lookup to ensure uniqueness and manage session metadata. + session[session_id] <- [[$session_id]] + participants[participant_type, participant_id] <- $participants + ?[session_id, participant_id, participant_type] := + session[session_id], + participants[participant_type, participant_id], + + :put session_lookup { + session_id, + participant_id, + participant_type, + } + """ + + session_update_cols, session_update_vals = cozo_process_mutate_data( + {k: v for k, v in session_data.items() if v is not None} + ) + + # Construct the datalog query for creating or updating session information. + update_query = f""" + input[{session_update_cols}] <- $session_update_vals + ids[session_id, developer_id] <- [[to_uuid($session_id), to_uuid($developer_id)]] + + ?[{session_update_cols}, session_id, developer_id] := + input[{session_update_cols}], + ids[session_id, developer_id], + + :put sessions {{ + {session_update_cols}, session_id, developer_id + }} + + :returning + """ + + queries = [ + verify_developer_id_query(developer_id), + *[ + verify_developer_owns_resource_query( + developer_id, + f"{participant_type}s", + **{f"{participant_type}_id": participant_id}, + ) + for participant_type, participant_id in participants + ], + clear_lookup_query, + lookup_query, + update_query, + ] + + query = "}\n\n{\n".join(queries) + query = f"{{ {query} }}" + + return ( + query, + { + "session_update_vals": session_update_vals, + "session_id": str(session_id), + "developer_id": str(developer_id), + "participants": participants, + }, + ) diff --git a/agents-api/agents_api/models/session/delete_session.py b/agents-api/agents_api/models/session/delete_session.py index bd34e11e3..7a27b32af 100644 --- a/agents-api/agents_api/models/session/delete_session.py +++ b/agents-api/agents_api/models/session/delete_session.py @@ -1,16 +1,33 @@ """This module contains the implementation for deleting sessions from the 'cozodb' database using datalog queries.""" -from beartype import beartype - from uuid import UUID +from beartype import beartype + -from ..utils import cozo_query +from ...autogen.openapi_model import ResourceDeletedResponse +from ...common.utils.datetime import utcnow +from ..utils import ( + cozo_query, + verify_developer_id_query, + verify_developer_owns_resource_query, + wrap_in_class, +) +@wrap_in_class( + ResourceDeletedResponse, + one=True, + transform=lambda d: { + "id": UUID(d.pop("session_id")), + "deleted_at": utcnow(), + "jobs": [], + }, +) @cozo_query @beartype def delete_session_query( + *, developer_id: UUID, session_id: UUID, ) -> tuple[str, dict]: @@ -22,14 +39,13 @@ def delete_session_query( - session_id (UUID): The unique identifier for the session to be deleted. Returns: - - pd.DataFrame: A DataFrame containing the result of the deletion query. + - ResourceDeletedResponse: The response indicating the deletion of the session. """ session_id = str(session_id) developer_id = str(developer_id) # Constructs and executes a datalog query to delete the specified session and its associated data based on the session_id and developer_id. - query = """ - { + delete_lookup_query = """ # Convert session_id to UUID format input[session_id] <- [[ to_uuid($session_id), @@ -37,24 +53,26 @@ def delete_session_query( # Select sessions based on the session_id provided ?[ - agent_id, - user_id, session_id, + participant_id, + participant_type, ] := input[session_id], *session_lookup{ - agent_id, - user_id, session_id, + participant_id, + participant_type, } # Delete entries from session_lookup table matching the criteria :delete session_lookup { - agent_id, - user_id, session_id, + participant_id, + participant_type, } - } { + """ + + delete_query = """ # Convert developer_id and session_id to UUID format input[developer_id, session_id] <- [[ to_uuid($developer_id), @@ -76,7 +94,19 @@ def delete_session_query( session_id, updated_at, } - } + :returning """ + queries = [ + verify_developer_id_query(developer_id), + verify_developer_owns_resource_query( + developer_id, "sessions", session_id=session_id + ), + delete_lookup_query, + delete_query, + ] + + query = "}\n\n{\n".join(queries) + query = f"{{ {query} }}" + return (query, {"session_id": session_id, "developer_id": developer_id}) diff --git a/agents-api/agents_api/models/session/get_session.py b/agents-api/agents_api/models/session/get_session.py index 0af38b0d0..ba258d139 100644 --- a/agents-api/agents_api/models/session/get_session.py +++ b/agents-api/agents_api/models/session/get_session.py @@ -47,6 +47,14 @@ def get_session_query( participant_type, } + # We have to do this dance because users can be zero or more + users_p[users] := + participants[users, "user"] + + users_p[users] := + not participants[_, "user"], + users = [] + ?[ agents, users, @@ -60,7 +68,7 @@ def get_session_query( token_budget, context_overflow, ] := input[developer_id, id], - participants[users, "user"], + users_p[users], participants[agents, "agent"], *sessions{ developer_id, diff --git a/agents-api/agents_api/models/session/list_sessions.py b/agents-api/agents_api/models/session/list_sessions.py index ec5c40310..4e8362226 100644 --- a/agents-api/agents_api/models/session/list_sessions.py +++ b/agents-api/agents_api/models/session/list_sessions.py @@ -1,21 +1,30 @@ """This module contains functions for querying session data from the 'cozodb' database.""" -from beartype import beartype - -from typing import Any +from typing import Any, Literal from uuid import UUID +from beartype import beartype + +from ...autogen.openapi_model import make_session from ...common.utils import json -from ..utils import cozo_query +from ..utils import ( + cozo_query, + verify_developer_id_query, + wrap_in_class, +) +@wrap_in_class(make_session) @cozo_query @beartype def list_sessions_query( + *, developer_id: UUID, limit: int = 100, offset: int = 0, + sort_by: Literal["created_at", "updated_at", "deleted_at"] = "created_at", + direction: Literal["asc", "desc"] = "desc", metadata_filter: dict[str, Any] = {}, ) -> tuple[str, dict]: """Lists sessions from the 'cozodb' database based on the provided filters. @@ -25,9 +34,6 @@ def list_sessions_query( limit (int): The maximum number of sessions to return. offset (int): The offset from which to start listing sessions. metadata_filter (dict[str, Any]): A dictionary of metadata fields to filter sessions by. - - Returns: - pd.DataFrame: A DataFrame containing the queried session data. """ metadata_filter_str = ", ".join( [ @@ -36,14 +42,31 @@ def list_sessions_query( ] ) - query = f""" + sort = f"{'-' if direction == 'desc' else ''}{sort_by}" + + list_query = f""" input[developer_id] <- [[ to_uuid($developer_id), ]] + participants[collect(participant_id), participant_type, session_id] := + *session_lookup{{ + session_id, + participant_id, + participant_type, + }} + + # We have to do this dance because users can be zero or more + users_p[users, session_id] := + participants[users, "user", session_id] + + users_p[users, session_id] := + not participants[_, "user", session_id], + users = [] + ?[ - agent_id, - user_id, + agents, + users, id, situation, summary, @@ -66,19 +89,25 @@ def list_sessions_query( context_overflow, @ "NOW" }}, - *session_lookup{{ - agent_id, - user_id, - session_id: id, - }}, + users_p[users, id], + participants[agents, "agent", id], updated_at = to_int(validity), {metadata_filter_str} :limit $limit :offset $offset - :sort -created_at + :sort {sort} """ + # Datalog query to retrieve agent information based on filters, sorted by creation date in descending order. + queries = [ + verify_developer_id_query(developer_id), + list_query, + ] + + query = "}\n\n{\n".join(queries) + query = f"{{ {query} }}" + # Execute the datalog query and return the results as a pandas DataFrame. return ( query, diff --git a/agents-api/agents_api/models/session/patch_session.py b/agents-api/agents_api/models/session/patch_session.py index 1f98e08cb..7071599b3 100644 --- a/agents-api/agents_api/models/session/patch_session.py +++ b/agents-api/agents_api/models/session/patch_session.py @@ -5,8 +5,14 @@ from uuid import UUID +from ...autogen.openapi_model import PatchSessionRequest, ResourceUpdatedResponse from ...common.utils.cozo import cozo_process_mutate_data -from ..utils import cozo_query +from ..utils import ( + cozo_query, + verify_developer_id_query, + verify_developer_owns_resource_query, + wrap_in_class, +) _fields = [ @@ -21,37 +27,33 @@ # TODO: Add support for updating `render_templates` field +@wrap_in_class( + ResourceUpdatedResponse, + one=True, + transform=lambda d: { + "id": d["session_id"], + "updated_at": d.pop("updated_at")[0], + "jobs": [], + **d, + }, +) @cozo_query @beartype def patch_session_query( + *, session_id: UUID, developer_id: UUID, - **update_data, + patch_session: PatchSessionRequest, ) -> tuple[str, dict]: """Patch session data in the 'cozodb' database. Parameters: - session_id (UUID): The unique identifier for the session to be updated. - developer_id (UUID): The unique identifier for the developer making the update. - - **update_data: Arbitrary keyword arguments representing the data to update. - - Returns: - pd.DataFrame: A pandas DataFrame containing the result of the update operation. - """ - # Process the update data to prepare it for the query. - assertion_query = """ - ?[session_id, developer_id] := - *sessions { - session_id, - developer_id, - }, - session_id = to_uuid($session_id), - developer_id = to_uuid($developer_id), - - # Assertion to ensure the session exists before updating. - :assert some + - patch_session (PatchSessionRequest): The request payload containing the updates to apply. """ + update_data = patch_session.model_dump(exclude_unset=True) metadata = update_data.pop("metadata", {}) or {} session_update_cols, session_update_vals = cozo_process_mutate_data( @@ -70,8 +72,7 @@ def patch_session_query( ) # Construct the datalog query for updating session information. - session_update_query = f""" - {{ + update_query = f""" input[{session_update_cols}] <- $session_update_vals ids[session_id, developer_id] <- [[to_uuid($session_id), to_uuid($developer_id)]] @@ -89,13 +90,21 @@ def patch_session_query( }} :returning - }} """ - combined_query = "{" + assertion_query + "}" + session_update_query + queries = [ + verify_developer_id_query(developer_id), + verify_developer_owns_resource_query( + developer_id, "sessions", session_id=session_id + ), + update_query, + ] + + query = "}\n\n{\n".join(queries) + query = f"{{ {query} }}" return ( - combined_query, + query, { "session_update_vals": session_update_vals, "session_id": str(session_id), diff --git a/agents-api/agents_api/models/session/update_session.py b/agents-api/agents_api/models/session/update_session.py index 902c5b832..6481122e4 100644 --- a/agents-api/agents_api/models/session/update_session.py +++ b/agents-api/agents_api/models/session/update_session.py @@ -1,11 +1,16 @@ -from uuid import UUID - from beartype import beartype +from uuid import UUID -from ...common.utils.cozo import cozo_process_mutate_data -from ..utils import cozo_query +from ...autogen.openapi_model import UpdateSessionRequest, ResourceUpdatedResponse +from ...common.utils.cozo import cozo_process_mutate_data +from ..utils import ( + cozo_query, + verify_developer_id_query, + verify_developer_owns_resource_query, + wrap_in_class, +) _fields = [ "situation", @@ -19,25 +24,26 @@ # TODO: Add support for updating `render_templates` field +@wrap_in_class( + ResourceUpdatedResponse, + one=True, + transform=lambda d: { + "id": d["session_id"], + "updated_at": d.pop("updated_at")[0], + "jobs": [], + **d, + }, +) @cozo_query @beartype def update_session_query( + *, session_id: UUID, developer_id: UUID, - **update_data, + update_session: UpdateSessionRequest, ) -> tuple[str, dict]: - # Process the update data to prepare it for the query. - assertion_query = """ - ?[session_id, developer_id] := - *sessions { - session_id, - developer_id, - }, - session_id = to_uuid($session_id), - developer_id = to_uuid($developer_id), - # Assertion to ensure the session exists before updating. - :assert some - """ + + update_data = update_session.model_dump(exclude_unset=True) session_update_cols, session_update_vals = cozo_process_mutate_data( {k: v for k, v in update_data.items() if v is not None} @@ -55,8 +61,7 @@ def update_session_query( ) # Construct the datalog query for updating session information. - session_update_query = f""" - {{ + update_query = f""" input[{session_update_cols}] <- $session_update_vals ids[session_id, developer_id] <- [[to_uuid($session_id), to_uuid($developer_id)]] @@ -73,13 +78,21 @@ def update_session_query( }} :returning - }} """ - combined_query = "{" + assertion_query + "}" + session_update_query + queries = [ + verify_developer_id_query(developer_id), + verify_developer_owns_resource_query( + developer_id, "sessions", session_id=session_id + ), + update_query, + ] + + query = "}\n\n{\n".join(queries) + query = f"{{ {query} }}" return ( - combined_query, + query, { "session_update_vals": session_update_vals, "session_id": str(session_id), diff --git a/agents-api/agents_api/models/user/update_user.py b/agents-api/agents_api/models/user/update_user.py index d1e3989e2..cf699fe21 100644 --- a/agents-api/agents_api/models/user/update_user.py +++ b/agents-api/agents_api/models/user/update_user.py @@ -47,19 +47,6 @@ def update_user_query( } ) - assertion_query = """ - ?[developer_id, user_id] := - *users { - developer_id, - user_id, - }, - developer_id = to_uuid($developer_id), - user_id = to_uuid($user_id), - - # Assertion to ensure the user exists before updating. - :assert some - """ - # Constructs the update operation for the user, setting new values and updating 'updated_at'. update_query = f""" # update the user @@ -84,11 +71,9 @@ def update_user_query( :returning """ - # Combine the assertion query with the update queries queries = [ verify_developer_id_query(developer_id), verify_developer_owns_resource_query(developer_id, "users", user_id=user_id), - assertion_query, update_query, ] diff --git a/agents-api/agents_api/models/utils.py b/agents-api/agents_api/models/utils.py index f8b004757..fd427d420 100644 --- a/agents-api/agents_api/models/utils.py +++ b/agents-api/agents_api/models/utils.py @@ -2,7 +2,6 @@ from typing import Callable, ParamSpec, Type from uuid import UUID -from beartype import beartype from pydantic import BaseModel import pandas as pd diff --git a/typespec/chat/endpoints.tsp b/typespec/chat/endpoints.tsp index 5418633c1..232a44ab0 100644 --- a/typespec/chat/endpoints.tsp +++ b/typespec/chat/endpoints.tsp @@ -21,13 +21,17 @@ interface Endpoints { generate( @header contentType: yaml | json; - @body + @path + @doc("The session ID") + id: uuid; + + @bodyRoot @doc("Request to generate a response from the model") body: ChatInput; ): { @statusCode _: "200"; - @body + @bodyRoot @doc("Response from the model") body: ChatResponse; }; diff --git a/typespec/main.tsp b/typespec/main.tsp index 93f1a472d..c7585a3d7 100644 --- a/typespec/main.tsp +++ b/typespec/main.tsp @@ -86,6 +86,9 @@ namespace Api { @route("/sessions/{id}/history") interface HistoryRoute extends Entries.Endpoints {} + @route("/sessions/{id}/chat") + interface ChatRoute extends Chat.Endpoints {} + @route("/embed") interface EmbedRoute extends Docs.EmbedEndpoints {}