from __future__ import annotations import json import logging from typing import Any from farm_data.services import get_farm_details 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 logger = logging.getLogger(__name__) KB_NAME = "soil_anomaly" SERVICE_ID = "soil_anomaly" SOIL_ANOMALY_PROMPT = ( "شما یک دستیار تخصصی تحلیل ناهنجاری داده های خاک و سنسور مزرعه هستی. " "ورودی شامل داده های ساختاریافته ناهنجاری، اطلاعات مزرعه، و متن های بازیابی شده از پایگاه دانش است. " "فقط JSON معتبر برگردان و فقط این کلیدها را تولید کن: " "summary, explanation, likely_cause, recommended_action, monitoring_priority, confidence. " "monitoring_priority فقط یکی از low, medium, high, urgent باشد. " "confidence عددی بین 0 و 1 باشد. " "اگر ناهنجاری معناداری وجود ندارد، این موضوع را شفاف و بدون اغراق بیان کن." ) def _clean_json(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 soil_anomaly LLM: %s", cleaned[:500]) return {} def _load_farm_or_error(farm_uuid: str) -> dict[str, Any]: farm_details = get_farm_details(farm_uuid) if farm_details is None: raise ValueError("farm_uuid نامعتبر است یا اطلاعات مزرعه پیدا نشد.") return farm_details def _build_service_client(cfg: RAGConfig): 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) return service, client, service.llm.model def _fallback_from_payload(anomaly_payload: dict[str, Any]) -> dict[str, Any]: interpretation = anomaly_payload.get("interpretation") or {} anomalies = anomaly_payload.get("anomalies") or [] top_anomaly = anomalies[0] if anomalies else None if top_anomaly is None: return { "summary": "در داده های اخیر ناهنجاری معناداری دیده نشد.", "explanation": interpretation.get("explanation") or "داده های فعلی با الگوی معمول مزرعه سازگار هستند و مورد غیرعادی برجسته ای دیده نمی شود.", "likely_cause": interpretation.get("likely_cause") or "شرایط فعلی مزرعه پایدار است یا داده کافی برای تشخیص رخداد غیرعادی وجود ندارد.", "recommended_action": interpretation.get("recommended_action") or "پایش عادی ادامه یابد و روندها در بازه بعدی دوباره بررسی شوند.", "monitoring_priority": "low", "confidence": 0.55, } severity = str(top_anomaly.get("severity") or "medium") priority_map = { "low": "medium", "medium": "high", "high": "urgent", "critical": "urgent", } return { "summary": f"ناهنجاري در شاخص {top_anomaly.get('label', 'نامشخص')} شناسايي شد.", "explanation": interpretation.get("explanation") or f"مقدار {top_anomaly.get('label', 'اين شاخص')} از الگوي آماري معمول مزرعه فاصله گرفته است.", "likely_cause": interpretation.get("likely_cause") or "اين الگو مي تواند ناشي از تغيير شرايط محيطي، آبياري، شوري يا خطاي اندازه گيري سنسور باشد.", "recommended_action": interpretation.get("recommended_action") or "روند اين شاخص و شرايط مزرعه در کوتاه مدت بازبيني و در صورت تداوم، اقدام اصلاحي انجام شود.", "monitoring_priority": priority_map.get(severity, "high"), "confidence": 0.7 if severity in {"high", "critical"} else 0.6, } def _build_messages( *, 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(SOIL_ANOMALY_PROMPT) system_parts.append( "[کانتکست ساختاریافته ناهنجاري خاک]\n" + json.dumps(structured_context, ensure_ascii=False, indent=2, default=str) ) 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 get_soil_anomaly_insight( *, farm_uuid: str, anomaly_payload: dict[str, Any], query: str | None = None, ) -> dict[str, Any]: cfg = load_rag_config() service, client, model = _build_service_client(cfg) farm_details = _load_farm_or_error(farm_uuid) fallback = _fallback_from_payload(anomaly_payload) user_query = query or "ناهنجاري هاي داده هاي خاک اين مزرعه را تفسير کن و اقدام مناسب پيشنهاد بده." structured_context = { "farm_uuid": farm_uuid, "anomaly_payload": anomaly_payload, "fallback_interpretation": fallback, } rag_context = build_rag_context( query=user_query, sensor_uuid=farm_uuid, config=cfg, kb_name=KB_NAME, service_id=SERVICE_ID, farm_details=farm_details, ) system_prompt, messages = _build_messages( service=service, cfg=cfg, query=user_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=user_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(raw) _complete_audit_log(audit_log, raw) except Exception as exc: logger.error("Soil anomaly insight failed for %s: %s", farm_uuid, exc) _fail_audit_log(audit_log, str(exc), json.dumps(fallback, ensure_ascii=False)) return { **fallback, "farm_uuid": farm_uuid, "knowledge_base": KB_NAME, "tone_file": service.tone_file, "raw_response": None, } if not parsed: parsed = fallback parsed.setdefault("summary", fallback["summary"]) parsed.setdefault("explanation", fallback["explanation"]) parsed.setdefault("likely_cause", fallback["likely_cause"]) parsed.setdefault("recommended_action", fallback["recommended_action"]) parsed.setdefault("monitoring_priority", fallback["monitoring_priority"]) parsed.setdefault("confidence", fallback["confidence"]) parsed["farm_uuid"] = farm_uuid parsed["knowledge_base"] = KB_NAME parsed["tone_file"] = service.tone_file parsed["raw_response"] = raw return parsed