formatter.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. from abc import ABC, abstractmethod
  2. from typing import Dict, List, Optional, TypedDict, Union
  3. class MessageContent(TypedDict, total=False):
  4. """Type definition for message content in LLM requests."""
  5. type: str # Required field
  6. text: Optional[str] # Optional field
  7. image_url: Optional[Dict[str, str]] # Optional field
  8. class Message(TypedDict):
  9. """Type definition for a message in a LLM inference request."""
  10. role: str
  11. content: Union[str, List[MessageContent]]
  12. class Conversation:
  13. """
  14. Data class representing a conversation which are list of messages.
  15. """
  16. def __init__(self, messages=None):
  17. self.messages = messages if messages is not None else []
  18. def add_message(self, message):
  19. """
  20. Add a message to the conversation.
  21. Args:
  22. message: Message object to add
  23. """
  24. self.messages.append(message)
  25. def get_message(self, index):
  26. """
  27. Get a message at a specific index.
  28. Args:
  29. index: Index of the message to retrieve
  30. Returns:
  31. Message: The message at the specified index
  32. Raises:
  33. IndexError: If the index is out of bounds
  34. """
  35. if index < 0 or index >= len(self.messages):
  36. raise IndexError(
  37. f"Message index {index} out of range (0-{len(self.messages)-1})"
  38. )
  39. return self.messages[index]
  40. class Formatter(ABC):
  41. """
  42. Abstract base class for formatters that convert messages to different formats.
  43. """
  44. def __init__(self):
  45. """
  46. Initialize the formatter.
  47. Subclasses can override this method to add specific initialization parameters.
  48. """
  49. pass
  50. @abstractmethod
  51. def format_data(self, data) -> List:
  52. """
  53. Format the message. This method must be implemented by subclasses.
  54. Args:
  55. data: List of Conversation objects
  56. Returns:
  57. List of formatted data
  58. """
  59. pass
  60. @abstractmethod
  61. def format_sample(self, sample) -> Union[Dict, str]:
  62. """
  63. Format a sample. This method must be implemented by subclasses.
  64. Args:
  65. sample: Conversation object
  66. Returns:
  67. Formatted sample in the appropriate format
  68. """
  69. pass
  70. def read_data(self, data):
  71. """
  72. Read Hugging Face data and convert it to a list of Conversation objects.
  73. Args:
  74. data: Hugging Face dataset or iterable data
  75. Returns:
  76. list: List of Conversation objects, each containing a list of Message objects
  77. """
  78. conversations = []
  79. for item in data:
  80. # Extract fields from the Hugging Face dataset item
  81. image = item.get("image", None)
  82. input_text = item.get("input", "")
  83. output_label = item.get("output", "")
  84. # Create a new conversation
  85. conversation = Conversation()
  86. # Create user content and user message
  87. user_content = [
  88. {"type": "text", "text": input_text},
  89. ]
  90. # Add image to user content
  91. if image is not None:
  92. user_content.append({"type": "image", "image_url": {"url": image}})
  93. user_message = {"role": "user", "content": user_content}
  94. # Create assistant message with text content
  95. assistant_content = [
  96. {"type": "text", "text": output_label},
  97. ]
  98. assistant_message = {"role": "assistant", "content": assistant_content}
  99. # Add messages to the conversation
  100. conversation.add_message(user_message)
  101. conversation.add_message(assistant_message)
  102. # Add the conversation to the list
  103. conversations.append(conversation)
  104. return conversations
  105. class TorchtuneFormatter(Formatter):
  106. """
  107. Formatter for Torchtune format.
  108. """
  109. def __init__(self, data=None):
  110. """
  111. Initialize the formatter.
  112. Args:
  113. data: Optional data to initialize with
  114. """
  115. super().__init__()
  116. self.conversation_data = None
  117. if data is not None:
  118. self.conversation_data = self.read_data(data)
  119. def format_data(self, data):
  120. """
  121. Format the data.
  122. Args:
  123. data: List of Conversation objects
  124. Returns:
  125. list: List of formatted data
  126. """
  127. formatted_data = []
  128. for conversation in data:
  129. formatted_data.append(self.format_sample(conversation))
  130. return formatted_data
  131. def format_sample(self, sample):
  132. """
  133. Format a sample.
  134. Args:
  135. sample: Conversation object
  136. Returns:
  137. dict: Formatted sample in Torchtune format
  138. """
  139. formatted_messages = []
  140. for message in sample.messages:
  141. formatted_messages.append(self.format(message))
  142. return {"messages": formatted_messages}
  143. def format(self, message):
  144. """
  145. Format a message in Torchtune format.
  146. Args:
  147. message: Message object to format
  148. Returns:
  149. dict: Formatted message in Torchtune format
  150. """
  151. # For Torchtune format, we can return the Message as is
  152. # since it's already in a compatible format
  153. return message
  154. class vLLMFormatter(Formatter):
  155. """
  156. Formatter for vLLM format.
  157. """
  158. def format_data(self, data):
  159. """
  160. Format the data.
  161. Args:
  162. data: List of Conversation objects
  163. Returns:
  164. list: List of formatted data in vLLM format
  165. """
  166. formatted_data = []
  167. for conversation in data:
  168. formatted_data.append(self.format_sample(conversation))
  169. return formatted_data
  170. def format_sample(self, sample):
  171. """
  172. Format a sample.
  173. Args:
  174. sample: Conversation object
  175. Returns:
  176. str: Formatted sample in vLLM format
  177. """
  178. formatted_messages = []
  179. for message in sample.messages:
  180. formatted_messages.append(self.format(message))
  181. return "\n".join(formatted_messages)
  182. def format(self, message):
  183. """
  184. Format a message in vLLM format.
  185. Args:
  186. message: Message object to format
  187. Returns:
  188. str: Formatted message in vLLM format
  189. """
  190. role = message["role"]
  191. content = message["content"]
  192. # Handle different content types
  193. if isinstance(content, str):
  194. return f"{role}: {content}"
  195. else:
  196. # For multimodal content, extract text parts
  197. text_parts = []
  198. for item in content:
  199. if item["type"] == "text" and "text" in item:
  200. text_parts.append(item["text"])
  201. return f"{role}: {' '.join(text_parts)}"
  202. class OpenAIFormatter(Formatter):
  203. """
  204. Formatter for OpenAI format.
  205. """
  206. def format_data(self, data):
  207. """
  208. Format the data.
  209. Args:
  210. data: List of Conversation objects
  211. Returns:
  212. dict: Formatted data in OpenAI format
  213. """
  214. formatted_data = []
  215. for conversation in data:
  216. formatted_data.append(self.format_sample(conversation))
  217. return formatted_data
  218. def format_sample(self, sample):
  219. """
  220. Format a sample.
  221. Args:
  222. sample: Conversation object
  223. Returns:
  224. dict: Formatted sample in OpenAI format
  225. """
  226. formatted_messages = []
  227. for message in sample.messages:
  228. formatted_messages.append(self.format(message))
  229. return {"messages": formatted_messages}
  230. def format(self, message):
  231. """
  232. Format a message in OpenAI format.
  233. Args:
  234. message: Message object to format
  235. Returns:
  236. dict: Formatted message in OpenAI format
  237. """
  238. # For OpenAI format, we can return the Message as is
  239. # since it's already in a compatible format
  240. return message