azettl commited on
Commit
e0bb2f8
Β·
verified Β·
1 Parent(s): dceb707

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +425 -246
app.py CHANGED
@@ -295,13 +295,14 @@ class OpenFloorAgentServer:
295
 
296
 
297
  class OpenFloorManager:
298
- """Central floor manager for coordinating all OpenFloor agents"""
299
 
300
  def __init__(self, port: int = 7860):
301
  self.port = port
302
  self.agent_registry = {} # speakerUri -> agent info
303
  self.active_conversations = {} # conversation_id -> conversation state
304
  self.visual_callback = None
 
305
 
306
  def register_agent(self, manifest: Manifest, agent_url: str):
307
  """Register an agent with the floor manager"""
@@ -322,32 +323,52 @@ class OpenFloorManager:
322
  """Create a new conversation with optional initial participants"""
323
  conversation_id = f"conv:{uuid.uuid4()}"
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  self.active_conversations[conversation_id] = {
326
- 'id': conversation_id,
327
  'participants': initial_participants or [],
328
  'messages': [],
329
  'created_at': datetime.now(),
330
  'status': 'active'
331
  }
332
 
 
 
333
  print(f"πŸ›οΈ Floor Manager: Created conversation {conversation_id}")
334
  return conversation_id
335
 
336
  def invite_agent_to_conversation(self, conversation_id: str, target_speaker_uri: str,
337
  inviting_speaker_uri: str) -> bool:
338
- """Send InviteEvent to an agent"""
339
  if conversation_id not in self.active_conversations:
 
340
  return False
341
 
342
  if target_speaker_uri not in self.agent_registry:
 
343
  return False
344
 
345
- conversation = self.active_conversations[conversation_id]
 
346
  target_agent = self.agent_registry[target_speaker_uri]
347
 
348
- # Create proper InviteEvent
349
  invite_envelope = Envelope(
350
- conversation=Conversation(id=conversation_id),
351
  sender=Sender(speakerUri=inviting_speaker_uri),
352
  events=[
353
  InviteEvent(
@@ -366,107 +387,190 @@ class OpenFloorManager:
366
 
367
  if response:
368
  # Add to conversation participants
369
- if target_speaker_uri not in conversation['participants']:
370
- conversation['participants'].append(target_speaker_uri)
371
- self._update_visual_state(conversation_id)
 
 
 
 
 
 
 
 
372
  return True
373
 
 
374
  return False
375
 
376
  def route_message(self, envelope: Envelope) -> bool:
377
- """Route message to appropriate recipients"""
378
  conversation_id = envelope.conversation.id
379
 
380
  if conversation_id not in self.active_conversations:
 
381
  return False
382
 
383
- conversation = self.active_conversations[conversation_id]
 
 
 
 
 
 
 
 
 
 
 
384
 
385
  # Process each event in the envelope
 
 
386
  for event in envelope.events:
387
  if hasattr(event, 'to') and event.to:
388
  # Directed message - send to specific agent
389
  target_uri = event.to.speakerUri
390
- if target_uri in self.agent_registry:
391
  target_agent = self.agent_registry[target_uri]
392
- self._send_to_agent(target_agent['url'], envelope)
 
 
 
393
  else:
394
- # Broadcast to all conversation participants
395
- for participant_uri in conversation['participants']:
396
- if participant_uri != envelope.sender.speakerUri: # Don't echo back
397
- if participant_uri in self.agent_registry:
398
- participant_agent = self.agent_registry[participant_uri]
399
- self._send_to_agent(participant_agent['url'], envelope)
400
-
401
- # Store message in conversation history
402
- conversation['messages'].append({
403
- 'envelope': envelope,
404
- 'timestamp': datetime.now()
405
- })
406
-
407
  self._update_visual_state(conversation_id)
408
- return True
 
409
 
410
  def _send_to_agent(self, agent_url: str, envelope: Envelope) -> bool:
411
- """Send envelope to specific agent"""
 
 
 
 
412
  try:
413
  response = requests.post(
414
- f"{agent_url}/openfloor/conversation",
415
  json=json.loads(envelope.to_json()),
416
  headers={'Content-Type': 'application/json'},
417
  timeout=30
418
  )
419
- return response.status_code == 200
 
 
 
 
 
420
  except Exception as e:
421
  print(f"πŸ›οΈ Floor Manager: Error sending to {agent_url}: {e}")
422
  return False
423
 
424
  def _update_visual_state(self, conversation_id: str):
425
  """Update visual interface based on conversation state"""
426
- if self.visual_callback and conversation_id in self.active_conversations:
427
- conversation = self.active_conversations[conversation_id]
428
 
429
- # Convert to visual format
430
- participants = []
431
- messages = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
- for participant_uri in conversation['participants']:
434
- if participant_uri in self.agent_registry:
435
- agent_info = self.agent_registry[participant_uri]
436
- participants.append(agent_info['manifest'].identification.conversationalName)
 
437
 
438
- for msg_info in conversation['messages']:
439
- envelope = msg_info['envelope']
440
- sender_uri = envelope.sender.speakerUri
441
-
442
- if sender_uri in self.agent_registry:
443
- sender_name = self.agent_registry[sender_uri]['manifest'].identification.conversationalName
444
-
445
- # Extract message content
446
- for event in envelope.events:
447
- if hasattr(event, 'eventType') and event.eventType == 'utterance':
448
- # Extract text from dialog event
449
- dialog_event = event.parameters.get('dialogEvent')
450
- if dialog_event:
451
- features = dialog_event.get('features', {})
452
- text_feature = features.get('text', {})
453
- tokens = text_feature.get('tokens', [])
454
- text = ' '.join([token.get('value', '') for token in tokens])
455
-
456
  messages.append({
457
  'speaker': sender_name,
458
  'text': text,
459
- 'timestamp': msg_info['timestamp'].strftime('%H:%M:%S')
 
460
  })
461
-
462
- self.visual_callback({
463
- 'participants': participants,
464
- 'messages': messages,
465
- 'currentSpeaker': None,
466
- 'thinking': [],
467
- 'showBubbles': participants,
468
- 'avatarImages': avatar_images
469
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
 
471
  def set_visual_callback(self, callback):
472
  """Set callback for visual updates"""
@@ -499,6 +603,7 @@ class OpenFloorManager:
499
  return jsonify({'error': 'Failed to route message'}), 400
500
 
501
  except Exception as e:
 
502
  return jsonify({'error': str(e)}), 500
503
 
504
  @app.route('/openfloor/invite', methods=['POST'])
@@ -517,6 +622,7 @@ class OpenFloorManager:
517
  return jsonify({'success': success})
518
 
519
  except Exception as e:
 
520
  return jsonify({'error': str(e)}), 500
521
 
522
  # Start server in background thread
@@ -590,7 +696,11 @@ class VisualConsensusEngine:
590
  self.moderator_model = moderator_model or MODERATOR_MODEL
591
  self.update_callback = update_callback
592
  self.session_id = session_id
593
-
 
 
 
 
594
  # Create OpenFloor research agents
595
  from research_tools import WebSearchTool, WikipediaSearchTool, ArxivSearchTool, GitHubSearchTool, SECSearchTool
596
 
@@ -602,8 +712,12 @@ class VisualConsensusEngine:
602
  'sec_edgar': OpenFloorResearchAgent(SECSearchTool(), port=8005)
603
  }
604
 
 
605
  self.start_openfloor_research_agents()
606
 
 
 
 
607
  # Available research agents for discovery
608
  self.available_research_agents = list(self.research_agents.keys())
609
 
@@ -643,6 +757,12 @@ class VisualConsensusEngine:
643
  'sambanova': sambanova_key
644
  }
645
 
 
 
 
 
 
 
646
  # PROFESSIONAL: Strong, expert role definitions matched to decision protocols
647
  self.roles = {
648
  'standard': "Provide expert analysis with clear reasoning and evidence.",
@@ -683,8 +803,7 @@ class VisualConsensusEngine:
683
  }
684
 
685
  def start_openfloor_research_agents(self):
686
- """Start all research agents as proper OpenFloor services"""
687
-
688
  agent_ports = {
689
  'web_search': 8001,
690
  'wikipedia': 8002,
@@ -703,18 +822,178 @@ class VisualConsensusEngine:
703
  self.agent_servers[agent_name] = {
704
  'server': server,
705
  'port': port,
706
- 'url': f"http://localhost:{port}/openfloor/conversation",
707
  'manifest_url': f"http://localhost:{port}/openfloor/manifest"
708
  }
 
 
 
 
709
 
710
  # Small delay between starting servers
711
  time.sleep(0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
  def update_visual_state(self, state_update: Dict[str, Any]):
714
  """Update the visual roundtable state for this session"""
715
  if self.update_callback:
716
  self.update_callback(state_update)
717
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  def handle_function_calls(self, completion, original_prompt: str, calling_model: str) -> str:
719
  """UNIFIED function call handler with enhanced research capabilities"""
720
 
@@ -870,131 +1149,34 @@ class VisualConsensusEngine:
870
  print(f"Error in follow-up completion for {calling_model}: {str(e)}")
871
  return message.content or "Analysis completed with research integration."
872
 
873
-
874
  def _execute_research_function(self, function_name: str, arguments: dict, requesting_model_name: str = None) -> str:
875
- """Execute research function using proper OpenFloor HTTP protocol"""
876
 
877
  query_param = arguments.get("query") or arguments.get("topic") or arguments.get("technology") or arguments.get("company")
878
 
879
- # Phase 1: Show research STARTING
880
- if query_param:
881
- self.show_research_starting(function_name, query_param)
 
 
882
 
883
  try:
884
- # Map function names to research agents
885
- function_to_agent = {
886
- "search_web": "web_search",
887
- "search_wikipedia": "wikipedia",
888
- "search_academic": "arxiv",
889
- "search_technology_trends": "github",
890
- "search_financial_data": "sec_edgar"
891
- }
892
 
893
- result = ""
 
894
 
895
- if function_name in function_to_agent:
896
- agent_name = function_to_agent[function_name]
897
-
898
- if agent_name not in self.agent_servers:
899
- return f"Research agent '{agent_name}' not available"
900
-
901
- agent_server = self.agent_servers[agent_name]
902
-
903
- self.update_research_progress(f"Sending HTTP request to OpenFloor agent...")
904
-
905
- # Create OpenFloor envelope
906
- conversation = Conversation()
907
- request_dialog = DialogEvent(
908
- speakerUri=f"tag:consilium.ai,2025:{requesting_model_name or 'expert'}",
909
- features={"text": TextFeature(values=[query_param])}
910
- )
911
-
912
- request_envelope = Envelope(
913
- conversation=conversation,
914
- sender=Sender(speakerUri=f"tag:consilium.ai,2025:{requesting_model_name or 'expert'}"),
915
- events=[
916
- UtteranceEvent(
917
- dialogEvent=request_dialog,
918
- to=To(speakerUri=self.research_agents[agent_name].manifest.identification.speakerUri)
919
- )
920
- ]
921
- )
922
-
923
- # Send HTTP POST request to OpenFloor agent service
924
- response = self._send_openfloor_request(agent_server['url'], request_envelope)
925
-
926
- if response:
927
- result = self._extract_research_result_from_envelope(response)
928
- self.update_research_progress(f"OpenFloor HTTP response received - {len(result)} characters")
929
- else:
930
- result = f"Failed to get response from {agent_name} OpenFloor service"
931
-
932
- else:
933
- result = f"Unknown research function: {function_name}"
934
-
935
- # Phase 3: Show research complete
936
- if query_param:
937
- self.show_research_complete(function_name, query_param, len(result), requesting_model_name)
938
-
939
  return result
940
 
941
  except Exception as e:
942
  error_msg = str(e)
943
- if query_param:
944
- self.show_research_error(function_name, query_param, error_msg, requesting_model_name)
945
- return f"OpenFloor HTTP research error: {error_msg}"
946
-
947
- def _send_openfloor_request(self, agent_url: str, envelope: Envelope) -> Optional[Envelope]:
948
- """Send HTTP request to OpenFloor agent service"""
949
- try:
950
- import requests
951
-
952
- print(f"πŸ” DEBUG: Sending request to {agent_url}")
953
-
954
- # Serialize envelope to JSON
955
- envelope_json = json.loads(envelope.to_json())
956
- print(f"πŸ” DEBUG: Envelope serialized, size: {len(str(envelope_json))} chars")
957
-
958
- # Send HTTP POST request
959
- response = requests.post(
960
- agent_url,
961
- json=envelope_json,
962
- headers={'Content-Type': 'application/json'},
963
- timeout=30
964
- )
965
-
966
- print(f"πŸ” DEBUG: Response status: {response.status_code}")
967
- print(f"πŸ” DEBUG: Response text: {response.text[:200]}...")
968
-
969
- if response.status_code == 200:
970
- # Parse response back to OpenFloor envelope
971
- response_data = response.json()
972
- return Envelope.from_json(json.dumps(response_data))
973
- else:
974
- print(f"OpenFloor HTTP error: {response.status_code} - {response.text}")
975
- return None
976
-
977
- except Exception as e:
978
- print(f"πŸ” DEBUG: Exception in _send_openfloor_request: {e}")
979
- import traceback
980
- traceback.print_exc()
981
- return None
982
-
983
- def _extract_research_result_from_envelope(self, envelope: Envelope) -> str:
984
- """Extract research result from OpenFloor response envelope"""
985
- try:
986
- for event in envelope.events:
987
- if hasattr(event, 'eventType') and event.eventType == 'utterance':
988
- dialog_event = event.parameters.get('dialogEvent')
989
- if dialog_event and hasattr(dialog_event, 'features'):
990
- text_feature = dialog_event.features.get('text')
991
- if text_feature and hasattr(text_feature, 'tokens'):
992
- return ' '.join([token.get('value', '') for token in text_feature.tokens])
993
-
994
- return "No research result found in OpenFloor response"
995
-
996
- except Exception as e:
997
- return f"Error extracting OpenFloor research result: {str(e)}"
998
 
999
  def show_research_starting(self, function: str, query: str):
1000
  """Invite specific research agent to join conversation"""
@@ -1036,7 +1218,6 @@ class VisualConsensusEngine:
1036
  "showBubbles": existing_bubbles
1037
  })
1038
 
1039
-
1040
  def show_research_complete(self, function: str, query: str, result_length: int, requesting_model_name: str = None):
1041
  """Show research complete and dismiss the specific agent"""
1042
  function_to_agent = {
@@ -1077,17 +1258,6 @@ class VisualConsensusEngine:
1077
  # Use the existing dismiss method
1078
  self.dismiss_research_agent(agent_name, "current_conversation")
1079
 
1080
- def estimate_research_time(self, function_name: str) -> str:
1081
- """Provide time estimates for different research functions"""
1082
- time_estimates = {
1083
- "search_web": "30-60 seconds",
1084
- "search_wikipedia": "15-30 seconds",
1085
- "search_academic": "2-5 minutes",
1086
- "search_technology_trends": "1-2 minutes",
1087
- "search_financial_data": "1-3 minutes"
1088
- }
1089
- return time_estimates.get(function_name, "1-3 minutes")
1090
-
1091
  def show_research_error(self, function: str, query: str, error: str, requesting_model_name: str = None):
1092
  """Show research error from the specific agent and dismiss it"""
1093
  function_to_agent = {
@@ -1128,55 +1298,6 @@ class VisualConsensusEngine:
1128
  # Dismiss the research agent since research failed
1129
  self.dismiss_research_agent(agent_name, "current_conversation")
1130
 
1131
- def update_research_progress(self, progress_text: str, function_name: str = None):
1132
- """Update research progress from the specific active research agent"""
1133
-
1134
- # Map function to agent to identify which agent is providing progress
1135
- function_to_agent = {
1136
- "search_web": "web_search",
1137
- "search_wikipedia": "wikipedia",
1138
- "search_academic": "arxiv",
1139
- "search_technology_trends": "github",
1140
- "search_financial_data": "sec_edgar"
1141
- }
1142
-
1143
- # Determine which research agent is active
1144
- if function_name and function_name in function_to_agent:
1145
- agent_name = function_to_agent[function_name]
1146
- research_agent = self.research_agents[agent_name]
1147
- agent_display_name = research_agent.manifest.identification.conversationalName
1148
- else:
1149
- # Fallback to generic research agent if function not specified
1150
- agent_display_name = "Research Agent"
1151
-
1152
- session = get_or_create_session_state(self.session_id)
1153
- current_state = session["roundtable_state"]
1154
- all_messages = list(current_state.get("messages", []))
1155
- participants = current_state.get("participants", [])
1156
-
1157
- # Ensure the specific research agent is visible
1158
- existing_bubbles = list(current_state.get("showBubbles", []))
1159
- if agent_display_name not in existing_bubbles:
1160
- existing_bubbles.append(agent_display_name)
1161
-
1162
- # Add progress message from the specific agent
1163
- progress_message = {
1164
- "speaker": agent_display_name,
1165
- "text": f"πŸ”„ {progress_text}",
1166
- "type": "research_progress"
1167
- }
1168
- all_messages.append(progress_message)
1169
-
1170
- # Keep the agent active and visible during progress
1171
- self.update_visual_state({
1172
- "participants": participants,
1173
- "messages": all_messages,
1174
- "currentSpeaker": agent_display_name,
1175
- "thinking": [],
1176
- "showBubbles": existing_bubbles
1177
- })
1178
- time.sleep(0.3)
1179
-
1180
  def invite_research_agent(self, agent_name: str, conversation_id: str, requesting_expert: str):
1181
  """Invite a research agent to join the conversation visually"""
1182
 
@@ -1483,6 +1604,32 @@ class VisualConsensusEngine:
1483
  if not available_models:
1484
  return "❌ No AI models available"
1485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1486
  model_roles = self.assign_roles(available_models, role_assignment)
1487
 
1488
  visual_participant_names = [self.models[model]['name'] for model in available_models]
@@ -2364,6 +2511,38 @@ with gr.Blocks(title="🎭 Consilium: Multi-AI Expert Consensus Platform - OFP (
2364
  - Creates interesting chains of reasoning and idea evolution
2365
  - Can lead to surprising consensus emergence
2366
  - **Use when:** You want to see how ideas build and evolve
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2367
  """)
2368
 
2369
  # Launch configuration
 
295
 
296
 
297
  class OpenFloorManager:
298
+ """Central floor manager for coordinating all OpenFloor agents - FIXED VERSION"""
299
 
300
  def __init__(self, port: int = 7860):
301
  self.port = port
302
  self.agent_registry = {} # speakerUri -> agent info
303
  self.active_conversations = {} # conversation_id -> conversation state
304
  self.visual_callback = None
305
+ self.message_history = {} # conversation_id -> message list
306
 
307
  def register_agent(self, manifest: Manifest, agent_url: str):
308
  """Register an agent with the floor manager"""
 
323
  """Create a new conversation with optional initial participants"""
324
  conversation_id = f"conv:{uuid.uuid4()}"
325
 
326
+ # Create proper OpenFloor conversation structure
327
+ conversants = []
328
+ if initial_participants:
329
+ for participant_uri in initial_participants:
330
+ if participant_uri in self.agent_registry:
331
+ manifest = self.agent_registry[participant_uri]['manifest']
332
+ conversants.append(Conversant(
333
+ identification=manifest.identification
334
+ ))
335
+
336
+ conversation = Conversation(
337
+ id=conversation_id,
338
+ conversants=conversants
339
+ )
340
+
341
  self.active_conversations[conversation_id] = {
342
+ 'conversation': conversation,
343
  'participants': initial_participants or [],
344
  'messages': [],
345
  'created_at': datetime.now(),
346
  'status': 'active'
347
  }
348
 
349
+ self.message_history[conversation_id] = []
350
+
351
  print(f"πŸ›οΈ Floor Manager: Created conversation {conversation_id}")
352
  return conversation_id
353
 
354
  def invite_agent_to_conversation(self, conversation_id: str, target_speaker_uri: str,
355
  inviting_speaker_uri: str) -> bool:
356
+ """Send proper OpenFloor InviteEvent to an agent"""
357
  if conversation_id not in self.active_conversations:
358
+ print(f"πŸ›οΈ Floor Manager: Conversation {conversation_id} not found")
359
  return False
360
 
361
  if target_speaker_uri not in self.agent_registry:
362
+ print(f"πŸ›οΈ Floor Manager: Agent {target_speaker_uri} not registered")
363
  return False
364
 
365
+ conversation_state = self.active_conversations[conversation_id]
366
+ conversation = conversation_state['conversation']
367
  target_agent = self.agent_registry[target_speaker_uri]
368
 
369
+ # Create proper OpenFloor InviteEvent
370
  invite_envelope = Envelope(
371
+ conversation=conversation,
372
  sender=Sender(speakerUri=inviting_speaker_uri),
373
  events=[
374
  InviteEvent(
 
387
 
388
  if response:
389
  # Add to conversation participants
390
+ if target_speaker_uri not in conversation_state['participants']:
391
+ conversation_state['participants'].append(target_speaker_uri)
392
+
393
+ # Add to conversation conversants
394
+ target_manifest = target_agent['manifest']
395
+ conversation.conversants.append(Conversant(
396
+ identification=target_manifest.identification
397
+ ))
398
+
399
+ self._update_visual_state(conversation_id)
400
+ print(f"πŸ›οΈ Floor Manager: Successfully invited {target_speaker_uri} to {conversation_id}")
401
  return True
402
 
403
+ print(f"πŸ›οΈ Floor Manager: Failed to invite {target_speaker_uri}")
404
  return False
405
 
406
  def route_message(self, envelope: Envelope) -> bool:
407
+ """Route message to appropriate recipients with proper OpenFloor semantics"""
408
  conversation_id = envelope.conversation.id
409
 
410
  if conversation_id not in self.active_conversations:
411
+ print(f"πŸ›οΈ Floor Manager: Cannot route - conversation {conversation_id} not found")
412
  return False
413
 
414
+ conversation_state = self.active_conversations[conversation_id]
415
+ sender_uri = envelope.sender.speakerUri
416
+
417
+ # Store message in conversation history
418
+ message_record = {
419
+ 'envelope': envelope,
420
+ 'timestamp': datetime.now(),
421
+ 'sender': sender_uri
422
+ }
423
+
424
+ conversation_state['messages'].append(message_record)
425
+ self.message_history[conversation_id].append(message_record)
426
 
427
  # Process each event in the envelope
428
+ routed_successfully = True
429
+
430
  for event in envelope.events:
431
  if hasattr(event, 'to') and event.to:
432
  # Directed message - send to specific agent
433
  target_uri = event.to.speakerUri
434
+ if target_uri in self.agent_registry and target_uri != sender_uri:
435
  target_agent = self.agent_registry[target_uri]
436
+ success = self._send_to_agent(target_agent['url'], envelope)
437
+ if not success:
438
+ routed_successfully = False
439
+ print(f"πŸ›οΈ Floor Manager: Failed to route directed message to {target_uri}")
440
  else:
441
+ # Broadcast to all conversation participants except sender
442
+ for participant_uri in conversation_state['participants']:
443
+ if participant_uri != sender_uri and participant_uri in self.agent_registry:
444
+ participant_agent = self.agent_registry[participant_uri]
445
+ success = self._send_to_agent(participant_agent['url'], envelope)
446
+ if not success:
447
+ routed_successfully = False
448
+ print(f"πŸ›οΈ Floor Manager: Failed to broadcast to {participant_uri}")
449
+
450
+ # Update visual state after routing
 
 
 
451
  self._update_visual_state(conversation_id)
452
+
453
+ return routed_successfully
454
 
455
  def _send_to_agent(self, agent_url: str, envelope: Envelope) -> bool:
456
+ """Send envelope to specific agent via HTTP POST"""
457
+ if agent_url == "internal://ai-expert":
458
+ # Internal AI experts don't have HTTP endpoints
459
+ return True
460
+
461
  try:
462
  response = requests.post(
463
+ f"{agent_url}/conversation",
464
  json=json.loads(envelope.to_json()),
465
  headers={'Content-Type': 'application/json'},
466
  timeout=30
467
  )
468
+ success = response.status_code == 200
469
+ if success:
470
+ print(f"πŸ›οΈ Floor Manager: Successfully sent message to {agent_url}")
471
+ else:
472
+ print(f"πŸ›οΈ Floor Manager: HTTP error {response.status_code} sending to {agent_url}")
473
+ return success
474
  except Exception as e:
475
  print(f"πŸ›οΈ Floor Manager: Error sending to {agent_url}: {e}")
476
  return False
477
 
478
  def _update_visual_state(self, conversation_id: str):
479
  """Update visual interface based on conversation state"""
480
+ if not self.visual_callback or conversation_id not in self.active_conversations:
481
+ return
482
 
483
+ conversation_state = self.active_conversations[conversation_id]
484
+
485
+ # Convert to visual format
486
+ participants = []
487
+ messages = []
488
+
489
+ # Get participant names
490
+ for participant_uri in conversation_state['participants']:
491
+ if participant_uri in self.agent_registry:
492
+ agent_info = self.agent_registry[participant_uri]
493
+ participants.append(agent_info['manifest'].identification.conversationalName)
494
+ else:
495
+ # Handle AI experts or other internal participants
496
+ participants.append(participant_uri.split(':')[-1] if ':' in participant_uri else participant_uri)
497
+
498
+ # Convert messages
499
+ for msg_record in conversation_state['messages']:
500
+ envelope = msg_record['envelope']
501
+ sender_uri = envelope.sender.speakerUri
502
 
503
+ # Get sender name
504
+ if sender_uri in self.agent_registry:
505
+ sender_name = self.agent_registry[sender_uri]['manifest'].identification.conversationalName
506
+ else:
507
+ sender_name = sender_uri.split(':')[-1] if ':' in sender_uri else sender_uri
508
 
509
+ # Extract message content from events
510
+ for event in envelope.events:
511
+ if hasattr(event, 'eventType'):
512
+ if event.eventType == 'utterance':
513
+ # Extract text from utterance dialog event
514
+ dialog_event = event.parameters.get('dialogEvent')
515
+ if dialog_event:
516
+ text = self._extract_text_from_dialog_event(dialog_event)
517
+ if text:
 
 
 
 
 
 
 
 
 
518
  messages.append({
519
  'speaker': sender_name,
520
  'text': text,
521
+ 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
522
+ 'type': 'utterance'
523
  })
524
+
525
+ elif event.eventType == 'context':
526
+ # Handle context events (like research results)
527
+ context_params = event.parameters
528
+ if 'research_function' in context_params:
529
+ messages.append({
530
+ 'speaker': sender_name,
531
+ 'text': f"πŸ” Research: {context_params.get('result', 'No result')}",
532
+ 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
533
+ 'type': 'research_result'
534
+ })
535
+
536
+ elif event.eventType == 'invite':
537
+ messages.append({
538
+ 'speaker': sender_name,
539
+ 'text': f"πŸ“¨ Invited agent to join conversation",
540
+ 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
541
+ 'type': 'system'
542
+ })
543
+
544
+ elif event.eventType == 'bye':
545
+ messages.append({
546
+ 'speaker': sender_name,
547
+ 'text': f"πŸ‘‹ Left the conversation",
548
+ 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
549
+ 'type': 'system'
550
+ })
551
+
552
+ # Call visual callback
553
+ self.visual_callback({
554
+ 'participants': participants,
555
+ 'messages': messages,
556
+ 'currentSpeaker': None,
557
+ 'thinking': [],
558
+ 'showBubbles': participants,
559
+ 'avatarImages': avatar_images
560
+ })
561
+
562
+ def _extract_text_from_dialog_event(self, dialog_event) -> str:
563
+ """Extract text from dialog event structure"""
564
+ try:
565
+ if isinstance(dialog_event, dict):
566
+ features = dialog_event.get('features', {})
567
+ text_feature = features.get('text', {})
568
+ tokens = text_feature.get('tokens', [])
569
+ return ' '.join([token.get('value', '') for token in tokens])
570
+ return ""
571
+ except Exception as e:
572
+ print(f"πŸ›οΈ Floor Manager: Error extracting text: {e}")
573
+ return ""
574
 
575
  def set_visual_callback(self, callback):
576
  """Set callback for visual updates"""
 
603
  return jsonify({'error': 'Failed to route message'}), 400
604
 
605
  except Exception as e:
606
+ print(f"πŸ›οΈ Floor Manager: Error handling conversation: {e}")
607
  return jsonify({'error': str(e)}), 500
608
 
609
  @app.route('/openfloor/invite', methods=['POST'])
 
622
  return jsonify({'success': success})
623
 
624
  except Exception as e:
625
+ print(f"πŸ›οΈ Floor Manager: Error inviting agent: {e}")
626
  return jsonify({'error': str(e)}), 500
627
 
628
  # Start server in background thread
 
696
  self.moderator_model = moderator_model or MODERATOR_MODEL
697
  self.update_callback = update_callback
698
  self.session_id = session_id
699
+
700
+ # Initialize OpenFloor Manager FIRST
701
+ self.floor_manager = OpenFloorManager(port=7860)
702
+ self.floor_manager.set_visual_callback(self.update_visual_state)
703
+
704
  # Create OpenFloor research agents
705
  from research_tools import WebSearchTool, WikipediaSearchTool, ArxivSearchTool, GitHubSearchTool, SECSearchTool
706
 
 
712
  'sec_edgar': OpenFloorResearchAgent(SECSearchTool(), port=8005)
713
  }
714
 
715
+ # Start research agents and register with floor manager
716
  self.start_openfloor_research_agents()
717
 
718
+ # Create a persistent conversation for this session
719
+ self.conversation_id = self.floor_manager.create_conversation([])
720
+
721
  # Available research agents for discovery
722
  self.available_research_agents = list(self.research_agents.keys())
723
 
 
757
  'sambanova': sambanova_key
758
  }
759
 
760
+ # Register AI experts with floor manager
761
+ self.register_ai_experts()
762
+
763
+ # Start floor manager service
764
+ self.floor_manager.start_floor_manager_service()
765
+
766
  # PROFESSIONAL: Strong, expert role definitions matched to decision protocols
767
  self.roles = {
768
  'standard': "Provide expert analysis with clear reasoning and evidence.",
 
803
  }
804
 
805
  def start_openfloor_research_agents(self):
806
+ """Start research agents and register them with the floor manager"""
 
807
  agent_ports = {
808
  'web_search': 8001,
809
  'wikipedia': 8002,
 
822
  self.agent_servers[agent_name] = {
823
  'server': server,
824
  'port': port,
825
+ 'url': f"http://localhost:{port}",
826
  'manifest_url': f"http://localhost:{port}/openfloor/manifest"
827
  }
828
+
829
+ # Register with floor manager
830
+ manifest = agent.get_manifest()
831
+ self.floor_manager.register_agent(manifest, f"http://localhost:{port}")
832
 
833
  # Small delay between starting servers
834
  time.sleep(0.5)
835
+
836
+ def register_ai_experts(self):
837
+ """Register AI expert models as OpenFloor agents"""
838
+ for model_key, model_info in self.models.items():
839
+ if model_info['available']:
840
+ # Create manifest for AI expert
841
+ expert_manifest = Manifest(
842
+ identification=Identification(
843
+ speakerUri=f"tag:consilium.ai,2025:{model_key}",
844
+ conversationalName=model_info['name'],
845
+ role="AI Expert",
846
+ organization="Consilium Expert Panel",
847
+ synopsis=f"Expert AI model: {model_info['name']}"
848
+ ),
849
+ capabilities=[
850
+ Capability(
851
+ keyphrases=["analysis", "expertise", "reasoning", "decision"],
852
+ descriptions=[f"Expert analysis and reasoning by {model_info['name']}"],
853
+ languages=["en-us"]
854
+ )
855
+ ]
856
+ )
857
+
858
+ # Register with floor manager (no URL since these are internal)
859
+ self.floor_manager.register_agent(expert_manifest, "internal://ai-expert")
860
 
861
  def update_visual_state(self, state_update: Dict[str, Any]):
862
  """Update the visual roundtable state for this session"""
863
  if self.update_callback:
864
  self.update_callback(state_update)
865
+
866
+ def send_research_request_via_floor(self, function_name: str, query: str, requesting_expert: str) -> str:
867
+ """Send research request through proper OpenFloor messaging"""
868
+
869
+ # Map function to research agent
870
+ function_to_agent = {
871
+ "search_web": "web_search",
872
+ "search_wikipedia": "wikipedia",
873
+ "search_academic": "arxiv",
874
+ "search_technology_trends": "github",
875
+ "search_financial_data": "sec_edgar"
876
+ }
877
+
878
+ if function_name not in function_to_agent:
879
+ return f"Unknown research function: {function_name}"
880
+
881
+ agent_name = function_to_agent[function_name]
882
+ research_agent = self.research_agents[agent_name]
883
+ target_speaker_uri = research_agent.manifest.identification.speakerUri
884
+ requesting_speaker_uri = f"tag:consilium.ai,2025:{requesting_expert}"
885
+
886
+ # Step 1: Invite research agent to conversation
887
+ success = self.floor_manager.invite_agent_to_conversation(
888
+ self.conversation_id,
889
+ target_speaker_uri,
890
+ requesting_speaker_uri
891
+ )
892
+
893
+ if not success:
894
+ return f"Failed to invite {agent_name} to conversation"
895
+
896
+ # Step 2: Send research request via floor manager
897
+ research_dialog = DialogEvent(
898
+ speakerUri=requesting_speaker_uri,
899
+ features={"text": TextFeature(values=[query])}
900
+ )
901
+
902
+ research_envelope = Envelope(
903
+ conversation=Conversation(id=self.conversation_id),
904
+ sender=Sender(speakerUri=requesting_speaker_uri),
905
+ events=[
906
+ UtteranceEvent(
907
+ dialogEvent=research_dialog,
908
+ to=To(speakerUri=target_speaker_uri)
909
+ )
910
+ ]
911
+ )
912
+
913
+ # Route through floor manager
914
+ routing_success = self.floor_manager.route_message(research_envelope)
915
+
916
+ if not routing_success:
917
+ return f"Failed to route research request to {agent_name}"
918
+
919
+ # Step 3: Wait for response and collect result
920
+ # In a real implementation, this would be asynchronous
921
+ # For now, we'll use the direct research call as fallback
922
+ try:
923
+ result = research_agent.tool.search(query)
924
+
925
+ # Step 4: Send result back through floor as ContextEvent
926
+ result_envelope = Envelope(
927
+ conversation=Conversation(id=self.conversation_id),
928
+ sender=Sender(speakerUri=target_speaker_uri),
929
+ events=[
930
+ ContextEvent(
931
+ parameters={
932
+ "research_function": function_name,
933
+ "query": query,
934
+ "requesting_expert": requesting_expert,
935
+ "result": result
936
+ }
937
+ )
938
+ ]
939
+ )
940
+
941
+ self.floor_manager.route_message(result_envelope)
942
+
943
+ # Step 5: Remove research agent from conversation
944
+ self.dismiss_research_agent_via_floor(agent_name, requesting_expert)
945
+
946
+ return result
947
+
948
+ except Exception as e:
949
+ error_msg = f"Research error: {str(e)}"
950
+
951
+ # Send error through floor
952
+ error_envelope = Envelope(
953
+ conversation=Conversation(id=self.conversation_id),
954
+ sender=Sender(speakerUri=target_speaker_uri),
955
+ events=[
956
+ ContextEvent(
957
+ parameters={
958
+ "research_error": error_msg,
959
+ "function": function_name,
960
+ "query": query
961
+ }
962
+ )
963
+ ]
964
+ )
965
+
966
+ self.floor_manager.route_message(error_envelope)
967
+ self.dismiss_research_agent_via_floor(agent_name, requesting_expert)
968
+
969
+ return error_msg
970
+
971
+ def dismiss_research_agent_via_floor(self, agent_name: str, requesting_expert: str):
972
+ """Properly dismiss research agent via OpenFloor messaging"""
973
+ research_agent = self.research_agents[agent_name]
974
+ target_speaker_uri = research_agent.manifest.identification.speakerUri
975
+
976
+ # Send bye event
977
+ bye_envelope = Envelope(
978
+ conversation=Conversation(id=self.conversation_id),
979
+ sender=Sender(speakerUri=target_speaker_uri),
980
+ events=[
981
+ ByeEvent(
982
+ parameters={
983
+ "message": "Research task completed. Leaving conversation."
984
+ }
985
+ )
986
+ ]
987
+ )
988
+
989
+ self.floor_manager.route_message(bye_envelope)
990
+
991
+ # Remove from conversation participants
992
+ if self.conversation_id in self.floor_manager.active_conversations:
993
+ conversation_state = self.floor_manager.active_conversations[self.conversation_id]
994
+ if target_speaker_uri in conversation_state['participants']:
995
+ conversation_state['participants'].remove(target_speaker_uri)
996
+
997
  def handle_function_calls(self, completion, original_prompt: str, calling_model: str) -> str:
998
  """UNIFIED function call handler with enhanced research capabilities"""
999
 
 
1149
  print(f"Error in follow-up completion for {calling_model}: {str(e)}")
1150
  return message.content or "Analysis completed with research integration."
1151
 
 
1152
  def _execute_research_function(self, function_name: str, arguments: dict, requesting_model_name: str = None) -> str:
1153
+ """Execute research function using proper OpenFloor messaging"""
1154
 
1155
  query_param = arguments.get("query") or arguments.get("topic") or arguments.get("technology") or arguments.get("company")
1156
 
1157
+ if not query_param:
1158
+ return "No query parameter found in research request"
1159
+
1160
+ # Show research starting
1161
+ self.show_research_starting(function_name, query_param)
1162
 
1163
  try:
1164
+ # Use OpenFloor messaging for research
1165
+ result = self.send_research_request_via_floor(
1166
+ function_name,
1167
+ query_param,
1168
+ requesting_model_name or 'unknown'
1169
+ )
 
 
1170
 
1171
+ # Show research complete
1172
+ self.show_research_complete(function_name, query_param, len(result), requesting_model_name)
1173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1174
  return result
1175
 
1176
  except Exception as e:
1177
  error_msg = str(e)
1178
+ self.show_research_error(function_name, query_param, error_msg, requesting_model_name)
1179
+ return f"OpenFloor research error: {error_msg}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1180
 
1181
  def show_research_starting(self, function: str, query: str):
1182
  """Invite specific research agent to join conversation"""
 
1218
  "showBubbles": existing_bubbles
1219
  })
1220
 
 
1221
  def show_research_complete(self, function: str, query: str, result_length: int, requesting_model_name: str = None):
1222
  """Show research complete and dismiss the specific agent"""
1223
  function_to_agent = {
 
1258
  # Use the existing dismiss method
1259
  self.dismiss_research_agent(agent_name, "current_conversation")
1260
 
 
 
 
 
 
 
 
 
 
 
 
1261
  def show_research_error(self, function: str, query: str, error: str, requesting_model_name: str = None):
1262
  """Show research error from the specific agent and dismiss it"""
1263
  function_to_agent = {
 
1298
  # Dismiss the research agent since research failed
1299
  self.dismiss_research_agent(agent_name, "current_conversation")
1300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1301
  def invite_research_agent(self, agent_name: str, conversation_id: str, requesting_expert: str):
1302
  """Invite a research agent to join the conversation visually"""
1303
 
 
1604
  if not available_models:
1605
  return "❌ No AI models available"
1606
 
1607
+ # Add AI experts to the conversation
1608
+ for model in available_models:
1609
+ expert_speaker_uri = f"tag:consilium.ai,2025:{model}"
1610
+ if self.conversation_id in self.floor_manager.active_conversations:
1611
+ conversation_state = self.floor_manager.active_conversations[self.conversation_id]
1612
+ if expert_speaker_uri not in conversation_state['participants']:
1613
+ conversation_state['participants'].append(expert_speaker_uri)
1614
+
1615
+ # Send conversation start event through floor
1616
+ start_envelope = Envelope(
1617
+ conversation=Conversation(id=self.conversation_id),
1618
+ sender=Sender(speakerUri="tag:consilium.ai,2025:session-manager"),
1619
+ events=[
1620
+ ContextEvent(
1621
+ parameters={
1622
+ "session_start": True,
1623
+ "question": question,
1624
+ "protocol": decision_protocol,
1625
+ "participants": [self.models[model]['name'] for model in available_models]
1626
+ }
1627
+ )
1628
+ ]
1629
+ )
1630
+
1631
+ self.floor_manager.route_message(start_envelope)
1632
+
1633
  model_roles = self.assign_roles(available_models, role_assignment)
1634
 
1635
  visual_participant_names = [self.models[model]['name'] for model in available_models]
 
2511
  - Creates interesting chains of reasoning and idea evolution
2512
  - Can lead to surprising consensus emergence
2513
  - **Use when:** You want to see how ideas build and evolve
2514
+
2515
+ ## πŸ›οΈ **OpenFloor Protocol Integration**
2516
+
2517
+ This implementation uses the [Open Floor Protocol (OFP)](https://github.com/open-voice-interoperability/openfloor-docs) for:
2518
+
2519
+ ### πŸ”„ **Inter-Agent Communication**
2520
+ - **Envelope Structure**: Standard JSON format for agent messaging
2521
+ - **Event Types**: Utterance, Context, Invite, Bye events
2522
+ - **Conversation Management**: Persistent conversation threads
2523
+ - **Agent Discovery**: Manifest-based capability matching
2524
+
2525
+ ### πŸ” **Research Agent Architecture**
2526
+ - **Dedicated Services**: Each research tool runs as an independent OpenFloor agent
2527
+ - **HTTP Endpoints**: `/openfloor/conversation` and `/openfloor/manifest`
2528
+ - **Proper Invitation Flow**: Agents are invited to join conversations for specific research tasks
2529
+ - **Context Events**: Research results are delivered via standardized context events
2530
+
2531
+ ### πŸ›οΈ **Floor Manager**
2532
+ - **Central Coordination**: Routes messages between all agents
2533
+ - **Conversation State**: Maintains participant lists and message history
2534
+ - **Visual Integration**: Updates UI based on OpenFloor conversation events
2535
+ - **Session Isolation**: Each user session has its own conversation context
2536
+
2537
+ ### πŸ“‹ **Event Flow Example**
2538
+ 1. **AI Expert** needs research β†’ generates function call
2539
+ 2. **Floor Manager** invites **Research Agent** to conversation
2540
+ 3. **Research Agent** joins and receives query via UtteranceEvent
2541
+ 4. **Research Agent** performs search and responds with ContextEvent
2542
+ 5. **Floor Manager** routes result back to requesting expert
2543
+ 6. **Research Agent** sends ByeEvent and leaves conversation
2544
+
2545
+ This architecture ensures true OpenFloor compliance while maintaining the visual roundtable experience.
2546
  """)
2547
 
2548
  # Launch configuration