Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add-OpenHermes-7B-Support #421

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
through programming." The agents within ChatDev **collaborate** by participating in specialized functional seminars,
including tasks such as designing, coding, testing, and documenting.
- The primary objective of ChatDev is to offer an **easy-to-use**, **highly customizable** and **extendable** framework,
which is based on large language models (LLMs) and serves as an ideal scenario for studying collective intelligence.
which is based on large language models (this fork uses OpenHermes 2.5 Mistral 7B) and serves as an ideal scenario for studying collective intelligence.

<p align="center">
<img src='./misc/company.png' width=600>
Expand Down
37 changes: 24 additions & 13 deletions camel/agents/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
openai_api_key_required,
)
from chatdev.utils import log_visualize
import re
try:
from openai.types.chat import ChatCompletion

Expand Down Expand Up @@ -109,6 +110,11 @@ def __init__(
else:
self.memory = None

def parse_number_bulets(self, text):
pattern = r'\d+\.\s+([^\n]+)'
matches = re.findall(pattern, text)
return matches

def reset(self) -> List[MessageType]:
r"""Resets the :obj:`ChatAgent` to its initial state and returns the
stored messages.
Expand Down Expand Up @@ -201,7 +207,7 @@ def use_memory(self,input_message) -> List[MessageType]:

return target_memory

@retry(wait=wait_exponential(min=5, max=60), stop=stop_after_attempt(5))
@retry(wait=wait_exponential(min=1, max=1), stop=stop_after_attempt(1))
@openai_api_key_required
def step(
self,
Expand Down Expand Up @@ -238,31 +244,36 @@ def step(
if num_tokens < self.model_token_limit:
response = self.model_backend.run(messages=openai_messages)
if openai_new_api:
if not isinstance(response, ChatCompletion):
raise RuntimeError("OpenAI returned unexpected struct")
#if not isinstance(response, ChatCompletion):
# raise RuntimeError("OpenAI returned unexpected struct")
output_messages = [
ChatMessage(role_name=self.role_name, role_type=self.role_type,
meta_dict=dict(), **dict(choice.message))
for choice in response.choices
#for choice in response.choices
for choice in self.parse_number_bulets(response)

]
info = self.get_info(
response.id,
response.usage,
[str(choice.finish_reason) for choice in response.choices],
#response.id,
#response.usage,
#[str(choice.finish_reason) for choice in self.parse_number_bulets(response)],
[str(choice) for choice in self.parse_number_bulets(response)],
num_tokens,
)
else:
if not isinstance(response, dict):
raise RuntimeError("OpenAI returned unexpected struct")
#if not isinstance(response, dict):
# raise RuntimeError("OpenAI returned unexpected struct")
output_messages = [
ChatMessage(role_name=self.role_name, role_type=self.role_type,
meta_dict=dict(), **dict(choice["message"]))
for choice in response["choices"]
#for choice in response["choices"]
for choice in self.parse_number_bulets(response)
]
info = self.get_info(
response["id"],
response["usage"],
[str(choice["finish_reason"]) for choice in response["choices"]],
#response["id"],
#response["usage"],
#[str(choice["finish_reason"]) for choice in self.parse_number_bulets(response)],
[str(choice) for choice in self.parse_number_bulets(response)],
num_tokens,
)

Expand Down
146 changes: 129 additions & 17 deletions camel/model_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,38 @@
from abc import ABC, abstractmethod
from typing import Any, Dict

import openai
import os
import tiktoken

from camel.typing import ModelType
from chatdev.statistics import prompt_cost
from chatdev.utils import log_visualize

try:
from openai.types.chat import ChatCompletion

openai_new_api = True # new openai api version
except ImportError:
openai_new_api = False # old openai api version

import os

OPENAI_API_KEY = os.environ['OPENAI_API_KEY']
if 'BASE_URL' in os.environ:
BASE_URL = os.environ['BASE_URL']
USE_OPENAI = False
if USE_OPENAI == True:
import openai
try:
from openai.types.chat import ChatCompletion

openai_new_api = True # new openai api version
except ImportError:
openai_new_api = False # old openai api version

OPENAI_API_KEY = os.environ['OPENAI_API_KEY']
if 'BASE_URL' in os.environ:
BASE_URL = os.environ['BASE_URL']
else:
BASE_URL = None
else:
BASE_URL = None

import re
from urllib.parse import urlencode
import subprocess
import json
import jsonstreams
from io import StringIO
from contextlib import redirect_stdout
BASE_URL = "http://localhost:11434/api/generate"
mistral_new_api = True # new mistral api version

class ModelBackend(ABC):
r"""Base class for different model backends.
Expand Down Expand Up @@ -145,6 +155,107 @@ def run(self, *args, **kwargs):
return response


class MistralAIModel(ModelBackend):
r"""Mistral API in a unified ModelBackend interface."""

def __init__(self, model_type: ModelType, model_config_dict: Dict) -> None:
super().__init__()
self.model_type = model_type
self.model_config_dict = model_config_dict

def generate_stream_json_response(self, prompt):
data = json.dumps({"model": "openhermes", "prompt": prompt})
process = subprocess.Popen(["curl", "-X", "POST", "-d", data, "http://localhost:11434/api/generate"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
full_response = ""
with jsonstreams.Stream(jsonstreams.Type.array, filename='./response_log.txt') as output:
while True:
line, _ = process.communicate()
if not line:
break
try:
record = line.decode("utf-8").split("\n")
for i in range(len(record)-1):
data = json.loads(record[i].replace('\0', ''))
if "response" in data:
full_response += data["response"]
with output.subobject() as output_e:
output_e.write('response', data["response"])
else:
return full_response.replace('\0', '')
if len(record)==1:
data = json.loads(record[0].replace('\0', ''))
if "error" in data:
full_response += data["error"]
with output.subobject() as output_e:
output_e.write('error', data["error"])
return full_response.replace('\0', '')
except Exception as error:
# handle the exception
print("An exception occurred:", error)
return full_response.replace('\0', '')

def run(self, *args, **kwargs):
string = "\n".join([message["content"] for message in kwargs["messages"]])
#fake model to enable tiktoken to work with mistral
#encoding = tiktoken.encoding_for_model(self.model_type.value)
encoding = tiktoken.encoding_for_model(ModelType.GPT_3_5_TURBO.value) #fake model to enable tiktoken to work with mistral
num_prompt_tokens = len(encoding.encode(string))
gap_between_send_receive = 15 * len(kwargs["messages"])
num_prompt_tokens += gap_between_send_receive

if mistral_new_api:
# Experimental, add base_url
num_max_token_map = {
"Mistral-7B": 8192,
}
num_max_token = num_max_token_map["Mistral-7B"] #hard coded model to enable tiktoken to work with mistral
num_max_completion_tokens = num_max_token - num_prompt_tokens
self.model_config_dict['max_tokens'] = num_max_completion_tokens

#response = client.chat.completions.create(*args, **kwargs, model=self.model_type.value,
# **self.model_config_dict)
print("DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG")
print("args:", args)
print("message: ", kwargs["messages"])
print("self.model_config_dict:", self.model_config_dict['max_tokens'])
print("prompt: ", string)
response = self.generate_stream_json_response("<|im_start|>system" + '\n' + string + "<|im_end|>")
print("--> ", response)
print("DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG")
log_visualize(
"**[Mistral_Usage_Info Receive]**\ncost: ${:.6f}\n".format(len(response.split())))
return response
else:
num_max_token_map = {
"Mistral-7B": 8192,
}
num_max_token = num_max_token_map[self.model_type.value]
num_max_completion_tokens = num_max_token - num_prompt_tokens
self.model_config_dict['max_tokens'] = num_max_completion_tokens
print("DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG")
print("args:", args)
print("message: ", kwargs["messages"])
print("self.model_config_dict:", self.model_config_dict['max_tokens'])
print("prompt: ", string)
response = self.generate_stream_json_response("<|im_start|>system" + '\n' + string + '\n' + "And always answer with a number of choices" +"<|im_end|>")
print("--> ", response)
print("DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG DEBUG")
log_visualize(
"**[Mistral_Usage_Info Receive]**\ncost: ${:.6f}\n".format(len(response.split())))

cost = prompt_cost(
self.model_type.value,
num_prompt_tokens=len(response.split()),#response["usage"]["prompt_tokens"],
num_completion_tokens=len(response.split()) #response["usage"]["completion_tokens"]
)

log_visualize(
"**[Mistral_Usage_Info Receive]**\n\ncost: ${:.6f}\n".format(
response["usage"]["total_tokens"], cost))

return response


class StubModel(ModelBackend):
r"""A dummy model used for unit tests."""

Expand Down Expand Up @@ -173,7 +284,7 @@ class ModelFactory:

@staticmethod
def create(model_type: ModelType, model_config_dict: Dict) -> ModelBackend:
default_model_type = ModelType.GPT_3_5_TURBO
default_model_type = ModelType.MISTRAL_7B

if model_type in {
ModelType.GPT_3_5_TURBO,
Expand All @@ -182,9 +293,10 @@ def create(model_type: ModelType, model_config_dict: Dict) -> ModelBackend:
ModelType.GPT_4_32k,
ModelType.GPT_4_TURBO,
ModelType.GPT_4_TURBO_V,
ModelType.MISTRAL_7B,
None
}:
model_class = OpenAIModel
model_class = MistralAIModel
elif model_type == ModelType.STUB:
model_class = StubModel
else:
Expand Down
3 changes: 2 additions & 1 deletion camel/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ class ModelType(Enum):
GPT_4_32k = "gpt-4-32k"
GPT_4_TURBO = "gpt-4-1106-preview"
GPT_4_TURBO_V = "gpt-4-1106-vision-preview"
MISTRAL_7B = "Mistral-7B"

STUB = "stub"

@property
def value_for_tiktoken(self):
return self.value if self.name != "STUB" else "gpt-3.5-turbo-16k-0613"
return self.value if self.name != "STUB" else "Mistral-7B"


class PhaseType(Enum):
Expand Down
Loading