UPDATE
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
from farm_data.services import get_farm_details
|
||||
from farm_data.context import load_farm_context
|
||||
from rag.api_provider import get_chat_client
|
||||
from rag.chat import (
|
||||
_complete_audit_log,
|
||||
_create_audit_log,
|
||||
_fail_audit_log,
|
||||
_load_service_tone,
|
||||
build_rag_context,
|
||||
)
|
||||
from rag.config import RAGConfig, get_service_config, load_rag_config
|
||||
|
||||
from .models import FarmAlertNotification
|
||||
from .alerts_tracker import build_farm_alerts_tracker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KB_NAME = "farm_alerts"
|
||||
SERVICE_ID = "farm_alerts"
|
||||
|
||||
TRACKER_PROMPT = (
|
||||
"وضعیت هشدارهای مزرعه را فقط بر اساس داده های ساختاریافته، اطلاعات مزرعه، و متون بازیابی شده از پایگاه دانش تحلیل کن. "
|
||||
"پاسخ فقط JSON معتبر باشد و این کلیدها را داشته باشد: headline, overview, status_level, notifications. "
|
||||
"status_level فقط یکی از danger, warning, info باشد. "
|
||||
"notifications باید آرایه ای از آبجکت ها با کلیدهای level, title, message, suggested_action, source_alert_id, source_metric_type باشد. "
|
||||
"سطوح level فقط یکی از danger, warning, info باشند. "
|
||||
"اگر هشدار مهمی وجود ندارد، notifications را خالی برگردان."
|
||||
)
|
||||
|
||||
TIMELINE_PROMPT = (
|
||||
"بر اساس داده های هشدار مزرعه، یک timeline عملیاتی بساز. "
|
||||
"پاسخ فقط JSON معتبر باشد و این کلیدها را داشته باشد: headline, overview, timeline, notifications. "
|
||||
"timeline باید آرایه ای از آبجکت ها با کلیدهای timestamp, level, title, description, source_alert_id, source_metric_type باشد. "
|
||||
"level فقط danger, warning, info باشد. "
|
||||
"notifications باید آرایه ای از آبجکت ها با کلیدهای level, title, message, suggested_action, source_alert_id, source_metric_type باشد."
|
||||
)
|
||||
|
||||
|
||||
def _json_dumps(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, indent=2, cls=DjangoJSONEncoder)
|
||||
|
||||
|
||||
def _clean_json_response(raw: str) -> dict[str, Any]:
|
||||
cleaned = (raw or "").strip()
|
||||
if cleaned.startswith("```"):
|
||||
cleaned = cleaned.strip("`")
|
||||
if cleaned.startswith("json"):
|
||||
cleaned = cleaned[4:]
|
||||
cleaned = cleaned.strip()
|
||||
if not cleaned:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logger.warning("Invalid JSON returned by farm_alerts LLM: %s", cleaned[:500])
|
||||
return {}
|
||||
|
||||
|
||||
def _severity_to_level(severity: str) -> str:
|
||||
normalized = (severity or "").strip().lower()
|
||||
if normalized in {"critical", "high", "danger"}:
|
||||
return FarmAlertNotification.LEVEL_DANGER
|
||||
if normalized in {"medium", "warning"}:
|
||||
return FarmAlertNotification.LEVEL_WARNING
|
||||
return FarmAlertNotification.LEVEL_INFO
|
||||
|
||||
|
||||
def _normalize_level(level: str | None) -> str:
|
||||
normalized = (level or "").strip().lower()
|
||||
if normalized in {
|
||||
FarmAlertNotification.LEVEL_DANGER,
|
||||
FarmAlertNotification.LEVEL_WARNING,
|
||||
FarmAlertNotification.LEVEL_INFO,
|
||||
}:
|
||||
return normalized
|
||||
if normalized in {"high", "critical"}:
|
||||
return FarmAlertNotification.LEVEL_DANGER
|
||||
if normalized in {"medium", "alert"}:
|
||||
return FarmAlertNotification.LEVEL_WARNING
|
||||
return FarmAlertNotification.LEVEL_INFO
|
||||
|
||||
|
||||
def _alert_identifier(alert: dict[str, Any]) -> str:
|
||||
metric_type = alert.get("metric_type", "alert")
|
||||
timestamp = alert.get("timestamp", "")
|
||||
return f"{metric_type}:{timestamp}"
|
||||
|
||||
|
||||
def _forecast_summary(context: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
forecasts = context.get("forecasts", [])
|
||||
return [
|
||||
{
|
||||
"date": getattr(item, "forecast_date", None),
|
||||
"temperature_min": getattr(item, "temperature_min", None),
|
||||
"temperature_max": getattr(item, "temperature_max", None),
|
||||
"humidity_mean": getattr(item, "humidity_mean", None),
|
||||
"precipitation": getattr(item, "precipitation", None),
|
||||
"et0": getattr(item, "et0", None),
|
||||
}
|
||||
for item in forecasts[:7]
|
||||
]
|
||||
|
||||
|
||||
def _farm_profile(context: dict[str, Any], farm_uuid: str) -> dict[str, Any]:
|
||||
sensor = context.get("sensor")
|
||||
location = context.get("location")
|
||||
plants = context.get("plants", [])
|
||||
irrigation_method = getattr(sensor, "irrigation_method", None) if sensor else None
|
||||
return {
|
||||
"farm_uuid": farm_uuid,
|
||||
"location": {
|
||||
"latitude": float(location.latitude) if location else None,
|
||||
"longitude": float(location.longitude) if location else None,
|
||||
},
|
||||
"plant_names": [getattr(plant, "name", "") for plant in plants],
|
||||
"irrigation_method": getattr(irrigation_method, "name", None),
|
||||
"last_sensor_update": getattr(sensor, "updated_at", None),
|
||||
}
|
||||
|
||||
|
||||
def _build_structured_context(farm_uuid: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
context = load_farm_context(farm_uuid)
|
||||
if context is None:
|
||||
raise ValueError("farm_uuid نامعتبر است یا اطلاعات هشدار مزرعه پیدا نشد.")
|
||||
|
||||
tracker = build_farm_alerts_tracker(sensor_id=farm_uuid, context=context, ai_bundle=None)
|
||||
structured = {
|
||||
"farm_profile": _farm_profile(context, farm_uuid),
|
||||
"tracker": tracker,
|
||||
"forecasts": _forecast_summary(context),
|
||||
}
|
||||
return context, structured
|
||||
|
||||
|
||||
def _build_fallback_notifications(tracker: dict[str, Any], endpoint: str) -> list[dict[str, Any]]:
|
||||
notifications: list[dict[str, Any]] = []
|
||||
for alert in tracker.get("alerts", [])[:5]:
|
||||
notifications.append(
|
||||
{
|
||||
"level": _severity_to_level(alert.get("severity")),
|
||||
"title": alert.get("title") or "هشدار مزرعه",
|
||||
"message": alert.get("summary") or alert.get("explanation") or "",
|
||||
"suggested_action": alert.get("recommended_action") or "",
|
||||
"source_alert_id": _alert_identifier(alert),
|
||||
"source_metric_type": alert.get("metric_type") or "",
|
||||
"payload": {
|
||||
"endpoint": endpoint,
|
||||
"alert": alert,
|
||||
},
|
||||
}
|
||||
)
|
||||
return notifications
|
||||
|
||||
|
||||
def _build_fallback_tracker_response(tracker: dict[str, Any]) -> dict[str, Any]:
|
||||
top_alert = tracker.get("mostCriticalIssue") or {}
|
||||
status_level = _severity_to_level(top_alert.get("severity")) if top_alert else FarmAlertNotification.LEVEL_INFO
|
||||
if tracker.get("totalAlerts", 0) <= 0:
|
||||
overview = "در حال حاضر هشدار فعالی برای مزرعه شناسایی نشده است."
|
||||
else:
|
||||
overview = top_alert.get("summary") or "چند هشدار فعال برای مزرعه شناسایی شده است."
|
||||
return {
|
||||
"headline": "ارزیابی فعلی هشدارهای مزرعه",
|
||||
"overview": overview,
|
||||
"status_level": status_level,
|
||||
"notifications": _build_fallback_notifications(tracker, FarmAlertNotification.ENDPOINT_TRACKER),
|
||||
}
|
||||
|
||||
|
||||
def _build_fallback_timeline_response(tracker: dict[str, Any]) -> dict[str, Any]:
|
||||
timeline = []
|
||||
for alert in tracker.get("alerts", [])[:6]:
|
||||
timeline.append(
|
||||
{
|
||||
"timestamp": alert.get("timestamp"),
|
||||
"level": _severity_to_level(alert.get("severity")),
|
||||
"title": alert.get("title") or "رویداد هشدار",
|
||||
"description": alert.get("explanation") or alert.get("summary") or "",
|
||||
"source_alert_id": _alert_identifier(alert),
|
||||
"source_metric_type": alert.get("metric_type") or "",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"headline": "خط زمانی هشدارهای مزرعه",
|
||||
"overview": "timeline بر اساس هشدارهای محاسبه شده مزرعه ساخته شد.",
|
||||
"timeline": timeline,
|
||||
"notifications": _build_fallback_notifications(tracker, FarmAlertNotification.ENDPOINT_TIMELINE),
|
||||
}
|
||||
|
||||
|
||||
def _notification_fingerprint(
|
||||
*,
|
||||
farm_uuid: str,
|
||||
endpoint: str,
|
||||
level: str,
|
||||
title: str,
|
||||
source_alert_id: str,
|
||||
source_metric_type: str,
|
||||
) -> str:
|
||||
raw = "|".join([
|
||||
str(farm_uuid),
|
||||
endpoint,
|
||||
level,
|
||||
source_alert_id or "-",
|
||||
source_metric_type or "-",
|
||||
title.strip(),
|
||||
])
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _save_notifications(
|
||||
*,
|
||||
farm_uuid: str,
|
||||
endpoint: str,
|
||||
notifications: list[dict[str, Any]],
|
||||
) -> list[FarmAlertNotification]:
|
||||
saved: list[FarmAlertNotification] = []
|
||||
for item in notifications:
|
||||
level = _normalize_level(item.get("level"))
|
||||
title = (item.get("title") or "هشدار مزرعه").strip()
|
||||
source_alert_id = (item.get("source_alert_id") or "").strip()
|
||||
source_metric_type = (item.get("source_metric_type") or "").strip()
|
||||
fingerprint = _notification_fingerprint(
|
||||
farm_uuid=farm_uuid,
|
||||
endpoint=endpoint,
|
||||
level=level,
|
||||
title=title,
|
||||
source_alert_id=source_alert_id,
|
||||
source_metric_type=source_metric_type,
|
||||
)
|
||||
payload = item.get("payload") if isinstance(item.get("payload"), dict) else {}
|
||||
notification, _ = FarmAlertNotification.objects.update_or_create(
|
||||
fingerprint=fingerprint,
|
||||
defaults={
|
||||
"farm_uuid": farm_uuid,
|
||||
"endpoint": endpoint,
|
||||
"level": level,
|
||||
"title": title,
|
||||
"message": item.get("message") or "",
|
||||
"suggested_action": item.get("suggested_action") or "",
|
||||
"source_alert_id": source_alert_id,
|
||||
"source_metric_type": source_metric_type,
|
||||
"payload": payload,
|
||||
},
|
||||
)
|
||||
saved.append(notification)
|
||||
return saved
|
||||
|
||||
|
||||
def _serialize_notification(notification: FarmAlertNotification) -> dict[str, Any]:
|
||||
return {
|
||||
"id": notification.id,
|
||||
"farm_uuid": str(notification.farm_uuid),
|
||||
"endpoint": notification.endpoint,
|
||||
"level": notification.level,
|
||||
"title": notification.title,
|
||||
"message": notification.message,
|
||||
"suggested_action": notification.suggested_action,
|
||||
"source_alert_id": notification.source_alert_id,
|
||||
"source_metric_type": notification.source_metric_type,
|
||||
"payload": notification.payload,
|
||||
"created_at": notification.created_at.isoformat(),
|
||||
"updated_at": notification.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _build_service_config(cfg: RAGConfig, service_id: str) -> tuple[Any, Any, str, Any]:
|
||||
service = get_service_config(service_id, cfg)
|
||||
service_cfg = RAGConfig(
|
||||
embedding=cfg.embedding,
|
||||
qdrant=cfg.qdrant,
|
||||
chunking=cfg.chunking,
|
||||
llm=service.llm,
|
||||
knowledge_bases=cfg.knowledge_bases,
|
||||
services=cfg.services,
|
||||
chromadb=cfg.chromadb,
|
||||
)
|
||||
client = get_chat_client(service_cfg)
|
||||
model = service.llm.model
|
||||
return service, service_cfg, model, client
|
||||
|
||||
|
||||
def _build_messages(
|
||||
*,
|
||||
prompt: str,
|
||||
service: Any,
|
||||
cfg: RAGConfig,
|
||||
query: str,
|
||||
rag_context: str,
|
||||
structured_context: dict[str, Any],
|
||||
) -> tuple[str, list[dict[str, str]]]:
|
||||
tone = _load_service_tone(service, cfg)
|
||||
system_parts = [tone] if tone else []
|
||||
if service.system_prompt:
|
||||
system_parts.append(service.system_prompt)
|
||||
system_parts.append(prompt)
|
||||
system_parts.append("[کانتکست ساختاریافته هشدار مزرعه]\n" + _json_dumps(structured_context))
|
||||
if rag_context:
|
||||
system_parts.append(rag_context)
|
||||
system_prompt = "\n\n".join(part for part in system_parts if part)
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query},
|
||||
]
|
||||
return system_prompt, messages
|
||||
|
||||
|
||||
def _llm_response(
|
||||
*,
|
||||
farm_uuid: str,
|
||||
service_id: str,
|
||||
prompt: str,
|
||||
query: str,
|
||||
structured_context: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], str, str]:
|
||||
cfg = load_rag_config()
|
||||
service, service_cfg, model, client = _build_service_config(cfg, service_id)
|
||||
farm_details = get_farm_details(farm_uuid)
|
||||
rag_context = build_rag_context(
|
||||
query=query,
|
||||
sensor_uuid=farm_uuid,
|
||||
config=cfg,
|
||||
kb_name=KB_NAME,
|
||||
service_id=service_id,
|
||||
farm_details=farm_details,
|
||||
)
|
||||
system_prompt, messages = _build_messages(
|
||||
prompt=prompt,
|
||||
service=service,
|
||||
cfg=cfg,
|
||||
query=query,
|
||||
rag_context=rag_context,
|
||||
structured_context=structured_context,
|
||||
)
|
||||
audit_log = _create_audit_log(
|
||||
farm_uuid=farm_uuid,
|
||||
service_id=service_id,
|
||||
model=model,
|
||||
query=query,
|
||||
system_prompt=system_prompt,
|
||||
messages=messages,
|
||||
)
|
||||
try:
|
||||
response = client.chat.completions.create(model=model, messages=messages)
|
||||
raw = response.choices[0].message.content.strip()
|
||||
parsed = _clean_json_response(raw)
|
||||
_complete_audit_log(audit_log, raw)
|
||||
return parsed, raw, service.tone_file or ""
|
||||
except Exception as exc:
|
||||
logger.error("farm_alerts llm error for %s: %s", farm_uuid, exc)
|
||||
_fail_audit_log(audit_log, str(exc))
|
||||
return {}, "", service.tone_file or ""
|
||||
|
||||
|
||||
def get_farm_alerts_tracker(*, farm_uuid: str, query: str | None = None) -> dict[str, Any]:
|
||||
_, structured_context = _build_structured_context(farm_uuid)
|
||||
tracker = structured_context["tracker"]
|
||||
user_query = query or "وضعیت فعلی هشدارهای مزرعه را ارزیابی کن و اگر لازم است notification بساز."
|
||||
llm_result, raw_response, tone_file = _llm_response(
|
||||
farm_uuid=farm_uuid,
|
||||
service_id=SERVICE_ID,
|
||||
prompt=TRACKER_PROMPT,
|
||||
query=user_query,
|
||||
structured_context=structured_context,
|
||||
)
|
||||
if not llm_result:
|
||||
llm_result = _build_fallback_tracker_response(tracker)
|
||||
|
||||
notifications_input = llm_result.get("notifications")
|
||||
if not isinstance(notifications_input, list):
|
||||
notifications_input = _build_fallback_notifications(tracker, FarmAlertNotification.ENDPOINT_TRACKER)
|
||||
saved_notifications = _save_notifications(
|
||||
farm_uuid=farm_uuid,
|
||||
endpoint=FarmAlertNotification.ENDPOINT_TRACKER,
|
||||
notifications=notifications_input,
|
||||
)
|
||||
return {
|
||||
"farm_uuid": farm_uuid,
|
||||
"service_id": SERVICE_ID,
|
||||
"knowledge_base": KB_NAME,
|
||||
"tone_file": tone_file,
|
||||
"tracker": tracker,
|
||||
"headline": llm_result.get("headline") or "ارزیابی فعلی هشدارهای مزرعه",
|
||||
"overview": llm_result.get("overview") or "",
|
||||
"status_level": _normalize_level(llm_result.get("status_level")),
|
||||
"notifications": [_serialize_notification(item) for item in saved_notifications],
|
||||
"raw_llm_response": raw_response or None,
|
||||
"structured_context": structured_context,
|
||||
}
|
||||
|
||||
|
||||
def get_farm_alerts_timeline(*, farm_uuid: str, query: str | None = None) -> dict[str, Any]:
|
||||
_, structured_context = _build_structured_context(farm_uuid)
|
||||
tracker = structured_context["tracker"]
|
||||
user_query = query or "برای هشدارهای مزرعه یک timeline عملیاتی بساز و اگر لازم است notification ثبت کن."
|
||||
llm_result, raw_response, tone_file = _llm_response(
|
||||
farm_uuid=farm_uuid,
|
||||
service_id=SERVICE_ID,
|
||||
prompt=TIMELINE_PROMPT,
|
||||
query=user_query,
|
||||
structured_context=structured_context,
|
||||
)
|
||||
if not llm_result:
|
||||
llm_result = _build_fallback_timeline_response(tracker)
|
||||
|
||||
timeline = llm_result.get("timeline")
|
||||
if not isinstance(timeline, list):
|
||||
timeline = _build_fallback_timeline_response(tracker).get("timeline", [])
|
||||
notifications_input = llm_result.get("notifications")
|
||||
if not isinstance(notifications_input, list):
|
||||
notifications_input = _build_fallback_notifications(tracker, FarmAlertNotification.ENDPOINT_TIMELINE)
|
||||
|
||||
saved_notifications = _save_notifications(
|
||||
farm_uuid=farm_uuid,
|
||||
endpoint=FarmAlertNotification.ENDPOINT_TIMELINE,
|
||||
notifications=notifications_input,
|
||||
)
|
||||
return {
|
||||
"farm_uuid": farm_uuid,
|
||||
"service_id": SERVICE_ID,
|
||||
"knowledge_base": KB_NAME,
|
||||
"tone_file": tone_file,
|
||||
"tracker": tracker,
|
||||
"headline": llm_result.get("headline") or "خط زمانی هشدارهای مزرعه",
|
||||
"overview": llm_result.get("overview") or "",
|
||||
"timeline": timeline,
|
||||
"notifications": [_serialize_notification(item) for item in saved_notifications],
|
||||
"raw_llm_response": raw_response or None,
|
||||
"structured_context": structured_context,
|
||||
}
|
||||
Reference in New Issue
Block a user