From 417b8b6f72765a2b256268cec36eb4b65882ab06 Mon Sep 17 00:00:00 2001 From: zosimovaa Date: Thu, 5 Mar 2026 11:03:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__pycache__/main.cpython-312.pyc | Bin 2123 -> 2992 bytes .../__pycache__/logging_setup.cpython-312.pyc | Bin 0 -> 3424 bytes app/core/logging_setup.py | 49 ++ app/main.py | 10 + .../__pycache__/application.cpython-312.pyc | Bin 2884 -> 4404 bytes app/modules/agent/README.md | 31 ++ .../agent/__pycache__/module.cpython-312.pyc | Bin 3243 -> 3542 bytes .../__pycache__/repository.cpython-312.pyc | Bin 11646 -> 13987 bytes .../agent/__pycache__/service.cpython-312.pyc | Bin 21670 -> 22893 bytes app/modules/agent/engine/graphs/__init__.py | 25 + .../__pycache__/__init__.cpython-312.pyc | Bin 945 -> 1441 bytes .../__pycache__/base_graph.cpython-312.pyc | Bin 3687 -> 3712 bytes .../docs_graph_logic.cpython-312.pyc | Bin 31433 -> 31766 bytes .../project_edits_logic.cpython-312.pyc | Bin 13770 -> 13936 bytes .../project_qa_graph.cpython-312.pyc | Bin 2645 -> 2670 bytes .../project_qa_step_graphs.cpython-312.pyc | Bin 0 -> 13410 bytes .../graphs/__pycache__/state.cpython-312.pyc | Bin 1468 -> 1659 bytes app/modules/agent/engine/graphs/base_graph.py | 2 +- .../agent/engine/graphs/docs_graph_logic.py | 20 +- .../engine/graphs/project_edits_logic.py | 18 +- .../agent/engine/graphs/project_qa_graph.py | 2 +- .../engine/graphs/project_qa_step_graphs.py | 172 +++++++ app/modules/agent/engine/graphs/state.py | 6 + .../execution_engine.cpython-312.pyc | Bin 7288 -> 9183 bytes .../__pycache__/service.cpython-312.pyc | Bin 5560 -> 6387 bytes .../__pycache__/step_registry.cpython-312.pyc | Bin 8141 -> 10890 bytes .../template_registry.cpython-312.pyc | Bin 15884 -> 18928 bytes .../engine/orchestrator/actions/__init__.py | 4 + .../__pycache__/__init__.cpython-312.pyc | Bin 757 -> 983 bytes .../code_explain_actions.cpython-312.pyc | Bin 0 -> 3093 bytes .../project_qa_actions.cpython-312.pyc | Bin 0 -> 9548 bytes .../project_qa_analyzer.cpython-312.pyc | Bin 0 -> 11166 bytes .../project_qa_support.cpython-312.pyc | Bin 0 -> 13388 bytes .../actions/code_explain_actions.py | 46 ++ .../actions/project_qa_actions.py | 117 +++++ .../actions/project_qa_analyzer.py | 154 ++++++ .../actions/project_qa_support.py | 166 +++++++ .../engine/orchestrator/execution_engine.py | 37 +- .../agent/engine/orchestrator/service.py | 18 + .../engine/orchestrator/step_registry.py | 53 +- .../engine/orchestrator/template_registry.py | 72 +++ app/modules/agent/engine/router/__init__.py | 16 +- .../__pycache__/__init__.cpython-312.pyc | Bin 2038 -> 3217 bytes .../__pycache__/context_store.cpython-312.pyc | Bin 1470 -> 1591 bytes .../intent_classifier.cpython-312.pyc | Bin 8159 -> 8276 bytes .../intent_switch_detector.cpython-312.pyc | Bin 0 -> 4057 bytes .../router_service.cpython-312.pyc | Bin 3099 -> 5408 bytes .../__pycache__/schemas.cpython-312.pyc | Bin 1730 -> 2132 bytes .../agent/engine/router/context_store.py | 2 + .../agent/engine/router/intent_classifier.py | 17 +- .../engine/router/intent_switch_detector.py | 81 ++++ .../agent/engine/router/router_service.py | 78 ++- app/modules/agent/engine/router/schemas.py | 7 + .../llm/__pycache__/service.cpython-312.pyc | Bin 1184 -> 2161 bytes app/modules/agent/llm/service.py | 30 +- app/modules/agent/module.py | 12 +- .../agent/prompts/code_explain_answer_v2.txt | 17 + .../agent/prompts/rag_intent_router_v2.txt | 24 + app/modules/agent/repository.py | 50 +- app/modules/agent/service.py | 47 +- app/modules/application.py | 23 + .../__pycache__/dialog_store.cpython-312.pyc | Bin 1790 -> 1861 bytes .../direct_service.cpython-312.pyc | Bin 0 -> 4133 bytes .../__pycache__/evidence_gate.cpython-312.pyc | Bin 0 -> 4456 bytes .../chat/__pycache__/module.cpython-312.pyc | Bin 6521 -> 7280 bytes .../chat/__pycache__/service.cpython-312.pyc | Bin 16558 -> 15856 bytes .../session_resolver.cpython-312.pyc | Bin 0 -> 2253 bytes app/modules/chat/dialog_store.py | 6 +- app/modules/chat/direct_service.py | 71 +++ app/modules/chat/evidence_gate.py | 62 +++ app/modules/chat/module.py | 26 +- app/modules/chat/service.py | 57 +-- app/modules/chat/session_resolver.py | 36 ++ app/modules/rag/README.md | 35 ++ app/modules/rag/explain/__init__.py | 36 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1179 bytes .../__pycache__/budgeter.cpython-312.pyc | Bin 0 -> 3703 bytes .../excerpt_planner.cpython-312.pyc | Bin 0 -> 2847 bytes .../graph_repository.cpython-312.pyc | Bin 0 -> 10398 bytes .../intent_builder.cpython-312.pyc | Bin 0 -> 6777 bytes .../layered_gateway.cpython-312.pyc | Bin 0 -> 10952 bytes .../__pycache__/models.cpython-312.pyc | Bin 0 -> 4574 bytes .../__pycache__/retriever_v2.cpython-312.pyc | Bin 0 -> 15760 bytes .../source_excerpt_fetcher.cpython-312.pyc | Bin 0 -> 3107 bytes .../__pycache__/trace_builder.cpython-312.pyc | Bin 0 -> 6423 bytes app/modules/rag/explain/budgeter.py | 62 +++ app/modules/rag/explain/excerpt_planner.py | 59 +++ app/modules/rag/explain/graph_repository.py | 216 +++++++++ app/modules/rag/explain/intent_builder.py | 102 ++++ app/modules/rag/explain/layered_gateway.py | 289 +++++++++++ app/modules/rag/explain/models.py | 91 ++++ app/modules/rag/explain/retriever_v2.py | 328 +++++++++++++ .../rag/explain/source_excerpt_fetcher.py | 53 ++ app/modules/rag/explain/trace_builder.py | 102 ++++ .../document_builder.cpython-312.pyc | Bin 1467 -> 1608 bytes .../code/code_text/document_builder.py | 2 + .../document_builder.cpython-312.pyc | Bin 2090 -> 2231 bytes .../indexing/code/edges/document_builder.py | 2 + .../document_builder.cpython-312.pyc | Bin 1952 -> 2093 bytes .../code/entrypoints/document_builder.py | 2 + .../document_builder.cpython-312.pyc | Bin 2386 -> 2527 bytes .../indexing/code/symbols/document_builder.py | 2 + .../document_upserter.cpython-312.pyc | Bin 0 -> 1348 bytes .../common/__pycache__/report.cpython-312.pyc | Bin 0 -> 1197 bytes app/modules/rag/intent_router.md | 201 ++++++++ app/modules/rag/intent_router_v2/__init__.py | 23 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 693 bytes .../anchor_extractor.cpython-312.pyc | Bin 0 -> 9195 bytes .../anchor_span_validator.cpython-312.pyc | Bin 0 -> 1539 bytes .../__pycache__/classifier.cpython-312.pyc | Bin 0 -> 7885 bytes ...onversation_anchor_builder.cpython-312.pyc | Bin 0 -> 2827 bytes .../conversation_policy.cpython-312.pyc | Bin 0 -> 3307 bytes .../evidence_policy_factory.cpython-312.pyc | Bin 0 -> 1613 bytes .../__pycache__/factory.cpython-312.pyc | Bin 0 -> 1625 bytes .../followup_detector.cpython-312.pyc | Bin 0 -> 1301 bytes .../graph_id_resolver.cpython-312.pyc | Bin 0 -> 755 bytes .../keyword_hint_builder.cpython-312.pyc | Bin 0 -> 2183 bytes .../keyword_hint_sanitizer.cpython-312.pyc | Bin 0 -> 3516 bytes .../layer_query_builder.cpython-312.pyc | Bin 0 -> 1817 bytes .../__pycache__/local_runner.cpython-312.pyc | Bin 0 -> 1898 bytes .../__pycache__/logger.cpython-312.pyc | Bin 0 -> 1643 bytes .../__pycache__/models.cpython-312.pyc | Bin 0 -> 10022 bytes .../negation_detector.cpython-312.pyc | Bin 0 -> 1129 bytes .../__pycache__/normalization.cpython-312.pyc | Bin 0 -> 4093 bytes .../normalization_terms.cpython-312.pyc | Bin 0 -> 2974 bytes .../__pycache__/protocols.cpython-312.pyc | Bin 0 -> 644 bytes .../query_normalizer.cpython-312.pyc | Bin 0 -> 266 bytes .../query_plan_builder.cpython-312.pyc | Bin 0 -> 13479 bytes .../retrieval_filter_builder.cpython-312.pyc | Bin 0 -> 6166 bytes .../retrieval_spec_factory.cpython-312.pyc | Bin 0 -> 5482 bytes .../__pycache__/router.cpython-312.pyc | Bin 0 -> 4382 bytes .../sub_intent_detector.cpython-312.pyc | Bin 0 -> 2635 bytes .../__pycache__/symbol_rules.cpython-312.pyc | Bin 0 -> 625 bytes .../__pycache__/term_mapping.cpython-312.pyc | Bin 0 -> 4258 bytes .../test_signals.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 2417 bytes .../rag/intent_router_v2/anchor_extractor.py | 144 ++++++ .../intent_router_v2/anchor_span_validator.py | 22 + .../rag/intent_router_v2/classifier.py | 113 +++++ .../conversation_anchor_builder.py | 61 +++ .../intent_router_v2/conversation_policy.py | 45 ++ .../evidence_policy_factory.py | 28 ++ app/modules/rag/intent_router_v2/factory.py | 22 + .../rag/intent_router_v2/followup_detector.py | 22 + .../rag/intent_router_v2/graph_id_resolver.py | 13 + .../intent_router_v2/keyword_hint_builder.py | 34 ++ .../keyword_hint_sanitizer.py | 50 ++ .../intent_router_v2/layer_query_builder.py | 29 ++ .../rag/intent_router_v2/local_runner.py | 25 + app/modules/rag/intent_router_v2/logger.py | 22 + app/modules/rag/intent_router_v2/models.py | 182 +++++++ .../rag/intent_router_v2/negation_detector.py | 17 + .../rag/intent_router_v2/normalization.py | 57 +++ .../intent_router_v2/normalization_terms.py | 48 ++ app/modules/rag/intent_router_v2/protocols.py | 7 + .../rag/intent_router_v2/query_normalizer.py | 3 + .../intent_router_v2/query_plan_builder.py | 223 +++++++++ .../retrieval_filter_builder.py | 111 +++++ .../retrieval_spec_factory.py | 118 +++++ app/modules/rag/intent_router_v2/router.py | 72 +++ .../intent_router_v2/sub_intent_detector.py | 23 + .../rag/intent_router_v2/symbol_rules.py | 51 ++ .../rag/intent_router_v2/term_mapping.py | 67 +++ .../rag/intent_router_v2/test_signals.py | 40 ++ .../cache_repository.cpython-312.pyc | Bin 0 -> 10281 bytes .../document_repository.cpython-312.pyc | Bin 0 -> 6718 bytes .../job_repository.cpython-312.pyc | Bin 0 -> 4141 bytes .../query_repository.cpython-312.pyc | Bin 6698 -> 7880 bytes .../__pycache__/repository.cpython-312.pyc | Bin 0 -> 6202 bytes ...etrieval_statement_builder.cpython-312.pyc | Bin 0 -> 10109 bytes .../schema_repository.cpython-312.pyc | Bin 0 -> 10493 bytes .../session_repository.cpython-312.pyc | Bin 0 -> 2547 bytes .../rag/persistence/document_repository.py | 13 +- .../rag/persistence/query_repository.py | 115 ++--- app/modules/rag/persistence/repository.py | 28 +- .../retrieval_statement_builder.py | 201 ++++++++ .../rag/persistence/schema_repository.py | 10 +- .../test_filter.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 4854 bytes .../__pycache__/test_filter.cpython-312.pyc | Bin 0 -> 4431 bytes app/modules/rag/retrieval/query_router.py | 43 -- app/modules/rag/retrieval/test_filter.py | 97 ++++ .../__pycache__/rag_service.cpython-312.pyc | Bin 0 -> 11750 bytes app/modules/rag/services/rag_service.py | 59 +-- app/modules/rag_session/module.py | 16 +- .../__pycache__/env_loader.cpython-312.pyc | Bin 0 -> 2163 bytes app/modules/shared/env_loader.py | 37 ++ .../__pycache__/client.cpython-312.pyc | Bin 4173 -> 4754 bytes app/modules/shared/gigachat/client.py | 79 +-- .../__pycache__/rag_sessions.cpython-312.pyc | Bin 1624 -> 1624 bytes docs/architecture/contracts_retrieval.json | 380 +++++++++++++++ docs/architecture/llm_inventory.md | 270 +++++++++++ docs/architecture/rag_chunks_column_audit.md | 13 + docs/architecture/retrieval_callgraph.mmd | 31 ++ docs/architecture/retrieval_inventory.md | 457 ++++++++++++++++++ pytest.ini | 3 + ..._client_retry.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 3697 bytes ...rvice_logging.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 6843 bytes ...logging_setup.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 3756 bytes ...intent_policy.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 17888 bytes .../test_router_service_intent_policy.py | 181 +++++++ ...plain_actions.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 5471 bytes ...rator_service.cpython-312-pytest-9.0.2.pyc | Bin 8437 -> 14323 bytes ...ct_qa_actions.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 6079 bytes ...swer_graph_v2.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 4549 bytes ...graph_v2_only.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 3171 bytes ...late_registry.cpython-312-pytest-9.0.2.pyc | Bin 10032 -> 13569 bytes .../orchestrator/test_code_explain_actions.py | 59 +++ .../orchestrator/test_orchestrator_service.py | 113 ++++- .../orchestrator/test_project_qa_actions.py | 71 +++ .../test_project_qa_answer_graph_v2.py | 74 +++ ...test_project_qa_retrieval_graph_v2_only.py | 49 ++ .../orchestrator/test_template_registry.py | 10 + tests/agent/test_gigachat_client_retry.py | 48 ++ tests/agent/test_llm_service_logging.py | 30 ++ tests/agent/test_logging_setup.py | 24 + ..._code_explain.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 5555 bytes ...at_api_simple_code_explain.cpython-312.pyc | Bin 0 -> 4136 bytes ...irect_service.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 7347 bytes .../test_direct_service.cpython-312.pyc | Bin 0 -> 4002 bytes .../chat/test_chat_api_simple_code_explain.py | 70 +++ tests/chat/test_direct_service.py | 61 +++ .../asserts_intent_router.cpython-312.pyc | Bin 0 -> 5517 bytes .../intent_router_testkit.cpython-312.pyc | Bin 0 -> 2710 bytes ...xing_pipeline.cpython-312-pytest-9.0.2.pyc | Bin 8490 -> 9920 bytes ...est_code_indexing_pipeline.cpython-312.pyc | Bin 0 -> 3833 bytes ...est_docs_indexing_pipeline.cpython-312.pyc | Bin 0 -> 3666 bytes ...ntent_builder.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 5982 bytes ...est_explain_intent_builder.cpython-312.pyc | Bin 0 -> 1034 bytes ...ter_e2e_flows.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 23270 bytes ...er_invariants.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 19316 bytes ...phrase_matrix.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 35062 bytes ...ent_router_v2.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 20174 bytes .../test_intent_router_v2.cpython-312.pyc | Bin 0 -> 6518 bytes ...v2_local_flow.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 20416 bytes ...ntent_router_v2_local_flow.cpython-312.pyc | Bin 0 -> 8052 bytes ...yered_gateway.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 7062 bytes ...normalization.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 9860 bytes .../test_query_normalization.cpython-312.pyc | Bin 0 -> 4053 bytes .../test_query_terms.cpython-312.pyc | Bin 0 -> 593 bytes ...rvice_logging.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 13338 bytes ...ement_builder.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 9121 bytes ...2_no_fallback.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 6530 bytes ...t_retriever_v2_no_fallback.cpython-312.pyc | Bin 0 -> 3405 bytes ...iever_v2_pack.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 8824 bytes .../test_retriever_v2_pack.cpython-312.pyc | Bin 0 -> 4643 bytes ...duction_first.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 9768 bytes ...trace_builder.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 7840 bytes .../test_trace_builder.cpython-312.pyc | Bin 0 -> 3173 bytes tests/rag/asserts_intent_router.py | 77 +++ tests/rag/intent_router_testkit.py | 45 ++ tests/rag/test_code_indexing_pipeline.py | 18 + tests/rag/test_explain_intent_builder.py | 22 + tests/rag/test_intent_router_e2e_flows.py | 126 +++++ tests/rag/test_intent_router_invariants.py | 120 +++++ tests/rag/test_layered_gateway.py | 78 +++ tests/rag/test_query_normalization.py | 63 +++ tests/rag/test_query_router.py | 12 - tests/rag/test_retrieval_statement_builder.py | 44 ++ tests/rag/test_retriever_v2_no_fallback.py | 52 ++ tests/rag/test_retriever_v2_pack.py | 105 ++++ .../rag/test_retriever_v2_production_first.py | 142 ++++++ tests/rag/test_trace_builder.py | 83 ++++ 261 files changed, 8215 insertions(+), 332 deletions(-) create mode 100644 app/core/__pycache__/logging_setup.cpython-312.pyc create mode 100644 app/core/logging_setup.py create mode 100644 app/modules/agent/engine/graphs/__pycache__/project_qa_step_graphs.cpython-312.pyc create mode 100644 app/modules/agent/engine/graphs/project_qa_step_graphs.py create mode 100644 app/modules/agent/engine/orchestrator/actions/__pycache__/code_explain_actions.cpython-312.pyc create mode 100644 app/modules/agent/engine/orchestrator/actions/__pycache__/project_qa_actions.cpython-312.pyc create mode 100644 app/modules/agent/engine/orchestrator/actions/__pycache__/project_qa_analyzer.cpython-312.pyc create mode 100644 app/modules/agent/engine/orchestrator/actions/__pycache__/project_qa_support.cpython-312.pyc create mode 100644 app/modules/agent/engine/orchestrator/actions/code_explain_actions.py create mode 100644 app/modules/agent/engine/orchestrator/actions/project_qa_actions.py create mode 100644 app/modules/agent/engine/orchestrator/actions/project_qa_analyzer.py create mode 100644 app/modules/agent/engine/orchestrator/actions/project_qa_support.py create mode 100644 app/modules/agent/engine/router/__pycache__/intent_switch_detector.cpython-312.pyc create mode 100644 app/modules/agent/engine/router/intent_switch_detector.py create mode 100644 app/modules/agent/prompts/code_explain_answer_v2.txt create mode 100644 app/modules/agent/prompts/rag_intent_router_v2.txt create mode 100644 app/modules/chat/__pycache__/direct_service.cpython-312.pyc create mode 100644 app/modules/chat/__pycache__/evidence_gate.cpython-312.pyc create mode 100644 app/modules/chat/__pycache__/session_resolver.cpython-312.pyc create mode 100644 app/modules/chat/direct_service.py create mode 100644 app/modules/chat/evidence_gate.py create mode 100644 app/modules/chat/session_resolver.py create mode 100644 app/modules/rag/explain/__init__.py create mode 100644 app/modules/rag/explain/__pycache__/__init__.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/budgeter.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/excerpt_planner.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/graph_repository.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/intent_builder.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/layered_gateway.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/models.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/retriever_v2.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/source_excerpt_fetcher.cpython-312.pyc create mode 100644 app/modules/rag/explain/__pycache__/trace_builder.cpython-312.pyc create mode 100644 app/modules/rag/explain/budgeter.py create mode 100644 app/modules/rag/explain/excerpt_planner.py create mode 100644 app/modules/rag/explain/graph_repository.py create mode 100644 app/modules/rag/explain/intent_builder.py create mode 100644 app/modules/rag/explain/layered_gateway.py create mode 100644 app/modules/rag/explain/models.py create mode 100644 app/modules/rag/explain/retriever_v2.py create mode 100644 app/modules/rag/explain/source_excerpt_fetcher.py create mode 100644 app/modules/rag/explain/trace_builder.py create mode 100644 app/modules/rag/indexing/common/__pycache__/document_upserter.cpython-312.pyc create mode 100644 app/modules/rag/indexing/common/__pycache__/report.cpython-312.pyc create mode 100644 app/modules/rag/intent_router.md create mode 100644 app/modules/rag/intent_router_v2/__init__.py create mode 100644 app/modules/rag/intent_router_v2/__pycache__/__init__.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/anchor_extractor.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/anchor_span_validator.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/classifier.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/conversation_anchor_builder.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/conversation_policy.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/evidence_policy_factory.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/factory.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/followup_detector.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/graph_id_resolver.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/keyword_hint_builder.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/keyword_hint_sanitizer.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/layer_query_builder.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/local_runner.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/logger.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/models.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/negation_detector.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/normalization.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/normalization_terms.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/protocols.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/query_normalizer.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/query_plan_builder.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/retrieval_filter_builder.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/retrieval_spec_factory.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/router.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/sub_intent_detector.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/symbol_rules.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/term_mapping.cpython-312.pyc create mode 100644 app/modules/rag/intent_router_v2/__pycache__/test_signals.cpython-312-pytest-9.0.2.pyc create mode 100644 app/modules/rag/intent_router_v2/anchor_extractor.py create mode 100644 app/modules/rag/intent_router_v2/anchor_span_validator.py create mode 100644 app/modules/rag/intent_router_v2/classifier.py create mode 100644 app/modules/rag/intent_router_v2/conversation_anchor_builder.py create mode 100644 app/modules/rag/intent_router_v2/conversation_policy.py create mode 100644 app/modules/rag/intent_router_v2/evidence_policy_factory.py create mode 100644 app/modules/rag/intent_router_v2/factory.py create mode 100644 app/modules/rag/intent_router_v2/followup_detector.py create mode 100644 app/modules/rag/intent_router_v2/graph_id_resolver.py create mode 100644 app/modules/rag/intent_router_v2/keyword_hint_builder.py create mode 100644 app/modules/rag/intent_router_v2/keyword_hint_sanitizer.py create mode 100644 app/modules/rag/intent_router_v2/layer_query_builder.py create mode 100644 app/modules/rag/intent_router_v2/local_runner.py create mode 100644 app/modules/rag/intent_router_v2/logger.py create mode 100644 app/modules/rag/intent_router_v2/models.py create mode 100644 app/modules/rag/intent_router_v2/negation_detector.py create mode 100644 app/modules/rag/intent_router_v2/normalization.py create mode 100644 app/modules/rag/intent_router_v2/normalization_terms.py create mode 100644 app/modules/rag/intent_router_v2/protocols.py create mode 100644 app/modules/rag/intent_router_v2/query_normalizer.py create mode 100644 app/modules/rag/intent_router_v2/query_plan_builder.py create mode 100644 app/modules/rag/intent_router_v2/retrieval_filter_builder.py create mode 100644 app/modules/rag/intent_router_v2/retrieval_spec_factory.py create mode 100644 app/modules/rag/intent_router_v2/router.py create mode 100644 app/modules/rag/intent_router_v2/sub_intent_detector.py create mode 100644 app/modules/rag/intent_router_v2/symbol_rules.py create mode 100644 app/modules/rag/intent_router_v2/term_mapping.py create mode 100644 app/modules/rag/intent_router_v2/test_signals.py create mode 100644 app/modules/rag/persistence/__pycache__/cache_repository.cpython-312.pyc create mode 100644 app/modules/rag/persistence/__pycache__/document_repository.cpython-312.pyc create mode 100644 app/modules/rag/persistence/__pycache__/job_repository.cpython-312.pyc create mode 100644 app/modules/rag/persistence/__pycache__/repository.cpython-312.pyc create mode 100644 app/modules/rag/persistence/__pycache__/retrieval_statement_builder.cpython-312.pyc create mode 100644 app/modules/rag/persistence/__pycache__/schema_repository.cpython-312.pyc create mode 100644 app/modules/rag/persistence/__pycache__/session_repository.cpython-312.pyc create mode 100644 app/modules/rag/persistence/retrieval_statement_builder.py create mode 100644 app/modules/rag/retrieval/__pycache__/test_filter.cpython-312-pytest-9.0.2.pyc create mode 100644 app/modules/rag/retrieval/__pycache__/test_filter.cpython-312.pyc delete mode 100644 app/modules/rag/retrieval/query_router.py create mode 100644 app/modules/rag/retrieval/test_filter.py create mode 100644 app/modules/rag/services/__pycache__/rag_service.cpython-312.pyc create mode 100644 app/modules/shared/__pycache__/env_loader.cpython-312.pyc create mode 100644 app/modules/shared/env_loader.py create mode 100644 docs/architecture/contracts_retrieval.json create mode 100644 docs/architecture/llm_inventory.md create mode 100644 docs/architecture/rag_chunks_column_audit.md create mode 100644 docs/architecture/retrieval_callgraph.mmd create mode 100644 docs/architecture/retrieval_inventory.md create mode 100644 pytest.ini create mode 100644 tests/agent/__pycache__/test_gigachat_client_retry.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/__pycache__/test_llm_service_logging.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/__pycache__/test_logging_setup.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/engine/router/__pycache__/test_router_service_intent_policy.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/engine/router/test_router_service_intent_policy.py create mode 100644 tests/agent/orchestrator/__pycache__/test_code_explain_actions.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/orchestrator/__pycache__/test_project_qa_actions.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/orchestrator/__pycache__/test_project_qa_answer_graph_v2.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/orchestrator/__pycache__/test_project_qa_retrieval_graph_v2_only.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/agent/orchestrator/test_code_explain_actions.py create mode 100644 tests/agent/orchestrator/test_project_qa_actions.py create mode 100644 tests/agent/orchestrator/test_project_qa_answer_graph_v2.py create mode 100644 tests/agent/orchestrator/test_project_qa_retrieval_graph_v2_only.py create mode 100644 tests/agent/test_gigachat_client_retry.py create mode 100644 tests/agent/test_llm_service_logging.py create mode 100644 tests/agent/test_logging_setup.py create mode 100644 tests/chat/__pycache__/test_chat_api_simple_code_explain.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/chat/__pycache__/test_chat_api_simple_code_explain.cpython-312.pyc create mode 100644 tests/chat/__pycache__/test_direct_service.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/chat/__pycache__/test_direct_service.cpython-312.pyc create mode 100644 tests/chat/test_chat_api_simple_code_explain.py create mode 100644 tests/chat/test_direct_service.py create mode 100644 tests/rag/__pycache__/asserts_intent_router.cpython-312.pyc create mode 100644 tests/rag/__pycache__/intent_router_testkit.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_code_indexing_pipeline.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_docs_indexing_pipeline.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_explain_intent_builder.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_explain_intent_builder.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_intent_router_e2e_flows.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_intent_router_invariants.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_intent_router_phrase_matrix.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_intent_router_v2.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_intent_router_v2.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_intent_router_v2_local_flow.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_intent_router_v2_local_flow.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_layered_gateway.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_query_normalization.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_query_normalization.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_query_terms.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_rag_service_logging.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_retrieval_statement_builder.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_retriever_v2_no_fallback.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_retriever_v2_no_fallback.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_retriever_v2_pack.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_retriever_v2_pack.cpython-312.pyc create mode 100644 tests/rag/__pycache__/test_retriever_v2_production_first.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_trace_builder.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/rag/__pycache__/test_trace_builder.cpython-312.pyc create mode 100644 tests/rag/asserts_intent_router.py create mode 100644 tests/rag/intent_router_testkit.py create mode 100644 tests/rag/test_explain_intent_builder.py create mode 100644 tests/rag/test_intent_router_e2e_flows.py create mode 100644 tests/rag/test_intent_router_invariants.py create mode 100644 tests/rag/test_layered_gateway.py create mode 100644 tests/rag/test_query_normalization.py delete mode 100644 tests/rag/test_query_router.py create mode 100644 tests/rag/test_retrieval_statement_builder.py create mode 100644 tests/rag/test_retriever_v2_no_fallback.py create mode 100644 tests/rag/test_retriever_v2_pack.py create mode 100644 tests/rag/test_retriever_v2_production_first.py create mode 100644 tests/rag/test_trace_builder.py diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index 2ec3f68da712494ca7c252a216fdbefdf1dbae71..b39911438faccec266e035c71b039a6c344865e4 100644 GIT binary patch delta 1067 zcmb_aOKTHR6ux)n+?hPu^pU3VRTETN2DNFrP<$ZPf+9#3wNMy@5|c@oFv)~FNuk}e z3-Pg$)&WIOq*##Bf-M&MBf2ppB`~(jBJPyzy7JzYb2vNQ8$sVr zXQv0T^Y!V9{+=xPj?Ej)dWDiGi4COkTQG1|PzB7nXoy)*q0Ye|SxJ@ZoEUP}rMeh* zC*?<=x&@@U*SIh{>{Zf)l#p~WkB50YEsyG*#fs+}4~i++k#EPXrN@p=1TUma3Y*>x8vL7H;HAzA7~c*Bb|vW;#$-9 zk2>(5bjaW6%(wq*7AaC}QV|?tXfvAOkd#Rinj;ry^n^)sbDd2xtWj#xIbX|+ClZ=v zDa4TtGo99RX~+98Zt00}GndlSjyyFs5xX0^PR^ zBiXo~8_v%BI zxqq+>Lsb|l!N^u{JG2?vg)!R8+S%NFE1nu)gzszCOco6`5I)xh>Sss??3WP6ioFe~ z4%)+C>d?g85#Ro=t5RGzZROF1ebm+R?=<^1>%uL>^#mRROWBdjA h4$#ScGoR$3hJ|?u72PJe*zg3*^U4J delta 330 zcmdlWep-O)X#+1U=!V<;7#E{Cd8e}LCL~&NKYjV5=P0Y;GE2u0In0%96fl+4iFZMLfl?+AV zKoy@ECg*VIFe*%*$)UukIe8Ct^At`aMs^7x z(@&FY@-?o^XpnG`8i)XyRs@nN5(W~#IBXyeu`AL6azTzM1}Xc%%*e=imqGe2gV9q4 f>$?nQAJ}9V`6sx4W&lzj?HJevn{yhO!R7z}K8Hw< diff --git a/app/core/__pycache__/logging_setup.cpython-312.pyc b/app/core/__pycache__/logging_setup.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfd21a2e0d7ef37accf2938566e0cac80584e36d GIT binary patch literal 3424 zcmcguO>7&-6`ozrl1oYyEiyFa*s&-(mbh{yDshUYisVE|WYvhn1hSGgl-0#*cf_nh zaoO3WA`uj#gMmnZi&O{#sc4J((u3guNq_+9A;>MKq5ugxAhC4;1L-9hZre-?;cq#=#Vq7^R9 zag64(e42MgPujzy8KikGBMlRNS!{059h{5%F|Vq;c0dcg+JO_@=}XpB5e{<$?XX51QiA z*Linb;?h1%+(Jv7hA(%gMa}OV%dZ7AFB~rbqYuWeEwsj^yDuWC%MM>uX<>0u&t=XT zbVac&LW_f!78CPRBWY!%C@(aKv5}w43&Xl5O(y1leaZaE!fC0Pc%Yp823bESDTAb* z)5u!MQp`6>nyzGxOo=L)l1WSxJY33Wm0U@omcFE@R*9_Y8p)}oWUj9)8rc%jGNeRu z7G0O(x^`)CetdG_k~!QmMQu31aRZAgeB6E(i1)Z%WVzmfejAt2Zy|$9t$=LsCA8eg zsg@WJrB*E z#xhYYQ(k>MspxTy>Z>FdSMvEda8z(@k})#ixg`8df;N%_&u1XqvPrB$el)*M{jh)W z$anK#)HVMQu+1l+`*suEjP&jNZ0GFN!D>X>66=xJ?o&HY?X6WKiCQFCi6lS7)yPkQ z5sK|Sy8TvJJiZU>&^^&%r*We584$Y=2`2(f?7sFqS~!jn^Y1c?=zR^M*0_cT84$Hb zCT#Fm;uwJx;K%*#2Y2p(72nyJhj?=-h-E~6Vfr=s)u{`wOv^7%&CN~ErY_nZ6XG3~ zDS^CJ3A+yRs*)`byGNdz{?%MtFxZl@$eJZ#%C5KVTY=a?+-$E=u<`}V_T`$j-hiiE(&Z@gKiVmd;7M$_1Mu`ELn*q|ARRHv)tB9J$h_+erNvbL$!e? zD+5pdb+8(p+CF#N!??GY@NgwI%!DWJkv&rxIP>AF)#!vPJ9#S<-3rn$D7q)qt&2De z92V;31`5Ko)!J9-4*=hPVs7=MzG8yKuE`0w6o3arJ9~|@g$y+c`D?uEA_2fkecfc? zZhGS|TXd>JBDZ|pio+uyYW6_~o(acxPw$-GQ{Ejd_YPb=S`Lkr#gQNLG?Zy8_5gAn zI1q*8Ml=rHJ!fDx731G`29_tzvX15-FNem;;+SJTWK)oB*W0-AqZf?KtP42+JywC{ zAWtm{Vn;xXKFYe2O$z=Fu*E@2MqgyEo(Iu|1N@*Z%5qLwA+l@-WO>EV3Ry;X%kpmu zO18n_DY1R3v69!bggyit+JW;ksh4J_pPRZkO=CxJg#mRbmdbTP)eOvGx^_Fq$b^!S! zO}Rq(&Gl~()Wd21-|hgA6cW#-1At?wB+0Yu~S{`R)KM zUCo7BtN3kjuxE!C2S8{9z?m=c%c16~_Si139J&(4KVZLxn>PGU*0=rL!AF|#f6(Fm zF?6gW$(Ncdo;a1rl2zhdBgq^;m0i+^yxk$`Az-i-@kt33i;pI{R02uI+GMCUX)+L~`{Vjhw z&UQR<4Y`=ZYW`hu!iCzi^l6x~tTzXtgF|rKKhcRVQO`e+|Bm-z?y str: + rendered = super().format(record) + scrubbed = self._scrub(rendered).rstrip("\n") + return scrubbed + "\n" + + def _scrub(self, message: str) -> str: + output = message + for pattern in self._KEY_VALUE_PATTERNS: + output = pattern.sub(self._replace_key_value, output) + for pattern in self._TEXT_PATTERNS: + output = pattern.sub(self._replace_text, output) + return output + + def _replace_key_value(self, match: re.Match[str]) -> str: + return f"{match.group(1)}=" + + def _replace_text(self, match: re.Match[str]) -> str: + return f"{match.group(1)} id=" + + +def configure_logging() -> None: + logging.basicConfig( + level=logging.WARNING, + force=True, + format="%(levelname)s:%(name)s:%(message)s", + ) + root_logger = logging.getLogger() + root_logger.setLevel(logging.WARNING) + formatter = ScrubbingFormatter("%(levelname)s:%(name)s:%(message)s") + for handler in root_logger.handlers: + handler.setFormatter(formatter) + logging.getLogger("uvicorn").setLevel(logging.WARNING) + logging.getLogger("uvicorn.error").setLevel(logging.WARNING) + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) diff --git a/app/main.py b/app/main.py index e28c308..9d2eed0 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,20 @@ +import logging + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from app.core.logging_setup import configure_logging from app.core.error_handlers import register_error_handlers from app.modules.application import ModularApplication +def _configure_logging() -> None: + configure_logging() + + +_configure_logging() + + def create_app() -> FastAPI: app = FastAPI(title="Agent Backend MVP", version="0.1.0") modules = ModularApplication() diff --git a/app/modules/__pycache__/application.cpython-312.pyc b/app/modules/__pycache__/application.cpython-312.pyc index 8a365844f92566ae227cf55a4e0c0abfe2252aca..cc9982e7756380d1fea389756bc4f7c1a8447b4f 100644 GIT binary patch literal 4404 zcmb7HOKcm*8J-nMiKM89Wr^ZLPs_4R(WYe84PwQSEIF#A0P(>1 z3vlO~|240FzJK{uPfwh{^U(QY>8k`Gf5#4g!X7yK2EZOsh$3i27eqlo+@W>oAu+^o zNDJ!`F`{>hova_$y7Z_RWjLb6^tc#jxKr!a6JmnlF0DuJ6?++uYJGaY*w1iGJEaeZ z0}RKt)B2z|$Z)qdq$kCsJ}eIFDKW)l2`#N>#0zsM;PwaM)fgqjNv|QT%Qmp z81C05^)uobfpm~tL^<_@C4*;F5zzQ!2|%2$gUsxJFl2$j51QPi6ctD0OY-`J4NRh2#{6>*S( z_e!!>SzlERqf{wl7?k#+QA>n4N}5wU?FL~!cU7aJJy0pQ#eO0iTRcY3U3onaBGfX8 zyQ)b`DtP^L-Z_nvyG7;dhHvf+|5)Bpsj7H}vUW>0)nCawI0^)A&V-NB6b!6YDyCsV zI7yLBc}+HOLD8EJU~5;l4ZuAZbLZwmwYUw-ExIS+zP)21Mg?Q+D6;QW#%MD&&Vd< zS}uYXq@97T2SLi%p;dDr?0b6wd)8c!InAxWE5othz9Cizzkd+xG`BW$O_b-Ty+26L za0|cPyy#<&_&DZlQ{sVx5ZA#RQe10(kE1t?afR#s7Ayz+9!DT;zaMkl!fy}4`yT7U zT(VqlUgZd4webS!Tin8LHy3=&k@;>DEpY_iy&U`PzIPj*lKX-Zc@jP7^ml1<%(vsH zeU#1XT<^CBUHjf`fi*Vs(!9wn-msbC=bh1noB&k#`nUYkvrd5ivW-J15 z&F3Msfrs~>$K=mv?S-R}!u8>SD6EJZ z&Ol%_u)8J0G@w$&n)~a7G);H<`HHAlx}CTQg;oG!XuHcPrn z&tWf&lX)RfEHDN#+%M=1%A-6F9%cVWlHdj!e*iZ~iF$JEv(2aB-*nf9&U_{Th}XyR zUsRrkUq!R^@k@u}vj|7(6SIdCml2NEXMc1!`#!>p^$UxK7p^0mu4ksc$b);|H`%k# z%|Cto;^QyR{&nUrGcWJ`+`gw-_q1189Su|U)O2k+XHTzK(<`;?xScIn*}`!ol#YXU z&vBHTf44rEfBv3*aml*4^yQ$v{DHOnfxRqR%i`hSNA=OU!_j%LKM(e=qEq0IoUCQW z?97annXxkkD^sYAPTHgM*66%H(A0T*YSEfnJdQ<@I8gjJK}P0l$&8)6Y$Y$(`Um&o zyYao=-Ch`=o|<`n#m+BT`K80u^}kISFx*)0w^5QB+52?&(^`7ePG7Rpm+bUaD}D9& zRBVLJC3QSVhQ>kr`^@h$b~==dJX7Z8&QW=dIy< zEj41NE?B7xAi)8S`=Ub-@wYLG9{?>3hN+8bbX0{`@cZwRwlxH#UL6MW<^xiU^V$Pa zOu*M3UdIKpL+Dvh<{}g+)V*nMBVu<7OFPBN!qRcVO~)?7l&QI0eUxdFjh=29eG2e` zT#ROFiGDjVZY9PUn6MHP3`|;yNe1Su#N5APkyxB<4G>i)Fwr(G-vLsB}F2gX7!VFkVL{uexMG~B;lj7^-;Hdgp| z%%x(NVzMV}0s}Jg96n*;F4*Ow$rIYw>7VhLb8;NND7d70E*J@ju~1d10X0S~7u7;w zQg|dz%cRiQEA}qyGMZ)7JN8;oUX1xMA$zyZjq^9RV65J9kj91#e;o_We}e1|IPFZD zUqx=6YS}9<3j11uV(R`~bW0dZ!P_u1KP{o;ySu(fKLQ2Pyn#3D+mIj#4KmjtZ+}g; y8f2wGt~JP84N_>34sZyv1FCivtO`0ZQb{h-? zy!240J(LG}Rq;~NgNGgkFTME}7+m}pqzxzq!8aQ=@!-II^Ua&@{g}7!D0LhQeN|PN z;F<`3ES^aFp*VfFpWYp!#6y-zA-GKnYRkg~ggm|22x(!6&}Z<0CRKGcx4vrBs%Fuu zGMM)qrX&=3@jLy59&d?7xffrp?nauRcoVb}suHmo-Ug$mRXL373VK>~Wvigu6l%3tW_<(b7m? z=CUR{5~Fkf4|V8m@yFxB0xonJA=bFd&}$Tqg^^yZ_oacQQpUaR?Ft=`0tSsBAR1C(>qmd0Dw1X_So;3~AELJNf<27o7 zOyxH|qmCXXk+TkHTQH6CbCb_Y@@!3CGYw|yxw7$6Uoz_18mpF!yk%zV zQ*(N*m@Kd&7Msk~YDv7x<8$=-l7h|NJ=`u=feDr z0JF*i*i{zck8(xnNH_yK!fUxB!%lEU=_tM^p7#Q$1il6L#x=U4i7be2-G*G^jQ;|b z;BXD0TRTewsH1cYKC8E=1|4;Jag;0g3>*-qah~Q7ae?>SJ2O5}?h3hP zMT^5mr~#R9f=ciZ(GN%A>ofp?$W>Confluence: fetch_page(url) Confluence-->>Router: page(content_markdown, metadata) ``` + +### `project/qa` reasoning flow +Назначение: оркестратор планирует шаги, а каждый шаг исполняется отдельным graph. Retrieval вызывается поздно, внутри шага `context_retrieval`. +```mermaid +sequenceDiagram + participant Runtime as GraphAgentRuntime + participant Orch as OrchestratorService + participant G1 as conversation_understanding + participant G2 as question_classification + participant G3 as context_retrieval + participant Rag as RagService + participant G4 as context_analysis + participant G5 as answer_composition + + Runtime->>Orch: run(task) + Orch->>G1: execute + G1-->>Orch: resolved_request + Orch->>G2: execute + G2-->>Orch: question_profile + Orch->>G3: execute + G3->>Rag: retrieve(query) + Rag-->>G3: rag_items + G3-->>Orch: source_bundle + Orch->>G4: execute + G4-->>Orch: analysis_brief + Orch->>G5: execute + G5-->>Orch: final_answer + Orch-->>Runtime: final_answer +``` diff --git a/app/modules/agent/__pycache__/module.cpython-312.pyc b/app/modules/agent/__pycache__/module.cpython-312.pyc index 8912070a687c30b1f59df4d566202f9d1871f13e..82e7d21722dcb75acc0a8504c56a3b57b5962a65 100644 GIT binary patch delta 1766 zcmZux&2Jk;6rb5|@7m5+<0Pa`T|1#{Dx4-or4d!3O+#CiMp0T2wS-*b-2|ic2ea!4 zmyKMcAXTCq${ZqkL_!F;5(oYSZgAoP(MlE^=z*IdL6r~(-do!VRLp9B^X9!bU%&U> zv?hL>jDH`C=>*!XpS~#l8F>&-m~>B|gzONDSkxh9nxhnD!4b-0P7Gzyk;-!J!S#=u zU>IEa9&t_tuG*^LZtzs8RCEpR+o(z2J|uQ`QMWzF_j z$=qnrdKN*sv?!^{Q2O z?3JQ?urz`Bgun)L?>7Hh+-%C;K;P)gEr=<$EyWw^N`&~buk?xEQoWJBzSR@9G!HKv zHedC%NA$ou;zxXvH~Lm@qA&SkeFRCz?5DES;{BQE_luHxJ zJ+e+Mc{|z)tq^*_r#FbH@|V@+(f`cEB;t@b{7dyi^(zXg&t~|XHk!t`L9MB*R~=ax zyI88JyDSMETjXzRFVJ&*N4tFSB~)O-1PTPB&MJXqm5QDzFg#fxK_ER-WVi-+gmugo zVc=p>+)kx{jiJcA5~>#YmB_TPFYsn$tQi*BS#UCSHU}LWLNH@V5~HrwpRxNU0IZSVHStt|_+i)ghLs@)eABtFEDJ6Qwy^W4c| zAnR6Gp|FUQc_{i~`<%WNpM3B|1o1^sbh#jkZ~7p?EGUBDId`)u>cIWx%r|Gw%sKPT z{VqSqrN2t05)9kX&+gS9IJeVTpN%3M-(dj@c!M>0iF2~GhSt7AG0k;9oP z8k6RXtq2Y2YD~_?7@dx)H(FC) z>zmt#oI0HB8v}k|Z<%uDA=@_(OS@%Bszclc<9^GQN5>}XTYdXJpLjcRZZe}!V!JXo zVY&LoIGb_$THn2|4Xgvc&&ny~nkr9eU4UW+iZ5aoP9QB-)J69yv zR0l(LWVz9<;0fRq`S=JSsk$&}kXO4_F-56|y9hVgzNbF5UQt=QcqX&AaCR^6@2Bkf z#4Y1}FP2bsTy}J3jAR$zPe(QE=g*sRa(RIz#}#6%X53;mj-v4<_n`Ip%glGwm-f{n zrX}9$icrD{4EMkI3Q1LT-hImF>%I{eF-p{gtiD<9puQp<7s4z$`5Iir4$dHi^u7u= zAy34r`q{b2pHy@1wF`9G$Ur^H_?1LPP_If~j~7K2w7U|kwMr4l^Nm6QD|FXAZ#_ZO zb@jcwm>Fu^eSv?wv^Hd9d_KF+)ZgyhHa#%k1riu!38<=&CP)!b<$<0VP-wVJK+)jB-e3908m4ssK;D?mfm+>K!l7)9NGdltl`T7S+ezqlr*RS#NEG zFs**`R-TCkPVX{K^FR6j<*YU1KgrW@9bHIi;Z=m8&N=^qE&Rw%st-~tQ-3%Gp8cI6 H#7X`GgBB~k diff --git a/app/modules/agent/__pycache__/repository.cpython-312.pyc b/app/modules/agent/__pycache__/repository.cpython-312.pyc index fdbb0a279c3bcf0df716c9d20f8e2db75f07c9f4..2e9c44444d8e9b710d5bbefc3ffecd0999889de3 100644 GIT binary patch delta 4087 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 K^%(+MM*je!HN33= diff --git a/app/modules/agent/__pycache__/service.cpython-312.pyc b/app/modules/agent/__pycache__/service.cpython-312.pyc index a030cfbf2968a9c498dc1e965051188c83925a9d..1ad28af27d36f7dc96042fd8077b68978ca14fd6 100644 GIT binary patch delta 8600 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 KIP*z*+5ZoQ6e?)| delta 7746 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 S;ZT|VppLZvPC&?~>i+?*5LL|p diff --git a/app/modules/agent/engine/graphs/__init__.py b/app/modules/agent/engine/graphs/__init__.py index 2821b8a..2564438 100644 --- a/app/modules/agent/engine/graphs/__init__.py +++ b/app/modules/agent/engine/graphs/__init__.py @@ -1,8 +1,13 @@ __all__ = [ "BaseGraphFactory", "DocsGraphFactory", + "ProjectQaAnalysisGraphFactory", + "ProjectQaAnswerGraphFactory", + "ProjectQaClassificationGraphFactory", + "ProjectQaConversationGraphFactory", "ProjectEditsGraphFactory", "ProjectQaGraphFactory", + "ProjectQaRetrievalGraphFactory", ] @@ -15,6 +20,26 @@ def __getattr__(name: str): from app.modules.agent.engine.graphs.docs_graph import DocsGraphFactory return DocsGraphFactory + if name == "ProjectQaConversationGraphFactory": + from app.modules.agent.engine.graphs.project_qa_step_graphs import ProjectQaConversationGraphFactory + + return ProjectQaConversationGraphFactory + if name == "ProjectQaClassificationGraphFactory": + from app.modules.agent.engine.graphs.project_qa_step_graphs import ProjectQaClassificationGraphFactory + + return ProjectQaClassificationGraphFactory + if name == "ProjectQaRetrievalGraphFactory": + from app.modules.agent.engine.graphs.project_qa_step_graphs import ProjectQaRetrievalGraphFactory + + return ProjectQaRetrievalGraphFactory + if name == "ProjectQaAnalysisGraphFactory": + from app.modules.agent.engine.graphs.project_qa_step_graphs import ProjectQaAnalysisGraphFactory + + return ProjectQaAnalysisGraphFactory + if name == "ProjectQaAnswerGraphFactory": + from app.modules.agent.engine.graphs.project_qa_step_graphs import ProjectQaAnswerGraphFactory + + return ProjectQaAnswerGraphFactory if name == "ProjectEditsGraphFactory": from app.modules.agent.engine.graphs.project_edits_graph import ProjectEditsGraphFactory diff --git a/app/modules/agent/engine/graphs/__pycache__/__init__.cpython-312.pyc b/app/modules/agent/engine/graphs/__pycache__/__init__.cpython-312.pyc index 55518beb101f1a0e8c4b374e2fe1bdc9961d7bfc..435ee9fbe4f072c40f73f81707b7e2670f87012f 100644 GIT binary patch literal 1441 zcma)+zi-n}5XYZ?Bz{Snx8@s8&al*4(6e*S} z#2>&`78b;U82Ce2EVCdBOO}|}D$3LWaql^Gh!AR@WZ(Hd@1F0zzT|VcTtu)QzkP50 zmJs?*AC4E_a}LMB*+&gzpm*E`GB}gl;tl?lxWRV>MN|sW$SvJ9?-9MdeOIr0Ez*fb z?zHOeKxFa(X+1UT-a~!G(d~|F4dM%bVfTee268WD@V2eHuC-;=byU@&KI(#8&#oXu zu)!OGjhdX#aj46!QzB+0XY)QNUKnlJDL}FwlBWLVlv*cqSn31y?IcE&^h`+p~5krP>sN+hQQEKpBj=+eQF3DONdiL#MGyTWOwQb>Jf z9}jp zbEdbD)fOeiI7nW_{GuXr%hn>(JlN=S<+MN)!r z{|o)MRHi!<+@+uAVz!R1LM7Fn>{TXvHI==Z%3jqp%dtF7Q`?=Wq-hP))1e4WBV%yT zFkJ2@xb2~@QYnyULTM((_Y-+Kz|;SU)IzBiV>^+T0=&fbEQiu^jGaWT1-Ql{)ljO& z*h=KZ057tL8cJ%6w-dP%;0lXWLa7qttwdf3@B)h{p`^t4Ng~e&c%DV(LTN6>8;Lv{ z;Ms4*@i(Jiic^6&bqFP+HARRdP19{#(;~t32y5vBBIf~;QMeu98>uum$pk#opNLEU bSq}t`<9_nR62B)MOkC!sU)O&jFf8*AQ|OK; delta 374 zcmZ3;y^)>oG%qg~0}$kgEzGoIp2%m;$THDQb>e%&$qtOtObrZ^V;K`;*>c%ySr{2= zSb`Z!1VO4npoU>JBZSq&sLAD5#02EL1PN#|g4spP5H^S^Vu7$hR1qtL4W>A6X-)27 zR+#*g$#HTwvneCX~wisQ?S^b<1`Qi?LmQu7oN3kno~GRdjM z#rhzL^whi(eW0LzZhlH>&g4iI11^4`nVdje96EV2i{4~uR!3Km&`O3P0U%Yx2PA%R t*nrii+7*caxuDP}mH-kTm>C%vKQXa#Gc~Y(5D;XPpOE~80Z4&$0ssSwQwab7 diff --git a/app/modules/agent/engine/graphs/__pycache__/base_graph.cpython-312.pyc b/app/modules/agent/engine/graphs/__pycache__/base_graph.cpython-312.pyc index b8ac2260d417a13da80066377c3e922f9ad47cd2..efb8c1f1d9e0232c1dc71b8822cf8310b78653f5 100644 GIT binary patch delta 177 zcmaDZ(;&-xnwOW00SG$VmSom!879ZbkRmX-kW-aWaPo9cZAPKVhdGb)Yck&A&dE=YPtMOPNv$Z^+{I8c&f}Zsav3o)o}YYyyN~&@pyTFro;8e`CP2kS8Xy8>c98{$ a5C#!~K;jpNO>TZlX-=wLk^N*FJ{JH^QX%sI diff --git a/app/modules/agent/engine/graphs/__pycache__/docs_graph_logic.cpython-312.pyc b/app/modules/agent/engine/graphs/__pycache__/docs_graph_logic.cpython-312.pyc index ce3f7852c97facbe3ba327205cd5a880722b4956..3cec23fc0047bf8e43c098aedc28f2a091c8e07d 100644 GIT binary patch delta 3787 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 delta 3404 zcmZ`*4R90J5!RD*lKd~*vLp)wGP1#k!4A~s&lsCuV*_%`j~zll24Z(oZ$nHbLpsn-mb|ZCXg0w3LKENIR5J(%pBm9cX(V zf7;vk-roN2`{8Z&tADWUUuR{doA3!-ej(~yeKPwayOeG=nf5_{$g5>Lb};jCGwe$% zmRym76K*({7LYs zlsELGA!Wh;TlbiDF>8|$-;`4<O#8MowgH+A|}+ZMG$d(+y64 zp=~;neMQic?}EAhwQLIfXiOoT^PfYXK2f}QHmRgbYIiJ}~6DrS;JmT3tkp)dXwck&2O7{Yc8$hTuc+orw$_dDd=XWdKiR&8?IZaTS#;JXAOdA?874+sts zkn6dK#H&O-NANlVhB1~1C;53|TqGDExJ2+0f#?@*qFe+p2qaK_8)2sjF3cKW0LRA` zYCowv#*kbw`y=0FT+=g9cqFv0Cmx9@a>|qsVd>nTG)Bm({w^;UAL%$oz~)u-V?5ET+(bV zC( z-^hu~70E5Ci1PZrKn_otlhD@O#aiGO&7b0Qse{)a-p=-FH7x}a&E(crSt^UzPsp&d zwOkq>DM#wH)?)93AP`rLix&8hi>f-t17RCmsd{qiP{uq3PVJQsTg?@ZmulZCW`NYf)t){ z(jJk-bn)Od1T+NX{NaSq9pPkB>FQ3Z{3I^vX(<;g5#CJu&k$2u$FVL&(d(J*o#A+g z5)xsc>WAkaEh(iws%IF58%iphu_)+tJ+WO6)!Dt zd5%llv*sx&LpU(yI(MXD^j#9-BlyPLpx2QE{jfEnR`nyP+uFH;R}kvCY;bo0QDQaC z(3vKQpAIgEfYM%cg-G(FZd=^k&@g|gZto5Acr@ODjy&gT%Jo?#}Y*0&x)^% zTqXRY+f$;&Br&uUx_v=A`k{FJ87%L`#`H)f3>wv;r(J zIJ;qmH8;pTP_Xf^bGUp`iSfq95!fw1*f;^oW1|Zm2(i%f4-ksU9uIy;Pp5esGOijT z7E|F!Yz<3;d$E(HmbAj(yM6nj&qXh}%ieL9{YIXBVa9+vc*(u|9ryA9ck9y)H*N4? zS8d+-xo*>&?u@x5mNx?okteiC@pX(Hh36C3y$Q0C>`mPcQ@0c!9PFOXF2ZlS*D?=O zKYnGM8CBvIg7iiRs^~g88fCB z7yD#5(p>=G<1Y3$ILbG$0A#6G*naq2y)amdr;?f<V&EUI6-fM%c!Xh;kHB0_S_| z?7H^Lp2dtcfvb0$JP)_Y7UR0Gg^qL~Ja8LEPG7yyAhFpCS9|lB9sb=rs;-gFn#JAk zzB>W6LfzgK<{K5R8Uj-`)f+b1Nuh~@63urR?A|;pdp8~%9^89NUE7Qq{R#0!jekkh zKMC#;ILU7#2}%e=Go(q(ee~)eioD0keq+efhaVi`f1-T^)Uj16tDUABW~(%DKXh(6 zl3ySNUJ2bW;m-xvE&91p0lvPlgHFgU{yyyJyYnrfx|f6tJz-Gf1K+yinAw$x^u&}| Vyb4}@qAjP@%BFo`BII~s zgN54K4<_0|4Q-DEG)=%nqok-cn>1jQM59RqCbp4i+3*34A2h^dLWv0mjrS~6FkzDU zoipd$IdkUB|IU?eQ*&N*I3$i8BX7JP|Ge@u=chLEOX{-iVeg2k&Lws_75JsGK|$ zNz%k&wae&GmAIitlA7Gx(RE1fjj8&1lFrf+v5u}puc00_VANhn8sNPB7Ky$SqpP{h{!MMTIh+z*vjS;^?>*PCzPF7uOyqjKFR*2_5YdKcK(s?e?!34a-912j zE6GmqGq%G9Uy$9~>1!wTsf)fJd9pfnvYzL>JQnYjH5$iQ zAI9wWMPTcovJp?vV=%L-mTZArt9FU6vb;`GO{-&k-TySOkX>;j+jCkPm(J8pd#`2J zEflUdmvIjNB4>3t7cC-c;*OJxJOw|O%C3B_;4sleR&Cb1cB=zg%TCy^)ez4z7@^$JKIh*;fF4y-foZQh5QY6@k* z4z`tqC7*Dk-&wf*gW?G0 zk3(@)QFVXm<5Ut;dsQ}Yb!iK#Mn)}6_l;Gjy>H>|9=sr#$8Ra~p z5-qM9G>ogO5HBM-7(!VOl|%1Om_^f0_IF?s>PX7RG^Uxfh01+&3*OX@X{k3sdt@yR zXCoz~6TXc^#V=W0zXJb6CQ26U#W&b5V)Mx6vDc=Xui4iv8 z>U?!RFYdsStuR|t>3#_d3wlzE8xMK}VU0$91G;AV36wgmMr=oPBVI?miNN~=88#(f!et9@G4jVi{#l|9vyny({y<3lgP<{8Mg%Jo|1nYdr`b%DKTN!*V=*S)1mYZ`c$+R%dP4QELR0Xpd3l(8O^8jvW>uDR@*FKz;^vre2%fbS`?pUb#gu82%-%UflT#^a1h-n>@_#g!w_eTz?9j}mc3_=($@T+ z=I=Rb&3{+xNY z%%Ep2+3oVOX;tGhJiX3Kd!(v)ei56;1|@UfD#?^8r%V$` z9O1&umk3&Sb+25*Dr5^o=J0;zy#xXOE@49@jQH)s8O+bXoBl?QJ-Q$L?~1b+c?~1zYF~g#QV7gD#M7+KY)XB2lU#eCmR3n%pA(moh% z_8N~dx5($lvzduU=fYxhmfnM!;PTD^tP3CxAu@>5h}RLQwa8CQj%QOcU?2{E1^cV( z_)dB$P$FoTrS-Bc?SY{db5Dp@Uow;kGt2wd(Uit1$0y3=Xt5k^N#*3NKN}iLJhLT% zn9QXu=&49;LusoMzHF&0XL{kMmL}Sf|GULb=>k|oHS{t(8FCq73Tj*JG%qg~0}yn!Ey-NJk=K}oF=?_R%S)XU#x=aFnL#oP47JQ9j1U$BLk(jJ zUkd*kW|$l!LyExUAXZgI!O0b@+KfVzm#}`}*JQlKos*v)pPZjpl3G!+c?O#qBjd@* z=h>S$FZiWj5LCV_n6Wv8V+y0D9?;w(O%MTcMUfGR5C#!~K;jpNO>TZlX-=wLk>zAT GE*AhBE-T0Y delta 123 zcmaDSa#e))G%qg~0}#}_o|oCNk=K}oF>bOW%S)!!Op^^*MFn|N_|`D5W(KKXU|?iO z;h!AFs>&!ZxrSAnMbNKGeDYd0@yUl+Z*5-9X2!^Pc=99mCg#h6j+=8irZ5WV0`(MW VfC!KkMTV0Fxg;1ZCTnrI008mK9|`~f diff --git a/app/modules/agent/engine/graphs/__pycache__/project_qa_step_graphs.cpython-312.pyc b/app/modules/agent/engine/graphs/__pycache__/project_qa_step_graphs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a5d88ec625e02d96663a03cca89bd02c44d39a0 GIT binary patch literal 13410 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 Z5Ixzkg|2!;LG)yRUQIiGpdca={Xab}m#_c; literal 0 HcmV?d00001 diff --git a/app/modules/agent/engine/graphs/__pycache__/state.cpython-312.pyc b/app/modules/agent/engine/graphs/__pycache__/state.cpython-312.pyc index df2056d4c3131370db30943b98c3d5a4499b6265..79a799fcb9b32ed4e2741aff6af4efe3564a520b 100644 GIT binary patch delta 411 zcmdnP{hNpPG%qg~0}yW7A_+Pm0^X-$fNNUKzzAWwp5q`MYvWa5UWyIQ)Tlf#tgSEi)(e7H@HWX;E@&d{Sv%3Xsc}n3tGS zS)5rMpH!5Yns$pPF|W8hwFt&b%LGctL&P>mGV?PszM7oLGC^5P>N1Pe4G1YLewju5 zhKR&v7Ks}|VwYLOZU~89W)YqIgT dict: @@ -294,7 +294,7 @@ class DocsContentComposer: f"Examples bundle:\n{state.get('rules_bundle', '')}", ] ) - raw = self._llm.generate("docs_generation", user_input) + raw = self._llm.generate("docs_generation", user_input, log_context="graph.docs.generate_doc_content") bundle = self._bundle.parse_docs_bundle(raw) if bundle: first_content = str(bundle[0].get("content", "")).strip() @@ -369,7 +369,7 @@ class DocsContentComposer: f"Generated document:\n{generated}", ] ) - raw = self._llm.generate("docs_self_check", user_input) + raw = self._llm.generate("docs_self_check", user_input, log_context="graph.docs.self_check") passed = DocsContextAnalyzer.parse_bool_marker(raw, "pass", default=False) feedback = DocsContextAnalyzer.parse_text_marker(raw, "feedback", default="No validation feedback provided.") return {"validation_attempts": attempts, "validation_passed": passed, "validation_feedback": feedback} @@ -379,7 +379,7 @@ class DocsContentComposer: bundle = state.get("generated_docs_bundle", []) or [] strategy = state.get("docs_strategy", "from_scratch") if strategy == "from_scratch" and not self._bundle.bundle_has_required_structure(bundle): - LOGGER.warning( + LOGGER.info( "build_changeset fallback bundle used: strategy=%s bundle_items=%s", strategy, len(bundle), @@ -452,7 +452,11 @@ class DocsContentComposer: ] ) try: - summary = self._llm.generate("docs_execution_summary", user_input).strip() + summary = self._llm.generate( + "docs_execution_summary", + user_input, + log_context="graph.docs.summarize_result", + ).strip() except Exception: summary = "" if not summary: diff --git a/app/modules/agent/engine/graphs/project_edits_logic.py b/app/modules/agent/engine/graphs/project_edits_logic.py index b48245f..661e302 100644 --- a/app/modules/agent/engine/graphs/project_edits_logic.py +++ b/app/modules/agent/engine/graphs/project_edits_logic.py @@ -48,7 +48,9 @@ class ProjectEditsLogic: }, ensure_ascii=False, ) - parsed = self._support.parse_json(self._llm.generate("project_edits_plan", user_input)) + parsed = self._support.parse_json( + self._llm.generate("project_edits_plan", user_input, log_context="graph.project_edits.plan_changes") + ) contracts = self._contracts.parse( parsed, request=str(state.get("message", "")), @@ -165,7 +167,13 @@ class ProjectEditsLogic: "changeset": [{"op": x.op.value, "path": x.path, "reason": x.reason} for x in changeset[:20]], "rule": "Changes must stay inside contract blocks and not affect unrelated sections.", } - parsed = self._support.parse_json(self._llm.generate("project_edits_self_check", json.dumps(payload, ensure_ascii=False))) + parsed = self._support.parse_json( + self._llm.generate( + "project_edits_self_check", + json.dumps(payload, ensure_ascii=False), + log_context="graph.project_edits.self_check", + ) + ) passed = bool(parsed.get("pass")) if isinstance(parsed, dict) else False feedback = str(parsed.get("feedback", "")).strip() if isinstance(parsed, dict) else "" return { @@ -192,7 +200,11 @@ class ProjectEditsLogic: "rag_context": self._support.shorten(state.get("rag_context", ""), 5000), "confluence_context": self._support.shorten(state.get("confluence_context", ""), 5000), } - raw = self._llm.generate("project_edits_hunks", json.dumps(prompt_payload, ensure_ascii=False)) + raw = self._llm.generate( + "project_edits_hunks", + json.dumps(prompt_payload, ensure_ascii=False), + log_context="graph.project_edits.generate_changeset", + ) parsed = self._support.parse_json(raw) hunks = parsed.get("hunks", []) if isinstance(parsed, dict) else [] if not isinstance(hunks, list) or not hunks: diff --git a/app/modules/agent/engine/graphs/project_qa_graph.py b/app/modules/agent/engine/graphs/project_qa_graph.py index 6dead1d..1f6f005 100644 --- a/app/modules/agent/engine/graphs/project_qa_graph.py +++ b/app/modules/agent/engine/graphs/project_qa_graph.py @@ -33,7 +33,7 @@ class ProjectQaGraphFactory: f"Confluence context:\n{state.get('confluence_context', '')}", ] ) - answer = self._llm.generate("project_answer", user_input) + answer = self._llm.generate("project_answer", user_input, log_context="graph.project_qa.answer") emit_progress_sync( state, stage="graph.project_qa.answer.done", diff --git a/app/modules/agent/engine/graphs/project_qa_step_graphs.py b/app/modules/agent/engine/graphs/project_qa_step_graphs.py new file mode 100644 index 0000000..f8059bc --- /dev/null +++ b/app/modules/agent/engine/graphs/project_qa_step_graphs.py @@ -0,0 +1,172 @@ +from __future__ import annotations +import logging + +from langgraph.graph import END, START, StateGraph + +from app.modules.agent.engine.graphs.progress import emit_progress_sync +from app.modules.agent.engine.graphs.state import AgentGraphState +from app.modules.agent.engine.orchestrator.actions.project_qa_analyzer import ProjectQaAnalyzer +from app.modules.agent.engine.orchestrator.actions.project_qa_support import ProjectQaSupport +from app.modules.agent.llm import AgentLlmService +from app.modules.contracts import RagRetriever +from app.modules.rag.explain import ExplainPack, PromptBudgeter + +LOGGER = logging.getLogger(__name__) + + +class ProjectQaConversationGraphFactory: + def __init__(self, llm: AgentLlmService | None = None) -> None: + self._support = ProjectQaSupport() + + def build(self, checkpointer=None): + graph = StateGraph(AgentGraphState) + graph.add_node("resolve_request", self._resolve_request) + graph.add_edge(START, "resolve_request") + graph.add_edge("resolve_request", END) + return graph.compile(checkpointer=checkpointer) + + def _resolve_request(self, state: AgentGraphState) -> dict: + emit_progress_sync(state, stage="graph.project_qa.conversation_understanding", message="Нормализую пользовательский запрос.") + resolved = self._support.resolve_request(str(state.get("message", "") or "")) + LOGGER.warning("graph step result: graph=project_qa/conversation_understanding normalized=%s", resolved.get("normalized_message", "")) + return {"resolved_request": resolved} + + +class ProjectQaClassificationGraphFactory: + def __init__(self, llm: AgentLlmService | None = None) -> None: + self._support = ProjectQaSupport() + + def build(self, checkpointer=None): + graph = StateGraph(AgentGraphState) + graph.add_node("classify_question", self._classify_question) + graph.add_edge(START, "classify_question") + graph.add_edge("classify_question", END) + return graph.compile(checkpointer=checkpointer) + + def _classify_question(self, state: AgentGraphState) -> dict: + resolved = state.get("resolved_request", {}) or {} + message = str(resolved.get("normalized_message") or state.get("message", "") or "") + profile = self._support.build_profile(message) + LOGGER.warning("graph step result: graph=project_qa/question_classification domain=%s intent=%s", profile.get("domain"), profile.get("intent")) + return {"question_profile": profile} + + +class ProjectQaRetrievalGraphFactory: + def __init__(self, rag: RagRetriever, llm: AgentLlmService | None = None) -> None: + self._rag = rag + self._support = ProjectQaSupport() + + def build(self, checkpointer=None): + graph = StateGraph(AgentGraphState) + graph.add_node("retrieve_context", self._retrieve_context) + graph.add_edge(START, "retrieve_context") + graph.add_edge("retrieve_context", END) + return graph.compile(checkpointer=checkpointer) + + def _retrieve_context(self, state: AgentGraphState) -> dict: + emit_progress_sync(state, stage="graph.project_qa.context_retrieval", message="Собираю контекст по проекту.") + resolved = state.get("resolved_request", {}) or {} + profile = state.get("question_profile", {}) or {} + files_map = dict(state.get("files_map", {}) or {}) + rag_items: list[dict] = [] + source_bundle = self._support.build_source_bundle(profile, list(rag_items), files_map) + LOGGER.warning( + "graph step result: graph=project_qa/context_retrieval mode=%s rag_items=%s file_candidates=%s legacy_rag=%s", + profile.get("domain"), + len(source_bundle.get("rag_items", []) or []), + len(source_bundle.get("file_candidates", []) or []), + False, + ) + return {"source_bundle": source_bundle} + + +class ProjectQaAnalysisGraphFactory: + def __init__(self, llm: AgentLlmService | None = None) -> None: + self._support = ProjectQaSupport() + self._analyzer = ProjectQaAnalyzer() + + def build(self, checkpointer=None): + graph = StateGraph(AgentGraphState) + graph.add_node("analyze_context", self._analyze_context) + graph.add_edge(START, "analyze_context") + graph.add_edge("analyze_context", END) + return graph.compile(checkpointer=checkpointer) + + def _analyze_context(self, state: AgentGraphState) -> dict: + explain_pack = state.get("explain_pack") + if explain_pack: + analysis = self._analysis_from_pack(explain_pack) + LOGGER.warning( + "graph step result: graph=project_qa/context_analysis findings=%s evidence=%s", + len(analysis.get("findings", []) or []), + len(analysis.get("evidence", []) or []), + ) + return {"analysis_brief": analysis} + bundle = state.get("source_bundle", {}) or {} + profile = bundle.get("profile", {}) or state.get("question_profile", {}) or {} + rag_items = list(bundle.get("rag_items", []) or []) + file_candidates = list(bundle.get("file_candidates", []) or []) + analysis = self._analyzer.analyze_code(profile, rag_items, file_candidates) if str(profile.get("domain")) == "code" else self._analyzer.analyze_docs(profile, rag_items) + LOGGER.warning( + "graph step result: graph=project_qa/context_analysis findings=%s evidence=%s", + len(analysis.get("findings", []) or []), + len(analysis.get("evidence", []) or []), + ) + return {"analysis_brief": analysis} + + def _analysis_from_pack(self, raw_pack) -> dict: + pack = ExplainPack.model_validate(raw_pack) + findings: list[str] = [] + evidence: list[str] = [] + for entrypoint in pack.selected_entrypoints[:3]: + findings.append(f"Entrypoint `{entrypoint.title}` maps to handler `{entrypoint.metadata.get('handler_symbol_id', '')}`.") + if entrypoint.source: + evidence.append(entrypoint.source) + for path in pack.trace_paths[:3]: + if path.symbol_ids: + findings.append(f"Trace path: {' -> '.join(path.symbol_ids)}") + for excerpt in pack.code_excerpts[:4]: + evidence.append(f"{excerpt.path}:{excerpt.start_line}-{excerpt.end_line} [{excerpt.evidence_id}]") + return { + "subject": pack.intent.normalized_query, + "findings": findings or ["No explain trace was built from the available code evidence."], + "evidence": evidence, + "gaps": list(pack.missing), + "answer_mode": "summary", + } + + +class ProjectQaAnswerGraphFactory: + def __init__(self, llm: AgentLlmService | None = None) -> None: + self._support = ProjectQaSupport() + self._llm = llm + self._budgeter = PromptBudgeter() + + def build(self, checkpointer=None): + graph = StateGraph(AgentGraphState) + graph.add_node("compose_answer", self._compose_answer) + graph.add_edge(START, "compose_answer") + graph.add_edge("compose_answer", END) + return graph.compile(checkpointer=checkpointer) + + def _compose_answer(self, state: AgentGraphState) -> dict: + profile = state.get("question_profile", {}) or {} + analysis = state.get("analysis_brief", {}) or {} + brief = self._support.build_answer_brief(profile, analysis) + explain_pack = state.get("explain_pack") + answer = self._compose_explain_answer(state, explain_pack) + if not answer: + answer = self._support.compose_answer(brief) + LOGGER.warning("graph step result: graph=project_qa/answer_composition answer_len=%s", len(answer or "")) + return {"answer_brief": brief, "final_answer": answer} + + def _compose_explain_answer(self, state: AgentGraphState, raw_pack) -> str: + if raw_pack is None or self._llm is None: + return "" + pack = ExplainPack.model_validate(raw_pack) + prompt_input = self._budgeter.build_prompt_input(str(state.get("message", "") or ""), pack) + return self._llm.generate( + "code_explain_answer_v2", + prompt_input, + log_context="graph.project_qa.answer_v2", + ).strip() diff --git a/app/modules/agent/engine/graphs/state.py b/app/modules/agent/engine/graphs/state.py index 8492114..72e297c 100644 --- a/app/modules/agent/engine/graphs/state.py +++ b/app/modules/agent/engine/graphs/state.py @@ -25,6 +25,12 @@ class AgentGraphState(TypedDict, total=False): validation_passed: bool validation_feedback: str validation_attempts: int + resolved_request: dict + question_profile: dict + source_bundle: dict + analysis_brief: dict + answer_brief: dict + final_answer: str answer: str changeset: list[ChangeItem] edits_requested_path: str diff --git a/app/modules/agent/engine/orchestrator/__pycache__/execution_engine.cpython-312.pyc b/app/modules/agent/engine/orchestrator/__pycache__/execution_engine.cpython-312.pyc index 48a46a27d1cb522ba54b4729855f3b7e76cd4ef5..bbbbd32abb9d2a3764ff62d4c5e61f8541d36c62 100644 GIT binary patch delta 3628 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 delta 1875 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 diff --git a/app/modules/agent/engine/orchestrator/__pycache__/step_registry.cpython-312.pyc b/app/modules/agent/engine/orchestrator/__pycache__/step_registry.cpython-312.pyc index 83b7e297b5818f3c24edfe781986400dd776fd16..75d92fb9cc6ce7bd2a0928278bf3efb55b280475 100644 GIT binary patch delta 5133 zcmZ`-du&_RdB2yum+v<(9}*>!qDYFA9a)wWE0$ziiY>{pYst3!NR_!PO{NlL!= z%BeI8bh;E%3vaD(R@iPASn2w3kPgcbU~mR(ZO~-P`apG~4%fJF5D!JszlJe!w_yHj z-*+kM;ba8(o$vh4Ip2AGuXBHW_?zb(zqH#e6#Ty4^TWaq+E*Mw`X5%h25U5xr}Bo9 zxonxXl&#a&vTfQ{wolv3j%i2PIqfXFrd?VeW653iOnWrmRPvU&X|C*>_R$n?5DXy0 zT=JI#(*dY6yk~~ygM3R1Om|s_bh=eXjZ=BcD^%Y4h7OkjSZG0)rfn<#9Cz5ye(}_3 z=E$+pBPX7p7+?9w?=z~NBeQva^s95FY@srelM1sHapivoEv{3-Y?04Nr?VtYAFZWn z)xWLd8D0_!{54)U-w zyc?k10CBiX4^(=zo?hU3HIA#Y#XfC}uga1v!m$^)wm#V0~*%?+>pk# z0(VH`LRCX?SmWA&dk#5$d(}YPtR^rF;F#7N0q&T_MS(l6aUH;YLE~aoV{rmF&hSGT z*i)o~@#>AmGaKdEjq*H}yNtybnzyl4<~#qIg|SrTsV25SrQ>9VP4xx&ukyKhiOMM5k(tZpu8_ZEch_2SrK~6xE-!42o99Ke8a0rc zEtSAxHe1Bmc_GJ(Y9L$5mKGMbrfN2{HfCzBAX^b%;)Tp5u&m3fKQ~*Rn-#YPX|1&I z*4wO!COBnOZTAQ=RrS~8dCD-F+(Hmi)tm$c<=J079w4piIrV^fVA=cgJ3{u=pD zQwyhBn!CN46>*W#Xp~sZ-}Q%p7e4~azD_*|q}RHK*8?NVHu9!9QG4J^toyo_Ep;Z< zW4Uh&-8!qZ_sZ?Pbv@J{+B#RyDv4n^FBWBjTX!!CL{PrMo7#bl05-xhQ+; zZmW_$DW^}?-H3Ql^rF)ByxjGC-HQmf9q}RJr|jvw-AebQ+&x(jAQGhPiPfT#9G8>h z^%g{0DSKqqphWxSXn#F~NE>DESWPOiLAe$ithXZ+-rgyKNEAgUmF^RA_lbH3A~6(w zSxFv|lSe-8L?pf`2;?LXNmBOs>UkwGA}2=bU5KP`R9;Dr$jOm<8j)^1AxVkslOwf# z^<4<<#@%yDdR$JA*PlY9hq8CB9#!H8<@mw+9z=RkG+f7FIdQnY7m>a#LHp|a5P5n# z@(d#TDSP`ZUJ3Wf;l6r5BF|Fx*y=M%=aAevRI49AXaM(-P+|jeY@j}f$k6s-2N5}h z)1TYWhQo*q)7yuQ(1<+;Sm)}f5LN{ke& zY5gRfdh0vZi!^@j+RykP&16-`fzLscDlvn52ljv12na^#dF zK$e_dt@Jv)I>}DLjtK1_Zoijy6V=g0??m=HyJ+u@KBrna@*`(EYg*D(X$%^{VgP1) z;+Ho_K@#$}5tqwNw~?6ZSk$B%rL1@*Q^;?b9q=YG$5*mKVU}EX1+5oAj_^e!)8ubl zy(y>4;2_~efK-Q&oymwic)ZyPh!HL#$X?5q=6Uj8t}s1G9PU=yNz(3)$=4a-Fp8Pd z`iN3i;#H;~@nsPrGunAnIEL**Ko&z&+ah`oINOHn)JC0$4&ookZFkfY4Bau^HYvfh z984?0J#uglx#zB#+-A3BnXPl=u%|C{9J=Q=5)WW0$^idA!<69sTQ@x2G=yF^hKPi2 zGa)cy5Z3zcATK{H`A90%P8@ze337hgLq>dFQsQh@bWhq3qA|##br6ev9AiPK%fzh1GWkhLK;2S|QGdnNM%}Zk1+_>{IFr;zu=7!iS{DQE6;3ASQ zAh`+z1A#bSlA5j`wW6sP6XYKQ3CM8a@~6n}12=4(;_8xJU3c~CuH9rgSZ(zx&Xnu~ zCv;bQb8Ou?xMmuxTPc5${$KJi7$bkuV!sd@OVdA392o7P-VM{EyP0<*4Dy{LW@vbC zHw~5ddY&Ci>i;FF2V_SoiTl<}_%Sd$Q%UlU#|Ov!tCpm#d4rlU0!-32ucw(j9;Gfq zR9(`8oi;|m_3HIS6X-~54Q?4&kp09D4dTXr25x+cO6w+sFGD@e3Rj>INHYyL9i;x^LtI-}ukYthgWz?ONxa`h=p*&(X_A?}t0?ly8@n zaK9YxzsLO4^tNd|JhD9c?fA{qO6r@Fbv^cb=;W4d!toqTl|xt1{B5T00)+h?=dG7E zQ~c&DMzl(s>3e=Nwf`EumB3f&#yNN7byrewFy|0x&b3wM^)2KbM*!X*TCW3OXEx0l zI|(>io8)SCg)|**&#C_j+ceE31~TbWAzzxFqGqTi_VXtI9e5RV0J8dGK=Ty_a$3i2 zTK%6oQ1a|*oCv@R`Lh&-#WV*0bmg^SBve9fC!vn3wJ;>))C{??nNmC`2^YwlYfyU(@W zJic=L*0no}w-?vB?&Z<@U`$J^{vSuaA9>)9ttAHTJ^j<6w}(BQ8>)<|DciY~& z6=sK=*wwaX3JDls-@4UupEmymaKa5B)X^pWzLSia8@WFh)SWFj~pAB7$2P)J)1Uc%O~_g_9fvq_WCZ8 zn?O7wI_$~Atv2ENP;H_En?tm%gA#w9NKKQq zqe0;f0T%LVd!qI{*3Tiq=palWLANfT77GV~yiREs)7ZHeQ6#PK7n49w9E#ux)>gdRc!%1P)BM!8;EgdTXHqB=K{ zGyF2tlf9G^(1)uAuu*UZ0!xmE`wd#k@D%w?I7$9CY>J>~-}VmG-lkT>Y)I_n7vX~j zJRo&94yl#lZc1XqJ>a3MaWMTJK<7)m*aInO&&E5lXY-+%xz;DRu`7cYqUn!nK-(Ux zXy+jO2xmei&<^OaX{aIYkns0_;fEm+PZt6dO`rRaiv5mqd`QJVq@us4CO_4gXx*n2 zkdKl4w}+z7(H~Kh|Dkt%$eeg+cCUFm*F5pn{M~)871lg^WOMIz!>5MhI@6~1?7c6MiXXWuWs_Py&JCrunDN@(IZb{yM4nnZ0(fNhe^vO9KnvtFCo zF^wY|Cp1z^iJ<8fN`&~~N+6{QD2V(3{oJ_nMAz73>(iI<#)`=lx_JkwdIf*+gS4?2qw-n;~O z9jVlI)Uw~|c&%U+OzpPCF{sJwVB2>yfjNlTIg5+bq zKu+1W-C-Pd%t&4$2)`xk!ue=kae$vf@2$d9pn+Eea@x5)7Nly3LYP8?LKJ|!=v)aI z&eF_cv!d4Kn=OeqxR%_QA&ahlTZ>LZe&+gt%L_{D?{Ez+A#%>0TQ#}V4(Eou=fmXunk@9+b!XP5_1vhI8#QgvH+dw+Z#=Ij_G*c}rkyelB=+9;vfekO^$nQQi z-H7U416tRB>7tAqiHS8{Pwvx_`%IBC9wesL@_PD!mOfy5fmtQaWhPr$m0`5DHB6tcI}j&8`E-QW*23;>8PrnAJg(1U{VVz-vqZT3-SHIBr>&5$llY0{LLpi~lxnYAN&_MnzMXr?HWrc*;a4r#eVW`;6Z zl+3oL*+^tMN<5>-hqU<6My8-=hPBM_M!H{5@7L1%H?~Zz~|6OXNSEZR{9M-P!IvgYj?6> z>wkfq2yDXwdB>k1!+{{_&xOga1EEhb@n8shKjCvR7`NM380&ey=?_Nf{rXg^@@j~@ zAB~g01-&F43lbr;7u(5bC_>JKqOLE1yFzn{@)Y@2Xs}l_IJMk5uN(u)@F?ZkQVa4R zT**?cN{mpvcZ?diYOSIgLRnF3Gv$ifGT4PhRdJJ0I3-A&LX)8~K}N#e{C=wLB4@)% zo{nt&iChUM$uGj+ba)+p=Swy-Kn_NR@Gx13bTMzRysd5 zJxtmcaErmHhE^aA(Tv1h-lImz?XF(pibcuW!7wGs(U|D!gXOITNr>hUnu0)*tFdnK zFNn*pVtwQviJ<55fJ&ca+cj&sxMLBRDoBne91e^G1nJA#T&n+}<0A?2E4B0b&Pj*L@J&z9$W;~TP~oTj8}R#r=u`KBs2Rl~Ja{VHEm zXeBWm?J@}7$H>+0iB;tcunN7w%2^5}0Lj@t_9K*-05F91O6TPzrAEm*g%Jwp01Upf z*)numG+QROnqu9WQQ$5nA1{tOO7aD{vPhol5&7eE*hNz6xf%^#7q5xBPtttS%_n{t zxaS*w+jsa4hCJw5-W&Md%C}c^Z&LFn*UsJ@*2hcQcxk;hzwS-m^Dex^ZTLdEFRA&G z>)zD5BV~#xQpf)$CwkN5cCWZ|*Hj$;X3vw88FcpvJelI|?%=3CG`15AZ>2DF-pYJ# zs)zku4-3ql+nEh)o1q&gjZ}iXl1s0C5qj2%7~Yv$v)m|Eo+~$J<(8~UZY%oA9C#Sc zTC3J>!A(zmWm@?vC27nJVf^!B$EPMvPE1RD=hab~Rm#sR z7pT_~>ZPHcUsU0NT6LryurVO)M=PA;; z=%Feu3YP&s5*qN&=vnx4bOv7hCvD0L;2=53hyA(L19ZR?g z6H1fQYd|=af1eVpiObIK6JhGBOgV7iUdj&pg@WOo!Jv9 z+HpS8uO1IGdpnPvnVsDkUYTEG#Qi6i%Pzq4kDWhBzcYBv{m<#2!Pj*`63!4&IAaj~ zBDqv`#t7H|xC*clunBM#U^8G7U<+U~U@Kq?U>jg7V7pi=+5kI5yXe^XIYrltW|kl! zmLvAt_;SHl4Sdy$KKOE9bVM7pdiL62{nBmnw$XRSfF{n9 z8MIt~*xyJvB9e0*f#!@Dg2lHf+=pR!8Q`!SIqgW*x zf!6`DenY{@_U~$UkXz=(DxD3PycW?aT0|T8XiSj}W}nfabDMC+i%Zu!OUhH^mTS?v zslnw2huxxOL;6qyU+SlZP>txlRkLW+IXRVU8uV3Y@-Lb<=*DLZuvV@9RSD;bSUZn2 zF5OolUGif}h}KPLmORbPNXdd`IG@m5$y_D_A91;mC`ezhtmWI}Rkmi?LZZxPog3c` zE6^x{UI3aaC1oX=$i&Yl;um_N4$VSybA@y^rP;*XY$BbFr$x;U3nV33#<3&5u^LFQ zSFD$Yh9UIN396&|YV2}s)zPx-Xt{a%j-%_fJ->?mJhs|9w%j}Ro_MEsQuPH^eXYyB z)|*FGd{M<2UFxv~h}#2ZK&Z%i7i`XTMm+WGrfJ56yJKS?xr4;cUTcjS9g7y$;s`q+ z4`J7=5O)*%w7#AFuC=L5^N3zfs}Y^dYi=Mul&l3wSOT6@)iVaslOoR;=LySK!*1Ex zbL}BJPpxC~hM8*iu%VW1cla%SI01E{hieI-mT>)|N!L=^Ln4TNuFC-Xvmxhb5F5D# z!FeNlVrPKmx}0}e7#5p2y;*EP-RwVI&1SLX)|N#h>-L6?t%WKE`NO)~Z6=-+n1Jk7 z&^7?M*IvrKlws2b$c}(+S0#kZ*~&AgwxnPoMJkeJ+XQU~e!3OGHUw}21+6ie%U+PE zoG7Go+4x*mgmEE}6(JppjfX2}k-KPGz4Ww{oGVBbsS!`nME*&-1JoAVA!8OWK_O*! zef1KWhe~oTb3qd0R60K=$%SYY?MCBz5R?;F3veQun9L+(IX#_Da*Kwvo4tvrMUETK7)rVpGcmAlPcwlfswg%MvNdevO$d?RSTR&Li|f&nzK^L ztA>gIc}iv^vWd(^IV~4G3njbDQ^EU~T_S4k(kOlw9G})|5?T2viGmfg`J9}_7#C{} z@i{Pgb1qz=^e&MFE~7a&Cb`z5bT4jfAA$h{y8vi`O%)LLgq+Jl`r+N=qz91cAObv# z^bmp|0viH10L`4p=cTNunPHCr)5Ay^5rjhmJpx0>s1VMC5gY}e`O2P(5=zSPTvo<1 z5rKy`Bj0`i#WpBpjowY$I$=^y7}!qRLkpd%2j1;!Lqu&jsD^eZ!A`Xy{BpyM2Bo#{ z_4(EQspbBum4*{a{mHdz!4+5+tStU3tt!5{ zf_^pBrv&$ID7bmx_3_m`$CvjUUuk$msh_M^KN|MhziV2H36Wl+|W^c{sM%~Z0n6f3NIGa_g>#Fmz^P`fwt*Rg1E;ZbyhKJPV-5VN}ZT)YARtHZm z51w2JpHf1Pg3+ka>aBsGd24lBI#fTrD7>JCV`^l#(%f4%sbrDzc=CnQD`8QYo>`sF zEl=l^{Ij1!HW(f<{0q^f;Zq&0LwO)mhBZM5pD?Jw=9h|N zU+Z_dKR4Fd>}y?u$A7hWx%fg`vl1Enb>AQMygT&PkTNm5ayYAu@BYjfvYb@pKYV~yQNy1N16 z{~RC-ZudVIzYke+ou5l&(uIrqauomXMG3_wJZjg=?;EJ}_XyrZa33Y5HC`I1CkMDm z*d^U&9}XNNds*Ay_;5e!BeV+9Mpb_c#+oZZ3+d?uben~Xd5I$1J@xz`TO8a%RQ8v_ z+>W<(E%>hv;=+4_cVJ9$UN!75Vt8X z^ji?kPlg(gzKgmr+EG8{PQQl}+;cwS?(-3k0Uu$oi*`^PnU755;NRJ9HZfcW7l}07 zOnTY#!!1u>NGWIL3-HidDY~!E{t-vKM?&|tsH24Oy|k^F@x1KW?1(x p{V3K-8b3hc76jNxePNA3!p~)-{ZwHq`#QX*jAVp-CUC^r{tHCUg<=2z delta 1887 zcmb7EO>7%Q6!zHOwbzalr*#r1P2z->Hk)6PG-^oWq$GC!2u_>yM-6R^$u6#oQ>U|= zR<(jCQn)}5R8JHMD%3)#TDeqhEri6CN*p*<;XM4-pu?C?8$E> zYC#;8;HVr&RoV_51ups2uoVV294`LA9=V)9F_`kdFG-q5^8)MG#T2;OK3L`}Fe<>| zNVB{Cf)^S~8eJut1gY9;RtkS~)XQ4g-C)55585Nu<^KxaQIIzly54a@uCLO!%(R`$ z&Sl401&(+^`?g~XZaZ?up@tGxU_O`4>hosOFjJUnY5kneQ`zMCRPsWn>VcfHB72bZWk<2iSH&Pvp*U}g;+P}~^T6X0m3iUuu84>9 zQ#Q)+LwT(l@D?PSO`3S$lq+TAU9wCiRIva&sEc6TkLxO&WeK=e)yiIk@2lGP+;Q?P zUY24kEi9yXCa+t{%%;mbp)}movLBPH9)5uM_Y%-<{AGfJ1pNek1W{ND_xhXBHJa>l zeho(@6rs6(g2jqU^{p&8LxIN#P7s`g2a%n%o#4df-W< zdT@+l1rhJFL<9$7V+E$;$H(I*lGE|SQ`1LnN`hPzdK(U~uHs_Dq(_~gRId_D61+x` zAUI5LgkYK=2EXik>fe%xPr+K#9P5Dz^(5zbAf`kTNZ`Cx_j76v1R|2|D(#2gWqj3 zywN{_4A=UNp)GDahnrZz^ckJMojI3W$Z2|(2og8Dfy(n62sfgk;5N;TzCDWevSjh^ z=zTXEfO|vLJ)csZT?negYss%8_>5qHfG(Ip2eg5HJVJB*FxVWC7PF~yS!ftORntn7 zZ3NrhrFGciYr~h{ea>eqaDFM3&6t;xX}sVDpTcOBaSs`v&EqB|_=4a|g6AX=+{99Y zURXg;fX$LlQ@(y?9!8qOl40tL7vW)SjCH}&*yN#Z{D>RUtE0N@4e4370x520-b&%q zV_sU+IfWS%M0xGBbYt^%_-NL+b4%t&@Krq5atG%yo0p8s&Q?I5)4btA*jn8OgCn~c z!2HONpG3A?hROK=+#d?>jMFYH?q_AgilPBIMlEQMo znagEuazZ+pi#kODDwf|P5UGS1rSS7;2b7Oh2gUi@zTkb(H&$Qs2%}dV>rO>F#MUb- qr8HZANtPy@>%n4vY&T;M;q&n@`v$%pZz@kX*%15FAq;QCh5rDz&8G4I diff --git a/app/modules/agent/engine/orchestrator/actions/__init__.py b/app/modules/agent/engine/orchestrator/actions/__init__.py index e220f9b..ab47e51 100644 --- a/app/modules/agent/engine/orchestrator/actions/__init__.py +++ b/app/modules/agent/engine/orchestrator/actions/__init__.py @@ -1,13 +1,17 @@ +from app.modules.agent.engine.orchestrator.actions.code_explain_actions import CodeExplainActions from app.modules.agent.engine.orchestrator.actions.docs_actions import DocsActions from app.modules.agent.engine.orchestrator.actions.edit_actions import EditActions from app.modules.agent.engine.orchestrator.actions.explain_actions import ExplainActions from app.modules.agent.engine.orchestrator.actions.gherkin_actions import GherkinActions +from app.modules.agent.engine.orchestrator.actions.project_qa_actions import ProjectQaActions from app.modules.agent.engine.orchestrator.actions.review_actions import ReviewActions __all__ = [ + "CodeExplainActions", "DocsActions", "EditActions", "ExplainActions", "GherkinActions", + "ProjectQaActions", "ReviewActions", ] diff --git a/app/modules/agent/engine/orchestrator/actions/__pycache__/__init__.cpython-312.pyc b/app/modules/agent/engine/orchestrator/actions/__pycache__/__init__.cpython-312.pyc index 69605654dde8b7ebea4566436cea8ad20e308a58..265f76644868545a2157f3df0a8179795562d1b7 100644 GIT binary patch delta 298 zcmey$dYzr`G%qg~0}wpZU79J*Jdsa=F=3**qkRfP3R@0WE_W0+BLk4lp2L&N8^z1W zkj{|8v4}B>uaZ-f>m^8^CgUw3=lqmZ*NTFi#LPU$_x&rRRBF& BQyu^S delta 159 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 RouQ~M>iX!nNN&iw`ae>)5%2&2 literal 0 HcmV?d00001 diff --git a/app/modules/agent/engine/orchestrator/actions/__pycache__/project_qa_analyzer.cpython-312.pyc b/app/modules/agent/engine/orchestrator/actions/__pycache__/project_qa_analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..551195d15b2f6bc4f9d162e03e9fb754d3ae3b21 GIT binary patch literal 11166 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 Oh+~uNZN|uqe*X)g4fJ^c literal 0 HcmV?d00001 diff --git a/app/modules/agent/engine/orchestrator/actions/__pycache__/project_qa_support.cpython-312.pyc b/app/modules/agent/engine/orchestrator/actions/__pycache__/project_qa_support.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6671b30e6379f20aaa984c523632d3f58f11632 GIT binary patch literal 13388 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�>_hEe%jS=JyCA|oKTtR3U6V_U|eW5<)bUR#=RcjO#7hm-$|qC7Ej zR!W(jR`K0*%7dlDvXx)ce4r1g@5k2^_&+Fla1P}e2t=_;;p0KEkG518A3|WxlzoG( zAoii3s(baVO%)>7KEZ}A#ccSzSAiG-LFBtA&Rhma@+M7ZGqi7tG2Eo$Ua z&#V;Ygp~-er>TyQl4p>>hYv<^AfpfA_(Wn3UC~)lEuYG{J2t-j4qKWDf*&#mCF`sk z^^SPcOPb+@IJZnX;e`wtZ#rv-&x}4d@?7#>xvoieHUm^!Kepzo_lh_9u-veDvf=(z zP_FG5vXUR8ziPf>PBy1j%IOY^km3;KFmlR5GH=KyGrXz>y>hqNeuMw`(7mgCHW+Fx5;SzFcRhb}$@ z5tVXJxp#bG+wrLi0sfkp%aApeMlTKKM}Kww9zI@O(emUV5)wj*nI zT^_hNFjgbmm#0n37aEczJg^)1*%y&{i7yTN4)Mo%oX4631X<^+c99>@7DK!YQwPj4 zW(zP4^DvFTG?k@k9z+sMTbVQogqXHhCvCFwU}45Gl?T5PUqHE68zaXi z{WDUOijGvSqCay2dQdTt7L4`(e;ip1T^27;!p5Mo@Z3c8!$?91Je){~ef42` zU-KevH!R%IaLAVPT+!hyx{)V|>bytLUgYzxT#0F}>-1VhGHq1eUexhUmA)jtEVVb*&f>4mlMN7ZX-(3xw5T=i_DM)^lh;qKV3F zL1)K!?eNyI!&5HYaOhCC;H~e^Y#8)4jf9&u!i?_*6FzN{L^^)6`E5 z?MW%%$32XERD~E=K`0xC^y(mTk(FoE$cnfU<#F%V9>4(5r7cLn+A|u?eO5DMyJ>gI z&JJitXNPR>n6$@#vG*UG|5X30`gB(${p?x!;DEe0o;Jl7a&bY=wSr=)mOEJ*RzVp( z0!~cwoCvp4)4;(+)OL!)C=*GLVb6}>r5H&>`9q=n9;V5iV??h8AiiS3nCD+p7^Hf1 z>W^J)ueuz+7*98BoiJjIH=H+Cr0obu?p4P`1;U&XM#?o;ua^{a)Ao-Ey zLN$eO&+X3Oi~D^My4pVwUELpuu6|)?tn9I*;hVMJs~gk_#z8$hpoXdNePb~cHDrWc zWrD;kbF7V{Vs}C>nBx_2-}IN3>2UV}$GAkHB$zd100ql!DOgJ=&`mH~r%2}*>ct7# zTI_g9G4%zae)3fn{j*_zUtr-$8bp6FYB1max~NI;3+=C>`H+`t+7tc(aVsd5F(sN7 z><&6dVn;7!WY|R_LZ_yA8P=NIG7JavPj_aVt*QuPepGqMyc-ou#(?cQ$X_fQVb2@9 zCF66$cV_k_8y~dy3Q)_9Q2YV%aL(wi-N5fX$V(trVqi`>+sC_xw~n@rw7t@v?3!|| z%Q)LrH~PYo<-979bgHN5L{-)?)wVFHwgtIrqK6XFZ;BbqAw~T?rzG(S0=`ZOTOKM_ z{lbn8)e3nSka)x5Yjh6f(*Gd&>Kym)Hv6z{)HGrmb0;^;j=Lru8*pA_Ynrxg$=J5Y zwtH?C5bBaEWyhLH$NF)nY}+tx+nlj&mTj#?g!<%e*|B!g(KHTt^R%rsV{4UdTM40h z)IH)JyF1w}S9vF^TE-upcC=<3t*OHk-LhlXq~pMiM`has)3(lxty8u=GGo@)Scc3q zHm=GEN3gp73r=Hh9NNp2N$a?G%Gvsfy)}2XG@_r;qZCI!b<0(}{S2=?b#Thr{)s(* z_Ep4l)C}2F*<84^g4rloO%7N}bL4XQ+f(0AEps4gyA$N z8W1kkF$rP3Q~{X&G+k100;RYR`+QovNI zD?!zmgRTQrqk2rV3lvmoL<;yb(rcelj&U>F!q{3~0u)#pb|CG1cKRwFp`3>j;#pOi zS?G+|$8Pqij7>bvxLdKQTT?BRtbiB8A36KvQ2^tO3pl-axJ>F_`XLf%X=lxF@93$K zQ%UdmqjK%W$=a={-LiArwDZ1<^FG;mKdy?_)Q=t+Ir7R=ICXVYu|DBp215`h;XG`&`}Mr zg1$3uVAo4p;>H#=1Baas{No1p?SpYel2)-mWQkGfOfXt7IrjiwYAWYevJ>F=(giD7 zdXwM{ph)|Wusgn^caPlt$_A{Ny>{BZ0!x!zC)?Lg+6Tr5CJv-q_8@oT%=CdLGY6hb zAMwivx&cTZ52r<>18LL1ZR}05ic9xDA}R>BFZ7i#6`Hw`584v2fB9KjsY2WX(RMYi zTkb3^zivI6vzk)~Go0l`J?5IHd{{$wim*m5b|DmEY5tS1EokU8D;F`o7g|rtBF4HH z@liU(UYX14Yot?r4N$BRb6+$)C+66{>};U9+)Csw<(5(Le+_!f@K*!K&aRI&(D0kD zZojhq)gAxrY8tPeay3o5_N1IYcmLG=vxbRiX44)71T)UATx|Ki`+4#EYOhFO(0!$| z(K-EE18|wWUc4;)SJVj?60*phApNm0K+pxBY>UqM{zM`gGYt3%0v*vizH7F zN!CoUTqmBL3nd|n&JrW40=3wY1<=zfZed^|;odT{?x8ow(DlPahqdXU+>a??BADd} zU(v!fMsS+#PA0;cu)b{_@o!KOy)4#Yz0CE8gl)O zA1>nhLu!c###gU(i%pb!ijp8D None: + self._retriever = retriever + self._intent_builder = ExplainIntentBuilder() + + def build_code_explain_pack(self, ctx: ExecutionContext) -> list[str]: + file_candidates = list((self.get(ctx, "source_bundle", {}) or {}).get("file_candidates", []) or []) + if self._retriever is None: + pack = ExplainPack( + intent=self._intent_builder.build(ctx.task.user_message), + missing=["code_explain_retriever_unavailable"], + ) + else: + pack = self._retriever.build_pack( + ctx.task.rag_session_id, + ctx.task.user_message, + file_candidates=file_candidates, + ) + LOGGER.warning( + "code explain action: task_id=%s entrypoints=%s seeds=%s paths=%s excerpts=%s missing=%s", + ctx.task.task_id, + len(pack.selected_entrypoints), + len(pack.seed_symbols), + len(pack.trace_paths), + len(pack.code_excerpts), + pack.missing, + ) + return [self.put(ctx, "explain_pack", ArtifactType.STRUCTURED_JSON, pack.model_dump(mode="json"))] diff --git a/app/modules/agent/engine/orchestrator/actions/project_qa_actions.py b/app/modules/agent/engine/orchestrator/actions/project_qa_actions.py new file mode 100644 index 0000000..570680d --- /dev/null +++ b/app/modules/agent/engine/orchestrator/actions/project_qa_actions.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from app.modules.agent.engine.orchestrator.actions.project_qa_analyzer import ProjectQaAnalyzer +from app.modules.agent.engine.orchestrator.actions.common import ActionSupport +from app.modules.agent.engine.orchestrator.actions.project_qa_support import ProjectQaSupport +from app.modules.agent.engine.orchestrator.execution_context import ExecutionContext +from app.modules.agent.engine.orchestrator.models import ArtifactType + + +class ProjectQaActions(ActionSupport): + def __init__(self) -> None: + self._support = ProjectQaSupport() + self._analyzer = ProjectQaAnalyzer() + + def classify_project_question(self, ctx: ExecutionContext) -> list[str]: + message = str(ctx.task.user_message or "") + profile = self._support.build_profile(message) + return [self.put(ctx, "question_profile", ArtifactType.STRUCTURED_JSON, profile)] + + def collect_project_sources(self, ctx: ExecutionContext) -> list[str]: + profile = self.get(ctx, "question_profile", {}) or {} + terms = list(profile.get("terms", []) or []) + entities = list(profile.get("entities", []) or []) + rag_items = list(ctx.task.metadata.get("rag_items", []) or []) + files_map = dict(ctx.task.metadata.get("files_map", {}) or {}) + explicit_test = any(term in {"test", "tests", "тест", "тесты"} for term in terms) + + ranked_rag = [] + for item in rag_items: + score = self._support.rag_score(item, terms, entities) + source = str(item.get("source", "") or "") + if not explicit_test and self._support.is_test_path(source): + score -= 3 + if score > 0: + ranked_rag.append((score, item)) + ranked_rag.sort(key=lambda pair: pair[0], reverse=True) + + ranked_files = [] + for path, payload in files_map.items(): + score = self._support.file_score(path, payload, terms, entities) + if not explicit_test and self._support.is_test_path(path): + score -= 3 + if score > 0: + ranked_files.append( + ( + score, + { + "path": path, + "content": str(payload.get("content", "")), + "content_hash": str(payload.get("content_hash", "")), + }, + ) + ) + ranked_files.sort(key=lambda pair: pair[0], reverse=True) + + bundle = { + "profile": profile, + "rag_items": [item for _, item in ranked_rag[:12]], + "file_candidates": [item for _, item in ranked_files[:10]], + "rag_total": len(ranked_rag), + "files_total": len(ranked_files), + } + return [self.put(ctx, "source_bundle", ArtifactType.STRUCTURED_JSON, bundle)] + + def analyze_project_sources(self, ctx: ExecutionContext) -> list[str]: + bundle = self.get(ctx, "source_bundle", {}) or {} + profile = bundle.get("profile", {}) or {} + rag_items = list(bundle.get("rag_items", []) or []) + file_candidates = list(bundle.get("file_candidates", []) or []) + + if str(profile.get("domain")) == "code": + analysis = self._analyzer.analyze_code(profile, rag_items, file_candidates) + else: + analysis = self._analyzer.analyze_docs(profile, rag_items) + return [self.put(ctx, "analysis_brief", ArtifactType.STRUCTURED_JSON, analysis)] + + def build_project_answer_brief(self, ctx: ExecutionContext) -> list[str]: + profile = self.get(ctx, "question_profile", {}) or {} + analysis = self.get(ctx, "analysis_brief", {}) or {} + brief = { + "question_profile": profile, + "resolved_subject": analysis.get("subject"), + "key_findings": analysis.get("findings", []), + "supporting_evidence": analysis.get("evidence", []), + "missing_evidence": analysis.get("gaps", []), + "answer_mode": analysis.get("answer_mode", "summary"), + } + return [self.put(ctx, "answer_brief", ArtifactType.STRUCTURED_JSON, brief)] + + def compose_project_answer(self, ctx: ExecutionContext) -> list[str]: + brief = self.get(ctx, "answer_brief", {}) or {} + profile = brief.get("question_profile", {}) or {} + russian = bool(profile.get("russian")) + answer_mode = str(brief.get("answer_mode") or "summary") + findings = list(brief.get("key_findings", []) or []) + evidence = list(brief.get("supporting_evidence", []) or []) + gaps = list(brief.get("missing_evidence", []) or []) + + title = "## Кратко" if russian else "## Summary" + lines = [title] + if answer_mode == "inventory": + lines.append("### Что реализовано" if russian else "### Implemented items") + else: + lines.append("### Что видно по проекту" if russian else "### What the project shows") + if findings: + lines.extend(f"- {item}" for item in findings) + else: + lines.append("Не удалось собрать подтвержденные выводы по доступным данным." if russian else "No supported findings could be assembled from the available data.") + if evidence: + lines.append("") + lines.append("### Где смотреть в проекте" if russian else "### Where to look in the project") + lines.extend(f"- `{item}`" for item in evidence[:5]) + if gaps: + lines.append("") + lines.append("### Что пока не подтверждено кодом" if russian else "### What is not yet confirmed in code") + lines.extend(f"- {item}" for item in gaps[:3]) + return [self.put(ctx, "final_answer", ArtifactType.TEXT, "\n".join(lines))] diff --git a/app/modules/agent/engine/orchestrator/actions/project_qa_analyzer.py b/app/modules/agent/engine/orchestrator/actions/project_qa_analyzer.py new file mode 100644 index 0000000..0374f52 --- /dev/null +++ b/app/modules/agent/engine/orchestrator/actions/project_qa_analyzer.py @@ -0,0 +1,154 @@ +from __future__ import annotations + + +class ProjectQaAnalyzer: + def analyze_code(self, profile: dict, rag_items: list[dict], file_candidates: list[dict]) -> dict: + terms = list(profile.get("terms", []) or []) + intent = str(profile.get("intent") or "lookup") + russian = bool(profile.get("russian")) + findings: list[str] = [] + evidence: list[str] = [] + gaps: list[str] = [] + + symbol_titles = [str(item.get("title", "") or "") for item in rag_items if str(item.get("layer", "")).startswith("C1")] + symbol_set = set(symbol_titles) + file_paths = [str(item.get("path", "") or item.get("source", "") or "") for item in rag_items] + file_paths.extend(str(item.get("path", "") or "") for item in file_candidates) + + if "ConfigManager" in profile.get("entities", []) or "configmanager" in terms or "config_manager" in terms: + alias_file = self.find_path(file_paths, "src/config_manager/__init__.py") + if alias_file: + findings.append( + "Публичный `ConfigManager` экспортируется из `src/config_manager/__init__.py` как alias на `ConfigManagerV2`." + if russian + else "Public `ConfigManager` is exported from `src/config_manager/__init__.py` as an alias to `ConfigManagerV2`." + ) + evidence.append("src/config_manager/__init__.py") + + if "controlchannel" in {name.lower() for name in symbol_set}: + findings.append( + "Базовый контракт управления задает `ControlChannel`: он определяет команды `start` и `stop` для внешнего канала управления." + if russian + else "`ControlChannel` defines the base management contract with `start` and `stop` commands." + ) + evidence.append("src/config_manager/v2/control/base.py") + + if "ControlChannelBridge" in symbol_set: + findings.append( + "`ControlChannelBridge` связывает внешний канал управления с lifecycle-методами менеджера: `on_start`, `on_stop`, `on_status`." + if russian + else "`ControlChannelBridge` connects the external control channel to manager lifecycle methods: `on_start`, `on_stop`, `on_status`." + ) + evidence.append("src/config_manager/v2/core/control_bridge.py") + + implementation_files = self.find_management_implementations(file_candidates) + if implementation_files: + labels = ", ".join(f"`{path}`" for path in implementation_files) + channel_names = self.implementation_names(implementation_files) + findings.append( + f"В коде найдены конкретные реализации каналов управления: {', '.join(channel_names)} ({labels})." + if russian + else f"Concrete management channel implementations were found in code: {', '.join(channel_names)} ({labels})." + ) + evidence.extend(implementation_files) + elif intent == "inventory": + gaps.append( + "В текущем контексте не удалось уверенно подтвердить конкретные файлы-реализации каналов, кроме базового контракта и bridge-слоя." + if russian + else "The current context does not yet confirm concrete channel implementation files beyond the base contract and bridge layer." + ) + + package_doc = self.find_management_doc(file_candidates) + if package_doc: + findings.append( + f"Пакет управления прямо описывает внешние каналы через `{package_doc}`." + if russian + else f"The control package directly describes external channels in `{package_doc}`." + ) + evidence.append(package_doc) + + subject = "management channels" + if profile.get("entities"): + subject = ", ".join(profile["entities"]) + return { + "subject": subject, + "findings": self.dedupe(findings), + "evidence": self.dedupe(evidence), + "gaps": gaps, + "answer_mode": "inventory" if intent == "inventory" else "summary", + } + + def analyze_docs(self, profile: dict, rag_items: list[dict]) -> dict: + findings: list[str] = [] + evidence: list[str] = [] + for item in rag_items[:5]: + title = str(item.get("title", "") or "") + source = str(item.get("source", "") or "") + content = str(item.get("content", "") or "").strip() + if content: + findings.append(content.splitlines()[0][:220]) + if source: + evidence.append(source) + elif title: + evidence.append(title) + return { + "subject": "docs", + "findings": self.dedupe(findings), + "evidence": self.dedupe(evidence), + "gaps": [] if findings else ["Недостаточно данных в документации." if profile.get("russian") else "Not enough data in documentation."], + "answer_mode": "summary", + } + + def find_management_implementations(self, file_candidates: list[dict]) -> list[str]: + found: list[str] = [] + for item in file_candidates: + path = str(item.get("path", "") or "") + lowered = path.lower() + if self.is_test_path(path): + continue + if any(token in lowered for token in ("http_channel.py", "telegram.py", "telegram_channel.py", "http.py")): + found.append(path) + continue + content = str(item.get("content", "") or "").lower() + if "controlchannel" in content and "class " in content: + found.append(path) + continue + if ("channel" in lowered or "control" in lowered) and any(token in content for token in ("http", "telegram", "bot")): + found.append(path) + return self.dedupe(found)[:4] + + def implementation_names(self, paths: list[str]) -> list[str]: + names: list[str] = [] + for path in paths: + stem = path.rsplit("/", 1)[-1].rsplit(".", 1)[0] + label = stem.replace("_", " ").strip() + if label and label not in names: + names.append(label) + return names + + def find_management_doc(self, file_candidates: list[dict]) -> str | None: + for item in file_candidates: + path = str(item.get("path", "") or "") + if self.is_test_path(path): + continue + content = str(item.get("content", "") or "").lower() + if any(token in content for token in ("каналы внешнего управления", "external control channels", "http api", "telegram")): + return path + return None + + def find_path(self, paths: list[str], target: str) -> str | None: + for path in paths: + if path == target: + return path + return None + + def dedupe(self, items: list[str]) -> list[str]: + seen: list[str] = [] + for item in items: + if item and item not in seen: + seen.append(item) + return seen + + def is_test_path(self, path: str) -> bool: + lowered = path.lower() + return lowered.startswith("tests/") or "/tests/" in lowered or lowered.startswith("test_") or "/test_" in lowered diff --git a/app/modules/agent/engine/orchestrator/actions/project_qa_support.py b/app/modules/agent/engine/orchestrator/actions/project_qa_support.py new file mode 100644 index 0000000..d449430 --- /dev/null +++ b/app/modules/agent/engine/orchestrator/actions/project_qa_support.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import re + +from app.modules.rag.retrieval.query_terms import extract_query_terms + + +class ProjectQaSupport: + def resolve_request(self, message: str) -> dict: + profile = self.build_profile(message) + subject = profile["entities"][0] if profile.get("entities") else "" + return { + "original_message": message, + "normalized_message": " ".join((message or "").split()), + "subject_hint": subject, + "source_hint": profile["domain"], + "russian": profile["russian"], + } + + def build_profile(self, message: str) -> dict: + lowered = message.lower() + return { + "domain": "code" if self.looks_like_code_question(lowered) else "docs", + "intent": self.detect_intent(lowered), + "terms": extract_query_terms(message), + "entities": self.extract_entities(message), + "russian": self.is_russian(message), + } + + def build_retrieval_query(self, resolved_request: dict, profile: dict) -> str: + normalized = str(resolved_request.get("normalized_message") or resolved_request.get("original_message") or "").strip() + if profile.get("domain") == "code" and "по коду" not in normalized.lower(): + return f"по коду {normalized}".strip() + return normalized + + def build_source_bundle(self, profile: dict, rag_items: list[dict], files_map: dict[str, dict]) -> dict: + terms = list(profile.get("terms", []) or []) + entities = list(profile.get("entities", []) or []) + explicit_test = any(term in {"test", "tests", "тест", "тесты"} for term in terms) + + ranked_rag: list[tuple[int, dict]] = [] + for item in rag_items: + score = self.rag_score(item, terms, entities) + source = str(item.get("source", "") or "") + if not explicit_test and self.is_test_path(source): + score -= 3 + if score > 0: + ranked_rag.append((score, item)) + ranked_rag.sort(key=lambda pair: pair[0], reverse=True) + + ranked_files: list[tuple[int, dict]] = [] + for path, payload in files_map.items(): + score = self.file_score(path, payload, terms, entities) + if not explicit_test and self.is_test_path(path): + score -= 3 + if score > 0: + ranked_files.append( + ( + score, + { + "path": path, + "content": str(payload.get("content", "")), + "content_hash": str(payload.get("content_hash", "")), + }, + ) + ) + ranked_files.sort(key=lambda pair: pair[0], reverse=True) + + return { + "profile": profile, + "rag_items": [item for _, item in ranked_rag[:12]], + "file_candidates": [item for _, item in ranked_files[:10]], + "rag_total": len(ranked_rag), + "files_total": len(ranked_files), + } + + def build_answer_brief(self, profile: dict, analysis: dict) -> dict: + return { + "question_profile": profile, + "resolved_subject": analysis.get("subject"), + "key_findings": analysis.get("findings", []), + "supporting_evidence": analysis.get("evidence", []), + "missing_evidence": analysis.get("gaps", []), + "answer_mode": analysis.get("answer_mode", "summary"), + } + + def compose_answer(self, brief: dict) -> str: + profile = brief.get("question_profile", {}) or {} + russian = bool(profile.get("russian")) + answer_mode = str(brief.get("answer_mode") or "summary") + findings = list(brief.get("key_findings", []) or []) + evidence = list(brief.get("supporting_evidence", []) or []) + gaps = list(brief.get("missing_evidence", []) or []) + + title = "## Кратко" if russian else "## Summary" + lines = [title] + lines.append("### Что реализовано" if answer_mode == "inventory" and russian else "### Implemented items" if answer_mode == "inventory" else "### Что видно по проекту" if russian else "### What the project shows") + if findings: + lines.extend(f"- {item}" for item in findings) + else: + lines.append("Не удалось собрать подтвержденные выводы по доступным данным." if russian else "No supported findings could be assembled from the available data.") + if evidence: + lines.append("") + lines.append("### Где смотреть в проекте" if russian else "### Where to look in the project") + lines.extend(f"- `{item}`" for item in evidence[:5]) + if gaps: + lines.append("") + lines.append("### Что пока не подтверждено кодом" if russian else "### What is not yet confirmed in code") + lines.extend(f"- {item}" for item in gaps[:3]) + return "\n".join(lines) + + def detect_intent(self, lowered: str) -> str: + if any(token in lowered for token in ("какие", "что уже реализ", "список", "перечень", "какие есть")): + return "inventory" + if any(token in lowered for token in ("где", "find", "where")): + return "lookup" + if any(token in lowered for token in ("сравни", "compare")): + return "compare" + return "explain" + + def looks_like_code_question(self, lowered: str) -> bool: + code_markers = ("по коду", "код", "реализ", "имплементац", "класс", "метод", "модул", "файл", "канал", "handler", "endpoint") + return any(marker in lowered for marker in code_markers) or bool(re.search(r"\b[A-Z][A-Za-z0-9_]{2,}\b", lowered)) + + def extract_entities(self, message: str) -> list[str]: + return re.findall(r"\b[A-Z][A-Za-z0-9_]{2,}\b", message)[:5] + + def rag_score(self, item: dict, terms: list[str], entities: list[str]) -> int: + haystacks = [ + str(item.get("source", "") or "").lower(), + str(item.get("title", "") or "").lower(), + str(item.get("content", "") or "").lower(), + str((item.get("metadata", {}) or {}).get("qname", "") or "").lower(), + ] + score = 0 + for term in terms: + if any(term in hay for hay in haystacks): + score += 3 + for entity in entities: + if any(entity.lower() in hay for hay in haystacks): + score += 5 + return score + + def file_score(self, path: str, payload: dict, terms: list[str], entities: list[str]) -> int: + content = str(payload.get("content", "") or "").lower() + path_lower = path.lower() + score = 0 + for term in terms: + if term in path_lower: + score += 4 + elif term in content: + score += 2 + for entity in entities: + entity_lower = entity.lower() + if entity_lower in path_lower: + score += 5 + elif entity_lower in content: + score += 3 + return score + + def is_test_path(self, path: str) -> bool: + lowered = path.lower() + return lowered.startswith("tests/") or "/tests/" in lowered or lowered.startswith("test_") or "/test_" in lowered + + def is_russian(self, text: str) -> bool: + return any("а" <= ch.lower() <= "я" or ch.lower() == "ё" for ch in text) diff --git a/app/modules/agent/engine/orchestrator/execution_engine.py b/app/modules/agent/engine/orchestrator/execution_engine.py index 5d87aab..e747fea 100644 --- a/app/modules/agent/engine/orchestrator/execution_engine.py +++ b/app/modules/agent/engine/orchestrator/execution_engine.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import inspect +import logging import time from app.modules.agent.engine.orchestrator.execution_context import ExecutionContext @@ -9,6 +10,8 @@ from app.modules.agent.engine.orchestrator.models import PlanStatus, PlanStep, S from app.modules.agent.engine.orchestrator.quality_gates import QualityGateRunner from app.modules.agent.engine.orchestrator.step_registry import StepRegistry +LOGGER = logging.getLogger(__name__) + class ExecutionEngine: def __init__(self, step_registry: StepRegistry, gates: QualityGateRunner) -> None: @@ -22,17 +25,18 @@ class ExecutionEngine: for step in ctx.plan.steps: dep_issue = self._dependency_issue(step, step_results) if dep_issue: - step_results.append( - StepResult( + result = StepResult( step_id=step.step_id, status=StepStatus.SKIPPED, warnings=[dep_issue], ) - ) + step_results.append(result) + self._log_step_result(ctx, step, result) continue result = await self._run_with_retry(step, ctx) step_results.append(result) + self._log_step_result(ctx, step, result) if result.status in {StepStatus.FAILED, StepStatus.RETRY_EXHAUSTED} and step.on_failure == "fail": ctx.plan.status = PlanStatus.FAILED return step_results @@ -65,6 +69,15 @@ class ExecutionEngine: while attempt < max_attempts: attempt += 1 started_at = time.monotonic() + LOGGER.warning( + "orchestrator step start: task_id=%s step_id=%s action_id=%s executor=%s attempt=%s graph_id=%s", + ctx.task.task_id, + step.step_id, + step.action_id, + step.executor, + attempt, + step.graph_id or "", + ) await self._emit_progress(ctx, f"orchestrator.step.{step.step_id}", step.title) try: @@ -113,3 +126,21 @@ class ExecutionEngine: result = ctx.progress_cb(stage, message, "task_progress", {"layer": "orchestrator"}) if inspect.isawaitable(result): await result + + def _log_step_result(self, ctx: ExecutionContext, step: PlanStep, result: StepResult) -> None: + artifact_keys = [] + for artifact_id in result.produced_artifact_ids: + item = next((artifact for artifact in ctx.artifacts.all_items() if artifact.artifact_id == artifact_id), None) + if item is not None: + artifact_keys.append(item.key) + LOGGER.warning( + "orchestrator step result: task_id=%s step_id=%s action_id=%s status=%s duration_ms=%s artifact_keys=%s warnings=%s error=%s", + ctx.task.task_id, + step.step_id, + step.action_id, + result.status.value, + result.duration_ms, + artifact_keys, + result.warnings, + result.error_message or "", + ) diff --git a/app/modules/agent/engine/orchestrator/service.py b/app/modules/agent/engine/orchestrator/service.py index 06227d0..7b7ace2 100644 --- a/app/modules/agent/engine/orchestrator/service.py +++ b/app/modules/agent/engine/orchestrator/service.py @@ -1,6 +1,7 @@ from __future__ import annotations import inspect +import logging from app.core.exceptions import AppError from app.modules.agent.engine.orchestrator.execution_context import ExecutionContext, GraphInvoker, GraphResolver, ProgressCallback @@ -14,6 +15,8 @@ from app.modules.agent.engine.orchestrator.step_registry import StepRegistry from app.modules.agent.engine.orchestrator.template_registry import ScenarioTemplateRegistry from app.schemas.common import ModuleName +LOGGER = logging.getLogger(__name__) + class OrchestratorService: def __init__( @@ -74,6 +77,21 @@ class OrchestratorService: ) result = self._assembler.assemble(ctx, step_results) await self._emit_progress(progress_cb, "orchestrator.done", "Execution plan completed.") + LOGGER.warning( + "orchestrator decision: task_id=%s scenario=%s plan_status=%s steps=%s changeset_items=%s answer_len=%s", + task.task_id, + task.scenario.value, + result.meta.get("plan", {}).get("status", ""), + [ + { + "step_id": step.step_id, + "status": step.status.value, + } + for step in result.steps + ], + len(result.changeset), + len(result.answer or ""), + ) return result async def _emit_progress(self, progress_cb: ProgressCallback | None, stage: str, message: str) -> None: diff --git a/app/modules/agent/engine/orchestrator/step_registry.py b/app/modules/agent/engine/orchestrator/step_registry.py index 736c473..01bbf71 100644 --- a/app/modules/agent/engine/orchestrator/step_registry.py +++ b/app/modules/agent/engine/orchestrator/step_registry.py @@ -2,29 +2,50 @@ from __future__ import annotations import asyncio from collections.abc import Callable +from typing import TYPE_CHECKING from app.modules.agent.engine.graphs.progress_registry import progress_registry -from app.modules.agent.engine.orchestrator.actions import DocsActions, EditActions, ExplainActions, GherkinActions, ReviewActions +from app.modules.agent.engine.orchestrator.actions import ( + CodeExplainActions, + DocsActions, + EditActions, + ExplainActions, + GherkinActions, + ProjectQaActions, + ReviewActions, +) from app.modules.agent.engine.orchestrator.execution_context import ExecutionContext from app.modules.agent.engine.orchestrator.models import ArtifactType, PlanStep +if TYPE_CHECKING: + from app.modules.rag.explain.retriever_v2 import CodeExplainRetrieverV2 + StepFn = Callable[[ExecutionContext], list[str]] class StepRegistry: - def __init__(self) -> None: + def __init__(self, code_explain_retriever: CodeExplainRetrieverV2 | None = None) -> None: + code_explain = CodeExplainActions(code_explain_retriever) explain = ExplainActions() review = ReviewActions() docs = DocsActions() edits = EditActions() gherkin = GherkinActions() + project_qa = ProjectQaActions() self._functions: dict[str, StepFn] = { "collect_state": self._collect_state, "finalize_graph_output": self._finalize_graph_output, + "execute_project_qa_graph": self._collect_state, + "build_code_explain_pack": code_explain.build_code_explain_pack, "collect_sources": explain.collect_sources, "extract_logic": explain.extract_logic, "summarize": explain.summarize, + "classify_project_question": project_qa.classify_project_question, + "collect_project_sources": project_qa.collect_project_sources, + "analyze_project_sources": project_qa.analyze_project_sources, + "build_project_answer_brief": project_qa.build_project_answer_brief, + "compose_project_answer": project_qa.compose_project_answer, "fetch_source_doc": review.fetch_source_doc, "normalize_document": review.normalize_document, "structural_check": review.structural_check, @@ -66,6 +87,7 @@ class StepRegistry: state = { "task_id": ctx.task.task_id, "project_id": ctx.task.rag_session_id, + "scenario": ctx.task.scenario.value, "message": ctx.task.user_message, "progress_key": ctx.task.task_id, "rag_context": str(ctx.task.metadata.get("rag_context", "")), @@ -86,7 +108,7 @@ class StepRegistry: raise RuntimeError(f"Unsupported graph_id: {graph_key}") graph = ctx.graph_resolver(domain_id, process_id) - state = ctx.artifacts.get_content("agent_state", {}) or {} + state = self._build_graph_state(ctx) if ctx.progress_cb is not None: progress_registry.register(ctx.task.task_id, ctx.progress_cb) @@ -96,8 +118,29 @@ class StepRegistry: if ctx.progress_cb is not None: progress_registry.unregister(ctx.task.task_id) - item = ctx.artifacts.put(key="graph_result", artifact_type=ArtifactType.STRUCTURED_JSON, content=result) - return [item.artifact_id] + return self._store_graph_outputs(step, ctx, result) + + def _build_graph_state(self, ctx: ExecutionContext) -> dict: + state = dict(ctx.artifacts.get_content("agent_state", {}) or {}) + for item in ctx.artifacts.all_items(): + state[item.key] = ctx.artifacts.get_content(item.key) + return state + + def _store_graph_outputs(self, step: PlanStep, ctx: ExecutionContext, result: dict) -> list[str]: + if not isinstance(result, dict): + raise RuntimeError("graph_result must be an object") + if len(step.outputs) == 1 and step.outputs[0].key == "graph_result": + item = ctx.artifacts.put(key="graph_result", artifact_type=ArtifactType.STRUCTURED_JSON, content=result) + return [item.artifact_id] + + artifact_ids: list[str] = [] + for output in step.outputs: + value = result.get(output.key) + if value is None and output.required: + raise RuntimeError(f"graph_output_missing:{step.step_id}:{output.key}") + item = ctx.artifacts.put(key=output.key, artifact_type=output.type, content=value) + artifact_ids.append(item.artifact_id) + return artifact_ids def _finalize_graph_output(self, ctx: ExecutionContext) -> list[str]: raw = ctx.artifacts.get_content("graph_result", {}) or {} diff --git a/app/modules/agent/engine/orchestrator/template_registry.py b/app/modules/agent/engine/orchestrator/template_registry.py index b6554a1..3dd90d6 100644 --- a/app/modules/agent/engine/orchestrator/template_registry.py +++ b/app/modules/agent/engine/orchestrator/template_registry.py @@ -16,6 +16,8 @@ class ScenarioTemplateRegistry: return builders.get(task.scenario, self._general)(task) def _general(self, task: TaskSpec) -> ExecutionPlan: + if task.routing.domain_id == "project" and task.routing.process_id == "qa": + return self._project_qa(task) steps = [ self._step("collect_state", "Collect state", "collect_state", outputs=[self._out("agent_state", ArtifactType.STRUCTURED_JSON)]), self._step( @@ -39,7 +41,77 @@ class ScenarioTemplateRegistry: ] return self._plan(task, "general_qa_v1", steps, [self._gate("non_empty_answer_or_changeset")]) + def _project_qa(self, task: TaskSpec) -> ExecutionPlan: + steps = [ + self._step("collect_state", "Collect state", "collect_state", outputs=[self._out("agent_state", ArtifactType.STRUCTURED_JSON)]), + self._step( + "conversation_understanding", + "Conversation understanding", + "execute_project_qa_graph", + executor="graph", + graph_id="project_qa/conversation_understanding", + depends_on=["collect_state"], + outputs=[self._out("resolved_request", ArtifactType.STRUCTURED_JSON)], + ), + self._step( + "question_classification", + "Question classification", + "execute_project_qa_graph", + executor="graph", + graph_id="project_qa/question_classification", + depends_on=["conversation_understanding"], + outputs=[self._out("question_profile", ArtifactType.STRUCTURED_JSON)], + ), + self._step( + "context_retrieval", + "Context retrieval", + "execute_project_qa_graph", + executor="graph", + graph_id="project_qa/context_retrieval", + depends_on=["question_classification"], + outputs=[self._out("source_bundle", ArtifactType.STRUCTURED_JSON)], + ), + ] + analysis_depends_on = ["context_retrieval"] + if task.scenario == Scenario.EXPLAIN_PART: + steps.append( + self._step( + "code_explain_pack_step", + "Build code explain pack", + "build_code_explain_pack", + depends_on=["context_retrieval"], + outputs=[self._out("explain_pack", ArtifactType.STRUCTURED_JSON)], + ) + ) + analysis_depends_on = ["code_explain_pack_step"] + steps.extend( + [ + self._step( + "context_analysis", + "Context analysis", + "execute_project_qa_graph", + executor="graph", + graph_id="project_qa/context_analysis", + depends_on=analysis_depends_on, + outputs=[self._out("analysis_brief", ArtifactType.STRUCTURED_JSON)], + ), + self._step( + "answer_composition", + "Answer composition", + "execute_project_qa_graph", + executor="graph", + graph_id="project_qa/answer_composition", + depends_on=["context_analysis"], + outputs=[self._out("answer_brief", ArtifactType.STRUCTURED_JSON, required=False), self._out("final_answer", ArtifactType.TEXT)], + gates=[self._gate("non_empty_answer_or_changeset")], + ), + ] + ) + return self._plan(task, "project_qa_reasoning_v1", steps, [self._gate("non_empty_answer_or_changeset")]) + def _explain(self, task: TaskSpec) -> ExecutionPlan: + if task.routing.domain_id == "project" and task.routing.process_id == "qa": + return self._project_qa(task) steps = [ self._step("collect_sources", "Collect sources", "collect_sources", outputs=[self._out("sources", ArtifactType.STRUCTURED_JSON)]), self._step("extract_logic", "Extract logic", "extract_logic", depends_on=["collect_sources"], outputs=[self._out("logic_model", ArtifactType.STRUCTURED_JSON)]), diff --git a/app/modules/agent/engine/router/__init__.py b/app/modules/agent/engine/router/__init__.py index 50da4fa..ee2de63 100644 --- a/app/modules/agent/engine/router/__init__.py +++ b/app/modules/agent/engine/router/__init__.py @@ -2,21 +2,28 @@ from pathlib import Path from typing import TYPE_CHECKING from app.modules.agent.llm import AgentLlmService +from app.modules.contracts import RagRetriever if TYPE_CHECKING: from app.modules.agent.repository import AgentRepository from app.modules.agent.engine.router.router_service import RouterService -def build_router_service(llm: AgentLlmService, agent_repository: "AgentRepository") -> "RouterService": +def build_router_service(llm: AgentLlmService, agent_repository: "AgentRepository", rag: RagRetriever) -> "RouterService": from app.modules.agent.engine.graphs import ( BaseGraphFactory, DocsGraphFactory, ProjectEditsGraphFactory, + ProjectQaAnalysisGraphFactory, + ProjectQaAnswerGraphFactory, + ProjectQaClassificationGraphFactory, + ProjectQaConversationGraphFactory, ProjectQaGraphFactory, + ProjectQaRetrievalGraphFactory, ) from app.modules.agent.engine.router.context_store import RouterContextStore from app.modules.agent.engine.router.intent_classifier import IntentClassifier + from app.modules.agent.engine.router.intent_switch_detector import IntentSwitchDetector from app.modules.agent.engine.router.registry import IntentRegistry from app.modules.agent.engine.router.router_service import RouterService @@ -26,13 +33,20 @@ def build_router_service(llm: AgentLlmService, agent_repository: "AgentRepositor registry.register("project", "qa", ProjectQaGraphFactory(llm).build) registry.register("project", "edits", ProjectEditsGraphFactory(llm).build) registry.register("docs", "generation", DocsGraphFactory(llm).build) + registry.register("project_qa", "conversation_understanding", ProjectQaConversationGraphFactory(llm).build) + registry.register("project_qa", "question_classification", ProjectQaClassificationGraphFactory(llm).build) + registry.register("project_qa", "context_retrieval", ProjectQaRetrievalGraphFactory(rag, llm).build) + registry.register("project_qa", "context_analysis", ProjectQaAnalysisGraphFactory(llm).build) + registry.register("project_qa", "answer_composition", ProjectQaAnswerGraphFactory(llm).build) classifier = IntentClassifier(llm) + switch_detector = IntentSwitchDetector() context_store = RouterContextStore(agent_repository) return RouterService( registry=registry, classifier=classifier, context_store=context_store, + switch_detector=switch_detector, ) diff --git a/app/modules/agent/engine/router/__pycache__/__init__.cpython-312.pyc b/app/modules/agent/engine/router/__pycache__/__init__.cpython-312.pyc index 12f70d904921e7f2d66efc3e45ca65760ac5670a..60dc2e97a9326160a4eb3a4c30fc2b973e20e883 100644 GIT binary patch literal 3217 zcmbtW-EZ4e6t@#QPST|LXwqh1&~0cXp-Z$?+S)QG{TSU~EnCWfWkRkO-=?l?C%bmL zWs0RD@r;moU=MrP3ldZJ2k^){FR@Z7ouvp(zyoiU3Ys(tiF56^Sysjd&BO8W`S|(X z-#t0!{uB-eDfs>M@H^?}07d;mUfiF*t?a0vd`pRxNXwK$7igO39@(RK3tn6I%8bGm zSX*agpTZS5nnGS2f;d+8D}h45_W9(X(o^UGokOhX7XxAtabk}cTJnqGFPTC}>@CrS za5a*S{!TugrY+{OpfAHS{PDHR=lSUi=chlsI6n)9$Ycqr`XyOeKzL0mA~1#WLMe}Q zETJ`ocSG{1Qr0BBjH?d-CSP9F5pL&LURhSGs6dc-yc>r(__pQ6Ldo)Bq_1KX125pu};&Q~CT92y7#atyEHF&M>bunJ@N6xQGrpW#5kXcHrg!F)C*8TVi-_@ z`$p*n6n38|5itrV%sx?KVjNHc`$p*l6#u?a5`fZE^8k*=Ig|f&N-;4BIKF+arXNta zS4Dx?a3|IveDt~%Y><70YP4H>23cRU7O(ZylC{Lcfepsl?E;lf&116lbij&E2^yNk zLS^}Vp-5VQ6`d&;wJuBQGA^&6qJCbK^ls0Qw&#j4sS0vcle*o9o^flp5bi=fzU!Qp z1x=Hdq@tipWwk5lXeX$wLeJ9nh9q}Gx-wteHMbj{AV2Ge<`;K5-E>*i(QSPJHcwh) z^dh{fy1OR3O(q=M!YxTJF3%txxG0nM+3VQzs3d7Ru42;i?j$7}K!bMan6p(uks&(l z8u$uyY>N}olCUc476;80VL`UIio>(zxhYsIB5YfX2&Z8Moe)Bg6>LZG5OlEk3@iMq zDgr~gpo)@OvIcIhB8_D5#V&rW7$lJ$!vgI|0AG%}CZTQMN=%Ps0&l4o8WI z5fLY%kB9^jNh11*7yx0Nd?6aPXIHjsJvc>jPQ5J0IkjD`5XJ{c-kDd-dv4G8AW44n zWyvm`_z(%tzBJt49}D4ihLcaq(M3kV%V&L1^UM2LJK*bSZZg_M+tk z8Qsp!pf$c-+8V!hIwwejB9_*WIta83f~SZyEk_*WGLi#hu3b@Wi&IfKrv`F7FR7Bw z^Vv$(O4vK+9nUU(Oa1f8u}x^hJ4!;%ldZUkSI)!Yd+lz zT@t#dEZuM_-=|kA(CsjpZB}yc9|*3mnBC(e4Fbcw)3A^D4)4u-?yieRAkHatvJQ*m zc|n$W{sD#GCY~`6PyOedf_xWG!z;OSY8OFlduf_(QK=R+_#1VqMP(m*qb=qfRMfWb l1ns+9-lpJDpUgYYYsh)7RCkDdJ5N)gWA)&%FF5;Ce*--bT8RJv delta 915 zcmaJ7M64vcVY4CTpU`F(eQl5G6k1O_6}{AnYNVklutWGYRx=4v`s* z;8k$xgAWkYgCNPxpW!9oMHbc&B%T-62jn1D?=pyj7R=Z6^;cb0(_Quc@9EL^o@XPl zcfqUG97X7}B8dU-SbM%c>R&4B!sUpQfQGEdFaqBQ%+M-Y;M20f88^Ab?PbcHhooq8 zcLf)nP0vr&pP#RPx_DouYfw?HZHP5t0$I|!`8&t}b@FO;PlBpNDA6h>AWvRTbceg38~@Qg(8)jb z6imDGxy$c?<6%(l8qusg`l&I)zWK9PIms(fu6kspom zZ0g2Zt-My7E3d3pYhqI{IYE~+UzZ*`N&H-6NMf^JE-;qy;IK$3NGr(5$Lt)vc zm{Hkc4((C&G|+;aH!Ye|bfQBanQ3gw7v_bYlu$P<)VzsqPPu31$JHc=UIprXL|(xl zKvk72{9%V<@|ksg_zVc}MyUD)U<>`w41=~2z&95ew{erqNyC0)?`My{9QlwvwRKZ2 zIDLIz8F6U~5fuw2RF+xam33!I9yo)Y@f0IU>2##z1}l{ogP>G;i^P~xe1Jo9I$Y-) zLHUX}3sj9#^)f(9#~3%!(IzS!pviqb(BHD3ppZ*c78I{y3tKY3GEgY#_6r zxLMCS;fWZgD%~0Nv*}pquhxU835je0h86(J0eK)BbpB8?YxPcU<#t}kS?=U>NAz$G z5-3WhF!V{!kpZ0K0wP4T5&s73clNc|l_k8XBL4prQsn|7{oP)3SHSHO)70fchlkzc zVA)iN*ZWOeytXqy_kE2Oz#aE6&De)(6Ut^>9~uj|(==yu^RivKkL W#-@~>lG~@`-h~rV|BL`GH~b4MC3y${ delta 387 zcmZvYKTE?v7{=dga!qp0rH%GaQd@Nif46fS%WMhi^*#G-RdYO^QlzG{++^&69-Lq?KO7^!v&bC zKC{~*nl}2Mt%X+ C7EI&- diff --git a/app/modules/agent/engine/router/__pycache__/intent_classifier.cpython-312.pyc b/app/modules/agent/engine/router/__pycache__/intent_classifier.cpython-312.pyc index 915280e692d9a29f4fdefdda179e36da76aad047..7411ec2d09d3d101a30ccf9b8b67240781eda817 100644 GIT binary patch delta 1795 zcmZuxZ%k8H6o0qvdwu;2v_+(&r6^Df!%_#+5d|hnG&9i%F4Jh5`rZQ{D=pslKp@>D z&i()E&JuOW63tvDfo+(6(Jf0f^TQUu$g(V5m(1*&CQH^>J~*=_yXUna`*V`_yZ4-P z@44sv?tSM@&kw!sTP~NKWAfMhCo1PZt?B3iO`%#ekyIw5JCup|7)_5TlCH%kHboO^NT%w9k_sip#zM>^ zDVi2V$FP#pqwuHc)n>_DI?q6nA?n3`!f|K044EXFiEuK3$hNXG|l)e?7ESw=G+71su0{ZKhggDr-THY@HY9%3B9Gc7OwB&0#Y);)3ri z^{sq{r=|=)zi+hn`!VCbB8&GDu116;%nRs0rjUX;=)T}_QE5rH-_CvArPd{ckk zB`cDusp(W)pBhuB8(kF)hCe)ya<3=PYkGp}vwV3QE@(k$MQCFXvBKy4(Mo{^(9(dg zjsdg@jm=m=$PgN2)4W$oCNxbQnu@2C6LB@AGv=ooAtlt-tg`snQtc}SGh7a4g@YBj z;D#la)#om7R+qcr0k^d;n)5d-IfP1kffH7c|kj&>e8{n*=34Ylz-KluTUn(lB%TY@zUamC2t;9blQ$oYz0={%S)4mJOG%f_|rrQ}zJCe!KI8Bf13hgrPDQ`+YPdB1J z3hmClq#b6Q>q$Rca>mFW_|X{*u7uI>WtOgtp>b=p6FjaqVuxPWZ(aYO{lQW#S6O?X zBeu0ad20%0Ha^rmEWiy{OJ67VO{Z-KG2i3}LwYzXxql~h#zBpk%A;4bkEsze-36)2 z^@73VemGnCM0Y|=7bhebOIH#`jdn?ZW*MYApmojLqyw(4X%Svy7EOYCYu0&k!RB13an)~82{ko= z|1oEKYx+#&3Y@OJMW$e%zk_^~ec7KQfm4|8OBfMT4uxh)#J$3l=`MJ>wi@28YYN}Q z1=S25+~pq3aNm2jT^M@r$#a7rbT4|g{Xt)4o(Q3@!_RfC!ZdCTm$!PzRpZmc?-gl&kYGI3yx1W;=92+i-F35|`T}7N19uM;N%UXeB z!__0k@a+q?_@sfe~hFM&(JJ!`$Z@3ObRv0))_-M1LqS;Tpv1;qN8upbk76ME{O m^BV+*P{-!?P~6H|z<5&Wrsv^GeSJm7M7r;@mQT8k_VqVMEUx|l delta 1637 zcmZWpeQZ-z6o0qv`)ps=uHD8y-rB8WTQ@$eOhC{L#)u)fU@{1@Fq^IK4Tpo0+k&vP z4h_molsJzH7-MFP31&0S)cBE*K;$11{$WV4HYSj$i6kaeMvU{ArPE+X@Y4NY=i(U0~rrVNDkENJ8a>Y_d71Mh{ z9Mnu5RQ4IG&wb6h8S8?W^JGiGbs%Y430EnAjWVK>qKz{_I4xuZ_*|}nZ(Tu!9c3v8 z6HH(g2Hch#!dWpRW`zCHL#xZ;5rMDGIN&W`(91j0?)wfU6^y%7nDzx=%H{FR$+Kd~ zJb-)c*Wn8}5EN%|Z=$S}k+>rxp$#r*RckXW-*aZD1;yr%b_B zal}x#LDnXMW`YPpK^jf+gk?gn)M6;82$zXBl2u8J52sRNqsJy{;SH&_If%;C6@=qL z-d}Y(G##1=e(!Icl74ji^MTr_{#$Tf+T*$BmV(}*AnD$s4_f3ct@l*XrxgWJ)p!_J zC(5}%Q4k(fZVuj)Klc772iujS>)X+5!b=y4Cy!X1kD1m6z7myDSHZc@O7qny^G67% z1){FfO(W2$HnsCrsG6wXb}(%nOm8V&XUyWOma_9GI;4_tOnqLte@?ig8cfRR+D+z~ z(`Q|9*WJdvQ0Lj+*+<*hd8SM7%@o}|K7Jt1lZVq5-(aV|;7Go}SHioV9c(rH>WQ#k zQ1uvl3O4GEtPckD2JGZR{u(%?S2Gzt(SKjs`U7t$&?Q{zQu_qwr5=XJcjG27DOcxy z@!nt!_6e3hI0luQZ%2qac^};HMI<{N{qT=(O?NLDDKpqdcKIn(K}`%FG{?;0v{jG} z4fB*`atfNFmbU9}SD)|1BepXuch*12{8WT}_#yq)gVBZ{8fdB075hHJqHX5r6Z2OET@sy3E|ZB>(g8b5nrPoPj9*lM^_rA8{!q_@dr zro(iat~qgOFmT*N9Jk>Ad?gbf4pV*D_m~jXAW^-Jt%Q%N-(+iGbxntK5<{nUL%gQ> zF|0Q=s)&v15j?&h+^q}l)|vEm_llna;e1^>_jL?$&vk5BF%S^ zW{_YF!K(ymg2M#%nZqQ}z;hZ#8zg!RQkC#_V{GpqQiG_?3={=q^H19BveRZ)_lz!* zw)jM)sGUg_1!ObX`Se2D#zm5g?S`nGLp`!@hHQ2@do`X|*pXNyU9lT#Lr*WO5QFCe yMFH8oaWS|M+qy_{u^FAhnKN;pfwz-c+tVS?+S$WlkzWx!*~(^ diff --git a/app/modules/agent/engine/router/__pycache__/intent_switch_detector.cpython-312.pyc b/app/modules/agent/engine/router/__pycache__/intent_switch_detector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0509b3df4ce09fc694408b00570e6fdd41df8310 GIT binary patch literal 4057 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 P-|i)b@qbOQH+T3igi74B literal 3099 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 MS7*K}atf~c4I`W0$N&HU delta 561 zcmZ9Jze~eV5Xaw3UYd}av@W9cM^jNW6^f#*i#Uiy+rdq!B1ml_1|or^4&5A_oFcdS z4~V!syEr(AAP!xe-2@{@U3{1N`$0Z;x$hm{d&zzJO4DyNO(p!)j!&Hn;Y7crM<>_o za{{q@ti(A*i4a?}lD1;?*vc-mv?F%jEt9Xfskd}91uor#cM4iBmji*rFgkg75VmL% zX2mI*f^D(7(#Hiqg-CO>*^cMAzJzJA;nss--)`=CmOsENXT;B7V?RRv0^>w`99)j_OcAX%B&@u3cRsmb1-e+)4wq22aukYf)2z!^G0Mss8txT;3$u5^7T;6@(bjaQ5N{?2xkg?-ODaP39Ez=!d71~VMbBBk_^l None: self._repo.update_router_context( @@ -25,5 +26,6 @@ class RouterContextStore: process_id=process_id, user_message=user_message, assistant_message=assistant_message, + decision_type=decision_type, max_history=max_history, ) diff --git a/app/modules/agent/engine/router/intent_classifier.py b/app/modules/agent/engine/router/intent_classifier.py index e478a8e..4e73c9b 100644 --- a/app/modules/agent/engine/router/intent_classifier.py +++ b/app/modules/agent/engine/router/intent_classifier.py @@ -17,11 +17,7 @@ class IntentClassifier: def __init__(self, llm: AgentLlmService) -> None: self._llm = llm - def classify(self, user_message: str, context: RouterContext, mode: str = "auto") -> RouteDecision: - forced = self._from_mode(mode) - if forced: - return forced - + def classify_new_intent(self, user_message: str, context: RouterContext) -> RouteDecision: text = (user_message or "").strip().lower() if text in self._short_confirmations and context.last_routing: return RouteDecision( @@ -30,6 +26,7 @@ class IntentClassifier: confidence=1.0, reason="short_confirmation", use_previous=True, + decision_type="continue", ) deterministic = self._deterministic_route(text) @@ -45,9 +42,10 @@ class IntentClassifier: process_id="general", confidence=0.8, reason="default", + decision_type="start", ) - def _from_mode(self, mode: str) -> RouteDecision | None: + def from_mode(self, mode: str) -> RouteDecision | None: mapping = { "project_qa": ("project", "qa"), "project_edits": ("project", "edits"), @@ -65,6 +63,8 @@ class IntentClassifier: process_id=route[1], confidence=1.0, reason=f"mode_override:{mode}", + decision_type="switch", + explicit_switch=True, ) def _classify_with_llm(self, user_message: str, context: RouterContext) -> RouteDecision | None: @@ -96,6 +96,7 @@ class IntentClassifier: process_id=route[1], confidence=confidence, reason=f"llm_router:{payload.get('reason', 'ok')}", + decision_type="start", ) def _parse_llm_payload(self, raw: str) -> dict[str, str | float] | None: @@ -139,6 +140,8 @@ class IntentClassifier: process_id="edits", confidence=0.97, reason="deterministic_targeted_file_edit", + decision_type="switch", + explicit_switch=True, ) if self._is_broad_docs_request(text): return RouteDecision( @@ -146,6 +149,8 @@ class IntentClassifier: process_id="generation", confidence=0.95, reason="deterministic_docs_generation", + decision_type="switch", + explicit_switch=True, ) return None diff --git a/app/modules/agent/engine/router/intent_switch_detector.py b/app/modules/agent/engine/router/intent_switch_detector.py new file mode 100644 index 0000000..151b57c --- /dev/null +++ b/app/modules/agent/engine/router/intent_switch_detector.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import re + +from app.modules.agent.engine.router.schemas import RouterContext + + +class IntentSwitchDetector: + _EXPLICIT_SWITCH_MARKERS = ( + "теперь", + "а теперь", + "давай теперь", + "переключись", + "переключаемся", + "сейчас другое", + "новая задача", + "new task", + "switch to", + "now do", + "instead", + ) + _FOLLOW_UP_MARKERS = ( + "а еще", + "а ещё", + "подробнее", + "почему", + "зачем", + "что если", + "и еще", + "и ещё", + "покажи подробнее", + "можешь подробнее", + ) + + def should_switch(self, user_message: str, context: RouterContext) -> bool: + if not context.dialog_started or context.active_intent is None: + return False + text = " ".join((user_message or "").strip().lower().split()) + if not text: + return False + if self._is_follow_up(text): + return False + if any(marker in text for marker in self._EXPLICIT_SWITCH_MARKERS): + return True + return self._is_strong_targeted_edit_request(text) or self._is_strong_docs_request(text) + + def _is_follow_up(self, text: str) -> bool: + return any(marker in text for marker in self._FOLLOW_UP_MARKERS) + + def _is_strong_targeted_edit_request(self, text: str) -> bool: + edit_markers = ( + "добавь", + "добавить", + "измени", + "исправь", + "обнови", + "удали", + "замени", + "append", + "update", + "edit", + "remove", + "replace", + ) + has_edit_marker = any(marker in text for marker in edit_markers) + has_file_marker = ( + "readme" in text + or bool(re.search(r"\b[\w.\-/]+\.(md|txt|rst|yaml|yml|json|toml|ini|cfg|py)\b", text)) + ) + return has_edit_marker and has_file_marker + + def _is_strong_docs_request(self, text: str) -> bool: + docs_markers = ( + "подготовь документац", + "сгенерируй документац", + "создай документац", + "опиши документац", + "generate documentation", + "write documentation", + ) + return any(marker in text for marker in docs_markers) diff --git a/app/modules/agent/engine/router/router_service.py b/app/modules/agent/engine/router/router_service.py index 9ebbb84..2ff7c7e 100644 --- a/app/modules/agent/engine/router/router_service.py +++ b/app/modules/agent/engine/router/router_service.py @@ -1,7 +1,8 @@ from app.modules.agent.engine.router.context_store import RouterContextStore from app.modules.agent.engine.router.intent_classifier import IntentClassifier +from app.modules.agent.engine.router.intent_switch_detector import IntentSwitchDetector from app.modules.agent.engine.router.registry import IntentRegistry -from app.modules.agent.engine.router.schemas import RouteResolution +from app.modules.agent.engine.router.schemas import RouteDecision, RouteResolution class RouterService: @@ -10,27 +11,48 @@ class RouterService: registry: IntentRegistry, classifier: IntentClassifier, context_store: RouterContextStore, + switch_detector: IntentSwitchDetector | None = None, min_confidence: float = 0.7, ) -> None: self._registry = registry self._classifier = classifier self._ctx = context_store + self._switch_detector = switch_detector or IntentSwitchDetector() self._min_confidence = min_confidence def resolve(self, user_message: str, conversation_key: str, mode: str = "auto") -> RouteResolution: context = self._ctx.get(conversation_key) - decision = self._classifier.classify(user_message, context, mode=mode) - if decision.confidence < self._min_confidence: - return self._fallback("low_confidence") - if not self._registry.is_valid(decision.domain_id, decision.process_id): - return self._fallback("invalid_route") - return RouteResolution( - domain_id=decision.domain_id, - process_id=decision.process_id, - confidence=decision.confidence, - reason=decision.reason, - fallback_used=False, - ) + forced = self._classifier.from_mode(mode) + if forced: + return self._resolution(forced) + + if not context.dialog_started or context.active_intent is None: + decision = self._classifier.classify_new_intent(user_message, context) + if not self._is_acceptable(decision): + return self._fallback("low_confidence") + return self._resolution( + decision.model_copy( + update={ + "decision_type": "start", + "explicit_switch": False, + } + ) + ) + + if self._switch_detector.should_switch(user_message, context): + decision = self._classifier.classify_new_intent(user_message, context) + if self._is_acceptable(decision): + return self._resolution( + decision.model_copy( + update={ + "decision_type": "switch", + "explicit_switch": True, + } + ) + ) + return self._continue_current(context, "explicit_switch_unresolved_keep_current") + + return self._continue_current(context, "continue_current_intent") def persist_context( self, @@ -40,6 +62,7 @@ class RouterService: process_id: str, user_message: str, assistant_message: str, + decision_type: str = "start", ) -> None: self._ctx.update( conversation_key, @@ -47,6 +70,7 @@ class RouterService: process_id=process_id, user_message=user_message, assistant_message=assistant_message, + decision_type=decision_type, ) def graph_factory(self, domain_id: str, process_id: str): @@ -59,4 +83,32 @@ class RouterService: confidence=0.0, reason=reason, fallback_used=True, + decision_type="start", + explicit_switch=False, + ) + + def _continue_current(self, context, reason: str) -> RouteResolution: + active = context.active_intent or context.last_routing or {"domain_id": "default", "process_id": "general"} + return RouteResolution( + domain_id=str(active["domain_id"]), + process_id=str(active["process_id"]), + confidence=1.0, + reason=reason, + fallback_used=False, + decision_type="continue", + explicit_switch=False, + ) + + def _is_acceptable(self, decision: RouteDecision) -> bool: + return decision.confidence >= self._min_confidence and self._registry.is_valid(decision.domain_id, decision.process_id) + + def _resolution(self, decision: RouteDecision) -> RouteResolution: + return RouteResolution( + domain_id=decision.domain_id, + process_id=decision.process_id, + confidence=decision.confidence, + reason=decision.reason, + fallback_used=False, + decision_type=decision.decision_type, + explicit_switch=decision.explicit_switch, ) diff --git a/app/modules/agent/engine/router/schemas.py b/app/modules/agent/engine/router/schemas.py index 0d15b1a..233d4fa 100644 --- a/app/modules/agent/engine/router/schemas.py +++ b/app/modules/agent/engine/router/schemas.py @@ -7,6 +7,8 @@ class RouteDecision(BaseModel): confidence: float = 0.0 reason: str = "" use_previous: bool = False + decision_type: str = "start" + explicit_switch: bool = False @field_validator("confidence") @classmethod @@ -20,8 +22,13 @@ class RouteResolution(BaseModel): confidence: float reason: str fallback_used: bool = False + decision_type: str = "start" + explicit_switch: bool = False class RouterContext(BaseModel): last_routing: dict[str, str] | None = None message_history: list[dict[str, str]] = Field(default_factory=list) + active_intent: dict[str, str] | None = None + dialog_started: bool = False + turn_index: int = 0 diff --git a/app/modules/agent/llm/__pycache__/service.cpython-312.pyc b/app/modules/agent/llm/__pycache__/service.cpython-312.pyc index efb82e15b56ca1a98a6194ce320f522d2a79bc4b..59772d4089060ee6b8a1f4586d2112348e3338d1 100644 GIT binary patch literal 2161 zcma)7&2Jk;6rcU{*E+RJNKNX}Euo}qM|3xB2UD~6GqWO zl+Nfl@JI`m+aS#%Ln$(JPe``K^Y!*np-?#I(pJ4}xddM@WEP{OX(Xv~_;ePA7eowcW&BT8GBwW*#q)z^+(Ik_22c8+}1 zUOxL%?D36V!yRoSm02ELHoB?&dTOkf8tbPHb`MSWQqx^|dH@7&V^{6&i82Va-th06mS2w+6;kO`|`cp^ymv9gE^p=gLKW`=6x zW~kSYnCuXHjy?+a=m?PLC1?f&7B|gmz3Q4~JNJ-_g=mK--{FTL@q6}l+;qd+VxaV=0etb!gcnvJT9}kb6 z@a_o_wlB+`?7+(am*@ePSCv0@!Z#K@$rIe@b)gm_Eo(NAWuyD4 zJ*tPvKX%a3gY@&&cJ4s}aHO3@$0fB887$@R2glpbZmVpxTzIbk&>3uUeKGnNWP`~z z^g*VcdShnhM@zMD|eLt7f!J+hMjBEOy;?`6ktT5 z?ABQK^qW6S`0MW5<-2?DJv0oFTojvP`(OzQo+H^nh)O(ccK$`xW|7d=7H#&7P&!Uyx_&Ch%D`@MK*H$D^!Q-CK$K=>vW7KjJJw|2N^ZgbaGpJ~O)g$$BZwiRXfY!iLbLQ;KlS*7ga znof#~NROdcTG-`JW_t6ByL`Vez&^;QyRs8?lg)M`3i7Em4&>%myPGCWcJO76J_PlL zt=(W5i>Xf@u?$+1<7)R9Exqc0$XG_-(8*Ej;4)IaIAnG@{Fdln&yyVy=WOBXO$zCg?@~9RjrGq^F8O>(`+yM str: + value = (text or "").replace("\n", "\\n").strip() + if len(value) <= max_chars: + return value + return value[:max_chars].rstrip() + "...[truncated]" + class AgentLlmService: def __init__(self, client: GigaChatClient, prompts: PromptLoader) -> None: self._client = client self._prompts = prompts - def generate(self, prompt_name: str, user_input: str) -> str: + def generate(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> str: system_prompt = self._prompts.load(prompt_name) if not system_prompt: system_prompt = "You are a helpful assistant." - return self._client.complete(system_prompt=system_prompt, user_prompt=user_input) + if log_context: + LOGGER.warning( + "graph llm input: context=%s prompt=%s user_input=%s", + log_context, + prompt_name, + _truncate_for_log(user_input), + ) + output = self._client.complete(system_prompt=system_prompt, user_prompt=user_input) + if log_context: + LOGGER.warning( + "graph llm output: context=%s prompt=%s output=%s", + log_context, + prompt_name, + _truncate_for_log(output), + ) + return output diff --git a/app/modules/agent/module.py b/app/modules/agent/module.py index 78cf353..e0bebe6 100644 --- a/app/modules/agent/module.py +++ b/app/modules/agent/module.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from fastapi import APIRouter from pydantic import BaseModel, HttpUrl +from typing import TYPE_CHECKING from app.modules.agent.changeset_validator import ChangeSetValidator from app.modules.agent.confluence_service import ConfluenceService @@ -19,12 +22,17 @@ class ConfluenceFetchRequest(BaseModel): url: HttpUrl +if TYPE_CHECKING: + from app.modules.rag.explain.retriever_v2 import CodeExplainRetrieverV2 + + class AgentModule: def __init__( self, rag_retriever: RagRetriever, agent_repository: AgentRepository, story_context_repository: StoryContextRepository, + code_explain_retriever: CodeExplainRetrieverV2 | None = None, ) -> None: self.confluence = ConfluenceService() self.changeset_validator = ChangeSetValidator() @@ -34,14 +42,16 @@ class AgentModule: client = GigaChatClient(settings, token_provider) prompt_loader = PromptLoader() llm = AgentLlmService(client=client, prompts=prompt_loader) + self.llm = llm story_recorder = StorySessionRecorder(story_context_repository) self.runtime = GraphAgentRuntime( rag=rag_retriever, confluence=self.confluence, changeset_validator=self.changeset_validator, - llm=llm, + llm=self.llm, agent_repository=agent_repository, story_recorder=story_recorder, + code_explain_retriever=code_explain_retriever, ) def internal_router(self) -> APIRouter: diff --git a/app/modules/agent/prompts/code_explain_answer_v2.txt b/app/modules/agent/prompts/code_explain_answer_v2.txt new file mode 100644 index 0000000..394e2fe --- /dev/null +++ b/app/modules/agent/prompts/code_explain_answer_v2.txt @@ -0,0 +1,17 @@ +Объяснение кода осуществляется только с использованием предоставленного ExplainPack. + +Правила: +- Сначала используйте доказательства. +- Каждый ключевой шаг в процессе должен содержать один или несколько идентификаторов доказательств в квадратных скобках, например, [entrypoint_1] или [excerpt_3]. +- Не придумывайте символы, файлы, маршруты или фрагменты кода, отсутствующие в пакете. +- Если доказательства неполные, укажите это явно. +- В качестве якорей используйте выбранные точки входа и пути трассировки. + +Верните Markdown со следующей структурой: +1. Краткое описание +2. Пошаговый процесс +3. Данные и побочные эффекты +4. Ошибки и граничные случаи +5. Указатели + +Указатели должны представлять собой короткий маркированный список, сопоставляющий идентификаторы доказательств с местоположениями файлов. \ No newline at end of file diff --git a/app/modules/agent/prompts/rag_intent_router_v2.txt b/app/modules/agent/prompts/rag_intent_router_v2.txt new file mode 100644 index 0000000..aee7599 --- /dev/null +++ b/app/modules/agent/prompts/rag_intent_router_v2.txt @@ -0,0 +1,24 @@ +Ты intent-router для layered RAG. +На вход ты получаешь JSON с полями: +- message: текущий запрос пользователя +- active_intent: текущий активный intent диалога или null +- last_query: предыдущий запрос пользователя +- allowed_intents: допустимые intent'ы + +Выбери ровно один intent из allowed_intents. +Верни только JSON без markdown и пояснений. + +Строгий формат ответа: +{"intent":"","confidence":,"reason":""} + +Правила: +- CODE_QA: объяснение по коду, архитектуре, классам, методам, файлам, блокам кода, поведению приложения по реализации. +- DOCS_QA: объяснение по документации, README, markdown, specs, runbooks, разделам документации. +- GENERATE_DOCS_FROM_CODE: просьба сгенерировать, подготовить или обновить документацию по коду. +- PROJECT_MISC: прочие вопросы по проекту, не относящиеся явно к коду или документации. + +Приоритет: +- Если пользователь просит именно подготовить документацию по коду, выбирай GENERATE_DOCS_FROM_CODE. +- Если пользователь спрашивает про конкретный класс, файл, метод или блок кода, выбирай CODE_QA. +- Если пользователь спрашивает про README, docs, markdown или конкретную документацию, выбирай DOCS_QA. +- Если сигнал неочевиден, выбирай PROJECT_MISC и confidence <= 0.6. diff --git a/app/modules/agent/repository.py b/app/modules/agent/repository.py index e9d3d46..2e764e0 100644 --- a/app/modules/agent/repository.py +++ b/app/modules/agent/repository.py @@ -18,6 +18,10 @@ class AgentRepository: conversation_key VARCHAR(64) PRIMARY KEY, last_domain_id VARCHAR(64) NULL, last_process_id VARCHAR(64) NULL, + active_domain_id VARCHAR(64) NULL, + active_process_id VARCHAR(64) NULL, + dialog_started BOOLEAN NOT NULL DEFAULT FALSE, + turn_index INTEGER NOT NULL DEFAULT 0, message_history_json TEXT NOT NULL DEFAULT '[]', updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ) @@ -64,14 +68,24 @@ class AgentRepository: """ ) ) + self._ensure_router_context_columns(conn) conn.commit() + def _ensure_router_context_columns(self, conn) -> None: + for statement in ( + "ALTER TABLE router_context ADD COLUMN IF NOT EXISTS active_domain_id VARCHAR(64) NULL", + "ALTER TABLE router_context ADD COLUMN IF NOT EXISTS active_process_id VARCHAR(64) NULL", + "ALTER TABLE router_context ADD COLUMN IF NOT EXISTS dialog_started BOOLEAN NOT NULL DEFAULT FALSE", + "ALTER TABLE router_context ADD COLUMN IF NOT EXISTS turn_index INTEGER NOT NULL DEFAULT 0", + ): + conn.execute(text(statement)) + def get_router_context(self, conversation_key: str) -> RouterContext: with get_engine().connect() as conn: row = conn.execute( text( """ - SELECT last_domain_id, last_process_id, message_history_json + SELECT last_domain_id, last_process_id, active_domain_id, active_process_id, dialog_started, turn_index, message_history_json FROM router_context WHERE conversation_key = :key """ @@ -82,7 +96,7 @@ class AgentRepository: if not row: return RouterContext() - history_raw = row[2] or "[]" + history_raw = row[6] or "[]" try: history = json.loads(history_raw) except json.JSONDecodeError: @@ -91,6 +105,9 @@ class AgentRepository: last = None if row[0] and row[1]: last = {"domain_id": str(row[0]), "process_id": str(row[1])} + active = None + if row[2] and row[3]: + active = {"domain_id": str(row[2]), "process_id": str(row[3])} clean_history = [] for item in history if isinstance(history, list) else []: @@ -101,7 +118,13 @@ class AgentRepository: if role in {"user", "assistant"} and content: clean_history.append({"role": role, "content": content}) - return RouterContext(last_routing=last, message_history=clean_history) + return RouterContext( + last_routing=last, + message_history=clean_history, + active_intent=active or last, + dialog_started=bool(row[4]), + turn_index=int(row[5] or 0), + ) def update_router_context( self, @@ -111,6 +134,7 @@ class AgentRepository: process_id: str, user_message: str, assistant_message: str, + decision_type: str, max_history: int, ) -> None: current = self.get_router_context(conversation_key) @@ -121,17 +145,29 @@ class AgentRepository: history.append({"role": "assistant", "content": assistant_message}) if max_history > 0: history = history[-max_history:] + current_active = current.active_intent or current.last_routing or {"domain_id": domain_id, "process_id": process_id} + next_active = ( + {"domain_id": domain_id, "process_id": process_id} + if decision_type in {"start", "switch"} + else current_active + ) + next_turn_index = max(0, int(current.turn_index or 0)) + (1 if user_message else 0) with get_engine().connect() as conn: conn.execute( text( """ INSERT INTO router_context ( - conversation_key, last_domain_id, last_process_id, message_history_json - ) VALUES (:key, :domain, :process, :history) + conversation_key, last_domain_id, last_process_id, active_domain_id, active_process_id, + dialog_started, turn_index, message_history_json + ) VALUES (:key, :domain, :process, :active_domain, :active_process, :dialog_started, :turn_index, :history) ON CONFLICT (conversation_key) DO UPDATE SET last_domain_id = EXCLUDED.last_domain_id, last_process_id = EXCLUDED.last_process_id, + active_domain_id = EXCLUDED.active_domain_id, + active_process_id = EXCLUDED.active_process_id, + dialog_started = EXCLUDED.dialog_started, + turn_index = EXCLUDED.turn_index, message_history_json = EXCLUDED.message_history_json, updated_at = CURRENT_TIMESTAMP """ @@ -140,6 +176,10 @@ class AgentRepository: "key": conversation_key, "domain": domain_id, "process": process_id, + "active_domain": str(next_active["domain_id"]), + "active_process": str(next_active["process_id"]), + "dialog_started": True, + "turn_index": next_turn_index, "history": json.dumps(history, ensure_ascii=False), }, ) diff --git a/app/modules/agent/service.py b/app/modules/agent/service.py index 0b3a114..7885ed4 100644 --- a/app/modules/agent/service.py +++ b/app/modules/agent/service.py @@ -1,12 +1,16 @@ +from __future__ import annotations + from dataclasses import dataclass, field from collections.abc import Awaitable, Callable import inspect import logging import re +from typing import TYPE_CHECKING from app.modules.agent.engine.orchestrator import OrchestratorService, TaskSpecBuilder from app.modules.agent.engine.orchestrator.metrics_persister import MetricsPersister from app.modules.agent.engine.orchestrator.models import RoutingMeta +from app.modules.agent.engine.orchestrator.step_registry import StepRegistry from app.modules.agent.engine.router import build_router_service from app.modules.agent.llm import AgentLlmService from app.modules.agent.story_session_recorder import StorySessionRecorder @@ -22,6 +26,9 @@ from app.schemas.common import ModuleName LOGGER = logging.getLogger(__name__) +if TYPE_CHECKING: + from app.modules.rag.explain.retriever_v2 import CodeExplainRetrieverV2 + def _truncate_for_log(text: str | None, max_chars: int = 1500) -> str: value = (text or "").replace("\n", "\\n").strip() @@ -47,13 +54,14 @@ class GraphAgentRuntime: llm: AgentLlmService, agent_repository: AgentRepository, story_recorder: StorySessionRecorder | None = None, + code_explain_retriever: CodeExplainRetrieverV2 | None = None, ) -> None: self._rag = rag self._confluence = confluence self._changeset_validator = changeset_validator - self._router = build_router_service(llm, agent_repository) + self._router = build_router_service(llm, agent_repository, rag) self._task_spec_builder = TaskSpecBuilder() - self._orchestrator = OrchestratorService() + self._orchestrator = OrchestratorService(step_registry=StepRegistry(code_explain_retriever)) self._metrics_persister = MetricsPersister(agent_repository) self._story_recorder = story_recorder self._checkpointer = None @@ -70,7 +78,7 @@ class GraphAgentRuntime: files: list[dict], progress_cb: Callable[[str, str, str, dict | None], Awaitable[None] | None] | None = None, ) -> AgentResult: - LOGGER.warning( + LOGGER.info( "GraphAgentRuntime.run started: task_id=%s dialog_session_id=%s mode=%s", task_id, dialog_session_id, @@ -96,9 +104,7 @@ class GraphAgentRuntime: meta={"domain_id": route.domain_id, "process_id": route.process_id}, ) files_map = self._build_files_map(files) - - await self._emit_progress(progress_cb, "agent.rag", "Собираю релевантный контекст из RAG.") - rag_ctx = await self._rag.retrieve(rag_session_id, message) + rag_ctx: list[dict] = [] await self._emit_progress(progress_cb, "agent.attachments", "Обрабатываю дополнительные вложения.") conf_pages = await self._fetch_confluence_pages(attachments) route_meta = RoutingMeta( @@ -157,8 +163,9 @@ class GraphAgentRuntime: process_id=route.process_id, user_message=message, assistant_message=final_answer, + decision_type=route.decision_type, ) - LOGGER.warning( + LOGGER.info( "final agent answer: task_id=%s route=%s/%s answer=%s", task_id, route.domain_id, @@ -178,7 +185,7 @@ class GraphAgentRuntime: answer=final_answer, meta={ "route": route.model_dump(), - "used_rag": True, + "used_rag": False, "used_confluence": bool(conf_pages), "changeset_filtered_out": True, "orchestrator": orchestrator_meta, @@ -193,6 +200,7 @@ class GraphAgentRuntime: process_id=route.process_id, user_message=message, assistant_message=final_answer or f"changeset:{len(validated)}", + decision_type=route.decision_type, ) final = AgentResult( result_type=TaskResultType.CHANGESET, @@ -200,7 +208,7 @@ class GraphAgentRuntime: changeset=validated, meta={ "route": route.model_dump(), - "used_rag": True, + "used_rag": False, "used_confluence": bool(conf_pages), "orchestrator": orchestrator_meta, "orchestrator_steps": orchestrator_steps, @@ -214,7 +222,7 @@ class GraphAgentRuntime: scenario=str(orchestrator_meta.get("scenario", task_spec.scenario.value)), quality=quality_meta, ) - LOGGER.warning( + LOGGER.info( "GraphAgentRuntime.run completed: task_id=%s route=%s/%s result_type=%s changeset_items=%s", task_id, route.domain_id, @@ -222,7 +230,7 @@ class GraphAgentRuntime: final.result_type.value, len(final.changeset), ) - LOGGER.warning( + LOGGER.info( "final agent answer: task_id=%s route=%s/%s answer=%s", task_id, route.domain_id, @@ -239,13 +247,14 @@ class GraphAgentRuntime: process_id=route.process_id, user_message=message, assistant_message=final_answer, + decision_type=route.decision_type, ) final = AgentResult( result_type=TaskResultType.ANSWER, answer=final_answer, meta={ "route": route.model_dump(), - "used_rag": True, + "used_rag": False, "used_confluence": bool(conf_pages), "orchestrator": orchestrator_meta, "orchestrator_steps": orchestrator_steps, @@ -259,7 +268,7 @@ class GraphAgentRuntime: scenario=str(orchestrator_meta.get("scenario", task_spec.scenario.value)), quality=quality_meta, ) - LOGGER.warning( + LOGGER.info( "GraphAgentRuntime.run completed: task_id=%s route=%s/%s result_type=%s answer_len=%s", task_id, route.domain_id, @@ -267,7 +276,7 @@ class GraphAgentRuntime: final.result_type.value, len(final.answer or ""), ) - LOGGER.warning( + LOGGER.info( "final agent answer: task_id=%s route=%s/%s answer=%s", task_id, route.domain_id, @@ -351,7 +360,7 @@ class GraphAgentRuntime: factory = self._router.graph_factory("default", "general") if factory is None: raise RuntimeError("No graph factory configured") - LOGGER.warning("_resolve_graph resolved: domain_id=%s process_id=%s", domain_id, process_id) + LOGGER.debug("_resolve_graph resolved: domain_id=%s process_id=%s", domain_id, process_id) return factory(self._checkpointer) def _invoke_graph(self, graph, state: dict, dialog_session_id: str): @@ -365,7 +374,7 @@ class GraphAgentRuntime: for item in attachments: if item.get("type") == "confluence_url": pages.append(await self._confluence.fetch_page(item["url"])) - LOGGER.warning("_fetch_confluence_pages completed: pages=%s", len(pages)) + LOGGER.info("_fetch_confluence_pages completed: pages=%s", len(pages)) return pages def _format_rag(self, items: list[dict]) -> str: @@ -411,7 +420,7 @@ class GraphAgentRuntime: "content": str(item.get("content", "")), "content_hash": str(item.get("content_hash", "")), } - LOGGER.warning("_build_files_map completed: files=%s", len(output)) + LOGGER.debug("_build_files_map completed: files=%s", len(output)) return output def _lookup_file(self, files_map: dict[str, dict], path: str) -> dict | None: @@ -437,7 +446,7 @@ class GraphAgentRuntime: ) item.base_hash = str(source["content_hash"]) enriched.append(item) - LOGGER.warning("_enrich_changeset_hashes completed: items=%s", len(enriched)) + LOGGER.debug("_enrich_changeset_hashes completed: items=%s", len(enriched)) return enriched def _sanitize_changeset(self, items: list[ChangeItem], files_map: dict[str, dict]) -> list[ChangeItem]: @@ -462,7 +471,7 @@ class GraphAgentRuntime: continue sanitized.append(item) if dropped_noop or dropped_ws: - LOGGER.warning( + LOGGER.info( "_sanitize_changeset dropped items: noop=%s whitespace_only=%s kept=%s", dropped_noop, dropped_ws, diff --git a/app/modules/application.py b/app/modules/application.py index caab8f3..fd3819b 100644 --- a/app/modules/application.py +++ b/app/modules/application.py @@ -1,9 +1,14 @@ from app.modules.agent.module import AgentModule from app.modules.agent.repository import AgentRepository from app.modules.agent.story_context_repository import StoryContextRepository, StoryContextSchemaRepository +from app.modules.chat.direct_service import CodeExplainChatService +from app.modules.chat.dialog_store import DialogSessionStore from app.modules.chat.repository import ChatRepository from app.modules.chat.module import ChatModule +from app.modules.chat.session_resolver import ChatSessionResolver +from app.modules.chat.task_store import TaskStore from app.modules.rag.persistence.repository import RagRepository +from app.modules.rag.explain import CodeExplainRetrieverV2, CodeGraphRepository, LayeredRetrievalGateway from app.modules.rag_session.module import RagModule from app.modules.rag_repo.module import RagRepoModule from app.modules.shared.bootstrap import bootstrap_database @@ -20,16 +25,32 @@ class ModularApplication: self.agent_repository = AgentRepository() self.story_context_schema_repository = StoryContextSchemaRepository() self.story_context_repository = StoryContextRepository() + self.chat_tasks = TaskStore() self.rag_session = RagModule(event_bus=self.events, retry=self.retry, repository=self.rag_repository) self.rag_repo = RagRepoModule( story_context_repository=self.story_context_repository, rag_repository=self.rag_repository, ) + self.code_explain_retriever = CodeExplainRetrieverV2( + gateway=LayeredRetrievalGateway(self.rag_repository, self.rag_session.embedder), + graph_repository=CodeGraphRepository(), + ) self.agent = AgentModule( rag_retriever=self.rag_session.rag, agent_repository=self.agent_repository, story_context_repository=self.story_context_repository, + code_explain_retriever=self.code_explain_retriever, + ) + self.direct_chat = CodeExplainChatService( + retriever=self.code_explain_retriever, + llm=self.agent.llm, + session_resolver=ChatSessionResolver( + dialogs=DialogSessionStore(self.chat_repository), + rag_session_exists=lambda rag_session_id: self.rag_session.sessions.get(rag_session_id) is not None, + ), + task_store=self.chat_tasks, + message_sink=self.chat_repository.add_message, ) self.chat = ChatModule( agent_runner=self.agent.runtime, @@ -37,6 +58,8 @@ class ModularApplication: retry=self.retry, rag_sessions=self.rag_session.sessions, repository=self.chat_repository, + direct_chat=self.direct_chat, + task_store=self.chat_tasks, ) def startup(self) -> None: diff --git a/app/modules/chat/__pycache__/dialog_store.cpython-312.pyc b/app/modules/chat/__pycache__/dialog_store.cpython-312.pyc index 8389fb6718934364277f7570c1aba9ff0489e3de..a8181568d6938e4317f5f9763087e7741c6c0189 100644 GIT binary patch delta 1043 zcmY*X&1(}u6rb6h-Ay)KYd>Oa6uQ>7vaQs6DLq)*Dncw&5%&_7%o+npHq304h!77I z^xz@pp#OlU;-BHkqatM?ps1iXA)Y*l?@gMfUD)5e@ArH2=AZNal=H!HYy#iX!Q0BW zj+4`A|6%8n*XD$?s#&vqi;~a~Cg?^rRZIJ6&~=S$6K?JiZtZKBP00S>Aik}9De$F- zK0nj4z05C+@Mw|=>h&lN;!0GPpr&~c2c>EtQE_hH+u1B`T;DV{Zr&(tcivoCr~Ns| zdrd;(Q%-#C4&gM^xwZyYCFv-&m}ex;wMtNp%DbVIP(0CPEMCzC%DRhmp}Rv@+i8dt z2pRb0DS&6>OD_MBXYIJ-K7luq=wz@SEnSZC{~pg0d^j}5xo zaB03yDWg%$T<8(EN6viJPxshr_p>?AaLE62rPhW3 delta 858 zcmY*XziSjh6rS0c{c(G4^hnJ51N9P;$VNod2x1H-3W~^CL=hJbIc~;qaLLN-9db5W zqzH(`F@*>gDFZ=B=|5m)qhKKmDJ`sASSt(hy}4Xs7WRAZee>plFwF9*) z+(9ka3y=iX1J(m4C4Jc%o6USBpCxIllc-+iDB0<%wOVs;8gLLHm^1kGW9!0*8iOoF zs#;YQ{mz=58$qT7@QQpter|)m@s)9m)FB}U2qW~%`tZnuLSakfnyab+362l~WIr}&a%OnAD^LrQd)_sjbl>a?!d;glIO z9vkgxVnfDM^87!&dv=wM>s9;yOVo=>3ZIfHBOF1n5l$kQ3mDC_SWY4wMKDGxn2inr zd(#^L>m;|RUEN&B34|R{V8|V2hnveefv{CCFy#Jq*Y=TvuvP!~;OoW3-p+j2q8z(xvxItP$5O6e|{*d^mX M$Q6CYo#J?*f0@Ik0{{R3 diff --git a/app/modules/chat/__pycache__/direct_service.cpython-312.pyc b/app/modules/chat/__pycache__/direct_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..662a496fdf78eee1239216409e6ae3a22d88c4ea GIT binary patch literal 4133 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 Uc-=qxzV{37AtCUTz+fKmU%&&}CjbBd literal 0 HcmV?d00001 diff --git a/app/modules/chat/__pycache__/evidence_gate.cpython-312.pyc b/app/modules/chat/__pycache__/evidence_gate.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9e75fb36b3a566556f795127019f247da36c77d GIT binary patch literal 4456 zcmb_gZ){W76~FJF|0hncu@h3jA>@xsNZ3Ld4Nw&@Fxn8<61FM<-i+@fIK0^Id(Y5B zW=dC3(}Dy{lah)dg4Bs1kQ!PL8D%N`hSU#xFAlZ$#3ZC?n%Jk5AXVkl&VBZCLU8!B ztN7f1=bU@qJ->7Az4@-B#DSn}%lsv}30nUm9kXz`1~n@}V;EtCDG~KiT@*#yv`F_c zT}&U_#TtD^0&VpJ&yF?6F9+g!(x6ymla!K&PL|? z?Lmj`-ElmMg;+$`6;_0h5Q)lQq0SzT$3@+e0Q&^2Bb|{INwAQ= ze!;XZ-qbqCM)#c@(3 zZ5PkKpA3un88^?*+bN!xO2JZzI8bNFB2qatuc29>=y`QGk!a2-lA9w(!%DM|{~Epr zKBp4xot@2cgs)9iMs!jvh# z_Zwh(Ztt-b+c0&C>te7S)29GL*nt_SIqbwN)E4Z*9Msl+ORz-uZ;xXk@96nkAIT#M zbQ7;31g@rLmq23}rKl9*$+%Ja0Z#4Ugc80l8smizB7&4q2&PzB5Rd4t`J*z?6bML_ zP?7DA#-a+(4+Iu5qRHea1Advr(;&(&4-T;pZOe>ZdBI96%o|otLIbQsz4>YK*2M@@vxW##4hd`lfwgYayJn0jBi=SSFic%$Jn%Q z?U3`4vvf$wxT`gHb=tXV6ldx-YIPf@oEsl`>c^D(Rnwk5s%y_9cTLt)k@fg9o?6XQ zJBr8NO?x(GD{7uu*)qorV(pHXT}|9!xOyG9m;kaD1{M~QR+pg=I!8z79u%PuRv~nf zPSL&dp`90!sk3FpXD-lZm$M)Zl0`v6688Uf71T?Byr9g%b`Dlxk8#Aj$ZCQXGk<0~ zz;4R0{lvUhkZpfL-2H@Pu^_8Eg_t7sC*skVBI{PUzwdBdgkvBPR*piyFDlE?SP$UM z3-K&O6an+|1vn+Dh=T5t1(+~}EqqRzCD~VMBJ1QJC)Gh)XQPVH_X6akRWLrVdeJ;E z59tKd@(Cb==yA(0GA(UdOWT8AKM1QW`@XN@$JVQz?`1jP;`Ld*rVI*|yKx z(*EENc8;Ksvpas$tN5REe~0XYwf|VYt(5(|6v($+`8JQ`TYo9g7t#?)CXRceMx`WX=2BFB?bM9^(wJYrmcNFG#7O znoTcVM3{xcki^D<0yEqtu7?4%)7U~T2vQwjxJWJ}H=N$Wu{)>6l$%@Y5^c62PH1d@ zrB=tHS`9f<(?zsEtMipwU5jcp4q=*9zf#AI`figE#7T05z{=z5Ux`v z2m@cK_sD>;Mq%0U)hfWyc-xeHq>bal2D&SQ9TDN-~MWapBRaa+?lF&YzO znS{|??jd<)pmx#RGyej(4p?Ql8bAtq?D~DyRhli`e6QoaH?y@v+uD)d(y4CVukPq6z7%l@BGOrXRXP(Wz73U<+aN3O%o@zmObfUyV|f5 z`W4~qpK?~`In}B)W1%m0U)w!??0!(&(3Wo8qpoRJEB8*j_PxZ~G9J3K`{wS6V-JGb zTb=3k`_-1;sFlB+cD;6JVweW+Z|tDMSd#KrfFH9K19-U@yEv!QFFu$VFZv5;=_}MAjye zN0vnH)smNx03r2+kUNrb!brr`lAjRcZD`~QxZNU&5I6i}(l~{l(G>Ukti{GTX1x|5 zWp-}aGhZp!kdq@!?Lj+v{vJufx5D#RBB>r;%I3cV;MgN5%e{D4zzNxuf8RDq;hrWT z_a6c>CWvxwbI86Jb}TUPxovY!^et&Ou#f{oeiO(HLs8T}(W?KThJT|CKUfg$Jnx;R fd_U3*6{2Pkp|cF4{WHeEkFI8_;VA-Qe8c|$PmnDV literal 0 HcmV?d00001 diff --git a/app/modules/chat/__pycache__/module.cpython-312.pyc b/app/modules/chat/__pycache__/module.cpython-312.pyc index ffc426a83a8882b961830d4aafa33f267ed6678d..9cb049052579bb8fc0dd7e6d3fc99ed45cb2558d 100644 GIT binary patch delta 3198 zcmZ`*YitzP6~1?8_Wkg9y$?TFKfz{jj4_0Q!BDW{5DF|06Ck6MWxX>tYi7L8on3>G zD^ta49ko@P-b(q`R%%lPK_3!7nm!bj{;JeJc8p|e)Rkzfs;$%?tJ+d&{@11||uN;p2J{%4Zc9#zY$MR}k(RQ0Y%dh!`>@fpYPzws#Ylf__78bJ4j97JAosj*e zV%2N)LJpWwt3hj^3m7Im|NrT3-L>e}B;BJ+i%^z1s(PIOj()D7mrNs_^VLRyH&z-3qZ@RQ`=mD4l+5eB z4aceOsx5lT`<(lh_+aa;OS5F2WaOI1y!Q^Z1C(o0+eEQ$TkC!R^uY&ww+AmaRCR>Z zqJdQqPk6eZ6|8y8d!QAnwTdk@X`b+Qe@vbyQwo19HArMC2Ja+IU@I&w$rDtU7k%^Y zsU4h#8nW&OTf;-R?di=C)suD_$4;NSa2D!!{-x2(=(P)HPo7R^&Zp15=Ey}>GLB5mV8bxDz)^VGK+Xi!*GIrjYF^x+7`TGJNM|mjMdh3el5nK#y_mFEnx-EAb{G6A2o{kYUnr4Ui zao@i9|163FZ63Yt)9DvGVF_x0^}~x{QW+{3u873}9O=@@Q=-m}C&!Fz(O^-u4Fjl> z<7LJhCBrT{9zCBm3zO_H=xrosd;2{oRXQ+2Pw;OCe%+YcI?ljE2<}~+!p3gOZwK4= zvHrKDsplPp7a?En`fNR~C^4?d0six#(mY1MX}cI(J>fOw#|_CJ23rG2L-EY>Hh;Wv z-Zh1!{nP{7nT9%!jU6IiANa9!o2ss~>T7PV2B$ zkles+$o9!l^YJriKMUZvXR~I>P-QlXt-8E!SThCKY`MA2En^NgImE7}3^M}k#+-d4 zbaMC$(6mbc&;BMD9w16=t*-CGy1t+7T-!gqvVVAae|kODv6kvvN%gIzhE`HT?_XU_ zjjkm}mzB}=rWF5EXn(((G`>uqtv8(g$i~{fd1yFFe@TZU@~^r;dfZBf75Q=7K9GN_ z(BUZW3nxFx736{7%JuL|)Kgs;e=pJ(d=U&)zWDglNb>OZn%S#pxq@&N;TnJ{F=4-m zicc}5ziyCT=J~oGQOXtNJxU+_LSYoHsTZPc@m}cQ1WVI)Y241S{J5cdSccz-rV;@alt;|-o;=)S^l#&^0l zUHrT8)WVbV*_9*NrIuCy_@;;S4!j>*9=-Bp&((_C>%T(R<2@A#g{P^Ws;1&0k%OCF z)NM#4kfRkB%9SVyDBo;;t9d!L=gHA?OUD1C=_I z6cXPhVqkK|BO48%+&nz!PE^OoQB~CmWHJ*aAhVIlFg&N(J^;s4oSVr{PcqyIj&~vp zXWdL*@J&$5sozrVWm|lB;>3PO^&z z_y@^W8sz0<2Y?@$7@VCbR zDiWo%OgjHe{AJSqSF)>2`u{?9m&tIM43){jG8y0(ni^Aol$5f3q%04V<{ delta 2546 zcmZWqUu+!38K2p|x4U=iyYu>N-}%qlPC_r>*rudOAWdr=f*{!Z38d*F)jr?ug$uj3 zJG+N;sgWbq52i0rMir$`4WNq35mFUZ0*OB_ytNNV@zWNoMQWw?ZCks7QdRAIvv*D? zBkgZ~^L_Ju^UXKk%qA!Qb+7rmTuw)<#UEa8-k<-eS=vB*J8b+Ka*>OD6krDjf+JuU z3%(dgj>Ng>%You3oJ+nMWSk7=vabcYqjRqKMv!%~oU48=FddWgjNcRFojm87UkHj$ zk#pVe4VEp(;se9)3rbFj^Q_+=3^)Uv=lsE-?39CwQwfHgpb+~x8S@TyfU+}`Pxw>*8S|wf;$L8E~wdg5t zZ8dB%Nt?cp57A!;qjVR)y;KL=Zp(Ja-bqGYLmqOliyXnlp6CjmhqK}fN} zCbY=PLS5;`!g@GEk$rmu^prUI&FzkA9W8dYk5Hs`glJvf#wlldsUxM^;%I(dV3LD* zQGtG>jY#Y0N`~T`MW>}c8MBeeM!kl(B{i^%H#6(UzH z9D^_Z%1c#88)wTu3A&IwdNGRI8#4@Zn5dU z$OHIVHS=sw^_;~@8Xs-vCyhzwGtIa{f0OS&c@R2z1T>EVoCkP}0ZTR4KLmoW=K0ma zDSVFJU-%V9^s~a-7~T9ykzj1o|MZUb!R?KW)=Jn~4(p`3>{S(VfzDXNC2+!hc5m&q zYmg1sx`Z#?{J_fN_uK}v3et?L2(XX0pJG=c+gAi1Z|s)vvHhT zYc2cD`eJHwbztr}`g(s!nSr_grN8YzR7?a_Gk2<|69oP2Gy91Cp?`Q|t8z=5-&fGx zGw)iTJalpA==c6E%i2Z!Y2RIm2;|Sh_Al>y-ATBVO*HL&736y&(idU6d7RlWnv}W8iI>W+%-_<+6QQ6zgKzXE z2>P9w2lqJvgNam^96|ZgR*oEETdEqgJa~8m9ypQ%fC44(rV)6$2yB~YpDZ)>Ys8Is zqDVie0N@o5=NW9GL@3GnpGsqi%=$z@)=;7X%b@&lq5;!U|B=K1mPLINNe-C#2-3~H z9-z}F7$Bn<{mJuY-a$A}lMt*)?tF_EcV9>fY=Do*hM^0TU$F8@PBxCtj_-D$3l<1(5!pxf3#64(f7&*dl{}*D%hYORPLr9mp^Q! z`)e@s+k5^pb2{~MhP0RhvoAc$AQ3Ufe?*6O(bQkiH+IqKUG%M8bbJ@h?4rqCG|LIY a2R;`REc_Dzm9cyI0losm%P>~ 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 F{{^&0VFUmG delta 4260 zcmaJ^YfxLq6~3#h7m|=ZIW8Tv4yWa8Fwbr=`{VJg#77arrmQTAxz!w zjK2Hr*|TTQp4~mWN`2`kUq<@(^7AzeE&ZRo8W?X`(HEhQR$PAVT*0XV#BdC!4z9dX z?v*h-nr#NX2Mce%g3^*x#ascWk+M&5OI?f|v41N!WIb*w6}5go zG9K{>Pmc#U>;ef(C8F~1@X*kpS5%$x;ZPvt7gZS@Q9c*ucw9^xG<&o=sXlH4XeVJ! z86E98&2H34-qAe5dR9Eds6E5#FX+iPw9TlC{8DR|eU=})tqrrNHFnVOQ^YnbvE-c5 zEW=KsFDxca1y!=pMPf8r$zKa9QJ4x#ke$>Oez&UlYDqTCE3%7LIWr|YtL93-BfILj zqM#nog2;%DLuHrDmszS!uA19WIk|7%rEqdPxQa^#2RT|$hAJwg_2lE1Z@e>JOl8~V&yZEYv=ci;y8utvvEbCw{{(}C#SII;Ur~P2a zX=}N4T3*GxfN=BKyofv_XJ(I)*RACpC$sgXyv;sLc5C3q$P>Bt*;83g7d2&VKYX?N z3Tyyc&T2xXIaRKPb44bk1`4y2S(f{5?YX2`V#ixa)P7P`%elF_tM%k#+ac6Inu=^v z@Vblb#8*_OYP^7!G_$A3uZn6fsmF<_02M_CF9+v?I!X{aYbe!Zx>ga+B~9G+Oa~!7g-S8GTar@ zmpm%7b-m=aVJ zoV1)fcMg}3KRN~xPt0Y9kt60S`zNBie#|**c?1m0XN7R+GQxXl#jVC&}@E4EwlD%rP%41S4)GhDLl40Cs?=^o4{oJjMZ# z43lqGJ+HuK%1LSUd&m>}OZ5;s*dZk$+yN>OTT$gb?(rP*3`urqit|0gV^}DB6hI2V z1h8HTqGIqF8b7>`EVx`%dqCU?AVs+d7{p474~9I8kC3-rI}SAf^-&Pz1H+y{tb{sv zjDrB+uBhzy96dScmDWPcJMc)qXK3)~;4usbO;pj8NtX|kw%UW*5cCNH%#-uAJ;u#o zr(?gLBxol z1CJLXZ>xMcZtAb0wM3_dj<}{rqh|Si5^4>|}54M#npqw>oxFe!Bsk*r~kT*sr9OWSer-#w0uYJE-!VX7ogx@=nXHWm*wy z&?&c4bk$IPH=m_icZ~>mfyB+=$6cFsw4J@%h(?>$cbk;J+gU2d3pD<27Q-^&&1A79 zfjrnSmAw%lmFLl4?Bx~DXSd;_9&w`&50MhB&^w6!?;@pem`yJ(4X|pa;)o3|c=`@j#I&`uPhWxGL$$aWT z+%|B{e|_PW`Zzhh=dA*`(WQ6Jzi2pKVwMnj!DcST?<}ENh|jiy-yl&{J(85jIlbC? zR<rME&r=v&If>nZ?7Le7K7+sV$3;$7#|rTh7JIt@VgTF5nA>{o|o9Y^{Y5 z7cCl)Bu;@Bgg`jtq%BUVMKon}vCls~7ZB!rk*S$z<2H3J%nMSVNj?}3`JIullj>)! zMNc-5)5IGNKEsb^1`wPX0q2}tbY_Ogw9$8Q7uhIrEsGWyG%L+HYw`9EWo_p?jgI{P zL4v&BH?^4Uw{sqcXZfi}S5`nr?Wq==88OtBs|C?Wr!3Idc-9Lc+VWq{I(;DyrkmBF zeVsnc(;1##2wE5^I-nVYC zzcjWombBC)EH$s4NxJ$HuD+X={;warW3u0~mc`3k;%!IXk0ytGiDBPO>tx(Gxo)r| z4OIz4)v794+ncEET{HAbs_pUJ{D=PK(@Fng!oPUa8jTyH_jHBN(+hFk7+5#jl16vJ z=$1%L!dUaUN}jJzX&7@!($tVJHLUuRO#_Lhfw*ZPt{aeib*yO}uep*H?Q0e7>n2;$ zRG%=_uMQ;}dJ_%3ADZ?_$_c!gCslamUXan@K+Y= z0pOOw`2g^Gz^wp~S@9Bp7T^FtAHcT&&H_vTK)T0Y1c00^Dk2N>K^{jbzsz6<$fbl{ z4h#VDzgMWIt9hPD$qE&9_sx!!f__sZ(gM>l1@=@vFg;^* zqzu5!j7Gm<0ai=IuIe2}WQrZDZ7Bx6@y^FGBw)oD^Nc9Q+FNG6J3vM1VSq2(TCsRu-{J~wU=o_Hl6u1j)I2eUT8006#Rpra9NKZEF>L8{cnW+W~3nx}`S^ zT+~cVZ5FDUVO8#c6}Y_PSOk{?+<=BbaJ#2qy@4<)aXKpTx~Ovx#gd@MbRPaMTIpmsFxvR67V% z#Sf~sV=IC$+Spk26Oq->VI!M{Aj|TrF|A&oGTG_vsWQYm6?r3Jw1s*T5Y=j}>Zoe# zRG;${BJB}yY99si73v;8^GN(=uzU91N8&F@d6%A{943zP218>)o-Q=v@VTf6q_CtbWP2dHbX@5be`Bop)H3Q^|TJlJEbYgtzxK z*XH}h)<5FU-b0A=;t0C%yr%NvJ%_2jH?Z2ht5S$b|Jp(lG#{_|#(fuf%a%3p=ipsretn%+7d^tBvTxC|PZ zX?!WPrHx`%ZBui~Ypu6mGuJI_vpp-=uxb_Av1G81y!rXwF53sq-=15FYB)nBC( z&xS`jXD)W$x&Cv@yP>%^wC&-QPHv@}&3oA~H#@eO@Fr&5iJ7hJ3^Tpdxs0Dy?t7J{ zTWM|&w>r62S9;TvCS7TAv*HyN+`>XfTIggJexq@m-b$Z-JnoHN+8Vvom5zDR1y{PT zx#UgGxRWzarCH|la_9XUPjB7zZmI4qb$j?;CwGteJm*U1XuQ+z_;g2_?qsI_I5OPL zjC3=z&oaYaX2i{m>?Xu)>Ln5fQiS$dQ4vWN{v-*+j}z=PNX-|ERtoys||<@a0s1ve%xu+pQFiB7A_DeIY7o`tGtE1#wRR?*3U`B|iu@ u&5$Wj&}nXu0eLBK9Jhl;Um*Dfn%F^8J4ksh|HO}RslN~q=H*{hUL4c_ literal 0 HcmV?d00001 diff --git a/app/modules/chat/dialog_store.py b/app/modules/chat/dialog_store.py index 16ad8f6..ea8f932 100644 --- a/app/modules/chat/dialog_store.py +++ b/app/modules/chat/dialog_store.py @@ -1,7 +1,11 @@ +from __future__ import annotations + from dataclasses import dataclass +from typing import TYPE_CHECKING from uuid import uuid4 -from app.modules.chat.repository import ChatRepository +if TYPE_CHECKING: + from app.modules.chat.repository import ChatRepository @dataclass diff --git a/app/modules/chat/direct_service.py b/app/modules/chat/direct_service.py new file mode 100644 index 0000000..fe6e063 --- /dev/null +++ b/app/modules/chat/direct_service.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import logging +from uuid import uuid4 + +from app.modules.agent.llm import AgentLlmService +from app.modules.chat.evidence_gate import CodeExplainEvidenceGate +from app.modules.chat.session_resolver import ChatSessionResolver +from app.modules.chat.task_store import TaskState, TaskStore +from app.modules.rag.explain import CodeExplainRetrieverV2, PromptBudgeter +from app.schemas.chat import ChatMessageRequest, TaskQueuedResponse, TaskResultType, TaskStatus + +LOGGER = logging.getLogger(__name__) + + +class CodeExplainChatService: + def __init__( + self, + retriever: CodeExplainRetrieverV2, + llm: AgentLlmService, + session_resolver: ChatSessionResolver, + task_store: TaskStore, + message_sink, + budgeter: PromptBudgeter | None = None, + evidence_gate: CodeExplainEvidenceGate | None = None, + ) -> None: + self._retriever = retriever + self._llm = llm + self._session_resolver = session_resolver + self._task_store = task_store + self._message_sink = message_sink + self._budgeter = budgeter or PromptBudgeter() + self._evidence_gate = evidence_gate or CodeExplainEvidenceGate() + + async def handle_message(self, request: ChatMessageRequest) -> TaskQueuedResponse: + dialog_session_id, rag_session_id = self._session_resolver.resolve(request) + task_id = str(uuid4()) + task = TaskState(task_id=task_id, status=TaskStatus.RUNNING) + self._task_store.save(task) + self._message_sink(dialog_session_id, "user", request.message, task_id=task_id) + pack = self._retriever.build_pack( + rag_session_id, + request.message, + file_candidates=[item.model_dump(mode="json") for item in request.files], + ) + decision = self._evidence_gate.evaluate(pack) + if decision.passed: + prompt_input = self._budgeter.build_prompt_input(request.message, pack) + answer = self._llm.generate( + "code_explain_answer_v2", + prompt_input, + log_context="chat.code_explain.direct", + ).strip() + else: + answer = decision.answer + self._message_sink(dialog_session_id, "assistant", answer, task_id=task_id) + task.status = TaskStatus.DONE + task.result_type = TaskResultType.ANSWER + task.answer = answer + self._task_store.save(task) + LOGGER.warning( + "direct code explain response: task_id=%s rag_session_id=%s excerpts=%s missing=%s", + task_id, + rag_session_id, + len(pack.code_excerpts), + pack.missing, + ) + return TaskQueuedResponse( + task_id=task_id, + status=TaskStatus.DONE.value, + ) diff --git a/app/modules/chat/evidence_gate.py b/app/modules/chat/evidence_gate.py new file mode 100644 index 0000000..6d12257 --- /dev/null +++ b/app/modules/chat/evidence_gate.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from app.modules.rag.explain.models import ExplainPack + + +@dataclass(slots=True) +class EvidenceGateDecision: + passed: bool + answer: str = "" + diagnostics: dict[str, list[str]] = field(default_factory=dict) + + +class CodeExplainEvidenceGate: + def __init__(self, min_excerpts: int = 2) -> None: + self._min_excerpts = min_excerpts + + def evaluate(self, pack: ExplainPack) -> EvidenceGateDecision: + diagnostics = self._diagnostics(pack) + if len(pack.code_excerpts) >= self._min_excerpts: + return EvidenceGateDecision(passed=True, diagnostics=diagnostics) + return EvidenceGateDecision( + passed=False, + answer=self._build_answer(pack, diagnostics), + diagnostics=diagnostics, + ) + + def _diagnostics(self, pack: ExplainPack) -> dict[str, list[str]]: + return { + "entrypoints": [item.title for item in pack.selected_entrypoints[:3] if item.title], + "symbols": [item.title for item in pack.seed_symbols[:5] if item.title], + "paths": self._paths(pack), + "missing": list(pack.missing), + } + + def _paths(self, pack: ExplainPack) -> list[str]: + values: list[str] = [] + for item in pack.selected_entrypoints + pack.seed_symbols: + path = item.source or (item.location.path if item.location else "") + if path and path not in values: + values.append(path) + for excerpt in pack.code_excerpts: + if excerpt.path and excerpt.path not in values: + values.append(excerpt.path) + return values[:6] + + def _build_answer(self, pack: ExplainPack, diagnostics: dict[str, list[str]]) -> str: + lines = [ + "Недостаточно опоры в коде, чтобы дать объяснение без догадок.", + "", + f"Найдено фрагментов кода: {len(pack.code_excerpts)} из {self._min_excerpts} минимально необходимых.", + ] + if diagnostics["paths"]: + lines.append(f"Пути: {', '.join(diagnostics['paths'])}") + if diagnostics["entrypoints"]: + lines.append(f"Entrypoints: {', '.join(diagnostics['entrypoints'])}") + if diagnostics["symbols"]: + lines.append(f"Символы: {', '.join(diagnostics['symbols'])}") + if diagnostics["missing"]: + lines.append(f"Диагностика: {', '.join(diagnostics['missing'])}") + return "\n".join(lines).strip() diff --git a/app/modules/chat/module.py b/app/modules/chat/module.py index 10c8964..e967bb7 100644 --- a/app/modules/chat/module.py +++ b/app/modules/chat/module.py @@ -1,13 +1,16 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + from fastapi import APIRouter, Header from fastapi.responses import StreamingResponse from app.core.exceptions import AppError +from app.modules.chat.direct_service import CodeExplainChatService from app.modules.chat.dialog_store import DialogSessionStore -from app.modules.chat.repository import ChatRepository from app.modules.chat.service import ChatOrchestrator from app.modules.chat.task_store import TaskStore -from app.modules.contracts import AgentRunner -from app.modules.rag_session.session_store import RagSessionStore from app.modules.shared.event_bus import EventBus from app.modules.shared.idempotency_store import IdempotencyStore from app.modules.shared.retry_executor import RetryExecutor @@ -20,6 +23,11 @@ from app.schemas.chat import ( ) from app.schemas.common import ModuleName +if TYPE_CHECKING: + from app.modules.chat.repository import ChatRepository + from app.modules.contracts import AgentRunner + from app.modules.rag_session.session_store import RagSessionStore + class ChatModule: def __init__( @@ -29,12 +37,16 @@ class ChatModule: retry: RetryExecutor, rag_sessions: RagSessionStore, repository: ChatRepository, + direct_chat: CodeExplainChatService | None = None, + task_store: TaskStore | None = None, ) -> None: self._rag_sessions = rag_sessions - self.tasks = TaskStore() + self._simple_code_explain_only = os.getenv("SIMPLE_CODE_EXPLAIN_ONLY", "true").lower() in {"1", "true", "yes"} + self.tasks = task_store or TaskStore() self.dialogs = DialogSessionStore(repository) self.idempotency = IdempotencyStore() self.events = event_bus + self.direct_chat = direct_chat self.chat = ChatOrchestrator( task_store=self.tasks, dialogs=self.dialogs, @@ -59,11 +71,13 @@ class ChatModule: rag_session_id=dialog.rag_session_id, ) - @router.post("/api/chat/messages", response_model=TaskQueuedResponse) + @router.post("/api/chat/messages", response_model=TaskQueuedResponse | TaskResultResponse) async def send_message( request: ChatMessageRequest, idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"), - ) -> TaskQueuedResponse: + ) -> TaskQueuedResponse | TaskResultResponse: + if self._simple_code_explain_only and self.direct_chat is not None: + return await self.direct_chat.handle_message(request) task = await self.chat.enqueue_message(request, idempotency_key) return TaskQueuedResponse(task_id=task.task_id, status=task.status.value) diff --git a/app/modules/chat/service.py b/app/modules/chat/service.py index 2ae6277..abaf539 100644 --- a/app/modules/chat/service.py +++ b/app/modules/chat/service.py @@ -6,6 +6,7 @@ from app.modules.contracts import AgentRunner from app.schemas.chat import ChatMessageRequest, TaskResultType, TaskStatus from app.schemas.common import ErrorPayload, ModuleName from app.modules.chat.dialog_store import DialogSessionStore +from app.modules.chat.session_resolver import ChatSessionResolver from app.modules.chat.task_store import TaskState, TaskStore from app.modules.shared.event_bus import EventBus from app.modules.shared.idempotency_store import IdempotencyStore @@ -41,6 +42,7 @@ class ChatOrchestrator: self._retry = retry self._rag_session_exists = rag_session_exists self._message_sink = message_sink + self._session_resolver = ChatSessionResolver(dialogs, rag_session_exists) async def enqueue_message( self, @@ -52,7 +54,7 @@ class ChatOrchestrator: if existing: task = self._task_store.get(existing) if task: - LOGGER.warning( + LOGGER.info( "enqueue_message reused task by idempotency key: task_id=%s mode=%s", task.task_id, request.mode.value, @@ -63,7 +65,7 @@ class ChatOrchestrator: if idempotency_key: self._idempotency.put(idempotency_key, task.task_id) asyncio.create_task(self._process_task(task.task_id, request)) - LOGGER.warning( + LOGGER.info( "enqueue_message created task: task_id=%s mode=%s", task.task_id, request.mode.value, @@ -135,6 +137,13 @@ class ChatOrchestrator: task.changeset = result.changeset if task.result_type == TaskResultType.ANSWER and task.answer: self._message_sink(dialog_session_id, "assistant", task.answer, task_id=task_id) + LOGGER.warning( + "outgoing chat response: task_id=%s dialog_session_id=%s result_type=%s answer=%s", + task_id, + dialog_session_id, + task.result_type.value, + _truncate_for_log(task.answer), + ) elif task.result_type == TaskResultType.CHANGESET: self._message_sink( dialog_session_id, @@ -146,6 +155,14 @@ class ChatOrchestrator: "changeset": [item.model_dump(mode="json") for item in task.changeset], }, ) + LOGGER.warning( + "outgoing chat response: task_id=%s dialog_session_id=%s result_type=%s changeset_items=%s answer=%s", + task_id, + dialog_session_id, + task.result_type.value, + len(task.changeset), + _truncate_for_log(task.answer or ""), + ) self._task_store.save(task) await self._events.publish( task_id, @@ -160,7 +177,7 @@ class ChatOrchestrator: }, ) await self._publish_progress(task_id, "task.done", "Обработка завершена.", progress=100) - LOGGER.warning( + LOGGER.info( "_process_task completed: task_id=%s status=%s result_type=%s changeset_items=%s", task_id, task.status.value, @@ -232,7 +249,7 @@ class ChatOrchestrator: if progress is not None: payload["progress"] = max(0, min(100, int(progress))) await self._events.publish(task_id, kind, payload) - LOGGER.warning( + LOGGER.debug( "_publish_progress emitted: task_id=%s kind=%s stage=%s progress=%s", task_id, kind, @@ -259,35 +276,7 @@ class ChatOrchestrator: meta={"heartbeat": True}, ) index += 1 - LOGGER.warning("_run_heartbeat stopped: task_id=%s ticks=%s", task_id, index) + LOGGER.debug("_run_heartbeat stopped: task_id=%s ticks=%s", task_id, index) def _resolve_sessions(self, request: ChatMessageRequest) -> tuple[str, str]: - # Legacy compatibility: old session_id/project_id flow. - if request.dialog_session_id and request.rag_session_id: - dialog = self._dialogs.get(request.dialog_session_id) - if not dialog: - raise AppError("dialog_not_found", "Dialog session not found", ModuleName.BACKEND) - if dialog.rag_session_id != request.rag_session_id: - raise AppError("dialog_rag_mismatch", "Dialog session does not belong to rag session", ModuleName.BACKEND) - LOGGER.warning( - "_resolve_sessions resolved by dialog_session_id: dialog_session_id=%s rag_session_id=%s", - request.dialog_session_id, - request.rag_session_id, - ) - return request.dialog_session_id, request.rag_session_id - - if request.session_id and request.project_id: - if not self._rag_session_exists(request.project_id): - raise AppError("rag_session_not_found", "RAG session not found", ModuleName.RAG) - LOGGER.warning( - "_resolve_sessions resolved by legacy session/project: session_id=%s project_id=%s", - request.session_id, - request.project_id, - ) - return request.session_id, request.project_id - - raise AppError( - "missing_sessions", - "dialog_session_id and rag_session_id are required", - ModuleName.BACKEND, - ) + return self._session_resolver.resolve(request) diff --git a/app/modules/chat/session_resolver.py b/app/modules/chat/session_resolver.py new file mode 100644 index 0000000..653523b --- /dev/null +++ b/app/modules/chat/session_resolver.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from app.core.exceptions import AppError +from app.schemas.chat import ChatMessageRequest +from app.schemas.common import ModuleName + +if TYPE_CHECKING: + from app.modules.chat.dialog_store import DialogSessionStore + + +class ChatSessionResolver: + def __init__(self, dialogs: DialogSessionStore, rag_session_exists) -> None: + self._dialogs = dialogs + self._rag_session_exists = rag_session_exists + + def resolve(self, request: ChatMessageRequest) -> tuple[str, str]: + if request.dialog_session_id and request.rag_session_id: + dialog = self._dialogs.get(request.dialog_session_id) + if not dialog: + raise AppError("dialog_not_found", "Dialog session not found", ModuleName.BACKEND) + if dialog.rag_session_id != request.rag_session_id: + raise AppError("dialog_rag_mismatch", "Dialog session does not belong to rag session", ModuleName.BACKEND) + return request.dialog_session_id, request.rag_session_id + + if request.session_id and request.project_id: + if not self._rag_session_exists(request.project_id): + raise AppError("rag_session_not_found", "RAG session not found", ModuleName.RAG) + return request.session_id, request.project_id + + raise AppError( + "missing_sessions", + "dialog_session_id and rag_session_id are required", + ModuleName.BACKEND, + ) diff --git a/app/modules/rag/README.md b/app/modules/rag/README.md index a30afbc..e0b9c35 100644 --- a/app/modules/rag/README.md +++ b/app/modules/rag/README.md @@ -90,6 +90,41 @@ sequenceDiagram Rag-->>Agent: items ``` +### Retrieval + project/qa reasoning +Назначение: `RAG` вызывается не в начале runtime, а внутри отдельного graph-шага `context_retrieval` для `project/qa`. +```mermaid +sequenceDiagram + participant Agent as GraphAgentRuntime + participant Orch as OrchestratorService + participant G1 as conversation_understanding + participant G2 as question_classification + participant G3 as context_retrieval + participant Rag as RagService + participant G4 as context_analysis + participant G5 as answer_composition + + Agent->>Orch: run(task) + Orch->>G1: execute + G1-->>Orch: resolved_request + Orch->>G2: execute + G2-->>Orch: question_profile + Orch->>G3: execute + G3->>Rag: retrieve(query) + Rag-->>G3: rag_items + G3-->>Orch: source_bundle + Orch->>G4: execute + G4-->>Orch: analysis_brief + Orch->>G5: execute + G5-->>Orch: final_answer + Orch-->>Agent: final_answer +``` + +Для `project/qa` это означает: +- ранний глобальный retrieval больше не нужен; +- `RAG` возвращает записи только для конкретного шага `context_retrieval`; +- оркестратор управляет цепочкой graph-шагов; +- пользовательский ответ собирается после анализа, а не напрямую из сырого retrieval. + ## 5. Слои, фиксируемые в RAG ### 5.1. Слои DOCS diff --git a/app/modules/rag/explain/__init__.py b/app/modules/rag/explain/__init__.py new file mode 100644 index 0000000..de44c1d --- /dev/null +++ b/app/modules/rag/explain/__init__.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from importlib import import_module + +__all__ = [ + "CodeExcerpt", + "CodeExplainRetrieverV2", + "CodeGraphRepository", + "EvidenceItem", + "ExplainIntent", + "ExplainIntentBuilder", + "ExplainPack", + "LayeredRetrievalGateway", + "PromptBudgeter", + "TracePath", +] + + +def __getattr__(name: str): + module_map = { + "CodeExcerpt": "app.modules.rag.explain.models", + "EvidenceItem": "app.modules.rag.explain.models", + "ExplainIntent": "app.modules.rag.explain.models", + "ExplainPack": "app.modules.rag.explain.models", + "TracePath": "app.modules.rag.explain.models", + "ExplainIntentBuilder": "app.modules.rag.explain.intent_builder", + "PromptBudgeter": "app.modules.rag.explain.budgeter", + "LayeredRetrievalGateway": "app.modules.rag.explain.layered_gateway", + "CodeGraphRepository": "app.modules.rag.explain.graph_repository", + "CodeExplainRetrieverV2": "app.modules.rag.explain.retriever_v2", + } + module_name = module_map.get(name) + if module_name is None: + raise AttributeError(name) + module = import_module(module_name) + return getattr(module, name) diff --git a/app/modules/rag/explain/__pycache__/__init__.cpython-312.pyc b/app/modules/rag/explain/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f717daff0ef8a5e6896b3ba833856996d83104f GIT binary patch literal 1179 zcmZ`&&1(}u6rb7MCT+F}m{$9N+Sp1pAXyMUQ3O9?tx75Ns@!s#fH#7TtZ$IYqIRxuz<*oC< zKKlehzac#z>nX-$t_CStZr!B^r zogPHpIV#vg+LDDu9(bX+(6H-FK-JJqPHSwDihDJj2+~CpP)7*wJOcy7!M#Vbb=$B9 zK#uII+#|e(@BdquZn1{mr|ctbP3xEf_Da^@$YU%9daTyV*G0C+>X4nb=>{qfF0}X4 zFsvlM@y1^dlY(rJ35@KqeJdm6c%qx?|mn7>_LKvQ_I6Ym$%$_gPIu za8HXmC7U}<<|{2r5UH^!vQ(fBGim{8rTbaXC@XE0MTp}&f{;qsk|rVPzQOzpZ18 XKcVTbXy%7unp!9ObMG#Epj-GGGEH6L literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/__pycache__/budgeter.cpython-312.pyc b/app/modules/rag/explain/__pycache__/budgeter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3953cd8546f60392608d1b75a2bee2fbdaaf3bee GIT binary patch literal 3703 zcmbsrTWk~A^^Rx!w&Rd^?bu07NF2Zv*}!JA%L?somIOqWhE)kAbzw)wGnlc*kG(U_ zl4#uB4?aSh6`+2^QIV2WyV_RlN2LDnN!6;0)E`d*%9AgY;K6RBXrQ|D;;U=gwm%mgLCBs8D{M9s% zOR{82rLUy0l%_zLERrA+DtwlDb_|duq)-ZC0W+6kfn8G?DKzXhQ-hGgz>YZ7SaXn} zDe%R`Zad>?;7xD5jop?zreMRLnAKn;Uc8LSQDQd%9s;}oB$CVlvcCT)aZci~lt_vE z)xPPMVjb1Yuw%ScWZvk8XK?;2#$bHkga1(L@egf4(j6;H*hcogre?EccS7>0b(6>r6(*ueIYb~`|Ydegc% z`)y~XLw>VK&ywcbvXlJd_ zE*H`%nddIX?1r146*#_-=2?L=tg;}ByupyZXt*Rfg=JP03cTUq3!I5rFBj8g$*`x3 z1sR%z!DOXk0UA34oq~T%8c)n=I6f}(66yJdO)$m$(5s;B@Ei+Dc&=0gIy75}2THTq zxn-OJKg_1wvF%rZLY)VQC z0`UOilVkmuJUHwF0HHOyJ%lV7w8$4Qsh5Tg8Xm-n$(Ud`w?r^08HQDou~5QYfa6^a zX>3y9k|0c~7bay0_Yy!#5AG*m2!P=txe-~eoG)QgJ@7CA`v}V_$b4SH7(K>omj zLk|QO+?!g)hZjzM>kCygS|qU^NvxgJMkdseiTg=)<*cjWgHJ{A1{VHZ-aZ zjq3f;m5a+4^}eB%)61vz!I(BUq7IJe1N*drL+ZexCV%(P6Q5&O*K^wC?WhNkx99fj zRryy(RZsN#2lW`lRX>PAd-OnfDR(QU1rDl#gKK+hfrA@?qx#g3w5g0bm8o6H)`Vi7StKg}c+$@s%^nXVwDMGY|XT zTJII~=^1T0qfTeE>AX6f-j@;c_i;iqW$JE_p8UJ&Ip9fH<|Hl0F`Rcwk>)q4?-+ML3doP~uLc@9Lh0&LP zVe|?w{u(JsUhFeB%gBdR^bV6PoPu+#l_q40cY%T#YH&prPoPqw@P8ZqCaM zjEtF>V1^Czav2v4W~x}=V>a{c(eTnSDbAQ7;5Y#=QzDqbVg4XB03FaZ(hFk}7bv*L{?Rj{=b&N7C5hF1h@$GUM;sQT0X&+{li9o%J zZw~lz81Unp13sbgqGSf!aPt(=*>e4R$1zju9ef%XWIC2e?W@xiMg1KOeuv!uM5F(< NjZ$OOQ$%1x_dnLWScL!p literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/__pycache__/excerpt_planner.cpython-312.pyc b/app/modules/rag/explain/__pycache__/excerpt_planner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c24cfe6852ddeb863736362310d3dd46f444608 GIT binary patch literal 2847 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 OmJ?=X_m>1ImGHmd_Pxpg literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/__pycache__/intent_builder.cpython-312.pyc b/app/modules/rag/explain/__pycache__/intent_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fad4aa5cd2faeb93d561ca98e53c773917212c2 GIT binary patch literal 6777 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 C@-_4T literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/__pycache__/layered_gateway.cpython-312.pyc b/app/modules/rag/explain/__pycache__/layered_gateway.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ec6b1ac13fab0f2d605f4c30ff1ec6a37fde9e4 GIT binary patch literal 10952 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 M7~59_Q@Yy!2Qj|;ivR!s literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/__pycache__/models.cpython-312.pyc b/app/modules/rag/explain/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78c41260e8bb5be91223c012df27aeeed6f1eacd GIT binary patch literal 4574 zcma)AOKcm*8Rm++3RaedRbT*uRg zc=RKu#^XztPnl`!QLbGvJ$lzA)aHq+uCr>bU9(C)S8iC;CZ*t01MGdb^lB64pGIh; zD3M5U=mA)VMv3%d=nE;hu_%KCEcGI~BZ(84szkX6H|*1CLKhpTO9)+jpe`wND(Y&Q zp5UqLPb;=*Ikzmw_jqj8WsfZKxx^Lv)Mq9Ph6!CYtF~{fnkC<5wbz<$dHzGsXDeK( znEtxQ<6f=&$hAGLQHNAqv6EVI%VpCcUO`WB)i4~hObvr445RFls*U56VSG|G?Lfog z@J2iZN@^P=VN#}|NXEypQ5+5%Mu--}U}GR<;|N)R&m*q@Bd>p-f8bH(jQ8l_SjBpia)LRC1sxQP0bxVvRa}9)$Ve1-(2o*YY&54$7$QovhRt zhKm4vJMR|2pCX;{BY(No8vmfN^uxia&z4&UPdBbVKi`y3cMc!@%x@h&+qn7scTM?h zXEOIP+nStjEIzL_<@wIU^h>!lajtRexz&=-v8nB?2Yb+8LH%YAB2lUk8MzgsZO@6( z_>0(%SE&$r3K1RvUKOKSNSA>4Q%FMWkSr!i3TA1Xi95r)q(}yI!{W}7VjAL0A!9hJ zjq6#S?s?UsVPsHd#PM;w=xP1~wd5=dAE?;XK=)yK5<#AU=2 z=H|*eI@S#?KXP50r!A*sR|z$I>iJ%MN;qXNKp)Po`+jA1E;n1Ut=W|oo&_4g0pJ<< zHZ{YqRVX-nnG&m7t{>|!v)0yeS)jlXp|mm!j!tOBUoYqx_8z|80fd7H=(X$+!UO{P z1Urmy96|WX432(*FpHogOd?Dn96^}g26S`pAwbxtz8@ZYR%jnM+>{T8J*Fw2!)dBD zaS6QbVpG1359$}AR-$xk&n%Vm~z%Iik($5y#M~*daZzh`ZvG(LN zMB>LSc{(r=t{ZHk258>dL>QAI5=Bgkj4ejn`5B{A4Mk@-0=0ewl4LQ_kGWb>AK}?0 zvql*u_o>e;x?$RjJ}vJxa?Eq9tVDSnk}3uxS8QCx72oo0D#ERvAyBzFDZJR&CeZp-1sbE~Brn#or`kQ5_2gUV3i3KWlg)!A*#g3Oggx|~LKy-w z=UoHXMPF+8*`0QJYMc^zQ`6I2TV`g7-h;3R z(b&(3iJ;IL7V*~LCB=^`f+1lR2Q1^Oea(? zTk`3E8r*O&HS3^xGc}epb?0Ak=zNsP`K4pJp;|rhDE4TqJsh} zNqb5#ZvFI54e1*l>qkHsjwo59;`+2iS;dEbL@7i>jkrs6C?o@MhpR6IJXSs^$V}1G z6m_*ReViwIo#{QZ^f=@b*bea+G$I(j7=HDMprag6VWFS&s2%DjdqN1?;)orUVd zV*O4&U*SnOPnD=4j!gA~{fZmTF)KXN&nga~PwS&%n~)=~@N@~>+Mt17dqJ-pFI%2x zIcr?Qvy%;_6%Jk_Z%G;bY;h01nEe{zJ_6SYRU6~&*Li<^ie!*u2%gjgVQF9f{e$Y z^9L(2872(>y}{K=jhK#al~@dAqMsLb5xu);%taH41x{2tkw8VniZ9<|lGp0R;P`l% z6=4OQ?Y&O`{u`Af>D$Q9evC~27&-O#$f<5DCe3f2??&L#9n+)_Hy67RxO5LGQV!*C y>5gcU{%07&rF$qQo!Qjz{xjXNc(4{O-E2ad+SI!dxO68H(!!?xKNtsH{{I0_iZcQL literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/__pycache__/retriever_v2.cpython-312.pyc b/app/modules/rag/explain/__pycache__/retriever_v2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d59a35095498fab1e8907bcb9fd16b5a7182dd19 GIT binary patch literal 15760 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 K=p}-|67*ltat{Ll literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/__pycache__/trace_builder.cpython-312.pyc b/app/modules/rag/explain/__pycache__/trace_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28cce2b2eee21909defea7d22b99d5c144f85ba6 GIT binary patch literal 6423 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 literal 0 HcmV?d00001 diff --git a/app/modules/rag/explain/budgeter.py b/app/modules/rag/explain/budgeter.py new file mode 100644 index 0000000..adcddfd --- /dev/null +++ b/app/modules/rag/explain/budgeter.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import json + +from app.modules.rag.explain.models import ExplainPack + + +class PromptBudgeter: + def __init__( + self, + *, + max_paths: int = 3, + max_symbols: int = 25, + max_excerpts: int = 40, + max_chars: int = 30000, + ) -> None: + self._max_paths = max_paths + self._max_symbols = max_symbols + self._max_excerpts = max_excerpts + self._max_chars = max_chars + + def build_prompt_input(self, question: str, pack: ExplainPack) -> str: + symbol_ids: list[str] = [] + for path in pack.trace_paths[: self._max_paths]: + for symbol_id in path.symbol_ids: + if symbol_id and symbol_id not in symbol_ids and len(symbol_ids) < self._max_symbols: + symbol_ids.append(symbol_id) + excerpts = [] + total_chars = 0 + for excerpt in pack.code_excerpts: + if symbol_ids and excerpt.symbol_id and excerpt.symbol_id not in symbol_ids: + continue + body = excerpt.content.strip() + remaining = self._max_chars - total_chars + if remaining <= 0 or len(excerpts) >= self._max_excerpts: + break + if len(body) > remaining: + body = body[:remaining].rstrip() + "...[truncated]" + excerpts.append( + { + "evidence_id": excerpt.evidence_id, + "title": excerpt.title, + "path": excerpt.path, + "start_line": excerpt.start_line, + "end_line": excerpt.end_line, + "focus": excerpt.focus, + "content": body, + } + ) + total_chars += len(body) + payload = { + "question": question, + "intent": pack.intent.model_dump(mode="json"), + "selected_entrypoints": [item.model_dump(mode="json") for item in pack.selected_entrypoints[:5]], + "seed_symbols": [item.model_dump(mode="json") for item in pack.seed_symbols[: self._max_symbols]], + "trace_paths": [path.model_dump(mode="json") for path in pack.trace_paths[: self._max_paths]], + "evidence_index": {key: value.model_dump(mode="json") for key, value in pack.evidence_index.items()}, + "code_excerpts": excerpts, + "missing": pack.missing, + "conflicts": pack.conflicts, + } + return json.dumps(payload, ensure_ascii=False, indent=2) diff --git a/app/modules/rag/explain/excerpt_planner.py b/app/modules/rag/explain/excerpt_planner.py new file mode 100644 index 0000000..04f98ba --- /dev/null +++ b/app/modules/rag/explain/excerpt_planner.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from app.modules.rag.explain.models import CodeExcerpt, LayeredRetrievalItem + + +class ExcerptPlanner: + _FOCUS_TOKENS = ("raise", "except", "db", "select", "insert", "update", "delete", "http", "publish", "emit") + + def plan(self, chunk: LayeredRetrievalItem, *, evidence_id: str, symbol_id: str | None) -> list[CodeExcerpt]: + location = chunk.location + if location is None: + return [] + excerpts = [ + CodeExcerpt( + evidence_id=evidence_id, + symbol_id=symbol_id, + title=chunk.title, + path=location.path, + start_line=location.start_line, + end_line=location.end_line, + content=chunk.content.strip(), + focus="overview", + ) + ] + focus = self._focus_excerpt(chunk, evidence_id=evidence_id, symbol_id=symbol_id) + if focus is not None: + excerpts.append(focus) + return excerpts + + def _focus_excerpt( + self, + chunk: LayeredRetrievalItem, + *, + evidence_id: str, + symbol_id: str | None, + ) -> CodeExcerpt | None: + location = chunk.location + if location is None: + return None + lines = chunk.content.splitlines() + for index, line in enumerate(lines): + lowered = line.lower() + if not any(token in lowered for token in self._FOCUS_TOKENS): + continue + start = max(0, index - 2) + end = min(len(lines), index + 3) + if end - start >= len(lines): + return None + return CodeExcerpt( + evidence_id=evidence_id, + symbol_id=symbol_id, + title=f"{chunk.title}:focus", + path=location.path, + start_line=(location.start_line or 1) + start, + end_line=(location.start_line or 1) + end - 1, + content="\n".join(lines[start:end]).strip(), + focus="focus", + ) + return None diff --git a/app/modules/rag/explain/graph_repository.py b/app/modules/rag/explain/graph_repository.py new file mode 100644 index 0000000..65d40db --- /dev/null +++ b/app/modules/rag/explain/graph_repository.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import json + +from sqlalchemy import text + +from app.modules.rag.explain.models import CodeLocation, LayeredRetrievalItem +from app.modules.shared.db import get_engine + + +class CodeGraphRepository: + def get_out_edges( + self, + rag_session_id: str, + src_symbol_ids: list[str], + edge_types: list[str], + limit_per_src: int, + ) -> list[LayeredRetrievalItem]: + if not src_symbol_ids: + return [] + sql = """ + SELECT path, content, layer, title, metadata_json, span_start, span_end + FROM rag_chunks + WHERE rag_session_id = :sid + AND layer = 'C2_DEPENDENCY_GRAPH' + AND CAST(metadata_json AS jsonb)->>'src_symbol_id' = ANY(:src_ids) + AND CAST(metadata_json AS jsonb)->>'edge_type' = ANY(:edge_types) + ORDER BY path, span_start + """ + with get_engine().connect() as conn: + rows = conn.execute( + text(sql), + {"sid": rag_session_id, "src_ids": src_symbol_ids, "edge_types": edge_types}, + ).mappings().fetchall() + grouped: dict[str, int] = {} + items: list[LayeredRetrievalItem] = [] + for row in rows: + metadata = self._loads(row.get("metadata_json")) + src_symbol_id = str(metadata.get("src_symbol_id") or "") + grouped[src_symbol_id] = grouped.get(src_symbol_id, 0) + 1 + if grouped[src_symbol_id] > limit_per_src: + continue + items.append(self._to_item(row, metadata)) + return items + + def get_in_edges( + self, + rag_session_id: str, + dst_symbol_ids: list[str], + edge_types: list[str], + limit_per_dst: int, + ) -> list[LayeredRetrievalItem]: + if not dst_symbol_ids: + return [] + sql = """ + SELECT path, content, layer, title, metadata_json, span_start, span_end + FROM rag_chunks + WHERE rag_session_id = :sid + AND layer = 'C2_DEPENDENCY_GRAPH' + AND CAST(metadata_json AS jsonb)->>'dst_symbol_id' = ANY(:dst_ids) + AND CAST(metadata_json AS jsonb)->>'edge_type' = ANY(:edge_types) + ORDER BY path, span_start + """ + with get_engine().connect() as conn: + rows = conn.execute( + text(sql), + {"sid": rag_session_id, "dst_ids": dst_symbol_ids, "edge_types": edge_types}, + ).mappings().fetchall() + grouped: dict[str, int] = {} + items: list[LayeredRetrievalItem] = [] + for row in rows: + metadata = self._loads(row.get("metadata_json")) + dst_symbol_id = str(metadata.get("dst_symbol_id") or "") + grouped[dst_symbol_id] = grouped.get(dst_symbol_id, 0) + 1 + if grouped[dst_symbol_id] > limit_per_dst: + continue + items.append(self._to_item(row, metadata)) + return items + + def resolve_symbol_by_ref( + self, + rag_session_id: str, + dst_ref: str, + package_hint: str | None = None, + ) -> LayeredRetrievalItem | None: + ref = (dst_ref or "").strip() + if not ref: + return None + with get_engine().connect() as conn: + rows = conn.execute( + text( + """ + SELECT path, content, layer, title, metadata_json, span_start, span_end, qname + FROM rag_chunks + WHERE rag_session_id = :sid + AND layer = 'C1_SYMBOL_CATALOG' + AND (qname = :ref OR title = :ref OR qname LIKE :tail) + ORDER BY path + LIMIT 12 + """ + ), + {"sid": rag_session_id, "ref": ref, "tail": f"%{ref}"}, + ).mappings().fetchall() + best: LayeredRetrievalItem | None = None + best_score = -1 + for row in rows: + metadata = self._loads(row.get("metadata_json")) + package = str(metadata.get("package_or_module") or "") + score = 0 + if str(row.get("qname") or "") == ref: + score += 3 + if str(row.get("title") or "") == ref: + score += 2 + if package_hint and package.startswith(package_hint): + score += 3 + if package_hint and package_hint in str(row.get("path") or ""): + score += 1 + if score > best_score: + best = self._to_item(row, metadata) + best_score = score + return best + + def get_symbols_by_ids(self, rag_session_id: str, symbol_ids: list[str]) -> list[LayeredRetrievalItem]: + if not symbol_ids: + return [] + with get_engine().connect() as conn: + rows = conn.execute( + text( + """ + SELECT path, content, layer, title, metadata_json, span_start, span_end + FROM rag_chunks + WHERE rag_session_id = :sid + AND layer = 'C1_SYMBOL_CATALOG' + AND symbol_id = ANY(:symbol_ids) + ORDER BY path, span_start + """ + ), + {"sid": rag_session_id, "symbol_ids": symbol_ids}, + ).mappings().fetchall() + return [self._to_item(row, self._loads(row.get("metadata_json"))) for row in rows] + + def get_chunks_by_symbol_ids( + self, + rag_session_id: str, + symbol_ids: list[str], + prefer_chunk_type: str = "symbol_block", + ) -> list[LayeredRetrievalItem]: + symbols = self.get_symbols_by_ids(rag_session_id, symbol_ids) + chunks: list[LayeredRetrievalItem] = [] + for symbol in symbols: + location = symbol.location + if location is None: + continue + chunk = self._chunk_for_symbol(rag_session_id, symbol, prefer_chunk_type=prefer_chunk_type) + if chunk is not None: + chunks.append(chunk) + return chunks + + def _chunk_for_symbol( + self, + rag_session_id: str, + symbol: LayeredRetrievalItem, + *, + prefer_chunk_type: str, + ) -> LayeredRetrievalItem | None: + location = symbol.location + if location is None: + return None + with get_engine().connect() as conn: + rows = conn.execute( + text( + """ + SELECT path, content, layer, title, metadata_json, span_start, span_end + FROM rag_chunks + WHERE rag_session_id = :sid + AND layer = 'C0_SOURCE_CHUNKS' + AND path = :path + AND COALESCE(span_start, 0) <= :end_line + AND COALESCE(span_end, 999999) >= :start_line + ORDER BY + CASE WHEN CAST(metadata_json AS jsonb)->>'chunk_type' = :prefer_chunk_type THEN 0 ELSE 1 END, + ABS(COALESCE(span_start, 0) - :start_line) + LIMIT 1 + """ + ), + { + "sid": rag_session_id, + "path": location.path, + "start_line": location.start_line or 0, + "end_line": location.end_line or 999999, + "prefer_chunk_type": prefer_chunk_type, + }, + ).mappings().fetchall() + if not rows: + return None + row = rows[0] + return self._to_item(row, self._loads(row.get("metadata_json"))) + + def _to_item(self, row, metadata: dict) -> LayeredRetrievalItem: + return LayeredRetrievalItem( + source=str(row.get("path") or ""), + content=str(row.get("content") or ""), + layer=str(row.get("layer") or ""), + title=str(row.get("title") or ""), + metadata=metadata, + location=CodeLocation( + path=str(row.get("path") or ""), + start_line=row.get("span_start"), + end_line=row.get("span_end"), + ), + ) + + def _loads(self, value) -> dict: + if not value: + return {} + return json.loads(str(value)) diff --git a/app/modules/rag/explain/intent_builder.py b/app/modules/rag/explain/intent_builder.py new file mode 100644 index 0000000..cd4cc3b --- /dev/null +++ b/app/modules/rag/explain/intent_builder.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import re + +from app.modules.rag.explain.models import ExplainHints, ExplainIntent +from app.modules.rag.retrieval.query_terms import extract_query_terms + + +class ExplainIntentBuilder: + _ROUTE_RE = re.compile(r"(/[A-Za-z0-9_./{}:-]+)") + _FILE_RE = re.compile(r"([A-Za-z0-9_./-]+\.py)") + _SYMBOL_RE = re.compile(r"\b([A-Z][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*|[A-Z][A-Za-z0-9_]{2,}|[a-z_][A-Za-z0-9_]{2,})\b") + _COMMAND_RE = re.compile(r"`([A-Za-z0-9:_-]+)`") + _TEST_KEYWORDS = ( + "тест", + "tests", + "test ", + "unit-test", + "unit test", + "юнит-тест", + "pytest", + "spec", + "как покрыто тестами", + "как проверяется", + "how is it tested", + "how it's tested", + ) + + def build(self, user_query: str) -> ExplainIntent: + normalized = " ".join((user_query or "").split()) + lowered = normalized.lower() + keywords = self._keywords(normalized) + hints = ExplainHints( + paths=self._dedupe(self._FILE_RE.findall(normalized)), + symbols=self._symbols(normalized), + endpoints=self._dedupe(self._ROUTE_RE.findall(normalized)), + commands=self._commands(normalized, lowered), + ) + return ExplainIntent( + raw_query=user_query, + normalized_query=normalized, + keywords=keywords[:12], + hints=hints, + include_tests=self._include_tests(lowered), + expected_entry_types=self._entry_types(lowered, hints), + depth=self._depth(lowered), + ) + + def _keywords(self, text: str) -> list[str]: + keywords = extract_query_terms(text) + for token in self._symbols(text): + if token not in keywords: + keywords.append(token) + for token in self._ROUTE_RE.findall(text): + if token not in keywords: + keywords.append(token) + return self._dedupe(keywords) + + def _symbols(self, text: str) -> list[str]: + values = [] + for raw in self._SYMBOL_RE.findall(text): + token = raw.strip() + if len(token) < 3: + continue + if token.endswith(".py"): + continue + values.append(token) + return self._dedupe(values) + + def _commands(self, text: str, lowered: str) -> list[str]: + values = list(self._COMMAND_RE.findall(text)) + if " command " in f" {lowered} ": + values.extend(re.findall(r"command\s+([A-Za-z0-9:_-]+)", lowered)) + if " cli " in f" {lowered} ": + values.extend(re.findall(r"cli\s+([A-Za-z0-9:_-]+)", lowered)) + return self._dedupe(values) + + def _entry_types(self, lowered: str, hints: ExplainHints) -> list[str]: + if hints.endpoints or any(token in lowered for token in ("endpoint", "route", "handler", "http", "api")): + return ["http"] + if hints.commands or any(token in lowered for token in ("cli", "command", "click", "typer")): + return ["cli"] + return ["http", "cli"] + + def _depth(self, lowered: str) -> str: + if any(token in lowered for token in ("deep", "подроб", "деталь", "full flow", "trace")): + return "deep" + if any(token in lowered for token in ("high level", "overview", "кратко", "summary")): + return "high" + return "medium" + + def _include_tests(self, lowered: str) -> bool: + normalized = f" {lowered} " + return any(token in normalized for token in self._TEST_KEYWORDS) + + def _dedupe(self, values: list[str]) -> list[str]: + result: list[str] = [] + for value in values: + item = value.strip() + if item and item not in result: + result.append(item) + return result diff --git a/app/modules/rag/explain/layered_gateway.py b/app/modules/rag/explain/layered_gateway.py new file mode 100644 index 0000000..fb104eb --- /dev/null +++ b/app/modules/rag/explain/layered_gateway.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Callable + +from app.modules.rag.explain.models import CodeLocation, LayeredRetrievalItem +from app.modules.rag.retrieval.test_filter import build_test_filters, debug_disable_test_filter + +LOGGER = logging.getLogger(__name__) + +if TYPE_CHECKING: + from app.modules.rag.persistence.repository import RagRepository + from app.modules.rag_session.embedding.gigachat_embedder import GigaChatEmbedder + + +@dataclass(slots=True) +class LayerRetrievalResult: + items: list[LayeredRetrievalItem] + missing: list[str] = field(default_factory=list) + + +class LayeredRetrievalGateway: + def __init__(self, repository: RagRepository, embedder: GigaChatEmbedder) -> None: + self._repository = repository + self._embedder = embedder + + def retrieve_layer( + self, + rag_session_id: str, + query: str, + layer: str, + *, + limit: int, + path_prefixes: list[str] | None = None, + exclude_tests: bool = True, + prefer_non_tests: bool = False, + include_spans: bool = False, + ) -> LayerRetrievalResult: + effective_exclude_tests = exclude_tests and not debug_disable_test_filter() + filter_args = self._filter_args(effective_exclude_tests) + query_embedding: list[float] | None = None + try: + query_embedding = self._embedder.embed([query])[0] + rows = self._repository.retrieve( + rag_session_id, + query_embedding, + query_text=query, + limit=limit, + layers=[layer], + path_prefixes=path_prefixes, + exclude_path_prefixes=filter_args["exclude_path_prefixes"], + exclude_like_patterns=filter_args["exclude_like_patterns"], + prefer_non_tests=prefer_non_tests or not effective_exclude_tests, + ) + return self._success_result( + rows, + rag_session_id=rag_session_id, + label="layered retrieval", + include_spans=include_spans, + layer=layer, + exclude_tests=effective_exclude_tests, + path_prefixes=path_prefixes, + ) + except Exception as exc: + if query_embedding is None: + self._log_failure( + label="layered retrieval", + rag_session_id=rag_session_id, + layer=layer, + exclude_tests=effective_exclude_tests, + path_prefixes=path_prefixes, + exc=exc, + ) + return LayerRetrievalResult(items=[], missing=[self._failure_missing(f"layer:{layer} retrieval_failed", exc)]) + retry_result = self._retry_without_test_filter( + operation=lambda: self._repository.retrieve( + rag_session_id, + query_embedding, + query_text=query, + limit=limit, + layers=[layer], + path_prefixes=path_prefixes, + exclude_path_prefixes=None, + exclude_like_patterns=None, + prefer_non_tests=True, + ), + label="layered retrieval", + rag_session_id=rag_session_id, + include_spans=include_spans, + layer=layer, + exclude_tests=effective_exclude_tests, + path_prefixes=path_prefixes, + exc=exc, + missing_prefix=f"layer:{layer} retrieval_failed", + ) + if retry_result is not None: + return retry_result + return LayerRetrievalResult(items=[], missing=[self._failure_missing(f"layer:{layer} retrieval_failed", exc)]) + + def retrieve_lexical_code( + self, + rag_session_id: str, + query: str, + *, + limit: int, + path_prefixes: list[str] | None = None, + exclude_tests: bool = True, + include_spans: bool = False, + ) -> LayerRetrievalResult: + effective_exclude_tests = exclude_tests and not debug_disable_test_filter() + filter_args = self._filter_args(effective_exclude_tests) + try: + rows = self._repository.retrieve_lexical_code( + rag_session_id, + query_text=query, + limit=limit, + path_prefixes=path_prefixes, + exclude_path_prefixes=filter_args["exclude_path_prefixes"], + exclude_like_patterns=filter_args["exclude_like_patterns"], + prefer_non_tests=not effective_exclude_tests, + ) + return self._success_result( + rows, + rag_session_id=rag_session_id, + label="lexical retrieval", + include_spans=include_spans, + exclude_tests=effective_exclude_tests, + path_prefixes=path_prefixes, + ) + except Exception as exc: + retry_result = self._retry_without_test_filter( + operation=lambda: self._repository.retrieve_lexical_code( + rag_session_id, + query_text=query, + limit=limit, + path_prefixes=path_prefixes, + exclude_path_prefixes=None, + exclude_like_patterns=None, + prefer_non_tests=True, + ), + label="lexical retrieval", + rag_session_id=rag_session_id, + include_spans=include_spans, + exclude_tests=effective_exclude_tests, + path_prefixes=path_prefixes, + exc=exc, + missing_prefix="layer:C0 lexical_retrieval_failed", + ) + if retry_result is not None: + return retry_result + return LayerRetrievalResult(items=[], missing=[self._failure_missing("layer:C0 lexical_retrieval_failed", exc)]) + + def _retry_without_test_filter( + self, + *, + operation: Callable[[], list[dict]], + label: str, + rag_session_id: str, + include_spans: bool, + exclude_tests: bool, + path_prefixes: list[str] | None, + exc: Exception, + missing_prefix: str, + layer: str | None = None, + ) -> LayerRetrievalResult | None: + if not exclude_tests: + self._log_failure( + label=label, + rag_session_id=rag_session_id, + layer=layer, + exclude_tests=exclude_tests, + path_prefixes=path_prefixes, + exc=exc, + ) + return None + self._log_failure( + label=label, + rag_session_id=rag_session_id, + layer=layer, + exclude_tests=exclude_tests, + path_prefixes=path_prefixes, + exc=exc, + retried_without_test_filter=True, + ) + try: + rows = operation() + except Exception as retry_exc: + self._log_failure( + label=f"{label} retry", + rag_session_id=rag_session_id, + layer=layer, + exclude_tests=False, + path_prefixes=path_prefixes, + exc=retry_exc, + ) + return None + result = self._success_result( + rows, + rag_session_id=rag_session_id, + label=f"{label} retry", + include_spans=include_spans, + layer=layer, + exclude_tests=False, + path_prefixes=path_prefixes, + ) + result.missing.append(f"{missing_prefix}:retried_without_test_filter") + return result + + def _success_result( + self, + rows: list[dict], + *, + rag_session_id: str, + label: str, + include_spans: bool, + exclude_tests: bool, + path_prefixes: list[str] | None, + layer: str | None = None, + ) -> LayerRetrievalResult: + items = [self._to_item(row, include_spans=include_spans) for row in rows] + LOGGER.warning( + "%s: rag_session_id=%s layer=%s exclude_tests=%s path_prefixes=%s returned_count=%s top_paths=%s", + label, + rag_session_id, + layer, + exclude_tests, + path_prefixes or [], + len(items), + [item.source for item in items[:3]], + ) + return LayerRetrievalResult(items=items) + + def _log_failure( + self, + *, + label: str, + rag_session_id: str, + exclude_tests: bool, + path_prefixes: list[str] | None, + exc: Exception, + layer: str | None = None, + retried_without_test_filter: bool = False, + ) -> None: + LOGGER.warning( + "%s failed: rag_session_id=%s layer=%s exclude_tests=%s path_prefixes=%s retried_without_test_filter=%s error=%s", + label, + rag_session_id, + layer, + exclude_tests, + path_prefixes or [], + retried_without_test_filter, + self._exception_summary(exc), + exc_info=True, + ) + + def _filter_args(self, exclude_tests: bool) -> dict[str, list[str] | None]: + test_filters = build_test_filters() if exclude_tests else None + return { + "exclude_path_prefixes": test_filters.exclude_path_prefixes if test_filters else None, + "exclude_like_patterns": test_filters.exclude_like_patterns if test_filters else None, + } + + def _failure_missing(self, prefix: str, exc: Exception) -> str: + return f"{prefix}:{self._exception_summary(exc)}" + + def _exception_summary(self, exc: Exception) -> str: + message = " ".join(str(exc).split()) + if len(message) > 180: + message = message[:177] + "..." + return f"{type(exc).__name__}:{message or 'no_message'}" + + def _to_item(self, row: dict, *, include_spans: bool) -> LayeredRetrievalItem: + location = None + if include_spans: + location = CodeLocation( + path=str(row.get("path") or ""), + start_line=row.get("span_start"), + end_line=row.get("span_end"), + ) + return LayeredRetrievalItem( + source=str(row.get("path") or ""), + content=str(row.get("content") or ""), + layer=str(row.get("layer") or ""), + title=str(row.get("title") or ""), + metadata=dict(row.get("metadata", {}) or {}), + score=row.get("distance"), + location=location, + ) diff --git a/app/modules/rag/explain/models.py b/app/modules/rag/explain/models.py new file mode 100644 index 0000000..90552cd --- /dev/null +++ b/app/modules/rag/explain/models.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class ExplainHints(BaseModel): + model_config = ConfigDict(extra="forbid") + + paths: list[str] = Field(default_factory=list) + symbols: list[str] = Field(default_factory=list) + endpoints: list[str] = Field(default_factory=list) + commands: list[str] = Field(default_factory=list) + + +class ExplainIntent(BaseModel): + model_config = ConfigDict(extra="forbid") + + raw_query: str + normalized_query: str + keywords: list[str] = Field(default_factory=list) + hints: ExplainHints = Field(default_factory=ExplainHints) + include_tests: bool = False + expected_entry_types: list[Literal["http", "cli"]] = Field(default_factory=list) + depth: Literal["high", "medium", "deep"] = "medium" + + +class CodeLocation(BaseModel): + model_config = ConfigDict(extra="forbid") + + path: str + start_line: int | None = None + end_line: int | None = None + + +class LayeredRetrievalItem(BaseModel): + model_config = ConfigDict(extra="forbid") + + source: str + content: str + layer: str + title: str + metadata: dict[str, Any] = Field(default_factory=dict) + score: float | None = None + location: CodeLocation | None = None + + +class TracePath(BaseModel): + model_config = ConfigDict(extra="forbid") + + symbol_ids: list[str] = Field(default_factory=list) + score: float = 0.0 + entrypoint_id: str | None = None + notes: list[str] = Field(default_factory=list) + + +class EvidenceItem(BaseModel): + model_config = ConfigDict(extra="forbid") + + evidence_id: str + kind: Literal["entrypoint", "symbol", "edge", "excerpt"] + summary: str + location: CodeLocation | None = None + supports: list[str] = Field(default_factory=list) + + +class CodeExcerpt(BaseModel): + model_config = ConfigDict(extra="forbid") + + evidence_id: str + symbol_id: str | None = None + title: str + path: str + start_line: int | None = None + end_line: int | None = None + content: str + focus: str = "overview" + + +class ExplainPack(BaseModel): + model_config = ConfigDict(extra="forbid") + + intent: ExplainIntent + selected_entrypoints: list[LayeredRetrievalItem] = Field(default_factory=list) + seed_symbols: list[LayeredRetrievalItem] = Field(default_factory=list) + trace_paths: list[TracePath] = Field(default_factory=list) + evidence_index: dict[str, EvidenceItem] = Field(default_factory=dict) + code_excerpts: list[CodeExcerpt] = Field(default_factory=list) + missing: list[str] = Field(default_factory=list) + conflicts: list[str] = Field(default_factory=list) diff --git a/app/modules/rag/explain/retriever_v2.py b/app/modules/rag/explain/retriever_v2.py new file mode 100644 index 0000000..cf31820 --- /dev/null +++ b/app/modules/rag/explain/retriever_v2.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from app.modules.rag.contracts.enums import RagLayer +from app.modules.rag.explain.intent_builder import ExplainIntentBuilder +from app.modules.rag.explain.layered_gateway import LayerRetrievalResult, LayeredRetrievalGateway +from app.modules.rag.explain.models import CodeExcerpt, EvidenceItem, ExplainPack, LayeredRetrievalItem +from app.modules.rag.explain.source_excerpt_fetcher import SourceExcerptFetcher +from app.modules.rag.explain.trace_builder import TraceBuilder +from app.modules.rag.retrieval.test_filter import exclude_tests_default, is_test_path + +LOGGER = logging.getLogger(__name__) +_MIN_EXCERPTS = 2 + +if TYPE_CHECKING: + from app.modules.rag.explain.graph_repository import CodeGraphRepository + from app.modules.rag.explain.models import ExplainIntent + + +class CodeExplainRetrieverV2: + def __init__( + self, + gateway: LayeredRetrievalGateway, + graph_repository: CodeGraphRepository, + intent_builder: ExplainIntentBuilder | None = None, + trace_builder: TraceBuilder | None = None, + excerpt_fetcher: SourceExcerptFetcher | None = None, + ) -> None: + self._gateway = gateway + self._graph = graph_repository + self._intent_builder = intent_builder or ExplainIntentBuilder() + self._trace_builder = trace_builder or TraceBuilder(graph_repository) + self._excerpt_fetcher = excerpt_fetcher or SourceExcerptFetcher(graph_repository) + + def build_pack( + self, + rag_session_id: str, + user_query: str, + *, + file_candidates: list[dict] | None = None, + ) -> ExplainPack: + intent = self._intent_builder.build(user_query) + path_prefixes = _path_prefixes(intent, file_candidates or []) + exclude_tests = exclude_tests_default() and not intent.include_tests + pack = self._run_pass(rag_session_id, intent, path_prefixes, exclude_tests=exclude_tests) + if exclude_tests and len(pack.code_excerpts) < _MIN_EXCERPTS: + self._merge_test_fallback(pack, rag_session_id, intent, path_prefixes) + self._log_pack(rag_session_id, pack) + return pack + + def _run_pass( + self, + rag_session_id: str, + intent: ExplainIntent, + path_prefixes: list[str], + *, + exclude_tests: bool, + ) -> ExplainPack: + missing: list[str] = [] + entrypoints_result = self._entrypoints(rag_session_id, intent, path_prefixes, exclude_tests=exclude_tests) + missing.extend(entrypoints_result.missing) + selected_entrypoints = self._filter_entrypoints(intent, entrypoints_result.items) + if not selected_entrypoints: + missing.append("layer:C3 empty") + seed_result = self._seed_symbols(rag_session_id, intent, path_prefixes, selected_entrypoints, exclude_tests=exclude_tests) + missing.extend(seed_result.missing) + seed_symbols = seed_result.items + if not seed_symbols: + missing.append("layer:C1 empty") + depth = 4 if intent.depth == "deep" else 3 if intent.depth == "medium" else 2 + trace_paths = self._trace_builder.build_paths(rag_session_id, seed_symbols, max_depth=depth) if seed_symbols else [] + excerpts, excerpt_evidence = self._excerpt_fetcher.fetch(rag_session_id, trace_paths) if trace_paths else ([], {}) + if not excerpts: + lexical_result = self._gateway.retrieve_lexical_code( + rag_session_id, + intent.normalized_query, + limit=6, + path_prefixes=path_prefixes or None, + exclude_tests=exclude_tests, + include_spans=True, + ) + missing.extend(lexical_result.missing) + excerpts, excerpt_evidence = _lexical_excerpts(lexical_result.items) + if not excerpts: + missing.append("layer:C0 empty") + evidence_index = _evidence_index(selected_entrypoints, seed_symbols) + evidence_index.update(excerpt_evidence) + missing.extend(_missing(selected_entrypoints, seed_symbols, trace_paths, excerpts)) + return ExplainPack( + intent=intent, + selected_entrypoints=selected_entrypoints, + seed_symbols=seed_symbols, + trace_paths=trace_paths, + evidence_index=evidence_index, + code_excerpts=excerpts, + missing=_cleanup_missing(_dedupe(missing), has_excerpts=bool(excerpts)), + conflicts=[], + ) + + def _merge_test_fallback( + self, + pack: ExplainPack, + rag_session_id: str, + intent: ExplainIntent, + path_prefixes: list[str], + ) -> None: + lexical_result = self._gateway.retrieve_lexical_code( + rag_session_id, + intent.normalized_query, + limit=6, + path_prefixes=path_prefixes or None, + exclude_tests=False, + include_spans=True, + ) + excerpt_offset = len([key for key in pack.evidence_index if key.startswith("excerpt_")]) + excerpts, evidence = _lexical_excerpts( + lexical_result.items, + start_index=excerpt_offset, + is_test_fallback=True, + ) + if not excerpts: + pack.missing = _dedupe(pack.missing + lexical_result.missing) + return + seen = {(item.path, item.start_line, item.end_line, item.content) for item in pack.code_excerpts} + for excerpt in excerpts: + key = (excerpt.path, excerpt.start_line, excerpt.end_line, excerpt.content) + if key in seen: + continue + pack.code_excerpts.append(excerpt) + seen.add(key) + pack.evidence_index.update(evidence) + pack.missing = _cleanup_missing(_dedupe(pack.missing + lexical_result.missing), has_excerpts=bool(pack.code_excerpts)) + + def _entrypoints( + self, + rag_session_id: str, + intent: ExplainIntent, + path_prefixes: list[str], + *, + exclude_tests: bool, + ) -> LayerRetrievalResult: + return self._gateway.retrieve_layer( + rag_session_id, + intent.normalized_query, + RagLayer.CODE_ENTRYPOINTS, + limit=6, + path_prefixes=path_prefixes or None, + exclude_tests=exclude_tests, + prefer_non_tests=True, + include_spans=True, + ) + + def _filter_entrypoints(self, intent: ExplainIntent, items: list[LayeredRetrievalItem]) -> list[LayeredRetrievalItem]: + if not intent.expected_entry_types: + return items[:3] + filtered = [item for item in items if str(item.metadata.get("entry_type") or "") in intent.expected_entry_types] + return filtered[:3] or items[:3] + + def _seed_symbols( + self, + rag_session_id: str, + intent: ExplainIntent, + path_prefixes: list[str], + entrypoints: list[LayeredRetrievalItem], + *, + exclude_tests: bool, + ) -> LayerRetrievalResult: + symbol_result = self._gateway.retrieve_layer( + rag_session_id, + intent.normalized_query, + RagLayer.CODE_SYMBOL_CATALOG, + limit=12, + path_prefixes=path_prefixes or None, + exclude_tests=exclude_tests, + prefer_non_tests=True, + include_spans=True, + ) + handlers: list[LayeredRetrievalItem] = [] + handler_ids = [str(item.metadata.get("handler_symbol_id") or "") for item in entrypoints] + if handler_ids: + handlers = self._graph.get_symbols_by_ids(rag_session_id, [item for item in handler_ids if item]) + seeds: list[LayeredRetrievalItem] = [] + seen: set[str] = set() + for item in handlers + symbol_result.items: + symbol_id = str(item.metadata.get("symbol_id") or "") + if not symbol_id or symbol_id in seen: + continue + seen.add(symbol_id) + seeds.append(item) + if len(seeds) >= 8: + break + return LayerRetrievalResult(items=seeds, missing=list(symbol_result.missing)) + + def _log_pack(self, rag_session_id: str, pack: ExplainPack) -> None: + prod_excerpt_count = len([excerpt for excerpt in pack.code_excerpts if not _is_test_excerpt(excerpt)]) + test_excerpt_count = len(pack.code_excerpts) - prod_excerpt_count + LOGGER.warning( + "code explain pack: rag_session_id=%s entrypoints=%s seeds=%s paths=%s excerpts=%s prod_excerpt_count=%s test_excerpt_count=%s missing=%s", + rag_session_id, + len(pack.selected_entrypoints), + len(pack.seed_symbols), + len(pack.trace_paths), + len(pack.code_excerpts), + prod_excerpt_count, + test_excerpt_count, + pack.missing, + ) + + +def _evidence_index( + entrypoints: list[LayeredRetrievalItem], + seed_symbols: list[LayeredRetrievalItem], +) -> dict[str, EvidenceItem]: + result: dict[str, EvidenceItem] = {} + for index, item in enumerate(entrypoints, start=1): + evidence_id = f"entrypoint_{index}" + result[evidence_id] = EvidenceItem( + evidence_id=evidence_id, + kind="entrypoint", + summary=item.title, + location=item.location, + supports=[str(item.metadata.get("handler_symbol_id") or "")], + ) + for index, item in enumerate(seed_symbols, start=1): + evidence_id = f"symbol_{index}" + result[evidence_id] = EvidenceItem( + evidence_id=evidence_id, + kind="symbol", + summary=item.title, + location=item.location, + supports=[str(item.metadata.get("symbol_id") or "")], + ) + return result + + +def _missing( + entrypoints: list[LayeredRetrievalItem], + seed_symbols: list[LayeredRetrievalItem], + trace_paths, + excerpts, +) -> list[str]: + missing: list[str] = [] + if not entrypoints: + missing.append("entrypoints") + if not seed_symbols: + missing.append("seed_symbols") + if not trace_paths: + missing.append("trace_paths") + if not excerpts: + missing.append("code_excerpts") + return missing + + +def _lexical_excerpts( + items: list[LayeredRetrievalItem], + *, + start_index: int = 0, + is_test_fallback: bool = False, +) -> tuple[list[CodeExcerpt], dict[str, EvidenceItem]]: + excerpts: list[CodeExcerpt] = [] + evidence_index: dict[str, EvidenceItem] = {} + for item in items: + evidence_id = f"excerpt_{start_index + len(evidence_index) + 1}" + location = item.location + evidence_index[evidence_id] = EvidenceItem( + evidence_id=evidence_id, + kind="excerpt", + summary=item.title or item.source, + location=location, + supports=[], + ) + focus = "lexical" + if _item_is_test(item): + focus = "test:lexical" + elif is_test_fallback: + focus = "lexical" + excerpts.append( + CodeExcerpt( + evidence_id=evidence_id, + symbol_id=str(item.metadata.get("symbol_id") or "") or None, + title=item.title or item.source, + path=item.source, + start_line=location.start_line if location else None, + end_line=location.end_line if location else None, + content=item.content, + focus=focus, + ) + ) + return excerpts, evidence_index + + +def _item_is_test(item: LayeredRetrievalItem) -> bool: + return bool(item.metadata.get("is_test")) or is_test_path(item.source) + + +def _is_test_excerpt(excerpt: CodeExcerpt) -> bool: + return excerpt.focus.startswith("test:") or is_test_path(excerpt.path) + + +def _path_prefixes(intent: ExplainIntent, file_candidates: list[dict]) -> list[str]: + values: list[str] = [] + for path in intent.hints.paths: + prefix = path.rsplit("/", 1)[0] if "/" in path else path + if prefix and prefix not in values: + values.append(prefix) + for item in file_candidates[:6]: + path = str(item.get("path") or "") + prefix = path.rsplit("/", 1)[0] if "/" in path else "" + if prefix and prefix not in values: + values.append(prefix) + return values + + +def _cleanup_missing(values: list[str], *, has_excerpts: bool) -> list[str]: + if not has_excerpts: + return values + return [value for value in values if value not in {"code_excerpts", "layer:C0 empty"}] + + +def _dedupe(values: list[str]) -> list[str]: + result: list[str] = [] + for value in values: + item = value.strip() + if item and item not in result: + result.append(item) + return result diff --git a/app/modules/rag/explain/source_excerpt_fetcher.py b/app/modules/rag/explain/source_excerpt_fetcher.py new file mode 100644 index 0000000..b45f6e7 --- /dev/null +++ b/app/modules/rag/explain/source_excerpt_fetcher.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from app.modules.rag.explain.excerpt_planner import ExcerptPlanner +from app.modules.rag.explain.models import CodeExcerpt, EvidenceItem, TracePath +from app.modules.rag.retrieval.test_filter import is_test_path + +if TYPE_CHECKING: + from app.modules.rag.explain.graph_repository import CodeGraphRepository + + +class SourceExcerptFetcher: + def __init__(self, graph_repository: CodeGraphRepository, planner: ExcerptPlanner | None = None) -> None: + self._graph = graph_repository + self._planner = planner or ExcerptPlanner() + + def fetch( + self, + rag_session_id: str, + trace_paths: list[TracePath], + *, + max_excerpts: int = 40, + ) -> tuple[list[CodeExcerpt], dict[str, EvidenceItem]]: + ordered_symbol_ids: list[str] = [] + for path in trace_paths: + for symbol_id in path.symbol_ids: + if symbol_id and symbol_id not in ordered_symbol_ids: + ordered_symbol_ids.append(symbol_id) + chunks = self._graph.get_chunks_by_symbol_ids(rag_session_id, ordered_symbol_ids) + excerpts: list[CodeExcerpt] = [] + evidence_index: dict[str, EvidenceItem] = {} + for chunk in chunks: + symbol_id = str(chunk.metadata.get("symbol_id") or "") + evidence_id = f"excerpt_{len(evidence_index) + 1}" + location = chunk.location + evidence_index[evidence_id] = EvidenceItem( + evidence_id=evidence_id, + kind="excerpt", + summary=chunk.title, + location=location, + supports=[symbol_id] if symbol_id else [], + ) + is_test_chunk = bool(chunk.metadata.get("is_test")) or is_test_path(location.path if location else chunk.source) + for excerpt in self._planner.plan(chunk, evidence_id=evidence_id, symbol_id=symbol_id): + if len(excerpts) >= max_excerpts: + break + if is_test_chunk and not excerpt.focus.startswith("test:"): + excerpt.focus = f"test:{excerpt.focus}" + excerpts.append(excerpt) + if len(excerpts) >= max_excerpts: + break + return excerpts, evidence_index diff --git a/app/modules/rag/explain/trace_builder.py b/app/modules/rag/explain/trace_builder.py new file mode 100644 index 0000000..791c160 --- /dev/null +++ b/app/modules/rag/explain/trace_builder.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from app.modules.rag.explain.models import LayeredRetrievalItem, TracePath + +if TYPE_CHECKING: + from app.modules.rag.explain.graph_repository import CodeGraphRepository + + +class TraceBuilder: + def __init__(self, graph_repository: CodeGraphRepository) -> None: + self._graph = graph_repository + + def build_paths( + self, + rag_session_id: str, + seed_symbols: list[LayeredRetrievalItem], + *, + max_depth: int, + max_paths: int = 3, + edge_types: list[str] | None = None, + ) -> list[TracePath]: + edges_filter = edge_types or ["calls", "imports", "inherits"] + symbol_map = self._symbol_map(seed_symbols) + paths: list[TracePath] = [] + for seed in seed_symbols: + seed_id = str(seed.metadata.get("symbol_id") or "") + if not seed_id: + continue + queue: list[tuple[list[str], float, list[str]]] = [([seed_id], 0.0, [])] + while queue and len(paths) < max_paths * 3: + current_path, score, notes = queue.pop(0) + src_symbol_id = current_path[-1] + out_edges = self._graph.get_out_edges(rag_session_id, [src_symbol_id], edges_filter, limit_per_src=4) + if not out_edges or len(current_path) >= max_depth: + paths.append(TracePath(symbol_ids=current_path, score=score, notes=notes)) + continue + for edge in out_edges: + metadata = edge.metadata + dst_symbol_id = str(metadata.get("dst_symbol_id") or "") + next_notes = list(notes) + next_score = score + self._edge_score(edge, symbol_map.get(src_symbol_id)) + if not dst_symbol_id: + dst_ref = str(metadata.get("dst_ref") or "") + package_hint = self._package_hint(symbol_map.get(src_symbol_id)) + resolved = self._graph.resolve_symbol_by_ref(rag_session_id, dst_ref, package_hint=package_hint) + if resolved is not None: + dst_symbol_id = str(resolved.metadata.get("symbol_id") or "") + symbol_map[dst_symbol_id] = resolved + next_score += 2.0 + next_notes.append(f"resolved:{dst_ref}") + if not dst_symbol_id or dst_symbol_id in current_path: + paths.append(TracePath(symbol_ids=current_path, score=next_score, notes=next_notes)) + continue + if dst_symbol_id not in symbol_map: + symbols = self._graph.get_symbols_by_ids(rag_session_id, [dst_symbol_id]) + if symbols: + symbol_map[dst_symbol_id] = symbols[0] + queue.append((current_path + [dst_symbol_id], next_score, next_notes)) + unique = self._unique_paths(paths) + unique.sort(key=lambda item: item.score, reverse=True) + return unique[:max_paths] or [TracePath(symbol_ids=[seed.metadata.get("symbol_id", "")], score=0.0) for seed in seed_symbols[:1]] + + def _edge_score(self, edge: LayeredRetrievalItem, source_symbol: LayeredRetrievalItem | None) -> float: + metadata = edge.metadata + score = 1.0 + if str(metadata.get("resolution") or "") == "resolved": + score += 2.0 + source_path = source_symbol.source if source_symbol is not None else "" + if source_path and edge.source == source_path: + score += 1.0 + if "tests/" in edge.source or "/tests/" in edge.source: + score -= 3.0 + return score + + def _package_hint(self, symbol: LayeredRetrievalItem | None) -> str | None: + if symbol is None: + return None + package = str(symbol.metadata.get("package_or_module") or "") + if not package: + return None + return ".".join(package.split(".")[:-1]) or package + + def _symbol_map(self, items: list[LayeredRetrievalItem]) -> dict[str, LayeredRetrievalItem]: + result: dict[str, LayeredRetrievalItem] = {} + for item in items: + symbol_id = str(item.metadata.get("symbol_id") or "") + if symbol_id: + result[symbol_id] = item + return result + + def _unique_paths(self, items: list[TracePath]) -> list[TracePath]: + result: list[TracePath] = [] + seen: set[tuple[str, ...]] = set() + for item in items: + key = tuple(symbol_id for symbol_id in item.symbol_ids if symbol_id) + if not key or key in seen: + continue + seen.add(key) + result.append(item) + return result diff --git a/app/modules/rag/indexing/code/code_text/__pycache__/document_builder.cpython-312.pyc b/app/modules/rag/indexing/code/code_text/__pycache__/document_builder.cpython-312.pyc index 7893715c62fc1d6cb0674cdedd7230c95476828f..0596ea010ba74eae872b7f70ebdf1dd148c9a37a 100644 GIT binary patch delta 452 zcmdnZeS(MgG%qg~0}xm}Sem(UBCjN4$V7Ehl@x{)mK@Gpt|%@>1||k~h7{Hoh7`8d zOdypE3{l*b?3x@COZ)^h8E^4q7RQ&Q7MH{qB$i}MJQUApw^@miosp?Ta`IV5Ilk45 z5IrfZDQt6CC+jn5GO|vNWYS`1hv=K!$mGh%JNY=1Jewp?Ws%h6H%#)3LX&xz6{L$e zfg+kLw^)i(bJB_=fSg;b$r+`2*+qhrotRg#fuxJ;Cx2p2vAH9yzJhH%_e$=|(k3?) z)jx8xiSd2m=M&)iz`)1J^%sJgfw)*2NPJ*sWMsU{p!A%<|1yLBT?W_hZ2XK|pBR7ySTg|kl1w%L diff --git a/app/modules/rag/indexing/code/code_text/document_builder.py b/app/modules/rag/indexing/code/code_text/document_builder.py index c42f37e..aa489e9 100644 --- a/app/modules/rag/indexing/code/code_text/document_builder.py +++ b/app/modules/rag/indexing/code/code_text/document_builder.py @@ -2,6 +2,7 @@ from __future__ import annotations from app.modules.rag.contracts import RagDocument, RagLayer, RagSource, RagSpan from app.modules.rag.indexing.code.code_text.chunker import CodeChunk +from app.modules.rag.retrieval.test_filter import is_test_path class CodeTextDocumentBuilder: @@ -17,6 +18,7 @@ class CodeTextDocumentBuilder: "chunk_index": chunk_index, "chunk_type": chunk.chunk_type, "module_or_unit": source.path.replace("/", ".").removesuffix(".py"), + "is_test": is_test_path(source.path), "artifact_type": "CODE", }, ) diff --git a/app/modules/rag/indexing/code/edges/__pycache__/document_builder.cpython-312.pyc b/app/modules/rag/indexing/code/edges/__pycache__/document_builder.cpython-312.pyc index 4e8a6b1cb0a1eb5c128bde5b5b735a556352ed88..58298438b5cb61d3700cac50631000b34e44aac5 100644 GIT binary patch delta 587 zcmZ1_uw9V%G%qg~0}yyTSejWkkyny2W}>>SN(w^?OAdD~PZSR$0~3QgLkepPLkin! zCXh-7hA7@jc1@0nHR%GHjJJ3)i{ndDi%a4Q5=$~BJ`Um%2C86YU|{^*v)P_;GhSQU_wQL}xibW@%WnC+KM_PRa+j{Pm z+?S#^3alE1NMTK3o5NWm3ll7ngR!y{ zCSPC@om|T-#;62ltz=GO!#k>L7vzM6d#hTP&_F?yf~38#P&Ou@tA~q!lSo&S724#sL&6=A10gwpQ?l zg35Ba08;H!#{d8T diff --git a/app/modules/rag/indexing/code/edges/document_builder.py b/app/modules/rag/indexing/code/edges/document_builder.py index cc6f784..8fb2b62 100644 --- a/app/modules/rag/indexing/code/edges/document_builder.py +++ b/app/modules/rag/indexing/code/edges/document_builder.py @@ -2,6 +2,7 @@ from __future__ import annotations from app.modules.rag.contracts import EvidenceLink, EvidenceType, RagDocument, RagLayer, RagSource, RagSpan from app.modules.rag.indexing.code.edges.extractor import PyEdge +from app.modules.rag.retrieval.test_filter import is_test_path class EdgeDocumentBuilder: @@ -22,6 +23,7 @@ class EdgeDocumentBuilder: "dst_symbol_id": edge.dst_symbol_id, "dst_ref": edge.dst_ref, "resolution": edge.resolution, + "is_test": is_test_path(source.path), "lang_payload": edge.metadata, "artifact_type": "CODE", }, diff --git a/app/modules/rag/indexing/code/entrypoints/__pycache__/document_builder.cpython-312.pyc b/app/modules/rag/indexing/code/entrypoints/__pycache__/document_builder.cpython-312.pyc index 4693571fc0236fe13c23de9ff228aa19d2eebf6a..29139ccbe79b7ed88143a64646dc70b8fb90b58b 100644 GIT binary patch delta 612 zcmZ3$zgB?nG%qg~0}!}8SejYHI+0I;F=nE=tx5_*3QG=mE>9E>BLfqIJ3|U<3quOq zY9^3U28Jl!N_I_-i8a{*nvA!2GK=F&Qj1IC3ld8*CO(hg76htbW?*3aEXB0hi_w8m zr$ijA3rN&3W=X)<2zoUmgk8gwB{jK^NnD(xL>4AnA_rq-DNI&i5}kaJNsLhf%=*g| z#>g=_gxQ^g9qNS1vzR;T1&i2#T8h|##4Q15e;3zySHF;;$N+y&zmVV}QIMPnh!6%5 zoFD>ZSrN#dA|VhV1|qmYgbavK1raPDf)z;I;shHR9N_4u$$X2YI5j7&ND(B?m6}&l zR9TQec@E23c95~fER2%_Si9x#NUN`4ThG0c`?9pj4F#?13dR=|jBhBae-z{t z-?lJ(VG?HLf~WyW`)Tq`zR1>GeM>d5pg=D-KczG$wOFqxFn?MG!xO%{2lU4!7$UhYdutU6DCZ0u)=tpy2wz%*e=imqF<{L-uus?7Iw}cNy%y OvxzWrePRF-VEq8FsDyz4 delta 432 zcmZ20uz;WMG%qg~0}y=XTax*gWg?#hW57gpTL~rxcZL*}7KRko)l49128Jl^N;XaQ zi5=OKnHeLv1%M*V3=E8)KQV4jV{~9Ns$t3!1FJ(JQkYX%=CGGY!URjCV5}^eNzB@e za+8=vC$lqYPR?T1Wn`Z`gV}xZ3+8r4fyo6dvYH}5wMD`pLI^}~f(U64Ap;@=L4+uX zP@a5%MS{NwWV9ypEtcZcoU|hO$&XlevU321ihnRnUc}lhd_zI&x`OdV1>+lv>L2+z z#Q44lP4;06mQ@rK;QGKI$jS8?sF=Y|ldVV|NEd;$7HLf0!)9Zy1!S&dDAERrXo3hH z7{Lx?6oZHshA&J)tXvQ^AZb5Mp2_;`%{~yd1|SV^{l7SDa`RJ4b5iY!On?%MKwJ#+ i*9T@sM#j4gO3xXxuQO!dW$^sYCd|n7i2+D}H3I-TpHnsf diff --git a/app/modules/rag/indexing/code/entrypoints/document_builder.py b/app/modules/rag/indexing/code/entrypoints/document_builder.py index 0315cfe..9a03147 100644 --- a/app/modules/rag/indexing/code/entrypoints/document_builder.py +++ b/app/modules/rag/indexing/code/entrypoints/document_builder.py @@ -2,6 +2,7 @@ from __future__ import annotations from app.modules.rag.contracts import EvidenceLink, EvidenceType, RagDocument, RagLayer, RagSource, RagSpan from app.modules.rag.indexing.code.entrypoints.registry import Entrypoint +from app.modules.rag.retrieval.test_filter import is_test_path class EntrypointDocumentBuilder: @@ -19,6 +20,7 @@ class EntrypointDocumentBuilder: "framework": entrypoint.framework, "route_or_command": entrypoint.route_or_command, "handler_symbol_id": entrypoint.handler_symbol_id, + "is_test": is_test_path(source.path), "lang_payload": entrypoint.metadata, "artifact_type": "CODE", }, diff --git a/app/modules/rag/indexing/code/symbols/__pycache__/document_builder.cpython-312.pyc b/app/modules/rag/indexing/code/symbols/__pycache__/document_builder.cpython-312.pyc index d32cf32c4693629d15366980b1b0fe8657615f4b..3acb05434cbbe4a22df0555f3161560ff336e764 100644 GIT binary patch delta 509 zcmca4bYGbFG%qg~0}$9hSekiqBCjN4$V7Ehl@x{)mK@Gpt|%@>1||k~h7{Hoh7`8d zOdypE3{l*b?3x@COMC@18E^4q7RQ&Q7MH{qB$i}MJQ%_y3RJ<&z`*$V+-7;kMkb~b z&B-#%aw@AC!Fm~Ln6k7WEMzi;BZYGg&*nYMYK)9RlOMBKGV)B8W$oi&hnhZl6>AW$ zSdlbPPmvUekeSTM7AGVLWZvRRNlnf#N-W7QDlU?rT*;;`%n4GV3?wvJZm|@n=A;#= zPhQ6+%_uVY6q`C5NVZsF@^`i`)_0`USFo+;Udeq~+T@0U+I0oviwed!6xBaU@`~|& z;TIL)`oJK{$@LkigTYUetwRYOb1qFJ!`6;D2sl|FliRpSpsU=03sbz^ddf+fk%giZB zEh+;02W&2?UB5VNAe!xpT!F@dLbzBCNPJ*sWMsU{p!A%<|1yLBT?UuC40hkyq!_tA JF#rj$I{N{b{xg!JSvwm2aPAoCViN@{X`QDRAcQE`#n38pmtrs_@aXG4Mp{j;yhw}U-(4? zxIQq5aB_VHs$%fdWGhky(nV|_!hCWChmDmbh$#RftU!b?hyVo#m|zDoia|sR!xttA zRxXGdkhGsB*W`B`Wj+wKsQQ0#*yQG?l;)(`6*&QwGJ*_|1`;2b85tSxGAKP~@W0I9 Tf0x1KJDVgU*Cz%b0X7%_?XFUx diff --git a/app/modules/rag/indexing/code/symbols/document_builder.py b/app/modules/rag/indexing/code/symbols/document_builder.py index 22085cc..2f81b5f 100644 --- a/app/modules/rag/indexing/code/symbols/document_builder.py +++ b/app/modules/rag/indexing/code/symbols/document_builder.py @@ -2,6 +2,7 @@ from __future__ import annotations from app.modules.rag.contracts import RagDocument, RagLayer, RagSource, RagSpan from app.modules.rag.indexing.code.symbols.extractor import PySymbol +from app.modules.rag.retrieval.test_filter import is_test_path class SymbolDocumentBuilder: @@ -26,6 +27,7 @@ class SymbolDocumentBuilder: "parent_symbol_id": symbol.parent_symbol_id, "package_or_module": source.path.replace("/", ".").removesuffix(".py"), "is_entry_candidate": bool(symbol.decorators), + "is_test": is_test_path(source.path), "lang_payload": symbol.lang_payload, "artifact_type": "CODE", }, diff --git a/app/modules/rag/indexing/common/__pycache__/document_upserter.cpython-312.pyc b/app/modules/rag/indexing/common/__pycache__/document_upserter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a3131f553c6db350ca27ea9cd2be93261526875 GIT binary patch literal 1348 zcmb7Dy=xRf6rb7MyW1ppkr>g7NpVUHY>GRClp#bA?5>EhISIqEGr4Tok8@@ZyyFUs z6#fS(Y%L6FEG;bss}v!!kV-7<#1n*6zBjvfmuMOv?AtfnI@;A-%kiu8X!w2p!(kMF(_1n0BD*kt5nf zj7*LpIYw$J5$vYZlRbr1YPE|sx6^5aG4BRKHKf~a_z@Q`{ixmW;xLS(2G5DIZjPni z>7_Pfe&i>N^_Q+eTOWCCI78v2UXd@04_;fRc6A6V;sN}z;Jh@QY+eJhKXy%w{mU8o zHV`*P^P)gpPJzQQBp#T~0oB0u1RLR3jwxr*GYc%45=y}|na@)TB9y}VwOUP9Fr<_x z_ZP2YmpieTs)7P?cKOV%p4inhd-cR#eYf#p?bP0sH^Av3y^$2a&{Q|TI3)i^bzmOn zP56iGZ5U$E8&hT8o*T7=axBeQ;yp>rTm&K!tm7toO3uTULTJD_Ctb;k8C|0lCF$Ws~JbaDQ4hIl}L^0VlMDi z^8C`-J8}VY@f^q2(Q*+t?ttn~>1asa(b;kdJ(Kt%Uew491!L(nW7#zayUf^&t{Y?? zi9F;jR5;BeqCxA6AWOK53O|KSJ0L@Y7FLEuT6jc%f!?NBJVrE~5}vc$1%_~@#CfF) z(6oB~!s>AM>Rucr()AK0aRBMc-~Nt}3Ys7yPt+$5R(CH0RD-k$$P0TttG*$Z!9n5E bQ~6<1O3%stZ{*=Q+5BC!={EgIQ0v@3w-rHQ literal 0 HcmV?d00001 diff --git a/app/modules/rag/indexing/common/__pycache__/report.cpython-312.pyc b/app/modules/rag/indexing/common/__pycache__/report.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61db02c7b3047cb19c6b67edb474d4d2d1177c9c GIT binary patch literal 1197 zcmZ8g&1(}u6rbIlP4lry3)X64KcZd?NLLhZ#gm`~t)TX@Qiow@Qn&0!oY~a2c*vnb zkGb{W)r+(T{|_&Q2*w3L@F3n=A_$&*vzwIK2m71%dv8A8*M2OQiwNXx<(*g35&EIX zSj?!Oqr60x89i2}XBO}^y3zU7t zk)tu>=&KN+V=$d#W-NgRn3*y>V!4!=BQ}w;+=%5(U+79YR0|!v_`8 zhR;FSN3YR_vYrvJjyiMN0akVTjDR(F#ssv9pFj&&8d#_i5+wzef-(REjwSb-QUzB3 z%j{Ljg|w;9x9q!ole9#%!QEJr&6_s$>P&c>Jgn1Js}3HQOKB^|I@G`hqa8%74W+aN zUANQXLejPy1wjW^F3>Rpp?0u zHJ2*&o>fnk%RTGzQNf%l9wD<(9Ksp20`r*@JZ_6{r<%U<40?UN{vWJ0%1NG(kOrKP zq)12>IOrB3FWc15JZZ&}NkUGIKOtvIC@S!(P(NXURm;Vc;?n@{kvOmPa|-ffP%VBUUSl}1iw(`-ZfkeN_FU{X5yTe%c!Y|>z}fMyjv3G#1# xDJ#Ssu>cM=6S4ttWN4c94PE((t{vx)Ug}PN)@J_b6%FbRFf@>U;aJi1s(;dwC5He2 literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router.md b/app/modules/rag/intent_router.md new file mode 100644 index 0000000..e4c7cac --- /dev/null +++ b/app/modules/rag/intent_router.md @@ -0,0 +1,201 @@ +# Intent Router Specification (MVP) — v1.1 +Version: 1.1 +Scope: Routing + query normalization + anchor extraction for layered RAG (CODE + DOCS) + +--- + +## 1) Цель + +Intent Router принимает: +- `user_query: string` +- `conversation_state: object` +- `repo_context: object` (язык/структура репо/доступные слои) + +И возвращает: +- `intent` +- `graph_id` +- `conversation_mode` +- `query_plan` (нормализация + якоря) +- `retrieval_spec` (запрос по слоям RAG) +- `evidence_policy` + +Router **не делает** retrieval и **не генерирует** ответ. + +--- + +## 2) MVP интенты (строго 4) + +- `CODE_QA` — объяснение/поиск по коду +- `DOCS_QA` — объяснение/поиск по документации +- `GENERATE_DOCS_FROM_CODE` — генерация документации по коду +- `PROJECT_MISC` — прочие вопросы по проекту + +--- + +## 3) Диалоговый режим (контекст темы) + +### 3.1 Политика +Router обязан сохранять intent в рамках темы. + +- Если `conversation_state.active_intent` задан +- и нет явного сигнала смены темы +- то `intent = conversation_state.active_intent` и `conversation_mode = CONTINUE` + +Смена intent допускается только если: +- есть явный сигнал смены домена/задачи, или +- новый запрос явно не соответствует текущему intent (жёсткое несоответствие) + +--- + +## 4) Обязательная нормализация запроса и извлечение якорей + +Router обязан выполнять: + +### 4.1 Query normalization +Выход должен содержать: +- `raw` — исходный запрос +- `normalized` — каноническая, детерминированная и meaning-preserving форма `raw` +- `expansions[]` — добавочные токены для retrieval/rerank +- `keyword_hints[]` — компактные ключевые токены (символы/пути/доменные термины) + +Требования: +- `raw` хранит исходную строку пользователя без изменений +- `normalized` строится **только** из `raw` и безопасных правил форматирования +- `normalized` не должен включать appended expansions, синонимы и догаданные keywords +- все enrichment должны жить только в `expansions[]`, `keyword_hints[]`, `anchors[]` + +### 4.2 RU→EN mapping (минимальный словарь) +Router обязан поддерживать RU→EN mapping терминов только как `expansions`: + +- `класс` → `class` +- `метод` → `method` +- `функция` → `function`, `def` +- `модуль` → `module` +- `пакет` → `package` +- `файл` → `file` +- `тест`, `юнит-тест` → `test`, `unit test` + +Словарь должен быть расширяемым, но эти ключи обязательны. + +### 4.3 Anchor extraction (якоря) +Router обязан извлекать **явные якоря** из user_query и conversation_state: + +Типы якорей: +- `FILE_PATH` — путь/часть пути (`src/...`, `package/module.py`, `README.md`) +- `SYMBOL` — идентификатор (CamelCase, snake_case, dotted path) +- `DOC_REF` — ссылка на doc file/section (если есть явные маркеры) +- `KEY_TERM` — важные термины, влияющие на retrieval (класс/метод/функция и т.п.) + +Каждый якорь должен возвращаться структурировано. + +--- + +## 5) Контракт выхода Router + +Top-level: + +```json +{ + "schema_version": "1.1", + "intent": "CODE_QA", + "graph_id": "CodeQAGraph", + "conversation_mode": "CONTINUE", + "query_plan": { + "raw": "", + "normalized": "", + "expansions": [], + "keyword_hints": [], + "anchors": [] + }, + "retrieval_spec": { + "domains": [], + "layer_queries": [], + "filters": {}, + "rerank_profile": "" + }, + "evidence_policy": { + "require_def": false, + "require_flow": false, + "require_spec": false, + "allow_answer_without_evidence": false + } +} + + +## 6) query_plan.anchors контракт + +{ + "type": "FILE_PATH | SYMBOL | DOC_REF | KEY_TERM", + "value": "string", + "subtype": "optional string", + "span": { "start": 0, "end": 0 }, + "confidence": 0.0 +} + +Требования: +- FILE_PATH.value хранит путь как в запросе (без попытки “исправить”) +- SYMBOL.value хранит символ как в запросе (с сохранением регистра) +- KEY_TERM используется для выставления expected evidence и выбора слоёв +- anchors может быть пустым, но router должен пытаться извлечь их всегда + + +## 7) retrieval_spec контракт (слои + фильтры) + +### 7.1 Структура + +{ + "domains": ["CODE", "DOCS"], + "layer_queries": [ + { "layer_id": "C1", "top_k": 30 }, + { "layer_id": "C3", "top_k": 15 } + ], + "filters": { + "test_policy": "EXCLUDE", + "path_scope": [], + "language": [] + }, + "rerank_profile": "code" +} + +## 7.2 Требования по intent + +- CODE_QA → domains = ["CODE"], rerank_profile="code" +- DOCS_QA → domains = ["DOCS"], rerank_profile="docs" +- GENERATE_DOCS_FROM_CODE → domains = ["CODE"], rerank_profile="generate" +- PROJECT_MISC → domains = ["CODE","DOCS"], rerank_profile="project" + +## 7.3 Требования по якорям + +- Если найден FILE_PATH → router обязан добавить filters.path_scope (минимум: этот путь/директория) +- Если найден SYMBOL → router обязан добавить SYMBOL в query_plan.keyword_hints и query_plan.expansions (при необходимости) +- Если найден KEY_TERM (например "класс") → router обязан добавить RU→EN expansions + +## 8) evidence_policy (минимальные требования) +{ + "require_def": true, + "require_flow": true, + "require_spec": false, + "allow_answer_without_evidence": false +} + +Требования: +- CODE_QA: require_def=true; require_flow=true +- DOCS_QA: require_spec=true +- GENERATE_DOCS_FROM_CODE: require_def=true +- PROJECT_MISC: allow_answer_without_evidence=true + +## 9) Минимально обязательные поля (строго) + +Router обязан всегда возвращать: +- intent +- graph_id +- conversation_mode +- query_plan.raw +- query_plan.normalized +- query_plan.expansions +- query_plan.anchors +- retrieval_spec.domains +- retrieval_spec.layer_queries +- retrieval_spec.filters.test_policy +- retrieval_spec.rerank_profile +- evidence_policy.* diff --git a/app/modules/rag/intent_router_v2/__init__.py b/app/modules/rag/intent_router_v2/__init__.py new file mode 100644 index 0000000..5933990 --- /dev/null +++ b/app/modules/rag/intent_router_v2/__init__.py @@ -0,0 +1,23 @@ +from app.modules.rag.intent_router_v2.factory import GigaChatIntentRouterFactory +from app.modules.rag.intent_router_v2.local_runner import IntentRouterScenarioRunner +from app.modules.rag.intent_router_v2.models import ( + ConversationState, + IntentDecision, + IntentRouterResult, + QueryAnchor, + QueryPlan, + RepoContext, +) +from app.modules.rag.intent_router_v2.router import IntentRouterV2 + +__all__ = [ + "ConversationState", + "GigaChatIntentRouterFactory", + "IntentDecision", + "IntentRouterResult", + "IntentRouterScenarioRunner", + "IntentRouterV2", + "QueryAnchor", + "QueryPlan", + "RepoContext", +] diff --git a/app/modules/rag/intent_router_v2/__pycache__/__init__.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b19f91fefb9dde0f13543ccdfc0ba06c4176b124 GIT binary patch literal 693 zcma)2&ubJh6i#M#XLfeFr4~>2){744Ec7e|*`j!nbt^sO62ffU4&=v>$rSfg`d9Sc zlm0FK2Z5eEc@v8idg@E2V)arVf@tjL`ZPl5n@@fRSR0pxWGBTL{na&%rp4&Qh}t FWw!+W%^d&$ literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/anchor_extractor.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/anchor_extractor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb56e070806a3854c860dbc7ff0c834d33f7603a GIT binary patch literal 9195 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 fvz68F)3Ew$c9sO>*V> literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/anchor_span_validator.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/anchor_span_validator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70eee15cc720d7b73143ea9a09dd2412a1945d0d GIT binary patch literal 1539 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 Gh5rWv6|=(t literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/conversation_anchor_builder.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/conversation_anchor_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4ab1a813aab581a3fff829f8aecaab417bcba14 GIT binary patch literal 2827 zcmahLO>Z1UwyMA8%l6DTGl@UeWNasx&L%Svu)yv@Sc07_5C#&%VP_S!I^AujO}l%j z>Twv;MmhK}NLj_8m69VzBcs*+1dd2tY=^~@5(p4*$ZZk_A$(z9bec(K`nJ`+eT1~et7c~AlHzED%eC7!BotcDJ;qrF@g5} z%(y8oN)^dWz-gt5Y$i<^{z@IIDb~O91d-O0HQO;QQa0LFg?c#-C%~IWLhS*52h$`* zJ`Sk>R@z8_wC%Mz`WTm0fp7=2gf`;?01ws5bX_Nw*6ovF9wepjWE}pcodVB~F;OT)za1rCHCcITkGueY#}3J}hg* ztN9k;^2QEdYkKHl?LuU0v07(wO|xCw*R=VO2l}Mg;eixFv=_i-)a)N#?|=G>!+*MQ z`00l9ubxd2B8C6I{_D3d5aGuvL{^3e!{nVm6c zudcK?$IVz1XlFiPb2BQ*G;(}m|e*2g?JDr2Onyk62wF6VtZM~eXGXH zEOF|kGZWhDFTC-0)_YGLP+oM?PlV(9=T(_We!Xfjc}{m~7E`EKBZdX(TGd^a1j#Ae zv|Pg?JT6R`{Ku;&Upq7XZtg#)znlNzc{R>*T2=QiQpk0p89E_#59*s`b;F0u4QU!R zJkX#(zFMwH6xfJx53??ti}4S{gr%77?hLAl&mzB>E*L zhvu+uIH+&CHA{nnuvq${PPG~oR$GSkZZAyPPIM|3izDb6>vkQuFXwBX%9vr!wQnI~2iSh29mI$1;bEs=D z+AO{A>U;0Mw~;=wmOirl#24vOw4B|)kv+7QJ+z*^^qI9$cGk~kHG&xj8a*TfM%~jKc?`n>Uj*dZj;0v4NdZHtcgkf! z2!wHTRWzUl7{dAeP{1M-f2Uo5d}jn=M~31n`9SupnMSs!+bgy9DpFj!k`*(Hzfvi)xPjmZkBat8$n&WDvNR9Of)}l;g?ml%SM1 zi!kBp`$Vc@_}pxcs&P=a_h}E(_g5?A+j{#JkUeV9J3o2hLTMGDBOq zMEJFL>sJ_CxCocILTJ^oeN~LSm}x$x|SaO=*8vijr1RvPp+j)4XK&#T{0W*uEqz#R3!XW+L?oflCAt9%?ee*Q5)c=a5l8|1$@Nwy~!Go6qum`sa>E z%6U-DvuG<3QeemPgmaR=000{I4S9xxaFy?=#`taP>a&)nv82Yk6kOYYcWK(&HQk9Q zB+Z2$lOl7pLBz&6ieS?<5vBgli1Hm;})%^i+9RRb@=Q3;PT zdmNg0vHj>M!lR?O`_WPCyl^zL{pyPzLt_sW& literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/conversation_policy.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/conversation_policy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f966b5de4d3636006fadcd3982606db5f7145c8 GIT binary patch literal 3307 zcmb_eUu+!38K2qR`(vM-BX^E-gwR|dkZfY^3=QBylwjHCO9Vc*K1Y?VidM_*I^5;{ zxU+k zF(;O#oJ5I0(nJ$45ly--xLP@1-6#A1fdiTHkXor!P1P(^D~2ra-6Pe?tj-K)A`A06 z-*u*9>J@WT&le1k%DYxv>Y1b)gD_qsI>}Lu(>1657q;6h(|iz{j^#t4|$F<82Q}#!hX}f+z3|mxwxqs7aCp0T*1u06gR6pP^cKDu4=MW>vIj>us*jh zT3>?VAHd&={TlZ(5Gd;npH|Hq$%dFHYY=3w>*Gco9Ne%zvM<)~ywDpcy=uSe4#5ss z0e&qAT>BEnYJE}LjiMidD0uS>SSvg@U905rB)BhMQVk>Toihj%;)72lgl>Ebokh|V zi#WTjy(~4Qg&OvOb2->no3U@%Q77mTtBppboBGW60k4{9FZd3Vjwnexk;&gwjp z@5m3uzyx84HUqaSj_o!}o&|X$sh0HlGvnzn^+AUYenPB zN3H09)iclvh1Wxe?9id*+|AHnTjz8K^Pn9%xct~k?2`i@9{AJIo1yXTLgD>OMJs&7 z3Le>jNJzdDr-*h3;U7TfvH*3B7CpS7Y`Zn-JT(NUbVAX?O`C)N>)v1|{I+>*6#rT< zH*tDJn5IxCgH2MzOx@N-H|@4{vjlNSW5kGlj9L09bbd>pClqs)oCBLL(&p(Sy^OQI$1xGhkC>=o&&aAYl#T9MWV zNA1DUn~~I7IAsM>Y#-RzQa(4q)epyYS%O2MO~EO8v`zs^oregz*qWjTH1VE+CN%|3 z^1S(=?|1q?>VTgF@C$q&_<@z6W98u{y@S0j$DZ?ya97l(JHfe5F><%e7pOM=}kNqlds z7yxtZaU>Y9>}ObG*kL4ac@riB;l1u@WzVED!!KlE=q$?{%04$$@y6o!<<*K-(BKC1 z?#hk~Zr^5J4&&9)ICM9g@8tg#5XS_Dt7ZSdYG|Mpjono;w?YqC2ZmNdL#^mu=r7e* zLr1=f_OJD)S4P*5kJ-n^ZuY0wqG_uq-DY1s_i17EvGkp!!hKF9$jCv4$IozG&N8`Z zn2g~Yl3``QH=bEzl}6aC&6M6D=D+=#c6!&(AevblA3bo{N7$TJ6Yo0xc!flOoPS+>(ly z7DM8P&p;GhBDaL#UjM?xtq9qZrVHb(zPW|*4XN8FZ^Q)O82wkU+qcIxfr=T~kHJjW zV9^j5!<&X5hSx{JdwnF}^^w^A`aq^$GMpWAx6+9u4$A+F?so8mYvUxtLXLISxo~cY Xl+wSDhyF&+{@Z_;?)fJH;;8)>s8mtb literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/evidence_policy_factory.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/evidence_policy_factory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2544ab522fa82207d770e705f49e77a100bb451 GIT binary patch literal 1613 zcmaJ>&2Jk;6rb6(*I#j+kfd>&rZSMUH7YjZj1VSSmq@`5X#&I*j5gj$vas28XVy*~ z9Ks<7j&LGE`~gAX05^`g_p*c@qERYULL9gm8VRW<-t0OwO{&gHZ{Nr7ea(Bj_tWVl zg2TSuwXzyQe@NlvL_!8Xegl~AkcmueqcvQ^Sn`Uktf@5>BL&?;rn-&H$et2<)FSO@ zUi(W5c`Q)KYmT@PJCdL|=9HEg1qVq#MnEvtXOli7Ona`^t$(u{Way;sIBH_@LurQHXH8^Q< z%823AS6x=`^@3C=4Q@uh;U=)W2@hy^I_f>l6^vHUUQf3f?O+psegp1wrlXijz-2rc zB{vek@0WNwDr=7={u}-vw*gz-3Pe*(X@>D(zZU{0ylMkEmbNOUdfINds zoWs31TiBg;I!ZoS5wXeYlukYtmVZh5P2(<8LENg~}FFqx@NmS*Nf)T7+vA~sc>ye+ei+|n!hTxnh> zfu=lHeV@pR@`_M7^_ZMQUK0^c?PZ~b%faHHWC=;+CnD1S?+O7IE>|KKoF}c~0_+Vh z8aBOGoT4q#VD4wM?s3w3vuId3lUXh5`j6s$B7|2)XxF21*vNQdCb@!#; zvp2s^AEpMrSv*KxJ%EpZ{mNDq=Dmq DJsGRd literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/factory.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/factory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23aa0d8817bb9bb2624bfd0e1de9b0223ee5d751 GIT binary patch literal 1625 zcmah}&u<$=6rR~#+iN?v>zGJQAWA5eDwk>(s#YpeK`KF1Kvbco^srL3T6V|owzF$9 zJ9ZnCNE~wTf6xP$9*vR%e+OJZiG;MWPly9IM39hr;=Ng~6IvAJ@b>+hH*ene?fh7) zRS2xlo{U4?Ammpx@&z_7db|hn83{;0If2UXL11L+NFHE^0bWN;Gnt1eHA!RQEO21aTI(7uvPo z(P-1O>cw$SdNSdVcZ7cc?#;wQ0$oDJ?JN!SUQ zxPKMA^~`&>H;^;WB8MIBlb(2-^gW-sEFNOs?Xc@@JoI3H2vxvyF86FPDZ{vnZ7k)! zd%edjh6aWq9PVcq$mHxc^!PQfpAkkJ8W2Yd^nmUej=rt6jr8@r%<1Mn_I%kB+de&2 z4FS>-pdVB*Igjnnk|>c7&(@P77ZSLLPkm<{8wmR7xE?ya(bNGBH<< z%r_>jw|;3|`MGuFuw{+R$+9(mdboUJRG&20o||Lqu=(DoI5~gu+xn6~EL|FJ94);ws!STIQqy%~FJi8nR$Mo`66g!A z`_;hXSwt+u*6)Z|fsZ(k0{Nl@Dr%^EdF$oLTPF-tyh$(p?~V*LHIQ`mbawL46D+WRmN}D< zvFU~X$mzl(wgjus@6a0@VKi0 literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/followup_detector.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/followup_detector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4e07a838817fdffff6f993f0955dcfcd856b370 GIT binary patch literal 1301 zcma)5-)j_C6h3!mcHLx?Y}`d%OG~#Rg+bW(`zefCtSw0MpcZ67hUw0%>^hlQ@64Fp zutG3_WLv>Lw2kyBk1e4P!PH131DEP7_af&wuSYm$O&Dp*k|D0Nwl@xf!RZM$PXR*zb~ zRrFn|CV~t?!s+l5Th6iTY=Qk3UJhs2V$g?vc84v8(`=F53MT_G61%|`B1h08jtjX6 z+RhFqMHLL+W=lc3183L63AW6xp>q+dif-&Cwz-*OOY9cj z>~HGkHlci5z9)+1auaI@jB0gg)HMU!@`k8!cBt(5mgDQx4Sb907xoU7+Efavy4m4U zzFJ@F89r+{R;^04@!cD_mM?pH1k!_Qe%N-4hV5zj_lT|7+S`Ka%`g=y`rCkCG!*i6(oS1NAV;W1<~-aF=1?Xu5h+j6*UICY-Te>igD__rrc;(&^0 z*(EMhV~h@n^3iyQX$lX#@52j?fAP`#4hr;XRCu$MRQgJ-S-YC~bNih1AghL1wUyoV zAiF2b?rCNBwNm?Ml!vLzN+$a#IWXg28E=mN(r6_=pCk8@U#(=eJxZlpX>CEd(SNP~ zep+j#G?vg-u{MmT3KdV7%brhpoLYWB9j^T3x;DPvgswZrsHN+?SJ$I&gLa>;pAQT> zicqmcx?QNGP)I#!4@(8b+e+-w6U&%_7gEn)eB%38TjQ}qQL4xq!M$OFQh?2|85KK= m;Ok4SC5EF*Xmkgr2ugb5)xVGlA&+6l>v)F&_KbP6he~57V^=g2ieQ8WTrJG*-d9Qfk>e} zAzW0IveAmYH zvSLE!kC9vg14jBVATA+bKn<8N1!jIU;waacSdRTe8xBz!&-a7S3wwbtyKz8_cKjpr zbtIR-!6gh_gAryhlUdAuXSpUzvBcDJtx3{Jt6WWZ!)E6s98lhshWf1TMI`P%gZ|m7ck4^-<-cbe>?t?+xVQ?n4<^{#}eABOc@H5 z5Ih_SU))2*u~mvv-y3jBu>l&N4SAN*(~;MYJtBvpB9HI^#9hr-H0Vo@K))#Y0AUK> z@`V|Gk9KDFpZrSOD~XH!Kevdz8K>!thGXiTjz)M0+*RO|7o~KCoAOtwXYpxv#6xu8 YD+Z~z{b>?HzQX$7M4D{=281Z*AM)+M3;+NC literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/keyword_hint_builder.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/keyword_hint_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7de9ed8cd6b471d5d0635aab3c530c89f9a3ad90 GIT binary patch literal 2183 zcmah~&2JM|5P$n=8^>`7aabHEpAj`qoRZQ4r2-|5frKVOgsO_8Xtl{M!Djuz+jSsj zEuo5ot43;&kPPa99C`@kQ2vBUJ@$feQLLv_t%Q2umL^E8ICb8xH)$ZH?=WxP%)FU- zv%i`BIS|-}U>*HxA^9VZ&+!!v8U;ao--pt7*@Gh9R@ z%@?$p8(FNLPiD;Cd{S36JXybQJEMi;XyNX`L(6Ci6W_B9In-j@QWQCJ-9t+F zw~cFglQCE*^7@TFG~SC)k+=Ai=WYANy2$jpx@m-&+3>rqEzY*;w0Mi1W(;Pw+hS9G z*Tcqc@owG>n(gC}8E|!}Mwi>zEoO_{k3;T)C8V}**twj_y58UQvZ82-^(+tnN0y@N z2eLfLBg=+s7>%F^KSU6w1%KH9B;q5Da!xU?%7!+T)-t96=l(vxQvcch_$7cE@mxt} zR7KZ`pl5Gr7>jU?h)OP}WmHVRH}O)7q^V&dOyO)k7x56@(DW;WH?^B45v~GQ3=-HJ zMliG2v_k&`-M0S+7q(n67kI&Fh-cg_Wr;@B~T95Cy`?)7@Ry z96ZW2hCJaQGM1Ymg3TACxA7XYHT3buPe5*?THBtIP?L5qNzsZFUFfSyM@xa_(7~lp zv=V~zE1O;k3V_RT@8J_ia6i<;8oi5(Eq?c-}7DLlGIU= zI;v8v)L#oq^TM-WyX$H6dDGz)4_sLBqNdQ?$lOG!@YK6&4W5BEwIQvc-G+YyQ%2@` z18%gM3y>mft~XtZe%m+JmJl8S2y>>-HBH%*)IB-7%354%y*V~?D(6N{1vjKF=Qgi9 z6QR{xomPjI1ki8QG@Dvd;?e9y3V0jpm?l;b#=2tTQ5*Dtch1mEOrLCn7t<~zEY^r% zuP)mY;ik~f)~o=s08Ab<&2WnWSMM}Wv4BHZ;<8dc|~_LxQ*VVb$EpA zh)!YXEXI`3k!n!&9kj+a3gK7#M4{C&r0R&Dy1tT!v>*%y3?sZ32#LWYh}9=S3?@PB z%}Ef;WN}*2lLh;i=?tdM|Gi|)q$jevjOl=JKw*zJ&tx{8OX?b?0JihS5z5gL+7Ei| z{nSb3G>(Cc_LtECWQAiG=2vv!1@irY&i#hQe&60U*IW+Y4L{v|qPqR$?Z&^volNU0 I0%Cjn2Rw-7(f|Me literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/keyword_hint_sanitizer.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/keyword_hint_sanitizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89d05eabfe35f7d3121d5ebf06f779ee63c15d5d GIT binary patch literal 3516 zcmbVOZ%`D;74M#z{mbeiu(;r#Z2Uhb?tY>)BmJ zvN=;3*w?SS-+TRjue;ytKdxKnM)3XkM{}`3CqjRvox;cE7`$8y%rugagcUT1doZRv zqcDSP4~r3ljv=}Xo5&>v3N2f#$(CYxJ(*sVEhRnMaCulrhZrd@CgloKCgeM-!$Ge-pzG? zr{6ZduYauHda~XUHQv)d(&vpI=y!}OPyAHj7R=7WFi7ejf$YuPWCIPF$u^#7H`(sS zV;w!kbx4*0PM;pKXuKlDr^4PU@20~>_pA(~PdzqRe>|WzHT#C~ebh{q};r=dHvJ>_JGyljUIB^?ba* zftGdn3jJJLSO@e%#U-vXll2@*+hJWhc;bk{839i=gC`D&(;OpoO{eDQr}19%Nt|RW z98<|&1+&+f$*o|`nRc8+mVm}eirxKU5G74lRGr` zSDnFhiBC#=On%<$xQU|-$Pb4gBUF|olYYen&`|H&6P+L{ZJma7^? z;H+TyrEQ}H&M}I3ci2SH)mMgIkOTB;?E>Z}xC3EGN*X!~xg(7=e3= zy6+Fqwa!$|zWTGRhClpNr8(oh^&7D1YHjaq5l%FoY+J#hM>LXzo>bOUt-#n#3)3D72{)Onb6$cEkv4*pJ)-f8oN7Mx+4&>u_4(k6_i9`KoykOfGqUJ z6xp%Zf_DgTHY2;wsdzz_;n_y%1W=v5^>aYh*n)vP4_ubm90nuFDl@%086N*Jtttg zsYk+)nCw$cjy9%yAf6bF=cS0Orj(>7xaqI}%MzjKUZnvIton`+T2M@9VGqW) zeB~CEP#f<c0KTi7kuH2J6re7 z&wcOAj@)Uz*KxPwv%?GZr}Wdp;^`sd^pHM0x;UIRhSLvEe{ZUHTKPcHeeY!4K(enl zeeR3gtoTIzO!{#ipIt58bWK`UBb0ifPYP&&;XWQ@=d( z^D_@h_ZKw#7QDOG%swpLzvKn;OJ%4s@EDc2_GDVJ-ijG+(H}J6-y3|c@aBWkz1dQK zwsI5o_=CaBv8=apX2)W6odN&eIywicZo|9n`XQ@RHYH!yX4$#dj6m((>OZ*Ypr`ao z=jBc~>Ar`a-Afx`#U(!~t(-bHHJlmOomEdCA3&8yn5U*MqHcjbxl_?v!To;SCQAAB zlzz+ATE%_p*a$TGtlWYm<>7~y4*~-@g&`X?M#GTS%Cw?oTVo0(1Vg5we}p~`;4x*- zopqiidttz|TQ@{CT~RSE#UwE)=QB^hI%5RNF&f`o^myfO4G$9P*MM9^UzAkLRL$)& zs%o>IvMXJeyJq-@o*hdps;IeJ}J$AG!a;+>VbLLLquXEwooc0j+WsxBiS^_ zsS(86!-Q5BvWXI?>gdtG;3Hds<1wM>IFFa?1Rm0b7*k%vxdX<>cNWLFCadfNQFS*E zdYgRLN@mIyz1t1%_SvSn4GZ4Ab4LwtD8psFWm8gSLU&dZ3hxEr&$bhuLI7NX;*fuX z0h|l4sTj&eu}Y}n!nS%SY&M)jOtAw_ih*aWeYJ3^G}wXsMpB2zUwJ9OXIZfh@4JFIhi7HaumjSJE0M=cygF>jg&M2F}ZLeF7uoPZ1wQDL_*=+ zrc)5&;-D-DrdtqlH4k`?APlEOB{xG}p~mQOCEFLp}WC3LRR^uMNazHHSQ;Fr10w(8}2#Rg?KzhiXzh;LL)Amo8;v7H<PY+cy?MX$ z=6my~yE}+ro&KVldn6$A8xuMUWVUegZJ?XTKnAu@9;Y#8JZEuvKF#Naw15$Z-bV(% zf(+pS*K$H>aYxDT``l(~Y?=ipcIRs1fR7mxdTyz6uM zPA6=<*5`^uggtQq6TB94rFq};K2D&s2bTVCamT+?1x9OE1>{PEbEPSCRL#0?-Jj)W zQI^Y`{Xcb)?iXeJl3N=TP?oRwE50d+i`)Diq;eB*eSV8wpO!PI=oCxZEmfqvZ8|ip zFX%Z-pSDb`Wnt5k9;4-TCp%}_no-Q_xq?k4y;L#_24N6W15~h0YlccW071c_Ap(fd zvK}O;uj3Q+VnVDE4IE~o5;5(vMv7$z z4zcj&uy>l<42{~fhn_@fiHa?LIdbC=t8Hi{S_*c9_IE(;qMuJhwnFvjNG&?D6TPzC zyL)1EHPAS7ZdGdZtJT3;{~J$`7-$~I{m&2z^){5g)$x4^olR6{YG+2Ci9$H| zYq)>Izjdx5D|LCOCJ$9FKm24*zWP)|-I4qLhvJW+OHCf^_K^?>{?$al+7eX$eO$d3 zl@2EbcG#47lkzn%o5Zv1n5nl6u7gj&Bxmq1u@;V5za$<^VGl+x z$a*Y}Do#)#IhndnagqwD$>bEpDR^^&wxbirzLRt2RG!FCu7{Cc;B0nIfCxFsw!48W zUwHnW{~f$h=6@5&UDW96-B{R|+vz&hP+qMogEeLFzP+o&ABW=qrII6cd7vf_+<$9V zKEER;tC#EXv08j=+un^QAN!L{2#zMyAPK`CM#~|j2ROnW8p6sy2v|AW`3w{pqJX>{ zGjiFMd@5tBuQ6;Y}{hi<>#<@uZk*{yfPYfmfTAUGctao`{IVvG?L e6b!^Rt9GBq82^F>en%fZlOi~UpCCqCj(-D*3$%a$ literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/local_runner.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/local_runner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bea058f81d839493ea57151478feadb601b4a9a4 GIT binary patch literal 1898 zcmah}&1)M+6rcT;Ex4#E6a|5uMOQ3}uuHB~m5Xv!DJoU9 zsB)R?YSlzBAs`W9v6ALyju4z$7vdkqJ`qhLTUkZ6x-NGA>C2*_JzKEKy#}fQUJAg`?2p2sjW8+-HiG12TUeEglC9XPEv+g=nT17%4_hjfuxAmcHXC{#CZ%oy+#WlF zWnrJ?-$s!TA(IOjSw{DSJcOm0ga>ViRQ9pEK-HM{+3jrh*`!skPgHAm=wg3@SmgEOms0$D+w%t%ZAPTvOxUSmu5|4AA;G5QmmJ4P z5!U&H)q*Cm7gjN0O{U)Xbb5MfmTI>w;yGTKFzir;$3>3;MzL$!VYO~n{F+Bq050}y zs#*4;<(06JB&;x0V_`bjr#gZDG)r-V@?^n>*5kD}KA$R%XX7QBdY&S*2#=k^Hrrc- zm5WNy2aVB};2z{m(foX6-jhkO?>sDvzRD15u+|JPW=Fs?B>es0Um$!zl zwXPp%e)4c?qwv{I!PzW08@Hm3g0od-fRven9348)J?1q4}4M zvRwZh6THS155G}7G3O114G+(%V$-B4(~QRsoOKhxWx27041y7I7RWusaI4e*Ewd6SvQFv8=%z)tMVc}~v~SL0YdAaBDz)|UPx zkUdEdgh%M~V>Iw5TG&JjkI;vIB!^p>o#gOla`^6hTgf+8)W6jMA@dXg;THb^?5^f0 literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/logger.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d40a1d55e8db758aa23e29d09f17e17071cccccc GIT binary patch literal 1643 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⪼0zW!gOeako#)r8U=qo^fj+D3Pb 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 Z$NMtA9IvTLH(3h(4S8|@Zv?8&`46l+JrV!_ literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/normalization.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/normalization.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55bd0d473e8b9d515d7278fc01748f1ebc1105d1 GIT binary patch literal 4093 zcma)8O>7&-6`uVewG=6@Ezu%v*Y=uLEUjdbP8>J2I64ZEdXx%J+}fqcIMlZ%C(J9MuN3`3DN2Riq$=ilYQIMNy

$`2#inI1}DK4e$lg9da8l6YVu7^nBE#K@mb-O{g z$I|V|(qj9!WI~ZLoN_Xm(q%oGN@}58%gyOu-<<8enVaqzI&);^zSuJ~5XjH?$seRR z1pVom`~LpfzJStych1tlQx#7m- z(cpO$8OXZ&Jcb4zUo_@cQy0-BwM31Hl8A6eXM3K3N17kle?fLeG^ z4oKEu3)?oM_4Q02_YVvX9hns~e(+PH+cWgO=>lJO&rEHF#g+czAB!IrU-XxjOHXD+ zF~`6RuPEOus#~=Gmqh7bo>!|sWf=iwLNPS9R`0jqvC8Rg%ff-np1J^{0Et6T+swsP*h#T z2{;N(k4A)uoJ=L75jig02C#4&rxF5|?+dy*uM791`dlib3u+QaBXbEgsrw_f6`3n! zLBejRO~0A-d1{{0XvlbKf!1p|jnMPjW)_${ijlo!B? z8zSXx0Zwd}bOon$HKMCZ7<_MVQo7nV+)^YJkIQLImF~|)byZ8t5!G<$SWaqjSyzoc zSFcZmgHrg~NNDo%NciGJ$k;o1Wn?rceGoi1aV;oaxgHt~OV@%1qh)Rx?y7C%TsU~G zVQRsurl+tfvIeWE@!JLiVPLQ%4h%pR5J0 z$w~fn)^OA{YHr`Q(DqxVPFS1v2*e_)@a-RES8f&9uid_~yQ|=+@EywwOABk^&rg^6 z{sIgA4Zf$u_pH0iyx;6EKUjM3&qIMSe*%W_T{ZLG7i^h7MKnh@`0f(ly>72*-kid_ z!3!l`_#CD@R^*OtlB17o4S5pUX2Lvx#wyYYz%06zRyy%;RcBq6jo`j*8$+{)5Q1c4 zV$M3UQA2XL8ohcePg#LZJr-D$rzX)eDkQcTuF;9HpacOCBH`gWckwbr%SiYF>G5L| z;c#$Fx;PdLnbE_Ij*JH{6B9QX8o3mN7H)@c7!W(@I0U80;=Rzv9wNM^B`xc4rb#Pq z@IGQA>6r6wKgfE-9|5cO9*9NswYTf{@vkQSdgCASMbC{N5aswq;e4fI|LWkA!L{th z;S;69Cth@wJKkFw`;GzQOTMGv#0Ow(bE5hsSho7T86hcD!(YwFs7`2*#B|7@1d%ap zznZk1VbgRRO^bBH6&g8QL8xw>@Jz1jjjcCH@neA0eg*>Ez`K{TOWC!KHKk1AP%QCc zneQudedb<6;CK@Jkl#8s>{!(^IEh8ji1zAXkhXaux!v8b6HPbnB{l+h#%=*yT8I)( zl9F;ll_aB8lB$UdZI>k7&B*bZrA3mIR78^Seli_7U`)3944Ese?%hGOM0CJkbAxz@ z{>5J1WF5Bd%}&PV{;`#^9j^``un7FEVqJw@pxSa3+7t?|y*IAIEGu%DXMZ6 zyzXFs-OvYMZ8r#@r(-<@_OY|l=_#-uH;Grm8Rjw{Lj#`Eu&@}kwwK?+)A~~eu0*P7 z99s`-7|m`UOD@$jpve~hRIif56y=Mw6~XLey!&}FfkC_3`EERv%muLHhZ`*dDh}y`td&CUhpC& zPkL|~ag<>*BMTEhh_(i47+mc_R`7rcyC=spsz$y6zN>qWJ;nLdPis$f#r@Q3oUU$B@UiJ(wT_ALN<|Xeb z@HL8|O`_}lB{ON66(>&`{^Otq-mRXlVfcMesr-mZxu~|VmslY+ZkXw;3Z5#%Am1ov_w*g zbTqDFxFnI$dhvV+9zdfblRz-= tuXnr{{WJ5o(c+EiqH?F?n=8AcMYQK9yPG=xJp%EQpQpS(AP}a{{|6pmcJBZH literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/normalization_terms.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/normalization_terms.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ca67ec1c91e0a85671ba1853b1ac33efe36cbb6 GIT binary patch literal 2974 zcmaJ@T}%{L6uvV%v+Ux+s<4Q^9Yll?bX))0qz2;;X|+nyqAw_!tUIGFF1z&3ECsS8 zqS$UDDYP$`v=423sMZ(Kl*Avj&rM%uLJXTpn>6WD-;^zB;#1GLGqdb2=zTbI?|07K zd(L;id*Jt)nrZ^$w=eD`#;XYV8yDUXoC#Oh1(O+~5|vAm0j`(h2v0sBDnCI~-vi!> z>g7iSS^NhVGH0)grBZ1lW+c)nJ?;cJHt2k~f&r5mqLE%sCB6K4aut64E(exp*2k>C ztjMhYE~i$hfvdb4yvnK7-vxSo>N-tOLr373)H=0hLg*FMdbJk5el?mA88}!G&*o1mP)4*@mMl3s?ko&Y#GV0Cd^s$k@>`&Enm)+Ev=j7 z%iGJLvc2YfIq=Nr^||>FvaW=zua?(Cs5Ma%HD^9r1zJEr%wN31R{*#0BH$Ii0=R`20r(xaq$1Sm!=oC(z!A@&pqnQ6 z%%^bZtXtgcolxc*oKv0fqFhbgfHKCd8*mj&Chii1=E}qayla5t*#?XNei@lx0pSnf zH(b~92KaT$1@{(FCLk}wr4ZLHJE*#dkbW!*MYfC^3AP_ z%?GXKgA2`vbHek0R6r8qZcD{E!6wYOy9g7;9ikPZA4Oa-vfMDI^BlC*etfkR!(;n_ z%XJTt`EHxYKDr+wZQm8mu*GEhGmXkTJ%EtNNY^npNumc43Ki*x4m#SjX4R!3+hLQw z8w^w)u3HSZTH)5kaJv<5zjywp3$vFNJC0f%M;AJdErgGof#dWrgq8`ZNX965W{6P~ zidW7U2jVKF58`mViGq`)4{l?O&vO0vUMohH_bB4XWJDlYu7}XKAxIW%9|)hKrfvUF zESb@C+n*U!V}=GaB4_j{`ozni7hT6Il04?BKtC~E(?_?hNt)}GV2h3Q2aq|6e{j7D--o#!EM9VXm7#&-(uCbe6KCkcjSWkaO0iRR(Q7=*u7d7sBc!*AWUXRf-o7tymGAdPCV&=44~^> z1iHrfF<%zskma$`tPeaOj4^>>0%DUvflM}*ih*}@ASMiLK$k`L$*_J!Pr?p*42&%> zDPX#w1Xm069az4GoYJ*FuNQah)FW6>QvUd$H@g#DZ&u?zMx5I3FFTbgII`y)puEl@-L$kK2 zK>458K4hJqWI376!7Ggv|a^{ gIHh*dbKt=CrEdfSZ4}cW{v<0*3ARA1;HdBLDyZ literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/protocols.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/protocols.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27aae31ffb4564c5888f6e22082876c80c76edf3 GIT binary patch literal 644 zcmY*Wy^hmB5T3CeW0DI;1p=Cg&IK3gYD9ZiMHEO#rdV0=Ua(~En%y{< z01$5iJwhu{AqqOAKywu{-lVuGzWLdiZ+`acd_Dz|&V8)Ekp9WeUM9i7?J*gbP(g(a zv{)dr9W_xK7coKvCs4%~P$e?{9EX}B876u9o1Gl}tW-+7($!j7a`^!PY!haa=Xb76dbE#^>=CiQgDoNCnVFIDv315AkY#Q& zaN7BlVvDznl~&THM!Mdp@s)PS#5lMRekw#5kL&{>UiD?eD+yt4G0{ziLxwp+x(7CF zMsNY&pTGE)ybMVkUeTS2^{l6c%)>|ZsT;alZ44hKuv}-p${vR?o|wCo!XIGo5&Ve} W@drHo1rPqFh&8@H{Rgbae*Xb`@|cMm z;*0W2OHzyC%Z&8$pqdg(GV}Am`fjnu$0z3G#K*5>_zW`tmkH1ieW)S&KtuG=4bd+I zSsxEGDz!+jpz;@o4MdAw5hu_PMj$R001_XV85tSxGI%}Umbt(s+sIzT4ip3cFMCVw literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/query_plan_builder.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/query_plan_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..038341ebae2da7b8cf4183906ee25c5f9c8bb4c8 GIT binary patch literal 13479 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 IrgSCz52TUT{r~^~ literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/retrieval_filter_builder.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/retrieval_filter_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d931a60186c9bfb80e19acae8fd24260730c8a7 GIT binary patch literal 6166 zcmb_gYfK#16}~h3e!wm)yI$THZ0wC~z_DYK6g$B@8pmWIfYW4Scf9Nju&@tuXBND8 zP^{Eaab-1jUl6HM=&uslRozDW;Ks@4^2#PWRh9NW? zVWYZ$E~*dcqlSPXY77{orhqAG4w&h@E@FvV1JE6t4R)& zB#l1PebVtUsx26c#igJWj>klwQLX5T^TMDY;jl0rjP!;hl7L0EtScUy27)ddg4Kdr z(H#$oPb~DjoE*Vn{)uthYY71-J7b~oI96?g!c-h2O2S3Sr&CMEgCZvhqQs5HLkUs! zhD9zWjKR021#KJ)Zzo`0b;Ar@h=mB~z;jZ3Qiz4LZE3AedoddqfmlES3NSnhusp-F zzh*4xX@9(x*Ih9Nbi57h1@_%(EpO)yFmK>Xcq6=xd?{~&w~2S~W_X*E7N1k~Y%{^B zL^#3=ST*&$G0@j}+7Fhuiir^}3|5Fqq0Jv1k+%i@VH?cG1VgqUBl6z<$gFITjj~=g z-C}MUwAnec4BH%ICif$R>!eLnAS9bX#^#!>azKVY=M-b;mhR>z#}EQJ`b{~9B~y+E zq!8VSd3OMIk^`3l7e3;FLItA%2mn@9ys!0$;XH1*LIL48^Bl4IdA2li?UIs&+y~ zg0Znga7bj!y_7N#v%VZ4HJScnkp@ zYY0wFHNZ?r5XDw9F$VtM3WKd+*hEASTXAr#RSOFa#}j}*gdgW1i5sPu#EobH)iRY- z4V0d(DIRhybe6c7s6cZWWj)AKweH@Va_`O9U77mBnYz}Dr#`c5ch-hH%^S$*vCdnw zrO58OK7DH&a#jLF@ai%lmFWpRPKQsjYu#(pQvbk-nrfYkhoiO~ZQ4 zp;XPGa-$jI7rkP1PMuS3XzZd}u|^=8U&y-RnzveapVvKS+B|fGYRg zm5T@0tJ+dkZE5%6jHhZLej~m#wwz3Rp3hWPKeQR0)+{nwtQ)W=6w1dsbja^d@nZyt z1tjIsLuPW(hnXa^1s}GKvr--@01lDNcB7jhhdep%Ii0M-EYA)>YJ#}Y`w6Ob^_}hR zQB9})lnCWN3VcMNBLvXKfjEO03To${L1>PVnF$!%{_u!E*hL1r8MIZ+2_k9RlT+$f z%mQS{YX|ix$^1??!}wU$e5U73Zn$UgRX+rqUP?|0m>`mBfXtQy^%qigGhd5jhwrvM%lLGdASotwf~pI~ zl0H4&O$KN%Hjz#dPXsOKdm`9pH$4>yX}Jp?*rb@W_3Y-ZQ?@I1(#rS z0ga(KBhSp4;0-CN8=VKIowLZ6qQ5;m394Y`nhWN0GC^GK;dL~Ye-U3>CWi$4G<|T#i z_+{0SfHVg2eNh^I#I)Wgx?$poDmqDDmi~brKS$Q9 zC7ab8*Vo_G*>|6&Vslr&fB3ZjY>&@^4-qz_7*F7kfLmw>GMy0PRW}!j$0tQD5}p)D zN)u>GSB+Slii9P#hJ#WU5C}?xu*C6U40$w;lUikg>xrbYV2D=8g;Kz0*C2wR!@kEY0)18HfA5OII%~POdt9 z8K;+Y=dR8z*;X9~GW!lF&OKyAzA7&rOOXYhg_0X3i=!Vz-jA#~4`dv!b%zfomYaWn z@WX>^j^iu6cl+-2DFbh%I^N1StJj^4DQDx-vEO$5ren?7mO1{y%IMw5ok;5VD}R&L z`@^aJure{VJ~5q|m|p9jx&HdXxf|!!-TPDS{mZW9VA|bwyKL2cM45YMzE^d)7n_%j zt5q#|HU8n;o81N5$a<08qf{PG*^b{iqYMnM54@ckcw51j)&}H^t8Ni5G1p&I?2Vt= z>XoLWE7sMfQyEv~V&mfB>n|ww+D~m|Tf6rvys1;eN)^-uCs(UZDuHvWl_4b}jn zmW;!@SiZP>ehy5jfiZw@{vi-a!FV1BI8U%zFz`&Vb44!K&*=cH^*k%KUc_h+;poY9CoK-nHMcueZOFYJVl&-nZU6TtgyY?-!Dc6B{bH-7AeSH2M#a#E4?G6L~ zk6E(eZvz8AVGH&5JUa^JxMJpYPli4{2GD?hERe~DY31Jx#4nkAD4VrVz6yri3@|GM z7@`5j_&+4}YXc6|7!Be{0pr)8vuVA{n)PVh^zLN6rzFoJkrXG=-OzlcRqN%_(&+VL zi^IP-nJSmGlKe-dWl)kQNcE!pu|c`3+=l;;m5&nSSWZU%tgNIiD<3Hzr`}aQ0cu5A zNw_xWK2~nS=xyaM$|uU7l#hIDA+mCgLUZ(%UB0C4l7#o{k?*j}C@_koEQiy_ld*Kq zQJyZBls0j#TuM76#Vk>W@%x;(5qKV1$aTS6qJ+=mCekmX{Vi8`=kXpG$D~xI2NG2W zi)qR6$O{*Hk6#rw$PNB@OwcY5NQ`SY2!tZ&FC}nnw!7H1BQSOsxnzXl;@u1F_W&DV9gL~_pm=WC zye4XaYoeCz*FsT^KM&^H}xJH;SPbb7FWb2B&&@1lJDZr1+&4 z-e&qo<_=A(lXx7KFz_$>pvmeOhWRII{8DdX*e?-joQRoaQYiVQF~~3p<_kn$bl?92 D8rRLQ literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/retrieval_spec_factory.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/retrieval_spec_factory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1fb8b8fae8bc0933773f47e19e87ffe8c63305f GIT binary patch literal 5482 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 literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/router.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1c763fb99acc21f2396ba546f95dddd6c1dbf8f GIT binary patch literal 4382 zcmahMTTC0-^$vcG9~eI{1Z;u(>ib8oMQzhZ>EG~iz5l`q$7o)mSS*!=a=uiQVH&z!EQ`hRbxDxr z{BmBTPi7E%y;NKmsjRa-C`tM324EwL>-n5m%!&r)rjRX{C=&W^QDN=r?c76AE=eG_ zulHf8QWojm(#i^8ZCbEX?<256vBa*|C|;}N zrJSz6|Djl>d2wBk9<7O4)0C{SVdH5tUd-MP0C-76l3{Wr)8i(ce>r>S>?oXb zGB%{-oCoPSSI!1B`v$|Yn%_7_V;fIq(pk2{7Q6^BhuQNpXiAlN&d45ylwoF!9rIl! zd>dEoF}WOFxXjHFQni^}?k-$y$*SGt0*5t5$BHfsS9O?NK?~Et)z)8inp}}CTwV)T zb(vg47N$d2+lmeF$Ta7t7&n@i&$hjgqUl;cpY8BNeoc2o>O)eP{}DO`zXo2xFBxt@ zQLh%_spT5{vf-lCiG(08n*Q6yAHcy*eKmkh6EHM=ZI%bI83IxXDA zE-iG_A}x5t7AtZo3tgXd?k`-k+g@hFd?i3(Xg;K5}iE@exE2+F5^E}mq6XgvkxwfIPJfAP- z%RIk1c+jJh7Qqiz$s6$ag0%WcY_b-;(%>%DxhvcD275wH%-0h48<+0YFWpDPm^v|6 zo4C_hxLsekgNV2~K3f~V*Ol$_i26ZE+S5z-ka`5MWm5w1dfVzAbw@VA>(nra7u) zwNtCEnvm6YfS3q)Rhg>2>ew-LFa#t!1KyHshr>ZhD>H97K@Q#d&{*v|gn(()iECOb zeh&NGa&567k9$8fR>Ox7Fx4$)bIcD<1I$)kZ+dpj^swbFw?nz=0*wPlrMp_>+A+@; z^tnOqL>IYkmbj|!)#0PORR`c>UGR3)zk2d0<&AB}yc<|Oew5zz9B}n6x&S+VR73d# z3zxIMAi&eb?ij|k^VYwY#= z>F?Z5KVF0~+6(EYoG(^H9->;*f(NpZj&d3y&e=6L6`mQnQVSj6ld=5LNmy6&$dx4s zdu#-Ll_J#@_o3{EvCw>0`Bn05p=I#FfXCu`7&ZB8_e%*&ls*p zb5|gF@mSB(P|%%nsY0_N;CqtiHK$O_J}prht7JS*`-Pvhg_yuWHr_-DjPI}Q@?iwo;szVpM7#eQUb$ltYA%LW_mv74JlI=+2MEwx- zt#1#*%KCoh`-!i~#uYaHYWVfzx3?PXWi>q53{NQGiDvkm5BaI zqBG6tMJ0Ms4GgG(;bvf535=_OL3KFsRq~ffb>cKsIW_u0GkRW$o^SP$*uoxRWByi% zjEps7GfHfx8M~;&ba{&ngqy=jWjOiQ_yoA?pEjHK1?9fbm{_XEms)Nz0y-Qc{`VvO zAD23aVedBe!Ln_mx?0Al(so^ zy`$4I`g@BWMf%`phk9^}iV1r&+N~S6I7W~o*7e)LV!?d8IdJGJ!Xl0$(X{_z$M=Lu zN1rTLXi>A{Zn&Ownq%q5AfwqJ%5-nvJkhy3{ky|b{s}4%b$oAFgquLnLyvP{Q&bcR zBF}3c9#bwfQULpS$g6^6Q0N%)FT&$@M85$&Bq$bg{ei8i{1rT2kd~87Pn)DJ(x zC;Y?o-1$~8;P~*H{;8G&-oSHmzU4xQB_kJFZiGA}HsA6hAg9wnwI}x83pm4eG&{{j zaJ$PDI9%8+#bThHg;p|YwvtI`C6h+g*l#71`(3ip)n7PXqQmp79$#<&FCHs;DC4&K zdcEJDIab`t?fb1iwiDW^V+D-{-S|*Ao*OH&Bpby*Pv)3Cwb^4=*{?P2FC6{bXZ~2p zgO#a$Ea-%WOWs8Y6NOQ3*~`YAUZqa}hb@3S1&@}EVVGSq{txo$E_t*|GP@+bOK$#+ ZEGuMrmps@d-~EsC5;O2`0uNo?{{b_)Z7Bc% literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/sub_intent_detector.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/sub_intent_detector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1dd93d7c260c4f778d44225b637aa2489b7ce80 GIT binary patch literal 2635 zcmcguU2GIp6ux(6w%xXrh25%)Qds#5V@bD2gE5j8v|Z8^R$!~vQW&Q@Q`oxQUGL1c z-KHj`ZBj}IV4}9snDE#JOcs^60#+YMe3@aJv`qBL7kwkvMBu@A&dhF8T2SMI_u=fh z=jYyY&-u=o{h_k5oWL5oFddTxLVm_galsxJUEBtAk|;!>anegWC?y;@NEB|2DEt-9 zHXb?_jByJUAkL-N+Ctmn3*)*uY@W9r%~5ciHOGx_jhyIqtY(dC<~j2sxPvhL3_9L0 zvc`=7cxR1k_FY*xW5l@{ZyGnQ8@J5Sv=5BUaXd0$dliwIHzC+B zjU1kxcB9`In9rJ{me3tf_QS($;RyJjF-NSDR7O`deXuN@i0J`j9sLCi(j18vQVX>q z1WU(b>HY*}T!7-y9t!M=HADxf#!)C`4_L2UR|lH=IvqCooD7e8JE52eit%`}!OlgJ}-C>Ic zEMk>5w;wrlpe1aTy?&%QEQMMQHCrxBqZVDB8cf638=;UbcjxH zedJ?02|aMWTp$_pIp0rbXn2Oq@D^8FXO;BI>|K>vg02qe8Xgu13_oPSH#C)LK{>7t z1e?@8DaDdK0AEe&dod`-0t$=usfmD`N(Dd@RW&V$ic^5|AP9rKNhKXuwIGvE1!Fe+ zC6-L>sZ$AizlOUTx+OZGWI~o+EYO8 z|3dB$lRGt?xxVR8R=FBBYdRk+BCS~rPHz|B~xlb>Qh;lRb}rXqPQGONKT$Kip>M3Wiuqq4{!5M(HpJC8eiSw{Q|C64Kz|)dI;BgBc@5ON>N!E?YHMy*H=N zH@2IN?Z(?D<{D2f_%ZfYrgLs*=YntZg2%rISCXYB z#m!Qa?uX4doh14a4$o46I<=u*Y-{u=tkqDD5RnT%0%J^z4dYH=mce){!gzvv{4^+b zC`^SlFcruU{S@eDyh0{8hg$;wb^t#`@QWP7l!jsImutGrbge(8pB8!M1*s)q<()zm zdciIedm5C3dsf%zQOG<7o_5V&0`CGmy~^$SOZAhDA2eRB&--@X@r9=4tT5e|Jv!Z* zJDSsT-QOR`y<+SN<$WR3t31$b0E@dY?oAkBg=CL|H=B>W8kyl(0AxJVI?Uw>3@B0|155C0b15W2pJ z?m?c0;lXfoyp`-k`Vba;0( z93M`{y#zX(7fZ|}Id{#EbC;WA&U60XcgI)PQ2j*K*vZ*d%+UWkxbMWguS}38aA%i6 zI{y56-`C~LYVI(Czf#p_?Y)hC;BMgnisdh1`^hM!-^k8syn7*f)qVS9O?GK?`ha$J RqOWB8-QpMN(8%3D>u>IT(boU~ literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/term_mapping.cpython-312.pyc b/app/modules/rag/intent_router_v2/__pycache__/term_mapping.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..926800fb814f49a226a18ac53f8ca63ccc675aa7 GIT binary patch literal 4258 zcmcH+TWk~A_1^LLmDuFvXTpM)#w;ep4#_U1Y&Jkcc$5%Y9=mGTbmV#_se@yuckDo1 zM*`D}N+f~(%O8uF^LAIt_329rY{R6U6TfTbEOl*Sj z78R*inse_x_nvd-Jny-GaylCbv_Jp&di*0hA)n)<(s*M)*WQM~5)p|=6J&~xQi@|P z!Hsgzn-Zoeew3$#BN8{MbE_~gdXc|LjnSxuBS(m6o+qN^Cby2W&T=8^KX4MFT4OYs zOr@jgcq%E6z}$6Qnmr@2sljM6m5j%tiFj6G86Rvucd&aj+MQMM-O8_(m2P!Wxvaj^ zJ=PU7*c#Ajbad?(FjyiI8KokDzorvZACwiQaz#Nfqv8bj!|B8iypCg-aKj+y<*EeKWY(uVk`7k=-Z&TiGI-qy?wSR)UIth zl^IH+)F-0T(-PCnu|!msHOrKg{$5JdY~z_^3^k;gL}?t*DKV3fH0yLUc0M{GY5aH` zrhHnG(~s?$WIP?fq2?4*vCNc|G_Hbzc3U@`-X={gHA@UCcO;;A30`^d0q@LGn9g1-JlN#-@|N^f$^KV{!%X zmth6Xr+f&Mw}BGQP(FcERt&bR6&6DdW`&L+3kF_ESR(>Fk)cT${*Gowv1Hbah!zA$ z%21Ax*t&DUn1EjrVj_gD?S#P+$O> z^uG|N@v@W{*PKtGWjrVlczix`R+g9?i6*3rkwemqFwIhvQY>|CPDJ9#G@u2R%A_TR^w3QSQ?MwVg5!j**}{FU4W;1Vfs>AJgRXf-lej0`RC_iP<&fF@5V3stTw>*t_~B{D(gI9*{J z#5|p>st4(eR#D!ZDP2|iq`9*Cq|ccuS>O;En*T3J!l@z6Iv!7o z(L{oIfI@R@bYIOXNEgA`MDX^(gCi$Ljtw3h7ET=ere>Ou(ps}HG;(D4*y*Ff>62$q z4Gx_SS(q1@K%Tjn)*7UX)6t}iX8WSKa~+&(Pi4 zqXxge>KH6H`4yX=b$}SLjp2idMGv&2Fj(4?<(S`18`N9HP3lWc-7Gp?f>v1xzP$)>otc>PjDMmp22rL_TYB<v>i-41Zm=2YM2Dl4)ODwmF)klXxlAG~kFStESaU4Yk^WQ2#QH z^%6*(WK)M@{lBS`d@8h%4BH_YLXxc~ok-_NjyqJnp(>W961@)G*DyW)o&4((x$ED5{msJm4|e@**9T$MzyIHB7`Ip2H`D*4 zjI0;VV0!EsC50{++XL7a66pHR! zb%SP^i6%0V%)0f7oMLH744L#?!jP?IUAKUYW`@jeDmAcAOs+)$Dk0NgHj+z zXQ)iC+3JVGlfnstp&f(4$nsRP;PW6UhIr#9hFV_EB+`am%!c9cRIrL+7Pbv$Sx=22 z*022ouqLC;;j(piJR+Pu2sdeyt8n;k)3$Q6XUX}Fvmo7T?tEy5C0#z_KX``qe72+B zOD&KKjHHqhJ~Pr8mehEh!T)Xt3h=@-V?!`{Y){1H^f{Q0LB)-jY?FS0A!=7391f2` zwJ`{IhL@!-pfSR|>@WauYN&*h(J4s~v<5*iYG@cc1mXLcXu?=Ayc>mNkaP*_LJ&c4 z00FA!1~Ej#@`Pczq62(X$iD(GPwsoS4~{TAIKs6Lj&L%?rtsOV zzsZfPX*`OovDDNwyfoQXU}mUl7)My?7@64CZ*U%9-vS(FdN~9DLX6VCkZqq6``^gf g`;G2}-o<@a_bHy;#m3%Cwtrf_MuU$C0G-u;0HF%q_5c6? literal 0 HcmV?d00001 diff --git a/app/modules/rag/intent_router_v2/__pycache__/test_signals.cpython-312-pytest-9.0.2.pyc b/app/modules/rag/intent_router_v2/__pycache__/test_signals.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9bc3dd82cb8a316442508656fbdc157314f97ae GIT binary patch literal 2417 zcmbtV-ES0C6u)=&W4606rKNlnQ@X`S7z?`ueISIC57{jkq zC>ai>P>w(qUCV+Y#KUD&;I&)$n?`N)nk@%jSK8^ttUA)3w`ObsrE zD$Z_hB49DoGDWa3hQd-^F_*ETD2|6~qFGc{gxL3;N#}A$9jF$u%HCa^_ZQdU=S{fi zedm&=UTj#Q+~sM7AI}heJc9#52oEAdY;YM^nga6(wLU^<`6>wS(J3-PMyUzdFzC=q zzpDS;>Uy18)#?*6gl9ZQ*XZ49G^b;dg~z&dmG)D^cO_)uL%DQ>czb#gy+rkQ3zB>$ zPsowHhIwuW;X`_yP{8*DIS2b(q!aX-R3abIDZu%Su%C>P&wV3whIY@88K011yG3wV z=jRz0ayCE0I5SM#FB+X(Iv;=2WZaDFSvDF!!b%#{bcm%bQ!DL>>zSy*GbNUb>V-lS zENNz%aWo9DoD~OUd>9arWoDe~1My7G0=35TMT>Ea$2w?cW+10$%~)Y{!2`A{WyE42>!AC*?b!OJ7P6w1ZNr9(_hKcN%FnYdFS-i()6bJ`lzFH z{#nKvURD~Y8V~{ZexgC^PIMhg9zC7Vl8N4=){_uHlj%G?D13TuOl*A4e&SHi@kCGW zjD*-3wK!tl!~#bTay(dUUM*eT24)-@=3Zcs-J|;T*T--11qE`8&MDg_yB^D%7vxq) zZoMPV%R9jSIHJrco%8X;ywW)r>9qZwWr$q`mljz56E3l`Ey#-yom`qcfUQzJuWn5*){Sq2uooT(Zb-|YfaTv#lc8G8 zAgD^-RKV^wVTMdl3vupJiNI9xP0*p*T=6V~7=CY#{DV)x^MP3t?5@-~$s@3fJ0`0S zECQy*GX)W;G*L3qbD~#-O-tvNIg+skRY?R&dbY?`^h>nTFB_m=s$HGcEoNv|{yfX^ zop3@Fe)*egxfFHB1dN}Hp!%_%Q0D5|5Vb4vSU*AJ0Kus)KN+wG3r0!;(rg52!L z&C}cGcv&AEYjVoLNPl6aKz@J`0w1u<%{TV#wma z)J753wEiOaF-_xG2odTlX0lc$XNolk3u9XCt68i3lf`rsW+wja#W)+`8Hym6>E!C%5ewjz54nTpKCH6qh}uS)A6(A=k8 z3hl4V+z!4EbQFGO64(kjr&HHEq3hc\b(?:[\w.-]+/)*[\w.-]+\.(?:py|md|rst|txt|yaml|yml|json|toml|ini|cfg)\b)") +_PATH_HINT_PATTERN = re.compile(r"(?P\b(?:src|app|docs|tests)/[\w./-]*[\w-]\b)") +_SYMBOL_PATTERN = re.compile( + r"\b(?P[A-Z][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+|[A-Z][A-Za-z0-9_]{2,}|[a-z_][A-Za-z0-9_]{2,})\b" +) +_DOC_SECTION_PATTERN = re.compile(r"(?:section|раздел)\s+[\"'`#]?(?P[A-Za-zА-Яа-я0-9_ ./:-]{2,})", re.IGNORECASE) +_WORD_RE = re.compile(r"[A-Za-zА-Яа-яЁё-]+") + + +class AnchorExtractor: + def __init__( + self, + mapper: RuEnTermMapper | None = None, + canonicalizer: KeyTermCanonicalizer | None = None, + ) -> None: + self._mapper = mapper or RuEnTermMapper() + self._canonicalizer = canonicalizer or KeyTermCanonicalizer() + + def extract(self, text: str) -> list[QueryAnchor]: + anchors = self._file_anchors(text) + anchors.extend(self._symbol_anchors(text, file_anchors=anchors)) + anchors.extend(self._doc_ref_anchors(text)) + anchors.extend(self._key_term_anchors(text)) + return self._dedupe(anchors) + + def _file_anchors(self, text: str) -> list[QueryAnchor]: + anchors = self._anchors_from_matches(_FILE_PATTERN.finditer(text), anchor_type="FILE_PATH", confidence=0.95) + anchors.extend(self._anchors_from_matches(_PATH_HINT_PATTERN.finditer(text), anchor_type="FILE_PATH", confidence=0.8)) + return anchors + + def _symbol_anchors(self, text: str, *, file_anchors: list[QueryAnchor]) -> list[QueryAnchor]: + anchors: list[QueryAnchor] = [] + path_ranges = [(anchor.span.start, anchor.span.end) for anchor in file_anchors if anchor.span is not None] + path_segments = self._path_segments(file_anchors) + for match in _SYMBOL_PATTERN.finditer(text): + value = match.group("value") + if value.endswith((".py", ".md")) or "/" in value: + continue + if self._is_inside_path(match.start("value"), match.end("value"), path_ranges): + continue + if self._is_keyword(value): + continue + if file_anchors and value.lower() in path_segments: + continue + anchors.append(self._anchor("SYMBOL", value, match.start("value"), match.end("value"), 0.88, source="user_text")) + return anchors + + def _doc_ref_anchors(self, text: str) -> list[QueryAnchor]: + anchors = self._anchors_from_matches(_DOC_SECTION_PATTERN.finditer(text), anchor_type="DOC_REF", confidence=0.75, subtype="section") + for match in _FILE_PATTERN.finditer(text): + value = match.group("value") + if not value.lower().endswith((".md", ".rst", ".txt")): + continue + anchors.append(self._anchor("DOC_REF", value, match.start("value"), match.end("value"), 0.92, subtype="file", source="user_text")) + return anchors + + def _key_term_anchors(self, text: str) -> list[QueryAnchor]: + literals = set(self._mapper.all_literal_terms()) + anchors: list[QueryAnchor] = [] + for token in _WORD_RE.finditer(text): + value = token.group(0) + normalized = value.lower() + canonical = self._canonicalizer.canonicalize(value) + if canonical is None and normalized not in literals: + continue + anchors.append( + self._anchor( + "KEY_TERM", + canonical or value, + token.start(), + token.end(), + 0.9, + source="user_text", + ) + ) + return anchors + + def _anchors_from_matches( + self, + matches, + *, + anchor_type: str, + confidence: float, + subtype: str | None = None, + ) -> list[QueryAnchor]: + return [ + self._anchor(anchor_type, match.group("value"), match.start("value"), match.end("value"), confidence, subtype=subtype) + for match in matches + ] + + def _anchor( + self, + anchor_type: str, + value: str, + start: int, + end: int, + confidence: float, + subtype: str | None = None, + source: str = "user_text", + ) -> QueryAnchor: + return QueryAnchor( + type=anchor_type, + value=value, + subtype=subtype, + source=source, + span=AnchorSpan(start=start, end=end), + confidence=confidence, + ) + + def _dedupe(self, anchors: list[QueryAnchor]) -> list[QueryAnchor]: + result: list[QueryAnchor] = [] + seen: set[tuple[str, str, str | None, str]] = set() + for anchor in anchors: + key = (anchor.type, anchor.value, anchor.subtype, anchor.source) + if key in seen: + continue + seen.add(key) + result.append(anchor) + return result + + def _is_inside_path(self, start: int, end: int, ranges: list[tuple[int, int]]) -> bool: + return any(start >= left and end <= right for left, right in ranges) + + def _is_keyword(self, token: str) -> bool: + return token.lower() in PY_KEYWORDS + + def _path_segments(self, anchors: list[QueryAnchor]) -> set[str]: + values: set[str] = set() + for anchor in anchors: + parts = re.split(r"[/.]+", anchor.value.lower()) + for part in parts: + if not part: + continue + values.add(part) + return values | COMMON_PATH_SEGMENTS diff --git a/app/modules/rag/intent_router_v2/anchor_span_validator.py b/app/modules/rag/intent_router_v2/anchor_span_validator.py new file mode 100644 index 0000000..3065972 --- /dev/null +++ b/app/modules/rag/intent_router_v2/anchor_span_validator.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.models import QueryAnchor + + +class AnchorSpanValidator: + def sanitize(self, anchors: list[QueryAnchor], raw_len: int) -> list[QueryAnchor]: + result: list[QueryAnchor] = [] + for anchor in anchors: + if anchor.source != "user_text": + result.append(anchor.model_copy(update={"span": None})) + continue + if anchor.span is None: + result.append(anchor) + continue + start = int(anchor.span.start) + end = int(anchor.span.end) + if 0 <= start < end <= raw_len: + result.append(anchor) + continue + result.append(anchor.model_copy(update={"span": None, "confidence": max(anchor.confidence * 0.5, 0.0)})) + return result diff --git a/app/modules/rag/intent_router_v2/classifier.py b/app/modules/rag/intent_router_v2/classifier.py new file mode 100644 index 0000000..0ba2c3a --- /dev/null +++ b/app/modules/rag/intent_router_v2/classifier.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import json +import re + +from app.modules.rag.intent_router_v2.models import ConversationState, IntentDecision +from app.modules.rag.intent_router_v2.protocols import TextGenerator +from app.modules.rag.intent_router_v2.test_signals import has_test_focus + +_CODE_FILE_PATH_RE = re.compile( + r"\b(?:[\w.-]+/)*[\w.-]+\.(?:py|js|jsx|ts|tsx|java|kt|go|rb|php|c|cc|cpp|h|hpp|cs|swift|rs)(?!\w)\b", + re.IGNORECASE, +) + + +class IntentClassifierV2: + _GENERATE_DOCS_MARKERS = ( + "сгенерируй документац", + "подготовь документац", + "создай документац", + "генерац", + "generate documentation", + "write documentation", + ) + _DOCS_MARKERS = ("документац", "readme", "docs/", ".md", "spec", "runbook", "markdown") + _CODE_MARKERS = ("по коду", "код", "класс", "метод", "функц", "модул", "пакет", "файл", "block", "блок", "handler", "endpoint") + + def __init__(self, llm: TextGenerator | None = None) -> None: + self._llm = llm + + def classify(self, user_query: str, conversation_state: ConversationState) -> IntentDecision: + deterministic = self._deterministic(user_query) + if deterministic: + return deterministic + llm_decision = self._classify_with_llm(user_query, conversation_state) + if llm_decision: + return llm_decision + return IntentDecision(intent="PROJECT_MISC", confidence=0.55, reason="fallback_project_misc") + + def _deterministic(self, user_query: str) -> IntentDecision | None: + text = " ".join((user_query or "").lower().split()) + if any(marker in text for marker in self._GENERATE_DOCS_MARKERS): + return IntentDecision(intent="GENERATE_DOCS_FROM_CODE", confidence=0.97, reason="deterministic_generate_docs") + if self._looks_like_docs_question(text): + return IntentDecision(intent="DOCS_QA", confidence=0.9, reason="deterministic_docs") + if self._looks_like_code_question(user_query, text): + return IntentDecision(intent="CODE_QA", confidence=0.9, reason="deterministic_code") + return None + + def _classify_with_llm(self, user_query: str, conversation_state: ConversationState) -> IntentDecision | None: + if self._llm is None: + return None + payload = json.dumps( + { + "message": user_query, + "active_intent": conversation_state.active_intent, + "last_query": conversation_state.last_query, + "allowed_intents": ["CODE_QA", "DOCS_QA", "GENERATE_DOCS_FROM_CODE", "PROJECT_MISC"], + }, + ensure_ascii=False, + ) + try: + raw = self._llm.generate("rag_intent_router_v2", payload, log_context="rag.intent_router_v2.classify").strip() + except Exception: + return None + parsed = self._parse(raw) + if parsed is None: + return None + return parsed + + def _parse(self, raw: str) -> IntentDecision | None: + candidate = self._strip_code_fence(raw) + try: + payload = json.loads(candidate) + except json.JSONDecodeError: + return None + intent = str(payload.get("intent") or "").strip().upper() + if intent not in {"CODE_QA", "DOCS_QA", "GENERATE_DOCS_FROM_CODE", "PROJECT_MISC"}: + return None + return IntentDecision( + intent=intent, + confidence=float(payload.get("confidence") or 0.7), + reason=str(payload.get("reason") or "llm").strip() or "llm", + ) + + def _strip_code_fence(self, text: str) -> str: + if not text.startswith("```"): + return text + lines = text.splitlines() + if len(lines) < 3 or lines[-1].strip() != "```": + return text + return "\n".join(lines[1:-1]).strip() + + def _looks_like_docs_question(self, text: str) -> bool: + if self._has_code_file_path(text): + return False + return any(marker in text for marker in self._DOCS_MARKERS) + + def _looks_like_code_question(self, raw_text: str, lowered: str) -> bool: + if self._has_code_file_path(raw_text): + return True + if has_test_focus(lowered): + return True + if any(marker in lowered for marker in self._DOCS_MARKERS) and not any(marker in lowered for marker in self._CODE_MARKERS): + return False + if any(marker in lowered for marker in self._CODE_MARKERS): + return True + if re.search(r"\b[A-Z][A-Za-z0-9_]{2,}(?:\.[A-Za-z_][A-Za-z0-9_]*)*\b", raw_text or ""): + return True + return bool(re.search(r"\b[a-z_][A-Za-z0-9_]{2,}\(", raw_text or "")) + + def _has_code_file_path(self, text: str) -> bool: + return bool(_CODE_FILE_PATH_RE.search(text or "")) diff --git a/app/modules/rag/intent_router_v2/conversation_anchor_builder.py b/app/modules/rag/intent_router_v2/conversation_anchor_builder.py new file mode 100644 index 0000000..2ebaa07 --- /dev/null +++ b/app/modules/rag/intent_router_v2/conversation_anchor_builder.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.followup_detector import FollowUpDetector +from app.modules.rag.intent_router_v2.models import ConversationState, QueryAnchor + + +class ConversationAnchorBuilder: + def __init__(self, followup: FollowUpDetector | None = None) -> None: + self._followup = followup or FollowUpDetector() + + def build( + self, + raw: str, + state: ConversationState, + *, + continue_mode: bool, + has_user_symbol: bool, + has_user_file_path: bool, + ) -> list[QueryAnchor]: + if not continue_mode: + return [] + anchors: list[QueryAnchor] = [] + if has_user_file_path: + return anchors + for path in self._paths_for_carryover(state.active_path_scope): + anchors.append( + QueryAnchor( + type="FILE_PATH", + value=path, + source="conversation_state", + span=None, + confidence=0.6, + ) + ) + if has_user_symbol: + return anchors + if not self._followup.is_follow_up(raw): + return anchors + symbol = state.active_symbol or (state.active_code_span_symbols[0] if state.active_code_span_symbols else None) + if symbol: + anchors.append( + QueryAnchor( + type="SYMBOL", + value=symbol, + source="conversation_state", + span=None, + confidence=0.64, + ) + ) + return anchors + + def _paths_for_carryover(self, active_path_scope: list[str]) -> list[str]: + paths = list(active_path_scope or []) + file_paths = [path for path in paths if self._looks_like_file(path)] + if file_paths: + return file_paths[:1] + return paths[:1] + + def _looks_like_file(self, value: str) -> bool: + tail = (value or "").rsplit("/", 1)[-1] + return "." in tail diff --git a/app/modules/rag/intent_router_v2/conversation_policy.py b/app/modules/rag/intent_router_v2/conversation_policy.py new file mode 100644 index 0000000..3777f03 --- /dev/null +++ b/app/modules/rag/intent_router_v2/conversation_policy.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.models import ConversationState, IntentDecision + + +class ConversationPolicy: + _SWITCH_MARKERS = ( + "теперь", + "а теперь", + "давай теперь", + "переключ", + "new task", + "switch to", + "instead", + ) + _DOCS_SIGNALS = ("документац", "readme", "docs/", ".md") + _CODE_SIGNALS = ("по коду", "класс", "метод", "файл", "блок кода", "function", "class") + + def resolve(self, decision: IntentDecision, user_query: str, conversation_state: ConversationState) -> tuple[str, str]: + active_intent = conversation_state.active_intent + if active_intent is None: + return decision.intent, "START" + if active_intent == decision.intent: + return active_intent, "CONTINUE" + if self._has_explicit_switch(user_query): + return decision.intent, "SWITCH" + if self._is_hard_mismatch(active_intent, decision.intent, user_query): + return decision.intent, "SWITCH" + return active_intent, "CONTINUE" + + def _has_explicit_switch(self, user_query: str) -> bool: + text = " ".join((user_query or "").lower().split()) + return any(marker in text for marker in self._SWITCH_MARKERS) + + def _is_hard_mismatch(self, active_intent: str, candidate_intent: str, user_query: str) -> bool: + if active_intent == candidate_intent: + return False + text = " ".join((user_query or "").lower().split()) + if candidate_intent == "GENERATE_DOCS_FROM_CODE": + return True + if candidate_intent == "DOCS_QA": + return any(signal in text for signal in self._DOCS_SIGNALS) + if candidate_intent == "CODE_QA" and active_intent == "DOCS_QA": + return any(signal in text for signal in self._CODE_SIGNALS) + return False diff --git a/app/modules/rag/intent_router_v2/evidence_policy_factory.py b/app/modules/rag/intent_router_v2/evidence_policy_factory.py new file mode 100644 index 0000000..c090bd6 --- /dev/null +++ b/app/modules/rag/intent_router_v2/evidence_policy_factory.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.models import EvidencePolicy + + +class EvidencePolicyFactory: + def build( + self, + intent: str, + *, + sub_intent: str = "EXPLAIN", + negations: list[str] | None = None, + has_user_anchor: bool = True, + ) -> EvidencePolicy: + negations_set = set(negations or []) + if intent == "CODE_QA": + if sub_intent == "OPEN_FILE": + return EvidencePolicy(require_def=False, require_flow=False, require_spec=False, allow_answer_without_evidence=False) + if sub_intent == "EXPLAIN_LOCAL": + return EvidencePolicy(require_def=True, require_flow=False, require_spec=False, allow_answer_without_evidence=False) + if "tests" in negations_set and not has_user_anchor: + return EvidencePolicy(require_def=True, require_flow=False, require_spec=False, allow_answer_without_evidence=False) + return EvidencePolicy(require_def=True, require_flow=True, require_spec=False, allow_answer_without_evidence=False) + if intent == "DOCS_QA": + return EvidencePolicy(require_def=False, require_flow=False, require_spec=True, allow_answer_without_evidence=False) + if intent == "GENERATE_DOCS_FROM_CODE": + return EvidencePolicy(require_def=True, require_flow=False, require_spec=False, allow_answer_without_evidence=False) + return EvidencePolicy(require_def=False, require_flow=False, require_spec=False, allow_answer_without_evidence=True) diff --git a/app/modules/rag/intent_router_v2/factory.py b/app/modules/rag/intent_router_v2/factory.py new file mode 100644 index 0000000..e16bc49 --- /dev/null +++ b/app/modules/rag/intent_router_v2/factory.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from app.modules.agent.llm import AgentLlmService +from app.modules.agent.prompt_loader import PromptLoader +from app.modules.rag.intent_router_v2.classifier import IntentClassifierV2 +from app.modules.rag.intent_router_v2.router import IntentRouterV2 +from app.modules.shared.env_loader import load_workspace_env +from app.modules.shared.gigachat.client import GigaChatClient +from app.modules.shared.gigachat.settings import GigaChatSettings +from app.modules.shared.gigachat.token_provider import GigaChatTokenProvider + + +class GigaChatIntentRouterFactory: + def build(self) -> IntentRouterV2: + load_workspace_env() + settings = GigaChatSettings.from_env() + token_provider = GigaChatTokenProvider(settings) + client = GigaChatClient(settings, token_provider) + prompt_loader = PromptLoader() + llm = AgentLlmService(client=client, prompts=prompt_loader) + classifier = IntentClassifierV2(llm=llm) + return IntentRouterV2(classifier=classifier) diff --git a/app/modules/rag/intent_router_v2/followup_detector.py b/app/modules/rag/intent_router_v2/followup_detector.py new file mode 100644 index 0000000..d91d662 --- /dev/null +++ b/app/modules/rag/intent_router_v2/followup_detector.py @@ -0,0 +1,22 @@ +from __future__ import annotations + + +class FollowUpDetector: + _MARKERS = ( + "что дальше", + "почему", + "зачем", + "а что", + "уточни", + "подробнее", + "как именно", + "покажи подробнее", + ) + + def is_follow_up(self, raw: str) -> bool: + text = " ".join((raw or "").lower().split()) + if not text: + return False + if len(text.split()) <= 4: + return True + return any(marker in text for marker in self._MARKERS) diff --git a/app/modules/rag/intent_router_v2/graph_id_resolver.py b/app/modules/rag/intent_router_v2/graph_id_resolver.py new file mode 100644 index 0000000..a6cbf2e --- /dev/null +++ b/app/modules/rag/intent_router_v2/graph_id_resolver.py @@ -0,0 +1,13 @@ +from __future__ import annotations + + +class GraphIdResolver: + _GRAPH_MAP = { + "CODE_QA": "CodeQAGraph", + "DOCS_QA": "DocsQAGraph", + "GENERATE_DOCS_FROM_CODE": "GenerateDocsFromCodeGraph", + "PROJECT_MISC": "ProjectMiscGraph", + } + + def resolve(self, intent: str) -> str: + return self._GRAPH_MAP[intent] diff --git a/app/modules/rag/intent_router_v2/keyword_hint_builder.py b/app/modules/rag/intent_router_v2/keyword_hint_builder.py new file mode 100644 index 0000000..ed10577 --- /dev/null +++ b/app/modules/rag/intent_router_v2/keyword_hint_builder.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import re + +from app.modules.rag.intent_router_v2.normalization import FILE_PATH_RE +from app.modules.rag.intent_router_v2.symbol_rules import COMMON_PATH_SEGMENTS, PY_KEYWORDS + +_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]{2,}") + + +class KeywordHintBuilder: + def build(self, text: str) -> list[str]: + hints: list[str] = [] + path_segments = self._path_segments(text) + for token in _IDENTIFIER_RE.findall(text or ""): + if token.lower() in PY_KEYWORDS: + continue + if token.lower() in path_segments: + continue + if token not in hints: + hints.append(token) + for match in FILE_PATH_RE.finditer(text or ""): + candidate = match.group(0).lower() + if candidate not in hints: + hints.append(candidate) + return hints[:12] + + def _path_segments(self, text: str) -> set[str]: + values: set[str] = set(COMMON_PATH_SEGMENTS) + for match in FILE_PATH_RE.finditer(text or ""): + for part in re.split(r"[/.]+", match.group(0).lower()): + if part: + values.add(part) + return values diff --git a/app/modules/rag/intent_router_v2/keyword_hint_sanitizer.py b/app/modules/rag/intent_router_v2/keyword_hint_sanitizer.py new file mode 100644 index 0000000..7d44f26 --- /dev/null +++ b/app/modules/rag/intent_router_v2/keyword_hint_sanitizer.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.models import QueryAnchor + + +class KeywordHintSanitizer: + _GENERIC_KEY_TERMS = {"файл", "класс", "метод", "функция", "документация"} + _DOMAIN_ALLOWLIST = {"RAG", "API", "HTTP", "SQL"} + _DIR_SCOPE_MARKERS = ("в папке", "в директории", "в каталоге") + + def sanitize(self, raw: str, anchors: list[QueryAnchor], base_hints: list[str]) -> list[str]: + text = (raw or "").lower() + allow_dirs = any(marker in text for marker in self._DIR_SCOPE_MARKERS) + file_paths = [anchor.value for anchor in anchors if anchor.type == "FILE_PATH" and self._looks_like_file(anchor.value)] + known_dirs = {path.rsplit("/", 1)[0] for path in file_paths if "/" in path} + result: list[str] = [] + + for anchor in anchors: + if anchor.type == "FILE_PATH": + if self._looks_like_directory(anchor.value): + if not allow_dirs and (known_dirs or file_paths): + continue + self._append(result, anchor.value) + if anchor.type == "SYMBOL": + self._append(result, anchor.value) + + for token in base_hints: + if token in self._DOMAIN_ALLOWLIST: + self._append(result, token) + continue + lowered = token.lower() + if lowered in self._GENERIC_KEY_TERMS: + continue + if token in known_dirs and not allow_dirs: + continue + if "/" in token and "." not in token and not allow_dirs and file_paths: + continue + self._append(result, token) + return result[:8] + + def _append(self, values: list[str], candidate: str) -> None: + if candidate and candidate not in values: + values.append(candidate) + + def _looks_like_file(self, value: str) -> bool: + tail = (value or "").rsplit("/", 1)[-1] + return "." in tail + + def _looks_like_directory(self, value: str) -> bool: + return "/" in (value or "") and not self._looks_like_file(value) diff --git a/app/modules/rag/intent_router_v2/layer_query_builder.py b/app/modules/rag/intent_router_v2/layer_query_builder.py new file mode 100644 index 0000000..b1f1e77 --- /dev/null +++ b/app/modules/rag/intent_router_v2/layer_query_builder.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.models import LayerQuery, RepoContext + + +class LayerQueryBuilder: + def build(self, intent: str, repo_context: RepoContext, *, domains: list[str], layers_map: dict[str, list[tuple[str, int]]]) -> list[LayerQuery]: + available = set(repo_context.available_layers or []) + result: list[LayerQuery] = [] + for layer_id, top_k in layers_map[intent]: + if not self._layer_matches_domains(layer_id, domains): + continue + if available and layer_id not in available: + continue + result.append(LayerQuery(layer_id=layer_id, top_k=top_k)) + if result: + return result + return [ + LayerQuery(layer_id=layer_id, top_k=top_k) + for layer_id, top_k in layers_map[intent] + if self._layer_matches_domains(layer_id, domains) + ] + + def _layer_matches_domains(self, layer_id: str, domains: list[str]) -> bool: + if domains == ["CODE"]: + return layer_id.startswith("C") + if domains == ["DOCS"]: + return layer_id.startswith("D") + return layer_id.startswith("C") or layer_id.startswith("D") diff --git a/app/modules/rag/intent_router_v2/local_runner.py b/app/modules/rag/intent_router_v2/local_runner.py new file mode 100644 index 0000000..96cc484 --- /dev/null +++ b/app/modules/rag/intent_router_v2/local_runner.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import logging + +from app.modules.rag.intent_router_v2.models import ConversationState, IntentRouterResult, RepoContext +from app.modules.rag.intent_router_v2.router import IntentRouterV2 + +LOGGER = logging.getLogger(__name__) + + +class IntentRouterScenarioRunner: + def __init__(self, router: IntentRouterV2) -> None: + self._router = router + + def run(self, queries: list[str], repo_context: RepoContext | None = None) -> list[IntentRouterResult]: + state = ConversationState() + context = repo_context or RepoContext() + results: list[IntentRouterResult] = [] + for index, user_query in enumerate(queries, start=1): + LOGGER.warning("intent router local input: turn=%s user_query=%s", index, user_query) + result = self._router.route(user_query, state, context) + LOGGER.warning("intent router local output: turn=%s result=%s", index, result.model_dump_json(ensure_ascii=False)) + results.append(result) + state = state.advance(result) + return results diff --git a/app/modules/rag/intent_router_v2/logger.py b/app/modules/rag/intent_router_v2/logger.py new file mode 100644 index 0000000..2775bf8 --- /dev/null +++ b/app/modules/rag/intent_router_v2/logger.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import logging + +from app.modules.rag.intent_router_v2.models import ConversationState, IntentRouterResult, RepoContext + +LOGGER = logging.getLogger(__name__) + + +class IntentRouterLogger: + def log_request(self, user_query: str, conversation_state: ConversationState, repo_context: RepoContext) -> None: + LOGGER.warning( + "intent router v2 request: turn=%s active_intent=%s user_query=%s languages=%s domains=%s", + conversation_state.turn_index + 1, + conversation_state.active_intent, + " ".join((user_query or "").split()), + repo_context.languages, + repo_context.available_domains, + ) + + def log_result(self, result: IntentRouterResult) -> None: + LOGGER.warning("intent router v2 result: %s", result.model_dump_json(ensure_ascii=False)) diff --git a/app/modules/rag/intent_router_v2/models.py b/app/modules/rag/intent_router_v2/models.py new file mode 100644 index 0000000..d5897d5 --- /dev/null +++ b/app/modules/rag/intent_router_v2/models.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +import re +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +IntentType = Literal["CODE_QA", "DOCS_QA", "GENERATE_DOCS_FROM_CODE", "PROJECT_MISC"] +ConversationMode = Literal["START", "CONTINUE", "SWITCH"] +AnchorType = Literal["FILE_PATH", "SYMBOL", "DOC_REF", "KEY_TERM"] +AnchorSource = Literal["user_text", "conversation_state", "heuristic"] +_INLINE_CODE_RE = re.compile(r"`([^`]*)`") +_CODE_SYMBOL_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]{2,})\b") + + +class AnchorSpan(BaseModel): + model_config = ConfigDict(extra="forbid") + + start: int = 0 + end: int = 0 + + +class QueryAnchor(BaseModel): + model_config = ConfigDict(extra="forbid") + + type: AnchorType + value: str + source: AnchorSource = "user_text" + subtype: str | None = None + span: AnchorSpan | None = None + confidence: float = 0.0 + + @field_validator("confidence") + @classmethod + def clamp_confidence(cls, value: float) -> float: + return max(0.0, min(1.0, float(value))) + + +class QueryPlan(BaseModel): + model_config = ConfigDict(extra="forbid") + + raw: str + normalized: str + sub_intent: str = "EXPLAIN" + negations: list[str] = Field(default_factory=list) + expansions: list[str] = Field(default_factory=list) + keyword_hints: list[str] = Field(default_factory=list) + anchors: list[QueryAnchor] = Field(default_factory=list) + + +class LayerQuery(BaseModel): + model_config = ConfigDict(extra="forbid") + + layer_id: str + top_k: int + + +class CodeRetrievalFilters(BaseModel): + model_config = ConfigDict(extra="forbid") + + test_policy: str = "EXCLUDE" + path_scope: list[str] = Field(default_factory=list) + language: list[str] = Field(default_factory=list) + + +class DocsRetrievalFilters(BaseModel): + model_config = ConfigDict(extra="forbid") + + path_scope: list[str] = Field(default_factory=list) + doc_kinds: list[str] = Field(default_factory=list) + doc_language: list[str] = Field(default_factory=list) + + +class HybridRetrievalFilters(BaseModel): + model_config = ConfigDict(extra="forbid") + + test_policy: str = "EXCLUDE" + path_scope: list[str] = Field(default_factory=list) + language: list[str] = Field(default_factory=list) + doc_kinds: list[str] = Field(default_factory=list) + doc_language: list[str] = Field(default_factory=list) + + +class RetrievalSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + domains: list[str] = Field(default_factory=list) + layer_queries: list[LayerQuery] = Field(default_factory=list) + filters: CodeRetrievalFilters | DocsRetrievalFilters | HybridRetrievalFilters = Field(default_factory=CodeRetrievalFilters) + rerank_profile: str = "" + + +class EvidencePolicy(BaseModel): + model_config = ConfigDict(extra="forbid") + + require_def: bool = False + require_flow: bool = False + require_spec: bool = False + allow_answer_without_evidence: bool = False + + +class IntentRouterResult(BaseModel): + model_config = ConfigDict(extra="forbid") + + schema_version: str = "1.1" + intent: IntentType + graph_id: str + conversation_mode: ConversationMode + query_plan: QueryPlan + retrieval_spec: RetrievalSpec + evidence_policy: EvidencePolicy + + +class ConversationState(BaseModel): + model_config = ConfigDict(extra="forbid") + + active_intent: IntentType | None = None + active_domain: str | None = None + active_anchors: list[QueryAnchor] = Field(default_factory=list) + active_symbol: str | None = None + active_path_scope: list[str] = Field(default_factory=list) + active_code_span_symbols: list[str] = Field(default_factory=list) + last_query: str = "" + turn_index: int = 0 + + def advance(self, result: IntentRouterResult) -> "ConversationState": + user_anchors = [anchor for anchor in result.query_plan.anchors if anchor.source == "user_text"] + symbol_candidates = [anchor.value for anchor in user_anchors if anchor.type == "SYMBOL"] + has_user_file_anchor = any(anchor.type == "FILE_PATH" for anchor in user_anchors) + if symbol_candidates: + active_symbol = symbol_candidates[-1] + elif has_user_file_anchor: + active_symbol = None + else: + active_symbol = self.active_symbol + raw_code_symbols = _extract_code_symbols(result.query_plan.raw) + active_code_span_symbols = raw_code_symbols or list(self.active_code_span_symbols) + path_scope = list(getattr(result.retrieval_spec.filters, "path_scope", []) or []) + active_domains = list(result.retrieval_spec.domains or []) + active_domain = active_domains[0] if len(active_domains) == 1 else self.active_domain + return ConversationState( + active_intent=result.intent, + active_domain=active_domain, + active_anchors=list(user_anchors), + active_symbol=active_symbol, + active_path_scope=path_scope or list(self.active_path_scope), + active_code_span_symbols=active_code_span_symbols, + last_query=result.query_plan.raw, + turn_index=self.turn_index + 1, + ) + + +class RepoContext(BaseModel): + model_config = ConfigDict(extra="forbid") + + languages: list[str] = Field(default_factory=list) + available_domains: list[str] = Field(default_factory=lambda: ["CODE", "DOCS"]) + available_layers: list[str] = Field(default_factory=list) + + +class IntentDecision(BaseModel): + model_config = ConfigDict(extra="forbid") + + intent: IntentType + confidence: float = 0.0 + reason: str = "" + + @field_validator("confidence") + @classmethod + def clamp_confidence(cls, value: float) -> float: + return max(0.0, min(1.0, float(value))) + + +def _extract_code_symbols(raw: str) -> list[str]: + symbols: list[str] = [] + for match in _INLINE_CODE_RE.finditer(raw or ""): + snippet = match.group(1) + for token in _CODE_SYMBOL_RE.findall(snippet): + if token not in symbols: + symbols.append(token) + return symbols[:8] diff --git a/app/modules/rag/intent_router_v2/negation_detector.py b/app/modules/rag/intent_router_v2/negation_detector.py new file mode 100644 index 0000000..c3fadeb --- /dev/null +++ b/app/modules/rag/intent_router_v2/negation_detector.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import re + +_TEST_NEG_RE = re.compile( + r"(?:не\s+про\s+тест|без\s+тест|кроме\s+тест|про\s+прод\s+код|только\s+прод|production\s+code)", + re.IGNORECASE, +) + + +class NegationDetector: + def detect(self, text: str) -> set[str]: + lowered = (text or "").lower() + negations: set[str] = set() + if _TEST_NEG_RE.search(lowered): + negations.add("tests") + return negations diff --git a/app/modules/rag/intent_router_v2/normalization.py b/app/modules/rag/intent_router_v2/normalization.py new file mode 100644 index 0000000..cbbca79 --- /dev/null +++ b/app/modules/rag/intent_router_v2/normalization.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import re + +CODE_SPAN_RE = re.compile(r"`[^`]*`") +FILE_PATH_RE = re.compile( + r"(? str: + text = raw or "" + protected = _ProtectedText() + text = self._protect(text, protected) + text = self._collapse_whitespace(text) + text = text.translate(QUOTE_TRANSLATION) + text = SPACE_BEFORE_PUNCT_RE.sub(r"\1", text) + text = SPACE_AFTER_PUNCT_RE.sub(r"\1 ", text) + text = self._collapse_whitespace(text) + return protected.restore(text) + + def _protect(self, text: str, protected: "_ProtectedText") -> str: + for pattern in (CODE_SPAN_RE, FILE_PATH_RE, DOTTED_IDENT_RE, CAMEL_RE, SNAKE_RE): + text = pattern.sub(protected.replace, text) + return text + + def _collapse_whitespace(self, text: str) -> str: + return WS_RE.sub(" ", text).strip() + + +class _ProtectedText: + def __init__(self) -> None: + self._items: dict[str, str] = {} + self._index = 0 + + def replace(self, match: re.Match[str]) -> str: + placeholder = f"@@P{self._index}@@" + self._items[placeholder] = match.group(0) + self._index += 1 + return placeholder + + def restore(self, text: str) -> str: + restored = text + for placeholder, value in self._items.items(): + restored = restored.replace(placeholder, value) + return restored diff --git a/app/modules/rag/intent_router_v2/normalization_terms.py b/app/modules/rag/intent_router_v2/normalization_terms.py new file mode 100644 index 0000000..41aa85e --- /dev/null +++ b/app/modules/rag/intent_router_v2/normalization_terms.py @@ -0,0 +1,48 @@ +from __future__ import annotations + + +class KeyTermCanonicalizer: + _ALIASES: dict[str, set[str]] = { + "файл": { + "файл", + "файла", + "файле", + "файлу", + "файлом", + "файлы", + "файлов", + "файлам", + "файлами", + }, + "класс": {"класс", "класса", "классе", "классу", "классом", "классы", "классов", "классам"}, + "функция": {"функция", "функции", "функцию", "функцией", "функциях"}, + "метод": {"метод", "метода", "методе", "методу", "методом", "методы"}, + "документация": {"документация", "документации", "документацию"}, + "тест": {"тест", "тесты", "тестов", "тестам", "тестами", "юнит-тест", "юниттест"}, + "модуль": {"модуль", "модуля"}, + "пакет": {"пакет"}, + } + + def __init__(self) -> None: + self._token_to_canonical = self._build_index() + + def canonicalize(self, token: str) -> str | None: + return self._token_to_canonical.get((token or "").lower()) + + def aliases(self) -> set[str]: + values: set[str] = set() + for forms in self._ALIASES.values(): + values.update(forms) + return values + + def is_test_term(self, token: str) -> bool: + canonical = self.canonicalize(token) + return canonical == "тест" + + def _build_index(self) -> dict[str, str]: + index: dict[str, str] = {} + for canonical, forms in self._ALIASES.items(): + index[canonical] = canonical + for form in forms: + index[form] = canonical + return index diff --git a/app/modules/rag/intent_router_v2/protocols.py b/app/modules/rag/intent_router_v2/protocols.py new file mode 100644 index 0000000..4b88f20 --- /dev/null +++ b/app/modules/rag/intent_router_v2/protocols.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from typing import Protocol + + +class TextGenerator(Protocol): + def generate(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> str: ... diff --git a/app/modules/rag/intent_router_v2/query_normalizer.py b/app/modules/rag/intent_router_v2/query_normalizer.py new file mode 100644 index 0000000..7b64914 --- /dev/null +++ b/app/modules/rag/intent_router_v2/query_normalizer.py @@ -0,0 +1,3 @@ +from app.modules.rag.intent_router_v2.normalization import QueryNormalizer + +__all__ = ["QueryNormalizer"] diff --git a/app/modules/rag/intent_router_v2/query_plan_builder.py b/app/modules/rag/intent_router_v2/query_plan_builder.py new file mode 100644 index 0000000..47f30fc --- /dev/null +++ b/app/modules/rag/intent_router_v2/query_plan_builder.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.anchor_extractor import AnchorExtractor +from app.modules.rag.intent_router_v2.anchor_span_validator import AnchorSpanValidator +from app.modules.rag.intent_router_v2.conversation_anchor_builder import ConversationAnchorBuilder +from app.modules.rag.intent_router_v2.keyword_hint_builder import KeywordHintBuilder +from app.modules.rag.intent_router_v2.keyword_hint_sanitizer import KeywordHintSanitizer +from app.modules.rag.intent_router_v2.models import ConversationState, QueryAnchor, QueryPlan +from app.modules.rag.intent_router_v2.negation_detector import NegationDetector +from app.modules.rag.intent_router_v2.normalization import QueryNormalizer +from app.modules.rag.intent_router_v2.sub_intent_detector import SubIntentDetector +from app.modules.rag.intent_router_v2.test_signals import has_test_focus, is_negative_test_request, is_test_related_token +from app.modules.rag.intent_router_v2.term_mapping import RuEnTermMapper + + +class QueryPlanBuilder: + _WHY_MARKERS = ("почему", "зачем", "откуда", "из-за чего") + _NEXT_STEP_MARKERS = ("что дальше", "дальше что", "и что теперь", "продолжай") + def __init__( + self, + normalizer: QueryNormalizer | None = None, + extractor: AnchorExtractor | None = None, + mapper: RuEnTermMapper | None = None, + keyword_hints: KeywordHintBuilder | None = None, + keyword_hint_sanitizer: KeywordHintSanitizer | None = None, + carryover: ConversationAnchorBuilder | None = None, + span_validator: AnchorSpanValidator | None = None, + sub_intent_detector: SubIntentDetector | None = None, + negation_detector: NegationDetector | None = None, + ) -> None: + self._normalizer = normalizer or QueryNormalizer() + self._extractor = extractor or AnchorExtractor() + self._mapper = mapper or RuEnTermMapper() + self._keyword_hints_builder = keyword_hints or KeywordHintBuilder() + self._keyword_hint_sanitizer = keyword_hint_sanitizer or KeywordHintSanitizer() + self._carryover = carryover or ConversationAnchorBuilder() + self._span_validator = span_validator or AnchorSpanValidator() + self._sub_intent_detector = sub_intent_detector or SubIntentDetector() + self._negation_detector = negation_detector or NegationDetector() + + def build( + self, + user_query: str, + conversation_state: ConversationState, + continue_mode: bool, + *, + conversation_mode: str = "START", + intent: str = "PROJECT_MISC", + ) -> QueryPlan: + raw = user_query or "" + normalized = self._normalizer.normalize(raw) + if not normalized and raw.strip(): + normalized = raw + negations = self._negation_detector.detect(normalized) + user_anchors = self._span_validator.sanitize(self._extractor.extract(raw), len(raw)) + has_file_path = any(anchor.type == "FILE_PATH" and anchor.source == "user_text" for anchor in user_anchors) + sub_intent = self._sub_intent_detector.detect(raw, has_file_path=has_file_path, negations=negations) + merged_anchors = self._merge_anchors( + raw, + user_anchors, + conversation_state, + continue_mode, + conversation_mode=conversation_mode, + intent=intent, + ) + skip_tests = "tests" in negations or is_negative_test_request(raw) + cleaned_anchors = self._remove_negated_test_terms(skip_tests, merged_anchors) + sub_intent = self._resolve_sub_intent(sub_intent, raw, cleaned_anchors, intent=intent, negations=negations) + if intent == "DOCS_QA": + sub_intent = "EXPLAIN" + expansions = self._expansions(normalized, cleaned_anchors, skip_tests=skip_tests) + keyword_hints = self._keyword_hints( + raw, + normalized, + cleaned_anchors, + skip_tests=skip_tests, + intent=intent, + state=conversation_state, + ) + return QueryPlan( + raw=raw, + normalized=normalized, + sub_intent=sub_intent, + negations=sorted(negations), + expansions=expansions, + keyword_hints=keyword_hints, + anchors=cleaned_anchors, + ) + + def _merge_anchors( + self, + raw: str, + anchors: list[QueryAnchor], + state: ConversationState, + continue_mode: bool, + *, + conversation_mode: str, + intent: str, + ) -> list[QueryAnchor]: + has_user_symbol = any(anchor.type == "SYMBOL" and anchor.source == "user_text" for anchor in anchors) + has_user_file = any(anchor.type == "FILE_PATH" and anchor.source == "user_text" for anchor in anchors) + inherited = self._carryover.build( + raw, + state, + continue_mode=continue_mode, + has_user_symbol=has_user_symbol, + has_user_file_path=has_user_file, + ) + if ( + conversation_mode == "SWITCH" + and intent == "DOCS_QA" + and not has_user_file + and not has_user_symbol + and state.active_symbol + ): + inherited.append( + QueryAnchor( + type="SYMBOL", + value=state.active_symbol, + source="conversation_state", + span=None, + confidence=0.62, + ) + ) + return self._dedupe(anchors + inherited) + + def _expansions(self, normalized: str, anchors: list[QueryAnchor], *, skip_tests: bool) -> list[str]: + values = self._mapper.expand(normalized) + has_symbol = any(anchor.type == "SYMBOL" for anchor in anchors) + if has_symbol: + values = [value for value in values if value.lower() not in {"def", "class"}] + if not skip_tests and has_test_focus(normalized): + for candidate in ("test", "unit test"): + if candidate not in values: + values.append(candidate) + for anchor in anchors: + if anchor.type == "SYMBOL" and anchor.value not in values: + values.append(anchor.value) + if skip_tests: + values = [value for value in values if not is_test_related_token(value)] + return values[:16] + + def _keyword_hints( + self, + raw: str, + normalized: str, + anchors: list[QueryAnchor], + *, + skip_tests: bool, + intent: str, + state: ConversationState, + ) -> list[str]: + values = self._keyword_hints_builder.build(normalized) + for anchor in anchors: + if anchor.type not in {"FILE_PATH", "SYMBOL"}: + continue + candidate = anchor.value + if candidate not in values: + values.append(candidate) + if skip_tests: + values = [value for value in values if not is_test_related_token(value)] + sanitized = self._keyword_hint_sanitizer.sanitize(raw, anchors, values) + if intent == "DOCS_QA" and not sanitized: + fallback = list(dict.fromkeys([*self._expansions(normalized, anchors, skip_tests=skip_tests)])) + sanitized = fallback[:3] + if state.active_symbol and state.active_symbol not in sanitized: + sanitized.append(state.active_symbol) + sanitized = sanitized[:5] + return sanitized + + def _remove_negated_test_terms(self, skip_tests: bool, anchors: list[QueryAnchor]) -> list[QueryAnchor]: + if not skip_tests: + return anchors + result: list[QueryAnchor] = [] + for anchor in anchors: + if anchor.type not in {"KEY_TERM", "SYMBOL"}: + result.append(anchor) + continue + if is_test_related_token(anchor.value): + continue + result.append(anchor) + return result + + def _dedupe(self, anchors: list[QueryAnchor]) -> list[QueryAnchor]: + result: list[QueryAnchor] = [] + seen: set[tuple[str, str, str | None, str]] = set() + for anchor in anchors: + key = (anchor.type, anchor.value, anchor.subtype, anchor.source) + if key in seen: + continue + seen.add(key) + result.append(anchor) + return result + + def _resolve_sub_intent( + self, + candidate: str, + raw: str, + anchors: list[QueryAnchor], + *, + intent: str, + negations: set[str], + ) -> str: + if candidate != "EXPLAIN": + return candidate + if intent != "CODE_QA": + return candidate + text = " ".join((raw or "").lower().split()) + has_symbol = any(anchor.type == "SYMBOL" and anchor.confidence >= 0.6 for anchor in anchors) + has_file = any(anchor.type == "FILE_PATH" and self._looks_like_file(anchor.value) and anchor.confidence >= 0.6 for anchor in anchors) + has_user_anchor = any(anchor.source == "user_text" for anchor in anchors) + is_why = any(marker in text for marker in self._WHY_MARKERS) + is_next_steps = any(marker in text for marker in self._NEXT_STEP_MARKERS) + is_short_generic = len(text.split()) <= 4 and text.endswith("?") + if (is_why and has_file and has_symbol) or ((is_next_steps or is_short_generic) and has_file): + return "EXPLAIN_LOCAL" + if "tests" in negations and not has_user_anchor and (has_file or has_symbol): + return "EXPLAIN_LOCAL" + return candidate + + def _looks_like_file(self, value: str) -> bool: + tail = (value or "").rsplit("/", 1)[-1] + return "." in tail diff --git a/app/modules/rag/intent_router_v2/retrieval_filter_builder.py b/app/modules/rag/intent_router_v2/retrieval_filter_builder.py new file mode 100644 index 0000000..ed86b9c --- /dev/null +++ b/app/modules/rag/intent_router_v2/retrieval_filter_builder.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.models import ( + CodeRetrievalFilters, + ConversationState, + DocsRetrievalFilters, + HybridRetrievalFilters, + QueryAnchor, + RepoContext, +) +from app.modules.rag.intent_router_v2.test_signals import has_test_focus, is_negative_test_request, is_test_related_token + + +class RetrievalFilterBuilder: + def build( + self, + domains: list[str], + anchors: list[QueryAnchor], + repo_context: RepoContext, + *, + raw_query: str, + conversation_state: ConversationState | None, + conversation_mode: str, + sub_intent: str = "EXPLAIN", + ) -> CodeRetrievalFilters | DocsRetrievalFilters | HybridRetrievalFilters: + path_scope = self._path_scope( + anchors, + conversation_state=conversation_state, + conversation_mode=conversation_mode, + raw_query=raw_query, + sub_intent=sub_intent, + ) + if domains == ["DOCS"]: + return DocsRetrievalFilters( + path_scope=path_scope, + doc_kinds=self._doc_kinds(anchors, raw_query), + doc_language=[], + ) + if domains == ["CODE"]: + return CodeRetrievalFilters( + test_policy=self._test_policy(raw_query, anchors), + path_scope=path_scope, + language=list(repo_context.languages), + ) + return HybridRetrievalFilters( + test_policy=self._test_policy(raw_query, anchors), + path_scope=path_scope, + language=list(repo_context.languages), + doc_kinds=self._doc_kinds(anchors, raw_query), + doc_language=[], + ) + + def _test_policy(self, raw_query: str, anchors: list[QueryAnchor]) -> str: + if is_negative_test_request(raw_query): + return "EXCLUDE" + if has_test_focus(raw_query): + return "INCLUDE" + has_test_keyterm = any(anchor.type == "KEY_TERM" and is_test_related_token(anchor.value) for anchor in anchors) + return "INCLUDE" if has_test_keyterm else "EXCLUDE" + + def _path_scope( + self, + anchors: list[QueryAnchor], + *, + conversation_state: ConversationState | None, + conversation_mode: str, + raw_query: str, + sub_intent: str, + ) -> list[str]: + values: list[str] = [] + has_user_file_anchor = False + file_values: list[str] = [] + for anchor in anchors: + if anchor.type != "FILE_PATH": + continue + if anchor.source == "user_text": + has_user_file_anchor = True + if anchor.value not in values: + values.append(anchor.value) + if self._looks_like_file_path(anchor.value) and anchor.value not in file_values: + file_values.append(anchor.value) + parent = anchor.value.rsplit("/", 1)[0] if "/" in anchor.value and self._looks_like_file_path(anchor.value) else "" + if parent and parent not in values: + values.append(parent) + if sub_intent in {"OPEN_FILE", "EXPLAIN_LOCAL"} and file_values and not self._is_explicit_directory_scope(raw_query): + return file_values[:6] + if has_user_file_anchor or conversation_mode != "CONTINUE": + return values[:6] + if values: + return values[:6] + inherited = list((conversation_state.active_path_scope if conversation_state else []) or []) + return inherited[:6] + + def _doc_kinds(self, anchors: list[QueryAnchor], raw_query: str) -> list[str]: + text = (raw_query or "").lower() + kinds: list[str] = [] + has_readme = "readme" in text or any( + anchor.type in {"DOC_REF", "FILE_PATH"} and anchor.value.lower().endswith("readme.md") + for anchor in anchors + ) + if has_readme: + kinds.append("README") + return kinds + + def _looks_like_file_path(self, value: str) -> bool: + filename = value.rsplit("/", 1)[-1] + return "." in filename + + def _is_explicit_directory_scope(self, raw_query: str) -> bool: + text = (raw_query or "").lower() + return any(marker in text for marker in ("в папке", "в директории", "в каталоге")) diff --git a/app/modules/rag/intent_router_v2/retrieval_spec_factory.py b/app/modules/rag/intent_router_v2/retrieval_spec_factory.py new file mode 100644 index 0000000..69ab20d --- /dev/null +++ b/app/modules/rag/intent_router_v2/retrieval_spec_factory.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from app.modules.rag.contracts.enums import RagLayer +from app.modules.rag.intent_router_v2.layer_query_builder import LayerQueryBuilder +from app.modules.rag.intent_router_v2.models import ConversationState, QueryAnchor, RepoContext, RetrievalSpec +from app.modules.rag.intent_router_v2.retrieval_filter_builder import RetrievalFilterBuilder + + +class RetrievalSpecFactory: + _LAYERS = { + "CODE_QA": [ + (RagLayer.CODE_ENTRYPOINTS, 6), + (RagLayer.CODE_SYMBOL_CATALOG, 8), + (RagLayer.CODE_DEPENDENCY_GRAPH, 6), + (RagLayer.CODE_SOURCE_CHUNKS, 8), + ], + "DOCS_QA": [ + (RagLayer.DOCS_MODULE_CATALOG, 5), + (RagLayer.DOCS_FACT_INDEX, 8), + (RagLayer.DOCS_SECTION_INDEX, 8), + (RagLayer.DOCS_POLICY_INDEX, 4), + ], + "GENERATE_DOCS_FROM_CODE": [ + (RagLayer.CODE_SYMBOL_CATALOG, 12), + (RagLayer.CODE_DEPENDENCY_GRAPH, 8), + (RagLayer.CODE_SOURCE_CHUNKS, 12), + (RagLayer.CODE_ENTRYPOINTS, 6), + ], + "PROJECT_MISC": [ + (RagLayer.DOCS_MODULE_CATALOG, 4), + (RagLayer.DOCS_SECTION_INDEX, 6), + (RagLayer.CODE_SYMBOL_CATALOG, 4), + (RagLayer.CODE_SOURCE_CHUNKS, 4), + ], + } + _DOMAINS = { + "CODE_QA": ["CODE"], + "DOCS_QA": ["DOCS"], + "GENERATE_DOCS_FROM_CODE": ["CODE"], + "PROJECT_MISC": ["CODE", "DOCS"], + } + _RERANK = { + "CODE_QA": "code", + "DOCS_QA": "docs", + "GENERATE_DOCS_FROM_CODE": "generate", + "PROJECT_MISC": "project", + } + _OPEN_FILE_LAYERS = [ + (RagLayer.CODE_SOURCE_CHUNKS, 12), + ] + _OPEN_FILE_WITH_SYMBOL_LAYERS = [ + (RagLayer.CODE_SOURCE_CHUNKS, 12), + (RagLayer.CODE_SYMBOL_CATALOG, 6), + ] + _EXPLAIN_LOCAL_LAYERS = [ + (RagLayer.CODE_SOURCE_CHUNKS, 12), + (RagLayer.CODE_SYMBOL_CATALOG, 8), + (RagLayer.CODE_DEPENDENCY_GRAPH, 4), + ] + + def __init__( + self, + layer_builder: LayerQueryBuilder | None = None, + filter_builder: RetrievalFilterBuilder | None = None, + ) -> None: + self._layer_builder = layer_builder or LayerQueryBuilder() + self._filter_builder = filter_builder or RetrievalFilterBuilder() + + def build( + self, + intent: str, + anchors: list[QueryAnchor], + repo_context: RepoContext, + *, + raw_query: str = "", + conversation_state: ConversationState | None = None, + conversation_mode: str = "START", + sub_intent: str = "EXPLAIN", + ) -> RetrievalSpec: + domains = self._domains(intent, repo_context) + layers_map = self._with_sub_intent_layers(intent, sub_intent, anchors) + layer_queries = self._layer_builder.build(intent, repo_context, domains=domains, layers_map=layers_map) + filters = self._filter_builder.build( + domains, + anchors, + repo_context, + raw_query=raw_query, + conversation_state=conversation_state, + conversation_mode=conversation_mode, + sub_intent=sub_intent, + ) + return RetrievalSpec( + domains=domains, + layer_queries=layer_queries, + filters=filters, + rerank_profile=self._RERANK[intent], + ) + + def _domains(self, intent: str, repo_context: RepoContext) -> list[str]: + available = set(repo_context.available_domains or ["CODE", "DOCS"]) + result = [domain for domain in self._DOMAINS[intent] if domain in available] + return result or list(self._DOMAINS[intent]) + + def _with_sub_intent_layers( + self, + intent: str, + sub_intent: str, + anchors: list[QueryAnchor], + ) -> dict[str, list[tuple[str, int]]]: + if intent != "CODE_QA": + return self._LAYERS + layers_map = dict(self._LAYERS) + if sub_intent == "OPEN_FILE": + has_symbol = any(anchor.type == "SYMBOL" and anchor.source == "user_text" for anchor in anchors) + layers_map["CODE_QA"] = list(self._OPEN_FILE_WITH_SYMBOL_LAYERS if has_symbol else self._OPEN_FILE_LAYERS) + elif sub_intent == "EXPLAIN_LOCAL": + layers_map["CODE_QA"] = list(self._EXPLAIN_LOCAL_LAYERS) + return layers_map diff --git a/app/modules/rag/intent_router_v2/router.py b/app/modules/rag/intent_router_v2/router.py new file mode 100644 index 0000000..85797d5 --- /dev/null +++ b/app/modules/rag/intent_router_v2/router.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from app.modules.rag.intent_router_v2.classifier import IntentClassifierV2 +from app.modules.rag.intent_router_v2.conversation_policy import ConversationPolicy +from app.modules.rag.intent_router_v2.evidence_policy_factory import EvidencePolicyFactory +from app.modules.rag.intent_router_v2.graph_id_resolver import GraphIdResolver +from app.modules.rag.intent_router_v2.logger import IntentRouterLogger +from app.modules.rag.intent_router_v2.models import ConversationState, IntentRouterResult, RepoContext +from app.modules.rag.intent_router_v2.query_plan_builder import QueryPlanBuilder +from app.modules.rag.intent_router_v2.retrieval_spec_factory import RetrievalSpecFactory + + +class IntentRouterV2: + def __init__( + self, + classifier: IntentClassifierV2 | None = None, + conversation_policy: ConversationPolicy | None = None, + query_plan_builder: QueryPlanBuilder | None = None, + retrieval_factory: RetrievalSpecFactory | None = None, + evidence_factory: EvidencePolicyFactory | None = None, + graph_resolver: GraphIdResolver | None = None, + logger: IntentRouterLogger | None = None, + ) -> None: + self._classifier = classifier or IntentClassifierV2() + self._conversation_policy = conversation_policy or ConversationPolicy() + self._query_plan_builder = query_plan_builder or QueryPlanBuilder() + self._retrieval_factory = retrieval_factory or RetrievalSpecFactory() + self._evidence_factory = evidence_factory or EvidencePolicyFactory() + self._graph_resolver = graph_resolver or GraphIdResolver() + self._logger = logger or IntentRouterLogger() + + def route( + self, + user_query: str, + conversation_state: ConversationState | None = None, + repo_context: RepoContext | None = None, + ) -> IntentRouterResult: + state = conversation_state or ConversationState() + context = repo_context or RepoContext() + self._logger.log_request(user_query, state, context) + decision = self._classifier.classify(user_query, state) + intent, conversation_mode = self._conversation_policy.resolve(decision, user_query, state) + query_plan = self._query_plan_builder.build( + user_query, + state, + continue_mode=conversation_mode == "CONTINUE", + conversation_mode=conversation_mode, + intent=intent, + ) + result = IntentRouterResult( + intent=intent, + graph_id=self._graph_resolver.resolve(intent), + conversation_mode=conversation_mode, + query_plan=query_plan, + retrieval_spec=self._retrieval_factory.build( + intent, + query_plan.anchors, + context, + raw_query=query_plan.raw, + conversation_state=state, + conversation_mode=conversation_mode, + sub_intent=query_plan.sub_intent, + ), + evidence_policy=self._evidence_factory.build( + intent, + sub_intent=query_plan.sub_intent, + negations=query_plan.negations, + has_user_anchor=any(anchor.source == "user_text" for anchor in query_plan.anchors), + ), + ) + self._logger.log_result(result) + return result diff --git a/app/modules/rag/intent_router_v2/sub_intent_detector.py b/app/modules/rag/intent_router_v2/sub_intent_detector.py new file mode 100644 index 0000000..c9ff705 --- /dev/null +++ b/app/modules/rag/intent_router_v2/sub_intent_detector.py @@ -0,0 +1,23 @@ +from __future__ import annotations + + +class SubIntentDetector: + _OPEN_VERBS = ("открой", "посмотри", "проверь", "уточни") + _EXPLAIN_MARKERS = ("объясни", "как работает", "почему", "что делает", "зачем", "логика", "флоу", "flow") + _TEST_MARKERS = ("тест", "pytest", "unit test", "юнит") + + def detect(self, raw: str, *, has_file_path: bool, negations: set[str]) -> str: + text = " ".join((raw or "").lower().split()) + if not text: + return "EXPLAIN" + if has_file_path and self._has_open_verb(text) and not self._has_explain_markers(text): + return "OPEN_FILE" + if "tests" not in negations and any(marker in text for marker in self._TEST_MARKERS): + return "FIND_TESTS" + return "EXPLAIN" + + def _has_open_verb(self, text: str) -> bool: + return any(text.startswith(verb) or f" {verb} " in f" {text} " for verb in self._OPEN_VERBS) + + def _has_explain_markers(self, text: str) -> bool: + return any(marker in text for marker in self._EXPLAIN_MARKERS) diff --git a/app/modules/rag/intent_router_v2/symbol_rules.py b/app/modules/rag/intent_router_v2/symbol_rules.py new file mode 100644 index 0000000..e754a49 --- /dev/null +++ b/app/modules/rag/intent_router_v2/symbol_rules.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +PY_KEYWORDS: set[str] = { + "and", + "as", + "assert", + "async", + "await", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "false", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "none", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "true", + "try", + "while", + "with", + "yield", +} + +COMMON_PATH_SEGMENTS: set[str] = { + "app", + "src", + "docs", + "tests", + "module", + "modules", + "core", + "pkg", + "lib", +} diff --git a/app/modules/rag/intent_router_v2/term_mapping.py b/app/modules/rag/intent_router_v2/term_mapping.py new file mode 100644 index 0000000..62aaab7 --- /dev/null +++ b/app/modules/rag/intent_router_v2/term_mapping.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import re + +from app.modules.rag.intent_router_v2.normalization_terms import KeyTermCanonicalizer + +_WORD_RE = re.compile(r"[A-Za-zА-Яа-яЁё-]+") + + +class RuEnTermMapper: + _CANONICAL_MAP = { + "класс": ["class"], + "метод": ["method"], + "функция": ["function", "def"], + "модуль": ["module"], + "пакет": ["package"], + "файл": ["file"], + "тест": ["test", "unit test"], + "документация": ["documentation", "docs"], + "readme": ["readme"], + } + _ENGLISH_SOURCES = { + "class": ["class"], + "method": ["method"], + "function": ["function", "def"], + "module": ["module"], + "package": ["package"], + "file": ["file"], + "test": ["test", "unit test"], + "tests": ["test", "unit test"], + "documentation": ["documentation", "docs"], + "docs": ["documentation", "docs"], + "readme": ["readme"], + "def": ["def"], + } + + def __init__(self, canonicalizer: KeyTermCanonicalizer | None = None) -> None: + self._canonicalizer = canonicalizer or KeyTermCanonicalizer() + + def expand(self, text: str) -> list[str]: + expansions: list[str] = [] + lowered = (text or "").lower() + for token in _WORD_RE.findall(lowered): + canonical = self._canonicalizer.canonicalize(token) or token + self._extend(expansions, self._CANONICAL_MAP.get(canonical, [])) + self._extend(expansions, self._ENGLISH_SOURCES.get(token, [])) + if "unit test" in lowered or "unit tests" in lowered: + self._extend(expansions, self._ENGLISH_SOURCES["test"]) + return expansions + + def key_terms(self) -> tuple[str, ...]: + return tuple(self._CANONICAL_MAP.keys()) + + def all_literal_terms(self) -> tuple[str, ...]: + values = set(self._canonicalizer.aliases()) + values.update(self._CANONICAL_MAP.keys()) + values.update(self._ENGLISH_SOURCES.keys()) + for targets in self._CANONICAL_MAP.values(): + values.update(target.lower() for target in targets) + for targets in self._ENGLISH_SOURCES.values(): + values.update(target.lower() for target in targets) + return tuple(sorted(values)) + + def _extend(self, result: list[str], values: list[str]) -> None: + for value in values: + if value not in result: + result.append(value) diff --git a/app/modules/rag/intent_router_v2/test_signals.py b/app/modules/rag/intent_router_v2/test_signals.py new file mode 100644 index 0000000..60c4a52 --- /dev/null +++ b/app/modules/rag/intent_router_v2/test_signals.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import re + +_NEGATIVE_TEST_RE = re.compile(r"\b(?:не|без|кроме)\b[^.?!]{0,28}\bтест", re.IGNORECASE) +_NEGATIVE_TEST_MARKERS = ("не про тест", "без тест", "кроме тест", "про прод код", "только прод", "production code") +_POSITIVE_TEST_MARKERS = ( + "тест", + "tests", + "pytest", + "unit test", + "unit tests", + "тестиру", +) +_TEST_TERMS = {"тест", "тесты", "test", "tests", "pytest", "unit", "unit test", "юнит-тест", "юниттест"} + + +def is_negative_test_request(text: str) -> bool: + lowered = (text or "").lower() + if _NEGATIVE_TEST_RE.search(lowered): + return True + return any(marker in lowered for marker in _NEGATIVE_TEST_MARKERS) + + +def has_test_focus(text: str) -> bool: + lowered = (text or "").lower() + if is_negative_test_request(lowered): + return False + return any(marker in lowered for marker in _POSITIVE_TEST_MARKERS) + + +def is_test_related_token(value: str) -> bool: + lowered = (value or "").lower().strip() + if not lowered: + return False + if lowered in _TEST_TERMS: + return True + if lowered.startswith("test"): + return True + return lowered.startswith("тест") diff --git a/app/modules/rag/persistence/__pycache__/cache_repository.cpython-312.pyc b/app/modules/rag/persistence/__pycache__/cache_repository.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35a64c2c700d824b48c7c81c44a3b4bc110d754b GIT binary patch literal 10281 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 literal 0 HcmV?d00001 diff --git a/app/modules/rag/persistence/__pycache__/query_repository.cpython-312.pyc b/app/modules/rag/persistence/__pycache__/query_repository.cpython-312.pyc index 8320bca6da3b6c34f229bde724456f2ccf7522e6..c590c356d18c549c8a885e0fcfd871d72ddb1bef 100644 GIT binary patch delta 2579 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 diff --git a/app/modules/rag/persistence/__pycache__/repository.cpython-312.pyc b/app/modules/rag/persistence/__pycache__/repository.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26cf73ca004e6a952ed4c2f4c40ba2c329c3009e GIT binary patch literal 6202 zcmcf_OKcm*b(Z9k%OxpN|DP4>W7(!7k+y5MN$RGR?b=OT%eCb;2orZfaaJ-_e$?4j zEir81gHc#$S|8m4Irz{5GTeiYLQl=DXfGxhL&U;H3ls&K8(kJKpi|$Q;gU;ARjTwu z0&m`Y-kW)!dGGD~F%(h>{C=|a>-1^|A%DS+{qqP!s@(JMW)J^@GzMMDGW}dCVXs=_DW5Q)cVB+okd~?k5*GKZV>fElb=xnXI07koE$sBBv zO|}g8;IM8OaIltfbx}7pJ-3+7=_w(QfL_r^wLbuIpXekZQIc@cJ}TYuB;3%tp!Lvx z>V{UP1Jnbpmkv@HS|1&vUT76MOnuP$=>e)htI`qbhc-Z80WEqGL3)q|pbgPc8ick3 z_=cbj(+(P@5%_n~u8(Cu+3QcoX!jj&BEl(pK6dR~OUG$1tms^eL?^5eXriftl;n^a zP3k5@s%>(i&BolL`l2Gy8e^txQ#xjWTKr7EjX7o;6lQ3bS)V;-cG{SOw!zQr-DNgl zkFAl1(rh%y@CI2vÐEaN6=~hHwCb;Z~NcE#>D8hL*Nejn>`NV0cQ)tEJFU8Ey-m zAEKW-XBG_9!RU50xx5_D=IKgCH{vY07+={m(t^`;o-66HA~U7bS`ab zTA{B+`!RbQt~oK@1aO-?>pxcRpStV$DEMq}x;z*Mtm9elWV!bUVCu7>czNgqVBu$d zN6UT301MdI&jK~zKz29jYcO-46v+}UY5OfT%+{Q+RWFe>!XhvpbuqMy71DFY3ez<+ zIS+!fei#!9VS|9O5dekZRw-L3`jDUtz->}dg7<>!!7XLHq>MjR4zXbvPB{;Q;f{Oc z89?@P{GwEJ*&)g)0&Z+|tSlQkGi`r}y4e691|!;ITAEtwGRrUNDHCu54>dYEDr7te zox*`u8CzMRDh!lV2-&gTv87Cwl*tFV$4gI@86jvrg2E~;q!4t!b}CAb>Q{2EVJ9JE z-P<*N6`ay|=;A`MQP|ADoo!(z0B;wjmGl6Tpq)XOaQD3r*FW3}PnE(`8{aR7&pc7i z{6C}#V4SWWiyguPIb}GEG7PpF#6f`eY8N0{3P(3$5V*vL%yp#L& zyQ2FMx(ocAp4Sb1OHDC7Y3dkxq8{OO!E}!5t2hFF#?51S4M?FYhJhynr>lG2QRo#0 zT7_(~8;4l~-B@^>x~^_1BPC_z7nzOO%?nSJb33hsLPBJeYC=fYzlsh{KLrx}6{Q6! zO^U8xNk5l%3c0dOZ6U?gVo{*6yg#^=WQ#`YU7CWKcBz{5lQ3&wR6wal?t1Py79b1_PO=`8<&aO&4=EEMyuZxHfGSSdXA zV5%IBKT+c1{B@6kwS-_w;^*H4 z&{!!nRt`;6iOWCqz^nu#_pYp8*$PgVg3}xH@$B!;e;RxXM=z~k`sngY8CJc-r7sX{ z8*o*~FJm(i_a_l~X3&>%Y8W-kE%sYLzJSF%`>>b?Ee0{BzCYN^^S`heI{~5yd-Xx3 zlUZw6OH18M<`yCMe+gDZh7w5Z;!P=1qg0LI$S+7#L|VkkupESD;%s)M~!n zw>XZ}Zvpr`8=fo$Pd+}i`O5DPJbCB6PlNA&jv3=Ko$gOM<(VPh?&ZcCu;qTkzI`d0 zc^E5Is|UjXFD7Y2gN9vl_^?jIT0@!7KmsssCR?K;o})0{s;S-haV=xfAyV`Q>mO`| z}G@^2H()K^VQ&ZHrTTHZ`@4ZcOJ5UN^RX8J;URX3~% zVmi}u`5Y{Vny}&f$PB|i4`CQGzlb{&<)XLf`;GLdo@VAiOS9~ozzuYQwXmfy-DGL~ zM|xCo1dXr|K+n$Wl%{iw3=hCABKQ^py#DNM0EPm9hml4k`8JHSe^c3;peGr4F>aG; zABptb6rQLAhbqD0O8;1;tH07cSQ$D}8JVtzN#szCC=sR};j8rqsBbt+HNQ8Z85Ld@7mIQhBO7gKisP`+d3s%nB2&_i0z2 zbFk3BY7EpBX{hUXDSRB&C3H1(r0OS;6Ez~EmgI((EHAKJVL8h)o6qS{ugKqS!(?m> zII?R9zJuT@07&wqh&M(p&wM_g*)d!Q&~(ZiEsk=g3NIoBQIq^w3nQC-^xqgQ*w0J( zTlN;>++qt%UfZ1+_;h*?lxII;-Ec^uIHbUkRPgiFAoLB9`mWy<}*z>O)K=y#v(%VnGt=u7(in zAf02?Fk%rhaJ1TqSQi;SSVbPy9-{WtdJ#KC`uc(Ui%_V}8d_it41I{H$S{Cd5ZT}= zV6FE2BMg{O0*XEx?LplytiT-wJ20&FtwE@`V)f^%7)vh3pw2c~GG!VJBa=1NKJR$m ziiwPjSx-FUibu2fUL^Jd8_$(>Dh#96vAwA9v)Hi>rWS^2VDICJjN+j4Bgyn$8;O@f z=Uyt(#P*~S?}<(#{7A5RokUR6F`dTfJpU5nuGr?I+sYYXhc_7oa;#c-Ag;P4NqSDk j|4L3gCzqa+tA8TbICA+px$>M$z4XpWlKOXokQ@7-n7cLZ literal 0 HcmV?d00001 diff --git a/app/modules/rag/persistence/__pycache__/retrieval_statement_builder.cpython-312.pyc b/app/modules/rag/persistence/__pycache__/retrieval_statement_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7969b20a29c26106191f3d3180883e922b17df3 GIT binary patch literal 10109 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 L|4flDqTv4k8l|`? literal 0 HcmV?d00001 diff --git a/app/modules/rag/persistence/__pycache__/schema_repository.cpython-312.pyc b/app/modules/rag/persistence/__pycache__/schema_repository.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bef89d31dbb0c3c850680b059a1c3255ad0b560 GIT binary patch literal 10493 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 KZxFc}WB&~(Kc8d( literal 0 HcmV?d00001 diff --git a/app/modules/rag/persistence/__pycache__/session_repository.cpython-312.pyc b/app/modules/rag/persistence/__pycache__/session_repository.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..342f5603d284dfd49bce8c13914ea193aefd2c18 GIT binary patch literal 2547 zcmd5;&2Jk;6rb5IJ9g~aZPGY_!j=TJ8=_bR!67P0R^md{)JFLz)IwUh-ihO2uicqV z6GyJfA(tqX3a3zkL=POO@*i;PnM0*Ugn) z4BQ%n^$sE^hYgey4B5b&Qcg4!Lx5Q_0)_~)T$i+#5qy?6MhkI-9vLdvSW<#Y;4G zCaW%fJd;{TtMloFjw{vo)%2&+vx_t7naOq{J@e26TZ9yBk!K)NUVS@IsG5Km%MOrr zQdqSJ<$$%7LbXyUS(;=^jFgw`X!kSQq4rg_#6vLd#;f?eoLpptvShwY){`^jhVGd% z{l?qLd`UHE=>{>?e67Zxq5xG*@`EBVtt4zFD^+8)Ojr_bOx7S!iCM%fkfc|CogU(G zvR1dFt2MaI^54$J=v)urPIen{=&(f)U!W(Eq3zFpj+}lNIsGUy+C<^Lk$cwT{@9(_ zt=T8Bfu+BdhS0zz>~rAn_;OQ(PSnL0i$8-i z8lum_`6C=^TixogYV+#7`SffWuzXIvFqggDO`=aeO3$S`w}3qWWlj;w-;bs^_@q1$ z+Ruf9%it2R3d>cK*wVFXwR~TqQGRxei#P~dq}8=UEYjnAE9xVMtOJ(VOCZv1Gz5-} zfH(>fsc#q{a{T1Z#8(plk-fpok7Aj}@snRi_hM&SvUnnTprB(zTg%%k%>atT4n*+S z524t=m($zzyD9fj5HL>od`4?^ATr?ch$7ip>Hxv-Z7!a|{5$Kx zWkc=RZ#pbqhgj@$5WSEz){Ug4N3mjWBz>2-oA_qr5Km*zLgo;5uQ;>hks znAb&ZsE&_f2X)=dSBS3LAzgQSH|YJketk7xc2DRD9&MP5Qy}i67D(h2lk(5KBZ8nX zPT@_|68hzl)~kv None: + self._builder = RetrievalStatementBuilder() + def retrieve( self, rag_session_id: str, @@ -18,89 +21,47 @@ class RagQueryRepository: limit: int = 5, layers: list[str] | None = None, path_prefixes: list[str] | None = None, + exclude_path_prefixes: list[str] | None = None, + exclude_like_patterns: list[str] | None = None, prefer_non_tests: bool = False, ) -> list[dict]: - emb = "[" + ",".join(str(x) for x in query_embedding) + "]" - filters = ["rag_session_id = :sid"] - params: dict = {"sid": rag_session_id, "emb": emb, "lim": limit} - if layers: - filters.append("layer = ANY(:layers)") - params["layers"] = layers - if path_prefixes: - or_filters = [] - for idx, prefix in enumerate(path_prefixes): - key = f"path_{idx}" - params[key] = f"{prefix}%" - or_filters.append(f"path LIKE :{key}") - filters.append("(" + " OR ".join(or_filters) + ")") - term_filters = [] - terms = extract_query_terms(query_text) - for idx, term in enumerate(terms): - exact_key = f"term_exact_{idx}" - prefix_key = f"term_prefix_{idx}" - contains_key = f"term_contains_{idx}" - params[exact_key] = term - params[prefix_key] = f"{term}%" - params[contains_key] = f"%{term}%" - term_filters.append( - "CASE " - f"WHEN lower(COALESCE(qname, '')) = :{exact_key} THEN 0 " - f"WHEN lower(COALESCE(symbol_id, '')) = :{exact_key} THEN 1 " - f"WHEN lower(COALESCE(title, '')) = :{exact_key} THEN 2 " - f"WHEN lower(COALESCE(qname, '')) LIKE :{prefix_key} THEN 3 " - f"WHEN lower(COALESCE(title, '')) LIKE :{prefix_key} THEN 4 " - f"WHEN lower(COALESCE(path, '')) LIKE :{contains_key} THEN 5 " - f"WHEN lower(COALESCE(content, '')) LIKE :{contains_key} THEN 6 " - "ELSE 100 END" - ) - lexical_sql = "LEAST(" + ", ".join(term_filters) + ")" if term_filters else "100" - test_penalty_sql = ( - "CASE " - "WHEN lower(path) LIKE 'tests/%' OR lower(path) LIKE '%/tests/%' OR lower(path) LIKE 'test_%' OR lower(path) LIKE '%/test_%' " - "THEN 1 ELSE 0 END" - if prefer_non_tests - else "0" + sql, params = self._builder.build_retrieve( + rag_session_id, + query_embedding, + query_text=query_text, + limit=limit, + layers=layers, + path_prefixes=path_prefixes, + exclude_path_prefixes=exclude_path_prefixes, + exclude_like_patterns=exclude_like_patterns, + prefer_non_tests=prefer_non_tests, ) - layer_rank_sql = ( - "CASE " - "WHEN layer = 'C3_ENTRYPOINTS' THEN 0 " - "WHEN layer = 'C1_SYMBOL_CATALOG' THEN 1 " - "WHEN layer = 'C2_DEPENDENCY_GRAPH' THEN 2 " - "WHEN layer = 'C0_SOURCE_CHUNKS' THEN 3 " - "WHEN layer = 'D1_MODULE_CATALOG' THEN 0 " - "WHEN layer = 'D2_FACT_INDEX' THEN 1 " - "WHEN layer = 'D3_SECTION_INDEX' THEN 2 " - "WHEN layer = 'D4_POLICY_INDEX' THEN 3 " - "ELSE 10 END" - ) - sql = f""" - SELECT path, content, layer, title, metadata_json, span_start, span_end, - {lexical_sql} AS lexical_rank, - {test_penalty_sql} AS test_penalty, - {layer_rank_sql} AS layer_rank, - (embedding <=> CAST(:emb AS vector)) AS distance - FROM rag_chunks - WHERE {' AND '.join(filters)} - ORDER BY lexical_rank ASC, test_penalty ASC, layer_rank ASC, embedding <=> CAST(:emb AS vector) - LIMIT :lim - """ with get_engine().connect() as conn: rows = conn.execute(text(sql), params).mappings().fetchall() return [self._row_to_dict(row) for row in rows] - def fallback_chunks(self, rag_session_id: str, *, limit: int = 5, layers: list[str] | None = None) -> list[dict]: - filters = ["rag_session_id = :sid"] - params: dict = {"sid": rag_session_id, "lim": limit} - if layers: - filters.append("layer = ANY(:layers)") - params["layers"] = layers - sql = f""" - SELECT path, content, layer, title, metadata_json, span_start, span_end - FROM rag_chunks - WHERE {' AND '.join(filters)} - ORDER BY id DESC - LIMIT :lim - """ + def retrieve_lexical_code( + self, + rag_session_id: str, + *, + query_text: str, + limit: int = 5, + path_prefixes: list[str] | None = None, + exclude_path_prefixes: list[str] | None = None, + exclude_like_patterns: list[str] | None = None, + prefer_non_tests: bool = False, + ) -> list[dict]: + sql, params = self._builder.build_lexical_code( + rag_session_id, + query_text=query_text, + limit=limit, + path_prefixes=path_prefixes, + exclude_path_prefixes=exclude_path_prefixes, + exclude_like_patterns=exclude_like_patterns, + prefer_non_tests=prefer_non_tests, + ) + if sql is None: + return [] with get_engine().connect() as conn: rows = conn.execute(text(sql), params).mappings().fetchall() return [self._row_to_dict(row) for row in rows] diff --git a/app/modules/rag/persistence/repository.py b/app/modules/rag/persistence/repository.py index a8418f5..54baefa 100644 --- a/app/modules/rag/persistence/repository.py +++ b/app/modules/rag/persistence/repository.py @@ -67,6 +67,9 @@ class RagRepository: query_text: str = "", limit: int = 5, layers: list[str] | None = None, + path_prefixes: list[str] | None = None, + exclude_path_prefixes: list[str] | None = None, + exclude_like_patterns: list[str] | None = None, prefer_non_tests: bool = False, ) -> list[dict]: return self._query.retrieve( @@ -75,8 +78,29 @@ class RagRepository: query_text=query_text, limit=limit, layers=layers, + path_prefixes=path_prefixes, + exclude_path_prefixes=exclude_path_prefixes, + exclude_like_patterns=exclude_like_patterns, prefer_non_tests=prefer_non_tests, ) - def fallback_chunks(self, rag_session_id: str, limit: int = 5, layers: list[str] | None = None) -> list[dict]: - return self._query.fallback_chunks(rag_session_id, limit=limit, layers=layers) + def retrieve_lexical_code( + self, + rag_session_id: str, + query_text: str, + *, + limit: int = 5, + path_prefixes: list[str] | None = None, + exclude_path_prefixes: list[str] | None = None, + exclude_like_patterns: list[str] | None = None, + prefer_non_tests: bool = False, + ) -> list[dict]: + return self._query.retrieve_lexical_code( + rag_session_id, + query_text=query_text, + limit=limit, + path_prefixes=path_prefixes, + exclude_path_prefixes=exclude_path_prefixes, + exclude_like_patterns=exclude_like_patterns, + prefer_non_tests=prefer_non_tests, + ) diff --git a/app/modules/rag/persistence/retrieval_statement_builder.py b/app/modules/rag/persistence/retrieval_statement_builder.py new file mode 100644 index 0000000..a9972bf --- /dev/null +++ b/app/modules/rag/persistence/retrieval_statement_builder.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +from app.modules.rag.retrieval.query_terms import extract_query_terms + +_LIKE_ESCAPE_SQL = " ESCAPE E'\\\\'" + + +class RetrievalStatementBuilder: + def build_retrieve( + self, + rag_session_id: str, + query_embedding: list[float], + *, + query_text: str = "", + limit: int = 5, + layers: list[str] | None = None, + path_prefixes: list[str] | None = None, + exclude_path_prefixes: list[str] | None = None, + exclude_like_patterns: list[str] | None = None, + prefer_non_tests: bool = False, + ) -> tuple[str, dict]: + emb = "[" + ",".join(str(x) for x in query_embedding) + "]" + filters = ["rag_session_id = :sid"] + params: dict = {"sid": rag_session_id, "emb": emb, "lim": limit} + self._append_prefix_group(filters, params, "path", path_prefixes) + self._append_prefix_group(filters, params, "exclude_prefix", exclude_path_prefixes, negate=True) + self._append_like_group(filters, params, "exclude_like", exclude_like_patterns, negate=True) + if layers: + filters.append("layer = ANY(:layers)") + params["layers"] = layers + lexical_sql = self._lexical_rank_sql(query_text, params) + test_penalty_sql = self._test_penalty_sql( + prefer_non_tests, + params, + base_key="penalty", + path_prefixes=exclude_path_prefixes, + like_patterns=exclude_like_patterns, + ) + layer_rank_sql = ( + "CASE " + "WHEN layer = 'C3_ENTRYPOINTS' THEN 0 " + "WHEN layer = 'C1_SYMBOL_CATALOG' THEN 1 " + "WHEN layer = 'C2_DEPENDENCY_GRAPH' THEN 2 " + "WHEN layer = 'C0_SOURCE_CHUNKS' THEN 3 " + "WHEN layer = 'D1_MODULE_CATALOG' THEN 0 " + "WHEN layer = 'D2_FACT_INDEX' THEN 1 " + "WHEN layer = 'D3_SECTION_INDEX' THEN 2 " + "WHEN layer = 'D4_POLICY_INDEX' THEN 3 " + "ELSE 10 END" + ) + sql = f""" + SELECT path, content, layer, title, metadata_json, span_start, span_end, + {lexical_sql} AS lexical_rank, + {test_penalty_sql} AS test_penalty, + {layer_rank_sql} AS layer_rank, + (embedding <=> CAST(:emb AS vector)) AS distance + FROM rag_chunks + WHERE {' AND '.join(filters)} + ORDER BY lexical_rank ASC, test_penalty ASC, layer_rank ASC, embedding <=> CAST(:emb AS vector) + LIMIT :lim + """ + return sql, params + + def build_lexical_code( + self, + rag_session_id: str, + *, + query_text: str, + limit: int = 5, + path_prefixes: list[str] | None = None, + exclude_path_prefixes: list[str] | None = None, + exclude_like_patterns: list[str] | None = None, + prefer_non_tests: bool = False, + ) -> tuple[str | None, dict]: + terms = extract_query_terms(query_text) + if not terms: + return None, {} + filters = ["rag_session_id = :sid", "layer = 'C0_SOURCE_CHUNKS'"] + params: dict = {"sid": rag_session_id, "lim": limit} + self._append_prefix_group(filters, params, "path", path_prefixes) + self._append_prefix_group(filters, params, "exclude_prefix", exclude_path_prefixes, negate=True) + self._append_like_group(filters, params, "exclude_like", exclude_like_patterns, negate=True) + lexical_filters: list[str] = [] + lexical_ranks: list[str] = [] + for idx, term in enumerate(terms): + exact_key = f"lex_exact_{idx}" + prefix_key = f"lex_prefix_{idx}" + contains_key = f"lex_contains_{idx}" + params[exact_key] = term + params[prefix_key] = f"{term}%" + params[contains_key] = f"%{term}%" + lexical_filters.append( + f"(lower(COALESCE(qname, '')) = :{exact_key} " + f"OR lower(COALESCE(title, '')) = :{exact_key} " + f"OR lower(COALESCE(path, '')) LIKE :{contains_key} " + f"OR lower(COALESCE(title, '')) LIKE :{prefix_key} " + f"OR lower(COALESCE(content, '')) LIKE :{contains_key})" + ) + lexical_ranks.append( + "CASE " + f"WHEN lower(COALESCE(qname, '')) = :{exact_key} THEN 0 " + f"WHEN lower(COALESCE(title, '')) = :{exact_key} THEN 1 " + f"WHEN lower(COALESCE(title, '')) LIKE :{prefix_key} THEN 2 " + f"WHEN lower(COALESCE(path, '')) LIKE :{contains_key} THEN 3 " + f"WHEN lower(COALESCE(content, '')) LIKE :{contains_key} THEN 4 " + "ELSE 100 END" + ) + filters.append("(" + " OR ".join(lexical_filters) + ")") + lexical_sql = "LEAST(" + ", ".join(lexical_ranks) + ")" + test_penalty_sql = self._test_penalty_sql( + prefer_non_tests, + params, + base_key="lex_penalty", + path_prefixes=exclude_path_prefixes, + like_patterns=exclude_like_patterns, + ) + sql = f""" + SELECT path, content, layer, title, metadata_json, span_start, span_end, + {lexical_sql} AS lexical_rank, + {test_penalty_sql} AS test_penalty + FROM rag_chunks + WHERE {' AND '.join(filters)} + ORDER BY lexical_rank ASC, test_penalty ASC, path ASC, span_start ASC + LIMIT :lim + """ + return sql, params + + def _lexical_rank_sql(self, query_text: str, params: dict) -> str: + term_filters: list[str] = [] + for idx, term in enumerate(extract_query_terms(query_text)): + exact_key = f"term_exact_{idx}" + prefix_key = f"term_prefix_{idx}" + contains_key = f"term_contains_{idx}" + params[exact_key] = term + params[prefix_key] = f"{term}%" + params[contains_key] = f"%{term}%" + term_filters.append( + "CASE " + f"WHEN lower(COALESCE(qname, '')) = :{exact_key} THEN 0 " + f"WHEN lower(COALESCE(symbol_id, '')) = :{exact_key} THEN 1 " + f"WHEN lower(COALESCE(title, '')) = :{exact_key} THEN 2 " + f"WHEN lower(COALESCE(qname, '')) LIKE :{prefix_key} THEN 3 " + f"WHEN lower(COALESCE(title, '')) LIKE :{prefix_key} THEN 4 " + f"WHEN lower(COALESCE(path, '')) LIKE :{contains_key} THEN 5 " + f"WHEN lower(COALESCE(content, '')) LIKE :{contains_key} THEN 6 " + "ELSE 100 END" + ) + return "LEAST(" + ", ".join(term_filters) + ")" if term_filters else "100" + + def _append_prefix_group(self, filters: list[str], params: dict, base_key: str, prefixes: list[str] | None, *, negate: bool = False) -> None: + if not prefixes: + return + items: list[str] = [] + for idx, prefix in enumerate(prefixes): + key = f"{base_key}_{idx}" + params[key] = self._escape_like_value(prefix) + "%" + items.append(f"path LIKE :{key}{_LIKE_ESCAPE_SQL}") + self._append_group(filters, items, negate=negate) + + def _append_like_group(self, filters: list[str], params: dict, base_key: str, patterns: list[str] | None, *, negate: bool = False) -> None: + if not patterns: + return + items: list[str] = [] + for idx, pattern in enumerate(patterns): + key = f"{base_key}_{idx}" + params[key] = pattern + items.append(f"lower(path) LIKE :{key}{_LIKE_ESCAPE_SQL}") + self._append_group(filters, items, negate=negate) + + def _append_group(self, filters: list[str], parts: list[str], *, negate: bool) -> None: + if not parts: + return + joined = " OR ".join(parts) + filters.append(f"NOT ({joined})" if negate else f"({joined})") + + def _test_penalty_sql( + self, + enabled: bool, + params: dict, + *, + base_key: str, + path_prefixes: list[str] | None, + like_patterns: list[str] | None, + ) -> str: + if not enabled: + return "0" + parts: list[str] = [] + for idx, prefix in enumerate(path_prefixes or []): + key = f"{base_key}_prefix_{idx}" + params[key] = self._escape_like_value(prefix) + "%" + parts.append(f"lower(path) LIKE :{key}{_LIKE_ESCAPE_SQL}") + for idx, pattern in enumerate(like_patterns or []): + key = f"{base_key}_like_{idx}" + params[key] = pattern + parts.append(f"lower(path) LIKE :{key}{_LIKE_ESCAPE_SQL}") + if not parts: + return "0" + return "CASE WHEN " + " OR ".join(parts) + " THEN 1 ELSE 0 END" + + def _escape_like_value(self, value: str) -> str: + return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") diff --git a/app/modules/rag/persistence/schema_repository.py b/app/modules/rag/persistence/schema_repository.py index 5648165..db03ae7 100644 --- a/app/modules/rag/persistence/schema_repository.py +++ b/app/modules/rag/persistence/schema_repository.py @@ -106,6 +106,7 @@ class RagSchemaRepository: ) self._ensure_columns(conn) self._ensure_indexes(conn) + self._drop_unused_rag_chunk_columns(conn) conn.commit() def _ensure_columns(self, conn) -> None: @@ -118,14 +119,12 @@ class RagSchemaRepository: "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS system_component TEXT NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS last_modified TIMESTAMPTZ NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS staleness_score DOUBLE PRECISION NULL", - "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS rag_doc_id VARCHAR(128) NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS layer VARCHAR(64) NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS lang VARCHAR(32) NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS repo_id VARCHAR(512) NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS commit_sha VARCHAR(128) NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS title TEXT NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS metadata_json TEXT NULL", - "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS links_json TEXT NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS span_start INTEGER NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS span_end INTEGER NULL", "ALTER TABLE rag_chunks ADD COLUMN IF NOT EXISTS symbol_id TEXT NULL", @@ -162,6 +161,13 @@ class RagSchemaRepository: ): conn.execute(text(statement)) + def _drop_unused_rag_chunk_columns(self, conn) -> None: + for statement in ( + "ALTER TABLE rag_chunks DROP COLUMN IF EXISTS rag_doc_id", + "ALTER TABLE rag_chunks DROP COLUMN IF EXISTS links_json", + ): + conn.execute(text(statement)) + def _ensure_indexes(self, conn) -> None: for statement in ( "CREATE INDEX IF NOT EXISTS idx_rag_chunks_session ON rag_chunks (rag_session_id)", diff --git a/app/modules/rag/retrieval/__pycache__/test_filter.cpython-312-pytest-9.0.2.pyc b/app/modules/rag/retrieval/__pycache__/test_filter.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0e2f50b41ef740f4a89f4d891ff60749c841be6 GIT binary patch literal 4854 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 str: - lowered = query.lower() - return RetrievalMode.CODE if any(hint in lowered for hint in self._CODE_HINTS) else RetrievalMode.DOCS - - def layers_for_mode(self, mode: str) -> list[str]: - return list(self._CODE_LAYERS if mode == RetrievalMode.CODE else self._DOCS_LAYERS) diff --git a/app/modules/rag/retrieval/test_filter.py b/app/modules/rag/retrieval/test_filter.py new file mode 100644 index 0000000..2e6118a --- /dev/null +++ b/app/modules/rag/retrieval/test_filter.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import os +import re +from dataclasses import dataclass +from fnmatch import fnmatch +from typing import Iterable + +DEFAULT_TEST_PATH_PATTERNS = ( + "tests/", + "test/", + "__tests__/", + "mocks/", + "fixtures/", + "stubs/", + "conftest.py", + "*_test.*", + "*.test.*", + "*.spec.*", +) + +_TRUE_VALUES = {"1", "true", "yes", "on"} +_SAFE_PATTERN_RE = re.compile(r"^[A-Za-z0-9_./*?-]+$") + + +@dataclass(slots=True) +class RetrievalPathFilter: + exclude_path_prefixes: list[str] + exclude_like_patterns: list[str] + + +def exclude_tests_default() -> bool: + return os.getenv("RAG_EXCLUDE_TESTS_DEFAULT", "true").strip().lower() in _TRUE_VALUES + + +def debug_disable_test_filter() -> bool: + return os.getenv("RAG_DEBUG_DISABLE_TEST_FILTER", "false").strip().lower() in _TRUE_VALUES + + +def configured_test_patterns() -> list[str]: + raw = os.getenv("RAG_TEST_PATH_PATTERNS", "") + if not raw.strip(): + return list(DEFAULT_TEST_PATH_PATTERNS) + return [item.strip() for item in raw.split(",") if item.strip()] + + +def build_test_filters(patterns: Iterable[str] | None = None) -> RetrievalPathFilter: + prefixes: list[str] = [] + like_patterns: list[str] = [] + for pattern in _validated_patterns(patterns or configured_test_patterns()): + if pattern.endswith("/"): + _append(prefixes, pattern) + _append(like_patterns, f"%/{pattern}%") + continue + sql_like = _glob_to_sql_like(pattern) + _append(like_patterns, sql_like) + if "/" not in pattern: + _append(like_patterns, f"%/{sql_like}") + return RetrievalPathFilter(exclude_path_prefixes=prefixes, exclude_like_patterns=like_patterns) + + +def is_test_path(path: str, patterns: Iterable[str] | None = None) -> bool: + normalized = (path or "").strip().lower() + if not normalized: + return False + for pattern in _validated_patterns(patterns or configured_test_patterns()): + if pattern.endswith("/"): + token = pattern.rstrip("/") + if normalized.startswith(pattern) or f"/{token}/" in normalized: + return True + continue + if fnmatch(normalized, pattern) or fnmatch(normalized, f"*/{pattern}"): + return True + return False + + +def _validated_patterns(patterns: Iterable[str]) -> list[str]: + result: list[str] = [] + for raw_pattern in patterns: + pattern = (raw_pattern or "").strip().lower() + if not pattern: + continue + if not _SAFE_PATTERN_RE.fullmatch(pattern): + continue + if pattern not in result: + result.append(pattern) + return result + + +def _glob_to_sql_like(pattern: str) -> str: + escaped = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + return escaped.replace("*", "%").replace("?", "_") + + +def _append(values: list[str], item: str) -> None: + if item and item not in values: + values.append(item) diff --git a/app/modules/rag/services/__pycache__/rag_service.cpython-312.pyc b/app/modules/rag/services/__pycache__/rag_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..423cdedb1e7faff2ba6e98a7de00cc39b6d91c91 GIT binary patch literal 11750 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 aTO+5V8$PBm{d9=Y)4emV{Dy*zO#FXKCG;!+ literal 0 HcmV?d00001 diff --git a/app/modules/rag/services/rag_service.py b/app/modules/rag/services/rag_service.py index ae2c996..3d9b31f 100644 --- a/app/modules/rag/services/rag_service.py +++ b/app/modules/rag/services/rag_service.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import hashlib +import logging import os from collections.abc import Awaitable, Callable from inspect import isawaitable @@ -11,9 +12,10 @@ from app.modules.rag.indexing.code.pipeline import CodeIndexingPipeline from app.modules.rag.indexing.common.report import IndexReport from app.modules.rag.indexing.docs.pipeline import DocsIndexingPipeline from app.modules.rag.persistence.repository import RagRepository -from app.modules.rag.retrieval.query_router import RagQueryRouter from app.modules.rag_session.embedding.gigachat_embedder import GigaChatEmbedder +LOGGER = logging.getLogger(__name__) + class RagService: def __init__( @@ -26,7 +28,6 @@ class RagService: self._repo = repository self._docs = DocsIndexingPipeline() self._code = CodeIndexingPipeline() - self._queries = RagQueryRouter() async def index_snapshot( self, @@ -55,36 +56,6 @@ class RagService: self._repo.apply_document_changes(rag_session_id, delete_paths, report.documents_list) return report.as_tuple() - async def retrieve(self, rag_session_id: str, query: str) -> list[dict]: - mode = self._queries.resolve_mode(query) - layers = self._queries.layers_for_mode(mode) - prefer_non_tests = mode == "code" and "test" not in query.lower() and "тест" not in query.lower() - try: - query_embedding = self._embedder.embed([query])[0] - rows = self._repo.retrieve( - rag_session_id, - query_embedding, - query_text=query, - limit=8, - layers=layers, - prefer_non_tests=prefer_non_tests, - ) - except Exception: - rows = self._repo.fallback_chunks(rag_session_id, limit=8, layers=layers) - if not rows and mode != "docs": - rows = self._repo.fallback_chunks(rag_session_id, limit=8, layers=self._queries.layers_for_mode("docs")) - return [ - { - "source": row["path"], - "content": row["content"], - "layer": row.get("layer"), - "title": row.get("title"), - "metadata": row.get("metadata", {}), - "score": row.get("distance"), - } - for row in rows - ] - async def _index_files( self, rag_session_id: str, @@ -99,15 +70,28 @@ class RagService: try: blob_sha = self._blob_sha(file) cached = await asyncio.to_thread(self._repo.get_cached_documents, repo_id, blob_sha) + pipelines = self._resolve_pipeline_names(path) if cached: report.documents_list.extend(self._with_file_metadata(cached, file, repo_id, blob_sha)) report.cache_hit_files += 1 + LOGGER.warning( + "rag ingest file: rag_session_id=%s path=%s processing=cache pipeline=%s", + rag_session_id, + path, + ",".join(pipelines), + ) else: built = self._build_documents(repo_id, path, file) embedded = await asyncio.to_thread(self._embed_documents, built, file, repo_id, blob_sha) report.documents_list.extend(embedded) await asyncio.to_thread(self._repo.cache_documents, repo_id, path, blob_sha, embedded) report.cache_miss_files += 1 + LOGGER.warning( + "rag ingest file: rag_session_id=%s path=%s processing=embed pipeline=%s", + rag_session_id, + path, + ",".join(pipelines), + ) report.indexed_files += 1 except Exception as exc: report.failed_files += 1 @@ -128,6 +112,16 @@ class RagService: docs.extend(self._docs.index_file(repo_id=repo_id, commit_sha=commit_sha, path=path, content=content)) return docs + def _resolve_pipeline_names(self, path: str) -> list[str]: + names: list[str] = [] + if self._docs.supports(path): + names.append("DOCS") + if self._code.supports(path): + names.append("CODE") + if not names: + names.append("DOCS") + return names + def _embed_documents(self, docs: list[RagDocument], file: dict, repo_id: str, blob_sha: str) -> list[RagDocument]: if not docs: return [] @@ -190,7 +184,6 @@ class RagService: if isawaitable(result): await result - class _PipelineReport(IndexReport): def __init__(self) -> None: super().__init__() diff --git a/app/modules/rag_session/module.py b/app/modules/rag_session/module.py index b44f953..63ea414 100644 --- a/app/modules/rag_session/module.py +++ b/app/modules/rag_session/module.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from fastapi.responses import StreamingResponse +from fastapi.responses import JSONResponse, StreamingResponse from app.core.exceptions import AppError from app.modules.rag_session.embedding.gigachat_embedder import GigaChatEmbedder @@ -37,6 +37,7 @@ class RagModule: token_provider = GigaChatTokenProvider(settings) client = GigaChatClient(settings, token_provider) embedder = GigaChatEmbedder(client) + self.embedder = embedder self.rag = RagService(embedder=embedder, repository=repository, chunker=TextChunker()) self.sessions = RagSessionStore(repository) self.jobs = IndexJobStore(repository) @@ -252,12 +253,13 @@ class RagModule: } @router.post("/retrieve") - async def retrieve(payload: dict) -> dict: - rag_session_id = payload.get("rag_session_id") or payload.get("project_id", "") - ctx = await self.rag.retrieve( - rag_session_id=rag_session_id, - query=payload.get("query", ""), + async def retrieve() -> JSONResponse: + return JSONResponse( + status_code=410, + content={ + "error": "deprecated", + "message": "POST /internal/rag/retrieve is deprecated.", + }, ) - return {"items": ctx} return router diff --git a/app/modules/shared/__pycache__/env_loader.cpython-312.pyc b/app/modules/shared/__pycache__/env_loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50fbdf91cc28ba4c0693f2381bb84eea7a2f7bd6 GIT binary patch literal 2163 zcmaJ?T}%{L6ux(7X8&OUm-45w5gXmox`^2LfDl`1W7JhkZ7gagBXfaShMmp5!vfnu zNNbZNArX@%KxzZ_r4ZY+ee<#JeG%7&?F5r1KJljF6EE$#vkahVxXGMz?z!ijd+xp8 zIrC>(SrEZEKeD7vhzLFB6L$zD0dIT+!Yopeigh%Edobp*NB8u2fD5`XB=(4)6MH1p zuLe}q-%o7i<^MG+|hm>^MFcniX(kuyc@v>qL#63Cj7)fVy zK~9h9Mz5mx;!+9@m4HG@c;g^wXOX$RW}#8cqIj1k<=N%8B|dcdmS2j;duzBhe{@>9&0ullwHC*yXZrD5?w(QJQ;|OopV@xYrIaQ=z7; zYSb2QDU8@&ZV3Scpd!7dLC5R>k0Ga(A!3Uecv|L#Ok&R1yq>tqAcKUWlhH&M$;ugO zq`;5K+17-j#Z;9OO?QoR7eoNM9{_3`uzNw=V|i#bz-k-hJF!&3-G_;)XnC@j z;^}6v<@fm3P(@XGxK(a9fR76t?kNEK+e6(hls)r;5}{R=WQJY;RJkkd@GOFiOmn*= zEOD=x?rHXpX-Vy{!SsTN#!%ds%t^y$-v_6lJ^4wx*HE=||0Zs-@#%H`HaQ;|%^1{- z#RGPL5~vlEjGA_kWpvHdwKQS2#7wGXK*bA&jcDcosQ9uSrew&-k_;t%+NkX@nC%0k zYSc*E6>{33LyE5Dh@4gQVG{S!DC`H7tBlAzuq`@N7$i_=TSf9=ak$bi;B zl5Z3Thq}}EFN5%A-*+EY#e+BCpfwi!mg>L?xWM_~QP756tttX+B$!F6P>;I7svE_< z$#sri%E|h_Fgm!ji4Se!BXmEA002QJX<(9b(~LiVtaQEaf=D?z1RpyE1R9Jlm_PYV zZsy9@6LTjQE9Xxw4ljN7VC+dl%Wqd79sm8>AN4CI&le-nnJY6i|J7rEeKAxqdBHh0 z<$W?PQvL_XRRVlsR*e4|fh7%ic`qgAFiq!zy2i}weB~xsb9gwtp0tOu+F9eM7Gj!-V3Oqx#|CBXN248!FxN4(5fIDB;3b@jV@gHl6>ZAYw literal 0 HcmV?d00001 diff --git a/app/modules/shared/env_loader.py b/app/modules/shared/env_loader.py new file mode 100644 index 0000000..6f04104 --- /dev/null +++ b/app/modules/shared/env_loader.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import os +from pathlib import Path + +_ENV_FILES = (".env", ".env.local") + + +def load_workspace_env(start_dir: str | Path | None = None) -> list[Path]: + base = Path(start_dir or Path.cwd()).resolve() + loaded: list[Path] = [] + for directory in reversed((base, *base.parents)): + for file_name in _ENV_FILES: + path = directory / file_name + if not path.is_file(): + continue + _load_env_file(path) + loaded.append(path) + return loaded + + +def _load_env_file(path: Path) -> None: + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, raw_value = line.split("=", 1) + name = key.removeprefix("export ").strip() + if not name or name in os.environ: + continue + os.environ[name] = _normalize_value(raw_value.strip()) + + +def _normalize_value(value: str) -> str: + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + return value[1:-1] + return value diff --git a/app/modules/shared/gigachat/__pycache__/client.cpython-312.pyc b/app/modules/shared/gigachat/__pycache__/client.cpython-312.pyc index 6a0c283a5b08c2999be31de1ff31c38d7a477733..3de65346d6ed4f7affbcb32d469f7ea634689dbc 100644 GIT binary patch literal 4754 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 Ya`GE;>KpRRKV4@S=GYGesd)we1(ddL(f|Me literal 4173 zcmc&%O>7&-6`uX$E=5YDNzt;bv|9hjHtm{}x}NJNU^QhtpgY+dhm_03^Y)C=zF_d zO0s3gEl_j_zL|OR=FOY8^WHb}hxYb10_FD~U)L2MA%DY;Tf}B#@fbA9L?arPCc{|s zX)ePL^BlH?w2%>pMY}DgrHp6T!x5ewCz|vD(L6VKN6WB0$wlRd*f|yDtoGyjX!7W# zq&dV`jsYX+GR{!b)U%@o@FFhnZ0<76{($A)(>2OcjzWienw`b-&?pm1hB=K4^BPA5 zji;g}XyS+mED?t#%|kt!41a-ofTsXY*1S}qo^F^6Tm^n!Y~z1K)Y?FzZBmT-tPaU#n&T{cDkXppx1bBKzo-H!lLA*D31~T`z(bF7dy^!} zor?07H_=)`PS|T0QNa=onjT>a2s1Ab1C^?(y407<=i`~2HlC(N+_;ovREv*-gHzz( zc*?dpmY=kgL_*K%W+E}wwR%G_cfi&f1g$?M_3lm6;zu3zuD)sUWB(%Xh}CXP8Ya!) zb~Aa?@{JpmH9M;|EQ7GU;a|blVZ&q$r$14~R=TJ;cC%R*1(@TWzZLVw-0!`Dr*Q>N z#O>H+%s%CA lWf`7FX_QnEF^EByZ^P_zZr~C`PAZl_! z_?&#fYl`LtYucWnAHDlT`Hkev2VHD^?i0 zrw@l=6E{RlL|IF|M3a#33`;ik49$(3mghan^pVM_t#0_Lsn)V8qgTeMVX7lZJx#TP z>cdarfKF{(OO#^LRhRgKD(vx6PES$8A`H`%B{%oRx}X;gvUX$!N2!@era;Vak`cow zVfei*rMa$QiTRviT5>Y4C&pPCQlVioJ#V!cMmpgbZUs*}Mh;D+Xdb=7FbP;b!%Ujv zMj{37wM3Inm~1oh1z6V-Kuxrbb>NElu~`_0Bs(cuay~hk&LuT8!^ng4sU^^flqG6O zGs$+K_)Z`O#y@1rW?>l6meO<{eJUNg1GstPJh&0kVX(98FZmaOyQ{(7wP1W&dJyR@ zzgv2DArh@dq7S+^&1I^=!FqS^+{tS2KtmF`JHC=U;kIeH(c$<0kh|a2TP~K0^>A1D z_0sDL;b=7+T?qGA!~M6yweY@5Xn$q@@k;QGdgP@C+hcR%bEB2M{q?Q;zXDC6hC;e~ z>RmnMYo%)oUH#Rr{>SUQ8#<=nXlx|@&IMm@)z>?_ciy+{eyF#ydGPi-3$MLheeLba z;OWZgvz2of7S3I)p1W8%o2sZ26j9m9BvxT`R*O#4y~wf)k5E^oH<|dpZ{k=z`-R0v()bnZ#)(y zccbByljcr6@JS~wyW#5HlG?I|4btt|=G_+jLmaYTGBj(7^5HM4uW zn~RI`3M7zC)&11&z0vTk$m1REEWj`M82Lm=@uMWgUx*RFA#;;E*WA)-ZFO4(WlcK* zu0|Kn+;v@kiT|iygf)uF3*wHn!d`DdT7^53jXTmcX&HA?fTF1VDkNLD_&GUM(zE$- zlOaG)`QkKlk!l*=iP=lQVTjA@+elO-xa}2)uq^!)BCu^Jx{L@bL>)w6+fm^`l-hnS zODFO)Wl~LTLCA^;t#s>cF{5D{E!Zwpf&gI!T4MyeF+tnG}iyjaG+b-5Be z@_$J9fMrTanOR>8s?*Z_K=(}FFFXGju)#xpvlcp0@t>$4d-KnM6SuE?CVqD2dSsUS zwe$;V_Hs>qd0stm>tZc3@TcIx`M`;aa^ma9k^dy?|LajF37+H(Jixz&+&@6>3VVmP zh<69VL#lAk9~kNp?)6B}hKCotShC3sO(d+g1Rf!H;6U4-NL(3DrkxRn&jg0|XND<} z?L%@1$q^*SkQ@hcgRphjM(AhwK(PtlBaI6{J|GRAm$p?l?P(CKkG|v77m|zEZz#Re zp4n7`Ky_QNt2@FX_8LdH9_b`E^UAF7^MMAz_FU(jc5B~_%hl+?$2P-R85fNzb_`gX z;bs_XzSv^Ui!CmQ!O9Cwm?MC{5|@Nd@a!e0OY diff --git a/app/modules/shared/gigachat/client.py b/app/modules/shared/gigachat/client.py index 2b00def..5096bbe 100644 --- a/app/modules/shared/gigachat/client.py +++ b/app/modules/shared/gigachat/client.py @@ -1,5 +1,8 @@ +import time + import requests +from app.core.constants import MAX_RETRIES from app.modules.shared.gigachat.errors import GigaChatError from app.modules.shared.gigachat.settings import GigaChatSettings from app.modules.shared.gigachat.token_provider import GigaChatTokenProvider @@ -19,23 +22,7 @@ class GigaChatClient: {"role": "user", "content": user_prompt}, ], } - try: - response = requests.post( - f"{self._settings.api_url.rstrip('/')}/chat/completions", - json=payload, - headers={ - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - }, - timeout=90, - verify=self._settings.ssl_verify, - ) - except requests.RequestException as exc: - raise GigaChatError(f"GigaChat completion request failed: {exc}") from exc - - if response.status_code >= 400: - raise GigaChatError(f"GigaChat completion error {response.status_code}: {response.text}") - + response = self._post_with_retry("/chat/completions", payload, token=token, timeout=90, operation_name="completion") data = response.json() choices = data.get("choices") or [] if not choices: @@ -49,25 +36,49 @@ class GigaChatClient: "model": self._settings.embedding_model, "input": texts, } - try: - response = requests.post( - f"{self._settings.api_url.rstrip('/')}/embeddings", - json=payload, - headers={ - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - }, - timeout=90, - verify=self._settings.ssl_verify, - ) - except requests.RequestException as exc: - raise GigaChatError(f"GigaChat embeddings request failed: {exc}") from exc - - if response.status_code >= 400: - raise GigaChatError(f"GigaChat embeddings error {response.status_code}: {response.text}") - + response = self._post_with_retry("/embeddings", payload, token=token, timeout=90, operation_name="embeddings") data = response.json() items = data.get("data") if not isinstance(items, list): raise GigaChatError("Unexpected GigaChat embeddings response") return [list(map(float, x.get("embedding") or [])) for x in items] + + def _post_with_retry( + self, + path: str, + payload: dict, + *, + token: str, + timeout: int, + operation_name: str, + ): + last_error: Exception | None = None + for attempt in range(1, MAX_RETRIES + 1): + try: + response = requests.post( + f"{self._settings.api_url.rstrip('/')}{path}", + json=payload, + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + }, + timeout=timeout, + verify=self._settings.ssl_verify, + ) + except requests.RequestException as exc: + last_error = GigaChatError(f"GigaChat {operation_name} request failed: {exc}") + else: + if response.status_code < 400: + return response + last_error = GigaChatError(f"GigaChat {operation_name} error {response.status_code}: {response.text}") + if not self._is_retryable_status(response.status_code): + raise last_error + if attempt == MAX_RETRIES: + break + time.sleep(0.1 * attempt) + if last_error is None: + raise GigaChatError(f"GigaChat {operation_name} failed without response") + raise last_error + + def _is_retryable_status(self, status_code: int) -> bool: + return status_code == 429 or status_code >= 500 diff --git a/app/schemas/__pycache__/rag_sessions.cpython-312.pyc b/app/schemas/__pycache__/rag_sessions.cpython-312.pyc index 5ad9da43028fe613744e3eaf70ea4c4a02d7b234..e2c24527903525ab4cad4653e68494688343e9ec 100644 GIT binary patch delta 20 acmcb?bAyNbG%qg~0}xykShA7Zmkj_t5CvBN delta 20 acmcb?bAyNbG%qg~0}x!ixNsx4FB query_embedding ASC" + ], + "notes": [ + "lexical_rank is derived from qname/symbol_id/title/path/content matching extracted query terms", + "test_penalty is applied only when prefer_non_tests=true", + "layer priority is C3 > C1 > C2 > C0 for code retrieval" + ] + } +} diff --git a/docs/architecture/llm_inventory.md b/docs/architecture/llm_inventory.md new file mode 100644 index 0000000..1156e69 --- /dev/null +++ b/docs/architecture/llm_inventory.md @@ -0,0 +1,270 @@ +# LLM Inventory + +## Provider and SDK + +- Provider in code: GigaChat / Sber +- Local SDK style: custom thin HTTP client over `requests` +- Core files: + - `app/modules/shared/gigachat/client.py` + - `app/modules/shared/gigachat/settings.py` + - `app/modules/shared/gigachat/token_provider.py` + - `app/modules/agent/llm/service.py` + +There is no OpenAI SDK, Azure SDK, or local model runtime in the current implementation. + +## Configuration + +Model and endpoint configuration are read from environment in `GigaChatSettings.from_env()`: + +- `GIGACHAT_AUTH_URL` + - default: `https://ngw.devices.sberbank.ru:9443/api/v2/oauth` +- `GIGACHAT_API_URL` + - default: `https://gigachat.devices.sberbank.ru/api/v1` +- `GIGACHAT_SCOPE` + - default: `GIGACHAT_API_PERS` +- `GIGACHAT_TOKEN` + - required for auth +- `GIGACHAT_SSL_VERIFY` + - default: `true` +- `GIGACHAT_MODEL` + - default: `GigaChat` +- `GIGACHAT_EMBEDDING_MODEL` + - default: `Embeddings` +- `AGENT_PROMPTS_DIR` + - optional prompt directory override + +PostgreSQL config for retrieval storage is separate: + +- `DATABASE_URL` + - default: `postgresql+psycopg://agent:agent@db:5432/agent` + +## Default models + +- Chat/completions model default: `GigaChat` +- Embedding model default: `Embeddings` + +## Completion payload + +Observed payload sent by `GigaChatClient.complete(...)`: + +```json +{ + "model": "GigaChat", + "messages": [ + {"role": "system", "content": ""}, + {"role": "user", "content": ""} + ] +} +``` + +Endpoint: + +- `POST {GIGACHAT_API_URL}/chat/completions` + +Observed response handling: + +- reads `choices[0].message.content` +- if no choices: returns empty string + +## Embeddings payload + +Observed payload sent by `GigaChatClient.embed(...)`: + +```json +{ + "model": "Embeddings", + "input": [ + "", + "" + ] +} +``` + +Endpoint: + +- `POST {GIGACHAT_API_URL}/embeddings` + +Observed response handling: + +- expects `data` list +- maps each `item.embedding` to `list[float]` + +## Parameters + +### Explicitly implemented + +- `model` +- `messages` +- `input` +- HTTP timeout: + - completions: `90s` + - embeddings: `90s` + - auth: `30s` +- TLS verification flag: + - `verify=settings.ssl_verify` + +### Not implemented in payload + +- `temperature` +- `top_p` +- `max_tokens` +- `response_format` +- tools/function calling +- streaming +- seed +- stop sequences + +`ASSUMPTION:` the service uses provider defaults for sampling and output length because these fields are not sent in the request payload. + +## Context and budget limits + +There is no centralized token budget manager in the current code. + +Observed practical limits instead: + +- prompt file text is loaded as-is from disk +- user input is passed as-is +- RAG context shaping happens outside the LLM client +- docs indexing summary truncation: + - docs module catalog summary: `4000` chars + - docs policy text: `4000` chars +- project QA source bundle caps: + - top `12` rag items + - top `10` file candidates +- logging truncation only: + - LLM input/output logs capped at `1500` chars for logs + +`ASSUMPTION:` there is no explicit max-context enforcement before chat completion requests. The current system relies on upstream graph logic to keep inputs small enough. + +## Retry, backoff, timeout + +### Timeouts + +- auth: `30s` +- chat completion: `90s` +- embeddings: `90s` + +### Retry + +- Generic async retry wrapper exists in `app/modules/shared/retry_executor.py` +- It retries only: + - `TimeoutError` + - `ConnectionError` + - `OSError` +- Retry constants: + - `MAX_RETRIES = 5` + - backoff: `0.1 * attempt` seconds + +### Important current limitation + +- `GigaChatClient` raises `GigaChatError` on HTTP and request failures. +- `RetryExecutor` does not catch `GigaChatError`. +- Result: LLM and embeddings calls are effectively not retried by this generic retry helper unless errors are converted upstream. + +## Prompt formation + +Prompt loading is handled by `PromptLoader`: + +- base dir: `app/modules/agent/prompts` +- override: `AGENT_PROMPTS_DIR` +- file naming convention: `.txt` + +Prompt composition model today: + +- system prompt: + - full contents of selected prompt file +- user prompt: + - raw runtime input string passed by the caller +- no separate developer prompt layer in the application payload + +If a prompt file is missing: + +- fallback system prompt: `You are a helpful assistant.` + +## Prompt templates present + +- `router_intent` +- `general_answer` +- `project_answer` +- `docs_detect` +- `docs_strategy` +- `docs_plan_sections` +- `docs_generation` +- `docs_self_check` +- `docs_execution_summary` +- `project_edits_plan` +- `project_edits_hunks` +- `project_edits_self_check` + +## Key LLM call entrypoints + +### Composition roots + +- `app/modules/agent/module.py` + - builds `GigaChatSettings` + - builds `GigaChatTokenProvider` + - builds `GigaChatClient` + - builds `PromptLoader` + - builds `AgentLlmService` +- `app/modules/rag_session/module.py` + - builds the same provider stack for embeddings used by RAG + +### Main abstraction + +- `AgentLlmService.generate(prompt_name, user_input, log_context=None)` + +### Current generate callsites + +- `app/modules/agent/engine/router/intent_classifier.py` + - `router_intent` +- `app/modules/agent/engine/graphs/base_graph.py` + - `general_answer` +- `app/modules/agent/engine/graphs/project_qa_graph.py` + - `project_answer` +- `app/modules/agent/engine/graphs/docs_graph_logic.py` + - `docs_detect` + - `docs_strategy` + - `docs_plan_sections` + - `docs_generation` + - `docs_self_check` + - `docs_execution_summary`-like usage via summary step +- `app/modules/agent/engine/graphs/project_edits_logic.py` + - `project_edits_plan` + - `project_edits_self_check` + - `project_edits_hunks` + +## Logging and observability + +`AgentLlmService` logs: + +- input: + - `graph llm input: context=... prompt=... user_input=...` +- output: + - `graph llm output: context=... prompt=... output=...` + +Log truncation: + +- 1500 chars + +RAG retrieval logs separately in `RagService`, but without embedding vectors. + +## Integration with retrieval + +There are two distinct GigaChat usages: + +1. Chat/completion path for agent reasoning and generation +2. Embedding path for RAG indexing and retrieval + +The embedding adapter is `GigaChatEmbedder`, used by: + +- `app/modules/rag/services/rag_service.py` + +## Notable limitations + +- Single provider coupling: chat and embeddings both depend on GigaChat-specific endpoints. +- No model routing by scenario. +- No tool/function calling. +- No centralized prompt token budgeting. +- No explicit retry for `GigaChatError`. +- No streaming completions. +- No structured response mode beyond prompt conventions and downstream parsing. diff --git a/docs/architecture/rag_chunks_column_audit.md b/docs/architecture/rag_chunks_column_audit.md new file mode 100644 index 0000000..68aaf26 --- /dev/null +++ b/docs/architecture/rag_chunks_column_audit.md @@ -0,0 +1,13 @@ +| column | used_by | safe_to_drop | notes | +| --- | --- | --- | --- | +| `layer` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | Core selector for C0-C3 and D1-D4 queries. | +| `title` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | Used in lexical ranking and prompt evidence labels. | +| `metadata_json` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | C2/C0 graph lookups and docs metadata depend on it. | +| `span_start`, `span_end` | `USED_BY_CODE_V2` | no | Needed for symbol-to-chunk resolution and locations. | +| `symbol_id`, `qname`, `kind`, `lang` | `USED_BY_CODE_V2` | no | Used by code indexing, ranking, trace building, and diagnostics. | +| `repo_id`, `commit_sha` | `USED_BY_CODE_V2`, `USED_BY_DOCS_INDEXING` | no | Used by indexing/cache and retained for provenance. | +| `entrypoint_type`, `framework` | `USED_BY_CODE_V2` | no | Used by C3 filtering and entrypoint diagnostics. | +| `doc_kind`, `module_id`, `section_path` | `USED_BY_DOCS_INDEXING` | no | Still written by docs indexing and covered by docs tests. | +| `artifact_type`, `section`, `doc_version`, `owner`, `system_component`, `last_modified`, `staleness_score` | `USED_BY_DOCS_INDEXING` | no | File metadata still flows through indexing/cache; left intact for now. | +| `rag_doc_id` | `UNUSED` | yes | Written into `rag_chunks` only; no reads in runtime/indexing code. | +| `links_json` | `UNUSED` | yes | Stored in `rag_chunks` only; reads exist for `rag_chunk_cache`, not `rag_chunks`. | diff --git a/docs/architecture/retrieval_callgraph.mmd b/docs/architecture/retrieval_callgraph.mmd new file mode 100644 index 0000000..95e3795 --- /dev/null +++ b/docs/architecture/retrieval_callgraph.mmd @@ -0,0 +1,31 @@ +flowchart TD + A["HTTP: POST /internal/rag/retrieve"] --> B["RagModule.internal_router.retrieve(payload)"] + B --> C["RagService.retrieve(rag_session_id, query)"] + C --> D["RagQueryRouter.resolve_mode(query)"] + D --> E["RagQueryRouter.layers_for_mode(mode)"] + C --> F["GigaChatEmbedder.embed([query])"] + F --> G["GigaChatClient.embed(payload)"] + G --> H["POST /embeddings"] + C --> I["RagRepository.retrieve(...)"] + I --> J["RagQueryRepository.retrieve(...)"] + J --> K["PostgreSQL rag_chunks + pgvector"] + K --> L["ORDER BY lexical_rank, test_penalty, layer_rank, vector distance"] + L --> M["rows: path/content/layer/title/metadata/span/distance"] + M --> N["normalize to {source, content, layer, title, metadata, score}"] + N --> O["response: {items: [...]}"] + + C --> P["embedding error?"] + P -->|yes| Q["RagRepository.fallback_chunks(...)"] + Q --> R["latest rows by id DESC"] + R --> N + + C --> S["no rows and mode != docs?"] + S -->|yes| T["fallback to docs layers"] + T --> I + + U["GraphAgentRuntime for project/qa"] --> V["ProjectQaRetrievalGraphFactory._retrieve_context"] + V --> C + V --> W["ProjectQaSupport.build_source_bundle(...)"] + W --> X["source_bundle"] + X --> Y["context_analysis"] + Y --> Z["answer_composition"] diff --git a/docs/architecture/retrieval_inventory.md b/docs/architecture/retrieval_inventory.md new file mode 100644 index 0000000..ed1a709 --- /dev/null +++ b/docs/architecture/retrieval_inventory.md @@ -0,0 +1,457 @@ +# Retrieval Inventory + +## Scope and method + +This document describes the retrieval and indexing pipeline as implemented in code today. The inventory is based primarily on: + +- `app/modules/rag/services/rag_service.py` +- `app/modules/rag/persistence/*.py` +- `app/modules/rag/indexing/code/**/*.py` +- `app/modules/rag/indexing/docs/**/*.py` +- `app/modules/rag_session/module.py` +- `app/modules/agent/engine/graphs/project_qa_step_graphs.py` +- `app/modules/agent/engine/orchestrator/*.py` + +`ASSUMPTION:` the intended layer semantics are the ones implied by code and tests, not by future architecture plans. This matters because only `C0` through `C3` are materially implemented today; `C4+` exist only as enum constants. + +## Current retrieval pipeline + +1. Retrieval entrypoint is `POST /internal/rag/retrieve` in `app/modules/rag_session/module.py`. +2. The endpoint calls `RagService.retrieve(rag_session_id, query)`. +3. `RagQueryRouter` chooses `docs` or `code` mode from the raw query text. +4. `RagService` computes a single embedding for the full query via `GigaChatEmbedder`. +5. `RagQueryRepository.retrieve(...)` runs one SQL query against `rag_chunks` in PostgreSQL with `pgvector`. +6. Ranking order is: + - lexical rank + - test-file penalty + - layer rank + - vector distance `embedding <=> query_embedding` +7. Response items are normalized to `{source, content, layer, title, metadata, score}`. +8. If embeddings fail, retrieval falls back to latest chunks from the same layers. +9. If code retrieval returns nothing, service falls back to docs layers. + +## Storage and indices + +- Primary store: PostgreSQL from `DATABASE_URL`, configured in `app/modules/shared/db.py`. +- Vector extension: `CREATE EXTENSION IF NOT EXISTS vector` in `app/modules/rag/persistence/schema_repository.py`. +- Primary table: `rag_chunks`. +- Cache tables: + - `rag_blob_cache` + - `rag_chunk_cache` + - `rag_session_chunk_map` +- SQL indexes currently created: + - `(rag_session_id)` + - `(rag_session_id, layer)` + - `(rag_session_id, layer, path)` + - `(qname)` + - `(symbol_id)` + - `(module_id)` + - `(doc_kind)` + - `(entrypoint_type, framework)` + +`ASSUMPTION:` there is no explicit ANN index for the vector column in schema code. The code creates general SQL indexes, but no `ivfflat`/`hnsw` index is defined here. + +## Layer: C0_SOURCE_CHUNKS + +### Implementation + +- Produced by `CodeIndexingPipeline.index_file(...)` in `app/modules/rag/indexing/code/pipeline.py`. +- Chunking logic: `CodeTextChunker.chunk(...)` in `app/modules/rag/indexing/code/code_text/chunker.py`. +- Document builder: `CodeTextDocumentBuilder.build(...)` in `app/modules/rag/indexing/code/code_text/document_builder.py`. +- Persisted via `RagDocumentRepository.insert_documents(...)` into `rag_chunks`. + +### Input contract + +This is an indexing layer, not a direct public retriever. The observed upstream indexing input is a file dict with at least: + +- required: + - `path: str` + - `content: str` +- optional: + - `commit_sha: str | None` + - `content_hash: str` + - metadata fields copied through by `RagService._document_metadata(...)` + +For retrieval, the layer is queried only indirectly through: + +- `rag_session_id: str` +- `query: str` +- inferred mode/layers from `RagQueryRouter` +- fixed `limit=8` + +### Output contract + +Stored document shape: + +- top-level: + - `layer = "C0_SOURCE_CHUNKS"` + - `lang = "python"` + - `source.repo_id` + - `source.commit_sha` + - `source.path` + - `title` + - `text` + - `span.start_line` + - `span.end_line` + - `embedding` +- metadata: + - `chunk_index` + - `chunk_type`: `symbol_block` or `window` + - `module_or_unit` + - `artifact_type = "CODE"` + - plus file-level metadata injected by `RagService` + +Returned retrieval item shape: + +- `source` +- `content` +- `layer` +- `title` +- `metadata` +- `score` + +No `line_start` / `line_end` are returned to the caller directly; they remain in DB columns `span_start` / `span_end` and are only used in logs. + +### Defaults & limits + +- AST chunking prefers one chunk per top-level class/function/async function. +- Fallback window chunking: + - `size = 80` lines + - `overlap = 15` lines +- Global retrieval limit from `RagService.retrieve(...)`: `8` +- Embedding batch size from env: + - `RAG_EMBED_BATCH_SIZE` + - default `16` + +### Known issues + +- Nested methods/functions are not emitted as C0 chunks unless represented inside a selected top-level block. +- Returned API payload omits line spans even though storage has them. +- No direct filter by path, namespace, symbol, or `top_k` is exposed through the current endpoint. + +## Layer: C1_SYMBOL_CATALOG + +### Implementation + +- Symbol extraction: `SymbolExtractor.extract(...)` in `app/modules/rag/indexing/code/symbols/extractor.py`. +- AST parsing: `PythonAstParser.parse_module(...)`. +- Document builder: `SymbolDocumentBuilder.build(...)`. +- Retrieval reads rows from `rag_chunks`; there is no dedicated symbol table. + +### Input contract + +Indexing input is the same per-file payload as C0. + +Observed symbol extraction source: + +- Python AST only +- supported symbol kinds: + - `class` + - `function` + - `method` + - `const` for top-level imports/import aliases + +Retrieval input is still the generic text query endpoint. Query terms are enriched by `extract_query_terms(...)`: + +- extracts identifier-like tokens from query text +- normalizes camelCase/PascalCase to snake_case +- adds special intent terms for management/control-related queries +- max observed query terms: `6` + +### Output contract + +Stored document shape: + +- top-level: + - `layer = "C1_SYMBOL_CATALOG"` + - `title = qname` + - `text = " \n\n"` + - `span.start_line` + - `span.end_line` +- metadata: + - `symbol_id` + - `qname` + - `kind` + - `signature` + - `decorators_or_annotations` + - `docstring_or_javadoc` + - `parent_symbol_id` + - `package_or_module` + - `is_entry_candidate` + - `lang_payload` + - `artifact_type = "CODE"` + +Observed `lang_payload` variants: + +- class: + - `bases` +- function/method: + - `async` +- import alias: + - `imported_from` + - `import_alias` + +### Defaults & limits + +- Only Python source files are indexed into C-layers. +- Import and import-from declarations are materialized as `const` symbols only at module top level. +- Retrieval ranking gives C1 priority rank `1`, after C3 and before C2/C0. + +### Known issues + +- No explicit visibility/public-private model. +- `parent_symbol_id` currently stores the parent qname string from the stack, not the parent symbol hash. This is an observed implementation detail. +- Cross-file symbol resolution is not implemented; `dst_symbol_id` in edges resolves only against symbols extracted from the same file. + +## Layer: C2_DEPENDENCY_GRAPH + +### Implementation + +- Edge extraction: `EdgeExtractor.extract(...)` in `app/modules/rag/indexing/code/edges/extractor.py`. +- Document builder: `EdgeDocumentBuilder.build(...)`. +- Built during `CodeIndexingPipeline.index_file(...)`. + +### Input contract + +Indexing input is the same per-file source payload as C0/C1. + +Graph construction method: + +- static analysis only +- Python AST walk only +- no runtime tracing +- no tree-sitter + +Observed edge types: + +- `calls` +- `imports` +- `inherits` + +### Output contract + +Stored document shape: + +- top-level: + - `layer = "C2_DEPENDENCY_GRAPH"` + - `title = ":"` + - `text = " "` + - `span.start_line` + - `span.end_line` + - `links` contains one evidence link of type `EDGE` +- metadata: + - `edge_id` + - `edge_type` + - `src_symbol_id` + - `src_qname` + - `dst_symbol_id` + - `dst_ref` + - `resolution`: `resolved` or `partial` + - `lang_payload` + - `artifact_type = "CODE"` + +Observed `lang_payload` usage: + +- for calls: may include `callsite_kind = "function_call"` + +### Defaults & limits + +- Edge extraction is per-file only. +- `imports` edges are emitted only while visiting a class/function scope; top-level imports do not become C2 edges. +- Layer rank in retrieval SQL: `2` + +### Known issues + +- There is no traversal API, graph repository, or query language over C2. Retrieval only treats edges as text/vector rows in `rag_chunks`. +- Destination resolution is local to the file-level qname map. +- Top-level module import relationships are incompletely represented because `visit_Import` / `visit_ImportFrom` skip when there is no current scope. + +## Layer: C3_ENTRYPOINTS + +### Implementation + +- Detection registry: `EntrypointDetectorRegistry.detect_all(...)`. +- Detectors: + - `FastApiEntrypointDetector` + - `FlaskEntrypointDetector` + - `TyperClickEntrypointDetector` +- Document builder: `EntrypointDocumentBuilder.build(...)`. + +### Input contract + +Indexing input is the same per-file source payload as other C-layers. + +Detected entrypoint families today: + +- HTTP: + - FastAPI decorators such as `.get`, `.post`, `.put`, `.patch`, `.delete`, `.route` + - Flask `.route` +- CLI: + - Typer/Click `.command` + - Typer/Click `.callback` + +Not detected: + +- Django routes +- Celery tasks +- RQ jobs +- cron jobs / scheduler entries + +### Output contract + +Stored document shape: + +- top-level: + - `layer = "C3_ENTRYPOINTS"` + - `title = route_or_command` + - `text = " "` + - `span.start_line` + - `span.end_line` + - `links` contains one evidence link of type `CODE_SPAN` +- metadata: + - `entry_id` + - `entry_type`: observed `http` or `cli` + - `framework`: observed `fastapi`, `flask`, `typer`, `click` + - `route_or_command` + - `handler_symbol_id` + - `lang_payload` + - `artifact_type = "CODE"` + +FastAPI-specific observed payload: + +- `lang_payload.methods = [HTTP_METHOD]` for `.get/.post/...` + +### Defaults & limits + +- Retrieval layer rank: `0` highest among code layers. +- Entrypoint mapping is handler-symbol centric: + - decorator match -> symbol -> `handler_symbol_id` + - physical location comes from symbol span + +### Known issues + +- Route parsing is string-based from decorator text, not semantic AST argument parsing. +- No dedicated entrypoint tags beyond `entry_type`, `framework`, and raw decorator-derived payload. +- Background jobs and non-decorator entrypoints are not indexed. + +## Dependency graph / trace current state + +### Exists or stub? + +- C2 exists and is populated. +- It is not a stub. +- It is also not a full-project dependency graph service; it is a set of per-edge documents stored in `rag_chunks`. + +### How the graph is built + +- static Python AST analysis +- no runtime instrumentation +- no import graph resolver across modules +- no tree-sitter + +### Edge types in data + +- `calls` +- `imports` +- `inherits` + +### Traversal API + +- No traversal API was found in `app/modules/rag/*` or `app/modules/agent/*`. +- No method accepts graph traversal parameters such as depth, start node, edge filters, or BFS/DFS strategy. +- Current access path is only retrieval over indexed edge documents. + +## Entrypoints current state + +### Implemented extraction + +- HTTP routes: + - FastAPI + - Flask +- CLI: + - Typer + - Click + +### Mapping model + +- `entrypoint -> handler_symbol_id -> symbol span/path` +- The entrypoint record itself stores: + - framework + - entry type + - raw route/command string + - handler symbol id + +### Tags/types + +- `entry_type` is the main normalized tag. +- Observed values: `http`, `cli`. +- `framework` is the second discriminator. +- There are no richer endpoint taxonomies such as `job`, `worker`, `webhook`, `scheduler`. + +## Defaults and operational limits + +- Query mode default: `docs` +- Code mode is enabled by keyword heuristics in `RagQueryRouter` +- Retrieval hard limit: `8` +- Fallback limit: `8` +- Query term extraction limit: `6` +- Ranked source bundle for project QA: + - top `12` RAG items + - top `10` file candidates +- No exposed `namespace`, `path_prefixes`, `top_k`, `max_chars`, `max_chunks`, `max_depth` in the public/internal retrieval endpoint + +`ASSUMPTION:` the absence of these controls in endpoint and service signatures means they are not part of the current supported contract, even though `RagQueryRepository.retrieve(...)` has an internal `path_prefixes` parameter. + +## Known cross-cutting issues + +- Retrieval contract is effectively text-only at API level; structured retrieval exists only as internal SQL parameters. +- Response payload drops explicit line spans even though spans are stored. +- Vector retrieval is coupled to a single provider-specific embedder. +- Docs mode is the default, so code retrieval depends on heuristic query phrasing unless the project/qa graph prepends `по коду`. +- There is no separate retrieval contract per layer exposed over API; all layer selection is implicit. + +## Where to plug ExplainPack pipeline + +### Option 1: replace or extend `project_qa/context_analysis` + +- Code location: + - `app/modules/agent/engine/graphs/project_qa_step_graphs.py` +- Why: + - retrieval is already complete at this step + - input bundle already contains ranked `rag_items` and `file_candidates` + - output is already a structured `analysis_brief` +- Risk: + - low + - minimal invasion if ExplainPack consumes `source_bundle` and emits the same `analysis_brief` shape + +### Option 2: insert a new orchestrator step between `context_retrieval` and `context_analysis` + +- Code location: + - `app/modules/agent/engine/orchestrator/template_registry.py` + - `app/modules/agent/engine/orchestrator/step_registry.py` +- Why: + - preserves current retrieval behavior + - makes ExplainPack an explicit pipeline stage with its own artifact + - cleanest for observability and future A/B migration +- Risk: + - low to medium + - requires one new artifact contract and one extra orchestration step, but no change to retrieval storage + +### Option 3: introduce ExplainPack inside `ExplainActions.extract_logic` + +- Code location: + - `app/modules/agent/engine/orchestrator/actions/explain_actions.py` +- Why: + - useful if ExplainPack is meant only for explain-style scenarios + - keeps general project QA untouched +- Risk: + - medium + - narrower integration point; may create duplicate reasoning logic separate from project QA analysis path + +## Bottom line + +- C0-C3 are implemented and persisted in one physical store: `rag_chunks`. +- Retrieval is a hybrid SQL ranking over lexical heuristics plus pgvector distance. +- C2 exists, but only as retrievable edge documents, not as a traversable graph subsystem. +- C3 covers FastAPI/Flask/Typer/Click only. +- The least invasive ExplainPack integration point is after retrieval and before answer composition, preferably as a new explicit orchestrator artifact or as a replacement for `context_analysis`. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..904d42b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +markers = + intent_router: intent router v2 suite (routing, normalization, anchors, retrieval spec invariants) diff --git a/tests/agent/__pycache__/test_gigachat_client_retry.cpython-312-pytest-9.0.2.pyc b/tests/agent/__pycache__/test_gigachat_client_retry.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91f3df6be129bf8d6cabc993467fd57bf1823826 GIT binary patch literal 3697 zcmb6cOKcm*b#^&RE`O4&k0n|DuyGn>5rFbXR?*fn9JqGcG*#`|0Sa9f>z$Fb(vr)} zEEQ280ihL8sc8{k3cElKx)eyD9&*ev#{j*^kpl|@7cEfq;F}B?aN$GI_jb7yr5gP_ zNN?u7dGmJOZ|3n&#bS;CykGu8z zT!yf-Gq>l!ZWrvf-N+wnQ%f0AttM2xA*uy zOu)T{GHtu+RJd)coNZSER`+pUuY}+ZZOJ>S(ByL3`i`>DFiPA!OJuZE31l; z!9U}&7E~oyc^NukUD|HIxXRR=h95Xg(U32P0*^NBCb^8C#SDoeT87De($t#7Mt%fM z9jH$Hn<=1@j3Nz)_q3G~$3YSt*Zz*cM6;+Uzz{8oVVogX;%7?$k-N~>Ueyb2d*%3{ z^OqAoI#uFEh|$;^>-3R1vO98So&Kh{J2}HA`d9e*!Wy@K?c>8xF8armXX(wf@l}!%)rO3jn=k#GCBj4A+b!+hw6?YLQ$3S zs-dFHb3-wJ%#(;tBRY~eL&!sf%H=qqHS$bP8Kaxm_6Q80za9-=xY6*{#6B|ja+-1K z9>dH!6=xP|IOGkqEb_)bA!xrUz@?=z-P3+uSTD?KEVX8~AlM6?@Pc$3!cLGGZE9Pn z13)7oR!jsTCQ<~%h)TMv$ zh!~K~)K=S5qFWNP{|FpNRyFwk!Kq&*OB1bZ;1hiXE{9f{4YHwT+S4K`6OJb8yEI|` z9)V^i(Ugq8fX8WBYZ)!NQD7RXX_@SJE88@i*~qK3Nu}SV_0?Xb;MHN=bKupkJ+HFK zOiu7$$&*SYz-^xk0@e3e0V{+j*F< zXW&12*?*_qCmZlSF6+xWx{T>75I`5+_sc-xO3XzEielI)9+vXpRZmLgHtAaF6o{vJ z+8q2RNq(DHxoL40^gcyB-V4Ag8}dpm5RPa({R)^WjylyH}fhP`LMBoI~5FSaT z2a9ltVcA<6G!UMY9fpeN*y0sRARM6=21=U)n|D8bYiD3`yD<5r_vC}yo8SFB`^AsG47b1k-uC!Uw#(Doul;nV_vW^F zGrGzYjZQg=pO@boA9w2Ee3>shm734Xc=VN#&Qi@gw4w~@1~snIlDp6&+))NR&Noj8pS)BF=*eDi7ieZ1%uLm6Ou9m$>j|8{#6a_D0(o^jFvxfWEYB zNQji8b?9-#TOea;FZHP@OIzvx_M5fFIZKwNrb;-yXhB>Amvuk1bd`cNL>E_j90_~F zNahmA0Q1sQ%+5r|`2!sjmW=nFN3n@Rdne<)lU9$)NDgm*D3mGq2X*YB1g3z%+c4@+ zl<5ZO#gMDq)mkG;14pQ1Hs%X7RW*U@j^L^Q%l6&6=R;Eo=Z}N0Mr=7B)O{9-+H-zz z*YTs4No8&6%yQJAdftu<^MdMiOdT~~&jz9by&GSy`A*ddVZ|z{d?lzZ@dGF4~mB(}x{=SLHnCRe#$c?F;gXtqWdhbq0rZWNIb{%ii(+_R|T8D~$ zm_PlP;@>wh9TPi9#M7@)IG%RnNAz^uE9r3)3fXEV=_TF;f2mPBPRMc`bV!o2;2#PP zay8pwsZ5(Eeh7QS4czz|{DdgS-MTExdB|Q?PJS1iq@27)eiLTZt zo<};im*(a8b611|dhzm>cm?dGG)?;pwr{`e{X3cZGkN<(TGxi35kN0;8ST_F0_geR K3GGbei+=%9G=Urd literal 0 HcmV?d00001 diff --git a/tests/agent/__pycache__/test_llm_service_logging.cpython-312-pytest-9.0.2.pyc b/tests/agent/__pycache__/test_llm_service_logging.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f1aaf5148937ef98509aea3a20c8bc6647ec303 GIT binary patch literal 6843 zcmeGgTWl29_0D5=XLrr6%_Beqcnu|CQL+m*kVgSSLrc>TLz`9+7U^hqXKdEYK6+>T zWLHHgsvHR^RB8)Rk&3E*fJ~+IEB&hY=trbh>Mpyk&8SiNRgA#*`yc$G@N=1vzhcGxpw7O992A#`PISQ}g90nX zqLdc}Qj`Zn0&fWqhM`3?BSmFU5r{~R5?wk^ba_VP!wg0lEd;c%M~mr-9yu=$#wUez z^dHDLkQSWe;bEg>A2W+@7<8Bdjec{1pX!XU$_D_ zkbzK>szOydvxAVDT$Qc&Y?qsiwD;NW%?e$t%0q(A`V5IPZHel#9dfxw-0ipoC}-OE z7_rs`##aI?ZL})PwCATmPIjhCxqZ%(0Q38+UWr$7N7X}D#3@)|Eu@F_NHtUtSS@U? zaz_u0lyGT&Sq-}*x#in%eFgI(?vzueb_Me)Fa>%!FP?K>)yyV6$bz=0H^eQAs0t-CHd5;P#_TTEyc zsDjNH_NFZ)?p=(18)&IQsWNI))P%*@v16~N%A+G1z+pKXRO@tk)YKWPI&7A6nyHqwqTxhUOB*t91SfHr zt)@^eeTC98b=IguWm?p1)tIQ5T1m5^B^_~O%P@!7UfA-SIL^p7(i^Po<`q81o?vC4 zh5JhJZ!wp}4O>~wG$yh~j4`wfBSzk~)Un>IR!HfzFlLleTBU+A&Ks7M#RfE1Sri2H z^l;TQizC4>^j;PE}Q%n5q`b`lx9D-l3}B9o0-cZ>UL@P3ae*!*xQp zLqWG8K-_#fI0?`ogMv-aO4hhRX&g>~qPwBbBS0rZ zyAZ5KfEJgo1CS2WEr>IwZp2audJt>{VBuxrYR+z%Bb#MSpXv%8eFjN5fu#We1>BOU zE1BuD^(~o3GBc;(Z?;X~H4iqB%xg^_Z7CV(>oIuAa&2zUb6-ICi%RXJwWj%BwJkwrJO@x3F z{@k7rXhv$%_m+kXNq3NuP8ah_U18x@6vK>ww#Do?p=F;dp&yDD*2<)&)FOJ~JgF*2ywJ5K+i1(#SCw5|uSKg->&JRh z?|kSJ*cjO13MXKrZnMvwg9dx7a1tNy0VmQKm}P}N{th0%7j;*mcU_TKs2sB&_N-!V zU$^u^<*2^;iF666J}&95c_Q7%DBZPBr2F`!J4v>Y-NX{c;W6eUcvgX2lrH>eLOMyt z1$>~%i?zr&Nv|82+6-AshR5MnhNUSP$WSuv3|J^z7~);Pc-C&+Kb%q}NV zgggy4Y+)FmO0no~F}fea%!vv;IQ1G!VV7lVmddhT2tsw0=ewAJs%$cM>zVyLr9GJO z218qKp_}?U`S&?;E0OzfxSl+C^VwGN)kY%s(E?^AZznp1spkUe_MWsAJBx`qASs@r zL$yp6eA2w%zF{+`OGrS_%h{Nd>En9eYnMdDCtW;c%(waKqe@} z{D$j0*txnMg(6#fNDhX8c>v+6QM@FOm)kN4IB3BT$|~apd}r5!g;L%Q#g^fA)|Y+ z+X@LFS|%LDQCxcvd?D%!J__x%;xGtL78&FjF&(}yVe_3Zd1`B2?wQ_q@l+$;GbiJ3me+a3gAJteTGK~ca?iz69*>6r z^L+1%A7p-&`D60%weZF8TxUO?L@^`w2xc`71opu3Uj_?nfB*mh literal 0 HcmV?d00001 diff --git a/tests/agent/__pycache__/test_logging_setup.cpython-312-pytest-9.0.2.pyc b/tests/agent/__pycache__/test_logging_setup.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22cb4ad23d2734c11e287c5bfb969efa1256b19a GIT binary patch literal 3756 zcmeHK&1)M+6yKHBtB;K&e^Un&t2U-yl3KAHry(REfi`hU@gYrVFoo?}Gm=+X?W!{? z#a1OSZ4T|J$GVV%dnly1&_ARXC908F3@wx%atkCUpZaEZXIGFwatQR2WtsWCuX%6Y z&U+gDk;$Y5c(T9$VkF{%@DC&S6AQqzivWHUkbp!}*bu!rR*e^9qEMAKhD2W?Z^$5t znTcw0BMCeS#qUcSsU1-rdcqc3S4DU9(;}&FZW>nkx=pIO<6u$@oOn|TMur5FLR)G{^d~37H7i+;q8TMyQb|N9W>JbA1n~~o3`e>gwc>oQDD8}KZe^UK zXg&yFL!c7_YfeTQ9JOM6O){wYGVh_LC89J^9>kiE#r6<7fikTjL-Z)wnc^!C_&v#a z(cF@`Up{{W=Vx1N$J-LhzRhI&&15q7Hj{5ClfywK#n-Ct5wE&O-&XgF)!m9!rlUT< zQ9FUg+R2#EO0<&Prk!e~=+9{U|L=AKz3gS3F~K8#g zFNs};ALcy5MS~i)busVg^sZ*0%WQssDUS>t^fd}=fS3t<4v5M`uWd0-jo1}jbhNMZ zLeaJyyzOWNJH)^Z-TYNh6RyN!afiJ4++SJFwDx=FlU)C{X+ zH%ECOmk36B(ZT2nTqBJq;in?jSm#UGXU9IT^ zyD5$8CCFioxF~vG8BUs5O84!b}TS(IpqS_{; z?yknlge*h6?21e0;mW%!91YKQ=5BO{*LH6nWyX7%Q;#yI`k9kQ*@++Q@9m?JseW#_ zf5Pf#a{ZBs=ecB7-o5^GTo|6}|f|8r+X564+Dlz zbv??*;RrVS7eYCQp%?&|^%b=vt#%ak;hm1M+LPFO7%+UQ>rp-qN3eN#CzNv-iUELG zU&(i*GaV(rzt&OC^d$Bk1`MC-dX$gD5p4F?LOF+_7yy{{m9dUA_uYj(NXcAJV(+0( z`$U%!J`6`N*b8_d<}efk05&Y#Q>Gs&(;aDc?=}no_ilHUSr5+kr06}AW|sMS|}U z!+};1vfM<#Bd5TIus;LtB$UA-G#l*wm_YyWPrBEWypu}UQ6(#ESc2g=d8ulnx{0q4 i_WzRo^icL6=V@FN#lHmMzVL1KUt#sRJSxt4R{sHQ0wqTP literal 0 HcmV?d00001 diff --git a/tests/agent/engine/router/__pycache__/test_router_service_intent_policy.cpython-312-pytest-9.0.2.pyc b/tests/agent/engine/router/__pycache__/test_router_service_intent_policy.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c85e675db941b7a42248629176dcd6f30e454acc GIT binary patch literal 17888 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 z bool: + return (domain_id, process_id) in { + ("default", "general"), + ("project", "qa"), + ("project", "edits"), + ("docs", "generation"), + } + + def get_factory(self, domain_id: str, process_id: str): + return object() + + +class _FakeClassifier: + def __init__(self, decision: RouteDecision | None = None, forced: RouteDecision | None = None) -> None: + self._decision = decision or RouteDecision(domain_id="project", process_id="qa", confidence=0.95, reason="new_intent") + self._forced = forced + self.calls = 0 + + def from_mode(self, mode: str) -> RouteDecision | None: + return self._forced if mode != "auto" else None + + def classify_new_intent(self, user_message: str, context: RouterContext) -> RouteDecision: + self.calls += 1 + return self._decision + + +class _FakeContextStore: + def __init__(self, context: RouterContext) -> None: + self._context = context + self.updated: list[dict] = [] + + def get(self, conversation_key: str) -> RouterContext: + return self._context + + def update(self, conversation_key: str, **kwargs) -> None: + self.updated.append({"conversation_key": conversation_key, **kwargs}) + + +class _FakeSwitchDetector: + def __init__(self, should_switch: bool) -> None: + self._should_switch = should_switch + + def should_switch(self, user_message: str, context: RouterContext) -> bool: + return self._should_switch + + +def test_router_service_classifies_first_message() -> None: + service = RouterService( + registry=_FakeRegistry(), + classifier=_FakeClassifier(), + context_store=_FakeContextStore(RouterContext()), + switch_detector=_FakeSwitchDetector(False), + ) + + route = service.resolve("Объясни как работает endpoint", "dialog-1") + + assert route.domain_id == "project" + assert route.process_id == "qa" + assert route.decision_type == "start" + + +def test_router_service_keeps_current_intent_for_follow_up() -> None: + context = RouterContext( + active_intent={"domain_id": "project", "process_id": "qa"}, + last_routing={"domain_id": "project", "process_id": "qa"}, + dialog_started=True, + turn_index=1, + ) + classifier = _FakeClassifier( + decision=RouteDecision(domain_id="docs", process_id="generation", confidence=0.99, reason="should_not_run") + ) + service = RouterService( + registry=_FakeRegistry(), + classifier=classifier, + context_store=_FakeContextStore(context), + switch_detector=_FakeSwitchDetector(False), + ) + + route = service.resolve("Покажи подробнее", "dialog-1") + + assert route.domain_id == "project" + assert route.process_id == "qa" + assert route.decision_type == "continue" + assert classifier.calls == 0 + + +def test_router_service_switches_only_on_explicit_new_intent() -> None: + context = RouterContext( + active_intent={"domain_id": "project", "process_id": "qa"}, + last_routing={"domain_id": "project", "process_id": "qa"}, + dialog_started=True, + turn_index=2, + ) + classifier = _FakeClassifier( + decision=RouteDecision(domain_id="project", process_id="edits", confidence=0.96, reason="explicit_edit") + ) + service = RouterService( + registry=_FakeRegistry(), + classifier=classifier, + context_store=_FakeContextStore(context), + switch_detector=_FakeSwitchDetector(True), + ) + + route = service.resolve("Теперь измени файл README.md", "dialog-1") + + assert route.domain_id == "project" + assert route.process_id == "edits" + assert route.decision_type == "switch" + assert route.explicit_switch is True + assert classifier.calls == 1 + + +def test_router_service_keeps_current_when_explicit_switch_is_unresolved() -> None: + context = RouterContext( + active_intent={"domain_id": "project", "process_id": "qa"}, + last_routing={"domain_id": "project", "process_id": "qa"}, + dialog_started=True, + turn_index=2, + ) + classifier = _FakeClassifier( + decision=RouteDecision(domain_id="docs", process_id="generation", confidence=0.2, reason="low_confidence") + ) + service = RouterService( + registry=_FakeRegistry(), + classifier=classifier, + context_store=_FakeContextStore(context), + switch_detector=_FakeSwitchDetector(True), + ) + + route = service.resolve("Теперь сделай что-то другое", "dialog-1") + + assert route.domain_id == "project" + assert route.process_id == "qa" + assert route.decision_type == "continue" + assert route.reason == "explicit_switch_unresolved_keep_current" + + +def test_router_service_persists_decision_type() -> None: + store = _FakeContextStore(RouterContext()) + service = RouterService( + registry=_FakeRegistry(), + classifier=_FakeClassifier(), + context_store=store, + switch_detector=_FakeSwitchDetector(False), + ) + + service.persist_context( + "dialog-1", + domain_id="project", + process_id="qa", + user_message="Объясни", + assistant_message="Ответ", + decision_type="continue", + ) + + assert store.updated[0]["decision_type"] == "continue" diff --git a/tests/agent/orchestrator/__pycache__/test_code_explain_actions.cpython-312-pytest-9.0.2.pyc b/tests/agent/orchestrator/__pycache__/test_code_explain_actions.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e83a0daf3f4b8b26158a69ead0804343d93ac78c GIT binary patch literal 5471 zcmcgwO>7&-6`m!RCk}yhm;;_kRh>H9`rMOj4A@J5Uam z0um|9#gK$80W(~V6eB_pno2oZj0#;gW94`;UQQGf2}d*jw%^_DLi_rU(sRB{Xs?;ErGHr-~k(4bbRSxi~=MGzRa%Whs~V2O3Q0B;Iqn zLiMvtRZ}ypQzgf!ST@vV&MxVt8kSC1EJt5*auJ?5#T;W+D>*aERh=h&i}R*t@zfhN zr&@E+nt=(AU#LKLYwn!xXk00jbW3AKg{NmUdjX7Wure&i<_cC6s(L9GK$l_~ufd2G zxM&y8Yo&#fyB=3Y+lBkbLAgeB5!hmYN;Gg?qCq`KW%vu}vK|@+3o5NdK!)}3tU@DK z!^H>$K;bF%Rc%4PpgYXa7j=fQNY5Iku9h^58dP(1o2QsIr`o!0Lm*Ux@>tE*nfh)` zXUi-Du6da0PK{ZmV0%guykRhc<@64;5{Rk%#7IUVf)b5kk4_}JMt|Ho6mL`-|JouR@O(Zo;=UzBJWdn;qevwZ$s2$K*DRz22tsbZc*0UZSulIPX zE0A38^iBxy<vL{3`iIWS&bem-Z9oH#Lp>XDqy&?jJH5E@V%SxIR&fRG~$ zj^%8uJjNB>qSXo@Dxg@vusmOr;LEUlUp>CBNruR{lhLZx39V{OAghex7pU>Y6 zaJi~ES9qj^;17t7SO=4cGVMJV{dmTzu(D`*+VBj!}57xODkG))72K;nXjqxl1 zs%ffBud-65T-BJ)6VR+)t{Em|2%Cq^3Ru{DNVTt2Y9YQ1*teL8%m31CfZEaRZ z4?J~BtRC?AEMpbMd(~N-h@_lZRahNv6OUo^s9J^8xH7yu zk>xv0xNY`xfZJ=N_4If{8UN`08-vaC_?CjdyKdPn__B$$ZfT>VZYtw927Mb}0pnJB zxS>4z(aOigM*7(;1%G$lvRl}|f~)!xI(%I8n?2QU1_gSx4xYSotT}q2vA@tz3LpI# z-pzDjOTpjWc7H%_xLGv@g4_xi+E@VGdES%qaOQkJjz8pp5erO>)3kS zLp7S934_q&_27EyR=dwC>12MIrqY+sf@WxTjsQ-!InJu&;A+KFCv}PT0>ACMO4j?m zxvc`i&f`gc#n2j11@{`@C={`p-l<9DBvd6ZVl zz=Q?4Y-5Aa0eNQ)s#_%;Ii_Y;EFRV@`#qfj(8if=*Gxxskcg995GsJD3b0;scn|h9 zYuMMXiz2hN)v{IvNQ~ebaFndOh%SaJfRZN!%`F2?0WNXH_Sk!bxul-QVF;hXr=b$4 zzZgq!f29nh9|*Lz5=KluG|7Wb)#TD-E)3Ct8W@|MgRrslAb8x-%TVj+IHinV08Wmy zn}=*iuiCp#I&Rn?{N2Z6e2pxVo6VjNdbws%Qx|!R?Ze4@6U7J! zI0(T7eVHe<_C-w`5Vjwi5)40BhTkMloPF#34^F){t)4%1VTR!qfguxSI6sCnT9bnNh@_%NTW5hANDs z{d*8=WINv5$_%ykJkuK7*Xn!|kkA0aZunk|B+_fI3-H}_K>VxTZy$kN4<-TpFdpLRP-L&wgUwZ`F3tP!>)8`h zC!PfW5Sf3>Wo{mK^^u242Jr|df+G!xTW}ENDx`r&2*25Z=lRE6$0%G`;Sv?bOG1b6 zclCJ;^wxc9{sg-YEsywD9fd}_E{#5jcX{$5ytDBCcxeo}7sq9ii%s){Ac=ss$eY*! z=*~t_d=~|#gKpm3=HN$jb}qoa0gcGCkM5%1$6fk;JQo*7oZ(iBY|jT>61I!Ji9ro? z6vyNs;2OB~77NWQrCYAc@o#dtHm_v7z3aUS>! zU(J#3{lm@6*)9kl{A#^nqPl4dh;|WNV7Y*HgnSp@5i9trL86E&P=lP@-GOHV$lk7ui#Y}0R0dX%VNz&ht;oc?MU&+kFfFcDR5)faYc-TXv iSEYyHkVK^~2+DhzJyN=n+IOG8`@xG5>3fpc&3^;cJR<4< literal 0 HcmV?d00001 diff --git a/tests/agent/orchestrator/__pycache__/test_orchestrator_service.cpython-312-pytest-9.0.2.pyc b/tests/agent/orchestrator/__pycache__/test_orchestrator_service.cpython-312-pytest-9.0.2.pyc index b9b202a4326a28204cfec1a0802c0e4821d00897..1b4196572422f8df9d60723a7b6f27cacc6a9621 100644 GIT binary patch literal 14323 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 diff --git a/tests/agent/orchestrator/__pycache__/test_project_qa_actions.cpython-312-pytest-9.0.2.pyc b/tests/agent/orchestrator/__pycache__/test_project_qa_actions.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..562c374afc72ebdf00ab98310d54d4aac1a51797 GIT binary patch literal 6079 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 literal 0 HcmV?d00001 diff --git a/tests/agent/orchestrator/__pycache__/test_project_qa_retrieval_graph_v2_only.cpython-312-pytest-9.0.2.pyc b/tests/agent/orchestrator/__pycache__/test_project_qa_retrieval_graph_v2_only.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..608cd1d9532044fdeee16534b6c3b56632b30ba9 GIT binary patch literal 3171 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 delta 178 zcmZq7+Th1`nwOW00SMwE7H0ahPvnzejGL$)!OfAv-og^aU&*M+xp9)5G^6I`jnZw5 zjK-TSWH}faT_-!ri3u11wH4Wb2zwCWv^iZ)n~^bl^JMu_HZe`6B9OU$ngX{tl1ejk zN;30`i-ITLF*arlm@Ht@r ExplainPack: + assert rag_session_id == "rag-1" + assert "endpoint" in user_query + assert file_candidates == [{"path": "app/api/users.py", "content": "..." }] + return ExplainPack(intent=ExplainIntent(raw_query=user_query, normalized_query=user_query)) + + +def _ctx() -> ExecutionContext: + task = TaskSpec( + task_id="task-1", + dialog_session_id="dialog-1", + rag_session_id="rag-1", + user_message="Explain endpoint get_user", + scenario=Scenario.EXPLAIN_PART, + routing=RoutingMeta(domain_id="project", process_id="qa", confidence=0.9, reason="test"), + constraints=TaskConstraints(), + output_contract=OutputContract(result_type="answer"), + metadata={"rag_context": "", "confluence_context": "", "files_map": {}}, + ) + plan = ExecutionPlan( + plan_id="plan-1", + task_id="task-1", + scenario=Scenario.EXPLAIN_PART, + template_id="tpl", + template_version="1", + steps=[], + ) + ctx = ExecutionContext(task=task, plan=plan, graph_resolver=lambda *_: None, graph_invoker=lambda *_: {}) + ctx.artifacts.put( + key="source_bundle", + artifact_type=ArtifactType.STRUCTURED_JSON, + content={"file_candidates": [{"path": "app/api/users.py", "content": "..."}]}, + ) + return ctx + + +def test_code_explain_actions_store_explain_pack() -> None: + ctx = _ctx() + actions = CodeExplainActions(_FakeRetriever()) + + actions.build_code_explain_pack(ctx) + + stored = ctx.artifacts.get_content("explain_pack", {}) + assert stored["intent"]["raw_query"] == "Explain endpoint get_user" diff --git a/tests/agent/orchestrator/test_orchestrator_service.py b/tests/agent/orchestrator/test_orchestrator_service.py index d1cdce2..5b33578 100644 --- a/tests/agent/orchestrator/test_orchestrator_service.py +++ b/tests/agent/orchestrator/test_orchestrator_service.py @@ -14,7 +14,7 @@ class DummyGraph: pass -def _task(scenario: Scenario) -> TaskSpec: +def _task(scenario: Scenario, *, domain_id: str = "project", process_id: str = "qa") -> TaskSpec: allow_writes = scenario in {Scenario.DOCS_FROM_ANALYTICS, Scenario.TARGETED_EDIT, Scenario.GHERKIN_MODEL} return TaskSpec( task_id="task-1", @@ -23,7 +23,7 @@ def _task(scenario: Scenario) -> TaskSpec: mode="auto", user_message="Explain this module", scenario=scenario, - routing=RoutingMeta(domain_id="project", process_id="qa", confidence=0.95, reason="unit-test"), + routing=RoutingMeta(domain_id=domain_id, process_id=process_id, confidence=0.95, reason="unit-test"), constraints=TaskConstraints(allow_writes=allow_writes, max_steps=16, max_retries_per_step=2, step_timeout_sec=90), output_contract=OutputContract(result_type="answer"), metadata={ @@ -38,8 +38,8 @@ def test_orchestrator_service_returns_answer() -> None: service = OrchestratorService() def graph_resolver(domain_id: str, process_id: str): - assert domain_id == "project" - assert process_id == "qa" + assert domain_id == "default" + assert process_id == "general" return DummyGraph() def graph_invoker(_graph, state: dict, dialog_session_id: str): @@ -47,7 +47,13 @@ def test_orchestrator_service_returns_answer() -> None: assert dialog_session_id == "dialog-1" return {"answer": "It works.", "changeset": []} - result = asyncio.run(service.run(task=_task(Scenario.GENERAL_QA), graph_resolver=graph_resolver, graph_invoker=graph_invoker)) + result = asyncio.run( + service.run( + task=_task(Scenario.GENERAL_QA, domain_id="default", process_id="general"), + graph_resolver=graph_resolver, + graph_invoker=graph_invoker, + ) + ) assert result.answer == "It works." assert result.meta["plan"]["status"] == "completed" @@ -70,3 +76,100 @@ def test_orchestrator_service_generates_changeset_for_docs_scenario() -> None: ) assert result.meta["plan"]["status"] == "completed" assert len(result.changeset) > 0 + + +def test_orchestrator_service_uses_project_qa_reasoning_without_graph() -> None: + service = OrchestratorService() + requested_graphs: list[tuple[str, str]] = [] + + def graph_resolver(domain_id: str, process_id: str): + requested_graphs.append((domain_id, process_id)) + return DummyGraph() + + def graph_invoker(_graph, state: dict, _dialog_session_id: str): + if "resolved_request" not in state: + return { + "resolved_request": { + "original_message": state["message"], + "normalized_message": state["message"], + "subject_hint": "", + "source_hint": "code", + "russian": True, + } + } + if "question_profile" not in state: + return { + "question_profile": { + "domain": "code", + "intent": "inventory", + "terms": ["control", "channel"], + "entities": [], + "russian": True, + } + } + if "source_bundle" not in state: + return { + "source_bundle": { + "profile": state["question_profile"], + "rag_items": [], + "file_candidates": [ + {"path": "src/config_manager/v2/control/base.py", "content": "class ControlChannel: pass"}, + {"path": "src/config_manager/v2/control/http_channel.py", "content": "class HttpControlChannel(ControlChannel): pass # http api"}, + ], + "rag_total": 0, + "files_total": 2, + } + } + if "analysis_brief" not in state: + return { + "analysis_brief": { + "subject": "management channels", + "findings": ["В коде найдены конкретные реализации каналов управления: http channel (`src/config_manager/v2/control/http_channel.py`)."], + "evidence": ["src/config_manager/v2/control/http_channel.py"], + "gaps": [], + "answer_mode": "inventory", + } + } + return { + "answer_brief": { + "question_profile": state["question_profile"], + "resolved_subject": "management channels", + "key_findings": ["В коде найдены конкретные реализации каналов управления: http channel (`src/config_manager/v2/control/http_channel.py`)."], + "supporting_evidence": ["src/config_manager/v2/control/http_channel.py"], + "missing_evidence": [], + "answer_mode": "inventory", + }, + "final_answer": "## Кратко\n### Что реализовано\n- В коде найдены конкретные реализации каналов управления: http channel (`src/config_manager/v2/control/http_channel.py`).", + } + + task = _task(Scenario.GENERAL_QA).model_copy( + update={ + "user_message": "Какие каналы управления уже реализованы?", + "metadata": { + "rag_context": "", + "confluence_context": "", + "files_map": { + "src/config_manager/v2/control/base.py": { + "content": "class ControlChannel:\n async def start(self):\n ..." + }, + "src/config_manager/v2/control/http_channel.py": { + "content": "class HttpControlChannel(ControlChannel):\n async def start(self):\n ...\n# http api" + }, + }, + "rag_items": [], + }, + } + ) + + result = asyncio.run(service.run(task=task, graph_resolver=graph_resolver, graph_invoker=graph_invoker)) + + assert "Что реализовано" in result.answer + assert "http channel" in result.answer.lower() + assert result.meta["plan"]["status"] == "completed" + assert requested_graphs == [ + ("project_qa", "conversation_understanding"), + ("project_qa", "question_classification"), + ("project_qa", "context_retrieval"), + ("project_qa", "context_analysis"), + ("project_qa", "answer_composition"), + ] diff --git a/tests/agent/orchestrator/test_project_qa_actions.py b/tests/agent/orchestrator/test_project_qa_actions.py new file mode 100644 index 0000000..ca35e1f --- /dev/null +++ b/tests/agent/orchestrator/test_project_qa_actions.py @@ -0,0 +1,71 @@ +from app.modules.agent.engine.orchestrator.actions.project_qa_actions import ProjectQaActions +from app.modules.agent.engine.orchestrator.execution_context import ExecutionContext +from app.modules.agent.engine.orchestrator.models import ( + ExecutionPlan, + OutputContract, + RoutingMeta, + Scenario, + TaskConstraints, + TaskSpec, +) + + +def _ctx(message: str, rag_items: list[dict], files_map: dict[str, dict]) -> ExecutionContext: + task = TaskSpec( + task_id="task-1", + dialog_session_id="dialog-1", + rag_session_id="rag-1", + user_message=message, + scenario=Scenario.GENERAL_QA, + routing=RoutingMeta(domain_id="project", process_id="qa", confidence=0.9, reason="test"), + constraints=TaskConstraints(), + output_contract=OutputContract(result_type="answer"), + metadata={ + "rag_items": rag_items, + "rag_context": "", + "confluence_context": "", + "files_map": files_map, + }, + ) + plan = ExecutionPlan( + plan_id="plan-1", + task_id="task-1", + scenario=Scenario.GENERAL_QA, + template_id="tpl", + template_version="1", + steps=[], + ) + return ExecutionContext(task=task, plan=plan, graph_resolver=lambda *_: None, graph_invoker=lambda *_: {}) + + +def test_project_qa_actions_build_inventory_answer_from_code_sources() -> None: + ctx = _ctx( + "Какие каналы управления уже реализованы?", + [], + { + "src/config_manager/v2/control/base.py": {"content": "class ControlChannel:\n async def start(self):\n ..."}, + "src/config_manager/v2/core/control_bridge.py": { + "content": "class ControlChannelBridge:\n async def on_start(self):\n ...\n async def on_status(self):\n ..." + }, + "src/config_manager/v2/control/http_channel.py": { + "content": "class HttpControlChannel(ControlChannel):\n async def start(self):\n ...\n# http api" + }, + "src/config_manager/v2/control/telegram_channel.py": { + "content": "class TelegramControlChannel(ControlChannel):\n async def start(self):\n ...\n# telegram bot" + }, + }, + ) + actions = ProjectQaActions() + + actions.classify_project_question(ctx) + actions.collect_project_sources(ctx) + actions.analyze_project_sources(ctx) + actions.build_project_answer_brief(ctx) + actions.compose_project_answer(ctx) + + answer = str(ctx.artifacts.get_content("final_answer", "")) + assert "### Что реализовано" in answer + assert "http channel" in answer.lower() + assert "telegram channel" in answer.lower() + assert "### Где смотреть в проекте" in answer + diff --git a/tests/agent/orchestrator/test_project_qa_answer_graph_v2.py b/tests/agent/orchestrator/test_project_qa_answer_graph_v2.py new file mode 100644 index 0000000..769557d --- /dev/null +++ b/tests/agent/orchestrator/test_project_qa_answer_graph_v2.py @@ -0,0 +1,74 @@ +import sys +import types + +langgraph = types.ModuleType("langgraph") +langgraph_graph = types.ModuleType("langgraph.graph") +langgraph_graph.END = "END" +langgraph_graph.START = "START" +langgraph_graph.StateGraph = object +sys.modules.setdefault("langgraph", langgraph) +sys.modules.setdefault("langgraph.graph", langgraph_graph) + +from app.modules.agent.engine.graphs.project_qa_step_graphs import ProjectQaAnswerGraphFactory + + +class _FakeLlm: + def __init__(self) -> None: + self.calls: list[tuple[str, str, str | None]] = [] + + def generate(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> str: + self.calls.append((prompt_name, user_input, log_context)) + return "## Summary\n[entrypoint_1] [excerpt_1]" + + +def test_project_qa_answer_graph_uses_v2_prompt_when_explain_pack_present() -> None: + llm = _FakeLlm() + factory = ProjectQaAnswerGraphFactory(llm) + + result = factory._compose_answer( + { + "message": "Explain endpoint get_user", + "question_profile": {"russian": False}, + "analysis_brief": {"findings": [], "evidence": [], "gaps": [], "answer_mode": "summary"}, + "explain_pack": { + "intent": { + "raw_query": "Explain endpoint get_user", + "normalized_query": "Explain endpoint get_user", + "keywords": ["get_user"], + "hints": {"paths": [], "symbols": [], "endpoints": [], "commands": []}, + "expected_entry_types": ["http"], + "depth": "medium", + }, + "selected_entrypoints": [], + "seed_symbols": [], + "trace_paths": [], + "evidence_index": { + "entrypoint_1": { + "evidence_id": "entrypoint_1", + "kind": "entrypoint", + "summary": "/users/{id}", + "location": {"path": "app/api/users.py", "start_line": 10, "end_line": 10}, + "supports": ["handler-1"], + } + }, + "code_excerpts": [ + { + "evidence_id": "excerpt_1", + "symbol_id": "handler-1", + "title": "get_user", + "path": "app/api/users.py", + "start_line": 10, + "end_line": 18, + "content": "async def get_user():\n return 1", + "focus": "overview", + } + ], + "missing": [], + "conflicts": [], + }, + } + ) + + assert result["final_answer"].startswith("## Summary") + assert llm.calls[0][0] == "code_explain_answer_v2" + assert '"evidence_id": "excerpt_1"' in llm.calls[0][1] diff --git a/tests/agent/orchestrator/test_project_qa_retrieval_graph_v2_only.py b/tests/agent/orchestrator/test_project_qa_retrieval_graph_v2_only.py new file mode 100644 index 0000000..739f8d9 --- /dev/null +++ b/tests/agent/orchestrator/test_project_qa_retrieval_graph_v2_only.py @@ -0,0 +1,49 @@ +import sys +import types + +langgraph = types.ModuleType("langgraph") +langgraph_graph = types.ModuleType("langgraph.graph") +langgraph_graph.END = "END" +langgraph_graph.START = "START" +langgraph_graph.StateGraph = object +sys.modules.setdefault("langgraph", langgraph) +sys.modules.setdefault("langgraph.graph", langgraph_graph) + +from app.modules.agent.engine.graphs.project_qa_step_graphs import ProjectQaRetrievalGraphFactory + + +class _FailingRag: + async def retrieve(self, rag_session_id: str, query: str): + raise AssertionError("legacy rag should not be called for explain_part") + + +def test_project_qa_retrieval_skips_legacy_rag_for_explain_part() -> None: + factory = ProjectQaRetrievalGraphFactory(_FailingRag()) + + result = factory._retrieve_context( + { + "scenario": "explain_part", + "project_id": "rag-1", + "resolved_request": { + "original_message": "Explain how ConfigManager works", + "normalized_message": "Explain how ConfigManager works", + }, + "question_profile": { + "domain": "code", + "intent": "explain", + "terms": ["configmanager"], + "entities": ["ConfigManager"], + "russian": False, + }, + "files_map": { + "src/config_manager/__init__.py": { + "content": "from .v2 import ConfigManagerV2 as ConfigManager", + "content_hash": "hash-1", + } + }, + } + ) + + bundle = result["source_bundle"] + assert bundle["rag_items"] == [] + assert bundle["files_total"] >= 1 diff --git a/tests/agent/orchestrator/test_template_registry.py b/tests/agent/orchestrator/test_template_registry.py index 30878fd..8572107 100644 --- a/tests/agent/orchestrator/test_template_registry.py +++ b/tests/agent/orchestrator/test_template_registry.py @@ -36,3 +36,13 @@ def test_template_registry_has_multi_step_review_docs_edit_gherkin() -> None: assert len(docs_steps) >= 9 assert len(edit_steps) >= 7 assert len(gherkin_steps) >= 8 + + +def test_template_registry_adds_code_explain_pack_step_for_project_explain() -> None: + registry = ScenarioTemplateRegistry() + + steps = [step.step_id for step in registry.build(_task(Scenario.EXPLAIN_PART)).steps] + + assert "code_explain_pack_step" in steps + assert steps.index("code_explain_pack_step") > steps.index("context_retrieval") + assert steps.index("code_explain_pack_step") < steps.index("context_analysis") diff --git a/tests/agent/test_gigachat_client_retry.py b/tests/agent/test_gigachat_client_retry.py new file mode 100644 index 0000000..288dc46 --- /dev/null +++ b/tests/agent/test_gigachat_client_retry.py @@ -0,0 +1,48 @@ +import requests + +from app.modules.shared.gigachat.client import GigaChatClient +from app.modules.shared.gigachat.settings import GigaChatSettings + + +class _FakeTokenProvider: + def get_access_token(self) -> str: + return "token" + + +class _FakeResponse: + def __init__(self, status_code: int, payload: dict, text: str = "") -> None: + self.status_code = status_code + self._payload = payload + self.text = text + + def json(self) -> dict: + return self._payload + + +def test_gigachat_client_retries_transient_http_errors(monkeypatch) -> None: + calls = {"count": 0} + + def fake_post(*args, **kwargs): + calls["count"] += 1 + if calls["count"] == 1: + return _FakeResponse(503, {}, "temporary") + return _FakeResponse(200, {"choices": [{"message": {"content": "ok"}}]}) + + monkeypatch.setattr(requests, "post", fake_post) + client = GigaChatClient( + GigaChatSettings( + auth_url="https://auth.example.test", + api_url="https://api.example.test", + scope="scope", + credentials="secret", + ssl_verify=True, + model="model", + embedding_model="embed", + ), + _FakeTokenProvider(), + ) + + result = client.complete("system", "user") + + assert result == "ok" + assert calls["count"] == 2 diff --git a/tests/agent/test_llm_service_logging.py b/tests/agent/test_llm_service_logging.py new file mode 100644 index 0000000..6cb3fa3 --- /dev/null +++ b/tests/agent/test_llm_service_logging.py @@ -0,0 +1,30 @@ +import logging + +from app.modules.agent.llm.service import AgentLlmService + + +class _FakeClient: + def complete(self, *, system_prompt: str, user_prompt: str) -> str: + assert system_prompt == "System prompt" + assert user_prompt == "User input" + return "LLM output" + + +class _FakePrompts: + def load(self, prompt_name: str) -> str: + assert prompt_name == "general_answer" + return "System prompt" + + +def test_llm_service_logs_input_and_output_for_graph_context(caplog) -> None: + service = AgentLlmService(_FakeClient(), _FakePrompts()) + + with caplog.at_level(logging.WARNING, logger="app.modules.agent.llm.service"): + result = service.generate("general_answer", "User input", log_context="graph.default.answer") + + assert result == "LLM output" + messages = [record.getMessage() for record in caplog.records] + assert any("graph llm input: context=graph.default.answer" in message for message in messages) + assert any("graph llm output: context=graph.default.answer" in message for message in messages) + assert any("User input" in message for message in messages) + assert any("LLM output" in message for message in messages) diff --git a/tests/agent/test_logging_setup.py b/tests/agent/test_logging_setup.py new file mode 100644 index 0000000..0521b56 --- /dev/null +++ b/tests/agent/test_logging_setup.py @@ -0,0 +1,24 @@ +import logging + +from app.core.logging_setup import ScrubbingFormatter + + +def test_scrubbing_formatter_redacts_identifiers_and_adds_blank_line() -> None: + formatter = ScrubbingFormatter("%(levelname)s:%(name)s:%(message)s") + record = logging.LogRecord( + name="test.logger", + level=logging.WARNING, + pathname=__file__, + lineno=1, + msg="router decision: task_id=task-1 dialog_session_id=dialog-1 graph_id=project_qa/context_retrieval", + args=(), + exc_info=None, + ) + + rendered = formatter.format(record) + + assert "task_id=" in rendered + assert "dialog_session_id=" in rendered + assert "graph_id=" in rendered + assert "task-1" not in rendered + assert rendered.endswith("\n") diff --git a/tests/chat/__pycache__/test_chat_api_simple_code_explain.cpython-312-pytest-9.0.2.pyc b/tests/chat/__pycache__/test_chat_api_simple_code_explain.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..405eda9b56af8619f6755cde2643aabe28c9e240 GIT binary patch literal 5555 zcmdTIOKcm*b@sz0x%^3@KDMPKlpWV$P*aifZ4(FaN2!B2jBBT9!nRqgxhsh>f0 zdg+7qao(G^GqdkE@9EF6m`(sz-QN@*2odr(Bs@^NgU+8|aFtlZl8PiJ@n5;5oUzQ;Ok|p40hQEk;VwT$GOkFplM7e5@7Yr9>{l$HC%AX*4$~ z5t;meSfLGKg|Exv47oAR(}5T9c}dQT0x#zC#yKwzyoAr2utu$s4K+7em(pWj;)2<< z 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 literal 0 HcmV?d00001 diff --git a/tests/chat/__pycache__/test_chat_api_simple_code_explain.cpython-312.pyc b/tests/chat/__pycache__/test_chat_api_simple_code_explain.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ded8368b888f6d7fb8263d8c7054084466aaa193 GIT binary patch literal 4136 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 L)YZ}fiND}qinm@C literal 0 HcmV?d00001 diff --git a/tests/chat/__pycache__/test_direct_service.cpython-312.pyc b/tests/chat/__pycache__/test_direct_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d59db5170ff93d1e18a483b3f60b1f217be3f1bb GIT binary patch literal 4002 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 oIeGqba`;|2B*pI%AotQ?Y3E%6 None: + self.calls = 0 + + async def handle_message(self, request): + self.calls += 1 + return TaskQueuedResponse( + task_id="task-1", + status="done", + ) + + +class _FakeRagSessions: + def get(self, rag_session_id: str): + return {"rag_session_id": rag_session_id} + + +class _FakeRepository: + def create_dialog(self, dialog_session_id: str, rag_session_id: str) -> None: + return None + + def get_dialog(self, dialog_session_id: str): + return None + + def add_message(self, dialog_session_id: str, role: str, content: str, task_id: str | None = None, payload: dict | None = None) -> None: + return None + + +def test_chat_messages_endpoint_uses_direct_service(monkeypatch) -> None: + monkeypatch.setenv("SIMPLE_CODE_EXPLAIN_ONLY", "true") + direct_chat = _FakeDirectChat() + module = ChatModule( + agent_runner=_FakeRuntime(), + event_bus=EventBus(), + retry=RetryExecutor(), + rag_sessions=_FakeRagSessions(), + repository=_FakeRepository(), + direct_chat=direct_chat, + task_store=TaskStore(), + ) + router = module.public_router() + endpoint = next(route.endpoint for route in router.routes if getattr(route, "path", "") == "/api/chat/messages") + response = asyncio.run( + endpoint( + ChatMessageRequest( + session_id="dialog-1", + project_id="rag-1", + message="Explain get_user", + ), + None, + ) + ) + + assert response.task_id == "task-1" + assert direct_chat.calls == 1 diff --git a/tests/chat/test_direct_service.py b/tests/chat/test_direct_service.py new file mode 100644 index 0000000..96d071c --- /dev/null +++ b/tests/chat/test_direct_service.py @@ -0,0 +1,61 @@ +import asyncio + +from app.modules.chat.direct_service import CodeExplainChatService +from app.modules.chat.session_resolver import ChatSessionResolver +from app.modules.chat.task_store import TaskStore +from app.modules.rag.explain.models import ExplainIntent, ExplainPack +from app.schemas.chat import ChatFileContext, ChatMessageRequest + + +class _FakeRetriever: + def build_pack(self, rag_session_id: str, user_query: str, *, file_candidates: list[dict] | None = None) -> ExplainPack: + return ExplainPack( + intent=ExplainIntent(raw_query=user_query, normalized_query=user_query), + missing=["code_excerpts"], + ) + + +class _FakeLlm: + def __init__(self) -> None: + self.calls = 0 + + def generate(self, prompt_name: str, user_input: str, *, log_context: str | None = None) -> str: + self.calls += 1 + return "should not be called" + + +class _FakeDialogs: + def get(self, dialog_session_id: str): + return None + + +def test_direct_service_skips_llm_when_evidence_is_insufficient() -> None: + messages: list[tuple[str, str, str, str | None]] = [] + llm = _FakeLlm() + task_store = TaskStore() + service = CodeExplainChatService( + retriever=_FakeRetriever(), + llm=llm, + session_resolver=ChatSessionResolver(_FakeDialogs(), lambda rag_session_id: rag_session_id == "rag-1"), + task_store=task_store, + message_sink=lambda dialog_session_id, role, content, task_id=None: messages.append((dialog_session_id, role, content, task_id)), + ) + + result = asyncio.run( + service.handle_message( + ChatMessageRequest( + session_id="dialog-1", + project_id="rag-1", + message="Explain get_user", + files=[ChatFileContext(path="app/api/users.py", content="", content_hash="x")], + ) + ) + ) + + task = task_store.get(result.task_id) + assert task is not None + assert task.answer is not None + assert "Недостаточно опоры в коде" in task.answer + assert result.status == "done" + assert llm.calls == 0 + assert [item[1] for item in messages] == ["user", "assistant"] diff --git a/tests/rag/__pycache__/asserts_intent_router.cpython-312.pyc b/tests/rag/__pycache__/asserts_intent_router.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3628608689e449c05a1640af46d62209078e192c GIT binary patch literal 5517 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 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/intent_router_testkit.cpython-312.pyc b/tests/rag/__pycache__/intent_router_testkit.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4262ead9c9c76dfaef2a8ee840b1e37292e5b8e2 GIT binary patch literal 2710 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 F;~y5PbhrQj delta 574 zcmX@$yUL00G%qg~0}%L&Ey=vZK9Ns?(QTr-g)mzROAAXBR|;DSE0E@{WYlEeSoNKk zQFC(4GuLR zN0YfoVsehKG#;hzg|!(qCQFOB0}V|UnPVggGDsan=mCjaEP1IFB}Gyov-oat6l4~p z=49rj7V&`PQu33Fi)1Goigq)~P2MZ&!sxY`Ni2q4M+qd$>ztpLmYMFGn3tHITI5$G z4iqfHbn@gb+39KuAURQxF4pAyyyB7~Ly&+mh)@O*LLfqTvb&s1y$OhE2O>a@Dl!8R z<{-ivMA(1`TOgsyS!54lgB)7q03>d4Cgx-&7RLkKT;v84bOI6XAi^0$xB!V_P9UKG z1K=>dWmuA0ToRw0pOP96^8MtwvdTXC#=*9@v4FJv1fR6wG diff --git a/tests/rag/__pycache__/test_code_indexing_pipeline.cpython-312.pyc b/tests/rag/__pycache__/test_code_indexing_pipeline.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60ae30de3c277cb4cc9785834783afded1924a6d GIT binary patch literal 3833 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 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_explain_intent_builder.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_explain_intent_builder.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bcbf1c4d59bd0206d9ed76d7c32809916d7458f GIT binary patch literal 5982 zcmeHLO>7)R7Ve(u`R&-m#0de+rVWYX(TbVGPU3jwAXxFUAjpMX4pC5VXS(d^$xP3v zy2pu4j}#0C&;rq(;DEH|fJhV}AtY822hiSD;?nK0HBoD&jkKq|A!ScF?R!;S-QzGA zHj&UCGU|Ety;twOs(!D!{=OR-DN68r@8+l0T3V9+g^c*6B5>X z+p!uZq1C7vR^)&}KcriKgnoS}xw5Fd7J;thNp;k?<7dj(BHgZRrn)Ilt}Az_0NYJ_ z>3VjET=7z#vMhBoUdH*GTM!y`79GLy(oB1qMcHH)i>Yf-D+4yWw(BG3r9|({jQfD7 zukRKpDaCs(OU~W_pARM)0(tQp(?QK)t;d?S4DBaxH%vDzdioQ3iqJE&A1kX6({9$x znRzd3$&AandqqzNJna**r1WyalPK3tfu3<&uD-97tn8*UA}hf2+L+a>vS>eMOlyMTUYM92U{Av*#gBNyAG>Jn3j z7||8oU1<|=pq+~2d^L?hIf7({;)eM4V9X#l6xXHauVVI`tMcmc9U8OjYNKNk-6am@ zNwXZ0%`n>}rqyXeZ=c-3fl1nKZCm$BV-(LgVdfm3k-`ic0%0K@0b%j!_6oMA8#El# zt45=tQ_`l@R+aQloXwXy~@l zB;n{YZ0uMq`#DNmG~A;vwrJCEb%<%huniY#!d#T{FvD^ajt>iXII=A=VTQeDNey%C ztq7%f5(`!AARn+t2AN}n%!xr}hBLfhC;1z4=IAyLM+c2&2aQfRDm1MnzuVOcejZN6 zXpoDQXUVeOrmZ@RAxB@HsTh`KQfry84_Wfx`=xhkkqtIo>^%G~%-V$CP=B>(^i7aFe-ZPBy^mP~Rg^x}K>JxnhpDoUF zCZ+*Mp*2+KC2$jUxpV`4#p*Etja%?NNL3`_7vCcN9qxqg_7>?!Zje%NyOf4=Wg2$R zil@L?23upldS0NUgxew|x-Hs%mca>#{pv%`2-|Lf691#ZCMmbw0wtP5Zk7^Fb_=w# zZhT)g@OHy|#+TVK9q=?RWJ&4$*zOr`fs}3{vgUuO@zqj`E&S{LR~nnQ zHUC@m2L^xgzx2NdJ_yzZw|l>*c=blH$85Qxm;*Y71a}A}l`Yg1&ja)zl82Gt^##S# zFntUOp2Fy(NDd&;fRu7{3NbE$^hqReIF@J`Nd?KTfjF3?Xpy2v0Ef@)xPsWmxdw6g zMnu0*BR1Qp!s2?~!Bt#$Yotk!fgkL@^JgGGSVF#+q=}bP@JWrnlEQ@qW-C5(99}=> zD~ECBIOeN|`wBi=k$`hC4V1(9Eu%&+ftyU3i#HT-7wg9WROk%!s+0<|l(-#gpDq={ zyx6_4h=vdAJeg&_XMpWUnUV|Ca$(yOi^5zaWBCZDU|(8pw#*KMrwj`Szy1adj z=DEZ*mPP5R#CpRbe>=ax?Vq9xP>*-b&O<=HPs_6WPf2=P+LB(ll`YB#|MN&jo@R#s E1uFl|W&i*H literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_explain_intent_builder.cpython-312.pyc b/tests/rag/__pycache__/test_explain_intent_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44339876ac9ac99d7596eebe59391c217194091b GIT binary patch literal 1034 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 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_intent_router_invariants.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_intent_router_invariants.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b507909feb6370792c6d0f35b3a61104bc9a184 GIT binary patch literal 19316 zcmeG^Yj6}tdNaGDUG0u`^#Ftr0x!l77K8*s2(XPU5JKj_l6d)SIrgyZti+0a$UVCP z;oS;nd^xyt0x6ecrOK61$&Z+FB}J~z6`wEm8UKk>`H>k`V#$Q6q$;UOQmIO%OfF~h zE8o{WuU$qDIJmm1cxtx$>+Y|=ndzSC@7eozp-_;6wY@$b*otf?R}rHRN@WKQhHf!~cO~2cto~Ev6_5l9S|YI+n~x zsn}G4NE0M6nmnCQ^l(M1qBk4m(PSziWizQMNr`7C68ci3{zOcXXxl_AcR~*twaH8_ zk;&;TMlq9>l&SRbY)U$rm^zguvcC4NmQ*5kGB%cwMoBg;Au&a7GLV#sSVoa1W2vO9 zH=1?%k$o zWPXhQ&?w2C6;M;;a)?Qf0w;SvG+q^+%hfpQ1xGznrw8CW>I>X>t=VGdMQ_1d@Jw<= zp&%%Km}@k#$KgY$K4gDE80F@n*3P+;wjK1-{6a9qJ_janeL+-x6jF(Etu&;Tbn zWtoF!tBqko1&>LSI5FL>ZeeD4IUon$_nh&;xYfv_Tvw<`^0eg3tuiS)t_$N^eH47= zb4%L&iw*iyFx=9m)D{NAm8G%&jrUy}+5R@J+3CtDS-`frnp_KgC5G z27l2BNa~+zZ>pbZKTtnaKaQwBS3iqrZ>d++pQ=C8&P8Gq6P@uaNkEbrO^$U;OcDHS zb${Q`o_^`zt~*V4W9YT}`(Kuh^dEX5kDsYY?Fz4Zl9?;~nWo3rO-yZ$Dv@L+g5Z{@ zq6c6xlX3}?R<3w-FEnGa<&Lp}Qnamr29;a$_???XwR2baJHAXd2gHDMvq_rOnB=2P zBm|uZt|tUuXpZ2DqX%I@BU92uDwfgxu}u6#mMFTAn}Q{|23E*?LJ#&%OwpwPvvwUyHV1N zPaxt|*($!tS?FQ~MzY&k=uvPGBJ8-6=sXJiOX-ZI5~n-&Bqo7(Wycfo9B_!P&R8-c zljLL~gKt3uTH*;s>BNRHU`U-fE=nhfjZw&8AQH*uVH%+{86(M9Ca1tFqi>~SXw^ya ztPIQvxgatp=~NPucr266Bw^H$uhCb5bitkCBBitgBRR|63awCA9#C6fD1`>Ud|eA2 zJiGT_g~m%yOh;#g$8HHN)6MFdtusQ;EunR~M_s#ZM%aF{y{p{5Q)}N@ZtvIH`)@SF zFF)~R^WU%h+m-4|$4mQ;Uuu4@?VUEYA%1orBsaxns_^Lfr>0k^;-h5&&skPxg%TDR zXr<6$dWF?&f>tvC(CenSTou-xe`b1071xyoJZD*%6-roOpp`<0=_#w(1g&NOpw}&- z`O@T!u=ZstiHl_;?w=t35=H|z>C4;&6@=EhY9F*G3Iy;|}FAFe^+_(!T$59=R*c42t=m$Pt zTkscZ^IS1d2q@PaS9Tlua3KH^Q9br>jC{ib8+nTZf}}J5Xj+&s@(l~%3MOA&0MEZj zyjl}aZhT*$GF4D+l9v>MhJ+>N7CV!oNy*F&2}=Me%MZUmv9^NeFC8Q}ER=~8oFF|g zG~7QZ?cH~vpP(?SH}q|i4i6nY)YmWdJ%4m?|6yRTo}D|R2H)NUe0%3kld|p$n64Otw`B5IdLvu_M~(kOoA&Qz{LF zNg_v*31Iw^GLeYuA@*n}BP2nYt2LjLpkzfJ!{iA}p2P%J9*#VYNfZ;*K=t~nX8<+I z@d(j6df&Q`%q7xfJND|pUJjPNnRH_HrmE^KPMjWRVXKqVZ)Y+IlL0nwqCCOi`!pk) zk#2t#YX*HZ!&ozPqyv#0O(tZ6Tc02?B_ZXNgaT3w$StRs%BzqepoMa%vUf48dfBDX z>60_UdMBseu(`aUSKH8A-Y}?b7`)Libh-P|()U)qvr26kLhd|tbHxU=rCk-;&+mX- z6WhxIp0h>;D_NUO_?iTHTU? zt~uDZ2NcQ&!M@F<1C$074d~%iCGLxL&!=t+i&eKD_>~u@=zFQ64=j%=`Za}`MJW0; zRMCeXHd}Kj$1dVT!D1@F_Je6-%$7xCEtPvJ5Ny%;KmJd{1rWu6uzn zRYao=4wg!_R@D5-BbY=mLEbXwBsio|Zv}2#cn4q9dHe!N&(q@ec zUdm)_jH&IeVrt1uDw#A*JO+9P2| zzwj>|ct-f5EevUemWs0q`YCri0(|g6usNp%*~#eS8n9GbwrH^M^6qS>=qY&IY|NlD z@pO4Zi}1A^8k*?gW=Xa*Pw3%b6m}mMovC2k(eFh=wQ0I(sFwBv%bShqc6AGrYN+$$ zTGrzQXj)4qESF6GskShq7C{dOB{$3VToz^?bRX1#JVmd~#K4Q-bIf$;EzW(W-8A4m zoT>j8{SJ$(#rQxAu2tRz22j6fU3P`z*<9u*7cqvwngQbM;WAfu_kcO8cU;A#Pr-VE z1@LU0wc&j))ma0%#m-W|6bsDU5D)w?YTP)og{q_IFZjn%#i}}Rl*f@}R2{|O7*`Yv z!GcI_Ky`&WW!CXLEKxqu@6CL32hH zz_aWO@{;#!m={Q1DlaRTqGD*#??TADqM4iT0(`Bt@C(APt|;bE5}=1YlAxfVs()AQ z6c=qC%s&J5>Yt%6Bcgo|WS^_*U%~HFD5^h>eDR^2jVqn%Rqd?yef49&`bhmmds927 zomH=Pq~$Mu_MG9^*fZ337#thAzyGD-1H1MO>H^l}(I_@Q2`VpvndFe^gD~71V4+|( z4*(1U2e>j=6#zyRI6*pWcLbO_GUyISCC?<}V9~U2AoFAf7PPI9UQBjjvKx{($SKx= zoD-$O+fPBVpW}gp7PwbIqNDay3(W>(4!@i~;NRnU?(Jm)H^u+C_Y@CC9X~w2bf|yt zV3em)98`HaHc`1uL9Q8zP?_sa;2H1=fb)Vpd#1Na{K9dQ;!`A<0)Mi@Q)b^-pTy;cpbseYrPRtmN;sGxrBI z2DzA*z?QTQCj_iZ(O^;c#xhe^ywp@go&^%*IZS#Wfte$S*kjHO0LI+7ke$P7RcS(k zSp+8sxI+|xU=XB}$&3uJX+Q&mRJ5k*UeIjSL+A5Mz2nI?gTG7{z%RBr+UyXusr3qn zf{NOD(0*waD`zuzSOiJLFzJIt_ZzpZ2dukx_@cs0GT(@p4r^66*^dOMaQ*^9gP82Y z%75At{mW|WNGbG+ z`dhDRq1P%UdNficXVAyebg5^0@AP(*>%|SKu>XU}a%ZpB*$bfB*<0@1uXXN67#aZ( z_m>4cXU!T`EYTu^t&|a?=+F*yDLBLEQusQ;21TnI06^VvdF`4JcK<_o`08l6XF%&2 zDEAC$Jwr3$VO1D@dy5(#E{OKEUweUbrfD#7P~aD3*f8AX+_u70w04_!kCr1O` zRWq2!4@CWA;6`1%A9(Q{dcA;tdRSyoR$=aMx?13^Fvl`aKV}7a2V2PX%~f9&_0z^rB;qHf31laNV`}S(QYfj8pVUIB$}h|Qo^A0HU4&#)rJ^BB_I;q3Q+3#m(hA3( z+tn>46qKK!EE*w9KDkZ~S>mdH(GE6C;5I*Ghh;NdW!bc!VE&PGs{VE#w`Z|!#Ptts zBrWB8!I88uQH^|p3qr8M_!q#n*7XJOtW`^{wTz1p z%IXMTwRn<)k_&g5Gfc)|T$IK8oKY6`dd+bMZt_GOozc*@Gs-@fU(1Y!=bcd(*~Iup z?`1~ozn#%v-;9Ea1(F2Xc+@`uv)|7R1*=kLf}m&Z9LN=)XukvI!QYFdvN0K)gBbMe zP^Uy^HCWToYIO9$*!zs#9MTC;@W4jIY%H(9C2R{{9t|NC_j!?EcpuyZjjqC&YDeJn zeHtFCZ{!HO!i(l({_0l%RTeg(K!7tmXEfxHtlE5nNLt;K+^Tsh+TN=%0;uxnbVW?UT{V1D6|tpK{F>&WkmiLN7Tbw3}j!Dm*>Cx7@Z-YugB*+P1OW_O#aaG{EO~s*sCMmjyg$jS5yOVTplO z$|zA}Xa=GboMA*Md>vtfp4ANipnX$ZsS3L<_mtOmYiqj!RM&Qw*Y4KV?nW3I0T6eW z1w3cX8dfaPB7?1z5u@nP4sLm0Cnn!?wps6#gt-3>9Ce8xS$SeVS$WWd!xSQw^MR#et=t6LA!-W&*cK5i1Q*;E0ipk3WnGFrY|UL>_Q2Llr$%mn zO>S$9g$e7Wa|?wVu4Nl_SPtCqtk97K@T@Qqx!KU%MR~d0QV^4D<7wTZXR^+;)|z>B zkU=rvv^hps4CcZ6&8dF`M#fL58|J5=5U^d4bM1ko#zxW&-RQ;%vK1lJ324kDi|DVk zEb5JYn(?8hvvNUjL*YNXtxNnQpKX~NO z%fmza29F%xOVFQB(9Oa`+?@#R!URJA+;=umpDx*hNd1_s#pE$e=8eE4=+PkqkbsQ7 z0paH{K@X~17^dy%WzzNm#IyXs3Ob-Fe7H>J-c==Y+uIKmYg2l~egoUX4jU5DHAQ|K zD5J|r`7VSb7|y4DwRLYPH1H*_g}&nyxNiyfxAj1TK*#kE6?qwU2Cgp)c+MIXtW=^U z23slgxeR*)5gXb~&}wGYH^tSe(5Z^6FDF&8vn=2_YgDjOiIx~_rO@Yc(rPzBs~G_3 zb`!m{o57X(K|mEZmjyg$jS5yO(Gr8L6#9G+u-Z+~Y6bwhQH`M;RD;VqRL5QnT4C+G zfReSxf&s990^vNM(>JT5PE!`2eIp@XoIf{LpLv0m22Z z`yU`cz2NRjzF^7GJa@tC=A$fPIHQXd5kr2I-<4}*pCiZ%O$k*;g@qUQg3)8#Wkt8| z1>|oV(`A=~jS2k3H$Sdz4PF4x4v6&|p@<$k?%&bZ#a3tg5vw=@1V)|r;vSB-YTZZI zZDbump`6W|?B}2(|1RnsiyrwEj{A2R`CO|xFXsJ2Q0TlfaCZ-wiQxQw>?c?N?;*_u zSnR~Xd>1_GoB@5{68sEU^*Qyg!KC*ou+f>THfWYpgXY8;d^kwGY<9D$2f-2RUMON7~X0K!{cZe9zPiMFsI`b zAVglpw@e^+$(9l(oFc6DjdUDu45$;K}|eX7VL8t8(Brk24{q0 z(1BH72e`5i)B`RxZ$UsO{I>*d2^x$>^jh;jEa)8RDy`g(v*)%ar$jQ1h6n4pi5MIDL= zq4Uy7vVs)+Uxbo7nat7GP8ZTKa#9afVCX@1SJ*`VSb}a@rMrA+U?7cQqV9ES!lhCr zeOs<@bd>13cBl3k7LxTWxd^uduMnra3dw(ad7l3#j(dwc7y6|j@}6IEX7Y;`j^FXm zp(Phzx$sIk^n@0A;%xA?FUUu4x7PC8Z#M_{)wf$jD7N_^X{zPd-fs5sTW>ds{PzE8 L^YbyDUjF|8!k*k2 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_intent_router_phrase_matrix.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_intent_router_phrase_matrix.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6baa84209984e79dea3124b2acd7a61cf1c38797 GIT binary patch literal 35062 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 IZ!c5)zZDNR4gdfE literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_intent_router_v2.cpython-312.pyc b/tests/rag/__pycache__/test_intent_router_v2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7c0380fe20a7be8d7d023e6129e42c80fb84305 GIT binary patch literal 6518 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 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_intent_router_v2_local_flow.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_intent_router_v2_local_flow.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50f714ae8ebef44888cb09827d4b851c4617c146 GIT binary patch literal 20416 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 F{|{U$)rSB8 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_intent_router_v2_local_flow.cpython-312.pyc b/tests/rag/__pycache__/test_intent_router_v2_local_flow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5b7221946369ec84639527f435e0d4d080ec902 GIT binary patch literal 8052 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 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_layered_gateway.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_layered_gateway.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49c720a33a7571ecd6255b9f62998c1024194749 GIT binary patch literal 7062 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 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_query_normalization.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_query_normalization.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59302d0197c682099f17b5f8b9d4532d393693e5 GIT binary patch literal 9860 zcmeHNUu@gP87C!5mPE;kk|uT9ZQ+8%wwy#xoPSGDrz>I&Lx6ecuwsD=jG;x^p)FC` zQLRGzTth#(sUHq;+~1LLFP}rKz753BIF(cR z3^&Bn+?VyIeLR;Hh5|gR2@VCJ#+PZ!hK52srv=A$xQZJ2*mDiqrG`W`toq*+hN6%Q zkjEeoK;8~{5b|x1w?V!g@(|?lDLxVTCyEayczfHcMU6}i=15k_=p~J$UA>sM!OsuB z)gSWs7YdNF>z0#OxH30}E!JHsf6HlG;ZQyO%b$6}X)hpF2l5|0mipN03 zIt7mSL&8m>7v}i~zW)AszO>^&cYf+f!j$^^B_xj}Ogmz@vq+uy*?y?U%qm|*WiDfy-G&A+&iF6 z$a#_*)6xY~p6KgUbV()pgl0%eJ}*H{S~JaFtQggdLN5-4*-MmBO3B~DmsEDgiW;2E zPuV?myw+A~L{8^286|IOraXB`hbDO?t&zQ882&4B5|U}|uW_js-#2~!e*DFH{6IB+ z!0H^W#VT>TEFdtiYU!FSD;v zzYisvd4h{6!Tp84Y6RDIf{QhYVS)=R>42o^eIj21e#0rR7&$|SB?0y@6JD1bOnb5q z1h%7+4X6BfYViv=8WIHX`Bnqjq0ToVf^FXwPR7_^&5?U+XnK zPc+zECSbA;E9voqo*sv7MI*zoH)P3vFpa)F8&3Rh*W$0?#7|j53MYQ5DyHfJ-pft_ z ztvQMBmvg?sk&JDPWaY_EiZGIJQfwbAo`cb~Zdf0|DR8>_Q#@+)rgKDt#2C>>;Ru1a z?A|Rkmo|H$#T#_H`-pCN0Q5j^A(>V0-7W2SSaP1XeY)Wu1&_i}P&eG8AjNd-40x2_ z654=vMMQCr?ynSa%#-p{aNrCYxdP~ho_6C&6MJGMZg0gGK;;J3OIg)9YLabe2f{&u zuRPg>2_C=6PE63VLUv>FJS6i$vIpt)$G{^5`yyY!9+-nJkEphMM8B07|7w}$o3K&}s|L z=*^8@5>9U+8JLmo)qwA7yk}cT#u=dhb4<8}WVolZ|85}}@PI!>By$qpFYA49PA_<9 zhN1{FucZN_NQWfrH{jq#C)MvL&dDmefdJ>rrD$_Q(g})4Hzv4d5%gL$&`C)|pyP;^ zLR2|G2yzfx9fqV7qLqM5ND|A?QT7lBIJW@^C>rTPkveZ>^vUYRsI2DT1P^dV9wwSH z4i4#pW|-iPHpwy2jNO_)*a!-Iw-%QX3hc3ju9>s5Lv(wC9jZ{8N+f zqE+n7R~8_4Mr9gjOI3Jku`|CLlUlR;xAJ&+Ey0f=JjLjp=bhQq0<{vkCiqHn0qKFcW zr`ROZYP{QAyZ-lMy@{nE*V0Yeh23^)^twP}sG=HE+smT!?4pnT?4qtFH+MV^Z4gzB zz%Nt@tJ}7oBgDqnyfd3TpJ(zpg5P8NV6hK2P6&uoyZ{lbPXHI(wtfqqj&G%28Tdwu zIyKPs(TofviTdah4+ppVAZQ&ZOzLUP7IH>fOZW(Wfq;SRzEX2^`#6hkAGc$mxR`}N zF5T@Zd;q;)Z{fc=8fe>~=r1AFkPLOr}59$-~oFI09cA1D#Ux@7|$|OM;MowVzB6dq7 z8h5^L&(p7 zj|afBGN~8fqW}o+8$?fE!pA^Tpbu>~-&=#GRw8Go&)pYy)y2J4aHjZc;{NG@mFVtz zRH{a$x#w!p&gs;Bar?UiZvoWlfO}c!m^tw-#QKo&?kbituGS#E>P4Z$6@hA(^s0dX z1uNnUmXMs=|Er!GJwV*(`QQ*_GcV5_vczOvzJ&arg z@{rfuBS7(rxC7DK{`Y&XLHC{4dVUVwcg~!eh3=c+AMhT&n>p2t?0}{Uyy{u#9wnDR zEp(6MB9M+eD`4qggiZ>v5>gNYq>$o<-by6NmaGW$Lx$>tD%?FYYWHzT3Gn$!%uPTzdL1znrX#yWiO!Ot!d|Sc3O%bvR8Mk9= zn%WVj5v%AY6*S0918&rLM`1giZSx%cOLX2o2))EQKTBSPNeEMrGhc?}Uw)qF|Hg4| za&N@`!JWS!+xgDxZ@pfRbyj1Y(~;F+fd3}Hx;?VUeIgnoDm=m|T47o8eNV zXvWPUL-4&fZ|1$(nfIAD|MYlV6tvB&w~`$|{Rej{jkAf84#b<3Ov!Yb%Fr`3jcF#$ zWY`%t!_9CR$BcvU*t9d#G}8n$r#R*r+3_kj(=4~jO@LbfHv@J7ZUM{#b^&$+<^g+T zx6%r7t@E7hfxdUu8T9@ae?pM9n#ZMuQE62{LDuqxv+A;fG)Yfp)d^kF6{~ev)fH7A z&*pUnJwIUa<4P_I({yD;2Wjum@(NmwWl=^-C$A{Taw0{~BQ;@fkRUfe7XGCU8t+yI zSZ*@CK+BBGE-*S%;iW2PQ(*wE8FkjMZL(&x`H`c<9*1Yt$l|Z8Yc->-bwkjt8Et#y zsQ=;7h2}qUTlh?%O;ABc%yQ;d^~J1eap8C*YH^Wxcmgcal9tqkytJTbmQPxil4)r! zt%!0qBPCVMYOAu+*o0cph429(tl4VAn#5>qa{SW8_;74;!fGd+iA!V8#Ye^P;N;+F z{G9a|VMd}Cqp?Ud7QQ5&8y~!Q-tw2{#;3-^Q89deDs}VAPSaxVK>YOb)8m0?ija?J3yEPAl^0Zn%4opZ(bK%cx>CaZC*343Bt4>)u#R$qKW4R7GsD_p`9Jhjij$wgN0|P1uY?e+-IZe^Tmlu;TB_|~m)CB_Hg})X8 z00#64MPFB8>eL8xtPJnhy@xOUI*1#pF+z+=)YE+fA3rcbQz*9)Ca@eAgrFHwlY=i}tGq z6Bo-r0lE*_#`v3Y7stz8<2~~g;U!s_7v}QGwA{NA{LxDgI@l|}xxf`~orbS5#z5l$ z$st8k&@u$boRnD7lZhowgll*Xl3xblFF6f=?I{2ajWSvEP1#0?-s7Vt7v1B(w*ki= zGP#h!haNt2e3_A?wF5-sGnZ*dOAQm(S~YPox&suDw#HP_Sq+a);Y3o`^n*(}#n%l% z7s`S1kCj=Od&^nTT8|H(6B6d|D~jQOZg#4fSDvl9cWuNo)#&Dhl5y z)teq9p^%?XE{sVkRG0|75Z^FRL*l5m9EjBX9MGiPvRviT40NkjlcXjVvq-ZzxXx9} zq3IIREml!wi%lzPkb{Q+5k~`97rY9zd|J1hwis3lUT@b8J;d$H^TqNE5v$e2`J^sF zsEBjfyeezxry%wd_-lCp;OU;uqUZ6#(4I>NPxrLnn0;gR=E?iV&lHajyf^lzc+nFn z41MB1vg+rw{7zK3rrYDWPk9iki=8 z<<|uEvO}|M@3PIdJb>^S5Z57Z5UcP13G`J;N>b#PQ!B3PNo`1q5pwyF{60xRi*Da6L&Ol`9)H{)?|dwb5zN)8NMG*u8R zy~@f)Y&3)rj5+^+G&7`;C4yjO7r0bbd9!!o6+U=xzHi=q^XARWuX6bzpq)Hp>o3{)D}kJg&)TEn~bBNyZaVt&R35 z1{gd2 zs|-*Cft62iCNtiu26p~3ZnwiErCF1S5RVvQiZ|GL+ziJajGLN8@dMx`gU$cAnS3); a&hy7eO}C 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 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_retrieval_statement_builder.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_retrieval_statement_builder.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d79cbb26aeaca261f1e6a3732793639efe59ce84 GIT binary patch literal 9121 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 Ymd>q*Q}S!;M-uXB`ENY|`8c)kPXy-NtpET3 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_retriever_v2_no_fallback.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_retriever_v2_no_fallback.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32afabf0c3231851dacea0f9ace72d38a9a20335 GIT binary patch literal 6530 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 KIVQa*=J#)KMbSb4 literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_retriever_v2_no_fallback.cpython-312.pyc b/tests/rag/__pycache__/test_retriever_v2_no_fallback.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6719c036aaa73132f8fc5bbb0ef9e28ed2f7f9e4 GIT binary patch literal 3405 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 VkY_iu;fD->M+evl`zBS!{{WI!Y2yF@ literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_retriever_v2_pack.cpython-312.pyc b/tests/rag/__pycache__/test_retriever_v2_pack.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfd2857a146f56fd7b10e1c1731020831840af15 GIT binary patch literal 4643 zcmcH-OKcQJwtA-L+w-$M7#lDi8~o`Y+h7v1!ekfX7(+q^);7D~uVD# z#*md2p;fM{J-~s354#c;XqDq0ojnolWd=v`sO8nJv|P9iv870y_Py%)*v2Sir7de- zy?*to>Q%jO`5)nMkU&#Pe=+|aA>=>!QEhSs*|-GAePR$pN|QNhQkrv3x+MH|rQLJ# zq%08^86}4M9x>#{E@#%HN68NheTY5fuR(0FqoNmo%IYy_<+mzce#3<7;*^FjUZgD$oW>V>S zgK9jdXSf|<`n1L==WrpKY1p1G<|)hDo-|$!bmp9CF+WJn0tirpsj9ND2SpM${5F~; zyobAPLz?Xc#vu#7oo2WmSMVFixuaR0D#3s|gZZ-IF}#NFq053>avF^I4gZtC~T%gVh9 z%;*^-O)AA@@CE8<*=@^c33iZMIdF8-|Z* z<5xfZ@Ztq+_{_wa3l~RU#SJ=@n5LG7=g}&n*|FCsfx5Jfu&?T|gPLZ}<+32EW=F>X zP1B@4N5M!_l%b(9)V&=!!(nCg-Vw&KO!eC`r|BuCAV&xRo`a!P?yy>pMx3a$SBHc% z41n0G1m`>OY1k1D}_<0OFlWQ(^+RmsyoRV zUJ3(ZOPL@NbPPV}@RzuFMCO>sq90{k5~Y)5Mp7(BBbTOgW*5sm)9cV>FlLrj=!R zVOtO&tAUfJss@gNV=xg0{vjaS4IC4^lcpd|)aF0}_q{1ejT2Q#G7NRB8$lO>9t4mp z35z4xi$F!tg5UsvWmi4)sqQTf@c|5S+!;K;G5c-mR`?bYvFCm}*XAaVy{!7>R1 zg2Dpq2xqB!65^1gjA<%z6x1A|jL>orUV$rB%lNP*RtDml(Vy=_bQmYgQj%d*Q9WBu zdRauPg!dtrml)cHh{)S4SmztT+%!+H@CiMTwninQ?p3kHbyQTeJQ3II9$pl10 z^<+@o0JY{>CZXTbO)CM3{HB?rLNCZ$ouJyW;iKC4#mi&EBiit}%gIm1-@;eeui%!| ztqMRVVV}INfEF|saZkci>i)mbx2q1`#C{f+-tIP8^qHUIHo|t`$q}j=1JqlyYLFdA z+0ifAdkAnB_6r235xkE8w`U(CNFqRwabhv1Lw2ePa3%f+0FYK(K~M0TrZRzUPxTWi96XMzH$hM;)Tms z3ZH4k%NjDoUwUy9$P~~$m7k7(7Z7M+%G^YfBO*$dOaZ7hbZFsK8Hfm5!)-mlKCUp= zUkp{s6RBEJd`a$-8|cRl1b2N*zS=$q-DyYr$vKf7 z6X5eF=fvJ_&RyxoE}Vu3bLbUEJqotf%d)E zD|;aZ7jkH~On4|eTfp0@zVtO%VjO<_&j6q#jCHTa4*V&y-rBd?I_GP3y5b0zY%wHECyMtfHk_0g}NDg8ycf8C)biqXWX(y;{8?xNgX zp|%&J?W;;+>C7{wt0;F>s2#;<$Ewn~q(hNklzU1_bWLe1Ds2z+qLNqxPNl1$4n0$b ziteVDq#T Jl1_<3{1>IPdaM8d literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_retriever_v2_production_first.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_retriever_v2_production_first.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7bb833e829b0fd07bc6808095a6f73986695d7d0 GIT binary patch literal 9768 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 NyS|NQU!<3R{|&A`tuO!p literal 0 HcmV?d00001 diff --git a/tests/rag/__pycache__/test_trace_builder.cpython-312-pytest-9.0.2.pyc b/tests/rag/__pycache__/test_trace_builder.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0eb654d7bf00fe0359e2dd812018ac5f00c6214 GIT binary patch literal 7840 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 T{qIYOw+LY0mXgvfck=%L3d>4F literal 0 HcmV?d00001 diff --git a/tests/rag/asserts_intent_router.py b/tests/rag/asserts_intent_router.py new file mode 100644 index 0000000..6df58c8 --- /dev/null +++ b/tests/rag/asserts_intent_router.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import re + +from app.modules.rag.intent_router_v2.models import IntentRouterResult + + +def assert_intent(out: IntentRouterResult, expected: str) -> None: + assert out.intent == expected + + +def assert_domains(out: IntentRouterResult, expected: list[str]) -> None: + assert out.retrieval_spec.domains == expected + + +def assert_has_file_path(out: IntentRouterResult, path: str) -> None: + assert any(anchor.type == "FILE_PATH" and anchor.value == path for anchor in out.query_plan.anchors) + + +def assert_path_scope(out: IntentRouterResult, file_path: str, dir_path: str | None = None) -> None: + scope = list(getattr(out.retrieval_spec.filters, "path_scope", []) or []) + assert file_path in scope + if dir_path is not None: + assert dir_path in scope + + +def assert_file_only_scope(out: IntentRouterResult, file_path: str) -> None: + scope = list(getattr(out.retrieval_spec.filters, "path_scope", []) or []) + assert scope == [file_path] + + +def assert_spans_valid(out: IntentRouterResult) -> None: + raw_len = len(out.query_plan.raw) + for anchor in out.query_plan.anchors: + if anchor.source == "conversation_state": + assert anchor.span is None + continue + assert anchor.span is not None + assert 0 <= anchor.span.start < anchor.span.end <= raw_len + + +def assert_test_policy(out: IntentRouterResult, expected: str) -> None: + assert getattr(out.retrieval_spec.filters, "test_policy", None) == expected + + +def assert_sub_intent(out: IntentRouterResult, expected: str) -> None: + assert out.query_plan.sub_intent == expected + + +def assert_no_symbol_keyword(out: IntentRouterResult, forbidden: set[str] | None = None) -> None: + denied = forbidden or {"def", "class", "return", "import", "from"} + symbols = {anchor.value.lower() for anchor in out.query_plan.anchors if anchor.type == "SYMBOL"} + assert symbols.isdisjoint({token.lower() for token in denied}) + + +def assert_domain_layer_prefixes(out: IntentRouterResult) -> None: + prefixes = {layer.layer_id[0] for layer in out.retrieval_spec.layer_queries if layer.layer_id} + if out.retrieval_spec.domains == ["CODE"]: + assert prefixes <= {"C"} + elif out.retrieval_spec.domains == ["DOCS"]: + assert prefixes <= {"D"} + else: + assert prefixes <= {"C", "D"} + + +def assert_no_symbol_leakage_from_paths(out: IntentRouterResult) -> None: + file_values = [anchor.value for anchor in out.query_plan.anchors if anchor.type == "FILE_PATH"] + if not file_values: + return + parts: set[str] = set() + for value in file_values: + for token in re.split(r"[/.]+", value.lower()): + if token: + parts.add(token) + for anchor in out.query_plan.anchors: + if anchor.type == "SYMBOL": + assert anchor.value.lower() not in parts diff --git a/tests/rag/intent_router_testkit.py b/tests/rag/intent_router_testkit.py new file mode 100644 index 0000000..257df03 --- /dev/null +++ b/tests/rag/intent_router_testkit.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from app.modules.rag.contracts.enums import RagLayer +from app.modules.rag.intent_router_v2 import ConversationState, IntentRouterV2, RepoContext + + +def repo_context() -> RepoContext: + return RepoContext( + languages=["python"], + available_domains=["CODE", "DOCS"], + available_layers=[ + RagLayer.CODE_ENTRYPOINTS, + RagLayer.CODE_SYMBOL_CATALOG, + RagLayer.CODE_DEPENDENCY_GRAPH, + RagLayer.CODE_SOURCE_CHUNKS, + RagLayer.DOCS_MODULE_CATALOG, + RagLayer.DOCS_FACT_INDEX, + RagLayer.DOCS_SECTION_INDEX, + RagLayer.DOCS_POLICY_INDEX, + ], + ) + + +def run_sequence(queries: list[str], *, router: IntentRouterV2 | None = None, trace_label: str = "intent-router") -> list: + active_router = router or IntentRouterV2() + state = ConversationState() + results = [] + for index, query in enumerate(queries, start=1): + result = active_router.route(query, state, repo_context()) + print_trace(index, query, result, label=trace_label) + results.append(result) + state = state.advance(result) + return results + + +def run_single(query: str, *, router: IntentRouterV2 | None = None, trace_label: str = "intent-router"): + result = run_sequence([query], router=router, trace_label=trace_label)[0] + return result + + +def print_trace(index: int, query: str, result, *, label: str = "intent-router") -> None: + print(f"[{label}][turn {index}] input: {query}") + print() + print(f"[{label}][turn {index}] output: {result.model_dump_json(ensure_ascii=False)}") + print("=" * 50) diff --git a/tests/rag/test_code_indexing_pipeline.py b/tests/rag/test_code_indexing_pipeline.py index 9ba4bf9..2f1a35b 100644 --- a/tests/rag/test_code_indexing_pipeline.py +++ b/tests/rag/test_code_indexing_pipeline.py @@ -55,3 +55,21 @@ def test_code_pipeline_indexes_import_alias_as_symbol() -> None: alias_doc = next(doc for doc in docs if doc.layer == RagLayer.CODE_SYMBOL_CATALOG and doc.metadata["qname"] == "ConfigManager") assert alias_doc.metadata["kind"] == "const" assert alias_doc.metadata["lang_payload"]["import_alias"] is True + + +def test_code_pipeline_marks_test_documents() -> None: + pipeline = CodeIndexingPipeline() + content = """ +def test_user_service(): + assert True +""" + + docs = pipeline.index_file( + repo_id="acme/proj", + commit_sha="abc123", + path="tests/test_users.py", + content=content, + ) + + assert docs + assert all(doc.metadata["is_test"] is True for doc in docs) diff --git a/tests/rag/test_explain_intent_builder.py b/tests/rag/test_explain_intent_builder.py new file mode 100644 index 0000000..f386561 --- /dev/null +++ b/tests/rag/test_explain_intent_builder.py @@ -0,0 +1,22 @@ +from app.modules.rag.explain.intent_builder import ExplainIntentBuilder + + +def test_explain_intent_builder_extracts_route_symbol_and_file_hints() -> None: + builder = ExplainIntentBuilder() + + intent = builder.build("Explain how /users/{user_id} reaches UserService.get_user in app/api/users.py") + + assert "/users/{user_id}" in intent.hints.endpoints + assert "UserService.get_user" in intent.hints.symbols + assert "app/api/users.py" in intent.hints.paths + assert intent.expected_entry_types == ["http"] + assert intent.include_tests is False + assert intent.depth == "medium" + + +def test_explain_intent_builder_enables_tests_when_user_asks_for_them() -> None: + builder = ExplainIntentBuilder() + + intent = builder.build("Покажи как это тестируется в pytest и какие tests покрывают UserService") + + assert intent.include_tests is True diff --git a/tests/rag/test_intent_router_e2e_flows.py b/tests/rag/test_intent_router_e2e_flows.py new file mode 100644 index 0000000..8f76ad7 --- /dev/null +++ b/tests/rag/test_intent_router_e2e_flows.py @@ -0,0 +1,126 @@ +import os + +import pytest + +from app.modules.rag.intent_router_v2 import GigaChatIntentRouterFactory +from app.modules.shared.env_loader import load_workspace_env +from tests.rag.asserts_intent_router import ( + assert_domains, + assert_file_only_scope, + assert_intent, + assert_test_policy, +) +from tests.rag.intent_router_testkit import run_sequence + +pytestmark = pytest.mark.intent_router + + +def _live_gigachat_enabled() -> bool: + load_workspace_env() + return os.getenv("RUN_INTENT_ROUTER_V2_LIVE", "").strip() == "1" and bool(os.getenv("GIGACHAT_TOKEN", "").strip()) + + +def test_e2e_path_carryover_flow() -> None: + first, second, third = run_sequence( + [ + "Посмотри файл app/core/config.py", + "Теперь объясни функцию load_config", + "Почему так?", + ] + ) + + assert_file_only_scope(first, "app/core/config.py") + assert "app/core/config.py" in second.retrieval_spec.filters.path_scope + assert "app/core/config.py" in third.retrieval_spec.filters.path_scope + second_file_anchors = [anchor.value for anchor in second.query_plan.anchors if anchor.type == "FILE_PATH" and anchor.source == "conversation_state"] + assert second_file_anchors == ["app/core/config.py"] + assert "app/core/config.py" in second.query_plan.keyword_hints + assert "app/core" not in second.query_plan.keyword_hints + assert any(anchor.type == "FILE_PATH" and anchor.source == "conversation_state" and anchor.span is None for anchor in third.query_plan.anchors) + carried_symbols = [anchor.value for anchor in third.query_plan.anchors if anchor.type == "SYMBOL" and anchor.source == "conversation_state"] + assert carried_symbols in ([], ["load_config"]) + assert third.query_plan.sub_intent == "EXPLAIN_LOCAL" + layer_ids = [item.layer_id for item in third.retrieval_spec.layer_queries] + assert "C3_ENTRYPOINTS" not in layer_ids + + +def test_e2e_docs_switch_from_code_topic() -> None: + first, second = run_sequence( + [ + "Объясни как работает ConfigManager", + "А что про это сказано в документации?", + ] + ) + + assert_intent(first, "CODE_QA") + assert_intent(second, "DOCS_QA") + assert second.conversation_mode == "SWITCH" + assert_domains(second, ["DOCS"]) + carried = [ + anchor + for anchor in second.query_plan.anchors + if anchor.type == "SYMBOL" and anchor.value == "ConfigManager" and anchor.source == "conversation_state" + ] + assert carried + assert carried[0].span is None + assert "ConfigManager" in second.query_plan.expansions + assert "ConfigManager" in second.query_plan.keyword_hints + + +def test_e2e_tests_toggle_flow() -> None: + first, second = run_sequence( + [ + "Покажи тесты для ConfigManager", + "А теперь не про тесты, а про прод код", + ] + ) + + assert_intent(first, "CODE_QA") + assert_intent(second, "CODE_QA") + assert_test_policy(first, "INCLUDE") + assert_test_policy(second, "EXCLUDE") + assert first.query_plan.sub_intent == "FIND_TESTS" + assert second.query_plan.sub_intent == "EXPLAIN" + assert "tests" in second.query_plan.negations + assert not second.query_plan.expansions + assert second.evidence_policy.require_flow is False + + +def test_e2e_open_file_then_generic_next_steps_is_lightweight() -> None: + first, second = run_sequence( + [ + "Открой файл app/core/config.py", + "Что дальше?", + ] + ) + + assert_file_only_scope(first, "app/core/config.py") + assert_file_only_scope(second, "app/core/config.py") + assert second.query_plan.sub_intent in {"EXPLAIN_LOCAL", "NEXT_STEPS"} + layer_ids = [item.layer_id for item in second.retrieval_spec.layer_queries] + assert "C3_ENTRYPOINTS" not in layer_ids + assert second.evidence_policy.require_flow is False + assert "app/core/config.py" in second.query_plan.keyword_hints + + +@pytest.mark.skipif( + not _live_gigachat_enabled(), + reason="requires RUN_INTENT_ROUTER_V2_LIVE=1 and GIGACHAT_TOKEN in environment or .env", +) +def test_intent_router_live_smoke_path_carryover() -> None: + router = GigaChatIntentRouterFactory().build() + first, second = run_sequence( + [ + "Открой файл app/core/config.py", + "Что дальше?", + ], + router=router, + trace_label="intent-router-live", + ) + + assert_file_only_scope(first, "app/core/config.py") + assert "app/core/config.py" in second.retrieval_spec.filters.path_scope + assert second.query_plan.sub_intent in {"EXPLAIN_LOCAL", "NEXT_STEPS"} + layer_ids = [item.layer_id for item in second.retrieval_spec.layer_queries] + assert "C3_ENTRYPOINTS" not in layer_ids + assert second.evidence_policy.require_flow is False diff --git a/tests/rag/test_intent_router_invariants.py b/tests/rag/test_intent_router_invariants.py new file mode 100644 index 0000000..361fd44 --- /dev/null +++ b/tests/rag/test_intent_router_invariants.py @@ -0,0 +1,120 @@ +import pytest + +from tests.rag.asserts_intent_router import ( + assert_domain_layer_prefixes, + assert_domains, + assert_file_only_scope, + assert_has_file_path, + assert_intent, + assert_no_symbol_keyword, + assert_no_symbol_leakage_from_paths, + assert_spans_valid, + assert_sub_intent, + assert_test_policy, +) +from tests.rag.intent_router_testkit import run_sequence + +pytestmark = pytest.mark.intent_router + + +def test_invariant_code_file_path_with_canonical_key_term() -> None: + result = run_sequence(["Уточни по файлу app/core/config.py"])[0] + + assert_intent(result, "CODE_QA") + assert_has_file_path(result, "app/core/config.py") + assert_file_only_scope(result, "app/core/config.py") + key_terms = [anchor.value for anchor in result.query_plan.anchors if anchor.type == "KEY_TERM"] + assert "файл" in key_terms + assert "файлу" not in key_terms + assert_spans_valid(result) + assert_domain_layer_prefixes(result) + + +def test_invariant_open_file_for_specified_file_phrase_uses_narrow_layers() -> None: + result = run_sequence(["Уточни по файлу app/core/config.py"])[0] + + assert_intent(result, "CODE_QA") + assert_sub_intent(result, "OPEN_FILE") + assert_file_only_scope(result, "app/core/config.py") + layer_ids = [item.layer_id for item in result.retrieval_spec.layer_queries] + assert layer_ids == ["C0_SOURCE_CHUNKS"] + assert result.evidence_policy.require_flow is False + + +def test_invariant_inline_code_span_routes_to_code_and_extracts_symbol() -> None: + result = run_sequence(["Уточни по коду `def build(x): return x`"])[0] + + assert_intent(result, "CODE_QA") + assert_spans_valid(result) + assert_no_symbol_keyword(result) + symbols = [anchor.value for anchor in result.query_plan.anchors if anchor.type == "SYMBOL"] + key_terms = [anchor.value for anchor in result.query_plan.anchors if anchor.type == "KEY_TERM"] + assert "build" in symbols + assert "def" in key_terms + + +def test_invariant_docs_cyrillic_path_with_quotes() -> None: + result = run_sequence(["Что сказано в «docs/архитектура.md»?"])[0] + + assert_intent(result, "DOCS_QA") + assert_sub_intent(result, "EXPLAIN") + assert_domains(result, ["DOCS"]) + assert "docs/архитектура.md" in result.query_plan.normalized + assert_has_file_path(result, "docs/архитектура.md") + assert any(anchor.type == "DOC_REF" for anchor in result.query_plan.anchors) + assert result.retrieval_spec.filters.doc_kinds == [] + assert_spans_valid(result) + assert_domain_layer_prefixes(result) + + +def test_invariant_file_check_phrase_not_project_misc() -> None: + result = run_sequence(["Проверь app/modules/rag/explain/intent_builder.py и объясни"])[0] + + assert_intent(result, "CODE_QA") + assert_domains(result, ["CODE"]) + assert_no_symbol_leakage_from_paths(result) + assert_domain_layer_prefixes(result) + + +def test_invariant_tests_include_routing() -> None: + result = run_sequence(["Где тесты на ConfigManager?"])[0] + + assert_intent(result, "CODE_QA") + assert_test_policy(result, "INCLUDE") + symbols = [anchor.value for anchor in result.query_plan.anchors if anchor.type == "SYMBOL"] + key_terms = [anchor.value for anchor in result.query_plan.anchors if anchor.type == "KEY_TERM"] + assert "ConfigManager" in symbols + assert "тест" in key_terms + + +def test_invariant_keyword_hints_and_expansions_for_function_identifier() -> None: + result = run_sequence(["Теперь объясни функцию load_config"])[0] + + assert_intent(result, "CODE_QA") + assert "load_config" in result.query_plan.keyword_hints + assert "функция" not in result.query_plan.keyword_hints + assert "def" not in result.query_plan.expansions + + +def test_invariant_open_file_sub_intent_uses_narrow_retrieval_profile() -> None: + result = run_sequence(["Открой файл app/core/config.py"])[0] + + assert_intent(result, "CODE_QA") + assert_sub_intent(result, "OPEN_FILE") + assert_file_only_scope(result, "app/core/config.py") + layer_ids = [item.layer_id for item in result.retrieval_spec.layer_queries] + assert "C0_SOURCE_CHUNKS" in layer_ids + assert "C1_SYMBOL_CATALOG" not in layer_ids + assert "C2_DEPENDENCY_GRAPH" not in layer_ids + assert "C3_ENTRYPOINTS" not in layer_ids + assert result.evidence_policy.require_flow is False + + +def test_invariant_docs_question_routes_to_docs() -> None: + result = run_sequence(["Что сказано в документации?"])[0] + + assert_intent(result, "DOCS_QA") + assert_domains(result, ["DOCS"]) + assert_domain_layer_prefixes(result) + assert result.query_plan.keyword_hints + assert any(item in result.query_plan.expansions for item in result.query_plan.keyword_hints) diff --git a/tests/rag/test_layered_gateway.py b/tests/rag/test_layered_gateway.py new file mode 100644 index 0000000..49cf6ce --- /dev/null +++ b/tests/rag/test_layered_gateway.py @@ -0,0 +1,78 @@ +from app.modules.rag.explain.layered_gateway import LayeredRetrievalGateway + + +class _Embedder: + def embed(self, texts: list[str]) -> list[list[float]]: + return [[0.1, 0.2]] + + +class _RetryingRepository: + def __init__(self) -> None: + self.calls: list[dict] = [] + + def retrieve(self, *args, **kwargs): + self.calls.append(kwargs) + if kwargs.get("exclude_path_prefixes"): + raise RuntimeError("syntax error at or near ')'") + return [ + { + "path": "app/users/service.py", + "content": "def get_user(): pass", + "layer": "C1_SYMBOL_CATALOG", + "title": "get_user", + "metadata": {"symbol_id": "symbol-1"}, + "distance": 0.1, + "span_start": 10, + "span_end": 11, + } + ] + + def retrieve_lexical_code(self, *args, **kwargs): + self.calls.append(kwargs) + if kwargs.get("exclude_path_prefixes"): + raise RuntimeError("broken lexical filter") + return [ + { + "path": "app/users/service.py", + "content": "def get_user(): pass", + "layer": "C0_SOURCE_CHUNKS", + "title": "get_user", + "metadata": {"symbol_id": "symbol-1"}, + "span_start": 10, + "span_end": 11, + } + ] + + +class _RecordingRepository: + def __init__(self) -> None: + self.calls: list[dict] = [] + + def retrieve(self, *args, **kwargs): + self.calls.append(kwargs) + return [] + + def retrieve_lexical_code(self, *args, **kwargs): + self.calls.append(kwargs) + return [] + + +def test_gateway_retries_without_test_filter_on_vector_failure() -> None: + gateway = LayeredRetrievalGateway(_RetryingRepository(), _Embedder()) + + result = gateway.retrieve_layer("rag-1", "Explain get_user", "C1_SYMBOL_CATALOG", limit=3, exclude_tests=True) + + assert len(result.items) == 1 + assert "layer:C1_SYMBOL_CATALOG retrieval_failed:retried_without_test_filter" in result.missing + + +def test_gateway_honors_debug_disable_test_filter(monkeypatch) -> None: + monkeypatch.setenv("RAG_DEBUG_DISABLE_TEST_FILTER", "true") + repository = _RecordingRepository() + gateway = LayeredRetrievalGateway(repository, _Embedder()) + + gateway.retrieve_layer("rag-1", "Explain get_user", "C1_SYMBOL_CATALOG", limit=3, exclude_tests=True) + + assert repository.calls + assert repository.calls[0]["exclude_path_prefixes"] is None + assert repository.calls[0]["exclude_like_patterns"] is None diff --git a/tests/rag/test_query_normalization.py b/tests/rag/test_query_normalization.py new file mode 100644 index 0000000..828c948 --- /dev/null +++ b/tests/rag/test_query_normalization.py @@ -0,0 +1,63 @@ +import pytest + +from app.modules.rag.intent_router_v2.normalization import QueryNormalizer + +pytestmark = pytest.mark.intent_router + + +def test_query_normalizer_collapses_whitespace() -> None: + normalizer = QueryNormalizer() + + normalized = normalizer.normalize(" Объясни как работает \n класс X ") + + assert normalized == "Объясни как работает класс X" + + +def test_query_normalizer_canonicalizes_quotes() -> None: + normalizer = QueryNormalizer() + + normalized = normalizer.normalize('Уточни «текст» и “текст”') + + assert normalized == 'Уточни "текст" и "текст"' + + +def test_query_normalizer_preserves_backticks_verbatim() -> None: + normalizer = QueryNormalizer() + + normalized = normalizer.normalize("Уточни по коду `def build(x):` ") + + assert normalized == "Уточни по коду `def build(x):`" + + +def test_query_normalizer_preserves_latin_and_cyrillic_file_paths() -> None: + normalizer = QueryNormalizer() + + normalized = normalizer.normalize("Сверь app/core/config.py и «docs/руководство.md»") + + assert "app/core/config.py" in normalized + assert "docs/руководство.md" in normalized + assert "config. py" not in normalized + assert "руководство. md" not in normalized + + +def test_query_normalizer_punctuation_spacing_does_not_break_extensions() -> None: + normalizer = QueryNormalizer() + + normalized = normalizer.normalize("Проверь docs/spec.md , затем app/main.py !") + + assert "docs/spec.md" in normalized + assert "app/main.py" in normalized + assert "spec. md" not in normalized + assert "main. py" not in normalized + + +def test_query_normalizer_idempotent_and_without_enrichment() -> None: + normalizer = QueryNormalizer() + raw = ' Прочитай «README.md» и docs/spec.md ' + + once = normalizer.normalize(raw) + twice = normalizer.normalize(once) + + assert twice == once + assert "documentation" not in once.lower() + assert "class" not in once.lower() diff --git a/tests/rag/test_query_router.py b/tests/rag/test_query_router.py deleted file mode 100644 index 8da84d7..0000000 --- a/tests/rag/test_query_router.py +++ /dev/null @@ -1,12 +0,0 @@ -from app.modules.rag.contracts.enums import RetrievalMode -from app.modules.rag.retrieval.query_router import RagQueryRouter - - -def test_query_router_uses_docs_by_default() -> None: - router = RagQueryRouter() - assert router.resolve_mode("Какие есть требования по биллингу?") == RetrievalMode.DOCS - - -def test_query_router_switches_to_code_on_explicit_code_requests() -> None: - router = RagQueryRouter() - assert router.resolve_mode("Объясни как работает код endpoint create invoice") == RetrievalMode.CODE diff --git a/tests/rag/test_retrieval_statement_builder.py b/tests/rag/test_retrieval_statement_builder.py new file mode 100644 index 0000000..f9b4cd8 --- /dev/null +++ b/tests/rag/test_retrieval_statement_builder.py @@ -0,0 +1,44 @@ +from app.modules.rag.persistence.retrieval_statement_builder import RetrievalStatementBuilder +from app.modules.rag.retrieval.test_filter import build_test_filters, is_test_path + + +def test_retrieve_builder_adds_test_exclusion_filters() -> None: + builder = RetrievalStatementBuilder() + test_filters = build_test_filters() + + sql, params = builder.build_retrieve( + "rag-1", + [0.1, 0.2], + query_text="Explain user service", + layers=["C0_SOURCE_CHUNKS"], + exclude_path_prefixes=test_filters.exclude_path_prefixes, + exclude_like_patterns=test_filters.exclude_like_patterns, + ) + + assert "NOT (" in sql + assert "path LIKE :exclude_prefix_0" in sql + assert "lower(path) LIKE :exclude_like_0" in sql + assert "ESCAPE E'\\\\'" in sql + assert params["exclude_prefix_0"] == "tests/%" + assert "%.test.%" in params.values() + assert "%\\_test.%" in params.values() + + +def test_lexical_builder_omits_test_filters_when_not_requested() -> None: + builder = RetrievalStatementBuilder() + + sql, params = builder.build_lexical_code( + "rag-1", + query_text="Explain user service", + prefer_non_tests=False, + ) + + assert sql is not None + assert "exclude_prefix" not in sql + assert "exclude_like" not in sql + assert not any(key.startswith("exclude_") for key in params) + + +def test_test_filter_does_not_treat_contest_file_as_test() -> None: + assert is_test_path("app/contest.py") is False + assert is_test_path("tests/test_users.py") is True diff --git a/tests/rag/test_retriever_v2_no_fallback.py b/tests/rag/test_retriever_v2_no_fallback.py new file mode 100644 index 0000000..dd7f0df --- /dev/null +++ b/tests/rag/test_retriever_v2_no_fallback.py @@ -0,0 +1,52 @@ +from app.modules.rag.explain import CodeExplainRetrieverV2, LayeredRetrievalGateway + + +class _ExplodingEmbedder: + def embed(self, texts: list[str]) -> list[list[float]]: + raise RuntimeError("embedding unavailable") + + +class _RepositoryWithoutFallback: + def retrieve(self, *args, **kwargs): + raise RuntimeError("vector retrieval unavailable") + + def retrieve_lexical_code( + self, + rag_session_id: str, + query_text: str, + *, + limit: int = 5, + path_prefixes: list[str] | None = None, + exclude_path_prefixes: list[str] | None = None, + exclude_like_patterns: list[str] | None = None, + prefer_non_tests: bool = False, + ): + return [] + + +class _FakeGraphRepository: + def get_symbols_by_ids(self, rag_session_id: str, symbol_ids: list[str]): + return [] + + def get_chunks_by_symbol_ids(self, rag_session_id: str, symbol_ids: list[str], prefer_chunk_type: str = "symbol_block"): + return [] + + def get_out_edges(self, rag_session_id: str, src_symbol_ids: list[str], edge_types: list[str], limit_per_src: int): + return [] + + def resolve_symbol_by_ref(self, rag_session_id: str, dst_ref: str, package_hint: str | None = None): + return None + + +def test_retriever_v2_returns_pack_without_fallback_method() -> None: + retriever = CodeExplainRetrieverV2( + gateway=LayeredRetrievalGateway(_RepositoryWithoutFallback(), _ExplodingEmbedder()), + graph_repository=_FakeGraphRepository(), + ) + + pack = retriever.build_pack("rag-1", "Explain get_user") + + assert pack.code_excerpts == [] + assert any(item.startswith("layer:C3_ENTRYPOINTS retrieval_failed") for item in pack.missing) + assert any(item.startswith("layer:C1_SYMBOL_CATALOG retrieval_failed") for item in pack.missing) + assert "layer:C0 empty" in pack.missing diff --git a/tests/rag/test_retriever_v2_pack.py b/tests/rag/test_retriever_v2_pack.py new file mode 100644 index 0000000..0fb5c91 --- /dev/null +++ b/tests/rag/test_retriever_v2_pack.py @@ -0,0 +1,105 @@ +from app.modules.rag.explain.models import CodeLocation, LayeredRetrievalItem +from app.modules.rag.explain.retriever_v2 import CodeExplainRetrieverV2 + + +class _FakeGateway: + def retrieve_layer( + self, + rag_session_id: str, + query: str, + layer: str, + *, + limit: int, + path_prefixes: list[str] | None = None, + exclude_tests: bool = True, + prefer_non_tests: bool = False, + include_spans: bool = False, + ): + if layer == "C3_ENTRYPOINTS": + return __import__("types").SimpleNamespace( + items=[ + LayeredRetrievalItem( + source="app/api/users.py", + content="GET /users/{id}", + layer=layer, + title="GET /users/{id}", + metadata={"entry_type": "http", "handler_symbol_id": "handler-1"}, + location=CodeLocation(path="app/api/users.py", start_line=10, end_line=10), + ) + ], + missing=[], + ) + if layer == "C1_SYMBOL_CATALOG": + return __import__("types").SimpleNamespace( + items=[ + LayeredRetrievalItem( + source="app/api/users.py", + content="def get_user_handler", + layer=layer, + title="get_user_handler", + metadata={"symbol_id": "handler-1"}, + location=CodeLocation(path="app/api/users.py", start_line=10, end_line=18), + ) + ], + missing=[], + ) + raise AssertionError(layer) + + def retrieve_lexical_code( + self, + rag_session_id: str, + query: str, + *, + limit: int, + path_prefixes: list[str] | None = None, + exclude_tests: bool = True, + include_spans: bool = False, + ): + return __import__("types").SimpleNamespace(items=[], missing=[]) + + +class _FakeGraphRepository: + def get_symbols_by_ids(self, rag_session_id: str, symbol_ids: list[str]): + return [ + LayeredRetrievalItem( + source="app/api/users.py", + content="def get_user_handler", + layer="C1_SYMBOL_CATALOG", + title="get_user_handler", + metadata={"symbol_id": "handler-1"}, + location=CodeLocation(path="app/api/users.py", start_line=10, end_line=18), + ) + ] + + def get_out_edges(self, rag_session_id: str, src_symbol_ids: list[str], edge_types: list[str], limit_per_src: int): + return [] + + def resolve_symbol_by_ref(self, rag_session_id: str, dst_ref: str, package_hint: str | None = None): + return None + + def get_chunks_by_symbol_ids(self, rag_session_id: str, symbol_ids: list[str], prefer_chunk_type: str = "symbol_block"): + return [ + LayeredRetrievalItem( + source="app/api/users.py", + content="async def get_user_handler(user_id: str):\n return await service.get_user(user_id)", + layer="C0_SOURCE_CHUNKS", + title="get_user_handler", + metadata={"symbol_id": "handler-1"}, + location=CodeLocation(path="app/api/users.py", start_line=10, end_line=18), + ) + ] + + +def test_retriever_v2_builds_pack_with_trace_and_excerpts() -> None: + retriever = CodeExplainRetrieverV2( + gateway=_FakeGateway(), + graph_repository=_FakeGraphRepository(), + ) + + pack = retriever.build_pack("rag-1", "Explain endpoint get_user") + + assert len(pack.selected_entrypoints) == 1 + assert len(pack.seed_symbols) == 1 + assert len(pack.trace_paths) == 1 + assert len(pack.code_excerpts) == 1 + assert pack.code_excerpts[0].path == "app/api/users.py" diff --git a/tests/rag/test_retriever_v2_production_first.py b/tests/rag/test_retriever_v2_production_first.py new file mode 100644 index 0000000..0971664 --- /dev/null +++ b/tests/rag/test_retriever_v2_production_first.py @@ -0,0 +1,142 @@ +from types import SimpleNamespace + +from app.modules.rag.explain.models import CodeLocation, LayeredRetrievalItem +from app.modules.rag.explain.retriever_v2 import CodeExplainRetrieverV2 + + +class _ProductionFirstGateway: + def __init__(self) -> None: + self.lexical_calls: list[bool] = [] + + def retrieve_layer( + self, + rag_session_id: str, + query: str, + layer: str, + *, + limit: int, + path_prefixes: list[str] | None = None, + exclude_tests: bool = True, + prefer_non_tests: bool = False, + include_spans: bool = False, + ): + if layer == "C3_ENTRYPOINTS": + return SimpleNamespace(items=[], missing=[]) + if layer == "C1_SYMBOL_CATALOG": + return SimpleNamespace(items=[], missing=[]) + raise AssertionError(layer) + + def retrieve_lexical_code( + self, + rag_session_id: str, + query: str, + *, + limit: int, + path_prefixes: list[str] | None = None, + exclude_tests: bool = True, + include_spans: bool = False, + ): + self.lexical_calls.append(exclude_tests) + if exclude_tests: + return SimpleNamespace( + items=[ + LayeredRetrievalItem( + source="app/users/service.py", + content="def get_user():\n return repo.get_user()", + layer="C0_SOURCE_CHUNKS", + title="get_user", + metadata={"symbol_id": "user-service", "is_test": False}, + location=CodeLocation(path="app/users/service.py", start_line=10, end_line=11), + ), + LayeredRetrievalItem( + source="app/users/repository.py", + content="def get_user_repo():\n return {}", + layer="C0_SOURCE_CHUNKS", + title="get_user_repo", + metadata={"symbol_id": "user-repo", "is_test": False}, + location=CodeLocation(path="app/users/repository.py", start_line=20, end_line=21), + ), + ], + missing=[], + ) + return SimpleNamespace( + items=[ + LayeredRetrievalItem( + source="tests/test_users.py", + content="def test_get_user():\n assert service.get_user()", + layer="C0_SOURCE_CHUNKS", + title="test_get_user", + metadata={"symbol_id": "test-user", "is_test": True}, + location=CodeLocation(path="tests/test_users.py", start_line=5, end_line=6), + ) + ], + missing=[], + ) + + +class _TestsOnlyGateway(_ProductionFirstGateway): + def retrieve_lexical_code( + self, + rag_session_id: str, + query: str, + *, + limit: int, + path_prefixes: list[str] | None = None, + exclude_tests: bool = True, + include_spans: bool = False, + ): + self.lexical_calls.append(exclude_tests) + if exclude_tests: + return SimpleNamespace(items=[], missing=[]) + return SimpleNamespace( + items=[ + LayeredRetrievalItem( + source="tests/test_users.py", + content="def test_get_user():\n assert service.get_user()", + layer="C0_SOURCE_CHUNKS", + title="test_get_user", + metadata={"symbol_id": "test-user", "is_test": True}, + location=CodeLocation(path="tests/test_users.py", start_line=5, end_line=6), + ) + ], + missing=[], + ) + + +class _FakeGraphRepository: + def get_symbols_by_ids(self, rag_session_id: str, symbol_ids: list[str]): + return [] + + def get_chunks_by_symbol_ids(self, rag_session_id: str, symbol_ids: list[str], prefer_chunk_type: str = "symbol_block"): + return [] + + def get_out_edges(self, rag_session_id: str, src_symbol_ids: list[str], edge_types: list[str], limit_per_src: int): + return [] + + def resolve_symbol_by_ref(self, rag_session_id: str, dst_ref: str, package_hint: str | None = None): + return None + + +def test_retriever_prefers_prod_chunks_and_skips_test_fallback_when_enough_evidence() -> None: + gateway = _ProductionFirstGateway() + retriever = CodeExplainRetrieverV2(gateway=gateway, graph_repository=_FakeGraphRepository()) + + pack = retriever.build_pack("rag-1", "Explain get_user") + + assert gateway.lexical_calls == [True] + assert [excerpt.path for excerpt in pack.code_excerpts] == [ + "app/users/service.py", + "app/users/repository.py", + ] + assert all(not excerpt.focus.startswith("test:") for excerpt in pack.code_excerpts) + + +def test_retriever_uses_test_fallback_when_production_evidence_is_missing() -> None: + gateway = _TestsOnlyGateway() + retriever = CodeExplainRetrieverV2(gateway=gateway, graph_repository=_FakeGraphRepository()) + + pack = retriever.build_pack("rag-1", "Explain get_user") + + assert gateway.lexical_calls == [True, False] + assert [excerpt.path for excerpt in pack.code_excerpts] == ["tests/test_users.py"] + assert pack.code_excerpts[0].focus == "test:lexical" diff --git a/tests/rag/test_trace_builder.py b/tests/rag/test_trace_builder.py new file mode 100644 index 0000000..0292121 --- /dev/null +++ b/tests/rag/test_trace_builder.py @@ -0,0 +1,83 @@ +from app.modules.rag.explain.models import CodeLocation, LayeredRetrievalItem +from app.modules.rag.explain.trace_builder import TraceBuilder + + +class _FakeGraphRepository: + def get_out_edges(self, rag_session_id: str, src_symbol_ids: list[str], edge_types: list[str], limit_per_src: int): + assert rag_session_id == "rag-1" + assert edge_types == ["calls", "imports", "inherits"] + if src_symbol_ids == ["handler-1"]: + return [ + LayeredRetrievalItem( + source="app/api/users.py", + content="handler calls get_user", + layer="C2_DEPENDENCY_GRAPH", + title="handler:calls", + metadata={ + "src_symbol_id": "handler-1", + "dst_symbol_id": None, + "dst_ref": "UserService.get_user", + "resolution": "partial", + "edge_type": "calls", + }, + location=CodeLocation(path="app/api/users.py", start_line=12, end_line=12), + ) + ] + return [] + + def resolve_symbol_by_ref(self, rag_session_id: str, dst_ref: str, package_hint: str | None = None): + assert rag_session_id == "rag-1" + assert dst_ref == "UserService.get_user" + assert package_hint == "app.api" + return LayeredRetrievalItem( + source="app/services/users.py", + content="method UserService.get_user", + layer="C1_SYMBOL_CATALOG", + title="UserService.get_user", + metadata={ + "symbol_id": "service-1", + "package_or_module": "app.api.users", + }, + location=CodeLocation(path="app/services/users.py", start_line=4, end_line=10), + ) + + def get_symbols_by_ids(self, rag_session_id: str, symbol_ids: list[str]): + assert rag_session_id == "rag-1" + if symbol_ids == ["service-1"]: + return [ + LayeredRetrievalItem( + source="app/services/users.py", + content="method UserService.get_user", + layer="C1_SYMBOL_CATALOG", + title="UserService.get_user", + metadata={ + "symbol_id": "service-1", + "package_or_module": "app.api.users", + }, + location=CodeLocation(path="app/services/users.py", start_line=4, end_line=10), + ) + ] + return [] + + +def test_trace_builder_resolves_partial_edges_across_files() -> None: + builder = TraceBuilder(_FakeGraphRepository()) + seeds = [ + LayeredRetrievalItem( + source="app/api/users.py", + content="function handler", + layer="C1_SYMBOL_CATALOG", + title="get_user", + metadata={ + "symbol_id": "handler-1", + "package_or_module": "app.api.users", + }, + location=CodeLocation(path="app/api/users.py", start_line=10, end_line=18), + ) + ] + + paths = builder.build_paths("rag-1", seeds, max_depth=3) + + assert len(paths) >= 1 + assert paths[0].symbol_ids == ["handler-1", "service-1"] + assert "resolved:UserService.get_user" in paths[0].notes