flock-core 0.5.8__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.

Files changed (52) hide show
  1. flock/agent.py +149 -62
  2. flock/api/themes.py +6 -2
  3. flock/artifact_collector.py +6 -3
  4. flock/batch_accumulator.py +3 -1
  5. flock/cli.py +3 -1
  6. flock/components.py +45 -56
  7. flock/context_provider.py +531 -0
  8. flock/correlation_engine.py +8 -4
  9. flock/dashboard/collector.py +48 -29
  10. flock/dashboard/events.py +10 -4
  11. flock/dashboard/launcher.py +3 -1
  12. flock/dashboard/models/graph.py +9 -3
  13. flock/dashboard/service.py +143 -72
  14. flock/dashboard/websocket.py +17 -4
  15. flock/engines/dspy_engine.py +174 -98
  16. flock/engines/examples/simple_batch_engine.py +9 -3
  17. flock/examples.py +6 -2
  18. flock/frontend/src/services/indexeddb.test.ts +4 -4
  19. flock/frontend/src/services/indexeddb.ts +1 -1
  20. flock/helper/cli_helper.py +14 -1
  21. flock/logging/auto_trace.py +6 -1
  22. flock/logging/formatters/enum_builder.py +3 -1
  23. flock/logging/formatters/theme_builder.py +32 -17
  24. flock/logging/formatters/themed_formatter.py +38 -22
  25. flock/logging/logging.py +21 -7
  26. flock/logging/telemetry.py +9 -3
  27. flock/logging/telemetry_exporter/duckdb_exporter.py +27 -25
  28. flock/logging/trace_and_logged.py +14 -5
  29. flock/mcp/__init__.py +3 -6
  30. flock/mcp/client.py +49 -19
  31. flock/mcp/config.py +12 -6
  32. flock/mcp/manager.py +6 -2
  33. flock/mcp/servers/sse/flock_sse_server.py +9 -3
  34. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +6 -2
  35. flock/mcp/tool.py +18 -6
  36. flock/mcp/types/handlers.py +3 -1
  37. flock/mcp/types/types.py +9 -3
  38. flock/orchestrator.py +204 -50
  39. flock/orchestrator_component.py +15 -5
  40. flock/patches/dspy_streaming_patch.py +12 -4
  41. flock/registry.py +9 -3
  42. flock/runtime.py +69 -18
  43. flock/service.py +19 -6
  44. flock/store.py +29 -10
  45. flock/subscription.py +6 -4
  46. flock/utilities.py +41 -13
  47. flock/utility/output_utility_component.py +31 -11
  48. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/METADATA +134 -4
  49. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/RECORD +52 -51
  50. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/WHEEL +0 -0
  51. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/entry_points.txt +0 -0
  52. {flock_core-0.5.8.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(description="Original values processed", default_factory=list)
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(self, agent, ctx, inputs: EvalInputs, output_group) -> EvalResult:
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(batch_size=batch_size, values=[item.value for item in items])
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(self, agent, ctx, inputs: EvalInputs, output_group) -> EvalResult:
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(self, agent, ctx, inputs: EvalInputs, output_group) -> EvalResult:
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
- channels: [],
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
- channels: [],
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
- channels: [],
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
- channels: [],
615
+ tags: [],
616
616
  run_history: [],
617
617
  total_runs: 0,
618
618
  total_errors: 0,
@@ -46,7 +46,7 @@ interface AgentRecord {
46
46
  max_concurrency: number;
47
47
  consumes_types: string[];
48
48
  from_agents: string[];
49
- channels: string[];
49
+ tags: string[];
50
50
  run_history: string[]; // [run_id] - last 100 runs
51
51
  total_runs: number;
52
52
  total_errors: number;
@@ -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",
@@ -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 {"true", "1", "yes", "on"}
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 = [pathlib.Path(f.path).stem for f in os.scandir(theme_folder) if f.is_file()]
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
- f"{normal_black}",
53
- f"{normal_blue}",
54
- f"{primary_background}",
55
- f"{selection_background}",
56
- f"{cursor_cursor}",
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
- ["ROUNDED", "SIMPLE", "SQUARE", "MINIMAL", "HEAVY", "DOUBLE_EDGE"]
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([["", "dim"], ["", "italic"], ["", "underline"]]),
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(create_rich_renderable(item, level + 1, theme, styles))
285
+ sub_tables.append(
286
+ create_rich_renderable(item, level + 1, theme, styles)
287
+ )
279
288
  return Group(*sub_tables)
280
- rendered_items = [create_rich_renderable(item, level + 1, theme, styles) for item in value]
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(chosen_theme) # shallow copy (good enough if colors remain unchanged)
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("\nEnter a filename to save the chosen theme (e.g. mytheme.toml): ")
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
- ["ROUNDED", "SIMPLE", "SQUARE", "MINIMAL", "HEAVY", "DOUBLE_EDGE"]
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([["", "dim"], ["", "italic"], ["", "underline"]]),
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(item, level + 1, theme, styles, max_length=max_length)
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 = s[:max_length] + f"[bold bright_yellow]...(+{omitted}chars)[/bold bright_yellow]"
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
- "background": f"on {syntax_theme['background']}",
349
- "text": syntax_theme["text"],
350
- "keyword": f"bold {syntax_theme['keyword']}",
351
- "builtin": f"bold {syntax_theme['builtin']}",
352
- "string": syntax_theme["string"],
353
- "name": syntax_theme["name"],
354
- "number": syntax_theme["number"],
355
- "operator": syntax_theme["operator"],
356
- "punctuation": syntax_theme["punctuation"],
357
- "error": f"bold {syntax_theme['error']}",
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 = theme.value + ".toml" if not theme.value.endswith(".toml") else theme.value
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(load_syntax_theme_from_file(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 = message[:MAX_LENGTH] + f"<yellow>...+({truncated_chars} chars)</yellow>"
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["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"] | int,
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["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"]
280
+ flock_level: Literal[
281
+ "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"
282
+ ]
276
283
  | int,
277
- external_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NO_LOGS", "SUCCESS"]
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"] | int,
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 message[:max_length] + f"...<yellow>+({truncated_chars} chars)</yellow>"
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) for i, (k, v) in enumerate(obj.items()) if i < max_items
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):
@@ -137,7 +137,9 @@ class TelemetryConfig:
137
137
  collector_endpoint=self.jaeger_endpoint,
138
138
  )
139
139
  else:
140
- raise ValueError("Invalid JAEGER_TRANSPORT specified. Use 'grpc' or 'http'.")
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(self.local_logging_dir, self.file_export_name)
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(self.local_logging_dir, self.sqlite_db_name)
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__(self, dir: str, db_name: str = "traces.duckdb", ttl_days: int | None = None):
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("CREATE INDEX IF NOT EXISTS idx_start_time ON spans(start_time)")
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("CREATE INDEX IF NOT EXISTS idx_created_at ON spans(created_at)")
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
- "name": event.name,
128
- "timestamp": event.timestamp,
129
- "attributes": dict(event.attributes or {}),
130
- }
131
- for event in span.events
132
- ]
133
- )
134
- links_json = json.dumps(
135
- [
136
- {
137
- "context": {
138
- "trace_id": format(link.context.trace_id, "032x"),
139
- "span_id": format(link.context.span_id, "016x"),
140
- },
141
- "attributes": dict(link.attributes or {}),
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] = set() # Blacklist: never trace these operations
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 [_serialize_value(item, max_depth, current_depth + 1) for item in value]
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) for k, v in value.items()
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 [_serialize_value(item, max_depth, current_depth + 1) for item in value]
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("output.value", json.dumps(serialized_result, default=str))
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
- args=["mcp-server-filesystem", "/tmp"]
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
  """