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.
Files changed (50) hide show
  1. disensa-0.1.0/.env.example +6 -0
  2. disensa-0.1.0/.gitignore +10 -0
  3. disensa-0.1.0/CLAUDE.md +71 -0
  4. disensa-0.1.0/PKG-INFO +155 -0
  5. disensa-0.1.0/README.md +126 -0
  6. disensa-0.1.0/configs/bc_small_world.yaml +14 -0
  7. disensa-0.1.0/configs/demo_camps.yaml +22 -0
  8. disensa-0.1.0/configs/demo_fake.yaml +17 -0
  9. disensa-0.1.0/configs/demo_fake_rewiring.yaml +18 -0
  10. disensa-0.1.0/configs/fj_scale_free.yaml +14 -0
  11. disensa-0.1.0/configs/mixed_api.yaml +24 -0
  12. disensa-0.1.0/configs/mixed_local.yaml +23 -0
  13. disensa-0.1.0/configs/numeric_bc.yaml +6 -0
  14. disensa-0.1.0/configs/quick.yaml +18 -0
  15. disensa-0.1.0/configs/wang/random.yaml +19 -0
  16. disensa-0.1.0/configs/wang/scale_free.yaml +19 -0
  17. disensa-0.1.0/configs/wang/small_world.yaml +19 -0
  18. disensa-0.1.0/pyproject.toml +63 -0
  19. disensa-0.1.0/src/disensa/__init__.py +7 -0
  20. disensa-0.1.0/src/disensa/analysis.py +198 -0
  21. disensa-0.1.0/src/disensa/app.py +340 -0
  22. disensa-0.1.0/src/disensa/baselines.py +96 -0
  23. disensa-0.1.0/src/disensa/cli.py +195 -0
  24. disensa-0.1.0/src/disensa/config.py +143 -0
  25. disensa-0.1.0/src/disensa/engine.py +320 -0
  26. disensa-0.1.0/src/disensa/ensemble.py +80 -0
  27. disensa-0.1.0/src/disensa/export.py +116 -0
  28. disensa-0.1.0/src/disensa/fake_llm.py +87 -0
  29. disensa-0.1.0/src/disensa/llm.py +164 -0
  30. disensa-0.1.0/src/disensa/metrics.py +70 -0
  31. disensa-0.1.0/src/disensa/networks.py +38 -0
  32. disensa-0.1.0/src/disensa/numeric.py +75 -0
  33. disensa-0.1.0/src/disensa/personas.py +83 -0
  34. disensa-0.1.0/src/disensa/prompts.py +81 -0
  35. disensa-0.1.0/src/disensa/rewiring.py +65 -0
  36. disensa-0.1.0/src/disensa/runs.py +119 -0
  37. disensa-0.1.0/src/disensa/static/app.html +1263 -0
  38. disensa-0.1.0/src/disensa/static/d3.min.js +2 -0
  39. disensa-0.1.0/src/disensa/static/docs.html +464 -0
  40. disensa-0.1.0/src/disensa/static/landing.html +273 -0
  41. disensa-0.1.0/tests/test_analysis.py +83 -0
  42. disensa-0.1.0/tests/test_app.py +115 -0
  43. disensa-0.1.0/tests/test_baselines.py +59 -0
  44. disensa-0.1.0/tests/test_engine.py +134 -0
  45. disensa-0.1.0/tests/test_ensemble.py +43 -0
  46. disensa-0.1.0/tests/test_export.py +51 -0
  47. disensa-0.1.0/tests/test_llm.py +49 -0
  48. disensa-0.1.0/tests/test_metrics.py +31 -0
  49. disensa-0.1.0/tests/test_rewiring.py +52 -0
  50. disensa-0.1.0/uv.lock +3677 -0
@@ -0,0 +1,6 @@
1
+ # Only fill in the providers you use. Never commit .env.
2
+ ANTHROPIC_API_KEY=
3
+ OPENAI_API_KEY=
4
+ XAI_API_KEY=
5
+ DEEPSEEK_API_KEY=
6
+ OPENROUTER_API_KEY=
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ .cache/
6
+ runs/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .DS_Store
@@ -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)
@@ -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,14 @@
1
+ name: bc_small_world
2
+ seed: 42
3
+ network:
4
+ topology: watts_strogatz
5
+ n: 50
6
+ k: 6
7
+ beta: 0.1
8
+ opinions:
9
+ distribution: uniform
10
+ baseline:
11
+ model: bounded_confidence
12
+ steps: 100
13
+ epsilon: 1.0
14
+ mu: 0.5
@@ -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,14 @@
1
+ name: fj_scale_free
2
+ seed: 42
3
+ network:
4
+ topology: barabasi_albert
5
+ n: 50
6
+ m: 3
7
+ opinions:
8
+ distribution: bimodal
9
+ mean: 1.2
10
+ std: 0.4
11
+ baseline:
12
+ model: friedkin_johnsen
13
+ steps: 100
14
+ stubbornness: 1.0
@@ -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"]
@@ -0,0 +1,7 @@
1
+ """disensa: multi-model LLM-agent opinion dynamics on social networks."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Opinion scale shared by every component (Wang et al. 2025 use [-2, 2]).
6
+ OPINION_MIN = -2.0
7
+ OPINION_MAX = 2.0