azettl commited on
Commit
286db3a
·
verified ·
1 Parent(s): 930476c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +3 -595
app.py CHANGED
@@ -14,6 +14,9 @@ import queue
14
  import uuid
15
  from gradio_consilium_roundtable import consilium_roundtable
16
  from research_tools.base_tool import BaseTool
 
 
 
17
  from openfloor import *
18
  from openfloor.manifest import *
19
  from openfloor.envelope import *
@@ -43,601 +46,6 @@ avatar_images = {
43
  "Wikipedia Research Agent": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Wikipedia-logo-v2.svg/103px-Wikipedia-logo-v2.svg.png"
44
  }
45
 
46
- class OpenFloorResearchAgent:
47
- """Wrap research tools as independent OpenFloor agents"""
48
-
49
- def __init__(self, tool: BaseTool, port: int = None):
50
- self.tool = tool
51
- self.port = port
52
- self.manifest = self._create_manifest()
53
- self.active_conversations = {}
54
-
55
- def _create_manifest(self) -> Manifest:
56
- """Create OpenFloor manifest for this research agent"""
57
- speaker_uri = f"tag:research.consilium,2025:{self.tool.name.lower().replace(' ', '-')}-agent"
58
-
59
- # Tool-specific keyphrases and capabilities
60
- tool_configs = {
61
- 'Web Search': {
62
- 'keyphrases': ['web', 'search', 'current', 'news', 'latest', 'recent'],
63
- 'synopsis': 'Real-time web search for current information and trends'
64
- },
65
- 'Wikipedia': {
66
- 'keyphrases': ['facts', 'encyclopedia', 'history', 'knowledge', 'definition'],
67
- 'synopsis': 'Authoritative encyclopedia research and factual verification'
68
- },
69
- 'arXiv': {
70
- 'keyphrases': ['academic', 'research', 'papers', 'science', 'study'],
71
- 'synopsis': 'Academic research papers and scientific literature analysis'
72
- },
73
- 'GitHub': {
74
- 'keyphrases': ['technology', 'code', 'development', 'programming', 'trends'],
75
- 'synopsis': 'Technology adoption trends and software development analysis'
76
- },
77
- 'SEC EDGAR': {
78
- 'keyphrases': ['financial', 'company', 'earnings', 'sec', 'filings'],
79
- 'synopsis': 'Corporate financial data and SEC regulatory filings research'
80
- }
81
- }
82
-
83
- config = tool_configs.get(self.tool.name, {
84
- 'keyphrases': ['research', 'data'],
85
- 'synopsis': self.tool.description
86
- })
87
-
88
- return Manifest(
89
- identification=Identification(
90
- speakerUri=speaker_uri,
91
- serviceUrl=f"http://localhost:{self.port}/openfloor" if self.port else None,
92
- conversationalName=f"{self.tool.name} Research Agent",
93
- organization="Consilium Research Division",
94
- role="Research Specialist",
95
- synopsis=config['synopsis']
96
- ),
97
- capabilities=[
98
- Capability(
99
- keyphrases=config['keyphrases'],
100
- descriptions=[self.tool.description],
101
- languages=["en-us"]
102
- )
103
- ]
104
- )
105
-
106
- def handle_utterance_event(self, envelope: Envelope) -> Envelope:
107
- """Handle research requests from AI experts"""
108
- print(f"🔍 DEBUG: {self.tool.name} - Starting handle_utterance_event")
109
-
110
- # Extract the query from the utterance
111
- for event in envelope.events:
112
- if hasattr(event, 'eventType') and event.eventType == 'utterance':
113
- dialog_event = event.parameters.get('dialogEvent')
114
-
115
- if dialog_event and isinstance(dialog_event, dict):
116
- # dialog_event is a dict, not an object - use dict access
117
- features = dialog_event.get('features')
118
- print(f"🔍 DEBUG: features: {features}")
119
-
120
- if features and 'text' in features:
121
- text_feature = features['text']
122
- print(f"🔍 DEBUG: text_feature: {text_feature}")
123
-
124
- if 'tokens' in text_feature:
125
- tokens = text_feature['tokens']
126
- query_text = ' '.join([token.get('value', '') for token in tokens])
127
-
128
- print(f"🔍 DEBUG: {self.tool.name} received query: '{query_text}'")
129
-
130
- # Perform the research
131
- import time
132
- start_time = time.time()
133
- research_result = self.tool.search(query_text)
134
- end_time = time.time()
135
-
136
- print(f"🔍 DEBUG: {self.tool.name} completed in {end_time - start_time:.2f}s")
137
- print(f"🔍 DEBUG: Result length: {len(research_result)} chars")
138
- print(f"🔍 DEBUG: Result preview: {research_result[:200]}...")
139
-
140
- # Create response envelope
141
- return self._create_response_envelope(envelope, research_result, query_text)
142
-
143
- return self._create_error_response(envelope, "Could not extract query from request")
144
-
145
- def _create_response_envelope(self, original_envelope: Envelope, research_result: str, query: str) -> Envelope:
146
- """Create OpenFloor response envelope with research results"""
147
-
148
- # Create response dialog event
149
- response_dialog = DialogEvent(
150
- speakerUri=self.manifest.identification.speakerUri,
151
- features={
152
- "text": TextFeature(values=[research_result])
153
- }
154
- )
155
-
156
- # Create context with research metadata
157
- research_context = ContextEvent(
158
- parameters={
159
- "research_tool": self.tool.name,
160
- "query": query,
161
- "source": self.tool.name.lower().replace(' ', '_'),
162
- "confidence": self._assess_result_confidence(research_result),
163
- "timestamp": datetime.now().isoformat()
164
- }
165
- )
166
-
167
- # Create response envelope
168
- response_envelope = Envelope(
169
- conversation=original_envelope.conversation,
170
- sender=Sender(speakerUri=self.manifest.identification.speakerUri),
171
- events=[
172
- UtteranceEvent(dialogEvent=response_dialog),
173
- research_context
174
- ]
175
- )
176
-
177
- return response_envelope
178
-
179
- def _assess_result_confidence(self, result: str) -> float:
180
- """Assess confidence in research result quality"""
181
- if not result or len(result) < 50:
182
- return 0.3
183
-
184
- quality_indicators = [
185
- (len(result) > 500, 0.2), # Substantial content
186
- (any(year in result for year in ['2024', '2025']), 0.2), # Recent data
187
- (result.count('\n') > 5, 0.1), # Well-structured
188
- ('error' not in result.lower(), 0.3), # No errors
189
- (any(indicator in result.lower() for indicator in ['data', 'study', 'research']), 0.2) # Authoritative
190
- ]
191
-
192
- confidence = 0.5 # Base confidence
193
- for condition, boost in quality_indicators:
194
- if condition:
195
- confidence += boost
196
-
197
- return min(1.0, confidence)
198
-
199
- def _create_error_response(self, original_envelope: Envelope, error_msg: str) -> Envelope:
200
- """Create error response envelope"""
201
- error_dialog = DialogEvent(
202
- speakerUri=self.manifest.identification.speakerUri,
203
- features={
204
- "text": TextFeature(values=[f"Research error: {error_msg}"])
205
- }
206
- )
207
-
208
- return Envelope(
209
- conversation=original_envelope.conversation,
210
- sender=Sender(speakerUri=self.manifest.identification.speakerUri),
211
- events=[UtteranceEvent(dialogEvent=error_dialog)]
212
- )
213
-
214
- def join_conversation(self, conversation_id: str) -> bool:
215
- """Join a conversation as an active research agent"""
216
- self.active_conversations[conversation_id] = {
217
- 'joined_at': datetime.now(),
218
- 'status': 'active'
219
- }
220
- return True
221
-
222
- def leave_conversation(self, conversation_id: str) -> bool:
223
- """Leave a conversation"""
224
- if conversation_id in self.active_conversations:
225
- del self.active_conversations[conversation_id]
226
- return True
227
-
228
- def get_manifest(self) -> Manifest:
229
- """Return the OpenFloor manifest for this research agent"""
230
- return self.manifest
231
-
232
-
233
- class OpenFloorAgentServer:
234
- """Run a research agent as an actual OpenFloor service"""
235
-
236
- def __init__(self, research_agent: OpenFloorResearchAgent, port: int):
237
- self.agent = research_agent
238
- self.port = port
239
- self.app = None
240
-
241
- def start_server(self):
242
- """Start the OpenFloor agent server"""
243
- from flask import Flask, request, jsonify
244
-
245
- app = Flask(f"research-agent-{self.port}")
246
-
247
- @app.route('/openfloor/conversation', methods=['POST'])
248
- def handle_conversation():
249
- try:
250
- print(f"🔍 DEBUG: Flask route called for agent {self.agent.tool.name}")
251
-
252
- # Parse incoming OpenFloor envelope
253
- envelope_data = request.get_json()
254
- print(f"🔍 DEBUG: Received envelope data: {str(envelope_data)[:200]}...")
255
-
256
- envelope = Envelope.from_json(json.dumps(envelope_data))
257
- print(f"🔍 DEBUG: Envelope parsed successfully")
258
-
259
- # Process the request
260
- print(f"🔍 DEBUG: Calling handle_utterance_event...")
261
- response_envelope = self.agent.handle_utterance_event(envelope)
262
- print(f"🔍 DEBUG: handle_utterance_event completed")
263
-
264
- # Return OpenFloor response
265
- response_json = json.loads(response_envelope.to_json())
266
- print(f"🔍 DEBUG: Returning response: {str(response_json)[:200]}...")
267
- return jsonify(response_json)
268
-
269
- except Exception as e:
270
- print(f"🔍 DEBUG: Exception in Flask route: {e}")
271
- import traceback
272
- traceback.print_exc()
273
-
274
- error_response = self.agent._create_error_response(
275
- envelope if 'envelope' in locals() else None,
276
- str(e)
277
- )
278
- return jsonify(json.loads(error_response.to_json())), 500
279
-
280
- @app.route('/openfloor/manifest', methods=['GET'])
281
- def get_manifest():
282
- """Return agent manifest"""
283
- return jsonify(json.loads(self.agent.manifest.to_json()))
284
-
285
- # Start server in background thread
286
- import threading
287
- server_thread = threading.Thread(
288
- target=lambda: app.run(host='localhost', port=self.port, debug=False)
289
- )
290
- server_thread.daemon = True
291
- server_thread.start()
292
-
293
- print(f"🚀 OpenFloor agent '{self.agent.manifest.identification.conversationalName}' started on port {self.port}")
294
- return True
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"""
309
- speaker_uri = manifest.identification.speakerUri
310
- self.agent_registry[speaker_uri] = {
311
- 'manifest': manifest,
312
- 'url': agent_url,
313
- 'status': 'available',
314
- 'last_seen': datetime.now()
315
- }
316
- print(f"🏛️ Floor Manager: Registered agent {manifest.identification.conversationalName}")
317
-
318
- def discover_agents(self) -> List[Manifest]:
319
- """Return manifests of all registered agents"""
320
- return [info['manifest'] for info in self.agent_registry.values()]
321
-
322
- def create_conversation(self, initial_participants: List[str] = None) -> str:
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(
375
- to=To(speakerUri=target_speaker_uri),
376
- parameters={
377
- 'conversation_id': conversation_id,
378
- 'invited_by': inviting_speaker_uri,
379
- 'invitation_message': f"You are invited to join the expert analysis discussion"
380
- }
381
- )
382
- ]
383
- )
384
-
385
- # Send invitation to target agent
386
- response = self._send_to_agent(target_agent['url'], invite_envelope)
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
- # Fix: Use proper OpenFloor endpoint
463
- response = requests.post(
464
- f"{agent_url}/openfloor/conversation", # ✅ Fixed endpoint
465
- json=json.loads(envelope.to_json()),
466
- headers={'Content-Type': 'application/json'},
467
- timeout=30
468
- )
469
- success = response.status_code == 200
470
- if success:
471
- print(f"🏛️ Floor Manager: Successfully sent message to {agent_url}")
472
- else:
473
- print(f"🏛️ Floor Manager: HTTP error {response.status_code} sending to {agent_url}")
474
- print(f"🏛️ Floor Manager: Response: {response.text}")
475
- return success
476
- except Exception as e:
477
- print(f"🏛️ Floor Manager: Error sending to {agent_url}: {e}")
478
- return False
479
-
480
- def _update_visual_state(self, conversation_id: str):
481
- """Update visual interface based on conversation state"""
482
- if not self.visual_callback or conversation_id not in self.active_conversations:
483
- return
484
-
485
- conversation_state = self.active_conversations[conversation_id]
486
-
487
- # Convert to visual format
488
- participants = []
489
- messages = []
490
-
491
- # Get participant names
492
- for participant_uri in conversation_state['participants']:
493
- if participant_uri in self.agent_registry:
494
- agent_info = self.agent_registry[participant_uri]
495
- participants.append(agent_info['manifest'].identification.conversationalName)
496
- else:
497
- # Handle AI experts or other internal participants
498
- participants.append(participant_uri.split(':')[-1] if ':' in participant_uri else participant_uri)
499
-
500
- # Convert messages
501
- for msg_record in conversation_state['messages']:
502
- envelope = msg_record['envelope']
503
- sender_uri = envelope.sender.speakerUri
504
-
505
- # Get sender name
506
- if sender_uri in self.agent_registry:
507
- sender_name = self.agent_registry[sender_uri]['manifest'].identification.conversationalName
508
- else:
509
- sender_name = sender_uri.split(':')[-1] if ':' in sender_uri else sender_uri
510
-
511
- # Extract message content from events
512
- for event in envelope.events:
513
- if hasattr(event, 'eventType'):
514
- if event.eventType == 'utterance':
515
- # Extract text from utterance dialog event
516
- dialog_event = event.parameters.get('dialogEvent')
517
- if dialog_event:
518
- text = self._extract_text_from_dialog_event(dialog_event)
519
- if text:
520
- messages.append({
521
- 'speaker': sender_name,
522
- 'text': text,
523
- 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
524
- 'type': 'utterance'
525
- })
526
-
527
- elif event.eventType == 'context':
528
- # Handle context events (like research results)
529
- context_params = event.parameters
530
- if 'research_function' in context_params:
531
- messages.append({
532
- 'speaker': sender_name,
533
- 'text': f"🔍 Research: {context_params.get('result', 'No result')}",
534
- 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
535
- 'type': 'research_result'
536
- })
537
-
538
- elif event.eventType == 'invite':
539
- messages.append({
540
- 'speaker': sender_name,
541
- 'text': f"📨 Invited agent to join conversation",
542
- 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
543
- 'type': 'system'
544
- })
545
-
546
- elif event.eventType == 'bye':
547
- messages.append({
548
- 'speaker': sender_name,
549
- 'text': f"👋 Left the conversation",
550
- 'timestamp': msg_record['timestamp'].strftime('%H:%M:%S'),
551
- 'type': 'system'
552
- })
553
-
554
- # Call visual callback
555
- self.visual_callback({
556
- 'participants': participants,
557
- 'messages': messages,
558
- 'currentSpeaker': None,
559
- 'thinking': [],
560
- 'showBubbles': participants,
561
- 'avatarImages': avatar_images
562
- })
563
-
564
- def _extract_text_from_dialog_event(self, dialog_event) -> str:
565
- """Extract text from dialog event structure"""
566
- try:
567
- if isinstance(dialog_event, dict):
568
- features = dialog_event.get('features', {})
569
- text_feature = features.get('text', {})
570
- tokens = text_feature.get('tokens', [])
571
- return ' '.join([token.get('value', '') for token in tokens])
572
- return ""
573
- except Exception as e:
574
- print(f"🏛️ Floor Manager: Error extracting text: {e}")
575
- return ""
576
-
577
- def set_visual_callback(self, callback):
578
- """Set callback for visual updates"""
579
- self.visual_callback = callback
580
-
581
- def start_floor_manager_service(self):
582
- """Start the floor manager HTTP service"""
583
- from flask import Flask, request, jsonify
584
-
585
- app = Flask("openfloor-manager")
586
-
587
- @app.route('/openfloor/discover', methods=['GET'])
588
- def discover_agents():
589
- """Return list of available agent manifests"""
590
- manifests = [json.loads(manifest.to_json()) for manifest in self.discover_agents()]
591
- return jsonify(manifests)
592
-
593
- @app.route('/openfloor/conversation', methods=['POST'])
594
- def handle_conversation():
595
- """Handle incoming conversation messages"""
596
- try:
597
- envelope_data = request.get_json()
598
- envelope = Envelope.from_json(json.dumps(envelope_data))
599
-
600
- success = self.route_message(envelope)
601
-
602
- if success:
603
- return jsonify({'status': 'routed'})
604
- else:
605
- return jsonify({'error': 'Failed to route message'}), 400
606
-
607
- except Exception as e:
608
- print(f"🏛️ Floor Manager: Error handling conversation: {e}")
609
- return jsonify({'error': str(e)}), 500
610
-
611
- @app.route('/openfloor/invite', methods=['POST'])
612
- def invite_agent():
613
- """Handle agent invitation requests"""
614
- try:
615
- data = request.get_json()
616
- conversation_id = data['conversation_id']
617
- target_speaker_uri = data['target_speaker_uri']
618
- inviting_speaker_uri = data['inviting_speaker_uri']
619
-
620
- success = self.invite_agent_to_conversation(
621
- conversation_id, target_speaker_uri, inviting_speaker_uri
622
- )
623
-
624
- return jsonify({'success': success})
625
-
626
- except Exception as e:
627
- print(f"🏛️ Floor Manager: Error inviting agent: {e}")
628
- return jsonify({'error': str(e)}), 500
629
-
630
- # Start server in background thread
631
- import threading
632
- server_thread = threading.Thread(
633
- target=lambda: app.run(host='localhost', port=self.port + 100, debug=False)
634
- )
635
- server_thread.daemon = True
636
- server_thread.start()
637
-
638
- print(f"🏛️ OpenFloor Manager started on port {self.port + 100}")
639
- return True
640
-
641
 
642
  def get_session_id(request: gr.Request = None) -> str:
643
  """Generate or retrieve session ID"""
 
14
  import uuid
15
  from gradio_consilium_roundtable import consilium_roundtable
16
  from research_tools.base_tool import BaseTool
17
+ from openfloor.OpenFloorResearchAgent import OpenFloorResearchAgent
18
+ from openfloor.OpenFloorAgentServer import OpenFloorAgentServer
19
+ from openfloor.OpenFloorManager import OpenFloorManager
20
  from openfloor import *
21
  from openfloor.manifest import *
22
  from openfloor.envelope import *
 
46
  "Wikipedia Research Agent": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Wikipedia-logo-v2.svg/103px-Wikipedia-logo-v2.svg.png"
47
  }
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  def get_session_id(request: gr.Request = None) -> str:
51
  """Generate or retrieve session ID"""