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/aspects/loader.py
CHANGED
|
@@ -111,7 +111,7 @@ class DynamicModelBuilder:
|
|
|
111
111
|
"""
|
|
112
112
|
if isinstance(value, Mapping):
|
|
113
113
|
nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value)
|
|
114
|
-
return nested_model,
|
|
114
|
+
return Optional[nested_model], None
|
|
115
115
|
|
|
116
116
|
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
117
117
|
if not value:
|
|
@@ -119,15 +119,15 @@ class DynamicModelBuilder:
|
|
|
119
119
|
|
|
120
120
|
elif isinstance(value[0], Mapping):
|
|
121
121
|
nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value[0])
|
|
122
|
-
return List[nested_model],
|
|
122
|
+
return Optional[List[nested_model]], None
|
|
123
123
|
|
|
124
124
|
else:
|
|
125
125
|
field_type = type(value[0]) if value[0] is not None else Any
|
|
126
|
-
return List[field_type],
|
|
126
|
+
return Optional[List[field_type]], None
|
|
127
127
|
|
|
128
128
|
else:
|
|
129
129
|
field_type = Optional[type(value)] if value is not None else Optional[Any]
|
|
130
|
-
return field_type,
|
|
130
|
+
return field_type, None
|
|
131
131
|
|
|
132
132
|
def create_dynamic_model(self, model_name: str, data: Any) -> Type[BaseModel]:
|
|
133
133
|
"""
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# api_config.yaml
|
|
2
|
+
# REST API Configuration Manager - Example Configuration
|
|
3
|
+
|
|
4
|
+
endpoints:
|
|
5
|
+
# Example 1: Simple GET endpoint with authentication
|
|
6
|
+
- name: github-user
|
|
7
|
+
base_url: https://api.github.com
|
|
8
|
+
path: /users/octocat
|
|
9
|
+
method: GET
|
|
10
|
+
timeout: 15
|
|
11
|
+
retry_count: 2
|
|
12
|
+
retry_backoff: 0.5
|
|
13
|
+
headers:
|
|
14
|
+
- name: Accept
|
|
15
|
+
value: application/vnd.github.v3+json
|
|
16
|
+
secure: false
|
|
17
|
+
- name: Authorization
|
|
18
|
+
value: GITHUB_TOKEN # Environment variable name
|
|
19
|
+
secure: true
|
|
20
|
+
request_schema: []
|
|
21
|
+
response_mapping:
|
|
22
|
+
- field_path: login
|
|
23
|
+
extract_as: username
|
|
24
|
+
- field_path: id
|
|
25
|
+
extract_as: user_id
|
|
26
|
+
- field_path: public_repos
|
|
27
|
+
extract_as: repo_count
|
|
28
|
+
default: 0
|
|
29
|
+
|
|
30
|
+
# Example 2: POST endpoint with nested payload
|
|
31
|
+
- name: user-creation-api
|
|
32
|
+
base_url: https://api.example.com
|
|
33
|
+
path: /v1/users
|
|
34
|
+
method: POST
|
|
35
|
+
timeout: 30
|
|
36
|
+
retry_count: 3
|
|
37
|
+
retry_backoff: 1.0
|
|
38
|
+
headers:
|
|
39
|
+
- name: Content-Type
|
|
40
|
+
value: application/json
|
|
41
|
+
secure: false
|
|
42
|
+
- name: X-API-Key
|
|
43
|
+
value: API_SECRET_KEY
|
|
44
|
+
secure: true
|
|
45
|
+
- name: X-Request-ID
|
|
46
|
+
value: unique-request-id-001
|
|
47
|
+
secure: false
|
|
48
|
+
request_schema:
|
|
49
|
+
- field_path: user.email
|
|
50
|
+
value: test@example.com
|
|
51
|
+
value_type: static
|
|
52
|
+
required: true
|
|
53
|
+
- field_path: user.name
|
|
54
|
+
value: user_name # Will be taken from context
|
|
55
|
+
value_type: dynamic
|
|
56
|
+
required: true
|
|
57
|
+
- field_path: user.metadata.source
|
|
58
|
+
value: api-config-manager
|
|
59
|
+
value_type: static
|
|
60
|
+
required: false
|
|
61
|
+
- field_path: user.metadata.environment
|
|
62
|
+
value: ENVIRONMENT
|
|
63
|
+
value_type: env
|
|
64
|
+
required: false
|
|
65
|
+
response_mapping:
|
|
66
|
+
- field_path: data.user.id
|
|
67
|
+
extract_as: created_user_id
|
|
68
|
+
- field_path: data.user.status
|
|
69
|
+
extract_as: user_status
|
|
70
|
+
default: unknown
|
|
71
|
+
- field_path: data.created_at
|
|
72
|
+
extract_as: timestamp
|
|
73
|
+
|
|
74
|
+
# Example 3: Complex nested response extraction
|
|
75
|
+
- name: search-api
|
|
76
|
+
base_url: https://api.service.com
|
|
77
|
+
path: /search
|
|
78
|
+
method: GET
|
|
79
|
+
timeout: 20
|
|
80
|
+
retry_count: 2
|
|
81
|
+
retry_backoff: 1.5
|
|
82
|
+
headers:
|
|
83
|
+
- name: Authorization
|
|
84
|
+
value: Bearer your-token-here
|
|
85
|
+
secure: false
|
|
86
|
+
request_schema: []
|
|
87
|
+
response_mapping:
|
|
88
|
+
- field_path: results[0].id
|
|
89
|
+
extract_as: first_result_id
|
|
90
|
+
default: null
|
|
91
|
+
- field_path: results[0].title
|
|
92
|
+
extract_as: first_result_title
|
|
93
|
+
default: "No results"
|
|
94
|
+
- field_path: meta.total
|
|
95
|
+
extract_as: total_results
|
|
96
|
+
default: 0
|
|
97
|
+
- field_path: meta.page
|
|
98
|
+
extract_as: current_page
|
|
99
|
+
default: 1
|
|
100
|
+
|
|
101
|
+
# Example 4: Health check endpoint (minimal config)
|
|
102
|
+
- name: health-check
|
|
103
|
+
base_url: https://status.example.com
|
|
104
|
+
path: /health
|
|
105
|
+
method: GET
|
|
106
|
+
timeout: 5
|
|
107
|
+
retry_count: 1
|
|
108
|
+
retry_backoff: 0.5
|
|
109
|
+
headers: []
|
|
110
|
+
request_schema: []
|
|
111
|
+
response_mapping:
|
|
112
|
+
- field_path: status
|
|
113
|
+
extract_as: service_status
|
|
114
|
+
default: down
|
|
115
|
+
- field_path: version
|
|
116
|
+
extract_as: service_version
|
|
117
|
+
|
|
118
|
+
# Example 5: PUT endpoint with authentication
|
|
119
|
+
- name: update-resource
|
|
120
|
+
base_url: https://api.platform.io
|
|
121
|
+
path: /v2/resources/12345
|
|
122
|
+
method: PUT
|
|
123
|
+
timeout: 25
|
|
124
|
+
retry_count: 3
|
|
125
|
+
retry_backoff: 2.0
|
|
126
|
+
headers:
|
|
127
|
+
- name: Content-Type
|
|
128
|
+
value: application/json
|
|
129
|
+
secure: false
|
|
130
|
+
- name: Authorization
|
|
131
|
+
value: PLATFORM_API_TOKEN
|
|
132
|
+
secure: true
|
|
133
|
+
- name: X-Client-Version
|
|
134
|
+
value: "1.0.0"
|
|
135
|
+
secure: false
|
|
136
|
+
request_schema:
|
|
137
|
+
- field_path: resource.name
|
|
138
|
+
value: resource_name
|
|
139
|
+
value_type: dynamic
|
|
140
|
+
required: true
|
|
141
|
+
- field_path: resource.attributes.priority
|
|
142
|
+
value: high
|
|
143
|
+
value_type: static
|
|
144
|
+
required: false
|
|
145
|
+
- field_path: resource.attributes.updated_by
|
|
146
|
+
value: USER_EMAIL
|
|
147
|
+
value_type: env
|
|
148
|
+
required: false
|
|
149
|
+
response_mapping:
|
|
150
|
+
- field_path: data.id
|
|
151
|
+
extract_as: resource_id
|
|
152
|
+
- field_path: data.updated_at
|
|
153
|
+
extract_as: last_updated
|
|
154
|
+
- field_path: data.attributes.status
|
|
155
|
+
extract_as: resource_status
|
|
156
|
+
default: pending
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# api_config.yaml
|
|
2
|
+
# REST API Configuration Manager - Example Configuration
|
|
3
|
+
|
|
4
|
+
endpoints:
|
|
5
|
+
- name: dashq
|
|
6
|
+
base_url: https://dashq-gateway-485vb8zi.uc.gateway.dev
|
|
7
|
+
path: /api/conversations/events
|
|
8
|
+
method: POST
|
|
9
|
+
timeout: 60
|
|
10
|
+
retry_count: 3
|
|
11
|
+
retry_backoff: 0.5
|
|
12
|
+
headers:
|
|
13
|
+
- name: model_id
|
|
14
|
+
value: meta-llama/Meta-Llama-3.1-8B-Instruct
|
|
15
|
+
secure: false
|
|
16
|
+
- name: x-api-key
|
|
17
|
+
value: AIzaSyAmL8blcS2hpPrEH2b84B8ugsVoV7AXrfc
|
|
18
|
+
secure: false # change to true later and place the value in .env file
|
|
19
|
+
request_schema:
|
|
20
|
+
- field_path: eventType
|
|
21
|
+
value: newConversation
|
|
22
|
+
value_type: static
|
|
23
|
+
required: true
|
|
24
|
+
- field_path: conversationId
|
|
25
|
+
value: conversation_id
|
|
26
|
+
value_type: dynamic
|
|
27
|
+
required: true
|
|
28
|
+
- field_path: payload.messageType
|
|
29
|
+
value: newInquiry
|
|
30
|
+
value_type: static
|
|
31
|
+
required: true
|
|
32
|
+
- field_path: payload.communityId
|
|
33
|
+
value: 3310
|
|
34
|
+
value_type: static
|
|
35
|
+
required: true
|
|
36
|
+
- field_path: payload.accountId
|
|
37
|
+
value: 1440
|
|
38
|
+
value_type: static
|
|
39
|
+
required: true
|
|
40
|
+
- field_path: payload.prospectFirstName
|
|
41
|
+
value: Kanye
|
|
42
|
+
value_type: static
|
|
43
|
+
required: true
|
|
44
|
+
- field_path: payload.prospectLastName
|
|
45
|
+
value: West
|
|
46
|
+
value_type: static
|
|
47
|
+
required: true
|
|
48
|
+
- field_path: payload.message
|
|
49
|
+
value: user_message
|
|
50
|
+
value_type: dynamic
|
|
51
|
+
required: true
|
|
52
|
+
- field_path: payload.datetime
|
|
53
|
+
value: "2025-06-25T11:12:27.245Z"
|
|
54
|
+
value_type: static
|
|
55
|
+
required: true
|
|
56
|
+
- field_path: payload.inboundChannel
|
|
57
|
+
value: text
|
|
58
|
+
value_type: static
|
|
59
|
+
required: true
|
|
60
|
+
- field_path: payload.outboundChannel
|
|
61
|
+
value: text
|
|
62
|
+
value_type: static
|
|
63
|
+
required: true
|
|
64
|
+
- field_path: payload.inquirySource
|
|
65
|
+
value: test.com
|
|
66
|
+
value_type: static
|
|
67
|
+
required: true
|
|
68
|
+
response_mapping:
|
|
69
|
+
- field_path: eventType
|
|
70
|
+
extract_as: guardrail_flag
|
|
71
|
+
# - field_path: conversationId
|
|
72
|
+
# extract_as: conversation_id
|
|
73
|
+
# - field_path: payload.accountId
|
|
74
|
+
# extract_as: account_id
|
|
75
|
+
# - field_path: payload.messageType
|
|
76
|
+
# extract_as: message_type
|
|
77
|
+
- field_path: message
|
|
78
|
+
extract_as: agent_reply
|
|
79
|
+
# - field_path: payload.datetime
|
|
80
|
+
# extract_as: datetime
|
|
81
|
+
- field_path: metadata
|
|
82
|
+
extract_as: metadata
|
|
83
|
+
# - field_path: payload.metadata.inquirySource
|
|
84
|
+
# extract_as: inquiry_source
|
|
85
|
+
# - field_path: payload.metadata.booking
|
|
86
|
+
# extract_as: booking
|
|
87
|
+
# - field_path: payload.metadata.tasks
|
|
88
|
+
# extract_as: tasks
|
|
89
|
+
# - field_path: payload.metadata.communityId
|
|
90
|
+
# extract_as: community_id
|
|
91
|
+
# - field_path: payload.handoffMetadata
|
|
92
|
+
# extract_as: handoff_metadata
|
|
93
|
+
# - field_path: payload.isConversationOver
|
|
94
|
+
# extract_as: is_conversation_over
|
levelapp/config/endpoint_.py
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
2
5
|
from enum import Enum
|
|
3
|
-
from
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, List, Dict
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
import httpx
|
|
10
|
+
import yaml
|
|
11
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
class HttpMethod(str, Enum):
|
|
9
15
|
GET = "GET"
|
|
10
16
|
POST = "POST"
|
|
11
17
|
PUT = "PUT"
|
|
12
|
-
|
|
18
|
+
PATCH = "PATCH"
|
|
13
19
|
DELETE = "DELETE"
|
|
14
20
|
|
|
15
21
|
|
|
@@ -59,4 +65,318 @@ class EndpointConfig(BaseModel):
|
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
class PayloadBuilder(ABC):
|
|
62
|
-
"""Abstract base for payload construction strategies."""
|
|
68
|
+
"""Abstract base for payload construction strategies."""
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def build(self, schema: List[RequestSchemaConfig], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
71
|
+
"""Build request payload from schema and context."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class JsonPathPayloadBuilder(PayloadBuilder):
|
|
76
|
+
"""Builds nested JSON payloads using dot-notation paths."""
|
|
77
|
+
def build(self, schema: List[RequestSchemaConfig], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
78
|
+
payload = {}
|
|
79
|
+
|
|
80
|
+
for field_config in schema:
|
|
81
|
+
value = self._resolve_value(field_config, context)
|
|
82
|
+
if value is None and field_config.required:
|
|
83
|
+
raise ValueError(f"Required field {field_config.field_path} has no value")
|
|
84
|
+
|
|
85
|
+
self._set_nested_value(payload, field_config.field_path, value)
|
|
86
|
+
|
|
87
|
+
return payload
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _resolve_value(config: RequestSchemaConfig, context: Dict[str, Any]) -> Any:
|
|
91
|
+
"""Resolve value based on type: statis, env, or dynamic."""
|
|
92
|
+
if config.value_type == "static":
|
|
93
|
+
return config.value
|
|
94
|
+
elif config.value_type == "env":
|
|
95
|
+
import os
|
|
96
|
+
return os.getenv(config.value)
|
|
97
|
+
elif config.value_type == "dynamic":
|
|
98
|
+
print(f"config value: {config.field_path}")
|
|
99
|
+
print(f"context: {context}")
|
|
100
|
+
return context.get(config.field_path)
|
|
101
|
+
|
|
102
|
+
return config.value
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _set_nested_value(obj: Dict, path: str, value: Any) -> None:
|
|
106
|
+
"""Set value in nested dict using dot notation."""
|
|
107
|
+
parts = path.split('.')
|
|
108
|
+
for part in parts[:-1]:
|
|
109
|
+
obj = obj.setdefault(part, {})
|
|
110
|
+
|
|
111
|
+
obj[parts[-1]] = value
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ResponseExtractor:
|
|
115
|
+
"""Extracts data from API responses based on mapping config."""
|
|
116
|
+
def extract(
|
|
117
|
+
self,
|
|
118
|
+
response_data: Dict[str, Any],
|
|
119
|
+
mappings: List[ResponseMappingConfig]
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
"""Extract mapped fields from response data."""
|
|
122
|
+
result = {}
|
|
123
|
+
|
|
124
|
+
for mapping in mappings:
|
|
125
|
+
try:
|
|
126
|
+
value = self._extract_by_path(response_data, mapping.field_path)
|
|
127
|
+
result[mapping.extract_as] = value if value is not None else mapping.default
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"Failed to extract '{mapping.field_path}':\n{e}")
|
|
131
|
+
result[mapping.extract_as] = mapping.default
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _extract_by_path(obj: Any, path: str) -> Any:
|
|
137
|
+
"""Extract value using JSONPath-like notation."""
|
|
138
|
+
parts = path.split('.')
|
|
139
|
+
current = obj
|
|
140
|
+
|
|
141
|
+
for part in parts:
|
|
142
|
+
if '[' in part and ']' in part:
|
|
143
|
+
key, idx = part.split('[')
|
|
144
|
+
idx = int(idx.rstrip(']'))
|
|
145
|
+
current = current[key][idx] if key else current[idx]
|
|
146
|
+
else:
|
|
147
|
+
current = current[part]
|
|
148
|
+
|
|
149
|
+
return current
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class APIClient:
|
|
154
|
+
"""HTTP client for REST API interactions."""
|
|
155
|
+
config: EndpointConfig
|
|
156
|
+
client: httpx.Client = field(init=False)
|
|
157
|
+
logger: logging.Logger = field(init=False)
|
|
158
|
+
|
|
159
|
+
def __post_init__(self):
|
|
160
|
+
self.logger = logging.getLogger(f"APIClient.{self.config.name}")
|
|
161
|
+
self.client = httpx.Client(
|
|
162
|
+
base_url=self.config.base_url,
|
|
163
|
+
timeout=self.config.timeout,
|
|
164
|
+
follow_redirects=True
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def __del__(self):
|
|
168
|
+
if hasattr(self, "client"):
|
|
169
|
+
self.client.close()
|
|
170
|
+
|
|
171
|
+
def _build_headers(self) -> Dict[str, str]:
|
|
172
|
+
"""Build headers with secure value resolution."""
|
|
173
|
+
headers = {}
|
|
174
|
+
import os
|
|
175
|
+
|
|
176
|
+
for header in self.config.headers:
|
|
177
|
+
if header.secure:
|
|
178
|
+
value = os.getenv(header.value)
|
|
179
|
+
if value is None:
|
|
180
|
+
self.logger.warning(f"Secure header '{header.name}' env var '{header.value}' not found")
|
|
181
|
+
continue
|
|
182
|
+
headers[header.name] = value
|
|
183
|
+
else:
|
|
184
|
+
headers[header.name] = header.value
|
|
185
|
+
|
|
186
|
+
return headers
|
|
187
|
+
|
|
188
|
+
def execute(
|
|
189
|
+
self,
|
|
190
|
+
payload: Dict[str, Any] | None = None,
|
|
191
|
+
query_params: Dict[str, Any] | None = None
|
|
192
|
+
) -> httpx.Response:
|
|
193
|
+
"""Execute API request with retry logic."""
|
|
194
|
+
headers = self._build_headers()
|
|
195
|
+
|
|
196
|
+
for attempt in range(self.config.retry_count):
|
|
197
|
+
try:
|
|
198
|
+
response = self.client.request(
|
|
199
|
+
method=self.config.method.value,
|
|
200
|
+
url=self.config.path,
|
|
201
|
+
json=payload,
|
|
202
|
+
params=query_params,
|
|
203
|
+
headers=headers
|
|
204
|
+
)
|
|
205
|
+
response.raise_for_status()
|
|
206
|
+
return response
|
|
207
|
+
|
|
208
|
+
except httpx.HTTPStatusError as e:
|
|
209
|
+
self.logger.error(f"HTTP {e.response.status_code}: {e.response.text}")
|
|
210
|
+
if attempt == self.config.retry_count:
|
|
211
|
+
raise e
|
|
212
|
+
|
|
213
|
+
except httpx.RequestError as e:
|
|
214
|
+
self.logger.error(f"Request failed (attempt {attempt + 1}): {e}")
|
|
215
|
+
if attempt == self.config.retry_count - 1:
|
|
216
|
+
raise
|
|
217
|
+
|
|
218
|
+
import time
|
|
219
|
+
time.sleep(self.config.retry_backoff * (attempt + 1))
|
|
220
|
+
|
|
221
|
+
raise RuntimeError("Max retries exceeded")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class ConnectivityTester:
|
|
225
|
+
"""Tests REST endpoint connectivity with configurable behavior."""
|
|
226
|
+
def __init__(self, config: EndpointConfig):
|
|
227
|
+
self.config = config
|
|
228
|
+
self.client = APIClient(config=config)
|
|
229
|
+
self.payload_builder = JsonPathPayloadBuilder()
|
|
230
|
+
self.response_extractor = ResponseExtractor()
|
|
231
|
+
self.logger = logging.getLogger(f"ConnectivityTester.{self.config.name}")
|
|
232
|
+
|
|
233
|
+
def test(self, context: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
234
|
+
"""Execute connectivity test (template Method)."""
|
|
235
|
+
context = context or {}
|
|
236
|
+
|
|
237
|
+
self.logger.info(f"Starting connectivity test for '{self.config.name}'")
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
payload = None
|
|
241
|
+
if self.config.request_schema:
|
|
242
|
+
payload = self.payload_builder.build(schema=self.config.request_schema, context=context)
|
|
243
|
+
# self.logger.debug(f"Request payload:\n{json.dumps(payload, indent=2, default=str)}\n---")
|
|
244
|
+
self.logger.debug(f"Request payload:\n{payload}\n---")
|
|
245
|
+
|
|
246
|
+
response = self.client.execute(payload=payload)
|
|
247
|
+
self.logger.info(f"Response status: {response.status_code}")
|
|
248
|
+
|
|
249
|
+
response_data = response.json() if response.text else {}
|
|
250
|
+
extracted = self.response_extractor.extract(
|
|
251
|
+
response_data=response_data,
|
|
252
|
+
mappings=self.config.response_mapping
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"success": True,
|
|
257
|
+
"status_code": response.status_code,
|
|
258
|
+
"extracted_data": extracted,
|
|
259
|
+
"raw_response": response_data
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
self.logger.error(f"Connectivity test failed: {e}", exc_info=e)
|
|
264
|
+
return {
|
|
265
|
+
"success": False,
|
|
266
|
+
"error": str(e),
|
|
267
|
+
"error_type": type(e).__name__
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ConfigurationManager:
|
|
272
|
+
"""Manages endpoint configurations and creates testers."""
|
|
273
|
+
def __init__(self, config_path: Path):
|
|
274
|
+
self.config_path = config_path
|
|
275
|
+
self.endpoints: Dict[str, EndpointConfig] = {}
|
|
276
|
+
self.logger = logging.getLogger("ConfigurationManager")
|
|
277
|
+
self._load_config()
|
|
278
|
+
|
|
279
|
+
def _load_config(self) -> None:
|
|
280
|
+
"""Load and validate YAML configuration."""
|
|
281
|
+
try:
|
|
282
|
+
with open(self.config_path, "r") as f:
|
|
283
|
+
data = yaml.safe_load(f)
|
|
284
|
+
|
|
285
|
+
for endpoint_data in data.get('endpoints', []):
|
|
286
|
+
config = EndpointConfig.model_validate(endpoint_data)
|
|
287
|
+
self.endpoints[config.name] = config
|
|
288
|
+
self.logger.info(f"Loaded endpoint config: {config.name}")
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
self.logger.error(f"Failed to load endpoint config: {e}", exc_info=e)
|
|
292
|
+
raise
|
|
293
|
+
|
|
294
|
+
def build_response_mapping(self, content: List[Dict[str, Any]]) -> List[ResponseMappingConfig]:
|
|
295
|
+
mappings = []
|
|
296
|
+
for el in content:
|
|
297
|
+
try:
|
|
298
|
+
mappings.append(ResponseMappingConfig.model_validate(el))
|
|
299
|
+
except ValidationError as e:
|
|
300
|
+
self.logger.error(f"Failed to validate response mapping: {e}", exc_info=e)
|
|
301
|
+
|
|
302
|
+
return mappings
|
|
303
|
+
|
|
304
|
+
def extract_response_data(
|
|
305
|
+
self,
|
|
306
|
+
endpoint_name: str,
|
|
307
|
+
context: Dict[str, Any],
|
|
308
|
+
mappings: List[ResponseMappingConfig]
|
|
309
|
+
) -> Dict[str, Any]:
|
|
310
|
+
if endpoint_name not in self.endpoints:
|
|
311
|
+
raise ValueError(f"Endpoint '{endpoint_name}' not found in configuration")
|
|
312
|
+
|
|
313
|
+
payload_builder = JsonPathPayloadBuilder()
|
|
314
|
+
client = APIClient(config=self.endpoints[endpoint_name])
|
|
315
|
+
extractor = ResponseExtractor()
|
|
316
|
+
payload = payload_builder.build(
|
|
317
|
+
schema=self.endpoints[endpoint_name].request_schema,
|
|
318
|
+
context=context
|
|
319
|
+
)
|
|
320
|
+
response = client.execute(payload=payload)
|
|
321
|
+
self.logger.info(f"Response status: {response.status_code}")
|
|
322
|
+
response_data = response.json() if response.text else {}
|
|
323
|
+
extracted = extractor.extract(
|
|
324
|
+
response_data=response_data,
|
|
325
|
+
mappings=mappings
|
|
326
|
+
)
|
|
327
|
+
return extracted
|
|
328
|
+
|
|
329
|
+
def get_tester(self, endpoint_name: str) -> ConnectivityTester:
|
|
330
|
+
"""Factory method: create connectivity tester for endpoint."""
|
|
331
|
+
if endpoint_name not in self.endpoints:
|
|
332
|
+
raise KeyError(f"Endpoint '{endpoint_name}' not found in configuration")
|
|
333
|
+
|
|
334
|
+
return ConnectivityTester(self.endpoints[endpoint_name])
|
|
335
|
+
|
|
336
|
+
def test_all(self, context: Dict[str, Any] | None = None) -> Dict[str, Dict[str, Any]]:
|
|
337
|
+
"""Test all configured endpoints."""
|
|
338
|
+
results = {}
|
|
339
|
+
for name in self.endpoints:
|
|
340
|
+
tester = self.get_tester(name)
|
|
341
|
+
results[name] = tester.test(context)
|
|
342
|
+
|
|
343
|
+
return results
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == '__main__':
|
|
347
|
+
# Example: Load configuration and test endpoint
|
|
348
|
+
config_manager = ConfigurationManager(Path("dashq_api.yaml"))
|
|
349
|
+
|
|
350
|
+
# # Test specific endpoint
|
|
351
|
+
# tester = config_manager.get_tester("dashq")
|
|
352
|
+
# result = tester.test(context={"conversationId": "439484ef-403b-43c5-9908-884486149d0b"})
|
|
353
|
+
#
|
|
354
|
+
# print(json.dumps(result, indent=2))
|
|
355
|
+
|
|
356
|
+
# # Test all endpoints
|
|
357
|
+
# all_results = config_manager.test_all(context={"environment": "production"})
|
|
358
|
+
# print(json.dumps(all_results, indent=2))
|
|
359
|
+
|
|
360
|
+
content_ = [
|
|
361
|
+
{
|
|
362
|
+
"field_path": "payload.message",
|
|
363
|
+
"extract_as": "agent_reply"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"field_path": "payload.metadata",
|
|
367
|
+
"extract_as": "generated_metadata"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"field_path": "payload.handoffMetadata",
|
|
371
|
+
"extract_as": "handoff_metadata"
|
|
372
|
+
}
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
mappings_ = config_manager.build_response_mapping(content=content_)
|
|
376
|
+
response_data_ = config_manager.extract_response_data(
|
|
377
|
+
endpoint_name="dashq",
|
|
378
|
+
context={"conversationId": "439484ef-403b-43c5-9908-884486149d0b"},
|
|
379
|
+
mappings=mappings_
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
print(f"extracted response data:\n{response_data_}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
endpoints:
|
|
2
|
+
- name: create-message
|
|
3
|
+
base_url: https://postman-echo.com
|
|
4
|
+
path: /post
|
|
5
|
+
method: POST
|
|
6
|
+
timeout: 15
|
|
7
|
+
retry_count: 2
|
|
8
|
+
retry_backoff: 0.5
|
|
9
|
+
|
|
10
|
+
headers:
|
|
11
|
+
- name: Content-Type
|
|
12
|
+
value: application/json
|
|
13
|
+
secure: false
|
|
14
|
+
|
|
15
|
+
request_schema:
|
|
16
|
+
# Static field
|
|
17
|
+
- field_path: message.source
|
|
18
|
+
value: system
|
|
19
|
+
value_type: static
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
# Dynamic field (from runtime context)
|
|
23
|
+
- field_path: message.user
|
|
24
|
+
value: user_name
|
|
25
|
+
value_type: dynamic
|
|
26
|
+
required: true
|
|
27
|
+
|
|
28
|
+
- field_path: message.text
|
|
29
|
+
value: message_text
|
|
30
|
+
value_type: dynamic
|
|
31
|
+
required: true
|
|
32
|
+
|
|
33
|
+
# Env-based field (from OS environment)
|
|
34
|
+
- field_path: metadata.env
|
|
35
|
+
value: ENVIRONMENT
|
|
36
|
+
value_type: env
|
|
37
|
+
required: false
|
|
38
|
+
|
|
39
|
+
response_mapping:
|
|
40
|
+
- field_path: json.message
|
|
41
|
+
extract_as: sent_message
|
|
42
|
+
- field_path: headers.content-type
|
|
43
|
+
extract_as: content_type
|
|
44
|
+
- field_path: url
|
|
45
|
+
extract_as: request_url
|
|
46
|
+
|
|
47
|
+
|
levelapp/core/session.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""levelapp/core/session.py"""
|
|
2
|
+
import asyncio
|
|
2
3
|
import threading
|
|
3
4
|
|
|
4
5
|
from abc import ABC
|
|
@@ -226,6 +227,13 @@ class EvaluationSession:
|
|
|
226
227
|
with self.step(step_name=f"{self.session_name}.collect_results", category=MetricType.RESULTS_COLLECTION):
|
|
227
228
|
self.workflow.collect_results()
|
|
228
229
|
|
|
230
|
+
def run_connectivity_test(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
231
|
+
if not self.workflow:
|
|
232
|
+
raise RuntimeError(f"{self._NAME} Workflow not initialized")
|
|
233
|
+
|
|
234
|
+
results = asyncio.run(self.workflow.test_connection(context=context))
|
|
235
|
+
return results
|
|
236
|
+
|
|
229
237
|
def get_stats(self) -> Dict[str, Any]:
|
|
230
238
|
if self.enable_monitoring:
|
|
231
239
|
return {
|
|
File without changes
|