flock-core 0.2.4__py3-none-any.whl → 0.2.5__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 CHANGED
@@ -1,5 +1,8 @@
1
1
  """Flock package initialization."""
2
2
 
3
+ from opentelemetry import trace
4
+
3
5
  from flock.config import TELEMETRY
4
6
 
5
7
  tracer = TELEMETRY.setup_tracing()
8
+ tracer = trace.get_tracer(__name__)
flock/config.py CHANGED
@@ -1,29 +1,47 @@
1
1
  # flock/config.py
2
- import os
2
+ from decouple import config
3
3
 
4
4
  from flock.core.logging.telemetry import TelemetryConfig
5
5
 
6
6
  # -- Connection and External Service Configurations --
7
- TEMPORAL_SERVER_URL = os.getenv("TEMPORAL_SERVER_URL", "localhost:7233")
8
- DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "openai/gpt-4o")
7
+ TEMPORAL_SERVER_URL = config("TEMPORAL_SERVER_URL", "localhost:7233")
8
+ DEFAULT_MODEL = config("DEFAULT_MODEL", "openai/gpt-4o")
9
+
9
10
 
10
11
  # API Keys and related settings
11
- TAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "")
12
- GITHUB_PAT = os.getenv("GITHUB_PAT", "")
13
- GITHUB_REPO = os.getenv("GITHUB_REPO", "")
14
- GITHUB_USERNAME = os.getenv("GITHUB_USERNAME", "")
12
+ TAVILY_API_KEY = config("TAVILY_API_KEY", "")
13
+ GITHUB_PAT = config("GITHUB_PAT", "")
14
+ GITHUB_REPO = config("GITHUB_REPO", "")
15
+ GITHUB_USERNAME = config("GITHUB_USERNAME", "")
15
16
 
16
17
  # -- Debugging and Logging Configurations --
17
- LOCAL_DEBUG = os.getenv("LOCAL_DEBUG", "0") == "1"
18
- LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG")
18
+ LOCAL_DEBUG = config("LOCAL_DEBUG", True)
19
+ LOG_LEVEL = config("LOG_LEVEL", "DEBUG")
20
+ LOGGING_DIR = config("LOGGING_DIR", "logs")
19
21
 
20
- OTL_SERVICE_NAME = os.getenv("OTL_SERVICE_NAME", "otel-flock")
21
- JAEGER_ENDPOINT = os.getenv(
22
+ OTEL_SERVICE_NAME = config("OTL_SERVICE_NAME", "otel-flock")
23
+ JAEGER_ENDPOINT = config(
22
24
  "JAEGER_ENDPOINT", "http://localhost:14268/api/traces"
23
25
  ) # Default gRPC endpoint for Jaeger
24
- JAEGER_TRANSPORT = os.getenv(
26
+ JAEGER_TRANSPORT = config(
25
27
  "JAEGER_TRANSPORT", "http"
26
28
  ).lower() # Options: "grpc" or "http"
29
+ OTEL_SQL_DATABASE_NAME = config("OTEL_SQL_DATABASE", "flock_events.db")
30
+ OTEL_FILE_NAME = config("OTEL_FILE_NAME", "flock_events.jsonl")
31
+ OTEL_ENABLE_SQL = config("OTEL_ENABLE_SQL", True)
32
+ OTEL_ENABLE_FILE = config("OTEL_ENABLE_FILE", True)
33
+ OTEL_ENABLE_JAEGER = config("OTEL_ENABLE_JAEGER", True)
27
34
 
28
35
 
29
- TELEMETRY = TelemetryConfig(OTL_SERVICE_NAME, JAEGER_ENDPOINT, JAEGER_TRANSPORT)
36
+ TELEMETRY = TelemetryConfig(
37
+ OTEL_SERVICE_NAME,
38
+ JAEGER_ENDPOINT,
39
+ JAEGER_TRANSPORT,
40
+ LOGGING_DIR,
41
+ OTEL_FILE_NAME,
42
+ OTEL_SQL_DATABASE_NAME,
43
+ OTEL_ENABLE_JAEGER,
44
+ OTEL_ENABLE_FILE,
45
+ OTEL_ENABLE_SQL,
46
+ )
47
+ TELEMETRY.setup_tracing()
flock/core/flock.py CHANGED
@@ -5,7 +5,7 @@ import uuid
5
5
  from typing import TypeVar
6
6
 
7
7
  from opentelemetry import trace
8
- from rich.prompt import Prompt
8
+ from opentelemetry.baggage import get_baggage, set_baggage
9
9
 
10
10
  from flock.core.context.context import FlockContext
11
11
  from flock.core.context.context_manager import initialize_context
@@ -63,6 +63,11 @@ class Flock:
63
63
  enable_logging=enable_logging,
64
64
  )
65
65
  logger.enable_logging = enable_logging
66
+ session_id = get_baggage("session_id")
67
+ if not session_id:
68
+ session_id = str(uuid.uuid4())
69
+ set_baggage("session_id", session_id)
70
+ span.set_attribute("session_id", get_baggage("session_id"))
66
71
 
67
72
  display_banner()
68
73
 
@@ -193,12 +198,16 @@ class Flock:
193
198
  run_id = f"{start_agent.name}_{uuid.uuid4().hex[:4]}"
194
199
  logger.debug("Generated run ID", run_id=run_id)
195
200
 
201
+ set_baggage("run_id", run_id)
202
+
196
203
  # TODO - Add a check for required input keys
197
204
  input_keys = top_level_to_keys(start_agent.input)
198
205
  for key in input_keys:
199
206
  if key.startswith("flock."):
200
207
  key = key[6:] # Remove the "flock." prefix
201
208
  if key not in input:
209
+ from rich.prompt import Prompt
210
+
202
211
  input[key] = Prompt.ask(
203
212
  f"Please enter {key} for {start_agent.name}"
204
213
  )
@@ -1,6 +1,5 @@
1
1
  # File: src/flock/core/logging.py
2
- """A unified logging module for Flock that works both in local/worker contexts
3
- and inside Temporal workflows.
2
+ """A unified logging module for Flock that works both in local/worker contexts and inside Temporal workflows.
4
3
 
5
4
  Key points:
6
5
  - We always have Temporal imported, so we cannot decide based on import.
@@ -24,6 +23,7 @@ with workflow.unsafe.imports_passed_through():
24
23
 
25
24
  def in_workflow_context() -> bool:
26
25
  """Returns True if this code is running inside a Temporal workflow context.
26
+
27
27
  It does this by attempting to call workflow.info() and returning True
28
28
  if successful. Otherwise, it returns False.
29
29
  """
@@ -31,10 +31,7 @@ def in_workflow_context() -> bool:
31
31
  workflow.logger.debug("Checking if in workflow context...")
32
32
  # loguru_logger.debug("Checking if in workflow context...")
33
33
  # This call will succeed only if we're in a workflow context.
34
- if hasattr(workflow.info(), "is_replaying"):
35
- return True
36
- else:
37
- return False
34
+ return bool(hasattr(workflow.info(), "is_replaying"))
38
35
  except Exception:
39
36
  return False
40
37
 
@@ -67,22 +64,24 @@ loguru_logger.add(
67
64
 
68
65
  # Define a dummy logger that does nothing
69
66
  class DummyLogger:
70
- def debug(self, *args, **kwargs):
67
+ """A dummy logger that does nothing when called."""
68
+
69
+ def debug(self, *args, **kwargs): # noqa: D102
71
70
  pass
72
71
 
73
- def info(self, *args, **kwargs):
72
+ def info(self, *args, **kwargs): # noqa: D102
74
73
  pass
75
74
 
76
- def warning(self, *args, **kwargs):
75
+ def warning(self, *args, **kwargs): # noqa: D102
77
76
  pass
78
77
 
79
- def error(self, *args, **kwargs):
78
+ def error(self, *args, **kwargs): # noqa: D102
80
79
  pass
81
80
 
82
- def exception(self, *args, **kwargs):
81
+ def exception(self, *args, **kwargs): # noqa: D102
83
82
  pass
84
83
 
85
- def success(self, *args, **kwargs):
84
+ def success(self, *args, **kwargs): # noqa: D102
86
85
  pass
87
86
 
88
87
 
@@ -92,7 +91,7 @@ dummy_logger = DummyLogger()
92
91
  class FlockLogger:
93
92
  """A unified logger that selects the appropriate logging mechanism based on context.
94
93
 
95
- - If running in a workflow context, it uses Temporal's builtin logger.
94
+ - If running in a workflow context, it uses Temporal's built-in logger.
96
95
  Additionally, if workflow.info().is_replaying is True, it suppresses debug/info/warning logs.
97
96
  - Otherwise, it uses Loguru.
98
97
  """
@@ -114,22 +113,22 @@ class FlockLogger:
114
113
  trace_id=get_current_trace_id(),
115
114
  )
116
115
 
117
- def debug(self, message: str, *args, **kwargs):
116
+ def debug(self, message: str, *args, **kwargs): # noqa: D102
118
117
  self._get_logger().debug(message, *args, **kwargs)
119
118
 
120
- def info(self, message: str, *args, **kwargs):
119
+ def info(self, message: str, *args, **kwargs): # noqa: D102
121
120
  self._get_logger().info(message, *args, **kwargs)
122
121
 
123
- def warning(self, message: str, *args, **kwargs):
122
+ def warning(self, message: str, *args, **kwargs): # noqa: D102
124
123
  self._get_logger().warning(message, *args, **kwargs)
125
124
 
126
- def error(self, message: str, *args, **kwargs):
125
+ def error(self, message: str, *args, **kwargs): # noqa: D102
127
126
  self._get_logger().error(message, *args, **kwargs)
128
127
 
129
- def exception(self, message: str, *args, **kwargs):
128
+ def exception(self, message: str, *args, **kwargs): # noqa: D102
130
129
  self._get_logger().exception(message, *args, **kwargs)
131
130
 
132
- def success(self, message: str, *args, **kwargs):
131
+ def success(self, message: str, *args, **kwargs): # noqa: D102
133
132
  self._get_logger().success(message, *args, **kwargs)
134
133
 
135
134
 
@@ -0,0 +1,31 @@
1
+ from opentelemetry.baggage import get_baggage
2
+ from opentelemetry.sdk.trace import SpanProcessor
3
+
4
+
5
+ class BaggageAttributeSpanProcessor(SpanProcessor):
6
+ """A custom span processor that, on span start, inspects the baggage items from the parent context
7
+ and attaches specified baggage keys as attributes on the span.
8
+ """
9
+
10
+ def __init__(self, baggage_keys=None):
11
+ # baggage_keys: list of baggage keys to attach to spans (e.g. ["session_id", "run_id"])
12
+ if baggage_keys is None:
13
+ baggage_keys = []
14
+ self.baggage_keys = baggage_keys
15
+
16
+ def on_start(self, span, parent_context):
17
+ # For each desired key, look up its value in the parent context baggage and set it as an attribute.
18
+ for key in self.baggage_keys:
19
+ value = get_baggage(key, context=parent_context)
20
+ if value is not None:
21
+ span.set_attribute(key, value)
22
+
23
+ def on_end(self, span):
24
+ # No action required on span end for this processor.
25
+ pass
26
+
27
+ def shutdown(self):
28
+ pass
29
+
30
+ def force_flush(self, timeout_millis: int = 30000):
31
+ pass
@@ -1,17 +1,25 @@
1
1
  """This module sets up OpenTelemetry tracing for a service."""
2
2
 
3
+ import sys
4
+
3
5
  from opentelemetry import trace
4
6
  from opentelemetry.sdk.resources import Resource
5
7
  from opentelemetry.sdk.trace import TracerProvider
6
- from opentelemetry.sdk.trace.export import (
7
- BatchSpanProcessor,
8
- )
8
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
9
+ from temporalio import workflow
9
10
 
10
- from flock.core.logging.telemetry_exporter.file_span import FileSpanExporter
11
- from flock.core.logging.telemetry_exporter.sqllite_span import (
12
- SQLiteSpanExporter,
11
+ from flock.core.logging.span_middleware.baggage_span_processor import (
12
+ BaggageAttributeSpanProcessor,
13
13
  )
14
14
 
15
+ with workflow.unsafe.imports_passed_through():
16
+ from flock.core.logging.telemetry_exporter.file_exporter import (
17
+ FileSpanExporter,
18
+ )
19
+ from flock.core.logging.telemetry_exporter.sqlite_exporter import (
20
+ SqliteTelemetryExporter,
21
+ )
22
+
15
23
 
16
24
  class TelemetryConfig:
17
25
  """This configuration class sets up OpenTelemetry tracing.
@@ -26,11 +34,15 @@ class TelemetryConfig:
26
34
  def __init__(
27
35
  self,
28
36
  service_name: str,
29
- jaeger_endpoint: str = None,
37
+ jaeger_endpoint: str | None = None,
30
38
  jaeger_transport: str = "grpc",
31
- file_export_path: str = None,
32
- sqlite_db_path: str = None,
33
- batch_processor_options: dict = None,
39
+ local_logging_dir: str | None = None,
40
+ file_export_name: str | None = None,
41
+ sqlite_db_name: str | None = None,
42
+ enable_jaeger: bool = True,
43
+ enable_file: bool = True,
44
+ enable_sql: bool = True,
45
+ batch_processor_options: dict | None = None,
34
46
  ):
35
47
  """:param service_name: Name of your service.
36
48
 
@@ -42,11 +54,16 @@ class TelemetryConfig:
42
54
  self.service_name = service_name
43
55
  self.jaeger_endpoint = jaeger_endpoint
44
56
  self.jaeger_transport = jaeger_transport
45
- self.file_export_path = file_export_path
46
- self.sqlite_db_path = sqlite_db_path
57
+ self.file_export_name = file_export_name
58
+ self.sqlite_db_name = sqlite_db_name
59
+ self.local_logging_dir = local_logging_dir
47
60
  self.batch_processor_options = batch_processor_options or {}
61
+ self.enable_jaeger = enable_jaeger
62
+ self.enable_file = enable_file
63
+ self.enable_sql = enable_sql
48
64
 
49
65
  def setup_tracing(self):
66
+ """Set up OpenTelemetry tracing with the specified exporters."""
50
67
  # Create a Resource with the service name.
51
68
  resource = Resource(attributes={"service.name": self.service_name})
52
69
  provider = TracerProvider(resource=resource)
@@ -55,8 +72,8 @@ class TelemetryConfig:
55
72
  # List to collect our span processors.
56
73
  span_processors = []
57
74
 
58
- # If a Jaeger endpoint is specified, add the Jaeger gRPC exporter.
59
- if self.jaeger_endpoint:
75
+ # If a Jaeger endpoint is specified, add the Jaeger exporter.
76
+ if self.jaeger_endpoint and self.enable_jaeger:
60
77
  if self.jaeger_transport == "grpc":
61
78
  from opentelemetry.exporter.jaeger.proto.grpc import (
62
79
  JaegerExporter,
@@ -77,33 +94,44 @@ class TelemetryConfig:
77
94
  "Invalid JAEGER_TRANSPORT specified. Use 'grpc' or 'http'."
78
95
  )
79
96
 
80
- span_processors.append(
81
- BatchSpanProcessor(
82
- jaeger_exporter, **self.batch_processor_options
83
- )
84
- )
97
+ span_processors.append(SimpleSpanProcessor(jaeger_exporter))
85
98
 
86
99
  # If a file path is provided, add the custom file exporter.
87
- if self.file_export_path:
88
- file_exporter = FileSpanExporter(self.file_export_path)
89
- span_processors.append(
90
- BatchSpanProcessor(
91
- file_exporter, **self.batch_processor_options
92
- )
100
+ if self.file_export_name and self.enable_file:
101
+ file_exporter = FileSpanExporter(
102
+ self.local_logging_dir, self.file_export_name
93
103
  )
104
+ span_processors.append(SimpleSpanProcessor(file_exporter))
94
105
 
95
- # If a SQLite database path is provided, add the custom SQLite exporter.
96
- if self.sqlite_db_path:
97
- sqlite_exporter = SQLiteSpanExporter(self.sqlite_db_path)
98
- span_processors.append(
99
- BatchSpanProcessor(
100
- sqlite_exporter, **self.batch_processor_options
101
- )
106
+ # If a SQLite database path is provided, ensure the DB exists and add the SQLite exporter.
107
+ if self.sqlite_db_name and self.enable_sql:
108
+ sqlite_exporter = SqliteTelemetryExporter(
109
+ self.local_logging_dir, self.sqlite_db_name
102
110
  )
111
+ span_processors.append(SimpleSpanProcessor(sqlite_exporter))
103
112
 
104
113
  # Register all span processors with the provider.
105
114
  for processor in span_processors:
106
115
  provider.add_span_processor(processor)
107
116
 
108
- # Return a tracer instance.
109
- return trace.get_tracer(__name__)
117
+ provider.add_span_processor(
118
+ BaggageAttributeSpanProcessor(baggage_keys=["session_id", "run_id"])
119
+ )
120
+
121
+ sys.excepthook = self.log_exception_to_otel
122
+
123
+ def log_exception_to_otel(self, exc_type, exc_value, exc_traceback):
124
+ """Log unhandled exceptions to OpenTelemetry."""
125
+ if issubclass(exc_type, KeyboardInterrupt):
126
+ # Allow normal handling of KeyboardInterrupt
127
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
128
+ return
129
+
130
+ # Use OpenTelemetry to record the exception
131
+ with self.global_tracer.start_as_current_span(
132
+ "UnhandledException"
133
+ ) as span:
134
+ span.record_exception(exc_value)
135
+ span.set_status(
136
+ trace.Status(trace.StatusCode.ERROR, str(exc_value))
137
+ )
@@ -0,0 +1,38 @@
1
+ """Base class for custom OpenTelemetry exporters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
6
+
7
+
8
+ class TelemetryExporter(SpanExporter, ABC):
9
+ """Base class for custom OpenTelemetry exporters."""
10
+
11
+ def __init__(self):
12
+ """Base class for custom OpenTelemetry exporters."""
13
+ super().__init__()
14
+
15
+ def _export(self, spans):
16
+ """Forward spans to the Jaeger exporter."""
17
+ try:
18
+ result = self.export(spans)
19
+ if result is None:
20
+ return SpanExportResult.SUCCESS
21
+ return result
22
+ except Exception:
23
+ return SpanExportResult.FAILURE
24
+ finally:
25
+ self.shutdown()
26
+
27
+ @abstractmethod
28
+ def export(self, spans) -> SpanExportResult | None:
29
+ """Export spans to the configured backend.
30
+
31
+ To be implemented by subclasses.
32
+ """
33
+ raise NotImplementedError("Subclasses must implement the export method")
34
+
35
+ @abstractmethod
36
+ def shutdown(self):
37
+ """Cleanup resources, if any. Optional for subclasses."""
38
+ pass
@@ -0,0 +1,85 @@
1
+ """A simple exporter that writes span data as JSON lines into a file."""
2
+
3
+ import json
4
+
5
+ from opentelemetry.sdk.trace.export import SpanExportResult
6
+ from opentelemetry.trace import Status, StatusCode
7
+ from temporalio import workflow
8
+
9
+ from flock.core.logging.telemetry_exporter.base_exporter import (
10
+ TelemetryExporter,
11
+ )
12
+
13
+ with workflow.unsafe.imports_passed_through():
14
+ from pathlib import Path
15
+
16
+
17
+ class FileSpanExporter(TelemetryExporter):
18
+ """A simple exporter that writes span data as JSON lines into a file."""
19
+
20
+ def __init__(self, dir: str, file_path: str = "flock_events.jsonl"):
21
+ """Initialize the exporter with a file path."""
22
+ super().__init__()
23
+ self.telemetry_path = Path(dir)
24
+ self.telemetry_path.mkdir(parents=True, exist_ok=True)
25
+ self.file_path = self.telemetry_path.joinpath(file_path).__str__()
26
+
27
+ def _span_to_json(self, span):
28
+ """Convert a ReadableSpan to a JSON-serializable dict."""
29
+ context = span.get_span_context()
30
+ status = span.status or Status(StatusCode.UNSET)
31
+
32
+ return {
33
+ "name": span.name,
34
+ "context": {
35
+ "trace_id": format(context.trace_id, "032x"),
36
+ "span_id": format(context.span_id, "016x"),
37
+ "trace_flags": context.trace_flags,
38
+ "trace_state": str(context.trace_state),
39
+ },
40
+ "kind": span.kind.name if span.kind else None,
41
+ "start_time": span.start_time,
42
+ "end_time": span.end_time,
43
+ "status": {
44
+ "status_code": status.status_code.name,
45
+ "description": status.description,
46
+ },
47
+ "attributes": dict(span.attributes or {}),
48
+ "events": [
49
+ {
50
+ "name": event.name,
51
+ "timestamp": event.timestamp,
52
+ "attributes": dict(event.attributes or {}),
53
+ }
54
+ for event in span.events
55
+ ],
56
+ "links": [
57
+ {
58
+ "context": {
59
+ "trace_id": format(link.context.trace_id, "032x"),
60
+ "span_id": format(link.context.span_id, "016x"),
61
+ },
62
+ "attributes": dict(link.attributes or {}),
63
+ }
64
+ for link in span.links
65
+ ],
66
+ "resource": {
67
+ attr_key: attr_value
68
+ for attr_key, attr_value in span.resource.attributes.items()
69
+ },
70
+ }
71
+
72
+ def export(self, spans):
73
+ """Write spans to a log file."""
74
+ try:
75
+ with open(self.file_path, "a") as f:
76
+ for span in spans:
77
+ json_span = self._span_to_json(span)
78
+ f.write(f"{json.dumps(json_span)}\n")
79
+ return SpanExportResult.SUCCESS
80
+ except Exception:
81
+ return SpanExportResult.FAILURE
82
+
83
+ def shutdown(self) -> None:
84
+ # Nothing special needed on shutdown.
85
+ pass
@@ -0,0 +1,103 @@
1
+ """Exporter for storing OpenTelemetry spans in SQLite."""
2
+
3
+ import json
4
+ import sqlite3
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from opentelemetry.sdk.trace.export import SpanExportResult
9
+
10
+ from flock.core.logging.telemetry_exporter.base_exporter import (
11
+ TelemetryExporter,
12
+ )
13
+
14
+
15
+ class SqliteTelemetryExporter(TelemetryExporter):
16
+ """Exporter for storing OpenTelemetry spans in SQLite."""
17
+
18
+ def __init__(self, dir: str, db_path: str = "flock_events.db"):
19
+ """Initialize the SQLite exporter.
20
+
21
+ Args:
22
+ db_path: Path to the SQLite database file
23
+ """
24
+ super().__init__()
25
+ self.telemetry_path = Path(dir)
26
+ self.telemetry_path.mkdir(parents=True, exist_ok=True)
27
+ # Create an absolute path to the database file:
28
+ self.db_path = self.telemetry_path.joinpath(db_path).resolve().__str__()
29
+ # Use the absolute path when connecting:
30
+ self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
31
+ self._initialize_database()
32
+
33
+ def _initialize_database(self):
34
+ """Set up the SQLite database schema."""
35
+ cursor = self.conn.cursor()
36
+ cursor.execute(
37
+ """
38
+ CREATE TABLE IF NOT EXISTS spans (
39
+ id TEXT PRIMARY KEY,
40
+ name TEXT,
41
+ trace_id TEXT,
42
+ span_id TEXT,
43
+ start_time INTEGER,
44
+ end_time INTEGER,
45
+ attributes TEXT,
46
+ status TEXT
47
+ )
48
+ """
49
+ )
50
+ self.conn.commit()
51
+
52
+ def _convert_attributes(self, attributes: dict[str, Any]) -> str:
53
+ """Convert span attributes to a JSON string.
54
+
55
+ Args:
56
+ attributes: Dictionary of span attributes
57
+
58
+ Returns:
59
+ JSON string representation of attributes
60
+ """
61
+ # Convert attributes to a serializable format
62
+ serializable_attrs = {}
63
+ for key, value in attributes.items():
64
+ # Convert complex types to strings if needed
65
+ if isinstance(value, dict | list | tuple):
66
+ serializable_attrs[key] = json.dumps(value)
67
+ else:
68
+ serializable_attrs[key] = str(value)
69
+ return json.dumps(serializable_attrs)
70
+
71
+ def export(self, spans) -> SpanExportResult:
72
+ """Export spans to SQLite."""
73
+ try:
74
+ cursor = self.conn.cursor()
75
+ for span in spans:
76
+ span_id = format(span.context.span_id, "016x")
77
+ trace_id = format(span.context.trace_id, "032x")
78
+ cursor.execute(
79
+ """
80
+ INSERT OR REPLACE INTO spans
81
+ (id, name, trace_id, span_id, start_time, end_time, attributes, status)
82
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
83
+ """,
84
+ (
85
+ span_id,
86
+ span.name,
87
+ trace_id,
88
+ span_id,
89
+ span.start_time,
90
+ span.end_time,
91
+ self._convert_attributes(span.attributes),
92
+ str(span.status),
93
+ ),
94
+ )
95
+ self.conn.commit()
96
+ return SpanExportResult.SUCCESS
97
+ except Exception as e:
98
+ print("Error exporting spans to SQLite:", e)
99
+ return SpanExportResult.FAILURE
100
+
101
+ def shutdown(self) -> None:
102
+ """Cleanup resources."""
103
+ pass
@@ -1,3 +1,5 @@
1
+ """A decorator that wraps a function in an OpenTelemetry span and logs its inputs, outputs, and exceptions."""
2
+
1
3
  import functools
2
4
  import inspect
3
5
 
@@ -10,7 +12,9 @@ tracer = trace.get_tracer(__name__)
10
12
 
11
13
 
12
14
  def traced_and_logged(func):
13
- """A decorator that wraps a function in an OpenTelemetry span and logs its inputs,
15
+ """A decorator that wraps a function in an OpenTelemetry span.
16
+
17
+ and logs its inputs,
14
18
  outputs, and exceptions. Supports both synchronous and asynchronous functions.
15
19
  """
16
20
  if inspect.iscoroutinefunction(func):
@@ -0,0 +1,49 @@
1
+ import subprocess
2
+ import time
3
+
4
+
5
+ def _check_docker_running():
6
+ """Check if Docker is running by calling 'docker info'."""
7
+ try:
8
+ result = subprocess.run(
9
+ ["docker", "info"],
10
+ stdout=subprocess.PIPE,
11
+ stderr=subprocess.PIPE,
12
+ text=True,
13
+ )
14
+ return result.returncode == 0
15
+ except Exception:
16
+ return False
17
+
18
+
19
+ def _start_docker():
20
+ """Attempt to start Docker.
21
+ This example first tries 'systemctl start docker' and then 'service docker start'.
22
+ Adjust as needed for your environment.
23
+ """
24
+ try:
25
+ print("Attempting to start Docker...")
26
+ result = subprocess.run(
27
+ ["sudo", "systemctl", "start", "docker"],
28
+ stdout=subprocess.PIPE,
29
+ stderr=subprocess.PIPE,
30
+ text=True,
31
+ )
32
+ if result.returncode != 0:
33
+ result = subprocess.run(
34
+ ["sudo", "service", "docker", "start"],
35
+ stdout=subprocess.PIPE,
36
+ stderr=subprocess.PIPE,
37
+ text=True,
38
+ )
39
+ # Give Docker a moment to start.
40
+ time.sleep(3)
41
+ if _check_docker_running():
42
+ print("Docker is now running.")
43
+ return True
44
+ else:
45
+ print("Docker did not start successfully.")
46
+ return False
47
+ except Exception as e:
48
+ print(f"Exception when trying to start Docker: {e}")
49
+ return False
@@ -0,0 +1,86 @@
1
+ import socket
2
+ import subprocess
3
+ from urllib.parse import urlparse
4
+
5
+
6
+ class JaegerInstaller:
7
+ jaeger_endpoint: str = None
8
+ jaeger_transport: str = "grpc"
9
+
10
+ def _check_jaeger_running(self):
11
+ """Check if Jaeger is reachable by attempting a socket connection.
12
+ For HTTP transport, we parse the URL; for gRPC, we expect "host:port".
13
+ """
14
+ try:
15
+ if self.jaeger_transport == "grpc":
16
+ host, port = self.jaeger_endpoint.split(":")
17
+ port = int(port)
18
+ elif self.jaeger_transport == "http":
19
+ parsed = urlparse(self.jaeger_endpoint)
20
+ host = parsed.hostname
21
+ port = parsed.port if parsed.port else 80
22
+ else:
23
+ return False
24
+
25
+ # Try connecting to the host and port.
26
+ with socket.create_connection((host, port), timeout=3):
27
+ return True
28
+ except Exception:
29
+ return False
30
+
31
+ def _is_jaeger_container_running(self):
32
+ """Check if a Jaeger container (using the official all-in-one image) is running.
33
+ This uses 'docker ps' to filter for containers running the Jaeger image.
34
+ """
35
+ try:
36
+ result = subprocess.run(
37
+ [
38
+ "docker",
39
+ "ps",
40
+ "--filter",
41
+ "ancestor=jaegertracing/all-in-one:latest",
42
+ "--format",
43
+ "{{.ID}}",
44
+ ],
45
+ stdout=subprocess.PIPE,
46
+ stderr=subprocess.PIPE,
47
+ text=True,
48
+ )
49
+ return bool(result.stdout.strip())
50
+ except Exception:
51
+ return False
52
+
53
+ def _provision_jaeger_container(self):
54
+ """Provision a Jaeger container using Docker."""
55
+ try:
56
+ print("Provisioning Jaeger container using Docker...")
57
+ result = subprocess.run(
58
+ [
59
+ "docker",
60
+ "run",
61
+ "-d",
62
+ "--name",
63
+ "jaeger",
64
+ "-p",
65
+ "16686:16686",
66
+ "-p",
67
+ "14250:14250",
68
+ "-p",
69
+ "14268:14268",
70
+ "jaegertracing/all-in-one:latest",
71
+ ],
72
+ stdout=subprocess.PIPE,
73
+ stderr=subprocess.PIPE,
74
+ text=True,
75
+ )
76
+ if result.returncode == 0:
77
+ print("Jaeger container started successfully.")
78
+ return True
79
+ else:
80
+ print(
81
+ f"Failed to start Jaeger container. Error: {result.stderr}"
82
+ )
83
+ return False
84
+ except Exception as e:
85
+ print(f"Exception when provisioning Jaeger container: {e}")
86
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flock-core
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Declarative LLM Orchestration at Scale
5
5
  Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
6
6
  License-File: LICENSE
@@ -11,6 +11,7 @@ Requires-Python: >=3.10
11
11
  Requires-Dist: cloudpickle>=3.1.1
12
12
  Requires-Dist: devtools>=0.12.2
13
13
  Requires-Dist: dspy==2.5.42
14
+ Requires-Dist: duckduckgo-search>=7.3.2
14
15
  Requires-Dist: httpx>=0.28.1
15
16
  Requires-Dist: loguru>=0.7.3
16
17
  Requires-Dist: msgpack>=1.1.0
@@ -22,6 +23,7 @@ Requires-Dist: opentelemetry-instrumentation-logging>=0.51b0
22
23
  Requires-Dist: opentelemetry-sdk>=1.30.0
23
24
  Requires-Dist: pydantic>=2.10.5
24
25
  Requires-Dist: python-box>=7.3.2
26
+ Requires-Dist: python-decouple>=3.8
25
27
  Requires-Dist: rich>=13.9.4
26
28
  Requires-Dist: temporalio>=1.9.0
27
29
  Requires-Dist: toml>=0.10.2
@@ -1,7 +1,7 @@
1
- flock/__init__.py,sha256=uJxXAxt0def69cccAAdLjBxQOFXVRO72RE82hJnODSw,108
2
- flock/config.py,sha256=hul_Vmgl8w5a_4YNfg3Mvll3iVEkjiR5At9D0WsVesw,1006
1
+ flock/__init__.py,sha256=P175tsTOByIhw_CIeMAybUEggKhtoSrc8a0gKotBJfo,177
2
+ flock/config.py,sha256=dyLqNng9ETTEx3CRCkz5N4g62Nu4ygPA6uQKEdbSgag,1499
3
3
  flock/core/__init__.py,sha256=0Xq_txurlxxjKGXjRn6GNJusGTiBcd7zw2eF0L7JyuU,183
4
- flock/core/flock.py,sha256=Iw7Frmz2aZheApxi2KSjsX7gA8ZcemzXhfKXeQdtY0w,9438
4
+ flock/core/flock.py,sha256=0NC-J_ZCojwWDepI6rbX-9jG_Hr2AKgY43ieijhD8DU,9820
5
5
  flock/core/flock_agent.py,sha256=59qQ7ohOy2lc1KjI6SV7IcrqYL86ofAhq32pZGgk6eA,27761
6
6
  flock/core/context/context.py,sha256=jH06w4C_O5CEL-YxjX_x_dmgLe9Rcllnn1Ebs0dvwaE,6171
7
7
  flock/core/context/context_manager.py,sha256=qMySVny_dbTNLh21RHK_YT0mNKIOrqJDZpi9ZVdBsxU,1103
@@ -9,17 +9,19 @@ flock/core/context/context_vars.py,sha256=0Hn6fM2iNc0_jIIU0B7KX-K2o8qXqtZ5EYtwuj
9
9
  flock/core/execution/local_executor.py,sha256=O_dgQ_HJPCp97ghdEoDSNDIiaYkogrUS0G2FfK04RRc,973
10
10
  flock/core/execution/temporal_executor.py,sha256=ai6ikr9rEiN2Kc-208OalxtfqL_FTt_UaH6a--oEkJM,2010
11
11
  flock/core/logging/__init__.py,sha256=Q8hp9-1ilPIUIV0jLgJ3_cP7COrea32cVwL7dicPnlM,82
12
- flock/core/logging/logging.py,sha256=F-FDz9etBXmAIT-fjx3pUfvNXsckgR6ONCMaFZIc4Kw,4619
13
- flock/core/logging/telemetry.py,sha256=T2CRSiqOWvOsXe-WRsObkkOkrrd6z-BwEYLaBUU2AAM,4017
14
- flock/core/logging/trace_and_logged.py,sha256=h4YH8s0KjK4tiBdrEZdCLd4fDzMB5-NKwqzrtkWhQw4,1999
12
+ flock/core/logging/logging.py,sha256=dOo_McbAt9_dST9Hr8RTpAGU-MHR_QvskIdmXrSvZRc,4789
13
+ flock/core/logging/telemetry.py,sha256=yEOfEZ3HBFeLCaHZA6QmsRdwZKtmUC6bQtEOTVeRR4o,5314
14
+ flock/core/logging/trace_and_logged.py,sha256=5vNrK1kxuPMoPJ0-QjQg-EDJL1oiEzvU6UNi6X8FiMs,2117
15
15
  flock/core/logging/formatters/base_formatter.py,sha256=CyG-X2NWq8sqEhFEO2aG7Mey5tVkIzoWiihW301_VIo,1023
16
16
  flock/core/logging/formatters/formatter_factory.py,sha256=hmH-NpCESHkioX0GBQ5CuQR4axyIXnSRWwAZCHylx6Q,1283
17
17
  flock/core/logging/formatters/pprint_formatter.py,sha256=tTm2WhwlCw-SX2Ouci5I9U_HVgxNGY5SSnzB9HZh8bg,692
18
18
  flock/core/logging/formatters/rich_formatters.py,sha256=h1FD0_cIdQBQ8P2x05XhgD1cmmP80IBNVT5jb3cAV9M,4776
19
19
  flock/core/logging/formatters/theme_builder.py,sha256=1RUEwPIDfCjwTapbK1liasA5SdukOn7YwbZ4H4j1WkI,17364
20
20
  flock/core/logging/formatters/themed_formatter.py,sha256=CbxmqUC7zkLzyIxngk-3dcpQ6vxPR6zaDNA2TAMitCI,16714
21
- flock/core/logging/telemetry_exporter/file_span.py,sha256=e4hr4D7tC9j4KT7JZBuZU0YxQCdHKADpXNeUNEwggN4,1294
22
- flock/core/logging/telemetry_exporter/sqllite_span.py,sha256=9bqxHt1mDQGyhKxA9dON5xDi_6FMOfBSdrU_zWV0xv4,2107
21
+ flock/core/logging/span_middleware/baggage_span_processor.py,sha256=gJfRl8FeB6jdtghTaRHCrOaTo4fhPMRKgjqtZj-8T48,1118
22
+ flock/core/logging/telemetry_exporter/base_exporter.py,sha256=rQJJzS6q9n2aojoSqwCnl7ZtHrh5LZZ-gkxUuI5WfrQ,1124
23
+ flock/core/logging/telemetry_exporter/file_exporter.py,sha256=nKAjJSZtA7FqHSTuTiFtYYepaxOq7l1rDvs8U8rSBlA,3023
24
+ flock/core/logging/telemetry_exporter/sqlite_exporter.py,sha256=CDsiMb9QcqeXelZ6ZqPSS56ovMPGqOu6whzBZRK__Vg,3498
23
25
  flock/core/mixin/dspy_integration.py,sha256=oT5YfXxPhHkMCuwhXoppBAYBGePUAKse7KebGSM-bq0,6880
24
26
  flock/core/mixin/prompt_parser.py,sha256=eOqI-FK3y17gVqpc_y5GF-WmK1Jv8mFlkZxTcgweoxI,5121
25
27
  flock/core/registry/agent_registry.py,sha256=QHdr3Cb-32PEdz8jFCIZSH9OlfpRwAJMtSRpHCWJDq4,4889
@@ -28,6 +30,8 @@ flock/core/tools/dev_tools/github.py,sha256=6ya2_eN-qITV3b_pYP24jQC3X4oZbRY5GKh1
28
30
  flock/core/util/cli_helper.py,sha256=aHLKjl5JBLIczLzjYeUcGQlVQRlypunxV2TYeAFX0KE,1030
29
31
  flock/core/util/input_resolver.py,sha256=OesGqX2Dld8myL9Qz04mmxLqoYqOSQC632pj1EMk9Yk,5456
30
32
  flock/core/util/serializable.py,sha256=SymJ0YrjBx48mOBItYSqoRpKuzIc4vKWRS6ScTzre7s,2573
33
+ flock/platform/docker_tools.py,sha256=fpA7-6rJBjPOUBLdQP4ny2QPgJ_042nmqRn5GtKnoYw,1445
34
+ flock/platform/jaeger_install.py,sha256=MyOMJQx4TQSMYvdUJxfiGSo3YCtsfkbNXcAcQ9bjETA,2898
31
35
  flock/themes/3024-day.toml,sha256=uOVHqEzSyHx0WlUk3D0lne4RBsNBAPCTy3C58yU7kEY,667
32
36
  flock/themes/3024-night.toml,sha256=qsXUwd6ZYz6J-R129_Ao2TKlvvK60svhZJJjB5c8Tfo,1667
33
37
  flock/themes/aardvark-blue.toml,sha256=Px1qevE6J1ZAc_jAqF_FX674KdIv_3pAYNKmmvDxIbE,1672
@@ -369,7 +373,8 @@ flock/workflow/activities.py,sha256=YEg-Gr8kzVsxWsmsZguIVhX2XwMRvhZ2OlnsJoG5g_A,
369
373
  flock/workflow/agent_activities.py,sha256=NhBZscflEf2IMfSRa_pBM_TRP7uVEF_O0ROvWZ33eDc,963
370
374
  flock/workflow/temporal_setup.py,sha256=VWBgmBgfTBjwM5ruS_dVpA5AVxx6EZ7oFPGw4j3m0l0,1091
371
375
  flock/workflow/workflow.py,sha256=I9MryXW_bqYVTHx-nl2epbTqeRy27CAWHHA7ZZA0nAk,1696
372
- flock_core-0.2.4.dist-info/METADATA,sha256=VztTdTQKj4pIiPnSearGjlMDmPA_1o0Nak_EefDzs4o,11488
373
- flock_core-0.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
374
- flock_core-0.2.4.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
375
- flock_core-0.2.4.dist-info/RECORD,,
376
+ flock_core-0.2.5.dist-info/METADATA,sha256=T9Yd-0aEUNgHlQnQp64zeDIkDgAsQRoPMF3WnBLUgCk,11564
377
+ flock_core-0.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
378
+ flock_core-0.2.5.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
379
+ flock_core-0.2.5.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
380
+ flock_core-0.2.5.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flock = flock:main
@@ -1,37 +0,0 @@
1
- import json
2
-
3
- from opentelemetry.sdk.trace.export import (
4
- SpanExporter,
5
- SpanExportResult,
6
- )
7
-
8
-
9
- class FileSpanExporter(SpanExporter):
10
- """A simple exporter that writes span data as JSON lines into a file."""
11
-
12
- def __init__(self, file_path: str):
13
- self.file_path = file_path
14
-
15
- def export(self, spans) -> SpanExportResult:
16
- try:
17
- with open(self.file_path, "a") as f:
18
- for span in spans:
19
- # Create a dictionary representation of the span.
20
- span_dict = {
21
- "name": span.name,
22
- "trace_id": format(span.context.trace_id, "032x"),
23
- "span_id": format(span.context.span_id, "016x"),
24
- "start_time": span.start_time,
25
- "end_time": span.end_time,
26
- "attributes": span.attributes,
27
- "status": str(span.status),
28
- }
29
- f.write(json.dumps(span_dict) + "\n")
30
- return SpanExportResult.SUCCESS
31
- except Exception as e:
32
- print("Error exporting spans to file:", e)
33
- return SpanExportResult.FAILURE
34
-
35
- def shutdown(self) -> None:
36
- # Nothing special needed on shutdown.
37
- pass
@@ -1,68 +0,0 @@
1
- import json
2
- import sqlite3
3
-
4
- from opentelemetry.sdk.trace.export import (
5
- SpanExporter,
6
- SpanExportResult,
7
- )
8
-
9
-
10
- class SQLiteSpanExporter(SpanExporter):
11
- """A custom exporter that writes span data into a SQLite database."""
12
-
13
- def __init__(self, sqlite_db_path: str):
14
- self.sqlite_db_path = sqlite_db_path
15
- self.conn = sqlite3.connect(
16
- self.sqlite_db_path, check_same_thread=False
17
- )
18
- self._create_table()
19
-
20
- def _create_table(self):
21
- cursor = self.conn.cursor()
22
- cursor.execute(
23
- """
24
- CREATE TABLE IF NOT EXISTS spans (
25
- id TEXT PRIMARY KEY,
26
- name TEXT,
27
- trace_id TEXT,
28
- span_id TEXT,
29
- start_time INTEGER,
30
- end_time INTEGER,
31
- attributes TEXT,
32
- status TEXT
33
- )
34
- """
35
- )
36
- self.conn.commit()
37
-
38
- def export(self, spans) -> SpanExportResult:
39
- try:
40
- cursor = self.conn.cursor()
41
- for span in spans:
42
- span_id = format(span.context.span_id, "016x")
43
- trace_id = format(span.context.trace_id, "032x")
44
- cursor.execute(
45
- """
46
- INSERT OR REPLACE INTO spans
47
- (id, name, trace_id, span_id, start_time, end_time, attributes, status)
48
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
49
- """,
50
- (
51
- span_id,
52
- span.name,
53
- trace_id,
54
- span_id,
55
- span.start_time,
56
- span.end_time,
57
- json.dumps(span.attributes),
58
- str(span.status),
59
- ),
60
- )
61
- self.conn.commit()
62
- return SpanExportResult.SUCCESS
63
- except Exception as e:
64
- print("Error exporting spans to SQLite:", e)
65
- return SpanExportResult.FAILURE
66
-
67
- def shutdown(self) -> None:
68
- self.conn.close()