levelapp 0.1.15__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.
- levelapp/__init__.py +0 -0
- levelapp/aspects/__init__.py +8 -0
- levelapp/aspects/loader.py +253 -0
- levelapp/aspects/logger.py +59 -0
- levelapp/aspects/monitor.py +617 -0
- levelapp/aspects/sanitizer.py +168 -0
- levelapp/clients/__init__.py +122 -0
- levelapp/clients/anthropic.py +112 -0
- levelapp/clients/gemini.py +130 -0
- levelapp/clients/groq.py +101 -0
- levelapp/clients/huggingface.py +162 -0
- levelapp/clients/ionos.py +126 -0
- levelapp/clients/mistral.py +106 -0
- levelapp/clients/openai.py +116 -0
- levelapp/comparator/__init__.py +5 -0
- levelapp/comparator/comparator.py +232 -0
- levelapp/comparator/extractor.py +108 -0
- levelapp/comparator/schemas.py +61 -0
- levelapp/comparator/scorer.py +269 -0
- levelapp/comparator/utils.py +136 -0
- levelapp/config/__init__.py +5 -0
- levelapp/config/endpoint.py +199 -0
- levelapp/config/prompts.py +57 -0
- levelapp/core/__init__.py +0 -0
- levelapp/core/base.py +386 -0
- levelapp/core/schemas.py +24 -0
- levelapp/core/session.py +336 -0
- levelapp/endpoint/__init__.py +0 -0
- levelapp/endpoint/client.py +188 -0
- levelapp/endpoint/client_test.py +41 -0
- levelapp/endpoint/manager.py +114 -0
- levelapp/endpoint/parsers.py +119 -0
- levelapp/endpoint/schemas.py +38 -0
- levelapp/endpoint/tester.py +52 -0
- levelapp/evaluator/__init__.py +3 -0
- levelapp/evaluator/evaluator.py +307 -0
- levelapp/metrics/__init__.py +63 -0
- levelapp/metrics/embedding.py +56 -0
- levelapp/metrics/embeddings/__init__.py +0 -0
- levelapp/metrics/embeddings/sentence_transformer.py +30 -0
- levelapp/metrics/embeddings/torch_based.py +56 -0
- levelapp/metrics/exact.py +182 -0
- levelapp/metrics/fuzzy.py +80 -0
- levelapp/metrics/token.py +103 -0
- levelapp/plugins/__init__.py +0 -0
- levelapp/repository/__init__.py +3 -0
- levelapp/repository/filesystem.py +203 -0
- levelapp/repository/firestore.py +291 -0
- levelapp/simulator/__init__.py +3 -0
- levelapp/simulator/schemas.py +116 -0
- levelapp/simulator/simulator.py +531 -0
- levelapp/simulator/utils.py +134 -0
- levelapp/visualization/__init__.py +7 -0
- levelapp/visualization/charts.py +358 -0
- levelapp/visualization/dashboard.py +240 -0
- levelapp/visualization/exporter.py +167 -0
- levelapp/visualization/templates/base.html +158 -0
- levelapp/visualization/templates/comparator_dashboard.html +57 -0
- levelapp/visualization/templates/simulator_dashboard.html +111 -0
- levelapp/workflow/__init__.py +6 -0
- levelapp/workflow/base.py +192 -0
- levelapp/workflow/config.py +96 -0
- levelapp/workflow/context.py +64 -0
- levelapp/workflow/factory.py +42 -0
- levelapp/workflow/registration.py +6 -0
- levelapp/workflow/runtime.py +19 -0
- levelapp-0.1.15.dist-info/METADATA +571 -0
- levelapp-0.1.15.dist-info/RECORD +70 -0
- levelapp-0.1.15.dist-info/WHEEL +4 -0
- levelapp-0.1.15.dist-info/licenses/LICENSE +0 -0
levelapp/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .logger import logger
|
|
2
|
+
from .loader import DataLoader
|
|
3
|
+
from .sanitizer import JSONSanitizer
|
|
4
|
+
from .monitor import MonitoringAspect, FunctionMonitor, MetricType, ExecutionMetrics
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = ['logger', 'DataLoader', 'JSONSanitizer', 'MonitoringAspect',
|
|
8
|
+
'FunctionMonitor', 'MetricType', 'ExecutionMetrics']
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""levelapp/aspects/loader.py"""
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from typing import Any, Type, TypeVar, List, Optional, Dict, Tuple
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
from pydantic import BaseModel, create_model, ValidationError
|
|
12
|
+
|
|
13
|
+
from rapidfuzz import utils
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
Model = TypeVar("Model", bound=BaseModel)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DynamicModelBuilder:
|
|
21
|
+
"""
|
|
22
|
+
A utility for creating dynamic Pydantic models at runtime from arbitrary Python
|
|
23
|
+
data structures (dicts, lists, primitives).
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
---------
|
|
27
|
+
- **Dynamic Model Generation**: Builds `pydantic.BaseModel` subclasses on the fly
|
|
28
|
+
based on the structure of the input data.
|
|
29
|
+
- **Recursive Nesting**: Handles arbitrarily nested dictionaries and lists by
|
|
30
|
+
generating nested models automatically. (Don't freak out, it's for shallow traversal).
|
|
31
|
+
- **Field Name Sanitization**: Ensures generated field names are valid Python identifiers.
|
|
32
|
+
- **Caching**: Maintains a cache of previously generated models to avoid redundant
|
|
33
|
+
re-construction and improve performance.
|
|
34
|
+
|
|
35
|
+
Use Cases:
|
|
36
|
+
----------
|
|
37
|
+
- Converting arbitrary JSON/dict data into structured, type-safe Pydantic models.
|
|
38
|
+
- Prototyping with dynamic/unknown payloads where upfront schema definition is not feasible.
|
|
39
|
+
|
|
40
|
+
Notes:
|
|
41
|
+
------
|
|
42
|
+
- Field names are sanitized using `utils.default_process` (from rapidfuzz),
|
|
43
|
+
replacing spaces with underscores and handling invalid identifiers.
|
|
44
|
+
- Lists are typed based on their first element only; heterogeneous lists
|
|
45
|
+
may not be fully captured (Sorry?).
|
|
46
|
+
- Model caching is based on `(model_name, str(data) or str(sorted(keys)))`.
|
|
47
|
+
This improves performance but may cause collisions if `str(data)` is ambiguous.
|
|
48
|
+
"""
|
|
49
|
+
def __init__(self):
|
|
50
|
+
"""
|
|
51
|
+
Initialize a DynamicModelBuilder instance.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
-----------
|
|
55
|
+
model_cache : Dict[Tuple[str, str], Type[BaseModel]]
|
|
56
|
+
Cache of generated models keyed by (model_name, data_signature).
|
|
57
|
+
Ensures models are reused instead of rebuilt.
|
|
58
|
+
"""
|
|
59
|
+
self.model_cache: Dict[Tuple[str, str], Type[BaseModel]] = {}
|
|
60
|
+
|
|
61
|
+
def clear_cache(self):
|
|
62
|
+
"""
|
|
63
|
+
Clear the internal model cache.
|
|
64
|
+
|
|
65
|
+
Use when schema changes are expected or to free memory in long-running processes.
|
|
66
|
+
"""
|
|
67
|
+
self.model_cache.clear()
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _sanitize_field_name(name: str) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Normalize and sanitize field names into valid Python identifiers.
|
|
73
|
+
|
|
74
|
+
- Converts to lowercase and strips unwanted characters using `utils.default_process`.
|
|
75
|
+
- Replaces spaces with underscores.
|
|
76
|
+
- Ensures field names are not empty; substitutes `"field_default"` if so.
|
|
77
|
+
- Prepends `"field_"` if the name starts with a digit.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name (str): The original field name from input data.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str: A valid Python identifier for use in a Pydantic model.
|
|
84
|
+
"""
|
|
85
|
+
name = utils.default_process(name).replace(' ', '_')
|
|
86
|
+
if not name:
|
|
87
|
+
return "field_default"
|
|
88
|
+
if name[0].isdigit():
|
|
89
|
+
return f"field_{name}"
|
|
90
|
+
return name
|
|
91
|
+
|
|
92
|
+
def _get_field_type(self, value: Any, model_name: str, key: str) -> Tuple[Any, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Infer the field type and default value for a given data value.
|
|
95
|
+
|
|
96
|
+
Supported cases:
|
|
97
|
+
- **Mapping (dict-like)**: Creates a nested dynamic model recursively.
|
|
98
|
+
- **Sequence (list/tuple, non-string)**:
|
|
99
|
+
- Empty list → `List[BaseModel]`
|
|
100
|
+
- List of dicts → `List[<nested model>]`
|
|
101
|
+
- List of primitives → `List[primitive type]`
|
|
102
|
+
- **Primitive**: Wraps in `Optional` to allow nulls.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
value (Any): The raw field value to inspect.
|
|
106
|
+
model_name (str): Name of the parent model.
|
|
107
|
+
key (str): Field name (before sanitization).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple[Any, Any]: A `(type, default_value)` tuple suitable for `create_model`.
|
|
111
|
+
"""
|
|
112
|
+
if isinstance(value, Mapping):
|
|
113
|
+
nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value)
|
|
114
|
+
return Optional[nested_model], None
|
|
115
|
+
|
|
116
|
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
117
|
+
if not value:
|
|
118
|
+
return List[BaseModel], ...
|
|
119
|
+
|
|
120
|
+
elif isinstance(value[0], Mapping):
|
|
121
|
+
nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value[0])
|
|
122
|
+
return Optional[List[nested_model]], None
|
|
123
|
+
|
|
124
|
+
else:
|
|
125
|
+
field_type = type(value[0]) if value[0] is not None else Any
|
|
126
|
+
return Optional[List[field_type]], None
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
field_type = Optional[type(value)] if value is not None else Optional[Any]
|
|
130
|
+
return field_type, None
|
|
131
|
+
|
|
132
|
+
def create_dynamic_model(self, model_name: str, data: Any) -> Type[BaseModel]:
|
|
133
|
+
"""
|
|
134
|
+
Create a dynamic Pydantic model from arbitrary input data.
|
|
135
|
+
|
|
136
|
+
- Handles nested dictionaries and lists by recursively generating
|
|
137
|
+
sub-models.
|
|
138
|
+
- Uses caching to avoid rebuilding models for the same schema.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
model_name (str): Suggested name of the generated model.
|
|
142
|
+
data (Any): Input data (dict, list, or primitive).
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Type[BaseModel]: A dynamically created Pydantic model class.
|
|
146
|
+
"""
|
|
147
|
+
model_name = self._sanitize_field_name(name=model_name)
|
|
148
|
+
cache_key = (model_name, str(data) if not isinstance(data, dict) else str(sorted(data.keys())))
|
|
149
|
+
|
|
150
|
+
if cache_key in self.model_cache:
|
|
151
|
+
return self.model_cache[cache_key]
|
|
152
|
+
|
|
153
|
+
if isinstance(data, Mapping):
|
|
154
|
+
fields = {
|
|
155
|
+
self._sanitize_field_name(name=key): self._get_field_type(value=value, model_name=model_name, key=key)
|
|
156
|
+
for key, value in data.items()
|
|
157
|
+
}
|
|
158
|
+
model = create_model(model_name, **fields)
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
field_type = Optional[type(data)] if data else Optional[Any]
|
|
162
|
+
model = create_model(model_name, value=(field_type, None))
|
|
163
|
+
|
|
164
|
+
self.model_cache[cache_key] = model
|
|
165
|
+
|
|
166
|
+
return model
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class DataLoader:
|
|
170
|
+
"""Main utility tool for loading configuration and reference data"""
|
|
171
|
+
def __init__(self):
|
|
172
|
+
self.builder = DynamicModelBuilder()
|
|
173
|
+
self._name = self.__class__.__name__
|
|
174
|
+
load_dotenv()
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def load_raw_data(path: str | None = None):
|
|
178
|
+
"""
|
|
179
|
+
Load raw data from JSON or YAML files.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
path (str): path to the JSON or YAML file.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
A dictionary containing the raw data.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
FileNotFoundError: if the file was not found in the path.
|
|
189
|
+
YAMLError: if there was an error parsing the YAML file.
|
|
190
|
+
JSONDecoderError: if there was an error parsing the JSON file.
|
|
191
|
+
IOError: if the file is corrupt and cannot be read.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
if not path:
|
|
195
|
+
path = os.getenv('WORKFLOW_CONFIG_PATH', 'no-file')
|
|
196
|
+
|
|
197
|
+
if not os.path.exists(path):
|
|
198
|
+
raise FileNotFoundError(f"The provided configuration file path '{path}' does not exist.")
|
|
199
|
+
|
|
200
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
201
|
+
if path.endswith((".yaml", ".yml")):
|
|
202
|
+
content = yaml.safe_load(f)
|
|
203
|
+
|
|
204
|
+
elif path.endswith(".json"):
|
|
205
|
+
content = json.load(f)
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError("[WorkflowConfiguration] Unsupported file format.")
|
|
209
|
+
|
|
210
|
+
return content
|
|
211
|
+
|
|
212
|
+
except FileNotFoundError as e:
|
|
213
|
+
raise FileNotFoundError(f"[EndpointConfig] Payload template file '{e.filename}' not found in path.")
|
|
214
|
+
|
|
215
|
+
except yaml.YAMLError as e:
|
|
216
|
+
raise ValueError(f"[EndpointConfig] Error parsing YAML file:\n{e}")
|
|
217
|
+
|
|
218
|
+
except json.JSONDecodeError as e:
|
|
219
|
+
raise ValueError(f"[EndpointConfig] Error parsing JSON file:\n{e}")
|
|
220
|
+
|
|
221
|
+
except IOError as e:
|
|
222
|
+
raise IOError(f"[EndpointConfig] Error reading file:\n{e}")
|
|
223
|
+
|
|
224
|
+
def create_dynamic_model(
|
|
225
|
+
self,
|
|
226
|
+
data: Dict[str, Any],
|
|
227
|
+
model_name: str = "ExtractedData"
|
|
228
|
+
) -> BaseModel | None:
|
|
229
|
+
"""
|
|
230
|
+
Load data into a dynamically created Pydantic model instance.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
data (Dict[str, Any]): The data to load.
|
|
234
|
+
model_name (str, optional): The name of the model. Defaults to "ExtractedData".
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
An Pydantic model instance.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValidationError: If a validation error occurs.
|
|
241
|
+
Exception: If an unexpected error occurs.
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
self.builder.clear_cache()
|
|
245
|
+
dynamic_model = self.builder.create_dynamic_model(model_name=model_name, data=data)
|
|
246
|
+
model_instance = dynamic_model.model_validate(data)
|
|
247
|
+
return model_instance
|
|
248
|
+
|
|
249
|
+
except ValidationError as e:
|
|
250
|
+
logger.exception(f"[{self._name}] Validation Error: {e.errors()}")
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.error(f"[{self._name}] An error occurred: {e}")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""levelapp/aspects/logger.py"""
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Logger:
|
|
8
|
+
"""Centralized logger for Levelapp (AOP-style)."""
|
|
9
|
+
|
|
10
|
+
_instance: Optional[logging.Logger] = None
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def get_logger(
|
|
14
|
+
cls,
|
|
15
|
+
name: str = "levelapp",
|
|
16
|
+
level: int = logging.INFO,
|
|
17
|
+
log_to_file: Optional[str] = None,
|
|
18
|
+
) -> logging.Logger:
|
|
19
|
+
"""
|
|
20
|
+
Get a configured logger instance (singleton).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
name (str): Logger name.
|
|
24
|
+
level (int): Logging level (default=INFO).
|
|
25
|
+
log_to_file (Optional[str]): File path for logging (default=None).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
logging.Logger: Configured logger instance.
|
|
29
|
+
"""
|
|
30
|
+
if cls._instance:
|
|
31
|
+
return cls._instance
|
|
32
|
+
|
|
33
|
+
logger_ = logging.getLogger(name)
|
|
34
|
+
logger_.setLevel(level)
|
|
35
|
+
logger_.propagate = False # prevent double logging
|
|
36
|
+
|
|
37
|
+
# Formatter with contextual info
|
|
38
|
+
formatter = logging.Formatter(
|
|
39
|
+
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
|
40
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Console handler
|
|
44
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
45
|
+
console_handler.setFormatter(formatter)
|
|
46
|
+
logger_.addHandler(console_handler)
|
|
47
|
+
|
|
48
|
+
# Optional file handler
|
|
49
|
+
if log_to_file:
|
|
50
|
+
file_handler = logging.FileHandler(log_to_file)
|
|
51
|
+
file_handler.setFormatter(formatter)
|
|
52
|
+
logger_.addHandler(file_handler)
|
|
53
|
+
|
|
54
|
+
cls._instance = logger_
|
|
55
|
+
return logger_
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Global access
|
|
59
|
+
logger = Logger.get_logger()
|