""" سرویس توصیه کودهی — بدون API، قابل فراخوانی از سایر سرویس‌ها از RAG با پایگاه دانش fertilization و لحن مخصوص کودهی استفاده می‌کند. """ import json import logging 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 = ( "بر اساس داده‌های خاک (NPK، pH)، مشخصات گیاه، مرحله رشد و پایگاه دانش کودهی، " "یک توصیه کودهی دقیق بده. " "پاسخ حتماً به فرمت JSON با ساختار زیر باشد:\n" '{\n' ' "plan": {\n' ' "npkRatio": "",\n' ' "amountPerHectare": "",\n' ' "applicationMethod": "",\n' ' "applicationInterval": "",\n' ' "reasoning": ""\n' ' }\n' '}\n' "فقط JSON خروجی بده، بدون توضیح اضافی. " "مقادیر را بر اساس شرایط واقعی خاک و گیاه محاسبه کن." ) 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 "توصیه کودهی برای مزرعه من چیست؟" 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 plant_name and growth_stage: plant_text = build_plant_text(plant_name, growth_stage) if plant_text: extra_parts.append("[اطلاعات گیاه]\n" + plant_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 = { "fertilizer_needed": None, "fertilizer_type": None, "amount_kg_per_hectare": None, "reason": f"خطا در دریافت توصیه: {exc}", "npk_status": None, "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 = { "plan": { "reasoning": raw, }, } result["raw_response"] = raw _complete_audit_log( audit_log, json.dumps(result, ensure_ascii=False, default=str), ) return result