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,1078 @@
|
|
1
|
+
"""Token counting functionality for LLM usage tracking."""
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
import json
|
5
|
+
import os
|
6
|
+
import time
|
7
|
+
from typing import Any, Dict, List, Optional
|
8
|
+
|
9
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
10
|
+
from langchain_core.outputs import LLMResult
|
11
|
+
from loguru import logger
|
12
|
+
from sqlalchemy import func, text
|
13
|
+
|
14
|
+
from .database import get_metrics_db
|
15
|
+
from .db_models import ModelUsage, TokenUsage
|
16
|
+
from .query_utils import get_research_mode_condition, get_time_filter_condition
|
17
|
+
|
18
|
+
|
19
|
+
class TokenCountingCallback(BaseCallbackHandler):
|
20
|
+
"""Callback handler for counting tokens across different models."""
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
research_id: Optional[int] = None,
|
25
|
+
research_context: Optional[Dict[str, Any]] = None,
|
26
|
+
):
|
27
|
+
"""Initialize the token counting callback.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
research_id: The ID of the research to track tokens for
|
31
|
+
research_context: Additional research context for enhanced tracking
|
32
|
+
"""
|
33
|
+
super().__init__()
|
34
|
+
self.research_id = research_id
|
35
|
+
self.research_context = research_context or {}
|
36
|
+
self.current_model = None
|
37
|
+
self.current_provider = None
|
38
|
+
self.preset_model = None # Model name set during callback creation
|
39
|
+
self.preset_provider = None # Provider set during callback creation
|
40
|
+
|
41
|
+
# Phase 1 Enhancement: Track timing and context
|
42
|
+
self.start_time = None
|
43
|
+
self.response_time_ms = None
|
44
|
+
self.success_status = "success"
|
45
|
+
self.error_type = None
|
46
|
+
|
47
|
+
# Call stack tracking
|
48
|
+
self.calling_file = None
|
49
|
+
self.calling_function = None
|
50
|
+
self.call_stack = None
|
51
|
+
|
52
|
+
# Track token counts in memory
|
53
|
+
self.counts = {
|
54
|
+
"total_tokens": 0,
|
55
|
+
"total_prompt_tokens": 0,
|
56
|
+
"total_completion_tokens": 0,
|
57
|
+
"by_model": {},
|
58
|
+
}
|
59
|
+
|
60
|
+
def on_llm_start(
|
61
|
+
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
|
62
|
+
) -> None:
|
63
|
+
"""Called when LLM starts running."""
|
64
|
+
# Phase 1 Enhancement: Start timing
|
65
|
+
self.start_time = time.time()
|
66
|
+
|
67
|
+
# Phase 1 Enhancement: Capture call stack information
|
68
|
+
try:
|
69
|
+
stack = inspect.stack()
|
70
|
+
|
71
|
+
# Skip the first few frames (this method, langchain internals)
|
72
|
+
# Look for the first frame that's in our project directory
|
73
|
+
for frame_info in stack[1:]:
|
74
|
+
file_path = frame_info.filename
|
75
|
+
# Look for any frame containing local_deep_research project
|
76
|
+
if (
|
77
|
+
"local_deep_research" in file_path
|
78
|
+
and "site-packages" not in file_path
|
79
|
+
and "venv" not in file_path
|
80
|
+
):
|
81
|
+
# Extract relative path from local_deep_research
|
82
|
+
if "src/local_deep_research" in file_path:
|
83
|
+
relative_path = file_path.split(
|
84
|
+
"src/local_deep_research"
|
85
|
+
)[-1].lstrip("/")
|
86
|
+
elif "local_deep_research/src" in file_path:
|
87
|
+
relative_path = file_path.split(
|
88
|
+
"local_deep_research/src"
|
89
|
+
)[-1].lstrip("/")
|
90
|
+
elif "local_deep_research" in file_path:
|
91
|
+
# Get everything after local_deep_research
|
92
|
+
relative_path = file_path.split("local_deep_research")[
|
93
|
+
-1
|
94
|
+
].lstrip("/")
|
95
|
+
else:
|
96
|
+
relative_path = os.path.basename(file_path)
|
97
|
+
|
98
|
+
self.calling_file = relative_path
|
99
|
+
self.calling_function = frame_info.function
|
100
|
+
|
101
|
+
# Capture a simplified call stack (just the relevant frames)
|
102
|
+
call_stack_frames = []
|
103
|
+
for frame in stack[1:6]: # Limit to 5 frames
|
104
|
+
if (
|
105
|
+
"local_deep_research" in frame.filename
|
106
|
+
and "site-packages" not in frame.filename
|
107
|
+
and "venv" not in frame.filename
|
108
|
+
):
|
109
|
+
frame_name = f"{os.path.basename(frame.filename)}:{frame.function}:{frame.lineno}"
|
110
|
+
call_stack_frames.append(frame_name)
|
111
|
+
|
112
|
+
self.call_stack = (
|
113
|
+
" -> ".join(call_stack_frames)
|
114
|
+
if call_stack_frames
|
115
|
+
else None
|
116
|
+
)
|
117
|
+
break
|
118
|
+
except Exception as e:
|
119
|
+
logger.debug(f"Error capturing call stack: {e}")
|
120
|
+
# Continue without call stack info if there's an error
|
121
|
+
|
122
|
+
# Debug logging
|
123
|
+
logger.debug(f"on_llm_start serialized: {serialized}")
|
124
|
+
logger.debug(f"on_llm_start kwargs: {kwargs}")
|
125
|
+
|
126
|
+
# First, use preset values if available
|
127
|
+
if self.preset_model:
|
128
|
+
self.current_model = self.preset_model
|
129
|
+
else:
|
130
|
+
# Try multiple locations for model name
|
131
|
+
model_name = None
|
132
|
+
|
133
|
+
# First check invocation_params
|
134
|
+
invocation_params = kwargs.get("invocation_params", {})
|
135
|
+
model_name = invocation_params.get(
|
136
|
+
"model"
|
137
|
+
) or invocation_params.get("model_name")
|
138
|
+
|
139
|
+
# Check kwargs directly
|
140
|
+
if not model_name:
|
141
|
+
model_name = kwargs.get("model") or kwargs.get("model_name")
|
142
|
+
|
143
|
+
# Check serialized data
|
144
|
+
if not model_name and "kwargs" in serialized:
|
145
|
+
model_name = serialized["kwargs"].get("model") or serialized[
|
146
|
+
"kwargs"
|
147
|
+
].get("model_name")
|
148
|
+
|
149
|
+
# Check for name in serialized data
|
150
|
+
if not model_name and "name" in serialized:
|
151
|
+
model_name = serialized["name"]
|
152
|
+
|
153
|
+
# If still not found and we have Ollama, try to extract from the instance
|
154
|
+
if (
|
155
|
+
not model_name
|
156
|
+
and "_type" in serialized
|
157
|
+
and "ChatOllama" in serialized["_type"]
|
158
|
+
):
|
159
|
+
# For Ollama, the model name might be in the serialized kwargs
|
160
|
+
if "kwargs" in serialized and "model" in serialized["kwargs"]:
|
161
|
+
model_name = serialized["kwargs"]["model"]
|
162
|
+
else:
|
163
|
+
# Default to the type if we can't find the actual model
|
164
|
+
model_name = "ollama"
|
165
|
+
|
166
|
+
# Final fallback
|
167
|
+
if not model_name:
|
168
|
+
if "_type" in serialized:
|
169
|
+
model_name = serialized["_type"]
|
170
|
+
else:
|
171
|
+
model_name = "unknown"
|
172
|
+
|
173
|
+
self.current_model = model_name
|
174
|
+
|
175
|
+
# Use preset provider if available
|
176
|
+
if self.preset_provider:
|
177
|
+
self.current_provider = self.preset_provider
|
178
|
+
else:
|
179
|
+
# Extract provider from serialized type or kwargs
|
180
|
+
if "_type" in serialized:
|
181
|
+
type_str = serialized["_type"]
|
182
|
+
if "ChatOllama" in type_str:
|
183
|
+
self.current_provider = "ollama"
|
184
|
+
elif "ChatOpenAI" in type_str:
|
185
|
+
self.current_provider = "openai"
|
186
|
+
elif "ChatAnthropic" in type_str:
|
187
|
+
self.current_provider = "anthropic"
|
188
|
+
else:
|
189
|
+
self.current_provider = kwargs.get("provider", "unknown")
|
190
|
+
else:
|
191
|
+
self.current_provider = kwargs.get("provider", "unknown")
|
192
|
+
|
193
|
+
# Initialize model tracking if needed
|
194
|
+
if self.current_model not in self.counts["by_model"]:
|
195
|
+
self.counts["by_model"][self.current_model] = {
|
196
|
+
"prompt_tokens": 0,
|
197
|
+
"completion_tokens": 0,
|
198
|
+
"total_tokens": 0,
|
199
|
+
"calls": 0,
|
200
|
+
"provider": self.current_provider,
|
201
|
+
}
|
202
|
+
|
203
|
+
# Increment call count
|
204
|
+
self.counts["by_model"][self.current_model]["calls"] += 1
|
205
|
+
|
206
|
+
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
|
207
|
+
"""Called when LLM ends running."""
|
208
|
+
# Phase 1 Enhancement: Calculate response time
|
209
|
+
if self.start_time:
|
210
|
+
self.response_time_ms = int((time.time() - self.start_time) * 1000)
|
211
|
+
|
212
|
+
# Extract token usage from response
|
213
|
+
token_usage = None
|
214
|
+
|
215
|
+
# Check multiple locations for token usage
|
216
|
+
if hasattr(response, "llm_output") and response.llm_output:
|
217
|
+
token_usage = response.llm_output.get(
|
218
|
+
"token_usage"
|
219
|
+
) or response.llm_output.get("usage", {})
|
220
|
+
|
221
|
+
# Check for usage metadata in generations (Ollama specific)
|
222
|
+
if not token_usage and hasattr(response, "generations"):
|
223
|
+
for generation_list in response.generations:
|
224
|
+
for generation in generation_list:
|
225
|
+
if hasattr(generation, "message") and hasattr(
|
226
|
+
generation.message, "usage_metadata"
|
227
|
+
):
|
228
|
+
usage_meta = generation.message.usage_metadata
|
229
|
+
token_usage = {
|
230
|
+
"prompt_tokens": usage_meta.get("input_tokens", 0),
|
231
|
+
"completion_tokens": usage_meta.get(
|
232
|
+
"output_tokens", 0
|
233
|
+
),
|
234
|
+
"total_tokens": usage_meta.get("total_tokens", 0),
|
235
|
+
}
|
236
|
+
break
|
237
|
+
# Also check response_metadata
|
238
|
+
elif hasattr(generation, "message") and hasattr(
|
239
|
+
generation.message, "response_metadata"
|
240
|
+
):
|
241
|
+
resp_meta = generation.message.response_metadata
|
242
|
+
if resp_meta.get("prompt_eval_count") or resp_meta.get(
|
243
|
+
"eval_count"
|
244
|
+
):
|
245
|
+
token_usage = {
|
246
|
+
"prompt_tokens": resp_meta.get(
|
247
|
+
"prompt_eval_count", 0
|
248
|
+
),
|
249
|
+
"completion_tokens": resp_meta.get(
|
250
|
+
"eval_count", 0
|
251
|
+
),
|
252
|
+
"total_tokens": resp_meta.get(
|
253
|
+
"prompt_eval_count", 0
|
254
|
+
)
|
255
|
+
+ resp_meta.get("eval_count", 0),
|
256
|
+
}
|
257
|
+
break
|
258
|
+
if token_usage:
|
259
|
+
break
|
260
|
+
|
261
|
+
if token_usage:
|
262
|
+
prompt_tokens = token_usage.get("prompt_tokens", 0)
|
263
|
+
completion_tokens = token_usage.get("completion_tokens", 0)
|
264
|
+
total_tokens = token_usage.get(
|
265
|
+
"total_tokens", prompt_tokens + completion_tokens
|
266
|
+
)
|
267
|
+
|
268
|
+
# Update in-memory counts
|
269
|
+
self.counts["total_prompt_tokens"] += prompt_tokens
|
270
|
+
self.counts["total_completion_tokens"] += completion_tokens
|
271
|
+
self.counts["total_tokens"] += total_tokens
|
272
|
+
|
273
|
+
if self.current_model:
|
274
|
+
self.counts["by_model"][self.current_model][
|
275
|
+
"prompt_tokens"
|
276
|
+
] += prompt_tokens
|
277
|
+
self.counts["by_model"][self.current_model][
|
278
|
+
"completion_tokens"
|
279
|
+
] += completion_tokens
|
280
|
+
self.counts["by_model"][self.current_model]["total_tokens"] += (
|
281
|
+
total_tokens
|
282
|
+
)
|
283
|
+
|
284
|
+
# Save to database if we have a research_id
|
285
|
+
if self.research_id:
|
286
|
+
self._save_to_db(prompt_tokens, completion_tokens)
|
287
|
+
|
288
|
+
def on_llm_error(self, error, **kwargs: Any) -> None:
|
289
|
+
"""Called when LLM encounters an error."""
|
290
|
+
# Phase 1 Enhancement: Track errors
|
291
|
+
if self.start_time:
|
292
|
+
self.response_time_ms = int((time.time() - self.start_time) * 1000)
|
293
|
+
|
294
|
+
self.success_status = "error"
|
295
|
+
self.error_type = str(type(error).__name__)
|
296
|
+
|
297
|
+
# Still save to database to track failed calls
|
298
|
+
if self.research_id:
|
299
|
+
self._save_to_db(0, 0)
|
300
|
+
|
301
|
+
def _save_to_db(self, prompt_tokens: int, completion_tokens: int):
|
302
|
+
"""Save token usage to the database."""
|
303
|
+
try:
|
304
|
+
db = get_metrics_db()
|
305
|
+
with db.get_session() as session:
|
306
|
+
# Phase 1 Enhancement: Prepare additional context
|
307
|
+
research_query = self.research_context.get("research_query")
|
308
|
+
research_mode = self.research_context.get("research_mode")
|
309
|
+
research_phase = self.research_context.get("research_phase")
|
310
|
+
search_iteration = self.research_context.get("search_iteration")
|
311
|
+
search_engines_planned = self.research_context.get(
|
312
|
+
"search_engines_planned"
|
313
|
+
)
|
314
|
+
search_engine_selected = self.research_context.get(
|
315
|
+
"search_engine_selected"
|
316
|
+
)
|
317
|
+
|
318
|
+
# Debug logging for search engine context
|
319
|
+
if search_engines_planned or search_engine_selected:
|
320
|
+
logger.info(
|
321
|
+
f"Token tracking - Search context: planned={search_engines_planned}, selected={search_engine_selected}, phase={research_phase}"
|
322
|
+
)
|
323
|
+
else:
|
324
|
+
logger.debug(
|
325
|
+
f"Token tracking - No search engine context yet, phase={research_phase}"
|
326
|
+
)
|
327
|
+
|
328
|
+
# Convert list to JSON string if needed
|
329
|
+
if isinstance(search_engines_planned, list):
|
330
|
+
search_engines_planned = json.dumps(search_engines_planned)
|
331
|
+
|
332
|
+
# Add token usage record with enhanced fields
|
333
|
+
token_usage = TokenUsage(
|
334
|
+
research_id=self.research_id,
|
335
|
+
model_name=self.current_model,
|
336
|
+
provider=self.current_provider, # Added provider for accurate cost tracking
|
337
|
+
prompt_tokens=prompt_tokens,
|
338
|
+
completion_tokens=completion_tokens,
|
339
|
+
total_tokens=prompt_tokens + completion_tokens,
|
340
|
+
# Phase 1 Enhancement: Research context
|
341
|
+
research_query=research_query,
|
342
|
+
research_mode=research_mode,
|
343
|
+
research_phase=research_phase,
|
344
|
+
search_iteration=search_iteration,
|
345
|
+
# Phase 1 Enhancement: Performance metrics
|
346
|
+
response_time_ms=self.response_time_ms,
|
347
|
+
success_status=self.success_status,
|
348
|
+
error_type=self.error_type,
|
349
|
+
# Phase 1 Enhancement: Search engine context
|
350
|
+
search_engines_planned=search_engines_planned,
|
351
|
+
search_engine_selected=search_engine_selected,
|
352
|
+
# Phase 1 Enhancement: Call stack tracking
|
353
|
+
calling_file=self.calling_file,
|
354
|
+
calling_function=self.calling_function,
|
355
|
+
call_stack=self.call_stack,
|
356
|
+
)
|
357
|
+
session.add(token_usage)
|
358
|
+
|
359
|
+
# Update or create model usage statistics
|
360
|
+
model_usage = (
|
361
|
+
session.query(ModelUsage)
|
362
|
+
.filter_by(
|
363
|
+
research_id=self.research_id,
|
364
|
+
model_name=self.current_model,
|
365
|
+
)
|
366
|
+
.first()
|
367
|
+
)
|
368
|
+
|
369
|
+
if model_usage:
|
370
|
+
model_usage.prompt_tokens += prompt_tokens
|
371
|
+
model_usage.completion_tokens += completion_tokens
|
372
|
+
model_usage.total_tokens += (
|
373
|
+
prompt_tokens + completion_tokens
|
374
|
+
)
|
375
|
+
model_usage.calls += 1
|
376
|
+
else:
|
377
|
+
model_usage = ModelUsage(
|
378
|
+
research_id=self.research_id,
|
379
|
+
model_name=self.current_model,
|
380
|
+
provider=self.current_provider,
|
381
|
+
prompt_tokens=prompt_tokens,
|
382
|
+
completion_tokens=completion_tokens,
|
383
|
+
total_tokens=prompt_tokens + completion_tokens,
|
384
|
+
calls=1,
|
385
|
+
)
|
386
|
+
session.add(model_usage)
|
387
|
+
|
388
|
+
except Exception as e:
|
389
|
+
logger.exception(f"Error saving token usage to database: {e}")
|
390
|
+
|
391
|
+
def get_counts(self) -> Dict[str, Any]:
|
392
|
+
"""Get the current token counts."""
|
393
|
+
return self.counts
|
394
|
+
|
395
|
+
|
396
|
+
class TokenCounter:
|
397
|
+
"""Manager class for token counting across the application."""
|
398
|
+
|
399
|
+
def __init__(self):
|
400
|
+
"""Initialize the token counter."""
|
401
|
+
self.db = get_metrics_db()
|
402
|
+
|
403
|
+
def create_callback(
|
404
|
+
self,
|
405
|
+
research_id: Optional[int] = None,
|
406
|
+
research_context: Optional[Dict[str, Any]] = None,
|
407
|
+
) -> TokenCountingCallback:
|
408
|
+
"""Create a new token counting callback.
|
409
|
+
|
410
|
+
Args:
|
411
|
+
research_id: The ID of the research to track tokens for
|
412
|
+
research_context: Additional research context for enhanced tracking
|
413
|
+
|
414
|
+
Returns:
|
415
|
+
A new TokenCountingCallback instance
|
416
|
+
"""
|
417
|
+
return TokenCountingCallback(
|
418
|
+
research_id=research_id, research_context=research_context
|
419
|
+
)
|
420
|
+
|
421
|
+
def get_research_metrics(self, research_id: int) -> Dict[str, Any]:
|
422
|
+
"""Get token metrics for a specific research.
|
423
|
+
|
424
|
+
Args:
|
425
|
+
research_id: The ID of the research
|
426
|
+
|
427
|
+
Returns:
|
428
|
+
Dictionary containing token usage metrics
|
429
|
+
"""
|
430
|
+
with self.db.get_session() as session:
|
431
|
+
# Get model usage for this research
|
432
|
+
model_usages = (
|
433
|
+
session.query(ModelUsage)
|
434
|
+
.filter_by(research_id=research_id)
|
435
|
+
.order_by(ModelUsage.total_tokens.desc())
|
436
|
+
.all()
|
437
|
+
)
|
438
|
+
|
439
|
+
model_usage = []
|
440
|
+
total_tokens = 0
|
441
|
+
total_calls = 0
|
442
|
+
|
443
|
+
for usage in model_usages:
|
444
|
+
model_usage.append(
|
445
|
+
{
|
446
|
+
"model": usage.model_name,
|
447
|
+
"provider": usage.provider,
|
448
|
+
"tokens": usage.total_tokens,
|
449
|
+
"calls": usage.calls,
|
450
|
+
"prompt_tokens": usage.prompt_tokens,
|
451
|
+
"completion_tokens": usage.completion_tokens,
|
452
|
+
}
|
453
|
+
)
|
454
|
+
total_tokens += usage.total_tokens
|
455
|
+
total_calls += usage.calls
|
456
|
+
|
457
|
+
return {
|
458
|
+
"research_id": research_id,
|
459
|
+
"total_tokens": total_tokens,
|
460
|
+
"total_calls": total_calls,
|
461
|
+
"model_usage": model_usage,
|
462
|
+
}
|
463
|
+
|
464
|
+
def get_overall_metrics(
|
465
|
+
self, period: str = "30d", research_mode: str = "all"
|
466
|
+
) -> Dict[str, Any]:
|
467
|
+
"""Get overall token metrics across all researches.
|
468
|
+
|
469
|
+
Args:
|
470
|
+
period: Time period to filter by ('7d', '30d', '3m', '1y', 'all')
|
471
|
+
research_mode: Research mode to filter by ('quick', 'detailed', 'all')
|
472
|
+
|
473
|
+
Returns:
|
474
|
+
Dictionary containing overall metrics
|
475
|
+
"""
|
476
|
+
with self.db.get_session() as session:
|
477
|
+
# Build base query with filters
|
478
|
+
query = session.query(TokenUsage)
|
479
|
+
|
480
|
+
# Apply time filter
|
481
|
+
time_condition = get_time_filter_condition(
|
482
|
+
period, TokenUsage.timestamp
|
483
|
+
)
|
484
|
+
if time_condition is not None:
|
485
|
+
query = query.filter(time_condition)
|
486
|
+
|
487
|
+
# Apply research mode filter
|
488
|
+
mode_condition = get_research_mode_condition(
|
489
|
+
research_mode, TokenUsage.research_mode
|
490
|
+
)
|
491
|
+
if mode_condition is not None:
|
492
|
+
query = query.filter(mode_condition)
|
493
|
+
|
494
|
+
# Total tokens and researches
|
495
|
+
total_tokens = (
|
496
|
+
query.with_entities(func.sum(TokenUsage.total_tokens)).scalar()
|
497
|
+
or 0
|
498
|
+
)
|
499
|
+
total_researches = (
|
500
|
+
query.with_entities(
|
501
|
+
func.count(func.distinct(TokenUsage.research_id))
|
502
|
+
).scalar()
|
503
|
+
or 0
|
504
|
+
)
|
505
|
+
|
506
|
+
# Model statistics using ORM aggregation
|
507
|
+
model_stats_query = session.query(
|
508
|
+
TokenUsage.model_name,
|
509
|
+
func.sum(TokenUsage.total_tokens).label("tokens"),
|
510
|
+
func.count().label("calls"),
|
511
|
+
func.sum(TokenUsage.prompt_tokens).label("prompt_tokens"),
|
512
|
+
func.sum(TokenUsage.completion_tokens).label(
|
513
|
+
"completion_tokens"
|
514
|
+
),
|
515
|
+
).filter(TokenUsage.model_name.isnot(None))
|
516
|
+
|
517
|
+
# Apply same filters to model stats
|
518
|
+
if time_condition is not None:
|
519
|
+
model_stats_query = model_stats_query.filter(time_condition)
|
520
|
+
if mode_condition is not None:
|
521
|
+
model_stats_query = model_stats_query.filter(mode_condition)
|
522
|
+
|
523
|
+
model_stats = (
|
524
|
+
model_stats_query.group_by(TokenUsage.model_name)
|
525
|
+
.order_by(func.sum(TokenUsage.total_tokens).desc())
|
526
|
+
.all()
|
527
|
+
)
|
528
|
+
|
529
|
+
# Get provider info from ModelUsage table
|
530
|
+
by_model = []
|
531
|
+
for stat in model_stats:
|
532
|
+
# Try to get provider from ModelUsage table
|
533
|
+
provider_info = (
|
534
|
+
session.query(ModelUsage.provider)
|
535
|
+
.filter(ModelUsage.model_name == stat.model_name)
|
536
|
+
.first()
|
537
|
+
)
|
538
|
+
provider = (
|
539
|
+
provider_info.provider if provider_info else "unknown"
|
540
|
+
)
|
541
|
+
|
542
|
+
by_model.append(
|
543
|
+
{
|
544
|
+
"model": stat.model_name,
|
545
|
+
"provider": provider,
|
546
|
+
"tokens": stat.tokens,
|
547
|
+
"calls": stat.calls,
|
548
|
+
"prompt_tokens": stat.prompt_tokens,
|
549
|
+
"completion_tokens": stat.completion_tokens,
|
550
|
+
}
|
551
|
+
)
|
552
|
+
|
553
|
+
# Get recent researches with token usage
|
554
|
+
# Note: This requires research_history table - for now we'll use available data
|
555
|
+
recent_research_query = session.query(
|
556
|
+
TokenUsage.research_id,
|
557
|
+
func.sum(TokenUsage.total_tokens).label("token_count"),
|
558
|
+
func.max(TokenUsage.timestamp).label("latest_timestamp"),
|
559
|
+
).filter(TokenUsage.research_id.isnot(None))
|
560
|
+
|
561
|
+
if time_condition is not None:
|
562
|
+
recent_research_query = recent_research_query.filter(
|
563
|
+
time_condition
|
564
|
+
)
|
565
|
+
if mode_condition is not None:
|
566
|
+
recent_research_query = recent_research_query.filter(
|
567
|
+
mode_condition
|
568
|
+
)
|
569
|
+
|
570
|
+
recent_research_data = (
|
571
|
+
recent_research_query.group_by(TokenUsage.research_id)
|
572
|
+
.order_by(func.max(TokenUsage.timestamp).desc())
|
573
|
+
.limit(10)
|
574
|
+
.all()
|
575
|
+
)
|
576
|
+
|
577
|
+
recent_researches = []
|
578
|
+
for research_data in recent_research_data:
|
579
|
+
# Get research query from token_usage table if available
|
580
|
+
research_query_data = (
|
581
|
+
session.query(TokenUsage.research_query)
|
582
|
+
.filter(
|
583
|
+
TokenUsage.research_id == research_data.research_id,
|
584
|
+
TokenUsage.research_query.isnot(None),
|
585
|
+
)
|
586
|
+
.first()
|
587
|
+
)
|
588
|
+
|
589
|
+
query_text = (
|
590
|
+
research_query_data.research_query
|
591
|
+
if research_query_data
|
592
|
+
else f"Research {research_data.research_id}"
|
593
|
+
)
|
594
|
+
|
595
|
+
recent_researches.append(
|
596
|
+
{
|
597
|
+
"id": research_data.research_id,
|
598
|
+
"query": query_text,
|
599
|
+
"tokens": research_data.token_count or 0,
|
600
|
+
"created_at": research_data.latest_timestamp,
|
601
|
+
}
|
602
|
+
)
|
603
|
+
|
604
|
+
# Token breakdown statistics
|
605
|
+
breakdown_query = query.with_entities(
|
606
|
+
func.sum(TokenUsage.prompt_tokens).label("total_input_tokens"),
|
607
|
+
func.sum(TokenUsage.completion_tokens).label(
|
608
|
+
"total_output_tokens"
|
609
|
+
),
|
610
|
+
func.avg(TokenUsage.prompt_tokens).label("avg_input_tokens"),
|
611
|
+
func.avg(TokenUsage.completion_tokens).label(
|
612
|
+
"avg_output_tokens"
|
613
|
+
),
|
614
|
+
func.avg(TokenUsage.total_tokens).label("avg_total_tokens"),
|
615
|
+
)
|
616
|
+
token_breakdown = breakdown_query.first()
|
617
|
+
|
618
|
+
return {
|
619
|
+
"total_tokens": total_tokens,
|
620
|
+
"total_researches": total_researches,
|
621
|
+
"by_model": by_model,
|
622
|
+
"recent_researches": recent_researches,
|
623
|
+
"token_breakdown": {
|
624
|
+
"total_input_tokens": int(
|
625
|
+
token_breakdown.total_input_tokens or 0
|
626
|
+
),
|
627
|
+
"total_output_tokens": int(
|
628
|
+
token_breakdown.total_output_tokens or 0
|
629
|
+
),
|
630
|
+
"avg_input_tokens": int(
|
631
|
+
token_breakdown.avg_input_tokens or 0
|
632
|
+
),
|
633
|
+
"avg_output_tokens": int(
|
634
|
+
token_breakdown.avg_output_tokens or 0
|
635
|
+
),
|
636
|
+
"avg_total_tokens": int(
|
637
|
+
token_breakdown.avg_total_tokens or 0
|
638
|
+
),
|
639
|
+
},
|
640
|
+
}
|
641
|
+
|
642
|
+
def get_enhanced_metrics(
|
643
|
+
self, period: str = "30d", research_mode: str = "all"
|
644
|
+
) -> Dict[str, Any]:
|
645
|
+
"""Get enhanced Phase 1 tracking metrics.
|
646
|
+
|
647
|
+
Args:
|
648
|
+
period: Time period to filter by ('7d', '30d', '3m', '1y', 'all')
|
649
|
+
research_mode: Research mode to filter by ('quick', 'detailed', 'all')
|
650
|
+
|
651
|
+
Returns:
|
652
|
+
Dictionary containing enhanced metrics data including time series
|
653
|
+
"""
|
654
|
+
with self.db.get_session() as session:
|
655
|
+
# Build base query with filters
|
656
|
+
query = session.query(TokenUsage)
|
657
|
+
|
658
|
+
# Apply time filter
|
659
|
+
time_condition = get_time_filter_condition(
|
660
|
+
period, TokenUsage.timestamp
|
661
|
+
)
|
662
|
+
if time_condition is not None:
|
663
|
+
query = query.filter(time_condition)
|
664
|
+
|
665
|
+
# Apply research mode filter
|
666
|
+
mode_condition = get_research_mode_condition(
|
667
|
+
research_mode, TokenUsage.research_mode
|
668
|
+
)
|
669
|
+
if mode_condition is not None:
|
670
|
+
query = query.filter(mode_condition)
|
671
|
+
|
672
|
+
# Get time series data for the chart - most important for "Token Consumption Over Time"
|
673
|
+
time_series_query = query.filter(
|
674
|
+
TokenUsage.timestamp.isnot(None), TokenUsage.total_tokens > 0
|
675
|
+
).order_by(TokenUsage.timestamp.asc())
|
676
|
+
|
677
|
+
# Limit to recent data for performance
|
678
|
+
if period != "all":
|
679
|
+
time_series_query = time_series_query.limit(200)
|
680
|
+
|
681
|
+
time_series_data = time_series_query.all()
|
682
|
+
|
683
|
+
# Format time series data with cumulative calculations
|
684
|
+
time_series = []
|
685
|
+
cumulative_tokens = 0
|
686
|
+
cumulative_prompt_tokens = 0
|
687
|
+
cumulative_completion_tokens = 0
|
688
|
+
|
689
|
+
for usage in time_series_data:
|
690
|
+
cumulative_tokens += usage.total_tokens or 0
|
691
|
+
cumulative_prompt_tokens += usage.prompt_tokens or 0
|
692
|
+
cumulative_completion_tokens += usage.completion_tokens or 0
|
693
|
+
|
694
|
+
time_series.append(
|
695
|
+
{
|
696
|
+
"timestamp": str(usage.timestamp)
|
697
|
+
if usage.timestamp
|
698
|
+
else None,
|
699
|
+
"tokens": usage.total_tokens or 0,
|
700
|
+
"prompt_tokens": usage.prompt_tokens or 0,
|
701
|
+
"completion_tokens": usage.completion_tokens or 0,
|
702
|
+
"cumulative_tokens": cumulative_tokens,
|
703
|
+
"cumulative_prompt_tokens": cumulative_prompt_tokens,
|
704
|
+
"cumulative_completion_tokens": cumulative_completion_tokens,
|
705
|
+
"research_id": usage.research_id,
|
706
|
+
"research_query": usage.research_query,
|
707
|
+
}
|
708
|
+
)
|
709
|
+
|
710
|
+
# Basic performance stats using ORM
|
711
|
+
performance_query = query.filter(
|
712
|
+
TokenUsage.response_time_ms.isnot(None)
|
713
|
+
)
|
714
|
+
total_calls = performance_query.count()
|
715
|
+
|
716
|
+
if total_calls > 0:
|
717
|
+
avg_response_time = (
|
718
|
+
performance_query.with_entities(
|
719
|
+
func.avg(TokenUsage.response_time_ms)
|
720
|
+
).scalar()
|
721
|
+
or 0
|
722
|
+
)
|
723
|
+
min_response_time = (
|
724
|
+
performance_query.with_entities(
|
725
|
+
func.min(TokenUsage.response_time_ms)
|
726
|
+
).scalar()
|
727
|
+
or 0
|
728
|
+
)
|
729
|
+
max_response_time = (
|
730
|
+
performance_query.with_entities(
|
731
|
+
func.max(TokenUsage.response_time_ms)
|
732
|
+
).scalar()
|
733
|
+
or 0
|
734
|
+
)
|
735
|
+
success_count = performance_query.filter(
|
736
|
+
TokenUsage.success_status == "success"
|
737
|
+
).count()
|
738
|
+
error_count = performance_query.filter(
|
739
|
+
TokenUsage.success_status == "error"
|
740
|
+
).count()
|
741
|
+
|
742
|
+
perf_stats = {
|
743
|
+
"avg_response_time": round(avg_response_time),
|
744
|
+
"min_response_time": min_response_time,
|
745
|
+
"max_response_time": max_response_time,
|
746
|
+
"success_rate": (
|
747
|
+
round((success_count / total_calls * 100), 1)
|
748
|
+
if total_calls > 0
|
749
|
+
else 0
|
750
|
+
),
|
751
|
+
"error_rate": (
|
752
|
+
round((error_count / total_calls * 100), 1)
|
753
|
+
if total_calls > 0
|
754
|
+
else 0
|
755
|
+
),
|
756
|
+
"total_enhanced_calls": total_calls,
|
757
|
+
}
|
758
|
+
else:
|
759
|
+
perf_stats = {
|
760
|
+
"avg_response_time": 0,
|
761
|
+
"min_response_time": 0,
|
762
|
+
"max_response_time": 0,
|
763
|
+
"success_rate": 0,
|
764
|
+
"error_rate": 0,
|
765
|
+
"total_enhanced_calls": 0,
|
766
|
+
}
|
767
|
+
|
768
|
+
# Research mode breakdown using ORM
|
769
|
+
mode_stats = (
|
770
|
+
query.filter(TokenUsage.research_mode.isnot(None))
|
771
|
+
.with_entities(
|
772
|
+
TokenUsage.research_mode,
|
773
|
+
func.count().label("count"),
|
774
|
+
func.avg(TokenUsage.total_tokens).label("avg_tokens"),
|
775
|
+
func.avg(TokenUsage.response_time_ms).label(
|
776
|
+
"avg_response_time"
|
777
|
+
),
|
778
|
+
)
|
779
|
+
.group_by(TokenUsage.research_mode)
|
780
|
+
.all()
|
781
|
+
)
|
782
|
+
|
783
|
+
modes = [
|
784
|
+
{
|
785
|
+
"mode": stat.research_mode,
|
786
|
+
"count": stat.count,
|
787
|
+
"avg_tokens": round(stat.avg_tokens or 0),
|
788
|
+
"avg_response_time": round(stat.avg_response_time or 0),
|
789
|
+
}
|
790
|
+
for stat in mode_stats
|
791
|
+
]
|
792
|
+
|
793
|
+
# Recent enhanced data (simplified)
|
794
|
+
recent_enhanced_query = (
|
795
|
+
query.filter(TokenUsage.research_query.isnot(None))
|
796
|
+
.order_by(TokenUsage.timestamp.desc())
|
797
|
+
.limit(50)
|
798
|
+
)
|
799
|
+
|
800
|
+
recent_enhanced_data = recent_enhanced_query.all()
|
801
|
+
recent_enhanced = [
|
802
|
+
{
|
803
|
+
"research_query": usage.research_query,
|
804
|
+
"research_mode": usage.research_mode,
|
805
|
+
"research_phase": usage.research_phase,
|
806
|
+
"search_iteration": usage.search_iteration,
|
807
|
+
"response_time_ms": usage.response_time_ms,
|
808
|
+
"success_status": usage.success_status,
|
809
|
+
"error_type": usage.error_type,
|
810
|
+
"search_engines_planned": usage.search_engines_planned,
|
811
|
+
"search_engine_selected": usage.search_engine_selected,
|
812
|
+
"total_tokens": usage.total_tokens,
|
813
|
+
"prompt_tokens": usage.prompt_tokens,
|
814
|
+
"completion_tokens": usage.completion_tokens,
|
815
|
+
"timestamp": str(usage.timestamp)
|
816
|
+
if usage.timestamp
|
817
|
+
else None,
|
818
|
+
"research_id": usage.research_id,
|
819
|
+
"calling_file": usage.calling_file,
|
820
|
+
"calling_function": usage.calling_function,
|
821
|
+
"call_stack": usage.call_stack,
|
822
|
+
}
|
823
|
+
for usage in recent_enhanced_data
|
824
|
+
]
|
825
|
+
|
826
|
+
# Search engine breakdown using ORM
|
827
|
+
search_engine_stats = (
|
828
|
+
query.filter(TokenUsage.search_engine_selected.isnot(None))
|
829
|
+
.with_entities(
|
830
|
+
TokenUsage.search_engine_selected,
|
831
|
+
func.count().label("count"),
|
832
|
+
func.avg(TokenUsage.total_tokens).label("avg_tokens"),
|
833
|
+
func.avg(TokenUsage.response_time_ms).label(
|
834
|
+
"avg_response_time"
|
835
|
+
),
|
836
|
+
)
|
837
|
+
.group_by(TokenUsage.search_engine_selected)
|
838
|
+
.all()
|
839
|
+
)
|
840
|
+
|
841
|
+
search_engines = [
|
842
|
+
{
|
843
|
+
"search_engine": stat.search_engine_selected,
|
844
|
+
"count": stat.count,
|
845
|
+
"avg_tokens": round(stat.avg_tokens or 0),
|
846
|
+
"avg_response_time": round(stat.avg_response_time or 0),
|
847
|
+
}
|
848
|
+
for stat in search_engine_stats
|
849
|
+
]
|
850
|
+
|
851
|
+
# Research phase breakdown using ORM
|
852
|
+
phase_stats = (
|
853
|
+
query.filter(TokenUsage.research_phase.isnot(None))
|
854
|
+
.with_entities(
|
855
|
+
TokenUsage.research_phase,
|
856
|
+
func.count().label("count"),
|
857
|
+
func.avg(TokenUsage.total_tokens).label("avg_tokens"),
|
858
|
+
func.avg(TokenUsage.response_time_ms).label(
|
859
|
+
"avg_response_time"
|
860
|
+
),
|
861
|
+
)
|
862
|
+
.group_by(TokenUsage.research_phase)
|
863
|
+
.all()
|
864
|
+
)
|
865
|
+
|
866
|
+
phases = [
|
867
|
+
{
|
868
|
+
"phase": stat.research_phase,
|
869
|
+
"count": stat.count,
|
870
|
+
"avg_tokens": round(stat.avg_tokens or 0),
|
871
|
+
"avg_response_time": round(stat.avg_response_time or 0),
|
872
|
+
}
|
873
|
+
for stat in phase_stats
|
874
|
+
]
|
875
|
+
|
876
|
+
# Call stack analysis using ORM
|
877
|
+
file_stats = (
|
878
|
+
query.filter(TokenUsage.calling_file.isnot(None))
|
879
|
+
.with_entities(
|
880
|
+
TokenUsage.calling_file,
|
881
|
+
func.count().label("count"),
|
882
|
+
func.avg(TokenUsage.total_tokens).label("avg_tokens"),
|
883
|
+
)
|
884
|
+
.group_by(TokenUsage.calling_file)
|
885
|
+
.order_by(func.count().desc())
|
886
|
+
.limit(10)
|
887
|
+
.all()
|
888
|
+
)
|
889
|
+
|
890
|
+
files = [
|
891
|
+
{
|
892
|
+
"file": stat.calling_file,
|
893
|
+
"count": stat.count,
|
894
|
+
"avg_tokens": round(stat.avg_tokens or 0),
|
895
|
+
}
|
896
|
+
for stat in file_stats
|
897
|
+
]
|
898
|
+
|
899
|
+
function_stats = (
|
900
|
+
query.filter(TokenUsage.calling_function.isnot(None))
|
901
|
+
.with_entities(
|
902
|
+
TokenUsage.calling_function,
|
903
|
+
func.count().label("count"),
|
904
|
+
func.avg(TokenUsage.total_tokens).label("avg_tokens"),
|
905
|
+
)
|
906
|
+
.group_by(TokenUsage.calling_function)
|
907
|
+
.order_by(func.count().desc())
|
908
|
+
.limit(10)
|
909
|
+
.all()
|
910
|
+
)
|
911
|
+
|
912
|
+
functions = [
|
913
|
+
{
|
914
|
+
"function": stat.calling_function,
|
915
|
+
"count": stat.count,
|
916
|
+
"avg_tokens": round(stat.avg_tokens or 0),
|
917
|
+
}
|
918
|
+
for stat in function_stats
|
919
|
+
]
|
920
|
+
|
921
|
+
return {
|
922
|
+
"recent_enhanced_data": recent_enhanced,
|
923
|
+
"performance_stats": perf_stats,
|
924
|
+
"mode_breakdown": modes,
|
925
|
+
"search_engine_stats": search_engines,
|
926
|
+
"phase_breakdown": phases,
|
927
|
+
"time_series_data": time_series,
|
928
|
+
"call_stack_analysis": {
|
929
|
+
"by_file": files,
|
930
|
+
"by_function": functions,
|
931
|
+
},
|
932
|
+
}
|
933
|
+
|
934
|
+
def get_research_timeline_metrics(self, research_id: int) -> Dict[str, Any]:
|
935
|
+
"""Get timeline metrics for a specific research.
|
936
|
+
|
937
|
+
Args:
|
938
|
+
research_id: The ID of the research
|
939
|
+
|
940
|
+
Returns:
|
941
|
+
Dictionary containing timeline metrics for the research
|
942
|
+
"""
|
943
|
+
with self.db.get_session() as session:
|
944
|
+
# Get all token usage for this research ordered by time including call stack
|
945
|
+
timeline_data = session.execute(
|
946
|
+
text(
|
947
|
+
"""
|
948
|
+
SELECT
|
949
|
+
timestamp,
|
950
|
+
total_tokens,
|
951
|
+
prompt_tokens,
|
952
|
+
completion_tokens,
|
953
|
+
response_time_ms,
|
954
|
+
success_status,
|
955
|
+
error_type,
|
956
|
+
research_phase,
|
957
|
+
search_iteration,
|
958
|
+
search_engine_selected,
|
959
|
+
model_name,
|
960
|
+
calling_file,
|
961
|
+
calling_function,
|
962
|
+
call_stack
|
963
|
+
FROM token_usage
|
964
|
+
WHERE research_id = :research_id
|
965
|
+
ORDER BY timestamp ASC
|
966
|
+
"""
|
967
|
+
),
|
968
|
+
{"research_id": research_id},
|
969
|
+
).fetchall()
|
970
|
+
|
971
|
+
# Format timeline data with cumulative tokens
|
972
|
+
timeline = []
|
973
|
+
cumulative_tokens = 0
|
974
|
+
cumulative_prompt_tokens = 0
|
975
|
+
cumulative_completion_tokens = 0
|
976
|
+
|
977
|
+
for row in timeline_data:
|
978
|
+
cumulative_tokens += row[1] or 0
|
979
|
+
cumulative_prompt_tokens += row[2] or 0
|
980
|
+
cumulative_completion_tokens += row[3] or 0
|
981
|
+
|
982
|
+
timeline.append(
|
983
|
+
{
|
984
|
+
"timestamp": str(row[0]) if row[0] else None,
|
985
|
+
"tokens": row[1] or 0,
|
986
|
+
"prompt_tokens": row[2] or 0,
|
987
|
+
"completion_tokens": row[3] or 0,
|
988
|
+
"cumulative_tokens": cumulative_tokens,
|
989
|
+
"cumulative_prompt_tokens": cumulative_prompt_tokens,
|
990
|
+
"cumulative_completion_tokens": cumulative_completion_tokens,
|
991
|
+
"response_time_ms": row[4],
|
992
|
+
"success_status": row[5],
|
993
|
+
"error_type": row[6],
|
994
|
+
"research_phase": row[7],
|
995
|
+
"search_iteration": row[8],
|
996
|
+
"search_engine_selected": row[9],
|
997
|
+
"model_name": row[10],
|
998
|
+
"calling_file": row[11],
|
999
|
+
"calling_function": row[12],
|
1000
|
+
"call_stack": row[13],
|
1001
|
+
}
|
1002
|
+
)
|
1003
|
+
|
1004
|
+
# Get research basic info
|
1005
|
+
research_info = session.execute(
|
1006
|
+
text(
|
1007
|
+
"""
|
1008
|
+
SELECT query, mode, status, created_at, completed_at
|
1009
|
+
FROM research_history
|
1010
|
+
WHERE id = :research_id
|
1011
|
+
"""
|
1012
|
+
),
|
1013
|
+
{"research_id": research_id},
|
1014
|
+
).fetchone()
|
1015
|
+
|
1016
|
+
research_details = {}
|
1017
|
+
if research_info:
|
1018
|
+
research_details = {
|
1019
|
+
"query": research_info[0],
|
1020
|
+
"mode": research_info[1],
|
1021
|
+
"status": research_info[2],
|
1022
|
+
"created_at": str(research_info[3])
|
1023
|
+
if research_info[3]
|
1024
|
+
else None,
|
1025
|
+
"completed_at": str(research_info[4])
|
1026
|
+
if research_info[4]
|
1027
|
+
else None,
|
1028
|
+
}
|
1029
|
+
|
1030
|
+
# Calculate summary stats
|
1031
|
+
total_calls = len(timeline_data)
|
1032
|
+
total_tokens = cumulative_tokens
|
1033
|
+
avg_response_time = sum(row[4] or 0 for row in timeline_data) / max(
|
1034
|
+
total_calls, 1
|
1035
|
+
)
|
1036
|
+
success_rate = (
|
1037
|
+
sum(1 for row in timeline_data if row[5] == "success")
|
1038
|
+
/ max(total_calls, 1)
|
1039
|
+
* 100
|
1040
|
+
)
|
1041
|
+
|
1042
|
+
# Phase breakdown for this research
|
1043
|
+
phase_stats = {}
|
1044
|
+
for row in timeline_data:
|
1045
|
+
phase = row[7] or "unknown"
|
1046
|
+
if phase not in phase_stats:
|
1047
|
+
phase_stats[phase] = {
|
1048
|
+
"count": 0,
|
1049
|
+
"tokens": 0,
|
1050
|
+
"avg_response_time": 0,
|
1051
|
+
}
|
1052
|
+
phase_stats[phase]["count"] += 1
|
1053
|
+
phase_stats[phase]["tokens"] += row[1] or 0
|
1054
|
+
if row[4]:
|
1055
|
+
phase_stats[phase]["avg_response_time"] += row[4]
|
1056
|
+
|
1057
|
+
# Calculate averages for phases
|
1058
|
+
for phase in phase_stats:
|
1059
|
+
if phase_stats[phase]["count"] > 0:
|
1060
|
+
phase_stats[phase]["avg_response_time"] = round(
|
1061
|
+
phase_stats[phase]["avg_response_time"]
|
1062
|
+
/ phase_stats[phase]["count"]
|
1063
|
+
)
|
1064
|
+
|
1065
|
+
return {
|
1066
|
+
"research_id": research_id,
|
1067
|
+
"research_details": research_details,
|
1068
|
+
"timeline": timeline,
|
1069
|
+
"summary": {
|
1070
|
+
"total_calls": total_calls,
|
1071
|
+
"total_tokens": total_tokens,
|
1072
|
+
"total_prompt_tokens": cumulative_prompt_tokens,
|
1073
|
+
"total_completion_tokens": cumulative_completion_tokens,
|
1074
|
+
"avg_response_time": round(avg_response_time),
|
1075
|
+
"success_rate": round(success_rate, 1),
|
1076
|
+
},
|
1077
|
+
"phase_stats": phase_stats,
|
1078
|
+
}
|