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,283 @@
|
|
|
1
|
+
"""DashboardEventCollector - captures agent lifecycle events for real-time dashboard.
|
|
2
|
+
|
|
3
|
+
This component hooks into the agent execution lifecycle to emit WebSocket events.
|
|
4
|
+
Phase 1: Events stored in in-memory buffer (max 100 events).
|
|
5
|
+
Phase 3: Extended to emit via WebSocket using WebSocketManager.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import traceback
|
|
9
|
+
from collections import deque
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import TYPE_CHECKING, Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import PrivateAttr
|
|
14
|
+
|
|
15
|
+
from flock.components import AgentComponent
|
|
16
|
+
from flock.dashboard.events import (
|
|
17
|
+
AgentActivatedEvent,
|
|
18
|
+
AgentCompletedEvent,
|
|
19
|
+
AgentErrorEvent,
|
|
20
|
+
MessagePublishedEvent,
|
|
21
|
+
SubscriptionInfo,
|
|
22
|
+
VisibilitySpec,
|
|
23
|
+
)
|
|
24
|
+
from flock.logging.logging import get_logger
|
|
25
|
+
from flock.runtime import Context
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger = get_logger("dashboard.collector")
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING: # pragma: no cover - type hints only
|
|
31
|
+
from flock.agent import Agent
|
|
32
|
+
from flock.artifacts import Artifact
|
|
33
|
+
from flock.dashboard.websocket import WebSocketManager
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DashboardEventCollector(AgentComponent):
|
|
37
|
+
"""Collects agent lifecycle events for dashboard visualization.
|
|
38
|
+
|
|
39
|
+
Implements AgentComponent interface to hook into agent execution:
|
|
40
|
+
- on_pre_consume: emits agent_activated
|
|
41
|
+
- on_post_publish: emits message_published
|
|
42
|
+
- on_terminate: emits agent_completed
|
|
43
|
+
- on_error: emits agent_error
|
|
44
|
+
|
|
45
|
+
Phase 1: Events stored in in-memory deque (max 100, LRU eviction).
|
|
46
|
+
Phase 3: Emits events via WebSocket using WebSocketManager.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Use PrivateAttr for non-Pydantic fields (AgentComponent extends BaseModel)
|
|
50
|
+
_events: deque[
|
|
51
|
+
AgentActivatedEvent | MessagePublishedEvent | AgentCompletedEvent | AgentErrorEvent
|
|
52
|
+
] = PrivateAttr(default=None)
|
|
53
|
+
|
|
54
|
+
# Track run start times for duration calculation
|
|
55
|
+
_run_start_times: dict[str, float] = PrivateAttr(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
# WebSocketManager for broadcasting events
|
|
58
|
+
_websocket_manager: Optional["WebSocketManager"] = PrivateAttr(default=None)
|
|
59
|
+
|
|
60
|
+
def __init__(self, **data):
|
|
61
|
+
super().__init__(**data)
|
|
62
|
+
# In-memory buffer with max 100 events (LRU eviction)
|
|
63
|
+
self._events = deque(maxlen=100)
|
|
64
|
+
self._run_start_times = {}
|
|
65
|
+
self._websocket_manager = None
|
|
66
|
+
|
|
67
|
+
def set_websocket_manager(self, manager: "WebSocketManager") -> None:
|
|
68
|
+
"""Set WebSocketManager for broadcasting events.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
manager: WebSocketManager instance to use for broadcasting
|
|
72
|
+
"""
|
|
73
|
+
self._websocket_manager = manager
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def events(self) -> deque:
|
|
77
|
+
"""Access events buffer."""
|
|
78
|
+
return self._events
|
|
79
|
+
|
|
80
|
+
async def on_pre_consume(
|
|
81
|
+
self, agent: "Agent", ctx: Context, inputs: list["Artifact"]
|
|
82
|
+
) -> list["Artifact"]:
|
|
83
|
+
"""Emit agent_activated event when agent begins consuming.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
agent: The agent that is consuming
|
|
87
|
+
ctx: Execution context with correlation_id
|
|
88
|
+
inputs: Artifacts being consumed
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Unmodified inputs (pass-through)
|
|
92
|
+
"""
|
|
93
|
+
# Record start time for duration calculation
|
|
94
|
+
self._run_start_times[ctx.task_id] = datetime.now(timezone.utc).timestamp()
|
|
95
|
+
|
|
96
|
+
# Extract consumed types and artifact IDs
|
|
97
|
+
consumed_types = list({artifact.type for artifact in inputs})
|
|
98
|
+
consumed_artifacts = [str(artifact.id) for artifact in inputs]
|
|
99
|
+
|
|
100
|
+
# Extract produced types from agent outputs
|
|
101
|
+
produced_types = [output.spec.type_name for output in agent.outputs]
|
|
102
|
+
|
|
103
|
+
# Build subscription info from agent's subscriptions
|
|
104
|
+
subscription_info = SubscriptionInfo(from_agents=[], channels=[], mode="both")
|
|
105
|
+
|
|
106
|
+
if agent.subscriptions:
|
|
107
|
+
# Get first subscription's config (agents typically have one)
|
|
108
|
+
sub = agent.subscriptions[0]
|
|
109
|
+
subscription_info.from_agents = list(sub.from_agents) if sub.from_agents else []
|
|
110
|
+
subscription_info.channels = list(sub.channels) if sub.channels else []
|
|
111
|
+
subscription_info.mode = sub.mode
|
|
112
|
+
|
|
113
|
+
# Create and store event
|
|
114
|
+
event = AgentActivatedEvent(
|
|
115
|
+
correlation_id=str(ctx.correlation_id) if ctx.correlation_id else "",
|
|
116
|
+
agent_name=agent.name,
|
|
117
|
+
agent_id=agent.name,
|
|
118
|
+
run_id=ctx.task_id, # Unique ID for this agent run
|
|
119
|
+
consumed_types=consumed_types,
|
|
120
|
+
consumed_artifacts=consumed_artifacts,
|
|
121
|
+
produced_types=produced_types,
|
|
122
|
+
subscription_info=subscription_info,
|
|
123
|
+
labels=list(agent.labels),
|
|
124
|
+
tenant_id=agent.tenant_id,
|
|
125
|
+
max_concurrency=agent.max_concurrency,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self._events.append(event)
|
|
129
|
+
logger.info(f"Agent activated: {agent.name} (correlation_id={event.correlation_id})")
|
|
130
|
+
|
|
131
|
+
# Broadcast via WebSocket if manager is configured
|
|
132
|
+
if self._websocket_manager:
|
|
133
|
+
await self._websocket_manager.broadcast(event)
|
|
134
|
+
else:
|
|
135
|
+
logger.warning("WebSocket manager not configured, event not broadcast")
|
|
136
|
+
|
|
137
|
+
return inputs
|
|
138
|
+
|
|
139
|
+
async def on_post_publish(self, agent: "Agent", ctx: Context, artifact: "Artifact") -> None:
|
|
140
|
+
"""Emit message_published event when artifact is published.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
agent: The agent that published the artifact
|
|
144
|
+
ctx: Execution context with correlation_id
|
|
145
|
+
artifact: The published artifact
|
|
146
|
+
"""
|
|
147
|
+
# Convert visibility to VisibilitySpec
|
|
148
|
+
visibility_spec = self._convert_visibility(artifact.visibility)
|
|
149
|
+
|
|
150
|
+
# Create and store event
|
|
151
|
+
event = MessagePublishedEvent(
|
|
152
|
+
correlation_id=str(ctx.correlation_id) if ctx.correlation_id else "",
|
|
153
|
+
artifact_id=str(artifact.id),
|
|
154
|
+
artifact_type=artifact.type,
|
|
155
|
+
produced_by=artifact.produced_by,
|
|
156
|
+
payload=artifact.payload,
|
|
157
|
+
visibility=visibility_spec,
|
|
158
|
+
tags=list(artifact.tags) if artifact.tags else [],
|
|
159
|
+
partition_key=artifact.partition_key,
|
|
160
|
+
version=artifact.version,
|
|
161
|
+
consumers=[], # Phase 1: empty, Phase 3: compute from subscription matching
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self._events.append(event)
|
|
165
|
+
logger.info(
|
|
166
|
+
f"Message published: {artifact.type} by {artifact.produced_by} (correlation_id={event.correlation_id})"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Broadcast via WebSocket if manager is configured
|
|
170
|
+
if self._websocket_manager:
|
|
171
|
+
await self._websocket_manager.broadcast(event)
|
|
172
|
+
else:
|
|
173
|
+
logger.warning("WebSocket manager not configured, event not broadcast")
|
|
174
|
+
|
|
175
|
+
async def on_terminate(self, agent: "Agent", ctx: Context) -> None:
|
|
176
|
+
"""Emit agent_completed event when agent finishes successfully.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
agent: The agent that completed
|
|
180
|
+
ctx: Execution context with final state
|
|
181
|
+
"""
|
|
182
|
+
# Calculate duration
|
|
183
|
+
start_time = self._run_start_times.get(ctx.task_id)
|
|
184
|
+
if start_time:
|
|
185
|
+
duration_ms = (datetime.now(timezone.utc).timestamp() - start_time) * 1000
|
|
186
|
+
del self._run_start_times[ctx.task_id]
|
|
187
|
+
else:
|
|
188
|
+
duration_ms = 0.0
|
|
189
|
+
|
|
190
|
+
# Extract artifacts produced from context state (if tracked)
|
|
191
|
+
artifacts_produced = ctx.state.get("artifacts_produced", [])
|
|
192
|
+
if not isinstance(artifacts_produced, list):
|
|
193
|
+
artifacts_produced = []
|
|
194
|
+
|
|
195
|
+
# Extract metrics from context state (if tracked)
|
|
196
|
+
metrics = ctx.state.get("metrics", {})
|
|
197
|
+
if not isinstance(metrics, dict):
|
|
198
|
+
metrics = {}
|
|
199
|
+
|
|
200
|
+
# Create and store event
|
|
201
|
+
event = AgentCompletedEvent(
|
|
202
|
+
correlation_id=str(ctx.correlation_id) if ctx.correlation_id else "",
|
|
203
|
+
agent_name=agent.name,
|
|
204
|
+
run_id=ctx.task_id,
|
|
205
|
+
duration_ms=duration_ms,
|
|
206
|
+
artifacts_produced=artifacts_produced,
|
|
207
|
+
metrics=metrics,
|
|
208
|
+
final_state=dict(ctx.state),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
self._events.append(event)
|
|
212
|
+
|
|
213
|
+
# Broadcast via WebSocket if manager is configured
|
|
214
|
+
if self._websocket_manager:
|
|
215
|
+
await self._websocket_manager.broadcast(event)
|
|
216
|
+
|
|
217
|
+
async def on_error(self, agent: "Agent", ctx: Context, error: Exception) -> None:
|
|
218
|
+
"""Emit agent_error event when agent execution fails.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
agent: The agent that failed
|
|
222
|
+
ctx: Execution context
|
|
223
|
+
error: The exception that was raised
|
|
224
|
+
"""
|
|
225
|
+
# Get error details
|
|
226
|
+
error_type = type(error).__name__
|
|
227
|
+
error_message = str(error)
|
|
228
|
+
# Use traceback.format_exception to get traceback from exception object
|
|
229
|
+
error_traceback = "".join(
|
|
230
|
+
traceback.format_exception(type(error), error, error.__traceback__)
|
|
231
|
+
)
|
|
232
|
+
failed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
233
|
+
|
|
234
|
+
# Clean up start time tracking
|
|
235
|
+
if ctx.task_id in self._run_start_times:
|
|
236
|
+
del self._run_start_times[ctx.task_id]
|
|
237
|
+
|
|
238
|
+
# Create and store event
|
|
239
|
+
event = AgentErrorEvent(
|
|
240
|
+
correlation_id=str(ctx.correlation_id) if ctx.correlation_id else "",
|
|
241
|
+
agent_name=agent.name,
|
|
242
|
+
run_id=ctx.task_id,
|
|
243
|
+
error_type=error_type,
|
|
244
|
+
error_message=error_message,
|
|
245
|
+
traceback=error_traceback,
|
|
246
|
+
failed_at=failed_at,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self._events.append(event)
|
|
250
|
+
|
|
251
|
+
# Broadcast via WebSocket if manager is configured
|
|
252
|
+
if self._websocket_manager:
|
|
253
|
+
await self._websocket_manager.broadcast(event)
|
|
254
|
+
|
|
255
|
+
def _convert_visibility(self, visibility) -> VisibilitySpec:
|
|
256
|
+
"""Convert flock.visibility.Visibility to VisibilitySpec.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
visibility: Visibility object from artifact
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
VisibilitySpec for event serialization
|
|
263
|
+
"""
|
|
264
|
+
# Get visibility kind from class name, stripping "Visibility" suffix
|
|
265
|
+
class_name = type(visibility).__name__
|
|
266
|
+
kind = class_name[: -len("Visibility")] if class_name.endswith("Visibility") else class_name
|
|
267
|
+
|
|
268
|
+
spec = VisibilitySpec(kind=kind)
|
|
269
|
+
|
|
270
|
+
# Extract type-specific fields
|
|
271
|
+
if kind == "Private":
|
|
272
|
+
spec.agents = list(visibility.agents) if hasattr(visibility, "agents") else []
|
|
273
|
+
elif kind == "Labelled":
|
|
274
|
+
spec.required_labels = (
|
|
275
|
+
list(visibility.required_labels) if hasattr(visibility, "required_labels") else []
|
|
276
|
+
)
|
|
277
|
+
elif kind == "Tenant":
|
|
278
|
+
spec.tenant_id = visibility.tenant_id if hasattr(visibility, "tenant_id") else None
|
|
279
|
+
|
|
280
|
+
return spec
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
__all__ = ["DashboardEventCollector"]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Event models for real-time dashboard.
|
|
2
|
+
|
|
3
|
+
Defines 5 core event types that capture agent execution lifecycle.
|
|
4
|
+
All schemas match DATA_MODEL.md specification exactly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SubscriptionInfo(BaseModel):
|
|
14
|
+
"""Subscription configuration for an agent."""
|
|
15
|
+
|
|
16
|
+
from_agents: list[str] = Field(default_factory=list)
|
|
17
|
+
channels: list[str] = Field(default_factory=list)
|
|
18
|
+
mode: str = "both" # "both" | "events" | "direct"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VisibilitySpec(BaseModel):
|
|
22
|
+
"""Visibility specification for artifacts.
|
|
23
|
+
|
|
24
|
+
Matches visibility types from flock.visibility module.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
kind: str # "Public" | "Private" | "Labelled" | "Tenant" | "After"
|
|
28
|
+
agents: list[str] | None = None # For Private
|
|
29
|
+
required_labels: list[str] | None = None # For Labelled
|
|
30
|
+
tenant_id: str | None = None # For Tenant
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentActivatedEvent(BaseModel):
|
|
34
|
+
"""Event emitted when agent begins consuming artifacts.
|
|
35
|
+
|
|
36
|
+
Corresponds to on_pre_consume lifecycle hook.
|
|
37
|
+
Schema per DATA_MODEL.md lines 53-66.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Event metadata
|
|
41
|
+
correlation_id: str
|
|
42
|
+
timestamp: str = Field(
|
|
43
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Agent identification
|
|
47
|
+
agent_name: str
|
|
48
|
+
agent_id: str # Same as agent.name (unique per orchestrator)
|
|
49
|
+
run_id: str # Context.task_id (unique per agent activation)
|
|
50
|
+
|
|
51
|
+
# Consumption info
|
|
52
|
+
consumed_types: list[str] # Artifact types being consumed
|
|
53
|
+
consumed_artifacts: list[str] # Artifact IDs being consumed
|
|
54
|
+
|
|
55
|
+
# Production info
|
|
56
|
+
produced_types: list[str] # Artifact types this agent can produce
|
|
57
|
+
|
|
58
|
+
# Subscription configuration
|
|
59
|
+
subscription_info: SubscriptionInfo
|
|
60
|
+
|
|
61
|
+
# Agent metadata
|
|
62
|
+
labels: list[str]
|
|
63
|
+
tenant_id: str | None = None
|
|
64
|
+
max_concurrency: int = 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MessagePublishedEvent(BaseModel):
|
|
68
|
+
"""Event emitted when artifact is published to blackboard.
|
|
69
|
+
|
|
70
|
+
Corresponds to on_post_publish lifecycle hook.
|
|
71
|
+
Schema per DATA_MODEL.md lines 100-115.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Event metadata
|
|
75
|
+
correlation_id: str
|
|
76
|
+
timestamp: str = Field(
|
|
77
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Artifact identification
|
|
81
|
+
artifact_id: str # UUID
|
|
82
|
+
artifact_type: str # "Movie", "Tagline", etc.
|
|
83
|
+
|
|
84
|
+
# Provenance
|
|
85
|
+
produced_by: str # agent.name or "external"
|
|
86
|
+
|
|
87
|
+
# Content
|
|
88
|
+
payload: dict[str, Any] # Full artifact.payload
|
|
89
|
+
|
|
90
|
+
# Access control
|
|
91
|
+
visibility: VisibilitySpec
|
|
92
|
+
tags: list[str] = Field(default_factory=list)
|
|
93
|
+
partition_key: str | None = None
|
|
94
|
+
version: int = 1
|
|
95
|
+
|
|
96
|
+
# Flow tracking (Phase 1: empty, Phase 3: computed from subscription matching)
|
|
97
|
+
consumers: list[str] = Field(default_factory=list) # [agent.name]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class StreamingOutputEvent(BaseModel):
|
|
101
|
+
"""Event emitted when agent generates LLM tokens or logs.
|
|
102
|
+
|
|
103
|
+
For Phase 1: This is optional and not fully implemented.
|
|
104
|
+
Schema per DATA_MODEL.md lines 152-159.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Event metadata
|
|
108
|
+
correlation_id: str
|
|
109
|
+
timestamp: str = Field(
|
|
110
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Agent identification
|
|
114
|
+
agent_name: str
|
|
115
|
+
run_id: str # Context.task_id
|
|
116
|
+
|
|
117
|
+
# Output info
|
|
118
|
+
output_type: str # "llm_token" | "log" | "stdout" | "stderr"
|
|
119
|
+
content: str # Token text or log line
|
|
120
|
+
sequence: int # Monotonic sequence for ordering
|
|
121
|
+
is_final: bool = False # True when agent completes this output stream
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AgentCompletedEvent(BaseModel):
|
|
125
|
+
"""Event emitted when agent execution finishes successfully.
|
|
126
|
+
|
|
127
|
+
Corresponds to on_terminate lifecycle hook.
|
|
128
|
+
Schema per DATA_MODEL.md lines 205-212.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
# Event metadata
|
|
132
|
+
correlation_id: str
|
|
133
|
+
timestamp: str = Field(
|
|
134
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Agent identification
|
|
138
|
+
agent_name: str
|
|
139
|
+
run_id: str # Context.task_id
|
|
140
|
+
|
|
141
|
+
# Execution metrics
|
|
142
|
+
duration_ms: float # Execution time in milliseconds
|
|
143
|
+
artifacts_produced: list[str] = Field(default_factory=list) # [artifact_id]
|
|
144
|
+
|
|
145
|
+
# Metrics and state
|
|
146
|
+
metrics: dict[str, Any] = Field(default_factory=dict) # {"tokens_used": 1234, "cost": 0.05}
|
|
147
|
+
final_state: dict[str, Any] = Field(default_factory=dict) # Context.state snapshot
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AgentErrorEvent(BaseModel):
|
|
151
|
+
"""Event emitted when agent execution fails.
|
|
152
|
+
|
|
153
|
+
Corresponds to on_error lifecycle hook.
|
|
154
|
+
Schema per DATA_MODEL.md lines 247-253.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
# Event metadata
|
|
158
|
+
correlation_id: str
|
|
159
|
+
timestamp: str = Field(
|
|
160
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Agent identification
|
|
164
|
+
agent_name: str
|
|
165
|
+
run_id: str # Context.task_id
|
|
166
|
+
|
|
167
|
+
# Error details
|
|
168
|
+
error_type: str # Exception class name
|
|
169
|
+
error_message: str # Exception message
|
|
170
|
+
traceback: str # Full Python traceback
|
|
171
|
+
failed_at: str # ISO timestamp of failure
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
__all__ = [
|
|
175
|
+
"AgentActivatedEvent",
|
|
176
|
+
"AgentCompletedEvent",
|
|
177
|
+
"AgentErrorEvent",
|
|
178
|
+
"MessagePublishedEvent",
|
|
179
|
+
"StreamingOutputEvent",
|
|
180
|
+
"SubscriptionInfo",
|
|
181
|
+
"VisibilitySpec",
|
|
182
|
+
]
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""DashboardLauncher - manages npm lifecycle and browser launch for the dashboard.
|
|
2
|
+
|
|
3
|
+
This module handles:
|
|
4
|
+
- npm dependency installation (first run)
|
|
5
|
+
- npm dev server (DASHBOARD_DEV=1) or production build
|
|
6
|
+
- Automatic browser launch
|
|
7
|
+
- Process cleanup on shutdown
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import webbrowser
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Frontend directory location (adjacent to this dashboard package)
|
|
19
|
+
FRONTEND_DIR = Path(__file__).parent.parent.parent.parent / "frontend"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DashboardLauncher:
|
|
23
|
+
"""Manages dashboard frontend lifecycle.
|
|
24
|
+
|
|
25
|
+
Responsibilities:
|
|
26
|
+
- Ensure npm dependencies installed
|
|
27
|
+
- Start npm dev server (dev mode) or build for production
|
|
28
|
+
- Launch browser automatically
|
|
29
|
+
- Clean up npm processes on shutdown
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
launcher = DashboardLauncher(port=8000)
|
|
33
|
+
launcher.start() # Starts npm and opens browser
|
|
34
|
+
# ... orchestrator runs ...
|
|
35
|
+
launcher.stop() # Cleanup
|
|
36
|
+
|
|
37
|
+
Or as context manager:
|
|
38
|
+
with DashboardLauncher(port=8000):
|
|
39
|
+
# orchestrator.serve() runs
|
|
40
|
+
pass # Automatically cleaned up
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, port: int = 8000, frontend_dir: Path | None = None):
|
|
44
|
+
"""Initialize dashboard launcher.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
port: HTTP port where dashboard will be served (default: 8000)
|
|
48
|
+
frontend_dir: Optional frontend directory path (defaults to FRONTEND_DIR)
|
|
49
|
+
"""
|
|
50
|
+
self.port = port
|
|
51
|
+
self.frontend_dir = frontend_dir or FRONTEND_DIR
|
|
52
|
+
self.dev_mode = os.getenv("DASHBOARD_DEV", "0") == "1"
|
|
53
|
+
self._npm_process: subprocess.Popen | None = None
|
|
54
|
+
|
|
55
|
+
def _ensure_npm_dependencies(self) -> None:
|
|
56
|
+
"""Ensure npm dependencies are installed.
|
|
57
|
+
|
|
58
|
+
Runs 'npm install'.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
print(f"[Dashboard] Installing npm dependencies in {self.frontend_dir}...")
|
|
62
|
+
try:
|
|
63
|
+
subprocess.run(
|
|
64
|
+
[self._get_npm_command(), "install"],
|
|
65
|
+
cwd=self.frontend_dir,
|
|
66
|
+
check=True,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
print("[Dashboard] npm dependencies installed successfully")
|
|
71
|
+
except subprocess.CalledProcessError as e:
|
|
72
|
+
print(f"[Dashboard] Warning: npm install failed: {e.stderr}")
|
|
73
|
+
print("[Dashboard] Continuing anyway, dashboard may not work correctly")
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
print("[Dashboard] Error: npm not found. Please install Node.js and npm.")
|
|
76
|
+
print("[Dashboard] Dashboard will not be available.")
|
|
77
|
+
|
|
78
|
+
def _get_npm_command(self) -> str:
|
|
79
|
+
"""Get npm command (npm or npm.cmd on Windows)."""
|
|
80
|
+
return "npm.cmd" if sys.platform == "win32" else "npm"
|
|
81
|
+
|
|
82
|
+
def _start_npm_process(self) -> None:
|
|
83
|
+
"""Start npm dev server or production build based on mode."""
|
|
84
|
+
if self.dev_mode:
|
|
85
|
+
self._start_dev_server()
|
|
86
|
+
else:
|
|
87
|
+
self._build_production()
|
|
88
|
+
|
|
89
|
+
def _start_dev_server(self) -> None:
|
|
90
|
+
"""Start npm dev server for hot-reload development."""
|
|
91
|
+
print(f"[Dashboard] Starting dev server (DASHBOARD_DEV=1) on port {self.port}...")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
self._npm_process = subprocess.Popen(
|
|
95
|
+
[self._get_npm_command(), "run", "dev"],
|
|
96
|
+
cwd=self.frontend_dir,
|
|
97
|
+
stdout=subprocess.PIPE,
|
|
98
|
+
stderr=subprocess.PIPE,
|
|
99
|
+
text=True,
|
|
100
|
+
)
|
|
101
|
+
print(f"[Dashboard] Dev server started (PID: {self._npm_process.pid})")
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
print("[Dashboard] Error: npm not found. Dev server not started.")
|
|
104
|
+
|
|
105
|
+
def _build_production(self) -> None:
|
|
106
|
+
"""Build frontend for production (static files)."""
|
|
107
|
+
print("[Dashboard] Building frontend for production...")
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
subprocess.run(
|
|
111
|
+
[self._get_npm_command(), "run", "build"],
|
|
112
|
+
cwd=self.frontend_dir,
|
|
113
|
+
check=True,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
)
|
|
117
|
+
print("[Dashboard] Production build completed")
|
|
118
|
+
|
|
119
|
+
# Copy build output from frontend/dist to src/flock/dashboard/static
|
|
120
|
+
self._copy_build_output()
|
|
121
|
+
|
|
122
|
+
except subprocess.CalledProcessError as e:
|
|
123
|
+
print(f"[Dashboard] Warning: Production build failed: {e.stderr}")
|
|
124
|
+
print("[Dashboard] Dashboard may not be available")
|
|
125
|
+
except FileNotFoundError:
|
|
126
|
+
print("[Dashboard] Error: npm not found. Build skipped.")
|
|
127
|
+
|
|
128
|
+
def _copy_build_output(self) -> None:
|
|
129
|
+
"""Copy built frontend files from frontend/dist to dashboard/static."""
|
|
130
|
+
import shutil
|
|
131
|
+
|
|
132
|
+
source_dir = self.frontend_dir / "dist"
|
|
133
|
+
# Dashboard directory is src/flock/dashboard
|
|
134
|
+
target_dir = Path(__file__).parent / "static"
|
|
135
|
+
|
|
136
|
+
if not source_dir.exists():
|
|
137
|
+
print(f"[Dashboard] Warning: Build output not found at {source_dir}")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Remove old static files if they exist
|
|
141
|
+
if target_dir.exists():
|
|
142
|
+
shutil.rmtree(target_dir)
|
|
143
|
+
|
|
144
|
+
# Copy dist to static
|
|
145
|
+
print(f"[Dashboard] Copying build output from {source_dir} to {target_dir}")
|
|
146
|
+
shutil.copytree(source_dir, target_dir)
|
|
147
|
+
print(f"[Dashboard] Static files ready at {target_dir}")
|
|
148
|
+
|
|
149
|
+
def _launch_browser(self) -> None:
|
|
150
|
+
"""Launch browser to dashboard URL after brief delay.
|
|
151
|
+
|
|
152
|
+
Waits 2 seconds to allow server to start before opening browser.
|
|
153
|
+
Catches errors gracefully (e.g., headless environments).
|
|
154
|
+
"""
|
|
155
|
+
# Wait for server to start
|
|
156
|
+
time.sleep(2)
|
|
157
|
+
|
|
158
|
+
dashboard_url = f"http://localhost:{self.port}"
|
|
159
|
+
print(f"[Dashboard] Opening browser to {dashboard_url}...")
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
webbrowser.open(dashboard_url)
|
|
163
|
+
print("[Dashboard] Browser launched successfully")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"[Dashboard] Could not launch browser: {e}")
|
|
166
|
+
print(f"[Dashboard] Please open {dashboard_url} manually")
|
|
167
|
+
|
|
168
|
+
def start(self) -> None:
|
|
169
|
+
"""Start dashboard: install deps, start npm, launch browser.
|
|
170
|
+
|
|
171
|
+
This method:
|
|
172
|
+
1. Ensures npm dependencies are installed
|
|
173
|
+
2. Starts npm dev server (dev mode) or builds production
|
|
174
|
+
3. Launches browser to dashboard URL
|
|
175
|
+
"""
|
|
176
|
+
print(f"[Dashboard] Starting dashboard on port {self.port}")
|
|
177
|
+
print(f"[Dashboard] Mode: {'DEVELOPMENT' if self.dev_mode else 'PRODUCTION'}")
|
|
178
|
+
print(f"[Dashboard] Frontend directory: {self.frontend_dir}")
|
|
179
|
+
|
|
180
|
+
# Step 1: Ensure dependencies installed
|
|
181
|
+
self._ensure_npm_dependencies()
|
|
182
|
+
|
|
183
|
+
# Step 2: Start npm process
|
|
184
|
+
self._start_npm_process()
|
|
185
|
+
|
|
186
|
+
# Step 3: Launch browser
|
|
187
|
+
self._launch_browser()
|
|
188
|
+
|
|
189
|
+
def stop(self) -> None:
|
|
190
|
+
"""Stop dashboard and cleanup npm processes.
|
|
191
|
+
|
|
192
|
+
Attempts graceful termination, falls back to kill if needed.
|
|
193
|
+
"""
|
|
194
|
+
if self._npm_process is None:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
print("[Dashboard] Stopping npm process...")
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Try graceful termination first
|
|
201
|
+
self._npm_process.terminate()
|
|
202
|
+
|
|
203
|
+
# Wait up to 5 seconds for process to exit
|
|
204
|
+
for _ in range(5):
|
|
205
|
+
if self._npm_process.poll() is not None:
|
|
206
|
+
print("[Dashboard] npm process stopped gracefully")
|
|
207
|
+
return
|
|
208
|
+
time.sleep(1)
|
|
209
|
+
|
|
210
|
+
# Force kill if still running
|
|
211
|
+
print("[Dashboard] npm process did not stop gracefully, forcing kill...")
|
|
212
|
+
self._npm_process.kill()
|
|
213
|
+
self._npm_process.wait(timeout=2)
|
|
214
|
+
print("[Dashboard] npm process killed")
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
print(f"[Dashboard] Error stopping npm process: {e}")
|
|
218
|
+
|
|
219
|
+
finally:
|
|
220
|
+
self._npm_process = None
|
|
221
|
+
|
|
222
|
+
def __enter__(self):
|
|
223
|
+
"""Context manager entry: start dashboard."""
|
|
224
|
+
self.start()
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
228
|
+
"""Context manager exit: stop dashboard."""
|
|
229
|
+
self.stop()
|
|
230
|
+
return False # Don't suppress exceptions
|