aiecs 1.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiecs/__init__.py +72 -0
- aiecs/__main__.py +41 -0
- aiecs/aiecs_client.py +469 -0
- aiecs/application/__init__.py +10 -0
- aiecs/application/executors/__init__.py +10 -0
- aiecs/application/executors/operation_executor.py +363 -0
- aiecs/application/knowledge_graph/__init__.py +7 -0
- aiecs/application/knowledge_graph/builder/__init__.py +37 -0
- aiecs/application/knowledge_graph/builder/document_builder.py +375 -0
- aiecs/application/knowledge_graph/builder/graph_builder.py +356 -0
- aiecs/application/knowledge_graph/builder/schema_mapping.py +531 -0
- aiecs/application/knowledge_graph/builder/structured_pipeline.py +443 -0
- aiecs/application/knowledge_graph/builder/text_chunker.py +319 -0
- aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
- aiecs/application/knowledge_graph/extractors/base.py +100 -0
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +327 -0
- aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +349 -0
- aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +244 -0
- aiecs/application/knowledge_graph/fusion/__init__.py +23 -0
- aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +387 -0
- aiecs/application/knowledge_graph/fusion/entity_linker.py +343 -0
- aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +580 -0
- aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +189 -0
- aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
- aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +344 -0
- aiecs/application/knowledge_graph/pattern_matching/query_executor.py +378 -0
- aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
- aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +199 -0
- aiecs/application/knowledge_graph/profiling/query_profiler.py +223 -0
- aiecs/application/knowledge_graph/reasoning/__init__.py +27 -0
- aiecs/application/knowledge_graph/reasoning/evidence_synthesis.py +347 -0
- aiecs/application/knowledge_graph/reasoning/inference_engine.py +504 -0
- aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +167 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/__init__.py +79 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_builder.py +513 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_nodes.py +630 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +654 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +477 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +390 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +217 -0
- aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +169 -0
- aiecs/application/knowledge_graph/reasoning/query_planner.py +872 -0
- aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +554 -0
- aiecs/application/knowledge_graph/retrieval/__init__.py +19 -0
- aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +596 -0
- aiecs/application/knowledge_graph/search/__init__.py +59 -0
- aiecs/application/knowledge_graph/search/hybrid_search.py +423 -0
- aiecs/application/knowledge_graph/search/reranker.py +295 -0
- aiecs/application/knowledge_graph/search/reranker_strategies.py +553 -0
- aiecs/application/knowledge_graph/search/text_similarity.py +398 -0
- aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
- aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +329 -0
- aiecs/application/knowledge_graph/traversal/path_scorer.py +269 -0
- aiecs/application/knowledge_graph/validators/__init__.py +13 -0
- aiecs/application/knowledge_graph/validators/relation_validator.py +189 -0
- aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
- aiecs/application/knowledge_graph/visualization/graph_visualizer.py +321 -0
- aiecs/common/__init__.py +9 -0
- aiecs/common/knowledge_graph/__init__.py +17 -0
- aiecs/common/knowledge_graph/runnable.py +484 -0
- aiecs/config/__init__.py +16 -0
- aiecs/config/config.py +498 -0
- aiecs/config/graph_config.py +137 -0
- aiecs/config/registry.py +23 -0
- aiecs/core/__init__.py +46 -0
- aiecs/core/interface/__init__.py +34 -0
- aiecs/core/interface/execution_interface.py +152 -0
- aiecs/core/interface/storage_interface.py +171 -0
- aiecs/domain/__init__.py +289 -0
- aiecs/domain/agent/__init__.py +189 -0
- aiecs/domain/agent/base_agent.py +697 -0
- aiecs/domain/agent/exceptions.py +103 -0
- aiecs/domain/agent/graph_aware_mixin.py +559 -0
- aiecs/domain/agent/hybrid_agent.py +490 -0
- aiecs/domain/agent/integration/__init__.py +26 -0
- aiecs/domain/agent/integration/context_compressor.py +222 -0
- aiecs/domain/agent/integration/context_engine_adapter.py +252 -0
- aiecs/domain/agent/integration/retry_policy.py +219 -0
- aiecs/domain/agent/integration/role_config.py +213 -0
- aiecs/domain/agent/knowledge_aware_agent.py +646 -0
- aiecs/domain/agent/lifecycle.py +296 -0
- aiecs/domain/agent/llm_agent.py +300 -0
- aiecs/domain/agent/memory/__init__.py +12 -0
- aiecs/domain/agent/memory/conversation.py +197 -0
- aiecs/domain/agent/migration/__init__.py +14 -0
- aiecs/domain/agent/migration/conversion.py +160 -0
- aiecs/domain/agent/migration/legacy_wrapper.py +90 -0
- aiecs/domain/agent/models.py +317 -0
- aiecs/domain/agent/observability.py +407 -0
- aiecs/domain/agent/persistence.py +289 -0
- aiecs/domain/agent/prompts/__init__.py +29 -0
- aiecs/domain/agent/prompts/builder.py +161 -0
- aiecs/domain/agent/prompts/formatters.py +189 -0
- aiecs/domain/agent/prompts/template.py +255 -0
- aiecs/domain/agent/registry.py +260 -0
- aiecs/domain/agent/tool_agent.py +257 -0
- aiecs/domain/agent/tools/__init__.py +12 -0
- aiecs/domain/agent/tools/schema_generator.py +221 -0
- aiecs/domain/community/__init__.py +155 -0
- aiecs/domain/community/agent_adapter.py +477 -0
- aiecs/domain/community/analytics.py +481 -0
- aiecs/domain/community/collaborative_workflow.py +642 -0
- aiecs/domain/community/communication_hub.py +645 -0
- aiecs/domain/community/community_builder.py +320 -0
- aiecs/domain/community/community_integration.py +800 -0
- aiecs/domain/community/community_manager.py +813 -0
- aiecs/domain/community/decision_engine.py +879 -0
- aiecs/domain/community/exceptions.py +225 -0
- aiecs/domain/community/models/__init__.py +33 -0
- aiecs/domain/community/models/community_models.py +268 -0
- aiecs/domain/community/resource_manager.py +457 -0
- aiecs/domain/community/shared_context_manager.py +603 -0
- aiecs/domain/context/__init__.py +58 -0
- aiecs/domain/context/context_engine.py +989 -0
- aiecs/domain/context/conversation_models.py +354 -0
- aiecs/domain/context/graph_memory.py +467 -0
- aiecs/domain/execution/__init__.py +12 -0
- aiecs/domain/execution/model.py +57 -0
- aiecs/domain/knowledge_graph/__init__.py +19 -0
- aiecs/domain/knowledge_graph/models/__init__.py +52 -0
- aiecs/domain/knowledge_graph/models/entity.py +130 -0
- aiecs/domain/knowledge_graph/models/evidence.py +194 -0
- aiecs/domain/knowledge_graph/models/inference_rule.py +186 -0
- aiecs/domain/knowledge_graph/models/path.py +179 -0
- aiecs/domain/knowledge_graph/models/path_pattern.py +173 -0
- aiecs/domain/knowledge_graph/models/query.py +272 -0
- aiecs/domain/knowledge_graph/models/query_plan.py +187 -0
- aiecs/domain/knowledge_graph/models/relation.py +136 -0
- aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
- aiecs/domain/knowledge_graph/schema/entity_type.py +135 -0
- aiecs/domain/knowledge_graph/schema/graph_schema.py +271 -0
- aiecs/domain/knowledge_graph/schema/property_schema.py +155 -0
- aiecs/domain/knowledge_graph/schema/relation_type.py +171 -0
- aiecs/domain/knowledge_graph/schema/schema_manager.py +496 -0
- aiecs/domain/knowledge_graph/schema/type_enums.py +205 -0
- aiecs/domain/task/__init__.py +13 -0
- aiecs/domain/task/dsl_processor.py +613 -0
- aiecs/domain/task/model.py +62 -0
- aiecs/domain/task/task_context.py +268 -0
- aiecs/infrastructure/__init__.py +24 -0
- aiecs/infrastructure/graph_storage/__init__.py +11 -0
- aiecs/infrastructure/graph_storage/base.py +601 -0
- aiecs/infrastructure/graph_storage/batch_operations.py +449 -0
- aiecs/infrastructure/graph_storage/cache.py +429 -0
- aiecs/infrastructure/graph_storage/distributed.py +226 -0
- aiecs/infrastructure/graph_storage/error_handling.py +390 -0
- aiecs/infrastructure/graph_storage/graceful_degradation.py +306 -0
- aiecs/infrastructure/graph_storage/health_checks.py +378 -0
- aiecs/infrastructure/graph_storage/in_memory.py +514 -0
- aiecs/infrastructure/graph_storage/index_optimization.py +483 -0
- aiecs/infrastructure/graph_storage/lazy_loading.py +410 -0
- aiecs/infrastructure/graph_storage/metrics.py +357 -0
- aiecs/infrastructure/graph_storage/migration.py +413 -0
- aiecs/infrastructure/graph_storage/pagination.py +471 -0
- aiecs/infrastructure/graph_storage/performance_monitoring.py +466 -0
- aiecs/infrastructure/graph_storage/postgres.py +871 -0
- aiecs/infrastructure/graph_storage/query_optimizer.py +635 -0
- aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
- aiecs/infrastructure/graph_storage/sqlite.py +623 -0
- aiecs/infrastructure/graph_storage/streaming.py +495 -0
- aiecs/infrastructure/messaging/__init__.py +13 -0
- aiecs/infrastructure/messaging/celery_task_manager.py +383 -0
- aiecs/infrastructure/messaging/websocket_manager.py +298 -0
- aiecs/infrastructure/monitoring/__init__.py +34 -0
- aiecs/infrastructure/monitoring/executor_metrics.py +174 -0
- aiecs/infrastructure/monitoring/global_metrics_manager.py +213 -0
- aiecs/infrastructure/monitoring/structured_logger.py +48 -0
- aiecs/infrastructure/monitoring/tracing_manager.py +410 -0
- aiecs/infrastructure/persistence/__init__.py +24 -0
- aiecs/infrastructure/persistence/context_engine_client.py +187 -0
- aiecs/infrastructure/persistence/database_manager.py +333 -0
- aiecs/infrastructure/persistence/file_storage.py +754 -0
- aiecs/infrastructure/persistence/redis_client.py +220 -0
- aiecs/llm/__init__.py +86 -0
- aiecs/llm/callbacks/__init__.py +11 -0
- aiecs/llm/callbacks/custom_callbacks.py +264 -0
- aiecs/llm/client_factory.py +420 -0
- aiecs/llm/clients/__init__.py +33 -0
- aiecs/llm/clients/base_client.py +193 -0
- aiecs/llm/clients/googleai_client.py +181 -0
- aiecs/llm/clients/openai_client.py +131 -0
- aiecs/llm/clients/vertex_client.py +437 -0
- aiecs/llm/clients/xai_client.py +184 -0
- aiecs/llm/config/__init__.py +51 -0
- aiecs/llm/config/config_loader.py +275 -0
- aiecs/llm/config/config_validator.py +236 -0
- aiecs/llm/config/model_config.py +151 -0
- aiecs/llm/utils/__init__.py +10 -0
- aiecs/llm/utils/validate_config.py +91 -0
- aiecs/main.py +363 -0
- aiecs/scripts/__init__.py +3 -0
- aiecs/scripts/aid/VERSION_MANAGEMENT.md +97 -0
- aiecs/scripts/aid/__init__.py +19 -0
- aiecs/scripts/aid/version_manager.py +215 -0
- aiecs/scripts/dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md +242 -0
- aiecs/scripts/dependance_check/README_DEPENDENCY_CHECKER.md +310 -0
- aiecs/scripts/dependance_check/__init__.py +17 -0
- aiecs/scripts/dependance_check/dependency_checker.py +938 -0
- aiecs/scripts/dependance_check/dependency_fixer.py +391 -0
- aiecs/scripts/dependance_check/download_nlp_data.py +396 -0
- aiecs/scripts/dependance_check/quick_dependency_check.py +270 -0
- aiecs/scripts/dependance_check/setup_nlp_data.sh +217 -0
- aiecs/scripts/dependance_patch/__init__.py +7 -0
- aiecs/scripts/dependance_patch/fix_weasel/README_WEASEL_PATCH.md +126 -0
- aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
- aiecs/scripts/dependance_patch/fix_weasel/fix_weasel_validator.py +128 -0
- aiecs/scripts/dependance_patch/fix_weasel/fix_weasel_validator.sh +82 -0
- aiecs/scripts/dependance_patch/fix_weasel/patch_weasel_library.sh +188 -0
- aiecs/scripts/dependance_patch/fix_weasel/run_weasel_patch.sh +41 -0
- aiecs/scripts/tools_develop/README.md +449 -0
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/__init__.py +21 -0
- aiecs/scripts/tools_develop/check_type_annotations.py +259 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +422 -0
- aiecs/scripts/tools_develop/verify_tools.py +356 -0
- aiecs/tasks/__init__.py +1 -0
- aiecs/tasks/worker.py +172 -0
- aiecs/tools/__init__.py +299 -0
- aiecs/tools/apisource/__init__.py +99 -0
- aiecs/tools/apisource/intelligence/__init__.py +19 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +381 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +413 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +388 -0
- aiecs/tools/apisource/monitoring/__init__.py +9 -0
- aiecs/tools/apisource/monitoring/metrics.py +303 -0
- aiecs/tools/apisource/providers/__init__.py +115 -0
- aiecs/tools/apisource/providers/base.py +664 -0
- aiecs/tools/apisource/providers/census.py +401 -0
- aiecs/tools/apisource/providers/fred.py +564 -0
- aiecs/tools/apisource/providers/newsapi.py +412 -0
- aiecs/tools/apisource/providers/worldbank.py +357 -0
- aiecs/tools/apisource/reliability/__init__.py +12 -0
- aiecs/tools/apisource/reliability/error_handler.py +375 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +391 -0
- aiecs/tools/apisource/tool.py +850 -0
- aiecs/tools/apisource/utils/__init__.py +9 -0
- aiecs/tools/apisource/utils/validators.py +338 -0
- aiecs/tools/base_tool.py +201 -0
- aiecs/tools/docs/__init__.py +121 -0
- aiecs/tools/docs/ai_document_orchestrator.py +599 -0
- aiecs/tools/docs/ai_document_writer_orchestrator.py +2403 -0
- aiecs/tools/docs/content_insertion_tool.py +1333 -0
- aiecs/tools/docs/document_creator_tool.py +1317 -0
- aiecs/tools/docs/document_layout_tool.py +1166 -0
- aiecs/tools/docs/document_parser_tool.py +994 -0
- aiecs/tools/docs/document_writer_tool.py +1818 -0
- aiecs/tools/knowledge_graph/__init__.py +17 -0
- aiecs/tools/knowledge_graph/graph_reasoning_tool.py +734 -0
- aiecs/tools/knowledge_graph/graph_search_tool.py +923 -0
- aiecs/tools/knowledge_graph/kg_builder_tool.py +476 -0
- aiecs/tools/langchain_adapter.py +542 -0
- aiecs/tools/schema_generator.py +275 -0
- aiecs/tools/search_tool/__init__.py +100 -0
- aiecs/tools/search_tool/analyzers.py +589 -0
- aiecs/tools/search_tool/cache.py +260 -0
- aiecs/tools/search_tool/constants.py +128 -0
- aiecs/tools/search_tool/context.py +216 -0
- aiecs/tools/search_tool/core.py +749 -0
- aiecs/tools/search_tool/deduplicator.py +123 -0
- aiecs/tools/search_tool/error_handler.py +271 -0
- aiecs/tools/search_tool/metrics.py +371 -0
- aiecs/tools/search_tool/rate_limiter.py +178 -0
- aiecs/tools/search_tool/schemas.py +277 -0
- aiecs/tools/statistics/__init__.py +80 -0
- aiecs/tools/statistics/ai_data_analysis_orchestrator.py +643 -0
- aiecs/tools/statistics/ai_insight_generator_tool.py +505 -0
- aiecs/tools/statistics/ai_report_orchestrator_tool.py +694 -0
- aiecs/tools/statistics/data_loader_tool.py +564 -0
- aiecs/tools/statistics/data_profiler_tool.py +658 -0
- aiecs/tools/statistics/data_transformer_tool.py +573 -0
- aiecs/tools/statistics/data_visualizer_tool.py +495 -0
- aiecs/tools/statistics/model_trainer_tool.py +487 -0
- aiecs/tools/statistics/statistical_analyzer_tool.py +459 -0
- aiecs/tools/task_tools/__init__.py +86 -0
- aiecs/tools/task_tools/chart_tool.py +732 -0
- aiecs/tools/task_tools/classfire_tool.py +922 -0
- aiecs/tools/task_tools/image_tool.py +447 -0
- aiecs/tools/task_tools/office_tool.py +684 -0
- aiecs/tools/task_tools/pandas_tool.py +635 -0
- aiecs/tools/task_tools/report_tool.py +635 -0
- aiecs/tools/task_tools/research_tool.py +392 -0
- aiecs/tools/task_tools/scraper_tool.py +715 -0
- aiecs/tools/task_tools/stats_tool.py +688 -0
- aiecs/tools/temp_file_manager.py +130 -0
- aiecs/tools/tool_executor/__init__.py +37 -0
- aiecs/tools/tool_executor/tool_executor.py +881 -0
- aiecs/utils/LLM_output_structor.py +445 -0
- aiecs/utils/__init__.py +34 -0
- aiecs/utils/base_callback.py +47 -0
- aiecs/utils/cache_provider.py +695 -0
- aiecs/utils/execution_utils.py +184 -0
- aiecs/utils/logging.py +1 -0
- aiecs/utils/prompt_loader.py +14 -0
- aiecs/utils/token_usage_repository.py +323 -0
- aiecs/ws/__init__.py +0 -0
- aiecs/ws/socket_server.py +52 -0
- aiecs-1.5.1.dist-info/METADATA +608 -0
- aiecs-1.5.1.dist-info/RECORD +302 -0
- aiecs-1.5.1.dist-info/WHEEL +5 -0
- aiecs-1.5.1.dist-info/entry_points.txt +10 -0
- aiecs-1.5.1.dist-info/licenses/LICENSE +225 -0
- aiecs-1.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query Profiler for Knowledge Graph
|
|
3
|
+
|
|
4
|
+
Provides detailed profiling and timing metrics for graph queries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Any, List, Optional
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class QueryProfile:
|
|
19
|
+
"""
|
|
20
|
+
Profile data for a single query execution
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
query_id: Unique identifier for the query
|
|
24
|
+
query_type: Type of query (search, traverse, etc.)
|
|
25
|
+
start_time: When the query started
|
|
26
|
+
end_time: When the query completed
|
|
27
|
+
duration_ms: Total duration in milliseconds
|
|
28
|
+
steps: List of execution steps with timing
|
|
29
|
+
metadata: Additional query metadata
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
query_id: str
|
|
33
|
+
query_type: str
|
|
34
|
+
start_time: datetime
|
|
35
|
+
end_time: Optional[datetime] = None
|
|
36
|
+
duration_ms: Optional[float] = None
|
|
37
|
+
steps: List[Dict[str, Any]] = field(default_factory=list)
|
|
38
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
def complete(self) -> None:
|
|
41
|
+
"""Mark query as complete and calculate duration"""
|
|
42
|
+
self.end_time = datetime.utcnow()
|
|
43
|
+
if self.start_time:
|
|
44
|
+
delta = self.end_time - self.start_time
|
|
45
|
+
self.duration_ms = delta.total_seconds() * 1000
|
|
46
|
+
|
|
47
|
+
def add_step(
|
|
48
|
+
self,
|
|
49
|
+
name: str,
|
|
50
|
+
duration_ms: float,
|
|
51
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Add an execution step"""
|
|
54
|
+
self.steps.append(
|
|
55
|
+
{
|
|
56
|
+
"name": name,
|
|
57
|
+
"duration_ms": duration_ms,
|
|
58
|
+
"metadata": metadata or {},
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
63
|
+
"""Convert to dictionary"""
|
|
64
|
+
return {
|
|
65
|
+
"query_id": self.query_id,
|
|
66
|
+
"query_type": self.query_type,
|
|
67
|
+
"start_time": self.start_time.isoformat(),
|
|
68
|
+
"end_time": self.end_time.isoformat() if self.end_time else None,
|
|
69
|
+
"duration_ms": self.duration_ms,
|
|
70
|
+
"steps": self.steps,
|
|
71
|
+
"metadata": self.metadata,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class QueryProfiler:
|
|
76
|
+
"""
|
|
77
|
+
Query profiler for detailed performance analysis
|
|
78
|
+
|
|
79
|
+
Tracks query execution with step-by-step timing and metadata.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```python
|
|
83
|
+
profiler = QueryProfiler()
|
|
84
|
+
|
|
85
|
+
# Profile a query
|
|
86
|
+
async with profiler.profile("search_query", "vector_search") as profile:
|
|
87
|
+
# Step 1
|
|
88
|
+
async with profiler.step(profile, "embedding_lookup"):
|
|
89
|
+
embedding = await get_embedding(query)
|
|
90
|
+
|
|
91
|
+
# Step 2
|
|
92
|
+
async with profiler.step(profile, "vector_search"):
|
|
93
|
+
results = await search(embedding)
|
|
94
|
+
|
|
95
|
+
# Get profile
|
|
96
|
+
profile_data = profiler.get_profile("search_query")
|
|
97
|
+
print(f"Total: {profile_data.duration_ms}ms")
|
|
98
|
+
for step in profile_data.steps:
|
|
99
|
+
print(f" {step['name']}: {step['duration_ms']}ms")
|
|
100
|
+
```
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, max_profiles: int = 1000):
|
|
104
|
+
"""
|
|
105
|
+
Initialize query profiler
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
max_profiles: Maximum number of profiles to keep in memory
|
|
109
|
+
"""
|
|
110
|
+
self.max_profiles = max_profiles
|
|
111
|
+
self.profiles: Dict[str, QueryProfile] = {}
|
|
112
|
+
self._profile_order: List[str] = []
|
|
113
|
+
|
|
114
|
+
@asynccontextmanager
|
|
115
|
+
async def profile(
|
|
116
|
+
self,
|
|
117
|
+
query_id: str,
|
|
118
|
+
query_type: str,
|
|
119
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
Context manager for profiling a query
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
query_id: Unique identifier for the query
|
|
126
|
+
query_type: Type of query
|
|
127
|
+
metadata: Additional metadata
|
|
128
|
+
|
|
129
|
+
Yields:
|
|
130
|
+
QueryProfile object
|
|
131
|
+
"""
|
|
132
|
+
# Create profile
|
|
133
|
+
profile = QueryProfile(
|
|
134
|
+
query_id=query_id,
|
|
135
|
+
query_type=query_type,
|
|
136
|
+
start_time=datetime.utcnow(),
|
|
137
|
+
metadata=metadata or {},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
yield profile
|
|
142
|
+
finally:
|
|
143
|
+
# Complete profile
|
|
144
|
+
profile.complete()
|
|
145
|
+
|
|
146
|
+
# Store profile
|
|
147
|
+
self._store_profile(profile)
|
|
148
|
+
|
|
149
|
+
@asynccontextmanager
|
|
150
|
+
async def step(
|
|
151
|
+
self,
|
|
152
|
+
profile: QueryProfile,
|
|
153
|
+
step_name: str,
|
|
154
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
155
|
+
):
|
|
156
|
+
"""
|
|
157
|
+
Context manager for profiling a query step
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
profile: Parent query profile
|
|
161
|
+
step_name: Name of the step
|
|
162
|
+
metadata: Additional metadata
|
|
163
|
+
"""
|
|
164
|
+
start_time = time.perf_counter()
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
yield
|
|
168
|
+
finally:
|
|
169
|
+
end_time = time.perf_counter()
|
|
170
|
+
duration_ms = (end_time - start_time) * 1000
|
|
171
|
+
profile.add_step(step_name, duration_ms, metadata)
|
|
172
|
+
|
|
173
|
+
def _store_profile(self, profile: QueryProfile) -> None:
|
|
174
|
+
"""Store profile with LRU eviction"""
|
|
175
|
+
# Add to storage
|
|
176
|
+
self.profiles[profile.query_id] = profile
|
|
177
|
+
self._profile_order.append(profile.query_id)
|
|
178
|
+
|
|
179
|
+
# Evict oldest if over limit
|
|
180
|
+
while len(self._profile_order) > self.max_profiles:
|
|
181
|
+
oldest_id = self._profile_order.pop(0)
|
|
182
|
+
if oldest_id in self.profiles:
|
|
183
|
+
del self.profiles[oldest_id]
|
|
184
|
+
|
|
185
|
+
def get_profile(self, query_id: str) -> Optional[QueryProfile]:
|
|
186
|
+
"""Get profile by query ID"""
|
|
187
|
+
return self.profiles.get(query_id)
|
|
188
|
+
|
|
189
|
+
def get_all_profiles(self) -> List[QueryProfile]:
|
|
190
|
+
"""Get all stored profiles"""
|
|
191
|
+
return list(self.profiles.values())
|
|
192
|
+
|
|
193
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
194
|
+
"""Get profiling statistics"""
|
|
195
|
+
if not self.profiles:
|
|
196
|
+
return {
|
|
197
|
+
"total_queries": 0,
|
|
198
|
+
"avg_duration_ms": 0,
|
|
199
|
+
"min_duration_ms": 0,
|
|
200
|
+
"max_duration_ms": 0,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
durations = [p.duration_ms for p in self.profiles.values() if p.duration_ms]
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"total_queries": len(self.profiles),
|
|
207
|
+
"avg_duration_ms": (sum(durations) / len(durations) if durations else 0),
|
|
208
|
+
"min_duration_ms": min(durations) if durations else 0,
|
|
209
|
+
"max_duration_ms": max(durations) if durations else 0,
|
|
210
|
+
"query_types": self._get_query_type_stats(),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def _get_query_type_stats(self) -> Dict[str, int]:
|
|
214
|
+
"""Get statistics by query type"""
|
|
215
|
+
stats: Dict[str, int] = {}
|
|
216
|
+
for profile in self.profiles.values():
|
|
217
|
+
stats[profile.query_type] = stats.get(profile.query_type, 0) + 1
|
|
218
|
+
return stats
|
|
219
|
+
|
|
220
|
+
def clear(self) -> None:
|
|
221
|
+
"""Clear all profiles"""
|
|
222
|
+
self.profiles.clear()
|
|
223
|
+
self._profile_order.clear()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Knowledge Graph Reasoning Module
|
|
3
|
+
|
|
4
|
+
Provides query planning, multi-hop reasoning, and inference capabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aiecs.application.knowledge_graph.reasoning.query_planner import (
|
|
8
|
+
QueryPlanner,
|
|
9
|
+
)
|
|
10
|
+
from aiecs.application.knowledge_graph.reasoning.reasoning_engine import (
|
|
11
|
+
ReasoningEngine,
|
|
12
|
+
)
|
|
13
|
+
from aiecs.application.knowledge_graph.reasoning.inference_engine import (
|
|
14
|
+
InferenceEngine,
|
|
15
|
+
InferenceCache,
|
|
16
|
+
)
|
|
17
|
+
from aiecs.application.knowledge_graph.reasoning.evidence_synthesis import (
|
|
18
|
+
EvidenceSynthesizer,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"QueryPlanner",
|
|
23
|
+
"ReasoningEngine",
|
|
24
|
+
"InferenceEngine",
|
|
25
|
+
"InferenceCache",
|
|
26
|
+
"EvidenceSynthesizer",
|
|
27
|
+
]
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evidence Synthesis
|
|
3
|
+
|
|
4
|
+
Combine and synthesize evidence from multiple sources for robust reasoning.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import List, Optional, Dict, Any
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from aiecs.domain.knowledge_graph.models.evidence import Evidence
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EvidenceSynthesizer:
|
|
14
|
+
"""
|
|
15
|
+
Evidence Synthesizer
|
|
16
|
+
|
|
17
|
+
Combines evidence from multiple sources to create more robust conclusions.
|
|
18
|
+
|
|
19
|
+
Features:
|
|
20
|
+
- Merge overlapping evidence
|
|
21
|
+
- Calculate combined confidence
|
|
22
|
+
- Detect contradictions
|
|
23
|
+
- Synthesize explanations
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
```python
|
|
27
|
+
synthesizer = EvidenceSynthesizer()
|
|
28
|
+
|
|
29
|
+
# Combine evidence from different sources
|
|
30
|
+
combined = synthesizer.synthesize_evidence([ev1, ev2, ev3])
|
|
31
|
+
|
|
32
|
+
# Get most reliable evidence
|
|
33
|
+
reliable = synthesizer.filter_by_confidence(combined, threshold=0.7)
|
|
34
|
+
```
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
confidence_threshold: float = 0.5,
|
|
40
|
+
contradiction_threshold: float = 0.3,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize evidence synthesizer
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
confidence_threshold: Minimum confidence for evidence
|
|
47
|
+
contradiction_threshold: Threshold for detecting contradictions
|
|
48
|
+
"""
|
|
49
|
+
self.confidence_threshold = confidence_threshold
|
|
50
|
+
self.contradiction_threshold = contradiction_threshold
|
|
51
|
+
|
|
52
|
+
def synthesize_evidence(
|
|
53
|
+
self, evidence_list: List[Evidence], method: str = "weighted_average"
|
|
54
|
+
) -> List[Evidence]:
|
|
55
|
+
"""
|
|
56
|
+
Synthesize evidence from multiple sources
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
evidence_list: List of evidence to synthesize
|
|
60
|
+
method: Synthesis method ("weighted_average", "max", "voting")
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Synthesized evidence list
|
|
64
|
+
"""
|
|
65
|
+
if not evidence_list:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
# Group evidence by entity overlap
|
|
69
|
+
groups = self._group_overlapping_evidence(evidence_list)
|
|
70
|
+
|
|
71
|
+
# Synthesize each group
|
|
72
|
+
synthesized = []
|
|
73
|
+
for group in groups:
|
|
74
|
+
if len(group) == 1:
|
|
75
|
+
synthesized.append(group[0])
|
|
76
|
+
else:
|
|
77
|
+
combined = self._combine_evidence_group(group, method)
|
|
78
|
+
synthesized.append(combined)
|
|
79
|
+
|
|
80
|
+
return synthesized
|
|
81
|
+
|
|
82
|
+
def _group_overlapping_evidence(self, evidence_list: List[Evidence]) -> List[List[Evidence]]:
|
|
83
|
+
"""
|
|
84
|
+
Group evidence that refers to overlapping entities
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
evidence_list: List of evidence to group
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of evidence groups
|
|
91
|
+
"""
|
|
92
|
+
groups = []
|
|
93
|
+
used = set()
|
|
94
|
+
|
|
95
|
+
for i, ev1 in enumerate(evidence_list):
|
|
96
|
+
if i in used:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
group = [ev1]
|
|
100
|
+
ev1_entities = set(ev1.get_entity_ids())
|
|
101
|
+
used.add(i)
|
|
102
|
+
|
|
103
|
+
# Find overlapping evidence
|
|
104
|
+
for j, ev2 in enumerate(evidence_list):
|
|
105
|
+
if j <= i or j in used:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
ev2_entities = set(ev2.get_entity_ids())
|
|
109
|
+
overlap = ev1_entities & ev2_entities
|
|
110
|
+
|
|
111
|
+
# If significant overlap, add to group
|
|
112
|
+
if len(overlap) > 0:
|
|
113
|
+
group.append(ev2)
|
|
114
|
+
used.add(j)
|
|
115
|
+
|
|
116
|
+
groups.append(group)
|
|
117
|
+
|
|
118
|
+
return groups
|
|
119
|
+
|
|
120
|
+
def _combine_evidence_group(self, group: List[Evidence], method: str) -> Evidence:
|
|
121
|
+
"""
|
|
122
|
+
Combine a group of overlapping evidence
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
group: Group of evidence to combine
|
|
126
|
+
method: Combination method
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Combined evidence
|
|
130
|
+
"""
|
|
131
|
+
if not group:
|
|
132
|
+
raise ValueError("Cannot combine empty evidence group")
|
|
133
|
+
|
|
134
|
+
if len(group) == 1:
|
|
135
|
+
return group[0]
|
|
136
|
+
|
|
137
|
+
# Collect all entities and relations
|
|
138
|
+
all_entities = []
|
|
139
|
+
all_relations = []
|
|
140
|
+
all_paths = []
|
|
141
|
+
seen_entity_ids = set()
|
|
142
|
+
seen_relation_ids = set()
|
|
143
|
+
|
|
144
|
+
for ev in group:
|
|
145
|
+
for entity in ev.entities:
|
|
146
|
+
if entity.id not in seen_entity_ids:
|
|
147
|
+
all_entities.append(entity)
|
|
148
|
+
seen_entity_ids.add(entity.id)
|
|
149
|
+
|
|
150
|
+
for relation in ev.relations:
|
|
151
|
+
if relation.id not in seen_relation_ids:
|
|
152
|
+
all_relations.append(relation)
|
|
153
|
+
seen_relation_ids.add(relation.id)
|
|
154
|
+
|
|
155
|
+
all_paths.extend(ev.paths)
|
|
156
|
+
|
|
157
|
+
# Calculate combined confidence and relevance
|
|
158
|
+
if method == "weighted_average":
|
|
159
|
+
# Weight by number of supporting evidence
|
|
160
|
+
total_confidence = sum(ev.confidence for ev in group)
|
|
161
|
+
total_relevance = sum(ev.relevance_score for ev in group)
|
|
162
|
+
confidence = total_confidence / len(group)
|
|
163
|
+
relevance = total_relevance / len(group)
|
|
164
|
+
|
|
165
|
+
elif method == "max":
|
|
166
|
+
# Take maximum
|
|
167
|
+
confidence = max(ev.confidence for ev in group)
|
|
168
|
+
relevance = max(ev.relevance_score for ev in group)
|
|
169
|
+
|
|
170
|
+
elif method == "voting":
|
|
171
|
+
# Majority voting with confidence weights
|
|
172
|
+
confidence = sum(ev.confidence for ev in group) / len(group)
|
|
173
|
+
relevance = sum(ev.relevance_score for ev in group) / len(group)
|
|
174
|
+
|
|
175
|
+
else:
|
|
176
|
+
# Default to weighted average
|
|
177
|
+
confidence = sum(ev.confidence for ev in group) / len(group)
|
|
178
|
+
relevance = sum(ev.relevance_score for ev in group) / len(group)
|
|
179
|
+
|
|
180
|
+
# Boost confidence if multiple sources agree
|
|
181
|
+
agreement_boost = min(0.1 * (len(group) - 1), 0.3)
|
|
182
|
+
confidence = min(1.0, confidence + agreement_boost)
|
|
183
|
+
|
|
184
|
+
# Create combined explanation
|
|
185
|
+
sources = list(set(ev.source for ev in group if ev.source))
|
|
186
|
+
explanation = f"Combined from {len(group)} sources: {', '.join(sources[:3])}"
|
|
187
|
+
if len(group) > 1:
|
|
188
|
+
explanation += (
|
|
189
|
+
f"\nAgreement across {len(group)} pieces of evidence increases confidence"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Create synthesized evidence
|
|
193
|
+
combined = Evidence(
|
|
194
|
+
evidence_id=f"synth_{uuid.uuid4().hex[:8]}",
|
|
195
|
+
evidence_type=group[0].evidence_type,
|
|
196
|
+
entities=all_entities,
|
|
197
|
+
relations=all_relations,
|
|
198
|
+
paths=all_paths,
|
|
199
|
+
confidence=confidence,
|
|
200
|
+
relevance_score=relevance,
|
|
201
|
+
explanation=explanation,
|
|
202
|
+
source="synthesis",
|
|
203
|
+
metadata={
|
|
204
|
+
"source_count": len(group),
|
|
205
|
+
"source_evidence_ids": [ev.evidence_id for ev in group],
|
|
206
|
+
"synthesis_method": method,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return combined
|
|
211
|
+
|
|
212
|
+
def filter_by_confidence(
|
|
213
|
+
self, evidence_list: List[Evidence], threshold: Optional[float] = None
|
|
214
|
+
) -> List[Evidence]:
|
|
215
|
+
"""
|
|
216
|
+
Filter evidence by confidence threshold
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
evidence_list: List of evidence to filter
|
|
220
|
+
threshold: Confidence threshold (uses default if None)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Filtered evidence list
|
|
224
|
+
"""
|
|
225
|
+
threshold = threshold if threshold is not None else self.confidence_threshold
|
|
226
|
+
return [ev for ev in evidence_list if ev.confidence >= threshold]
|
|
227
|
+
|
|
228
|
+
def detect_contradictions(self, evidence_list: List[Evidence]) -> List[Dict[str, Any]]:
|
|
229
|
+
"""
|
|
230
|
+
Detect contradictions in evidence
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
evidence_list: List of evidence to check
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of detected contradictions
|
|
237
|
+
"""
|
|
238
|
+
contradictions = []
|
|
239
|
+
|
|
240
|
+
# Group by entity
|
|
241
|
+
entity_evidence: Dict[str, List[Evidence]] = defaultdict(list)
|
|
242
|
+
for ev in evidence_list:
|
|
243
|
+
for entity in ev.entities:
|
|
244
|
+
entity_evidence[entity.id].append(ev)
|
|
245
|
+
|
|
246
|
+
# Check for contradictory claims
|
|
247
|
+
for entity_id, evidence_group in entity_evidence.items():
|
|
248
|
+
if len(evidence_group) < 2:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
# Look for low confidence with high relevance (potential
|
|
252
|
+
# contradiction)
|
|
253
|
+
confidences = [ev.confidence for ev in evidence_group]
|
|
254
|
+
if max(confidences) - min(confidences) > self.contradiction_threshold:
|
|
255
|
+
contradictions.append(
|
|
256
|
+
{
|
|
257
|
+
"entity_id": entity_id,
|
|
258
|
+
"evidence_ids": [ev.evidence_id for ev in evidence_group],
|
|
259
|
+
"confidence_range": (
|
|
260
|
+
min(confidences),
|
|
261
|
+
max(confidences),
|
|
262
|
+
),
|
|
263
|
+
"description": f"Conflicting confidence scores for entity {entity_id}",
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return contradictions
|
|
268
|
+
|
|
269
|
+
def estimate_overall_confidence(self, evidence_list: List[Evidence]) -> float:
|
|
270
|
+
"""
|
|
271
|
+
Estimate overall confidence from evidence list
|
|
272
|
+
|
|
273
|
+
Considers:
|
|
274
|
+
- Individual confidence scores
|
|
275
|
+
- Agreement across evidence
|
|
276
|
+
- Source diversity
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
evidence_list: List of evidence
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Overall confidence score (0-1)
|
|
283
|
+
"""
|
|
284
|
+
if not evidence_list:
|
|
285
|
+
return 0.0
|
|
286
|
+
|
|
287
|
+
# Base confidence (average)
|
|
288
|
+
base_confidence = sum(ev.confidence for ev in evidence_list) / len(evidence_list)
|
|
289
|
+
|
|
290
|
+
# Source diversity bonus
|
|
291
|
+
sources = set(ev.source for ev in evidence_list if ev.source)
|
|
292
|
+
diversity_bonus = min(0.1 * (len(sources) - 1), 0.2)
|
|
293
|
+
|
|
294
|
+
# Agreement bonus (entities appearing in multiple evidence)
|
|
295
|
+
entity_counts: Dict[str, int] = defaultdict(int)
|
|
296
|
+
for ev in evidence_list:
|
|
297
|
+
for entity_id in ev.get_entity_ids():
|
|
298
|
+
entity_counts[entity_id] += 1
|
|
299
|
+
|
|
300
|
+
# Average entity appearance count
|
|
301
|
+
if entity_counts:
|
|
302
|
+
avg_appearances = sum(entity_counts.values()) / len(entity_counts)
|
|
303
|
+
agreement_bonus = min(0.1 * (avg_appearances - 1), 0.15)
|
|
304
|
+
else:
|
|
305
|
+
agreement_bonus = 0.0
|
|
306
|
+
|
|
307
|
+
# Combined confidence
|
|
308
|
+
overall = base_confidence + diversity_bonus + agreement_bonus
|
|
309
|
+
return min(1.0, overall)
|
|
310
|
+
|
|
311
|
+
def rank_by_reliability(self, evidence_list: List[Evidence]) -> List[Evidence]:
|
|
312
|
+
"""
|
|
313
|
+
Rank evidence by reliability
|
|
314
|
+
|
|
315
|
+
Considers:
|
|
316
|
+
- Confidence score
|
|
317
|
+
- Relevance score
|
|
318
|
+
- Source credibility
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
evidence_list: List of evidence to rank
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Ranked evidence list (most reliable first)
|
|
325
|
+
"""
|
|
326
|
+
# Calculate reliability score for each evidence
|
|
327
|
+
scored = []
|
|
328
|
+
for ev in evidence_list:
|
|
329
|
+
# Base score from confidence and relevance
|
|
330
|
+
reliability = (ev.confidence * 0.6) + (ev.relevance_score * 0.4)
|
|
331
|
+
|
|
332
|
+
# Boost for synthesis (already vetted)
|
|
333
|
+
if ev.source == "synthesis":
|
|
334
|
+
reliability *= 1.1
|
|
335
|
+
|
|
336
|
+
# Boost for multiple supporting elements
|
|
337
|
+
element_count = len(ev.entities) + len(ev.relations) + len(ev.paths)
|
|
338
|
+
if element_count > 3:
|
|
339
|
+
reliability *= 1.05
|
|
340
|
+
|
|
341
|
+
reliability = min(1.0, reliability)
|
|
342
|
+
scored.append((ev, reliability))
|
|
343
|
+
|
|
344
|
+
# Sort by reliability (descending)
|
|
345
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
346
|
+
|
|
347
|
+
return [ev for ev, score in scored]
|