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
|
@@ -8,8 +8,15 @@ wrapper over the ``record_usage_event`` hook (which only enqueues). No DB I/O.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import logging
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
11
16
|
from reflexio.server.usage_metrics import record_usage_event
|
|
12
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
13
20
|
_INTERNAL = (
|
|
14
21
|
"internal" # == BillingCallerType.INTERNAL.value (kept literal; OSS stays clean)
|
|
15
22
|
)
|
|
@@ -29,7 +36,10 @@ def record_extraction_tokens(
|
|
|
29
36
|
) -> None:
|
|
30
37
|
"""Emit the Learning cost facet — call only when extraction fired.
|
|
31
38
|
|
|
32
|
-
No-op when ``billing_input_tokens <= 0``.
|
|
39
|
+
No-op when ``billing_input_tokens <= 0``. Each call mints a fresh
|
|
40
|
+
``event_key=f"tok:{uuid4()}"`` so two token emits under the same
|
|
41
|
+
``request_id`` (e.g. profile + playbook extraction in one request) never
|
|
42
|
+
collapse into one billed event downstream.
|
|
33
43
|
|
|
34
44
|
Args:
|
|
35
45
|
org_id: Organisation identifier.
|
|
@@ -51,6 +61,7 @@ def record_extraction_tokens(
|
|
|
51
61
|
pipeline=pipeline,
|
|
52
62
|
request_id=request_id,
|
|
53
63
|
session_id=session_id,
|
|
64
|
+
event_key=f"tok:{uuid.uuid4()}",
|
|
54
65
|
count_value=billing_input_tokens,
|
|
55
66
|
prompt_tokens=prompt_tokens,
|
|
56
67
|
completion_tokens=completion_tokens,
|
|
@@ -68,12 +79,26 @@ def record_learnings_generated(
|
|
|
68
79
|
platform_llm: bool | None,
|
|
69
80
|
platform_storage: bool | None,
|
|
70
81
|
pipeline: str | None = None,
|
|
82
|
+
user_id: str | None = None,
|
|
71
83
|
request_id: str | None = None,
|
|
72
84
|
session_id: str | None = None,
|
|
85
|
+
source: str | None = None,
|
|
86
|
+
agent_version: str | None = None,
|
|
87
|
+
playbook_name: str | None = None,
|
|
88
|
+
entity_type: str | None = None,
|
|
89
|
+
metadata: Mapping[str, Any] | None = None,
|
|
73
90
|
) -> None:
|
|
74
91
|
"""Emit the Learning value facet — number of profiles/playbooks generated.
|
|
75
92
|
|
|
76
|
-
|
|
93
|
+
Documented FALLBACK for callers that genuinely lack a per-record id list
|
|
94
|
+
(e.g. dedup/consolidation can reduce the persisted count below the raw
|
|
95
|
+
extracted count, so there is no safe 1:1 id per unit of ``count``). Prefer
|
|
96
|
+
:func:`record_learnings_generated_records` whenever the caller has the
|
|
97
|
+
durable learning ids in scope. No-op when ``count <= 0``.
|
|
98
|
+
|
|
99
|
+
Emits a single event carrying a synthesized ``event_key=f"learn-batch:{uuid4()}"``
|
|
100
|
+
(distinct per call) so this aggregate event still has a dedup key, even
|
|
101
|
+
though it is not entity-backed.
|
|
77
102
|
|
|
78
103
|
Args:
|
|
79
104
|
org_id: Organisation identifier.
|
|
@@ -81,8 +106,14 @@ def record_learnings_generated(
|
|
|
81
106
|
platform_llm: True iff the platform supplies the LLM for this org.
|
|
82
107
|
platform_storage: True iff the platform supplies storage; None defers to rollup.
|
|
83
108
|
pipeline: Optional pipeline tag (e.g. ``"playbook"``).
|
|
109
|
+
user_id: Optional user ID tied to the generated learning.
|
|
84
110
|
request_id: Optional request correlation ID.
|
|
85
111
|
session_id: Optional session ID.
|
|
112
|
+
source: Optional metering source/path label.
|
|
113
|
+
agent_version: Optional agent version tied to the generated learning.
|
|
114
|
+
playbook_name: Optional playbook name for playbook learnings.
|
|
115
|
+
entity_type: Optional entity type (e.g. ``"profile"``).
|
|
116
|
+
metadata: Optional path-specific usage metadata.
|
|
86
117
|
"""
|
|
87
118
|
if count <= 0:
|
|
88
119
|
return
|
|
@@ -91,15 +122,237 @@ def record_learnings_generated(
|
|
|
91
122
|
event_name="learnings_generated",
|
|
92
123
|
event_category="learning",
|
|
93
124
|
pipeline=pipeline,
|
|
125
|
+
user_id=user_id,
|
|
94
126
|
request_id=request_id,
|
|
95
127
|
session_id=session_id,
|
|
128
|
+
source=source,
|
|
129
|
+
agent_version=agent_version,
|
|
130
|
+
playbook_name=playbook_name,
|
|
131
|
+
entity_type=entity_type,
|
|
132
|
+
event_key=f"learn-batch:{uuid.uuid4()}",
|
|
96
133
|
count_value=count,
|
|
97
134
|
platform_llm=platform_llm,
|
|
98
135
|
platform_storage=platform_storage,
|
|
99
136
|
caller_type=_INTERNAL,
|
|
137
|
+
metadata=metadata,
|
|
100
138
|
)
|
|
101
139
|
|
|
102
140
|
|
|
141
|
+
def record_learnings_generated_records(
|
|
142
|
+
*,
|
|
143
|
+
org_id: str,
|
|
144
|
+
learning_ids: list[str],
|
|
145
|
+
platform_llm: bool | None,
|
|
146
|
+
platform_storage: bool | None,
|
|
147
|
+
pipeline: str | None = None,
|
|
148
|
+
user_id: str | None = None,
|
|
149
|
+
request_id: str | None = None,
|
|
150
|
+
session_id: str | None = None,
|
|
151
|
+
source: str | None = None,
|
|
152
|
+
agent_version: str | None = None,
|
|
153
|
+
playbook_name: str | None = None,
|
|
154
|
+
entity_type: str | None = None,
|
|
155
|
+
metadata: Mapping[str, Any] | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Emit the Learning value facet — one event per generated learning record.
|
|
158
|
+
|
|
159
|
+
Entity-backed alternative to :func:`record_learnings_generated`: emits one
|
|
160
|
+
``learnings_generated`` event per id in ``learning_ids`` (``count_value=1``,
|
|
161
|
+
``event_key=f"learn:{entity_type}:{id}"``, ``entity_id=id``) instead of a
|
|
162
|
+
single aggregate event, so downstream dedup can key on the learning id.
|
|
163
|
+
The ``entity_type`` segment is required for collision-freedom: entity-backed
|
|
164
|
+
callers draw ids from separate autoincrement primary keys in separate
|
|
165
|
+
tables (e.g. ``user_playbook_id`` and ``agent_playbook_id`` both start at
|
|
166
|
+
1), so the same integer id can legitimately occur in two tables — without
|
|
167
|
+
the entity-type segment those would mint the same ``event_key`` and
|
|
168
|
+
collapse into one event downstream. When ``entity_type`` is falsy, a
|
|
169
|
+
stable ``"_"`` placeholder is used (``learn:_:{id}``) rather than emitting
|
|
170
|
+
``entity_type=None`` literally into the key. The summed ``count_value``
|
|
171
|
+
across the emitted events equals ``len(learning_ids)`` — unchanged from
|
|
172
|
+
the total a caller would have passed as ``count`` to
|
|
173
|
+
:func:`record_learnings_generated`.
|
|
174
|
+
|
|
175
|
+
Callers must pass real, durable ids — never fabricate one to pad the
|
|
176
|
+
list. No-op when ``learning_ids`` is empty.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
org_id: Organisation identifier.
|
|
180
|
+
learning_ids: Ids of the learnings durably generated in this run
|
|
181
|
+
(e.g. ``profile_id`` / ``user_playbook_id`` / ``agent_playbook_id``).
|
|
182
|
+
platform_llm: True iff the platform supplies the LLM for this org.
|
|
183
|
+
platform_storage: True iff the platform supplies storage; None defers to rollup.
|
|
184
|
+
pipeline: Optional pipeline tag (e.g. ``"playbook"``).
|
|
185
|
+
user_id: Optional user ID tied to the generated learning.
|
|
186
|
+
request_id: Optional request correlation ID.
|
|
187
|
+
session_id: Optional session ID.
|
|
188
|
+
source: Optional metering source/path label.
|
|
189
|
+
agent_version: Optional agent version tied to the generated learning.
|
|
190
|
+
playbook_name: Optional playbook name for playbook learnings.
|
|
191
|
+
entity_type: Optional entity type (e.g. ``"profile"``).
|
|
192
|
+
metadata: Optional path-specific usage metadata (shared across events).
|
|
193
|
+
"""
|
|
194
|
+
key_entity_type = entity_type or "_"
|
|
195
|
+
for learning_id in learning_ids:
|
|
196
|
+
record_usage_event(
|
|
197
|
+
org_id=org_id,
|
|
198
|
+
event_name="learnings_generated",
|
|
199
|
+
event_category="learning",
|
|
200
|
+
pipeline=pipeline,
|
|
201
|
+
user_id=user_id,
|
|
202
|
+
request_id=request_id,
|
|
203
|
+
session_id=session_id,
|
|
204
|
+
source=source,
|
|
205
|
+
agent_version=agent_version,
|
|
206
|
+
playbook_name=playbook_name,
|
|
207
|
+
entity_type=entity_type,
|
|
208
|
+
entity_id=learning_id,
|
|
209
|
+
event_key=f"learn:{key_entity_type}:{learning_id}",
|
|
210
|
+
count_value=1,
|
|
211
|
+
platform_llm=platform_llm,
|
|
212
|
+
platform_storage=platform_storage,
|
|
213
|
+
caller_type=_INTERNAL,
|
|
214
|
+
metadata=metadata,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def emit_learnings_generated(
|
|
219
|
+
*,
|
|
220
|
+
org_id: str,
|
|
221
|
+
configurator: Any,
|
|
222
|
+
count: int,
|
|
223
|
+
source: str,
|
|
224
|
+
pipeline: str | None = None,
|
|
225
|
+
user_id: str | None = None,
|
|
226
|
+
request_id: str | None = None,
|
|
227
|
+
agent_version: str | None = None,
|
|
228
|
+
playbook_name: str | None = None,
|
|
229
|
+
entity_type: str | None = None,
|
|
230
|
+
metadata: Mapping[str, Any] | None = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Resolve ``platform_llm`` from config and emit the Learning value facet.
|
|
233
|
+
|
|
234
|
+
Convenience wrapper for the non-extraction learning-mutation paths (reflection,
|
|
235
|
+
resumable-extraction finalization, aggregation, offline-tuner auto-apply). It
|
|
236
|
+
owns the ``configurator.get_config()`` + ``platform_llm_from_config`` lookup so
|
|
237
|
+
each call site stays a thin one-liner, and — critically — is **guarded**: the
|
|
238
|
+
product path must never fail because metering failed, so config resolution and
|
|
239
|
+
emission are wrapped and any exception is logged and swallowed (mirroring the
|
|
240
|
+
extraction path's ``_record_billing_learning_events``). No-op when
|
|
241
|
+
``count <= 0``.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
org_id: Organisation identifier.
|
|
245
|
+
configurator: Object exposing ``get_config()`` for platform-LLM resolution.
|
|
246
|
+
count: Number of learnings durably produced by this path.
|
|
247
|
+
source: Metering source/path label (e.g. ``"reflection"``).
|
|
248
|
+
pipeline: Optional pipeline tag (e.g. ``"playbook"``).
|
|
249
|
+
user_id: Optional user ID tied to the generated learning.
|
|
250
|
+
request_id: Optional request correlation ID.
|
|
251
|
+
agent_version: Optional agent version tied to the generated learning.
|
|
252
|
+
playbook_name: Optional playbook name for playbook learnings.
|
|
253
|
+
entity_type: Optional entity type (e.g. ``"profile"``).
|
|
254
|
+
metadata: Optional path-specific usage metadata.
|
|
255
|
+
"""
|
|
256
|
+
if count <= 0:
|
|
257
|
+
return
|
|
258
|
+
try:
|
|
259
|
+
from reflexio.server.billing_signals import platform_llm_from_config
|
|
260
|
+
|
|
261
|
+
config = configurator.get_config()
|
|
262
|
+
record_learnings_generated(
|
|
263
|
+
org_id=org_id,
|
|
264
|
+
count=count,
|
|
265
|
+
platform_llm=platform_llm_from_config(config),
|
|
266
|
+
platform_storage=None,
|
|
267
|
+
pipeline=pipeline,
|
|
268
|
+
user_id=user_id,
|
|
269
|
+
request_id=request_id,
|
|
270
|
+
source=source,
|
|
271
|
+
agent_version=agent_version,
|
|
272
|
+
playbook_name=playbook_name,
|
|
273
|
+
entity_type=entity_type,
|
|
274
|
+
metadata=metadata,
|
|
275
|
+
)
|
|
276
|
+
except Exception:
|
|
277
|
+
logger.warning(
|
|
278
|
+
"emit_learnings_generated failed for source=%s org=%s; "
|
|
279
|
+
"learnings_generated event not emitted",
|
|
280
|
+
source,
|
|
281
|
+
org_id,
|
|
282
|
+
exc_info=True,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def emit_learnings_generated_records(
|
|
287
|
+
*,
|
|
288
|
+
org_id: str,
|
|
289
|
+
configurator: Any,
|
|
290
|
+
learning_ids: list[str],
|
|
291
|
+
source: str,
|
|
292
|
+
pipeline: str | None = None,
|
|
293
|
+
user_id: str | None = None,
|
|
294
|
+
request_id: str | None = None,
|
|
295
|
+
agent_version: str | None = None,
|
|
296
|
+
playbook_name: str | None = None,
|
|
297
|
+
entity_type: str | None = None,
|
|
298
|
+
metadata: Mapping[str, Any] | None = None,
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Resolve ``platform_llm`` from config and emit one event per learning id.
|
|
301
|
+
|
|
302
|
+
Entity-backed counterpart to :func:`emit_learnings_generated`, currently
|
|
303
|
+
adopted by two of the non-extraction learning-mutation paths —
|
|
304
|
+
resumable-extraction finalization and aggregation — the callers with
|
|
305
|
+
durable per-record ids in scope. Extraction, reflection, and offline-tuner
|
|
306
|
+
auto-apply do not have a safe 1:1 id per unit of count (see
|
|
307
|
+
:func:`record_learnings_generated_records`) and use the count-based
|
|
308
|
+
:func:`emit_learnings_generated` fallback instead. Same guard semantics:
|
|
309
|
+
config resolution and emission are wrapped and any exception is logged
|
|
310
|
+
and swallowed — the product path must never fail because metering
|
|
311
|
+
failed. No-op when ``learning_ids`` is empty.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
org_id: Organisation identifier.
|
|
315
|
+
configurator: Object exposing ``get_config()`` for platform-LLM resolution.
|
|
316
|
+
learning_ids: Ids of the learnings durably produced by this path.
|
|
317
|
+
source: Metering source/path label (e.g. ``"reflection"``).
|
|
318
|
+
pipeline: Optional pipeline tag (e.g. ``"playbook"``).
|
|
319
|
+
user_id: Optional user ID tied to the generated learning.
|
|
320
|
+
request_id: Optional request correlation ID.
|
|
321
|
+
agent_version: Optional agent version tied to the generated learning.
|
|
322
|
+
playbook_name: Optional playbook name for playbook learnings.
|
|
323
|
+
entity_type: Optional entity type (e.g. ``"profile"``).
|
|
324
|
+
metadata: Optional path-specific usage metadata (shared across events).
|
|
325
|
+
"""
|
|
326
|
+
if not learning_ids:
|
|
327
|
+
return
|
|
328
|
+
try:
|
|
329
|
+
from reflexio.server.billing_signals import platform_llm_from_config
|
|
330
|
+
|
|
331
|
+
config = configurator.get_config()
|
|
332
|
+
record_learnings_generated_records(
|
|
333
|
+
org_id=org_id,
|
|
334
|
+
learning_ids=learning_ids,
|
|
335
|
+
platform_llm=platform_llm_from_config(config),
|
|
336
|
+
platform_storage=None,
|
|
337
|
+
pipeline=pipeline,
|
|
338
|
+
user_id=user_id,
|
|
339
|
+
request_id=request_id,
|
|
340
|
+
source=source,
|
|
341
|
+
agent_version=agent_version,
|
|
342
|
+
playbook_name=playbook_name,
|
|
343
|
+
entity_type=entity_type,
|
|
344
|
+
metadata=metadata,
|
|
345
|
+
)
|
|
346
|
+
except Exception:
|
|
347
|
+
logger.warning(
|
|
348
|
+
"emit_learnings_generated_records failed for source=%s org=%s; "
|
|
349
|
+
"learnings_generated events not emitted",
|
|
350
|
+
source,
|
|
351
|
+
org_id,
|
|
352
|
+
exc_info=True,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
103
356
|
def record_applied_learnings(
|
|
104
357
|
*,
|
|
105
358
|
org_id: str,
|
|
@@ -114,6 +367,8 @@ def record_applied_learnings(
|
|
|
114
367
|
"""Emit the Application line — surfaced top-K learnings.
|
|
115
368
|
|
|
116
369
|
No-op unless ``caller_type == "production_agent"`` AND ``surfaced_count > 0``.
|
|
370
|
+
Each call mints a fresh ``event_key=f"applied:{uuid4()}"`` — a distinct
|
|
371
|
+
key per search-response moment, never collapsed by ``request_id``.
|
|
117
372
|
|
|
118
373
|
Args:
|
|
119
374
|
org_id: Organisation identifier.
|
|
@@ -134,6 +389,7 @@ def record_applied_learnings(
|
|
|
134
389
|
pipeline=pipeline,
|
|
135
390
|
request_id=request_id,
|
|
136
391
|
session_id=session_id,
|
|
392
|
+
event_key=f"applied:{uuid.uuid4()}",
|
|
137
393
|
count_value=surfaced_count,
|
|
138
394
|
platform_llm=platform_llm,
|
|
139
395
|
platform_storage=platform_storage,
|
|
@@ -152,7 +408,10 @@ def record_search_request(
|
|
|
152
408
|
|
|
153
409
|
No-op unless ``caller_type == "production_agent"``. Unlike
|
|
154
410
|
:func:`record_applied_learnings`, empty search responses still count because
|
|
155
|
-
this measures requests made, not learnings surfaced.
|
|
411
|
+
this measures requests made, not learnings surfaced. Each call mints a
|
|
412
|
+
fresh ``event_key=f"search:{uuid4()}"`` — a distinct key per request, so
|
|
413
|
+
two searches under the same ``request_id`` are never collapsed into one
|
|
414
|
+
billed event downstream.
|
|
156
415
|
|
|
157
416
|
Args:
|
|
158
417
|
org_id: Organisation identifier.
|
|
@@ -168,6 +427,7 @@ def record_search_request(
|
|
|
168
427
|
event_category="application",
|
|
169
428
|
request_id=request_id,
|
|
170
429
|
session_id=session_id,
|
|
430
|
+
event_key=f"search:{uuid.uuid4()}",
|
|
171
431
|
count_value=1,
|
|
172
432
|
caller_type=caller_type,
|
|
173
433
|
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Process-wide bounded executor for deferred fire-and-forget callbacks.
|
|
2
|
+
|
|
3
|
+
Replaces thread-per-fire in the debounce schedulers (tagging, group
|
|
4
|
+
evaluation, playbook optimization) so thread count is O(1) instead of
|
|
5
|
+
O(active keys) (spec section 6.2). Sizes are module constants, not env vars.
|
|
6
|
+
|
|
7
|
+
Overflow policy: drop-oldest (a dropped fire is a stale debounced trigger —
|
|
8
|
+
the next event for that key re-creates it) with one warning log per drop and
|
|
9
|
+
a throttled anomaly when drops exceed ``_DROP_RATE_PER_MINUTE`` in a rolling
|
|
10
|
+
minute (the "this is routine, resize it" signal).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from collections import deque
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import NamedTuple
|
|
21
|
+
|
|
22
|
+
from reflexio.server.tracing import capture_anomaly
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_WORKERS = 16
|
|
27
|
+
_QUEUE_SIZE = 256
|
|
28
|
+
_DROP_RATE_PER_MINUTE = 10
|
|
29
|
+
_DROP_ANOMALY_THROTTLE_SECONDS = 3600.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _DropFacts(NamedTuple):
|
|
33
|
+
"""Facts about one drop, collected while ``_cond`` is held.
|
|
34
|
+
|
|
35
|
+
Carries everything :meth:`BoundedCallbackExecutor._emit_drop` needs to log
|
|
36
|
+
+ escalate AFTER the lock is released, so the contended lock is never held
|
|
37
|
+
across a logging/anomaly call.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
dropped_name: str
|
|
41
|
+
drops_last_minute: int
|
|
42
|
+
fire_anomaly: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BoundedCallbackExecutor:
|
|
46
|
+
"""Fixed worker pool + bounded FIFO queue with drop-oldest overflow.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
workers: Number of daemon worker threads.
|
|
50
|
+
queue_size: Max queued callbacks before drop-oldest applies.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self, *, workers: int = _WORKERS, queue_size: int = _QUEUE_SIZE
|
|
55
|
+
) -> None:
|
|
56
|
+
self._queue: deque[tuple[str, Callable[[], None]]] = deque()
|
|
57
|
+
self._queue_size = queue_size
|
|
58
|
+
self._cond = threading.Condition()
|
|
59
|
+
self._drop_times: deque[float] = deque()
|
|
60
|
+
self._last_drop_anomaly = 0.0
|
|
61
|
+
for i in range(workers):
|
|
62
|
+
threading.Thread(
|
|
63
|
+
target=self._worker_loop,
|
|
64
|
+
daemon=True,
|
|
65
|
+
name=f"callback-exec-{i}",
|
|
66
|
+
).start()
|
|
67
|
+
|
|
68
|
+
def submit(self, name: str, fn: Callable[[], None]) -> None:
|
|
69
|
+
"""Enqueue a callback; on a full queue, evict the oldest queued fire.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Short label for logs (e.g. the scheduler key).
|
|
73
|
+
fn: Zero-arg callback to run on a worker.
|
|
74
|
+
"""
|
|
75
|
+
drop_facts: _DropFacts | None = None
|
|
76
|
+
with self._cond:
|
|
77
|
+
if len(self._queue) >= self._queue_size:
|
|
78
|
+
dropped_name, _ = self._queue.popleft()
|
|
79
|
+
drop_facts = self._record_drop_locked(dropped_name)
|
|
80
|
+
self._queue.append((name, fn))
|
|
81
|
+
self._cond.notify()
|
|
82
|
+
if drop_facts is not None:
|
|
83
|
+
self._emit_drop(drop_facts)
|
|
84
|
+
|
|
85
|
+
def _record_drop_locked(self, dropped_name: str) -> _DropFacts:
|
|
86
|
+
"""Collect the facts of one drop while ``self._cond`` is held.
|
|
87
|
+
|
|
88
|
+
Only bookkeeping happens under the lock (all 16 workers + all
|
|
89
|
+
producers contend on it): pruning the rolling drop-time window and
|
|
90
|
+
deciding — and, if firing, committing — the throttle state
|
|
91
|
+
(``_last_drop_anomaly``) so the decision stays race-free. Logging and
|
|
92
|
+
anomaly emission are the caller's job, done AFTER the lock is
|
|
93
|
+
released (see :meth:`_emit_drop`).
|
|
94
|
+
"""
|
|
95
|
+
now = time.monotonic()
|
|
96
|
+
self._drop_times.append(now)
|
|
97
|
+
while self._drop_times and now - self._drop_times[0] > 60.0:
|
|
98
|
+
self._drop_times.popleft()
|
|
99
|
+
drops_last_minute = len(self._drop_times)
|
|
100
|
+
fire_anomaly = (
|
|
101
|
+
drops_last_minute > _DROP_RATE_PER_MINUTE
|
|
102
|
+
and now - self._last_drop_anomaly > _DROP_ANOMALY_THROTTLE_SECONDS
|
|
103
|
+
)
|
|
104
|
+
if fire_anomaly:
|
|
105
|
+
self._last_drop_anomaly = now
|
|
106
|
+
return _DropFacts(
|
|
107
|
+
dropped_name=dropped_name,
|
|
108
|
+
drops_last_minute=drops_last_minute,
|
|
109
|
+
fire_anomaly=fire_anomaly,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _emit_drop(self, facts: _DropFacts) -> None:
|
|
113
|
+
"""Log one drop and (if decided under the lock) escalate the anomaly.
|
|
114
|
+
|
|
115
|
+
Called with ``self._cond`` NOT held — logging and ``capture_anomaly``
|
|
116
|
+
never happen while the contended lock is owned. References the
|
|
117
|
+
module-level ``capture_anomaly`` name (not a bound attribute) so
|
|
118
|
+
tests that ``monkeypatch.setattr(module, "capture_anomaly", ...)``
|
|
119
|
+
still intercept the call.
|
|
120
|
+
"""
|
|
121
|
+
logger.warning(
|
|
122
|
+
"event=callback_executor_drop_oldest dropped=%s", facts.dropped_name
|
|
123
|
+
)
|
|
124
|
+
if facts.fire_anomaly:
|
|
125
|
+
capture_anomaly(
|
|
126
|
+
"callback_executor.drop_rate_exceeded",
|
|
127
|
+
drops_last_minute=facts.drops_last_minute,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _worker_loop(self) -> None:
|
|
131
|
+
while True:
|
|
132
|
+
with self._cond:
|
|
133
|
+
while not self._queue:
|
|
134
|
+
self._cond.wait()
|
|
135
|
+
name, fn = self._queue.popleft()
|
|
136
|
+
try:
|
|
137
|
+
fn()
|
|
138
|
+
except BaseException: # noqa: BLE001 — daemon worker threads must
|
|
139
|
+
# survive anything a callback raises (including a callback
|
|
140
|
+
# that itself raises SystemExit); KeyboardInterrupt is only
|
|
141
|
+
# ever delivered to the main thread, so swallowing it here is
|
|
142
|
+
# safe. The fixed 16-worker pool must never silently shrink.
|
|
143
|
+
logger.exception(
|
|
144
|
+
"event=callback_executor_callback_failed name=%s", name
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_executor: BoundedCallbackExecutor | None = None
|
|
149
|
+
_executor_lock = threading.Lock()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def submit_callback(name: str, fn: Callable[[], None]) -> None:
|
|
153
|
+
"""Submit to the lazily-created process-wide executor.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
name: Short label for logs.
|
|
157
|
+
fn: Zero-arg callback.
|
|
158
|
+
"""
|
|
159
|
+
global _executor # noqa: PLW0603
|
|
160
|
+
if _executor is None:
|
|
161
|
+
with _executor_lock:
|
|
162
|
+
if _executor is None:
|
|
163
|
+
_executor = BoundedCallbackExecutor()
|
|
164
|
+
_executor.submit(name, fn)
|