Spaces:
Sleeping
Sleeping
BolyosCsaba commited on
Commit Β·
eac355f
1
Parent(s): d419fb8
envelop refactor
Browse files- AGENT_MANAGEMENT.md +183 -0
- ENVELOPE_BROADCASTING.md +288 -0
- QUICKSTART.md +11 -5
- install_and_run.sh +3 -3
- requirements.txt +1 -0
- src/__pycache__/__init__.cpython-313.pyc +0 -0
- src/__pycache__/agent_manager.cpython-313.pyc +0 -0
- src/__pycache__/floor_manager.cpython-313.pyc +0 -0
- src/agent_manager.py +310 -0
- src/floor_manager.py +352 -0
- src/protocol/__init__.py +0 -2
- src/protocol/__pycache__/__init__.cpython-313.pyc +0 -0
- src/protocol/__pycache__/envelope.cpython-313.pyc +0 -0
- src/protocol/__pycache__/events.cpython-313.pyc +0 -0
- src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- src/utils/__pycache__/config.cpython-313.pyc +0 -0
- src/utils/__pycache__/helpers.cpython-313.pyc +0 -0
- src/utils/__pycache__/logger.cpython-313.pyc +0 -0
- test_app.py +78 -8
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 |
-
###
|
| 4 |
|
| 5 |
```bash
|
| 6 |
-
#
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
```
|
| 9 |
|
| 10 |
-
###
|
| 11 |
|
| 12 |
```bash
|
| 13 |
-
#
|
|
|
|
|
|
|
|
|
|
| 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
|
| 18 |
-
echo "π¦ Installing
|
| 19 |
-
pip install
|
| 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 |
-
|
| 242 |
-
|
| 243 |
-
|
| 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],
|