flock-core 0.5.10__py3-none-any.whl → 0.5.20__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 +1 -1
- flock/agent/__init__.py +30 -0
- flock/agent/builder_helpers.py +192 -0
- flock/agent/builder_validator.py +169 -0
- flock/agent/component_lifecycle.py +325 -0
- flock/agent/context_resolver.py +141 -0
- flock/agent/mcp_integration.py +212 -0
- flock/agent/output_processor.py +304 -0
- flock/api/__init__.py +20 -0
- flock/api/models.py +283 -0
- flock/{service.py → api/service.py} +121 -63
- flock/cli.py +2 -2
- flock/components/__init__.py +41 -0
- flock/components/agent/__init__.py +22 -0
- flock/{components.py → components/agent/base.py} +4 -3
- flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
- flock/components/orchestrator/__init__.py +22 -0
- flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
- flock/components/orchestrator/circuit_breaker.py +95 -0
- flock/components/orchestrator/collection.py +143 -0
- flock/components/orchestrator/deduplication.py +78 -0
- flock/core/__init__.py +30 -0
- flock/core/agent.py +953 -0
- flock/{artifacts.py → core/artifacts.py} +1 -1
- flock/{context_provider.py → core/context_provider.py} +3 -3
- flock/core/orchestrator.py +1102 -0
- flock/{store.py → core/store.py} +99 -454
- flock/{subscription.py → core/subscription.py} +1 -1
- flock/dashboard/collector.py +5 -5
- flock/dashboard/graph_builder.py +7 -7
- flock/dashboard/routes/__init__.py +21 -0
- flock/dashboard/routes/control.py +327 -0
- flock/dashboard/routes/helpers.py +340 -0
- flock/dashboard/routes/themes.py +76 -0
- flock/dashboard/routes/traces.py +521 -0
- flock/dashboard/routes/websocket.py +108 -0
- flock/dashboard/service.py +44 -1294
- flock/engines/dspy/__init__.py +20 -0
- flock/engines/dspy/artifact_materializer.py +216 -0
- flock/engines/dspy/signature_builder.py +474 -0
- flock/engines/dspy/streaming_executor.py +858 -0
- flock/engines/dspy_engine.py +45 -1330
- flock/engines/examples/simple_batch_engine.py +2 -2
- flock/examples.py +7 -7
- flock/logging/logging.py +1 -16
- flock/models/__init__.py +10 -0
- flock/models/system_artifacts.py +33 -0
- flock/orchestrator/__init__.py +45 -0
- flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
- flock/orchestrator/artifact_manager.py +168 -0
- flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
- flock/orchestrator/component_runner.py +389 -0
- flock/orchestrator/context_builder.py +167 -0
- flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
- flock/orchestrator/event_emitter.py +167 -0
- flock/orchestrator/initialization.py +184 -0
- flock/orchestrator/lifecycle_manager.py +226 -0
- flock/orchestrator/mcp_manager.py +202 -0
- flock/orchestrator/scheduler.py +189 -0
- flock/orchestrator/server_manager.py +234 -0
- flock/orchestrator/tracing.py +147 -0
- flock/storage/__init__.py +10 -0
- flock/storage/artifact_aggregator.py +158 -0
- flock/storage/in_memory/__init__.py +6 -0
- flock/storage/in_memory/artifact_filter.py +114 -0
- flock/storage/in_memory/history_aggregator.py +115 -0
- flock/storage/sqlite/__init__.py +10 -0
- flock/storage/sqlite/agent_history_queries.py +154 -0
- flock/storage/sqlite/consumption_loader.py +100 -0
- flock/storage/sqlite/query_builder.py +112 -0
- flock/storage/sqlite/query_params_builder.py +91 -0
- flock/storage/sqlite/schema_manager.py +168 -0
- flock/storage/sqlite/summary_queries.py +194 -0
- flock/utils/__init__.py +14 -0
- flock/utils/async_utils.py +67 -0
- flock/{runtime.py → utils/runtime.py} +3 -3
- flock/utils/time_utils.py +53 -0
- flock/utils/type_resolution.py +38 -0
- flock/{utilities.py → utils/utilities.py} +2 -2
- flock/utils/validation.py +57 -0
- flock/utils/visibility.py +79 -0
- flock/utils/visibility_utils.py +134 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/METADATA +69 -61
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/RECORD +89 -31
- flock/agent.py +0 -1578
- flock/orchestrator.py +0 -1746
- /flock/{visibility.py → core/visibility.py} +0 -0
- /flock/{helper → utils}/cli_helper.py +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/WHEEL +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""SQLite summary query utilities.
|
|
2
|
+
|
|
3
|
+
Provides focused methods for executing summary/aggregation queries.
|
|
4
|
+
Extracted from summarize_artifacts to reduce complexity and improve testability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import aiosqlite
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQLiteSummaryQueries:
|
|
17
|
+
"""
|
|
18
|
+
Executes summary SQL queries for artifact statistics.
|
|
19
|
+
|
|
20
|
+
Each method handles one specific aggregation query, making them
|
|
21
|
+
simple, testable, and easy to maintain.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
async def count_total(
|
|
25
|
+
self,
|
|
26
|
+
conn: aiosqlite.Connection,
|
|
27
|
+
where_clause: str,
|
|
28
|
+
params: tuple[Any, ...],
|
|
29
|
+
) -> int:
|
|
30
|
+
"""
|
|
31
|
+
Get total artifact count.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
conn: Database connection
|
|
35
|
+
where_clause: SQL WHERE clause (e.g., " WHERE type = ?")
|
|
36
|
+
params: Parameter values for WHERE clause
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Total count of matching artifacts
|
|
40
|
+
"""
|
|
41
|
+
count_query = f"SELECT COUNT(*) AS total FROM artifacts{where_clause}" # nosec B608
|
|
42
|
+
cursor = await conn.execute(count_query, params)
|
|
43
|
+
total_row = await cursor.fetchone()
|
|
44
|
+
await cursor.close()
|
|
45
|
+
return total_row["total"] if total_row else 0
|
|
46
|
+
|
|
47
|
+
async def group_by_type(
|
|
48
|
+
self,
|
|
49
|
+
conn: aiosqlite.Connection,
|
|
50
|
+
where_clause: str,
|
|
51
|
+
params: tuple[Any, ...],
|
|
52
|
+
) -> dict[str, int]:
|
|
53
|
+
"""
|
|
54
|
+
Get artifact counts grouped by type.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
conn: Database connection
|
|
58
|
+
where_clause: SQL WHERE clause
|
|
59
|
+
params: Parameter values for WHERE clause
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict mapping canonical type names to counts
|
|
63
|
+
"""
|
|
64
|
+
by_type_query = f"""
|
|
65
|
+
SELECT canonical_type, COUNT(*) AS count
|
|
66
|
+
FROM artifacts
|
|
67
|
+
{where_clause}
|
|
68
|
+
GROUP BY canonical_type
|
|
69
|
+
""" # nosec B608
|
|
70
|
+
cursor = await conn.execute(by_type_query, params)
|
|
71
|
+
by_type_rows = await cursor.fetchall()
|
|
72
|
+
await cursor.close()
|
|
73
|
+
return {row["canonical_type"]: row["count"] for row in by_type_rows}
|
|
74
|
+
|
|
75
|
+
async def group_by_producer(
|
|
76
|
+
self,
|
|
77
|
+
conn: aiosqlite.Connection,
|
|
78
|
+
where_clause: str,
|
|
79
|
+
params: tuple[Any, ...],
|
|
80
|
+
) -> dict[str, int]:
|
|
81
|
+
"""
|
|
82
|
+
Get artifact counts grouped by producer.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
conn: Database connection
|
|
86
|
+
where_clause: SQL WHERE clause
|
|
87
|
+
params: Parameter values for WHERE clause
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dict mapping producer names to counts
|
|
91
|
+
"""
|
|
92
|
+
by_producer_query = f"""
|
|
93
|
+
SELECT produced_by, COUNT(*) AS count
|
|
94
|
+
FROM artifacts
|
|
95
|
+
{where_clause}
|
|
96
|
+
GROUP BY produced_by
|
|
97
|
+
""" # nosec B608
|
|
98
|
+
cursor = await conn.execute(by_producer_query, params)
|
|
99
|
+
by_producer_rows = await cursor.fetchall()
|
|
100
|
+
await cursor.close()
|
|
101
|
+
return {row["produced_by"]: row["count"] for row in by_producer_rows}
|
|
102
|
+
|
|
103
|
+
async def group_by_visibility(
|
|
104
|
+
self,
|
|
105
|
+
conn: aiosqlite.Connection,
|
|
106
|
+
where_clause: str,
|
|
107
|
+
params: tuple[Any, ...],
|
|
108
|
+
) -> dict[str, int]:
|
|
109
|
+
"""
|
|
110
|
+
Get artifact counts grouped by visibility kind.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
conn: Database connection
|
|
114
|
+
where_clause: SQL WHERE clause
|
|
115
|
+
params: Parameter values for WHERE clause
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict mapping visibility kinds to counts
|
|
119
|
+
"""
|
|
120
|
+
by_visibility_query = f"""
|
|
121
|
+
SELECT json_extract(visibility, '$.kind') AS visibility_kind, COUNT(*) AS count
|
|
122
|
+
FROM artifacts
|
|
123
|
+
{where_clause}
|
|
124
|
+
GROUP BY json_extract(visibility, '$.kind')
|
|
125
|
+
""" # nosec B608
|
|
126
|
+
cursor = await conn.execute(by_visibility_query, params)
|
|
127
|
+
by_visibility_rows = await cursor.fetchall()
|
|
128
|
+
await cursor.close()
|
|
129
|
+
return {
|
|
130
|
+
(row["visibility_kind"] or "Unknown"): row["count"]
|
|
131
|
+
for row in by_visibility_rows
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async def count_tags(
|
|
135
|
+
self,
|
|
136
|
+
conn: aiosqlite.Connection,
|
|
137
|
+
where_clause: str,
|
|
138
|
+
params: tuple[Any, ...],
|
|
139
|
+
) -> dict[str, int]:
|
|
140
|
+
"""
|
|
141
|
+
Get tag occurrence counts.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
conn: Database connection
|
|
145
|
+
where_clause: SQL WHERE clause
|
|
146
|
+
params: Parameter values for WHERE clause
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dict mapping tag names to occurrence counts
|
|
150
|
+
"""
|
|
151
|
+
tag_query = f"""
|
|
152
|
+
SELECT json_each.value AS tag, COUNT(*) AS count
|
|
153
|
+
FROM artifacts
|
|
154
|
+
JOIN json_each(artifacts.tags)
|
|
155
|
+
{where_clause}
|
|
156
|
+
GROUP BY json_each.value
|
|
157
|
+
""" # nosec B608
|
|
158
|
+
cursor = await conn.execute(tag_query, params)
|
|
159
|
+
tag_rows = await cursor.fetchall()
|
|
160
|
+
await cursor.close()
|
|
161
|
+
return {row["tag"]: row["count"] for row in tag_rows}
|
|
162
|
+
|
|
163
|
+
async def get_date_range(
|
|
164
|
+
self,
|
|
165
|
+
conn: aiosqlite.Connection,
|
|
166
|
+
where_clause: str,
|
|
167
|
+
params: tuple[Any, ...],
|
|
168
|
+
) -> tuple[str | None, str | None]:
|
|
169
|
+
"""
|
|
170
|
+
Get earliest and latest creation timestamps.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
conn: Database connection
|
|
174
|
+
where_clause: SQL WHERE clause
|
|
175
|
+
params: Parameter values for WHERE clause
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Tuple of (earliest, latest) ISO timestamp strings, or (None, None)
|
|
179
|
+
"""
|
|
180
|
+
range_query = f"""
|
|
181
|
+
SELECT MIN(created_at) AS earliest, MAX(created_at) AS latest
|
|
182
|
+
FROM artifacts
|
|
183
|
+
{where_clause}
|
|
184
|
+
""" # nosec B608
|
|
185
|
+
cursor = await conn.execute(range_query, params)
|
|
186
|
+
range_row = await cursor.fetchone()
|
|
187
|
+
await cursor.close()
|
|
188
|
+
|
|
189
|
+
if not range_row:
|
|
190
|
+
return None, None
|
|
191
|
+
|
|
192
|
+
earliest = range_row["earliest"] if range_row["earliest"] else None
|
|
193
|
+
latest = range_row["latest"] if range_row["latest"] else None
|
|
194
|
+
return earliest, latest
|
flock/utils/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shared utilities for Flock framework."""
|
|
2
|
+
|
|
3
|
+
from flock.utils.type_resolution import TypeResolutionHelper
|
|
4
|
+
from flock.utils.visibility import VisibilityDeserializer
|
|
5
|
+
from flock.utils.async_utils import async_lock_required, AsyncLockRequired
|
|
6
|
+
from flock.utils.validation import ArtifactValidator
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"TypeResolutionHelper",
|
|
10
|
+
"VisibilityDeserializer",
|
|
11
|
+
"async_lock_required",
|
|
12
|
+
"AsyncLockRequired",
|
|
13
|
+
"ArtifactValidator",
|
|
14
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Async utility decorators and helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncLockRequired:
|
|
12
|
+
"""Decorator ensuring async lock acquisition.
|
|
13
|
+
|
|
14
|
+
This utility eliminates 15+ duplicate lock acquisition patterns
|
|
15
|
+
scattered throughout orchestrator.py and agent.py.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, lock_attr: str = "_lock"):
|
|
19
|
+
"""
|
|
20
|
+
Initialize decorator.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
lock_attr: Name of lock attribute on class (default: "_lock")
|
|
24
|
+
"""
|
|
25
|
+
self.lock_attr = lock_attr
|
|
26
|
+
|
|
27
|
+
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
28
|
+
"""Apply decorator to function."""
|
|
29
|
+
lock_attr = self.lock_attr # Capture in closure
|
|
30
|
+
|
|
31
|
+
@wraps(func)
|
|
32
|
+
async def wrapper(instance: Any, *args: Any, **kwargs: Any) -> Any:
|
|
33
|
+
lock = getattr(instance, lock_attr)
|
|
34
|
+
async with lock:
|
|
35
|
+
return await func(instance, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def async_lock_required(lock_attr: str = "_lock") -> AsyncLockRequired:
|
|
41
|
+
"""
|
|
42
|
+
Decorator ensuring async lock acquisition.
|
|
43
|
+
|
|
44
|
+
This decorator automatically acquires and releases an async lock
|
|
45
|
+
before executing the decorated method, preventing race conditions.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
lock_attr: Name of the lock attribute on the class (default: "_lock")
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
AsyncLockRequired decorator instance
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> class MyClass:
|
|
55
|
+
... def __init__(self):
|
|
56
|
+
... self._lock = asyncio.Lock()
|
|
57
|
+
...
|
|
58
|
+
... @async_lock_required()
|
|
59
|
+
... async def my_method(self):
|
|
60
|
+
... # Lock automatically acquired here
|
|
61
|
+
... await asyncio.sleep(0.1)
|
|
62
|
+
... return "done"
|
|
63
|
+
|
|
64
|
+
>>> obj = MyClass()
|
|
65
|
+
>>> result = await obj.my_method() # Lock acquired/released automatically
|
|
66
|
+
"""
|
|
67
|
+
return AsyncLockRequired(lock_attr)
|
|
@@ -7,7 +7,7 @@ from uuid import UUID
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
9
9
|
|
|
10
|
-
from flock.artifacts import Artifact
|
|
10
|
+
from flock.core.artifacts import Artifact
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EvalInputs(BaseModel):
|
|
@@ -99,7 +99,7 @@ class EvalResult(BaseModel):
|
|
|
99
99
|
... )
|
|
100
100
|
... return EvalResult.from_object(processed, agent=agent)
|
|
101
101
|
"""
|
|
102
|
-
from flock.artifacts import Artifact
|
|
102
|
+
from flock.core.artifacts import Artifact
|
|
103
103
|
from flock.registry import type_registry
|
|
104
104
|
|
|
105
105
|
type_name = type_registry.name_for(type(obj))
|
|
@@ -154,7 +154,7 @@ class EvalResult(BaseModel):
|
|
|
154
154
|
... movie, tagline, agent=agent, metrics={"confidence": 0.9}
|
|
155
155
|
... )
|
|
156
156
|
"""
|
|
157
|
-
from flock.artifacts import Artifact
|
|
157
|
+
from flock.core.artifacts import Artifact
|
|
158
158
|
from flock.registry import type_registry
|
|
159
159
|
|
|
160
160
|
artifacts = []
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Time formatting utilities.
|
|
2
|
+
|
|
3
|
+
Provides human-readable time span formatting for date ranges.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_time_span(earliest: datetime | None, latest: datetime | None) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Format time span between two datetimes as human-readable string.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
earliest: Start datetime
|
|
17
|
+
latest: End datetime
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Human-readable span description:
|
|
21
|
+
- "X days" for spans >= 2 days
|
|
22
|
+
- "X.Y hours" for spans >= 1 hour
|
|
23
|
+
- "X minutes" for spans > 0
|
|
24
|
+
- "moments" for zero span
|
|
25
|
+
- "empty" if no dates provided
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> from datetime import datetime, timedelta
|
|
29
|
+
>>> now = datetime.now()
|
|
30
|
+
>>> format_time_span(now, now + timedelta(days=3))
|
|
31
|
+
"3 days"
|
|
32
|
+
>>> format_time_span(now, now + timedelta(hours=2))
|
|
33
|
+
"2.0 hours"
|
|
34
|
+
>>> format_time_span(now, now + timedelta(minutes=45))
|
|
35
|
+
"45 minutes"
|
|
36
|
+
"""
|
|
37
|
+
if not earliest or not latest:
|
|
38
|
+
return "empty"
|
|
39
|
+
|
|
40
|
+
span = latest - earliest
|
|
41
|
+
|
|
42
|
+
if span.days >= 2:
|
|
43
|
+
return f"{span.days} days"
|
|
44
|
+
|
|
45
|
+
if span.total_seconds() >= 3600:
|
|
46
|
+
hours = span.total_seconds() / 3600
|
|
47
|
+
return f"{hours:.1f} hours"
|
|
48
|
+
|
|
49
|
+
if span.total_seconds() > 0:
|
|
50
|
+
minutes = max(1, int(span.total_seconds() / 60))
|
|
51
|
+
return f"{minutes} minutes"
|
|
52
|
+
|
|
53
|
+
return "moments"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Type registry resolution utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from flock.registry import TypeRegistry
|
|
7
|
+
|
|
8
|
+
from flock.registry import RegistryError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TypeResolutionHelper:
|
|
12
|
+
"""Helper for safe type resolution.
|
|
13
|
+
|
|
14
|
+
This utility eliminates 8+ duplicate type resolution patterns
|
|
15
|
+
scattered across agent.py, store.py, orchestrator.py, and context_provider.py.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def safe_resolve(registry: "TypeRegistry", type_name: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Safely resolve type name to canonical form.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
registry: Type registry instance
|
|
25
|
+
type_name: Type name to resolve
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Canonical type name (or original if not found)
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> canonical = TypeResolutionHelper.safe_resolve(registry, "MyType")
|
|
32
|
+
>>> # Returns "my_module.MyType" if found, else "MyType"
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return registry.resolve_name(type_name)
|
|
36
|
+
except RegistryError:
|
|
37
|
+
# Type not found or ambiguous - return original name
|
|
38
|
+
return type_name
|
|
@@ -19,11 +19,11 @@ from rich.pretty import Pretty
|
|
|
19
19
|
from rich.table import Table
|
|
20
20
|
from rich.text import Text
|
|
21
21
|
|
|
22
|
-
from flock.components import AgentComponent
|
|
22
|
+
from flock.components.agent import AgentComponent
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from flock.runtime import Context, EvalInputs, EvalResult
|
|
26
|
+
from flock.utils.runtime import Context, EvalInputs, EvalResult
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class MetricsUtility(AgentComponent):
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Common validation utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ValidationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArtifactValidator:
|
|
9
|
+
"""Validates artifacts against predicates.
|
|
10
|
+
|
|
11
|
+
This utility consolidates artifact validation patterns
|
|
12
|
+
used across agent.py for output validation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def validate_artifact(
|
|
17
|
+
artifact: Any,
|
|
18
|
+
model_cls: type[BaseModel],
|
|
19
|
+
predicate: Callable[[BaseModel], bool] | None = None,
|
|
20
|
+
) -> tuple[bool, BaseModel | None, str | None]:
|
|
21
|
+
"""
|
|
22
|
+
Validate artifact payload against model and optional predicate.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
artifact: Artifact to validate
|
|
26
|
+
model_cls: Pydantic model class
|
|
27
|
+
predicate: Optional validation predicate
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Tuple of (is_valid, model_instance, error_message)
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> from pydantic import BaseModel
|
|
34
|
+
>>> class MyModel(BaseModel):
|
|
35
|
+
... name: str
|
|
36
|
+
... age: int
|
|
37
|
+
>>> artifact = type("obj", (), {"payload": {"name": "Alice", "age": 30}})()
|
|
38
|
+
>>> is_valid, model, error = ArtifactValidator.validate_artifact(
|
|
39
|
+
... artifact, MyModel, lambda m: m.age >= 18
|
|
40
|
+
... )
|
|
41
|
+
>>> assert is_valid
|
|
42
|
+
>>> assert model.name == "Alice"
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
# Validate against model
|
|
46
|
+
model_instance = model_cls(**artifact.payload)
|
|
47
|
+
|
|
48
|
+
# Apply predicate if provided
|
|
49
|
+
if predicate and not predicate(model_instance):
|
|
50
|
+
return False, model_instance, "Predicate validation failed"
|
|
51
|
+
|
|
52
|
+
return True, model_instance, None
|
|
53
|
+
|
|
54
|
+
except ValidationError as e:
|
|
55
|
+
return False, None, str(e)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return False, None, f"Validation error: {e}"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Visibility deserialization utilities."""
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flock.core.visibility import (
|
|
7
|
+
AfterVisibility,
|
|
8
|
+
LabelledVisibility,
|
|
9
|
+
PrivateVisibility,
|
|
10
|
+
PublicVisibility,
|
|
11
|
+
TenantVisibility,
|
|
12
|
+
Visibility,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VisibilityDeserializer:
|
|
17
|
+
"""Deserializes visibility from dict/str representation.
|
|
18
|
+
|
|
19
|
+
This utility eliminates 5+ duplicate visibility deserialization patterns
|
|
20
|
+
scattered across store.py, context_provider.py, and orchestrator.py.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def deserialize(data: dict[str, Any] | str) -> Visibility:
|
|
25
|
+
"""
|
|
26
|
+
Deserialize visibility from various formats.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
data: Dict with 'kind' field or string
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Visibility instance
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If visibility kind is unknown
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> vis = VisibilityDeserializer.deserialize({"kind": "Public"})
|
|
39
|
+
>>> assert isinstance(vis, PublicVisibility)
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(data, str):
|
|
42
|
+
kind = data
|
|
43
|
+
props = {}
|
|
44
|
+
else:
|
|
45
|
+
kind = data.get("kind")
|
|
46
|
+
props = data
|
|
47
|
+
|
|
48
|
+
if kind == "Public":
|
|
49
|
+
return PublicVisibility()
|
|
50
|
+
|
|
51
|
+
if kind == "Private":
|
|
52
|
+
agents = set(props.get("agents", []))
|
|
53
|
+
return PrivateVisibility(agents=agents)
|
|
54
|
+
|
|
55
|
+
if kind == "Labelled":
|
|
56
|
+
required_labels = set(props.get("required_labels", []))
|
|
57
|
+
return LabelledVisibility(required_labels=required_labels)
|
|
58
|
+
|
|
59
|
+
if kind == "Tenant":
|
|
60
|
+
tenant_id = props.get("tenant_id")
|
|
61
|
+
return TenantVisibility(tenant_id=tenant_id)
|
|
62
|
+
|
|
63
|
+
if kind == "After":
|
|
64
|
+
ttl_value = props.get("ttl")
|
|
65
|
+
# Handle timedelta or raw seconds
|
|
66
|
+
if isinstance(ttl_value, (int, float)):
|
|
67
|
+
ttl = timedelta(seconds=ttl_value)
|
|
68
|
+
elif isinstance(ttl_value, dict):
|
|
69
|
+
# Pydantic dict representation
|
|
70
|
+
ttl = timedelta(**ttl_value)
|
|
71
|
+
else:
|
|
72
|
+
ttl = ttl_value or timedelta()
|
|
73
|
+
|
|
74
|
+
then_data = props.get("then")
|
|
75
|
+
then = VisibilityDeserializer.deserialize(then_data) if then_data else None
|
|
76
|
+
|
|
77
|
+
return AfterVisibility(ttl=ttl, then=then)
|
|
78
|
+
|
|
79
|
+
raise ValueError(f"Unknown visibility kind: {kind}")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Visibility deserialization utilities.
|
|
2
|
+
|
|
3
|
+
This module handles complex visibility object deserialization from JSON data.
|
|
4
|
+
Extracted from store.py to reduce complexity and improve testability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from datetime import timedelta
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from flock.core.visibility import (
|
|
14
|
+
AfterVisibility,
|
|
15
|
+
LabelledVisibility,
|
|
16
|
+
PrivateVisibility,
|
|
17
|
+
PublicVisibility,
|
|
18
|
+
TenantVisibility,
|
|
19
|
+
Visibility,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ISO_DURATION_RE = re.compile(
|
|
24
|
+
r"^P(?:T?(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)$"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_iso_duration(value: str | None) -> timedelta:
|
|
29
|
+
"""
|
|
30
|
+
Parse ISO 8601 duration string to timedelta.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
value: ISO 8601 duration string (e.g., "PT1H30M")
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Parsed timedelta, or zero timedelta if invalid
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> parse_iso_duration("PT1H")
|
|
40
|
+
timedelta(hours=1)
|
|
41
|
+
>>> parse_iso_duration("PT30M")
|
|
42
|
+
timedelta(minutes=30)
|
|
43
|
+
>>> parse_iso_duration(None)
|
|
44
|
+
timedelta(0)
|
|
45
|
+
"""
|
|
46
|
+
if not value:
|
|
47
|
+
return timedelta(0)
|
|
48
|
+
match = ISO_DURATION_RE.match(value)
|
|
49
|
+
if not match:
|
|
50
|
+
return timedelta(0)
|
|
51
|
+
hours = int(match.group("hours") or 0)
|
|
52
|
+
minutes = int(match.group("minutes") or 0)
|
|
53
|
+
seconds = int(match.group("seconds") or 0)
|
|
54
|
+
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def deserialize_visibility(data: Any) -> Visibility:
|
|
58
|
+
"""
|
|
59
|
+
Deserialize visibility object from JSON data.
|
|
60
|
+
|
|
61
|
+
Handles all visibility types: Public, Private, Labelled, Tenant, After.
|
|
62
|
+
Uses dictionary dispatch to reduce complexity vs if-elif chain.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
data: JSON data dict or Visibility instance
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Visibility object (defaults to PublicVisibility if invalid)
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
>>> deserialize_visibility({"kind": "Public"})
|
|
72
|
+
PublicVisibility()
|
|
73
|
+
>>> deserialize_visibility({"kind": "Private", "agents": ["agent1"]})
|
|
74
|
+
PrivateVisibility(agents={"agent1"})
|
|
75
|
+
"""
|
|
76
|
+
# Early returns for simple cases
|
|
77
|
+
if isinstance(data, Visibility):
|
|
78
|
+
return data
|
|
79
|
+
if not data:
|
|
80
|
+
return PublicVisibility()
|
|
81
|
+
|
|
82
|
+
# Extract kind
|
|
83
|
+
kind = data.get("kind") if isinstance(data, dict) else None
|
|
84
|
+
if not kind:
|
|
85
|
+
return PublicVisibility()
|
|
86
|
+
|
|
87
|
+
# Dispatch to appropriate deserializer
|
|
88
|
+
return _VISIBILITY_DESERIALIZERS.get(kind, _deserialize_public)(data)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _deserialize_public(data: dict[str, Any]) -> PublicVisibility:
|
|
92
|
+
"""Deserialize PublicVisibility."""
|
|
93
|
+
return PublicVisibility()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _deserialize_private(data: dict[str, Any]) -> PrivateVisibility:
|
|
97
|
+
"""Deserialize PrivateVisibility."""
|
|
98
|
+
return PrivateVisibility(agents=set(data.get("agents", [])))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _deserialize_labelled(data: dict[str, Any]) -> LabelledVisibility:
|
|
102
|
+
"""Deserialize LabelledVisibility."""
|
|
103
|
+
return LabelledVisibility(required_labels=set(data.get("required_labels", [])))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _deserialize_tenant(data: dict[str, Any]) -> TenantVisibility:
|
|
107
|
+
"""Deserialize TenantVisibility."""
|
|
108
|
+
return TenantVisibility(tenant_id=data.get("tenant_id"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _deserialize_after(data: dict[str, Any]) -> AfterVisibility:
|
|
112
|
+
"""
|
|
113
|
+
Deserialize AfterVisibility with recursive 'then' handling.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
data: JSON data dict with 'ttl' and optional 'then' fields
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
AfterVisibility instance
|
|
120
|
+
"""
|
|
121
|
+
ttl = parse_iso_duration(data.get("ttl"))
|
|
122
|
+
then_data = data.get("then") if isinstance(data, dict) else None
|
|
123
|
+
then_visibility = deserialize_visibility(then_data) if then_data else None
|
|
124
|
+
return AfterVisibility(ttl=ttl, then=then_visibility)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Dispatch table for visibility types
|
|
128
|
+
_VISIBILITY_DESERIALIZERS = {
|
|
129
|
+
"Public": _deserialize_public,
|
|
130
|
+
"Private": _deserialize_private,
|
|
131
|
+
"Labelled": _deserialize_labelled,
|
|
132
|
+
"Tenant": _deserialize_tenant,
|
|
133
|
+
"After": _deserialize_after,
|
|
134
|
+
}
|