-
Notifications
You must be signed in to change notification settings - Fork 99
[Traffic Intersection Agent] Websockets implementation for pushing updates in real-time #1815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
krish918
wants to merge
9
commits into
open-edge-platform:main
Choose a base branch
from
krish918:trafficagent/websocket
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
290caf8
🏷️ Updated types,, minor refactoring
krish918 0ab63d0
⚡️Added websocket endpoint, refactored DataAggregator to Push notifs
krish918 29bb13c
Merge branch 'main' into routeagent/realtime-update
krish918 539fb43
♻ Updated websocket endpoint's query param, max size of websocket
krish918 e1e2ebd
⚡️🧵 Implemented websocket client at traffic dashboard UI, changes for…
krish918 7c8256c
🧵 all methods async, minor refactoring, removed unused methods
krish918 ed5ebb2
Added default websockets endpoint and removed unused file
krish918 1495175
minor exception handling changes for websocket conn in UI
krish918 baa65b2
removed hardcoded model name in traffic_agent.json
krish918 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,19 @@ | ||
| # Copyright (C) 2025 Intel Corporation | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| """API routes for Traffic Intersection Agent.""" | ||
|
|
||
| from datetime import datetime, timedelta | ||
| from typing import Dict, Any | ||
| from typing import Annotated, Dict, Any | ||
|
|
||
| from fastapi import APIRouter, HTTPException, Depends, Query, Request | ||
| from fastapi.responses import JSONResponse | ||
| from fastapi import APIRouter, HTTPException, Query, Request, WebSocket, WebSocketDisconnect | ||
| from fastapi.encoders import jsonable_encoder | ||
| import structlog | ||
|
|
||
| from services.data_aggregator import DataAggregatorService | ||
| from services.weather_service import WeatherService | ||
|
|
||
| logger = structlog.get_logger(__name__) | ||
|
|
||
| logger = structlog.get_logger(__name__) | ||
| router = APIRouter() | ||
|
|
||
|
|
||
| def get_data_aggregator(request): | ||
| """Dependency to get data aggregator service from app state.""" | ||
| return request.app.state.data_aggregator | ||
|
|
@@ -26,10 +24,62 @@ def get_weather_service(request): | |
| return request.app.state.weather_service | ||
|
|
||
|
|
||
| def _build_response_dict(traffic_response: Any, weather_data: Any, include_images: bool) -> Dict[str, Any]: | ||
| """Helper to build traffic intelligence response dictionary.""" | ||
| response_dict = { | ||
| "timestamp": traffic_response.timestamp, | ||
| "response_age": traffic_response.response_age if traffic_response.response_age else None, | ||
| "intersection_id": traffic_response.intersection_id, | ||
| "data": { | ||
| "intersection_id": traffic_response.data.intersection_id, | ||
| "intersection_name": traffic_response.data.intersection_name, | ||
| "latitude": traffic_response.data.latitude, | ||
| "longitude": traffic_response.data.longitude, | ||
| "timestamp": traffic_response.data.timestamp.isoformat(), | ||
| "north_camera": traffic_response.data.north_camera, | ||
| "south_camera": traffic_response.data.south_camera, | ||
| "east_camera": traffic_response.data.east_camera, | ||
| "west_camera": traffic_response.data.west_camera, | ||
| "total_density": traffic_response.data.total_density, | ||
| "intersection_status": traffic_response.data.intersection_status, | ||
| "north_pedestrian": traffic_response.data.north_pedestrian, | ||
| "south_pedestrian": traffic_response.data.south_pedestrian, | ||
| "east_pedestrian": traffic_response.data.east_pedestrian, | ||
| "west_pedestrian": traffic_response.data.west_pedestrian, | ||
| "total_pedestrian_count": traffic_response.data.total_pedestrian_count, | ||
| "north_timestamp": traffic_response.data.north_timestamp.isoformat() if traffic_response.data.north_timestamp else None, | ||
| "south_timestamp": traffic_response.data.south_timestamp.isoformat() if traffic_response.data.south_timestamp else None, | ||
| "east_timestamp": traffic_response.data.east_timestamp.isoformat() if traffic_response.data.east_timestamp else None, | ||
| "west_timestamp": traffic_response.data.west_timestamp.isoformat() if traffic_response.data.west_timestamp else None, | ||
| }, | ||
| "weather_data": weather_data.__dict__, | ||
| "vlm_analysis": { | ||
| "traffic_summary": traffic_response.vlm_analysis.traffic_summary, | ||
| "alerts": [ | ||
| { | ||
| "alert_type": alert.alert_type.value, | ||
| "level": alert.level.value, | ||
| "description": alert.description, | ||
| "weather_related": alert.weather_related | ||
| } | ||
| for alert in traffic_response.vlm_analysis.alerts | ||
| ], | ||
| "recommendations": traffic_response.vlm_analysis.recommendations or [], | ||
| "analysis_timestamp": traffic_response.vlm_analysis.analysis_timestamp.isoformat() if traffic_response.vlm_analysis.analysis_timestamp else None | ||
| } | ||
| } | ||
|
|
||
| # Add camera images only if requested | ||
| if include_images: | ||
| response_dict["camera_images"] = traffic_response.camera_images | ||
|
|
||
| return response_dict | ||
|
|
||
|
|
||
| @router.get("/traffic/current", response_model=Dict[str, Any]) | ||
| async def get_current_traffic_intelligence( | ||
| request: Request, | ||
| images: bool = Query(default=True, description="Include camera images in response") | ||
| images: Annotated[bool, Query(description="Include camera images in response")] = True, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Get current traffic intelligence data for the intersection. | ||
|
|
@@ -49,62 +99,16 @@ async def get_current_traffic_intelligence( | |
| raise HTTPException(status_code=404, detail="No traffic data available") | ||
|
|
||
| # Get current weather data | ||
| weather_service = get_weather_service(request) | ||
| weather_service: WeatherService = get_weather_service(request) | ||
| weather_data = await weather_service.get_current_weather() | ||
|
|
||
| # Convert to dict for JSON response | ||
| response_dict = { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed reusable code - to shift to common method. |
||
| "timestamp": traffic_response.timestamp, | ||
| "response_age": traffic_response.response_age if traffic_response.response_age else None, | ||
| "intersection_id": traffic_response.intersection_id, | ||
| "data": { | ||
| "intersection_id": traffic_response.data.intersection_id, | ||
| "intersection_name": traffic_response.data.intersection_name, | ||
| "latitude": traffic_response.data.latitude, | ||
| "longitude": traffic_response.data.longitude, | ||
| "timestamp": traffic_response.data.timestamp.isoformat(), | ||
| "north_camera": traffic_response.data.north_camera, | ||
| "south_camera": traffic_response.data.south_camera, | ||
| "east_camera": traffic_response.data.east_camera, | ||
| "west_camera": traffic_response.data.west_camera, | ||
| "total_density": traffic_response.data.total_density, | ||
| "intersection_status": traffic_response.data.intersection_status, | ||
| "north_pedestrian": traffic_response.data.north_pedestrian, | ||
| "south_pedestrian": traffic_response.data.south_pedestrian, | ||
| "east_pedestrian": traffic_response.data.east_pedestrian, | ||
| "west_pedestrian": traffic_response.data.west_pedestrian, | ||
| "total_pedestrian_count": traffic_response.data.total_pedestrian_count, | ||
| "north_timestamp": traffic_response.data.north_timestamp, | ||
| "south_timestamp": traffic_response.data.south_timestamp, | ||
| "east_timestamp": traffic_response.data.east_timestamp, | ||
| "west_timestamp": traffic_response.data.west_timestamp, | ||
| }, | ||
| "weather_data": weather_data.__dict__, | ||
| "vlm_analysis": { | ||
| "traffic_summary": traffic_response.vlm_analysis.traffic_summary, | ||
| "alerts": [ | ||
| { | ||
| "alert_type": alert.alert_type.value, | ||
| "level": alert.level.value, | ||
| "description": alert.description, | ||
| "weather_related": alert.weather_related | ||
| } | ||
| for alert in traffic_response.vlm_analysis.alerts | ||
| ], | ||
| "recommendations": traffic_response.vlm_analysis.recommendations or [], | ||
| "analysis_timestamp": traffic_response.vlm_analysis.analysis_timestamp.isoformat() if traffic_response.vlm_analysis.analysis_timestamp else None | ||
| } | ||
| } | ||
|
|
||
| # Add camera images only if requested | ||
| if images: | ||
| response_dict["camera_images"] = traffic_response.camera_images | ||
| response_dict = _build_response_dict(traffic_response, weather_data, images) | ||
|
|
||
| logger.info("Current traffic intelligence served", | ||
| intersection_id=traffic_response.intersection_id, | ||
| total_density=traffic_response.data.total_density, | ||
| total_pedestrian_count=traffic_response.data.total_pedestrian_count, | ||
| alerts_count=len(traffic_response.vlm_analysis.alerts)) | ||
| total_pedestrian_count=traffic_response.data.total_pedestrian_count) | ||
|
|
||
| return response_dict | ||
|
|
||
|
|
@@ -113,3 +117,57 @@ async def get_current_traffic_intelligence( | |
| except Exception as e: | ||
| logger.error("Failed to get current traffic intelligence", error=str(e)) | ||
| raise HTTPException(status_code=500, detail="Internal server error") | ||
|
|
||
|
|
||
| @router.websocket("/traffic/current/ws") | ||
| async def ws_current_traffic_intelligence( | ||
| websocket: WebSocket, | ||
| images: Annotated[bool, Query()] = True, | ||
| ): | ||
| """ | ||
| WebSocket endpoint for real-time traffic intersection data. | ||
|
|
||
| Pushes updated traffic intersection data to the client whenever | ||
| new VLM analysis results become available. This is a drop-in | ||
| alternative to the REST GET /traffic/current endpoint and returns | ||
| the same data in the same format. | ||
|
|
||
| Query Parameters: | ||
| images: If false, camera_images will be excluded from response (default: true) | ||
| """ | ||
| await websocket.accept() | ||
|
|
||
| try: | ||
| data_aggregator: DataAggregatorService = get_data_aggregator(websocket) | ||
| weather_service: WeatherService = get_weather_service(websocket) | ||
|
|
||
| # Run a loop to wait for new data events and push updates to the client | ||
| while True: | ||
| traffic_response = await data_aggregator.get_current_traffic_intelligence() | ||
| if traffic_response: | ||
| weather_data = await weather_service.get_current_weather() | ||
| response_dict = _build_response_dict(traffic_response, weather_data, images) | ||
|
|
||
| await websocket.send_json(jsonable_encoder(response_dict)) | ||
|
|
||
| logger.debug("Traffic Intersection data pushed to client", | ||
| intersection_id=traffic_response.intersection_id, | ||
| total_density=traffic_response.data.total_density, | ||
| total_pedestrian_count=traffic_response.data.total_pedestrian_count) | ||
| else: | ||
| await websocket.send_json({ | ||
| "status": "waiting", | ||
| "message": "VLM analysis not yet available." | ||
| }) | ||
|
|
||
| event = data_aggregator.new_data_event | ||
| logger.debug("WebSocket waiting for new data event") | ||
| await event.wait() | ||
| except WebSocketDisconnect: | ||
| logger.info("WebSocket client disconnected") | ||
| except Exception as e: | ||
| logger.error("WebSocket error in Smart Traffic Intersection Agent", error=str(e)) | ||
| try: | ||
| await websocket.close(code=1011, reason="Internal server error") | ||
| except Exception as e: | ||
| logger.error("Failed to close WebSocket after error", error=str(e)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
refactoring: Common method used for use with both REST API and Websocket endpoint.