claude-smart 0.2.48 → 0.2.50
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/.claude-plugin/marketplace.json +2 -2
- package/README.md +11 -40
- package/bin/claude-smart.js +393 -55
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +255 -7
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +766 -123
- package/plugin/scripts/codex-hook.js +79 -229
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +118 -137
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +116 -21
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +93 -29
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +56 -11
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Warm-before-ready readiness gate for the in-process local embedder.
|
|
2
|
+
|
|
3
|
+
Phase 2 preparation for the embedding-stability redesign. Everything here is
|
|
4
|
+
**dormant** until a future config flip sets ``REFLEXIO_EMBEDDING_PROVIDER=inprocess``
|
|
5
|
+
with a ``local/*`` default embedding model. Pre-flip (the current daemon-mode
|
|
6
|
+
prod state, or any cloud/off/OSS-dev deployment) none of this changes ``/health``
|
|
7
|
+
or startup behaviour.
|
|
8
|
+
|
|
9
|
+
Three concerns live here:
|
|
10
|
+
|
|
11
|
+
- **D5 warm-before-ready** — a process-level ``threading.Event`` set once the
|
|
12
|
+
in-process embedder has loaded, plus a non-blocking startup thread that loads
|
|
13
|
+
it. ``/health`` reports 503 while the gate is active and the embedder is not
|
|
14
|
+
yet warm, so the load balancer does not route embedding traffic at a worker
|
|
15
|
+
that would pay the ~2-8s cold-load on its first request.
|
|
16
|
+
- **D8 config guards** — loud startup warnings for foot-guns of the in-process
|
|
17
|
+
topology: multiple uvicorn workers (each loads its own model copy), a
|
|
18
|
+
half-configured daemon-disable / provider pair, and an in-process provider set
|
|
19
|
+
alongside a configured service endpoint (which the provider silently overrides).
|
|
20
|
+
|
|
21
|
+
The gate is intentionally cheap to evaluate (no network probe, no provider
|
|
22
|
+
auto-detection) because ``/health`` is the ALB + container health target and is
|
|
23
|
+
polled frequently.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
import threading
|
|
31
|
+
import time
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
|
|
34
|
+
_LOGGER = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Warm-before-ready retry: cold model load is ~2-8s and reliable in prod, but a
|
|
37
|
+
# bounded retry absorbs a transient blip before giving up (a give-up leaves
|
|
38
|
+
# /health at 503, forcing a full ECS task replacement).
|
|
39
|
+
_WARM_MAX_ATTEMPTS = 3
|
|
40
|
+
_WARM_RETRY_BACKOFF_S = 2.0
|
|
41
|
+
|
|
42
|
+
_ENV_PROVIDER = "REFLEXIO_EMBEDDING_PROVIDER"
|
|
43
|
+
_ENV_DISABLE_DAEMON = "REFLEXIO_DISABLE_LOCAL_EMBEDDING_DAEMON"
|
|
44
|
+
# Service-endpoint envs, named to match embedding_service_provider.py exactly. An
|
|
45
|
+
# in-process provider takes precedence over both, so either being set alongside
|
|
46
|
+
# PROVIDER=inprocess silently strands the configured service host.
|
|
47
|
+
_ENV_SERVICE_URL = "REFLEXIO_EMBEDDING_SERVICE_URL"
|
|
48
|
+
_ENV_DAEMON_HOST = "REFLEXIO_EMBEDDING_DAEMON_HOST"
|
|
49
|
+
# Recorded by ``reflexio.server.__main__`` before ``uvicorn.run`` so the guard
|
|
50
|
+
# can read the configured worker count from inside a worker process, where
|
|
51
|
+
# uvicorn exposes no worker-count env of its own.
|
|
52
|
+
_ENV_WORKERS = "REFLEXIO_SERVER_WORKERS"
|
|
53
|
+
_ENV_WEB_CONCURRENCY = "WEB_CONCURRENCY"
|
|
54
|
+
|
|
55
|
+
_INPROCESS = "inprocess"
|
|
56
|
+
|
|
57
|
+
# Process-level readiness signal: set once the in-process embedder is loaded.
|
|
58
|
+
_ready = threading.Event()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def mark_embedder_ready() -> None:
|
|
62
|
+
"""Mark the in-process embedder as loaded/warm for this process."""
|
|
63
|
+
_ready.set()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_embedder_ready() -> bool:
|
|
67
|
+
"""Return True once the in-process embedder has been warmed in this process."""
|
|
68
|
+
return _ready.is_set()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def reset_warmup_state_for_test() -> None:
|
|
72
|
+
"""Clear the readiness signal. Test-only hygiene helper."""
|
|
73
|
+
_ready.clear()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _provider() -> str:
|
|
77
|
+
return os.environ.get(_ENV_PROVIDER, "").strip().lower()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def inprocess_local_gate_active() -> bool:
|
|
81
|
+
"""Return True iff this deployment serves embeddings via the in-process local path.
|
|
82
|
+
|
|
83
|
+
The gate is active only when BOTH hold:
|
|
84
|
+
|
|
85
|
+
- ``REFLEXIO_EMBEDDING_PROVIDER == "inprocess"`` (explicit config flip), and
|
|
86
|
+
- the resolved default embedding model is a ``local/*`` model.
|
|
87
|
+
|
|
88
|
+
Any other configuration (cloud, off, daemon-mode ``local_service`` /
|
|
89
|
+
``internal_service``, or no explicit provider at all) returns False, keeping
|
|
90
|
+
the warm-before-ready behaviour dormant. Cheap by construction: neither
|
|
91
|
+
branch performs a network probe or provider auto-detection.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
bool: True when warm-before-ready ``/health`` gating should apply.
|
|
95
|
+
"""
|
|
96
|
+
from reflexio.server.llm.providers.embedding_service_provider import (
|
|
97
|
+
embedding_provider_mode,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
mode = embedding_provider_mode()
|
|
102
|
+
except Exception: # noqa: BLE001
|
|
103
|
+
# An invalid REFLEXIO_EMBEDDING_PROVIDER must not turn /health (polled on
|
|
104
|
+
# every ALB probe) into a 500 — treat unresolvable config as gate-off.
|
|
105
|
+
_LOGGER.debug(
|
|
106
|
+
"Embedding provider mode unresolvable; gate inactive", exc_info=True
|
|
107
|
+
)
|
|
108
|
+
return False
|
|
109
|
+
if mode != _INPROCESS:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
model = resolve_model_name(ModelRole.EMBEDDING)
|
|
116
|
+
except Exception: # noqa: BLE001
|
|
117
|
+
# No embedding-capable provider resolvable — cannot be the in-process
|
|
118
|
+
# local path, so the gate stays inactive rather than wedging /health.
|
|
119
|
+
_LOGGER.debug("Embedding model resolution failed; gate inactive", exc_info=True)
|
|
120
|
+
return False
|
|
121
|
+
return model.startswith("local/")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _resolve_local_embedder_loader() -> Callable[[], object] | None:
|
|
125
|
+
"""Return a zero-arg loader for the embedder matching the resolved local model.
|
|
126
|
+
|
|
127
|
+
Dispatches on the SAME model the gate resolved so readiness reflects the
|
|
128
|
+
embedder that will actually serve: ``local/nomic-*`` -> ``NomicEmbedder``
|
|
129
|
+
(sentence-transformers), any other ``local/*`` -> ``LocalEmbedder`` (chromadb
|
|
130
|
+
ONNX, e.g. the OSS ``local/minilm-l6-v2`` default). Returns None when no
|
|
131
|
+
in-process local model resolves.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Callable[[], object] | None: A loader that warms the correct singleton,
|
|
135
|
+
or None if the resolved model is not an in-process local one.
|
|
136
|
+
"""
|
|
137
|
+
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
model = resolve_model_name(ModelRole.EMBEDDING)
|
|
141
|
+
except Exception: # noqa: BLE001
|
|
142
|
+
return None
|
|
143
|
+
if not model.startswith("local/"):
|
|
144
|
+
return None
|
|
145
|
+
if "nomic" in model:
|
|
146
|
+
from reflexio.server.llm.providers.nomic_embedding_provider import NomicEmbedder
|
|
147
|
+
|
|
148
|
+
return lambda: NomicEmbedder.get()._load()
|
|
149
|
+
from reflexio.server.llm.providers.local_embedding_provider import LocalEmbedder
|
|
150
|
+
|
|
151
|
+
return lambda: LocalEmbedder.get()._load()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _warm_embedder() -> None:
|
|
155
|
+
"""Load the resolved in-process local embedder, then flip the readiness signal.
|
|
156
|
+
|
|
157
|
+
Warms whichever embedder the gate's resolved ``local/*`` model maps to (see
|
|
158
|
+
:func:`_resolve_local_embedder_loader`), so readiness reflects the model that
|
|
159
|
+
will actually serve — not a hardcoded one. Fire-and-forget in a daemon thread
|
|
160
|
+
so a slow load never blocks startup; a bounded retry absorbs a transient blip.
|
|
161
|
+
On final failure the readiness signal stays clear and ``/health`` keeps
|
|
162
|
+
reporting not-ready — the intended fail-safe (deploy fails loud, old rev serves).
|
|
163
|
+
"""
|
|
164
|
+
loader = _resolve_local_embedder_loader()
|
|
165
|
+
if loader is None:
|
|
166
|
+
_LOGGER.warning(
|
|
167
|
+
"Warmup gate active but no in-process local embedder resolved; "
|
|
168
|
+
"/health stays not-ready."
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
for attempt in range(1, _WARM_MAX_ATTEMPTS + 1):
|
|
172
|
+
try:
|
|
173
|
+
loader()
|
|
174
|
+
mark_embedder_ready()
|
|
175
|
+
_LOGGER.info("In-process embedder warm; /health now reports ready.")
|
|
176
|
+
return
|
|
177
|
+
except Exception: # noqa: BLE001
|
|
178
|
+
_LOGGER.warning(
|
|
179
|
+
"In-process embedder warmup attempt %d/%d failed.",
|
|
180
|
+
attempt,
|
|
181
|
+
_WARM_MAX_ATTEMPTS,
|
|
182
|
+
exc_info=True,
|
|
183
|
+
)
|
|
184
|
+
if attempt < _WARM_MAX_ATTEMPTS:
|
|
185
|
+
time.sleep(_WARM_RETRY_BACKOFF_S * attempt)
|
|
186
|
+
_LOGGER.error(
|
|
187
|
+
"In-process embedder warmup exhausted %d attempts; /health stays not-ready.",
|
|
188
|
+
_WARM_MAX_ATTEMPTS,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def maybe_start_embedder_warmup() -> bool:
|
|
193
|
+
"""Spawn the non-blocking warm-before-ready thread when the gate is active.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
bool: True when a warmup thread was started (gate active), else False.
|
|
197
|
+
"""
|
|
198
|
+
if not inprocess_local_gate_active():
|
|
199
|
+
return False
|
|
200
|
+
threading.Thread(target=_warm_embedder, daemon=True, name="embedder-warmup").start()
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _detected_worker_count() -> int | None:
|
|
205
|
+
"""Best-effort read of the configured uvicorn worker count.
|
|
206
|
+
|
|
207
|
+
uvicorn exposes no worker-count env inside a worker process, so
|
|
208
|
+
``reflexio.server.__main__`` records it in ``REFLEXIO_SERVER_WORKERS``. A
|
|
209
|
+
``WEB_CONCURRENCY`` fallback covers gunicorn-style entrypoints. Returns
|
|
210
|
+
``None`` when neither is set (custom entrypoint) — the guard then warns that
|
|
211
|
+
it could not verify the count rather than refusing.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
int | None: The detected worker count, or None if undetectable.
|
|
215
|
+
"""
|
|
216
|
+
for name in (_ENV_WORKERS, _ENV_WEB_CONCURRENCY):
|
|
217
|
+
raw = os.environ.get(name)
|
|
218
|
+
if raw:
|
|
219
|
+
try:
|
|
220
|
+
return int(raw)
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _guard_workers_multiply_model() -> None:
|
|
227
|
+
"""D8(a): warn when in-process mode runs under multiple workers.
|
|
228
|
+
|
|
229
|
+
In-process embedding loads one model copy per worker process, so a
|
|
230
|
+
memory-bounded host that is fine with 1 worker can OOM at N. Warn-only: the
|
|
231
|
+
worker count is not always reliably detectable from inside a worker.
|
|
232
|
+
"""
|
|
233
|
+
if _provider() != _INPROCESS:
|
|
234
|
+
return
|
|
235
|
+
count = _detected_worker_count()
|
|
236
|
+
if count is not None and count > 1:
|
|
237
|
+
_LOGGER.warning(
|
|
238
|
+
"%s=%s with %d uvicorn workers: the in-process embedder loads one "
|
|
239
|
+
"model copy PER worker process, multiplying memory on a "
|
|
240
|
+
"memory-bounded task. Prefer 1 worker for in-process embedding, or "
|
|
241
|
+
"run the shared embedding daemon.",
|
|
242
|
+
_ENV_PROVIDER,
|
|
243
|
+
_INPROCESS,
|
|
244
|
+
count,
|
|
245
|
+
)
|
|
246
|
+
elif count is None:
|
|
247
|
+
_LOGGER.warning(
|
|
248
|
+
"%s=%s but the uvicorn worker count could not be verified (neither "
|
|
249
|
+
"%s nor %s is set). If this deployment runs multiple workers, each "
|
|
250
|
+
"loads its own in-process model copy.",
|
|
251
|
+
_ENV_PROVIDER,
|
|
252
|
+
_INPROCESS,
|
|
253
|
+
_ENV_WORKERS,
|
|
254
|
+
_ENV_WEB_CONCURRENCY,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _guard_daemon_disable_half_pair() -> None:
|
|
259
|
+
"""D8(b): warn when the daemon-disable flag and provider disagree.
|
|
260
|
+
|
|
261
|
+
``REFLEXIO_DISABLE_LOCAL_EMBEDDING_DAEMON`` (truthy) and
|
|
262
|
+
``REFLEXIO_EMBEDDING_PROVIDER=inprocess`` describe the same intent from two
|
|
263
|
+
angles and are meant to be flipped together. If exactly one is set the
|
|
264
|
+
topology is half-configured (e.g. the daemon is disabled but requests still
|
|
265
|
+
route to daemon mode, or vice-versa).
|
|
266
|
+
"""
|
|
267
|
+
from reflexio.server.env_utils import env_truthy
|
|
268
|
+
|
|
269
|
+
daemon_disabled = env_truthy(os.environ.get(_ENV_DISABLE_DAEMON, ""))
|
|
270
|
+
inprocess = _provider() == _INPROCESS
|
|
271
|
+
if daemon_disabled != inprocess:
|
|
272
|
+
_LOGGER.warning(
|
|
273
|
+
"Half-configured in-process embedding: %s=%s and %s=%s disagree. "
|
|
274
|
+
"Set them together (disable the daemon AND select the in-process "
|
|
275
|
+
"provider) or neither.",
|
|
276
|
+
_ENV_DISABLE_DAEMON,
|
|
277
|
+
daemon_disabled,
|
|
278
|
+
_ENV_PROVIDER,
|
|
279
|
+
_provider() or "<unset>",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _guard_inprocess_overrides_service_host() -> None:
|
|
284
|
+
"""D8(c): warn when in-process mode is set alongside a configured service host.
|
|
285
|
+
|
|
286
|
+
``REFLEXIO_EMBEDDING_PROVIDER=inprocess`` wins the provider-precedence race in
|
|
287
|
+
``embedding_provider_mode`` outright, so a co-configured
|
|
288
|
+
``REFLEXIO_EMBEDDING_SERVICE_URL`` / ``REFLEXIO_EMBEDDING_DAEMON_HOST`` is
|
|
289
|
+
silently ignored and this process loads its own in-process local model instead
|
|
290
|
+
of routing to the service. On a GPU-service / self-host fleet that means an
|
|
291
|
+
unintended per-instance CPU model load. Warn-only: the intended in-process
|
|
292
|
+
flip leaves both endpoint envs unset, so this stays silent there.
|
|
293
|
+
"""
|
|
294
|
+
if _provider() != _INPROCESS:
|
|
295
|
+
return
|
|
296
|
+
configured = [
|
|
297
|
+
name
|
|
298
|
+
for name in (_ENV_SERVICE_URL, _ENV_DAEMON_HOST)
|
|
299
|
+
if os.environ.get(name, "").strip()
|
|
300
|
+
]
|
|
301
|
+
if not configured:
|
|
302
|
+
return
|
|
303
|
+
_LOGGER.warning(
|
|
304
|
+
"%s=%s takes precedence and loads an in-process local model, silently "
|
|
305
|
+
"ignoring the configured service endpoint(s): %s. Unset the endpoint(s) "
|
|
306
|
+
"to keep the in-process embedder, or unset %s to route embeddings to the "
|
|
307
|
+
"service.",
|
|
308
|
+
_ENV_PROVIDER,
|
|
309
|
+
_INPROCESS,
|
|
310
|
+
", ".join(configured),
|
|
311
|
+
_ENV_PROVIDER,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def run_startup_config_guards() -> None:
|
|
316
|
+
"""Run the D8 config guards once at server startup (idempotent, warn-only)."""
|
|
317
|
+
_guard_workers_multiply_model()
|
|
318
|
+
_guard_daemon_disable_half_pair()
|
|
319
|
+
_guard_inprocess_overrides_service_host()
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
__all__ = [
|
|
323
|
+
"inprocess_local_gate_active",
|
|
324
|
+
"is_embedder_ready",
|
|
325
|
+
"mark_embedder_ready",
|
|
326
|
+
"maybe_start_embedder_warmup",
|
|
327
|
+
"reset_warmup_state_for_test",
|
|
328
|
+
"run_startup_config_guards",
|
|
329
|
+
]
|
|
@@ -54,9 +54,19 @@ _LOCAL_SERVICE_PROBE_FAILURE_CACHE_SECONDS = 5.0
|
|
|
54
54
|
# never hands out a connection the load balancer already closed.
|
|
55
55
|
_HTTP_KEEPALIVE_EXPIRY_SECONDS = 50.0
|
|
56
56
|
_EMBEDDING_RETRY_BACKOFF_SECONDS = 0.1
|
|
57
|
+
# ``embedding_provider_mode`` is called on every embedding request (often several
|
|
58
|
+
# times per request), and a down daemon resolves to ``inprocess`` every time, so
|
|
59
|
+
# an unconditional warning would flood the logs. Emit the fallback WARNING at most
|
|
60
|
+
# once per this interval per process.
|
|
61
|
+
_INPROCESS_FALLBACK_WARN_INTERVAL_SECONDS = 60.0
|
|
57
62
|
_SERVICE_MODES = {"local_service", "internal_service"}
|
|
58
63
|
_VALID_MODES = {"cloud", *_SERVICE_MODES, "inprocess", "off"}
|
|
59
64
|
_local_service_probe_cache: tuple[float, bool, str | None] | None = None
|
|
65
|
+
# Reason the most recent *fresh* /health probe failed (populated by
|
|
66
|
+
# ``_local_service_status``), surfaced in the inprocess-fallback WARNING.
|
|
67
|
+
_last_probe_failure_reason: str | None = None
|
|
68
|
+
# Monotonic timestamp of the last inprocess-fallback WARNING, for rate limiting.
|
|
69
|
+
_last_inprocess_fallback_warn_at: float | None = None
|
|
60
70
|
_http_client_lock = threading.Lock()
|
|
61
71
|
_http_client_instance: httpx.Client | None = None
|
|
62
72
|
_http_client_pid: int | None = None
|
|
@@ -154,7 +164,7 @@ def _http_client() -> httpx.Client:
|
|
|
154
164
|
|
|
155
165
|
|
|
156
166
|
def _local_service_status() -> tuple[bool, str | None]:
|
|
157
|
-
global _local_service_probe_cache
|
|
167
|
+
global _local_service_probe_cache, _last_probe_failure_reason
|
|
158
168
|
now = time.monotonic()
|
|
159
169
|
if _local_service_probe_cache is not None:
|
|
160
170
|
cached_at, cached_reachable, cached_model = _local_service_probe_cache
|
|
@@ -166,6 +176,7 @@ def _local_service_status() -> tuple[bool, str | None]:
|
|
|
166
176
|
if now - cached_at < ttl:
|
|
167
177
|
return cached_reachable, cached_model
|
|
168
178
|
|
|
179
|
+
reason: str | None = None
|
|
169
180
|
try:
|
|
170
181
|
response = _http_client().get(
|
|
171
182
|
f"{_local_service_url()}/health",
|
|
@@ -175,19 +186,53 @@ def _local_service_status() -> tuple[bool, str | None]:
|
|
|
175
186
|
active_model = response.json().get("active_model") if reachable else None
|
|
176
187
|
if not isinstance(active_model, str):
|
|
177
188
|
active_model = None
|
|
178
|
-
|
|
189
|
+
if not reachable:
|
|
190
|
+
reason = f"/health returned HTTP {response.status_code}"
|
|
191
|
+
except httpx.HTTPError as exc:
|
|
179
192
|
reachable = False
|
|
180
193
|
active_model = None
|
|
181
|
-
|
|
194
|
+
reason = f"{type(exc).__name__}: {exc}"
|
|
195
|
+
except ValueError as exc:
|
|
182
196
|
reachable = False
|
|
183
197
|
active_model = None
|
|
198
|
+
reason = f"invalid /health response: {exc}"
|
|
199
|
+
_last_probe_failure_reason = reason
|
|
184
200
|
_local_service_probe_cache = (now, reachable, active_model)
|
|
185
201
|
return reachable, active_model
|
|
186
202
|
|
|
187
203
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
def _warn_inprocess_fallback(model: str | None, reason: str | None) -> None:
|
|
205
|
+
"""Warn (rate-limited) that a ``local/*`` model fell back to the in-process embedder.
|
|
206
|
+
|
|
207
|
+
Emitted only when daemon-mode resolution for a ``local/*`` model fails the
|
|
208
|
+
``/health`` probe (or the daemon serves a different model) and routing falls
|
|
209
|
+
back to ``inprocess`` — NOT when ``inprocess`` was configured explicitly. The
|
|
210
|
+
fallback loads a second copy of the embedding model into this worker process,
|
|
211
|
+
so operators need to distinguish it from an intentional in-process config.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
model (str | None): The ``local/*`` embedding model being resolved.
|
|
215
|
+
reason (str | None): Why the daemon path was rejected, if known.
|
|
216
|
+
"""
|
|
217
|
+
global _last_inprocess_fallback_warn_at
|
|
218
|
+
now = time.monotonic()
|
|
219
|
+
if (
|
|
220
|
+
_last_inprocess_fallback_warn_at is not None
|
|
221
|
+
and now - _last_inprocess_fallback_warn_at
|
|
222
|
+
< _INPROCESS_FALLBACK_WARN_INTERVAL_SECONDS
|
|
223
|
+
):
|
|
224
|
+
return
|
|
225
|
+
_last_inprocess_fallback_warn_at = now
|
|
226
|
+
_LOGGER.warning(
|
|
227
|
+
"Embedding daemon probe at %s failed for model %r; falling back to an "
|
|
228
|
+
"in-process embedding model copy (a second model is loaded into this "
|
|
229
|
+
"worker process). Probe failure: %s. Further fallback warnings are "
|
|
230
|
+
"suppressed for %.0fs.",
|
|
231
|
+
_local_service_url(),
|
|
232
|
+
model,
|
|
233
|
+
reason or "unknown",
|
|
234
|
+
_INPROCESS_FALLBACK_WARN_INTERVAL_SECONDS,
|
|
235
|
+
)
|
|
191
236
|
|
|
192
237
|
|
|
193
238
|
def _ordered_embeddings_from_response(
|
|
@@ -260,7 +305,18 @@ def embedding_provider_mode(model: str | None = None) -> EmbeddingProviderMode:
|
|
|
260
305
|
return "local_service"
|
|
261
306
|
|
|
262
307
|
if _is_local_model(model):
|
|
263
|
-
|
|
308
|
+
reachable, active_model = _local_service_status()
|
|
309
|
+
if reachable and (active_model is None or active_model == model):
|
|
310
|
+
return "local_service"
|
|
311
|
+
if reachable:
|
|
312
|
+
reason = (
|
|
313
|
+
f"daemon is reachable but serves active_model={active_model!r}, "
|
|
314
|
+
f"not {model!r}"
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
reason = _last_probe_failure_reason
|
|
318
|
+
_warn_inprocess_fallback(model, reason)
|
|
319
|
+
return "inprocess"
|
|
264
320
|
return "cloud"
|
|
265
321
|
|
|
266
322
|
|
|
@@ -311,7 +367,12 @@ def get_service_embeddings(
|
|
|
311
367
|
chunk = texts[start : start + chunk_size]
|
|
312
368
|
embeddings.extend(
|
|
313
369
|
_post_embedding_batch(
|
|
314
|
-
url,
|
|
370
|
+
url,
|
|
371
|
+
model=model,
|
|
372
|
+
texts=chunk,
|
|
373
|
+
dimensions=dimensions,
|
|
374
|
+
timeout=timeout,
|
|
375
|
+
mode=mode,
|
|
315
376
|
)
|
|
316
377
|
)
|
|
317
378
|
return embeddings
|
|
@@ -324,6 +385,7 @@ def _post_embedding_batch(
|
|
|
324
385
|
texts: list[str],
|
|
325
386
|
dimensions: int | None,
|
|
326
387
|
timeout: float,
|
|
388
|
+
mode: EmbeddingProviderMode,
|
|
327
389
|
) -> list[list[float]]:
|
|
328
390
|
"""POST one bounded batch of texts to the embedding service."""
|
|
329
391
|
payload: dict[str, Any] = {"model": model, "input": texts}
|
|
@@ -368,9 +430,22 @@ def _post_embedding_batch(
|
|
|
368
430
|
last_error = exc
|
|
369
431
|
break
|
|
370
432
|
|
|
371
|
-
|
|
433
|
+
# For HTTP-status failures the response body carries the actionable
|
|
434
|
+
# detail (e.g. the daemon's 409 "already owns model X" message) — without
|
|
435
|
+
# it the log reads as a connectivity problem when it is a config conflict.
|
|
436
|
+
error_detail = str(last_error)
|
|
437
|
+
if isinstance(last_error, httpx.HTTPStatusError):
|
|
438
|
+
body = last_error.response.text.strip()
|
|
439
|
+
if body:
|
|
440
|
+
error_detail = f"{last_error} — response body: {body[:300]}"
|
|
441
|
+
_LOGGER.warning(
|
|
442
|
+
"Embedding service unavailable at %s: %s",
|
|
443
|
+
url,
|
|
444
|
+
error_detail,
|
|
445
|
+
extra={"mode": mode},
|
|
446
|
+
)
|
|
372
447
|
raise EmbeddingUnavailableError(
|
|
373
|
-
f"Embedding service unavailable at {url}: {
|
|
448
|
+
f"Embedding service unavailable at {url}: {error_detail}"
|
|
374
449
|
) from last_error
|
|
375
450
|
|
|
376
451
|
|