補齊自癒稽核與 OpenClaw 記憶閉環
All checks were successful
CD Pipeline / deploy (push) Successful in 1m16s

This commit is contained in:
OoO
2026-04-29 23:02:06 +08:00
parent 1c2dc6cd61
commit f0e044aa48
4 changed files with 241 additions and 23 deletions

View File

@@ -121,6 +121,21 @@ def _embedding_worker_loop():
try:
session = get_session()
try:
session.execute(
text("""
UPDATE embedding_retry_queue
SET status = 'pending',
updated_at = :now,
last_error = COALESCE(last_error, '') || ' | reset stale processing'
WHERE status = 'processing'
AND updated_at < :cutoff
"""),
{
"now": datetime.now(),
"cutoff": datetime.fromtimestamp(time.time() - 15 * 60),
},
)
session.commit()
rows = session.execute(
text("""
SELECT id, target_table, target_id, text_content, model
@@ -155,7 +170,8 @@ threading.Thread(target=_embedding_worker_loop, daemon=True).start()
def store_insight(insight_type: str, content: str, period: str = None,
product_sku: str = None, metadata: dict = None,
ai_model: str = None) -> int:
ai_model: str = None, confidence: float = None,
created_by: str = None, status: str = None) -> int:
"""
將 AI 產出存入 ai_insights 表並排程向量化。
- Cache-aside同 insight_type + period + product_sku 已存在則覆蓋
@@ -181,26 +197,44 @@ def store_insight(insight_type: str, content: str, period: str = None,
insight_id = existing.id
sys_log.info(f"[OCLearn] 更新 insight_type={insight_type} period={period}")
else:
new_insight = AIInsight(
insight_type=insight_type,
period=period,
product_sku=product_sku,
content=content,
metadata_json=meta_str,
created_at=datetime.now(),
updated_at=datetime.now(),
)
insight_kwargs = {
"insight_type": insight_type,
"period": period,
"product_sku": product_sku,
"content": content,
"metadata_json": meta_str,
"status": status or "approved",
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
if confidence is not None:
insight_kwargs["confidence"] = confidence
if created_by:
insight_kwargs["created_by"] = created_by
new_insight = AIInsight(**insight_kwargs)
session.add(new_insight)
session.commit()
insight_id = new_insight.id
sys_log.info(f"[OCLearn] 新增 insight_type={insight_type} period={period}")
# 若已有 ai_model 欄位則寫入Migration 010 後可用,先試再容錯)
# 若已有進階欄位則寫入Migration 010/015 後可用,先試再容錯)
update_fields = {}
if ai_model:
update_fields["ai_model"] = ai_model
if confidence is not None:
update_fields["confidence"] = confidence
if created_by:
update_fields["created_by"] = created_by
if status:
update_fields["status"] = status
if update_fields:
try:
assignments = ", ".join(f"{key} = :{key}" for key in update_fields)
params = dict(update_fields)
params["i"] = insight_id
session.execute(
text("UPDATE ai_insights SET ai_model = :m WHERE id = :i"),
{"m": ai_model, "i": insight_id},
text(f"UPDATE ai_insights SET {assignments}, updated_at = :now WHERE id = :i"),
{**params, "now": datetime.now()},
)
session.commit()
except Exception:
@@ -208,7 +242,28 @@ def store_insight(insight_type: str, content: str, period: str = None,
# 推入 DB retry queue持久化
embed_target_text = f"{insight_type} ({period or ''}): {content}"
_enqueue_embedding("ai_insights", insight_id, embed_target_text)
embedding_queued = _enqueue_embedding("ai_insights", insight_id, embed_target_text)
if not embedding_queued:
try:
metadata_payload = metadata.copy() if isinstance(metadata, dict) else {}
metadata_payload["embedding_queue_error"] = True
session.execute(
text("""
UPDATE ai_insights
SET metadata_json = :meta,
updated_at = :now
WHERE id = :id
"""),
{
"meta": json.dumps(metadata_payload, ensure_ascii=False),
"now": datetime.now(),
"id": insight_id,
},
)
session.commit()
except Exception as meta_err:
session.rollback()
sys_log.warning(f"[OCLearn] embedding queue error metadata 寫入失敗: {meta_err}")
return insight_id
@@ -247,6 +302,10 @@ def build_rag_context(query: str, insight_type: str = None, period: str = None,
"""
session = get_session()
try:
semantic_context = _build_semantic_rag_context(session, query, insight_type, period, top_k)
if semantic_context:
return semantic_context
q = session.query(AIInsight)
if insight_type:
q = q.filter_by(insight_type=insight_type)
@@ -288,6 +347,50 @@ def build_rag_context(query: str, insight_type: str = None, period: str = None,
session.close()
def _build_semantic_rag_context(session, query: str, insight_type: str = None,
period: str = None, top_k: int = 5) -> str:
if not query:
return ""
try:
vec = ollama_service.generate_embedding(query)
if not vec:
return ""
filters = ["embedding IS NOT NULL", "status IN ('approved', 'active', 'executed')"]
params = {"qvec": str(vec), "lim": top_k}
if insight_type:
filters.append("insight_type = :insight_type")
params["insight_type"] = insight_type
if period:
filters.append("period = :period")
params["period"] = period
rows = session.execute(
text(f"""
SELECT id, insight_type, period, content, avg_quality, decay_exempt, created_at,
embedding <=> CAST(:qvec AS vector) AS distance
FROM ai_insights
WHERE {' AND '.join(filters)}
ORDER BY distance ASC, created_at DESC
LIMIT :lim
"""),
params,
).fetchall()
if not rows:
return ""
parts = []
for row in rows:
base = row.avg_quality if row.avg_quality is not None else 0.5
effective = base if row.decay_exempt else compute_effective_score(base, row.created_at)
p_tag = f"[{row.period}]" if row.period else "[語意記憶]"
parts.append(
f"{p_tag} {row.insight_type} (語意距離={row.distance:.3f}, 分數={effective:.2f}):\n{row.content}"
)
sys_log.info(f"[OCLearn] semantic RAG context: {len(parts)}")
return "\n\n---\n\n".join(parts)
except Exception as exc:
sys_log.debug(f"[OCLearn] semantic RAG fallback to decay ranking: {exc}")
return ""
def build_rag_context_by_date(start_date: str, end_date: str) -> str:
"""週報 RAG依日期區間過濾 ai_insights"""
session = get_session()