tapps-agents 3.5.40__py3-none-any.whl → 3.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tapps_agents/__init__.py +2 -2
- tapps_agents/agents/__init__.py +22 -22
- tapps_agents/agents/analyst/__init__.py +5 -5
- tapps_agents/agents/architect/__init__.py +5 -5
- tapps_agents/agents/architect/agent.py +1033 -1033
- tapps_agents/agents/architect/pattern_detector.py +75 -75
- tapps_agents/agents/cleanup/__init__.py +7 -7
- tapps_agents/agents/cleanup/agent.py +445 -445
- tapps_agents/agents/debugger/__init__.py +7 -7
- tapps_agents/agents/debugger/agent.py +310 -310
- tapps_agents/agents/debugger/error_analyzer.py +437 -437
- tapps_agents/agents/designer/__init__.py +5 -5
- tapps_agents/agents/designer/agent.py +786 -786
- tapps_agents/agents/designer/visual_designer.py +638 -638
- tapps_agents/agents/documenter/__init__.py +7 -7
- tapps_agents/agents/documenter/agent.py +531 -531
- tapps_agents/agents/documenter/doc_generator.py +472 -472
- tapps_agents/agents/documenter/doc_validator.py +393 -393
- tapps_agents/agents/documenter/framework_doc_updater.py +493 -493
- tapps_agents/agents/enhancer/__init__.py +7 -7
- tapps_agents/agents/evaluator/__init__.py +7 -7
- tapps_agents/agents/evaluator/agent.py +443 -443
- tapps_agents/agents/evaluator/priority_evaluator.py +641 -641
- tapps_agents/agents/evaluator/quality_analyzer.py +147 -147
- tapps_agents/agents/evaluator/report_generator.py +344 -344
- tapps_agents/agents/evaluator/usage_analyzer.py +192 -192
- tapps_agents/agents/evaluator/workflow_analyzer.py +189 -189
- tapps_agents/agents/implementer/__init__.py +7 -7
- tapps_agents/agents/implementer/agent.py +798 -798
- tapps_agents/agents/implementer/auto_fix.py +1119 -1119
- tapps_agents/agents/implementer/code_generator.py +73 -73
- tapps_agents/agents/improver/__init__.py +1 -1
- tapps_agents/agents/improver/agent.py +753 -753
- tapps_agents/agents/ops/__init__.py +1 -1
- tapps_agents/agents/ops/agent.py +619 -619
- tapps_agents/agents/ops/dependency_analyzer.py +600 -600
- tapps_agents/agents/orchestrator/__init__.py +5 -5
- tapps_agents/agents/orchestrator/agent.py +522 -522
- tapps_agents/agents/planner/__init__.py +7 -7
- tapps_agents/agents/planner/agent.py +1127 -1127
- tapps_agents/agents/reviewer/__init__.py +24 -24
- tapps_agents/agents/reviewer/agent.py +3513 -3513
- tapps_agents/agents/reviewer/aggregator.py +213 -213
- tapps_agents/agents/reviewer/batch_review.py +448 -448
- tapps_agents/agents/reviewer/cache.py +443 -443
- tapps_agents/agents/reviewer/context7_enhancer.py +630 -630
- tapps_agents/agents/reviewer/context_detector.py +203 -203
- tapps_agents/agents/reviewer/docker_compose_validator.py +158 -158
- tapps_agents/agents/reviewer/dockerfile_validator.py +176 -176
- tapps_agents/agents/reviewer/error_handling.py +126 -126
- tapps_agents/agents/reviewer/feedback_generator.py +490 -490
- tapps_agents/agents/reviewer/influxdb_validator.py +316 -316
- tapps_agents/agents/reviewer/issue_tracking.py +169 -169
- tapps_agents/agents/reviewer/library_detector.py +295 -295
- tapps_agents/agents/reviewer/library_patterns.py +268 -268
- tapps_agents/agents/reviewer/maintainability_scorer.py +593 -593
- tapps_agents/agents/reviewer/metric_strategies.py +276 -276
- tapps_agents/agents/reviewer/mqtt_validator.py +160 -160
- tapps_agents/agents/reviewer/output_enhancer.py +105 -105
- tapps_agents/agents/reviewer/pattern_detector.py +241 -241
- tapps_agents/agents/reviewer/performance_scorer.py +357 -357
- tapps_agents/agents/reviewer/phased_review.py +516 -516
- tapps_agents/agents/reviewer/progressive_review.py +435 -435
- tapps_agents/agents/reviewer/react_scorer.py +331 -331
- tapps_agents/agents/reviewer/score_constants.py +228 -228
- tapps_agents/agents/reviewer/score_validator.py +507 -507
- tapps_agents/agents/reviewer/scorer_registry.py +373 -373
- tapps_agents/agents/reviewer/scoring.py +1566 -1566
- tapps_agents/agents/reviewer/service_discovery.py +534 -534
- tapps_agents/agents/reviewer/tools/__init__.py +41 -41
- tapps_agents/agents/reviewer/tools/parallel_executor.py +581 -581
- tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -250
- tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -284
- tapps_agents/agents/reviewer/typescript_scorer.py +1142 -1142
- tapps_agents/agents/reviewer/validation.py +208 -208
- tapps_agents/agents/reviewer/websocket_validator.py +132 -132
- tapps_agents/agents/tester/__init__.py +7 -7
- tapps_agents/agents/tester/accessibility_auditor.py +309 -309
- tapps_agents/agents/tester/agent.py +1080 -1080
- tapps_agents/agents/tester/batch_generator.py +54 -54
- tapps_agents/agents/tester/context_learner.py +51 -51
- tapps_agents/agents/tester/coverage_analyzer.py +386 -386
- tapps_agents/agents/tester/coverage_test_generator.py +290 -290
- tapps_agents/agents/tester/debug_enhancer.py +238 -238
- tapps_agents/agents/tester/device_emulator.py +241 -241
- tapps_agents/agents/tester/integration_generator.py +62 -62
- tapps_agents/agents/tester/network_recorder.py +300 -300
- tapps_agents/agents/tester/performance_monitor.py +320 -320
- tapps_agents/agents/tester/test_fixer.py +316 -316
- tapps_agents/agents/tester/test_generator.py +632 -632
- tapps_agents/agents/tester/trace_manager.py +234 -234
- tapps_agents/agents/tester/visual_regression.py +291 -291
- tapps_agents/analysis/pattern_detector.py +36 -36
- tapps_agents/beads/hydration.py +213 -213
- tapps_agents/beads/parse.py +32 -32
- tapps_agents/beads/specs.py +206 -206
- tapps_agents/cli/__init__.py +9 -9
- tapps_agents/cli/__main__.py +8 -8
- tapps_agents/cli/base.py +478 -478
- tapps_agents/cli/command_classifier.py +72 -72
- tapps_agents/cli/commands/__init__.py +2 -2
- tapps_agents/cli/commands/analyst.py +173 -173
- tapps_agents/cli/commands/architect.py +109 -109
- tapps_agents/cli/commands/cleanup_agent.py +92 -92
- tapps_agents/cli/commands/common.py +126 -126
- tapps_agents/cli/commands/debugger.py +90 -90
- tapps_agents/cli/commands/designer.py +112 -112
- tapps_agents/cli/commands/documenter.py +136 -136
- tapps_agents/cli/commands/enhancer.py +110 -110
- tapps_agents/cli/commands/evaluator.py +255 -255
- tapps_agents/cli/commands/health.py +665 -665
- tapps_agents/cli/commands/implementer.py +301 -301
- tapps_agents/cli/commands/improver.py +91 -91
- tapps_agents/cli/commands/knowledge.py +111 -111
- tapps_agents/cli/commands/learning.py +172 -172
- tapps_agents/cli/commands/observability.py +283 -283
- tapps_agents/cli/commands/ops.py +135 -135
- tapps_agents/cli/commands/orchestrator.py +116 -116
- tapps_agents/cli/commands/planner.py +237 -237
- tapps_agents/cli/commands/reviewer.py +1872 -1872
- tapps_agents/cli/commands/status.py +285 -285
- tapps_agents/cli/commands/task.py +227 -219
- tapps_agents/cli/commands/tester.py +191 -191
- tapps_agents/cli/commands/top_level.py +3586 -3586
- tapps_agents/cli/feedback.py +936 -936
- tapps_agents/cli/formatters.py +608 -608
- tapps_agents/cli/help/__init__.py +7 -7
- tapps_agents/cli/help/static_help.py +425 -425
- tapps_agents/cli/network_detection.py +110 -110
- tapps_agents/cli/output_compactor.py +274 -274
- tapps_agents/cli/parsers/__init__.py +2 -2
- tapps_agents/cli/parsers/analyst.py +186 -186
- tapps_agents/cli/parsers/architect.py +167 -167
- tapps_agents/cli/parsers/cleanup_agent.py +228 -228
- tapps_agents/cli/parsers/debugger.py +116 -116
- tapps_agents/cli/parsers/designer.py +182 -182
- tapps_agents/cli/parsers/documenter.py +134 -134
- tapps_agents/cli/parsers/enhancer.py +113 -113
- tapps_agents/cli/parsers/evaluator.py +213 -213
- tapps_agents/cli/parsers/implementer.py +168 -168
- tapps_agents/cli/parsers/improver.py +132 -132
- tapps_agents/cli/parsers/ops.py +159 -159
- tapps_agents/cli/parsers/orchestrator.py +98 -98
- tapps_agents/cli/parsers/planner.py +145 -145
- tapps_agents/cli/parsers/reviewer.py +462 -462
- tapps_agents/cli/parsers/tester.py +124 -124
- tapps_agents/cli/progress_heartbeat.py +254 -254
- tapps_agents/cli/streaming_progress.py +336 -336
- tapps_agents/cli/utils/__init__.py +6 -6
- tapps_agents/cli/utils/agent_lifecycle.py +48 -48
- tapps_agents/cli/utils/error_formatter.py +82 -82
- tapps_agents/cli/utils/error_recovery.py +188 -188
- tapps_agents/cli/utils/output_handler.py +59 -59
- tapps_agents/cli/utils/prompt_enhancer.py +319 -319
- tapps_agents/cli/validators/__init__.py +9 -9
- tapps_agents/cli/validators/command_validator.py +81 -81
- tapps_agents/context7/__init__.py +112 -112
- tapps_agents/context7/agent_integration.py +869 -869
- tapps_agents/context7/analytics.py +382 -382
- tapps_agents/context7/analytics_dashboard.py +299 -299
- tapps_agents/context7/async_cache.py +681 -681
- tapps_agents/context7/backup_client.py +958 -958
- tapps_agents/context7/cache_locking.py +194 -194
- tapps_agents/context7/cache_metadata.py +214 -214
- tapps_agents/context7/cache_prewarm.py +488 -488
- tapps_agents/context7/cache_structure.py +168 -168
- tapps_agents/context7/cache_warming.py +604 -604
- tapps_agents/context7/circuit_breaker.py +376 -376
- tapps_agents/context7/cleanup.py +461 -461
- tapps_agents/context7/commands.py +858 -858
- tapps_agents/context7/credential_validation.py +276 -276
- tapps_agents/context7/cross_reference_resolver.py +168 -168
- tapps_agents/context7/cross_references.py +424 -424
- tapps_agents/context7/doc_manager.py +225 -225
- tapps_agents/context7/fuzzy_matcher.py +369 -369
- tapps_agents/context7/kb_cache.py +404 -404
- tapps_agents/context7/language_detector.py +219 -219
- tapps_agents/context7/library_detector.py +725 -725
- tapps_agents/context7/lookup.py +738 -738
- tapps_agents/context7/metadata.py +258 -258
- tapps_agents/context7/refresh_queue.py +300 -300
- tapps_agents/context7/security.py +373 -373
- tapps_agents/context7/staleness_policies.py +278 -278
- tapps_agents/context7/tiles_integration.py +47 -47
- tapps_agents/continuous_bug_fix/__init__.py +20 -20
- tapps_agents/continuous_bug_fix/bug_finder.py +306 -306
- tapps_agents/continuous_bug_fix/bug_fix_coordinator.py +177 -177
- tapps_agents/continuous_bug_fix/commit_manager.py +178 -178
- tapps_agents/continuous_bug_fix/continuous_bug_fixer.py +322 -322
- tapps_agents/continuous_bug_fix/proactive_bug_finder.py +285 -285
- tapps_agents/core/__init__.py +298 -298
- tapps_agents/core/adaptive_cache_config.py +432 -432
- tapps_agents/core/agent_base.py +647 -647
- tapps_agents/core/agent_cache.py +466 -466
- tapps_agents/core/agent_learning.py +1865 -1865
- tapps_agents/core/analytics_dashboard.py +563 -563
- tapps_agents/core/analytics_enhancements.py +597 -597
- tapps_agents/core/anonymization.py +274 -274
- tapps_agents/core/artifact_context_builder.py +293 -0
- tapps_agents/core/ast_parser.py +228 -228
- tapps_agents/core/async_file_ops.py +402 -402
- tapps_agents/core/best_practice_consultant.py +299 -299
- tapps_agents/core/brownfield_analyzer.py +299 -299
- tapps_agents/core/brownfield_review.py +541 -541
- tapps_agents/core/browser_controller.py +513 -513
- tapps_agents/core/capability_registry.py +418 -418
- tapps_agents/core/change_impact_analyzer.py +190 -190
- tapps_agents/core/checkpoint_manager.py +377 -377
- tapps_agents/core/code_generator.py +329 -329
- tapps_agents/core/code_validator.py +276 -276
- tapps_agents/core/command_registry.py +327 -327
- tapps_agents/core/config.py +33 -0
- tapps_agents/core/context_gathering/__init__.py +2 -2
- tapps_agents/core/context_gathering/repository_explorer.py +28 -28
- tapps_agents/core/context_intelligence/__init__.py +2 -2
- tapps_agents/core/context_intelligence/relevance_scorer.py +24 -24
- tapps_agents/core/context_intelligence/token_budget_manager.py +27 -27
- tapps_agents/core/context_manager.py +240 -240
- tapps_agents/core/cursor_feedback_monitor.py +146 -146
- tapps_agents/core/cursor_verification.py +290 -290
- tapps_agents/core/customization_loader.py +280 -280
- tapps_agents/core/customization_schema.py +260 -260
- tapps_agents/core/customization_template.py +238 -238
- tapps_agents/core/debug_logger.py +124 -124
- tapps_agents/core/design_validator.py +298 -298
- tapps_agents/core/diagram_generator.py +226 -226
- tapps_agents/core/docker_utils.py +232 -232
- tapps_agents/core/document_generator.py +617 -617
- tapps_agents/core/domain_detector.py +30 -30
- tapps_agents/core/error_envelope.py +454 -454
- tapps_agents/core/error_handler.py +270 -270
- tapps_agents/core/estimation_tracker.py +189 -189
- tapps_agents/core/eval_prompt_engine.py +116 -116
- tapps_agents/core/evaluation_base.py +119 -119
- tapps_agents/core/evaluation_models.py +320 -320
- tapps_agents/core/evaluation_orchestrator.py +225 -225
- tapps_agents/core/evaluators/__init__.py +7 -7
- tapps_agents/core/evaluators/architectural_evaluator.py +205 -205
- tapps_agents/core/evaluators/behavioral_evaluator.py +160 -160
- tapps_agents/core/evaluators/performance_profile_evaluator.py +160 -160
- tapps_agents/core/evaluators/security_posture_evaluator.py +148 -148
- tapps_agents/core/evaluators/spec_compliance_evaluator.py +181 -181
- tapps_agents/core/exceptions.py +107 -107
- tapps_agents/core/expert_config_generator.py +293 -293
- tapps_agents/core/export_schema.py +202 -202
- tapps_agents/core/external_feedback_models.py +102 -102
- tapps_agents/core/external_feedback_storage.py +213 -213
- tapps_agents/core/fallback_strategy.py +314 -314
- tapps_agents/core/feedback_analyzer.py +162 -162
- tapps_agents/core/feedback_collector.py +178 -178
- tapps_agents/core/git_operations.py +445 -445
- tapps_agents/core/hardware_profiler.py +151 -151
- tapps_agents/core/instructions.py +324 -324
- tapps_agents/core/io_guardrails.py +69 -69
- tapps_agents/core/issue_manifest.py +249 -249
- tapps_agents/core/issue_schema.py +139 -139
- tapps_agents/core/json_utils.py +128 -128
- tapps_agents/core/knowledge_graph.py +446 -446
- tapps_agents/core/language_detector.py +296 -296
- tapps_agents/core/learning_confidence.py +242 -242
- tapps_agents/core/learning_dashboard.py +246 -246
- tapps_agents/core/learning_decision.py +384 -384
- tapps_agents/core/learning_explainability.py +578 -578
- tapps_agents/core/learning_export.py +287 -287
- tapps_agents/core/learning_integration.py +228 -228
- tapps_agents/core/llm_behavior.py +232 -232
- tapps_agents/core/long_duration_support.py +786 -786
- tapps_agents/core/mcp_setup.py +106 -106
- tapps_agents/core/memory_integration.py +396 -396
- tapps_agents/core/meta_learning.py +666 -666
- tapps_agents/core/module_path_sanitizer.py +199 -199
- tapps_agents/core/multi_agent_orchestrator.py +382 -382
- tapps_agents/core/network_errors.py +125 -125
- tapps_agents/core/nfr_validator.py +336 -336
- tapps_agents/core/offline_mode.py +158 -158
- tapps_agents/core/output_contracts.py +300 -300
- tapps_agents/core/output_formatter.py +300 -300
- tapps_agents/core/path_normalizer.py +174 -174
- tapps_agents/core/path_validator.py +322 -322
- tapps_agents/core/pattern_library.py +250 -250
- tapps_agents/core/performance_benchmark.py +301 -301
- tapps_agents/core/performance_monitor.py +184 -184
- tapps_agents/core/playwright_mcp_controller.py +771 -771
- tapps_agents/core/policy_loader.py +135 -135
- tapps_agents/core/progress.py +166 -166
- tapps_agents/core/project_profile.py +354 -354
- tapps_agents/core/project_type_detector.py +454 -454
- tapps_agents/core/prompt_base.py +223 -223
- tapps_agents/core/prompt_learning/__init__.py +2 -2
- tapps_agents/core/prompt_learning/learning_loop.py +24 -24
- tapps_agents/core/prompt_learning/project_prompt_store.py +25 -25
- tapps_agents/core/prompt_learning/skills_prompt_analyzer.py +35 -35
- tapps_agents/core/prompt_optimization/__init__.py +6 -6
- tapps_agents/core/prompt_optimization/ab_tester.py +114 -114
- tapps_agents/core/prompt_optimization/correlation_analyzer.py +160 -160
- tapps_agents/core/prompt_optimization/progressive_refiner.py +129 -129
- tapps_agents/core/prompt_optimization/prompt_library.py +37 -37
- tapps_agents/core/requirements_evaluator.py +431 -431
- tapps_agents/core/resource_aware_executor.py +449 -449
- tapps_agents/core/resource_monitor.py +343 -343
- tapps_agents/core/resume_handler.py +298 -298
- tapps_agents/core/retry_handler.py +197 -197
- tapps_agents/core/review_checklists.py +479 -479
- tapps_agents/core/role_loader.py +201 -201
- tapps_agents/core/role_template_loader.py +201 -201
- tapps_agents/core/runtime_mode.py +60 -60
- tapps_agents/core/security_scanner.py +342 -342
- tapps_agents/core/skill_agent_registry.py +194 -194
- tapps_agents/core/skill_integration.py +208 -208
- tapps_agents/core/skill_loader.py +492 -492
- tapps_agents/core/skill_template.py +341 -341
- tapps_agents/core/skill_validator.py +478 -478
- tapps_agents/core/stack_analyzer.py +35 -35
- tapps_agents/core/startup.py +174 -174
- tapps_agents/core/storage_manager.py +397 -397
- tapps_agents/core/storage_models.py +166 -166
- tapps_agents/core/story_evaluator.py +410 -410
- tapps_agents/core/subprocess_utils.py +170 -170
- tapps_agents/core/task_duration.py +296 -296
- tapps_agents/core/task_memory.py +582 -582
- tapps_agents/core/task_state.py +226 -226
- tapps_agents/core/tech_stack_priorities.py +208 -208
- tapps_agents/core/temp_directory.py +194 -194
- tapps_agents/core/template_merger.py +600 -600
- tapps_agents/core/template_selector.py +280 -280
- tapps_agents/core/test_generator.py +286 -286
- tapps_agents/core/tiered_context.py +253 -253
- tapps_agents/core/token_monitor.py +345 -345
- tapps_agents/core/traceability.py +254 -254
- tapps_agents/core/trajectory_tracker.py +50 -50
- tapps_agents/core/unicode_safe.py +143 -143
- tapps_agents/core/unified_cache_config.py +170 -170
- tapps_agents/core/unified_state.py +324 -324
- tapps_agents/core/validate_cursor_setup.py +237 -237
- tapps_agents/core/validation_registry.py +136 -136
- tapps_agents/core/validators/__init__.py +4 -4
- tapps_agents/core/validators/python_validator.py +87 -87
- tapps_agents/core/verification_agent.py +90 -90
- tapps_agents/core/visual_feedback.py +644 -644
- tapps_agents/core/workflow_validator.py +197 -197
- tapps_agents/core/worktree.py +367 -367
- tapps_agents/docker/__init__.py +10 -10
- tapps_agents/docker/analyzer.py +186 -186
- tapps_agents/docker/debugger.py +229 -229
- tapps_agents/docker/error_patterns.py +216 -216
- tapps_agents/epic/__init__.py +22 -22
- tapps_agents/epic/beads_sync.py +115 -115
- tapps_agents/epic/markdown_sync.py +105 -105
- tapps_agents/epic/models.py +96 -96
- tapps_agents/experts/__init__.py +163 -163
- tapps_agents/experts/agent_integration.py +243 -243
- tapps_agents/experts/auto_generator.py +331 -331
- tapps_agents/experts/base_expert.py +536 -536
- tapps_agents/experts/builtin_registry.py +261 -261
- tapps_agents/experts/business_metrics.py +565 -565
- tapps_agents/experts/cache.py +266 -266
- tapps_agents/experts/confidence_breakdown.py +306 -306
- tapps_agents/experts/confidence_calculator.py +336 -336
- tapps_agents/experts/confidence_metrics.py +236 -236
- tapps_agents/experts/domain_config.py +311 -311
- tapps_agents/experts/domain_detector.py +550 -550
- tapps_agents/experts/domain_utils.py +84 -84
- tapps_agents/experts/expert_config.py +113 -113
- tapps_agents/experts/expert_engine.py +465 -465
- tapps_agents/experts/expert_registry.py +744 -744
- tapps_agents/experts/expert_synthesizer.py +70 -70
- tapps_agents/experts/governance.py +197 -197
- tapps_agents/experts/history_logger.py +312 -312
- tapps_agents/experts/knowledge/README.md +180 -180
- tapps_agents/experts/knowledge/accessibility/accessible-forms.md +331 -331
- tapps_agents/experts/knowledge/accessibility/aria-patterns.md +344 -344
- tapps_agents/experts/knowledge/accessibility/color-contrast.md +285 -285
- tapps_agents/experts/knowledge/accessibility/keyboard-navigation.md +332 -332
- tapps_agents/experts/knowledge/accessibility/screen-readers.md +282 -282
- tapps_agents/experts/knowledge/accessibility/semantic-html.md +355 -355
- tapps_agents/experts/knowledge/accessibility/testing-accessibility.md +369 -369
- tapps_agents/experts/knowledge/accessibility/wcag-2.1.md +296 -296
- tapps_agents/experts/knowledge/accessibility/wcag-2.2.md +211 -211
- tapps_agents/experts/knowledge/agent-learning/best-practices.md +715 -715
- tapps_agents/experts/knowledge/agent-learning/pattern-extraction.md +282 -282
- tapps_agents/experts/knowledge/agent-learning/prompt-optimization.md +320 -320
- tapps_agents/experts/knowledge/ai-frameworks/model-optimization.md +90 -90
- tapps_agents/experts/knowledge/ai-frameworks/openvino-patterns.md +260 -260
- tapps_agents/experts/knowledge/api-design-integration/api-gateway-patterns.md +309 -309
- tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +521 -521
- tapps_agents/experts/knowledge/api-design-integration/api-versioning.md +421 -421
- tapps_agents/experts/knowledge/api-design-integration/async-protocol-patterns.md +61 -61
- tapps_agents/experts/knowledge/api-design-integration/contract-testing.md +221 -221
- tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +489 -489
- tapps_agents/experts/knowledge/api-design-integration/fastapi-patterns.md +360 -360
- tapps_agents/experts/knowledge/api-design-integration/fastapi-testing.md +262 -262
- tapps_agents/experts/knowledge/api-design-integration/graphql-patterns.md +582 -582
- tapps_agents/experts/knowledge/api-design-integration/grpc-best-practices.md +499 -499
- tapps_agents/experts/knowledge/api-design-integration/mqtt-patterns.md +455 -455
- tapps_agents/experts/knowledge/api-design-integration/rate-limiting.md +507 -507
- tapps_agents/experts/knowledge/api-design-integration/restful-api-design.md +618 -618
- tapps_agents/experts/knowledge/api-design-integration/websocket-patterns.md +480 -480
- tapps_agents/experts/knowledge/cloud-infrastructure/cloud-native-patterns.md +175 -175
- tapps_agents/experts/knowledge/cloud-infrastructure/container-health-checks.md +261 -261
- tapps_agents/experts/knowledge/cloud-infrastructure/containerization.md +222 -222
- tapps_agents/experts/knowledge/cloud-infrastructure/cost-optimization.md +122 -122
- tapps_agents/experts/knowledge/cloud-infrastructure/disaster-recovery.md +153 -153
- tapps_agents/experts/knowledge/cloud-infrastructure/dockerfile-patterns.md +285 -285
- tapps_agents/experts/knowledge/cloud-infrastructure/infrastructure-as-code.md +187 -187
- tapps_agents/experts/knowledge/cloud-infrastructure/kubernetes-patterns.md +253 -253
- tapps_agents/experts/knowledge/cloud-infrastructure/multi-cloud-strategies.md +155 -155
- tapps_agents/experts/knowledge/cloud-infrastructure/serverless-architecture.md +200 -200
- tapps_agents/experts/knowledge/code-quality-analysis/README.md +16 -16
- tapps_agents/experts/knowledge/code-quality-analysis/code-metrics.md +137 -137
- tapps_agents/experts/knowledge/code-quality-analysis/complexity-analysis.md +181 -181
- tapps_agents/experts/knowledge/code-quality-analysis/technical-debt-patterns.md +191 -191
- tapps_agents/experts/knowledge/data-privacy-compliance/anonymization.md +313 -313
- tapps_agents/experts/knowledge/data-privacy-compliance/ccpa.md +255 -255
- tapps_agents/experts/knowledge/data-privacy-compliance/consent-management.md +282 -282
- tapps_agents/experts/knowledge/data-privacy-compliance/data-minimization.md +275 -275
- tapps_agents/experts/knowledge/data-privacy-compliance/data-retention.md +297 -297
- tapps_agents/experts/knowledge/data-privacy-compliance/data-subject-rights.md +383 -383
- tapps_agents/experts/knowledge/data-privacy-compliance/encryption-privacy.md +285 -285
- tapps_agents/experts/knowledge/data-privacy-compliance/gdpr.md +344 -344
- tapps_agents/experts/knowledge/data-privacy-compliance/hipaa.md +385 -385
- tapps_agents/experts/knowledge/data-privacy-compliance/privacy-by-design.md +280 -280
- tapps_agents/experts/knowledge/database-data-management/acid-vs-cap.md +164 -164
- tapps_agents/experts/knowledge/database-data-management/backup-and-recovery.md +182 -182
- tapps_agents/experts/knowledge/database-data-management/data-modeling.md +172 -172
- tapps_agents/experts/knowledge/database-data-management/database-design.md +187 -187
- tapps_agents/experts/knowledge/database-data-management/flux-query-optimization.md +342 -342
- tapps_agents/experts/knowledge/database-data-management/influxdb-connection-patterns.md +432 -432
- tapps_agents/experts/knowledge/database-data-management/influxdb-patterns.md +442 -442
- tapps_agents/experts/knowledge/database-data-management/migration-strategies.md +216 -216
- tapps_agents/experts/knowledge/database-data-management/nosql-patterns.md +259 -259
- tapps_agents/experts/knowledge/database-data-management/scalability-patterns.md +184 -184
- tapps_agents/experts/knowledge/database-data-management/sql-optimization.md +175 -175
- tapps_agents/experts/knowledge/database-data-management/time-series-modeling.md +444 -444
- tapps_agents/experts/knowledge/development-workflow/README.md +16 -16
- tapps_agents/experts/knowledge/development-workflow/automation-best-practices.md +216 -216
- tapps_agents/experts/knowledge/development-workflow/build-strategies.md +198 -198
- tapps_agents/experts/knowledge/development-workflow/deployment-patterns.md +205 -205
- tapps_agents/experts/knowledge/development-workflow/git-workflows.md +205 -205
- tapps_agents/experts/knowledge/documentation-knowledge-management/README.md +16 -16
- tapps_agents/experts/knowledge/documentation-knowledge-management/api-documentation-patterns.md +231 -231
- tapps_agents/experts/knowledge/documentation-knowledge-management/documentation-standards.md +191 -191
- tapps_agents/experts/knowledge/documentation-knowledge-management/knowledge-management.md +171 -171
- tapps_agents/experts/knowledge/documentation-knowledge-management/technical-writing-guide.md +192 -192
- tapps_agents/experts/knowledge/observability-monitoring/alerting-patterns.md +461 -461
- tapps_agents/experts/knowledge/observability-monitoring/apm-tools.md +459 -459
- tapps_agents/experts/knowledge/observability-monitoring/distributed-tracing.md +367 -367
- tapps_agents/experts/knowledge/observability-monitoring/logging-strategies.md +478 -478
- tapps_agents/experts/knowledge/observability-monitoring/metrics-and-monitoring.md +510 -510
- tapps_agents/experts/knowledge/observability-monitoring/observability-best-practices.md +492 -492
- tapps_agents/experts/knowledge/observability-monitoring/open-telemetry.md +573 -573
- tapps_agents/experts/knowledge/observability-monitoring/slo-sli-sla.md +419 -419
- tapps_agents/experts/knowledge/performance/anti-patterns.md +284 -284
- tapps_agents/experts/knowledge/performance/api-performance.md +256 -256
- tapps_agents/experts/knowledge/performance/caching.md +327 -327
- tapps_agents/experts/knowledge/performance/database-performance.md +252 -252
- tapps_agents/experts/knowledge/performance/optimization-patterns.md +327 -327
- tapps_agents/experts/knowledge/performance/profiling.md +297 -297
- tapps_agents/experts/knowledge/performance/resource-management.md +293 -293
- tapps_agents/experts/knowledge/performance/scalability.md +306 -306
- tapps_agents/experts/knowledge/security/owasp-top10.md +209 -209
- tapps_agents/experts/knowledge/security/secure-coding-practices.md +207 -207
- tapps_agents/experts/knowledge/security/threat-modeling.md +220 -220
- tapps_agents/experts/knowledge/security/vulnerability-patterns.md +342 -342
- tapps_agents/experts/knowledge/software-architecture/docker-compose-patterns.md +314 -314
- tapps_agents/experts/knowledge/software-architecture/microservices-patterns.md +379 -379
- tapps_agents/experts/knowledge/software-architecture/service-communication.md +316 -316
- tapps_agents/experts/knowledge/testing/best-practices.md +310 -310
- tapps_agents/experts/knowledge/testing/coverage-analysis.md +293 -293
- tapps_agents/experts/knowledge/testing/mocking.md +256 -256
- tapps_agents/experts/knowledge/testing/test-automation.md +276 -276
- tapps_agents/experts/knowledge/testing/test-data.md +271 -271
- tapps_agents/experts/knowledge/testing/test-design-patterns.md +280 -280
- tapps_agents/experts/knowledge/testing/test-maintenance.md +236 -236
- tapps_agents/experts/knowledge/testing/test-strategies.md +311 -311
- tapps_agents/experts/knowledge/user-experience/information-architecture.md +325 -325
- tapps_agents/experts/knowledge/user-experience/interaction-design.md +363 -363
- tapps_agents/experts/knowledge/user-experience/prototyping.md +293 -293
- tapps_agents/experts/knowledge/user-experience/usability-heuristics.md +337 -337
- tapps_agents/experts/knowledge/user-experience/usability-testing.md +311 -311
- tapps_agents/experts/knowledge/user-experience/user-journeys.md +296 -296
- tapps_agents/experts/knowledge/user-experience/user-research.md +373 -373
- tapps_agents/experts/knowledge/user-experience/ux-principles.md +340 -340
- tapps_agents/experts/knowledge_freshness.py +321 -321
- tapps_agents/experts/knowledge_ingestion.py +438 -438
- tapps_agents/experts/knowledge_need_detector.py +93 -93
- tapps_agents/experts/knowledge_validator.py +382 -382
- tapps_agents/experts/observability.py +440 -440
- tapps_agents/experts/passive_notifier.py +238 -238
- tapps_agents/experts/proactive_orchestrator.py +32 -32
- tapps_agents/experts/rag_chunker.py +205 -205
- tapps_agents/experts/rag_embedder.py +152 -152
- tapps_agents/experts/rag_evaluation.py +299 -299
- tapps_agents/experts/rag_index.py +303 -303
- tapps_agents/experts/rag_metrics.py +293 -293
- tapps_agents/experts/rag_safety.py +263 -263
- tapps_agents/experts/report_generator.py +296 -296
- tapps_agents/experts/setup_wizard.py +441 -441
- tapps_agents/experts/simple_rag.py +431 -431
- tapps_agents/experts/vector_rag.py +354 -354
- tapps_agents/experts/weight_distributor.py +304 -304
- tapps_agents/health/__init__.py +24 -24
- tapps_agents/health/base.py +75 -75
- tapps_agents/health/checks/__init__.py +22 -22
- tapps_agents/health/checks/automation.py +127 -127
- tapps_agents/health/checks/context7_cache.py +210 -210
- tapps_agents/health/checks/environment.py +116 -116
- tapps_agents/health/checks/execution.py +170 -170
- tapps_agents/health/checks/knowledge_base.py +187 -187
- tapps_agents/health/checks/outcomes.py +324 -324
- tapps_agents/health/collector.py +280 -280
- tapps_agents/health/dashboard.py +137 -137
- tapps_agents/health/metrics.py +151 -151
- tapps_agents/health/orchestrator.py +271 -271
- tapps_agents/health/registry.py +166 -166
- tapps_agents/hooks/__init__.py +33 -33
- tapps_agents/hooks/config.py +140 -140
- tapps_agents/hooks/events.py +135 -135
- tapps_agents/hooks/executor.py +128 -128
- tapps_agents/hooks/manager.py +143 -143
- tapps_agents/integration/__init__.py +8 -8
- tapps_agents/integration/service_integrator.py +121 -121
- tapps_agents/integrations/__init__.py +10 -10
- tapps_agents/integrations/clawdbot.py +525 -525
- tapps_agents/integrations/memory_bridge.py +356 -356
- tapps_agents/mcp/__init__.py +18 -18
- tapps_agents/mcp/gateway.py +112 -112
- tapps_agents/mcp/servers/__init__.py +13 -13
- tapps_agents/mcp/servers/analysis.py +204 -204
- tapps_agents/mcp/servers/context7.py +198 -198
- tapps_agents/mcp/servers/filesystem.py +218 -218
- tapps_agents/mcp/servers/git.py +201 -201
- tapps_agents/mcp/tool_registry.py +115 -115
- tapps_agents/quality/__init__.py +54 -54
- tapps_agents/quality/coverage_analyzer.py +379 -379
- tapps_agents/quality/enforcement.py +82 -82
- tapps_agents/quality/gates/__init__.py +37 -37
- tapps_agents/quality/gates/approval_gate.py +255 -255
- tapps_agents/quality/gates/base.py +84 -84
- tapps_agents/quality/gates/exceptions.py +43 -43
- tapps_agents/quality/gates/policy_gate.py +195 -195
- tapps_agents/quality/gates/registry.py +239 -239
- tapps_agents/quality/gates/security_gate.py +156 -156
- tapps_agents/quality/quality_gates.py +369 -369
- tapps_agents/quality/secret_scanner.py +335 -335
- tapps_agents/session/__init__.py +19 -19
- tapps_agents/session/manager.py +256 -256
- tapps_agents/simple_mode/__init__.py +66 -66
- tapps_agents/simple_mode/agent_contracts.py +357 -357
- tapps_agents/simple_mode/beads_hooks.py +151 -151
- tapps_agents/simple_mode/code_snippet_handler.py +382 -382
- tapps_agents/simple_mode/documentation_manager.py +395 -395
- tapps_agents/simple_mode/documentation_reader.py +187 -187
- tapps_agents/simple_mode/file_inference.py +292 -292
- tapps_agents/simple_mode/framework_change_detector.py +268 -268
- tapps_agents/simple_mode/intent_parser.py +510 -510
- tapps_agents/simple_mode/learning_progression.py +358 -358
- tapps_agents/simple_mode/nl_handler.py +700 -700
- tapps_agents/simple_mode/onboarding.py +253 -253
- tapps_agents/simple_mode/orchestrators/__init__.py +38 -38
- tapps_agents/simple_mode/orchestrators/base.py +185 -185
- tapps_agents/simple_mode/orchestrators/breakdown_orchestrator.py +49 -49
- tapps_agents/simple_mode/orchestrators/brownfield_orchestrator.py +135 -135
- tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2700 -2667
- tapps_agents/simple_mode/orchestrators/deliverable_checklist.py +349 -349
- tapps_agents/simple_mode/orchestrators/enhance_orchestrator.py +53 -53
- tapps_agents/simple_mode/orchestrators/epic_orchestrator.py +122 -122
- tapps_agents/simple_mode/orchestrators/explore_orchestrator.py +184 -184
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +723 -723
- tapps_agents/simple_mode/orchestrators/plan_analysis_orchestrator.py +206 -206
- tapps_agents/simple_mode/orchestrators/pr_orchestrator.py +237 -237
- tapps_agents/simple_mode/orchestrators/refactor_orchestrator.py +222 -222
- tapps_agents/simple_mode/orchestrators/requirements_tracer.py +262 -262
- tapps_agents/simple_mode/orchestrators/resume_orchestrator.py +210 -210
- tapps_agents/simple_mode/orchestrators/review_orchestrator.py +161 -161
- tapps_agents/simple_mode/orchestrators/test_orchestrator.py +82 -82
- tapps_agents/simple_mode/output_aggregator.py +340 -340
- tapps_agents/simple_mode/result_formatters.py +598 -598
- tapps_agents/simple_mode/step_dependencies.py +382 -382
- tapps_agents/simple_mode/step_results.py +276 -276
- tapps_agents/simple_mode/streaming.py +388 -388
- tapps_agents/simple_mode/variations.py +129 -129
- tapps_agents/simple_mode/visual_feedback.py +238 -238
- tapps_agents/simple_mode/zero_config.py +274 -274
- tapps_agents/suggestions/__init__.py +8 -8
- tapps_agents/suggestions/inline_suggester.py +52 -52
- tapps_agents/templates/__init__.py +8 -8
- tapps_agents/templates/microservice_generator.py +274 -274
- tapps_agents/utils/env_validator.py +291 -291
- tapps_agents/workflow/__init__.py +171 -171
- tapps_agents/workflow/acceptance_verifier.py +132 -132
- tapps_agents/workflow/agent_handlers/__init__.py +41 -41
- tapps_agents/workflow/agent_handlers/analyst_handler.py +75 -75
- tapps_agents/workflow/agent_handlers/architect_handler.py +107 -107
- tapps_agents/workflow/agent_handlers/base.py +84 -84
- tapps_agents/workflow/agent_handlers/debugger_handler.py +100 -100
- tapps_agents/workflow/agent_handlers/designer_handler.py +110 -110
- tapps_agents/workflow/agent_handlers/documenter_handler.py +94 -94
- tapps_agents/workflow/agent_handlers/implementer_handler.py +235 -235
- tapps_agents/workflow/agent_handlers/ops_handler.py +62 -62
- tapps_agents/workflow/agent_handlers/orchestrator_handler.py +43 -43
- tapps_agents/workflow/agent_handlers/planner_handler.py +98 -98
- tapps_agents/workflow/agent_handlers/registry.py +119 -119
- tapps_agents/workflow/agent_handlers/reviewer_handler.py +119 -119
- tapps_agents/workflow/agent_handlers/tester_handler.py +69 -69
- tapps_agents/workflow/analytics_accessor.py +337 -337
- tapps_agents/workflow/analytics_alerts.py +416 -416
- tapps_agents/workflow/analytics_dashboard_cursor.py +281 -281
- tapps_agents/workflow/analytics_dual_write.py +103 -103
- tapps_agents/workflow/analytics_integration.py +119 -119
- tapps_agents/workflow/analytics_query_parser.py +278 -278
- tapps_agents/workflow/analytics_visualizer.py +259 -259
- tapps_agents/workflow/artifact_helper.py +204 -204
- tapps_agents/workflow/audit_logger.py +263 -263
- tapps_agents/workflow/auto_execution_config.py +340 -340
- tapps_agents/workflow/auto_progression.py +586 -586
- tapps_agents/workflow/branch_cleanup.py +349 -349
- tapps_agents/workflow/checkpoint.py +256 -256
- tapps_agents/workflow/checkpoint_manager.py +178 -178
- tapps_agents/workflow/code_artifact.py +179 -179
- tapps_agents/workflow/common_enums.py +96 -96
- tapps_agents/workflow/confirmation_handler.py +130 -130
- tapps_agents/workflow/context_analyzer.py +222 -222
- tapps_agents/workflow/context_artifact.py +230 -230
- tapps_agents/workflow/cursor_chat.py +94 -94
- tapps_agents/workflow/cursor_executor.py +2337 -2196
- tapps_agents/workflow/cursor_skill_helper.py +516 -516
- tapps_agents/workflow/dependency_resolver.py +244 -244
- tapps_agents/workflow/design_artifact.py +156 -156
- tapps_agents/workflow/detector.py +751 -751
- tapps_agents/workflow/direct_execution_fallback.py +301 -301
- tapps_agents/workflow/docs_artifact.py +168 -168
- tapps_agents/workflow/enforcer.py +389 -389
- tapps_agents/workflow/enhancement_artifact.py +142 -142
- tapps_agents/workflow/error_recovery.py +806 -806
- tapps_agents/workflow/event_bus.py +183 -183
- tapps_agents/workflow/event_log.py +612 -612
- tapps_agents/workflow/events.py +63 -63
- tapps_agents/workflow/exceptions.py +43 -43
- tapps_agents/workflow/execution_graph.py +498 -498
- tapps_agents/workflow/execution_plan.py +126 -126
- tapps_agents/workflow/file_utils.py +186 -186
- tapps_agents/workflow/gate_evaluator.py +182 -182
- tapps_agents/workflow/gate_integration.py +200 -200
- tapps_agents/workflow/graph_visualizer.py +130 -130
- tapps_agents/workflow/health_checker.py +206 -206
- tapps_agents/workflow/logging_helper.py +243 -243
- tapps_agents/workflow/manifest.py +582 -582
- tapps_agents/workflow/marker_writer.py +250 -250
- tapps_agents/workflow/message_formatter.py +188 -188
- tapps_agents/workflow/messaging.py +325 -325
- tapps_agents/workflow/metadata_models.py +91 -91
- tapps_agents/workflow/metrics_integration.py +226 -226
- tapps_agents/workflow/migration_utils.py +116 -116
- tapps_agents/workflow/models.py +148 -111
- tapps_agents/workflow/nlp_config.py +198 -198
- tapps_agents/workflow/nlp_error_handler.py +207 -207
- tapps_agents/workflow/nlp_executor.py +163 -163
- tapps_agents/workflow/nlp_parser.py +528 -528
- tapps_agents/workflow/observability_dashboard.py +451 -451
- tapps_agents/workflow/observer.py +170 -170
- tapps_agents/workflow/ops_artifact.py +257 -257
- tapps_agents/workflow/output_passing.py +214 -214
- tapps_agents/workflow/parallel_executor.py +463 -463
- tapps_agents/workflow/planning_artifact.py +179 -179
- tapps_agents/workflow/preset_loader.py +285 -285
- tapps_agents/workflow/preset_recommender.py +270 -270
- tapps_agents/workflow/progress_logger.py +145 -145
- tapps_agents/workflow/progress_manager.py +303 -303
- tapps_agents/workflow/progress_monitor.py +186 -186
- tapps_agents/workflow/progress_updates.py +423 -423
- tapps_agents/workflow/quality_artifact.py +158 -158
- tapps_agents/workflow/quality_loopback.py +101 -101
- tapps_agents/workflow/recommender.py +387 -387
- tapps_agents/workflow/remediation_loop.py +166 -166
- tapps_agents/workflow/result_aggregator.py +300 -300
- tapps_agents/workflow/review_artifact.py +185 -185
- tapps_agents/workflow/schema_validator.py +522 -522
- tapps_agents/workflow/session_handoff.py +178 -178
- tapps_agents/workflow/skill_invoker.py +648 -648
- tapps_agents/workflow/state_manager.py +756 -756
- tapps_agents/workflow/state_persistence_config.py +331 -331
- tapps_agents/workflow/status_monitor.py +449 -449
- tapps_agents/workflow/step_checkpoint.py +314 -314
- tapps_agents/workflow/step_details.py +201 -201
- tapps_agents/workflow/story_models.py +147 -147
- tapps_agents/workflow/streaming.py +416 -416
- tapps_agents/workflow/suggestion_engine.py +552 -552
- tapps_agents/workflow/testing_artifact.py +186 -186
- tapps_agents/workflow/timeline.py +158 -158
- tapps_agents/workflow/token_integration.py +209 -209
- tapps_agents/workflow/validation.py +217 -217
- tapps_agents/workflow/visual_feedback.py +391 -391
- tapps_agents/workflow/workflow_chain.py +95 -95
- tapps_agents/workflow/workflow_summary.py +219 -219
- tapps_agents/workflow/worktree_manager.py +724 -724
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/METADATA +672 -672
- tapps_agents-3.6.0.dist-info/RECORD +758 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/licenses/LICENSE +22 -22
- tapps_agents/health/checks/outcomes.backup_20260204_064058.py +0 -324
- tapps_agents/health/checks/outcomes.backup_20260204_064256.py +0 -324
- tapps_agents/health/checks/outcomes.backup_20260204_064600.py +0 -324
- tapps_agents-3.5.40.dist-info/RECORD +0 -760
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/top_level.txt +0 -0
tapps_agents/context7/lookup.py
CHANGED
|
@@ -1,738 +1,738 @@
|
|
|
1
|
-
"""
|
|
2
|
-
KB-First Lookup - Context7 documentation lookup with KB-first caching.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from collections.abc import Callable
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from datetime import UTC, datetime
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
12
|
-
|
|
13
|
-
from .fuzzy_matcher import FuzzyMatcher
|
|
14
|
-
from .kb_cache import CacheEntry, KBCache
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from ..mcp.gateway import MCPGateway
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class LookupResult:
|
|
25
|
-
"""Result from KB-first lookup."""
|
|
26
|
-
|
|
27
|
-
success: bool
|
|
28
|
-
content: str | None = None
|
|
29
|
-
source: str = "cache" # "cache", "api", "fuzzy_match", "stale_fallback", "stale_fuzzy_fallback"
|
|
30
|
-
library: str | None = None
|
|
31
|
-
topic: str | None = None
|
|
32
|
-
context7_id: str | None = None
|
|
33
|
-
cached_entry: CacheEntry | None = None
|
|
34
|
-
error: str | None = None
|
|
35
|
-
response_time_ms: float = 0.0
|
|
36
|
-
fuzzy_score: float | None = None
|
|
37
|
-
matched_topic: str | None = None
|
|
38
|
-
warning: str | None = None # P1 Improvement: Warning message for stale data
|
|
39
|
-
|
|
40
|
-
def to_dict(self) -> dict[str, Any]:
|
|
41
|
-
"""Convert to dictionary."""
|
|
42
|
-
return {
|
|
43
|
-
"success": self.success,
|
|
44
|
-
"content": self.content,
|
|
45
|
-
"source": self.source,
|
|
46
|
-
"library": self.library,
|
|
47
|
-
"topic": self.topic,
|
|
48
|
-
"context7_id": self.context7_id,
|
|
49
|
-
"error": self.error,
|
|
50
|
-
"response_time_ms": self.response_time_ms,
|
|
51
|
-
"fuzzy_score": self.fuzzy_score,
|
|
52
|
-
"matched_topic": self.matched_topic,
|
|
53
|
-
"warning": self.warning,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class KBLookup:
|
|
58
|
-
"""KB-first lookup workflow for Context7 documentation."""
|
|
59
|
-
|
|
60
|
-
def __init__(
|
|
61
|
-
self,
|
|
62
|
-
kb_cache: KBCache,
|
|
63
|
-
mcp_gateway: MCPGateway | None = None,
|
|
64
|
-
resolve_library_func: Callable[[str], Any] | None = None,
|
|
65
|
-
get_docs_func: Callable[[str, str | None], Any] | None = None,
|
|
66
|
-
fuzzy_threshold: float = 0.7,
|
|
67
|
-
):
|
|
68
|
-
"""
|
|
69
|
-
Initialize KB-first lookup.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
kb_cache: KBCache instance
|
|
73
|
-
mcp_gateway: Optional MCPGateway instance (used if provided)
|
|
74
|
-
resolve_library_func: Optional function to resolve library name to Context7 ID (via MCP)
|
|
75
|
-
get_docs_func: Optional function to get docs from Context7 API (via MCP)
|
|
76
|
-
fuzzy_threshold: Fuzzy matching threshold (0.0-1.0)
|
|
77
|
-
"""
|
|
78
|
-
self.kb_cache = kb_cache
|
|
79
|
-
self.mcp_gateway = mcp_gateway
|
|
80
|
-
self.resolve_library_func = resolve_library_func
|
|
81
|
-
self.get_docs_func = get_docs_func
|
|
82
|
-
self.fuzzy_matcher = FuzzyMatcher(threshold=fuzzy_threshold)
|
|
83
|
-
# Initialize staleness policy manager and refresh queue (lazy-loaded)
|
|
84
|
-
self._staleness_policy_manager = None
|
|
85
|
-
self._refresh_queue = None
|
|
86
|
-
|
|
87
|
-
async def _process_refresh_queue_async(self, max_items: int = 1) -> None:
|
|
88
|
-
"""
|
|
89
|
-
Process refresh queue in background (non-blocking).
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
max_items: Maximum number of items to process
|
|
93
|
-
"""
|
|
94
|
-
try:
|
|
95
|
-
from .refresh_queue import RefreshQueue
|
|
96
|
-
from .staleness_policies import StalenessPolicyManager
|
|
97
|
-
|
|
98
|
-
if self._refresh_queue is None:
|
|
99
|
-
if self._staleness_policy_manager is None:
|
|
100
|
-
self._staleness_policy_manager = StalenessPolicyManager()
|
|
101
|
-
self._refresh_queue = RefreshQueue(
|
|
102
|
-
self.kb_cache.cache_structure.refresh_queue_file,
|
|
103
|
-
self._staleness_policy_manager
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Get highest priority tasks (limit to max_items)
|
|
107
|
-
tasks_processed = 0
|
|
108
|
-
while tasks_processed < max_items:
|
|
109
|
-
task = self._refresh_queue.get_next_task(max_priority=10)
|
|
110
|
-
if not task:
|
|
111
|
-
break
|
|
112
|
-
|
|
113
|
-
# Process task via lookup (which will fetch and cache)
|
|
114
|
-
try:
|
|
115
|
-
result = await self.lookup(
|
|
116
|
-
library=task.library, topic=task.topic, use_fuzzy_match=False
|
|
117
|
-
)
|
|
118
|
-
if result.success:
|
|
119
|
-
# Mark task as completed (removes from queue)
|
|
120
|
-
self._refresh_queue.mark_task_completed(
|
|
121
|
-
task.library, task.topic, error=None
|
|
122
|
-
)
|
|
123
|
-
tasks_processed += 1
|
|
124
|
-
else:
|
|
125
|
-
# Mark task as failed (keeps in queue for retry)
|
|
126
|
-
self._refresh_queue.mark_task_completed(
|
|
127
|
-
task.library, task.topic, error=result.error or "Refresh failed"
|
|
128
|
-
)
|
|
129
|
-
tasks_processed += 1 # Count as processed even if failed
|
|
130
|
-
except Exception as e:
|
|
131
|
-
logger.warning(f"Failed to refresh {task.library}/{task.topic}: {e}")
|
|
132
|
-
# Mark task as failed
|
|
133
|
-
self._refresh_queue.mark_task_completed(
|
|
134
|
-
task.library, task.topic, error=str(e)
|
|
135
|
-
)
|
|
136
|
-
tasks_processed += 1
|
|
137
|
-
except Exception as e:
|
|
138
|
-
logger.warning(f"Background refresh processing failed: {e}")
|
|
139
|
-
|
|
140
|
-
def _try_stale_cache_fallback(
|
|
141
|
-
self,
|
|
142
|
-
library: str,
|
|
143
|
-
topic: str | None,
|
|
144
|
-
start_time: "datetime",
|
|
145
|
-
max_stale_days: int = 30
|
|
146
|
-
) -> LookupResult | None:
|
|
147
|
-
"""
|
|
148
|
-
Try to get stale cached data as a fallback when API fails.
|
|
149
|
-
|
|
150
|
-
P1 Improvement: Graceful Context7 failure handling with cache fallback.
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
library: Library name
|
|
154
|
-
topic: Optional topic name
|
|
155
|
-
start_time: Lookup start time for response time calculation
|
|
156
|
-
max_stale_days: Maximum age in days for stale data (default 30)
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
LookupResult with stale data if available, None otherwise
|
|
160
|
-
"""
|
|
161
|
-
from datetime import UTC
|
|
162
|
-
try:
|
|
163
|
-
# Try exact match first
|
|
164
|
-
cached_entry = self.kb_cache.get(library, topic)
|
|
165
|
-
if cached_entry and cached_entry.content:
|
|
166
|
-
# Check if entry is within acceptable staleness
|
|
167
|
-
if cached_entry.cached_at:
|
|
168
|
-
try:
|
|
169
|
-
cached_time = datetime.fromisoformat(
|
|
170
|
-
cached_entry.cached_at.replace("Z", "+00:00")
|
|
171
|
-
)
|
|
172
|
-
age_days = (datetime.now(UTC) - cached_time).days
|
|
173
|
-
|
|
174
|
-
if age_days <= max_stale_days:
|
|
175
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
176
|
-
return LookupResult(
|
|
177
|
-
success=True,
|
|
178
|
-
content=cached_entry.content,
|
|
179
|
-
source="stale_fallback", # Indicate stale fallback
|
|
180
|
-
library=library,
|
|
181
|
-
topic=topic,
|
|
182
|
-
context7_id=cached_entry.context7_id,
|
|
183
|
-
cached_entry=cached_entry,
|
|
184
|
-
response_time_ms=response_time,
|
|
185
|
-
warning=f"Using stale cached data (age: {age_days} days). API was unavailable.",
|
|
186
|
-
)
|
|
187
|
-
except (ValueError, TypeError):
|
|
188
|
-
# Can't parse date, use data anyway with warning
|
|
189
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
190
|
-
return LookupResult(
|
|
191
|
-
success=True,
|
|
192
|
-
content=cached_entry.content,
|
|
193
|
-
source="stale_fallback",
|
|
194
|
-
library=library,
|
|
195
|
-
topic=topic,
|
|
196
|
-
context7_id=cached_entry.context7_id,
|
|
197
|
-
cached_entry=cached_entry,
|
|
198
|
-
response_time_ms=response_time,
|
|
199
|
-
warning="Using cached data (age unknown). API was unavailable.",
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
# Try fuzzy match for stale data
|
|
203
|
-
if self.fuzzy_matcher:
|
|
204
|
-
from .metadata import MetadataManager
|
|
205
|
-
metadata_manager = MetadataManager(self.kb_cache.cache_structure)
|
|
206
|
-
index = metadata_manager.load_cache_index()
|
|
207
|
-
|
|
208
|
-
available_entries = []
|
|
209
|
-
for lib_name, lib_data in index.libraries.items():
|
|
210
|
-
topics = lib_data.get("topics", {})
|
|
211
|
-
for topic_name in topics.keys():
|
|
212
|
-
available_entries.append((lib_name, topic_name))
|
|
213
|
-
|
|
214
|
-
if available_entries:
|
|
215
|
-
fuzzy_matches = self.fuzzy_matcher.find_matching_entry(
|
|
216
|
-
library_query=library,
|
|
217
|
-
topic_query=topic,
|
|
218
|
-
available_entries=available_entries,
|
|
219
|
-
max_results=1,
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
if fuzzy_matches:
|
|
223
|
-
best_match = fuzzy_matches[0]
|
|
224
|
-
fuzzy_entry = self.kb_cache.get(best_match.library, best_match.topic)
|
|
225
|
-
if fuzzy_entry and fuzzy_entry.content:
|
|
226
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
227
|
-
return LookupResult(
|
|
228
|
-
success=True,
|
|
229
|
-
content=fuzzy_entry.content,
|
|
230
|
-
source="stale_fuzzy_fallback",
|
|
231
|
-
library=best_match.library,
|
|
232
|
-
topic=best_match.topic,
|
|
233
|
-
context7_id=fuzzy_entry.context7_id,
|
|
234
|
-
cached_entry=fuzzy_entry,
|
|
235
|
-
response_time_ms=response_time,
|
|
236
|
-
fuzzy_score=best_match.score,
|
|
237
|
-
matched_topic=best_match.topic,
|
|
238
|
-
warning=f"Using fuzzy-matched stale data for '{best_match.library}/{best_match.topic}'. API was unavailable.",
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
return None
|
|
242
|
-
except Exception as e:
|
|
243
|
-
logger.debug(f"Stale cache fallback failed for {library}/{topic}: {e}")
|
|
244
|
-
return None
|
|
245
|
-
|
|
246
|
-
async def lookup(
|
|
247
|
-
self, library: str, topic: str | None = None, use_fuzzy_match: bool = False
|
|
248
|
-
) -> LookupResult:
|
|
249
|
-
"""
|
|
250
|
-
Perform KB-first lookup for library documentation.
|
|
251
|
-
|
|
252
|
-
Workflow:
|
|
253
|
-
1. Check KB cache (exact match)
|
|
254
|
-
2. If miss and fuzzy enabled: Try fuzzy matching (Phase 2)
|
|
255
|
-
3. If still miss: Resolve library ID (if needed)
|
|
256
|
-
4. If still miss: Fetch from Context7 API
|
|
257
|
-
5. Store in cache
|
|
258
|
-
|
|
259
|
-
Args:
|
|
260
|
-
library: Library name
|
|
261
|
-
topic: Optional topic name
|
|
262
|
-
use_fuzzy_match: Whether to use fuzzy matching (Phase 2 feature)
|
|
263
|
-
|
|
264
|
-
Returns:
|
|
265
|
-
LookupResult with documentation content
|
|
266
|
-
"""
|
|
267
|
-
# #region agent log
|
|
268
|
-
from ..core.debug_logger import write_debug_log
|
|
269
|
-
write_debug_log(
|
|
270
|
-
{
|
|
271
|
-
"sessionId": "debug-session",
|
|
272
|
-
"runId": "run1",
|
|
273
|
-
"hypothesisId": "E",
|
|
274
|
-
"message": "KBLookup.lookup called",
|
|
275
|
-
"data": {"library": library, "topic": topic},
|
|
276
|
-
},
|
|
277
|
-
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
278
|
-
location="context7/lookup.py:lookup:entry",
|
|
279
|
-
)
|
|
280
|
-
# #endregion
|
|
281
|
-
start_time = datetime.now(UTC)
|
|
282
|
-
|
|
283
|
-
# Default topic if not provided
|
|
284
|
-
if topic is None:
|
|
285
|
-
topic = "overview"
|
|
286
|
-
|
|
287
|
-
# Step 1: Check KB cache (exact match)
|
|
288
|
-
# #region agent log
|
|
289
|
-
write_debug_log(
|
|
290
|
-
{
|
|
291
|
-
"sessionId": "debug-session",
|
|
292
|
-
"runId": "run1",
|
|
293
|
-
"hypothesisId": "E",
|
|
294
|
-
"message": "About to check KB cache",
|
|
295
|
-
"data": {"library": library, "topic": topic},
|
|
296
|
-
},
|
|
297
|
-
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
298
|
-
location="context7/lookup.py:lookup:before_cache_check",
|
|
299
|
-
)
|
|
300
|
-
# #endregion
|
|
301
|
-
cached_entry = self.kb_cache.get(library, topic)
|
|
302
|
-
# #region agent log
|
|
303
|
-
write_debug_log(
|
|
304
|
-
{
|
|
305
|
-
"sessionId": "debug-session",
|
|
306
|
-
"runId": "run1",
|
|
307
|
-
"hypothesisId": "E",
|
|
308
|
-
"message": "Cache check completed",
|
|
309
|
-
"data": {"library": library, "cached": cached_entry is not None},
|
|
310
|
-
},
|
|
311
|
-
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
312
|
-
location="context7/lookup.py:lookup:after_cache_check",
|
|
313
|
-
)
|
|
314
|
-
# #endregion
|
|
315
|
-
if cached_entry:
|
|
316
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
317
|
-
# R4: Record cache hit with latency
|
|
318
|
-
from .analytics import Analytics
|
|
319
|
-
from .metadata import MetadataManager
|
|
320
|
-
analytics = Analytics(
|
|
321
|
-
self.kb_cache.cache_structure,
|
|
322
|
-
MetadataManager(self.kb_cache.cache_structure)
|
|
323
|
-
)
|
|
324
|
-
analytics.record_cache_hit(response_time_ms=response_time)
|
|
325
|
-
|
|
326
|
-
# Check if entry is stale
|
|
327
|
-
if cached_entry.cached_at:
|
|
328
|
-
from .staleness_policies import StalenessPolicyManager
|
|
329
|
-
if self._staleness_policy_manager is None:
|
|
330
|
-
self._staleness_policy_manager = StalenessPolicyManager()
|
|
331
|
-
|
|
332
|
-
is_stale = self._staleness_policy_manager.is_entry_stale(
|
|
333
|
-
library, cached_entry.cached_at
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
if is_stale:
|
|
337
|
-
# Queue refresh for background processing (non-blocking)
|
|
338
|
-
from .refresh_queue import RefreshQueue
|
|
339
|
-
if self._refresh_queue is None:
|
|
340
|
-
self._refresh_queue = RefreshQueue(
|
|
341
|
-
self.kb_cache.cache_structure.refresh_queue_file,
|
|
342
|
-
self._staleness_policy_manager
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
self._refresh_queue.add_task(
|
|
346
|
-
library, topic, priority=7, reason="staleness"
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Process refresh queue in background (non-blocking, limit to 1 item)
|
|
350
|
-
# Try to process in background if event loop is available
|
|
351
|
-
import asyncio
|
|
352
|
-
try:
|
|
353
|
-
loop = asyncio.get_running_loop()
|
|
354
|
-
# Event loop is running, create task for background processing
|
|
355
|
-
asyncio.create_task(
|
|
356
|
-
self._process_refresh_queue_async(max_items=1)
|
|
357
|
-
)
|
|
358
|
-
except RuntimeError:
|
|
359
|
-
# No running event loop - queue will be processed on next lookup
|
|
360
|
-
# or via manual refresh command
|
|
361
|
-
logger.debug("No running event loop, refresh queued for later processing")
|
|
362
|
-
|
|
363
|
-
# Return stale entry immediately (non-blocking)
|
|
364
|
-
# Plan 3.2: append cached_at for agent visibility
|
|
365
|
-
content = cached_entry.content or ""
|
|
366
|
-
if cached_entry.cached_at:
|
|
367
|
-
content += f"\n\nContext7 cache for {library}: cached at {cached_entry.cached_at}; consider refreshing if docs changed."
|
|
368
|
-
return LookupResult(
|
|
369
|
-
success=True,
|
|
370
|
-
content=content,
|
|
371
|
-
source="cache_stale", # Indicate stale source
|
|
372
|
-
library=library,
|
|
373
|
-
topic=topic,
|
|
374
|
-
context7_id=cached_entry.context7_id,
|
|
375
|
-
cached_entry=cached_entry,
|
|
376
|
-
response_time_ms=response_time,
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
# Entry is fresh - return it. Plan 3.2: append cached_at for agent visibility.
|
|
380
|
-
content = cached_entry.content or ""
|
|
381
|
-
if cached_entry.cached_at:
|
|
382
|
-
content += f"\n\nContext7 cache for {library}: cached at {cached_entry.cached_at}; consider refreshing if docs changed."
|
|
383
|
-
return LookupResult(
|
|
384
|
-
success=True,
|
|
385
|
-
content=content,
|
|
386
|
-
source="cache",
|
|
387
|
-
library=library,
|
|
388
|
-
topic=topic,
|
|
389
|
-
context7_id=cached_entry.context7_id,
|
|
390
|
-
cached_entry=cached_entry,
|
|
391
|
-
response_time_ms=response_time,
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
# Step 2: Fuzzy matching (Phase 2)
|
|
395
|
-
if use_fuzzy_match:
|
|
396
|
-
# Get available entries from cache index
|
|
397
|
-
from .metadata import MetadataManager
|
|
398
|
-
|
|
399
|
-
metadata_manager = MetadataManager(self.kb_cache.cache_structure)
|
|
400
|
-
index = metadata_manager.load_cache_index()
|
|
401
|
-
|
|
402
|
-
# Build list of available entries
|
|
403
|
-
available_entries = []
|
|
404
|
-
for lib_name, lib_data in index.libraries.items():
|
|
405
|
-
topics = lib_data.get("topics", {})
|
|
406
|
-
for topic_name in topics.keys():
|
|
407
|
-
available_entries.append((lib_name, topic_name))
|
|
408
|
-
|
|
409
|
-
# Try fuzzy matching
|
|
410
|
-
fuzzy_matches = self.fuzzy_matcher.find_matching_entry(
|
|
411
|
-
library_query=library,
|
|
412
|
-
topic_query=topic,
|
|
413
|
-
available_entries=available_entries,
|
|
414
|
-
max_results=1,
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
if fuzzy_matches:
|
|
418
|
-
best_match = fuzzy_matches[0]
|
|
419
|
-
# Try to get the matched entry from cache
|
|
420
|
-
fuzzy_entry = self.kb_cache.get(best_match.library, best_match.topic)
|
|
421
|
-
if fuzzy_entry:
|
|
422
|
-
response_time = (
|
|
423
|
-
datetime.now(UTC) - start_time
|
|
424
|
-
).total_seconds() * 1000
|
|
425
|
-
# R4: Record fuzzy match with latency
|
|
426
|
-
from .analytics import Analytics
|
|
427
|
-
from .metadata import MetadataManager
|
|
428
|
-
analytics = Analytics(
|
|
429
|
-
self.kb_cache.cache_structure,
|
|
430
|
-
MetadataManager(self.kb_cache.cache_structure)
|
|
431
|
-
)
|
|
432
|
-
analytics.record_fuzzy_match()
|
|
433
|
-
analytics.record_cache_hit(response_time_ms=response_time)
|
|
434
|
-
return LookupResult(
|
|
435
|
-
success=True,
|
|
436
|
-
content=fuzzy_entry.content,
|
|
437
|
-
source="fuzzy_match",
|
|
438
|
-
library=best_match.library,
|
|
439
|
-
topic=best_match.topic,
|
|
440
|
-
context7_id=fuzzy_entry.context7_id,
|
|
441
|
-
cached_entry=fuzzy_entry,
|
|
442
|
-
response_time_ms=response_time,
|
|
443
|
-
fuzzy_score=best_match.score,
|
|
444
|
-
matched_topic=best_match.topic,
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
# R4: Record cache miss before API call
|
|
448
|
-
from .analytics import Analytics
|
|
449
|
-
from .metadata import MetadataManager
|
|
450
|
-
analytics = Analytics(
|
|
451
|
-
self.kb_cache.cache_structure,
|
|
452
|
-
MetadataManager(self.kb_cache.cache_structure)
|
|
453
|
-
)
|
|
454
|
-
analytics.record_cache_miss()
|
|
455
|
-
|
|
456
|
-
# Step 3: Resolve library ID if needed
|
|
457
|
-
context7_id = None
|
|
458
|
-
|
|
459
|
-
# Use backup client with automatic fallback (MCP Gateway -> HTTP)
|
|
460
|
-
from .backup_client import call_context7_resolve_with_fallback
|
|
461
|
-
|
|
462
|
-
# #region agent log
|
|
463
|
-
write_debug_log(
|
|
464
|
-
{
|
|
465
|
-
"sessionId": "debug-session",
|
|
466
|
-
"runId": "run1",
|
|
467
|
-
"hypothesisId": "E",
|
|
468
|
-
"message": "About to resolve library ID",
|
|
469
|
-
"data": {"library": library, "topic": topic, "has_mcp_gateway": self.mcp_gateway is not None},
|
|
470
|
-
},
|
|
471
|
-
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
472
|
-
location="context7/lookup.py:lookup:before_resolve",
|
|
473
|
-
)
|
|
474
|
-
# #endregion
|
|
475
|
-
|
|
476
|
-
# CRITICAL FIX: Check quota BEFORE making API calls
|
|
477
|
-
# This prevents unnecessary API calls when quota is already exceeded
|
|
478
|
-
try:
|
|
479
|
-
from .backup_client import is_context7_quota_exceeded, get_context7_quota_message
|
|
480
|
-
if is_context7_quota_exceeded():
|
|
481
|
-
quota_msg = get_context7_quota_message() or "Monthly quota exceeded"
|
|
482
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
483
|
-
logger.debug(
|
|
484
|
-
f"Context7 API quota exceeded for '{library}' (topic: {topic}): {quota_msg}. "
|
|
485
|
-
f"Skipping API call. Consider upgrading your Context7 plan or waiting for quota reset."
|
|
486
|
-
)
|
|
487
|
-
return LookupResult(
|
|
488
|
-
success=False,
|
|
489
|
-
source="api",
|
|
490
|
-
library=library,
|
|
491
|
-
topic=topic,
|
|
492
|
-
error=f"Context7 API quota exceeded: {quota_msg}. Consider upgrading your plan or waiting for quota reset.",
|
|
493
|
-
response_time_ms=response_time,
|
|
494
|
-
)
|
|
495
|
-
except Exception:
|
|
496
|
-
pass # If quota check fails, continue (graceful degradation)
|
|
497
|
-
|
|
498
|
-
try:
|
|
499
|
-
# #region agent log
|
|
500
|
-
write_debug_log(
|
|
501
|
-
{
|
|
502
|
-
"sessionId": "debug-session",
|
|
503
|
-
"runId": "run1",
|
|
504
|
-
"hypothesisId": "E",
|
|
505
|
-
"message": "About to await call_context7_resolve_with_fallback",
|
|
506
|
-
"data": {"library": library},
|
|
507
|
-
},
|
|
508
|
-
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
509
|
-
location="context7/lookup.py:lookup:before_await_resolve",
|
|
510
|
-
)
|
|
511
|
-
# #endregion
|
|
512
|
-
resolve_result = await call_context7_resolve_with_fallback(
|
|
513
|
-
library, self.mcp_gateway
|
|
514
|
-
)
|
|
515
|
-
# #region agent log
|
|
516
|
-
write_debug_log(
|
|
517
|
-
{
|
|
518
|
-
"sessionId": "debug-session",
|
|
519
|
-
"runId": "run1",
|
|
520
|
-
"hypothesisId": "E",
|
|
521
|
-
"message": "call_context7_resolve_with_fallback returned",
|
|
522
|
-
"data": {"library": library, "success": resolve_result.get("success") if isinstance(resolve_result, dict) else None},
|
|
523
|
-
},
|
|
524
|
-
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
525
|
-
location="context7/lookup.py:lookup:after_resolve",
|
|
526
|
-
)
|
|
527
|
-
# #endregion
|
|
528
|
-
if resolve_result.get("success"):
|
|
529
|
-
matches = resolve_result.get("result", {}).get("matches", [])
|
|
530
|
-
if matches and len(matches) > 0:
|
|
531
|
-
# Extract context7_id from first match
|
|
532
|
-
first_match = matches[0]
|
|
533
|
-
if isinstance(first_match, dict):
|
|
534
|
-
context7_id = first_match.get("id")
|
|
535
|
-
# Also try library_id as fallback
|
|
536
|
-
if not context7_id:
|
|
537
|
-
context7_id = first_match.get("library_id")
|
|
538
|
-
else:
|
|
539
|
-
context7_id = str(first_match)
|
|
540
|
-
|
|
541
|
-
# Validate that we actually got an ID
|
|
542
|
-
if not context7_id:
|
|
543
|
-
logger.warning(
|
|
544
|
-
f"Context7 library resolution succeeded for '{library}' (topic: {topic}) "
|
|
545
|
-
f"but no ID found in match result: {first_match}. "
|
|
546
|
-
f"Cannot fetch documentation without library ID."
|
|
547
|
-
)
|
|
548
|
-
else:
|
|
549
|
-
logger.warning(
|
|
550
|
-
f"Context7 library resolution succeeded for '{library}' (topic: {topic}) "
|
|
551
|
-
f"but no matches returned. Cannot fetch documentation without library ID."
|
|
552
|
-
)
|
|
553
|
-
elif "quota exceeded" in resolve_result.get("error", "").lower():
|
|
554
|
-
# R3: Quota exceeded - clear error message with actionable guidance
|
|
555
|
-
error_msg = resolve_result.get("error", "Context7 API quota exceeded")
|
|
556
|
-
# Avoid log spam: once quota is exceeded, subsequent calls are expected to fail.
|
|
557
|
-
try:
|
|
558
|
-
from .backup_client import is_context7_quota_exceeded
|
|
559
|
-
already_exceeded = is_context7_quota_exceeded()
|
|
560
|
-
except Exception:
|
|
561
|
-
already_exceeded = False
|
|
562
|
-
|
|
563
|
-
if already_exceeded:
|
|
564
|
-
logger.debug(
|
|
565
|
-
f"Context7 API quota exceeded for '{library}' (topic: {topic}): {error_msg}. "
|
|
566
|
-
f"Continuing without Context7 documentation."
|
|
567
|
-
)
|
|
568
|
-
else:
|
|
569
|
-
logger.warning(
|
|
570
|
-
f"Context7 API quota exceeded for library '{library}' (topic: {topic}): {error_msg}. "
|
|
571
|
-
f"Consider upgrading your Context7 plan or waiting for quota reset. "
|
|
572
|
-
f"Continuing without Context7 documentation."
|
|
573
|
-
)
|
|
574
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
575
|
-
return LookupResult(
|
|
576
|
-
success=False,
|
|
577
|
-
source="api",
|
|
578
|
-
library=library,
|
|
579
|
-
topic=topic,
|
|
580
|
-
error=f"Context7 API quota exceeded: {error_msg}. Consider upgrading your plan or waiting for quota reset.",
|
|
581
|
-
response_time_ms=response_time,
|
|
582
|
-
)
|
|
583
|
-
else:
|
|
584
|
-
# R3: Context7 unavailable - distinguish between different error types
|
|
585
|
-
error_msg = resolve_result.get("error", "Context7 not available")
|
|
586
|
-
if "not configured" in error_msg.lower() or "not found" in error_msg.lower():
|
|
587
|
-
# Not configured - provide setup instructions
|
|
588
|
-
logger.info(
|
|
589
|
-
f"Context7 not configured for library '{library}' (topic: {topic}): {error_msg}. "
|
|
590
|
-
f"To enable Context7, set CONTEXT7_API_KEY or configure MCP server. "
|
|
591
|
-
f"Continuing without Context7 documentation."
|
|
592
|
-
)
|
|
593
|
-
elif "network" in error_msg.lower() or "connection" in error_msg.lower():
|
|
594
|
-
# Network error
|
|
595
|
-
logger.warning(
|
|
596
|
-
f"Context7 network error for library '{library}' (topic: {topic}): {error_msg}. "
|
|
597
|
-
f"Check your network connection. Continuing without Context7 documentation."
|
|
598
|
-
)
|
|
599
|
-
else:
|
|
600
|
-
# Generic unavailable
|
|
601
|
-
logger.info(
|
|
602
|
-
f"Context7 not available for library '{library}' (topic: {topic}): {error_msg}. "
|
|
603
|
-
f"Continuing without Context7 documentation."
|
|
604
|
-
)
|
|
605
|
-
except Exception as e:
|
|
606
|
-
# Continue without context7_id if resolution fails
|
|
607
|
-
logger.warning(
|
|
608
|
-
f"Failed to resolve Context7 library id for '{library}' (topic: {topic}): {e}. "
|
|
609
|
-
f"Continuing without Context7 documentation.",
|
|
610
|
-
exc_info=True
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
# Step 4: Fetch from Context7 API
|
|
614
|
-
if context7_id:
|
|
615
|
-
content = None
|
|
616
|
-
|
|
617
|
-
# Use backup client with automatic fallback (MCP Gateway -> HTTP)
|
|
618
|
-
from .backup_client import call_context7_get_docs_with_fallback
|
|
619
|
-
|
|
620
|
-
try:
|
|
621
|
-
api_result = await call_context7_get_docs_with_fallback(
|
|
622
|
-
context7_id, topic, mode="code", page=1, mcp_gateway=self.mcp_gateway
|
|
623
|
-
)
|
|
624
|
-
# R4: Record API call
|
|
625
|
-
analytics.record_api_call()
|
|
626
|
-
if api_result.get("success"):
|
|
627
|
-
result_data = api_result.get("result", {})
|
|
628
|
-
content = (
|
|
629
|
-
result_data.get("content")
|
|
630
|
-
if isinstance(result_data, dict)
|
|
631
|
-
else result_data
|
|
632
|
-
)
|
|
633
|
-
else:
|
|
634
|
-
# Context7 unavailable - log and continue
|
|
635
|
-
error_msg = api_result.get("error", "Context7 not available")
|
|
636
|
-
logger.info(
|
|
637
|
-
f"Context7 not available for library '{library}' (topic: {topic}): {error_msg}. "
|
|
638
|
-
f"Continuing without Context7 documentation."
|
|
639
|
-
)
|
|
640
|
-
content = None
|
|
641
|
-
except Exception as e:
|
|
642
|
-
# R4: Still record API call attempt
|
|
643
|
-
analytics.record_api_call()
|
|
644
|
-
logger.warning(
|
|
645
|
-
f"Failed to fetch Context7 docs for library '{library}' (topic: {topic}): {e}. "
|
|
646
|
-
f"Continuing without Context7 documentation.",
|
|
647
|
-
exc_info=True
|
|
648
|
-
)
|
|
649
|
-
content = None
|
|
650
|
-
|
|
651
|
-
# Step 5: Store in cache if we got content
|
|
652
|
-
if content:
|
|
653
|
-
try:
|
|
654
|
-
cached_entry = self.kb_cache.store(
|
|
655
|
-
library=library,
|
|
656
|
-
topic=topic,
|
|
657
|
-
content=content,
|
|
658
|
-
context7_id=context7_id,
|
|
659
|
-
)
|
|
660
|
-
except (RuntimeError, OSError, PermissionError) as e:
|
|
661
|
-
# Cache lock or file write failed - log but continue
|
|
662
|
-
# The content is still available, just not cached
|
|
663
|
-
logger.debug(
|
|
664
|
-
f"Failed to cache Context7 docs for {library}/{topic}: {e}. "
|
|
665
|
-
f"Content retrieved but not cached."
|
|
666
|
-
)
|
|
667
|
-
cached_entry = None
|
|
668
|
-
|
|
669
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
670
|
-
# R4: Record latency for API call (already recorded API call above)
|
|
671
|
-
if response_time > 0:
|
|
672
|
-
analytics.record_cache_hit(response_time_ms=response_time) # Reuse method to record latency
|
|
673
|
-
return LookupResult(
|
|
674
|
-
success=True,
|
|
675
|
-
content=content,
|
|
676
|
-
source="api",
|
|
677
|
-
library=library,
|
|
678
|
-
topic=topic,
|
|
679
|
-
context7_id=context7_id,
|
|
680
|
-
cached_entry=cached_entry,
|
|
681
|
-
response_time_ms=response_time,
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
# If we reach here, lookup failed
|
|
685
|
-
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
686
|
-
|
|
687
|
-
# P1 Improvement: Try stale cache fallback before returning failure
|
|
688
|
-
stale_result = self._try_stale_cache_fallback(library, topic, start_time)
|
|
689
|
-
if stale_result and stale_result.success:
|
|
690
|
-
logger.info(
|
|
691
|
-
f"Context7 API failed for '{library}' (topic: {topic}), "
|
|
692
|
-
f"using stale cached data as fallback."
|
|
693
|
-
)
|
|
694
|
-
return stale_result
|
|
695
|
-
|
|
696
|
-
# Provide more specific error message based on why we failed
|
|
697
|
-
if context7_id is None:
|
|
698
|
-
error_msg = (
|
|
699
|
-
f"Could not resolve library ID for '{library}'. "
|
|
700
|
-
f"This is required to fetch documentation from Context7 API. "
|
|
701
|
-
f"Library resolution may have failed or returned no matches."
|
|
702
|
-
)
|
|
703
|
-
# Use debug level for library not found (common, expected case)
|
|
704
|
-
# Only warn if it's a known popular library that should exist in Context7
|
|
705
|
-
# Valid libraries not in Context7 (like 'openai', 'yaml') will log at debug level to reduce noise
|
|
706
|
-
known_libraries = {
|
|
707
|
-
"react", "vue", "angular", "fastapi", "django", "flask",
|
|
708
|
-
"pytest", "playwright", "typescript", "javascript", "node",
|
|
709
|
-
"express", "nextjs", "svelte", "tailwind", "bootstrap"
|
|
710
|
-
}
|
|
711
|
-
if library.lower() in known_libraries:
|
|
712
|
-
logger.info(
|
|
713
|
-
f"Context7 lookup failed for library '{library}' (topic: {topic}): {error_msg}. "
|
|
714
|
-
f"Continuing without Context7 documentation."
|
|
715
|
-
)
|
|
716
|
-
else:
|
|
717
|
-
logger.debug(
|
|
718
|
-
f"Context7 lookup failed for library '{library}' (topic: {topic}): {error_msg}. "
|
|
719
|
-
f"Library may not be in Context7 database. Continuing without Context7 documentation."
|
|
720
|
-
)
|
|
721
|
-
else:
|
|
722
|
-
error_msg = (
|
|
723
|
-
f"Failed to fetch documentation from Context7 API for '{library}' (ID: {context7_id}). "
|
|
724
|
-
f"API call may have failed or returned no content."
|
|
725
|
-
)
|
|
726
|
-
logger.warning(
|
|
727
|
-
f"Context7 lookup failed for library '{library}' (topic: {topic}): {error_msg}. "
|
|
728
|
-
f"Continuing without Context7 documentation."
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
return LookupResult(
|
|
732
|
-
success=False,
|
|
733
|
-
source="cache" if context7_id is None else "api",
|
|
734
|
-
library=library,
|
|
735
|
-
topic=topic,
|
|
736
|
-
error=error_msg,
|
|
737
|
-
response_time_ms=response_time,
|
|
738
|
-
)
|
|
1
|
+
"""
|
|
2
|
+
KB-First Lookup - Context7 documentation lookup with KB-first caching.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from .fuzzy_matcher import FuzzyMatcher
|
|
14
|
+
from .kb_cache import CacheEntry, KBCache
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..mcp.gateway import MCPGateway
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class LookupResult:
|
|
25
|
+
"""Result from KB-first lookup."""
|
|
26
|
+
|
|
27
|
+
success: bool
|
|
28
|
+
content: str | None = None
|
|
29
|
+
source: str = "cache" # "cache", "api", "fuzzy_match", "stale_fallback", "stale_fuzzy_fallback"
|
|
30
|
+
library: str | None = None
|
|
31
|
+
topic: str | None = None
|
|
32
|
+
context7_id: str | None = None
|
|
33
|
+
cached_entry: CacheEntry | None = None
|
|
34
|
+
error: str | None = None
|
|
35
|
+
response_time_ms: float = 0.0
|
|
36
|
+
fuzzy_score: float | None = None
|
|
37
|
+
matched_topic: str | None = None
|
|
38
|
+
warning: str | None = None # P1 Improvement: Warning message for stale data
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, Any]:
|
|
41
|
+
"""Convert to dictionary."""
|
|
42
|
+
return {
|
|
43
|
+
"success": self.success,
|
|
44
|
+
"content": self.content,
|
|
45
|
+
"source": self.source,
|
|
46
|
+
"library": self.library,
|
|
47
|
+
"topic": self.topic,
|
|
48
|
+
"context7_id": self.context7_id,
|
|
49
|
+
"error": self.error,
|
|
50
|
+
"response_time_ms": self.response_time_ms,
|
|
51
|
+
"fuzzy_score": self.fuzzy_score,
|
|
52
|
+
"matched_topic": self.matched_topic,
|
|
53
|
+
"warning": self.warning,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class KBLookup:
|
|
58
|
+
"""KB-first lookup workflow for Context7 documentation."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
kb_cache: KBCache,
|
|
63
|
+
mcp_gateway: MCPGateway | None = None,
|
|
64
|
+
resolve_library_func: Callable[[str], Any] | None = None,
|
|
65
|
+
get_docs_func: Callable[[str, str | None], Any] | None = None,
|
|
66
|
+
fuzzy_threshold: float = 0.7,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize KB-first lookup.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
kb_cache: KBCache instance
|
|
73
|
+
mcp_gateway: Optional MCPGateway instance (used if provided)
|
|
74
|
+
resolve_library_func: Optional function to resolve library name to Context7 ID (via MCP)
|
|
75
|
+
get_docs_func: Optional function to get docs from Context7 API (via MCP)
|
|
76
|
+
fuzzy_threshold: Fuzzy matching threshold (0.0-1.0)
|
|
77
|
+
"""
|
|
78
|
+
self.kb_cache = kb_cache
|
|
79
|
+
self.mcp_gateway = mcp_gateway
|
|
80
|
+
self.resolve_library_func = resolve_library_func
|
|
81
|
+
self.get_docs_func = get_docs_func
|
|
82
|
+
self.fuzzy_matcher = FuzzyMatcher(threshold=fuzzy_threshold)
|
|
83
|
+
# Initialize staleness policy manager and refresh queue (lazy-loaded)
|
|
84
|
+
self._staleness_policy_manager = None
|
|
85
|
+
self._refresh_queue = None
|
|
86
|
+
|
|
87
|
+
async def _process_refresh_queue_async(self, max_items: int = 1) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Process refresh queue in background (non-blocking).
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
max_items: Maximum number of items to process
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
from .refresh_queue import RefreshQueue
|
|
96
|
+
from .staleness_policies import StalenessPolicyManager
|
|
97
|
+
|
|
98
|
+
if self._refresh_queue is None:
|
|
99
|
+
if self._staleness_policy_manager is None:
|
|
100
|
+
self._staleness_policy_manager = StalenessPolicyManager()
|
|
101
|
+
self._refresh_queue = RefreshQueue(
|
|
102
|
+
self.kb_cache.cache_structure.refresh_queue_file,
|
|
103
|
+
self._staleness_policy_manager
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Get highest priority tasks (limit to max_items)
|
|
107
|
+
tasks_processed = 0
|
|
108
|
+
while tasks_processed < max_items:
|
|
109
|
+
task = self._refresh_queue.get_next_task(max_priority=10)
|
|
110
|
+
if not task:
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
# Process task via lookup (which will fetch and cache)
|
|
114
|
+
try:
|
|
115
|
+
result = await self.lookup(
|
|
116
|
+
library=task.library, topic=task.topic, use_fuzzy_match=False
|
|
117
|
+
)
|
|
118
|
+
if result.success:
|
|
119
|
+
# Mark task as completed (removes from queue)
|
|
120
|
+
self._refresh_queue.mark_task_completed(
|
|
121
|
+
task.library, task.topic, error=None
|
|
122
|
+
)
|
|
123
|
+
tasks_processed += 1
|
|
124
|
+
else:
|
|
125
|
+
# Mark task as failed (keeps in queue for retry)
|
|
126
|
+
self._refresh_queue.mark_task_completed(
|
|
127
|
+
task.library, task.topic, error=result.error or "Refresh failed"
|
|
128
|
+
)
|
|
129
|
+
tasks_processed += 1 # Count as processed even if failed
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.warning(f"Failed to refresh {task.library}/{task.topic}: {e}")
|
|
132
|
+
# Mark task as failed
|
|
133
|
+
self._refresh_queue.mark_task_completed(
|
|
134
|
+
task.library, task.topic, error=str(e)
|
|
135
|
+
)
|
|
136
|
+
tasks_processed += 1
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.warning(f"Background refresh processing failed: {e}")
|
|
139
|
+
|
|
140
|
+
def _try_stale_cache_fallback(
|
|
141
|
+
self,
|
|
142
|
+
library: str,
|
|
143
|
+
topic: str | None,
|
|
144
|
+
start_time: "datetime",
|
|
145
|
+
max_stale_days: int = 30
|
|
146
|
+
) -> LookupResult | None:
|
|
147
|
+
"""
|
|
148
|
+
Try to get stale cached data as a fallback when API fails.
|
|
149
|
+
|
|
150
|
+
P1 Improvement: Graceful Context7 failure handling with cache fallback.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
library: Library name
|
|
154
|
+
topic: Optional topic name
|
|
155
|
+
start_time: Lookup start time for response time calculation
|
|
156
|
+
max_stale_days: Maximum age in days for stale data (default 30)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
LookupResult with stale data if available, None otherwise
|
|
160
|
+
"""
|
|
161
|
+
from datetime import UTC
|
|
162
|
+
try:
|
|
163
|
+
# Try exact match first
|
|
164
|
+
cached_entry = self.kb_cache.get(library, topic)
|
|
165
|
+
if cached_entry and cached_entry.content:
|
|
166
|
+
# Check if entry is within acceptable staleness
|
|
167
|
+
if cached_entry.cached_at:
|
|
168
|
+
try:
|
|
169
|
+
cached_time = datetime.fromisoformat(
|
|
170
|
+
cached_entry.cached_at.replace("Z", "+00:00")
|
|
171
|
+
)
|
|
172
|
+
age_days = (datetime.now(UTC) - cached_time).days
|
|
173
|
+
|
|
174
|
+
if age_days <= max_stale_days:
|
|
175
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
176
|
+
return LookupResult(
|
|
177
|
+
success=True,
|
|
178
|
+
content=cached_entry.content,
|
|
179
|
+
source="stale_fallback", # Indicate stale fallback
|
|
180
|
+
library=library,
|
|
181
|
+
topic=topic,
|
|
182
|
+
context7_id=cached_entry.context7_id,
|
|
183
|
+
cached_entry=cached_entry,
|
|
184
|
+
response_time_ms=response_time,
|
|
185
|
+
warning=f"Using stale cached data (age: {age_days} days). API was unavailable.",
|
|
186
|
+
)
|
|
187
|
+
except (ValueError, TypeError):
|
|
188
|
+
# Can't parse date, use data anyway with warning
|
|
189
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
190
|
+
return LookupResult(
|
|
191
|
+
success=True,
|
|
192
|
+
content=cached_entry.content,
|
|
193
|
+
source="stale_fallback",
|
|
194
|
+
library=library,
|
|
195
|
+
topic=topic,
|
|
196
|
+
context7_id=cached_entry.context7_id,
|
|
197
|
+
cached_entry=cached_entry,
|
|
198
|
+
response_time_ms=response_time,
|
|
199
|
+
warning="Using cached data (age unknown). API was unavailable.",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Try fuzzy match for stale data
|
|
203
|
+
if self.fuzzy_matcher:
|
|
204
|
+
from .metadata import MetadataManager
|
|
205
|
+
metadata_manager = MetadataManager(self.kb_cache.cache_structure)
|
|
206
|
+
index = metadata_manager.load_cache_index()
|
|
207
|
+
|
|
208
|
+
available_entries = []
|
|
209
|
+
for lib_name, lib_data in index.libraries.items():
|
|
210
|
+
topics = lib_data.get("topics", {})
|
|
211
|
+
for topic_name in topics.keys():
|
|
212
|
+
available_entries.append((lib_name, topic_name))
|
|
213
|
+
|
|
214
|
+
if available_entries:
|
|
215
|
+
fuzzy_matches = self.fuzzy_matcher.find_matching_entry(
|
|
216
|
+
library_query=library,
|
|
217
|
+
topic_query=topic,
|
|
218
|
+
available_entries=available_entries,
|
|
219
|
+
max_results=1,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if fuzzy_matches:
|
|
223
|
+
best_match = fuzzy_matches[0]
|
|
224
|
+
fuzzy_entry = self.kb_cache.get(best_match.library, best_match.topic)
|
|
225
|
+
if fuzzy_entry and fuzzy_entry.content:
|
|
226
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
227
|
+
return LookupResult(
|
|
228
|
+
success=True,
|
|
229
|
+
content=fuzzy_entry.content,
|
|
230
|
+
source="stale_fuzzy_fallback",
|
|
231
|
+
library=best_match.library,
|
|
232
|
+
topic=best_match.topic,
|
|
233
|
+
context7_id=fuzzy_entry.context7_id,
|
|
234
|
+
cached_entry=fuzzy_entry,
|
|
235
|
+
response_time_ms=response_time,
|
|
236
|
+
fuzzy_score=best_match.score,
|
|
237
|
+
matched_topic=best_match.topic,
|
|
238
|
+
warning=f"Using fuzzy-matched stale data for '{best_match.library}/{best_match.topic}'. API was unavailable.",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return None
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.debug(f"Stale cache fallback failed for {library}/{topic}: {e}")
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
async def lookup(
|
|
247
|
+
self, library: str, topic: str | None = None, use_fuzzy_match: bool = False
|
|
248
|
+
) -> LookupResult:
|
|
249
|
+
"""
|
|
250
|
+
Perform KB-first lookup for library documentation.
|
|
251
|
+
|
|
252
|
+
Workflow:
|
|
253
|
+
1. Check KB cache (exact match)
|
|
254
|
+
2. If miss and fuzzy enabled: Try fuzzy matching (Phase 2)
|
|
255
|
+
3. If still miss: Resolve library ID (if needed)
|
|
256
|
+
4. If still miss: Fetch from Context7 API
|
|
257
|
+
5. Store in cache
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
library: Library name
|
|
261
|
+
topic: Optional topic name
|
|
262
|
+
use_fuzzy_match: Whether to use fuzzy matching (Phase 2 feature)
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
LookupResult with documentation content
|
|
266
|
+
"""
|
|
267
|
+
# #region agent log
|
|
268
|
+
from ..core.debug_logger import write_debug_log
|
|
269
|
+
write_debug_log(
|
|
270
|
+
{
|
|
271
|
+
"sessionId": "debug-session",
|
|
272
|
+
"runId": "run1",
|
|
273
|
+
"hypothesisId": "E",
|
|
274
|
+
"message": "KBLookup.lookup called",
|
|
275
|
+
"data": {"library": library, "topic": topic},
|
|
276
|
+
},
|
|
277
|
+
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
278
|
+
location="context7/lookup.py:lookup:entry",
|
|
279
|
+
)
|
|
280
|
+
# #endregion
|
|
281
|
+
start_time = datetime.now(UTC)
|
|
282
|
+
|
|
283
|
+
# Default topic if not provided
|
|
284
|
+
if topic is None:
|
|
285
|
+
topic = "overview"
|
|
286
|
+
|
|
287
|
+
# Step 1: Check KB cache (exact match)
|
|
288
|
+
# #region agent log
|
|
289
|
+
write_debug_log(
|
|
290
|
+
{
|
|
291
|
+
"sessionId": "debug-session",
|
|
292
|
+
"runId": "run1",
|
|
293
|
+
"hypothesisId": "E",
|
|
294
|
+
"message": "About to check KB cache",
|
|
295
|
+
"data": {"library": library, "topic": topic},
|
|
296
|
+
},
|
|
297
|
+
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
298
|
+
location="context7/lookup.py:lookup:before_cache_check",
|
|
299
|
+
)
|
|
300
|
+
# #endregion
|
|
301
|
+
cached_entry = self.kb_cache.get(library, topic)
|
|
302
|
+
# #region agent log
|
|
303
|
+
write_debug_log(
|
|
304
|
+
{
|
|
305
|
+
"sessionId": "debug-session",
|
|
306
|
+
"runId": "run1",
|
|
307
|
+
"hypothesisId": "E",
|
|
308
|
+
"message": "Cache check completed",
|
|
309
|
+
"data": {"library": library, "cached": cached_entry is not None},
|
|
310
|
+
},
|
|
311
|
+
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
312
|
+
location="context7/lookup.py:lookup:after_cache_check",
|
|
313
|
+
)
|
|
314
|
+
# #endregion
|
|
315
|
+
if cached_entry:
|
|
316
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
317
|
+
# R4: Record cache hit with latency
|
|
318
|
+
from .analytics import Analytics
|
|
319
|
+
from .metadata import MetadataManager
|
|
320
|
+
analytics = Analytics(
|
|
321
|
+
self.kb_cache.cache_structure,
|
|
322
|
+
MetadataManager(self.kb_cache.cache_structure)
|
|
323
|
+
)
|
|
324
|
+
analytics.record_cache_hit(response_time_ms=response_time)
|
|
325
|
+
|
|
326
|
+
# Check if entry is stale
|
|
327
|
+
if cached_entry.cached_at:
|
|
328
|
+
from .staleness_policies import StalenessPolicyManager
|
|
329
|
+
if self._staleness_policy_manager is None:
|
|
330
|
+
self._staleness_policy_manager = StalenessPolicyManager()
|
|
331
|
+
|
|
332
|
+
is_stale = self._staleness_policy_manager.is_entry_stale(
|
|
333
|
+
library, cached_entry.cached_at
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if is_stale:
|
|
337
|
+
# Queue refresh for background processing (non-blocking)
|
|
338
|
+
from .refresh_queue import RefreshQueue
|
|
339
|
+
if self._refresh_queue is None:
|
|
340
|
+
self._refresh_queue = RefreshQueue(
|
|
341
|
+
self.kb_cache.cache_structure.refresh_queue_file,
|
|
342
|
+
self._staleness_policy_manager
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
self._refresh_queue.add_task(
|
|
346
|
+
library, topic, priority=7, reason="staleness"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Process refresh queue in background (non-blocking, limit to 1 item)
|
|
350
|
+
# Try to process in background if event loop is available
|
|
351
|
+
import asyncio
|
|
352
|
+
try:
|
|
353
|
+
loop = asyncio.get_running_loop()
|
|
354
|
+
# Event loop is running, create task for background processing
|
|
355
|
+
asyncio.create_task(
|
|
356
|
+
self._process_refresh_queue_async(max_items=1)
|
|
357
|
+
)
|
|
358
|
+
except RuntimeError:
|
|
359
|
+
# No running event loop - queue will be processed on next lookup
|
|
360
|
+
# or via manual refresh command
|
|
361
|
+
logger.debug("No running event loop, refresh queued for later processing")
|
|
362
|
+
|
|
363
|
+
# Return stale entry immediately (non-blocking)
|
|
364
|
+
# Plan 3.2: append cached_at for agent visibility
|
|
365
|
+
content = cached_entry.content or ""
|
|
366
|
+
if cached_entry.cached_at:
|
|
367
|
+
content += f"\n\nContext7 cache for {library}: cached at {cached_entry.cached_at}; consider refreshing if docs changed."
|
|
368
|
+
return LookupResult(
|
|
369
|
+
success=True,
|
|
370
|
+
content=content,
|
|
371
|
+
source="cache_stale", # Indicate stale source
|
|
372
|
+
library=library,
|
|
373
|
+
topic=topic,
|
|
374
|
+
context7_id=cached_entry.context7_id,
|
|
375
|
+
cached_entry=cached_entry,
|
|
376
|
+
response_time_ms=response_time,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Entry is fresh - return it. Plan 3.2: append cached_at for agent visibility.
|
|
380
|
+
content = cached_entry.content or ""
|
|
381
|
+
if cached_entry.cached_at:
|
|
382
|
+
content += f"\n\nContext7 cache for {library}: cached at {cached_entry.cached_at}; consider refreshing if docs changed."
|
|
383
|
+
return LookupResult(
|
|
384
|
+
success=True,
|
|
385
|
+
content=content,
|
|
386
|
+
source="cache",
|
|
387
|
+
library=library,
|
|
388
|
+
topic=topic,
|
|
389
|
+
context7_id=cached_entry.context7_id,
|
|
390
|
+
cached_entry=cached_entry,
|
|
391
|
+
response_time_ms=response_time,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Step 2: Fuzzy matching (Phase 2)
|
|
395
|
+
if use_fuzzy_match:
|
|
396
|
+
# Get available entries from cache index
|
|
397
|
+
from .metadata import MetadataManager
|
|
398
|
+
|
|
399
|
+
metadata_manager = MetadataManager(self.kb_cache.cache_structure)
|
|
400
|
+
index = metadata_manager.load_cache_index()
|
|
401
|
+
|
|
402
|
+
# Build list of available entries
|
|
403
|
+
available_entries = []
|
|
404
|
+
for lib_name, lib_data in index.libraries.items():
|
|
405
|
+
topics = lib_data.get("topics", {})
|
|
406
|
+
for topic_name in topics.keys():
|
|
407
|
+
available_entries.append((lib_name, topic_name))
|
|
408
|
+
|
|
409
|
+
# Try fuzzy matching
|
|
410
|
+
fuzzy_matches = self.fuzzy_matcher.find_matching_entry(
|
|
411
|
+
library_query=library,
|
|
412
|
+
topic_query=topic,
|
|
413
|
+
available_entries=available_entries,
|
|
414
|
+
max_results=1,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if fuzzy_matches:
|
|
418
|
+
best_match = fuzzy_matches[0]
|
|
419
|
+
# Try to get the matched entry from cache
|
|
420
|
+
fuzzy_entry = self.kb_cache.get(best_match.library, best_match.topic)
|
|
421
|
+
if fuzzy_entry:
|
|
422
|
+
response_time = (
|
|
423
|
+
datetime.now(UTC) - start_time
|
|
424
|
+
).total_seconds() * 1000
|
|
425
|
+
# R4: Record fuzzy match with latency
|
|
426
|
+
from .analytics import Analytics
|
|
427
|
+
from .metadata import MetadataManager
|
|
428
|
+
analytics = Analytics(
|
|
429
|
+
self.kb_cache.cache_structure,
|
|
430
|
+
MetadataManager(self.kb_cache.cache_structure)
|
|
431
|
+
)
|
|
432
|
+
analytics.record_fuzzy_match()
|
|
433
|
+
analytics.record_cache_hit(response_time_ms=response_time)
|
|
434
|
+
return LookupResult(
|
|
435
|
+
success=True,
|
|
436
|
+
content=fuzzy_entry.content,
|
|
437
|
+
source="fuzzy_match",
|
|
438
|
+
library=best_match.library,
|
|
439
|
+
topic=best_match.topic,
|
|
440
|
+
context7_id=fuzzy_entry.context7_id,
|
|
441
|
+
cached_entry=fuzzy_entry,
|
|
442
|
+
response_time_ms=response_time,
|
|
443
|
+
fuzzy_score=best_match.score,
|
|
444
|
+
matched_topic=best_match.topic,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# R4: Record cache miss before API call
|
|
448
|
+
from .analytics import Analytics
|
|
449
|
+
from .metadata import MetadataManager
|
|
450
|
+
analytics = Analytics(
|
|
451
|
+
self.kb_cache.cache_structure,
|
|
452
|
+
MetadataManager(self.kb_cache.cache_structure)
|
|
453
|
+
)
|
|
454
|
+
analytics.record_cache_miss()
|
|
455
|
+
|
|
456
|
+
# Step 3: Resolve library ID if needed
|
|
457
|
+
context7_id = None
|
|
458
|
+
|
|
459
|
+
# Use backup client with automatic fallback (MCP Gateway -> HTTP)
|
|
460
|
+
from .backup_client import call_context7_resolve_with_fallback
|
|
461
|
+
|
|
462
|
+
# #region agent log
|
|
463
|
+
write_debug_log(
|
|
464
|
+
{
|
|
465
|
+
"sessionId": "debug-session",
|
|
466
|
+
"runId": "run1",
|
|
467
|
+
"hypothesisId": "E",
|
|
468
|
+
"message": "About to resolve library ID",
|
|
469
|
+
"data": {"library": library, "topic": topic, "has_mcp_gateway": self.mcp_gateway is not None},
|
|
470
|
+
},
|
|
471
|
+
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
472
|
+
location="context7/lookup.py:lookup:before_resolve",
|
|
473
|
+
)
|
|
474
|
+
# #endregion
|
|
475
|
+
|
|
476
|
+
# CRITICAL FIX: Check quota BEFORE making API calls
|
|
477
|
+
# This prevents unnecessary API calls when quota is already exceeded
|
|
478
|
+
try:
|
|
479
|
+
from .backup_client import is_context7_quota_exceeded, get_context7_quota_message
|
|
480
|
+
if is_context7_quota_exceeded():
|
|
481
|
+
quota_msg = get_context7_quota_message() or "Monthly quota exceeded"
|
|
482
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
483
|
+
logger.debug(
|
|
484
|
+
f"Context7 API quota exceeded for '{library}' (topic: {topic}): {quota_msg}. "
|
|
485
|
+
f"Skipping API call. Consider upgrading your Context7 plan or waiting for quota reset."
|
|
486
|
+
)
|
|
487
|
+
return LookupResult(
|
|
488
|
+
success=False,
|
|
489
|
+
source="api",
|
|
490
|
+
library=library,
|
|
491
|
+
topic=topic,
|
|
492
|
+
error=f"Context7 API quota exceeded: {quota_msg}. Consider upgrading your plan or waiting for quota reset.",
|
|
493
|
+
response_time_ms=response_time,
|
|
494
|
+
)
|
|
495
|
+
except Exception:
|
|
496
|
+
pass # If quota check fails, continue (graceful degradation)
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
# #region agent log
|
|
500
|
+
write_debug_log(
|
|
501
|
+
{
|
|
502
|
+
"sessionId": "debug-session",
|
|
503
|
+
"runId": "run1",
|
|
504
|
+
"hypothesisId": "E",
|
|
505
|
+
"message": "About to await call_context7_resolve_with_fallback",
|
|
506
|
+
"data": {"library": library},
|
|
507
|
+
},
|
|
508
|
+
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
509
|
+
location="context7/lookup.py:lookup:before_await_resolve",
|
|
510
|
+
)
|
|
511
|
+
# #endregion
|
|
512
|
+
resolve_result = await call_context7_resolve_with_fallback(
|
|
513
|
+
library, self.mcp_gateway
|
|
514
|
+
)
|
|
515
|
+
# #region agent log
|
|
516
|
+
write_debug_log(
|
|
517
|
+
{
|
|
518
|
+
"sessionId": "debug-session",
|
|
519
|
+
"runId": "run1",
|
|
520
|
+
"hypothesisId": "E",
|
|
521
|
+
"message": "call_context7_resolve_with_fallback returned",
|
|
522
|
+
"data": {"library": library, "success": resolve_result.get("success") if isinstance(resolve_result, dict) else None},
|
|
523
|
+
},
|
|
524
|
+
project_root=self.project_root if hasattr(self, 'project_root') else None,
|
|
525
|
+
location="context7/lookup.py:lookup:after_resolve",
|
|
526
|
+
)
|
|
527
|
+
# #endregion
|
|
528
|
+
if resolve_result.get("success"):
|
|
529
|
+
matches = resolve_result.get("result", {}).get("matches", [])
|
|
530
|
+
if matches and len(matches) > 0:
|
|
531
|
+
# Extract context7_id from first match
|
|
532
|
+
first_match = matches[0]
|
|
533
|
+
if isinstance(first_match, dict):
|
|
534
|
+
context7_id = first_match.get("id")
|
|
535
|
+
# Also try library_id as fallback
|
|
536
|
+
if not context7_id:
|
|
537
|
+
context7_id = first_match.get("library_id")
|
|
538
|
+
else:
|
|
539
|
+
context7_id = str(first_match)
|
|
540
|
+
|
|
541
|
+
# Validate that we actually got an ID
|
|
542
|
+
if not context7_id:
|
|
543
|
+
logger.warning(
|
|
544
|
+
f"Context7 library resolution succeeded for '{library}' (topic: {topic}) "
|
|
545
|
+
f"but no ID found in match result: {first_match}. "
|
|
546
|
+
f"Cannot fetch documentation without library ID."
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
logger.warning(
|
|
550
|
+
f"Context7 library resolution succeeded for '{library}' (topic: {topic}) "
|
|
551
|
+
f"but no matches returned. Cannot fetch documentation without library ID."
|
|
552
|
+
)
|
|
553
|
+
elif "quota exceeded" in resolve_result.get("error", "").lower():
|
|
554
|
+
# R3: Quota exceeded - clear error message with actionable guidance
|
|
555
|
+
error_msg = resolve_result.get("error", "Context7 API quota exceeded")
|
|
556
|
+
# Avoid log spam: once quota is exceeded, subsequent calls are expected to fail.
|
|
557
|
+
try:
|
|
558
|
+
from .backup_client import is_context7_quota_exceeded
|
|
559
|
+
already_exceeded = is_context7_quota_exceeded()
|
|
560
|
+
except Exception:
|
|
561
|
+
already_exceeded = False
|
|
562
|
+
|
|
563
|
+
if already_exceeded:
|
|
564
|
+
logger.debug(
|
|
565
|
+
f"Context7 API quota exceeded for '{library}' (topic: {topic}): {error_msg}. "
|
|
566
|
+
f"Continuing without Context7 documentation."
|
|
567
|
+
)
|
|
568
|
+
else:
|
|
569
|
+
logger.warning(
|
|
570
|
+
f"Context7 API quota exceeded for library '{library}' (topic: {topic}): {error_msg}. "
|
|
571
|
+
f"Consider upgrading your Context7 plan or waiting for quota reset. "
|
|
572
|
+
f"Continuing without Context7 documentation."
|
|
573
|
+
)
|
|
574
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
575
|
+
return LookupResult(
|
|
576
|
+
success=False,
|
|
577
|
+
source="api",
|
|
578
|
+
library=library,
|
|
579
|
+
topic=topic,
|
|
580
|
+
error=f"Context7 API quota exceeded: {error_msg}. Consider upgrading your plan or waiting for quota reset.",
|
|
581
|
+
response_time_ms=response_time,
|
|
582
|
+
)
|
|
583
|
+
else:
|
|
584
|
+
# R3: Context7 unavailable - distinguish between different error types
|
|
585
|
+
error_msg = resolve_result.get("error", "Context7 not available")
|
|
586
|
+
if "not configured" in error_msg.lower() or "not found" in error_msg.lower():
|
|
587
|
+
# Not configured - provide setup instructions
|
|
588
|
+
logger.info(
|
|
589
|
+
f"Context7 not configured for library '{library}' (topic: {topic}): {error_msg}. "
|
|
590
|
+
f"To enable Context7, set CONTEXT7_API_KEY or configure MCP server. "
|
|
591
|
+
f"Continuing without Context7 documentation."
|
|
592
|
+
)
|
|
593
|
+
elif "network" in error_msg.lower() or "connection" in error_msg.lower():
|
|
594
|
+
# Network error
|
|
595
|
+
logger.warning(
|
|
596
|
+
f"Context7 network error for library '{library}' (topic: {topic}): {error_msg}. "
|
|
597
|
+
f"Check your network connection. Continuing without Context7 documentation."
|
|
598
|
+
)
|
|
599
|
+
else:
|
|
600
|
+
# Generic unavailable
|
|
601
|
+
logger.info(
|
|
602
|
+
f"Context7 not available for library '{library}' (topic: {topic}): {error_msg}. "
|
|
603
|
+
f"Continuing without Context7 documentation."
|
|
604
|
+
)
|
|
605
|
+
except Exception as e:
|
|
606
|
+
# Continue without context7_id if resolution fails
|
|
607
|
+
logger.warning(
|
|
608
|
+
f"Failed to resolve Context7 library id for '{library}' (topic: {topic}): {e}. "
|
|
609
|
+
f"Continuing without Context7 documentation.",
|
|
610
|
+
exc_info=True
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
# Step 4: Fetch from Context7 API
|
|
614
|
+
if context7_id:
|
|
615
|
+
content = None
|
|
616
|
+
|
|
617
|
+
# Use backup client with automatic fallback (MCP Gateway -> HTTP)
|
|
618
|
+
from .backup_client import call_context7_get_docs_with_fallback
|
|
619
|
+
|
|
620
|
+
try:
|
|
621
|
+
api_result = await call_context7_get_docs_with_fallback(
|
|
622
|
+
context7_id, topic, mode="code", page=1, mcp_gateway=self.mcp_gateway
|
|
623
|
+
)
|
|
624
|
+
# R4: Record API call
|
|
625
|
+
analytics.record_api_call()
|
|
626
|
+
if api_result.get("success"):
|
|
627
|
+
result_data = api_result.get("result", {})
|
|
628
|
+
content = (
|
|
629
|
+
result_data.get("content")
|
|
630
|
+
if isinstance(result_data, dict)
|
|
631
|
+
else result_data
|
|
632
|
+
)
|
|
633
|
+
else:
|
|
634
|
+
# Context7 unavailable - log and continue
|
|
635
|
+
error_msg = api_result.get("error", "Context7 not available")
|
|
636
|
+
logger.info(
|
|
637
|
+
f"Context7 not available for library '{library}' (topic: {topic}): {error_msg}. "
|
|
638
|
+
f"Continuing without Context7 documentation."
|
|
639
|
+
)
|
|
640
|
+
content = None
|
|
641
|
+
except Exception as e:
|
|
642
|
+
# R4: Still record API call attempt
|
|
643
|
+
analytics.record_api_call()
|
|
644
|
+
logger.warning(
|
|
645
|
+
f"Failed to fetch Context7 docs for library '{library}' (topic: {topic}): {e}. "
|
|
646
|
+
f"Continuing without Context7 documentation.",
|
|
647
|
+
exc_info=True
|
|
648
|
+
)
|
|
649
|
+
content = None
|
|
650
|
+
|
|
651
|
+
# Step 5: Store in cache if we got content
|
|
652
|
+
if content:
|
|
653
|
+
try:
|
|
654
|
+
cached_entry = self.kb_cache.store(
|
|
655
|
+
library=library,
|
|
656
|
+
topic=topic,
|
|
657
|
+
content=content,
|
|
658
|
+
context7_id=context7_id,
|
|
659
|
+
)
|
|
660
|
+
except (RuntimeError, OSError, PermissionError) as e:
|
|
661
|
+
# Cache lock or file write failed - log but continue
|
|
662
|
+
# The content is still available, just not cached
|
|
663
|
+
logger.debug(
|
|
664
|
+
f"Failed to cache Context7 docs for {library}/{topic}: {e}. "
|
|
665
|
+
f"Content retrieved but not cached."
|
|
666
|
+
)
|
|
667
|
+
cached_entry = None
|
|
668
|
+
|
|
669
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
670
|
+
# R4: Record latency for API call (already recorded API call above)
|
|
671
|
+
if response_time > 0:
|
|
672
|
+
analytics.record_cache_hit(response_time_ms=response_time) # Reuse method to record latency
|
|
673
|
+
return LookupResult(
|
|
674
|
+
success=True,
|
|
675
|
+
content=content,
|
|
676
|
+
source="api",
|
|
677
|
+
library=library,
|
|
678
|
+
topic=topic,
|
|
679
|
+
context7_id=context7_id,
|
|
680
|
+
cached_entry=cached_entry,
|
|
681
|
+
response_time_ms=response_time,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
# If we reach here, lookup failed
|
|
685
|
+
response_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
|
686
|
+
|
|
687
|
+
# P1 Improvement: Try stale cache fallback before returning failure
|
|
688
|
+
stale_result = self._try_stale_cache_fallback(library, topic, start_time)
|
|
689
|
+
if stale_result and stale_result.success:
|
|
690
|
+
logger.info(
|
|
691
|
+
f"Context7 API failed for '{library}' (topic: {topic}), "
|
|
692
|
+
f"using stale cached data as fallback."
|
|
693
|
+
)
|
|
694
|
+
return stale_result
|
|
695
|
+
|
|
696
|
+
# Provide more specific error message based on why we failed
|
|
697
|
+
if context7_id is None:
|
|
698
|
+
error_msg = (
|
|
699
|
+
f"Could not resolve library ID for '{library}'. "
|
|
700
|
+
f"This is required to fetch documentation from Context7 API. "
|
|
701
|
+
f"Library resolution may have failed or returned no matches."
|
|
702
|
+
)
|
|
703
|
+
# Use debug level for library not found (common, expected case)
|
|
704
|
+
# Only warn if it's a known popular library that should exist in Context7
|
|
705
|
+
# Valid libraries not in Context7 (like 'openai', 'yaml') will log at debug level to reduce noise
|
|
706
|
+
known_libraries = {
|
|
707
|
+
"react", "vue", "angular", "fastapi", "django", "flask",
|
|
708
|
+
"pytest", "playwright", "typescript", "javascript", "node",
|
|
709
|
+
"express", "nextjs", "svelte", "tailwind", "bootstrap"
|
|
710
|
+
}
|
|
711
|
+
if library.lower() in known_libraries:
|
|
712
|
+
logger.info(
|
|
713
|
+
f"Context7 lookup failed for library '{library}' (topic: {topic}): {error_msg}. "
|
|
714
|
+
f"Continuing without Context7 documentation."
|
|
715
|
+
)
|
|
716
|
+
else:
|
|
717
|
+
logger.debug(
|
|
718
|
+
f"Context7 lookup failed for library '{library}' (topic: {topic}): {error_msg}. "
|
|
719
|
+
f"Library may not be in Context7 database. Continuing without Context7 documentation."
|
|
720
|
+
)
|
|
721
|
+
else:
|
|
722
|
+
error_msg = (
|
|
723
|
+
f"Failed to fetch documentation from Context7 API for '{library}' (ID: {context7_id}). "
|
|
724
|
+
f"API call may have failed or returned no content."
|
|
725
|
+
)
|
|
726
|
+
logger.warning(
|
|
727
|
+
f"Context7 lookup failed for library '{library}' (topic: {topic}): {error_msg}. "
|
|
728
|
+
f"Continuing without Context7 documentation."
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return LookupResult(
|
|
732
|
+
success=False,
|
|
733
|
+
source="cache" if context7_id is None else "api",
|
|
734
|
+
library=library,
|
|
735
|
+
topic=topic,
|
|
736
|
+
error=error_msg,
|
|
737
|
+
response_time_ms=response_time,
|
|
738
|
+
)
|