회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s
This commit is contained in:
parent
a479db7a5a
commit
a0311c5957
100 changed files with 4884 additions and 11210 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import UTC, datetime
|
||||
|
|
@ -26,6 +25,13 @@ from .calibration_transfer import (
|
|||
assess_synthetic_subgroup_drift,
|
||||
assess_transfer,
|
||||
)
|
||||
from .outcome_repository_values import (
|
||||
canonical_hash as _canonical_hash,
|
||||
created_role as _created_role,
|
||||
public_row as _public_row,
|
||||
value as _value,
|
||||
)
|
||||
from .practice_competency import target_techniques
|
||||
|
||||
|
||||
class CalibrationTransferNotFoundError(LookupError):
|
||||
|
|
@ -64,36 +70,6 @@ _POSITIVE_CLIENT_STATES = frozenset(
|
|||
)
|
||||
|
||||
|
||||
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return row[key]
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _public_row(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""API 응답에서 학습자 피드백 정책 판정 전용 열을 제거한다."""
|
||||
|
||||
payload = dict(row)
|
||||
payload.pop("source_learner_feedback_enabled", None)
|
||||
return payload
|
||||
|
||||
|
||||
def _canonical_hash(payload: Mapping[str, Any]) -> str:
|
||||
serialized = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _created_role(principal: Principal) -> str:
|
||||
return "instructor" if principal.role == Role.TEACHER else principal.role.value
|
||||
|
||||
|
||||
def _ensure_unique_evidence(
|
||||
evidence_turn_ids: Sequence[UUID], *, required: bool = False
|
||||
) -> tuple[UUID, ...]:
|
||||
|
|
@ -182,6 +158,16 @@ async def append_prediction_revision(
|
|||
reason = revision_reason.strip()
|
||||
if not reason:
|
||||
raise CalibrationTransferStateError("revision_reason must not be blank")
|
||||
if not (0.0 <= predicted_success_probability <= 1.0):
|
||||
raise CalibrationTransferStateError(
|
||||
"predicted_success_probability must be between 0.0 and 1.0"
|
||||
)
|
||||
if not (0.0 <= confidence <= 1.0):
|
||||
raise CalibrationTransferStateError(
|
||||
"confidence must be between 0.0 and 1.0"
|
||||
)
|
||||
if instrument_id == "vignette.calibration-self-prediction":
|
||||
instrument_id = "calibration-mirror-g5"
|
||||
payload = {
|
||||
"prediction_revision_id": str(prediction_revision_id),
|
||||
"history_id": str(history_id),
|
||||
|
|
@ -318,6 +304,11 @@ async def append_prediction_revision(
|
|||
asyncpg.ForeignKeyViolationError,
|
||||
asyncpg.ObjectNotInPrerequisiteStateError,
|
||||
) as exc:
|
||||
message = str(exc)
|
||||
if "lock" in message.lower() or "reveal" in message.lower():
|
||||
raise CalibrationTransferStateError(
|
||||
f"self-prediction history is locked or revealed: {message}"
|
||||
) from exc
|
||||
raise CalibrationTransferStateError(
|
||||
"prediction revision violated provenance or history invariants"
|
||||
) from exc
|
||||
|
|
@ -880,21 +871,9 @@ async def append_transfer_suite(
|
|||
|
||||
|
||||
def _actual_target_techniques(competency_id: str) -> frozenset[str]:
|
||||
key = competency_id.lower()
|
||||
if any(token in key for token in ("empathy", "empathic", "reflection")):
|
||||
return frozenset({"empathy", "reflection", "validation", "restatement"})
|
||||
if any(token in key for token in ("open_question", "open-question")):
|
||||
return frozenset({"facilitative_question", "exploration", "clarification"})
|
||||
if any(token in key for token in ("rupture", "repair", "impact")):
|
||||
return frozenset(
|
||||
{"opinion_check", "validation", "reflection", "here_and_now_focus"}
|
||||
)
|
||||
if any(token in key for token in ("goal", "collaborative", "reagreement")):
|
||||
return frozenset(
|
||||
{"consent_motivation_check", "opinion_check", "restatement"}
|
||||
)
|
||||
if any(token in key for token in ("presence", "response-space")):
|
||||
return frozenset({"holding", "reflection", "here_and_now_focus"})
|
||||
targets = target_techniques(competency_id)
|
||||
if targets is not None:
|
||||
return targets
|
||||
raise CalibrationTransferStateError(
|
||||
f"unsupported actual transfer competency: {competency_id}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Literal, Protocol
|
||||
from uuid import UUID, uuid5
|
||||
|
|
@ -26,6 +24,10 @@ from .continuous_improvement import (
|
|||
promote_content_to_catalog,
|
||||
release_allowed,
|
||||
)
|
||||
from .outcome_repository_values import (
|
||||
canonical_hash as _canonical_hash,
|
||||
value as _value,
|
||||
)
|
||||
|
||||
|
||||
DATA_CLASSIFICATION = "synthetic_replay_red_team_coverage_drift"
|
||||
|
|
@ -101,24 +103,6 @@ class RollbackExecutor(Protocol):
|
|||
) -> RollbackExecutionReceipt: ...
|
||||
|
||||
|
||||
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return row[key]
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _canonical_hash(payload: Any) -> str:
|
||||
serialized = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
async def _begin_submission(
|
||||
conn: asyncpg.Connection,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ separate superseding events and never mutate the original evidence or graph.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid5
|
||||
|
|
@ -39,6 +37,12 @@ from .practice_runtime_observer import (
|
|||
derive_runtime_episode,
|
||||
observation_model_run_id,
|
||||
)
|
||||
from .outcome_repository_values import (
|
||||
canonical_hash as _canonical_hash,
|
||||
created_role as _created_role,
|
||||
public_row as _public_row,
|
||||
value as _value,
|
||||
)
|
||||
|
||||
|
||||
_RUNTIME_ATTEMPT_NAMESPACE = UUID("52e24f06-34be-54cb-9092-5122e384c814")
|
||||
|
|
@ -60,36 +64,6 @@ class DeliberatePracticeFeedbackDisabledError(PermissionError):
|
|||
"""처방/연습 원천 회기의 학습자 피드백 스냅샷이 비활성이다."""
|
||||
|
||||
|
||||
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return row[key]
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _public_row(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""API 응답에서 정책 판정 전용 내부 열을 제거한다."""
|
||||
|
||||
payload = dict(row)
|
||||
payload.pop("source_learner_feedback_enabled", None)
|
||||
return payload
|
||||
|
||||
|
||||
def _canonical_hash(payload: Mapping[str, Any]) -> str:
|
||||
serialized = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _created_role(principal: Principal) -> str:
|
||||
return "instructor" if principal.role == Role.TEACHER else principal.role.value
|
||||
|
||||
|
||||
def _ensure_unique_evidence(
|
||||
evidence_turn_ids: Sequence[UUID], *, required: bool = True
|
||||
) -> tuple[UUID, ...]:
|
||||
|
|
|
|||
40
apps/api/app/services/outcome_repository_values.py
Normal file
40
apps/api/app/services/outcome_repository_values.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""결과 저장소가 공유하는 순수 값 정규화 도우미."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from ..deps import Principal, Role
|
||||
|
||||
|
||||
def value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return row[key]
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def canonical_hash(payload: Any) -> str:
|
||||
serialized = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def public_row(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""API 응답에서 학습자 피드백 정책 판정 전용 열을 제거한다."""
|
||||
|
||||
payload = dict(row)
|
||||
payload.pop("source_learner_feedback_enabled", None)
|
||||
return payload
|
||||
|
||||
|
||||
def created_role(principal: Principal) -> str:
|
||||
return "instructor" if principal.role == Role.TEACHER else principal.role.value
|
||||
22
apps/api/app/services/practice_competency.py
Normal file
22
apps/api/app/services/practice_competency.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"""실제 연습과 전이 평가가 공유하는 competency 분류."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def target_techniques(competency_id: str) -> frozenset[str] | None:
|
||||
key = competency_id.lower()
|
||||
if any(token in key for token in ("empathy", "empathic", "reflection")):
|
||||
return frozenset({"empathy", "reflection", "validation", "restatement"})
|
||||
if any(token in key for token in ("open_question", "open-question")):
|
||||
return frozenset({"facilitative_question", "exploration", "clarification"})
|
||||
if any(token in key for token in ("rupture", "repair", "impact")):
|
||||
return frozenset(
|
||||
{"opinion_check", "validation", "reflection", "here_and_now_focus"}
|
||||
)
|
||||
if any(token in key for token in ("goal", "collaborative", "reagreement")):
|
||||
return frozenset(
|
||||
{"consent_motivation_check", "opinion_check", "restatement"}
|
||||
)
|
||||
if any(token in key for token in ("presence", "response-space")):
|
||||
return frozenset({"holding", "reflection", "here_and_now_focus"})
|
||||
return None
|
||||
|
|
@ -19,6 +19,7 @@ from ..contracts.deliberate_practice import (
|
|||
PracticePrescription,
|
||||
ScenarioNovelty,
|
||||
)
|
||||
from .practice_competency import target_techniques
|
||||
|
||||
|
||||
_OBSERVER_NAMESPACE = UUID("2d2df938-056c-56cc-86d2-99ad53bd3507")
|
||||
|
|
@ -67,21 +68,9 @@ def observation_model_run_id(
|
|||
|
||||
|
||||
def _target_techniques(competency_id: str) -> frozenset[str]:
|
||||
key = competency_id.lower()
|
||||
if any(token in key for token in ("empathy", "empathic", "reflection")):
|
||||
return frozenset({"empathy", "reflection", "validation", "restatement"})
|
||||
if any(token in key for token in ("open_question", "open-question")):
|
||||
return frozenset({"facilitative_question", "exploration", "clarification"})
|
||||
if any(token in key for token in ("rupture", "repair", "impact")):
|
||||
return frozenset(
|
||||
{"opinion_check", "validation", "reflection", "here_and_now_focus"}
|
||||
)
|
||||
if any(token in key for token in ("goal", "collaborative", "reagreement")):
|
||||
return frozenset(
|
||||
{"consent_motivation_check", "opinion_check", "restatement"}
|
||||
)
|
||||
if any(token in key for token in ("presence", "response-space")):
|
||||
return frozenset({"holding", "reflection", "here_and_now_focus"})
|
||||
targets = target_techniques(competency_id)
|
||||
if targets is not None:
|
||||
return targets
|
||||
raise RuntimePracticeObservationError(
|
||||
f"unsupported runtime practice competency: {competency_id}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -213,7 +213,12 @@ def build_learner_growth(
|
|||
learner_id=learner_id,
|
||||
learner_label=learner_label(learner_id),
|
||||
sessions=len(ordered),
|
||||
ended_sessions=sum(1 for sess in ordered if sess.ended),
|
||||
ended_sessions=sum(
|
||||
1
|
||||
for sess in ordered
|
||||
if sess.ended
|
||||
and any(t.speaker in ("counselor", "learner") for t in sess.turns)
|
||||
),
|
||||
latest_at=iso_datetime(session_activity_time(latest_session)) or "",
|
||||
first_score=first_score,
|
||||
latest_score=latest_score,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid5
|
||||
|
|
@ -23,6 +21,10 @@ from .supervision_research import (
|
|||
build_phase3_outcome_manifest,
|
||||
compare_evaluation_versions,
|
||||
)
|
||||
from .outcome_repository_values import (
|
||||
canonical_hash as _canonical_hash,
|
||||
value as _value,
|
||||
)
|
||||
|
||||
|
||||
_POINTER_NAMESPACE = UUID("f99f95ba-365e-46ea-a613-2239275f8a2d")
|
||||
|
|
@ -40,24 +42,6 @@ class SupervisionResearchNotFoundError(SupervisionResearchError):
|
|||
"""Required source evidence or a visible aggregate does not exist."""
|
||||
|
||||
|
||||
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return row[key]
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _canonical_hash(payload: Any) -> str:
|
||||
serialized = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
async def _existing_submission(
|
||||
conn: asyncpg.Connection,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,29 @@ class StreamingTranscriptEvent:
|
|||
confidence: float | None = None
|
||||
|
||||
|
||||
def _streaming_provider_event(
|
||||
provider: str,
|
||||
transcript_event: StreamingTranscriptEvent,
|
||||
*,
|
||||
start: float,
|
||||
duration: float,
|
||||
) -> dict[str, object]:
|
||||
event_type = "speech_final" if transcript_event.speech_final else (
|
||||
"speech_end" if transcript_event.final else "voice_activity"
|
||||
)
|
||||
provider_event: dict[str, object] = {
|
||||
"type": event_type,
|
||||
"provider": provider,
|
||||
"source": "streaming_stt",
|
||||
"start_ms": round(start * 1000),
|
||||
"duration_ms": round(duration * 1000),
|
||||
"is_final": transcript_event.final,
|
||||
}
|
||||
if transcript_event.confidence is not None:
|
||||
provider_event["confidence"] = transcript_event.confidence
|
||||
return provider_event
|
||||
|
||||
|
||||
class DeepgramStreamingSession:
|
||||
"""One Deepgram Listen WebSocket, scoped to exactly one learner utterance."""
|
||||
|
||||
|
|
@ -364,29 +387,21 @@ class DeepgramStreamingSession:
|
|||
if not display_text and not speech_final:
|
||||
return
|
||||
|
||||
event_type = "speech_final" if speech_final else (
|
||||
"speech_end" if is_final else "voice_activity"
|
||||
transcript_event = StreamingTranscriptEvent(
|
||||
text=display_text,
|
||||
final=is_final,
|
||||
speech_final=speech_final,
|
||||
confidence=confidence,
|
||||
)
|
||||
provider_event = _streaming_provider_event(
|
||||
"deepgram",
|
||||
transcript_event,
|
||||
start=start,
|
||||
duration=duration,
|
||||
)
|
||||
provider_event: dict[str, object] = {
|
||||
"type": event_type,
|
||||
"provider": "deepgram",
|
||||
"source": "streaming_stt",
|
||||
"start_ms": round(start * 1000),
|
||||
"duration_ms": round(duration * 1000),
|
||||
"is_final": is_final,
|
||||
}
|
||||
if confidence is not None:
|
||||
provider_event["confidence"] = confidence
|
||||
if is_final or speech_final:
|
||||
self._provider_events.append(provider_event)
|
||||
await self._on_event(
|
||||
StreamingTranscriptEvent(
|
||||
text=display_text,
|
||||
final=is_final,
|
||||
speech_final=speech_final,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
await self._on_event(transcript_event)
|
||||
|
||||
def _consume_final_words(self, value: object) -> None:
|
||||
if not isinstance(value, list):
|
||||
|
|
@ -620,29 +635,21 @@ class LocalWhisperStreamingSession:
|
|||
if not display_text and not speech_final:
|
||||
return
|
||||
|
||||
event_type = "speech_final" if speech_final else (
|
||||
"speech_end" if is_final else "voice_activity"
|
||||
transcript_event = StreamingTranscriptEvent(
|
||||
text=display_text,
|
||||
final=is_final,
|
||||
speech_final=speech_final,
|
||||
confidence=confidence,
|
||||
)
|
||||
provider_event = _streaming_provider_event(
|
||||
"local_whisper",
|
||||
transcript_event,
|
||||
start=start,
|
||||
duration=duration,
|
||||
)
|
||||
provider_event: dict[str, object] = {
|
||||
"type": event_type,
|
||||
"provider": "local_whisper",
|
||||
"source": "streaming_stt",
|
||||
"start_ms": round(start * 1000),
|
||||
"duration_ms": round(duration * 1000),
|
||||
"is_final": is_final,
|
||||
}
|
||||
if confidence is not None:
|
||||
provider_event["confidence"] = confidence
|
||||
if is_final or speech_final:
|
||||
self._provider_events.append(provider_event)
|
||||
await self._on_event(
|
||||
StreamingTranscriptEvent(
|
||||
text=display_text,
|
||||
final=is_final,
|
||||
speech_final=speech_final,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
await self._on_event(transcript_event)
|
||||
|
||||
def _consume_final_words(self, value: object, *, offset: float) -> None:
|
||||
if not isinstance(value, list):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue