ioc-scope-mce-core 2.0.0__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.
- ioc_scope_mce_core-2.0.0.dist-info/METADATA +47 -0
- ioc_scope_mce_core-2.0.0.dist-info/RECORD +74 -0
- ioc_scope_mce_core-2.0.0.dist-info/WHEEL +5 -0
- ioc_scope_mce_core-2.0.0.dist-info/top_level.txt +1 -0
- mce/__init__.py +27 -0
- mce/core/catalog.py +208 -0
- mce/core/config.py +33 -0
- mce/core/helpers.py +184 -0
- mce/core/metadata.py +192 -0
- mce/core/metric.py +163 -0
- mce/core/provider.py +36 -0
- mce/core/registry/__init__.py +98 -0
- mce/core/registry/discovery.py +739 -0
- mce/core/registry/provider.py +40 -0
- mce/core/registry/service.py +89 -0
- mce/core/specs/__init__.py +21 -0
- mce/core/specs/_registry.py +223 -0
- mce/core/specs/agent_to_agent_interactions.py +53 -0
- mce/core/specs/agent_to_tool_interactions.py +14 -0
- mce/core/specs/answer_correctness.py +18 -0
- mce/core/specs/answer_relevancy.py +18 -0
- mce/core/specs/bias.py +18 -0
- mce/core/specs/call_count.py +14 -0
- mce/core/specs/coherence.py +18 -0
- mce/core/specs/completion_rate.py +53 -0
- mce/core/specs/component_conflict_rate.py +18 -0
- mce/core/specs/consistency.py +18 -0
- mce/core/specs/context_preservation.py +18 -0
- mce/core/specs/conversation_completeness.py +18 -0
- mce/core/specs/cost.py +14 -0
- mce/core/specs/cycles_count.py +14 -0
- mce/core/specs/duration.py +14 -0
- mce/core/specs/general_structure_and_style.py +18 -0
- mce/core/specs/goal_success_rate.py +18 -0
- mce/core/specs/graph_determinism_score.py +14 -0
- mce/core/specs/groundedness.py +18 -0
- mce/core/specs/hallucination.py +18 -0
- mce/core/specs/information_retention.py +18 -0
- mce/core/specs/intent_recognition_accuracy.py +18 -0
- mce/core/specs/llm_average_confidence.py +14 -0
- mce/core/specs/llm_error_rate.py +76 -0
- mce/core/specs/llm_maximum_confidence.py +14 -0
- mce/core/specs/llm_minimum_confidence.py +14 -0
- mce/core/specs/response_completeness.py +18 -0
- mce/core/specs/role_adherence.py +18 -0
- mce/core/specs/sentiment.py +18 -0
- mce/core/specs/task_completion.py +18 -0
- mce/core/specs/task_delegation_accuracy.py +18 -0
- mce/core/specs/token_count.py +14 -0
- mce/core/specs/tonality.py +18 -0
- mce/core/specs/tool_error.py +14 -0
- mce/core/specs/tool_error_rate.py +14 -0
- mce/core/specs/tool_utilization_accuracy.py +95 -0
- mce/core/specs/topic_adherence.py +18 -0
- mce/core/specs/toxicity.py +18 -0
- mce/core/specs/workflow_cohesion_index.py +18 -0
- mce/core/specs/workflow_efficiency.py +90 -0
- mce/core/types.py +99 -0
- mce/engine/__init__.py +29 -0
- mce/engine/caching.py +611 -0
- mce/engine/context_builder.py +298 -0
- mce/engine/data/model_prices.json +1 -0
- mce/engine/engine.py +886 -0
- mce/engine/llm.py +338 -0
- mce/engine/pricing.py +146 -0
- mce/engine/scheduler.py +61 -0
- mce/engine/strategies.py +129 -0
- mce/engine/usage.py +84 -0
- mce/engine/worker.py +12 -0
- mce/providers/__init__.py +9 -0
- mce/providers/base_wrapper.py +133 -0
- mce/providers/catalog.py +28 -0
- mce/providers/template/__init__.py +4 -0
- mce/providers/virtual/aggregators.py +159 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ioc-scope-mce-core
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Metrics Computation Engine core
|
|
5
|
+
Author-email: Outshift Open <oss-github@cisco.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Requires-Python: <3.15,>=3.13
|
|
8
|
+
Requires-Dist: pydantic>=2.0.0
|
|
9
|
+
Requires-Dist: click>=8.0.0
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
+
Requires-Dist: requests>=2.28.0
|
|
12
|
+
Requires-Dist: networkx>=3.0.0
|
|
13
|
+
Requires-Dist: packaging>=23.0
|
|
14
|
+
Requires-Dist: pandas>=1.5.0
|
|
15
|
+
Requires-Dist: pydantic-settings>=2.10.1
|
|
16
|
+
Requires-Dist: typer>=0.9.0
|
|
17
|
+
Requires-Dist: rdflib>=7.0.0
|
|
18
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
19
|
+
Requires-Dist: ioc-scope-ontology>=1.1.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
23
|
+
Requires-Dist: black; extra == "dev"
|
|
24
|
+
Requires-Dist: flake8; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy; extra == "dev"
|
|
26
|
+
Requires-Dist: httpx; extra == "dev"
|
|
27
|
+
Provides-Extra: neo4j
|
|
28
|
+
Requires-Dist: neo4j>=6.1.0; extra == "neo4j"
|
|
29
|
+
Provides-Extra: deepeval
|
|
30
|
+
Requires-Dist: ioc-scope-mce-provider-deepeval>=2.0.0; extra == "deepeval"
|
|
31
|
+
Provides-Extra: llm
|
|
32
|
+
Requires-Dist: ioc-scope-mce-provider-native>=2.0.0; extra == "llm"
|
|
33
|
+
Provides-Extra: opik
|
|
34
|
+
Requires-Dist: ioc-scope-mce-provider-opik>=2.0.0; extra == "opik"
|
|
35
|
+
Provides-Extra: ragas
|
|
36
|
+
Requires-Dist: ioc-scope-mce-provider-ragas>=2.0.0; extra == "ragas"
|
|
37
|
+
Provides-Extra: server
|
|
38
|
+
Requires-Dist: fastapi>=0.111.0; extra == "server"
|
|
39
|
+
Requires-Dist: uvicorn>=0.30.0; extra == "server"
|
|
40
|
+
Provides-Extra: all
|
|
41
|
+
Requires-Dist: ioc-scope-mce-provider-native>=2.0.0; extra == "all"
|
|
42
|
+
Requires-Dist: ioc-scope-mce-provider-sdk>=2.0.0; extra == "all"
|
|
43
|
+
Requires-Dist: ioc-scope-mce-provider-deepeval>=2.0.0; extra == "all"
|
|
44
|
+
Requires-Dist: ioc-scope-mce-provider-opik>=2.0.0; extra == "all"
|
|
45
|
+
Requires-Dist: ioc-scope-mce-provider-ragas>=2.0.0; extra == "all"
|
|
46
|
+
Requires-Dist: fastapi>=0.111.0; extra == "all"
|
|
47
|
+
Requires-Dist: uvicorn>=0.30.0; extra == "all"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
mce/__init__.py,sha256=wMRx_jlzot3wmSSQ9yR078H8XKg9UGEAwrJp5wSVQ9I,886
|
|
2
|
+
mce/core/catalog.py,sha256=9BPycJRunRj6LCeeCiKgCsF3XuzcKowcXYWKhgARGQQ,8282
|
|
3
|
+
mce/core/config.py,sha256=XNznNiy1WdQmXz-TZhyFRiE5sc1fQa-iJdefESUW814,1044
|
|
4
|
+
mce/core/helpers.py,sha256=d2k7upydKMGJjsyHH6KCTkjGbWNTyER5TjV4CzSnFgo,6430
|
|
5
|
+
mce/core/metadata.py,sha256=JVxS39FSGnVKul0CUOrSwKZHqCbI95dAwKkYPJY0wEE,5859
|
|
6
|
+
mce/core/metric.py,sha256=G1l1WUn_-4IP43Yrw66wcUjGTbqokjStG_sAbcuJEhY,5659
|
|
7
|
+
mce/core/provider.py,sha256=9rVSq1eXgvyFEB6QWsIPS63OxU8b7-h-wcHOtGbN_CA,1373
|
|
8
|
+
mce/core/types.py,sha256=otm3X1gIhO3vF975LdpKSBAPf-qlvqR5c44aMe5kfYQ,3086
|
|
9
|
+
mce/core/registry/__init__.py,sha256=o1S_LhJfgMhgxmWXaECJclMtgpVzaro28YQv9DgTaD0,2914
|
|
10
|
+
mce/core/registry/discovery.py,sha256=zo6t4Xndg8GhSepkAVkYzfV_GJw57s1MfDn1--Q80e8,27387
|
|
11
|
+
mce/core/registry/provider.py,sha256=5QIq93ghTYHRWGS6C9xcOc7wJJx7b1lq7EH9S6h_JRw,1261
|
|
12
|
+
mce/core/registry/service.py,sha256=SBQgORDsg1qZkPI9J810tvRtp3_lVpkLPoGZ6cC6beQ,2888
|
|
13
|
+
mce/core/specs/__init__.py,sha256=wxxV-Uv1ljibU1RrYFjZScqE2UVLXcRRe7NM1POWTqE,563
|
|
14
|
+
mce/core/specs/_registry.py,sha256=jI3qCQ2g4hn-qXBgjgZMNG658kGIi1uFWUOkAS-1Ao8,8076
|
|
15
|
+
mce/core/specs/agent_to_agent_interactions.py,sha256=JeQXwh9PQ_HyMapxspmss-Hye-ktVDTt0bpdGabH7-g,1638
|
|
16
|
+
mce/core/specs/agent_to_tool_interactions.py,sha256=yWsi7UXJ5V6f-4Fk8T8Qlv0WOjtNUI_M7kYB8DdQyds,521
|
|
17
|
+
mce/core/specs/answer_correctness.py,sha256=3dT-wmYvX9s_0pnAKkXpHifosVg9FxahH-FLy930vDE,712
|
|
18
|
+
mce/core/specs/answer_relevancy.py,sha256=bYsPBQZLoVHcOSpmdg3Q_Eqkeu4xU4YHyzWl9Fp9YUY,718
|
|
19
|
+
mce/core/specs/bias.py,sha256=zos8-G3L8o24SuztZ_n9dFUl7_ox3jjbMv6qLD4hNp0,706
|
|
20
|
+
mce/core/specs/call_count.py,sha256=IZO66wbUWMUi79v62dipgtZ781iHN7p5u95OcrJDR5g,540
|
|
21
|
+
mce/core/specs/coherence.py,sha256=WnAU5H5vR1bQEG-lGgg-KWJGcx4QAlA71lhF83e-qn8,698
|
|
22
|
+
mce/core/specs/completion_rate.py,sha256=9yJQWobhsCorXep-gMNVT9477Y1CRUBbImYuZyIFDQ0,1856
|
|
23
|
+
mce/core/specs/component_conflict_rate.py,sha256=GkBJINRW82fEaeUCVjEiwfa4yFJrO1ps46SBFUeV4QE,701
|
|
24
|
+
mce/core/specs/consistency.py,sha256=3E_Tk5YTB6G5hwiwc3YoI_otHILJcRlRuitwH9Iu9s4,693
|
|
25
|
+
mce/core/specs/context_preservation.py,sha256=HETT74CH7inEJuDwMDS_qDhELOMRh8ql7PdTeNvPmY0,701
|
|
26
|
+
mce/core/specs/conversation_completeness.py,sha256=DQBGA88MQkUvkk4oBc9XVi8eyi8ywlzAajax8BMt_zk,728
|
|
27
|
+
mce/core/specs/cost.py,sha256=rYeETaAUAszXW6nSyAUNSleqwoBrnzrrxBRpmEOMjXw,546
|
|
28
|
+
mce/core/specs/cycles_count.py,sha256=0g-RvcYZonT3F_cm6SjaQJ0bTuzjHXMvJ8UZmswcVGw,511
|
|
29
|
+
mce/core/specs/duration.py,sha256=8ck8rLwaLwc7s80ST0CucPcaW5AvnwVKuaprbUZHaW0,528
|
|
30
|
+
mce/core/specs/general_structure_and_style.py,sha256=CkqEUnynn5x4zrfc80PQyJJUO88qlBhfTQzUI-4DYUs,741
|
|
31
|
+
mce/core/specs/goal_success_rate.py,sha256=VPZ1XUCMq1M5WMg3h1SRdA3Vxi6D2Pblyy6FUoQkPjs,690
|
|
32
|
+
mce/core/specs/graph_determinism_score.py,sha256=w-y5pyPexR0wbh5It2OHkC1DtTyOoIjPqdks3XSa3j8,528
|
|
33
|
+
mce/core/specs/groundedness.py,sha256=5C9Ftmzl9Yev-cjNuX1ZMVpYhtbQ6zzFHBvEnscTjJM,689
|
|
34
|
+
mce/core/specs/hallucination.py,sha256=TqPJeIS0kveq_f34vfL6cWGFSM2cPepqCDBvBQ5Dp-E,678
|
|
35
|
+
mce/core/specs/information_retention.py,sha256=DDDNPI7YovEbMhtk8vblAHI5fIJIEDmxPQ1rT7DuiSU,708
|
|
36
|
+
mce/core/specs/intent_recognition_accuracy.py,sha256=ECApsUgj2kQwOe4TxQd8Q2GO0tofWuR89aowuW1Z_BU,704
|
|
37
|
+
mce/core/specs/llm_average_confidence.py,sha256=WQXJ1oOTun4dJg12Ind8TGsZdOdFRXzJRXtidJy91tc,527
|
|
38
|
+
mce/core/specs/llm_error_rate.py,sha256=Ykn8n0Ogn9XwKT2M3S6M8nd44I8PZVV4kiey3EAypIk,2483
|
|
39
|
+
mce/core/specs/llm_maximum_confidence.py,sha256=vtgP_fHeGVZC-QGEWdLMoBQMwDROmLAcp1-HMuUmN8Y,527
|
|
40
|
+
mce/core/specs/llm_minimum_confidence.py,sha256=KoLJEVD83PQmIpo2sDAKwUdcaBuLuPl3ZAIIaIokorw,527
|
|
41
|
+
mce/core/specs/response_completeness.py,sha256=3KxaYbJg8jwYIk7bVzo6A2HWXjqNyu44k_xPp_5kQYA,692
|
|
42
|
+
mce/core/specs/role_adherence.py,sha256=X7oHPDYELByqXoS9Ts15550lzRlPZWi5zC36qPBUpfo,711
|
|
43
|
+
mce/core/specs/sentiment.py,sha256=XnAg69SUTmRWtM-7MG-rZ-QrRB21jgwbT4BhIIIcveg,663
|
|
44
|
+
mce/core/specs/task_completion.py,sha256=-2RXHd3eShvOxA7_PHSwA36-YlIKDqEDirTksZHWCJw,735
|
|
45
|
+
mce/core/specs/task_delegation_accuracy.py,sha256=noHXyy7s5Ldcg0CjRF5kKOtMisp-OGS8NHqAxNJ7tSM,726
|
|
46
|
+
mce/core/specs/token_count.py,sha256=D80Y4GkoQFpAsoRSDvYNmme9XXbYgX_CE_CVxvxkdyU,545
|
|
47
|
+
mce/core/specs/tonality.py,sha256=_saGt5R6Vlc6dDX1Yve5B9Ln_2mW_3uhviEam-8X6P4,677
|
|
48
|
+
mce/core/specs/tool_error.py,sha256=TJ2TZ9bGmG3-4wsh9qL4wxEybVW_m0HTqLhnjYoU9ws,518
|
|
49
|
+
mce/core/specs/tool_error_rate.py,sha256=98-pNR_m4RZcB8DKxTzYV9EgSRJvw4J75HSqkl3EHcw,525
|
|
50
|
+
mce/core/specs/tool_utilization_accuracy.py,sha256=5TBW_EIzxxVOeiQcgXMEvD8UI8W1Wi8v2IiHqhf1Lrw,3035
|
|
51
|
+
mce/core/specs/topic_adherence.py,sha256=Xn9VnKPXif00AGGgRWCVHpglJ6QCCCm-vnteC4ye07w,700
|
|
52
|
+
mce/core/specs/toxicity.py,sha256=AWqCtevXMVT4hIxHuwJMF2ntc7ODewZ1wNVS_la_cFI,710
|
|
53
|
+
mce/core/specs/workflow_cohesion_index.py,sha256=y4LkBwO8kPHcZ37kBGVYF7YsvJREy0NTEjufrxzpARM,710
|
|
54
|
+
mce/core/specs/workflow_efficiency.py,sha256=qpKNKKX4bE-Fz6OmNSnenpqmBQQj0vwArEe1At8ykLk,3047
|
|
55
|
+
mce/engine/__init__.py,sha256=mfLW6UKKVo6maM5jD9r7r6eQSW8deEwiln4FuzZF1J0,807
|
|
56
|
+
mce/engine/caching.py,sha256=oD83Bwnbe2YYuNSBEcaPRpqfOVCPoqh0WseO_WzTamM,24097
|
|
57
|
+
mce/engine/context_builder.py,sha256=Weu03WEJGDM0ehABo4oBbuZe56D_5HC5Z4-dn3ubsMg,10868
|
|
58
|
+
mce/engine/engine.py,sha256=mY6L8rG6COHe5Lozkh0LUXA_NEYqzgExSWCbZopYSlw,34610
|
|
59
|
+
mce/engine/llm.py,sha256=Nksjh1lXNJXrEukgprL35phd1dzaZEG7QIQrHjOM27s,12350
|
|
60
|
+
mce/engine/pricing.py,sha256=brdkvEZukuDedEh9j2R8px3s4eTL12x9uzn7V1LWFs8,5292
|
|
61
|
+
mce/engine/scheduler.py,sha256=S7J2evwodA_CScSzGOzOPQ4hX59Bm2pR1Sy6KLrH9KA,2154
|
|
62
|
+
mce/engine/strategies.py,sha256=qzQy3QjVEVxO_O5pBojUWIZ7aKbQlYdkk7EztY-q7LY,4813
|
|
63
|
+
mce/engine/usage.py,sha256=WUVz50xClk4y7Kqjz0TJtcF4L7Vx3rKNDiG-0MIson0,3008
|
|
64
|
+
mce/engine/worker.py,sha256=sCdotByhpVQ_0nY1KhuuPQiMCA8anLZ-rASjnHQybcI,526
|
|
65
|
+
mce/engine/data/model_prices.json,sha256=NWKp4UqRt5gN-Ht468-v5c43asJurTKj1HYVh7v4m8w,322046
|
|
66
|
+
mce/providers/__init__.py,sha256=U0m0dDHDeU7LiiGdBKmqddgbzR9l3kO4I-S7olR0p5w,361
|
|
67
|
+
mce/providers/base_wrapper.py,sha256=uWhv3dG41qw4jmDPXJsj--U3-JYiPtK9oCODuFIhH6E,4852
|
|
68
|
+
mce/providers/catalog.py,sha256=8nDFvC7D-2qCbMmBRtOoI8tF4Z4HAy1iW3jgIWCt1O8,799
|
|
69
|
+
mce/providers/template/__init__.py,sha256=-7KPZOheq2Mevhzt7vt6VZgK8lBTBQ4HEXgJm-njqTE,177
|
|
70
|
+
mce/providers/virtual/aggregators.py,sha256=Yv8bHtSzv5s-W6TsozdZAjjrJI1vhIruv_2OEuv49BU,5311
|
|
71
|
+
ioc_scope_mce_core-2.0.0.dist-info/METADATA,sha256=GXuvWV0glctawmazwUY2IrZy4hr6FAx6kp1vkR2XzGE,1863
|
|
72
|
+
ioc_scope_mce_core-2.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
73
|
+
ioc_scope_mce_core-2.0.0.dist-info/top_level.txt,sha256=Fxy5kcrJSiMg-a89I_3tPx7wdnlIQ5vM85AN-Krho4U,4
|
|
74
|
+
ioc_scope_mce_core-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mce
|
mce/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pkgutil import extend_path
|
|
5
|
+
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
# Allow multiple workspace packages to contribute to top-level `mce`.
|
|
9
|
+
__path__ = extend_path(__path__, __name__)
|
|
10
|
+
|
|
11
|
+
# Suppress Pydantic v1 compatibility warning from opik in Python 3.14+
|
|
12
|
+
# Must be at the top level before any other imports
|
|
13
|
+
warnings.filterwarnings(
|
|
14
|
+
"ignore",
|
|
15
|
+
message="Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater",
|
|
16
|
+
category=UserWarning,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Expose key public components
|
|
20
|
+
try:
|
|
21
|
+
from .core.types import MetricResult as MetricResult
|
|
22
|
+
from .core.metric import Metric as Metric
|
|
23
|
+
from .engine.engine import MetricEngine as MetricEngine
|
|
24
|
+
|
|
25
|
+
__all__ = ["MetricResult", "Metric", "MetricEngine"]
|
|
26
|
+
except ImportError:
|
|
27
|
+
__all__: list[str] = []
|
mce/core/catalog.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import importlib
|
|
7
|
+
import inspect
|
|
8
|
+
import pkgutil
|
|
9
|
+
import threading
|
|
10
|
+
import statistics
|
|
11
|
+
from mce.core.metric import Metric
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Lazy imports for heavy dependencies
|
|
16
|
+
try:
|
|
17
|
+
from scope_ontology import OntologyService
|
|
18
|
+
except ImportError:
|
|
19
|
+
OntologyService = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MetricCatalogService:
|
|
23
|
+
"""
|
|
24
|
+
Core Service: unified registry for Metric instances.
|
|
25
|
+
Supports lazy creation of templated aggregate (virtual) metrics.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_instance = None
|
|
29
|
+
_lock = threading.Lock() # Guards singleton creation
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._contracts: dict[str, Metric | type[Metric]] = {}
|
|
33
|
+
self._unified_metrics: dict[str, list[Metric]] = {}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_instance(cls):
|
|
37
|
+
"""Thread-safe singleton accessor."""
|
|
38
|
+
if cls._instance is None:
|
|
39
|
+
with cls._lock:
|
|
40
|
+
if cls._instance is None: # double-checked locking
|
|
41
|
+
cls._instance = MetricCatalogService()
|
|
42
|
+
return cls._instance
|
|
43
|
+
|
|
44
|
+
def register_metric(self, metric: Metric | type[Metric]) -> None:
|
|
45
|
+
"""Register a unified Metric plugin or abstract Base."""
|
|
46
|
+
if isinstance(metric, type):
|
|
47
|
+
# Abstract Class registration
|
|
48
|
+
if not hasattr(metric, "metadata"):
|
|
49
|
+
return # Skip if no metadata
|
|
50
|
+
mid = metric.metadata.name
|
|
51
|
+
self._contracts[mid] = metric
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Instance registration
|
|
55
|
+
mid = metric.metric_id
|
|
56
|
+
if mid not in self._contracts:
|
|
57
|
+
self._contracts[mid] = metric
|
|
58
|
+
|
|
59
|
+
if mid not in self._unified_metrics:
|
|
60
|
+
self._unified_metrics[mid] = []
|
|
61
|
+
self._unified_metrics[mid].append(metric)
|
|
62
|
+
|
|
63
|
+
def get_contract(self, metric_id: str) -> Metric | type[Metric] | None:
|
|
64
|
+
"""Look up a registered contract (or unified metric) by ID."""
|
|
65
|
+
return self._contracts.get(metric_id)
|
|
66
|
+
|
|
67
|
+
def get_metric(self, metric_id: str) -> Metric | None:
|
|
68
|
+
"""
|
|
69
|
+
Get a unified metric instance.
|
|
70
|
+
|
|
71
|
+
**Lazy Loading for Templated Aggregates** (Option C):
|
|
72
|
+
If metric_id matches pattern "operation_BaseMetric" (e.g., "avg_AnswerRelevancy"),
|
|
73
|
+
automatically create and cache an AggregateMetric instance.
|
|
74
|
+
|
|
75
|
+
Supported operations:
|
|
76
|
+
- avg_* → AverageMetric(base_metric)
|
|
77
|
+
- max_* → AggregateMetric(base_metric, "max", max)
|
|
78
|
+
- min_* → AggregateMetric(base_metric, "min", min)
|
|
79
|
+
- sum_* → AggregateMetric(base_metric, "sum", sum)
|
|
80
|
+
"""
|
|
81
|
+
# Check if already registered
|
|
82
|
+
metrics = self._unified_metrics.get(metric_id, [])
|
|
83
|
+
if metrics:
|
|
84
|
+
return metrics[0]
|
|
85
|
+
|
|
86
|
+
# Lazy loading: detect templated aggregate pattern
|
|
87
|
+
operation_mapping = {
|
|
88
|
+
"avg": ("avg", "AverageMetric"),
|
|
89
|
+
"max": ("max", "max"),
|
|
90
|
+
"min": ("min", "min"),
|
|
91
|
+
"sum": ("sum", "sum"),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for prefix, (op_name, op_type) in operation_mapping.items():
|
|
95
|
+
if metric_id.startswith(f"{prefix}_"):
|
|
96
|
+
base_metric_id = metric_id[len(prefix) + 1 :] # Remove "avg_" prefix
|
|
97
|
+
|
|
98
|
+
# Validate base metric exists
|
|
99
|
+
if base_metric_id not in self._contracts:
|
|
100
|
+
return None # Base metric not found, cannot create aggregate
|
|
101
|
+
|
|
102
|
+
# Import and instantiate appropriate aggregate metric
|
|
103
|
+
if op_type == "AverageMetric":
|
|
104
|
+
from mce.providers.virtual.aggregators import AverageMetric
|
|
105
|
+
|
|
106
|
+
aggregate_metric = AverageMetric(target_metric_id=base_metric_id)
|
|
107
|
+
else:
|
|
108
|
+
# max / min / sum operations
|
|
109
|
+
from mce.providers.virtual.aggregators import AggregateMetric
|
|
110
|
+
|
|
111
|
+
# Fix #20: statistics imported at module level
|
|
112
|
+
op_fn_map = {
|
|
113
|
+
"max": max,
|
|
114
|
+
"min": min,
|
|
115
|
+
"sum": sum,
|
|
116
|
+
"avg": statistics.mean,
|
|
117
|
+
}
|
|
118
|
+
aggregate_metric = AggregateMetric(
|
|
119
|
+
target_metric_id=base_metric_id,
|
|
120
|
+
operation_name=op_name,
|
|
121
|
+
operation_fn=op_fn_map[op_name],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Resolve target_types for the new metric
|
|
125
|
+
self._resolve_target_types(aggregate_metric)
|
|
126
|
+
|
|
127
|
+
# Register the lazily created metric
|
|
128
|
+
self.register_metric(aggregate_metric)
|
|
129
|
+
return aggregate_metric
|
|
130
|
+
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _resolve_target_types(self, metric: "Metric") -> None:
|
|
134
|
+
"""Resolve target_types for a lazily created aggregate metric.
|
|
135
|
+
|
|
136
|
+
Fix #16: delegates to the single source of truth in discovery.py
|
|
137
|
+
(ensure_target_types) instead of duplicating the mapping logic here.
|
|
138
|
+
The lazy import avoids the circular dependency
|
|
139
|
+
(catalog → discovery → catalog).
|
|
140
|
+
"""
|
|
141
|
+
from mce.core.registry.discovery import (
|
|
142
|
+
ensure_target_types,
|
|
143
|
+
) # lazy — avoids circular import
|
|
144
|
+
|
|
145
|
+
ensure_target_types(metric)
|
|
146
|
+
|
|
147
|
+
# If ensure_target_types could not resolve (no scope/attachment_point),
|
|
148
|
+
# fall back to ontology_class-based mapping for aggregate metrics which
|
|
149
|
+
# inherit the base metric's class name rather than a MAS prefix.
|
|
150
|
+
md = metric.metadata
|
|
151
|
+
if not getattr(md, "target_types", None):
|
|
152
|
+
ap = md.attachment_point
|
|
153
|
+
if ap and OntologyService is not None:
|
|
154
|
+
try:
|
|
155
|
+
onto = OntologyService.get_instance()
|
|
156
|
+
md.target_types = onto.get_subclasses_of(ap)
|
|
157
|
+
return
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.warning(
|
|
160
|
+
"OntologyService.get_subclasses_of(%r) failed: %s — "
|
|
161
|
+
"falling back to ontology_class mapping for '%s'.",
|
|
162
|
+
ap,
|
|
163
|
+
e,
|
|
164
|
+
md.name,
|
|
165
|
+
)
|
|
166
|
+
# Last resort: map known ontology_class strings.
|
|
167
|
+
_class_map = {
|
|
168
|
+
"Session": {"mas:Session"},
|
|
169
|
+
"LLMCall": {"mas:LLMCall"},
|
|
170
|
+
"mas:LLMCall": {"mas:LLMCall"},
|
|
171
|
+
"ToolCall": {"mas:ToolCall"},
|
|
172
|
+
"mas:ToolCall": {"mas:ToolCall"},
|
|
173
|
+
}
|
|
174
|
+
md.target_types = _class_map.get(md.ontology_class, set())
|
|
175
|
+
|
|
176
|
+
def get_all_contracts(self) -> list[Metric | type[Metric]]:
|
|
177
|
+
return list(self._contracts.values())
|
|
178
|
+
|
|
179
|
+
def discover_metrics(self, package_path: str = "mce.metrics"):
|
|
180
|
+
"""Auto-discover concrete Metric subclasses in the specified package."""
|
|
181
|
+
try:
|
|
182
|
+
module = importlib.import_module(package_path)
|
|
183
|
+
path = getattr(module, "__path__", [])
|
|
184
|
+
# NOTE: use module_name (not `name`) so the inner member loop cannot
|
|
185
|
+
# shadow the outer iteration variable — that was a silent bug.
|
|
186
|
+
# Fix #5: use walk_packages (recursive) instead of iter_modules (single-level)
|
|
187
|
+
# so metrics in sub-packages are not silently dropped.
|
|
188
|
+
for _, module_name, _ in pkgutil.walk_packages(
|
|
189
|
+
path, prefix=f"{package_path}."
|
|
190
|
+
):
|
|
191
|
+
sub_module = importlib.import_module(module_name)
|
|
192
|
+
for member_name, obj in inspect.getmembers(sub_module):
|
|
193
|
+
if (
|
|
194
|
+
inspect.isclass(obj)
|
|
195
|
+
and issubclass(obj, Metric)
|
|
196
|
+
and obj is not Metric
|
|
197
|
+
):
|
|
198
|
+
if inspect.isabstract(obj):
|
|
199
|
+
continue # Skip abstract bases
|
|
200
|
+
try:
|
|
201
|
+
instance = obj()
|
|
202
|
+
self.register_metric(instance)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.warning(
|
|
205
|
+
"Failed to instantiate metric %s: %s", member_name, e
|
|
206
|
+
)
|
|
207
|
+
except ImportError as e:
|
|
208
|
+
logger.warning("Could not import package %s: %s", package_path, e)
|
mce/core/config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MceSettings(BaseSettings):
|
|
9
|
+
"""
|
|
10
|
+
Configuration for the MCE library.
|
|
11
|
+
Settings can be overridden by environment variables with the prefix MCE_.
|
|
12
|
+
|
|
13
|
+
Provider availability is determined automatically: if a package is importable
|
|
14
|
+
(e.g. deepeval, ragas, opik) its metrics are discovered and registered.
|
|
15
|
+
Install the metapackage to get everything, or install individual provider
|
|
16
|
+
packages to pick only what you need.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
strict_fail_fast: bool = Field(
|
|
20
|
+
default=False, description="Raise exceptions on provider initialization failure"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
model_config = SettingsConfigDict(
|
|
24
|
+
env_prefix="MCE_",
|
|
25
|
+
env_file=".env",
|
|
26
|
+
env_file_encoding="utf-8",
|
|
27
|
+
case_sensitive=False,
|
|
28
|
+
extra="ignore",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Global settings instance
|
|
33
|
+
settings = MceSettings()
|
mce/core/helpers.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from mce.core.metric import Metric
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _safe_session(context: dict[str, Any]) -> Any:
|
|
11
|
+
"""Return the session object from context if available."""
|
|
12
|
+
return context.get("session")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _conversation_data(context: dict[str, Any]) -> dict[str, Any]:
|
|
16
|
+
"""Return conversation data from context or session."""
|
|
17
|
+
data = context.get("conversation_data")
|
|
18
|
+
if isinstance(data, dict):
|
|
19
|
+
return data
|
|
20
|
+
session = _safe_session(context)
|
|
21
|
+
if session is not None:
|
|
22
|
+
session_data = getattr(session, "conversation_data", None)
|
|
23
|
+
if isinstance(session_data, dict):
|
|
24
|
+
return session_data
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _conversation_text(context: dict[str, Any]) -> str:
|
|
29
|
+
"""Extract the conversation text from context or session."""
|
|
30
|
+
for key in ["conversation_text", "transcript", "conversation"]:
|
|
31
|
+
value = context.get(key)
|
|
32
|
+
if isinstance(value, str) and value:
|
|
33
|
+
return value
|
|
34
|
+
data = _conversation_data(context)
|
|
35
|
+
if "conversation" in data and isinstance(data["conversation"], str):
|
|
36
|
+
return data["conversation"]
|
|
37
|
+
if "elements" in data:
|
|
38
|
+
elements = data.get("elements")
|
|
39
|
+
if isinstance(elements, (list, dict)):
|
|
40
|
+
return json.dumps(elements, indent=2)
|
|
41
|
+
if isinstance(elements, str):
|
|
42
|
+
return elements
|
|
43
|
+
query, response = _query_response(context)
|
|
44
|
+
if query or response:
|
|
45
|
+
return f"User: {query}\nAssistant: {response}".strip()
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _query_response(context: dict[str, Any]) -> tuple[str, str]:
|
|
50
|
+
"""Extract the input query and final response from context or session."""
|
|
51
|
+
query = context.get("input_query") or context.get("input_text")
|
|
52
|
+
response = context.get("final_response") or context.get("output_text")
|
|
53
|
+
data = _conversation_data(context)
|
|
54
|
+
if not query and isinstance(data.get("query"), str):
|
|
55
|
+
query = data.get("query")
|
|
56
|
+
if not response and isinstance(data.get("response"), str):
|
|
57
|
+
response = data.get("response")
|
|
58
|
+
session = _safe_session(context)
|
|
59
|
+
if session is not None:
|
|
60
|
+
if not query and isinstance(getattr(session, "input_query", None), str):
|
|
61
|
+
query = getattr(session, "input_query")
|
|
62
|
+
if not response and isinstance(getattr(session, "final_response", None), str):
|
|
63
|
+
response = getattr(session, "final_response")
|
|
64
|
+
|
|
65
|
+
llm_spans = context.get("llm_spans") or []
|
|
66
|
+
if not llm_spans and session is not None and hasattr(session, "llm_spans"):
|
|
67
|
+
llm_spans = session.llm_spans or []
|
|
68
|
+
|
|
69
|
+
if not query:
|
|
70
|
+
for span in llm_spans:
|
|
71
|
+
query = _span_value(span, "inputContent", "prompt", "input_text")
|
|
72
|
+
if query:
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if not response:
|
|
76
|
+
for span in reversed(llm_spans):
|
|
77
|
+
response = _span_value(span, "outputContent", "completion", "output_text")
|
|
78
|
+
if response:
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
return query or "", response or ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _llm_binary_score(metric: Metric, prompt: str) -> tuple[float, str]:
|
|
85
|
+
"""Get a binary score from an LLM response using the first numeric token."""
|
|
86
|
+
response = metric.call_llm(prompt).strip()
|
|
87
|
+
match = re.search(r"-?\d+(?:\.\d+)?", response)
|
|
88
|
+
score = float(match.group()) if match else 0.0
|
|
89
|
+
score = max(0.0, min(1.0, score))
|
|
90
|
+
return score, response
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _span_dict(span: Any) -> dict[str, Any]:
|
|
94
|
+
"""Normalize span to a dictionary for attribute access."""
|
|
95
|
+
if isinstance(span, dict):
|
|
96
|
+
return span
|
|
97
|
+
return getattr(span, "__dict__", {})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _span_attrs(span: Any) -> dict[str, Any]:
|
|
101
|
+
"""Extract raw span attributes with multiple fallback keys."""
|
|
102
|
+
data = _span_dict(span)
|
|
103
|
+
if isinstance(data.get("attrs"), dict):
|
|
104
|
+
return data["attrs"]
|
|
105
|
+
raw_span = data.get("raw_span_data")
|
|
106
|
+
if isinstance(raw_span, dict):
|
|
107
|
+
for key in ["SpanAttributes", "attributes"]:
|
|
108
|
+
attrs = raw_span.get(key)
|
|
109
|
+
if isinstance(attrs, dict):
|
|
110
|
+
return attrs
|
|
111
|
+
return {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _span_value(span: Any, *keys: str) -> str | None:
|
|
115
|
+
"""Return the first available string value from span data or attributes."""
|
|
116
|
+
data = _span_dict(span)
|
|
117
|
+
attrs = _span_attrs(span)
|
|
118
|
+
for key in keys:
|
|
119
|
+
value = data.get(key)
|
|
120
|
+
if isinstance(value, str) and value:
|
|
121
|
+
return value
|
|
122
|
+
attr_value = attrs.get(key)
|
|
123
|
+
if isinstance(attr_value, str) and attr_value:
|
|
124
|
+
return attr_value
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _first_number(context: dict[str, Any], keys: list[str]) -> float:
|
|
129
|
+
for key in keys:
|
|
130
|
+
value = context.get(key)
|
|
131
|
+
if isinstance(value, (int, float)):
|
|
132
|
+
return float(value)
|
|
133
|
+
return 0.0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _find_key_value(data: Any, target_key: str) -> Any:
|
|
137
|
+
if isinstance(data, dict):
|
|
138
|
+
if target_key in data:
|
|
139
|
+
return data[target_key]
|
|
140
|
+
for value in data.values():
|
|
141
|
+
result = _find_key_value(value, target_key)
|
|
142
|
+
if result is not None:
|
|
143
|
+
return result
|
|
144
|
+
elif isinstance(data, list):
|
|
145
|
+
for item in data:
|
|
146
|
+
result = _find_key_value(item, target_key)
|
|
147
|
+
if result is not None:
|
|
148
|
+
return result
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _extract_logprobs(span: Any) -> list[float]:
|
|
153
|
+
span_dict = span if isinstance(span, dict) else getattr(span, "__dict__", {})
|
|
154
|
+
output_payload = span_dict.get("output_payload") or span_dict.get("output")
|
|
155
|
+
if output_payload is None:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
logprobs_block = _find_key_value(output_payload, "logprobs")
|
|
159
|
+
if not isinstance(logprobs_block, dict):
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
content = logprobs_block.get("content")
|
|
163
|
+
if not isinstance(content, list):
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
logprobs: list[float] = []
|
|
167
|
+
for token_dict in content:
|
|
168
|
+
if isinstance(token_dict, dict) and isinstance(
|
|
169
|
+
token_dict.get("logprob"), (int, float)
|
|
170
|
+
):
|
|
171
|
+
logprobs.append(float(token_dict["logprob"]))
|
|
172
|
+
return logprobs
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _session_logprobs(context: dict[str, Any]) -> list[float]:
|
|
176
|
+
spans = context.get("llm_spans") or []
|
|
177
|
+
session = context.get("session")
|
|
178
|
+
if not spans and session is not None and hasattr(session, "llm_spans"):
|
|
179
|
+
spans = session.llm_spans or []
|
|
180
|
+
|
|
181
|
+
logprobs: list[float] = []
|
|
182
|
+
for span in spans:
|
|
183
|
+
logprobs.extend(_extract_logprobs(span))
|
|
184
|
+
return logprobs
|