Skip to content

Commit

Permalink
AI-170: Added websocket_input and websocket_output components (#50)
Browse files Browse the repository at this point in the history
* feat: AI-170: websocket input and output components (start)

* Refactor HttpServerInputBase to work as a websocket input component

* refactor: implement WebSocket input component using Flask-SocketIO

* style: Format code and improve consistency in websocket_input.py

* refactor: update WebSocket input to use send_message and improve invoke method

* refactor: wrap WebSocket messages in events before enqueueing

* feat: implement WebsocketOutput component for sending messages

* refactor: implement WebSocket input/output with shared connection management

* refactor: simplify WebSocket output component and improve error handling

* style: replace f-strings with format directives in logging statements

* feat: implement WebSocket connection tracking and improve message handling

* refactor: replace f-strings with format directives in logging statements

* chore: Remove unused comment about f-strings in logging

* fix: replace self.socketio.sid with request.sid for correct session ID handling

* More changes

* feat: add HTML serving capability to WebsocketInput

* refactor: Improve WebsocketInput class structure and remove unused import

* refactor: simplify WebsocketInput component

* fix: resolve HTML file path issues in WebsocketInput

* refactor: implement event processing for WebSocket input

* Add example for websocket

* refactor: centralize event processing and error handling in ComponentBase

* fix: update websocket.yaml to correctly pass payload and socket_id

* style: enhance UI design and responsiveness of WebSocket example app

* style: implement light/dark mode theme based on browser preference

* fix: improve dark mode implementation and add smooth transitions

* style: adjust input and button spacing in WebSocket example app

* feat: add info icon with popup explanation to WebSocket example app

* refactor: simplify WebSocket example app interface

* More adjustments

* style: reduce padding above top title and adjust container spacing

* Remove unnecessary whitespace

* Fix a few warnings

* feat: add payload encoding and format to WebSocket components

* refactor: reorganize imports and improve code formatting

* refactor: simplify encode_payload function

* refactor: use centralized payload encoding/decoding functions

* Some additional cleanup

* change the default payload encoding to 'none' so that strings will be sent

* Fix warning

* refactor: implement WebSocket base class and enhance output functionality

* feat: import request object in websocket_input.py

* refactor: move common config to base class and use deep copy

* refactor: merge base_info into websocket component info structures

* More refactoring to allow websocket_output to share some code

* refactor: make WebsocketBase abstract and enforce listen_port requirement

* refactor: remove listen_port validation in WebsocketBase

* feat: implement threaded WebSocket server for output component

* default the encoding to none

* feat: enable WebSocket server debugging

* Tweak the dynamic module loader to give better output if a dynamically loaded module fails due to it not being able to be loaded by it failing to load another module. Also turn off websocket debug logs

* AI-170: Add new broker type: dev_broker (#51)

* feat: implement DevBroker for development purposes

* refactor: convert Solace message to dictionary in receive_message

* refactor: align DevBroker with solace_messaging subscription handling

* More changes

* More tweaks

* Last few issues

* refactor: standardize use of 'queue_name' in dev_broker_messaging

* Remove queue_id
  • Loading branch information
efunneko authored Oct 15, 2024
1 parent 6d8f4b7 commit 2995ef0
Show file tree
Hide file tree
Showing 14 changed files with 839 additions and 90 deletions.
42 changes: 42 additions & 0 deletions examples/websocket/websocket.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
# Example configuration for a WebSocket flow
# This flow creates a WebSocket server that echoes messages back to clients
# It also serves an example HTML file for easy testing

log:
stdout_log_level: INFO
log_file_level: DEBUG
log_file: solace_ai_connector.log

flows:
- name: websocket_echo
components:
# WebSocket Input
- component_name: websocket_input
component_module: websocket_input
component_config:
listen_port: 5000
serve_html: true
html_path: "examples/websocket/websocket_example_app.html"

# Pass Through
- component_name: pass_through
component_module: pass_through
component_config: {}
input_transforms:
- type: copy
source_expression: input.payload
dest_expression: user_data.input:payload
- type: copy
source_expression: input.user_properties:socket_id
dest_expression: user_data.input:socket_id
input_selection:
source_expression: user_data.input

# WebSocket Output
- component_name: websocket_output
component_module: websocket_output
component_config:
payload_encoding: none
input_selection:
source_expression: previous
266 changes: 266 additions & 0 deletions examples/websocket/websocket_example_app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Example App</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #e6f3ff;
--container-bg: #ffffff;
--text-color: #333333;
--header-color: #3498db;
--input-bg: #f8f9fa;
--input-border: #ced4da;
--button-bg: #3498db;
--button-color: #ffffff;
--button-hover: #2980b9;
--message-bg: #ffffff;
--message-border: #e9ecef;
--message-alt-bg: #f8f9fa;
--timestamp-color: #6c757d;
--error-color: #dc3545;
--shadow-color: rgba(0, 0, 0, 0.1);
}

@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--container-bg: #2c2c2c;
--text-color: #e0e0e0;
--header-color: #4fa3d1;
--input-bg: #3a3a3a;
--input-border: #555555;
--button-bg: #4fa3d1;
--button-color: #ffffff;
--button-hover: #3a7ca5;
--message-bg: #2c2c2c;
--message-border: #444444;
--message-alt-bg: #333333;
--timestamp-color: #a0a0a0;
--error-color: #ff6b6b;
--shadow-color: rgba(0, 0, 0, 0.3);
}
}

body {
font-family: 'Roboto', Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
padding: 20px;
transition: background-color 0.3s, color 0.3s;
}

.container {
max-width: 800px;
margin: 0 auto;
background-color: var(--container-bg);
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow-color);
transition: background-color 0.3s, box-shadow 0.3s;
}

h1, h2 {
margin-top: 0;
color: var(--header-color);
margin-bottom: 20px;
transition: color 0.3s;
}

#connection-section, #input-section, #output-section {
background-color: var(--input-bg);
padding: 20px;
margin-bottom: 30px;
border-radius: 8px;
box-shadow: 0 1px 3px var(--shadow-color);
transition: background-color 0.3s, box-shadow 0.3s;
}

input[type="text"], #json-input {
width: calc(100% - 20px); /* Subtract 20px to account for padding */
padding: 10px;
border: 1px solid var(--input-border);
border-radius: 4px;
font-size: 16px;
background-color: var(--input-bg);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
margin-bottom: 10px; /* Add space below inputs */
}

button {
margin-top: 5px; /* Add space above buttons */
background-color: var(--button-bg);
color: var(--button-color);
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}

button:hover {
background-color: var(--button-hover);
}

#status {
font-weight: bold;
margin-left: 10px;
color: var(--header-color);
transition: color 0.3s;
}

.message {
background-color: var(--message-bg);
border: 1px solid var(--message-border);
border-radius: 4px;
padding: 15px;
margin-bottom: 15px;
transition: background-color 0.3s, border-color 0.3s;
}

.message:nth-child(even) {
background-color: var(--message-alt-bg);
}

.timestamp {
font-size: 0.8em;
color: var(--timestamp-color);
transition: color 0.3s;
}

.error {
color: var(--error-color);
margin-top: 5px;
transition: color 0.3s;
}

@media (max-width: 600px) {
body {
padding: 10px;
}

.container {
padding: 15px;
}

input[type="text"], #json-input, button {
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>WebSocket Example App</h1>
<p>This is a simple app to show how JSON can be sent into a solace-ai-connector flow and how to receive output from it. Just hit connect to connect to the flow, type in some JSON and hit send. Your JSON should be echoed back to you.</p>

<div id="connection-section">
<input type="text" id="url-input" value="ws://localhost:5000">
<button id="connect-button">Connect</button>
<span id="status">Disconnected</span>
</div>

<div id="input-section">
<textarea id="json-input" placeholder="Enter JSON here"></textarea>
<button id="send-button">Send</button>
<div id="input-error" class="error"></div>
</div>

<div id="output-section">
<h2>Received Messages</h2>
<div id="messages"></div>
<button id="clear-button">Clear Messages</button>
</div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script>
const urlInput = document.getElementById('url-input');
const connectButton = document.getElementById('connect-button');
const status = document.getElementById('status');
const jsonInput = document.getElementById('json-input');
const sendButton = document.getElementById('send-button');
const inputError = document.getElementById('input-error');
const messagesDiv = document.getElementById('messages');
const clearButton = document.getElementById('clear-button');

let socket;

function connect() {
socket = io(urlInput.value);

socket.on('connect', () => {
status.textContent = 'Connected';
connectButton.textContent = 'Disconnect';
});

socket.on('disconnect', () => {
status.textContent = 'Disconnected';
connectButton.textContent = 'Connect';
});

socket.on('message', (data) => {
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
const timestamp = new Date().toLocaleString();
messageDiv.innerHTML = `
<div class="timestamp">${timestamp}</div>
<pre>${JSON.stringify(JSON.parse(data), null, 2)}</pre>
`;
messagesDiv.appendChild(messageDiv);
});
}

function disconnect() {
if (socket) {
socket.disconnect();
socket = null;
}
}

connectButton.addEventListener('click', () => {
if (socket && socket.connected) {
disconnect();
} else {
connect();
}
});

function sendMessage() {
const jsonString = jsonInput.value.trim();
try {
const jsonObject = JSON.parse(jsonString);
socket.emit('message', JSON.stringify(jsonObject));
jsonInput.value = '';
inputError.textContent = '';
} catch (error) {
inputError.textContent = 'Invalid JSON: ' + error.message;
}
}

sendButton.addEventListener('click', sendMessage);

jsonInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});

clearButton.addEventListener('click', () => {
messagesDiv.innerHTML = '';
});

// Info icon functionality
const infoIcon = document.getElementById('info-icon');
const infoPopup = document.getElementById('info-popup');
let isInfoPopupVisible = false;


</script>
</body>
</html>
Loading

0 comments on commit 2995ef0

Please sign in to comment.