├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── modules ├── __init__.py ├── experiment │ ├── debate_factory.py │ ├── scalar_debate.py │ ├── template.py │ └── vector2d_debate.py ├── llm │ ├── __init__.py │ ├── agent.py │ ├── agent_2d.py │ ├── api_key.py │ ├── gpt.py │ └── role.py ├── prompt │ ├── __init__.py │ ├── form.py │ ├── personality.py │ ├── scenario.py │ ├── scenario_2d.py │ └── summarize.py └── visual │ ├── __init__.py │ ├── box_plot.py │ ├── gen_html.py │ ├── plot.py │ ├── plot_2d.py │ ├── read_data.py │ └── util.py ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | log 3 | html 4 | *.p 5 | test 6 | config -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | VOLUME [ "/data" ] 4 | WORKDIR /tmp 5 | COPY requirements.txt ./ 6 | RUN pip install -r requirements.txt 7 | 8 | WORKDIR /data 9 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at Westlake University] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 20 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Multi-Agent Consensus Seeking via
Large Language Models

4 |
5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 | This file is the source code of our paper "Multi-Agent Consensus Seeking via Large Language Models". 19 | 20 | https://github.com/WestlakeIntelligentRobotics/ConsensusLLM-code/assets/51610063/930ab64c-1ed9-46da-9044-3d253f3c1339 21 | 22 | 23 | ### Prerequisites 24 | 25 | Before you begin, ensure you have met the following requirements: 26 | 27 | - **Python**: This project is primarily developed in Python. Make sure you have Python installed on your system. You can download it from [python.org](https://www.python.org/downloads/). 28 | 29 | - **Python Libraries**: You will need to install several Python libraries to run the code. You can install these libraries using pip, Python's package manager. To install the required libraries, run the following command: 30 | 31 | ```bash 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | The `requirements.txt` file, included in this repository, lists all the necessary Python libraries along with their versions. 36 | 37 | - **Docker (Optional)**: If you plan to deploy the project using Docker, you'll need to have Docker installed on your system. 38 | 39 | 1. You can download Docker Desktop from [docker.com](https://www.docker.com/products/docker-desktop). 40 | 41 | 2. **Build the Docker Image:** 42 | 43 | To build a Docker image from the included Dockerfile, navigate to the project directory in your terminal and run the following command: 44 | 45 | ```bash 46 | docker build -t consensus-debate . 47 | ``` 48 | 49 | This command will create a Docker image named `my-project-image` using the Dockerfile in the current directory. 50 | 51 | 3. **Mounting Your Code:** 52 | 53 | You can mount your code into the Docker container by using the `-v` option when running the container. For example: 54 | 55 | ```bash 56 | docker run -it -p -v /path/to/your/code:/data consensus-debate /bin/bash 57 | ``` 58 | 59 | Replace `/path/to/your/code` with the absolute path to your project directory. The `-v` flag maps your local code directory to the `/data` directory inside the Docker container. 60 | 61 | - **OpenAI API Keys:** 62 | 63 | You will need valid OpenAI API keys to interact with the OpenAI services. Follow these steps to add your API keys to the `config.yml` file: 64 | 65 | 1. Create a `config/keys.yml` file in the root directory of the project if it doesn't already exist. 66 | 67 | 2. Open the `config/keys.yml` file with a text editor. 68 | 69 | 3. Add your API keys in the following format, replacing the placeholder keys with your actual OpenAI API keys: 70 | 71 | ```yaml 72 | api_keys: 73 | 0: "sk-YourFirstAPIKeyHere" 74 | 1: "sk-YourSecondAPIKeyHere" 75 | 2: "sk-YourThirdAPIKeyHere" 76 | ``` 77 | 78 | You can add multiple API keys as needed, and they will be accessible by index. 79 | 80 | 4. Set the `api_base` value to the OpenAI API endpoint: 81 | 82 | ```yaml 83 | api_base: 'https://api.openai.com/v1' 84 | ``` 85 | 86 | 5. Save and close the `config.yml` file. 87 | 88 | By ensuring you have these prerequisites in place, you'll be ready to use the code and run the experiments described in this project. 89 | 90 | ### Running Experiments 91 | 92 | 1. **Create Test Files**: In the "test" directory, create one or more Python test files (e.g., `my_experiment.py`) that define the experiments you want to run. These test files should import and use your Python template as a framework for conducting experiments. Below is an example of what a test file might look like: 93 | 94 | ```python 95 | import datetime 96 | import subprocess 97 | 98 | def main(n_agent): 99 | rounds = 9 # number of rounds in single experiment 100 | agents = n_agent 101 | n_stubborn = 0 # number of stubborn agents 102 | n_suggestible = 0 # number of suggestible agents 103 | n_exp = 9 # number of experiments 104 | current_datetime = datetime.datetime.now() 105 | # Format the date as a string 106 | formatted_date = current_datetime.strftime("%Y-%m-%d_%H-%M") 107 | out_file = "./log/scalar_debate/n_agents{}_rounds{}_n_exp{}_{}".format(agents, rounds, n_exp, formatted_date) 108 | # Build the command line 109 | cmd = [ 110 | 'python', './run.py', 111 | '--rounds', str(rounds), 112 | '--out_file', out_file, 113 | '--agents', str(agents), 114 | '--n_stubborn', str(n_stubborn), 115 | '--n_suggestible', str(n_suggestible), 116 | '--n_exp', str(n_exp), 117 | # '--not_full_connected' # uncomment this if you want use other topology structures 118 | ] 119 | 120 | # Run the command 121 | subprocess.run(cmd) 122 | 123 | if __name__ == "__main__": 124 | main(n_agent=3) 125 | ``` 126 | 127 | Customize the experiment setup according to your specific needs. 128 | 129 | 2. **Setting the Experiment Type**: Before running experiments, make sure to set the appropriate experiment type in the `run.py` file: 130 | 131 | ```python 132 | exp = debate_factory("2d", args, connectivity_matrix=m) 133 | ``` 134 | 135 | The first parameter, `"2d"`, specifies the type of experiment. You can use `"scalar"` for scalar debate or `"2d"` for vector debate. 136 | 137 | 3. **Run Experiments**: You can run the experiments from the command line by executing the test files in the root directory: 138 | 139 | ```bash 140 | python test/my_experiment.py 141 | ``` 142 | 143 | Replace `my_experiment.py` with the name of the test file you want to run. This will execute your experiment using the Python template and generate results accordingly. 144 | 145 | 4. **Locating Experiment Results**: After running experiments using the provided test files, you can find the data files and logs in the "log" directory. The "log" directory is defined in your test files, and it's where your experiment results are stored. 146 | 147 | ### Plotting and Generating HTML 148 | 149 | #### Plotting Data 150 | 151 | The code includes functionality for automatic plotting of data when running experiments. However, if you wish to manually plot a specific data file, you can use the following command: 152 | 153 | **scaler debate**: 154 | 155 | ```bash 156 | python -m modules.visual.plot ./log/scalar_debate_temp_0_7/n_agents8_rounds9_n_exp9_2023-10-11_13-44/data.p 157 | ``` 158 | 159 | **vector debate**: 160 | ```bash 161 | python -m modules.visual.plot_2d ./log/vector2d_debate/n_agents3_rounds20_n_exp1_2023-10-27_14-37/trajectory.p 162 | ``` 163 | 164 | Replace the file path with the path to the specific data file you want to plot. This command will generate plots based on the provided data file. 165 | 166 | #### Generating HTML Reports 167 | 168 | The code automatically generates HTML reports for experiments. However, if you want to manually generate an HTML report for a specific experiment or dataset, you can use the following command: 169 | 170 | ```bash 171 | python ./gen_html.py 172 | ``` 173 | 174 | This command will generate an HTML report based on the data and logs available in the "log" directory. 175 | 176 | Please note that for automatically generated HTML reports, the script may take into account the latest experiment data and log files available in the "log" directory. However, running `gen_html.py` manually allows you to create an HTML report at any time, independently of experiment execution. 177 | 178 | ### Collaborators 179 | - [Huaben Chen](https://github.com/huabench) 180 | - [Wenkang Ji](https://github.com/jwk1rose) 181 | 182 | ### License: 183 | This project is licensed under the [MIT License](LICENSE). 184 | 185 | 186 | ### Citing 187 | If you find our work useful, please consider citing: 188 | ```BibTeX 189 | @misc{chen2023multiagent, 190 | title={Multi-Agent Consensus Seeking via Large Language Models}, 191 | author={Huaben Chen and Wenkang Ji and Lufeng Xu and Shiyu Zhao}, 192 | year={2023}, 193 | eprint={2310.20151}, 194 | archivePrefix={arXiv}, 195 | primaryClass={cs.CL} 196 | } 197 | ``` 198 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindyLab/ConsensusLLM-code/a1012fd2a54b0de17e8d4febce3fd59b148fdbd8/modules/__init__.py -------------------------------------------------------------------------------- /modules/experiment/debate_factory.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | from .scalar_debate import ScalarDebate 27 | from .vector2d_debate import Vector2dDebate 28 | 29 | def debate_factory(name, args, connectivity_matrix): 30 | """ 31 | Create a debate instance based on the given name and arguments. 32 | 33 | Args: 34 | name (str): The name of the debate type (either "scalar" or "2d"). 35 | args (dict): A dictionary of arguments to initialize the debate. 36 | connectivity_matrix (list): The connectivity matrix for the debate. 37 | 38 | Returns: 39 | Debate: An instance of the appropriate debate class (ScalarDebate or Vector2dDebate). 40 | 41 | Note: 42 | If the 'name' argument is not recognized, the function returns None. 43 | 44 | Example: 45 | To create a ScalarDebate: 46 | debate_factory("scalar", args, connectivity_matrix) 47 | 48 | To create a Vector2dDebate: 49 | debate_factory("2d", args, connectivity_matrix) 50 | """ 51 | if name == "scalar": 52 | return ScalarDebate(args, connectivity_matrix) 53 | elif name == "2d": 54 | return Vector2dDebate(args, connectivity_matrix) 55 | else: 56 | return None -------------------------------------------------------------------------------- /modules/experiment/scalar_debate.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import numpy as np 27 | from concurrent.futures import ThreadPoolExecutor, as_completed 28 | from .template import Template 29 | from ..llm.agent import Agent, GPT 30 | from ..llm.api_key import api_keys 31 | from ..llm.role import names 32 | from ..prompt.scenario import agent_role, game_description, round_description 33 | from ..prompt.form import agent_output_form 34 | from ..prompt.personality import stubborn, suggestible 35 | from ..visual.gen_html import gen_html 36 | from ..visual.plot import plot_result 37 | 38 | class ScalarDebate(Template): 39 | """ 40 | A class representing a simulation of scalar debate between multiple agents. 41 | 42 | This class extends the Template class and provides functionality to set up 43 | and run a simulation where multiple agents engage in debates, taking into 44 | account their positions, personalities, and knowledge connectivity. 45 | 46 | Args: 47 | args: Command-line arguments and configuration. 48 | connectivity_matrix: Matrix defining agent knowledge connectivity. 49 | 50 | Raises: 51 | ValueError: If arguments are invalid or insufficient. 52 | """ 53 | def __init__(self, args, connectivity_matrix): 54 | super().__init__(args) 55 | self._n_agents = args.agents 56 | self._init_input = game_description + "\n\n" + agent_output_form 57 | self._round_description = round_description 58 | self._positions = [[]] * args.n_exp 59 | self._output_file = args.out_file 60 | self._n_suggestible = args.n_suggestible 61 | self._n_stubborn = args.n_stubborn 62 | np.random.seed(0) 63 | 64 | # Define the connectivity matrix for agent knowledge 65 | # m(i, j) = 1 means agent i knows the position of agent j 66 | self._m = connectivity_matrix 67 | 68 | # Safety checks 69 | if args.n_stubborn + args.n_suggestible > self._n_agents: 70 | raise ValueError("stubborn + suggestible agents exceed " 71 | f"total agents: {self._n_agents}") 72 | if len(api_keys) < self._n_agents * args.n_exp: 73 | raise ValueError("api_keys are not enough for " 74 | f"{self._n_agents} agents") 75 | if self._m.shape[0] != self._m.shape[1]: 76 | raise ValueError("connectivity_matrix is not a square matrix, " 77 | f"shape: {self._m.shape}") 78 | if self._m.shape[0] != self._n_agents: 79 | raise ValueError("connectivity_matrix size doesn't match the " 80 | f"number of agents: {self._m.shape}") 81 | 82 | def _generate_agents(self, simulation_ind): 83 | """ 84 | Generate agent instances based on provided parameters. 85 | 86 | Args: 87 | simulation_ind: Index of the current simulation. 88 | 89 | Returns: 90 | List of generated agents. 91 | """ 92 | agents = [] 93 | position = np.random.randint(0, 100, size=self._n_agents) 94 | for idx in range(self._n_agents): 95 | position_others = position[self._m[idx, :]] 96 | 97 | # Create agent instances 98 | agent = Agent(position=position[idx], 99 | other_position=position_others, 100 | key=api_keys[simulation_ind * self._n_agents + idx], 101 | model="gpt-3.5-turbo-0613", 102 | name=names[idx]) 103 | 104 | # Add personality, neutral by default 105 | personality = "" 106 | if idx < self._n_stubborn: 107 | personality = stubborn 108 | elif self._n_stubborn <= idx < ( 109 | self._n_stubborn + self._n_suggestible): 110 | personality = suggestible 111 | agent.memories_update(role='system', 112 | content=agent_role + personality) 113 | agents.append(agent) 114 | 115 | self._positions[simulation_ind] = position 116 | return agents 117 | 118 | def _generate_question(self, agent, round) -> str: 119 | """ 120 | Generate a question for an agent in a given round. 121 | 122 | Args: 123 | agent: The agent for which to generate the question. 124 | round: The current round number. 125 | 126 | Returns: 127 | A formatted question for the agent. 128 | """ 129 | if round == 0: 130 | input = self._init_input.format(agent.position, 131 | agent.other_position) 132 | else: 133 | input = self._round_description.format(agent.position, 134 | agent.other_position) 135 | return input 136 | 137 | def _exp_postprocess(self): 138 | """ 139 | Perform post-processing after the experiment, including saving 140 | records and generating plots. 141 | """ 142 | is_success, filename = self.save_record(self._output_file) 143 | if is_success: 144 | # Call functions to plot and generate HTML 145 | plot_result(filename, self._output_file) 146 | gen_html(filename, self._output_file) 147 | 148 | def _round_postprocess(self, simulation_ind, round, results, agents): 149 | """ 150 | Perform post-processing for each round of the simulation. 151 | 152 | Args: 153 | simulation_ind: Index of the current simulation. 154 | round: The current round number. 155 | results: Results from the round. 156 | agents: List of agents. 157 | """ 158 | for idx, agent in enumerate(agents): 159 | res_filtered = np.array(results)[self._m[idx, :]] 160 | other_position = [x for _, x in res_filtered] 161 | agent.other_position = other_position 162 | 163 | def _update_record(self, record, agent_contexts, simulation_ind, agents): 164 | """ 165 | Update the record with agent contexts for a given simulation. 166 | 167 | Args: 168 | record: The record to be updated. 169 | agent_contexts: Contexts of the agents. 170 | simulation_ind: Index of the current simulation. 171 | agents: List of agents. 172 | """ 173 | record[tuple(self._positions[simulation_ind])] = agent_contexts 174 | -------------------------------------------------------------------------------- /modules/experiment/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | from abc import ABC, abstractmethod 27 | from concurrent.futures import ThreadPoolExecutor, as_completed 28 | import threading 29 | import queue 30 | import pickle 31 | import os 32 | from tqdm import tqdm 33 | 34 | class Template(ABC): 35 | """ 36 | A template class for designing and running experiments with multiple agents 37 | and rounds. 38 | 39 | This abstract class defines a template for designing experiments where 40 | multiple agents interact in multiple rounds. Subclasses must implement 41 | various methods to customize the behavior of the experiment, including 42 | generating questions, managing agents, updating experiment records, and 43 | performing post-processing. 44 | 45 | Attributes: 46 | _record (dict): A dictionary for recording experiment data. 47 | _n_agent (int): Number of agents participating in the experiment. 48 | _n_round (int): Number of rounds in the experiment. 49 | _n_experiment (int): Number of independent experiments to run. 50 | _lock (threading.Lock): 51 | A lock for ensuring thread safety during data updates. 52 | 53 | Subclasses should implement the following abstract methods: 54 | - _generate_question 55 | - _generate_agents 56 | - _update_record 57 | - _round_postprocess 58 | - _exp_postprocess 59 | 60 | Public Methods: 61 | - run: Run the experiment using a thread pool for concurrency. 62 | - save_record: Save the experiment record to a file. 63 | 64 | To use this template, create a subclass that defines the specific behavior 65 | of the experiment. 66 | """ 67 | def __init__(self, args): 68 | """ 69 | Initialize the Template with provided arguments. 70 | 71 | Initializes instance variables for managing the experiment. 72 | """ 73 | self._record = {} # A dictionary for recording data 74 | self._n_agent = args.agents # Number of agents 75 | self._n_round = args.rounds # Number of rounds 76 | self._n_experiment = args.n_exp # Number of experiments 77 | self._lock = threading.Lock() # Lock for thread safety 78 | 79 | @abstractmethod 80 | def _generate_question(self, agent, round) -> str: 81 | """ 82 | Generate a question for an agent in a specific round. 83 | 84 | Args: 85 | agent: An agent participating in the experiment. 86 | round: The current round of the experiment. 87 | 88 | Returns: 89 | str: The generated question. 90 | """ 91 | pass 92 | 93 | @abstractmethod 94 | def _generate_agents(self, simulation_ind): 95 | """ 96 | Generate a set of agents for a simulation. 97 | 98 | Args: 99 | simulation_ind: Index of the current simulation. 100 | 101 | Returns: 102 | list: A list of agent objects. 103 | """ 104 | pass 105 | 106 | @abstractmethod 107 | def _update_record(self, record, agent_contexts, simulation_ind, agents): 108 | """ 109 | Update the experiment record based on agent data. 110 | 111 | Args: 112 | record: The experiment record to be updated. 113 | agent_contexts: List of agent histories and data. 114 | simulation_ind: Index of the current simulation. 115 | agents: List of agents participating in the experiment. 116 | """ 117 | pass 118 | 119 | @abstractmethod 120 | def _round_postprocess(self, simulation_ind, round, results, agents): 121 | """ 122 | Perform post-processing for a round of the experiment. 123 | 124 | Args: 125 | simulation_ind: Index of the current simulation. 126 | round: The current round of the experiment. 127 | results: List of results from agents. 128 | agents: List of agents participating in the experiment. 129 | """ 130 | pass 131 | 132 | @abstractmethod 133 | def _exp_postprocess(self): 134 | """ 135 | Perform post-processing for the entire experiment. 136 | """ 137 | pass 138 | 139 | def run(self): 140 | """ 141 | Run the experiment using a thread pool for concurrency. 142 | """ 143 | try: 144 | with ThreadPoolExecutor(max_workers=self._n_experiment) as executor: 145 | progress = tqdm(total=self._n_experiment * self._n_round, 146 | desc="Processing", dynamic_ncols=True) 147 | futures = {executor.submit(self._run_once, sim_ind, progress) 148 | for sim_ind in range(self._n_experiment)} 149 | 150 | for future in as_completed(futures): 151 | if future.exception() is not None: 152 | print("A thread raised an exception: " 153 | f"{future.exception()}") 154 | progress.close() 155 | except Exception as e: 156 | print(f"An exception occurred: {e}") 157 | finally: 158 | self._exp_postprocess() 159 | 160 | def _run_once(self, simulation_ind, progress): 161 | """ 162 | Run a single simulation of the experiment. 163 | 164 | Args: 165 | simulation_ind: Index of the current simulation. 166 | progress: Progress bar for tracking the simulation's progress. 167 | """ 168 | agents = self._generate_agents(simulation_ind) 169 | try: 170 | for round in range(self._n_round): 171 | results = queue.Queue() 172 | n_thread = len(agents) if round < 4 else 1 173 | with ThreadPoolExecutor(n_thread) as agent_executor: 174 | futures = [] 175 | for agent_ind, agent in enumerate(agents): 176 | question = self. _generate_question(agent, round) 177 | futures.append(agent_executor 178 | .submit(agent.answer, question, 179 | agent_ind, round, 180 | simulation_ind)) 181 | 182 | for ind, future in enumerate(as_completed(futures)): 183 | if future.exception() is not None: 184 | print("A thread raised an exception: " 185 | f"{future.exception()}") 186 | else: 187 | idx, result = future.result() 188 | results.put((idx, result)) 189 | results = list(results.queue) 190 | results = sorted(results, key=lambda x: x[0]) 191 | progress.update(1) 192 | self._round_postprocess(simulation_ind, round, results, agents) 193 | 194 | except Exception as e: 195 | print(f"error:{e}") 196 | finally: 197 | agent_contexts = [agent.get_history() for agent in agents] 198 | with self._lock: 199 | self._update_record(self._record, agent_contexts, 200 | simulation_ind, agents) 201 | 202 | def save_record(self, output_dir: str): 203 | """ 204 | Save the experiment record to a file. 205 | 206 | Args: 207 | output_dir: The directory where the record will be saved. 208 | 209 | Returns: 210 | Tuple: A tuple with a success indicator and the file path. 211 | """ 212 | try: 213 | if not os.path.exists(output_dir): 214 | os.makedirs(output_dir) 215 | data_file = output_dir + '/data.p' 216 | # Save the record to a pickle file 217 | pickle.dump(self._record, open(data_file, "wb")) 218 | return True, data_file 219 | except Exception as e: 220 | print(f"An exception occurred while saving the file: {e}") 221 | print("Saving to the current directory instead.") 222 | # Backup in case of an exception 223 | pickle.dump(self._record, open("backup_output_file.p", "wb")) 224 | return False, "" -------------------------------------------------------------------------------- /modules/experiment/vector2d_debate.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import numpy as np 27 | import pickle 28 | 29 | from .template import Template 30 | from ..llm.agent_2d import Agent2D 31 | from ..llm.api_key import api_keys 32 | from ..llm.role import names 33 | from ..prompt.form import agent_output_form 34 | from ..prompt.personality import stubborn, suggestible 35 | from ..prompt.scenario_2d import agent_role, game_description, round_description 36 | from ..visual.gen_html import gen_html 37 | from ..visual.plot_2d import plot_xy, video 38 | 39 | class Vector2dDebate(Template): 40 | """ 41 | Vector2dDebate is a class that simulates a 2D debate scenario with multiple 42 | agents. 43 | 44 | This class provides the framework to conduct 2D debates with agents and 45 | record their trajectories. 46 | 47 | Args: 48 | args: 49 | An object containing configuration parameters for the debate 50 | simulation. 51 | connectivity_matrix: 52 | A square matrix defining agent knowledge connectivity. 53 | 54 | Raises: 55 | ValueError: 56 | If the sum of stubborn and suggestible agents exceeds the total 57 | number of agents, 58 | if there are insufficient API keys for the agents, or if the 59 | connectivity matrix is not appropriate. 60 | """ 61 | def __init__(self, args, connectivity_matrix): 62 | """ 63 | Initialize the Vector2dDebate instance. 64 | 65 | Args: 66 | args: An object containing configuration options. 67 | connectivity_matrix: A matrix defining agent knowledge connectivity. 68 | 69 | Raises: 70 | ValueError: If the input parameters are invalid. 71 | """ 72 | super().__init__(args) 73 | self._dt = 0.1 74 | self._n_agents = args.agents 75 | self._init_input = game_description + "\n\n" + agent_output_form 76 | self._round_description = round_description 77 | self._positions = [[]] * args.n_exp 78 | self._output_file = args.out_file 79 | self._n_suggestible = args.n_suggestible 80 | self._n_stubborn = args.n_stubborn 81 | self._trajectory = {"pos": {}, "target": {}} # A dictionary for recording agent trajectories 82 | 83 | # np.random.seed(0) 84 | # Define the connectivity matrix for agent knowledge 85 | # m(i, j) = 1 means agent i knows the position of agent j 86 | self._m = connectivity_matrix 87 | 88 | # Safety checks for input parameters 89 | if args.n_stubborn + args.n_suggestible > self._n_agents: 90 | raise ValueError("stubborn + suggestible agents is more than " 91 | f"{self._n_agents}") 92 | if len(api_keys) < self._n_agents * args.n_exp: 93 | raise ValueError("api_keys are not enough for " 94 | f"{self._n_agents} agents") 95 | if self._m.shape[0] != self._m.shape[1]: 96 | raise ValueError("connectivity_matrix is not a square matrix, " 97 | f"shape: {self._m.shape}") 98 | if self._m.shape[0] != self._n_agents: 99 | raise ValueError("connectivity_matrix is not enough for " 100 | f"{self._n_agents} agents, shape: {self._m.shape}") 101 | 102 | def _generate_agents(self, simulation_ind): 103 | """Generate agent instances for the simulation. 104 | 105 | Args: 106 | simulation_ind: Index of the simulation. 107 | 108 | Returns: 109 | List of Agent2D instances. 110 | """ 111 | agents = [] 112 | position = (np.array([[20, 20], [80, 20], [50, 80]]) 113 | + np.random.randint(-10, 10, size=(self._n_agents, 2))) 114 | 115 | for idx in range(self._n_agents): 116 | position_others = [(x, y) for x, y in position[self._m[idx, :]]] 117 | agent = Agent2D(position=tuple(position[idx]), 118 | other_position=position_others, 119 | key=api_keys[simulation_ind * self._n_agents + idx], 120 | model="gpt-3.5-turbo-0613", 121 | name=names[idx]) 122 | # add personality, neutral by default 123 | personality = "" 124 | if idx < self._n_stubborn: 125 | personality = stubborn 126 | elif (self._n_stubborn <= idx 127 | < self._n_stubborn + self._n_suggestible): 128 | personality = suggestible 129 | agent.memories_update(role='system', 130 | content=agent_role + personality) 131 | agents.append(agent) 132 | self._positions[simulation_ind] = position 133 | return agents 134 | 135 | def _generate_question(self, agent, round) -> str: 136 | """Generate a question for an agent in a round. 137 | 138 | Args: 139 | agent: An Agent2D instance. 140 | round: The current round. 141 | 142 | Returns: 143 | A formatted string containing the question. 144 | """ 145 | input = self._init_input.format(agent.position, agent.other_position) 146 | return input 147 | 148 | def _exp_postprocess(self): 149 | """Post-process the experiment data, including saving and 150 | generating visualizations.""" 151 | is_success, filename = self.save_record(self._output_file) 152 | if is_success: 153 | # Call functions to plot and generate HTML 154 | trajectory_file = self._output_file + '/trajectory.p' 155 | plot_xy(trajectory_file) 156 | video(trajectory_file) 157 | gen_html(filename, self._output_file) 158 | 159 | def _round_postprocess(self, simulation_ind, round, results, agents): 160 | """Post-process data at the end of each round of the simulation. 161 | 162 | Args: 163 | simulation_ind: Index of the simulation. 164 | round: The current round. 165 | results: Results data. 166 | agents: List of Agent2D instances. 167 | """ 168 | origin_result = [] 169 | for i in range(int(2 / self._dt)): 170 | for agent in agents: 171 | agent.move(self._dt) 172 | if i == int(2 / self._dt) - 1: 173 | origin_result.append(agent.position) 174 | for idx, agent in enumerate(agents): 175 | res_filtered = np.array(origin_result)[self._m[idx, :]] 176 | other_position = [tuple(x) for x in res_filtered] 177 | agent.other_position = other_position 178 | 179 | def _update_record(self, record, agent_contexts, simulation_ind, agents): 180 | """Update the experiment record with agent data. 181 | 182 | Args: 183 | record: Experiment record data. 184 | agent_contexts: Contexts of agents. 185 | simulation_ind: Index of the simulation. 186 | agents: List of Agent2D instances. 187 | """ 188 | record[tuple(tuple(pos) for pos in self._positions[simulation_ind])] = ( 189 | agent_contexts) 190 | self._trajectory['pos'][simulation_ind] = [agent.trajectory 191 | for agent in agents] 192 | self._trajectory['target'][simulation_ind] = [agent.target_trajectory 193 | for agent in agents] 194 | 195 | def save_record(self, output_dir: str): 196 | """Save the experiment record and agent trajectories. 197 | 198 | Args: 199 | output_dir: Directory where the data will be saved. 200 | 201 | Returns: 202 | A tuple (is_success, filename). 203 | """ 204 | res = super().save_record(output_dir) 205 | try: 206 | data_file_trajectory = output_dir + '/trajectory.p' 207 | pickle.dump(self._trajectory, open(data_file_trajectory, "wb")) 208 | except Exception as e: 209 | print("Error saving trajectory") 210 | pickle.dump(self._trajectory, open("trajectory.p", "wb")) 211 | return res -------------------------------------------------------------------------------- /modules/llm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindyLab/ConsensusLLM-code/a1012fd2a54b0de17e8d4febce3fd59b148fdbd8/modules/llm/__init__.py -------------------------------------------------------------------------------- /modules/llm/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import re 27 | from .gpt import GPT 28 | from ..prompt.summarize import summarizer_role 29 | from ..prompt.form import summarizer_output_form 30 | 31 | class Agent(GPT): 32 | """ 33 | A class representing an agent with position control. 34 | 35 | Args: 36 | position (float): Current position of the agent. 37 | other_position (list of float): Positions of other agents. 38 | key (str): API key for the GPT model. 39 | name (str): Name of the agent (optional). 40 | model (str): GPT model name (default is 'gpt-3.5-turbo-0613'). 41 | temperature (float): 42 | GPT temperature for text generation (default is 0.7). 43 | """ 44 | def __init__(self, position, other_position, key: str, name=None, 45 | model: str = 'gpt-3.5-turbo-0613', temperature: float = 0.7): 46 | super().__init__(key=key, model=model, temperature=temperature) 47 | self._name = name 48 | self._position = position # Current position of the agent 49 | self._other_position = other_position # Positions of other agents 50 | self._trajectory = [self.position] # Record the agent's movement trajectory 51 | self._summarizer = GPT(key=key, model="gpt-3.5-turbo-0613", 52 | keep_memory=False) 53 | self._summarize_result = "" 54 | self._summarizer_descriptions = summarizer_output_form 55 | self._summarizer.memories_update(role='system', content=summarizer_role) 56 | 57 | @property 58 | def name(self): 59 | return self._name 60 | 61 | @property 62 | def position(self): 63 | return self._position 64 | 65 | @position.setter 66 | def position(self, value): 67 | self._position = value 68 | 69 | @property 70 | def other_position(self): 71 | return self._other_position 72 | 73 | @other_position.setter 74 | def other_position(self, value): 75 | self._other_position = value 76 | 77 | @property 78 | def summarize_result(self): 79 | return self._summarize_result 80 | 81 | def answer(self, input, idx, round, simulation_ind, try_times=0) -> tuple: 82 | """ 83 | Generate an answer using the GPT model. 84 | 85 | Args: 86 | input (str): Input text or prompt. 87 | idx: Index. 88 | round: Round. 89 | simulation_ind: Simulation index. 90 | try_times (int): Number of times the answer generation is attempted. 91 | 92 | Returns: 93 | tuple: Index and the updated position of the agent. 94 | """ 95 | try: 96 | answer = self.generate_answer(input=input, try_times=try_times) 97 | self.position = self.parse_output(answer) 98 | return idx, self.position 99 | except Exception as e: 100 | try_times += 1 101 | if try_times < 3: 102 | print(f"An error occurred when agent {self._name} tried to " 103 | f"generate answers: {e},try_times: {try_times + 1}/3.") 104 | return self.answer(input=input, idx=idx, 105 | round=round, simulation_ind=simulation_ind, 106 | try_times=try_times) 107 | else: 108 | print("After three attempts, the error still remains " 109 | f"unresolved, the input is:\n'{input}'\n.") 110 | 111 | def summarize(self, agent_answers): 112 | """ 113 | Generate a summary of agent answers. 114 | 115 | Args: 116 | agent_answers (list): List of agent answers. 117 | """ 118 | if len(agent_answers) == 0: 119 | self._summarize_result = "" 120 | else: 121 | self._summarize_result = self._summarizer.generate_answer( 122 | self._summarizer_descriptions.format(agent_answers)) 123 | 124 | def parse_output(self, output): 125 | """ 126 | Parse the output for visualization. 127 | 128 | Args: 129 | output (str): Model's output. 130 | 131 | Returns: 132 | float: Parsed position value. 133 | """ 134 | matches = re.findall(r'[-+]?\d*\.\d+|\d+', output) 135 | if matches: 136 | x = float(matches[-1]) 137 | self._trajectory.append(x) 138 | return x 139 | else: 140 | raise ValueError(f"output: \n{output}\n can not be parsed") 141 | -------------------------------------------------------------------------------- /modules/llm/agent_2d.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import re 27 | import numpy as np 28 | from .gpt import GPT 29 | from ..prompt.summarize import summarizer_role 30 | from ..prompt.form import summarizer_output_form 31 | 32 | class Agent2D(GPT): 33 | """ 34 | A class representing a 2D agent with position control. 35 | 36 | Args: 37 | position (tuple): Current position of the agent (x, y). 38 | other_position (list of tuples): Positions of other agents. 39 | key (str): API key for the GPT model. 40 | name (str): Name of the agent (optional). 41 | model (str): GPT model name (default is 'gpt-3.5-turbo-0613'). 42 | temperature (float): 43 | GPT temperature for text generation (default is 0.7). 44 | keep_memory (bool): 45 | Whether to keep a memory of conversations (default is False). 46 | """ 47 | 48 | def __init__(self, position, other_position, key: str, name=None, 49 | model: str = 'gpt-3.5-turbo-0613', temperature: float = 0.7, 50 | keep_memory=False): 51 | super().__init__(key=key, model=model, temperature=temperature, 52 | keep_memory=keep_memory) 53 | self._name = name 54 | self._velocity = np.zeros(2) # Current velocity of the agent 55 | self._max_traction_force = 50 # Maximum traction force of the agent (N) 56 | self._max_velocity = 3 # Maximum velocity of the agent (m/s) 57 | self._m = 15 # Mass of the agent (kg) 58 | self._mu = 0.02 # Friction coefficient 59 | # PID Parameters 60 | self.Kp = np.array([1.2, 1.2], dtype=np.float64) 61 | self.Ki = np.array([0.0, 0.0], dtype=np.float64) 62 | self.Kd = np.array([6, 6], dtype=np.float64) 63 | self.prev_error = np.array([0, 0], dtype=np.float64) 64 | self.integral = np.array([0, 0], dtype=np.float64) 65 | self._target_position = None # Target position of the agent 66 | self._position = position # Current position of the agent 67 | self._other_position = other_position # Positions of other agents 68 | self._trajectory = [] # Record the agent's movement trajectory 69 | self._target_trajectory = [] # Record the agent's target trajectory 70 | self._summarizer = GPT(key=key, model="gpt-3.5-turbo-0613", 71 | keep_memory=False) 72 | self._summarize_result = "" 73 | self._summarizer_descriptions = summarizer_output_form 74 | self._summarizer.memories_update(role='system', content=summarizer_role) 75 | 76 | @property 77 | def name(self): 78 | return self._name 79 | 80 | @property 81 | def position(self): 82 | return self._position 83 | 84 | @position.setter 85 | def position(self, value): 86 | self._position = value 87 | 88 | @property 89 | def other_position(self): 90 | return self._other_position 91 | 92 | @property 93 | def trajectory(self): 94 | return self._trajectory 95 | 96 | @property 97 | def target_trajectory(self): 98 | return self._target_trajectory 99 | 100 | @property 101 | def target_position(self): 102 | return self._target_position 103 | 104 | @other_position.setter 105 | def other_position(self, value): 106 | self._other_position = value 107 | 108 | @property 109 | def summarize_result(self): 110 | return self._summarize_result 111 | 112 | def answer(self, input, idx, round, simulation_ind, try_times=0) -> tuple: 113 | """ 114 | Generate an answer using the GPT model. 115 | 116 | Args: 117 | input (str): Input text or prompt. 118 | idx: Index. 119 | round: Round. 120 | simulation_ind: Simulation index. 121 | try_times (int): Number of times the answer generation is attempted. 122 | 123 | Returns: 124 | tuple: Index and the target position (x, y). 125 | """ 126 | try: 127 | answer = self.generate_answer(input=input, try_times=try_times) 128 | self._target_position = self.parse_output(answer) 129 | self._target_trajectory.append(self._target_position) 130 | return idx, self._target_position 131 | except Exception as e: 132 | try_times += 1 133 | if try_times < 3: 134 | print(f"An error occurred when agent {self._name} tried to " 135 | f"generate answers: {e},try_times: {try_times + 1}/3.") 136 | return self.answer(input=input, idx=idx, round=round, 137 | simulation_ind=simulation_ind, 138 | try_times=try_times) 139 | else: 140 | print("After three attempts, the error still remains " 141 | f"unresolved, the input is:\n'{input}'\n.") 142 | return idx, self._target_position 143 | 144 | def summarize(self, agent_answers): 145 | """ 146 | Generate a summary of agent answers. 147 | 148 | Args: 149 | agent_answers (list): List of agent answers. 150 | """ 151 | if len(agent_answers) == 0: 152 | self._summarize_result = "" 153 | else: 154 | self._summarize_result = self._summarizer.generate_answer( 155 | self._summarizer_descriptions.format(agent_answers)) 156 | 157 | def parse_output(self, output): 158 | """ 159 | Parse the output for visualization. 160 | 161 | Args: 162 | output (str): Model's output. 163 | 164 | Returns: 165 | tuple: Parsed position value (x, y). 166 | """ 167 | matches = re.findall(r'\((.*?)\)', output) 168 | if matches: 169 | last_match = matches[-1] 170 | numbers = re.findall(r'[-+]?\d*\.\d+|\d+', last_match) 171 | if len(numbers) == 2: 172 | x = float(numbers[0]) 173 | y = float(numbers[1]) 174 | return (x, y) 175 | else: 176 | raise ValueError(f"The last match {last_match} does " 177 | "not contain exactly 2 numbers.") 178 | else: 179 | raise ValueError(f"No array found in the output: \n{output}") 180 | 181 | def move(self, time_duration: float): 182 | """ 183 | Move the agent based on PID control. 184 | 185 | Args: 186 | time_duration (float): Time duration for the movement. 187 | """ 188 | if self._target_position is None: 189 | print("Target not set!") 190 | return 191 | error = np.array(self._target_position) - np.array(self._position) 192 | self.integral += error * time_duration 193 | derivative = (error - self.prev_error) / time_duration 194 | force = self.Kp * error + self.Ki * self.integral + self.Kd * derivative 195 | force_magnitude = np.linalg.norm(force) 196 | if force_magnitude > self._max_traction_force: 197 | force = (force / force_magnitude) * self._max_traction_force 198 | # friction_force = -self._mu * self._m * 9.8 * np.sign(self._velocity) if abs( 199 | # np.linalg.norm(self._velocity)) > 0.1 else 0 200 | friction_force = 0 201 | net_force = force + friction_force 202 | acceleration = net_force / self._m 203 | self._velocity += acceleration * time_duration 204 | # Limit the velocity 205 | velocity_magnitude = np.linalg.norm(self._velocity) 206 | if velocity_magnitude > self._max_velocity: 207 | self._velocity = (self._velocity / velocity_magnitude) * self._max_velocity 208 | self._position += self._velocity * time_duration + 0.5 * acceleration * time_duration ** 2 209 | self._position = tuple(np.round(self._position, 2)) 210 | self.prev_error = error 211 | self._trajectory.append(self._position) 212 | # print(f"{self._name} position: {self._position}, " 213 | # f"target: {self._target_position}, velocity: {self._velocity}, " 214 | # f"force: {force}, friction_force: {friction_force}, " 215 | # f"net_force: {net_force}, acceleration: {acceleration}") 216 | return self._position -------------------------------------------------------------------------------- /modules/llm/api_key.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import openai 27 | import math 28 | import yaml 29 | 30 | # Load the configuration from the YAML file 31 | with open('./config/keys.yml', 'r') as config_file: 32 | config = yaml.safe_load(config_file) 33 | 34 | openai.api_base = config.get('api_base', '') 35 | api_keys_all = config.get('api_keys', {}) 36 | # User ID for which we need to slice the dictionary. 37 | user_id = 2 38 | # Total number of users among whom the dictionary needs to be distributed. 39 | user_count = 3 40 | 41 | # Calculate the number of keys each user should get. 42 | keys_per_user = math.ceil(len(api_keys_all) / user_count) 43 | 44 | # Calculate the starting and ending index for slicing the dictionary 45 | # for the given user_id. 46 | start = keys_per_user * user_id 47 | end = min(keys_per_user * (user_id + 1), len(api_keys_all)) 48 | print("user {}/{} ,api_key index start: {}, end: {}" 49 | .format(user_id, user_count, start, end)) 50 | # Slicing the dictionary based on the calculated start and end. 51 | api_keys = {i - start: v 52 | for i, (k, v) in enumerate(api_keys_all.items()) 53 | if start <= i < end} 54 | if __name__ == '__main__': 55 | print(api_keys) 56 | -------------------------------------------------------------------------------- /modules/llm/gpt.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import openai 27 | 28 | class GPT: 29 | """ 30 | Initialize the GPT class for interacting with OpenAI's GPT model. 31 | GPT provides basic methods for interacting with the model and parsing its 32 | output. 33 | """ 34 | 35 | def __init__(self, key: str, model: str = 'gpt-3.5-turbo-0613', 36 | temperature: float = 0.7, keep_memory: bool = True): 37 | """ 38 | Initialize the GPT class. 39 | 40 | Args: 41 | key (str): OpenAI API key. 42 | model (str): The model to use (default: gpt-3.5-turbo-0613). 43 | temperature (float): Temperature for text generation (default: 0.7). 44 | keep_memory (bool): Whether to retain memories (default: True). 45 | """ 46 | self._model = model 47 | self._openai_key = key 48 | self._cost = 0 49 | self._memories = [] 50 | self._keep_memory = keep_memory 51 | self._temperature = temperature 52 | self._history = [] 53 | 54 | def get_memories(self): 55 | """ 56 | Get the current memories. 57 | 58 | Returns: 59 | list: List of memories. 60 | """ 61 | return self._memories 62 | 63 | def get_history(self): 64 | """ 65 | Get the conversation history. 66 | 67 | Returns: 68 | list: List of conversation history. 69 | """ 70 | return self._history 71 | 72 | def memories_update(self, role: str, content: str): 73 | """ 74 | Update memories to set roles (system, user, assistant) and content, 75 | forming a complete memory. 76 | 77 | Args: 78 | role (str): Role (system, user, assistant). 79 | content (str): Content. 80 | 81 | Raises: 82 | ValueError: If an unrecognized role is provided or if roles are 83 | added in an incorrect sequence. 84 | """ 85 | if role not in ["system", "user", "assistant"]: 86 | raise ValueError(f"Unrecognized role: {role}") 87 | 88 | if role == "system" and len(self._memories) > 0: 89 | raise ValueError('System role can only be added when memories are ' 90 | 'empty') 91 | if (role == "user" and len(self._memories) > 0 and 92 | self._memories[-1]["role"] == "user"): 93 | raise ValueError('User role can only be added if the previous ' 94 | 'round was a system or assistant role') 95 | if (role == "assistant" and len(self._memories) > 0 and 96 | self._memories[-1]["role"] != "user"): 97 | raise ValueError('Assistant role can only be added if the previous ' 98 | 'round was a user role') 99 | self._memories.append({"role": role, "content": content}) 100 | self._history.append({"role": role, "content": content}) 101 | 102 | def generate_answer(self, input: str, try_times=0, **kwargs) -> str: 103 | """ 104 | Interact with the GPT model and generate an answer. 105 | 106 | Args: 107 | input (str): Prompt or user input. 108 | try_times (int): Number of attempts (default is 0). 109 | kwargs: Additional parameters for the model. 110 | 111 | Returns: 112 | str: Text-based output result. 113 | 114 | Raises: 115 | ConnectionError: If there's an error in generating the answer. 116 | """ 117 | if not self._keep_memory: 118 | self._memories = [self._memories[0]] 119 | 120 | if try_times == 0: 121 | self._memories.append({"role": "user", "content": input}) 122 | self._history.append({"role": "user", "content": input}) 123 | else: 124 | if self._memories[-1]["role"] == "assistant": 125 | self._memories = self._memories[:-1] 126 | 127 | openai.api_key = self._openai_key 128 | 129 | try: 130 | response = openai.ChatCompletion.create( 131 | model=self._model, 132 | messages=self._memories, 133 | temperature=self._temperature, 134 | **kwargs 135 | ) 136 | self._cost += response['usage']["total_tokens"] 137 | content = response['choices'][0]['message']['content'] 138 | self._memories.append({"role": "assistant", "content": content}) 139 | self._history.append({"role": "assistant", "content": content}) 140 | return content 141 | except Exception as e: 142 | raise ConnectionError(f"Error in generate_answer: {e}") 143 | -------------------------------------------------------------------------------- /modules/llm/role.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | names = [ 27 | "Alice", "Bob", "Charlie", "David", "Emily", "Frank", "Grace", "Helen", 28 | "Ivy", "Jack", "Karen", "Leo", "Mandy", "Nina", "Oscar", "Paul", "Quincy", 29 | "Rita", "Steve", "Tina", "Ursula", "Vera", "Will", "Xena", "Yara", "Zack", 30 | "Anna", "Bill", "Cathy", "Derek", "Elise", "Finn", "Gloria", "Harry", 31 | "Isabel", "Jake", "Katie", "Liam", "Mona", "Nick", "Olivia", "Peter", 32 | "Queen", "Rachel", "Sam", "Tracy", "Ulysses", "Vicky", "Walter", "Xander", 33 | "Yvonne", "Zeus", "Amy", "Brian", "Clara", "Dean", "Eva", "Fred", "Gina", 34 | "Henry", "Iris", "John", "Kelly", "Luke", "Maria", "Nate", "Owen", "Pam", 35 | "Quinn", "Rose", "Sara", "Tom", "Una", "Victor", "Wendy", "Xavier", 36 | "Yasmine", "Zara", "Alan", "Beth", "Chris", "Diana", "Erik", "Faye", 37 | "George", "Holly", "Ian", "Julia", "Ken", "Laura", "Mike", "Nora", "Otis", 38 | "Penny", "Quinton", "Rebecca", "Sid", "Tara", "Uma", "Vince", "Wanda", 39 | "Xerxes", "Yoshi", "Zoe", 40 | ] -------------------------------------------------------------------------------- /modules/prompt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindyLab/ConsensusLLM-code/a1012fd2a54b0de17e8d4febce3fd59b148fdbd8/modules/prompt/__init__.py -------------------------------------------------------------------------------- /modules/prompt/form.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | agent_output_form = '''Strictly follow the 'Reasoning:..., Position:...' format to provide your answer. 27 | In the 'Reasoning' section, it is your thought process, while the 'position' section is only the location you wish to move to in this round, without any further explanation needed. 28 | ''' 29 | 30 | summarizer_output_form = '''Read the text below:\n'{}', extract the positions each player chose in the last round and present it in the format 'player ...: ...'. 31 | Finally, provide a summary of all players' strategies and thinking.''' 32 | -------------------------------------------------------------------------------- /modules/prompt/personality.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | personality = ["", 27 | "", 28 | "You are an extremely stubborn person, prefer to remain stationary.", 29 | "You are an extremely suggestible person, prefer to move to someone else's position.", 30 | "You are a person who is extremely susceptible to the influence of others, prefer to move to someone else's position.", 31 | "You are very selfish.", 32 | "You are very selfless.", 33 | "You are very selfless, willing to consider others' needs.", 34 | "You are very selfish, only considering your own interests.", 35 | "Your movement speed is very slow.", 36 | "Your movement speed is very fast.", 37 | ] 38 | 39 | personality_list = personality[0:2] 40 | stubborn = personality[2] 41 | suggestible = personality[3] 42 | 43 | 44 | __all__ = ['personality_list', 'stubborn', 'suggestible'] -------------------------------------------------------------------------------- /modules/prompt/scenario.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | agent_role = ('You are an agent moving in a one-dimensional space.') 27 | 28 | # game_description = """"Another agent is present in the space, and you need to gather. Your position is: {} and the other agent's position is: {}." 29 | # You need to choose a position to move to in order to gather, and briefly explain the reasoning behind your decision. 30 | # """ 31 | 32 | # round_description = """You have moved to {}, and the latest position of another agent is: {}., 33 | # please choose the position you want to move to next. 34 | # """ 35 | # agent_role = 'You are an agent moving in a one-dimensional space.' 36 | # 37 | game_description = """There are many other agents in the space, you all need to gather at the same position, your position is: {}, other people's positions are: {}. 38 | You need to choose a position to move to in order to gather, and briefly explain the reasoning behind your decision. 39 | """ 40 | 41 | round_description = """You have now moved to {}, the positions of other agents are {}, 42 | please choose the position you want to move to next. 43 | """ 44 | -------------------------------------------------------------------------------- /modules/prompt/scenario_2d.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | agent_role = 'You are a robot moving in a two-dimensional space.' 27 | 28 | game_description = """There are many other robots in the space. You all need to gather at the same position. Your position is: {}, and the positions of others are: {}. 29 | Choose a position to move to in order to gather, and briefly explain the reasoning behind your decision. 30 | """ 31 | 32 | round_description = """You have now moved to {}. The positions of other robots are {}. 33 | Please choose the next position you want to move to. 34 | """ -------------------------------------------------------------------------------- /modules/prompt/summarize.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | summarizer_role = 'You are someone who is skilled at discerning patterns from text, extracting key information, and is sensitive to numbers within 100.' 27 | -------------------------------------------------------------------------------- /modules/visual/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindyLab/ConsensusLLM-code/a1012fd2a54b0de17e8d4febce3fd59b148fdbd8/modules/visual/__init__.py -------------------------------------------------------------------------------- /modules/visual/box_plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import random 27 | import matplotlib.pyplot as plt 28 | import numpy as np 29 | import sys 30 | import os 31 | import glob 32 | from .read_data import read_from_file 33 | 34 | def extract_data_from_file(file): 35 | """ 36 | Extract data from a file. 37 | 38 | Args: 39 | file (str): The file path to read data from. 40 | 41 | Returns: 42 | numpy.ndarray: A NumPy array containing the extracted data. 43 | 44 | Raises: 45 | Exception: If an error occurs during data extraction. 46 | 47 | This function reads data from a file, filters it, calculates bias, and 48 | returns a NumPy array. 49 | """ 50 | results = read_from_file(file) 51 | try: 52 | # Filter results ensuring each inner list is of length 10 and contains no None values 53 | filtered_results = [ 54 | mid_list for mid_list in results 55 | if all(len(inner_list) == 10 for inner_list in mid_list) 56 | and all(item is not None 57 | for inner_list in mid_list 58 | for item in inner_list) 59 | ] 60 | 61 | # Convert the filtered results to a numpy array for further processing 62 | filtered_results = np.array(filtered_results) 63 | 64 | except Exception as e: 65 | print('ERROR:--' + file) 66 | return [] 67 | 68 | bias = [] 69 | # Process each agent's results 70 | for agent_results in filtered_results: 71 | # Extract the last value from each result and sort them 72 | last_values = sorted([res[-1] for res in agent_results]) 73 | 74 | # Calculate the mean difference between consecutive sorted last values 75 | differences = np.mean([last_values[i + 1] - last_values[i] 76 | for i in range(len(last_values) - 1)]) 77 | 78 | # If the mean difference is less than 1, compute the bias for each result of the agent 79 | if differences < 1: 80 | agent_bias = [res[-1] - res[0] for res in agent_results] 81 | bias.append(agent_bias) 82 | 83 | # Calculate the mean bias for all agents 84 | data = np.mean(bias, axis=1) 85 | return data 86 | 87 | def get_data_files(dir, directory_pattern): 88 | """ 89 | Get data files from a directory based on a directory pattern. 90 | 91 | Args: 92 | dir (str): The directory to search for data files. 93 | directory_pattern (str): The pattern to match subdirectories. 94 | 95 | Returns: 96 | list: A list of file paths. 97 | 98 | This function searches for data files in a directory based on the provided 99 | pattern and returns their paths. 100 | """ 101 | file_paths = [] 102 | for directory in glob.glob(os.path.join(dir, directory_pattern)): 103 | data_file_path = os.path.join(directory, 'data.p') 104 | if os.path.isfile(data_file_path): 105 | file_paths.append(data_file_path) 106 | return file_paths 107 | 108 | def extract_data_from_files(files): 109 | """ 110 | Extract data from a list of data files. 111 | 112 | Args: 113 | files (list): A list of data file paths. 114 | 115 | Returns: 116 | list: A list of extracted data. 117 | 118 | This function extracts data from a list of data files and accumulates it, 119 | returning the extracted data. 120 | """ 121 | data_list = [] 122 | 123 | for file in files: 124 | data_list.extend(extract_data_from_file(file)) 125 | 126 | # Check if the length of the accumulated data list is at least 300 127 | if len(data_list) >= 300: 128 | print(len(data_list)) 129 | return data_list 130 | 131 | # Create a data structure to store results 132 | def plot_result(data): 133 | """ 134 | Plot the results as a box plot. 135 | 136 | Args: 137 | data (list): A list of data to be plotted. 138 | 139 | This function creates a box plot to visualize the data. 140 | """ 141 | plt.boxplot(data, labels=['2 Agents', '3 Agents', '4 Agents', '5 Agents'], 142 | patch_artist=True, 143 | boxprops=dict(facecolor='cyan', color='black'), 144 | whiskerprops=dict(color='black'), 145 | capprops=dict(color='black'), 146 | medianprops=dict(color='red')) 147 | 148 | plt.xlabel('Agents number') 149 | plt.ylabel('Values') 150 | plt.title('Box Plot Example') 151 | plt.axhline(y=0, color='red', linestyle='--', alpha=0.2) 152 | plt.show() 153 | 154 | def plot_combined_results(data1, data2): 155 | """ 156 | Plot combined results as box plots with scatter plots. 157 | 158 | Args: 159 | data1 (list): Data for the first set of box plots. 160 | data2 (list): Data for the second set of box plots. 161 | 162 | This function creates combined box plots with scatter plots to compare 163 | two datasets. 164 | """ 165 | fig, ax = plt.subplots(figsize=(8, 4.5)) 166 | 167 | # Define colors 168 | color_primary_full = (246 / 255, 111 / 255, 106 / 255, 1) 169 | color_secondary_full = (4 / 255, 183 / 255, 188 / 255, 1) 170 | color_primary_translucent = (246 / 255, 111 / 255, 106 / 255, 0.7) 171 | color_secondary_translucent = (4 / 255, 183 / 255, 188 / 255, 0.7) 172 | 173 | # Define box properties 174 | box_properties_data1 = dict(facecolor=color_secondary_translucent, 175 | color='black') 176 | box_properties_data2 = dict(facecolor=color_primary_translucent, 177 | color='black') 178 | 179 | # Plot boxplots for data1 180 | box_plot_data1 = ax.boxplot( 181 | data1, 182 | positions=np.array(range(len(data1))) * 2.0 - 0.4, 183 | patch_artist=True, 184 | boxprops=box_properties_data1, 185 | whiskerprops=dict(color='black'), 186 | capprops=dict(color='black'), 187 | medianprops=dict(color='red'), 188 | widths=0.6, showfliers=False) 189 | 190 | # Plot boxplots for data2 191 | box_plot_data2 = ax.boxplot( 192 | data2, 193 | positions=np.array(range(len(data2))) * 2.0 + 0.4, 194 | patch_artist=True, 195 | boxprops=box_properties_data2, 196 | whiskerprops=dict(color='black'), 197 | capprops=dict(color='black'), 198 | medianprops=dict(color='red'), 199 | widths=0.6, showfliers=False) 200 | 201 | # Scatter plot for data1 202 | for i, data in enumerate(data1): 203 | y = data 204 | x = np.random.normal(i * 2.0 - 0.4, 0.030, len(y)) 205 | ax.scatter(x, y, alpha=0.5, edgecolor='black', 206 | facecolor=color_secondary_full, s=15, zorder=2) 207 | 208 | # Scatter plot for data2 209 | for i, data in enumerate(data2): 210 | y = data 211 | x = np.random.normal(i * 2.0 + 0.4, 0.030, len(y)) 212 | ax.scatter(x, y, alpha=0.5, edgecolor='black', 213 | facecolor=color_primary_full, s=15, zorder=2) 214 | 215 | # Set x and y axis labels and ticks 216 | ax.set_xticks(np.array(range(len(data1))) * 2.0) 217 | ax.set_xticklabels(['2', '4', '6', '8']) 218 | ax.set_xlabel('Agent number', fontsize=13) 219 | ax.set_ylabel('Bias', fontsize=13) 220 | 221 | -------------------------------------------------------------------------------- /modules/visual/gen_html.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | from modules.visual.util import render_conversations_to_html 27 | from modules.visual.read_data import read_from_file, read_conversations 28 | import os 29 | import sys 30 | 31 | def gen_html(data_path, html_dir): 32 | """ 33 | Generate HTML output for conversations. 34 | 35 | Args: 36 | data_path (str): The path to the data file. 37 | html_dir (str): The directory to save the generated HTML files. 38 | 39 | Generates HTML output for the conversations and saves them in the 40 | specified directory. 41 | """ 42 | results = read_conversations(data_path) 43 | 44 | for ind, res in enumerate(results): 45 | output_file = os.path.join(html_dir, f'simulation_{ind}.html') 46 | if os.path.exists(output_file): 47 | continue 48 | try: 49 | render_conversations_to_html(res, output_file, ind) 50 | print(f'HTML output has been written to {output_file}') 51 | except: 52 | continue 53 | 54 | if __name__ == "__main__": 55 | log_directory = os.path.dirname( 56 | os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 57 | 58 | category = 'log/scalar_debate/n_agents3_rounds9_n_exp3_2023-10-07_16-38.p' 59 | directory_path = os.path.join(log_directory, category) 60 | 61 | files = [os.path.join(directory_path, file) 62 | for file in os.listdir(directory_path) 63 | if os.path.isfile(os.path.join(directory_path, file))] 64 | 65 | for file in files: 66 | if file.endswith(".p"): 67 | gen_html(file, directory_path) 68 | -------------------------------------------------------------------------------- /modules/visual/plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import re 27 | import matplotlib.pyplot as plt 28 | from matplotlib.ticker import MaxNLocator 29 | import numpy as np 30 | import sys 31 | import os 32 | from .read_data import read_from_file 33 | 34 | # Function to plot a single case 35 | def plot_single(data_path, pic_dir, name): 36 | """ 37 | Plot a single case's data. 38 | 39 | Args: 40 | data_path (str): Path to the data file. 41 | pic_dir (str): Directory to save the resulting plot. 42 | name (str): Name for the resulting plot file. 43 | """ 44 | plt.figure(figsize=(6.4, 3.0)) 45 | 46 | n_stubborn = 0 47 | n_suggestible = 0 48 | match = re.search(r'\((\d+),(\d+)\)', data_path) 49 | match_1 = re.search(r'case(\d+)', data_path) 50 | ind = int(match_1.group(1)) - 1 51 | if match: 52 | n_stubborn = int(match.group(1)) 53 | n_suggestible = int(match.group(2)) 54 | results = read_from_file(data_path) 55 | agent_count = len(results[0]) 56 | 57 | round_values = [res[0] for res in results[ind]] 58 | average_round0 = np.mean(round_values) 59 | 60 | ax = plt.gca() 61 | 62 | # Customize axis properties 63 | ax.tick_params(axis='both', which='major', labelsize=11) 64 | for spine in ax.spines.values(): 65 | spine.set_linewidth(1.5) 66 | 67 | # Plot data 68 | for agent_id, res in enumerate(results[ind]): 69 | if agent_id < n_stubborn: 70 | label = f'Agent {agent_id + 1}:stubborn' 71 | elif agent_id < n_stubborn + n_suggestible: 72 | label = f'Agent {agent_id + 1}:suggestible' 73 | else: 74 | label = f'Agent {agent_id + 1}' 75 | 76 | alpha_value = 1 - (1 - 0.4) / (agent_count - 1) * agent_id 77 | plt.plot(res, label=label, marker='o', markersize=3, 78 | alpha=alpha_value, linewidth=1.5) 79 | 80 | # Plot aesthetics 81 | plt.axhline(average_round0, color='red', linestyle='--', 82 | linewidth=0.5, label='Average value') 83 | for round_num in range(1, len(results[0][0])): 84 | plt.axvline(round_num, color='gray', linestyle='--', 85 | linewidth=0.5) 86 | 87 | plt.xlabel('Round') 88 | plt.ylabel('Agent state') 89 | plt.ylim(0, 100) 90 | plt.xlim(0, len(res)) 91 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 92 | # plt.xticks(fontsize=20) 93 | # plt.yticks(fontsize=20) 94 | # Legend handling 95 | if agent_count >= 8: 96 | legend = plt.legend(loc='center left', bbox_to_anchor=(1.05, 0.5), fontsize='small') 97 | else: 98 | # legend = plt.legend(loc='upper right', ) 99 | # legend = plt.legend(loc='lower right', ) 100 | # legend = plt.legend(loc='center right', ) 101 | # legend = plt.legend(fontsize='25') 102 | legend = plt.legend() 103 | 104 | plt.tight_layout() 105 | frame = legend.get_frame() 106 | frame.set_alpha(0.75) 107 | # frame.set_facecolor() 108 | 109 | plt.savefig(pic_dir + f'/svg/result_{name}.svg') 110 | plt.show() 111 | 112 | 113 | # Create a data structure to store results 114 | def plot_result(data_path, pic_dir): 115 | results = read_from_file(data_path) 116 | E = len(results) 117 | N = len(results[0]) # Number of agents 118 | R = len(results[0][0]) # Number of rounds 119 | print(E, N, R) 120 | 121 | fig, axes = plt.subplots(nrows=3, ncols=3, figsize=(12, 9)) 122 | for eval_id, agent_results in enumerate(results): 123 | row = eval_id // 3 # Determine the row for the subplot 124 | col = eval_id % 3 # Determine the co lumn for the subplot 125 | ax = axes[row, col] 126 | round0_values = [res[0] for res in agent_results] 127 | average_round0 = np.mean(round0_values) 128 | for agent_id, res in enumerate(agent_results): 129 | ax.plot(range(0, len(res)), res, label=f'Agent {agent_id + 1}', 130 | marker='o', markersize=3, 131 | alpha=1 - (1-0.4)/(len(agent_results)-1)*agent_id) 132 | ax.axhline(average_round0, color='red', linestyle='--', 133 | linewidth=0.5, label='Average value') 134 | ax.set_title(f'Case {eval_id + 1}') 135 | ax.set_xlabel('Round') 136 | ax.set_ylabel('Agent state') 137 | ax.set_ylim(0, 100) 138 | ax.set_xlim(0, len(res) - 1) 139 | ax.xaxis.set_major_locator(MaxNLocator(integer=True)) 140 | ax.legend() 141 | 142 | # Add vertical dashed lines for each round to all subplots 143 | for ax in axes.flatten(): 144 | for round_num in range(1, R): 145 | ax.axvline(round_num, color='gray', linestyle='--', linewidth=0.5) 146 | 147 | # Adjust layout to prevent subplot overlap 148 | plt.tight_layout() 149 | # plt.savefig(pic_dir + '/result.svg', format='svg') 150 | plt.savefig(pic_dir + '/result.png') 151 | # Show the plot 152 | plt.show() 153 | 154 | if __name__ == '__main__': 155 | file = sys.argv[1] 156 | name = sys.argv[2] 157 | # directories = [d for d in os.listdir(directory_path) if os.path.isdir(os.path.join(directory_path, d))] 158 | 159 | # for file in directories: 160 | # # print(directory_path+"\\"+file+"\data.p") 161 | # plot_result(directory_path+"\\"+file +"\data.p", directory_path+"\\"+file) 162 | # print(os.path.dirname(file)) 163 | # plot_result(file, os.path.dirname(file)) 164 | plot_single(file, os.path.dirname(file), name) 165 | -------------------------------------------------------------------------------- /modules/visual/plot_2d.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import os 27 | import pickle 28 | import sys 29 | 30 | import matplotlib.pyplot as plt 31 | import numpy as np 32 | from matplotlib.animation import FuncAnimation 33 | 34 | # Define a color palette for plotting 35 | colors = np.array([ 36 | [34, 115, 174], 37 | [74, 128, 107], 38 | [241, 163, 94], 39 | [158, 13, 52], 40 | ]) / 255 41 | 42 | def read_from_file(path): 43 | """ 44 | Read data from a binary file. 45 | 46 | Args: 47 | path (str): The path to the binary file. 48 | 49 | Returns: 50 | object: The data loaded from the file. 51 | """ 52 | with open(path, 'rb') as f: 53 | return pickle.load(f) 54 | 55 | def plot_xy(data_path): 56 | """ 57 | Plot the x and y coordinates of robots' trajectories. 58 | 59 | Args: 60 | data_path (str): The path to the data file containing trajectory data. 61 | """ 62 | data = read_from_file(data_path) 63 | all_positions = np.array(data['pos'][0]) 64 | all_targets = np.array(data['target'][0]) 65 | 66 | num_robots, num_points, _ = all_positions.shape 67 | num_targets = all_targets.shape[1] 68 | 69 | multiple = num_points // num_targets 70 | replicated_targets = np.repeat(all_targets, multiple, axis=1) 71 | 72 | round_time = np.arange(0, num_points * 0.1, 0.1) 73 | 74 | # Create subplots for each robot's trajectory 75 | fig, axs = plt.subplots(2, num_robots, figsize=(9, 4)) 76 | coord_labels = ['x', 'y'] 77 | 78 | for i in range(num_robots): 79 | for j, coord in enumerate(coord_labels): 80 | axs[j, i].set_xlim(0, 40) 81 | axs[j, i].tick_params(axis='both', labelsize=7) 82 | axs[j, i].plot(round_time, all_positions[i, :, j], 83 | color=colors[i], linestyle='-', linewidth=1, 84 | label="Actual") 85 | axs[j, i].plot(round_time, replicated_targets[i, :, j], 86 | color=colors[i], linestyle='--', linewidth=1, 87 | label="Planned") 88 | axs[j, i].set_title(f"Robot {i + 1}", fontsize=9) 89 | if i == 0: 90 | axs[j, i].set_ylabel(coord + ' (m)', fontsize=9) 91 | if coord == 'x': 92 | axs[j, i].legend(fontsize=7) 93 | 94 | plt.tight_layout() 95 | plt.savefig(os.path.join(os.path.dirname(data_path), 'trajectory.svg')) 96 | plt.show() 97 | 98 | def video(data_path): 99 | """ 100 | Create an animation of robot trajectories. 101 | 102 | Args: 103 | data_path (str): The path to the data file containing trajectory data. 104 | """ 105 | data = read_from_file(data_path) 106 | fig, ax = plt.subplots(figsize=(8, 4)) 107 | lines = [] 108 | dashed_lines = [] 109 | scatters = [] 110 | start_scatters = [] 111 | 112 | for idx in range(len(data['pos'][0])): 113 | line, = ax.plot([], [], lw=2, color=colors[idx], 114 | label=f'Robot {idx + 1} trajectory') 115 | dashed_line, = ax.plot([], [], lw=2, linestyle='--', 116 | alpha=0.5, color=colors[idx]) 117 | scatter = ax.scatter([], [], marker='o', 118 | c=colors[idx].reshape(1, -1), s=50) 119 | start_pos = data['pos'][0][idx][0] 120 | start_scatter = ax.scatter(start_pos[0], start_pos[1], alpha=0.5, 121 | c=colors[idx].reshape(1, -1), s=100, 122 | marker='o', 123 | label=f'Robot {idx + 1} initial position') 124 | lines.append(line) 125 | dashed_lines.append(dashed_line) 126 | scatters.append(scatter) 127 | start_scatters.append(start_scatter) 128 | mean_start_x = np.array([data['pos'][0][idx][0][0] 129 | for idx in range(len(data['pos'][0]))]).mean() 130 | mean_start_y = np.array([data['pos'][0][idx][0][1] 131 | for idx in range(len(data['pos'][0]))]).mean() 132 | mean_start_scatter = ax.scatter([], [], c=colors[-1].reshape(1, -1), 133 | marker='$*$', s=100, 134 | label="Average initial position") 135 | mean_start_scatter.set_offsets([mean_start_x, mean_start_y]) 136 | 137 | def init(): 138 | ax.set_xlabel('x (m)') 139 | ax.set_ylabel('y (m)') 140 | ax.set_ylim(0, 80) 141 | ax.set_xticks(range(-20, 130, 10)) 142 | for line, dashed_line, scatter in zip(lines, dashed_lines, scatters): 143 | line.set_data([], []) 144 | dashed_line.set_data([], []) 145 | scatter.set_offsets(np.empty((0, 2))) 146 | handles, labels = ax.get_legend_handles_labels() 147 | ax.legend(handles=handles, labels=labels, loc="upper left", 148 | labelspacing=0.6, fontsize=10) 149 | return lines + dashed_lines + scatters + start_scatters 150 | 151 | def animate(i): 152 | for idx, (line, dashed_line, scatter) in enumerate(zip(lines, dashed_lines, scatters)): 153 | all_positions = [] 154 | for key in data['pos']: 155 | all_positions.extend(data['pos'][key][idx]) 156 | line.set_data([x for x, y in all_positions[:i + 1]], 157 | [y for x, y in all_positions[:i + 1]]) 158 | target_key = max(0, i - 20) // 20 159 | start_x, start_y = all_positions[i] 160 | target_x, target_y = data['target'][0][idx][target_key] 161 | dashed_line.set_data([start_x, target_x], [start_y, target_y]) 162 | scatter.set_offsets([start_x, start_y]) 163 | 164 | if i == len(data['pos'][0][0]) - 1: 165 | img_output_path = os.path.join(os.path.dirname(data_path), 166 | 'last_frame.svg') 167 | plt.savefig(img_output_path, bbox_inches='tight') 168 | return lines + dashed_lines + scatters 169 | 170 | output_path = os.path.join(os.path.dirname(data_path), 'animation.gif') 171 | ani = FuncAnimation(fig, animate, frames=len(data['pos'][0][0]), 172 | init_func=init, blit=False) 173 | ani.save(output_path, fps=20) 174 | plt.show() 175 | 176 | if __name__ == '__main__': 177 | data_path = sys.argv[1] 178 | plot_xy(data_path) 179 | video(data_path) -------------------------------------------------------------------------------- /modules/visual/read_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import pickle 27 | import re 28 | 29 | def parse_answer(sentence): 30 | """ 31 | Parses a sentence to extract a floating-point number. 32 | 33 | Args: 34 | sentence (str): The input sentence to parse. 35 | 36 | Returns: 37 | float or None: The last floating-point number found in the sentence, 38 | or None if none is found. 39 | """ 40 | floats = re.findall(r'[-+]?\d*\.\d+|\d+', sentence) 41 | if floats: 42 | return float(floats[-1]) 43 | else: 44 | return None 45 | 46 | def parse_p_file(filename): 47 | """ 48 | Parses a Pickle file and returns its content. 49 | 50 | Args: 51 | filename (str): The name of the Pickle file to parse. 52 | 53 | Returns: 54 | object: The content of the Pickle file. 55 | """ 56 | objects = [] 57 | with open(filename, "rb") as openfile: 58 | while True: 59 | try: 60 | objects.append(pickle.load(openfile)) 61 | except EOFError: 62 | break 63 | return objects[0] 64 | 65 | def read_conversations(filename): 66 | """ 67 | Reads conversations from a Pickle file and extracts them. 68 | 69 | Args: 70 | filename (str): The name of the Pickle file containing conversations. 71 | 72 | Returns: 73 | list: A list of conversation objects. 74 | """ 75 | object = parse_p_file(filename) 76 | conversations = [value for key, value in object.items()] 77 | return conversations 78 | 79 | def read_from_file(filename): 80 | """ 81 | Reads and extracts data from a Pickle file containing text conversations. 82 | 83 | Args: 84 | filename (str): The name of the Pickle file containing text 85 | conversations. 86 | 87 | Returns: 88 | list: A list of text answers extracted from the file. 89 | """ 90 | object = parse_p_file(filename) 91 | final_ans = [] 92 | count = 0 93 | for key, value in object.items(): 94 | text_answers = [] 95 | agent_contexts = value 96 | count += 1 97 | for agent_id, agent_context in enumerate(agent_contexts): 98 | ans = [key[agent_id]] 99 | for i, msg in enumerate(agent_context): 100 | if i > 0 and i % 2 == 0: 101 | text_answer = agent_context[i]['content'] 102 | text_answer = text_answer.replace(",", ".") 103 | text_answer = parse_answer(text_answer) 104 | ans.append(text_answer) 105 | text_answers.append(ans) 106 | final_ans.append(text_answers) 107 | return final_ans 108 | 109 | if __name__ == "__main__": 110 | res = """Based on the advice of your two friends, the position to meet your 111 | friend is 65.5, which is the midpoint of your position (64) and your 112 | friend's position (67). Therefore, the position to meet your 113 | friend is 65..""" 114 | print(parse_answer(res)) 115 | -------------------------------------------------------------------------------- /modules/visual/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) [2023] [Intelligent Unmanned Systems Laboratory at 5 | Westlake University] 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 22 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | def render_conversations_to_html(conversations, output_file, simulation_ind): 27 | """ 28 | Render conversations to an HTML file. 29 | 30 | Args: 31 | conversations (list): List of conversation data. 32 | output_file (str): The path to the output HTML file. 33 | simulation_ind (int): Index of the simulation. 34 | 35 | The function takes conversation data and generates an HTML file displaying the conversations. 36 | """ 37 | # Number of agents 38 | num_agents = len(conversations) 39 | 40 | # Determine if scrolling is needed 41 | enable_scroll = num_agents > 1 42 | 43 | # Define CSS styles for avatars and chat boxes 44 | css_styles = ''' 45 | .avatar {{ 46 | width: 50px; 47 | height: 50px; 48 | border-radius: 50%; 49 | margin-right: 10px; 50 | float: left; /* Align avatars to the left */ 51 | }} 52 | .chat-box {{ 53 | background-color: #f1f0f0; 54 | padding: 10px; 55 | margin: 5px; 56 | border-radius: 10px; 57 | display: block; 58 | clear: both; /* Clear the float to prevent overlapping */ 59 | word-wrap: break-word; /* This property wraps long words and text to the next line */ 60 | }} 61 | 62 | .user {{ 63 | background-color: #f0f0f0; 64 | color: #222; 65 | }} 66 | 67 | .assistant {{ 68 | background-color: #3498db; 69 | color: #fff; 70 | }} 71 | 72 | .conversation-container {{ 73 | display: grid; 74 | grid-template-columns: repeat({num_agents}, 1fr); /* Create columns for each agent */ 75 | grid-gap: 20px; /* Gap between columns */ 76 | margin-bottom: 20px; /* Add margin between conversation groups */ 77 | }} 78 | 79 | .conversation-title {{ 80 | grid-column: span {num_agents}; 81 | font-weight: bold; 82 | text-align: center; 83 | padding-bottom: 20px; 84 | }} 85 | 86 | .agent-conversation {{ 87 | grid-column: span 1; 88 | padding-right: 40px; 89 | }} 90 | 91 | .agent-messages {{ 92 | display: flex; 93 | flex-direction: row; /* Chat boxes displayed vertically */ 94 | }} 95 | '''.format(num_agents=num_agents) 96 | 97 | messages_in_line = [[row[i] for row in conversations] for i in range(len(conversations[0]))] 98 | agent_tiles = ["Agent {} of Case {}".format(ind + 1, simulation_ind + 1) for ind in 99 | range(len(conversations))] 100 | messages_in_line = [agent_tiles] + messages_in_line 101 | # Initialize the HTML string 102 | html = ''.format(css_styles) 103 | 104 | # Create a container for all agents' conversations 105 | if enable_scroll: 106 | html += '
' 107 | else: 108 | html += '
' 109 | 110 | for ind, msgs_in_row in enumerate(messages_in_line): 111 | html += '
' 112 | html += '
' 113 | # Create a container for agent's conversation 114 | for message in msgs_in_row: 115 | html += '
' 116 | 117 | if (ind == 0): 118 | html += '
{}
'.format(message) 119 | else: 120 | role = message["role"] 121 | content = message["content"] 122 | 123 | # Define avatars for user and assistant 124 | user_avatar = '../images/user.png' # Replace with the actual path or URL 125 | assistant_avatar = '../images/robot.jpg' 126 | # print(ind % 3, role, assistant_avatar) 127 | # Determine the current avatar based on the role 128 | 129 | current_avatar = user_avatar if role in ["system", "user"] else assistant_avatar 130 | 131 | # Add the avatar image element 132 | avatar_element = ''.format(current_avatar) 133 | 134 | # Add the chat box with the avatar and content 135 | chat_box = '
{}
'.format(role, content) 136 | 137 | # Create a container for agent's messages 138 | agent_messages = '
{}
'.format(avatar_element + chat_box) 139 | 140 | # Add the agent's messages to the agent's conversation 141 | html += agent_messages 142 | # Close the agent's conversation container 143 | html += '
' 144 | 145 | # Close the conversation container 146 | html += '
' 147 | html += '
' 148 | # Close the container for all agents' conversations 149 | 150 | html += '
' 151 | # Close the body and HTML document 152 | html += '' 153 | 154 | # Write the HTML to the specified output file 155 | with open(output_file, 'w', encoding='utf-8') as file: 156 | file.write(html) 157 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openai 2 | PyYAML 3 | numpy 4 | matplotlib -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import numpy as np 3 | from modules.experiment.debate_factory import debate_factory 4 | 5 | if __name__ == "__main__": 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('--agents', type=int, default=2, 8 | help='number of agents') 9 | parser.add_argument('--n_stubborn', type=int, default=0, 10 | help='number of stubborn agents') 11 | parser.add_argument('--n_suggestible', type=int, default=0, 12 | help='number of suggestible agents') 13 | parser.add_argument('--rounds', type=int, default=9, 14 | help='number of rounds') 15 | parser.add_argument('--n_exp', type=int, default=3, 16 | help='number of independent experiments') 17 | parser.add_argument('--out_file', type=str, default='', 18 | help='path to save the output') 19 | parser.add_argument('--summarize_mode', type=str, default="last_round", 20 | help='all_rounds or last_round: summarize all rounds memories or last round memories') 21 | parser.add_argument('--not_full_connected', action="store_true", 22 | help='True if each agent knows all the position of other agents') 23 | # parse and set arguments 24 | args = parser.parse_args() 25 | # define connectivity matrix 26 | N = args.agents 27 | m = np.ones((N, N), dtype=bool) 28 | np.fill_diagonal(m, False) 29 | 30 | if(args.not_full_connected): 31 | m = np.array( 32 | # [ 33 | # [False, False, False], 34 | # [True, False, False], 35 | # [True, False, False], 36 | # ] 37 | [ 38 | [False, True, True], 39 | [True, False, False], 40 | [True, False, False], 41 | ] 42 | ) 43 | exp = debate_factory("2d", args, connectivity_matrix=m) 44 | exp.run() 45 | --------------------------------------------------------------------------------