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.

utils.py 7.5 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import asyncio
  2. import os
  3. import random
  4. import sys
  5. from asyncio import Future
  6. from autogen_core.base import AgentRuntime, CancellationToken
  7. from autogen_core.components import DefaultTopicId, Image, RoutedAgent, message_handler
  8. from textual.app import App, ComposeResult
  9. from textual.containers import ScrollableContainer
  10. from textual.widgets import Button, Footer, Header, Input, Markdown, Static
  11. from textual_imageview.viewer import ImageViewer
  12. sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
  13. from common.types import (
  14. MultiModalMessage,
  15. PublishNow,
  16. RespondNow,
  17. TextMessage,
  18. ToolApprovalRequest,
  19. ToolApprovalResponse,
  20. )
  21. class ChatAppMessage(Static):
  22. def __init__(self, message: TextMessage | MultiModalMessage) -> None: # type: ignore
  23. self._message = message
  24. super().__init__()
  25. def on_mount(self) -> None:
  26. self.styles.margin = 1
  27. self.styles.padding = 1
  28. self.styles.border = ("solid", "blue")
  29. def compose(self) -> ComposeResult:
  30. if isinstance(self._message, TextMessage):
  31. yield Markdown(f"{self._message.source}:")
  32. yield Markdown(self._message.content)
  33. else:
  34. yield Markdown(f"{self._message.source}:")
  35. for content in self._message.content:
  36. if isinstance(content, str):
  37. yield Markdown(content)
  38. elif isinstance(content, Image):
  39. viewer = ImageViewer(content.image)
  40. viewer.styles.min_width = 50
  41. viewer.styles.min_height = 50
  42. yield viewer
  43. class WelcomeMessage(Static):
  44. def on_mount(self) -> None:
  45. self.styles.margin = 1
  46. self.styles.padding = 1
  47. self.styles.border = ("solid", "blue")
  48. class ChatInput(Input):
  49. def on_mount(self) -> None:
  50. self.focus()
  51. def on_input_submitted(self, event: Input.Submitted) -> None:
  52. self.clear()
  53. class ToolApprovalRequestNotice(Static):
  54. def __init__(self, request: ToolApprovalRequest, response_future: Future[ToolApprovalResponse]) -> None: # type: ignore
  55. self._tool_call = request.tool_call
  56. self._future = response_future
  57. super().__init__()
  58. def compose(self) -> ComposeResult:
  59. yield Static(f"Tool call: {self._tool_call.name}, arguments: {self._tool_call.arguments[:50]}")
  60. yield Button("Approve", id="approve", variant="warning")
  61. yield Button("Deny", id="deny", variant="default")
  62. def on_mount(self) -> None:
  63. self.styles.margin = 1
  64. self.styles.padding = 1
  65. self.styles.border = ("solid", "red")
  66. def on_button_pressed(self, event: Button.Pressed) -> None:
  67. button_id = event.button.id
  68. assert button_id is not None
  69. if button_id == "approve":
  70. self._future.set_result(ToolApprovalResponse(tool_call_id=self._tool_call.id, approved=True, reason=""))
  71. else:
  72. self._future.set_result(ToolApprovalResponse(tool_call_id=self._tool_call.id, approved=False, reason=""))
  73. self.remove()
  74. class TextualChatApp(App): # type: ignore
  75. """A Textual app for a chat interface."""
  76. def __init__(self, runtime: AgentRuntime, welcoming_notice: str | None = None, user_name: str = "User") -> None: # type: ignore
  77. self._runtime = runtime
  78. self._welcoming_notice = welcoming_notice
  79. self._user_name = user_name
  80. super().__init__()
  81. def compose(self) -> ComposeResult:
  82. yield Header()
  83. yield Footer()
  84. yield ScrollableContainer(id="chat-messages")
  85. yield ChatInput()
  86. def on_mount(self) -> None:
  87. if self._welcoming_notice is not None:
  88. chat_messages = self.query_one("#chat-messages")
  89. notice = WelcomeMessage(self._welcoming_notice, id="welcome")
  90. chat_messages.mount(notice)
  91. @property
  92. def welcoming_notice(self) -> str | None:
  93. return self._welcoming_notice
  94. @welcoming_notice.setter
  95. def welcoming_notice(self, value: str) -> None:
  96. self._welcoming_notice = value
  97. async def on_input_submitted(self, event: Input.Submitted) -> None:
  98. user_input = event.value
  99. await self.publish_user_message(user_input)
  100. async def post_request_user_input_notice(self) -> None:
  101. chat_messages = self.query_one("#chat-messages")
  102. notice = Static("Please enter your input.", id="typing")
  103. chat_messages.mount(notice)
  104. notice.scroll_visible()
  105. async def publish_user_message(self, user_input: str) -> None:
  106. chat_messages = self.query_one("#chat-messages")
  107. # Remove all typing messages.
  108. chat_messages.query("#typing").remove()
  109. # Publish the user message to the runtime.
  110. await self._runtime.publish_message(
  111. TextMessage(source=self._user_name, content=user_input), topic_id=DefaultTopicId()
  112. )
  113. async def post_runtime_message(self, message: TextMessage | MultiModalMessage) -> None: # type: ignore
  114. """Post a message from the agent runtime to the message list."""
  115. chat_messages = self.query_one("#chat-messages")
  116. msg = ChatAppMessage(message)
  117. chat_messages.mount(msg)
  118. msg.scroll_visible()
  119. async def handle_tool_approval_request(self, message: ToolApprovalRequest) -> ToolApprovalResponse: # type: ignore
  120. chat_messages = self.query_one("#chat-messages")
  121. future: Future[ToolApprovalResponse] = asyncio.get_event_loop().create_future() # type: ignore
  122. tool_call_approval_notice = ToolApprovalRequestNotice(message, future)
  123. chat_messages.mount(tool_call_approval_notice)
  124. tool_call_approval_notice.scroll_visible()
  125. return await future
  126. class TextualUserAgent(RoutedAgent): # type: ignore
  127. """An agent that is used to receive messages from the runtime."""
  128. def __init__(self, description: str, app: TextualChatApp) -> None: # type: ignore
  129. super().__init__(description)
  130. self._app = app
  131. @message_handler # type: ignore
  132. async def on_text_message(self, message: TextMessage, cancellation_token: CancellationToken) -> None: # type: ignore
  133. await self._app.post_runtime_message(message)
  134. @message_handler # type: ignore
  135. async def on_multi_modal_message(self, message: MultiModalMessage, cancellation_token: CancellationToken) -> None: # type: ignore
  136. # Save the message to file.
  137. # Generate a ramdom file name.
  138. for content in message.content:
  139. if isinstance(content, Image):
  140. filename = f"{self.metadata['type']}_{message.source}_{random.randbytes(16).hex()}.png"
  141. content.image.save(filename)
  142. await self._app.post_runtime_message(message)
  143. @message_handler # type: ignore
  144. async def on_respond_now(self, message: RespondNow, cancellation_token: CancellationToken) -> None: # type: ignore
  145. await self._app.post_request_user_input_notice()
  146. @message_handler # type: ignore
  147. async def on_publish_now(self, message: PublishNow, cancellation_token: CancellationToken) -> None: # type: ignore
  148. await self._app.post_request_user_input_notice()
  149. @message_handler # type: ignore
  150. async def on_tool_approval_request(
  151. self, message: ToolApprovalRequest, cancellation_token: CancellationToken
  152. ) -> ToolApprovalResponse:
  153. return await self._app.handle_tool_approval_request(message)