""" سرویس توصیه کودهی — بدون API، قابل فراخوانی از سایر سرویس‌ها از RAG با پایگاه دانش fertilization و لحن مخصوص کودهی استفاده می‌کند. """ import json import logging from django.apps import apps from farm_data.models import SensorData 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 load_rag_config, RAGConfig, get_service_config from rag.user_data import build_plant_text logger = logging.getLogger(__name__) KB_NAME = "fertilization" SERVICE_ID = "fertilization" DEFAULT_FERTILIZATION_PROMPT = ( "از داده های خاک، مرحله رشد و خروجی بهینه ساز شبیه سازی برای ساخت توصیه کودهی استفاده کن. " "اگر بلوک [خروجی بهینه ساز شبیه سازی] وجود داشت، همان را مرجع اصلی فرمول، مقدار، روش مصرف و اعتبار قرار بده. " "پاسخ فقط JSON معتبر با کلید sections باشد." ) def _get_optimizer(): return apps.get_app_config("crop_simulation").get_recommendation_optimizer() def _unique_items(items: list[str]) -> list[str]: seen = set() output = [] for item in items: normalized = (item or "").strip() if not normalized or normalized in seen: continue seen.add(normalized) output.append(normalized) return output def _find_section(sections: list[dict], section_type: str) -> dict | None: for section in sections: if isinstance(section, dict) and section.get("type") == section_type: return section return None def _build_fertilization_fallback(*, optimized_result: dict | None) -> dict: if optimized_result: recommended = optimized_result["recommended_strategy"] list_items = [ f"دوز پیشنهادی: {recommended['amount_kg_per_ha']} کیلوگرم در هکتار", f"روش مصرف: {recommended['application_method']}", f"پنجره اجرا: {recommended['validity_period']}", ] warning_text = "قبل از اختلاط یا محلول سازی، سازگاری کود با آب و شرایط مزرعه بررسی شود." return { "sections": [ { "type": "recommendation", "title": "برنامه کودهی بهینه", "icon": "leaf", "content": ( f"سناریوی {recommended['label']} برای این مزرعه مناسب تر ارزیابی شد." ), "fertilizerType": recommended["fertilizer_type"], "amount": f"{recommended['amount_kg_per_ha']} کیلوگرم در هکتار", "applicationMethod": recommended["application_method"], "timing": recommended["timing"], "validityPeriod": recommended["validity_period"], "expandableExplanation": " ".join(recommended.get("reasoning", [])), }, { "type": "list", "title": "نکات اجرایی و اختلاط", "icon": "list", "items": _unique_items(list_items), }, { "type": "warning", "title": "هشدار کودهی", "icon": "alert-triangle", "content": warning_text, }, ] } return { "sections": [ { "type": "recommendation", "title": "برنامه کودهی پیشنهادی", "icon": "leaf", "content": "پیشنهاد کودهی بر اساس داده های فعلی با قطعیت متوسط آماده شده است.", "fertilizerType": "کود کامل متعادل", "amount": "پس از پایش دوباره عناصر اصلی تعیین شود", "applicationMethod": "ترجیحا همراه آب آبیاری", "timing": "صبح زود", "validityPeriod": "معتبر برای 5 روز آینده", "expandableExplanation": "به دلیل محدود بودن داده های تغذیه ای، تصمیم نهایی باید با پایش مجدد تکمیل شود.", }, { "type": "list", "title": "نکات اجرایی و اختلاط", "icon": "list", "items": [ "قبل از مصرف، EC و pH محلول بررسی شود.", "در صورت مشاهده بارش موثر، زمان مصرف بازبینی شود.", ], }, { "type": "warning", "title": "هشدار کودهی", "icon": "alert-triangle", "content": "بدون بررسی دوباره مزرعه از مصرف سنگین کود خودداری شود.", }, ] } def _merge_fertilization_response( *, parsed_result: dict, optimized_result: dict | None, ) -> dict: fallback = _build_fertilization_fallback(optimized_result=optimized_result) if not isinstance(parsed_result, dict): return fallback sections = parsed_result.get("sections") if not isinstance(sections, list): return fallback recommendation = _find_section(sections, "recommendation") or {} list_section = _find_section(sections, "list") or {} warning_section = _find_section(sections, "warning") or {} fallback_recommendation = fallback["sections"][0] fallback_list = fallback["sections"][1] fallback_warning = fallback["sections"][2] merged_recommendation = {**recommendation, **fallback_recommendation} merged_recommendation["content"] = recommendation.get("content") or fallback_recommendation["content"] merged_recommendation["title"] = recommendation.get("title") or fallback_recommendation["title"] merged_recommendation["expandableExplanation"] = ( recommendation.get("expandableExplanation") or fallback_recommendation["expandableExplanation"] ) merged_list = { **fallback_list, **list_section, "items": _unique_items( list(list_section.get("items", [])) + list(fallback_list["items"]) )[:5], } merged_warning = { **fallback_warning, **warning_section, "content": warning_section.get("content") or fallback_warning["content"], } return {"sections": [merged_recommendation, merged_list, merged_warning]} def get_fertilization_recommendation( sensor_uuid: str, plant_name: str | None = None, growth_stage: str | None = None, query: str | None = None, config: RAGConfig | None = None, limit: int = 8, ) -> dict: """ توصیه کودهی برای یک سنسور (کاربر). از RAG با پایگاه دانش fertilization استفاده می‌کند. Args: sensor_uuid: شناسه سنسور کاربر plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant) growth_stage: مرحله رشد گیاه query: سوال اختیاری config: تنظیمات RAG limit: تعداد چانک‌های بازیابی‌شده Returns: dict با کلیدهای fertilizer_needed, fertilizer_type, amount_kg_per_hectare, reason, npk_status, raw_response """ cfg = config or load_rag_config() 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 user_query = query or "توصیه کودهی برای مزرعه من چیست؟" sensor = ( SensorData.objects.select_related("center_location") .prefetch_related("plants") .filter(farm_uuid=sensor_uuid) .first() ) resolved_plant_name = plant_name plant = None if not resolved_plant_name and sensor is not None: plant = sensor.plants.first() if plant is not None: resolved_plant_name = plant.name elif sensor is not None and plant_name: plant = sensor.plants.filter(name=plant_name).first() or sensor.plants.first() forecasts = [] optimized_result = None if sensor is not None and getattr(sensor, "center_location", None) is not None: from weather.models import WeatherForecast forecasts = list( WeatherForecast.objects.filter( location=sensor.center_location, forecast_date__isnull=False, ).order_by("forecast_date")[:7] ) if sensor is not None and plant is not None: optimized_result = _get_optimizer().optimize_fertilization( sensor=sensor, plant=plant, forecasts=forecasts, growth_stage=growth_stage, ) context = build_rag_context( user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID, ) extra_parts: list[str] = [] if resolved_plant_name and growth_stage: plant_text = build_plant_text(resolved_plant_name, growth_stage) if plant_text: extra_parts.append("[اطلاعات گیاه]\n" + plant_text) if optimized_result is not None: extra_parts.append( "[خروجی بهینه ساز شبیه سازی]\n" + optimized_result["context_text"] ) if extra_parts: context = "\n\n---\n\n".join(extra_parts) + ("\n\n---\n\n" + context if context else "") 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(DEFAULT_FERTILIZATION_PROMPT) if context: system_parts.append("\n\n" + context) system_content = "\n".join(system_parts) messages = [ {"role": "system", "content": system_content}, {"role": "user", "content": user_query}, ] audit_log = _create_audit_log( farm_uuid=sensor_uuid, service_id=SERVICE_ID, model=model, query=user_query, system_prompt=system_content, messages=messages, ) try: response = client.chat.completions.create( model=model, messages=messages, ) raw = response.choices[0].message.content.strip() except Exception as exc: logger.error("Fertilization recommendation error for %s: %s", sensor_uuid, exc) result = _build_fertilization_fallback(optimized_result=optimized_result) result["error"] = f"خطا در دریافت توصیه: {exc}" result["raw_response"] = None _fail_audit_log( audit_log, str(exc), response_text=json.dumps(result, ensure_ascii=False, default=str), ) return result try: cleaned = raw if cleaned.startswith("```"): cleaned = cleaned.strip("`").removeprefix("json").strip() result = json.loads(cleaned) except (json.JSONDecodeError, ValueError): result = {} result = _merge_fertilization_response( parsed_result=result, optimized_result=optimized_result, ) result["raw_response"] = raw result["simulation_optimizer"] = optimized_result _complete_audit_log( audit_log, json.dumps(result, ensure_ascii=False, default=str), ) return result