flock-core 0.5.9__py3-none-any.whl → 0.5.10__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/agent.py +149 -62
- flock/api/themes.py +6 -2
- flock/artifact_collector.py +6 -3
- flock/batch_accumulator.py +3 -1
- flock/cli.py +3 -1
- flock/components.py +45 -56
- flock/context_provider.py +531 -0
- flock/correlation_engine.py +8 -4
- flock/dashboard/collector.py +48 -29
- flock/dashboard/events.py +10 -4
- flock/dashboard/launcher.py +3 -1
- flock/dashboard/models/graph.py +9 -3
- flock/dashboard/service.py +143 -72
- flock/dashboard/websocket.py +17 -4
- flock/engines/dspy_engine.py +174 -98
- flock/engines/examples/simple_batch_engine.py +9 -3
- flock/examples.py +6 -2
- flock/frontend/src/services/indexeddb.test.ts +4 -4
- flock/frontend/src/services/indexeddb.ts +1 -1
- flock/helper/cli_helper.py +14 -1
- flock/logging/auto_trace.py +6 -1
- flock/logging/formatters/enum_builder.py +3 -1
- flock/logging/formatters/theme_builder.py +32 -17
- flock/logging/formatters/themed_formatter.py +38 -22
- flock/logging/logging.py +21 -7
- flock/logging/telemetry.py +9 -3
- flock/logging/telemetry_exporter/duckdb_exporter.py +27 -25
- flock/logging/trace_and_logged.py +14 -5
- flock/mcp/__init__.py +3 -6
- flock/mcp/client.py +49 -19
- flock/mcp/config.py +12 -6
- flock/mcp/manager.py +6 -2
- flock/mcp/servers/sse/flock_sse_server.py +9 -3
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +6 -2
- flock/mcp/tool.py +18 -6
- flock/mcp/types/handlers.py +3 -1
- flock/mcp/types/types.py +9 -3
- flock/orchestrator.py +204 -50
- flock/orchestrator_component.py +15 -5
- flock/patches/dspy_streaming_patch.py +12 -4
- flock/registry.py +9 -3
- flock/runtime.py +69 -18
- flock/service.py +19 -6
- flock/store.py +29 -10
- flock/subscription.py +6 -4
- flock/utilities.py +41 -13
- flock/utility/output_utility_component.py +31 -11
- {flock_core-0.5.9.dist-info → flock_core-0.5.10.dist-info}/METADATA +132 -2
- {flock_core-0.5.9.dist-info → flock_core-0.5.10.dist-info}/RECORD +52 -51
- {flock_core-0.5.9.dist-info → flock_core-0.5.10.dist-info}/WHEEL +0 -0
- {flock_core-0.5.9.dist-info → flock_core-0.5.10.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.9.dist-info → flock_core-0.5.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,7 +21,9 @@ class BatchSummary(BaseModel):
|
|
|
21
21
|
"""Output payload describing the batch that was processed."""
|
|
22
22
|
|
|
23
23
|
batch_size: int = Field(description="Number of items included in this evaluation")
|
|
24
|
-
values: list[int] = Field(
|
|
24
|
+
values: list[int] = Field(
|
|
25
|
+
description="Original values processed", default_factory=list
|
|
26
|
+
)
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class SimpleBatchEngine(EngineComponent):
|
|
@@ -32,7 +34,9 @@ class SimpleBatchEngine(EngineComponent):
|
|
|
32
34
|
can verify that all artifacts were processed together.
|
|
33
35
|
"""
|
|
34
36
|
|
|
35
|
-
async def evaluate(
|
|
37
|
+
async def evaluate(
|
|
38
|
+
self, agent, ctx, inputs: EvalInputs, output_group
|
|
39
|
+
) -> EvalResult:
|
|
36
40
|
"""Process single item or batch with auto-detection.
|
|
37
41
|
|
|
38
42
|
Auto-detects batch mode via ctx.is_batch flag (set by orchestrator when
|
|
@@ -57,7 +61,9 @@ class SimpleBatchEngine(EngineComponent):
|
|
|
57
61
|
return EvalResult.empty()
|
|
58
62
|
|
|
59
63
|
batch_size = len(items)
|
|
60
|
-
summary = BatchSummary(
|
|
64
|
+
summary = BatchSummary(
|
|
65
|
+
batch_size=batch_size, values=[item.value for item in items]
|
|
66
|
+
)
|
|
61
67
|
|
|
62
68
|
state = dict(inputs.state)
|
|
63
69
|
state["batch_size"] = summary.batch_size
|
flock/examples.py
CHANGED
|
@@ -50,7 +50,9 @@ def announce(tagline: Tagline) -> dict[str, str]:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class MovieEngine(EngineComponent):
|
|
53
|
-
async def evaluate(
|
|
53
|
+
async def evaluate(
|
|
54
|
+
self, agent, ctx, inputs: EvalInputs, output_group
|
|
55
|
+
) -> EvalResult:
|
|
54
56
|
idea = Idea(**inputs.artifacts[0].payload)
|
|
55
57
|
synopsis = f"{idea.topic} told as a {idea.genre} adventure."
|
|
56
58
|
movie = Movie(fun_title=idea.topic.upper(), runtime=120, synopsis=synopsis)
|
|
@@ -63,7 +65,9 @@ class MovieEngine(EngineComponent):
|
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
class TaglineEngine(EngineComponent):
|
|
66
|
-
async def evaluate(
|
|
68
|
+
async def evaluate(
|
|
69
|
+
self, agent, ctx, inputs: EvalInputs, output_group
|
|
70
|
+
) -> EvalResult:
|
|
67
71
|
movie = Movie(**inputs.artifacts[0].payload)
|
|
68
72
|
tagline = Tagline(line=f"Don't miss {movie.fun_title}!")
|
|
69
73
|
artifact = Artifact(
|
|
@@ -117,7 +117,7 @@ describe('IndexedDBService', () => {
|
|
|
117
117
|
max_concurrency: 1,
|
|
118
118
|
consumes_types: ['Idea'],
|
|
119
119
|
from_agents: [],
|
|
120
|
-
|
|
120
|
+
tags: [],
|
|
121
121
|
run_history: [],
|
|
122
122
|
total_runs: 0,
|
|
123
123
|
total_errors: 0,
|
|
@@ -140,7 +140,7 @@ describe('IndexedDBService', () => {
|
|
|
140
140
|
max_concurrency: 1,
|
|
141
141
|
consumes_types: ['Idea'],
|
|
142
142
|
from_agents: [],
|
|
143
|
-
|
|
143
|
+
tags: [],
|
|
144
144
|
run_history: [],
|
|
145
145
|
total_runs: 0,
|
|
146
146
|
total_errors: 0,
|
|
@@ -168,7 +168,7 @@ describe('IndexedDBService', () => {
|
|
|
168
168
|
max_concurrency: 1,
|
|
169
169
|
consumes_types: [],
|
|
170
170
|
from_agents: [],
|
|
171
|
-
|
|
171
|
+
tags: [],
|
|
172
172
|
run_history: [],
|
|
173
173
|
total_runs: 0,
|
|
174
174
|
total_errors: 0,
|
|
@@ -612,7 +612,7 @@ describe('IndexedDBService', () => {
|
|
|
612
612
|
max_concurrency: 1,
|
|
613
613
|
consumes_types: [],
|
|
614
614
|
from_agents: [],
|
|
615
|
-
|
|
615
|
+
tags: [],
|
|
616
616
|
run_history: [],
|
|
617
617
|
total_runs: 0,
|
|
618
618
|
total_errors: 0,
|
flock/helper/cli_helper.py
CHANGED
|
@@ -41,9 +41,22 @@ def display_hummingbird():
|
|
|
41
41
|
|
|
42
42
|
def init_console(clear_screen: bool = True, show_banner: bool = True, model: str = ""):
|
|
43
43
|
"""Display the Flock banner."""
|
|
44
|
+
import sys
|
|
45
|
+
|
|
44
46
|
from rich.console import Console
|
|
45
47
|
from rich.syntax import Text
|
|
46
48
|
|
|
49
|
+
if sys.platform == "win32":
|
|
50
|
+
try:
|
|
51
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
52
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
53
|
+
except AttributeError:
|
|
54
|
+
# Python < 3.7
|
|
55
|
+
import codecs
|
|
56
|
+
|
|
57
|
+
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.buffer, "strict")
|
|
58
|
+
sys.stderr = codecs.getwriter("utf-8")(sys.stderr.buffer, "strict")
|
|
59
|
+
|
|
47
60
|
console = Console()
|
|
48
61
|
banner_text = Text(
|
|
49
62
|
f"""
|
|
@@ -52,7 +65,7 @@ def init_console(clear_screen: bool = True, show_banner: bool = True, model: str
|
|
|
52
65
|
│ ▒█▀▀▀ █░░ █▀▀█ █▀▀ █░█ │
|
|
53
66
|
│ ▒█▀▀▀ █░░ █░░█ █░░ █▀▄ │
|
|
54
67
|
│ ▒█░░░ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀░▀ │
|
|
55
|
-
╰━━━━━━━━━v{__version__}
|
|
68
|
+
╰━━━━━━━━━v{__version__}━━━━━━━━╯
|
|
56
69
|
🦆 🐤 🐧 🐓
|
|
57
70
|
""",
|
|
58
71
|
justify="center",
|
flock/logging/auto_trace.py
CHANGED
|
@@ -9,7 +9,12 @@ from flock.logging.trace_and_logged import _trace_filter_config, traced_and_logg
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# Check if auto-tracing is enabled via environment variable
|
|
12
|
-
ENABLE_AUTO_TRACE = os.getenv("FLOCK_AUTO_TRACE", "true").lower() in {
|
|
12
|
+
ENABLE_AUTO_TRACE = os.getenv("FLOCK_AUTO_TRACE", "true").lower() in {
|
|
13
|
+
"true",
|
|
14
|
+
"1",
|
|
15
|
+
"yes",
|
|
16
|
+
"on",
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
# Parse trace filter configuration from environment variables
|
|
@@ -10,7 +10,9 @@ theme_folder = pathlib.Path(__file__).parent.parent.parent / "themes"
|
|
|
10
10
|
if not theme_folder.exists():
|
|
11
11
|
raise FileNotFoundError(f"Theme folder not found: {theme_folder}")
|
|
12
12
|
|
|
13
|
-
theme_files = [
|
|
13
|
+
theme_files = [
|
|
14
|
+
pathlib.Path(f.path).stem for f in os.scandir(theme_folder) if f.is_file()
|
|
15
|
+
]
|
|
14
16
|
|
|
15
17
|
theme_enum_entries = {}
|
|
16
18
|
for theme in theme_files:
|
|
@@ -47,15 +47,13 @@ def generate_default_rich_block(theme: dict | None = None) -> dict[str, Any]:
|
|
|
47
47
|
"""
|
|
48
48
|
|
|
49
49
|
def random_background():
|
|
50
|
-
return random.choice(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
]
|
|
58
|
-
)
|
|
50
|
+
return random.choice([
|
|
51
|
+
f"{normal_black}",
|
|
52
|
+
f"{normal_blue}",
|
|
53
|
+
f"{primary_background}",
|
|
54
|
+
f"{selection_background}",
|
|
55
|
+
f"{cursor_cursor}",
|
|
56
|
+
])
|
|
59
57
|
|
|
60
58
|
if theme is not None:
|
|
61
59
|
bright = theme["colors"].get("bright", {})
|
|
@@ -148,12 +146,21 @@ def generate_default_rich_block(theme: dict | None = None) -> dict[str, Any]:
|
|
|
148
146
|
# Non-color layout properties, randomly chosen.
|
|
149
147
|
default_non_color_props = {
|
|
150
148
|
"table_show_lines": random.choice([True, False]),
|
|
151
|
-
"table_box": random.choice(
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
"table_box": random.choice([
|
|
150
|
+
"ROUNDED",
|
|
151
|
+
"SIMPLE",
|
|
152
|
+
"SQUARE",
|
|
153
|
+
"MINIMAL",
|
|
154
|
+
"HEAVY",
|
|
155
|
+
"DOUBLE_EDGE",
|
|
156
|
+
]),
|
|
154
157
|
"panel_padding": random.choice([[1, 2], [1, 1], [2, 2], [0, 2]]),
|
|
155
158
|
"panel_title_align": random.choice(["left", "center", "right"]),
|
|
156
|
-
"table_row_styles": random.choice([
|
|
159
|
+
"table_row_styles": random.choice([
|
|
160
|
+
["", "dim"],
|
|
161
|
+
["", "italic"],
|
|
162
|
+
["", "underline"],
|
|
163
|
+
]),
|
|
157
164
|
}
|
|
158
165
|
# Extra table layout properties (non-content).
|
|
159
166
|
default_extra_table_props = {
|
|
@@ -275,9 +282,13 @@ def create_rich_renderable(
|
|
|
275
282
|
sub_tables = []
|
|
276
283
|
for i, item in enumerate(value):
|
|
277
284
|
sub_tables.append(f"[bold]Item {i + 1}[/bold]")
|
|
278
|
-
sub_tables.append(
|
|
285
|
+
sub_tables.append(
|
|
286
|
+
create_rich_renderable(item, level + 1, theme, styles)
|
|
287
|
+
)
|
|
279
288
|
return Group(*sub_tables)
|
|
280
|
-
rendered_items = [
|
|
289
|
+
rendered_items = [
|
|
290
|
+
create_rich_renderable(item, level + 1, theme, styles) for item in value
|
|
291
|
+
]
|
|
281
292
|
if all(isinstance(item, str) for item in rendered_items):
|
|
282
293
|
return "\n".join(rendered_items)
|
|
283
294
|
return Group(*rendered_items)
|
|
@@ -422,7 +433,9 @@ def theme_builder():
|
|
|
422
433
|
samples = []
|
|
423
434
|
for i, rich_block in enumerate(sample_rich_blocks):
|
|
424
435
|
# Build a sample theme: copy the chosen theme and override its [rich] block.
|
|
425
|
-
sample_theme = dict(
|
|
436
|
+
sample_theme = dict(
|
|
437
|
+
chosen_theme
|
|
438
|
+
) # shallow copy (good enough if colors remain unchanged)
|
|
426
439
|
sample_theme["rich"] = rich_block
|
|
427
440
|
sample_table = generate_sample_table(sample_theme, dummy_data)
|
|
428
441
|
samples.append((sample_theme, sample_table))
|
|
@@ -445,7 +458,9 @@ def theme_builder():
|
|
|
445
458
|
return
|
|
446
459
|
|
|
447
460
|
# Ask for file name to save the chosen theme.
|
|
448
|
-
filename = console.input(
|
|
461
|
+
filename = console.input(
|
|
462
|
+
"\nEnter a filename to save the chosen theme (e.g. mytheme.toml): "
|
|
463
|
+
)
|
|
449
464
|
save_path = themes_dir / filename
|
|
450
465
|
save_theme(chosen_sample_theme, save_path)
|
|
451
466
|
console.print(f"\n[green]Theme saved as {save_path}.[/green]")
|
|
@@ -141,13 +141,22 @@ def generate_default_rich_block(theme: dict | None = None) -> dict[str, Any]:
|
|
|
141
141
|
# Randomly choose non color properties.
|
|
142
142
|
default_non_color_props = {
|
|
143
143
|
"table_show_lines": random.choice([True, False]),
|
|
144
|
-
"table_box": random.choice(
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
"table_box": random.choice([
|
|
145
|
+
"ROUNDED",
|
|
146
|
+
"SIMPLE",
|
|
147
|
+
"SQUARE",
|
|
148
|
+
"MINIMAL",
|
|
149
|
+
"HEAVY",
|
|
150
|
+
"DOUBLE_EDGE",
|
|
151
|
+
]),
|
|
147
152
|
"panel_padding": random.choice([[1, 2], [1, 1], [2, 2], [0, 2]]),
|
|
148
153
|
"panel_title_align": random.choice(["left", "center", "right"]),
|
|
149
154
|
# Add table_row_styles property.
|
|
150
|
-
"table_row_styles": random.choice([
|
|
155
|
+
"table_row_styles": random.choice([
|
|
156
|
+
["", "dim"],
|
|
157
|
+
["", "italic"],
|
|
158
|
+
["", "underline"],
|
|
159
|
+
]),
|
|
151
160
|
}
|
|
152
161
|
# Extra table layout properties (non content properties).
|
|
153
162
|
default_extra_table_props = {
|
|
@@ -294,7 +303,9 @@ def create_rich_renderable(
|
|
|
294
303
|
for i, item in enumerate(value):
|
|
295
304
|
sub_tables.append(f"[bold]Item {i + 1}[/bold]")
|
|
296
305
|
sub_tables.append(
|
|
297
|
-
create_rich_renderable(
|
|
306
|
+
create_rich_renderable(
|
|
307
|
+
item, level + 1, theme, styles, max_length=max_length
|
|
308
|
+
)
|
|
298
309
|
)
|
|
299
310
|
return Group(*sub_tables)
|
|
300
311
|
rendered_items = [
|
|
@@ -311,7 +322,10 @@ def create_rich_renderable(
|
|
|
311
322
|
s = str(value).strip()
|
|
312
323
|
if max_length > 0 and len(s) > max_length:
|
|
313
324
|
omitted = len(s) - max_length
|
|
314
|
-
s =
|
|
325
|
+
s = (
|
|
326
|
+
s[:max_length]
|
|
327
|
+
+ f"[bold bright_yellow]...(+{omitted}chars)[/bold bright_yellow]"
|
|
328
|
+
)
|
|
315
329
|
if isinstance(value, str) and "\n" in value:
|
|
316
330
|
return f"\n{s}\n"
|
|
317
331
|
return s
|
|
@@ -343,20 +357,18 @@ def load_syntax_theme_from_file(filepath: str) -> dict:
|
|
|
343
357
|
|
|
344
358
|
def create_rich_syntax_theme(syntax_theme: dict) -> Theme:
|
|
345
359
|
"""Convert a syntax theme dict to a Rich-compatible Theme."""
|
|
346
|
-
return Theme(
|
|
347
|
-
{
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
)
|
|
360
|
+
return Theme({
|
|
361
|
+
"background": f"on {syntax_theme['background']}",
|
|
362
|
+
"text": syntax_theme["text"],
|
|
363
|
+
"keyword": f"bold {syntax_theme['keyword']}",
|
|
364
|
+
"builtin": f"bold {syntax_theme['builtin']}",
|
|
365
|
+
"string": syntax_theme["string"],
|
|
366
|
+
"name": syntax_theme["name"],
|
|
367
|
+
"number": syntax_theme["number"],
|
|
368
|
+
"operator": syntax_theme["operator"],
|
|
369
|
+
"punctuation": syntax_theme["punctuation"],
|
|
370
|
+
"error": f"bold {syntax_theme['error']}",
|
|
371
|
+
})
|
|
360
372
|
|
|
361
373
|
|
|
362
374
|
def create_pygments_syntax_theme(syntax_theme: dict) -> PygmentsSyntaxTheme:
|
|
@@ -485,7 +497,9 @@ class ThemedAgentResultFormatter:
|
|
|
485
497
|
theme = self.theme
|
|
486
498
|
themes_dir = pathlib.Path(__file__).parent.parent.parent / "themes"
|
|
487
499
|
all_themes = list(themes_dir.glob("*.toml"))
|
|
488
|
-
theme =
|
|
500
|
+
theme = (
|
|
501
|
+
theme.value + ".toml" if not theme.value.endswith(".toml") else theme.value
|
|
502
|
+
)
|
|
489
503
|
theme = pathlib.Path(__file__).parent.parent.parent / "themes" / theme
|
|
490
504
|
|
|
491
505
|
if pathlib.Path(theme) not in all_themes:
|
|
@@ -495,7 +509,9 @@ class ThemedAgentResultFormatter:
|
|
|
495
509
|
|
|
496
510
|
styles = get_default_styles(theme_dict)
|
|
497
511
|
self.styles = styles
|
|
498
|
-
self.syntax_style = create_pygments_syntax_theme(
|
|
512
|
+
self.syntax_style = create_pygments_syntax_theme(
|
|
513
|
+
load_syntax_theme_from_file(theme)
|
|
514
|
+
)
|
|
499
515
|
|
|
500
516
|
console = Console()
|
|
501
517
|
for item in result:
|
flock/logging/logging.py
CHANGED
|
@@ -177,7 +177,9 @@ def custom_format(record):
|
|
|
177
177
|
# MAX_LENGTH = 500 # Example value
|
|
178
178
|
if len(message) > MAX_LENGTH:
|
|
179
179
|
truncated_chars = len(message) - MAX_LENGTH
|
|
180
|
-
message =
|
|
180
|
+
message = (
|
|
181
|
+
message[:MAX_LENGTH] + f"<yellow>...+({truncated_chars} chars)</yellow>"
|
|
182
|
+
)
|
|
181
183
|
|
|
182
184
|
# Determine if category needs bolding (can refine this logic)
|
|
183
185
|
needs_bold = category in BOLD_CATEGORIES
|
|
@@ -262,7 +264,10 @@ logging.basicConfig(level=LOG_LEVELS["ERROR"]) # Default to ERROR level for fal
|
|
|
262
264
|
|
|
263
265
|
|
|
264
266
|
def get_default_severity(
|
|
265
|
-
level: Literal[
|
|
267
|
+
level: Literal[
|
|
268
|
+
"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"
|
|
269
|
+
]
|
|
270
|
+
| int,
|
|
266
271
|
) -> int:
|
|
267
272
|
"""Get the default severity for a given level."""
|
|
268
273
|
if isinstance(level, str):
|
|
@@ -272,13 +277,18 @@ def get_default_severity(
|
|
|
272
277
|
|
|
273
278
|
|
|
274
279
|
def configure_logging(
|
|
275
|
-
flock_level: Literal[
|
|
280
|
+
flock_level: Literal[
|
|
281
|
+
"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"
|
|
282
|
+
]
|
|
276
283
|
| int,
|
|
277
|
-
external_level: Literal[
|
|
284
|
+
external_level: Literal[
|
|
285
|
+
"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"
|
|
286
|
+
]
|
|
278
287
|
| int,
|
|
279
288
|
specific_levels: dict[
|
|
280
289
|
str,
|
|
281
|
-
Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"]
|
|
290
|
+
Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"]
|
|
291
|
+
| int,
|
|
282
292
|
]
|
|
283
293
|
| None = None,
|
|
284
294
|
) -> None:
|
|
@@ -378,7 +388,9 @@ class FlockLogger:
|
|
|
378
388
|
"""Truncate a message if it exceeds max_length and add truncation indicator."""
|
|
379
389
|
if len(message) > max_length:
|
|
380
390
|
truncated_chars = len(message) - max_length
|
|
381
|
-
return
|
|
391
|
+
return (
|
|
392
|
+
message[:max_length] + f"...<yellow>+({truncated_chars} chars)</yellow>"
|
|
393
|
+
)
|
|
382
394
|
return message
|
|
383
395
|
|
|
384
396
|
def debug(
|
|
@@ -565,7 +577,9 @@ def truncate_for_logging(obj, max_item_length=100, max_items=10):
|
|
|
565
577
|
if isinstance(obj, dict):
|
|
566
578
|
if len(obj) > max_items:
|
|
567
579
|
return {
|
|
568
|
-
k: truncate_for_logging(v)
|
|
580
|
+
k: truncate_for_logging(v)
|
|
581
|
+
for i, (k, v) in enumerate(obj.items())
|
|
582
|
+
if i < max_items
|
|
569
583
|
}
|
|
570
584
|
return {k: truncate_for_logging(v) for k, v in obj.items()}
|
|
571
585
|
if isinstance(obj, list):
|
flock/logging/telemetry.py
CHANGED
|
@@ -137,7 +137,9 @@ class TelemetryConfig:
|
|
|
137
137
|
collector_endpoint=self.jaeger_endpoint,
|
|
138
138
|
)
|
|
139
139
|
else:
|
|
140
|
-
raise ValueError(
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"Invalid JAEGER_TRANSPORT specified. Use 'grpc' or 'http'."
|
|
142
|
+
)
|
|
141
143
|
|
|
142
144
|
span_processors.append(SimpleSpanProcessor(jaeger_exporter))
|
|
143
145
|
|
|
@@ -168,12 +170,16 @@ class TelemetryConfig:
|
|
|
168
170
|
|
|
169
171
|
# If a file path is provided, add the custom file exporter.
|
|
170
172
|
if self.file_export_name and self.enable_file:
|
|
171
|
-
file_exporter = FileSpanExporter(
|
|
173
|
+
file_exporter = FileSpanExporter(
|
|
174
|
+
self.local_logging_dir, self.file_export_name
|
|
175
|
+
)
|
|
172
176
|
span_processors.append(SimpleSpanProcessor(file_exporter))
|
|
173
177
|
|
|
174
178
|
# If a SQLite database path is provided, ensure the DB exists and add the SQLite exporter.
|
|
175
179
|
if self.sqlite_db_name and self.enable_sql:
|
|
176
|
-
sqlite_exporter = SqliteTelemetryExporter(
|
|
180
|
+
sqlite_exporter = SqliteTelemetryExporter(
|
|
181
|
+
self.local_logging_dir, self.sqlite_db_name
|
|
182
|
+
)
|
|
177
183
|
span_processors.append(SimpleSpanProcessor(sqlite_exporter))
|
|
178
184
|
|
|
179
185
|
# If a DuckDB database path is provided, add the DuckDB exporter.
|
|
@@ -23,7 +23,9 @@ class DuckDBSpanExporter(TelemetryExporter):
|
|
|
23
23
|
The database is a single file with zero configuration required.
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
def __init__(
|
|
26
|
+
def __init__(
|
|
27
|
+
self, dir: str, db_name: str = "traces.duckdb", ttl_days: int | None = None
|
|
28
|
+
):
|
|
27
29
|
"""Initialize the DuckDB exporter.
|
|
28
30
|
|
|
29
31
|
Args:
|
|
@@ -68,9 +70,13 @@ class DuckDBSpanExporter(TelemetryExporter):
|
|
|
68
70
|
# Create indexes for common query patterns
|
|
69
71
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_trace_id ON spans(trace_id)")
|
|
70
72
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_service ON spans(service)")
|
|
71
|
-
conn.execute(
|
|
73
|
+
conn.execute(
|
|
74
|
+
"CREATE INDEX IF NOT EXISTS idx_start_time ON spans(start_time)"
|
|
75
|
+
)
|
|
72
76
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_name ON spans(name)")
|
|
73
|
-
conn.execute(
|
|
77
|
+
conn.execute(
|
|
78
|
+
"CREATE INDEX IF NOT EXISTS idx_created_at ON spans(created_at)"
|
|
79
|
+
)
|
|
74
80
|
|
|
75
81
|
# Cleanup old traces if TTL is configured
|
|
76
82
|
if self.ttl_days is not None:
|
|
@@ -121,28 +127,24 @@ class DuckDBSpanExporter(TelemetryExporter):
|
|
|
121
127
|
|
|
122
128
|
# Serialize complex fields to JSON
|
|
123
129
|
attributes_json = json.dumps(dict(span.attributes or {}))
|
|
124
|
-
events_json = json.dumps(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
for link in span.links
|
|
144
|
-
]
|
|
145
|
-
)
|
|
130
|
+
events_json = json.dumps([
|
|
131
|
+
{
|
|
132
|
+
"name": event.name,
|
|
133
|
+
"timestamp": event.timestamp,
|
|
134
|
+
"attributes": dict(event.attributes or {}),
|
|
135
|
+
}
|
|
136
|
+
for event in span.events
|
|
137
|
+
])
|
|
138
|
+
links_json = json.dumps([
|
|
139
|
+
{
|
|
140
|
+
"context": {
|
|
141
|
+
"trace_id": format(link.context.trace_id, "032x"),
|
|
142
|
+
"span_id": format(link.context.span_id, "016x"),
|
|
143
|
+
},
|
|
144
|
+
"attributes": dict(link.attributes or {}),
|
|
145
|
+
}
|
|
146
|
+
for link in span.links
|
|
147
|
+
])
|
|
146
148
|
resource_json = json.dumps(dict(span.resource.attributes.items()))
|
|
147
149
|
|
|
148
150
|
# Get parent span ID if exists
|
|
@@ -20,7 +20,9 @@ class TraceFilterConfig:
|
|
|
20
20
|
|
|
21
21
|
def __init__(self):
|
|
22
22
|
self.services: set[str] | None = None # Whitelist: only trace these services
|
|
23
|
-
self.ignore_operations: set[str] =
|
|
23
|
+
self.ignore_operations: set[str] = (
|
|
24
|
+
set()
|
|
25
|
+
) # Blacklist: never trace these operations
|
|
24
26
|
|
|
25
27
|
def should_trace(self, service: str, operation: str) -> bool:
|
|
26
28
|
"""Check if an operation should be traced based on filters.
|
|
@@ -74,17 +76,22 @@ def _serialize_value(value, max_depth=10, current_depth=0):
|
|
|
74
76
|
|
|
75
77
|
# Handle lists/tuples
|
|
76
78
|
if isinstance(value, (list, tuple)):
|
|
77
|
-
return [
|
|
79
|
+
return [
|
|
80
|
+
_serialize_value(item, max_depth, current_depth + 1) for item in value
|
|
81
|
+
]
|
|
78
82
|
|
|
79
83
|
# Handle dicts
|
|
80
84
|
if isinstance(value, dict):
|
|
81
85
|
return {
|
|
82
|
-
str(k): _serialize_value(v, max_depth, current_depth + 1)
|
|
86
|
+
str(k): _serialize_value(v, max_depth, current_depth + 1)
|
|
87
|
+
for k, v in value.items()
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
# Handle sets
|
|
86
91
|
if isinstance(value, set):
|
|
87
|
-
return [
|
|
92
|
+
return [
|
|
93
|
+
_serialize_value(item, max_depth, current_depth + 1) for item in value
|
|
94
|
+
]
|
|
88
95
|
|
|
89
96
|
# For custom objects with __dict__, serialize their attributes
|
|
90
97
|
if hasattr(value, "__dict__"):
|
|
@@ -277,7 +284,9 @@ def traced_and_logged(func):
|
|
|
277
284
|
# Capture output value as JSON
|
|
278
285
|
try:
|
|
279
286
|
serialized_result = _serialize_value(result)
|
|
280
|
-
span.set_attribute(
|
|
287
|
+
span.set_attribute(
|
|
288
|
+
"output.value", json.dumps(serialized_result, default=str)
|
|
289
|
+
)
|
|
281
290
|
except Exception as e:
|
|
282
291
|
span.set_attribute("output.value", str(result))
|
|
283
292
|
span.set_attribute("output.serialization_error", str(e))
|
flock/mcp/__init__.py
CHANGED
|
@@ -28,16 +28,13 @@ Example Usage:
|
|
|
28
28
|
orchestrator.add_mcp(
|
|
29
29
|
name="filesystem",
|
|
30
30
|
connection_params=StdioServerParameters(
|
|
31
|
-
command="uvx",
|
|
32
|
-
|
|
33
|
-
)
|
|
31
|
+
command="uvx", args=["mcp-server-filesystem", "/tmp"]
|
|
32
|
+
),
|
|
34
33
|
)
|
|
35
34
|
|
|
36
35
|
# Build agent with MCP access
|
|
37
36
|
agent = (
|
|
38
|
-
orchestrator.agent("file_agent")
|
|
39
|
-
.with_mcps(["filesystem"])
|
|
40
|
-
.build()
|
|
37
|
+
orchestrator.agent("file_agent").with_mcps(["filesystem"]).build()
|
|
41
38
|
)
|
|
42
39
|
```
|
|
43
40
|
"""
|