""" ساخت دیتای خاک و هواشناسی کاربر از sensor_data، location_data و weather — Schema-agnostic هر سنسور = یک کاربر. شناسایی با uuid_sensor. مدل‌های Django داخل توابع import می‌شوند تا از AppRegistryNotReady جلوگیری شود. """ from datetime import date from django.db.models import Model EXCLUDE_FIELD_NAMES = {"id", "created_at", "updated_at", "task_id", "recorded_at", "fetched_at"} def _model_to_data_fields(instance: Model, exclude: set[str] | None = None) -> dict: """ استخراج فیلدهای داده از یک instance با استفاده از introspection. تغییرات بعدی در مدل باعث شکستن نمی‌شود. """ exclude = exclude or set() out: dict = {} for f in instance._meta.get_fields(): if f.many_to_many or f.one_to_many or f.one_to_one and f.auto_created: continue if f.name in exclude or f.name in EXCLUDE_FIELD_NAMES: continue if hasattr(f, "related_model") and f.related_model: continue # FK try: val = getattr(instance, f.name, None) if val is not None: out[f.name] = val except Exception: pass return out def build_user_soil_text(sensor_uuid: str) -> str | None: """ ساخت متن قابل embed برای یک سنسور (کاربر). از SensorData → SoilLocation → SoilDepthData خوانده می‌شود. Returns: متن متنی قابل چانک، یا None اگر سنسور یافت نشد. """ from sensor_data.models import SensorData from location_data.models import SoilDepthData try: sensor = SensorData.objects.select_related("location").get( uuid_sensor=sensor_uuid ) except SensorData.DoesNotExist: return None parts: list[str] = [] # شناسه سنسور parts.append(f"سنسور: {sensor.uuid_sensor}") # موقعیت مزرعه loc = sensor.location parts.append( f"موقعیت مزرعه: عرض {loc.latitude}، طول {loc.longitude}" ) # خوانش‌های سنسور (schema-agnostic) sensor_fields = _model_to_data_fields( sensor, exclude={"uuid_sensor", "location_id", "location"} ) if sensor_fields: sensor_lines = [f" {k}: {v}" for k, v in sorted(sensor_fields.items())] parts.append("خوانش‌های سنسور:\n" + "\n".join(sensor_lines)) # داده‌های خاک به تفکیک عمق depths = ( SoilDepthData.objects.filter(soil_location=loc) .order_by("depth_label") .all() ) if depths: depth_parts = [] for d in depths: d_data = _model_to_data_fields( d, exclude={"soil_location", "soil_location_id"} ) if d_data: lines = [f" {k}: {v}" for k, v in sorted(d_data.items())] depth_parts.append(f" عمق {d.depth_label}:\n" + "\n".join(lines)) if depth_parts: parts.append("داده‌های خاک:\n" + "\n".join(depth_parts)) return "\n\n".join(parts) if len(parts) > 1 else None def get_all_sensor_uuids() -> list[str]: """لیست همه uuid_sensor های موجود.""" from sensor_data.models import SensorData return [ str(u) for u in SensorData.objects.values_list("uuid_sensor", flat=True).distinct() ] def build_user_weather_text(sensor_uuid: str) -> str | None: """ ساخت متن هواشناسی قابل embed برای یک سنسور (کاربر). پیش‌بینی ۷ روز آینده از WeatherForecast خوانده می‌شود. Returns: متن فارسی ساختاریافته، یا None اگر داده‌ای نباشد. """ from sensor_data.models import SensorData from weather.models import WeatherForecast try: sensor = SensorData.objects.select_related("location").get( uuid_sensor=sensor_uuid ) except SensorData.DoesNotExist: return None loc = sensor.location forecasts = ( WeatherForecast.objects.filter( location=loc, forecast_date__gte=date.today(), ) .order_by("forecast_date")[:7] ) if not forecasts: return None parts: list[str] = [] parts.append(f"پیش‌بینی هواشناسی سنسور {sensor_uuid} (موقعیت: {loc.latitude}, {loc.longitude})") for fc in forecasts: fc_data = _model_to_data_fields( fc, exclude={"location", "location_id", "forecast_date"} ) lines = [f" {k}: {v}" for k, v in sorted(fc_data.items())] day_text = f" تاریخ {fc.forecast_date}:\n" + "\n".join(lines) parts.append(day_text) return "\n\n".join(parts) if len(parts) > 1 else None def load_user_sources() -> list[tuple[str, str]]: """ بارگذاری منابع دیتای کاربران از DB (خاک + هواشناسی). Returns: [(source_id, content), ...] source_id = user:{uuid} یا weather:{uuid} """ uuids = get_all_sensor_uuids() sources: list[tuple[str, str]] = [] for uid in uuids: text = build_user_soil_text(str(uid)) if text and text.strip(): sources.append((f"user:{uid}", text)) weather_text = build_user_weather_text(str(uid)) if weather_text and weather_text.strip(): sources.append((f"weather:{uid}", weather_text)) return sources def build_plant_text(plant_name: str, growth_stage: str) -> str | None: """ ساخت متن اطلاعات گیاه از جدول Plant برای استفاده در context LLM. """ from plant.models import Plant plant = Plant.objects.filter(name=plant_name).first() if not plant: return None lines = [ f"نام گیاه: {plant.name}", f"مرحله رشد: {growth_stage}", ] if plant.light: lines.append(f"نور مورد نیاز: {plant.light}") if plant.watering: lines.append(f"آبیاری: {plant.watering}") if plant.soil: lines.append(f"خاک مناسب: {plant.soil}") if plant.temperature: lines.append(f"دمای مناسب: {plant.temperature}") if plant.planting_season: lines.append(f"فصل کاشت: {plant.planting_season}") if plant.harvest_time: lines.append(f"زمان برداشت: {plant.harvest_time}") if plant.spacing: lines.append(f"فاصله کاشت: {plant.spacing}") if plant.fertilizer: lines.append(f"کود مناسب: {plant.fertilizer}") return "\n".join(lines) def build_irrigation_method_text(method_name: str) -> str | None: """ ساخت متن مشخصات روش آبیاری از جدول IrrigationMethod برای استفاده در context LLM. """ from irrigation.models import IrrigationMethod method = IrrigationMethod.objects.filter(name=method_name).first() if not method: return None lines = [f"روش آبیاری: {method.name}"] if method.category: lines.append(f"دسته‌بندی: {method.category}") if method.description: lines.append(f"توضیحات: {method.description}") if method.water_efficiency_percent is not None: lines.append(f"راندمان مصرف آب: {method.water_efficiency_percent}%") if method.water_pressure_required: lines.append(f"فشار مورد نیاز: {method.water_pressure_required}") if method.flow_rate: lines.append(f"دبی جریان: {method.flow_rate}") if method.coverage_area: lines.append(f"مساحت پوشش: {method.coverage_area}") if method.soil_type: lines.append(f"نوع خاک مناسب: {method.soil_type}") if method.climate_suitability: lines.append(f"اقلیم مناسب: {method.climate_suitability}") return "\n".join(lines)