11import difflib
2+ import json
23import logging
34import os
45import shutil
6+ import threading
57import time
8+ import urllib .parse
9+ from http .server import HTTPServer
610from pathlib import Path
7- from typing import Any , Callable , Optional
11+ from typing import Any
12+
13+ from .dashboard_html import OBSERVER_HTML
14+ from .pyob_dashboard import ObserverHandler
15+ from .targeted_reviewer import TargetedReviewer
816
917logger = logging .getLogger (__name__ )
1018
1119
20+ # [TargetedReviewer class moved to targeted_reviewer.py]
21+
22+
1223class EntranceMixin :
13- """
14- Mixin providing core iteration logic.
15- Attributes are declared here to satisfy strict Mypy checks.
16- """
17-
18- target_dir : str
19- pyob_dir : str
20- ENGINE_FILES : list [str ]
21- llm_engine : Any
22- code_parser : Any
23- cascade_queue : list [str ]
24- cascade_diffs : dict [str , str ]
25- session_pr_count : int
26- self_evolved_flag : bool
27- memory_path : str
28- history_path : str
29- analysis_path : str
30- symbols_path : str
31- manual_target_file : Optional [str ]
32- key_cooldowns : dict [str , float ]
33-
34- def pick_target_file (self ) -> str :
35- return ""
36-
37- def _read_file (self , path : str ) -> str :
38- return ""
39-
40- def _extract_path_from_llm_response (self , text : str ) -> str :
41- return ""
42-
43- def get_valid_llm_response (
44- self , p : str , v : Callable [[str ], bool ], context : str
45- ) -> str :
46- return ""
47-
48- def update_analysis_for_single_file (self , abs_p : str , rel_p : str ):
49- pass
50-
51- def update_ledger_for_file (self , rel_p : str , code : str ):
52- pass
53-
54- def detect_symbolic_ripples (self , o : str , n : str , p : str ) -> list [str ]:
55- return []
56-
57- def _run_final_verification_and_heal (self , b : dict ) -> bool :
58- return False
59-
60- def handle_git_librarian (self , p : str , i : int ):
61- pass
62-
63- def append_to_history (self , p : str , o : str , n : str ):
64- pass
65-
66- def wrap_up_evolution_session (self ):
67- pass
68-
69- def generate_pr_summary (self , rel_path : str , diff_text : str ) -> dict :
70- return {}
71-
72- def execute_targeted_iteration (self , iteration : int ):
73- """Orchestrates a single targeted evolution step."""
24+ def start_dashboard (self : Any ):
25+ # 1. Save to the internal .pyob folder
26+ obs_path = os .path .join (self .pyob_dir , "observer.html" )
27+
28+ # Dynamically modify OBSERVER_HTML string to inject manual target UI and logic
29+ modified_html_content = OBSERVER_HTML
30+
31+ # Insert HTML block for manual target selection
32+ html_to_insert = """
33+ <!-- NEW INTERACTIVE FEATURE START -->
34+ <div class="control-section" style="background-color: #333333; padding: 15px; border-radius: 6px; margin-top: 20px;">
35+ <h3>Manual Target Selection</h3>
36+ <form id="set-target-form" action="/set_target" method="POST">
37+ <input type="text" id="target-file-path" name="file_path" placeholder="e.g., src/my_module/my_file.py" style="width: 70%; padding: 8px; margin-right: 10px; border: 1px solid #555; background-color: #1c1c1c; color: #d4d4d4; border-radius: 4px;">
38+ <button type="submit" style="padding: 8px 15px; background-color: #6a9955; color: white; border: none; border-radius: 4px; cursor: pointer;">Set Next Target</button>
39+ </form>
40+ <p id="target-message" style="margin-top: 10px; color: #dcdcaa;"></p>
41+ </div>
42+ <!-- NEW INTERACTIVE FEATURE END -->
43+ """
44+ # Assuming OBSERVER_HTML has a structure like: ... </div>\n\n <h2>Live Log</h2>
45+ insertion_marker_html = " </div>\n \n <h2>Live Log</h2>"
46+ if insertion_marker_html in modified_html_content :
47+ modified_html_content = modified_html_content .replace (
48+ insertion_marker_html , html_to_insert + "\n " + insertion_marker_html
49+ )
50+ else :
51+ logger .warning (
52+ "HTML insertion marker not found in OBSERVER_HTML. Manual target UI may not be visible."
53+ )
54+
55+ # Insert JavaScript for manual target form submission handling
56+ js_to_insert = """
57+ // Handle manual target form submission
58+ document.getElementById('set-target-form').addEventListener('submit', function(event) {
59+ event.preventDefault(); // Prevent default form submission
60+
61+ const filePathInput = document.getElementById('target-file-path');
62+ const filePath = filePathInput.value;
63+ const targetMessage = document.getElementById('target-message');
64+
65+ if (!filePath) {
66+ targetMessage.textContent = 'Please enter a file path.';
67+ targetMessage.style.color = 'red';
68+ return;
69+ }
70+
71+ fetch('/set_target', {
72+ method: 'POST',
73+ headers: {
74+ 'Content-Type': 'application/x-www-form-urlencoded',
75+ },
76+ body: `file_path=${encodeURIComponent(filePath)}`
77+ })
78+ .then(response => {
79+ const isOk = response.ok; // Capture response.ok status
80+ return response.json().then(data => ({ data, isOk })); // Pass data and status
81+ })
82+ .then(({ data, isOk }) => { // Destructure to get both
83+ targetMessage.textContent = data.message;
84+ // Check response.ok from the original fetch response, not data
85+ targetMessage.style.color = isOk ? '#6a9955' : 'red';
86+ if (isOk) {
87+ filePathInput.value = ''; // Clear input on success
88+ }
89+ })
90+ .catch(error => {
91+ console.error('Error setting manual target:', error);
92+ targetMessage.textContent = 'An error occurred while setting target.';
93+ targetMessage.style.color = 'red';
94+ });
95+ });
96+ """
97+ # Assuming OBSERVER_HTML has a script tag ending with ' </script>'
98+ insertion_marker_js = " </script>"
99+ if insertion_marker_js in modified_html_content :
100+ modified_html_content = modified_html_content .replace (
101+ insertion_marker_js , js_to_insert + "\n " + insertion_marker_js
102+ )
103+ else :
104+ logger .warning (
105+ "JavaScript insertion marker not found in OBSERVER_HTML. Manual target logic may not be functional."
106+ )
107+
108+ with open (obs_path , "w" , encoding = "utf-8" ) as f :
109+ f .write (modified_html_content )
110+
111+ # 2. Initialize and Start the Live Server
112+
113+ # Dynamically add do_POST method for manual target handling
114+ def _dynamic_do_POST_method (handler_instance : ObserverHandler ):
115+ if handler_instance .path == "/set_target" :
116+ try :
117+ content_length = int (
118+ handler_instance .headers .get ("Content-Length" , 0 )
119+ )
120+ except (ValueError , TypeError ):
121+ content_length = 0 # Default to 0 if header is malformed
122+
123+ if content_length > 0 :
124+ post_data = handler_instance .rfile .read (content_length ).decode (
125+ "utf-8"
126+ )
127+ parsed_data = urllib .parse .parse_qs (post_data )
128+ file_path = parsed_data .get ("file_path" , ["" ])[0 ]
129+ else :
130+ file_path = "" # No content, no file_path
131+
132+ if file_path and handler_instance .controller :
133+ handler_instance .controller .set_manual_target_file (file_path )
134+ message = f"Manual target set to: { file_path } "
135+ status_code = 200
136+ else :
137+ message = (
138+ "Error: No file path provided or controller not available."
139+ )
140+ status_code = 400
141+
142+ handler_instance .send_response (status_code )
143+ handler_instance .send_header ("Content-type" , "application/json" )
144+ handler_instance .end_headers ()
145+ handler_instance .wfile .write (
146+ json .dumps ({"message" : message }).encode ("utf-8" )
147+ )
148+ else :
149+ handler_instance .send_error (404 )
150+
151+ # Fix: Use setattr to bypass Mypy [method-assign] error
152+ setattr (ObserverHandler , "do_POST" , _dynamic_do_POST_method )
153+
154+ ObserverHandler .controller = self
155+
156+ def run_server ():
157+ try :
158+ server = HTTPServer (("localhost" , 5000 ), ObserverHandler )
159+ server .serve_forever ()
160+ except Exception as e :
161+ logger .error (f"Dashboard failed to start: { e } " )
162+
163+ threading .Thread (target = run_server , daemon = True ).start ()
164+
165+ print ("\n " + "=" * 60 )
166+ print ("PyOuroBoros (PyOB) OBSERVER IS LIVE" )
167+ print ("URL: http://localhost:5000" )
168+ print (f"FILE: { obs_path } " )
169+ print ("=" * 60 + "\n " )
170+
171+ def execute_targeted_iteration (self : Any , iteration : int ):
74172 backup_state = self .llm_engine .backup_workspace ()
75173 target_diff = ""
76-
77174 if self .cascade_queue :
78175 target_rel_path = self .cascade_queue .pop (0 )
79176 target_diff = self .cascade_diffs .get (target_rel_path , "" )
@@ -95,40 +192,35 @@ def execute_targeted_iteration(self, iteration: int):
95192 project_name = os .path .basename (self .target_dir )
96193 base_backup_path = Path .home () / "Documents" / "PYOB_Backups" / project_name
97194 pod_path = base_backup_path / f"safety_pod_v{ iteration } _{ timestamp } "
195+
98196 try :
99197 pod_path .mkdir (parents = True , exist_ok = True )
198+ logger .warning (
199+ f"SELF-EVOLUTION: Sheltering engine source EXTERNALLY in { pod_path } "
200+ )
100201 for f_name in self .ENGINE_FILES :
101202 src = os .path .join (self .target_dir , "src" , "pyob" , f_name )
102203 if os .path .exists (src ):
103204 shutil .copy (src , str (pod_path ))
104205 except Exception as e :
105- logger .error (f"Failed to create safety pod: { e } " )
206+ logger .error (f"Failed to create external safety pod: { e } " )
106207
107208 target_abs_path = os .path .join (self .target_dir , target_rel_path )
108209 self .llm_engine .session_context = []
109210 if is_cascade and target_diff :
110- msg = f"CRITICAL SYMBOLIC RIPPLE: This file depends on code that was just modified.\n ### CHANGE DIFF:\n { target_diff } "
211+ msg = f"CRITICAL SYMBOLIC RIPPLE: This file depends on code that was just modified. Ensure this file is updated to support these changes: \n \n ### DEPDENDENCY CHANGE DIFF:\n { target_diff } "
111212 self .llm_engine .session_context .append (msg )
112213
113214 old_content = ""
114215 if os .path .exists (target_abs_path ):
115216 with open (target_abs_path , "r" , encoding = "utf-8" , errors = "ignore" ) as f :
116217 old_content = f .read ()
117218
118- from pyob .targeted_reviewer import TargetedReviewer
119-
120219 reviewer = TargetedReviewer (self .target_dir , target_abs_path )
121220 reviewer .session_context = self .llm_engine .session_context [:]
122- if hasattr (self , "key_cooldowns" ):
123- reviewer .key_cooldowns = self .key_cooldowns
124- if hasattr (self , "session_pr_count" ):
125- reviewer .session_pr_count = self .session_pr_count
126-
127221 reviewer .run_pipeline (iteration )
128222
129223 self .llm_engine .session_context = reviewer .session_context [:]
130- if hasattr (reviewer , "session_pr_count" ):
131- self .session_pr_count = reviewer .session_pr_count
132224
133225 new_content = ""
134226 if os .path .exists (target_abs_path ):
@@ -140,35 +232,43 @@ def execute_targeted_iteration(self, iteration: int):
140232 self .update_ledger_for_file (target_rel_path , new_content )
141233
142234 if old_content != new_content :
143- logger .info (f"Edit successful. Verifying { target_rel_path } ..." )
235+ logger .info (
236+ f"Edit successful. Checking ripples and running final verification for { target_rel_path } ..."
237+ )
144238 self .append_to_history (target_rel_path , old_content , new_content )
239+
145240 current_diff = "" .join (
146241 difflib .unified_diff (
147242 old_content .splitlines (keepends = True ),
148243 new_content .splitlines (keepends = True ),
149244 )
150245 )
246+
151247 ripples = self .detect_symbolic_ripples (
152248 old_content , new_content , target_rel_path
153249 )
154250 if ripples :
251+ logger .warning (
252+ f"CROSS-FILE DEPTH TRIGGERED! Queuing { len (ripples )} files."
253+ )
155254 for r in ripples :
156255 if r not in self .cascade_queue :
157256 self .cascade_queue .append (r )
158257 self .cascade_diffs [r ] = current_diff
159258
259+ logger .info ("\n " + "=" * 20 + " FINAL VERIFICATION " + "=" * 20 )
160260 if not self ._run_final_verification_and_heal (backup_state ):
161- logger .error ("Final verification failed. Changes rolled back." )
261+ logger .error (
262+ "Final verification failed and could not be auto-repaired. Iteration changes have been rolled back."
263+ )
162264 else :
265+ logger .info ("Final verification successful. Application is stable." )
163266 self .handle_git_librarian (target_rel_path , iteration )
267+
164268 if is_engine_file :
269+ logger .warning (
270+ f"SELF-EVOLUTION: `{ target_rel_path } ` was successfully updated."
271+ )
165272 self .self_evolved_flag = True
166273
167- # --- THE FINAL WRAP-UP GATE ---
168- if getattr (self , "session_pr_count" , 0 ) >= 8 and not getattr (
169- self , "cascade_queue" , []
170- ):
171- logger .info (
172- f"🏆 MISSION ACCOMPLISHED: { self .session_pr_count } PRs achieved."
173- )
174- self .wrap_up_evolution_session ()
274+ logger .info ("=" * 60 + "\n " )
0 commit comments