From 7f6022ea7aa3830e1b07059340fc039cf5593469 Mon Sep 17 00:00:00 2001 From: bwmac Date: Mon, 6 Jan 2025 16:24:39 -0500 Subject: [PATCH 01/64] Adds async convenience functions --- synapseclient/api/agent_services.py | 225 ++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 synapseclient/api/agent_services.py diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py new file mode 100644 index 000000000..c571f157a --- /dev/null +++ b/synapseclient/api/agent_services.py @@ -0,0 +1,225 @@ +"""This module is responsible for exposing the services defined at: + +""" + +import json +import asyncio +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +from async_lru import alru_cache + +if TYPE_CHECKING: + from synapseclient import Synapse + +from synapseclient.core.exceptions import SynapseTimeoutError + + +async def register_agent( + request: Dict[str, Any], + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Registers an agent with Synapse OR gets existing agent registration. + + Arguments: + request: The request for the agent matching + + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The requested agent matching + + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + return await client.rest_put_async( + uri="/agent/registration", body=json.dumps(request) + ) + + +async def get_agent( + registration_id: str, synapse_client: Optional["Synapse"] = None +) -> Dict[str, Any]: + """ + Gets information about an existing agent registration. + + Arguments: + registration_id: The ID of the agent registration to get. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The requested agent registration matching + + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + return await client.rest_get_async(uri=f"/agent/registration/{registration_id}") + + +async def start_session( + request: Dict[str, Any], + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Starts a new chat session with an agent. + + Arguments: + request: The request for the session matching + + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + return await client.rest_post_async(uri="/agent/session", body=json.dumps(request)) + + +async def get_session( + session_id: str, + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Gets information about an existing chat session. + + Arguments: + session_id: The ID of the session to get. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The requested session matching + + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + return await client.rest_get_async(uri=f"/agent/session/{session_id}") + + +async def update_session( + request: Dict[str, Any], + session_id: str, + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Updates the access level for a chat session. + + Arguments: + request: The request for the session matching + + session_id: The ID of the session to update. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + return await client.rest_put_async( + uri=f"/agent/session/{session_id}", body=json.dumps(request) + ) + + +async def send_prompt( + request: Dict[str, Any], + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Sends a prompt to an agent starting an asyncronous job. + + Arguments: + request: The request for the prompt matching + + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The response matching + + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + return await client.rest_post_async( + uri="/agent/chat/async/start", body=json.dumps(request) + ) + + +async def get_response( + prompt_token: str, + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Gets the response to a prompt. + + Arguments: + prompt_token: The token of the prompt to get the response for. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The response matching + + If the reponse is ready. Else, it will return a reponse matching + + + Raises: + TimeoutError: If the response is not ready after 1 minute + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + start_time = asyncio.get_event_loop().time() + TIMEOUT = 60 + + while True: + if asyncio.get_event_loop().time() - start_time > TIMEOUT: + raise SynapseTimeoutError( + f"Timeout waiting for response: {TIMEOUT} seconds" + ) + + response = await client.rest_get_async( + uri=f"/agent/chat/async/get/{prompt_token}" + ) + if response.get("jobState") != "PROCESSING": + return response + await asyncio.sleep(0.5) + + +async def get_trace( + request: Dict[str, Any], + prompt_token: str, + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Gets the trace of a prompt. + + Arguments: + request: The request for the trace matching + + prompt_token: The token of the prompt to get the trace for. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The trace matching + + """ + from synapseclient import Synapse + + client = Synapse.get_client(synapse_client=synapse_client) + return await client.rest_post_async( + uri=f"/agent/chat/trace/{prompt_token}", body=json.dumps(request) + ) From 60dc255c29ed71addc24413284bd9246a28c6e0a Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 7 Jan 2025 10:15:44 -0500 Subject: [PATCH 02/64] expose convenience functions --- agent_test.py | 120 ++++++++++++++++++++++++++++++++++ synapseclient/api/__init__.py | 19 ++++++ 2 files changed, 139 insertions(+) create mode 100644 agent_test.py diff --git a/agent_test.py b/agent_test.py new file mode 100644 index 000000000..9ea91ca48 --- /dev/null +++ b/agent_test.py @@ -0,0 +1,120 @@ +import asyncio +from synapseclient import Synapse +import json +from synapseclient.api import ( + register_agent, + get_agent, + start_session, + get_session, + update_session, + send_prompt, + get_response, + get_trace, +) + + +AWS_AGENT_ID = "APLZVUZ4HR" + + +async def main(): + syn = Synapse() + syn.login() + + # Register the agent + agent_registration_response = await register_agent( + request={ + "awsAgentId": AWS_AGENT_ID, + }, + synapse_client=syn, + ) + print("AGENT REGISTERED:") + print(agent_registration_response) + print("--------------------------------") + + # Get the agent information + agent_response = await get_agent( + registration_id=agent_registration_response["agentRegistrationId"], + synapse_client=syn, + ) + print("AGENT INFORMATION:") + print(agent_response) + print("--------------------------------") + + # Start a chat session + session_response = await start_session( + request={ + "agentAccessLevel": "PUBLICLY_ACCESSIBLE", + "agentRegistrationId": agent_response["agentRegistrationId"], + }, + synapse_client=syn, + ) + print("SESSION STARTED:") + print(session_response) + print("--------------------------------") + + # Get the session information + session_response = await get_session( + session_id=session_response["sessionId"], + synapse_client=syn, + ) + print("SESSION INFORMATION:") + print(session_response) + print("--------------------------------") + + # Update the session access level + session_response = await update_session( + request={ + "sessionId": session_response["sessionId"], + "agentAccessLevel": "READ_YOUR_PRIVATE_DATA", + }, + session_id=session_response["sessionId"], + synapse_client=syn, + ) + print("SESSION UPDATED:") + print(session_response) + print("--------------------------------") + + # Send a prompt to the agent + prompt_response = await send_prompt( + request={ + "concreteType": "org.sagebionetworks.repo.model.agent.AgentChatRequest", + "sessionId": session_response["sessionId"], + "chatText": "What is your purpose?", + "enableTrace": True, + }, + synapse_client=syn, + ) + print("PROMPT SENT:") + print(prompt_response) + print("--------------------------------") + + import time + + # Wait for the agent to respond + time.sleep(20) + + # Get the response from the agent + response_response = await get_response( + prompt_token=prompt_response["token"], + synapse_client=syn, + ) + print("RESPONSE:") + print(response_response) + print("--------------------------------") + + # Get the trace of the prompt + trace_response = await get_trace( + request={ + "jobId": prompt_response["token"], + "newerThanTimestamp": 0, + }, + prompt_token=prompt_response["token"], + synapse_client=syn, + ) + print("TRACE:") + print(trace_response) + print("--------------------------------") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/synapseclient/api/__init__.py b/synapseclient/api/__init__.py index 3211aaf38..e42d2aab3 100644 --- a/synapseclient/api/__init__.py +++ b/synapseclient/api/__init__.py @@ -38,6 +38,16 @@ put_file_multipart_add, put_file_multipart_complete, ) +from .agent_services import ( + register_agent, + get_agent, + start_session, + get_session, + update_session, + send_prompt, + get_response, + get_trace, +) __all__ = [ # annotations @@ -78,4 +88,13 @@ "get_transfer_config", # entity_factory "get_from_entity_factory", + # agent_services + "register_agent", + "get_agent", + "start_session", + "get_session", + "update_session", + "send_prompt", + "get_response", + "get_trace", ] From 3fd345c83d444616d6f101204cae9a00320cc5ae Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 7 Jan 2025 14:58:07 -0500 Subject: [PATCH 03/64] updates convenience functions --- synapseclient/api/agent_services.py | 101 ++++++++++++++++++---------- 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index c571f157a..d1d7638db 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -4,9 +4,7 @@ import json import asyncio -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union - -from async_lru import alru_cache +from typing import TYPE_CHECKING, Any, Dict, Optional if TYPE_CHECKING: from synapseclient import Synapse @@ -15,26 +13,34 @@ async def register_agent( - request: Dict[str, Any], + cloud_agent_id: str, + cloud_alias_id: Optional[str] = None, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Registers an agent with Synapse OR gets existing agent registration. Arguments: - request: The request for the agent matching - + cloud_agent_id: The cloud provider ID of the agent to register. + cloud_alias_id: The cloud provider alias ID of the agent to register. + In the Synapse API, this defaults to 'TSTALIASID'. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. Returns: - The requested agent matching + The registered agent matching """ from synapseclient import Synapse client = Synapse.get_client(synapse_client=synapse_client) + + # Request matching + request = { + "awsAgentId": cloud_agent_id, + "awsAliasId": cloud_alias_id if cloud_alias_id else None, + } return await client.rest_put_async( uri="/agent/registration", body=json.dumps(request) ) @@ -63,15 +69,16 @@ async def get_agent( async def start_session( - request: Dict[str, Any], + access_level: str, + registration_id: str, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Starts a new chat session with an agent. Arguments: - request: The request for the session matching - + access_level: The access level of the agent. + registration_id: The ID of the agent registration to start the session for. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -79,18 +86,24 @@ async def start_session( from synapseclient import Synapse client = Synapse.get_client(synapse_client=synapse_client) + + # Request matching + request = { + "accessLevel": access_level, + "agentRegistrationId": registration_id, + } return await client.rest_post_async(uri="/agent/session", body=json.dumps(request)) async def get_session( - session_id: str, + id: str, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Gets information about an existing chat session. Arguments: - session_id: The ID of the session to get. + id: The ID of the session to get. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -102,21 +115,20 @@ async def get_session( from synapseclient import Synapse client = Synapse.get_client(synapse_client=synapse_client) - return await client.rest_get_async(uri=f"/agent/session/{session_id}") + return await client.rest_get_async(uri=f"/agent/session/{id}") async def update_session( - request: Dict[str, Any], - session_id: str, + id: str, + access_level: str, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Updates the access level for a chat session. Arguments: - request: The request for the session matching - - session_id: The ID of the session to update. + id: The ID of the session to update. + access_level: The access level of the agent. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -124,21 +136,30 @@ async def update_session( from synapseclient import Synapse client = Synapse.get_client(synapse_client=synapse_client) + + # Request matching + request = { + "sessionId": id, + "accessLevel": access_level, + } return await client.rest_put_async( - uri=f"/agent/session/{session_id}", body=json.dumps(request) + uri=f"/agent/session/{id}", body=json.dumps(request) ) async def send_prompt( - request: Dict[str, Any], + id: str, + prompt: str, + enable_trace: bool = False, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Sends a prompt to an agent starting an asyncronous job. Arguments: - request: The request for the prompt matching - + id: The ID of the session to send the prompt to. + prompt: The prompt to send to the agent. + enable_trace: Whether to enable trace for the prompt. Defaults to False. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -150,20 +171,28 @@ async def send_prompt( from synapseclient import Synapse client = Synapse.get_client(synapse_client=synapse_client) + + # Request matching + request = { + "concreteType": "org.sagebionetworks.repo.model.agent.AgentChatRequest", + "sessionId": id, + "chatText": prompt, + "enableTrace": enable_trace, + } return await client.rest_post_async( uri="/agent/chat/async/start", body=json.dumps(request) ) async def get_response( - prompt_token: str, + prompt_id: str, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Gets the response to a prompt. Arguments: - prompt_token: The token of the prompt to get the response for. + prompt_id: The token of the prompt to get the response for. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -171,7 +200,8 @@ async def get_response( Returns: The response matching - If the reponse is ready. Else, it will return a reponse matching + If the reponse is ready. + Else, it will return a reponse matching Raises: @@ -189,26 +219,23 @@ async def get_response( f"Timeout waiting for response: {TIMEOUT} seconds" ) - response = await client.rest_get_async( - uri=f"/agent/chat/async/get/{prompt_token}" - ) + response = await client.rest_get_async(uri=f"/agent/chat/async/get/{prompt_id}") if response.get("jobState") != "PROCESSING": return response await asyncio.sleep(0.5) async def get_trace( - request: Dict[str, Any], - prompt_token: str, + prompt_id: str, + newer_than: Optional[int] = None, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Gets the trace of a prompt. Arguments: - request: The request for the trace matching - - prompt_token: The token of the prompt to get the trace for. + prompt_id: The token of the prompt to get the trace for. + newer_than: The timestamp to get trace results newer than. Defaults to None (all results). synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -220,6 +247,12 @@ async def get_trace( from synapseclient import Synapse client = Synapse.get_client(synapse_client=synapse_client) + + # Request matching + request = { + "jobId": prompt_id, + "newerThanTimestamp": newer_than, + } return await client.rest_post_async( - uri=f"/agent/chat/trace/{prompt_token}", body=json.dumps(request) + uri=f"/agent/chat/trace/{prompt_id}", body=json.dumps(request) ) From 9c2c6d99ebe9f539fcd4a285489cb1944e5b9ed8 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 14:26:23 -0500 Subject: [PATCH 04/64] updates agent_services --- synapseclient/api/agent_services.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index d1d7638db..b1fdd28c4 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -70,7 +70,7 @@ async def get_agent( async def start_session( access_level: str, - registration_id: str, + agent_registration_id: str, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ @@ -78,7 +78,7 @@ async def start_session( Arguments: access_level: The access level of the agent. - registration_id: The ID of the agent registration to start the session for. + agent_registration_id: The ID of the agent registration to start the session for. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -89,8 +89,8 @@ async def start_session( # Request matching request = { - "accessLevel": access_level, - "agentRegistrationId": registration_id, + "agentAccessLevel": access_level, + "agentRegistrationId": agent_registration_id, } return await client.rest_post_async(uri="/agent/session", body=json.dumps(request)) @@ -140,7 +140,7 @@ async def update_session( # Request matching request = { "sessionId": id, - "accessLevel": access_level, + "agentAccessLevel": access_level, } return await client.rest_put_async( uri=f"/agent/session/{id}", body=json.dumps(request) @@ -189,7 +189,9 @@ async def get_response( synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ - Gets the response to a prompt. + Gets the response to a prompt. If the response is not ready, the endpoint will return a reponse matching + . + In this case, the response retrieval is retried every second until the response is ready or a timeout is reached. Arguments: prompt_id: The token of the prompt to get the response for. @@ -200,9 +202,6 @@ async def get_response( Returns: The response matching - If the reponse is ready. - Else, it will return a reponse matching - Raises: TimeoutError: If the response is not ready after 1 minute @@ -220,9 +219,11 @@ async def get_response( ) response = await client.rest_get_async(uri=f"/agent/chat/async/get/{prompt_id}") - if response.get("jobState") != "PROCESSING": - return response - await asyncio.sleep(0.5) + if response.get("jobState") == "PROCESSING": + await asyncio.sleep(1) + continue + + return response async def get_trace( From 52078b3052f26ba1f1c204c5f32eb33c7c28425c Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 14:28:53 -0500 Subject: [PATCH 05/64] removes rest_get_async exception handling --- synapseclient/client.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/synapseclient/client.py b/synapseclient/client.py index 61bcf73c5..8aba3217d 100644 --- a/synapseclient/client.py +++ b/synapseclient/client.py @@ -6373,20 +6373,17 @@ async def rest_get_async( Returns: JSON encoding of response """ - try: - response = await self._rest_call_async( - "get", - uri, - None, - endpoint, - headers, - retry_policy, - requests_session_async_synapse, - **kwargs, - ) - return self._return_rest_body(response) - except Exception: - self.logger.exception("Error in rest_get_async") + response = await self._rest_call_async( + "get", + uri, + None, + endpoint, + headers, + retry_policy, + requests_session_async_synapse, + **kwargs, + ) + return self._return_rest_body(response) async def rest_post_async( self, From fbcfcef506f43142b809e7135af52e4a8160c1df Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 14:41:47 -0500 Subject: [PATCH 06/64] pre-commit fixes --- agent_test.py | 14 +++++++------- synapseclient/api/__init__.py | 20 ++++++++++---------- synapseclient/api/agent_services.py | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/agent_test.py b/agent_test.py index 9ea91ca48..3bf56bf2b 100644 --- a/agent_test.py +++ b/agent_test.py @@ -1,18 +1,18 @@ import asyncio -from synapseclient import Synapse import json + +from synapseclient import Synapse from synapseclient.api import ( - register_agent, get_agent, - start_session, - get_session, - update_session, - send_prompt, get_response, + get_session, get_trace, + register_agent, + send_prompt, + start_session, + update_session, ) - AWS_AGENT_ID = "APLZVUZ4HR" diff --git a/synapseclient/api/__init__.py b/synapseclient/api/__init__.py index e42d2aab3..fe94e0de5 100644 --- a/synapseclient/api/__init__.py +++ b/synapseclient/api/__init__.py @@ -1,4 +1,14 @@ # These are all of the models that are used by the Synapse client. +from .agent_services import ( + get_agent, + get_response, + get_session, + get_trace, + register_agent, + send_prompt, + start_session, + update_session, +) from .annotations import set_annotations, set_annotations_async from .configuration_services import ( get_client_authenticated_s3_profile, @@ -38,16 +48,6 @@ put_file_multipart_add, put_file_multipart_complete, ) -from .agent_services import ( - register_agent, - get_agent, - start_session, - get_session, - update_session, - send_prompt, - get_response, - get_trace, -) __all__ = [ # annotations diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index b1fdd28c4..821541a93 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -2,8 +2,8 @@ """ -import json import asyncio +import json from typing import TYPE_CHECKING, Any, Dict, Optional if TYPE_CHECKING: From 079a02cf6730a43b25417c3baa16ff0139f71d7b Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 14:50:38 -0500 Subject: [PATCH 07/64] delete accidentally committed script --- agent_test.py | 120 -------------------------------------------------- 1 file changed, 120 deletions(-) delete mode 100644 agent_test.py diff --git a/agent_test.py b/agent_test.py deleted file mode 100644 index 3bf56bf2b..000000000 --- a/agent_test.py +++ /dev/null @@ -1,120 +0,0 @@ -import asyncio -import json - -from synapseclient import Synapse -from synapseclient.api import ( - get_agent, - get_response, - get_session, - get_trace, - register_agent, - send_prompt, - start_session, - update_session, -) - -AWS_AGENT_ID = "APLZVUZ4HR" - - -async def main(): - syn = Synapse() - syn.login() - - # Register the agent - agent_registration_response = await register_agent( - request={ - "awsAgentId": AWS_AGENT_ID, - }, - synapse_client=syn, - ) - print("AGENT REGISTERED:") - print(agent_registration_response) - print("--------------------------------") - - # Get the agent information - agent_response = await get_agent( - registration_id=agent_registration_response["agentRegistrationId"], - synapse_client=syn, - ) - print("AGENT INFORMATION:") - print(agent_response) - print("--------------------------------") - - # Start a chat session - session_response = await start_session( - request={ - "agentAccessLevel": "PUBLICLY_ACCESSIBLE", - "agentRegistrationId": agent_response["agentRegistrationId"], - }, - synapse_client=syn, - ) - print("SESSION STARTED:") - print(session_response) - print("--------------------------------") - - # Get the session information - session_response = await get_session( - session_id=session_response["sessionId"], - synapse_client=syn, - ) - print("SESSION INFORMATION:") - print(session_response) - print("--------------------------------") - - # Update the session access level - session_response = await update_session( - request={ - "sessionId": session_response["sessionId"], - "agentAccessLevel": "READ_YOUR_PRIVATE_DATA", - }, - session_id=session_response["sessionId"], - synapse_client=syn, - ) - print("SESSION UPDATED:") - print(session_response) - print("--------------------------------") - - # Send a prompt to the agent - prompt_response = await send_prompt( - request={ - "concreteType": "org.sagebionetworks.repo.model.agent.AgentChatRequest", - "sessionId": session_response["sessionId"], - "chatText": "What is your purpose?", - "enableTrace": True, - }, - synapse_client=syn, - ) - print("PROMPT SENT:") - print(prompt_response) - print("--------------------------------") - - import time - - # Wait for the agent to respond - time.sleep(20) - - # Get the response from the agent - response_response = await get_response( - prompt_token=prompt_response["token"], - synapse_client=syn, - ) - print("RESPONSE:") - print(response_response) - print("--------------------------------") - - # Get the trace of the prompt - trace_response = await get_trace( - request={ - "jobId": prompt_response["token"], - "newerThanTimestamp": 0, - }, - prompt_token=prompt_response["token"], - synapse_client=syn, - ) - print("TRACE:") - print(trace_response) - print("--------------------------------") - - -if __name__ == "__main__": - asyncio.run(main()) From 5ae3257fcf5f3139f239dece89c7b5094b3ef3f8 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 16:15:48 -0500 Subject: [PATCH 08/64] adds initial agent implementation --- synapseclient/models/agent.py | 441 ++++++++++++++++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 synapseclient/models/agent.py diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py new file mode 100644 index 000000000..9e8fe1059 --- /dev/null +++ b/synapseclient/models/agent.py @@ -0,0 +1,441 @@ +from enum import Enum +from typing import Dict, Optional, List +from datetime import datetime +from dataclasses import dataclass, field +from synapseclient import Synapse +from synapseclient.api import ( + register_agent, + get_agent, + start_session, + get_session, + update_session, + send_prompt, + get_response, + get_trace, +) +from synapseclient.core.async_utils import async_to_sync, otel_trace_method + + +class AgentType(Enum): + """ + Enum representing the type of agent as defined in + + 'BASELINE' is a default agent provided by Synapse. + 'CUSTOM' is a custom agent that has been registered by a user. + """ + + BASELINE = "BASELINE" + CUSTOM = "CUSTOM" + + +class AgentSessionAccessLevel(Enum): + """ + Enum representing the access level of the agent session as defined in + + """ + + PUBLICLY_ACCESSIBLE = "PUBLICLY_ACCESSIBLE" + READ_YOUR_PRIVATE_DATA = "READ_YOUR_PRIVATE_DATA" + WRITE_YOUR_PRIVATE_DATA = "WRITE_YOUR_PRIVATE_DATA" + + +@dataclass +class AgentPrompt: + """Represents a prompt, response, and metadata within an AgentSession. + + Attributes: + id: The unique ID of the agent prompt. + prompt: The prompt to send to the agent. + response: The response from the agent. + trace: The trace of the agent session. + """ + + id: Optional[str] = None + """The unique ID of the agent prompt.""" + + prompt: Optional[str] = None + """The prompt sent to the agent.""" + + response: Optional[str] = None + """The response from the agent.""" + + trace: Optional[str] = None + """The trace or "though process" of the agent when responding to the prompt.""" + + +@dataclass +class AgentSession: + """Represents a [Synapse Agent Session](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentSession.html) + + Attributes: + id: The unique ID of the agent session. Can only be used by the user that created it. + access_level: The access level of the agent session. + One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. + started_on: The date the agent session was started. + started_by: The ID of the user who started the agent session. + modified_on: The date the agent session was last modified. + agent_registration_id: The registration ID of the agent that will be used for this session. + etag: The etag of the agent session. + """ + + id: Optional[str] = None + """The unique ID of the agent session. Can only be used by the user that created it.""" + + access_level: Optional[AgentSessionAccessLevel] = ( + AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) + """The access level of the agent session. + One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. + Defaults to PUBLICLY_ACCESSIBLE. + """ + + started_on: Optional[datetime] = None + """The date the agent session was started.""" + + started_by: Optional[int] = None + """The ID of the user who started the agent session.""" + + modified_on: Optional[datetime] = None + """The date the agent session was last modified.""" + + agent_registration_id: Optional[int] = None + """The registration ID of the agent that will be used for this session.""" + + etag: Optional[str] = None + """The etag of the agent session.""" + + chat_history: List[AgentPrompt] = field(default_factory=list) + """A list of AgentPrompt objects.""" + + def fill_from_dict(self, synapse_agent_session: Dict[str, str]) -> "AgentSession": + """ + Converts a response from the REST API into this dataclass. + + Arguments: + synapse_agent_session: The response from the REST API. + + Returns: + The AgentSession object. + """ + self.id = synapse_agent_session.get("sessionId", None) + self.access_level = synapse_agent_session.get("agentAccessLevel", None) + self.started_on = synapse_agent_session.get("startedOn", None) + self.started_by = synapse_agent_session.get("startedBy", None) + self.modified_on = synapse_agent_session.get("modifiedOn", None) + self.agent_registration_id = synapse_agent_session.get( + "agentRegistrationId", None + ) + self.etag = synapse_agent_session.get("etag", None) + return self + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Start_Session: {self.id}" + ) + async def start_async( + self, *, synapse_client: Optional[Synapse] = None + ) -> "AgentSession": + """Starts an agent session. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The new AgentSession object. + """ + syn = Synapse.get_client(synapse_client=synapse_client) + session_response = await start_session( + access_level=self.access_level.value, + agent_registration_id=self.agent_registration_id, + synapse_client=syn, + ) + return self.fill_from_dict(session_response) + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Get_Session: {self.id}" + ) + async def get_async( + self, *, synapse_client: Optional[Synapse] = None + ) -> "AgentSession": + """Gets an agent session. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The retrieved AgentSession object. + """ + syn = Synapse.get_client(synapse_client=synapse_client) + session_response = await get_session( + id=self.id, + synapse_client=syn, + ) + return self.fill_from_dict(synapse_agent_session=session_response) + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Update_Session: {self.id}" + ) + async def update_async( + self, + *, + access_level: AgentSessionAccessLevel, + synapse_client: Optional[Synapse] = None, + ) -> "AgentSession": + """Updates an agent session. Only updates to the access level are currently supported. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The updated AgentSession object. + """ + syn = Synapse.get_client(synapse_client=synapse_client) + + self.access_level = access_level + session_response = await update_session( + id=self.id, + access_level=self.access_level.value, + synapse_client=syn, + ) + return self.fill_from_dict(session_response) + + @otel_trace_method(method_to_trace_name=lambda self, **kwargs: f"Prompt: {self.id}") + async def prompt_async( + self, + *, + prompt: str, + enable_trace: bool = False, + newer_than: Optional[int] = None, + print_response: bool = False, + synapse_client: Optional[Synapse] = None, + ) -> None: + """Sends a prompt to the agent and adds the response to the AgentSession's chat history. + + Arguments: + prompt: The prompt to send to the agent. + enable_trace: Whether to enable trace for the prompt. + newer_than: The timestamp to get trace results newer than. Defaults to None (all results). + print: Whether to print the response to the console. + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + """ + syn = Synapse.get_client(synapse_client=synapse_client) + prompt_response = await send_prompt( + id=self.id, + prompt=prompt, + enable_trace=enable_trace, + synapse_client=syn, + ) + prompt_id = prompt_response["token"] + + answer_response = await get_response( + prompt_id=prompt_id, + synapse_client=syn, + ) + response = answer_response["responseText"] + + if enable_trace: + trace_response = await get_trace( + prompt_id=prompt_id, + newer_than=newer_than, + synapse_client=syn, + ) + trace = trace_response["page"][0]["message"] + + self.chat_history.append( + AgentPrompt( + id=prompt_id, + prompt=prompt, + response=response, + trace=trace, + ) + ) + + if print_response: + print(f"PROMPT:\n{prompt}\n") + print(f"RESPONSE:\n{response}\n") + if enable_trace: + print(f"TRACE:\n{trace}") + + +@dataclass +class Agent: + """Represents a [Synapse Agent Registration](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentRegistration.html) + + Attributes: + cloud_agent_id: The unique ID of the agent in the cloud provider. + cloud_alias_id: The alias ID of the agent in the cloud provider. + In the Synapse API, this defaults to 'TSTALIASID'. + synapse_registration_id: The ID number of the agent assigned by Synapse. + registered_on: The date the agent was registered. + type: The type of agent. + """ + + cloud_agent_id: Optional[str] = None + """The unique ID of the agent in the cloud provider.""" + + cloud_alias_id: Optional[str] = None + """The alias ID of the agent in the cloud provider. In the Synapse API, this defaults to 'TSTALIASID'.""" + + registration_id: Optional[int] = None + """The ID number of the agent assigned by Synapse.""" + + registered_on: Optional[datetime] = None + """The date the agent was registered.""" + + type: Optional[AgentType] = None + """The type of agent. One of either BASELINE or CUSTOM.""" + + sessions: Dict[str, AgentSession] = field(default_factory=dict) + """A dictionary of AgentSession objects, keyed by session ID.""" + + current_session: Optional[str] = None + """The ID of the current session. Prompts will be sent to this session by default.""" + + def fill_from_dict(self, agent_registration: Dict[str, str]) -> "Agent": + """ + Converts a response from the REST API into this dataclass. + + Arguments: + agent_registration: The response from the REST API. + + Returns: + The Agent object. + """ + self.cloud_agent_id = agent_registration.get("awsAgentId", None) + self.cloud_alias_id = agent_registration.get("awsAliasId", None) + self.registration_id = agent_registration.get("agentRegistrationId", None) + self.registered_on = agent_registration.get("registeredOn", None) + self.type = agent_registration.get("type", None) + return self + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Register_Agent: {self.registration_id}" + ) + async def register_async( + self, *, synapse_client: Optional[Synapse] = None + ) -> "Agent": + """Registers an agent with the Synapse API. If agent exists, it will be retrieved. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The registered or existing Agent object. + """ + syn = Synapse.get_client(synapse_client=synapse_client) + agent_response = await register_agent( + cloud_agent_id=self.cloud_agent_id, + cloud_alias_id=self.cloud_alias_id, + synapse_client=syn, + ) + return self.fill_from_dict(agent_response) + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Get_Agent: {self.registration_id}" + ) + async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": + """Gets an existing agent. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The existing Agent object. + """ + syn = Synapse.get_client(synapse_client=synapse_client) + agent_response = await get_agent( + registration_id=self.registration_id, + synapse_client=syn, + ) + print(agent_response) + breakpoint() + return self.fill_from_dict(agent_response) + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Start_Agent_Session: {self.registration_id}" + ) + async def start_session_async( + self, + *, + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + synapse_client: Optional[Synapse] = None, + ) -> "AgentSession": + """Starts an agent session. + + Arguments: + access_level: The access level of the agent session. + Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. + Defaults to PUBLICLY_ACCESSIBLE. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. + + Returns: + The new AgentSession object. + """ + access_level = AgentSessionAccessLevel(access_level) + syn = Synapse.get_client(synapse_client=synapse_client) + session = await AgentSession( + agent_registration_id=self.registration_id, access_level=access_level + ).start_async(synapse_client=syn) + self.sessions[session.id] = session + self.current_session = session.id + return session + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Get_Agent_Session: {self.registration_id}" + ) + async def get_session_async( + self, *, session_id: str, synapse_client: Optional[Synapse] = None + ) -> "AgentSession": + syn = Synapse.get_client(synapse_client=synapse_client) + session = await AgentSession(id=session_id).get_async(synapse_client=syn) + if session.id not in self.sessions: + self.sessions[session.id] = session + self.current_session = session.id + return session + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Prompt_Agent_Session: {self.registration_id}" + ) + async def prompt( + self, + *, + session_id: Optional[str] = None, + prompt: str, + enable_trace: bool = False, + newer_than: Optional[int] = None, + print_response: bool = False, + synapse_client: Optional[Synapse] = None, + ) -> None: + """Sends a prompt to the agent for the current session. + If no session is currently active, a new session will be started. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + """ + syn = Synapse.get_client(synapse_client=synapse_client) + + if session_id: + if session_id not in self.sessions: + await self.get_session_async(session_id=session_id, synapse_client=syn) + session = self.sessions[session_id] + else: + if not self.current_session: + await self.start_session_async(synapse_client=syn) + session = self.sessions[self.current_session] + + await session.prompt_async( + prompt=prompt, + enable_trace=enable_trace, + newer_than=newer_than, + print_response=print_response, + synapse_client=syn, + ) + + @otel_trace_method( + method_to_trace_name=lambda self, **kwargs: f"Get_Agent_Session_Chat_History: {self.registration_id}" + ) + def get_chat_history(self) -> List[AgentPrompt]: + """Gets the chat history for the current session.""" + return self.sessions[self.current_session].chat_history From 84edb76a079b7881f489d407851cdb72dbda6f79 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 16:23:25 -0500 Subject: [PATCH 09/64] clean up agent --- synapseclient/models/agent.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 9e8fe1059..7a32368dc 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -1,19 +1,20 @@ -from enum import Enum -from typing import Dict, Optional, List -from datetime import datetime from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from typing import Dict, List, Optional + from synapseclient import Synapse from synapseclient.api import ( - register_agent, get_agent, - start_session, - get_session, - update_session, - send_prompt, get_response, + get_session, get_trace, + register_agent, + send_prompt, + start_session, + update_session, ) -from synapseclient.core.async_utils import async_to_sync, otel_trace_method +from synapseclient.core.async_utils import otel_trace_method class AgentType(Enum): @@ -84,7 +85,7 @@ class AgentSession: access_level: Optional[AgentSessionAccessLevel] = ( AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE ) - """The access level of the agent session. + """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. """ @@ -346,8 +347,6 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent registration_id=self.registration_id, synapse_client=syn, ) - print(agent_response) - breakpoint() return self.fill_from_dict(agent_response) @otel_trace_method( @@ -412,6 +411,11 @@ async def prompt( If no session is currently active, a new session will be started. Arguments: + session_id: The ID of the session to send the prompt to. If None, the current session will be used. + prompt: The prompt to send to the agent. + enable_trace: Whether to enable trace for the prompt. + newer_than: The timestamp to get trace results newer than. Defaults to None (all results). + print_response: Whether to print the response to the console. synapse_client: The Synapse client to use for the request. If None, the default client will be used. """ syn = Synapse.get_client(synapse_client=synapse_client) @@ -419,13 +423,11 @@ async def prompt( if session_id: if session_id not in self.sessions: await self.get_session_async(session_id=session_id, synapse_client=syn) - session = self.sessions[session_id] else: if not self.current_session: await self.start_session_async(synapse_client=syn) - session = self.sessions[self.current_session] - await session.prompt_async( + await self.sessions[self.current_session].prompt_async( prompt=prompt, enable_trace=enable_trace, newer_than=newer_than, From 88c6f0618349d281476bdff3d1fe5b63b95b7146 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 16:47:58 -0500 Subject: [PATCH 10/64] adds missing docstrings --- synapseclient/models/agent.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 7a32368dc..0e53f3a97 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -420,9 +420,12 @@ async def prompt( """ syn = Synapse.get_client(synapse_client=synapse_client) + # TODO: Iron this out. It's a little confusing. if session_id: if session_id not in self.sessions: await self.get_session_async(session_id=session_id, synapse_client=syn) + else: + self.current_session = session_id else: if not self.current_session: await self.start_session_async(synapse_client=syn) @@ -440,4 +443,5 @@ async def prompt( ) def get_chat_history(self) -> List[AgentPrompt]: """Gets the chat history for the current session.""" + # TODO: Is this the best way to do this? return self.sessions[self.current_session].chat_history From f34e9cc723c9792d46780d73de654c6d5427aa77 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 8 Jan 2025 16:53:36 -0500 Subject: [PATCH 11/64] pre-commit --- synapseclient/models/agent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 0e53f3a97..0b6417a8e 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -82,9 +82,9 @@ class AgentSession: id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[AgentSessionAccessLevel] = ( - AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. From 05e73f3317c378ef64273a88e8faa8eb1de60877 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 10 Jan 2025 10:14:29 -0500 Subject: [PATCH 12/64] updates agent_services --- synapseclient/api/agent_services.py | 24 +++++++++++++------ .../core/constants/concrete_types.py | 3 +++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index 821541a93..7d7d43838 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from synapseclient import Synapse +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST from synapseclient.core.exceptions import SynapseTimeoutError @@ -19,6 +20,8 @@ async def register_agent( ) -> Dict[str, Any]: """ Registers an agent with Synapse OR gets existing agent registration. + Sends a request matching + Arguments: cloud_agent_id: The cloud provider ID of the agent to register. @@ -36,10 +39,9 @@ async def register_agent( client = Synapse.get_client(synapse_client=synapse_client) - # Request matching request = { "awsAgentId": cloud_agent_id, - "awsAliasId": cloud_alias_id if cloud_alias_id else None, + "awsAliasId": cloud_alias_id if cloud_alias_id else "TSTALIASID", } return await client.rest_put_async( uri="/agent/registration", body=json.dumps(request) @@ -75,6 +77,8 @@ async def start_session( ) -> Dict[str, Any]: """ Starts a new chat session with an agent. + Sends a request matching + Arguments: access_level: The access level of the agent. @@ -87,7 +91,6 @@ async def start_session( client = Synapse.get_client(synapse_client=synapse_client) - # Request matching request = { "agentAccessLevel": access_level, "agentRegistrationId": agent_registration_id, @@ -125,6 +128,8 @@ async def update_session( ) -> Dict[str, Any]: """ Updates the access level for a chat session. + Sends a request matching + Arguments: id: The ID of the session to update. @@ -137,7 +142,6 @@ async def update_session( client = Synapse.get_client(synapse_client=synapse_client) - # Request matching request = { "sessionId": id, "agentAccessLevel": access_level, @@ -155,6 +159,8 @@ async def send_prompt( ) -> Dict[str, Any]: """ Sends a prompt to an agent starting an asyncronous job. + Sends a request matching + Arguments: id: The ID of the session to send the prompt to. @@ -172,9 +178,8 @@ async def send_prompt( client = Synapse.get_client(synapse_client=synapse_client) - # Request matching request = { - "concreteType": "org.sagebionetworks.repo.model.agent.AgentChatRequest", + "concreteType": AGENT_CHAT_REQUEST, "sessionId": id, "chatText": prompt, "enableTrace": enable_trace, @@ -209,6 +214,8 @@ async def get_response( from synapseclient import Synapse client = Synapse.get_client(synapse_client=synapse_client) + # TODO: Create async compliant version of _waitForAsync and add this logic there + # synapseclient/core/async_utils.py start_time = asyncio.get_event_loop().time() TIMEOUT = 60 @@ -233,10 +240,14 @@ async def get_trace( ) -> Dict[str, Any]: """ Gets the trace of a prompt. + Sends a request matching + Arguments: prompt_id: The token of the prompt to get the trace for. newer_than: The timestamp to get trace results newer than. Defaults to None (all results). + Timestamps should be in milliseconds since the epoch per the API documentation. + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/TraceEvent.html synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -249,7 +260,6 @@ async def get_trace( client = Synapse.get_client(synapse_client=synapse_client) - # Request matching request = { "jobId": prompt_id, "newerThanTimestamp": newer_than, diff --git a/synapseclient/core/constants/concrete_types.py b/synapseclient/core/constants/concrete_types.py index f8d4ee442..e2033c030 100644 --- a/synapseclient/core/constants/concrete_types.py +++ b/synapseclient/core/constants/concrete_types.py @@ -68,3 +68,6 @@ # Activity/Provenance USED_URL = "org.sagebionetworks.repo.model.provenance.UsedURL" USED_ENTITY = "org.sagebionetworks.repo.model.provenance.UsedEntity" + +# Agent +AGENT_CHAT_REQUEST = "org.sagebionetworks.repo.model.agent.AgentChatRequest" From 9b6e0353558964f53a17c1e461bcd94832602c04 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 10 Jan 2025 10:59:51 -0500 Subject: [PATCH 13/64] updates agent.py --- synapseclient/models/agent.py | 107 +++++++++++++++++----------------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 0b6417a8e..a479a303e 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from datetime import datetime from enum import Enum -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from synapseclient import Synapse from synapseclient.api import ( @@ -17,22 +17,27 @@ from synapseclient.core.async_utils import otel_trace_method -class AgentType(Enum): +class AgentType(str, Enum): """ Enum representing the type of agent as defined in - 'BASELINE' is a default agent provided by Synapse. - 'CUSTOM' is a custom agent that has been registered by a user. + + - BASELINE is a default agent provided by Synapse. + - CUSTOM is a custom agent that has been registered by a user. """ BASELINE = "BASELINE" CUSTOM = "CUSTOM" -class AgentSessionAccessLevel(Enum): +class AgentSessionAccessLevel(str, Enum): """ Enum representing the access level of the agent session as defined in + + - PUBLICLY_ACCESSIBLE: The agent can only access publicly accessible data. + - READ_YOUR_PRIVATE_DATA: The agent can read the user's private data. + - WRITE_YOUR_PRIVATE_DATA: The agent can write to the user's private data. """ PUBLICLY_ACCESSIBLE = "PUBLICLY_ACCESSIBLE" @@ -61,9 +66,10 @@ class AgentPrompt: """The response from the agent.""" trace: Optional[str] = None - """The trace or "though process" of the agent when responding to the prompt.""" + """The trace or "thought process" of the agent when responding to the prompt.""" +# TODO Add example usage to the docstring @dataclass class AgentSession: """Represents a [Synapse Agent Session](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentSession.html) @@ -77,6 +83,7 @@ class AgentSession: modified_on: The date the agent session was last modified. agent_registration_id: The registration ID of the agent that will be used for this session. etag: The etag of the agent session. + """ id: Optional[str] = None @@ -129,9 +136,7 @@ def fill_from_dict(self, synapse_agent_session: Dict[str, str]) -> "AgentSession self.etag = synapse_agent_session.get("etag", None) return self - @otel_trace_method( - method_to_trace_name=lambda self, **kwargs: f"Start_Session: {self.id}" - ) + @otel_trace_method(method_to_trace_name=lambda self, **kwargs: "Start_Session") async def start_async( self, *, synapse_client: Optional[Synapse] = None ) -> "AgentSession": @@ -143,11 +148,10 @@ async def start_async( Returns: The new AgentSession object. """ - syn = Synapse.get_client(synapse_client=synapse_client) session_response = await start_session( - access_level=self.access_level.value, + access_level=self.access_level, agent_registration_id=self.agent_registration_id, - synapse_client=syn, + synapse_client=synapse_client, ) return self.fill_from_dict(session_response) @@ -165,10 +169,9 @@ async def get_async( Returns: The retrieved AgentSession object. """ - syn = Synapse.get_client(synapse_client=synapse_client) session_response = await get_session( id=self.id, - synapse_client=syn, + synapse_client=synapse_client, ) return self.fill_from_dict(synapse_agent_session=session_response) @@ -178,10 +181,10 @@ async def get_async( async def update_async( self, *, - access_level: AgentSessionAccessLevel, synapse_client: Optional[Synapse] = None, ) -> "AgentSession": - """Updates an agent session. Only updates to the access level are currently supported. + """Updates an agent session. + Only updates to the access level are currently supported. Arguments: synapse_client: The Synapse client to use for the request. If None, the default client will be used. @@ -189,24 +192,21 @@ async def update_async( Returns: The updated AgentSession object. """ - syn = Synapse.get_client(synapse_client=synapse_client) - - self.access_level = access_level session_response = await update_session( id=self.id, - access_level=self.access_level.value, - synapse_client=syn, + access_level=self.access_level, + synapse_client=synapse_client, ) return self.fill_from_dict(session_response) @otel_trace_method(method_to_trace_name=lambda self, **kwargs: f"Prompt: {self.id}") async def prompt_async( self, - *, prompt: str, enable_trace: bool = False, - newer_than: Optional[int] = None, print_response: bool = False, + newer_than: Optional[int] = None, + *, synapse_client: Optional[Synapse] = None, ) -> None: """Sends a prompt to the agent and adds the response to the AgentSession's chat history. @@ -214,22 +214,21 @@ async def prompt_async( Arguments: prompt: The prompt to send to the agent. enable_trace: Whether to enable trace for the prompt. + print_response: Whether to print the response to the console. newer_than: The timestamp to get trace results newer than. Defaults to None (all results). - print: Whether to print the response to the console. synapse_client: The Synapse client to use for the request. If None, the default client will be used. """ - syn = Synapse.get_client(synapse_client=synapse_client) prompt_response = await send_prompt( id=self.id, prompt=prompt, enable_trace=enable_trace, - synapse_client=syn, + synapse_client=synapse_client, ) prompt_id = prompt_response["token"] answer_response = await get_response( prompt_id=prompt_id, - synapse_client=syn, + synapse_client=synapse_client, ) response = answer_response["responseText"] @@ -237,7 +236,7 @@ async def prompt_async( trace_response = await get_trace( prompt_id=prompt_id, newer_than=newer_than, - synapse_client=syn, + synapse_client=synapse_client, ) trace = trace_response["page"][0]["message"] @@ -257,6 +256,7 @@ async def prompt_async( print(f"TRACE:\n{trace}") +# TODO Add example usage to the docstring @dataclass class Agent: """Represents a [Synapse Agent Registration](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentRegistration.html) @@ -288,8 +288,8 @@ class Agent: sessions: Dict[str, AgentSession] = field(default_factory=dict) """A dictionary of AgentSession objects, keyed by session ID.""" - current_session: Optional[str] = None - """The ID of the current session. Prompts will be sent to this session by default.""" + current_session: Optional[AgentSession] = None + """The current session. Prompts will be sent to this session by default.""" def fill_from_dict(self, agent_registration: Dict[str, str]) -> "Agent": """ @@ -354,10 +354,10 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent ) async def start_session_async( self, - *, access_level: Optional[ AgentSessionAccessLevel ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + *, synapse_client: Optional[Synapse] = None, ) -> "AgentSession": """Starts an agent session. @@ -373,12 +373,11 @@ async def start_session_async( The new AgentSession object. """ access_level = AgentSessionAccessLevel(access_level) - syn = Synapse.get_client(synapse_client=synapse_client) session = await AgentSession( agent_registration_id=self.registration_id, access_level=access_level - ).start_async(synapse_client=syn) + ).start_async(synapse_client=synapse_client) self.sessions[session.id] = session - self.current_session = session.id + self.current_session = session return session @otel_trace_method( @@ -387,11 +386,12 @@ async def start_session_async( async def get_session_async( self, *, session_id: str, synapse_client: Optional[Synapse] = None ) -> "AgentSession": - syn = Synapse.get_client(synapse_client=synapse_client) - session = await AgentSession(id=session_id).get_async(synapse_client=syn) + session = await AgentSession(id=session_id).get_async( + synapse_client=synapse_client + ) if session.id not in self.sessions: self.sessions[session.id] = session - self.current_session = session.id + self.current_session = session return session @otel_trace_method( @@ -400,48 +400,47 @@ async def get_session_async( async def prompt( self, *, - session_id: Optional[str] = None, prompt: str, enable_trace: bool = False, - newer_than: Optional[int] = None, print_response: bool = False, + session: Optional[AgentSession] = None, + newer_than: Optional[int] = None, synapse_client: Optional[Synapse] = None, ) -> None: """Sends a prompt to the agent for the current session. If no session is currently active, a new session will be started. Arguments: - session_id: The ID of the session to send the prompt to. If None, the current session will be used. prompt: The prompt to send to the agent. enable_trace: Whether to enable trace for the prompt. - newer_than: The timestamp to get trace results newer than. Defaults to None (all results). print_response: Whether to print the response to the console. + session_id: The ID of the session to send the prompt to. If None, the current session will be used. + newer_than: The timestamp to get trace results newer than. Defaults to None (all results). synapse_client: The Synapse client to use for the request. If None, the default client will be used. """ - syn = Synapse.get_client(synapse_client=synapse_client) - - # TODO: Iron this out. It's a little confusing. - if session_id: - if session_id not in self.sessions: - await self.get_session_async(session_id=session_id, synapse_client=syn) + # TODO: Iron this out. Make sure we cover all cases. + if session: + if session.id not in self.sessions: + await self.get_session_async( + session_id=session.id, synapse_client=synapse_client + ) else: - self.current_session = session_id + self.current_session = session else: if not self.current_session: - await self.start_session_async(synapse_client=syn) + await self.start_session_async(synapse_client=synapse_client) - await self.sessions[self.current_session].prompt_async( + await self.current_session.prompt_async( prompt=prompt, enable_trace=enable_trace, newer_than=newer_than, print_response=print_response, - synapse_client=syn, + synapse_client=synapse_client, ) @otel_trace_method( method_to_trace_name=lambda self, **kwargs: f"Get_Agent_Session_Chat_History: {self.registration_id}" ) - def get_chat_history(self) -> List[AgentPrompt]: + def get_chat_history(self) -> Union[List[AgentPrompt], None]: """Gets the chat history for the current session.""" - # TODO: Is this the best way to do this? - return self.sessions[self.current_session].chat_history + return self.current_session.chat_history if self.current_session else None From dadbac395204a531ed4e0630740475b9869be0da Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 10 Jan 2025 13:00:12 -0500 Subject: [PATCH 14/64] Updates alias ID handling --- synapseclient/api/agent_services.py | 7 +++---- synapseclient/models/agent.py | 14 +++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index 7d7d43838..dfc7baddb 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -39,10 +39,9 @@ async def register_agent( client = Synapse.get_client(synapse_client=synapse_client) - request = { - "awsAgentId": cloud_agent_id, - "awsAliasId": cloud_alias_id if cloud_alias_id else "TSTALIASID", - } + request = {"awsAgentId": cloud_agent_id} + if cloud_alias_id: + request["awsAliasId"] = cloud_alias_id return await client.rest_put_async( uri="/agent/registration", body=json.dumps(request) ) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index a479a303e..8cb270bd3 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -238,6 +238,7 @@ async def prompt_async( newer_than=newer_than, synapse_client=synapse_client, ) + # TODO: This is a bit hacky. Do we need to parse the page instead? trace = trace_response["page"][0]["message"] self.chat_history.append( @@ -264,7 +265,7 @@ class Agent: Attributes: cloud_agent_id: The unique ID of the agent in the cloud provider. cloud_alias_id: The alias ID of the agent in the cloud provider. - In the Synapse API, this defaults to 'TSTALIASID'. + Defaults to 'TSTALIASID' in the Synapse API. synapse_registration_id: The ID number of the agent assigned by Synapse. registered_on: The date the agent was registered. type: The type of agent. @@ -274,7 +275,9 @@ class Agent: """The unique ID of the agent in the cloud provider.""" cloud_alias_id: Optional[str] = None - """The alias ID of the agent in the cloud provider. In the Synapse API, this defaults to 'TSTALIASID'.""" + """The alias ID of the agent in the cloud provider. + Defaults to 'TSTALIASID' in the Synapse API. + """ registration_id: Optional[int] = None """The ID number of the agent assigned by Synapse.""" @@ -384,7 +387,7 @@ async def start_session_async( method_to_trace_name=lambda self, **kwargs: f"Get_Agent_Session: {self.registration_id}" ) async def get_session_async( - self, *, session_id: str, synapse_client: Optional[Synapse] = None + self, session_id: str, *, synapse_client: Optional[Synapse] = None ) -> "AgentSession": session = await AgentSession(id=session_id).get_async( synapse_client=synapse_client @@ -399,12 +402,12 @@ async def get_session_async( ) async def prompt( self, - *, prompt: str, enable_trace: bool = False, print_response: bool = False, session: Optional[AgentSession] = None, newer_than: Optional[int] = None, + *, synapse_client: Optional[Synapse] = None, ) -> None: """Sends a prompt to the agent for the current session. @@ -438,9 +441,6 @@ async def prompt( synapse_client=synapse_client, ) - @otel_trace_method( - method_to_trace_name=lambda self, **kwargs: f"Get_Agent_Session_Chat_History: {self.registration_id}" - ) def get_chat_history(self) -> Union[List[AgentPrompt], None]: """Gets the chat history for the current session.""" return self.current_session.chat_history if self.current_session else None From 9c7547ad6e8ac26aeeb1e35a364f2e7bbf39f298 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 10 Jan 2025 15:45:24 -0500 Subject: [PATCH 15/64] adds syncronous interface --- synapseclient/models/__init__.py | 4 + synapseclient/models/agent.py | 26 +++- .../models/protocols/agent_protocol.py | 144 ++++++++++++++++++ 3 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 synapseclient/models/protocols/agent_protocol.py diff --git a/synapseclient/models/__init__.py b/synapseclient/models/__init__.py index a487a3827..09aa2ac32 100644 --- a/synapseclient/models/__init__.py +++ b/synapseclient/models/__init__.py @@ -1,5 +1,6 @@ # These are all of the models that are used by the Synapse client. from synapseclient.models.activity import Activity, UsedEntity, UsedURL +from synapseclient.models.agent import Agent, AgentSession, AgentSessionAccessLevel from synapseclient.models.annotations import Annotations from synapseclient.models.file import File, FileHandle from synapseclient.models.folder import Folder @@ -38,4 +39,7 @@ "TeamMember", "UserProfile", "UserPreference", + "Agent", + "AgentSession", + "AgentSessionAccessLevel", ] diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 8cb270bd3..903a88f98 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -14,7 +14,11 @@ start_session, update_session, ) -from synapseclient.core.async_utils import otel_trace_method +from synapseclient.core.async_utils import async_to_sync, otel_trace_method +from synapseclient.models.protocols.agent_protocol import ( + AgentSessionSynchronousProtocol, + AgentSynchronousProtocol, +) class AgentType(str, Enum): @@ -71,7 +75,8 @@ class AgentPrompt: # TODO Add example usage to the docstring @dataclass -class AgentSession: +@async_to_sync +class AgentSession(AgentSessionSynchronousProtocol): """Represents a [Synapse Agent Session](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentSession.html) Attributes: @@ -259,7 +264,8 @@ async def prompt_async( # TODO Add example usage to the docstring @dataclass -class Agent: +@async_to_sync +class Agent(AgentSynchronousProtocol): """Represents a [Synapse Agent Registration](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentRegistration.html) Attributes: @@ -364,6 +370,7 @@ async def start_session_async( synapse_client: Optional[Synapse] = None, ) -> "AgentSession": """Starts an agent session. + Adds the session to the Agent's sessions dictionary and sets it as the current session. Arguments: access_level: The access level of the agent session. @@ -389,6 +396,17 @@ async def start_session_async( async def get_session_async( self, session_id: str, *, synapse_client: Optional[Synapse] = None ) -> "AgentSession": + """Gets an existing agent session. + Adds the session to the Agent's sessions dictionary and sets it as the current session. + + Arguments: + session_id: The ID of the session to get. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. + + Returns: + The existing AgentSession object. + """ session = await AgentSession(id=session_id).get_async( synapse_client=synapse_client ) @@ -400,7 +418,7 @@ async def get_session_async( @otel_trace_method( method_to_trace_name=lambda self, **kwargs: f"Prompt_Agent_Session: {self.registration_id}" ) - async def prompt( + async def prompt_async( self, prompt: str, enable_trace: bool = False, diff --git a/synapseclient/models/protocols/agent_protocol.py b/synapseclient/models/protocols/agent_protocol.py new file mode 100644 index 000000000..5a0ca138c --- /dev/null +++ b/synapseclient/models/protocols/agent_protocol.py @@ -0,0 +1,144 @@ +"""Protocol for the methods of the Agent and AgentSession classes that have +synchronous counterparts generated at runtime.""" + +from typing import TYPE_CHECKING, Optional, Protocol + +from synapseclient import Synapse + +if TYPE_CHECKING: + from synapseclient.models import Agent, AgentSession, AgentSessionAccessLevel + + +class AgentSessionSynchronousProtocol(Protocol): + """Protocol for the methods of the AgentSession class that have synchronous counterparts + generated at runtime.""" + + def start(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": + """Starts an agent session. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The new AgentSession object. + """ + return self + + def get(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": + """Gets an existing agent session. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The existing AgentSession object. + """ + return self + + def update(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": + """Updates an existing agent session. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The updated AgentSession object. + """ + return self + + def prompt(self, *, synapse_client: Optional[Synapse] = None) -> None: + """Sends a prompt to the agent and adds the response to the AgentSession's chat history. + + Arguments: + prompt: The prompt to send to the agent. + enable_trace: Whether to enable trace for the prompt. + print_response: Whether to print the response to the console. + newer_than: The timestamp to get trace results newer than. Defaults to None (all results). + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + """ + return None + + +class AgentSynchronousProtocol(Protocol): + """Protocol for the methods of the Agent class that have synchronous counterparts + generated at runtime.""" + + def register(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": + """Registers an agent with the Synapse API. If agent exists, it will be retrieved. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The registered or existing Agent object. + """ + return self + + def get(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": + """Gets an existing agent. + + Arguments: + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The existing Agent object. + """ + return self + + def start_session( + self, + access_level: Optional["AgentSessionAccessLevel"] = "PUBLICLY_ACCESSIBLE", + *, + synapse_client: Optional[Synapse] = None, + ) -> "AgentSession": + """Starts an agent session. + Adds the session to the Agent's sessions dictionary and sets it as the current session. + Arguments: + access_level: The access level of the agent session. + Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. + Defaults to PUBLICLY_ACCESSIBLE. + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + + Returns: + The new AgentSession object. + """ + return AgentSession() + + def get_session( + self, session_id: str, *, synapse_client: Optional[Synapse] = None + ) -> "AgentSession": + """Gets an existing agent session. + Adds the session to the Agent's sessions dictionary and sets it as the current session. + + Arguments: + session_id: The ID of the session to get. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. + + Returns: + The existing AgentSession object. + """ + return AgentSession() + + def prompt( + self, + prompt: str, + enable_trace: bool = False, + print_response: bool = False, + session: Optional["AgentSession"] = None, + newer_than: Optional[int] = None, + *, + synapse_client: Optional[Synapse] = None, + ) -> None: + """Sends a prompt to the agent for the current session. + If no session is currently active, a new session will be started. + + Arguments: + prompt: The prompt to send to the agent. + enable_trace: Whether to enable trace for the prompt. + print_response: Whether to print the response to the console. + session_id: The ID of the session to send the prompt to. If None, the current session will be used. + newer_than: The timestamp to get trace results newer than. Defaults to None (all results). + synapse_client: The Synapse client to use for the request. If None, the default client will be used. + """ + return None From 7f44822df6b36cf22baa08a3d429e93907251ff2 Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 14 Jan 2025 10:24:53 -0500 Subject: [PATCH 16/64] prevent cicular import in storable_entity_components --- synapseclient/models/services/storable_entity_components.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapseclient/models/services/storable_entity_components.py b/synapseclient/models/services/storable_entity_components.py index 8615cb9c9..a12d064bd 100644 --- a/synapseclient/models/services/storable_entity_components.py +++ b/synapseclient/models/services/storable_entity_components.py @@ -4,10 +4,9 @@ from synapseclient import Synapse from synapseclient.core.exceptions import SynapseError -from synapseclient.models import Annotations if TYPE_CHECKING: - from synapseclient.models import File, Folder, Project, Table + from synapseclient.models import File, Folder, Project, Table, Annotations class FailureStrategy(Enum): From 691799a7fd47876964df68175a968d6b6d0daabb Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 14 Jan 2025 10:27:04 -0500 Subject: [PATCH 17/64] remove promt sending and receiving from agent_service --- synapseclient/api/__init__.py | 4 -- synapseclient/api/agent_services.py | 82 ----------------------------- synapseclient/models/agent.py | 27 +++++----- 3 files changed, 15 insertions(+), 98 deletions(-) diff --git a/synapseclient/api/__init__.py b/synapseclient/api/__init__.py index fe94e0de5..f41f782fc 100644 --- a/synapseclient/api/__init__.py +++ b/synapseclient/api/__init__.py @@ -1,11 +1,9 @@ # These are all of the models that are used by the Synapse client. from .agent_services import ( get_agent, - get_response, get_session, get_trace, register_agent, - send_prompt, start_session, update_session, ) @@ -94,7 +92,5 @@ "start_session", "get_session", "update_session", - "send_prompt", - "get_response", "get_trace", ] diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index dfc7baddb..d8abaa1f9 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -150,88 +150,6 @@ async def update_session( ) -async def send_prompt( - id: str, - prompt: str, - enable_trace: bool = False, - synapse_client: Optional["Synapse"] = None, -) -> Dict[str, Any]: - """ - Sends a prompt to an agent starting an asyncronous job. - Sends a request matching - - - Arguments: - id: The ID of the session to send the prompt to. - prompt: The prompt to send to the agent. - enable_trace: Whether to enable trace for the prompt. Defaults to False. - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor. - - Returns: - The response matching - - """ - from synapseclient import Synapse - - client = Synapse.get_client(synapse_client=synapse_client) - - request = { - "concreteType": AGENT_CHAT_REQUEST, - "sessionId": id, - "chatText": prompt, - "enableTrace": enable_trace, - } - return await client.rest_post_async( - uri="/agent/chat/async/start", body=json.dumps(request) - ) - - -async def get_response( - prompt_id: str, - synapse_client: Optional["Synapse"] = None, -) -> Dict[str, Any]: - """ - Gets the response to a prompt. If the response is not ready, the endpoint will return a reponse matching - . - In this case, the response retrieval is retried every second until the response is ready or a timeout is reached. - - Arguments: - prompt_id: The token of the prompt to get the response for. - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor. - - Returns: - The response matching - - - Raises: - TimeoutError: If the response is not ready after 1 minute - """ - from synapseclient import Synapse - - client = Synapse.get_client(synapse_client=synapse_client) - # TODO: Create async compliant version of _waitForAsync and add this logic there - # synapseclient/core/async_utils.py - start_time = asyncio.get_event_loop().time() - TIMEOUT = 60 - - while True: - if asyncio.get_event_loop().time() - start_time > TIMEOUT: - raise SynapseTimeoutError( - f"Timeout waiting for response: {TIMEOUT} seconds" - ) - - response = await client.rest_get_async(uri=f"/agent/chat/async/get/{prompt_id}") - if response.get("jobState") == "PROCESSING": - await asyncio.sleep(1) - continue - - return response - - async def get_trace( prompt_id: str, newer_than: Optional[int] = None, diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 903a88f98..4c6d38210 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -6,11 +6,9 @@ from synapseclient import Synapse from synapseclient.api import ( get_agent, - get_response, get_session, get_trace, register_agent, - send_prompt, start_session, update_session, ) @@ -19,6 +17,9 @@ AgentSessionSynchronousProtocol, AgentSynchronousProtocol, ) +from synapseclient.models.mixins.asynchronous_job import AsynchronousJob + +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST class AgentType(str, Enum): @@ -60,6 +61,8 @@ class AgentPrompt: trace: The trace of the agent session. """ + concrete_type: str = AGENT_CHAT_REQUEST + id: Optional[str] = None """The unique ID of the agent prompt.""" @@ -76,7 +79,7 @@ class AgentPrompt: # TODO Add example usage to the docstring @dataclass @async_to_sync -class AgentSession(AgentSessionSynchronousProtocol): +class AgentSession(AgentSessionSynchronousProtocol, AsynchronousJob): """Represents a [Synapse Agent Session](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentSession.html) Attributes: @@ -94,9 +97,9 @@ class AgentSession(AgentSessionSynchronousProtocol): id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[ - AgentSessionAccessLevel - ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + access_level: Optional[AgentSessionAccessLevel] = ( + AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. @@ -223,16 +226,17 @@ async def prompt_async( newer_than: The timestamp to get trace results newer than. Defaults to None (all results). synapse_client: The Synapse client to use for the request. If None, the default client will be used. """ - prompt_response = await send_prompt( - id=self.id, + prompt_id = await self.send_job_async( + request_type=AGENT_CHAT_REQUEST, + session_id=self.id, prompt=prompt, enable_trace=enable_trace, synapse_client=synapse_client, ) - prompt_id = prompt_response["token"] - answer_response = await get_response( - prompt_id=prompt_id, + answer_response = await self.get_job_async( + job_id=prompt_id, + request_type=AGENT_CHAT_REQUEST, synapse_client=synapse_client, ) response = answer_response["responseText"] @@ -243,7 +247,6 @@ async def prompt_async( newer_than=newer_than, synapse_client=synapse_client, ) - # TODO: This is a bit hacky. Do we need to parse the page instead? trace = trace_response["page"][0]["message"] self.chat_history.append( From 389dd86a6595b64bd76a4f2da8c66589721a445a Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 14 Jan 2025 10:27:18 -0500 Subject: [PATCH 18/64] adds initial (dirty) async job mixin --- .../models/mixins/asynchronous_job.py | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 synapseclient/models/mixins/asynchronous_job.py diff --git a/synapseclient/models/mixins/asynchronous_job.py b/synapseclient/models/mixins/asynchronous_job.py new file mode 100644 index 000000000..5a50a3d93 --- /dev/null +++ b/synapseclient/models/mixins/asynchronous_job.py @@ -0,0 +1,281 @@ +import asyncio +import json +import time +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional, Dict, Any +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST + +from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError + +from enum import Enum + + +if TYPE_CHECKING: + from synapseclient import Synapse + + +class AsynchronousJobState(str, Enum): + """Enum representing the state of a Synapse Asynchronous Job: + + + - PROCESSING: The job is being processed. + - FAILED: The job has failed. + - COMPLETE: The job has been completed. + """ + + PROCESSING = "PROCESSING" + FAILED = "FAILED" + COMPLETE = "COMPLETE" + + +class CallersContext(str, Enum): + """Enum representing information about a web service call: + + + - SESSION_ID: Each web service request is issued a unique session ID (UUID) that is included in the call's access record. + Events that are triggered by a web service request should include the session ID so that they can be linked to + each other and the call's access record. + """ + + SESSION_ID = "SESSION_ID" + + +@dataclass +class AsynchronousJobStatus: + """Represents a Synapse Asynchronous Job Status object: + + + Attributes: + state: The state of the job. Either PROCESSING, FAILED, or COMPLETE. + canceling: Whether the job has been requested to be cancelled. + request_body: The body of an Asynchronous job request. Will be one of the models described here: + + response_body: The body of an Asynchronous job response. Will be one of the models described here: + + etag: The etag of the job status. Changes whenever the status changes. + id: The ID if the job issued when this job was started. + started_by_user_id: The ID of the user that started the job. + started_on: The date-time when the status was last changed to PROCESSING. + changed_on: The date-time when the status of this job was last changed. + progress_message: The current message of the progress tracker. + progress_current: A value indicating how much progress has been made. + I.e. a value of 50 indicates that 50% of the work has been + completed if progress_total is 100. + progress_total: A value indicating the total amount of work to complete. + exception: The exception that needs to be thrown if the job fails. + error_message: A one-line error message when the job fails. + error_details: Full stack trace of the error when the job fails. + runtime_ms: The number of milliseconds from the start to completion of this job. + callers_context: Contextual information about a web service call. + """ + + state: Optional["AsynchronousJobState"] = None + """The state of the job. Either PROCESSING, FAILED, or COMPLETE.""" + + canceling: Optional[bool] = False + """Whether the job has been requested to be cancelled.""" + + request_body: Optional[dict] = None + """The body of an Asynchronous job request. Will be one of the models described here: + """ + + response_body: Optional[dict] = None + """The body of an Asynchronous job response. Will be one of the models described here: + """ + + etag: Optional[str] = None + """The etag of the job status. Changes whenever the status changes.""" + + id: Optional[str] = None + """The ID if the job issued when this job was started.""" + + started_by_user_id: Optional[int] = None + """The ID of the user that started the job.""" + + started_on: Optional[str] = None + """The date-time when the status was last changed to PROCESSING.""" + + changed_on: Optional[str] = None + """The date-time when the status of this job was last changed.""" + + progress_message: Optional[str] = None + """The current message of the progress tracker.""" + + progress_current: Optional[int] = None + """A value indicating how much progress has been made. + I.e. a value of 50 indicates that 50% of the work has been + completed if progress_total is 100.""" + + progress_total: Optional[int] = None + """A value indicating the total amount of work to complete.""" + + exception: Optional[str] = None + """The exception that needs to be thrown if the job fails.""" + + error_message: Optional[str] = None + """A one-line error message when the job fails.""" + + error_details: Optional[str] = None + """Full stack trace of the error when the job fails.""" + + runtime_ms: Optional[int] = None + """The number of milliseconds from the start to completion of this job.""" + + callers_context: Optional["CallersContext"] = None + """Contextual information about a web service call.""" + + def fill_from_dict(self, async_job_status: dict) -> "AsynchronousJobStatus": + """Converts a response from the REST API into this dataclass. + + Arguments: + async_job_status: The response from the REST API. + + Returns: + A AsynchronousJobStatus object. + """ + self.state = async_job_status.get("jobState", None) + self.canceling = async_job_status.get("jobCanceling", None) + self.request_body = async_job_status.get("requestBody", None) + self.response_body = async_job_status.get("responseBody", None) + self.etag = async_job_status.get("etag", None) + self.id = async_job_status.get("jobId", None) + self.started_by_user_id = async_job_status.get("startedByUserId", None) + self.started_on = async_job_status.get("startedOn", None) + self.changed_on = async_job_status.get("changedOn", None) + self.progress_message = async_job_status.get("progressMessage", None) + self.progress_current = async_job_status.get("progressCurrent", None) + self.progress_total = async_job_status.get("progressTotal", None) + self.exception = async_job_status.get("exception", None) + self.error_message = async_job_status.get("errorMessage", None) + self.error_details = async_job_status.get("errorDetails", None) + self.runtime_ms = async_job_status.get("runtimeMs", None) + self.callers_context = async_job_status.get("callersContext", None) + return self + + +class AsynchronousJob: + """ + Mixin for objects that can have Asynchronous Jobs. + """ + + ASYNC_JOB_URIS = { + AGENT_CHAT_REQUEST: "/agent/chat/async", + } + + async def send_job_async( + self, + request_type: str, + session_id: str, + prompt: str, + enable_trace: bool, + synapse_client: Optional["Synapse"] = None, + ) -> str: + """ + Sends the job to the Synapse API. Request body matches: + + Returns the job ID. + + Arguments: + request_type: The type of the job. + session_id: The ID of the session to send the prompt to. + prompt: The prompt to send to the agent. + enable_trace: Whether to enable trace for the prompt. Defaults to False. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The job ID retrieved from the response. + + """ + request = { + "concreteType": request_type, + "sessionId": session_id, + "chatText": prompt, + "enableTrace": enable_trace, + } + response = await synapse_client.rest_post_async( + uri=f"{self.ASYNC_JOB_URIS[request_type]}/start", body=json.dumps(request) + ) + return response["token"] + + async def get_job_async( + self, + job_id: str, + request_type: str, + synapse_client: "Synapse", + endpoint: str = None, + ) -> Dict[str, Any]: + """ + Gets the job from the server using its ID. Handles progress tracking, failures and timeouts. + + Arguments: + job_id: The ID of the job to get. + request_type: The type of the job. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + endpoint: The endpoint to use for the request. Defaults to None. + + Returns: + The response body matching + + + Raises: + SynapseError: If the job fails. + SynapseTimeoutError: If the job does not complete within the timeout. + """ + start_time = asyncio.get_event_loop().time() + SLEEP = 1 + TIMEOUT = 60 + + last_message = "" + last_progress = 0 + last_total = 1 + progressed = False + + while asyncio.get_event_loop().time() - start_time < TIMEOUT: + result = await synapse_client.rest_get_async( + uri=f"{self.ASYNC_JOB_URIS[request_type]}/get/{job_id}", + endpoint=endpoint, + ) + job_status = AsynchronousJobStatus().fill_from_dict(async_job_status=result) + if job_status.state == AsynchronousJobState.PROCESSING: + # TODO: Is this adequate to determine if the endpoint tracks progress? + progress_tracking = any( + [ + job_status.progress_message, + job_status.progress_current, + job_status.progress_total, + ] + ) + progressed = ( + job_status.progress_message != last_message + or last_progress != job_status.progress_current + ) + if progress_tracking and progressed: + last_message = job_status.progress_message + last_progress = job_status.progress_current + last_total = job_status.progress_total + + synapse_client._print_transfer_progress( + last_progress, + last_total, + prefix=last_message, + isBytes=False, + ) + start_time = asyncio.get_event_loop().time() + await asyncio.sleep(SLEEP) + elif job_status.state == AsynchronousJobState.FAILED: + raise SynapseError( + f"{job_status.error_message}\n{job_status.error_details}", + async_job_status=job_status.id, + ) + else: + break + else: + raise SynapseTimeoutError( + f"Timeout waiting for query results: {time.time() - start_time} seconds" + ) + + return result From 15db364a6450e912a251e67e4d9a1cfb8622e7a8 Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 14 Jan 2025 10:28:12 -0500 Subject: [PATCH 19/64] pre-commit run --- synapseclient/api/agent_services.py | 4 ---- synapseclient/models/agent.py | 11 +++++------ synapseclient/models/mixins/asynchronous_job.py | 8 +++----- .../models/services/storable_entity_components.py | 2 +- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index d8abaa1f9..57fdb84c0 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -2,16 +2,12 @@ """ -import asyncio import json from typing import TYPE_CHECKING, Any, Dict, Optional if TYPE_CHECKING: from synapseclient import Synapse -from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST -from synapseclient.core.exceptions import SynapseTimeoutError - async def register_agent( cloud_agent_id: str, diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 4c6d38210..fdd389c36 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -13,13 +13,12 @@ update_session, ) from synapseclient.core.async_utils import async_to_sync, otel_trace_method +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST +from synapseclient.models.mixins.asynchronous_job import AsynchronousJob from synapseclient.models.protocols.agent_protocol import ( AgentSessionSynchronousProtocol, AgentSynchronousProtocol, ) -from synapseclient.models.mixins.asynchronous_job import AsynchronousJob - -from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST class AgentType(str, Enum): @@ -97,9 +96,9 @@ class AgentSession(AgentSessionSynchronousProtocol, AsynchronousJob): id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[AgentSessionAccessLevel] = ( - AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. diff --git a/synapseclient/models/mixins/asynchronous_job.py b/synapseclient/models/mixins/asynchronous_job.py index 5a50a3d93..1712eab8c 100644 --- a/synapseclient/models/mixins/asynchronous_job.py +++ b/synapseclient/models/mixins/asynchronous_job.py @@ -2,13 +2,11 @@ import json import time from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional, Dict, Any -from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST - -from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError - from enum import Enum +from typing import TYPE_CHECKING, Any, Dict, Optional +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST +from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError if TYPE_CHECKING: from synapseclient import Synapse diff --git a/synapseclient/models/services/storable_entity_components.py b/synapseclient/models/services/storable_entity_components.py index a12d064bd..9f5d52c05 100644 --- a/synapseclient/models/services/storable_entity_components.py +++ b/synapseclient/models/services/storable_entity_components.py @@ -6,7 +6,7 @@ from synapseclient.core.exceptions import SynapseError if TYPE_CHECKING: - from synapseclient.models import File, Folder, Project, Table, Annotations + from synapseclient.models import Annotations, File, Folder, Project, Table class FailureStrategy(Enum): From 165570c08ccf605237a36984bf55d783b0725b88 Mon Sep 17 00:00:00 2001 From: BryanFauble <17128019+BryanFauble@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:59:00 -0700 Subject: [PATCH 20/64] [SYNPY-1544] potential changes to mixin (#1153) * Changes for async mixin * Remove arg * bug fix * generalizes send_job_and_wait_async * removes typing.Self --------- Co-authored-by: bwmac --- synapseclient/models/agent.py | 131 ++++--- .../models/mixins/asynchronous_job.py | 332 ++++++++++++------ 2 files changed, 304 insertions(+), 159 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index fdd389c36..117f61a87 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -14,7 +14,7 @@ ) from synapseclient.core.async_utils import async_to_sync, otel_trace_method from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST -from synapseclient.models.mixins.asynchronous_job import AsynchronousJob +from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator from synapseclient.models.protocols.agent_protocol import ( AgentSessionSynchronousProtocol, AgentSynchronousProtocol, @@ -50,13 +50,15 @@ class AgentSessionAccessLevel(str, Enum): @dataclass -class AgentPrompt: +class AgentPrompt(AsynchronousCommunicator): """Represents a prompt, response, and metadata within an AgentSession. Attributes: id: The unique ID of the agent prompt. + session_id: The ID of the session that the prompt is associated with. prompt: The prompt to send to the agent. response: The response from the agent. + enable_trace: Whether tracing is enabled for the prompt. trace: The trace of the agent session. """ @@ -65,20 +67,68 @@ class AgentPrompt: id: Optional[str] = None """The unique ID of the agent prompt.""" + session_id: Optional[str] = None + """The ID of the session that the prompt is associated with.""" + prompt: Optional[str] = None """The prompt sent to the agent.""" response: Optional[str] = None """The response from the agent.""" + enable_trace: Optional[bool] = False + """Whether tracing is enabled for the prompt.""" + trace: Optional[str] = None """The trace or "thought process" of the agent when responding to the prompt.""" + def to_synapse_request(self): + """Converts the request to a request expected of the Synapse REST API.""" + return { + "concreteType": self.concrete_type, + "sessionId": self.session_id, + "chatText": self.prompt, + "enableTrace": self.enable_trace, + } + + def fill_from_dict(self, synapse_response: Dict[str, str]) -> "AgentPrompt": + """ + Converts a response from the REST API into this dataclass. + + Arguments: + agent_prompt: The response from the REST API. + + Returns: + The AgentPrompt object. + """ + self.id = synapse_response.get("jobId", None) + self.session_id = synapse_response.get("sessionId", None) + self.response = synapse_response.get("responseText", None) + return self + + async def _post_exchange_async( + self, *, synapse_client: Optional[Synapse] = None, **kwargs + ) -> None: + """Retrieves information about the trace of this prompt with the agent. + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + """ + if self.enable_trace: + trace_response = await get_trace( + prompt_id=self.id, + newer_than=kwargs.get("newer_than", None), + synapse_client=synapse_client, + ) + self.trace = trace_response["page"][0]["message"] + # TODO Add example usage to the docstring @dataclass @async_to_sync -class AgentSession(AgentSessionSynchronousProtocol, AsynchronousJob): +class AgentSession(AgentSessionSynchronousProtocol): """Represents a [Synapse Agent Session](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentSession.html) Attributes: @@ -150,7 +200,9 @@ async def start_async( """Starts an agent session. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The new AgentSession object. @@ -171,7 +223,9 @@ async def get_async( """Gets an agent session. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The retrieved AgentSession object. @@ -194,7 +248,9 @@ async def update_async( Only updates to the access level are currently supported. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The updated AgentSession object. @@ -223,45 +279,24 @@ async def prompt_async( enable_trace: Whether to enable trace for the prompt. print_response: Whether to print the response to the console. newer_than: The timestamp to get trace results newer than. Defaults to None (all results). - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. """ - prompt_id = await self.send_job_async( - request_type=AGENT_CHAT_REQUEST, - session_id=self.id, - prompt=prompt, - enable_trace=enable_trace, - synapse_client=synapse_client, - ) - answer_response = await self.get_job_async( - job_id=prompt_id, - request_type=AGENT_CHAT_REQUEST, - synapse_client=synapse_client, + agent_prompt = AgentPrompt( + prompt=prompt, session_id=self.id, enable_trace=enable_trace ) - response = answer_response["responseText"] - - if enable_trace: - trace_response = await get_trace( - prompt_id=prompt_id, - newer_than=newer_than, - synapse_client=synapse_client, - ) - trace = trace_response["page"][0]["message"] - - self.chat_history.append( - AgentPrompt( - id=prompt_id, - prompt=prompt, - response=response, - trace=trace, - ) + await agent_prompt.send_job_and_wait_async( + synapse_client=synapse_client, post_exchange_args={"newer_than": newer_than} ) + self.chat_history.append(agent_prompt) if print_response: print(f"PROMPT:\n{prompt}\n") - print(f"RESPONSE:\n{response}\n") + print(f"RESPONSE:\n{agent_prompt.response}\n") if enable_trace: - print(f"TRACE:\n{trace}") + print(f"TRACE:\n{agent_prompt.trace}") # TODO Add example usage to the docstring @@ -328,7 +363,9 @@ async def register_async( """Registers an agent with the Synapse API. If agent exists, it will be retrieved. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The registered or existing Agent object. @@ -348,7 +385,9 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent """Gets an existing agent. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The existing Agent object. @@ -378,8 +417,9 @@ async def start_session_async( access_level: The access level of the agent session. Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The new AgentSession object. @@ -403,8 +443,9 @@ async def get_session_async( Arguments: session_id: The ID of the session to get. - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The existing AgentSession object. @@ -439,7 +480,9 @@ async def prompt_async( print_response: Whether to print the response to the console. session_id: The ID of the session to send the prompt to. If None, the current session will be used. newer_than: The timestamp to get trace results newer than. Defaults to None (all results). - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. """ # TODO: Iron this out. Make sure we cover all cases. if session: diff --git a/synapseclient/models/mixins/asynchronous_job.py b/synapseclient/models/mixins/asynchronous_job.py index 1712eab8c..098994f2d 100644 --- a/synapseclient/models/mixins/asynchronous_job.py +++ b/synapseclient/models/mixins/asynchronous_job.py @@ -3,13 +3,87 @@ import time from dataclasses import dataclass from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import Any, Dict, Optional +from synapseclient import Synapse from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError -if TYPE_CHECKING: - from synapseclient import Synapse +ASYNC_JOB_URIS = { + AGENT_CHAT_REQUEST: "/agent/chat/async", +} + + +class AsynchronousCommunicator: + """Mixin to handle communication with the Synapse Asynchronous Job service.""" + + def to_synapse_request(self) -> None: + """Converts the request to a request expected of the Synapse REST API. + + This is a placeholder for any additional logic that needs to be run before the exchange with Synapse. + It must be overridden by subclasses if needed. + """ + raise NotImplementedError("to_synapse_request must be implemented.") + + def fill_from_dict( + self, synapse_response: Dict[str, str] + ) -> "AsynchronousCommunicator": + """ + Converts a response from the REST API into this dataclass. + + This is a placeholder for any additional logic that needs to be run after the exchange with Synapse. + It must be overridden by subclasses if needed. + + Arguments: + synapse_response: The response from the REST API. + + Returns: + An instance of this class. + """ + raise NotImplementedError("fill_from_dict must be implemented.") + + async def _post_exchange_async( + self, synapse_client: Optional[Synapse] = None, **kwargs + ) -> None: + """Any additional logic to run after the exchange with Synapse. + + This is a placeholder for any additional logic that needs to be run after the exchange with Synapse. + It must be overridden by subclasses if needed. + + Arguments: + synapse_client: The Synapse client to use for the request. + **kwargs: Additional arguments to pass to the request. + """ + pass + + async def send_job_and_wait_async( + self, + post_exchange_args: Optional[Dict[str, Any]] = None, + *, + synapse_client: Optional[Synapse] = None, + ) -> "AsynchronousCommunicator": + """Send the job to the Asynchronous Job service and wait for it to complete. + + This is a placeholder for any additional logic that needs to be run after the exchange with Synapse. + It must be overridden by subclasses if needed. + + Arguments: + post_exchange_args: Additional arguments to pass to the request. + synapse_client: The Synapse client to use for the request. + + Returns: + An instance of this class. + """ + result = await send_job_and_wait_async( + request=self.to_synapse_request(), + request_type=self.concrete_type, + synapse_client=synapse_client, + ) + self.fill_from_dict(synapse_response=result) + await self._post_exchange_async( + **post_exchange_args, synapse_client=synapse_client + ) + return self class AsynchronousJobState(str, Enum): @@ -151,129 +225,157 @@ def fill_from_dict(self, async_job_status: dict) -> "AsynchronousJobStatus": return self -class AsynchronousJob: +async def send_job_and_wait_async( + request: Dict[str, Any], + request_type: str, + endpoint: str = None, + *, + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: """ - Mixin for objects that can have Asynchronous Jobs. + Sends the job to the Synapse API and waits for the response. Request body matches: + + + Arguments: + request: A request matching . + endpoint: The endpoint to use for the request. Defaults to None. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The response body matching + + + Raises: + SynapseError: If the job fails. + SynapseTimeoutError: If the job does not complete within the timeout. """ - - ASYNC_JOB_URIS = { - AGENT_CHAT_REQUEST: "/agent/chat/async", + job_id = await send_job_async(request=request, synapse_client=synapse_client) + return { + "jobId": job_id, + **await get_job_async( + job_id=job_id, + request_type=request_type, + synapse_client=synapse_client, + endpoint=endpoint, + ), } - async def send_job_async( - self, - request_type: str, - session_id: str, - prompt: str, - enable_trace: bool, - synapse_client: Optional["Synapse"] = None, - ) -> str: - """ - Sends the job to the Synapse API. Request body matches: - - Returns the job ID. - Arguments: - request_type: The type of the job. - session_id: The ID of the session to send the prompt to. - prompt: The prompt to send to the agent. - enable_trace: Whether to enable trace for the prompt. Defaults to False. - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor. +async def send_job_async( + request: Dict[str, Any], + *, + synapse_client: Optional["Synapse"] = None, +) -> str: + """ + Sends the job to the Synapse API. Request body matches: + + Returns the job ID. + + Arguments: + request: A request matching . + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The job ID retrieved from the response. + + """ + if not request: + raise ValueError("request must be provided.") - Returns: - The job ID retrieved from the response. - - """ - request = { - "concreteType": request_type, - "sessionId": session_id, - "chatText": prompt, - "enableTrace": enable_trace, - } - response = await synapse_client.rest_post_async( - uri=f"{self.ASYNC_JOB_URIS[request_type]}/start", body=json.dumps(request) - ) - return response["token"] + request_type = request.get("concreteType") - async def get_job_async( - self, - job_id: str, - request_type: str, - synapse_client: "Synapse", - endpoint: str = None, - ) -> Dict[str, Any]: - """ - Gets the job from the server using its ID. Handles progress tracking, failures and timeouts. + if not request_type or request_type not in ASYNC_JOB_URIS: + raise ValueError(f"Unsupported request type: {request_type}") - Arguments: - job_id: The ID of the job to get. - request_type: The type of the job. - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor. - endpoint: The endpoint to use for the request. Defaults to None. + client = Synapse.get_client(synapse_client=synapse_client) + response = await client.rest_post_async( + uri=f"{ASYNC_JOB_URIS[request_type]}/start", body=json.dumps(request) + ) + return response["token"] - Returns: - The response body matching - - Raises: - SynapseError: If the job fails. - SynapseTimeoutError: If the job does not complete within the timeout. - """ - start_time = asyncio.get_event_loop().time() - SLEEP = 1 - TIMEOUT = 60 - - last_message = "" - last_progress = 0 - last_total = 1 - progressed = False - - while asyncio.get_event_loop().time() - start_time < TIMEOUT: - result = await synapse_client.rest_get_async( - uri=f"{self.ASYNC_JOB_URIS[request_type]}/get/{job_id}", - endpoint=endpoint, +async def get_job_async( + job_id: str, + request_type: str, + endpoint: str = None, + *, + synapse_client: Optional["Synapse"] = None, +) -> Dict[str, Any]: + """ + Gets the job from the server using its ID. Handles progress tracking, failures and timeouts. + + Arguments: + job_id: The ID of the job to get. + request_type: The type of the job. + endpoint: The endpoint to use for the request. Defaults to None. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. + + Returns: + The response body matching + + + Raises: + SynapseError: If the job fails. + SynapseTimeoutError: If the job does not complete within the timeout. + """ + client = Synapse.get_client(synapse_client=synapse_client) + start_time = asyncio.get_event_loop().time() + SLEEP = 1 + TIMEOUT = 60 + + last_message = "" + last_progress = 0 + last_total = 1 + progressed = False + + while asyncio.get_event_loop().time() - start_time < TIMEOUT: + result = await client.rest_get_async( + uri=f"{ASYNC_JOB_URIS[request_type]}/get/{job_id}", + endpoint=endpoint, + ) + job_status = AsynchronousJobStatus().fill_from_dict(async_job_status=result) + if job_status.state == AsynchronousJobState.PROCESSING: + # TODO: Is this adequate to determine if the endpoint tracks progress? + progress_tracking = any( + [ + job_status.progress_message, + job_status.progress_current, + job_status.progress_total, + ] ) - job_status = AsynchronousJobStatus().fill_from_dict(async_job_status=result) - if job_status.state == AsynchronousJobState.PROCESSING: - # TODO: Is this adequate to determine if the endpoint tracks progress? - progress_tracking = any( - [ - job_status.progress_message, - job_status.progress_current, - job_status.progress_total, - ] - ) - progressed = ( - job_status.progress_message != last_message - or last_progress != job_status.progress_current - ) - if progress_tracking and progressed: - last_message = job_status.progress_message - last_progress = job_status.progress_current - last_total = job_status.progress_total - - synapse_client._print_transfer_progress( - last_progress, - last_total, - prefix=last_message, - isBytes=False, - ) - start_time = asyncio.get_event_loop().time() - await asyncio.sleep(SLEEP) - elif job_status.state == AsynchronousJobState.FAILED: - raise SynapseError( - f"{job_status.error_message}\n{job_status.error_details}", - async_job_status=job_status.id, + progressed = ( + job_status.progress_message != last_message + or last_progress != job_status.progress_current + ) + if progress_tracking and progressed: + last_message = job_status.progress_message + last_progress = job_status.progress_current + last_total = job_status.progress_total + + client._print_transfer_progress( + last_progress, + last_total, + prefix=last_message, + isBytes=False, ) - else: - break - else: - raise SynapseTimeoutError( - f"Timeout waiting for query results: {time.time() - start_time} seconds" + start_time = asyncio.get_event_loop().time() + await asyncio.sleep(SLEEP) + elif job_status.state == AsynchronousJobState.FAILED: + raise SynapseError( + f"{job_status.error_message}\n{job_status.error_details}", + async_job_status=job_status.id, ) + else: + break + else: + raise SynapseTimeoutError( + f"Timeout waiting for query results: {time.time() - start_time} seconds" + ) - return result + return result From 8adf5e5325b5d612c83f50d79621afa31932f7c8 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 15 Jan 2025 12:08:00 -0500 Subject: [PATCH 21/64] cleans up agent logic --- synapseclient/models/agent.py | 22 ++++++++----------- .../models/mixins/asynchronous_job.py | 16 +------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 117f61a87..a5ddf58bc 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -24,7 +24,7 @@ class AgentType(str, Enum): """ Enum representing the type of agent as defined in - + - BASELINE is a default agent provided by Synapse. - CUSTOM is a custom agent that has been registered by a user. @@ -37,7 +37,7 @@ class AgentType(str, Enum): class AgentSessionAccessLevel(str, Enum): """ Enum representing the access level of the agent session as defined in - + - PUBLICLY_ACCESSIBLE: The agent can only access publicly accessible data. - READ_YOUR_PRIVATE_DATA: The agent can read the user's private data. @@ -96,7 +96,7 @@ def fill_from_dict(self, synapse_response: Dict[str, str]) -> "AgentPrompt": Converts a response from the REST API into this dataclass. Arguments: - agent_prompt: The response from the REST API. + synapse_response: The response from the REST API. Returns: The AgentPrompt object. @@ -146,9 +146,9 @@ class AgentSession(AgentSessionSynchronousProtocol): id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[ - AgentSessionAccessLevel - ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + access_level: Optional[AgentSessionAccessLevel] = ( + AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. @@ -484,14 +484,10 @@ async def prompt_async( `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. """ - # TODO: Iron this out. Make sure we cover all cases. if session: - if session.id not in self.sessions: - await self.get_session_async( - session_id=session.id, synapse_client=synapse_client - ) - else: - self.current_session = session + await self.get_session_async( + session_id=session.id, synapse_client=synapse_client + ) else: if not self.current_session: await self.start_session_async(synapse_client=synapse_client) diff --git a/synapseclient/models/mixins/asynchronous_job.py b/synapseclient/models/mixins/asynchronous_job.py index 098994f2d..8271451ee 100644 --- a/synapseclient/models/mixins/asynchronous_job.py +++ b/synapseclient/models/mixins/asynchronous_job.py @@ -18,11 +18,7 @@ class AsynchronousCommunicator: """Mixin to handle communication with the Synapse Asynchronous Job service.""" def to_synapse_request(self) -> None: - """Converts the request to a request expected of the Synapse REST API. - - This is a placeholder for any additional logic that needs to be run before the exchange with Synapse. - It must be overridden by subclasses if needed. - """ + """Converts the request to a request expected of the Synapse REST API.""" raise NotImplementedError("to_synapse_request must be implemented.") def fill_from_dict( @@ -31,9 +27,6 @@ def fill_from_dict( """ Converts a response from the REST API into this dataclass. - This is a placeholder for any additional logic that needs to be run after the exchange with Synapse. - It must be overridden by subclasses if needed. - Arguments: synapse_response: The response from the REST API. @@ -47,9 +40,6 @@ async def _post_exchange_async( ) -> None: """Any additional logic to run after the exchange with Synapse. - This is a placeholder for any additional logic that needs to be run after the exchange with Synapse. - It must be overridden by subclasses if needed. - Arguments: synapse_client: The Synapse client to use for the request. **kwargs: Additional arguments to pass to the request. @@ -64,9 +54,6 @@ async def send_job_and_wait_async( ) -> "AsynchronousCommunicator": """Send the job to the Asynchronous Job service and wait for it to complete. - This is a placeholder for any additional logic that needs to be run after the exchange with Synapse. - It must be overridden by subclasses if needed. - Arguments: post_exchange_args: Additional arguments to pass to the request. synapse_client: The Synapse client to use for the request. @@ -341,7 +328,6 @@ async def get_job_async( ) job_status = AsynchronousJobStatus().fill_from_dict(async_job_status=result) if job_status.state == AsynchronousJobState.PROCESSING: - # TODO: Is this adequate to determine if the endpoint tracks progress? progress_tracking = any( [ job_status.progress_message, From 336039087b2fa13e11e85d3d1443b25473c1ba9e Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 15 Jan 2025 13:31:07 -0500 Subject: [PATCH 22/64] adds async job unit tests --- .../models/mixins/asynchronous_job.py | 14 +- .../async/unit_test_asynchronous_job.py | 279 ++++++++++++++++++ 2 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py diff --git a/synapseclient/models/mixins/asynchronous_job.py b/synapseclient/models/mixins/asynchronous_job.py index 8271451ee..a759cfe7d 100644 --- a/synapseclient/models/mixins/asynchronous_job.py +++ b/synapseclient/models/mixins/asynchronous_job.py @@ -289,6 +289,8 @@ async def get_job_async( job_id: str, request_type: str, endpoint: str = None, + sleep: int = 1, + timeout: int = 60, *, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: @@ -299,6 +301,9 @@ async def get_job_async( job_id: The ID of the job to get. request_type: The type of the job. endpoint: The endpoint to use for the request. Defaults to None. + sleep: The number of seconds to wait between requests. Defaults to 1. + timeout: The number of seconds to wait for the job to complete or progress + before raising a SynapseTimeoutError. Defaults to 60. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -309,19 +314,17 @@ async def get_job_async( Raises: SynapseError: If the job fails. - SynapseTimeoutError: If the job does not complete within the timeout. + SynapseTimeoutError: If the job does not complete or progress within the timeout interval. """ client = Synapse.get_client(synapse_client=synapse_client) start_time = asyncio.get_event_loop().time() - SLEEP = 1 - TIMEOUT = 60 last_message = "" last_progress = 0 last_total = 1 progressed = False - while asyncio.get_event_loop().time() - start_time < TIMEOUT: + while asyncio.get_event_loop().time() - start_time < timeout: result = await client.rest_get_async( uri=f"{ASYNC_JOB_URIS[request_type]}/get/{job_id}", endpoint=endpoint, @@ -351,11 +354,10 @@ async def get_job_async( isBytes=False, ) start_time = asyncio.get_event_loop().time() - await asyncio.sleep(SLEEP) + await asyncio.sleep(sleep) elif job_status.state == AsynchronousJobState.FAILED: raise SynapseError( f"{job_status.error_message}\n{job_status.error_details}", - async_job_status=job_status.id, ) else: break diff --git a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py new file mode 100644 index 000000000..63fabd1da --- /dev/null +++ b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py @@ -0,0 +1,279 @@ +"""Unit tests for Asynchronous Job logic.""" + +import pytest + +import asyncio +import json + +from synapseclient import Synapse + +from synapseclient.models.mixins import asynchronous_job +from synapseclient.models.mixins.asynchronous_job import ( + send_job_async, + get_job_async, + send_job_and_wait_async, + AsynchronousJobStatus, + AsynchronousJobState, + ASYNC_JOB_URIS, +) +from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError + +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST + +from unittest.mock import patch + + +class TestSendJobAsync: + """Unit tests for send_job_async.""" + + good_request = {"concreteType": AGENT_CHAT_REQUEST} + bad_request_no_concrete_type = {"otherKey": "otherValue"} + bad_request_invalid_concrete_type = {"concreteType": "InvalidConcreteType"} + request_type = AGENT_CHAT_REQUEST + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + async def test_send_job_async_when_request_is_missing(self) -> None: + with pytest.raises(ValueError, match="request must be provided."): + # WHEN I call send_job_async without a request + # THEN I should get a ValueError + await send_job_async(request=None) + + async def test_send_job_async_when_request_is_missing_concrete_type(self) -> None: + with pytest.raises(ValueError, match="Unsupported request type: None"): + # GIVEN a request with no concrete type + # WHEN I call send_job_async + # THEN I should get a ValueError + await send_job_async(request=self.bad_request_no_concrete_type) + + async def test_send_job_async_when_request_is_invalid_concrete_type(self) -> None: + with pytest.raises( + ValueError, match="Unsupported request type: InvalidConcreteType" + ): + # GIVEN a request with an invalid concrete type + # WHEN I call send_job_async + # THEN I should get a ValueError + await send_job_async(request=self.bad_request_invalid_concrete_type) + + async def test_send_job_async_when_request_is_valid(self) -> None: + with ( + patch.object( + Synapse, "get_client", return_value=self.syn + ) as mock_get_client, + patch.object( + Synapse, + "rest_post_async", + return_value={"token": "123"}, + ) as mock_rest_post_async, + ): + # WHEN I call send_job_async with a good request + job_id = await send_job_async( + request=self.good_request, synapse_client=self.syn + ) + # THEN the return value should be the token + assert job_id == "123" + # AND get_client should have been called + mock_get_client.assert_called_once_with(synapse_client=self.syn) + # AND rest_post_async should have been called with the correct arguments + mock_rest_post_async.assert_called_once_with( + uri=f"{ASYNC_JOB_URIS[self.request_type]}/start", + body=json.dumps(self.good_request), + ) + + +class TestGetJobAsync: + """Unit tests for get_job_async.""" + + request_type = AGENT_CHAT_REQUEST + job_id = "123" + + processing_job_status = AsynchronousJobStatus( + state=AsynchronousJobState.PROCESSING, + progress_message="Processing", + progress_current=1, + progress_total=100, + ) + failed_job_status = AsynchronousJobStatus( + state=AsynchronousJobState.FAILED, + progress_message="Failed", + progress_current=1, + progress_total=100, + error_message="Error", + error_details="Details", + id="123", + ) + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + async def test_get_job_async_when_job_fails(self) -> None: + with ( + patch.object( + Synapse, + "rest_get_async", + return_value={}, + ) as mock_rest_get_async, + patch.object( + AsynchronousJobStatus, + "fill_from_dict", + return_value=self.failed_job_status, + ) as mock_fill_from_dict, + ): + with pytest.raises( + SynapseError, + match=f"{self.failed_job_status.error_message}\n{self.failed_job_status.error_details}", + ): + # WHEN I call get_job_async + # AND the job fails in the Synapse API + # THEN I should get a SynapseError with the error message and details + await get_job_async( + job_id="123", + request_type=AGENT_CHAT_REQUEST, + synapse_client=self.syn, + sleep=1, + timeout=60, + endpoint=None, + ) + # AND rest_get_async should have been called once with the correct arguments + mock_rest_get_async.assert_called_once_with( + uri=f"{ASYNC_JOB_URIS[AGENT_CHAT_REQUEST]}/get/{self.job_id}", + endpoint=None, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + async_job_status=mock_rest_get_async.return_value, + ) + + async def test_get_job_async_when_job_times_out(self) -> None: + with ( + patch.object( + Synapse, + "rest_get_async", + return_value={}, + ) as mock_rest_get_async, + patch.object( + AsynchronousJobStatus, + "fill_from_dict", + return_value=self.processing_job_status, + ) as mock_fill_from_dict, + ): + with pytest.raises( + SynapseTimeoutError, match="Timeout waiting for query results:" + ): + # WHEN I call get_job_async + # AND the job does not complete or progress within the timeout interval + # THEN I should get a SynapseTimeoutError + await get_job_async( + job_id=self.job_id, + request_type=self.request_type, + synapse_client=self.syn, + endpoint=None, + timeout=0, + sleep=1, + ) + # AND rest_get_async should not have been called + mock_rest_get_async.assert_not_called() + # AND fill_from_dict should not have been called + mock_fill_from_dict.assert_not_called() + + +class TestSendJobAndWaitAsync: + """Unit tests for send_job_and_wait_async.""" + + good_request = {"concreteType": AGENT_CHAT_REQUEST} + job_id = "123" + request_type = AGENT_CHAT_REQUEST + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + async def test_send_job_and_wait_async(self) -> None: + with ( + patch.object( + asynchronous_job, + "send_job_async", + return_value=self.job_id, + ) as mock_send_job_async, + patch.object( + asynchronous_job, + "get_job_async", + return_value={ + "key": "value", + }, + ) as mock_get_job_async, + ): + # WHEN I call send_job_and_wait_async with a good request + # THEN the return value should be a dictionary with the job ID + # and response key value pair(s) + assert await send_job_and_wait_async( + request=self.good_request, + request_type=self.request_type, + synapse_client=self.syn, + endpoint=None, + ) == { + "jobId": self.job_id, + "key": "value", + } + # AND send_job_async should have been called once with the correct arguments + mock_send_job_async.assert_called_once_with( + request=self.good_request, + synapse_client=self.syn, + ) + # AND get_job_async should have been called once with the correct arguments + mock_get_job_async.assert_called_once_with( + job_id=self.job_id, + request_type=self.request_type, + synapse_client=self.syn, + endpoint=None, + ) + + +class TestAsynchronousJobStatus: + """Unit tests for AsynchronousJobStatus.""" + + def test_fill_from_dict(self) -> None: + # GIVEN a dictionary with job status information + async_job_status_dict = { + "jobState": AsynchronousJobState.PROCESSING, + "jobCanceling": False, + "requestBody": {"key": "value"}, + "responseBody": {"key": "value"}, + "etag": "123", + "jobId": "123", + "startedByUserId": "123", + "startedOn": "123", + "changedOn": "123", + "progressMessage": "Processing", + "progressCurrent": 1, + "progressTotal": 100, + "exception": None, + "errorMessage": None, + "errorDetails": None, + "runtimeMs": 1000, + "callersContext": None, + } + # WHEN I call fill_from_dict on it + async_job_status = AsynchronousJobStatus().fill_from_dict(async_job_status_dict) + # THEN the resulting AsynchronousJobStatus object + # should have the correct attribute values + assert async_job_status.state == AsynchronousJobState.PROCESSING + assert async_job_status.canceling is False + assert async_job_status.request_body == {"key": "value"} + assert async_job_status.response_body == {"key": "value"} + assert async_job_status.etag == "123" + assert async_job_status.id == "123" + assert async_job_status.started_by_user_id == "123" + assert async_job_status.started_on == "123" + assert async_job_status.changed_on == "123" + assert async_job_status.progress_message == "Processing" + assert async_job_status.progress_current == 1 + assert async_job_status.progress_total == 100 + assert async_job_status.exception is None + assert async_job_status.error_message is None + assert async_job_status.error_details is None + assert async_job_status.runtime_ms == 1000 + assert async_job_status.callers_context is None From 40842b21c55784cac222d869076007302d81442e Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 15 Jan 2025 15:33:16 -0500 Subject: [PATCH 23/64] updates async job tests --- .../async/unit_test_asynchronous_job.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py index 63fabd1da..a62443988 100644 --- a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py +++ b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py @@ -20,7 +20,7 @@ from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST -from unittest.mock import patch +from unittest.mock import patch, AsyncMock class TestSendJobAsync: @@ -59,12 +59,13 @@ async def test_send_job_async_when_request_is_invalid_concrete_type(self) -> Non async def test_send_job_async_when_request_is_valid(self) -> None: with ( - patch.object( - Synapse, "get_client", return_value=self.syn + patch( + "synapseclient.Synapse.get_client", + return_value=self.syn, ) as mock_get_client, - patch.object( - Synapse, - "rest_post_async", + patch( + "synapseclient.Synapse.rest_post_async", + new_callable=AsyncMock, return_value={"token": "123"}, ) as mock_rest_post_async, ): @@ -111,9 +112,9 @@ def init_syn(self, syn: Synapse) -> None: async def test_get_job_async_when_job_fails(self) -> None: with ( - patch.object( - Synapse, - "rest_get_async", + patch( + "synapseclient.Synapse.rest_get_async", + new_callable=AsyncMock, return_value={}, ) as mock_rest_get_async, patch.object( @@ -149,9 +150,9 @@ async def test_get_job_async_when_job_fails(self) -> None: async def test_get_job_async_when_job_times_out(self) -> None: with ( - patch.object( - Synapse, - "rest_get_async", + patch( + "synapseclient.Synapse.rest_get_async", + new_callable=AsyncMock, return_value={}, ) as mock_rest_get_async, patch.object( @@ -193,14 +194,14 @@ def init_syn(self, syn: Synapse) -> None: async def test_send_job_and_wait_async(self) -> None: with ( - patch.object( - asynchronous_job, - "send_job_async", + patch( + "synapseclient.models.mixins.asynchronous_job.send_job_async", + new_callable=AsyncMock, return_value=self.job_id, ) as mock_send_job_async, - patch.object( - asynchronous_job, - "get_job_async", + patch( + "synapseclient.models.mixins.asynchronous_job.get_job_async", + new_callable=AsyncMock, return_value={ "key": "value", }, From 05cc2ddf22dc7721eeebc3285879201429411e2a Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 13:19:43 -0500 Subject: [PATCH 24/64] adds agent unit tests --- synapseclient/models/agent.py | 20 +- .../async/unit_test_asynchronous_job.py | 21 +- .../models/async/unit_test_agent_async.py | 697 ++++++++++++++++++ .../models/synchronous/unit_test_agent.py | 588 +++++++++++++++ 4 files changed, 1304 insertions(+), 22 deletions(-) create mode 100644 tests/unit/synapseclient/models/async/unit_test_agent_async.py create mode 100644 tests/unit/synapseclient/models/synchronous/unit_test_agent.py diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index a5ddf58bc..c4d1583aa 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -146,9 +146,9 @@ class AgentSession(AgentSessionSynchronousProtocol): id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[AgentSessionAccessLevel] = ( - AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. @@ -212,7 +212,7 @@ async def start_async( agent_registration_id=self.agent_registration_id, synapse_client=synapse_client, ) - return self.fill_from_dict(session_response) + return self.fill_from_dict(synapse_agent_session=session_response) @otel_trace_method( method_to_trace_name=lambda self, **kwargs: f"Get_Session: {self.id}" @@ -260,7 +260,7 @@ async def update_async( access_level=self.access_level, synapse_client=synapse_client, ) - return self.fill_from_dict(session_response) + return self.fill_from_dict(synapse_agent_session=session_response) @otel_trace_method(method_to_trace_name=lambda self, **kwargs: f"Prompt: {self.id}") async def prompt_async( @@ -284,12 +284,12 @@ async def prompt_async( instance from the Synapse class constructor. """ - agent_prompt = AgentPrompt( + agent_prompt = await AgentPrompt( prompt=prompt, session_id=self.id, enable_trace=enable_trace - ) - await agent_prompt.send_job_and_wait_async( + ).send_job_and_wait_async( synapse_client=synapse_client, post_exchange_args={"newer_than": newer_than} ) + self.chat_history.append(agent_prompt) if print_response: @@ -376,7 +376,7 @@ async def register_async( cloud_alias_id=self.cloud_alias_id, synapse_client=syn, ) - return self.fill_from_dict(agent_response) + return self.fill_from_dict(agent_registration=agent_response) @otel_trace_method( method_to_trace_name=lambda self, **kwargs: f"Get_Agent: {self.registration_id}" @@ -397,7 +397,7 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent registration_id=self.registration_id, synapse_client=syn, ) - return self.fill_from_dict(agent_response) + return self.fill_from_dict(agent_registration=agent_response) @otel_trace_method( method_to_trace_name=lambda self, **kwargs: f"Start_Agent_Session: {self.registration_id}" diff --git a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py index a62443988..a5d4c9a91 100644 --- a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py +++ b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py @@ -1,26 +1,23 @@ """Unit tests for Asynchronous Job logic.""" -import pytest - import asyncio import json +from unittest.mock import AsyncMock, patch -from synapseclient import Synapse +import pytest +from synapseclient import Synapse +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST +from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError from synapseclient.models.mixins import asynchronous_job from synapseclient.models.mixins.asynchronous_job import ( - send_job_async, + ASYNC_JOB_URIS, + AsynchronousJobState, + AsynchronousJobStatus, get_job_async, send_job_and_wait_async, - AsynchronousJobStatus, - AsynchronousJobState, - ASYNC_JOB_URIS, + send_job_async, ) -from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError - -from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST - -from unittest.mock import patch, AsyncMock class TestSendJobAsync: diff --git a/tests/unit/synapseclient/models/async/unit_test_agent_async.py b/tests/unit/synapseclient/models/async/unit_test_agent_async.py new file mode 100644 index 000000000..1b822465a --- /dev/null +++ b/tests/unit/synapseclient/models/async/unit_test_agent_async.py @@ -0,0 +1,697 @@ +"""Unit tests for Asynchronous methods in Agent, AgentSession, and AgentPrompt classes.""" + +import asyncio +from unittest.mock import AsyncMock, patch + +import pytest + +from synapseclient import Synapse +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST +from synapseclient.models.agent import ( + Agent, + AgentPrompt, + AgentSession, + AgentSessionAccessLevel, + AgentType, +) + + +class TestAgentPrompt: + """Unit tests for the AgentPrompt class' asynchronous methods.""" + + agent_prompt = AgentPrompt( + id="123", + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=True, + ) + synapse_request = { + "concreteType": agent_prompt.concrete_type, + "sessionId": agent_prompt.session_id, + "chatText": agent_prompt.prompt, + "enableTrace": agent_prompt.enable_trace, + } + synapse_response = { + "jobId": "123", + "sessionId": "456", + "responseText": "World", + } + trace_response = { + "page": [ + { + "message": "I'm a robot", + } + ] + } + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + async def test_to_synapse_request(self): + # WHEN I call to_synapse_request on an initialized AgentPrompt + result = self.agent_prompt.to_synapse_request() + # THEN the result should be a dictionary with the correct keys and values + assert result == { + "concreteType": self.agent_prompt.concrete_type, + "sessionId": self.agent_prompt.session_id, + "chatText": self.agent_prompt.prompt, + "enableTrace": self.agent_prompt.enable_trace, + } + + async def test_fill_from_dict(self): + # WHEN I call fill_from_dict on an initialized AgentPrompt with a synapse_response + result_agent_prompt = self.agent_prompt.fill_from_dict(self.synapse_response) + # THEN the result should be an AgentPrompt with the correct values + assert result_agent_prompt.id == self.synapse_response["jobId"] + assert result_agent_prompt.session_id == self.synapse_response["sessionId"] + assert result_agent_prompt.response == self.synapse_response["responseText"] + + async def test_post_exchange_async_trace_enabled(self): + with patch( + "synapseclient.models.agent.get_trace", + new_callable=AsyncMock, + return_value=self.trace_response, + ) as mock_get_trace: + # WHEN I call _post_exchange_async on an initialized AgentPrompt with enable_trace=True + await self.agent_prompt._post_exchange_async(synapse_client=self.syn) + # THEN the mock_get_trace should have been called with the correct arguments + mock_get_trace.assert_called_once_with( + prompt_id=self.agent_prompt.id, + newer_than=None, + synapse_client=self.syn, + ) + # AND the trace should be set to the response from the mock_get_trace + assert self.agent_prompt.trace == self.trace_response["page"][0]["message"] + + async def test_post_exchange_async_trace_disabled(self): + with patch( + "synapseclient.models.agent.get_trace", + new_callable=AsyncMock, + return_value=self.trace_response, + ) as mock_get_trace: + self.agent_prompt.enable_trace = False + # WHEN I call _post_exchange_async on an initialized AgentPrompt with enable_trace=False + await self.agent_prompt._post_exchange_async(synapse_client=self.syn) + # THEN the mock_get_trace should not have been called + mock_get_trace.assert_not_called() + + async def test_send_job_and_wait_async(self): + with ( + patch( + "synapseclient.models.mixins.asynchronous_job.send_job_and_wait_async", + new_callable=AsyncMock, + return_value=self.synapse_response, + ) as mock_send_job_and_wait_async, + patch.object( + self.agent_prompt, + "to_synapse_request", + return_value=self.synapse_request, + ) as mock_to_synapse_request, + patch.object( + self.agent_prompt, + "fill_from_dict", + ) as mock_fill_from_dict, + patch.object( + self.agent_prompt, + "_post_exchange_async", + new_callable=AsyncMock, + ) as mock_post_exchange_async, + ): + # WHEN I call send_job_and_wait_async on an initialized AgentPrompt + await self.agent_prompt.send_job_and_wait_async( + post_exchange_args={"foo": "bar"}, synapse_client=self.syn + ) + # THEN the mock_send_job_and_wait_async should have been called with the correct arguments + mock_send_job_and_wait_async.assert_called_once_with( + request=mock_to_synapse_request.return_value, + request_type=self.agent_prompt.concrete_type, + synapse_client=self.syn, + ) + # THEN the mock_fill_from_dict should have been called with the correct arguments + mock_fill_from_dict.assert_called_once_with( + synapse_response=self.synapse_response + ) + # AND the mock_post_exchange_async should have been called with the correct arguments + mock_post_exchange_async.assert_called_once_with( + synapse_client=self.syn, **{"foo": "bar"} + ) + + +class TestAgentSession: + """Unit tests for the AgentSession class' synchronous methods.""" + + test_session = AgentSession( + id="123", + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + started_on="2024-01-01T00:00:00Z", + started_by="123456789", + modified_on="2024-01-01T00:00:00Z", + agent_registration_id="0", + etag="11111111-1111-1111-1111-111111111111", + ) + + session_response = { + "sessionId": test_session.id, + "agentAccessLevel": test_session.access_level, + "startedOn": test_session.started_on, + "startedBy": test_session.started_by, + "modifiedOn": test_session.modified_on, + "agentRegistrationId": test_session.agent_registration_id, + "etag": test_session.etag, + } + + updated_test_session = AgentSession( + id=test_session.id, + access_level=AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA, + started_on=test_session.started_on, + started_by=test_session.started_by, + modified_on=test_session.modified_on, + agent_registration_id=test_session.agent_registration_id, + etag=test_session.etag, + ) + + updated_session_response = { + "sessionId": updated_test_session.id, + "agentAccessLevel": updated_test_session.access_level, + "startedOn": updated_test_session.started_on, + "startedBy": updated_test_session.started_by, + "modifiedOn": updated_test_session.modified_on, + "agentRegistrationId": updated_test_session.agent_registration_id, + "etag": updated_test_session.etag, + } + + test_prompt_trace_enabled = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=True, + response="World", + trace="Trace", + ) + + test_prompt_trace_disabled = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=False, + response="World", + trace=None, + ) + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + async def test_fill_from_dict(self) -> None: + # WHEN I call fill_from_dict on an empty AgentSession with a synapse_response + result_session = AgentSession().fill_from_dict(self.session_response) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + + async def test_start_async(self) -> None: + with ( + patch( + "synapseclient.models.agent.start_session", + new_callable=AsyncMock, + return_value=self.session_response, + ) as mock_start_session, + patch.object( + AgentSession, + "fill_from_dict", + return_value=self.test_session, + ) as mock_fill_from_dict, + ): + # GIVEN an AgentSession with access_level and agent_registration_id + initial_session = AgentSession( + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + agent_registration_id=0, + ) + # WHEN I call start + result_session = await initial_session.start_async(synapse_client=self.syn) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND start_session should have been called once with the correct arguments + mock_start_session.assert_called_once_with( + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + agent_registration_id=0, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + synapse_agent_session=self.session_response + ) + + async def test_get_async(self) -> None: + with ( + patch( + "synapseclient.models.agent.get_session", + new_callable=AsyncMock, + return_value=self.session_response, + ) as mock_get_session, + patch.object( + AgentSession, + "fill_from_dict", + return_value=self.test_session, + ) as mock_fill_from_dict, + ): + # GIVEN an AgentSession with an agent_registration_id + initial_session = AgentSession( + agent_registration_id=0, + ) + # WHEN I call get + result_session = await initial_session.get_async(synapse_client=self.syn) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND get_session should have been called once with the correct arguments + mock_get_session.assert_called_once_with( + id=initial_session.id, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + synapse_agent_session=self.session_response + ) + + async def test_update_async(self) -> None: + with ( + patch( + "synapseclient.models.agent.update_session", + new_callable=AsyncMock, + return_value=self.updated_session_response, + ) as mock_update_session, + patch.object( + AgentSession, + "fill_from_dict", + return_value=self.updated_test_session, + ) as mock_fill_from_dict, + ): + # GIVEN an AgentSession with an updated access_level + # WHEN I call update + result_session = await self.updated_test_session.update_async( + synapse_client=self.syn + ) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.updated_test_session + # AND update_session should have been called once with the correct arguments + mock_update_session.assert_called_once_with( + id=self.updated_test_session.id, + access_level=AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + synapse_agent_session=self.updated_session_response + ) + + async def test_prompt_trace_enabled_print_response(self) -> None: + with ( + patch( + "synapseclient.models.agent.AgentPrompt.send_job_and_wait_async", + new_callable=AsyncMock, + return_value=self.test_prompt_trace_enabled, + ) as mock_send_job_and_wait_async, + patch( + "synapseclient.models.agent.print", + side_effect=print, + ) as mock_print, + ): + # GIVEN an existing AgentSession + # WHEN I call prompt with trace enabled and print_response enabled + await self.test_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + newer_than=0, + synapse_client=self.syn, + ) + # THEN the result should be an AgentPrompt with the correct values appended to the chat history + assert self.test_prompt_trace_enabled in self.test_session.chat_history + # AND send_job_and_wait_async should have been called once with the correct arguments + mock_send_job_and_wait_async.assert_called_once_with( + synapse_client=self.syn, post_exchange_args={"newer_than": 0} + ) + # AND the trace should be printed + mock_print.assert_called_with( + f"TRACE:\n{self.test_prompt_trace_enabled.trace}" + ) + + async def test_prompt_trace_disabled_no_print(self) -> None: + with ( + patch( + "synapseclient.models.agent.AgentPrompt.send_job_and_wait_async", + new_callable=AsyncMock, + return_value=self.test_prompt_trace_disabled, + ) as mock_send_job_and_wait_async, + patch( + "synapseclient.models.agent.print", + side_effect=print, + ) as mock_print, + ): + # WHEN I call prompt with trace disabled and print_response disabled + await self.test_session.prompt_async( + prompt="Hello", + enable_trace=False, + print_response=False, + newer_than=0, + synapse_client=self.syn, + ) + # THEN the result should be an AgentPrompt with the correct values appended to the chat history + assert self.test_prompt_trace_disabled in self.test_session.chat_history + # AND send_job_and_wait_async should have been called once with the correct arguments + mock_send_job_and_wait_async.assert_called_once_with( + synapse_client=self.syn, post_exchange_args={"newer_than": 0} + ) + # AND print should not have been called + mock_print.assert_not_called() + + +class TestAgent: + """Unit tests for the Agent class' synchronous methods.""" + + def get_example_agent(self) -> Agent: + return Agent( + cloud_agent_id="123", + cloud_alias_id="456", + registration_id=0, + type=AgentType.BASELINE, + registered_on="2024-01-01T00:00:00Z", + sessions={}, + current_session=None, + ) + + test_agent = Agent( + cloud_agent_id="123", + cloud_alias_id="456", + registration_id=0, + type=AgentType.BASELINE, + registered_on="2024-01-01T00:00:00Z", + sessions={}, + current_session=None, + ) + + agent_response = { + "awsAgentId": test_agent.cloud_agent_id, + "awsAliasId": test_agent.cloud_alias_id, + "agentRegistrationId": test_agent.registration_id, + "registeredOn": test_agent.registered_on, + "type": test_agent.type, + } + + test_session = AgentSession( + id="123", + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + started_on="2024-01-01T00:00:00Z", + started_by="123456789", + modified_on="2024-01-01T00:00:00Z", + agent_registration_id="0", + etag="11111111-1111-1111-1111-111111111111", + ) + + test_prompt = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=True, + response="World", + trace="Trace", + ) + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + async def test_fill_from_dict(self) -> None: + # GIVEN an empty Agent + empty_agent = Agent() + # WHEN I call fill_from_dict on an empty Agent with a synapse_response + result_agent = empty_agent.fill_from_dict( + agent_registration=self.agent_response + ) + # THEN the result should be an Agent with the correct values + assert result_agent == self.test_agent + + async def test_register_async(self) -> None: + with ( + patch( + "synapseclient.models.agent.register_agent", + new_callable=AsyncMock, + return_value=self.agent_response, + ) as mock_register_agent, + patch.object( + Agent, + "fill_from_dict", + return_value=self.test_agent, + ) as mock_fill_from_dict, + ): + # GIVEN an Agent with a cloud_agent_id + initial_agent = Agent( + cloud_agent_id="123", + cloud_alias_id="456", + ) + # WHEN I call register + result_agent = await initial_agent.register_async(synapse_client=self.syn) + # THEN the result should be an Agent with the correct values + assert result_agent == self.test_agent + # AND register_agent should have been called once with the correct arguments + mock_register_agent.assert_called_once_with( + cloud_agent_id="123", + cloud_alias_id="456", + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + agent_registration=self.agent_response + ) + + async def test_get_async(self) -> None: + with ( + patch( + "synapseclient.models.agent.get_agent", + new_callable=AsyncMock, + return_value=self.agent_response, + ) as mock_get_agent, + patch.object( + Agent, + "fill_from_dict", + return_value=self.test_agent, + ) as mock_fill_from_dict, + ): + # GIVEN an Agent with a registration_id + initial_agent = Agent( + registration_id=0, + ) + # WHEN I call get + result_agent = await initial_agent.get_async(synapse_client=self.syn) + # THEN the result should be an Agent with the correct values + assert result_agent == self.test_agent + # AND get_agent should have been called once with the correct arguments + mock_get_agent.assert_called_once_with( + registration_id=0, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + agent_registration=self.agent_response + ) + + async def test_start_session_async(self) -> None: + with ( + patch.object( + AgentSession, + "start_async", + new_callable=AsyncMock, + return_value=self.test_session, + ) as mock_start_session, + ): + # GIVEN an existing Agent + my_agent = self.get_example_agent() + # WHEN I call start_session + result_session = await my_agent.start_session_async( + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + synapse_client=self.syn, + ) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND start_session should have been called once with the correct arguments + mock_start_session.assert_called_once_with( + synapse_client=self.syn, + ) + # AND the current_session should be set to the new session + assert my_agent.current_session == self.test_session + # AND the sessions dictionary should have the new session + assert my_agent.sessions[self.test_session.id] == self.test_session + + async def test_get_session_async(self) -> None: + with ( + patch.object( + AgentSession, + "get_async", + new_callable=AsyncMock, + return_value=self.test_session, + ) as mock_get_session, + ): + # GIVEN an existing AgentSession + my_agent = self.get_example_agent() + # WHEN I call get_session + result_session = await my_agent.get_session_async( + session_id="123", synapse_client=self.syn + ) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND get_session should have been called once with the correct arguments + mock_get_session.assert_called_once_with( + synapse_client=self.syn, + ) + # AND the current_session should be set to the session + assert my_agent.current_session == self.test_session + # AND the sessions dictionary should have the session + assert my_agent.sessions[self.test_session.id] == self.test_session + + async def test_prompt_session_selected(self) -> None: + with ( + patch.object( + AgentSession, + "get_async", + new_callable=AsyncMock, + return_value=self.test_session, + ) as mock_get_async, + patch.object( + Agent, + "start_session_async", + new_callable=AsyncMock, + ) as mock_start_session, + patch.object( + AgentSession, + "prompt_async", + ) as mock_prompt_async, + ): + # GIVEN an existing AgentSession + my_agent = self.get_example_agent() + # WHEN I call prompt with a session selected + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + session=self.test_session, + newer_than=0, + synapse_client=self.syn, + ) + # AND get_session_async should have been called once with the correct arguments + mock_get_async.assert_called_once_with( + synapse_client=self.syn, + ) + # AND start_session_async should not have been called + mock_start_session.assert_not_called() + # AND prompt_async should have been called once with the correct arguments + mock_prompt_async.assert_called_once_with( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + + async def test_prompt_session_none_current_session_none(self) -> None: + with ( + patch.object( + Agent, + "get_session_async", + new_callable=AsyncMock, + ) as mock_get_session, + patch.object( + AgentSession, + "start_async", + new_callable=AsyncMock, + return_value=self.test_session, + ) as mock_start_async, + patch.object( + AgentSession, + "prompt_async", + ) as mock_prompt_async, + ): + # GIVEN an existing Agent with no current session + my_agent = self.get_example_agent() + # WHEN I call prompt with no session selected and no current session set + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + newer_than=0, + synapse_client=self.syn, + ) + # THEN get_session_async should not have been called + mock_get_session.assert_not_called() + # AND start_session_async should have been called once with the correct arguments + mock_start_async.assert_called_once_with( + synapse_client=self.syn, + ) + # AND prompt_async should have been called once with the correct arguments + mock_prompt_async.assert_called_once_with( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + + async def test_prompt_session_none_current_session_present(self) -> None: + with ( + patch.object( + Agent, + "get_session_async", + new_callable=AsyncMock, + ) as mock_get_session, + patch.object( + AgentSession, + "start_async", + new_callable=AsyncMock, + ) as mock_start_async, + patch.object( + AgentSession, + "prompt_async", + ) as mock_prompt_async, + ): + # GIVEN an existing Agent with a current session + my_agent = self.get_example_agent() + my_agent.current_session = self.test_session + # WHEN I call prompt with no session selected and a current session set + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + # THEN get_session_async and start_session_async should not have been called + mock_get_session.assert_not_called() + mock_start_async.assert_not_called() + # AND prompt_async should have been called once with the correct arguments + mock_prompt_async.assert_called_once_with( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + + async def test_get_chat_history_when_current_session_none(self) -> None: + # GIVEN an existing Agent with no current session + my_agent = self.get_example_agent() + # WHEN I call get_chat_history + result_chat_history = my_agent.get_chat_history() + # THEN the result should be None + assert result_chat_history is None + + async def test_get_chat_history_when_current_session_and_chat_history_present( + self, + ) -> None: + # GIVEN an existing Agent with a current session and chat history + my_agent = self.get_example_agent() + my_agent.current_session = self.test_session + my_agent.current_session.chat_history = [self.test_prompt] + # WHEN I call get_chat_history + result_chat_history = my_agent.get_chat_history() + # THEN the result should be the chat history + assert self.test_prompt in result_chat_history diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_agent.py b/tests/unit/synapseclient/models/synchronous/unit_test_agent.py new file mode 100644 index 000000000..951f30ed8 --- /dev/null +++ b/tests/unit/synapseclient/models/synchronous/unit_test_agent.py @@ -0,0 +1,588 @@ +"""Unit tests for Synchronous methods in Agent, AgentSession, and AgentPrompt classes.""" + +from unittest.mock import patch + +import pytest + +from synapseclient import Synapse +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST +from synapseclient.models.agent import ( + Agent, + AgentPrompt, + AgentSession, + AgentSessionAccessLevel, + AgentType, +) + + +class TestAgentPrompt: + """Unit tests for the AgentPrompt class' synchronous methods.""" + + test_prompt = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=True, + ) + prompt_request = { + "concreteType": test_prompt.concrete_type, + "sessionId": test_prompt.session_id, + "chatText": test_prompt.prompt, + "enableTrace": test_prompt.enable_trace, + } + prompt_response = { + "jobId": "123", + "sessionId": "456", + "responseText": "World", + } + + def test_to_synapse_request(self) -> None: + # GIVEN an existing AgentPrompt + # WHEN I call to_synapse_request + result_request = self.test_prompt.to_synapse_request() + # THEN the result should be a dictionary with the correct keys and values + assert result_request == self.prompt_request + + def test_fill_from_dict(self) -> None: + # GIVEN an existing AgentPrompt + # WHEN I call fill_from_dict + result_prompt = self.test_prompt.fill_from_dict(self.prompt_response) + # THEN the result should be an AgentPrompt with the correct values + assert result_prompt == self.test_prompt + + +class TestAgentSession: + """Unit tests for the AgentSession class' synchronous methods.""" + + test_session = AgentSession( + id="123", + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + started_on="2024-01-01T00:00:00Z", + started_by="123456789", + modified_on="2024-01-01T00:00:00Z", + agent_registration_id="0", + etag="11111111-1111-1111-1111-111111111111", + ) + + session_response = { + "sessionId": test_session.id, + "agentAccessLevel": test_session.access_level, + "startedOn": test_session.started_on, + "startedBy": test_session.started_by, + "modifiedOn": test_session.modified_on, + "agentRegistrationId": test_session.agent_registration_id, + "etag": test_session.etag, + } + + updated_test_session = AgentSession( + id=test_session.id, + access_level=AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA, + started_on=test_session.started_on, + started_by=test_session.started_by, + modified_on=test_session.modified_on, + agent_registration_id=test_session.agent_registration_id, + etag=test_session.etag, + ) + + updated_session_response = { + "sessionId": updated_test_session.id, + "agentAccessLevel": updated_test_session.access_level, + "startedOn": updated_test_session.started_on, + "startedBy": updated_test_session.started_by, + "modifiedOn": updated_test_session.modified_on, + "agentRegistrationId": updated_test_session.agent_registration_id, + "etag": updated_test_session.etag, + } + + test_prompt_trace_enabled = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=True, + response="World", + trace="Trace", + ) + + test_prompt_trace_disabled = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=False, + response="World", + trace=None, + ) + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + def test_fill_from_dict(self) -> None: + # WHEN I call fill_from_dict on an empty AgentSession with a synapse_response + result_session = AgentSession().fill_from_dict(self.session_response) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + + def test_start(self) -> None: + with ( + patch( + "synapseclient.models.agent.start_session", + return_value=self.session_response, + ) as mock_start_session, + patch.object( + AgentSession, + "fill_from_dict", + return_value=self.test_session, + ) as mock_fill_from_dict, + ): + # GIVEN an AgentSession with access_level and agent_registration_id + initial_session = AgentSession( + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + agent_registration_id=0, + ) + # WHEN I call start + result_session = initial_session.start(synapse_client=self.syn) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND start_session should have been called once with the correct arguments + mock_start_session.assert_called_once_with( + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + agent_registration_id=0, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + synapse_agent_session=self.session_response + ) + + def test_get(self) -> None: + with ( + patch( + "synapseclient.models.agent.get_session", + return_value=self.session_response, + ) as mock_get_session, + patch.object( + AgentSession, + "fill_from_dict", + return_value=self.test_session, + ) as mock_fill_from_dict, + ): + # GIVEN an AgentSession with an agent_registration_id + initial_session = AgentSession( + agent_registration_id=0, + ) + # WHEN I call get + result_session = initial_session.get(synapse_client=self.syn) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND get_session should have been called once with the correct arguments + mock_get_session.assert_called_once_with( + id=initial_session.id, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + synapse_agent_session=self.session_response + ) + + def test_update(self) -> None: + with ( + patch( + "synapseclient.models.agent.update_session", + return_value=self.updated_session_response, + ) as mock_update_session, + patch.object( + AgentSession, + "fill_from_dict", + return_value=self.updated_test_session, + ) as mock_fill_from_dict, + ): + # GIVEN an AgentSession with an updated access_level + # WHEN I call update + result_session = self.updated_test_session.update(synapse_client=self.syn) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.updated_test_session + # AND update_session should have been called once with the correct arguments + mock_update_session.assert_called_once_with( + id=self.updated_test_session.id, + access_level=AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + synapse_agent_session=self.updated_session_response + ) + + def test_prompt_trace_enabled_print_response(self) -> None: + with ( + patch( + "synapseclient.models.agent.AgentPrompt.send_job_and_wait_async", + return_value=self.test_prompt_trace_enabled, + ) as mock_send_job_and_wait_async, + patch( + "synapseclient.models.agent.print", + side_effect=print, + ) as mock_print, + ): + # GIVEN an existing AgentSession + # WHEN I call prompt with trace enabled and print_response enabled + self.test_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + newer_than=0, + synapse_client=self.syn, + ) + # THEN the result should be an AgentPrompt with the correct values appended to the chat history + assert self.test_prompt_trace_enabled in self.test_session.chat_history + # AND send_job_and_wait_async should have been called once with the correct arguments + mock_send_job_and_wait_async.assert_called_once_with( + synapse_client=self.syn, post_exchange_args={"newer_than": 0} + ) + # AND the trace should be printed + mock_print.assert_called_with( + f"TRACE:\n{self.test_prompt_trace_enabled.trace}" + ) + + def test_prompt_trace_disabled_no_print(self) -> None: + with ( + patch( + "synapseclient.models.agent.AgentPrompt.send_job_and_wait_async", + return_value=self.test_prompt_trace_disabled, + ) as mock_send_job_and_wait_async, + patch( + "synapseclient.models.agent.print", + side_effect=print, + ) as mock_print, + ): + # WHEN I call prompt with trace disabled and print_response disabled + self.test_session.prompt( + prompt="Hello", + enable_trace=False, + print_response=False, + newer_than=0, + synapse_client=self.syn, + ) + # THEN the result should be an AgentPrompt with the correct values appended to the chat history + assert self.test_prompt_trace_disabled in self.test_session.chat_history + # AND send_job_and_wait_async should have been called once with the correct arguments + mock_send_job_and_wait_async.assert_called_once_with( + synapse_client=self.syn, post_exchange_args={"newer_than": 0} + ) + # AND print should not have been called + mock_print.assert_not_called() + + +class TestAgent: + """Unit tests for the Agent class' synchronous methods.""" + + def get_example_agent(self) -> Agent: + return Agent( + cloud_agent_id="123", + cloud_alias_id="456", + registration_id=0, + type=AgentType.BASELINE, + registered_on="2024-01-01T00:00:00Z", + sessions={}, + current_session=None, + ) + + test_agent = Agent( + cloud_agent_id="123", + cloud_alias_id="456", + registration_id=0, + type=AgentType.BASELINE, + registered_on="2024-01-01T00:00:00Z", + sessions={}, + current_session=None, + ) + + agent_response = { + "awsAgentId": test_agent.cloud_agent_id, + "awsAliasId": test_agent.cloud_alias_id, + "agentRegistrationId": test_agent.registration_id, + "registeredOn": test_agent.registered_on, + "type": test_agent.type, + } + + test_session = AgentSession( + id="123", + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + started_on="2024-01-01T00:00:00Z", + started_by="123456789", + modified_on="2024-01-01T00:00:00Z", + agent_registration_id="0", + etag="11111111-1111-1111-1111-111111111111", + ) + + test_prompt = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + session_id="456", + prompt="Hello", + enable_trace=True, + response="World", + trace="Trace", + ) + + @pytest.fixture(autouse=True, scope="function") + def init_syn(self, syn: Synapse) -> None: + self.syn = syn + + def test_fill_from_dict(self) -> None: + # GIVEN an empty Agent + empty_agent = Agent() + # WHEN I call fill_from_dict on an empty Agent with a synapse_response + result_agent = empty_agent.fill_from_dict( + agent_registration=self.agent_response + ) + # THEN the result should be an Agent with the correct values + assert result_agent == self.test_agent + + def test_register(self) -> None: + with ( + patch( + "synapseclient.models.agent.register_agent", + return_value=self.agent_response, + ) as mock_register_agent, + patch.object( + Agent, + "fill_from_dict", + return_value=self.test_agent, + ) as mock_fill_from_dict, + ): + # GIVEN an Agent with a cloud_agent_id + initial_agent = Agent( + cloud_agent_id="123", + cloud_alias_id="456", + ) + # WHEN I call register + result_agent = initial_agent.register(synapse_client=self.syn) + # THEN the result should be an Agent with the correct values + assert result_agent == self.test_agent + # AND register_agent should have been called once with the correct arguments + mock_register_agent.assert_called_once_with( + cloud_agent_id="123", + cloud_alias_id="456", + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + agent_registration=self.agent_response + ) + + def test_get(self) -> None: + with ( + patch( + "synapseclient.models.agent.get_agent", + return_value=self.agent_response, + ) as mock_get_agent, + patch.object( + Agent, + "fill_from_dict", + return_value=self.test_agent, + ) as mock_fill_from_dict, + ): + # GIVEN an Agent with a registration_id + initial_agent = Agent( + registration_id=0, + ) + # WHEN I call get + result_agent = initial_agent.get(synapse_client=self.syn) + # THEN the result should be an Agent with the correct values + assert result_agent == self.test_agent + # AND get_agent should have been called once with the correct arguments + mock_get_agent.assert_called_once_with( + registration_id=0, + synapse_client=self.syn, + ) + # AND fill_from_dict should have been called once with the correct arguments + mock_fill_from_dict.assert_called_once_with( + agent_registration=self.agent_response + ) + + def test_start_session(self) -> None: + with patch.object( + AgentSession, + "start_async", + return_value=self.test_session, + ) as mock_start_session: + # GIVEN an existing Agent + my_agent = self.get_example_agent() + # WHEN I call start_session + result_session = my_agent.start_session( + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + synapse_client=self.syn, + ) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND start_session should have been called once with the correct arguments + mock_start_session.assert_called_once_with( + synapse_client=self.syn, + ) + # AND the current_session should be set to the new session + assert my_agent.current_session == self.test_session + # AND the sessions dictionary should have the new session + assert my_agent.sessions[self.test_session.id] == self.test_session + + def test_get_session(self) -> None: + with patch.object( + AgentSession, + "get_async", + return_value=self.test_session, + ) as mock_get_session: + # GIVEN an existing AgentSession + my_agent = self.get_example_agent() + # WHEN I call get_session + result_session = my_agent.get_session( + session_id="123", synapse_client=self.syn + ) + # THEN the result should be an AgentSession with the correct values + assert result_session == self.test_session + # AND get_session should have been called once with the correct arguments + mock_get_session.assert_called_once_with( + synapse_client=self.syn, + ) + # AND the current_session should be set to the session + assert my_agent.current_session == self.test_session + # AND the sessions dictionary should have the session + assert my_agent.sessions[self.test_session.id] == self.test_session + + def test_prompt_session_selected(self) -> None: + with ( + patch.object( + AgentSession, + "get_async", + return_value=self.test_session, + ) as mock_get_async, + patch.object( + Agent, + "start_session_async", + ) as mock_start_session, + patch.object( + AgentSession, + "prompt_async", + ) as mock_prompt_async, + ): + # GIVEN an existing AgentSession + my_agent = self.get_example_agent() + # WHEN I call prompt with a session selected + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + session=self.test_session, + newer_than=0, + synapse_client=self.syn, + ) + # AND get_session_async should have been called once with the correct arguments + mock_get_async.assert_called_once_with( + synapse_client=self.syn, + ) + # AND start_session_async should not have been called + mock_start_session.assert_not_called() + # AND prompt_async should have been called once with the correct arguments + mock_prompt_async.assert_called_once_with( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + + def test_prompt_session_none_current_session_none(self) -> None: + with ( + patch.object( + Agent, + "get_session_async", + ) as mock_get_session, + patch.object( + AgentSession, + "start_async", + return_value=self.test_session, + ) as mock_start_async, + patch.object( + AgentSession, + "prompt_async", + ) as mock_prompt_async, + ): + # GIVEN an existing Agent with no current session + my_agent = self.get_example_agent() + # WHEN I call prompt with no session selected and no current session set + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + newer_than=0, + synapse_client=self.syn, + ) + # THEN get_session_async should not have been called + mock_get_session.assert_not_called() + # AND start_session_async should have been called once with the correct arguments + mock_start_async.assert_called_once_with( + synapse_client=self.syn, + ) + # AND prompt_async should have been called once with the correct arguments + mock_prompt_async.assert_called_once_with( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + + def test_prompt_session_none_current_session_present(self) -> None: + with ( + patch.object( + Agent, + "get_session_async", + ) as mock_get_session, + patch.object( + AgentSession, + "start_async", + ) as mock_start_async, + patch.object( + AgentSession, + "prompt_async", + ) as mock_prompt_async, + ): + # GIVEN an existing Agent with a current session + my_agent = self.get_example_agent() + my_agent.current_session = self.test_session + # WHEN I call prompt with no session selected and a current session set + my_agent.prompt( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + # THEN get_session_async and start_session_async should not have been called + mock_get_session.assert_not_called() + mock_start_async.assert_not_called() + # AND prompt_async should have been called once with the correct arguments + mock_prompt_async.assert_called_once_with( + prompt="Hello", + enable_trace=True, + newer_than=0, + print_response=True, + synapse_client=self.syn, + ) + + def test_get_chat_history_when_current_session_none(self) -> None: + # GIVEN an existing Agent with no current session + my_agent = self.get_example_agent() + # WHEN I call get_chat_history + result_chat_history = my_agent.get_chat_history() + # THEN the result should be None + assert result_chat_history is None + + def test_get_chat_history_when_current_session_and_chat_history_present( + self, + ) -> None: + # GIVEN an existing Agent with a current session and chat history + my_agent = self.get_example_agent() + my_agent.current_session = self.test_session + my_agent.current_session.chat_history = [self.test_prompt] + # WHEN I call get_chat_history + result_chat_history = my_agent.get_chat_history() + # THEN the result should be the chat history + assert self.test_prompt in result_chat_history From e1b00cd435f45dce4cb9e0c5cc7358ebc56bdd39 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 14:42:01 -0500 Subject: [PATCH 25/64] adds integration tests --- synapseclient/models/agent.py | 6 +- .../models/protocols/agent_protocol.py | 10 +- .../models/async/test_agent_async.py | 211 ++++++++++++++++++ .../models/synchronous/test_agent.py | 177 +++++++++++++++ 4 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 tests/integration/synapseclient/models/async/test_agent_async.py create mode 100644 tests/integration/synapseclient/models/synchronous/test_agent.py diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index c4d1583aa..d8de13063 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -146,9 +146,9 @@ class AgentSession(AgentSessionSynchronousProtocol): id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[ - AgentSessionAccessLevel - ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + access_level: Optional[AgentSessionAccessLevel] = ( + AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. diff --git a/synapseclient/models/protocols/agent_protocol.py b/synapseclient/models/protocols/agent_protocol.py index 5a0ca138c..6c12a03aa 100644 --- a/synapseclient/models/protocols/agent_protocol.py +++ b/synapseclient/models/protocols/agent_protocol.py @@ -46,7 +46,15 @@ def update(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": """ return self - def prompt(self, *, synapse_client: Optional[Synapse] = None) -> None: + def prompt( + self, + prompt: str, + enable_trace: bool = False, + print_response: bool = False, + newer_than: Optional[int] = None, + *, + synapse_client: Optional[Synapse] = None, + ) -> None: """Sends a prompt to the agent and adds the response to the AgentSession's chat history. Arguments: diff --git a/tests/integration/synapseclient/models/async/test_agent_async.py b/tests/integration/synapseclient/models/async/test_agent_async.py new file mode 100644 index 000000000..48a9c3f39 --- /dev/null +++ b/tests/integration/synapseclient/models/async/test_agent_async.py @@ -0,0 +1,211 @@ +"""Integration tests for the asynchronous methods of the AgentPrompt, AgentSession, and Agent classes.""" + +import pytest + +from synapseclient import Synapse +from synapseclient.models.agent import ( + Agent, + AgentSession, + AgentPrompt, + AgentSessionAccessLevel, +) +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST + + +AGENT_AWS_ID = "QOTV3KQM1X" +AGENT_REGISTRATION_ID = "29" + + +class TestAgentPrompt: + """Integration tests for the synchronous methods of the AgentPrompt class.""" + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + async def test_send_job_and_wait_async_with_post_exchange_args(self) -> None: + # GIVEN an AgentPrompt with a valid concrete type, prompt, and enable_trace + test_prompt = AgentPrompt( + concrete_type=AGENT_CHAT_REQUEST, + prompt="hello", + enable_trace=True, + ) + # AND the ID of an existing agent session + test_session = await AgentSession( + agent_registration_id=AGENT_REGISTRATION_ID + ).start_async(synapse_client=self.syn) + test_prompt.session_id = test_session.id + # WHEN I send the job and wait for it to complete + await test_prompt.send_job_and_wait_async( + post_exchange_args={"newer_than": 0}, + synapse_client=self.syn, + ) + # THEN I expect the AgentPrompt to be updated with the response and trace + assert test_prompt.response is not None + assert test_prompt.trace is not None + + +class TestAgentSession: + """Integration tests for the synchronous methods of the AgentSession class.""" + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + async def test_start(self) -> None: + # GIVEN an agent session with a valid agent registration id + agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + + # WHEN the start method is called + result_session = await agent_session.start_async(synapse_client=self.syn) + + # THEN the result should be an AgentSession object + # with expected attributes including an empty chat history + assert result_session.id is not None + assert ( + result_session.access_level == AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) + assert result_session.started_on is not None + assert result_session.started_by is not None + assert result_session.modified_on is not None + assert result_session.agent_registration_id == AGENT_REGISTRATION_ID + assert result_session.etag is not None + assert result_session.chat_history == [] + + async def test_get(self) -> None: + # GIVEN an agent session with a valid agent registration id + agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + # WHEN I start a session + await agent_session.start_async(synapse_client=self.syn) + # THEN I expect to be able to get the session with its id + new_session = await AgentSession(id=agent_session.id).get_async( + synapse_client=self.syn + ) + assert new_session == agent_session + + async def test_update(self) -> None: + # GIVEN an agent session with a valid agent registration id and access level set + agent_session = AgentSession( + agent_registration_id=AGENT_REGISTRATION_ID, + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + ) + # WHEN I start a session + await agent_session.start_async(synapse_client=self.syn) + # AND I update the access level of the session + agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + await agent_session.update_async(synapse_client=self.syn) + # THEN I expect the access level to be updated + assert ( + agent_session.access_level == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + ) + + async def test_prompt(self) -> None: + # GIVEN an agent session with a valid agent registration id + agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + # WHEN I start a session + await agent_session.start_async(synapse_client=self.syn) + # THEN I expect to be able to prompt the agent + await agent_session.prompt_async( + prompt="hello", + enable_trace=True, + ) + # AND I expect the chat history to be updated with the prompt and response + assert len(agent_session.chat_history) == 1 + assert agent_session.chat_history[0].prompt == "hello" + assert agent_session.chat_history[0].response is not None + assert agent_session.chat_history[0].trace is not None + + +class TestAgent: + """Integration tests for the synchronous methods of the Agent class.""" + + def get_test_agent(self) -> Agent: + return Agent( + cloud_agent_id="QOTV3KQM1X", + cloud_alias_id="TSTALIASID", + registration_id="29", + registered_on="2025-01-16T18:57:35.680Z", + type="CUSTOM", + sessions={}, + current_session=None, + ) + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + async def test_register(self) -> None: + # GIVEN an Agent with a valid agent AWS id + agent = Agent(cloud_agent_id=AGENT_AWS_ID) + # WHEN I register the agent + await agent.register_async(synapse_client=self.syn) + # THEN I expect the agent to be registered + expected_agent = self.get_test_agent() + assert agent == expected_agent + + async def test_get(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID) + # WHEN I get the agent + await agent.get_async(synapse_client=self.syn) + # THEN I expect the agent to be returned + expected_agent = self.get_test_agent() + assert agent == expected_agent + + async def test_start_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID) + # WHEN I start a session + await agent.start_session_async(synapse_client=self.syn) + # THEN I expect a current session to be set + assert agent.current_session is not None + # AND I expect the session to be in the sessions dictionary + assert agent.sessions[agent.current_session.id] == agent.current_session + + async def test_get_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID) + # WHEN I start a session + await agent.start_session_async(synapse_client=self.syn) + # THEN I expect to be able to get the session with its id + existing_session = await agent.get_session_async( + session_id=agent.current_session.id + ) + # AND I expect those sessions to be the same + assert existing_session == agent.current_session + + async def test_prompt_with_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = await Agent(registration_id=AGENT_REGISTRATION_ID).get_async( + synapse_client=self.syn + ) + # AND a session started separately + session = await AgentSession( + agent_registration_id=AGENT_REGISTRATION_ID + ).start_async(synapse_client=self.syn) + # WHEN I prompt the agent with a session + await agent.prompt_async(prompt="hello", enable_trace=True, session=session) + test_session = agent.sessions[session.id] + # THEN I expect the chat history to be updated with the prompt and response + assert len(test_session.chat_history) == 1 + assert test_session.chat_history[0].prompt == "hello" + assert test_session.chat_history[0].response is not None + assert test_session.chat_history[0].trace is not None + # AND I expect the current session to be the session provided + assert agent.current_session.id == session.id + + async def test_prompt_no_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = await Agent(registration_id=AGENT_REGISTRATION_ID).get_async( + synapse_client=self.syn + ) + # WHEN I prompt the agent without a current session set + # and no session provided + await agent.prompt_async(prompt="hello", enable_trace=True) + # THEN I expect a new session to be started and set as the current session + assert agent.current_session is not None + # AND I expect the chat history to be updated with the prompt and response + assert len(agent.current_session.chat_history) == 1 + assert agent.current_session.chat_history[0].prompt == "hello" + assert agent.current_session.chat_history[0].response is not None + assert agent.current_session.chat_history[0].trace is not None diff --git a/tests/integration/synapseclient/models/synchronous/test_agent.py b/tests/integration/synapseclient/models/synchronous/test_agent.py new file mode 100644 index 000000000..c3391b098 --- /dev/null +++ b/tests/integration/synapseclient/models/synchronous/test_agent.py @@ -0,0 +1,177 @@ +"""Integration tests for the synchronous methods of the AgentSession and Agent classes.""" + +import pytest + +from synapseclient import Synapse +from synapseclient.models.agent import Agent, AgentSession, AgentSessionAccessLevel + +AGENT_AWS_ID = "QOTV3KQM1X" +AGENT_REGISTRATION_ID = "29" + + +class TestAgentSession: + """Integration tests for the synchronous methods of the AgentSession class.""" + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + def test_start(self) -> None: + # GIVEN an agent session with a valid agent registration id + agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + + # WHEN the start method is called + result_session = agent_session.start(synapse_client=self.syn) + + # THEN the result should be an AgentSession object + # with expected attributes including an empty chat history + assert result_session.id is not None + assert ( + result_session.access_level == AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) + assert result_session.started_on is not None + assert result_session.started_by is not None + assert result_session.modified_on is not None + assert result_session.agent_registration_id == AGENT_REGISTRATION_ID + assert result_session.etag is not None + assert result_session.chat_history == [] + + def test_get(self) -> None: + # GIVEN an agent session with a valid agent registration id + agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + # WHEN I start a session + agent_session.start(synapse_client=self.syn) + # THEN I expect to be able to get the session with its id + new_session = AgentSession(id=agent_session.id).get(synapse_client=self.syn) + assert new_session == agent_session + + def test_update(self) -> None: + # GIVEN an agent session with a valid agent registration id and access level set + agent_session = AgentSession( + agent_registration_id=AGENT_REGISTRATION_ID, + access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, + ) + # WHEN I start a session + agent_session.start(synapse_client=self.syn) + # AND I update the access level of the session + agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + agent_session.update(synapse_client=self.syn) + # THEN I expect the access level to be updated + assert ( + agent_session.access_level == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + ) + + def test_prompt(self) -> None: + # GIVEN an agent session with a valid agent registration id + agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + # WHEN I start a session + agent_session.start(synapse_client=self.syn) + # THEN I expect to be able to prompt the agent + agent_session.prompt( + prompt="hello", + enable_trace=True, + ) + # AND I expect the chat history to be updated with the prompt and response + assert len(agent_session.chat_history) == 1 + assert agent_session.chat_history[0].prompt == "hello" + assert agent_session.chat_history[0].response is not None + assert agent_session.chat_history[0].trace is not None + + +class TestAgent: + """Integration tests for the synchronous methods of the Agent class.""" + + def get_test_agent(self) -> Agent: + return Agent( + cloud_agent_id="QOTV3KQM1X", + cloud_alias_id="TSTALIASID", + registration_id="29", + registered_on="2025-01-16T18:57:35.680Z", + type="CUSTOM", + sessions={}, + current_session=None, + ) + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + def test_register(self) -> None: + # GIVEN an Agent with a valid agent AWS id + agent = Agent(cloud_agent_id=AGENT_AWS_ID) + # WHEN I register the agent + agent.register(synapse_client=self.syn) + # THEN I expect the agent to be registered + expected_agent = self.get_test_agent() + assert agent == expected_agent + + def test_get(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID) + # WHEN I get the agent + agent.get(synapse_client=self.syn) + # THEN I expect the agent to be returned + expected_agent = self.get_test_agent() + assert agent == expected_agent + + def test_start_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( + synapse_client=self.syn + ) + # WHEN I start a session + agent.start_session(synapse_client=self.syn) + # THEN I expect a current session to be set + assert agent.current_session is not None + # AND I expect the session to be in the sessions dictionary + assert agent.sessions[agent.current_session.id] == agent.current_session + + def test_get_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( + synapse_client=self.syn + ) + # WHEN I start a session + session = agent.start_session(synapse_client=self.syn) + # THEN I expect to be able to get the session with its id + existing_session = agent.get_session(session_id=session.id) + # AND I expect those sessions to be the same + assert existing_session == session + # AND I expect it to be the current session + assert existing_session == agent.current_session + + def test_prompt_with_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( + synapse_client=self.syn + ) + # AND a session started separately + session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID).start( + synapse_client=self.syn + ) + # WHEN I prompt the agent with a session + agent.prompt(prompt="hello", enable_trace=True, session=session) + test_session = agent.sessions[session.id] + # THEN I expect the chat history to be updated with the prompt and response + assert len(test_session.chat_history) == 1 + assert test_session.chat_history[0].prompt == "hello" + assert test_session.chat_history[0].response is not None + assert test_session.chat_history[0].trace is not None + # AND I expect the current session to be the session provided + assert agent.current_session.id == session.id + + def test_prompt_no_session(self) -> None: + # GIVEN an Agent with a valid agent registration id + agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( + synapse_client=self.syn + ) + # WHEN I prompt the agent without a current session set + # and no session provided + agent.prompt(prompt="hello", enable_trace=True) + # THEN I expect a new session to be started and set as the current session + assert agent.current_session is not None + # AND I expect the chat history to be updated with the prompt and response + assert len(agent.current_session.chat_history) == 1 + assert agent.current_session.chat_history[0].prompt == "hello" + assert agent.current_session.chat_history[0].response is not None + assert agent.current_session.chat_history[0].trace is not None From 717b695d9b5f48805b25882dafc77ed1f4e5b316 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 14:42:49 -0500 Subject: [PATCH 26/64] pre-commit --- synapseclient/models/agent.py | 6 +++--- .../synapseclient/models/async/test_agent_async.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index d8de13063..c4d1583aa 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -146,9 +146,9 @@ class AgentSession(AgentSessionSynchronousProtocol): id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[AgentSessionAccessLevel] = ( - AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. diff --git a/tests/integration/synapseclient/models/async/test_agent_async.py b/tests/integration/synapseclient/models/async/test_agent_async.py index 48a9c3f39..cc47214a2 100644 --- a/tests/integration/synapseclient/models/async/test_agent_async.py +++ b/tests/integration/synapseclient/models/async/test_agent_async.py @@ -3,14 +3,13 @@ import pytest from synapseclient import Synapse +from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST from synapseclient.models.agent import ( Agent, - AgentSession, AgentPrompt, + AgentSession, AgentSessionAccessLevel, ) -from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST - AGENT_AWS_ID = "QOTV3KQM1X" AGENT_REGISTRATION_ID = "29" From f294cdd131465db0ef5263ed8cdf66cc3bc0d544 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 15:43:53 -0500 Subject: [PATCH 27/64] adds examples to agent.py --- synapseclient/models/agent.py | 118 ++++++++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 4 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index c4d1583aa..f0eee70a1 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -141,14 +141,62 @@ class AgentSession(AgentSessionSynchronousProtocol): agent_registration_id: The registration ID of the agent that will be used for this session. etag: The etag of the agent session. + Note: It is recommended to use the `Agent` class to start and get sessions, + but you are free to use this class directly if you wish. + + Example: Start a session and send a prompt. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + + syn = Synapse() + syn.login() + + my_session = AgentSession(agent_registration_id=my_agent_registration_id).start( + synapse_client=syn + ) + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + synapse_client=syn, + ) + + Example: Get an existing session and send a prompt. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + + syn = Synapse() + syn.login() + + my_session = AgentSession(id="my_session_id").get(synapse_client=syn) + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + synapse_client=syn, + ) + + Example: Update the access level of an existing session. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + + syn = Synapse() + syn.login() + + my_session = AgentSession(id="my_session_id").get(synapse_client=syn) + my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + my_session.update(synapse_client=syn) """ id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[ - AgentSessionAccessLevel - ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + access_level: Optional[AgentSessionAccessLevel] = ( + AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. @@ -312,6 +360,68 @@ class Agent(AgentSynchronousProtocol): synapse_registration_id: The ID number of the agent assigned by Synapse. registered_on: The date the agent was registered. type: The type of agent. + + Example: Chat with the baseline Synapse Agent + + You can chat with the same agent which is available in the Synapse UI at https://www.synapse.org/Chat:default. + By default, this "baseline" agent is used when a registration ID is not provided. In the background, + the Agent class will start a session and set that new session as the current session if one is not already set. + + syn = Synapse() + syn.login() + + my_agent = Agent().start_session(synapse_client=syn) + my_agent.prompt( + prompt="Can you tell me about the AD Knowledge Portal dataset?", + enable_trace=True, + print_response=True, + ) + + Example: Register and chat with a custom agent **Only available for internal users (Sage Bionetworks employees)** + + Alternatively, you can register a custom agent and chat with it provided you have already created it. + + syn = Synapse() + syn.login(silent=True) + + my_agent = Agent(cloud_agent_id=AWS_AGENT_ID) + my_agent.register(synapse_client=syn) + + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + synapse_client=syn, + ) + + Advanced Example: Start and prompt multiple sessions + + Here, we connect to a custom agent and start one session with the prompt "Hello". In the background, + this first session is being set as the current session and future prompts will be sent to this session + by default. If we want to send a prompt to a different session, we can do so by starting it and calling + prompt again, but with our new session as an argument. We now have two sessions, both stored in the + my_agent.sessions dictionary. After the second prompt, my_second_session is now the current session. + + syn = Synapse() + syn.login() + + my_agent = Agent(registration_id=my_registration_id).get(synapse_client=syn) + + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + synapse_client=syn, + ) + + my_second_session = my_agent.start_session(synapse_client=syn) + my_agent.prompt( + prompt="Hello again", + enable_trace=True, + print_response=True, + session=my_second_session, + synapse_client=syn, + ) """ cloud_agent_id: Optional[str] = None @@ -360,7 +470,7 @@ def fill_from_dict(self, agent_registration: Dict[str, str]) -> "Agent": async def register_async( self, *, synapse_client: Optional[Synapse] = None ) -> "Agent": - """Registers an agent with the Synapse API. If agent exists, it will be retrieved. + """Registers an agent with the Synapse API. If agent already exists, it will be retrieved. Arguments: synapse_client: If not passed in and caching was not disabled by From fdd665fadbe51af31ecdf96bac38cf306f9692f1 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 15:45:18 -0500 Subject: [PATCH 28/64] removes todos --- synapseclient/models/agent.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index f0eee70a1..414739b0d 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -125,7 +125,6 @@ async def _post_exchange_async( self.trace = trace_response["page"][0]["message"] -# TODO Add example usage to the docstring @dataclass @async_to_sync class AgentSession(AgentSessionSynchronousProtocol): @@ -347,7 +346,6 @@ async def prompt_async( print(f"TRACE:\n{agent_prompt.trace}") -# TODO Add example usage to the docstring @dataclass @async_to_sync class Agent(AgentSynchronousProtocol): From 6197f38ee0bd6cb85c5c2ac4dd3e51b92702bba9 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 16:19:16 -0500 Subject: [PATCH 29/64] adds POC script --- .../oop_poc_agent.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/scripts/object_orientated_programming_poc/oop_poc_agent.py diff --git a/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py b/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py new file mode 100644 index 000000000..1c19335f8 --- /dev/null +++ b/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py @@ -0,0 +1,30 @@ +""" +The purpose of this script is to demonstrate how to use the new OOP interface for Synapse AI Agents. + +1. Register and send a prompt to a custom agent +2. Get a baseline agent instance and send a prompt to it +""" + +from synapseclient import Synapse +from synapseclient.models import Agent + +CLOUD_AGENT_ID = "my_agent_id" +AGENT_REGISTRATION_ID = 123 + +syn = Synapse() +syn.login() + + +def register_and_send_prompt_to_custom_agent(): + my_custom_agent = Agent(cloud_agent_id=CLOUD_AGENT_ID) + my_custom_agent.register(synapse_client=syn) + my_custom_agent.prompt(prompt="Hello, how are you?") + + +def get_baseline_agent_and_send_prompt_to_it(): + baseline_agent = Agent().get(synapse_client=syn) + baseline_agent.prompt(prompt="Hello, how are you?") + + +register_and_send_prompt_to_custom_agent() +get_baseline_agent_and_send_prompt_to_it() From 33639a6216bb811ad5b13142690c3d10771d0415 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 16:20:02 -0500 Subject: [PATCH 30/64] add to mixins --- synapseclient/models/agent.py | 8 ++++---- synapseclient/models/mixins/__init__.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 414739b0d..8a74a3d61 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -14,7 +14,7 @@ ) from synapseclient.core.async_utils import async_to_sync, otel_trace_method from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST -from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator +from synapseclient.models.mixins import AsynchronousCommunicator from synapseclient.models.protocols.agent_protocol import ( AgentSessionSynchronousProtocol, AgentSynchronousProtocol, @@ -193,9 +193,9 @@ class AgentSession(AgentSessionSynchronousProtocol): id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[AgentSessionAccessLevel] = ( - AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. diff --git a/synapseclient/models/mixins/__init__.py b/synapseclient/models/mixins/__init__.py index 0fb23dac7..93a98589c 100644 --- a/synapseclient/models/mixins/__init__.py +++ b/synapseclient/models/mixins/__init__.py @@ -1,9 +1,11 @@ """References to the mixins that are used in the Synapse models.""" from synapseclient.models.mixins.access_control import AccessControllable +from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator from synapseclient.models.mixins.storable_container import StorableContainer __all__ = [ "AccessControllable", "StorableContainer", + "AsynchronousCommunicator", ] From 72cdff1a9dd11a7e0f1dff6dc59dc3af5e4982a0 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 16:31:43 -0500 Subject: [PATCH 31/64] adds agent docs --- docs/reference/experimental/async/agent.md | 11 +++++++++++ docs/reference/experimental/sync/agent.md | 23 ++++++++++++++++++++++ mkdocs.yml | 4 ++++ 3 files changed, 38 insertions(+) create mode 100644 docs/reference/experimental/async/agent.md create mode 100644 docs/reference/experimental/sync/agent.md diff --git a/docs/reference/experimental/async/agent.md b/docs/reference/experimental/async/agent.md new file mode 100644 index 000000000..050775205 --- /dev/null +++ b/docs/reference/experimental/async/agent.md @@ -0,0 +1,11 @@ +# Agent + +::: synapseclient.models.Agent + options: + members: + - register_async + - get_async + - start_session_async + - get_session_async + - prompt_async + - get_chat_history diff --git a/docs/reference/experimental/sync/agent.md b/docs/reference/experimental/sync/agent.md new file mode 100644 index 000000000..8aae41bda --- /dev/null +++ b/docs/reference/experimental/sync/agent.md @@ -0,0 +1,23 @@ +# Agent + +## Example Script: + +
+ Working with a Synapse Agent + +```python +{!docs/scripts/object_orientated_programming_poc/oop_poc_agent.py!} +``` +
+ +## API Reference + +::: synapseclient.models.Agent + options: + members: + - register + - get + - start_session + - get_session + - prompt + - get_chat_history diff --git a/mkdocs.yml b/mkdocs.yml index 768dcd0e3..516d4a2f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,10 @@ nav: - Core: reference/core.md - REST Apis: reference/rest_apis.md - Experimental: + - Synchronous: + - Agent: reference/experimental/sync/agent.md + - Asynchronous: + - Agent: reference/experimental/async/agent.md - Object-Orientated Models: reference/oop/models.md - Async Object-Orientated Models: reference/oop/models_async.md - Further Reading: From 7a3fd1384a961082be244dc7882d2d7779b5fe8f Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 16:45:53 -0500 Subject: [PATCH 32/64] updates agent docs --- docs/reference/experimental/async/agent.md | 26 +++++++++++++++++----- docs/reference/experimental/sync/agent.md | 11 +++++++++ synapseclient/models/__init__.py | 8 ++++++- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/reference/experimental/async/agent.md b/docs/reference/experimental/async/agent.md index 050775205..a16089b58 100644 --- a/docs/reference/experimental/async/agent.md +++ b/docs/reference/experimental/async/agent.md @@ -3,9 +3,23 @@ ::: synapseclient.models.Agent options: members: - - register_async - - get_async - - start_session_async - - get_session_async - - prompt_async - - get_chat_history + - register_async + - get_async + - start_session_async + - get_session_async + - prompt_async + - get_chat_history +--- +::: synapseclient.models.AgentSession + options: + members: + - start_async + - get_async + - update_async + - prompt_async +--- +::: synapseclient.models.AgentPrompt + options: + members: + - send_job_and_wait_async +--- diff --git a/docs/reference/experimental/sync/agent.md b/docs/reference/experimental/sync/agent.md index 8aae41bda..1f9dbce33 100644 --- a/docs/reference/experimental/sync/agent.md +++ b/docs/reference/experimental/sync/agent.md @@ -21,3 +21,14 @@ - get_session - prompt - get_chat_history +--- +::: synapseclient.models.AgentSession + options: + members: + - start + - get + - update + - prompt +--- +::: synapseclient.models.AgentPrompt +--- diff --git a/synapseclient/models/__init__.py b/synapseclient/models/__init__.py index 09aa2ac32..ed5d4f18d 100644 --- a/synapseclient/models/__init__.py +++ b/synapseclient/models/__init__.py @@ -1,6 +1,11 @@ # These are all of the models that are used by the Synapse client. from synapseclient.models.activity import Activity, UsedEntity, UsedURL -from synapseclient.models.agent import Agent, AgentSession, AgentSessionAccessLevel +from synapseclient.models.agent import ( + Agent, + AgentSession, + AgentSessionAccessLevel, + AgentPrompt, +) from synapseclient.models.annotations import Annotations from synapseclient.models.file import File, FileHandle from synapseclient.models.folder import Folder @@ -42,4 +47,5 @@ "Agent", "AgentSession", "AgentSessionAccessLevel", + "AgentPrompt", ] From 2bcc349e1519ad156d340c74eeea8f1a7c162f4f Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 16 Jan 2025 17:19:28 -0500 Subject: [PATCH 33/64] reorganize documentation --- docs/reference/experimental/async/activity.md | 20 +++ docs/reference/experimental/async/agent.md | 2 + docs/reference/experimental/async/file.md | 23 +++ docs/reference/experimental/async/folder.md | 16 ++ docs/reference/experimental/async/project.md | 15 ++ docs/reference/experimental/async/table.md | 17 ++ docs/reference/experimental/async/team.md | 14 ++ .../experimental/async/user_profile.md | 11 ++ .../mixins/access_controllable.md | 3 + .../mixins/asynchronous_communicator.md | 3 + .../experimental/mixins/failure_strategy.md | 3 + .../experimental/mixins/storable_container.md | 3 + docs/reference/experimental/sync/activity.md | 30 ++++ docs/reference/experimental/sync/file.md | 33 ++++ docs/reference/experimental/sync/folder.md | 26 +++ docs/reference/experimental/sync/project.md | 25 +++ docs/reference/experimental/sync/table.md | 27 +++ docs/reference/experimental/sync/team.md | 24 +++ .../experimental/sync/user_profile.md | 13 ++ docs/reference/oop/models.md | 169 ------------------ docs/reference/oop/models_async.md | 100 ----------- mkdocs.yml | 21 ++- synapseclient/models/__init__.py | 2 +- 23 files changed, 328 insertions(+), 272 deletions(-) create mode 100644 docs/reference/experimental/async/activity.md create mode 100644 docs/reference/experimental/async/file.md create mode 100644 docs/reference/experimental/async/folder.md create mode 100644 docs/reference/experimental/async/project.md create mode 100644 docs/reference/experimental/async/table.md create mode 100644 docs/reference/experimental/async/team.md create mode 100644 docs/reference/experimental/async/user_profile.md create mode 100644 docs/reference/experimental/mixins/access_controllable.md create mode 100644 docs/reference/experimental/mixins/asynchronous_communicator.md create mode 100644 docs/reference/experimental/mixins/failure_strategy.md create mode 100644 docs/reference/experimental/mixins/storable_container.md create mode 100644 docs/reference/experimental/sync/activity.md create mode 100644 docs/reference/experimental/sync/file.md create mode 100644 docs/reference/experimental/sync/folder.md create mode 100644 docs/reference/experimental/sync/project.md create mode 100644 docs/reference/experimental/sync/table.md create mode 100644 docs/reference/experimental/sync/team.md create mode 100644 docs/reference/experimental/sync/user_profile.md delete mode 100644 docs/reference/oop/models.md delete mode 100644 docs/reference/oop/models_async.md diff --git a/docs/reference/experimental/async/activity.md b/docs/reference/experimental/async/activity.md new file mode 100644 index 000000000..6ec94b55f --- /dev/null +++ b/docs/reference/experimental/async/activity.md @@ -0,0 +1,20 @@ +# Activity + +## API Reference + +::: synapseclient.models.Activity + options: + members: + - from_parent_async + - store_async + - delete_async +--- +::: synapseclient.models.UsedEntity + options: + filters: + - "!" +--- +::: synapseclient.models.UsedURL + options: + filters: + - "!" diff --git a/docs/reference/experimental/async/agent.md b/docs/reference/experimental/async/agent.md index a16089b58..8d8a6581f 100644 --- a/docs/reference/experimental/async/agent.md +++ b/docs/reference/experimental/async/agent.md @@ -1,5 +1,7 @@ # Agent +## API reference + ::: synapseclient.models.Agent options: members: diff --git a/docs/reference/experimental/async/file.md b/docs/reference/experimental/async/file.md new file mode 100644 index 000000000..3cd2d60aa --- /dev/null +++ b/docs/reference/experimental/async/file.md @@ -0,0 +1,23 @@ +# File + +## API Reference + +::: synapseclient.models.File + options: + inherited_members: true + members: + - get_async + - store_async + - copy_async + - delete_async + - from_id_async + - from_path_async + - change_metadata_async + - get_permissions_async + - get_acl_async + - set_permissions_async +--- +::: synapseclient.models.file.FileHandle + options: + filters: + - "!" diff --git a/docs/reference/experimental/async/folder.md b/docs/reference/experimental/async/folder.md new file mode 100644 index 000000000..fa9e41a52 --- /dev/null +++ b/docs/reference/experimental/async/folder.md @@ -0,0 +1,16 @@ +# Folder + +## API Reference + +::: synapseclient.models.Folder + options: + inherited_members: true + members: + - get_async + - store_async + - delete_async + - copy_async + - sync_from_synapse_async + - get_permissions_async + - get_acl_async + - set_permissions_async diff --git a/docs/reference/experimental/async/project.md b/docs/reference/experimental/async/project.md new file mode 100644 index 000000000..d4fde2b15 --- /dev/null +++ b/docs/reference/experimental/async/project.md @@ -0,0 +1,15 @@ +# Project + +## API reference + +::: synapseclient.models.Project + options: + inherited_members: true + members: + - get_async + - store_async + - delete_async + - sync_from_synapse_async + - get_permissions_async + - get_acl_async + - set_permissions_async diff --git a/docs/reference/experimental/async/table.md b/docs/reference/experimental/async/table.md new file mode 100644 index 000000000..c314c37ba --- /dev/null +++ b/docs/reference/experimental/async/table.md @@ -0,0 +1,17 @@ +# Table + +## API Reference + +::: synapseclient.models.Table + options: + inherited_members: true + members: + - get_async + - store_schema_async + - store_rows_from_csv_async + - delete_rows_async + - query_async + - delete_async + - get_permissions_async + - get_acl_async + - set_permissions_async diff --git a/docs/reference/experimental/async/team.md b/docs/reference/experimental/async/team.md new file mode 100644 index 000000000..46a83c623 --- /dev/null +++ b/docs/reference/experimental/async/team.md @@ -0,0 +1,14 @@ +# Team + +## API Reference + +::: synapseclient.models.Team + options: + members: + - create_async + - delete_async + - from_id_async + - from_name_async + - members_async + - invite_async + - open_invitations_async diff --git a/docs/reference/experimental/async/user_profile.md b/docs/reference/experimental/async/user_profile.md new file mode 100644 index 000000000..1f7449255 --- /dev/null +++ b/docs/reference/experimental/async/user_profile.md @@ -0,0 +1,11 @@ +# UserProfile + +## API Reference + +::: synapseclient.models.UserProfile + options: + members: + - get_async + - from_id_async + - from_username_async + - is_certified_async diff --git a/docs/reference/experimental/mixins/access_controllable.md b/docs/reference/experimental/mixins/access_controllable.md new file mode 100644 index 000000000..96e7f70b9 --- /dev/null +++ b/docs/reference/experimental/mixins/access_controllable.md @@ -0,0 +1,3 @@ +# AccessControllable + +::: synapseclient.models.mixins.AccessControllable diff --git a/docs/reference/experimental/mixins/asynchronous_communicator.md b/docs/reference/experimental/mixins/asynchronous_communicator.md new file mode 100644 index 000000000..bfc081057 --- /dev/null +++ b/docs/reference/experimental/mixins/asynchronous_communicator.md @@ -0,0 +1,3 @@ +# AsynchronousCommunicator + +::: synapseclient.models.mixins.AsynchronousCommunicator diff --git a/docs/reference/experimental/mixins/failure_strategy.md b/docs/reference/experimental/mixins/failure_strategy.md new file mode 100644 index 000000000..3809b74f5 --- /dev/null +++ b/docs/reference/experimental/mixins/failure_strategy.md @@ -0,0 +1,3 @@ +# FailureStrategy + +::: synapseclient.models.FailureStrategy diff --git a/docs/reference/experimental/mixins/storable_container.md b/docs/reference/experimental/mixins/storable_container.md new file mode 100644 index 000000000..49e10a5e3 --- /dev/null +++ b/docs/reference/experimental/mixins/storable_container.md @@ -0,0 +1,3 @@ +# StorableContainer + +::: synapseclient.models.mixins.StorableContainer diff --git a/docs/reference/experimental/sync/activity.md b/docs/reference/experimental/sync/activity.md new file mode 100644 index 000000000..ed05409f9 --- /dev/null +++ b/docs/reference/experimental/sync/activity.md @@ -0,0 +1,30 @@ +# Activity + +## Example Script + +
+ Working with activities + +```python +{!docs/scripts/object_orientated_programming_poc/oop_poc_activity.py!} +``` +
+ +## API Reference + +::: synapseclient.models.Activity + options: + members: + - from_parent + - store + - delete +--- +::: synapseclient.models.UsedEntity + options: + filters: + - "!" +--- +::: synapseclient.models.UsedURL + options: + filters: + - "!" diff --git a/docs/reference/experimental/sync/file.md b/docs/reference/experimental/sync/file.md new file mode 100644 index 000000000..9f858a79d --- /dev/null +++ b/docs/reference/experimental/sync/file.md @@ -0,0 +1,33 @@ +# File + +## Example Script + +
+ Working with files + +```python +{!docs/scripts/object_orientated_programming_poc/oop_poc_file.py!} +``` +
+ +## API Reference + +::: synapseclient.models.File + options: + inherited_members: true + members: + - get + - store + - copy + - delete + - from_id + - from_path + - change_metadata + - get_permissions + - get_acl + - set_permissions +--- +::: synapseclient.models.file.FileHandle + options: + filters: + - "!" diff --git a/docs/reference/experimental/sync/folder.md b/docs/reference/experimental/sync/folder.md new file mode 100644 index 000000000..204ae7046 --- /dev/null +++ b/docs/reference/experimental/sync/folder.md @@ -0,0 +1,26 @@ +# Folder + +## Example Script + +
+ Working with folders + +```python +{!docs/scripts/object_orientated_programming_poc/oop_poc_folder.py!} +``` +
+ +## API Reference + +::: synapseclient.models.Folder + options: + inherited_members: true + members: + - get + - store + - delete + - copy + - sync_from_synapse + - get_permissions + - get_acl + - set_permissions diff --git a/docs/reference/experimental/sync/project.md b/docs/reference/experimental/sync/project.md new file mode 100644 index 000000000..953039a20 --- /dev/null +++ b/docs/reference/experimental/sync/project.md @@ -0,0 +1,25 @@ +# Project + +## Example Script + +
+ Working with a project + +```python +{!docs/scripts/object_orientated_programming_poc/oop_poc_project.py!} +``` +
+ +## API reference + +::: synapseclient.models.Project + options: + inherited_members: true + members: + - get + - store + - delete + - sync_from_synapse + - get_permissions + - get_acl + - set_permissions diff --git a/docs/reference/experimental/sync/table.md b/docs/reference/experimental/sync/table.md new file mode 100644 index 000000000..7b04af0cc --- /dev/null +++ b/docs/reference/experimental/sync/table.md @@ -0,0 +1,27 @@ +# Table + +## Example Script + +
+ Working with tables + +```python +{!docs/scripts/object_orientated_programming_poc/oop_poc_table.py!} +``` +
+ +## API Reference + +::: synapseclient.models.Table + options: + inherited_members: true + members: + - get + - store_schema + - store_rows_from_csv + - delete_rows + - query + - delete + - get_permissions + - get_acl + - set_permissions diff --git a/docs/reference/experimental/sync/team.md b/docs/reference/experimental/sync/team.md new file mode 100644 index 000000000..1af58f260 --- /dev/null +++ b/docs/reference/experimental/sync/team.md @@ -0,0 +1,24 @@ +# Team + +## Example Script + +
+ Working with teams + +```python +{!docs/scripts/object_orientated_programming_poc/oop_poc_team.py!} +``` +
+ +## API Reference + +::: synapseclient.models.Team + options: + members: + - create + - delete + - from_id + - from_name + - members + - invite + - open_invitations diff --git a/docs/reference/experimental/sync/user_profile.md b/docs/reference/experimental/sync/user_profile.md new file mode 100644 index 000000000..552532bcc --- /dev/null +++ b/docs/reference/experimental/sync/user_profile.md @@ -0,0 +1,13 @@ +# UserProfile + +## API Reference + +::: synapseclient.models.UserProfile + options: + members: + - get + - from_id + - from_username + - is_certified +--- +::: synapseclient.models.UserPreference diff --git a/docs/reference/oop/models.md b/docs/reference/oop/models.md deleted file mode 100644 index 2c7ebc153..000000000 --- a/docs/reference/oop/models.md +++ /dev/null @@ -1,169 +0,0 @@ -Contained within this file are experimental interfaces for working with the Synapse Python -Client. Unless otherwise noted these interfaces are subject to change at any time. Use -at your own risk. - -## Sample Scripts: - -
- Working with a project - -```python -{!docs/scripts/object_orientated_programming_poc/oop_poc_project.py!} -``` -
- -
- Working with folders - -```python -{!docs/scripts/object_orientated_programming_poc/oop_poc_folder.py!} -``` -
- -
- Working with files - -```python -{!docs/scripts/object_orientated_programming_poc/oop_poc_file.py!} -``` -
- -
- Working with tables - -```python -{!docs/scripts/object_orientated_programming_poc/oop_poc_table.py!} -``` -
- -
- Current Synapse interface for working with a project - -```python -{!docs/scripts/object_orientated_programming_poc/synapse_project.py!} -``` -
- -
- Working with activities - -```python -{!docs/scripts/object_orientated_programming_poc/oop_poc_activity.py!} -``` -
- -
- Working with teams - -```python -{!docs/scripts/object_orientated_programming_poc/oop_poc_team.py!} -``` -
- -## API reference - -::: synapseclient.models.Project - options: - inherited_members: true - members: - - get - - store - - delete - - sync_from_synapse - - get_permissions - - get_acl - - set_permissions ---- -::: synapseclient.models.Folder - options: - inherited_members: true - members: - - get - - store - - delete - - copy - - sync_from_synapse - - get_permissions - - get_acl - - set_permissions ---- -::: synapseclient.models.File - options: - inherited_members: true - members: - - get - - store - - copy - - delete - - from_id - - from_path - - change_metadata - - get_permissions - - get_acl - - set_permissions -::: synapseclient.models.file.FileHandle - options: - filters: - - "!" ---- -::: synapseclient.models.Table - options: - inherited_members: true - members: - - get - - store_schema - - store_rows_from_csv - - delete_rows - - query - - delete - - get_permissions - - get_acl - - set_permissions ---- -::: synapseclient.models.Activity - options: - members: - - from_parent - - store - - delete - -::: synapseclient.models.UsedEntity - options: - filters: - - "!" -::: synapseclient.models.UsedURL - options: - filters: - - "!" ---- -::: synapseclient.models.Team - options: - members: - - create - - delete - - from_id - - from_name - - members - - invite - - open_invitations ---- -::: synapseclient.models.UserProfile - options: - members: - - get - - from_id - - from_username - - is_certified -::: synapseclient.models.UserPreference ---- -::: synapseclient.models.Annotations - options: - members: - - from_dict ---- -::: synapseclient.models.mixins.AccessControllable ---- - -::: synapseclient.models.mixins.StorableContainer ---- -::: synapseclient.models.FailureStrategy diff --git a/docs/reference/oop/models_async.md b/docs/reference/oop/models_async.md deleted file mode 100644 index c61ce0df6..000000000 --- a/docs/reference/oop/models_async.md +++ /dev/null @@ -1,100 +0,0 @@ -Contained within this file are experimental interfaces for working with the Synapse Python -Client. Unless otherwise noted these interfaces are subject to change at any time. Use -at your own risk. - -These APIs also introduce [AsyncIO](https://docs.python.org/3/library/asyncio.html) to -the client. - -## Sample Scripts: -See [this page for sample scripts](models.md#sample-scripts). -The sample scripts are from a synchronous context, -replace any of the method calls with the async counter-party and they will be -functionally equivalent. - -## API reference - -::: synapseclient.models.Project - options: - inherited_members: true - members: - - get_async - - store_async - - delete_async - - sync_from_synapse_async - - get_permissions_async - - get_acl_async - - set_permissions_async ---- -::: synapseclient.models.Folder - options: - inherited_members: true - members: - - get_async - - store_async - - delete_async - - copy_async - - sync_from_synapse_async - - get_permissions_async - - get_acl_async - - set_permissions_async ---- -::: synapseclient.models.File - options: - inherited_members: true - members: - - get_async - - store_async - - copy_async - - delete_async - - from_id_async - - from_path_async - - change_metadata_async - - get_permissions_async - - get_acl_async - - set_permissions_async ---- -::: synapseclient.models.Table - options: - inherited_members: true - members: - - get_async - - store_schema_async - - store_rows_from_csv_async - - delete_rows_async - - query_async - - delete_async - - get_permissions_async - - get_acl_async - - set_permissions_async ---- -::: synapseclient.models.Activity - options: - members: - - from_parent_async - - store_async - - delete_async - ---- -::: synapseclient.models.Team - options: - members: - - create_async - - delete_async - - from_id_async - - from_name_async - - members_async - - invite_async - - open_invitations_async ---- -::: synapseclient.models.UserProfile - options: - members: - - get_async - - from_id_async - - from_username_async - - is_certified_async ---- -::: synapseclient.models.Annotations - options: - members: - - store_async diff --git a/mkdocs.yml b/mkdocs.yml index 516d4a2f0..da15fd324 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,12 +75,29 @@ nav: - Core: reference/core.md - REST Apis: reference/rest_apis.md - Experimental: + - Mixins: + - AccessControllable: reference/experimental/mixins/access_controllable.md + - StorableContainer: reference/experimental/mixins/storable_container.md + - AsynchronousCommunicator: reference/experimental/mixins/asynchronous_communicator.md + - FailureStrategy: reference/experimental/mixins/failure_strategy.md - Synchronous: - Agent: reference/experimental/sync/agent.md + - Project: reference/experimental/sync/project.md + - Folder: reference/experimental/sync/folder.md + - File: reference/experimental/sync/file.md + - Table: reference/experimental/sync/table.md + - Activity: reference/experimental/sync/activity.md + - Team: reference/experimental/sync/team.md + - UserProfile: reference/experimental/sync/user_profile.md - Asynchronous: - Agent: reference/experimental/async/agent.md - - Object-Orientated Models: reference/oop/models.md - - Async Object-Orientated Models: reference/oop/models_async.md + - Project: reference/experimental/async/project.md + - Folder: reference/experimental/async/folder.md + - File: reference/experimental/async/file.md + - Table: reference/experimental/async/table.md + - Activity: reference/experimental/async/activity.md + - Team: reference/experimental/async/team.md + - UserProfile: reference/experimental/async/user_profile.md - Further Reading: - Home: explanations/home.md - Domain Models of Synapse: explanations/domain_models_of_synapse.md diff --git a/synapseclient/models/__init__.py b/synapseclient/models/__init__.py index ed5d4f18d..1e2f686ed 100644 --- a/synapseclient/models/__init__.py +++ b/synapseclient/models/__init__.py @@ -2,9 +2,9 @@ from synapseclient.models.activity import Activity, UsedEntity, UsedURL from synapseclient.models.agent import ( Agent, + AgentPrompt, AgentSession, AgentSessionAccessLevel, - AgentPrompt, ) from synapseclient.models.annotations import Annotations from synapseclient.models.file import File, FileHandle From 0082ea625e4874c0893f22a0e9f502fd922699e1 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 09:40:17 -0500 Subject: [PATCH 34/64] updates poc script --- .../oop_poc_agent.py | 85 +++++++++++++++++-- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py b/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py index 1c19335f8..9b6a5cdef 100644 --- a/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py +++ b/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py @@ -5,26 +5,95 @@ 2. Get a baseline agent instance and send a prompt to it """ -from synapseclient import Synapse -from synapseclient.models import Agent +import synapseclient +from synapseclient.models import Agent, AgentSession, AgentSessionAccessLevel -CLOUD_AGENT_ID = "my_agent_id" -AGENT_REGISTRATION_ID = 123 +CLOUD_AGENT_ID = "QOTV3KQM1X" +AGENT_REGISTRATION_ID = 29 -syn = Synapse() +syn = synapseclient.Synapse(debug=True) syn.login() +# Using the Agent class + +# Register a custom agent and send a prompt to it def register_and_send_prompt_to_custom_agent(): my_custom_agent = Agent(cloud_agent_id=CLOUD_AGENT_ID) my_custom_agent.register(synapse_client=syn) - my_custom_agent.prompt(prompt="Hello, how are you?") + my_custom_agent.prompt( + prompt="Hello", enable_trace=True, print_response=True, synapse_client=syn + ) +# Create an Agent Object and prompt. +# By default, this will send a prompt to a new session with the baseline Synapse Agent. def get_baseline_agent_and_send_prompt_to_it(): - baseline_agent = Agent().get(synapse_client=syn) - baseline_agent.prompt(prompt="Hello, how are you?") + baseline_agent = Agent() + baseline_agent.prompt( + prompt="What is Synapse?", + enable_trace=True, + print_response=True, + synapse_client=syn, + ) + + +# Conduct more than one session with the same agent +def conduct_multiple_sessions_with_same_agent(): + my_agent = Agent(registration_id=AGENT_REGISTRATION_ID).get(synapse_client=syn) + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + synapse_client=syn, + ) + my_second_session = my_agent.start_session(synapse_client=syn) + my_agent.prompt( + prompt="Hello again", + enable_trace=True, + print_response=True, + session=my_second_session, + synapse_client=syn, + ) + + +# Using the AgentSession class + + +# Start a new session with a custom agent and send a prompt to it +def start_new_session_with_custom_agent_and_send_prompt_to_it(): + my_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID).start( + synapse_client=syn + ) + my_session.prompt( + prompt="Hello", enable_trace=True, print_response=True, synapse_client=syn + ) + + +# Start a new session with the baseline Synapse Agent and send a prompt to it +def start_new_session_with_baseline_agent_and_send_prompt_to_it(): + my_session = AgentSession().start(synapse_client=syn) + my_session.prompt( + prompt="What is Synapse?", + enable_trace=True, + print_response=True, + synapse_client=syn, + ) + + +# Start a new session with a custom agent and then update what the agent has access to +def start_new_session_with_custom_agent_and_update_access_to_it(): + my_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID).start( + synapse_client=syn + ) + print(f"Access level before update: {my_session.access_level}") + my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + my_session.update(synapse_client=syn) + print(f"Access level after update: {my_session.access_level}") register_and_send_prompt_to_custom_agent() get_baseline_agent_and_send_prompt_to_it() +conduct_multiple_sessions_with_same_agent() +start_new_session_with_baseline_agent_and_send_prompt_to_it() +start_new_session_with_custom_agent_and_update_access_to_it() From 6adde6a7f325c47a5e474d8cd4ea5f4ceeb474b6 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 09:41:46 -0500 Subject: [PATCH 35/64] clean up --- synapseclient/models/agent.py | 3 ++- .../synapseclient/models/synchronous/test_agent.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 8a74a3d61..be5cf2f75 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -320,6 +320,7 @@ async def prompt_async( synapse_client: Optional[Synapse] = None, ) -> None: """Sends a prompt to the agent and adds the response to the AgentSession's chat history. + A session must be started before sending a prompt. Arguments: prompt: The prompt to send to the agent. @@ -368,7 +369,7 @@ class Agent(AgentSynchronousProtocol): syn = Synapse() syn.login() - my_agent = Agent().start_session(synapse_client=syn) + my_agent = Agent() my_agent.prompt( prompt="Can you tell me about the AD Knowledge Portal dataset?", enable_trace=True, diff --git a/tests/integration/synapseclient/models/synchronous/test_agent.py b/tests/integration/synapseclient/models/synchronous/test_agent.py index c3391b098..bce7b3fe6 100644 --- a/tests/integration/synapseclient/models/synchronous/test_agent.py +++ b/tests/integration/synapseclient/models/synchronous/test_agent.py @@ -5,8 +5,8 @@ from synapseclient import Synapse from synapseclient.models.agent import Agent, AgentSession, AgentSessionAccessLevel -AGENT_AWS_ID = "QOTV3KQM1X" -AGENT_REGISTRATION_ID = "29" +CLOUD_AGENT_ID = "QOTV3KQM1X" +AGENT_REGISTRATION_ID = 29 class TestAgentSession: @@ -32,7 +32,7 @@ def test_start(self) -> None: assert result_session.started_on is not None assert result_session.started_by is not None assert result_session.modified_on is not None - assert result_session.agent_registration_id == AGENT_REGISTRATION_ID + assert result_session.agent_registration_id == str(AGENT_REGISTRATION_ID) assert result_session.etag is not None assert result_session.chat_history == [] @@ -98,7 +98,7 @@ def init(self, syn: Synapse) -> None: def test_register(self) -> None: # GIVEN an Agent with a valid agent AWS id - agent = Agent(cloud_agent_id=AGENT_AWS_ID) + agent = Agent(cloud_agent_id=CLOUD_AGENT_ID) # WHEN I register the agent agent.register(synapse_client=self.syn) # THEN I expect the agent to be registered From c20b952c025bf2e0db0c4f55436ca4b12d054838 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 09:44:02 -0500 Subject: [PATCH 36/64] add docstring --- .../object_orientated_programming_poc/oop_poc_agent.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py b/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py index 9b6a5cdef..2703f41a9 100644 --- a/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py +++ b/docs/scripts/object_orientated_programming_poc/oop_poc_agent.py @@ -2,12 +2,18 @@ The purpose of this script is to demonstrate how to use the new OOP interface for Synapse AI Agents. 1. Register and send a prompt to a custom agent -2. Get a baseline agent instance and send a prompt to it +2. Send a prompt to the baseline Synapse Agent +3. Conduct more than one session with the same agent +4. Start a new session with a custom agent and send a prompt to it +5. Start a new session with the baseline Synapse Agent and send a prompt to it +6. Start a new session with a custom agent and then update what the agent has access to """ import synapseclient from synapseclient.models import Agent, AgentSession, AgentSessionAccessLevel +# IDs for a bedrock agent with the instructions: +# "You are a test agent that when greeted with: 'hello' will always response with: 'world'" CLOUD_AGENT_ID = "QOTV3KQM1X" AGENT_REGISTRATION_ID = 29 From 6d80b599a439957c5112c1e8adb46891777e5216 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 09:48:03 -0500 Subject: [PATCH 37/64] removes unused imports --- .../synapseclient/mixins/async/unit_test_asynchronous_job.py | 2 -- tests/unit/synapseclient/models/async/unit_test_agent_async.py | 1 - 2 files changed, 3 deletions(-) diff --git a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py index a5d4c9a91..a7fccb436 100644 --- a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py +++ b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py @@ -1,6 +1,5 @@ """Unit tests for Asynchronous Job logic.""" -import asyncio import json from unittest.mock import AsyncMock, patch @@ -9,7 +8,6 @@ from synapseclient import Synapse from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST from synapseclient.core.exceptions import SynapseError, SynapseTimeoutError -from synapseclient.models.mixins import asynchronous_job from synapseclient.models.mixins.asynchronous_job import ( ASYNC_JOB_URIS, AsynchronousJobState, diff --git a/tests/unit/synapseclient/models/async/unit_test_agent_async.py b/tests/unit/synapseclient/models/async/unit_test_agent_async.py index 1b822465a..8765e8d5c 100644 --- a/tests/unit/synapseclient/models/async/unit_test_agent_async.py +++ b/tests/unit/synapseclient/models/async/unit_test_agent_async.py @@ -1,6 +1,5 @@ """Unit tests for Asynchronous methods in Agent, AgentSession, and AgentPrompt classes.""" -import asyncio from unittest.mock import AsyncMock, patch import pytest From 89db642e7b72ab68bfbd2329bd17111931493378 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 10:02:59 -0500 Subject: [PATCH 38/64] split too long lines --- synapseclient/models/agent.py | 61 ++++++++++++------- .../models/mixins/asynchronous_job.py | 17 +++--- .../models/protocols/agent_protocol.py | 37 +++++++---- .../models/async/test_agent_async.py | 3 +- .../async/unit_test_asynchronous_job.py | 5 +- .../models/async/unit_test_agent_async.py | 21 ++++--- 6 files changed, 92 insertions(+), 52 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index be5cf2f75..2fabd5a3f 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -131,13 +131,16 @@ class AgentSession(AgentSessionSynchronousProtocol): """Represents a [Synapse Agent Session](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/agent/AgentSession.html) Attributes: - id: The unique ID of the agent session. Can only be used by the user that created it. + id: The unique ID of the agent session. + Can only be used by the user that created it. access_level: The access level of the agent session. - One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. + One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, + or WRITE_YOUR_PRIVATE_DATA. started_on: The date the agent session was started. started_by: The ID of the user who started the agent session. modified_on: The date the agent session was last modified. - agent_registration_id: The registration ID of the agent that will be used for this session. + agent_registration_id: The registration ID of the agent that will + be used for this session. etag: The etag of the agent session. Note: It is recommended to use the `Agent` class to start and get sessions, @@ -191,14 +194,15 @@ class AgentSession(AgentSessionSynchronousProtocol): """ id: Optional[str] = None - """The unique ID of the agent session. Can only be used by the user that created it.""" + """The unique ID of the agent session. + Can only be used by the user that created it.""" access_level: Optional[ AgentSessionAccessLevel ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. - One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. - Defaults to PUBLICLY_ACCESSIBLE. + One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or + WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. """ started_on: Optional[datetime] = None @@ -319,14 +323,15 @@ async def prompt_async( *, synapse_client: Optional[Synapse] = None, ) -> None: - """Sends a prompt to the agent and adds the response to the AgentSession's chat history. - A session must be started before sending a prompt. + """Sends a prompt to the agent and adds the response to the AgentSession's + chat history. A session must be started before sending a prompt. Arguments: prompt: The prompt to send to the agent. enable_trace: Whether to enable trace for the prompt. print_response: Whether to print the response to the console. - newer_than: The timestamp to get trace results newer than. Defaults to None (all results). + newer_than: The timestamp to get trace results newer than. + Defaults to None (all results). synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. @@ -362,9 +367,11 @@ class Agent(AgentSynchronousProtocol): Example: Chat with the baseline Synapse Agent - You can chat with the same agent which is available in the Synapse UI at https://www.synapse.org/Chat:default. - By default, this "baseline" agent is used when a registration ID is not provided. In the background, - the Agent class will start a session and set that new session as the current session if one is not already set. + You can chat with the same agent which is available in the Synapse UI + at https://www.synapse.org/Chat:default. By default, this "baseline" agent + is used when a registration ID is not provided. In the background, + the Agent class will start a session and set that new session as the + current session if one is not already set. syn = Synapse() syn.login() @@ -376,9 +383,11 @@ class Agent(AgentSynchronousProtocol): print_response=True, ) - Example: Register and chat with a custom agent **Only available for internal users (Sage Bionetworks employees)** + Example: Register and chat with a custom agent + **Only available for internal users (Sage Bionetworks employees)** - Alternatively, you can register a custom agent and chat with it provided you have already created it. + Alternatively, you can register a custom agent and chat with it provided + you have already created it. syn = Synapse() syn.login(silent=True) @@ -395,11 +404,13 @@ class Agent(AgentSynchronousProtocol): Advanced Example: Start and prompt multiple sessions - Here, we connect to a custom agent and start one session with the prompt "Hello". In the background, - this first session is being set as the current session and future prompts will be sent to this session - by default. If we want to send a prompt to a different session, we can do so by starting it and calling - prompt again, but with our new session as an argument. We now have two sessions, both stored in the - my_agent.sessions dictionary. After the second prompt, my_second_session is now the current session. + Here, we connect to a custom agent and start one session with the prompt "Hello". + In the background, this first session is being set as the current session + and future prompts will be sent to this session by default. If we want to send a + prompt to a different session, we can do so by starting it and calling prompt again, + but with our new session as an argument. We now have two sessions, both stored in the + my_agent.sessions dictionary. After the second prompt, my_second_session is now + the current session. syn = Synapse() syn.login() @@ -469,7 +480,8 @@ def fill_from_dict(self, agent_registration: Dict[str, str]) -> "Agent": async def register_async( self, *, synapse_client: Optional[Synapse] = None ) -> "Agent": - """Registers an agent with the Synapse API. If agent already exists, it will be retrieved. + """Registers an agent with the Synapse API. + If agent already exists, it will be retrieved. Arguments: synapse_client: If not passed in and caching was not disabled by @@ -524,7 +536,8 @@ async def start_session_async( Arguments: access_level: The access level of the agent session. - Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. + Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, + or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created @@ -548,7 +561,8 @@ async def get_session_async( self, session_id: str, *, synapse_client: Optional[Synapse] = None ) -> "AgentSession": """Gets an existing agent session. - Adds the session to the Agent's sessions dictionary and sets it as the current session. + Adds the session to the Agent's sessions dictionary and + sets it as the current session. Arguments: session_id: The ID of the session to get. @@ -587,7 +601,8 @@ async def prompt_async( prompt: The prompt to send to the agent. enable_trace: Whether to enable trace for the prompt. print_response: Whether to print the response to the console. - session_id: The ID of the session to send the prompt to. If None, the current session will be used. + session_id: The ID of the session to send the prompt to. + If None, the current session will be used. newer_than: The timestamp to get trace results newer than. Defaults to None (all results). synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created diff --git a/synapseclient/models/mixins/asynchronous_job.py b/synapseclient/models/mixins/asynchronous_job.py index a759cfe7d..64840cc64 100644 --- a/synapseclient/models/mixins/asynchronous_job.py +++ b/synapseclient/models/mixins/asynchronous_job.py @@ -91,9 +91,10 @@ class CallersContext(str, Enum): """Enum representing information about a web service call: - - SESSION_ID: Each web service request is issued a unique session ID (UUID) that is included in the call's access record. - Events that are triggered by a web service request should include the session ID so that they can be linked to - each other and the call's access record. + - SESSION_ID: Each web service request is issued a unique session ID (UUID) + that is included in the call's access record. + Events that are triggered by a web service request should include the session ID + so that they can be linked to each other and the call's access record. """ SESSION_ID = "SESSION_ID" @@ -107,9 +108,11 @@ class AsynchronousJobStatus: Attributes: state: The state of the job. Either PROCESSING, FAILED, or COMPLETE. canceling: Whether the job has been requested to be cancelled. - request_body: The body of an Asynchronous job request. Will be one of the models described here: + request_body: The body of an Asynchronous job request. + Will be one of the models described here: - response_body: The body of an Asynchronous job response. Will be one of the models described here: + response_body: The body of an Asynchronous job response. + Will be one of the models described here: etag: The etag of the job status. Changes whenever the status changes. id: The ID if the job issued when this job was started. @@ -118,8 +121,8 @@ class AsynchronousJobStatus: changed_on: The date-time when the status of this job was last changed. progress_message: The current message of the progress tracker. progress_current: A value indicating how much progress has been made. - I.e. a value of 50 indicates that 50% of the work has been - completed if progress_total is 100. + I.e. a value of 50 indicates that 50% of the work has been + completed if progress_total is 100. progress_total: A value indicating the total amount of work to complete. exception: The exception that needs to be thrown if the job fails. error_message: A one-line error message when the job fails. diff --git a/synapseclient/models/protocols/agent_protocol.py b/synapseclient/models/protocols/agent_protocol.py index 6c12a03aa..b2235cb7d 100644 --- a/synapseclient/models/protocols/agent_protocol.py +++ b/synapseclient/models/protocols/agent_protocol.py @@ -17,7 +17,8 @@ def start(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": """Starts an agent session. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. Returns: The new AgentSession object. @@ -28,7 +29,8 @@ def get(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": """Gets an existing agent session. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. Returns: The existing AgentSession object. @@ -39,7 +41,8 @@ def update(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": """Updates an existing agent session. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. Returns: The updated AgentSession object. @@ -61,8 +64,10 @@ def prompt( prompt: The prompt to send to the agent. enable_trace: Whether to enable trace for the prompt. print_response: Whether to print the response to the console. - newer_than: The timestamp to get trace results newer than. Defaults to None (all results). - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + newer_than: The timestamp to get trace results newer than. + Defaults to None (all results). + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. """ return None @@ -75,7 +80,8 @@ def register(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": """Registers an agent with the Synapse API. If agent exists, it will be retrieved. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. Returns: The registered or existing Agent object. @@ -86,7 +92,8 @@ def get(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": """Gets an existing agent. Arguments: - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. Returns: The existing Agent object. @@ -103,9 +110,10 @@ def start_session( Adds the session to the Agent's sessions dictionary and sets it as the current session. Arguments: access_level: The access level of the agent session. - Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. - Defaults to PUBLICLY_ACCESSIBLE. - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, + or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. Returns: The new AgentSession object. @@ -145,8 +153,11 @@ def prompt( prompt: The prompt to send to the agent. enable_trace: Whether to enable trace for the prompt. print_response: Whether to print the response to the console. - session_id: The ID of the session to send the prompt to. If None, the current session will be used. - newer_than: The timestamp to get trace results newer than. Defaults to None (all results). - synapse_client: The Synapse client to use for the request. If None, the default client will be used. + session_id: The ID of the session to send the prompt to. + If None, the current session will be used. + newer_than: The timestamp to get trace results newer than. + Defaults to None (all results). + synapse_client: The Synapse client to use for the request. + If None, the default client will be used. """ return None diff --git a/tests/integration/synapseclient/models/async/test_agent_async.py b/tests/integration/synapseclient/models/async/test_agent_async.py index cc47214a2..21745aec3 100644 --- a/tests/integration/synapseclient/models/async/test_agent_async.py +++ b/tests/integration/synapseclient/models/async/test_agent_async.py @@ -83,7 +83,8 @@ async def test_get(self) -> None: assert new_session == agent_session async def test_update(self) -> None: - # GIVEN an agent session with a valid agent registration id and access level set + # GIVEN an agent session with a valid agent + # registration id and access level set agent_session = AgentSession( agent_registration_id=AGENT_REGISTRATION_ID, access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, diff --git a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py index a7fccb436..056976dcc 100644 --- a/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py +++ b/tests/unit/synapseclient/mixins/async/unit_test_asynchronous_job.py @@ -120,7 +120,10 @@ async def test_get_job_async_when_job_fails(self) -> None: ): with pytest.raises( SynapseError, - match=f"{self.failed_job_status.error_message}\n{self.failed_job_status.error_details}", + match=( + f"{self.failed_job_status.error_message}\n" + f"{self.failed_job_status.error_details}" + ), ): # WHEN I call get_job_async # AND the job fails in the Synapse API diff --git a/tests/unit/synapseclient/models/async/unit_test_agent_async.py b/tests/unit/synapseclient/models/async/unit_test_agent_async.py index 8765e8d5c..cb0f405d0 100644 --- a/tests/unit/synapseclient/models/async/unit_test_agent_async.py +++ b/tests/unit/synapseclient/models/async/unit_test_agent_async.py @@ -73,7 +73,8 @@ async def test_post_exchange_async_trace_enabled(self): new_callable=AsyncMock, return_value=self.trace_response, ) as mock_get_trace: - # WHEN I call _post_exchange_async on an initialized AgentPrompt with enable_trace=True + # WHEN I call _post_exchange_async on an + # initialized AgentPrompt with enable_trace=True await self.agent_prompt._post_exchange_async(synapse_client=self.syn) # THEN the mock_get_trace should have been called with the correct arguments mock_get_trace.assert_called_once_with( @@ -91,7 +92,8 @@ async def test_post_exchange_async_trace_disabled(self): return_value=self.trace_response, ) as mock_get_trace: self.agent_prompt.enable_trace = False - # WHEN I call _post_exchange_async on an initialized AgentPrompt with enable_trace=False + # WHEN I call _post_exchange_async on an + # initialized AgentPrompt with enable_trace=False await self.agent_prompt._post_exchange_async(synapse_client=self.syn) # THEN the mock_get_trace should not have been called mock_get_trace.assert_not_called() @@ -122,7 +124,8 @@ async def test_send_job_and_wait_async(self): await self.agent_prompt.send_job_and_wait_async( post_exchange_args={"foo": "bar"}, synapse_client=self.syn ) - # THEN the mock_send_job_and_wait_async should have been called with the correct arguments + # THEN the mock_send_job_and_wait_async should + # have been called with the correct arguments mock_send_job_and_wait_async.assert_called_once_with( request=mock_to_synapse_request.return_value, request_type=self.agent_prompt.concrete_type, @@ -325,9 +328,11 @@ async def test_prompt_trace_enabled_print_response(self) -> None: newer_than=0, synapse_client=self.syn, ) - # THEN the result should be an AgentPrompt with the correct values appended to the chat history + # THEN the result should be an AgentPrompt with the correct + # values appended to the chat history assert self.test_prompt_trace_enabled in self.test_session.chat_history - # AND send_job_and_wait_async should have been called once with the correct arguments + # AND send_job_and_wait_async should have + # been called once with the correct arguments mock_send_job_and_wait_async.assert_called_once_with( synapse_client=self.syn, post_exchange_args={"newer_than": 0} ) @@ -356,9 +361,11 @@ async def test_prompt_trace_disabled_no_print(self) -> None: newer_than=0, synapse_client=self.syn, ) - # THEN the result should be an AgentPrompt with the correct values appended to the chat history + # THEN the result should be an AgentPrompt with the + # correct values appended to the chat history assert self.test_prompt_trace_disabled in self.test_session.chat_history - # AND send_job_and_wait_async should have been called once with the correct arguments + # AND send_job_and_wait_async should have been + # called once with the correct arguments mock_send_job_and_wait_async.assert_called_once_with( synapse_client=self.syn, post_exchange_args={"newer_than": 0} ) From 066504ae597bfb13b635b737f45e6f28fe534dce Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 12:45:37 -0500 Subject: [PATCH 39/64] force synapse_client kwarg --- synapseclient/api/agent_services.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/synapseclient/api/agent_services.py b/synapseclient/api/agent_services.py index 57fdb84c0..6cb65e1fd 100644 --- a/synapseclient/api/agent_services.py +++ b/synapseclient/api/agent_services.py @@ -12,6 +12,7 @@ async def register_agent( cloud_agent_id: str, cloud_alias_id: Optional[str] = None, + *, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ @@ -44,7 +45,9 @@ async def register_agent( async def get_agent( - registration_id: str, synapse_client: Optional["Synapse"] = None + registration_id: str, + *, + synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ Gets information about an existing agent registration. @@ -68,6 +71,7 @@ async def get_agent( async def start_session( access_level: str, agent_registration_id: str, + *, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ @@ -95,6 +99,7 @@ async def start_session( async def get_session( id: str, + *, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ @@ -119,6 +124,7 @@ async def get_session( async def update_session( id: str, access_level: str, + *, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: """ @@ -148,6 +154,7 @@ async def update_session( async def get_trace( prompt_id: str, + *, newer_than: Optional[int] = None, synapse_client: Optional["Synapse"] = None, ) -> Dict[str, Any]: From 807287f2ce86a34eec66ee2513ea8dfcbc3abd76 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:00:15 -0500 Subject: [PATCH 40/64] updates agent.py --- synapseclient/models/agent.py | 190 +++++++++++++++++----------------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 2fabd5a3f..1a9376276 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -143,63 +143,70 @@ class AgentSession(AgentSessionSynchronousProtocol): be used for this session. etag: The etag of the agent session. - Note: It is recommended to use the `Agent` class to start and get sessions, + Note: It is recommended to use the `Agent` class to conduct chat sessions, but you are free to use this class directly if you wish. Example: Start a session and send a prompt. + Start a session with a custom agent by providing the agent's registration ID and calling `start()`. + Then, send a prompt to the agent. - from synapseclient import Synapse - from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel - syn = Synapse() - syn.login() - my_session = AgentSession(agent_registration_id=my_agent_registration_id).start( - synapse_client=syn - ) - my_session.prompt( - prompt="Hello", - enable_trace=True, - print_response=True, - synapse_client=syn, - ) + AGENT_REGISTRATION_ID = 0 # replace with your custom agent's registration id + + syn = Synapse() + syn.login() + + my_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID).start() + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) Example: Get an existing session and send a prompt. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, send a prompt to the agent. - from synapseclient import Synapse - from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel - syn = Synapse() - syn.login() + SESSION_ID = "my_session_id" # replace with your existing session's ID - my_session = AgentSession(id="my_session_id").get(synapse_client=syn) - my_session.prompt( - prompt="Hello", - enable_trace=True, - print_response=True, - synapse_client=syn, - ) + syn = Synapse() + syn.login() + + my_session = AgentSession(id=SESSION_ID).get() + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) Example: Update the access level of an existing session. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, update the access level of the session and call `update()`. - from synapseclient import Synapse - from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel - syn = Synapse() - syn.login() + syn = Synapse() + syn.login() - my_session = AgentSession(id="my_session_id").get(synapse_client=syn) - my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA - my_session.update(synapse_client=syn) + my_session = AgentSession(id="my_session_id").get() + my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + my_session.update() """ id: Optional[str] = None """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[ - AgentSessionAccessLevel - ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + access_level: Optional[AgentSessionAccessLevel] = ( + AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. @@ -366,72 +373,67 @@ class Agent(AgentSynchronousProtocol): type: The type of agent. Example: Chat with the baseline Synapse Agent - - You can chat with the same agent which is available in the Synapse UI - at https://www.synapse.org/Chat:default. By default, this "baseline" agent - is used when a registration ID is not provided. In the background, - the Agent class will start a session and set that new session as the - current session if one is not already set. - - syn = Synapse() - syn.login() - - my_agent = Agent() - my_agent.prompt( - prompt="Can you tell me about the AD Knowledge Portal dataset?", - enable_trace=True, - print_response=True, - ) + You can chat with the same agent which is available in the Synapse UI + at https://www.synapse.org/Chat:default. By default, this "baseline" agent + is used when a registration ID is not provided. In the background, + the Agent class will start a session and set that new session as the + current session if one is not already set. + + syn = Synapse() + syn.login() + + my_agent = Agent() + my_agent.prompt( + prompt="Can you tell me about the AD Knowledge Portal dataset?", + enable_trace=True, + print_response=True, + ) Example: Register and chat with a custom agent - **Only available for internal users (Sage Bionetworks employees)** - - Alternatively, you can register a custom agent and chat with it provided - you have already created it. - - syn = Synapse() - syn.login(silent=True) - - my_agent = Agent(cloud_agent_id=AWS_AGENT_ID) - my_agent.register(synapse_client=syn) - - my_agent.prompt( - prompt="Hello", - enable_trace=True, - print_response=True, - synapse_client=syn, - ) - - Advanced Example: Start and prompt multiple sessions + **Only available for internal users (Sage Bionetworks employees)** - Here, we connect to a custom agent and start one session with the prompt "Hello". - In the background, this first session is being set as the current session - and future prompts will be sent to this session by default. If we want to send a - prompt to a different session, we can do so by starting it and calling prompt again, - but with our new session as an argument. We now have two sessions, both stored in the - my_agent.sessions dictionary. After the second prompt, my_second_session is now - the current session. + Alternatively, you can register a custom agent and chat with it provided + you have already created it. - syn = Synapse() - syn.login() + syn = Synapse() + syn.login(silent=True) - my_agent = Agent(registration_id=my_registration_id).get(synapse_client=syn) + my_agent = Agent(cloud_agent_id=AWS_AGENT_ID) + my_agent.register() - my_agent.prompt( - prompt="Hello", + my_agent.prompt( + prompt="Hello", enable_trace=True, print_response=True, - synapse_client=syn, ) - my_second_session = my_agent.start_session(synapse_client=syn) - my_agent.prompt( - prompt="Hello again", - enable_trace=True, - print_response=True, - session=my_second_session, - synapse_client=syn, - ) + Advanced Example: Start and prompt multiple sessions + Here, we connect to a custom agent and start one session with the prompt "Hello". + In the background, this first session is being set as the current session + and future prompts will be sent to this session by default. If we want to send a + prompt to a different session, we can do so by starting it and calling prompt again, + but with our new session as an argument. We now have two sessions, both stored in the + `my_agent.sessions` dictionary. After the second prompt, `my_second_session` is now + the current session. + + syn = Synapse() + syn.login() + + my_agent = Agent(registration_id=my_registration_id).get() + + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + my_second_session = my_agent.start_session() + my_agent.prompt( + prompt="Hello again", + enable_trace=True, + print_response=True, + session=my_second_session, + ) """ cloud_agent_id: Optional[str] = None @@ -491,11 +493,10 @@ async def register_async( Returns: The registered or existing Agent object. """ - syn = Synapse.get_client(synapse_client=synapse_client) agent_response = await register_agent( cloud_agent_id=self.cloud_agent_id, cloud_alias_id=self.cloud_alias_id, - synapse_client=syn, + synapse_client=synapse_client, ) return self.fill_from_dict(agent_registration=agent_response) @@ -513,10 +514,9 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent Returns: The existing Agent object. """ - syn = Synapse.get_client(synapse_client=synapse_client) agent_response = await get_agent( registration_id=self.registration_id, - synapse_client=syn, + synapse_client=synapse_client, ) return self.fill_from_dict(agent_registration=agent_response) From 6274364fde6420ca28966e95f7719400e67d5993 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:02:34 -0500 Subject: [PATCH 41/64] updates synapse_client docstring description --- .../models/protocols/agent_protocol.py | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/synapseclient/models/protocols/agent_protocol.py b/synapseclient/models/protocols/agent_protocol.py index b2235cb7d..5c30eea6f 100644 --- a/synapseclient/models/protocols/agent_protocol.py +++ b/synapseclient/models/protocols/agent_protocol.py @@ -17,8 +17,9 @@ def start(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": """Starts an agent session. Arguments: - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The new AgentSession object. @@ -29,8 +30,9 @@ def get(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": """Gets an existing agent session. Arguments: - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The existing AgentSession object. @@ -41,8 +43,9 @@ def update(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": """Updates an existing agent session. Arguments: - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The updated AgentSession object. @@ -66,8 +69,9 @@ def prompt( print_response: Whether to print the response to the console. newer_than: The timestamp to get trace results newer than. Defaults to None (all results). - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. """ return None @@ -80,8 +84,9 @@ def register(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": """Registers an agent with the Synapse API. If agent exists, it will be retrieved. Arguments: - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The registered or existing Agent object. @@ -92,8 +97,9 @@ def get(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": """Gets an existing agent. Arguments: - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The existing Agent object. @@ -112,8 +118,9 @@ def start_session( access_level: The access level of the agent session. Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The new AgentSession object. @@ -128,8 +135,9 @@ def get_session( Arguments: session_id: The ID of the session to get. - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: The existing AgentSession object. @@ -157,7 +165,8 @@ def prompt( If None, the current session will be used. newer_than: The timestamp to get trace results newer than. Defaults to None (all results). - synapse_client: The Synapse client to use for the request. - If None, the default client will be used. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. """ return None From 6750ef6c7fb0ed1c86a006b6586753aaf315c3e1 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:20:51 -0500 Subject: [PATCH 42/64] updates asynchronous_job --- .../models/mixins/asynchronous_job.py | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/synapseclient/models/mixins/asynchronous_job.py b/synapseclient/models/mixins/asynchronous_job.py index 64840cc64..aac481663 100644 --- a/synapseclient/models/mixins/asynchronous_job.py +++ b/synapseclient/models/mixins/asynchronous_job.py @@ -53,13 +53,47 @@ async def send_job_and_wait_async( synapse_client: Optional[Synapse] = None, ) -> "AsynchronousCommunicator": """Send the job to the Asynchronous Job service and wait for it to complete. + Intended to be called by a class inheriting from this mixin to start a job + in the Synapse API and wait for it to complete. The inheriting class needs to + represent an asynchronous job request and response and include all necessary attributes. + This was initially implemented to be used in the AgentPrompt class which can be used + as an example. Arguments: post_exchange_args: Additional arguments to pass to the request. - synapse_client: The Synapse client to use for the request. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor. Returns: An instance of this class. + + Example: Using this function + This function was initially implemented to be used in the AgentPrompt class + to send a prompt to an AI agent and wait for the response. It can also be used + in any other class that needs to use an Asynchronous Job. + + The inheriting class (AgentPrompt) will typically not be used directly, but rather + through a higher level class (AgentSession), but this example shows how you would + use this function. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentPrompt + + syn = Synapse() + syn.login() + + agent_prompt = AgentPrompt( + id=None, + session_id="123", + prompt="Hello", + response=None, + enable_trace=True, + trace=None, + ) + # This will fill the id, response, and trace + # attributes with the response from the API + agent_prompt.send_job_and_wait_async() """ result = await send_job_and_wait_async( request=self.to_synapse_request(), @@ -195,7 +229,11 @@ def fill_from_dict(self, async_job_status: dict) -> "AsynchronousJobStatus": Returns: A AsynchronousJobStatus object. """ - self.state = async_job_status.get("jobState", None) + self.state = ( + AsynchronousJobState(async_job_status.get("jobState")) + if async_job_status.get("jobState") + else None + ) self.canceling = async_job_status.get("jobCanceling", None) self.request_body = async_job_status.get("requestBody", None) self.response_body = async_job_status.get("responseBody", None) From af56bee146d22a4b757b05a1883c99a5c7dd27d4 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:27:58 -0500 Subject: [PATCH 43/64] updates integration tests --- .../synapseclient/models/async/test_agent_async.py | 14 +++++++++++--- .../synapseclient/models/synchronous/test_agent.py | 14 ++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/integration/synapseclient/models/async/test_agent_async.py b/tests/integration/synapseclient/models/async/test_agent_async.py index 21745aec3..cacf8dfd0 100644 --- a/tests/integration/synapseclient/models/async/test_agent_async.py +++ b/tests/integration/synapseclient/models/async/test_agent_async.py @@ -11,6 +11,10 @@ AgentSessionAccessLevel, ) +# These are the ID values for a "Hello World" agent registered on Synapse. +# The Bedrock agent is hosted on Sage Bionetworks AWS infrastructure. +# CFN Template: +# https://raw.githubusercontent.com/Sage-Bionetworks-Workflows/dpe-agents/refs/heads/main/client_integration_test/template.json AGENT_AWS_ID = "QOTV3KQM1X" AGENT_REGISTRATION_ID = "29" @@ -95,8 +99,12 @@ async def test_update(self) -> None: agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA await agent_session.update_async(synapse_client=self.syn) # THEN I expect the access level to be updated + updated_session = await AgentSession(id=agent_session.id).get_async( + synapse_client=self.syn + ) assert ( - agent_session.access_level == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + updated_session.access_level + == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA ) async def test_prompt(self) -> None: @@ -121,9 +129,9 @@ class TestAgent: def get_test_agent(self) -> Agent: return Agent( - cloud_agent_id="QOTV3KQM1X", + cloud_agent_id=AGENT_AWS_ID, cloud_alias_id="TSTALIASID", - registration_id="29", + registration_id=AGENT_REGISTRATION_ID, registered_on="2025-01-16T18:57:35.680Z", type="CUSTOM", sessions={}, diff --git a/tests/integration/synapseclient/models/synchronous/test_agent.py b/tests/integration/synapseclient/models/synchronous/test_agent.py index bce7b3fe6..f7cd1844c 100644 --- a/tests/integration/synapseclient/models/synchronous/test_agent.py +++ b/tests/integration/synapseclient/models/synchronous/test_agent.py @@ -5,8 +5,12 @@ from synapseclient import Synapse from synapseclient.models.agent import Agent, AgentSession, AgentSessionAccessLevel +# These are the ID values for a "Hello World" agent registered on Synapse. +# The Bedrock agent is hosted on Sage Bionetworks AWS infrastructure. +# CFN Template: +# https://raw.githubusercontent.com/Sage-Bionetworks-Workflows/dpe-agents/refs/heads/main/client_integration_test/template.json CLOUD_AGENT_ID = "QOTV3KQM1X" -AGENT_REGISTRATION_ID = 29 +AGENT_REGISTRATION_ID = "29" class TestAgentSession: @@ -57,8 +61,10 @@ def test_update(self) -> None: agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA agent_session.update(synapse_client=self.syn) # THEN I expect the access level to be updated + updated_session = AgentSession(id=agent_session.id).get(synapse_client=self.syn) assert ( - agent_session.access_level == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + updated_session.access_level + == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA ) def test_prompt(self) -> None: @@ -83,9 +89,9 @@ class TestAgent: def get_test_agent(self) -> Agent: return Agent( - cloud_agent_id="QOTV3KQM1X", + cloud_agent_id=CLOUD_AGENT_ID, cloud_alias_id="TSTALIASID", - registration_id="29", + registration_id=AGENT_REGISTRATION_ID, registered_on="2025-01-16T18:57:35.680Z", type="CUSTOM", sessions={}, From 724a8b0c962c98ffeabe5130f4487d9bd1f08d2f Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:32:38 -0500 Subject: [PATCH 44/64] pre-commit --- synapseclient/models/agent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 1a9376276..d1ae8f9b4 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -204,9 +204,9 @@ class AgentSession(AgentSessionSynchronousProtocol): """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[AgentSessionAccessLevel] = ( - AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. From a96b1a9b971ccdd2a6ba294d12e73a01b5e9bd45 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:40:43 -0500 Subject: [PATCH 45/64] agent inherited members --- docs/reference/experimental/sync/agent.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/experimental/sync/agent.md b/docs/reference/experimental/sync/agent.md index 1f9dbce33..06e2ec016 100644 --- a/docs/reference/experimental/sync/agent.md +++ b/docs/reference/experimental/sync/agent.md @@ -14,6 +14,7 @@ ::: synapseclient.models.Agent options: + inherited_members: true members: - register - get From f860fa6898225ee8501a0435d7783797a2ac1495 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:49:32 -0500 Subject: [PATCH 46/64] updates docs for inherited members --- docs/reference/experimental/sync/activity.md | 1 + docs/reference/experimental/sync/team.md | 1 + docs/reference/experimental/sync/user_profile.md | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/reference/experimental/sync/activity.md b/docs/reference/experimental/sync/activity.md index ed05409f9..d70563d43 100644 --- a/docs/reference/experimental/sync/activity.md +++ b/docs/reference/experimental/sync/activity.md @@ -14,6 +14,7 @@ ::: synapseclient.models.Activity options: + inherited_members: true members: - from_parent - store diff --git a/docs/reference/experimental/sync/team.md b/docs/reference/experimental/sync/team.md index 1af58f260..06d62b2fc 100644 --- a/docs/reference/experimental/sync/team.md +++ b/docs/reference/experimental/sync/team.md @@ -14,6 +14,7 @@ ::: synapseclient.models.Team options: + inherited_members: true members: - create - delete diff --git a/docs/reference/experimental/sync/user_profile.md b/docs/reference/experimental/sync/user_profile.md index 552532bcc..94f65e441 100644 --- a/docs/reference/experimental/sync/user_profile.md +++ b/docs/reference/experimental/sync/user_profile.md @@ -4,6 +4,7 @@ ::: synapseclient.models.UserProfile options: + inherited_members: true members: - get - from_id From 1ca93d89f91cbf906b658f019b176c9726121395 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 13:55:52 -0500 Subject: [PATCH 47/64] missing inherited members --- docs/reference/experimental/async/activity.md | 1 + docs/reference/experimental/async/team.md | 1 + docs/reference/experimental/async/user_profile.md | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/reference/experimental/async/activity.md b/docs/reference/experimental/async/activity.md index 6ec94b55f..2f1408c28 100644 --- a/docs/reference/experimental/async/activity.md +++ b/docs/reference/experimental/async/activity.md @@ -4,6 +4,7 @@ ::: synapseclient.models.Activity options: + inherited_members: true members: - from_parent_async - store_async diff --git a/docs/reference/experimental/async/team.md b/docs/reference/experimental/async/team.md index 46a83c623..f031f7a61 100644 --- a/docs/reference/experimental/async/team.md +++ b/docs/reference/experimental/async/team.md @@ -4,6 +4,7 @@ ::: synapseclient.models.Team options: + inherited_members: true members: - create_async - delete_async diff --git a/docs/reference/experimental/async/user_profile.md b/docs/reference/experimental/async/user_profile.md index 1f7449255..960a88eb9 100644 --- a/docs/reference/experimental/async/user_profile.md +++ b/docs/reference/experimental/async/user_profile.md @@ -4,6 +4,7 @@ ::: synapseclient.models.UserProfile options: + inherited_members: true members: - get_async - from_id_async From 7537f4e510143c3531dd23771b0659c24d4d915a Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 14:06:31 -0500 Subject: [PATCH 48/64] updates doc formatting --- docs/reference/experimental/async/activity.md | 7 +++---- docs/reference/experimental/async/agent.md | 1 + docs/reference/experimental/async/user_profile.md | 3 +++ docs/reference/experimental/sync/activity.md | 6 +++--- docs/reference/experimental/sync/agent.md | 3 +++ docs/reference/experimental/sync/team.md | 14 +++++++------- docs/reference/experimental/sync/user_profile.md | 9 +++++---- 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/reference/experimental/async/activity.md b/docs/reference/experimental/async/activity.md index 2f1408c28..aefe78578 100644 --- a/docs/reference/experimental/async/activity.md +++ b/docs/reference/experimental/async/activity.md @@ -4,11 +4,10 @@ ::: synapseclient.models.Activity options: - inherited_members: true members: - - from_parent_async - - store_async - - delete_async + - from_parent_async + - store_async + - delete_async --- ::: synapseclient.models.UsedEntity options: diff --git a/docs/reference/experimental/async/agent.md b/docs/reference/experimental/async/agent.md index 8d8a6581f..956f721b9 100644 --- a/docs/reference/experimental/async/agent.md +++ b/docs/reference/experimental/async/agent.md @@ -22,6 +22,7 @@ --- ::: synapseclient.models.AgentPrompt options: + inherited_members: true members: - send_job_and_wait_async --- diff --git a/docs/reference/experimental/async/user_profile.md b/docs/reference/experimental/async/user_profile.md index 960a88eb9..e2bdd46af 100644 --- a/docs/reference/experimental/async/user_profile.md +++ b/docs/reference/experimental/async/user_profile.md @@ -10,3 +10,6 @@ - from_id_async - from_username_async - is_certified_async +--- +::: synapseclient.models.UserPreference +--- diff --git a/docs/reference/experimental/sync/activity.md b/docs/reference/experimental/sync/activity.md index d70563d43..4ab8f2ab2 100644 --- a/docs/reference/experimental/sync/activity.md +++ b/docs/reference/experimental/sync/activity.md @@ -16,9 +16,9 @@ options: inherited_members: true members: - - from_parent - - store - - delete + - from_parent + - store + - delete --- ::: synapseclient.models.UsedEntity options: diff --git a/docs/reference/experimental/sync/agent.md b/docs/reference/experimental/sync/agent.md index 06e2ec016..8e30229f9 100644 --- a/docs/reference/experimental/sync/agent.md +++ b/docs/reference/experimental/sync/agent.md @@ -25,6 +25,7 @@ --- ::: synapseclient.models.AgentSession options: + inherited_members: true members: - start - get @@ -32,4 +33,6 @@ - prompt --- ::: synapseclient.models.AgentPrompt + options: + inherited_members: true --- diff --git a/docs/reference/experimental/sync/team.md b/docs/reference/experimental/sync/team.md index 06d62b2fc..68e632de9 100644 --- a/docs/reference/experimental/sync/team.md +++ b/docs/reference/experimental/sync/team.md @@ -16,10 +16,10 @@ options: inherited_members: true members: - - create - - delete - - from_id - - from_name - - members - - invite - - open_invitations + - create + - delete + - from_id + - from_name + - members + - invite + - open_invitations diff --git a/docs/reference/experimental/sync/user_profile.md b/docs/reference/experimental/sync/user_profile.md index 94f65e441..eaf4fb0f3 100644 --- a/docs/reference/experimental/sync/user_profile.md +++ b/docs/reference/experimental/sync/user_profile.md @@ -6,9 +6,10 @@ options: inherited_members: true members: - - get - - from_id - - from_username - - is_certified + - get + - from_id + - from_username + - is_certified --- ::: synapseclient.models.UserPreference +--- From d2f4ade522b4749e9ac12d6fd39780140fbf6ace Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 14:18:22 -0500 Subject: [PATCH 49/64] try team formatting change --- docs/reference/experimental/async/team.md | 16 ++++++++-------- docs/reference/experimental/sync/team.md | 15 ++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/reference/experimental/async/team.md b/docs/reference/experimental/async/team.md index f031f7a61..decf22401 100644 --- a/docs/reference/experimental/async/team.md +++ b/docs/reference/experimental/async/team.md @@ -4,12 +4,12 @@ ::: synapseclient.models.Team options: - inherited_members: true members: - - create_async - - delete_async - - from_id_async - - from_name_async - - members_async - - invite_async - - open_invitations_async + - create_async + - delete_async + - from_id_async + - from_name_async + - members_async + - invite_async + - open_invitations_async +--- diff --git a/docs/reference/experimental/sync/team.md b/docs/reference/experimental/sync/team.md index 68e632de9..39c4728b9 100644 --- a/docs/reference/experimental/sync/team.md +++ b/docs/reference/experimental/sync/team.md @@ -16,10 +16,11 @@ options: inherited_members: true members: - - create - - delete - - from_id - - from_name - - members - - invite - - open_invitations + - create + - delete + - from_id + - from_name + - members + - invite + - open_invitations +--- From 9b4a168e9b912c2404e76891d6d0fa86e398ffb5 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 14:20:50 -0500 Subject: [PATCH 50/64] updates script description --- docs/reference/experimental/sync/agent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/experimental/sync/agent.md b/docs/reference/experimental/sync/agent.md index 8e30229f9..e179054ac 100644 --- a/docs/reference/experimental/sync/agent.md +++ b/docs/reference/experimental/sync/agent.md @@ -3,7 +3,7 @@ ## Example Script:
- Working with a Synapse Agent + Working with Synapse agents ```python {!docs/scripts/object_orientated_programming_poc/oop_poc_agent.py!} From f567d4cade462d2fc17dd2c6800714ab40dbec5a Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 15:17:11 -0500 Subject: [PATCH 51/64] adds Annotation lazy import --- synapseclient/models/services/storable_entity_components.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapseclient/models/services/storable_entity_components.py b/synapseclient/models/services/storable_entity_components.py index 9f5d52c05..8eafa5739 100644 --- a/synapseclient/models/services/storable_entity_components.py +++ b/synapseclient/models/services/storable_entity_components.py @@ -6,7 +6,7 @@ from synapseclient.core.exceptions import SynapseError if TYPE_CHECKING: - from synapseclient.models import Annotations, File, Folder, Project, Table + from synapseclient.models import File, Folder, Project, Table class FailureStrategy(Enum): @@ -242,6 +242,8 @@ async def _store_activity_and_annotations( or last_persistent_instance.annotations != root_resource.annotations ) ): + from synapseclient.models import Annotations + result = await Annotations( id=root_resource.id, etag=root_resource.etag, From aede05f0c78132c1251433993b033d3dd17a12e0 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 15:20:53 -0500 Subject: [PATCH 52/64] try team formatting change --- docs/reference/experimental/sync/team.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/reference/experimental/sync/team.md b/docs/reference/experimental/sync/team.md index 39c4728b9..0b018ee8e 100644 --- a/docs/reference/experimental/sync/team.md +++ b/docs/reference/experimental/sync/team.md @@ -14,13 +14,13 @@ ::: synapseclient.models.Team options: - inherited_members: true - members: - - create - - delete - - from_id - - from_name - - members - - invite - - open_invitations + inherited_members: true + members: + - create + - delete + - from_id + - from_name + - members + - invite + - open_invitations --- From 34be93d4785525b8734167d5994297a358b2fcf8 Mon Sep 17 00:00:00 2001 From: bwmac Date: Fri, 17 Jan 2025 15:24:12 -0500 Subject: [PATCH 53/64] more formatting changes --- docs/reference/experimental/sync/agent.md | 12 ++++++------ docs/reference/experimental/sync/team.md | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/reference/experimental/sync/agent.md b/docs/reference/experimental/sync/agent.md index e179054ac..911c332af 100644 --- a/docs/reference/experimental/sync/agent.md +++ b/docs/reference/experimental/sync/agent.md @@ -16,12 +16,12 @@ options: inherited_members: true members: - - register - - get - - start_session - - get_session - - prompt - - get_chat_history + - register + - get + - start_session + - get_session + - prompt + - get_chat_history --- ::: synapseclient.models.AgentSession options: diff --git a/docs/reference/experimental/sync/team.md b/docs/reference/experimental/sync/team.md index 0b018ee8e..39c4728b9 100644 --- a/docs/reference/experimental/sync/team.md +++ b/docs/reference/experimental/sync/team.md @@ -14,13 +14,13 @@ ::: synapseclient.models.Team options: - inherited_members: true - members: - - create - - delete - - from_id - - from_name - - members - - invite - - open_invitations + inherited_members: true + members: + - create + - delete + - from_id + - from_name + - members + - invite + - open_invitations --- From 1c900653141d360fd5259ed08963169415d4d9c7 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 22 Jan 2025 14:12:07 -0500 Subject: [PATCH 54/64] address review comments in agent.py --- synapseclient/models/agent.py | 60 +++++++++++++------ .../models/async/unit_test_agent_async.py | 20 +++---- .../models/synchronous/unit_test_agent.py | 20 +++---- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index d1ae8f9b4..fa676b4ff 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -153,13 +153,10 @@ class AgentSession(AgentSessionSynchronousProtocol): from synapseclient import Synapse from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel - - AGENT_REGISTRATION_ID = 0 # replace with your custom agent's registration id - syn = Synapse() syn.login() - my_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID).start() + my_session = AgentSession(agent_registration_id="foo").start() my_session.prompt( prompt="Hello", enable_trace=True, @@ -173,12 +170,10 @@ class AgentSession(AgentSessionSynchronousProtocol): from synapseclient import Synapse from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel - SESSION_ID = "my_session_id" # replace with your existing session's ID - syn = Synapse() syn.login() - my_session = AgentSession(id=SESSION_ID).get() + my_session = AgentSession(id="foo").get() my_session.prompt( prompt="Hello", enable_trace=True, @@ -195,7 +190,7 @@ class AgentSession(AgentSessionSynchronousProtocol): syn = Synapse() syn.login() - my_session = AgentSession(id="my_session_id").get() + my_session = AgentSession(id="foo").get() my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA my_session.update() """ @@ -353,10 +348,10 @@ async def prompt_async( self.chat_history.append(agent_prompt) if print_response: - print(f"PROMPT:\n{prompt}\n") - print(f"RESPONSE:\n{agent_prompt.response}\n") + synapse_client.logger.info(f"PROMPT:\n{prompt}\n") + synapse_client.logger.info(f"RESPONSE:\n{agent_prompt.response}\n") if enable_trace: - print(f"TRACE:\n{agent_prompt.trace}") + synapse_client.logger.info(f"TRACE:\n{agent_prompt.trace}") @dataclass @@ -368,9 +363,11 @@ class Agent(AgentSynchronousProtocol): cloud_agent_id: The unique ID of the agent in the cloud provider. cloud_alias_id: The alias ID of the agent in the cloud provider. Defaults to 'TSTALIASID' in the Synapse API. - synapse_registration_id: The ID number of the agent assigned by Synapse. + registration_id: The ID number of the agent assigned by Synapse. registered_on: The date the agent was registered. type: The type of agent. + sessions: A dictionary of AgentSession objects, keyed by session ID. + current_session: The current session. Prompts will be sent to this session by default. Example: Chat with the baseline Synapse Agent You can chat with the same agent which is available in the Synapse UI @@ -379,6 +376,9 @@ class Agent(AgentSynchronousProtocol): the Agent class will start a session and set that new session as the current session if one is not already set. + from synapseclient import Synapse + from synapseclient.models.agent import Agent + syn = Synapse() syn.login() @@ -395,17 +395,20 @@ class Agent(AgentSynchronousProtocol): Alternatively, you can register a custom agent and chat with it provided you have already created it. + from synapseclient import Synapse + from synapseclient.models.agent import Agent + syn = Synapse() - syn.login(silent=True) + syn.login() - my_agent = Agent(cloud_agent_id=AWS_AGENT_ID) + my_agent = Agent(cloud_agent_id="foo") my_agent.register() my_agent.prompt( prompt="Hello", - enable_trace=True, - print_response=True, - ) + enable_trace=True, + print_response=True, + ) Advanced Example: Start and prompt multiple sessions Here, we connect to a custom agent and start one session with the prompt "Hello". @@ -419,7 +422,7 @@ class Agent(AgentSynchronousProtocol): syn = Synapse() syn.login() - my_agent = Agent(registration_id=my_registration_id).get() + my_agent = Agent(registration_id="foo").get() my_agent.prompt( prompt="Hello", @@ -473,7 +476,11 @@ def fill_from_dict(self, agent_registration: Dict[str, str]) -> "Agent": self.cloud_alias_id = agent_registration.get("awsAliasId", None) self.registration_id = agent_registration.get("agentRegistrationId", None) self.registered_on = agent_registration.get("registeredOn", None) - self.type = agent_registration.get("type", None) + self.type = ( + AgentType(agent_registration.get("type")) + if agent_registration.get("type", None) + else None + ) return self @otel_trace_method( @@ -607,6 +614,21 @@ async def prompt_async( synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. + + Example: Prompt the baseline Synapse Agent to add annotations to a file on Synapse + The baseline Synpase Agent can be used to add annotations to files. + + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + my_agent = Agent() + my_agent.prompt( + prompt="Add the annotation 'test' to the file 'syn123456789'", + enable_trace=True, + print_response=True, + ) """ if session: await self.get_session_async( diff --git a/tests/unit/synapseclient/models/async/unit_test_agent_async.py b/tests/unit/synapseclient/models/async/unit_test_agent_async.py index cb0f405d0..290094301 100644 --- a/tests/unit/synapseclient/models/async/unit_test_agent_async.py +++ b/tests/unit/synapseclient/models/async/unit_test_agent_async.py @@ -314,10 +314,10 @@ async def test_prompt_trace_enabled_print_response(self) -> None: new_callable=AsyncMock, return_value=self.test_prompt_trace_enabled, ) as mock_send_job_and_wait_async, - patch( - "synapseclient.models.agent.print", - side_effect=print, - ) as mock_print, + patch.object( + self.syn.logger, + "info", + ) as mock_logger_info, ): # GIVEN an existing AgentSession # WHEN I call prompt with trace enabled and print_response enabled @@ -337,7 +337,7 @@ async def test_prompt_trace_enabled_print_response(self) -> None: synapse_client=self.syn, post_exchange_args={"newer_than": 0} ) # AND the trace should be printed - mock_print.assert_called_with( + mock_logger_info.assert_called_with( f"TRACE:\n{self.test_prompt_trace_enabled.trace}" ) @@ -348,10 +348,10 @@ async def test_prompt_trace_disabled_no_print(self) -> None: new_callable=AsyncMock, return_value=self.test_prompt_trace_disabled, ) as mock_send_job_and_wait_async, - patch( - "synapseclient.models.agent.print", - side_effect=print, - ) as mock_print, + patch.object( + self.syn.logger, + "info", + ) as mock_logger_info, ): # WHEN I call prompt with trace disabled and print_response disabled await self.test_session.prompt_async( @@ -370,7 +370,7 @@ async def test_prompt_trace_disabled_no_print(self) -> None: synapse_client=self.syn, post_exchange_args={"newer_than": 0} ) # AND print should not have been called - mock_print.assert_not_called() + mock_logger_info.assert_not_called() class TestAgent: diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_agent.py b/tests/unit/synapseclient/models/synchronous/unit_test_agent.py index 951f30ed8..83f33cb7b 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_agent.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_agent.py @@ -218,10 +218,10 @@ def test_prompt_trace_enabled_print_response(self) -> None: "synapseclient.models.agent.AgentPrompt.send_job_and_wait_async", return_value=self.test_prompt_trace_enabled, ) as mock_send_job_and_wait_async, - patch( - "synapseclient.models.agent.print", - side_effect=print, - ) as mock_print, + patch.object( + self.syn.logger, + "info", + ) as mock_logger_info, ): # GIVEN an existing AgentSession # WHEN I call prompt with trace enabled and print_response enabled @@ -239,7 +239,7 @@ def test_prompt_trace_enabled_print_response(self) -> None: synapse_client=self.syn, post_exchange_args={"newer_than": 0} ) # AND the trace should be printed - mock_print.assert_called_with( + mock_logger_info.assert_called_with( f"TRACE:\n{self.test_prompt_trace_enabled.trace}" ) @@ -249,10 +249,10 @@ def test_prompt_trace_disabled_no_print(self) -> None: "synapseclient.models.agent.AgentPrompt.send_job_and_wait_async", return_value=self.test_prompt_trace_disabled, ) as mock_send_job_and_wait_async, - patch( - "synapseclient.models.agent.print", - side_effect=print, - ) as mock_print, + patch.object( + self.syn.logger, + "info", + ) as mock_logger_info, ): # WHEN I call prompt with trace disabled and print_response disabled self.test_session.prompt( @@ -269,7 +269,7 @@ def test_prompt_trace_disabled_no_print(self) -> None: synapse_client=self.syn, post_exchange_args={"newer_than": 0} ) # AND print should not have been called - mock_print.assert_not_called() + mock_logger_info.assert_not_called() class TestAgent: From ad5df6d99ee94edfa122252dd2a28e9004d8f632 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 22 Jan 2025 14:16:24 -0500 Subject: [PATCH 55/64] move synchronous docs up a layer --- mkdocs.yml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index da15fd324..68f9e0053 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,20 +75,14 @@ nav: - Core: reference/core.md - REST Apis: reference/rest_apis.md - Experimental: - - Mixins: - - AccessControllable: reference/experimental/mixins/access_controllable.md - - StorableContainer: reference/experimental/mixins/storable_container.md - - AsynchronousCommunicator: reference/experimental/mixins/asynchronous_communicator.md - - FailureStrategy: reference/experimental/mixins/failure_strategy.md - - Synchronous: - - Agent: reference/experimental/sync/agent.md - - Project: reference/experimental/sync/project.md - - Folder: reference/experimental/sync/folder.md - - File: reference/experimental/sync/file.md - - Table: reference/experimental/sync/table.md - - Activity: reference/experimental/sync/activity.md - - Team: reference/experimental/sync/team.md - - UserProfile: reference/experimental/sync/user_profile.md + - Agent: reference/experimental/sync/agent.md + - Project: reference/experimental/sync/project.md + - Folder: reference/experimental/sync/folder.md + - File: reference/experimental/sync/file.md + - Table: reference/experimental/sync/table.md + - Activity: reference/experimental/sync/activity.md + - Team: reference/experimental/sync/team.md + - UserProfile: reference/experimental/sync/user_profile.md - Asynchronous: - Agent: reference/experimental/async/agent.md - Project: reference/experimental/async/project.md @@ -98,6 +92,11 @@ nav: - Activity: reference/experimental/async/activity.md - Team: reference/experimental/async/team.md - UserProfile: reference/experimental/async/user_profile.md + - Mixins: + - AccessControllable: reference/experimental/mixins/access_controllable.md + - StorableContainer: reference/experimental/mixins/storable_container.md + - AsynchronousCommunicator: reference/experimental/mixins/asynchronous_communicator.md + - FailureStrategy: reference/experimental/mixins/failure_strategy.md - Further Reading: - Home: explanations/home.md - Domain Models of Synapse: explanations/domain_models_of_synapse.md From 4d6cfa96ec33d45296959b63459594c618e88c92 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 22 Jan 2025 14:23:55 -0500 Subject: [PATCH 56/64] adds syn login --- synapseclient/models/agent.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index fa676b4ff..89654cf36 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -199,9 +199,9 @@ class AgentSession(AgentSessionSynchronousProtocol): """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[ - AgentSessionAccessLevel - ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + access_level: Optional[AgentSessionAccessLevel] = ( + AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE + ) """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. @@ -338,20 +338,18 @@ async def prompt_async( `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. """ - agent_prompt = await AgentPrompt( prompt=prompt, session_id=self.id, enable_trace=enable_trace ).send_job_and_wait_async( synapse_client=synapse_client, post_exchange_args={"newer_than": newer_than} ) - self.chat_history.append(agent_prompt) - if print_response: - synapse_client.logger.info(f"PROMPT:\n{prompt}\n") - synapse_client.logger.info(f"RESPONSE:\n{agent_prompt.response}\n") + client = Synapse.get_client(synapse_client=synapse_client) + client.logger.info(f"PROMPT:\n{prompt}\n") + client.logger.info(f"RESPONSE:\n{agent_prompt.response}\n") if enable_trace: - synapse_client.logger.info(f"TRACE:\n{agent_prompt.trace}") + client.logger.info(f"TRACE:\n{agent_prompt.trace}") @dataclass From 9b67e63a50522c31b23af7d6f1bcfada2cf43fa9 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 22 Jan 2025 14:28:49 -0500 Subject: [PATCH 57/64] adds warning message to docs --- docs/reference/experimental/async/activity.md | 4 ++++ docs/reference/experimental/async/agent.md | 4 ++++ docs/reference/experimental/async/file.md | 4 ++++ docs/reference/experimental/async/folder.md | 4 ++++ docs/reference/experimental/async/project.md | 4 ++++ docs/reference/experimental/async/table.md | 4 ++++ docs/reference/experimental/async/team.md | 4 ++++ docs/reference/experimental/async/user_profile.md | 4 ++++ docs/reference/experimental/sync/activity.md | 4 ++++ docs/reference/experimental/sync/agent.md | 4 ++++ docs/reference/experimental/sync/file.md | 4 ++++ docs/reference/experimental/sync/folder.md | 4 ++++ docs/reference/experimental/sync/project.md | 4 ++++ docs/reference/experimental/sync/table.md | 4 ++++ docs/reference/experimental/sync/team.md | 4 ++++ docs/reference/experimental/sync/user_profile.md | 4 ++++ synapseclient/models/agent.py | 6 +++--- 17 files changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/reference/experimental/async/activity.md b/docs/reference/experimental/async/activity.md index aefe78578..59e2f0061 100644 --- a/docs/reference/experimental/async/activity.md +++ b/docs/reference/experimental/async/activity.md @@ -1,5 +1,9 @@ # Activity +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API Reference ::: synapseclient.models.Activity diff --git a/docs/reference/experimental/async/agent.md b/docs/reference/experimental/async/agent.md index 956f721b9..be2e74c36 100644 --- a/docs/reference/experimental/async/agent.md +++ b/docs/reference/experimental/async/agent.md @@ -1,5 +1,9 @@ # Agent +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API reference ::: synapseclient.models.Agent diff --git a/docs/reference/experimental/async/file.md b/docs/reference/experimental/async/file.md index 3cd2d60aa..e2fe12300 100644 --- a/docs/reference/experimental/async/file.md +++ b/docs/reference/experimental/async/file.md @@ -1,5 +1,9 @@ # File +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API Reference ::: synapseclient.models.File diff --git a/docs/reference/experimental/async/folder.md b/docs/reference/experimental/async/folder.md index fa9e41a52..c11983a99 100644 --- a/docs/reference/experimental/async/folder.md +++ b/docs/reference/experimental/async/folder.md @@ -1,5 +1,9 @@ # Folder +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API Reference ::: synapseclient.models.Folder diff --git a/docs/reference/experimental/async/project.md b/docs/reference/experimental/async/project.md index d4fde2b15..b628d4e19 100644 --- a/docs/reference/experimental/async/project.md +++ b/docs/reference/experimental/async/project.md @@ -1,5 +1,9 @@ # Project +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API reference ::: synapseclient.models.Project diff --git a/docs/reference/experimental/async/table.md b/docs/reference/experimental/async/table.md index c314c37ba..63f3b3a0b 100644 --- a/docs/reference/experimental/async/table.md +++ b/docs/reference/experimental/async/table.md @@ -1,5 +1,9 @@ # Table +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API Reference ::: synapseclient.models.Table diff --git a/docs/reference/experimental/async/team.md b/docs/reference/experimental/async/team.md index decf22401..0dd066e35 100644 --- a/docs/reference/experimental/async/team.md +++ b/docs/reference/experimental/async/team.md @@ -1,5 +1,9 @@ # Team +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API Reference ::: synapseclient.models.Team diff --git a/docs/reference/experimental/async/user_profile.md b/docs/reference/experimental/async/user_profile.md index e2bdd46af..7174061d9 100644 --- a/docs/reference/experimental/async/user_profile.md +++ b/docs/reference/experimental/async/user_profile.md @@ -1,5 +1,9 @@ # UserProfile +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API Reference ::: synapseclient.models.UserProfile diff --git a/docs/reference/experimental/sync/activity.md b/docs/reference/experimental/sync/activity.md index 4ab8f2ab2..f0547e13c 100644 --- a/docs/reference/experimental/sync/activity.md +++ b/docs/reference/experimental/sync/activity.md @@ -1,5 +1,9 @@ # Activity +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## Example Script
diff --git a/docs/reference/experimental/sync/agent.md b/docs/reference/experimental/sync/agent.md index 911c332af..3d8cb7f08 100644 --- a/docs/reference/experimental/sync/agent.md +++ b/docs/reference/experimental/sync/agent.md @@ -1,5 +1,9 @@ # Agent +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## Example Script:
diff --git a/docs/reference/experimental/sync/file.md b/docs/reference/experimental/sync/file.md index 9f858a79d..9b49e7603 100644 --- a/docs/reference/experimental/sync/file.md +++ b/docs/reference/experimental/sync/file.md @@ -1,5 +1,9 @@ # File +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## Example Script
diff --git a/docs/reference/experimental/sync/folder.md b/docs/reference/experimental/sync/folder.md index 204ae7046..5a1cb5ddb 100644 --- a/docs/reference/experimental/sync/folder.md +++ b/docs/reference/experimental/sync/folder.md @@ -1,5 +1,9 @@ # Folder +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## Example Script
diff --git a/docs/reference/experimental/sync/project.md b/docs/reference/experimental/sync/project.md index 953039a20..e8cebfed5 100644 --- a/docs/reference/experimental/sync/project.md +++ b/docs/reference/experimental/sync/project.md @@ -1,5 +1,9 @@ # Project +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## Example Script
diff --git a/docs/reference/experimental/sync/table.md b/docs/reference/experimental/sync/table.md index 7b04af0cc..058826d0d 100644 --- a/docs/reference/experimental/sync/table.md +++ b/docs/reference/experimental/sync/table.md @@ -1,5 +1,9 @@ # Table +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## Example Script
diff --git a/docs/reference/experimental/sync/team.md b/docs/reference/experimental/sync/team.md index 39c4728b9..46fc51305 100644 --- a/docs/reference/experimental/sync/team.md +++ b/docs/reference/experimental/sync/team.md @@ -1,5 +1,9 @@ # Team +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## Example Script
diff --git a/docs/reference/experimental/sync/user_profile.md b/docs/reference/experimental/sync/user_profile.md index eaf4fb0f3..46424f4b5 100644 --- a/docs/reference/experimental/sync/user_profile.md +++ b/docs/reference/experimental/sync/user_profile.md @@ -1,5 +1,9 @@ # UserProfile +Contained within this file are experimental interfaces for working with the Synapse Python +Client. Unless otherwise noted these interfaces are subject to change at any time. Use +at your own risk. + ## API Reference ::: synapseclient.models.UserProfile diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 89654cf36..4b74bc73c 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -199,9 +199,9 @@ class AgentSession(AgentSessionSynchronousProtocol): """The unique ID of the agent session. Can only be used by the user that created it.""" - access_level: Optional[AgentSessionAccessLevel] = ( - AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) + access_level: Optional[ + AgentSessionAccessLevel + ] = AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE """The access level of the agent session. One of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. From 13c8a0b52e757f099e21555f3a3c228175c14bdc Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 22 Jan 2025 16:11:59 -0500 Subject: [PATCH 58/64] updates docstring examples --- synapseclient/models/agent.py | 243 +++++++++++++++++- .../models/protocols/agent_protocol.py | 238 ++++++++++++++++- 2 files changed, 466 insertions(+), 15 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 4b74bc73c..af230d0b7 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -144,14 +144,14 @@ class AgentSession(AgentSessionSynchronousProtocol): etag: The etag of the agent session. Note: It is recommended to use the `Agent` class to conduct chat sessions, - but you are free to use this class directly if you wish. + but you are free to use AgentSession directly if you wish. Example: Start a session and send a prompt. Start a session with a custom agent by providing the agent's registration ID and calling `start()`. Then, send a prompt to the agent. from synapseclient import Synapse - from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + from synapseclient.models.agent import AgentSession syn = Synapse() syn.login() @@ -168,7 +168,7 @@ class AgentSession(AgentSessionSynchronousProtocol): Then, send a prompt to the agent. from synapseclient import Synapse - from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + from synapseclient.models.agent import AgentSession syn = Synapse() syn.login() @@ -259,6 +259,23 @@ async def start_async( Returns: The new AgentSession object. + + Example: Start a session and send a prompt. + Start a session with a custom agent by providing the agent's registration ID and calling `start()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession + + syn = Synapse() + syn.login() + + my_session = await AgentSession(agent_registration_id="foo").start_async() + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ session_response = await start_session( access_level=self.access_level, @@ -282,6 +299,23 @@ async def get_async( Returns: The retrieved AgentSession object. + + Example: Get an existing session and send a prompt. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession + + syn = Synapse() + syn.login() + + my_session = await AgentSession(id="foo").get_async() + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ session_response = await get_session( id=self.id, @@ -307,6 +341,20 @@ async def update_async( Returns: The updated AgentSession object. + + Example: Update the access level of an existing session. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, update the access level of the session and call `update()`. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + + syn = Synapse() + syn.login() + + my_session = await AgentSession(id="foo").get_async() + my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + await my_session.update_async() """ session_response = await update_session( id=self.id, @@ -337,6 +385,23 @@ async def prompt_async( synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. + + Example: Send a prompt within an existing session. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession + + syn = Synapse() + syn.login() + + my_session = await AgentSession(id="foo").get_async() + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ agent_prompt = await AgentPrompt( prompt=prompt, session_id=self.id, enable_trace=enable_trace @@ -401,7 +466,23 @@ class Agent(AgentSynchronousProtocol): my_agent = Agent(cloud_agent_id="foo") my_agent.register() + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + Example: Get and chat with an existing agent + Retrieve an existing agent by providing the agent's registration ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + my_agent = Agent(registration_id="foo").get() my_agent.prompt( prompt="Hello", enable_trace=True, @@ -497,6 +578,27 @@ async def register_async( Returns: The registered or existing Agent object. + + Example: Register and chat with a custom agent + **Only available for internal users (Sage Bionetworks employees)** + + Alternatively, you can register a custom agent and chat with it provided + you have already created it. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent(cloud_agent_id="foo") + await my_agent.register_async() + + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ agent_response = await register_agent( cloud_agent_id=self.cloud_agent_id, @@ -518,6 +620,23 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent Returns: The existing Agent object. + + Example: Get and chat with an existing agent + Retrieve an existing agent by providing the agent's registration ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = await Agent(registration_id="foo").get_async() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ agent_response = await get_agent( registration_id=self.registration_id, @@ -550,6 +669,40 @@ async def start_session_async( Returns: The new AgentSession object. + + Example: Start a session and send a prompt with the baseline Synapse Agent. + The baseline Synapse Agent is the default agent used when a registration ID is not provided. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent() + await my_agent.start_session_async() + await my_agent.prompt_async( + prompt="Can you tell me about the AD Knowledge Portal dataset?", + enable_trace=True, + print_response=True, + ) + + Example: Start a session and send a prompt with a custom agent. + The baseline Synapse Agent is the default agent used when a registration ID is not provided. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent(cloud_agent_id="foo") + await my_agent.start_session_async() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ access_level = AgentSessionAccessLevel(access_level) session = await AgentSession( @@ -577,6 +730,23 @@ async def get_session_async( Returns: The existing AgentSession object. + + Example: Get an existing session and send a prompt. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_session = await Agent().get_session_async(session_id="foo") + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ session = await AgentSession(id=session_id).get_async( synapse_client=synapse_client @@ -622,11 +792,55 @@ async def prompt_async( syn.login() my_agent = Agent() - my_agent.prompt( + await my_agent.prompt_async( prompt="Add the annotation 'test' to the file 'syn123456789'", enable_trace=True, print_response=True, ) + + Example: Prompt a custom agent. + If you have already registered a custom agent, you can prompt it by providing the agent's registration ID. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent(registration_id="foo") + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + Advanced Example: Start and prompt multiple sessions + Here, we connect to a custom agent and start one session with the prompt "Hello". + In the background, this first session is being set as the current session + and future prompts will be sent to this session by default. If we want to send a + prompt to a different session, we can do so by starting it and calling prompt again, + but with our new session as an argument. We now have two sessions, both stored in the + `my_agent.sessions` dictionary. After the second prompt, `my_second_session` is now + the current session. + + syn = Synapse() + syn.login() + + my_agent = Agent(registration_id="foo").get() + + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + my_second_session = await my_agent.start_session_async() + await my_agent.prompt_async( + prompt="Hello again", + enable_trace=True, + print_response=True, + session=my_second_session, + ) """ if session: await self.get_session_async( @@ -645,5 +859,24 @@ async def prompt_async( ) def get_chat_history(self) -> Union[List[AgentPrompt], None]: - """Gets the chat history for the current session.""" + """Gets the chat history for the current session. + + Example: Get the chat history for the current session. + First, send a prompt to the agent. + Then, retrieve the chat history for the current session by calling `get_chat_history()`. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + print(my_agent.get_chat_history()) + """ return self.current_session.chat_history if self.current_session else None diff --git a/synapseclient/models/protocols/agent_protocol.py b/synapseclient/models/protocols/agent_protocol.py index 5c30eea6f..ef52bf3a3 100644 --- a/synapseclient/models/protocols/agent_protocol.py +++ b/synapseclient/models/protocols/agent_protocol.py @@ -23,11 +23,28 @@ def start(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": Returns: The new AgentSession object. + + Example: Start a session and send a prompt. + Start a session with a custom agent by providing the agent's registration ID and calling `start()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession + + syn = Synapse() + syn.login() + + my_session = AgentSession(agent_registration_id="foo").start() + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ return self def get(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": - """Gets an existing agent session. + """Gets an agent session. Arguments: synapse_client: If not passed in and caching was not disabled by @@ -35,12 +52,30 @@ def get(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": instance from the Synapse class constructor. Returns: - The existing AgentSession object. + The retrieved AgentSession object. + + Example: Get an existing session and send a prompt. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession + + syn = Synapse() + syn.login() + + my_session = AgentSession(id="foo").get() + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ return self def update(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": - """Updates an existing agent session. + """Updates an agent session. + Only updates to the access level are currently supported. Arguments: synapse_client: If not passed in and caching was not disabled by @@ -49,6 +84,20 @@ def update(self, *, synapse_client: Optional[Synapse] = None) -> "AgentSession": Returns: The updated AgentSession object. + + Example: Update the access level of an existing session. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, update the access level of the session and call `update()`. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel + + syn = Synapse() + syn.login() + + my_session = AgentSession(id="foo").get() + my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + my_session.update() """ return self @@ -61,7 +110,8 @@ def prompt( *, synapse_client: Optional[Synapse] = None, ) -> None: - """Sends a prompt to the agent and adds the response to the AgentSession's chat history. + """Sends a prompt to the agent and adds the response to the AgentSession's + chat history. A session must be started before sending a prompt. Arguments: prompt: The prompt to send to the agent. @@ -72,6 +122,23 @@ def prompt( synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. + + Example: Send a prompt within an existing session. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import AgentSession + + syn = Synapse() + syn.login() + + my_session = AgentSession(id="foo").get() + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ return None @@ -81,7 +148,8 @@ class AgentSynchronousProtocol(Protocol): generated at runtime.""" def register(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": - """Registers an agent with the Synapse API. If agent exists, it will be retrieved. + """Registers an agent with the Synapse API. + If agent already exists, it will be retrieved. Arguments: synapse_client: If not passed in and caching was not disabled by @@ -90,6 +158,27 @@ def register(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": Returns: The registered or existing Agent object. + + Example: Register and chat with a custom agent + **Only available for internal users (Sage Bionetworks employees)** + + Alternatively, you can register a custom agent and chat with it provided + you have already created it. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent(cloud_agent_id="foo") + my_agent.register() + + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ return self @@ -103,6 +192,23 @@ def get(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": Returns: The existing Agent object. + + Example: Get and chat with an existing agent + Retrieve an existing agent by providing the agent's registration ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent(registration_id="foo").get() + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ return self @@ -114,16 +220,52 @@ def start_session( ) -> "AgentSession": """Starts an agent session. Adds the session to the Agent's sessions dictionary and sets it as the current session. + Arguments: access_level: The access level of the agent session. Must be one of PUBLICLY_ACCESSIBLE, READ_YOUR_PRIVATE_DATA, - or WRITE_YOUR_PRIVATE_DATA. Defaults to PUBLICLY_ACCESSIBLE. + or WRITE_YOUR_PRIVATE_DATA. + Defaults to PUBLICLY_ACCESSIBLE. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. Returns: The new AgentSession object. + + Example: Start a session and send a prompt with the baseline Synapse Agent. + The baseline Synapse Agent is the default agent used when a registration ID is not provided. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent() + my_agent.start_session() + my_agent.prompt( + prompt="Can you tell me about the AD Knowledge Portal dataset?", + enable_trace=True, + print_response=True, + ) + + Example: Start a session and send a prompt with a custom agent. + The baseline Synapse Agent is the default agent used when a registration ID is not provided. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent(cloud_agent_id="foo") + my_agent.start_session() + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ return AgentSession() @@ -131,7 +273,8 @@ def get_session( self, session_id: str, *, synapse_client: Optional[Synapse] = None ) -> "AgentSession": """Gets an existing agent session. - Adds the session to the Agent's sessions dictionary and sets it as the current session. + Adds the session to the Agent's sessions dictionary and + sets it as the current session. Arguments: session_id: The ID of the session to get. @@ -141,6 +284,23 @@ def get_session( Returns: The existing AgentSession object. + + Example: Get an existing session and send a prompt. + Retrieve an existing session by providing the session ID and calling `get()`. + Then, send a prompt to the agent. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_session = Agent().get_session(session_id="foo") + my_session.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) """ return AgentSession() @@ -155,7 +315,7 @@ def prompt( synapse_client: Optional[Synapse] = None, ) -> None: """Sends a prompt to the agent for the current session. - If no session is currently active, a new session will be started. + If no session is currently active, a new session will be started. Arguments: prompt: The prompt to send to the agent. @@ -163,10 +323,68 @@ def prompt( print_response: Whether to print the response to the console. session_id: The ID of the session to send the prompt to. If None, the current session will be used. - newer_than: The timestamp to get trace results newer than. - Defaults to None (all results). + newer_than: The timestamp to get trace results newer than. Defaults to None (all results). synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor. + + Example: Prompt the baseline Synapse Agent to add annotations to a file on Synapse + The baseline Synpase Agent can be used to add annotations to files. + + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + my_agent = Agent() + my_agent.prompt( + prompt="Add the annotation 'test' to the file 'syn123456789'", + enable_trace=True, + print_response=True, + ) + + Example: Prompt a custom agent. + If you have already registered a custom agent, you can prompt it by providing the agent's registration ID. + + from synapseclient import Synapse + from synapseclient.models.agent import Agent + + syn = Synapse() + syn.login() + + my_agent = Agent(registration_id="foo") + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + Advanced Example: Start and prompt multiple sessions + Here, we connect to a custom agent and start one session with the prompt "Hello". + In the background, this first session is being set as the current session + and future prompts will be sent to this session by default. If we want to send a + prompt to a different session, we can do so by starting it and calling prompt again, + but with our new session as an argument. We now have two sessions, both stored in the + `my_agent.sessions` dictionary. After the second prompt, `my_second_session` is now + the current session. + + syn = Synapse() + syn.login() + + my_agent = Agent(registration_id="foo").get() + + my_agent.prompt( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + my_second_session = my_agent.start_session() + my_agent.prompt( + prompt="Hello again", + enable_trace=True, + print_response=True, + session=my_second_session, + ) """ return None From 6cd5e5c28787928e36511f7154db394ebc4e4b99 Mon Sep 17 00:00:00 2001 From: bwmac Date: Wed, 22 Jan 2025 18:06:05 -0500 Subject: [PATCH 59/64] updates docstrings --- synapseclient/models/agent.py | 236 +++++++++++++++++++++------------- 1 file changed, 145 insertions(+), 91 deletions(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index af230d0b7..0bf570bf4 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -264,18 +264,22 @@ async def start_async( Start a session with a custom agent by providing the agent's registration ID and calling `start()`. Then, send a prompt to the agent. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import AgentSession syn = Synapse() syn.login() - my_session = await AgentSession(agent_registration_id="foo").start_async() - await my_session.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + async def main(): + my_session = await AgentSession(agent_registration_id="foo").start_async() + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) """ session_response = await start_session( access_level=self.access_level, @@ -304,18 +308,22 @@ async def get_async( Retrieve an existing session by providing the session ID and calling `get()`. Then, send a prompt to the agent. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import AgentSession syn = Synapse() syn.login() - my_session = await AgentSession(id="foo").get_async() - await my_session.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + async def main(): + my_session = await AgentSession(id="foo").get_async() + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) """ session_response = await get_session( id=self.id, @@ -346,15 +354,19 @@ async def update_async( Retrieve an existing session by providing the session ID and calling `get()`. Then, update the access level of the session and call `update()`. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import AgentSession, AgentSessionAccessLevel syn = Synapse() syn.login() - my_session = await AgentSession(id="foo").get_async() - my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA - await my_session.update_async() + async def main(): + my_session = await AgentSession(id="foo").get_async() + my_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA + await my_session.update_async() + + asyncio.run(main()) """ session_response = await update_session( id=self.id, @@ -390,18 +402,22 @@ async def prompt_async( Retrieve an existing session by providing the session ID and calling `get()`. Then, send a prompt to the agent. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import AgentSession syn = Synapse() syn.login() - my_session = await AgentSession(id="foo").get_async() - await my_session.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + async def main(): + my_session = await AgentSession(id="foo").get_async() + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) """ agent_prompt = await AgentPrompt( prompt=prompt, session_id=self.id, enable_trace=enable_trace @@ -585,20 +601,23 @@ async def register_async( Alternatively, you can register a custom agent and chat with it provided you have already created it. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import Agent syn = Synapse() syn.login() - my_agent = Agent(cloud_agent_id="foo") - await my_agent.register_async() + async def main(): + my_agent = Agent(cloud_agent_id="foo") + await my_agent.register_async() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) - await my_agent.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + asyncio.run(main()) """ agent_response = await register_agent( cloud_agent_id=self.cloud_agent_id, @@ -625,18 +644,22 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent Retrieve an existing agent by providing the agent's registration ID and calling `get()`. Then, send a prompt to the agent. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import Agent syn = Synapse() syn.login() - my_agent = await Agent(registration_id="foo").get_async() - await my_agent.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + async def main(): + my_agent = await Agent(registration_id="foo").get_async() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) """ agent_response = await get_agent( registration_id=self.registration_id, @@ -673,36 +696,44 @@ async def start_session_async( Example: Start a session and send a prompt with the baseline Synapse Agent. The baseline Synapse Agent is the default agent used when a registration ID is not provided. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import Agent syn = Synapse() syn.login() - my_agent = Agent() - await my_agent.start_session_async() - await my_agent.prompt_async( - prompt="Can you tell me about the AD Knowledge Portal dataset?", - enable_trace=True, - print_response=True, - ) + async def main(): + my_agent = Agent() + await my_agent.start_session_async() + await my_agent.prompt_async( + prompt="Can you tell me about the AD Knowledge Portal dataset?", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) Example: Start a session and send a prompt with a custom agent. The baseline Synapse Agent is the default agent used when a registration ID is not provided. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import Agent syn = Synapse() syn.login() - my_agent = Agent(cloud_agent_id="foo") - await my_agent.start_session_async() - await my_agent.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + async def main(): + my_agent = Agent(cloud_agent_id="foo") + await my_agent.start_session_async() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) """ access_level = AgentSessionAccessLevel(access_level) session = await AgentSession( @@ -735,18 +766,22 @@ async def get_session_async( Retrieve an existing session by providing the session ID and calling `get()`. Then, send a prompt to the agent. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import Agent syn = Synapse() syn.login() - my_session = await Agent().get_session_async(session_id="foo") - await my_session.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + async def main(): + my_session = await Agent().get_session_async(session_id="foo") + await my_session.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) """ session = await AgentSession(id=session_id).get_async( synapse_client=synapse_client @@ -786,33 +821,42 @@ async def prompt_async( Example: Prompt the baseline Synapse Agent to add annotations to a file on Synapse The baseline Synpase Agent can be used to add annotations to files. - from synapseclient import Synapse + import asyncio + from synapseclient import Synapse + from synapseclient.models.agent import Agent - syn = Synapse() - syn.login() + syn = Synapse() + syn.login() - my_agent = Agent() - await my_agent.prompt_async( - prompt="Add the annotation 'test' to the file 'syn123456789'", - enable_trace=True, - print_response=True, - ) + async def main(): + my_agent = Agent() + await my_agent.prompt_async( + prompt="Add the annotation 'test' to the file 'syn123456789'", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) Example: Prompt a custom agent. If you have already registered a custom agent, you can prompt it by providing the agent's registration ID. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import Agent syn = Synapse() syn.login() - my_agent = Agent(registration_id="foo") - await my_agent.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) + async def main(): + my_agent = Agent(registration_id="foo") + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + asyncio.run(main()) Advanced Example: Start and prompt multiple sessions Here, we connect to a custom agent and start one session with the prompt "Hello". @@ -823,24 +867,30 @@ async def prompt_async( `my_agent.sessions` dictionary. After the second prompt, `my_second_session` is now the current session. + import asyncio + from synapseclient import Synapse + from synapseclient.models.agent import Agent + syn = Synapse() syn.login() - my_agent = Agent(registration_id="foo").get() - - await my_agent.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) - - my_second_session = await my_agent.start_session_async() - await my_agent.prompt_async( - prompt="Hello again", - enable_trace=True, - print_response=True, - session=my_second_session, - ) + async def main(): + my_agent = Agent(registration_id="foo").get() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + + my_second_session = await my_agent.start_session_async() + await my_agent.prompt_async( + prompt="Hello again", + enable_trace=True, + print_response=True, + session=my_second_session, + ) + + asyncio.run(main()) """ if session: await self.get_session_async( @@ -865,18 +915,22 @@ def get_chat_history(self) -> Union[List[AgentPrompt], None]: First, send a prompt to the agent. Then, retrieve the chat history for the current session by calling `get_chat_history()`. + import asyncio from synapseclient import Synapse from synapseclient.models.agent import Agent syn = Synapse() syn.login() - my_agent = Agent() - await my_agent.prompt_async( - prompt="Hello", - enable_trace=True, - print_response=True, - ) - print(my_agent.get_chat_history()) + async def main(): + my_agent = Agent() + await my_agent.prompt_async( + prompt="Hello", + enable_trace=True, + print_response=True, + ) + print(my_agent.get_chat_history()) + + asyncio.run(main()) """ return self.current_session.chat_history if self.current_session else None From 151b96bc7c71a5ca1d930acdbc70c6f30816d709 Mon Sep 17 00:00:00 2001 From: bwmac Date: Thu, 23 Jan 2025 14:04:03 -0500 Subject: [PATCH 60/64] adds error handling for agent.get --- synapseclient/models/agent.py | 9 ++++++++- .../synapseclient/models/async/test_agent_async.py | 7 +++++++ .../synapseclient/models/synchronous/test_agent.py | 7 +++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 0bf570bf4..e1ce6bea7 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -641,7 +641,7 @@ async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent The existing Agent object. Example: Get and chat with an existing agent - Retrieve an existing agent by providing the agent's registration ID and calling `get()`. + Retrieve an existing custom agent by providing the agent's registration ID and calling `get_async()`. Then, send a prompt to the agent. import asyncio @@ -661,6 +661,13 @@ async def main(): asyncio.run(main()) """ + if not self.registration_id: + raise ValueError( + "Registration ID is required to retrieve a custom agent. " + "If you are trying to use the baseline agent, you do not need to " + "use `get` or `get_async`. Instead, simply create an `Agent` object " + "and start prompting `my_agent = Agent(); my_agent.prompt(...)`.", + ) agent_response = await get_agent( registration_id=self.registration_id, synapse_client=synapse_client, diff --git a/tests/integration/synapseclient/models/async/test_agent_async.py b/tests/integration/synapseclient/models/async/test_agent_async.py index cacf8dfd0..478581066 100644 --- a/tests/integration/synapseclient/models/async/test_agent_async.py +++ b/tests/integration/synapseclient/models/async/test_agent_async.py @@ -160,6 +160,13 @@ async def test_get(self) -> None: expected_agent = self.get_test_agent() assert agent == expected_agent + async def test_get_no_registration_id(self) -> None: + # GIVEN an Agent with no registration id + agent = Agent() + # WHEN I get the agent, I expect a ValueError to be raised + with pytest.raises(ValueError, match="Registration ID is required"): + await agent.get_async(synapse_client=self.syn) + async def test_start_session(self) -> None: # GIVEN an Agent with a valid agent registration id agent = Agent(registration_id=AGENT_REGISTRATION_ID) diff --git a/tests/integration/synapseclient/models/synchronous/test_agent.py b/tests/integration/synapseclient/models/synchronous/test_agent.py index f7cd1844c..1b06e1a26 100644 --- a/tests/integration/synapseclient/models/synchronous/test_agent.py +++ b/tests/integration/synapseclient/models/synchronous/test_agent.py @@ -120,6 +120,13 @@ def test_get(self) -> None: expected_agent = self.get_test_agent() assert agent == expected_agent + def test_get_no_registration_id(self) -> None: + # GIVEN an Agent with no registration id + agent = Agent() + # WHEN I get the agent, I expect a ValueError to be raised + with pytest.raises(ValueError, match="Registration ID is required"): + agent.get(synapse_client=self.syn) + def test_start_session(self) -> None: # GIVEN an Agent with a valid agent registration id agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( From 5a741d2c054ffa9210fafb7b78cbc0744acd64ee Mon Sep 17 00:00:00 2001 From: bwmac Date: Mon, 27 Jan 2025 08:14:38 -0500 Subject: [PATCH 61/64] async integration tests --- .../models/synchronous/test_agent.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/integration/synapseclient/models/synchronous/test_agent.py b/tests/integration/synapseclient/models/synchronous/test_agent.py index 1b06e1a26..d081de023 100644 --- a/tests/integration/synapseclient/models/synchronous/test_agent.py +++ b/tests/integration/synapseclient/models/synchronous/test_agent.py @@ -20,7 +20,7 @@ class TestAgentSession: def init(self, syn: Synapse) -> None: self.syn = syn - def test_start(self) -> None: + async def test_start(self) -> None: # GIVEN an agent session with a valid agent registration id agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) @@ -40,7 +40,7 @@ def test_start(self) -> None: assert result_session.etag is not None assert result_session.chat_history == [] - def test_get(self) -> None: + async def test_get(self) -> None: # GIVEN an agent session with a valid agent registration id agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) # WHEN I start a session @@ -49,7 +49,7 @@ def test_get(self) -> None: new_session = AgentSession(id=agent_session.id).get(synapse_client=self.syn) assert new_session == agent_session - def test_update(self) -> None: + async def test_update(self) -> None: # GIVEN an agent session with a valid agent registration id and access level set agent_session = AgentSession( agent_registration_id=AGENT_REGISTRATION_ID, @@ -67,7 +67,7 @@ def test_update(self) -> None: == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA ) - def test_prompt(self) -> None: + async def test_prompt(self) -> None: # GIVEN an agent session with a valid agent registration id agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) # WHEN I start a session @@ -102,7 +102,7 @@ def get_test_agent(self) -> Agent: def init(self, syn: Synapse) -> None: self.syn = syn - def test_register(self) -> None: + async def test_register(self) -> None: # GIVEN an Agent with a valid agent AWS id agent = Agent(cloud_agent_id=CLOUD_AGENT_ID) # WHEN I register the agent @@ -111,7 +111,7 @@ def test_register(self) -> None: expected_agent = self.get_test_agent() assert agent == expected_agent - def test_get(self) -> None: + async def test_get(self) -> None: # GIVEN an Agent with a valid agent registration id agent = Agent(registration_id=AGENT_REGISTRATION_ID) # WHEN I get the agent @@ -120,14 +120,14 @@ def test_get(self) -> None: expected_agent = self.get_test_agent() assert agent == expected_agent - def test_get_no_registration_id(self) -> None: + async def test_get_no_registration_id(self) -> None: # GIVEN an Agent with no registration id agent = Agent() # WHEN I get the agent, I expect a ValueError to be raised with pytest.raises(ValueError, match="Registration ID is required"): agent.get(synapse_client=self.syn) - def test_start_session(self) -> None: + async def test_start_session(self) -> None: # GIVEN an Agent with a valid agent registration id agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( synapse_client=self.syn @@ -139,7 +139,7 @@ def test_start_session(self) -> None: # AND I expect the session to be in the sessions dictionary assert agent.sessions[agent.current_session.id] == agent.current_session - def test_get_session(self) -> None: + async def test_get_session(self) -> None: # GIVEN an Agent with a valid agent registration id agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( synapse_client=self.syn @@ -153,7 +153,7 @@ def test_get_session(self) -> None: # AND I expect it to be the current session assert existing_session == agent.current_session - def test_prompt_with_session(self) -> None: + async def test_prompt_with_session(self) -> None: # GIVEN an Agent with a valid agent registration id agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( synapse_client=self.syn @@ -173,7 +173,7 @@ def test_prompt_with_session(self) -> None: # AND I expect the current session to be the session provided assert agent.current_session.id == session.id - def test_prompt_no_session(self) -> None: + async def test_prompt_no_session(self) -> None: # GIVEN an Agent with a valid agent registration id agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( synapse_client=self.syn From 84105f993a2e6c138b2369aa042c1f41f8914a1d Mon Sep 17 00:00:00 2001 From: bwmac Date: Mon, 27 Jan 2025 14:39:49 -0500 Subject: [PATCH 62/64] fix conditional --- synapseclient/models/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index e1ce6bea7..0a5cefb3d 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -661,7 +661,7 @@ async def main(): asyncio.run(main()) """ - if not self.registration_id: + if self.registration_id is None: raise ValueError( "Registration ID is required to retrieve a custom agent. " "If you are trying to use the baseline agent, you do not need to " From 82c4196d0ea6b5a04a042397ccff97d9939de57d Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 28 Jan 2025 10:32:30 -0500 Subject: [PATCH 63/64] disables integration tests --- .../models/async/test_agent_async.py | 450 +++++++++--------- .../models/synchronous/test_agent.py | 378 +++++++-------- 2 files changed, 416 insertions(+), 412 deletions(-) diff --git a/tests/integration/synapseclient/models/async/test_agent_async.py b/tests/integration/synapseclient/models/async/test_agent_async.py index 478581066..dd7ef53e4 100644 --- a/tests/integration/synapseclient/models/async/test_agent_async.py +++ b/tests/integration/synapseclient/models/async/test_agent_async.py @@ -1,226 +1,228 @@ """Integration tests for the asynchronous methods of the AgentPrompt, AgentSession, and Agent classes.""" -import pytest - -from synapseclient import Synapse -from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST -from synapseclient.models.agent import ( - Agent, - AgentPrompt, - AgentSession, - AgentSessionAccessLevel, -) - -# These are the ID values for a "Hello World" agent registered on Synapse. -# The Bedrock agent is hosted on Sage Bionetworks AWS infrastructure. -# CFN Template: -# https://raw.githubusercontent.com/Sage-Bionetworks-Workflows/dpe-agents/refs/heads/main/client_integration_test/template.json -AGENT_AWS_ID = "QOTV3KQM1X" -AGENT_REGISTRATION_ID = "29" - - -class TestAgentPrompt: - """Integration tests for the synchronous methods of the AgentPrompt class.""" - - @pytest.fixture(autouse=True, scope="function") - def init(self, syn: Synapse) -> None: - self.syn = syn - - async def test_send_job_and_wait_async_with_post_exchange_args(self) -> None: - # GIVEN an AgentPrompt with a valid concrete type, prompt, and enable_trace - test_prompt = AgentPrompt( - concrete_type=AGENT_CHAT_REQUEST, - prompt="hello", - enable_trace=True, - ) - # AND the ID of an existing agent session - test_session = await AgentSession( - agent_registration_id=AGENT_REGISTRATION_ID - ).start_async(synapse_client=self.syn) - test_prompt.session_id = test_session.id - # WHEN I send the job and wait for it to complete - await test_prompt.send_job_and_wait_async( - post_exchange_args={"newer_than": 0}, - synapse_client=self.syn, - ) - # THEN I expect the AgentPrompt to be updated with the response and trace - assert test_prompt.response is not None - assert test_prompt.trace is not None - - -class TestAgentSession: - """Integration tests for the synchronous methods of the AgentSession class.""" - - @pytest.fixture(autouse=True, scope="function") - def init(self, syn: Synapse) -> None: - self.syn = syn - - async def test_start(self) -> None: - # GIVEN an agent session with a valid agent registration id - agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) - - # WHEN the start method is called - result_session = await agent_session.start_async(synapse_client=self.syn) - - # THEN the result should be an AgentSession object - # with expected attributes including an empty chat history - assert result_session.id is not None - assert ( - result_session.access_level == AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) - assert result_session.started_on is not None - assert result_session.started_by is not None - assert result_session.modified_on is not None - assert result_session.agent_registration_id == AGENT_REGISTRATION_ID - assert result_session.etag is not None - assert result_session.chat_history == [] - - async def test_get(self) -> None: - # GIVEN an agent session with a valid agent registration id - agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) - # WHEN I start a session - await agent_session.start_async(synapse_client=self.syn) - # THEN I expect to be able to get the session with its id - new_session = await AgentSession(id=agent_session.id).get_async( - synapse_client=self.syn - ) - assert new_session == agent_session - - async def test_update(self) -> None: - # GIVEN an agent session with a valid agent - # registration id and access level set - agent_session = AgentSession( - agent_registration_id=AGENT_REGISTRATION_ID, - access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, - ) - # WHEN I start a session - await agent_session.start_async(synapse_client=self.syn) - # AND I update the access level of the session - agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA - await agent_session.update_async(synapse_client=self.syn) - # THEN I expect the access level to be updated - updated_session = await AgentSession(id=agent_session.id).get_async( - synapse_client=self.syn - ) - assert ( - updated_session.access_level - == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA - ) - - async def test_prompt(self) -> None: - # GIVEN an agent session with a valid agent registration id - agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) - # WHEN I start a session - await agent_session.start_async(synapse_client=self.syn) - # THEN I expect to be able to prompt the agent - await agent_session.prompt_async( - prompt="hello", - enable_trace=True, - ) - # AND I expect the chat history to be updated with the prompt and response - assert len(agent_session.chat_history) == 1 - assert agent_session.chat_history[0].prompt == "hello" - assert agent_session.chat_history[0].response is not None - assert agent_session.chat_history[0].trace is not None - - -class TestAgent: - """Integration tests for the synchronous methods of the Agent class.""" - - def get_test_agent(self) -> Agent: - return Agent( - cloud_agent_id=AGENT_AWS_ID, - cloud_alias_id="TSTALIASID", - registration_id=AGENT_REGISTRATION_ID, - registered_on="2025-01-16T18:57:35.680Z", - type="CUSTOM", - sessions={}, - current_session=None, - ) - - @pytest.fixture(autouse=True, scope="function") - def init(self, syn: Synapse) -> None: - self.syn = syn - - async def test_register(self) -> None: - # GIVEN an Agent with a valid agent AWS id - agent = Agent(cloud_agent_id=AGENT_AWS_ID) - # WHEN I register the agent - await agent.register_async(synapse_client=self.syn) - # THEN I expect the agent to be registered - expected_agent = self.get_test_agent() - assert agent == expected_agent - - async def test_get(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID) - # WHEN I get the agent - await agent.get_async(synapse_client=self.syn) - # THEN I expect the agent to be returned - expected_agent = self.get_test_agent() - assert agent == expected_agent - - async def test_get_no_registration_id(self) -> None: - # GIVEN an Agent with no registration id - agent = Agent() - # WHEN I get the agent, I expect a ValueError to be raised - with pytest.raises(ValueError, match="Registration ID is required"): - await agent.get_async(synapse_client=self.syn) - - async def test_start_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID) - # WHEN I start a session - await agent.start_session_async(synapse_client=self.syn) - # THEN I expect a current session to be set - assert agent.current_session is not None - # AND I expect the session to be in the sessions dictionary - assert agent.sessions[agent.current_session.id] == agent.current_session - - async def test_get_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID) - # WHEN I start a session - await agent.start_session_async(synapse_client=self.syn) - # THEN I expect to be able to get the session with its id - existing_session = await agent.get_session_async( - session_id=agent.current_session.id - ) - # AND I expect those sessions to be the same - assert existing_session == agent.current_session - - async def test_prompt_with_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = await Agent(registration_id=AGENT_REGISTRATION_ID).get_async( - synapse_client=self.syn - ) - # AND a session started separately - session = await AgentSession( - agent_registration_id=AGENT_REGISTRATION_ID - ).start_async(synapse_client=self.syn) - # WHEN I prompt the agent with a session - await agent.prompt_async(prompt="hello", enable_trace=True, session=session) - test_session = agent.sessions[session.id] - # THEN I expect the chat history to be updated with the prompt and response - assert len(test_session.chat_history) == 1 - assert test_session.chat_history[0].prompt == "hello" - assert test_session.chat_history[0].response is not None - assert test_session.chat_history[0].trace is not None - # AND I expect the current session to be the session provided - assert agent.current_session.id == session.id - - async def test_prompt_no_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = await Agent(registration_id=AGENT_REGISTRATION_ID).get_async( - synapse_client=self.syn - ) - # WHEN I prompt the agent without a current session set - # and no session provided - await agent.prompt_async(prompt="hello", enable_trace=True) - # THEN I expect a new session to be started and set as the current session - assert agent.current_session is not None - # AND I expect the chat history to be updated with the prompt and response - assert len(agent.current_session.chat_history) == 1 - assert agent.current_session.chat_history[0].prompt == "hello" - assert agent.current_session.chat_history[0].response is not None - assert agent.current_session.chat_history[0].trace is not None +# These tests have been disabled until out `test` user has needed permissions +# Context: https://sagebionetworks.jira.com/browse/SYNPY-1544?focusedCommentId=235070 +# import pytest + +# from synapseclient import Synapse +# from synapseclient.core.constants.concrete_types import AGENT_CHAT_REQUEST +# from synapseclient.models.agent import ( +# Agent, +# AgentPrompt, +# AgentSession, +# AgentSessionAccessLevel, +# ) + +# # These are the ID values for a "Hello World" agent registered on Synapse. +# # The Bedrock agent is hosted on Sage Bionetworks AWS infrastructure. +# # CFN Template: +# # https://raw.githubusercontent.com/Sage-Bionetworks-Workflows/dpe-agents/refs/heads/main/client_integration_test/template.json +# AGENT_AWS_ID = "QOTV3KQM1X" +# AGENT_REGISTRATION_ID = "29" + + +# class TestAgentPrompt: +# """Integration tests for the synchronous methods of the AgentPrompt class.""" + +# @pytest.fixture(autouse=True, scope="function") +# def init(self, syn: Synapse) -> None: +# self.syn = syn + +# async def test_send_job_and_wait_async_with_post_exchange_args(self) -> None: +# # GIVEN an AgentPrompt with a valid concrete type, prompt, and enable_trace +# test_prompt = AgentPrompt( +# concrete_type=AGENT_CHAT_REQUEST, +# prompt="hello", +# enable_trace=True, +# ) +# # AND the ID of an existing agent session +# test_session = await AgentSession( +# agent_registration_id=AGENT_REGISTRATION_ID +# ).start_async(synapse_client=self.syn) +# test_prompt.session_id = test_session.id +# # WHEN I send the job and wait for it to complete +# await test_prompt.send_job_and_wait_async( +# post_exchange_args={"newer_than": 0}, +# synapse_client=self.syn, +# ) +# # THEN I expect the AgentPrompt to be updated with the response and trace +# assert test_prompt.response is not None +# assert test_prompt.trace is not None + + +# class TestAgentSession: +# """Integration tests for the synchronous methods of the AgentSession class.""" + +# @pytest.fixture(autouse=True, scope="function") +# def init(self, syn: Synapse) -> None: +# self.syn = syn + +# async def test_start(self) -> None: +# # GIVEN an agent session with a valid agent registration id +# agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + +# # WHEN the start method is called +# result_session = await agent_session.start_async(synapse_client=self.syn) + +# # THEN the result should be an AgentSession object +# # with expected attributes including an empty chat history +# assert result_session.id is not None +# assert ( +# result_session.access_level == AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE +# ) +# assert result_session.started_on is not None +# assert result_session.started_by is not None +# assert result_session.modified_on is not None +# assert result_session.agent_registration_id == AGENT_REGISTRATION_ID +# assert result_session.etag is not None +# assert result_session.chat_history == [] + +# async def test_get(self) -> None: +# # GIVEN an agent session with a valid agent registration id +# agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) +# # WHEN I start a session +# await agent_session.start_async(synapse_client=self.syn) +# # THEN I expect to be able to get the session with its id +# new_session = await AgentSession(id=agent_session.id).get_async( +# synapse_client=self.syn +# ) +# assert new_session == agent_session + +# async def test_update(self) -> None: +# # GIVEN an agent session with a valid agent +# # registration id and access level set +# agent_session = AgentSession( +# agent_registration_id=AGENT_REGISTRATION_ID, +# access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, +# ) +# # WHEN I start a session +# await agent_session.start_async(synapse_client=self.syn) +# # AND I update the access level of the session +# agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA +# await agent_session.update_async(synapse_client=self.syn) +# # THEN I expect the access level to be updated +# updated_session = await AgentSession(id=agent_session.id).get_async( +# synapse_client=self.syn +# ) +# assert ( +# updated_session.access_level +# == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA +# ) + +# async def test_prompt(self) -> None: +# # GIVEN an agent session with a valid agent registration id +# agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) +# # WHEN I start a session +# await agent_session.start_async(synapse_client=self.syn) +# # THEN I expect to be able to prompt the agent +# await agent_session.prompt_async( +# prompt="hello", +# enable_trace=True, +# ) +# # AND I expect the chat history to be updated with the prompt and response +# assert len(agent_session.chat_history) == 1 +# assert agent_session.chat_history[0].prompt == "hello" +# assert agent_session.chat_history[0].response is not None +# assert agent_session.chat_history[0].trace is not None + + +# class TestAgent: +# """Integration tests for the synchronous methods of the Agent class.""" + +# def get_test_agent(self) -> Agent: +# return Agent( +# cloud_agent_id=AGENT_AWS_ID, +# cloud_alias_id="TSTALIASID", +# registration_id=AGENT_REGISTRATION_ID, +# registered_on="2025-01-16T18:57:35.680Z", +# type="CUSTOM", +# sessions={}, +# current_session=None, +# ) + +# @pytest.fixture(autouse=True, scope="function") +# def init(self, syn: Synapse) -> None: +# self.syn = syn + +# async def test_register(self) -> None: +# # GIVEN an Agent with a valid agent AWS id +# agent = Agent(cloud_agent_id=AGENT_AWS_ID) +# # WHEN I register the agent +# await agent.register_async(synapse_client=self.syn) +# # THEN I expect the agent to be registered +# expected_agent = self.get_test_agent() +# assert agent == expected_agent + +# async def test_get(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID) +# # WHEN I get the agent +# await agent.get_async(synapse_client=self.syn) +# # THEN I expect the agent to be returned +# expected_agent = self.get_test_agent() +# assert agent == expected_agent + +# async def test_get_no_registration_id(self) -> None: +# # GIVEN an Agent with no registration id +# agent = Agent() +# # WHEN I get the agent, I expect a ValueError to be raised +# with pytest.raises(ValueError, match="Registration ID is required"): +# await agent.get_async(synapse_client=self.syn) + +# async def test_start_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID) +# # WHEN I start a session +# await agent.start_session_async(synapse_client=self.syn) +# # THEN I expect a current session to be set +# assert agent.current_session is not None +# # AND I expect the session to be in the sessions dictionary +# assert agent.sessions[agent.current_session.id] == agent.current_session + +# async def test_get_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID) +# # WHEN I start a session +# await agent.start_session_async(synapse_client=self.syn) +# # THEN I expect to be able to get the session with its id +# existing_session = await agent.get_session_async( +# session_id=agent.current_session.id +# ) +# # AND I expect those sessions to be the same +# assert existing_session == agent.current_session + +# async def test_prompt_with_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = await Agent(registration_id=AGENT_REGISTRATION_ID).get_async( +# synapse_client=self.syn +# ) +# # AND a session started separately +# session = await AgentSession( +# agent_registration_id=AGENT_REGISTRATION_ID +# ).start_async(synapse_client=self.syn) +# # WHEN I prompt the agent with a session +# await agent.prompt_async(prompt="hello", enable_trace=True, session=session) +# test_session = agent.sessions[session.id] +# # THEN I expect the chat history to be updated with the prompt and response +# assert len(test_session.chat_history) == 1 +# assert test_session.chat_history[0].prompt == "hello" +# assert test_session.chat_history[0].response is not None +# assert test_session.chat_history[0].trace is not None +# # AND I expect the current session to be the session provided +# assert agent.current_session.id == session.id + +# async def test_prompt_no_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = await Agent(registration_id=AGENT_REGISTRATION_ID).get_async( +# synapse_client=self.syn +# ) +# # WHEN I prompt the agent without a current session set +# # and no session provided +# await agent.prompt_async(prompt="hello", enable_trace=True) +# # THEN I expect a new session to be started and set as the current session +# assert agent.current_session is not None +# # AND I expect the chat history to be updated with the prompt and response +# assert len(agent.current_session.chat_history) == 1 +# assert agent.current_session.chat_history[0].prompt == "hello" +# assert agent.current_session.chat_history[0].response is not None +# assert agent.current_session.chat_history[0].trace is not None diff --git a/tests/integration/synapseclient/models/synchronous/test_agent.py b/tests/integration/synapseclient/models/synchronous/test_agent.py index d081de023..07b77291e 100644 --- a/tests/integration/synapseclient/models/synchronous/test_agent.py +++ b/tests/integration/synapseclient/models/synchronous/test_agent.py @@ -1,190 +1,192 @@ """Integration tests for the synchronous methods of the AgentSession and Agent classes.""" -import pytest - -from synapseclient import Synapse -from synapseclient.models.agent import Agent, AgentSession, AgentSessionAccessLevel - -# These are the ID values for a "Hello World" agent registered on Synapse. -# The Bedrock agent is hosted on Sage Bionetworks AWS infrastructure. -# CFN Template: -# https://raw.githubusercontent.com/Sage-Bionetworks-Workflows/dpe-agents/refs/heads/main/client_integration_test/template.json -CLOUD_AGENT_ID = "QOTV3KQM1X" -AGENT_REGISTRATION_ID = "29" - - -class TestAgentSession: - """Integration tests for the synchronous methods of the AgentSession class.""" - - @pytest.fixture(autouse=True, scope="function") - def init(self, syn: Synapse) -> None: - self.syn = syn - - async def test_start(self) -> None: - # GIVEN an agent session with a valid agent registration id - agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) - - # WHEN the start method is called - result_session = agent_session.start(synapse_client=self.syn) - - # THEN the result should be an AgentSession object - # with expected attributes including an empty chat history - assert result_session.id is not None - assert ( - result_session.access_level == AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE - ) - assert result_session.started_on is not None - assert result_session.started_by is not None - assert result_session.modified_on is not None - assert result_session.agent_registration_id == str(AGENT_REGISTRATION_ID) - assert result_session.etag is not None - assert result_session.chat_history == [] - - async def test_get(self) -> None: - # GIVEN an agent session with a valid agent registration id - agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) - # WHEN I start a session - agent_session.start(synapse_client=self.syn) - # THEN I expect to be able to get the session with its id - new_session = AgentSession(id=agent_session.id).get(synapse_client=self.syn) - assert new_session == agent_session - - async def test_update(self) -> None: - # GIVEN an agent session with a valid agent registration id and access level set - agent_session = AgentSession( - agent_registration_id=AGENT_REGISTRATION_ID, - access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, - ) - # WHEN I start a session - agent_session.start(synapse_client=self.syn) - # AND I update the access level of the session - agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA - agent_session.update(synapse_client=self.syn) - # THEN I expect the access level to be updated - updated_session = AgentSession(id=agent_session.id).get(synapse_client=self.syn) - assert ( - updated_session.access_level - == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA - ) - - async def test_prompt(self) -> None: - # GIVEN an agent session with a valid agent registration id - agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) - # WHEN I start a session - agent_session.start(synapse_client=self.syn) - # THEN I expect to be able to prompt the agent - agent_session.prompt( - prompt="hello", - enable_trace=True, - ) - # AND I expect the chat history to be updated with the prompt and response - assert len(agent_session.chat_history) == 1 - assert agent_session.chat_history[0].prompt == "hello" - assert agent_session.chat_history[0].response is not None - assert agent_session.chat_history[0].trace is not None - - -class TestAgent: - """Integration tests for the synchronous methods of the Agent class.""" - - def get_test_agent(self) -> Agent: - return Agent( - cloud_agent_id=CLOUD_AGENT_ID, - cloud_alias_id="TSTALIASID", - registration_id=AGENT_REGISTRATION_ID, - registered_on="2025-01-16T18:57:35.680Z", - type="CUSTOM", - sessions={}, - current_session=None, - ) - - @pytest.fixture(autouse=True, scope="function") - def init(self, syn: Synapse) -> None: - self.syn = syn - - async def test_register(self) -> None: - # GIVEN an Agent with a valid agent AWS id - agent = Agent(cloud_agent_id=CLOUD_AGENT_ID) - # WHEN I register the agent - agent.register(synapse_client=self.syn) - # THEN I expect the agent to be registered - expected_agent = self.get_test_agent() - assert agent == expected_agent - - async def test_get(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID) - # WHEN I get the agent - agent.get(synapse_client=self.syn) - # THEN I expect the agent to be returned - expected_agent = self.get_test_agent() - assert agent == expected_agent - - async def test_get_no_registration_id(self) -> None: - # GIVEN an Agent with no registration id - agent = Agent() - # WHEN I get the agent, I expect a ValueError to be raised - with pytest.raises(ValueError, match="Registration ID is required"): - agent.get(synapse_client=self.syn) - - async def test_start_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( - synapse_client=self.syn - ) - # WHEN I start a session - agent.start_session(synapse_client=self.syn) - # THEN I expect a current session to be set - assert agent.current_session is not None - # AND I expect the session to be in the sessions dictionary - assert agent.sessions[agent.current_session.id] == agent.current_session - - async def test_get_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( - synapse_client=self.syn - ) - # WHEN I start a session - session = agent.start_session(synapse_client=self.syn) - # THEN I expect to be able to get the session with its id - existing_session = agent.get_session(session_id=session.id) - # AND I expect those sessions to be the same - assert existing_session == session - # AND I expect it to be the current session - assert existing_session == agent.current_session - - async def test_prompt_with_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( - synapse_client=self.syn - ) - # AND a session started separately - session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID).start( - synapse_client=self.syn - ) - # WHEN I prompt the agent with a session - agent.prompt(prompt="hello", enable_trace=True, session=session) - test_session = agent.sessions[session.id] - # THEN I expect the chat history to be updated with the prompt and response - assert len(test_session.chat_history) == 1 - assert test_session.chat_history[0].prompt == "hello" - assert test_session.chat_history[0].response is not None - assert test_session.chat_history[0].trace is not None - # AND I expect the current session to be the session provided - assert agent.current_session.id == session.id - - async def test_prompt_no_session(self) -> None: - # GIVEN an Agent with a valid agent registration id - agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( - synapse_client=self.syn - ) - # WHEN I prompt the agent without a current session set - # and no session provided - agent.prompt(prompt="hello", enable_trace=True) - # THEN I expect a new session to be started and set as the current session - assert agent.current_session is not None - # AND I expect the chat history to be updated with the prompt and response - assert len(agent.current_session.chat_history) == 1 - assert agent.current_session.chat_history[0].prompt == "hello" - assert agent.current_session.chat_history[0].response is not None - assert agent.current_session.chat_history[0].trace is not None +# These tests have been disabled until out `test` user has needed permissions +# Context: https://sagebionetworks.jira.com/browse/SYNPY-1544?focusedCommentId=235070 +# import pytest + +# from synapseclient import Synapse +# from synapseclient.models.agent import Agent, AgentSession, AgentSessionAccessLevel + +# # These are the ID values for a "Hello World" agent registered on Synapse. +# # The Bedrock agent is hosted on Sage Bionetworks AWS infrastructure. +# # CFN Template: +# # https://raw.githubusercontent.com/Sage-Bionetworks-Workflows/dpe-agents/refs/heads/main/client_integration_test/template.json +# CLOUD_AGENT_ID = "QOTV3KQM1X" +# AGENT_REGISTRATION_ID = "29" + + +# class TestAgentSession: +# """Integration tests for the synchronous methods of the AgentSession class.""" + +# @pytest.fixture(autouse=True, scope="function") +# def init(self, syn: Synapse) -> None: +# self.syn = syn + +# async def test_start(self) -> None: +# # GIVEN an agent session with a valid agent registration id +# agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) + +# # WHEN the start method is called +# result_session = agent_session.start(synapse_client=self.syn) + +# # THEN the result should be an AgentSession object +# # with expected attributes including an empty chat history +# assert result_session.id is not None +# assert ( +# result_session.access_level == AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE +# ) +# assert result_session.started_on is not None +# assert result_session.started_by is not None +# assert result_session.modified_on is not None +# assert result_session.agent_registration_id == str(AGENT_REGISTRATION_ID) +# assert result_session.etag is not None +# assert result_session.chat_history == [] + +# async def test_get(self) -> None: +# # GIVEN an agent session with a valid agent registration id +# agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) +# # WHEN I start a session +# agent_session.start(synapse_client=self.syn) +# # THEN I expect to be able to get the session with its id +# new_session = AgentSession(id=agent_session.id).get(synapse_client=self.syn) +# assert new_session == agent_session + +# async def test_update(self) -> None: +# # GIVEN an agent session with a valid agent registration id and access level set +# agent_session = AgentSession( +# agent_registration_id=AGENT_REGISTRATION_ID, +# access_level=AgentSessionAccessLevel.PUBLICLY_ACCESSIBLE, +# ) +# # WHEN I start a session +# agent_session.start(synapse_client=self.syn) +# # AND I update the access level of the session +# agent_session.access_level = AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA +# agent_session.update(synapse_client=self.syn) +# # THEN I expect the access level to be updated +# updated_session = AgentSession(id=agent_session.id).get(synapse_client=self.syn) +# assert ( +# updated_session.access_level +# == AgentSessionAccessLevel.READ_YOUR_PRIVATE_DATA +# ) + +# async def test_prompt(self) -> None: +# # GIVEN an agent session with a valid agent registration id +# agent_session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID) +# # WHEN I start a session +# agent_session.start(synapse_client=self.syn) +# # THEN I expect to be able to prompt the agent +# agent_session.prompt( +# prompt="hello", +# enable_trace=True, +# ) +# # AND I expect the chat history to be updated with the prompt and response +# assert len(agent_session.chat_history) == 1 +# assert agent_session.chat_history[0].prompt == "hello" +# assert agent_session.chat_history[0].response is not None +# assert agent_session.chat_history[0].trace is not None + + +# class TestAgent: +# """Integration tests for the synchronous methods of the Agent class.""" + +# def get_test_agent(self) -> Agent: +# return Agent( +# cloud_agent_id=CLOUD_AGENT_ID, +# cloud_alias_id="TSTALIASID", +# registration_id=AGENT_REGISTRATION_ID, +# registered_on="2025-01-16T18:57:35.680Z", +# type="CUSTOM", +# sessions={}, +# current_session=None, +# ) + +# @pytest.fixture(autouse=True, scope="function") +# def init(self, syn: Synapse) -> None: +# self.syn = syn + +# async def test_register(self) -> None: +# # GIVEN an Agent with a valid agent AWS id +# agent = Agent(cloud_agent_id=CLOUD_AGENT_ID) +# # WHEN I register the agent +# agent.register(synapse_client=self.syn) +# # THEN I expect the agent to be registered +# expected_agent = self.get_test_agent() +# assert agent == expected_agent + +# async def test_get(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID) +# # WHEN I get the agent +# agent.get(synapse_client=self.syn) +# # THEN I expect the agent to be returned +# expected_agent = self.get_test_agent() +# assert agent == expected_agent + +# async def test_get_no_registration_id(self) -> None: +# # GIVEN an Agent with no registration id +# agent = Agent() +# # WHEN I get the agent, I expect a ValueError to be raised +# with pytest.raises(ValueError, match="Registration ID is required"): +# agent.get(synapse_client=self.syn) + +# async def test_start_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( +# synapse_client=self.syn +# ) +# # WHEN I start a session +# agent.start_session(synapse_client=self.syn) +# # THEN I expect a current session to be set +# assert agent.current_session is not None +# # AND I expect the session to be in the sessions dictionary +# assert agent.sessions[agent.current_session.id] == agent.current_session + +# async def test_get_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( +# synapse_client=self.syn +# ) +# # WHEN I start a session +# session = agent.start_session(synapse_client=self.syn) +# # THEN I expect to be able to get the session with its id +# existing_session = agent.get_session(session_id=session.id) +# # AND I expect those sessions to be the same +# assert existing_session == session +# # AND I expect it to be the current session +# assert existing_session == agent.current_session + +# async def test_prompt_with_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( +# synapse_client=self.syn +# ) +# # AND a session started separately +# session = AgentSession(agent_registration_id=AGENT_REGISTRATION_ID).start( +# synapse_client=self.syn +# ) +# # WHEN I prompt the agent with a session +# agent.prompt(prompt="hello", enable_trace=True, session=session) +# test_session = agent.sessions[session.id] +# # THEN I expect the chat history to be updated with the prompt and response +# assert len(test_session.chat_history) == 1 +# assert test_session.chat_history[0].prompt == "hello" +# assert test_session.chat_history[0].response is not None +# assert test_session.chat_history[0].trace is not None +# # AND I expect the current session to be the session provided +# assert agent.current_session.id == session.id + +# async def test_prompt_no_session(self) -> None: +# # GIVEN an Agent with a valid agent registration id +# agent = Agent(registration_id=AGENT_REGISTRATION_ID).get( +# synapse_client=self.syn +# ) +# # WHEN I prompt the agent without a current session set +# # and no session provided +# agent.prompt(prompt="hello", enable_trace=True) +# # THEN I expect a new session to be started and set as the current session +# assert agent.current_session is not None +# # AND I expect the chat history to be updated with the prompt and response +# assert len(agent.current_session.chat_history) == 1 +# assert agent.current_session.chat_history[0].prompt == "hello" +# assert agent.current_session.chat_history[0].response is not None +# assert agent.current_session.chat_history[0].trace is not None From ab260af623d992aaa8814e969e3c72ba88df067d Mon Sep 17 00:00:00 2001 From: bwmac Date: Tue, 28 Jan 2025 10:37:23 -0500 Subject: [PATCH 64/64] updates docstring for clarity --- synapseclient/models/agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapseclient/models/agent.py b/synapseclient/models/agent.py index 0a5cefb3d..344373f8a 100644 --- a/synapseclient/models/agent.py +++ b/synapseclient/models/agent.py @@ -630,7 +630,8 @@ async def main(): method_to_trace_name=lambda self, **kwargs: f"Get_Agent: {self.registration_id}" ) async def get_async(self, *, synapse_client: Optional[Synapse] = None) -> "Agent": - """Gets an existing agent. + """Gets an existing custom agent. There is no need to use this method + if you are trying to use the baseline agent. Arguments: synapse_client: If not passed in and caching was not disabled by