33"""
44
55import hashlib
6+ import itertools
7+ import os
68import random
9+ import threading
710import time
811from dataclasses import dataclass
912from pathlib import Path
@@ -44,6 +47,10 @@ class Excuse:
4447 metadata : Dict [str , Any ]
4548
4649
50+ # Monotonic counter to guarantee changing seeds across rapid calls
51+ _SEED_COUNTER = itertools .count ()
52+
53+
4754class ExcuseGenerator :
4855 """
4956 Base excuse generator class.
@@ -82,13 +89,13 @@ def __init__(self, language: str = "en", data_path: Optional[Path] = None):
8289 def _build_markov_chain (self ):
8390 """Build Markov chain from technical terms."""
8491 # Get technical terms from data
85- technical_terms = []
92+ technical_terms : List [ str ] = []
8693 for category in ["quantum" , "technical" , "ai" ]:
8794 if category in self .data :
8895 technical_terms .extend (self .data [category ])
8996
9097 # Extract individual words and build corpus
91- corpus = []
98+ corpus : List [ str ] = []
9299 for term in technical_terms :
93100 corpus .extend (term .split ())
94101
@@ -113,22 +120,22 @@ def generate(
113120 """
114121 severity = self .analyzer .analyze (error_message )
115122
116- # Generate quantum seed for reproducible randomness
123+ # Create a local RNG seeded with high-entropy seed
117124 quantum_seed = self ._generate_quantum_seed (error_message )
118- random .seed (quantum_seed )
125+ rng = random .Random (quantum_seed )
119126
120127 # Select categories
121128 if category and category in self .data :
122129 primary_category = category
123130 else :
124- primary_category = random .choice (list (self .data .keys ()))
131+ primary_category = rng .choice (list (self .data .keys ()))
125132
126133 # Ensure we don't pick recommendations or connectors as primary
127134 while primary_category in ["recommendations" , "connectors" , "intensifiers" ]:
128- primary_category = random .choice (list (self .data .keys ()))
135+ primary_category = rng .choice (list (self .data .keys ()))
129136
130137 # Get excuse components
131- primary_excuse = random .choice (self .data [primary_category ])
138+ primary_excuse = rng .choice (self .data [primary_category ])
132139
133140 # Get secondary category
134141 available_categories = [
@@ -137,16 +144,16 @@ def generate(
137144 if k
138145 not in [primary_category , "recommendations" , "connectors" , "intensifiers" ]
139146 ]
140- secondary_category = random .choice (available_categories )
141- secondary_excuse = random .choice (self .data [secondary_category ])
147+ secondary_category = rng .choice (available_categories )
148+ secondary_excuse = rng .choice (self .data [secondary_category ])
142149
143150 # Get intensifier based on severity
144- intensifier = self ._get_intensifier (severity )
151+ intensifier = self ._get_intensifier (severity , rng )
145152
146153 # Get connector
147- connector = random .choice (self .data .get ("connectors" , ["which caused" ]))
154+ connector = rng .choice (self .data .get ("connectors" , ["which caused" ]))
148155
149- # Generate Markov nonsense
156+ # Generate Markov nonsense (uses module RNG internally; that's fine)
150157 markov_phrase = self .markov .generate (length = 5 )
151158
152159 # Construct the excuse
@@ -160,7 +167,7 @@ def generate(
160167 )
161168
162169 # Get recommendation
163- recommendation = random .choice (
170+ recommendation = rng .choice (
164171 self .data .get ("recommendations" , ["Try turning it off and on again" ])
165172 )
166173
@@ -174,7 +181,7 @@ def generate(
174181 severity = severity ,
175182 category = primary_category ,
176183 quality_score = quality_score ,
177- quantum_probability = random .random (),
184+ quantum_probability = rng .random (),
178185 language = self .language ,
179186 timestamp = time .time (),
180187 metadata = {
@@ -188,12 +195,20 @@ def generate(
188195 return excuse
189196
190197 def _generate_quantum_seed (self , error_message : str ) -> int :
191- """Generate a 'quantum' seed from the error message."""
192- hash_input = f"{ error_message } { time .time ()} "
193- hash_hex = hashlib .md5 (hash_input .encode ()).hexdigest ()[:8 ]
194- return int (hash_hex , 16 )
195-
196- def _get_intensifier (self , severity : str ) -> str :
198+ """
199+ Generate a high-entropy 64-bit seed that changes on every call,
200+ even when invoked multiple times within the same OS clock tick.
201+ """
202+ t1 = time .time_ns ()
203+ t2 = time .perf_counter_ns () # monotonic, high-res
204+ pid = os .getpid ()
205+ tid = threading .get_ident ()
206+ ctr = next (_SEED_COUNTER )
207+ h = hashlib .blake2b (digest_size = 8 )
208+ h .update (f"{ error_message } |{ t1 } |{ t2 } |{ pid } |{ tid } |{ ctr } " .encode ("utf-8" ))
209+ return int .from_bytes (h .digest (), "big" )
210+
211+ def _get_intensifier (self , severity : str , rng : random .Random ) -> str :
197212 """Get an intensifier based on severity."""
198213 intensifiers = self .data .get ("intensifiers" , {})
199214
@@ -202,7 +217,7 @@ def _get_intensifier(self, severity: str) -> str:
202217 else :
203218 severity_intensifiers = ["definitely" ]
204219
205- return random .choice (severity_intensifiers )
220+ return rng .choice (severity_intensifiers )
206221
207222 def _calculate_quality_score (self , excuse_text : str , seed : int ) -> int :
208223 """Calculate a 'quality score' for the excuse."""
@@ -219,15 +234,10 @@ def _calculate_quality_score(self, excuse_text: str, seed: int) -> int:
219234
220235 def generate_batch (self , count : int = 5 ) -> List [Excuse ]:
221236 """
222- Generate multiple excuses.
223-
224- Args:
225- count: Number of excuses to generate
226-
227- Returns:
228- List of Excuse objects
237+ Generate multiple excuses, ensuring textual uniqueness.
229238 """
230- excuses = []
239+ excuses : List [Excuse ] = []
240+ seen : set [str ] = set ()
231241
232242 # Sample error messages for variety
233243 sample_errors = [
@@ -241,10 +251,15 @@ def generate_batch(self, count: int = 5) -> List[Excuse]:
241251 "MemoryError: Out of memory" ,
242252 ]
243253
244- for _ in range (count ):
254+ attempts = 0
255+ max_attempts = count * 10
256+ while len (excuses ) < count and attempts < max_attempts :
245257 error = random .choice (sample_errors )
246258 excuse = self .generate (error )
247- excuses .append (excuse )
259+ if excuse .text not in seen :
260+ seen .add (excuse .text )
261+ excuses .append (excuse )
262+ attempts += 1
248263
249264 return excuses
250265
0 commit comments