Files
NexQuant/rdagent/oai/llm_utils.py
T

755 lines
31 KiB
Python
Raw Normal View History

2024-06-12 15:12:11 +08:00
from __future__ import annotations
2024-05-21 22:48:41 +08:00
import datetime
import hashlib
import json
import multiprocessing
import os
import re
import sqlite3
import ssl
import time
import urllib.request
import uuid
from copy import deepcopy
from pathlib import Path
2024-06-12 15:12:11 +08:00
from typing import Any
2024-05-21 22:48:41 +08:00
import numpy as np
import tiktoken
2024-06-14 12:59:44 +08:00
from rdagent.core.conf import RD_AGENT_SETTINGS
2024-05-30 10:33:07 +08:00
from rdagent.core.utils import SingletonBaseClass
2024-07-17 15:00:13 +08:00
from rdagent.log import LogColors
from rdagent.log import rdagent_logger as logger
2024-05-21 22:48:41 +08:00
DEFAULT_QLIB_DOT_PATH = Path("./")
2024-07-17 15:00:13 +08:00
2024-06-12 15:12:11 +08:00
def md5_hash(input_string: str) -> str:
hash_md5 = hashlib.md5(usedforsecurity=False)
2024-05-21 22:48:41 +08:00
input_bytes = input_string.encode("utf-8")
2024-06-12 15:12:11 +08:00
hash_md5.update(input_bytes)
return hash_md5.hexdigest()
2024-05-21 22:48:41 +08:00
2024-06-05 15:36:15 +08:00
try:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
except ImportError:
2024-07-16 20:35:42 +08:00
logger.warning("azure.identity is not installed.")
2024-06-05 15:36:15 +08:00
2024-05-21 22:48:41 +08:00
try:
import openai
except ImportError:
2024-07-16 20:35:42 +08:00
logger.warning("openai is not installed.")
2024-05-21 22:48:41 +08:00
try:
from llama import Llama
except ImportError:
2024-07-16 20:35:42 +08:00
logger.warning("llama is not installed.")
2024-05-21 22:48:41 +08:00
class ConvManager:
"""
This is a conversation manager of LLM
It is for convenience of exporting conversation for debugging.
"""
def __init__(
self,
2024-06-12 15:12:11 +08:00
path: Path | str = DEFAULT_QLIB_DOT_PATH / "llm_conv",
2024-05-21 22:48:41 +08:00
recent_n: int = 10,
) -> None:
self.path = Path(path)
self.path.mkdir(parents=True, exist_ok=True)
self.recent_n = recent_n
2024-06-12 15:12:11 +08:00
def _rotate_files(self) -> None:
2024-05-21 22:48:41 +08:00
pairs = []
for f in self.path.glob("*.json"):
m = re.match(r"(\d+).json", f.name)
if m is not None:
n = int(m.group(1))
pairs.append((n, f))
pairs.sort(key=lambda x: x[0])
for n, f in pairs[: self.recent_n][::-1]:
2024-06-12 15:12:11 +08:00
if (self.path / f"{n+1}.json").exists():
(self.path / f"{n+1}.json").unlink()
2024-05-21 22:48:41 +08:00
f.rename(self.path / f"{n+1}.json")
2024-06-12 15:12:11 +08:00
def append(self, conv: tuple[list, str]) -> None:
2024-05-21 22:48:41 +08:00
self._rotate_files()
2024-06-12 15:12:11 +08:00
with (self.path / "0.json").open("w") as file:
json.dump(conv, file)
2024-05-21 22:48:41 +08:00
# TODO: reseve line breaks to make it more convient to edit file directly.
class SQliteLazyCache(SingletonBaseClass):
2024-06-12 15:12:11 +08:00
def __init__(self, cache_location: str) -> None:
2024-05-21 22:48:41 +08:00
super().__init__()
self.cache_location = cache_location
2024-06-12 15:12:11 +08:00
db_file_exist = Path(cache_location).exists()
2024-07-16 20:35:42 +08:00
# TODO: sqlite3 does not support multiprocessing.
2024-05-21 22:48:41 +08:00
self.conn = sqlite3.connect(cache_location)
self.c = self.conn.cursor()
if not db_file_exist:
self.c.execute(
"""
CREATE TABLE chat_cache (
md5_key TEXT PRIMARY KEY,
chat TEXT
)
2024-05-30 10:33:07 +08:00
""",
2024-05-21 22:48:41 +08:00
)
self.c.execute(
"""
CREATE TABLE embedding_cache (
md5_key TEXT PRIMARY KEY,
embedding TEXT
)
2024-05-30 10:33:07 +08:00
""",
2024-05-21 22:48:41 +08:00
)
self.c.execute(
"""
CREATE TABLE message_cache (
conversation_id TEXT PRIMARY KEY,
message TEXT
)
""",
)
2024-05-21 22:48:41 +08:00
self.conn.commit()
2024-06-12 15:12:11 +08:00
def chat_get(self, key: str) -> str | None:
2024-05-21 22:48:41 +08:00
md5_key = md5_hash(key)
self.c.execute("SELECT chat FROM chat_cache WHERE md5_key=?", (md5_key,))
result = self.c.fetchone()
if result is None:
return None
2024-06-12 15:12:11 +08:00
return result[0]
2024-05-21 22:48:41 +08:00
2024-06-12 15:12:11 +08:00
def embedding_get(self, key: str) -> list | dict | str | None:
2024-05-21 22:48:41 +08:00
md5_key = md5_hash(key)
self.c.execute("SELECT embedding FROM embedding_cache WHERE md5_key=?", (md5_key,))
result = self.c.fetchone()
if result is None:
return None
2024-06-12 15:12:11 +08:00
return json.loads(result[0])
2024-05-21 22:48:41 +08:00
2024-06-12 15:12:11 +08:00
def chat_set(self, key: str, value: str) -> None:
2024-05-21 22:48:41 +08:00
md5_key = md5_hash(key)
self.c.execute(
"INSERT OR REPLACE INTO chat_cache (md5_key, chat) VALUES (?, ?)",
(md5_key, value),
)
self.conn.commit()
2024-06-12 15:12:11 +08:00
def embedding_set(self, content_to_embedding_dict: dict) -> None:
2024-05-21 22:48:41 +08:00
for key, value in content_to_embedding_dict.items():
md5_key = md5_hash(key)
self.c.execute(
"INSERT OR REPLACE INTO embedding_cache (md5_key, embedding) VALUES (?, ?)",
(md5_key, json.dumps(value)),
)
self.conn.commit()
def message_get(self, conversation_id: str) -> list[str]:
self.c.execute("SELECT message FROM message_cache WHERE conversation_id=?", (conversation_id,))
result = self.c.fetchone()
if result is None:
return []
return json.loads(result[0])
def message_set(self, conversation_id: str, message_value: list[str]) -> None:
self.c.execute(
"INSERT OR REPLACE INTO message_cache (conversation_id, message) VALUES (?, ?)",
(conversation_id, json.dumps(message_value)),
)
self.conn.commit()
2024-05-21 22:48:41 +08:00
class SessionChatHistoryCache(SingletonBaseClass):
def __init__(self) -> None:
"""load all history conversation json file from self.session_cache_location"""
self.cache = SQliteLazyCache(cache_location=RD_AGENT_SETTINGS.prompt_cache_path)
2024-05-21 22:48:41 +08:00
2024-06-12 15:12:11 +08:00
def message_get(self, conversation_id: str) -> list[str]:
return self.cache.message_get(conversation_id)
2024-05-21 22:48:41 +08:00
2024-06-12 15:12:11 +08:00
def message_set(self, conversation_id: str, message_value: list[str]) -> None:
self.cache.message_set(conversation_id, message_value)
2024-05-21 22:48:41 +08:00
class ChatSession:
2024-06-12 15:12:11 +08:00
def __init__(self, api_backend: Any, conversation_id: str | None = None, system_prompt: str | None = None) -> None:
2024-05-21 22:48:41 +08:00
self.conversation_id = str(uuid.uuid4()) if conversation_id is None else conversation_id
self.cfg = RD_AGENT_SETTINGS
2024-05-21 22:48:41 +08:00
self.system_prompt = system_prompt if system_prompt is not None else self.cfg.default_system_prompt
self.api_backend = api_backend
2024-06-12 15:12:11 +08:00
def build_chat_completion_message(self, user_prompt: str) -> list[dict[str, Any]]:
2024-05-21 22:48:41 +08:00
history_message = SessionChatHistoryCache().message_get(self.conversation_id)
messages = history_message
if not messages:
messages.append({"role": "system", "content": self.system_prompt})
messages.append(
{
"role": "user",
"content": user_prompt,
2024-05-30 10:33:07 +08:00
},
2024-05-21 22:48:41 +08:00
)
return messages
2024-06-12 15:12:11 +08:00
def build_chat_completion_message_and_calculate_token(self, user_prompt: str) -> Any:
messages = self.build_chat_completion_message(user_prompt)
2024-05-21 22:48:41 +08:00
return self.api_backend.calculate_token_from_messages(messages)
2024-06-12 15:12:11 +08:00
def build_chat_completion(self, user_prompt: str, **kwargs: Any) -> str:
2024-05-21 22:48:41 +08:00
"""
this function is to build the session messages
user prompt should always be provided
"""
2024-06-14 12:59:44 +08:00
messages = self.build_chat_completion_message(user_prompt)
2024-07-17 15:00:13 +08:00
2024-07-16 20:35:42 +08:00
with logger.tag(f"session_{self.conversation_id}"):
response = self.api_backend._try_create_chat_completion_or_embedding( # noqa: SLF001
messages=messages,
chat_completion=True,
**kwargs,
)
2024-05-21 22:48:41 +08:00
messages.append(
{
"role": "assistant",
"content": response,
2024-05-30 10:33:07 +08:00
},
2024-05-21 22:48:41 +08:00
)
SessionChatHistoryCache().message_set(self.conversation_id, messages)
return response
2024-06-12 15:12:11 +08:00
def get_conversation_id(self) -> str:
2024-05-21 22:48:41 +08:00
return self.conversation_id
2024-06-12 15:12:11 +08:00
def display_history(self) -> None:
2024-05-21 22:48:41 +08:00
# TODO: Realize a beautiful presentation format for history messages
pass
class APIBackend:
def __init__( # noqa: C901, PLR0912, PLR0915
2024-05-21 22:48:41 +08:00
self,
*,
2024-06-12 15:12:11 +08:00
chat_api_key: str | None = None,
chat_model: str | None = None,
chat_api_base: str | None = None,
chat_api_version: str | None = None,
embedding_api_key: str | None = None,
embedding_model: str | None = None,
embedding_api_base: str | None = None,
embedding_api_version: str | None = None,
use_chat_cache: bool | None = None,
dump_chat_cache: bool | None = None,
use_embedding_cache: bool | None = None,
dump_embedding_cache: bool | None = None,
2024-05-21 22:48:41 +08:00
) -> None:
self.cfg = RD_AGENT_SETTINGS
2024-05-21 22:48:41 +08:00
if self.cfg.use_llama2:
self.generator = Llama.build(
ckpt_dir=self.cfg.llama2_ckpt_dir,
tokenizer_path=self.cfg.llama2_tokenizer_path,
max_seq_len=self.cfg.max_tokens,
max_batch_size=self.cfg.llams2_max_batch_size,
)
self.encoder = None
elif self.cfg.use_gcr_endpoint:
2024-06-12 15:12:11 +08:00
gcr_endpoint_type = self.cfg.gcr_endpoint_type
if gcr_endpoint_type == "llama2_70b":
self.gcr_endpoint_key = self.cfg.llama2_70b_endpoint_key
2024-05-21 22:48:41 +08:00
self.gcr_endpoint_deployment = self.cfg.llama2_70b_endpoint_deployment
self.gcr_endpoint = self.cfg.llama2_70b_endpoint
2024-06-12 15:12:11 +08:00
elif gcr_endpoint_type == "llama3_70b":
self.gcr_endpoint_key = self.cfg.llama3_70b_endpoint_key
2024-05-21 22:48:41 +08:00
self.gcr_endpoint_deployment = self.cfg.llama3_70b_endpoint_deployment
self.gcr_endpoint = self.cfg.llama3_70b_endpoint
2024-06-12 15:12:11 +08:00
elif gcr_endpoint_type == "phi2":
self.gcr_endpoint_key = self.cfg.phi2_endpoint_key
2024-05-21 22:48:41 +08:00
self.gcr_endpoint_deployment = self.cfg.phi2_endpoint_deployment
self.gcr_endpoint = self.cfg.phi2_endpoint
2024-06-12 15:12:11 +08:00
elif gcr_endpoint_type == "phi3_4k":
self.gcr_endpoint_key = self.cfg.phi3_4k_endpoint_key
2024-05-21 22:48:41 +08:00
self.gcr_endpoint_deployment = self.cfg.phi3_4k_endpoint_deployment
self.gcr_endpoint = self.cfg.phi3_4k_endpoint
2024-06-12 15:12:11 +08:00
elif gcr_endpoint_type == "phi3_128k":
self.gcr_endpoint_key = self.cfg.phi3_128k_endpoint_key
2024-05-21 22:48:41 +08:00
self.gcr_endpoint_deployment = self.cfg.phi3_128k_endpoint_deployment
self.gcr_endpoint = self.cfg.phi3_128k_endpoint
else:
2024-06-12 15:12:11 +08:00
error_message = f"Invalid gcr_endpoint_type: {gcr_endpoint_type}"
raise ValueError(error_message)
2024-05-21 22:48:41 +08:00
self.headers = {
"Content-Type": "application/json",
2024-06-12 15:12:11 +08:00
"Authorization": ("Bearer " + self.gcr_endpoint_key),
2024-05-21 22:48:41 +08:00
"azureml-model-deployment": self.gcr_endpoint_deployment,
}
self.gcr_endpoint_temperature = self.cfg.gcr_endpoint_temperature
self.gcr_endpoint_top_p = self.cfg.gcr_endpoint_top_p
self.gcr_endpoint_do_sample = self.cfg.gcr_endpoint_do_sample
self.gcr_endpoint_max_token = self.cfg.gcr_endpoint_max_token
2024-06-12 15:12:11 +08:00
if not os.environ.get("PYTHONHTTPSVERIFY", "") and hasattr(ssl, "_create_unverified_context"):
ssl._create_default_https_context = ssl._create_unverified_context # noqa: SLF001
2024-05-21 22:48:41 +08:00
self.encoder = None
else:
self.use_azure = self.cfg.use_azure
2024-06-05 15:36:15 +08:00
self.use_azure_token_provider = self.cfg.use_azure_token_provider
self.managed_identity_client_id = self.cfg.managed_identity_client_id
2024-05-21 22:48:41 +08:00
2024-08-02 15:49:58 +08:00
# Priority: chat_api_key/embedding_api_key > openai_api_key > os.environ.get("OPENAI_API_KEY")
# TODO: Simplify the key design. Consider Pandatic's field alias & priority.
self.chat_api_key = (
chat_api_key
or self.cfg.chat_openai_api_key
or self.cfg.openai_api_key
or os.environ.get("OPENAI_API_KEY")
)
self.embedding_api_key = (
embedding_api_key
or self.cfg.embedding_openai_api_key
or self.cfg.openai_api_key
or os.environ.get("OPENAI_API_KEY")
)
2024-05-21 22:48:41 +08:00
self.chat_model = self.cfg.chat_model if chat_model is None else chat_model
self.encoder = tiktoken.encoding_for_model(self.chat_model)
self.chat_api_base = self.cfg.chat_azure_api_base if chat_api_base is None else chat_api_base
self.chat_api_version = self.cfg.chat_azure_api_version if chat_api_version is None else chat_api_version
self.chat_stream = self.cfg.chat_stream
self.chat_seed = self.cfg.chat_seed
self.embedding_model = self.cfg.embedding_model if embedding_model is None else embedding_model
self.embedding_api_base = (
self.cfg.embedding_azure_api_base if embedding_api_base is None else embedding_api_base
)
self.embedding_api_version = (
self.cfg.embedding_azure_api_version if embedding_api_version is None else embedding_api_version
)
if self.use_azure:
2024-06-05 15:36:15 +08:00
if self.use_azure_token_provider:
dac_kwargs = {}
if self.managed_identity_client_id is not None:
dac_kwargs["managed_identity_client_id"] = self.managed_identity_client_id
credential = DefaultAzureCredential(**dac_kwargs)
2024-06-05 15:36:15 +08:00
token_provider = get_bearer_token_provider(
credential,
"https://cognitiveservices.azure.com/.default",
2024-06-05 15:36:15 +08:00
)
self.chat_client = openai.AzureOpenAI(
azure_ad_token_provider=token_provider,
api_version=self.chat_api_version,
azure_endpoint=self.chat_api_base,
)
self.embedding_client = openai.AzureOpenAI(
azure_ad_token_provider=token_provider,
api_version=self.embedding_api_version,
azure_endpoint=self.embedding_api_base,
)
else:
self.chat_client = openai.AzureOpenAI(
api_key=self.chat_api_key,
api_version=self.chat_api_version,
azure_endpoint=self.chat_api_base,
)
self.embedding_client = openai.AzureOpenAI(
api_key=self.embedding_api_key,
api_version=self.embedding_api_version,
azure_endpoint=self.embedding_api_base,
)
2024-05-21 22:48:41 +08:00
else:
self.chat_client = openai.OpenAI(api_key=self.chat_api_key)
self.embedding_client = openai.OpenAI(api_key=self.embedding_api_key)
self.dump_chat_cache = self.cfg.dump_chat_cache if dump_chat_cache is None else dump_chat_cache
self.use_chat_cache = self.cfg.use_chat_cache if use_chat_cache is None else use_chat_cache
self.dump_embedding_cache = (
self.cfg.dump_embedding_cache if dump_embedding_cache is None else dump_embedding_cache
)
self.use_embedding_cache = self.cfg.use_embedding_cache if use_embedding_cache is None else use_embedding_cache
if self.dump_chat_cache or self.use_chat_cache or self.dump_embedding_cache or self.use_embedding_cache:
self.cache_file_location = self.cfg.prompt_cache_path
2024-06-05 15:36:15 +08:00
self.cache = SQliteLazyCache(cache_location=self.cache_file_location)
2024-05-21 22:48:41 +08:00
# transfer the config to the class if the config is not supposed to change during the runtime
self.use_llama2 = self.cfg.use_llama2
self.use_gcr_endpoint = self.cfg.use_gcr_endpoint
self.retry_wait_seconds = self.cfg.retry_wait_seconds
2024-06-12 15:12:11 +08:00
def build_chat_session(
self,
conversation_id: str | None = None,
session_system_prompt: str | None = None,
) -> ChatSession:
2024-05-21 22:48:41 +08:00
"""
conversation_id is a 256-bit string created by uuid.uuid4() and is also
the file name under session_cache_folder/ for each conversation
"""
2024-06-12 15:12:11 +08:00
return ChatSession(self, conversation_id, session_system_prompt)
2024-05-21 22:48:41 +08:00
def build_messages(
self,
2024-06-12 15:12:11 +08:00
user_prompt: str,
system_prompt: str | None = None,
former_messages: list[dict] | None = None,
*,
shrink_multiple_break: bool = False,
) -> list[dict]:
2024-05-21 22:48:41 +08:00
"""build the messages to avoid implementing several redundant lines of code"""
2024-06-12 15:12:11 +08:00
if former_messages is None:
former_messages = []
2024-05-21 22:48:41 +08:00
# shrink multiple break will recursively remove multiple breaks(more than 2)
if shrink_multiple_break:
while "\n\n\n" in user_prompt:
user_prompt = user_prompt.replace("\n\n\n", "\n\n")
2024-06-12 15:12:11 +08:00
if system_prompt is not None:
while "\n\n\n" in system_prompt:
system_prompt = system_prompt.replace("\n\n\n", "\n\n")
2024-05-21 22:48:41 +08:00
system_prompt = self.cfg.default_system_prompt if system_prompt is None else system_prompt
messages = [
{
"role": "system",
"content": system_prompt,
2024-05-30 10:33:07 +08:00
},
2024-05-21 22:48:41 +08:00
]
messages.extend(former_messages[-1 * self.cfg.max_past_message_include :])
messages.append(
{
"role": "user",
"content": user_prompt,
2024-05-30 10:33:07 +08:00
},
2024-05-21 22:48:41 +08:00
)
return messages
def build_messages_and_create_chat_completion(
self,
2024-06-12 15:12:11 +08:00
user_prompt: str,
system_prompt: str | None = None,
former_messages: list | None = None,
chat_cache_prefix: str = "",
*,
shrink_multiple_break: bool = False,
**kwargs: Any,
) -> str:
if former_messages is None:
former_messages = []
messages = self.build_messages(
user_prompt, system_prompt, former_messages, shrink_multiple_break=shrink_multiple_break
)
2024-06-12 15:12:11 +08:00
return self._try_create_chat_completion_or_embedding(
2024-05-21 22:48:41 +08:00
messages=messages,
chat_completion=True,
chat_cache_prefix=chat_cache_prefix,
**kwargs,
)
2024-06-12 15:12:11 +08:00
def create_embedding(self, input_content: str | list[str], **kwargs: Any) -> list[Any] | Any:
input_content_list = [input_content] if isinstance(input_content, str) else input_content
2024-05-21 22:48:41 +08:00
resp = self._try_create_chat_completion_or_embedding(
input_content_list=input_content_list,
embedding=True,
**kwargs,
2024-05-21 22:48:41 +08:00
)
if isinstance(input_content, str):
return resp[0]
2024-06-12 15:12:11 +08:00
return resp
2024-05-21 22:48:41 +08:00
2024-06-12 15:12:11 +08:00
def _create_chat_completion_auto_continue(self, messages: list, **kwargs: dict) -> str:
2024-05-21 22:48:41 +08:00
"""
2024-06-12 15:12:11 +08:00
Call the chat completion function and automatically continue the conversation if the finish_reason is length.
TODO: This function only continues once, maybe need to continue more than once in the future.
2024-05-21 22:48:41 +08:00
"""
response, finish_reason = self._create_chat_completion_inner_function(messages=messages, **kwargs)
if finish_reason == "length":
new_message = deepcopy(messages)
new_message.append({"role": "assistant", "content": response})
new_message.append(
{
"role": "user",
"content": "continue the former output with no overlap",
2024-05-30 10:33:07 +08:00
},
2024-05-21 22:48:41 +08:00
)
new_response, finish_reason = self._create_chat_completion_inner_function(messages=new_message, **kwargs)
return response + new_response
2024-06-12 15:12:11 +08:00
return response
2024-05-21 22:48:41 +08:00
2024-06-12 15:12:11 +08:00
def _try_create_chat_completion_or_embedding(
self,
max_retry: int = 10,
*,
chat_completion: bool = False,
embedding: bool = False,
**kwargs: Any,
2024-06-12 15:12:11 +08:00
) -> Any:
2024-05-21 22:48:41 +08:00
assert not (chat_completion and embedding), "chat_completion and embedding cannot be True at the same time"
max_retry = self.cfg.max_retry if self.cfg.max_retry is not None else max_retry
for i in range(max_retry):
try:
if embedding:
2024-06-12 15:12:11 +08:00
return self._create_embedding_inner_function(**kwargs)
if chat_completion:
return self._create_chat_completion_auto_continue(**kwargs)
except openai.BadRequestError as e: # noqa: PERF203
2024-07-16 20:35:42 +08:00
logger.warning(e)
logger.warning(f"Retrying {i+1}th time...")
2024-06-12 15:12:11 +08:00
if "'messages' must contain the word 'json' in some form" in e.message:
2024-05-21 22:48:41 +08:00
kwargs["add_json_in_prompt"] = True
2024-06-12 15:12:11 +08:00
elif embedding and "maximum context length" in e.message:
kwargs["input_content_list"] = [
content[: len(content) // 2] for content in kwargs.get("input_content_list", [])
]
except Exception as e: # noqa: BLE001
2024-07-16 20:35:42 +08:00
logger.warning(e)
logger.warning(f"Retrying {i+1}th time...")
2024-06-12 15:12:11 +08:00
time.sleep(self.retry_wait_seconds)
error_message = f"Failed to create chat completion after {max_retry} retries."
raise RuntimeError(error_message)
2024-05-21 22:48:41 +08:00
def _create_embedding_inner_function(
self, input_content_list: list[str], **kwargs: Any
) -> list[Any]: # noqa: ARG002
2024-05-21 22:48:41 +08:00
content_to_embedding_dict = {}
filtered_input_content_list = []
if self.use_embedding_cache:
for content in input_content_list:
cache_result = self.cache.embedding_get(content)
if cache_result is not None:
content_to_embedding_dict[content] = cache_result
else:
filtered_input_content_list.append(content)
else:
filtered_input_content_list = input_content_list
if len(filtered_input_content_list) > 0:
if self.use_azure:
response = self.embedding_client.embeddings.create(
model=self.embedding_model,
input=filtered_input_content_list,
)
else:
response = self.embedding_client.embeddings.create(
model=self.embedding_model,
input=filtered_input_content_list,
)
for index, data in enumerate(response.data):
content_to_embedding_dict[filtered_input_content_list[index]] = data.embedding
if self.dump_embedding_cache:
self.cache.embedding_set(content_to_embedding_dict)
2024-06-12 15:12:11 +08:00
return [content_to_embedding_dict[content] for content in input_content_list]
2024-07-16 20:35:42 +08:00
def _build_log_messages(self, messages: list[dict]) -> str:
2024-05-21 22:48:41 +08:00
log_messages = ""
for m in messages:
log_messages += (
f"\n{LogColors.MAGENTA}{LogColors.BOLD}Role:{LogColors.END}"
2024-06-12 15:12:11 +08:00
f"{LogColors.CYAN}{m['role']}{LogColors.END}\n"
f"{LogColors.MAGENTA}{LogColors.BOLD}Content:{LogColors.END} "
f"{LogColors.CYAN}{m['content']}{LogColors.END}\n"
2024-05-21 22:48:41 +08:00
)
return log_messages
def _create_chat_completion_inner_function( # noqa: C901, PLR0912, PLR0915
2024-05-21 22:48:41 +08:00
self,
2024-06-12 15:12:11 +08:00
messages: list[dict],
temperature: float | None = None,
max_tokens: int | None = None,
chat_cache_prefix: str = "",
frequency_penalty: float | None = None,
presence_penalty: float | None = None,
*,
json_mode: bool = False,
add_json_in_prompt: bool = False,
2024-05-21 22:48:41 +08:00
) -> str:
2024-07-16 20:35:42 +08:00
# TODO: we can add this function back to avoid so much `self.cfg.log_llm_chat_content`
if self.cfg.log_llm_chat_content:
logger.info(self._build_log_messages(messages), tag="llm_messages")
2024-05-21 22:48:41 +08:00
# TODO: fail to use loguru adaptor due to stream response
input_content_json = json.dumps(messages)
input_content_json = (
chat_cache_prefix + input_content_json
) # FIXME this is a hack to make sure the cache represents the round index
if self.use_chat_cache:
cache_result = self.cache.chat_get(input_content_json)
if cache_result is not None:
2024-07-16 20:35:42 +08:00
if self.cfg.log_llm_chat_content:
logger.info(f"{LogColors.CYAN}Response:{cache_result}{LogColors.END}", tag="llm_messages")
2024-05-21 22:48:41 +08:00
return cache_result, None
if temperature is None:
temperature = self.cfg.chat_temperature
if max_tokens is None:
max_tokens = self.cfg.chat_max_tokens
if frequency_penalty is None:
frequency_penalty = self.cfg.chat_frequency_penalty
if presence_penalty is None:
presence_penalty = self.cfg.chat_presence_penalty
finish_reason = None
if self.use_llama2:
response = self.generator.chat_completion(
messages, # type: ignore
max_gen_len=max_tokens,
temperature=temperature,
)
resp = response[0]["generation"]["content"]
2024-07-16 20:35:42 +08:00
if self.cfg.log_llm_chat_content:
logger.info(f"{LogColors.CYAN}Response:{resp}{LogColors.END}", tag="llm_messages")
2024-05-21 22:48:41 +08:00
elif self.use_gcr_endpoint:
body = str.encode(
json.dumps(
{
"input_data": {
"input_string": messages,
"parameters": {
"temperature": self.gcr_endpoint_temperature,
"top_p": self.gcr_endpoint_top_p,
"do_sample": self.gcr_endpoint_do_sample,
"max_new_tokens": self.gcr_endpoint_max_token,
},
2024-05-30 10:33:07 +08:00
},
},
),
2024-05-21 22:48:41 +08:00
)
req = urllib.request.Request(self.gcr_endpoint, body, self.headers) # noqa: S310
response = urllib.request.urlopen(req) # noqa: S310
2024-05-21 22:48:41 +08:00
resp = json.loads(response.read().decode())["output"]
2024-07-16 20:35:42 +08:00
if self.cfg.log_llm_chat_content:
logger.info(f"{LogColors.CYAN}Response:{resp}{LogColors.END}", tag="llm_messages")
2024-05-21 22:48:41 +08:00
else:
2024-07-30 18:06:48 +08:00
kwargs = dict(
model=self.chat_model,
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
stream=self.chat_stream,
seed=self.chat_seed,
frequency_penalty=frequency_penalty,
presence_penalty=presence_penalty,
)
if json_mode:
if add_json_in_prompt:
for message in messages[::-1]:
message["content"] = message["content"] + "\nPlease respond in json format."
if message["role"] == "system":
break
kwargs["response_format"] = {"type": "json_object"}
response = self.chat_client.chat.completions.create(**kwargs)
2024-05-21 22:48:41 +08:00
if self.chat_stream:
resp = ""
2024-07-16 20:35:42 +08:00
# TODO: with logger.config(stream=self.chat_stream): and add a `stream_start` flag to add timestamp for first message.
if self.cfg.log_llm_chat_content:
logger.info(f"{LogColors.CYAN}Response:{LogColors.END}", tag="llm_messages")
2024-07-17 15:00:13 +08:00
2024-05-21 22:48:41 +08:00
for chunk in response:
content = (
chunk.choices[0].delta.content
if len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None
else ""
)
if self.cfg.log_llm_chat_content:
2024-07-16 20:35:42 +08:00
logger.info(LogColors.CYAN + content + LogColors.END, raw=True, tag="llm_messages")
2024-05-21 22:48:41 +08:00
resp += content
if len(chunk.choices) > 0 and chunk.choices[0].finish_reason is not None:
finish_reason = chunk.choices[0].finish_reason
2024-07-17 15:00:13 +08:00
2024-07-16 20:35:42 +08:00
if self.cfg.log_llm_chat_content:
logger.info("\n", raw=True, tag="llm_messages")
2024-07-17 15:00:13 +08:00
2024-05-21 22:48:41 +08:00
else:
resp = response.choices[0].message.content
finish_reason = response.choices[0].finish_reason
2024-07-16 20:35:42 +08:00
if self.cfg.log_llm_chat_content:
logger.info(f"{LogColors.CYAN}Response:{resp}{LogColors.END}", tag="llm_messages")
2024-05-21 22:48:41 +08:00
if json_mode:
json.loads(resp)
if self.dump_chat_cache:
self.cache.chat_set(input_content_json, resp)
return resp, finish_reason
2024-06-12 15:12:11 +08:00
def calculate_token_from_messages(self, messages: list[dict]) -> int:
2024-05-21 22:48:41 +08:00
if self.use_llama2 or self.use_gcr_endpoint:
2024-07-16 20:35:42 +08:00
logger.warning("num_tokens_from_messages() is not implemented for model llama2.")
2024-05-21 22:48:41 +08:00
return 0 # TODO implement this function for llama2
if "gpt4" in self.chat_model or "gpt-4" in self.chat_model:
tokens_per_message = 3
tokens_per_name = 1
else:
2024-06-07 00:23:11 +08:00
tokens_per_message = 4 # every message follows <start>{role/name}\n{content}<end>\n
2024-05-21 22:48:41 +08:00
tokens_per_name = -1 # if there's a name, the role is omitted
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(self.encoder.encode(value))
if key == "name":
num_tokens += tokens_per_name
2024-06-07 00:23:11 +08:00
num_tokens += 3 # every reply is primed with <start>assistant<message>
2024-05-21 22:48:41 +08:00
return num_tokens
def build_messages_and_calculate_token(
self,
2024-06-12 15:12:11 +08:00
user_prompt: str,
system_prompt: str | None,
former_messages: list[dict] | None = None,
*,
shrink_multiple_break: bool = False,
) -> int:
if former_messages is None:
former_messages = []
messages = self.build_messages(
user_prompt, system_prompt, former_messages, shrink_multiple_break=shrink_multiple_break
)
2024-05-21 22:48:41 +08:00
return self.calculate_token_from_messages(messages)
2024-06-12 15:12:11 +08:00
def calculate_embedding_process(str_list: list) -> list:
2024-05-21 22:48:41 +08:00
return APIBackend().create_embedding(str_list)
2024-06-12 15:12:11 +08:00
def create_embedding_with_multiprocessing(str_list: list, slice_count: int = 50, nproc: int = 8) -> list:
2024-05-21 22:48:41 +08:00
embeddings = []
pool = multiprocessing.Pool(nproc)
result_list = [
pool.apply_async(calculate_embedding_process, (str_list[index : index + slice_count],))
for index in range(0, len(str_list), slice_count)
]
2024-05-21 22:48:41 +08:00
pool.close()
pool.join()
for res in result_list:
embeddings.extend(res.get())
return embeddings
2024-06-12 15:12:11 +08:00
def calculate_embedding_distance_between_str_list(
source_str_list: list[str],
target_str_list: list[str],
2024-06-12 15:12:11 +08:00
) -> list[list[float]]:
if not source_str_list or not target_str_list:
2024-05-21 22:48:41 +08:00
return [[]]
embeddings = create_embedding_with_multiprocessing(source_str_list + target_str_list, slice_count=50, nproc=8)
source_embeddings = embeddings[: len(source_str_list)]
target_embeddings = embeddings[len(source_str_list) :]
2024-05-21 22:48:41 +08:00
source_embeddings_np = np.array(source_embeddings)
target_embeddings_np = np.array(target_embeddings)
source_embeddings_np = source_embeddings_np / np.linalg.norm(source_embeddings_np, axis=1, keepdims=True)
target_embeddings_np = target_embeddings_np / np.linalg.norm(target_embeddings_np, axis=1, keepdims=True)
similarity_matrix = np.dot(source_embeddings_np, target_embeddings_np.T)
return similarity_matrix.tolist()