BolyosCsaba commited on
Commit
eac355f
Β·
1 Parent(s): d419fb8

envelop refactor

Browse files
AGENT_MANAGEMENT.md ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent Management - OpenFloor Protocol
2
+
3
+ ## Overview
4
+
5
+ The FloorManager now includes comprehensive agent management capabilities that follow the OpenFloor Protocol (OFP) 1.0.0 specification for discovering and inviting agents to conversations.
6
+
7
+ ## Features
8
+
9
+ ### 1. **Agent Discovery (getManifests)**
10
+ The system can discover agent capabilities by sending `getManifests` events to agent service URLs.
11
+
12
+ **Protocol Reference:** https://openfloor.dev/protocol/specifications/inter-agent-message#h2-117-getmanifests-event
13
+
14
+ **Example Request:**
15
+ ```json
16
+ {
17
+ "openFloor": {
18
+ "schema": {
19
+ "version": "1.0.0"
20
+ },
21
+ "conversation": {
22
+ "id": "manifest_request_abc123"
23
+ },
24
+ "sender": {
25
+ "speakerUri": "tag:floormanager.local,2025:session_xyz",
26
+ "serviceUrl": "http://localhost:7860/ofp"
27
+ },
28
+ "events": [
29
+ {
30
+ "eventType": "getManifests"
31
+ }
32
+ ]
33
+ }
34
+ }
35
+ ```
36
+
37
+ **Expected Response:**
38
+ The agent should respond with a `publishManifests` event containing their manifest information.
39
+
40
+ ### 2. **Agent Invitation (invite)**
41
+ Once discovered, agents can be invited to join specific conversations.
42
+
43
+ **Protocol Reference:** https://openfloor.dev/protocol/specifications/inter-agent-message#h2-113-invite-event
44
+
45
+ **Example Request:**
46
+ ```json
47
+ {
48
+ "openFloor": {
49
+ "schema": {
50
+ "version": "1.0.0"
51
+ },
52
+ "conversation": {
53
+ "id": "conversation_abc123"
54
+ },
55
+ "sender": {
56
+ "speakerUri": "tag:floormanager.local,2025:session_xyz",
57
+ "serviceUrl": "http://localhost:7860/ofp"
58
+ },
59
+ "events": [
60
+ {
61
+ "eventType": "invite",
62
+ "to": {
63
+ "serviceUrl": "https://agent.example.com/ofp",
64
+ "speakerUri": "tag:agent.example.com,2025:1234"
65
+ }
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## Implementation
73
+
74
+ ### AgentManager Class
75
+
76
+ Located in `src/agent_manager.py`, the `AgentManager` class provides the following methods:
77
+
78
+ #### `get_agent_manifest(agent_service_url, timeout=10)`
79
+ Retrieves an agent's manifest by sending a getManifests event.
80
+
81
+ **Parameters:**
82
+ - `agent_service_url` (str): The HTTP endpoint of the agent
83
+ - `timeout` (int): Request timeout in seconds
84
+
85
+ **Returns:**
86
+ - `Dict[str, Any]`: The agent's manifest or None if failed
87
+
88
+ #### `invite_agent(agent_speaker_uri, agent_service_url, conversation_id, timeout=10)`
89
+ Invites an agent to join a conversation.
90
+
91
+ **Parameters:**
92
+ - `agent_speaker_uri` (str): The agent's unique speaker URI
93
+ - `agent_service_url` (str): The agent's service endpoint
94
+ - `conversation_id` (str): The conversation to join
95
+ - `timeout` (int): Request timeout in seconds
96
+
97
+ **Returns:**
98
+ - `bool`: True if invite was sent successfully
99
+
100
+ #### `add_agent(agent_service_url, conversation_id, timeout=10)`
101
+ Convenience method that combines manifest retrieval and invitation.
102
+
103
+ **Parameters:**
104
+ - `agent_service_url` (str): The agent's service endpoint
105
+ - `conversation_id` (str): The conversation to join
106
+ - `timeout` (int): Request timeout in seconds
107
+
108
+ **Returns:**
109
+ - `AgentInfo`: Object containing agent details or None if failed
110
+
111
+ ## Usage Example
112
+
113
+ ```python
114
+ from src.agent_manager import AgentManager
115
+
116
+ # Initialize manager
117
+ agent_manager = AgentManager(
118
+ floor_manager_uri="tag:floormanager.local,2025:session_123",
119
+ floor_manager_url="http://localhost:7860/ofp"
120
+ )
121
+
122
+ # Add an agent (gets manifest and invites)
123
+ agent_info = await agent_manager.add_agent(
124
+ agent_service_url="https://agent.example.com/ofp",
125
+ conversation_id="conversation_abc123"
126
+ )
127
+
128
+ if agent_info:
129
+ print(f"Agent added: {agent_info.speaker_uri}")
130
+ print(f"Manifest: {agent_info.manifest}")
131
+ ```
132
+
133
+ ## UI Integration
134
+
135
+ The test interface (`test_app.py`) includes a new "OFP Agent" tab in the Agent Management section:
136
+
137
+ 1. **Create a Session**: Initialize a new floor session
138
+ 2. **Add OFP Agent**: Enter the agent's service URL
139
+ 3. **System Workflow**:
140
+ - Sends `getManifests` request to discover the agent
141
+ - Extracts the agent's `speakerUri` from the manifest
142
+ - Sends `invite` event to invite the agent to the conversation
143
+ - Adds the agent to the session
144
+
145
+ ## Agent Requirements
146
+
147
+ For an agent to work with this system, it must:
148
+
149
+ 1. **Expose an HTTP endpoint** that accepts POST requests with OFP envelopes
150
+ 2. **Respond to `getManifests` events** with a `publishManifests` event containing:
151
+ - `speakerUri`: Unique identifier for the agent
152
+ - `conversationalName`: Display name (optional)
153
+ - Other manifest fields as needed
154
+ 3. **Accept `invite` events** and join the conversation
155
+
156
+ ## Example Agent
157
+
158
+ See the BadWord Sentinel agent for a reference implementation:
159
+ https://huggingface.co/spaces/BladeSzaSza/OFPBadWord/raw/main/src/ofp_client.py
160
+
161
+ ## Protocol Compliance
162
+
163
+ This implementation follows:
164
+ - **OFP 1.0.0 Specification**
165
+ - **Inter-Agent Message Format**: https://openfloor.dev/protocol/specifications/inter-agent-message
166
+ - **Event Types**: getManifests, publishManifests, invite, acceptInvite
167
+
168
+ ## Error Handling
169
+
170
+ The system handles:
171
+ - **Network timeouts**: Configurable timeout for HTTP requests
172
+ - **Invalid responses**: Gracefully handles malformed responses
173
+ - **Missing manifests**: Returns None if agent doesn't provide manifest
174
+ - **Connection failures**: Logs errors and returns appropriate status
175
+
176
+ ## Future Enhancements
177
+
178
+ Potential improvements:
179
+ - Support for `acceptInvite` and `declineInvite` responses
180
+ - Agent capability negotiation
181
+ - Automatic retry logic for failed invitations
182
+ - Agent health monitoring
183
+ - Support for agent authentication/authorization
ENVELOPE_BROADCASTING.md ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Envelope Broadcasting - OpenFloor Protocol
2
+
3
+ ## Overview
4
+
5
+ The FloorManager implements comprehensive envelope broadcasting to ensure all participants in a conversation stay synchronized. When an agent sends a message to the floor, the floor manager automatically forwards it to all other participants.
6
+
7
+ ## Architecture
8
+
9
+ ### FloorManager Class (`src/floor_manager.py`)
10
+
11
+ The FloorManager is responsible for:
12
+ 1. **Managing floor sessions** - Creating and tracking conversation sessions
13
+ 2. **Tracking participants** - Maintaining a list of all agents in each session
14
+ 3. **Broadcasting envelopes** - Forwarding messages to all participants
15
+ 4. **Floor control** - Managing speaking permissions (grant/revoke floor)
16
+
17
+ ### FloorSession Class
18
+
19
+ Represents an active conversation session with:
20
+ - `session_id`: Unique identifier for the conversation
21
+ - `participants`: Dictionary of AgentInfo objects (speaker_uri β†’ AgentInfo)
22
+ - `floor_holder`: Current speaker's URI
23
+ - `floor_manager_uri`: Floor manager's speaker URI
24
+ - `floor_manager_url`: Floor manager's service endpoint
25
+
26
+ ## How Envelope Broadcasting Works
27
+
28
+ ### 1. **Message Reception**
29
+ When the floor receives an envelope from an agent:
30
+
31
+ ```python
32
+ await floor_manager.handle_incoming_envelope(
33
+ session_id="conversation_123",
34
+ envelope_dict={
35
+ "openFloor": {
36
+ "schema": {"version": "1.0.0"},
37
+ "conversation": {"id": "conversation_123"},
38
+ "sender": {
39
+ "speakerUri": "tag:agent1.com,2025:1234",
40
+ "serviceUrl": "https://agent1.com/ofp"
41
+ },
42
+ "events": [{
43
+ "eventType": "utterance",
44
+ "parameters": {
45
+ "dialogEvent": {
46
+ "speakerUri": "tag:agent1.com,2025:1234",
47
+ "features": {
48
+ "text": {
49
+ "mimeType": "text/plain",
50
+ "tokens": [{"token": "Hello everyone!"}]
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }]
56
+ }
57
+ }
58
+ )
59
+ ```
60
+
61
+ ### 2. **Processing**
62
+ The floor manager:
63
+ 1. **Validates** the envelope
64
+ 2. **Extracts** sender information
65
+ 3. **Processes** floor control events (if any)
66
+ 4. **Broadcasts** to all participants except the sender
67
+
68
+ ### 3. **Broadcasting**
69
+ ```python
70
+ results = await floor_manager.broadcast_envelope(
71
+ session_id="conversation_123",
72
+ envelope_dict=envelope_dict,
73
+ sender_uri="tag:agent1.com,2025:1234" # Exclude sender
74
+ )
75
+ ```
76
+
77
+ ### 4. **Delivery**
78
+ The envelope is POSTed to each participant's service URL:
79
+
80
+ ```http
81
+ POST https://agent2.com/ofp
82
+ Content-Type: application/json
83
+
84
+ {
85
+ "openFloor": {
86
+ "schema": {"version": "1.0.0"},
87
+ "conversation": {"id": "conversation_123"},
88
+ "sender": {
89
+ "speakerUri": "tag:agent1.com,2025:1234",
90
+ "serviceUrl": "https://agent1.com/ofp"
91
+ },
92
+ "events": [...]
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## Key Features
98
+
99
+ ### βœ… Automatic Broadcasting
100
+ - **Every message** sent to the floor is automatically forwarded
101
+ - **All participants** receive updates (except the sender)
102
+ - **No manual intervention** required
103
+
104
+ ### βœ… Participant Tracking
105
+ ```python
106
+ session = floor_manager.get_session(session_id)
107
+ print(f"Participants: {len(session.participants)}")
108
+
109
+ for speaker_uri, agent_info in session.participants.items():
110
+ print(f" - {speaker_uri} at {agent_info.service_url}")
111
+ ```
112
+
113
+ ### βœ… Floor Control Events
114
+ The system automatically processes and broadcasts:
115
+ - `requestFloor` - Agent requests speaking permission
116
+ - `grantFloor` - Floor manager grants permission
117
+ - `revokeFloor` - Floor manager revokes permission
118
+ - `yieldFloor` - Agent voluntarily yields the floor
119
+
120
+ ### βœ… Error Handling
121
+ - **Timeout handling**: Configurable timeouts for each delivery
122
+ - **Failure tracking**: Returns success/failure status for each participant
123
+ - **Logging**: Detailed logs for debugging
124
+ - **Graceful degradation**: Continues broadcasting even if some deliveries fail
125
+
126
+ ## Usage Example
127
+
128
+ ### Creating a Session with Broadcasting
129
+
130
+ ```python
131
+ from src.floor_manager import FloorManager
132
+
133
+ # Initialize floor manager
134
+ floor_manager = FloorManager()
135
+
136
+ # Create a session
137
+ session = floor_manager.create_session(
138
+ session_id="meeting_123",
139
+ floor_manager_uri="tag:floormanager.local,2025:meeting_123",
140
+ floor_manager_url="http://localhost:7860/ofp"
141
+ )
142
+
143
+ # Add participants
144
+ agent_manager = floor_manager.get_agent_manager("meeting_123")
145
+
146
+ # Add agent 1
147
+ agent1_info = await agent_manager.add_agent(
148
+ agent_service_url="https://agent1.com/ofp",
149
+ conversation_id="meeting_123",
150
+ session_participants=session.participants
151
+ )
152
+
153
+ # Add agent 2
154
+ agent2_info = await agent_manager.add_agent(
155
+ agent_service_url="https://agent2.com/ofp",
156
+ conversation_id="meeting_123",
157
+ session_participants=session.participants
158
+ )
159
+
160
+ # Now when agent1 sends a message, agent2 will automatically receive it
161
+ await floor_manager.handle_incoming_envelope(
162
+ session_id="meeting_123",
163
+ envelope_dict=agent1_message
164
+ )
165
+ # β†’ agent2 receives the envelope at https://agent2.com/ofp
166
+ ```
167
+
168
+ ### Sending Floor Notifications
169
+
170
+ ```python
171
+ # Grant floor to agent1
172
+ await floor_manager.send_floor_notification(
173
+ session_id="meeting_123",
174
+ event_type="grantFloor",
175
+ target_uri="tag:agent1.com,2025:1234"
176
+ )
177
+ # β†’ All participants receive grantFloor notification
178
+ ```
179
+
180
+ ## Message Flow Diagram
181
+
182
+ ```
183
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
184
+ β”‚ Agent 1 β”‚
185
+ β”‚ (Sender) β”‚
186
+ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
187
+ β”‚ 1. Send utterance
188
+ β”‚ "Hello everyone!"
189
+ β–Ό
190
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
191
+ β”‚ Floor Manager β”‚
192
+ β”‚ β”‚
193
+ β”‚ β€’ Receives envelope β”‚
194
+ β”‚ β€’ Validates sender β”‚
195
+ β”‚ β€’ Processes events β”‚
196
+ β”‚ β€’ Broadcasts β”‚
197
+ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
198
+ β”‚ β”‚
199
+ β”‚ β”‚ 2. Forward to participants
200
+ β”‚ β”‚ (excluding sender)
201
+ β–Ό β–Ό
202
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
203
+ β”‚ Agent 2 β”‚ β”‚ Agent 3 β”‚
204
+ β”‚(Receives)β”‚ β”‚(Receives)β”‚
205
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
206
+ ```
207
+
208
+ ## Protocol Compliance
209
+
210
+ This implementation ensures:
211
+
212
+ ### βœ… Real-time Synchronization
213
+ All participants receive messages as they're sent to the floor
214
+
215
+ ### βœ… Sender Exclusion
216
+ The original sender doesn't receive their own message back (avoiding loops)
217
+
218
+ ### βœ… Event Processing
219
+ Floor control events are processed before broadcasting
220
+
221
+ ### βœ… OFP Specification Compliance
222
+ Follows the OpenFloor Protocol for envelope structure and delivery
223
+
224
+ ## Configuration
225
+
226
+ ### Timeout Settings
227
+ ```python
228
+ # Default timeout: 10 seconds
229
+ results = await floor_manager.broadcast_envelope(
230
+ session_id="meeting_123",
231
+ envelope_dict=envelope,
232
+ timeout=15 # Custom timeout
233
+ )
234
+ ```
235
+
236
+ ### Broadcast Results
237
+ ```python
238
+ results = await floor_manager.broadcast_envelope(...)
239
+ # Returns: {'tag:agent1.com,2025:1': True, 'tag:agent2.com,2025:2': False}
240
+
241
+ successful = sum(1 for success in results.values() if success)
242
+ failed = sum(1 for success in results.values() if not success)
243
+ ```
244
+
245
+ ## Benefits
246
+
247
+ ### πŸ”„ **Automatic Synchronization**
248
+ All participants stay up-to-date without manual message routing
249
+
250
+ ### πŸ“‘ **Reliable Delivery**
251
+ Robust error handling ensures messages reach as many participants as possible
252
+
253
+ ### 🎯 **Centralized Control**
254
+ Floor manager acts as the single source of truth for the conversation
255
+
256
+ ### πŸ” **Transparency**
257
+ Detailed logging and result tracking for debugging and monitoring
258
+
259
+ ### ⚑ **Performance**
260
+ Parallel delivery to multiple participants (can be enhanced with async HTTP)
261
+
262
+ ## Future Enhancements
263
+
264
+ Potential improvements:
265
+ - **Parallel HTTP requests** using `aiohttp` for faster broadcasting
266
+ - **Retry logic** for failed deliveries
267
+ - **Message queuing** for offline participants
268
+ - **Delivery confirmations** (acknowledgment receipts)
269
+ - **Priority messaging** for floor control events
270
+ - **Rate limiting** to prevent spam
271
+ - **Message filtering** based on participant preferences
272
+
273
+ ## Testing
274
+
275
+ To test envelope broadcasting:
276
+
277
+ 1. **Start the floor manager**
278
+ 2. **Add multiple OFP agents** to a session
279
+ 3. **Send a message** from one agent
280
+ 4. **Verify** all other agents receive the envelope
281
+ 5. **Check logs** for delivery status
282
+
283
+ Example test scenario:
284
+ ```python
285
+ # Add 3 agents to a session
286
+ # Have agent 1 send "Hello"
287
+ # Verify agents 2 and 3 receive the message
288
+ # Verify agent 1 does NOT receive it back
QUICKSTART.md CHANGED
@@ -1,16 +1,22 @@
1
  ## πŸš€ Quick Start Guide - Test the Floor Manager Interface
2
 
3
- ### Step 1: Install Dependencies
4
 
5
  ```bash
6
- # Install only Gradio for testing (no need for full stack yet)
7
- pip install gradio
 
 
 
8
  ```
9
 
10
- ### Step 2: Run the Test Interface
11
 
12
  ```bash
13
- # Run the test app
 
 
 
14
  python test_app.py
15
  ```
16
 
 
1
  ## πŸš€ Quick Start Guide - Test the Floor Manager Interface
2
 
3
+ ### Option 1: Quick Install & Run (Recommended)
4
 
5
  ```bash
6
+ # Make the script executable (first time only)
7
+ chmod +x install_and_run.sh
8
+
9
+ # Install dependencies and run
10
+ ./install_and_run.sh
11
  ```
12
 
13
+ ### Option 2: Manual Installation
14
 
15
  ```bash
16
+ # Step 1: Install all dependencies
17
+ pip install -r requirements.txt
18
+
19
+ # Step 2: Run the test app
20
  python test_app.py
21
  ```
22
 
install_and_run.sh CHANGED
@@ -14,9 +14,9 @@ fi
14
  echo "βœ… Python 3 found: $(python3 --version)"
15
  echo ""
16
 
17
- # Install Gradio
18
- echo "πŸ“¦ Installing Gradio..."
19
- pip install gradio
20
 
21
  echo ""
22
  echo "======================================"
 
14
  echo "βœ… Python 3 found: $(python3 --version)"
15
  echo ""
16
 
17
+ # Install all dependencies
18
+ echo "πŸ“¦ Installing dependencies from requirements.txt..."
19
+ pip install -r requirements.txt
20
 
21
  echo ""
22
  echo "======================================"
requirements.txt CHANGED
@@ -8,6 +8,7 @@ pydantic-settings
8
  # HTTP client for agent communication
9
  httpx
10
  aiohttp
 
11
 
12
  # Utilities
13
  python-multipart
 
8
  # HTTP client for agent communication
9
  httpx
10
  aiohttp
11
+ requests
12
 
13
  # Utilities
14
  python-multipart
src/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (227 Bytes). View file
 
src/__pycache__/agent_manager.cpython-313.pyc ADDED
Binary file (11.2 kB). View file
 
src/__pycache__/floor_manager.cpython-313.pyc ADDED
Binary file (13.1 kB). View file
 
src/agent_manager.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Manager for OpenFloor Protocol
3
+ Handles agent discovery, invitation, and manifest management
4
+ """
5
+
6
+ import requests
7
+ import logging
8
+ from typing import Dict, Optional, Any
9
+ from dataclasses import dataclass
10
+ from datetime import datetime
11
+
12
+ from .protocol.envelope import create_envelope, create_inter_agent_message
13
+ from .protocol.events import create_invite_event, create_get_manifest_event
14
+ from .utils.helpers import generate_message_id
15
+ from .utils.config import settings
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class AgentInfo:
22
+ """Information about an agent"""
23
+ speaker_uri: str
24
+ service_url: str
25
+ manifest: Optional[Dict[str, Any]] = None
26
+ conversation_id: Optional[str] = None
27
+ invited_at: Optional[datetime] = None
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ return {
31
+ "speaker_uri": self.speaker_uri,
32
+ "service_url": self.service_url,
33
+ "manifest": self.manifest,
34
+ "conversation_id": self.conversation_id,
35
+ "invited_at": self.invited_at.isoformat() if self.invited_at else None
36
+ }
37
+
38
+
39
+ class AgentManager:
40
+ """
41
+ Manages agent discovery and invitation for OpenFloor Protocol
42
+
43
+ Based on OFP 1.0.0 specification:
44
+ - getManifests event: Discover agent capabilities
45
+ - invite event: Invite agents to join conversations
46
+ """
47
+
48
+ def __init__(self, floor_manager_uri: str, floor_manager_url: str):
49
+ """
50
+ Initialize Agent Manager
51
+
52
+ Args:
53
+ floor_manager_uri: Speaker URI of the floor manager
54
+ floor_manager_url: Service URL of the floor manager
55
+ """
56
+ self.floor_manager_uri = floor_manager_uri
57
+ self.floor_manager_url = floor_manager_url
58
+ self.agents: Dict[str, AgentInfo] = {}
59
+ logger.info(f"Agent Manager initialized: {floor_manager_uri}")
60
+
61
+ async def get_agent_manifest(
62
+ self,
63
+ agent_service_url: str,
64
+ timeout: int = 10
65
+ ) -> Optional[Dict[str, Any]]:
66
+ """
67
+ Get an agent's manifest by sending getManifests event
68
+
69
+ Per OFP spec: https://openfloor.dev/protocol/specifications/inter-agent-message#h2-117-getmanifests-event
70
+
71
+ Args:
72
+ agent_service_url: URL of the agent's service
73
+ timeout: Request timeout in seconds
74
+
75
+ Returns:
76
+ Agent manifest dictionary or None if failed
77
+ """
78
+ try:
79
+ logger.info(f"Requesting manifest from agent: {agent_service_url}")
80
+
81
+ # Create getManifests event envelope
82
+ conversation_id = f"manifest_request_{generate_message_id()}"
83
+
84
+ # Create OFP envelope with getManifests event
85
+ envelope_dict = {
86
+ "openFloor": {
87
+ "schema": {
88
+ "version": settings.OFP_VERSION
89
+ },
90
+ "conversation": {
91
+ "id": conversation_id
92
+ },
93
+ "sender": {
94
+ "speakerUri": self.floor_manager_uri,
95
+ "serviceUrl": self.floor_manager_url
96
+ },
97
+ "events": [
98
+ {
99
+ "eventType": "getManifests"
100
+ }
101
+ ]
102
+ }
103
+ }
104
+
105
+ # Send HTTPS POST request to agent
106
+ response = requests.post(
107
+ agent_service_url,
108
+ json=envelope_dict,
109
+ headers={
110
+ 'Content-Type': 'application/json',
111
+ 'User-Agent': f'OFP-FloorManager/{settings.OFP_VERSION}'
112
+ },
113
+ timeout=timeout
114
+ )
115
+
116
+ response.raise_for_status()
117
+
118
+ # Parse response - should contain publishManifests event
119
+ response_data = response.json()
120
+
121
+ if "openFloor" in response_data:
122
+ events = response_data["openFloor"].get("events", [])
123
+ for event in events:
124
+ if event.get("eventType") == "publishManifests":
125
+ manifest = event.get("parameters", {}).get("manifest")
126
+ if manifest:
127
+ logger.info(f"βœ“ Received manifest from {agent_service_url}")
128
+ return manifest
129
+
130
+ logger.warning(f"No manifest found in response from {agent_service_url}")
131
+ return None
132
+
133
+ except requests.exceptions.Timeout:
134
+ logger.error(f"βœ— Timeout getting manifest from {agent_service_url}")
135
+ return None
136
+
137
+ except requests.exceptions.RequestException as e:
138
+ logger.error(f"βœ— Failed to get manifest from {agent_service_url}: {e}")
139
+ return None
140
+
141
+ except Exception as e:
142
+ logger.error(f"βœ— Unexpected error getting manifest: {e}")
143
+ return None
144
+
145
+ async def invite_agent(
146
+ self,
147
+ agent_speaker_uri: str,
148
+ agent_service_url: str,
149
+ conversation_id: str,
150
+ timeout: int = 10
151
+ ) -> bool:
152
+ """
153
+ Invite an agent to join a conversation
154
+
155
+ Per OFP spec: https://openfloor.dev/protocol/specifications/inter-agent-message#h2-113-invite-event
156
+
157
+ Args:
158
+ agent_speaker_uri: Speaker URI of the agent to invite
159
+ agent_service_url: Service URL of the agent
160
+ conversation_id: Conversation ID to invite agent to
161
+ timeout: Request timeout in seconds
162
+
163
+ Returns:
164
+ True if invite was sent successfully, False otherwise
165
+ """
166
+ try:
167
+ logger.info(f"Inviting agent {agent_speaker_uri} to conversation {conversation_id}")
168
+
169
+ # Create OFP envelope with invite event
170
+ envelope_dict = {
171
+ "openFloor": {
172
+ "schema": {
173
+ "version": settings.OFP_VERSION
174
+ },
175
+ "conversation": {
176
+ "id": conversation_id
177
+ },
178
+ "sender": {
179
+ "speakerUri": self.floor_manager_uri,
180
+ "serviceUrl": self.floor_manager_url
181
+ },
182
+ "events": [
183
+ {
184
+ "eventType": "invite",
185
+ "to": {
186
+ "serviceUrl": agent_service_url,
187
+ "speakerUri": agent_speaker_uri
188
+ }
189
+ }
190
+ ]
191
+ }
192
+ }
193
+
194
+ # Send HTTPS POST request to agent
195
+ response = requests.post(
196
+ agent_service_url,
197
+ json=envelope_dict,
198
+ headers={
199
+ 'Content-Type': 'application/json',
200
+ 'User-Agent': f'OFP-FloorManager/{settings.OFP_VERSION}'
201
+ },
202
+ timeout=timeout
203
+ )
204
+
205
+ response.raise_for_status()
206
+
207
+ # Store agent info
208
+ agent_info = AgentInfo(
209
+ speaker_uri=agent_speaker_uri,
210
+ service_url=agent_service_url,
211
+ conversation_id=conversation_id,
212
+ invited_at=datetime.now()
213
+ )
214
+ self.agents[agent_speaker_uri] = agent_info
215
+
216
+ logger.info(f"βœ“ Successfully invited agent {agent_speaker_uri}")
217
+ return True
218
+
219
+ except requests.exceptions.Timeout:
220
+ logger.error(f"βœ— Timeout inviting agent {agent_speaker_uri}")
221
+ return False
222
+
223
+ except requests.exceptions.RequestException as e:
224
+ logger.error(f"βœ— Failed to invite agent {agent_speaker_uri}: {e}")
225
+ return False
226
+
227
+ except Exception as e:
228
+ logger.error(f"βœ— Unexpected error inviting agent: {e}")
229
+ return False
230
+
231
+ async def add_agent(
232
+ self,
233
+ agent_service_url: str,
234
+ conversation_id: str,
235
+ timeout: int = 10,
236
+ session_participants: Optional[Dict[str, AgentInfo]] = None
237
+ ) -> Optional[AgentInfo]:
238
+ """
239
+ Add an agent by getting their manifest and inviting them to a conversation
240
+
241
+ This is a convenience method that combines:
242
+ 1. Getting the agent's manifest (to discover their speaker URI)
243
+ 2. Inviting the agent to the conversation
244
+ 3. Optionally adding to session participants
245
+
246
+ Args:
247
+ agent_service_url: Service URL of the agent
248
+ conversation_id: Conversation ID to invite agent to
249
+ timeout: Request timeout in seconds
250
+ session_participants: Optional dict to add the agent to
251
+
252
+ Returns:
253
+ AgentInfo object if successful, None otherwise
254
+ """
255
+ try:
256
+ # Step 1: Get agent's manifest
257
+ manifest = await self.get_agent_manifest(agent_service_url, timeout)
258
+ if not manifest:
259
+ logger.error(f"Failed to get manifest for agent: {agent_service_url}")
260
+ return None
261
+
262
+ # Extract speaker URI from manifest
263
+ agent_speaker_uri = manifest.get("speakerUri")
264
+ if not agent_speaker_uri:
265
+ logger.error(f"Manifest missing speakerUri: {agent_service_url}")
266
+ return None
267
+
268
+ # Step 2: Invite the agent
269
+ success = await self.invite_agent(
270
+ agent_speaker_uri,
271
+ agent_service_url,
272
+ conversation_id,
273
+ timeout
274
+ )
275
+
276
+ if not success:
277
+ logger.error(f"Failed to invite agent: {agent_speaker_uri}")
278
+ return None
279
+
280
+ # Update agent info with manifest
281
+ agent_info = self.agents[agent_speaker_uri]
282
+ agent_info.manifest = manifest
283
+
284
+ # Add to session participants if provided
285
+ if session_participants is not None:
286
+ session_participants[agent_speaker_uri] = agent_info
287
+ logger.info(f"Added {agent_speaker_uri} to session participants")
288
+
289
+ logger.info(f"βœ“ Successfully added agent {agent_speaker_uri} to conversation {conversation_id}")
290
+ return agent_info
291
+
292
+ except Exception as e:
293
+ logger.error(f"βœ— Error adding agent: {e}")
294
+ return None
295
+
296
+ def get_agent_info(self, agent_speaker_uri: str) -> Optional[AgentInfo]:
297
+ """Get information about an agent"""
298
+ return self.agents.get(agent_speaker_uri)
299
+
300
+ def list_agents(self) -> Dict[str, AgentInfo]:
301
+ """List all known agents"""
302
+ return self.agents.copy()
303
+
304
+ def remove_agent(self, agent_speaker_uri: str) -> bool:
305
+ """Remove an agent from the manager"""
306
+ if agent_speaker_uri in self.agents:
307
+ del self.agents[agent_speaker_uri]
308
+ logger.info(f"Removed agent: {agent_speaker_uri}")
309
+ return True
310
+ return False
src/floor_manager.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Floor Manager - Handles conversation sessions and envelope broadcasting
3
+ """
4
+
5
+ import requests
6
+ import logging
7
+ from typing import Dict, Optional, List, Any
8
+ from datetime import datetime
9
+ from dataclasses import dataclass
10
+
11
+ from .agent_manager import AgentManager, AgentInfo
12
+ from .utils.helpers import generate_message_id
13
+ from .utils.config import settings
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class FloorSession:
20
+ """Represents an active floor conversation session"""
21
+ session_id: str
22
+ floor_manager_uri: str
23
+ floor_manager_url: str
24
+ participants: Dict[str, AgentInfo] # speaker_uri -> AgentInfo
25
+ created_at: datetime
26
+ floor_holder: Optional[str] = None # speaker_uri of current floor holder
27
+
28
+ def add_participant(self, agent_info: AgentInfo):
29
+ """Add a participant to the session"""
30
+ self.participants[agent_info.speaker_uri] = agent_info
31
+
32
+ def remove_participant(self, speaker_uri: str):
33
+ """Remove a participant from the session"""
34
+ if speaker_uri in self.participants:
35
+ del self.participants[speaker_uri]
36
+ if self.floor_holder == speaker_uri:
37
+ self.floor_holder = None
38
+
39
+ def get_participant_urls(self, exclude_speaker: Optional[str] = None) -> List[str]:
40
+ """
41
+ Get list of all participant service URLs
42
+
43
+ Args:
44
+ exclude_speaker: Optional speaker URI to exclude (e.g., the sender)
45
+
46
+ Returns:
47
+ List of service URLs
48
+ """
49
+ urls = []
50
+ for speaker_uri, agent_info in self.participants.items():
51
+ if exclude_speaker and speaker_uri == exclude_speaker:
52
+ continue
53
+ if agent_info.service_url:
54
+ urls.append(agent_info.service_url)
55
+ return urls
56
+
57
+
58
+ class FloorManager:
59
+ """
60
+ Floor Manager for OpenFloor Protocol
61
+
62
+ Responsibilities:
63
+ - Manage floor sessions
64
+ - Handle agent discovery and invitation
65
+ - Broadcast envelopes to all participants
66
+ - Manage floor control (grant/revoke)
67
+ """
68
+
69
+ def __init__(self):
70
+ """Initialize Floor Manager"""
71
+ self.sessions: Dict[str, FloorSession] = {}
72
+ logger.info("Floor Manager initialized")
73
+
74
+ def create_session(
75
+ self,
76
+ session_id: str,
77
+ floor_manager_uri: str,
78
+ floor_manager_url: str
79
+ ) -> FloorSession:
80
+ """
81
+ Create a new floor session
82
+
83
+ Args:
84
+ session_id: Unique session identifier
85
+ floor_manager_uri: Speaker URI of the floor manager
86
+ floor_manager_url: Service URL of the floor manager
87
+
88
+ Returns:
89
+ FloorSession object
90
+ """
91
+ session = FloorSession(
92
+ session_id=session_id,
93
+ floor_manager_uri=floor_manager_uri,
94
+ floor_manager_url=floor_manager_url,
95
+ participants={},
96
+ created_at=datetime.now()
97
+ )
98
+ self.sessions[session_id] = session
99
+ logger.info(f"Created floor session: {session_id}")
100
+ return session
101
+
102
+ def get_session(self, session_id: str) -> Optional[FloorSession]:
103
+ """Get a session by ID"""
104
+ return self.sessions.get(session_id)
105
+
106
+ def get_agent_manager(self, session_id: str) -> Optional[AgentManager]:
107
+ """
108
+ Get an AgentManager for a specific session
109
+
110
+ Args:
111
+ session_id: Session identifier
112
+
113
+ Returns:
114
+ AgentManager instance or None
115
+ """
116
+ session = self.get_session(session_id)
117
+ if not session:
118
+ return None
119
+
120
+ return AgentManager(
121
+ floor_manager_uri=session.floor_manager_uri,
122
+ floor_manager_url=session.floor_manager_url
123
+ )
124
+
125
+ async def broadcast_envelope(
126
+ self,
127
+ session_id: str,
128
+ envelope_dict: Dict[str, Any],
129
+ sender_uri: Optional[str] = None,
130
+ timeout: int = 10
131
+ ) -> Dict[str, bool]:
132
+ """
133
+ Broadcast an envelope to all participants in a session
134
+
135
+ Per OFP specification, when an agent sends a message to the floor,
136
+ the floor manager should forward it to all other participants.
137
+
138
+ Args:
139
+ session_id: Session identifier
140
+ envelope_dict: OFP envelope dictionary to broadcast
141
+ sender_uri: Speaker URI of the sender (to exclude from broadcast)
142
+ timeout: Request timeout in seconds
143
+
144
+ Returns:
145
+ Dict mapping participant URIs to success status
146
+ """
147
+ session = self.get_session(session_id)
148
+ if not session:
149
+ logger.error(f"Session not found: {session_id}")
150
+ return {}
151
+
152
+ results = {}
153
+ participant_urls = session.get_participant_urls(exclude_speaker=sender_uri)
154
+
155
+ if not participant_urls:
156
+ logger.info(f"No participants to broadcast to in session {session_id}")
157
+ return results
158
+
159
+ logger.info(f"Broadcasting envelope to {len(participant_urls)} participants")
160
+
161
+ for speaker_uri, agent_info in session.participants.items():
162
+ # Skip the sender
163
+ if sender_uri and speaker_uri == sender_uri:
164
+ continue
165
+
166
+ if not agent_info.service_url:
167
+ logger.warning(f"No service URL for participant: {speaker_uri}")
168
+ results[speaker_uri] = False
169
+ continue
170
+
171
+ try:
172
+ logger.debug(f"Sending envelope to {speaker_uri} at {agent_info.service_url}")
173
+
174
+ response = requests.post(
175
+ agent_info.service_url,
176
+ json=envelope_dict,
177
+ headers={
178
+ 'Content-Type': 'application/json',
179
+ 'User-Agent': f'OFP-FloorManager/{settings.OFP_VERSION}'
180
+ },
181
+ timeout=timeout
182
+ )
183
+
184
+ response.raise_for_status()
185
+ results[speaker_uri] = True
186
+ logger.info(f"βœ“ Envelope delivered to {speaker_uri}")
187
+
188
+ except requests.exceptions.Timeout:
189
+ logger.error(f"βœ— Timeout sending to {speaker_uri}")
190
+ results[speaker_uri] = False
191
+
192
+ except requests.exceptions.RequestException as e:
193
+ logger.error(f"βœ— Failed to send to {speaker_uri}: {e}")
194
+ results[speaker_uri] = False
195
+
196
+ except Exception as e:
197
+ logger.error(f"βœ— Unexpected error sending to {speaker_uri}: {e}")
198
+ results[speaker_uri] = False
199
+
200
+ successful = sum(1 for success in results.values() if success)
201
+ logger.info(f"Broadcast complete: {successful}/{len(results)} successful")
202
+
203
+ return results
204
+
205
+ async def handle_incoming_envelope(
206
+ self,
207
+ session_id: str,
208
+ envelope_dict: Dict[str, Any]
209
+ ) -> bool:
210
+ """
211
+ Handle an incoming envelope and broadcast to participants
212
+
213
+ This is the main entry point for envelopes received by the floor.
214
+ The floor manager will:
215
+ 1. Validate the envelope
216
+ 2. Process any floor control events
217
+ 3. Broadcast to all other participants
218
+
219
+ Args:
220
+ session_id: Session identifier
221
+ envelope_dict: OFP envelope dictionary
222
+
223
+ Returns:
224
+ True if handled successfully
225
+ """
226
+ try:
227
+ # Extract sender information
228
+ sender_info = envelope_dict.get("openFloor", {}).get("sender", {})
229
+ sender_uri = sender_info.get("speakerUri")
230
+
231
+ if not sender_uri:
232
+ logger.error("Envelope missing sender speakerUri")
233
+ return False
234
+
235
+ logger.info(f"Received envelope from {sender_uri} for session {session_id}")
236
+
237
+ # Process floor control events (grant, revoke, request, etc.)
238
+ await self._process_floor_events(session_id, envelope_dict)
239
+
240
+ # Broadcast to all participants except sender
241
+ await self.broadcast_envelope(
242
+ session_id=session_id,
243
+ envelope_dict=envelope_dict,
244
+ sender_uri=sender_uri
245
+ )
246
+
247
+ return True
248
+
249
+ except Exception as e:
250
+ logger.error(f"Error handling envelope: {e}")
251
+ return False
252
+
253
+ async def _process_floor_events(
254
+ self,
255
+ session_id: str,
256
+ envelope_dict: Dict[str, Any]
257
+ ):
258
+ """
259
+ Process floor control events (grantFloor, revokeFloor, requestFloor, etc.)
260
+
261
+ Args:
262
+ session_id: Session identifier
263
+ envelope_dict: OFP envelope dictionary
264
+ """
265
+ session = self.get_session(session_id)
266
+ if not session:
267
+ return
268
+
269
+ events = envelope_dict.get("openFloor", {}).get("events", [])
270
+ sender_uri = envelope_dict.get("openFloor", {}).get("sender", {}).get("speakerUri")
271
+
272
+ for event in events:
273
+ event_type = event.get("eventType")
274
+
275
+ if event_type == "requestFloor":
276
+ logger.info(f"Floor requested by {sender_uri}")
277
+ # In a full implementation, this would trigger convener logic
278
+
279
+ elif event_type == "grantFloor":
280
+ to_uri = event.get("to", {}).get("speakerUri")
281
+ if to_uri:
282
+ session.floor_holder = to_uri
283
+ logger.info(f"Floor granted to {to_uri}")
284
+
285
+ elif event_type == "revokeFloor":
286
+ logger.info(f"Floor revoked from {session.floor_holder}")
287
+ session.floor_holder = None
288
+
289
+ elif event_type == "yieldFloor":
290
+ if sender_uri == session.floor_holder:
291
+ session.floor_holder = None
292
+ logger.info(f"Floor yielded by {sender_uri}")
293
+
294
+ async def send_floor_notification(
295
+ self,
296
+ session_id: str,
297
+ event_type: str,
298
+ target_uri: Optional[str] = None,
299
+ reason: Optional[str] = None
300
+ ) -> bool:
301
+ """
302
+ Send a floor control notification to all participants
303
+
304
+ Args:
305
+ session_id: Session identifier
306
+ event_type: Type of floor event (grantFloor, revokeFloor, etc.)
307
+ target_uri: Optional target speaker URI
308
+ reason: Optional reason for the event
309
+
310
+ Returns:
311
+ True if sent successfully
312
+ """
313
+ session = self.get_session(session_id)
314
+ if not session:
315
+ return False
316
+
317
+ # Create floor notification envelope
318
+ event = {
319
+ "eventType": event_type
320
+ }
321
+
322
+ if target_uri:
323
+ event["to"] = {
324
+ "speakerUri": target_uri
325
+ }
326
+
327
+ if reason:
328
+ event["reason"] = reason
329
+
330
+ envelope_dict = {
331
+ "openFloor": {
332
+ "schema": {
333
+ "version": settings.OFP_VERSION
334
+ },
335
+ "conversation": {
336
+ "id": session_id
337
+ },
338
+ "sender": {
339
+ "speakerUri": session.floor_manager_uri,
340
+ "serviceUrl": session.floor_manager_url
341
+ },
342
+ "events": [event]
343
+ }
344
+ }
345
+
346
+ # Broadcast to all participants
347
+ results = await self.broadcast_envelope(
348
+ session_id=session_id,
349
+ envelope_dict=envelope_dict
350
+ )
351
+
352
+ return any(results.values())
src/protocol/__init__.py CHANGED
@@ -9,7 +9,6 @@ from .events import (
9
  create_invite_event,
10
  create_floor_request_event,
11
  )
12
- from .handler import ProtocolHandler
13
 
14
  __all__ = [
15
  "Envelope",
@@ -21,5 +20,4 @@ __all__ = [
21
  "create_revoke_floor_event",
22
  "create_invite_event",
23
  "create_floor_request_event",
24
- "ProtocolHandler",
25
  ]
 
9
  create_invite_event,
10
  create_floor_request_event,
11
  )
 
12
 
13
  __all__ = [
14
  "Envelope",
 
20
  "create_revoke_floor_event",
21
  "create_invite_event",
22
  "create_floor_request_event",
 
23
  ]
src/protocol/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (679 Bytes). View file
 
src/protocol/__pycache__/envelope.cpython-313.pyc ADDED
Binary file (3.85 kB). View file
 
src/protocol/__pycache__/events.cpython-313.pyc ADDED
Binary file (4.71 kB). View file
 
src/utils/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (311 Bytes). View file
 
src/utils/__pycache__/config.cpython-313.pyc ADDED
Binary file (1.36 kB). View file
 
src/utils/__pycache__/helpers.cpython-313.pyc ADDED
Binary file (2.51 kB). View file
 
src/utils/__pycache__/logger.cpython-313.pyc ADDED
Binary file (1.51 kB). View file
 
test_app.py CHANGED
@@ -7,6 +7,9 @@ import gradio as gr
7
  from datetime import datetime
8
  from typing import List, Tuple, Optional
9
  import uuid
 
 
 
10
 
11
 
12
  # Simple mock classes for testing
@@ -114,19 +117,28 @@ class MockFloorSession:
114
  # Global session (in production, this would be managed differently)
115
  current_session: Optional[MockFloorSession] = None
116
 
 
 
 
117
 
118
  def create_new_session():
119
  """Create a new floor session"""
120
- global current_session
121
  session_id = f"session_{uuid.uuid4().hex[:8]}"
122
  current_session = MockFloorSession(session_id)
123
 
 
 
 
 
 
124
  return (
125
  session_id,
126
  current_session.format_chat_history(),
127
  current_session.get_floor_status(),
128
  current_session.get_agent_list(),
129
- gr.update(interactive=True)
 
130
  )
131
 
132
 
@@ -151,6 +163,45 @@ def add_agent(agent_name: str):
151
  )
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  def send_message(agent_name: str, message: str):
155
  """Send a message from an agent"""
156
  if not current_session:
@@ -238,12 +289,22 @@ with gr.Blocks(title="OFP Floor Manager Test") as demo:
238
  gr.Markdown("---")
239
  gr.Markdown("### Agent Management")
240
 
241
- agent_name_input = gr.Textbox(
242
- label="Agent Name",
243
- placeholder="Enter agent name..."
244
- )
 
 
 
 
 
 
 
 
 
 
 
245
 
246
- add_agent_btn = gr.Button("βž• Add Agent")
247
  add_agent_status = gr.Textbox(
248
  label="Status",
249
  interactive=False
@@ -309,7 +370,7 @@ with gr.Blocks(title="OFP Floor Manager Test") as demo:
309
  # Event Handlers
310
  create_btn.click(
311
  fn=create_new_session,
312
- outputs=[session_id, chatbot, floor_status, agent_table, send_btn]
313
  )
314
 
315
  add_agent_btn.click(
@@ -321,6 +382,15 @@ with gr.Blocks(title="OFP Floor Manager Test") as demo:
321
  outputs=[message_agent]
322
  )
323
 
 
 
 
 
 
 
 
 
 
324
  send_btn.click(
325
  fn=send_message,
326
  inputs=[message_agent, message_input],
 
7
  from datetime import datetime
8
  from typing import List, Tuple, Optional
9
  import uuid
10
+ import asyncio
11
+ from src.floor_manager import FloorManager, FloorSession
12
+ from src.agent_manager import AgentManager, AgentInfo
13
 
14
 
15
  # Simple mock classes for testing
 
117
  # Global session (in production, this would be managed differently)
118
  current_session: Optional[MockFloorSession] = None
119
 
120
+ # Agent Manager for OFP agent interactions
121
+ agent_manager: Optional[AgentManager] = None
122
+
123
 
124
  def create_new_session():
125
  """Create a new floor session"""
126
+ global current_session, agent_manager
127
  session_id = f"session_{uuid.uuid4().hex[:8]}"
128
  current_session = MockFloorSession(session_id)
129
 
130
+ # Initialize agent manager
131
+ floor_manager_uri = f"tag:floormanager.local,2025:{session_id}"
132
+ floor_manager_url = "http://localhost:7860/ofp"
133
+ agent_manager = AgentManager(floor_manager_uri, floor_manager_url)
134
+
135
  return (
136
  session_id,
137
  current_session.format_chat_history(),
138
  current_session.get_floor_status(),
139
  current_session.get_agent_list(),
140
+ gr.update(interactive=True),
141
+ gr.update(interactive=True) # Enable OFP agent URL input
142
  )
143
 
144
 
 
163
  )
164
 
165
 
166
+ def add_ofp_agent(agent_url: str):
167
+ """Add an OFP agent by URL"""
168
+ if not current_session or not agent_manager:
169
+ return "❌ Create a session first!", agent_url, current_session.format_chat_history() if current_session else [], current_session.get_floor_status() if current_session else "No session", current_session.get_agent_list() if current_session else [], gr.Dropdown(choices=[])
170
+
171
+ if not agent_url.strip():
172
+ return "❌ Agent URL cannot be empty!", agent_url, current_session.format_chat_history(), current_session.get_floor_status(), current_session.get_agent_list(), gr.Dropdown(choices=[])
173
+
174
+ try:
175
+ # Run async function in event loop
176
+ loop = asyncio.new_event_loop()
177
+ asyncio.set_event_loop(loop)
178
+ agent_info = loop.run_until_complete(
179
+ agent_manager.add_agent(agent_url.strip(), current_session.session_id)
180
+ )
181
+ loop.close()
182
+
183
+ if not agent_info:
184
+ return f"❌ Failed to add OFP agent from {agent_url}", agent_url, current_session.format_chat_history(), current_session.get_floor_status(), current_session.get_agent_list(), gr.Dropdown(choices=[])
185
+
186
+ # Add agent to mock session
187
+ agent_name = agent_info.manifest.get("conversationalName", agent_info.speaker_uri.split(":")[-1])
188
+ current_session.add_agent(agent_name)
189
+
190
+ agent_choices = get_agent_dropdown_choices()
191
+
192
+ return (
193
+ f"βœ… Added OFP agent: {agent_name} ({agent_info.speaker_uri})",
194
+ "",
195
+ current_session.format_chat_history(),
196
+ current_session.get_floor_status(),
197
+ current_session.get_agent_list(),
198
+ gr.Dropdown(choices=agent_choices)
199
+ )
200
+
201
+ except Exception as e:
202
+ return f"❌ Error adding OFP agent: {str(e)}", agent_url, current_session.format_chat_history(), current_session.get_floor_status(), current_session.get_agent_list(), gr.Dropdown(choices=[])
203
+
204
+
205
  def send_message(agent_name: str, message: str):
206
  """Send a message from an agent"""
207
  if not current_session:
 
289
  gr.Markdown("---")
290
  gr.Markdown("### Agent Management")
291
 
292
+ with gr.Tabs():
293
+ with gr.Tab("Manual Agent"):
294
+ agent_name_input = gr.Textbox(
295
+ label="Agent Name",
296
+ placeholder="Enter agent name..."
297
+ )
298
+ add_agent_btn = gr.Button("βž• Add Manual Agent")
299
+
300
+ with gr.Tab("OFP Agent"):
301
+ ofp_agent_url = gr.Textbox(
302
+ label="Agent Service URL",
303
+ placeholder="https://agent.example.com/ofp",
304
+ interactive=False
305
+ )
306
+ add_ofp_agent_btn = gr.Button("🌐 Add OFP Agent")
307
 
 
308
  add_agent_status = gr.Textbox(
309
  label="Status",
310
  interactive=False
 
370
  # Event Handlers
371
  create_btn.click(
372
  fn=create_new_session,
373
+ outputs=[session_id, chatbot, floor_status, agent_table, send_btn, ofp_agent_url]
374
  )
375
 
376
  add_agent_btn.click(
 
382
  outputs=[message_agent]
383
  )
384
 
385
+ add_ofp_agent_btn.click(
386
+ fn=add_ofp_agent,
387
+ inputs=[ofp_agent_url],
388
+ outputs=[add_agent_status, ofp_agent_url, chatbot, floor_status, agent_table, agent_selector]
389
+ ).then(
390
+ fn=lambda: gr.Dropdown(choices=get_agent_dropdown_choices()),
391
+ outputs=[message_agent]
392
+ )
393
+
394
  send_btn.click(
395
  fn=send_message,
396
  inputs=[message_agent, message_input],