feat(learning): chair-feedback style corrections auto-flow to writer (P1 #6) #344
@@ -2875,6 +2875,48 @@ async def get_style_patterns(pattern_type: str | None = None) -> list[dict]:
|
|||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def append_global_rule(
|
||||||
|
category: str, key: str, items: list[str], seed_if_missing: list | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""Append items to a _global appeal_type_rules list value (the writer-consumed
|
||||||
|
methodology channel), idempotently and under a row lock. Returns how many NEW
|
||||||
|
items were added (existing duplicates are skipped). Single locked read-modify-
|
||||||
|
write (MET-2/3) so concurrent appends can't drop items. Shared by the /training
|
||||||
|
promote gate (web `_append_methodology_override`) and chair-feedback auto-flow
|
||||||
|
(G2 — one append implementation)."""
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"SELECT rule_value FROM appeal_type_rules "
|
||||||
|
"WHERE appeal_type = '_global' AND rule_category = $1 AND rule_key = $2 "
|
||||||
|
"FOR UPDATE",
|
||||||
|
category, key,
|
||||||
|
)
|
||||||
|
if row:
|
||||||
|
current = row["rule_value"]
|
||||||
|
if isinstance(current, str):
|
||||||
|
try:
|
||||||
|
current = json.loads(current)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
current = []
|
||||||
|
else:
|
||||||
|
current = list(seed_if_missing or [])
|
||||||
|
if not isinstance(current, list):
|
||||||
|
current = []
|
||||||
|
added = [s for s in items if s and s not in current]
|
||||||
|
if not added and row:
|
||||||
|
return 0
|
||||||
|
merged = current + added
|
||||||
|
await conn.execute(
|
||||||
|
"INSERT INTO appeal_type_rules (id, appeal_type, rule_category, rule_key, rule_value) "
|
||||||
|
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::text::jsonb) "
|
||||||
|
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::text::jsonb",
|
||||||
|
category, key, json.dumps(merged, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
return len(added)
|
||||||
|
|
||||||
|
|
||||||
async def get_methodology_overrides(category: str) -> dict:
|
async def get_methodology_overrides(category: str) -> dict:
|
||||||
"""Chair's /methodology edits for one category (golden_ratios / discussion_rules /
|
"""Chair's /methodology edits for one category (golden_ratios / discussion_rules /
|
||||||
content_checklists). Returns {rule_key: parsed_value}. These OVERRIDE the hardcoded
|
content_checklists). Returns {rule_key: parsed_value}. These OVERRIDE the hardcoded
|
||||||
|
|||||||
@@ -394,13 +394,35 @@ async def record_chair_feedback(
|
|||||||
lesson_extracted=lesson_extracted,
|
lesson_extracted=lesson_extracted,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Auto-flow chair-authored STYLE feedback to the writer (closes the dead
|
||||||
|
# chair_feedback→lesson chain — 27 feedback rows had produced 0 lessons). The
|
||||||
|
# chair is the highest authority, so a style correction she writes flows
|
||||||
|
# immediately — the chair IS the gate. It rides the SAME discussion_rules channel
|
||||||
|
# promote uses (db.append_global_rule, G2), reaching every block, without the
|
||||||
|
# style_corpus coupling decision_lessons require. SUBSTANCE feedback
|
||||||
|
# (missing_content/factual_error/other) is case-specific → recorded only.
|
||||||
|
# (INV-LRN1 graduated gate; 07-learning §1.2.)
|
||||||
|
_STYLE_FB = {"style", "wrong_tone", "wrong_structure"}
|
||||||
|
flowed = 0
|
||||||
|
if lesson_extracted.strip() and category in _STYLE_FB:
|
||||||
|
try:
|
||||||
|
flowed = await db.append_global_rule(
|
||||||
|
"discussion_rules", "universal", [lesson_extracted.strip()],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("chair-feedback auto-flow failed for %s: %s", case_number, e)
|
||||||
|
|
||||||
|
msg = f"הערה נרשמה בהצלחה. קטגוריה: {category}."
|
||||||
|
if flowed:
|
||||||
|
msg += " הלקח (סגנון) זרם אוטומטית לכותב."
|
||||||
return ok({
|
return ok({
|
||||||
"feedback_id": str(feedback_id),
|
"feedback_id": str(feedback_id),
|
||||||
|
"flowed_to_writer": bool(flowed),
|
||||||
"next_steps": [
|
"next_steps": [
|
||||||
"כדי להפיק לקח מההערה, הפעל: analyze_chair_feedback",
|
"כדי להפיק לקח מההערה, הפעל: analyze_chair_feedback",
|
||||||
"כדי לסמן כמטופל: resolve_chair_feedback",
|
"כדי לסמן כמטופל: resolve_chair_feedback",
|
||||||
],
|
],
|
||||||
}, message=f"הערה נרשמה בהצלחה. קטגוריה: {category}.")
|
}, message=msg)
|
||||||
|
|
||||||
|
|
||||||
_CURATOR_FINDING_CATEGORIES = {"style", "structure", "lexicon", "tabular", "general"}
|
_CURATOR_FINDING_CATEGORIES = {"style", "structure", "lexicon", "tabular", "general"}
|
||||||
|
|||||||
35
web/app.py
35
web/app.py
@@ -4906,35 +4906,12 @@ class PromoteLearningRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
async def _append_methodology_override(category: str, key: str, items: list[str]) -> None:
|
async def _append_methodology_override(category: str, key: str, items: list[str]) -> None:
|
||||||
"""Read current (override-or-default) list value, append new items, upsert override.
|
"""Thin wrapper over db.append_global_rule (the single locked append impl, G2) —
|
||||||
Shared by the T14 approval gate to fold approved learnings into writer-consumed channels.
|
seeds from _METHODOLOGY_DEFAULTS when no override row exists yet. Shared by the
|
||||||
|
T14 promote gate; chair-feedback auto-flow calls db.append_global_rule directly."""
|
||||||
MET-2/3 (INV-IA3): the read-modify-write runs inside ONE transaction with the
|
await db.append_global_rule(
|
||||||
existing override row locked FOR UPDATE, so a concurrent promote (or a methodology
|
category, key, items,
|
||||||
PUT) can't interleave between the read and the write and silently drop items.
|
seed_if_missing=list(_METHODOLOGY_DEFAULTS.get(category, {}).get(key, [])),
|
||||||
The methodology PUT overwrites the same row; the MET-1 invalidation (גל-1) makes
|
|
||||||
the /methodology editor refetch on promote so it edits post-append state."""
|
|
||||||
pool = await db.get_pool()
|
|
||||||
async with pool.acquire() as conn:
|
|
||||||
async with conn.transaction():
|
|
||||||
row = await conn.fetchrow(
|
|
||||||
"SELECT rule_value FROM appeal_type_rules "
|
|
||||||
"WHERE appeal_type = '_global' AND rule_category = $1 AND rule_key = $2 "
|
|
||||||
"FOR UPDATE",
|
|
||||||
category, key,
|
|
||||||
)
|
|
||||||
if row:
|
|
||||||
current = _coerce_json(row["rule_value"]) or []
|
|
||||||
else:
|
|
||||||
current = list(_METHODOLOGY_DEFAULTS.get(category, {}).get(key, []))
|
|
||||||
if not isinstance(current, list):
|
|
||||||
current = []
|
|
||||||
merged = current + [s for s in items if s and s not in current]
|
|
||||||
await conn.execute(
|
|
||||||
"INSERT INTO appeal_type_rules (id, appeal_type, rule_category, rule_key, rule_value) "
|
|
||||||
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::text::jsonb) "
|
|
||||||
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::text::jsonb",
|
|
||||||
category, key, json.dumps(merged, ensure_ascii=False),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user