ragbits-core 0.16.0__py3-none-any.whl → 1.4.0.dev202512021005__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.
Files changed (63) hide show
  1. ragbits/core/__init__.py +21 -2
  2. ragbits/core/audit/__init__.py +15 -157
  3. ragbits/core/audit/metrics/__init__.py +83 -0
  4. ragbits/core/audit/metrics/base.py +198 -0
  5. ragbits/core/audit/metrics/logfire.py +19 -0
  6. ragbits/core/audit/metrics/otel.py +65 -0
  7. ragbits/core/audit/traces/__init__.py +171 -0
  8. ragbits/core/audit/{base.py → traces/base.py} +9 -5
  9. ragbits/core/audit/{cli.py → traces/cli.py} +8 -4
  10. ragbits/core/audit/traces/logfire.py +18 -0
  11. ragbits/core/audit/{otel.py → traces/otel.py} +5 -8
  12. ragbits/core/config.py +15 -0
  13. ragbits/core/embeddings/__init__.py +2 -1
  14. ragbits/core/embeddings/base.py +19 -0
  15. ragbits/core/embeddings/dense/base.py +10 -1
  16. ragbits/core/embeddings/dense/fastembed.py +22 -1
  17. ragbits/core/embeddings/dense/litellm.py +37 -10
  18. ragbits/core/embeddings/dense/local.py +15 -1
  19. ragbits/core/embeddings/dense/noop.py +11 -1
  20. ragbits/core/embeddings/dense/vertex_multimodal.py +14 -1
  21. ragbits/core/embeddings/sparse/bag_of_tokens.py +47 -17
  22. ragbits/core/embeddings/sparse/base.py +10 -1
  23. ragbits/core/embeddings/sparse/fastembed.py +25 -2
  24. ragbits/core/llms/__init__.py +3 -3
  25. ragbits/core/llms/base.py +612 -88
  26. ragbits/core/llms/exceptions.py +27 -0
  27. ragbits/core/llms/litellm.py +408 -83
  28. ragbits/core/llms/local.py +180 -41
  29. ragbits/core/llms/mock.py +88 -23
  30. ragbits/core/prompt/__init__.py +2 -2
  31. ragbits/core/prompt/_cli.py +32 -19
  32. ragbits/core/prompt/base.py +105 -19
  33. ragbits/core/prompt/{discovery/prompt_discovery.py → discovery.py} +1 -1
  34. ragbits/core/prompt/exceptions.py +22 -6
  35. ragbits/core/prompt/prompt.py +180 -98
  36. ragbits/core/sources/__init__.py +2 -0
  37. ragbits/core/sources/azure.py +1 -1
  38. ragbits/core/sources/base.py +8 -1
  39. ragbits/core/sources/gcs.py +1 -1
  40. ragbits/core/sources/git.py +1 -1
  41. ragbits/core/sources/google_drive.py +595 -0
  42. ragbits/core/sources/hf.py +71 -31
  43. ragbits/core/sources/local.py +1 -1
  44. ragbits/core/sources/s3.py +1 -1
  45. ragbits/core/utils/config_handling.py +13 -2
  46. ragbits/core/utils/function_schema.py +220 -0
  47. ragbits/core/utils/helpers.py +22 -0
  48. ragbits/core/utils/lazy_litellm.py +44 -0
  49. ragbits/core/vector_stores/base.py +18 -1
  50. ragbits/core/vector_stores/chroma.py +28 -11
  51. ragbits/core/vector_stores/hybrid.py +1 -1
  52. ragbits/core/vector_stores/hybrid_strategies.py +21 -8
  53. ragbits/core/vector_stores/in_memory.py +13 -4
  54. ragbits/core/vector_stores/pgvector.py +123 -47
  55. ragbits/core/vector_stores/qdrant.py +15 -7
  56. ragbits/core/vector_stores/weaviate.py +440 -0
  57. {ragbits_core-0.16.0.dist-info → ragbits_core-1.4.0.dev202512021005.dist-info}/METADATA +22 -6
  58. ragbits_core-1.4.0.dev202512021005.dist-info/RECORD +79 -0
  59. {ragbits_core-0.16.0.dist-info → ragbits_core-1.4.0.dev202512021005.dist-info}/WHEEL +1 -1
  60. ragbits/core/prompt/discovery/__init__.py +0 -3
  61. ragbits/core/prompt/lab/__init__.py +0 -0
  62. ragbits/core/prompt/lab/app.py +0 -262
  63. ragbits_core-0.16.0.dist-info/RECORD +0 -72
ragbits/core/__init__.py CHANGED
@@ -1,9 +1,28 @@
1
1
  import os
2
+ from concurrent.futures import ThreadPoolExecutor
2
3
 
3
4
  import typer
4
5
 
5
- from ragbits.core import audit
6
+ from ragbits.core.audit.traces import set_trace_handlers
7
+
8
+ _config_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="config-import")
9
+ _config_future = None
10
+
11
+
12
+ def _import_and_run_config() -> None:
13
+ from ragbits.core.config import import_modules_from_config
14
+
15
+ import_modules_from_config()
16
+
17
+
18
+ def ensure_config_loaded() -> None:
19
+ """Wait for config import to complete if it hasn't already."""
20
+ if _config_future:
21
+ _config_future.result()
22
+
6
23
 
7
24
  if os.getenv("RAGBITS_VERBOSE", "0") == "1":
8
25
  typer.echo('Verbose mode is enabled with environment variable "RAGBITS_VERBOSE".')
9
- audit.set_trace_handlers("cli")
26
+ set_trace_handlers("cli")
27
+
28
+ _config_future = _config_executor.submit(_import_and_run_config)
@@ -1,157 +1,15 @@
1
- import asyncio
2
- import inspect
3
- from collections.abc import Callable, Iterator
4
- from contextlib import ExitStack, contextmanager
5
- from functools import wraps
6
- from types import SimpleNamespace
7
- from typing import Any, ParamSpec, TypeVar
8
-
9
- from ragbits.core.audit.base import TraceHandler
10
-
11
- __all__ = ["TraceHandler", "set_trace_handlers", "trace", "traceable"]
12
-
13
- _trace_handlers: list[TraceHandler] = []
14
-
15
- Handler = str | TraceHandler
16
-
17
- P = ParamSpec("P")
18
- R = TypeVar("R")
19
-
20
-
21
- def set_trace_handlers(handlers: Handler | list[Handler]) -> None:
22
- """
23
- Setup trace handlers.
24
-
25
- Args:
26
- handlers: List of trace handlers to be used.
27
-
28
- Raises:
29
- ValueError: If handler is not found.
30
- TypeError: If handler type is invalid.
31
- """
32
- global _trace_handlers # noqa: PLW0602
33
-
34
- if isinstance(handlers, Handler):
35
- handlers = [handlers]
36
-
37
- for handler in handlers: # type: ignore
38
- if isinstance(handler, TraceHandler):
39
- _trace_handlers.append(handler)
40
- elif isinstance(handler, str):
41
- if handler == "otel":
42
- from ragbits.core.audit.otel import OtelTraceHandler
43
-
44
- if not any(isinstance(item, OtelTraceHandler) for item in _trace_handlers):
45
- _trace_handlers.append(OtelTraceHandler())
46
- elif handler == "cli":
47
- from ragbits.core.audit.cli import CLITraceHandler
48
-
49
- if not any(isinstance(item, CLITraceHandler) for item in _trace_handlers):
50
- _trace_handlers.append(CLITraceHandler())
51
- else:
52
- raise ValueError(f"Handler {handler} not found.")
53
- else:
54
- raise TypeError(f"Invalid handler type: {type(handler)}")
55
-
56
-
57
- def clear_event_handlers() -> None:
58
- """
59
- Clear all trace handlers.
60
- """
61
- global _trace_handlers # noqa: PLW0602
62
-
63
- _trace_handlers.clear()
64
-
65
-
66
- @contextmanager
67
- def trace(name: str | None = None, **inputs: Any) -> Iterator[SimpleNamespace]: # noqa: ANN401
68
- """
69
- Context manager for processing a trace.
70
-
71
- Args:
72
- name: The name of the trace.
73
- inputs: The input data.
74
-
75
- Yields:
76
- The output data.
77
- """
78
- # We need to go up 2 frames (trace() and __enter__()) to get the parent function.
79
- parent_frame = inspect.stack()[2].frame
80
- name = (
81
- (
82
- f"{cls.__class__.__qualname__}.{parent_frame.f_code.co_name}"
83
- if (cls := parent_frame.f_locals.get("self"))
84
- else parent_frame.f_code.co_name
85
- )
86
- if name is None
87
- else name
88
- )
89
-
90
- with ExitStack() as stack:
91
- outputs = [stack.enter_context(handler.trace(name, **inputs)) for handler in _trace_handlers]
92
- yield (out := SimpleNamespace())
93
- for output in outputs:
94
- output.__dict__.update(vars(out))
95
-
96
-
97
- def traceable(func: Callable[P, R]) -> Callable[P, R]:
98
- """
99
- Decorator for making a function traceable.
100
-
101
- Args:
102
- func: The function to be decorated.
103
-
104
- Returns:
105
- The decorated function.
106
- """
107
-
108
- @wraps(func)
109
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
110
- inputs = _get_function_inputs(func, args, kwargs)
111
- with trace(name=func.__qualname__, **inputs) as outputs:
112
- returned = func(*args, **kwargs)
113
- if returned is not None:
114
- outputs.returned = returned
115
- return returned
116
-
117
- @wraps(func)
118
- async def wrapper_async(*args: P.args, **kwargs: P.kwargs) -> R:
119
- inputs = _get_function_inputs(func, args, kwargs)
120
- with trace(name=func.__qualname__, **inputs) as outputs:
121
- returned = await func(*args, **kwargs) # type: ignore
122
- if returned is not None:
123
- outputs.returned = returned
124
- return returned
125
-
126
- return wrapper_async if asyncio.iscoroutinefunction(func) else wrapper # type: ignore
127
-
128
-
129
- def _get_function_inputs(func: Callable, args: tuple, kwargs: dict) -> dict:
130
- """
131
- Get the dictionary of inputs for a function based on positional and keyword arguments.
132
-
133
- Args:
134
- func: The function to get inputs for.
135
- args: The positional arguments.
136
- kwargs: The keyword arguments.
137
-
138
- Returns:
139
- The dictionary of inputs.
140
- """
141
- sig_params = inspect.signature(func).parameters
142
- merged = {}
143
- pos_args_used = 0
144
-
145
- for param_name, param in sig_params.items():
146
- if param_name in kwargs:
147
- merged[param_name] = kwargs[param_name]
148
- elif pos_args_used < len(args):
149
- if param_name not in ("self", "cls", "args", "kwargs"):
150
- merged[param_name] = args[pos_args_used]
151
- pos_args_used += 1
152
- elif param.default is not param.empty:
153
- merged[param_name] = param.default
154
-
155
- merged.update({k: v for k, v in kwargs.items() if k not in merged})
156
-
157
- return merged
1
+ from ragbits.core.audit.metrics import clear_metric_handlers, set_metric_handlers
2
+ from ragbits.core.audit.metrics.base import MetricHandler
3
+ from ragbits.core.audit.traces import clear_trace_handlers, set_trace_handlers, trace, traceable
4
+ from ragbits.core.audit.traces.base import TraceHandler
5
+
6
+ __all__ = [
7
+ "MetricHandler",
8
+ "TraceHandler",
9
+ "clear_metric_handlers",
10
+ "clear_trace_handlers",
11
+ "set_metric_handlers",
12
+ "set_trace_handlers",
13
+ "trace",
14
+ "traceable",
15
+ ]
@@ -0,0 +1,83 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from ragbits.core.audit.metrics.base import MetricHandler, MetricType, register_metric
5
+
6
+ __all__ = [
7
+ "MetricHandler",
8
+ "MetricType",
9
+ "clear_metric_handlers",
10
+ "record_metric",
11
+ "register_metric",
12
+ "set_metric_handlers",
13
+ ]
14
+
15
+ Handler = str | MetricHandler
16
+
17
+ _metric_handlers: list[MetricHandler] = []
18
+
19
+
20
+ def set_metric_handlers(handlers: Handler | list[Handler]) -> None:
21
+ """
22
+ Set the global metric handlers.
23
+
24
+ Args:
25
+ handlers: List of metric handlers to be used.
26
+
27
+ Raises:
28
+ ValueError: If handler is not found.
29
+ TypeError: If handler type is invalid.
30
+ """
31
+ global _metric_handlers # noqa: PLW0602
32
+
33
+ if isinstance(handlers, Handler):
34
+ handlers = [handlers]
35
+
36
+ for handler in handlers:
37
+ if isinstance(handler, MetricHandler):
38
+ _metric_handlers.append(handler)
39
+ elif isinstance(handler, str):
40
+ match handler.lower():
41
+ case "otel":
42
+ from ragbits.core.audit.metrics.otel import OtelMetricHandler
43
+
44
+ if not any(isinstance(item, OtelMetricHandler) for item in _metric_handlers):
45
+ _metric_handlers.append(OtelMetricHandler())
46
+
47
+ case "logfire":
48
+ from ragbits.core.audit.metrics.logfire import LogfireMetricHandler
49
+
50
+ if not any(isinstance(item, LogfireMetricHandler) for item in _metric_handlers):
51
+ _metric_handlers.append(LogfireMetricHandler())
52
+
53
+ case _:
54
+ raise ValueError(f"Not found handler: {handler}")
55
+ else:
56
+ raise TypeError(f"Invalid handler type: {type(handler)}")
57
+
58
+
59
+ def clear_metric_handlers() -> None:
60
+ """
61
+ Clear all metric handlers.
62
+ """
63
+ global _metric_handlers # noqa: PLW0602
64
+ _metric_handlers.clear()
65
+
66
+
67
+ def record_metric(
68
+ metric: str | Enum,
69
+ value: int | float,
70
+ metric_type: MetricType,
71
+ **attributes: Any, # noqa: ANN401
72
+ ) -> None:
73
+ """
74
+ Record a metric of any type using the global metric handlers.
75
+
76
+ Args:
77
+ metric: The metric key (name or enum value) to record
78
+ value: The value to record
79
+ metric_type: The type of metric (histogram, counter, gauge)
80
+ **attributes: Additional metadata for the metric
81
+ """
82
+ for handler in _metric_handlers:
83
+ handler.record_metric(metric_key=metric, value=value, attributes=attributes, metric_type=metric_type)
@@ -0,0 +1,198 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from enum import Enum, auto
4
+ from typing import Any
5
+
6
+
7
+ class MetricType(Enum):
8
+ """Supported metric types."""
9
+
10
+ HISTOGRAM = "histogram"
11
+ COUNTER = "counter"
12
+ GAUGE = "gauge"
13
+
14
+
15
+ @dataclass
16
+ class Metric:
17
+ """
18
+ Represents the metric configuration data.
19
+ """
20
+
21
+ name: str
22
+ description: str
23
+ unit: str
24
+ type: MetricType
25
+
26
+
27
+ class LLMMetric(Enum):
28
+ """
29
+ LLM-related metrics that can be recorded.
30
+ Each metric has a predefined type and is registered in the global registry.
31
+ """
32
+
33
+ # Histogram metrics
34
+ PROMPT_THROUGHPUT = auto()
35
+ TOKEN_THROUGHPUT = auto()
36
+ INPUT_TOKENS = auto()
37
+ TIME_TO_FIRST_TOKEN = auto()
38
+
39
+
40
+ # Global registry for all metrics by type
41
+ METRICS_REGISTRY: dict[MetricType, dict[Any, Metric]] = {
42
+ MetricType.HISTOGRAM: {},
43
+ MetricType.COUNTER: {},
44
+ MetricType.GAUGE: {},
45
+ }
46
+
47
+
48
+ # Register default LLM metrics
49
+ METRICS_REGISTRY[MetricType.HISTOGRAM][LLMMetric.PROMPT_THROUGHPUT] = Metric(
50
+ name="prompt_throughput",
51
+ description="Tracks the response time of LLM calls in seconds",
52
+ unit="s",
53
+ type=MetricType.HISTOGRAM,
54
+ )
55
+ METRICS_REGISTRY[MetricType.HISTOGRAM][LLMMetric.TOKEN_THROUGHPUT] = Metric(
56
+ name="token_throughput",
57
+ description="Tracks tokens generated per second",
58
+ unit="tokens/s",
59
+ type=MetricType.HISTOGRAM,
60
+ )
61
+ METRICS_REGISTRY[MetricType.HISTOGRAM][LLMMetric.INPUT_TOKENS] = Metric(
62
+ name="input_tokens",
63
+ description="Tracks the number of input tokens per request",
64
+ unit="tokens",
65
+ type=MetricType.HISTOGRAM,
66
+ )
67
+ METRICS_REGISTRY[MetricType.HISTOGRAM][LLMMetric.TIME_TO_FIRST_TOKEN] = Metric(
68
+ name="time_to_first_token",
69
+ description="Tracks the time to first token in seconds",
70
+ unit="s",
71
+ type=MetricType.HISTOGRAM,
72
+ )
73
+
74
+
75
+ def register_metric(key: str | Enum, metric: Metric) -> None:
76
+ """
77
+ Register a new metric in the global registry by type.
78
+
79
+ Args:
80
+ key: The metric key (enum value or string)
81
+ metric: The metric configuration
82
+ """
83
+ METRICS_REGISTRY[metric.type][key] = metric
84
+
85
+
86
+ def get_metric(key: str | Enum, metric_type: MetricType) -> Metric | None:
87
+ """
88
+ Get a metric from the registry by key and type.
89
+
90
+ Args:
91
+ key: The metric key (enum value or string)
92
+ metric_type: The type of metric to retrieve
93
+
94
+ Returns:
95
+ The metric configuration if found, None otherwise
96
+ """
97
+ return METRICS_REGISTRY[metric_type].get(key)
98
+
99
+
100
+ class MetricHandler(ABC):
101
+ """
102
+ Base class for all metric handlers.
103
+ """
104
+
105
+ def __init__(self, metric_prefix: str = "ragbits") -> None:
106
+ """
107
+ Initialize the MetricHandler instance.
108
+
109
+ Args:
110
+ metric_prefix: Prefix for all metric names.
111
+ """
112
+ super().__init__()
113
+ self._metric_prefix = metric_prefix
114
+ self._metrics: dict[str, Any] = {}
115
+
116
+ @abstractmethod
117
+ def create_metric(
118
+ self, name: str, unit: str = "", description: str = "", metric_type: MetricType = MetricType.HISTOGRAM
119
+ ) -> Any: # noqa: ANN401
120
+ """
121
+ Create a metric of the given type.
122
+
123
+ Args:
124
+ name: The metric name.
125
+ unit: The metric unit.
126
+ description: The metric description.
127
+ metric_type: The type of the metric (histogram, counter, gauge).
128
+
129
+ Returns:
130
+ The initialized metric.
131
+ """
132
+
133
+ @abstractmethod
134
+ def _record(self, metric: Any, value: int | float, attributes: dict | None = None) -> None: # noqa: ANN401
135
+ """
136
+ Low-level method to record a value for a specified metric.
137
+ This method should not be called directly, use record_metric instead.
138
+
139
+ Args:
140
+ metric: The metric to record.
141
+ value: The value to record for the metric.
142
+ attributes: Additional metadata for the metric.
143
+ """
144
+
145
+ def register_metric_instance(
146
+ self, name: str, unit: str = "", description: str = "", metric_type: MetricType = MetricType.HISTOGRAM
147
+ ) -> None:
148
+ """
149
+ Register a metric instance.
150
+
151
+ Args:
152
+ name: The metric name.
153
+ unit: The metric unit.
154
+ description: The metric description.
155
+ metric_type: The type of the metric (histogram, counter, gauge).
156
+ """
157
+ self._metrics[name] = self.create_metric(
158
+ name=f"{self._metric_prefix}_{name}",
159
+ unit=unit,
160
+ description=description,
161
+ metric_type=metric_type,
162
+ )
163
+
164
+ def record_metric(
165
+ self,
166
+ metric_key: str | Enum,
167
+ value: int | float,
168
+ attributes: dict | None = None,
169
+ metric_type: MetricType = MetricType.HISTOGRAM,
170
+ ) -> None:
171
+ """
172
+ Record the value for a specified metric.
173
+
174
+ Args:
175
+ metric_key: The metric key (name or enum value) to record.
176
+ value: The value to record for the metric.
177
+ attributes: Additional metadata for the metric.
178
+ metric_type: The type of the metric (histogram, counter, gauge).
179
+ """
180
+ metric_cfg = get_metric(metric_key, metric_type)
181
+ if metric_cfg:
182
+ metric_name = metric_cfg.name
183
+ if metric_name not in self._metrics:
184
+ self.register_metric_instance(
185
+ name=metric_name,
186
+ unit=metric_cfg.unit,
187
+ description=metric_cfg.description,
188
+ metric_type=metric_type,
189
+ )
190
+ else:
191
+ metric_name = str(metric_key)
192
+ if metric_name not in self._metrics:
193
+ self.register_metric_instance(metric_name, metric_type=metric_type)
194
+ self._record(
195
+ metric=self._metrics[metric_name],
196
+ value=value,
197
+ attributes=attributes,
198
+ )
@@ -0,0 +1,19 @@
1
+ from typing import Any
2
+
3
+ import logfire
4
+
5
+ from ragbits.core.audit.metrics.otel import OtelMetricHandler
6
+
7
+
8
+ class LogfireMetricHandler(OtelMetricHandler):
9
+ """
10
+ Logfire metric handler.
11
+ """
12
+
13
+ def __init__(self, metric_prefix: str = "ragbits", *args: Any, **kwargs: Any) -> None: # noqa: ANN401
14
+ """
15
+ Initialize the LogfireMetricHandler instance.
16
+ """
17
+ logfire.configure(*args, **kwargs)
18
+ logfire.instrument_system_metrics()
19
+ super().__init__(metric_prefix=metric_prefix)
@@ -0,0 +1,65 @@
1
+ from typing import Any
2
+
3
+ from opentelemetry.metrics import MeterProvider, get_meter
4
+
5
+ from ragbits.core.audit.metrics.base import MetricHandler, MetricType
6
+
7
+
8
+ class OtelMetricHandler(MetricHandler):
9
+ """
10
+ OpenTelemetry metric handler.
11
+ """
12
+
13
+ def __init__(self, provider: MeterProvider | None = None, metric_prefix: str = "ragbits") -> None:
14
+ """
15
+ Initialize the OtelMetricHandler instance.
16
+
17
+ Args:
18
+ provider: The meter provider to use.
19
+ metric_prefix: Prefix for all metric names.
20
+ """
21
+ super().__init__(metric_prefix=metric_prefix)
22
+ self._meter = get_meter(name=__name__, meter_provider=provider)
23
+
24
+ def create_metric(
25
+ self, name: str, unit: str = "", description: str = "", metric_type: MetricType = MetricType.HISTOGRAM
26
+ ) -> Any: # noqa: ANN401
27
+ """
28
+ Create a metric of the specified type.
29
+
30
+ Args:
31
+ name: The metric name.
32
+ unit: The metric unit.
33
+ description: The metric description.
34
+ metric_type: The type of the metric (histogram, counter, gauge).
35
+
36
+ Returns:
37
+ The initialized metric.
38
+ """
39
+ if metric_type == MetricType.HISTOGRAM:
40
+ return self._meter.create_histogram(name=name, unit=unit, description=description)
41
+ elif metric_type == MetricType.COUNTER:
42
+ return self._meter.create_counter(name=name, unit=unit, description=description)
43
+ elif metric_type == MetricType.GAUGE:
44
+ return self._meter.create_gauge(name=name, unit=unit, description=description)
45
+ else:
46
+ raise ValueError(f"Unsupported metric type: {metric_type}")
47
+
48
+ def _record(self, metric: Any, value: int | float, attributes: dict | None = None) -> None: # noqa
49
+ """
50
+ Record the value for a specified metric.
51
+
52
+ Args:
53
+ metric: The metric to record (histogram, counter, or gauge).
54
+ value: The value to record for the metric.
55
+ attributes: Additional metadata for the metric.
56
+ """
57
+ # Determine metric type by instance
58
+ if hasattr(metric, "record"):
59
+ # Histogram or Gauge (OpenTelemetry Python API)
60
+ metric.record(value, attributes=attributes)
61
+ elif hasattr(metric, "add"):
62
+ # Counter
63
+ metric.add(value, attributes=attributes)
64
+ else:
65
+ raise TypeError("Unsupported metric instance for recording")