mirror of
https://github.com/aljazceru/gpt-engineer.git
synced 2025-12-17 12:45:26 +01:00
* Added LangChain integration * Fixed issue created by git checkin process * Added ':' to characters to remove from end of file path * Tested initial migration to LangChain, removed comments and logging used for debugging * Tested initial migration to LangChain, removed comments and logging used for debugging * Converted camelCase to snake_case * Turns out we need the exception handling * Testing Hugging Face Integrations via LangChain * Added LangChain loadable models * Renames "qa" prompt to "clarify", since it's used in the "clarify" step, asking for clarification * Fixed loading model yaml files * Fixed streaming * Added modeldir cli option * Fixed typing * Fixed interaction with token logging * Fix spelling + dependency issues + typing * Fix spelling + tests * Removed unneeded logging which caused test to fail * Cleaned up code * Incorporated feedback - deleted unnecessary functions & logger.info - used LangChain ChatLLM instead of LLM to naturally communicate with gpt-4 - deleted loading model from yaml file, as LC doesn't offer this for ChatModels * Update gpt_engineer/steps.py Co-authored-by: Anton Osika <anton.osika@gmail.com> * Incorporated feedback - Fixed failing test - Removed parsing complexity by using # type: ignore - Replace every ocurence of ai.last_message_content with its content * Fixed test * Update gpt_engineer/steps.py --------- Co-authored-by: H <holden.robbins@gmail.com> Co-authored-by: Anton Osika <anton.osika@gmail.com>
198 lines
6.3 KiB
Python
198 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
from dataclasses import dataclass
|
|
from typing import List, Optional, Union
|
|
|
|
import openai
|
|
import tiktoken
|
|
|
|
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
|
|
from langchain.chat_models import ChatOpenAI
|
|
from langchain.chat_models.base import BaseChatModel
|
|
from langchain.schema import (
|
|
AIMessage,
|
|
HumanMessage,
|
|
SystemMessage,
|
|
messages_from_dict,
|
|
messages_to_dict,
|
|
)
|
|
|
|
Message = Union[AIMessage, HumanMessage, SystemMessage]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class TokenUsage:
|
|
step_name: str
|
|
in_step_prompt_tokens: int
|
|
in_step_completion_tokens: int
|
|
in_step_total_tokens: int
|
|
total_prompt_tokens: int
|
|
total_completion_tokens: int
|
|
total_tokens: int
|
|
|
|
|
|
class AI:
|
|
def __init__(self, model_name="gpt-4", temperature=0.1):
|
|
self.temperature = temperature
|
|
self.model_name = fallback_model(model_name)
|
|
self.llm = create_chat_model(self.model_name, temperature)
|
|
self.tokenizer = get_tokenizer(self.model_name)
|
|
|
|
# initialize token usage log
|
|
self.cumulative_prompt_tokens = 0
|
|
self.cumulative_completion_tokens = 0
|
|
self.cumulative_total_tokens = 0
|
|
self.token_usage_log = []
|
|
|
|
def start(self, system: str, user: str, step_name: str) -> List[Message]:
|
|
messages: List[Message] = [
|
|
SystemMessage(content=system),
|
|
HumanMessage(content=user),
|
|
]
|
|
return self.next(messages, step_name=step_name)
|
|
|
|
def fsystem(self, msg: str) -> SystemMessage:
|
|
return SystemMessage(content=msg)
|
|
|
|
def fuser(self, msg: str) -> HumanMessage:
|
|
return HumanMessage(content=msg)
|
|
|
|
def fassistant(self, msg: str) -> AIMessage:
|
|
return AIMessage(content=msg)
|
|
|
|
def next(
|
|
self,
|
|
messages: List[Message],
|
|
prompt: Optional[str] = None,
|
|
*,
|
|
step_name: str,
|
|
) -> List[Message]:
|
|
if prompt:
|
|
messages.append(self.fuser(prompt))
|
|
|
|
logger.debug(f"Creating a new chat completion: {messages}")
|
|
|
|
callsbacks = [StreamingStdOutCallbackHandler()]
|
|
response = self.llm(messages, callbacks=callsbacks) # type: ignore
|
|
messages.append(response)
|
|
|
|
logger.debug(f"Chat completion finished: {messages}")
|
|
|
|
self.update_token_usage_log(
|
|
messages=messages, answer=response.content, step_name=step_name
|
|
)
|
|
|
|
return messages
|
|
|
|
@staticmethod
|
|
def serialize_messages(messages: List[Message]) -> str:
|
|
return json.dumps(messages_to_dict(messages))
|
|
|
|
@staticmethod
|
|
def deserialize_messages(jsondictstr: str) -> List[Message]:
|
|
return list(messages_from_dict(json.loads(jsondictstr))) # type: ignore
|
|
|
|
def update_token_usage_log(
|
|
self, messages: List[Message], answer: str, step_name: str
|
|
) -> None:
|
|
prompt_tokens = self.num_tokens_from_messages(messages)
|
|
completion_tokens = self.num_tokens(answer)
|
|
total_tokens = prompt_tokens + completion_tokens
|
|
|
|
self.cumulative_prompt_tokens += prompt_tokens
|
|
self.cumulative_completion_tokens += completion_tokens
|
|
self.cumulative_total_tokens += total_tokens
|
|
|
|
self.token_usage_log.append(
|
|
TokenUsage(
|
|
step_name=step_name,
|
|
in_step_prompt_tokens=prompt_tokens,
|
|
in_step_completion_tokens=completion_tokens,
|
|
in_step_total_tokens=total_tokens,
|
|
total_prompt_tokens=self.cumulative_prompt_tokens,
|
|
total_completion_tokens=self.cumulative_completion_tokens,
|
|
total_tokens=self.cumulative_total_tokens,
|
|
)
|
|
)
|
|
|
|
def format_token_usage_log(self) -> str:
|
|
result = "step_name,"
|
|
result += "prompt_tokens_in_step,completion_tokens_in_step,total_tokens_in_step"
|
|
result += ",total_prompt_tokens,total_completion_tokens,total_tokens\n"
|
|
for log in self.token_usage_log:
|
|
result += log.step_name + ","
|
|
result += str(log.in_step_prompt_tokens) + ","
|
|
result += str(log.in_step_completion_tokens) + ","
|
|
result += str(log.in_step_total_tokens) + ","
|
|
result += str(log.total_prompt_tokens) + ","
|
|
result += str(log.total_completion_tokens) + ","
|
|
result += str(log.total_tokens) + "\n"
|
|
return result
|
|
|
|
def num_tokens(self, txt: str) -> int:
|
|
return len(self.tokenizer.encode(txt))
|
|
|
|
def num_tokens_from_messages(self, messages: List[Message]) -> int:
|
|
"""Returns the number of tokens used by a list of messages."""
|
|
n_tokens = 0
|
|
for message in messages:
|
|
n_tokens += (
|
|
4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
|
|
)
|
|
n_tokens += self.num_tokens(message.content)
|
|
n_tokens += 2 # every reply is primed with <im_start>assistant
|
|
return n_tokens
|
|
|
|
|
|
def fallback_model(model: str) -> str:
|
|
try:
|
|
openai.Model.retrieve(model)
|
|
return model
|
|
except openai.InvalidRequestError:
|
|
print(
|
|
f"Model {model} not available for provided API key. Reverting "
|
|
"to gpt-3.5-turbo. Sign up for the GPT-4 wait list here: "
|
|
"https://openai.com/waitlist/gpt-4-api\n"
|
|
)
|
|
return "gpt-3.5-turbo"
|
|
|
|
|
|
def create_chat_model(model: str, temperature) -> BaseChatModel:
|
|
if model == "gpt-4":
|
|
return ChatOpenAI(
|
|
model="gpt-4",
|
|
temperature=temperature,
|
|
streaming=True,
|
|
client=openai.ChatCompletion,
|
|
)
|
|
elif model == "gpt-3.5-turbo":
|
|
return ChatOpenAI(
|
|
model="gpt-3.5-turbo",
|
|
temperature=temperature,
|
|
streaming=True,
|
|
client=openai.ChatCompletion,
|
|
)
|
|
else:
|
|
raise ValueError(f"Model {model} is not supported.")
|
|
|
|
|
|
def get_tokenizer(model: str):
|
|
if "gpt-4" in model or "gpt-3.5" in model:
|
|
return tiktoken.encoding_for_model(model)
|
|
|
|
logger.debug(
|
|
f"No encoder implemented for model {model}."
|
|
"Defaulting to tiktoken cl100k_base encoder."
|
|
"Use results only as estimates."
|
|
)
|
|
return tiktoken.get_encoding("cl100k_base")
|
|
|
|
|
|
def serialize_messages(messages: List[Message]) -> str:
|
|
return AI.serialize_messages(messages)
|