-
Notifications
You must be signed in to change notification settings - Fork 1
feat(dashboard): consume TEPP topic influence evidence #650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
73d037e
8f6fffe
af68b6e
d4b9c9c
a9c25e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| from __future__ import annotations | ||
|
|
||
| from datetime import date | ||
| import json | ||
| from typing import Any, Protocol | ||
|
|
||
| from backend.app.post_eligibility import SOURCE_POST_ELIGIBILITY_SQL | ||
|
|
@@ -106,11 +107,11 @@ def _operations_case_jsonld( | |
| class _Connection(Protocol): | ||
| async def fetchrow(self, query: str, *args: object) -> Any: | ||
| """Fetch one projected row.""" | ||
| pass | ||
| pass # pragma: no cover - structural Protocol member | ||
|
|
||
| async def fetch(self, query: str, *args: object) -> list[Any]: | ||
| """Fetch projected rows.""" | ||
| pass | ||
| pass # pragma: no cover - structural Protocol member | ||
|
|
||
|
|
||
| def _visible_period_sql(alias: str = "post") -> str: | ||
|
|
@@ -250,6 +251,18 @@ async def fetch_operations_dashboard( | |
| """, | ||
| *args, | ||
| ) | ||
| topic_context = ( | ||
| { | ||
| "status_code": "not_applicable", | ||
| "reason_code": "external_information_view", | ||
| "next_action": "전체 Dashboard로 전환해 Topic model influence를 확인하세요.", | ||
| "required_contracts": [], | ||
| "model_run": None, | ||
| "topics": [], | ||
| } | ||
| if external_only | ||
| else await _fetch_topic_context_dashboard(conn, visible, args) | ||
| ) | ||
| facts: dict[tuple[str, str], list[dict[str, str]]] = {} | ||
| for row in fact_rows: | ||
| key = (str(row["post_id"]), row["case_kind_code"]) | ||
|
|
@@ -304,6 +317,7 @@ async def fetch_operations_dashboard( | |
| } | ||
| for kind, label in CASE_KIND_LABELS.items() | ||
| ], | ||
| "topic_context": topic_context, | ||
| "cases": [ | ||
| { | ||
| "post_id": str(row["post_id"]), | ||
|
|
@@ -331,6 +345,271 @@ async def fetch_operations_dashboard( | |
| } | ||
|
|
||
|
|
||
| async def _fetch_topic_context_dashboard( | ||
| conn: _Connection, | ||
| visible_post_sql: str, | ||
| args: tuple[object, ...], | ||
| ) -> dict[str, Any]: | ||
| """Project exact accepted producer rows or an actionable unavailable state.""" | ||
| authorized_model_scope = """ | ||
| ((scope.scope_kind_code = 'analysis_scope_corporate_entity' | ||
| and scope.corporate_entity_id::text = any($1::text[]) | ||
| and cardinality($2::text[]) = 0) | ||
| or | ||
| (scope.scope_kind_code = 'analysis_scope_process_unit' | ||
| and scope.process_unit_id::text = any($2::text[]))) | ||
| """ | ||
| readiness = await conn.fetchrow( | ||
| f""" | ||
| with visible_post as ( | ||
| select post.post_id | ||
| from source_post post | ||
| where {visible_post_sql} | ||
| ) | ||
| select exists ( | ||
| select 1 | ||
| from topic_context_membership membership | ||
| join topic_model_run model | ||
| on model.topic_model_run_id = membership.topic_model_run_id | ||
| join analysis_run analysis on analysis.analysis_run_id = model.analysis_run_id | ||
| join analysis_run_scope scope on scope.analysis_run_id = analysis.analysis_run_id | ||
| join visible_post on visible_post.post_id = membership.source_post_id | ||
| where {authorized_model_scope} | ||
| ) as tepp_posterior_persisted, | ||
| exists ( | ||
| select 1 | ||
| from topic_post_context_influence influence | ||
| join topic_context_membership membership | ||
| on membership.topic_model_run_id = influence.topic_model_run_id | ||
| and membership.topic_context_membership_id = influence.topic_context_membership_id | ||
| join topic_model_run model | ||
| on model.topic_model_run_id = influence.topic_model_run_id | ||
| join analysis_run analysis on analysis.analysis_run_id = model.analysis_run_id | ||
| join analysis_run_scope scope on scope.analysis_run_id = analysis.analysis_run_id | ||
| join visible_post on visible_post.post_id = membership.source_post_id | ||
| where {authorized_model_scope} | ||
| ) as fast_mlsirm_influence_persisted | ||
| """, | ||
| *args, | ||
| ) | ||
|
Comment on lines
+362
to
+394
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Readiness query runs even when unused The Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| rows = await conn.fetch( | ||
| f""" | ||
| with visible_post as ( | ||
| select post.post_id, | ||
| coalesce(post.event_occurred_at, post.created_at) as occurred_at | ||
| from source_post post | ||
| where {visible_post_sql} | ||
| ), eligible as ( | ||
| select model.topic_model_run_id, model.tepp_run_id, model.tepp_snapshot_id, | ||
| model.tepp_schema_version, model.tepp_model_contract_version, | ||
| model.tepp_artifact_sha256, model.posterior_draw_set_id, | ||
| model.posterior_draw_count, model.topic_count, | ||
| snapshot.snapshot_sha256 as source_snapshot_sha256, | ||
| analysis.knowledge_cutoff, | ||
| influence_run.topic_influence_run_id, | ||
| influence_run.fast_mlsirm_schema_version, | ||
| influence_run.fast_mlsirm_version, | ||
| influence_run.fast_mlsirm_code_revision, | ||
| influence_run.fast_mlsirm_artifact_sha256, | ||
| influence_run.compute_backend_code, | ||
| influence_run.precision_code, | ||
| influence_run.membership_fingerprint_sha256, | ||
| influence.topic_index, activity.state_code, | ||
| activity.valid_from as activity_valid_from, | ||
| activity.valid_to as activity_valid_to, | ||
| membership.dimension_code, membership.context_id, | ||
| context.context_label, membership.membership_weight, | ||
| membership.evidence_sha256 as membership_evidence_sha256, | ||
| membership.source_post_id, visible_post.occurred_at, | ||
| influence.influence_value, | ||
| influence.uncertainty_method_code, | ||
| influence.uncertainty_lower_value, | ||
| influence.uncertainty_upper_value, | ||
| influence.diagnostic_status_code, | ||
| influence_run.accepted_at | ||
| from topic_post_context_influence influence | ||
| join topic_influence_run influence_run | ||
| on influence_run.topic_model_run_id = influence.topic_model_run_id | ||
| and influence_run.topic_influence_run_id = influence.topic_influence_run_id | ||
| join topic_model_run model | ||
| on model.topic_model_run_id = influence.topic_model_run_id | ||
| join analysis_run analysis on analysis.analysis_run_id = model.analysis_run_id | ||
| join analysis_run_scope scope on scope.analysis_run_id = analysis.analysis_run_id | ||
| join analysis_source_snapshot snapshot | ||
| on snapshot.analysis_source_snapshot_id = analysis.analysis_source_snapshot_id | ||
| join topic_context_membership membership | ||
| on membership.topic_model_run_id = influence.topic_model_run_id | ||
| and membership.topic_context_membership_id = influence.topic_context_membership_id | ||
| join topic_context_definition context | ||
| on context.topic_model_run_id = membership.topic_model_run_id | ||
| and context.dimension_code = membership.dimension_code | ||
| and context.context_id = membership.context_id | ||
| join visible_post on visible_post.post_id = membership.source_post_id | ||
| join topic_activity_interval activity | ||
| on activity.topic_model_run_id = influence.topic_model_run_id | ||
| and activity.topic_index = influence.topic_index | ||
| and visible_post.occurred_at >= activity.valid_from | ||
| and visible_post.occurred_at < activity.valid_to | ||
|
Comment on lines
+448
to
+452
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Influence rows dropped when no activity interval covers the post The Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| where visible_post.occurred_at >= membership.valid_from | ||
| and visible_post.occurred_at < membership.valid_to | ||
| and {authorized_model_scope} | ||
| ), selected as ( | ||
| select topic_model_run_id, topic_influence_run_id | ||
| from eligible | ||
| order by accepted_at desc, topic_model_run_id, topic_influence_run_id | ||
| limit 1 | ||
| ) | ||
| select eligible.*, | ||
| coalesce(( | ||
| select jsonb_agg(jsonb_build_object( | ||
| 'event_code', relation.event_code, | ||
| 'source_topic_index', relation.source_topic_index, | ||
| 'target_topic_index', relation.target_topic_index, | ||
| 'event_time', relation.event_time, | ||
| 'evidence_sha256', relation.evidence_sha256 | ||
| ) order by relation.event_time, relation.relation_ordinal) | ||
| from topic_lineage_relation relation | ||
| where relation.topic_model_run_id = eligible.topic_model_run_id | ||
| and (relation.source_topic_index = eligible.topic_index | ||
| or relation.target_topic_index = eligible.topic_index) | ||
| ), '[]'::jsonb) as lineage_events | ||
| from eligible | ||
| join selected using (topic_model_run_id, topic_influence_run_id) | ||
| order by eligible.topic_index, | ||
| case eligible.dimension_code | ||
| when 'business_unit' then 0 | ||
| when 'process_unit' then 1 | ||
| when 'team' then 2 | ||
| else 3 | ||
| end, | ||
| eligible.context_label, | ||
| eligible.influence_value desc, | ||
| eligible.occurred_at, | ||
| eligible.source_post_id | ||
| """, | ||
| *args, | ||
| ) | ||
| if not rows: | ||
| tepp_ready = bool(readiness and readiness["tepp_posterior_persisted"]) | ||
| return { | ||
| "status_code": "unavailable", | ||
| "reason_code": ( | ||
| "fast_mlsirm_influence_not_persisted" | ||
| if tepp_ready | ||
| else "tepp_topic_posterior_not_persisted" | ||
| ), | ||
| "next_action": ( | ||
| "동일 TEPP run·snapshot·cutoff에 결합된 fast-mlsirm 결과를 완료하세요." | ||
| if tepp_ready | ||
| else "TEPP posterior topic 계약 결과를 먼저 완료하세요." | ||
| ), | ||
| "required_contracts": [ | ||
| { | ||
| "authority": "TEPP", | ||
| "schema_version": "tepp.topic_context_posterior.v1", | ||
| "state_code": "persisted" if tepp_ready else "not_persisted", | ||
| }, | ||
| { | ||
| "authority": "fast-mlsirm", | ||
| "schema_version": "fast_mlsirm.topic_context_influence.v1", | ||
| "state_code": ( | ||
| "persisted" | ||
| if readiness and readiness["fast_mlsirm_influence_persisted"] | ||
| else "not_persisted" | ||
| ), | ||
| }, | ||
| ], | ||
| "model_run": None, | ||
| "topics": [], | ||
| } | ||
|
|
||
| first = rows[0] | ||
| topics: dict[int, dict[str, Any]] = {} | ||
| for row in rows: | ||
| topic_index = int(row["topic_index"]) | ||
| raw_lineage_events = row["lineage_events"] | ||
| lineage_events = ( | ||
| json.loads(raw_lineage_events) | ||
| if isinstance(raw_lineage_events, str) | ||
| else list(raw_lineage_events) | ||
| ) | ||
| topic = topics.setdefault( | ||
| topic_index, | ||
| { | ||
| "topic_index": topic_index, | ||
| "activity_intervals": [], | ||
| "lineage_events": lineage_events, | ||
| "contexts": [], | ||
| }, | ||
| ) | ||
| interval = { | ||
| "state_code": row["state_code"], | ||
| "valid_from": row["activity_valid_from"].isoformat(), | ||
| "valid_to": row["activity_valid_to"].isoformat(), | ||
| } | ||
| if interval not in topic["activity_intervals"]: | ||
| topic["activity_intervals"].append(interval) | ||
| context_key = (row["dimension_code"], row["context_id"]) | ||
| context = next( | ||
| ( | ||
| item | ||
| for item in topic["contexts"] | ||
| if (item["dimension_code"], item["context_id"]) == context_key | ||
| ), | ||
| None, | ||
| ) | ||
| if context is None: | ||
| context = { | ||
| "dimension_code": row["dimension_code"], | ||
| "context_id": row["context_id"], | ||
| "context_label": row["context_label"], | ||
| "influences": [], | ||
| } | ||
| topic["contexts"].append(context) | ||
| context["influences"].append( | ||
| { | ||
| "post_id": str(row["source_post_id"]), | ||
| "occurred_at": row["occurred_at"].isoformat(), | ||
| "topic_state_code": row["state_code"], | ||
| "model_influence": float(row["influence_value"]), | ||
| "uncertainty_method_code": row["uncertainty_method_code"], | ||
| "uncertainty_lower_value": float(row["uncertainty_lower_value"]), | ||
| "uncertainty_upper_value": float(row["uncertainty_upper_value"]), | ||
| "diagnostic_status_code": row["diagnostic_status_code"], | ||
| "membership_weight": float(row["membership_weight"]), | ||
| "membership_evidence_sha256": row["membership_evidence_sha256"], | ||
| } | ||
| ) | ||
|
|
||
| return { | ||
| "status_code": "accepted", | ||
| "reason_code": None, | ||
| "next_action": "Topic과 조직 수준을 선택해 model influence와 근거 글을 확인하세요.", | ||
| "required_contracts": [ | ||
| {"authority": "TEPP", "schema_version": first["tepp_schema_version"], "state_code": "persisted"}, | ||
| {"authority": "fast-mlsirm", "schema_version": first["fast_mlsirm_schema_version"], "state_code": "persisted"}, | ||
| ], | ||
| "model_run": { | ||
| "tepp_run_id": first["tepp_run_id"], | ||
| "tepp_snapshot_id": first["tepp_snapshot_id"], | ||
| "source_snapshot_sha256": first["source_snapshot_sha256"], | ||
| "knowledge_cutoff": first["knowledge_cutoff"].isoformat(), | ||
| "tepp_model_contract_version": first["tepp_model_contract_version"], | ||
| "tepp_artifact_sha256": first["tepp_artifact_sha256"], | ||
| "posterior_draw_set_id": first["posterior_draw_set_id"], | ||
| "posterior_draw_count": int(first["posterior_draw_count"]), | ||
| "topic_count": int(first["topic_count"]), | ||
| "fast_mlsirm_version": first["fast_mlsirm_version"], | ||
| "fast_mlsirm_code_revision": first["fast_mlsirm_code_revision"], | ||
| "fast_mlsirm_artifact_sha256": first["fast_mlsirm_artifact_sha256"], | ||
| "compute_backend_code": first["compute_backend_code"], | ||
| "precision_code": first["precision_code"], | ||
| "membership_fingerprint_sha256": first["membership_fingerprint_sha256"], | ||
| }, | ||
| "topics": list(topics.values()), | ||
| } | ||
|
|
||
|
|
||
| def _period_label(period_start: date | None, period_end: date | None) -> str: | ||
| """Format the exact event-time interval represented by the projection.""" | ||
| if period_start and period_end: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Dashboard crashes on every non-external request
_fetch_topic_context_dashboardpasses all five query parameters (*args) to two SQL statements that reference only$1–$4; the fifth (external_only) is unused. asyncpg rejects the extra argument, so every non-external dashboard request fails. The mock-connection tests never bind real parameters, so CI stays green.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.