""" سرویس توصیه کودهی — بدون 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 _validate_fertilization_response(parsed_result: dict) -> dict: if not isinstance(parsed_result, dict): raise ValueError("Fertilization recommendation response is not a JSON object.") sections = parsed_result.get("sections") if not isinstance(sections, list) or not sections: raise ValueError("Fertilization recommendation response is missing sections.") for index, section in enumerate(sections): if not isinstance(section, dict): raise ValueError(f"Fertilization recommendation section {index} is invalid.") missing = [key for key in ("type", "title", "icon") if key not in section] if missing: raise ValueError( f"Fertilization recommendation section {index} is missing fields: {', '.join(missing)}" ) return parsed_result def get_fertilization_recommendation( farm_uuid: str | None = None, plant_name: str | None = None, growth_stage: str | None = None, query: str | None = None, config: RAGConfig | None = None, limit: int = 8, sensor_uuid: str | None = None, ) -> dict: """ توصیه کودهی برای یک مزرعه. از RAG با پایگاه دانش fertilization استفاده می‌کند. Args: farm_uuid: شناسه مزرعه plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant) growth_stage: مرحله رشد گیاه query: سوال اختیاری config: تنظیمات RAG limit: تعداد چانک‌های بازیابی‌شده Returns: dict ساختاریافته برای توصیه کودهی """ 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 resolved_farm_uuid = str(farm_uuid or sensor_uuid or "").strip() if not resolved_farm_uuid: raise ValueError("farm_uuid is required.") user_query = query or "توصیه کودهی برای مزرعه من چیست؟" sensor = ( SensorData.objects.select_related("center_location") .prefetch_related("plants") .filter(farm_uuid=resolved_farm_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, resolved_farm_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=resolved_farm_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", resolved_farm_uuid, exc) _fail_audit_log(audit_log, str(exc)) raise RuntimeError( f"Fertilization recommendation failed for farm {resolved_farm_uuid}." ) from exc try: cleaned = raw if cleaned.startswith("```"): cleaned = cleaned.strip("`").removeprefix("json").strip() result = json.loads(cleaned) except (json.JSONDecodeError, ValueError): result = {} result = _validate_fertilization_response(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