igel-qe-core 1.0.0
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.
- package/dist/cli/index.js +38 -0
- package/dist/mcp/server.js +104 -0
- package/knowledge_base/__init__.py +3 -0
- package/knowledge_base/__pycache__/__init__.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/__pycache__/confidence.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/config.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/config.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/config.cpython-38.pyc +0 -0
- package/knowledge_base/__pycache__/context_fusion.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/context_fusion.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/feedback_analytics.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/feedback_applier.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/feedback_cli.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/feedback_curator.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/feedback_handler.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/generate.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/generate_qcapps134.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/input_classifier.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/input_classifier.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/kg_extractor.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/knowledge_graph.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/manual_tc_parser.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/manual_tc_parser.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/precedence.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/prompts.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/prompts.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/reranker.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/reranker.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/retrieval_debug.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/retrieval_debug.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/retriever.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/retriever.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/server.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/tc_transformer.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/tc_transformer.cpython-37.pyc +0 -0
- package/knowledge_base/__pycache__/test_generator.cpython-314.pyc +0 -0
- package/knowledge_base/__pycache__/test_generator.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__init__.py +6 -0
- package/knowledge_base/agents/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/fusion_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/fusion_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/graph.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/graph.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/healing_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/intent_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/intent_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/kg_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/kg_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/memory_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/memory_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/rca_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/reflection_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/reflection_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/regression_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/retrieval_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/retrieval_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/script_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/script_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/state.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/state.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/temporal_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/temporal_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/__pycache__/validation_agent.cpython-37.pyc +0 -0
- package/knowledge_base/agents/__pycache__/validation_agent.cpython-38.pyc +0 -0
- package/knowledge_base/agents/automation_design_agent.py +123 -0
- package/knowledge_base/agents/code_intelligence_agent.py +147 -0
- package/knowledge_base/agents/confidence_agent.py +94 -0
- package/knowledge_base/agents/execution_validation_agent.py +129 -0
- package/knowledge_base/agents/feedback_agent.py +56 -0
- package/knowledge_base/agents/fusion_agent.py +113 -0
- package/knowledge_base/agents/graph.py +152 -0
- package/knowledge_base/agents/healing_agent.py +62 -0
- package/knowledge_base/agents/intent_agent.py +76 -0
- package/knowledge_base/agents/kg_agent.py +91 -0
- package/knowledge_base/agents/memory_agent.py +62 -0
- package/knowledge_base/agents/rca_agent.py +53 -0
- package/knowledge_base/agents/reflection_agent.py +62 -0
- package/knowledge_base/agents/regression_agent.py +81 -0
- package/knowledge_base/agents/requirement_agent.py +71 -0
- package/knowledge_base/agents/retrieval_agent.py +66 -0
- package/knowledge_base/agents/scenario_agent.py +81 -0
- package/knowledge_base/agents/script_agent.py +121 -0
- package/knowledge_base/agents/script_generator_agent.py +77 -0
- package/knowledge_base/agents/state.py +53 -0
- package/knowledge_base/agents/temporal_agent.py +48 -0
- package/knowledge_base/agents/testcase_agent.py +111 -0
- package/knowledge_base/agents/validation_agent.py +49 -0
- package/knowledge_base/agents/workflow_engine.py +87 -0
- package/knowledge_base/alembic/README.md +104 -0
- package/knowledge_base/alembic/__pycache__/env.cpython-37.pyc +0 -0
- package/knowledge_base/alembic/__pycache__/env.cpython-38.pyc +0 -0
- package/knowledge_base/alembic/env.py +76 -0
- package/knowledge_base/alembic/script.py.mako +5 -0
- package/knowledge_base/alembic/versions/.gitkeep +26 -0
- package/knowledge_base/alembic/versions/001_phase0_feedback.py +122 -0
- package/knowledge_base/alembic/versions/002_feedback_promotions.py +60 -0
- package/knowledge_base/alembic/versions/003_enterprise_hardening.py +74 -0
- package/knowledge_base/alembic/versions/__pycache__/001_phase0_feedback.cpython-37.pyc +0 -0
- package/knowledge_base/alembic/versions/__pycache__/001_phase0_feedback.cpython-38.pyc +0 -0
- package/knowledge_base/alembic/versions/__pycache__/002_feedback_promotions.cpython-37.pyc +0 -0
- package/knowledge_base/alembic/versions/__pycache__/002_feedback_promotions.cpython-38.pyc +0 -0
- package/knowledge_base/api/__init__.py +1 -0
- package/knowledge_base/api/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/api/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/api/__pycache__/middleware.cpython-38.pyc +0 -0
- package/knowledge_base/api/__pycache__/server.cpython-37.pyc +0 -0
- package/knowledge_base/api/__pycache__/server.cpython-38.pyc +0 -0
- package/knowledge_base/api/middleware.py +78 -0
- package/knowledge_base/api/server.py +778 -0
- package/knowledge_base/chunking/__init__.py +0 -0
- package/knowledge_base/chunking/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/chunking/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/chunking/__pycache__/chunker.cpython-37.pyc +0 -0
- package/knowledge_base/chunking/__pycache__/chunker.cpython-38.pyc +0 -0
- package/knowledge_base/chunking/__pycache__/validators.cpython-37.pyc +0 -0
- package/knowledge_base/chunking/__pycache__/validators.cpython-38.pyc +0 -0
- package/knowledge_base/chunking/chunker.py +402 -0
- package/knowledge_base/chunking/validators.py +144 -0
- package/knowledge_base/cli/__init__.py +1 -0
- package/knowledge_base/cli/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/cli/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/cli/__pycache__/feedback_cli.cpython-37.pyc +0 -0
- package/knowledge_base/cli/__pycache__/feedback_cli.cpython-38.pyc +0 -0
- package/knowledge_base/cli/__pycache__/generate.cpython-37.pyc +0 -0
- package/knowledge_base/cli/__pycache__/generate.cpython-38.pyc +0 -0
- package/knowledge_base/cli/__pycache__/generate_qcapps134.cpython-37.pyc +0 -0
- package/knowledge_base/cli/feedback_cli.py +121 -0
- package/knowledge_base/cli/generate.py +308 -0
- package/knowledge_base/cli/generate_qcapps134.py +391 -0
- package/knowledge_base/config/.env +100 -0
- package/knowledge_base/config/.env.example +75 -0
- package/knowledge_base/config/__init__.py +5 -0
- package/knowledge_base/config/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/config/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/config/__pycache__/settings.cpython-37.pyc +0 -0
- package/knowledge_base/config/__pycache__/settings.cpython-38.pyc +0 -0
- package/knowledge_base/config/settings.py +280 -0
- package/knowledge_base/connectors/__init__.py +5 -0
- package/knowledge_base/connectors/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/base.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/base.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/bitbucket_connector.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/bitbucket_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/community_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/confluence_connector.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/confluence_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/jira_connector.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/jira_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/kb_igel_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/playwright_fetcher.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/query_bridge.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/search_backends.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/serviceNow_connector.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/serviceNow_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/sync_scheduler.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/sync_scheduler.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/teamcity_connector.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/teamcity_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/testRail_connector.cpython-37.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/testRail_connector.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/__pycache__/web_fetcher.cpython-38.pyc +0 -0
- package/knowledge_base/connectors/base.py +44 -0
- package/knowledge_base/connectors/bitbucket_connector.py +25 -0
- package/knowledge_base/connectors/community_connector.py +112 -0
- package/knowledge_base/connectors/confluence_connector.py +165 -0
- package/knowledge_base/connectors/jira_connector.py +174 -0
- package/knowledge_base/connectors/kb_igel_connector.py +145 -0
- package/knowledge_base/connectors/playwright_fetcher.py +50 -0
- package/knowledge_base/connectors/query_bridge.py +170 -0
- package/knowledge_base/connectors/search_backends.py +368 -0
- package/knowledge_base/connectors/serviceNow_connector.py +25 -0
- package/knowledge_base/connectors/sync_scheduler.py +111 -0
- package/knowledge_base/connectors/teamcity_connector.py +25 -0
- package/knowledge_base/connectors/testRail_connector.py +25 -0
- package/knowledge_base/connectors/web_fetcher.py +280 -0
- package/knowledge_base/core/__init__.py +8 -0
- package/knowledge_base/core/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/core/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/core/__pycache__/cache.cpython-38.pyc +0 -0
- package/knowledge_base/core/__pycache__/circuit_breaker.cpython-38.pyc +0 -0
- package/knowledge_base/core/__pycache__/confidence.cpython-37.pyc +0 -0
- package/knowledge_base/core/__pycache__/confidence.cpython-38.pyc +0 -0
- package/knowledge_base/core/__pycache__/logging_setup.cpython-38.pyc +0 -0
- package/knowledge_base/core/__pycache__/metrics.cpython-38.pyc +0 -0
- package/knowledge_base/core/__pycache__/precedence.cpython-37.pyc +0 -0
- package/knowledge_base/core/__pycache__/precedence.cpython-38.pyc +0 -0
- package/knowledge_base/core/__pycache__/request_context.cpython-38.pyc +0 -0
- package/knowledge_base/core/cache.py +98 -0
- package/knowledge_base/core/circuit_breaker.py +33 -0
- package/knowledge_base/core/confidence.py +254 -0
- package/knowledge_base/core/logging_setup.py +39 -0
- package/knowledge_base/core/metrics.py +63 -0
- package/knowledge_base/core/precedence.py +354 -0
- package/knowledge_base/core/request_context.py +54 -0
- package/knowledge_base/db/__init__.py +0 -0
- package/knowledge_base/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/knowledge_base/db/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/db/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/db/__pycache__/client.cpython-314.pyc +0 -0
- package/knowledge_base/db/__pycache__/client.cpython-37.pyc +0 -0
- package/knowledge_base/db/__pycache__/client.cpython-38.pyc +0 -0
- package/knowledge_base/db/__pycache__/connector_sync.cpython-38.pyc +0 -0
- package/knowledge_base/db/__pycache__/feedback_schema.cpython-37.pyc +0 -0
- package/knowledge_base/db/__pycache__/feedback_schema.cpython-38.pyc +0 -0
- package/knowledge_base/db/__pycache__/kg_introspect.cpython-314.pyc +0 -0
- package/knowledge_base/db/__pycache__/kg_introspect.cpython-37.pyc +0 -0
- package/knowledge_base/db/__pycache__/kg_introspect.cpython-38.pyc +0 -0
- package/knowledge_base/db/__pycache__/schema.cpython-37.pyc +0 -0
- package/knowledge_base/db/__pycache__/schema.cpython-38.pyc +0 -0
- package/knowledge_base/db/client.py +100 -0
- package/knowledge_base/db/connector_sync.py +43 -0
- package/knowledge_base/db/feedback_schema.py +548 -0
- package/knowledge_base/db/igel_schema.py +150 -0
- package/knowledge_base/db/kg_introspect.py +116 -0
- package/knowledge_base/db/migrations.py +22 -0
- package/knowledge_base/db/schema.py +649 -0
- package/knowledge_base/deploy/Dockerfile +23 -0
- package/knowledge_base/deploy/feedback_automation.sh +18 -0
- package/knowledge_base/deploy/firecrawl-docker.sh +50 -0
- package/knowledge_base/deploy/igel-testgen.service +26 -0
- package/knowledge_base/deploy/install_reranker.sh +92 -0
- package/knowledge_base/deploy/requirements.txt +68 -0
- package/knowledge_base/deploy/run_server.sh +35 -0
- package/knowledge_base/docs/TEST_GENERATION_GUIDE.md +174 -0
- package/knowledge_base/embeddings/__init__.py +0 -0
- package/knowledge_base/embeddings/__pycache__/__init__.cpython-314.pyc +0 -0
- package/knowledge_base/embeddings/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/embeddings/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/embeddings/__pycache__/azure_embedder.cpython-314.pyc +0 -0
- package/knowledge_base/embeddings/__pycache__/azure_embedder.cpython-37.pyc +0 -0
- package/knowledge_base/embeddings/__pycache__/azure_embedder.cpython-38.pyc +0 -0
- package/knowledge_base/embeddings/azure_embedder.py +99 -0
- package/knowledge_base/eval/__init__.py +33 -0
- package/knowledge_base/eval/__main__.py +172 -0
- package/knowledge_base/eval/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/eval/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/eval/__pycache__/__main__.cpython-37.pyc +0 -0
- package/knowledge_base/eval/__pycache__/baseline.cpython-37.pyc +0 -0
- package/knowledge_base/eval/__pycache__/baseline.cpython-38.pyc +0 -0
- package/knowledge_base/eval/__pycache__/harness.cpython-37.pyc +0 -0
- package/knowledge_base/eval/__pycache__/harness.cpython-38.pyc +0 -0
- package/knowledge_base/eval/__pycache__/scorers.cpython-37.pyc +0 -0
- package/knowledge_base/eval/__pycache__/scorers.cpython-38.pyc +0 -0
- package/knowledge_base/eval/baseline.py +213 -0
- package/knowledge_base/eval/harness.py +400 -0
- package/knowledge_base/eval/queries_50.json +302 -0
- package/knowledge_base/eval/scorers.py +494 -0
- package/knowledge_base/exemplars/README.md +7 -0
- package/knowledge_base/exemplars/__pycache__/pytest_reference_snippet.cpython-37.pyc +0 -0
- package/knowledge_base/exemplars/gold_manual_concise_snippet.md +8 -0
- package/knowledge_base/exemplars/gold_manual_detailed_snippet.md +7 -0
- package/knowledge_base/exemplars/pytest_reference_snippet.py +37 -0
- package/knowledge_base/feedback/__init__.py +10 -0
- package/knowledge_base/feedback/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/analytics.cpython-37.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/analytics.cpython-38.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/applier.cpython-37.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/applier.cpython-38.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/curator.cpython-37.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/curator.cpython-38.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/handler.cpython-37.pyc +0 -0
- package/knowledge_base/feedback/__pycache__/handler.cpython-38.pyc +0 -0
- package/knowledge_base/feedback/analytics.py +72 -0
- package/knowledge_base/feedback/applier.py +93 -0
- package/knowledge_base/feedback/curator.py +49 -0
- package/knowledge_base/feedback/handler.py +151 -0
- package/knowledge_base/generation/__init__.py +5 -0
- package/knowledge_base/generation/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/generation/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/generation/__pycache__/input_classifier.cpython-37.pyc +0 -0
- package/knowledge_base/generation/__pycache__/input_classifier.cpython-38.pyc +0 -0
- package/knowledge_base/generation/__pycache__/manual_tc_parser.cpython-37.pyc +0 -0
- package/knowledge_base/generation/__pycache__/manual_tc_parser.cpython-38.pyc +0 -0
- package/knowledge_base/generation/__pycache__/prompts.cpython-37.pyc +0 -0
- package/knowledge_base/generation/__pycache__/prompts.cpython-38.pyc +0 -0
- package/knowledge_base/generation/__pycache__/tc_transformer.cpython-37.pyc +0 -0
- package/knowledge_base/generation/__pycache__/test_generator.cpython-37.pyc +0 -0
- package/knowledge_base/generation/__pycache__/test_generator.cpython-38.pyc +0 -0
- package/knowledge_base/generation/input_classifier.py +148 -0
- package/knowledge_base/generation/manual_tc_parser.py +180 -0
- package/knowledge_base/generation/prompts.py +755 -0
- package/knowledge_base/generation/tc_transformer.py +368 -0
- package/knowledge_base/generation/test_generator.py +553 -0
- package/knowledge_base/graph_db/__init__.py +5 -0
- package/knowledge_base/graph_db/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/graphiti_client.cpython-37.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/kg_traversal.cpython-37.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/migrate_kg.cpython-37.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/neo4j_client.cpython-37.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/neo4j_client.cpython-38.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/schema.cpython-37.pyc +0 -0
- package/knowledge_base/graph_db/__pycache__/temporal_ingest.cpython-37.pyc +0 -0
- package/knowledge_base/graph_db/graphiti_client.py +127 -0
- package/knowledge_base/graph_db/kg_traversal.py +192 -0
- package/knowledge_base/graph_db/migrate_kg.py +93 -0
- package/knowledge_base/graph_db/neo4j_client.py +116 -0
- package/knowledge_base/graph_db/schema.py +22 -0
- package/knowledge_base/graph_db/temporal_ingest.py +59 -0
- package/knowledge_base/ingestion/__init__.py +1 -0
- package/knowledge_base/ingestion/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/ingest.cpython-37.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/ingest.cpython-38.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/ingest_images.cpython-37.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/ingest_images.cpython-38.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/kg_extractor.cpython-37.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/kg_extractor.cpython-38.pyc +0 -0
- package/knowledge_base/ingestion/__pycache__/web_ingest.cpython-38.pyc +0 -0
- package/knowledge_base/ingestion/ingest.py +346 -0
- package/knowledge_base/ingestion/ingest_images.py +330 -0
- package/knowledge_base/ingestion/kg_extractor.py +358 -0
- package/knowledge_base/ingestion/web_ingest.py +83 -0
- package/knowledge_base/intelligence/__init__.py +1 -0
- package/knowledge_base/intelligence/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/intelligence/__pycache__/execution_ingest.cpython-37.pyc +0 -0
- package/knowledge_base/intelligence/__pycache__/predictive_engine.cpython-37.pyc +0 -0
- package/knowledge_base/intelligence/__pycache__/risk_scorer.cpython-37.pyc +0 -0
- package/knowledge_base/intelligence/execution_ingest.py +34 -0
- package/knowledge_base/intelligence/predictive_engine.py +7 -0
- package/knowledge_base/intelligence/risk_scorer.py +8 -0
- package/knowledge_base/memory/__init__.py +6 -0
- package/knowledge_base/memory/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/memory/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/memory/__pycache__/cognee_client.cpython-37.pyc +0 -0
- package/knowledge_base/memory/__pycache__/cognee_client.cpython-38.pyc +0 -0
- package/knowledge_base/memory/__pycache__/ingest_memory.cpython-37.pyc +0 -0
- package/knowledge_base/memory/__pycache__/memory_types.cpython-37.pyc +0 -0
- package/knowledge_base/memory/__pycache__/memory_types.cpython-38.pyc +0 -0
- package/knowledge_base/memory/cognee_client.py +164 -0
- package/knowledge_base/memory/ingest_memory.py +60 -0
- package/knowledge_base/memory/memory_types.py +23 -0
- package/knowledge_base/models/test_requirement.py +84 -0
- package/knowledge_base/parsers/__init__.py +0 -0
- package/knowledge_base/parsers/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/csv_parser.cpython-37.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/csv_parser.cpython-38.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/doc_parser.cpython-37.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/doc_parser.cpython-38.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/html_parser.cpython-38.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/image_extractor.cpython-37.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/image_extractor.cpython-38.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/markdown_parser.cpython-37.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/markdown_parser.cpython-38.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/pdf_parser.cpython-37.pyc +0 -0
- package/knowledge_base/parsers/__pycache__/pdf_parser.cpython-38.pyc +0 -0
- package/knowledge_base/parsers/csv_parser.py +126 -0
- package/knowledge_base/parsers/doc_parser.py +176 -0
- package/knowledge_base/parsers/html_parser.py +151 -0
- package/knowledge_base/parsers/image_extractor.py +370 -0
- package/knowledge_base/parsers/markdown_parser.py +116 -0
- package/knowledge_base/parsers/pdf_parser.py +178 -0
- package/knowledge_base/retrieval/__init__.py +1 -0
- package/knowledge_base/retrieval/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/context_fusion.cpython-37.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/context_fusion.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/knowledge_graph.cpython-37.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/knowledge_graph.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/reranker.cpython-37.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/reranker.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/retrieval_debug.cpython-37.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/retrieval_debug.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/retriever.cpython-37.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/retriever.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/unified_query.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/__pycache__/web_retriever.cpython-38.pyc +0 -0
- package/knowledge_base/retrieval/context_fusion.py +264 -0
- package/knowledge_base/retrieval/knowledge_graph.py +267 -0
- package/knowledge_base/retrieval/reranker.py +308 -0
- package/knowledge_base/retrieval/retrieval_debug.py +69 -0
- package/knowledge_base/retrieval/retriever.py +885 -0
- package/knowledge_base/retrieval/unified_query.py +189 -0
- package/knowledge_base/retrieval/web_retriever.py +337 -0
- package/knowledge_base/validators/__init__.py +1 -0
- package/knowledge_base/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/knowledge_base/validators/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/validators/__pycache__/step_fidelity.cpython-314.pyc +0 -0
- package/knowledge_base/validators/__pycache__/step_fidelity.cpython-37.pyc +0 -0
- package/knowledge_base/validators/step_fidelity.py +88 -0
- package/knowledge_base/vision/__init__.py +0 -0
- package/knowledge_base/vision/__pycache__/__init__.cpython-37.pyc +0 -0
- package/knowledge_base/vision/__pycache__/__init__.cpython-38.pyc +0 -0
- package/knowledge_base/vision/__pycache__/image_describer.cpython-37.pyc +0 -0
- package/knowledge_base/vision/__pycache__/image_describer.cpython-38.pyc +0 -0
- package/knowledge_base/vision/image_describer.py +202 -0
- package/package.json +31 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IGEL Knowledge Base — Document Ingestion Pipeline
|
|
3
|
+
==================================================
|
|
4
|
+
Usage:
|
|
5
|
+
python -m knowledge_base.ingestion.ingest # Ingest all new documents
|
|
6
|
+
python -m knowledge_base.ingestion.ingest --file x.pdf # Single file
|
|
7
|
+
python -m knowledge_base.ingestion.ingest --reset # Drop + rebuild schema, then ingest all
|
|
8
|
+
python -m knowledge_base.ingestion.ingest --dry-run # Parse + chunk without writing to DB
|
|
9
|
+
python -m knowledge_base.ingestion.ingest --stats # Show DB statistics and exit
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import argparse
|
|
13
|
+
import hashlib
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
|
|
23
|
+
from knowledge_base.config import cfg
|
|
24
|
+
from knowledge_base.db.client import init_pool, test_connection, close_pool
|
|
25
|
+
from knowledge_base.db.schema import (
|
|
26
|
+
create_schema, drop_schema, document_exists,
|
|
27
|
+
get_stats, ingest_document_atomic,
|
|
28
|
+
)
|
|
29
|
+
from knowledge_base.parsers.pdf_parser import parse_pdf
|
|
30
|
+
from knowledge_base.parsers.markdown_parser import parse_markdown_file
|
|
31
|
+
from knowledge_base.parsers.doc_parser import parse_doc
|
|
32
|
+
from knowledge_base.parsers.csv_parser import parse_csv
|
|
33
|
+
from knowledge_base.chunking.chunker import chunk_sections
|
|
34
|
+
from knowledge_base.chunking.validators import detect_product
|
|
35
|
+
from knowledge_base.embeddings.azure_embedder import embed_texts, format_for_pgvector
|
|
36
|
+
from knowledge_base.ingestion.kg_extractor import extract_for_document
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
logging.basicConfig(level=getattr(logging, cfg.LOG_LEVEL), format="%(levelname)s %(message)s")
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── File discovery ────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def discover_files(kb_dir: Path, single_file: str | None = None) -> list[Path]:
|
|
46
|
+
if single_file:
|
|
47
|
+
p = kb_dir / single_file
|
|
48
|
+
if not p.exists():
|
|
49
|
+
console.print(f"[red]File not found: {p}[/red]")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
return [p]
|
|
52
|
+
|
|
53
|
+
files = []
|
|
54
|
+
for f in sorted(kb_dir.iterdir()):
|
|
55
|
+
if not f.is_file():
|
|
56
|
+
continue
|
|
57
|
+
if f.name.startswith("~$") or f.name.startswith("."):
|
|
58
|
+
continue
|
|
59
|
+
if f.suffix.lower() in cfg.SUPPORTED_EXTENSIONS:
|
|
60
|
+
files.append(f)
|
|
61
|
+
return files
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def file_hash(path: Path) -> str:
|
|
65
|
+
h = hashlib.md5()
|
|
66
|
+
with open(path, "rb") as f:
|
|
67
|
+
for block in iter(lambda: f.read(65536), b""):
|
|
68
|
+
h.update(block)
|
|
69
|
+
return h.hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── Parsing dispatcher ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def parse_file(path: Path) -> tuple[list, str]:
|
|
75
|
+
"""Returns (sections, parse_method_label)."""
|
|
76
|
+
suffix = path.suffix.lower()
|
|
77
|
+
|
|
78
|
+
if suffix == ".pdf":
|
|
79
|
+
return parse_pdf(path, cfg.MARKDOWN_OUTPUT_DIR)
|
|
80
|
+
|
|
81
|
+
if suffix == ".md":
|
|
82
|
+
return parse_markdown_file(path), "markdown"
|
|
83
|
+
|
|
84
|
+
if suffix in (".doc", ".docx"):
|
|
85
|
+
return parse_doc(path), "docx"
|
|
86
|
+
|
|
87
|
+
if suffix in (".csv", ".xlsx", ".xls"):
|
|
88
|
+
return parse_csv(path), "csv"
|
|
89
|
+
|
|
90
|
+
if suffix == ".mht":
|
|
91
|
+
return _parse_mht(path), "mht"
|
|
92
|
+
|
|
93
|
+
raise ValueError(f"No parser for: {suffix}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parse_mht(path: Path) -> list:
|
|
97
|
+
"""MHT = MIME HTML archive — extract readable text."""
|
|
98
|
+
from knowledge_base.parsers.markdown_parser import Section
|
|
99
|
+
import re
|
|
100
|
+
|
|
101
|
+
raw = path.read_bytes().decode("utf-8", errors="replace")
|
|
102
|
+
# Strip MIME headers and HTML tags
|
|
103
|
+
text = re.sub(r"<[^>]+>", " ", raw)
|
|
104
|
+
text = re.sub(r"&[a-z]+;", " ", text)
|
|
105
|
+
text = re.sub(r"\s{3,}", "\n\n", text).strip()
|
|
106
|
+
return [Section(title=path.stem, level=0, content=text)]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── Core ingestion ────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def ingest_file(path: Path, dry_run: bool = False, skip_kg: bool = False) -> dict:
|
|
112
|
+
result = {
|
|
113
|
+
"file": path.name,
|
|
114
|
+
"status": "ok",
|
|
115
|
+
"parse_method": "?",
|
|
116
|
+
"parents": 0,
|
|
117
|
+
"chunks": 0,
|
|
118
|
+
"kg_nodes": 0,
|
|
119
|
+
"kg_edges": 0,
|
|
120
|
+
"warnings": 0,
|
|
121
|
+
"validation_skipped": 0,
|
|
122
|
+
"skipped": False,
|
|
123
|
+
"error": None,
|
|
124
|
+
"elapsed": 0.0,
|
|
125
|
+
}
|
|
126
|
+
t0 = time.time()
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if path.name.startswith("~$") or path.name.startswith("."):
|
|
130
|
+
result["status"] = "skipped"
|
|
131
|
+
result["skipped"] = True
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
# Deduplication check — re-ingest if prior run left an empty document
|
|
135
|
+
fhash = file_hash(path)
|
|
136
|
+
existing_id: str | None = None
|
|
137
|
+
if not dry_run:
|
|
138
|
+
existing_id = document_exists(fhash)
|
|
139
|
+
if existing_id:
|
|
140
|
+
from knowledge_base.db.schema import clear_document_chunks, document_has_chunks
|
|
141
|
+
if document_has_chunks(existing_id):
|
|
142
|
+
result["status"] = "skipped"
|
|
143
|
+
result["skipped"] = True
|
|
144
|
+
return result
|
|
145
|
+
logger.warning("Re-ingesting %s (previous ingest had 0 chunks)", path.name)
|
|
146
|
+
clear_document_chunks(existing_id)
|
|
147
|
+
|
|
148
|
+
# Parse
|
|
149
|
+
sections, parse_method = parse_file(path)
|
|
150
|
+
result["parse_method"] = parse_method
|
|
151
|
+
|
|
152
|
+
if not sections:
|
|
153
|
+
result["status"] = "empty"
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
# Chunk
|
|
157
|
+
product = detect_product("", path.name)
|
|
158
|
+
parents, warnings = chunk_sections(sections, path.name, product)
|
|
159
|
+
result["parents"] = len(parents)
|
|
160
|
+
result["warnings"] = len(warnings)
|
|
161
|
+
|
|
162
|
+
if warnings:
|
|
163
|
+
for w in warnings[:5]: # log first 5 to avoid noise
|
|
164
|
+
logger.warning(w)
|
|
165
|
+
|
|
166
|
+
if dry_run:
|
|
167
|
+
total_children = sum(len(p.children) for p in parents)
|
|
168
|
+
result["chunks"] = total_children
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
doc_id, parent_count, total_children, validation_skipped = ingest_document_atomic(
|
|
172
|
+
file_name=path.name,
|
|
173
|
+
file_path=str(path),
|
|
174
|
+
file_type=path.suffix.lower().lstrip("."),
|
|
175
|
+
file_hash=fhash,
|
|
176
|
+
product=product,
|
|
177
|
+
doc_metadata={"parse_method": parse_method, "section_count": len(sections)},
|
|
178
|
+
parents=parents,
|
|
179
|
+
existing_id=existing_id,
|
|
180
|
+
embed_fn=embed_texts,
|
|
181
|
+
format_fn=format_for_pgvector,
|
|
182
|
+
batch_size=cfg.BATCH_SIZE,
|
|
183
|
+
)
|
|
184
|
+
result["validation_skipped"] = validation_skipped
|
|
185
|
+
result["chunks"] = total_children
|
|
186
|
+
|
|
187
|
+
if not skip_kg:
|
|
188
|
+
try:
|
|
189
|
+
kg_n, kg_e = extract_for_document(doc_id)
|
|
190
|
+
result["kg_nodes"] = kg_n
|
|
191
|
+
result["kg_edges"] = kg_e
|
|
192
|
+
except Exception as kg_err:
|
|
193
|
+
logger.warning("KG extraction failed for %s: %s", path.name, kg_err)
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
result["status"] = "error"
|
|
197
|
+
result["error"] = str(e)
|
|
198
|
+
logger.exception("Failed to ingest %s", path.name)
|
|
199
|
+
|
|
200
|
+
result["elapsed"] = round(time.time() - t0, 1)
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def main():
|
|
207
|
+
parser = argparse.ArgumentParser(description="IGEL Knowledge Base Ingestion Pipeline")
|
|
208
|
+
parser.add_argument("--file", type=str, help="Ingest a single file by name")
|
|
209
|
+
parser.add_argument("--reset", action="store_true", help="Drop and recreate schema before ingesting")
|
|
210
|
+
parser.add_argument("--dry-run", action="store_true", help="Parse + chunk without writing to DB")
|
|
211
|
+
parser.add_argument("--skip-kg", action="store_true", help="Skip knowledge graph extraction after ingestion")
|
|
212
|
+
parser.add_argument("--enrich-orphans", action="store_true", help="Run LLM KG enrichment on orphan parent chunks")
|
|
213
|
+
parser.add_argument("--stats", action="store_true", help="Print knowledge base statistics and exit")
|
|
214
|
+
args = parser.parse_args()
|
|
215
|
+
|
|
216
|
+
console.rule("[bold blue]IGEL Knowledge Base Ingestion Pipeline[/bold blue]")
|
|
217
|
+
console.print(f" KB directory : {cfg.KB_DIR}")
|
|
218
|
+
console.print(f" Database : {cfg.DB_HOST}:{cfg.DB_PORT}/{cfg.DB_NAME}")
|
|
219
|
+
console.print(f" Embeddings : {cfg.AZURE_EMBEDDING_DEPLOYMENT} ({cfg.EMBEDDING_DIM}d)")
|
|
220
|
+
console.print(f" Dry run : {args.dry_run}")
|
|
221
|
+
console.print(f" KG sync : {'disabled' if args.skip_kg else 'automatic'}")
|
|
222
|
+
console.print()
|
|
223
|
+
|
|
224
|
+
if not args.dry_run:
|
|
225
|
+
console.print("[bold]Connecting to PostgreSQL...[/bold]")
|
|
226
|
+
init_pool()
|
|
227
|
+
if not test_connection():
|
|
228
|
+
console.print("[red]Cannot connect to database. Check .env settings.[/red]")
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
|
|
231
|
+
if args.enrich_orphans and not args.dry_run:
|
|
232
|
+
from knowledge_base.ingestion.kg_extractor import enrich_orphan_parents
|
|
233
|
+
console.print("[bold]Enriching orphan parent chunks with LLM KG extraction...[/bold]")
|
|
234
|
+
init_pool()
|
|
235
|
+
n, e = enrich_orphan_parents(limit=500)
|
|
236
|
+
console.print(f" Enriched: {n} nodes, {e} edges")
|
|
237
|
+
close_pool()
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
if args.stats:
|
|
241
|
+
stats = get_stats()
|
|
242
|
+
t = Table(title="Knowledge Base Statistics")
|
|
243
|
+
t.add_column("Metric")
|
|
244
|
+
t.add_column("Value", justify="right")
|
|
245
|
+
for k, v in stats.items():
|
|
246
|
+
t.add_row(k.replace("_", " ").title(), str(v))
|
|
247
|
+
console.print(t)
|
|
248
|
+
close_pool()
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
if args.reset and not args.dry_run:
|
|
252
|
+
console.print("[yellow]Resetting schema (drop + recreate)...[/yellow]")
|
|
253
|
+
drop_schema()
|
|
254
|
+
|
|
255
|
+
if not args.dry_run:
|
|
256
|
+
console.print("[bold]Initializing schema...[/bold]")
|
|
257
|
+
create_schema()
|
|
258
|
+
|
|
259
|
+
files = discover_files(cfg.KB_DIR, args.file)
|
|
260
|
+
console.print(f"\nFound [bold]{len(files)}[/bold] files to process\n")
|
|
261
|
+
|
|
262
|
+
results = []
|
|
263
|
+
|
|
264
|
+
with Progress(
|
|
265
|
+
SpinnerColumn(),
|
|
266
|
+
TextColumn("[progress.description]{task.description}"),
|
|
267
|
+
BarColumn(),
|
|
268
|
+
TaskProgressColumn(),
|
|
269
|
+
TimeElapsedColumn(),
|
|
270
|
+
console=console,
|
|
271
|
+
) as progress:
|
|
272
|
+
task = progress.add_task("Ingesting...", total=len(files))
|
|
273
|
+
|
|
274
|
+
for path in files:
|
|
275
|
+
progress.update(task, description=f"[cyan]{path.name[:50]}[/cyan]")
|
|
276
|
+
r = ingest_file(path, dry_run=args.dry_run, skip_kg=args.skip_kg)
|
|
277
|
+
results.append(r)
|
|
278
|
+
progress.advance(task)
|
|
279
|
+
|
|
280
|
+
# ── Summary ───────────────────────────────────────────────────────────────
|
|
281
|
+
console.print()
|
|
282
|
+
console.rule("[bold green]Ingestion Complete[/bold green]")
|
|
283
|
+
|
|
284
|
+
summary = Table(title="Results Summary")
|
|
285
|
+
summary.add_column("File", style="cyan", max_width=45)
|
|
286
|
+
summary.add_column("Status", justify="center")
|
|
287
|
+
summary.add_column("Method")
|
|
288
|
+
summary.add_column("Parents", justify="right")
|
|
289
|
+
summary.add_column("Chunks", justify="right")
|
|
290
|
+
summary.add_column("KG", justify="right")
|
|
291
|
+
summary.add_column("Warnings", justify="right")
|
|
292
|
+
summary.add_column("Time(s)", justify="right")
|
|
293
|
+
|
|
294
|
+
ok = skipped = errors = total_parents = total_chunks = total_kg_nodes = total_kg_edges = 0
|
|
295
|
+
for r in results:
|
|
296
|
+
if r["skipped"]:
|
|
297
|
+
skipped += 1
|
|
298
|
+
status_str = "[yellow]SKIP[/yellow]"
|
|
299
|
+
elif r["status"] == "error":
|
|
300
|
+
errors += 1
|
|
301
|
+
status_str = f"[red]ERROR[/red]"
|
|
302
|
+
elif r["status"] == "empty":
|
|
303
|
+
status_str = "[dim]EMPTY[/dim]"
|
|
304
|
+
else:
|
|
305
|
+
ok += 1
|
|
306
|
+
status_str = "[green]OK[/green]"
|
|
307
|
+
|
|
308
|
+
total_parents += r["parents"]
|
|
309
|
+
total_chunks += r["chunks"]
|
|
310
|
+
total_kg_nodes += r.get("kg_nodes", 0)
|
|
311
|
+
total_kg_edges += r.get("kg_edges", 0)
|
|
312
|
+
|
|
313
|
+
kg_cell = f"{r.get('kg_nodes', 0)}n/{r.get('kg_edges', 0)}e" if r.get("kg_nodes") else ""
|
|
314
|
+
summary.add_row(
|
|
315
|
+
r["file"],
|
|
316
|
+
status_str,
|
|
317
|
+
r["parse_method"],
|
|
318
|
+
str(r["parents"]),
|
|
319
|
+
str(r["chunks"]),
|
|
320
|
+
kg_cell,
|
|
321
|
+
str(r["warnings"]) if r["warnings"] else "",
|
|
322
|
+
str(r["elapsed"]),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
console.print(summary)
|
|
326
|
+
console.print()
|
|
327
|
+
console.print(f" Processed : [green]{ok}[/green] | Skipped: [yellow]{skipped}[/yellow] | Errors: [red]{errors}[/red]")
|
|
328
|
+
console.print(f" Total parent chunks : {total_parents}")
|
|
329
|
+
console.print(f" Total child chunks : {total_chunks} (these are vector-indexed)")
|
|
330
|
+
if total_kg_nodes or total_kg_edges:
|
|
331
|
+
console.print(f" KG sync : {total_kg_nodes} node ops, {total_kg_edges} edges created")
|
|
332
|
+
|
|
333
|
+
if not args.dry_run:
|
|
334
|
+
stats = get_stats()
|
|
335
|
+
console.print(f"\n DB totals → docs: {stats['documents']} | parents: {stats['parent_chunks']} | chunks: {stats['chunks']}")
|
|
336
|
+
if stats["missing_embeddings"] > 0:
|
|
337
|
+
console.print(f" [yellow]Warning: {stats['missing_embeddings']} chunks have no embedding[/yellow]")
|
|
338
|
+
close_pool()
|
|
339
|
+
|
|
340
|
+
if errors > 0:
|
|
341
|
+
console.print(f"\n[red]{errors} file(s) failed — check logs above for details[/red]")
|
|
342
|
+
sys.exit(1)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
if __name__ == "__main__":
|
|
346
|
+
main()
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IGEL Knowledge Base — Image Ingestion Pipeline
|
|
3
|
+
==============================================
|
|
4
|
+
Iterates over every document already ingested into kb_documents, extracts
|
|
5
|
+
embedded images, runs them through GPT-4.1 Vision for verbatim + semantic
|
|
6
|
+
description, embeds the combined context, and links each image back to:
|
|
7
|
+
|
|
8
|
+
• its source document (kb_documents.id, hard FK, ON DELETE CASCADE)
|
|
9
|
+
• the nearest text section (kb_parent_chunks.id, soft FK)
|
|
10
|
+
|
|
11
|
+
Idempotent at the document level: a document already covered (has ≥1 row in
|
|
12
|
+
kb_images) is skipped unless --force is passed.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python -m knowledge_base.ingestion.ingest_images # all docs
|
|
16
|
+
python -m knowledge_base.ingestion.ingest_images --document NAME # single doc by file_name
|
|
17
|
+
python -m knowledge_base.ingestion.ingest_images --force # re-process even covered docs
|
|
18
|
+
python -m knowledge_base.ingestion.ingest_images --stats # show image stats and exit
|
|
19
|
+
python -m knowledge_base.ingestion.ingest_images --limit N # process at most N docs
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
import argparse
|
|
23
|
+
import logging
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
|
|
30
|
+
from rich.table import Table
|
|
31
|
+
|
|
32
|
+
from knowledge_base.config import cfg
|
|
33
|
+
from knowledge_base.db.client import init_pool, test_connection, close_pool
|
|
34
|
+
from knowledge_base.db.schema import (
|
|
35
|
+
create_schema, get_stats,
|
|
36
|
+
insert_image, find_nearest_parent_chunk,
|
|
37
|
+
list_documents_for_image_pass, document_has_images,
|
|
38
|
+
)
|
|
39
|
+
from knowledge_base.parsers.image_extractor import extract_images
|
|
40
|
+
from knowledge_base.embeddings.azure_embedder import embed_texts, format_for_pgvector
|
|
41
|
+
from knowledge_base.vision.image_describer import describe_image, build_embedding_text
|
|
42
|
+
|
|
43
|
+
console = Console()
|
|
44
|
+
logging.basicConfig(level=getattr(logging, cfg.LOG_LEVEL), format="%(levelname)s %(message)s")
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# Quiet down noisy HTTP loggers from openai/httpx during long runs
|
|
48
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
49
|
+
logging.getLogger("openai").setLevel(logging.WARNING)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_section_title_for_chunk(parent_chunk_id: str | None) -> str:
|
|
53
|
+
"""Look up section_title for the nearest parent chunk (best-effort)."""
|
|
54
|
+
if not parent_chunk_id:
|
|
55
|
+
return ""
|
|
56
|
+
from knowledge_base.db.client import get_conn
|
|
57
|
+
with get_conn() as conn:
|
|
58
|
+
with conn.cursor() as cur:
|
|
59
|
+
cur.execute(
|
|
60
|
+
"SELECT section_title FROM kb_parent_chunks WHERE id = %s",
|
|
61
|
+
(parent_chunk_id,)
|
|
62
|
+
)
|
|
63
|
+
row = cur.fetchone()
|
|
64
|
+
return (row[0] if row and row[0] else "") or ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def ingest_one_document(doc: dict, force: bool = False) -> dict:
|
|
68
|
+
result = {
|
|
69
|
+
"file": doc["file_name"],
|
|
70
|
+
"status": "ok",
|
|
71
|
+
"extracted": 0,
|
|
72
|
+
"described": 0,
|
|
73
|
+
"inserted": 0,
|
|
74
|
+
"errors": 0,
|
|
75
|
+
"elapsed": 0.0,
|
|
76
|
+
}
|
|
77
|
+
t0 = time.time()
|
|
78
|
+
|
|
79
|
+
# Skip if already covered
|
|
80
|
+
if not force and document_has_images(doc["id"]):
|
|
81
|
+
result["status"] = "skipped"
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
file_path = Path(doc["file_path"])
|
|
85
|
+
if not file_path.exists():
|
|
86
|
+
# Fall back to KB_DIR
|
|
87
|
+
candidate = cfg.KB_DIR / doc["file_name"]
|
|
88
|
+
if candidate.exists():
|
|
89
|
+
file_path = candidate
|
|
90
|
+
else:
|
|
91
|
+
result["status"] = "missing_file"
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
images = extract_images(file_path)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.exception("extract_images failed for %s", file_path.name)
|
|
98
|
+
result["status"] = "extract_error"
|
|
99
|
+
result["errors"] += 1
|
|
100
|
+
result["elapsed"] = round(time.time() - t0, 1)
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
result["extracted"] = len(images)
|
|
104
|
+
if not images:
|
|
105
|
+
result["status"] = "no_images"
|
|
106
|
+
result["elapsed"] = round(time.time() - t0, 1)
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
# Process images: describe + embed + insert
|
|
110
|
+
embed_texts_buffer: list[str] = []
|
|
111
|
+
pending: list[dict] = [] # accumulate metadata to insert after batch embedding
|
|
112
|
+
|
|
113
|
+
from knowledge_base.vision.image_describer import ImageDescription
|
|
114
|
+
|
|
115
|
+
for img in images:
|
|
116
|
+
parent_chunk_id = find_nearest_parent_chunk(doc["id"], img.page_number)
|
|
117
|
+
section_title = get_section_title_for_chunk(parent_chunk_id)
|
|
118
|
+
|
|
119
|
+
if img.is_recurring:
|
|
120
|
+
# Recurring header/logo/footer — describe ONCE per unique image (already
|
|
121
|
+
# deduped by hash), but with a cheap, fixed description so we don't burn
|
|
122
|
+
# vision tokens on the same logo. Binary is still preserved.
|
|
123
|
+
desc = ImageDescription(
|
|
124
|
+
image_type="recurring_header_or_logo",
|
|
125
|
+
verbatim_text="",
|
|
126
|
+
ui_elements=[],
|
|
127
|
+
semantic_description=(
|
|
128
|
+
f"Recurring page element appearing on {len(img.occurrence_pages)} of "
|
|
129
|
+
f"the document's pages (e.g. header, footer, logo, watermark). "
|
|
130
|
+
f"First seen on page {img.page_number}."
|
|
131
|
+
),
|
|
132
|
+
test_relevance="Reference image only",
|
|
133
|
+
raw_json={"recurring": True, "occurrence_pages": img.occurrence_pages},
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
try:
|
|
137
|
+
desc = describe_image(
|
|
138
|
+
image_bytes=img.image_bytes,
|
|
139
|
+
image_format=img.image_format,
|
|
140
|
+
document_name=doc["file_name"],
|
|
141
|
+
section_title=section_title,
|
|
142
|
+
page_number=img.page_number,
|
|
143
|
+
caption=img.caption,
|
|
144
|
+
surrounding_text=img.surrounding_text,
|
|
145
|
+
)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.exception("describe_image failed for %s img#%d", file_path.name, img.sequence_index)
|
|
148
|
+
result["errors"] += 1
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
if desc.error:
|
|
152
|
+
result["errors"] += 1
|
|
153
|
+
else:
|
|
154
|
+
result["described"] += 1
|
|
155
|
+
|
|
156
|
+
embed_text = build_embedding_text(
|
|
157
|
+
desc, doc["file_name"], section_title, img.page_number,
|
|
158
|
+
img.caption, img.surrounding_text,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
embed_texts_buffer.append(embed_text)
|
|
162
|
+
pending.append({
|
|
163
|
+
"img": img,
|
|
164
|
+
"desc": desc,
|
|
165
|
+
"parent_chunk_id": parent_chunk_id,
|
|
166
|
+
"section_title": section_title,
|
|
167
|
+
"embed_text": embed_text,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
# Batch-embed all images for this document
|
|
171
|
+
if not pending:
|
|
172
|
+
result["status"] = "no_described"
|
|
173
|
+
result["elapsed"] = round(time.time() - t0, 1)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
embeddings = embed_texts(embed_texts_buffer, batch_size=cfg.BATCH_SIZE)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.exception("embed_texts failed for %s", file_path.name)
|
|
180
|
+
result["status"] = "embed_error"
|
|
181
|
+
result["errors"] += len(pending)
|
|
182
|
+
result["elapsed"] = round(time.time() - t0, 1)
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
# Insert
|
|
186
|
+
for p, embedding in zip(pending, embeddings):
|
|
187
|
+
img = p["img"]
|
|
188
|
+
desc = p["desc"]
|
|
189
|
+
try:
|
|
190
|
+
insert_image(
|
|
191
|
+
document_id=doc["id"],
|
|
192
|
+
parent_chunk_id=p["parent_chunk_id"],
|
|
193
|
+
sequence_index=img.sequence_index,
|
|
194
|
+
page_number=img.page_number,
|
|
195
|
+
bbox=img.bbox,
|
|
196
|
+
image_hash=img.image_hash,
|
|
197
|
+
image_format=img.image_format,
|
|
198
|
+
width=img.width,
|
|
199
|
+
height=img.height,
|
|
200
|
+
image_bytes=img.image_bytes,
|
|
201
|
+
image_type=desc.image_type,
|
|
202
|
+
verbatim_text=desc.verbatim_text,
|
|
203
|
+
ui_elements=desc.ui_elements,
|
|
204
|
+
semantic_description=desc.semantic_description,
|
|
205
|
+
test_relevance=desc.test_relevance,
|
|
206
|
+
surrounding_text=img.surrounding_text,
|
|
207
|
+
caption=img.caption,
|
|
208
|
+
description_embedding=format_for_pgvector(embedding),
|
|
209
|
+
metadata={
|
|
210
|
+
"section_title": p["section_title"],
|
|
211
|
+
"raw_response": desc.raw_json,
|
|
212
|
+
"describer_error": desc.error,
|
|
213
|
+
"occurrence_pages": img.occurrence_pages,
|
|
214
|
+
"is_recurring": img.is_recurring,
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
result["inserted"] += 1
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.exception("insert_image failed for %s img#%d", file_path.name, img.sequence_index)
|
|
220
|
+
result["errors"] += 1
|
|
221
|
+
|
|
222
|
+
result["elapsed"] = round(time.time() - t0, 1)
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
parser = argparse.ArgumentParser(description="IGEL Knowledge Base — image ingestion pass")
|
|
228
|
+
parser.add_argument("--document", type=str, help="Single document by file_name")
|
|
229
|
+
parser.add_argument("--force", action="store_true", help="Re-process docs that already have images")
|
|
230
|
+
parser.add_argument("--stats", action="store_true", help="Show image stats and exit")
|
|
231
|
+
parser.add_argument("--limit", type=int, default=0, help="Process at most N documents")
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
|
|
234
|
+
console.rule("[bold blue]IGEL KB — Image Ingestion[/bold blue]")
|
|
235
|
+
console.print(f" Database : {cfg.DB_HOST}:{cfg.DB_PORT}/{cfg.DB_NAME}")
|
|
236
|
+
console.print(f" Vision LLM : {cfg.AZURE_DEPLOYMENT}")
|
|
237
|
+
console.print(f" Embeddings : {cfg.AZURE_EMBEDDING_DEPLOYMENT} ({cfg.EMBEDDING_DIM}d)")
|
|
238
|
+
console.print()
|
|
239
|
+
|
|
240
|
+
init_pool()
|
|
241
|
+
if not test_connection():
|
|
242
|
+
console.print("[red]Cannot connect to database[/red]")
|
|
243
|
+
sys.exit(1)
|
|
244
|
+
create_schema() # idempotent — ensures kb_images table exists
|
|
245
|
+
|
|
246
|
+
if args.stats:
|
|
247
|
+
s = get_stats()
|
|
248
|
+
t = Table(title="KB Stats")
|
|
249
|
+
for k, v in s.items():
|
|
250
|
+
t.add_row(k.replace("_", " ").title(), str(v))
|
|
251
|
+
console.print(t)
|
|
252
|
+
close_pool()
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
docs = list_documents_for_image_pass()
|
|
256
|
+
if args.document:
|
|
257
|
+
docs = [d for d in docs if d["file_name"] == args.document]
|
|
258
|
+
if not docs:
|
|
259
|
+
console.print(f"[red]No document found with file_name={args.document}[/red]")
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
if args.limit:
|
|
262
|
+
docs = docs[: args.limit]
|
|
263
|
+
|
|
264
|
+
console.print(f"Found [bold]{len(docs)}[/bold] documents to process\n")
|
|
265
|
+
|
|
266
|
+
results = []
|
|
267
|
+
with Progress(
|
|
268
|
+
SpinnerColumn(),
|
|
269
|
+
TextColumn("[progress.description]{task.description}"),
|
|
270
|
+
BarColumn(),
|
|
271
|
+
TaskProgressColumn(),
|
|
272
|
+
TimeElapsedColumn(),
|
|
273
|
+
console=console,
|
|
274
|
+
) as progress:
|
|
275
|
+
task = progress.add_task("Image ingest...", total=len(docs))
|
|
276
|
+
|
|
277
|
+
for d in docs:
|
|
278
|
+
progress.update(task, description=f"[cyan]{d['file_name'][:60]}[/cyan]")
|
|
279
|
+
r = ingest_one_document(d, force=args.force)
|
|
280
|
+
results.append(r)
|
|
281
|
+
progress.advance(task)
|
|
282
|
+
|
|
283
|
+
# Summary
|
|
284
|
+
console.print()
|
|
285
|
+
console.rule("[bold green]Image Ingest Complete[/bold green]")
|
|
286
|
+
summary = Table(title="Per-Document Image Stats")
|
|
287
|
+
summary.add_column("File", style="cyan", max_width=45)
|
|
288
|
+
summary.add_column("Status", justify="center")
|
|
289
|
+
summary.add_column("Extracted", justify="right")
|
|
290
|
+
summary.add_column("Described", justify="right")
|
|
291
|
+
summary.add_column("Inserted", justify="right")
|
|
292
|
+
summary.add_column("Errors", justify="right")
|
|
293
|
+
summary.add_column("Time(s)", justify="right")
|
|
294
|
+
|
|
295
|
+
total_extracted = total_inserted = total_errors = 0
|
|
296
|
+
skipped = no_images = ok = 0
|
|
297
|
+
for r in results:
|
|
298
|
+
status = r["status"]
|
|
299
|
+
if status == "skipped":
|
|
300
|
+
status_str = "[yellow]SKIP[/yellow]"; skipped += 1
|
|
301
|
+
elif status == "no_images":
|
|
302
|
+
status_str = "[dim]NONE[/dim]"; no_images += 1
|
|
303
|
+
elif status == "ok":
|
|
304
|
+
status_str = "[green]OK[/green]"; ok += 1
|
|
305
|
+
else:
|
|
306
|
+
status_str = f"[red]{status.upper()}[/red]"
|
|
307
|
+
|
|
308
|
+
total_extracted += r["extracted"]
|
|
309
|
+
total_inserted += r["inserted"]
|
|
310
|
+
total_errors += r["errors"]
|
|
311
|
+
|
|
312
|
+
summary.add_row(
|
|
313
|
+
r["file"], status_str,
|
|
314
|
+
str(r["extracted"]), str(r["described"]), str(r["inserted"]),
|
|
315
|
+
str(r["errors"]) if r["errors"] else "",
|
|
316
|
+
str(r["elapsed"]),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
console.print(summary)
|
|
320
|
+
console.print()
|
|
321
|
+
console.print(f" Processed: [green]{ok}[/green] | Skipped: [yellow]{skipped}[/yellow] | No images: [dim]{no_images}[/dim]")
|
|
322
|
+
console.print(f" Total extracted: {total_extracted} | Inserted: {total_inserted} | Errors: {total_errors}")
|
|
323
|
+
|
|
324
|
+
s = get_stats()
|
|
325
|
+
console.print(f"\n DB totals -> images: {s['images']} | missing embeddings: {s['images_missing_embeddings']}")
|
|
326
|
+
close_pool()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
if __name__ == "__main__":
|
|
330
|
+
main()
|