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,151 @@
|
|
|
1
|
+
"""Parse HTML pages (kb.igel.com, community sites) into Section objects for chunking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from html import unescape
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from urllib.parse import urljoin, urlparse
|
|
10
|
+
|
|
11
|
+
from knowledge_base.parsers.markdown_parser import Section
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ParsedWebPage:
|
|
16
|
+
url: str
|
|
17
|
+
title: str
|
|
18
|
+
sections: list[Section]
|
|
19
|
+
raw_text: str
|
|
20
|
+
source_type: str = "kb_web"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _strip_tags(html: str) -> str:
|
|
24
|
+
text = re.sub(r"(?is)<(script|style|nav|footer|header|aside)[^>]*>.*?</\1>", " ", html)
|
|
25
|
+
text = re.sub(r"(?is)<br\s*/?>", "\n", text)
|
|
26
|
+
text = re.sub(r"(?is)</?(p|div|li|h[1-6]|tr|td|th)[^>]*>", "\n", text)
|
|
27
|
+
text = re.sub(r"<[^>]+>", " ", text)
|
|
28
|
+
text = unescape(text)
|
|
29
|
+
text = re.sub(r"[ \t]+\n", "\n", text)
|
|
30
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
31
|
+
return text.strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_title(html: str, fallback: str) -> str:
|
|
35
|
+
m = re.search(r"(?is)<title[^>]*>(.*?)</title>", html)
|
|
36
|
+
if m:
|
|
37
|
+
title = _strip_tags(m.group(1)).strip()
|
|
38
|
+
if title:
|
|
39
|
+
return title[:200]
|
|
40
|
+
for tag in ("h1", "h2"):
|
|
41
|
+
m = re.search(rf"(?is)<{tag}[^>]*>(.*?)</{tag}>", html)
|
|
42
|
+
if m:
|
|
43
|
+
title = _strip_tags(m.group(1)).strip()
|
|
44
|
+
if title:
|
|
45
|
+
return title[:200]
|
|
46
|
+
return fallback
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_main_html(html: str) -> str:
|
|
50
|
+
"""Best-effort main content extraction for Confluence / Scroll Sites pages."""
|
|
51
|
+
for pattern in (
|
|
52
|
+
r'(?is)<main[^>]*>(.*?)</main>',
|
|
53
|
+
r'(?is)<article[^>]*>(.*?)</article>',
|
|
54
|
+
r'(?is)<div[^>]*id="main-content"[^>]*>(.*?)</div>',
|
|
55
|
+
r'(?is)<div[^>]*class="[^"]*wiki-content[^"]*"[^>]*>(.*?)</div>',
|
|
56
|
+
r'(?is)<div[^>]*class="[^"]*page-content[^"]*"[^>]*>(.*?)</div>',
|
|
57
|
+
r'(?is)<div[^>]*class="[^"]*content[^"]*"[^>]*>(.*?)</div>',
|
|
58
|
+
):
|
|
59
|
+
m = re.search(pattern, html)
|
|
60
|
+
if m and len(m.group(1).strip()) > 200:
|
|
61
|
+
return m.group(1)
|
|
62
|
+
return html
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _split_headings(text: str, page_title: str, url: str) -> list[Section]:
|
|
66
|
+
lines = text.split("\n")
|
|
67
|
+
sections: list[Section] = []
|
|
68
|
+
current_title = page_title
|
|
69
|
+
current_lines: list[str] = []
|
|
70
|
+
|
|
71
|
+
for line in lines:
|
|
72
|
+
heading = re.match(r"^(#{1,4})\s+(.+)", line.strip())
|
|
73
|
+
if heading:
|
|
74
|
+
content = "\n".join(current_lines).strip()
|
|
75
|
+
if content:
|
|
76
|
+
sections.append(
|
|
77
|
+
Section(
|
|
78
|
+
title=current_title,
|
|
79
|
+
level=len(heading.group(1)),
|
|
80
|
+
content=content,
|
|
81
|
+
page_hint=0,
|
|
82
|
+
metadata={"canonical_url": url, "source_type": "kb_web"},
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
current_title = heading.group(2).strip()
|
|
86
|
+
current_lines = []
|
|
87
|
+
else:
|
|
88
|
+
current_lines.append(line)
|
|
89
|
+
|
|
90
|
+
content = "\n".join(current_lines).strip()
|
|
91
|
+
if content:
|
|
92
|
+
sections.append(
|
|
93
|
+
Section(
|
|
94
|
+
title=current_title,
|
|
95
|
+
level=1,
|
|
96
|
+
content=content,
|
|
97
|
+
page_hint=0,
|
|
98
|
+
metadata={"canonical_url": url, "source_type": "kb_web"},
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not sections and text.strip():
|
|
103
|
+
sections.append(
|
|
104
|
+
Section(
|
|
105
|
+
title=page_title,
|
|
106
|
+
level=1,
|
|
107
|
+
content=text.strip(),
|
|
108
|
+
page_hint=0,
|
|
109
|
+
metadata={"canonical_url": url, "source_type": "kb_web"},
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
return sections
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def parse_html_page(html: str, url: str, *, source_type: str = "kb_web") -> ParsedWebPage:
|
|
116
|
+
"""Convert an HTML page into structured sections."""
|
|
117
|
+
title = _extract_title(html, fallback=urlparse(url).path.split("/")[-1] or url)
|
|
118
|
+
main_html = _extract_main_html(html)
|
|
119
|
+
text = _strip_tags(main_html)
|
|
120
|
+
|
|
121
|
+
# Promote obvious headings (ALL CAPS lines or numbered sections)
|
|
122
|
+
promoted: list[str] = []
|
|
123
|
+
for line in text.split("\n"):
|
|
124
|
+
stripped = line.strip()
|
|
125
|
+
if re.match(r"^[A-Z][A-Z0-9\s\-/&]{8,}$", stripped):
|
|
126
|
+
promoted.append(f"## {stripped.title()}")
|
|
127
|
+
elif re.match(r"^\d+\.\s+[A-Z]", stripped):
|
|
128
|
+
promoted.append(stripped)
|
|
129
|
+
else:
|
|
130
|
+
promoted.append(line)
|
|
131
|
+
text = "\n".join(promoted)
|
|
132
|
+
|
|
133
|
+
sections = _split_headings(text, title, url)
|
|
134
|
+
for sec in sections:
|
|
135
|
+
sec.metadata["canonical_url"] = url
|
|
136
|
+
sec.metadata["source_type"] = source_type
|
|
137
|
+
|
|
138
|
+
return ParsedWebPage(url=url, title=title, sections=sections, raw_text=text, source_type=source_type)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def resolve_links(html: str, base_url: str) -> list[str]:
|
|
142
|
+
"""Extract same-domain links from HTML for crawl expansion."""
|
|
143
|
+
base_host = urlparse(base_url).netloc
|
|
144
|
+
links: list[str] = []
|
|
145
|
+
for href in re.findall(r'(?is)href=["\']([^"\']+)["\']', html):
|
|
146
|
+
if href.startswith("#") or href.startswith("mailto:"):
|
|
147
|
+
continue
|
|
148
|
+
full = urljoin(base_url, href)
|
|
149
|
+
if urlparse(full).netloc == base_host and full not in links:
|
|
150
|
+
links.append(full.split("#")[0])
|
|
151
|
+
return links[:50]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image extraction from PDFs, DOCX, and DOC files.
|
|
3
|
+
|
|
4
|
+
Each extracted image carries enough context to be linked back to its source
|
|
5
|
+
document and the nearest text section:
|
|
6
|
+
- bytes (raw, byte-perfect)
|
|
7
|
+
- format / dimensions
|
|
8
|
+
- page_number (PDF) or paragraph_index (DOCX)
|
|
9
|
+
- bbox on page (PDF only)
|
|
10
|
+
- surrounding_text (paragraphs above + below)
|
|
11
|
+
- caption (if heuristically detectable)
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import io
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ExtractedImage:
|
|
26
|
+
sequence_index: int
|
|
27
|
+
image_bytes: bytes
|
|
28
|
+
image_format: str # "png" | "jpeg" | "jpg" | ...
|
|
29
|
+
width: int | None
|
|
30
|
+
height: int | None
|
|
31
|
+
page_number: int | None # 1-based for PDFs (first occurrence)
|
|
32
|
+
bbox: dict | None # {x0,y0,x1,y1} for PDFs (first occurrence)
|
|
33
|
+
surrounding_text: str # paragraph(s) above + below the FIRST occurrence
|
|
34
|
+
caption: str | None
|
|
35
|
+
occurrence_pages: list[int] = field(default_factory=list) # all pages this image appears on
|
|
36
|
+
is_recurring: bool = False # likely header/logo/footer if appears on >25% of pages
|
|
37
|
+
image_hash: str = field(init=False)
|
|
38
|
+
|
|
39
|
+
def __post_init__(self):
|
|
40
|
+
self.image_hash = hashlib.sha256(self.image_bytes).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── PDFs (PyMuPDF) ────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
# Filter: skip tiny decorative images and giant page-wide background images
|
|
46
|
+
MIN_IMAGE_BYTES = 2 * 1024 # 2 KB
|
|
47
|
+
MIN_DIMENSION = 50 # pixels
|
|
48
|
+
MAX_DIMENSION = 8000 # pixels (filter out scanned-page backgrounds)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def extract_images_from_pdf(pdf_path: Path) -> list[ExtractedImage]:
|
|
52
|
+
"""Extract unique images from a PDF.
|
|
53
|
+
|
|
54
|
+
Critical: same logo / header / footer often appears on every page.
|
|
55
|
+
We dedupe by sha256 across the WHOLE document, keep the first occurrence's
|
|
56
|
+
page_number/bbox/surrounding_text, and record all pages it appears on so the
|
|
57
|
+
describer can flag recurring headers.
|
|
58
|
+
"""
|
|
59
|
+
import fitz # PyMuPDF
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
doc = fitz.open(str(pdf_path))
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error("PyMuPDF could not open %s: %s", pdf_path.name, e)
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
page_count = len(doc)
|
|
68
|
+
|
|
69
|
+
# First pass: build (hash → list of occurrences) and (xref → bytes/meta)
|
|
70
|
+
# An "occurrence" = the page it appeared on, plus first-seen bbox + page text.
|
|
71
|
+
# We process pages in order so the FIRST occurrence becomes the canonical one.
|
|
72
|
+
occurrences: dict[str, dict] = {} # hash -> {bytes, ext, w, h, first_page, first_bbox, first_page_text, pages: [int]}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
for page_idx in range(page_count):
|
|
76
|
+
page = doc[page_idx]
|
|
77
|
+
page_num = page_idx + 1
|
|
78
|
+
page_text = page.get_text("text") or ""
|
|
79
|
+
|
|
80
|
+
seen_in_page: set[str] = set()
|
|
81
|
+
for img_info in page.get_images(full=True):
|
|
82
|
+
xref = img_info[0]
|
|
83
|
+
try:
|
|
84
|
+
info = doc.extract_image(xref)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.warning("extract_image failed (xref=%s) in %s: %s", xref, pdf_path.name, e)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
img_bytes = info.get("image")
|
|
90
|
+
ext = (info.get("ext") or "png").lower()
|
|
91
|
+
w = info.get("width")
|
|
92
|
+
h = info.get("height")
|
|
93
|
+
|
|
94
|
+
if not img_bytes or len(img_bytes) < MIN_IMAGE_BYTES:
|
|
95
|
+
continue
|
|
96
|
+
if w and h:
|
|
97
|
+
if w < MIN_DIMENSION or h < MIN_DIMENSION:
|
|
98
|
+
continue
|
|
99
|
+
if w > MAX_DIMENSION or h > MAX_DIMENSION:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
hsh = hashlib.sha256(img_bytes).hexdigest()
|
|
103
|
+
if hsh in seen_in_page:
|
|
104
|
+
continue
|
|
105
|
+
seen_in_page.add(hsh)
|
|
106
|
+
|
|
107
|
+
if hsh in occurrences:
|
|
108
|
+
# Already recorded — just track the page
|
|
109
|
+
occurrences[hsh]["pages"].append(page_num)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# First time seeing this image — record canonical position
|
|
113
|
+
bbox = None
|
|
114
|
+
try:
|
|
115
|
+
rects = page.get_image_rects(xref)
|
|
116
|
+
if rects:
|
|
117
|
+
r = rects[0]
|
|
118
|
+
bbox = {"x0": float(r.x0), "y0": float(r.y0),
|
|
119
|
+
"x1": float(r.x1), "y1": float(r.y1)}
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
occurrences[hsh] = {
|
|
124
|
+
"bytes": img_bytes,
|
|
125
|
+
"ext": ext,
|
|
126
|
+
"w": w, "h": h,
|
|
127
|
+
"first_page": page_num,
|
|
128
|
+
"first_page_idx": page_idx,
|
|
129
|
+
"first_bbox": bbox,
|
|
130
|
+
"first_page_text": page_text,
|
|
131
|
+
"pages": [page_num],
|
|
132
|
+
}
|
|
133
|
+
finally:
|
|
134
|
+
# Defer doc.close until after we read previous/next page text below
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Second pass: build ExtractedImage per unique hash, with surrounding_text
|
|
138
|
+
# composed from the FIRST page's neighbors. Mark recurring if appears on
|
|
139
|
+
# > 25% of pages.
|
|
140
|
+
images: list[ExtractedImage] = []
|
|
141
|
+
seq = 0
|
|
142
|
+
recurring_threshold = max(3, int(page_count * 0.25))
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Sort by first occurrence so sequence_index reflects reading order
|
|
146
|
+
for hsh, occ in sorted(occurrences.items(), key=lambda kv: (kv[1]["first_page"], kv[1]["pages"][0])):
|
|
147
|
+
page_idx = occ["first_page_idx"]
|
|
148
|
+
page_num = occ["first_page"]
|
|
149
|
+
page_text = occ["first_page_text"]
|
|
150
|
+
|
|
151
|
+
surrounding_parts = []
|
|
152
|
+
if page_idx > 0:
|
|
153
|
+
try:
|
|
154
|
+
prev = doc[page_idx - 1].get_text("text") or ""
|
|
155
|
+
if prev:
|
|
156
|
+
surrounding_parts.append(prev[-600:].strip())
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
if page_text.strip():
|
|
160
|
+
surrounding_parts.append(page_text.strip()[:1500])
|
|
161
|
+
if page_idx + 1 < page_count:
|
|
162
|
+
try:
|
|
163
|
+
nxt = doc[page_idx + 1].get_text("text") or ""
|
|
164
|
+
if nxt:
|
|
165
|
+
surrounding_parts.append(nxt[:600].strip())
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
surrounding_text = "\n\n".join(surrounding_parts)
|
|
169
|
+
|
|
170
|
+
caption = _find_caption(page_text, occ["first_bbox"])
|
|
171
|
+
occurrence_pages = sorted(set(occ["pages"]))
|
|
172
|
+
is_recurring = len(occurrence_pages) >= recurring_threshold and page_count >= 4
|
|
173
|
+
|
|
174
|
+
seq += 1
|
|
175
|
+
images.append(ExtractedImage(
|
|
176
|
+
sequence_index=seq,
|
|
177
|
+
image_bytes=occ["bytes"],
|
|
178
|
+
image_format=occ["ext"],
|
|
179
|
+
width=occ["w"],
|
|
180
|
+
height=occ["h"],
|
|
181
|
+
page_number=page_num,
|
|
182
|
+
bbox=occ["first_bbox"],
|
|
183
|
+
surrounding_text=surrounding_text,
|
|
184
|
+
caption=caption,
|
|
185
|
+
occurrence_pages=occurrence_pages,
|
|
186
|
+
is_recurring=is_recurring,
|
|
187
|
+
))
|
|
188
|
+
finally:
|
|
189
|
+
doc.close()
|
|
190
|
+
|
|
191
|
+
return images
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _find_caption(page_text: str, bbox: dict | None) -> str | None:
|
|
195
|
+
import re
|
|
196
|
+
if not page_text:
|
|
197
|
+
return None
|
|
198
|
+
# Heuristic: find a "Figure N: ..." or "Fig. N ..." or "Table N: ..." line
|
|
199
|
+
m = re.search(r"^(?:Figure|Fig\.|Table)\s*\d+[:.]?\s*[^\n]{0,200}",
|
|
200
|
+
page_text, flags=re.MULTILINE)
|
|
201
|
+
if m:
|
|
202
|
+
return m.group(0).strip()
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ── DOCX (python-docx + relationships) ───────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
# Inline shapes / images live in the document's relationships.
|
|
209
|
+
# We iterate paragraphs in order; whenever a run contains a drawing element
|
|
210
|
+
# referencing an image relationship, we record that image at that point.
|
|
211
|
+
|
|
212
|
+
def extract_images_from_docx(docx_path: Path) -> list[ExtractedImage]:
|
|
213
|
+
from docx import Document
|
|
214
|
+
from docx.oxml.ns import qn
|
|
215
|
+
|
|
216
|
+
images: list[ExtractedImage] = []
|
|
217
|
+
seq = 0
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
doc = Document(str(docx_path))
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error("python-docx could not open %s: %s", docx_path.name, e)
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
# Build a flat ordered list of (paragraph_index, paragraph_text, image_rels[])
|
|
226
|
+
paragraphs = doc.paragraphs
|
|
227
|
+
|
|
228
|
+
# First pass: linearize into events
|
|
229
|
+
events: list[tuple[str, int, str | list]] = []
|
|
230
|
+
# event types: ("text", para_idx, text), ("image", para_idx, [rel_id, ...])
|
|
231
|
+
for p_idx, para in enumerate(paragraphs):
|
|
232
|
+
text = para.text.strip()
|
|
233
|
+
if text:
|
|
234
|
+
events.append(("text", p_idx, text))
|
|
235
|
+
|
|
236
|
+
# Find image relationship ids inside this paragraph
|
|
237
|
+
rel_ids: list[str] = []
|
|
238
|
+
for run in para.runs:
|
|
239
|
+
for blip in run._element.iter(qn("a:blip")):
|
|
240
|
+
rid = blip.get(qn("r:embed"))
|
|
241
|
+
if rid:
|
|
242
|
+
rel_ids.append(rid)
|
|
243
|
+
if rel_ids:
|
|
244
|
+
events.append(("image", p_idx, rel_ids))
|
|
245
|
+
|
|
246
|
+
# Second pass: emit ExtractedImage at each "image" event with surrounding text
|
|
247
|
+
rels = doc.part.related_parts
|
|
248
|
+
for ev_idx, ev in enumerate(events):
|
|
249
|
+
if ev[0] != "image":
|
|
250
|
+
continue
|
|
251
|
+
_, p_idx, rel_ids = ev
|
|
252
|
+
|
|
253
|
+
# Surrounding text: 2 prior text events + 2 following text events
|
|
254
|
+
before = [e[2] for e in events[max(0, ev_idx - 6):ev_idx] if e[0] == "text"][-3:]
|
|
255
|
+
after = [e[2] for e in events[ev_idx + 1: ev_idx + 7] if e[0] == "text"][:3]
|
|
256
|
+
surrounding_text = "\n\n".join(before + after)
|
|
257
|
+
|
|
258
|
+
# Caption: heuristic — paragraph immediately after that starts with Figure/Table
|
|
259
|
+
caption = None
|
|
260
|
+
for e in events[ev_idx + 1: ev_idx + 4]:
|
|
261
|
+
if e[0] == "text":
|
|
262
|
+
t = e[2]
|
|
263
|
+
if t[:7].lower() in ("figure ", "fig. ", "table ") or \
|
|
264
|
+
t.lower().startswith(("figure", "fig.", "table")):
|
|
265
|
+
caption = t[:200]
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
for rel_id in rel_ids:
|
|
269
|
+
part = rels.get(rel_id)
|
|
270
|
+
if part is None or not hasattr(part, "blob"):
|
|
271
|
+
continue
|
|
272
|
+
img_bytes: bytes = part.blob
|
|
273
|
+
if not img_bytes or len(img_bytes) < MIN_IMAGE_BYTES:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Try to determine format + dimensions via Pillow
|
|
277
|
+
ext = "png"
|
|
278
|
+
w = h = None
|
|
279
|
+
try:
|
|
280
|
+
from PIL import Image
|
|
281
|
+
im = Image.open(io.BytesIO(img_bytes))
|
|
282
|
+
ext = (im.format or "png").lower()
|
|
283
|
+
if ext == "jpeg":
|
|
284
|
+
ext = "jpg"
|
|
285
|
+
w, h = im.size
|
|
286
|
+
if w < MIN_DIMENSION or h < MIN_DIMENSION:
|
|
287
|
+
continue
|
|
288
|
+
if w > MAX_DIMENSION or h > MAX_DIMENSION:
|
|
289
|
+
continue
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
seq += 1
|
|
294
|
+
images.append(ExtractedImage(
|
|
295
|
+
sequence_index=seq,
|
|
296
|
+
image_bytes=img_bytes,
|
|
297
|
+
image_format=ext,
|
|
298
|
+
width=w,
|
|
299
|
+
height=h,
|
|
300
|
+
page_number=None, # DOCX has no page numbers without rendering
|
|
301
|
+
bbox=None,
|
|
302
|
+
surrounding_text=surrounding_text,
|
|
303
|
+
caption=caption,
|
|
304
|
+
))
|
|
305
|
+
|
|
306
|
+
return images
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ── DOC binary (Word legacy) — convert to DOCX first via Word COM if available ──
|
|
310
|
+
|
|
311
|
+
def extract_images_from_doc(doc_path: Path) -> list[ExtractedImage]:
|
|
312
|
+
"""Best-effort: convert .doc to .docx via Word COM, then reuse DOCX extractor.
|
|
313
|
+
Returns [] if Microsoft Word isn't available on this host."""
|
|
314
|
+
import tempfile
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
import win32com.client # type: ignore
|
|
318
|
+
import pythoncom # type: ignore
|
|
319
|
+
except ImportError:
|
|
320
|
+
logger.info("pywin32 not available — cannot extract images from .doc: %s", doc_path.name)
|
|
321
|
+
return []
|
|
322
|
+
|
|
323
|
+
pythoncom.CoInitialize()
|
|
324
|
+
word = None
|
|
325
|
+
try:
|
|
326
|
+
word = win32com.client.Dispatch("Word.Application")
|
|
327
|
+
word.Visible = False
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.info("Microsoft Word COM unavailable — skipping images for %s (%s)", doc_path.name, e)
|
|
330
|
+
return []
|
|
331
|
+
|
|
332
|
+
tmp = Path(tempfile.gettempdir()) / f"{doc_path.stem}_converted.docx"
|
|
333
|
+
try:
|
|
334
|
+
d = word.Documents.Open(str(doc_path.resolve()))
|
|
335
|
+
# WdSaveFormat.wdFormatXMLDocument = 12 (.docx)
|
|
336
|
+
d.SaveAs(str(tmp), FileFormat=12)
|
|
337
|
+
d.Close(False)
|
|
338
|
+
word.Quit()
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.warning("Could not convert %s -> docx: %s", doc_path.name, e)
|
|
341
|
+
try:
|
|
342
|
+
if word is not None:
|
|
343
|
+
word.Quit()
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
return []
|
|
347
|
+
|
|
348
|
+
if not tmp.exists():
|
|
349
|
+
return []
|
|
350
|
+
|
|
351
|
+
images = extract_images_from_docx(tmp)
|
|
352
|
+
try:
|
|
353
|
+
tmp.unlink()
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
return images
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ── Dispatcher ────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
def extract_images(file_path: Path) -> list[ExtractedImage]:
|
|
362
|
+
suffix = file_path.suffix.lower()
|
|
363
|
+
if suffix == ".pdf":
|
|
364
|
+
return extract_images_from_pdf(file_path)
|
|
365
|
+
if suffix == ".docx":
|
|
366
|
+
return extract_images_from_docx(file_path)
|
|
367
|
+
if suffix == ".doc":
|
|
368
|
+
return extract_images_from_doc(file_path)
|
|
369
|
+
# CSV, MD, MHT, etc. — no embedded images
|
|
370
|
+
return []
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parses pre-converted Markdown files from marker-pdf output.
|
|
3
|
+
Priority: if a PDF already has a markdown_output folder, use it — it's higher quality
|
|
4
|
+
than raw pdfplumber extraction because it's been processed with Azure OpenAI LLM.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Section:
|
|
14
|
+
title: str
|
|
15
|
+
level: int # 1=H1, 2=H2, 3=H3, 0=no heading
|
|
16
|
+
content: str
|
|
17
|
+
page_hint: int = 0 # approximate page (from image filenames or context)
|
|
18
|
+
metadata: dict = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_markdown_file(md_path: Path) -> list[Section]:
|
|
22
|
+
text = md_path.read_text(encoding="utf-8", errors="replace")
|
|
23
|
+
return _split_into_sections(text, str(md_path.name))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _split_into_sections(text: str, source_name: str) -> list[Section]:
|
|
27
|
+
# Strip HTML comments (marker-pdf source headers)
|
|
28
|
+
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL).strip()
|
|
29
|
+
|
|
30
|
+
lines = text.split("\n")
|
|
31
|
+
sections: list[Section] = []
|
|
32
|
+
current_title = source_name
|
|
33
|
+
current_level = 0
|
|
34
|
+
current_lines: list[str] = []
|
|
35
|
+
page_hint = 1
|
|
36
|
+
|
|
37
|
+
for line in lines:
|
|
38
|
+
heading_match = re.match(r"^(#{1,4})\s+(.+)", line)
|
|
39
|
+
if heading_match:
|
|
40
|
+
# Flush previous section
|
|
41
|
+
content = "\n".join(current_lines).strip()
|
|
42
|
+
if content:
|
|
43
|
+
sections.append(Section(
|
|
44
|
+
title=current_title,
|
|
45
|
+
level=current_level,
|
|
46
|
+
content=content,
|
|
47
|
+
page_hint=page_hint,
|
|
48
|
+
))
|
|
49
|
+
current_title = heading_match.group(2).strip()
|
|
50
|
+
current_level = len(heading_match.group(1))
|
|
51
|
+
current_lines = []
|
|
52
|
+
else:
|
|
53
|
+
# Track page hints from image references like images/page_5_img_1.png
|
|
54
|
+
page_ref = re.search(r"page_(\d+)", line)
|
|
55
|
+
if page_ref:
|
|
56
|
+
page_hint = int(page_ref.group(1))
|
|
57
|
+
current_lines.append(line)
|
|
58
|
+
|
|
59
|
+
# Flush last section
|
|
60
|
+
content = "\n".join(current_lines).strip()
|
|
61
|
+
if content:
|
|
62
|
+
sections.append(Section(
|
|
63
|
+
title=current_title,
|
|
64
|
+
level=current_level,
|
|
65
|
+
content=content,
|
|
66
|
+
page_hint=page_hint,
|
|
67
|
+
))
|
|
68
|
+
|
|
69
|
+
# Merge tiny orphan sections (heading-only, < 50 chars) into next sibling
|
|
70
|
+
return _merge_orphan_sections(sections)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _merge_orphan_sections(sections: list[Section]) -> list[Section]:
|
|
74
|
+
merged: list[Section] = []
|
|
75
|
+
i = 0
|
|
76
|
+
while i < len(sections):
|
|
77
|
+
sec = sections[i]
|
|
78
|
+
# If this section has almost no content and there's a next section at same/lower level
|
|
79
|
+
if len(sec.content) < 60 and i + 1 < len(sections):
|
|
80
|
+
next_sec = sections[i + 1]
|
|
81
|
+
# Prepend this section's content into next section with heading as context
|
|
82
|
+
next_sec.content = f"**{sec.title}**\n{sec.content}\n\n{next_sec.content}".strip()
|
|
83
|
+
i += 1
|
|
84
|
+
continue
|
|
85
|
+
merged.append(sec)
|
|
86
|
+
i += 1
|
|
87
|
+
return merged
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_markdown_for_pdf(pdf_filename: str, markdown_output_dir: Path) -> Path | None:
|
|
91
|
+
"""
|
|
92
|
+
Given a PDF filename, find its pre-converted markdown file in markdown_output/.
|
|
93
|
+
Returns the .md file path if found, else None.
|
|
94
|
+
"""
|
|
95
|
+
stem = Path(pdf_filename).stem
|
|
96
|
+
# Normalize: spaces → underscores, remove parens/commas (matches convert_pdfs.py logic)
|
|
97
|
+
safe_dir = stem.replace(" ", "_").replace("(", "").replace(")", "").replace(",", "")
|
|
98
|
+
md_path = markdown_output_dir / safe_dir / f"{stem}.md"
|
|
99
|
+
|
|
100
|
+
if md_path.exists():
|
|
101
|
+
return md_path
|
|
102
|
+
|
|
103
|
+
# Also try with cleaned filename
|
|
104
|
+
for folder in markdown_output_dir.iterdir():
|
|
105
|
+
if folder.is_dir():
|
|
106
|
+
candidate = folder / f"{folder.name}.md"
|
|
107
|
+
if candidate.exists() and _fuzzy_match(stem, folder.name):
|
|
108
|
+
return candidate
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _fuzzy_match(a: str, b: str) -> bool:
|
|
114
|
+
"""Simple match ignoring spaces, underscores, case."""
|
|
115
|
+
norm = lambda s: s.lower().replace("_", "").replace(" ", "").replace("-", "")
|
|
116
|
+
return norm(a) in norm(b) or norm(b) in norm(a)
|