You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

_oai_assistant.py 6.1 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. from typing import Any, Callable, List, Mapping
  2. import openai
  3. from agnext.components import DefaultTopicId, RoutedAgent, message_handler
  4. from agnext.core import (
  5. CancellationToken,
  6. MessageContext, # type: ignore
  7. )
  8. from openai import AsyncAssistantEventHandler
  9. from openai.types import ResponseFormatJSONObject, ResponseFormatText
  10. from ..types import PublishNow, Reset, RespondNow, ResponseFormat, TextMessage
  11. class OpenAIAssistantAgent(RoutedAgent):
  12. """An agent implementation that uses the OpenAI Assistant API to generate
  13. responses.
  14. Args:
  15. description (str): The description of the agent.
  16. client (openai.AsyncClient): The client to use for the OpenAI API.
  17. assistant_id (str): The assistant ID to use for the OpenAI API.
  18. thread_id (str): The thread ID to use for the OpenAI API.
  19. assistant_event_handler_factory (Callable[[], AsyncAssistantEventHandler], optional):
  20. A factory function to create an async assistant event handler. Defaults to None.
  21. If provided, the agent will use the streaming mode with the event handler.
  22. If not provided, the agent will use the blocking mode to generate responses.
  23. """
  24. def __init__(
  25. self,
  26. description: str,
  27. client: openai.AsyncClient,
  28. assistant_id: str,
  29. thread_id: str,
  30. assistant_event_handler_factory: (Callable[[], AsyncAssistantEventHandler] | None) = None,
  31. ) -> None:
  32. super().__init__(description)
  33. self._client = client
  34. self._assistant_id = assistant_id
  35. self._thread_id = thread_id
  36. self._assistant_event_handler_factory = assistant_event_handler_factory
  37. @message_handler()
  38. async def on_text_message(self, message: TextMessage, ctx: MessageContext) -> None:
  39. """Handle a text message. This method adds the message to the thread."""
  40. # Save the message to the thread.
  41. _ = await self._client.beta.threads.messages.create(
  42. thread_id=self._thread_id,
  43. content=message.content,
  44. role="user",
  45. metadata={"sender": message.source},
  46. )
  47. @message_handler()
  48. async def on_reset(self, message: Reset, ctx: MessageContext) -> None:
  49. """Handle a reset message. This method deletes all messages in the thread."""
  50. # Get all messages in this thread.
  51. all_msgs: List[str] = []
  52. while True:
  53. if not all_msgs:
  54. msgs = await self._client.beta.threads.messages.list(self._thread_id)
  55. else:
  56. msgs = await self._client.beta.threads.messages.list(self._thread_id, after=all_msgs[-1])
  57. for msg in msgs.data:
  58. all_msgs.append(msg.id)
  59. if not msgs.has_next_page():
  60. break
  61. # Delete all the messages.
  62. for msg_id in all_msgs:
  63. status = await self._client.beta.threads.messages.delete(message_id=msg_id, thread_id=self._thread_id)
  64. assert status.deleted is True
  65. @message_handler()
  66. async def on_respond_now(self, message: RespondNow, ctx: MessageContext) -> TextMessage:
  67. """Handle a respond now message. This method generates a response and returns it to the sender."""
  68. return await self._generate_response(message.response_format, ctx.cancellation_token)
  69. @message_handler()
  70. async def on_publish_now(self, message: PublishNow, ctx: MessageContext) -> None:
  71. """Handle a publish now message. This method generates a response and publishes it."""
  72. response = await self._generate_response(message.response_format, ctx.cancellation_token)
  73. await self.publish_message(response, DefaultTopicId())
  74. async def _generate_response(
  75. self,
  76. requested_response_format: ResponseFormat,
  77. cancellation_token: CancellationToken,
  78. ) -> TextMessage:
  79. # Handle response format.
  80. if requested_response_format == ResponseFormat.json_object:
  81. response_format = ResponseFormatJSONObject(type="json_object") # type: ignore
  82. else:
  83. response_format = ResponseFormatText(type="text") # type: ignore
  84. if self._assistant_event_handler_factory is not None:
  85. # Use event handler and streaming mode if available.
  86. async with self._client.beta.threads.runs.stream(
  87. thread_id=self._thread_id,
  88. assistant_id=self._assistant_id,
  89. event_handler=self._assistant_event_handler_factory(),
  90. response_format=response_format, # type: ignore
  91. ) as stream:
  92. run = await stream.get_final_run()
  93. else:
  94. # Use blocking mode.
  95. run = await self._client.beta.threads.runs.create(
  96. thread_id=self._thread_id,
  97. assistant_id=self._assistant_id,
  98. response_format=response_format, # type: ignore
  99. )
  100. if run.status != "completed":
  101. # TODO: handle other statuses.
  102. raise ValueError(f"Run did not complete successfully: {run}")
  103. # Get the last message from the run.
  104. response = await self._client.beta.threads.messages.list(self._thread_id, run_id=run.id, order="desc", limit=1)
  105. last_message_content = response.data[0].content
  106. # TODO: handle array of content.
  107. text_content = [content for content in last_message_content if content.type == "text"]
  108. if not text_content:
  109. raise ValueError(f"Expected text content in the last message: {last_message_content}")
  110. # TODO: handle multiple text content.
  111. return TextMessage(content=text_content[0].text.value, source=self.metadata["type"])
  112. def save_state(self) -> Mapping[str, Any]:
  113. return {
  114. "assistant_id": self._assistant_id,
  115. "thread_id": self._thread_id,
  116. }
  117. def load_state(self, state: Mapping[str, Any]) -> None:
  118. self._assistant_id = state["assistant_id"]
  119. self._thread_id = state["thread_id"]