agromatrix: invalidate wrong photo labels and tighten correction parsing
This commit is contained in:
@@ -165,8 +165,6 @@ def _extract_agromatrix_correction_label(text: str) -> Optional[str]:
|
|||||||
patterns = [
|
patterns = [
|
||||||
r"правильн\w*\s+відповід\w*[:\-]?\s*([a-zа-яіїєґ0-9'’\-\s]{2,60})",
|
r"правильн\w*\s+відповід\w*[:\-]?\s*([a-zа-яіїєґ0-9'’\-\s]{2,60})",
|
||||||
r"це\s+не\s+[a-zа-яіїєґ0-9'’\-\s]{1,60},?\s+а\s+([a-zа-яіїєґ0-9'’\-\s]{2,60})",
|
r"це\s+не\s+[a-zа-яіїєґ0-9'’\-\s]{1,60},?\s+а\s+([a-zа-яіїєґ0-9'’\-\s]{2,60})",
|
||||||
# Strict "це <label>" form, but never "це не ...".
|
|
||||||
r"це\s+(?!не\b)([a-zа-яіїєґ0-9'’\-\s]{2,40})",
|
|
||||||
]
|
]
|
||||||
for pat in patterns:
|
for pat in patterns:
|
||||||
m = re.search(pat, t)
|
m = re.search(pat, t)
|
||||||
@@ -194,6 +192,25 @@ def _extract_agromatrix_correction_label(text: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_agromatrix_negative_feedback(text: str) -> bool:
|
||||||
|
t = (text or "").strip().lower()
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
markers = (
|
||||||
|
"це помилка",
|
||||||
|
"це не вірно",
|
||||||
|
"це невірно",
|
||||||
|
"неправильно",
|
||||||
|
"не вірно",
|
||||||
|
"невірно",
|
||||||
|
"не так",
|
||||||
|
"помилка у відповіді",
|
||||||
|
"відповідь не вірна",
|
||||||
|
"відповідь невірна",
|
||||||
|
)
|
||||||
|
return any(m in t for m in markers)
|
||||||
|
|
||||||
|
|
||||||
def _is_agromatrix_correction_only_message(text: str) -> bool:
|
def _is_agromatrix_correction_only_message(text: str) -> bool:
|
||||||
t = (text or "").strip().lower()
|
t = (text or "").strip().lower()
|
||||||
if not t:
|
if not t:
|
||||||
@@ -286,6 +303,31 @@ async def _save_agromatrix_photo_learning(
|
|||||||
logger.warning(f"AgroMatrix photo learning ingest failed: {e}")
|
logger.warning(f"AgroMatrix photo learning ingest failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _invalidate_agromatrix_photo_learning(
|
||||||
|
*,
|
||||||
|
file_id: str,
|
||||||
|
reason: str,
|
||||||
|
dao_id: str,
|
||||||
|
) -> None:
|
||||||
|
if not file_id:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await memory_client.upsert_fact(
|
||||||
|
user_id=AGROMATRIX_GLOBAL_KNOWLEDGE_USER_ID,
|
||||||
|
fact_key=f"agromatrix:photo_label:{file_id}",
|
||||||
|
fact_value="",
|
||||||
|
fact_value_json={
|
||||||
|
"label": "",
|
||||||
|
"invalidated": True,
|
||||||
|
"reason": reason,
|
||||||
|
"updated_at": datetime.utcnow().isoformat(),
|
||||||
|
},
|
||||||
|
team_id=dao_id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"AgroMatrix photo learning invalidation failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def _get_agromatrix_photo_prior(file_id: str, dao_id: str) -> Optional[str]:
|
async def _get_agromatrix_photo_prior(file_id: str, dao_id: str) -> Optional[str]:
|
||||||
if not file_id:
|
if not file_id:
|
||||||
return None
|
return None
|
||||||
@@ -299,6 +341,8 @@ async def _get_agromatrix_photo_prior(file_id: str, dao_id: str) -> Optional[str
|
|||||||
return None
|
return None
|
||||||
data = fact.get("fact_value_json") if isinstance(fact, dict) else None
|
data = fact.get("fact_value_json") if isinstance(fact, dict) else None
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
|
if bool(data.get("invalidated")):
|
||||||
|
return None
|
||||||
label = str(data.get("label") or "").strip()
|
label = str(data.get("label") or "").strip()
|
||||||
if label:
|
if label:
|
||||||
return label
|
return label
|
||||||
@@ -2795,54 +2839,127 @@ async def handle_telegram_webhook(
|
|||||||
# AgroMatrix: capture user correction for latest photo and persist anonymized learning.
|
# AgroMatrix: capture user correction for latest photo and persist anonymized learning.
|
||||||
if agent_config.agent_id == "agromatrix" and text:
|
if agent_config.agent_id == "agromatrix" and text:
|
||||||
corrected_label = _extract_agromatrix_correction_label(text)
|
corrected_label = _extract_agromatrix_correction_label(text)
|
||||||
if corrected_label:
|
negative_feedback = _is_agromatrix_negative_feedback(text)
|
||||||
recent_file_id = _get_recent_photo_file_id(agent_config.agent_id, chat_id, user_id)
|
recent_file_id = _get_recent_photo_file_id(agent_config.agent_id, chat_id, user_id)
|
||||||
if not recent_file_id:
|
if not recent_file_id:
|
||||||
try:
|
try:
|
||||||
mc = await memory_client.get_context(
|
mc = await memory_client.get_context(
|
||||||
user_id=f"tg:{user_id}",
|
user_id=f"tg:{user_id}",
|
||||||
agent_id=agent_config.agent_id,
|
agent_id=agent_config.agent_id,
|
||||||
team_id=dao_id,
|
team_id=dao_id,
|
||||||
channel_id=chat_id,
|
channel_id=chat_id,
|
||||||
limit=80,
|
limit=80,
|
||||||
)
|
|
||||||
recent_file_id = _extract_recent_photo_file_id_from_memory(mc)
|
|
||||||
except Exception:
|
|
||||||
recent_file_id = None
|
|
||||||
if not recent_file_id:
|
|
||||||
recent_file_id = await _get_agromatrix_last_photo_ref(
|
|
||||||
chat_id=chat_id,
|
|
||||||
user_id=user_id,
|
|
||||||
dao_id=dao_id,
|
|
||||||
)
|
)
|
||||||
if recent_file_id:
|
recent_file_id = _extract_recent_photo_file_id_from_memory(mc)
|
||||||
await _save_agromatrix_photo_learning(
|
except Exception:
|
||||||
file_id=recent_file_id,
|
recent_file_id = None
|
||||||
label=corrected_label,
|
if not recent_file_id:
|
||||||
source="user_correction",
|
recent_file_id = await _get_agromatrix_last_photo_ref(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
dao_id=dao_id,
|
dao_id=dao_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if corrected_label and recent_file_id:
|
||||||
|
await _save_agromatrix_photo_learning(
|
||||||
|
file_id=recent_file_id,
|
||||||
|
label=corrected_label,
|
||||||
|
source="user_correction",
|
||||||
|
chat_id=chat_id,
|
||||||
|
user_id=user_id,
|
||||||
|
dao_id=dao_id,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"AgroMatrix learning updated: file_id={recent_file_id}, label={corrected_label}"
|
||||||
|
)
|
||||||
|
if _is_agromatrix_correction_only_message(text):
|
||||||
|
ack = (
|
||||||
|
f"Дякую, зафіксував виправлення: {corrected_label}. "
|
||||||
|
"Для цього фото надалі використовуватиму саме цю мітку."
|
||||||
)
|
)
|
||||||
logger.info(
|
await send_telegram_message(chat_id, ack, telegram_token)
|
||||||
f"AgroMatrix learning updated: file_id={recent_file_id}, label={corrected_label}"
|
await memory_client.save_chat_turn(
|
||||||
|
agent_id=agent_config.agent_id,
|
||||||
|
team_id=dao_id,
|
||||||
|
user_id=f"tg:{user_id}",
|
||||||
|
message=text,
|
||||||
|
response=ack,
|
||||||
|
channel_id=chat_id,
|
||||||
|
scope="short_term",
|
||||||
|
save_agent_response=True,
|
||||||
|
agent_metadata={"agromatrix_learning_updated": True},
|
||||||
|
username=username,
|
||||||
)
|
)
|
||||||
if _is_agromatrix_correction_only_message(text):
|
return {"ok": True, "agent": agent_config.agent_id, "mode": "learning_updated"}
|
||||||
ack = f"Прийняв. Зберіг корекцію: {corrected_label}. Далі врахую це у відповідях."
|
elif corrected_label and _is_agromatrix_correction_only_message(text):
|
||||||
await send_telegram_message(chat_id, ack, telegram_token)
|
ack = (
|
||||||
await memory_client.save_chat_turn(
|
f"Прийняв виправлення: {corrected_label}. "
|
||||||
agent_id=agent_config.agent_id,
|
"Але не бачу останнє фото в контексті, надішли фото ще раз і я зафіксую корекцію саме до нього."
|
||||||
team_id=dao_id,
|
)
|
||||||
user_id=f"tg:{user_id}",
|
await send_telegram_message(chat_id, ack, telegram_token)
|
||||||
message=text,
|
await memory_client.save_chat_turn(
|
||||||
response=ack,
|
agent_id=agent_config.agent_id,
|
||||||
channel_id=chat_id,
|
team_id=dao_id,
|
||||||
scope="short_term",
|
user_id=f"tg:{user_id}",
|
||||||
save_agent_response=True,
|
message=text,
|
||||||
agent_metadata={"agromatrix_learning_ack": True},
|
response=ack,
|
||||||
username=username,
|
channel_id=chat_id,
|
||||||
)
|
scope="short_term",
|
||||||
return {"ok": True, "agent": agent_config.agent_id, "mode": "learning_ack"}
|
save_agent_response=True,
|
||||||
|
agent_metadata={"agromatrix_learning_no_photo_context": True},
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
return {"ok": True, "agent": agent_config.agent_id, "mode": "learning_no_photo_context"}
|
||||||
|
|
||||||
|
# If user says answer is wrong but does not provide a replacement label,
|
||||||
|
# invalidate stale prior so it won't keep forcing repeated wrong guesses.
|
||||||
|
if negative_feedback and not corrected_label and recent_file_id:
|
||||||
|
await _invalidate_agromatrix_photo_learning(
|
||||||
|
file_id=recent_file_id,
|
||||||
|
reason="user_marked_previous_answer_wrong_without_replacement",
|
||||||
|
dao_id=dao_id,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"AgroMatrix learning invalidated: file_id={recent_file_id}, reason=negative_feedback_no_label"
|
||||||
|
)
|
||||||
|
if _is_agromatrix_correction_only_message(text):
|
||||||
|
ack = (
|
||||||
|
"Прийняв. Попередню мітку для цього фото скасовано. "
|
||||||
|
"Напиши правильну назву культури або попроси: 'перевір фото ще раз'."
|
||||||
|
)
|
||||||
|
await send_telegram_message(chat_id, ack, telegram_token)
|
||||||
|
await memory_client.save_chat_turn(
|
||||||
|
agent_id=agent_config.agent_id,
|
||||||
|
team_id=dao_id,
|
||||||
|
user_id=f"tg:{user_id}",
|
||||||
|
message=text,
|
||||||
|
response=ack,
|
||||||
|
channel_id=chat_id,
|
||||||
|
scope="short_term",
|
||||||
|
save_agent_response=True,
|
||||||
|
agent_metadata={"agromatrix_learning_invalidated": True},
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
return {"ok": True, "agent": agent_config.agent_id, "mode": "learning_invalidated"}
|
||||||
|
elif negative_feedback and not corrected_label and _is_agromatrix_correction_only_message(text):
|
||||||
|
ack = (
|
||||||
|
"Прийняв, що попередня відповідь була хибна. "
|
||||||
|
"Щоб закріпити правильну мітку, напиши у форматі: 'правильна відповідь: <назва культури>'."
|
||||||
|
)
|
||||||
|
await send_telegram_message(chat_id, ack, telegram_token)
|
||||||
|
await memory_client.save_chat_turn(
|
||||||
|
agent_id=agent_config.agent_id,
|
||||||
|
team_id=dao_id,
|
||||||
|
user_id=f"tg:{user_id}",
|
||||||
|
message=text,
|
||||||
|
response=ack,
|
||||||
|
channel_id=chat_id,
|
||||||
|
scope="short_term",
|
||||||
|
save_agent_response=True,
|
||||||
|
agent_metadata={"agromatrix_negative_feedback_no_photo_context": True},
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
return {"ok": True, "agent": agent_config.agent_id, "mode": "negative_feedback_ack"}
|
||||||
|
|
||||||
# Photo/image intent guard:
|
# Photo/image intent guard:
|
||||||
# if text references a photo/image, try to resolve latest file_id and route to vision.
|
# if text references a photo/image, try to resolve latest file_id and route to vision.
|
||||||
|
|||||||
Reference in New Issue
Block a user