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,554 @@
|
|
|
1
|
+
"""Retrieved-learning relevance/impact evaluator.
|
|
2
|
+
|
|
3
|
+
Judges every learning that was retrieved and injected into a session
|
|
4
|
+
(``Interaction.retrieved_learnings``) with two LLM judge families:
|
|
5
|
+
|
|
6
|
+
- **relevance**: does the learning apply to the session's task/response?
|
|
7
|
+
- **impact**: relative to the agent's definition of success, did applying it
|
|
8
|
+
move the response toward success, away from it, or not materially change it
|
|
9
|
+
(counterfactual judgment from the observed transcript)?
|
|
10
|
+
|
|
11
|
+
Plain class colocated with the group-evaluation runner (no
|
|
12
|
+
``BaseGenerationService`` machinery — this is a judge that writes to its own
|
|
13
|
+
table, like the shadow-comparison judge). Consumes the bounded storage
|
|
14
|
+
snapshot; never loads full ``Interaction`` objects.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
22
|
+
|
|
23
|
+
from pydantic import ConfigDict, Field
|
|
24
|
+
|
|
25
|
+
from reflexio.models.api_schema.domain import RetrievedLearningEvaluationResult
|
|
26
|
+
from reflexio.models.structured_output import StrictStructuredOutput
|
|
27
|
+
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
28
|
+
from reflexio.server.services.service_utils import (
|
|
29
|
+
log_llm_messages,
|
|
30
|
+
log_model_response,
|
|
31
|
+
slice_content_by_tokens,
|
|
32
|
+
)
|
|
33
|
+
from reflexio.server.services.storage.storage_base.retrieved_learning_state import (
|
|
34
|
+
CANONICAL_RETRIEVED_KINDS,
|
|
35
|
+
BoundedRetrievedLearningSnapshot,
|
|
36
|
+
)
|
|
37
|
+
from reflexio.server.site_var.site_var_manager import SiteVarManager
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
41
|
+
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
RETRIEVED_LEARNING_RELEVANCE_PROMPT_ID = "retrieved_learning_relevance"
|
|
46
|
+
RETRIEVED_LEARNING_IMPACT_PROMPT_ID = "retrieved_learning_impact"
|
|
47
|
+
|
|
48
|
+
# Bounded-input constants (code constants by design — no env knobs).
|
|
49
|
+
MAX_CANONICAL_CANDIDATES = 1_000
|
|
50
|
+
CANDIDATES_PER_CHUNK = 25
|
|
51
|
+
LEARNING_BODY_TOKEN_LIMIT = 256
|
|
52
|
+
TRANSCRIPT_TOKEN_LIMIT = 8_000
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RetrievedLearningRelevanceVerdict(StrictStructuredOutput):
|
|
56
|
+
"""One relevance verdict, keyed by the echoed opaque ``learning_ref``."""
|
|
57
|
+
|
|
58
|
+
learning_ref: str = Field(
|
|
59
|
+
description="Exact echo of the learning_ref shown in the prompt,"
|
|
60
|
+
" e.g. 'profile:abc123'"
|
|
61
|
+
)
|
|
62
|
+
is_relevant: bool = Field(
|
|
63
|
+
description="Whether this learning applies to the user's task and the"
|
|
64
|
+
" agent response in this session"
|
|
65
|
+
)
|
|
66
|
+
relevance_reason: str = Field(
|
|
67
|
+
description="Why this learning is or is not relevant to the session"
|
|
68
|
+
)
|
|
69
|
+
model_config = ConfigDict(
|
|
70
|
+
extra="allow",
|
|
71
|
+
json_schema_extra={"additionalProperties": False},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RetrievedLearningRelevanceOutput(StrictStructuredOutput):
|
|
76
|
+
"""Relevance judge output: exactly one verdict per listed learning."""
|
|
77
|
+
|
|
78
|
+
verdicts: list[RetrievedLearningRelevanceVerdict] = Field(
|
|
79
|
+
description="Exactly one verdict per learning listed in the prompt"
|
|
80
|
+
)
|
|
81
|
+
model_config = ConfigDict(
|
|
82
|
+
extra="allow",
|
|
83
|
+
json_schema_extra={"additionalProperties": False},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RetrievedLearningImpactVerdict(StrictStructuredOutput):
|
|
88
|
+
"""One impact verdict, keyed by the echoed opaque ``learning_ref``."""
|
|
89
|
+
|
|
90
|
+
learning_ref: str = Field(
|
|
91
|
+
description="Exact echo of the learning_ref shown in the prompt,"
|
|
92
|
+
" e.g. 'user_playbook:42'"
|
|
93
|
+
)
|
|
94
|
+
impact: Literal["positive", "negative", "neutral"] = Field(
|
|
95
|
+
description="Whether applying this learning plausibly improved,"
|
|
96
|
+
" harmed, or did not materially change the agent response"
|
|
97
|
+
)
|
|
98
|
+
impact_reason: str = Field(
|
|
99
|
+
description="Counterfactual reasoning for the impact judgment"
|
|
100
|
+
)
|
|
101
|
+
model_config = ConfigDict(
|
|
102
|
+
extra="allow",
|
|
103
|
+
json_schema_extra={"additionalProperties": False},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RetrievedLearningImpactOutput(StrictStructuredOutput):
|
|
108
|
+
"""Impact judge output: exactly one verdict per listed learning."""
|
|
109
|
+
|
|
110
|
+
verdicts: list[RetrievedLearningImpactVerdict] = Field(
|
|
111
|
+
description="Exactly one verdict per learning listed in the prompt"
|
|
112
|
+
)
|
|
113
|
+
model_config = ConfigDict(
|
|
114
|
+
extra="allow",
|
|
115
|
+
json_schema_extra={"additionalProperties": False},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class LearningCandidate:
|
|
121
|
+
"""One resolved historically attached learning.
|
|
122
|
+
|
|
123
|
+
``title`` comes from the resolved original row (playbook name; empty for
|
|
124
|
+
profiles) and is shown to the judges only — it is never persisted.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
kind: str
|
|
128
|
+
learning_id: str
|
|
129
|
+
title: str
|
|
130
|
+
content: str
|
|
131
|
+
trigger: str
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def learning_ref(self) -> str:
|
|
135
|
+
return f"{self.kind}:{self.learning_id}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class RetrievedLearningEvaluationRun:
|
|
140
|
+
"""Evaluator output handed to the storage replacement CAS.
|
|
141
|
+
|
|
142
|
+
``outcome`` is ``"evaluated"`` when at least one usable verdict exists (or
|
|
143
|
+
the candidate set is legitimately empty) and ``"failed"`` when no judge
|
|
144
|
+
produced any usable verdict or a bound was exceeded — the runner then
|
|
145
|
+
preserves the prior snapshot via ``finish_retrieved_learning_evaluation_run``.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
outcome: Literal["evaluated", "failed"]
|
|
149
|
+
proposed_status: Literal["complete", "degraded"] = "complete"
|
|
150
|
+
rows: list[RetrievedLearningEvaluationResult] = field(default_factory=list)
|
|
151
|
+
diagnostics: dict[str, Any] = field(default_factory=dict)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class RetrievedLearningEvaluator:
|
|
155
|
+
"""Judge retrieved learnings for session relevance and response impact."""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
request_context: RequestContext,
|
|
160
|
+
llm_client: LiteLLMClient,
|
|
161
|
+
agent_context: str = "",
|
|
162
|
+
success_definition: str = "",
|
|
163
|
+
):
|
|
164
|
+
"""Initialize the evaluator.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
request_context (RequestContext): Storage + prompt manager.
|
|
168
|
+
llm_client (LiteLLMClient): Unified LLM client.
|
|
169
|
+
agent_context (str): Context about the agent, injected into both
|
|
170
|
+
judge prompts.
|
|
171
|
+
success_definition (str): The agent's definition of success (from
|
|
172
|
+
``AgentSuccessConfig.success_definition_prompt``), injected into
|
|
173
|
+
the impact judge only so impact is judged relative to what the
|
|
174
|
+
org actually considers success. Empty means the impact judge
|
|
175
|
+
falls back to general task helpfulness.
|
|
176
|
+
"""
|
|
177
|
+
self.request_context = request_context
|
|
178
|
+
self.client = llm_client
|
|
179
|
+
self.agent_context = agent_context
|
|
180
|
+
self.success_definition = success_definition
|
|
181
|
+
|
|
182
|
+
config = self.request_context.configurator.get_config()
|
|
183
|
+
llm_config = config.llm_config if config else None
|
|
184
|
+
model_setting = SiteVarManager().get_site_var("llm_model_setting")
|
|
185
|
+
site_var = model_setting if isinstance(model_setting, dict) else {}
|
|
186
|
+
self.model_name = resolve_model_name(
|
|
187
|
+
ModelRole.EVALUATION,
|
|
188
|
+
site_var_value=site_var.get("default_evaluate_model_name"),
|
|
189
|
+
config_override=llm_config.generation_model_name if llm_config else None,
|
|
190
|
+
api_key_config=config.api_key_config if config else None,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# ===============================
|
|
194
|
+
# public methods
|
|
195
|
+
# ===============================
|
|
196
|
+
|
|
197
|
+
def evaluate(
|
|
198
|
+
self,
|
|
199
|
+
user_id: str,
|
|
200
|
+
session_id: str,
|
|
201
|
+
agent_version: str,
|
|
202
|
+
snapshot: BoundedRetrievedLearningSnapshot,
|
|
203
|
+
) -> RetrievedLearningEvaluationRun:
|
|
204
|
+
"""Evaluate every attached learning whose original row still exists.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
user_id (str): Session owner.
|
|
208
|
+
session_id (str): Evaluated session.
|
|
209
|
+
agent_version (str): Version requested by the runner.
|
|
210
|
+
snapshot (BoundedRetrievedLearningSnapshot): Bounded projection
|
|
211
|
+
loaded by storage.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
RetrievedLearningEvaluationRun: Proposed rows + status, or a
|
|
215
|
+
failure with a sanitized reason.
|
|
216
|
+
"""
|
|
217
|
+
diagnostics: dict[str, Any] = {
|
|
218
|
+
"invalid_ref_count": 0,
|
|
219
|
+
"failed_relevance_chunks": 0,
|
|
220
|
+
"failed_impact_chunks": 0,
|
|
221
|
+
}
|
|
222
|
+
if snapshot.attachment_limit_exceeded:
|
|
223
|
+
diagnostics["error_type"] = "attachment_limit_exceeded"
|
|
224
|
+
return RetrievedLearningEvaluationRun(
|
|
225
|
+
outcome="failed", diagnostics=diagnostics
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
refs = self._collect_canonical_refs(snapshot, diagnostics)
|
|
229
|
+
if len(refs) > MAX_CANONICAL_CANDIDATES:
|
|
230
|
+
# Never truncate silently: zero LLM calls, preserve prior snapshot.
|
|
231
|
+
diagnostics["error_type"] = "candidate_limit_exceeded"
|
|
232
|
+
return RetrievedLearningEvaluationRun(
|
|
233
|
+
outcome="failed", diagnostics=diagnostics
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
candidates = self._resolve_candidates(user_id, refs, diagnostics)
|
|
237
|
+
diagnostics["candidate_count"] = len(candidates)
|
|
238
|
+
if not candidates:
|
|
239
|
+
# No attached or eligible refs: zero LLM calls; the storage
|
|
240
|
+
# replacement clears prior rows and records not_applicable.
|
|
241
|
+
return RetrievedLearningEvaluationRun(
|
|
242
|
+
outcome="evaluated",
|
|
243
|
+
proposed_status="complete",
|
|
244
|
+
rows=[],
|
|
245
|
+
diagnostics=diagnostics,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
transcript = self._format_transcript(snapshot)
|
|
249
|
+
relevance: dict[str, RetrievedLearningRelevanceVerdict] = {}
|
|
250
|
+
impact: dict[str, RetrievedLearningImpactVerdict] = {}
|
|
251
|
+
chunks = [
|
|
252
|
+
candidates[i : i + CANDIDATES_PER_CHUNK]
|
|
253
|
+
for i in range(0, len(candidates), CANDIDATES_PER_CHUNK)
|
|
254
|
+
]
|
|
255
|
+
for chunk in chunks:
|
|
256
|
+
chunk_refs = {c.learning_ref for c in chunk}
|
|
257
|
+
relevance_output = self._run_judge(
|
|
258
|
+
RETRIEVED_LEARNING_RELEVANCE_PROMPT_ID,
|
|
259
|
+
RetrievedLearningRelevanceOutput,
|
|
260
|
+
transcript,
|
|
261
|
+
chunk,
|
|
262
|
+
)
|
|
263
|
+
if relevance_output is None:
|
|
264
|
+
diagnostics["failed_relevance_chunks"] += 1
|
|
265
|
+
else:
|
|
266
|
+
for verdict in relevance_output.verdicts:
|
|
267
|
+
if verdict.learning_ref in chunk_refs:
|
|
268
|
+
relevance[verdict.learning_ref] = verdict
|
|
269
|
+
impact_output = self._run_judge(
|
|
270
|
+
RETRIEVED_LEARNING_IMPACT_PROMPT_ID,
|
|
271
|
+
RetrievedLearningImpactOutput,
|
|
272
|
+
transcript,
|
|
273
|
+
chunk,
|
|
274
|
+
)
|
|
275
|
+
if impact_output is None:
|
|
276
|
+
diagnostics["failed_impact_chunks"] += 1
|
|
277
|
+
else:
|
|
278
|
+
for verdict in impact_output.verdicts:
|
|
279
|
+
if verdict.learning_ref in chunk_refs:
|
|
280
|
+
impact[verdict.learning_ref] = verdict
|
|
281
|
+
|
|
282
|
+
if not relevance and not impact:
|
|
283
|
+
diagnostics["error_type"] = "all_judges_failed"
|
|
284
|
+
return RetrievedLearningEvaluationRun(
|
|
285
|
+
outcome="failed", diagnostics=diagnostics
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Build rows by iterating canonical input candidates — never LLM
|
|
289
|
+
# output — so every candidate appears exactly once and unknown output
|
|
290
|
+
# cannot be inserted.
|
|
291
|
+
created_at = snapshot.earliest_request_created_at or 0
|
|
292
|
+
rows: list[RetrievedLearningEvaluationResult] = []
|
|
293
|
+
for candidate in candidates:
|
|
294
|
+
relevance_verdict = relevance.get(candidate.learning_ref)
|
|
295
|
+
impact_verdict = impact.get(candidate.learning_ref)
|
|
296
|
+
rows.append(
|
|
297
|
+
RetrievedLearningEvaluationResult(
|
|
298
|
+
user_id=user_id,
|
|
299
|
+
session_id=session_id,
|
|
300
|
+
agent_version=agent_version,
|
|
301
|
+
kind=candidate.kind, # type: ignore[arg-type]
|
|
302
|
+
learning_id=candidate.learning_id,
|
|
303
|
+
is_relevant=(
|
|
304
|
+
relevance_verdict.is_relevant if relevance_verdict else None
|
|
305
|
+
),
|
|
306
|
+
relevance_reason=(
|
|
307
|
+
relevance_verdict.relevance_reason if relevance_verdict else ""
|
|
308
|
+
),
|
|
309
|
+
impact=impact_verdict.impact if impact_verdict else None,
|
|
310
|
+
impact_reason=(
|
|
311
|
+
impact_verdict.impact_reason if impact_verdict else ""
|
|
312
|
+
),
|
|
313
|
+
created_at=created_at,
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
degraded = (
|
|
317
|
+
diagnostics["failed_relevance_chunks"] > 0
|
|
318
|
+
or diagnostics["failed_impact_chunks"] > 0
|
|
319
|
+
)
|
|
320
|
+
return RetrievedLearningEvaluationRun(
|
|
321
|
+
outcome="evaluated",
|
|
322
|
+
proposed_status="degraded" if degraded else "complete",
|
|
323
|
+
rows=rows,
|
|
324
|
+
diagnostics=diagnostics,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# ===============================
|
|
328
|
+
# candidate construction
|
|
329
|
+
# ===============================
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _collect_canonical_refs(
|
|
333
|
+
snapshot: BoundedRetrievedLearningSnapshot,
|
|
334
|
+
diagnostics: dict[str, Any],
|
|
335
|
+
) -> dict[tuple[str, str], None]:
|
|
336
|
+
"""Dedupe ``(kind, learning_id)`` refs in chronological order.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
dict: ``(kind, learning_id) -> None`` used as an ordered set.
|
|
340
|
+
"""
|
|
341
|
+
refs: dict[tuple[str, str], None] = {}
|
|
342
|
+
for interaction in snapshot.interactions:
|
|
343
|
+
for kind, learning_id in interaction.refs:
|
|
344
|
+
if kind not in CANONICAL_RETRIEVED_KINDS:
|
|
345
|
+
diagnostics["invalid_ref_count"] += 1
|
|
346
|
+
continue
|
|
347
|
+
refs.setdefault((kind, learning_id), None)
|
|
348
|
+
return refs
|
|
349
|
+
|
|
350
|
+
def _resolve_candidates(
|
|
351
|
+
self,
|
|
352
|
+
user_id: str,
|
|
353
|
+
refs: dict[tuple[str, str], None],
|
|
354
|
+
diagnostics: dict[str, Any],
|
|
355
|
+
) -> list[LearningCandidate]:
|
|
356
|
+
"""Resolve attached refs to their original rows; skip missing rows.
|
|
357
|
+
|
|
358
|
+
Lifecycle status and agent-playbook approval do not affect historical
|
|
359
|
+
evaluation. Learning content/trigger/title come from the original row;
|
|
360
|
+
unresolvable refs are skipped, never followed to a successor.
|
|
361
|
+
|
|
362
|
+
Resolution is best-effort, not a durability guarantee: row retention
|
|
363
|
+
(``RETENTION_TARGETS``) evicts tombstoned rows first and
|
|
364
|
+
``gc_expired_tombstones`` hard-deletes aged ones, so a learning that was
|
|
365
|
+
genuinely served can age out and then resolve to nothing. That is a
|
|
366
|
+
silent skip by design — an old session simply judges fewer learnings.
|
|
367
|
+
"""
|
|
368
|
+
storage = self.request_context.storage
|
|
369
|
+
if storage is None:
|
|
370
|
+
return []
|
|
371
|
+
profile_ids = [lid for (kind, lid) in refs if kind == "profile"]
|
|
372
|
+
user_playbook_ids: list[int] = []
|
|
373
|
+
agent_playbook_ids: list[int] = []
|
|
374
|
+
for kind, lid in refs:
|
|
375
|
+
if kind not in ("user_playbook", "agent_playbook"):
|
|
376
|
+
continue
|
|
377
|
+
try:
|
|
378
|
+
parsed = int(lid)
|
|
379
|
+
except ValueError:
|
|
380
|
+
diagnostics["invalid_ref_count"] += 1
|
|
381
|
+
continue
|
|
382
|
+
if parsed <= 0:
|
|
383
|
+
diagnostics["invalid_ref_count"] += 1
|
|
384
|
+
continue
|
|
385
|
+
if kind == "user_playbook":
|
|
386
|
+
user_playbook_ids.append(parsed)
|
|
387
|
+
else:
|
|
388
|
+
agent_playbook_ids.append(parsed)
|
|
389
|
+
|
|
390
|
+
resolved: dict[tuple[str, str], LearningCandidate] = {}
|
|
391
|
+
if profile_ids:
|
|
392
|
+
for profile in storage.get_profiles_by_ids(
|
|
393
|
+
user_id, profile_ids, include_inactive=True
|
|
394
|
+
):
|
|
395
|
+
resolved[("profile", profile.profile_id)] = LearningCandidate(
|
|
396
|
+
kind="profile",
|
|
397
|
+
learning_id=profile.profile_id,
|
|
398
|
+
title="",
|
|
399
|
+
content=profile.content,
|
|
400
|
+
trigger="",
|
|
401
|
+
)
|
|
402
|
+
if user_playbook_ids:
|
|
403
|
+
for playbook in storage.get_user_playbooks_by_ids(
|
|
404
|
+
user_id, user_playbook_ids, include_inactive=True
|
|
405
|
+
):
|
|
406
|
+
key = ("user_playbook", str(playbook.user_playbook_id))
|
|
407
|
+
resolved[key] = LearningCandidate(
|
|
408
|
+
kind="user_playbook",
|
|
409
|
+
learning_id=str(playbook.user_playbook_id),
|
|
410
|
+
title=playbook.playbook_name,
|
|
411
|
+
content=playbook.content,
|
|
412
|
+
trigger=playbook.trigger or "",
|
|
413
|
+
)
|
|
414
|
+
for playbook in storage.get_agent_playbooks_by_ids(
|
|
415
|
+
agent_playbook_ids,
|
|
416
|
+
include_inactive=True,
|
|
417
|
+
):
|
|
418
|
+
key = ("agent_playbook", str(playbook.agent_playbook_id))
|
|
419
|
+
resolved[key] = LearningCandidate(
|
|
420
|
+
kind="agent_playbook",
|
|
421
|
+
learning_id=str(playbook.agent_playbook_id),
|
|
422
|
+
title=playbook.playbook_name,
|
|
423
|
+
content=playbook.content,
|
|
424
|
+
trigger=playbook.trigger or "",
|
|
425
|
+
)
|
|
426
|
+
# Preserve first-seen order of the attached refs.
|
|
427
|
+
return [resolved[key] for key in refs if key in resolved]
|
|
428
|
+
|
|
429
|
+
# ===============================
|
|
430
|
+
# judging
|
|
431
|
+
# ===============================
|
|
432
|
+
|
|
433
|
+
@staticmethod
|
|
434
|
+
def _format_transcript(snapshot: BoundedRetrievedLearningSnapshot) -> str:
|
|
435
|
+
lines = [
|
|
436
|
+
f"{interaction.role}: {interaction.content}"
|
|
437
|
+
for interaction in snapshot.interactions
|
|
438
|
+
if interaction.role or interaction.content
|
|
439
|
+
]
|
|
440
|
+
return slice_content_by_tokens("\n".join(lines), TRANSCRIPT_TOKEN_LIMIT)
|
|
441
|
+
|
|
442
|
+
def _learnings_payload(self, chunk: list[LearningCandidate]) -> str:
|
|
443
|
+
import json
|
|
444
|
+
|
|
445
|
+
return json.dumps(
|
|
446
|
+
[
|
|
447
|
+
{
|
|
448
|
+
"learning_ref": c.learning_ref,
|
|
449
|
+
"kind": c.kind,
|
|
450
|
+
"title": c.title,
|
|
451
|
+
"content": slice_content_by_tokens(
|
|
452
|
+
c.content, LEARNING_BODY_TOKEN_LIMIT
|
|
453
|
+
),
|
|
454
|
+
"trigger": slice_content_by_tokens(
|
|
455
|
+
c.trigger, LEARNING_BODY_TOKEN_LIMIT
|
|
456
|
+
),
|
|
457
|
+
}
|
|
458
|
+
for c in chunk
|
|
459
|
+
],
|
|
460
|
+
indent=2,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def _run_judge[
|
|
464
|
+
TJudgeOutput: (RetrievedLearningRelevanceOutput, RetrievedLearningImpactOutput)
|
|
465
|
+
](
|
|
466
|
+
self,
|
|
467
|
+
prompt_id: str,
|
|
468
|
+
output_model: type[TJudgeOutput],
|
|
469
|
+
transcript: str,
|
|
470
|
+
chunk: list[LearningCandidate],
|
|
471
|
+
) -> TJudgeOutput | None:
|
|
472
|
+
"""Run one judge call for one chunk, with one bounded semantic repair.
|
|
473
|
+
|
|
474
|
+
A response is valid only when its verdict refs exactly equal the
|
|
475
|
+
chunk's input refs (no duplicates, no missing, no unknown). One
|
|
476
|
+
corrective retry naming only the coverage error; after the second
|
|
477
|
+
failure the chunk is marked failed.
|
|
478
|
+
"""
|
|
479
|
+
expected_refs = {c.learning_ref for c in chunk}
|
|
480
|
+
variables: dict[str, Any] = {
|
|
481
|
+
"agent_context_prompt": self.agent_context,
|
|
482
|
+
"interactions": transcript,
|
|
483
|
+
"learnings": self._learnings_payload(chunk),
|
|
484
|
+
}
|
|
485
|
+
# Only the impact judge is anchored to the definition of success;
|
|
486
|
+
# relevance stays success-agnostic ("does this learning apply?").
|
|
487
|
+
if prompt_id == RETRIEVED_LEARNING_IMPACT_PROMPT_ID:
|
|
488
|
+
variables["success_definition_prompt"] = self.success_definition
|
|
489
|
+
prompt = self.request_context.prompt_manager.render_prompt(prompt_id, variables)
|
|
490
|
+
messages: list[dict[str, Any]] = [{"role": "user", "content": prompt}]
|
|
491
|
+
for attempt in range(2):
|
|
492
|
+
log_llm_messages(logger, f"Retrieved-learning judge {prompt_id}", messages)
|
|
493
|
+
response = self.client.generate_chat_response(
|
|
494
|
+
messages=messages,
|
|
495
|
+
model=self.model_name,
|
|
496
|
+
response_format=output_model,
|
|
497
|
+
)
|
|
498
|
+
if not isinstance(response, output_model):
|
|
499
|
+
logger.warning(
|
|
500
|
+
"event=retrieved_learning_eval_judge_bad_response prompt=%s"
|
|
501
|
+
" attempt=%d type=%s",
|
|
502
|
+
prompt_id,
|
|
503
|
+
attempt,
|
|
504
|
+
type(response).__name__,
|
|
505
|
+
)
|
|
506
|
+
return None
|
|
507
|
+
log_model_response(logger, f"Judge {prompt_id} response", response)
|
|
508
|
+
coverage_error = _verdict_coverage_error(
|
|
509
|
+
[v.learning_ref for v in response.verdicts], expected_refs
|
|
510
|
+
)
|
|
511
|
+
if coverage_error is None:
|
|
512
|
+
return response
|
|
513
|
+
if attempt == 0:
|
|
514
|
+
logger.info(
|
|
515
|
+
"event=retrieved_learning_eval_chunk_retry prompt=%s reason=%s",
|
|
516
|
+
prompt_id,
|
|
517
|
+
coverage_error,
|
|
518
|
+
)
|
|
519
|
+
messages = [
|
|
520
|
+
*messages,
|
|
521
|
+
{"role": "assistant", "content": response.model_dump_json()},
|
|
522
|
+
{
|
|
523
|
+
"role": "user",
|
|
524
|
+
"content": (
|
|
525
|
+
"Your previous response had a coverage error:"
|
|
526
|
+
f" {coverage_error}. Return exactly one verdict per"
|
|
527
|
+
" learning_ref listed in the original prompt — no"
|
|
528
|
+
" duplicates, no omissions, no other refs."
|
|
529
|
+
),
|
|
530
|
+
},
|
|
531
|
+
]
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _verdict_coverage_error(refs: list[str], expected: set[str]) -> str | None:
|
|
536
|
+
"""Name the coverage error for a judge response, or None when exact."""
|
|
537
|
+
seen: set[str] = set()
|
|
538
|
+
duplicates: set[str] = set()
|
|
539
|
+
unknown: set[str] = set()
|
|
540
|
+
for ref in refs:
|
|
541
|
+
if ref in seen:
|
|
542
|
+
duplicates.add(ref)
|
|
543
|
+
seen.add(ref)
|
|
544
|
+
if ref not in expected:
|
|
545
|
+
unknown.add(ref)
|
|
546
|
+
missing = expected - seen
|
|
547
|
+
problems: list[str] = []
|
|
548
|
+
if missing:
|
|
549
|
+
problems.append(f"missing refs {sorted(missing)}")
|
|
550
|
+
if duplicates:
|
|
551
|
+
problems.append(f"duplicate refs {sorted(duplicates)}")
|
|
552
|
+
if unknown:
|
|
553
|
+
problems.append(f"unknown refs {sorted(unknown)}")
|
|
554
|
+
return "; ".join(problems) if problems else None
|
package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py
CHANGED
|
@@ -19,6 +19,7 @@ from reflexio.models.api_schema.internal_schema import SessionDescriptor
|
|
|
19
19
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
20
20
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
21
21
|
from reflexio.server.services.agent_success_evaluation.runner import (
|
|
22
|
+
GroupEvaluationOutcome,
|
|
22
23
|
run_group_evaluation,
|
|
23
24
|
)
|
|
24
25
|
from reflexio.server.services.evaluation_overview.eval_sampler import (
|
|
@@ -253,7 +254,7 @@ def _dispatch_one(
|
|
|
253
254
|
"""
|
|
254
255
|
if job.cancel_event.is_set():
|
|
255
256
|
raise _CancelledError
|
|
256
|
-
run_group_evaluation(
|
|
257
|
+
outcome = run_group_evaluation(
|
|
257
258
|
org_id=job.org_id,
|
|
258
259
|
user_id=sc.user_id,
|
|
259
260
|
session_id=sc.session_id,
|
|
@@ -263,6 +264,39 @@ def _dispatch_one(
|
|
|
263
264
|
llm_client=llm_client,
|
|
264
265
|
force_regenerate=True,
|
|
265
266
|
)
|
|
267
|
+
reason = _outcome_failure_reason(outcome)
|
|
268
|
+
if reason is not None:
|
|
269
|
+
raise _OutcomeFailureError(reason)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _outcome_failure_reason(outcome: GroupEvaluationOutcome) -> str | None:
|
|
273
|
+
"""Classify a runner outcome for the regen counters.
|
|
274
|
+
|
|
275
|
+
Completed requires BOTH families in a settled state: agent success in
|
|
276
|
+
{complete, not_applicable, skipped} and retrieved-learning in
|
|
277
|
+
{complete, not_applicable, skipped, superseded}. Everything else counts
|
|
278
|
+
failed with an explicit reason; an unrecognized combination is logged and
|
|
279
|
+
counted failed rather than silently passed.
|
|
280
|
+
"""
|
|
281
|
+
agent_ok = outcome.agent_success_status in ("complete", "not_applicable", "skipped")
|
|
282
|
+
retrieved_ok = outcome.retrieved_learning_status in (
|
|
283
|
+
"complete",
|
|
284
|
+
"not_applicable",
|
|
285
|
+
"skipped",
|
|
286
|
+
"superseded",
|
|
287
|
+
)
|
|
288
|
+
if agent_ok and retrieved_ok:
|
|
289
|
+
return None
|
|
290
|
+
if outcome.agent_success_status == "failed":
|
|
291
|
+
return "agent_success_failed"
|
|
292
|
+
if outcome.retrieved_learning_status in ("pending", "stale", "degraded", "failed"):
|
|
293
|
+
return f"retrieved_learning_{outcome.retrieved_learning_status}"
|
|
294
|
+
logger.warning("Unrecognized group evaluation outcome combination: %s", outcome)
|
|
295
|
+
return f"unrecognized_outcome:{outcome.agent_success_status}:{outcome.retrieved_learning_status}"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class _OutcomeFailureError(Exception):
|
|
299
|
+
"""A settled-but-unsuccessful runner outcome, counted as a job failure."""
|
|
266
300
|
|
|
267
301
|
|
|
268
302
|
class _CancelledError(Exception):
|