aiecs 1.0.1__py3-none-any.whl → 1.7.17__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 +435 -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 +3949 -0
- aiecs/domain/agent/exceptions.py +99 -0
- aiecs/domain/agent/graph_aware_mixin.py +569 -0
- aiecs/domain/agent/hybrid_agent.py +1731 -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 +894 -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 +377 -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 +230 -37
- aiecs/llm/client_resolver.py +155 -0
- aiecs/llm/clients/__init__.py +38 -0
- aiecs/llm/clients/base_client.py +328 -0
- aiecs/llm/clients/google_function_calling_mixin.py +415 -0
- aiecs/llm/clients/googleai_client.py +314 -0
- aiecs/llm/clients/openai_client.py +158 -0
- aiecs/llm/clients/openai_compatible_mixin.py +367 -0
- aiecs/llm/clients/vertex_client.py +1186 -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 +1464 -0
- aiecs/tools/docs/document_layout_tool.py +1160 -0
- aiecs/tools/docs/document_parser_tool.py +1016 -0
- aiecs/tools/docs/document_writer_tool.py +2008 -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 +220 -141
- 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.17.dist-info}/METADATA +52 -15
- aiecs-1.7.17.dist-info/RECORD +337 -0
- aiecs-1.7.17.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.17.dist-info}/WHEEL +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Node Definitions for Logic Query Parser
|
|
3
|
+
|
|
4
|
+
This module defines the Abstract Syntax Tree (AST) node hierarchy for the Logic Query DSL.
|
|
5
|
+
Each node is self-contained and responsible for its own validation and conversion to QueryPlan.
|
|
6
|
+
|
|
7
|
+
Design Principles:
|
|
8
|
+
1. Self-Contained: Each node owns its complete structure (e.g., FindNode has filters)
|
|
9
|
+
2. Polymorphic Conversion: Each node implements its own to_query_plan() method
|
|
10
|
+
3. Immutable: Nodes are frozen dataclasses (cannot be modified after creation)
|
|
11
|
+
4. Type-Safe: Full type hints for all fields and methods
|
|
12
|
+
|
|
13
|
+
Phase: 2.4 - Logic Query Parser
|
|
14
|
+
Version: 1.0
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .query_context import QueryContext
|
|
27
|
+
|
|
28
|
+
# Import QueryPlan models
|
|
29
|
+
try:
|
|
30
|
+
from aiecs.domain.knowledge_graph.models.query_plan import (
|
|
31
|
+
QueryPlan,
|
|
32
|
+
QueryStep,
|
|
33
|
+
QueryOperation,
|
|
34
|
+
)
|
|
35
|
+
from aiecs.domain.knowledge_graph.models.query import GraphQuery, QueryType
|
|
36
|
+
|
|
37
|
+
QUERY_PLAN_AVAILABLE = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
QUERY_PLAN_AVAILABLE = False
|
|
40
|
+
# Use TYPE_CHECKING to avoid redefinition errors
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from typing import Any
|
|
43
|
+
QueryPlan: Any # type: ignore[assignment,no-redef]
|
|
44
|
+
QueryStep: Any # type: ignore[assignment,no-redef]
|
|
45
|
+
QueryOperation: Any # type: ignore[assignment,no-redef]
|
|
46
|
+
GraphQuery: Any # type: ignore[assignment,no-redef]
|
|
47
|
+
QueryType: Any # type: ignore[assignment,no-redef]
|
|
48
|
+
else:
|
|
49
|
+
from typing import Any
|
|
50
|
+
QueryPlan = None # type: ignore[assignment]
|
|
51
|
+
QueryStep = None # type: ignore[assignment]
|
|
52
|
+
QueryOperation = None # type: ignore[assignment]
|
|
53
|
+
GraphQuery = None # type: ignore[assignment]
|
|
54
|
+
QueryType = None # type: ignore[assignment]
|
|
55
|
+
|
|
56
|
+
# Placeholder for ValidationError (will be defined in error_handler.py)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ValidationError:
|
|
61
|
+
"""Validation error with location information"""
|
|
62
|
+
|
|
63
|
+
line: int
|
|
64
|
+
column: int
|
|
65
|
+
message: str
|
|
66
|
+
suggestion: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ASTNode(ABC):
|
|
71
|
+
"""
|
|
72
|
+
Base class for all AST nodes
|
|
73
|
+
|
|
74
|
+
All AST nodes must:
|
|
75
|
+
1. Store line/column metadata for error reporting
|
|
76
|
+
2. Implement validate() for semantic validation
|
|
77
|
+
3. Implement conversion to query plan (via to_query_plan or to_filter_dict)
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
line: Line number in source query (1-based)
|
|
81
|
+
column: Column number in source query (1-based)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
line: int
|
|
85
|
+
column: int
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def validate(self, schema: Any, entity_type: Optional[str] = None) -> List[ValidationError]:
|
|
89
|
+
"""
|
|
90
|
+
Validate this node against the schema
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
schema: SchemaManager instance for validation
|
|
94
|
+
entity_type: Optional entity type context for property validation
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of validation errors (empty if valid)
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class QueryNode(ASTNode):
|
|
103
|
+
"""
|
|
104
|
+
Top-level query node: Find + optional Traversals + optional WHERE
|
|
105
|
+
|
|
106
|
+
Represents a complete query with:
|
|
107
|
+
- Required: FindNode for entity selection
|
|
108
|
+
- Optional: List of TraversalNodes for graph navigation
|
|
109
|
+
- Optional: WHERE clause (embedded in FindNode.filters)
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
Find(Person) FOLLOWS AuthoredBy WHERE year > 2020
|
|
113
|
+
|
|
114
|
+
QueryNode(
|
|
115
|
+
find=FindNode(entity_type="Person", ...),
|
|
116
|
+
traversals=[TraversalNode(relation_type="AuthoredBy", ...)],
|
|
117
|
+
...
|
|
118
|
+
)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
find: "FindNode"
|
|
122
|
+
traversals: List["TraversalNode"] = field(default_factory=list)
|
|
123
|
+
|
|
124
|
+
def validate(self, schema: Any) -> List[ValidationError]:
|
|
125
|
+
"""Validate all parts of the query"""
|
|
126
|
+
errors = []
|
|
127
|
+
errors.extend(self.find.validate(schema))
|
|
128
|
+
for traversal in self.traversals:
|
|
129
|
+
errors.extend(traversal.validate(schema))
|
|
130
|
+
return errors
|
|
131
|
+
|
|
132
|
+
def to_query_plan(self, context: "QueryContext", original_query: str = "") -> Any:
|
|
133
|
+
"""
|
|
134
|
+
Convert QueryNode to QueryPlan
|
|
135
|
+
|
|
136
|
+
Creates a QueryPlan with multiple steps for complex queries with traversals.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
context: Query context for variable resolution
|
|
140
|
+
original_query: Original query string for documentation
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
QueryPlan with one or more QuerySteps
|
|
144
|
+
"""
|
|
145
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
146
|
+
raise ImportError("QueryPlan models not available")
|
|
147
|
+
|
|
148
|
+
# Generate plan ID
|
|
149
|
+
plan_id = f"plan_{uuid.uuid4().hex[:8]}"
|
|
150
|
+
|
|
151
|
+
# Convert to query steps
|
|
152
|
+
steps = self.to_query_steps(context)
|
|
153
|
+
|
|
154
|
+
# Create explanation
|
|
155
|
+
explanation = self._generate_explanation()
|
|
156
|
+
|
|
157
|
+
# Create QueryPlan
|
|
158
|
+
plan = QueryPlan(
|
|
159
|
+
plan_id=plan_id,
|
|
160
|
+
original_query=original_query or str(self),
|
|
161
|
+
steps=steps,
|
|
162
|
+
explanation=explanation,
|
|
163
|
+
optimized=False,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Calculate total cost
|
|
167
|
+
plan.total_estimated_cost = plan.calculate_total_cost()
|
|
168
|
+
|
|
169
|
+
return plan
|
|
170
|
+
|
|
171
|
+
def to_query_steps(self, context: "QueryContext") -> List[Any]:
|
|
172
|
+
"""
|
|
173
|
+
Convert QueryNode to list of QuerySteps
|
|
174
|
+
|
|
175
|
+
For simple queries (Find only), creates a single step.
|
|
176
|
+
For complex queries (Find + Traversals), creates multiple steps.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
context: Query context for variable resolution
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of QueryStep objects
|
|
183
|
+
"""
|
|
184
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
185
|
+
raise ImportError("QueryPlan models not available")
|
|
186
|
+
|
|
187
|
+
steps = []
|
|
188
|
+
|
|
189
|
+
# Step 1: Find entities (always present)
|
|
190
|
+
find_step = self.find.to_query_step(context, step_id="step_1")
|
|
191
|
+
steps.append(find_step)
|
|
192
|
+
|
|
193
|
+
# Steps 2+: Traversals (if any)
|
|
194
|
+
for i, traversal in enumerate(self.traversals, start=2):
|
|
195
|
+
step_id = f"step_{i}"
|
|
196
|
+
depends_on = [f"step_{i-1}"] # Each step depends on previous
|
|
197
|
+
traversal_step = traversal.to_query_step(context, step_id=step_id, depends_on=depends_on)
|
|
198
|
+
steps.append(traversal_step)
|
|
199
|
+
|
|
200
|
+
return steps
|
|
201
|
+
|
|
202
|
+
def _generate_explanation(self) -> str:
|
|
203
|
+
"""Generate human-readable explanation of the query"""
|
|
204
|
+
parts = [f"Find {self.find.entity_type} entities"]
|
|
205
|
+
|
|
206
|
+
if self.find.entity_name:
|
|
207
|
+
parts.append(f"named '{self.find.entity_name}'")
|
|
208
|
+
|
|
209
|
+
if self.find.filters:
|
|
210
|
+
parts.append(f"with {len(self.find.filters)} filter(s)")
|
|
211
|
+
|
|
212
|
+
if self.traversals:
|
|
213
|
+
parts.append(f"then traverse {len(self.traversals)} relation(s)")
|
|
214
|
+
|
|
215
|
+
return " ".join(parts)
|
|
216
|
+
|
|
217
|
+
def __repr__(self) -> str:
|
|
218
|
+
"""String representation for debugging"""
|
|
219
|
+
traversals_str = f", traversals={len(self.traversals)}" if self.traversals else ""
|
|
220
|
+
return f"QueryNode(find={self.find}{traversals_str})"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass(frozen=True)
|
|
224
|
+
class FindNode(ASTNode):
|
|
225
|
+
"""
|
|
226
|
+
Entity selection node: Find(EntityType) or Find(EntityType[`Name`])
|
|
227
|
+
|
|
228
|
+
Represents entity selection with optional filters.
|
|
229
|
+
This node is self-contained and owns its filters.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
entity_type: Type of entity to find (e.g., "Person", "Paper")
|
|
233
|
+
entity_name: Optional specific entity name (e.g., "Alice")
|
|
234
|
+
filters: List of filter nodes (from WHERE clause)
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
Find(Person[`Alice`]) WHERE age > 30
|
|
238
|
+
|
|
239
|
+
FindNode(
|
|
240
|
+
entity_type="Person",
|
|
241
|
+
entity_name="Alice",
|
|
242
|
+
filters=[PropertyFilterNode(property="age", operator=">", value=30)]
|
|
243
|
+
)
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
entity_type: str
|
|
247
|
+
entity_name: Optional[str] = None
|
|
248
|
+
filters: List["FilterNode"] = field(default_factory=list)
|
|
249
|
+
|
|
250
|
+
def validate(self, schema: Any, entity_type: Optional[str] = None) -> List[ValidationError]:
|
|
251
|
+
"""
|
|
252
|
+
Validate entity type and all filters
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
schema: Schema object for validation
|
|
256
|
+
entity_type: Optional entity type (uses self.entity_type if not provided)
|
|
257
|
+
"""
|
|
258
|
+
errors = []
|
|
259
|
+
|
|
260
|
+
# Use self.entity_type if entity_type parameter not provided
|
|
261
|
+
effective_entity_type = entity_type if entity_type is not None else self.entity_type
|
|
262
|
+
|
|
263
|
+
# Validate entity type exists (if schema has this method)
|
|
264
|
+
if hasattr(schema, "has_entity_type"):
|
|
265
|
+
if not schema.has_entity_type(effective_entity_type):
|
|
266
|
+
errors.append(
|
|
267
|
+
ValidationError(
|
|
268
|
+
self.line,
|
|
269
|
+
self.column,
|
|
270
|
+
f"Entity type '{effective_entity_type}' not found",
|
|
271
|
+
suggestion=f"Available types: {', '.join(schema.get_entity_types())}",
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Validate all filters (pass entity_type for property validation)
|
|
276
|
+
for filter_node in self.filters:
|
|
277
|
+
errors.extend(filter_node.validate(schema, entity_type=effective_entity_type))
|
|
278
|
+
|
|
279
|
+
return errors
|
|
280
|
+
|
|
281
|
+
def to_query_step(
|
|
282
|
+
self,
|
|
283
|
+
context: "QueryContext",
|
|
284
|
+
step_id: str = "step_1",
|
|
285
|
+
depends_on: Optional[List[str]] = None,
|
|
286
|
+
) -> Any:
|
|
287
|
+
"""
|
|
288
|
+
Convert FindNode to QueryStep
|
|
289
|
+
|
|
290
|
+
Creates a QueryStep for entity lookup/filter operation.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
context: Query context for variable resolution
|
|
294
|
+
step_id: Unique identifier for this step
|
|
295
|
+
depends_on: List of step IDs this step depends on
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
QueryStep for entity lookup/filter
|
|
299
|
+
"""
|
|
300
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
301
|
+
raise ImportError("QueryPlan models not available")
|
|
302
|
+
|
|
303
|
+
# Build property filters
|
|
304
|
+
properties = {}
|
|
305
|
+
if self.filters:
|
|
306
|
+
# Combine all filters into a single filter dict
|
|
307
|
+
for filter_node in self.filters:
|
|
308
|
+
filter_dict = filter_node.to_filter_dict(context)
|
|
309
|
+
properties.update(filter_dict)
|
|
310
|
+
|
|
311
|
+
# Determine query type and operation
|
|
312
|
+
# If entity_name is provided, it's an entity lookup
|
|
313
|
+
# Otherwise, it's a filter operation
|
|
314
|
+
if self.entity_name:
|
|
315
|
+
query_type = QueryType.ENTITY_LOOKUP
|
|
316
|
+
operation = QueryOperation.ENTITY_LOOKUP
|
|
317
|
+
else:
|
|
318
|
+
# For filter operations, we use ENTITY_LOOKUP query type with
|
|
319
|
+
# filters
|
|
320
|
+
query_type = QueryType.ENTITY_LOOKUP
|
|
321
|
+
operation = QueryOperation.FILTER
|
|
322
|
+
|
|
323
|
+
# Create GraphQuery
|
|
324
|
+
query = GraphQuery(
|
|
325
|
+
query_type=query_type,
|
|
326
|
+
entity_type=self.entity_type,
|
|
327
|
+
entity_id=self.entity_name, # If specific entity name is provided
|
|
328
|
+
properties=properties,
|
|
329
|
+
max_results=100, # Default limit
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Create description
|
|
333
|
+
description = f"Find {self.entity_type} entities"
|
|
334
|
+
if self.entity_name:
|
|
335
|
+
description += f" named '{self.entity_name}'"
|
|
336
|
+
if self.filters:
|
|
337
|
+
description += f" with {len(self.filters)} filter(s)"
|
|
338
|
+
|
|
339
|
+
# Create QueryStep
|
|
340
|
+
step = QueryStep(
|
|
341
|
+
step_id=step_id,
|
|
342
|
+
operation=operation,
|
|
343
|
+
query=query,
|
|
344
|
+
depends_on=depends_on or [],
|
|
345
|
+
description=description,
|
|
346
|
+
estimated_cost=0.3, # Low cost for simple entity lookup
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return step
|
|
350
|
+
|
|
351
|
+
def __repr__(self) -> str:
|
|
352
|
+
"""String representation for debugging"""
|
|
353
|
+
name_str = f"[`{self.entity_name}`]" if self.entity_name else ""
|
|
354
|
+
filters_str = f", filters={len(self.filters)}" if self.filters else ""
|
|
355
|
+
return f"FindNode({self.entity_type}{name_str}{filters_str})"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dataclass(frozen=True)
|
|
359
|
+
class TraversalNode(ASTNode):
|
|
360
|
+
"""
|
|
361
|
+
Graph traversal node: FOLLOWS RelationType [direction]
|
|
362
|
+
|
|
363
|
+
Represents navigation along graph relationships.
|
|
364
|
+
|
|
365
|
+
Attributes:
|
|
366
|
+
relation_type: Type of relation to follow (e.g., "AuthoredBy")
|
|
367
|
+
direction: Direction of traversal ("outgoing", "incoming", or None for default)
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
FOLLOWS AuthoredBy INCOMING
|
|
371
|
+
|
|
372
|
+
TraversalNode(
|
|
373
|
+
relation_type="AuthoredBy",
|
|
374
|
+
direction="incoming"
|
|
375
|
+
)
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
relation_type: str
|
|
379
|
+
direction: Optional[str] = "outgoing" # "incoming" | "outgoing" | None
|
|
380
|
+
|
|
381
|
+
def validate(self, schema: Any, entity_type: Optional[str] = None) -> List[ValidationError]:
|
|
382
|
+
"""
|
|
383
|
+
Validate relation type exists
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
schema: Schema object for validation
|
|
387
|
+
entity_type: Optional entity type (not used for traversal validation)
|
|
388
|
+
"""
|
|
389
|
+
errors = []
|
|
390
|
+
|
|
391
|
+
# Validate relation type exists (if schema has this method)
|
|
392
|
+
if hasattr(schema, "has_relation_type"):
|
|
393
|
+
if not schema.has_relation_type(self.relation_type):
|
|
394
|
+
errors.append(
|
|
395
|
+
ValidationError(
|
|
396
|
+
self.line,
|
|
397
|
+
self.column,
|
|
398
|
+
f"Relation type '{self.relation_type}' not found",
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Validate direction
|
|
403
|
+
if self.direction and self.direction not in ["incoming", "outgoing"]:
|
|
404
|
+
errors.append(
|
|
405
|
+
ValidationError(
|
|
406
|
+
self.line,
|
|
407
|
+
self.column,
|
|
408
|
+
f"Invalid direction '{self.direction}'. Must be 'incoming' or 'outgoing'",
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return errors
|
|
413
|
+
|
|
414
|
+
def to_query_step(self, context: "QueryContext", step_id: str, depends_on: List[str]) -> Any:
|
|
415
|
+
"""
|
|
416
|
+
Convert TraversalNode to QueryStep
|
|
417
|
+
|
|
418
|
+
Creates a QueryStep for graph traversal operation.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
context: Query context for variable resolution
|
|
422
|
+
step_id: Unique identifier for this step
|
|
423
|
+
depends_on: List of step IDs this step depends on
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
QueryStep for graph traversal
|
|
427
|
+
"""
|
|
428
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
429
|
+
raise ImportError("QueryPlan models not available")
|
|
430
|
+
|
|
431
|
+
# Create GraphQuery for traversal
|
|
432
|
+
query = GraphQuery(
|
|
433
|
+
query_type=QueryType.TRAVERSAL,
|
|
434
|
+
relation_type=self.relation_type,
|
|
435
|
+
max_depth=1, # Single hop traversal
|
|
436
|
+
max_results=100, # Default limit
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Create description
|
|
440
|
+
direction_str = self.direction.upper() if self.direction else "OUTGOING"
|
|
441
|
+
description = f"Traverse {self.relation_type} relation ({direction_str})"
|
|
442
|
+
|
|
443
|
+
# Create QueryStep
|
|
444
|
+
step = QueryStep(
|
|
445
|
+
step_id=step_id,
|
|
446
|
+
operation=QueryOperation.TRAVERSAL,
|
|
447
|
+
query=query,
|
|
448
|
+
depends_on=depends_on,
|
|
449
|
+
description=description,
|
|
450
|
+
estimated_cost=0.5, # Medium cost for traversal
|
|
451
|
+
metadata={"direction": self.direction or "outgoing"},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return step
|
|
455
|
+
|
|
456
|
+
def __repr__(self) -> str:
|
|
457
|
+
"""String representation for debugging"""
|
|
458
|
+
dir_str = f" {self.direction.upper()}" if self.direction else ""
|
|
459
|
+
return f"TraversalNode({self.relation_type}{dir_str})"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@dataclass(frozen=True)
|
|
463
|
+
class FilterNode(ASTNode):
|
|
464
|
+
"""
|
|
465
|
+
Base class for filter nodes (WHERE conditions)
|
|
466
|
+
|
|
467
|
+
Filter nodes represent conditions in WHERE clauses.
|
|
468
|
+
They convert to filter dictionaries (MongoDB-style) for query execution.
|
|
469
|
+
|
|
470
|
+
Subclasses:
|
|
471
|
+
- PropertyFilterNode: property operator value (e.g., age > 30)
|
|
472
|
+
- BooleanFilterNode: AND/OR/NOT combinations
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
@abstractmethod
|
|
476
|
+
def to_filter_dict(self, context: "QueryContext") -> Dict[str, Any]:
|
|
477
|
+
"""
|
|
478
|
+
Convert filter to MongoDB-style filter dictionary
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
context: Query context for variable resolution
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Filter dictionary (e.g., {"age": {"$gt": 30}})
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@dataclass(frozen=True)
|
|
489
|
+
class PropertyFilterNode(FilterNode):
|
|
490
|
+
"""
|
|
491
|
+
Property filter node: property operator value
|
|
492
|
+
|
|
493
|
+
Represents a comparison between a property and a value.
|
|
494
|
+
|
|
495
|
+
Attributes:
|
|
496
|
+
property_path: Property name or nested path (e.g., "age" or "address.city")
|
|
497
|
+
operator: Comparison operator (==, !=, >, <, >=, <=, IN, CONTAINS)
|
|
498
|
+
value: Value to compare against
|
|
499
|
+
|
|
500
|
+
Example:
|
|
501
|
+
age > 30
|
|
502
|
+
|
|
503
|
+
PropertyFilterNode(
|
|
504
|
+
property_path="age",
|
|
505
|
+
operator=">",
|
|
506
|
+
value=30
|
|
507
|
+
)
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
property_path: str # Can be nested: "address.city"
|
|
511
|
+
operator: str # "==", "!=", ">", "<", ">=", "<=", "IN", "CONTAINS"
|
|
512
|
+
value: Any
|
|
513
|
+
|
|
514
|
+
def to_filter_dict(self, context: "QueryContext") -> Dict[str, Any]:
|
|
515
|
+
"""Convert to MongoDB-style filter dict"""
|
|
516
|
+
operator_map = {
|
|
517
|
+
"==": "$eq",
|
|
518
|
+
"!=": "$ne",
|
|
519
|
+
">": "$gt",
|
|
520
|
+
"<": "$lt",
|
|
521
|
+
">=": "$gte",
|
|
522
|
+
"<=": "$lte",
|
|
523
|
+
"IN": "$in",
|
|
524
|
+
"CONTAINS": "$regex",
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
mongo_op = operator_map.get(self.operator, "$eq")
|
|
528
|
+
|
|
529
|
+
# For CONTAINS, convert to regex pattern
|
|
530
|
+
if self.operator == "CONTAINS":
|
|
531
|
+
return {self.property_path: {mongo_op: self.value}}
|
|
532
|
+
|
|
533
|
+
return {self.property_path: {mongo_op: self.value}}
|
|
534
|
+
|
|
535
|
+
def validate(self, schema: Any, entity_type: Optional[str] = None) -> List[ValidationError]:
|
|
536
|
+
"""
|
|
537
|
+
Validate property exists and type matches
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
schema: Schema object for validation
|
|
541
|
+
entity_type: Optional entity type context for property validation
|
|
542
|
+
"""
|
|
543
|
+
errors = []
|
|
544
|
+
|
|
545
|
+
# Validate operator is valid
|
|
546
|
+
valid_operators = ["==", "!=", ">", "<", ">=", "<=", "IN", "CONTAINS"]
|
|
547
|
+
if self.operator not in valid_operators:
|
|
548
|
+
errors.append(
|
|
549
|
+
ValidationError(
|
|
550
|
+
self.line,
|
|
551
|
+
self.column,
|
|
552
|
+
f"Invalid operator '{self.operator}'. Must be one of: {', '.join(valid_operators)}",
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Validate IN operator has list value
|
|
557
|
+
if self.operator == "IN" and not isinstance(self.value, list):
|
|
558
|
+
errors.append(
|
|
559
|
+
ValidationError(
|
|
560
|
+
self.line,
|
|
561
|
+
self.column,
|
|
562
|
+
f"IN operator requires a list value, got {type(self.value).__name__}",
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Validate CONTAINS operator has string value
|
|
567
|
+
if self.operator == "CONTAINS" and not isinstance(self.value, str):
|
|
568
|
+
errors.append(
|
|
569
|
+
ValidationError(
|
|
570
|
+
self.line,
|
|
571
|
+
self.column,
|
|
572
|
+
f"CONTAINS operator requires a string value, got {type(self.value).__name__}",
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Validate property exists in schema (if entity_type provided)
|
|
577
|
+
if entity_type:
|
|
578
|
+
errors.extend(self._validate_property_in_schema(schema, entity_type))
|
|
579
|
+
else:
|
|
580
|
+
logger.debug(
|
|
581
|
+
f"PropertyFilterNode.validate: "
|
|
582
|
+
f"FALLBACK - No entity_type provided, skipping property validation for '{self.property_path}'"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return errors
|
|
586
|
+
|
|
587
|
+
def _validate_property_in_schema(self, schema: Any, entity_type: str) -> List[ValidationError]:
|
|
588
|
+
"""
|
|
589
|
+
Validate that property exists in entity type schema
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
schema: Schema object for validation
|
|
593
|
+
entity_type: Entity type to validate against
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
List of validation errors
|
|
597
|
+
"""
|
|
598
|
+
errors = []
|
|
599
|
+
|
|
600
|
+
# Check if schema has required methods
|
|
601
|
+
if not hasattr(schema, "get_entity_type"):
|
|
602
|
+
logger.debug(
|
|
603
|
+
f"PropertyFilterNode._validate_property_in_schema: "
|
|
604
|
+
f"FALLBACK - schema missing 'get_entity_type' method, skipping property validation"
|
|
605
|
+
)
|
|
606
|
+
return errors
|
|
607
|
+
|
|
608
|
+
# Get entity type schema
|
|
609
|
+
entity_schema = schema.get_entity_type(entity_type)
|
|
610
|
+
if entity_schema is None:
|
|
611
|
+
# Entity type doesn't exist - error already reported by FindNode
|
|
612
|
+
logger.debug(
|
|
613
|
+
f"PropertyFilterNode._validate_property_in_schema: "
|
|
614
|
+
f"FALLBACK - entity type '{entity_type}' not found in schema, skipping property validation"
|
|
615
|
+
)
|
|
616
|
+
return errors
|
|
617
|
+
|
|
618
|
+
# Validate nested property path recursively
|
|
619
|
+
errors.extend(
|
|
620
|
+
self._validate_nested_property_path(
|
|
621
|
+
entity_schema=entity_schema,
|
|
622
|
+
property_path=self.property_path,
|
|
623
|
+
entity_type=entity_type,
|
|
624
|
+
current_path="",
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
return errors
|
|
629
|
+
|
|
630
|
+
def _validate_nested_property_path(
|
|
631
|
+
self,
|
|
632
|
+
entity_schema: Any,
|
|
633
|
+
property_path: str,
|
|
634
|
+
entity_type: str,
|
|
635
|
+
current_path: str = "",
|
|
636
|
+
) -> List[ValidationError]:
|
|
637
|
+
"""
|
|
638
|
+
Recursively validate a nested property path
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
entity_schema: Current entity schema (may be nested)
|
|
642
|
+
property_path: Remaining property path to validate
|
|
643
|
+
entity_type: Root entity type name
|
|
644
|
+
current_path: Accumulated path so far (for error messages)
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
List of validation errors
|
|
648
|
+
"""
|
|
649
|
+
errors = []
|
|
650
|
+
|
|
651
|
+
# Split property path into parts
|
|
652
|
+
property_parts = property_path.split(".")
|
|
653
|
+
current_property = property_parts[0]
|
|
654
|
+
remaining_path = ".".join(property_parts[1:]) if len(property_parts) > 1 else None
|
|
655
|
+
|
|
656
|
+
# Build full path for error messages
|
|
657
|
+
full_path = f"{current_path}.{current_property}" if current_path else current_property
|
|
658
|
+
|
|
659
|
+
# Check if entity schema has get_property method
|
|
660
|
+
if not hasattr(entity_schema, "get_property"):
|
|
661
|
+
logger.debug(
|
|
662
|
+
f"PropertyFilterNode._validate_nested_property_path: "
|
|
663
|
+
f"FALLBACK - entity_schema missing 'get_property' method for '{entity_type}', "
|
|
664
|
+
f"skipping nested property validation at '{full_path}'"
|
|
665
|
+
)
|
|
666
|
+
return errors
|
|
667
|
+
|
|
668
|
+
# Get property schema
|
|
669
|
+
property_schema = entity_schema.get_property(current_property)
|
|
670
|
+
if property_schema is None:
|
|
671
|
+
# Property doesn't exist - get available properties for suggestion
|
|
672
|
+
available_props = []
|
|
673
|
+
if hasattr(entity_schema, "properties"):
|
|
674
|
+
available_props = list(entity_schema.properties.keys())
|
|
675
|
+
logger.debug(
|
|
676
|
+
f"PropertyFilterNode._validate_nested_property_path: "
|
|
677
|
+
f"Using 'properties' attribute to get available properties for '{entity_type}'"
|
|
678
|
+
)
|
|
679
|
+
elif hasattr(entity_schema, "get_property_names"):
|
|
680
|
+
available_props = entity_schema.get_property_names()
|
|
681
|
+
logger.debug(
|
|
682
|
+
f"PropertyFilterNode._validate_nested_property_path: "
|
|
683
|
+
f"FALLBACK - Using 'get_property_names()' method instead of 'properties' attribute "
|
|
684
|
+
f"for '{entity_type}'"
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
suggestion = None
|
|
688
|
+
if available_props:
|
|
689
|
+
context = f"{entity_type}.{current_path}" if current_path else entity_type
|
|
690
|
+
suggestion = f"Available properties for {context}: {', '.join(available_props[:5])}"
|
|
691
|
+
if len(available_props) > 5:
|
|
692
|
+
suggestion += f" (and {len(available_props) - 5} more)"
|
|
693
|
+
|
|
694
|
+
errors.append(
|
|
695
|
+
ValidationError(
|
|
696
|
+
self.line,
|
|
697
|
+
self.column,
|
|
698
|
+
f"Property '{full_path}' not found in {entity_type if not current_path else current_path}",
|
|
699
|
+
suggestion=suggestion,
|
|
700
|
+
)
|
|
701
|
+
)
|
|
702
|
+
return errors # Can't continue validation if property doesn't exist
|
|
703
|
+
|
|
704
|
+
# Check if there's more nesting to validate
|
|
705
|
+
if remaining_path:
|
|
706
|
+
# Check if current property is DICT type (supports nesting)
|
|
707
|
+
if not hasattr(property_schema, "property_type"):
|
|
708
|
+
# Can't determine if nesting is supported
|
|
709
|
+
errors.append(
|
|
710
|
+
ValidationError(
|
|
711
|
+
self.line,
|
|
712
|
+
self.column,
|
|
713
|
+
f"Cannot validate nested path '{full_path}.{remaining_path}': "
|
|
714
|
+
f"property '{current_property}' type unknown",
|
|
715
|
+
suggestion="Ensure property schema defines property_type",
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
return errors
|
|
719
|
+
|
|
720
|
+
property_type = property_schema.property_type
|
|
721
|
+
|
|
722
|
+
# Check if property type supports nesting
|
|
723
|
+
if hasattr(property_type, "value"):
|
|
724
|
+
type_value = property_type.value
|
|
725
|
+
elif hasattr(property_type, "name"):
|
|
726
|
+
type_value = property_type.name
|
|
727
|
+
else:
|
|
728
|
+
type_value = str(property_type)
|
|
729
|
+
|
|
730
|
+
# Import PropertyType to check if it's DICT
|
|
731
|
+
from aiecs.domain.knowledge_graph.schema.property_schema import PropertyType
|
|
732
|
+
|
|
733
|
+
if type_value == PropertyType.DICT.value or type_value == "dict":
|
|
734
|
+
# Property is DICT type - check for nested schema
|
|
735
|
+
nested_schema = self._get_nested_schema(property_schema)
|
|
736
|
+
if nested_schema is None:
|
|
737
|
+
# No nested schema defined - can't validate deeper nesting
|
|
738
|
+
errors.append(
|
|
739
|
+
ValidationError(
|
|
740
|
+
self.line,
|
|
741
|
+
self.column,
|
|
742
|
+
f"Cannot validate nested path '{full_path}.{remaining_path}': "
|
|
743
|
+
f"property '{current_property}' is DICT type but nested schema not defined",
|
|
744
|
+
suggestion=f"Define nested schema for '{current_property}' or use flat property path",
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
return errors
|
|
748
|
+
|
|
749
|
+
# Recursively validate remaining path
|
|
750
|
+
errors.extend(
|
|
751
|
+
self._validate_nested_property_path(
|
|
752
|
+
entity_schema=nested_schema,
|
|
753
|
+
property_path=remaining_path,
|
|
754
|
+
entity_type=entity_type,
|
|
755
|
+
current_path=full_path,
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
else:
|
|
759
|
+
# Property is not DICT type - can't nest further
|
|
760
|
+
errors.append(
|
|
761
|
+
ValidationError(
|
|
762
|
+
self.line,
|
|
763
|
+
self.column,
|
|
764
|
+
f"Cannot access nested path '{full_path}.{remaining_path}': "
|
|
765
|
+
f"property '{current_property}' is {type_value} type, not DICT",
|
|
766
|
+
suggestion=f"Use '{full_path}' directly or change property type to DICT",
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
return errors
|
|
771
|
+
|
|
772
|
+
def _get_nested_schema(self, property_schema: Any) -> Optional[Any]:
|
|
773
|
+
"""
|
|
774
|
+
Get nested schema for a DICT property
|
|
775
|
+
|
|
776
|
+
Checks for nested schema in multiple ways:
|
|
777
|
+
1. property_schema.nested_schema attribute
|
|
778
|
+
2. property_schema.schema attribute (if not a callable method)
|
|
779
|
+
3. property_schema.properties attribute (treat as EntityType-like)
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
property_schema: Property schema to get nested schema from
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
Nested schema object or None if not found
|
|
786
|
+
"""
|
|
787
|
+
# Check for explicit nested_schema attribute
|
|
788
|
+
if hasattr(property_schema, "nested_schema"):
|
|
789
|
+
nested_schema = getattr(property_schema, "nested_schema", None)
|
|
790
|
+
if nested_schema is not None:
|
|
791
|
+
return nested_schema
|
|
792
|
+
|
|
793
|
+
# Check for schema attribute (but not if it's a callable method)
|
|
794
|
+
if hasattr(property_schema, "schema"):
|
|
795
|
+
schema_attr = getattr(property_schema, "schema", None)
|
|
796
|
+
# Only use if it's not callable (Pydantic models have schema() method)
|
|
797
|
+
if schema_attr is not None and not callable(schema_attr):
|
|
798
|
+
return schema_attr
|
|
799
|
+
|
|
800
|
+
# Check if property_schema has properties attribute (treat as EntityType-like)
|
|
801
|
+
if hasattr(property_schema, "properties"):
|
|
802
|
+
properties = getattr(property_schema, "properties", None)
|
|
803
|
+
# Only use if it's a dict-like structure (not a Pydantic method)
|
|
804
|
+
if properties and isinstance(properties, dict) and len(properties) > 0:
|
|
805
|
+
# Create a mock entity schema-like object
|
|
806
|
+
class NestedSchema:
|
|
807
|
+
def __init__(self, properties):
|
|
808
|
+
self.properties = properties
|
|
809
|
+
|
|
810
|
+
def get_property(self, property_name: str):
|
|
811
|
+
if isinstance(self.properties, dict):
|
|
812
|
+
return self.properties.get(property_name)
|
|
813
|
+
return None
|
|
814
|
+
|
|
815
|
+
def get_property_names(self):
|
|
816
|
+
if isinstance(self.properties, dict):
|
|
817
|
+
return list(self.properties.keys())
|
|
818
|
+
return []
|
|
819
|
+
|
|
820
|
+
return NestedSchema(properties)
|
|
821
|
+
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
def __repr__(self) -> str:
|
|
825
|
+
"""String representation for debugging"""
|
|
826
|
+
value_repr = f'"{self.value}"' if isinstance(self.value, str) else str(self.value)
|
|
827
|
+
return f"PropertyFilterNode({self.property_path} {self.operator} {value_repr})"
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@dataclass(frozen=True)
|
|
831
|
+
class BooleanFilterNode(FilterNode):
|
|
832
|
+
"""
|
|
833
|
+
Boolean filter node: AND/OR/NOT combinations
|
|
834
|
+
|
|
835
|
+
Represents boolean combinations of filters.
|
|
836
|
+
|
|
837
|
+
Attributes:
|
|
838
|
+
operator: Boolean operator ("AND", "OR", "NOT")
|
|
839
|
+
operands: List of filter nodes to combine
|
|
840
|
+
|
|
841
|
+
Example:
|
|
842
|
+
age > 30 AND status == "active"
|
|
843
|
+
|
|
844
|
+
BooleanFilterNode(
|
|
845
|
+
operator="AND",
|
|
846
|
+
operands=[
|
|
847
|
+
PropertyFilterNode(property_path="age", operator=">", value=30),
|
|
848
|
+
PropertyFilterNode(property_path="status", operator="==", value="active")
|
|
849
|
+
]
|
|
850
|
+
)
|
|
851
|
+
"""
|
|
852
|
+
|
|
853
|
+
operator: str # "AND", "OR", "NOT"
|
|
854
|
+
operands: List[FilterNode] = field(default_factory=list)
|
|
855
|
+
|
|
856
|
+
def to_filter_dict(self, context: "QueryContext") -> Dict[str, Any]:
|
|
857
|
+
"""Convert to MongoDB-style boolean filter"""
|
|
858
|
+
op_map = {"AND": "$and", "OR": "$or", "NOT": "$not"}
|
|
859
|
+
|
|
860
|
+
mongo_op = op_map.get(self.operator, "$and")
|
|
861
|
+
operand_dicts = [op.to_filter_dict(context) for op in self.operands]
|
|
862
|
+
|
|
863
|
+
# NOT operator has special handling (single operand)
|
|
864
|
+
if self.operator == "NOT":
|
|
865
|
+
if len(operand_dicts) == 1:
|
|
866
|
+
return {mongo_op: operand_dicts[0]}
|
|
867
|
+
else:
|
|
868
|
+
# Multiple operands: NOT (a AND b AND c) = NOT {$and: [a, b,
|
|
869
|
+
# c]}
|
|
870
|
+
return {mongo_op: {"$and": operand_dicts}}
|
|
871
|
+
|
|
872
|
+
return {mongo_op: operand_dicts}
|
|
873
|
+
|
|
874
|
+
def validate(self, schema: Any, entity_type: Optional[str] = None) -> List[ValidationError]:
|
|
875
|
+
"""
|
|
876
|
+
Validate all operands
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
schema: Schema object for validation
|
|
880
|
+
entity_type: Optional entity type context for property validation
|
|
881
|
+
"""
|
|
882
|
+
errors = []
|
|
883
|
+
|
|
884
|
+
# Validate operator is valid
|
|
885
|
+
valid_operators = ["AND", "OR", "NOT"]
|
|
886
|
+
if self.operator not in valid_operators:
|
|
887
|
+
errors.append(
|
|
888
|
+
ValidationError(
|
|
889
|
+
self.line,
|
|
890
|
+
self.column,
|
|
891
|
+
f"Invalid boolean operator '{self.operator}'. Must be one of: {', '.join(valid_operators)}",
|
|
892
|
+
)
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Validate operand count
|
|
896
|
+
if not self.operands:
|
|
897
|
+
errors.append(
|
|
898
|
+
ValidationError(
|
|
899
|
+
self.line,
|
|
900
|
+
self.column,
|
|
901
|
+
f"Boolean operator '{self.operator}' requires at least one operand",
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# Validate all operands (pass entity_type for property validation)
|
|
906
|
+
for operand in self.operands:
|
|
907
|
+
errors.extend(operand.validate(schema, entity_type=entity_type))
|
|
908
|
+
|
|
909
|
+
return errors
|
|
910
|
+
|
|
911
|
+
def __repr__(self) -> str:
|
|
912
|
+
"""String representation for debugging"""
|
|
913
|
+
return f"BooleanFilterNode({self.operator}, operands={len(self.operands)})"
|