devcouncil 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"""Optional semantic-layer integration for DevCouncil's LLM path.
|
|
2
|
+
|
|
3
|
+
Wraps ``semantic_layer`` cache, router, and compressor components behind a lazy
|
|
4
|
+
adapter that degrades gracefully when optional deps are not installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import atexit
|
|
11
|
+
import logging
|
|
12
|
+
import random
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from devcouncil.llm.provider import LLMResponse
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from devcouncil.app.config import DevCouncilConfig, SemanticLayerConfig as SemanticLayerSettings
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_ADAPTER_BY_ROOT: dict[Path, SemanticLayerAdapter] = {}
|
|
24
|
+
_SHUTDOWN_REGISTERED = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def prompt_text_from_messages(messages: list[dict[str, str]]) -> str:
|
|
28
|
+
"""Stable text key for semantic cache lookup from chat messages."""
|
|
29
|
+
parts: list[str] = []
|
|
30
|
+
for msg in messages:
|
|
31
|
+
role = msg.get("role", "user")
|
|
32
|
+
content = (msg.get("content") or "").strip()
|
|
33
|
+
if content:
|
|
34
|
+
parts.append(f"{role}:{content}")
|
|
35
|
+
return "\n".join(parts)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def semantic_deps_available() -> bool:
|
|
39
|
+
"""True when the lightweight semantic-layer runtime deps are importable."""
|
|
40
|
+
try:
|
|
41
|
+
import faiss # noqa: F401
|
|
42
|
+
import numpy # noqa: F401
|
|
43
|
+
except ImportError:
|
|
44
|
+
return False
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def semantic_embedding_deps_available() -> bool:
|
|
49
|
+
"""True when sentence-transformers is importable for embedding."""
|
|
50
|
+
try:
|
|
51
|
+
import sentence_transformers # noqa: F401
|
|
52
|
+
except ImportError:
|
|
53
|
+
return False
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _register_shutdown_once() -> None:
|
|
58
|
+
global _SHUTDOWN_REGISTERED
|
|
59
|
+
if _SHUTDOWN_REGISTERED:
|
|
60
|
+
return
|
|
61
|
+
atexit.register(_persist_all_adapters)
|
|
62
|
+
_SHUTDOWN_REGISTERED = True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _persist_all_adapters() -> None:
|
|
66
|
+
for adapter in list(_ADAPTER_BY_ROOT.values()):
|
|
67
|
+
try:
|
|
68
|
+
adapter.persist_cache()
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
logger.debug("Semantic cache shutdown persist failed: %s", exc)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def reset_semantic_adapters_for_tests() -> None:
|
|
74
|
+
"""Clear the per-project adapter singleton — tests only."""
|
|
75
|
+
_ADAPTER_BY_ROOT.clear()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SemanticLayerAdapter:
|
|
79
|
+
"""Lazy facade over semantic_layer components, driven by DevCouncil config."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, settings: SemanticLayerSettings, project_root: Path = Path(".")) -> None:
|
|
82
|
+
self.settings = settings
|
|
83
|
+
self.project_root = project_root
|
|
84
|
+
self._initialized = False
|
|
85
|
+
self._init_failed = False
|
|
86
|
+
self._cache: Any = None
|
|
87
|
+
self._router: Any = None
|
|
88
|
+
self._compressor: Any = None
|
|
89
|
+
self._embedder: Any = None
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def active(self) -> bool:
|
|
93
|
+
return self.settings.enabled and not self._init_failed
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def cache_enabled(self) -> bool:
|
|
97
|
+
return self.active and self.settings.cache.enabled
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def router_enabled(self) -> bool:
|
|
101
|
+
return self.active and self.settings.router.enabled
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def compressor_enabled(self) -> bool:
|
|
105
|
+
return self.active and self.settings.compressor.enabled
|
|
106
|
+
|
|
107
|
+
def cache_dir(self) -> Path:
|
|
108
|
+
return self.project_root / ".devcouncil" / "cache" / "semantic"
|
|
109
|
+
|
|
110
|
+
def cache_base_path(self) -> Path:
|
|
111
|
+
return self.cache_dir() / self.settings.cache.namespace
|
|
112
|
+
|
|
113
|
+
def _ensure_initialized(self) -> bool:
|
|
114
|
+
if self._initialized:
|
|
115
|
+
return not self._init_failed
|
|
116
|
+
self._initialized = True
|
|
117
|
+
if not self.settings.enabled:
|
|
118
|
+
self._init_failed = True
|
|
119
|
+
return False
|
|
120
|
+
if not semantic_deps_available():
|
|
121
|
+
logger.info(
|
|
122
|
+
"Semantic layer enabled in config but optional deps are missing "
|
|
123
|
+
"(install with: uv sync --group semantic). Falling back to standard LLM path."
|
|
124
|
+
)
|
|
125
|
+
self._init_failed = True
|
|
126
|
+
return False
|
|
127
|
+
try:
|
|
128
|
+
from semantic_layer.cache import SemanticCache
|
|
129
|
+
from semantic_layer.compressor import SemanticCompressor
|
|
130
|
+
from semantic_layer.config import (
|
|
131
|
+
CacheConfig,
|
|
132
|
+
CompressorConfig,
|
|
133
|
+
EmbeddingConfig,
|
|
134
|
+
RouterConfig,
|
|
135
|
+
)
|
|
136
|
+
from semantic_layer.embeddings import EmbeddingService
|
|
137
|
+
from semantic_layer.router import SemanticRouter
|
|
138
|
+
|
|
139
|
+
emb = self.settings.embedding
|
|
140
|
+
embedding_cfg = EmbeddingConfig(
|
|
141
|
+
model_name=emb.model_name,
|
|
142
|
+
dimension=emb.dimension,
|
|
143
|
+
device=emb.device, # type: ignore[arg-type]
|
|
144
|
+
batch_size=emb.batch_size,
|
|
145
|
+
normalize=emb.normalize,
|
|
146
|
+
)
|
|
147
|
+
self._embedder = EmbeddingService.get_instance(embedding_cfg)
|
|
148
|
+
|
|
149
|
+
cache_cfg = self.settings.cache
|
|
150
|
+
self._cache = SemanticCache(
|
|
151
|
+
CacheConfig(
|
|
152
|
+
backend=cache_cfg.backend, # type: ignore[arg-type]
|
|
153
|
+
similarity_threshold=cache_cfg.similarity_threshold,
|
|
154
|
+
ood_threshold=cache_cfg.ood_threshold,
|
|
155
|
+
margin_threshold=cache_cfg.margin_threshold,
|
|
156
|
+
ttl_seconds=cache_cfg.ttl_seconds,
|
|
157
|
+
max_entries=cache_cfg.max_entries,
|
|
158
|
+
namespace=cache_cfg.namespace,
|
|
159
|
+
exploration_rate=cache_cfg.exploration_rate,
|
|
160
|
+
),
|
|
161
|
+
self._embedder,
|
|
162
|
+
embedding_cfg.dimension,
|
|
163
|
+
)
|
|
164
|
+
self._load_persisted_cache()
|
|
165
|
+
|
|
166
|
+
router_cfg = self.settings.router
|
|
167
|
+
self._router = SemanticRouter(
|
|
168
|
+
RouterConfig(
|
|
169
|
+
complexity_threshold=router_cfg.complexity_threshold,
|
|
170
|
+
small_model=router_cfg.small_model or "qwen2.5:1.5b",
|
|
171
|
+
large_model=router_cfg.large_model or "llama3.1:8b",
|
|
172
|
+
),
|
|
173
|
+
self._embedder,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
comp_cfg = self.settings.compressor
|
|
177
|
+
self._compressor = SemanticCompressor(
|
|
178
|
+
CompressorConfig(
|
|
179
|
+
token_budget=comp_cfg.token_budget,
|
|
180
|
+
top_k=comp_cfg.top_k,
|
|
181
|
+
chunk_token_size=comp_cfg.chunk_token_size,
|
|
182
|
+
chunk_overlap=comp_cfg.chunk_overlap,
|
|
183
|
+
min_chunk_score=comp_cfg.min_chunk_score,
|
|
184
|
+
mmr_lambda=comp_cfg.mmr_lambda,
|
|
185
|
+
),
|
|
186
|
+
self._embedder,
|
|
187
|
+
)
|
|
188
|
+
if self.cache_enabled:
|
|
189
|
+
_register_shutdown_once()
|
|
190
|
+
return True
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
logger.warning("Semantic layer initialization failed; disabling: %s", exc)
|
|
193
|
+
self._init_failed = True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def _load_persisted_cache(self) -> None:
|
|
197
|
+
if self._cache is None:
|
|
198
|
+
return
|
|
199
|
+
base = self.cache_base_path()
|
|
200
|
+
faiss_path = Path(f"{base}.faiss")
|
|
201
|
+
json_path = Path(f"{base}.json")
|
|
202
|
+
if not faiss_path.is_file() or not json_path.is_file():
|
|
203
|
+
return
|
|
204
|
+
try:
|
|
205
|
+
self._cache.load(str(base))
|
|
206
|
+
logger.info(
|
|
207
|
+
"Loaded semantic cache from %s (%d entries)",
|
|
208
|
+
base,
|
|
209
|
+
len(self._cache._entries),
|
|
210
|
+
)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
logger.warning("Could not load persisted semantic cache from %s: %s", base, exc)
|
|
213
|
+
|
|
214
|
+
def persist_cache(self) -> None:
|
|
215
|
+
"""Write the in-memory FAISS index and metadata to disk."""
|
|
216
|
+
if not self.cache_enabled or self._cache is None:
|
|
217
|
+
return
|
|
218
|
+
base = self.cache_base_path()
|
|
219
|
+
try:
|
|
220
|
+
base.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
self._cache.persist(str(base))
|
|
222
|
+
logger.debug("Persisted semantic cache to %s (%d entries)", base, len(self._cache._entries))
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.warning("Semantic cache persist failed: %s", exc)
|
|
225
|
+
|
|
226
|
+
def warm_up(self) -> bool:
|
|
227
|
+
"""Preload the embedding model so the first LLM call is not blocked."""
|
|
228
|
+
if not self.settings.enabled:
|
|
229
|
+
return False
|
|
230
|
+
if not self._ensure_initialized() or self._embedder is None:
|
|
231
|
+
return False
|
|
232
|
+
try:
|
|
233
|
+
self._embedder.embed_one("warmup")
|
|
234
|
+
logger.debug("Semantic embedding model warmed up (%s)", self.settings.embedding.model_name)
|
|
235
|
+
return True
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
logger.warning("Semantic embedding warm-up failed: %s", exc)
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
async def _embed_one_async(self, text: str) -> Any:
|
|
241
|
+
assert self._embedder is not None
|
|
242
|
+
return await asyncio.to_thread(self._embedder.embed_one, text)
|
|
243
|
+
|
|
244
|
+
def _intent_for_prompt(self, prompt: str, query_vec: Any, *, model: str, role: str) -> str:
|
|
245
|
+
route = self._router.route(prompt, query_vec) if self._router else None
|
|
246
|
+
intent = f"{role}:{model}"
|
|
247
|
+
if route is not None:
|
|
248
|
+
intent = f"{role}:{model}:{route.intent}"
|
|
249
|
+
return intent
|
|
250
|
+
|
|
251
|
+
def lookup_cache(
|
|
252
|
+
self,
|
|
253
|
+
messages: list[dict[str, str]],
|
|
254
|
+
*,
|
|
255
|
+
model: str,
|
|
256
|
+
role: str,
|
|
257
|
+
) -> LLMResponse | None:
|
|
258
|
+
"""Return a cached LLM response for a semantically similar prompt, if any."""
|
|
259
|
+
if not self.cache_enabled or not self._ensure_initialized():
|
|
260
|
+
return None
|
|
261
|
+
assert self._cache is not None and self._embedder is not None
|
|
262
|
+
|
|
263
|
+
prompt = prompt_text_from_messages(messages)
|
|
264
|
+
if not prompt.strip():
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
query_vec = self._embedder.embed_one(prompt)
|
|
269
|
+
return self._lookup_with_vector(prompt, query_vec, model=model, role=role)
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
logger.debug("Semantic cache lookup failed: %s", exc)
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
async def lookup_cache_async(
|
|
275
|
+
self,
|
|
276
|
+
messages: list[dict[str, str]],
|
|
277
|
+
*,
|
|
278
|
+
model: str,
|
|
279
|
+
role: str,
|
|
280
|
+
) -> LLMResponse | None:
|
|
281
|
+
"""Async semantic cache lookup; embedding runs in a worker thread."""
|
|
282
|
+
if not self.cache_enabled or not self._ensure_initialized():
|
|
283
|
+
return None
|
|
284
|
+
assert self._cache is not None and self._embedder is not None
|
|
285
|
+
|
|
286
|
+
prompt = prompt_text_from_messages(messages)
|
|
287
|
+
if not prompt.strip():
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
query_vec = await self._embed_one_async(prompt)
|
|
292
|
+
return self._lookup_with_vector(prompt, query_vec, model=model, role=role)
|
|
293
|
+
except Exception as exc:
|
|
294
|
+
logger.debug("Semantic cache lookup failed: %s", exc)
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
def _lookup_with_vector(
|
|
298
|
+
self,
|
|
299
|
+
prompt: str,
|
|
300
|
+
query_vec: Any,
|
|
301
|
+
*,
|
|
302
|
+
model: str,
|
|
303
|
+
role: str,
|
|
304
|
+
) -> LLMResponse | None:
|
|
305
|
+
assert self._cache is not None
|
|
306
|
+
intent = self._intent_for_prompt(prompt, query_vec, model=model, role=role)
|
|
307
|
+
force_miss = random.random() < self.settings.cache.exploration_rate
|
|
308
|
+
result = self._cache.lookup(prompt, query_vec, intent=intent, force_miss=force_miss)
|
|
309
|
+
if not result.hit or not result.response:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
logger.info(
|
|
313
|
+
"Semantic cache HIT role=%s model=%s similarity=%.3f",
|
|
314
|
+
role,
|
|
315
|
+
model,
|
|
316
|
+
result.similarity,
|
|
317
|
+
)
|
|
318
|
+
return LLMResponse(
|
|
319
|
+
content=result.response,
|
|
320
|
+
model=model,
|
|
321
|
+
usage={"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
|
|
322
|
+
raw_response={
|
|
323
|
+
"semantic_cache": True,
|
|
324
|
+
"similarity": result.similarity,
|
|
325
|
+
"entry_id": result.entry_id,
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def store_cache(
|
|
330
|
+
self,
|
|
331
|
+
messages: list[dict[str, str]],
|
|
332
|
+
response: LLMResponse,
|
|
333
|
+
*,
|
|
334
|
+
model: str,
|
|
335
|
+
role: str,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Persist a validated LLM response in the semantic cache."""
|
|
338
|
+
if not self.cache_enabled or not self._ensure_initialized():
|
|
339
|
+
return
|
|
340
|
+
assert self._cache is not None and self._embedder is not None
|
|
341
|
+
|
|
342
|
+
prompt = prompt_text_from_messages(messages)
|
|
343
|
+
if not prompt.strip() or not (response.content or "").strip():
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
query_vec = self._embedder.embed_one(prompt)
|
|
348
|
+
intent = self._intent_for_prompt(prompt, query_vec, model=model, role=role)
|
|
349
|
+
self._cache.put(prompt, response.content, query_vec, intent=intent)
|
|
350
|
+
except Exception as exc:
|
|
351
|
+
logger.debug("Semantic cache store failed: %s", exc)
|
|
352
|
+
|
|
353
|
+
async def store_cache_async(
|
|
354
|
+
self,
|
|
355
|
+
messages: list[dict[str, str]],
|
|
356
|
+
response: LLMResponse,
|
|
357
|
+
*,
|
|
358
|
+
model: str,
|
|
359
|
+
role: str,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Async semantic cache store; embedding runs in a worker thread."""
|
|
362
|
+
if not self.cache_enabled or not self._ensure_initialized():
|
|
363
|
+
return
|
|
364
|
+
assert self._cache is not None and self._embedder is not None
|
|
365
|
+
|
|
366
|
+
prompt = prompt_text_from_messages(messages)
|
|
367
|
+
if not prompt.strip() or not (response.content or "").strip():
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
query_vec = await self._embed_one_async(prompt)
|
|
372
|
+
intent = self._intent_for_prompt(prompt, query_vec, model=model, role=role)
|
|
373
|
+
self._cache.put(prompt, response.content, query_vec, intent=intent)
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
logger.debug("Semantic cache store failed: %s", exc)
|
|
376
|
+
|
|
377
|
+
async def maybe_route_model_async(
|
|
378
|
+
self,
|
|
379
|
+
messages: list[dict[str, str]],
|
|
380
|
+
*,
|
|
381
|
+
configured_model: str,
|
|
382
|
+
role_provider: str | None,
|
|
383
|
+
) -> str:
|
|
384
|
+
"""Async model routing; embedding runs in a worker thread."""
|
|
385
|
+
if not self.router_enabled or not self._ensure_initialized():
|
|
386
|
+
return configured_model
|
|
387
|
+
assert self._router is not None and self._embedder is not None
|
|
388
|
+
|
|
389
|
+
if (role_provider or "").strip().lower() not in {"ollama", "ollama-local", "ollama_local"}:
|
|
390
|
+
return configured_model
|
|
391
|
+
|
|
392
|
+
prompt = prompt_text_from_messages(messages)
|
|
393
|
+
if not prompt.strip():
|
|
394
|
+
return configured_model
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
query_vec = await self._embed_one_async(prompt)
|
|
398
|
+
return self._route_with_vector(prompt, query_vec, configured_model=configured_model)
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
logger.debug("Semantic routing failed: %s", exc)
|
|
401
|
+
return configured_model
|
|
402
|
+
|
|
403
|
+
def _route_with_vector(self, prompt: str, query_vec: Any, *, configured_model: str) -> str:
|
|
404
|
+
assert self._router is not None
|
|
405
|
+
decision = self._router.route(prompt, query_vec)
|
|
406
|
+
logger.debug(
|
|
407
|
+
"Semantic route role=%s complexity=%.3f tier=%s model=%s",
|
|
408
|
+
configured_model,
|
|
409
|
+
decision.complexity_score,
|
|
410
|
+
decision.tier.value,
|
|
411
|
+
decision.model_name,
|
|
412
|
+
)
|
|
413
|
+
return str(decision.model_name)
|
|
414
|
+
|
|
415
|
+
async def maybe_compress_messages_async(self, messages: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
416
|
+
"""Async message compression; embedding runs in a worker thread."""
|
|
417
|
+
if not self.compressor_enabled or not self._ensure_initialized():
|
|
418
|
+
return messages
|
|
419
|
+
assert self._compressor is not None and self._embedder is not None
|
|
420
|
+
|
|
421
|
+
prepared = self._compression_inputs(messages)
|
|
422
|
+
if prepared is None:
|
|
423
|
+
return messages
|
|
424
|
+
query, context_docs = prepared
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
query_vec = await self._embed_one_async(query)
|
|
428
|
+
return self._compress_with_vector(messages, query, context_docs, query_vec)
|
|
429
|
+
except Exception as exc:
|
|
430
|
+
logger.debug("Semantic compression failed: %s", exc)
|
|
431
|
+
return messages
|
|
432
|
+
|
|
433
|
+
def _compression_inputs(self, messages: list[dict[str, str]]) -> tuple[str, list[str]] | None:
|
|
434
|
+
min_chars = self.settings.compressor.min_chars
|
|
435
|
+
total_chars = sum(len(m.get("content") or "") for m in messages)
|
|
436
|
+
if total_chars < min_chars:
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
user_messages = [m for m in messages if m.get("role") == "user" and (m.get("content") or "").strip()]
|
|
440
|
+
if not user_messages:
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
query = user_messages[-1]["content"]
|
|
444
|
+
context_docs = [
|
|
445
|
+
m["content"]
|
|
446
|
+
for m in messages
|
|
447
|
+
if m is not user_messages[-1] and (m.get("content") or "").strip()
|
|
448
|
+
]
|
|
449
|
+
if not context_docs:
|
|
450
|
+
blob = query
|
|
451
|
+
if len(blob) <= min_chars:
|
|
452
|
+
return None
|
|
453
|
+
split_at = max(500, len(blob) - 2000)
|
|
454
|
+
context_docs = [blob[:split_at]]
|
|
455
|
+
query = blob[split_at:].strip() or blob[-2000:]
|
|
456
|
+
return query, context_docs
|
|
457
|
+
|
|
458
|
+
def _compress_with_vector(
|
|
459
|
+
self,
|
|
460
|
+
messages: list[dict[str, str]],
|
|
461
|
+
query: str,
|
|
462
|
+
context_docs: list[str],
|
|
463
|
+
query_vec: Any,
|
|
464
|
+
) -> list[dict[str, str]]:
|
|
465
|
+
assert self._compressor is not None
|
|
466
|
+
compressed = self._compressor.compress(query, context_docs, query_vec)
|
|
467
|
+
if not compressed.text.strip():
|
|
468
|
+
return messages
|
|
469
|
+
|
|
470
|
+
user_messages = [m for m in messages if m.get("role") == "user" and (m.get("content") or "").strip()]
|
|
471
|
+
compressed_messages: list[dict[str, str]] = []
|
|
472
|
+
replaced = False
|
|
473
|
+
for msg in messages:
|
|
474
|
+
if msg.get("role") == "user" and not replaced and msg is user_messages[-1]:
|
|
475
|
+
compressed_messages.append(
|
|
476
|
+
{
|
|
477
|
+
"role": "user",
|
|
478
|
+
"content": f"Context:\n{compressed.text}\n\nQuestion: {query}",
|
|
479
|
+
}
|
|
480
|
+
)
|
|
481
|
+
replaced = True
|
|
482
|
+
elif msg.get("role") != "user" or msg is not user_messages[-1]:
|
|
483
|
+
if msg.get("role") != "user":
|
|
484
|
+
compressed_messages.append(dict(msg))
|
|
485
|
+
if not replaced:
|
|
486
|
+
compressed_messages.append(
|
|
487
|
+
{"role": "user", "content": f"Context:\n{compressed.text}\n\nQuestion: {query}"}
|
|
488
|
+
)
|
|
489
|
+
logger.info(
|
|
490
|
+
"Semantic compression applied: %d -> %d chunks, ~%d tokens",
|
|
491
|
+
compressed.chunks_total,
|
|
492
|
+
compressed.chunks_used,
|
|
493
|
+
compressed.estimated_tokens,
|
|
494
|
+
)
|
|
495
|
+
return compressed_messages
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def check_semantic_layer(
|
|
499
|
+
project_root: Path,
|
|
500
|
+
config: DevCouncilConfig | None = None,
|
|
501
|
+
) -> list[tuple[str, str, str]]:
|
|
502
|
+
"""Doctor rows for semantic-layer readiness (never raises)."""
|
|
503
|
+
ok = "[green]OK[/green]"
|
|
504
|
+
warn = "[yellow]WARN[/yellow]"
|
|
505
|
+
info = "[cyan]INFO[/cyan]"
|
|
506
|
+
rows: list[tuple[str, str, str]] = []
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
from devcouncil.app.config import load_config
|
|
510
|
+
|
|
511
|
+
cfg = config if config is not None else load_config(project_root)
|
|
512
|
+
except Exception as exc:
|
|
513
|
+
rows.append(("Semantic layer", info, f"Config unavailable; skipping semantic checks ({exc})."))
|
|
514
|
+
return rows
|
|
515
|
+
|
|
516
|
+
sem = cfg.semantic_layer
|
|
517
|
+
if not sem.enabled:
|
|
518
|
+
rows.append(
|
|
519
|
+
(
|
|
520
|
+
"Semantic layer",
|
|
521
|
+
info,
|
|
522
|
+
"Disabled. Enable with: dev config set semantic_layer.enabled true "
|
|
523
|
+
"(requires: uv sync --group semantic).",
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
return rows
|
|
527
|
+
|
|
528
|
+
if not semantic_deps_available():
|
|
529
|
+
rows.append(
|
|
530
|
+
(
|
|
531
|
+
"Semantic deps",
|
|
532
|
+
warn,
|
|
533
|
+
"Missing faiss/numpy. Install with: uv sync --group semantic.",
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
return rows
|
|
537
|
+
|
|
538
|
+
rows.append(("Semantic layer", ok, "Enabled in config."))
|
|
539
|
+
|
|
540
|
+
if not semantic_embedding_deps_available():
|
|
541
|
+
rows.append(
|
|
542
|
+
(
|
|
543
|
+
"Semantic embedder",
|
|
544
|
+
warn,
|
|
545
|
+
"Missing sentence-transformers. Install with: uv sync --group semantic.",
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
rows.append(
|
|
550
|
+
(
|
|
551
|
+
"Semantic embedder",
|
|
552
|
+
ok,
|
|
553
|
+
f"sentence-transformers available; model={sem.embedding.model_name}.",
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
cache_dir = project_root / ".devcouncil" / "cache" / "semantic"
|
|
558
|
+
try:
|
|
559
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
560
|
+
probe = cache_dir / ".write_probe"
|
|
561
|
+
probe.write_text("ok", encoding="utf-8")
|
|
562
|
+
probe.unlink(missing_ok=True)
|
|
563
|
+
persisted = cache_dir / f"{sem.cache.namespace}.faiss"
|
|
564
|
+
detail = f"Writable at {cache_dir}."
|
|
565
|
+
if persisted.is_file():
|
|
566
|
+
detail += f" Persisted index present ({sem.cache.namespace})."
|
|
567
|
+
rows.append(("Semantic cache dir", ok, detail))
|
|
568
|
+
except Exception as exc:
|
|
569
|
+
rows.append(
|
|
570
|
+
(
|
|
571
|
+
"Semantic cache dir",
|
|
572
|
+
warn,
|
|
573
|
+
f"Not writable at {cache_dir}: {exc}.",
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
adapter = _ADAPTER_BY_ROOT.get(project_root.resolve())
|
|
578
|
+
if adapter is None:
|
|
579
|
+
adapter = SemanticLayerAdapter(sem, project_root)
|
|
580
|
+
if adapter.warm_up():
|
|
581
|
+
rows.append(("Semantic warm-up", ok, "Embedding model loaded successfully."))
|
|
582
|
+
else:
|
|
583
|
+
rows.append(
|
|
584
|
+
(
|
|
585
|
+
"Semantic warm-up",
|
|
586
|
+
warn,
|
|
587
|
+
f"Could not load embedding model {sem.embedding.model_name!r}. "
|
|
588
|
+
"First LLM call will retry or fall back to the standard path.",
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
return rows
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def load_semantic_adapter(project_root: Path) -> SemanticLayerAdapter | None:
|
|
596
|
+
"""Build a semantic adapter from project config, or None when disabled/unavailable."""
|
|
597
|
+
try:
|
|
598
|
+
from devcouncil.app.config import load_config
|
|
599
|
+
|
|
600
|
+
cfg = load_config(project_root)
|
|
601
|
+
if not cfg.semantic_layer.enabled:
|
|
602
|
+
return None
|
|
603
|
+
key = project_root.resolve()
|
|
604
|
+
existing = _ADAPTER_BY_ROOT.get(key)
|
|
605
|
+
if existing is not None:
|
|
606
|
+
return existing
|
|
607
|
+
adapter = SemanticLayerAdapter(cfg.semantic_layer, project_root)
|
|
608
|
+
_ADAPTER_BY_ROOT[key] = adapter
|
|
609
|
+
return adapter
|
|
610
|
+
except FileNotFoundError:
|
|
611
|
+
return None
|
|
612
|
+
except Exception as exc:
|
|
613
|
+
logger.debug("Could not load semantic layer config: %s", exc)
|
|
614
|
+
return None
|
|
@@ -9,13 +9,15 @@ from datetime import datetime, timezone
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Callable
|
|
11
11
|
|
|
12
|
-
import yaml
|
|
12
|
+
import yaml
|
|
13
13
|
|
|
14
14
|
from devcouncil.executors.agent_registry import (
|
|
15
15
|
load_agent_profiles,
|
|
16
16
|
load_cli_agent_specs,
|
|
17
17
|
normalize_agent_name,
|
|
18
18
|
)
|
|
19
|
+
from devcouncil.utils.fsio import atomic_write_text
|
|
20
|
+
from devcouncil.utils.json_persist import read_json, write_json
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
DEFAULT_OBJECTIVE = (
|
|
@@ -156,7 +158,7 @@ def load_agent_eval_dataset(path: Path) -> list[dict[str, Any]]:
|
|
|
156
158
|
if line.strip()
|
|
157
159
|
]
|
|
158
160
|
else:
|
|
159
|
-
raw =
|
|
161
|
+
raw = read_json(evals_path)
|
|
160
162
|
if isinstance(raw, dict):
|
|
161
163
|
if isinstance(raw.get("examples"), list):
|
|
162
164
|
examples = raw["examples"]
|
|
@@ -304,7 +306,7 @@ def _default_artifact_path(project_root: Path, agent: str, profile_name: str) ->
|
|
|
304
306
|
|
|
305
307
|
def _write_result_artifact(path: Path, payload: dict[str, Any]) -> None:
|
|
306
308
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
307
|
-
path
|
|
309
|
+
write_json(path, payload)
|
|
308
310
|
|
|
309
311
|
|
|
310
312
|
def _apply_profile_preamble(project_root: Path, profile_name: str, best_preamble: str) -> None:
|
|
@@ -315,4 +317,4 @@ def _apply_profile_preamble(project_root: Path, profile_name: str, best_preamble
|
|
|
315
317
|
profile = profiles.setdefault(profile_name, {})
|
|
316
318
|
profile["prompt_preamble"] = best_preamble
|
|
317
319
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
318
|
-
config_path
|
|
320
|
+
atomic_write_text(config_path, yaml.safe_dump(raw_config, sort_keys=False))
|
|
@@ -36,13 +36,14 @@ from collections.abc import Awaitable, Callable
|
|
|
36
36
|
from dataclasses import asdict, dataclass, field
|
|
37
37
|
from datetime import datetime, timezone
|
|
38
38
|
from pathlib import Path
|
|
39
|
-
from typing import Any, Literal
|
|
39
|
+
from typing import Any, Literal, cast
|
|
40
40
|
|
|
41
41
|
from pydantic import BaseModel, Field
|
|
42
42
|
|
|
43
43
|
# Reuse the small list-coercion helper from the agent-profile optimizer so both
|
|
44
44
|
# optimizers share one eval-dataset field contract (required_terms, forbidden_terms…).
|
|
45
45
|
from devcouncil.optimization.gepa_agent import _string_list
|
|
46
|
+
from devcouncil.utils.json_persist import write_json
|
|
46
47
|
|
|
47
48
|
logger = logging.getLogger(__name__)
|
|
48
49
|
|
|
@@ -593,7 +594,7 @@ def make_llm_rollout(
|
|
|
593
594
|
result = await router.complete_structured(
|
|
594
595
|
role, messages, _AgentAnswer, fallback=_AgentAnswer(answer="")
|
|
595
596
|
)
|
|
596
|
-
return result.answer
|
|
597
|
+
return str(result.answer)
|
|
597
598
|
|
|
598
599
|
return rollout
|
|
599
600
|
|
|
@@ -632,8 +633,11 @@ def make_llm_optimizer(
|
|
|
632
633
|
{"role": "system", "content": system},
|
|
633
634
|
{"role": "user", "content": user},
|
|
634
635
|
]
|
|
635
|
-
return
|
|
636
|
-
|
|
636
|
+
return cast(
|
|
637
|
+
SkillEditProposal,
|
|
638
|
+
await router.complete_structured(
|
|
639
|
+
role, messages, SkillEditProposal, fallback=SkillEditProposal()
|
|
640
|
+
),
|
|
637
641
|
)
|
|
638
642
|
|
|
639
643
|
return optimizer
|
|
@@ -670,4 +674,4 @@ def write_result_artifact(path: Path, result: SkillOptResult, *, objective: str,
|
|
|
670
674
|
"applied": result.applied,
|
|
671
675
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
672
676
|
}
|
|
673
|
-
path
|
|
677
|
+
write_json(path, payload)
|