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.

assistant.py 10 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. """This is an example of a terminal-based ChatGPT clone
  2. using an OpenAIAssistantAgent and event-based orchestration."""
  3. import argparse
  4. import asyncio
  5. import logging
  6. import os
  7. import re
  8. import sys
  9. from typing import List
  10. import aiofiles
  11. import openai
  12. from agnext.application import SingleThreadedAgentRuntime
  13. from agnext.components import DefaultTopicId, RoutedAgent, message_handler
  14. from agnext.core import AgentId, AgentRuntime, MessageContext
  15. from openai import AsyncAssistantEventHandler
  16. from openai.types.beta.thread import ToolResources
  17. from openai.types.beta.threads import Message, Text, TextDelta
  18. from openai.types.beta.threads.runs import RunStep, RunStepDelta
  19. from typing_extensions import override
  20. sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
  21. from agnext.core import AgentInstantiationContext
  22. from common.agents import OpenAIAssistantAgent
  23. from common.memory import BufferedChatMemory
  24. from common.patterns._group_chat_manager import GroupChatManager
  25. from common.types import PublishNow, TextMessage
  26. sep = "-" * 50
  27. class UserProxyAgent(RoutedAgent):
  28. def __init__( # type: ignore
  29. self,
  30. client: openai.AsyncClient, # type: ignore
  31. assistant_id: str,
  32. thread_id: str,
  33. vector_store_id: str,
  34. ) -> None: # type: ignore
  35. super().__init__(
  36. description="A human user",
  37. ) # type: ignore
  38. self._client = client
  39. self._assistant_id = assistant_id
  40. self._thread_id = thread_id
  41. self._vector_store_id = vector_store_id
  42. @message_handler() # type: ignore
  43. async def on_text_message(self, message: TextMessage, ctx: MessageContext) -> None:
  44. # TODO: render image if message has image.
  45. # print(f"{message.source}: {message.content}")
  46. pass
  47. async def _get_user_input(self, prompt: str) -> str:
  48. loop = asyncio.get_event_loop()
  49. return await loop.run_in_executor(None, input, prompt)
  50. @message_handler() # type: ignore
  51. async def on_publish_now(self, message: PublishNow, ctx: MessageContext) -> None:
  52. while True:
  53. user_input = await self._get_user_input(f"\n{sep}\nYou: ")
  54. # Parse upload file command '[upload code_interpreter | file_search filename]'.
  55. match = re.search(r"\[upload\s+(code_interpreter|file_search)\s+(.+)\]", user_input)
  56. if match:
  57. # Purpose of the file.
  58. purpose = match.group(1)
  59. # Extract file path.
  60. file_path = match.group(2)
  61. if not os.path.exists(file_path):
  62. print(f"File not found: {file_path}")
  63. continue
  64. # Filename.
  65. file_name = os.path.basename(file_path)
  66. # Read file content.
  67. async with aiofiles.open(file_path, "rb") as f:
  68. file_content = await f.read()
  69. if purpose == "code_interpreter":
  70. # Upload file.
  71. file = await self._client.files.create(file=(file_name, file_content), purpose="assistants")
  72. # Get existing file ids from tool resources.
  73. thread = await self._client.beta.threads.retrieve(thread_id=self._thread_id)
  74. tool_resources: ToolResources = thread.tool_resources if thread.tool_resources else ToolResources()
  75. assert tool_resources.code_interpreter is not None
  76. if tool_resources.code_interpreter.file_ids:
  77. file_ids = tool_resources.code_interpreter.file_ids
  78. else:
  79. file_ids = [file.id]
  80. # Update thread with new file.
  81. await self._client.beta.threads.update(
  82. thread_id=self._thread_id,
  83. tool_resources={"code_interpreter": {"file_ids": file_ids}},
  84. )
  85. elif purpose == "file_search":
  86. # Upload file to vector store.
  87. file_batch = await self._client.beta.vector_stores.file_batches.upload_and_poll(
  88. vector_store_id=self._vector_store_id,
  89. files=[(file_name, file_content)],
  90. )
  91. assert file_batch.status == "completed"
  92. print(f"Uploaded file: {file_name}")
  93. continue
  94. elif user_input.startswith("[upload"):
  95. print("Invalid upload command. Please use '[upload code_interpreter | file_search filename]'.")
  96. continue
  97. elif user_input.strip().lower() == "exit":
  98. # Exit handler.
  99. return
  100. else:
  101. # Publish user input and exit handler.
  102. await self.publish_message(
  103. TextMessage(content=user_input, source=self.metadata["type"]), topic_id=DefaultTopicId()
  104. )
  105. return
  106. class EventHandler(AsyncAssistantEventHandler):
  107. @override
  108. async def on_text_delta(self, delta: TextDelta, snapshot: Text) -> None:
  109. print(delta.value, end="", flush=True)
  110. @override
  111. async def on_run_step_created(self, run_step: RunStep) -> None:
  112. details = run_step.step_details
  113. if details.type == "tool_calls":
  114. for tool in details.tool_calls:
  115. if tool.type == "code_interpreter":
  116. print("\nGenerating code to interpret:\n\n```python")
  117. @override
  118. async def on_run_step_done(self, run_step: RunStep) -> None:
  119. details = run_step.step_details
  120. if details.type == "tool_calls":
  121. for tool in details.tool_calls:
  122. if tool.type == "code_interpreter":
  123. print("\n```\nExecuting code...")
  124. @override
  125. async def on_run_step_delta(self, delta: RunStepDelta, snapshot: RunStep) -> None:
  126. details = delta.step_details
  127. if details is not None and details.type == "tool_calls":
  128. for tool in details.tool_calls or []:
  129. if tool.type == "code_interpreter" and tool.code_interpreter and tool.code_interpreter.input:
  130. print(tool.code_interpreter.input, end="", flush=True)
  131. @override
  132. async def on_message_created(self, message: Message) -> None:
  133. print(f"{sep}\nAssistant:\n")
  134. @override
  135. async def on_message_done(self, message: Message) -> None:
  136. # print a citation to the file searched
  137. if not message.content:
  138. return
  139. content = message.content[0]
  140. if not content.type == "text":
  141. return
  142. text_content = content.text
  143. annotations = text_content.annotations
  144. citations: List[str] = []
  145. for index, annotation in enumerate(annotations):
  146. text_content.value = text_content.value.replace(annotation.text, f"[{index}]")
  147. if file_citation := getattr(annotation, "file_citation", None):
  148. client = openai.AsyncClient()
  149. cited_file = await client.files.retrieve(file_citation.file_id)
  150. citations.append(f"[{index}] {cited_file.filename}")
  151. if citations:
  152. print("\n".join(citations))
  153. async def assistant_chat(runtime: AgentRuntime) -> str:
  154. oai_assistant = openai.beta.assistants.create(
  155. model="gpt-4-turbo",
  156. description="An AI assistant that helps with everyday tasks.",
  157. instructions="Help the user with their task.",
  158. tools=[{"type": "code_interpreter"}, {"type": "file_search"}],
  159. )
  160. vector_store = openai.beta.vector_stores.create()
  161. thread = openai.beta.threads.create(
  162. tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}},
  163. )
  164. await runtime.register(
  165. "Assistant",
  166. lambda: OpenAIAssistantAgent(
  167. description="An AI assistant that helps with everyday tasks.",
  168. client=openai.AsyncClient(),
  169. assistant_id=oai_assistant.id,
  170. thread_id=thread.id,
  171. assistant_event_handler_factory=lambda: EventHandler(),
  172. ),
  173. )
  174. await runtime.register(
  175. "User",
  176. lambda: UserProxyAgent(
  177. client=openai.AsyncClient(),
  178. assistant_id=oai_assistant.id,
  179. thread_id=thread.id,
  180. vector_store_id=vector_store.id,
  181. ),
  182. )
  183. # Create a group chat manager to facilitate a turn-based conversation.
  184. await runtime.register(
  185. "GroupChatManager",
  186. lambda: GroupChatManager(
  187. description="A group chat manager.",
  188. memory=BufferedChatMemory(buffer_size=10),
  189. participants=[
  190. AgentId("Assistant", AgentInstantiationContext.current_agent_id().key),
  191. AgentId("User", AgentInstantiationContext.current_agent_id().key),
  192. ],
  193. ),
  194. )
  195. return "User"
  196. async def main() -> None:
  197. usage = """Chat with an AI assistant backed by OpenAI Assistant API.
  198. You can upload files to the assistant using the command:
  199. [upload code_interpreter | file_search filename]
  200. where 'code_interpreter' or 'file_search' is the purpose of the file and
  201. 'filename' is the path to the file. For example:
  202. [upload code_interpreter data.csv]
  203. This will upload data.csv to the assistant for use with the code interpreter tool.
  204. Type "exit" to exit the chat.
  205. """
  206. runtime = SingleThreadedAgentRuntime()
  207. user = await assistant_chat(runtime)
  208. runtime.start()
  209. print(usage)
  210. # Request the user to start the conversation.
  211. await runtime.send_message(PublishNow(), AgentId(user, "default"))
  212. # TODO: have a way to exit the loop.
  213. if __name__ == "__main__":
  214. parser = argparse.ArgumentParser(description="Chat with an AI assistant.")
  215. parser.add_argument("--verbose", action="store_true", help="Enable verbose logging.")
  216. args = parser.parse_args()
  217. if args.verbose:
  218. logging.basicConfig(level=logging.WARNING)
  219. logging.getLogger("agnext").setLevel(logging.DEBUG)
  220. handler = logging.FileHandler("assistant.log")
  221. logging.getLogger("agnext").addHandler(handler)
  222. asyncio.run(main())