1+ #!/usr/bin/env python3
2+ """
3+ Check for evolution changes in Living-LLM system and determine if auto-commit is needed.
4+ """
5+
6+ import os
7+ import json
8+ import yaml
9+ import hashlib
10+ from pathlib import Path
11+ from datetime import datetime
12+ import git
13+ import sys
14+
15+ def calculate_file_hash (file_path ):
16+ """Calculate SHA256 hash of a file."""
17+ if not os .path .exists (file_path ):
18+ return None
19+
20+ with open (file_path , 'rb' ) as f :
21+ return hashlib .sha256 (f .read ()).hexdigest ()
22+
23+ def load_evolution_state ():
24+ """Load current evolution state from checkpoints."""
25+ evolution_dir = Path ("evolution_checkpoints" )
26+ if not evolution_dir .exists ():
27+ return None
28+
29+ # Find latest evolution checkpoint
30+ checkpoints = list (evolution_dir .glob ("evolution_gen_*.json" ))
31+ if not checkpoints :
32+ return None
33+
34+ latest_checkpoint = max (checkpoints , key = os .path .getctime )
35+
36+ try :
37+ with open (latest_checkpoint , 'r' ) as f :
38+ return json .load (f )
39+ except Exception as e :
40+ print (f"Error loading evolution state: { e } " )
41+ return None
42+
43+ def load_previous_state ():
44+ """Load previous evolution state from git metadata."""
45+ state_file = Path (".github/evolution_state.json" )
46+ if not state_file .exists ():
47+ return {}
48+
49+ try :
50+ with open (state_file , 'r' ) as f :
51+ return json .load (f )
52+ except :
53+ return {}
54+
55+ def save_current_state (state ):
56+ """Save current evolution state for future comparison."""
57+ state_file = Path (".github/evolution_state.json" )
58+ state_file .parent .mkdir (parents = True , exist_ok = True )
59+
60+ with open (state_file , 'w' ) as f :
61+ json .dump (state , f , indent = 2 , default = str )
62+
63+ def check_model_changes ():
64+ """Check if model architecture files have changed."""
65+ model_files = [
66+ "src/core/model/transformer.py" ,
67+ "src/core/attention/multi_head_attention.py" ,
68+ "src/core/layers/" ,
69+ "configs/model/"
70+ ]
71+
72+ changes = {}
73+ for file_path in model_files :
74+ if os .path .exists (file_path ):
75+ if os .path .isdir (file_path ):
76+ # Check all files in directory
77+ for root , dirs , files in os .walk (file_path ):
78+ for file in files :
79+ if file .endswith ('.py' ) or file .endswith ('.yaml' ):
80+ full_path = os .path .join (root , file )
81+ changes [full_path ] = calculate_file_hash (full_path )
82+ else :
83+ changes [file_path ] = calculate_file_hash (file_path )
84+
85+ return changes
86+
87+ def check_training_progress ():
88+ """Check training progress and metrics."""
89+ logs_dir = Path ("logs/training" )
90+ if not logs_dir .exists ():
91+ return {}
92+
93+ # Get latest training log
94+ log_files = list (logs_dir .glob ("*.log" ))
95+ if not log_files :
96+ return {}
97+
98+ latest_log = max (log_files , key = os .path .getctime )
99+
100+ # Extract key metrics from log
101+ metrics = {
102+ 'latest_loss' : None ,
103+ 'best_loss' : None ,
104+ 'total_steps' : 0 ,
105+ 'last_update' : datetime .now ().isoformat ()
106+ }
107+
108+ try :
109+ with open (latest_log , 'r' ) as f :
110+ lines = f .readlines ()
111+ for line in reversed (lines [- 100 :]): # Check last 100 lines
112+ if 'loss:' in line .lower ():
113+ # Extract loss value
114+ import re
115+ loss_match = re .search (r'loss:\s*([0-9.]+)' , line .lower ())
116+ if loss_match and metrics ['latest_loss' ] is None :
117+ metrics ['latest_loss' ] = float (loss_match .group (1 ))
118+
119+ if 'step' in line .lower ():
120+ step_match = re .search (r'step[:\s]+(\d+)' , line .lower ())
121+ if step_match :
122+ metrics ['total_steps' ] = max (metrics ['total_steps' ], int (step_match .group (1 )))
123+ except Exception as e :
124+ print (f"Error parsing training logs: { e } " )
125+
126+ return metrics
127+
128+ def determine_change_type (current_state , previous_state ):
129+ """Determine the type and significance of changes."""
130+ changes = {
131+ 'has_changes' : False ,
132+ 'major_evolution' : False ,
133+ 'architecture_changed' : False ,
134+ 'performance_improved' : False ,
135+ 'new_generation' : False ,
136+ 'change_summary' : []
137+ }
138+
139+ # Check evolution progress
140+ if 'evolution' in current_state and 'evolution' in previous_state :
141+ current_gen = current_state ['evolution' ].get ('generation' , 0 )
142+ previous_gen = previous_state ['evolution' ].get ('generation' , 0 )
143+
144+ if current_gen > previous_gen :
145+ changes ['has_changes' ] = True
146+ changes ['new_generation' ] = True
147+ changes ['change_summary' ].append (f"Evolution advanced to generation { current_gen } " )
148+
149+ # Check if fitness improved significantly
150+ current_fitness = current_state ['evolution' ].get ('best_fitness' , 0 )
151+ previous_fitness = previous_state ['evolution' ].get ('best_fitness' , 0 )
152+
153+ if current_fitness > previous_fitness * 1.1 : # 10% improvement
154+ changes ['major_evolution' ] = True
155+ changes ['change_summary' ].append (f"Major fitness improvement: { previous_fitness :.4f} → { current_fitness :.4f} " )
156+
157+ # Check model architecture changes
158+ current_model = current_state .get ('model_files' , {})
159+ previous_model = previous_state .get ('model_files' , {})
160+
161+ architecture_changes = []
162+ for file_path , current_hash in current_model .items ():
163+ previous_hash = previous_model .get (file_path )
164+ if previous_hash != current_hash :
165+ changes ['has_changes' ] = True
166+ changes ['architecture_changed' ] = True
167+ architecture_changes .append (file_path )
168+
169+ if architecture_changes :
170+ changes ['change_summary' ].append (f"Architecture files changed: { ', ' .join (architecture_changes )} " )
171+
172+ # Check training performance
173+ current_training = current_state .get ('training' , {})
174+ previous_training = previous_state .get ('training' , {})
175+
176+ current_loss = current_training .get ('latest_loss' )
177+ previous_loss = previous_training .get ('latest_loss' )
178+
179+ if current_loss and previous_loss and current_loss < previous_loss * 0.95 : # 5% improvement
180+ changes ['has_changes' ] = True
181+ changes ['performance_improved' ] = True
182+ changes ['change_summary' ].append (f"Training loss improved: { previous_loss :.4f} → { current_loss :.4f} " )
183+
184+ return changes
185+
186+ def main ():
187+ """Main function to check for evolution changes."""
188+ print ("🔍 Checking for Living-LLM evolution changes..." )
189+
190+ # Load current system state
191+ current_state = {
192+ 'timestamp' : datetime .now ().isoformat (),
193+ 'evolution' : load_evolution_state (),
194+ 'model_files' : check_model_changes (),
195+ 'training' : check_training_progress ()
196+ }
197+
198+ # Load previous state
199+ previous_state = load_previous_state ()
200+
201+ # Determine changes
202+ changes = determine_change_type (current_state , previous_state )
203+
204+ # Output results for GitHub Actions
205+ print (f"::set-output name=has_changes::{ str (changes ['has_changes' ]).lower ()} " )
206+ print (f"::set-output name=major_evolution::{ str (changes ['major_evolution' ]).lower ()} " )
207+ print (f"::set-output name=architecture_changed::{ str (changes ['architecture_changed' ]).lower ()} " )
208+ print (f"::set-output name=performance_improved::{ str (changes ['performance_improved' ]).lower ()} " )
209+
210+ if changes ['has_changes' ]:
211+ # Generate evolution ID
212+ evolution_id = f"{ datetime .now ().strftime ('%Y%m%d_%H%M%S' )} "
213+ print (f"::set-output name=evolution_id::{ evolution_id } " )
214+
215+ # Save current state for next run
216+ save_current_state (current_state )
217+
218+ print ("✨ Evolution changes detected!" )
219+ for change in changes ['change_summary' ]:
220+ print (f" • { change } " )
221+ else :
222+ print ("🔄 No significant changes detected." )
223+
224+ # Save state for next comparison
225+ save_current_state (current_state )
226+
227+ return 0 if not changes ['has_changes' ] else 1
228+
229+ if __name__ == "__main__" :
230+ sys .exit (main ())
0 commit comments