pynode-flow 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vLLM Node - Sends images and prompts to a vLLM OpenAI-compatible server.
|
|
3
|
+
|
|
4
|
+
Calls the /v1/chat/completions endpoint with vision-language model support.
|
|
5
|
+
All configuration fields (server_url, model, system_prompt, prompt, etc.)
|
|
6
|
+
can be set in the UI and overridden at runtime via incoming message fields.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import base64
|
|
11
|
+
import threading
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional, Dict, Any, List
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import requests
|
|
17
|
+
REQUESTS_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
REQUESTS_AVAILABLE = False
|
|
20
|
+
import urllib.request
|
|
21
|
+
import urllib.error
|
|
22
|
+
import ssl
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from ..base_node import BaseNode, MessageKeys, Info
|
|
26
|
+
except ImportError:
|
|
27
|
+
from base_node import BaseNode, MessageKeys, Info
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Info panel
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
_info = Info()
|
|
35
|
+
_info.add_text(
|
|
36
|
+
"Sends images and text prompts to a vLLM server using the "
|
|
37
|
+
"OpenAI-compatible /v1/chat/completions endpoint."
|
|
38
|
+
)
|
|
39
|
+
_info.add_header("Inputs")
|
|
40
|
+
_info.add_bullets(
|
|
41
|
+
("Input 0:", "Message with image data and/or prompt text"),
|
|
42
|
+
)
|
|
43
|
+
_info.add_header("Outputs")
|
|
44
|
+
_info.add_bullets(
|
|
45
|
+
("Output 0:", "Model response"),
|
|
46
|
+
)
|
|
47
|
+
_info.add_bullets(
|
|
48
|
+
("payload.content:", "The model's text response"),
|
|
49
|
+
("payload.model:", "Model name used"),
|
|
50
|
+
("payload.usage:", "Token usage statistics"),
|
|
51
|
+
("payload.elapsed:", "Response time in seconds"),
|
|
52
|
+
)
|
|
53
|
+
_info.add_header("Configuration")
|
|
54
|
+
_info.add_text(
|
|
55
|
+
"All fields below can be set in the properties panel. "
|
|
56
|
+
"Any field can be overridden at runtime by the incoming message:"
|
|
57
|
+
)
|
|
58
|
+
_info.add_bullets(
|
|
59
|
+
("msg.payload.prompt:", "Override the user prompt"),
|
|
60
|
+
("msg.payload.system_prompt:", "Override the system prompt"),
|
|
61
|
+
("msg.payload.image:", "Image data (base64 string or image dict)"),
|
|
62
|
+
("msg.payload.server_url:", "Override the server URL"),
|
|
63
|
+
("msg.payload.model:", "Override the model name"),
|
|
64
|
+
("msg.payload.max_tokens:", "Override max tokens"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class VLLMNode(BaseNode):
|
|
69
|
+
"""
|
|
70
|
+
vLLM Node - Interface to vLLM OpenAI-compatible vision-language models.
|
|
71
|
+
|
|
72
|
+
Builds an OpenAI chat completions request with optional image input
|
|
73
|
+
and sends it to a vLLM server. Configuration can be set via the UI
|
|
74
|
+
properties panel and overridden at runtime through incoming message fields.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
display_name = 'vLLM'
|
|
78
|
+
info = str(_info)
|
|
79
|
+
icon = '🤖'
|
|
80
|
+
category = 'vision'
|
|
81
|
+
color = '#7C3AED'
|
|
82
|
+
border_color = '#5B21B6'
|
|
83
|
+
text_color = '#FFFFFF'
|
|
84
|
+
input_count = 1
|
|
85
|
+
output_count = 1
|
|
86
|
+
|
|
87
|
+
DEFAULT_CONFIG: Dict[str, Any] = {
|
|
88
|
+
'server_url': 'http://localhost:8000',
|
|
89
|
+
'model': 'Qwen/Qwen3.5-9B',
|
|
90
|
+
'system_prompt': 'You are a helpful assistant that analyzes images.',
|
|
91
|
+
'prompt': 'Describe what you see in this image.',
|
|
92
|
+
'max_tokens': 1024,
|
|
93
|
+
'timeout': 60,
|
|
94
|
+
'enable_thinking': False,
|
|
95
|
+
'image_path': 'payload.image',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
properties = [
|
|
99
|
+
{
|
|
100
|
+
'name': 'server_url',
|
|
101
|
+
'label': 'Server URL',
|
|
102
|
+
'type': 'text',
|
|
103
|
+
'placeholder': 'http://localhost:8000',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
'name': 'model',
|
|
107
|
+
'label': 'Model',
|
|
108
|
+
'type': 'text',
|
|
109
|
+
'placeholder': 'Qwen/Qwen3.5-9B',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
'name': 'system_prompt',
|
|
113
|
+
'label': 'System Prompt',
|
|
114
|
+
'type': 'textarea',
|
|
115
|
+
'placeholder': 'You are a helpful assistant.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
'name': 'prompt',
|
|
119
|
+
'label': 'User Prompt',
|
|
120
|
+
'type': 'textarea',
|
|
121
|
+
'placeholder': 'Describe what you see in this image.',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
'name': 'max_tokens',
|
|
125
|
+
'label': 'Max Tokens',
|
|
126
|
+
'type': 'text',
|
|
127
|
+
'placeholder': '1024',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
'name': 'timeout',
|
|
131
|
+
'label': 'Timeout (seconds)',
|
|
132
|
+
'type': 'text',
|
|
133
|
+
'placeholder': '60',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
'name': 'enable_thinking',
|
|
137
|
+
'label': 'Enable Thinking',
|
|
138
|
+
'type': 'checkbox',
|
|
139
|
+
'default': False,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
'name': 'image_path',
|
|
143
|
+
'label': 'Image Path in Message',
|
|
144
|
+
'type': 'text',
|
|
145
|
+
'placeholder': 'payload.image',
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
def __init__(self, node_id: Optional[str] = None, name: str = "vllm"):
|
|
150
|
+
super().__init__(node_id, name)
|
|
151
|
+
self.configure(self.DEFAULT_CONFIG.copy())
|
|
152
|
+
|
|
153
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
154
|
+
"""Handle incoming messages by calling vLLM in a background thread."""
|
|
155
|
+
thread = threading.Thread(
|
|
156
|
+
target=self._call_vllm,
|
|
157
|
+
args=(msg,),
|
|
158
|
+
daemon=True,
|
|
159
|
+
)
|
|
160
|
+
thread.start()
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
# Resolve a config value, allowing msg override
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
def _resolve(self, msg: Dict[str, Any], key: str, default: Any = None) -> Any:
|
|
166
|
+
"""
|
|
167
|
+
Get a value from msg.payload.{key} first, falling back to config.
|
|
168
|
+
"""
|
|
169
|
+
payload = msg.get(MessageKeys.PAYLOAD)
|
|
170
|
+
if isinstance(payload, dict) and key in payload:
|
|
171
|
+
return payload[key]
|
|
172
|
+
return self.config.get(key, default)
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
# Build the image data URL from various input formats
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
def _get_image_data_url(self, msg: Dict[str, Any]) -> Optional[str]:
|
|
178
|
+
"""
|
|
179
|
+
Extract image from the message and return as a data URL string.
|
|
180
|
+
Supports: base64 string, dict with data/encoding/format, numpy array.
|
|
181
|
+
"""
|
|
182
|
+
image_path = self.config.get('image_path', 'payload.image')
|
|
183
|
+
image_data = self._get_nested_value(msg, image_path)
|
|
184
|
+
|
|
185
|
+
# Also check msg.payload.image as direct override
|
|
186
|
+
payload = msg.get(MessageKeys.PAYLOAD)
|
|
187
|
+
if image_data is None and isinstance(payload, dict):
|
|
188
|
+
image_data = payload.get('image')
|
|
189
|
+
|
|
190
|
+
if image_data is None:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
# Already a data URL
|
|
194
|
+
if isinstance(image_data, str) and image_data.startswith('data:image'):
|
|
195
|
+
return image_data
|
|
196
|
+
|
|
197
|
+
# Raw base64 string
|
|
198
|
+
if isinstance(image_data, str):
|
|
199
|
+
return f'data:image/jpeg;base64,{image_data}'
|
|
200
|
+
|
|
201
|
+
# Camera node dict format: {'format': 'jpeg', 'encoding': 'base64', 'data': '...'}
|
|
202
|
+
if isinstance(image_data, dict):
|
|
203
|
+
data = image_data.get('data')
|
|
204
|
+
fmt = image_data.get('format', 'jpeg')
|
|
205
|
+
encoding = image_data.get('encoding', '')
|
|
206
|
+
|
|
207
|
+
if encoding == 'base64' and isinstance(data, str):
|
|
208
|
+
return f'data:image/{fmt};base64,{data}'
|
|
209
|
+
|
|
210
|
+
# numpy array inside dict — encode to JPEG base64
|
|
211
|
+
try:
|
|
212
|
+
import numpy as np
|
|
213
|
+
import cv2
|
|
214
|
+
if isinstance(data, np.ndarray):
|
|
215
|
+
ret, buf = cv2.imencode('.jpg', data)
|
|
216
|
+
if ret:
|
|
217
|
+
b64 = base64.b64encode(buf.tobytes()).decode('utf-8')
|
|
218
|
+
return f'data:image/jpeg;base64,{b64}'
|
|
219
|
+
except ImportError:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
# Direct numpy array
|
|
223
|
+
try:
|
|
224
|
+
import numpy as np
|
|
225
|
+
import cv2
|
|
226
|
+
if isinstance(image_data, np.ndarray):
|
|
227
|
+
ret, buf = cv2.imencode('.jpg', image_data)
|
|
228
|
+
if ret:
|
|
229
|
+
b64 = base64.b64encode(buf.tobytes()).decode('utf-8')
|
|
230
|
+
return f'data:image/jpeg;base64,{b64}'
|
|
231
|
+
except ImportError:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
self.report_error(f"Unsupported image format: {type(image_data).__name__}")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
# Main request logic
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
def _call_vllm(self, msg: Dict[str, Any]):
|
|
241
|
+
"""Build the payload and call the vLLM chat completions endpoint."""
|
|
242
|
+
import time as _time
|
|
243
|
+
|
|
244
|
+
server_url = self._resolve(msg, 'server_url', self.DEFAULT_CONFIG['server_url'])
|
|
245
|
+
model = self._resolve(msg, 'model', self.DEFAULT_CONFIG['model'])
|
|
246
|
+
system_prompt = self._resolve(msg, 'system_prompt', self.DEFAULT_CONFIG['system_prompt'])
|
|
247
|
+
prompt = self._resolve(msg, 'prompt', self.DEFAULT_CONFIG['prompt'])
|
|
248
|
+
max_tokens = int(self._resolve(msg, 'max_tokens', self.DEFAULT_CONFIG['max_tokens']))
|
|
249
|
+
timeout = int(self._resolve(msg, 'timeout', self.DEFAULT_CONFIG['timeout']))
|
|
250
|
+
enable_thinking = self._resolve(msg, 'enable_thinking', self.DEFAULT_CONFIG['enable_thinking'])
|
|
251
|
+
if isinstance(enable_thinking, str):
|
|
252
|
+
enable_thinking = enable_thinking.lower() in ('true', '1', 'yes')
|
|
253
|
+
|
|
254
|
+
# Build user content (text only, or text + image)
|
|
255
|
+
user_content: Any
|
|
256
|
+
image_url = self._get_image_data_url(msg)
|
|
257
|
+
if image_url:
|
|
258
|
+
user_content = [
|
|
259
|
+
{"type": "image_url", "image_url": {"url": image_url}},
|
|
260
|
+
{"type": "text", "text": prompt},
|
|
261
|
+
]
|
|
262
|
+
else:
|
|
263
|
+
user_content = prompt
|
|
264
|
+
|
|
265
|
+
# Build messages
|
|
266
|
+
messages = []
|
|
267
|
+
if system_prompt:
|
|
268
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
269
|
+
messages.append({"role": "user", "content": user_content})
|
|
270
|
+
|
|
271
|
+
# Build request payload
|
|
272
|
+
request_payload: Dict[str, Any] = {
|
|
273
|
+
"model": model,
|
|
274
|
+
"messages": messages,
|
|
275
|
+
"max_tokens": max_tokens,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
request_payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking}
|
|
279
|
+
|
|
280
|
+
url = f"{server_url.rstrip('/')}/v1/chat/completions"
|
|
281
|
+
start = _time.time()
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
response_data = self._post_json(url, request_payload, timeout)
|
|
285
|
+
elapsed = _time.time() - start
|
|
286
|
+
|
|
287
|
+
# Extract content from response
|
|
288
|
+
content = ''
|
|
289
|
+
usage = {}
|
|
290
|
+
if isinstance(response_data, dict):
|
|
291
|
+
choices = response_data.get('choices', [])
|
|
292
|
+
if choices:
|
|
293
|
+
content = choices[0].get('message', {}).get('content', '')
|
|
294
|
+
usage = response_data.get('usage', {})
|
|
295
|
+
|
|
296
|
+
out_msg = self.create_message(
|
|
297
|
+
payload={
|
|
298
|
+
'content': content,
|
|
299
|
+
'model': model,
|
|
300
|
+
'usage': usage,
|
|
301
|
+
'elapsed': round(elapsed, 3),
|
|
302
|
+
},
|
|
303
|
+
topic=msg.get(MessageKeys.TOPIC, ''),
|
|
304
|
+
)
|
|
305
|
+
# Preserve non-internal fields from original message
|
|
306
|
+
for key in msg:
|
|
307
|
+
if key not in out_msg and key != MessageKeys.PAYLOAD and not key.startswith('_'):
|
|
308
|
+
out_msg[key] = msg[key]
|
|
309
|
+
|
|
310
|
+
self.send(out_msg)
|
|
311
|
+
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
elapsed = _time.time() - start
|
|
314
|
+
logger.error("VLLMNode [%s] request failed: %s", self.name, exc)
|
|
315
|
+
self.report_error(f"vLLM request failed: {exc}")
|
|
316
|
+
|
|
317
|
+
out_msg = self.create_message(
|
|
318
|
+
payload={
|
|
319
|
+
'content': None,
|
|
320
|
+
'error': str(exc),
|
|
321
|
+
'model': model,
|
|
322
|
+
'elapsed': round(elapsed, 3),
|
|
323
|
+
},
|
|
324
|
+
topic=f"{msg.get(MessageKeys.TOPIC, '')}/error",
|
|
325
|
+
)
|
|
326
|
+
self.send(out_msg)
|
|
327
|
+
|
|
328
|
+
# ------------------------------------------------------------------
|
|
329
|
+
# HTTP helpers
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
def _post_json(self, url: str, payload: Dict[str, Any], timeout: int) -> Any:
|
|
332
|
+
"""POST JSON and return parsed response. Uses requests if available."""
|
|
333
|
+
data = json.dumps(payload).encode('utf-8')
|
|
334
|
+
headers = {'Content-Type': 'application/json'}
|
|
335
|
+
|
|
336
|
+
if REQUESTS_AVAILABLE:
|
|
337
|
+
resp = requests.post(url, json=payload, timeout=timeout)
|
|
338
|
+
resp.raise_for_status()
|
|
339
|
+
return resp.json()
|
|
340
|
+
|
|
341
|
+
# Fallback to urllib
|
|
342
|
+
req = urllib.request.Request(url, data=data, headers=headers, method='POST')
|
|
343
|
+
ctx = ssl.create_default_context()
|
|
344
|
+
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
|
345
|
+
return json.loads(resp.read().decode('utf-8'))
|