회기 무발화 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
|
|
@ -779,6 +779,24 @@ async def _runtime_health_metrics(*, db_ok: bool) -> RuntimeHealthMetrics:
|
|||
return metrics
|
||||
|
||||
|
||||
def _usage_daily_cost(
|
||||
daily_buckets: dict[str, dict[str, int | float | list[UsageCostBasis]]],
|
||||
) -> list[AdminUsageDailyCost]:
|
||||
return [
|
||||
AdminUsageDailyCost(
|
||||
day=day,
|
||||
turns=int(values["turns"]),
|
||||
tokens_in=int(values["tokens_in"]),
|
||||
tokens_out=int(values["tokens_out"]),
|
||||
cost_usd=round(float(values["cost_usd"]), 6),
|
||||
cost_basis=_aggregate_cost_basis(
|
||||
cast(list[UsageCostBasis], values["cost_bases"])
|
||||
),
|
||||
)
|
||||
for day, values in sorted(daily_buckets.items())
|
||||
]
|
||||
|
||||
|
||||
async def _usage_from_database(window_days: int) -> AdminUsageResponse:
|
||||
async with acquire(role="admin") as conn:
|
||||
total_row = await conn.fetchrow(
|
||||
|
|
@ -916,19 +934,7 @@ async def _usage_from_database(window_days: int) -> AdminUsageResponse:
|
|||
budget=_usage_budget(total_cost, total_cost_basis),
|
||||
evaluator_cache=_usage_evaluator_cache(),
|
||||
by_provider=all_breakdowns[:12],
|
||||
daily_cost=[
|
||||
AdminUsageDailyCost(
|
||||
day=day,
|
||||
turns=int(values["turns"]),
|
||||
tokens_in=int(values["tokens_in"]),
|
||||
tokens_out=int(values["tokens_out"]),
|
||||
cost_usd=round(float(values["cost_usd"]), 6),
|
||||
cost_basis=_aggregate_cost_basis(
|
||||
cast(list[UsageCostBasis], values["cost_bases"])
|
||||
),
|
||||
)
|
||||
for day, values in sorted(daily_buckets.items())
|
||||
],
|
||||
daily_cost=_usage_daily_cost(daily_buckets),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1321,19 +1327,7 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
budget=_usage_budget(total_cost, total_cost_basis),
|
||||
evaluator_cache=_usage_evaluator_cache(),
|
||||
by_provider=by_provider[:12],
|
||||
daily_cost=[
|
||||
AdminUsageDailyCost(
|
||||
day=day,
|
||||
turns=int(values["turns"]),
|
||||
tokens_in=int(values["tokens_in"]),
|
||||
tokens_out=int(values["tokens_out"]),
|
||||
cost_usd=round(float(values["cost_usd"]), 6),
|
||||
cost_basis=_aggregate_cost_basis(
|
||||
cast(list[UsageCostBasis], values["cost_bases"])
|
||||
),
|
||||
)
|
||||
for day, values in sorted(daily_buckets.items())
|
||||
],
|
||||
daily_cost=_usage_daily_cost(daily_buckets),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from typing import Annotated, Any, Optional
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .. import session_persistence
|
||||
from .. import session_evaluation_repository, session_persistence
|
||||
from ..deps import Principal, Role, require_role
|
||||
from ..engine_client import EngineError, engine_client
|
||||
from ..runtime_policy import runtime_fallback_allowed
|
||||
|
|
@ -167,7 +167,7 @@ async def reevaluate_session(
|
|||
)
|
||||
except EngineError as e:
|
||||
detail = f"engine unavailable: {e}"
|
||||
write = session_persistence.SessionEvaluationWrite.from_error(
|
||||
write = session_evaluation_repository.SessionEvaluationWrite.from_error(
|
||||
session_id=session_id,
|
||||
learner_id=sess.learner_id,
|
||||
scope=body.scope if body.scope in ("session_end", "stage_transition") else "session_end",
|
||||
|
|
@ -176,7 +176,7 @@ async def reevaluate_session(
|
|||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
)
|
||||
saved = await session_persistence.save_session_evaluation(write)
|
||||
saved = await session_evaluation_repository.save_session_evaluation(write)
|
||||
if not saved:
|
||||
logger.error(
|
||||
"session evaluation retry error record did not reach durable store: session_id=%s error=%s",
|
||||
|
|
@ -188,14 +188,14 @@ async def reevaluate_session(
|
|||
detail=_safe_session_evaluation_retry_detail(),
|
||||
)
|
||||
|
||||
write = session_persistence.SessionEvaluationWrite.from_result(
|
||||
write = session_evaluation_repository.SessionEvaluationWrite.from_result(
|
||||
session_id=session_id,
|
||||
learner_id=sess.learner_id,
|
||||
result=result,
|
||||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
)
|
||||
saved = await session_persistence.save_session_evaluation(write)
|
||||
saved = await session_evaluation_repository.save_session_evaluation(write)
|
||||
if not saved:
|
||||
detail = "session evaluation retry result was generated but could not be saved"
|
||||
logger.error("%s: session_id=%s status=%s", detail, session_id, write.status)
|
||||
|
|
@ -302,7 +302,7 @@ async def get_session_evaluation(
|
|||
아직 평가 트리거가 없었다면 deep=None + 빈 분포.
|
||||
"""
|
||||
await _load_session_or_404(session_id, principal)
|
||||
record, durable = await session_persistence.load_session_evaluation(session_id, principal)
|
||||
record, durable = await session_evaluation_repository.load_session_evaluation(session_id, principal)
|
||||
if record is None:
|
||||
return EvaluationSummary(
|
||||
session_id=session_id,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from fastapi import APIRouter, HTTPException, Request, status
|
|||
from pydantic import BaseModel, Field, field_validator
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from .. import db, session_persistence, turn_runtime
|
||||
from .. import db, session_evaluation_repository, session_persistence, turn_runtime
|
||||
from ..auth_sessions import user_has_consent, user_onboarding_complete
|
||||
from ..config import settings
|
||||
from ..deps import CurrentPrincipal, Principal, Role
|
||||
|
|
@ -29,6 +29,7 @@ from ..engine_client import EngineError, engine_client
|
|||
from ..persona_repository import get_catalog_persona
|
||||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..session_evaluation_input import enriched_masked_turns
|
||||
from ..session_turn_memory import build_turn_memory
|
||||
from ..session_evaluation_timeout import (
|
||||
session_evaluation_outer_timeout_seconds,
|
||||
session_evaluation_timeout_seconds as _session_evaluation_timeout_seconds,
|
||||
|
|
@ -50,15 +51,28 @@ from ..services import (
|
|||
session_learning_producer,
|
||||
state_machine,
|
||||
)
|
||||
from ..session_dashboard_projection import (
|
||||
LearnerDashboardResponse,
|
||||
dashboard_achievements as _dashboard_achievements,
|
||||
dashboard_feedback as _dashboard_feedback,
|
||||
dashboard_growth as _dashboard_growth,
|
||||
dashboard_overview as _dashboard_overview,
|
||||
dashboard_persona_progress as _dashboard_persona_progress,
|
||||
dashboard_training_exposure as _dashboard_training_exposure,
|
||||
)
|
||||
from ..session_projection import (
|
||||
LEARNER_VISIBLE_AI_ROLE,
|
||||
iso as _iso,
|
||||
learner_visible_turns as _learner_visible_turns,
|
||||
stage_label as _stage_label,
|
||||
)
|
||||
from ..session_read_model import (
|
||||
CaseMemoryPreview,
|
||||
CaseProgressStats,
|
||||
LearnerCaseListResponse,
|
||||
LearnerCaseSummary,
|
||||
LearnerDashboardResponse,
|
||||
LearnerSessionsResponse,
|
||||
LearnerSessionSummary,
|
||||
LEARNER_VISIBLE_AI_ROLE,
|
||||
ReviewCaseWorksheet,
|
||||
ReviewCaseWorksheetSaveRequest,
|
||||
ReviewWorksheetItem as ReviewWorksheetItem,
|
||||
|
|
@ -73,18 +87,9 @@ from ..session_read_model import (
|
|||
StageLabel,
|
||||
build_session_progress,
|
||||
build_session_review,
|
||||
dashboard_achievements as _dashboard_achievements,
|
||||
dashboard_feedback as _dashboard_feedback,
|
||||
dashboard_growth as _dashboard_growth,
|
||||
dashboard_overview as _dashboard_overview,
|
||||
dashboard_persona_progress as _dashboard_persona_progress,
|
||||
dashboard_training_exposure as _dashboard_training_exposure,
|
||||
iso as _iso,
|
||||
learner_summary as _learner_summary,
|
||||
learner_visible_turns as _learner_visible_turns,
|
||||
session_detail as _session_detail,
|
||||
session_share_payload as _session_share_payload,
|
||||
stage_label as _stage_label,
|
||||
)
|
||||
from ..store import InProcSession, TurnRecord, store
|
||||
|
||||
|
|
@ -548,12 +553,7 @@ async def _prepare_turn_context(
|
|||
state=sess.state,
|
||||
learner_text=learner_text,
|
||||
learner_identity=sess.learner_label,
|
||||
memory=orchestrator.TurnMemory(
|
||||
recall_summary=recall.recall_summary,
|
||||
pinned_facts=recall.pinned_facts,
|
||||
recent_turns=sess.recent_turns(visible_to="client"),
|
||||
kb_behavior_cues=kb_cues,
|
||||
),
|
||||
memory=build_turn_memory(sess, recall, kb_cues),
|
||||
theory_mode=sess.theory_mode,
|
||||
scenario_context=scenario_context,
|
||||
)
|
||||
|
|
@ -956,14 +956,14 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
|
|||
# 예산 뒤에 outer grace를 두어 정상 결과를 timeout error로 바꾸지 않는다.
|
||||
timeout=session_evaluation_outer_timeout_seconds(),
|
||||
)
|
||||
write = session_persistence.SessionEvaluationWrite.from_result(
|
||||
write = session_evaluation_repository.SessionEvaluationWrite.from_result(
|
||||
session_id=sess.session_id,
|
||||
learner_id=sess.learner_id,
|
||||
result=result,
|
||||
counselor_identity=sess.learner_label,
|
||||
client_identity=sess.persona.display_name,
|
||||
)
|
||||
saved = await session_persistence.save_session_evaluation(write)
|
||||
saved = await session_evaluation_repository.save_session_evaluation(write)
|
||||
if not saved:
|
||||
logger.error(
|
||||
"session evaluation save did not reach durable store: session_id=%s status=%s scope=%s",
|
||||
|
|
@ -988,7 +988,7 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
|
|||
except asyncio.TimeoutError:
|
||||
message = f"session evaluation timeout after {timeout_seconds:g}s"
|
||||
logger.exception("%s: session_id=%s", message, sess.session_id)
|
||||
write = session_persistence.SessionEvaluationWrite.from_error(
|
||||
write = session_evaluation_repository.SessionEvaluationWrite.from_error(
|
||||
session_id=sess.session_id,
|
||||
learner_id=sess.learner_id,
|
||||
scope="session_end",
|
||||
|
|
@ -997,7 +997,7 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
|
|||
counselor_identity=sess.learner_label,
|
||||
client_identity=sess.persona.display_name,
|
||||
)
|
||||
saved = await session_persistence.save_session_evaluation(write)
|
||||
saved = await session_evaluation_repository.save_session_evaluation(write)
|
||||
if not saved:
|
||||
logger.error(
|
||||
"session evaluation error save did not reach durable store: session_id=%s error=%s",
|
||||
|
|
@ -1008,7 +1008,7 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
|
|||
await _enqueue_session_review_ready_notification(sess.session_id)
|
||||
except Exception as exc:
|
||||
logger.exception("session evaluation failed: session_id=%s", sess.session_id)
|
||||
write = session_persistence.SessionEvaluationWrite.from_error(
|
||||
write = session_evaluation_repository.SessionEvaluationWrite.from_error(
|
||||
session_id=sess.session_id,
|
||||
learner_id=sess.learner_id,
|
||||
scope="session_end",
|
||||
|
|
@ -1017,7 +1017,7 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
|
|||
counselor_identity=sess.learner_label,
|
||||
client_identity=sess.persona.display_name,
|
||||
)
|
||||
saved = await session_persistence.save_session_evaluation(write)
|
||||
saved = await session_evaluation_repository.save_session_evaluation(write)
|
||||
if not saved:
|
||||
logger.error(
|
||||
"session evaluation failure record did not reach durable store: session_id=%s error=%s",
|
||||
|
|
@ -1162,7 +1162,7 @@ async def _review_ready(sess: InProcSession, principal: Principal) -> bool:
|
|||
return False
|
||||
if len(turns) != len(sess.turns):
|
||||
return False
|
||||
evaluation_record, _ = await session_persistence.load_session_evaluation(
|
||||
evaluation_record, _ = await session_evaluation_repository.load_session_evaluation(
|
||||
sess.session_id,
|
||||
principal,
|
||||
)
|
||||
|
|
@ -1774,7 +1774,7 @@ async def get_session_review(
|
|||
(
|
||||
evaluation_record,
|
||||
evaluation_durable,
|
||||
) = await session_persistence.load_session_evaluation(
|
||||
) = await session_evaluation_repository.load_session_evaluation(
|
||||
session_id,
|
||||
review_principal,
|
||||
)
|
||||
|
|
@ -2245,7 +2245,8 @@ async def end_session(
|
|||
await _end_persisted_session(sess, carry)
|
||||
invalidate_session_context_cache(session_id)
|
||||
rupture_runtime.schedule_session_scan(session_id, trigger="session_ended")
|
||||
if not was_ended:
|
||||
has_counselor_turns = any(t.speaker in ("counselor", "learner") for t in sess.turns)
|
||||
if not was_ended and has_counselor_turns:
|
||||
_schedule_session_evaluation(sess)
|
||||
|
||||
return SessionEndResponse(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from typing import Annotated, Literal
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .. import session_persistence
|
||||
from .. import session_evaluation_repository, session_persistence
|
||||
from ..deps import Principal, Role, require_role
|
||||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..session_read_model import (
|
||||
|
|
@ -350,7 +350,7 @@ async def teacher_dashboard(principal: TeacherPrincipal) -> TeacherDashboardResp
|
|||
ended_session_ids,
|
||||
principal,
|
||||
)
|
||||
evaluation_records, evaluations_durable = await session_persistence.list_session_evaluations(
|
||||
evaluation_records, evaluations_durable = await session_evaluation_repository.list_session_evaluations(
|
||||
ended_session_ids,
|
||||
principal,
|
||||
)
|
||||
|
|
@ -403,7 +403,11 @@ async def teacher_dashboard(principal: TeacherPrincipal) -> TeacherDashboardResp
|
|||
source="database" if durable else "runtime",
|
||||
total_learners=len(learners),
|
||||
active_sessions=sum(1 for sess in sessions if not sess.ended),
|
||||
ended_sessions=sum(1 for sess in sessions if sess.ended),
|
||||
ended_sessions=sum(
|
||||
1
|
||||
for sess in sessions
|
||||
if sess.ended and any(t.speaker in ("counselor", "learner") for t in sess.turns)
|
||||
),
|
||||
safety_alerts=safety_alerts,
|
||||
learner_growth=learner_growth,
|
||||
pending_reviews=pending_reviews[:20],
|
||||
|
|
@ -442,7 +446,7 @@ async def learner_analysis(
|
|||
ended_session_ids,
|
||||
principal,
|
||||
)
|
||||
evaluation_records, evaluations_durable = await session_persistence.list_session_evaluations(
|
||||
evaluation_records, evaluations_durable = await session_evaluation_repository.list_session_evaluations(
|
||||
ended_session_ids,
|
||||
principal,
|
||||
)
|
||||
|
|
@ -462,7 +466,11 @@ async def learner_analysis(
|
|||
learner_label=growth.learner_label,
|
||||
total_sessions=len(ordered),
|
||||
active_sessions=sum(1 for sess in ordered if not sess.ended),
|
||||
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)
|
||||
),
|
||||
pending_reviews=pending_reviews,
|
||||
closed_reviews=closed_reviews,
|
||||
summary=growth,
|
||||
|
|
@ -495,7 +503,7 @@ async def update_session_review_status(
|
|||
detail="active sessions cannot be closed as reviewed",
|
||||
)
|
||||
if sess.ended and request.status == "closed" and learner_visible_turns(sess):
|
||||
evaluation_record, evaluation_durable = await session_persistence.load_session_evaluation(
|
||||
evaluation_record, evaluation_durable = await session_evaluation_repository.load_session_evaluation(
|
||||
session_id,
|
||||
principal,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from ..persona_repository import (
|
|||
get_session_voice_map,
|
||||
)
|
||||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..session_turn_memory import build_turn_memory
|
||||
from ..services import (
|
||||
evaluator,
|
||||
guardrail,
|
||||
|
|
@ -1459,12 +1460,7 @@ async def _prepare_voice_turn_context(
|
|||
state=sess.state,
|
||||
learner_text=learner_text,
|
||||
learner_identity=sess.learner_label,
|
||||
memory=orchestrator.TurnMemory(
|
||||
recall_summary=recall.recall_summary,
|
||||
pinned_facts=recall.pinned_facts,
|
||||
recent_turns=sess.recent_turns(visible_to="client"),
|
||||
kb_behavior_cues=kb_cues,
|
||||
),
|
||||
memory=build_turn_memory(sess, recall, kb_cues),
|
||||
theory_mode=sess.theory_mode,
|
||||
scenario_context=scenario_context,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue