Skip to content

Commit

Permalink
af
Browse files Browse the repository at this point in the history
  • Loading branch information
samholt committed Apr 17, 2024
1 parent 22c04d9 commit 70f7789
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 65 deletions.
8 changes: 5 additions & 3 deletions l2mac/l2mac.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ def get_llm_response(self, messages, max_tokens=None, tool_choice='auto'):
# self.message_hash_same_increase_temperature = 0
# self.message_hash = message_hash
llm_config['tools'] = self.functions
if tool_choice is not None:
if tool_choice == 'auto':
llm_config['tool_choice'] = 'auto'
elif tool_choice is not None:
llm_config['tool_choice'] = {"type": "function", "function": {"name": tool_choice}}
else:
llm_config['tool_choice'] = 'none'
Expand Down Expand Up @@ -318,8 +320,8 @@ def _run(self, steps: int = 10):
"""})
response_message = self.get_llm_response(self.sub_messages)
self.sub_messages.append(response_message)
if response_message.get("function_call"):
function_return_message, self.file_dict = process_function_call_and_return_message(response_message["function_call"], self.file_dict, functions=self.functions)
if response_message.get('tool_calls'):
function_return_messages, self.file_dict = process_function_call_and_return_message(response_message["tool_calls"], self.file_dict, tools=self.functions)
if 'status' in json.loads(function_return_message['content']) and json.loads(function_return_message['content'])['status'] == 'TASK_STEP_COMPLETE':
task_step_complete = True
self.sub_messages.append({"role": "user", "content": f"""
Expand Down
3 changes: 2 additions & 1 deletion l2mac/llm_providers/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,5 +246,6 @@ def chat_completion_rl_inner(**kwargs):
response = client.chat.completions.create(**kwargs)
# if logger:
# logger.info(f"[{name}][OpenAI API Returned] Elapsed request time: {perf_counter() - t0}s | response: {response}")
response = json.loads(response.model_dump_json())
response = json.loads(response.model_dump_json()) # Convert to dict, easier to save as a replay buffer for debugging
response = {k:v for k,v in response.items() if v is not None} # OpenAI-API expects none objects to be removed
return response
132 changes: 72 additions & 60 deletions l2mac/tools/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,74 +180,86 @@ def process_functions_into_function_names(tools: List[dict] = []):
return function_names


def process_function_call_and_return_message(message_function_call: dict, file_dict: dict, logger=None, tools=[], enable_tests=True):
def process_function_call_and_return_message(tool_calls: dict, file_dict: dict, logger=None, tools=[], enable_tests=True):
function_name = ''
if len(tools) >= 1:
functions_available_keys = process_functions_into_function_names(tools)
else:
functions_available_keys = list(available_functions_factory().keys())
try:
functions_available = available_functions_factory()
function_name = message_function_call["name"]
if function_name not in functions_available_keys:
json_fun_content = json.dumps({'status': 'error', 'message': f'Function `{function_name}` not found. Please only use the functions listed given, which are: {functions_available_keys}'})
functions_available = available_functions_factory()
return_messages = []
for tool_call in tool_calls:
try:
function_name = tool_call['function']['name']
if function_name not in functions_available_keys:
json_fun_content = json.dumps({'status': 'error', 'message': f'Function `{function_name}` not found. Please only use the functions listed given, which are: {functions_available_keys}'})
function_return_message = {
"tool_call_id": tool_call['id'],
"role": "tool",
"name": function_name,
"content": json_fun_content,
}
return_messages.append(function_return_message)
continue
fuction_to_call = functions_available[function_name]
function_args = json.loads(tool_call['function']["arguments"])
function_args['file_dict'] = file_dict
function_args['enable_tests'] = enable_tests
if (function_name == 'write_files') or (function_name == 'delete_files'):
function_response, file_dict = fuction_to_call(**function_args)
else:
function_response = fuction_to_call(**function_args)
function_return_message = {
"role": "function",
"tool_call_id": tool_call['id'],
"role": "tool",
"name": function_name,
"content": function_response,
}
return_messages.append(function_return_message)
except KeyError as e:
if logger:
logger.error(f'Error in process_function_call_and_return_message()')
logger.error(e)
logger.error(traceback.format_exc())
logger.error(f'process_function_call_and_return_message({tool_call},{file_dict})')
error_message = "".join(traceback.format_exception_only(type(e), e))
json_fun_content = json.dumps({'status': 'error', 'message': f'Error Decoding Message: {error_message}'})
function_return_message = {
"tool_call_id": tool_call['id'],
"role": "tool",
"name": function_name,
"content": json_fun_content,
}
return function_return_message, file_dict
fuction_to_call = functions_available[function_name]
function_args = json.loads(message_function_call["arguments"])
function_args['file_dict'] = file_dict
function_args['enable_tests'] = enable_tests
if (function_name == 'write_files') or (function_name == 'delete_files'):
function_response, file_dict = fuction_to_call(**function_args)
else:
function_response = fuction_to_call(**function_args)
function_return_message = {
"role": "function",
return_messages.append(function_return_message)
except json.decoder.JSONDecodeError as e:
if logger:
logger.error(f'Error in process_function_call_and_return_message()')
logger.error(e)
logger.error(traceback.format_exc())
logger.error(f'process_function_call_and_return_message({tool_call},{file_dict})')
error_message = "".join(traceback.format_exception_only(type(e), e))
error_content = f'Response was too long and was cut off. Please give a shorter response! As the response was cut off early, there was a error decoding the response: {error_message}'
json_fun_content = json.dumps({'status': 'error', 'message': error_content})
function_return_message = {
"tool_call_id": tool_call['id'],
"role": "tool",
"name": function_name,
"content": function_response,
"content": json_fun_content,
}
return_messages.append(function_return_message)
except Exception as e:
if logger:
logger.error(f'Error in process_function_call_and_return_message()')
logger.error(e)
logger.error(traceback.format_exc())
logger.error(f'process_function_call_and_return_message({tool_call},{file_dict})')
error_message = "".join(traceback.format_exception_only(type(e), e))
json_fun_content = json.dumps({'status': 'error', 'message': f'Error running function: {error_message}'})
function_return_message = {
"tool_call_id": tool_call['id'],
"role": "tool",
"name": function_name,
"content": json_fun_content,
}
except KeyError as e:
if logger:
logger.error(f'Error in process_function_call_and_return_message()')
logger.error(e)
logger.error(traceback.format_exc())
logger.error(f'process_function_call_and_return_message({message_function_call},{file_dict})')
error_message = "".join(traceback.format_exception_only(type(e), e))
json_fun_content = json.dumps({'status': 'error', 'message': f'Error Decoding Message: {error_message}'})
function_return_message = {
"role": "function",
"name": function_name,
"content": json_fun_content,
}
except json.decoder.JSONDecodeError as e:
if logger:
logger.error(f'Error in process_function_call_and_return_message()')
logger.error(e)
logger.error(traceback.format_exc())
logger.error(f'process_function_call_and_return_message({message_function_call},{file_dict})')
error_message = "".join(traceback.format_exception_only(type(e), e))
error_content = f'Response was too long and was cut off. Please give a shorter response! As the response was cut off early, there was a error decoding the response: {error_message}'
json_fun_content = json.dumps({'status': 'error', 'message': error_content})
function_return_message = {
"role": "function",
"name": function_name,
"content": json_fun_content,
}
except Exception as e:
if logger:
logger.error(f'Error in process_function_call_and_return_message()')
logger.error(e)
logger.error(traceback.format_exc())
logger.error(f'process_function_call_and_return_message({message_function_call},{file_dict})')
error_message = "".join(traceback.format_exception_only(type(e), e))
json_fun_content = json.dumps({'status': 'error', 'message': f'Error running function: {error_message}'})
function_return_message = {
"role": "function",
"name": function_name,
"content": json_fun_content,
}
return function_return_message, file_dict
return_messages.append(function_return_message)
return return_messages, file_dict
59 changes: 58 additions & 1 deletion l2mac/tools/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,61 @@ def delete_files(files: List[str], file_dict: dict, enable_tests=True):
"status": "success",
"message": "delete_files completed successfully."
}
return json.dumps(output), file_dict
return json.dumps(output), file_dict


# Write unit tests for the functions above
def test_implement_git_diff_on_file_dict():
file_dict = {}
change_files_and_contents = [{'file_path': 'requirements.txt', 'file_contents': 'pytest==7.1.2\nflask==2.1.2\n'}, 'file_path:', 'blackjack/card.py', 'file_contents:', '"""Defines the Card class for the Blackjack game."""\n\n\nclass Card:\n\t"""Represents a single playing card."""\n\n\tdef __init__(self, suit, value):\n\t\t"""Initialize a new card.\n\n\t\tArgs:\n\t\t\tsuit (str): The suit of the card (e.g., \'Hearts\', \'Diamonds\', \'Clubs\', \'Spades\').\n\t\t\tvalue (str): The value of the card (e.g., \'2\', \'3\', \'4\', \'5\', \'6\', \'7\', \'8\', \'9\', \'10\', \'Jack\', \'Queen\', \'King\', \'Ace\').\n\t\t"""\n\t\tself.suit = suit\n\t\tself.value = value\n\n\tdef __repr__(self):\n\t\treturn f"{self.value} of {self.suit}"', 'file_path:', 'blackjack/deck.py', 'file_contents:', '"""Defines the Deck class for the Blackjack game."""\n\nfrom .card import Card\nimport random\n\nclass Deck:\n\t"""Represents a deck of playing cards."""\n\n\tdef __init__(self):\n\t\t"""Initialize a new deck of cards."""\n\t\tself.cards = [Card(suit, value) for suit in [\'Hearts\', \'Diamonds\', \'Clubs\', \'Spades\'] for value in [\'2\', \'3\', \'4\', \'5\', \'6\', \'7\', \'8\', \'9\', \'10\', \'Jack\', \'Queen\', \'King\', \'Ace\']]\n\t\trandom.shuffle(self.cards)\n\n\tdef draw_card(self):\n\t\t"""Draw a card from the deck."""\n\t\treturn self.cards.pop()', 'file_path:', 'blackjack/hand.py', 'file_contents:', '"""Defines the Hand class for the Blackjack game."""\n\n\nclass Hand:\n\t"""Represents a player\'s or dealer\'s hand in the game."""\n\n\tdef __init__(self):\n\t\t"""Initialize a new hand with no cards."""\n\t\tself.cards = []\n\t\tself.value = 0\n\t\tself.aces = 0\n\n\tdef add_card(self, card):\n\t\t"""Add a card to the hand and adjust the score."""\n\t\tself.cards.append(card)\n\t\tif card.value == \'Ace\':\n\t\t\tself.aces += 1\n\t\tself.adjust_for_ace()\n\n\tdef adjust_for_ace(self):\n\t\t"""Adjust the hand\'s value if an ace is present."""\n\t\twhile self.value > 21 and self.aces:\n\t\t\tself.value -= 10\n\t\t\tself.aces -= 1', 'file_path:', 'blackjack/game.py', 'file_contents:', '"""Defines the Game class for the Blackjack game."""\n\nfrom .deck import Deck\nfrom .hand import Hand\n\n\nclass Game:\n\t"""Manages the flow and rules of a Blackjack game."""\n\n\tdef __init__(self):\n\t\t"""Initialize the game with a deck and two hands."""\n\t\tself.deck = Deck()\n\t\tself.player_hand = Hand()\n\t\tself.dealer_hand = Hand()\n\n\tdef deal_cards(self):\n\t\t"""Deal two cards to both player and dealer."""\n\t\tfor _ in range(2):\n\t\t\tself.player_hand.add_card(self.deck.draw_card())\n\t\t\tself.dealer_hand.add_card(self.deck.draw_card())\n\n\tdef player_hit(self):\n\t\t"""Handle player hitting."""\n\t\tself.player_hand.add_card(self.deck.draw_card())\n\t\tif self.player_hand.value > 21:\n\t\t\treturn \'Bust\'\n\t\telse:\n\t\t\treturn \'Continue\'\n\n\tdef player_stand(self):\n\t\t"""Handle player standing."""\n\t\t# Game logic for when player stands\n\n\tdef compare_hands(self):\n\t\t"""Compare the player\'s and dealer\'s hands to determine the winner."""\n\t\t# Logic to determine the winner', 'file_path:', 'blackjack/main.py', 'file_contents:', '"""Main script for the CLI Blackjack game."""\n\nfrom .game import Game\n\n\ndef main():\n\t"""Set up and run the Blackjack game."""\n\tgame = Game()\n\t# Game setup and loop logic here\n\nif __name__ == \'__main__\':\n\tmain()', 'file_path:', 'tests/test_card.py', 'file_contents:', '"""Unit tests for the Card class in the Blackjack game."""\n\nimport pytest\nfrom blackjack.card import Card\n\n\ndef test_card_initialization():\n\t"""Test that a card is initialized with the correct suit and value."""\n\tcard = Card(\'Hearts\', \'2\')\n\tassert card.suit == \'Hearts\'\n\tassert card.value == \'2\'', 'file_path:', 'tests/test_deck.py', 'file_contents:', '"""Unit tests for the Deck class in the Blackjack game."""\n\nimport pytest\nfrom blackjack.deck import Deck\n\n\ndef test_deck_initialization():\n\t"""Test that a deck is correctly initialized with 52 cards."""\n\tdeck = Deck()\n\tassert len(deck.cards) == 52\n\n\ndef test_draw_card():\n\t"""Test that drawing a card from the deck reduces its size by one."""\n\tdeck = Deck()\n\tdeck.draw_card()\n\tassert len(deck.cards) == 51', 'file_path:', 'tests/test_hand.py', 'file_contents:', '"""Unit tests for the Hand class in the Blackjack game."""\n\nimport pytest\nfrom blackjack.hand import Hand\nfrom blackjack.card import Card\n\n\ndef test_hand_initialization():\n\t"""Test that a hand is correctly initialized with no cards."""\n\thand = Hand()\n\tassert len(hand.cards) == 0\n\n\ndef test_add_card():\n\t"""Test that adding a card to the hand works correctly."""\n\thand = Hand()\n\thand.add_card(Card(\'Hearts\', \'2\'))\n\tassert len(hand.cards) == 1\n\tassert hand.cards[0].value == \'2\'', 'file_path:', 'tests/test_game.py', 'file_contents:', '"""Unit tests for the Game class in the Blackjack game."""\n\nimport pytest\nfrom blackjack.game import Game\n\n\ndef test_game_initialization():\n\t"""Test that the game is initialized with a deck and two hands."""\n\tgame = Game()\n\tassert game.deck\n\tassert game.player_hand\n\tassert game.dealer_hand\n\n\ndef test_deal_cards():\n\t"""Test that dealing cards works correctly."""\n\tgame = Game()\n\tgame.deal_cards()\n\tassert len(game.player_hand.cards) == 2\n\tassert len(game.dealer_hand.cards) == 2', 'file_path:', 'tests/conftest.py', 'file_contents:', '"""Configuration file for pytest in the Blackjack game project."""\n\nimport pytest\n\n# Configuration and fixtures for pytest can be added here.']
new_file_dict = implement_git_diff_on_file_dict(file_dict, change_files_and_contents)
assert new_file_dict == {'test.py': ['import time', 'import os']}
print('All tests passed.')


def test_write_files():
# Test write_files
files_and_contents = [
{
"file_path": "test.py",
"file_contents": "+ 1: import time\n+ 2: import os"
}
]
file_dict = {}
output, file_dict = write_files(files_and_contents, file_dict)
assert output == '{"write_files_status": "success", "message": "All tests passed."}'
assert file_dict == {'test.py': ['import time', 'import os']}
# Test implement_git_diff_on_file_dict
file_dict = {}
change_files_and_contents = [
{
"file_path": "test.py",
"file_contents": "+ 1: import time\n+ 2: import os"
}
]
file_dict = implement_git_diff_on_file_dict(file_dict, change_files_and_contents)
assert file_dict == {'test.py': ['import time', 'import os']}
# Test update_file_contents
existing_file_contents = "import time\nimport os"
change_file_contents = "+ 1: import time\n+ 2: import os"
new_file_contents = update_file_contents(existing_file_contents, change_file_contents)
assert new_file_contents == ['import time', 'import os']
# Test delete_files
files = ['test.py']
file_dict = {'test.py': ['import time', 'import os']}
output, file_dict = delete_files(files, file_dict)
assert output == '{"status": "success", "message": "delete_files completed successfully."}'
assert file_dict == {}
# Test delete_files with -1
files = ['-1']
file_dict = {'test.py': ['import time', 'import os']}
output, file_dict = delete_files(files, file_dict)
assert output == '{"status": "success", "message": "delete_files completed successfully."}'
assert file_dict == {}
# Test delete_files with file not in file_dict
files = ['test.py']
file_dict = {}
output, file_dict = delete_files(files, file_dict)
assert output == '{"status": "success", "message": "delete_files completed successfully."}'
assert file_dict == {}
print('All tests passed.')
2 changes: 2 additions & 0 deletions tests/test_function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def run_conversation():
available_functions = {
"get_current_weather": get_current_weather,
} # only one function in this example, but you can have multiple
response_message = json.loads(response_message.model_dump_json())
response_message = {k:v for k,v in response_message.items() if v is not None}
messages.append(response_message) # extend conversation with assistant's reply
# Step 4: send the info for each function call and function response to the model
for tool_call in tool_calls:
Expand Down

0 comments on commit 70f7789

Please sign in to comment.