This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user