message_transformer.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import logging
  2. from collections.abc import Generator
  3. from datetime import date, datetime
  4. from decimal import Decimal
  5. from mimetypes import guess_extension
  6. from typing import Optional, cast
  7. from uuid import UUID
  8. import numpy as np
  9. import pytz
  10. from flask_login import current_user
  11. from core.file import File, FileTransferMethod, FileType
  12. from core.tools.entities.tool_entities import ToolInvokeMessage
  13. from core.tools.tool_file_manager import ToolFileManager
  14. logger = logging.getLogger(__name__)
  15. def safe_json_value(v):
  16. if isinstance(v, datetime):
  17. tz_name = getattr(current_user, "timezone", None) if current_user is not None else None
  18. if not tz_name:
  19. tz_name = "UTC"
  20. return v.astimezone(pytz.timezone(tz_name)).isoformat()
  21. elif isinstance(v, date):
  22. return v.isoformat()
  23. elif isinstance(v, UUID):
  24. return str(v)
  25. elif isinstance(v, Decimal):
  26. return float(v)
  27. elif isinstance(v, bytes):
  28. try:
  29. return v.decode("utf-8")
  30. except UnicodeDecodeError:
  31. return v.hex()
  32. elif isinstance(v, memoryview):
  33. return v.tobytes().hex()
  34. elif isinstance(v, np.ndarray):
  35. return v.tolist()
  36. elif isinstance(v, dict):
  37. return safe_json_dict(v)
  38. elif isinstance(v, list | tuple | set):
  39. return [safe_json_value(i) for i in v]
  40. else:
  41. return v
  42. def safe_json_dict(d):
  43. if not isinstance(d, dict):
  44. raise TypeError("safe_json_dict() expects a dictionary (dict) as input")
  45. return {k: safe_json_value(v) for k, v in d.items()}
  46. class ToolFileMessageTransformer:
  47. @classmethod
  48. def transform_tool_invoke_messages(
  49. cls,
  50. messages: Generator[ToolInvokeMessage, None, None],
  51. user_id: str,
  52. tenant_id: str,
  53. conversation_id: Optional[str] = None,
  54. ) -> Generator[ToolInvokeMessage, None, None]:
  55. """
  56. Transform tool message and handle file download
  57. """
  58. for message in messages:
  59. if message.type in {ToolInvokeMessage.MessageType.TEXT, ToolInvokeMessage.MessageType.LINK}:
  60. yield message
  61. elif message.type == ToolInvokeMessage.MessageType.IMAGE and isinstance(
  62. message.message, ToolInvokeMessage.TextMessage
  63. ):
  64. # try to download image
  65. try:
  66. assert isinstance(message.message, ToolInvokeMessage.TextMessage)
  67. tool_file_manager = ToolFileManager()
  68. tool_file = tool_file_manager.create_file_by_url(
  69. user_id=user_id,
  70. tenant_id=tenant_id,
  71. file_url=message.message.text,
  72. conversation_id=conversation_id,
  73. )
  74. url = f"/files/tools/{tool_file.id}{guess_extension(tool_file.mimetype) or '.png'}"
  75. yield ToolInvokeMessage(
  76. type=ToolInvokeMessage.MessageType.IMAGE_LINK,
  77. message=ToolInvokeMessage.TextMessage(text=url),
  78. meta=message.meta.copy() if message.meta is not None else {},
  79. )
  80. except Exception as e:
  81. yield ToolInvokeMessage(
  82. type=ToolInvokeMessage.MessageType.TEXT,
  83. message=ToolInvokeMessage.TextMessage(
  84. text=f"Failed to download image: {message.message.text}: {e}"
  85. ),
  86. meta=message.meta.copy() if message.meta is not None else {},
  87. )
  88. elif message.type == ToolInvokeMessage.MessageType.BLOB:
  89. # get mime type and save blob to storage
  90. meta = message.meta or {}
  91. mimetype = meta.get("mime_type", "application/octet-stream")
  92. # get filename from meta
  93. filename = meta.get("filename", None)
  94. # if message is str, encode it to bytes
  95. if not isinstance(message.message, ToolInvokeMessage.BlobMessage):
  96. raise ValueError("unexpected message type")
  97. assert isinstance(message.message.blob, bytes)
  98. tool_file_manager = ToolFileManager()
  99. tool_file = tool_file_manager.create_file_by_raw(
  100. user_id=user_id,
  101. tenant_id=tenant_id,
  102. conversation_id=conversation_id,
  103. file_binary=message.message.blob,
  104. mimetype=mimetype,
  105. filename=filename,
  106. )
  107. url = cls.get_tool_file_url(tool_file_id=tool_file.id, extension=guess_extension(tool_file.mimetype))
  108. # check if file is image
  109. if "image" in mimetype:
  110. yield ToolInvokeMessage(
  111. type=ToolInvokeMessage.MessageType.IMAGE_LINK,
  112. message=ToolInvokeMessage.TextMessage(text=url),
  113. meta=meta.copy() if meta is not None else {},
  114. )
  115. else:
  116. yield ToolInvokeMessage(
  117. type=ToolInvokeMessage.MessageType.BINARY_LINK,
  118. message=ToolInvokeMessage.TextMessage(text=url),
  119. meta=meta.copy() if meta is not None else {},
  120. )
  121. elif message.type == ToolInvokeMessage.MessageType.FILE:
  122. meta = message.meta or {}
  123. file = meta.get("file", None)
  124. if isinstance(file, File):
  125. if file.transfer_method == FileTransferMethod.TOOL_FILE:
  126. assert file.related_id is not None
  127. url = cls.get_tool_file_url(tool_file_id=file.related_id, extension=file.extension)
  128. if file.type == FileType.IMAGE:
  129. yield ToolInvokeMessage(
  130. type=ToolInvokeMessage.MessageType.IMAGE_LINK,
  131. message=ToolInvokeMessage.TextMessage(text=url),
  132. meta=meta.copy() if meta is not None else {},
  133. )
  134. else:
  135. yield ToolInvokeMessage(
  136. type=ToolInvokeMessage.MessageType.LINK,
  137. message=ToolInvokeMessage.TextMessage(text=url),
  138. meta=meta.copy() if meta is not None else {},
  139. )
  140. else:
  141. yield message
  142. elif message.type == ToolInvokeMessage.MessageType.JSON:
  143. if isinstance(message.message, ToolInvokeMessage.JsonMessage):
  144. json_msg = cast(ToolInvokeMessage.JsonMessage, message.message)
  145. json_msg.json_object = safe_json_value(json_msg.json_object)
  146. yield message
  147. else:
  148. yield message
  149. @classmethod
  150. def get_tool_file_url(cls, tool_file_id: str, extension: Optional[str]) -> str:
  151. return f"/files/tools/{tool_file_id}{extension or '.bin'}"