disensa 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- disensa-0.1.0/.env.example +6 -0
- disensa-0.1.0/.gitignore +10 -0
- disensa-0.1.0/CLAUDE.md +71 -0
- disensa-0.1.0/PKG-INFO +155 -0
- disensa-0.1.0/README.md +126 -0
- disensa-0.1.0/configs/bc_small_world.yaml +14 -0
- disensa-0.1.0/configs/demo_camps.yaml +22 -0
- disensa-0.1.0/configs/demo_fake.yaml +17 -0
- disensa-0.1.0/configs/demo_fake_rewiring.yaml +18 -0
- disensa-0.1.0/configs/fj_scale_free.yaml +14 -0
- disensa-0.1.0/configs/mixed_api.yaml +24 -0
- disensa-0.1.0/configs/mixed_local.yaml +23 -0
- disensa-0.1.0/configs/numeric_bc.yaml +6 -0
- disensa-0.1.0/configs/quick.yaml +18 -0
- disensa-0.1.0/configs/wang/random.yaml +19 -0
- disensa-0.1.0/configs/wang/scale_free.yaml +19 -0
- disensa-0.1.0/configs/wang/small_world.yaml +19 -0
- disensa-0.1.0/pyproject.toml +63 -0
- disensa-0.1.0/src/disensa/__init__.py +7 -0
- disensa-0.1.0/src/disensa/analysis.py +198 -0
- disensa-0.1.0/src/disensa/app.py +340 -0
- disensa-0.1.0/src/disensa/baselines.py +96 -0
- disensa-0.1.0/src/disensa/cli.py +195 -0
- disensa-0.1.0/src/disensa/config.py +143 -0
- disensa-0.1.0/src/disensa/engine.py +320 -0
- disensa-0.1.0/src/disensa/ensemble.py +80 -0
- disensa-0.1.0/src/disensa/export.py +116 -0
- disensa-0.1.0/src/disensa/fake_llm.py +87 -0
- disensa-0.1.0/src/disensa/llm.py +164 -0
- disensa-0.1.0/src/disensa/metrics.py +70 -0
- disensa-0.1.0/src/disensa/networks.py +38 -0
- disensa-0.1.0/src/disensa/numeric.py +75 -0
- disensa-0.1.0/src/disensa/personas.py +83 -0
- disensa-0.1.0/src/disensa/prompts.py +81 -0
- disensa-0.1.0/src/disensa/rewiring.py +65 -0
- disensa-0.1.0/src/disensa/runs.py +119 -0
- disensa-0.1.0/src/disensa/static/app.html +1263 -0
- disensa-0.1.0/src/disensa/static/d3.min.js +2 -0
- disensa-0.1.0/src/disensa/static/docs.html +464 -0
- disensa-0.1.0/src/disensa/static/landing.html +273 -0
- disensa-0.1.0/tests/test_analysis.py +83 -0
- disensa-0.1.0/tests/test_app.py +115 -0
- disensa-0.1.0/tests/test_baselines.py +59 -0
- disensa-0.1.0/tests/test_engine.py +134 -0
- disensa-0.1.0/tests/test_ensemble.py +43 -0
- disensa-0.1.0/tests/test_export.py +51 -0
- disensa-0.1.0/tests/test_llm.py +49 -0
- disensa-0.1.0/tests/test_metrics.py +31 -0
- disensa-0.1.0/tests/test_rewiring.py +52 -0
- disensa-0.1.0/uv.lock +3677 -0
disensa-0.1.0/.gitignore
ADDED
disensa-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# disensa — context for coding agents
|
|
2
|
+
|
|
3
|
+
Bachelor's thesis project (BAI, University of Milan; supervisors Flaminio Squazzoni, Stefano Zapperi).
|
|
4
|
+
LLM-agent opinion dynamics on social networks. It extends Wang et al. (COLING 2025,
|
|
5
|
+
arXiv:2409.19338), who used a single LLM for every agent, in two directions:
|
|
6
|
+
|
|
7
|
+
1. **Model heterogeneity**: different agents run on different LLMs (`ensemble.py`).
|
|
8
|
+
2. **Individual-level inspection**: every agent's reading, reasoning and posts are logged, so
|
|
9
|
+
single trajectories can be traced and explained (`analysis.py`).
|
|
10
|
+
|
|
11
|
+
Research questions are deliberately not fixed yet — do not invent or add them.
|
|
12
|
+
|
|
13
|
+
## Conventions
|
|
14
|
+
|
|
15
|
+
- **Git: never add `Co-Authored-By: Claude`, `Claude-Session:` or any other Claude attribution to
|
|
16
|
+
commit messages or PR descriptions, unless Theo explicitly asks.** Commits are authored by Theo only.
|
|
17
|
+
- Opinions live on **[-2, 2]** everywhere (−2 strongly oppose, +2 strongly support the topic statement).
|
|
18
|
+
- Everything is seeded. Personas depend only on `seed` (never on models), so the same
|
|
19
|
+
population can be replayed under different model mixes. Separate RNG streams:
|
|
20
|
+
feeds `seed`, personas `seed+1000`, model assignment `seed+2000`, rewiring `seed+3000`.
|
|
21
|
+
- Days are synchronous: agents read day t−1 and all updates apply together.
|
|
22
|
+
- The model is an agent attribute, separate from the persona.
|
|
23
|
+
- One fixed scorer model rates every post (never the agent's own model) — Chuang et al.
|
|
24
|
+
- LLM calls go through `LLMClient` only (LiteLLM + disk cache). `fake/<behaviour>` models run
|
|
25
|
+
offline and deterministically; use them in tests. Never call real APIs in tests.
|
|
26
|
+
- Model output is untrusted: parse with `parse_json`, and a malformed reply must never crash a
|
|
27
|
+
run — log it in `error` and keep the previous state.
|
|
28
|
+
- Run outputs: see the docstring in `runs.py`. Parquet for tables, JSONL for text.
|
|
29
|
+
- Metrics follow Wang et al. §4.2 (`metrics.py`); cite the source in docstrings when adding more.
|
|
30
|
+
|
|
31
|
+
## Layout
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
src/disensa/
|
|
35
|
+
config.py Pydantic schemas for YAML experiment configs
|
|
36
|
+
networks.py ER / WS / BA graphs, initial opinions
|
|
37
|
+
baselines.py DeGroot, bounded confidence, Deffuant, Friedkin–Johnsen
|
|
38
|
+
metrics.py polarization, global disagreement, neighbour correlation
|
|
39
|
+
personas.py Wang-style personas (gender, age, education, Big Five)
|
|
40
|
+
prompts.py every prompt template (quoted verbatim in the thesis)
|
|
41
|
+
llm.py LiteLLM client, cache, JSON parsing/repair
|
|
42
|
+
fake_llm.py offline deterministic backend
|
|
43
|
+
engine.py the simulation loop
|
|
44
|
+
ensemble.py model assignment strategies
|
|
45
|
+
rewiring.py unfollow/follow network dynamics
|
|
46
|
+
analysis.py loading runs, trajectories, shift detection and attribution
|
|
47
|
+
numeric.py numeric baselines written in the run format (so they open in the app)
|
|
48
|
+
export.py run → JSON for the page; `viz` embeds it (and d3) into one offline file
|
|
49
|
+
app.py `disensa app`: local stdlib HTTP server + JSON API, runs in threads
|
|
50
|
+
static/ landing.html (start page at /), docs.html (/docs), app.html (the lab at /lab: header that expands into
|
|
51
|
+
the new-run form and runs list, plus the viewer), vendored d3.min.js. The project name is not shown in the UI for now;
|
|
52
|
+
model text is untrusted: insert with textContent only
|
|
53
|
+
cli.py `disensa` command
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Commands
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync --extra analysis
|
|
60
|
+
uv run pytest && uv run ruff check . && uv run ruff format --check .
|
|
61
|
+
uv run disensa run configs/demo_fake.yaml # offline, seconds
|
|
62
|
+
uv run disensa run configs/quick.yaml # Ollama, minutes
|
|
63
|
+
uv run disensa inspect runs/<run_dir> # per-model table + largest opinion shifts
|
|
64
|
+
uv run disensa inspect runs/<run_dir> --agent 7
|
|
65
|
+
uv run disensa app # web app: configure, run, watch live
|
|
66
|
+
uv run disensa viz runs/<run_dir> # single offline viewer file for one run
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Not done yet
|
|
70
|
+
|
|
71
|
+
- M6: running on INDACO (vLLM + SLURM). The code only needs `api_base` pointed at a vLLM server.
|
disensa-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: disensa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-model LLM-agent opinion dynamics on social networks
|
|
5
|
+
Project-URL: Repository, https://github.com/theoradicella/disensa
|
|
6
|
+
Author: Theo Radicella
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: diskcache>=5.6
|
|
10
|
+
Requires-Dist: litellm>=1.40
|
|
11
|
+
Requires-Dist: networkx>=3.2
|
|
12
|
+
Requires-Dist: numpy>=1.26
|
|
13
|
+
Requires-Dist: pandas>=2.2
|
|
14
|
+
Requires-Dist: pyarrow>=15
|
|
15
|
+
Requires-Dist: pydantic>=2.7
|
|
16
|
+
Requires-Dist: python-dotenv>=1.0
|
|
17
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Requires-Dist: tenacity>=8.2
|
|
19
|
+
Requires-Dist: typer>=0.12
|
|
20
|
+
Provides-Extra: analysis
|
|
21
|
+
Requires-Dist: duckdb>=1.0; extra == 'analysis'
|
|
22
|
+
Requires-Dist: matplotlib>=3.8; extra == 'analysis'
|
|
23
|
+
Requires-Dist: ruptures>=1.1; extra == 'analysis'
|
|
24
|
+
Requires-Dist: scikit-learn>=1.4; extra == 'analysis'
|
|
25
|
+
Requires-Dist: scipy>=1.12; extra == 'analysis'
|
|
26
|
+
Requires-Dist: seaborn>=0.13; extra == 'analysis'
|
|
27
|
+
Requires-Dist: statsmodels>=0.14; extra == 'analysis'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# disensa
|
|
31
|
+
|
|
32
|
+
Opinion dynamics with LLM agents on social networks, where different agents can run on
|
|
33
|
+
**different language models**. It extends the Social Simulation Framework of Wang et al.
|
|
34
|
+
(COLING 2025, arXiv:2409.19338), which used one LLM for every agent, with:
|
|
35
|
+
|
|
36
|
+
- a **multi-model ensemble**: each agent's reasoning comes from a model you choose
|
|
37
|
+
(Claude, GPT, Grok, DeepSeek, Llama, Gemma, Mistral, Qwen, …);
|
|
38
|
+
- **individual-level inspection**: every agent's reading, reasoning and posts are logged, so
|
|
39
|
+
you can trace how and why a single agent moved;
|
|
40
|
+
- **network rewiring** (optional): agents unfollow people they disagree with;
|
|
41
|
+
- an **interactive d3 viewer** for every run.
|
|
42
|
+
|
|
43
|
+
## Setup
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv sync --extra analysis # dependencies incl. analysis tools
|
|
47
|
+
cp .env.example .env # only if you use paid APIs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For free local models, install [Ollama](https://ollama.com) and pull a few:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ollama pull llama3.2:3b && ollama pull llama3.1:8b && ollama pull qwen2.5:7b
|
|
54
|
+
OLLAMA_NUM_PARALLEL=4 OLLAMA_MAX_LOADED_MODELS=3 ollama serve
|
|
55
|
+
uv run disensa ping ollama_chat/llama3.2:3b # check it answers
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## The app
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv run disensa app # opens http://127.0.0.1:8765
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The start page (a swarm of agents that scatters from your cursor) leads to the lab at `/lab`.
|
|
65
|
+
|
|
66
|
+
Pick a preset or set everything by hand (engine, topology, initial opinions, models,
|
|
67
|
+
recommender, rewiring, numeric algorithm), press **Run simulation**, and watch it live.
|
|
68
|
+
Nodes are shaded from pale blue (oppose) to deep blue (support) and drift left to right with
|
|
69
|
+
their opinion, so camps separate on screen. The **Runs** tab lists past runs to replay.
|
|
70
|
+
Start with the `demo_camps` preset: offline, a few seconds.
|
|
71
|
+
|
|
72
|
+
## Command line
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv run disensa run configs/demo_fake.yaml # offline fake models, a few seconds
|
|
76
|
+
uv run disensa run configs/quick.yaml # 10 agents, 5 days, Ollama, a few minutes
|
|
77
|
+
uv run disensa inspect runs/<run_dir> # per-model behaviour + largest opinion shifts
|
|
78
|
+
uv run disensa inspect runs/<run_dir> --agent 3 # one agent's day-by-day story
|
|
79
|
+
uv run disensa viz runs/<run_dir> # open the interactive viewer
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Config | What it is |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `demo_camps.yaml` | Offline demo where the population splits into camps |
|
|
85
|
+
| `demo_fake.yaml`, `demo_fake_rewiring.yaml` | Offline demos (fake models); try the whole pipeline for free |
|
|
86
|
+
| `numeric_bc.yaml` | Classic bounded confidence, opens in the app like any run |
|
|
87
|
+
| `quick.yaml` | Smallest real run on Ollama, for checking prompts |
|
|
88
|
+
| `wang/{small_world,scale_free,random}.yaml` | Wang et al. replication, one model, 50 agents × 30 days |
|
|
89
|
+
| `mixed_local.yaml` | Three open-weight models from three labs, on Ollama |
|
|
90
|
+
| `mixed_api.yaml` | Claude, GPT, Grok, DeepSeek through their APIs (paid) |
|
|
91
|
+
| `bc_small_world.yaml`, `fj_scale_free.yaml` | Numeric baselines (`disensa baseline`) |
|
|
92
|
+
|
|
93
|
+
`--days N` overrides the number of days for a quick try.
|
|
94
|
+
|
|
95
|
+
## How a run works
|
|
96
|
+
|
|
97
|
+
1. **Network**: Erdős–Rényi, Watts–Strogatz or Barabási–Albert, seeded.
|
|
98
|
+
2. **Agents**: each gets a persona (gender, age, education, Big Five poles, initial
|
|
99
|
+
opinion on [-2, 2]) and a model (`assignment`: balanced, random, or clustered by community).
|
|
100
|
+
3. **Day 0**: every agent writes a first post about the topic.
|
|
101
|
+
4. **Each day**: an agent reads a few posts from its neighbours (optionally only those
|
|
102
|
+
within the recommender threshold), then returns its reasoning, new belief, new post and an
|
|
103
|
+
updated memory in one call. A fixed scorer model rates every post on [-2, 2].
|
|
104
|
+
5. **End of day**: optional rewiring; metrics are recorded.
|
|
105
|
+
|
|
106
|
+
Everything is cached on disk, so re-running the same config costs nothing.
|
|
107
|
+
|
|
108
|
+
## Run outputs
|
|
109
|
+
|
|
110
|
+
`runs/<timestamp>_<name>_s<seed>/`
|
|
111
|
+
|
|
112
|
+
| File | Contents |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `meta.json` | config, git commit, timing, call counts |
|
|
115
|
+
| `agents.parquet` | persona, model, degree per agent |
|
|
116
|
+
| `opinions.parquet` | per day and agent: self-reported belief, scorer rating, opinion used |
|
|
117
|
+
| `metrics.parquet` | per day: polarization, global disagreement, neighbour correlation, per-model means |
|
|
118
|
+
| `events.jsonl` | per day and agent: feed, reasoning, post, memory, raw reply, errors; rewiring events |
|
|
119
|
+
| `edges.jsonl` | the network on day 0 and on every day it changed |
|
|
120
|
+
| `viewer.html` | written by `disensa viz`: one offline file with the viewer and data |
|
|
121
|
+
|
|
122
|
+
## Layout
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
src/disensa/
|
|
126
|
+
config.py typed YAML experiment configs (Pydantic)
|
|
127
|
+
networks.py graphs and initial opinions
|
|
128
|
+
baselines.py DeGroot, bounded confidence, Deffuant, Friedkin–Johnsen
|
|
129
|
+
metrics.py polarization, global disagreement, neighbour correlation (Wang et al. §4.2)
|
|
130
|
+
personas.py Wang-style personas
|
|
131
|
+
prompts.py every prompt, in one place
|
|
132
|
+
llm.py one async client for all providers (LiteLLM) + cache + JSON repair
|
|
133
|
+
fake_llm.py deterministic offline backend
|
|
134
|
+
engine.py the simulation loop
|
|
135
|
+
ensemble.py model assignment
|
|
136
|
+
rewiring.py unfollow/follow dynamics
|
|
137
|
+
analysis.py timelines, opinion shifts, per-model summaries, change points
|
|
138
|
+
numeric.py numeric baselines in the run format
|
|
139
|
+
export.py run → viewer data / offline viewer file
|
|
140
|
+
app.py local web app server
|
|
141
|
+
static/ the app page + vendored d3
|
|
142
|
+
cli.py `disensa` command
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Roadmap
|
|
146
|
+
|
|
147
|
+
- [x] M0 skeleton, configs, LLM client
|
|
148
|
+
- [x] M1 networks, numeric baselines, metrics
|
|
149
|
+
- [x] M2 LLM agents, Wang et al. loop, scorer
|
|
150
|
+
- [x] M3 multi-model ensemble
|
|
151
|
+
- [x] M4 rewiring
|
|
152
|
+
- [x] M5 individual trajectory tooling
|
|
153
|
+
- [x] M7 d3 viewer
|
|
154
|
+
- [x] Web app: configure, run and watch simulations live
|
|
155
|
+
- [ ] M6 INDACO: vLLM + SLURM sweeps (point `api_base` at a vLLM server)
|
disensa-0.1.0/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# disensa
|
|
2
|
+
|
|
3
|
+
Opinion dynamics with LLM agents on social networks, where different agents can run on
|
|
4
|
+
**different language models**. It extends the Social Simulation Framework of Wang et al.
|
|
5
|
+
(COLING 2025, arXiv:2409.19338), which used one LLM for every agent, with:
|
|
6
|
+
|
|
7
|
+
- a **multi-model ensemble**: each agent's reasoning comes from a model you choose
|
|
8
|
+
(Claude, GPT, Grok, DeepSeek, Llama, Gemma, Mistral, Qwen, …);
|
|
9
|
+
- **individual-level inspection**: every agent's reading, reasoning and posts are logged, so
|
|
10
|
+
you can trace how and why a single agent moved;
|
|
11
|
+
- **network rewiring** (optional): agents unfollow people they disagree with;
|
|
12
|
+
- an **interactive d3 viewer** for every run.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync --extra analysis # dependencies incl. analysis tools
|
|
18
|
+
cp .env.example .env # only if you use paid APIs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For free local models, install [Ollama](https://ollama.com) and pull a few:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ollama pull llama3.2:3b && ollama pull llama3.1:8b && ollama pull qwen2.5:7b
|
|
25
|
+
OLLAMA_NUM_PARALLEL=4 OLLAMA_MAX_LOADED_MODELS=3 ollama serve
|
|
26
|
+
uv run disensa ping ollama_chat/llama3.2:3b # check it answers
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## The app
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv run disensa app # opens http://127.0.0.1:8765
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The start page (a swarm of agents that scatters from your cursor) leads to the lab at `/lab`.
|
|
36
|
+
|
|
37
|
+
Pick a preset or set everything by hand (engine, topology, initial opinions, models,
|
|
38
|
+
recommender, rewiring, numeric algorithm), press **Run simulation**, and watch it live.
|
|
39
|
+
Nodes are shaded from pale blue (oppose) to deep blue (support) and drift left to right with
|
|
40
|
+
their opinion, so camps separate on screen. The **Runs** tab lists past runs to replay.
|
|
41
|
+
Start with the `demo_camps` preset: offline, a few seconds.
|
|
42
|
+
|
|
43
|
+
## Command line
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run disensa run configs/demo_fake.yaml # offline fake models, a few seconds
|
|
47
|
+
uv run disensa run configs/quick.yaml # 10 agents, 5 days, Ollama, a few minutes
|
|
48
|
+
uv run disensa inspect runs/<run_dir> # per-model behaviour + largest opinion shifts
|
|
49
|
+
uv run disensa inspect runs/<run_dir> --agent 3 # one agent's day-by-day story
|
|
50
|
+
uv run disensa viz runs/<run_dir> # open the interactive viewer
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Config | What it is |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `demo_camps.yaml` | Offline demo where the population splits into camps |
|
|
56
|
+
| `demo_fake.yaml`, `demo_fake_rewiring.yaml` | Offline demos (fake models); try the whole pipeline for free |
|
|
57
|
+
| `numeric_bc.yaml` | Classic bounded confidence, opens in the app like any run |
|
|
58
|
+
| `quick.yaml` | Smallest real run on Ollama, for checking prompts |
|
|
59
|
+
| `wang/{small_world,scale_free,random}.yaml` | Wang et al. replication, one model, 50 agents × 30 days |
|
|
60
|
+
| `mixed_local.yaml` | Three open-weight models from three labs, on Ollama |
|
|
61
|
+
| `mixed_api.yaml` | Claude, GPT, Grok, DeepSeek through their APIs (paid) |
|
|
62
|
+
| `bc_small_world.yaml`, `fj_scale_free.yaml` | Numeric baselines (`disensa baseline`) |
|
|
63
|
+
|
|
64
|
+
`--days N` overrides the number of days for a quick try.
|
|
65
|
+
|
|
66
|
+
## How a run works
|
|
67
|
+
|
|
68
|
+
1. **Network**: Erdős–Rényi, Watts–Strogatz or Barabási–Albert, seeded.
|
|
69
|
+
2. **Agents**: each gets a persona (gender, age, education, Big Five poles, initial
|
|
70
|
+
opinion on [-2, 2]) and a model (`assignment`: balanced, random, or clustered by community).
|
|
71
|
+
3. **Day 0**: every agent writes a first post about the topic.
|
|
72
|
+
4. **Each day**: an agent reads a few posts from its neighbours (optionally only those
|
|
73
|
+
within the recommender threshold), then returns its reasoning, new belief, new post and an
|
|
74
|
+
updated memory in one call. A fixed scorer model rates every post on [-2, 2].
|
|
75
|
+
5. **End of day**: optional rewiring; metrics are recorded.
|
|
76
|
+
|
|
77
|
+
Everything is cached on disk, so re-running the same config costs nothing.
|
|
78
|
+
|
|
79
|
+
## Run outputs
|
|
80
|
+
|
|
81
|
+
`runs/<timestamp>_<name>_s<seed>/`
|
|
82
|
+
|
|
83
|
+
| File | Contents |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `meta.json` | config, git commit, timing, call counts |
|
|
86
|
+
| `agents.parquet` | persona, model, degree per agent |
|
|
87
|
+
| `opinions.parquet` | per day and agent: self-reported belief, scorer rating, opinion used |
|
|
88
|
+
| `metrics.parquet` | per day: polarization, global disagreement, neighbour correlation, per-model means |
|
|
89
|
+
| `events.jsonl` | per day and agent: feed, reasoning, post, memory, raw reply, errors; rewiring events |
|
|
90
|
+
| `edges.jsonl` | the network on day 0 and on every day it changed |
|
|
91
|
+
| `viewer.html` | written by `disensa viz`: one offline file with the viewer and data |
|
|
92
|
+
|
|
93
|
+
## Layout
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
src/disensa/
|
|
97
|
+
config.py typed YAML experiment configs (Pydantic)
|
|
98
|
+
networks.py graphs and initial opinions
|
|
99
|
+
baselines.py DeGroot, bounded confidence, Deffuant, Friedkin–Johnsen
|
|
100
|
+
metrics.py polarization, global disagreement, neighbour correlation (Wang et al. §4.2)
|
|
101
|
+
personas.py Wang-style personas
|
|
102
|
+
prompts.py every prompt, in one place
|
|
103
|
+
llm.py one async client for all providers (LiteLLM) + cache + JSON repair
|
|
104
|
+
fake_llm.py deterministic offline backend
|
|
105
|
+
engine.py the simulation loop
|
|
106
|
+
ensemble.py model assignment
|
|
107
|
+
rewiring.py unfollow/follow dynamics
|
|
108
|
+
analysis.py timelines, opinion shifts, per-model summaries, change points
|
|
109
|
+
numeric.py numeric baselines in the run format
|
|
110
|
+
export.py run → viewer data / offline viewer file
|
|
111
|
+
app.py local web app server
|
|
112
|
+
static/ the app page + vendored d3
|
|
113
|
+
cli.py `disensa` command
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Roadmap
|
|
117
|
+
|
|
118
|
+
- [x] M0 skeleton, configs, LLM client
|
|
119
|
+
- [x] M1 networks, numeric baselines, metrics
|
|
120
|
+
- [x] M2 LLM agents, Wang et al. loop, scorer
|
|
121
|
+
- [x] M3 multi-model ensemble
|
|
122
|
+
- [x] M4 rewiring
|
|
123
|
+
- [x] M5 individual trajectory tooling
|
|
124
|
+
- [x] M7 d3 viewer
|
|
125
|
+
- [x] Web app: configure, run and watch simulations live
|
|
126
|
+
- [ ] M6 INDACO: vLLM + SLURM sweeps (point `api_base` at a vLLM server)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Offline demo where the population splits into camps (fake models, a few seconds).
|
|
2
|
+
# Tribal agents only listen to people within 0.7 of their view; the recommender hides
|
|
3
|
+
# distant neighbours; rewiring lets agents unfollow people they disagree with.
|
|
4
|
+
name: demo_camps
|
|
5
|
+
seed: 3
|
|
6
|
+
network: {topology: watts_strogatz, n: 60, k: 6, beta: 0.15}
|
|
7
|
+
opinions: {distribution: uniform}
|
|
8
|
+
topic:
|
|
9
|
+
name: euthanasia
|
|
10
|
+
statement: Euthanasia should be legal for terminally ill adults who clearly request it.
|
|
11
|
+
simulation:
|
|
12
|
+
days: 25
|
|
13
|
+
feed: {min_posts: 2, max_posts: 5}
|
|
14
|
+
recommender: {enabled: true, threshold: 1.5}
|
|
15
|
+
rewiring: {enabled: true, probability: 0.25, unfollow_threshold: 1.0, follow: similar, follow_threshold: 0.5}
|
|
16
|
+
assignment: balanced
|
|
17
|
+
models:
|
|
18
|
+
- {name: fake/tribal, weight: 3}
|
|
19
|
+
- {name: fake/stubborn, weight: 1}
|
|
20
|
+
scorer:
|
|
21
|
+
name: fake/scorer
|
|
22
|
+
llm: {max_concurrency: 64}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Offline demo with the fake backend: no model needed, runs in seconds.
|
|
2
|
+
# Useful to try the pipeline, the trajectory tools and the visualization.
|
|
3
|
+
name: demo_fake
|
|
4
|
+
seed: 7
|
|
5
|
+
network: {topology: watts_strogatz, n: 60, k: 6, beta: 0.1}
|
|
6
|
+
opinions: {distribution: uniform}
|
|
7
|
+
simulation:
|
|
8
|
+
days: 30
|
|
9
|
+
feed: {min_posts: 2, max_posts: 5}
|
|
10
|
+
recommender: {enabled: false}
|
|
11
|
+
models:
|
|
12
|
+
- {name: fake/bounded, weight: 2}
|
|
13
|
+
- {name: fake/conformist, weight: 1}
|
|
14
|
+
- {name: fake/stubborn, weight: 1}
|
|
15
|
+
scorer:
|
|
16
|
+
name: fake/scorer
|
|
17
|
+
llm: {max_concurrency: 64}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Offline demo with rewiring: agents unfollow people they disagree with.
|
|
2
|
+
name: demo_fake_rewiring
|
|
3
|
+
seed: 7
|
|
4
|
+
network: {topology: watts_strogatz, n: 60, k: 6, beta: 0.1}
|
|
5
|
+
opinions: {distribution: uniform}
|
|
6
|
+
simulation:
|
|
7
|
+
days: 30
|
|
8
|
+
feed: {min_posts: 2, max_posts: 5}
|
|
9
|
+
recommender: {enabled: false}
|
|
10
|
+
rewiring: {enabled: true, probability: 0.2, unfollow_threshold: 1.2, follow: similar, follow_threshold: 0.5}
|
|
11
|
+
assignment: clustered
|
|
12
|
+
models:
|
|
13
|
+
- {name: fake/bounded, weight: 2}
|
|
14
|
+
- {name: fake/conformist, weight: 1}
|
|
15
|
+
- {name: fake/stubborn, weight: 1}
|
|
16
|
+
scorer:
|
|
17
|
+
name: fake/scorer
|
|
18
|
+
llm: {max_concurrency: 64}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Multi-model ensemble through APIs (costs money). Put keys in .env first:
|
|
2
|
+
# ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY
|
|
3
|
+
# Model names change often: check each provider's current model list before running.
|
|
4
|
+
name: mixed_api
|
|
5
|
+
seed: 1
|
|
6
|
+
network: {topology: watts_strogatz, n: 20, k: 4, beta: 0.1}
|
|
7
|
+
opinions: {distribution: uniform}
|
|
8
|
+
topic:
|
|
9
|
+
name: euthanasia
|
|
10
|
+
statement: Euthanasia should be legal for terminally ill adults who clearly request it.
|
|
11
|
+
simulation:
|
|
12
|
+
days: 10
|
|
13
|
+
feed: {min_posts: 1, max_posts: 4}
|
|
14
|
+
recommender: {enabled: true, threshold: 2.0}
|
|
15
|
+
opinion_source: self
|
|
16
|
+
assignment: balanced
|
|
17
|
+
models:
|
|
18
|
+
- {name: anthropic/claude-haiku-4-5}
|
|
19
|
+
- {name: openai/gpt-4o-mini}
|
|
20
|
+
- {name: xai/grok-3-mini}
|
|
21
|
+
- {name: deepseek/deepseek-chat}
|
|
22
|
+
scorer:
|
|
23
|
+
name: ollama_chat/qwen2.5:7b # scorer stays local and outside the agents' labs
|
|
24
|
+
llm: {max_concurrency: 4}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Multi-model ensemble on your Mac: three open-weight models from three labs, 20 agents.
|
|
2
|
+
# Pull first: ollama pull llama3.1:8b && ollama pull gemma2:9b && ollama pull mistral:7b && ollama pull qwen2.5:7b
|
|
3
|
+
# Start with: OLLAMA_NUM_PARALLEL=4 OLLAMA_MAX_LOADED_MODELS=4 ollama serve
|
|
4
|
+
name: mixed_local
|
|
5
|
+
seed: 1
|
|
6
|
+
network: {topology: watts_strogatz, n: 20, k: 4, beta: 0.1}
|
|
7
|
+
opinions: {distribution: uniform}
|
|
8
|
+
topic:
|
|
9
|
+
name: euthanasia
|
|
10
|
+
statement: Euthanasia should be legal for terminally ill adults who clearly request it.
|
|
11
|
+
simulation:
|
|
12
|
+
days: 10
|
|
13
|
+
feed: {min_posts: 1, max_posts: 4}
|
|
14
|
+
recommender: {enabled: true, threshold: 2.0}
|
|
15
|
+
opinion_source: self
|
|
16
|
+
assignment: balanced # balanced | random | clustered
|
|
17
|
+
models:
|
|
18
|
+
- {name: ollama_chat/llama3.1:8b} # Meta
|
|
19
|
+
- {name: ollama_chat/gemma2:9b} # Google
|
|
20
|
+
- {name: ollama_chat/mistral:7b} # Mistral
|
|
21
|
+
scorer:
|
|
22
|
+
name: ollama_chat/qwen2.5:7b # Alibaba: a lab none of the agents come from
|
|
23
|
+
llm: {max_concurrency: 4}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Classic bounded confidence (Hegselmann–Krause) on a small world: narrow confidence -> clusters.
|
|
2
|
+
name: numeric_bc
|
|
3
|
+
seed: 3
|
|
4
|
+
network: {topology: watts_strogatz, n: 60, k: 6, beta: 0.15}
|
|
5
|
+
opinions: {distribution: uniform}
|
|
6
|
+
baseline: {model: bounded_confidence, steps: 40, epsilon: 0.7, mu: 0.5}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Fast local check: 10 agents, 5 days, small model. A few minutes on an M3 Pro.
|
|
2
|
+
name: quick
|
|
3
|
+
seed: 1
|
|
4
|
+
network: {topology: watts_strogatz, n: 10, k: 4, beta: 0.1}
|
|
5
|
+
opinions: {distribution: uniform}
|
|
6
|
+
topic:
|
|
7
|
+
name: euthanasia
|
|
8
|
+
statement: Euthanasia should be legal for terminally ill adults who clearly request it.
|
|
9
|
+
simulation:
|
|
10
|
+
days: 5
|
|
11
|
+
feed: {min_posts: 1, max_posts: 3}
|
|
12
|
+
recommender: {enabled: true, threshold: 2.0}
|
|
13
|
+
opinion_source: self
|
|
14
|
+
models:
|
|
15
|
+
- name: ollama_chat/llama3.2:3b
|
|
16
|
+
scorer:
|
|
17
|
+
name: ollama_chat/qwen2.5:7b
|
|
18
|
+
llm: {max_concurrency: 4}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Wang et al. (2025) replication, single model: 50 agents, 30 days, recommender on.
|
|
2
|
+
# Roughly 3,000 agent calls + 1,500 scorer calls: run overnight locally, or on a server.
|
|
3
|
+
name: wang_random
|
|
4
|
+
seed: 42
|
|
5
|
+
network: {topology: erdos_renyi, n: 50, p: 0.12}
|
|
6
|
+
opinions: {distribution: uniform}
|
|
7
|
+
topic:
|
|
8
|
+
name: euthanasia
|
|
9
|
+
statement: Euthanasia should be legal for terminally ill adults who clearly request it.
|
|
10
|
+
simulation:
|
|
11
|
+
days: 30
|
|
12
|
+
feed: {min_posts: 1, max_posts: 5}
|
|
13
|
+
recommender: {enabled: true, threshold: 2.0}
|
|
14
|
+
opinion_source: self
|
|
15
|
+
models:
|
|
16
|
+
- name: ollama_chat/llama3.1:8b
|
|
17
|
+
scorer:
|
|
18
|
+
name: ollama_chat/qwen2.5:7b
|
|
19
|
+
llm: {max_concurrency: 4}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Wang et al. (2025) replication, single model: 50 agents, 30 days, recommender on.
|
|
2
|
+
# Roughly 3,000 agent calls + 1,500 scorer calls: run overnight locally, or on a server.
|
|
3
|
+
name: wang_scale_free
|
|
4
|
+
seed: 42
|
|
5
|
+
network: {topology: barabasi_albert, n: 50, m: 3}
|
|
6
|
+
opinions: {distribution: uniform}
|
|
7
|
+
topic:
|
|
8
|
+
name: euthanasia
|
|
9
|
+
statement: Euthanasia should be legal for terminally ill adults who clearly request it.
|
|
10
|
+
simulation:
|
|
11
|
+
days: 30
|
|
12
|
+
feed: {min_posts: 1, max_posts: 5}
|
|
13
|
+
recommender: {enabled: true, threshold: 2.0}
|
|
14
|
+
opinion_source: self
|
|
15
|
+
models:
|
|
16
|
+
- name: ollama_chat/llama3.1:8b
|
|
17
|
+
scorer:
|
|
18
|
+
name: ollama_chat/qwen2.5:7b
|
|
19
|
+
llm: {max_concurrency: 4}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Wang et al. (2025) replication, single model: 50 agents, 30 days, recommender on.
|
|
2
|
+
# Roughly 3,000 agent calls + 1,500 scorer calls: run overnight locally, or on a server.
|
|
3
|
+
name: wang_small_world
|
|
4
|
+
seed: 42
|
|
5
|
+
network: {topology: watts_strogatz, n: 50, k: 6, beta: 0.1}
|
|
6
|
+
opinions: {distribution: uniform}
|
|
7
|
+
topic:
|
|
8
|
+
name: euthanasia
|
|
9
|
+
statement: Euthanasia should be legal for terminally ill adults who clearly request it.
|
|
10
|
+
simulation:
|
|
11
|
+
days: 30
|
|
12
|
+
feed: {min_posts: 1, max_posts: 5}
|
|
13
|
+
recommender: {enabled: true, threshold: 2.0}
|
|
14
|
+
opinion_source: self
|
|
15
|
+
models:
|
|
16
|
+
- name: ollama_chat/llama3.1:8b
|
|
17
|
+
scorer:
|
|
18
|
+
name: ollama_chat/qwen2.5:7b
|
|
19
|
+
llm: {max_concurrency: 4}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "disensa"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Multi-model LLM-agent opinion dynamics on social networks"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
authors = [{ name = "Theo Radicella" }]
|
|
8
|
+
license = { text = "MIT" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
"pydantic>=2.7",
|
|
11
|
+
"pyyaml>=6.0",
|
|
12
|
+
"typer>=0.12",
|
|
13
|
+
"numpy>=1.26",
|
|
14
|
+
"networkx>=3.2",
|
|
15
|
+
"pandas>=2.2",
|
|
16
|
+
"pyarrow>=15",
|
|
17
|
+
"litellm>=1.40",
|
|
18
|
+
"diskcache>=5.6",
|
|
19
|
+
"tenacity>=8.2",
|
|
20
|
+
"python-dotenv>=1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/theoradicella/disensa"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
analysis = [
|
|
28
|
+
"duckdb>=1.0",
|
|
29
|
+
"scipy>=1.12",
|
|
30
|
+
"statsmodels>=0.14",
|
|
31
|
+
"ruptures>=1.1",
|
|
32
|
+
"matplotlib>=3.8",
|
|
33
|
+
"seaborn>=0.13",
|
|
34
|
+
"scikit-learn>=1.4",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
disensa = "disensa.cli:app"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.5"]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/disensa"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = "py311"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.per-file-ignores]
|
|
62
|
+
"src/disensa/prompts.py" = ["E501"] # prompt templates are quoted verbatim
|
|
63
|
+
"tests/*" = ["E501"]
|