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.
Files changed (70) hide show
  1. levelapp/__init__.py +0 -0
  2. levelapp/aspects/__init__.py +8 -0
  3. levelapp/aspects/loader.py +253 -0
  4. levelapp/aspects/logger.py +59 -0
  5. levelapp/aspects/monitor.py +617 -0
  6. levelapp/aspects/sanitizer.py +168 -0
  7. levelapp/clients/__init__.py +122 -0
  8. levelapp/clients/anthropic.py +112 -0
  9. levelapp/clients/gemini.py +130 -0
  10. levelapp/clients/groq.py +101 -0
  11. levelapp/clients/huggingface.py +162 -0
  12. levelapp/clients/ionos.py +126 -0
  13. levelapp/clients/mistral.py +106 -0
  14. levelapp/clients/openai.py +116 -0
  15. levelapp/comparator/__init__.py +5 -0
  16. levelapp/comparator/comparator.py +232 -0
  17. levelapp/comparator/extractor.py +108 -0
  18. levelapp/comparator/schemas.py +61 -0
  19. levelapp/comparator/scorer.py +269 -0
  20. levelapp/comparator/utils.py +136 -0
  21. levelapp/config/__init__.py +5 -0
  22. levelapp/config/endpoint.py +199 -0
  23. levelapp/config/prompts.py +57 -0
  24. levelapp/core/__init__.py +0 -0
  25. levelapp/core/base.py +386 -0
  26. levelapp/core/schemas.py +24 -0
  27. levelapp/core/session.py +336 -0
  28. levelapp/endpoint/__init__.py +0 -0
  29. levelapp/endpoint/client.py +188 -0
  30. levelapp/endpoint/client_test.py +41 -0
  31. levelapp/endpoint/manager.py +114 -0
  32. levelapp/endpoint/parsers.py +119 -0
  33. levelapp/endpoint/schemas.py +38 -0
  34. levelapp/endpoint/tester.py +52 -0
  35. levelapp/evaluator/__init__.py +3 -0
  36. levelapp/evaluator/evaluator.py +307 -0
  37. levelapp/metrics/__init__.py +63 -0
  38. levelapp/metrics/embedding.py +56 -0
  39. levelapp/metrics/embeddings/__init__.py +0 -0
  40. levelapp/metrics/embeddings/sentence_transformer.py +30 -0
  41. levelapp/metrics/embeddings/torch_based.py +56 -0
  42. levelapp/metrics/exact.py +182 -0
  43. levelapp/metrics/fuzzy.py +80 -0
  44. levelapp/metrics/token.py +103 -0
  45. levelapp/plugins/__init__.py +0 -0
  46. levelapp/repository/__init__.py +3 -0
  47. levelapp/repository/filesystem.py +203 -0
  48. levelapp/repository/firestore.py +291 -0
  49. levelapp/simulator/__init__.py +3 -0
  50. levelapp/simulator/schemas.py +116 -0
  51. levelapp/simulator/simulator.py +531 -0
  52. levelapp/simulator/utils.py +134 -0
  53. levelapp/visualization/__init__.py +7 -0
  54. levelapp/visualization/charts.py +358 -0
  55. levelapp/visualization/dashboard.py +240 -0
  56. levelapp/visualization/exporter.py +167 -0
  57. levelapp/visualization/templates/base.html +158 -0
  58. levelapp/visualization/templates/comparator_dashboard.html +57 -0
  59. levelapp/visualization/templates/simulator_dashboard.html +111 -0
  60. levelapp/workflow/__init__.py +6 -0
  61. levelapp/workflow/base.py +192 -0
  62. levelapp/workflow/config.py +96 -0
  63. levelapp/workflow/context.py +64 -0
  64. levelapp/workflow/factory.py +42 -0
  65. levelapp/workflow/registration.py +6 -0
  66. levelapp/workflow/runtime.py +19 -0
  67. levelapp-0.1.15.dist-info/METADATA +571 -0
  68. levelapp-0.1.15.dist-info/RECORD +70 -0
  69. levelapp-0.1.15.dist-info/WHEEL +4 -0
  70. 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
@@ -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"