diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a4ba6d..ba3308e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,4 +45,4 @@ jobs: run: | pip install pytest cd pilot - PYTHONPATH=. pytest -m "not slow and not uses_tokens" + PYTHONPATH=. pytest -m "not slow and not uses_tokens and not ux_test" diff --git a/pilot/helpers/agents/AGENTS.md b/pilot/helpers/agents/AGENTS.md index df40325..d806ea8 100644 --- a/pilot/helpers/agents/AGENTS.md +++ b/pilot/helpers/agents/AGENTS.md @@ -54,7 +54,6 @@ TODO: - Tasks provided as "programmatic goals" **(TODO: consider BDD)** - ## Code Monkey **TODO: not listed in `ROLES`** diff --git a/pilot/helpers/agents/Developer.py b/pilot/helpers/agents/Developer.py index c331093..dd0ad05 100644 --- a/pilot/helpers/agents/Developer.py +++ b/pilot/helpers/agents/Developer.py @@ -94,7 +94,7 @@ class Developer(Agent): data = step['command'] # TODO END additional_message = 'Let\'s start with the step #0:\n\n' if i == 0 else f'So far, steps { ", ".join(f"#{j}" for j in range(i)) } are finished so let\'s do step #{i + 1} now.\n\n' - return run_command_until_success(data['command'], data['timeout'], convo, additional_message=additional_message) + return run_command_until_success(convo, data['command'], timeout=data['timeout'], additional_message=additional_message) def step_human_intervention(self, convo, step: dict): """ @@ -108,7 +108,14 @@ class Developer(Agent): if self.run_command is not None else step['human_intervention_description'] response = self.project.ask_for_human_intervention('I need human intervention:', human_intervention_description, - cbs={ 'r': lambda conv: run_command_until_success(self.run_command, None, conv, force=True, return_cli_response=True) }, + cbs={ + 'r': lambda conv: run_command_until_success(conv, + self.run_command, + process_name='app', + timeout=None, + force=True, + return_cli_response=True) + }, convo=convo) if 'user_input' not in response: @@ -136,6 +143,9 @@ class Developer(Agent): return { "success": llm_response == 'DONE', "cli_response": cli_response, "llm_response": llm_response } def task_postprocessing(self, convo, development_task, continue_development, task_result, last_branch_name): + # TODO: why does `run_command` belong to the Developer class, rather than just being passed? + # ...It's set by execute_task() -> task_postprocessing(), but that is called by various sources. + # What is it at step_human_intervention()? self.run_command = convo.send_message('development/get_run_command.prompt', {}) if self.run_command.startswith('`'): self.run_command = self.run_command[1:] @@ -273,9 +283,14 @@ class Developer(Agent): '\nIf you want to run the app, ' + \ yellow_bold('just type "r" and press ENTER and that will run `' + self.run_command + '`') # continue_description = '' + # TODO: Wait for a specific string in the output or timeout? response = self.project.ask_for_human_intervention( user_description, - cbs={ 'r': lambda convo: run_command_until_success(self.run_command, None, convo, force=True, return_cli_response=True, is_root_task=True) }, + cbs={'r': lambda convo: run_command_until_success(convo, self.run_command, + process_name='app', + timeout=None, + force=True, + return_cli_response=True, is_root_task=True)}, convo=iteration_convo, is_root_task=True) @@ -349,7 +364,7 @@ class Developer(Agent): if installation_commands is not None: for cmd in installation_commands: - run_command_until_success(cmd['command'], cmd['timeout'], self.convo_os_specific_tech) + run_command_until_success(self.convo_os_specific_tech, cmd['command'], timeout=cmd['timeout']) logger.info('The entire tech stack is installed and ready to be used.') @@ -398,7 +413,7 @@ class Developer(Agent): test_type, description = convo.send_message('development/task/step_check.prompt', {}, GET_TEST_TYPE) if test_type == 'command_test': - return run_command_until_success(description['command'], description['timeout'], convo) + return run_command_until_success(convo, description['command'], timeout=description['timeout']) elif test_type == 'automated_test': # TODO get code monkey to implement the automated test pass @@ -429,7 +444,7 @@ class Developer(Agent): }, EXECUTE_COMMANDS) if type == 'COMMAND': for cmd in step_details: - run_command_until_success(cmd['command'], cmd['timeout'], convo) + run_command_until_success(convo, cmd['command'], timeout=cmd['timeout']) # elif type == 'CODE_CHANGE': # code_changes_details = get_step_code_changes() # # TODO: give to code monkey for implementation diff --git a/pilot/helpers/agents/test_Developer.py b/pilot/helpers/agents/test_Developer.py index cd2e46e..b356f4e 100644 --- a/pilot/helpers/agents/test_Developer.py +++ b/pilot/helpers/agents/test_Developer.py @@ -42,7 +42,7 @@ class TestDeveloper: @patch('helpers.AgentConvo.save_development_step') @patch('helpers.AgentConvo.create_gpt_chat_completion', return_value={'text': '{"command": "python --version", "timeout": 10}'}) - @patch('helpers.cli.execute_command', return_value=('', 'DONE')) + @patch('helpers.cli.execute_command', return_value=('', 'DONE', None)) def test_install_technology(self, mock_execute_command, mock_completion, mock_save, mock_get_saved_step): # Given @@ -61,7 +61,7 @@ class TestDeveloper: @patch('helpers.AgentConvo.create_gpt_chat_completion', return_value={'text': '{"type": "command_test", "command": {"command": "npm run test", "timeout": 3000}}'}) # 2nd arg of return_value: `None` to debug, 'DONE' if successful - @patch('helpers.cli.execute_command', return_value=('stdout:\n```\n\n```', 'DONE')) + @patch('helpers.cli.execute_command', return_value=('stdout:\n```\n\n```', 'DONE', None)) # @patch('helpers.cli.ask_user', return_value='yes') # @patch('helpers.cli.get_saved_command_run') def test_code_changes_command_test(self, mock_get_saved_step, mock_save, mock_chat_completion, @@ -126,7 +126,7 @@ class TestDeveloper: # Then assert result == {'success': True, 'user_input': 'no'} - @patch('helpers.cli.execute_command', return_value=('stdout:\n```\n\n```', 'DONE')) + @patch('helpers.cli.execute_command', return_value=('stdout:\n```\n\n```', 'DONE', None)) @patch('helpers.AgentConvo.get_saved_development_step') @patch('helpers.AgentConvo.save_development_step') @patch('utils.llm_connection.requests.post') diff --git a/pilot/helpers/cli.py b/pilot/helpers/cli.py index 545129e..dcba947 100644 --- a/pilot/helpers/cli.py +++ b/pilot/helpers/cli.py @@ -5,6 +5,7 @@ import threading import queue import time import platform +from typing import Dict from logger.logger import logger from utils.style import yellow, green, red, yellow_bold, white_bold @@ -16,6 +17,9 @@ from const.code_execution import MIN_COMMAND_RUN_TIME, MAX_COMMAND_RUN_TIME, MAX interrupted = False +running_processes: Dict[str, int] = {} +"""Holds a list of process IDs, mapped to the `process_name` provided in the call to `execute_command()`.""" + def enqueue_output(out, q): for line in iter(out.readline, ''): @@ -25,7 +29,7 @@ def enqueue_output(out, q): out.close() -def run_command(command, root_path, q_stdout, q_stderr, pid_container): +def run_command(command, root_path, q_stdout, q_stderr) -> subprocess.Popen: """ Execute a command in a subprocess. @@ -34,12 +38,11 @@ def run_command(command, root_path, q_stdout, q_stderr, pid_container): root_path (str): The directory in which to run the command. q_stdout (Queue): A queue to capture stdout. q_stderr (Queue): A queue to capture stderr. - pid_container (list): A list to store the process ID. Returns: subprocess.Popen: The subprocess object. """ - logger.info(f'Running `{command}`') + logger.info(f'Running `{command}` on {platform.system()}') if platform.system() == 'Windows': # Check the operating system process = subprocess.Popen( command, @@ -60,7 +63,6 @@ def run_command(command, root_path, q_stdout, q_stderr, pid_container): cwd=root_path ) - pid_container[0] = process.pid t_stdout = threading.Thread(target=enqueue_output, args=(process.stdout, q_stdout)) t_stderr = threading.Thread(target=enqueue_output, args=(process.stderr, q_stderr)) t_stdout.daemon = True @@ -70,7 +72,22 @@ def run_command(command, root_path, q_stdout, q_stderr, pid_container): return process -def terminate_process(pid): +def terminate_named_process(process_name: str) -> None: + if process_name in running_processes: + terminate_process(running_processes[process_name], process_name) + + +def terminate_running_processes(): + for process_name in list(running_processes.keys()): + terminate_process(running_processes[process_name], process_name) + + +def terminate_process(pid: int, name=None) -> None: + if name is None: + logger.info('Terminating process %s', pid) + else: + logger.info('Terminating process "%s" (pid: %s)', name, pid) + if platform.system() == "Windows": try: subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)]) @@ -82,8 +99,12 @@ def terminate_process(pid): except OSError as e: logger.error(f'Error while terminating process: {e}') + for process_name in list(running_processes.keys()): + if running_processes[process_name] == pid: + del running_processes[process_name] -def execute_command(project, command, timeout=None, force=False): + +def execute_command(project, command, timeout=None, process_name: str = None, force=False): """ Execute a command and capture its output. @@ -91,6 +112,8 @@ def execute_command(project, command, timeout=None, force=False): project: The project associated with the command. command (str): The command to run. timeout (int, optional): The maximum execution time in milliseconds. Default is None. + process_name (str, optional): A name for the process. + If `timeout` is not provided, can be used to terminate the process. force (bool, optional): Whether to execute the command without confirmation. Default is False. Returns: @@ -98,6 +121,7 @@ def execute_command(project, command, timeout=None, force=False): or: '', 'DONE' if user answered 'no' or 'skip' llm_response (str): The response from the agent. TODO: this seems to be 'DONE' (no or skip) or None + exit_code (int): The exit code of the process. """ if timeout is not None: if timeout < 1000: @@ -106,50 +130,60 @@ def execute_command(project, command, timeout=None, force=False): if not force: print(yellow_bold(f'\n--------- EXECUTE COMMAND ----------')) - answer = ask_user( - project, - f'Can I execute the command: `' + yellow_bold(command) + f'` with {timeout}ms timeout?', - False, - hint='If yes, just press ENTER' - ) + question = f'Can I execute the command: `{yellow_bold(command)}`' + if timeout is not None: + question += f' with {timeout}ms timeout?' + else: + question += '?' + + answer = ask_user(project, question, False, hint='If yes, just press ENTER') # TODO: I think AutoGPT allows other feedback here, like: # "That's not going to work, let's do X instead" # We don't explicitly make "no" or "skip" options to the user # see https://github.com/Pythagora-io/gpt-pilot/issues/122 + print('answer: ' + answer) if answer == 'no': - return '', 'DONE' + return '', 'DONE', None elif answer == 'skip': - return '', 'DONE' - + return '', 'DONE', None # TODO when a shell built-in commands (like cd or source) is executed, the output is not captured properly - this will need to be changed at some point # TODO: Windows support if "cd " in command or "source " in command: command = "bash -c '" + command + "'" - project.command_runs_count += 1 command_run = get_saved_command_run(project, command) if command_run is not None and project.skip_steps: # if we do, use it project.checkpoints['last_command_run'] = command_run print(yellow(f'Restoring command run response id {command_run.id}:\n```\n{command_run.cli_response}```')) - return command_run.cli_response, None + return command_run.cli_response, None, None return_value = None q_stderr = queue.Queue() q = queue.Queue() pid_container = [None] - process = run_command(command, project.root_path, q, q_stderr, pid_container) + process = run_command(command, project.root_path, q, q_stderr) + + if process_name is not None: + terminate_named_process(process_name) + running_processes[process_name] = process.pid + output = '' stderr_output = '' start_time = time.time() interrupted = False + # Note: If we don't need to log the output in real-time, we can remove q, q_stderr, the threads and this while loop. + # if timeout is not None: + # timeout /= 1000 + # output, stderr_output = process.communicate(timeout=timeout) + try: - while True and return_value is None: + while True: elapsed_time = time.time() - start_time if timeout is not None: # TODO: print to IPC using a different message type so VS Code can ignore it or update the previous value @@ -158,7 +192,7 @@ def execute_command(project, command, timeout=None, force=False): # Check if process has finished if process.poll() is not None: # Get remaining lines from the queue - time.sleep(0.1) # TODO this shouldn't be used + time.sleep(0.1) # TODO this shouldn't be used while not q.empty(): output_line = q.get_nowait() if output_line not in output: @@ -170,7 +204,7 @@ def execute_command(project, command, timeout=None, force=False): # If timeout is reached, kill the process if timeout is not None and elapsed_time * 1000 > timeout: raise TimeoutError("Command exceeded the specified timeout.") - # os.killpg(pid_container[0], signal.SIGKILL) + # os.killpg(process.pid, signal.SIGKILL) # break try: @@ -193,6 +227,9 @@ def execute_command(project, command, timeout=None, force=False): stderr_output += stderr_line print(red('CLI ERROR:') + stderr_line, end='') # Print with different color for distinction logger.error('CLI ERROR: ' + stderr_line) + + if process_name is not None: + break except (KeyboardInterrupt, TimeoutError) as e: interrupted = True @@ -203,7 +240,11 @@ def execute_command(project, command, timeout=None, force=False): print('\nTimeout detected. Stopping command execution...') logger.warn('Timeout detected. Stopping command execution...') - terminate_process(pid_container[0]) + terminate_process(process.pid) + + elapsed_time = time.time() - start_time + print(f'{command} took {round(elapsed_time * 1000)}ms to execute.') + logger.info(f'{command} took {round(elapsed_time * 1000)}ms to execute.') # stderr_output = '' # while not q_stderr.empty(): @@ -215,9 +256,10 @@ def execute_command(project, command, timeout=None, force=False): return_value = 'stderr:\n```\n' + stderr_output[0:MAX_COMMAND_OUTPUT_LENGTH] + '\n```\n' return_value += 'stdout:\n```\n' + output[-MAX_COMMAND_OUTPUT_LENGTH:] + '\n```' - command_run = save_command_run(project, command, return_value) + save_command_run(project, command, return_value) + + return return_value, None, process.returncode - return return_value, None def build_directory_tree(path, prefix="", ignore=None, is_last=False, files=None, add_descriptions=False): """Build the directory tree structure in tree-like format. @@ -272,31 +314,57 @@ def execute_command_and_check_cli_response(command, timeout, convo): - llm_response (str): 'DONE' or 'NEEDS_DEBUGGING' """ # TODO: Prompt mentions `command` could be `INSTALLED` or `NOT_INSTALLED`, where is this handled? - cli_response, llm_response = execute_command(convo.agent.project, command, timeout) + cli_response, llm_response, exit_code = execute_command(convo.agent.project, command, timeout=timeout) if llm_response is None: llm_response = convo.send_message('dev_ops/ran_command.prompt', { 'cli_response': cli_response, 'command': command }) return cli_response, llm_response -def run_command_until_success(command, timeout, convo, additional_message=None, force=False, - return_cli_response=False, is_root_task=False): +def run_command_until_success(convo, command, + timeout: int | None, + process_name: str | None = None, + additional_message=None, + force=False, + return_cli_response=False, + is_root_task=False): """ Run a command until it succeeds or reaches a timeout. Args: + convo (AgentConvo): The conversation object. command (str): The command to run. timeout (int): The maximum execution time in milliseconds. - convo (AgentConvo): The conversation object. + process_name: A name for the process. + If `timeout` is not provided, can be used to terminate the process. additional_message (str, optional): Additional message to include in the response. force (bool, optional): Whether to execute the command without confirmation. Default is False. + return_cli_response (bool, optional): If True, may raise TooDeepRecursionError(cli_response) + is_root_task (bool, optional): If True and TokenLimitError is raised, will call `convo.load_branch(reset_branch_id)` """ - cli_response, response = execute_command(convo.agent.project, command, timeout, force) + cli_response, response, exit_code = execute_command(convo.agent.project, + command, + timeout=timeout, + process_name=process_name, + force=force) + if response is None: - response = convo.send_message('dev_ops/ran_command.prompt', - {'cli_response': cli_response, 'command': command, 'additional_message': additional_message}) + logger.info(f'{command} exit code: {exit_code}') + if exit_code is None: + response = 'DONE' + else: + # "I ran the command and the output was... respond with 'DONE' or 'NEEDS_DEBUGGING'" + response = convo.send_message('dev_ops/ran_command.prompt', + { + 'cli_response': cli_response, + 'command': command, + 'additional_message': additional_message, + 'exit_code': exit_code + }) + logger.debug(f'LLM response: {response}') if response != 'DONE': + # 'NEEDS_DEBUGGING' print(red(f'Got incorrect CLI response:')) print(cli_response) print(red('-------------------')) diff --git a/pilot/main.py b/pilot/main.py index 14ed573..ceb8f4c 100644 --- a/pilot/main.py +++ b/pilot/main.py @@ -7,10 +7,9 @@ import sys import traceback from dotenv import load_dotenv load_dotenv() -from helpers.ipc import IPCClient -from const.ipc import MESSAGE_TYPE -from utils.style import red +from utils.style import red +from utils.custom_print import get_custom_print from helpers.Project import Project from utils.arguments import get_arguments from utils.exit import exit_gpt_pilot @@ -34,46 +33,9 @@ def init(): return arguments -def get_custom_print(args): - built_in_print = builtins.print - - def print_to_external_process(*args, **kwargs): - # message = " ".join(map(str, args)) - message = args[0] - - if 'type' not in kwargs: - kwargs['type'] = 'verbose' - elif kwargs['type'] == MESSAGE_TYPE['local']: - local_print(*args, **kwargs) - return - - ipc_client_instance.send({ - 'type': MESSAGE_TYPE[kwargs['type']], - 'content': message, - }) - if kwargs['type'] == MESSAGE_TYPE['user_input_request']: - return ipc_client_instance.listen() - - def local_print(*args, **kwargs): - message = " ".join(map(str, args)) - if 'type' in kwargs: - if kwargs['type'] == MESSAGE_TYPE['info']: - return - del kwargs['type'] - - built_in_print(message, **kwargs) - - ipc_client_instance = None - if '--external-log-process-port' in args: - ipc_client_instance = IPCClient(args['--external-log-process-port']) - return print_to_external_process, ipc_client_instance - else: - return local_print, ipc_client_instance - - if __name__ == "__main__": try: - # sys.argv.append('--ux-test=' + 'run_command_until_success') + # sys.argv.append('--ux-test=' + 'continue_development') args = init() builtins.print, ipc_client_instance = get_custom_print(args) @@ -83,7 +45,7 @@ if __name__ == "__main__": print({ 'db_data': get_created_apps_with_steps() }, type='info') elif '--ux-test' in args: from test.ux_tests import run_test - run_test(args['--ux-test']) + run_test(args['--ux-test'], args) else: # TODO get checkpoint from database and fill the project with it project = Project(args, ipc_client_instance=ipc_client_instance) diff --git a/pilot/prompts/dev_ops/ran_command.prompt b/pilot/prompts/dev_ops/ran_command.prompt index c0cb0cf..a586686 100644 --- a/pilot/prompts/dev_ops/ran_command.prompt +++ b/pilot/prompts/dev_ops/ran_command.prompt @@ -1,6 +1,8 @@ -{{ additional_info }}I ran the command `{{ command }}` and for this response from CLI: +{{ additional_info }}I ran the command `{{ command }}` +{%- if exit_code %}, the exit code was {{ exit_code }} {% endif -%} +and the output was: ``` {{ cli_response }} ``` -If the command was successfully executed, respond with `DONE` and if it wasn't, respond with `NEEDS_DEBUGGING`. \ No newline at end of file +If the command was successfully executed, respond with `DONE` and if it wasn't, respond with `NEEDS_DEBUGGING`. diff --git a/pilot/test/ux_tests/Dev_continue_development.py b/pilot/test/ux_tests/Dev_continue_development.py new file mode 100644 index 0000000..d676b2b --- /dev/null +++ b/pilot/test/ux_tests/Dev_continue_development.py @@ -0,0 +1,36 @@ +import pytest +from unittest.mock import patch + +from helpers.AgentConvo import AgentConvo +from helpers.agents import Developer +from .utils import create_project +from helpers.cli import terminate_running_processes +from test.mock_questionary import MockQuestionary + + +@pytest.mark.ux_test +@patch('utils.questionary.get_saved_user_input') +@patch('helpers.cli.get_saved_command_run') +@patch('helpers.AgentConvo.get_saved_development_step') +@patch('helpers.AgentConvo.save_development_step') +def test_continue_development(mock_4, mock_3, mock_2, mock_1): + # Given + project = create_project('continue_development', 'hello_world_server') + # execute_command(project, 'npm install', 13000) + + developer = Developer(project) + project.developer = developer + convo = AgentConvo(developer) + convo.load_branch = lambda last_branch_name: None + developer.run_command = 'node server.js' + + # Note: uncomment the following 2 lines and indent the remaining lines when debugging without console input + mock_questionary = MockQuestionary(['r', 'continue']) + with patch('utils.questionary.questionary', mock_questionary): + + # When + # `continue_development` calls `run_command_until_success()` if the user types "r" + developer.continue_development(convo, 'branch_name', 'The web page should say "Hello, World!"') + print('end of "continue_development" scenario') + + terminate_running_processes() diff --git a/pilot/test/ux_tests/__init__.py b/pilot/test/ux_tests/__init__.py index 104f87f..9eeb9f6 100644 --- a/pilot/test/ux_tests/__init__.py +++ b/pilot/test/ux_tests/__init__.py @@ -1,10 +1,20 @@ -from .run_command_until_success import run_command_until_success +# from .run_command_until_success import run_command_until_success +from .cli_execute_command import cli_execute_command +from .Dev_continue_development import test_continue_development +from .utils import use_args -def run_test(test_name: str): +def run_test(test_name: str, args): print(f'Running UX test "{test_name}"...') - if test_name == 'run_command_until_success': - return run_command_until_success() + tests = { + # 'run_command_until_success': run_command_until_success, + 'cli_execute_command': cli_execute_command, + 'continue_development': test_continue_development, + } + + if test_name in tests: + use_args(args) + return tests[test_name]() print(f'UX test "{test_name}" not found') diff --git a/pilot/utils/custom_print.py b/pilot/utils/custom_print.py new file mode 100644 index 0000000..b063541 --- /dev/null +++ b/pilot/utils/custom_print.py @@ -0,0 +1,40 @@ +import builtins +from helpers.ipc import IPCClient +from const.ipc import MESSAGE_TYPE + + +def get_custom_print(args): + built_in_print = builtins.print + + def print_to_external_process(*args, **kwargs): + # message = " ".join(map(str, args)) + message = args[0] + + if 'type' not in kwargs: + kwargs['type'] = 'verbose' + elif kwargs['type'] == MESSAGE_TYPE['local']: + local_print(*args, **kwargs) + return + + ipc_client_instance.send({ + 'type': MESSAGE_TYPE[kwargs['type']], + 'content': message, + }) + if kwargs['type'] == MESSAGE_TYPE['user_input_request']: + return ipc_client_instance.listen() + + def local_print(*args, **kwargs): + message = " ".join(map(str, args)) + if 'type' in kwargs: + if kwargs['type'] == MESSAGE_TYPE['info']: + return + del kwargs['type'] + + built_in_print(message, **kwargs) + + ipc_client_instance = None + if '--external-log-process-port' in args: + ipc_client_instance = IPCClient(args['--external-log-process-port']) + return print_to_external_process, ipc_client_instance + else: + return local_print, ipc_client_instance diff --git a/pilot/utils/exit.py b/pilot/utils/exit.py index 4b5d286..fafec5f 100644 --- a/pilot/utils/exit.py +++ b/pilot/utils/exit.py @@ -3,6 +3,7 @@ import os import hashlib import requests +from helpers.cli import terminate_running_processes from utils.questionary import get_user_feedback @@ -43,6 +44,7 @@ def get_path_id(): def exit_gpt_pilot(ask_feedback=True): + terminate_running_processes() path_id = get_path_id() send_telemetry(path_id) diff --git a/pilot/utils/llm_connection.py b/pilot/utils/llm_connection.py index 16255c2..d913d27 100644 --- a/pilot/utils/llm_connection.py +++ b/pilot/utils/llm_connection.py @@ -262,7 +262,8 @@ def stream_gpt_completion(data, req_type, project): model = os.getenv('MODEL_NAME', 'gpt-4') endpoint = os.getenv('ENDPOINT') - logger.info(f'> Request model: {model} ({data["model"]}) messages: {data["messages"]}') + logger.info(f'> Request model: {model} ({data["model"]})\n' + + '\n'.join([f"{message['role']}: {message['content']}" for message in data['messages']])) if endpoint == 'AZURE': # If yes, get the AZURE_ENDPOINT from .ENV file diff --git a/pytest.ini b/pytest.ini index b0c4c73..fe9925b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,4 +5,5 @@ python_files = test_*.py markers = slow: marks tests as slow (deselect with '-m "not slow"') uses_tokens: Integration tests which use tokens + ux_test: Tests which are used to test the UX daily: tests which should be run daily diff --git a/requirements.txt b/requirements.txt index 986e162..2da398d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ prompt-toolkit==3.0.39 psycopg2-binary==2.9.6 python-dotenv==1.0.0 python-editor==1.0.4 +pytest==7.4.2 questionary==1.10.0 readchar==4.0.5 regex==2023.6.3