├── .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 |
--------------------------------------------------------------------------------