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.
- ragbits/core/__init__.py +21 -2
- ragbits/core/audit/__init__.py +15 -157
- ragbits/core/audit/metrics/__init__.py +83 -0
- ragbits/core/audit/metrics/base.py +198 -0
- ragbits/core/audit/metrics/logfire.py +19 -0
- ragbits/core/audit/metrics/otel.py +65 -0
- ragbits/core/audit/traces/__init__.py +171 -0
- ragbits/core/audit/{base.py → traces/base.py} +9 -5
- ragbits/core/audit/{cli.py → traces/cli.py} +8 -4
- ragbits/core/audit/traces/logfire.py +18 -0
- ragbits/core/audit/{otel.py → traces/otel.py} +5 -8
- ragbits/core/config.py +15 -0
- ragbits/core/embeddings/__init__.py +2 -1
- ragbits/core/embeddings/base.py +19 -0
- ragbits/core/embeddings/dense/base.py +10 -1
- ragbits/core/embeddings/dense/fastembed.py +22 -1
- ragbits/core/embeddings/dense/litellm.py +37 -10
- ragbits/core/embeddings/dense/local.py +15 -1
- ragbits/core/embeddings/dense/noop.py +11 -1
- ragbits/core/embeddings/dense/vertex_multimodal.py +14 -1
- ragbits/core/embeddings/sparse/bag_of_tokens.py +47 -17
- ragbits/core/embeddings/sparse/base.py +10 -1
- ragbits/core/embeddings/sparse/fastembed.py +25 -2
- ragbits/core/llms/__init__.py +3 -3
- ragbits/core/llms/base.py +612 -88
- ragbits/core/llms/exceptions.py +27 -0
- ragbits/core/llms/litellm.py +408 -83
- ragbits/core/llms/local.py +180 -41
- ragbits/core/llms/mock.py +88 -23
- ragbits/core/prompt/__init__.py +2 -2
- ragbits/core/prompt/_cli.py +32 -19
- ragbits/core/prompt/base.py +105 -19
- ragbits/core/prompt/{discovery/prompt_discovery.py → discovery.py} +1 -1
- ragbits/core/prompt/exceptions.py +22 -6
- ragbits/core/prompt/prompt.py +180 -98
- ragbits/core/sources/__init__.py +2 -0
- ragbits/core/sources/azure.py +1 -1
- ragbits/core/sources/base.py +8 -1
- ragbits/core/sources/gcs.py +1 -1
- ragbits/core/sources/git.py +1 -1
- ragbits/core/sources/google_drive.py +595 -0
- ragbits/core/sources/hf.py +71 -31
- ragbits/core/sources/local.py +1 -1
- ragbits/core/sources/s3.py +1 -1
- ragbits/core/utils/config_handling.py +13 -2
- ragbits/core/utils/function_schema.py +220 -0
- ragbits/core/utils/helpers.py +22 -0
- ragbits/core/utils/lazy_litellm.py +44 -0
- ragbits/core/vector_stores/base.py +18 -1
- ragbits/core/vector_stores/chroma.py +28 -11
- ragbits/core/vector_stores/hybrid.py +1 -1
- ragbits/core/vector_stores/hybrid_strategies.py +21 -8
- ragbits/core/vector_stores/in_memory.py +13 -4
- ragbits/core/vector_stores/pgvector.py +123 -47
- ragbits/core/vector_stores/qdrant.py +15 -7
- ragbits/core/vector_stores/weaviate.py +440 -0
- {ragbits_core-0.16.0.dist-info → ragbits_core-1.4.0.dev202512021005.dist-info}/METADATA +22 -6
- ragbits_core-1.4.0.dev202512021005.dist-info/RECORD +79 -0
- {ragbits_core-0.16.0.dist-info → ragbits_core-1.4.0.dev202512021005.dist-info}/WHEEL +1 -1
- ragbits/core/prompt/discovery/__init__.py +0 -3
- ragbits/core/prompt/lab/__init__.py +0 -0
- ragbits/core/prompt/lab/app.py +0 -262
- 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
|
|
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
|
-
|
|
26
|
+
set_trace_handlers("cli")
|
|
27
|
+
|
|
28
|
+
_config_future = _config_executor.submit(_import_and_run_config)
|
ragbits/core/audit/__init__.py
CHANGED
|
@@ -1,157 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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")
|