discourse-sim 0.1.0__py3-none-any.whl
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.
- discourse_sim/__init__.py +29 -0
- discourse_sim/agents/__init__.py +2 -0
- discourse_sim/agents/agent.py +129 -0
- discourse_sim/config.py +222 -0
- discourse_sim/core.py +333 -0
- discourse_sim/simulation/__init__.py +2 -0
- discourse_sim/simulation/engine.py +381 -0
- discourse_sim/timeline/__init__.py +2 -0
- discourse_sim/timeline/timeline.py +79 -0
- discourse_sim/tools/__init__.py +2 -0
- discourse_sim/tools/search_tools.py +78 -0
- discourse_sim/utils/__init__.py +2 -0
- discourse_sim/utils/dataframes.py +89 -0
- discourse_sim-0.1.0.dist-info/METADATA +286 -0
- discourse_sim-0.1.0.dist-info/RECORD +18 -0
- discourse_sim-0.1.0.dist-info/WHEEL +5 -0
- discourse_sim-0.1.0.dist-info/licenses/LICENSE +21 -0
- discourse_sim-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
discourse_sim
|
|
3
|
+
=============
|
|
4
|
+
LLM-augmented Agent-Based Social Simulation for modelling
|
|
5
|
+
public discourse dynamics following a critical real-world event.
|
|
6
|
+
|
|
7
|
+
Quick start
|
|
8
|
+
-----------
|
|
9
|
+
from discourse_sim import DiscourseSimulation
|
|
10
|
+
|
|
11
|
+
sim = DiscourseSimulation(
|
|
12
|
+
critical_event="In November 2023, Dublin city centre saw a major riot...",
|
|
13
|
+
event_date="2023-11-23",
|
|
14
|
+
n_agents=100,
|
|
15
|
+
n_days=15,
|
|
16
|
+
agent_distribution={"centrist": 0.45, "far_right": 0.20,
|
|
17
|
+
"pro_imm": 0.25, "media": 0.10},
|
|
18
|
+
topic="immigration in Ireland",
|
|
19
|
+
ollama_model="mistral:7b-instruct-q4_0",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
sim.run()
|
|
23
|
+
df = sim.to_dataframe()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from discourse_sim.core import DiscourseSimulation
|
|
27
|
+
|
|
28
|
+
__all__ = ["DiscourseSimulation"]
|
|
29
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
discourse_sim.agents.agent
|
|
3
|
+
==========================
|
|
4
|
+
Agent dataclass and population factory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import random
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from discourse_sim.config import SimConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
QUIRK_OPTIONS = [
|
|
18
|
+
"uses sarcasm heavily",
|
|
19
|
+
"adds emojis liberally",
|
|
20
|
+
"writes short, punchy sentences",
|
|
21
|
+
"writes long reflective sentences",
|
|
22
|
+
"asks rhetorical questions constantly",
|
|
23
|
+
"uses casual slang",
|
|
24
|
+
"formal and polished tone",
|
|
25
|
+
"heavy use of hashtags",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Agent:
|
|
31
|
+
"""
|
|
32
|
+
A synthetic social media user.
|
|
33
|
+
|
|
34
|
+
Identity
|
|
35
|
+
--------
|
|
36
|
+
id : unique string identifier (e.g. "agent_0")
|
|
37
|
+
kind : ideological type from SimConfig.agent_distribution
|
|
38
|
+
attitude : [-1, +1] — -1 = strongly pro-topic-subject, +1 = strongly against
|
|
39
|
+
exposure : [0, 1] — cumulative threat narrative saturation
|
|
40
|
+
|
|
41
|
+
Beliefs
|
|
42
|
+
-------
|
|
43
|
+
economic_threat_belief : perceived economic harm from the event subject
|
|
44
|
+
cultural_threat_belief : perceived cultural threat
|
|
45
|
+
security_threat_belief : perceived security/crime risk (updated by threat news)
|
|
46
|
+
humanitarian_belief : humanitarian obligation weight (updated by framing)
|
|
47
|
+
|
|
48
|
+
Psychology
|
|
49
|
+
----------
|
|
50
|
+
openness : [0,1] — how readily beliefs change (0=rigid, 1=open)
|
|
51
|
+
conformity : [0,1] — peer influence susceptibility
|
|
52
|
+
emotional_reactivity : [0,1] — news impact amplifier
|
|
53
|
+
trust_peers : [0,1] — weight given to social neighbourhood
|
|
54
|
+
|
|
55
|
+
State
|
|
56
|
+
-----
|
|
57
|
+
mood : [-1,+1] — affective state; decays; shocked by threat news
|
|
58
|
+
messages : list of all posts generated so far
|
|
59
|
+
attitude_history : attitude value after each timestep
|
|
60
|
+
reasoning_log : tool-call audit trail per day
|
|
61
|
+
quirk : writing style (assigned once, fixed)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
id: str
|
|
65
|
+
kind: str
|
|
66
|
+
attitude: float
|
|
67
|
+
exposure: float
|
|
68
|
+
|
|
69
|
+
economic_threat_belief: float = 0.0
|
|
70
|
+
cultural_threat_belief: float = 0.0
|
|
71
|
+
security_threat_belief: float = 0.0
|
|
72
|
+
humanitarian_belief: float = 0.0
|
|
73
|
+
|
|
74
|
+
openness: float = 0.5
|
|
75
|
+
conformity: float = 0.5
|
|
76
|
+
emotional_reactivity: float = 0.5
|
|
77
|
+
trust_peers: float = 0.7
|
|
78
|
+
|
|
79
|
+
mood: float = 0.0
|
|
80
|
+
messages: list = field(default_factory=list)
|
|
81
|
+
attitude_history: list = field(default_factory=list)
|
|
82
|
+
reasoning_log: list = field(default_factory=list)
|
|
83
|
+
quirk: str = ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def make_agents(config: "SimConfig") -> dict[str, Agent]:
|
|
87
|
+
"""
|
|
88
|
+
Spawn N agents according to SimConfig.agent_distribution and kind_priors.
|
|
89
|
+
|
|
90
|
+
The distribution is converted to a weighted sampling pool so that
|
|
91
|
+
proportions are respected even for non-round N values.
|
|
92
|
+
"""
|
|
93
|
+
rng = random.Random(config.network_seed)
|
|
94
|
+
|
|
95
|
+
# Build weighted pool (oversample then trim)
|
|
96
|
+
pool: list[str] = []
|
|
97
|
+
for kind, proportion in config.agent_distribution.items():
|
|
98
|
+
count = round(proportion * config.n_agents)
|
|
99
|
+
pool.extend([kind] * count)
|
|
100
|
+
|
|
101
|
+
# Adjust pool length to exactly n_agents
|
|
102
|
+
while len(pool) < config.n_agents:
|
|
103
|
+
pool.append(rng.choice(list(config.agent_distribution.keys())))
|
|
104
|
+
pool = pool[: config.n_agents]
|
|
105
|
+
rng.shuffle(pool)
|
|
106
|
+
|
|
107
|
+
agents: dict[str, Agent] = {}
|
|
108
|
+
for i, kind in enumerate(pool):
|
|
109
|
+
priors = config.kind_priors[kind]
|
|
110
|
+
|
|
111
|
+
def u(key: str) -> float:
|
|
112
|
+
lo, hi = priors[key]
|
|
113
|
+
return rng.uniform(lo, hi)
|
|
114
|
+
|
|
115
|
+
agents[f"agent_{i}"] = Agent(
|
|
116
|
+
id=f"agent_{i}",
|
|
117
|
+
kind=kind,
|
|
118
|
+
attitude=u("attitude"),
|
|
119
|
+
exposure=rng.uniform(0.0, 0.3),
|
|
120
|
+
economic_threat_belief=u("economic_threat"),
|
|
121
|
+
cultural_threat_belief=u("cultural_threat"),
|
|
122
|
+
humanitarian_belief=u("humanitarian"),
|
|
123
|
+
openness=u("openness"),
|
|
124
|
+
emotional_reactivity=u("emotional_react"),
|
|
125
|
+
conformity=rng.uniform(0.3, 0.8),
|
|
126
|
+
trust_peers=rng.uniform(0.4, 0.9),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return agents
|
discourse_sim/config.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
discourse_sim.config
|
|
3
|
+
====================
|
|
4
|
+
Central configuration dataclass. All user-facing parameters live here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import date
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── Default agent kinds and their priors ────────────────────────────────────
|
|
16
|
+
# Users can extend / override these by passing custom_kind_priors to SimConfig.
|
|
17
|
+
DEFAULT_KIND_PRIORS: Dict[str, dict] = {
|
|
18
|
+
"far_right": {
|
|
19
|
+
"attitude": (0.5, 1.0),
|
|
20
|
+
"economic_threat": (0.4, 0.9),
|
|
21
|
+
"cultural_threat": (0.4, 0.9),
|
|
22
|
+
"humanitarian": (-0.5, 0.1),
|
|
23
|
+
"openness": (0.1, 0.4),
|
|
24
|
+
"emotional_react": (0.6, 1.0),
|
|
25
|
+
},
|
|
26
|
+
"pro_imm": {
|
|
27
|
+
"attitude": (-1.0, -0.3),
|
|
28
|
+
"economic_threat": (-0.5, 0.1),
|
|
29
|
+
"cultural_threat": (-0.5, 0.0),
|
|
30
|
+
"humanitarian": (0.4, 1.0),
|
|
31
|
+
"openness": (0.6, 1.0),
|
|
32
|
+
"emotional_react": (0.3, 0.7),
|
|
33
|
+
},
|
|
34
|
+
"centrist": {
|
|
35
|
+
"attitude": (-0.4, 0.4),
|
|
36
|
+
"economic_threat": (-0.2, 0.4),
|
|
37
|
+
"cultural_threat": (-0.2, 0.3),
|
|
38
|
+
"humanitarian": (0.0, 0.5),
|
|
39
|
+
"openness": (0.4, 0.7),
|
|
40
|
+
"emotional_react": (0.3, 0.6),
|
|
41
|
+
},
|
|
42
|
+
"media": {
|
|
43
|
+
"attitude": (-0.3, 0.3),
|
|
44
|
+
"economic_threat": (-0.2, 0.3),
|
|
45
|
+
"cultural_threat": (-0.2, 0.2),
|
|
46
|
+
"humanitarian": (0.1, 0.5),
|
|
47
|
+
"openness": (0.5, 0.8),
|
|
48
|
+
"emotional_react": (0.2, 0.5),
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SimConfig:
|
|
55
|
+
"""
|
|
56
|
+
All configuration for a DiscourseSimulation run.
|
|
57
|
+
|
|
58
|
+
Required parameters
|
|
59
|
+
-------------------
|
|
60
|
+
critical_event : str
|
|
61
|
+
Plain-text description of the event all agents are reacting to.
|
|
62
|
+
This is injected into every agent prompt on every day.
|
|
63
|
+
Example:
|
|
64
|
+
"In November 2023, Dublin city centre erupted in riots following
|
|
65
|
+
a knife attack near a school. Far-right agitators set buses alight
|
|
66
|
+
and attacked Gardaí. The event dominated Irish media for weeks."
|
|
67
|
+
|
|
68
|
+
event_date : str (ISO format: "YYYY-MM-DD")
|
|
69
|
+
Calendar date of Day 0. All subsequent days are offset from this.
|
|
70
|
+
|
|
71
|
+
topic : str
|
|
72
|
+
The subject domain agents post about.
|
|
73
|
+
Example: "immigration in Ireland", "housing policy", "climate protests"
|
|
74
|
+
Used to anchor web search queries and scoring prompts.
|
|
75
|
+
|
|
76
|
+
Optional parameters
|
|
77
|
+
-------------------
|
|
78
|
+
n_agents : int (default 100)
|
|
79
|
+
Total number of synthetic agents.
|
|
80
|
+
|
|
81
|
+
n_days : int (default 15)
|
|
82
|
+
Number of simulation timesteps (one per day).
|
|
83
|
+
|
|
84
|
+
agent_distribution : dict (default Ireland 2025 empirical distribution)
|
|
85
|
+
Proportions of each agent kind. Must sum to 1.0.
|
|
86
|
+
Keys must match entries in kind_priors.
|
|
87
|
+
Example: {"centrist": 0.45, "far_right": 0.20,
|
|
88
|
+
"pro_imm": 0.25, "media": 0.10}
|
|
89
|
+
|
|
90
|
+
kind_priors : dict (default DEFAULT_KIND_PRIORS)
|
|
91
|
+
Per-kind prior distributions for attitude and belief dimensions.
|
|
92
|
+
Each entry: {"attitude": (lo, hi), "economic_threat": (lo, hi), ...}
|
|
93
|
+
Users can pass a fully custom dict or override individual kinds.
|
|
94
|
+
|
|
95
|
+
timeline : dict[int, str] (default: None → auto-generated via LLM search)
|
|
96
|
+
Optional explicit daily news entries keyed by day index (0-based).
|
|
97
|
+
If provided, overrides automatic news retrieval for those days.
|
|
98
|
+
Any day not present falls back to a generic continuation message.
|
|
99
|
+
Example:
|
|
100
|
+
{
|
|
101
|
+
0: "[VERIFIED] Day 0: The march took place at ...",
|
|
102
|
+
4: "[VERIFIED] Day 4: Government announced ...",
|
|
103
|
+
}
|
|
104
|
+
If None, agents rely entirely on live DuckDuckGo search results.
|
|
105
|
+
|
|
106
|
+
search_query_prefix : str (default: derived from topic + event summary)
|
|
107
|
+
Prefix prepended to all DuckDuckGo search queries.
|
|
108
|
+
Example: "Dublin immigration march April 2025 Ireland"
|
|
109
|
+
Override this to localise searches for non-Irish events.
|
|
110
|
+
|
|
111
|
+
ollama_model : str (default "mistral:7b-instruct-q4_0")
|
|
112
|
+
Any Ollama model string. Model must be pulled locally.
|
|
113
|
+
|
|
114
|
+
temperature : float (default 0.75)
|
|
115
|
+
LLM temperature for post generation.
|
|
116
|
+
|
|
117
|
+
network_k : int (default 6)
|
|
118
|
+
Watts-Strogatz nearest-neighbour count.
|
|
119
|
+
|
|
120
|
+
network_p : float (default 0.3)
|
|
121
|
+
Watts-Strogatz rewiring probability.
|
|
122
|
+
|
|
123
|
+
network_seed : int (default 42)
|
|
124
|
+
Random seed for network and agent initialisation.
|
|
125
|
+
|
|
126
|
+
use_llm_scoring : bool (default True)
|
|
127
|
+
If False, uses keyword heuristic instead of LLM for attitude scoring.
|
|
128
|
+
|
|
129
|
+
verbose : bool (default True)
|
|
130
|
+
Print per-agent and per-day progress to stdout.
|
|
131
|
+
|
|
132
|
+
threat_keywords : list[str]
|
|
133
|
+
Words in daily news that trigger security_threat_belief increase
|
|
134
|
+
and exposure accumulation. Override for non-immigration topics.
|
|
135
|
+
|
|
136
|
+
humanitarian_keywords : list[str]
|
|
137
|
+
Words in daily news that trigger humanitarian_belief increase.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# ── Required ──────────────────────────────────────────────────────────────
|
|
141
|
+
critical_event: str
|
|
142
|
+
event_date: str # "YYYY-MM-DD"
|
|
143
|
+
topic: str
|
|
144
|
+
|
|
145
|
+
# ── Agent population ──────────────────────────────────────────────────────
|
|
146
|
+
n_agents: int = 100
|
|
147
|
+
n_days: int = 15
|
|
148
|
+
|
|
149
|
+
agent_distribution: Dict[str, float] = field(default_factory=lambda: {
|
|
150
|
+
"centrist": 0.45,
|
|
151
|
+
"far_right": 0.20,
|
|
152
|
+
"pro_imm": 0.25,
|
|
153
|
+
"media": 0.10,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
kind_priors: Dict[str, dict] = field(
|
|
157
|
+
default_factory=lambda: DEFAULT_KIND_PRIORS
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# ── Timeline ──────────────────────────────────────────────────────────────
|
|
161
|
+
timeline: Optional[Dict[int, str]] = None # None = rely on live search
|
|
162
|
+
|
|
163
|
+
# ── Search ────────────────────────────────────────────────────────────────
|
|
164
|
+
search_query_prefix: Optional[str] = None # None = auto-derived
|
|
165
|
+
|
|
166
|
+
# ── LLM ───────────────────────────────────────────────────────────────────
|
|
167
|
+
ollama_model: str = "mistral:7b-instruct-q4_0"
|
|
168
|
+
temperature: float = 0.75
|
|
169
|
+
use_llm_scoring: bool = True
|
|
170
|
+
|
|
171
|
+
# ── Network ───────────────────────────────────────────────────────────────
|
|
172
|
+
network_k: int = 6
|
|
173
|
+
network_p: float = 0.3
|
|
174
|
+
network_seed: int = 42
|
|
175
|
+
|
|
176
|
+
# ── Belief update keywords ────────────────────────────────────────────────
|
|
177
|
+
threat_keywords: List[str] = field(default_factory=lambda: [
|
|
178
|
+
"arson", "attack", "violence", "crime", "danger",
|
|
179
|
+
"get them out", "deportation", "deport", "riot", "assault",
|
|
180
|
+
])
|
|
181
|
+
humanitarian_keywords: List[str] = field(default_factory=lambda: [
|
|
182
|
+
"refugee", "asylum", "rights", "children", "family",
|
|
183
|
+
"compassion", "solidarity", "waiting", "humanitarian",
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
# ── Display ───────────────────────────────────────────────────────────────
|
|
187
|
+
verbose: bool = True
|
|
188
|
+
|
|
189
|
+
# ── Post-init validation ──────────────────────────────────────────────────
|
|
190
|
+
def __post_init__(self):
|
|
191
|
+
# Validate date
|
|
192
|
+
try:
|
|
193
|
+
self._parsed_date = date.fromisoformat(self.event_date)
|
|
194
|
+
except ValueError:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"event_date must be ISO format 'YYYY-MM-DD', got: {self.event_date!r}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Validate distribution sums to ~1.0
|
|
200
|
+
total = sum(self.agent_distribution.values())
|
|
201
|
+
if abs(total - 1.0) > 0.01:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"agent_distribution proportions must sum to 1.0, got {total:.3f}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Validate all distribution keys have priors
|
|
207
|
+
for kind in self.agent_distribution:
|
|
208
|
+
if kind not in self.kind_priors:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"Agent kind '{kind}' in agent_distribution has no entry in kind_priors. "
|
|
211
|
+
f"Add a prior for it or use one of: {list(self.kind_priors.keys())}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Auto-derive search prefix if not provided
|
|
215
|
+
if self.search_query_prefix is None:
|
|
216
|
+
# Use first 8 words of critical_event + topic
|
|
217
|
+
words = self.critical_event.split()[:8]
|
|
218
|
+
self.search_query_prefix = " ".join(words) + " " + self.topic
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def parsed_date(self) -> date:
|
|
222
|
+
return self._parsed_date
|