levelapp 0.1.4__py3-none-any.whl → 0.1.5__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.
Potentially problematic release.
This version of levelapp might be problematic. Click here for more details.
- levelapp/aspects/loader.py +4 -4
- levelapp/config/api_config.yaml +156 -0
- levelapp/config/dashq_api.yaml +94 -0
- levelapp/config/endpoint_.py +325 -5
- levelapp/config/endpoints.yaml +47 -0
- levelapp/core/session.py +8 -0
- levelapp/endpoint/__init__.py +0 -0
- levelapp/endpoint/client.py +102 -0
- levelapp/endpoint/manager.py +114 -0
- levelapp/endpoint/parsers.py +120 -0
- levelapp/endpoint/schemas.py +38 -0
- levelapp/endpoint/tester.py +53 -0
- levelapp/endpoint/usage_example.py +39 -0
- levelapp/evaluator/evaluator.py +9 -1
- levelapp/repository/filesystem.py +203 -0
- levelapp/simulator/schemas.py +4 -4
- levelapp/simulator/simulator.py +57 -43
- levelapp/simulator/utils.py +51 -174
- levelapp/workflow/base.py +33 -2
- levelapp/workflow/config.py +6 -2
- levelapp/workflow/context.py +3 -1
- levelapp/workflow/runtime.py +3 -3
- {levelapp-0.1.4.dist-info → levelapp-0.1.5.dist-info}/METADATA +146 -31
- {levelapp-0.1.4.dist-info → levelapp-0.1.5.dist-info}/RECORD +26 -15
- {levelapp-0.1.4.dist-info → levelapp-0.1.5.dist-info}/WHEEL +0 -0
- {levelapp-0.1.4.dist-info → levelapp-0.1.5.dist-info}/licenses/LICENSE +0 -0
levelapp/simulator/schemas.py
CHANGED
|
@@ -9,7 +9,7 @@ from uuid import UUID, uuid4
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
|
|
11
11
|
from typing import Dict, Any, List
|
|
12
|
-
from pydantic import BaseModel, Field, computed_field
|
|
12
|
+
from pydantic import BaseModel, Field, computed_field
|
|
13
13
|
|
|
14
14
|
from levelapp.evaluator.evaluator import JudgeEvaluationResults
|
|
15
15
|
|
|
@@ -24,13 +24,12 @@ class InteractionLevel(str, Enum):
|
|
|
24
24
|
class Interaction(BaseModel):
|
|
25
25
|
"""Represents a single interaction within a conversation."""
|
|
26
26
|
id: UUID = Field(default_factory=uuid4, description="Interaction identifier")
|
|
27
|
+
user_message_path: str = Field(..., description="Path of the user message in the request payload")
|
|
27
28
|
user_message: str = Field(..., description="The user's query message")
|
|
28
|
-
# generated_reply: str = Field(..., description="The agent's reply message")
|
|
29
29
|
reference_reply: str = Field(..., description="The preset reference message")
|
|
30
30
|
interaction_type: InteractionLevel = Field(default=InteractionLevel.INITIAL, description="Type of interaction")
|
|
31
31
|
reference_metadata: Dict[str, Any] = Field(default_factory=dict, description="Expected metadata")
|
|
32
|
-
|
|
33
|
-
guardrail_flag: bool = Field(default=False, description="Flag for guardrail signaling")
|
|
32
|
+
guardrail_flag: Any = Field(default=False, description="Flag for guardrail signaling")
|
|
34
33
|
request_payload: Dict[str, Any] = Field(default_factory=dict, description="Additional request payload")
|
|
35
34
|
|
|
36
35
|
|
|
@@ -40,6 +39,7 @@ class ConversationScript(BaseModel):
|
|
|
40
39
|
interactions: List[Interaction] = Field(default_factory=list, description="List of interactions")
|
|
41
40
|
description: str = Field(default="no-description", description="A short description of the conversation")
|
|
42
41
|
details: Dict[str, str] = Field(default_factory=dict, description="Conversation details")
|
|
42
|
+
variable_request_schema: bool = Field(default=False, description="The payload schema changes for each request")
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class ScriptsBatch(BaseModel):
|
levelapp/simulator/simulator.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
'simulators/service.py': Service layer to manage conversation simulation and evaluation.
|
|
3
3
|
"""
|
|
4
|
-
import json
|
|
5
4
|
import time
|
|
6
5
|
import asyncio
|
|
7
6
|
|
|
@@ -9,8 +8,12 @@ from datetime import datetime
|
|
|
9
8
|
from collections import defaultdict
|
|
10
9
|
from typing import Dict, Any, List
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
from levelapp.
|
|
11
|
+
|
|
12
|
+
from levelapp.core.base import BaseProcess, BaseEvaluator
|
|
13
|
+
from levelapp.endpoint.client import EndpointConfig
|
|
14
|
+
from levelapp.endpoint.manager import EndpointConfigManager
|
|
15
|
+
|
|
16
|
+
from levelapp.core.schemas import EvaluatorType
|
|
14
17
|
from levelapp.simulator.schemas import (
|
|
15
18
|
InteractionEvaluationResults,
|
|
16
19
|
ScriptsBatch,
|
|
@@ -18,13 +21,10 @@ from levelapp.simulator.schemas import (
|
|
|
18
21
|
SimulationResults
|
|
19
22
|
)
|
|
20
23
|
from levelapp.simulator.utils import (
|
|
21
|
-
extract_interaction_details,
|
|
22
|
-
async_interaction_request,
|
|
23
24
|
calculate_average_scores,
|
|
24
25
|
summarize_verdicts,
|
|
25
26
|
)
|
|
26
27
|
from levelapp.aspects import logger
|
|
27
|
-
from levelapp.core.schemas import EvaluatorType
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class ConversationSimulator(BaseProcess):
|
|
@@ -32,29 +32,26 @@ class ConversationSimulator(BaseProcess):
|
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
34
34
|
self,
|
|
35
|
-
|
|
35
|
+
endpoint_config: EndpointConfig | None = None,
|
|
36
36
|
evaluators: Dict[EvaluatorType, BaseEvaluator] | None = None,
|
|
37
37
|
providers: List[str] | None = None,
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
):
|
|
40
40
|
"""
|
|
41
41
|
Initialize the ConversationSimulator.
|
|
42
42
|
|
|
43
43
|
Args:
|
|
44
|
-
|
|
44
|
+
endpoint_config (EndpointConfig): Endpoint configuration.
|
|
45
45
|
evaluators (EvaluationService): Service for evaluating interactions.
|
|
46
46
|
endpoint_config (EndpointConfig): Configuration object for VLA.
|
|
47
47
|
"""
|
|
48
48
|
self._CLASS_NAME = self.__class__.__name__
|
|
49
49
|
|
|
50
|
-
self.
|
|
50
|
+
self.endpoint_config = endpoint_config
|
|
51
51
|
self.evaluators = evaluators
|
|
52
52
|
self.providers = providers
|
|
53
|
-
self.endpoint_config = endpoint_config
|
|
54
53
|
|
|
55
|
-
self.
|
|
56
|
-
self._credentials: str | None = None
|
|
57
|
-
self._headers: Dict[str, Any] | None = None
|
|
54
|
+
self.endpoint_cm = EndpointConfigManager()
|
|
58
55
|
|
|
59
56
|
self.test_batch: ScriptsBatch | None = None
|
|
60
57
|
self.evaluation_verdicts: Dict[str, List[str]] = defaultdict(list)
|
|
@@ -62,36 +59,34 @@ class ConversationSimulator(BaseProcess):
|
|
|
62
59
|
|
|
63
60
|
def setup(
|
|
64
61
|
self,
|
|
65
|
-
|
|
62
|
+
endpoint_config: EndpointConfig,
|
|
66
63
|
evaluators: Dict[EvaluatorType, BaseEvaluator],
|
|
67
64
|
providers: List[str],
|
|
68
|
-
endpoint_config: EndpointConfig,
|
|
69
65
|
) -> None:
|
|
70
66
|
"""
|
|
71
67
|
Initialize the ConversationSimulator.
|
|
72
68
|
|
|
73
69
|
Args:
|
|
74
|
-
|
|
70
|
+
endpoint_config (EndpointConfig): Configuration object for user endpoint API.
|
|
75
71
|
evaluators (Dict[str, BaseEvaluator]): List of evaluator objects for evaluating interactions.
|
|
76
72
|
providers (List[str]): List of LLM provider names.
|
|
77
|
-
|
|
73
|
+
|
|
78
74
|
"""
|
|
79
75
|
_LOG: str = f"[{self._CLASS_NAME}][{self.setup.__name__}]"
|
|
80
76
|
logger.info(f"{_LOG} Setting up the Conversation Simulator..")
|
|
81
77
|
|
|
82
|
-
self.
|
|
78
|
+
if not self.endpoint_cm:
|
|
79
|
+
self.endpoint_cm = EndpointConfigManager()
|
|
80
|
+
|
|
81
|
+
self.endpoint_config = endpoint_config
|
|
82
|
+
self.endpoint_cm.set_endpoints(endpoints_config=[endpoint_config])
|
|
83
|
+
|
|
83
84
|
self.evaluators = evaluators
|
|
84
85
|
self.providers = providers
|
|
85
86
|
|
|
86
87
|
if not self.providers:
|
|
87
88
|
logger.warning(f"{_LOG} No LLM providers were provided. The Judge Evaluation process will not be executed.")
|
|
88
89
|
|
|
89
|
-
self.endpoint_config = endpoint_config
|
|
90
|
-
|
|
91
|
-
self._url = endpoint_config.full_url
|
|
92
|
-
self._credentials = endpoint_config.api_key.get_secret_value()
|
|
93
|
-
self._headers = endpoint_config.headers
|
|
94
|
-
|
|
95
90
|
def get_evaluator(self, name: EvaluatorType) -> BaseEvaluator:
|
|
96
91
|
"""
|
|
97
92
|
Retrieve an evaluator by name.
|
|
@@ -278,22 +273,37 @@ class ConversationSimulator(BaseProcess):
|
|
|
278
273
|
start_time = time.time()
|
|
279
274
|
|
|
280
275
|
results = []
|
|
276
|
+
contextual_mode: bool = script.variable_request_schema
|
|
277
|
+
logger.info(f"{_LOG} Contextual Mode ON: {contextual_mode}")
|
|
281
278
|
interactions = script.interactions
|
|
282
279
|
|
|
283
280
|
for interaction in interactions:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
281
|
+
if contextual_mode:
|
|
282
|
+
from levelapp.simulator.utils import set_by_path
|
|
283
|
+
request_payload = interaction.request_payload
|
|
284
|
+
user_message = interaction.user_message
|
|
285
|
+
set_by_path(
|
|
286
|
+
obj=request_payload,
|
|
287
|
+
path=interaction.user_message_path,
|
|
288
|
+
value=user_message,
|
|
289
|
+
)
|
|
290
|
+
logger.info(f"{_LOG} Request payload (Variable Request Schema):\n{request_payload}\n---")
|
|
291
|
+
else:
|
|
292
|
+
user_message = interaction.user_message
|
|
293
|
+
request_payload = interaction.request_payload
|
|
294
|
+
request_payload.update({"user_message": user_message})
|
|
295
|
+
logger.info(f"{_LOG} Request payload (Configured Request Schema):\n{request_payload}\n---")
|
|
296
|
+
|
|
297
|
+
mappings = self.endpoint_config.response_mapping
|
|
298
|
+
|
|
299
|
+
response = await self.endpoint_cm.send_request(
|
|
300
|
+
endpoint_config=self.endpoint_config,
|
|
301
|
+
context=request_payload,
|
|
302
|
+
contextual_mode=contextual_mode
|
|
295
303
|
)
|
|
296
304
|
|
|
305
|
+
logger.info(f"{_LOG} Response:\n{response}\n---")
|
|
306
|
+
|
|
297
307
|
reference_reply = interaction.reference_reply
|
|
298
308
|
reference_metadata = interaction.reference_metadata
|
|
299
309
|
reference_guardrail_flag: bool = interaction.guardrail_flag
|
|
@@ -312,14 +322,18 @@ class ConversationSimulator(BaseProcess):
|
|
|
312
322
|
results.append(result)
|
|
313
323
|
continue
|
|
314
324
|
|
|
315
|
-
interaction_details =
|
|
316
|
-
response=response
|
|
317
|
-
|
|
325
|
+
interaction_details = self.endpoint_cm.extract_response_data(
|
|
326
|
+
response=response,
|
|
327
|
+
mappings=mappings,
|
|
318
328
|
)
|
|
319
329
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
330
|
+
logger.info(f"{_LOG} Interaction details:\n{interaction_details}\n---")
|
|
331
|
+
|
|
332
|
+
generated_reply = interaction_details.get("agent_reply", "")
|
|
333
|
+
generated_metadata = interaction_details.get("metadata", {})
|
|
334
|
+
extracted_guardrail_flag = interaction_details.get("guardrail_flag", False)
|
|
335
|
+
|
|
336
|
+
logger.info(f"{_LOG} Generated reply:\n{generated_reply}\n---")
|
|
323
337
|
|
|
324
338
|
evaluation_results = await self.evaluate_interaction(
|
|
325
339
|
user_input=user_message,
|
|
@@ -346,7 +360,7 @@ class ConversationSimulator(BaseProcess):
|
|
|
346
360
|
"reference_reply": reference_reply,
|
|
347
361
|
"generated_metadata": generated_metadata,
|
|
348
362
|
"reference_metadata": reference_metadata,
|
|
349
|
-
"guardrail_details":
|
|
363
|
+
"guardrail_details": extracted_guardrail_flag,
|
|
350
364
|
"evaluation_results": evaluation_results.model_dump(),
|
|
351
365
|
}
|
|
352
366
|
|
levelapp/simulator/utils.py
CHANGED
|
@@ -1,197 +1,74 @@
|
|
|
1
1
|
"""
|
|
2
2
|
'simulators/aspects.py': Utility functions for handling VLA interactions and requests.
|
|
3
3
|
"""
|
|
4
|
-
import re
|
|
5
|
-
import ast
|
|
6
|
-
import json
|
|
7
4
|
import httpx
|
|
8
5
|
|
|
9
|
-
from
|
|
10
|
-
from string import Template
|
|
11
|
-
from typing import Any, Dict, List, Union, Iterable
|
|
6
|
+
from typing import Any, Dict, List, Union
|
|
12
7
|
|
|
13
|
-
from pydantic import ValidationError
|
|
14
8
|
|
|
15
9
|
from levelapp.clients import ClientRegistry
|
|
16
10
|
from levelapp.config.prompts import SUMMARIZATION_PROMPT_TEMPLATE
|
|
17
|
-
from levelapp.simulator.schemas import InteractionResults
|
|
18
11
|
from levelapp.aspects import MonitoringAspect, MetricType, logger
|
|
19
12
|
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
def default(self, obj):
|
|
23
|
-
if isinstance(obj, UUID):
|
|
24
|
-
return str(obj)
|
|
25
|
-
return json.JSONEncoder.default(self, obj)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
_PLACEHOLDER_RE = re.compile(r"\$\{([^}]+)\}") # captures inner name(s) of ${...}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _traverse_path(d: Dict[str, Any], path: str):
|
|
32
|
-
"""Traverse a dot-separated path (payload.metadata.budget) and return value or None."""
|
|
33
|
-
parts = path.split(".")
|
|
34
|
-
cur = d
|
|
35
|
-
try:
|
|
36
|
-
for p in parts:
|
|
37
|
-
if isinstance(cur, dict) and p in cur:
|
|
38
|
-
cur = cur[p]
|
|
39
|
-
else:
|
|
40
|
-
return None
|
|
41
|
-
return cur
|
|
42
|
-
except Exception:
|
|
43
|
-
return None
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _recursive_find(container: Any, target_key: str):
|
|
14
|
+
def set_by_path(obj: Dict, path: str, value: Any) -> None:
|
|
47
15
|
"""
|
|
48
|
-
|
|
49
|
-
Returns the value if found, else None.
|
|
50
|
-
"""
|
|
51
|
-
if isinstance(container, dict):
|
|
52
|
-
# direct hit
|
|
53
|
-
if target_key in container:
|
|
54
|
-
return container[target_key]
|
|
55
|
-
# recurse into values
|
|
56
|
-
for v in container.values():
|
|
57
|
-
found = _recursive_find(v, target_key)
|
|
58
|
-
if found is not None:
|
|
59
|
-
return found
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
if isinstance(container, list):
|
|
63
|
-
for item in container:
|
|
64
|
-
found = _recursive_find(item, target_key)
|
|
65
|
-
if found is not None:
|
|
66
|
-
return found
|
|
67
|
-
return None
|
|
68
|
-
|
|
69
|
-
# not a container
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _extract_placeholders(template_str: str) -> Iterable[str]:
|
|
74
|
-
"""Return list of placeholder names in a template string (inner contents of ${...})."""
|
|
75
|
-
return [m.group(1) for m in _PLACEHOLDER_RE.finditer(template_str)]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def extract_interaction_details(
|
|
79
|
-
response: str | Dict[str, Any],
|
|
80
|
-
template: Dict[str, Any],
|
|
81
|
-
) -> InteractionResults:
|
|
82
|
-
"""
|
|
83
|
-
Parse response (str or dict), look up placeholders recursively in the response and
|
|
84
|
-
use Template.safe_substitute with a mapping built from those lookups.
|
|
85
|
-
"""
|
|
86
|
-
try:
|
|
87
|
-
response_dict = response if isinstance(response, dict) else json.loads(response)
|
|
88
|
-
print(f"response:\n{response_dict}\n--")
|
|
89
|
-
if not isinstance(response_dict, dict):
|
|
90
|
-
raise ValueError("Response is not a valid dictionary")
|
|
91
|
-
|
|
92
|
-
output: Dict[str, Any] = {}
|
|
93
|
-
|
|
94
|
-
for out_key, tpl_str in template.items():
|
|
95
|
-
# Build mapping for placeholders found in tpl_str
|
|
96
|
-
placeholders = _extract_placeholders(tpl_str)
|
|
97
|
-
mapping: Dict[str, str] = {}
|
|
98
|
-
|
|
99
|
-
for ph in placeholders:
|
|
100
|
-
value = None
|
|
101
|
-
|
|
102
|
-
# 1) If ph looks like a dotted path, try explicit path traversal first
|
|
103
|
-
if "." in ph:
|
|
104
|
-
value = _traverse_path(response_dict, ph)
|
|
105
|
-
|
|
106
|
-
# 2) If not found yet, try recursive search for the bare key (last path segment)
|
|
107
|
-
if value is None:
|
|
108
|
-
bare = ph.split(".")[-1]
|
|
109
|
-
value = _recursive_find(response_dict, bare)
|
|
110
|
-
|
|
111
|
-
# Prepare mapping value for Template substitution:
|
|
112
|
-
# - dict/list -> JSON string (so substitution yields valid JSON text)
|
|
113
|
-
# - None -> empty string
|
|
114
|
-
# - otherwise -> str(value)
|
|
115
|
-
if isinstance(value, (dict, list)):
|
|
116
|
-
try:
|
|
117
|
-
mapping[ph] = json.dumps(value, ensure_ascii=False)
|
|
118
|
-
except Exception:
|
|
119
|
-
mapping[ph] = str(value)
|
|
120
|
-
elif value is None:
|
|
121
|
-
mapping[ph] = ""
|
|
122
|
-
else:
|
|
123
|
-
mapping[ph] = str(value)
|
|
124
|
-
|
|
125
|
-
# Perform substitution using Template (safe_substitute: missing keys left intact)
|
|
126
|
-
substituted = Template(tpl_str).safe_substitute(mapping)
|
|
127
|
-
output[out_key] = substituted
|
|
128
|
-
|
|
129
|
-
# Post-process generated_metadata if present: convert JSON text back to dict/list when possible
|
|
130
|
-
raw_meta = output.get("generated_metadata", {})
|
|
131
|
-
if isinstance(raw_meta, str) and raw_meta:
|
|
132
|
-
# Try json first (since we used json.dumps above for mapping)
|
|
133
|
-
try:
|
|
134
|
-
output["generated_metadata"] = json.loads(raw_meta)
|
|
135
|
-
except Exception:
|
|
136
|
-
# fallback to ast.literal_eval (handles Python dict strings)
|
|
137
|
-
try:
|
|
138
|
-
output["generated_metadata"] = ast.literal_eval(raw_meta)
|
|
139
|
-
except Exception:
|
|
140
|
-
# if parsing fails, keep the original raw string or use an empty dict
|
|
141
|
-
output["generated_metadata"] = raw_meta
|
|
142
|
-
|
|
143
|
-
# If generated_metadata is empty string, normalize to {}
|
|
144
|
-
if output.get("generated_metadata") == "":
|
|
145
|
-
output["generated_metadata"] = {}
|
|
146
|
-
|
|
147
|
-
print(f"output:\n{output}\n---")
|
|
148
|
-
# Return validated model
|
|
149
|
-
return InteractionResults.model_validate(output)
|
|
150
|
-
|
|
151
|
-
except json.JSONDecodeError as e:
|
|
152
|
-
logger.error(f"[extract_interaction_details] Failed to parse JSON response: {e}")
|
|
153
|
-
return InteractionResults()
|
|
154
|
-
|
|
155
|
-
except ValidationError as e:
|
|
156
|
-
logger.exception(f"[extract_interaction_details] InteractionResults validation failed: {e}")
|
|
157
|
-
return InteractionResults()
|
|
158
|
-
|
|
159
|
-
except Exception as e:
|
|
160
|
-
logger.exception(f"[extract_interaction_details] Unexpected error: {e}")
|
|
161
|
-
return InteractionResults()
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
@MonitoringAspect.monitor(name="interaction_request", category=MetricType.API_CALL)
|
|
165
|
-
async def async_interaction_request(
|
|
166
|
-
url: str,
|
|
167
|
-
headers: Dict[str, str],
|
|
168
|
-
payload: Dict[str, Any],
|
|
169
|
-
) -> httpx.Response | None:
|
|
170
|
-
"""
|
|
171
|
-
Perform an asynchronous interaction request.
|
|
16
|
+
Sets a value in a nested dictionary using JSON path-like notation.
|
|
172
17
|
|
|
173
18
|
Args:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
19
|
+
obj (dict): Dictionary to modify.
|
|
20
|
+
path (str): Path (e.g., "a.b[0].c") indicating where to set the value.
|
|
21
|
+
value (Any): Value to assign at the specified path.
|
|
177
22
|
|
|
178
23
|
Returns:
|
|
179
|
-
|
|
24
|
+
None
|
|
180
25
|
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
26
|
+
parts = path.split(".")
|
|
27
|
+
current = obj
|
|
28
|
+
|
|
29
|
+
for i, part in enumerate(parts):
|
|
30
|
+
is_last = i == len(parts) - 1
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# Handle list index access, e.g., key[0] or [1]
|
|
34
|
+
if '[' in part and ']' in part:
|
|
35
|
+
key, idx = part.split('[')
|
|
36
|
+
idx = int(idx.rstrip(']'))
|
|
37
|
+
|
|
38
|
+
# If we have a key before the list
|
|
39
|
+
if key:
|
|
40
|
+
if key not in current or not isinstance(current[key], list):
|
|
41
|
+
current[key] = []
|
|
42
|
+
while len(current[key]) <= idx:
|
|
43
|
+
current[key].append({})
|
|
44
|
+
target = current[key]
|
|
45
|
+
else:
|
|
46
|
+
if not isinstance(current, list):
|
|
47
|
+
print("[set_by_path][WARNING] Expected a list at this level.")
|
|
48
|
+
return
|
|
49
|
+
while len(current) <= idx:
|
|
50
|
+
current.append({})
|
|
51
|
+
target = current
|
|
52
|
+
|
|
53
|
+
if is_last:
|
|
54
|
+
target[idx] = value
|
|
55
|
+
else:
|
|
56
|
+
if not isinstance(target[idx], dict):
|
|
57
|
+
target[idx] = {}
|
|
58
|
+
current = target[idx]
|
|
190
59
|
|
|
191
|
-
|
|
192
|
-
|
|
60
|
+
else:
|
|
61
|
+
# Regular dictionary key
|
|
62
|
+
if is_last:
|
|
63
|
+
current[part] = value
|
|
64
|
+
else:
|
|
65
|
+
if part not in current or not isinstance(current[part], dict):
|
|
66
|
+
current[part] = {}
|
|
67
|
+
current = current[part]
|
|
193
68
|
|
|
194
|
-
|
|
69
|
+
except (KeyError, IndexError, TypeError, AttributeError) as e:
|
|
70
|
+
print(f"[set_by_path][ERROR] Error type <{e.__class__.__name__}> : {e.args[0]}")
|
|
71
|
+
return
|
|
195
72
|
|
|
196
73
|
|
|
197
74
|
@MonitoringAspect.monitor(
|
levelapp/workflow/base.py
CHANGED
|
@@ -7,6 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any, Dict
|
|
8
8
|
|
|
9
9
|
from levelapp.core.base import BaseProcess
|
|
10
|
+
from levelapp.endpoint.client import EndpointConfig
|
|
11
|
+
from levelapp.endpoint.manager import EndpointConfigManager
|
|
10
12
|
from levelapp.simulator.schemas import ScriptsBatch
|
|
11
13
|
from levelapp.simulator.simulator import ConversationSimulator
|
|
12
14
|
from levelapp.workflow.runtime import WorkflowContext
|
|
@@ -68,6 +70,18 @@ class BaseWorkflow(ABC):
|
|
|
68
70
|
"""
|
|
69
71
|
return self._results
|
|
70
72
|
|
|
73
|
+
@abstractmethod
|
|
74
|
+
async def test_connection(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
75
|
+
"""
|
|
76
|
+
Abstract method for testing endpoint connection.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
context (Dict[str, Any]): The context (request payload) to test connectivity with.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The test connectivity result.
|
|
83
|
+
"""
|
|
84
|
+
|
|
71
85
|
@abstractmethod
|
|
72
86
|
def _setup_process(self, context: WorkflowContext) -> BaseProcess:
|
|
73
87
|
"""
|
|
@@ -105,13 +119,30 @@ class SimulatorWorkflow(BaseWorkflow):
|
|
|
105
119
|
"""
|
|
106
120
|
simulator = ConversationSimulator()
|
|
107
121
|
simulator.setup(
|
|
108
|
-
|
|
122
|
+
endpoint_config=context.endpoint,
|
|
109
123
|
evaluators=context.evaluators,
|
|
110
124
|
providers=context.providers,
|
|
111
|
-
endpoint_config=context.endpoint_config,
|
|
112
125
|
)
|
|
126
|
+
|
|
113
127
|
return simulator
|
|
114
128
|
|
|
129
|
+
async def test_connection(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Runs a connectivity test of the configured endpoint.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
context (Dict[str, Any]): The request payload to send for testing.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The test connectivity result.
|
|
138
|
+
"""
|
|
139
|
+
endpoint_cm = EndpointConfigManager()
|
|
140
|
+
endpoint_cm.set_endpoints(endpoints_config=[self.context.endpoint])
|
|
141
|
+
tester = endpoint_cm.get_tester(endpoint_name=self.context.endpoint.name)
|
|
142
|
+
results = await tester.test(context=context)
|
|
143
|
+
|
|
144
|
+
return results
|
|
145
|
+
|
|
115
146
|
def _load_input_data(self, context: WorkflowContext) -> Dict[str, Any]:
|
|
116
147
|
"""
|
|
117
148
|
Concrete implementation for loading the reference data.
|
levelapp/workflow/config.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import List, Dict, Any, Optional
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
5
|
from levelapp.aspects import logger
|
|
6
|
-
from levelapp.
|
|
6
|
+
from levelapp.endpoint.client import EndpointConfig
|
|
7
7
|
from levelapp.core.schemas import WorkflowType, RepositoryType, EvaluatorType
|
|
8
8
|
|
|
9
9
|
|
|
@@ -39,9 +39,9 @@ class WorkflowConfig(BaseModel):
|
|
|
39
39
|
Supports both file-based loading and in-memory dictionary creation.
|
|
40
40
|
"""
|
|
41
41
|
process: ProcessConfig
|
|
42
|
+
endpoint: EndpointConfig
|
|
42
43
|
evaluation: EvaluationConfig
|
|
43
44
|
reference_data: ReferenceDataConfig
|
|
44
|
-
endpoint: EndpointConfig
|
|
45
45
|
repository: RepositoryConfig
|
|
46
46
|
|
|
47
47
|
class Config:
|
|
@@ -90,3 +90,7 @@ class WorkflowConfig(BaseModel):
|
|
|
90
90
|
self.reference_data.data = content
|
|
91
91
|
logger.info(f"[{self.__class__.__name__}] Reference data loaded from provided content")
|
|
92
92
|
|
|
93
|
+
|
|
94
|
+
if __name__ == '__main__':
|
|
95
|
+
workflow_config = WorkflowConfig.load(path="../../src/data/workflow_config.yaml")
|
|
96
|
+
print(f"Workflow Configuration:\n{workflow_config.model_dump_json(indent=2)}")
|
levelapp/workflow/context.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""levelapp/workflow/context.py: Builds runtime WorkflowContext from WorkflowConfig."""
|
|
2
2
|
from typing import Dict, Callable
|
|
3
3
|
|
|
4
|
+
from levelapp.repository.filesystem import FileSystemRepository
|
|
4
5
|
from levelapp.workflow.config import WorkflowConfig
|
|
5
6
|
from levelapp.core.base import BaseRepository, BaseEvaluator
|
|
6
7
|
from levelapp.workflow.runtime import WorkflowContext
|
|
@@ -19,6 +20,7 @@ class WorkflowContextBuilder:
|
|
|
19
20
|
# Map repository type to constructor that accepts the WorkflowConfig
|
|
20
21
|
self.repository_map: Dict[RepositoryType, Callable[[WorkflowConfig], BaseRepository]] = {
|
|
21
22
|
RepositoryType.FIRESTORE: lambda cfg: FirestoreRepository(cfg),
|
|
23
|
+
RepositoryType.FILESYSTEM: lambda cfg: FileSystemRepository(cfg),
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
# Map evaluator type to constructor that accepts the WorkflowConfig
|
|
@@ -57,6 +59,6 @@ class WorkflowContextBuilder:
|
|
|
57
59
|
repository=repository,
|
|
58
60
|
evaluators=evaluators,
|
|
59
61
|
providers=providers,
|
|
60
|
-
|
|
62
|
+
endpoint=endpoint_config,
|
|
61
63
|
inputs=inputs,
|
|
62
64
|
)
|
levelapp/workflow/runtime.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import Dict, List, Any
|
|
4
4
|
|
|
5
|
-
from levelapp.
|
|
5
|
+
from levelapp.endpoint.client import EndpointConfig
|
|
6
6
|
from levelapp.core.base import BaseRepository, BaseEvaluator
|
|
7
7
|
from levelapp.workflow.config import WorkflowConfig
|
|
8
8
|
from levelapp.core.schemas import EvaluatorType
|
|
@@ -12,8 +12,8 @@ from levelapp.core.schemas import EvaluatorType
|
|
|
12
12
|
class WorkflowContext:
|
|
13
13
|
"""Immutable data holder for workflow execution context."""
|
|
14
14
|
config: WorkflowConfig
|
|
15
|
+
endpoint: EndpointConfig
|
|
15
16
|
repository: BaseRepository
|
|
16
17
|
evaluators: Dict[EvaluatorType, BaseEvaluator]
|
|
17
18
|
providers: List[str]
|
|
18
|
-
|
|
19
|
-
inputs: Dict[str, Any]
|
|
19
|
+
inputs: Dict[str, Any]
|