agenta 0.12.3__py3-none-any.whl → 0.32.0a1__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 agenta might be problematic. Click here for more details.
- agenta/__init__.py +64 -7
- agenta/cli/helper.py +7 -3
- agenta/cli/main.py +15 -50
- agenta/cli/variant_commands.py +50 -29
- agenta/client/Readme.md +72 -64
- agenta/client/api.py +2 -2
- agenta/client/backend/__init__.py +193 -22
- agenta/client/backend/access_control/__init__.py +1 -0
- agenta/client/backend/access_control/client.py +167 -0
- agenta/client/backend/apps/__init__.py +1 -0
- agenta/client/backend/apps/client.py +1691 -0
- agenta/client/backend/bases/__init__.py +1 -0
- agenta/client/backend/bases/client.py +190 -0
- agenta/client/backend/client.py +2508 -5712
- agenta/client/backend/configs/__init__.py +1 -0
- agenta/client/backend/configs/client.py +604 -0
- agenta/client/backend/containers/__init__.py +5 -0
- agenta/client/backend/containers/client.py +648 -0
- agenta/client/backend/containers/types/__init__.py +5 -0
- agenta/client/backend/{types → containers/types}/container_templates_response.py +1 -2
- agenta/client/backend/core/__init__.py +30 -0
- agenta/client/backend/core/client_wrapper.py +42 -9
- agenta/client/backend/core/file.py +70 -0
- agenta/client/backend/core/http_client.py +575 -0
- agenta/client/backend/core/jsonable_encoder.py +33 -39
- agenta/client/backend/core/pydantic_utilities.py +325 -0
- agenta/client/backend/core/query_encoder.py +60 -0
- agenta/client/backend/core/remove_none_from_dict.py +2 -2
- agenta/client/backend/core/request_options.py +35 -0
- agenta/client/backend/core/serialization.py +276 -0
- agenta/client/backend/environments/__init__.py +1 -0
- agenta/client/backend/environments/client.py +196 -0
- agenta/client/backend/evaluations/__init__.py +1 -0
- agenta/client/backend/evaluations/client.py +1469 -0
- agenta/client/backend/evaluators/__init__.py +1 -0
- agenta/client/backend/evaluators/client.py +1283 -0
- agenta/client/backend/observability/__init__.py +1 -0
- agenta/client/backend/observability/client.py +1286 -0
- agenta/client/backend/observability_v_1/__init__.py +5 -0
- agenta/client/backend/observability_v_1/client.py +763 -0
- agenta/client/backend/observability_v_1/types/__init__.py +7 -0
- agenta/client/backend/observability_v_1/types/format.py +5 -0
- agenta/client/backend/observability_v_1/types/query_analytics_response.py +7 -0
- agenta/client/backend/observability_v_1/types/query_traces_response.py +11 -0
- agenta/client/backend/scopes/__init__.py +1 -0
- agenta/client/backend/scopes/client.py +114 -0
- agenta/client/backend/testsets/__init__.py +1 -0
- agenta/client/backend/testsets/client.py +1284 -0
- agenta/client/backend/types/__init__.py +154 -26
- agenta/client/backend/types/agenta_node_dto.py +48 -0
- agenta/client/backend/types/agenta_node_dto_nodes_value.py +6 -0
- agenta/client/backend/types/agenta_nodes_response.py +30 -0
- agenta/client/backend/types/agenta_root_dto.py +30 -0
- agenta/client/backend/types/agenta_roots_response.py +30 -0
- agenta/client/backend/types/agenta_tree_dto.py +30 -0
- agenta/client/backend/types/agenta_trees_response.py +30 -0
- agenta/client/backend/types/aggregated_result.py +16 -31
- agenta/client/backend/types/aggregated_result_evaluator_config.py +8 -0
- agenta/client/backend/types/analytics_response.py +24 -0
- agenta/client/backend/types/app.py +17 -30
- agenta/client/backend/types/app_variant_response.py +36 -0
- agenta/client/backend/types/app_variant_revision.py +17 -32
- agenta/client/backend/types/base_output.py +13 -28
- agenta/client/backend/types/body_import_testset.py +15 -31
- agenta/client/backend/types/bucket_dto.py +26 -0
- agenta/client/backend/types/collect_status_response.py +22 -0
- agenta/client/backend/types/config_db.py +16 -31
- agenta/client/backend/types/config_dto.py +32 -0
- agenta/client/backend/types/config_response_model.py +32 -0
- agenta/client/backend/types/correct_answer.py +22 -0
- agenta/client/backend/types/create_app_output.py +13 -28
- agenta/client/backend/types/create_span.py +45 -0
- agenta/client/backend/types/create_trace_response.py +22 -0
- agenta/client/backend/types/docker_env_vars.py +13 -28
- agenta/client/backend/types/environment_output.py +22 -34
- agenta/client/backend/types/environment_output_extended.py +31 -0
- agenta/client/backend/types/environment_revision.py +26 -0
- agenta/client/backend/types/error.py +22 -0
- agenta/client/backend/types/evaluation.py +22 -33
- agenta/client/backend/types/evaluation_scenario.py +18 -33
- agenta/client/backend/types/evaluation_scenario_input.py +16 -31
- agenta/client/backend/types/evaluation_scenario_output.py +17 -30
- agenta/client/backend/types/evaluation_scenario_result.py +14 -29
- agenta/client/backend/types/evaluation_scenario_score_update.py +21 -0
- agenta/client/backend/types/evaluation_status_enum.py +11 -29
- agenta/client/backend/types/evaluation_type.py +3 -21
- agenta/client/backend/types/evaluator.py +20 -31
- agenta/client/backend/types/evaluator_config.py +21 -33
- agenta/client/backend/types/evaluator_mapping_output_interface.py +21 -0
- agenta/client/backend/types/evaluator_output_interface.py +21 -0
- agenta/client/backend/types/exception_dto.py +26 -0
- agenta/client/backend/types/get_config_response.py +23 -0
- agenta/client/backend/types/header_dto.py +22 -0
- agenta/client/backend/types/http_validation_error.py +14 -29
- agenta/client/backend/types/human_evaluation.py +18 -34
- agenta/client/backend/types/human_evaluation_scenario.py +22 -38
- agenta/client/backend/types/human_evaluation_scenario_input.py +13 -28
- agenta/client/backend/types/human_evaluation_scenario_output.py +13 -28
- agenta/client/backend/types/human_evaluation_scenario_update.py +30 -0
- agenta/client/backend/types/human_evaluation_update.py +22 -0
- agenta/client/backend/types/image.py +18 -32
- agenta/client/backend/types/invite_request.py +16 -30
- agenta/client/backend/types/legacy_analytics_response.py +29 -0
- agenta/client/backend/types/legacy_data_point.py +27 -0
- agenta/client/backend/types/lifecycle_dto.py +24 -0
- agenta/client/backend/types/link_dto.py +24 -0
- agenta/client/backend/types/list_api_keys_response.py +24 -0
- agenta/client/backend/types/llm_run_rate_limit.py +13 -28
- agenta/client/backend/types/llm_tokens.py +23 -0
- agenta/client/backend/types/metrics_dto.py +24 -0
- agenta/client/backend/types/new_human_evaluation.py +27 -0
- agenta/client/backend/types/new_testset.py +16 -31
- agenta/client/backend/types/node_dto.py +24 -0
- agenta/client/backend/types/node_type.py +19 -0
- agenta/client/backend/types/o_tel_context_dto.py +22 -0
- agenta/client/backend/types/o_tel_event_dto.py +23 -0
- agenta/client/backend/types/o_tel_extra_dto.py +26 -0
- agenta/client/backend/types/o_tel_link_dto.py +23 -0
- agenta/client/backend/types/o_tel_span_dto.py +37 -0
- agenta/client/backend/types/o_tel_span_kind.py +15 -0
- agenta/client/backend/types/o_tel_spans_response.py +24 -0
- agenta/client/backend/types/o_tel_status_code.py +8 -0
- agenta/client/backend/types/organization.py +22 -35
- agenta/client/backend/types/organization_output.py +13 -28
- agenta/client/backend/types/outputs.py +5 -0
- agenta/client/backend/types/parent_dto.py +21 -0
- agenta/client/backend/types/permission.py +41 -0
- agenta/client/backend/types/projects_response.py +28 -0
- agenta/client/backend/types/provider_key_dto.py +23 -0
- agenta/client/backend/types/provider_kind.py +21 -0
- agenta/client/backend/types/reference_dto.py +23 -0
- agenta/client/backend/types/reference_request_model.py +23 -0
- agenta/client/backend/types/result.py +18 -31
- agenta/client/backend/types/root_dto.py +21 -0
- agenta/client/backend/types/{human_evaluation_scenario_score.py → score.py} +1 -1
- agenta/client/backend/types/secret_dto.py +24 -0
- agenta/client/backend/types/{human_evaluation_scenario_update_score.py → secret_kind.py} +1 -1
- agenta/client/backend/types/secret_response_dto.py +27 -0
- agenta/client/backend/types/simple_evaluation_output.py +13 -28
- agenta/client/backend/types/span.py +39 -49
- agenta/client/backend/types/span_detail.py +44 -0
- agenta/client/backend/types/span_dto.py +54 -0
- agenta/client/backend/types/span_dto_nodes_value.py +9 -0
- agenta/client/backend/types/span_status_code.py +5 -0
- agenta/client/backend/types/span_variant.py +23 -0
- agenta/client/backend/types/status_code.py +5 -0
- agenta/client/backend/types/status_dto.py +23 -0
- agenta/client/backend/types/template.py +14 -29
- agenta/client/backend/types/template_image_info.py +21 -35
- agenta/client/backend/types/test_set_output_response.py +20 -33
- agenta/client/backend/types/test_set_simple_response.py +13 -28
- agenta/client/backend/types/time_dto.py +23 -0
- agenta/client/backend/types/trace_detail.py +44 -0
- agenta/client/backend/types/tree_dto.py +23 -0
- agenta/client/backend/types/tree_type.py +5 -0
- agenta/client/backend/types/update_app_output.py +22 -0
- agenta/client/backend/types/uri.py +13 -28
- agenta/client/backend/types/validation_error.py +13 -28
- agenta/client/backend/types/variant_action.py +14 -29
- agenta/client/backend/types/variant_action_enum.py +1 -19
- agenta/client/backend/types/with_pagination.py +26 -0
- agenta/client/backend/types/workspace_member_response.py +23 -0
- agenta/client/backend/types/workspace_permission.py +25 -0
- agenta/client/backend/types/workspace_response.py +29 -0
- agenta/client/backend/types/workspace_role.py +15 -0
- agenta/client/backend/types/workspace_role_response.py +23 -0
- agenta/client/backend/variants/__init__.py +5 -0
- agenta/client/backend/variants/client.py +2814 -0
- agenta/client/backend/variants/types/__init__.py +7 -0
- agenta/client/backend/variants/types/add_variant_from_base_and_config_response.py +8 -0
- agenta/client/backend/vault/__init__.py +1 -0
- agenta/client/backend/vault/client.py +685 -0
- agenta/client/client.py +1 -1
- agenta/config.py +0 -2
- agenta/config.toml +0 -1
- agenta/docker/docker-assets/Dockerfile.cloud.template +2 -1
- agenta/docker/docker-assets/Dockerfile.template +2 -1
- agenta/docker/docker_utils.py +11 -12
- agenta/sdk/__init__.py +58 -7
- agenta/sdk/agenta_init.py +182 -164
- agenta/sdk/assets.py +95 -0
- agenta/sdk/client.py +56 -0
- agenta/sdk/context/__init__.py +0 -0
- agenta/sdk/context/exporting.py +25 -0
- agenta/sdk/context/routing.py +27 -0
- agenta/sdk/context/tracing.py +28 -0
- agenta/sdk/decorators/__init__.py +0 -0
- agenta/sdk/decorators/routing.py +576 -0
- agenta/sdk/decorators/tracing.py +296 -0
- agenta/sdk/litellm/__init__.py +1 -0
- agenta/sdk/litellm/litellm.py +314 -0
- agenta/sdk/litellm/mockllm.py +27 -0
- agenta/sdk/litellm/mocks/__init__.py +26 -0
- agenta/sdk/managers/__init__.py +6 -0
- agenta/sdk/managers/config.py +208 -0
- agenta/sdk/managers/deployment.py +45 -0
- agenta/sdk/managers/secrets.py +38 -0
- agenta/sdk/managers/shared.py +639 -0
- agenta/sdk/managers/variant.py +182 -0
- agenta/sdk/managers/vault.py +16 -0
- agenta/sdk/middleware/__init__.py +0 -0
- agenta/sdk/middleware/auth.py +180 -0
- agenta/sdk/middleware/cache.py +47 -0
- agenta/sdk/middleware/config.py +255 -0
- agenta/sdk/middleware/cors.py +29 -0
- agenta/sdk/middleware/inline.py +38 -0
- agenta/sdk/middleware/mock.py +33 -0
- agenta/sdk/middleware/otel.py +40 -0
- agenta/sdk/middleware/vault.py +145 -0
- agenta/sdk/router.py +0 -7
- agenta/sdk/tracing/__init__.py +1 -0
- agenta/sdk/tracing/attributes.py +141 -0
- agenta/sdk/tracing/conventions.py +49 -0
- agenta/sdk/tracing/exporters.py +103 -0
- agenta/sdk/tracing/inline.py +1146 -0
- agenta/sdk/tracing/processors.py +121 -0
- agenta/sdk/tracing/spans.py +136 -0
- agenta/sdk/tracing/tracing.py +237 -0
- agenta/sdk/types.py +478 -74
- agenta/sdk/utils/__init__.py +0 -0
- agenta/sdk/utils/constants.py +1 -0
- agenta/sdk/utils/{helper/openai_cost.py → costs.py} +3 -0
- agenta/sdk/utils/exceptions.py +59 -0
- agenta/sdk/utils/globals.py +6 -10
- agenta/sdk/utils/helpers.py +8 -0
- agenta/sdk/utils/logging.py +21 -0
- agenta/sdk/utils/singleton.py +13 -0
- agenta/sdk/utils/timing.py +58 -0
- {agenta-0.12.3.dist-info → agenta-0.32.0a1.dist-info}/METADATA +98 -151
- agenta-0.32.0a1.dist-info/RECORD +263 -0
- {agenta-0.12.3.dist-info → agenta-0.32.0a1.dist-info}/WHEEL +1 -1
- agenta/client/backend/types/add_variant_from_base_and_config_response.py +0 -7
- agenta/client/backend/types/app_variant_output.py +0 -47
- agenta/client/backend/types/app_variant_output_extended.py +0 -50
- agenta/client/backend/types/delete_evaluation.py +0 -36
- agenta/client/backend/types/evaluation_webhook.py +0 -36
- agenta/client/backend/types/feedback.py +0 -40
- agenta/client/backend/types/get_config_reponse.py +0 -39
- agenta/client/backend/types/list_api_keys_output.py +0 -39
- agenta/client/backend/types/trace.py +0 -48
- agenta/sdk/agenta_decorator.py +0 -443
- agenta/sdk/context.py +0 -41
- agenta-0.12.3.dist-info/RECORD +0 -114
- {agenta-0.12.3.dist-info → agenta-0.32.0a1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from agenta.sdk.managers.shared import SharedManager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VariantManager(SharedManager):
|
|
7
|
+
@classmethod
|
|
8
|
+
def create(
|
|
9
|
+
cls,
|
|
10
|
+
*,
|
|
11
|
+
parameters: dict,
|
|
12
|
+
variant_slug: str,
|
|
13
|
+
#
|
|
14
|
+
app_id: Optional[str] = None,
|
|
15
|
+
app_slug: Optional[str] = None,
|
|
16
|
+
):
|
|
17
|
+
variant = SharedManager.add(
|
|
18
|
+
app_id=app_id,
|
|
19
|
+
app_slug=app_slug,
|
|
20
|
+
variant_slug=variant_slug,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if variant:
|
|
24
|
+
variant = SharedManager.commit(
|
|
25
|
+
parameters=parameters,
|
|
26
|
+
app_id=app_id,
|
|
27
|
+
app_slug=app_slug,
|
|
28
|
+
variant_slug=variant_slug,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return variant
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
async def acreate(
|
|
35
|
+
cls,
|
|
36
|
+
*,
|
|
37
|
+
parameters: dict,
|
|
38
|
+
variant_slug: str,
|
|
39
|
+
#
|
|
40
|
+
app_id: Optional[str] = None,
|
|
41
|
+
app_slug: Optional[str] = None,
|
|
42
|
+
):
|
|
43
|
+
variant = await SharedManager.aadd(
|
|
44
|
+
app_id=app_id,
|
|
45
|
+
app_slug=app_slug,
|
|
46
|
+
variant_slug=variant_slug,
|
|
47
|
+
)
|
|
48
|
+
if variant:
|
|
49
|
+
variant = await SharedManager.acommit(
|
|
50
|
+
parameters=parameters,
|
|
51
|
+
app_id=app_id,
|
|
52
|
+
app_slug=app_slug,
|
|
53
|
+
variant_slug=variant_slug,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return variant
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def commit(
|
|
60
|
+
cls,
|
|
61
|
+
*,
|
|
62
|
+
parameters: dict,
|
|
63
|
+
variant_slug: str,
|
|
64
|
+
#
|
|
65
|
+
app_id: Optional[str] = None,
|
|
66
|
+
app_slug: Optional[str] = None,
|
|
67
|
+
):
|
|
68
|
+
variant = SharedManager.commit(
|
|
69
|
+
parameters=parameters,
|
|
70
|
+
app_id=app_id,
|
|
71
|
+
app_slug=app_slug,
|
|
72
|
+
variant_slug=variant_slug,
|
|
73
|
+
)
|
|
74
|
+
return variant
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
async def acommit(
|
|
78
|
+
cls,
|
|
79
|
+
*,
|
|
80
|
+
parameters: dict,
|
|
81
|
+
variant_slug: str,
|
|
82
|
+
#
|
|
83
|
+
app_id: Optional[str] = None,
|
|
84
|
+
app_slug: Optional[str] = None,
|
|
85
|
+
):
|
|
86
|
+
variant = await SharedManager.acommit(
|
|
87
|
+
parameters=parameters,
|
|
88
|
+
app_id=app_id,
|
|
89
|
+
app_slug=app_slug,
|
|
90
|
+
variant_slug=variant_slug,
|
|
91
|
+
)
|
|
92
|
+
return variant
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def delete(
|
|
96
|
+
cls,
|
|
97
|
+
*,
|
|
98
|
+
variant_slug: str,
|
|
99
|
+
#
|
|
100
|
+
app_id: Optional[str] = None,
|
|
101
|
+
app_slug: Optional[str] = None,
|
|
102
|
+
):
|
|
103
|
+
message = SharedManager.delete(
|
|
104
|
+
app_id=app_id,
|
|
105
|
+
app_slug=app_slug,
|
|
106
|
+
variant_slug=variant_slug,
|
|
107
|
+
)
|
|
108
|
+
return message
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
async def adelete(
|
|
112
|
+
cls,
|
|
113
|
+
*,
|
|
114
|
+
variant_slug: str,
|
|
115
|
+
#
|
|
116
|
+
app_id: Optional[str] = None,
|
|
117
|
+
app_slug: Optional[str] = None,
|
|
118
|
+
):
|
|
119
|
+
message = await SharedManager.adelete(
|
|
120
|
+
app_id=app_id,
|
|
121
|
+
app_slug=app_slug,
|
|
122
|
+
variant_slug=variant_slug,
|
|
123
|
+
)
|
|
124
|
+
return message
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def list(
|
|
128
|
+
cls,
|
|
129
|
+
*,
|
|
130
|
+
app_id: Optional[str] = None,
|
|
131
|
+
app_slug: Optional[str] = None,
|
|
132
|
+
):
|
|
133
|
+
variants = SharedManager.list(
|
|
134
|
+
app_id=app_id,
|
|
135
|
+
app_slug=app_slug,
|
|
136
|
+
)
|
|
137
|
+
return variants
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
async def alist(
|
|
141
|
+
cls,
|
|
142
|
+
*,
|
|
143
|
+
app_id: Optional[str] = None,
|
|
144
|
+
app_slug: Optional[str] = None,
|
|
145
|
+
):
|
|
146
|
+
variants = await SharedManager.alist(
|
|
147
|
+
app_id=app_id,
|
|
148
|
+
app_slug=app_slug,
|
|
149
|
+
)
|
|
150
|
+
return variants
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def history(
|
|
154
|
+
cls,
|
|
155
|
+
*,
|
|
156
|
+
variant_slug: str,
|
|
157
|
+
#
|
|
158
|
+
app_id: Optional[str] = None,
|
|
159
|
+
app_slug: Optional[str] = None,
|
|
160
|
+
):
|
|
161
|
+
variants = SharedManager.history(
|
|
162
|
+
app_id=app_id,
|
|
163
|
+
app_slug=app_slug,
|
|
164
|
+
variant_slug=variant_slug,
|
|
165
|
+
)
|
|
166
|
+
return variants
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
async def ahistory(
|
|
170
|
+
cls,
|
|
171
|
+
*,
|
|
172
|
+
variant_slug: str,
|
|
173
|
+
#
|
|
174
|
+
app_id: Optional[str] = None,
|
|
175
|
+
app_slug: Optional[str] = None,
|
|
176
|
+
):
|
|
177
|
+
variants = await SharedManager.ahistory(
|
|
178
|
+
app_id=app_id,
|
|
179
|
+
app_slug=app_slug,
|
|
180
|
+
variant_slug=variant_slug,
|
|
181
|
+
)
|
|
182
|
+
return variants
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
|
|
3
|
+
from agenta.sdk.context.routing import routing_context
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VaultManager:
|
|
7
|
+
@staticmethod
|
|
8
|
+
def get_from_route() -> Optional[Dict[str, Any]]:
|
|
9
|
+
context = routing_context.get()
|
|
10
|
+
|
|
11
|
+
secrets = context.secrets
|
|
12
|
+
|
|
13
|
+
if not secrets:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
return secrets
|
|
File without changes
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from typing import Callable, Optional
|
|
2
|
+
|
|
3
|
+
from os import getenv
|
|
4
|
+
from json import dumps
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
8
|
+
from fastapi import FastAPI, Request
|
|
9
|
+
from fastapi.responses import JSONResponse
|
|
10
|
+
|
|
11
|
+
from agenta.sdk.middleware.cache import TTLLRUCache, CACHE_CAPACITY, CACHE_TTL
|
|
12
|
+
from agenta.sdk.utils.constants import TRUTHY
|
|
13
|
+
from agenta.sdk.utils.exceptions import display_exception
|
|
14
|
+
|
|
15
|
+
import agenta as ag
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_SHARED_SERVICE = getenv("AGENTA_SHARED_SERVICE", "false").lower() in TRUTHY
|
|
19
|
+
_CACHE_ENABLED = getenv("AGENTA_MIDDLEWARE_CACHE_ENABLED", "true").lower() in TRUTHY
|
|
20
|
+
_UNAUTHORIZED_ALLOWED = (
|
|
21
|
+
getenv("AGENTA_UNAUTHORIZED_EXECUTION_ALLOWED", "false").lower() in TRUTHY
|
|
22
|
+
)
|
|
23
|
+
_ALWAYS_ALLOW_LIST = ["/health"]
|
|
24
|
+
|
|
25
|
+
_cache = TTLLRUCache(capacity=CACHE_CAPACITY, ttl=CACHE_TTL)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DenyResponse(JSONResponse):
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
status_code: int = 401,
|
|
32
|
+
detail: str = "Unauthorized",
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(
|
|
35
|
+
status_code=status_code,
|
|
36
|
+
content={"detail": detail},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DenyException(Exception):
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
status_code: int = 401,
|
|
44
|
+
content: str = "Unauthorized",
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__()
|
|
47
|
+
|
|
48
|
+
self.status_code = status_code
|
|
49
|
+
self.content = content
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AuthMiddleware(BaseHTTPMiddleware):
|
|
53
|
+
def __init__(self, app: FastAPI):
|
|
54
|
+
super().__init__(app)
|
|
55
|
+
|
|
56
|
+
self.host = ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.host
|
|
57
|
+
self.resource_id = (
|
|
58
|
+
ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.service_id
|
|
59
|
+
if not _SHARED_SERVICE
|
|
60
|
+
else None
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def dispatch(self, request: Request, call_next: Callable):
|
|
64
|
+
try:
|
|
65
|
+
if _UNAUTHORIZED_ALLOWED or request.url.path in _ALWAYS_ALLOW_LIST:
|
|
66
|
+
request.state.auth = {}
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
credentials = await self._get_credentials(request)
|
|
70
|
+
|
|
71
|
+
request.state.auth = {"credentials": credentials}
|
|
72
|
+
|
|
73
|
+
return await call_next(request)
|
|
74
|
+
|
|
75
|
+
except DenyException as deny:
|
|
76
|
+
display_exception("Auth Middleware Exception")
|
|
77
|
+
|
|
78
|
+
return DenyResponse(
|
|
79
|
+
status_code=deny.status_code,
|
|
80
|
+
detail=deny.content,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except: # pylint: disable=bare-except
|
|
84
|
+
display_exception("Auth Middleware Exception")
|
|
85
|
+
|
|
86
|
+
return DenyResponse(
|
|
87
|
+
status_code=500,
|
|
88
|
+
detail="Auth: Unexpected Error.",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
async def _get_credentials(self, request: Request) -> Optional[str]:
|
|
92
|
+
try:
|
|
93
|
+
authorization = request.headers.get("authorization", None)
|
|
94
|
+
|
|
95
|
+
headers = {"Authorization": authorization} if authorization else None
|
|
96
|
+
|
|
97
|
+
access_token = request.cookies.get("sAccessToken", None)
|
|
98
|
+
|
|
99
|
+
cookies = {"sAccessToken": access_token} if access_token else None
|
|
100
|
+
|
|
101
|
+
baggage = request.state.otel.get("baggage") if request.state.otel else {}
|
|
102
|
+
|
|
103
|
+
project_id = (
|
|
104
|
+
# CLEANEST
|
|
105
|
+
baggage.get("project_id")
|
|
106
|
+
# ALTERNATIVE
|
|
107
|
+
or request.query_params.get("project_id")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
params = {"action": "run_service", "resource_type": "service"}
|
|
111
|
+
|
|
112
|
+
if self.resource_id:
|
|
113
|
+
params["resource_id"] = self.resource_id
|
|
114
|
+
|
|
115
|
+
if project_id:
|
|
116
|
+
params["project_id"] = project_id
|
|
117
|
+
|
|
118
|
+
_hash = dumps(
|
|
119
|
+
{
|
|
120
|
+
"headers": headers,
|
|
121
|
+
"cookies": cookies,
|
|
122
|
+
"params": params,
|
|
123
|
+
},
|
|
124
|
+
sort_keys=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if _CACHE_ENABLED:
|
|
128
|
+
credentials = _cache.get(_hash)
|
|
129
|
+
|
|
130
|
+
if credentials:
|
|
131
|
+
return credentials
|
|
132
|
+
|
|
133
|
+
async with httpx.AsyncClient() as client:
|
|
134
|
+
response = await client.get(
|
|
135
|
+
f"{self.host}/api/permissions/verify",
|
|
136
|
+
headers=headers,
|
|
137
|
+
cookies=cookies,
|
|
138
|
+
params=params,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if response.status_code == 401:
|
|
142
|
+
raise DenyException(
|
|
143
|
+
status_code=401,
|
|
144
|
+
content="Invalid credentials",
|
|
145
|
+
)
|
|
146
|
+
elif response.status_code == 403:
|
|
147
|
+
raise DenyException(
|
|
148
|
+
status_code=403,
|
|
149
|
+
content="Service execution not allowed.",
|
|
150
|
+
)
|
|
151
|
+
elif response.status_code != 200:
|
|
152
|
+
raise DenyException(
|
|
153
|
+
status_code=400,
|
|
154
|
+
content="Auth: Unexpected Error.",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
auth = response.json()
|
|
158
|
+
|
|
159
|
+
if auth.get("effect") != "allow":
|
|
160
|
+
raise DenyException(
|
|
161
|
+
status_code=403,
|
|
162
|
+
content="Service execution not allowed.",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
credentials = auth.get("credentials")
|
|
166
|
+
|
|
167
|
+
_cache.put(_hash, credentials)
|
|
168
|
+
|
|
169
|
+
return credentials
|
|
170
|
+
|
|
171
|
+
except DenyException as deny:
|
|
172
|
+
raise deny
|
|
173
|
+
|
|
174
|
+
except Exception as exc: # pylint: disable=bare-except
|
|
175
|
+
display_exception("Auth Middleware Exception (suppressed)")
|
|
176
|
+
|
|
177
|
+
raise DenyException(
|
|
178
|
+
status_code=500,
|
|
179
|
+
content="Auth: Unexpected Error.",
|
|
180
|
+
) from exc
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from os import getenv
|
|
2
|
+
from time import time
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
|
|
5
|
+
CACHE_CAPACITY = int(getenv("AGENTA_MIDDLEWARE_CACHE_CAPACITY", "512"))
|
|
6
|
+
CACHE_TTL = int(getenv("AGENTA_MIDDLEWARE_CACHE_TTL", str(5 * 60))) # 5 minutes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TTLLRUCache:
|
|
10
|
+
def __init__(self, capacity: int, ttl: int):
|
|
11
|
+
self.cache = OrderedDict()
|
|
12
|
+
self.capacity = capacity
|
|
13
|
+
self.ttl = ttl
|
|
14
|
+
|
|
15
|
+
def get(self, key):
|
|
16
|
+
# CACHE
|
|
17
|
+
if key not in self.cache:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
value, expiry = self.cache[key]
|
|
21
|
+
# -----
|
|
22
|
+
|
|
23
|
+
# TTL
|
|
24
|
+
if time() > expiry:
|
|
25
|
+
del self.cache[key]
|
|
26
|
+
|
|
27
|
+
return None
|
|
28
|
+
# ---
|
|
29
|
+
|
|
30
|
+
# LRU
|
|
31
|
+
self.cache.move_to_end(key)
|
|
32
|
+
# ---
|
|
33
|
+
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
def put(self, key, value):
|
|
37
|
+
# CACHE
|
|
38
|
+
if key in self.cache:
|
|
39
|
+
del self.cache[key]
|
|
40
|
+
# CACHE & LRU
|
|
41
|
+
elif len(self.cache) >= self.capacity:
|
|
42
|
+
self.cache.popitem(last=False)
|
|
43
|
+
# -----------
|
|
44
|
+
|
|
45
|
+
# TTL
|
|
46
|
+
self.cache[key] = (value, time() + self.ttl)
|
|
47
|
+
# ---
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from typing import Callable, Optional, Tuple, Dict
|
|
2
|
+
|
|
3
|
+
from os import getenv
|
|
4
|
+
from json import dumps
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
+
from fastapi import Request, FastAPI
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from agenta.sdk.middleware.cache import TTLLRUCache, CACHE_CAPACITY, CACHE_TTL
|
|
14
|
+
from agenta.sdk.utils.constants import TRUTHY
|
|
15
|
+
from agenta.sdk.utils.exceptions import suppress
|
|
16
|
+
|
|
17
|
+
import agenta as ag
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_CACHE_ENABLED = getenv("AGENTA_MIDDLEWARE_CACHE_ENABLED", "true").lower() in TRUTHY
|
|
21
|
+
|
|
22
|
+
_cache = TTLLRUCache(capacity=CACHE_CAPACITY, ttl=CACHE_TTL)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Reference(BaseModel):
|
|
26
|
+
id: Optional[str] = None
|
|
27
|
+
slug: Optional[str] = None
|
|
28
|
+
version: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigMiddleware(BaseHTTPMiddleware):
|
|
32
|
+
def __init__(self, app: FastAPI):
|
|
33
|
+
super().__init__(app)
|
|
34
|
+
|
|
35
|
+
self.host = ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.host
|
|
36
|
+
self.application_id = ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.app_id
|
|
37
|
+
|
|
38
|
+
async def dispatch(
|
|
39
|
+
self,
|
|
40
|
+
request: Request,
|
|
41
|
+
call_next: Callable,
|
|
42
|
+
):
|
|
43
|
+
request.state.config = {"parameters": None, "references": None}
|
|
44
|
+
|
|
45
|
+
with suppress():
|
|
46
|
+
parameters, references = await self._get_config(request)
|
|
47
|
+
|
|
48
|
+
request.state.config = {
|
|
49
|
+
"parameters": parameters,
|
|
50
|
+
"references": references,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return await call_next(request)
|
|
54
|
+
|
|
55
|
+
# @atimeit
|
|
56
|
+
async def _get_config(self, request: Request) -> Optional[Tuple[Dict, Dict]]:
|
|
57
|
+
credentials = request.state.auth.get("credentials")
|
|
58
|
+
|
|
59
|
+
headers = None
|
|
60
|
+
if credentials:
|
|
61
|
+
headers = {"Authorization": credentials}
|
|
62
|
+
|
|
63
|
+
application_ref = await self._parse_application_ref(request)
|
|
64
|
+
variant_ref = await self._parse_variant_ref(request)
|
|
65
|
+
environment_ref = await self._parse_environment_ref(request)
|
|
66
|
+
|
|
67
|
+
refs = {}
|
|
68
|
+
if application_ref:
|
|
69
|
+
refs["application_ref"] = application_ref.model_dump()
|
|
70
|
+
if variant_ref:
|
|
71
|
+
refs["variant_ref"] = variant_ref.model_dump()
|
|
72
|
+
if environment_ref:
|
|
73
|
+
refs["environment_ref"] = environment_ref.model_dump()
|
|
74
|
+
|
|
75
|
+
if not refs:
|
|
76
|
+
return None, None
|
|
77
|
+
|
|
78
|
+
_hash = dumps(
|
|
79
|
+
{
|
|
80
|
+
"headers": headers,
|
|
81
|
+
"refs": refs,
|
|
82
|
+
},
|
|
83
|
+
sort_keys=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if _CACHE_ENABLED:
|
|
87
|
+
config_cache = _cache.get(_hash)
|
|
88
|
+
|
|
89
|
+
if config_cache:
|
|
90
|
+
parameters = config_cache.get("parameters")
|
|
91
|
+
references = config_cache.get("references")
|
|
92
|
+
|
|
93
|
+
return parameters, references
|
|
94
|
+
|
|
95
|
+
config = None
|
|
96
|
+
async with httpx.AsyncClient() as client:
|
|
97
|
+
response = await client.post(
|
|
98
|
+
f"{self.host}/api/variants/configs/fetch",
|
|
99
|
+
headers=headers,
|
|
100
|
+
json=refs,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if response.status_code != 200:
|
|
104
|
+
return None, None
|
|
105
|
+
|
|
106
|
+
config = response.json()
|
|
107
|
+
|
|
108
|
+
if not config:
|
|
109
|
+
_cache.put(_hash, {"parameters": None, "references": None})
|
|
110
|
+
|
|
111
|
+
return None, None
|
|
112
|
+
|
|
113
|
+
parameters = config.get("params")
|
|
114
|
+
|
|
115
|
+
references = {}
|
|
116
|
+
|
|
117
|
+
for ref_key in ["application_ref", "variant_ref", "environment_ref"]:
|
|
118
|
+
refs = config.get(ref_key)
|
|
119
|
+
if refs:
|
|
120
|
+
ref_prefix = ref_key.split("_", maxsplit=1)[0]
|
|
121
|
+
|
|
122
|
+
for ref_part_key in ["id", "slug", "version"]:
|
|
123
|
+
ref_part = refs.get(ref_part_key)
|
|
124
|
+
|
|
125
|
+
if ref_part:
|
|
126
|
+
references[ref_prefix + "." + ref_part_key] = ref_part
|
|
127
|
+
|
|
128
|
+
_cache.put(_hash, {"parameters": parameters, "references": references})
|
|
129
|
+
|
|
130
|
+
return parameters, references
|
|
131
|
+
|
|
132
|
+
async def _parse_application_ref(
|
|
133
|
+
self,
|
|
134
|
+
request: Request,
|
|
135
|
+
) -> Optional[Reference]:
|
|
136
|
+
baggage = request.state.otel.get("baggage") if request.state.otel else {}
|
|
137
|
+
|
|
138
|
+
body = {}
|
|
139
|
+
try:
|
|
140
|
+
body = await request.json()
|
|
141
|
+
except: # pylint: disable=bare-except
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
application_id = (
|
|
145
|
+
# CLEANEST
|
|
146
|
+
baggage.get("application_id")
|
|
147
|
+
# ALTERNATIVE
|
|
148
|
+
or request.query_params.get("application_id")
|
|
149
|
+
# LEGACY
|
|
150
|
+
or request.query_params.get("app_id")
|
|
151
|
+
or self.application_id
|
|
152
|
+
)
|
|
153
|
+
application_slug = (
|
|
154
|
+
# CLEANEST
|
|
155
|
+
baggage.get("application_slug")
|
|
156
|
+
# ALTERNATIVE
|
|
157
|
+
or request.query_params.get("application_slug")
|
|
158
|
+
# LEGACY
|
|
159
|
+
or request.query_params.get("app_slug")
|
|
160
|
+
or body.get("app")
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if not any([application_id, application_slug]):
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
return Reference(
|
|
167
|
+
id=application_id,
|
|
168
|
+
slug=application_slug,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def _parse_variant_ref(
|
|
172
|
+
self,
|
|
173
|
+
request: Request,
|
|
174
|
+
) -> Optional[Reference]:
|
|
175
|
+
baggage = request.state.otel.get("baggage") if request.state.otel else {}
|
|
176
|
+
|
|
177
|
+
body = {}
|
|
178
|
+
try:
|
|
179
|
+
body = await request.json()
|
|
180
|
+
except: # pylint: disable=bare-except
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
variant_id = (
|
|
184
|
+
# CLEANEST
|
|
185
|
+
baggage.get("variant_id")
|
|
186
|
+
# ALTERNATIVE
|
|
187
|
+
or request.query_params.get("variant_id")
|
|
188
|
+
)
|
|
189
|
+
variant_slug = (
|
|
190
|
+
# CLEANEST
|
|
191
|
+
baggage.get("variant_slug")
|
|
192
|
+
# ALTERNATIVE
|
|
193
|
+
or request.query_params.get("variant_slug")
|
|
194
|
+
# LEGACY
|
|
195
|
+
or request.query_params.get("config")
|
|
196
|
+
or body.get("config")
|
|
197
|
+
)
|
|
198
|
+
variant_version = (
|
|
199
|
+
# CLEANEST
|
|
200
|
+
baggage.get("variant_version")
|
|
201
|
+
# ALTERNATIVE
|
|
202
|
+
or request.query_params.get("variant_version")
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if not any([variant_id, variant_slug, variant_version]):
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
return Reference(
|
|
209
|
+
id=variant_id,
|
|
210
|
+
slug=variant_slug,
|
|
211
|
+
version=variant_version,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async def _parse_environment_ref(
|
|
215
|
+
self,
|
|
216
|
+
request: Request,
|
|
217
|
+
) -> Optional[Reference]:
|
|
218
|
+
baggage = request.state.otel.get("baggage") if request.state.otel else {}
|
|
219
|
+
|
|
220
|
+
body = {}
|
|
221
|
+
try:
|
|
222
|
+
body = await request.json()
|
|
223
|
+
except: # pylint: disable=bare-except
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
environment_id = (
|
|
227
|
+
# CLEANEST
|
|
228
|
+
baggage.get("environment_id")
|
|
229
|
+
# ALTERNATIVE
|
|
230
|
+
or request.query_params.get("environment_id")
|
|
231
|
+
)
|
|
232
|
+
environment_slug = (
|
|
233
|
+
# CLEANEST
|
|
234
|
+
baggage.get("environment_slug")
|
|
235
|
+
# ALTERNATIVE
|
|
236
|
+
or request.query_params.get("environment_slug")
|
|
237
|
+
# LEGACY
|
|
238
|
+
or request.query_params.get("environment")
|
|
239
|
+
or body.get("environment")
|
|
240
|
+
)
|
|
241
|
+
environment_version = (
|
|
242
|
+
# CLEANEST
|
|
243
|
+
baggage.get("environment_version")
|
|
244
|
+
# ALTERNATIVE
|
|
245
|
+
or request.query_params.get("environment_version")
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not any([environment_id, environment_slug, environment_version]):
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
return Reference(
|
|
252
|
+
id=environment_id,
|
|
253
|
+
slug=environment_slug,
|
|
254
|
+
version=environment_version,
|
|
255
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from os import getenv
|
|
2
|
+
|
|
3
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
4
|
+
from fastapi.middleware.cors import CORSMiddleware as _CORSMiddleware
|
|
5
|
+
|
|
6
|
+
_TRUTHY = {"true", "1", "t", "y", "yes", "on", "enable", "enabled"}
|
|
7
|
+
_USE_CORS = getenv("AGENTA_USE_CORS", "enable").lower() in _TRUTHY
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CORSMiddleware(_CORSMiddleware):
|
|
11
|
+
def __init__(self, app: ASGIApp):
|
|
12
|
+
self.app = app
|
|
13
|
+
|
|
14
|
+
if _USE_CORS:
|
|
15
|
+
super().__init__(
|
|
16
|
+
app=app,
|
|
17
|
+
allow_origins=["*"],
|
|
18
|
+
allow_methods=["*"],
|
|
19
|
+
allow_headers=["*"],
|
|
20
|
+
allow_credentials=True,
|
|
21
|
+
expose_headers=None,
|
|
22
|
+
max_age=None,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
26
|
+
if _USE_CORS:
|
|
27
|
+
return await super().__call__(scope, receive, send)
|
|
28
|
+
|
|
29
|
+
return await self.app(scope, receive, send)
|