2026-02-27 20:06:46 +03:30
|
|
|
"""
|
2026-04-06 23:50:24 +03:30
|
|
|
ساخت دیتای خاک و هواشناسی کاربر از farm_data، location_data و weather — Schema-agnostic
|
|
|
|
|
هر سنسور = یک کاربر. شناسایی با farm_uuid.
|
2026-02-27 20:06:46 +03:30
|
|
|
|
|
|
|
|
مدلهای Django داخل توابع import میشوند تا از AppRegistryNotReady جلوگیری شود.
|
|
|
|
|
"""
|
2026-03-19 22:54:29 +03:30
|
|
|
from datetime import date
|
|
|
|
|
|
2026-02-27 20:06:46 +03:30
|
|
|
from django.db.models import Model
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 22:54:29 +03:30
|
|
|
EXCLUDE_FIELD_NAMES = {"id", "created_at", "updated_at", "task_id", "recorded_at", "fetched_at"}
|
2026-02-27 20:06:46 +03:30
|
|
|
|
|
|
|
|
|
|
|
|
|
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 اگر سنسور یافت نشد.
|
|
|
|
|
"""
|
2026-04-06 23:50:24 +03:30
|
|
|
from farm_data.models import SensorData
|
2026-03-19 22:54:29 +03:30
|
|
|
from location_data.models import SoilDepthData
|
2026-02-27 20:06:46 +03:30
|
|
|
|
|
|
|
|
try:
|
2026-04-06 23:50:24 +03:30
|
|
|
sensor = SensorData.objects.select_related("center_location").get(
|
|
|
|
|
farm_uuid=sensor_uuid
|
2026-02-27 20:06:46 +03:30
|
|
|
)
|
|
|
|
|
except SensorData.DoesNotExist:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
parts: list[str] = []
|
|
|
|
|
|
|
|
|
|
# شناسه سنسور
|
2026-04-06 23:50:24 +03:30
|
|
|
parts.append(f"سنسور: {sensor.farm_uuid}")
|
2026-02-27 20:06:46 +03:30
|
|
|
|
|
|
|
|
# موقعیت مزرعه
|
2026-04-06 23:50:24 +03:30
|
|
|
loc = sensor.center_location
|
2026-02-27 20:06:46 +03:30
|
|
|
parts.append(
|
|
|
|
|
f"موقعیت مزرعه: عرض {loc.latitude}، طول {loc.longitude}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# خوانشهای سنسور (schema-agnostic)
|
|
|
|
|
sensor_fields = _model_to_data_fields(
|
2026-04-06 23:50:24 +03:30
|
|
|
sensor, exclude={"farm_uuid", "center_location_id", "center_location", "location"}
|
2026-02-27 20:06:46 +03:30
|
|
|
)
|
|
|
|
|
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))
|
|
|
|
|
|
2026-03-19 22:54:29 +03:30
|
|
|
return "\n\n".join(parts) if len(parts) > 1 else None
|
2026-02-27 20:06:46 +03:30
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_all_sensor_uuids() -> list[str]:
|
2026-04-06 23:50:24 +03:30
|
|
|
"""لیست همه farm_uuid های موجود."""
|
|
|
|
|
from farm_data.models import SensorData
|
2026-02-27 20:06:46 +03:30
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
str(u) for u in
|
2026-04-06 23:50:24 +03:30
|
|
|
SensorData.objects.values_list("farm_uuid", flat=True).distinct()
|
2026-02-27 20:06:46 +03:30
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 22:54:29 +03:30
|
|
|
def build_user_weather_text(sensor_uuid: str) -> str | None:
|
|
|
|
|
"""
|
|
|
|
|
ساخت متن هواشناسی قابل embed برای یک سنسور (کاربر).
|
|
|
|
|
پیشبینی ۷ روز آینده از WeatherForecast خوانده میشود.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
متن فارسی ساختاریافته، یا None اگر دادهای نباشد.
|
|
|
|
|
"""
|
2026-04-06 23:50:24 +03:30
|
|
|
from farm_data.models import SensorData
|
2026-03-19 22:54:29 +03:30
|
|
|
from weather.models import WeatherForecast
|
|
|
|
|
|
|
|
|
|
try:
|
2026-04-06 23:50:24 +03:30
|
|
|
sensor = SensorData.objects.select_related("center_location").get(
|
|
|
|
|
farm_uuid=sensor_uuid
|
2026-03-19 22:54:29 +03:30
|
|
|
)
|
|
|
|
|
except SensorData.DoesNotExist:
|
|
|
|
|
return None
|
|
|
|
|
|
2026-04-06 23:50:24 +03:30
|
|
|
loc = sensor.center_location
|
2026-03-19 22:54:29 +03:30
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-02-27 20:06:46 +03:30
|
|
|
def load_user_sources() -> list[tuple[str, str]]:
|
|
|
|
|
"""
|
2026-03-19 22:54:29 +03:30
|
|
|
بارگذاری منابع دیتای کاربران از DB (خاک + هواشناسی).
|
2026-02-27 20:06:46 +03:30
|
|
|
Returns: [(source_id, content), ...]
|
2026-03-19 22:54:29 +03:30
|
|
|
source_id = user:{uuid} یا weather:{uuid}
|
2026-02-27 20:06:46 +03:30
|
|
|
"""
|
|
|
|
|
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))
|
2026-03-19 22:54:29 +03:30
|
|
|
weather_text = build_user_weather_text(str(uid))
|
|
|
|
|
if weather_text and weather_text.strip():
|
|
|
|
|
sources.append((f"weather:{uid}", weather_text))
|
2026-02-27 20:06:46 +03:30
|
|
|
return sources
|
2026-03-19 22:54:29 +03:30
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_plant_text(plant_name: str, growth_stage: str) -> str | None:
|
|
|
|
|
"""
|
2026-05-05 01:46:10 +03:30
|
|
|
ساخت متن اطلاعات گیاه از snapshotهای `farm_data` برای استفاده در context LLM.
|
2026-03-19 22:54:29 +03:30
|
|
|
"""
|
2026-05-05 01:46:10 +03:30
|
|
|
from farm_data.models import PlantCatalogSnapshot
|
|
|
|
|
from farm_data.services import build_plant_text_from_snapshot
|
2026-03-19 22:54:29 +03:30
|
|
|
|
2026-05-05 01:46:10 +03:30
|
|
|
plant = PlantCatalogSnapshot.objects.filter(name=plant_name).first()
|
2026-03-19 22:54:29 +03:30
|
|
|
if not plant:
|
|
|
|
|
return None
|
|
|
|
|
|
2026-05-05 01:46:10 +03:30
|
|
|
return build_plant_text_from_snapshot(plant, growth_stage)
|
2026-03-19 22:54:29 +03:30
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|