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
flock/registry.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Runtime registries for blackboard artifact and function declarations."""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RegistryError(RuntimeError):
|
|
16
|
+
"""Raised when a registry operation fails."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TypeRegistry:
|
|
20
|
+
"""In-memory registry for blackboard artifact types."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._by_name: dict[str, type[BaseModel]] = {}
|
|
24
|
+
self._by_cls: dict[type[BaseModel], str] = {}
|
|
25
|
+
|
|
26
|
+
def register(self, model: type[BaseModel], name: str | None = None) -> str:
|
|
27
|
+
if not issubclass(model, BaseModel):
|
|
28
|
+
raise RegistryError("Only Pydantic models can be registered as artifact types.")
|
|
29
|
+
type_name = (
|
|
30
|
+
name or getattr(model, "__flock_type__", None) or f"{model.__module__}.{model.__name__}"
|
|
31
|
+
)
|
|
32
|
+
existing_model = self._by_name.get(type_name)
|
|
33
|
+
if existing_model is not None and existing_model is not model:
|
|
34
|
+
self._by_cls.pop(existing_model, None)
|
|
35
|
+
existing_name = self._by_cls.get(model)
|
|
36
|
+
if existing_name and existing_name != type_name:
|
|
37
|
+
self._by_name.pop(existing_name, None)
|
|
38
|
+
|
|
39
|
+
self._by_name[type_name] = model
|
|
40
|
+
self._by_cls[model] = type_name
|
|
41
|
+
model.__flock_type__ = type_name
|
|
42
|
+
return type_name
|
|
43
|
+
|
|
44
|
+
def resolve(self, type_name: str) -> type[BaseModel]:
|
|
45
|
+
try:
|
|
46
|
+
return self._by_name[type_name]
|
|
47
|
+
except KeyError as exc: # pragma: no cover - defensive
|
|
48
|
+
raise RegistryError(f"Unknown artifact type '{type_name}'.") from exc
|
|
49
|
+
|
|
50
|
+
def resolve_name(self, type_name: str) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Resolve a type name (simple or qualified) to its canonical form.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
type_name: Simple name ("Document") or qualified ("__main__.Document")
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Canonical type name from registry
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
RegistryError: Type not found or ambiguous
|
|
62
|
+
"""
|
|
63
|
+
# If already canonical, return as-is (O(1) lookup)
|
|
64
|
+
if type_name in self._by_name:
|
|
65
|
+
return type_name
|
|
66
|
+
|
|
67
|
+
# Search for models with matching simple name (O(n) scan)
|
|
68
|
+
matches = []
|
|
69
|
+
for canonical_name, model_cls in self._by_name.items():
|
|
70
|
+
if model_cls.__name__ == type_name:
|
|
71
|
+
matches.append(canonical_name)
|
|
72
|
+
|
|
73
|
+
if len(matches) == 0:
|
|
74
|
+
raise RegistryError(f"Unknown artifact type '{type_name}'.")
|
|
75
|
+
if len(matches) == 1:
|
|
76
|
+
return matches[0]
|
|
77
|
+
raise RegistryError(
|
|
78
|
+
f"Ambiguous type name '{type_name}'. Matches: {', '.join(matches)}. Use qualified name."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def name_for(self, model: type[BaseModel]) -> str:
|
|
82
|
+
try:
|
|
83
|
+
return self._by_cls[model]
|
|
84
|
+
except KeyError as exc:
|
|
85
|
+
raise RegistryError(
|
|
86
|
+
f"Model '{model.__name__}' is not registered as an artifact type."
|
|
87
|
+
) from exc
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class FunctionRegistry:
|
|
91
|
+
"""Registry for deterministic callable helpers (flock_tool)."""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self._callables: dict[str, Callable[..., Any]] = {}
|
|
95
|
+
|
|
96
|
+
def register(self, func: Callable[..., Any], *, name: str | None = None) -> str:
|
|
97
|
+
func_name = name or getattr(func, "__flock_tool__", None) or func.__name__
|
|
98
|
+
existing = self._callables.get(func_name)
|
|
99
|
+
if existing is func:
|
|
100
|
+
return func_name
|
|
101
|
+
self._callables[func_name] = func
|
|
102
|
+
func.__flock_tool__ = func_name
|
|
103
|
+
return func_name
|
|
104
|
+
|
|
105
|
+
def resolve(self, func_name: str) -> Callable[..., Any]:
|
|
106
|
+
try:
|
|
107
|
+
return self._callables[func_name]
|
|
108
|
+
except KeyError as exc:
|
|
109
|
+
raise RegistryError(
|
|
110
|
+
f"Function '{func_name}' is not registered with flock_tool."
|
|
111
|
+
) from exc
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
type_registry = TypeRegistry()
|
|
115
|
+
function_registry = FunctionRegistry()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def flock_type(model: type[BaseModel] | None = None, *, name: str | None = None) -> Any:
|
|
119
|
+
"""Decorator to register a Pydantic model as a blackboard artifact type."""
|
|
120
|
+
|
|
121
|
+
def _wrap(cls: type[BaseModel]) -> type[BaseModel]:
|
|
122
|
+
type_registry.register(cls, name=name)
|
|
123
|
+
return cls
|
|
124
|
+
|
|
125
|
+
if model is None:
|
|
126
|
+
return _wrap
|
|
127
|
+
return _wrap(model)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def flock_tool(func: Callable[..., Any] | None = None, *, name: str | None = None) -> Any:
|
|
131
|
+
"""Decorator to register a deterministic helper function for agents."""
|
|
132
|
+
|
|
133
|
+
def _wrap(callable_: Callable[..., Any]) -> Callable[..., Any]:
|
|
134
|
+
function_registry.register(callable_, name=name)
|
|
135
|
+
return callable_
|
|
136
|
+
|
|
137
|
+
if func is None:
|
|
138
|
+
return _wrap
|
|
139
|
+
return _wrap(func)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
__all__ = [
|
|
143
|
+
"RegistryError",
|
|
144
|
+
"flock_tool",
|
|
145
|
+
"flock_type",
|
|
146
|
+
"function_registry",
|
|
147
|
+
"type_registry",
|
|
148
|
+
]
|
flock/runtime.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Runtime envelopes exchanged between orchestrator and components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from flock.artifacts import Artifact
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EvalInputs(BaseModel):
|
|
14
|
+
artifacts: list[Artifact] = Field(default_factory=list)
|
|
15
|
+
state: dict[str, Any] = Field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
def first_as(self, model_cls: type[BaseModel]) -> BaseModel | None:
|
|
18
|
+
"""Extract first artifact as model instance.
|
|
19
|
+
|
|
20
|
+
Convenience method to deserialize the first artifact's payload
|
|
21
|
+
into a typed Pydantic model.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
model_cls: Pydantic model class to deserialize to
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Model instance or None if no artifacts
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> class TaskProcessor(EngineComponent):
|
|
31
|
+
... async def evaluate(self, agent, ctx, inputs: EvalInputs) -> EvalResult:
|
|
32
|
+
... task = inputs.first_as(Task)
|
|
33
|
+
... if not task:
|
|
34
|
+
... return EvalResult.empty()
|
|
35
|
+
... # Process task...
|
|
36
|
+
"""
|
|
37
|
+
if not self.artifacts:
|
|
38
|
+
return None
|
|
39
|
+
return model_cls(**self.artifacts[0].payload)
|
|
40
|
+
|
|
41
|
+
def all_as(self, model_cls: type[BaseModel]) -> list[BaseModel]:
|
|
42
|
+
"""Extract all artifacts as model instances.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
model_cls: Pydantic model class to deserialize to
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of model instances (empty if no artifacts)
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> tasks = inputs.all_as(Task)
|
|
52
|
+
>>> for task in tasks:
|
|
53
|
+
... # Process each task
|
|
54
|
+
"""
|
|
55
|
+
return [model_cls(**artifact.payload) for artifact in self.artifacts]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class EvalResult(BaseModel):
|
|
59
|
+
artifacts: list[Artifact] = Field(default_factory=list)
|
|
60
|
+
state: dict[str, Any] = Field(default_factory=dict)
|
|
61
|
+
metrics: dict[str, float] = Field(default_factory=dict)
|
|
62
|
+
logs: list[str] = Field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_object(
|
|
66
|
+
cls,
|
|
67
|
+
obj: BaseModel,
|
|
68
|
+
*,
|
|
69
|
+
agent,
|
|
70
|
+
state: dict | None = None,
|
|
71
|
+
metrics: dict | None = None,
|
|
72
|
+
errors: list[str] | None = None,
|
|
73
|
+
) -> EvalResult:
|
|
74
|
+
"""Create EvalResult from a single model instance.
|
|
75
|
+
|
|
76
|
+
Automatically constructs an Artifact from the model instance,
|
|
77
|
+
handling type registry lookup and payload serialization.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
obj: Pydantic model instance to publish as artifact
|
|
81
|
+
agent: Agent producing the artifact (for produced_by field)
|
|
82
|
+
state: Optional state dict to include in result
|
|
83
|
+
metrics: Optional metrics dict (e.g., {"confidence": 0.95})
|
|
84
|
+
errors: Optional list of error messages
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
EvalResult with single artifact
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> class TaskProcessor(EngineComponent):
|
|
91
|
+
... async def evaluate(self, agent, ctx, inputs: EvalInputs) -> EvalResult:
|
|
92
|
+
... task = inputs.first_as(Task)
|
|
93
|
+
... processed = Task(name=f"Done: {task.name}", priority=task.priority)
|
|
94
|
+
... return EvalResult.from_object(processed, agent=agent)
|
|
95
|
+
"""
|
|
96
|
+
from flock.artifacts import Artifact
|
|
97
|
+
from flock.registry import type_registry
|
|
98
|
+
|
|
99
|
+
type_name = type_registry.name_for(type(obj))
|
|
100
|
+
artifact = Artifact(
|
|
101
|
+
type=type_name,
|
|
102
|
+
payload=obj.model_dump(),
|
|
103
|
+
produced_by=agent.name,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return cls(
|
|
107
|
+
artifacts=[artifact],
|
|
108
|
+
state=state or {},
|
|
109
|
+
metrics=metrics or {},
|
|
110
|
+
errors=errors or [],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_objects(
|
|
115
|
+
cls,
|
|
116
|
+
*objs: BaseModel,
|
|
117
|
+
agent,
|
|
118
|
+
state: dict | None = None,
|
|
119
|
+
metrics: dict | None = None,
|
|
120
|
+
errors: list[str] | None = None,
|
|
121
|
+
) -> EvalResult:
|
|
122
|
+
"""Create EvalResult from multiple model instances.
|
|
123
|
+
|
|
124
|
+
Automatically constructs Artifacts from all model instances.
|
|
125
|
+
Useful when an agent produces multiple outputs in one evaluation.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
*objs: Pydantic model instances to publish as artifacts
|
|
129
|
+
agent: Agent producing the artifacts
|
|
130
|
+
state: Optional state dict
|
|
131
|
+
metrics: Optional metrics dict
|
|
132
|
+
errors: Optional list of error messages
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
EvalResult with multiple artifacts
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> class MovieEngine(EngineComponent):
|
|
139
|
+
... async def evaluate(self, agent, ctx, inputs: EvalInputs) -> EvalResult:
|
|
140
|
+
... idea = inputs.first_as(Idea)
|
|
141
|
+
... movie = Movie(title=idea.topic.upper(), runtime=240, synopsis="...")
|
|
142
|
+
... tagline = Tagline(line="Don't miss it!")
|
|
143
|
+
... return EvalResult.from_objects(
|
|
144
|
+
... movie, tagline,
|
|
145
|
+
... agent=agent,
|
|
146
|
+
... metrics={"confidence": 0.9}
|
|
147
|
+
... )
|
|
148
|
+
"""
|
|
149
|
+
from flock.artifacts import Artifact
|
|
150
|
+
from flock.registry import type_registry
|
|
151
|
+
|
|
152
|
+
artifacts = []
|
|
153
|
+
for obj in objs:
|
|
154
|
+
type_name = type_registry.name_for(type(obj))
|
|
155
|
+
artifact = Artifact(
|
|
156
|
+
type=type_name,
|
|
157
|
+
payload=obj.model_dump(),
|
|
158
|
+
produced_by=agent.name,
|
|
159
|
+
)
|
|
160
|
+
artifacts.append(artifact)
|
|
161
|
+
|
|
162
|
+
return cls(
|
|
163
|
+
artifacts=artifacts,
|
|
164
|
+
state=state or {},
|
|
165
|
+
metrics=metrics or {},
|
|
166
|
+
errors=errors or [],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def empty(
|
|
171
|
+
cls,
|
|
172
|
+
state: dict | None = None,
|
|
173
|
+
metrics: dict | None = None,
|
|
174
|
+
errors: list[str] | None = None,
|
|
175
|
+
) -> EvalResult:
|
|
176
|
+
"""Return empty result with no artifacts.
|
|
177
|
+
|
|
178
|
+
Useful when:
|
|
179
|
+
- Conditions aren't met for processing
|
|
180
|
+
- Agent only updates state without producing output
|
|
181
|
+
- Processing failed (use errors parameter)
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
state: Optional state dict
|
|
185
|
+
metrics: Optional metrics dict
|
|
186
|
+
errors: Optional list of error messages
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
EvalResult with empty artifacts list
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
>>> class ConditionalProcessor(EngineComponent):
|
|
193
|
+
... async def evaluate(self, agent, ctx, inputs: EvalInputs) -> EvalResult:
|
|
194
|
+
... task = inputs.first_as(Task)
|
|
195
|
+
... if task.priority < 3:
|
|
196
|
+
... return EvalResult.empty() # Skip low priority
|
|
197
|
+
... # Process high priority tasks...
|
|
198
|
+
|
|
199
|
+
>>> # With error reporting
|
|
200
|
+
>>> return EvalResult.empty(errors=["Validation failed: missing field"])
|
|
201
|
+
"""
|
|
202
|
+
return cls(
|
|
203
|
+
artifacts=[],
|
|
204
|
+
state=state or {},
|
|
205
|
+
metrics=metrics or {},
|
|
206
|
+
errors=errors or [],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def with_state(
|
|
211
|
+
cls,
|
|
212
|
+
state: dict,
|
|
213
|
+
*,
|
|
214
|
+
metrics: dict | None = None,
|
|
215
|
+
errors: list[str] | None = None,
|
|
216
|
+
) -> EvalResult:
|
|
217
|
+
"""Return result with only state updates (no artifacts).
|
|
218
|
+
|
|
219
|
+
Useful for agents that only update context without producing outputs,
|
|
220
|
+
such as validation or enrichment agents.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
state: State dict to pass to downstream agents
|
|
224
|
+
metrics: Optional metrics dict
|
|
225
|
+
errors: Optional list of error messages
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
EvalResult with state but no artifacts
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
>>> class ValidationAgent(EngineComponent):
|
|
232
|
+
... async def evaluate(self, agent, ctx, inputs: EvalInputs) -> EvalResult:
|
|
233
|
+
... task = inputs.first_as(Task)
|
|
234
|
+
... is_valid = task.priority >= 1
|
|
235
|
+
... return EvalResult.with_state(
|
|
236
|
+
... {"validation_passed": is_valid, "validator": "priority_check"}
|
|
237
|
+
... )
|
|
238
|
+
"""
|
|
239
|
+
return cls(
|
|
240
|
+
artifacts=[],
|
|
241
|
+
state=state,
|
|
242
|
+
metrics=metrics or {},
|
|
243
|
+
errors=errors or [],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class Context(BaseModel):
|
|
248
|
+
board: Any
|
|
249
|
+
orchestrator: Any
|
|
250
|
+
correlation_id: UUID | None = None # NEW!
|
|
251
|
+
task_id: str
|
|
252
|
+
state: dict[str, Any] = Field(default_factory=dict)
|
|
253
|
+
|
|
254
|
+
def get_variable(self, key: str, default: Any = None) -> Any:
|
|
255
|
+
return self.state.get(key, default)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
__all__ = [
|
|
259
|
+
"Context",
|
|
260
|
+
"EvalInputs",
|
|
261
|
+
"EvalResult",
|
|
262
|
+
]
|
flock/service.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""HTTP control plane for the blackboard orchestrator."""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, HTTPException
|
|
10
|
+
from fastapi.responses import PlainTextResponse
|
|
11
|
+
|
|
12
|
+
from flock.registry import type_registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from flock.orchestrator import Flock
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BlackboardHTTPService:
|
|
20
|
+
def __init__(self, orchestrator: Flock) -> None:
|
|
21
|
+
self.orchestrator = orchestrator
|
|
22
|
+
self.app = FastAPI(title="Blackboard Agents Service", version="1.0.0")
|
|
23
|
+
self._register_routes()
|
|
24
|
+
|
|
25
|
+
def _register_routes(self) -> None:
|
|
26
|
+
app = self.app
|
|
27
|
+
orchestrator = self.orchestrator
|
|
28
|
+
|
|
29
|
+
@app.post("/api/v1/artifacts")
|
|
30
|
+
async def publish_artifact(body: dict[str, Any]) -> dict[str, str]:
|
|
31
|
+
type_name = body.get("type")
|
|
32
|
+
payload = body.get("payload") or {}
|
|
33
|
+
if not type_name:
|
|
34
|
+
raise HTTPException(status_code=400, detail="type is required")
|
|
35
|
+
try:
|
|
36
|
+
await orchestrator.publish({"type": type_name, **payload})
|
|
37
|
+
except Exception as exc: # pragma: no cover - FastAPI converts
|
|
38
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
39
|
+
return {"status": "accepted"}
|
|
40
|
+
|
|
41
|
+
@app.get("/api/v1/artifacts/{artifact_id}")
|
|
42
|
+
async def get_artifact(artifact_id: UUID) -> dict[str, Any]:
|
|
43
|
+
artifact = await orchestrator.store.get(artifact_id)
|
|
44
|
+
if artifact is None:
|
|
45
|
+
raise HTTPException(status_code=404, detail="artifact not found")
|
|
46
|
+
return {
|
|
47
|
+
"id": str(artifact.id),
|
|
48
|
+
"type": artifact.type,
|
|
49
|
+
"payload": artifact.payload,
|
|
50
|
+
"produced_by": artifact.produced_by,
|
|
51
|
+
"visibility": artifact.visibility.model_dump(mode="json"),
|
|
52
|
+
"created_at": artifact.created_at.isoformat(),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@app.post("/api/v1/agents/{name}/run")
|
|
56
|
+
async def run_agent(name: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
try:
|
|
58
|
+
agent = orchestrator.get_agent(name)
|
|
59
|
+
except KeyError as exc:
|
|
60
|
+
raise HTTPException(status_code=404, detail="agent not found") from exc
|
|
61
|
+
|
|
62
|
+
inputs_data: list[dict[str, Any]] = body.get("inputs") or []
|
|
63
|
+
inputs = []
|
|
64
|
+
for item in inputs_data:
|
|
65
|
+
type_name = item.get("type")
|
|
66
|
+
payload = item.get("payload") or {}
|
|
67
|
+
if not type_name:
|
|
68
|
+
raise HTTPException(status_code=400, detail="Each input requires 'type'.")
|
|
69
|
+
model = type_registry.resolve(type_name)
|
|
70
|
+
instance = model(**payload)
|
|
71
|
+
inputs.append(instance)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
outputs = await orchestrator.direct_invoke(agent, inputs)
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=500, detail=f"Agent execution failed: {exc}"
|
|
78
|
+
) from exc
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"artifacts": [
|
|
82
|
+
{
|
|
83
|
+
"id": str(artifact.id),
|
|
84
|
+
"type": artifact.type,
|
|
85
|
+
"payload": artifact.payload,
|
|
86
|
+
"produced_by": artifact.produced_by,
|
|
87
|
+
}
|
|
88
|
+
for artifact in outputs
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@app.get("/api/v1/agents")
|
|
93
|
+
async def list_agents() -> dict[str, Any]:
|
|
94
|
+
return {
|
|
95
|
+
"agents": [
|
|
96
|
+
{
|
|
97
|
+
"name": agent.name,
|
|
98
|
+
"description": agent.description,
|
|
99
|
+
"subscriptions": [
|
|
100
|
+
{
|
|
101
|
+
"types": list(subscription.type_names),
|
|
102
|
+
"mode": subscription.mode,
|
|
103
|
+
"delivery": subscription.delivery,
|
|
104
|
+
}
|
|
105
|
+
for subscription in agent.subscriptions
|
|
106
|
+
],
|
|
107
|
+
"outputs": [output.spec.type_name for output in agent.outputs],
|
|
108
|
+
}
|
|
109
|
+
for agent in orchestrator.agents
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@app.get("/health")
|
|
114
|
+
async def health() -> dict[str, str]: # pragma: no cover - trivial
|
|
115
|
+
return {"status": "ok"}
|
|
116
|
+
|
|
117
|
+
@app.get("/metrics")
|
|
118
|
+
async def metrics() -> PlainTextResponse:
|
|
119
|
+
lines = [f"blackboard_{key} {value}" for key, value in orchestrator.metrics.items()]
|
|
120
|
+
return PlainTextResponse("\n".join(lines))
|
|
121
|
+
|
|
122
|
+
def run(
|
|
123
|
+
self, host: str = "127.0.0.1", port: int = 8000
|
|
124
|
+
) -> None: # pragma: no cover - manual execution
|
|
125
|
+
import uvicorn
|
|
126
|
+
|
|
127
|
+
uvicorn.run(self.app, host=host, port=port)
|
|
128
|
+
|
|
129
|
+
async def run_async(
|
|
130
|
+
self, host: str = "127.0.0.1", port: int = 8000
|
|
131
|
+
) -> None: # pragma: no cover - manual execution
|
|
132
|
+
"""Run the service asynchronously (for use within async context)."""
|
|
133
|
+
import uvicorn
|
|
134
|
+
|
|
135
|
+
config = uvicorn.Config(self.app, host=host, port=port)
|
|
136
|
+
server = uvicorn.Server(config)
|
|
137
|
+
await server.serve()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
__all__ = ["BlackboardHTTPService"]
|
flock/store.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Blackboard storage primitives."""
|
|
5
|
+
|
|
6
|
+
from asyncio import Lock
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from flock.registry import type_registry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import builtins
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
from flock.artifacts import Artifact
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BlackboardStore:
|
|
22
|
+
async def publish(self, artifact: Artifact) -> None:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
async def get(self, artifact_id: UUID) -> Artifact | None:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
async def list(self) -> builtins.list[Artifact]:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
async def list_by_type(self, type_name: str) -> builtins.list[Artifact]:
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InMemoryBlackboardStore(BlackboardStore):
|
|
36
|
+
"""Simple in-memory implementation suitable for local dev and tests."""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._lock = Lock()
|
|
40
|
+
self._by_id: dict[UUID, Artifact] = {}
|
|
41
|
+
self._by_type: dict[str, list[Artifact]] = defaultdict(list)
|
|
42
|
+
|
|
43
|
+
async def publish(self, artifact: Artifact) -> None:
|
|
44
|
+
async with self._lock:
|
|
45
|
+
self._by_id[artifact.id] = artifact
|
|
46
|
+
self._by_type[artifact.type].append(artifact)
|
|
47
|
+
|
|
48
|
+
async def get(self, artifact_id: UUID) -> Artifact | None:
|
|
49
|
+
async with self._lock:
|
|
50
|
+
return self._by_id.get(artifact_id)
|
|
51
|
+
|
|
52
|
+
async def list(self) -> builtins.list[Artifact]:
|
|
53
|
+
async with self._lock:
|
|
54
|
+
return list(self._by_id.values())
|
|
55
|
+
|
|
56
|
+
async def list_by_type(self, type_name: str) -> builtins.list[Artifact]:
|
|
57
|
+
async with self._lock:
|
|
58
|
+
canonical = type_registry.resolve_name(type_name)
|
|
59
|
+
return list(self._by_type.get(canonical, []))
|
|
60
|
+
|
|
61
|
+
async def extend(self, artifacts: Iterable[Artifact]) -> None: # pragma: no cover - helper
|
|
62
|
+
for artifact in artifacts:
|
|
63
|
+
await self.publish(artifact)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"BlackboardStore",
|
|
68
|
+
"InMemoryBlackboardStore",
|
|
69
|
+
]
|