claude-smart 0.2.43 → 0.2.44
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 +1 -1
- package/README.md +1 -1
- package/bin/claude-smart.js +2 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +21 -1
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/backend-service.sh +50 -4
- package/plugin/scripts/cli.sh +2 -1
- package/plugin/scripts/ensure-plugin-root.sh +1 -0
- package/plugin/scripts/hook_entry.sh +5 -3
- package/plugin/scripts/smart-install.sh +2 -2
- package/plugin/src/README.md +57 -0
- package/plugin/uv.lock +126 -5
- package/plugin/vendor/reflexio/.env.example +9 -0
- package/plugin/vendor/reflexio/pyproject.toml +5 -2
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +0 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +7 -4
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +4 -4
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +2 -2
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +13 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +3 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +59 -6
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +7 -1
- package/plugin/vendor/reflexio/reflexio/server/api.py +188 -34
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +33 -11
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +278 -29
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +457 -181
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +22 -12
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +137 -9
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +36 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +27 -23
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +19 -9
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +1 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +44 -2
- package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +91 -24
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +18 -5
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +88 -3
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +36 -5
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +32 -22
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +89 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +46 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +8 -4
|
@@ -43,6 +43,12 @@ logger = logging.getLogger(__name__)
|
|
|
43
43
|
FINISH_EXTRACTION_TOOL_NAME = "finish_extraction"
|
|
44
44
|
PROFILE_EXTRACTOR_KIND = "profile"
|
|
45
45
|
|
|
46
|
+
# Max in-process retries when the model returns a plain-text turn with no tool
|
|
47
|
+
# call (finished_reason="no_tool_call"). tool_choice="required" is not always
|
|
48
|
+
# honored by every provider/model, so a single bad turn would otherwise drop the
|
|
49
|
+
# extraction entirely. Retries re-issue the same prompt from scratch.
|
|
50
|
+
_NO_TOOL_CALL_MAX_RETRIES = 2
|
|
51
|
+
|
|
46
52
|
|
|
47
53
|
def _record_agent_usage_event(
|
|
48
54
|
*,
|
|
@@ -351,18 +357,6 @@ class ResumableExtractionAgent:
|
|
|
351
357
|
extra_tool_context: Any | None = None,
|
|
352
358
|
log_label: str | None = None,
|
|
353
359
|
) -> AgentRunResult:
|
|
354
|
-
finish_ctx = _FinishExtractionContext()
|
|
355
|
-
ctx: Any = (
|
|
356
|
-
_ExtractionAgentToolContext(
|
|
357
|
-
finish_context=finish_ctx,
|
|
358
|
-
extra_tool_context=extra_tool_context,
|
|
359
|
-
)
|
|
360
|
-
if extra_tool_context is not None
|
|
361
|
-
else finish_ctx
|
|
362
|
-
)
|
|
363
|
-
registry = ToolRegistry(
|
|
364
|
-
[*(extra_tools or []), create_finish_extraction_tool(output_schema)]
|
|
365
|
-
)
|
|
366
360
|
max_steps = self.max_steps
|
|
367
361
|
if run.max_steps_remaining is not None:
|
|
368
362
|
max_steps = min(max_steps, max(0, run.max_steps_remaining))
|
|
@@ -387,29 +381,101 @@ class ResumableExtractionAgent:
|
|
|
387
381
|
else "required"
|
|
388
382
|
)
|
|
389
383
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
384
|
+
# Retry only the no_tool_call termination: the model emitted plain text
|
|
385
|
+
# with zero tool calls, so nothing was committed and re-issuing the same
|
|
386
|
+
# prompt is safe. Each attempt uses a fresh finish_ctx/registry so no
|
|
387
|
+
# partial state leaks across attempts. We do NOT retry once the model has
|
|
388
|
+
# already registered async tool calls (e.g. attach_pending_info_request)
|
|
389
|
+
# — restarting from scratch would discard them.
|
|
390
|
+
for attempt in range(_NO_TOOL_CALL_MAX_RETRIES + 1):
|
|
391
|
+
finish_ctx = _FinishExtractionContext()
|
|
392
|
+
ctx: Any = (
|
|
393
|
+
_ExtractionAgentToolContext(
|
|
394
|
+
finish_context=finish_ctx,
|
|
395
|
+
extra_tool_context=extra_tool_context,
|
|
396
|
+
)
|
|
397
|
+
if extra_tool_context is not None
|
|
398
|
+
else finish_ctx
|
|
399
|
+
)
|
|
400
|
+
registry = ToolRegistry(
|
|
401
|
+
[*(extra_tools or []), create_finish_extraction_tool(output_schema)]
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
result = run_tool_loop(
|
|
405
|
+
client=self.client,
|
|
406
|
+
messages=messages,
|
|
407
|
+
registry=registry,
|
|
408
|
+
model_role=self.model_role,
|
|
409
|
+
max_steps=max_steps,
|
|
410
|
+
ctx=ctx,
|
|
411
|
+
finish_tool_name=FINISH_EXTRACTION_TOOL_NAME,
|
|
412
|
+
tool_choice=tool_choice,
|
|
413
|
+
log_label=log_label,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
should_retry = (
|
|
417
|
+
result.finished_reason == "no_tool_call"
|
|
418
|
+
and not result.pending_tool_call_ids
|
|
419
|
+
and attempt < _NO_TOOL_CALL_MAX_RETRIES
|
|
420
|
+
)
|
|
421
|
+
if not should_retry:
|
|
422
|
+
break
|
|
423
|
+
logger.warning(
|
|
424
|
+
"event=extraction_agent_retry org_id=%s user_id=%s "
|
|
425
|
+
"extractor_kind=%s run_id=%s request_id=%s "
|
|
426
|
+
"finished_reason=%s attempt=%d max_retries=%d",
|
|
427
|
+
run.binding.org_id,
|
|
428
|
+
run.binding.user_id,
|
|
429
|
+
run.binding.extractor_kind,
|
|
430
|
+
run.id,
|
|
431
|
+
run.binding.request_id,
|
|
432
|
+
result.finished_reason,
|
|
433
|
+
attempt + 1,
|
|
434
|
+
_NO_TOOL_CALL_MAX_RETRIES,
|
|
435
|
+
)
|
|
436
|
+
_record_agent_usage_event(
|
|
437
|
+
run=run,
|
|
438
|
+
event_name="extraction_agent_retry",
|
|
439
|
+
error_kind=result.finished_reason,
|
|
440
|
+
metadata={"attempt": attempt + 1},
|
|
441
|
+
)
|
|
401
442
|
|
|
402
443
|
committed_output = (
|
|
403
444
|
finish_ctx.output.model_dump() if finish_ctx.output is not None else None
|
|
404
445
|
)
|
|
446
|
+
active_statuses = (AgentRunStatus.RUNNING, AgentRunStatus.RESUMING)
|
|
405
447
|
if result.finished_reason == "finish_tool" and committed_output is not None:
|
|
406
|
-
self.storage.update_agent_run_status(
|
|
448
|
+
stored_run = self.storage.update_agent_run_status(
|
|
407
449
|
run.id,
|
|
408
450
|
AgentRunStatus.AGENT_COMPLETED,
|
|
409
451
|
committed_output=committed_output,
|
|
410
452
|
pending_tool_call_ids=result.pending_tool_call_ids,
|
|
411
453
|
max_steps_remaining=result.max_steps_remaining,
|
|
454
|
+
expected_statuses=active_statuses,
|
|
412
455
|
)
|
|
456
|
+
if (
|
|
457
|
+
stored_run is None
|
|
458
|
+
or stored_run.status != AgentRunStatus.AGENT_COMPLETED
|
|
459
|
+
):
|
|
460
|
+
logger.warning(
|
|
461
|
+
"event=extraction_agent_late_output_discarded org_id=%s "
|
|
462
|
+
"user_id=%s extractor_kind=%s run_id=%s request_id=%s "
|
|
463
|
+
"stored_status=%s",
|
|
464
|
+
run.binding.org_id,
|
|
465
|
+
run.binding.user_id,
|
|
466
|
+
run.binding.extractor_kind,
|
|
467
|
+
run.id,
|
|
468
|
+
run.binding.request_id,
|
|
469
|
+
stored_run.status if stored_run is not None else None,
|
|
470
|
+
)
|
|
471
|
+
return AgentRunResult(
|
|
472
|
+
run_id=run.id,
|
|
473
|
+
output=None,
|
|
474
|
+
pending_tool_call_ids=[],
|
|
475
|
+
messages=result.messages,
|
|
476
|
+
trace=result.trace,
|
|
477
|
+
finished_reason="late_output_discarded",
|
|
478
|
+
)
|
|
413
479
|
logger.info(
|
|
414
480
|
"event=extraction_agent_finished org_id=%s user_id=%s "
|
|
415
481
|
"extractor_kind=%s run_id=%s request_id=%s "
|
|
@@ -437,6 +503,7 @@ class ResumableExtractionAgent:
|
|
|
437
503
|
AgentRunStatus.FAILED,
|
|
438
504
|
max_steps_remaining=result.max_steps_remaining,
|
|
439
505
|
last_error=last_error,
|
|
506
|
+
expected_statuses=active_statuses,
|
|
440
507
|
)
|
|
441
508
|
logger.warning(
|
|
442
509
|
"event=extraction_agent_failed org_id=%s user_id=%s "
|
|
@@ -161,7 +161,9 @@ def _select_current_extractor_config(
|
|
|
161
161
|
f"Unsupported extractor kind {run.binding.extractor_kind!r}"
|
|
162
162
|
)
|
|
163
163
|
|
|
164
|
-
if config is not None and isinstance(
|
|
164
|
+
if config is not None and isinstance(
|
|
165
|
+
config, ProfileExtractorConfig | PlaybookConfig
|
|
166
|
+
):
|
|
165
167
|
return config
|
|
166
168
|
raise ResumeWorkerError(
|
|
167
169
|
f"Current extractor config not found for {run.binding.extractor_kind}"
|
|
@@ -34,9 +34,12 @@ def get_extractor_name(config: object) -> str:
|
|
|
34
34
|
return SINGLETON_USER_PLAYBOOK_NAME
|
|
35
35
|
if isinstance(config, AgentSuccessConfig):
|
|
36
36
|
return SINGLETON_AGENT_SUCCESS_EVALUATION_NAME
|
|
37
|
-
return
|
|
38
|
-
config, "
|
|
39
|
-
|
|
37
|
+
return (
|
|
38
|
+
getattr(config, "extractor_name", None)
|
|
39
|
+
or getattr(config, "playbook_name", None)
|
|
40
|
+
or getattr(config, "evaluation_name", "unknown")
|
|
41
|
+
or "unknown"
|
|
42
|
+
)
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
def filter_extractor_configs[TExtractorConfig](
|
|
@@ -23,7 +23,7 @@ def get_extractor_window_params[TExtractorConfig](
|
|
|
23
23
|
Get effective window_size and stride_size for a specific extractor.
|
|
24
24
|
|
|
25
25
|
Uses extractor's override values if set, otherwise falls back to global values,
|
|
26
|
-
then to defaults (
|
|
26
|
+
then to defaults (DEFAULT_WINDOW_SIZE / DEFAULT_STRIDE_SIZE from config_schema).
|
|
27
27
|
|
|
28
28
|
Args:
|
|
29
29
|
extractor_config: Extractor configuration object
|
|
@@ -172,10 +172,20 @@ class GenerationService:
|
|
|
172
172
|
)
|
|
173
173
|
|
|
174
174
|
try:
|
|
175
|
-
|
|
176
|
-
request_id =
|
|
175
|
+
caller_request_id = publish_user_interaction_request.request_id
|
|
176
|
+
request_id = (
|
|
177
|
+
caller_request_id
|
|
178
|
+
if caller_request_id is not None
|
|
179
|
+
else str(uuid.uuid4())
|
|
180
|
+
)
|
|
177
181
|
result.request_id = request_id
|
|
178
182
|
|
|
183
|
+
if (
|
|
184
|
+
caller_request_id is not None
|
|
185
|
+
and self.storage.get_request(request_id) is not None # type: ignore[reportOptionalMemberAccess]
|
|
186
|
+
):
|
|
187
|
+
raise ValueError(f"request_id {request_id!r} already exists")
|
|
188
|
+
|
|
179
189
|
new_interactions: list[Interaction] = (
|
|
180
190
|
GenerationService.get_interaction_from_publish_user_interaction_request(
|
|
181
191
|
publish_user_interaction_request, request_id
|
|
@@ -326,7 +336,8 @@ class GenerationService:
|
|
|
326
336
|
error_type=type(e).__name__,
|
|
327
337
|
):
|
|
328
338
|
logger.exception(
|
|
329
|
-
"Generation service failed for request %s",
|
|
339
|
+
"Generation service failed for request %s",
|
|
340
|
+
request_id,
|
|
330
341
|
)
|
|
331
342
|
result.warnings.append(msg)
|
|
332
343
|
finally:
|
|
@@ -380,7 +391,8 @@ class GenerationService:
|
|
|
380
391
|
error_type=type(e).__name__,
|
|
381
392
|
):
|
|
382
393
|
logger.exception(
|
|
383
|
-
"Failed to refresh user profile for user id: %s",
|
|
394
|
+
"Failed to refresh user profile for user id: %s",
|
|
395
|
+
user_id,
|
|
384
396
|
)
|
|
385
397
|
raise
|
|
386
398
|
|
|
@@ -521,7 +533,8 @@ class GenerationService:
|
|
|
521
533
|
error_type=type(e).__name__,
|
|
522
534
|
):
|
|
523
535
|
logger.exception(
|
|
524
|
-
"Failed to cleanup retention target %s",
|
|
536
|
+
"Failed to cleanup retention target %s",
|
|
537
|
+
target_name,
|
|
525
538
|
)
|
|
526
539
|
finally:
|
|
527
540
|
mgr.release_simple_lock()
|
|
@@ -403,7 +403,7 @@ class OperationStateManager:
|
|
|
403
403
|
request when the lock is held by someone else. Required for the
|
|
404
404
|
rerun loop to operate on the SAME interactions the original
|
|
405
405
|
publish enqueued, not whatever the bookmark currently points at
|
|
406
|
-
(R2
|
|
406
|
+
(R2).
|
|
407
407
|
|
|
408
408
|
Returns:
|
|
409
409
|
bool: True if lock acquired (proceed with generation), False if skipped
|
|
@@ -545,7 +545,7 @@ class OperationStateManager:
|
|
|
545
545
|
FIFO drain — preserves order so blocked publishes are processed in the
|
|
546
546
|
order they arrived. Replaces the older single-slot ``pending_request_id``
|
|
547
547
|
last-wins behaviour that silently dropped earlier blocked publishes
|
|
548
|
-
(R2
|
|
548
|
+
(R2).
|
|
549
549
|
|
|
550
550
|
On a legacy state row (written by a pre-fix server before redeploy)
|
|
551
551
|
the queue is missing but ``pending_request_id`` may be set; we surface
|
|
@@ -33,6 +33,66 @@ logger = logging.getLogger(__name__)
|
|
|
33
33
|
# ===============================
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _coerce_existing_position(value: object) -> int:
|
|
37
|
+
"""Accept either a bare int position or an ``"EXISTING-N"`` label.
|
|
38
|
+
|
|
39
|
+
Wired ONLY to ``UnifyDecision.archive_existing_ids`` — the one field whose
|
|
40
|
+
int contract is genuinely a list **position** (resolved via
|
|
41
|
+
``existing_by_position`` as ``f"EXISTING-{idx}"`` in ``_apply_unify``).
|
|
42
|
+
``RejectNewDecision.superseded_by_existing_id`` and
|
|
43
|
+
``DifferentiateDecision.existing_id`` are DB ``user_playbook_id`` values
|
|
44
|
+
(resolved via ``existing_by_id`` in ``_apply_reject_new`` /
|
|
45
|
+
``_apply_differentiate``) — coercing ``"EXISTING-N"`` to ``N`` on those
|
|
46
|
+
fields would silently misroute decisions when the position int doesn't
|
|
47
|
+
map to a real DB id.
|
|
48
|
+
|
|
49
|
+
The consolidation prompt labels rows as ``[EXISTING-0]``, ``[EXISTING-1]``
|
|
50
|
+
etc. (see ``_format_playbooks_with_prefix``) and ``_apply_unify``
|
|
51
|
+
reconstructs ``f"EXISTING-{position}"`` from the integer the LLM returns.
|
|
52
|
+
Strong structured-output models (GPT-4o, Claude) honor the ``list[int]``
|
|
53
|
+
schema and return the bare integer ``0``; weaker models (e.g. MiniMax-M3)
|
|
54
|
+
ignore the int constraint and return the literal label ``"EXISTING-0"``
|
|
55
|
+
instead — which then fails pydantic validation and the whole
|
|
56
|
+
consolidation batch dies.
|
|
57
|
+
|
|
58
|
+
Strip the prefix when present so the schema tolerates both shapes
|
|
59
|
+
without changing the int contract downstream consumers rely on. Plain
|
|
60
|
+
numeric strings (``"5"``) are also accepted for symmetry with how
|
|
61
|
+
most JSON-coerced models handle ID-like values. Negative values are
|
|
62
|
+
rejected — list positions are always ``>= 0``.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: when ``value`` is not a non-negative int or a recognized
|
|
66
|
+
position-label / numeric string.
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(value, bool):
|
|
69
|
+
# ``bool`` is a subclass of ``int`` in Python; reject explicitly so a
|
|
70
|
+
# stray ``True`` doesn't silently become position 1.
|
|
71
|
+
raise ValueError(f"existing-position must be int, got bool: {value!r}")
|
|
72
|
+
if isinstance(value, int):
|
|
73
|
+
if value < 0:
|
|
74
|
+
raise ValueError(f"existing-position must be >= 0, got {value!r}")
|
|
75
|
+
return value
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
stripped = value.strip()
|
|
78
|
+
for prefix in ("EXISTING-", "EXISTING_", "existing-", "existing_"):
|
|
79
|
+
if stripped.startswith(prefix):
|
|
80
|
+
stripped = stripped[len(prefix) :]
|
|
81
|
+
break
|
|
82
|
+
try:
|
|
83
|
+
parsed = int(stripped)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"existing-position must be int or 'EXISTING-N' label, got {value!r}"
|
|
87
|
+
) from exc
|
|
88
|
+
if parsed < 0:
|
|
89
|
+
raise ValueError(f"existing-position must be >= 0, got {value!r}")
|
|
90
|
+
return parsed
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"existing-position must be int or 'EXISTING-N' label, got {type(value).__name__}: {value!r}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
36
96
|
class UnifyDecision(BaseModel):
|
|
37
97
|
"""Collapse NEW (+ 0..N EXISTING) into one row with LLM-supplied content.
|
|
38
98
|
|
|
@@ -57,11 +117,28 @@ class UnifyDecision(BaseModel):
|
|
|
57
117
|
rationale: str
|
|
58
118
|
reason: str = ""
|
|
59
119
|
|
|
120
|
+
@field_validator("archive_existing_ids", mode="before")
|
|
121
|
+
@classmethod
|
|
122
|
+
def _coerce_archive_ids(cls, value: object) -> object:
|
|
123
|
+
if value is None:
|
|
124
|
+
return []
|
|
125
|
+
if isinstance(value, list):
|
|
126
|
+
return [_coerce_existing_position(item) for item in value]
|
|
127
|
+
return value
|
|
128
|
+
|
|
60
129
|
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
61
130
|
|
|
62
131
|
|
|
63
132
|
class RejectNewDecision(BaseModel):
|
|
64
|
-
"""The new candidate is redundant; an existing row supersedes it (storage no-op).
|
|
133
|
+
"""The new candidate is redundant; an existing row supersedes it (storage no-op).
|
|
134
|
+
|
|
135
|
+
``superseded_by_existing_id`` is a DB ``user_playbook_id`` (resolved
|
|
136
|
+
against ``existing_by_id`` in ``_apply_reject_new``), NOT a list
|
|
137
|
+
position — no ``"EXISTING-N"`` coercion is wired here. If the LLM
|
|
138
|
+
returns ``"EXISTING-N"`` for this field, pydantic rejects it loudly
|
|
139
|
+
rather than silently misrouting the decision (see
|
|
140
|
+
``_coerce_existing_position`` docstring).
|
|
141
|
+
"""
|
|
65
142
|
|
|
66
143
|
kind: Literal["reject_new"] = "reject_new"
|
|
67
144
|
new_id: str
|
|
@@ -72,7 +149,13 @@ class RejectNewDecision(BaseModel):
|
|
|
72
149
|
|
|
73
150
|
|
|
74
151
|
class DifferentiateDecision(BaseModel):
|
|
75
|
-
"""Both rules valid in distinct contexts: refine both triggers.
|
|
152
|
+
"""Both rules valid in distinct contexts: refine both triggers.
|
|
153
|
+
|
|
154
|
+
``existing_id`` is a DB ``user_playbook_id`` (resolved against
|
|
155
|
+
``existing_by_id`` in ``_apply_differentiate``), NOT a list position —
|
|
156
|
+
no ``"EXISTING-N"`` coercion is wired here. See
|
|
157
|
+
``_coerce_existing_position`` docstring.
|
|
158
|
+
"""
|
|
76
159
|
|
|
77
160
|
kind: Literal["differentiate"] = "differentiate"
|
|
78
161
|
new_id: str
|
|
@@ -571,7 +654,9 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
571
654
|
):
|
|
572
655
|
logger.exception(
|
|
573
656
|
"event=consolidation_apply_failed kind=%s new_id=%s existing_id=%s",
|
|
574
|
-
decision.kind,
|
|
657
|
+
decision.kind,
|
|
658
|
+
new_id_str,
|
|
659
|
+
existing_id_str,
|
|
575
660
|
)
|
|
576
661
|
continue
|
|
577
662
|
new_rows.extend(rows)
|
|
@@ -7,12 +7,12 @@ import logging
|
|
|
7
7
|
import os
|
|
8
8
|
import uuid
|
|
9
9
|
from datetime import UTC, datetime
|
|
10
|
+
from typing import Any, cast
|
|
10
11
|
|
|
11
12
|
from pydantic import BaseModel, ConfigDict, Field
|
|
12
13
|
|
|
13
14
|
from reflexio.models.api_schema.retriever_schema import SearchUserProfileRequest
|
|
14
15
|
from reflexio.models.api_schema.service_schemas import Status, UserProfile
|
|
15
|
-
from reflexio.models.config_schema import EMBEDDING_DIMENSIONS
|
|
16
16
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
17
17
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
18
18
|
from reflexio.server.services.deduplication_utils import (
|
|
@@ -20,6 +20,7 @@ from reflexio.server.services.deduplication_utils import (
|
|
|
20
20
|
format_dedup_timestamp,
|
|
21
21
|
parse_item_id,
|
|
22
22
|
)
|
|
23
|
+
from reflexio.server.services.embedding_text import embedding_input
|
|
23
24
|
from reflexio.server.services.profile.profile_generation_service_utils import (
|
|
24
25
|
ProfileTimeToLive,
|
|
25
26
|
calculate_expiration_timestamp,
|
|
@@ -302,11 +303,41 @@ class ProfileDeduplicator(BaseDeduplicator):
|
|
|
302
303
|
if not query_texts:
|
|
303
304
|
return []
|
|
304
305
|
|
|
305
|
-
#
|
|
306
|
+
# Generate embeddings with the request storage backend so dedup search
|
|
307
|
+
# uses the same model/prefix/routing as normal profile search.
|
|
308
|
+
embeddings: list[list[float] | None]
|
|
306
309
|
try:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
+
get_storage_embedding = getattr(storage, "_get_embedding", None)
|
|
311
|
+
if callable(get_storage_embedding):
|
|
312
|
+
logger.info(
|
|
313
|
+
"Profile dedup query embeddings: source=storage model=%s",
|
|
314
|
+
getattr(storage, "embedding_model_name", "unknown"),
|
|
315
|
+
)
|
|
316
|
+
embeddings = [
|
|
317
|
+
cast(
|
|
318
|
+
list[float],
|
|
319
|
+
get_storage_embedding(query_text, purpose="query"),
|
|
320
|
+
)
|
|
321
|
+
for query_text in query_texts
|
|
322
|
+
]
|
|
323
|
+
else:
|
|
324
|
+
storage_with_embeddings = cast(Any, storage)
|
|
325
|
+
embedding_model_name = storage_with_embeddings.embedding_model_name
|
|
326
|
+
embedding_dimensions = storage_with_embeddings.embedding_dimensions
|
|
327
|
+
logger.info(
|
|
328
|
+
"Profile dedup query embeddings: source=llm_client model=%s",
|
|
329
|
+
embedding_model_name,
|
|
330
|
+
)
|
|
331
|
+
embeddings = list(
|
|
332
|
+
self.client.get_embeddings(
|
|
333
|
+
[
|
|
334
|
+
embedding_input(query_text, purpose="query")
|
|
335
|
+
for query_text in query_texts
|
|
336
|
+
],
|
|
337
|
+
model=embedding_model_name,
|
|
338
|
+
dimensions=embedding_dimensions,
|
|
339
|
+
)
|
|
340
|
+
)
|
|
310
341
|
except Exception as e:
|
|
311
342
|
logger.warning("Failed to generate embeddings for dedup search: %s", e)
|
|
312
343
|
embeddings = [None] * len(query_texts)
|
package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py
CHANGED
|
@@ -198,7 +198,8 @@ class ProfileGenerationService(
|
|
|
198
198
|
error_type=type(e).__name__,
|
|
199
199
|
):
|
|
200
200
|
logger.exception(
|
|
201
|
-
"Failed to save profiles for user id: %s",
|
|
201
|
+
"Failed to save profiles for user id: %s",
|
|
202
|
+
user_id,
|
|
202
203
|
)
|
|
203
204
|
return
|
|
204
205
|
|
|
@@ -224,7 +225,8 @@ class ProfileGenerationService(
|
|
|
224
225
|
):
|
|
225
226
|
logger.exception(
|
|
226
227
|
"Failed to delete superseded profile %s for user %s",
|
|
227
|
-
profile_id,
|
|
228
|
+
profile_id,
|
|
229
|
+
user_id,
|
|
228
230
|
)
|
|
229
231
|
|
|
230
232
|
# Create profile changelog post-deduplication
|
|
@@ -250,7 +252,8 @@ class ProfileGenerationService(
|
|
|
250
252
|
error_type=type(e).__name__,
|
|
251
253
|
):
|
|
252
254
|
logger.exception(
|
|
253
|
-
"Failed to add profile change log for user %s",
|
|
255
|
+
"Failed to add profile change log for user %s",
|
|
256
|
+
user_id,
|
|
254
257
|
)
|
|
255
258
|
|
|
256
259
|
def check_and_update_profiles(self, profiles: list[UserProfile]) -> None:
|
|
@@ -253,7 +253,8 @@ class ReflectionService:
|
|
|
253
253
|
):
|
|
254
254
|
logger.exception(
|
|
255
255
|
"event=reflection_apply_failed kind=%s target_id=%s",
|
|
256
|
-
decision.target_kind,
|
|
256
|
+
decision.target_kind,
|
|
257
|
+
decision.target_id,
|
|
257
258
|
)
|
|
258
259
|
continue
|
|
259
260
|
if applied:
|
|
@@ -498,7 +499,8 @@ class ReflectionService:
|
|
|
498
499
|
logger.exception(
|
|
499
500
|
"event=reflection_archive_after_insert_failed kind=profile "
|
|
500
501
|
"cited_id=%s new_id=%s",
|
|
501
|
-
cited.profile_id,
|
|
502
|
+
cited.profile_id,
|
|
503
|
+
new_profile.profile_id,
|
|
502
504
|
)
|
|
503
505
|
return True
|
|
504
506
|
if not archived:
|
|
@@ -597,7 +599,8 @@ class ReflectionService:
|
|
|
597
599
|
logger.exception(
|
|
598
600
|
"event=reflection_archive_after_insert_failed kind=playbook "
|
|
599
601
|
"cited_id=%s new_id=%s",
|
|
600
|
-
cited.user_playbook_id,
|
|
602
|
+
cited.user_playbook_id,
|
|
603
|
+
new_playbook.user_playbook_id,
|
|
601
604
|
)
|
|
602
605
|
return True
|
|
603
606
|
if not archived:
|
|
@@ -15,6 +15,7 @@ from reflexio.server.llm.rerank import score_pairs
|
|
|
15
15
|
from reflexio.server.llm.rerank.cross_encoder_reranker import (
|
|
16
16
|
CrossEncoderUnavailableError,
|
|
17
17
|
)
|
|
18
|
+
from reflexio.server.tracing import profile_step
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
@@ -44,27 +45,36 @@ def apply_relevance_floor[T](
|
|
|
44
45
|
"""
|
|
45
46
|
if not items:
|
|
46
47
|
return []
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
with profile_step(
|
|
49
|
+
"search.rerank.relevance_floor",
|
|
50
|
+
arm=arm,
|
|
51
|
+
items=len(items),
|
|
52
|
+
) as span:
|
|
53
|
+
try:
|
|
54
|
+
scores = score_pairs(query, [content_of(item) for item in items])
|
|
55
|
+
except CrossEncoderUnavailableError:
|
|
56
|
+
span.set_data("available", False)
|
|
57
|
+
logger.warning(
|
|
58
|
+
"event=relevance_floor_unavailable arm=%s items=%d (returning unfiltered top_k)",
|
|
59
|
+
arm,
|
|
60
|
+
len(items),
|
|
61
|
+
)
|
|
62
|
+
return items[:top_k]
|
|
63
|
+
span.set_data("available", True)
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
survivors = [item for item, score in ranked if score >= floor]
|
|
61
|
-
dropped = len(ranked) - len(survivors)
|
|
62
|
-
if dropped:
|
|
63
|
-
logger.info(
|
|
64
|
-
"event=relevance_floor arm=%s kept=%d dropped=%d floor=%.2f",
|
|
65
|
-
arm,
|
|
66
|
-
len(survivors),
|
|
67
|
-
dropped,
|
|
68
|
-
floor,
|
|
65
|
+
ranked = sorted(
|
|
66
|
+
zip(items, scores, strict=True), key=lambda pair: pair[1], reverse=True
|
|
69
67
|
)
|
|
70
|
-
|
|
68
|
+
survivors = [item for item, score in ranked if score >= floor]
|
|
69
|
+
dropped = len(ranked) - len(survivors)
|
|
70
|
+
span.set_data("kept", len(survivors))
|
|
71
|
+
span.set_data("dropped", dropped)
|
|
72
|
+
if dropped:
|
|
73
|
+
logger.info(
|
|
74
|
+
"event=relevance_floor arm=%s kept=%d dropped=%d floor=%.2f",
|
|
75
|
+
arm,
|
|
76
|
+
len(survivors),
|
|
77
|
+
dropped,
|
|
78
|
+
floor,
|
|
79
|
+
)
|
|
80
|
+
return survivors[:top_k]
|