e768098d0e
Flake8 Lint / flake8 (push) Waiting to run
Spell check CI / Spell_Check (push) Waiting to run
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
291 lines
12 KiB
Python
291 lines
12 KiB
Python
import asyncio
|
|
import logging
|
|
import time
|
|
import uuid
|
|
from typing import List
|
|
from openai.version import VERSION as OPENAI_VERSION
|
|
import os
|
|
from abc import ABC, abstractmethod
|
|
import tiktoken
|
|
from dotenv import load_dotenv
|
|
from prompt import PromptLimitException
|
|
|
|
|
|
class AOAI(ABC):
|
|
def __init__(self, **kwargs):
|
|
if OPENAI_VERSION.startswith("0."):
|
|
raise Exception(
|
|
"Please upgrade your OpenAI package to version >= 1.0.0 or "
|
|
"using the command: pip install --upgrade openai."
|
|
)
|
|
init_params = {}
|
|
api_type = os.environ.get("API_TYPE")
|
|
if os.getenv("OPENAI_API_VERSION") is not None:
|
|
init_params["api_version"] = os.environ.get("OPENAI_API_VERSION")
|
|
if os.getenv("OPENAI_ORG_ID") is not None:
|
|
init_params["organization"] = os.environ.get("OPENAI_ORG_ID")
|
|
if os.getenv("OPENAI_API_KEY") is None:
|
|
raise ValueError("OPENAI_API_KEY is not set in environment variables")
|
|
if os.getenv("OPENAI_API_BASE") is not None:
|
|
if api_type == "azure":
|
|
init_params["azure_endpoint"] = os.environ.get("OPENAI_API_BASE")
|
|
else:
|
|
init_params["base_url"] = os.environ.get("OPENAI_API_BASE")
|
|
init_params["api_key"] = os.environ.get("OPENAI_API_KEY")
|
|
# A few sanity checks
|
|
if api_type == "azure":
|
|
if init_params.get("azure_endpoint") is None:
|
|
raise ValueError(
|
|
"OPENAI_API_BASE is not set in environment variables, this is required when api_type==azure"
|
|
)
|
|
if init_params.get("api_version") is None:
|
|
raise ValueError(
|
|
"OPENAI_API_VERSION is not set in environment variables, this is required when api_type==azure"
|
|
)
|
|
if init_params["api_key"].startswith("sk-"):
|
|
raise ValueError(
|
|
"OPENAI_API_KEY should not start with sk- when api_type==azure, "
|
|
"are you using openai key by mistake?"
|
|
)
|
|
from openai import AzureOpenAI as Client
|
|
from openai import AsyncAzureOpenAI as AsyncClient
|
|
else:
|
|
from openai import OpenAI as Client
|
|
from openai import AsyncClient as AsyncClient
|
|
self.client = Client(**init_params)
|
|
self.async_client = AsyncClient(**init_params)
|
|
self.default_engine = None
|
|
self.engine = kwargs.pop('model', None) or os.environ.get("MODEL")
|
|
self.total_tokens = 4000
|
|
self.max_tokens = kwargs.pop('max_tokens', None) or os.environ.get("MAX_TOKENS") or 1200
|
|
if self.engine == "gpt-4-32k":
|
|
self.total_tokens = 31000
|
|
if self.engine == "gpt-4":
|
|
self.total_tokens = 7000
|
|
if self.engine == "gpt-3.5-turbo-16k":
|
|
self.total_tokens = 15000
|
|
if self.max_tokens > self.total_tokens:
|
|
raise ValueError(f"max_tokens must be less than total_tokens, "
|
|
f"total_tokens is {self.total_tokens}, max_tokens is {self.max_tokens}")
|
|
self.tokens_limit = self.total_tokens - self.max_tokens
|
|
|
|
def count_tokens(self, text: str) -> int:
|
|
try:
|
|
encoding = tiktoken.encoding_for_model(self.engine)
|
|
except KeyError:
|
|
encoding = tiktoken.encoding_for_model(self.default_engine)
|
|
return len(encoding.encode(text))
|
|
|
|
def query(self, text, **kwargs):
|
|
stream = kwargs.pop("stream", False)
|
|
for i in range(3):
|
|
try:
|
|
if not stream:
|
|
return self.query_with_no_stream(text, **kwargs)
|
|
else:
|
|
return "".join(self.query_with_stream(text, **kwargs))
|
|
except Exception as e:
|
|
logging.error(f"Query failed, message={e}, "
|
|
f"will retry request llm after {(i + 1) * (i + 1)} seconds.")
|
|
time.sleep((i + 1) * (i + 1))
|
|
raise Exception("Query failed, and retry 3 times, but still failed.")
|
|
|
|
async def async_query(self, text, **kwargs):
|
|
stream = kwargs.pop("stream", False)
|
|
for i in range(3):
|
|
try:
|
|
if not stream:
|
|
res = await self.async_query_with_no_stream(text, **kwargs)
|
|
return res
|
|
else:
|
|
res = await self.async_query_with_stream(text, **kwargs)
|
|
return "".join(res)
|
|
except Exception as e:
|
|
logging.error(f"llm response error, message={e}, "
|
|
f"will retry request llm after {(i + 1) * (i + 1)} seconds.")
|
|
await asyncio.sleep((i + 1) * (i + 1))
|
|
raise Exception("llm response error, and retry 3 times, but still failed.")
|
|
|
|
@abstractmethod
|
|
def query_with_no_stream(self, text, **kwargs):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def query_with_stream(self, text, **kwargs):
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def async_query_with_no_stream(self, text, **kwargs):
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def async_query_with_stream(self, text, **kwargs):
|
|
pass
|
|
|
|
|
|
class ChatLLM(AOAI):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.default_engine = "gpt-3.5-turbo"
|
|
self.engine = self.engine or self.default_engine
|
|
self.system_prompt = "You are a Python engineer."
|
|
self.conversation = dict()
|
|
|
|
def query_with_no_stream(self, text, **kwargs):
|
|
conversation_id = kwargs.pop('conversation', None)
|
|
messages = self.create_prompt(text, conversation_id)
|
|
self.validate_tokens(messages)
|
|
temperature = kwargs.pop("temperature", 0.1)
|
|
response = self.client.chat.completions.create(
|
|
model=self.engine,
|
|
messages=messages,
|
|
temperature=temperature,
|
|
max_tokens=self.max_tokens,
|
|
stream=False,
|
|
**kwargs,
|
|
)
|
|
response_role = response.choices[0].message.role
|
|
full_response = response.choices[0].message.content
|
|
self.add_to_conversation(text, "user", conversation_id=conversation_id)
|
|
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
|
|
return full_response
|
|
|
|
def query_with_stream(self, text, **kwargs):
|
|
conversation_id = kwargs.pop('conversation', None)
|
|
messages = self.create_prompt(text, conversation_id)
|
|
self.validate_tokens(messages)
|
|
temperature = kwargs.pop("temperature", 0.1)
|
|
response = self.client.chat.completions.create(
|
|
model=self.engine,
|
|
messages=messages,
|
|
temperature=temperature,
|
|
max_tokens=self.max_tokens,
|
|
stream=True,
|
|
**kwargs,
|
|
)
|
|
|
|
response_role = None
|
|
full_response = ""
|
|
for chunk in response:
|
|
delta = chunk.choices[0].delta
|
|
response_role = delta.role
|
|
if delta.content:
|
|
content = delta.content
|
|
full_response += content
|
|
yield content
|
|
self.add_to_conversation(text, "user", conversation_id=conversation_id)
|
|
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
|
|
|
|
async def async_query_with_no_stream(self, text, **kwargs):
|
|
conversation_id = kwargs.pop('conversation', None)
|
|
messages = self.create_prompt(text, conversation_id)
|
|
self.validate_tokens(messages)
|
|
temperature = kwargs.pop("temperature", 0.1)
|
|
response = await self.async_client.chat.completions.create(
|
|
model=self.engine,
|
|
messages=messages,
|
|
temperature=temperature,
|
|
max_tokens=self.max_tokens,
|
|
stream=False,
|
|
**kwargs,
|
|
)
|
|
response_role = response.choices[0].message.role
|
|
full_response = response.choices[0].message.content
|
|
self.add_to_conversation(text, "user", conversation_id=conversation_id)
|
|
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
|
|
return full_response
|
|
|
|
async def async_query_with_stream(self, text, **kwargs):
|
|
conversation_id = kwargs.pop('conversation', None)
|
|
messages = self.create_prompt(text, conversation_id)
|
|
self.validate_tokens(messages)
|
|
temperature = kwargs.pop("temperature", 0.1)
|
|
response = await self.async_client.chat.completions.create(
|
|
model=self.engine,
|
|
messages=messages,
|
|
temperature=temperature,
|
|
max_tokens=self.max_tokens,
|
|
stream=True,
|
|
**kwargs,
|
|
)
|
|
|
|
response_role = None
|
|
full_response = ""
|
|
for chunk in response:
|
|
delta = chunk.choices[0].delta
|
|
response_role = delta.role
|
|
if delta.content:
|
|
content = delta.content
|
|
full_response += content
|
|
yield content
|
|
self.add_to_conversation(text, "user", conversation_id=conversation_id)
|
|
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
|
|
|
|
def get_unique_conversation_id(self):
|
|
return str(uuid.uuid4()).replace('-', '')
|
|
|
|
def add_to_conversation(self, message: str, role: str, conversation_id: str) -> None:
|
|
"""
|
|
Add a message to the conversation
|
|
"""
|
|
if type(conversation_id) is str:
|
|
self.conversation[conversation_id].append({"role": role, "content": message})
|
|
|
|
def del_conversation(self, conversation_id: str) -> None:
|
|
if conversation_id in self.conversation:
|
|
del self.conversation[conversation_id]
|
|
|
|
def init_conversation(self, conversation_id: str, system_prompt) -> None:
|
|
"""
|
|
Init a new conversation
|
|
"""
|
|
if type(conversation_id) is str:
|
|
self.conversation[conversation_id] = [{"role": "system", "content": system_prompt}]
|
|
|
|
def get_tokens_count(self, messages: List[dict]) -> int:
|
|
"""
|
|
Get token count
|
|
"""
|
|
num_tokens = 0
|
|
for message in messages:
|
|
# every message follows <im_start>{role/name}\n{content}<im_end>\n
|
|
num_tokens += 5
|
|
for key, value in message.items():
|
|
if value:
|
|
num_tokens += self.count_tokens(value)
|
|
if key == "name": # if there's a name, the role is omitted
|
|
num_tokens += 5 # role is always required and always 1 token
|
|
num_tokens += 5 # every reply is primed with <im_start>assistant
|
|
return num_tokens
|
|
|
|
def validate_tokens(self, messages: List[dict]) -> None:
|
|
total_tokens = self.get_tokens_count(messages)
|
|
if total_tokens > self.tokens_limit:
|
|
message = f"token count {total_tokens} exceeds limit {self.tokens_limit}"
|
|
raise PromptLimitException(message)
|
|
|
|
def create_prompt(self, text: str, conversation_id: str = None):
|
|
unique_conversation_id = self.get_unique_conversation_id()
|
|
conversation_id = conversation_id or unique_conversation_id
|
|
if conversation_id not in self.conversation:
|
|
self.init_conversation(conversation_id=conversation_id, system_prompt=self.system_prompt)
|
|
|
|
_conversation = self.conversation[conversation_id] + [{"role": "user", "content": text}]
|
|
|
|
while self.get_tokens_count(_conversation) > self.tokens_limit and len(_conversation) > 2:
|
|
_conversation.pop(1)
|
|
|
|
if unique_conversation_id == conversation_id:
|
|
self.del_conversation(conversation_id=unique_conversation_id)
|
|
|
|
return _conversation
|
|
|
|
|
|
if __name__ == "__main__":
|
|
load_dotenv()
|
|
llm = ChatLLM()
|
|
print(llm.query(text='how are you?'))
|
|
res = llm.query_with_stream(text='how are you?')
|
|
for item in res:
|
|
print(item)
|