@tyroneross/build-loop 0.30.3
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/.agents/plugins/marketplace.json +10 -0
- package/.claude-plugin/marketplace.json +38 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.codex-plugin/plugin.json +41 -0
- package/.cursor/rules/build-loop-surface.mdc +22 -0
- package/AGENTS.md +498 -0
- package/LICENSE +202 -0
- package/README.md +403 -0
- package/agents/alignment-checker.md +124 -0
- package/agents/api-assessor.md +136 -0
- package/agents/architecture-scout.md +185 -0
- package/agents/assessment-orchestrator.md +163 -0
- package/agents/build-orchestrator.md +198 -0
- package/agents/database-assessor.md +133 -0
- package/agents/design-contract-specialist.md +239 -0
- package/agents/fact-checker.md +69 -0
- package/agents/fix-critique.md +161 -0
- package/agents/frontend-assessor.md +136 -0
- package/agents/implementer.md +185 -0
- package/agents/independent-auditor.md +143 -0
- package/agents/mock-scanner.md +78 -0
- package/agents/optimize-runner.md +112 -0
- package/agents/overfitting-reviewer.md +96 -0
- package/agents/performance-assessor.md +159 -0
- package/agents/plan-critic.md +85 -0
- package/agents/promotion-reviewer.md +121 -0
- package/agents/recurring-pattern-detector.md +248 -0
- package/agents/retrospective-synthesizer.md +109 -0
- package/agents/root-cause-investigator.md +285 -0
- package/agents/scope-auditor.md +121 -0
- package/agents/security-reviewer.md +151 -0
- package/agents/self-improvement-architect.md +125 -0
- package/agents/synthesis-critic.md +99 -0
- package/agents/transcript-pattern-miner.md +165 -0
- package/agents/ui-validator.md +193 -0
- package/codex-skills/build-loop/SKILL.md +20 -0
- package/codex-skills/debug-loop/SKILL.md +18 -0
- package/codex-skills/handoff/SKILL.md +22 -0
- package/codex-skills/knowledge/SKILL.md +21 -0
- package/codex-skills/optimize/SKILL.md +18 -0
- package/codex-skills/research/SKILL.md +18 -0
- package/commands/agent-rally-point.md +301 -0
- package/commands/assess.md +41 -0
- package/commands/debug.md +20 -0
- package/commands/debugger-detail.md +34 -0
- package/commands/debugger-scan.md +15 -0
- package/commands/debugger-status.md +20 -0
- package/commands/debugger.md +32 -0
- package/commands/handoff.md +113 -0
- package/commands/knowledge-review.md +16 -0
- package/commands/optimize.md +31 -0
- package/commands/plan-verify.md +39 -0
- package/commands/promote-experiment.md +129 -0
- package/commands/research.md +22 -0
- package/commands/run.md +12 -0
- package/commands/self-review.md +51 -0
- package/commands/start-prd.md +57 -0
- package/commands/test.md +34 -0
- package/dist/src/assessment-orchestrator.d.ts +78 -0
- package/dist/src/assessment-orchestrator.d.ts.map +1 -0
- package/dist/src/assessment-orchestrator.js +304 -0
- package/dist/src/assessment-orchestrator.js.map +1 -0
- package/dist/src/audit-miner.d.ts +21 -0
- package/dist/src/audit-miner.d.ts.map +1 -0
- package/dist/src/audit-miner.js +421 -0
- package/dist/src/audit-miner.js.map +1 -0
- package/dist/src/batch-operations.d.ts +30 -0
- package/dist/src/batch-operations.d.ts.map +1 -0
- package/dist/src/batch-operations.js +339 -0
- package/dist/src/batch-operations.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +176 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-engine.d.ts +45 -0
- package/dist/src/context-engine.d.ts.map +1 -0
- package/dist/src/context-engine.js +135 -0
- package/dist/src/context-engine.js.map +1 -0
- package/dist/src/debug-wrapper.d.ts +58 -0
- package/dist/src/debug-wrapper.d.ts.map +1 -0
- package/dist/src/debug-wrapper.js +285 -0
- package/dist/src/debug-wrapper.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +149 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interactive-verifier.d.ts +26 -0
- package/dist/src/interactive-verifier.d.ts.map +1 -0
- package/dist/src/interactive-verifier.js +435 -0
- package/dist/src/interactive-verifier.js.map +1 -0
- package/dist/src/lessons.d.ts +59 -0
- package/dist/src/lessons.d.ts.map +1 -0
- package/dist/src/lessons.js +172 -0
- package/dist/src/lessons.js.map +1 -0
- package/dist/src/log-reader.d.ts +39 -0
- package/dist/src/log-reader.d.ts.map +1 -0
- package/dist/src/log-reader.js +291 -0
- package/dist/src/log-reader.js.map +1 -0
- package/dist/src/logger.d.ts +59 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +328 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/parallel-retrieval.d.ts +52 -0
- package/dist/src/parallel-retrieval.d.ts.map +1 -0
- package/dist/src/parallel-retrieval.js +309 -0
- package/dist/src/parallel-retrieval.js.map +1 -0
- package/dist/src/pattern-extractor.d.ts +30 -0
- package/dist/src/pattern-extractor.d.ts.map +1 -0
- package/dist/src/pattern-extractor.js +386 -0
- package/dist/src/pattern-extractor.js.map +1 -0
- package/dist/src/result-aggregator.d.ts +44 -0
- package/dist/src/result-aggregator.d.ts.map +1 -0
- package/dist/src/result-aggregator.js +315 -0
- package/dist/src/result-aggregator.js.map +1 -0
- package/dist/src/retrieval.d.ts +139 -0
- package/dist/src/retrieval.d.ts.map +1 -0
- package/dist/src/retrieval.js +724 -0
- package/dist/src/retrieval.js.map +1 -0
- package/dist/src/setup/auto-setup.d.ts +15 -0
- package/dist/src/setup/auto-setup.d.ts.map +1 -0
- package/dist/src/setup/auto-setup.js +157 -0
- package/dist/src/setup/auto-setup.js.map +1 -0
- package/dist/src/setup/configure-hooks.d.ts +8 -0
- package/dist/src/setup/configure-hooks.d.ts.map +1 -0
- package/dist/src/setup/configure-hooks.js +16 -0
- package/dist/src/setup/configure-hooks.js.map +1 -0
- package/dist/src/setup/create-slash-commands.d.ts +8 -0
- package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
- package/dist/src/setup/create-slash-commands.js +101 -0
- package/dist/src/setup/create-slash-commands.js.map +1 -0
- package/dist/src/setup/index.d.ts +8 -0
- package/dist/src/setup/index.d.ts.map +1 -0
- package/dist/src/setup/index.js +17 -0
- package/dist/src/setup/index.js.map +1 -0
- package/dist/src/setup/inject-claude-md.d.ts +10 -0
- package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
- package/dist/src/setup/inject-claude-md.js +150 -0
- package/dist/src/setup/inject-claude-md.js.map +1 -0
- package/dist/src/setup/uninstall.d.ts +22 -0
- package/dist/src/setup/uninstall.d.ts.map +1 -0
- package/dist/src/setup/uninstall.js +235 -0
- package/dist/src/setup/uninstall.js.map +1 -0
- package/dist/src/storage.d.ts +192 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +1037 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/string-similarity.d.ts +8 -0
- package/dist/src/string-similarity.d.ts.map +1 -0
- package/dist/src/string-similarity.js +57 -0
- package/dist/src/string-similarity.js.map +1 -0
- package/dist/src/traces/adapters/browser.d.ts +69 -0
- package/dist/src/traces/adapters/browser.d.ts.map +1 -0
- package/dist/src/traces/adapters/browser.js +280 -0
- package/dist/src/traces/adapters/browser.js.map +1 -0
- package/dist/src/traces/adapters/index.d.ts +23 -0
- package/dist/src/traces/adapters/index.d.ts.map +1 -0
- package/dist/src/traces/adapters/index.js +67 -0
- package/dist/src/traces/adapters/index.js.map +1 -0
- package/dist/src/traces/adapters/langchain.d.ts +42 -0
- package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
- package/dist/src/traces/adapters/langchain.js +151 -0
- package/dist/src/traces/adapters/langchain.js.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.js +164 -0
- package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
- package/dist/src/traces/adapters/sentry.d.ts +58 -0
- package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
- package/dist/src/traces/adapters/sentry.js +212 -0
- package/dist/src/traces/adapters/sentry.js.map +1 -0
- package/dist/src/traces/index.d.ts +10 -0
- package/dist/src/traces/index.d.ts.map +1 -0
- package/dist/src/traces/index.js +45 -0
- package/dist/src/traces/index.js.map +1 -0
- package/dist/src/traces/storage.d.ts +78 -0
- package/dist/src/traces/storage.d.ts.map +1 -0
- package/dist/src/traces/storage.js +399 -0
- package/dist/src/traces/storage.js.map +1 -0
- package/dist/src/traces/summarizer.d.ts +83 -0
- package/dist/src/traces/summarizer.d.ts.map +1 -0
- package/dist/src/traces/summarizer.js +247 -0
- package/dist/src/traces/summarizer.js.map +1 -0
- package/dist/src/traces/types.d.ts +365 -0
- package/dist/src/traces/types.d.ts.map +1 -0
- package/dist/src/traces/types.js +14 -0
- package/dist/src/traces/types.js.map +1 -0
- package/dist/src/types.d.ts +495 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +27 -0
- package/dist/src/types.js.map +1 -0
- package/docs/agent-surface-policy.md +45 -0
- package/docs/memory-setup.md +226 -0
- package/hooks/_arch_scan_bg.py +88 -0
- package/hooks/_plugin_drift_check_bg.py +85 -0
- package/hooks/_session_start_lib.sh +42 -0
- package/hooks/git/pre-push +141 -0
- package/hooks/hooks.json +126 -0
- package/hooks/post-commit +30 -0
- package/hooks/pre-commit +29 -0
- package/hooks/pre-edit-apppulse.sh +6 -0
- package/hooks/pre-edit-architecture.sh +106 -0
- package/hooks/pre-edit-rally-point.sh +92 -0
- package/hooks/session-start-apppulse.sh +6 -0
- package/hooks/session-start-architecture.sh +10 -0
- package/hooks/session-start-git-hooks.sh +12 -0
- package/hooks/session-start-plugin-drift.sh +27 -0
- package/hooks/session-start-rally-point.sh +44 -0
- package/hooks/session-start-retrieval.sh +24 -0
- package/hooks/session-start-worktree-gc.sh +105 -0
- package/hooks/test_apppulse_hooks.sh +6 -0
- package/hooks/test_rally_point_hooks.sh +388 -0
- package/hooks/test_sec008_hooks.sh +66 -0
- package/package.json +111 -0
- package/scripts/README.md +301 -0
- package/scripts/_db_url.py +61 -0
- package/scripts/_paths.py +445 -0
- package/scripts/blm.py +162 -0
- package/scripts/blm_api.py +263 -0
- package/scripts/check_cache_sync.py +490 -0
- package/scripts/embed_backend.py +475 -0
- package/scripts/install_memory.py +556 -0
- package/scripts/lessons_index/__init__.py +77 -0
- package/scripts/lessons_index/__main__.py +135 -0
- package/scripts/lessons_index/ingest.py +288 -0
- package/scripts/lessons_index/query.py +285 -0
- package/scripts/lessons_index/schema.py +114 -0
- package/scripts/memory_context/__init__.py +610 -0
- package/scripts/memory_graph/__init__.py +393 -0
- package/scripts/project_resolver.py +172 -0
- package/scripts/prune_codex_plugin_cache.py +23 -0
- package/scripts/prune_plugin_cache.py +359 -0
- package/scripts/semantic_index/__init__.py +482 -0
- package/scripts/semantic_index/_bench_hybrid.py +175 -0
- package/scripts/semantic_index/backfill.py +263 -0
- package/scripts/semantic_index/hybrid.py +174 -0
- package/scripts/semantic_index/test_backfill.py +292 -0
- package/scripts/semantic_index/test_hybrid.py +541 -0
- package/scripts/sync_navgator_lessons.py +666 -0
- package/scripts/sync_plugin_cache.py +388 -0
- package/skills/agent-rally-point/SKILL.md +165 -0
- package/skills/agent-rally-watcher/SKILL.md +83 -0
- package/skills/api-registry-bridge/SKILL.md +59 -0
- package/skills/architecture/dead/SKILL.md +56 -0
- package/skills/architecture/impact/SKILL.md +88 -0
- package/skills/architecture/review/SKILL.md +227 -0
- package/skills/architecture/rules/SKILL.md +72 -0
- package/skills/architecture/scan/SKILL.md +79 -0
- package/skills/architecture/trace/SKILL.md +65 -0
- package/skills/attribution-standard/SKILL.md +122 -0
- package/skills/authentication/SKILL.md +81 -0
- package/skills/authentication/references/better-auth-magic-link.md +135 -0
- package/skills/authentication/references/better-auth-setup.md +224 -0
- package/skills/authentication/references/google-calendar-sync.md +278 -0
- package/skills/authentication/references/google-cloud-console.md +159 -0
- package/skills/authentication/references/google-geocoding-directions.md +126 -0
- package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
- package/skills/authentication/references/google-maps.md +162 -0
- package/skills/authentication/references/google-oauth-setup.md +343 -0
- package/skills/authentication/references/google-places.md +173 -0
- package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
- package/skills/authentication/references/resend-email.md +227 -0
- package/skills/authentication/references/resend-otp-magic-link.md +153 -0
- package/skills/authentication/references/supabase-auth.md +172 -0
- package/skills/auto-decision-capture/SKILL.md +419 -0
- package/skills/build-loop/SKILL.md +516 -0
- package/skills/build-loop/detect-plugins.mjs +82 -0
- package/skills/build-loop/eval-guide.md +65 -0
- package/skills/build-loop/fallbacks.md +549 -0
- package/skills/build-loop/phases/fact-check.md +42 -0
- package/skills/build-loop/phases/ui-validation.md +267 -0
- package/skills/build-loop/references/apple-native-planning.md +439 -0
- package/skills/build-loop/references/capability-routing.md +181 -0
- package/skills/build-loop/references/codex-subagents.md +98 -0
- package/skills/build-loop/references/coordination.md +161 -0
- package/skills/build-loop/references/correction-aware-capture.md +177 -0
- package/skills/build-loop/references/independent-auditor.md +72 -0
- package/skills/build-loop/references/intent-capability-pack.md +170 -0
- package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
- package/skills/build-loop/references/leadership.md +72 -0
- package/skills/build-loop/references/memory.md +284 -0
- package/skills/build-loop/references/modular-systems-pack.md +96 -0
- package/skills/build-loop/references/output-style.md +129 -0
- package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
- package/skills/build-loop/references/phase-1-assess.md +218 -0
- package/skills/build-loop/references/phase-2-plan.md +77 -0
- package/skills/build-loop/references/phase-3-execute.md +47 -0
- package/skills/build-loop/references/phase-4-review.md +310 -0
- package/skills/build-loop/references/phase-5-iterate.md +71 -0
- package/skills/build-loop/references/phase-6-learn.md +56 -0
- package/skills/build-loop/references/recent-design-structures.md +274 -0
- package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
- package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
- package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
- package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
- package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
- package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
- package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
- package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
- package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
- package/skills/build-loop/references/self-recursive-dev.md +77 -0
- package/skills/build-loop/references/self-review.md +232 -0
- package/skills/build-loop/references/ui-io-contract.md +116 -0
- package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
- package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
- package/skills/build-loop/templates/backlog-item.md +35 -0
- package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
- package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
- package/skills/build-loop/templates/ux-fix-plan.md +40 -0
- package/skills/building-with-deepagents/SKILL.md +195 -0
- package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
- package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
- package/skills/capabilities/SKILL.md +91 -0
- package/skills/debug-loop/SKILL.md +278 -0
- package/skills/debug-loop/references/convergence-rules.md +145 -0
- package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
- package/skills/debugging/assess/SKILL.md +111 -0
- package/skills/debugging/debug-loop/SKILL.md +231 -0
- package/skills/debugging/memory/SKILL.md +147 -0
- package/skills/debugging/store/SKILL.md +153 -0
- package/skills/debugging-memory/SKILL.md +440 -0
- package/skills/debugging-memory/examples/incident-example.json +67 -0
- package/skills/debugging-memory/references/incident-documentation.md +280 -0
- package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
- package/skills/debugging-memory/references/pattern-extraction.md +163 -0
- package/skills/debugging-memory/references/subagent-integration.md +312 -0
- package/skills/defenseclaw-bridge/SKILL.md +156 -0
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
- package/skills/defenseclaw-bridge/references/output-format.md +108 -0
- package/skills/handoff/SKILL.md +94 -0
- package/skills/ibr-bridge/SKILL.md +68 -0
- package/skills/knowledge/SKILL.md +127 -0
- package/skills/knowledge/references/recall-integration.md +83 -0
- package/skills/knowledge/templates/madr-minimal.md +56 -0
- package/skills/knowledge-review/SKILL.md +112 -0
- package/skills/logging-tracer/SKILL.md +202 -0
- package/skills/logging-tracer/references/log-analysis.md +128 -0
- package/skills/logging-tracer/references/stack-templates.md +274 -0
- package/skills/logging-tracer-bridge/SKILL.md +70 -0
- package/skills/mcp-builder/SKILL.md +278 -0
- package/skills/mcp-builder/references/mcp-security.md +106 -0
- package/skills/model-tiering/SKILL.md +216 -0
- package/skills/native-ax-driver/SKILL.md +220 -0
- package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
- package/skills/native-ax-driver/scripts/native_driver.py +495 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
- package/skills/optimize/SKILL.md +202 -0
- package/skills/optimize/profiles.md +61 -0
- package/skills/plan-verify/SKILL.md +115 -0
- package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
- package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
- package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
- package/skills/plugin-builder/SKILL.md +329 -0
- package/skills/plugin-builder/references/authoritative-sources.md +104 -0
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
- package/skills/plugin-builder/references/components-guide.md +221 -0
- package/skills/plugin-builder/references/distribution.md +317 -0
- package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
- package/skills/plugin-builder/references/hooks-reference.md +210 -0
- package/skills/plugin-builder/references/manifest-schema.md +111 -0
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
- package/skills/plugin-tests/SKILL.md +94 -0
- package/skills/prd-bridge/SKILL.md +135 -0
- package/skills/research/SKILL.md +80 -0
- package/skills/security-methodology/SKILL.md +105 -0
- package/skills/security-methodology/references/cross-source-matrix.md +77 -0
- package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
- package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
- package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
- package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
- package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
- package/skills/self-improve/SKILL.md +298 -0
- package/skills/spec-writing/SKILL.md +548 -0
- package/skills/spec-writing/scripts/check_checklist.py +478 -0
- package/skills/sync-skills/SKILL.md +132 -0
- package/skills/telemetry/SKILL.md +66 -0
- package/skills/ui-design/SKILL.md +75 -0
- package/skills/ui-design/references/design-patterns-multi.md +165 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
- package/skills/ui-design/references/evidence-capture-policy.md +55 -0
- package/skills/ui-design/references/ui-guidance-sources.md +330 -0
- package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
- package/skills/ui-design/references/universal-design-principles.md +419 -0
- package/templates/memory/MEMORY.md.template +55 -0
- package/templates/memory/constitution.md.template +64 -0
- package/templates/memory/manifest.json +47 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Embedding backend abstraction for repo-local episodic memory.
|
|
5
|
+
|
|
6
|
+
Default: MLX (`mlx-community/mxbai-embed-large-v1`, 1024-dim). Faster
|
|
7
|
+
per-call once warm (~10ms) and dramatically faster on batches (~2ms
|
|
8
|
+
amortized at batch=10) compared to Ollama HTTP (~15ms warm).
|
|
9
|
+
|
|
10
|
+
Fallback: Ollama (`bge-m3`, 1024-dim). Hybrid recall Phase A migrated
|
|
11
|
+
the Ollama default from `mxbai-embed-large` to BGE-M3 — same dimension,
|
|
12
|
+
better hybrid retrieval performance, ColBERT-mode-ready for Phase E
|
|
13
|
+
late-interaction. The MLX default stays at mxbai-embed-large-v1 because
|
|
14
|
+
no `mlx-community/bge-m3` weights are cached locally; cross-backend
|
|
15
|
+
vectors are NOT comparable, so callers writing rows must record
|
|
16
|
+
`embedding_model_version` and recall must re-embed when querying rows
|
|
17
|
+
written by a different model. See research entry
|
|
18
|
+
`build-loop-search-architecture` for rationale.
|
|
19
|
+
|
|
20
|
+
The active backend is chosen on first call from $EMBED_BACKEND
|
|
21
|
+
({"mlx","ollama"}, default "mlx"). If MLX init fails (import error,
|
|
22
|
+
model load error, first-call failure), the module logs a warning to
|
|
23
|
+
stderr and falls through to Ollama for the rest of the process. Once
|
|
24
|
+
fallen through, MLX is not retried — keeps stop-hook latency
|
|
25
|
+
predictable.
|
|
26
|
+
|
|
27
|
+
Public API:
|
|
28
|
+
embed(text) -> list[float] (single text)
|
|
29
|
+
embed([t1, t2, ...]) -> list[list[float]] (batched)
|
|
30
|
+
dimension() -> int (always 1024)
|
|
31
|
+
active_backend() -> "mlx" | "ollama" (after first call)
|
|
32
|
+
active_model() -> model id string (after first call)
|
|
33
|
+
|
|
34
|
+
Env vars:
|
|
35
|
+
EMBED_BACKEND "mlx" (default) or "ollama"
|
|
36
|
+
EMBED_MODEL model id (defaults: mxbai-embed-large-v1 for MLX,
|
|
37
|
+
bge-m3 for Ollama)
|
|
38
|
+
MLX_FORCE_FAIL any truthy value forces fallback (used by tests)
|
|
39
|
+
|
|
40
|
+
Output format: Python lists of floats. Callers don't need numpy / mlx.
|
|
41
|
+
|
|
42
|
+
Exit semantics: this module never calls sys.exit. On total backend
|
|
43
|
+
failure (MLX broken AND Ollama unreachable), embed() raises
|
|
44
|
+
RuntimeError. Callers decide policy (write_decision.py logs and
|
|
45
|
+
swallows; recall.py exits 2).
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import json
|
|
50
|
+
import os
|
|
51
|
+
import sys
|
|
52
|
+
import urllib.request
|
|
53
|
+
from typing import Sequence
|
|
54
|
+
|
|
55
|
+
EMBED_DIM = 1024
|
|
56
|
+
MLX_DEFAULT_MODEL = "mlx-community/mxbai-embed-large-v1"
|
|
57
|
+
# Phase A hybrid-recall migration: Ollama default switched from
|
|
58
|
+
# `mxbai-embed-large` to `bge-m3`. Both are 1024-dim, so the pgvector
|
|
59
|
+
# column dimension is unchanged, but the vector spaces are NOT
|
|
60
|
+
# comparable. Rows written by mxbai must be re-embedded with bge-m3
|
|
61
|
+
# before recall can fuse them with bge-m3-embedded queries — see
|
|
62
|
+
# scripts/migrate_reembed_to_bgem3.py.
|
|
63
|
+
OLLAMA_DEFAULT_MODEL = "bge-m3"
|
|
64
|
+
OLLAMA_HOST = "127.0.0.1"
|
|
65
|
+
OLLAMA_PORT = 11434
|
|
66
|
+
OLLAMA_TIMEOUT_S = 60
|
|
67
|
+
|
|
68
|
+
# Daemon-side keep-alive override. Default Ollama evicts an idle model
|
|
69
|
+
# after 5 minutes — re-paying ~250-500ms model-load on the next call.
|
|
70
|
+
# 24h matches the cadence of a typical work session and amortizes warmup
|
|
71
|
+
# across every Stop hook firing in that window. Override per-process via
|
|
72
|
+
# OLLAMA_KEEP_ALIVE env (matches Ollama's own env var name).
|
|
73
|
+
OLLAMA_KEEP_ALIVE = os.environ.get("OLLAMA_KEEP_ALIVE", "24h")
|
|
74
|
+
|
|
75
|
+
# ---- Daemon (Phase H) ---------------------------------------------------
|
|
76
|
+
# Long-running stdlib HTTP server that holds the embedder backend in
|
|
77
|
+
# memory across recall.py invocations. Probed once per process; cached.
|
|
78
|
+
# When the daemon is up, every embed() call routes through it (~10-20ms
|
|
79
|
+
# loopback overhead vs ~3000ms cold MLX load per fresh process).
|
|
80
|
+
EMBED_DAEMON_HOST = os.environ.get("EMBED_DAEMON_HOST", "127.0.0.1")
|
|
81
|
+
try:
|
|
82
|
+
EMBED_DAEMON_PORT = int(os.environ.get("EMBED_DAEMON_PORT", "8766"))
|
|
83
|
+
except ValueError:
|
|
84
|
+
EMBED_DAEMON_PORT = 8766
|
|
85
|
+
EMBED_DAEMON_PROBE_TIMEOUT_S = 0.1 # 100ms — fast fail when daemon is down.
|
|
86
|
+
EMBED_DAEMON_CALL_TIMEOUT_S = 30.0 # generous; covers MLX cold-loaded large batches.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _log(msg: str) -> None:
|
|
90
|
+
print(f"[embed_backend] {msg}", file=sys.stderr, flush=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------- Ollama backend ----------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class OllamaBackend:
|
|
97
|
+
"""Persistent-HTTP Ollama backend. Reuses one HTTPConnection across calls."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, model: str = OLLAMA_DEFAULT_MODEL) -> None:
|
|
100
|
+
self.model = model
|
|
101
|
+
self._conn = None # lazy
|
|
102
|
+
|
|
103
|
+
def _ensure_conn(self):
|
|
104
|
+
# Stdlib http.client; recreate on broken-pipe-style errors.
|
|
105
|
+
import http.client
|
|
106
|
+
|
|
107
|
+
if self._conn is None:
|
|
108
|
+
self._conn = http.client.HTTPConnection(OLLAMA_HOST, OLLAMA_PORT, timeout=OLLAMA_TIMEOUT_S)
|
|
109
|
+
return self._conn
|
|
110
|
+
|
|
111
|
+
def _post_one(self, text: str) -> list[float]:
|
|
112
|
+
body = json.dumps({
|
|
113
|
+
"model": self.model,
|
|
114
|
+
"prompt": text,
|
|
115
|
+
"keep_alive": OLLAMA_KEEP_ALIVE,
|
|
116
|
+
}).encode("utf-8")
|
|
117
|
+
# Try persistent conn; on failure recreate once.
|
|
118
|
+
for attempt in (1, 2):
|
|
119
|
+
try:
|
|
120
|
+
conn = self._ensure_conn()
|
|
121
|
+
conn.request(
|
|
122
|
+
"POST",
|
|
123
|
+
"/api/embeddings",
|
|
124
|
+
body=body,
|
|
125
|
+
headers={"Content-Type": "application/json", "Connection": "keep-alive"},
|
|
126
|
+
)
|
|
127
|
+
resp = conn.getresponse()
|
|
128
|
+
payload = resp.read()
|
|
129
|
+
if resp.status != 200:
|
|
130
|
+
raise RuntimeError(f"ollama HTTP {resp.status}: {payload[:200]!r}")
|
|
131
|
+
data = json.loads(payload)
|
|
132
|
+
emb = data.get("embedding")
|
|
133
|
+
if not isinstance(emb, list):
|
|
134
|
+
raise RuntimeError("ollama: missing 'embedding' in response")
|
|
135
|
+
return [float(x) for x in emb]
|
|
136
|
+
except (ConnectionError, OSError, RuntimeError):
|
|
137
|
+
# Reset and retry once.
|
|
138
|
+
try:
|
|
139
|
+
if self._conn is not None:
|
|
140
|
+
self._conn.close()
|
|
141
|
+
except Exception: # noqa: BLE001
|
|
142
|
+
pass
|
|
143
|
+
self._conn = None
|
|
144
|
+
if attempt == 2:
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
raise RuntimeError("ollama: unreachable")
|
|
148
|
+
|
|
149
|
+
def embed(self, text: str) -> list[float]:
|
|
150
|
+
return self._post_one(text)
|
|
151
|
+
|
|
152
|
+
def embed_batch(self, texts: Sequence[str]) -> list[list[float]]:
|
|
153
|
+
# Ollama /api/embeddings does not natively batch; loop on persistent conn.
|
|
154
|
+
return [self._post_one(t) for t in texts]
|
|
155
|
+
|
|
156
|
+
def name(self) -> str:
|
|
157
|
+
return "ollama"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------- MLX backend ----------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class MLXBackend:
|
|
164
|
+
"""Local MLX backend using mlx-embeddings.
|
|
165
|
+
|
|
166
|
+
Lazy-loads the model on first call (~220ms warm cache). The model
|
|
167
|
+
object is cached for the process lifetime.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, model_id: str = MLX_DEFAULT_MODEL) -> None:
|
|
171
|
+
self.model_id = model_id
|
|
172
|
+
self._model = None
|
|
173
|
+
self._tokenizer = None
|
|
174
|
+
self._generate = None
|
|
175
|
+
|
|
176
|
+
def _ensure_loaded(self) -> None:
|
|
177
|
+
if self._model is not None:
|
|
178
|
+
return
|
|
179
|
+
if os.environ.get("MLX_FORCE_FAIL"):
|
|
180
|
+
raise RuntimeError("MLX_FORCE_FAIL set; simulated failure")
|
|
181
|
+
# Local import keeps Linux installs (where mlx-embeddings is not
|
|
182
|
+
# available) from blowing up at module import time.
|
|
183
|
+
from mlx_embeddings import generate, load # type: ignore
|
|
184
|
+
|
|
185
|
+
self._model, self._tokenizer = load(self.model_id)
|
|
186
|
+
self._generate = generate
|
|
187
|
+
|
|
188
|
+
def embed(self, text: str) -> list[float]:
|
|
189
|
+
self._ensure_loaded()
|
|
190
|
+
if not text:
|
|
191
|
+
raise ValueError("embed_backend: empty text not supported")
|
|
192
|
+
out = self._generate(self._model, self._tokenizer, texts=[text])
|
|
193
|
+
return [float(x) for x in out.text_embeds[0].tolist()]
|
|
194
|
+
|
|
195
|
+
def embed_batch(self, texts: Sequence[str]) -> list[list[float]]:
|
|
196
|
+
self._ensure_loaded()
|
|
197
|
+
if not texts:
|
|
198
|
+
return []
|
|
199
|
+
if any((not t) for t in texts):
|
|
200
|
+
raise ValueError("embed_backend: empty text in batch not supported")
|
|
201
|
+
out = self._generate(self._model, self._tokenizer, texts=list(texts))
|
|
202
|
+
return [[float(x) for x in row.tolist()] for row in out.text_embeds]
|
|
203
|
+
|
|
204
|
+
def name(self) -> str:
|
|
205
|
+
return "mlx"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------- Daemon backend (Phase H) ----------
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class DaemonBackend:
|
|
212
|
+
"""HTTP client for `scripts/embed_daemon.py`.
|
|
213
|
+
|
|
214
|
+
Speaks the same `embed`/`embed_batch`/`name` interface as the in-process
|
|
215
|
+
backends so `_select_backend()` can hand it to callers unchanged. The
|
|
216
|
+
daemon holds the model in memory across recall.py invocations,
|
|
217
|
+
eliminating the ~3000ms MLX cold-load cliff on every fresh process.
|
|
218
|
+
|
|
219
|
+
Failure handling: any HTTP error is raised so `_select_backend()` can
|
|
220
|
+
fall back to the in-process MLX/Ollama path. Once the in-process
|
|
221
|
+
backend is selected for this process, the daemon is not retried —
|
|
222
|
+
keeps latency predictable and avoids per-call probe cost.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
*,
|
|
228
|
+
host: str = EMBED_DAEMON_HOST,
|
|
229
|
+
port: int = EMBED_DAEMON_PORT,
|
|
230
|
+
backend_name: str = "unknown",
|
|
231
|
+
model: str = "unknown",
|
|
232
|
+
) -> None:
|
|
233
|
+
self.host = host
|
|
234
|
+
self.port = port
|
|
235
|
+
# Carries the BACKEND the daemon advertises (mlx|ollama) plus its
|
|
236
|
+
# model id, so callers asking active_backend() / active_model()
|
|
237
|
+
# see the underlying truth, not the literal string "daemon".
|
|
238
|
+
self._daemon_backend = backend_name
|
|
239
|
+
self.model = model
|
|
240
|
+
self._conn = None # lazy http.client.HTTPConnection
|
|
241
|
+
|
|
242
|
+
def _ensure_conn(self):
|
|
243
|
+
import http.client # noqa: PLC0415
|
|
244
|
+
|
|
245
|
+
if self._conn is None:
|
|
246
|
+
self._conn = http.client.HTTPConnection(
|
|
247
|
+
self.host, self.port, timeout=EMBED_DAEMON_CALL_TIMEOUT_S
|
|
248
|
+
)
|
|
249
|
+
return self._conn
|
|
250
|
+
|
|
251
|
+
def _post_embed(self, texts: list[str]) -> list[list[float]]:
|
|
252
|
+
body = json.dumps({"texts": texts}).encode("utf-8")
|
|
253
|
+
for attempt in (1, 2):
|
|
254
|
+
try:
|
|
255
|
+
conn = self._ensure_conn()
|
|
256
|
+
conn.request(
|
|
257
|
+
"POST",
|
|
258
|
+
"/embed",
|
|
259
|
+
body=body,
|
|
260
|
+
headers={
|
|
261
|
+
"Content-Type": "application/json",
|
|
262
|
+
"Connection": "keep-alive",
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
resp = conn.getresponse()
|
|
266
|
+
payload = resp.read()
|
|
267
|
+
if resp.status != 200:
|
|
268
|
+
raise RuntimeError(
|
|
269
|
+
f"embed daemon HTTP {resp.status}: {payload[:200]!r}"
|
|
270
|
+
)
|
|
271
|
+
data = json.loads(payload)
|
|
272
|
+
if not data.get("ok"):
|
|
273
|
+
raise RuntimeError(
|
|
274
|
+
f"embed daemon not-ok: {data.get('error', 'unknown')}"
|
|
275
|
+
)
|
|
276
|
+
embs = data.get("embeddings")
|
|
277
|
+
if not isinstance(embs, list):
|
|
278
|
+
raise RuntimeError(
|
|
279
|
+
"embed daemon: missing 'embeddings' in response"
|
|
280
|
+
)
|
|
281
|
+
return [[float(x) for x in row] for row in embs]
|
|
282
|
+
except (ConnectionError, OSError, RuntimeError):
|
|
283
|
+
# Reset and retry once. If the daemon was up at probe but
|
|
284
|
+
# has since died, this gives us one shot to reconnect
|
|
285
|
+
# before kicking back to the caller for fallback.
|
|
286
|
+
try:
|
|
287
|
+
if self._conn is not None:
|
|
288
|
+
self._conn.close()
|
|
289
|
+
except Exception: # noqa: BLE001
|
|
290
|
+
pass
|
|
291
|
+
self._conn = None
|
|
292
|
+
if attempt == 2:
|
|
293
|
+
raise
|
|
294
|
+
raise RuntimeError("embed daemon: unreachable")
|
|
295
|
+
|
|
296
|
+
def embed(self, text: str) -> list[float]:
|
|
297
|
+
out = self._post_embed([text])
|
|
298
|
+
if not out:
|
|
299
|
+
raise RuntimeError("embed daemon returned empty embeddings list")
|
|
300
|
+
return out[0]
|
|
301
|
+
|
|
302
|
+
def embed_batch(self, texts: Sequence[str]) -> list[list[float]]:
|
|
303
|
+
if not texts:
|
|
304
|
+
return []
|
|
305
|
+
return self._post_embed(list(texts))
|
|
306
|
+
|
|
307
|
+
def name(self) -> str:
|
|
308
|
+
# Surface the underlying backend rather than "daemon" so callers
|
|
309
|
+
# writing rows tag them with the actual model that produced them.
|
|
310
|
+
return self._daemon_backend or "daemon"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---------- module-level singleton ----------
|
|
314
|
+
|
|
315
|
+
_BACKEND = None # type: ignore # OllamaBackend | MLXBackend | DaemonBackend | None
|
|
316
|
+
_FALLBACK_REASON: str | None = None
|
|
317
|
+
_DAEMON_PROBED: bool = False # Per-process probe latch.
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _probe_daemon() -> DaemonBackend | None:
|
|
321
|
+
"""One-shot probe of the embed daemon's /health endpoint.
|
|
322
|
+
|
|
323
|
+
Latched via `_DAEMON_PROBED` so we never re-probe within the same
|
|
324
|
+
process. If `EMBED_FORCE_INPROCESS` is set, the probe is skipped
|
|
325
|
+
entirely and returns None — used by tests and by users diagnosing
|
|
326
|
+
a degraded daemon.
|
|
327
|
+
|
|
328
|
+
Returns a configured `DaemonBackend` when the daemon is up AND
|
|
329
|
+
reports `warm: true`, else None.
|
|
330
|
+
"""
|
|
331
|
+
global _DAEMON_PROBED
|
|
332
|
+
if _DAEMON_PROBED:
|
|
333
|
+
# Caller checks this through the cached _BACKEND singleton.
|
|
334
|
+
return None
|
|
335
|
+
_DAEMON_PROBED = True
|
|
336
|
+
if os.environ.get("EMBED_FORCE_INPROCESS"):
|
|
337
|
+
return None
|
|
338
|
+
try:
|
|
339
|
+
import urllib.request # noqa: PLC0415
|
|
340
|
+
|
|
341
|
+
url = f"http://{EMBED_DAEMON_HOST}:{EMBED_DAEMON_PORT}/health"
|
|
342
|
+
req = urllib.request.Request(url, method="GET")
|
|
343
|
+
with urllib.request.urlopen(req, timeout=EMBED_DAEMON_PROBE_TIMEOUT_S) as resp: # noqa: S310
|
|
344
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
345
|
+
# Daemon is "available" iff it claims the backend is warm. A
|
|
346
|
+
# daemon serving in degraded mode (backend not loaded) returns
|
|
347
|
+
# 503 on POST, so treating warm=False as unavailable lets us
|
|
348
|
+
# fall back to the in-process path which may succeed via a
|
|
349
|
+
# different load path.
|
|
350
|
+
if body.get("ok") and body.get("warm"):
|
|
351
|
+
backend_name = body.get("backend", "unknown")
|
|
352
|
+
model_name = body.get("model", "unknown")
|
|
353
|
+
_log(
|
|
354
|
+
f"embed daemon up at {EMBED_DAEMON_HOST}:{EMBED_DAEMON_PORT} "
|
|
355
|
+
f"(backend={backend_name} model={model_name})"
|
|
356
|
+
)
|
|
357
|
+
return DaemonBackend(
|
|
358
|
+
backend_name=backend_name,
|
|
359
|
+
model=model_name,
|
|
360
|
+
)
|
|
361
|
+
except Exception as e: # noqa: BLE001
|
|
362
|
+
# Quiet by design — probe failures are routine when the daemon
|
|
363
|
+
# isn't running. Logged at debug level only.
|
|
364
|
+
_log_debug = os.environ.get("EMBED_BACKEND_DEBUG")
|
|
365
|
+
if _log_debug:
|
|
366
|
+
_log(f"embed daemon probe failed: {e!r}")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _select_backend():
|
|
371
|
+
global _BACKEND, _FALLBACK_REASON
|
|
372
|
+
if _BACKEND is not None:
|
|
373
|
+
return _BACKEND
|
|
374
|
+
|
|
375
|
+
# Phase H: try the daemon FIRST. When it's up, this is the entire
|
|
376
|
+
# selection — we don't load MLX/Ollama in-process at all.
|
|
377
|
+
daemon = _probe_daemon()
|
|
378
|
+
if daemon is not None:
|
|
379
|
+
_BACKEND = daemon
|
|
380
|
+
return _BACKEND
|
|
381
|
+
|
|
382
|
+
requested = os.environ.get("EMBED_BACKEND", "mlx").lower().strip()
|
|
383
|
+
custom_model = os.environ.get("EMBED_MODEL")
|
|
384
|
+
|
|
385
|
+
if requested == "ollama":
|
|
386
|
+
_BACKEND = OllamaBackend(model=custom_model or OLLAMA_DEFAULT_MODEL)
|
|
387
|
+
return _BACKEND
|
|
388
|
+
|
|
389
|
+
if requested != "mlx":
|
|
390
|
+
_log(f"unknown EMBED_BACKEND={requested!r}; using mlx")
|
|
391
|
+
|
|
392
|
+
# Try MLX; fall through to Ollama on any error.
|
|
393
|
+
candidate = MLXBackend(model_id=custom_model or MLX_DEFAULT_MODEL)
|
|
394
|
+
try:
|
|
395
|
+
# Force the lazy load NOW so failure is detected before first
|
|
396
|
+
# production call. Adds ~220ms one-time cold start; we eat it
|
|
397
|
+
# here so the first user-facing call is steady-state.
|
|
398
|
+
candidate._ensure_loaded()
|
|
399
|
+
_BACKEND = candidate
|
|
400
|
+
return _BACKEND
|
|
401
|
+
except Exception as e: # noqa: BLE001
|
|
402
|
+
_FALLBACK_REASON = f"MLX init failed: {e!r}"
|
|
403
|
+
_log(f"falling back to ollama ({_FALLBACK_REASON})")
|
|
404
|
+
_BACKEND = OllamaBackend(model=OLLAMA_DEFAULT_MODEL)
|
|
405
|
+
return _BACKEND
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def embed(text): # type: ignore[no-untyped-def]
|
|
409
|
+
"""Embed a single string or a list of strings.
|
|
410
|
+
|
|
411
|
+
Single → list[float] of length 1024.
|
|
412
|
+
Batch → list[list[float]] (each length 1024).
|
|
413
|
+
"""
|
|
414
|
+
backend = _select_backend()
|
|
415
|
+
if isinstance(text, str):
|
|
416
|
+
return backend.embed(text)
|
|
417
|
+
if isinstance(text, (list, tuple)):
|
|
418
|
+
return backend.embed_batch(list(text))
|
|
419
|
+
raise TypeError(f"embed() expects str or list[str], got {type(text).__name__}")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def dimension() -> int:
|
|
423
|
+
"""Return the embedding dimensionality (1024)."""
|
|
424
|
+
return EMBED_DIM
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def active_backend() -> str:
|
|
428
|
+
"""Return 'mlx' or 'ollama' (after first embed call)."""
|
|
429
|
+
return _select_backend().name()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def active_model() -> str:
|
|
433
|
+
"""Return the model identifier the active backend is configured to use.
|
|
434
|
+
|
|
435
|
+
Useful for `embedding_model_version` provenance: every write should
|
|
436
|
+
record which model produced the vector so cross-space mismatches can
|
|
437
|
+
be detected at recall time.
|
|
438
|
+
"""
|
|
439
|
+
backend = _select_backend()
|
|
440
|
+
return getattr(backend, "model", None) or getattr(backend, "model_id", "unknown")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def fallback_reason() -> str | None:
|
|
444
|
+
"""Return why we fell through to Ollama, or None if MLX is active."""
|
|
445
|
+
_select_backend()
|
|
446
|
+
return _FALLBACK_REASON
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def reset_for_tests() -> None:
|
|
450
|
+
"""Clear the singleton so tests can re-select with new env.
|
|
451
|
+
|
|
452
|
+
Closes the Ollama / Daemon HTTP connection if one was open to avoid
|
|
453
|
+
ResourceWarning on rapid backend switches. Also clears the
|
|
454
|
+
per-process daemon-probe latch so `_select_backend()` will probe
|
|
455
|
+
the daemon again on the next call.
|
|
456
|
+
"""
|
|
457
|
+
global _BACKEND, _FALLBACK_REASON, _DAEMON_PROBED
|
|
458
|
+
if _BACKEND is not None and isinstance(_BACKEND, (OllamaBackend, DaemonBackend)):
|
|
459
|
+
try:
|
|
460
|
+
if _BACKEND._conn is not None:
|
|
461
|
+
_BACKEND._conn.close()
|
|
462
|
+
except Exception: # noqa: BLE001
|
|
463
|
+
pass
|
|
464
|
+
_BACKEND = None
|
|
465
|
+
_FALLBACK_REASON = None
|
|
466
|
+
_DAEMON_PROBED = False
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if __name__ == "__main__":
|
|
470
|
+
# CLI: `python3 embed_backend.py 'some text'` → print dim and first 5 floats
|
|
471
|
+
if len(sys.argv) < 2:
|
|
472
|
+
print("usage: embed_backend.py <text>", file=sys.stderr)
|
|
473
|
+
sys.exit(1)
|
|
474
|
+
v = embed(sys.argv[1])
|
|
475
|
+
print(f"backend={active_backend()} dim={len(v)} preview={v[:5]}")
|