-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfoo_gui.py
More file actions
1221 lines (991 loc) · 48.7 KB
/
foo_gui.py
File metadata and controls
1221 lines (991 loc) · 48.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
FOOGUI.py
GUI interface for the multi-agent chat system.
Compatible with HelperGUI.py and ClaudeGUI.py architecture.
By Juan B. Gutiérrez, Professor of Mathematics
University of Texas at San Antonio.
License: Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
"""
import os
import sys
import json
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QWidget, QTextEdit, QLineEdit, QVBoxLayout,
QPushButton, QTabWidget, QHBoxLayout, QCheckBox, QLabel, QScrollArea,
QFileDialog, QMessageBox
)
from PyQt5.QtCore import QThread, pyqtSignal, QEvent, Qt, QUrl, QTimer
from PyQt5.QtGui import QDragEnterEvent, QDropEvent
from cls_foo import MultiAgentOrchestrator
class BroadcastTextEdit(QTextEdit):
"""Custom QTextEdit that reliably handles Enter key for broadcasting"""
def __init__(self, parent=None):
super().__init__(parent)
self.parent_widget = parent
def keyPressEvent(self, event):
"""Override keyPressEvent for direct Enter key handling"""
# Handle all possible Enter key variations
if event.key() in (Qt.Key_Return, Qt.Key_Enter) or event.text() == '\r':
# Check if Shift is held (for multiline input)
if event.modifiers() & Qt.ShiftModifier:
# Shift+Enter: insert newline normally
super().keyPressEvent(event)
else:
# Plain Enter: broadcast message
text = self.toPlainText().strip()
if text:
if self.parent_widget and hasattr(self.parent_widget, 'broadcast_message_text'):
self.parent_widget.broadcast_message_text(text)
self.clear()
# Don't call super() to prevent newline insertion
return
# For all other keys, use default behavior
super().keyPressEvent(event)
class AgentTextEdit(QTextEdit):
"""Custom QTextEdit for individual agent inputs"""
def __init__(self, parent=None):
super().__init__(parent)
self.agent_tab = parent # Store reference to the AgentTab
def keyPressEvent(self, event):
"""Handle Enter key for individual agent inputs"""
# Handle all possible Enter key variations
if event.key() in (Qt.Key_Return, Qt.Key_Enter) or event.text() == '\r':
# Check if Shift is held (for multiline input)
if event.modifiers() & Qt.ShiftModifier:
# Shift+Enter: insert newline normally
super().keyPressEvent(event)
else:
# Plain Enter: send message
text = self.toPlainText().strip()
if text and self.agent_tab:
self.setEnabled(False)
self.agent_tab.handle_input(text)
self.clear()
return
super().keyPressEvent(event)
class VulnerabilityWorker(QThread):
"""Worker thread for vulnerability analysis"""
result_ready = pyqtSignal(dict, dict) # Returns (requests_dict, responses_dict)
def __init__(self, orchestrator, source_agent_name):
super().__init__()
self.orchestrator = orchestrator
self.source_agent_name = source_agent_name
def run(self):
try:
# Get the source agent's latest response for the request message
source_agent = None
for agent in self.orchestrator.agents:
if agent.name == self.source_agent_name:
source_agent = agent
break
if not source_agent or not source_agent.latest_response:
self.result_ready.emit({}, {"Error": "No response found for source agent"})
return
# Build the request message that will be sent
request_message = f"Agent {self.source_agent_name} answered the same question as follows, find flaws: {source_agent.latest_response}"
# Send to other agents and get responses
responses = self.orchestrator.send_vulnerability_analysis(self.source_agent_name)
# Create requests dict for UI display
requests = {}
for agent_name in responses.keys():
requests[agent_name] = request_message
self.result_ready.emit(requests, responses)
except Exception as e:
self.result_ready.emit({}, {"Error": str(e)})
class JudgmentWorker(QThread):
"""Worker thread for judgment analysis"""
result_ready = pyqtSignal(dict, dict) # Returns (requests_dict, responses_dict)
def __init__(self, orchestrator, source_agent_name):
super().__init__()
self.orchestrator = orchestrator
self.source_agent_name = source_agent_name
def run(self):
try:
# Call orchestrator to send judgment analysis
# Now returns both responses and the actual messages sent
responses, messages = self.orchestrator.send_judgment_analysis(self.source_agent_name)
if not responses:
self.result_ready.emit({}, {"Error": "No responses from harmonizer agents"})
return
# Use the actual messages that were sent (from orchestrator)
# This ensures UI shows exactly what each agent received
self.result_ready.emit(messages, responses)
except Exception as e:
self.result_ready.emit({}, {"Error": str(e)})
class ReflectionWorker(QThread):
"""Worker thread for reflection analysis"""
result_ready = pyqtSignal(str, str) # Returns (request_message, response)
def __init__(self, orchestrator, target_agent_name):
super().__init__()
self.orchestrator = orchestrator
self.target_agent_name = target_agent_name
def run(self):
try:
# Collect reflections from harmonizer agents to build the request message
reflections = []
for agent in self.orchestrator.get_harmonizer_agents():
if agent.latest_response and agent.latest_response.strip():
reflections.append(agent.latest_response.strip())
if not reflections:
self.result_ready.emit("", "No reflections found from harmonizer agents")
return
composite = "---".join(reflections)
request_message = (
"Judgment of your response has resulted in the observations that follow. "
"Regenerate your version of the text under review taking into account the consensus of these observations. If you object to an observation, explain why. \n \n " + composite
)
response = self.orchestrator.send_reflection_analysis(self.target_agent_name)
self.result_ready.emit(request_message, response if response else "No reflection response received")
except Exception as e:
self.result_ready.emit("", f"Error: {e}")
class AgentWorker(QThread):
"""Generic worker thread for agent interactions"""
result_ready = pyqtSignal(str)
def __init__(self, agent, message):
super().__init__()
self.agent = agent
self.message = message
def run(self):
try:
response = self.agent.send_message(self.message)
self.result_ready.emit(response)
except Exception as e:
self.result_ready.emit(f"Error: {e}")
class AgentTab(QWidget):
"""Individual agent tab widget compatible with HelperGUI.py style"""
def __init__(self, agent, orchestrator, config):
super().__init__()
self.agent = agent
self.orchestrator = orchestrator
self.config = config
self.user = orchestrator.user
self.name = agent.name
# Initialize worker references
self.worker = None
self.vulnerability_worker = None
self.judgment_worker = None
self.reflection_worker = None
# Initialize UI first
self.init_ui()
# Check if history was already loaded during agent initialization
has_history = hasattr(agent, 'history_data') and agent.history_data.get('history')
if has_history and len(agent.history_data['history']) > 1:
# Display the loaded history - no introduction needed
self.display_loaded_history()
# Use QTimer to ensure gear icon removal happens after tab is fully added
QTimer.singleShot(50, self.clear_tab_pending)
else:
# No history exists - this is a new chat, so introduce
self.handle_input("Introduce yourself.")
def closeEvent(self, event):
"""Handle widget closure by stopping all worker threads"""
self.stop_all_workers()
super().closeEvent(event)
def stop_all_workers(self):
"""Stop all worker threads before destruction"""
try:
workers = [
('worker', self.worker),
('vulnerability_worker', self.vulnerability_worker),
('judgment_worker', self.judgment_worker),
('reflection_worker', self.reflection_worker)
]
for name, worker in workers:
if worker and worker.isRunning():
worker.terminate()
if not worker.wait(1000): # Wait up to 1 second
print(f"Warning: {name} for {self.name} did not terminate gracefully")
else:
print(f"Stopped {name} for {self.name}")
except Exception as e:
print(f"Error stopping workers for {self.name}: {e}")
def init_ui(self):
layout = QVBoxLayout()
# Agent controls row
row = QHBoxLayout()
self.checkbox = QCheckBox(f"Enable {self.name}")
self.checkbox.setChecked(True)
self.checkbox.stateChanged.connect(self.toggle_active)
row.addWidget(self.checkbox)
self.harmonizer_checkbox = QCheckBox("Harmonizer")
self.harmonizer_checkbox.setChecked(getattr(self.agent, 'harmonizer', False))
self.harmonizer_checkbox.stateChanged.connect(self.toggle_harmonizer)
row.addWidget(self.harmonizer_checkbox)
layout.addLayout(row)
# Text display area
self.text_area = QTextEdit()
self.text_area.setReadOnly(True)
self.text_area.setAcceptDrops(True) # Enable drag and drop like ClaudeGUI.py
self.text_area.dragEnterEvent = self.dragEnterEvent
self.text_area.dropEvent = self.dropEvent
layout.addWidget(self.text_area)
# User input area
self.user_input = AgentTextEdit(self)
self.user_input.setFixedHeight(60)
self.user_input.setPlaceholderText("Type your message and press Enter")
layout.addWidget(self.user_input)
# Button row
button_row = QHBoxLayout()
self.copy_button = QPushButton("Copy Latest Answer")
self.copy_button.clicked.connect(self.copy_latest_answer)
button_row.addWidget(self.copy_button)
self.vulnerability_button = QPushButton("Vulnerability")
self.vulnerability_button.clicked.connect(self.send_vulnerability_message)
button_row.addWidget(self.vulnerability_button)
self.judgment_button = QPushButton("Judgment")
self.judgment_button.clicked.connect(self.send_judgment_message)
button_row.addWidget(self.judgment_button)
self.reflection_button = QPushButton("Reflection")
self.reflection_button.clicked.connect(self.send_reflection_message)
button_row.addWidget(self.reflection_button)
layout.addLayout(button_row)
self.setLayout(layout)
# Apply font sizes
fontsize = int(self.config.get("fontsize", 10))
for widget in [self.text_area, self.user_input, self.copy_button,
self.vulnerability_button, self.judgment_button,
self.reflection_button, self.checkbox, self.harmonizer_checkbox]:
font = widget.font()
font.setPointSize(fontsize)
widget.setFont(font)
def dragEnterEvent(self, event: QDragEnterEvent):
"""Handle drag enter events (compatible with ClaudeGUI.py)"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event: QDropEvent):
"""Handle file drop events (compatible with ClaudeGUI.py)"""
urls = event.mimeData().urls()
if urls:
file_path = urls[0].toLocalFile()
self.upload_file(file_path)
def upload_file(self, file_path):
"""Upload file to agent"""
try:
if hasattr(self.agent, 'upload_file'):
# OpenAI agent - use upload_file method
file_id = self.agent.upload_file(file_path)
if file_id:
self.text_area.append(f"File uploaded successfully: ID {file_id}")
self.text_area.append(">>>>>>>>>>>>>>>>>>>>>>>>>>")
elif hasattr(self.agent, 'process_file_upload'):
# Claude agent - use process_file_upload method
self.text_area.append(f"Processing file: {file_path}")
self.text_area.append(">>>>>>>>>>>>>>>>>>>>>>>>>>")
response = self.agent.process_file_upload(file_path)
self.show_response(response)
else:
self.text_area.append(f"File upload not supported for {self.name}")
except Exception as e:
self.text_area.append(f"Error uploading file: {e}")
def toggle_active(self, state):
"""Toggle agent active state"""
self.agent.active = bool(state)
def toggle_harmonizer(self, state):
"""Toggle agent harmonizer state"""
self.agent.harmonizer = bool(state)
def mark_tab_pending(self):
"""Mark tab as pending with gear icon"""
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent:
index = parent.tabs.indexOf(self)
if index != -1:
current_name = parent.tabs.tabText(index)
if not current_name.startswith("⚙"):
parent.tabs.setTabText(index, f"⚙ {self.name}")
def clear_tab_pending(self):
"""Clear pending gear icon from tab"""
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent:
index = parent.tabs.indexOf(self)
if index != -1:
parent.tabs.setTabText(index, self.name)
def display_loaded_history(self):
"""Display loaded conversation history in the text area"""
try:
history_data = self.agent.history_data.get('history', [])
chat_id = self.agent.history_data.get('chat_id')
if not history_data:
return
self.text_area.append("=== RESTORED CONVERSATION ===")
if chat_id:
self.text_area.append(f"Chat ID: {chat_id}")
# For Claude agents, use display_history if available, otherwise use history
display_data = getattr(self.agent, 'display_history', history_data)
if not display_data:
display_data = history_data
for entry in display_data:
if not isinstance(entry, dict):
continue
role = entry.get('role', 'unknown')
content = entry.get('content', '')
timestamp = entry.get('timestamp', '')
# Skip system messages for display
if role == 'user' and 'Introduce yourself as' in content:
continue
if role == 'user':
time_str = f" ({timestamp})" if timestamp else ""
self.text_area.append(f"{self.user}{time_str}: {content}")
self.text_area.append(">>>>>>>>>>>>>>>>>>>>>>>>>>")
elif role == 'assistant':
time_str = f" ({timestamp})" if timestamp else ""
self.text_area.append(f"{self.name}{time_str}: {content}")
self.text_area.append("<<<<<<<<<<<<<<<<<<<<<<<<<<")
# Update latest response for copy functionality
self.agent.latest_response = content
message_count = len([e for e in display_data if e.get('role') in ['user', 'assistant']])
self.text_area.append(f"=== CONVERSATION RESTORED ({message_count} messages) ===")
integrity_text = self.agent.get_integrity_display_text()
if integrity_text:
self.text_area.append("=" * 50)
self.text_area.append(integrity_text)
self.text_area.append("=" * 50)
except Exception as e:
self.text_area.append(f"Error displaying history: {e}")
print(f"Error displaying history for {self.name}: {e}")
def handle_input(self, text):
"""Handle user input to agent"""
if not self.agent.active:
return
self.mark_tab_pending() # Show gear icon when working
self.text_area.append(f"{self.user}: {text}")
self.text_area.append(">>>>>>>>>>>>>>>>>>>>>>>>>>")
# Use orchestrator's blockchain-enabled messaging instead of direct agent call
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent and hasattr(parent, 'orchestrator'):
# Create and start worker thread that uses orchestrator
self.worker = BlockchainAgentWorker(parent.orchestrator, self.agent, text)
self.worker.result_ready.connect(self.show_response)
self.worker.start()
else:
# Fallback to direct agent call
self.worker = AgentWorker(self.agent, text)
self.worker.result_ready.connect(self.show_response)
self.worker.start()
def show_response(self, response):
"""Display agent response"""
# Show integrity warning if present
integrity_text = self.agent.get_integrity_display_text()
if integrity_text:
self.text_area.append("=" * 30)
self.text_area.append(integrity_text)
self.text_area.append("=" * 30)
self.text_area.append(f"{self.name}: {response}")
self.text_area.append("<<<<<<<<<<<<<<<<<<<<<<<<<<")
self.clear_tab_pending() # Remove gear icon when finished
self.user_input.setEnabled(True)
# Notify parent that agent finished
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent and hasattr(parent, 'agent_finished'):
parent.agent_finished()
def send_vulnerability_message(self):
"""Send vulnerability analysis request (asynchronous)"""
# Disable button to prevent multiple clicks
self.vulnerability_button.setEnabled(False)
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent:
# Mark other agent tabs as working
for tab in parent.agent_tabs:
if tab.name != self.name and tab.agent.active:
tab.mark_tab_pending()
tab.user_input.setEnabled(False)
# Start async worker
self.vulnerability_worker = VulnerabilityWorker(self.orchestrator, self.name)
self.vulnerability_worker.result_ready.connect(self.handle_vulnerability_results)
self.vulnerability_worker.start()
def handle_vulnerability_results(self, requests, responses):
"""Handle vulnerability analysis results"""
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent:
for agent_name, response in responses.items():
# Find the agent tab and display both request and response
for tab in parent.agent_tabs:
if tab.name == agent_name:
# Show the actual request message that was sent
if agent_name in requests:
tab.text_area.append(f"{self.user}: {requests[agent_name]}")
tab.text_area.append(">>>>>>>>>>>>>>>>>>>>>>>>>>")
# Show the response
tab.text_area.append(f"{tab.name}: {response}")
tab.text_area.append("<<<<<<<<<<<<<<<<<<<<<<<<<<")
tab.clear_tab_pending() # Remove gear icon
tab.user_input.setEnabled(True) # Re-enable input
break
# Re-enable button
self.vulnerability_button.setEnabled(True)
def send_judgment_message(self):
"""Send judgment analysis request (asynchronous)"""
# Disable button to prevent multiple clicks
self.judgment_button.setEnabled(False)
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent:
# Mark harmonizer agent tabs as working
for tab in parent.agent_tabs:
if getattr(tab.agent, 'harmonizer', False) and tab.agent.active:
tab.mark_tab_pending()
tab.user_input.setEnabled(False)
# Start async worker
self.judgment_worker = JudgmentWorker(self.orchestrator, self.name)
self.judgment_worker.result_ready.connect(self.handle_judgment_results)
self.judgment_worker.start()
def handle_judgment_results(self, requests, responses):
"""Handle judgment analysis results"""
parent = self.parent()
while parent and not isinstance(parent, MultiAgentChatGUI):
parent = parent.parent()
if parent:
for agent_name, response in responses.items():
# Find the agent tab and display both request and response
for tab in parent.agent_tabs:
if tab.name == agent_name:
# Show the actual request message that was sent
if agent_name in requests:
tab.text_area.append(f"{self.user}: {requests[agent_name]}")
tab.text_area.append(">>>>>>>>>>>>>>>>>>>>>>>>>>")
# Show the response
tab.text_area.append(f"{tab.name}: {response}")
tab.text_area.append("<<<<<<<<<<<<<<<<<<<<<<<<<<")
tab.clear_tab_pending() # Remove gear icon
tab.user_input.setEnabled(True) # Re-enable input
break
# Re-enable button
self.judgment_button.setEnabled(True)
def send_reflection_message(self):
"""Send reflection analysis request (asynchronous)"""
# Disable button and mark this tab as working
self.reflection_button.setEnabled(False)
self.mark_tab_pending()
self.user_input.setEnabled(False)
# Start async worker
self.reflection_worker = ReflectionWorker(self.orchestrator, self.name)
self.reflection_worker.result_ready.connect(self.handle_reflection_results)
self.reflection_worker.start()
def handle_reflection_results(self, request_message, response):
"""Handle reflection analysis results"""
# Show the actual request message that was sent
if request_message:
self.text_area.append(f"{self.user}: {request_message}")
self.text_area.append(">>>>>>>>>>>>>>>>>>>>>>>>>>")
# Show the response
self.text_area.append(f"{self.name}: {response}")
self.text_area.append("<<<<<<<<<<<<<<<<<<<<<<<<<<")
# Remove gear icon and re-enable controls
self.clear_tab_pending()
self.user_input.setEnabled(True)
self.reflection_button.setEnabled(True)
def copy_latest_answer(self):
"""Copy latest agent response to clipboard (compatible with HelperGUI.py)"""
QApplication.clipboard().setText(self.agent.latest_response)
self.text_area.append("Latest answer copied to clipboard.")
class MultiAgentChatGUI(QWidget):
"""Main GUI class for multi-agent chat system"""
def __init__(self):
super().__init__()
# Load configuration and initialize orchestrator
self.master_config_path = "config.json"
self.load_configuration()
self.active_agents_working = 0
# Initialize UI
self.init_ui()
# Create agent tabs
self.create_agent_tabs()
def load_configuration(self):
"""Load configuration from appropriate location"""
master_config = None
# Try to load master config
if not os.path.exists(self.master_config_path):
print(f"Master config file not found: {self.master_config_path}")
# Prompt user to select config file
selected_config = self._prompt_for_config_file()
if selected_config:
self.master_config_path = selected_config
else:
# User cancelled - cannot proceed
QMessageBox.critical(
None,
"Configuration Required",
"No configuration file selected. The application cannot start without a valid config file."
)
sys.exit(1)
try:
# Load master config
with open(self.master_config_path, "r") as f:
master_config = json.load(f)
cwd = master_config["CONFIG"].get("CWD", "/chats")
# If CWD is not /chats, check for config.json in that directory
if cwd != "/chats":
# Convert relative path to absolute if needed
if cwd.startswith("/"):
cwd_path = cwd[1:] # Remove leading slash for relative path
else:
cwd_path = cwd
config_in_cwd = os.path.join(cwd_path, "config.json")
if os.path.exists(config_in_cwd):
print(f"Loading config from CWD: {config_in_cwd}")
with open(config_in_cwd, "r") as f:
self.current_config_data = json.load(f)
self.current_config_path = config_in_cwd
else:
print(f"No config.json found in CWD: {cwd_path}, using master config")
self.current_config_data = master_config
self.current_config_path = self.master_config_path
else:
# CWD is /chats, use master config
self.current_config_data = master_config
self.current_config_path = self.master_config_path
# Initialize orchestrator with current config
self.orchestrator = MultiAgentOrchestrator(self.current_config_path)
except FileNotFoundError as e:
print(f"Configuration file not found: {e}")
# Prompt user to select config file
selected_config = self._prompt_for_config_file()
if selected_config:
self.master_config_path = selected_config
# Retry loading with the selected config
self.load_configuration()
else:
QMessageBox.critical(
None,
"Configuration Required",
"No configuration file selected. The application cannot start without a valid config file."
)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error parsing configuration file: {e}")
QMessageBox.critical(
None,
"Invalid Configuration",
f"The configuration file is not valid JSON:\n{e}\n\nPlease select a valid config file."
)
# Prompt for a different config file
selected_config = self._prompt_for_config_file()
if selected_config:
self.master_config_path = selected_config
# Retry loading with the selected config
self.load_configuration()
else:
sys.exit(1)
except Exception as e:
print(f"Error loading configuration: {e}")
# Only use fallback if we have a valid master_config
if master_config is not None:
print("Using master config as fallback")
self.current_config_data = master_config
self.current_config_path = self.master_config_path
try:
self.orchestrator = MultiAgentOrchestrator(self.master_config_path)
except Exception as orch_error:
QMessageBox.critical(
None,
"Initialization Failed",
f"Failed to initialize orchestrator:\n{orch_error}"
)
sys.exit(1)
else:
QMessageBox.critical(
None,
"Configuration Error",
f"Failed to load configuration:\n{e}"
)
sys.exit(1)
def _prompt_for_config_file(self):
"""
Prompt user to select a configuration file.
Returns the selected file path or None if cancelled.
"""
QMessageBox.warning(
None,
"Config File Not Found",
f"Configuration file not found: {self.master_config_path}\n\n"
"Please select a valid configuration file (config.json or config_*.json)"
)
config_file_path, _ = QFileDialog.getOpenFileName(
None,
"Select Configuration File",
"",
"Config Files (config*.json);;All JSON Files (*.json);;All Files (*.*)"
)
if config_file_path:
print(f"User selected config file: {config_file_path}")
return config_file_path
else:
print("User cancelled config file selection")
return None
def update_cwd_in_config(self, new_cwd):
"""Update the CWD in master config file"""
try:
# Always update the master config file
with open(self.master_config_path, "r") as f:
master_config = json.load(f)
master_config["CONFIG"]["CWD"] = new_cwd
with open(self.master_config_path, "w") as f:
json.dump(master_config, f, indent=4)
print(f"Updated CWD in master config to: {new_cwd}")
except Exception as e:
print(f"Error updating CWD in config: {e}")
def restart_interface(self):
"""Restart the entire interface with new configuration"""
try:
print("Restarting interface with new configuration...")
# Clear current interface
self.clear_interface()
# Reload configuration
self.load_configuration()
# Update window title with new CWD
cwd = self.current_config_data["CONFIG"].get("CWD", "/chats")
self.setWindowTitle(f"The Flaws of Others - Multi-agent Consensus - CWD: {cwd}")
# Recreate interface
self.create_agent_tabs()
print("Interface restarted successfully")
except Exception as e:
print(f"Error restarting interface: {e}")
def clear_interface(self):
"""Clear the current interface completely"""
try:
# Stop all running worker threads first
for tab in self.agent_tabs:
self.stop_agent_workers(tab)
# Remove all agent tabs
while self.tabs.count() > 0:
widget = self.tabs.widget(0)
self.tabs.removeTab(0)
if widget:
widget.deleteLater()
# Clear agent tabs list
self.agent_tabs = []
# Reset working agents counter
self.active_agents_working = 0
print("Interface cleared successfully")
except Exception as e:
print(f"Error clearing interface: {e}")
def stop_agent_workers(self, tab):
"""Stop all worker threads for an agent tab"""
try:
# Stop main agent worker if running
if hasattr(tab, 'worker') and tab.worker.isRunning():
tab.worker.terminate()
tab.worker.wait(1000) # Wait up to 1 second for termination
print(f"Stopped main worker for {tab.name}")
# Stop vulnerability worker if running
if hasattr(tab, 'vulnerability_worker') and tab.vulnerability_worker.isRunning():
tab.vulnerability_worker.terminate()
tab.vulnerability_worker.wait(1000)
print(f"Stopped vulnerability worker for {tab.name}")
# Stop judgment worker if running
if hasattr(tab, 'judgment_worker') and tab.judgment_worker.isRunning():
tab.judgment_worker.terminate()
tab.judgment_worker.wait(1000)
print(f"Stopped judgment worker for {tab.name}")
# Stop reflection worker if running
if hasattr(tab, 'reflection_worker') and tab.reflection_worker.isRunning():
tab.reflection_worker.terminate()
tab.reflection_worker.wait(1000)
print(f"Stopped reflection worker for {tab.name}")
except Exception as e:
print(f"Error stopping workers for {tab.name}: {e}")
def init_ui(self):
"""Initialize the main UI"""
# Set window title with CWD
cwd = self.current_config_data["CONFIG"].get("CWD", "/chats")
self.setWindowTitle(f"The Flaws of Others - Multi-agent Consensus - CWD: {cwd}")
self.setGeometry(100, 100, 800, 600)
self.setAcceptDrops(True) # Enable drag and drop
# Get font size from config
fontsize = int(self.orchestrator.config.get("fontsize", 10))
# Create tab widget
self.tabs = QTabWidget()
self.tabs.setStyleSheet(f"QTabBar::tab {{ font-size: {fontsize}pt; min-width: {fontsize * 15}px; padding: 10px; }}")
self.tabs.currentChanged.connect(self.focus_current_input)
# Create broadcast input
self.user_input = BroadcastTextEdit(self)
self.user_input.setPlaceholderText("Broadcast message to all active agents (Enter to send, Shift+Enter for newline)")
self.user_input.setFixedHeight(60)
self.user_input.setFocusPolicy(Qt.StrongFocus)
font = self.user_input.font()
font.setPointSize(fontsize)
self.user_input.setFont(font)
# Create buttons
self.reset_button = QPushButton("Reset")
self.reset_button.clicked.connect(self.reset_all_agents)
font = self.reset_button.font()
font.setPointSize(fontsize)
self.reset_button.setFont(font)
self.load_button = QPushButton("Load")
self.load_button.clicked.connect(self.load_agent_files)
font = self.load_button.font()
font.setPointSize(fontsize)
self.load_button.setFont(font)
# Layout
layout = QVBoxLayout()
layout.addWidget(self.tabs)
label = QLabel("Message to All Active Agents:")
font = label.font()
font.setPointSize(fontsize)
label.setFont(font)
layout.addWidget(label)
# Bottom row with buttons and broadcast field
bottom_layout = QHBoxLayout()
bottom_layout.addWidget(self.reset_button)
bottom_layout.addWidget(self.load_button)
bottom_layout.addWidget(self.user_input, 1)
layout.addLayout(bottom_layout)
self.setLayout(layout)
def create_agent_tabs(self):
"""Create tabs for each agent"""
self.agent_tabs = []
for agent in self.orchestrator.agents:
tab = AgentTab(agent, self.orchestrator, self.orchestrator.config)
# Start with gear icon - will be removed by AgentTab if history loads
self.tabs.addTab(tab, f"⚙ {agent.name}")
self.agent_tabs.append(tab)
# Force update of tab status after a short delay to ensure proper initialization
from PyQt5.QtCore import QTimer
QTimer.singleShot(100, lambda t=tab: self.check_and_update_tab_status(t))
def check_and_update_tab_status(self, tab):
"""Check if tab should have gear icon removed after initialization"""
# If agent has history and is not currently working, remove gear icon
has_history = hasattr(tab.agent, 'history_data') and tab.agent.history_data.get('history')
if has_history and len(tab.agent.history_data['history']) > 1:
# History was loaded, ensure gear icon is removed
tab.clear_tab_pending()
def dragEnterEvent(self, event: QDragEnterEvent):
"""Handle drag enter events"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event: QDropEvent):
"""Handle file drop events - upload to current tab's agent"""
urls = event.mimeData().urls()
if urls:
file_path = urls[0].toLocalFile()
current_index = self.tabs.currentIndex()
if 0 <= current_index < len(self.agent_tabs):
self.agent_tabs[current_index].upload_file(file_path)
def broadcast_message_text(self, text):
"""Broadcast message to all active agents"""
print(f"Broadcasting message: '{text}' to active agents")
# Disable broadcast input while agents work
self.user_input.setEnabled(False)
self.active_agents_working = 0
# Send to all active agents
for tab in self.agent_tabs:
if tab.agent.active:
self.active_agents_working += 1
tab.handle_input(text)
print(f"Message sent to {self.active_agents_working} active agents")
# Re-enable immediately if no agents are active
if self.active_agents_working == 0:
self.user_input.setEnabled(True)
def agent_finished(self):
"""Called when an agent finishes processing"""
self.active_agents_working -= 1
print(f"Agent finished. {self.active_agents_working} agents still working.")
# Re-enable broadcast field when all agents are done
if self.active_agents_working <= 0:
self.active_agents_working = 0
self.user_input.setEnabled(True)
print("All agents finished. Broadcast field re-enabled.")
def focus_current_input(self, index):
"""Handle tab focus changes"""
if hasattr(self, 'user_input') and not self.user_input.hasFocus():
if 0 <= index < len(self.agent_tabs):
self.agent_tabs[index].user_input.setFocus()
def showEvent(self, event):
"""Ensure broadcast field gets focus when window is shown"""
super().showEvent(event)
self.user_input.setFocus()
def reset_all_agents(self):
"""Reset all agents with warning and file deletion"""
cwd = self.current_config_data["CONFIG"].get("CWD", "/chats")
# Show warning dialog
warning_msg = (
f"WARNING: Resetting the chat will delete the files for the agents "
f"named in the active configuration file and in the current working directory ({cwd}). "
f"New instances of agents will be created. Is this what you want?"