aiecs 1.0.1__py3-none-any.whl → 1.7.6__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.
Potentially problematic release.
This version of aiecs might be problematic. Click here for more details.
- aiecs/__init__.py +13 -16
- aiecs/__main__.py +7 -7
- aiecs/aiecs_client.py +269 -75
- aiecs/application/executors/operation_executor.py +79 -54
- aiecs/application/knowledge_graph/__init__.py +7 -0
- aiecs/application/knowledge_graph/builder/__init__.py +37 -0
- aiecs/application/knowledge_graph/builder/data_quality.py +302 -0
- aiecs/application/knowledge_graph/builder/data_reshaping.py +293 -0
- aiecs/application/knowledge_graph/builder/document_builder.py +369 -0
- aiecs/application/knowledge_graph/builder/graph_builder.py +490 -0
- aiecs/application/knowledge_graph/builder/import_optimizer.py +396 -0
- aiecs/application/knowledge_graph/builder/schema_inference.py +462 -0
- aiecs/application/knowledge_graph/builder/schema_mapping.py +563 -0
- aiecs/application/knowledge_graph/builder/structured_pipeline.py +1384 -0
- aiecs/application/knowledge_graph/builder/text_chunker.py +317 -0
- aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
- aiecs/application/knowledge_graph/extractors/base.py +98 -0
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +422 -0
- aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +347 -0
- aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +241 -0
- aiecs/application/knowledge_graph/fusion/__init__.py +78 -0
- aiecs/application/knowledge_graph/fusion/ab_testing.py +395 -0
- aiecs/application/knowledge_graph/fusion/abbreviation_expander.py +327 -0
- aiecs/application/knowledge_graph/fusion/alias_index.py +597 -0
- aiecs/application/knowledge_graph/fusion/alias_matcher.py +384 -0
- aiecs/application/knowledge_graph/fusion/cache_coordinator.py +343 -0
- aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +433 -0
- aiecs/application/knowledge_graph/fusion/entity_linker.py +511 -0
- aiecs/application/knowledge_graph/fusion/evaluation_dataset.py +240 -0
- aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +632 -0
- aiecs/application/knowledge_graph/fusion/matching_config.py +489 -0
- aiecs/application/knowledge_graph/fusion/name_normalizer.py +352 -0
- aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +183 -0
- aiecs/application/knowledge_graph/fusion/semantic_name_matcher.py +464 -0
- aiecs/application/knowledge_graph/fusion/similarity_pipeline.py +534 -0
- aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
- aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +342 -0
- aiecs/application/knowledge_graph/pattern_matching/query_executor.py +366 -0
- aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
- aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +195 -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 +341 -0
- aiecs/application/knowledge_graph/reasoning/inference_engine.py +500 -0
- aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +163 -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 +913 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +866 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +475 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +396 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +208 -0
- aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +170 -0
- aiecs/application/knowledge_graph/reasoning/query_planner.py +855 -0
- aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +518 -0
- aiecs/application/knowledge_graph/retrieval/__init__.py +27 -0
- aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +211 -0
- aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +592 -0
- aiecs/application/knowledge_graph/retrieval/strategy_types.py +23 -0
- aiecs/application/knowledge_graph/search/__init__.py +59 -0
- aiecs/application/knowledge_graph/search/hybrid_search.py +457 -0
- aiecs/application/knowledge_graph/search/reranker.py +293 -0
- aiecs/application/knowledge_graph/search/reranker_strategies.py +535 -0
- aiecs/application/knowledge_graph/search/text_similarity.py +392 -0
- aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
- aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +305 -0
- aiecs/application/knowledge_graph/traversal/path_scorer.py +271 -0
- aiecs/application/knowledge_graph/validators/__init__.py +13 -0
- aiecs/application/knowledge_graph/validators/relation_validator.py +239 -0
- aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
- aiecs/application/knowledge_graph/visualization/graph_visualizer.py +313 -0
- aiecs/common/__init__.py +9 -0
- aiecs/common/knowledge_graph/__init__.py +17 -0
- aiecs/common/knowledge_graph/runnable.py +471 -0
- aiecs/config/__init__.py +20 -5
- aiecs/config/config.py +762 -31
- aiecs/config/graph_config.py +131 -0
- aiecs/config/tool_config.py +399 -0
- aiecs/core/__init__.py +29 -13
- aiecs/core/interface/__init__.py +2 -2
- aiecs/core/interface/execution_interface.py +22 -22
- aiecs/core/interface/storage_interface.py +37 -88
- aiecs/core/registry/__init__.py +31 -0
- aiecs/core/registry/service_registry.py +92 -0
- aiecs/domain/__init__.py +270 -1
- aiecs/domain/agent/__init__.py +191 -0
- aiecs/domain/agent/base_agent.py +3870 -0
- aiecs/domain/agent/exceptions.py +99 -0
- aiecs/domain/agent/graph_aware_mixin.py +569 -0
- aiecs/domain/agent/hybrid_agent.py +1435 -0
- aiecs/domain/agent/integration/__init__.py +29 -0
- aiecs/domain/agent/integration/context_compressor.py +216 -0
- aiecs/domain/agent/integration/context_engine_adapter.py +587 -0
- aiecs/domain/agent/integration/protocols.py +281 -0
- aiecs/domain/agent/integration/retry_policy.py +218 -0
- aiecs/domain/agent/integration/role_config.py +213 -0
- aiecs/domain/agent/knowledge_aware_agent.py +1892 -0
- aiecs/domain/agent/lifecycle.py +291 -0
- aiecs/domain/agent/llm_agent.py +692 -0
- aiecs/domain/agent/memory/__init__.py +12 -0
- aiecs/domain/agent/memory/conversation.py +1124 -0
- aiecs/domain/agent/migration/__init__.py +14 -0
- aiecs/domain/agent/migration/conversion.py +163 -0
- aiecs/domain/agent/migration/legacy_wrapper.py +86 -0
- aiecs/domain/agent/models.py +884 -0
- aiecs/domain/agent/observability.py +479 -0
- aiecs/domain/agent/persistence.py +449 -0
- aiecs/domain/agent/prompts/__init__.py +29 -0
- aiecs/domain/agent/prompts/builder.py +159 -0
- aiecs/domain/agent/prompts/formatters.py +187 -0
- aiecs/domain/agent/prompts/template.py +255 -0
- aiecs/domain/agent/registry.py +253 -0
- aiecs/domain/agent/tool_agent.py +444 -0
- aiecs/domain/agent/tools/__init__.py +15 -0
- aiecs/domain/agent/tools/schema_generator.py +364 -0
- aiecs/domain/community/__init__.py +155 -0
- aiecs/domain/community/agent_adapter.py +469 -0
- aiecs/domain/community/analytics.py +432 -0
- aiecs/domain/community/collaborative_workflow.py +648 -0
- aiecs/domain/community/communication_hub.py +634 -0
- aiecs/domain/community/community_builder.py +320 -0
- aiecs/domain/community/community_integration.py +796 -0
- aiecs/domain/community/community_manager.py +803 -0
- aiecs/domain/community/decision_engine.py +849 -0
- aiecs/domain/community/exceptions.py +231 -0
- aiecs/domain/community/models/__init__.py +33 -0
- aiecs/domain/community/models/community_models.py +234 -0
- aiecs/domain/community/resource_manager.py +461 -0
- aiecs/domain/community/shared_context_manager.py +589 -0
- aiecs/domain/context/__init__.py +40 -10
- aiecs/domain/context/context_engine.py +1910 -0
- aiecs/domain/context/conversation_models.py +87 -53
- aiecs/domain/context/graph_memory.py +582 -0
- aiecs/domain/execution/model.py +12 -4
- aiecs/domain/knowledge_graph/__init__.py +19 -0
- aiecs/domain/knowledge_graph/models/__init__.py +52 -0
- aiecs/domain/knowledge_graph/models/entity.py +148 -0
- aiecs/domain/knowledge_graph/models/evidence.py +178 -0
- aiecs/domain/knowledge_graph/models/inference_rule.py +184 -0
- aiecs/domain/knowledge_graph/models/path.py +171 -0
- aiecs/domain/knowledge_graph/models/path_pattern.py +171 -0
- aiecs/domain/knowledge_graph/models/query.py +261 -0
- aiecs/domain/knowledge_graph/models/query_plan.py +181 -0
- aiecs/domain/knowledge_graph/models/relation.py +202 -0
- aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
- aiecs/domain/knowledge_graph/schema/entity_type.py +131 -0
- aiecs/domain/knowledge_graph/schema/graph_schema.py +253 -0
- aiecs/domain/knowledge_graph/schema/property_schema.py +143 -0
- aiecs/domain/knowledge_graph/schema/relation_type.py +163 -0
- aiecs/domain/knowledge_graph/schema/schema_manager.py +691 -0
- aiecs/domain/knowledge_graph/schema/type_enums.py +209 -0
- aiecs/domain/task/dsl_processor.py +172 -56
- aiecs/domain/task/model.py +20 -8
- aiecs/domain/task/task_context.py +27 -24
- aiecs/infrastructure/__init__.py +0 -2
- aiecs/infrastructure/graph_storage/__init__.py +11 -0
- aiecs/infrastructure/graph_storage/base.py +837 -0
- aiecs/infrastructure/graph_storage/batch_operations.py +458 -0
- aiecs/infrastructure/graph_storage/cache.py +424 -0
- aiecs/infrastructure/graph_storage/distributed.py +223 -0
- aiecs/infrastructure/graph_storage/error_handling.py +380 -0
- aiecs/infrastructure/graph_storage/graceful_degradation.py +294 -0
- aiecs/infrastructure/graph_storage/health_checks.py +378 -0
- aiecs/infrastructure/graph_storage/in_memory.py +1197 -0
- aiecs/infrastructure/graph_storage/index_optimization.py +446 -0
- aiecs/infrastructure/graph_storage/lazy_loading.py +431 -0
- aiecs/infrastructure/graph_storage/metrics.py +344 -0
- aiecs/infrastructure/graph_storage/migration.py +400 -0
- aiecs/infrastructure/graph_storage/pagination.py +483 -0
- aiecs/infrastructure/graph_storage/performance_monitoring.py +456 -0
- aiecs/infrastructure/graph_storage/postgres.py +1563 -0
- aiecs/infrastructure/graph_storage/property_storage.py +353 -0
- aiecs/infrastructure/graph_storage/protocols.py +76 -0
- aiecs/infrastructure/graph_storage/query_optimizer.py +642 -0
- aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
- aiecs/infrastructure/graph_storage/sqlite.py +1373 -0
- aiecs/infrastructure/graph_storage/streaming.py +487 -0
- aiecs/infrastructure/graph_storage/tenant.py +412 -0
- aiecs/infrastructure/messaging/celery_task_manager.py +92 -54
- aiecs/infrastructure/messaging/websocket_manager.py +51 -35
- aiecs/infrastructure/monitoring/__init__.py +22 -0
- aiecs/infrastructure/monitoring/executor_metrics.py +45 -11
- aiecs/infrastructure/monitoring/global_metrics_manager.py +212 -0
- aiecs/infrastructure/monitoring/structured_logger.py +3 -7
- aiecs/infrastructure/monitoring/tracing_manager.py +63 -35
- aiecs/infrastructure/persistence/__init__.py +14 -1
- aiecs/infrastructure/persistence/context_engine_client.py +184 -0
- aiecs/infrastructure/persistence/database_manager.py +67 -43
- aiecs/infrastructure/persistence/file_storage.py +180 -103
- aiecs/infrastructure/persistence/redis_client.py +74 -21
- aiecs/llm/__init__.py +73 -25
- aiecs/llm/callbacks/__init__.py +11 -0
- aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +26 -19
- aiecs/llm/client_factory.py +224 -36
- aiecs/llm/client_resolver.py +155 -0
- aiecs/llm/clients/__init__.py +38 -0
- aiecs/llm/clients/base_client.py +324 -0
- aiecs/llm/clients/google_function_calling_mixin.py +457 -0
- aiecs/llm/clients/googleai_client.py +241 -0
- aiecs/llm/clients/openai_client.py +158 -0
- aiecs/llm/clients/openai_compatible_mixin.py +367 -0
- aiecs/llm/clients/vertex_client.py +897 -0
- aiecs/llm/clients/xai_client.py +201 -0
- aiecs/llm/config/__init__.py +51 -0
- aiecs/llm/config/config_loader.py +272 -0
- aiecs/llm/config/config_validator.py +206 -0
- aiecs/llm/config/model_config.py +143 -0
- aiecs/llm/protocols.py +149 -0
- aiecs/llm/utils/__init__.py +10 -0
- aiecs/llm/utils/validate_config.py +89 -0
- aiecs/main.py +140 -121
- aiecs/scripts/aid/VERSION_MANAGEMENT.md +138 -0
- aiecs/scripts/aid/__init__.py +19 -0
- aiecs/scripts/aid/module_checker.py +499 -0
- aiecs/scripts/aid/version_manager.py +235 -0
- aiecs/scripts/{DEPENDENCY_SYSTEM_SUMMARY.md → dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md} +1 -0
- aiecs/scripts/{README_DEPENDENCY_CHECKER.md → dependance_check/README_DEPENDENCY_CHECKER.md} +1 -0
- aiecs/scripts/dependance_check/__init__.py +15 -0
- aiecs/scripts/dependance_check/dependency_checker.py +1835 -0
- aiecs/scripts/{dependency_fixer.py → dependance_check/dependency_fixer.py} +192 -90
- aiecs/scripts/{download_nlp_data.py → dependance_check/download_nlp_data.py} +203 -71
- aiecs/scripts/dependance_patch/__init__.py +7 -0
- aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
- aiecs/scripts/{fix_weasel_validator.py → dependance_patch/fix_weasel/fix_weasel_validator.py} +21 -14
- aiecs/scripts/{patch_weasel_library.sh → dependance_patch/fix_weasel/patch_weasel_library.sh} +1 -1
- aiecs/scripts/knowledge_graph/__init__.py +3 -0
- aiecs/scripts/knowledge_graph/run_threshold_experiments.py +212 -0
- aiecs/scripts/migrations/multi_tenancy/README.md +142 -0
- aiecs/scripts/tools_develop/README.md +671 -0
- aiecs/scripts/tools_develop/README_CONFIG_CHECKER.md +273 -0
- aiecs/scripts/tools_develop/TOOLS_CONFIG_GUIDE.md +1287 -0
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/__init__.py +21 -0
- aiecs/scripts/tools_develop/check_all_tools_config.py +548 -0
- aiecs/scripts/tools_develop/check_type_annotations.py +257 -0
- aiecs/scripts/tools_develop/pre-commit-schema-coverage.sh +66 -0
- aiecs/scripts/tools_develop/schema_coverage.py +511 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +475 -0
- aiecs/scripts/tools_develop/verify_executor_config_fix.py +98 -0
- aiecs/scripts/tools_develop/verify_tools.py +352 -0
- aiecs/tasks/__init__.py +0 -1
- aiecs/tasks/worker.py +115 -47
- aiecs/tools/__init__.py +194 -72
- aiecs/tools/apisource/__init__.py +99 -0
- aiecs/tools/apisource/intelligence/__init__.py +19 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +632 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +417 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +385 -0
- aiecs/tools/apisource/monitoring/__init__.py +9 -0
- aiecs/tools/apisource/monitoring/metrics.py +330 -0
- aiecs/tools/apisource/providers/__init__.py +112 -0
- aiecs/tools/apisource/providers/base.py +671 -0
- aiecs/tools/apisource/providers/census.py +397 -0
- aiecs/tools/apisource/providers/fred.py +535 -0
- aiecs/tools/apisource/providers/newsapi.py +409 -0
- aiecs/tools/apisource/providers/worldbank.py +352 -0
- aiecs/tools/apisource/reliability/__init__.py +12 -0
- aiecs/tools/apisource/reliability/error_handler.py +363 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +376 -0
- aiecs/tools/apisource/tool.py +832 -0
- aiecs/tools/apisource/utils/__init__.py +9 -0
- aiecs/tools/apisource/utils/validators.py +334 -0
- aiecs/tools/base_tool.py +415 -21
- aiecs/tools/docs/__init__.py +121 -0
- aiecs/tools/docs/ai_document_orchestrator.py +607 -0
- aiecs/tools/docs/ai_document_writer_orchestrator.py +2350 -0
- aiecs/tools/docs/content_insertion_tool.py +1320 -0
- aiecs/tools/docs/document_creator_tool.py +1323 -0
- aiecs/tools/docs/document_layout_tool.py +1160 -0
- aiecs/tools/docs/document_parser_tool.py +1011 -0
- aiecs/tools/docs/document_writer_tool.py +1829 -0
- aiecs/tools/knowledge_graph/__init__.py +17 -0
- aiecs/tools/knowledge_graph/graph_reasoning_tool.py +807 -0
- aiecs/tools/knowledge_graph/graph_search_tool.py +944 -0
- aiecs/tools/knowledge_graph/kg_builder_tool.py +524 -0
- aiecs/tools/langchain_adapter.py +300 -138
- aiecs/tools/schema_generator.py +455 -0
- aiecs/tools/search_tool/__init__.py +100 -0
- aiecs/tools/search_tool/analyzers.py +581 -0
- aiecs/tools/search_tool/cache.py +264 -0
- aiecs/tools/search_tool/constants.py +128 -0
- aiecs/tools/search_tool/context.py +224 -0
- aiecs/tools/search_tool/core.py +778 -0
- aiecs/tools/search_tool/deduplicator.py +119 -0
- aiecs/tools/search_tool/error_handler.py +242 -0
- aiecs/tools/search_tool/metrics.py +343 -0
- aiecs/tools/search_tool/rate_limiter.py +172 -0
- aiecs/tools/search_tool/schemas.py +275 -0
- aiecs/tools/statistics/__init__.py +80 -0
- aiecs/tools/statistics/ai_data_analysis_orchestrator.py +646 -0
- aiecs/tools/statistics/ai_insight_generator_tool.py +508 -0
- aiecs/tools/statistics/ai_report_orchestrator_tool.py +684 -0
- aiecs/tools/statistics/data_loader_tool.py +555 -0
- aiecs/tools/statistics/data_profiler_tool.py +638 -0
- aiecs/tools/statistics/data_transformer_tool.py +580 -0
- aiecs/tools/statistics/data_visualizer_tool.py +498 -0
- aiecs/tools/statistics/model_trainer_tool.py +507 -0
- aiecs/tools/statistics/statistical_analyzer_tool.py +472 -0
- aiecs/tools/task_tools/__init__.py +49 -36
- aiecs/tools/task_tools/chart_tool.py +200 -184
- aiecs/tools/task_tools/classfire_tool.py +268 -267
- aiecs/tools/task_tools/image_tool.py +175 -131
- aiecs/tools/task_tools/office_tool.py +226 -146
- aiecs/tools/task_tools/pandas_tool.py +477 -121
- aiecs/tools/task_tools/report_tool.py +390 -142
- aiecs/tools/task_tools/research_tool.py +149 -79
- aiecs/tools/task_tools/scraper_tool.py +339 -145
- aiecs/tools/task_tools/stats_tool.py +448 -209
- aiecs/tools/temp_file_manager.py +26 -24
- aiecs/tools/tool_executor/__init__.py +18 -16
- aiecs/tools/tool_executor/tool_executor.py +364 -52
- aiecs/utils/LLM_output_structor.py +74 -48
- aiecs/utils/__init__.py +14 -3
- aiecs/utils/base_callback.py +0 -3
- aiecs/utils/cache_provider.py +696 -0
- aiecs/utils/execution_utils.py +50 -31
- aiecs/utils/prompt_loader.py +1 -0
- aiecs/utils/token_usage_repository.py +37 -11
- aiecs/ws/socket_server.py +14 -4
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/METADATA +52 -15
- aiecs-1.7.6.dist-info/RECORD +337 -0
- aiecs-1.7.6.dist-info/entry_points.txt +13 -0
- aiecs/config/registry.py +0 -19
- aiecs/domain/context/content_engine.py +0 -982
- aiecs/llm/base_client.py +0 -99
- aiecs/llm/openai_client.py +0 -125
- aiecs/llm/vertex_client.py +0 -186
- aiecs/llm/xai_client.py +0 -184
- aiecs/scripts/dependency_checker.py +0 -857
- aiecs/scripts/quick_dependency_check.py +0 -269
- aiecs/tools/task_tools/search_api.py +0 -7
- aiecs-1.0.1.dist-info/RECORD +0 -90
- aiecs-1.0.1.dist-info/entry_points.txt +0 -7
- /aiecs/scripts/{setup_nlp_data.sh → dependance_check/setup_nlp_data.sh} +0 -0
- /aiecs/scripts/{README_WEASEL_PATCH.md → dependance_patch/fix_weasel/README_WEASEL_PATCH.md} +0 -0
- /aiecs/scripts/{fix_weasel_validator.sh → dependance_patch/fix_weasel/fix_weasel_validator.sh} +0 -0
- /aiecs/scripts/{run_weasel_patch.sh → dependance_patch/fix_weasel/run_weasel_patch.sh} +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/WHEEL +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Text Similarity Utilities
|
|
3
|
+
|
|
4
|
+
Provides various text similarity and matching functions for knowledge graph operations.
|
|
5
|
+
Includes BM25, Jaccard, cosine similarity, Levenshtein distance, and fuzzy matching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import math
|
|
10
|
+
from typing import List, Optional, Tuple, Callable, Any
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from difflib import SequenceMatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BM25Scorer:
|
|
16
|
+
"""
|
|
17
|
+
BM25 (Best Matching 25) scorer for text similarity
|
|
18
|
+
|
|
19
|
+
BM25 is a ranking function used to estimate the relevance of documents
|
|
20
|
+
to a given search query. It's an improvement over TF-IDF.
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
scorer = BM25Scorer(corpus=[
|
|
25
|
+
"The quick brown fox jumps over the lazy dog",
|
|
26
|
+
"A quick brown dog jumps over a lazy fox",
|
|
27
|
+
"The lazy dog sleeps all day"
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
scores = scorer.score("quick brown fox")
|
|
31
|
+
# Returns scores for each document in corpus
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
corpus: List[str],
|
|
37
|
+
k1: float = 1.5,
|
|
38
|
+
b: float = 0.75,
|
|
39
|
+
tokenizer: Optional[Callable[[str], List[str]]] = None,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Initialize BM25 scorer
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
corpus: List of documents to score against
|
|
46
|
+
k1: Term frequency saturation parameter (default: 1.5)
|
|
47
|
+
b: Length normalization parameter (default: 0.75)
|
|
48
|
+
tokenizer: Optional tokenizer function (default: simple word split)
|
|
49
|
+
"""
|
|
50
|
+
self.k1 = k1
|
|
51
|
+
self.b = b
|
|
52
|
+
self.tokenizer = tokenizer or self._default_tokenizer
|
|
53
|
+
|
|
54
|
+
# Tokenize corpus
|
|
55
|
+
self.documents = [self.tokenizer(doc) for doc in corpus]
|
|
56
|
+
self.doc_count = len(self.documents)
|
|
57
|
+
|
|
58
|
+
# Calculate document lengths
|
|
59
|
+
self.doc_lengths = [len(doc) for doc in self.documents]
|
|
60
|
+
self.avg_doc_length = sum(self.doc_lengths) / self.doc_count if self.doc_count > 0 else 0
|
|
61
|
+
|
|
62
|
+
# Build term frequency dictionary
|
|
63
|
+
self.term_freqs = []
|
|
64
|
+
self.doc_freqs: Counter[str] = Counter()
|
|
65
|
+
|
|
66
|
+
for doc in self.documents:
|
|
67
|
+
tf = Counter(doc)
|
|
68
|
+
self.term_freqs.append(tf)
|
|
69
|
+
for term in set(doc):
|
|
70
|
+
self.doc_freqs[term] += 1
|
|
71
|
+
|
|
72
|
+
# Calculate IDF (Inverse Document Frequency)
|
|
73
|
+
self.idf = {}
|
|
74
|
+
for term, df in self.doc_freqs.items():
|
|
75
|
+
self.idf[term] = math.log((self.doc_count - df + 0.5) / (df + 0.5) + 1.0)
|
|
76
|
+
|
|
77
|
+
def _default_tokenizer(self, text: str) -> List[str]:
|
|
78
|
+
"""Default tokenizer: lowercase and split on whitespace"""
|
|
79
|
+
return re.findall(r"\w+", text.lower())
|
|
80
|
+
|
|
81
|
+
def score(self, query: str) -> List[float]:
|
|
82
|
+
"""
|
|
83
|
+
Score documents against query
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
query: Query string
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of BM25 scores for each document
|
|
90
|
+
"""
|
|
91
|
+
query_terms = self.tokenizer(query)
|
|
92
|
+
scores = []
|
|
93
|
+
|
|
94
|
+
for i, doc in enumerate(self.documents):
|
|
95
|
+
score = 0.0
|
|
96
|
+
doc_length = self.doc_lengths[i]
|
|
97
|
+
term_freq = self.term_freqs[i]
|
|
98
|
+
|
|
99
|
+
for term in query_terms:
|
|
100
|
+
if term in term_freq:
|
|
101
|
+
tf = term_freq[term]
|
|
102
|
+
idf = self.idf.get(term, 0.0)
|
|
103
|
+
|
|
104
|
+
# BM25 formula
|
|
105
|
+
numerator = idf * tf * (self.k1 + 1)
|
|
106
|
+
denominator = tf + self.k1 * (1 - self.b + self.b * (doc_length / self.avg_doc_length))
|
|
107
|
+
score += numerator / denominator
|
|
108
|
+
|
|
109
|
+
scores.append(score)
|
|
110
|
+
|
|
111
|
+
return scores
|
|
112
|
+
|
|
113
|
+
def get_top_n(self, query: str, n: int = 10) -> List[Tuple[int, float]]:
|
|
114
|
+
"""
|
|
115
|
+
Get top N documents by BM25 score
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
query: Query string
|
|
119
|
+
n: Number of top results to return
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of (document_index, score) tuples, sorted by score descending
|
|
123
|
+
"""
|
|
124
|
+
scores = self.score(query)
|
|
125
|
+
indexed_scores = [(i, score) for i, score in enumerate(scores)]
|
|
126
|
+
indexed_scores.sort(key=lambda x: x[1], reverse=True)
|
|
127
|
+
return indexed_scores[:n]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def jaccard_similarity(set1: set, set2: set) -> float:
|
|
131
|
+
"""
|
|
132
|
+
Calculate Jaccard similarity between two sets
|
|
133
|
+
|
|
134
|
+
Jaccard similarity = (size of intersection) / (size of union)
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
set1: First set
|
|
138
|
+
set2: Second set
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Jaccard similarity score (0.0 to 1.0)
|
|
142
|
+
"""
|
|
143
|
+
if not set1 and not set2:
|
|
144
|
+
return 1.0
|
|
145
|
+
|
|
146
|
+
intersection = len(set1 & set2)
|
|
147
|
+
union = len(set1 | set2)
|
|
148
|
+
|
|
149
|
+
if union == 0:
|
|
150
|
+
return 0.0
|
|
151
|
+
|
|
152
|
+
return intersection / union
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def jaccard_similarity_text(text1: str, text2: str, tokenizer: Optional[Callable[[str], Any]] = None) -> float:
|
|
156
|
+
"""
|
|
157
|
+
Calculate Jaccard similarity between two text strings
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
text1: First text string
|
|
161
|
+
text2: Second text string
|
|
162
|
+
tokenizer: Optional tokenizer function (default: word split)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Jaccard similarity score (0.0 to 1.0)
|
|
166
|
+
"""
|
|
167
|
+
if tokenizer is None:
|
|
168
|
+
|
|
169
|
+
def tokenizer(t):
|
|
170
|
+
return set(re.findall(r"\w+", t.lower()))
|
|
171
|
+
|
|
172
|
+
else:
|
|
173
|
+
# Wrap tokenizer to ensure it returns a set
|
|
174
|
+
original_tokenizer = tokenizer
|
|
175
|
+
|
|
176
|
+
def tokenizer(t):
|
|
177
|
+
return set(original_tokenizer(t))
|
|
178
|
+
|
|
179
|
+
set1 = tokenizer(text1)
|
|
180
|
+
set2 = tokenizer(text2)
|
|
181
|
+
|
|
182
|
+
return jaccard_similarity(set1, set2)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def cosine_similarity_text(text1: str, text2: str, tokenizer: Optional[Callable[[str], List[str]]] = None) -> float:
|
|
186
|
+
"""
|
|
187
|
+
Calculate cosine similarity between two text strings
|
|
188
|
+
|
|
189
|
+
Cosine similarity measures the cosine of the angle between two vectors
|
|
190
|
+
in a multi-dimensional space. For text, vectors are TF-IDF representations.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
text1: First text string
|
|
194
|
+
text2: Second text string
|
|
195
|
+
tokenizer: Optional tokenizer function (default: word split)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Cosine similarity score (0.0 to 1.0)
|
|
199
|
+
"""
|
|
200
|
+
if tokenizer is None:
|
|
201
|
+
|
|
202
|
+
def tokenizer(t):
|
|
203
|
+
return re.findall(r"\w+", t.lower())
|
|
204
|
+
|
|
205
|
+
tokens1 = tokenizer(text1)
|
|
206
|
+
tokens2 = tokenizer(text2)
|
|
207
|
+
|
|
208
|
+
# Build vocabulary
|
|
209
|
+
vocab = set(tokens1) | set(tokens2)
|
|
210
|
+
|
|
211
|
+
if not vocab:
|
|
212
|
+
return 1.0 if not text1 and not text2 else 0.0
|
|
213
|
+
|
|
214
|
+
# Create term frequency vectors
|
|
215
|
+
tf1 = Counter(tokens1)
|
|
216
|
+
tf2 = Counter(tokens2)
|
|
217
|
+
|
|
218
|
+
# Calculate dot product and magnitudes
|
|
219
|
+
dot_product = sum(tf1.get(term, 0) * tf2.get(term, 0) for term in vocab)
|
|
220
|
+
magnitude1 = math.sqrt(sum(tf1.get(term, 0) ** 2 for term in vocab))
|
|
221
|
+
magnitude2 = math.sqrt(sum(tf2.get(term, 0) ** 2 for term in vocab))
|
|
222
|
+
|
|
223
|
+
if magnitude1 == 0 or magnitude2 == 0:
|
|
224
|
+
return 0.0
|
|
225
|
+
|
|
226
|
+
similarity = dot_product / (magnitude1 * magnitude2)
|
|
227
|
+
# Handle floating point precision issues
|
|
228
|
+
return min(1.0, max(0.0, similarity))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def levenshtein_distance(s1: str, s2: str) -> int:
|
|
232
|
+
"""
|
|
233
|
+
Calculate Levenshtein distance (edit distance) between two strings
|
|
234
|
+
|
|
235
|
+
Levenshtein distance is the minimum number of single-character edits
|
|
236
|
+
(insertions, deletions, or substitutions) required to change one string
|
|
237
|
+
into another.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
s1: First string
|
|
241
|
+
s2: Second string
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Levenshtein distance (0 = identical, higher = more different)
|
|
245
|
+
"""
|
|
246
|
+
if len(s1) < len(s2):
|
|
247
|
+
return levenshtein_distance(s2, s1)
|
|
248
|
+
|
|
249
|
+
if len(s2) == 0:
|
|
250
|
+
return len(s1)
|
|
251
|
+
|
|
252
|
+
# Use dynamic programming
|
|
253
|
+
previous_row = list(range(len(s2) + 1))
|
|
254
|
+
|
|
255
|
+
for i, c1 in enumerate(s1):
|
|
256
|
+
current_row = [i + 1]
|
|
257
|
+
for j, c2 in enumerate(s2):
|
|
258
|
+
insertions = previous_row[j + 1] + 1
|
|
259
|
+
deletions = current_row[j] + 1
|
|
260
|
+
substitutions = previous_row[j] + (c1 != c2)
|
|
261
|
+
current_row.append(min(insertions, deletions, substitutions))
|
|
262
|
+
previous_row = current_row
|
|
263
|
+
|
|
264
|
+
return previous_row[-1]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def normalized_levenshtein_similarity(s1: str, s2: str) -> float:
|
|
268
|
+
"""
|
|
269
|
+
Calculate normalized Levenshtein similarity (0.0 to 1.0)
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
s1: First string
|
|
273
|
+
s2: Second string
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Normalized similarity score (1.0 = identical, 0.0 = completely different)
|
|
277
|
+
"""
|
|
278
|
+
max_len = max(len(s1), len(s2))
|
|
279
|
+
if max_len == 0:
|
|
280
|
+
return 1.0
|
|
281
|
+
|
|
282
|
+
distance = levenshtein_distance(s1, s2)
|
|
283
|
+
return 1.0 - (distance / max_len)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def fuzzy_match(
|
|
287
|
+
query: str,
|
|
288
|
+
candidates: List[str],
|
|
289
|
+
threshold: float = 0.6,
|
|
290
|
+
method: str = "jaccard",
|
|
291
|
+
) -> List[Tuple[str, float]]:
|
|
292
|
+
"""
|
|
293
|
+
Find fuzzy matches for a query string in a list of candidates
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
query: Query string to match
|
|
297
|
+
candidates: List of candidate strings
|
|
298
|
+
threshold: Minimum similarity threshold (0.0 to 1.0)
|
|
299
|
+
method: Similarity method ("jaccard", "cosine", "levenshtein", "ratio")
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of (candidate, similarity_score) tuples above threshold,
|
|
303
|
+
sorted by score descending
|
|
304
|
+
"""
|
|
305
|
+
results = []
|
|
306
|
+
|
|
307
|
+
for candidate in candidates:
|
|
308
|
+
if method == "jaccard":
|
|
309
|
+
score = jaccard_similarity_text(query, candidate)
|
|
310
|
+
elif method == "cosine":
|
|
311
|
+
score = cosine_similarity_text(query, candidate)
|
|
312
|
+
elif method == "levenshtein":
|
|
313
|
+
score = normalized_levenshtein_similarity(query, candidate)
|
|
314
|
+
elif method == "ratio":
|
|
315
|
+
# Use SequenceMatcher ratio (built-in fuzzy matching)
|
|
316
|
+
score = SequenceMatcher(None, query.lower(), candidate.lower()).ratio()
|
|
317
|
+
else:
|
|
318
|
+
raise ValueError(f"Unknown method: {method}. Use 'jaccard', 'cosine', 'levenshtein', or 'ratio'")
|
|
319
|
+
|
|
320
|
+
if score >= threshold:
|
|
321
|
+
results.append((candidate, score))
|
|
322
|
+
|
|
323
|
+
# Sort by score descending
|
|
324
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
325
|
+
return results
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class TextSimilarity:
|
|
329
|
+
"""
|
|
330
|
+
Convenience class for text similarity operations
|
|
331
|
+
|
|
332
|
+
Provides a unified interface for various text similarity methods.
|
|
333
|
+
|
|
334
|
+
Example::
|
|
335
|
+
|
|
336
|
+
similarity = TextSimilarity()
|
|
337
|
+
|
|
338
|
+
# Jaccard similarity
|
|
339
|
+
score = similarity.jaccard("hello world", "world hello")
|
|
340
|
+
|
|
341
|
+
# Cosine similarity
|
|
342
|
+
score = similarity.cosine("machine learning", "deep learning")
|
|
343
|
+
|
|
344
|
+
# Levenshtein distance
|
|
345
|
+
distance = similarity.levenshtein("kitten", "sitting")
|
|
346
|
+
|
|
347
|
+
# Fuzzy matching
|
|
348
|
+
matches = similarity.fuzzy_match(
|
|
349
|
+
"python",
|
|
350
|
+
["python3", "pyton", "java", "pythn"],
|
|
351
|
+
threshold=0.7
|
|
352
|
+
)
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def __init__(self, tokenizer: Optional[Callable[[str], List[str]]] = None):
|
|
356
|
+
"""
|
|
357
|
+
Initialize TextSimilarity
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
tokenizer: Optional tokenizer function for text processing
|
|
361
|
+
"""
|
|
362
|
+
self.tokenizer = tokenizer
|
|
363
|
+
|
|
364
|
+
def jaccard(self, text1: str, text2: str) -> float:
|
|
365
|
+
"""Calculate Jaccard similarity between two texts"""
|
|
366
|
+
return jaccard_similarity_text(text1, text2, self.tokenizer)
|
|
367
|
+
|
|
368
|
+
def cosine(self, text1: str, text2: str) -> float:
|
|
369
|
+
"""Calculate cosine similarity between two texts"""
|
|
370
|
+
return cosine_similarity_text(text1, text2, self.tokenizer)
|
|
371
|
+
|
|
372
|
+
def levenshtein(self, text1: str, text2: str) -> int:
|
|
373
|
+
"""Calculate Levenshtein distance between two texts"""
|
|
374
|
+
return levenshtein_distance(text1, text2)
|
|
375
|
+
|
|
376
|
+
def levenshtein_similarity(self, text1: str, text2: str) -> float:
|
|
377
|
+
"""Calculate normalized Levenshtein similarity"""
|
|
378
|
+
return normalized_levenshtein_similarity(text1, text2)
|
|
379
|
+
|
|
380
|
+
def fuzzy_match(
|
|
381
|
+
self,
|
|
382
|
+
query: str,
|
|
383
|
+
candidates: List[str],
|
|
384
|
+
threshold: float = 0.6,
|
|
385
|
+
method: str = "jaccard",
|
|
386
|
+
) -> List[Tuple[str, float]]:
|
|
387
|
+
"""Find fuzzy matches for a query"""
|
|
388
|
+
return fuzzy_match(query, candidates, threshold, method)
|
|
389
|
+
|
|
390
|
+
def bm25(self, corpus: List[str], k1: float = 1.5, b: float = 0.75) -> BM25Scorer:
|
|
391
|
+
"""Create a BM25 scorer for a corpus"""
|
|
392
|
+
return BM25Scorer(corpus, k1=k1, b=b, tokenizer=self.tokenizer)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Knowledge Graph Traversal Application Layer
|
|
3
|
+
|
|
4
|
+
Advanced traversal algorithms and path ranking utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aiecs.application.knowledge_graph.traversal.path_scorer import PathScorer
|
|
8
|
+
from aiecs.application.knowledge_graph.traversal.enhanced_traversal import (
|
|
9
|
+
EnhancedTraversal,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"PathScorer",
|
|
14
|
+
"EnhancedTraversal",
|
|
15
|
+
]
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced Graph Traversal
|
|
3
|
+
|
|
4
|
+
Provides advanced traversal capabilities with PathPattern support,
|
|
5
|
+
cycle detection, and sophisticated path filtering.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from collections import deque
|
|
10
|
+
from aiecs.domain.knowledge_graph.models.relation import Relation
|
|
11
|
+
from aiecs.domain.knowledge_graph.models.path import Path
|
|
12
|
+
from aiecs.domain.knowledge_graph.models.path_pattern import (
|
|
13
|
+
PathPattern,
|
|
14
|
+
TraversalDirection,
|
|
15
|
+
)
|
|
16
|
+
from aiecs.infrastructure.graph_storage.base import GraphStore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EnhancedTraversal:
|
|
20
|
+
"""
|
|
21
|
+
Enhanced Graph Traversal Service
|
|
22
|
+
|
|
23
|
+
Provides advanced traversal capabilities beyond basic BFS:
|
|
24
|
+
- PathPattern-based traversal
|
|
25
|
+
- Cycle detection and handling
|
|
26
|
+
- Depth-limited traversal with constraints
|
|
27
|
+
- Path filtering by pattern
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
traversal = EnhancedTraversal(graph_store)
|
|
32
|
+
|
|
33
|
+
# Define pattern
|
|
34
|
+
pattern = PathPattern(
|
|
35
|
+
relation_types=["WORKS_FOR", "LOCATED_IN"],
|
|
36
|
+
max_depth=2,
|
|
37
|
+
allow_cycles=False
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Traverse with pattern
|
|
41
|
+
paths = await traversal.traverse_with_pattern(
|
|
42
|
+
start_entity_id="person_1",
|
|
43
|
+
pattern=pattern,
|
|
44
|
+
max_results=10
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, graph_store: GraphStore):
|
|
50
|
+
"""
|
|
51
|
+
Initialize enhanced traversal service
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
graph_store: Graph storage backend to use
|
|
55
|
+
"""
|
|
56
|
+
self.graph_store = graph_store
|
|
57
|
+
|
|
58
|
+
async def traverse_with_pattern(
|
|
59
|
+
self,
|
|
60
|
+
start_entity_id: str,
|
|
61
|
+
pattern: PathPattern,
|
|
62
|
+
max_results: int = 100,
|
|
63
|
+
) -> List[Path]:
|
|
64
|
+
"""
|
|
65
|
+
Traverse graph following a path pattern
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
start_entity_id: Starting entity ID
|
|
69
|
+
pattern: Path pattern to follow
|
|
70
|
+
max_results: Maximum number of paths to return
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of paths matching the pattern
|
|
74
|
+
"""
|
|
75
|
+
start_entity = await self.graph_store.get_entity(start_entity_id)
|
|
76
|
+
if start_entity is None:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
# Check if start entity is allowed
|
|
80
|
+
if not pattern.is_entity_allowed(start_entity.id, start_entity.entity_type):
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
paths: List[Path] = []
|
|
84
|
+
# visited_in_path: Set[str] = set() if not pattern.allow_cycles else
|
|
85
|
+
# None # Reserved for future use
|
|
86
|
+
|
|
87
|
+
# BFS with pattern matching
|
|
88
|
+
queue: deque = deque()
|
|
89
|
+
queue.append(
|
|
90
|
+
{
|
|
91
|
+
"entity": start_entity,
|
|
92
|
+
"path_entities": [start_entity],
|
|
93
|
+
"path_edges": [],
|
|
94
|
+
"depth": 0,
|
|
95
|
+
"visited": ({start_entity.id} if not pattern.allow_cycles else set()),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
while queue and len(paths) < max_results:
|
|
100
|
+
current = queue.popleft()
|
|
101
|
+
current_entity = current["entity"]
|
|
102
|
+
current_depth = current["depth"]
|
|
103
|
+
path_entities = current["path_entities"]
|
|
104
|
+
path_edges = current["path_edges"]
|
|
105
|
+
visited_nodes = current["visited"]
|
|
106
|
+
|
|
107
|
+
# Add path if it meets length requirements
|
|
108
|
+
if pattern.is_valid_path_length(len(path_edges)):
|
|
109
|
+
path = Path(nodes=path_entities, edges=path_edges)
|
|
110
|
+
paths.append(path)
|
|
111
|
+
|
|
112
|
+
# Continue traversal if not at max depth
|
|
113
|
+
if not pattern.should_continue_traversal(current_depth):
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# Get neighbors based on pattern direction
|
|
117
|
+
# pattern.direction is already a string due to use_enum_values=True
|
|
118
|
+
direction_str = pattern.direction if isinstance(pattern.direction, str) else pattern.direction.value
|
|
119
|
+
neighbors = await self.graph_store.get_neighbors(
|
|
120
|
+
current_entity.id,
|
|
121
|
+
relation_type=None, # We'll filter by pattern
|
|
122
|
+
direction=direction_str,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
for neighbor in neighbors:
|
|
126
|
+
# Check if entity is allowed
|
|
127
|
+
if not pattern.is_entity_allowed(neighbor.id, neighbor.entity_type):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Check for cycles
|
|
131
|
+
if not pattern.allow_cycles and neighbor.id in visited_nodes:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Get the relation between current and neighbor
|
|
135
|
+
# We need to find the actual relation
|
|
136
|
+
relation = await self._find_relation(current_entity.id, neighbor.id, pattern.direction)
|
|
137
|
+
|
|
138
|
+
if relation is None:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Check if relation is allowed at this depth
|
|
142
|
+
if not pattern.is_relation_allowed(relation.relation_type, current_depth):
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# For incoming direction, we need to reverse the relation for path construction
|
|
146
|
+
# because paths expect edges[i].source_id == nodes[i].id
|
|
147
|
+
direction_str = pattern.direction if isinstance(pattern.direction, str) else pattern.direction.value
|
|
148
|
+
if direction_str == "incoming":
|
|
149
|
+
# Reverse the relation: if we have e1->e2 and we're going from e2 to e1,
|
|
150
|
+
# the path needs e2->e1 (source=current, target=neighbor)
|
|
151
|
+
path_relation = Relation(
|
|
152
|
+
id=f"{relation.id}_reversed",
|
|
153
|
+
relation_type=relation.relation_type,
|
|
154
|
+
source_id=current_entity.id,
|
|
155
|
+
target_id=neighbor.id,
|
|
156
|
+
weight=relation.weight,
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
path_relation = relation
|
|
160
|
+
|
|
161
|
+
# Create new path state
|
|
162
|
+
new_path_entities = path_entities + [neighbor]
|
|
163
|
+
new_path_edges = path_edges + [path_relation]
|
|
164
|
+
new_visited = visited_nodes | {neighbor.id} if not pattern.allow_cycles else visited_nodes
|
|
165
|
+
|
|
166
|
+
queue.append(
|
|
167
|
+
{
|
|
168
|
+
"entity": neighbor,
|
|
169
|
+
"path_entities": new_path_entities,
|
|
170
|
+
"path_edges": new_path_edges,
|
|
171
|
+
"depth": current_depth + 1,
|
|
172
|
+
"visited": new_visited,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return paths
|
|
177
|
+
|
|
178
|
+
async def _find_relation(self, source_id: str, target_id: str, direction: TraversalDirection) -> Optional[Relation]:
|
|
179
|
+
"""
|
|
180
|
+
Find the relation between two entities
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
source_id: Source entity ID
|
|
184
|
+
target_id: Target entity ID
|
|
185
|
+
direction: Traversal direction (can be enum or string)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Relation if found, None otherwise
|
|
189
|
+
"""
|
|
190
|
+
# Try to find the actual relation in the graph store
|
|
191
|
+
# This works with both InMemoryGraphStore and SQLiteGraphStore
|
|
192
|
+
|
|
193
|
+
# Handle both enum and string directions
|
|
194
|
+
direction_str = direction if isinstance(direction, str) else direction.value
|
|
195
|
+
direction_enum = TraversalDirection(direction_str) if isinstance(direction, str) else direction
|
|
196
|
+
|
|
197
|
+
if direction_enum == TraversalDirection.OUTGOING or direction_enum == TraversalDirection.BOTH:
|
|
198
|
+
# Look for outgoing relations from source
|
|
199
|
+
neighbors = await self.graph_store.get_neighbors(source_id, relation_type=None, direction="outgoing")
|
|
200
|
+
for neighbor in neighbors:
|
|
201
|
+
if neighbor.id == target_id:
|
|
202
|
+
# Found the neighbor, now get the relation
|
|
203
|
+
# This is a workaround - ideally get_neighbors would return relations too
|
|
204
|
+
# For now, check if the store exposes relations
|
|
205
|
+
from aiecs.infrastructure.graph_storage.in_memory import (
|
|
206
|
+
InMemoryGraphStore,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if isinstance(self.graph_store, InMemoryGraphStore):
|
|
210
|
+
for rel in self.graph_store.relations.values():
|
|
211
|
+
if rel.source_id == source_id and rel.target_id == target_id:
|
|
212
|
+
return rel
|
|
213
|
+
else:
|
|
214
|
+
# For SQLite or other stores, try to get the relation
|
|
215
|
+
# This is a placeholder - real implementation would
|
|
216
|
+
# query the DB
|
|
217
|
+
return Relation(
|
|
218
|
+
id=f"rel_{source_id}_{target_id}",
|
|
219
|
+
relation_type="CONNECTED_TO",
|
|
220
|
+
source_id=source_id,
|
|
221
|
+
target_id=target_id,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if direction_enum == TraversalDirection.INCOMING or direction_enum == TraversalDirection.BOTH:
|
|
225
|
+
# Look for incoming relations to source (i.e., outgoing from
|
|
226
|
+
# target)
|
|
227
|
+
neighbors = await self.graph_store.get_neighbors(target_id, relation_type=None, direction="outgoing")
|
|
228
|
+
for neighbor in neighbors:
|
|
229
|
+
if neighbor.id == source_id:
|
|
230
|
+
from aiecs.infrastructure.graph_storage.in_memory import (
|
|
231
|
+
InMemoryGraphStore,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if isinstance(self.graph_store, InMemoryGraphStore):
|
|
235
|
+
for rel in self.graph_store.relations.values():
|
|
236
|
+
if rel.source_id == target_id and rel.target_id == source_id:
|
|
237
|
+
return rel
|
|
238
|
+
else:
|
|
239
|
+
return Relation(
|
|
240
|
+
id=f"rel_{target_id}_{source_id}",
|
|
241
|
+
relation_type="CONNECTED_TO",
|
|
242
|
+
source_id=target_id,
|
|
243
|
+
target_id=source_id,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
def detect_cycles(self, path: Path) -> bool:
|
|
249
|
+
"""
|
|
250
|
+
Detect if a path contains cycles (repeated nodes)
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
path: Path to check
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if path contains cycles
|
|
257
|
+
"""
|
|
258
|
+
entity_ids = path.get_entity_ids()
|
|
259
|
+
return len(entity_ids) != len(set(entity_ids))
|
|
260
|
+
|
|
261
|
+
def filter_paths_without_cycles(self, paths: List[Path]) -> List[Path]:
|
|
262
|
+
"""
|
|
263
|
+
Filter out paths that contain cycles
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
paths: List of paths to filter
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of paths without cycles
|
|
270
|
+
"""
|
|
271
|
+
return [path for path in paths if not self.detect_cycles(path)]
|
|
272
|
+
|
|
273
|
+
async def find_all_paths_between(
|
|
274
|
+
self,
|
|
275
|
+
source_id: str,
|
|
276
|
+
target_id: str,
|
|
277
|
+
pattern: Optional[PathPattern] = None,
|
|
278
|
+
max_paths: int = 10,
|
|
279
|
+
) -> List[Path]:
|
|
280
|
+
"""
|
|
281
|
+
Find all paths between two entities matching a pattern
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
source_id: Source entity ID
|
|
285
|
+
target_id: Target entity ID
|
|
286
|
+
pattern: Optional path pattern to follow
|
|
287
|
+
max_paths: Maximum number of paths to return
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of paths from source to target
|
|
291
|
+
"""
|
|
292
|
+
if pattern is None:
|
|
293
|
+
pattern = PathPattern(max_depth=5, allow_cycles=False)
|
|
294
|
+
|
|
295
|
+
# Traverse from source
|
|
296
|
+
all_paths = await self.traverse_with_pattern(
|
|
297
|
+
start_entity_id=source_id,
|
|
298
|
+
pattern=pattern,
|
|
299
|
+
max_results=max_paths * 10, # Get more paths for filtering
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Filter paths that end at target
|
|
303
|
+
target_paths = [path for path in all_paths if path.end_entity.id == target_id]
|
|
304
|
+
|
|
305
|
+
return target_paths[:max_paths]
|