virtual-context 0.3.4__tar.gz → 0.3.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- virtual_context-0.3.6/.github/workflows/python-publish.yml +29 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/.gitignore +3 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/PKG-INFO +1 -1
- {virtual_context-0.3.4 → virtual_context-0.3.6}/pyproject.toml +1 -1
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_assembler.py +31 -1
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_state.py +3 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_redesign.py +62 -4
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy.py +41 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_formats.py +153 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_request_captures_persistence.py +74 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_retriever.py +7 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_generator.py +59 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_vcattach.py +90 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/__init__.py +1 -1
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/assembler.py +7 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/compaction_pipeline.py +12 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/compactor.py +189 -15
- virtual_context-0.3.6/virtual_context/core/llm_utils.py +145 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/retriever.py +2 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_generator.py +119 -8
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tagging_pipeline.py +1 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/engine.py +4 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/formats.py +234 -149
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/handlers.py +8 -37
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/metrics.py +60 -18
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/server.py +34 -2
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/session_state.py +2 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/state.py +1 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/filesystem.py +19 -7
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/postgres.py +44 -11
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/sqlite.py +51 -15
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/types.py +5 -1
- virtual_context-0.3.4/virtual_context/core/llm_utils.py +0 -65
- {virtual_context-0.3.4 → virtual_context-0.3.6}/CLAUDE.md +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/LICENSE +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/README-draft.md +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/README.md +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/README.md.backup +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/assets/dashboard.png +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/assets/hero.png +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/models.yaml +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/scripts/install.ps1 +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/scripts/install.sh +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/specs/cache-aware-compaction.md +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/REGRESSION_MAP.md +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/conftest.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/docker-compose.test.yml +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/conftest.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/test_compaction.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/test_retrieval.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/test_tagging.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/conftest.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_compactor.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_pipeline.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_provider.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_tag_generator.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/proxy/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/proxy/test_dashboard_cors.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/proxy/test_metrics.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_backend_integration.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_budget_enforcement.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_cli_init.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_compaction_commit_prune.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_compactor.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_compactor_concurrent.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_composite_store.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_config.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_context_bleed.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_conversation_identity.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_conversation_lifecycle.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_conversation_scoping.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_deferred_payload_compaction.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_embedding_tag_generator.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_empty_turn_skip.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_integration.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_lookback.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_sync_turns.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_enrichment.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_graph_integration.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_link_checker.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_link_query.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_link_types.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_links_sqlite.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fill_pass.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_find_quote.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_format_agnostic.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_headless.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_history_filter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_idf_retrieval.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_ingest_index_integrity.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_longmemeval_auth.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_mcp_server.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_media.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_message_filter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_metrics_persistence.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_model_catalog.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_model_limits.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_monitor.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_multi_instance.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_noop_fact_link_store.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_openrouter_provider.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_paging.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_passthrough_filter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_presets.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_prev_context_leak.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_provider_adapters.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_dashboard.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_message_filter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_session.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_streaming.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_raw_content.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_recall_all.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_registry_lifecycle.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_rrf_scoring.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_segmenter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_semantic_search.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_sender_identity.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_session_cache.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_session_date.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_session_state.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_storage_protocols.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_store_recovery.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_store_sqlite.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_stub_turn_handling.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_supersession.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_supersession_migration.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_canonicalizer.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_consolidator.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_splitter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_telemetry.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_telemetry_integration.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_loop.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_output_interceptor.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_result_filter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_tags.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tui.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_turn_grouping.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_turn_tag_index.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_unified_budget.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_upstream_trim.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_verb_expansion.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual-context.yaml +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual-context.yaml.example +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/cli/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/cli/main.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/config.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/conversation_identity.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/composite_store.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/conversation_store.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/embedding_provider.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/embedding_tag_generator.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/engine_utils.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/fact_query.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/fts_preprocessor.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/hint_builder.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/math_utils.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/model_catalog.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/monitor.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/paging_manager.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/protocols.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/provider_adapters.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/quote_search.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/retrieval_assembler.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/retrieval_scoring.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/search_engine.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/segmenter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/semantic_search.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/store.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_canonicalizer.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_consolidator.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_scoring.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_splitter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/telemetry.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/temporal_resolver.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tool_loop.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tool_query.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/turn_tag_index.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/data/models.yaml +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/curator.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/date_resolver.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/parsers.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/supersession.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/mcp/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/mcp/server.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/model_limits.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/openclaw/virtual-context.mjs +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/patterns.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/agentic.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/base.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/coding.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/anthropic.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/base.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/generic_openai.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/ollama_native.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/_envelope.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/dashboard.html +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/dashboard.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/helpers.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/media.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/message_filter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/multi.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/registry.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/session_cache.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/favicon-16x16.png +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/favicon-32x32.png +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/favicon.ico +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/site.webmanifest +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/tool_output_interceptor.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/vcattach.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/falkordb.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/helpers.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/neo4j.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/noop_fact_link_store.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/token_counter.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/app.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/chat.tcss +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/chat_provider.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/headless.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/modals/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/modals/turn_inspector.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/state.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/__init__.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/budget_bar.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/chat_view.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/input_box.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/tag_panel.py +0 -0
- {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/turn_list.py +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Upload Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Build
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install build
|
|
24
|
+
python -m build
|
|
25
|
+
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
with:
|
|
29
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: virtual-context
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: OS-style virtual memory for LLM session context management
|
|
5
5
|
Project-URL: Homepage, https://virtual-context.com
|
|
6
6
|
Project-URL: Repository, https://github.com/virtual-context/virtual-context
|
|
@@ -4,7 +4,7 @@ from datetime import datetime, timezone
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from virtual_context.core.assembler import ContextAssembler
|
|
7
|
+
from virtual_context.core.assembler import ContextAssembler, format_tag_section
|
|
8
8
|
from virtual_context.types import (
|
|
9
9
|
AssemblerConfig,
|
|
10
10
|
Message,
|
|
@@ -77,6 +77,36 @@ def test_assemble_xml_tags(assembler, retrieval_result):
|
|
|
77
77
|
assert "</virtual-context>" in section
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def test_format_tag_section_renders_code_refs():
|
|
81
|
+
now = datetime.now(timezone.utc)
|
|
82
|
+
section = format_tag_section(
|
|
83
|
+
"backend",
|
|
84
|
+
[
|
|
85
|
+
StoredSummary(
|
|
86
|
+
ref="ref-1",
|
|
87
|
+
primary_tag="backend",
|
|
88
|
+
tags=["backend"],
|
|
89
|
+
summary="Request cache boundary moved ahead of the mutable reminder.",
|
|
90
|
+
summary_tokens=18,
|
|
91
|
+
full_tokens=50,
|
|
92
|
+
metadata=SegmentMetadata(
|
|
93
|
+
code_refs=[
|
|
94
|
+
{"file": "virtual_context/proxy/formats.py", "line": 1312, "symbol": "inject_context"},
|
|
95
|
+
{"file": "virtual_context/core/provider_adapters.py", "symbol": "AnthropicAdapter"},
|
|
96
|
+
],
|
|
97
|
+
),
|
|
98
|
+
created_at=now,
|
|
99
|
+
start_timestamp=now,
|
|
100
|
+
end_timestamp=now,
|
|
101
|
+
),
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
assert "[refs:" in section
|
|
106
|
+
assert "virtual_context/proxy/formats.py:1312 inject_context" in section
|
|
107
|
+
assert "virtual_context/core/provider_adapters.py AnthropicAdapter" in section
|
|
108
|
+
|
|
109
|
+
|
|
80
110
|
def test_trim_conversation(assembler):
|
|
81
111
|
messages = [
|
|
82
112
|
Message(role="user", content="x" * 400),
|
|
@@ -28,6 +28,7 @@ def _make_snapshot(
|
|
|
28
28
|
tags=[f"tag-{i}", "common"],
|
|
29
29
|
primary_tag=f"tag-{i}",
|
|
30
30
|
timestamp=datetime(2026, 1, 15, 10, i, 0, tzinfo=timezone.utc),
|
|
31
|
+
code_refs=[{"file": f"src/module_{i}.py", "line": i + 1, "symbol": f"func_{i}"}],
|
|
31
32
|
)
|
|
32
33
|
for i in range(turn_count)
|
|
33
34
|
]
|
|
@@ -75,6 +76,7 @@ class TestEngineStateSQLite:
|
|
|
75
76
|
assert restored.tags == orig.tags
|
|
76
77
|
assert restored.primary_tag == orig.primary_tag
|
|
77
78
|
assert restored.timestamp == orig.timestamp
|
|
79
|
+
assert restored.code_refs == orig.code_refs
|
|
78
80
|
|
|
79
81
|
def test_load_empty_store(self, tmp_path):
|
|
80
82
|
"""Loading from an empty store returns None."""
|
|
@@ -131,6 +133,7 @@ class TestEngineStateFilesystem:
|
|
|
131
133
|
assert restored.message_hash == orig.message_hash
|
|
132
134
|
assert restored.tags == orig.tags
|
|
133
135
|
assert restored.primary_tag == orig.primary_tag
|
|
136
|
+
assert restored.code_refs == orig.code_refs
|
|
134
137
|
|
|
135
138
|
def test_load_empty_store(self, tmp_path):
|
|
136
139
|
"""Loading from an empty store returns None."""
|
|
@@ -61,17 +61,75 @@ def test_compaction_uses_replace_facts_for_segment():
|
|
|
61
61
|
|
|
62
62
|
def test_code_mode_prompt_appended():
|
|
63
63
|
"""When code_mode is True, the compactor prompt should include the code mode block."""
|
|
64
|
-
from virtual_context.core.compactor import
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
from virtual_context.core.compactor import (
|
|
65
|
+
CODE_FACT_EXTRACTION_PROMPT,
|
|
66
|
+
CODE_SUMMARY_PROMPT,
|
|
67
|
+
CODE_TAG_SUMMARY_ROLLUP_PROMPT,
|
|
68
|
+
)
|
|
69
|
+
assert "Do NOT extract intermediary" in CODE_FACT_EXTRACTION_PROMPT
|
|
70
|
+
assert "conclusions, findings, discoveries" in CODE_FACT_EXTRACTION_PROMPT
|
|
71
|
+
assert "engineering conversation segment" in CODE_SUMMARY_PROMPT
|
|
72
|
+
assert '"code_refs"' in CODE_SUMMARY_PROMPT
|
|
73
|
+
assert "rolling up engineering context" in CODE_TAG_SUMMARY_ROLLUP_PROMPT
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_compactor_template_path_appends_code_fact_prompt():
|
|
77
|
+
from datetime import datetime, timezone
|
|
78
|
+
from unittest.mock import MagicMock
|
|
79
|
+
|
|
80
|
+
from virtual_context.core.compactor import CODE_FACT_EXTRACTION_PROMPT, DomainCompactor
|
|
81
|
+
from virtual_context.types import Message, TaggedSegment
|
|
82
|
+
|
|
83
|
+
captured: dict[str, str] = {}
|
|
84
|
+
|
|
85
|
+
llm = MagicMock()
|
|
86
|
+
|
|
87
|
+
def _complete(*, system, user, max_tokens):
|
|
88
|
+
captured["user"] = user
|
|
89
|
+
return ('{"summary":"ok","entities":[],"key_decisions":[],"action_items":[],"date_references":[],"refined_tags":[],"code_refs":[]}', {})
|
|
90
|
+
|
|
91
|
+
llm.complete.side_effect = _complete
|
|
92
|
+
|
|
93
|
+
compactor = DomainCompactor(
|
|
94
|
+
llm_provider=llm,
|
|
95
|
+
config=CompactorConfig(code_mode=True),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
seg = TaggedSegment(
|
|
99
|
+
primary_tag="backend",
|
|
100
|
+
tags=["backend"],
|
|
101
|
+
messages=[
|
|
102
|
+
Message(role="user", content="We changed the request cache boundary in formats.py."),
|
|
103
|
+
Message(role="assistant", content="It now sits above the mutable VC injection block."),
|
|
104
|
+
],
|
|
105
|
+
token_count=40,
|
|
106
|
+
start_timestamp=datetime.now(timezone.utc),
|
|
107
|
+
end_timestamp=datetime.now(timezone.utc),
|
|
108
|
+
turn_count=1,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
compactor._compact_one(seg)
|
|
112
|
+
|
|
113
|
+
assert CODE_FACT_EXTRACTION_PROMPT.strip() in captured["user"]
|
|
67
114
|
|
|
68
115
|
|
|
69
116
|
def test_tag_generator_accepts_code_mode():
|
|
70
|
-
from virtual_context.core.tag_generator import
|
|
117
|
+
from virtual_context.core.tag_generator import (
|
|
118
|
+
CODE_TAG_GENERATOR_PROMPT_COMPACT,
|
|
119
|
+
CODE_TAG_GENERATOR_PROMPT_DETAILED,
|
|
120
|
+
CODE_TAG_GENERATOR_PROMPT_SMALL_MODEL,
|
|
121
|
+
LLMTagGenerator,
|
|
122
|
+
)
|
|
71
123
|
from virtual_context.types import TagGeneratorConfig
|
|
72
124
|
from unittest.mock import MagicMock
|
|
73
125
|
gen = LLMTagGenerator(llm_provider=MagicMock(), config=TagGeneratorConfig(), code_mode=True)
|
|
74
126
|
assert gen._code_mode is True
|
|
127
|
+
assert "engineering and code conversations" in CODE_TAG_GENERATOR_PROMPT_DETAILED
|
|
128
|
+
assert "fact_signals" in CODE_TAG_GENERATOR_PROMPT_DETAILED
|
|
129
|
+
assert "engineering and code conversations" in CODE_TAG_GENERATOR_PROMPT_COMPACT
|
|
130
|
+
assert "fact_signals" in CODE_TAG_GENERATOR_PROMPT_COMPACT
|
|
131
|
+
assert "engineering and code conversations" in CODE_TAG_GENERATOR_PROMPT_SMALL_MODEL
|
|
132
|
+
assert "fact_signals" in CODE_TAG_GENERATOR_PROMPT_SMALL_MODEL
|
|
75
133
|
|
|
76
134
|
|
|
77
135
|
def test_build_tag_generator_passes_code_mode():
|
|
@@ -965,6 +965,47 @@ class TestRequestCapture:
|
|
|
965
965
|
m = ProxyMetrics()
|
|
966
966
|
m.update_request_tags(99, response_tags=["x"]) # should not raise
|
|
967
967
|
|
|
968
|
+
def test_same_turn_different_turn_ids_do_not_collide(self):
|
|
969
|
+
m = ProxyMetrics()
|
|
970
|
+
m.capture_request(
|
|
971
|
+
7,
|
|
972
|
+
{"messages": []},
|
|
973
|
+
"anthropic",
|
|
974
|
+
conversation_id="conv-1",
|
|
975
|
+
turn_id="req-a",
|
|
976
|
+
inbound_tags=["first"],
|
|
977
|
+
)
|
|
978
|
+
m.capture_request(
|
|
979
|
+
7,
|
|
980
|
+
{"messages": []},
|
|
981
|
+
"anthropic",
|
|
982
|
+
conversation_id="conv-1",
|
|
983
|
+
turn_id="req-b",
|
|
984
|
+
inbound_tags=["second"],
|
|
985
|
+
)
|
|
986
|
+
m.capture_response(
|
|
987
|
+
7,
|
|
988
|
+
{"content": "a"},
|
|
989
|
+
conversation_id="conv-1",
|
|
990
|
+
turn_id="req-a",
|
|
991
|
+
upstream_input_tokens=100,
|
|
992
|
+
)
|
|
993
|
+
m.capture_response(
|
|
994
|
+
7,
|
|
995
|
+
{"content": "b"},
|
|
996
|
+
conversation_id="conv-1",
|
|
997
|
+
turn_id="req-b",
|
|
998
|
+
upstream_input_tokens=200,
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
req_a = m.get_captured_request(7, conversation_id="conv-1", turn_id="req-a")
|
|
1002
|
+
req_b = m.get_captured_request(7, conversation_id="conv-1", turn_id="req-b")
|
|
1003
|
+
|
|
1004
|
+
assert req_a["inbound_tags"] == ["first"]
|
|
1005
|
+
assert req_a["upstream_input_tokens"] == 100
|
|
1006
|
+
assert req_b["inbound_tags"] == ["second"]
|
|
1007
|
+
assert req_b["upstream_input_tokens"] == 200
|
|
1008
|
+
|
|
968
1009
|
|
|
969
1010
|
# ---------------------------------------------------------------------------
|
|
970
1011
|
# Request event model field
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import copy
|
|
6
6
|
import json
|
|
7
|
+
import math
|
|
7
8
|
from unittest.mock import MagicMock
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
@@ -14,6 +15,7 @@ from virtual_context.proxy.formats import (
|
|
|
14
15
|
OpenAIFormat,
|
|
15
16
|
OpenAIResponsesFormat,
|
|
16
17
|
GeminiFormat,
|
|
18
|
+
_estimate_media_tokens,
|
|
17
19
|
detect_format,
|
|
18
20
|
get_format,
|
|
19
21
|
)
|
|
@@ -30,6 +32,10 @@ from virtual_context.proxy.server import (
|
|
|
30
32
|
_strip_vc_prompt,
|
|
31
33
|
)
|
|
32
34
|
|
|
35
|
+
_ONE_BY_ONE_PNG_B64 = (
|
|
36
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/a4sAAAAASUVORK5CYII="
|
|
37
|
+
)
|
|
38
|
+
|
|
33
39
|
|
|
34
40
|
# ---------------------------------------------------------------------------
|
|
35
41
|
# Format detection
|
|
@@ -236,6 +242,125 @@ class TestAnthropicFormat:
|
|
|
236
242
|
assert content[1]["cache_control"] == {"type": "ephemeral"}
|
|
237
243
|
assert content[2]["text"].startswith("<system-reminder>")
|
|
238
244
|
|
|
245
|
+
def test_estimate_message_tokens_counts_calibrated_tool_result_image_and_signature(self):
|
|
246
|
+
msg = {
|
|
247
|
+
"role": "user",
|
|
248
|
+
"content": [
|
|
249
|
+
{
|
|
250
|
+
"type": "tool_result",
|
|
251
|
+
"tool_use_id": "tool-1",
|
|
252
|
+
"content": [
|
|
253
|
+
{"type": "text", "text": "Screenshot captured"},
|
|
254
|
+
{
|
|
255
|
+
"type": "image",
|
|
256
|
+
"source": {
|
|
257
|
+
"type": "base64",
|
|
258
|
+
"media_type": "image/png",
|
|
259
|
+
"data": _ONE_BY_ONE_PNG_B64,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
{"type": "thinking", "text": "Working", "signature": "sig" * 1000},
|
|
265
|
+
],
|
|
266
|
+
}
|
|
267
|
+
original = copy.deepcopy(msg)
|
|
268
|
+
estimate = self.fmt.estimate_message_tokens(msg)
|
|
269
|
+
|
|
270
|
+
media_tokens = sum(self.fmt._estimate_media_tokens(m) for m in self.fmt._collect_media_from_value(msg))
|
|
271
|
+
expected = (
|
|
272
|
+
self.fmt._TOOL_CALL_ID_TOKENS
|
|
273
|
+
+ math.ceil(self.fmt._count("Screenshot captured") * self.fmt._TOOL_RESULT_TEXT_SCALE)
|
|
274
|
+
+ math.ceil(self.fmt._count("Working") * self.fmt._ASSISTANT_TEXT_SCALE)
|
|
275
|
+
+ math.ceil(self.fmt._count("sig" * 1000) * self.fmt._THINKING_SIGNATURE_SCALE)
|
|
276
|
+
+ media_tokens
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
assert estimate == expected
|
|
280
|
+
assert msg == original
|
|
281
|
+
|
|
282
|
+
def test_estimate_payload_tokens_from_serialized_ignores_nested_tool_result_image(self):
|
|
283
|
+
body = {
|
|
284
|
+
"messages": [
|
|
285
|
+
{
|
|
286
|
+
"role": "user",
|
|
287
|
+
"content": [
|
|
288
|
+
{
|
|
289
|
+
"type": "tool_result",
|
|
290
|
+
"tool_use_id": "tool-1",
|
|
291
|
+
"content": [
|
|
292
|
+
{"type": "text", "text": "Screenshot captured"},
|
|
293
|
+
{
|
|
294
|
+
"type": "image",
|
|
295
|
+
"source": {
|
|
296
|
+
"type": "base64",
|
|
297
|
+
"media_type": "image/png",
|
|
298
|
+
"data": _ONE_BY_ONE_PNG_B64,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
"system": "You are helpful.",
|
|
307
|
+
}
|
|
308
|
+
serialized = json.dumps(body, default=str)
|
|
309
|
+
estimate = self.fmt.estimate_payload_tokens_from_serialized(body, serialized)
|
|
310
|
+
|
|
311
|
+
media_tokens = sum(self.fmt._estimate_media_tokens(m) for m in self.fmt._collect_media(body))
|
|
312
|
+
expected = (
|
|
313
|
+
math.ceil(self.fmt._count("You are helpful.") * self.fmt._SYSTEM_SCALE)
|
|
314
|
+
+ self.fmt._TOOL_CALL_ID_TOKENS
|
|
315
|
+
+ math.ceil(self.fmt._count("Screenshot captured") * self.fmt._TOOL_RESULT_TEXT_SCALE)
|
|
316
|
+
+ media_tokens
|
|
317
|
+
)
|
|
318
|
+
assert estimate == expected
|
|
319
|
+
|
|
320
|
+
def test_estimate_message_tokens_counts_tool_use_semantics_not_wrapper_json(self):
|
|
321
|
+
fmt = AnthropicFormat()
|
|
322
|
+
fmt.set_token_counter(lambda text: len(text) if text else 0)
|
|
323
|
+
msg = {
|
|
324
|
+
"role": "assistant",
|
|
325
|
+
"content": [
|
|
326
|
+
{
|
|
327
|
+
"type": "tool_use",
|
|
328
|
+
"id": "toolu_123",
|
|
329
|
+
"name": "probe_tool",
|
|
330
|
+
"input": {"path": "/tmp/demo.txt", "limit": 5},
|
|
331
|
+
}
|
|
332
|
+
],
|
|
333
|
+
}
|
|
334
|
+
expected = (
|
|
335
|
+
fmt._TOOL_CALL_ID_TOKENS
|
|
336
|
+
+ math.ceil(len("probe_tool") * fmt._TOOL_NAME_SCALE)
|
|
337
|
+
+ math.ceil(
|
|
338
|
+
len(json.dumps({"path": "/tmp/demo.txt", "limit": 5}, sort_keys=True))
|
|
339
|
+
* fmt._TOOL_USE_INPUT_SCALE
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
assert fmt.estimate_message_tokens(msg) == expected
|
|
343
|
+
|
|
344
|
+
def test_estimate_message_tokens_tool_id_length_does_not_change_estimate(self):
|
|
345
|
+
fmt = AnthropicFormat()
|
|
346
|
+
fmt.set_token_counter(lambda text: len(text) if text else 0)
|
|
347
|
+
short = {
|
|
348
|
+
"role": "assistant",
|
|
349
|
+
"content": [{"type": "tool_use", "id": "t1", "name": "probe_tool", "input": {"path": "/tmp/demo.txt"}}],
|
|
350
|
+
}
|
|
351
|
+
long = {
|
|
352
|
+
"role": "assistant",
|
|
353
|
+
"content": [{"type": "tool_use", "id": "toolu_0123456789abcdef", "name": "probe_tool", "input": {"path": "/tmp/demo.txt"}}],
|
|
354
|
+
}
|
|
355
|
+
assert fmt.estimate_message_tokens(short) == fmt.estimate_message_tokens(long)
|
|
356
|
+
|
|
357
|
+
def test_estimate_tools_tokens_scales_anthropic_tools(self):
|
|
358
|
+
fmt = AnthropicFormat()
|
|
359
|
+
fmt.set_token_counter(lambda text: len(text) if text else 0)
|
|
360
|
+
body = {"tools": [{"name": "probe_tool", "description": "Expand topic", "input_schema": {"type": "object"}}]}
|
|
361
|
+
raw = len(json.dumps(body["tools"]))
|
|
362
|
+
assert fmt.estimate_tools_tokens(body) == math.ceil(raw * fmt._TOOLS_SCALE)
|
|
363
|
+
|
|
239
364
|
def test_extract_conversation_id(self):
|
|
240
365
|
body = {"messages": [
|
|
241
366
|
{"role": "user", "content": "hi"},
|
|
@@ -461,6 +586,34 @@ class TestGeminiFormat:
|
|
|
461
586
|
body = {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]}
|
|
462
587
|
assert len(self.fmt.get_messages(body)) == 1
|
|
463
588
|
|
|
589
|
+
def test_estimate_message_tokens_recurse_nested_inline_data(self):
|
|
590
|
+
msg = {
|
|
591
|
+
"role": "user",
|
|
592
|
+
"parts": [
|
|
593
|
+
{
|
|
594
|
+
"functionResponse": {
|
|
595
|
+
"name": "chrome",
|
|
596
|
+
"response": {
|
|
597
|
+
"content": [
|
|
598
|
+
{"text": "screenshot"},
|
|
599
|
+
{
|
|
600
|
+
"inline_data": {
|
|
601
|
+
"mime_type": "image/png",
|
|
602
|
+
"data": _ONE_BY_ONE_PNG_B64,
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
]
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
],
|
|
610
|
+
}
|
|
611
|
+
estimate = self.fmt.estimate_message_tokens(msg)
|
|
612
|
+
blanked = copy.deepcopy(msg)
|
|
613
|
+
blanked["parts"][0]["functionResponse"]["response"]["content"][1]["inline_data"]["data"] = ""
|
|
614
|
+
expected = self.fmt._count(json.dumps(blanked, default=str)) + 1
|
|
615
|
+
assert estimate == expected
|
|
616
|
+
|
|
464
617
|
def test_inject_context(self):
|
|
465
618
|
body = {"contents": [
|
|
466
619
|
{"role": "user", "parts": [{"text": "hi"}]},
|
|
@@ -175,6 +175,40 @@ class TestRequestCapturesPersistence:
|
|
|
175
175
|
assert [c["model"] for c in store.load_request_captures(conversation_id="c1")] == ["model-a"]
|
|
176
176
|
assert [c["model"] for c in store.load_request_captures(conversation_id="c2")] == ["model-b"]
|
|
177
177
|
|
|
178
|
+
def test_same_turns_are_isolated_by_turn_id(self, tmp_path):
|
|
179
|
+
store = SQLiteStore(tmp_path / "test.db")
|
|
180
|
+
for turn_id, model in (("req-a", "model-a"), ("req-b", "model-b")):
|
|
181
|
+
store.save_request_capture({
|
|
182
|
+
"turn": 7,
|
|
183
|
+
"turn_id": turn_id,
|
|
184
|
+
"ts": "2026-03-20T10:00:00+00:00",
|
|
185
|
+
"api_format": "anthropic",
|
|
186
|
+
"model": model,
|
|
187
|
+
"stream": False,
|
|
188
|
+
"message_count": 1,
|
|
189
|
+
"conversation_id": "c1",
|
|
190
|
+
"inbound_tags": [],
|
|
191
|
+
"response_tags": [],
|
|
192
|
+
"passthrough": False,
|
|
193
|
+
"inbound_tokens": 0,
|
|
194
|
+
"outbound_tokens": 0,
|
|
195
|
+
"inbound_bytes": 0,
|
|
196
|
+
"outbound_bytes": 0,
|
|
197
|
+
"context_tokens": 0,
|
|
198
|
+
"overhead_ms": 0,
|
|
199
|
+
"turns_dropped": 0,
|
|
200
|
+
"turns_stubbed": 0,
|
|
201
|
+
"message_preview": "",
|
|
202
|
+
"upstream_input_tokens": 0,
|
|
203
|
+
"upstream_output_tokens": 0,
|
|
204
|
+
"cache_creation_input_tokens": 0,
|
|
205
|
+
"cache_read_input_tokens": 0,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
loaded = store.load_request_captures(conversation_id="c1")
|
|
209
|
+
assert len(loaded) == 2
|
|
210
|
+
assert sorted(c["turn_id"] for c in loaded) == ["req-a", "req-b"]
|
|
211
|
+
|
|
178
212
|
def test_prune_turn_messages_keeps_suffix(self, tmp_path):
|
|
179
213
|
store = SQLiteStore(tmp_path / "test.db")
|
|
180
214
|
for turn in range(6):
|
|
@@ -288,6 +322,46 @@ class TestProxyMetricsStoreIntegration:
|
|
|
288
322
|
assert loaded[0]["upstream_input_tokens"] == 100
|
|
289
323
|
assert loaded[0]["upstream_output_tokens"] == 50
|
|
290
324
|
|
|
325
|
+
def test_capture_response_uses_turn_id_when_turn_collides(self, tmp_path):
|
|
326
|
+
from virtual_context.proxy.metrics import ProxyMetrics
|
|
327
|
+
|
|
328
|
+
store = self._make_store(tmp_path)
|
|
329
|
+
m = ProxyMetrics(store=store)
|
|
330
|
+
m.capture_request(
|
|
331
|
+
turn=1,
|
|
332
|
+
body={"model": "test-a", "messages": []},
|
|
333
|
+
api_format="anthropic",
|
|
334
|
+
conversation_id="c1",
|
|
335
|
+
turn_id="req-a",
|
|
336
|
+
)
|
|
337
|
+
m.capture_request(
|
|
338
|
+
turn=1,
|
|
339
|
+
body={"model": "test-b", "messages": []},
|
|
340
|
+
api_format="anthropic",
|
|
341
|
+
conversation_id="c1",
|
|
342
|
+
turn_id="req-b",
|
|
343
|
+
)
|
|
344
|
+
m.capture_response(
|
|
345
|
+
turn=1,
|
|
346
|
+
body={"content": "hello-a"},
|
|
347
|
+
upstream_input_tokens=100,
|
|
348
|
+
conversation_id="c1",
|
|
349
|
+
turn_id="req-a",
|
|
350
|
+
)
|
|
351
|
+
m.capture_response(
|
|
352
|
+
turn=1,
|
|
353
|
+
body={"content": "hello-b"},
|
|
354
|
+
upstream_input_tokens=200,
|
|
355
|
+
conversation_id="c1",
|
|
356
|
+
turn_id="req-b",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
loaded = sorted(
|
|
360
|
+
store.load_request_captures(conversation_id="c1"),
|
|
361
|
+
key=lambda capture: capture["turn_id"],
|
|
362
|
+
)
|
|
363
|
+
assert [capture["upstream_input_tokens"] for capture in loaded] == [100, 200]
|
|
364
|
+
|
|
291
365
|
def test_update_tags_persists(self, tmp_path):
|
|
292
366
|
from virtual_context.proxy.metrics import ProxyMetrics
|
|
293
367
|
store = self._make_store(tmp_path)
|
|
@@ -498,6 +498,7 @@ class TestSummaryFloor:
|
|
|
498
498
|
tag="authentication",
|
|
499
499
|
summary="JWT setup with RS256 signing and 1-hour expiry. Refresh token rotation.",
|
|
500
500
|
summary_tokens=25,
|
|
501
|
+
code_refs=[{"file": "auth/jwt.py", "line": 88, "symbol": "issue_token"}],
|
|
501
502
|
source_turn_numbers=[0, 1],
|
|
502
503
|
created_at=now,
|
|
503
504
|
updated_at=now,
|
|
@@ -531,6 +532,12 @@ class TestSummaryFloor:
|
|
|
531
532
|
assert len(result.summaries) == 2
|
|
532
533
|
assert result.total_tokens > 0
|
|
533
534
|
assert result.retrieval_metadata.get("summary_floor") is True
|
|
535
|
+
assert any(
|
|
536
|
+
summary.metadata.code_refs == [
|
|
537
|
+
{"file": "auth/jwt.py", "line": 88, "symbol": "issue_token"}
|
|
538
|
+
]
|
|
539
|
+
for summary in result.summaries
|
|
540
|
+
)
|
|
534
541
|
store.close()
|
|
535
542
|
|
|
536
543
|
def test_floor_inactive_pre_compaction(self, tmp_sqlite_db):
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from virtual_context.core.tag_generator import (
|
|
6
|
+
CODE_TAG_GENERATOR_PROMPTS,
|
|
6
7
|
KeywordTagGenerator,
|
|
7
8
|
LLMTagGenerator,
|
|
8
9
|
build_tag_generator,
|
|
@@ -122,6 +123,64 @@ class TestLLMTagGenerator:
|
|
|
122
123
|
assert generator._tag_vocabulary["auth"] == 10
|
|
123
124
|
assert generator._tag_vocabulary["database"] == 5
|
|
124
125
|
|
|
126
|
+
def test_code_refs_parsed_and_normalized(self):
|
|
127
|
+
provider = MockLLMProvider(
|
|
128
|
+
response=(
|
|
129
|
+
'{"tags": ["backend"], "primary": "backend", '
|
|
130
|
+
'"code_refs": ['
|
|
131
|
+
'{"file": " server.py ", "line": "42", "symbol": "handle_request"}, '
|
|
132
|
+
'{"path": "server.py", "line": 42, "name": "handle_request"}, '
|
|
133
|
+
'{"file": "retriever.py", "class": "ContextRetriever"}'
|
|
134
|
+
']}'
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
config = TagGeneratorConfig(type="llm", max_tags=5, min_tags=1)
|
|
138
|
+
generator = LLMTagGenerator(llm_provider=provider, config=config, code_mode=True)
|
|
139
|
+
|
|
140
|
+
result = generator.generate_tags("Patch the request path")
|
|
141
|
+
|
|
142
|
+
assert result.code_refs == [
|
|
143
|
+
{"file": "server.py", "line": 42, "symbol": "handle_request"},
|
|
144
|
+
{"file": "retriever.py", "symbol": "ContextRetriever"},
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
@pytest.mark.parametrize("prompt_mode", ["detailed", "compact", "small_model"])
|
|
148
|
+
def test_code_mode_uses_dedicated_tagger_prompt_family(self, prompt_mode):
|
|
149
|
+
from virtual_context.core.compactor import CODE_FACT_EXTRACTION_PROMPT
|
|
150
|
+
|
|
151
|
+
provider = MockLLMProvider(
|
|
152
|
+
response='{"tags": ["backend"], "primary": "backend", "fact_signals": [], "code_refs": []}'
|
|
153
|
+
)
|
|
154
|
+
config = TagGeneratorConfig(type="llm", max_tags=5, min_tags=1, prompt_mode=prompt_mode)
|
|
155
|
+
generator = LLMTagGenerator(llm_provider=provider, config=config, code_mode=True)
|
|
156
|
+
|
|
157
|
+
generator.generate_tags("We fixed the cache boundary in server.py.")
|
|
158
|
+
|
|
159
|
+
system = provider.calls[0]["system"]
|
|
160
|
+
assert system == CODE_TAG_GENERATOR_PROMPTS[prompt_mode].format(min_tags=1, max_tags=5)
|
|
161
|
+
assert "fact_signals" in system
|
|
162
|
+
assert "code_refs" in system
|
|
163
|
+
assert CODE_FACT_EXTRACTION_PROMPT.strip() not in system
|
|
164
|
+
|
|
165
|
+
def test_fact_signals_parsed_from_code_mode_response(self):
|
|
166
|
+
provider = MockLLMProvider(
|
|
167
|
+
response=(
|
|
168
|
+
'{"tags": ["backend"], "primary": "backend", '
|
|
169
|
+
'"fact_signals": ['
|
|
170
|
+
'{"subject": "cache boundary", "verb": "changed", "object": "moved above vc reminder injection", '
|
|
171
|
+
'"status": "completed", "fact_type": "world", "what": "Cache boundary changed to sit above the VC reminder injection."}'
|
|
172
|
+
'], "code_refs": []}'
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
config = TagGeneratorConfig(type="llm", max_tags=5, min_tags=1)
|
|
176
|
+
generator = LLMTagGenerator(llm_provider=provider, config=config, code_mode=True)
|
|
177
|
+
|
|
178
|
+
result = generator.generate_tags("Patch the cache boundary.")
|
|
179
|
+
|
|
180
|
+
assert len(result.fact_signals) == 1
|
|
181
|
+
assert result.fact_signals[0].subject == "cache boundary"
|
|
182
|
+
assert result.fact_signals[0].verb == "changed"
|
|
183
|
+
|
|
125
184
|
def test_select_relevant_store_tags_reuses_cached_embeddings(self):
|
|
126
185
|
provider = MockLLMProvider(response='{"tags": ["database"], "primary": "database"}')
|
|
127
186
|
cache_loads: list[list[str]] = []
|