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/core/base.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""levelapp/core/base.py"""
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
from typing import List, Dict, Any, Callable, TypeVar, Type
|
|
12
|
+
|
|
13
|
+
from levelapp.aspects import JSONSanitizer, logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Model = TypeVar("Model", bound=BaseModel)
|
|
17
|
+
Context = TypeVar("Context")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseProcess(ABC):
|
|
21
|
+
"""Interface for the evaluation classes."""
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def run(self, **kwargs) -> Any:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseEvaluator(ABC):
|
|
28
|
+
"""Abstract base class for evaluator components."""
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def evaluate(
|
|
31
|
+
self,
|
|
32
|
+
generated_data: str | Dict[str, Any],
|
|
33
|
+
reference_data: str | Dict[str, Any],
|
|
34
|
+
**kwargs
|
|
35
|
+
):
|
|
36
|
+
"""Evaluate system output to reference output."""
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def async_evaluate(
|
|
41
|
+
self,
|
|
42
|
+
generated_data: str | Dict[str, Any],
|
|
43
|
+
reference_data: str | Dict[str, Any],
|
|
44
|
+
**kwargs
|
|
45
|
+
):
|
|
46
|
+
"""Asynchronous evaluation method."""
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BaseChatClient(ABC):
|
|
51
|
+
"""
|
|
52
|
+
Abstract base class for integrating different LLM provider clients.
|
|
53
|
+
|
|
54
|
+
This class defines the common interface and request lifecycle for
|
|
55
|
+
calling chat-based large language models (LLMs). It enforces
|
|
56
|
+
provider-specific implementations for:
|
|
57
|
+
- endpoint path resolution
|
|
58
|
+
- request headers
|
|
59
|
+
- request payload
|
|
60
|
+
- response parsing
|
|
61
|
+
|
|
62
|
+
Subclasses (e.g., `OpenAIClient`, `MistralClient`, `AnthropicClient`, `IonosClient`)
|
|
63
|
+
must override the abstract members to handle provider-specific request/response formats.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
Initialize the base chat client.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
**kwargs: Arbitrary keyword arguments. Expected keys include:
|
|
72
|
+
- base_url (str): The base API URL for the LLM provider.
|
|
73
|
+
"""
|
|
74
|
+
self.base_url = kwargs.get("base_url")
|
|
75
|
+
self.sanitizer = JSONSanitizer()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def endpoint_path(self) -> str:
|
|
80
|
+
"""
|
|
81
|
+
API path (relative to `base_url`) for the provider’s chat endpoint.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
- OpenAI: "/v1/chat/completions"
|
|
85
|
+
- Mistral: "/chat/completions"
|
|
86
|
+
- Anthropic: "/v1/messages"
|
|
87
|
+
- IONOS: "/models/model-id/predictions"
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
str: Provider-specific endpoint path.
|
|
91
|
+
"""
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
|
|
94
|
+
def _build_endpoint(self) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Construct the full request endpoint URL.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
str: Complete endpoint URL (base_url + endpoint_path).
|
|
100
|
+
"""
|
|
101
|
+
return f"{self.base_url}/{self.endpoint_path.lstrip('/')}"
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def _build_headers(self) -> Dict[str, str]:
|
|
105
|
+
"""
|
|
106
|
+
Construct HTTP request headers for the provider.
|
|
107
|
+
|
|
108
|
+
This typically includes authentication (e.g., API key or Bearer token),
|
|
109
|
+
content type, and provider-specific headers.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dict[str, str]: HTTP headers.
|
|
113
|
+
"""
|
|
114
|
+
raise NotImplementedError
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def _build_payload(self, message: str) -> Dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Construct the request body payload for the provider.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
message (str): User message to send to the LLM.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dict[str, Any]: JSON-serializable payload as required by the provider API.
|
|
126
|
+
"""
|
|
127
|
+
raise NotImplementedError
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def parse_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
|
131
|
+
"""
|
|
132
|
+
Parse the raw provider response into a normalized format.
|
|
133
|
+
|
|
134
|
+
The normalized format should include:
|
|
135
|
+
- "output": str or structured response content
|
|
136
|
+
- "metadata": Dict containing tokens, cost, or other provider stats
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
response (Dict[str, Any]): Raw JSON response returned by the provider.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dict[str, Any]: Normalized output structure.
|
|
143
|
+
"""
|
|
144
|
+
raise NotImplementedError
|
|
145
|
+
|
|
146
|
+
def call(self, message: str) -> Dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Make a synchronous call to the provider API.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
message (str): User input message to send.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Dict[str, Any]: Provider's raw JSON response.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
requests.exceptions.RequestException: On any network or HTTP error.
|
|
158
|
+
"""
|
|
159
|
+
url = self._build_endpoint()
|
|
160
|
+
headers = self._build_headers()
|
|
161
|
+
payload = self._build_payload(message)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
|
165
|
+
response.raise_for_status()
|
|
166
|
+
return response.json()
|
|
167
|
+
|
|
168
|
+
except requests.exceptions.HTTPError as http_err:
|
|
169
|
+
logger.error(f"[BaseChatClient] HTTP error occurred: {http_err}")
|
|
170
|
+
raise
|
|
171
|
+
except requests.exceptions.ConnectionError as conn_err:
|
|
172
|
+
logger.error(f"[BaseChatClient] Connection error occurred: {conn_err}")
|
|
173
|
+
raise
|
|
174
|
+
except requests.exceptions.Timeout as timeout_err:
|
|
175
|
+
logger.error(f"[BaseChatClient] Timeout error occurred: {timeout_err}")
|
|
176
|
+
raise
|
|
177
|
+
except requests.exceptions.RequestException as req_err:
|
|
178
|
+
logger.error(f"[BaseChatClient] An unexpected error occurred: {req_err}")
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
async def acall(self, message: str) -> Dict[str, Any]:
|
|
182
|
+
"""
|
|
183
|
+
Make an asynchronous call to the provider API.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
message (str): User input message to send.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Dict[str, Any]: Provider's raw JSON response.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
httpx.RequestError, httpx.TimeoutException, httpx.HTTPStatusError:
|
|
193
|
+
On any network or HTTP error.
|
|
194
|
+
"""
|
|
195
|
+
url = self._build_endpoint()
|
|
196
|
+
headers = self._build_headers()
|
|
197
|
+
payload = self._build_payload(message)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
async with httpx.AsyncClient(timeout=300) as client:
|
|
201
|
+
response = await client.post(url, headers=headers, json=payload)
|
|
202
|
+
response.raise_for_status()
|
|
203
|
+
return response.json()
|
|
204
|
+
|
|
205
|
+
except httpx.HTTPStatusError as http_err:
|
|
206
|
+
logger.error(f"[BaseChatClient.acall] HTTP error: {http_err}")
|
|
207
|
+
raise
|
|
208
|
+
except httpx.RequestError as req_err:
|
|
209
|
+
logger.error(f"[BaseChatClient.acall] Request error: {req_err}")
|
|
210
|
+
raise
|
|
211
|
+
except httpx.TimeoutException as timeout_err:
|
|
212
|
+
logger.error(f"[BaseChatClient.acall] Timeout: {timeout_err}")
|
|
213
|
+
raise
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"[BaseChatClient.acall] Unexpected error: {e}")
|
|
216
|
+
raise
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class BaseMetric(ABC):
|
|
220
|
+
"""Abstract base class for metrics collection."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, processor: Callable | None = None, score_cutoff: float | None = None):
|
|
223
|
+
"""
|
|
224
|
+
Initialize the metric.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
processor (Optional[Callable]): Optional function to preprocess strings before comparison.
|
|
228
|
+
score_cutoff (Optional[float]): Minimum similarity score for an early match cutoff.
|
|
229
|
+
"""
|
|
230
|
+
self.processor = processor
|
|
231
|
+
self.score_cutoff = score_cutoff
|
|
232
|
+
|
|
233
|
+
@abstractmethod
|
|
234
|
+
def compute(self, generated: str, reference: str) -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Evaluate the generated text against the reference text.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
generated (str): The generated text to evaluate.
|
|
240
|
+
reference (str): The reference text to compare against.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Dict[str, Any]: Evaluation results including match level and justification.
|
|
244
|
+
"""
|
|
245
|
+
raise NotImplementedError
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def name(self) -> str:
|
|
249
|
+
"""
|
|
250
|
+
Get the name of the metric.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
str: Name of the metric.
|
|
254
|
+
"""
|
|
255
|
+
return self.__class__.__name__.lower()
|
|
256
|
+
|
|
257
|
+
# TODO-0: You know what..We can remove this at some point.
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _validate_inputs(generated: str, reference: str) -> None:
|
|
260
|
+
"""Validate that both inputs are strings."""
|
|
261
|
+
if not (isinstance(generated, str) and isinstance(reference, str)):
|
|
262
|
+
raise TypeError("Both 'generated' and 'reference' must be strings.")
|
|
263
|
+
|
|
264
|
+
def _get_params(self) -> Dict[str, Any]:
|
|
265
|
+
"""Return a serializable dictionary of metric parameters."""
|
|
266
|
+
return {
|
|
267
|
+
"processor": repr(self.processor) if self.processor else None,
|
|
268
|
+
"score_cutoff": self.score_cutoff
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def _build_metadata(self, **extra_inputs) -> Dict[str, Any]:
|
|
272
|
+
"""Construct a consistent metadata dictionary."""
|
|
273
|
+
return {
|
|
274
|
+
"type": self.__class__.__name__,
|
|
275
|
+
"params": self._get_params(),
|
|
276
|
+
"inputs": extra_inputs,
|
|
277
|
+
"timestamp": datetime.datetime.now()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class BaseRepository(ABC):
|
|
282
|
+
"""
|
|
283
|
+
Abstract base class for pluggable NoSQL data stores.
|
|
284
|
+
Supports document-based operations with Pydantic model parsing.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
@abstractmethod
|
|
288
|
+
def connect(self) -> None:
|
|
289
|
+
"""Initialize connection or client."""
|
|
290
|
+
raise NotImplementedError
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def close(self) -> None:
|
|
294
|
+
"""Close connection or client."""
|
|
295
|
+
raise NotImplementedError
|
|
296
|
+
|
|
297
|
+
@abstractmethod
|
|
298
|
+
def retrieve_document(
|
|
299
|
+
self,
|
|
300
|
+
collection_id: str,
|
|
301
|
+
section_id: str,
|
|
302
|
+
sub_collection_id: str,
|
|
303
|
+
document_id: str,
|
|
304
|
+
model_type: Type[Model]
|
|
305
|
+
) -> Model | None:
|
|
306
|
+
"""
|
|
307
|
+
Retrieve and parse a document from the datastore based on its type.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
collection_id (str): Collection reference.
|
|
311
|
+
section_id (str): Section reference.
|
|
312
|
+
sub_collection_id (str): Sub-collection reference.
|
|
313
|
+
document_id (str): Reference of the document to retrieve.
|
|
314
|
+
model_type (Type[BaseModel]): Pydantic class to instantiate.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Parsed model instance or None if document was not found.
|
|
318
|
+
"""
|
|
319
|
+
raise NotImplementedError
|
|
320
|
+
|
|
321
|
+
@abstractmethod
|
|
322
|
+
def store_document(
|
|
323
|
+
self,
|
|
324
|
+
collection_id: str,
|
|
325
|
+
section_id: str,
|
|
326
|
+
sub_collection_id: str,
|
|
327
|
+
document_id: str,
|
|
328
|
+
data: Model
|
|
329
|
+
) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Store a pydantic model instance as a document.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
collection_id (str): Collection reference.
|
|
335
|
+
section_id (str): Section reference.
|
|
336
|
+
sub_collection_id (str): Sub-collection reference.
|
|
337
|
+
document_id (str): Reference of the document to store.
|
|
338
|
+
data (Model): Pydantic model instance.
|
|
339
|
+
"""
|
|
340
|
+
raise NotImplementedError
|
|
341
|
+
|
|
342
|
+
@abstractmethod
|
|
343
|
+
def query_collection(
|
|
344
|
+
self,
|
|
345
|
+
collection_id: str,
|
|
346
|
+
section_id: str,
|
|
347
|
+
sub_collection_id: str,
|
|
348
|
+
filters: Dict[str, Any],
|
|
349
|
+
model_type: Type[Model]
|
|
350
|
+
) -> List[Model]:
|
|
351
|
+
"""
|
|
352
|
+
Query documents in a collection with optional filters.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
collection_id (str): Collection reference.
|
|
356
|
+
section_id (str): Section reference.
|
|
357
|
+
sub_collection_id (str): Sub-collection reference.
|
|
358
|
+
filters (Dict[str, Any]): Filters to apply to the query (implementation dependent).
|
|
359
|
+
model_type (Type[BaseModel]): Pydantic class to instantiate.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
List[Model]: Query results.
|
|
363
|
+
"""
|
|
364
|
+
raise NotImplementedError
|
|
365
|
+
|
|
366
|
+
@abstractmethod
|
|
367
|
+
def delete_document(
|
|
368
|
+
self,
|
|
369
|
+
collection_id: str,
|
|
370
|
+
section_id: str,
|
|
371
|
+
sub_collection_id: str,
|
|
372
|
+
document_id: str
|
|
373
|
+
) -> bool:
|
|
374
|
+
"""
|
|
375
|
+
Delete a document.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
collection_id (str): Collection reference.
|
|
379
|
+
section_id (str): Section reference.
|
|
380
|
+
sub_collection_id (str): Sub-collection reference.
|
|
381
|
+
document_id (str): Reference of the document to delete.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
True if deleted, False if not.
|
|
385
|
+
"""
|
|
386
|
+
raise NotImplementedError
|
levelapp/core/schemas.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExtendedEnum(Enum):
|
|
5
|
+
@classmethod
|
|
6
|
+
def list(cls):
|
|
7
|
+
return [e.value for e in cls]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkflowType(ExtendedEnum):
|
|
11
|
+
SIMULATOR = "SIMULATOR"
|
|
12
|
+
COMPARATOR = "COMPARATOR"
|
|
13
|
+
ASSESSOR = "ASSESSOR"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RepositoryType(ExtendedEnum):
|
|
17
|
+
FIRESTORE = "FIRESTORE"
|
|
18
|
+
FILESYSTEM = "FILESYSTEM"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EvaluatorType(ExtendedEnum):
|
|
22
|
+
JUDGE = "JUDGE"
|
|
23
|
+
REFERENCE = "REFERENCE"
|
|
24
|
+
RAG = "RAG"
|