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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Write-plan dataclasses for the durable-learning compute/persist split (gate b).
|
|
2
|
+
|
|
3
|
+
Compute resolves these in-memory plans issuing **no** learning DB write; persist
|
|
4
|
+
applies them inside one short fenced ``commit_scope``. This module starts with the
|
|
5
|
+
extractor stride-bookmark advance and grows additional write-plan dataclasses in the
|
|
6
|
+
later gate-(b) tasks (profile/playbook/reflection/generation/deferred plans).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from reflexio.models.api_schema.domain.entities import (
|
|
16
|
+
UserPlaybook,
|
|
17
|
+
UserProfile,
|
|
18
|
+
)
|
|
19
|
+
from reflexio.models.api_schema.service_schemas import Interaction
|
|
20
|
+
from reflexio.server.llm.token_accounting import RunTokenTotals
|
|
21
|
+
from reflexio.server.services.base_generation_service import (
|
|
22
|
+
BaseGenerationService,
|
|
23
|
+
PreparedGenerationRun,
|
|
24
|
+
)
|
|
25
|
+
from reflexio.server.services.reflection.reflection_service_utils import (
|
|
26
|
+
ReflectionDecision,
|
|
27
|
+
ReflectionResult,
|
|
28
|
+
ReflectionServiceRequest,
|
|
29
|
+
)
|
|
30
|
+
from reflexio.server.services.reflection.service import ReflectionService
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ExtractorBookmarkAdvance:
|
|
35
|
+
"""The extractor stride-bookmark advance, deferred out of the extractor (F1).
|
|
36
|
+
|
|
37
|
+
The extractor no longer self-advances its bookmark inside ``run()`` (a DB
|
|
38
|
+
write). It emits this on the ``ExtractionOutcome`` instead, so the advance is
|
|
39
|
+
applied later — inside the persist fence on the durable path, or right after
|
|
40
|
+
result-processing on the synchronous ``.run()`` path — keeping the bookmark
|
|
41
|
+
advance atomic with the row writes it corresponds to.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
extractor_name: str
|
|
45
|
+
processed_interactions: list[Interaction]
|
|
46
|
+
# str | None (not str): the playbook extractor runs org-level with
|
|
47
|
+
# user_id=None (PlaybookGenerationServiceConfig.user_id is Optional), which
|
|
48
|
+
# maps to an unscoped bookmark key. update_extractor_bookmark accepts None.
|
|
49
|
+
user_id: str | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ProfileWritePlan:
|
|
54
|
+
"""Resolved profile write-plan for the compute/persist split (gate b).
|
|
55
|
+
|
|
56
|
+
``ProfileGenerationService._resolve_write_plan`` produces this in compute
|
|
57
|
+
(dedup 2nd-LLM call + existing-row reads + source/status assignment +
|
|
58
|
+
precomputed ``.embedding`` via ``precompute_profile_embeddings``), issuing
|
|
59
|
+
**no** learning DB write. ``_persist_write_plan`` applies it inside the
|
|
60
|
+
fence: ``add_user_profile(..., skip_embedding=True)`` (embeddings already
|
|
61
|
+
set) then ``supersede_profiles_by_ids``.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
user_id: Owning user id for the row writes.
|
|
65
|
+
request_id: Generation request id — the lineage key
|
|
66
|
+
``supersede_profiles_by_ids`` records. Compute drops any supersede
|
|
67
|
+
ids when this is empty (unreconstructable), so persist only ever
|
|
68
|
+
supersedes with a non-empty request_id.
|
|
69
|
+
new_profiles: New profile rows with ``source``/``status`` set and
|
|
70
|
+
``.embedding`` precomputed in compute.
|
|
71
|
+
superseded_ids: Existing profile ids to soft-supersede on the dedup
|
|
72
|
+
path.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
user_id: str
|
|
76
|
+
request_id: str
|
|
77
|
+
new_profiles: list[UserProfile]
|
|
78
|
+
superseded_ids: list[str]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class PlaybookWritePlan:
|
|
83
|
+
"""Resolved playbook write-plan for the compute/persist split (gate b).
|
|
84
|
+
|
|
85
|
+
``PlaybookGenerationService._resolve_write_plan`` produces this in compute
|
|
86
|
+
(``dedupe_and_drop_empty`` + the deduplicator's 2nd-LLM call + existing-row
|
|
87
|
+
reads + source/status assignment + precomputed ``.embedding`` via
|
|
88
|
+
``precompute_user_playbook_embeddings``), issuing **no** learning DB write.
|
|
89
|
+
``_persist_write_plan`` applies it inside the fence:
|
|
90
|
+
``save_user_playbooks(..., skip_embedding=True)`` (which assigns survivor
|
|
91
|
+
ids) then ``_apply_consolidation_lineage`` (which MUST see those ids, so it
|
|
92
|
+
runs AFTER the save).
|
|
93
|
+
|
|
94
|
+
The off-thread schedulers (``_enqueue_user_playbook_optimization`` +
|
|
95
|
+
``_trigger_playbook_aggregation``) are NOT part of persist — they fire
|
|
96
|
+
post-commit in ``emit_generation_side_effects`` (durable / ``.run()`` path)
|
|
97
|
+
or right after persist in the permanent ``_finalize_extracted_items``
|
|
98
|
+
wrapper (synchronous resume/manual path). ``output_pending_status`` /
|
|
99
|
+
``skip_aggregation`` are snapshotted here so the scheduler dispatch reads the
|
|
100
|
+
plan rather than the reused service instance.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
request_id: Generation request id — the lineage key
|
|
104
|
+
``_apply_consolidation_lineage`` records on merges/supersedes.
|
|
105
|
+
output_pending_status: Whether the run emits PENDING rows (rerun mode);
|
|
106
|
+
when True the aggregation trigger is suppressed.
|
|
107
|
+
skip_aggregation: Whether aggregation is skipped (extract-only); when
|
|
108
|
+
True the aggregation trigger is suppressed.
|
|
109
|
+
new_playbooks: New playbook rows with ``source``/``status`` set and
|
|
110
|
+
``.embedding`` precomputed in compute. Survivor ids are assigned by
|
|
111
|
+
``save_user_playbooks`` in persist, before lineage reads them.
|
|
112
|
+
superseded_ids: ALL archived existing ids (merge sources + leftovers)
|
|
113
|
+
routed through ``_apply_consolidation_lineage``.
|
|
114
|
+
merge_groups: ``(survivor_index_into_new_playbooks, source_existing_ids)``
|
|
115
|
+
per dedup merge group.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
request_id: str
|
|
119
|
+
output_pending_status: bool
|
|
120
|
+
skip_aggregation: bool
|
|
121
|
+
new_playbooks: list[UserPlaybook]
|
|
122
|
+
superseded_ids: list[int]
|
|
123
|
+
merge_groups: list[tuple[int, list[int]]]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class ReflectionWritePlan:
|
|
128
|
+
"""Resolved reflection write-plan for the compute/persist split (F5, V2).
|
|
129
|
+
|
|
130
|
+
``ReflectionService.compute`` produces this issuing **no** learning DB write;
|
|
131
|
+
``persist`` applies the whole cap/validate/CAS apply loop verbatim inside the
|
|
132
|
+
fence, then advances the reflection stride-bookmark; ``emit_side_effects``
|
|
133
|
+
fires the post-commit billing + aggregation triggers. The apply loop stays a
|
|
134
|
+
single monolith (no per-revision ``ResolvedReflectionRevision`` dataclass).
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
request: The originating reflection request (user_id / request_id /
|
|
138
|
+
agent_version).
|
|
139
|
+
result: The reflection result. Compute seeds it with the gate/citation
|
|
140
|
+
counters resolved so far; persist's apply loop mutates it in place
|
|
141
|
+
(revised/failed/skipped/... counters) as it applies each decision.
|
|
142
|
+
decisions: ``ReflectionOutput.decisions`` to apply — ``[]`` on the four
|
|
143
|
+
early bookmark-advance paths (nothing to apply, only advance).
|
|
144
|
+
profiles_by_id: Cited profile rows resolved in compute, keyed by
|
|
145
|
+
``profile_id`` (the apply loop's ``_apply_revision`` input).
|
|
146
|
+
playbooks_by_id: Cited playbook rows resolved in compute, keyed by
|
|
147
|
+
``user_playbook_id``.
|
|
148
|
+
max_revisions_per_pass: Per-pass revision cap applied inside the loop.
|
|
149
|
+
bookmark_interactions: The window the reflection stride-bookmark advances
|
|
150
|
+
over in persist (``[]`` on the empty-window advance path).
|
|
151
|
+
advance_bookmark: ``True`` on all five bookmark-advance paths (the four
|
|
152
|
+
early-advance returns + the normal end). Persist advances the
|
|
153
|
+
reflection bookmark iff this is ``True``.
|
|
154
|
+
record_learnings: ``True`` only on the normal-end path — gates the
|
|
155
|
+
post-commit ``_record_learnings_generated`` billing event in emit.
|
|
156
|
+
replacement_profiles: Replacement ``UserProfile`` rows whose embeddings
|
|
157
|
+
were precomputed in compute (V2), keyed by the **cited** profile_id
|
|
158
|
+
they replace. Persist inserts these with ``skip_embedding=True`` so
|
|
159
|
+
no embedding runs inside the fence. Empty on non-compute (direct)
|
|
160
|
+
``_replace_*`` calls, which then build+embed the row themselves.
|
|
161
|
+
replacement_playbooks: Same, keyed by the cited ``user_playbook_id``.
|
|
162
|
+
aggregation_successor_ids: Playbook successor ids collected during the
|
|
163
|
+
persist apply loop; their aggregation trigger is dispatched in
|
|
164
|
+
``emit_side_effects`` (post-commit) so it never fires on a
|
|
165
|
+
rolled-back job.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
request: ReflectionServiceRequest
|
|
169
|
+
result: ReflectionResult
|
|
170
|
+
decisions: list[ReflectionDecision]
|
|
171
|
+
profiles_by_id: dict[str, UserProfile]
|
|
172
|
+
playbooks_by_id: dict[int, UserPlaybook]
|
|
173
|
+
max_revisions_per_pass: int
|
|
174
|
+
bookmark_interactions: list[Interaction]
|
|
175
|
+
advance_bookmark: bool
|
|
176
|
+
record_learnings: bool
|
|
177
|
+
replacement_profiles: dict[str, UserProfile] = field(default_factory=dict)
|
|
178
|
+
replacement_playbooks: dict[int, UserPlaybook] = field(default_factory=dict)
|
|
179
|
+
aggregation_successor_ids: list[int] = field(default_factory=list)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class GenerationComputePlan:
|
|
184
|
+
"""Resolved compute output of one ``BaseGenerationService`` run (gate b).
|
|
185
|
+
|
|
186
|
+
``compute_generation`` runs the prepare gate + extractor + dedup/embedding
|
|
187
|
+
resolution (``_resolve_write_plan``) and drives the ``agent_run`` rows to
|
|
188
|
+
their terminal state (``_finalize_extraction_runs`` — agent_run only, §4.3),
|
|
189
|
+
issuing **no** learning DB write. ``persist_generation`` applies
|
|
190
|
+
``write_plan`` + the extractor bookmark advance inside the fence;
|
|
191
|
+
``emit_generation_side_effects`` fires the post-commit telemetry + billing.
|
|
192
|
+
|
|
193
|
+
The billing inputs (``extraction_run_ids`` / ``token_totals`` /
|
|
194
|
+
``generated_count`` / ``prepared``) are **snapshotted at compute time** so
|
|
195
|
+
the fence-crossing emit reads this plan rather than the reused service
|
|
196
|
+
instance's mutable ``_last_*`` accumulators (purity contract, plan §File
|
|
197
|
+
Structure). See ``emit_generation_side_effects`` for the single-use-instance
|
|
198
|
+
invariant that also keeps the money helper's ``self._last_*`` reads safe.
|
|
199
|
+
|
|
200
|
+
Attributes:
|
|
201
|
+
prepared: The prepared generation run (identifier / extractor_name /
|
|
202
|
+
extractor_config), reused by emit for telemetry + billing input.
|
|
203
|
+
generated_count: Learnings produced by this extraction run.
|
|
204
|
+
write_plan: The resolved write-plan (``ProfileWritePlan`` /
|
|
205
|
+
``PlaybookWritePlan`` in Tasks 6-7, a ``_LegacyItems`` shim marker
|
|
206
|
+
until then) or ``None`` when the extractor produced nothing.
|
|
207
|
+
bookmark_advance: The deferred extractor stride-bookmark advance (F1),
|
|
208
|
+
applied inside the persist scope. ``None`` when the extractor
|
|
209
|
+
produced no output or the service has no stride bookmark.
|
|
210
|
+
generation_start: ``perf_counter`` captured at compute start; emit reads
|
|
211
|
+
it for the ``generation_succeeded`` ``duration_ms`` (parity).
|
|
212
|
+
extraction_run_ids: Snapshot of the run's ``agent_run`` ids.
|
|
213
|
+
token_totals: Snapshot of the run's LLM token totals (billing cost
|
|
214
|
+
facet), or ``None`` when the extractor reported none.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
prepared: PreparedGenerationRun[Any]
|
|
218
|
+
generated_count: int
|
|
219
|
+
write_plan: Any
|
|
220
|
+
bookmark_advance: ExtractorBookmarkAdvance | None
|
|
221
|
+
generation_start: float
|
|
222
|
+
extraction_run_ids: list[str]
|
|
223
|
+
token_totals: RunTokenTotals | None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class DeferredLearningPlan:
|
|
228
|
+
"""One durable-learning job's resolved compute output (gate b, Task 8).
|
|
229
|
+
|
|
230
|
+
``GenerationService.compute_deferred_learning`` acquires the same-user guard
|
|
231
|
+
(F4), then runs reflection + profile + playbook compute holding **no**
|
|
232
|
+
``commit_scope``, and assembles this plan. It carries each present half as a
|
|
233
|
+
``(held_service_instance, its_compute_plan)`` pair so
|
|
234
|
+
``persist_deferred_learning`` can apply the fence-critical writes and
|
|
235
|
+
``emit_deferred_learning_side_effects`` can fire the post-commit telemetry on
|
|
236
|
+
the very instances that produced the plans (the single-use-instance invariant
|
|
237
|
+
that keeps the money helpers' ``self._last_*`` reads valid — see
|
|
238
|
+
``BaseGenerationService.emit_generation_side_effects``).
|
|
239
|
+
|
|
240
|
+
Attributes:
|
|
241
|
+
request_id: The job's generation request id (lineage key).
|
|
242
|
+
user_id: The user the job learns for; also the per-user F4 lock scope.
|
|
243
|
+
agent_version: Resolved agent version — carried so the post-commit
|
|
244
|
+
``schedule_tagging`` dispatch in emit has it (the plan is all emit
|
|
245
|
+
receives).
|
|
246
|
+
lock_acquired: ``False`` when the F4 same-user guard denied this job
|
|
247
|
+
(another durable job for the same user is mid-flight). On ``False``
|
|
248
|
+
the three plan halves are ``None`` and NO LLM/compute ran — the
|
|
249
|
+
worker (Task 9) must leave the job reclaimable and must NOT
|
|
250
|
+
``complete_learning_job`` it.
|
|
251
|
+
reflection: ``(ReflectionService, ReflectionWritePlan)`` when reflection
|
|
252
|
+
resolved something to persist, else ``None`` (gate closed / disabled
|
|
253
|
+
/ compute error captured into ``warnings``).
|
|
254
|
+
profile: ``(ProfileGenerationService, GenerationComputePlan)`` when the
|
|
255
|
+
profile extractor produced a plan, else ``None``.
|
|
256
|
+
playbook: ``(PlaybookGenerationService, GenerationComputePlan)`` when the
|
|
257
|
+
playbook extractor produced a plan, else ``None``.
|
|
258
|
+
warnings: Best-effort per-half compute failures (mirrors
|
|
259
|
+
``GenerationServiceResult.warnings``) — a failed half is dropped from
|
|
260
|
+
the plan, the others still persist.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
request_id: str
|
|
264
|
+
user_id: str
|
|
265
|
+
agent_version: str
|
|
266
|
+
lock_acquired: bool
|
|
267
|
+
reflection: tuple[ReflectionService, ReflectionWritePlan] | None
|
|
268
|
+
profile: tuple[BaseGenerationService, GenerationComputePlan] | None
|
|
269
|
+
playbook: tuple[BaseGenerationService, GenerationComputePlan] | None
|
|
270
|
+
warnings: list[str] = field(default_factory=list)
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
"""DurableLearningWorker: claim jobs from the durable queue,
|
|
2
|
-
|
|
1
|
+
"""DurableLearningWorker: claim jobs from the durable queue, split each into
|
|
2
|
+
compute (no writer transaction) -> persist (short fenced commit_scope) ->
|
|
3
|
+
post-commit side-effects so exactly one of N racing workers commits its outputs.
|
|
3
4
|
|
|
4
|
-
Design constraints (
|
|
5
|
-
- LLM
|
|
5
|
+
Design constraints (gate b — do not remove):
|
|
6
|
+
- LLM extraction + dedup + embeddings run in compute_deferred_learning, OUTSIDE
|
|
7
|
+
the commit_scope. Only the fence-critical writes (profile/playbook rows +
|
|
8
|
+
bookmark advances + complete_learning_job) run inside the scope; billing /
|
|
9
|
+
telemetry / tagging / off-thread schedulers + the per-user lock release run
|
|
10
|
+
AFTER the scope commits (emit_deferred_learning_side_effects). Holding the
|
|
11
|
+
writer transaction across the ~30-120s LLM window is exactly what this split
|
|
12
|
+
removes.
|
|
6
13
|
- No heartbeat thread (deferred; would deadlock a SQLite scope anyway).
|
|
7
14
|
- Exactly-once is guaranteed by the fenced complete_learning_job:
|
|
8
15
|
rowcount == 0 -> lease was stolen -> _SupersededError -> rollback.
|
|
@@ -27,8 +34,9 @@ class _SupersededError(Exception):
|
|
|
27
34
|
"""Raised inside commit_scope when complete_learning_job returns 0.
|
|
28
35
|
|
|
29
36
|
Propagating this exception causes commit_scope to roll back all writes
|
|
30
|
-
(profiles, playbooks) made by
|
|
31
|
-
worker — guaranteeing exactly-once side effects.
|
|
37
|
+
(profiles, playbooks, bookmark advances) made by persist_deferred_learning
|
|
38
|
+
for the superseded worker — guaranteeing exactly-once side effects. The
|
|
39
|
+
post-commit emit is skipped, so a superseded job phantom-bills nothing.
|
|
32
40
|
"""
|
|
33
41
|
|
|
34
42
|
def __init__(self, job_id: str) -> None:
|
|
@@ -142,28 +150,59 @@ class DurableLearningWorker:
|
|
|
142
150
|
)
|
|
143
151
|
return False
|
|
144
152
|
|
|
153
|
+
gen: GenerationService | None = None
|
|
145
154
|
try:
|
|
146
155
|
reflexio = get_reflexio(
|
|
147
156
|
org_id=ctx.org_id, storage_base_dir=ctx.storage_base_dir
|
|
148
157
|
)
|
|
149
158
|
gen = GenerationService(llm_client=reflexio.llm_client, request_context=ctx)
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
# COMPUTE — LLM extraction + dedup + embeddings, NO writer transaction
|
|
161
|
+
# held. Acquires the per-user F4 lock; issues no learning DB write.
|
|
162
|
+
plan = gen.compute_deferred_learning(
|
|
163
|
+
user_id=job.user_id,
|
|
164
|
+
request_id=job.latest_request_id,
|
|
165
|
+
session_id=request.session_id,
|
|
166
|
+
source=request.source,
|
|
167
|
+
agent_version=request.agent_version,
|
|
168
|
+
force_extraction=job.force_extraction,
|
|
169
|
+
skip_aggregation=job.skip_aggregation,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Same-user contention (F4): another same-user durable job holds the
|
|
173
|
+
# per-user lock. Leave THIS job reclaimable (dead=False) — do NOT
|
|
174
|
+
# complete it — so the queue re-claims it once the holder finishes.
|
|
175
|
+
# REFUND the attempt (refund_attempt=True): claim_learning_jobs did
|
|
176
|
+
# attempts += 1 on this claim, but no real work ran, and the ~2s poll
|
|
177
|
+
# would otherwise re-claim (and re-increment) every couple of seconds
|
|
178
|
+
# while the holder is in its ~60s compute — inflating attempts past
|
|
179
|
+
# max_attempts in seconds so the eventual winner dead-letters on its
|
|
180
|
+
# first transient error with zero real retries. The refund nets each
|
|
181
|
+
# contention cycle (claim +1, release -1) to zero. No lock to release
|
|
182
|
+
# (compute never acquired it).
|
|
183
|
+
if not plan.lock_acquired:
|
|
184
|
+
storage.fail_learning_job(
|
|
185
|
+
job_id=job.job_id,
|
|
186
|
+
claim_token=claim_token,
|
|
187
|
+
dead=False,
|
|
188
|
+
refund_attempt=True,
|
|
161
189
|
)
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
# PERSIST — short fenced scope: fence-critical writes + bookmark
|
|
193
|
+
# advances only, then the claim-token fence. rows == 0 -> lease
|
|
194
|
+
# stolen -> _SupersededError -> the scope rolls persist back.
|
|
195
|
+
with storage.commit_scope():
|
|
196
|
+
gen.persist_deferred_learning(plan)
|
|
162
197
|
rows = storage.complete_learning_job(
|
|
163
198
|
job_id=job.job_id, claim_token=claim_token
|
|
164
199
|
)
|
|
165
200
|
if rows == 0:
|
|
166
201
|
raise _SupersededError(job.job_id)
|
|
202
|
+
|
|
203
|
+
# POST-COMMIT — billing / telemetry / tagging / off-thread schedulers
|
|
204
|
+
# + the per-user lock release, only for the winning worker.
|
|
205
|
+
gen.emit_deferred_learning_side_effects(plan)
|
|
167
206
|
logger.info(
|
|
168
207
|
"event=learning_job_done job_id=%s org_id=%s user_id=%s",
|
|
169
208
|
job.job_id,
|
|
@@ -177,6 +216,9 @@ class DurableLearningWorker:
|
|
|
177
216
|
job.job_id,
|
|
178
217
|
job.org_id,
|
|
179
218
|
)
|
|
219
|
+
# The persist rolled back and emit never ran, so the per-user lock is
|
|
220
|
+
# still held by this compute — release it so the reclaim isn't blocked.
|
|
221
|
+
self._release_user_lock(gen, job)
|
|
180
222
|
return False
|
|
181
223
|
except Exception:
|
|
182
224
|
logger.exception(
|
|
@@ -185,9 +227,36 @@ class DurableLearningWorker:
|
|
|
185
227
|
job.org_id,
|
|
186
228
|
job.user_id,
|
|
187
229
|
)
|
|
230
|
+
# emit (which releases the lock) never ran on this path — release the
|
|
231
|
+
# per-user lock so a failed job doesn't strand it.
|
|
232
|
+
self._release_user_lock(gen, job)
|
|
188
233
|
# attempts was incremented by claim_learning_jobs; go dead when we've
|
|
189
234
|
# exhausted max_attempts total claim attempts.
|
|
190
235
|
storage.fail_learning_job(
|
|
191
236
|
job_id=job.job_id, claim_token=claim_token, dead=dead
|
|
192
237
|
)
|
|
193
238
|
return False
|
|
239
|
+
|
|
240
|
+
def _release_user_lock(
|
|
241
|
+
self, gen: GenerationService | None, job: LearningJob
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Best-effort release of the per-user F4 lock after a rolled-back/failed
|
|
244
|
+
job, so a job whose emit (the normal release site) never ran does not
|
|
245
|
+
strand the lock and block the same user's re-claim.
|
|
246
|
+
|
|
247
|
+
Safe to call even when compute never acquired the lock: the release is a
|
|
248
|
+
CAS on the holder (``clear_in_progress_lock_if_owner``) — a no-op unless
|
|
249
|
+
this compute's ``request_id`` still owns it. Never raises.
|
|
250
|
+
"""
|
|
251
|
+
if gen is None or job.latest_request_id is None:
|
|
252
|
+
return
|
|
253
|
+
try:
|
|
254
|
+
gen._release_durable_learning_lock(
|
|
255
|
+
user_id=job.user_id, request_id=job.latest_request_id
|
|
256
|
+
)
|
|
257
|
+
except Exception:
|
|
258
|
+
logger.exception(
|
|
259
|
+
"event=learning_job_lock_release_failed job_id=%s org_id=%s",
|
|
260
|
+
job.job_id,
|
|
261
|
+
job.org_id,
|
|
262
|
+
)
|
package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py
CHANGED
|
@@ -11,7 +11,6 @@ from enum import StrEnum
|
|
|
11
11
|
|
|
12
12
|
_FULL_MIN_DAYS = 14
|
|
13
13
|
_FULL_MIN_SHADOW_N = 500
|
|
14
|
-
_SHADOW_OFF_MIN_DAYS = 7
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class HeroState(StrEnum):
|
|
@@ -50,7 +49,7 @@ def compute_hero_state(
|
|
|
50
49
|
migration pair in supabase/data/supabase/migrations/. The
|
|
51
50
|
per-turn shadow grade in F1 lives on a different surface.)
|
|
52
51
|
total_results (int): Total AgentSuccessEvaluationResult rows in the
|
|
53
|
-
trend window
|
|
52
|
+
trend window, used to distinguish EMPTY from states with data.
|
|
54
53
|
|
|
55
54
|
Returns:
|
|
56
55
|
HeroState: The single applicable state. Empty wins over everything;
|
|
@@ -59,15 +58,7 @@ def compute_hero_state(
|
|
|
59
58
|
if total_results == 0:
|
|
60
59
|
return HeroState.EMPTY
|
|
61
60
|
if not shadow_enabled:
|
|
62
|
-
|
|
63
|
-
days_since_first_eval is not None
|
|
64
|
-
and days_since_first_eval >= _SHADOW_OFF_MIN_DAYS
|
|
65
|
-
):
|
|
66
|
-
return HeroState.SHADOW_OFF
|
|
67
|
-
# <7 days since first eval AND shadow off → still onboarding;
|
|
68
|
-
# render as EMPTY so the frontend shows onboarding rather than a
|
|
69
|
-
# partly-formed trend.
|
|
70
|
-
return HeroState.EMPTY
|
|
61
|
+
return HeroState.SHADOW_OFF
|
|
71
62
|
if days_since_first_eval is None or days_since_first_eval < _FULL_MIN_DAYS:
|
|
72
63
|
return HeroState.EARLY
|
|
73
64
|
if n_shadow_in_window < _FULL_MIN_SHADOW_N:
|
|
@@ -10,7 +10,11 @@ from __future__ import annotations
|
|
|
10
10
|
from collections.abc import Mapping
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
from reflexio.models.api_schema.domain import CitationKind
|
|
14
|
+
|
|
15
|
+
CitationKey = tuple[
|
|
16
|
+
CitationKind, str
|
|
17
|
+
] # (kind, real_id) — matches PlaybookApplicationStat
|
|
14
18
|
SessionIdentity = tuple[str, str] # (user_id, session_id)
|
|
15
19
|
|
|
16
20
|
|
|
@@ -19,7 +23,7 @@ class RuleAttribution:
|
|
|
19
23
|
"""One row of the "rules that moved the needle" panel."""
|
|
20
24
|
|
|
21
25
|
rule_id: str
|
|
22
|
-
kind:
|
|
26
|
+
kind: CitationKind
|
|
23
27
|
title: str
|
|
24
28
|
successes_with: int
|
|
25
29
|
failures_with: int
|
|
@@ -47,6 +47,7 @@ from reflexio.server.services.evaluation_overview.components.hero_state import (
|
|
|
47
47
|
compute_hero_state,
|
|
48
48
|
)
|
|
49
49
|
from reflexio.server.services.evaluation_overview.components.rule_attribution import (
|
|
50
|
+
CitationKey,
|
|
50
51
|
compute_net_sessions,
|
|
51
52
|
)
|
|
52
53
|
from reflexio.server.services.evaluation_overview.components.shadow_aggregation import (
|
|
@@ -80,14 +81,14 @@ class EvaluationOverviewService:
|
|
|
80
81
|
Time windows used here:
|
|
81
82
|
- ``[request.from_ts, request.to_ts]`` is the *trend* window. The hero
|
|
82
83
|
chart, rule attribution, and the rule-attribution session set are
|
|
83
|
-
all computed over it. The frontend
|
|
84
|
-
|
|
84
|
+
all computed over it. The frontend defaults to a 7-day range but
|
|
85
|
+
the user can widen it via the date pickers.
|
|
85
86
|
- The *tile baseline* and *distribution baseline* are tighter:
|
|
86
87
|
``last_7d`` (≤ to_ts) vs ``prior_7d`` (the 7d before that).
|
|
87
|
-
Tying these to ``request.from_ts`` is wrong —
|
|
88
|
-
|
|
89
|
-
so every tile would display "no baseline"
|
|
90
|
-
data the org has.
|
|
88
|
+
Tying these to ``request.from_ts`` is wrong — for any window wider
|
|
89
|
+
than 7 days ``prior`` would land before the requested window and
|
|
90
|
+
always be empty, so every tile would display "no baseline"
|
|
91
|
+
regardless of how much data the org has.
|
|
91
92
|
"""
|
|
92
93
|
# Tile + distribution baselines are always last-7d-vs-prior-7d,
|
|
93
94
|
# anchored to ``request.to_ts`` (which is "now" from the frontend's
|
|
@@ -206,6 +207,9 @@ class EvaluationOverviewService:
|
|
|
206
207
|
results: list[AgentSuccessEvaluationResult],
|
|
207
208
|
earliest_eval_ts: int | None,
|
|
208
209
|
) -> HeroBlock:
|
|
210
|
+
# days_since only feeds the EARLY-vs-FULL gate, which is reachable only
|
|
211
|
+
# when shadow_enabled is True. Shadow is hardcoded off below, so this is
|
|
212
|
+
# currently inert — retained for the (pending) shadow re-enable.
|
|
209
213
|
if earliest_eval_ts is None:
|
|
210
214
|
days_since = None
|
|
211
215
|
else:
|
|
@@ -264,8 +268,8 @@ class EvaluationOverviewService:
|
|
|
264
268
|
def _build_attribution(
|
|
265
269
|
self,
|
|
266
270
|
results: list[AgentSuccessEvaluationResult],
|
|
267
|
-
citations_by_session: dict[ResultKey, list[
|
|
268
|
-
rule_titles: dict[
|
|
271
|
+
citations_by_session: dict[ResultKey, list[CitationKey]],
|
|
272
|
+
rule_titles: dict[CitationKey, str],
|
|
269
273
|
) -> list[RuleAttributionRow]:
|
|
270
274
|
is_success_by_session = {
|
|
271
275
|
(r.user_id, r.session_id): r.is_success for r in results
|
|
@@ -291,7 +295,7 @@ class EvaluationOverviewService:
|
|
|
291
295
|
|
|
292
296
|
def _load_citations(
|
|
293
297
|
self, result_keys: list[ResultKey]
|
|
294
|
-
) -> tuple[dict[ResultKey, list[
|
|
298
|
+
) -> tuple[dict[ResultKey, list[CitationKey]], dict[CitationKey, str]]:
|
|
295
299
|
"""Pull `Interaction.citations` keyed by user/session, with title lookup.
|
|
296
300
|
|
|
297
301
|
Falls back to empty data when the underlying storage method returns
|
|
@@ -300,8 +304,8 @@ class EvaluationOverviewService:
|
|
|
300
304
|
"""
|
|
301
305
|
wanted = set(result_keys)
|
|
302
306
|
session_ids = sorted({session_id for _, session_id in result_keys})
|
|
303
|
-
citations_by_session: dict[ResultKey, list[
|
|
304
|
-
rule_titles: dict[
|
|
307
|
+
citations_by_session: dict[ResultKey, list[CitationKey]] = defaultdict(list)
|
|
308
|
+
rule_titles: dict[CitationKey, str] = {}
|
|
305
309
|
for citation in self.storage.get_citations_by_session_ids(session_ids): # type: ignore[attr-defined]
|
|
306
310
|
result_key = (citation.user_id, citation.session_id)
|
|
307
311
|
if result_key not in wanted:
|
|
@@ -369,8 +373,8 @@ class EvaluationOverviewService:
|
|
|
369
373
|
previous: list[AgentSuccessEvaluationResult],
|
|
370
374
|
session_sources: dict[ResultKey, str],
|
|
371
375
|
bucket: BucketLiteral,
|
|
372
|
-
citations_by_session: dict[ResultKey, list[
|
|
373
|
-
rule_titles: dict[
|
|
376
|
+
citations_by_session: dict[ResultKey, list[CitationKey]],
|
|
377
|
+
rule_titles: dict[CitationKey, str],
|
|
374
378
|
current_scores: list[ImportedScore],
|
|
375
379
|
prior_scores: list[ImportedScore],
|
|
376
380
|
) -> SourceSetComparison:
|
|
@@ -47,14 +47,25 @@ def build_extractor_agent_run_record(
|
|
|
47
47
|
org_id: str,
|
|
48
48
|
extractor_kind: str,
|
|
49
49
|
user_id: str | None,
|
|
50
|
-
request_id: str,
|
|
51
50
|
agent_version: str | None,
|
|
52
51
|
source: str | None,
|
|
53
52
|
request_interaction_data_models: list[RequestInteractionDataModel],
|
|
54
53
|
extractor_config: BaseModel,
|
|
55
54
|
service_config: Any,
|
|
56
55
|
agent_context: str,
|
|
56
|
+
generation_request_id: str | None = None,
|
|
57
|
+
request_id: str | None = None,
|
|
57
58
|
) -> AgentRunRecord:
|
|
59
|
+
if generation_request_id is not None:
|
|
60
|
+
if request_id is not None and request_id != generation_request_id:
|
|
61
|
+
raise TypeError(
|
|
62
|
+
"generation_request_id and request_id must match when both are provided"
|
|
63
|
+
)
|
|
64
|
+
elif request_id is not None:
|
|
65
|
+
generation_request_id = request_id
|
|
66
|
+
else:
|
|
67
|
+
raise TypeError("generation_request_id is required")
|
|
68
|
+
|
|
58
69
|
source_interaction_ids = extract_source_interaction_ids(
|
|
59
70
|
request_interaction_data_models
|
|
60
71
|
)
|
|
@@ -66,7 +77,9 @@ def build_extractor_agent_run_record(
|
|
|
66
77
|
org_id=org_id,
|
|
67
78
|
extractor_kind=extractor_kind,
|
|
68
79
|
user_id=user_id,
|
|
69
|
-
request_id
|
|
80
|
+
# The durable storage boundary still uses the legacy ``request_id``
|
|
81
|
+
# field name for run provenance.
|
|
82
|
+
request_id=generation_request_id,
|
|
70
83
|
agent_version=agent_version,
|
|
71
84
|
source=source,
|
|
72
85
|
source_interaction_ids=source_interaction_ids,
|
|
@@ -80,7 +93,9 @@ def build_extractor_agent_run_record(
|
|
|
80
93
|
),
|
|
81
94
|
status=AgentRunStatus.RUNNING,
|
|
82
95
|
generation_request_snapshot={
|
|
83
|
-
|
|
96
|
+
# Snapshot payloads also keep the legacy ``request_id`` key so
|
|
97
|
+
# existing storage readers do not need a migration.
|
|
98
|
+
"request_id": generation_request_id,
|
|
84
99
|
"source": source,
|
|
85
100
|
"source_interaction_ids": source_interaction_ids,
|
|
86
101
|
"session_count": len(request_interaction_data_models),
|
|
@@ -7,6 +7,9 @@ from typing import TYPE_CHECKING, Literal
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from reflexio.server.llm.token_accounting import RunTokenTotals
|
|
10
|
+
from reflexio.server.services.deferred_learning_plan import (
|
|
11
|
+
ExtractorBookmarkAdvance,
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
@dataclass(frozen=True)
|
|
@@ -17,6 +20,9 @@ class ExtractionOutcome[T]:
|
|
|
17
20
|
items: list[T] = field(default_factory=list)
|
|
18
21
|
run_id: str | None = None
|
|
19
22
|
token_totals: RunTokenTotals | None = None
|
|
23
|
+
# The stride-bookmark advance the extractor no longer applies itself (F1);
|
|
24
|
+
# applied downstream in persist (durable) or ``.run()``'s persist half.
|
|
25
|
+
bookmark_advance: ExtractorBookmarkAdvance | None = None
|
|
20
26
|
|
|
21
27
|
@classmethod
|
|
22
28
|
def completed(
|
|
@@ -25,9 +31,14 @@ class ExtractionOutcome[T]:
|
|
|
25
31
|
*,
|
|
26
32
|
run_id: str | None = None,
|
|
27
33
|
token_totals: RunTokenTotals | None = None,
|
|
34
|
+
bookmark_advance: ExtractorBookmarkAdvance | None = None,
|
|
28
35
|
) -> ExtractionOutcome[T]:
|
|
29
36
|
return cls(
|
|
30
|
-
status="completed",
|
|
37
|
+
status="completed",
|
|
38
|
+
items=items,
|
|
39
|
+
run_id=run_id,
|
|
40
|
+
token_totals=token_totals,
|
|
41
|
+
bookmark_advance=bookmark_advance,
|
|
31
42
|
)
|
|
32
43
|
|
|
33
44
|
@classmethod
|
|
@@ -151,7 +151,7 @@ def format_prior_knowledge_context(matches: list[PriorAnswerMatch]) -> str | Non
|
|
|
151
151
|
for match in resolved:
|
|
152
152
|
lines.extend(
|
|
153
153
|
[
|
|
154
|
-
f"-
|
|
154
|
+
f"- pending_tool_call_id: {match.pending_tool_call_id}",
|
|
155
155
|
f" question: {match.question_text}",
|
|
156
156
|
f" answer: {_format_result(match.result)}",
|
|
157
157
|
f" resolved_at: {_format_datetime(match.resolved_at)}",
|
|
@@ -161,6 +161,7 @@ def run_resumable_extraction_agent(
|
|
|
161
161
|
log_label: str,
|
|
162
162
|
) -> AgentRunResult:
|
|
163
163
|
"""Run and finalize a config-gated classic extraction agent pass."""
|
|
164
|
+
generation_request_id = request_id
|
|
164
165
|
pending_config = _pending_tool_call_config(request_context)
|
|
165
166
|
storage = request_context.storage
|
|
166
167
|
if storage is None:
|
|
@@ -172,7 +173,7 @@ def run_resumable_extraction_agent(
|
|
|
172
173
|
org_id=request_context.org_id,
|
|
173
174
|
extractor_kind=extractor_kind,
|
|
174
175
|
user_id=user_id,
|
|
175
|
-
|
|
176
|
+
generation_request_id=generation_request_id,
|
|
176
177
|
agent_version=agent_version,
|
|
177
178
|
source=source,
|
|
178
179
|
request_interaction_data_models=request_interaction_data_models,
|