claude-smart 0.2.49 → 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 +10 -43
- package/bin/claude-smart.js +105 -0
- 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/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +197 -1
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +757 -119
- package/plugin/scripts/codex-hook.js +63 -225
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +117 -136
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/smart-install.sh +15 -1
- package/plugin/src/claude_smart/cli.py +14 -0
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +4 -1
- 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 +1 -1
- 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/client/client.py +126 -3
- 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 +177 -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 +66 -3
- 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 +62 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- 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 +20 -5
- 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/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/worker.py +85 -16
- 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_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
- 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 +122 -44
- 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 +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
- 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 +36 -1
- 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/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 +43 -0
- 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/_user.py +45 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
- 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 +50 -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 +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -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
|
@@ -7,7 +7,6 @@ import logging
|
|
|
7
7
|
import os
|
|
8
8
|
import uuid
|
|
9
9
|
from datetime import UTC, datetime
|
|
10
|
-
from typing import Any, cast
|
|
11
10
|
|
|
12
11
|
from pydantic import BaseModel, ConfigDict, Field
|
|
13
12
|
|
|
@@ -20,8 +19,8 @@ from reflexio.server.services.deduplication_utils import (
|
|
|
20
19
|
BaseDeduplicator,
|
|
21
20
|
format_dedup_timestamp,
|
|
22
21
|
parse_item_id,
|
|
22
|
+
resolve_dedup_query_embeddings,
|
|
23
23
|
)
|
|
24
|
-
from reflexio.server.services.embedding_text import embedding_input
|
|
25
24
|
from reflexio.server.services.profile.profile_generation_service_utils import (
|
|
26
25
|
ProfileTimeToLive,
|
|
27
26
|
calculate_expiration_timestamp,
|
|
@@ -306,42 +305,9 @@ class ProfileConsolidator(BaseDeduplicator):
|
|
|
306
305
|
|
|
307
306
|
# Generate embeddings with the request storage backend so dedup search
|
|
308
307
|
# uses the same model/prefix/routing as normal profile search.
|
|
309
|
-
embeddings
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if callable(get_storage_embedding):
|
|
313
|
-
logger.info(
|
|
314
|
-
"Profile dedup query embeddings: source=storage model=%s",
|
|
315
|
-
getattr(storage, "embedding_model_name", "unknown"),
|
|
316
|
-
)
|
|
317
|
-
embeddings = [
|
|
318
|
-
cast(
|
|
319
|
-
list[float],
|
|
320
|
-
get_storage_embedding(query_text, purpose="query"),
|
|
321
|
-
)
|
|
322
|
-
for query_text in query_texts
|
|
323
|
-
]
|
|
324
|
-
else:
|
|
325
|
-
storage_with_embeddings = cast(Any, storage)
|
|
326
|
-
embedding_model_name = storage_with_embeddings.embedding_model_name
|
|
327
|
-
embedding_dimensions = storage_with_embeddings.embedding_dimensions
|
|
328
|
-
logger.info(
|
|
329
|
-
"Profile dedup query embeddings: source=llm_client model=%s",
|
|
330
|
-
embedding_model_name,
|
|
331
|
-
)
|
|
332
|
-
embeddings = list(
|
|
333
|
-
self.client.get_embeddings(
|
|
334
|
-
[
|
|
335
|
-
embedding_input(query_text, purpose="query")
|
|
336
|
-
for query_text in query_texts
|
|
337
|
-
],
|
|
338
|
-
model=embedding_model_name,
|
|
339
|
-
dimensions=embedding_dimensions,
|
|
340
|
-
)
|
|
341
|
-
)
|
|
342
|
-
except Exception as e:
|
|
343
|
-
logger.warning("Failed to generate embeddings for dedup search: %s", e)
|
|
344
|
-
embeddings = [None] * len(query_texts)
|
|
308
|
+
embeddings = resolve_dedup_query_embeddings(
|
|
309
|
+
storage, self.client, query_texts, entity_label="Profile"
|
|
310
|
+
)
|
|
345
311
|
|
|
346
312
|
# Search for each new profile.
|
|
347
313
|
#
|
|
@@ -465,7 +431,14 @@ class ProfileConsolidator(BaseDeduplicator):
|
|
|
465
431
|
|
|
466
432
|
dedup_output = response
|
|
467
433
|
except Exception as e:
|
|
468
|
-
|
|
434
|
+
# Graceful degradation: dedup is best-effort — on any failure
|
|
435
|
+
# (commonly a transient minimax timeout/overload) keep the profiles
|
|
436
|
+
# un-deduped rather than failing the caller. WARNING, not ERROR, so a
|
|
437
|
+
# flaky provider doesn't flood error alerts for a handled fallback.
|
|
438
|
+
logger.warning(
|
|
439
|
+
"Failed to identify duplicates (%s); keeping profiles un-deduped",
|
|
440
|
+
str(e),
|
|
441
|
+
)
|
|
469
442
|
return _strip_deletion_markers(new_profiles), [], []
|
|
470
443
|
|
|
471
444
|
if not dedup_output.duplicate_groups and not dedup_output.deletions:
|
|
@@ -14,6 +14,7 @@ from reflexio.models.config_schema import ProfileExtractorConfig
|
|
|
14
14
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
15
15
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
16
16
|
from reflexio.server.llm.token_accounting import RunTokenTotals, sum_trace_tokens
|
|
17
|
+
from reflexio.server.services.deferred_learning_plan import ExtractorBookmarkAdvance
|
|
17
18
|
from reflexio.server.services.extraction.outcome import ExtractionOutcome
|
|
18
19
|
from reflexio.server.services.extraction.resumable_agent import (
|
|
19
20
|
run_resumable_extraction_agent,
|
|
@@ -181,25 +182,6 @@ class ProfileExtractor:
|
|
|
181
182
|
)
|
|
182
183
|
return session_data_models
|
|
183
184
|
|
|
184
|
-
def _update_operation_state(
|
|
185
|
-
self, request_interaction_data_models: list[RequestInteractionDataModel]
|
|
186
|
-
) -> None:
|
|
187
|
-
"""
|
|
188
|
-
Update operation state after processing interactions.
|
|
189
|
-
|
|
190
|
-
Args:
|
|
191
|
-
request_interaction_data_models: The interactions that were processed
|
|
192
|
-
"""
|
|
193
|
-
all_interactions = extract_interactions_from_request_interaction_data_models(
|
|
194
|
-
request_interaction_data_models
|
|
195
|
-
)
|
|
196
|
-
mgr = self._create_state_manager()
|
|
197
|
-
mgr.update_extractor_bookmark(
|
|
198
|
-
extractor_name=get_extractor_name(self.config),
|
|
199
|
-
processed_interactions=all_interactions,
|
|
200
|
-
user_id=self.service_config.user_id,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
185
|
def run(self) -> list[UserProfile] | ExtractionOutcome[UserProfile] | None:
|
|
204
186
|
"""
|
|
205
187
|
Extract profiles from request interaction groups.
|
|
@@ -209,10 +191,12 @@ class ProfileExtractor:
|
|
|
209
191
|
2. Applies time range filter for rerun flows
|
|
210
192
|
3. Calls LLM to extract profiles
|
|
211
193
|
4. Converts raw extraction to UserProfile objects
|
|
212
|
-
5.
|
|
194
|
+
5. Defers the stride-bookmark advance onto the outcome (applied in persist)
|
|
213
195
|
|
|
214
196
|
Returns:
|
|
215
|
-
|
|
197
|
+
None when there are no interactions to process; otherwise an
|
|
198
|
+
ExtractionOutcome carrying the extracted profiles, the resumable
|
|
199
|
+
run_id (when set), and the deferred bookmark advance.
|
|
216
200
|
"""
|
|
217
201
|
# Collect interactions using extractor's own window_size/stride_size settings
|
|
218
202
|
request_interaction_data_models = self._get_interactions()
|
|
@@ -257,22 +241,33 @@ class ProfileExtractor:
|
|
|
257
241
|
request_id=self.service_config.request_id,
|
|
258
242
|
source_interaction_ids=source_interaction_ids,
|
|
259
243
|
)
|
|
244
|
+
# Defer the stride-bookmark advance onto the outcome instead of
|
|
245
|
+
# self-advancing here (F1): the advance is applied downstream — inside
|
|
246
|
+
# the persist fence on the durable path, or in ``.run()``'s persist
|
|
247
|
+
# half — so it stays atomic with the profile row writes it corresponds
|
|
248
|
+
# to. Only produced when output was generated (bookmark-iff-rows).
|
|
249
|
+
bookmark_advance: ExtractorBookmarkAdvance | None = None
|
|
260
250
|
if raw_profiles:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# Dropping the run_id here would orphan the run in AGENT_COMPLETED and
|
|
268
|
-
# sever the resolve -> resume chain. Mirrors PlaybookExtractor.run().
|
|
269
|
-
if self._last_resumable_run_id:
|
|
270
|
-
return ExtractionOutcome.completed(
|
|
271
|
-
user_profiles,
|
|
272
|
-
run_id=self._last_resumable_run_id,
|
|
273
|
-
token_totals=self._last_resumable_token_totals,
|
|
251
|
+
bookmark_advance = ExtractorBookmarkAdvance(
|
|
252
|
+
extractor_name=get_extractor_name(self.config),
|
|
253
|
+
processed_interactions=extract_interactions_from_request_interaction_data_models(
|
|
254
|
+
request_interaction_data_models
|
|
255
|
+
),
|
|
256
|
+
user_id=self.service_config.user_id,
|
|
274
257
|
)
|
|
275
|
-
|
|
258
|
+
|
|
259
|
+
# Always return an ExtractionOutcome so the bookmark advance rides along
|
|
260
|
+
# even in the non-resumable case. A resumable run must also surface its
|
|
261
|
+
# run_id so the generation service can finalize the _agent_runs row
|
|
262
|
+
# (FINALIZED_PENDING_TOOL when the agent created a follow-up ask and
|
|
263
|
+
# finished with empty output); dropping it would orphan the run in
|
|
264
|
+
# AGENT_COMPLETED and sever the resolve -> resume chain.
|
|
265
|
+
return ExtractionOutcome.completed(
|
|
266
|
+
user_profiles,
|
|
267
|
+
run_id=self._last_resumable_run_id,
|
|
268
|
+
token_totals=self._last_resumable_token_totals,
|
|
269
|
+
bookmark_advance=bookmark_advance,
|
|
270
|
+
)
|
|
276
271
|
|
|
277
272
|
def _convert_raw_to_user_profiles(
|
|
278
273
|
self,
|
|
@@ -27,6 +27,7 @@ from reflexio.server.services.base_generation_service import (
|
|
|
27
27
|
BaseGenerationService,
|
|
28
28
|
StatusChangeOperation,
|
|
29
29
|
)
|
|
30
|
+
from reflexio.server.services.deferred_learning_plan import ProfileWritePlan
|
|
30
31
|
from reflexio.server.services.profile.components.extractor import ProfileExtractor
|
|
31
32
|
from reflexio.server.services.profile.profile_generation_service_utils import (
|
|
32
33
|
ProfileGenerationRequest,
|
|
@@ -123,9 +124,10 @@ class ProfileGenerationService(
|
|
|
123
124
|
else:
|
|
124
125
|
existing_profiles = self.storage.get_user_profile(request.user_id) # type: ignore[reportOptionalMemberAccess]
|
|
125
126
|
|
|
127
|
+
generation_request_id = request.request_id
|
|
126
128
|
return ProfileGenerationServiceConfig(
|
|
127
129
|
user_id=request.user_id,
|
|
128
|
-
request_id=
|
|
130
|
+
request_id=generation_request_id,
|
|
129
131
|
source=request.source,
|
|
130
132
|
existing_data=existing_profiles,
|
|
131
133
|
allow_manual_trigger=self.allow_manual_trigger,
|
|
@@ -147,12 +149,24 @@ class ProfileGenerationService(
|
|
|
147
149
|
[p for result in results if result for p in result]
|
|
148
150
|
)
|
|
149
151
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
+
def _resolve_write_plan(
|
|
153
|
+
self, results: list[list[UserProfile]]
|
|
154
|
+
) -> ProfileWritePlan | None:
|
|
155
|
+
"""Compute-half of profile finalization — NO learning DB write.
|
|
156
|
+
|
|
157
|
+
Flattens the extractor results, runs the deduplicator (the 2nd LLM call
|
|
158
|
+
+ reads of existing rows), assigns ``source``/``status``, resolves the
|
|
159
|
+
missing-``request_id`` guard (dropping unreconstructable supersede ids),
|
|
160
|
+
and **precomputes embeddings** on the new rows. Returns a
|
|
161
|
+
:class:`ProfileWritePlan` for the persist half, or ``None`` when there is
|
|
162
|
+
nothing to write. Issues no ``add_user_profile``/``supersede_*`` — the
|
|
163
|
+
write is the persist half's job (compute is write-free).
|
|
164
|
+
"""
|
|
152
165
|
user_id = self.service_config.user_id # type: ignore[reportOptionalMemberAccess]
|
|
153
166
|
source = self.service_config.source # type: ignore[reportOptionalMemberAccess]
|
|
154
|
-
|
|
167
|
+
generation_request_id = self.service_config.request_id # type: ignore[reportOptionalMemberAccess]
|
|
155
168
|
|
|
169
|
+
all_new_profiles = [p for result in results if result for p in result]
|
|
156
170
|
existing_ids_to_delete: list[str] = []
|
|
157
171
|
|
|
158
172
|
# Always run deduplicator when enabled and there are new profiles
|
|
@@ -170,7 +184,9 @@ class ProfileGenerationService(
|
|
|
170
184
|
output_pending_status=self.output_pending_status,
|
|
171
185
|
)
|
|
172
186
|
all_new_profiles, existing_ids_to_delete, _superseded_profiles = (
|
|
173
|
-
consolidator.deduplicate(
|
|
187
|
+
consolidator.deduplicate(
|
|
188
|
+
all_new_profiles, user_id, generation_request_id
|
|
189
|
+
)
|
|
174
190
|
)
|
|
175
191
|
logger.info(
|
|
176
192
|
"Profile updates after deduplication: %d profiles, %d existing to delete",
|
|
@@ -183,60 +199,120 @@ class ProfileGenerationService(
|
|
|
183
199
|
profile.source = source
|
|
184
200
|
profile.status = Status.PENDING if self.output_pending_status else None
|
|
185
201
|
|
|
186
|
-
#
|
|
202
|
+
# Missing-request_id guard (moved here, in compute). An empty request_id
|
|
203
|
+
# makes the supersede unreconstructable (the lineage events are keyed on
|
|
204
|
+
# it). Fail loud and drop those ids entirely — never silently
|
|
205
|
+
# hard-delete. Persist then only supersedes with a non-empty request_id.
|
|
206
|
+
if existing_ids_to_delete and not generation_request_id:
|
|
207
|
+
capture_anomaly(
|
|
208
|
+
"lineage.dedup.missing_request_id",
|
|
209
|
+
level="error",
|
|
210
|
+
org_id=self.org_id,
|
|
211
|
+
user_id=user_id,
|
|
212
|
+
)
|
|
213
|
+
existing_ids_to_delete = []
|
|
214
|
+
|
|
215
|
+
if not all_new_profiles and not existing_ids_to_delete:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
# Precompute embeddings on the new rows (compute-side, NO DB write). The
|
|
219
|
+
# persist half passes skip_embedding=True so no embedding runs in the fence.
|
|
187
220
|
if all_new_profiles:
|
|
221
|
+
self.storage.precompute_profile_embeddings(all_new_profiles) # type: ignore[reportOptionalMemberAccess]
|
|
222
|
+
|
|
223
|
+
return ProfileWritePlan(
|
|
224
|
+
user_id=user_id,
|
|
225
|
+
request_id=generation_request_id,
|
|
226
|
+
new_profiles=all_new_profiles,
|
|
227
|
+
superseded_ids=existing_ids_to_delete,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _persist_write_plan(self, plan: ProfileWritePlan) -> None:
|
|
231
|
+
"""Persist-half of profile finalization — apply the resolved write-plan.
|
|
232
|
+
|
|
233
|
+
Issues only the fence-critical row writes: inserts the new profiles
|
|
234
|
+
(``skip_embedding=True`` — embeddings were precomputed in compute) then
|
|
235
|
+
soft-supersedes the dedup'd existing ids. NO LLM / embedding / dedup.
|
|
236
|
+
The soft-supersede emits the lineage events the profile change log is
|
|
237
|
+
reconstructed from (the legacy ``profile_change_logs`` table is no longer
|
|
238
|
+
written — see reconstruct_profile_change_log).
|
|
239
|
+
|
|
240
|
+
On a write failure this **re-raises** (symmetric with playbook
|
|
241
|
+
``_persist_write_plan``): on the durable path the raise rolls back the
|
|
242
|
+
fenced ``commit_scope`` so the rows AND the extractor bookmark advance
|
|
243
|
+
(applied by ``persist_generation`` only if persist returns) are discarded
|
|
244
|
+
together — never a "write failed but bookmark advanced" window. On the
|
|
245
|
+
synchronous ``.run()`` path ``_run_generation`` catches it, records
|
|
246
|
+
``generation_failed``, and leaves the bookmark un-advanced so the next
|
|
247
|
+
publish retries the window.
|
|
248
|
+
"""
|
|
249
|
+
user_id = plan.user_id
|
|
250
|
+
generation_request_id = plan.request_id
|
|
251
|
+
|
|
252
|
+
# Save new profiles (embeddings already set → skip re-embedding).
|
|
253
|
+
if plan.new_profiles:
|
|
188
254
|
try:
|
|
189
|
-
self.storage.add_user_profile(
|
|
255
|
+
self.storage.add_user_profile( # type: ignore[reportOptionalMemberAccess]
|
|
256
|
+
user_id, plan.new_profiles, skip_embedding=True
|
|
257
|
+
)
|
|
190
258
|
except Exception as e:
|
|
191
259
|
with sentry_tags(
|
|
192
260
|
subsystem="profile_generation",
|
|
193
261
|
op="save_profiles",
|
|
194
262
|
org_id=self.org_id,
|
|
195
263
|
user_id=user_id,
|
|
196
|
-
request_id=
|
|
264
|
+
request_id=generation_request_id,
|
|
197
265
|
error_type=type(e).__name__,
|
|
198
266
|
):
|
|
199
267
|
logger.exception(
|
|
200
268
|
"Failed to save profiles for user id: %s",
|
|
201
269
|
user_id,
|
|
202
270
|
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
capture_anomaly(
|
|
215
|
-
"lineage.dedup.missing_request_id",
|
|
216
|
-
level="error",
|
|
217
|
-
org_id=self.org_id,
|
|
271
|
+
# Re-raise so the bookmark advance is skipped / the fence rolls
|
|
272
|
+
# back (F1 symmetry with playbook persist) — never advance the
|
|
273
|
+
# extractor bookmark over a window whose rows failed to write.
|
|
274
|
+
raise
|
|
275
|
+
|
|
276
|
+
# Always soft-supersede superseded existing profiles (never hard-delete
|
|
277
|
+
# on the dedup path). Compute already dropped these when request_id was
|
|
278
|
+
# empty, so any ids here carry a valid lineage key.
|
|
279
|
+
if plan.superseded_ids:
|
|
280
|
+
try:
|
|
281
|
+
self.storage.supersede_profiles_by_ids( # type: ignore[reportOptionalMemberAccess]
|
|
218
282
|
user_id=user_id,
|
|
283
|
+
profile_ids=plan.superseded_ids,
|
|
284
|
+
request_id=generation_request_id,
|
|
219
285
|
)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
286
|
+
except Exception as e:
|
|
287
|
+
with sentry_tags(
|
|
288
|
+
subsystem="profile_generation",
|
|
289
|
+
op="supersede_profiles",
|
|
290
|
+
org_id=self.org_id,
|
|
291
|
+
user_id=user_id,
|
|
292
|
+
request_id=generation_request_id,
|
|
293
|
+
error_type=type(e).__name__,
|
|
294
|
+
):
|
|
295
|
+
logger.exception(
|
|
296
|
+
"Failed to soft-delete superseded profiles for user %s",
|
|
297
|
+
user_id,
|
|
226
298
|
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
299
|
+
# Re-raise for the same reason: a half-applied persist (new rows
|
|
300
|
+
# in, supersede failed) must not advance the bookmark. Playbook's
|
|
301
|
+
# _apply_consolidation_lineage raises here too.
|
|
302
|
+
raise
|
|
303
|
+
|
|
304
|
+
def _finalize_extracted_items(self, all_new_profiles: list[UserProfile]) -> None:
|
|
305
|
+
"""Permanent V3 wrapper: compute-then-persist together (no external fence).
|
|
306
|
+
|
|
307
|
+
Kept for the synchronous resume/manual callers
|
|
308
|
+
(``ExtractionResumeWorker`` calls this directly). Routes them through the
|
|
309
|
+
same ``_resolve_write_plan`` (compute) + ``_persist_write_plan``
|
|
310
|
+
(persist) split the durable worker uses — with no external
|
|
311
|
+
``commit_scope`` — so the result is identical to the pre-split monolith.
|
|
312
|
+
"""
|
|
313
|
+
plan = self._resolve_write_plan([all_new_profiles])
|
|
314
|
+
if plan is not None:
|
|
315
|
+
self._persist_write_plan(plan)
|
|
240
316
|
|
|
241
317
|
def check_and_update_profiles(self, profiles: list[UserProfile]) -> None:
|
|
242
318
|
"""check if the profiles are expired and update them if they are"""
|
|
@@ -436,9 +512,10 @@ class ProfileGenerationService(
|
|
|
436
512
|
"""
|
|
437
513
|
# Handle rerun requests (have start_time/end_time datetime objects)
|
|
438
514
|
if isinstance(request, RerunProfileGenerationRequest):
|
|
515
|
+
operation_request_id = f"rerun_{uuid.uuid4().hex[:8]}"
|
|
439
516
|
return ProfileGenerationRequest(
|
|
440
517
|
user_id=user_id,
|
|
441
|
-
request_id=
|
|
518
|
+
request_id=operation_request_id,
|
|
442
519
|
source=request.source,
|
|
443
520
|
rerun_start_time=(
|
|
444
521
|
int(request.start_time.timestamp()) if request.start_time else None
|
|
@@ -449,9 +526,10 @@ class ProfileGenerationService(
|
|
|
449
526
|
auto_run=False,
|
|
450
527
|
)
|
|
451
528
|
# Handle manual requests (ManualProfileGenerationRequest)
|
|
529
|
+
operation_request_id = f"manual_{uuid.uuid4().hex[:8]}"
|
|
452
530
|
return ProfileGenerationRequest(
|
|
453
531
|
user_id=user_id,
|
|
454
|
-
request_id=
|
|
532
|
+
request_id=operation_request_id,
|
|
455
533
|
source=request.source,
|
|
456
534
|
auto_run=False,
|
|
457
535
|
)
|