From 173b375ec628961a80b8c1a5963a00972d74469b Mon Sep 17 00:00:00 2001 From: Stepan Date: Sat, 21 Oct 2023 18:33:59 +0100 Subject: [PATCH 1/9] Create style.yml --- .github/workflows/style.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/style.yml diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..9c1900d --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,25 @@ +name: style + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' # caching pip dependencies + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: | + black --check --diff . + isort --check --diff . From 3e80a7380124581894d39168fd9843b6e156263f Mon Sep 17 00:00:00 2001 From: Stepan Date: Sat, 21 Oct 2023 18:36:29 +0100 Subject: [PATCH 2/9] Update style.yml --- .github/workflows/style.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 9c1900d..7a88271 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -18,7 +18,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint + pip install -r requirements.txt - name: Analysing the code with pylint run: | black --check --diff . From a701f0183acd2004217c58872a2b6e19a237406e Mon Sep 17 00:00:00 2001 From: "laurence.hook" Date: Sat, 21 Oct 2023 21:34:18 +0100 Subject: [PATCH 3/9] tracks how often someone has spoken so that the chairman doesnt leave anyone out --- agents/agent.py | 21 +++++++++------------ agents/chairman.py | 43 +++++++++++++++++++++++++++---------------- agents/sme.py | 3 ++- gpt/gpt_client.py | 23 ++++++++++++----------- main.py | 4 +++- 5 files changed, 53 insertions(+), 41 deletions(-) diff --git a/agents/agent.py b/agents/agent.py index 4dfdc4a..0d4c0e1 100644 --- a/agents/agent.py +++ b/agents/agent.py @@ -2,21 +2,18 @@ from gpt.gpt_client import GPTClient +DEFAULT_SYSTEM_PROMPT = dedent("""\ +Provide succinct, fact-based answers. Eliminate filler words and politeness. +Concentrate on delivering actionable insights and concrete solutions. +Avoid vague or generic statements. Stick to the topic at hand. +# If you response doesn't meet these standards, reply with the exact phrase 'no comment' +""" +) class Agent: - def __init__(self, name: str, user_prompt: str): + def __init__(self, name: str, user_prompt: str, system_prompt: str = DEFAULT_SYSTEM_PROMPT): self.name = name - - self.common_instructions = dedent( - """\ - Provide succinct, fact-based answers. Eliminate filler words and politeness. - Concentrate on delivering actionable insights and concrete solutions. - Avoid vague or generic statements. Stick to the topic at hand. - If the query doesn't meet these standards, reply with 'no comment.' - """ - ) - - self.gpt_client = GPTClient(self.common_instructions, user_prompt) + self.gpt_client = GPTClient(system_prompt, user_prompt) def query_gpt(self, transcript: str) -> str: return self.gpt_client.query(transcript) diff --git a/agents/chairman.py b/agents/chairman.py index fb4b936..ed8cd01 100644 --- a/agents/chairman.py +++ b/agents/chairman.py @@ -3,34 +3,42 @@ from agents.agent import Agent from agents.sme import SME -logger.disable(__name__) + +# logger.disable(__name__) class Chairman(Agent): - def __init__(self, name: str, executives: list): + def __init__(self, name: str, SMEs: list[SME]): # Construct the user_prompt string with details of the executives - exec_details = "" - for executive_agent in executives: - exec_details += ( - f"{executive_agent.name}: expert in {executive_agent.expertise} " - f"and concerned about {', '.join(executive_agent.concerns)}.\n" - ) - user_prompt = ( - f"Your task is to decide who should speak next among meeting participates. " - f"Answer with only the name and nothing else. " - f"Do not call on the same person too often.\nParticipants: {exec_details}" + self.user_prompt = self.update_user_prompt(SMEs) + + system_prompt = ( + f"Answer with only the name and nothing else." ) # Call the superclass constructor with the constructed user_prompt - super().__init__(name, user_prompt) + super().__init__(name, self.user_prompt, system_prompt) + + self.SMEs = SMEs - self.executives = executives + def update_user_prompt(self, SMEs: list[SME]) -> str: + frequency_info_list = [] + for SME in SMEs: + frequency_info_list.append( + f"{SME.name}: expertise: {SME.expertise}. " + f"concerns: {', '.join(SME.concerns)}. spoken count: {SME.spoken_count}.\n" + ) + + return ( + f"Your task is to read the transcript and decide who should speak next. Do not choose the same person all of the time.\n" + f"Participants:\n{''.join(frequency_info_list)} " + ) def decide_if_meeting_over(self, transcript: list) -> bool: return False - def decide_next_speaker(self, transcript_list: list) -> SME: + def decide_next_speaker(self, transcript_list: list[str]) -> SME: transcript = " ".join(transcript_list) while True: @@ -38,10 +46,13 @@ def decide_next_speaker(self, transcript_list: list) -> SME: logger.info(f"Chairman called speaker: {next_speaker}") next_executive = next( - (exec for exec in self.executives if exec.name == next_speaker), None + (exec for exec in self.SMEs if exec.name == next_speaker), None ) if next_executive is not None: + next_executive.spoken_count += 1 # Update the frequency count + self.user_prompt = self.update_user_prompt(self.SMEs) + self.gpt_client.user_prompt = self.user_prompt return next_executive logger.info(f"{next_speaker} is not a valid exec...") diff --git a/agents/sme.py b/agents/sme.py index c083d91..bc6c795 100644 --- a/agents/sme.py +++ b/agents/sme.py @@ -17,7 +17,8 @@ def __init__(self, name: str, expertise: str, concerns: list[str]): super().__init__(name, user_prompt) self.expertise = expertise self.concerns = concerns + self.spoken_count = 0 - def opinion(self, transcript_list: list) -> str: + def opinion(self, transcript_list: list[str]) -> str: transcript = " ".join(transcript_list) return self.query_gpt(transcript) diff --git a/gpt/gpt_client.py b/gpt/gpt_client.py index 6900d7f..4845bd1 100644 --- a/gpt/gpt_client.py +++ b/gpt/gpt_client.py @@ -1,3 +1,4 @@ +import json import os from time import time @@ -5,7 +6,7 @@ from dotenv import load_dotenv from loguru import logger -logger.disable(__name__) +#logger.disable(__name__) # Load environment variables from .env file load_dotenv() @@ -19,9 +20,9 @@ class GPTClient: def __init__( - self, common_instructions: str, user_prompt: str, model: str = "gpt-4" + self, system_instructions: str, user_prompt: str, model: str = "gpt-4" ): - self.system_instructions = common_instructions + self.system_instructions = system_instructions self.user_prompt = user_prompt self.model = model self.max_tokens = 100 @@ -36,17 +37,17 @@ def __init__( def query(self, transcript: str) -> str: start_time = time() + messages = [ + {"role": "system", "content": self.system_instructions}, + {"role": "user", "content": self.user_prompt}, + {"role": "assistant", "content": transcript}, + ] + logger.info(json.dumps(messages, indent=4)) response = openai.ChatCompletion.create( model=self.model, temperature=self.temperature, - - messages=[ - {"role": "system", "content": self.system_instructions}, - {"role": "user", "content": self.user_prompt}, - {"role": "assistant", "content": transcript}, - - ], + messages=messages, ) end_time = time() @@ -54,6 +55,6 @@ def query(self, transcript: str) -> str: # Log the time taken and token usage logger.info(f"GPT query took {elapsed_time:.2f} seconds") - logger.info(f"Tokens used in the request: {response['usage']['total_tokens']}") + logger.info(f"Tokens used in the request: {response['usage']}") return response.choices[0].message.content.strip() diff --git a/main.py b/main.py index 9948244..c267a4f 100644 --- a/main.py +++ b/main.py @@ -80,7 +80,7 @@ def main(idea: tuple[str], config: Path = None): chairman = Chairman("Chairman", smes) - transcript = [idea] + transcript = [f"We are here to discuss this idea:\n{idea}\n\n"] print(transcript) while not chairman.decide_if_meeting_over(transcript): @@ -89,6 +89,8 @@ def main(idea: tuple[str], config: Path = None): opinion = speaker.opinion(transcript) print_with_wrap(f"\033[94m{speaker.name}\033[0m: {opinion}") + if opinion.strip().rstrip(".") != 'no comment': + transcript.append(opinion) if __name__ == "__main__": From c8c1a1523845a08412ea9ce5ecb0bb95365fd27b Mon Sep 17 00:00:00 2001 From: "laurence.hook" Date: Sat, 21 Oct 2023 23:02:24 +0100 Subject: [PATCH 4/9] incremental improvements --- agents/agent.py | 2 +- agents/chairman.py | 2 -- agents/idea_refiner.py | 18 ++++++++++++++++++ agents/sme.py | 4 ++-- gpt/gpt_client.py | 5 ++--- logger_config.py | 7 +++++++ main.py | 11 +++++++---- 7 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 agents/idea_refiner.py create mode 100644 logger_config.py diff --git a/agents/agent.py b/agents/agent.py index 0d4c0e1..903596a 100644 --- a/agents/agent.py +++ b/agents/agent.py @@ -6,7 +6,7 @@ Provide succinct, fact-based answers. Eliminate filler words and politeness. Concentrate on delivering actionable insights and concrete solutions. Avoid vague or generic statements. Stick to the topic at hand. -# If you response doesn't meet these standards, reply with the exact phrase 'no comment' +If your response doesn't meet these standards, reply with the exact phrase 'no comment' """ ) diff --git a/agents/chairman.py b/agents/chairman.py index ed8cd01..e195068 100644 --- a/agents/chairman.py +++ b/agents/chairman.py @@ -4,8 +4,6 @@ from agents.sme import SME -# logger.disable(__name__) - class Chairman(Agent): def __init__(self, name: str, SMEs: list[SME]): diff --git a/agents/idea_refiner.py b/agents/idea_refiner.py new file mode 100644 index 0000000..4effbb4 --- /dev/null +++ b/agents/idea_refiner.py @@ -0,0 +1,18 @@ +from loguru import logger + +from agents.agent import Agent +from agents.sme import SME + + +class IdeaRefiner(Agent): + def __init__(self, name: str): + # Construct the user_prompt string with details of the executives + + self.user_prompt = "You are going to presented with an topic for discussion at a meeting. Your task to think deeply and refine the topic presented and note obvious high level constraints and considerations. Your output will serve as an introduction to the meeting participants." + + + # Call the superclass constructor with the constructed user_prompt + super().__init__(name, self.user_prompt) + + def refine_idea(self, idea: str) -> str: + return self.query_gpt(idea) diff --git a/agents/sme.py b/agents/sme.py index bc6c795..b99dff7 100644 --- a/agents/sme.py +++ b/agents/sme.py @@ -8,8 +8,8 @@ def __init__(self, name: str, expertise: str, concerns: list[str]): f"Adopt the persona of the {name}.", f"Your expertise is {expertise}.", f"Your concerns are {', '.join(concerns)}.", - "You should aim to provide original technical insights that align with these areas " - "of expertise and concerns. Do not repeat points that have already been made.", + f"You will be shown a transacript of a meeting. You have been asked to speak by the meeting chairman. Specifically, provide insights on {', '.join(concerns)} based on the meeting transcript. " + "Do not repeat points that have already been made.", ] user_prompt = " ".join(user_prompt_list) diff --git a/gpt/gpt_client.py b/gpt/gpt_client.py index 4845bd1..8007ade 100644 --- a/gpt/gpt_client.py +++ b/gpt/gpt_client.py @@ -6,7 +6,6 @@ from dotenv import load_dotenv from loguru import logger -#logger.disable(__name__) # Load environment variables from .env file load_dotenv() @@ -20,7 +19,7 @@ class GPTClient: def __init__( - self, system_instructions: str, user_prompt: str, model: str = "gpt-4" + self, system_instructions: str, user_prompt: str, model: str = "gpt-3.5-turbo" ): self.system_instructions = system_instructions self.user_prompt = user_prompt @@ -42,7 +41,7 @@ def query(self, transcript: str) -> str: {"role": "user", "content": self.user_prompt}, {"role": "assistant", "content": transcript}, ] - logger.info(json.dumps(messages, indent=4)) + logger.info(json.dumps(messages, indent=4).replace("\\n", "\n")) response = openai.ChatCompletion.create( model=self.model, diff --git a/logger_config.py b/logger_config.py new file mode 100644 index 0000000..492f407 --- /dev/null +++ b/logger_config.py @@ -0,0 +1,7 @@ +from loguru import logger + +# Remove the default sink +logger.remove(0) + +# Add a new sink that logs to a file +logger.add("my_log_file.log") diff --git a/main.py b/main.py index c267a4f..1b9eab8 100644 --- a/main.py +++ b/main.py @@ -4,11 +4,12 @@ from loguru import logger from agents.chairman import Chairman +from agents.idea_refiner import IdeaRefiner from agents.sme import SME from utils.parse_config import parse_yaml_config from utils.print_with_wrap import print_with_wrap +import logger_config -logger.disable(__name__) # typical C-suite of executives DEFAULT_SME_DICT = ( @@ -79,15 +80,17 @@ def main(idea: tuple[str], config: Path = None): smes = [SME(**d) for d in sme_dict] chairman = Chairman("Chairman", smes) + refiner = IdeaRefiner("Refiner") + refinement = refiner.refine_idea(idea) - transcript = [f"We are here to discuss this idea:\n{idea}\n\n"] - - print(transcript) + transcript = [f"\nWe are here to discuss this idea:\n{idea}\n{refinement}\n"] + print_with_wrap(f"{transcript[0]}") while not chairman.decide_if_meeting_over(transcript): speaker: SME = chairman.decide_next_speaker(transcript) opinion = speaker.opinion(transcript) + print_with_wrap(f"\033[94m{speaker.name}\033[0m: {opinion}") if opinion.strip().rstrip(".") != 'no comment': transcript.append(opinion) From e3aad6316d65799b97bbd49fb00caaeba99bc93b Mon Sep 17 00:00:00 2001 From: "laurence.hook" Date: Sat, 21 Oct 2023 23:36:36 +0100 Subject: [PATCH 5/9] add handling for open ai rate limiter --- gpt/gpt_client.py | 61 ++++++++++++++++++++++++---------------- main.py | 12 +++++--- utils/print_with_wrap.py | 7 +++-- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/gpt/gpt_client.py b/gpt/gpt_client.py index 8007ade..8c593f0 100644 --- a/gpt/gpt_client.py +++ b/gpt/gpt_client.py @@ -6,7 +6,6 @@ from dotenv import load_dotenv from loguru import logger - # Load environment variables from .env file load_dotenv() api_key = os.getenv("openai.api_key") @@ -16,10 +15,13 @@ openai.api_key = api_key +GPT3 = "gpt-3.5-turbo" +GPT4 = "gpt-4" + class GPTClient: def __init__( - self, system_instructions: str, user_prompt: str, model: str = "gpt-3.5-turbo" + self, system_instructions: str, user_prompt: str, model: str = GPT4 ): self.system_instructions = system_instructions self.user_prompt = user_prompt @@ -35,25 +37,36 @@ def __init__( logger.info(f"Temperature: {self.temperature}") def query(self, transcript: str) -> str: - start_time = time() - messages = [ - {"role": "system", "content": self.system_instructions}, - {"role": "user", "content": self.user_prompt}, - {"role": "assistant", "content": transcript}, - ] - logger.info(json.dumps(messages, indent=4).replace("\\n", "\n")) - - response = openai.ChatCompletion.create( - model=self.model, - temperature=self.temperature, - messages=messages, - ) - - end_time = time() - elapsed_time = end_time - start_time - - # Log the time taken and token usage - logger.info(f"GPT query took {elapsed_time:.2f} seconds") - logger.info(f"Tokens used in the request: {response['usage']}") - - return response.choices[0].message.content.strip() + max_retries = 6 # Number of retries + retry_delay = 10 # Delay between retries in seconds + + for i in range(max_retries): + try: + start_time = time() + messages = [ + {"role": "system", "content": self.system_instructions}, + {"role": "user", "content": self.user_prompt}, + {"role": "assistant", "content": transcript}, + ] + logger.info(json.dumps(messages, indent=4).replace("\\n", "\n")) + + response = openai.ChatCompletion.create( + model=self.model, + temperature=self.temperature, + messages=messages, + ) + + end_time = time() + elapsed_time = end_time - start_time + + # Log the time taken and token usage + logger.info(f"GPT query took {elapsed_time:.2f} seconds") + logger.info(f"Tokens used in the request: {response['usage']}") + + return response.choices[0].message.content.strip() + except openai.error.RateLimitError as e: + logger.warning(f"Rate limit reached. Retrying in {retry_delay} seconds. Details: {e}") + time.sleep(retry_delay) + + logger.error(f"Max retries reached. Could not complete the GPT query.") + return "Rate limit reached. Could not complete the request." diff --git a/main.py b/main.py index 1b9eab8..e61b1c0 100644 --- a/main.py +++ b/main.py @@ -81,16 +81,20 @@ def main(idea: tuple[str], config: Path = None): chairman = Chairman("Chairman", smes) refiner = IdeaRefiner("Refiner") - refinement = refiner.refine_idea(idea) - transcript = [f"\nWe are here to discuss this idea:\n{idea}\n{refinement}\n"] - print_with_wrap(f"{transcript[0]}") + transcript = [] + transcript.append("") + transcript.append("We are here to discuss this idea:") + transcript.append(idea) + transcript.append(refiner.refine_idea((idea))) + + print_with_wrap("\n".join(transcript)) + while not chairman.decide_if_meeting_over(transcript): speaker: SME = chairman.decide_next_speaker(transcript) opinion = speaker.opinion(transcript) - print_with_wrap(f"\033[94m{speaker.name}\033[0m: {opinion}") if opinion.strip().rstrip(".") != 'no comment': transcript.append(opinion) diff --git a/utils/print_with_wrap.py b/utils/print_with_wrap.py index 78f64cc..00c3e69 100644 --- a/utils/print_with_wrap.py +++ b/utils/print_with_wrap.py @@ -2,5 +2,8 @@ def print_with_wrap(text: str, wrap_length: int = 180): - wrapped_text = textwrap.fill(text, wrap_length) - print(wrapped_text) + lines = text.split('\n') + for line in lines: + wrapped_text = textwrap.wrap(line, wrap_length) + for segment in wrapped_text: + print(segment) From a395812d09bf7752a1015dfd9c41d659a47a817e Mon Sep 17 00:00:00 2001 From: "laurence.hook" Date: Sat, 21 Oct 2023 23:41:15 +0100 Subject: [PATCH 6/9] add handling for open ai rate limiter --- gpt/gpt_client.py | 6 +++--- main.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gpt/gpt_client.py b/gpt/gpt_client.py index 8c593f0..3ee5947 100644 --- a/gpt/gpt_client.py +++ b/gpt/gpt_client.py @@ -1,6 +1,6 @@ import json import os -from time import time +import time import openai from dotenv import load_dotenv @@ -42,7 +42,7 @@ def query(self, transcript: str) -> str: for i in range(max_retries): try: - start_time = time() + start_time = time.time() messages = [ {"role": "system", "content": self.system_instructions}, {"role": "user", "content": self.user_prompt}, @@ -56,7 +56,7 @@ def query(self, transcript: str) -> str: messages=messages, ) - end_time = time() + end_time = time.time() elapsed_time = end_time - start_time # Log the time taken and token usage diff --git a/main.py b/main.py index e61b1c0..96b4423 100644 --- a/main.py +++ b/main.py @@ -83,10 +83,10 @@ def main(idea: tuple[str], config: Path = None): refiner = IdeaRefiner("Refiner") transcript = [] - transcript.append("") + transcript.append("\n") transcript.append("We are here to discuss this idea:") transcript.append(idea) - transcript.append(refiner.refine_idea((idea))) + transcript.append(f"\n{refiner.refine_idea((idea))}\n") print_with_wrap("\n".join(transcript)) @@ -96,6 +96,7 @@ def main(idea: tuple[str], config: Path = None): opinion = speaker.opinion(transcript) print_with_wrap(f"\033[94m{speaker.name}\033[0m: {opinion}") + print() if opinion.strip().rstrip(".") != 'no comment': transcript.append(opinion) From dfce35fac67c042abe2deaba55d2b04e7ab498fa Mon Sep 17 00:00:00 2001 From: "laurence.hook" Date: Sun, 22 Oct 2023 00:19:20 +0100 Subject: [PATCH 7/9] tweaks --- agents/agent.py | 5 +++-- constants.py | 1 + gpt/gpt_client.py | 4 ++-- main.py | 15 ++++++++------- 4 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 constants.py diff --git a/agents/agent.py b/agents/agent.py index 903596a..9ee7b5c 100644 --- a/agents/agent.py +++ b/agents/agent.py @@ -1,12 +1,13 @@ from textwrap import dedent +from constants import NO_COMMENT from gpt.gpt_client import GPTClient -DEFAULT_SYSTEM_PROMPT = dedent("""\ +DEFAULT_SYSTEM_PROMPT = dedent(f"""\ Provide succinct, fact-based answers. Eliminate filler words and politeness. Concentrate on delivering actionable insights and concrete solutions. Avoid vague or generic statements. Stick to the topic at hand. -If your response doesn't meet these standards, reply with the exact phrase 'no comment' +If your response doesn't meet these standards, reply with the exact words '{NO_COMMENT}' """ ) diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..420b0a0 --- /dev/null +++ b/constants.py @@ -0,0 +1 @@ +NO_COMMENT = "NO COMMENT" \ No newline at end of file diff --git a/gpt/gpt_client.py b/gpt/gpt_client.py index 3ee5947..fbbf853 100644 --- a/gpt/gpt_client.py +++ b/gpt/gpt_client.py @@ -27,7 +27,7 @@ def __init__( self.user_prompt = user_prompt self.model = model self.max_tokens = 100 - self.temperature: float = 0.6 + self.temperature: float = 0.1 # Log initial configuration on startup logger.info(f"Initializing GPTClient with the following configuration:") logger.info(f"System Instructions: {self.system_instructions}") @@ -69,4 +69,4 @@ def query(self, transcript: str) -> str: time.sleep(retry_delay) logger.error(f"Max retries reached. Could not complete the GPT query.") - return "Rate limit reached. Could not complete the request." + return "Error in GPT client that could not be resolved by retrying." diff --git a/main.py b/main.py index 96b4423..8dfd786 100644 --- a/main.py +++ b/main.py @@ -6,11 +6,11 @@ from agents.chairman import Chairman from agents.idea_refiner import IdeaRefiner from agents.sme import SME +from constants import NO_COMMENT from utils.parse_config import parse_yaml_config from utils.print_with_wrap import print_with_wrap import logger_config - # typical C-suite of executives DEFAULT_SME_DICT = ( { @@ -82,14 +82,15 @@ def main(idea: tuple[str], config: Path = None): chairman = Chairman("Chairman", smes) refiner = IdeaRefiner("Refiner") - transcript = [] - transcript.append("\n") - transcript.append("We are here to discuss this idea:") - transcript.append(idea) - transcript.append(f"\n{refiner.refine_idea((idea))}\n") + transcript = ["", ".", "We are here to discuss this idea:", idea, "."] print_with_wrap("\n".join(transcript)) + refined_idea = refiner.refine_idea(idea) + transcript.append(refined_idea) + print_with_wrap(refined_idea) + print() + while not chairman.decide_if_meeting_over(transcript): speaker: SME = chairman.decide_next_speaker(transcript) @@ -97,7 +98,7 @@ def main(idea: tuple[str], config: Path = None): print_with_wrap(f"\033[94m{speaker.name}\033[0m: {opinion}") print() - if opinion.strip().rstrip(".") != 'no comment': + if opinion.strip().rstrip(".").upper() != NO_COMMENT: transcript.append(opinion) From aed859ace8fc8a9077f84bbe42bf37dacf0d693c Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 22 Oct 2023 20:19:30 +0100 Subject: [PATCH 8/9] extract client from Agent --- agents/agent.py | 21 +++++++++--- agents/chairman.py | 28 ++++++++-------- agents/idea_refiner.py | 22 +++++++------ agents/sme.py | 33 +++++++++++++------ clients/__init__.py | 3 ++ clients/base.py | 7 ++++ clients/config.py | 7 ++++ clients/get_client.py | 16 +++++++++ {gpt => clients}/gpt_client.py | 59 ++++++++++++++++++++-------------- constants.py | 2 +- gpt/__init__.py | 0 logger_config.py | 7 ---- main.py | 31 ++++++++++++++---- requirements.txt | 3 +- utils/logging.py | 10 ++++++ utils/parse_config.py | 1 + utils/print_with_wrap.py | 2 +- 17 files changed, 172 insertions(+), 80 deletions(-) create mode 100644 clients/__init__.py create mode 100644 clients/base.py create mode 100644 clients/config.py create mode 100644 clients/get_client.py rename {gpt => clients}/gpt_client.py (56%) delete mode 100644 gpt/__init__.py delete mode 100644 logger_config.py create mode 100644 utils/logging.py diff --git a/agents/agent.py b/agents/agent.py index 9ee7b5c..b99a0a1 100644 --- a/agents/agent.py +++ b/agents/agent.py @@ -1,9 +1,10 @@ from textwrap import dedent +from clients.base import AIClient from constants import NO_COMMENT -from gpt.gpt_client import GPTClient -DEFAULT_SYSTEM_PROMPT = dedent(f"""\ +DEFAULT_SYSTEM_PROMPT = dedent( + f"""\ Provide succinct, fact-based answers. Eliminate filler words and politeness. Concentrate on delivering actionable insights and concrete solutions. Avoid vague or generic statements. Stick to the topic at hand. @@ -11,10 +12,20 @@ """ ) + class Agent: - def __init__(self, name: str, user_prompt: str, system_prompt: str = DEFAULT_SYSTEM_PROMPT): + def __init__( + self, + client: AIClient, + name: str, + user_prompt: str, + system_prompt: str = DEFAULT_SYSTEM_PROMPT, + ): self.name = name - self.gpt_client = GPTClient(system_prompt, user_prompt) + + self.client = client + self.client.common_instructions = system_prompt + self.client.user_prompt = user_prompt def query_gpt(self, transcript: str) -> str: - return self.gpt_client.query(transcript) + return self.client.query(transcript) diff --git a/agents/chairman.py b/agents/chairman.py index e195068..fca74b4 100644 --- a/agents/chairman.py +++ b/agents/chairman.py @@ -2,34 +2,32 @@ from agents.agent import Agent from agents.sme import SME - +from clients.base import AIClient class Chairman(Agent): - def __init__(self, name: str, SMEs: list[SME]): + def __init__(self, client: AIClient, executives: list[SME], name: str = "Chairman"): # Construct the user_prompt string with details of the executives + self.user_prompt = self.update_user_prompt(executives) - self.user_prompt = self.update_user_prompt(SMEs) - - system_prompt = ( - f"Answer with only the name and nothing else." - ) + system_prompt = f"Answer with only the name and nothing else." # Call the superclass constructor with the constructed user_prompt - super().__init__(name, self.user_prompt, system_prompt) + super().__init__(client, name, self.user_prompt, system_prompt) - self.SMEs = SMEs + self.executives = executives def update_user_prompt(self, SMEs: list[SME]) -> str: frequency_info_list = [] - for SME in SMEs: + for sme in SMEs: frequency_info_list.append( - f"{SME.name}: expertise: {SME.expertise}. " - f"concerns: {', '.join(SME.concerns)}. spoken count: {SME.spoken_count}.\n" + f"{sme.name}: expertise: {sme.expertise}. " + f"concerns: {', '.join(sme.concerns)}. spoken count: {sme.spoken_count}.\n" ) return ( - f"Your task is to read the transcript and decide who should speak next. Do not choose the same person all of the time.\n" + f"Your task is to read the transcript and decide who should speak next. " + f"Do not choose the same person all of the time.\n" f"Participants:\n{''.join(frequency_info_list)} " ) @@ -44,12 +42,12 @@ def decide_next_speaker(self, transcript_list: list[str]) -> SME: logger.info(f"Chairman called speaker: {next_speaker}") next_executive = next( - (exec for exec in self.SMEs if exec.name == next_speaker), None + (exec for exec in self.executives if exec.name == next_speaker), None ) if next_executive is not None: next_executive.spoken_count += 1 # Update the frequency count - self.user_prompt = self.update_user_prompt(self.SMEs) + self.user_prompt = self.update_user_prompt(self.executives) self.gpt_client.user_prompt = self.user_prompt return next_executive diff --git a/agents/idea_refiner.py b/agents/idea_refiner.py index 4effbb4..09b2605 100644 --- a/agents/idea_refiner.py +++ b/agents/idea_refiner.py @@ -1,18 +1,22 @@ -from loguru import logger +from textwrap import dedent from agents.agent import Agent -from agents.sme import SME +from clients import AIClient - -class IdeaRefiner(Agent): - def __init__(self, name: str): - # Construct the user_prompt string with details of the executives - - self.user_prompt = "You are going to presented with an topic for discussion at a meeting. Your task to think deeply and refine the topic presented and note obvious high level constraints and considerations. Your output will serve as an introduction to the meeting participants." +REFINER_PROMPT = dedent( + """\ + You are going to presented with an topic for discussion at a meeting. + Your task to think deeply and refine the topic presented and note obvious + high level constraints and considerations. + Your output will serve as an introduction to the meeting participants. + """ +) +class IdeaRefiner(Agent): + def __init__(self, client: AIClient, name: str = "Refiner"): # Call the superclass constructor with the constructed user_prompt - super().__init__(name, self.user_prompt) + super().__init__(client, name, REFINER_PROMPT) def refine_idea(self, idea: str) -> str: return self.query_gpt(idea) diff --git a/agents/sme.py b/agents/sme.py index b99dff7..4132b93 100644 --- a/agents/sme.py +++ b/agents/sme.py @@ -1,20 +1,33 @@ +from textwrap import dedent + from agents.agent import Agent +from clients.base import AIClient + +USER_PROMPT_TEMPLATE = dedent( + """\ + Adopt the persona of the {name}.\n + Your expertise is {expertise}.\n + Your concerns are {concerns}.\n + You will be shown a transcript of a meeting. + You have been asked to speak by the meeting chairman. + Specifically, provide insights on {concerns} + based on the meeting transcript.\n + Do not repeat points that have already been made + """ +) class SME(Agent): - def __init__(self, name: str, expertise: str, concerns: list[str]): + def __init__( + self, client: AIClient, name: str, expertise: str, concerns: list[str] + ): # Construct the user_prompt string - user_prompt_list = [ - f"Adopt the persona of the {name}.", - f"Your expertise is {expertise}.", - f"Your concerns are {', '.join(concerns)}.", - f"You will be shown a transacript of a meeting. You have been asked to speak by the meeting chairman. Specifically, provide insights on {', '.join(concerns)} based on the meeting transcript. " - "Do not repeat points that have already been made.", - ] - user_prompt = " ".join(user_prompt_list) + user_prompt = USER_PROMPT_TEMPLATE.format( + name=name, expertise=expertise, concerns=", ".join(concerns) + ) # Call the superclass constructor with the constructed user_prompt - super().__init__(name, user_prompt) + super().__init__(client, name, user_prompt) self.expertise = expertise self.concerns = concerns self.spoken_count = 0 diff --git a/clients/__init__.py b/clients/__init__.py new file mode 100644 index 0000000..d86cd49 --- /dev/null +++ b/clients/__init__.py @@ -0,0 +1,3 @@ +from .base import AIClient +from .config import AIClientConfig +from .get_client import AIClientType, GPTClient, get_ai_client diff --git a/clients/base.py b/clients/base.py new file mode 100644 index 0000000..8cc094b --- /dev/null +++ b/clients/base.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class AIClient(ABC): + @abstractmethod + def query(self, transcript: str): + pass diff --git a/clients/config.py b/clients/config.py new file mode 100644 index 0000000..beb6193 --- /dev/null +++ b/clients/config.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class AIClientConfig: + api_key: str + model: str | None diff --git a/clients/get_client.py b/clients/get_client.py new file mode 100644 index 0000000..1a28d27 --- /dev/null +++ b/clients/get_client.py @@ -0,0 +1,16 @@ +from enum import Enum + +from clients.base import AIClient +from clients.config import AIClientConfig +from clients.gpt_client import GPTClient + + +class AIClientType(str, Enum): + ChatGPT = "ChatGPT" + + +def get_ai_client(client_type: AIClientType, config: AIClientConfig) -> AIClient: + if client_type == AIClientType.ChatGPT: + return GPTClient(config.api_key) + else: + raise ValueError(f"Unknown AI client type: {client_type}") diff --git a/gpt/gpt_client.py b/clients/gpt_client.py similarity index 56% rename from gpt/gpt_client.py rename to clients/gpt_client.py index fbbf853..8bb49e5 100644 --- a/gpt/gpt_client.py +++ b/clients/gpt_client.py @@ -1,51 +1,61 @@ import json -import os import time +from enum import Enum import openai -from dotenv import load_dotenv from loguru import logger -# Load environment variables from .env file -load_dotenv() -api_key = os.getenv("openai.api_key") +from .base import AIClient -if not api_key: - raise ValueError("API key not found in environment variables") -openai.api_key = api_key +class Models(str, Enum): + GPT3 = "gpt-3.5-turbo" + GPT4 = "gpt-4" -GPT3 = "gpt-3.5-turbo" -GPT4 = "gpt-4" - -class GPTClient: - def __init__( - self, system_instructions: str, user_prompt: str, model: str = GPT4 - ): - self.system_instructions = system_instructions - self.user_prompt = user_prompt +class GPTClient(AIClient): + def __init__(self, api_key: str, model: str = Models.GPT4.value): + openai.api_key = api_key + self._system_instructions = None + self._user_prompt = None self.model = model self.max_tokens = 100 self.temperature: float = 0.1 # Log initial configuration on startup logger.info(f"Initializing GPTClient with the following configuration:") - logger.info(f"System Instructions: {self.system_instructions}") - logger.info(f"User Prompt: {self.user_prompt}") logger.info(f"Model: {self.model}") logger.info(f"Max Tokens: {self.max_tokens}") logger.info(f"Temperature: {self.temperature}") + @property + def system_instructions(self): + return self._system_instructions + + @system_instructions.setter + def system_instructions(self, value): + logger.debug(f"Setting system instructions: {self._system_instructions}") + self._system_instructions = value + + @property + def user_prompt(self): + return self._user_prompt + + @user_prompt.setter + def user_prompt(self, value): + logger.debug(f"Setting user prompt: {self._user_prompt}") + self._user_prompt = value + def query(self, transcript: str) -> str: max_retries = 6 # Number of retries retry_delay = 10 # Delay between retries in seconds + # TODO: use backoff decorator for i in range(max_retries): try: start_time = time.time() messages = [ - {"role": "system", "content": self.system_instructions}, - {"role": "user", "content": self.user_prompt}, + {"role": "system", "content": self._system_instructions}, + {"role": "user", "content": self._user_prompt}, {"role": "assistant", "content": transcript}, ] logger.info(json.dumps(messages, indent=4).replace("\\n", "\n")) @@ -56,8 +66,7 @@ def query(self, transcript: str) -> str: messages=messages, ) - end_time = time.time() - elapsed_time = end_time - start_time + elapsed_time = time.time() - start_time # Log the time taken and token usage logger.info(f"GPT query took {elapsed_time:.2f} seconds") @@ -65,7 +74,9 @@ def query(self, transcript: str) -> str: return response.choices[0].message.content.strip() except openai.error.RateLimitError as e: - logger.warning(f"Rate limit reached. Retrying in {retry_delay} seconds. Details: {e}") + logger.warning( + f"Rate limit reached. Retrying in {retry_delay} seconds. Details: {e}" + ) time.sleep(retry_delay) logger.error(f"Max retries reached. Could not complete the GPT query.") diff --git a/constants.py b/constants.py index 420b0a0..15eec53 100644 --- a/constants.py +++ b/constants.py @@ -1 +1 @@ -NO_COMMENT = "NO COMMENT" \ No newline at end of file +NO_COMMENT = "NO COMMENT" diff --git a/gpt/__init__.py b/gpt/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/logger_config.py b/logger_config.py deleted file mode 100644 index 492f407..0000000 --- a/logger_config.py +++ /dev/null @@ -1,7 +0,0 @@ -from loguru import logger - -# Remove the default sink -logger.remove(0) - -# Add a new sink that logs to a file -logger.add("my_log_file.log") diff --git a/main.py b/main.py index 8dfd786..cb6d412 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,19 @@ +import os from pathlib import Path import click -from loguru import logger +from dotenv import load_dotenv from agents.chairman import Chairman from agents.idea_refiner import IdeaRefiner from agents.sme import SME +from clients import AIClientConfig, AIClientType, get_ai_client from constants import NO_COMMENT +from utils.logging import configure_logging from utils.parse_config import parse_yaml_config from utils.print_with_wrap import print_with_wrap -import logger_config + +load_dotenv() # typical C-suite of executives DEFAULT_SME_DICT = ( @@ -71,18 +75,31 @@ default=None, help="yaml file with team personalities details", ) -def main(idea: tuple[str], config: Path = None): +@click.option("-v", "--verbose", default=1, count=True) +def main(idea: str, config: Path = None, verbose: int = 1): + configure_logging(verbose) + load_dotenv() + client = get_ai_client( + AIClientType.ChatGPT, AIClientConfig(api_key=os.getenv("openai.api_key")) + ) if config: sme_dict = parse_yaml_config(config) else: sme_dict = DEFAULT_SME_DICT - smes = [SME(**d) for d in sme_dict] + smes = [SME(client=client, **d) for d in sme_dict] + + chairman = Chairman(client, smes) - chairman = Chairman("Chairman", smes) - refiner = IdeaRefiner("Refiner") + refiner = IdeaRefiner(client, "Refiner") - transcript = ["", ".", "We are here to discuss this idea:", idea, "."] + transcript = [ + "", + ".", + "We are here to discuss this idea:", + idea, + ".", + ] print_with_wrap("\n".join(transcript)) diff --git a/requirements.txt b/requirements.txt index b163a96..41b6544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ python-dotenv~=1.0.0 black~=23.10.0 click~=8.1.7 loguru~=0.7.2 -PyYAML~=6.0.1 \ No newline at end of file +PyYAML~=6.0.1 +isort~=5.12.0 \ No newline at end of file diff --git a/utils/logging.py b/utils/logging.py new file mode 100644 index 0000000..9ccf003 --- /dev/null +++ b/utils/logging.py @@ -0,0 +1,10 @@ +import sys + +from loguru import logger + + +def configure_logging(verbose: int): + logging_levels = {0: "ERROR", 1: "INFO", 2: "DEBUG"} + logger.remove(0) + logger.add(sys.stdout, level=logging_levels.get(verbose)) + logger.add("dream_team_gpt.log", level="DEBUG") diff --git a/utils/parse_config.py b/utils/parse_config.py index ec81cac..bdffc9c 100644 --- a/utils/parse_config.py +++ b/utils/parse_config.py @@ -1,4 +1,5 @@ from pathlib import Path + import yaml from loguru import logger diff --git a/utils/print_with_wrap.py b/utils/print_with_wrap.py index 00c3e69..7adfc51 100644 --- a/utils/print_with_wrap.py +++ b/utils/print_with_wrap.py @@ -2,7 +2,7 @@ def print_with_wrap(text: str, wrap_length: int = 180): - lines = text.split('\n') + lines = text.split("\n") for line in lines: wrapped_text = textwrap.wrap(line, wrap_length) for segment in wrapped_text: From ab7e11306523eb4ad055cdc50b503f490c8b9253 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 22 Oct 2023 20:23:50 +0100 Subject: [PATCH 9/9] introduce constants module --- constants/__init__.py | 2 ++ constants/default_sme.py | 42 ++++++++++++++++++++++++ constants.py => constants/strings.py | 0 main.py | 48 +--------------------------- 4 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 constants/__init__.py create mode 100644 constants/default_sme.py rename constants.py => constants/strings.py (100%) diff --git a/constants/__init__.py b/constants/__init__.py new file mode 100644 index 0000000..488fc76 --- /dev/null +++ b/constants/__init__.py @@ -0,0 +1,2 @@ +from .default_sme import DEFAULT_SME_DICT +from .strings import NO_COMMENT diff --git a/constants/default_sme.py b/constants/default_sme.py new file mode 100644 index 0000000..20f80a6 --- /dev/null +++ b/constants/default_sme.py @@ -0,0 +1,42 @@ +DEFAULT_SME_DICT = ( + { + "name": "CEO", + "expertise": "Corporate Strategy", + "concerns": ["Market Entry", "Competitive Positioning"], + }, + { + "name": "CFO", + "expertise": "Financial Products", + "concerns": ["Rate Management", "Regulatory Compliance"], + }, + { + "name": "COO", + "expertise": "Operational Efficiency", + "concerns": ["Scalability", "Cost Optimization"], + }, + { + "name": "CMO", + "expertise": "Customer Acquisition", + "concerns": ["Target Market", "Onboarding Experience"], + }, + { + "name": "CTO", + "expertise": "Technical Infrastructure", + "concerns": ["Data Security", "System Integration"], + }, + { + "name": "CRO", + "expertise": "Risk Management", + "concerns": ["Fraud Detection", "Compliance"], + }, + { + "name": "CCO", + "expertise": "Customer Experience", + "concerns": ["UX/UI Design", "Customer Support"], + }, + { + "name": "CPO", + "expertise": "Product Management", + "concerns": ["Feature Rollout", "Customer Feedback"], + }, +) diff --git a/constants.py b/constants/strings.py similarity index 100% rename from constants.py rename to constants/strings.py diff --git a/main.py b/main.py index cb6d412..d3ee432 100644 --- a/main.py +++ b/main.py @@ -8,57 +8,11 @@ from agents.idea_refiner import IdeaRefiner from agents.sme import SME from clients import AIClientConfig, AIClientType, get_ai_client -from constants import NO_COMMENT +from constants import DEFAULT_SME_DICT, NO_COMMENT from utils.logging import configure_logging from utils.parse_config import parse_yaml_config from utils.print_with_wrap import print_with_wrap -load_dotenv() - -# typical C-suite of executives -DEFAULT_SME_DICT = ( - { - "name": "CEO", - "expertise": "Corporate Strategy", - "concerns": ["Market Entry", "Competitive Positioning"], - }, - { - "name": "CFO", - "expertise": "Financial Products", - "concerns": ["Rate Management", "Regulatory Compliance"], - }, - { - "name": "COO", - "expertise": "Operational Efficiency", - "concerns": ["Scalability", "Cost Optimization"], - }, - { - "name": "CMO", - "expertise": "Customer Acquisition", - "concerns": ["Target Market", "Onboarding Experience"], - }, - { - "name": "CTO", - "expertise": "Technical Infrastructure", - "concerns": ["Data Security", "System Integration"], - }, - { - "name": "CRO", - "expertise": "Risk Management", - "concerns": ["Fraud Detection", "Compliance"], - }, - { - "name": "CCO", - "expertise": "Customer Experience", - "concerns": ["UX/UI Design", "Customer Support"], - }, - { - "name": "CPO", - "expertise": "Product Management", - "concerns": ["Feature Rollout", "Customer Feedback"], - }, -) - @click.command() @click.option(