flock-core 0.5.0b53__py3-none-any.whl → 0.5.0b55__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 CHANGED
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
10
10
  from pydantic import BaseModel
11
11
 
12
12
  from flock.artifacts import Artifact, ArtifactSpec
13
+ from flock.logging.auto_trace import AutoTracedMeta
13
14
  from flock.logging.logging import get_logger
14
15
  from flock.registry import function_registry, type_registry
15
16
  from flock.runtime import Context, EvalInputs, EvalResult
@@ -50,8 +51,11 @@ class AgentOutput:
50
51
  )
51
52
 
52
53
 
53
- class Agent:
54
- """Executable agent constructed via `AgentBuilder`."""
54
+ class Agent(metaclass=AutoTracedMeta):
55
+ """Executable agent constructed via `AgentBuilder`.
56
+
57
+ All public methods are automatically traced via OpenTelemetry.
58
+ """
55
59
 
56
60
  def __init__(self, name: str, *, orchestrator: Flock) -> None:
57
61
  self.name = name
flock/components.py CHANGED
@@ -5,8 +5,11 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from pydantic import BaseModel, Field, create_model
8
+ from pydantic._internal._model_construction import ModelMetaclass
8
9
  from typing_extensions import Self, TypeVar
9
10
 
11
+ from flock.logging.auto_trace import AutoTracedMeta
12
+
10
13
 
11
14
  if TYPE_CHECKING: # pragma: no cover - type checking only
12
15
  from uuid import UUID
@@ -18,6 +21,14 @@ if TYPE_CHECKING: # pragma: no cover - type checking only
18
21
  T = TypeVar("T", bound="AgentComponentConfig")
19
22
 
20
23
 
24
+ class TracedModelMeta(ModelMetaclass, AutoTracedMeta):
25
+ """Combined metaclass for Pydantic models with auto-tracing.
26
+
27
+ This metaclass combines Pydantic's ModelMetaclass with AutoTracedMeta
28
+ to enable both Pydantic functionality and automatic method tracing.
29
+ """
30
+
31
+
21
32
  class AgentComponentConfig(BaseModel):
22
33
  enabled: bool = True
23
34
  model: str | None = None
@@ -37,7 +48,12 @@ class AgentComponentConfig(BaseModel):
37
48
  return create_model(f"Dynamic{cls.__name__}", __base__=cls, **field_definitions)
38
49
 
39
50
 
40
- class AgentComponent(BaseModel):
51
+ class AgentComponent(BaseModel, metaclass=TracedModelMeta):
52
+ """Base class for agent components with lifecycle hooks.
53
+
54
+ All public methods are automatically traced via OpenTelemetry.
55
+ """
56
+
41
57
  name: str | None = None
42
58
  config: AgentComponentConfig = Field(default_factory=AgentComponentConfig)
43
59
 
@@ -407,6 +407,299 @@ class DashboardHTTPService(BlackboardHTTPService):
407
407
  """
408
408
  raise HTTPException(status_code=501, detail="Resume functionality coming in Phase 12")
409
409
 
410
+ @app.get("/api/traces")
411
+ async def get_traces() -> list[dict[str, Any]]:
412
+ """Get OpenTelemetry traces from DuckDB.
413
+
414
+ Returns list of trace spans in OTEL format.
415
+
416
+ Returns:
417
+ [
418
+ {
419
+ "name": "Agent.execute",
420
+ "context": {
421
+ "trace_id": "...",
422
+ "span_id": "...",
423
+ ...
424
+ },
425
+ "start_time": 1234567890,
426
+ "end_time": 1234567891,
427
+ "attributes": {...},
428
+ "status": {...}
429
+ },
430
+ ...
431
+ ]
432
+ """
433
+ import json
434
+ from pathlib import Path
435
+
436
+ import duckdb
437
+
438
+ db_path = Path(".flock/traces.duckdb")
439
+
440
+ if not db_path.exists():
441
+ logger.warning(
442
+ "Trace database not found. Make sure FLOCK_AUTO_TRACE=true FLOCK_TRACE_FILE=true"
443
+ )
444
+ return []
445
+
446
+ try:
447
+ with duckdb.connect(str(db_path), read_only=True) as conn:
448
+ # Query all spans from DuckDB
449
+ result = conn.execute("""
450
+ SELECT
451
+ trace_id, span_id, parent_id, name, service, operation,
452
+ kind, start_time, end_time, duration_ms,
453
+ status_code, status_description,
454
+ attributes, events, links, resource
455
+ FROM spans
456
+ ORDER BY start_time DESC
457
+ """).fetchall()
458
+
459
+ spans = []
460
+ for row in result:
461
+ # Reconstruct OTEL span format from DuckDB row
462
+ span = {
463
+ "name": row[3], # name
464
+ "context": {
465
+ "trace_id": row[0], # trace_id
466
+ "span_id": row[1], # span_id
467
+ "trace_flags": 0,
468
+ "trace_state": "",
469
+ },
470
+ "kind": row[6], # kind
471
+ "start_time": row[7], # start_time
472
+ "end_time": row[8], # end_time
473
+ "status": {
474
+ "status_code": row[10], # status_code
475
+ "description": row[11], # status_description
476
+ },
477
+ "attributes": json.loads(row[12]) if row[12] else {}, # attributes
478
+ "events": json.loads(row[13]) if row[13] else [], # events
479
+ "links": json.loads(row[14]) if row[14] else [], # links
480
+ "resource": json.loads(row[15]) if row[15] else {}, # resource
481
+ }
482
+
483
+ # Add parent_id if exists
484
+ if row[2]: # parent_id
485
+ span["parent_id"] = row[2]
486
+
487
+ spans.append(span)
488
+
489
+ logger.debug(f"Loaded {len(spans)} spans from DuckDB")
490
+ return spans
491
+
492
+ except Exception as e:
493
+ logger.exception(f"Error reading traces from DuckDB: {e}")
494
+ return []
495
+
496
+ @app.get("/api/traces/services")
497
+ async def get_trace_services() -> dict[str, Any]:
498
+ """Get list of unique services that have been traced.
499
+
500
+ Returns:
501
+ {
502
+ "services": ["Flock", "Agent", "DSPyEngine", ...],
503
+ "operations": ["Flock.publish", "Agent.execute", ...]
504
+ }
505
+ """
506
+ import duckdb
507
+ from pathlib import Path
508
+
509
+ db_path = Path(".flock/traces.duckdb")
510
+
511
+ if not db_path.exists():
512
+ return {"services": [], "operations": []}
513
+
514
+ try:
515
+ with duckdb.connect(str(db_path), read_only=True) as conn:
516
+ # Get unique services
517
+ services_result = conn.execute("""
518
+ SELECT DISTINCT service
519
+ FROM spans
520
+ WHERE service IS NOT NULL
521
+ ORDER BY service
522
+ """).fetchall()
523
+
524
+ # Get unique operations
525
+ operations_result = conn.execute("""
526
+ SELECT DISTINCT name
527
+ FROM spans
528
+ WHERE name IS NOT NULL
529
+ ORDER BY name
530
+ """).fetchall()
531
+
532
+ return {
533
+ "services": [row[0] for row in services_result],
534
+ "operations": [row[0] for row in operations_result],
535
+ }
536
+
537
+ except Exception as e:
538
+ logger.exception(f"Error reading trace services: {e}")
539
+ return {"services": [], "operations": []}
540
+
541
+ @app.post("/api/traces/clear")
542
+ async def clear_traces() -> dict[str, Any]:
543
+ """Clear all traces from DuckDB database.
544
+
545
+ Returns:
546
+ {
547
+ "success": true,
548
+ "deleted_count": 123,
549
+ "error": null
550
+ }
551
+ """
552
+ result = Flock.clear_traces()
553
+ if result["success"]:
554
+ logger.info(f"Cleared {result['deleted_count']} trace spans via API")
555
+ else:
556
+ logger.error(f"Failed to clear traces: {result['error']}")
557
+
558
+ return result
559
+
560
+ @app.post("/api/traces/query")
561
+ async def execute_trace_query(request: dict[str, Any]) -> dict[str, Any]:
562
+ """
563
+ Execute a DuckDB SQL query on the traces database.
564
+
565
+ Security: Only SELECT queries allowed, rate-limited.
566
+ """
567
+ import duckdb
568
+ from pathlib import Path
569
+
570
+ query = request.get("query", "").strip()
571
+
572
+ if not query:
573
+ return {"error": "Query cannot be empty", "results": [], "columns": []}
574
+
575
+ # Security: Only allow SELECT queries
576
+ query_upper = query.upper().strip()
577
+ if not query_upper.startswith("SELECT"):
578
+ return {"error": "Only SELECT queries are allowed", "results": [], "columns": []}
579
+
580
+ # Check for dangerous keywords
581
+ dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "TRUNCATE"]
582
+ if any(keyword in query_upper for keyword in dangerous):
583
+ return {
584
+ "error": "Query contains forbidden operations",
585
+ "results": [],
586
+ "columns": [],
587
+ }
588
+
589
+ db_path = Path(".flock/traces.duckdb")
590
+ if not db_path.exists():
591
+ return {"error": "Trace database not found", "results": [], "columns": []}
592
+
593
+ try:
594
+ with duckdb.connect(str(db_path), read_only=True) as conn:
595
+ result = conn.execute(query).fetchall()
596
+ columns = [desc[0] for desc in conn.description] if conn.description else []
597
+
598
+ # Convert to JSON-serializable format
599
+ results = []
600
+ for row in result:
601
+ row_dict = {}
602
+ for i, col in enumerate(columns):
603
+ val = row[i]
604
+ # Convert bytes to string, handle other types
605
+ if isinstance(val, bytes):
606
+ row_dict[col] = val.decode("utf-8")
607
+ else:
608
+ row_dict[col] = val
609
+ results.append(row_dict)
610
+
611
+ return {"results": results, "columns": columns, "row_count": len(results)}
612
+ except Exception as e:
613
+ logger.error(f"DuckDB query error: {e}")
614
+ return {"error": str(e), "results": [], "columns": []}
615
+
616
+ @app.get("/api/traces/stats")
617
+ async def get_trace_stats() -> dict[str, Any]:
618
+ """Get statistics about the trace database.
619
+
620
+ Returns:
621
+ {
622
+ "total_spans": 123,
623
+ "total_traces": 45,
624
+ "services_count": 5,
625
+ "oldest_trace": "2025-10-07T12:00:00Z",
626
+ "newest_trace": "2025-10-07T14:30:00Z",
627
+ "database_size_mb": 12.5
628
+ }
629
+ """
630
+ import duckdb
631
+ from pathlib import Path
632
+ from datetime import datetime
633
+
634
+ db_path = Path(".flock/traces.duckdb")
635
+
636
+ if not db_path.exists():
637
+ return {
638
+ "total_spans": 0,
639
+ "total_traces": 0,
640
+ "services_count": 0,
641
+ "oldest_trace": None,
642
+ "newest_trace": None,
643
+ "database_size_mb": 0,
644
+ }
645
+
646
+ try:
647
+ with duckdb.connect(str(db_path), read_only=True) as conn:
648
+ # Get total spans
649
+ total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[0]
650
+
651
+ # Get total unique traces
652
+ total_traces = conn.execute(
653
+ "SELECT COUNT(DISTINCT trace_id) FROM spans"
654
+ ).fetchone()[0]
655
+
656
+ # Get services count
657
+ services_count = conn.execute(
658
+ "SELECT COUNT(DISTINCT service) FROM spans WHERE service IS NOT NULL"
659
+ ).fetchone()[0]
660
+
661
+ # Get time range
662
+ time_range = conn.execute("""
663
+ SELECT
664
+ MIN(start_time) as oldest,
665
+ MAX(start_time) as newest
666
+ FROM spans
667
+ """).fetchone()
668
+
669
+ oldest_trace = None
670
+ newest_trace = None
671
+ if time_range and time_range[0]:
672
+ # Convert nanoseconds to datetime
673
+ oldest_trace = datetime.fromtimestamp(
674
+ time_range[0] / 1_000_000_000
675
+ ).isoformat()
676
+ newest_trace = datetime.fromtimestamp(
677
+ time_range[1] / 1_000_000_000
678
+ ).isoformat()
679
+
680
+ # Get file size
681
+ size_mb = db_path.stat().st_size / (1024 * 1024)
682
+
683
+ return {
684
+ "total_spans": total_spans,
685
+ "total_traces": total_traces,
686
+ "services_count": services_count,
687
+ "oldest_trace": oldest_trace,
688
+ "newest_trace": newest_trace,
689
+ "database_size_mb": round(size_mb, 2),
690
+ }
691
+
692
+ except Exception as e:
693
+ logger.exception(f"Error reading trace stats: {e}")
694
+ return {
695
+ "total_spans": 0,
696
+ "total_traces": 0,
697
+ "services_count": 0,
698
+ "oldest_trace": None,
699
+ "newest_trace": None,
700
+ "database_size_mb": 0,
701
+ }
702
+
410
703
  @app.get("/api/streaming-history/{agent_name}")
411
704
  async def get_streaming_history(agent_name: str) -> dict[str, Any]:
412
705
  """Get historical streaming output for a specific agent.
flock/frontend/README.md CHANGED
@@ -34,9 +34,95 @@ The dashboard offers two complementary visualization modes:
34
34
  ### Extensible Module System
35
35
  - **Custom Visualizations**: Add specialized views via the module system
36
36
  - **Event Log Module**: Built-in table view for detailed event inspection
37
+ - **Trace Viewer Module**: Jaeger-style distributed tracing with timeline and statistics
37
38
  - **Context Menu Integration**: Right-click to add modules at any location
38
39
  - **Persistent Layout**: Module positions and sizes are saved across sessions
39
40
 
41
+ ### Trace Viewer Module 🔍
42
+
43
+ The **Trace Viewer** provides production-grade distributed tracing powered by OpenTelemetry and DuckDB, enabling deep debugging and performance analysis.
44
+
45
+ #### Features
46
+
47
+ - **Timeline View**: Waterfall visualization showing span hierarchies and execution order
48
+ - Parent-child relationships with visual indentation
49
+ - Service-specific color coding for easy identification
50
+ - Duration bars proportional to execution time
51
+ - Hover tooltips with detailed timing information
52
+ - Expand/collapse nested spans
53
+
54
+ - **Statistics View**: Tabular view with comprehensive metrics
55
+ - Sortable columns (name, duration, status)
56
+ - Quick filtering and search
57
+ - Status indicators (OK, ERROR)
58
+ - Export capabilities
59
+
60
+ - **Full I/O Capture**: Complete input/output data for every operation
61
+ - All function arguments captured as JSON
62
+ - Return values fully serialized
63
+ - Automatic deep object serialization
64
+ - No truncation (except strings >5000 chars)
65
+
66
+ - **JSON Viewer**: Beautiful, interactive JSON exploration
67
+ - Syntax highlighting with theme integration
68
+ - Collapsible tree structure
69
+ - **Expand All / Collapse All** buttons for quick navigation
70
+ - Supports nested objects and arrays
71
+ - Preserves data types (strings, numbers, booleans)
72
+
73
+ - **Multi-Trace Support**: Open multiple traces simultaneously
74
+ - Compare execution patterns side-by-side
75
+ - Toggle traces on/off
76
+ - Independent expansion states
77
+
78
+ - **Performance**: Built on DuckDB (10-100x faster than SQLite)
79
+ - Sub-millisecond query times
80
+ - Handles thousands of spans efficiently
81
+ - SQL-powered analytics backend
82
+
83
+ #### Usage
84
+
85
+ 1. **Enable Tracing** in your Flock application:
86
+ ```bash
87
+ export FLOCK_AUTO_TRACE=true
88
+ export FLOCK_TRACE_FILE=true
89
+ ```
90
+
91
+ 2. **Run Your Application** to generate traces
92
+ ```bash
93
+ python your_flock_app.py
94
+ ```
95
+
96
+ 3. **Open Trace Viewer** in the dashboard:
97
+ - Click "Add Module" or right-click canvas
98
+ - Select "Trace Viewer"
99
+ - Browse and select traces to visualize
100
+
101
+ 4. **Analyze Traces**:
102
+ - Click trace rows to expand timeline/statistics
103
+ - Use search to filter by operation name
104
+ - Expand JSON attributes to inspect I/O data
105
+ - Click "Expand All" to see complete JSON structures
106
+
107
+ #### What Gets Traced
108
+
109
+ Every traced operation captures:
110
+ - ✅ **Input Arguments**: All function parameters (excluding `self`/`cls`)
111
+ - ✅ **Output Values**: Complete return values
112
+ - ✅ **Timing Data**: Start time, end time, duration in milliseconds
113
+ - ✅ **Span Hierarchy**: Parent-child relationships for call stacks
114
+ - ✅ **Service Names**: Automatically extracted from class names
115
+ - ✅ **Status Codes**: OK, ERROR, UNSET with error messages
116
+ - ✅ **Metadata**: Correlation IDs, agent names, context info
117
+
118
+ #### Tips
119
+
120
+ - **Finding Bottlenecks**: Sort Statistics view by duration (descending)
121
+ - **Error Investigation**: Look for red ERROR status indicators
122
+ - **I/O Debugging**: Expand JSON viewers to see exact inputs that caused issues
123
+ - **Multi-Trace Comparison**: Open related traces to compare execution patterns
124
+ - **JSON Navigation**: Use "Expand All" for complex nested structures
125
+
40
126
  ### Modern UI/UX
41
127
  - **Glassmorphism Design**: Modern dark theme with semi-transparent surfaces and blur effects
42
128
  - **Keyboard Shortcuts**: Navigate efficiently with Ctrl+M, Ctrl+F, and Esc
@@ -0,0 +1,140 @@
1
+ import React, { useState } from 'react';
2
+ import JsonView from '@uiw/react-json-view';
3
+
4
+ interface JsonAttributeRendererProps {
5
+ value: string;
6
+ maxStringLength?: number;
7
+ }
8
+
9
+ /**
10
+ * Renders an attribute value, parsing it as JSON if possible and displaying
11
+ * it with a beautiful collapsible JSON viewer. Falls back to plain text if not JSON.
12
+ */
13
+ const JsonAttributeRenderer: React.FC<JsonAttributeRendererProps> = ({
14
+ value,
15
+ maxStringLength = 200
16
+ }) => {
17
+ const [collapsed, setCollapsed] = useState<boolean | number>(2);
18
+
19
+ // Try to parse as JSON
20
+ try {
21
+ const parsed = JSON.parse(value);
22
+
23
+ // If it's a simple primitive after parsing, just show it as text
24
+ if (typeof parsed === 'string' || typeof parsed === 'number' || typeof parsed === 'boolean' || parsed === null) {
25
+ return (
26
+ <div style={{
27
+ fontFamily: 'var(--font-family-mono)',
28
+ fontSize: 'var(--font-size-body-xs)',
29
+ color: 'var(--color-text-secondary)',
30
+ wordBreak: 'break-word',
31
+ }}>
32
+ {String(parsed)}
33
+ </div>
34
+ );
35
+ }
36
+
37
+ // It's a complex object or array - use JSON viewer
38
+ return (
39
+ <div>
40
+ <div style={{
41
+ display: 'flex',
42
+ gap: 'var(--gap-xs)',
43
+ marginBottom: 'var(--gap-xs)',
44
+ }}>
45
+ <button
46
+ onClick={() => setCollapsed(false)}
47
+ style={{
48
+ padding: '2px 8px',
49
+ fontSize: 'var(--font-size-body-xs)',
50
+ color: 'var(--color-text-secondary)',
51
+ backgroundColor: 'var(--color-bg-elevated)',
52
+ border: '1px solid var(--color-border-subtle)',
53
+ borderRadius: 'var(--radius-xs)',
54
+ cursor: 'pointer',
55
+ fontFamily: 'var(--font-family-mono)',
56
+ }}
57
+ onMouseEnter={(e) => {
58
+ e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)';
59
+ }}
60
+ onMouseLeave={(e) => {
61
+ e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)';
62
+ }}
63
+ >
64
+ Expand All
65
+ </button>
66
+ <button
67
+ onClick={() => setCollapsed(true)}
68
+ style={{
69
+ padding: '2px 8px',
70
+ fontSize: 'var(--font-size-body-xs)',
71
+ color: 'var(--color-text-secondary)',
72
+ backgroundColor: 'var(--color-bg-elevated)',
73
+ border: '1px solid var(--color-border-subtle)',
74
+ borderRadius: 'var(--radius-xs)',
75
+ cursor: 'pointer',
76
+ fontFamily: 'var(--font-family-mono)',
77
+ }}
78
+ onMouseEnter={(e) => {
79
+ e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)';
80
+ }}
81
+ onMouseLeave={(e) => {
82
+ e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)';
83
+ }}
84
+ >
85
+ Collapse All
86
+ </button>
87
+ </div>
88
+ <div style={{
89
+ maxHeight: '400px',
90
+ overflowY: 'auto',
91
+ overflowX: 'auto',
92
+ }}>
93
+ <JsonView
94
+ value={parsed}
95
+ collapsed={collapsed}
96
+ displayDataTypes={false}
97
+ shortenTextAfterLength={0}
98
+ style={{
99
+ backgroundColor: 'var(--color-bg-elevated)',
100
+ fontSize: 'var(--font-size-body-xs)',
101
+ fontFamily: 'var(--font-family-mono)',
102
+ padding: 'var(--space-component-sm)',
103
+ borderRadius: 'var(--radius-sm)',
104
+ '--w-rjv-line-color': 'var(--color-text-tertiary)',
105
+ '--w-rjv-key-string': 'var(--color-primary-500)',
106
+ '--w-rjv-info-color': 'var(--color-text-secondary)',
107
+ '--w-rjv-curlybraces-color': 'var(--color-text-tertiary)',
108
+ '--w-rjv-brackets-color': 'var(--color-text-tertiary)',
109
+ '--w-rjv-arrow-color': 'var(--color-text-tertiary)',
110
+ '--w-rjv-edit-color': 'var(--color-primary-500)',
111
+ '--w-rjv-add-color': 'var(--color-success)',
112
+ '--w-rjv-del-color': 'var(--color-error)',
113
+ '--w-rjv-update-color': 'var(--color-warning)',
114
+ '--w-rjv-border-left-color': 'var(--color-border-subtle)',
115
+ } as React.CSSProperties}
116
+ />
117
+ </div>
118
+ </div>
119
+ );
120
+ } catch (e) {
121
+ // Not valid JSON - display as plain text with word wrap
122
+ const displayValue = value.length > maxStringLength
123
+ ? value.substring(0, maxStringLength) + '...'
124
+ : value;
125
+
126
+ return (
127
+ <div style={{
128
+ fontFamily: 'var(--font-family-mono)',
129
+ fontSize: 'var(--font-size-body-xs)',
130
+ color: 'var(--color-text-secondary)',
131
+ wordBreak: 'break-word',
132
+ whiteSpace: 'pre-wrap',
133
+ }}>
134
+ {displayValue}
135
+ </div>
136
+ );
137
+ }
138
+ };
139
+
140
+ export default JsonAttributeRenderer;