local-deep-research 0.4.4__py3-none-any.whl → 0.5.2__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.
- local_deep_research/__init__.py +7 -0
- local_deep_research/__version__.py +1 -1
- local_deep_research/advanced_search_system/answer_decoding/__init__.py +5 -0
- local_deep_research/advanced_search_system/answer_decoding/browsecomp_answer_decoder.py +421 -0
- local_deep_research/advanced_search_system/candidate_exploration/README.md +219 -0
- local_deep_research/advanced_search_system/candidate_exploration/__init__.py +25 -0
- local_deep_research/advanced_search_system/candidate_exploration/adaptive_explorer.py +329 -0
- local_deep_research/advanced_search_system/candidate_exploration/base_explorer.py +341 -0
- local_deep_research/advanced_search_system/candidate_exploration/constraint_guided_explorer.py +436 -0
- local_deep_research/advanced_search_system/candidate_exploration/diversity_explorer.py +457 -0
- local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py +250 -0
- local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +255 -0
- local_deep_research/advanced_search_system/candidates/__init__.py +5 -0
- local_deep_research/advanced_search_system/candidates/base_candidate.py +59 -0
- local_deep_research/advanced_search_system/constraint_checking/README.md +150 -0
- local_deep_research/advanced_search_system/constraint_checking/__init__.py +35 -0
- local_deep_research/advanced_search_system/constraint_checking/base_constraint_checker.py +122 -0
- local_deep_research/advanced_search_system/constraint_checking/constraint_checker.py +223 -0
- local_deep_research/advanced_search_system/constraint_checking/constraint_satisfaction_tracker.py +387 -0
- local_deep_research/advanced_search_system/constraint_checking/dual_confidence_checker.py +424 -0
- local_deep_research/advanced_search_system/constraint_checking/evidence_analyzer.py +174 -0
- local_deep_research/advanced_search_system/constraint_checking/intelligent_constraint_relaxer.py +503 -0
- local_deep_research/advanced_search_system/constraint_checking/rejection_engine.py +143 -0
- local_deep_research/advanced_search_system/constraint_checking/strict_checker.py +259 -0
- local_deep_research/advanced_search_system/constraint_checking/threshold_checker.py +213 -0
- local_deep_research/advanced_search_system/constraints/__init__.py +6 -0
- local_deep_research/advanced_search_system/constraints/base_constraint.py +58 -0
- local_deep_research/advanced_search_system/constraints/constraint_analyzer.py +143 -0
- local_deep_research/advanced_search_system/evidence/__init__.py +12 -0
- local_deep_research/advanced_search_system/evidence/base_evidence.py +57 -0
- local_deep_research/advanced_search_system/evidence/evaluator.py +159 -0
- local_deep_research/advanced_search_system/evidence/requirements.py +122 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +3 -1
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +8 -2
- local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +43 -29
- local_deep_research/advanced_search_system/findings/repository.py +54 -17
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +3 -1
- local_deep_research/advanced_search_system/query_generation/adaptive_query_generator.py +405 -0
- local_deep_research/advanced_search_system/questions/__init__.py +16 -0
- local_deep_research/advanced_search_system/questions/atomic_fact_question.py +171 -0
- local_deep_research/advanced_search_system/questions/browsecomp_question.py +287 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +13 -4
- local_deep_research/advanced_search_system/questions/entity_aware_question.py +184 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +9 -3
- local_deep_research/advanced_search_system/search_optimization/cross_constraint_manager.py +624 -0
- local_deep_research/advanced_search_system/source_management/diversity_manager.py +613 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +42 -0
- local_deep_research/advanced_search_system/strategies/adaptive_decomposition_strategy.py +564 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +4 -4
- local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py +1031 -0
- local_deep_research/advanced_search_system/strategies/browsecomp_optimized_strategy.py +778 -0
- local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py +446 -0
- local_deep_research/advanced_search_system/strategies/constrained_search_strategy.py +1348 -0
- local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py +522 -0
- local_deep_research/advanced_search_system/strategies/direct_search_strategy.py +217 -0
- local_deep_research/advanced_search_system/strategies/dual_confidence_strategy.py +320 -0
- local_deep_research/advanced_search_system/strategies/dual_confidence_with_rejection.py +219 -0
- local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py +369 -0
- local_deep_research/advanced_search_system/strategies/entity_aware_source_strategy.py +140 -0
- local_deep_research/advanced_search_system/strategies/evidence_based_strategy.py +1248 -0
- local_deep_research/advanced_search_system/strategies/evidence_based_strategy_v2.py +1337 -0
- local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +537 -0
- local_deep_research/advanced_search_system/strategies/improved_evidence_based_strategy.py +782 -0
- local_deep_research/advanced_search_system/strategies/iterative_reasoning_strategy.py +760 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +55 -21
- local_deep_research/advanced_search_system/strategies/llm_driven_modular_strategy.py +865 -0
- local_deep_research/advanced_search_system/strategies/modular_strategy.py +1142 -0
- local_deep_research/advanced_search_system/strategies/parallel_constrained_strategy.py +506 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +34 -16
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +29 -9
- local_deep_research/advanced_search_system/strategies/recursive_decomposition_strategy.py +492 -0
- local_deep_research/advanced_search_system/strategies/smart_decomposition_strategy.py +284 -0
- local_deep_research/advanced_search_system/strategies/smart_query_strategy.py +515 -0
- local_deep_research/advanced_search_system/strategies/source_based_strategy.py +48 -24
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +34 -14
- local_deep_research/advanced_search_system/tools/base_tool.py +7 -2
- local_deep_research/api/benchmark_functions.py +6 -2
- local_deep_research/api/research_functions.py +10 -4
- local_deep_research/benchmarks/__init__.py +9 -7
- local_deep_research/benchmarks/benchmark_functions.py +6 -2
- local_deep_research/benchmarks/cli/benchmark_commands.py +27 -10
- local_deep_research/benchmarks/cli.py +38 -13
- local_deep_research/benchmarks/comparison/__init__.py +4 -2
- local_deep_research/benchmarks/comparison/evaluator.py +316 -239
- local_deep_research/benchmarks/datasets/__init__.py +1 -1
- local_deep_research/benchmarks/datasets/base.py +91 -72
- local_deep_research/benchmarks/datasets/browsecomp.py +54 -33
- local_deep_research/benchmarks/datasets/custom_dataset_template.py +19 -19
- local_deep_research/benchmarks/datasets/simpleqa.py +14 -14
- local_deep_research/benchmarks/datasets/utils.py +48 -29
- local_deep_research/benchmarks/datasets.py +4 -11
- local_deep_research/benchmarks/efficiency/__init__.py +8 -4
- local_deep_research/benchmarks/efficiency/resource_monitor.py +223 -171
- local_deep_research/benchmarks/efficiency/speed_profiler.py +62 -48
- local_deep_research/benchmarks/evaluators/browsecomp.py +3 -1
- local_deep_research/benchmarks/evaluators/composite.py +6 -2
- local_deep_research/benchmarks/evaluators/simpleqa.py +36 -13
- local_deep_research/benchmarks/graders.py +32 -10
- local_deep_research/benchmarks/metrics/README.md +1 -1
- local_deep_research/benchmarks/metrics/calculation.py +25 -10
- local_deep_research/benchmarks/metrics/reporting.py +7 -3
- local_deep_research/benchmarks/metrics/visualization.py +42 -23
- local_deep_research/benchmarks/metrics.py +1 -1
- local_deep_research/benchmarks/optimization/__init__.py +3 -1
- local_deep_research/benchmarks/optimization/api.py +7 -1
- local_deep_research/benchmarks/optimization/optuna_optimizer.py +75 -26
- local_deep_research/benchmarks/runners.py +48 -15
- local_deep_research/citation_handler.py +65 -92
- local_deep_research/citation_handlers/__init__.py +15 -0
- local_deep_research/citation_handlers/base_citation_handler.py +70 -0
- local_deep_research/citation_handlers/forced_answer_citation_handler.py +179 -0
- local_deep_research/citation_handlers/precision_extraction_handler.py +550 -0
- local_deep_research/citation_handlers/standard_citation_handler.py +80 -0
- local_deep_research/config/llm_config.py +271 -169
- local_deep_research/config/search_config.py +14 -5
- local_deep_research/defaults/__init__.py +0 -1
- local_deep_research/metrics/__init__.py +13 -0
- local_deep_research/metrics/database.py +58 -0
- local_deep_research/metrics/db_models.py +115 -0
- local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
- local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
- local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
- local_deep_research/metrics/migrate_research_ratings.py +31 -0
- local_deep_research/metrics/models.py +61 -0
- local_deep_research/metrics/pricing/__init__.py +12 -0
- local_deep_research/metrics/pricing/cost_calculator.py +237 -0
- local_deep_research/metrics/pricing/pricing_cache.py +143 -0
- local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
- local_deep_research/metrics/query_utils.py +51 -0
- local_deep_research/metrics/search_tracker.py +380 -0
- local_deep_research/metrics/token_counter.py +1078 -0
- local_deep_research/migrate_db.py +3 -1
- local_deep_research/report_generator.py +22 -8
- local_deep_research/search_system.py +390 -9
- local_deep_research/test_migration.py +15 -5
- local_deep_research/utilities/db_utils.py +7 -4
- local_deep_research/utilities/es_utils.py +115 -104
- local_deep_research/utilities/llm_utils.py +15 -5
- local_deep_research/utilities/log_utils.py +151 -0
- local_deep_research/utilities/search_cache.py +387 -0
- local_deep_research/utilities/search_utilities.py +14 -6
- local_deep_research/utilities/threading_utils.py +92 -0
- local_deep_research/utilities/url_utils.py +6 -0
- local_deep_research/web/api.py +347 -0
- local_deep_research/web/app.py +13 -17
- local_deep_research/web/app_factory.py +71 -66
- local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
- local_deep_research/web/database/migrations.py +20 -3
- local_deep_research/web/database/models.py +74 -25
- local_deep_research/web/database/schema_upgrade.py +49 -29
- local_deep_research/web/models/database.py +63 -83
- local_deep_research/web/routes/api_routes.py +56 -22
- local_deep_research/web/routes/benchmark_routes.py +4 -1
- local_deep_research/web/routes/globals.py +22 -0
- local_deep_research/web/routes/history_routes.py +71 -46
- local_deep_research/web/routes/metrics_routes.py +1155 -0
- local_deep_research/web/routes/research_routes.py +192 -54
- local_deep_research/web/routes/settings_routes.py +156 -55
- local_deep_research/web/services/research_service.py +412 -251
- local_deep_research/web/services/resource_service.py +36 -11
- local_deep_research/web/services/settings_manager.py +55 -17
- local_deep_research/web/services/settings_service.py +12 -4
- local_deep_research/web/services/socket_service.py +295 -188
- local_deep_research/web/static/css/custom_dropdown.css +180 -0
- local_deep_research/web/static/css/styles.css +39 -1
- local_deep_research/web/static/js/components/detail.js +633 -267
- local_deep_research/web/static/js/components/details.js +751 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
- local_deep_research/web/static/js/components/fallback/ui.js +23 -23
- local_deep_research/web/static/js/components/history.js +76 -76
- local_deep_research/web/static/js/components/logpanel.js +61 -13
- local_deep_research/web/static/js/components/progress.js +13 -2
- local_deep_research/web/static/js/components/research.js +99 -12
- local_deep_research/web/static/js/components/results.js +239 -106
- local_deep_research/web/static/js/main.js +40 -40
- local_deep_research/web/static/js/services/audio.js +1 -1
- local_deep_research/web/static/js/services/formatting.js +11 -11
- local_deep_research/web/static/js/services/keyboard.js +157 -0
- local_deep_research/web/static/js/services/pdf.js +80 -80
- local_deep_research/web/static/sounds/README.md +1 -1
- local_deep_research/web/templates/base.html +1 -0
- local_deep_research/web/templates/components/log_panel.html +7 -1
- local_deep_research/web/templates/components/mobile_nav.html +1 -1
- local_deep_research/web/templates/components/sidebar.html +3 -0
- local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
- local_deep_research/web/templates/pages/details.html +325 -24
- local_deep_research/web/templates/pages/history.html +1 -1
- local_deep_research/web/templates/pages/metrics.html +1929 -0
- local_deep_research/web/templates/pages/progress.html +2 -2
- local_deep_research/web/templates/pages/research.html +53 -17
- local_deep_research/web/templates/pages/results.html +12 -1
- local_deep_research/web/templates/pages/star_reviews.html +803 -0
- local_deep_research/web/utils/formatters.py +9 -3
- local_deep_research/web_search_engines/default_search_engines.py +5 -3
- local_deep_research/web_search_engines/engines/full_search.py +8 -2
- local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
- local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
- local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
- local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
- local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
- local_deep_research/web_search_engines/search_engine_base.py +83 -35
- local_deep_research/web_search_engines/search_engine_factory.py +25 -8
- local_deep_research/web_search_engines/search_engines_config.py +9 -3
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/METADATA +7 -1
- local_deep_research-0.5.2.dist-info/RECORD +265 -0
- local_deep_research-0.4.4.dist-info/RECORD +0 -177
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/WHEEL +0 -0
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/entry_points.txt +0 -0
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1155 @@
|
|
1
|
+
"""Routes for metrics dashboard."""
|
2
|
+
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
|
5
|
+
from flask import Blueprint, jsonify, request
|
6
|
+
from loguru import logger
|
7
|
+
from sqlalchemy import case, func
|
8
|
+
|
9
|
+
from ...metrics import TokenCounter
|
10
|
+
from ...metrics.db_models import ResearchRating, TokenUsage
|
11
|
+
from ...metrics.query_utils import get_time_filter_condition
|
12
|
+
from ...metrics.search_tracker import get_search_tracker
|
13
|
+
from ...utilities.db_utils import get_db_session
|
14
|
+
from ..database.models import Research, ResearchStrategy
|
15
|
+
from ..utils.templates import render_template_with_defaults
|
16
|
+
|
17
|
+
# Create a Blueprint for metrics
|
18
|
+
metrics_bp = Blueprint("metrics", __name__, url_prefix="/metrics")
|
19
|
+
|
20
|
+
|
21
|
+
def get_rating_analytics(period="30d", research_mode="all"):
|
22
|
+
"""Get rating analytics for the specified period and research mode."""
|
23
|
+
try:
|
24
|
+
from ...metrics.database import get_metrics_db
|
25
|
+
from ...metrics.db_models import ResearchRating
|
26
|
+
|
27
|
+
db = get_metrics_db()
|
28
|
+
|
29
|
+
# Calculate date range
|
30
|
+
days_map = {"7d": 7, "30d": 30, "90d": 90, "365d": 365, "all": None}
|
31
|
+
|
32
|
+
days = days_map.get(period, 30)
|
33
|
+
|
34
|
+
with db.get_session() as session:
|
35
|
+
query = session.query(ResearchRating)
|
36
|
+
|
37
|
+
# Apply time filter
|
38
|
+
if days:
|
39
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
40
|
+
query = query.filter(ResearchRating.rated_at >= cutoff_date)
|
41
|
+
|
42
|
+
# Get all ratings
|
43
|
+
ratings = query.all()
|
44
|
+
|
45
|
+
if not ratings:
|
46
|
+
return {
|
47
|
+
"rating_analytics": {
|
48
|
+
"avg_rating": None,
|
49
|
+
"total_ratings": 0,
|
50
|
+
"rating_distribution": {},
|
51
|
+
"satisfaction_stats": {
|
52
|
+
"very_satisfied": 0, # 5 stars
|
53
|
+
"satisfied": 0, # 4 stars
|
54
|
+
"neutral": 0, # 3 stars
|
55
|
+
"dissatisfied": 0, # 2 stars
|
56
|
+
"very_dissatisfied": 0, # 1 star
|
57
|
+
},
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
# Calculate statistics
|
62
|
+
rating_values = [r.rating for r in ratings]
|
63
|
+
avg_rating = sum(rating_values) / len(rating_values)
|
64
|
+
|
65
|
+
# Rating distribution
|
66
|
+
rating_counts = {}
|
67
|
+
for i in range(1, 6):
|
68
|
+
rating_counts[str(i)] = rating_values.count(i)
|
69
|
+
|
70
|
+
# Satisfaction categories
|
71
|
+
satisfaction_stats = {
|
72
|
+
"very_satisfied": rating_values.count(5),
|
73
|
+
"satisfied": rating_values.count(4),
|
74
|
+
"neutral": rating_values.count(3),
|
75
|
+
"dissatisfied": rating_values.count(2),
|
76
|
+
"very_dissatisfied": rating_values.count(1),
|
77
|
+
}
|
78
|
+
|
79
|
+
return {
|
80
|
+
"rating_analytics": {
|
81
|
+
"avg_rating": round(avg_rating, 1),
|
82
|
+
"total_ratings": len(ratings),
|
83
|
+
"rating_distribution": rating_counts,
|
84
|
+
"satisfaction_stats": satisfaction_stats,
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
except Exception as e:
|
89
|
+
logger.exception(f"Error getting rating analytics: {e}")
|
90
|
+
return {
|
91
|
+
"rating_analytics": {
|
92
|
+
"avg_rating": None,
|
93
|
+
"total_ratings": 0,
|
94
|
+
"rating_distribution": {},
|
95
|
+
"satisfaction_stats": {
|
96
|
+
"very_satisfied": 0,
|
97
|
+
"satisfied": 0,
|
98
|
+
"neutral": 0,
|
99
|
+
"dissatisfied": 0,
|
100
|
+
"very_dissatisfied": 0,
|
101
|
+
},
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
|
106
|
+
def get_available_strategies():
|
107
|
+
"""Get list of all available search strategies from the search system."""
|
108
|
+
# This list comes from the AdvancedSearchSystem.__init__ method
|
109
|
+
strategies = [
|
110
|
+
{"name": "standard", "description": "Basic iterative search strategy"},
|
111
|
+
{
|
112
|
+
"name": "iterdrag",
|
113
|
+
"description": "Iterative Dense Retrieval Augmented Generation",
|
114
|
+
},
|
115
|
+
{
|
116
|
+
"name": "source-based",
|
117
|
+
"description": "Focuses on finding and extracting from sources",
|
118
|
+
},
|
119
|
+
{
|
120
|
+
"name": "parallel",
|
121
|
+
"description": "Runs multiple search queries in parallel",
|
122
|
+
},
|
123
|
+
{"name": "rapid", "description": "Quick single-pass search"},
|
124
|
+
{
|
125
|
+
"name": "recursive",
|
126
|
+
"description": "Recursive decomposition of complex queries",
|
127
|
+
},
|
128
|
+
{
|
129
|
+
"name": "iterative",
|
130
|
+
"description": "Loop-based reasoning with persistent knowledge",
|
131
|
+
},
|
132
|
+
{"name": "adaptive", "description": "Adaptive step-by-step reasoning"},
|
133
|
+
{
|
134
|
+
"name": "smart",
|
135
|
+
"description": "Automatically chooses best strategy based on query",
|
136
|
+
},
|
137
|
+
{
|
138
|
+
"name": "browsecomp",
|
139
|
+
"description": "Optimized for BrowseComp-style puzzle queries",
|
140
|
+
},
|
141
|
+
{
|
142
|
+
"name": "evidence",
|
143
|
+
"description": "Enhanced evidence-based verification with improved candidate discovery",
|
144
|
+
},
|
145
|
+
{
|
146
|
+
"name": "constrained",
|
147
|
+
"description": "Progressive constraint-based search that narrows candidates step by step",
|
148
|
+
},
|
149
|
+
{
|
150
|
+
"name": "parallel-constrained",
|
151
|
+
"description": "Parallel constraint-based search with combined constraint execution",
|
152
|
+
},
|
153
|
+
{
|
154
|
+
"name": "early-stop-constrained",
|
155
|
+
"description": "Parallel constraint search with immediate evaluation and early stopping at 99% confidence",
|
156
|
+
},
|
157
|
+
{
|
158
|
+
"name": "smart-query",
|
159
|
+
"description": "Smart query generation strategy",
|
160
|
+
},
|
161
|
+
{
|
162
|
+
"name": "dual-confidence",
|
163
|
+
"description": "Dual confidence scoring with positive/negative/uncertainty",
|
164
|
+
},
|
165
|
+
{
|
166
|
+
"name": "dual-confidence-with-rejection",
|
167
|
+
"description": "Dual confidence with early rejection of poor candidates",
|
168
|
+
},
|
169
|
+
{
|
170
|
+
"name": "concurrent-dual-confidence",
|
171
|
+
"description": "Concurrent search & evaluation with progressive constraint relaxation",
|
172
|
+
},
|
173
|
+
{
|
174
|
+
"name": "modular",
|
175
|
+
"description": "Modular architecture using constraint checking and candidate exploration modules",
|
176
|
+
},
|
177
|
+
{
|
178
|
+
"name": "modular-parallel",
|
179
|
+
"description": "Modular strategy with parallel exploration",
|
180
|
+
},
|
181
|
+
{
|
182
|
+
"name": "focused-iteration",
|
183
|
+
"description": "Focused iteration strategy optimized for accuracy",
|
184
|
+
},
|
185
|
+
{
|
186
|
+
"name": "browsecomp-entity",
|
187
|
+
"description": "Entity-focused search for BrowseComp questions with knowledge graph building",
|
188
|
+
},
|
189
|
+
]
|
190
|
+
return strategies
|
191
|
+
|
192
|
+
|
193
|
+
def get_strategy_analytics(period="30d"):
|
194
|
+
"""Get strategy usage analytics for the specified period."""
|
195
|
+
try:
|
196
|
+
# Calculate date range
|
197
|
+
days_map = {"7d": 7, "30d": 30, "90d": 90, "365d": 365, "all": None}
|
198
|
+
days = days_map.get(period, 30)
|
199
|
+
|
200
|
+
session = get_db_session()
|
201
|
+
|
202
|
+
try:
|
203
|
+
# Check if we have any ResearchStrategy records
|
204
|
+
strategy_count = session.query(ResearchStrategy).count()
|
205
|
+
|
206
|
+
if strategy_count == 0:
|
207
|
+
logger.warning("No research strategies found in database")
|
208
|
+
return {
|
209
|
+
"strategy_analytics": {
|
210
|
+
"total_research_with_strategy": 0,
|
211
|
+
"total_research": 0,
|
212
|
+
"most_popular_strategy": None,
|
213
|
+
"strategy_usage": [],
|
214
|
+
"strategy_distribution": {},
|
215
|
+
"available_strategies": get_available_strategies(),
|
216
|
+
"message": "Strategy tracking not yet available - run a research to start tracking",
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
# Base query for strategy usage (no JOIN needed since we just want strategy counts)
|
221
|
+
query = session.query(
|
222
|
+
ResearchStrategy.strategy_name,
|
223
|
+
func.count(ResearchStrategy.id).label("usage_count"),
|
224
|
+
)
|
225
|
+
|
226
|
+
# Apply time filter if specified
|
227
|
+
if days:
|
228
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
229
|
+
query = query.filter(ResearchStrategy.created_at >= cutoff_date)
|
230
|
+
|
231
|
+
# Group by strategy and order by usage
|
232
|
+
strategy_results = (
|
233
|
+
query.group_by(ResearchStrategy.strategy_name)
|
234
|
+
.order_by(func.count(ResearchStrategy.id).desc())
|
235
|
+
.all()
|
236
|
+
)
|
237
|
+
|
238
|
+
# Get total strategy count for percentage calculation
|
239
|
+
total_query = session.query(ResearchStrategy)
|
240
|
+
if days:
|
241
|
+
total_query = total_query.filter(
|
242
|
+
ResearchStrategy.created_at >= cutoff_date
|
243
|
+
)
|
244
|
+
total_research = total_query.count()
|
245
|
+
|
246
|
+
finally:
|
247
|
+
session.close()
|
248
|
+
|
249
|
+
# Format strategy data
|
250
|
+
strategy_usage = []
|
251
|
+
strategy_distribution = {}
|
252
|
+
|
253
|
+
for strategy_name, usage_count in strategy_results:
|
254
|
+
percentage = (
|
255
|
+
(usage_count / total_research * 100)
|
256
|
+
if total_research > 0
|
257
|
+
else 0
|
258
|
+
)
|
259
|
+
strategy_usage.append(
|
260
|
+
{
|
261
|
+
"strategy": strategy_name,
|
262
|
+
"count": usage_count,
|
263
|
+
"percentage": round(percentage, 1),
|
264
|
+
}
|
265
|
+
)
|
266
|
+
strategy_distribution[strategy_name] = usage_count
|
267
|
+
|
268
|
+
# Find most popular strategy
|
269
|
+
most_popular = strategy_usage[0]["strategy"] if strategy_usage else None
|
270
|
+
|
271
|
+
return {
|
272
|
+
"strategy_analytics": {
|
273
|
+
"total_research_with_strategy": sum(
|
274
|
+
item["count"] for item in strategy_usage
|
275
|
+
),
|
276
|
+
"total_research": total_research,
|
277
|
+
"most_popular_strategy": most_popular,
|
278
|
+
"strategy_usage": strategy_usage,
|
279
|
+
"strategy_distribution": strategy_distribution,
|
280
|
+
"available_strategies": get_available_strategies(),
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
284
|
+
except Exception as e:
|
285
|
+
logger.exception(f"Error getting strategy analytics: {e}")
|
286
|
+
return {
|
287
|
+
"strategy_analytics": {
|
288
|
+
"total_research_with_strategy": 0,
|
289
|
+
"total_research": 0,
|
290
|
+
"most_popular_strategy": None,
|
291
|
+
"strategy_usage": [],
|
292
|
+
"strategy_distribution": {},
|
293
|
+
"available_strategies": get_available_strategies(),
|
294
|
+
"error": str(e),
|
295
|
+
}
|
296
|
+
}
|
297
|
+
|
298
|
+
|
299
|
+
@metrics_bp.route("/")
|
300
|
+
def metrics_dashboard():
|
301
|
+
"""Render the metrics dashboard page."""
|
302
|
+
return render_template_with_defaults("pages/metrics.html")
|
303
|
+
|
304
|
+
|
305
|
+
@metrics_bp.route("/api/metrics")
|
306
|
+
def api_metrics():
|
307
|
+
"""Get overall metrics data."""
|
308
|
+
try:
|
309
|
+
# Get time period and research mode from query parameters
|
310
|
+
period = request.args.get("period", "30d")
|
311
|
+
research_mode = request.args.get("mode", "all")
|
312
|
+
|
313
|
+
token_counter = TokenCounter()
|
314
|
+
search_tracker = get_search_tracker()
|
315
|
+
|
316
|
+
# Get both token and search metrics
|
317
|
+
token_metrics = token_counter.get_overall_metrics(
|
318
|
+
period=period, research_mode=research_mode
|
319
|
+
)
|
320
|
+
search_metrics = search_tracker.get_search_metrics(
|
321
|
+
period=period, research_mode=research_mode
|
322
|
+
)
|
323
|
+
|
324
|
+
# Get user satisfaction rating data
|
325
|
+
try:
|
326
|
+
from sqlalchemy import func
|
327
|
+
|
328
|
+
from ...metrics.db_models import ResearchRating
|
329
|
+
|
330
|
+
with get_db_session() as session:
|
331
|
+
# Build base query with time filter
|
332
|
+
ratings_query = session.query(ResearchRating)
|
333
|
+
time_condition = get_time_filter_condition(
|
334
|
+
period, ResearchRating.rated_at
|
335
|
+
)
|
336
|
+
if time_condition is not None:
|
337
|
+
ratings_query = ratings_query.filter(time_condition)
|
338
|
+
|
339
|
+
# Get average rating
|
340
|
+
avg_rating = ratings_query.with_entities(
|
341
|
+
func.avg(ResearchRating.rating).label("avg_rating")
|
342
|
+
).scalar()
|
343
|
+
|
344
|
+
# Get total rating count
|
345
|
+
total_ratings = ratings_query.count()
|
346
|
+
|
347
|
+
user_satisfaction = {
|
348
|
+
"avg_rating": round(avg_rating, 1) if avg_rating else None,
|
349
|
+
"total_ratings": total_ratings,
|
350
|
+
}
|
351
|
+
except Exception as e:
|
352
|
+
logger.warning(f"Error getting user satisfaction data: {e}")
|
353
|
+
user_satisfaction = {"avg_rating": None, "total_ratings": 0}
|
354
|
+
|
355
|
+
# Get strategy analytics
|
356
|
+
strategy_data = get_strategy_analytics(period)
|
357
|
+
|
358
|
+
# Combine metrics
|
359
|
+
combined_metrics = {
|
360
|
+
**token_metrics,
|
361
|
+
**search_metrics,
|
362
|
+
**strategy_data,
|
363
|
+
"user_satisfaction": user_satisfaction,
|
364
|
+
}
|
365
|
+
|
366
|
+
return jsonify(
|
367
|
+
{
|
368
|
+
"status": "success",
|
369
|
+
"metrics": combined_metrics,
|
370
|
+
"period": period,
|
371
|
+
"research_mode": research_mode,
|
372
|
+
}
|
373
|
+
)
|
374
|
+
except Exception as e:
|
375
|
+
logger.exception(f"Error getting metrics: {e}")
|
376
|
+
return (
|
377
|
+
jsonify(
|
378
|
+
{
|
379
|
+
"status": "error",
|
380
|
+
"message": "An internal error occurred. Please try again later.",
|
381
|
+
}
|
382
|
+
),
|
383
|
+
500,
|
384
|
+
)
|
385
|
+
|
386
|
+
|
387
|
+
@metrics_bp.route("/api/metrics/research/<int:research_id>")
|
388
|
+
def api_research_metrics(research_id):
|
389
|
+
"""Get metrics for a specific research."""
|
390
|
+
try:
|
391
|
+
token_counter = TokenCounter()
|
392
|
+
metrics = token_counter.get_research_metrics(research_id)
|
393
|
+
return jsonify({"status": "success", "metrics": metrics})
|
394
|
+
except Exception as e:
|
395
|
+
logger.exception(f"Error getting research metrics: {e}")
|
396
|
+
return (
|
397
|
+
jsonify(
|
398
|
+
{
|
399
|
+
"status": "error",
|
400
|
+
"message": "An internal error occurred. Please try again later.",
|
401
|
+
}
|
402
|
+
),
|
403
|
+
500,
|
404
|
+
)
|
405
|
+
|
406
|
+
|
407
|
+
@metrics_bp.route("/api/metrics/research/<int:research_id>/timeline")
|
408
|
+
def api_research_timeline_metrics(research_id):
|
409
|
+
"""Get timeline metrics for a specific research."""
|
410
|
+
try:
|
411
|
+
token_counter = TokenCounter()
|
412
|
+
timeline_metrics = token_counter.get_research_timeline_metrics(
|
413
|
+
research_id
|
414
|
+
)
|
415
|
+
return jsonify({"status": "success", "metrics": timeline_metrics})
|
416
|
+
except Exception as e:
|
417
|
+
logger.exception(f"Error getting research timeline metrics: {e}")
|
418
|
+
return (
|
419
|
+
jsonify(
|
420
|
+
{
|
421
|
+
"status": "error",
|
422
|
+
"message": "An internal error occurred. Please try again later.",
|
423
|
+
}
|
424
|
+
),
|
425
|
+
500,
|
426
|
+
)
|
427
|
+
|
428
|
+
|
429
|
+
@metrics_bp.route("/api/metrics/research/<int:research_id>/search")
|
430
|
+
def api_research_search_metrics(research_id):
|
431
|
+
"""Get search metrics for a specific research."""
|
432
|
+
try:
|
433
|
+
search_tracker = get_search_tracker()
|
434
|
+
search_metrics = search_tracker.get_research_search_metrics(research_id)
|
435
|
+
return jsonify({"status": "success", "metrics": search_metrics})
|
436
|
+
except Exception as e:
|
437
|
+
logger.exception(f"Error getting research search metrics: {e}")
|
438
|
+
return (
|
439
|
+
jsonify(
|
440
|
+
{
|
441
|
+
"status": "error",
|
442
|
+
"message": "An internal error occurred. Please try again later.",
|
443
|
+
}
|
444
|
+
),
|
445
|
+
500,
|
446
|
+
)
|
447
|
+
|
448
|
+
|
449
|
+
@metrics_bp.route("/api/metrics/enhanced")
|
450
|
+
def api_enhanced_metrics():
|
451
|
+
"""Get enhanced Phase 1 tracking metrics."""
|
452
|
+
try:
|
453
|
+
# Get time period and research mode from query parameters
|
454
|
+
period = request.args.get("period", "30d")
|
455
|
+
research_mode = request.args.get("mode", "all")
|
456
|
+
|
457
|
+
token_counter = TokenCounter()
|
458
|
+
search_tracker = get_search_tracker()
|
459
|
+
|
460
|
+
enhanced_metrics = token_counter.get_enhanced_metrics(
|
461
|
+
period=period, research_mode=research_mode
|
462
|
+
)
|
463
|
+
|
464
|
+
# Add search time series data for the chart
|
465
|
+
search_time_series = search_tracker.get_search_time_series(
|
466
|
+
period=period, research_mode=research_mode
|
467
|
+
)
|
468
|
+
enhanced_metrics["search_time_series"] = search_time_series
|
469
|
+
|
470
|
+
# Add rating analytics
|
471
|
+
rating_analytics = get_rating_analytics(period, research_mode)
|
472
|
+
enhanced_metrics.update(rating_analytics)
|
473
|
+
|
474
|
+
return jsonify(
|
475
|
+
{
|
476
|
+
"status": "success",
|
477
|
+
"metrics": enhanced_metrics,
|
478
|
+
"period": period,
|
479
|
+
"research_mode": research_mode,
|
480
|
+
}
|
481
|
+
)
|
482
|
+
except Exception as e:
|
483
|
+
logger.exception(f"Error getting enhanced metrics: {e}")
|
484
|
+
return (
|
485
|
+
jsonify(
|
486
|
+
{
|
487
|
+
"status": "error",
|
488
|
+
"message": "An internal error occurred. Please try again later.",
|
489
|
+
}
|
490
|
+
),
|
491
|
+
500,
|
492
|
+
)
|
493
|
+
|
494
|
+
|
495
|
+
@metrics_bp.route("/api/ratings/<int:research_id>", methods=["GET"])
|
496
|
+
def api_get_research_rating(research_id):
|
497
|
+
"""Get rating for a specific research session."""
|
498
|
+
try:
|
499
|
+
from ...metrics.database import get_metrics_db
|
500
|
+
from ...metrics.db_models import ResearchRating
|
501
|
+
|
502
|
+
db = get_metrics_db()
|
503
|
+
with db.get_session() as session:
|
504
|
+
rating = (
|
505
|
+
session.query(ResearchRating)
|
506
|
+
.filter_by(research_id=research_id)
|
507
|
+
.first()
|
508
|
+
)
|
509
|
+
|
510
|
+
if rating:
|
511
|
+
return jsonify(
|
512
|
+
{
|
513
|
+
"status": "success",
|
514
|
+
"rating": rating.rating,
|
515
|
+
"rated_at": rating.rated_at.isoformat(),
|
516
|
+
"updated_at": rating.updated_at.isoformat(),
|
517
|
+
}
|
518
|
+
)
|
519
|
+
else:
|
520
|
+
return jsonify({"status": "success", "rating": None})
|
521
|
+
|
522
|
+
except Exception as e:
|
523
|
+
logger.exception(f"Error getting research rating: {e}")
|
524
|
+
return (
|
525
|
+
jsonify(
|
526
|
+
{
|
527
|
+
"status": "error",
|
528
|
+
"message": "An internal error occurred. Please try again later.",
|
529
|
+
}
|
530
|
+
),
|
531
|
+
500,
|
532
|
+
)
|
533
|
+
|
534
|
+
|
535
|
+
@metrics_bp.route("/api/ratings/<int:research_id>", methods=["POST"])
|
536
|
+
def api_save_research_rating(research_id):
|
537
|
+
"""Save or update rating for a specific research session."""
|
538
|
+
try:
|
539
|
+
from sqlalchemy import func
|
540
|
+
|
541
|
+
from ...metrics.database import get_metrics_db
|
542
|
+
from ...metrics.db_models import ResearchRating
|
543
|
+
|
544
|
+
data = request.get_json()
|
545
|
+
rating_value = data.get("rating")
|
546
|
+
|
547
|
+
if (
|
548
|
+
not rating_value
|
549
|
+
or not isinstance(rating_value, int)
|
550
|
+
or rating_value < 1
|
551
|
+
or rating_value > 5
|
552
|
+
):
|
553
|
+
return (
|
554
|
+
jsonify(
|
555
|
+
{
|
556
|
+
"status": "error",
|
557
|
+
"message": "Rating must be an integer between 1 and 5",
|
558
|
+
}
|
559
|
+
),
|
560
|
+
400,
|
561
|
+
)
|
562
|
+
|
563
|
+
db = get_metrics_db()
|
564
|
+
with db.get_session() as session:
|
565
|
+
# Check if rating already exists
|
566
|
+
existing_rating = (
|
567
|
+
session.query(ResearchRating)
|
568
|
+
.filter_by(research_id=research_id)
|
569
|
+
.first()
|
570
|
+
)
|
571
|
+
|
572
|
+
if existing_rating:
|
573
|
+
# Update existing rating
|
574
|
+
existing_rating.rating = rating_value
|
575
|
+
existing_rating.updated_at = func.now()
|
576
|
+
else:
|
577
|
+
# Create new rating
|
578
|
+
new_rating = ResearchRating(
|
579
|
+
research_id=research_id, rating=rating_value
|
580
|
+
)
|
581
|
+
session.add(new_rating)
|
582
|
+
|
583
|
+
session.commit()
|
584
|
+
|
585
|
+
return jsonify(
|
586
|
+
{
|
587
|
+
"status": "success",
|
588
|
+
"message": "Rating saved successfully",
|
589
|
+
"rating": rating_value,
|
590
|
+
}
|
591
|
+
)
|
592
|
+
|
593
|
+
except Exception as e:
|
594
|
+
logger.exception(f"Error saving research rating: {e}")
|
595
|
+
return (
|
596
|
+
jsonify(
|
597
|
+
{
|
598
|
+
"status": "error",
|
599
|
+
"message": "An internal error occurred. Please try again later.",
|
600
|
+
}
|
601
|
+
),
|
602
|
+
500,
|
603
|
+
)
|
604
|
+
|
605
|
+
|
606
|
+
@metrics_bp.route("/star-reviews")
|
607
|
+
def star_reviews():
|
608
|
+
"""Display star reviews metrics page."""
|
609
|
+
return render_template_with_defaults("pages/star_reviews.html")
|
610
|
+
|
611
|
+
|
612
|
+
@metrics_bp.route("/costs")
|
613
|
+
def cost_analytics():
|
614
|
+
"""Display cost analytics page."""
|
615
|
+
return render_template_with_defaults("pages/cost_analytics.html")
|
616
|
+
|
617
|
+
|
618
|
+
@metrics_bp.route("/api/star-reviews")
|
619
|
+
def api_star_reviews():
|
620
|
+
"""Get star reviews analytics data."""
|
621
|
+
try:
|
622
|
+
period = request.args.get("period", "30d")
|
623
|
+
|
624
|
+
with get_db_session() as session:
|
625
|
+
# Build base query with time filter
|
626
|
+
base_query = session.query(ResearchRating)
|
627
|
+
time_condition = get_time_filter_condition(
|
628
|
+
period, ResearchRating.rated_at
|
629
|
+
)
|
630
|
+
if time_condition is not None:
|
631
|
+
base_query = base_query.filter(time_condition)
|
632
|
+
|
633
|
+
# Overall rating statistics
|
634
|
+
overall_stats = session.query(
|
635
|
+
func.avg(ResearchRating.rating).label("avg_rating"),
|
636
|
+
func.count(ResearchRating.rating).label("total_ratings"),
|
637
|
+
func.sum(case((ResearchRating.rating == 5, 1), else_=0)).label(
|
638
|
+
"five_star"
|
639
|
+
),
|
640
|
+
func.sum(case((ResearchRating.rating == 4, 1), else_=0)).label(
|
641
|
+
"four_star"
|
642
|
+
),
|
643
|
+
func.sum(case((ResearchRating.rating == 3, 1), else_=0)).label(
|
644
|
+
"three_star"
|
645
|
+
),
|
646
|
+
func.sum(case((ResearchRating.rating == 2, 1), else_=0)).label(
|
647
|
+
"two_star"
|
648
|
+
),
|
649
|
+
func.sum(case((ResearchRating.rating == 1, 1), else_=0)).label(
|
650
|
+
"one_star"
|
651
|
+
),
|
652
|
+
)
|
653
|
+
|
654
|
+
if time_condition is not None:
|
655
|
+
overall_stats = overall_stats.filter(time_condition)
|
656
|
+
|
657
|
+
overall_stats = overall_stats.first()
|
658
|
+
|
659
|
+
# Ratings by LLM model (get from token_usage since Research doesn't have model field)
|
660
|
+
llm_ratings_query = session.query(
|
661
|
+
func.coalesce(TokenUsage.model_name, "Unknown").label("model"),
|
662
|
+
func.avg(ResearchRating.rating).label("avg_rating"),
|
663
|
+
func.count(ResearchRating.rating).label("rating_count"),
|
664
|
+
func.sum(case((ResearchRating.rating >= 4, 1), else_=0)).label(
|
665
|
+
"positive_ratings"
|
666
|
+
),
|
667
|
+
).outerjoin(
|
668
|
+
TokenUsage, ResearchRating.research_id == TokenUsage.research_id
|
669
|
+
)
|
670
|
+
|
671
|
+
if time_condition is not None:
|
672
|
+
llm_ratings_query = llm_ratings_query.filter(time_condition)
|
673
|
+
|
674
|
+
llm_ratings = (
|
675
|
+
llm_ratings_query.group_by(TokenUsage.model_name)
|
676
|
+
.order_by(func.avg(ResearchRating.rating).desc())
|
677
|
+
.all()
|
678
|
+
)
|
679
|
+
|
680
|
+
# Ratings by search engine (join with token_usage to get search engine info)
|
681
|
+
search_engine_ratings_query = session.query(
|
682
|
+
func.coalesce(
|
683
|
+
TokenUsage.search_engine_selected, "Unknown"
|
684
|
+
).label("search_engine"),
|
685
|
+
func.avg(ResearchRating.rating).label("avg_rating"),
|
686
|
+
func.count(ResearchRating.rating).label("rating_count"),
|
687
|
+
func.sum(case((ResearchRating.rating >= 4, 1), else_=0)).label(
|
688
|
+
"positive_ratings"
|
689
|
+
),
|
690
|
+
).outerjoin(
|
691
|
+
TokenUsage, ResearchRating.research_id == TokenUsage.research_id
|
692
|
+
)
|
693
|
+
|
694
|
+
if time_condition is not None:
|
695
|
+
search_engine_ratings_query = (
|
696
|
+
search_engine_ratings_query.filter(time_condition)
|
697
|
+
)
|
698
|
+
|
699
|
+
search_engine_ratings = (
|
700
|
+
search_engine_ratings_query.group_by(
|
701
|
+
TokenUsage.search_engine_selected
|
702
|
+
)
|
703
|
+
.having(func.count(ResearchRating.rating) > 0)
|
704
|
+
.order_by(func.avg(ResearchRating.rating).desc())
|
705
|
+
.all()
|
706
|
+
)
|
707
|
+
|
708
|
+
# Rating trends over time
|
709
|
+
rating_trends_query = session.query(
|
710
|
+
func.date(ResearchRating.rated_at).label("date"),
|
711
|
+
func.avg(ResearchRating.rating).label("avg_rating"),
|
712
|
+
func.count(ResearchRating.rating).label("daily_count"),
|
713
|
+
)
|
714
|
+
|
715
|
+
if time_condition is not None:
|
716
|
+
rating_trends_query = rating_trends_query.filter(time_condition)
|
717
|
+
|
718
|
+
rating_trends = (
|
719
|
+
rating_trends_query.group_by(func.date(ResearchRating.rated_at))
|
720
|
+
.order_by("date")
|
721
|
+
.all()
|
722
|
+
)
|
723
|
+
|
724
|
+
# Recent ratings with research details
|
725
|
+
recent_ratings_query = (
|
726
|
+
session.query(
|
727
|
+
ResearchRating.rating,
|
728
|
+
ResearchRating.rated_at,
|
729
|
+
ResearchRating.research_id,
|
730
|
+
Research.query,
|
731
|
+
Research.mode,
|
732
|
+
TokenUsage.model_name,
|
733
|
+
Research.created_at,
|
734
|
+
)
|
735
|
+
.outerjoin(Research, ResearchRating.research_id == Research.id)
|
736
|
+
.outerjoin(
|
737
|
+
TokenUsage,
|
738
|
+
ResearchRating.research_id == TokenUsage.research_id,
|
739
|
+
)
|
740
|
+
)
|
741
|
+
|
742
|
+
if time_condition is not None:
|
743
|
+
recent_ratings_query = recent_ratings_query.filter(
|
744
|
+
time_condition
|
745
|
+
)
|
746
|
+
|
747
|
+
recent_ratings = (
|
748
|
+
recent_ratings_query.order_by(ResearchRating.rated_at.desc())
|
749
|
+
.limit(20)
|
750
|
+
.all()
|
751
|
+
)
|
752
|
+
|
753
|
+
return jsonify(
|
754
|
+
{
|
755
|
+
"overall_stats": {
|
756
|
+
"avg_rating": round(overall_stats.avg_rating or 0, 2),
|
757
|
+
"total_ratings": overall_stats.total_ratings or 0,
|
758
|
+
"rating_distribution": {
|
759
|
+
"5": overall_stats.five_star or 0,
|
760
|
+
"4": overall_stats.four_star or 0,
|
761
|
+
"3": overall_stats.three_star or 0,
|
762
|
+
"2": overall_stats.two_star or 0,
|
763
|
+
"1": overall_stats.one_star or 0,
|
764
|
+
},
|
765
|
+
},
|
766
|
+
"llm_ratings": [
|
767
|
+
{
|
768
|
+
"model": rating.model,
|
769
|
+
"avg_rating": round(rating.avg_rating or 0, 2),
|
770
|
+
"rating_count": rating.rating_count or 0,
|
771
|
+
"positive_ratings": rating.positive_ratings or 0,
|
772
|
+
"satisfaction_rate": round(
|
773
|
+
(rating.positive_ratings or 0)
|
774
|
+
/ max(rating.rating_count or 1, 1)
|
775
|
+
* 100,
|
776
|
+
1,
|
777
|
+
),
|
778
|
+
}
|
779
|
+
for rating in llm_ratings
|
780
|
+
],
|
781
|
+
"search_engine_ratings": [
|
782
|
+
{
|
783
|
+
"search_engine": rating.search_engine,
|
784
|
+
"avg_rating": round(rating.avg_rating or 0, 2),
|
785
|
+
"rating_count": rating.rating_count or 0,
|
786
|
+
"positive_ratings": rating.positive_ratings or 0,
|
787
|
+
"satisfaction_rate": round(
|
788
|
+
(rating.positive_ratings or 0)
|
789
|
+
/ max(rating.rating_count or 1, 1)
|
790
|
+
* 100,
|
791
|
+
1,
|
792
|
+
),
|
793
|
+
}
|
794
|
+
for rating in search_engine_ratings
|
795
|
+
],
|
796
|
+
"rating_trends": [
|
797
|
+
{
|
798
|
+
"date": str(trend.date),
|
799
|
+
"avg_rating": round(trend.avg_rating or 0, 2),
|
800
|
+
"count": trend.daily_count or 0,
|
801
|
+
}
|
802
|
+
for trend in rating_trends
|
803
|
+
],
|
804
|
+
"recent_ratings": [
|
805
|
+
{
|
806
|
+
"rating": rating.rating,
|
807
|
+
"rated_at": str(rating.rated_at),
|
808
|
+
"research_id": rating.research_id,
|
809
|
+
"query": (
|
810
|
+
rating.query
|
811
|
+
if rating.query
|
812
|
+
else f"Research Session #{rating.research_id}"
|
813
|
+
),
|
814
|
+
"mode": rating.mode
|
815
|
+
if rating.mode
|
816
|
+
else "Standard Research",
|
817
|
+
"llm_model": (
|
818
|
+
rating.model_name
|
819
|
+
if rating.model_name
|
820
|
+
else "LLM Model"
|
821
|
+
),
|
822
|
+
"created_at": (
|
823
|
+
str(rating.created_at)
|
824
|
+
if rating.created_at
|
825
|
+
else str(rating.rated_at)
|
826
|
+
),
|
827
|
+
}
|
828
|
+
for rating in recent_ratings
|
829
|
+
],
|
830
|
+
}
|
831
|
+
)
|
832
|
+
|
833
|
+
except Exception:
|
834
|
+
logger.exception("Error getting star reviews data")
|
835
|
+
return (
|
836
|
+
jsonify(
|
837
|
+
{"error": "An internal error occurred. Please try again later."}
|
838
|
+
),
|
839
|
+
500,
|
840
|
+
)
|
841
|
+
|
842
|
+
|
843
|
+
@metrics_bp.route("/api/pricing")
|
844
|
+
def api_pricing():
|
845
|
+
"""Get current LLM pricing data."""
|
846
|
+
try:
|
847
|
+
from ...metrics.pricing.pricing_fetcher import PricingFetcher
|
848
|
+
|
849
|
+
# Use static pricing data instead of async
|
850
|
+
fetcher = PricingFetcher()
|
851
|
+
pricing_data = fetcher.static_pricing
|
852
|
+
|
853
|
+
return jsonify(
|
854
|
+
{
|
855
|
+
"status": "success",
|
856
|
+
"pricing": pricing_data,
|
857
|
+
"last_updated": datetime.now().isoformat(),
|
858
|
+
"note": "Pricing data is from static configuration. Real-time APIs not available for most providers.",
|
859
|
+
}
|
860
|
+
)
|
861
|
+
|
862
|
+
except Exception:
|
863
|
+
logger.exception("Error fetching pricing data")
|
864
|
+
return jsonify({"error": "Internal Server Error"}), 500
|
865
|
+
|
866
|
+
|
867
|
+
@metrics_bp.route("/api/pricing/<model_name>")
|
868
|
+
def api_model_pricing(model_name):
|
869
|
+
"""Get pricing for a specific model."""
|
870
|
+
try:
|
871
|
+
# Optional provider parameter
|
872
|
+
provider = request.args.get("provider")
|
873
|
+
|
874
|
+
from ...metrics.pricing.cost_calculator import CostCalculator
|
875
|
+
|
876
|
+
# Use synchronous approach with cached/static pricing
|
877
|
+
calculator = CostCalculator()
|
878
|
+
pricing = calculator.cache.get_model_pricing(
|
879
|
+
model_name
|
880
|
+
) or calculator.calculate_cost_sync(model_name, 1000, 1000).get(
|
881
|
+
"pricing_used", {}
|
882
|
+
)
|
883
|
+
|
884
|
+
return jsonify(
|
885
|
+
{
|
886
|
+
"status": "success",
|
887
|
+
"model": model_name,
|
888
|
+
"provider": provider,
|
889
|
+
"pricing": pricing,
|
890
|
+
"last_updated": datetime.now().isoformat(),
|
891
|
+
}
|
892
|
+
)
|
893
|
+
|
894
|
+
except Exception as e:
|
895
|
+
logger.error(f"Error getting pricing for {model_name}: {e}")
|
896
|
+
return jsonify({"error": "An internal error occurred"}), 500
|
897
|
+
|
898
|
+
|
899
|
+
@metrics_bp.route("/api/cost-calculation", methods=["POST"])
|
900
|
+
def api_cost_calculation():
|
901
|
+
"""Calculate cost for token usage."""
|
902
|
+
try:
|
903
|
+
data = request.get_json()
|
904
|
+
|
905
|
+
if not data:
|
906
|
+
return jsonify({"error": "No data provided"}), 400
|
907
|
+
|
908
|
+
model_name = data.get("model_name")
|
909
|
+
provider = data.get("provider") # Optional provider parameter
|
910
|
+
prompt_tokens = data.get("prompt_tokens", 0)
|
911
|
+
completion_tokens = data.get("completion_tokens", 0)
|
912
|
+
|
913
|
+
if not model_name:
|
914
|
+
return jsonify({"error": "model_name is required"}), 400
|
915
|
+
|
916
|
+
from ...metrics.pricing.cost_calculator import CostCalculator
|
917
|
+
|
918
|
+
# Use synchronous cost calculation
|
919
|
+
calculator = CostCalculator()
|
920
|
+
cost_data = calculator.calculate_cost_sync(
|
921
|
+
model_name, prompt_tokens, completion_tokens
|
922
|
+
)
|
923
|
+
|
924
|
+
return jsonify(
|
925
|
+
{
|
926
|
+
"status": "success",
|
927
|
+
"model_name": model_name,
|
928
|
+
"provider": provider,
|
929
|
+
"prompt_tokens": prompt_tokens,
|
930
|
+
"completion_tokens": completion_tokens,
|
931
|
+
"total_tokens": prompt_tokens + completion_tokens,
|
932
|
+
**cost_data,
|
933
|
+
}
|
934
|
+
)
|
935
|
+
|
936
|
+
except Exception as e:
|
937
|
+
logger.error(f"Error calculating cost: {e}")
|
938
|
+
return jsonify({"error": "An internal error occurred"}), 500
|
939
|
+
|
940
|
+
|
941
|
+
@metrics_bp.route("/api/research-costs/<int:research_id>")
|
942
|
+
def api_research_costs(research_id):
|
943
|
+
"""Get cost analysis for a specific research session."""
|
944
|
+
try:
|
945
|
+
with get_db_session() as session:
|
946
|
+
# Get token usage records for this research
|
947
|
+
usage_records = (
|
948
|
+
session.query(TokenUsage)
|
949
|
+
.filter(TokenUsage.research_id == research_id)
|
950
|
+
.all()
|
951
|
+
)
|
952
|
+
|
953
|
+
if not usage_records:
|
954
|
+
return jsonify(
|
955
|
+
{
|
956
|
+
"status": "success",
|
957
|
+
"research_id": research_id,
|
958
|
+
"total_cost": 0.0,
|
959
|
+
"message": "No token usage data found for this research session",
|
960
|
+
}
|
961
|
+
)
|
962
|
+
|
963
|
+
# Convert to dict format for cost calculation
|
964
|
+
usage_data = []
|
965
|
+
for record in usage_records:
|
966
|
+
usage_data.append(
|
967
|
+
{
|
968
|
+
"model_name": record.model_name,
|
969
|
+
"provider": getattr(
|
970
|
+
record, "provider", None
|
971
|
+
), # Handle both old and new records
|
972
|
+
"prompt_tokens": record.prompt_tokens,
|
973
|
+
"completion_tokens": record.completion_tokens,
|
974
|
+
"timestamp": record.timestamp,
|
975
|
+
}
|
976
|
+
)
|
977
|
+
|
978
|
+
from ...metrics.pricing.cost_calculator import CostCalculator
|
979
|
+
|
980
|
+
# Use synchronous calculation for research costs
|
981
|
+
calculator = CostCalculator()
|
982
|
+
costs = []
|
983
|
+
for record in usage_data:
|
984
|
+
cost_data = calculator.calculate_cost_sync(
|
985
|
+
record["model_name"],
|
986
|
+
record["prompt_tokens"],
|
987
|
+
record["completion_tokens"],
|
988
|
+
)
|
989
|
+
costs.append({**record, **cost_data})
|
990
|
+
|
991
|
+
total_cost = sum(c["total_cost"] for c in costs)
|
992
|
+
total_prompt_tokens = sum(r["prompt_tokens"] for r in usage_data)
|
993
|
+
total_completion_tokens = sum(
|
994
|
+
r["completion_tokens"] for r in usage_data
|
995
|
+
)
|
996
|
+
|
997
|
+
cost_summary = {
|
998
|
+
"total_cost": round(total_cost, 6),
|
999
|
+
"total_tokens": total_prompt_tokens + total_completion_tokens,
|
1000
|
+
"prompt_tokens": total_prompt_tokens,
|
1001
|
+
"completion_tokens": total_completion_tokens,
|
1002
|
+
}
|
1003
|
+
|
1004
|
+
return jsonify(
|
1005
|
+
{
|
1006
|
+
"status": "success",
|
1007
|
+
"research_id": research_id,
|
1008
|
+
**cost_summary,
|
1009
|
+
}
|
1010
|
+
)
|
1011
|
+
|
1012
|
+
except Exception as e:
|
1013
|
+
logger.error(f"Error getting research costs for {research_id}: {e}")
|
1014
|
+
return jsonify({"error": "An internal error occurred"}), 500
|
1015
|
+
|
1016
|
+
|
1017
|
+
@metrics_bp.route("/api/cost-analytics")
|
1018
|
+
def api_cost_analytics():
|
1019
|
+
"""Get cost analytics across all research sessions."""
|
1020
|
+
try:
|
1021
|
+
period = request.args.get("period", "30d")
|
1022
|
+
|
1023
|
+
# Add error handling for empty data
|
1024
|
+
with get_db_session() as session:
|
1025
|
+
# Get token usage for the period
|
1026
|
+
query = session.query(TokenUsage)
|
1027
|
+
time_condition = get_time_filter_condition(
|
1028
|
+
period, TokenUsage.timestamp
|
1029
|
+
)
|
1030
|
+
if time_condition is not None:
|
1031
|
+
query = query.filter(time_condition)
|
1032
|
+
|
1033
|
+
usage_records = query.all()
|
1034
|
+
|
1035
|
+
if not usage_records:
|
1036
|
+
return jsonify(
|
1037
|
+
{
|
1038
|
+
"status": "success",
|
1039
|
+
"period": period,
|
1040
|
+
"total_cost": 0.0,
|
1041
|
+
"message": "No token usage data found for this period",
|
1042
|
+
}
|
1043
|
+
)
|
1044
|
+
|
1045
|
+
# Convert to dict format
|
1046
|
+
usage_data = []
|
1047
|
+
for record in usage_records:
|
1048
|
+
usage_data.append(
|
1049
|
+
{
|
1050
|
+
"model_name": record.model_name,
|
1051
|
+
"provider": getattr(
|
1052
|
+
record, "provider", None
|
1053
|
+
), # Handle both old and new records
|
1054
|
+
"prompt_tokens": record.prompt_tokens,
|
1055
|
+
"completion_tokens": record.completion_tokens,
|
1056
|
+
"research_id": record.research_id,
|
1057
|
+
"timestamp": record.timestamp,
|
1058
|
+
}
|
1059
|
+
)
|
1060
|
+
|
1061
|
+
from ...metrics.pricing.cost_calculator import CostCalculator
|
1062
|
+
|
1063
|
+
# Use synchronous calculation
|
1064
|
+
calculator = CostCalculator()
|
1065
|
+
|
1066
|
+
# Calculate overall costs
|
1067
|
+
costs = []
|
1068
|
+
for record in usage_data:
|
1069
|
+
cost_data = calculator.calculate_cost_sync(
|
1070
|
+
record["model_name"],
|
1071
|
+
record["prompt_tokens"],
|
1072
|
+
record["completion_tokens"],
|
1073
|
+
)
|
1074
|
+
costs.append({**record, **cost_data})
|
1075
|
+
|
1076
|
+
total_cost = sum(c["total_cost"] for c in costs)
|
1077
|
+
total_prompt_tokens = sum(r["prompt_tokens"] for r in usage_data)
|
1078
|
+
total_completion_tokens = sum(
|
1079
|
+
r["completion_tokens"] for r in usage_data
|
1080
|
+
)
|
1081
|
+
|
1082
|
+
cost_summary = {
|
1083
|
+
"total_cost": round(total_cost, 6),
|
1084
|
+
"total_tokens": total_prompt_tokens + total_completion_tokens,
|
1085
|
+
"prompt_tokens": total_prompt_tokens,
|
1086
|
+
"completion_tokens": total_completion_tokens,
|
1087
|
+
}
|
1088
|
+
|
1089
|
+
# Group by research_id for per-research costs
|
1090
|
+
research_costs = {}
|
1091
|
+
for record in usage_data:
|
1092
|
+
rid = record["research_id"]
|
1093
|
+
if rid not in research_costs:
|
1094
|
+
research_costs[rid] = []
|
1095
|
+
research_costs[rid].append(record)
|
1096
|
+
|
1097
|
+
# Calculate cost per research
|
1098
|
+
research_summaries = {}
|
1099
|
+
for rid, records in research_costs.items():
|
1100
|
+
research_total = 0
|
1101
|
+
for record in records:
|
1102
|
+
cost_data = calculator.calculate_cost_sync(
|
1103
|
+
record["model_name"],
|
1104
|
+
record["prompt_tokens"],
|
1105
|
+
record["completion_tokens"],
|
1106
|
+
)
|
1107
|
+
research_total += cost_data["total_cost"]
|
1108
|
+
research_summaries[rid] = {
|
1109
|
+
"total_cost": round(research_total, 6)
|
1110
|
+
}
|
1111
|
+
|
1112
|
+
# Top expensive research sessions
|
1113
|
+
top_expensive = sorted(
|
1114
|
+
[
|
1115
|
+
(rid, data["total_cost"])
|
1116
|
+
for rid, data in research_summaries.items()
|
1117
|
+
],
|
1118
|
+
key=lambda x: x[1],
|
1119
|
+
reverse=True,
|
1120
|
+
)[:10]
|
1121
|
+
|
1122
|
+
return jsonify(
|
1123
|
+
{
|
1124
|
+
"status": "success",
|
1125
|
+
"period": period,
|
1126
|
+
"overview": cost_summary,
|
1127
|
+
"top_expensive_research": [
|
1128
|
+
{"research_id": rid, "total_cost": cost}
|
1129
|
+
for rid, cost in top_expensive
|
1130
|
+
],
|
1131
|
+
"research_count": len(research_summaries),
|
1132
|
+
}
|
1133
|
+
)
|
1134
|
+
|
1135
|
+
except Exception as e:
|
1136
|
+
logger.exception(f"Error getting cost analytics: {e}")
|
1137
|
+
# Return a more graceful error response
|
1138
|
+
return (
|
1139
|
+
jsonify(
|
1140
|
+
{
|
1141
|
+
"status": "success",
|
1142
|
+
"period": period,
|
1143
|
+
"overview": {
|
1144
|
+
"total_cost": 0.0,
|
1145
|
+
"total_tokens": 0,
|
1146
|
+
"prompt_tokens": 0,
|
1147
|
+
"completion_tokens": 0,
|
1148
|
+
},
|
1149
|
+
"top_expensive_research": [],
|
1150
|
+
"research_count": 0,
|
1151
|
+
"error": "Cost analytics temporarily unavailable",
|
1152
|
+
}
|
1153
|
+
),
|
1154
|
+
200,
|
1155
|
+
) # Return 200 to avoid breaking the UI
|