flock-core 0.5.0b28__py3-none-any.whl → 0.5.56b0__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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/__init__.py +12 -217
- flock/agent.py +678 -0
- flock/api/themes.py +71 -0
- flock/artifacts.py +79 -0
- flock/cli.py +75 -0
- flock/components.py +173 -0
- flock/dashboard/__init__.py +28 -0
- flock/dashboard/collector.py +283 -0
- flock/dashboard/events.py +182 -0
- flock/dashboard/launcher.py +230 -0
- flock/dashboard/service.py +537 -0
- flock/dashboard/websocket.py +235 -0
- flock/engines/__init__.py +6 -0
- flock/engines/dspy_engine.py +856 -0
- flock/examples.py +128 -0
- flock/{core/util → helper}/cli_helper.py +4 -3
- flock/{core/logging → logging}/__init__.py +2 -3
- flock/{core/logging → logging}/formatters/enum_builder.py +3 -4
- flock/{core/logging → logging}/formatters/theme_builder.py +19 -44
- flock/{core/logging → logging}/formatters/themed_formatter.py +69 -115
- flock/{core/logging → logging}/logging.py +77 -61
- flock/{core/logging → logging}/telemetry.py +20 -26
- flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
- flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +6 -9
- flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
- flock/{core/logging → logging}/trace_and_logged.py +20 -24
- flock/mcp/__init__.py +91 -0
- flock/{core/mcp/mcp_client.py → mcp/client.py} +103 -154
- flock/{core/mcp/mcp_config.py → mcp/config.py} +62 -117
- flock/mcp/manager.py +255 -0
- flock/mcp/servers/sse/__init__.py +1 -1
- flock/mcp/servers/sse/flock_sse_server.py +11 -53
- flock/mcp/servers/stdio/__init__.py +1 -1
- flock/mcp/servers/stdio/flock_stdio_server.py +8 -48
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +17 -62
- flock/mcp/servers/websockets/flock_websocket_server.py +7 -40
- flock/{core/mcp/flock_mcp_tool.py → mcp/tool.py} +16 -26
- flock/mcp/types/__init__.py +42 -0
- flock/{core/mcp → mcp}/types/callbacks.py +9 -15
- flock/{core/mcp → mcp}/types/factories.py +7 -6
- flock/{core/mcp → mcp}/types/handlers.py +13 -18
- flock/{core/mcp → mcp}/types/types.py +70 -74
- flock/{core/mcp → mcp}/util/helpers.py +1 -1
- flock/orchestrator.py +645 -0
- flock/registry.py +148 -0
- flock/runtime.py +262 -0
- flock/service.py +140 -0
- flock/store.py +69 -0
- flock/subscription.py +111 -0
- flock/themes/andromeda.toml +1 -1
- flock/themes/apple-system-colors.toml +1 -1
- flock/themes/arcoiris.toml +1 -1
- flock/themes/atomonelight.toml +1 -1
- flock/themes/ayu copy.toml +1 -1
- flock/themes/ayu-light.toml +1 -1
- flock/themes/belafonte-day.toml +1 -1
- flock/themes/belafonte-night.toml +1 -1
- flock/themes/blulocodark.toml +1 -1
- flock/themes/breeze.toml +1 -1
- flock/themes/broadcast.toml +1 -1
- flock/themes/brogrammer.toml +1 -1
- flock/themes/builtin-dark.toml +1 -1
- flock/themes/builtin-pastel-dark.toml +1 -1
- flock/themes/catppuccin-latte.toml +1 -1
- flock/themes/catppuccin-macchiato.toml +1 -1
- flock/themes/catppuccin-mocha.toml +1 -1
- flock/themes/cga.toml +1 -1
- flock/themes/chalk.toml +1 -1
- flock/themes/ciapre.toml +1 -1
- flock/themes/coffee-theme.toml +1 -1
- flock/themes/cyberpunkscarletprotocol.toml +1 -1
- flock/themes/dark+.toml +1 -1
- flock/themes/darkermatrix.toml +1 -1
- flock/themes/darkside.toml +1 -1
- flock/themes/desert.toml +1 -1
- flock/themes/django.toml +1 -1
- flock/themes/djangosmooth.toml +1 -1
- flock/themes/doomone.toml +1 -1
- flock/themes/dotgov.toml +1 -1
- flock/themes/dracula+.toml +1 -1
- flock/themes/duckbones.toml +1 -1
- flock/themes/encom.toml +1 -1
- flock/themes/espresso.toml +1 -1
- flock/themes/everblush.toml +1 -1
- flock/themes/fairyfloss.toml +1 -1
- flock/themes/fideloper.toml +1 -1
- flock/themes/fishtank.toml +1 -1
- flock/themes/flexoki-light.toml +1 -1
- flock/themes/floraverse.toml +1 -1
- flock/themes/framer.toml +1 -1
- flock/themes/galizur.toml +1 -1
- flock/themes/github.toml +1 -1
- flock/themes/grass.toml +1 -1
- flock/themes/grey-green.toml +1 -1
- flock/themes/gruvboxlight.toml +1 -1
- flock/themes/guezwhoz.toml +1 -1
- flock/themes/harper.toml +1 -1
- flock/themes/hax0r-blue.toml +1 -1
- flock/themes/hopscotch.256.toml +1 -1
- flock/themes/ic-green-ppl.toml +1 -1
- flock/themes/iceberg-dark.toml +1 -1
- flock/themes/japanesque.toml +1 -1
- flock/themes/jubi.toml +1 -1
- flock/themes/kibble.toml +1 -1
- flock/themes/kolorit.toml +1 -1
- flock/themes/kurokula.toml +1 -1
- flock/themes/materialdesigncolors.toml +1 -1
- flock/themes/matrix.toml +1 -1
- flock/themes/mellifluous.toml +1 -1
- flock/themes/midnight-in-mojave.toml +1 -1
- flock/themes/monokai-remastered.toml +1 -1
- flock/themes/monokai-soda.toml +1 -1
- flock/themes/neon.toml +1 -1
- flock/themes/neopolitan.toml +1 -1
- flock/themes/nord-light.toml +1 -1
- flock/themes/ocean.toml +1 -1
- flock/themes/onehalfdark.toml +1 -1
- flock/themes/onehalflight.toml +1 -1
- flock/themes/palenighthc.toml +1 -1
- flock/themes/paulmillr.toml +1 -1
- flock/themes/pencildark.toml +1 -1
- flock/themes/pnevma.toml +1 -1
- flock/themes/purple-rain.toml +1 -1
- flock/themes/purplepeter.toml +1 -1
- flock/themes/raycast-dark.toml +1 -1
- flock/themes/red-sands.toml +1 -1
- flock/themes/relaxed.toml +1 -1
- flock/themes/retro.toml +1 -1
- flock/themes/rose-pine.toml +1 -1
- flock/themes/royal.toml +1 -1
- flock/themes/ryuuko.toml +1 -1
- flock/themes/sakura.toml +1 -1
- flock/themes/scarlet-protocol.toml +1 -1
- flock/themes/seoulbones-dark.toml +1 -1
- flock/themes/shades-of-purple.toml +1 -1
- flock/themes/smyck.toml +1 -1
- flock/themes/softserver.toml +1 -1
- flock/themes/solarized-darcula.toml +1 -1
- flock/themes/square.toml +1 -1
- flock/themes/sugarplum.toml +1 -1
- flock/themes/thayer-bright.toml +1 -1
- flock/themes/tokyonight.toml +1 -1
- flock/themes/tomorrow.toml +1 -1
- flock/themes/ubuntu.toml +1 -1
- flock/themes/ultradark.toml +1 -1
- flock/themes/ultraviolent.toml +1 -1
- flock/themes/unikitty.toml +1 -1
- flock/themes/urple.toml +1 -1
- flock/themes/vesper.toml +1 -1
- flock/themes/vimbones.toml +1 -1
- flock/themes/wildcherry.toml +1 -1
- flock/themes/wilmersdorf.toml +1 -1
- flock/themes/wryan.toml +1 -1
- flock/themes/xcodedarkhc.toml +1 -1
- flock/themes/xcodelight.toml +1 -1
- flock/themes/zenbones-light.toml +1 -1
- flock/themes/zenwritten-dark.toml +1 -1
- flock/utilities.py +301 -0
- flock/{components/utility → utility}/output_utility_component.py +68 -53
- flock/visibility.py +107 -0
- flock_core-0.5.56b0.dist-info/METADATA +747 -0
- flock_core-0.5.56b0.dist-info/RECORD +398 -0
- flock_core-0.5.56b0.dist-info/entry_points.txt +2 -0
- {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/licenses/LICENSE +1 -1
- flock/adapter/__init__.py +0 -14
- flock/adapter/azure_adapter.py +0 -68
- flock/adapter/chroma_adapter.py +0 -73
- flock/adapter/faiss_adapter.py +0 -97
- flock/adapter/pinecone_adapter.py +0 -51
- flock/adapter/vector_base.py +0 -47
- flock/cli/assets/release_notes.md +0 -140
- flock/cli/config.py +0 -8
- flock/cli/constants.py +0 -36
- flock/cli/create_agent.py +0 -1
- flock/cli/create_flock.py +0 -280
- flock/cli/execute_flock.py +0 -620
- flock/cli/load_agent.py +0 -1
- flock/cli/load_examples.py +0 -1
- flock/cli/load_flock.py +0 -192
- flock/cli/load_release_notes.py +0 -20
- flock/cli/loaded_flock_cli.py +0 -254
- flock/cli/manage_agents.py +0 -459
- flock/cli/registry_management.py +0 -889
- flock/cli/runner.py +0 -41
- flock/cli/settings.py +0 -857
- flock/cli/utils.py +0 -135
- flock/cli/view_results.py +0 -29
- flock/cli/yaml_editor.py +0 -396
- flock/components/__init__.py +0 -30
- flock/components/evaluation/__init__.py +0 -9
- flock/components/evaluation/declarative_evaluation_component.py +0 -606
- flock/components/routing/__init__.py +0 -15
- flock/components/routing/conditional_routing_component.py +0 -494
- flock/components/routing/default_routing_component.py +0 -103
- flock/components/routing/llm_routing_component.py +0 -206
- flock/components/utility/__init__.py +0 -22
- flock/components/utility/example_utility_component.py +0 -250
- flock/components/utility/feedback_utility_component.py +0 -206
- flock/components/utility/memory_utility_component.py +0 -550
- flock/components/utility/metrics_utility_component.py +0 -700
- flock/config.py +0 -61
- flock/core/__init__.py +0 -110
- flock/core/agent/__init__.py +0 -16
- flock/core/agent/default_agent.py +0 -216
- flock/core/agent/flock_agent_components.py +0 -104
- flock/core/agent/flock_agent_execution.py +0 -101
- flock/core/agent/flock_agent_integration.py +0 -260
- flock/core/agent/flock_agent_lifecycle.py +0 -186
- flock/core/agent/flock_agent_serialization.py +0 -381
- flock/core/api/__init__.py +0 -10
- flock/core/api/custom_endpoint.py +0 -45
- flock/core/api/endpoints.py +0 -254
- flock/core/api/main.py +0 -162
- flock/core/api/models.py +0 -97
- flock/core/api/run_store.py +0 -224
- flock/core/api/runner.py +0 -44
- flock/core/api/service.py +0 -214
- flock/core/component/__init__.py +0 -15
- flock/core/component/agent_component_base.py +0 -309
- flock/core/component/evaluation_component.py +0 -62
- flock/core/component/routing_component.py +0 -74
- flock/core/component/utility_component.py +0 -69
- flock/core/config/flock_agent_config.py +0 -58
- flock/core/config/scheduled_agent_config.py +0 -40
- flock/core/context/context.py +0 -213
- flock/core/context/context_manager.py +0 -37
- flock/core/context/context_vars.py +0 -10
- flock/core/evaluation/utils.py +0 -396
- flock/core/execution/batch_executor.py +0 -369
- flock/core/execution/evaluation_executor.py +0 -438
- flock/core/execution/local_executor.py +0 -31
- flock/core/execution/opik_executor.py +0 -103
- flock/core/execution/temporal_executor.py +0 -164
- flock/core/flock.py +0 -634
- flock/core/flock_agent.py +0 -336
- flock/core/flock_factory.py +0 -613
- flock/core/flock_scheduler.py +0 -166
- flock/core/flock_server_manager.py +0 -136
- flock/core/interpreter/python_interpreter.py +0 -689
- flock/core/mcp/__init__.py +0 -1
- flock/core/mcp/flock_mcp_server.py +0 -680
- flock/core/mcp/mcp_client_manager.py +0 -201
- flock/core/mcp/types/__init__.py +0 -1
- flock/core/mixin/dspy_integration.py +0 -403
- flock/core/mixin/prompt_parser.py +0 -125
- flock/core/orchestration/__init__.py +0 -15
- flock/core/orchestration/flock_batch_processor.py +0 -94
- flock/core/orchestration/flock_evaluator.py +0 -113
- flock/core/orchestration/flock_execution.py +0 -295
- flock/core/orchestration/flock_initialization.py +0 -149
- flock/core/orchestration/flock_server_manager.py +0 -67
- flock/core/orchestration/flock_web_server.py +0 -117
- flock/core/registry/__init__.py +0 -45
- flock/core/registry/agent_registry.py +0 -69
- flock/core/registry/callable_registry.py +0 -139
- flock/core/registry/component_discovery.py +0 -142
- flock/core/registry/component_registry.py +0 -64
- flock/core/registry/config_mapping.py +0 -64
- flock/core/registry/decorators.py +0 -137
- flock/core/registry/registry_hub.py +0 -205
- flock/core/registry/server_registry.py +0 -57
- flock/core/registry/type_registry.py +0 -86
- flock/core/serialization/__init__.py +0 -13
- flock/core/serialization/callable_registry.py +0 -52
- flock/core/serialization/flock_serializer.py +0 -832
- flock/core/serialization/json_encoder.py +0 -41
- flock/core/serialization/secure_serializer.py +0 -175
- flock/core/serialization/serializable.py +0 -342
- flock/core/serialization/serialization_utils.py +0 -412
- flock/core/util/file_path_utils.py +0 -223
- flock/core/util/hydrator.py +0 -309
- flock/core/util/input_resolver.py +0 -164
- flock/core/util/loader.py +0 -59
- flock/core/util/splitter.py +0 -219
- flock/di.py +0 -27
- flock/platform/docker_tools.py +0 -49
- flock/platform/jaeger_install.py +0 -86
- flock/webapp/__init__.py +0 -1
- flock/webapp/app/__init__.py +0 -0
- flock/webapp/app/api/__init__.py +0 -0
- flock/webapp/app/api/agent_management.py +0 -241
- flock/webapp/app/api/execution.py +0 -709
- flock/webapp/app/api/flock_management.py +0 -129
- flock/webapp/app/api/registry_viewer.py +0 -30
- flock/webapp/app/chat.py +0 -665
- flock/webapp/app/config.py +0 -104
- flock/webapp/app/dependencies.py +0 -117
- flock/webapp/app/main.py +0 -1070
- flock/webapp/app/middleware.py +0 -113
- flock/webapp/app/models_ui.py +0 -7
- flock/webapp/app/services/__init__.py +0 -0
- flock/webapp/app/services/feedback_file_service.py +0 -363
- flock/webapp/app/services/flock_service.py +0 -337
- flock/webapp/app/services/sharing_models.py +0 -81
- flock/webapp/app/services/sharing_store.py +0 -762
- flock/webapp/app/templates/theme_mapper.html +0 -326
- flock/webapp/app/theme_mapper.py +0 -812
- flock/webapp/app/utils.py +0 -85
- flock/webapp/run.py +0 -215
- flock/webapp/static/css/chat.css +0 -301
- flock/webapp/static/css/components.css +0 -167
- flock/webapp/static/css/header.css +0 -39
- flock/webapp/static/css/layout.css +0 -46
- flock/webapp/static/css/sidebar.css +0 -127
- flock/webapp/static/css/two-pane.css +0 -48
- flock/webapp/templates/base.html +0 -200
- flock/webapp/templates/chat.html +0 -152
- flock/webapp/templates/chat_settings.html +0 -19
- flock/webapp/templates/flock_editor.html +0 -16
- flock/webapp/templates/index.html +0 -12
- flock/webapp/templates/partials/_agent_detail_form.html +0 -93
- flock/webapp/templates/partials/_agent_list.html +0 -18
- flock/webapp/templates/partials/_agent_manager_view.html +0 -51
- flock/webapp/templates/partials/_agent_tools_checklist.html +0 -14
- flock/webapp/templates/partials/_chat_container.html +0 -15
- flock/webapp/templates/partials/_chat_messages.html +0 -57
- flock/webapp/templates/partials/_chat_settings_form.html +0 -85
- flock/webapp/templates/partials/_create_flock_form.html +0 -50
- flock/webapp/templates/partials/_dashboard_flock_detail.html +0 -17
- flock/webapp/templates/partials/_dashboard_flock_file_list.html +0 -16
- flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +0 -28
- flock/webapp/templates/partials/_dashboard_upload_flock_form.html +0 -16
- flock/webapp/templates/partials/_dynamic_input_form_content.html +0 -22
- flock/webapp/templates/partials/_env_vars_table.html +0 -23
- flock/webapp/templates/partials/_execution_form.html +0 -118
- flock/webapp/templates/partials/_execution_view_container.html +0 -28
- flock/webapp/templates/partials/_flock_file_list.html +0 -23
- flock/webapp/templates/partials/_flock_properties_form.html +0 -52
- flock/webapp/templates/partials/_flock_upload_form.html +0 -16
- flock/webapp/templates/partials/_header_flock_status.html +0 -5
- flock/webapp/templates/partials/_load_manager_view.html +0 -49
- flock/webapp/templates/partials/_registry_table.html +0 -25
- flock/webapp/templates/partials/_registry_viewer_content.html +0 -70
- flock/webapp/templates/partials/_results_display.html +0 -78
- flock/webapp/templates/partials/_settings_env_content.html +0 -9
- flock/webapp/templates/partials/_settings_theme_content.html +0 -14
- flock/webapp/templates/partials/_settings_view.html +0 -36
- flock/webapp/templates/partials/_share_chat_link_snippet.html +0 -11
- flock/webapp/templates/partials/_share_link_snippet.html +0 -35
- flock/webapp/templates/partials/_sidebar.html +0 -74
- flock/webapp/templates/partials/_streaming_results_container.html +0 -195
- flock/webapp/templates/partials/_structured_data_view.html +0 -40
- flock/webapp/templates/partials/_theme_preview.html +0 -36
- flock/webapp/templates/registry_viewer.html +0 -84
- flock/webapp/templates/shared_run_page.html +0 -140
- flock/workflow/__init__.py +0 -0
- flock/workflow/activities.py +0 -196
- flock/workflow/agent_activities.py +0 -24
- flock/workflow/agent_execution_activity.py +0 -202
- flock/workflow/flock_workflow.py +0 -214
- flock/workflow/temporal_config.py +0 -96
- flock/workflow/temporal_setup.py +0 -68
- flock_core-0.5.0b28.dist-info/METADATA +0 -274
- flock_core-0.5.0b28.dist-info/RECORD +0 -561
- flock_core-0.5.0b28.dist-info/entry_points.txt +0 -2
- /flock/{core/logging → logging}/formatters/themes.py +0 -0
- /flock/{core/logging → logging}/span_middleware/baggage_span_processor.py +0 -0
- /flock/{core/mcp → mcp}/util/__init__.py +0 -0
- {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""WebSocket connection manager for real-time dashboard communication.
|
|
2
|
+
|
|
3
|
+
Manages WebSocket client pool, broadcasts events to all connected clients,
|
|
4
|
+
and implements heartbeat/ping mechanism to keep connections alive.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
from collections import defaultdict, deque
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from fastapi import WebSocket
|
|
13
|
+
|
|
14
|
+
from flock.dashboard.events import (
|
|
15
|
+
AgentActivatedEvent,
|
|
16
|
+
AgentCompletedEvent,
|
|
17
|
+
AgentErrorEvent,
|
|
18
|
+
MessagePublishedEvent,
|
|
19
|
+
StreamingOutputEvent,
|
|
20
|
+
)
|
|
21
|
+
from flock.logging.logging import get_logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = get_logger("dashboard.websocket")
|
|
25
|
+
|
|
26
|
+
# Type alias for dashboard events
|
|
27
|
+
DashboardEvent = Union[
|
|
28
|
+
AgentActivatedEvent,
|
|
29
|
+
MessagePublishedEvent,
|
|
30
|
+
StreamingOutputEvent,
|
|
31
|
+
AgentCompletedEvent,
|
|
32
|
+
AgentErrorEvent,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class WebSocketManager:
|
|
37
|
+
"""Manages WebSocket connections and broadcasts dashboard events.
|
|
38
|
+
|
|
39
|
+
Features:
|
|
40
|
+
- Connection pool management (add/remove clients)
|
|
41
|
+
- Broadcast events to all connected clients
|
|
42
|
+
- Heartbeat/ping mechanism (DISABLED by default - causes unnecessary disconnects)
|
|
43
|
+
- Graceful handling of disconnected clients during broadcast
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, heartbeat_interval: int = 120, enable_heartbeat: bool = False):
|
|
47
|
+
"""Initialize WebSocket manager.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
heartbeat_interval: Seconds between heartbeat pings (default: 120)
|
|
51
|
+
enable_heartbeat: Enable heartbeat pings (default: False - disabled to prevent
|
|
52
|
+
unnecessary disconnects. WebSocket auto-reconnects on real network issues.)
|
|
53
|
+
"""
|
|
54
|
+
self.clients: set[WebSocket] = set()
|
|
55
|
+
self.heartbeat_interval = heartbeat_interval
|
|
56
|
+
self.enable_heartbeat = enable_heartbeat
|
|
57
|
+
self._heartbeat_task: asyncio.Task | None = None
|
|
58
|
+
self._shutdown = False
|
|
59
|
+
|
|
60
|
+
# Store streaming output events by agent_name for history (max 128000 per agent)
|
|
61
|
+
self._streaming_history: dict[str, deque[StreamingOutputEvent]] = defaultdict(
|
|
62
|
+
lambda: deque(maxlen=128000)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def add_client(self, websocket: WebSocket) -> None:
|
|
66
|
+
"""Add WebSocket client to connection pool.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
websocket: FastAPI WebSocket connection to add
|
|
70
|
+
"""
|
|
71
|
+
self.clients.add(websocket)
|
|
72
|
+
logger.info(f"WebSocket client added. Total clients: {len(self.clients)}")
|
|
73
|
+
|
|
74
|
+
# Start heartbeat task if enabled and not already running
|
|
75
|
+
if self.enable_heartbeat and self._heartbeat_task is None and not self._shutdown:
|
|
76
|
+
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
|
77
|
+
|
|
78
|
+
async def remove_client(self, websocket: WebSocket) -> None:
|
|
79
|
+
"""Remove WebSocket client from connection pool.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
websocket: FastAPI WebSocket connection to remove
|
|
83
|
+
"""
|
|
84
|
+
self.clients.discard(websocket)
|
|
85
|
+
logger.info(f"WebSocket client removed. Total clients: {len(self.clients)}")
|
|
86
|
+
|
|
87
|
+
# Stop heartbeat task if no clients remain
|
|
88
|
+
if len(self.clients) == 0 and self._heartbeat_task is not None:
|
|
89
|
+
self._heartbeat_task.cancel()
|
|
90
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
91
|
+
await self._heartbeat_task
|
|
92
|
+
self._heartbeat_task = None
|
|
93
|
+
|
|
94
|
+
async def broadcast(self, event: DashboardEvent) -> None:
|
|
95
|
+
"""Broadcast event to all connected clients as JSON.
|
|
96
|
+
|
|
97
|
+
Handles disconnected clients gracefully by removing them from pool.
|
|
98
|
+
Uses return_exceptions=True to prevent one client failure from affecting others.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
event: Dashboard event to broadcast (AgentActivatedEvent, etc.)
|
|
102
|
+
"""
|
|
103
|
+
# Store streaming output events for history (always, even if no clients)
|
|
104
|
+
if isinstance(event, StreamingOutputEvent):
|
|
105
|
+
self._streaming_history[event.agent_name].append(event)
|
|
106
|
+
logger.debug(
|
|
107
|
+
f"Stored streaming event for {event.agent_name}, history size: {len(self._streaming_history[event.agent_name])}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# If no clients, still log but don't broadcast
|
|
111
|
+
if not self.clients:
|
|
112
|
+
logger.debug(
|
|
113
|
+
f"No clients connected, stored event but skipping broadcast of {type(event).__name__}"
|
|
114
|
+
)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Log broadcast attempt
|
|
118
|
+
logger.debug(f"Broadcasting {type(event).__name__} to {len(self.clients)} client(s)")
|
|
119
|
+
|
|
120
|
+
# Serialize event to JSON using Pydantic's model_dump_json
|
|
121
|
+
message = event.model_dump_json()
|
|
122
|
+
logger.debug(f"Event JSON: {message[:200]}...") # Log first 200 chars
|
|
123
|
+
|
|
124
|
+
# Broadcast to all clients concurrently
|
|
125
|
+
# Use return_exceptions=True to handle client failures gracefully
|
|
126
|
+
# Use send_text() for FastAPI WebSocket (send JSON string as text)
|
|
127
|
+
clients_list = list(self.clients) # Copy to avoid modification during iteration
|
|
128
|
+
send_tasks = [client.send_text(message) for client in clients_list]
|
|
129
|
+
results = await asyncio.gather(*send_tasks, return_exceptions=True)
|
|
130
|
+
|
|
131
|
+
# Remove clients that failed to receive the message
|
|
132
|
+
failed_clients = []
|
|
133
|
+
for client, result in zip(clients_list, results, strict=False):
|
|
134
|
+
if isinstance(result, Exception):
|
|
135
|
+
logger.warning(f"Failed to send to client: {result}")
|
|
136
|
+
failed_clients.append(client)
|
|
137
|
+
|
|
138
|
+
# Clean up failed clients
|
|
139
|
+
for client in failed_clients:
|
|
140
|
+
await self.remove_client(client)
|
|
141
|
+
|
|
142
|
+
async def _heartbeat_loop(self) -> None:
|
|
143
|
+
"""Send ping to all clients every heartbeat_interval seconds.
|
|
144
|
+
|
|
145
|
+
Keeps WebSocket connections alive and detects disconnected clients.
|
|
146
|
+
Runs continuously until cancelled or all clients disconnect.
|
|
147
|
+
"""
|
|
148
|
+
logger.info(f"Starting heartbeat loop (interval: {self.heartbeat_interval}s)")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
while not self._shutdown and len(self.clients) > 0:
|
|
152
|
+
await asyncio.sleep(self.heartbeat_interval)
|
|
153
|
+
|
|
154
|
+
if not self.clients:
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Send ping to all clients
|
|
158
|
+
ping_tasks = []
|
|
159
|
+
for client in list(self.clients): # Copy to avoid modification during iteration
|
|
160
|
+
ping_tasks.append(self._ping_client(client))
|
|
161
|
+
|
|
162
|
+
# Execute pings concurrently
|
|
163
|
+
await asyncio.gather(*ping_tasks, return_exceptions=True)
|
|
164
|
+
|
|
165
|
+
except asyncio.CancelledError:
|
|
166
|
+
logger.info("Heartbeat loop cancelled")
|
|
167
|
+
raise
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.exception(f"Heartbeat loop error: {e}")
|
|
170
|
+
|
|
171
|
+
async def _ping_client(self, client: WebSocket) -> None:
|
|
172
|
+
"""Send ping to single client.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
client: WebSocket client to ping
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
await client.send_json({"type": "ping", "timestamp": asyncio.get_event_loop().time()})
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(f"Failed to ping client: {e}")
|
|
181
|
+
await self.remove_client(client)
|
|
182
|
+
|
|
183
|
+
async def start_heartbeat(self) -> None:
|
|
184
|
+
"""Start heartbeat task manually (for testing).
|
|
185
|
+
|
|
186
|
+
In production, heartbeat is disabled by default (enable_heartbeat=False).
|
|
187
|
+
Only starts if enable_heartbeat=True.
|
|
188
|
+
"""
|
|
189
|
+
if self.enable_heartbeat and self._heartbeat_task is None and not self._shutdown:
|
|
190
|
+
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
|
191
|
+
|
|
192
|
+
async def shutdown(self) -> None:
|
|
193
|
+
"""Shutdown manager and close all WebSocket connections.
|
|
194
|
+
|
|
195
|
+
Cancels heartbeat task and closes all client connections gracefully.
|
|
196
|
+
"""
|
|
197
|
+
logger.info("Shutting down WebSocketManager")
|
|
198
|
+
self._shutdown = True
|
|
199
|
+
|
|
200
|
+
# Cancel heartbeat task
|
|
201
|
+
if self._heartbeat_task is not None:
|
|
202
|
+
self._heartbeat_task.cancel()
|
|
203
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
204
|
+
await self._heartbeat_task
|
|
205
|
+
self._heartbeat_task = None
|
|
206
|
+
|
|
207
|
+
# Close all client connections
|
|
208
|
+
close_tasks = []
|
|
209
|
+
for client in list(self.clients):
|
|
210
|
+
# Handle both real WebSocket and mock objects
|
|
211
|
+
if hasattr(client, "close") and callable(client.close):
|
|
212
|
+
result = client.close()
|
|
213
|
+
# Only await if it's a coroutine
|
|
214
|
+
if asyncio.iscoroutine(result):
|
|
215
|
+
close_tasks.append(result)
|
|
216
|
+
|
|
217
|
+
if close_tasks:
|
|
218
|
+
await asyncio.gather(*close_tasks, return_exceptions=True)
|
|
219
|
+
|
|
220
|
+
self.clients.clear()
|
|
221
|
+
logger.info("WebSocketManager shutdown complete")
|
|
222
|
+
|
|
223
|
+
def get_streaming_history(self, agent_name: str) -> list[StreamingOutputEvent]:
|
|
224
|
+
"""Get historical streaming output events for a specific agent.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
agent_name: Name of the agent to get history for
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of StreamingOutputEvent events for the agent
|
|
231
|
+
"""
|
|
232
|
+
return list(self._streaming_history.get(agent_name, []))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
__all__ = ["DashboardEvent", "WebSocketManager"]
|