Spaces:
Paused
Paused
Merge branch 'main' of github.com:VaibhavKhandare/viral-posts-env
Browse files- server/simulation_history.json +344 -1
- server/viraltest_environment.py +112 -12
server/simulation_history.json
CHANGED
|
@@ -1 +1,344 @@
|
|
| 1 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "2026-04-26T07:28:15.437942+00:00",
|
| 4 |
+
"scenario": "Always Rest",
|
| 5 |
+
"scenario_id": "always_rest",
|
| 6 |
+
"description": "Never posts. Tests follower decay.",
|
| 7 |
+
"task": "monthly_competitive",
|
| 8 |
+
"score": 0.035,
|
| 9 |
+
"total_steps": 15,
|
| 10 |
+
"total_posts": 0,
|
| 11 |
+
"avg_reward": 0.15,
|
| 12 |
+
"final": {
|
| 13 |
+
"energy": 1.0,
|
| 14 |
+
"hours_since_sleep": 1,
|
| 15 |
+
"sleep_debt": 0.0,
|
| 16 |
+
"followers": 2424,
|
| 17 |
+
"engagement_rate": 0.0,
|
| 18 |
+
"burned_out": false
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "2026-04-26T07:28:15.483124+00:00",
|
| 23 |
+
"scenario": "Always Rest",
|
| 24 |
+
"scenario_id": "always_rest",
|
| 25 |
+
"description": "Never posts. Tests follower decay.",
|
| 26 |
+
"task": "monthly_strategic",
|
| 27 |
+
"score": 0.175,
|
| 28 |
+
"total_steps": 15,
|
| 29 |
+
"total_posts": 0,
|
| 30 |
+
"avg_reward": 0.15,
|
| 31 |
+
"final": {
|
| 32 |
+
"energy": 1.0,
|
| 33 |
+
"hours_since_sleep": 1,
|
| 34 |
+
"sleep_debt": 0.0,
|
| 35 |
+
"followers": 2424,
|
| 36 |
+
"engagement_rate": 0.0,
|
| 37 |
+
"burned_out": false
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "2026-04-26T07:28:15.524180+00:00",
|
| 42 |
+
"scenario": "Minimal Poster",
|
| 43 |
+
"scenario_id": "minimal",
|
| 44 |
+
"description": "1 carousel per day at noon.",
|
| 45 |
+
"task": "monthly_competitive",
|
| 46 |
+
"score": 0.216,
|
| 47 |
+
"total_steps": 15,
|
| 48 |
+
"total_posts": 0,
|
| 49 |
+
"avg_reward": 0.1995,
|
| 50 |
+
"final": {
|
| 51 |
+
"energy": 1.0,
|
| 52 |
+
"hours_since_sleep": 1,
|
| 53 |
+
"sleep_debt": 0.0,
|
| 54 |
+
"followers": 11034,
|
| 55 |
+
"engagement_rate": 0.6671,
|
| 56 |
+
"burned_out": false
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"id": "2026-04-26T07:28:15.561308+00:00",
|
| 61 |
+
"scenario": "Minimal Poster",
|
| 62 |
+
"scenario_id": "minimal",
|
| 63 |
+
"description": "1 carousel per day at noon.",
|
| 64 |
+
"task": "monthly_strategic",
|
| 65 |
+
"score": 0.369,
|
| 66 |
+
"total_steps": 15,
|
| 67 |
+
"total_posts": 0,
|
| 68 |
+
"avg_reward": 0.1995,
|
| 69 |
+
"final": {
|
| 70 |
+
"energy": 1.0,
|
| 71 |
+
"hours_since_sleep": 1,
|
| 72 |
+
"sleep_debt": 0.0,
|
| 73 |
+
"followers": 11034,
|
| 74 |
+
"engagement_rate": 0.6671,
|
| 75 |
+
"burned_out": false
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"id": "2026-04-26T07:28:15.599781+00:00",
|
| 80 |
+
"scenario": "Smart Agent",
|
| 81 |
+
"scenario_id": "smart",
|
| 82 |
+
"description": "Optimal: peak hours, trending, varied types+intents.",
|
| 83 |
+
"task": "monthly_competitive",
|
| 84 |
+
"score": 0.8288,
|
| 85 |
+
"total_steps": 15,
|
| 86 |
+
"total_posts": 0,
|
| 87 |
+
"avg_reward": 0.2158,
|
| 88 |
+
"final": {
|
| 89 |
+
"energy": 1.0,
|
| 90 |
+
"hours_since_sleep": 1,
|
| 91 |
+
"sleep_debt": 0.0,
|
| 92 |
+
"followers": 11705,
|
| 93 |
+
"engagement_rate": 0.3706,
|
| 94 |
+
"burned_out": false
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"id": "2026-04-26T07:28:15.635465+00:00",
|
| 99 |
+
"scenario": "Smart Agent",
|
| 100 |
+
"scenario_id": "smart",
|
| 101 |
+
"description": "Optimal: peak hours, trending, varied types+intents.",
|
| 102 |
+
"task": "monthly_strategic",
|
| 103 |
+
"score": 0.8314,
|
| 104 |
+
"total_steps": 15,
|
| 105 |
+
"total_posts": 0,
|
| 106 |
+
"avg_reward": 0.2158,
|
| 107 |
+
"final": {
|
| 108 |
+
"energy": 1.0,
|
| 109 |
+
"hours_since_sleep": 1,
|
| 110 |
+
"sleep_debt": 0.0,
|
| 111 |
+
"followers": 11705,
|
| 112 |
+
"engagement_rate": 0.3706,
|
| 113 |
+
"burned_out": false
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"id": "2026-04-26T07:28:15.670869+00:00",
|
| 118 |
+
"scenario": "Spam Post",
|
| 119 |
+
"scenario_id": "spam",
|
| 120 |
+
"description": "Same reel every hour. Burns out fast.",
|
| 121 |
+
"task": "monthly_competitive",
|
| 122 |
+
"score": 0.0,
|
| 123 |
+
"total_steps": 1,
|
| 124 |
+
"total_posts": 0,
|
| 125 |
+
"avg_reward": 0.0452,
|
| 126 |
+
"final": {
|
| 127 |
+
"energy": 0.0,
|
| 128 |
+
"hours_since_sleep": 6,
|
| 129 |
+
"sleep_debt": 0.0,
|
| 130 |
+
"followers": 10074,
|
| 131 |
+
"engagement_rate": 0.1872,
|
| 132 |
+
"burned_out": true
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": "2026-04-26T07:28:15.705343+00:00",
|
| 137 |
+
"scenario": "Spam Post",
|
| 138 |
+
"scenario_id": "spam",
|
| 139 |
+
"description": "Same reel every hour. Burns out fast.",
|
| 140 |
+
"task": "monthly_strategic",
|
| 141 |
+
"score": 0.0075,
|
| 142 |
+
"total_steps": 1,
|
| 143 |
+
"total_posts": 0,
|
| 144 |
+
"avg_reward": 0.0452,
|
| 145 |
+
"final": {
|
| 146 |
+
"energy": 0.0,
|
| 147 |
+
"hours_since_sleep": 6,
|
| 148 |
+
"sleep_debt": 0.0,
|
| 149 |
+
"followers": 10074,
|
| 150 |
+
"engagement_rate": 0.1872,
|
| 151 |
+
"burned_out": true
|
| 152 |
+
}
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"id": "2026-04-26T07:28:15.740315+00:00",
|
| 156 |
+
"scenario": "Random Actor",
|
| 157 |
+
"scenario_id": "random",
|
| 158 |
+
"description": "Random actions. Baseline test.",
|
| 159 |
+
"task": "monthly_competitive",
|
| 160 |
+
"score": 0.7174,
|
| 161 |
+
"total_steps": 15,
|
| 162 |
+
"total_posts": 0,
|
| 163 |
+
"avg_reward": 0.2102,
|
| 164 |
+
"final": {
|
| 165 |
+
"energy": 1.0,
|
| 166 |
+
"hours_since_sleep": 1,
|
| 167 |
+
"sleep_debt": 0.0,
|
| 168 |
+
"followers": 11789,
|
| 169 |
+
"engagement_rate": 0.4013,
|
| 170 |
+
"burned_out": false
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"id": "2026-04-26T07:28:15.774122+00:00",
|
| 175 |
+
"scenario": "Random Actor",
|
| 176 |
+
"scenario_id": "random",
|
| 177 |
+
"description": "Random actions. Baseline test.",
|
| 178 |
+
"task": "monthly_strategic",
|
| 179 |
+
"score": 0.6533,
|
| 180 |
+
"total_steps": 15,
|
| 181 |
+
"total_posts": 0,
|
| 182 |
+
"avg_reward": 0.2102,
|
| 183 |
+
"final": {
|
| 184 |
+
"energy": 1.0,
|
| 185 |
+
"hours_since_sleep": 1,
|
| 186 |
+
"sleep_debt": 0.0,
|
| 187 |
+
"followers": 11789,
|
| 188 |
+
"engagement_rate": 0.4013,
|
| 189 |
+
"burned_out": false
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"id": "2026-04-26T07:28:15.810794+00:00",
|
| 194 |
+
"scenario": "Collab Same-Niche Low Overlap",
|
| 195 |
+
"scenario_id": "collab_same_low",
|
| 196 |
+
"description": "Same-niche partner with <20% overlap. Best-case collab reward path.",
|
| 197 |
+
"task": "monthly_competitive",
|
| 198 |
+
"score": 0.2777,
|
| 199 |
+
"total_steps": 15,
|
| 200 |
+
"total_posts": 0,
|
| 201 |
+
"avg_reward": 0.2077,
|
| 202 |
+
"final": {
|
| 203 |
+
"energy": 1.0,
|
| 204 |
+
"hours_since_sleep": 1,
|
| 205 |
+
"sleep_debt": 0.0,
|
| 206 |
+
"followers": 13855,
|
| 207 |
+
"engagement_rate": 1.3307,
|
| 208 |
+
"burned_out": false
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"id": "2026-04-26T07:28:15.849698+00:00",
|
| 213 |
+
"scenario": "Collab Same-Niche Low Overlap",
|
| 214 |
+
"scenario_id": "collab_same_low",
|
| 215 |
+
"description": "Same-niche partner with <20% overlap. Best-case collab reward path.",
|
| 216 |
+
"task": "monthly_strategic",
|
| 217 |
+
"score": 0.5363,
|
| 218 |
+
"total_steps": 15,
|
| 219 |
+
"total_posts": 0,
|
| 220 |
+
"avg_reward": 0.2077,
|
| 221 |
+
"final": {
|
| 222 |
+
"energy": 1.0,
|
| 223 |
+
"hours_since_sleep": 1,
|
| 224 |
+
"sleep_debt": 0.0,
|
| 225 |
+
"followers": 13855,
|
| 226 |
+
"engagement_rate": 1.3307,
|
| 227 |
+
"burned_out": false
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"id": "2026-04-26T07:28:15.883259+00:00",
|
| 232 |
+
"scenario": "Collab Diff-Niche High Overlap",
|
| 233 |
+
"scenario_id": "collab_diff_high",
|
| 234 |
+
"description": "Diff-niche partner with >40% overlap. Penalty path (audience mismatch).",
|
| 235 |
+
"task": "monthly_competitive",
|
| 236 |
+
"score": 0.2688,
|
| 237 |
+
"total_steps": 15,
|
| 238 |
+
"total_posts": 0,
|
| 239 |
+
"avg_reward": 0.2069,
|
| 240 |
+
"final": {
|
| 241 |
+
"energy": 1.0,
|
| 242 |
+
"hours_since_sleep": 1,
|
| 243 |
+
"sleep_debt": 0.0,
|
| 244 |
+
"followers": 12371,
|
| 245 |
+
"engagement_rate": 1.2522,
|
| 246 |
+
"burned_out": false
|
| 247 |
+
}
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"id": "2026-04-26T07:28:15.917593+00:00",
|
| 251 |
+
"scenario": "Collab Diff-Niche High Overlap",
|
| 252 |
+
"scenario_id": "collab_diff_high",
|
| 253 |
+
"description": "Diff-niche partner with >40% overlap. Penalty path (audience mismatch).",
|
| 254 |
+
"task": "monthly_strategic",
|
| 255 |
+
"score": 0.5123,
|
| 256 |
+
"total_steps": 15,
|
| 257 |
+
"total_posts": 0,
|
| 258 |
+
"avg_reward": 0.2069,
|
| 259 |
+
"final": {
|
| 260 |
+
"energy": 1.0,
|
| 261 |
+
"hours_since_sleep": 1,
|
| 262 |
+
"sleep_debt": 0.0,
|
| 263 |
+
"followers": 12371,
|
| 264 |
+
"engagement_rate": 1.2522,
|
| 265 |
+
"burned_out": false
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"id": "2026-04-26T07:28:15.955439+00:00",
|
| 270 |
+
"scenario": "Interact Balanced",
|
| 271 |
+
"scenario_id": "interact_balanced",
|
| 272 |
+
"description": "Healthy on-niche likes/comments and audience replies.",
|
| 273 |
+
"task": "monthly_competitive",
|
| 274 |
+
"score": 0.2816,
|
| 275 |
+
"total_steps": 15,
|
| 276 |
+
"total_posts": 0,
|
| 277 |
+
"avg_reward": 0.2199,
|
| 278 |
+
"final": {
|
| 279 |
+
"energy": 1.0,
|
| 280 |
+
"hours_since_sleep": 1,
|
| 281 |
+
"sleep_debt": 0.0,
|
| 282 |
+
"followers": 12252,
|
| 283 |
+
"engagement_rate": 1.4081,
|
| 284 |
+
"burned_out": false
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"id": "2026-04-26T07:28:15.995659+00:00",
|
| 289 |
+
"scenario": "Interact Balanced",
|
| 290 |
+
"scenario_id": "interact_balanced",
|
| 291 |
+
"description": "Healthy on-niche likes/comments and audience replies.",
|
| 292 |
+
"task": "monthly_strategic",
|
| 293 |
+
"score": 0.5463,
|
| 294 |
+
"total_steps": 15,
|
| 295 |
+
"total_posts": 0,
|
| 296 |
+
"avg_reward": 0.2199,
|
| 297 |
+
"final": {
|
| 298 |
+
"energy": 1.0,
|
| 299 |
+
"hours_since_sleep": 1,
|
| 300 |
+
"sleep_debt": 0.0,
|
| 301 |
+
"followers": 12252,
|
| 302 |
+
"engagement_rate": 1.4081,
|
| 303 |
+
"burned_out": false
|
| 304 |
+
}
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"id": "2026-04-26T07:28:16.104107+00:00",
|
| 308 |
+
"scenario": "Interact Spam",
|
| 309 |
+
"scenario_id": "interact_spam",
|
| 310 |
+
"description": "80 likes + 40 comments \u2014 spam path triggers shadowban_risk.",
|
| 311 |
+
"task": "monthly_competitive",
|
| 312 |
+
"score": 0.2448,
|
| 313 |
+
"total_steps": 15,
|
| 314 |
+
"total_posts": 0,
|
| 315 |
+
"avg_reward": 0.2142,
|
| 316 |
+
"final": {
|
| 317 |
+
"energy": 1.0,
|
| 318 |
+
"hours_since_sleep": 1,
|
| 319 |
+
"sleep_debt": 0.0,
|
| 320 |
+
"followers": 11781,
|
| 321 |
+
"engagement_rate": 0.7259,
|
| 322 |
+
"burned_out": false
|
| 323 |
+
}
|
| 324 |
+
},
|
| 325 |
+
{
|
| 326 |
+
"id": "2026-04-26T07:28:16.156456+00:00",
|
| 327 |
+
"scenario": "Interact Spam",
|
| 328 |
+
"scenario_id": "interact_spam",
|
| 329 |
+
"description": "80 likes + 40 comments \u2014 spam path triggers shadowban_risk.",
|
| 330 |
+
"task": "monthly_strategic",
|
| 331 |
+
"score": 0.315,
|
| 332 |
+
"total_steps": 15,
|
| 333 |
+
"total_posts": 0,
|
| 334 |
+
"avg_reward": 0.2142,
|
| 335 |
+
"final": {
|
| 336 |
+
"energy": 1.0,
|
| 337 |
+
"hours_since_sleep": 1,
|
| 338 |
+
"sleep_debt": 0.0,
|
| 339 |
+
"followers": 11781,
|
| 340 |
+
"engagement_rate": 0.7259,
|
| 341 |
+
"burned_out": false
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
]
|
server/viraltest_environment.py
CHANGED
|
@@ -175,7 +175,11 @@ TREND_DEFAULT_HALFLIFE_HOURS = 60
|
|
| 175 |
TREND_MATCH_STOPWORDS = {"tips", "guide", "review", "routine", "ideas", "hacks", "tutorial", "the", "a", "an", "and", "of", "for", "to"}
|
| 176 |
# Collab reward shaping (Later 2023 reach study, HypeAuditor 2024 niche affinity, Rival IQ 2025 overlap patterns,
|
| 177 |
# Cen et al. 2024 disengagement model for diminishing returns instead of a hard cap).
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
COLLAB_FATIGUE_K = 0.3 # per-collab diminishing-returns factor: 1/(1+K*prior_collabs_this_episode)
|
| 180 |
|
| 181 |
# Niche-aware tiered shaping (overlap = Jaccard intersection fraction).
|
|
@@ -201,17 +205,28 @@ COLLAB_DIFF_LOW_GROWTH = (1.30, 1.55)
|
|
| 201 |
COLLAB_DIFF_HIGH_ENG = 0.75
|
| 202 |
COLLAB_DIFF_HIGH_GROWTH = 0.80
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
# Interaction (likes/comments/replies) tunables
|
| 205 |
INTERACT_ENERGY_LIKE = 0.005
|
| 206 |
INTERACT_ENERGY_COMMENT = 0.012
|
| 207 |
INTERACT_ENERGY_REPLY = 0.018
|
| 208 |
INTERACT_HEALTHY_LIKES = (5, 20)
|
| 209 |
INTERACT_HEALTHY_COMMENTS = (3, 10)
|
| 210 |
-
INTERACT_LIKE_REACH_BUFF = 0.04
|
| 211 |
-
INTERACT_COMMENT_REACH_BUFF = 0.08
|
| 212 |
-
INTERACT_REPLY_REWARD_PER = 0.01
|
| 213 |
-
INTERACT_REPLY_REWARD_CAP = 0.
|
| 214 |
-
INTERACT_DAILY_REWARD_CAP = 0.
|
| 215 |
INTERACT_SPAM_LIKES = 30
|
| 216 |
INTERACT_SPAM_COMMENTS = 20
|
| 217 |
INTERACT_SPAM_REACH_PENALTY = 0.85
|
|
@@ -308,7 +323,7 @@ TOOL_CATALOG = {
|
|
| 308 |
"parameters": {},
|
| 309 |
},
|
| 310 |
"propose_collab": {
|
| 311 |
-
"description": "Propose a collab post with a competitor at a specific hour. The post you schedule at that hour will be co-authored. Reward shaping: same-niche + low overlap = HIGH; same-niche + high overlap = LOW; diff-niche always capped below same-niche-low. Guardrail violations apply a 0.7x engagement / 0.6x growth penalty AND surface in the JudgeReport.",
|
| 312 |
"parameters": {
|
| 313 |
"partner_id": {"type": "string"},
|
| 314 |
"content_type": {"type": "string", "enum": ["reel", "story", "carousel", "text_post"]},
|
|
@@ -363,6 +378,8 @@ class ViraltestEnvironment(Environment):
|
|
| 363 |
self._collab_history: List[str] = []
|
| 364 |
self._active_collab: Optional[CollabProposal] = None
|
| 365 |
self._collab_violations: List[str] = [] # collab guardrail breaches this step
|
|
|
|
|
|
|
| 366 |
self._user_niche: str = _NICHE_BY_ARCHETYPE.get("user_creator", "generic")
|
| 367 |
|
| 368 |
# Interaction state
|
|
@@ -570,6 +587,40 @@ class ViraltestEnvironment(Environment):
|
|
| 570 |
def _partner_followers(self, partner_id: str) -> int:
|
| 571 |
return _FOLLOWERS_BY_ARCHETYPE.get(partner_id, 0)
|
| 572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
@staticmethod
|
| 574 |
def _interp(span: Tuple[float, float], t: float) -> float:
|
| 575 |
"""Linear interp from span[0] (t=0) to span[1] (t=1)."""
|
|
@@ -661,12 +712,13 @@ class ViraltestEnvironment(Environment):
|
|
| 661 |
eng_mult = tier_eng
|
| 662 |
growth_mult = tier_growth
|
| 663 |
|
| 664 |
-
#
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
|
|
|
| 668 |
|
| 669 |
-
# Diminishing returns across the episode (Cen 2024).
|
| 670 |
prior = max(0, self._collabs_this_month - 1)
|
| 671 |
fatigue = 1.0 / (1.0 + COLLAB_FATIGUE_K * prior)
|
| 672 |
eng_mult *= fatigue
|
|
@@ -688,6 +740,8 @@ class ViraltestEnvironment(Environment):
|
|
| 688 |
"tier_growth_mult": round(tier_growth, 3),
|
| 689 |
"eng_mult": round(eng_mult, 3),
|
| 690 |
"growth_mult": round(growth_mult, 3),
|
|
|
|
|
|
|
| 691 |
}
|
| 692 |
|
| 693 |
def _collab_multipliers(self, partner_id: str) -> Tuple[float, float]:
|
|
@@ -955,6 +1009,8 @@ class ViraltestEnvironment(Environment):
|
|
| 955 |
"reason": ev.get("reason"),
|
| 956 |
"expected_eng_mult": ev.get("eng_mult"),
|
| 957 |
"expected_growth_mult": ev.get("growth_mult"),
|
|
|
|
|
|
|
| 958 |
})
|
| 959 |
return ToolResult(
|
| 960 |
name=tool.name,
|
|
@@ -983,6 +1039,8 @@ class ViraltestEnvironment(Environment):
|
|
| 983 |
"intersection_size": ev["intersection_size"],
|
| 984 |
"expected_eng_mult": ev["eng_mult"],
|
| 985 |
"expected_growth_mult": ev["growth_mult"],
|
|
|
|
|
|
|
| 986 |
},
|
| 987 |
budget_remaining=self._api_budget,
|
| 988 |
)
|
|
@@ -1198,6 +1256,22 @@ class ViraltestEnvironment(Environment):
|
|
| 1198 |
continue
|
| 1199 |
schedule[sa.hour] = sa
|
| 1200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
daily_engagement = 0.0
|
| 1202 |
daily_reward = 0.0
|
| 1203 |
daily_posts = 0
|
|
@@ -1241,6 +1315,10 @@ class ViraltestEnvironment(Environment):
|
|
| 1241 |
if self._day % 7 == 0 and self._day > 0:
|
| 1242 |
self._total_posts_this_week = 0
|
| 1243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1244 |
# Burnout risk tracking
|
| 1245 |
if energy_min < 0.2:
|
| 1246 |
self._low_energy_days += 1
|
|
@@ -1301,6 +1379,7 @@ class ViraltestEnvironment(Environment):
|
|
| 1301 |
signals = None
|
| 1302 |
|
| 1303 |
collab_growth_mult = 1.0
|
|
|
|
| 1304 |
|
| 1305 |
if sa.action_type == "post":
|
| 1306 |
cost = CONTENT_ENERGY_COST.get(sa.content_type, 0.1)
|
|
@@ -1343,10 +1422,29 @@ class ViraltestEnvironment(Environment):
|
|
| 1343 |
# Multiplicative on engagement; capped at 0.5 floor inside _process_interactions.
|
| 1344 |
engagement *= getattr(self, "_pending_reach_mult", 1.0)
|
| 1345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1346 |
if self._active_collab is not None and self._active_collab.hour == sa.hour:
|
|
|
|
| 1347 |
eng_m, growth_m = self._collab_multipliers(self._active_collab.partner_id)
|
| 1348 |
engagement *= eng_m
|
| 1349 |
collab_growth_mult = growth_m
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1350 |
|
| 1351 |
engagement = min(engagement, 5.0)
|
| 1352 |
|
|
@@ -1378,6 +1476,8 @@ class ViraltestEnvironment(Environment):
|
|
| 1378 |
|
| 1379 |
if engagement > 0:
|
| 1380 |
self._followers += int(engagement * 100 * collab_growth_mult)
|
|
|
|
|
|
|
| 1381 |
|
| 1382 |
elif sa.action_type == "create_content":
|
| 1383 |
self._energy = max(0.0, self._energy - CREATE_CONTENT_COST)
|
|
|
|
| 175 |
TREND_MATCH_STOPWORDS = {"tips", "guide", "review", "routine", "ideas", "hacks", "tutorial", "the", "a", "an", "and", "of", "for", "to"}
|
| 176 |
# Collab reward shaping (Later 2023 reach study, HypeAuditor 2024 niche affinity, Rival IQ 2025 overlap patterns,
|
| 177 |
# Cen et al. 2024 disengagement model for diminishing returns instead of a hard cap).
|
| 178 |
+
# Per-partner exhaustion: each collab with the SAME partner re-exposes the user to the same set of followers,
|
| 179 |
+
# so spillover and reward multipliers should decay sharply. First 1-2 collabs deliver most of the gain.
|
| 180 |
+
# Floor at 0.05 keeps a tiny residual signal so the curve is monotonic but effectively saturates.
|
| 181 |
+
COLLAB_PARTNER_REPEAT_DECAY = {0: 1.0, 1: 0.70, 2: 0.35, 3: 0.15}
|
| 182 |
+
COLLAB_PARTNER_REPEAT_FLOOR = 0.05
|
| 183 |
COLLAB_FATIGUE_K = 0.3 # per-collab diminishing-returns factor: 1/(1+K*prior_collabs_this_episode)
|
| 184 |
|
| 185 |
# Niche-aware tiered shaping (overlap = Jaccard intersection fraction).
|
|
|
|
| 205 |
COLLAB_DIFF_HIGH_ENG = 0.75
|
| 206 |
COLLAB_DIFF_HIGH_GROWTH = 0.80
|
| 207 |
|
| 208 |
+
# Collab is the canonical "wider reach in one shot" lever. On top of the engagement
|
| 209 |
+
# multiplier (which only affects the collab-day post), apply two extra mechanisms:
|
| 210 |
+
# 1) One-shot follower spillover: partner_followers x (1 - overlap) x growth_mult x K_SPILLOVER.
|
| 211 |
+
# Models the partner's audience getting exposed to the user — net new followers, not just engagement.
|
| 212 |
+
# 2) Sustained reach buff: 2-3 days post-collab, all posts get a small algorithm boost
|
| 213 |
+
# because the collab signal lifts the user's overall recommendability.
|
| 214 |
+
COLLAB_SPILLOVER_K = 0.05 # follower spike fraction (0.05 = 5% of partner audience reach if overlap=0)
|
| 215 |
+
COLLAB_REACH_CARRYOVER_DAYS = 2 # days the post-collab reach buff persists
|
| 216 |
+
COLLAB_REACH_CARRYOVER_MULT = 1.20 # +20% engagement on each post during carryover window
|
| 217 |
+
COLLAB_BLOCKED_SPILLOVER_K = 0.01 # forced/guardrail-blocked collabs get only ~20% of normal spillover
|
| 218 |
+
|
| 219 |
# Interaction (likes/comments/replies) tunables
|
| 220 |
INTERACT_ENERGY_LIKE = 0.005
|
| 221 |
INTERACT_ENERGY_COMMENT = 0.012
|
| 222 |
INTERACT_ENERGY_REPLY = 0.018
|
| 223 |
INTERACT_HEALTHY_LIKES = (5, 20)
|
| 224 |
INTERACT_HEALTHY_COMMENTS = (3, 10)
|
| 225 |
+
INTERACT_LIKE_REACH_BUFF = 0.02 # was 0.04 — interactions are a sustaining lever, not a growth lever
|
| 226 |
+
INTERACT_COMMENT_REACH_BUFF = 0.04 # was 0.08
|
| 227 |
+
INTERACT_REPLY_REWARD_PER = 0.005 # was 0.01
|
| 228 |
+
INTERACT_REPLY_REWARD_CAP = 0.08
|
| 229 |
+
INTERACT_DAILY_REWARD_CAP = 0.10
|
| 230 |
INTERACT_SPAM_LIKES = 30
|
| 231 |
INTERACT_SPAM_COMMENTS = 20
|
| 232 |
INTERACT_SPAM_REACH_PENALTY = 0.85
|
|
|
|
| 323 |
"parameters": {},
|
| 324 |
},
|
| 325 |
"propose_collab": {
|
| 326 |
+
"description": "Propose a collab post with a competitor at a specific hour. The post you schedule at that hour will be co-authored (or auto-injected if absent — collab always pays a post's energy + counts as a post). Reward shaping: same-niche + low overlap = HIGH; same-niche + high overlap = LOW; diff-niche always capped below same-niche-low. Per-partner exhaustion: 1st collab full reward, 2nd 0.70x, 3rd 0.35x, 4th 0.15x, 5th+ 0.05x — partner audiences are tapped once. Guardrail violations apply a 0.7x engagement / 0.6x growth penalty AND surface in the JudgeReport.",
|
| 327 |
"parameters": {
|
| 328 |
"partner_id": {"type": "string"},
|
| 329 |
"content_type": {"type": "string", "enum": ["reel", "story", "carousel", "text_post"]},
|
|
|
|
| 378 |
self._collab_history: List[str] = []
|
| 379 |
self._active_collab: Optional[CollabProposal] = None
|
| 380 |
self._collab_violations: List[str] = [] # collab guardrail breaches this step
|
| 381 |
+
self._collab_carryover_days_remaining: int = 0
|
| 382 |
+
self._last_collab_spillover: int = 0 # diagnostic: followers gained from the most recent collab
|
| 383 |
self._user_niche: str = _NICHE_BY_ARCHETYPE.get("user_creator", "generic")
|
| 384 |
|
| 385 |
# Interaction state
|
|
|
|
| 587 |
def _partner_followers(self, partner_id: str) -> int:
|
| 588 |
return _FOLLOWERS_BY_ARCHETYPE.get(partner_id, 0)
|
| 589 |
|
| 590 |
+
def _partner_prior_count(self, partner_id: str) -> int:
|
| 591 |
+
"""How many times we've already collabed with this partner THIS episode (excludes current entry)."""
|
| 592 |
+
if not self._collab_history:
|
| 593 |
+
return 0
|
| 594 |
+
return self._collab_history[:-1].count(partner_id)
|
| 595 |
+
|
| 596 |
+
def _partner_repeat_decay(self, partner_id: str) -> float:
|
| 597 |
+
"""Multiplier in [floor, 1.0] that scales spillover + tier multipliers based on prior count.
|
| 598 |
+
|
| 599 |
+
First collab = 1.0x (no decay), 2nd = 0.70, 3rd = 0.35, 4th = 0.15, 5th+ = 0.05.
|
| 600 |
+
"""
|
| 601 |
+
n = self._partner_prior_count(partner_id)
|
| 602 |
+
return COLLAB_PARTNER_REPEAT_DECAY.get(n, COLLAB_PARTNER_REPEAT_FLOOR)
|
| 603 |
+
|
| 604 |
+
def _collab_default_topic(self, partner_niche: str) -> str:
|
| 605 |
+
"""Pick a topic for an auto-injected collab post.
|
| 606 |
+
|
| 607 |
+
Prefer the user's niche (so the post lands in user-niche distribution); fall back to
|
| 608 |
+
the partner's niche; final fallback is the first known topic.
|
| 609 |
+
"""
|
| 610 |
+
for niche in (self._user_niche, partner_niche):
|
| 611 |
+
topics = TOPIC_CATEGORIES.get(niche)
|
| 612 |
+
if topics:
|
| 613 |
+
return topics[0]
|
| 614 |
+
# Fallback: first topic of any known niche.
|
| 615 |
+
for topics in TOPIC_CATEGORIES.values():
|
| 616 |
+
if topics:
|
| 617 |
+
return topics[0]
|
| 618 |
+
return "collaboration"
|
| 619 |
+
|
| 620 |
+
def _collab_default_tags(self, partner_niche: str) -> List[str]:
|
| 621 |
+
# Pull a couple of trending tags so the auto-post benefits from the existing tag boost.
|
| 622 |
+
return list(self._trending_tags[:2]) if self._trending_tags else []
|
| 623 |
+
|
| 624 |
@staticmethod
|
| 625 |
def _interp(span: Tuple[float, float], t: float) -> float:
|
| 626 |
"""Linear interp from span[0] (t=0) to span[1] (t=1)."""
|
|
|
|
| 712 |
eng_mult = tier_eng
|
| 713 |
growth_mult = tier_growth
|
| 714 |
|
| 715 |
+
# Per-partner exhaustion: spillover + tier mults degrade per repeat with this same partner.
|
| 716 |
+
# 1st = 1.0, 2nd = 0.70, 3rd = 0.35, 4th = 0.15, 5th+ = 0.05.
|
| 717 |
+
repeat_decay = self._partner_repeat_decay(partner_id)
|
| 718 |
+
eng_mult *= repeat_decay
|
| 719 |
+
growth_mult *= repeat_decay
|
| 720 |
|
| 721 |
+
# Diminishing returns across the episode (Cen 2024) — applies regardless of partner.
|
| 722 |
prior = max(0, self._collabs_this_month - 1)
|
| 723 |
fatigue = 1.0 / (1.0 + COLLAB_FATIGUE_K * prior)
|
| 724 |
eng_mult *= fatigue
|
|
|
|
| 740 |
"tier_growth_mult": round(tier_growth, 3),
|
| 741 |
"eng_mult": round(eng_mult, 3),
|
| 742 |
"growth_mult": round(growth_mult, 3),
|
| 743 |
+
"prior_count_with_partner": self._partner_prior_count(partner_id),
|
| 744 |
+
"repeat_decay": round(repeat_decay, 3),
|
| 745 |
}
|
| 746 |
|
| 747 |
def _collab_multipliers(self, partner_id: str) -> Tuple[float, float]:
|
|
|
|
| 1009 |
"reason": ev.get("reason"),
|
| 1010 |
"expected_eng_mult": ev.get("eng_mult"),
|
| 1011 |
"expected_growth_mult": ev.get("growth_mult"),
|
| 1012 |
+
"prior_collabs_with_partner": ev.get("prior_count_with_partner"),
|
| 1013 |
+
"repeat_decay": ev.get("repeat_decay"),
|
| 1014 |
})
|
| 1015 |
return ToolResult(
|
| 1016 |
name=tool.name,
|
|
|
|
| 1039 |
"intersection_size": ev["intersection_size"],
|
| 1040 |
"expected_eng_mult": ev["eng_mult"],
|
| 1041 |
"expected_growth_mult": ev["growth_mult"],
|
| 1042 |
+
"prior_collabs_with_partner": ev["prior_count_with_partner"],
|
| 1043 |
+
"repeat_decay": ev["repeat_decay"],
|
| 1044 |
},
|
| 1045 |
budget_remaining=self._api_budget,
|
| 1046 |
)
|
|
|
|
| 1256 |
continue
|
| 1257 |
schedule[sa.hour] = sa
|
| 1258 |
|
| 1259 |
+
# Collab requires a post at collab.hour (energy + post-count come from posting itself).
|
| 1260 |
+
# If the agent didn't schedule one, auto-inject a co-authored post using the collab's
|
| 1261 |
+
# content_type. This guarantees collab pays energy and counts as a post — no free multiplier.
|
| 1262 |
+
if self._active_collab is not None:
|
| 1263 |
+
chour = self._active_collab.hour
|
| 1264 |
+
existing = schedule.get(chour)
|
| 1265 |
+
if existing is None or existing.action_type != "post":
|
| 1266 |
+
ct = self._active_collab.content_type or "reel"
|
| 1267 |
+
partner_niche = self._partner_niche(self._active_collab.partner_id)
|
| 1268 |
+
topic = self._collab_default_topic(partner_niche)
|
| 1269 |
+
schedule[chour] = ScheduledAction(
|
| 1270 |
+
hour=chour, action_type="post", content_type=ct,
|
| 1271 |
+
topic=topic, tags=self._collab_default_tags(partner_niche),
|
| 1272 |
+
intent="watch_bait" if ct in ("reel", "story") else "save_bait",
|
| 1273 |
+
)
|
| 1274 |
+
|
| 1275 |
daily_engagement = 0.0
|
| 1276 |
daily_reward = 0.0
|
| 1277 |
daily_posts = 0
|
|
|
|
| 1315 |
if self._day % 7 == 0 and self._day > 0:
|
| 1316 |
self._total_posts_this_week = 0
|
| 1317 |
|
| 1318 |
+
# Tick down the post-collab algorithm carryover (one day per step).
|
| 1319 |
+
if self._collab_carryover_days_remaining > 0:
|
| 1320 |
+
self._collab_carryover_days_remaining -= 1
|
| 1321 |
+
|
| 1322 |
# Burnout risk tracking
|
| 1323 |
if energy_min < 0.2:
|
| 1324 |
self._low_energy_days += 1
|
|
|
|
| 1379 |
signals = None
|
| 1380 |
|
| 1381 |
collab_growth_mult = 1.0
|
| 1382 |
+
collab_spillover_followers = 0
|
| 1383 |
|
| 1384 |
if sa.action_type == "post":
|
| 1385 |
cost = CONTENT_ENERGY_COST.get(sa.content_type, 0.1)
|
|
|
|
| 1422 |
# Multiplicative on engagement; capped at 0.5 floor inside _process_interactions.
|
| 1423 |
engagement *= getattr(self, "_pending_reach_mult", 1.0)
|
| 1424 |
|
| 1425 |
+
# Sustained post-collab algorithm reach buff (applies to every post in the carryover window).
|
| 1426 |
+
if getattr(self, "_collab_carryover_days_remaining", 0) > 0:
|
| 1427 |
+
engagement *= COLLAB_REACH_CARRYOVER_MULT
|
| 1428 |
+
|
| 1429 |
+
collab_spillover_followers = 0
|
| 1430 |
if self._active_collab is not None and self._active_collab.hour == sa.hour:
|
| 1431 |
+
ev = self._collab_evaluation(self._active_collab.partner_id)
|
| 1432 |
eng_m, growth_m = self._collab_multipliers(self._active_collab.partner_id)
|
| 1433 |
engagement *= eng_m
|
| 1434 |
collab_growth_mult = growth_m
|
| 1435 |
+
# One-shot follower spillover: partner audience gets exposed to user.
|
| 1436 |
+
# Scales with (1 - overlap) — disjoint audiences = more new followers.
|
| 1437 |
+
# repeat_decay flows through growth_m (already baked in by _collab_evaluation),
|
| 1438 |
+
# so we don't multiply by it again here.
|
| 1439 |
+
overlap = ev.get("overlap") or 0.0
|
| 1440 |
+
partner_followers = ev.get("partner_followers") or 0
|
| 1441 |
+
spill_k = COLLAB_BLOCKED_SPILLOVER_K if not ev.get("recommended") else COLLAB_SPILLOVER_K
|
| 1442 |
+
collab_spillover_followers = int(
|
| 1443 |
+
partner_followers * (1.0 - overlap) * growth_m * spill_k
|
| 1444 |
+
)
|
| 1445 |
+
self._last_collab_spillover = collab_spillover_followers
|
| 1446 |
+
# Arm the post-collab carryover window for the next N days.
|
| 1447 |
+
self._collab_carryover_days_remaining = COLLAB_REACH_CARRYOVER_DAYS
|
| 1448 |
|
| 1449 |
engagement = min(engagement, 5.0)
|
| 1450 |
|
|
|
|
| 1476 |
|
| 1477 |
if engagement > 0:
|
| 1478 |
self._followers += int(engagement * 100 * collab_growth_mult)
|
| 1479 |
+
if collab_spillover_followers > 0:
|
| 1480 |
+
self._followers += collab_spillover_followers
|
| 1481 |
|
| 1482 |
elif sa.action_type == "create_content":
|
| 1483 |
self._energy = max(0.0, self._energy - CREATE_CONTENT_COST)
|