misc.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import hashlib
  2. import re
  3. import time
  4. import uuid
  5. from datetime import timedelta
  6. from pathlib import Path
  7. from typing import Callable, Optional
  8. import collections.abc
  9. def deep_update(d, u):
  10. for k, v in u.items():
  11. if isinstance(v, collections.abc.Mapping):
  12. d[k] = deep_update(d.get(k, {}), v)
  13. else:
  14. d[k] = v
  15. return d
  16. def get_message_list(messages, message_id):
  17. """
  18. Reconstructs a list of messages in order up to the specified message_id.
  19. :param message_id: ID of the message to reconstruct the chain
  20. :param messages: Message history dict containing all messages
  21. :return: List of ordered messages starting from the root to the given message
  22. """
  23. # Find the message by its id
  24. current_message = messages.get(message_id)
  25. if not current_message:
  26. return None
  27. # Reconstruct the chain by following the parentId links
  28. message_list = []
  29. while current_message:
  30. message_list.insert(
  31. 0, current_message
  32. ) # Insert the message at the beginning of the list
  33. parent_id = current_message["parentId"]
  34. current_message = messages.get(parent_id) if parent_id else None
  35. return message_list
  36. def get_messages_content(messages: list[dict]) -> str:
  37. return "\n".join(
  38. [
  39. f"{message['role'].upper()}: {get_content_from_message(message)}"
  40. for message in messages
  41. ]
  42. )
  43. def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
  44. for message in reversed(messages):
  45. if message["role"] == "user":
  46. return message
  47. return None
  48. def get_content_from_message(message: dict) -> Optional[str]:
  49. if isinstance(message["content"], list):
  50. for item in message["content"]:
  51. if item["type"] == "text":
  52. return item["text"]
  53. else:
  54. return message["content"]
  55. return None
  56. def get_last_user_message(messages: list[dict]) -> Optional[str]:
  57. message = get_last_user_message_item(messages)
  58. if message is None:
  59. return None
  60. return get_content_from_message(message)
  61. def get_last_assistant_message_item(messages: list[dict]) -> Optional[dict]:
  62. for message in reversed(messages):
  63. if message["role"] == "assistant":
  64. return message
  65. return None
  66. def get_last_assistant_message(messages: list[dict]) -> Optional[str]:
  67. for message in reversed(messages):
  68. if message["role"] == "assistant":
  69. return get_content_from_message(message)
  70. return None
  71. def get_system_message(messages: list[dict]) -> Optional[dict]:
  72. for message in messages:
  73. if message["role"] == "system":
  74. return message
  75. return None
  76. def remove_system_message(messages: list[dict]) -> list[dict]:
  77. return [message for message in messages if message["role"] != "system"]
  78. def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict]]:
  79. return get_system_message(messages), remove_system_message(messages)
  80. def prepend_to_first_user_message_content(
  81. content: str, messages: list[dict]
  82. ) -> list[dict]:
  83. for message in messages:
  84. if message["role"] == "user":
  85. if isinstance(message["content"], list):
  86. for item in message["content"]:
  87. if item["type"] == "text":
  88. item["text"] = f"{content}\n{item['text']}"
  89. else:
  90. message["content"] = f"{content}\n{message['content']}"
  91. break
  92. return messages
  93. def add_or_update_system_message(content: str, messages: list[dict]):
  94. """
  95. Adds a new system message at the beginning of the messages list
  96. or updates the existing system message at the beginning.
  97. :param msg: The message to be added or appended.
  98. :param messages: The list of message dictionaries.
  99. :return: The updated list of message dictionaries.
  100. """
  101. if messages and messages[0].get("role") == "system":
  102. messages[0]["content"] = f"{content}\n{messages[0]['content']}"
  103. else:
  104. # Insert at the beginning
  105. messages.insert(0, {"role": "system", "content": content})
  106. return messages
  107. def add_or_update_user_message(content: str, messages: list[dict]):
  108. """
  109. Adds a new user message at the end of the messages list
  110. or updates the existing user message at the end.
  111. :param msg: The message to be added or appended.
  112. :param messages: The list of message dictionaries.
  113. :return: The updated list of message dictionaries.
  114. """
  115. if messages and messages[-1].get("role") == "user":
  116. messages[-1]["content"] = f"{messages[-1]['content']}\n{content}"
  117. else:
  118. # Insert at the end
  119. messages.append({"role": "user", "content": content})
  120. return messages
  121. def append_or_update_assistant_message(content: str, messages: list[dict]):
  122. """
  123. Adds a new assistant message at the end of the messages list
  124. or updates the existing assistant message at the end.
  125. :param msg: The message to be added or appended.
  126. :param messages: The list of message dictionaries.
  127. :return: The updated list of message dictionaries.
  128. """
  129. if messages and messages[-1].get("role") == "assistant":
  130. messages[-1]["content"] = f"{messages[-1]['content']}\n{content}"
  131. else:
  132. # Insert at the end
  133. messages.append({"role": "assistant", "content": content})
  134. return messages
  135. def openai_chat_message_template(model: str):
  136. return {
  137. "id": f"{model}-{str(uuid.uuid4())}",
  138. "created": int(time.time()),
  139. "model": model,
  140. "choices": [{"index": 0, "logprobs": None, "finish_reason": None}],
  141. }
  142. def openai_chat_chunk_message_template(
  143. model: str,
  144. content: Optional[str] = None,
  145. tool_calls: Optional[list[dict]] = None,
  146. usage: Optional[dict] = None,
  147. ) -> dict:
  148. template = openai_chat_message_template(model)
  149. template["object"] = "chat.completion.chunk"
  150. template["choices"][0]["index"] = 0
  151. template["choices"][0]["delta"] = {}
  152. if content:
  153. template["choices"][0]["delta"]["content"] = content
  154. if tool_calls:
  155. template["choices"][0]["delta"]["tool_calls"] = tool_calls
  156. if not content and not tool_calls:
  157. template["choices"][0]["finish_reason"] = "stop"
  158. if usage:
  159. template["usage"] = usage
  160. return template
  161. def openai_chat_completion_message_template(
  162. model: str,
  163. message: Optional[str] = None,
  164. tool_calls: Optional[list[dict]] = None,
  165. usage: Optional[dict] = None,
  166. ) -> dict:
  167. template = openai_chat_message_template(model)
  168. template["object"] = "chat.completion"
  169. if message is not None:
  170. template["choices"][0]["message"] = {
  171. "content": message,
  172. "role": "assistant",
  173. **({"tool_calls": tool_calls} if tool_calls else {}),
  174. }
  175. template["choices"][0]["finish_reason"] = "stop"
  176. if usage:
  177. template["usage"] = usage
  178. return template
  179. def get_gravatar_url(email):
  180. # Trim leading and trailing whitespace from
  181. # an email address and force all characters
  182. # to lower case
  183. address = str(email).strip().lower()
  184. # Create a SHA256 hash of the final string
  185. hash_object = hashlib.sha256(address.encode())
  186. hash_hex = hash_object.hexdigest()
  187. # Grab the actual image URL
  188. return f"https://www.gravatar.com/avatar/{hash_hex}?d=mp"
  189. def calculate_sha256(file_path, chunk_size):
  190. # Compute SHA-256 hash of a file efficiently in chunks
  191. sha256 = hashlib.sha256()
  192. with open(file_path, "rb") as f:
  193. while chunk := f.read(chunk_size):
  194. sha256.update(chunk)
  195. return sha256.hexdigest()
  196. def calculate_sha256_string(string):
  197. # Create a new SHA-256 hash object
  198. sha256_hash = hashlib.sha256()
  199. # Update the hash object with the bytes of the input string
  200. sha256_hash.update(string.encode("utf-8"))
  201. # Get the hexadecimal representation of the hash
  202. hashed_string = sha256_hash.hexdigest()
  203. return hashed_string
  204. def validate_email_format(email: str) -> bool:
  205. if email.endswith("@localhost"):
  206. return True
  207. return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
  208. def sanitize_filename(file_name):
  209. # Convert to lowercase
  210. lower_case_file_name = file_name.lower()
  211. # Remove special characters using regular expression
  212. sanitized_file_name = re.sub(r"[^\w\s]", "", lower_case_file_name)
  213. # Replace spaces with dashes
  214. final_file_name = re.sub(r"\s+", "-", sanitized_file_name)
  215. return final_file_name
  216. def extract_folders_after_data_docs(path):
  217. # Convert the path to a Path object if it's not already
  218. path = Path(path)
  219. # Extract parts of the path
  220. parts = path.parts
  221. # Find the index of '/data/docs' in the path
  222. try:
  223. index_data_docs = parts.index("data") + 1
  224. index_docs = parts.index("docs", index_data_docs) + 1
  225. except ValueError:
  226. return []
  227. # Exclude the filename and accumulate folder names
  228. tags = []
  229. folders = parts[index_docs:-1]
  230. for idx, _ in enumerate(folders):
  231. tags.append("/".join(folders[: idx + 1]))
  232. return tags
  233. def parse_duration(duration: str) -> Optional[timedelta]:
  234. if duration == "-1" or duration == "0":
  235. return None
  236. # Regular expression to find number and unit pairs
  237. pattern = r"(-?\d+(\.\d+)?)(ms|s|m|h|d|w)"
  238. matches = re.findall(pattern, duration)
  239. if not matches:
  240. raise ValueError("Invalid duration string")
  241. total_duration = timedelta()
  242. for number, _, unit in matches:
  243. number = float(number)
  244. if unit == "ms":
  245. total_duration += timedelta(milliseconds=number)
  246. elif unit == "s":
  247. total_duration += timedelta(seconds=number)
  248. elif unit == "m":
  249. total_duration += timedelta(minutes=number)
  250. elif unit == "h":
  251. total_duration += timedelta(hours=number)
  252. elif unit == "d":
  253. total_duration += timedelta(days=number)
  254. elif unit == "w":
  255. total_duration += timedelta(weeks=number)
  256. return total_duration
  257. def parse_ollama_modelfile(model_text):
  258. parameters_meta = {
  259. "mirostat": int,
  260. "mirostat_eta": float,
  261. "mirostat_tau": float,
  262. "num_ctx": int,
  263. "repeat_last_n": int,
  264. "repeat_penalty": float,
  265. "temperature": float,
  266. "seed": int,
  267. "tfs_z": float,
  268. "num_predict": int,
  269. "top_k": int,
  270. "top_p": float,
  271. "num_keep": int,
  272. "typical_p": float,
  273. "presence_penalty": float,
  274. "frequency_penalty": float,
  275. "penalize_newline": bool,
  276. "numa": bool,
  277. "num_batch": int,
  278. "num_gpu": int,
  279. "main_gpu": int,
  280. "low_vram": bool,
  281. "f16_kv": bool,
  282. "vocab_only": bool,
  283. "use_mmap": bool,
  284. "use_mlock": bool,
  285. "num_thread": int,
  286. }
  287. data = {"base_model_id": None, "params": {}}
  288. # Parse base model
  289. base_model_match = re.search(
  290. r"^FROM\s+(\w+)", model_text, re.MULTILINE | re.IGNORECASE
  291. )
  292. if base_model_match:
  293. data["base_model_id"] = base_model_match.group(1)
  294. # Parse template
  295. template_match = re.search(
  296. r'TEMPLATE\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
  297. )
  298. if template_match:
  299. data["params"] = {"template": template_match.group(1).strip()}
  300. # Parse stops
  301. stops = re.findall(r'PARAMETER stop "(.*?)"', model_text, re.IGNORECASE)
  302. if stops:
  303. data["params"]["stop"] = stops
  304. # Parse other parameters from the provided list
  305. for param, param_type in parameters_meta.items():
  306. param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE)
  307. if param_match:
  308. value = param_match.group(1)
  309. try:
  310. if param_type is int:
  311. value = int(value)
  312. elif param_type is float:
  313. value = float(value)
  314. elif param_type is bool:
  315. value = value.lower() == "true"
  316. except Exception as e:
  317. print(e)
  318. continue
  319. data["params"][param] = value
  320. # Parse adapter
  321. adapter_match = re.search(r"ADAPTER (.+)", model_text, re.IGNORECASE)
  322. if adapter_match:
  323. data["params"]["adapter"] = adapter_match.group(1)
  324. # Parse system description
  325. system_desc_match = re.search(
  326. r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
  327. )
  328. system_desc_match_single = re.search(
  329. r"SYSTEM\s+([^\n]+)", model_text, re.IGNORECASE
  330. )
  331. if system_desc_match:
  332. data["params"]["system"] = system_desc_match.group(1).strip()
  333. elif system_desc_match_single:
  334. data["params"]["system"] = system_desc_match_single.group(1).strip()
  335. # Parse messages
  336. messages = []
  337. message_matches = re.findall(r"MESSAGE (\w+) (.+)", model_text, re.IGNORECASE)
  338. for role, content in message_matches:
  339. messages.append({"role": role, "content": content})
  340. if messages:
  341. data["params"]["messages"] = messages
  342. return data