truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__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.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +645 -23
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +15 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"""Quality Reporter API endpoints.
|
|
2
|
+
|
|
3
|
+
This module provides REST API endpoints for quality assessment and reporting
|
|
4
|
+
of validation rules, integrating with truthound's QualityReporter module.
|
|
5
|
+
|
|
6
|
+
Endpoints:
|
|
7
|
+
- GET /quality/formats - Get available report formats and options
|
|
8
|
+
- POST /quality/sources/{source_id}/score - Score validation rules
|
|
9
|
+
- POST /quality/sources/{source_id}/report - Generate quality report
|
|
10
|
+
- GET /quality/sources/{source_id}/report/download - Download report
|
|
11
|
+
- GET /quality/sources/{source_id}/summary - Get quality summary
|
|
12
|
+
- POST /quality/filter - Filter quality scores
|
|
13
|
+
- POST /quality/compare - Compare quality scores
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Annotated, Any
|
|
19
|
+
|
|
20
|
+
from fastapi import APIRouter, HTTPException, Path, Query
|
|
21
|
+
from fastapi.responses import Response
|
|
22
|
+
|
|
23
|
+
from truthound_dashboard.core.quality_reporter import (
|
|
24
|
+
QualityFilter,
|
|
25
|
+
QualityReportConfig,
|
|
26
|
+
QualityReportFormat,
|
|
27
|
+
QualityReportStatus,
|
|
28
|
+
QualityReporterService,
|
|
29
|
+
QualityThresholds,
|
|
30
|
+
)
|
|
31
|
+
from truthound_dashboard.schemas.quality_reporter import (
|
|
32
|
+
QualityCompareRequest,
|
|
33
|
+
QualityCompareResponse,
|
|
34
|
+
QualityFilterRequest,
|
|
35
|
+
QualityFormatsResponse,
|
|
36
|
+
QualityLevel,
|
|
37
|
+
QualityLevelDistribution,
|
|
38
|
+
QualityMetricsSchema,
|
|
39
|
+
QualityReportConfigSchema,
|
|
40
|
+
QualityReportFormat as QualityReportFormatSchema,
|
|
41
|
+
QualityReportGenerateRequest,
|
|
42
|
+
QualityReportResponse,
|
|
43
|
+
QualityReportStatus as QualityReportStatusSchema,
|
|
44
|
+
QualityScoreRequest,
|
|
45
|
+
QualityScoreResponse,
|
|
46
|
+
QualityScoreSchema,
|
|
47
|
+
QualityStatisticsSchema,
|
|
48
|
+
QualitySummaryResponse,
|
|
49
|
+
QualityThresholdsSchema,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from .deps import SessionDep
|
|
53
|
+
|
|
54
|
+
router = APIRouter(prefix="/quality")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# Dependency
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def get_quality_reporter_service(session: SessionDep) -> QualityReporterService:
|
|
63
|
+
"""Get quality reporter service dependency."""
|
|
64
|
+
return QualityReporterService(session)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
QualityReporterServiceDep = Annotated[
|
|
68
|
+
QualityReporterService,
|
|
69
|
+
__import__("fastapi").Depends(get_quality_reporter_service)
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Helper Functions
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _convert_score_to_response(score: dict[str, Any]) -> QualityScoreSchema:
|
|
79
|
+
"""Convert internal score to response schema."""
|
|
80
|
+
metrics = score.get("metrics", {})
|
|
81
|
+
return QualityScoreSchema(
|
|
82
|
+
rule_name=score["rule_name"],
|
|
83
|
+
rule_type=score.get("rule_type"),
|
|
84
|
+
column=score.get("column"),
|
|
85
|
+
metrics=QualityMetricsSchema(
|
|
86
|
+
f1_score=metrics.get("f1_score", 0.0),
|
|
87
|
+
precision=metrics.get("precision", 0.0),
|
|
88
|
+
recall=metrics.get("recall", 0.0),
|
|
89
|
+
accuracy=metrics.get("accuracy", 0.0),
|
|
90
|
+
confidence=metrics.get("confidence", 0.0),
|
|
91
|
+
quality_level=QualityLevel(metrics.get("quality_level", "unacceptable")),
|
|
92
|
+
),
|
|
93
|
+
confusion_matrix=score.get("confusion_matrix"),
|
|
94
|
+
test_sample_size=score.get("test_sample_size", 0),
|
|
95
|
+
evaluation_time_ms=score.get("evaluation_time_ms", 0.0),
|
|
96
|
+
recommendation=score.get("recommendation"),
|
|
97
|
+
should_use=score.get("should_use", True),
|
|
98
|
+
issues=score.get("issues", []),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _convert_statistics_to_response(
|
|
103
|
+
stats: dict[str, Any] | None,
|
|
104
|
+
) -> QualityStatisticsSchema | None:
|
|
105
|
+
"""Convert internal statistics to response schema."""
|
|
106
|
+
if not stats:
|
|
107
|
+
return None
|
|
108
|
+
return QualityStatisticsSchema(
|
|
109
|
+
total_count=stats.get("total_count", 0),
|
|
110
|
+
excellent_count=stats.get("excellent_count", 0),
|
|
111
|
+
good_count=stats.get("good_count", 0),
|
|
112
|
+
acceptable_count=stats.get("acceptable_count", 0),
|
|
113
|
+
poor_count=stats.get("poor_count", 0),
|
|
114
|
+
unacceptable_count=stats.get("unacceptable_count", 0),
|
|
115
|
+
should_use_count=stats.get("should_use_count", 0),
|
|
116
|
+
avg_f1=stats.get("avg_f1", 0.0),
|
|
117
|
+
min_f1=stats.get("min_f1", 0.0),
|
|
118
|
+
max_f1=stats.get("max_f1", 0.0),
|
|
119
|
+
avg_precision=stats.get("avg_precision", 0.0),
|
|
120
|
+
avg_recall=stats.get("avg_recall", 0.0),
|
|
121
|
+
avg_confidence=stats.get("avg_confidence", 0.0),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _convert_distribution_to_response(
|
|
126
|
+
dist: list[dict[str, Any]] | None,
|
|
127
|
+
) -> list[QualityLevelDistribution] | None:
|
|
128
|
+
"""Convert internal distribution to response schema."""
|
|
129
|
+
if not dist:
|
|
130
|
+
return None
|
|
131
|
+
return [
|
|
132
|
+
QualityLevelDistribution(
|
|
133
|
+
level=QualityLevel(d["level"]),
|
|
134
|
+
count=d["count"],
|
|
135
|
+
percentage=d["percentage"],
|
|
136
|
+
)
|
|
137
|
+
for d in dist
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _request_to_filter(request: QualityFilterRequest) -> QualityFilter:
|
|
142
|
+
"""Convert request schema to internal filter."""
|
|
143
|
+
from truthound_dashboard.core.quality_reporter import (
|
|
144
|
+
QualityLevel as InternalLevel,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
min_level = None
|
|
148
|
+
max_level = None
|
|
149
|
+
if request.min_level:
|
|
150
|
+
min_level = InternalLevel(request.min_level.value)
|
|
151
|
+
if request.max_level:
|
|
152
|
+
max_level = InternalLevel(request.max_level.value)
|
|
153
|
+
|
|
154
|
+
return QualityFilter(
|
|
155
|
+
min_level=min_level,
|
|
156
|
+
max_level=max_level,
|
|
157
|
+
min_f1=request.min_f1,
|
|
158
|
+
max_f1=request.max_f1,
|
|
159
|
+
min_confidence=request.min_confidence,
|
|
160
|
+
should_use_only=request.should_use_only,
|
|
161
|
+
include_columns=request.include_columns,
|
|
162
|
+
exclude_columns=request.exclude_columns,
|
|
163
|
+
rule_types=request.rule_types,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _request_to_config(request: QualityReportConfigSchema | None) -> QualityReportConfig:
|
|
168
|
+
"""Convert request schema to internal config."""
|
|
169
|
+
if not request:
|
|
170
|
+
return QualityReportConfig()
|
|
171
|
+
|
|
172
|
+
return QualityReportConfig(
|
|
173
|
+
title=request.title,
|
|
174
|
+
description=request.description,
|
|
175
|
+
include_metrics=request.include_metrics,
|
|
176
|
+
include_confusion_matrix=request.include_confusion_matrix,
|
|
177
|
+
include_recommendations=request.include_recommendations,
|
|
178
|
+
include_statistics=request.include_statistics,
|
|
179
|
+
include_summary=request.include_summary,
|
|
180
|
+
include_charts=request.include_charts,
|
|
181
|
+
metric_precision=request.metric_precision,
|
|
182
|
+
percentage_format=request.percentage_format,
|
|
183
|
+
sort_order=request.sort_order.value if request.sort_order else "f1_desc",
|
|
184
|
+
max_scores=request.max_scores,
|
|
185
|
+
theme=request.theme,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _request_to_thresholds(
|
|
190
|
+
request: QualityThresholdsSchema | None,
|
|
191
|
+
) -> QualityThresholds:
|
|
192
|
+
"""Convert request schema to internal thresholds."""
|
|
193
|
+
if not request:
|
|
194
|
+
return QualityThresholds()
|
|
195
|
+
|
|
196
|
+
return QualityThresholds(
|
|
197
|
+
excellent=request.excellent,
|
|
198
|
+
good=request.good,
|
|
199
|
+
acceptable=request.acceptable,
|
|
200
|
+
poor=request.poor,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# =============================================================================
|
|
205
|
+
# Endpoints
|
|
206
|
+
# =============================================================================
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@router.get(
|
|
210
|
+
"/formats",
|
|
211
|
+
response_model=QualityFormatsResponse,
|
|
212
|
+
summary="Get available quality report formats",
|
|
213
|
+
description="List all available report formats, sort orders, themes, and options",
|
|
214
|
+
)
|
|
215
|
+
async def get_formats(
|
|
216
|
+
service: QualityReporterServiceDep,
|
|
217
|
+
) -> QualityFormatsResponse:
|
|
218
|
+
"""Get available quality report formats and options.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Available formats, sort orders, themes, and default quality thresholds.
|
|
222
|
+
"""
|
|
223
|
+
data = service.get_available_formats()
|
|
224
|
+
return QualityFormatsResponse(
|
|
225
|
+
formats=data["formats"],
|
|
226
|
+
sort_orders=data["sort_orders"],
|
|
227
|
+
themes=data["themes"],
|
|
228
|
+
default_thresholds=QualityThresholdsSchema(**data["default_thresholds"]),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@router.post(
|
|
233
|
+
"/sources/{source_id}/score",
|
|
234
|
+
response_model=QualityScoreResponse,
|
|
235
|
+
summary="Score validation rules for a source",
|
|
236
|
+
description="Evaluate quality of validation rules using F1 score, precision, recall, and accuracy",
|
|
237
|
+
)
|
|
238
|
+
async def score_source(
|
|
239
|
+
service: QualityReporterServiceDep,
|
|
240
|
+
source_id: Annotated[str, Path(description="Source ID to score")],
|
|
241
|
+
request: QualityScoreRequest | None = None,
|
|
242
|
+
) -> QualityScoreResponse:
|
|
243
|
+
"""Score validation rules for a source.
|
|
244
|
+
|
|
245
|
+
This endpoint evaluates the quality of validation rules by:
|
|
246
|
+
- Computing F1 score, precision, recall, and accuracy
|
|
247
|
+
- Assigning quality levels (excellent, good, acceptable, poor, unacceptable)
|
|
248
|
+
- Generating recommendations for rule usage
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
service: Quality reporter service.
|
|
252
|
+
source_id: Source ID to score.
|
|
253
|
+
request: Optional scoring configuration.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Quality score result with metrics and statistics.
|
|
257
|
+
"""
|
|
258
|
+
request = request or QualityScoreRequest()
|
|
259
|
+
thresholds = _request_to_thresholds(request.thresholds)
|
|
260
|
+
|
|
261
|
+
result = await service.score_source(
|
|
262
|
+
source_id,
|
|
263
|
+
validation_id=request.validation_id,
|
|
264
|
+
rule_names=request.rule_names,
|
|
265
|
+
sample_size=request.sample_size,
|
|
266
|
+
thresholds=thresholds,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Convert internal result to response
|
|
270
|
+
scores = [_convert_score_to_response(s.to_dict()) for s in result.scores]
|
|
271
|
+
statistics = _convert_statistics_to_response(
|
|
272
|
+
result.statistics.to_dict() if result.statistics else None
|
|
273
|
+
)
|
|
274
|
+
distribution = _convert_distribution_to_response(
|
|
275
|
+
[d.to_dict() for d in result.level_distribution] if result.level_distribution else None
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return QualityScoreResponse(
|
|
279
|
+
id=result.id,
|
|
280
|
+
source_id=result.source_id,
|
|
281
|
+
source_name=result.source_name,
|
|
282
|
+
validation_id=result.validation_id,
|
|
283
|
+
status=QualityReportStatusSchema(result.status.value),
|
|
284
|
+
scores=scores,
|
|
285
|
+
statistics=statistics,
|
|
286
|
+
level_distribution=distribution,
|
|
287
|
+
sample_size=result.sample_size,
|
|
288
|
+
evaluation_time_ms=result.evaluation_time_ms,
|
|
289
|
+
error_message=result.error_message,
|
|
290
|
+
created_at=result.created_at,
|
|
291
|
+
updated_at=result.updated_at,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@router.post(
|
|
296
|
+
"/sources/{source_id}/report",
|
|
297
|
+
response_model=QualityReportResponse,
|
|
298
|
+
summary="Generate quality report",
|
|
299
|
+
description="Generate a comprehensive quality report in various formats (HTML, JSON, Markdown, etc.)",
|
|
300
|
+
)
|
|
301
|
+
async def generate_report(
|
|
302
|
+
service: QualityReporterServiceDep,
|
|
303
|
+
source_id: Annotated[str, Path(description="Source ID for the report")],
|
|
304
|
+
request: QualityReportGenerateRequest | None = None,
|
|
305
|
+
) -> QualityReportResponse:
|
|
306
|
+
"""Generate a quality report for a source.
|
|
307
|
+
|
|
308
|
+
Generates a comprehensive quality report that includes:
|
|
309
|
+
- Quality scores for all validation rules
|
|
310
|
+
- Aggregate statistics and level distribution
|
|
311
|
+
- Visual charts (HTML format)
|
|
312
|
+
- Recommendations
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
service: Quality reporter service.
|
|
316
|
+
source_id: Source ID for the report.
|
|
317
|
+
request: Report generation configuration.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Generated report metadata.
|
|
321
|
+
"""
|
|
322
|
+
request = request or QualityReportGenerateRequest()
|
|
323
|
+
|
|
324
|
+
# Convert format enum
|
|
325
|
+
from truthound_dashboard.core.quality_reporter import (
|
|
326
|
+
QualityReportFormat as InternalFormat,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
format_value = request.format.value if request.format else "html"
|
|
330
|
+
internal_format = InternalFormat(format_value)
|
|
331
|
+
|
|
332
|
+
# Convert config and filter
|
|
333
|
+
config = _request_to_config(request.config)
|
|
334
|
+
filter_config = _request_to_filter(request.filter) if request.filter else None
|
|
335
|
+
|
|
336
|
+
result = await service.generate_report(
|
|
337
|
+
source_id=source_id,
|
|
338
|
+
validation_id=request.validation_id,
|
|
339
|
+
format=internal_format,
|
|
340
|
+
config=config,
|
|
341
|
+
filter_config=filter_config,
|
|
342
|
+
score_rules=request.score_rules,
|
|
343
|
+
sample_size=request.sample_size,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
statistics = _convert_statistics_to_response(
|
|
347
|
+
result.statistics.to_dict() if result.statistics else None
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return QualityReportResponse(
|
|
351
|
+
id=result.id,
|
|
352
|
+
source_id=result.source_id,
|
|
353
|
+
source_name=result.source_name,
|
|
354
|
+
validation_id=result.validation_id,
|
|
355
|
+
format=QualityReportFormatSchema(result.format.value),
|
|
356
|
+
status=QualityReportStatusSchema(result.status.value),
|
|
357
|
+
filename=result.filename,
|
|
358
|
+
file_path=result.file_path,
|
|
359
|
+
file_size_bytes=result.file_size_bytes,
|
|
360
|
+
content_type=result.content_type,
|
|
361
|
+
generation_time_ms=result.generation_time_ms,
|
|
362
|
+
scores_count=result.scores_count,
|
|
363
|
+
statistics=statistics,
|
|
364
|
+
error_message=result.error_message,
|
|
365
|
+
download_count=result.download_count,
|
|
366
|
+
expires_at=result.expires_at,
|
|
367
|
+
created_at=result.created_at,
|
|
368
|
+
updated_at=result.updated_at,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@router.get(
|
|
373
|
+
"/sources/{source_id}/report/download",
|
|
374
|
+
summary="Download quality report",
|
|
375
|
+
description="Download the generated quality report content",
|
|
376
|
+
)
|
|
377
|
+
async def download_report(
|
|
378
|
+
service: QualityReporterServiceDep,
|
|
379
|
+
source_id: Annotated[str, Path(description="Source ID")],
|
|
380
|
+
format: Annotated[
|
|
381
|
+
QualityReportFormatSchema,
|
|
382
|
+
Query(description="Report format"),
|
|
383
|
+
] = QualityReportFormatSchema.HTML,
|
|
384
|
+
title: Annotated[str | None, Query(description="Report title")] = None,
|
|
385
|
+
include_charts: Annotated[
|
|
386
|
+
bool, Query(description="Include charts (HTML only)")
|
|
387
|
+
] = True,
|
|
388
|
+
theme: Annotated[
|
|
389
|
+
str, Query(description="Report theme")
|
|
390
|
+
] = "professional",
|
|
391
|
+
max_scores: Annotated[
|
|
392
|
+
int | None, Query(description="Maximum scores to include", ge=1)
|
|
393
|
+
] = None,
|
|
394
|
+
) -> Response:
|
|
395
|
+
"""Download quality report as file.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
service: Quality reporter service.
|
|
399
|
+
source_id: Source ID.
|
|
400
|
+
format: Report format.
|
|
401
|
+
title: Optional report title.
|
|
402
|
+
include_charts: Include charts in HTML reports.
|
|
403
|
+
theme: Report theme.
|
|
404
|
+
max_scores: Maximum scores to include.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Report file content.
|
|
408
|
+
"""
|
|
409
|
+
from truthound_dashboard.core.quality_reporter import (
|
|
410
|
+
QualityReportFormat as InternalFormat,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
config = QualityReportConfig(
|
|
414
|
+
title=title,
|
|
415
|
+
include_charts=include_charts,
|
|
416
|
+
theme=theme,
|
|
417
|
+
max_scores=max_scores,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
internal_format = InternalFormat(format.value)
|
|
421
|
+
result = await service.generate_report(
|
|
422
|
+
source_id=source_id,
|
|
423
|
+
format=internal_format,
|
|
424
|
+
config=config,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if result.status != QualityReportStatus.COMPLETED or not result.content:
|
|
428
|
+
raise HTTPException(
|
|
429
|
+
status_code=500,
|
|
430
|
+
detail=result.error_message or "Failed to generate report",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Determine media type
|
|
434
|
+
media_types = {
|
|
435
|
+
QualityReportFormatSchema.CONSOLE: "text/plain",
|
|
436
|
+
QualityReportFormatSchema.JSON: "application/json",
|
|
437
|
+
QualityReportFormatSchema.HTML: "text/html",
|
|
438
|
+
QualityReportFormatSchema.MARKDOWN: "text/markdown",
|
|
439
|
+
QualityReportFormatSchema.JUNIT: "application/xml",
|
|
440
|
+
}
|
|
441
|
+
media_type = media_types.get(format, "text/plain")
|
|
442
|
+
|
|
443
|
+
return Response(
|
|
444
|
+
content=result.content,
|
|
445
|
+
media_type=media_type,
|
|
446
|
+
headers={
|
|
447
|
+
"Content-Disposition": f'attachment; filename="{result.filename}"',
|
|
448
|
+
},
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@router.get(
|
|
453
|
+
"/sources/{source_id}/report/preview",
|
|
454
|
+
summary="Preview quality report",
|
|
455
|
+
description="Preview the quality report content inline",
|
|
456
|
+
)
|
|
457
|
+
async def preview_report(
|
|
458
|
+
service: QualityReporterServiceDep,
|
|
459
|
+
source_id: Annotated[str, Path(description="Source ID")],
|
|
460
|
+
format: Annotated[
|
|
461
|
+
QualityReportFormatSchema,
|
|
462
|
+
Query(description="Report format"),
|
|
463
|
+
] = QualityReportFormatSchema.HTML,
|
|
464
|
+
theme: Annotated[str, Query(description="Report theme")] = "professional",
|
|
465
|
+
max_scores: Annotated[
|
|
466
|
+
int | None, Query(description="Maximum scores", ge=1)
|
|
467
|
+
] = 20,
|
|
468
|
+
) -> Response:
|
|
469
|
+
"""Preview quality report inline.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
service: Quality reporter service.
|
|
473
|
+
source_id: Source ID.
|
|
474
|
+
format: Report format.
|
|
475
|
+
theme: Report theme.
|
|
476
|
+
max_scores: Maximum scores to include.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Report content for inline display.
|
|
480
|
+
"""
|
|
481
|
+
from truthound_dashboard.core.quality_reporter import (
|
|
482
|
+
QualityReportFormat as InternalFormat,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
config = QualityReportConfig(
|
|
486
|
+
theme=theme,
|
|
487
|
+
max_scores=max_scores,
|
|
488
|
+
include_charts=True,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
internal_format = InternalFormat(format.value)
|
|
492
|
+
result = await service.generate_report(
|
|
493
|
+
source_id=source_id,
|
|
494
|
+
format=internal_format,
|
|
495
|
+
config=config,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if result.status != QualityReportStatus.COMPLETED or not result.content:
|
|
499
|
+
raise HTTPException(
|
|
500
|
+
status_code=500,
|
|
501
|
+
detail=result.error_message or "Failed to generate report",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
media_types = {
|
|
505
|
+
QualityReportFormatSchema.CONSOLE: "text/plain",
|
|
506
|
+
QualityReportFormatSchema.JSON: "application/json",
|
|
507
|
+
QualityReportFormatSchema.HTML: "text/html",
|
|
508
|
+
QualityReportFormatSchema.MARKDOWN: "text/markdown",
|
|
509
|
+
QualityReportFormatSchema.JUNIT: "application/xml",
|
|
510
|
+
}
|
|
511
|
+
media_type = media_types.get(format, "text/plain")
|
|
512
|
+
|
|
513
|
+
return Response(content=result.content, media_type=media_type)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@router.get(
|
|
517
|
+
"/sources/{source_id}/summary",
|
|
518
|
+
response_model=QualitySummaryResponse,
|
|
519
|
+
summary="Get quality summary",
|
|
520
|
+
description="Get a summary of quality scores for a source",
|
|
521
|
+
)
|
|
522
|
+
async def get_summary(
|
|
523
|
+
service: QualityReporterServiceDep,
|
|
524
|
+
source_id: Annotated[str, Path(description="Source ID")],
|
|
525
|
+
validation_id: Annotated[
|
|
526
|
+
str | None, Query(description="Validation ID")
|
|
527
|
+
] = None,
|
|
528
|
+
sample_size: Annotated[
|
|
529
|
+
int, Query(description="Sample size", ge=100, le=1000000)
|
|
530
|
+
] = 10000,
|
|
531
|
+
) -> QualitySummaryResponse:
|
|
532
|
+
"""Get quality summary for a source.
|
|
533
|
+
|
|
534
|
+
Returns aggregate statistics including:
|
|
535
|
+
- Total rules scored
|
|
536
|
+
- Quality level distribution
|
|
537
|
+
- Recommendations summary
|
|
538
|
+
- Metric averages (F1, precision, recall, confidence)
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
service: Quality reporter service.
|
|
542
|
+
source_id: Source ID.
|
|
543
|
+
validation_id: Optional validation ID.
|
|
544
|
+
sample_size: Sample size for scoring.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
Quality summary.
|
|
548
|
+
"""
|
|
549
|
+
summary = await service.get_summary(
|
|
550
|
+
source_id,
|
|
551
|
+
validation_id=validation_id,
|
|
552
|
+
sample_size=sample_size,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
if "error" in summary:
|
|
556
|
+
raise HTTPException(status_code=500, detail=summary["error"])
|
|
557
|
+
|
|
558
|
+
statistics = _convert_statistics_to_response(summary["statistics"])
|
|
559
|
+
if not statistics:
|
|
560
|
+
statistics = QualityStatisticsSchema()
|
|
561
|
+
|
|
562
|
+
distribution = [
|
|
563
|
+
QualityLevelDistribution(
|
|
564
|
+
level=QualityLevel(d["level"]),
|
|
565
|
+
count=d["count"],
|
|
566
|
+
percentage=d["percentage"],
|
|
567
|
+
)
|
|
568
|
+
for d in summary.get("level_distribution", [])
|
|
569
|
+
]
|
|
570
|
+
|
|
571
|
+
return QualitySummaryResponse(
|
|
572
|
+
total_rules=summary["total_rules"],
|
|
573
|
+
statistics=statistics,
|
|
574
|
+
level_distribution=distribution,
|
|
575
|
+
recommendations=summary["recommendations"],
|
|
576
|
+
metric_averages=summary["metric_averages"],
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@router.post(
|
|
581
|
+
"/compare",
|
|
582
|
+
response_model=QualityCompareResponse,
|
|
583
|
+
summary="Compare quality scores",
|
|
584
|
+
description="Compare and rank quality scores across sources",
|
|
585
|
+
)
|
|
586
|
+
async def compare_scores(
|
|
587
|
+
service: QualityReporterServiceDep,
|
|
588
|
+
request: QualityCompareRequest,
|
|
589
|
+
) -> QualityCompareResponse:
|
|
590
|
+
"""Compare quality scores across sources.
|
|
591
|
+
|
|
592
|
+
Compares and ranks quality scores by specified metrics.
|
|
593
|
+
Can optionally group results by column, level, or rule type.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
service: Quality reporter service.
|
|
597
|
+
request: Comparison configuration.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Comparison result with ranked scores.
|
|
601
|
+
"""
|
|
602
|
+
# Get scores from sources
|
|
603
|
+
all_scores = []
|
|
604
|
+
|
|
605
|
+
if request.source_ids:
|
|
606
|
+
for source_id in request.source_ids:
|
|
607
|
+
result = await service.score_source(source_id)
|
|
608
|
+
if result.status == QualityReportStatus.COMPLETED:
|
|
609
|
+
all_scores.extend(result.scores)
|
|
610
|
+
|
|
611
|
+
if not all_scores:
|
|
612
|
+
raise HTTPException(
|
|
613
|
+
status_code=400,
|
|
614
|
+
detail="No scores available for comparison",
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Compare scores
|
|
618
|
+
comparison = await service.compare_scores(
|
|
619
|
+
all_scores,
|
|
620
|
+
sort_by=request.sort_by,
|
|
621
|
+
descending=request.descending,
|
|
622
|
+
group_by=request.group_by,
|
|
623
|
+
max_results=request.max_results,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Convert to response
|
|
627
|
+
scores = [
|
|
628
|
+
_convert_score_to_response(s) for s in comparison.get("scores", [])
|
|
629
|
+
]
|
|
630
|
+
|
|
631
|
+
best_rule = None
|
|
632
|
+
if comparison.get("best_rule"):
|
|
633
|
+
best_rule = _convert_score_to_response(comparison["best_rule"])
|
|
634
|
+
|
|
635
|
+
worst_rule = None
|
|
636
|
+
if comparison.get("worst_rule"):
|
|
637
|
+
worst_rule = _convert_score_to_response(comparison["worst_rule"])
|
|
638
|
+
|
|
639
|
+
statistics = _convert_statistics_to_response(comparison.get("statistics"))
|
|
640
|
+
|
|
641
|
+
# Convert groups
|
|
642
|
+
groups = None
|
|
643
|
+
if comparison.get("groups"):
|
|
644
|
+
groups = {
|
|
645
|
+
key: [_convert_score_to_response(s) for s in group_scores]
|
|
646
|
+
for key, group_scores in comparison["groups"].items()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return QualityCompareResponse(
|
|
650
|
+
scores=scores,
|
|
651
|
+
ranked_by=comparison["ranked_by"],
|
|
652
|
+
best_rule=best_rule,
|
|
653
|
+
worst_rule=worst_rule,
|
|
654
|
+
groups=groups,
|
|
655
|
+
statistics=statistics,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
@router.post(
|
|
660
|
+
"/filter",
|
|
661
|
+
response_model=list[QualityScoreSchema],
|
|
662
|
+
summary="Filter quality scores",
|
|
663
|
+
description="Filter quality scores by various criteria",
|
|
664
|
+
)
|
|
665
|
+
async def filter_scores(
|
|
666
|
+
service: QualityReporterServiceDep,
|
|
667
|
+
source_id: Annotated[str, Query(description="Source ID to filter scores from")],
|
|
668
|
+
request: QualityFilterRequest,
|
|
669
|
+
) -> list[QualityScoreSchema]:
|
|
670
|
+
"""Filter quality scores by criteria.
|
|
671
|
+
|
|
672
|
+
Available filters:
|
|
673
|
+
- Quality level (min/max)
|
|
674
|
+
- F1 score range
|
|
675
|
+
- Confidence threshold
|
|
676
|
+
- Specific columns
|
|
677
|
+
- Rule types
|
|
678
|
+
- Should-use recommendation
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
service: Quality reporter service.
|
|
682
|
+
source_id: Source ID to get scores from.
|
|
683
|
+
request: Filter configuration.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
Filtered quality scores.
|
|
687
|
+
"""
|
|
688
|
+
# Get scores first
|
|
689
|
+
result = await service.score_source(source_id)
|
|
690
|
+
|
|
691
|
+
if result.status != QualityReportStatus.COMPLETED:
|
|
692
|
+
raise HTTPException(
|
|
693
|
+
status_code=500,
|
|
694
|
+
detail=result.error_message or "Failed to score source",
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Apply filter
|
|
698
|
+
filter_config = _request_to_filter(request)
|
|
699
|
+
filtered = await service.filter_scores(result.scores, filter_config)
|
|
700
|
+
|
|
701
|
+
return [_convert_score_to_response(s.to_dict()) for s in filtered]
|