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/utilities.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Built-in utility components for metrics and logging."""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import contextlib
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.json import JSON
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.pretty import Pretty
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
from flock.components import AgentComponent
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from flock.runtime import Context, EvalInputs, EvalResult
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MetricsUtility(AgentComponent):
|
|
30
|
+
"""Records simple runtime metrics per agent execution."""
|
|
31
|
+
|
|
32
|
+
name: str | None = "metrics"
|
|
33
|
+
|
|
34
|
+
async def on_pre_evaluate(self, agent, ctx: Context, inputs: EvalInputs) -> EvalInputs:
|
|
35
|
+
ctx.state.setdefault("metrics", {})[f"{agent.name}:start"] = time.perf_counter()
|
|
36
|
+
return inputs
|
|
37
|
+
|
|
38
|
+
async def on_post_evaluate(
|
|
39
|
+
self, agent, ctx: Context, inputs: EvalInputs, result: EvalResult
|
|
40
|
+
) -> EvalResult:
|
|
41
|
+
metrics = ctx.state.setdefault("metrics", {})
|
|
42
|
+
start = metrics.get(f"{agent.name}:start")
|
|
43
|
+
if start:
|
|
44
|
+
metrics[f"{agent.name}:duration_ms"] = (time.perf_counter() - start) * 1000
|
|
45
|
+
result.metrics.update({k: v for k, v in metrics.items() if k.endswith("duration_ms")})
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LoggingUtility(AgentComponent):
|
|
50
|
+
"""Rich-powered logging with optional streaming previews."""
|
|
51
|
+
|
|
52
|
+
name: str | None = "logs"
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
console: Console | None = None,
|
|
57
|
+
*,
|
|
58
|
+
highlight_json: bool = True,
|
|
59
|
+
stream_tokens: bool = True,
|
|
60
|
+
) -> None:
|
|
61
|
+
super().__init__()
|
|
62
|
+
if console is None:
|
|
63
|
+
console = Console(
|
|
64
|
+
file=sys.stdout,
|
|
65
|
+
force_terminal=True,
|
|
66
|
+
highlight=False,
|
|
67
|
+
log_time=True,
|
|
68
|
+
log_path=False,
|
|
69
|
+
)
|
|
70
|
+
self._console = console
|
|
71
|
+
self._highlight_json = highlight_json
|
|
72
|
+
self._stream_tokens = stream_tokens
|
|
73
|
+
self._stream_context: dict[str, tuple[asyncio.Queue, asyncio.Task]] = {}
|
|
74
|
+
|
|
75
|
+
async def on_initialize(self, agent, ctx: Context) -> None:
|
|
76
|
+
self._console.log(f"[{agent.name}] start task={ctx.task_id}")
|
|
77
|
+
await super().on_initialize(agent, ctx)
|
|
78
|
+
|
|
79
|
+
async def on_pre_consume(self, agent, ctx: Context, inputs: list[Any]):
|
|
80
|
+
summary = ", ".join(self._summarize_artifact(art) for art in inputs) or "<none>"
|
|
81
|
+
self._console.log(f"[{agent.name}] consume n={len(inputs)} artifacts -> {summary}")
|
|
82
|
+
self._render_artifacts(agent.name, inputs, role="input")
|
|
83
|
+
return await super().on_pre_consume(agent, ctx, inputs)
|
|
84
|
+
|
|
85
|
+
async def on_pre_evaluate(self, agent, ctx: Context, inputs: EvalInputs) -> EvalInputs:
|
|
86
|
+
if self._stream_tokens:
|
|
87
|
+
self._maybe_start_stream(agent, ctx)
|
|
88
|
+
return await super().on_pre_evaluate(agent, ctx, inputs)
|
|
89
|
+
|
|
90
|
+
async def on_post_evaluate(
|
|
91
|
+
self, agent, ctx: Context, inputs: EvalInputs, result: EvalResult
|
|
92
|
+
) -> EvalResult:
|
|
93
|
+
self._render_metrics(agent.name, result.metrics)
|
|
94
|
+
self._render_artifacts(agent.name, result.artifacts or inputs.artifacts, role="output")
|
|
95
|
+
if result.logs:
|
|
96
|
+
self._render_logs(agent.name, result.logs)
|
|
97
|
+
awaited = await super().on_post_evaluate(agent, ctx, inputs, result)
|
|
98
|
+
if self._stream_tokens:
|
|
99
|
+
await self._finalize_stream(agent, ctx)
|
|
100
|
+
return awaited
|
|
101
|
+
|
|
102
|
+
async def on_post_publish(self, agent, ctx: Context, artifact):
|
|
103
|
+
visibility = getattr(artifact.visibility, "kind", "Public")
|
|
104
|
+
subtitle = f"visibility={visibility}"
|
|
105
|
+
panel = self._build_artifact_panel(artifact, role="published", subtitle=subtitle)
|
|
106
|
+
self._console.print(panel)
|
|
107
|
+
await super().on_post_publish(agent, ctx, artifact)
|
|
108
|
+
|
|
109
|
+
async def on_error(self, agent, ctx: Context, error: Exception) -> None:
|
|
110
|
+
self._console.log(f"[{agent.name}] error {error!r}", style="bold red")
|
|
111
|
+
if self._stream_tokens:
|
|
112
|
+
await self._abort_stream(agent, ctx)
|
|
113
|
+
await super().on_error(agent, ctx, error)
|
|
114
|
+
|
|
115
|
+
async def on_terminate(self, agent, ctx: Context) -> None:
|
|
116
|
+
if self._stream_tokens:
|
|
117
|
+
await self._abort_stream(agent, ctx)
|
|
118
|
+
self._console.log(f"[{agent.name}] end task={ctx.task_id}")
|
|
119
|
+
await super().on_terminate(agent, ctx)
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# Rendering helpers
|
|
123
|
+
|
|
124
|
+
def _render_artifacts(self, agent_name: str, artifacts: Sequence[Any], *, role: str) -> None:
|
|
125
|
+
for artifact in artifacts:
|
|
126
|
+
panel = self._build_artifact_panel(artifact, role=role)
|
|
127
|
+
self._console.print(panel)
|
|
128
|
+
|
|
129
|
+
def _build_artifact_panel(
|
|
130
|
+
self, artifact: Any, *, role: str, subtitle: str | None = None
|
|
131
|
+
) -> Panel:
|
|
132
|
+
title = f"{role} • {self._summarize_artifact(artifact)}"
|
|
133
|
+
if subtitle is None:
|
|
134
|
+
produced_by = getattr(artifact, "produced_by", None)
|
|
135
|
+
visibility = getattr(artifact, "visibility", None)
|
|
136
|
+
visibility_name = getattr(visibility, "kind", None)
|
|
137
|
+
pieces = []
|
|
138
|
+
if produced_by:
|
|
139
|
+
pieces.append(f"from={produced_by}")
|
|
140
|
+
if visibility_name:
|
|
141
|
+
pieces.append(f"visibility={visibility_name}")
|
|
142
|
+
subtitle = " | ".join(pieces)
|
|
143
|
+
|
|
144
|
+
payload = getattr(artifact, "payload", None)
|
|
145
|
+
renderable = self._render_payload(payload)
|
|
146
|
+
return Panel(renderable, title=title, subtitle=subtitle, border_style="cyan")
|
|
147
|
+
|
|
148
|
+
def _render_payload(self, payload: Any):
|
|
149
|
+
if payload is None:
|
|
150
|
+
return Pretty(payload)
|
|
151
|
+
if isinstance(payload, Mapping):
|
|
152
|
+
if self._highlight_json:
|
|
153
|
+
try:
|
|
154
|
+
return JSON.from_data(payload, indent=2, sort_keys=True)
|
|
155
|
+
except Exception: # nosec B110 - pragma: no cover - serialization guard
|
|
156
|
+
pass
|
|
157
|
+
return Pretty(dict(payload))
|
|
158
|
+
if isinstance(payload, (list, tuple, set)):
|
|
159
|
+
return Pretty(payload)
|
|
160
|
+
if hasattr(payload, "model_dump"):
|
|
161
|
+
model_dict = payload.model_dump()
|
|
162
|
+
return JSON.from_data(model_dict, indent=2, sort_keys=True)
|
|
163
|
+
return Pretty(payload)
|
|
164
|
+
|
|
165
|
+
def _render_metrics(self, agent_name: str, metrics: Mapping[str, Any]) -> None:
|
|
166
|
+
if not metrics:
|
|
167
|
+
return
|
|
168
|
+
table = Table(title=f"{agent_name} metrics", box=None, show_header=False)
|
|
169
|
+
for key, value in metrics.items():
|
|
170
|
+
display = f"{value:.2f}" if isinstance(value, (int, float)) else str(value)
|
|
171
|
+
table.add_row(key, display)
|
|
172
|
+
self._console.print(table)
|
|
173
|
+
|
|
174
|
+
def _render_logs(self, agent_name: str, logs: Iterable[str]) -> None:
|
|
175
|
+
if not logs:
|
|
176
|
+
return
|
|
177
|
+
textual: list[str] = []
|
|
178
|
+
json_sections: list[JSON] = []
|
|
179
|
+
for line in logs:
|
|
180
|
+
if line.startswith("dspy.output="):
|
|
181
|
+
_, _, payload = line.partition("=")
|
|
182
|
+
try:
|
|
183
|
+
json_sections.append(
|
|
184
|
+
JSON.from_data(json.loads(payload), indent=2, sort_keys=True)
|
|
185
|
+
)
|
|
186
|
+
continue
|
|
187
|
+
except json.JSONDecodeError:
|
|
188
|
+
textual.append(line)
|
|
189
|
+
else:
|
|
190
|
+
textual.append(line)
|
|
191
|
+
for payload in json_sections:
|
|
192
|
+
panel = Panel(payload, title=f"{agent_name} ▸ dspy.output", border_style="green")
|
|
193
|
+
self._console.print(panel)
|
|
194
|
+
if textual:
|
|
195
|
+
body = Text("\n".join(textual) + "\n")
|
|
196
|
+
panel = Panel(body, title=f"{agent_name} logs", border_style="magenta")
|
|
197
|
+
self._console.print(panel)
|
|
198
|
+
|
|
199
|
+
def _summarize_artifact(self, artifact: Any) -> str:
|
|
200
|
+
try:
|
|
201
|
+
art_id = getattr(artifact, "id", None)
|
|
202
|
+
prefix = str(art_id)[:8] if art_id else "?"
|
|
203
|
+
art_type = getattr(artifact, "type", type(artifact).__name__)
|
|
204
|
+
return f"{art_type}@{prefix}"
|
|
205
|
+
except Exception: # pragma: no cover - defensive
|
|
206
|
+
return repr(artifact)
|
|
207
|
+
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
# Streaming support
|
|
210
|
+
|
|
211
|
+
def _maybe_start_stream(self, agent, ctx: Context) -> None:
|
|
212
|
+
stream_key = self._stream_key(agent, ctx)
|
|
213
|
+
if stream_key in self._stream_context:
|
|
214
|
+
return
|
|
215
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
216
|
+
self._attach_stream_queue(ctx.state, queue)
|
|
217
|
+
task = asyncio.create_task(self._consume_stream(agent.name, stream_key, queue))
|
|
218
|
+
self._stream_context[stream_key] = (queue, task)
|
|
219
|
+
|
|
220
|
+
async def _finalize_stream(self, agent, ctx: Context) -> None:
|
|
221
|
+
stream_key = self._stream_key(agent, ctx)
|
|
222
|
+
record = self._stream_context.pop(stream_key, None)
|
|
223
|
+
self._detach_stream_queue(ctx.state)
|
|
224
|
+
if not record:
|
|
225
|
+
return
|
|
226
|
+
queue, task = record
|
|
227
|
+
if not task.done():
|
|
228
|
+
await queue.put({"kind": "end"})
|
|
229
|
+
try:
|
|
230
|
+
await asyncio.wait_for(task, timeout=2.0)
|
|
231
|
+
except asyncio.TimeoutError: # pragma: no cover - defensive cancel
|
|
232
|
+
task.cancel()
|
|
233
|
+
|
|
234
|
+
async def _abort_stream(self, agent, ctx: Context) -> None:
|
|
235
|
+
stream_key = self._stream_key(agent, ctx)
|
|
236
|
+
record = self._stream_context.pop(stream_key, None)
|
|
237
|
+
self._detach_stream_queue(ctx.state)
|
|
238
|
+
if not record:
|
|
239
|
+
return
|
|
240
|
+
queue, task = record
|
|
241
|
+
if not task.done():
|
|
242
|
+
await queue.put({"kind": "end", "error": "aborted"})
|
|
243
|
+
task.cancel()
|
|
244
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
245
|
+
await task
|
|
246
|
+
|
|
247
|
+
async def _consume_stream(self, agent_name: str, stream_key: str, queue: asyncio.Queue) -> None:
|
|
248
|
+
body = Text()
|
|
249
|
+
live: Live | None = None
|
|
250
|
+
try:
|
|
251
|
+
while True:
|
|
252
|
+
event = await queue.get()
|
|
253
|
+
if event is None or event.get("kind") == "end":
|
|
254
|
+
break
|
|
255
|
+
kind = event.get("kind")
|
|
256
|
+
if live is None:
|
|
257
|
+
live_panel = Panel(body, title=f"{agent_name} ▸ streaming", border_style="cyan")
|
|
258
|
+
live = Live(
|
|
259
|
+
live_panel,
|
|
260
|
+
console=self._console,
|
|
261
|
+
refresh_per_second=12,
|
|
262
|
+
transient=True,
|
|
263
|
+
)
|
|
264
|
+
live.__enter__()
|
|
265
|
+
if kind == "chunk":
|
|
266
|
+
chunk = event.get("chunk") or ""
|
|
267
|
+
body.append(chunk)
|
|
268
|
+
elif kind == "status":
|
|
269
|
+
message = event.get("message") or ""
|
|
270
|
+
stage = event.get("stage")
|
|
271
|
+
line = f"[{stage}] {message}" if stage else message
|
|
272
|
+
body.append(f"\n{line}\n", style="dim")
|
|
273
|
+
elif kind == "error":
|
|
274
|
+
message = event.get("message") or ""
|
|
275
|
+
body.append(f"\n⚠ {message}\n", style="bold red")
|
|
276
|
+
if live is not None:
|
|
277
|
+
live.update(Panel(body, title=f"{agent_name} ▸ streaming", border_style="cyan"))
|
|
278
|
+
finally:
|
|
279
|
+
if live is not None:
|
|
280
|
+
live.__exit__(None, None, None)
|
|
281
|
+
if body.plain:
|
|
282
|
+
self._console.print(
|
|
283
|
+
Panel(body, title=f"{agent_name} ▸ stream transcript", border_style="cyan")
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _stream_key(self, agent, ctx: Context) -> str:
|
|
287
|
+
return f"{ctx.task_id}:{agent.name}"
|
|
288
|
+
|
|
289
|
+
def _attach_stream_queue(self, state: MutableMapping[str, Any], queue: asyncio.Queue) -> None:
|
|
290
|
+
state.setdefault("_logging", {})["stream_queue"] = queue
|
|
291
|
+
|
|
292
|
+
def _detach_stream_queue(self, state: MutableMapping[str, Any]) -> None:
|
|
293
|
+
try:
|
|
294
|
+
logging_state = state.get("_logging")
|
|
295
|
+
if isinstance(logging_state, MutableMapping):
|
|
296
|
+
logging_state.pop("stream_queue", None)
|
|
297
|
+
except Exception: # nosec B110 - pragma: no cover - defensive
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
__all__ = ["LoggingUtility", "MetricsUtility"]
|
|
@@ -6,19 +6,18 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
|
-
from flock.
|
|
10
|
-
from flock.
|
|
11
|
-
from flock.core.context.context import FlockContext
|
|
12
|
-
from flock.core.context.context_vars import FLOCK_BATCH_SILENT_MODE
|
|
13
|
-
from flock.core.logging.formatters.themed_formatter import (
|
|
9
|
+
from flock.components import AgentComponent, AgentComponentConfig
|
|
10
|
+
from flock.logging.formatters.themed_formatter import (
|
|
14
11
|
ThemedAgentResultFormatter,
|
|
15
12
|
)
|
|
16
|
-
from flock.
|
|
17
|
-
from flock.
|
|
18
|
-
from flock.
|
|
13
|
+
from flock.logging.formatters.themes import OutputTheme
|
|
14
|
+
from flock.logging.logging import get_logger
|
|
15
|
+
from flock.runtime import Context, EvalInputs, EvalResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
19
|
+
from flock.agent import Agent
|
|
19
20
|
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from flock.core.flock_agent import FlockAgent
|
|
22
21
|
|
|
23
22
|
logger = get_logger("components.utility.output")
|
|
24
23
|
|
|
@@ -27,14 +26,10 @@ class OutputUtilityConfig(AgentComponentConfig):
|
|
|
27
26
|
"""Configuration for output formatting and display."""
|
|
28
27
|
|
|
29
28
|
theme: OutputTheme = Field(
|
|
30
|
-
default=OutputTheme.
|
|
31
|
-
)
|
|
32
|
-
render_table: bool = Field(
|
|
33
|
-
default=False, description="Whether to render output as a table"
|
|
34
|
-
)
|
|
35
|
-
max_length: int = Field(
|
|
36
|
-
default=1000, description="Maximum length for displayed output"
|
|
29
|
+
default=OutputTheme.catppuccin_mocha, description="Theme for output formatting"
|
|
37
30
|
)
|
|
31
|
+
render_table: bool = Field(default=True, description="Whether to render output as a table")
|
|
32
|
+
max_length: int = Field(default=1000, description="Maximum length for displayed output")
|
|
38
33
|
truncate_long_values: bool = Field(
|
|
39
34
|
default=True, description="Whether to truncate long values in display"
|
|
40
35
|
)
|
|
@@ -59,8 +54,7 @@ class OutputUtilityConfig(AgentComponentConfig):
|
|
|
59
54
|
)
|
|
60
55
|
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
class OutputUtilityComponent(UtilityComponent):
|
|
57
|
+
class OutputUtilityComponent(AgentComponent):
|
|
64
58
|
"""Utility component that handles output formatting and display."""
|
|
65
59
|
|
|
66
60
|
config: OutputUtilityConfig = Field(
|
|
@@ -88,12 +82,11 @@ class OutputUtilityComponent(UtilityComponent):
|
|
|
88
82
|
# Default formatting based on type
|
|
89
83
|
if isinstance(value, dict):
|
|
90
84
|
return self._format_dict(value)
|
|
91
|
-
|
|
85
|
+
if isinstance(value, list):
|
|
92
86
|
return self._format_list(value)
|
|
93
|
-
|
|
87
|
+
if isinstance(value, str) and self.config.format_code_blocks:
|
|
94
88
|
return self._format_potential_code(value)
|
|
95
|
-
|
|
96
|
-
return str(value)
|
|
89
|
+
return str(value)
|
|
97
90
|
|
|
98
91
|
def _format_dict(self, d: dict[str, Any], indent: int = 0) -> str:
|
|
99
92
|
"""Format a dictionary with proper indentation."""
|
|
@@ -124,6 +117,7 @@ class OutputUtilityComponent(UtilityComponent):
|
|
|
124
117
|
|
|
125
118
|
def _format_potential_code(self, text: str) -> str:
|
|
126
119
|
"""Apply syntax highlighting to potential code blocks."""
|
|
120
|
+
|
|
127
121
|
# Simple pattern matching for code blocks
|
|
128
122
|
def replace_code_block(match):
|
|
129
123
|
language = match.group(1) or "text"
|
|
@@ -131,57 +125,74 @@ class OutputUtilityComponent(UtilityComponent):
|
|
|
131
125
|
return f"[CODE:{language}]\n{code}\n[/CODE]"
|
|
132
126
|
|
|
133
127
|
# Replace markdown-style code blocks
|
|
134
|
-
text =
|
|
135
|
-
r"```(\w+)?\n(.*?)\n```", replace_code_block, text, flags=re.DOTALL
|
|
136
|
-
)
|
|
137
|
-
return text
|
|
128
|
+
return re.sub(r"```(\w+)?\n(.*?)\n```", replace_code_block, text, flags=re.DOTALL)
|
|
138
129
|
|
|
139
130
|
async def on_post_evaluate(
|
|
140
|
-
self,
|
|
141
|
-
agent: "FlockAgent",
|
|
142
|
-
inputs: dict[str, Any],
|
|
143
|
-
context: FlockContext | None = None,
|
|
144
|
-
result: dict[str, Any] | None = None,
|
|
131
|
+
self, agent: "Agent", ctx: Context, inputs: EvalInputs, result: EvalResult
|
|
145
132
|
) -> dict[str, Any]:
|
|
146
133
|
"""Format and display the output."""
|
|
147
134
|
logger.debug("Formatting and displaying output")
|
|
148
135
|
|
|
149
136
|
streaming_live_handled = False
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
137
|
+
output_queued = False
|
|
138
|
+
streamed_artifact_id = None
|
|
139
|
+
|
|
140
|
+
if ctx:
|
|
141
|
+
streaming_live_handled = bool(ctx.get_variable("_flock_stream_live_active", False))
|
|
142
|
+
output_queued = bool(ctx.get_variable("_flock_output_queued", False))
|
|
143
|
+
streamed_artifact_id = ctx.get_variable("_flock_streamed_artifact_id")
|
|
144
|
+
|
|
154
145
|
if streaming_live_handled:
|
|
155
|
-
|
|
146
|
+
ctx.state.pop("_flock_stream_live_active", None)
|
|
156
147
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
if output_queued:
|
|
149
|
+
ctx.state.pop("_flock_output_queued", None)
|
|
150
|
+
|
|
151
|
+
if streamed_artifact_id:
|
|
152
|
+
ctx.state.pop("_flock_streamed_artifact_id", None)
|
|
161
153
|
|
|
154
|
+
# If streaming was handled, we need to update the final display with the real artifact ID
|
|
155
|
+
if streaming_live_handled and streamed_artifact_id:
|
|
156
|
+
logger.debug(
|
|
157
|
+
f"Updating streamed display with final artifact ID: {streamed_artifact_id}"
|
|
158
|
+
)
|
|
159
|
+
# The streaming display already showed everything, we just need to update the ID
|
|
160
|
+
# This is handled by a final refresh in the streaming code
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
# Skip output if streaming already handled it (and no ID to update)
|
|
162
164
|
if streaming_live_handled:
|
|
163
165
|
logger.debug("Skipping static table because streaming rendered live output.")
|
|
164
166
|
return result
|
|
165
167
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
# If output was queued due to concurrent stream, wait and then display
|
|
169
|
+
if output_queued:
|
|
170
|
+
# Wait for active streams to complete
|
|
171
|
+
orchestrator = getattr(ctx, "orchestrator", None)
|
|
172
|
+
if orchestrator:
|
|
173
|
+
import asyncio
|
|
174
|
+
|
|
175
|
+
# Wait until no streams are active
|
|
176
|
+
max_wait = 30 # seconds
|
|
177
|
+
waited = 0
|
|
178
|
+
while getattr(orchestrator, "_active_streams", 0) > 0 and waited < max_wait:
|
|
179
|
+
await asyncio.sleep(0.1)
|
|
180
|
+
waited += 0.1
|
|
181
|
+
logger.debug(
|
|
182
|
+
f"Queued output displayed after waiting {waited:.1f}s for streams to complete."
|
|
183
|
+
)
|
|
169
184
|
|
|
170
185
|
logger.debug("Formatting and displaying output to console.")
|
|
171
186
|
|
|
172
|
-
if self.config.print_context and
|
|
187
|
+
if self.config.print_context and ctx:
|
|
173
188
|
# Add context snapshot if requested (be careful with large contexts)
|
|
174
189
|
try:
|
|
175
190
|
# Create a copy or select relevant parts to avoid modifying original result dict directly
|
|
176
191
|
display_result = result.copy()
|
|
177
|
-
display_result["context_snapshot"] = (
|
|
178
|
-
context.to_dict()
|
|
179
|
-
) # Potential performance hit
|
|
192
|
+
display_result["context_snapshot"] = ctx.to_dict() # Potential performance hit
|
|
180
193
|
except Exception:
|
|
181
194
|
display_result = result.copy()
|
|
182
|
-
display_result["context_snapshot"] =
|
|
183
|
-
"[Error serializing context]"
|
|
184
|
-
)
|
|
195
|
+
display_result["context_snapshot"] = "[Error serializing context]"
|
|
185
196
|
result_to_display = display_result
|
|
186
197
|
else:
|
|
187
198
|
result_to_display = result
|
|
@@ -192,8 +203,12 @@ class OutputUtilityComponent(UtilityComponent):
|
|
|
192
203
|
max_length=self.config.max_length,
|
|
193
204
|
render_table=self.config.render_table,
|
|
194
205
|
)
|
|
195
|
-
model = agent.model if agent.model else
|
|
196
|
-
|
|
206
|
+
model = agent.model if agent.model else ctx.get_variable("model")
|
|
207
|
+
# Handle None model gracefully
|
|
208
|
+
model_display = model if model is not None else "default"
|
|
209
|
+
self._formatter.display_result(
|
|
210
|
+
result_to_display.artifacts, agent.name + " - " + model_display
|
|
211
|
+
)
|
|
197
212
|
|
|
198
213
|
return result # Return the original, unmodified result
|
|
199
214
|
|
flock/visibility.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Artifact visibility policies."""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentIdentity(BaseModel):
|
|
17
|
+
"""Minimal identity information about an agent for visibility checks."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
labels: set[str] = Field(default_factory=set)
|
|
21
|
+
tenant_id: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Visibility(BaseModel):
|
|
25
|
+
"""Base visibility contract."""
|
|
26
|
+
|
|
27
|
+
kind: Literal["Public", "Private", "Labelled", "Tenant", "After"]
|
|
28
|
+
|
|
29
|
+
def allows(self, agent: AgentIdentity, *, now: datetime | None = None) -> bool:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PublicVisibility(Visibility):
|
|
34
|
+
kind: Literal["Public"] = "Public"
|
|
35
|
+
|
|
36
|
+
def allows(
|
|
37
|
+
self, agent: AgentIdentity, *, now: datetime | None = None
|
|
38
|
+
) -> bool: # pragma: no cover - trivial
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PrivateVisibility(Visibility):
|
|
43
|
+
kind: Literal["Private"] = "Private"
|
|
44
|
+
agents: set[str] = Field(default_factory=set)
|
|
45
|
+
|
|
46
|
+
def allows(self, agent: AgentIdentity, *, now: datetime | None = None) -> bool:
|
|
47
|
+
return agent.name in self.agents
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LabelledVisibility(Visibility):
|
|
51
|
+
kind: Literal["Labelled"] = "Labelled"
|
|
52
|
+
required_labels: set[str] = Field(default_factory=set)
|
|
53
|
+
|
|
54
|
+
def allows(self, agent: AgentIdentity, *, now: datetime | None = None) -> bool:
|
|
55
|
+
return self.required_labels.issubset(agent.labels)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TenantVisibility(Visibility):
|
|
59
|
+
kind: Literal["Tenant"] = "Tenant"
|
|
60
|
+
tenant_id: str | None = None
|
|
61
|
+
|
|
62
|
+
def allows(self, agent: AgentIdentity, *, now: datetime | None = None) -> bool:
|
|
63
|
+
if self.tenant_id is None:
|
|
64
|
+
return True
|
|
65
|
+
return agent.tenant_id == self.tenant_id
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AfterVisibility(Visibility):
|
|
69
|
+
kind: Literal["After"] = "After"
|
|
70
|
+
ttl: timedelta = Field(default=timedelta())
|
|
71
|
+
then: Visibility | None = None
|
|
72
|
+
_created_at: datetime = PrivateAttr(default_factory=lambda: datetime.now(timezone.utc))
|
|
73
|
+
|
|
74
|
+
def allows(self, agent: AgentIdentity, *, now: datetime | None = None) -> bool:
|
|
75
|
+
now = now or datetime.now(timezone.utc)
|
|
76
|
+
if now - self._created_at >= self.ttl:
|
|
77
|
+
if self.then:
|
|
78
|
+
return self.then.allows(agent, now=now)
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def ensure_visibility(value: Visibility | None) -> Visibility:
|
|
84
|
+
if value is None:
|
|
85
|
+
return PublicVisibility()
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def only_for(*agent_names: str) -> PrivateVisibility:
|
|
90
|
+
return PrivateVisibility(agents=set(agent_names))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def agents_from_names(names: Iterable[str]) -> set[str]: # pragma: no cover - helper
|
|
94
|
+
return set(names)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
"AfterVisibility",
|
|
99
|
+
"AgentIdentity",
|
|
100
|
+
"LabelledVisibility",
|
|
101
|
+
"PrivateVisibility",
|
|
102
|
+
"PublicVisibility",
|
|
103
|
+
"TenantVisibility",
|
|
104
|
+
"Visibility",
|
|
105
|
+
"ensure_visibility",
|
|
106
|
+
"only_for",
|
|
107
|
+
]
|